@nguyenphp/antigravity-marketing 1.0.18 → 1.0.20

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 (231) hide show
  1. package/README.md +130 -78
  2. package/package.json +4 -3
  3. package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
  4. package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
  5. package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
  6. package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
  7. package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
  8. package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
  9. package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
  10. package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
  11. package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
  12. package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
  13. package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
  14. package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
  15. package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
  16. package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
  17. package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
  18. package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
  19. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
  20. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
  21. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
  22. package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
  23. package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
  24. package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
  25. package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
  26. package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
  27. package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
  28. package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
  29. package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
  30. package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
  31. package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
  32. package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
  33. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
  34. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
  35. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
  36. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
  37. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
  38. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
  39. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
  40. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
  41. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
  42. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
  43. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
  44. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
  45. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
  46. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
  47. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
  48. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
  49. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
  50. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
  51. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
  52. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
  53. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
  54. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
  55. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
  56. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
  57. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
  58. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
  59. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
  60. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
  61. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
  62. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
  63. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
  64. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
  65. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
  66. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
  67. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
  68. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
  69. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
  70. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
  71. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
  72. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
  73. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
  74. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
  75. package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
  76. package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
  77. package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
  78. package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
  79. package/templates/.agent/skills/minimax-pdf/README.md +222 -0
  80. package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
  81. package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
  82. package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
  83. package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
  84. package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
  85. package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
  86. package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
  87. package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
  88. package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
  89. package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
  90. package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
  91. package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
  92. package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
  93. package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
  94. package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
  95. package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
  96. package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
  97. package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
  98. package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
  99. package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
  100. package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
  101. package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
  102. package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
  103. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
  104. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
  105. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
  106. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
  107. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
  108. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
  109. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
  110. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
  111. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
  112. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
  113. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
  114. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
  115. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
  116. package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
  117. package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
  118. package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
  119. package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
  120. package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
  121. package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
  122. package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
  123. package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
  124. package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
  125. package/templates/.agent/workflows/analyze.md +3 -0
  126. package/templates/.agent/workflows/brand-report.md +44 -0
  127. package/templates/.agent/workflows/report.md +49 -0
  128. package/templates/.agent/agents/backend-specialist.md +0 -263
  129. package/templates/.agent/agents/database-architect.md +0 -226
  130. package/templates/.agent/agents/debugger.md +0 -225
  131. package/templates/.agent/agents/devops-engineer.md +0 -242
  132. package/templates/.agent/agents/frontend-specialist.md +0 -527
  133. package/templates/.agent/agents/game-developer.md +0 -162
  134. package/templates/.agent/agents/mobile-developer.md +0 -377
  135. package/templates/.agent/agents/penetration-tester.md +0 -188
  136. package/templates/.agent/agents/security-auditor.md +0 -170
  137. package/templates/.agent/agents/test-engineer.md +0 -158
  138. package/templates/.agent/skills/api-patterns/SKILL.md +0 -81
  139. package/templates/.agent/skills/api-patterns/api-style.md +0 -42
  140. package/templates/.agent/skills/api-patterns/auth.md +0 -24
  141. package/templates/.agent/skills/api-patterns/documentation.md +0 -26
  142. package/templates/.agent/skills/api-patterns/graphql.md +0 -41
  143. package/templates/.agent/skills/api-patterns/rate-limiting.md +0 -31
  144. package/templates/.agent/skills/api-patterns/response.md +0 -37
  145. package/templates/.agent/skills/api-patterns/rest.md +0 -40
  146. package/templates/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
  147. package/templates/.agent/skills/api-patterns/security-testing.md +0 -122
  148. package/templates/.agent/skills/api-patterns/trpc.md +0 -41
  149. package/templates/.agent/skills/api-patterns/versioning.md +0 -22
  150. package/templates/.agent/skills/app-builder/SKILL.md +0 -75
  151. package/templates/.agent/skills/app-builder/agent-coordination.md +0 -71
  152. package/templates/.agent/skills/app-builder/feature-building.md +0 -53
  153. package/templates/.agent/skills/app-builder/project-detection.md +0 -34
  154. package/templates/.agent/skills/app-builder/scaffolding.md +0 -118
  155. package/templates/.agent/skills/app-builder/tech-stack.md +0 -40
  156. package/templates/.agent/skills/app-builder/templates/SKILL.md +0 -39
  157. package/templates/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
  158. package/templates/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
  159. package/templates/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
  160. package/templates/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
  161. package/templates/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
  162. package/templates/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
  163. package/templates/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
  164. package/templates/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -82
  165. package/templates/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -100
  166. package/templates/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -106
  167. package/templates/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -101
  168. package/templates/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
  169. package/templates/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -93
  170. package/templates/.agent/skills/architecture/SKILL.md +0 -55
  171. package/templates/.agent/skills/architecture/context-discovery.md +0 -43
  172. package/templates/.agent/skills/architecture/examples.md +0 -94
  173. package/templates/.agent/skills/architecture/pattern-selection.md +0 -68
  174. package/templates/.agent/skills/architecture/patterns-reference.md +0 -50
  175. package/templates/.agent/skills/architecture/trade-off-analysis.md +0 -77
  176. package/templates/.agent/skills/bash-linux/SKILL.md +0 -199
  177. package/templates/.agent/skills/behavioral-modes/SKILL.md +0 -242
  178. package/templates/.agent/skills/clean-code/SKILL.md +0 -201
  179. package/templates/.agent/skills/code-review-checklist/SKILL.md +0 -109
  180. package/templates/.agent/skills/database-design/SKILL.md +0 -52
  181. package/templates/.agent/skills/database-design/database-selection.md +0 -43
  182. package/templates/.agent/skills/database-design/indexing.md +0 -39
  183. package/templates/.agent/skills/database-design/migrations.md +0 -48
  184. package/templates/.agent/skills/database-design/optimization.md +0 -36
  185. package/templates/.agent/skills/database-design/orm-selection.md +0 -30
  186. package/templates/.agent/skills/database-design/schema-design.md +0 -56
  187. package/templates/.agent/skills/database-design/scripts/schema_validator.py +0 -172
  188. package/templates/.agent/skills/deployment-procedures/SKILL.md +0 -241
  189. package/templates/.agent/skills/docker-expert/SKILL.md +0 -409
  190. package/templates/.agent/skills/game-development/2d-games/SKILL.md +0 -119
  191. package/templates/.agent/skills/game-development/3d-games/SKILL.md +0 -135
  192. package/templates/.agent/skills/game-development/SKILL.md +0 -167
  193. package/templates/.agent/skills/game-development/game-art/SKILL.md +0 -185
  194. package/templates/.agent/skills/game-development/game-audio/SKILL.md +0 -190
  195. package/templates/.agent/skills/game-development/game-design/SKILL.md +0 -129
  196. package/templates/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
  197. package/templates/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
  198. package/templates/.agent/skills/game-development/pc-games/SKILL.md +0 -144
  199. package/templates/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
  200. package/templates/.agent/skills/game-development/web-games/SKILL.md +0 -150
  201. package/templates/.agent/skills/lint-and-validate/SKILL.md +0 -45
  202. package/templates/.agent/skills/lint-and-validate/scripts/lint_runner.py +0 -172
  203. package/templates/.agent/skills/lint-and-validate/scripts/type_coverage.py +0 -173
  204. package/templates/.agent/skills/mcp-builder/SKILL.md +0 -176
  205. package/templates/.agent/skills/nestjs-expert/SKILL.md +0 -552
  206. package/templates/.agent/skills/nextjs-best-practices/SKILL.md +0 -203
  207. package/templates/.agent/skills/nodejs-best-practices/SKILL.md +0 -333
  208. package/templates/.agent/skills/parallel-agents/SKILL.md +0 -175
  209. package/templates/.agent/skills/performance-profiling/SKILL.md +0 -143
  210. package/templates/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +0 -76
  211. package/templates/.agent/skills/powershell-windows/SKILL.md +0 -167
  212. package/templates/.agent/skills/prisma-expert/SKILL.md +0 -355
  213. package/templates/.agent/skills/python-patterns/SKILL.md +0 -441
  214. package/templates/.agent/skills/react-patterns/SKILL.md +0 -198
  215. package/templates/.agent/skills/red-team-tactics/SKILL.md +0 -199
  216. package/templates/.agent/skills/server-management/SKILL.md +0 -161
  217. package/templates/.agent/skills/systematic-debugging/SKILL.md +0 -109
  218. package/templates/.agent/skills/tdd-workflow/SKILL.md +0 -149
  219. package/templates/.agent/skills/testing-patterns/SKILL.md +0 -178
  220. package/templates/.agent/skills/testing-patterns/scripts/test_runner.py +0 -219
  221. package/templates/.agent/skills/typescript-expert/SKILL.md +0 -429
  222. package/templates/.agent/skills/vue-expert/SKILL.md +0 -374
  223. package/templates/.agent/skills/vulnerability-scanner/SKILL.md +0 -276
  224. package/templates/.agent/skills/vulnerability-scanner/checklists.md +0 -121
  225. package/templates/.agent/skills/vulnerability-scanner/scripts/security_scan.py +0 -458
  226. package/templates/.agent/skills/webapp-testing/SKILL.md +0 -187
  227. package/templates/.agent/skills/webapp-testing/scripts/playwright_runner.py +0 -173
  228. package/templates/.agent/workflows/debug.md +0 -103
  229. package/templates/.agent/workflows/deploy.md +0 -176
  230. package/templates/.agent/workflows/enhance.md +0 -63
  231. package/templates/.agent/workflows/test.md +0 -144
@@ -0,0 +1,1055 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ render_body.py — Build the inner-page PDF from tokens.json + content.json.
4
+
5
+ Usage:
6
+ python3 render_body.py --tokens tokens.json --content content.json --out body.pdf
7
+
8
+ Block types:
9
+ h1 h2 h3 Headings (h1 adds a full-width accent rule below)
10
+ body Justified prose paragraph
11
+ bullet Bullet list item (• prefix)
12
+ numbered Auto-numbered list item (resets when interrupted)
13
+ callout Highlighted insight box with left accent bar
14
+ table Data table with accent header + alternating rows
15
+ image Inline image from file path
16
+ figure Image with auto-numbered "Figure N:" caption
17
+ code Monospace code block with accent left border
18
+ math Display math formula via matplotlib mathtext
19
+ chart Bar / line / pie chart rendered via matplotlib
20
+ flowchart Process diagram rendered via matplotlib
21
+ bibliography Numbered reference list
22
+ divider Full-width accent rule
23
+ caption Small muted text (e.g., under a figure)
24
+ pagebreak Force a new page
25
+ spacer Vertical whitespace (pt field, default 12)
26
+
27
+ Exit codes: 0 success, 1 bad args/missing file, 2 missing dep, 3 render error
28
+ """
29
+
30
+ import argparse
31
+ import io
32
+ import json
33
+ import os
34
+ import sys
35
+ import importlib.util
36
+
37
+
38
+ # ── Dependency bootstrap ───────────────────────────────────────────────────────
39
+ def ensure_deps():
40
+ missing = [p for p in ("reportlab", "pypdf")
41
+ if importlib.util.find_spec(p) is None]
42
+ if missing:
43
+ import subprocess
44
+ subprocess.check_call(
45
+ [sys.executable, "-m", "pip", "install",
46
+ "--break-system-packages", "-q"] + missing
47
+ )
48
+
49
+
50
+ ensure_deps()
51
+
52
+ from reportlab.platypus import (
53
+ BaseDocTemplate, PageTemplate, Frame,
54
+ Paragraph, Spacer, Table, TableStyle,
55
+ HRFlowable, PageBreak, Flowable, KeepTogether,
56
+ Preformatted, Image as RLImage,
57
+ )
58
+ from reportlab.lib.pagesizes import A4
59
+ from reportlab.lib.styles import ParagraphStyle
60
+ from reportlab.lib.colors import HexColor
61
+ from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER
62
+ from reportlab.pdfbase import pdfmetrics
63
+ from reportlab.pdfbase.ttfonts import TTFont
64
+
65
+
66
+ # ── Font registration ──────────────────────────────────────────────────────────
67
+ def register_fonts(tokens: dict):
68
+ """Register TTF fonts from token font_paths if present."""
69
+ for name, fpath in tokens.get("font_paths", {}).items():
70
+ if os.path.exists(fpath):
71
+ try:
72
+ pdfmetrics.registerFont(TTFont(name, fpath))
73
+ except Exception:
74
+ pass
75
+
76
+
77
+ # ══════════════════════════════════════════════════════════════════════════════
78
+ # Custom Flowables
79
+ # ══════════════════════════════════════════════════════════════════════════════
80
+
81
+ class CalloutBox(Flowable):
82
+ """Highlighted insight box: coloured background + 4px left accent bar."""
83
+
84
+ def __init__(self, text: str, style, accent: str, bg: str):
85
+ super().__init__()
86
+ self._para = Paragraph(text, style)
87
+ self._accent = HexColor(accent)
88
+ self._bg = HexColor(bg)
89
+
90
+ def wrap(self, aw, ah):
91
+ self._w = aw
92
+ _, ph = self._para.wrap(aw - 36, ah)
93
+ self._h = ph + 22
94
+ return aw, self._h
95
+
96
+ def draw(self):
97
+ c = self.canv
98
+ c.setFillColor(self._bg)
99
+ c.roundRect(0, 0, self._w, self._h, 5, fill=1, stroke=0)
100
+ c.setFillColor(self._accent)
101
+ c.rect(0, 0, 4, self._h, fill=1, stroke=0)
102
+ self._para.drawOn(c, 18, 11)
103
+
104
+
105
+ class BibliographyItem(Flowable):
106
+ """Single hanging-indent bibliography entry rendered as [N] text."""
107
+
108
+ LABEL_W = 28
109
+
110
+ def __init__(self, ref_id: str, text: str, style, dark: str):
111
+ super().__init__()
112
+ self._id = ref_id
113
+ self._text = text
114
+ self._style = style
115
+ self._dark = HexColor(dark)
116
+
117
+ def wrap(self, aw, ah):
118
+ self._w = aw
119
+ self._para = Paragraph(self._text, self._style)
120
+ _, ph = self._para.wrap(aw - self.LABEL_W, ah)
121
+ self._h = ph + 4
122
+ return aw, self._h
123
+
124
+ def draw(self):
125
+ c = self.canv
126
+ c.setFillColor(self._dark)
127
+ c.setFont("Helvetica-Bold", 8.5)
128
+ c.drawString(0, self._h - 12, f"[{self._id}]")
129
+ self._para.drawOn(c, self.LABEL_W, 2)
130
+
131
+
132
+ # ══════════════════════════════════════════════════════════════════════════════
133
+ # Page template (header + footer)
134
+ # ══════════════════════════════════════════════════════════════════════════════
135
+
136
+ class BeautifulDoc(BaseDocTemplate):
137
+ def __init__(self, path: str, tokens: dict, **kw):
138
+ self._t = tokens
139
+ super().__init__(path, **kw)
140
+ fr = Frame(
141
+ self.leftMargin, self.bottomMargin,
142
+ self.width, self.height, id="body",
143
+ )
144
+ tmpl = PageTemplate(id="main", frames=fr, onPage=self._decorate)
145
+ self.addPageTemplates([tmpl])
146
+
147
+ def _decorate(self, canv, doc):
148
+ t = self._t
149
+ lm = doc.leftMargin
150
+ rm = doc.rightMargin
151
+ pw = doc.pagesize[0]
152
+ ph = doc.pagesize[1]
153
+ top = ph - doc.topMargin
154
+
155
+ canv.saveState()
156
+
157
+ # Header accent rule
158
+ canv.setStrokeColor(HexColor(t["accent"]))
159
+ canv.setLineWidth(1.5)
160
+ canv.line(lm, top + 12, pw - rm, top + 12)
161
+
162
+ # Header: title (left) + date (right)
163
+ canv.setFillColor(HexColor(t["muted"]))
164
+ canv.setFont(t["font_body_rl"], t["size_meta"])
165
+ canv.drawString(lm, top + 16, t["title"].upper())
166
+ canv.drawRightString(pw - rm, top + 16, t.get("date", ""))
167
+
168
+ # Footer rule
169
+ canv.setStrokeColor(HexColor("#DDDDDD"))
170
+ canv.setLineWidth(0.5)
171
+ canv.line(lm, doc.bottomMargin - 12, pw - rm, doc.bottomMargin - 12)
172
+
173
+ # Footer: author (left) + page number (right)
174
+ canv.setFillColor(HexColor(t["muted"]))
175
+ canv.setFont(t["font_body_rl"], t["size_meta"])
176
+ canv.drawString(lm, doc.bottomMargin - 22, t.get("author", ""))
177
+ canv.drawRightString(pw - rm, doc.bottomMargin - 22, str(doc.page))
178
+
179
+ canv.restoreState()
180
+
181
+
182
+ # ══════════════════════════════════════════════════════════════════════════════
183
+ # Style factory
184
+ # ══════════════════════════════════════════════════════════════════════════════
185
+
186
+ def make_styles(t: dict) -> dict:
187
+ hf = t["font_display_rl"]
188
+ bf = t["font_body_rl"]
189
+ bfb = t["font_body_b_rl"]
190
+ dk = t["body_text"]
191
+ d = t["dark"]
192
+ mu = t["muted"]
193
+
194
+ return {
195
+ "h1": ParagraphStyle("H1",
196
+ fontName=hf, fontSize=t["size_h1"],
197
+ leading=t["size_h1"] * 1.3,
198
+ textColor=HexColor(d),
199
+ spaceBefore=t["section_gap"], spaceAfter=4,
200
+ ),
201
+ "h2": ParagraphStyle("H2",
202
+ fontName=hf, fontSize=t["size_h2"],
203
+ leading=t["size_h2"] * 1.4,
204
+ textColor=HexColor(d),
205
+ spaceBefore=18, spaceAfter=5,
206
+ ),
207
+ "h3": ParagraphStyle("H3",
208
+ fontName=bfb, fontSize=t["size_h3"],
209
+ leading=t["size_h3"] * 1.5,
210
+ textColor=HexColor(d),
211
+ spaceBefore=12, spaceAfter=3,
212
+ ),
213
+ "body": ParagraphStyle("Body",
214
+ fontName=bf, fontSize=t["size_body"],
215
+ leading=t["line_gap"],
216
+ textColor=HexColor(dk),
217
+ spaceAfter=t["para_gap"], alignment=TA_JUSTIFY,
218
+ ),
219
+ "bullet": ParagraphStyle("Bullet",
220
+ fontName=bf, fontSize=t["size_body"],
221
+ leading=t["line_gap"] - 1,
222
+ textColor=HexColor(dk),
223
+ spaceAfter=4, leftIndent=14,
224
+ ),
225
+ "numbered": ParagraphStyle("Numbered",
226
+ fontName=bf, fontSize=t["size_body"],
227
+ leading=t["line_gap"] - 1,
228
+ textColor=HexColor(dk),
229
+ spaceAfter=4, leftIndent=22, firstLineIndent=-22,
230
+ ),
231
+ "callout": ParagraphStyle("Callout",
232
+ fontName=bfb, fontSize=t["size_body"] + 0.5, leading=16,
233
+ textColor=HexColor(d),
234
+ ),
235
+ "caption": ParagraphStyle("Caption",
236
+ fontName=bf, fontSize=t["size_caption"], leading=13,
237
+ textColor=HexColor(mu), spaceAfter=6,
238
+ alignment=TA_CENTER,
239
+ ),
240
+ "table_header": ParagraphStyle("TblH",
241
+ fontName=bfb, fontSize=9.5, leading=13,
242
+ textColor=HexColor("#FFFFFF"),
243
+ ),
244
+ "table_cell": ParagraphStyle("TblC",
245
+ fontName=bf, fontSize=9.5, leading=13,
246
+ textColor=HexColor(dk),
247
+ ),
248
+ "code": ParagraphStyle("Code",
249
+ fontName="Courier", fontSize=8.5, leading=12.5,
250
+ textColor=HexColor(dk),
251
+ ),
252
+ "code_lang": ParagraphStyle("CodeLang",
253
+ fontName="Courier", fontSize=7, leading=10,
254
+ textColor=HexColor(mu),
255
+ ),
256
+ "bib": ParagraphStyle("Bib",
257
+ fontName=bf, fontSize=9, leading=14,
258
+ textColor=HexColor(dk),
259
+ ),
260
+ "bib_title": ParagraphStyle("BibTitle",
261
+ fontName=hf, fontSize=t["size_h2"],
262
+ leading=t["size_h2"] * 1.4,
263
+ textColor=HexColor(d),
264
+ spaceBefore=t["section_gap"], spaceAfter=8,
265
+ ),
266
+ "math_fallback": ParagraphStyle("MathFb",
267
+ fontName="Courier", fontSize=9, leading=13,
268
+ textColor=HexColor(dk),
269
+ ),
270
+ "eq_label": ParagraphStyle("EqLabel",
271
+ fontName="Helvetica", fontSize=9, leading=12,
272
+ textColor=HexColor(mu),
273
+ ),
274
+ }
275
+
276
+
277
+ # ══════════════════════════════════════════════════════════════════════════════
278
+ # Shared helpers
279
+ # ══════════════════════════════════════════════════════════════════════════════
280
+
281
+ def _divider(accent: str) -> HRFlowable:
282
+ return HRFlowable(
283
+ width="100%", thickness=1.2,
284
+ color=HexColor(accent),
285
+ spaceBefore=14, spaceAfter=14,
286
+ )
287
+
288
+
289
+ def _image_from_bytes(png_bytes: bytes, usable_w: float,
290
+ max_frac: float = 0.88) -> RLImage:
291
+ """Create a scaled RLImage from PNG bytes, bounded to max_frac of usable_w."""
292
+ img = RLImage(io.BytesIO(png_bytes))
293
+ max_w = usable_w * max_frac
294
+ if img.drawWidth > max_w:
295
+ scale = max_w / img.drawWidth
296
+ img.drawWidth = max_w
297
+ img.drawHeight = img.drawHeight * scale
298
+ return img
299
+
300
+
301
+ # ══════════════════════════════════════════════════════════════════════════════
302
+ # PNG renderers (matplotlib)
303
+ # ══════════════════════════════════════════════════════════════════════════════
304
+
305
+ from typing import List, Dict, Optional, Any, Union
306
+
307
+ # ... (finding the specific line)
308
+ def _render_math_png(expr: str, dpi: int = 180) -> Union[bytes, None]:
309
+ """
310
+ Render a LaTeX math expression via matplotlib mathtext.
311
+ No LaTeX binary required — uses matplotlib's built-in math parser.
312
+ Supports: fractions (\\frac), integrals (\\int), sums (\\sum),
313
+ Greek letters, sub/superscripts, etc.
314
+ """
315
+ try:
316
+ import matplotlib
317
+ matplotlib.use("Agg")
318
+ import matplotlib.pyplot as plt
319
+
320
+ fig = plt.figure(figsize=(8, 1.2))
321
+ fig.patch.set_facecolor("white")
322
+ ax = fig.add_axes([0, 0, 1, 1])
323
+ ax.set_axis_off()
324
+ ax.set_facecolor("white")
325
+ ax.text(0.5, 0.5, f"${expr}$",
326
+ fontsize=16, ha="center", va="center",
327
+ transform=ax.transAxes)
328
+ buf = io.BytesIO()
329
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
330
+ facecolor="white", pad_inches=0.1)
331
+ plt.close(fig)
332
+ buf.seek(0)
333
+ return buf.read()
334
+ except Exception:
335
+ return None
336
+
337
+
338
+ def _render_chart_png(item: dict, accent: str, dpi: int = 150) -> Optional[bytes]:
339
+ """
340
+ Render bar / line / pie chart to PNG using matplotlib.
341
+
342
+ Required fields:
343
+ chart_type "bar" | "line" | "pie" (default "bar")
344
+ labels list of category strings
345
+ datasets list of {label?, values: list[number]}
346
+
347
+ Optional fields:
348
+ title chart title
349
+ x_label X-axis label
350
+ y_label Y-axis label
351
+ """
352
+ try:
353
+ import matplotlib
354
+ matplotlib.use("Agg")
355
+ import matplotlib.pyplot as plt
356
+ import matplotlib.colors as mcolors
357
+ import colorsys
358
+ import numpy as np
359
+
360
+ chart_type = item.get("chart_type", "bar")
361
+ title_text = item.get("title", "")
362
+ labels = item.get("labels", [])
363
+ datasets = item.get("datasets", [])
364
+
365
+ # Derive a consistent palette from the document accent color
366
+ r, g, b = mcolors.to_rgb(accent)
367
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
368
+ palette = [
369
+ colorsys.hsv_to_rgb(
370
+ (h + i * 0.13) % 1.0,
371
+ max(0.35, s - i * 0.08),
372
+ min(0.92, v + i * 0.04),
373
+ )
374
+ for i in range(max(len(datasets), 1))
375
+ ]
376
+
377
+ fig, ax = plt.subplots(figsize=(7, 3.6), dpi=dpi)
378
+ fig.patch.set_facecolor("white")
379
+ ax.set_facecolor("white")
380
+
381
+ if chart_type == "bar":
382
+ x = np.arange(len(labels))
383
+ n = max(len(datasets), 1)
384
+ width = 0.68 / n
385
+ for i, ds in enumerate(datasets):
386
+ offset = (i - (n - 1) / 2) * width
387
+ ax.bar(x + offset, ds.get("values", []), width * 0.88,
388
+ label=ds.get("label", f"Series {i+1}"),
389
+ color=palette[i % len(palette)], edgecolor="none")
390
+ ax.set_xticks(x)
391
+ ax.set_xticklabels(labels, fontsize=8.5)
392
+ ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
393
+ ax.set_axisbelow(True)
394
+ if item.get("x_label"):
395
+ ax.set_xlabel(item["x_label"], fontsize=8.5)
396
+ if item.get("y_label"):
397
+ ax.set_ylabel(item["y_label"], fontsize=8.5)
398
+
399
+ elif chart_type == "line":
400
+ x = np.arange(len(labels))
401
+ for i, ds in enumerate(datasets):
402
+ ax.plot(x, ds.get("values", []), marker="o", markersize=3.5,
403
+ label=ds.get("label", f"Series {i+1}"),
404
+ color=palette[i % len(palette)], linewidth=1.8)
405
+ ax.set_xticks(x)
406
+ ax.set_xticklabels(labels, fontsize=8.5)
407
+ ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
408
+ ax.set_axisbelow(True)
409
+ if item.get("x_label"):
410
+ ax.set_xlabel(item["x_label"], fontsize=8.5)
411
+ if item.get("y_label"):
412
+ ax.set_ylabel(item["y_label"], fontsize=8.5)
413
+
414
+ elif chart_type == "pie":
415
+ vals = datasets[0].get("values", []) if datasets else []
416
+ colors = [
417
+ colorsys.hsv_to_rgb(
418
+ (h + i * 0.11) % 1.0,
419
+ max(0.30, s - i * 0.06),
420
+ min(0.92, v + i * 0.03),
421
+ )
422
+ for i in range(len(vals))
423
+ ]
424
+ ax.pie(vals, labels=labels, colors=colors,
425
+ autopct="%1.1f%%", pctdistance=0.82,
426
+ wedgeprops=dict(edgecolor="white", linewidth=1.4),
427
+ textprops=dict(fontsize=8.5))
428
+
429
+ # Shared styling
430
+ for spine in ax.spines.values():
431
+ spine.set_linewidth(0.5)
432
+ spine.set_color("#CCCCCC")
433
+ ax.tick_params(axis="both", length=0, labelsize=8.5)
434
+ if title_text:
435
+ ax.set_title(title_text, fontsize=10, pad=8,
436
+ color="#333333", fontweight="bold")
437
+ if len(datasets) > 1 and chart_type != "pie":
438
+ ax.legend(frameon=False, fontsize=8, loc="upper right")
439
+
440
+ plt.tight_layout(pad=0.4)
441
+ buf = io.BytesIO()
442
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
443
+ facecolor="white", pad_inches=0.06)
444
+ plt.close(fig)
445
+ buf.seek(0)
446
+ return buf.read()
447
+ except Exception:
448
+ return None
449
+
450
+
451
+ def _render_flowchart_png(item: dict, accent: str, dark: str,
452
+ muted: str, dpi: int = 130) -> Union[bytes, None]:
453
+ """
454
+ Render a top-to-bottom flowchart using matplotlib patches and arrows.
455
+
456
+ Node schema: {id, label, shape?}
457
+ shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
458
+
459
+ Edge schema: {from, to, label?}
460
+ Forward edges (to a later node) draw straight arrows.
461
+ Back edges (to an earlier node) draw a curved arc to the right.
462
+ """
463
+ try:
464
+ import matplotlib
465
+ matplotlib.use("Agg")
466
+ import matplotlib.pyplot as plt
467
+ import matplotlib.patches as mpatch
468
+ from matplotlib.patches import FancyBboxPatch
469
+ import matplotlib.colors as mcolors
470
+
471
+ nodes_list = item.get("nodes", [])
472
+ edges = item.get("edges", [])
473
+ if not nodes_list:
474
+ return None
475
+
476
+ nodes = {n["id"]: n for n in nodes_list}
477
+ order = {n["id"]: i for i, n in enumerate(nodes_list)}
478
+
479
+ n_nodes = len(nodes_list)
480
+ BOX_W = 4.2
481
+ BOX_H = 0.58
482
+ STEP_Y = 1.25
483
+ CX = 5.0
484
+
485
+ fig_h = max(3.5, n_nodes * STEP_Y + 0.8)
486
+ fig, ax = plt.subplots(figsize=(6, fig_h), dpi=dpi)
487
+ fig.patch.set_facecolor("white")
488
+ ax.set_facecolor("white")
489
+ ax.set_xlim(0, 10)
490
+ ax.set_ylim(-0.6, n_nodes * STEP_Y + 0.2)
491
+ ax.invert_yaxis()
492
+ ax.axis("off")
493
+
494
+ acc_rgb = mcolors.to_rgb(accent)
495
+ dark_rgb = mcolors.to_rgb(dark)
496
+ muted_rgb = mcolors.to_rgb(muted)
497
+
498
+ # Node positions (cx, cy) — preserves input order
499
+ pos = {nid: (CX, i * STEP_Y) for nid, i in order.items()}
500
+
501
+ # ── Draw edges (behind nodes) ──────────────────────────────────────────
502
+ for edge in edges:
503
+ src, dst = edge.get("from"), edge.get("to")
504
+ if src not in pos or dst not in pos:
505
+ continue
506
+ x1, y1 = pos[src]
507
+ x2, y2 = pos[dst]
508
+ lbl = edge.get("label", "")
509
+
510
+ src_shape = nodes.get(src, {}).get("shape", "rect")
511
+ dst_shape = nodes.get(dst, {}).get("shape", "rect")
512
+ dy_src = BOX_H * (0.80 if src_shape == "diamond" else 0.50)
513
+ dy_dst = BOX_H * (0.80 if dst_shape == "diamond" else 0.50)
514
+
515
+ y_start = y1 + dy_src
516
+ y_end = y2 - dy_dst
517
+
518
+ # Forward edge: straight; back-edge: curved arc
519
+ conn = "arc3,rad=0.0" if y_end > y_start + 0.01 else "arc3,rad=0.42"
520
+
521
+ ax.annotate("",
522
+ xy=(x2, y_end), xytext=(x1, y_start),
523
+ arrowprops=dict(
524
+ arrowstyle="-|>", color=muted_rgb,
525
+ lw=1.0, mutation_scale=10,
526
+ connectionstyle=conn,
527
+ ),
528
+ )
529
+ if lbl:
530
+ mid_x = (x1 + x2) / 2 + 0.28
531
+ mid_y = (y_start + y_end) / 2
532
+ ax.text(mid_x, mid_y, lbl, fontsize=7.5,
533
+ color=muted_rgb, ha="left", va="center")
534
+
535
+ # ── Draw nodes (in front of edges) ────────────────────────────────────
536
+ for nid, (cx, cy) in pos.items():
537
+ node = nodes[nid]
538
+ shape = node.get("shape", "rect")
539
+ label = node.get("label", nid)
540
+ left = cx - BOX_W / 2
541
+ bot = cy - BOX_H / 2
542
+
543
+ if shape in ("oval", "terminal"):
544
+ el = mpatch.Ellipse(
545
+ (cx, cy), BOX_W * 0.78, BOX_H * 1.15,
546
+ facecolor=acc_rgb, edgecolor=acc_rgb, linewidth=0,
547
+ )
548
+ ax.add_patch(el)
549
+ ax.text(cx, cy, label, ha="center", va="center",
550
+ fontsize=8.5, fontweight="bold", color="white")
551
+
552
+ elif shape == "diamond":
553
+ d = BOX_W * 0.44
554
+ diamond = plt.Polygon(
555
+ [(cx, cy - d * 0.72), (cx + d, cy),
556
+ (cx, cy + d * 0.72), (cx - d, cy)],
557
+ facecolor="#FFFCF0",
558
+ edgecolor=accent, linewidth=1.2,
559
+ )
560
+ ax.add_patch(diamond)
561
+ ax.text(cx, cy, label, ha="center", va="center",
562
+ fontsize=8, color=dark_rgb)
563
+
564
+ elif shape == "parallelogram":
565
+ skew = 0.30
566
+ para = plt.Polygon(
567
+ [(left + skew, bot), (left + BOX_W + skew, bot),
568
+ (left + BOX_W, bot + BOX_H), (left, bot + BOX_H)],
569
+ facecolor="white",
570
+ edgecolor=accent, linewidth=1.2,
571
+ )
572
+ ax.add_patch(para)
573
+ ax.text(cx, cy, label, ha="center", va="center",
574
+ fontsize=8.5, color=dark_rgb)
575
+
576
+ else: # rect (default)
577
+ rect = FancyBboxPatch(
578
+ (left, bot), BOX_W, BOX_H,
579
+ boxstyle="round,pad=0.04",
580
+ facecolor="white",
581
+ edgecolor=accent, linewidth=1.2,
582
+ )
583
+ ax.add_patch(rect)
584
+ ax.text(cx, cy, label, ha="center", va="center",
585
+ fontsize=8.5, color=dark_rgb)
586
+
587
+ plt.tight_layout(pad=0.2)
588
+ buf = io.BytesIO()
589
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
590
+ facecolor="white", pad_inches=0.08)
591
+ plt.close(fig)
592
+ buf.seek(0)
593
+ return buf.read()
594
+ except Exception:
595
+ return None
596
+
597
+
598
+ # ══════════════════════════════════════════════════════════════════════════════
599
+ # Block renderers
600
+ #
601
+ # All functions share the same signature:
602
+ # _add_XXX(story: list, item: dict, ctx: dict)
603
+ #
604
+ # ctx keys:
605
+ # tokens dict design tokens from palette.py
606
+ # styles dict ParagraphStyle objects from make_styles()
607
+ # usable_w float usable page width in points
608
+ # acc str accent hex color
609
+ # acc_lt str light accent hex color
610
+ # mu str muted hex color
611
+ # dark str dark hex color
612
+ # figure_n int auto-incrementing figure counter (mutable)
613
+ # numbered_n int auto-incrementing list counter (mutable)
614
+ # ══════════════════════════════════════════════════════════════════════════════
615
+
616
+ def _add_heading(story: list, item: dict, ctx: dict, level: int):
617
+ key = f"h{level}"
618
+ para = Paragraph(item["text"], ctx["styles"][key])
619
+ if level == 1:
620
+ story.append(KeepTogether([para, _divider(ctx["acc"])]))
621
+ else:
622
+ story.append(para)
623
+
624
+
625
+ def _add_body(story: list, item: dict, ctx: dict):
626
+ story.append(Paragraph(item["text"], ctx["styles"]["body"]))
627
+
628
+
629
+ def _add_bullet(story: list, item: dict, ctx: dict):
630
+ story.append(Paragraph(
631
+ f"\u2022\u2002{item['text']}", ctx["styles"]["bullet"]
632
+ ))
633
+
634
+
635
+ def _add_numbered(story: list, item: dict, ctx: dict):
636
+ ctx["numbered_n"] += 1
637
+ story.append(Paragraph(
638
+ f"{ctx['numbered_n']}.\u2002{item['text']}",
639
+ ctx["styles"]["numbered"],
640
+ ))
641
+
642
+
643
+ def _add_callout(story: list, item: dict, ctx: dict):
644
+ story.append(Spacer(1, 8))
645
+ story.append(CalloutBox(
646
+ item["text"], ctx["styles"]["callout"], ctx["acc"], ctx["acc_lt"]
647
+ ))
648
+ story.append(Spacer(1, 8))
649
+
650
+
651
+ def _add_table(story: list, item: dict, ctx: dict):
652
+ t = ctx["tokens"]
653
+ styles = ctx["styles"]
654
+ usable_w = ctx["usable_w"]
655
+ acc = ctx["acc"]
656
+ acc_lt = ctx["acc_lt"]
657
+
658
+ headers = [Paragraph(h, styles["table_header"]) for h in item["headers"]]
659
+ rows = [
660
+ [Paragraph(str(c), styles["table_cell"]) for c in row]
661
+ for row in item.get("rows", [])
662
+ ]
663
+ n_cols = len(item["headers"])
664
+
665
+ # Optional col_widths as fractions summing to 1.0
666
+ if "col_widths" in item and len(item["col_widths"]) == n_cols:
667
+ col_w = [usable_w * f for f in item["col_widths"]]
668
+ else:
669
+ col_w = [usable_w / n_cols] * n_cols
670
+
671
+ tbl = Table([headers] + rows, colWidths=col_w)
672
+ tbl.setStyle(TableStyle([
673
+ ("BACKGROUND", (0, 0), (-1, 0), HexColor(acc)),
674
+ ("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#FFFFFF")),
675
+ ("FONTNAME", (0, 0), (-1, 0), t["font_body_b_rl"]),
676
+ ("FONTSIZE", (0, 0), (-1, 0), 9.5),
677
+ ("TOPPADDING", (0, 0), (-1, 0), 7),
678
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
679
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1),
680
+ [HexColor("#FFFFFF"), HexColor(acc_lt)]),
681
+ ("FONTNAME", (0, 1), (-1, -1), t["font_body_rl"]),
682
+ ("FONTSIZE", (0, 1), (-1, -1), 9.5),
683
+ ("TOPPADDING", (0, 1), (-1, -1), 6),
684
+ ("BOTTOMPADDING", (0, 1), (-1, -1), 6),
685
+ ("LEFTPADDING", (0, 0), (-1, -1), 10),
686
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
687
+ ("BOX", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
688
+ ("LINEBELOW", (0, 0), (-1, 0), 1.2, HexColor(acc)),
689
+ ("TEXTCOLOR", (0, 1), (-1, -1), HexColor(t["body_text"])),
690
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
691
+ ]))
692
+ story.append(tbl)
693
+ if item.get("caption"):
694
+ story.append(Spacer(1, 4))
695
+ story.append(Paragraph(item["caption"], styles["caption"]))
696
+ story.append(Spacer(1, 12))
697
+
698
+
699
+ def _add_image(story: list, item: dict, ctx: dict):
700
+ path = str(item.get("path", item.get("src", "")))
701
+ if not os.path.exists(path):
702
+ story.append(Paragraph(
703
+ f"[Image not found: {path}]", ctx["styles"]["caption"]
704
+ ))
705
+ return
706
+ try:
707
+ img = RLImage(path)
708
+ uw = ctx["usable_w"]
709
+ if img.drawWidth > uw:
710
+ scale = uw / img.drawWidth
711
+ img.drawWidth = uw
712
+ img.drawHeight = img.drawHeight * scale
713
+ story.append(img)
714
+ except Exception as e:
715
+ story.append(Paragraph(f"[Image error: {e}]", ctx["styles"]["caption"]))
716
+ return
717
+ if item.get("caption"):
718
+ story.append(Spacer(1, 4))
719
+ story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
720
+ story.append(Spacer(1, 8))
721
+
722
+
723
+ def _add_figure(story: list, item: dict, ctx: dict):
724
+ """Like image but auto-numbers the caption as 'Figure N: ...'."""
725
+ ctx["figure_n"] += 1
726
+ raw_cap = item.get("caption", "")
727
+ caption = f"Figure {ctx['figure_n']}: {raw_cap}" if raw_cap \
728
+ else f"Figure {ctx['figure_n']}"
729
+ _add_image(story, {**item, "caption": caption}, ctx)
730
+
731
+
732
+ def _add_code(story: list, item: dict, ctx: dict):
733
+ acc = ctx["acc"]
734
+ acc_lt = ctx["acc_lt"]
735
+ mu = ctx["mu"]
736
+ uw = ctx["usable_w"]
737
+ lang = item.get("language", "")
738
+
739
+ pre = Preformatted(item.get("text", ""), ctx["styles"]["code"])
740
+ tbl = Table([[pre]], colWidths=[uw])
741
+ tbl.setStyle(TableStyle([
742
+ ("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
743
+ ("LINEBEFORE", (0, 0), ( 0, -1), 3, HexColor(acc)),
744
+ ("BOX", (0, 0), (-1, -1), 0.5, HexColor(mu)),
745
+ ("LEFTPADDING", (0, 0), (-1, -1), 14),
746
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
747
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
748
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
749
+ ]))
750
+ story.append(Spacer(1, 6))
751
+ if lang:
752
+ story.append(Paragraph(lang.upper(), ctx["styles"]["code_lang"]))
753
+ story.append(tbl)
754
+ story.append(Spacer(1, 6))
755
+
756
+
757
+ def _add_math(story: list, item: dict, ctx: dict):
758
+ """
759
+ Display math block.
760
+
761
+ Fields:
762
+ text LaTeX math expression (without enclosing $)
763
+ label optional equation label, e.g. "(1)" — displayed right-aligned
764
+ caption optional caption below the formula
765
+
766
+ Example:
767
+ {"type": "math", "text": "E = mc^2", "label": "(1)"}
768
+ {"type": "math", "text": "\\\\int_0^\\\\infty e^{-x^2}\\\\,dx = \\\\frac{\\\\sqrt{\\\\pi}}{2}"}
769
+ """
770
+ acc = ctx["acc"]
771
+ acc_lt = ctx["acc_lt"]
772
+ uw = ctx["usable_w"]
773
+ expr = item.get("text", "").strip()
774
+ label = item.get("label", "").strip()
775
+
776
+ png = _render_math_png(expr)
777
+
778
+ if png is None:
779
+ # Graceful text fallback if matplotlib unavailable
780
+ story.append(Spacer(1, 6))
781
+ pre = Preformatted(f" {expr}", ctx["styles"]["math_fallback"])
782
+ tbl = Table([[pre]], colWidths=[uw])
783
+ tbl.setStyle(TableStyle([
784
+ ("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
785
+ ("LEFTPADDING", (0, 0), (-1, -1), 14),
786
+ ("RIGHTPADDING", (0, 0), (-1, -1), 14),
787
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
788
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
789
+ ]))
790
+ story.append(tbl)
791
+ story.append(Spacer(1, 6))
792
+ return
793
+
794
+ img = _image_from_bytes(png, uw, max_frac=0.72)
795
+ story.append(Spacer(1, 10))
796
+
797
+ if label:
798
+ label_w = 44
799
+ formula_w = uw - label_w
800
+ lbl_para = Paragraph(label, ctx["styles"]["eq_label"])
801
+ row_tbl = Table([[img, lbl_para]], colWidths=[formula_w, label_w])
802
+ row_tbl.setStyle(TableStyle([
803
+ ("ALIGN", (0, 0), (0, 0), "CENTER"),
804
+ ("ALIGN", (1, 0), (1, 0), "RIGHT"),
805
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
806
+ ]))
807
+ story.append(row_tbl)
808
+ else:
809
+ row_tbl = Table([[img]], colWidths=[uw])
810
+ row_tbl.setStyle(TableStyle([
811
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
812
+ ]))
813
+ story.append(row_tbl)
814
+
815
+ if item.get("caption"):
816
+ story.append(Spacer(1, 4))
817
+ story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
818
+ story.append(Spacer(1, 10))
819
+
820
+
821
+ def _add_chart(story: list, item: dict, ctx: dict):
822
+ """
823
+ Render a chart (bar / line / pie) via matplotlib.
824
+
825
+ Fields:
826
+ chart_type "bar" | "line" | "pie" (default "bar")
827
+ title chart title
828
+ labels list of category strings
829
+ datasets list of {label?, values: list[number]}
830
+ x_label X-axis label (bar/line)
831
+ y_label Y-axis label (bar/line)
832
+ caption caption text below chart
833
+ figure bool (default true) — prefix caption with "Figure N:"
834
+ """
835
+ uw = ctx["usable_w"]
836
+ png = _render_chart_png(item, ctx["acc"])
837
+
838
+ if png is None:
839
+ story.append(Paragraph(
840
+ "[Chart: install matplotlib to render — pip install matplotlib]",
841
+ ctx["styles"]["caption"],
842
+ ))
843
+ return
844
+
845
+ img = _image_from_bytes(png, uw, max_frac=0.95)
846
+ story.append(Spacer(1, 8))
847
+ row_tbl = Table([[img]], colWidths=[uw])
848
+ row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
849
+ story.append(row_tbl)
850
+
851
+ raw_cap = item.get("caption", "")
852
+ use_fig = item.get("figure", True)
853
+ if raw_cap or use_fig:
854
+ ctx["figure_n"] += 1
855
+ prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
856
+ story.append(Spacer(1, 4))
857
+ story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
858
+ story.append(Spacer(1, 10))
859
+
860
+
861
+ def _add_flowchart(story: list, item: dict, ctx: dict):
862
+ """
863
+ Render a flowchart via matplotlib.
864
+
865
+ Fields:
866
+ nodes list of {id, label, shape?}
867
+ shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
868
+ edges list of {from, to, label?}
869
+ caption caption below the diagram
870
+ figure bool (default true) — prefix caption with "Figure N:"
871
+ """
872
+ uw = ctx["usable_w"]
873
+ png = _render_flowchart_png(item, ctx["acc"], ctx["dark"], ctx["mu"])
874
+
875
+ if png is None:
876
+ story.append(Paragraph(
877
+ "[Flowchart: install matplotlib to render — pip install matplotlib]",
878
+ ctx["styles"]["caption"],
879
+ ))
880
+ return
881
+
882
+ img = _image_from_bytes(png, uw, max_frac=0.78)
883
+ story.append(Spacer(1, 8))
884
+ row_tbl = Table([[img]], colWidths=[uw])
885
+ row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
886
+ story.append(row_tbl)
887
+
888
+ raw_cap = item.get("caption", "")
889
+ use_fig = item.get("figure", True)
890
+ if raw_cap or use_fig:
891
+ ctx["figure_n"] += 1
892
+ prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
893
+ story.append(Spacer(1, 4))
894
+ story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
895
+ story.append(Spacer(1, 10))
896
+
897
+
898
+ def _add_bibliography(story: list, item: dict, ctx: dict):
899
+ """
900
+ Numbered reference list with hanging indent.
901
+
902
+ Fields:
903
+ title section heading (default "References"); set "" to suppress
904
+ items list of {id, text}
905
+
906
+ Example:
907
+ {"type": "bibliography",
908
+ "items": [
909
+ {"id": "1", "text": "Smith, J. (2023). Title. Journal, 10(2), 1–15."},
910
+ {"id": "2", "text": "Doe, A. (2022). Another title. Publisher."}
911
+ ]}
912
+ """
913
+ heading = item.get("title", "References")
914
+ if heading:
915
+ story.append(KeepTogether([
916
+ Paragraph(heading, ctx["styles"]["bib_title"]),
917
+ _divider(ctx["acc"]),
918
+ ]))
919
+
920
+ for ref in item.get("items", []):
921
+ story.append(Spacer(1, 4))
922
+ story.append(BibliographyItem(
923
+ str(ref.get("id", "")),
924
+ ref.get("text", ""),
925
+ ctx["styles"]["bib"],
926
+ ctx["dark"],
927
+ ))
928
+
929
+
930
+ # ══════════════════════════════════════════════════════════════════════════════
931
+ # Story builder
932
+ # ══════════════════════════════════════════════════════════════════════════════
933
+
934
+ # Block types that break a numbered list sequence
935
+ _RESETS_NUMBERED = frozenset({
936
+ "h1", "h2", "h3", "body", "bullet", "callout", "table",
937
+ "image", "figure", "code", "math", "chart", "flowchart",
938
+ "bibliography", "divider", "caption", "pagebreak", "spacer",
939
+ })
940
+
941
+
942
+ def build_story(content: list, tokens: dict, styles: dict) -> list:
943
+ usable_w = A4[0] - tokens["margin_left"] - tokens["margin_right"]
944
+
945
+ ctx: dict = {
946
+ "tokens": tokens,
947
+ "styles": styles,
948
+ "usable_w": usable_w,
949
+ "acc": tokens["accent"],
950
+ "acc_lt": tokens["accent_lt"],
951
+ "mu": tokens["muted"],
952
+ "dark": tokens["dark"],
953
+ "figure_n": 0,
954
+ "numbered_n": 0,
955
+ }
956
+
957
+ story: list = []
958
+
959
+ for item in content:
960
+ kind = item.get("type", "body")
961
+
962
+ if kind in _RESETS_NUMBERED:
963
+ ctx["numbered_n"] = 0
964
+
965
+ if kind == "h1": _add_heading(story, item, ctx, 1)
966
+ elif kind == "h2": _add_heading(story, item, ctx, 2)
967
+ elif kind == "h3": _add_heading(story, item, ctx, 3)
968
+ elif kind == "body": _add_body(story, item, ctx)
969
+ elif kind == "bullet": _add_bullet(story, item, ctx)
970
+ elif kind == "numbered": _add_numbered(story, item, ctx)
971
+ elif kind == "callout": _add_callout(story, item, ctx)
972
+ elif kind == "table": _add_table(story, item, ctx)
973
+ elif kind == "image": _add_image(story, item, ctx)
974
+ elif kind == "figure": _add_figure(story, item, ctx)
975
+ elif kind == "code": _add_code(story, item, ctx)
976
+ elif kind == "math": _add_math(story, item, ctx)
977
+ elif kind == "chart": _add_chart(story, item, ctx)
978
+ elif kind == "flowchart": _add_flowchart(story, item, ctx)
979
+ elif kind == "bibliography": _add_bibliography(story, item, ctx)
980
+ elif kind == "divider": story.append(_divider(ctx["acc"]))
981
+ elif kind == "caption":
982
+ story.append(Paragraph(item["text"], styles["caption"]))
983
+ elif kind == "pagebreak": story.append(PageBreak())
984
+ elif kind == "spacer": story.append(Spacer(1, item.get("pt", 12)))
985
+
986
+ return story
987
+
988
+
989
+ # ══════════════════════════════════════════════════════════════════════════════
990
+ # Main build
991
+ # ══════════════════════════════════════════════════════════════════════════════
992
+
993
+ def build(tokens: dict, content: list, out_path: str) -> dict:
994
+ register_fonts(tokens)
995
+ styles = make_styles(tokens)
996
+
997
+ doc = BeautifulDoc(
998
+ out_path, tokens,
999
+ pagesize=A4,
1000
+ leftMargin=tokens["margin_left"],
1001
+ rightMargin=tokens["margin_right"],
1002
+ topMargin=tokens["margin_top"],
1003
+ bottomMargin=tokens["margin_bottom"],
1004
+ )
1005
+ doc.build(build_story(content, tokens, styles))
1006
+
1007
+ size = os.path.getsize(out_path)
1008
+ return {"status": "ok", "out": out_path, "size_kb": size // 1024}
1009
+
1010
+
1011
+ # ══════════════════════════════════════════════════════════════════════════════
1012
+ # CLI
1013
+ # ══════════════════════════════════════════════════════════════════════════════
1014
+
1015
+ def main():
1016
+ parser = argparse.ArgumentParser(
1017
+ description="Render body PDF from tokens.json + content.json"
1018
+ )
1019
+ parser.add_argument("--tokens", default="tokens.json")
1020
+ parser.add_argument("--content", default="content.json")
1021
+ parser.add_argument("--out", default="body.pdf")
1022
+ args = parser.parse_args()
1023
+
1024
+ for fpath in (args.tokens, args.content):
1025
+ if not os.path.exists(fpath):
1026
+ print(
1027
+ json.dumps({"status": "error",
1028
+ "error": f"File not found: {fpath}"}),
1029
+ file=sys.stderr,
1030
+ )
1031
+ sys.exit(1)
1032
+
1033
+ with open(args.tokens, encoding="utf-8") as f:
1034
+ tokens = json.load(f)
1035
+ with open(args.content, encoding="utf-8") as f:
1036
+ content = json.load(f)
1037
+
1038
+ try:
1039
+ result = build(tokens, content, args.out)
1040
+ print(json.dumps(result))
1041
+ except Exception as e:
1042
+ import traceback
1043
+ print(
1044
+ json.dumps({
1045
+ "status": "error",
1046
+ "error": str(e),
1047
+ "trace": traceback.format_exc(),
1048
+ }),
1049
+ file=sys.stderr,
1050
+ )
1051
+ sys.exit(3)
1052
+
1053
+
1054
+ if __name__ == "__main__":
1055
+ main()