codeannex 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {codeannex-0.4.2 → codeannex-0.4.4}/PKG-INFO +6 -2
  2. {codeannex-0.4.2 → codeannex-0.4.4}/README.md +5 -1
  3. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/__main__.py +7 -0
  4. codeannex-0.4.4/codeannex/interface/cli.py +188 -0
  5. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/fonts.py +7 -1
  6. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/text_utils.py +29 -14
  7. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/PKG-INFO +6 -2
  8. {codeannex-0.4.2 → codeannex-0.4.4}/pyproject.toml +1 -1
  9. codeannex-0.4.2/codeannex/interface/cli.py +0 -161
  10. {codeannex-0.4.2 → codeannex-0.4.4}/LICENSE +0 -0
  11. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/__init__.py +0 -0
  12. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/__init__.py +0 -0
  13. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/config.py +0 -0
  14. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/pdf_builder.py +0 -0
  15. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/interface/__init__.py +0 -0
  16. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/__init__.py +0 -0
  17. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/file_utils.py +0 -0
  18. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/git_utils.py +0 -0
  19. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/__init__.py +0 -0
  20. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/highlight.py +0 -0
  21. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/SOURCES.txt +0 -0
  22. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/dependency_links.txt +0 -0
  23. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/entry_points.txt +0 -0
  24. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/requires.txt +0 -0
  25. {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/top_level.txt +0 -0
  26. {codeannex-0.4.2 → codeannex-0.4.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeannex
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Generates a professional PDF source code annex with Smart Index, Images and Emoji support.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/tanhleno/codeannex
@@ -36,6 +36,8 @@ Generates a professional PDF annex from a project's source code — featuring sy
36
36
  ## 🚀 Key Features
37
37
 
38
38
  - **Interactive Wizard 2.0** — Step-by-step configuration with smart sections (Project, Style, Typography, Layout, Filters) and explicit default prompts.
39
+ - **Smart Emoji Support** — Automatic discovery of standard emoji fonts (**Segoe UI Emoji** on Windows, **DejaVu Sans** on Linux).
40
+ - **Graceful Emoji Fallback** — If no emoji font is found, it uses official Unicode names (e.g., `[ROCKET]`) for perfect readability.
39
41
  - **Git Version Tracking** — Automatically detects **Repository URL**, **Branch**, and **Commit SHA**. Smart root detection avoids Git metadata on subdirectories.
40
42
  - **Smart SVG Rendering** — Files are rendered as both a high-quality image and XML code. Entries are intelligently deduplicated in the summary.
41
43
  - **Improved Document Structure** — Subdirectories and their contents are listed before root files for better organization.
@@ -58,7 +60,7 @@ For full SVG support (required for crisp line numbers and SVG image rendering):
58
60
  pipx install "codeannex[svg]"
59
61
  ```
60
62
 
61
- *Alternatively, you can use standard pip:* `pip install codeannex`
63
+ *Note: For best emoji rendering on Linux, you may want to install `fonts-noto-color-emoji` or `ttf-dejavu`.*
62
64
 
63
65
  ## 📖 Usage
64
66
 
@@ -102,6 +104,8 @@ Default output filename is `{project_name}_code_annex.pdf`.
102
104
  ### Fonts
103
105
  - `--font-path PATH` — Additional directory to search for `.ttf`/`.otf` files.
104
106
  - `--title-font` / `--normal-font` / `--mono-font` — System font names.
107
+ - `--emoji-font NAME` — Custom font for emojis.
108
+ - `--emoji-description` — Force textual descriptions (e.g., `[GRINNING FACE]`) instead of glyphs.
105
109
 
106
110
  ## 🧪 Testing
107
111
 
@@ -5,6 +5,8 @@ Generates a professional PDF annex from a project's source code — featuring sy
5
5
  ## 🚀 Key Features
6
6
 
7
7
  - **Interactive Wizard 2.0** — Step-by-step configuration with smart sections (Project, Style, Typography, Layout, Filters) and explicit default prompts.
8
+ - **Smart Emoji Support** — Automatic discovery of standard emoji fonts (**Segoe UI Emoji** on Windows, **DejaVu Sans** on Linux).
9
+ - **Graceful Emoji Fallback** — If no emoji font is found, it uses official Unicode names (e.g., `[ROCKET]`) for perfect readability.
8
10
  - **Git Version Tracking** — Automatically detects **Repository URL**, **Branch**, and **Commit SHA**. Smart root detection avoids Git metadata on subdirectories.
9
11
  - **Smart SVG Rendering** — Files are rendered as both a high-quality image and XML code. Entries are intelligently deduplicated in the summary.
10
12
  - **Improved Document Structure** — Subdirectories and their contents are listed before root files for better organization.
@@ -27,7 +29,7 @@ For full SVG support (required for crisp line numbers and SVG image rendering):
27
29
  pipx install "codeannex[svg]"
28
30
  ```
29
31
 
30
- *Alternatively, you can use standard pip:* `pip install codeannex`
32
+ *Note: For best emoji rendering on Linux, you may want to install `fonts-noto-color-emoji` or `ttf-dejavu`.*
31
33
 
32
34
  ## 📖 Usage
33
35
 
@@ -71,6 +73,8 @@ Default output filename is `{project_name}_code_annex.pdf`.
71
73
  ### Fonts
72
74
  - `--font-path PATH` — Additional directory to search for `.ttf`/`.otf` files.
73
75
  - `--title-font` / `--normal-font` / `--mono-font` — System font names.
76
+ - `--emoji-font NAME` — Custom font for emojis.
77
+ - `--emoji-description` — Force textual descriptions (e.g., `[GRINNING FACE]`) instead of glyphs.
74
78
 
75
79
  ## 🧪 Testing
76
80
 
@@ -36,6 +36,13 @@ def check_emoji_font_style():
36
36
 
37
37
 
38
38
  def main():
39
+ try:
40
+ _main_impl()
41
+ except KeyboardInterrupt:
42
+ print(f"\n\n\033[33m⚠️ Operation aborted by user (Ctrl+C).\033[0m")
43
+ sys.exit(0)
44
+
45
+ def _main_impl():
39
46
  args, unknown = parse_args()
40
47
  is_interactive = len(sys.argv) <= 2 and not args.no_input
41
48
  if is_interactive: run_interactive_wizard(args)
@@ -0,0 +1,188 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from ..core.config import PDFConfig
4
+ from ..io.git_utils import get_git_info, get_git_remotes
5
+ from ..renderer.fonts import register_emoji_font
6
+
7
+ # ANSI Colors for a better CLI experience
8
+ BOLD = "\033[1m"
9
+ BLUE = "\033[34m"
10
+ CYAN = "\033[36m"
11
+ GREEN = "\033[32m"
12
+ YELLOW = "\033[33m"
13
+ RESET = "\033[0m"
14
+
15
+ def parse_args():
16
+ parser = argparse.ArgumentParser(description="Generates a PDF code annex with Smart Index and Images.")
17
+ parser.add_argument("dir", nargs="?", default=".", help="Project directory")
18
+ parser.add_argument("-o", "--output", default=None, help="Output PDF filename")
19
+ parser.add_argument("-n", "--name", default=None, help="Project name")
20
+ parser.add_argument("--no-input", action="store_true", help="Disable interactive mode")
21
+ parser.add_argument("--cover-title", default="TECHNICAL ANNEX", help="Cover title")
22
+ parser.add_argument("--margin", type=float, default=None, help="General margin (cm)")
23
+ parser.add_argument("--margin-left", type=float, default=None, help="Left margin (cm)")
24
+ parser.add_argument("--margin-right", type=float, default=None, help="Right margin (cm)")
25
+ parser.add_argument("--margin-top", type=float, default=None, help="Top margin (cm)")
26
+ parser.add_argument("--margin-bottom", type=float, default=None, help="Bottom margin (cm)")
27
+ parser.add_argument("--start-page", type=int, default=1, help="Start page")
28
+ parser.add_argument("--show-project", action="store_true", help="Show project in footer")
29
+ parser.add_argument("--repo-url", default=None, help="Repo URL")
30
+ parser.add_argument("--branch", default=None, help="Branch name")
31
+ parser.add_argument("--repo-label", default="Repository Name: ", help="Label for repo")
32
+ parser.add_argument("--project-label", default="Project: ", help="Label for project")
33
+ parser.add_argument("--page-width", type=float, default=None, help="Width (mm)")
34
+ parser.add_argument("--page-height", type=float, default=None, help="Height (mm)")
35
+ parser.add_argument("--include", action="append", default=None, help="Include pattern")
36
+ parser.add_argument("--exclude", action="append", default=None, help="Exclude pattern")
37
+ parser.add_argument("--no-git", action="store_true", help="No Git")
38
+ parser.add_argument("--file-part-format", default="(part {current}/{total})", help="Part format")
39
+ parser.add_argument("--summary-title", default="Summary / File Index", help="Summary title")
40
+ parser.add_argument("--cover-subtitle", default="Source Code Documentation", help="Cover subtitle")
41
+ parser.add_argument("--page-number-size", type=int, default=8, help="Page number size")
42
+ parser.add_argument("--page-number-format", default="{n}", help="Page number format")
43
+ parser.add_argument("--no-page-numbers", action="store_true", help="No page numbers")
44
+ parser.add_argument("--page-bg-color", default="#ffffff", help="Page BG color")
45
+ parser.add_argument("--normal-font", default=None, help="Normal font")
46
+ parser.add_argument("--normal-size", type=int, default=10, help="Normal size")
47
+ parser.add_argument("--normal-color", default="#4c4f69", help="Normal color")
48
+ parser.add_argument("--bold-font", default=None, help="Bold font")
49
+ parser.add_argument("--title-font", default=None, help="Title font")
50
+ parser.add_argument("--title-size", type=int, default=28, help="Title size")
51
+ parser.add_argument("--title-color", default="#1e1e2e", help="Title color")
52
+ parser.add_argument("--subtitle-font", default=None, help="Subtitle font")
53
+ parser.add_argument("--subtitle-size", type=int, default=18, help="Subtitle size")
54
+ parser.add_argument("--subtitle-color", default=None, help="Subtitle color")
55
+ parser.add_argument("--primary-color", default="#1e66f5", help="Primary color")
56
+ parser.add_argument("--code-size", type=int, default=10, help="Code size")
57
+ parser.add_argument("--code-bg", default="#1e1e2e", help="Code background")
58
+ parser.add_argument("--mono-font", default=None, help="Mono font")
59
+ parser.add_argument("--emoji-font", default=None, help="Emoji font")
60
+ parser.add_argument("--font-path", action="append", default=None, help="Additional directory to search for fonts")
61
+ parser.add_argument("--emoji-description", action="store_true", help="Emoji desc")
62
+ parser.add_argument("--check-emoji-font", action="store_true", help="Check emoji")
63
+ return parser.parse_known_args()
64
+
65
+ def _print_header(step, total, title, details):
66
+ print(f"\n{BLUE}Step {step}/{total}{RESET} {BOLD}--- {title} ---{RESET}")
67
+ if details:
68
+ print(f" {CYAN}i{RESET} {details}")
69
+
70
+ def _ask_section(step, total, title, details, default_yes=False):
71
+ _print_header(step, total, title, details)
72
+ prompt = f"{GREEN}Y{RESET}/n" if default_yes else f"y/{GREEN}N{RESET}"
73
+ res = input(f" Customize this section? ({prompt}): ").strip().lower()
74
+ if not res: return default_yes
75
+ return res == 'y'
76
+
77
+ def _input_field(label, default):
78
+ prompt = f" {label} {CYAN}[{default if default is not None else ''}]{RESET}: "
79
+ return input(prompt).strip()
80
+
81
+ def run_interactive_wizard(args):
82
+ try:
83
+ print(f"\n{BOLD}{BLUE}✨ Welcome to codeannex Interactive Wizard! ✨{RESET}")
84
+ print(f"{YELLOW}Uppercase letters in prompts indicate the [default] action on Enter.{RESET}\n")
85
+
86
+ root = Path(args.dir).resolve()
87
+ git_url, git_branch, _ = get_git_info(root)
88
+ remotes = get_git_remotes(root)
89
+ total_steps = 6
90
+
91
+ # 1. Project Identity
92
+ _print_header(1, total_steps, "Project Identity", "Basic identification of your document")
93
+ args.name = _input_field("Project Name", args.name or root.name) or (args.name or root.name)
94
+
95
+ # 2. Repository Info
96
+ has_git = bool(remotes or git_branch)
97
+ if has_git:
98
+ if len(remotes) > 1:
99
+ _print_header(2, total_steps, "Repository Info", "Multiple Git remotes detected")
100
+ remote_names = list(remotes.keys())
101
+
102
+ # Identify default index
103
+ default_idx = 1
104
+ if "origin" in remote_names:
105
+ default_idx = remote_names.index("origin") + 1
106
+
107
+ for i, name in enumerate(remote_names, 1):
108
+ marker = f"{GREEN}*{RESET}" if i == default_idx else " "
109
+ print(f" {i}. {marker} {name} ({remotes[name]})")
110
+ print(f" 0. [ Manual / None ]")
111
+
112
+ choice = _input_field(f"Select remote (1-{len(remote_names)}) or '0' for manual", str(default_idx))
113
+
114
+ if choice == "0":
115
+ git_url = None
116
+ elif choice.isdigit() and 1 <= int(choice) <= len(remote_names):
117
+ selected_remote = remote_names[int(choice)-1]
118
+ git_url = remotes[selected_remote]
119
+ else:
120
+ git_url = remotes[remote_names[default_idx-1]]
121
+ elif len(remotes) == 1:
122
+ git_url = list(remotes.values())[0]
123
+
124
+ _print_header(2, total_steps, "Repository Info", f"Detected: {git_branch or 'N/A'} @ {git_url or 'N/A'}")
125
+ if input(f" Use detected Git info? ({GREEN}Y{RESET}/n): ").strip().lower() != 'n':
126
+ args.branch = git_branch
127
+ args.repo_url = git_url
128
+ else:
129
+ args.branch = _input_field("Branch Name", git_branch or 'None') or git_branch
130
+ args.repo_url = _input_field("Repository URL", git_url or 'None') or git_url
131
+ else:
132
+ if _ask_section(2, total_steps, "Repository Info", "Branch and Repository URL (No Git detected)"):
133
+ args.branch = _input_field("Branch Name", "None") or None
134
+ args.repo_url = _input_field("Repository URL", "None") or None
135
+
136
+ # 3. Visual Style
137
+ if _ask_section(3, total_steps, "Visual Style", "Titles, Subtitles, Accent Colors"):
138
+ args.cover_title = _input_field("Cover Title", args.cover_title) or args.cover_title
139
+ args.cover_subtitle = _input_field("Cover Subtitle", args.cover_subtitle) or args.cover_subtitle
140
+ args.primary_color = _input_field("Primary Accent Color (HEX)", args.primary_color) or args.primary_color
141
+ args.title_color = _input_field("Title Color (HEX)", args.title_color) or args.title_color
142
+
143
+ # 4. Typography
144
+ if _ask_section(4, total_steps, "Typography", "Fonts and Text Sizes"):
145
+ args.title_font = _input_field("Title Font", args.title_font or 'Helvetica') or args.title_font
146
+ args.normal_font = _input_field("Normal Font", args.normal_font or 'Helvetica') or args.normal_font
147
+ args.mono_font = _input_field("Monospace Font", args.mono_font or 'Auto') or args.mono_font
148
+ args.code_size = int(_input_field("Code Font Size", args.code_size) or args.code_size)
149
+
150
+ paths = _input_field("Additional Font Paths (comma-separated)", "None")
151
+ if paths:
152
+ args.font_path = [p.strip() for p in paths.split(",") if p.strip()]
153
+
154
+ # Emoji Font Check & Support
155
+ emoji_f, emoji_p = register_emoji_font()
156
+ if not emoji_f:
157
+ print(f"\n {YELLOW}⚠️ No emoji font detected!{RESET}")
158
+ print(f" To render emojis, install {BOLD}Symbola{RESET} or {BOLD}Google Noto Emoji{RESET}.")
159
+ print(f" Or provide a custom path above.")
160
+
161
+ choice = input(f" Use text descriptions for emojis (e.g. [smile])? (y/{GREEN}N{RESET}): ").strip().lower()
162
+ if choice == 'y':
163
+ args.emoji_description = True
164
+ else:
165
+ print(f" {GREEN}✅ Emoji font detected:{RESET} {emoji_f} ({emoji_p or 'System'})")
166
+ # 5. Page Layout
167
+ if _ask_section(5, total_steps, "Page Layout & Margins", "Margins, Paper Size, Page Numbering"):
168
+ args.margin_top = float(_input_field("Top Margin (cm)", args.margin_top or 2.0) or (args.margin_top or 2.0))
169
+ args.margin_bottom = float(_input_field("Bottom Margin (cm)", args.margin_bottom or 2.0) or (args.margin_bottom or 2.0))
170
+ args.page_width = float(_input_field("Page Width (mm)", 210.0) or 210.0)
171
+ args.page_height = float(_input_field("Page Height (mm)", 297.0) or 297.0)
172
+ args.no_page_numbers = input(f" Disable page numbers? (y/{GREEN}N{RESET}): ").strip().lower() == 'y'
173
+ if not args.no_page_numbers:
174
+ args.start_page = int(_input_field("Start Page Number", args.start_page) or args.start_page)
175
+
176
+ # 6. Filters
177
+ if _ask_section(6, total_steps, "File Filters", "Include/Exclude patterns (glob)"):
178
+ inc = _input_field("Include Patterns (comma-separated)", "None")
179
+ if inc: args.include = [p.strip() for p in inc.split(",") if p.strip()]
180
+ exc = _input_field("Exclude Patterns (comma-separated)", "None")
181
+ if exc: args.exclude = [p.strip() for p in exc.split(",") if p.strip()]
182
+
183
+ print(f"\n{BOLD}{GREEN}🚀 Configuration complete! Generating PDF...{RESET}\n")
184
+
185
+ except KeyboardInterrupt:
186
+ import sys
187
+ print(f"\n\n{YELLOW}⚠️ Wizard aborted by user (Ctrl+C).{RESET}")
188
+ sys.exit(0)
@@ -86,7 +86,13 @@ TTF_SEARCH_PATHS = [
86
86
  "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
87
87
  "C:\\Windows\\Fonts\\consola.ttf", "C:\\Windows\\Fonts\\cour.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "C:\\Windows\\Fonts\\arial.ttf"
88
88
  ]
89
- EMOJI_SEARCH_PATHS = ["/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf", "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf"]
89
+ EMOJI_SEARCH_PATHS = [
90
+ "C:\\Windows\\Fonts\\seguiemj.ttf", # Windows Standard Emoji
91
+ "C:\\Windows\\Fonts\\seguisym.ttf", # Windows Standard Symbol
92
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", # Linux Standard Fallback
93
+ "/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf",
94
+ "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf"
95
+ ]
90
96
 
91
97
  def _register_font(name: str, paths: list, fallback):
92
98
  for p in paths:
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import unicodedata
2
3
  from reportlab.lib import colors
3
4
  from reportlab.pdfbase import pdfmetrics
4
5
 
@@ -7,16 +8,29 @@ def sanitize_text(text: str) -> str:
7
8
  if not text: return ""
8
9
  return re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
9
10
 
11
+ def _get_emoji_label(char: str) -> str:
12
+ """Returns a textual label for an emoji character."""
13
+ try:
14
+ name = unicodedata.name(char)
15
+ return f"[{name}]"
16
+ except (ValueError, KeyError):
17
+ return f"[Emoji-{ord(char):X}]"
18
+
10
19
  def get_safe_string_width(text, font_name, font_size, emoji_font=None, emoji_description=False):
11
20
  from .fonts import is_char_supported
12
21
  total_w = 0.0
13
22
  for char in text:
14
23
  if is_char_supported(char, font_name):
15
24
  total_w += pdfmetrics.stringWidth(char, font_name, font_size)
25
+ elif emoji_description:
26
+ label = _get_emoji_label(char)
27
+ total_w += pdfmetrics.stringWidth(label, font_name, font_size)
16
28
  elif emoji_font:
17
- if emoji_description: total_w += pdfmetrics.stringWidth(f"[{char}]", font_name, font_size)
18
- else: total_w += pdfmetrics.stringWidth(char, emoji_font, font_size)
19
- else: total_w += pdfmetrics.stringWidth("?", font_name, font_size)
29
+ total_w += pdfmetrics.stringWidth(char, emoji_font, font_size)
30
+ else:
31
+ # Fallback to description if no font is available, even if emoji_description=False
32
+ label = _get_emoji_label(char)
33
+ total_w += pdfmetrics.stringWidth(label, font_name, font_size)
20
34
  return total_w
21
35
 
22
36
  def draw_text_with_fallback(canvas, x, y, text, font_name, font_size, emoji_font=None, color=None, emoji_description=False):
@@ -28,20 +42,21 @@ def draw_text_with_fallback(canvas, x, y, text, font_name, font_size, emoji_font
28
42
  canvas.setFont(font_name, font_size)
29
43
  canvas.drawString(curr_x, y, char)
30
44
  curr_x += pdfmetrics.stringWidth(char, font_name, font_size)
45
+ elif emoji_description:
46
+ canvas.setFont(font_name, font_size)
47
+ label = _get_emoji_label(char)
48
+ canvas.drawString(curr_x, y, label)
49
+ curr_x += pdfmetrics.stringWidth(label, font_name, font_size)
31
50
  elif emoji_font:
32
- if emoji_description:
33
- canvas.setFont(font_name, font_size)
34
- label = f"[{char}]"
35
- canvas.drawString(curr_x, y, label)
36
- curr_x += pdfmetrics.stringWidth(label, font_name, font_size)
37
- else:
38
- canvas.setFont(emoji_font, font_size)
39
- canvas.drawString(curr_x, y, char)
40
- curr_x += pdfmetrics.stringWidth(char, emoji_font, font_size)
51
+ canvas.setFont(emoji_font, font_size)
52
+ canvas.drawString(curr_x, y, char)
53
+ curr_x += pdfmetrics.stringWidth(char, emoji_font, font_size)
41
54
  else:
55
+ # Fallback to description if no font is available
42
56
  canvas.setFont(font_name, font_size)
43
- canvas.drawString(curr_x, y, "?")
44
- curr_x += pdfmetrics.stringWidth("?", font_name, font_size)
57
+ label = _get_emoji_label(char)
58
+ canvas.drawString(curr_x, y, label)
59
+ curr_x += pdfmetrics.stringWidth(label, font_name, font_size)
45
60
  return curr_x
46
61
 
47
62
  def draw_centred_text_with_fallback(canvas, x, y, text, font_name, font_size, emoji_font=None, color=None, emoji_description=False):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeannex
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Generates a professional PDF source code annex with Smart Index, Images and Emoji support.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/tanhleno/codeannex
@@ -36,6 +36,8 @@ Generates a professional PDF annex from a project's source code — featuring sy
36
36
  ## 🚀 Key Features
37
37
 
38
38
  - **Interactive Wizard 2.0** — Step-by-step configuration with smart sections (Project, Style, Typography, Layout, Filters) and explicit default prompts.
39
+ - **Smart Emoji Support** — Automatic discovery of standard emoji fonts (**Segoe UI Emoji** on Windows, **DejaVu Sans** on Linux).
40
+ - **Graceful Emoji Fallback** — If no emoji font is found, it uses official Unicode names (e.g., `[ROCKET]`) for perfect readability.
39
41
  - **Git Version Tracking** — Automatically detects **Repository URL**, **Branch**, and **Commit SHA**. Smart root detection avoids Git metadata on subdirectories.
40
42
  - **Smart SVG Rendering** — Files are rendered as both a high-quality image and XML code. Entries are intelligently deduplicated in the summary.
41
43
  - **Improved Document Structure** — Subdirectories and their contents are listed before root files for better organization.
@@ -58,7 +60,7 @@ For full SVG support (required for crisp line numbers and SVG image rendering):
58
60
  pipx install "codeannex[svg]"
59
61
  ```
60
62
 
61
- *Alternatively, you can use standard pip:* `pip install codeannex`
63
+ *Note: For best emoji rendering on Linux, you may want to install `fonts-noto-color-emoji` or `ttf-dejavu`.*
62
64
 
63
65
  ## 📖 Usage
64
66
 
@@ -102,6 +104,8 @@ Default output filename is `{project_name}_code_annex.pdf`.
102
104
  ### Fonts
103
105
  - `--font-path PATH` — Additional directory to search for `.ttf`/`.otf` files.
104
106
  - `--title-font` / `--normal-font` / `--mono-font` — System font names.
107
+ - `--emoji-font NAME` — Custom font for emojis.
108
+ - `--emoji-description` — Force textual descriptions (e.g., `[GRINNING FACE]`) instead of glyphs.
105
109
 
106
110
  ## 🧪 Testing
107
111
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeannex"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Generates a professional PDF source code annex with Smart Index, Images and Emoji support."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,161 +0,0 @@
1
- import argparse
2
- from pathlib import Path
3
- from ..core.config import PDFConfig
4
- from ..io.git_utils import get_git_info, get_git_remotes
5
-
6
- # ANSI Colors for a better CLI experience
7
- BOLD = "\033[1m"
8
- BLUE = "\033[34m"
9
- CYAN = "\033[36m"
10
- GREEN = "\033[32m"
11
- YELLOW = "\033[33m"
12
- RESET = "\033[0m"
13
-
14
- def parse_args():
15
- parser = argparse.ArgumentParser(description="Generates a PDF code annex with Smart Index and Images.")
16
- parser.add_argument("dir", nargs="?", default=".", help="Project directory")
17
- parser.add_argument("-o", "--output", default=None, help="Output PDF filename")
18
- parser.add_argument("-n", "--name", default=None, help="Project name")
19
- parser.add_argument("--no-input", action="store_true", help="Disable interactive mode")
20
- parser.add_argument("--cover-title", default="TECHNICAL ANNEX", help="Cover title")
21
- parser.add_argument("--margin", type=float, default=None, help="General margin (cm)")
22
- parser.add_argument("--margin-left", type=float, default=None, help="Left margin (cm)")
23
- parser.add_argument("--margin-right", type=float, default=None, help="Right margin (cm)")
24
- parser.add_argument("--margin-top", type=float, default=None, help="Top margin (cm)")
25
- parser.add_argument("--margin-bottom", type=float, default=None, help="Bottom margin (cm)")
26
- parser.add_argument("--start-page", type=int, default=1, help="Start page")
27
- parser.add_argument("--show-project", action="store_true", help="Show project in footer")
28
- parser.add_argument("--repo-url", default=None, help="Repo URL")
29
- parser.add_argument("--branch", default=None, help="Branch name")
30
- parser.add_argument("--repo-label", default="Repository Name: ", help="Label for repo")
31
- parser.add_argument("--project-label", default="Project: ", help="Label for project")
32
- parser.add_argument("--page-width", type=float, default=None, help="Width (mm)")
33
- parser.add_argument("--page-height", type=float, default=None, help="Height (mm)")
34
- parser.add_argument("--include", action="append", default=None, help="Include pattern")
35
- parser.add_argument("--exclude", action="append", default=None, help="Exclude pattern")
36
- parser.add_argument("--no-git", action="store_true", help="No Git")
37
- parser.add_argument("--file-part-format", default="(part {current}/{total})", help="Part format")
38
- parser.add_argument("--summary-title", default="Summary / File Index", help="Summary title")
39
- parser.add_argument("--cover-subtitle", default="Source Code Documentation", help="Cover subtitle")
40
- parser.add_argument("--page-number-size", type=int, default=8, help="Page number size")
41
- parser.add_argument("--page-number-format", default="{n}", help="Page number format")
42
- parser.add_argument("--no-page-numbers", action="store_true", help="No page numbers")
43
- parser.add_argument("--page-bg-color", default="#ffffff", help="Page BG color")
44
- parser.add_argument("--normal-font", default=None, help="Normal font")
45
- parser.add_argument("--normal-size", type=int, default=10, help="Normal size")
46
- parser.add_argument("--normal-color", default="#4c4f69", help="Normal color")
47
- parser.add_argument("--bold-font", default=None, help="Bold font")
48
- parser.add_argument("--title-font", default=None, help="Title font")
49
- parser.add_argument("--title-size", type=int, default=28, help="Title size")
50
- parser.add_argument("--title-color", default="#1e1e2e", help="Title color")
51
- parser.add_argument("--subtitle-font", default=None, help="Subtitle font")
52
- parser.add_argument("--subtitle-size", type=int, default=18, help="Subtitle size")
53
- parser.add_argument("--subtitle-color", default=None, help="Subtitle color")
54
- parser.add_argument("--primary-color", default="#1e66f5", help="Primary color")
55
- parser.add_argument("--code-size", type=int, default=10, help="Code size")
56
- parser.add_argument("--code-bg", default="#1e1e2e", help="Code background")
57
- parser.add_argument("--mono-font", default=None, help="Mono font")
58
- parser.add_argument("--emoji-font", default=None, help="Emoji font")
59
- parser.add_argument("--font-path", action="append", default=None, help="Additional directory to search for fonts")
60
- parser.add_argument("--emoji-description", action="store_true", help="Emoji desc")
61
- parser.add_argument("--check-emoji-font", action="store_true", help="Check emoji")
62
- return parser.parse_known_args()
63
-
64
- def _print_header(step, total, title, details):
65
- print(f"\n{BLUE}Step {step}/{total}{RESET} {BOLD}--- {title} ---{RESET}")
66
- if details:
67
- print(f" {CYAN}i{RESET} {details}")
68
-
69
- def _ask_section(step, total, title, details, default_yes=False):
70
- _print_header(step, total, title, details)
71
- prompt = f"{GREEN}Y{RESET}/n" if default_yes else f"y/{GREEN}N{RESET}"
72
- res = input(f" Customize this section? ({prompt}): ").strip().lower()
73
- if not res: return default_yes
74
- return res == 'y'
75
-
76
- def _input_field(label, default):
77
- prompt = f" {label} {CYAN}[{default if default is not None else ''}]{RESET}: "
78
- return input(prompt).strip()
79
-
80
- def run_interactive_wizard(args):
81
- print(f"\n{BOLD}{BLUE}✨ Welcome to codeannex Interactive Wizard! ✨{RESET}")
82
- print(f"{YELLOW}Uppercase letters in prompts indicate the [default] action on Enter.{RESET}\n")
83
-
84
- root = Path(args.dir).resolve()
85
- git_url, git_branch, _ = get_git_info(root)
86
- remotes = get_git_remotes(root)
87
- total_steps = 6
88
-
89
- # 1. Project Identity
90
- _print_header(1, total_steps, "Project Identity", "Basic identification of your document")
91
- args.name = _input_field("Project Name", args.name or root.name) or (args.name or root.name)
92
-
93
- # 2. Repository Info
94
- has_git = bool(remotes or git_branch)
95
- if has_git:
96
- if len(remotes) > 1:
97
- _print_header(2, total_steps, "Repository Info", "Multiple Git remotes detected")
98
- print(f" Available remotes:")
99
- remote_names = list(remotes.keys())
100
- for i, name in enumerate(remote_names, 1):
101
- print(f" {i}. {name} ({remotes[name]})")
102
-
103
- choice = _input_field("Select remote (number) or press Enter for origin", "1")
104
- if choice.isdigit() and 1 <= int(choice) <= len(remote_names):
105
- selected_remote = remote_names[int(choice)-1]
106
- git_url = remotes[selected_remote]
107
- elif "origin" in remotes:
108
- git_url = remotes["origin"]
109
- else:
110
- git_url = remotes[remote_names[0]]
111
- elif len(remotes) == 1:
112
- git_url = list(remotes.values())[0]
113
-
114
- _print_header(2, total_steps, "Repository Info", f"Detected: {git_branch or 'N/A'} @ {git_url or 'N/A'}")
115
- if input(f" Use detected Git info? ({GREEN}Y{RESET}/n): ").strip().lower() != 'n':
116
- args.branch = git_branch
117
- args.repo_url = git_url
118
- else:
119
- args.branch = _input_field("Branch Name", git_branch or 'None') or git_branch
120
- args.repo_url = _input_field("Repository URL", git_url or 'None') or git_url
121
- else:
122
- if _ask_section(2, total_steps, "Repository Info", "Branch and Repository URL (No Git detected)"):
123
- args.branch = _input_field("Branch Name", "None") or None
124
- args.repo_url = _input_field("Repository URL", "None") or None
125
-
126
- # 3. Visual Style
127
- if _ask_section(3, total_steps, "Visual Style", "Titles, Subtitles, Accent Colors"):
128
- args.cover_title = _input_field("Cover Title", args.cover_title) or args.cover_title
129
- args.cover_subtitle = _input_field("Cover Subtitle", args.cover_subtitle) or args.cover_subtitle
130
- args.primary_color = _input_field("Primary Accent Color (HEX)", args.primary_color) or args.primary_color
131
- args.title_color = _input_field("Title Color (HEX)", args.title_color) or args.title_color
132
-
133
- # 4. Typography
134
- if _ask_section(4, total_steps, "Typography", "Fonts and Text Sizes"):
135
- args.title_font = _input_field("Title Font", args.title_font or 'Helvetica') or args.title_font
136
- args.normal_font = _input_field("Normal Font", args.normal_font or 'Helvetica') or args.normal_font
137
- args.mono_font = _input_field("Monospace Font", args.mono_font or 'Auto') or args.mono_font
138
- args.code_size = int(_input_field("Code Font Size", args.code_size) or args.code_size)
139
-
140
- paths = _input_field("Additional Font Paths (comma-separated)", "None")
141
- if paths:
142
- args.font_path = [p.strip() for p in paths.split(",") if p.strip()]
143
-
144
- # 5. Page Layout
145
- if _ask_section(5, total_steps, "Page Layout & Margins", "Margins, Paper Size, Page Numbering"):
146
- args.margin_top = float(_input_field("Top Margin (cm)", args.margin_top or 2.0) or (args.margin_top or 2.0))
147
- args.margin_bottom = float(_input_field("Bottom Margin (cm)", args.margin_bottom or 2.0) or (args.margin_bottom or 2.0))
148
- args.page_width = float(_input_field("Page Width (mm)", 210.0) or 210.0)
149
- args.page_height = float(_input_field("Page Height (mm)", 297.0) or 297.0)
150
- args.no_page_numbers = input(f" Disable page numbers? (y/{GREEN}N{RESET}): ").strip().lower() == 'y'
151
- if not args.no_page_numbers:
152
- args.start_page = int(_input_field("Start Page Number", args.start_page) or args.start_page)
153
-
154
- # 6. Filters
155
- if _ask_section(6, total_steps, "File Filters", "Include/Exclude patterns (glob)"):
156
- inc = _input_field("Include Patterns (comma-separated)", "None")
157
- if inc: args.include = [p.strip() for p in inc.split(",") if p.strip()]
158
- exc = _input_field("Exclude Patterns (comma-separated)", "None")
159
- if exc: args.exclude = [p.strip() for p in exc.split(",") if p.strip()]
160
-
161
- print(f"\n{BOLD}{GREEN}🚀 Configuration complete! Generating PDF...{RESET}\n")
File without changes
File without changes