From ee51a791ae52b63088ac0de7bd2702500d205298 Mon Sep 17 00:00:00 2001 From: JohnZastrow Date: Sun, 17 May 2026 19:49:06 -0400 Subject: [PATCH 1/2] Add Markdown and PDF export formats Adds .md and .pdf as export options alongside the existing .csv/.json. Both are human-readable, export-only outputs (import still round-trips csv/json only). PDF is rendered via QTextDocument + QPrinter and Markdown is generated as plain text, so no new dependencies are needed. - UI: add .md/.pdf to the format combo box - export_plugins(): new .md/.pdf branches - add build_report_rows()/build_markdown()/build_html() helpers - set_filter(): cover *.md and *.pdf - metadata: bump to 0.3.0 + changelog Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin_exporter/metadata.txt | 6 +- plugin_exporter/plugin_exporter.py | 122 +++++++++++++++++- .../plugin_exporter_dialog_base.ui | 10 ++ 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/plugin_exporter/metadata.txt b/plugin_exporter/metadata.txt index 28beb2b..08aa060 100644 --- a/plugin_exporter/metadata.txt +++ b/plugin_exporter/metadata.txt @@ -7,11 +7,11 @@ name=Plugin Exporter qgisMinimumVersion=3.20 qgisMaximumVersion=4.99 description=A QGIS plugin for exporting plugins -version=0.2.2 +version=0.3.0 author=Francis Lapointe email=francis.lapointe5@usherbrooke.ca -about=Plugin Exporter is a QGIS plugin that can export installed plugins into a .csv or .json file. The user can export all the installed plugins or select the plugins they want to export. Plugin Exporter can also use the generated file to install the plugins back in QGIS. Third party repositories are also supported as of v0.2.0. +about=Plugin Exporter is a QGIS plugin that can export installed plugins into a .csv, .json, .md or .pdf file. The user can export all the installed plugins or select the plugins they want to export. Plugin Exporter can also use a .csv or .json file to install the plugins back in QGIS. Third party repositories are also supported as of v0.2.0. tracker=https://github.com/Scriptbash/PluginExporter/issues repository=https://github.com/Scriptbash/PluginExporter @@ -24,6 +24,8 @@ supportsQt6=True hasProcessingProvider=no # Uncomment the following line and add your changelog: changelog= + v0.3.0 + - Add Markdown (.md) and PDF (.pdf) export formats (export only) v0.2.2 - Migrate to pyQt6 v0.2.1 diff --git a/plugin_exporter/plugin_exporter.py b/plugin_exporter/plugin_exporter.py index 3a9e337..ea51b57 100644 --- a/plugin_exporter/plugin_exporter.py +++ b/plugin_exporter/plugin_exporter.py @@ -23,7 +23,8 @@ """ from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication -from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtGui import QIcon, QTextDocument +from qgis.PyQt.QtPrintSupport import QPrinter from qgis.PyQt.QtWidgets import QAction, QLabel, QCheckBox from qgis.core import Qgis, QgsSettings from pyplugin_installer.installer_data import repositories @@ -32,7 +33,9 @@ import os.path import csv import json +import html import pathlib +from datetime import datetime # Initialize Qt resources from file resources.py from .resources import * @@ -371,6 +374,22 @@ def export_plugins(self): self.iface.messageBar().pushSuccess( "Success", "Selected plugins were exported successfully." ) + elif file_format == ".md": + with open(output_file, "w", encoding="utf8") as file: + file.write(self.build_markdown(plugin_list, repos)) + self.iface.messageBar().pushSuccess( + "Success", "Selected plugins were exported successfully." + ) + elif file_format == ".pdf": + document = QTextDocument() + document.setHtml(self.build_html(plugin_list, repos)) + printer = QPrinter() + printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) + printer.setOutputFileName(output_file) + document.print(printer) + self.iface.messageBar().pushSuccess( + "Success", "Selected plugins were exported successfully." + ) except IsADirectoryError: self.iface.messageBar().pushMessage( "Error", @@ -485,6 +504,98 @@ def add_repository(self, repo_info): "Success", repo_name + " was added to the repositories." ) + # Splits the third party repositories and the selected plugins into rows. + # Used by the human readable exports (.md and .pdf), which are export only: + # they cannot be imported back since they don't preserve every metadata field. + def build_report_rows(self, plugin_list, repos): + repo_rows = [] + if repos: + for name, value in repos.items(): + if name == "QGIS Official Plugin Repository": + continue + repo_rows.append([name, value["url"]]) + + plugin_rows = [] + for plugin in plugin_list: + plugin_rows.append( + [ + plugin.get("name", ""), + plugin.get("version_installed", ""), + plugin.get("author_name", ""), + plugin.get("id", ""), + plugin.get("zip_repository", ""), + ] + ) + return repo_rows, plugin_rows + + # Builds a Markdown document listing the selected plugins + def build_markdown(self, plugin_list, repos): + repo_rows, plugin_rows = self.build_report_rows(plugin_list, repos) + generated = datetime.now().strftime("%Y-%m-%d %H:%M") + + def cell(text): + return str(text).replace("|", "\\|").replace("\n", " ") + + lines = [ + "# QGIS Plugins Export", + "", + "_Generated by Plugin Exporter on " + generated + "_", + "", + ] + if repo_rows: + lines += ["## Third party repositories", "", "| Name | URL |", "| --- | --- |"] + for row in repo_rows: + lines.append("| " + " | ".join(cell(c) for c in row) + " |") + lines.append("") + lines += [ + "## Plugins", + "", + "| Name | Version | Author | ID | Repository |", + "| --- | --- | --- | --- | --- |", + ] + for row in plugin_rows: + lines.append("| " + " | ".join(cell(c) for c in row) + " |") + lines.append("") + return "\n".join(lines) + + # Builds an HTML document used to render the PDF export + def build_html(self, plugin_list, repos): + repo_rows, plugin_rows = self.build_report_rows(plugin_list, repos) + generated = datetime.now().strftime("%Y-%m-%d %H:%M") + + def table(headers, rows): + html_rows = [ + "" + + "".join("" + html.escape(h) + "" for h in headers) + + "" + ] + for row in rows: + html_rows.append( + "" + + "".join("" + html.escape(str(c)) + "" for c in row) + + "" + ) + return ( + '' + + "".join(html_rows) + + "
" + ) + + parts = [ + "", + "

QGIS Plugins Export

", + "

Generated by Plugin Exporter on " + html.escape(generated) + "

", + ] + if repo_rows: + parts.append("

Third party repositories

") + parts.append(table(["Name", "URL"], repo_rows)) + parts.append("

Plugins

") + parts.append( + table(["Name", "Version", "Author", "ID", "Repository"], plugin_rows) + ) + parts.append("") + return "".join(parts) + # Disables and enables widgets def toggle_widget(self): if self.dlg.rd_import.isChecked(): @@ -502,7 +613,12 @@ def toggle_widget(self): # Sets the file extension filter for the QgsFileWidget def set_filter(self): - if self.dlg.combo_file_format.currentText() == ".json": + current_format = self.dlg.combo_file_format.currentText() + if current_format == ".json": self.dlg.file_output_export.setFilter("*.json") - elif self.dlg.combo_file_format.currentText() == ".csv": + elif current_format == ".csv": self.dlg.file_output_export.setFilter("*.csv") + elif current_format == ".md": + self.dlg.file_output_export.setFilter("*.md") + elif current_format == ".pdf": + self.dlg.file_output_export.setFilter("*.pdf") diff --git a/plugin_exporter/plugin_exporter_dialog_base.ui b/plugin_exporter/plugin_exporter_dialog_base.ui index 441f353..029732b 100644 --- a/plugin_exporter/plugin_exporter_dialog_base.ui +++ b/plugin_exporter/plugin_exporter_dialog_base.ui @@ -32,6 +32,16 @@ .json + + + .md + + + + + .pdf + + From 1ca6c914fdf5fddd31c859e818104584995c64f8 Mon Sep 17 00:00:00 2001 From: JohnZastrow Date: Sun, 17 May 2026 20:40:07 -0400 Subject: [PATCH 2/2] Include all plugin metadata fields in Markdown and PDF exports The JSON export dumps each plugin's full metadata dict, but Markdown and PDF only emitted a fixed 5-column subset. They now render every field the plugin manager exposes per plugin (one Field/Value table per plugin), matching the JSON export's completeness. Co-Authored-By: Claude Opus 4.7 (1M context) --- plugin_exporter/plugin_exporter.py | 64 +++++++++++++++--------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/plugin_exporter/plugin_exporter.py b/plugin_exporter/plugin_exporter.py index ea51b57..5559be0 100644 --- a/plugin_exporter/plugin_exporter.py +++ b/plugin_exporter/plugin_exporter.py @@ -504,10 +504,12 @@ def add_repository(self, repo_info): "Success", repo_name + " was added to the repositories." ) - # Splits the third party repositories and the selected plugins into rows. - # Used by the human readable exports (.md and .pdf), which are export only: - # they cannot be imported back since they don't preserve every metadata field. - def build_report_rows(self, plugin_list, repos): + # Collects the third party repositories and every metadata field of each + # selected plugin. Used by the human readable exports (.md and .pdf), which + # are export only: they cannot be imported back. Each plugin keeps all of + # the fields the plugin manager exposes for it (same data as the .json + # export), not just a fixed subset. + def build_report_data(self, plugin_list, repos): repo_rows = [] if repos: for name, value in repos.items(): @@ -515,22 +517,17 @@ def build_report_rows(self, plugin_list, repos): continue repo_rows.append([name, value["url"]]) - plugin_rows = [] + plugins = [] for plugin in plugin_list: - plugin_rows.append( - [ - plugin.get("name", ""), - plugin.get("version_installed", ""), - plugin.get("author_name", ""), - plugin.get("id", ""), - plugin.get("zip_repository", ""), - ] - ) - return repo_rows, plugin_rows + # Preserve the metadata key order; fall back to the id for the title + title = plugin.get("name") or plugin.get("id") or "Unknown plugin" + fields = [(str(k), "" if v is None else str(v)) for k, v in plugin.items()] + plugins.append((title, fields)) + return repo_rows, plugins # Builds a Markdown document listing the selected plugins def build_markdown(self, plugin_list, repos): - repo_rows, plugin_rows = self.build_report_rows(plugin_list, repos) + repo_rows, plugins = self.build_report_data(plugin_list, repos) generated = datetime.now().strftime("%Y-%m-%d %H:%M") def cell(text): @@ -547,22 +544,27 @@ def cell(text): for row in repo_rows: lines.append("| " + " | ".join(cell(c) for c in row) + " |") lines.append("") - lines += [ - "## Plugins", - "", - "| Name | Version | Author | ID | Repository |", - "| --- | --- | --- | --- | --- |", - ] - for row in plugin_rows: - lines.append("| " + " | ".join(cell(c) for c in row) + " |") - lines.append("") + lines += ["## Plugins", ""] + for title, fields in plugins: + lines += [ + "### " + cell(title), + "", + "| Field | Value |", + "| --- | --- |", + ] + for key, value in fields: + lines.append("| " + cell(key) + " | " + cell(value) + " |") + lines.append("") return "\n".join(lines) # Builds an HTML document used to render the PDF export def build_html(self, plugin_list, repos): - repo_rows, plugin_rows = self.build_report_rows(plugin_list, repos) + repo_rows, plugins = self.build_report_data(plugin_list, repos) generated = datetime.now().strftime("%Y-%m-%d %H:%M") + def esc(text): + return html.escape(str(text)).replace("\n", "
") + def table(headers, rows): html_rows = [ "" @@ -571,9 +573,7 @@ def table(headers, rows): ] for row in rows: html_rows.append( - "" - + "".join("" + html.escape(str(c)) + "" for c in row) - + "" + "" + "".join("" + esc(c) + "" for c in row) + "" ) return ( '' @@ -590,9 +590,9 @@ def table(headers, rows): parts.append("

Third party repositories

") parts.append(table(["Name", "URL"], repo_rows)) parts.append("

Plugins

") - parts.append( - table(["Name", "Version", "Author", "ID", "Repository"], plugin_rows) - ) + for title, fields in plugins: + parts.append("

" + esc(title) + "

") + parts.append(table(["Field", "Value"], fields)) parts.append("") return "".join(parts)