From ea04c29d2937c81a19fcb501ccd9efd42f7f4e56 Mon Sep 17 00:00:00 2001 From: Tobia Rigon <42972324+TobiaRigon@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:01:48 +0200 Subject: [PATCH] Aggiunge supporto HTML e PDF --- README.md | 6 +++--- promptpack/cli.py | 6 +++++- promptpack/gui/preview.py | 16 ++++++++++++-- promptpack/gui/settings_dialog.py | 10 ++++++--- promptpack/utils.py | 36 +++++++++++++++++++++++++++++-- 5 files changed, 63 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7e572dd..3853610 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - Live preview window to see the generated output before exporting - Default filters for file extensions and folders (e.g., skip `.env`, `node_modules`, `.git`, etc.) - Saves and loads user settings to/from a JSON file -- Choose export format (TXT, Markdown or JSON) and optionally include code blocks and headings +- Choose export format (TXT, Markdown, JSON, HTML or PDF) and optionally include code blocks and headings - Optionally export only the file tree without contents - Switch between dark and light mode, with buttons and fields adopting dark colors when the theme is set to "dark" - Files listed in a `.gitignore` file are automatically deselected @@ -59,7 +59,7 @@ The file `promptpack.py` simply launches the application. 2. **Select Files**: Opens an expandable tree of all folders and files. You can include/exclude each item via checkboxes. - Default selections are based on the current settings. 3. **Settings**: Define default allowed extensions, excluded folders and files. Also choose: - - Export format (txt, md, json) + - Export format (txt, md, json, html, pdf) - Include file headings - Use code blocks for each file (Markdown only) - Export only the file tree @@ -138,7 +138,7 @@ python -m promptpack.cli ./input ./out --no-heading --no-code-block | ----------------- | ------------------------------------------------------------------- | | `source` | Source folder to analyze | | `dest` | Destination folder for the generated output | -| `--format` | Export format: `txt`, `md`, `json` | +| `--format` | Export format: `txt`, `md`, `json`, `html`, `pdf` | | `--tree-only` | Export only the folder structure, without file contents | | `--no-heading` | Do not include headings for each file | | `--no-code-block` | Do not wrap contents in code blocks (Markdown only) | diff --git a/promptpack/cli.py b/promptpack/cli.py index 64db4da..d48a2c1 100644 --- a/promptpack/cli.py +++ b/promptpack/cli.py @@ -29,7 +29,11 @@ def main(): parser = argparse.ArgumentParser(description="PromptPack CLI") parser.add_argument("source", help="cartella sorgente") parser.add_argument("dest", help="cartella di destinazione") - parser.add_argument("--format", choices=["txt", "md", "json"], dest="format") + parser.add_argument( + "--format", + choices=["txt", "md", "json", "html", "pdf"], + dest="format", + ) parser.add_argument("--tree-only", action="store_true") parser.add_argument("--no-heading", action="store_true") parser.add_argument("--no-code-block", action="store_true") diff --git a/promptpack/gui/preview.py b/promptpack/gui/preview.py index 1d757f6..a0d2995 100644 --- a/promptpack/gui/preview.py +++ b/promptpack/gui/preview.py @@ -171,7 +171,10 @@ def generate_preview_lines(app, start_folder, included_files, warn_on_limit=True for path in sorted(included_files): rel_path = path.relative_to(start_folder) if app.tree_only.get(): - line = f"{rel_path.as_posix()}\n" + if app.export_format.get() == "html": + line = f"

{rel_path.as_posix()}

\n" + else: + line = f"{rel_path.as_posix()}\n" tokens = estimate_token_count(line) if token_count + tokens > max_tokens: break @@ -185,10 +188,19 @@ def generate_preview_lines(app, start_folder, included_files, warn_on_limit=True content = sanitize_sensitive_data(content) chunk = [] if app.include_heading.get(): - chunk.append(f"## {rel_path.as_posix()}\n") + if app.export_format.get() == "html": + chunk.append(f"

{rel_path.as_posix()}

\n") + else: + chunk.append(f"## {rel_path.as_posix()}\n") if app.export_format.get() == "md" and app.use_code_block.get(): lang = LANG_MAP.get(path.suffix, '') chunk.append(f"```{lang}\n{content}\n```\n") + elif app.export_format.get() == "html": + if app.use_code_block.get(): + lang = LANG_MAP.get(path.suffix, '') + chunk.append(f"
" + content + "
\n") + else: + chunk.append(f"
{content}
\n") else: chunk.append(f"{content}\n") text = ''.join(chunk) diff --git a/promptpack/gui/settings_dialog.py b/promptpack/gui/settings_dialog.py index 2983b02..7093204 100644 --- a/promptpack/gui/settings_dialog.py +++ b/promptpack/gui/settings_dialog.py @@ -93,9 +93,13 @@ def prompt_list(title, key): ttk.Label(scrollable, text=app.t("output_opts"), style="Heading.TLabel").pack(padx=10, pady=(20, 5)) ttk.Label(scrollable, text=app.t("export_format")).pack(pady=(5, 0)) - ttk.Radiobutton(scrollable, text="TXT", variable=app.export_format, value="txt").pack(pady=2) - ttk.Radiobutton(scrollable, text="Markdown", variable=app.export_format, value="md").pack(pady=2) - ttk.Radiobutton(scrollable, text="JSON", variable=app.export_format, value="json").pack(pady=2) + format_options = ["txt", "md", "json", "html", "pdf"] + format_menu = ttk.OptionMenu(scrollable, app.export_format, app.export_format.get(), *format_options) + format_menu.pack(pady=5) + menu_widget = format_menu["menu"] + bg = "#2d2d2d" if app.theme.get() == "dark" else "#ffffff" + fg = "#dcdcdc" if app.theme.get() == "dark" else "#000000" + menu_widget.configure(bg=bg, fg=fg, activebackground=bg, activeforeground=fg) ttk.Checkbutton(scrollable, text=app.t("include_headings"), variable=app.include_heading).pack(pady=5) ttk.Checkbutton(scrollable, text=app.t("use_code"), variable=app.use_code_block).pack(pady=5) ttk.Checkbutton(scrollable, text=app.t("tree_only"), variable=app.tree_only).pack(pady=5) diff --git a/promptpack/utils.py b/promptpack/utils.py index d176e8a..52ae15b 100644 --- a/promptpack/utils.py +++ b/promptpack/utils.py @@ -93,6 +93,10 @@ def generate_output( lines = [] header = "" token_count = 0 + elif export_format == "html": + lines = [f"

Project: {project_name} - {date_str}

\n"] + header = "" + token_count = estimate_token_count(project_name) else: lines = [] header = f"Project: {project_name} - {date_str}\n\n" @@ -104,6 +108,22 @@ def _flush(): if export_format == "json": output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.json" output_file.write_text(json.dumps(data, indent=2), encoding="utf-8") + elif export_format == "html": + output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.html" + html = "" + "".join(lines) + "" + output_file.write_text(html, encoding="utf-8") + elif export_format == "pdf": + try: + from fpdf import FPDF + except ImportError as exc: + raise RuntimeError("fpdf library required for PDF export") from exc + output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.pdf" + pdf = FPDF() + pdf.add_page() + pdf.set_font("Courier", size=12) + for line in lines: + pdf.multi_cell(0, 10, line) + pdf.output(str(output_file)) else: suffix = "md" if export_format == "md" else "txt" output_file = Path(dest_folder) / f"{project_name}-{date_str}-part{part}.{suffix}" @@ -126,7 +146,10 @@ def _flush(): data["files"].append(line_data) token_count += block_tokens else: - line = f"{rel_path.as_posix()}\n" + if export_format == "html": + line = f"

{rel_path.as_posix()}

\n" + else: + line = f"{rel_path.as_posix()}\n" block_tokens = estimate_token_count(line) if token_count + block_tokens > max_tokens: output_files.append(_flush()) @@ -156,10 +179,19 @@ def _flush(): else: new_lines = [] if include_heading: - new_lines.append(f"## {rel_path.as_posix()}\n") + if export_format == "html": + new_lines.append(f"

{rel_path.as_posix()}

\n") + else: + new_lines.append(f"## {rel_path.as_posix()}\n") if export_format == "md" and use_code_block: lang = LANG_MAP.get(path.suffix, '') new_lines.append(f"```{lang}\n{content}\n```\n\n") + elif export_format == "html": + if use_code_block: + lang = LANG_MAP.get(path.suffix, '') + new_lines.append(f"
" + content + "
\n\n") + else: + new_lines.append(f"
{content}
\n\n") else: new_lines.append(f"{content}\n\n") block = ''.join(new_lines)