@nguyenphp/antigravity-marketing 1.0.16 → 1.0.19

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 (376) hide show
  1. package/README.md +187 -74
  2. package/bin/index.js +4 -4
  3. package/package.json +4 -3
  4. package/templates/.agent/agents/backend-specialist.md +263 -0
  5. package/templates/.agent/agents/database-architect.md +226 -0
  6. package/templates/.agent/agents/debugger.md +225 -0
  7. package/templates/.agent/agents/devops-engineer.md +242 -0
  8. package/templates/.agent/agents/documentation-writer.md +104 -0
  9. package/templates/.agent/agents/explorer-agent.md +73 -0
  10. package/templates/.agent/agents/frontend-specialist.md +527 -0
  11. package/templates/.agent/agents/game-developer.md +162 -0
  12. package/templates/.agent/agents/mobile-developer.md +377 -0
  13. package/templates/.agent/agents/orchestrator.md +400 -0
  14. package/templates/.agent/agents/penetration-tester.md +188 -0
  15. package/templates/.agent/agents/performance-optimizer.md +187 -0
  16. package/templates/.agent/agents/project-planner.md +403 -0
  17. package/templates/.agent/agents/security-auditor.md +170 -0
  18. package/templates/.agent/agents/seo-specialist.md +111 -0
  19. package/templates/.agent/agents/test-engineer.md +158 -0
  20. package/templates/.agent/rules/GEMINI.md +248 -0
  21. package/templates/.agent/skills/analytics-marketing/SKILL.md +172 -324
  22. package/templates/.agent/skills/api-patterns/SKILL.md +81 -0
  23. package/templates/.agent/skills/api-patterns/api-style.md +42 -0
  24. package/templates/.agent/skills/api-patterns/auth.md +24 -0
  25. package/templates/.agent/skills/api-patterns/documentation.md +26 -0
  26. package/templates/.agent/skills/api-patterns/graphql.md +41 -0
  27. package/templates/.agent/skills/api-patterns/rate-limiting.md +31 -0
  28. package/templates/.agent/skills/api-patterns/response.md +37 -0
  29. package/templates/.agent/skills/api-patterns/rest.md +40 -0
  30. package/templates/.agent/skills/api-patterns/scripts/api_validator.py +211 -0
  31. package/templates/.agent/skills/api-patterns/security-testing.md +122 -0
  32. package/templates/.agent/skills/api-patterns/trpc.md +41 -0
  33. package/templates/.agent/skills/api-patterns/versioning.md +22 -0
  34. package/templates/.agent/skills/app-builder/SKILL.md +75 -0
  35. package/templates/.agent/skills/app-builder/agent-coordination.md +71 -0
  36. package/templates/.agent/skills/app-builder/feature-building.md +53 -0
  37. package/templates/.agent/skills/app-builder/project-detection.md +34 -0
  38. package/templates/.agent/skills/app-builder/scaffolding.md +118 -0
  39. package/templates/.agent/skills/app-builder/tech-stack.md +40 -0
  40. package/templates/.agent/skills/app-builder/templates/SKILL.md +39 -0
  41. package/templates/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  42. package/templates/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  43. package/templates/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  44. package/templates/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  45. package/templates/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  46. package/templates/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  47. package/templates/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  48. package/templates/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +82 -0
  49. package/templates/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +100 -0
  50. package/templates/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +106 -0
  51. package/templates/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +101 -0
  52. package/templates/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  53. package/templates/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +93 -0
  54. package/templates/.agent/skills/architecture/SKILL.md +55 -0
  55. package/templates/.agent/skills/architecture/context-discovery.md +43 -0
  56. package/templates/.agent/skills/architecture/examples.md +94 -0
  57. package/templates/.agent/skills/architecture/pattern-selection.md +68 -0
  58. package/templates/.agent/skills/architecture/patterns-reference.md +50 -0
  59. package/templates/.agent/skills/architecture/trade-off-analysis.md +77 -0
  60. package/templates/.agent/skills/banner-design/SKILL.md +192 -0
  61. package/templates/.agent/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  62. package/templates/.agent/skills/bash-linux/SKILL.md +199 -0
  63. package/templates/.agent/skills/behavioral-modes/SKILL.md +242 -0
  64. package/templates/.agent/skills/brainstorming/SKILL.md +163 -0
  65. package/templates/.agent/skills/brainstorming/dynamic-questioning.md +350 -0
  66. package/templates/.agent/skills/brand/SKILL.md +97 -0
  67. package/templates/.agent/skills/brand/references/approval-checklist.md +169 -0
  68. package/templates/.agent/skills/brand/references/asset-organization.md +157 -0
  69. package/templates/.agent/skills/brand/references/brand-guideline-template.md +140 -0
  70. package/templates/.agent/skills/brand/references/color-palette-management.md +186 -0
  71. package/templates/.agent/skills/brand/references/consistency-checklist.md +94 -0
  72. package/templates/.agent/skills/brand/references/logo-usage-rules.md +185 -0
  73. package/templates/.agent/skills/brand/references/messaging-framework.md +85 -0
  74. package/templates/.agent/skills/brand/references/typography-specifications.md +214 -0
  75. package/templates/.agent/skills/brand/references/update.md +118 -0
  76. package/templates/.agent/skills/brand/references/visual-identity.md +96 -0
  77. package/templates/.agent/skills/brand/references/voice-framework.md +88 -0
  78. package/templates/.agent/skills/brand/scripts/extract-colors.cjs +341 -0
  79. package/templates/.agent/skills/brand/scripts/inject-brand-context.cjs +349 -0
  80. package/templates/.agent/skills/brand/scripts/sync-brand-to-tokens.cjs +266 -0
  81. package/templates/.agent/skills/brand/scripts/validate-asset.cjs +387 -0
  82. package/templates/.agent/skills/brand/templates/brand-guidelines-starter.md +275 -0
  83. package/templates/.agent/skills/clean-code/SKILL.md +201 -0
  84. package/templates/.agent/skills/code-review-checklist/SKILL.md +109 -0
  85. package/templates/.agent/skills/copywriting/SKILL.md +250 -0
  86. package/templates/.agent/skills/database-design/SKILL.md +52 -0
  87. package/templates/.agent/skills/database-design/database-selection.md +43 -0
  88. package/templates/.agent/skills/database-design/indexing.md +39 -0
  89. package/templates/.agent/skills/database-design/migrations.md +48 -0
  90. package/templates/.agent/skills/database-design/optimization.md +36 -0
  91. package/templates/.agent/skills/database-design/orm-selection.md +30 -0
  92. package/templates/.agent/skills/database-design/schema-design.md +56 -0
  93. package/templates/.agent/skills/database-design/scripts/schema_validator.py +172 -0
  94. package/templates/.agent/skills/deployment-procedures/SKILL.md +241 -0
  95. package/templates/.agent/skills/docker-expert/SKILL.md +409 -0
  96. package/templates/.agent/skills/frontend-design/animation-guide.md +331 -0
  97. package/templates/.agent/skills/frontend-design/color-system.md +311 -0
  98. package/templates/.agent/skills/frontend-design/decision-trees.md +418 -0
  99. package/templates/.agent/skills/frontend-design/motion-graphics.md +306 -0
  100. package/templates/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  101. package/templates/.agent/skills/frontend-design/scripts/ux_audit.py +722 -0
  102. package/templates/.agent/skills/frontend-design/typography-system.md +345 -0
  103. package/templates/.agent/skills/frontend-design/ux-psychology.md +541 -0
  104. package/templates/.agent/skills/frontend-design/visual-effects.md +383 -0
  105. package/templates/.agent/skills/frontend-slides/SKILL.md +92 -0
  106. package/templates/.agent/skills/frontend-slides/STYLE_PRESETS.md +347 -0
  107. package/templates/.agent/skills/frontend-slides/animation-patterns.md +110 -0
  108. package/templates/.agent/skills/frontend-slides/examples/n8n-jupviec-automation.html +789 -0
  109. package/templates/.agent/skills/frontend-slides/examples/n8n-jupviec-automation.pptx +0 -0
  110. package/templates/.agent/skills/frontend-slides/html-template.md +347 -0
  111. package/templates/.agent/skills/frontend-slides/scripts/export-pptx.py +58 -0
  112. package/templates/.agent/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  113. package/templates/.agent/skills/frontend-slides/viewport-base.css +153 -0
  114. package/templates/.agent/skills/game-development/2d-games/SKILL.md +119 -0
  115. package/templates/.agent/skills/game-development/3d-games/SKILL.md +135 -0
  116. package/templates/.agent/skills/game-development/SKILL.md +167 -0
  117. package/templates/.agent/skills/game-development/game-art/SKILL.md +185 -0
  118. package/templates/.agent/skills/game-development/game-audio/SKILL.md +190 -0
  119. package/templates/.agent/skills/game-development/game-design/SKILL.md +129 -0
  120. package/templates/.agent/skills/game-development/mobile-games/SKILL.md +108 -0
  121. package/templates/.agent/skills/game-development/multiplayer/SKILL.md +132 -0
  122. package/templates/.agent/skills/game-development/pc-games/SKILL.md +144 -0
  123. package/templates/.agent/skills/game-development/vr-ar/SKILL.md +123 -0
  124. package/templates/.agent/skills/game-development/web-games/SKILL.md +150 -0
  125. package/templates/.agent/skills/geo-fundamentals/SKILL.md +156 -0
  126. package/templates/.agent/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
  127. package/templates/.agent/skills/growth-engine/SKILL.md +244 -0
  128. package/templates/.agent/skills/i18n-localization/SKILL.md +154 -0
  129. package/templates/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  130. package/templates/.agent/skills/lint-and-validate/SKILL.md +45 -0
  131. package/templates/.agent/skills/lint-and-validate/scripts/lint_runner.py +172 -0
  132. package/templates/.agent/skills/lint-and-validate/scripts/type_coverage.py +173 -0
  133. package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
  134. package/templates/.agent/skills/mcp-builder/SKILL.md +176 -0
  135. package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
  136. package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
  137. package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
  138. package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
  139. package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
  140. package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
  141. package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
  142. package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
  143. package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
  144. package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
  145. package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
  146. package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
  147. package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
  148. package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
  149. package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
  150. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
  151. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
  152. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
  153. package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
  154. package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
  155. package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
  156. package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
  157. package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
  158. package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
  159. package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
  160. package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
  161. package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
  162. package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
  163. package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
  164. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
  165. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
  166. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
  167. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
  168. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
  169. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
  170. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
  171. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
  172. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
  173. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
  174. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
  175. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
  176. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
  177. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
  178. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
  179. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
  180. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
  181. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
  182. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
  183. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
  184. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
  185. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
  186. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
  187. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
  188. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
  189. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
  190. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
  191. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
  192. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
  193. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
  194. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
  195. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
  196. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
  197. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
  198. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
  199. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
  200. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
  201. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
  202. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
  203. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
  204. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
  205. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
  206. package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
  207. package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
  208. package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
  209. package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
  210. package/templates/.agent/skills/minimax-pdf/README.md +222 -0
  211. package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
  212. package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
  213. package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
  214. package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
  215. package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
  216. package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
  217. package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
  218. package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
  219. package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
  220. package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
  221. package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
  222. package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
  223. package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
  224. package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
  225. package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
  226. package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
  227. package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
  228. package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
  229. package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
  230. package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
  231. package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
  232. package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
  233. package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
  234. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
  235. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
  236. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
  237. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
  238. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
  239. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
  240. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
  241. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
  242. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
  243. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
  244. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
  245. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
  246. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
  247. package/templates/.agent/skills/mobile-design/SKILL.md +394 -0
  248. package/templates/.agent/skills/mobile-design/decision-trees.md +516 -0
  249. package/templates/.agent/skills/mobile-design/mobile-backend.md +491 -0
  250. package/templates/.agent/skills/mobile-design/mobile-color-system.md +420 -0
  251. package/templates/.agent/skills/mobile-design/mobile-debugging.md +122 -0
  252. package/templates/.agent/skills/mobile-design/mobile-design-thinking.md +357 -0
  253. package/templates/.agent/skills/mobile-design/mobile-navigation.md +458 -0
  254. package/templates/.agent/skills/mobile-design/mobile-performance.md +767 -0
  255. package/templates/.agent/skills/mobile-design/mobile-testing.md +356 -0
  256. package/templates/.agent/skills/mobile-design/mobile-typography.md +433 -0
  257. package/templates/.agent/skills/mobile-design/platform-android.md +666 -0
  258. package/templates/.agent/skills/mobile-design/platform-ios.md +561 -0
  259. package/templates/.agent/skills/mobile-design/scripts/mobile_audit.py +670 -0
  260. package/templates/.agent/skills/mobile-design/touch-psychology.md +537 -0
  261. package/templates/.agent/skills/nestjs-expert/SKILL.md +552 -0
  262. package/templates/.agent/skills/nextjs-best-practices/SKILL.md +203 -0
  263. package/templates/.agent/skills/nodejs-best-practices/SKILL.md +333 -0
  264. package/templates/.agent/skills/parallel-agents/SKILL.md +175 -0
  265. package/templates/.agent/skills/performance-profiling/SKILL.md +143 -0
  266. package/templates/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
  267. package/templates/.agent/skills/plan-writing/SKILL.md +152 -0
  268. package/templates/.agent/skills/powershell-windows/SKILL.md +167 -0
  269. package/templates/.agent/skills/ppc-advertising/SKILL.md +183 -475
  270. package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
  271. package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
  272. package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
  273. package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
  274. package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
  275. package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
  276. package/templates/.agent/skills/prisma-expert/SKILL.md +355 -0
  277. package/templates/.agent/skills/python-patterns/SKILL.md +441 -0
  278. package/templates/.agent/skills/react-patterns/SKILL.md +198 -0
  279. package/templates/.agent/skills/red-team-tactics/SKILL.md +199 -0
  280. package/templates/.agent/skills/remotion-best-practices/SKILL.md +45 -111
  281. package/templates/.agent/skills/remotion-best-practices/rules/3d.md +4 -4
  282. package/templates/.agent/skills/remotion-best-practices/rules/animations.md +5 -7
  283. package/templates/.agent/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  284. package/templates/.agent/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  285. package/templates/.agent/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +103 -0
  286. package/templates/.agent/skills/remotion-best-practices/rules/assets.md +78 -0
  287. package/templates/.agent/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
  288. package/templates/.agent/skills/remotion-best-practices/rules/audio.md +1 -4
  289. package/templates/.agent/skills/remotion-best-practices/rules/calculate-metadata.md +47 -17
  290. package/templates/.agent/skills/remotion-best-practices/rules/can-decode.md +75 -0
  291. package/templates/.agent/skills/remotion-best-practices/rules/charts.md +80 -48
  292. package/templates/.agent/skills/remotion-best-practices/rules/compositions.md +22 -14
  293. package/templates/.agent/skills/remotion-best-practices/rules/display-captions.md +79 -21
  294. package/templates/.agent/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  295. package/templates/.agent/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
  296. package/templates/.agent/skills/remotion-best-practices/rules/fonts.md +96 -54
  297. package/templates/.agent/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  298. package/templates/.agent/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  299. package/templates/.agent/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
  300. package/templates/.agent/skills/remotion-best-practices/rules/gifs.md +21 -18
  301. package/templates/.agent/skills/remotion-best-practices/rules/images.md +6 -2
  302. package/templates/.agent/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  303. package/templates/.agent/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  304. package/templates/.agent/skills/remotion-best-practices/rules/lottie.md +10 -7
  305. package/templates/.agent/skills/remotion-best-practices/rules/maps.md +412 -0
  306. package/templates/.agent/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  307. package/templates/.agent/skills/remotion-best-practices/rules/measuring-text.md +140 -0
  308. package/templates/.agent/skills/remotion-best-practices/rules/parameters.md +109 -0
  309. package/templates/.agent/skills/remotion-best-practices/rules/sequencing.md +13 -1
  310. package/templates/.agent/skills/remotion-best-practices/rules/sfx.md +26 -0
  311. package/templates/.agent/skills/remotion-best-practices/rules/subtitles.md +36 -0
  312. package/templates/.agent/skills/remotion-best-practices/rules/tailwind.md +11 -0
  313. package/templates/.agent/skills/remotion-best-practices/rules/text-animations.md +4 -115
  314. package/templates/.agent/skills/remotion-best-practices/rules/timing.md +19 -19
  315. package/templates/.agent/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  316. package/templates/.agent/skills/remotion-best-practices/rules/transitions.md +117 -42
  317. package/templates/.agent/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  318. package/templates/.agent/skills/remotion-best-practices/rules/trimming.md +51 -0
  319. package/templates/.agent/skills/remotion-best-practices/rules/voiceover.md +99 -0
  320. package/templates/.agent/skills/seo-fundamentals/SKILL.md +83 -441
  321. package/templates/.agent/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
  322. package/templates/.agent/skills/server-management/SKILL.md +161 -0
  323. package/templates/.agent/skills/systematic-debugging/SKILL.md +109 -0
  324. package/templates/.agent/skills/tdd-workflow/SKILL.md +149 -0
  325. package/templates/.agent/skills/testing-patterns/SKILL.md +178 -0
  326. package/templates/.agent/skills/testing-patterns/scripts/test_runner.py +219 -0
  327. package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
  328. package/templates/.agent/skills/typescript-expert/SKILL.md +429 -0
  329. package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +1 -1
  330. package/templates/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  331. package/templates/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  332. package/templates/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  333. package/templates/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  334. package/templates/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  335. package/templates/.agent/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  336. package/templates/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  337. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  338. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  339. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  340. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  341. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  342. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  343. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  344. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  345. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  346. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  347. package/templates/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  348. package/templates/.agent/skills/ui-ux-pro-max/data/styles.csv +59 -0
  349. package/templates/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  350. package/templates/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  351. package/templates/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  352. package/templates/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  353. package/templates/.agent/skills/ui-ux-pro-max/scripts/core.py +257 -0
  354. package/templates/.agent/skills/ui-ux-pro-max/scripts/design_system.py +487 -0
  355. package/templates/.agent/skills/ui-ux-pro-max/scripts/search.py +76 -0
  356. package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
  357. package/templates/.agent/skills/vue-expert/SKILL.md +374 -0
  358. package/templates/.agent/skills/vulnerability-scanner/SKILL.md +276 -0
  359. package/templates/.agent/skills/vulnerability-scanner/checklists.md +121 -0
  360. package/templates/.agent/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
  361. package/templates/.agent/skills/webapp-testing/SKILL.md +187 -0
  362. package/templates/.agent/skills/webapp-testing/scripts/playwright_runner.py +173 -0
  363. package/templates/.agent/workflows/analyze.md +3 -0
  364. package/templates/.agent/workflows/brainstorm.md +113 -0
  365. package/templates/.agent/workflows/brand-report.md +44 -0
  366. package/templates/.agent/workflows/create.md +59 -0
  367. package/templates/.agent/workflows/debug.md +103 -0
  368. package/templates/.agent/workflows/deploy.md +176 -0
  369. package/templates/.agent/workflows/enhance.md +63 -0
  370. package/templates/.agent/workflows/orchestrate.md +237 -0
  371. package/templates/.agent/workflows/plan.md +89 -0
  372. package/templates/.agent/workflows/preview.md +80 -0
  373. package/templates/.agent/workflows/report.md +49 -0
  374. package/templates/.agent/workflows/status.md +86 -0
  375. package/templates/.agent/workflows/test.md +144 -0
  376. package/templates/.agent/workflows/ui-ux-pro-max.md +231 -0
@@ -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()