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.
- {codeannex-0.4.2 → codeannex-0.4.4}/PKG-INFO +6 -2
- {codeannex-0.4.2 → codeannex-0.4.4}/README.md +5 -1
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/__main__.py +7 -0
- codeannex-0.4.4/codeannex/interface/cli.py +188 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/fonts.py +7 -1
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/text_utils.py +29 -14
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/PKG-INFO +6 -2
- {codeannex-0.4.2 → codeannex-0.4.4}/pyproject.toml +1 -1
- codeannex-0.4.2/codeannex/interface/cli.py +0 -161
- {codeannex-0.4.2 → codeannex-0.4.4}/LICENSE +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/__init__.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/__init__.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/config.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/core/pdf_builder.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/interface/__init__.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/__init__.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/file_utils.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/io/git_utils.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/__init__.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex/renderer/highlight.py +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/SOURCES.txt +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/dependency_links.txt +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/entry_points.txt +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/requires.txt +0 -0
- {codeannex-0.4.2 → codeannex-0.4.4}/codeannex.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 = [
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|