@rosetears/aili-pi 0.1.0

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 (531) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/THIRD_PARTY_NOTICES.md +64 -0
  4. package/extensions/index.ts +6 -0
  5. package/install.sh +5 -0
  6. package/manifests/adapter-evidence.json +45 -0
  7. package/manifests/capabilities.json +124 -0
  8. package/manifests/live-verification.json +26 -0
  9. package/manifests/provenance.json +71 -0
  10. package/manifests/roles.json +474 -0
  11. package/manifests/sbom.json +7797 -0
  12. package/manifests/skill-compatibility.json +8100 -0
  13. package/manifests/subagent-provenance.json +37 -0
  14. package/package.json +91 -0
  15. package/prompts/build.md +8 -0
  16. package/prompts/define.md +8 -0
  17. package/prompts/ideate.md +8 -0
  18. package/prompts/local-review.md +8 -0
  19. package/prompts/ship.md +8 -0
  20. package/roles/agent-evaluator.md +47 -0
  21. package/roles/ai-regression-scout.md +47 -0
  22. package/roles/browser-qa-runner.md +47 -0
  23. package/roles/code-reviewer.md +47 -0
  24. package/roles/code-scout.md +47 -0
  25. package/roles/convergence-reviewer.md +55 -0
  26. package/roles/doc-researcher.md +47 -0
  27. package/roles/e2e-artifact-runner.md +47 -0
  28. package/roles/implementer.md +47 -0
  29. package/roles/opensource-sanitizer.md +47 -0
  30. package/roles/plan-auditor.md +47 -0
  31. package/roles/pr-test-analyzer.md +47 -0
  32. package/roles/security-auditor.md +47 -0
  33. package/roles/silent-failure-reviewer.md +47 -0
  34. package/roles/spec-miner.md +47 -0
  35. package/roles/test-coverage-reviewer.md +47 -0
  36. package/roles/test-engineer.md +48 -0
  37. package/roles/web-performance-auditor.md +47 -0
  38. package/roles/web-researcher.md +47 -0
  39. package/scripts/apply-adapter-evidence.ts +73 -0
  40. package/scripts/bootstrap.sh +180 -0
  41. package/scripts/generate-provenance.ts +143 -0
  42. package/scripts/local-package-e2e.ts +51 -0
  43. package/scripts/sync-roles.ts +213 -0
  44. package/scripts/sync-skills.ts +356 -0
  45. package/scripts/validate-runtime.ts +11 -0
  46. package/skills/academic-paper-review/SKILL.md +81 -0
  47. package/skills/agents-md-initialization/SKILL.md +121 -0
  48. package/skills/agents-md-initialization/references/agents-template.md +83 -0
  49. package/skills/agents-md-initialization/references/agents_md.py +215 -0
  50. package/skills/ai-regression-scout/SKILL.md +24 -0
  51. package/skills/aili-delivery-flow/SKILL.md +125 -0
  52. package/skills/aili-delivery-flow/references/artifact-contracts.md +138 -0
  53. package/skills/aili-delivery-flow/references/backend-routing.md +40 -0
  54. package/skills/aili-delivery-flow/references/build-execution-loop.md +113 -0
  55. package/skills/aili-delivery-flow/references/build-goal-mode.md +3 -0
  56. package/skills/aili-delivery-flow/references/direct-vs-delegated-work.md +38 -0
  57. package/skills/aili-delivery-flow/references/implementation-packages.md +54 -0
  58. package/skills/aili-delivery-flow/references/lifecycle.md +127 -0
  59. package/skills/aili-delivery-flow/references/protocols/acceptance-test-plan.md +18 -0
  60. package/skills/aili-delivery-flow/references/protocols/alignment-questionnaire.md +10 -0
  61. package/skills/aili-delivery-flow/references/protocols/closeout-report.md +101 -0
  62. package/skills/aili-delivery-flow/references/protocols/compact-evidence-pack.md +46 -0
  63. package/skills/aili-delivery-flow/references/protocols/idea-brief.md +10 -0
  64. package/skills/aili-delivery-flow/references/protocols/implementation-package.md +53 -0
  65. package/skills/aili-delivery-flow/references/protocols/research-evidence-pack.md +19 -0
  66. package/skills/aili-delivery-flow/references/protocols/review-report.md +7 -0
  67. package/skills/aili-delivery-flow/references/protocols/spec-draft.md +12 -0
  68. package/skills/aili-delivery-flow/references/protocols/subagent-result.md +60 -0
  69. package/skills/aili-delivery-flow/references/protocols/subagent-task-packet.md +29 -0
  70. package/skills/aili-delivery-flow/references/protocols/worktree-context.md +109 -0
  71. package/skills/aili-delivery-flow/references/questionnaire-policy.md +62 -0
  72. package/skills/aili-delivery-flow/references/review-repair-loop.md +16 -0
  73. package/skills/aili-delivery-flow/references/test-document-policy.md +35 -0
  74. package/skills/android-native-dev/SKILL.md +722 -0
  75. package/skills/android-native-dev/references/accessibility.md +209 -0
  76. package/skills/android-native-dev/references/adaptive-screens.md +231 -0
  77. package/skills/android-native-dev/references/design-style-guide.md +365 -0
  78. package/skills/android-native-dev/references/functional-requirements.md +229 -0
  79. package/skills/android-native-dev/references/motion-system.md +203 -0
  80. package/skills/android-native-dev/references/performance-stability.md +223 -0
  81. package/skills/android-native-dev/references/privacy-security.md +244 -0
  82. package/skills/android-native-dev/references/testing.md +554 -0
  83. package/skills/android-native-dev/references/visual-design.md +246 -0
  84. package/skills/api-and-interface-design/SKILL.md +318 -0
  85. package/skills/browser-qa/SKILL.md +32 -0
  86. package/skills/browser-testing-with-devtools/SKILL.md +323 -0
  87. package/skills/build-failure-repair/SKILL.md +49 -0
  88. package/skills/chart-visualization/SKILL.md +73 -0
  89. package/skills/ci-cd-and-automation/SKILL.md +353 -0
  90. package/skills/code-review-and-quality/SKILL.md +375 -0
  91. package/skills/code-review-quality-gates/SKILL.md +132 -0
  92. package/skills/code-simplification/SKILL.md +371 -0
  93. package/skills/comment-accuracy-review/SKILL.md +37 -0
  94. package/skills/consulting-analysis/SKILL.md +69 -0
  95. package/skills/context-engineering/SKILL.md +375 -0
  96. package/skills/coverage-review/SKILL.md +24 -0
  97. package/skills/data-analysis/SKILL.md +74 -0
  98. package/skills/deprecation-and-migration/SKILL.md +222 -0
  99. package/skills/documentation-and-adrs/SKILL.md +330 -0
  100. package/skills/e2e-artifact-handling/SKILL.md +30 -0
  101. package/skills/evidence-scoped-retrospective/SKILL.md +156 -0
  102. package/skills/explain-by-allegory/SKILL.md +99 -0
  103. package/skills/flutter-dev/SKILL.md +162 -0
  104. package/skills/flutter-dev/references/animations.md +497 -0
  105. package/skills/flutter-dev/references/bloc-state.md +281 -0
  106. package/skills/flutter-dev/references/forms.md +656 -0
  107. package/skills/flutter-dev/references/gorouter-navigation.md +257 -0
  108. package/skills/flutter-dev/references/localization.md +510 -0
  109. package/skills/flutter-dev/references/networking.md +566 -0
  110. package/skills/flutter-dev/references/performance.md +305 -0
  111. package/skills/flutter-dev/references/platform-specific.md +417 -0
  112. package/skills/flutter-dev/references/project-structure.md +273 -0
  113. package/skills/flutter-dev/references/riverpod-state.md +232 -0
  114. package/skills/flutter-dev/references/testing.md +364 -0
  115. package/skills/flutter-dev/references/widget-patterns.md +233 -0
  116. package/skills/frontend-dev/SKILL.md +468 -0
  117. package/skills/frontend-dev/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  118. package/skills/frontend-dev/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  119. package/skills/frontend-dev/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  120. package/skills/frontend-dev/canvas-fonts/BigShoulders-OFL.txt +93 -0
  121. package/skills/frontend-dev/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  122. package/skills/frontend-dev/canvas-fonts/Boldonse-OFL.txt +93 -0
  123. package/skills/frontend-dev/canvas-fonts/Boldonse-Regular.ttf +0 -0
  124. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  125. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  126. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  127. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  128. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  129. package/skills/frontend-dev/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  130. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  131. package/skills/frontend-dev/canvas-fonts/DMMono-OFL.txt +93 -0
  132. package/skills/frontend-dev/canvas-fonts/DMMono-Regular.ttf +0 -0
  133. package/skills/frontend-dev/canvas-fonts/EricaOne-OFL.txt +94 -0
  134. package/skills/frontend-dev/canvas-fonts/EricaOne-Regular.ttf +0 -0
  135. package/skills/frontend-dev/canvas-fonts/GeistMono-Bold.ttf +0 -0
  136. package/skills/frontend-dev/canvas-fonts/GeistMono-OFL.txt +93 -0
  137. package/skills/frontend-dev/canvas-fonts/GeistMono-Regular.ttf +0 -0
  138. package/skills/frontend-dev/canvas-fonts/Gloock-OFL.txt +93 -0
  139. package/skills/frontend-dev/canvas-fonts/Gloock-Regular.ttf +0 -0
  140. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  141. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  142. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  143. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  144. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  145. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  146. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  147. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  148. package/skills/frontend-dev/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  149. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  150. package/skills/frontend-dev/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  151. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  152. package/skills/frontend-dev/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  153. package/skills/frontend-dev/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  154. package/skills/frontend-dev/canvas-fonts/Italiana-OFL.txt +93 -0
  155. package/skills/frontend-dev/canvas-fonts/Italiana-Regular.ttf +0 -0
  156. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  157. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  158. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  159. package/skills/frontend-dev/canvas-fonts/Jura-Light.ttf +0 -0
  160. package/skills/frontend-dev/canvas-fonts/Jura-Medium.ttf +0 -0
  161. package/skills/frontend-dev/canvas-fonts/Jura-OFL.txt +93 -0
  162. package/skills/frontend-dev/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  163. package/skills/frontend-dev/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  164. package/skills/frontend-dev/canvas-fonts/Lora-Bold.ttf +0 -0
  165. package/skills/frontend-dev/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  166. package/skills/frontend-dev/canvas-fonts/Lora-Italic.ttf +0 -0
  167. package/skills/frontend-dev/canvas-fonts/Lora-OFL.txt +93 -0
  168. package/skills/frontend-dev/canvas-fonts/Lora-Regular.ttf +0 -0
  169. package/skills/frontend-dev/canvas-fonts/NationalPark-Bold.ttf +0 -0
  170. package/skills/frontend-dev/canvas-fonts/NationalPark-OFL.txt +93 -0
  171. package/skills/frontend-dev/canvas-fonts/NationalPark-Regular.ttf +0 -0
  172. package/skills/frontend-dev/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  173. package/skills/frontend-dev/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  174. package/skills/frontend-dev/canvas-fonts/Outfit-Bold.ttf +0 -0
  175. package/skills/frontend-dev/canvas-fonts/Outfit-OFL.txt +93 -0
  176. package/skills/frontend-dev/canvas-fonts/Outfit-Regular.ttf +0 -0
  177. package/skills/frontend-dev/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  178. package/skills/frontend-dev/canvas-fonts/PixelifySans-OFL.txt +93 -0
  179. package/skills/frontend-dev/canvas-fonts/PoiretOne-OFL.txt +93 -0
  180. package/skills/frontend-dev/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  181. package/skills/frontend-dev/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  182. package/skills/frontend-dev/canvas-fonts/RedHatMono-OFL.txt +93 -0
  183. package/skills/frontend-dev/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  184. package/skills/frontend-dev/canvas-fonts/Silkscreen-OFL.txt +93 -0
  185. package/skills/frontend-dev/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  186. package/skills/frontend-dev/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  187. package/skills/frontend-dev/canvas-fonts/SmoochSans-OFL.txt +93 -0
  188. package/skills/frontend-dev/canvas-fonts/Tektur-Medium.ttf +0 -0
  189. package/skills/frontend-dev/canvas-fonts/Tektur-OFL.txt +93 -0
  190. package/skills/frontend-dev/canvas-fonts/Tektur-Regular.ttf +0 -0
  191. package/skills/frontend-dev/canvas-fonts/WorkSans-Bold.ttf +0 -0
  192. package/skills/frontend-dev/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  193. package/skills/frontend-dev/canvas-fonts/WorkSans-Italic.ttf +0 -0
  194. package/skills/frontend-dev/canvas-fonts/WorkSans-OFL.txt +93 -0
  195. package/skills/frontend-dev/canvas-fonts/WorkSans-Regular.ttf +0 -0
  196. package/skills/frontend-dev/canvas-fonts/YoungSerif-OFL.txt +93 -0
  197. package/skills/frontend-dev/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  198. package/skills/frontend-dev/references/asset-prompt-guide.md +43 -0
  199. package/skills/frontend-dev/references/env-setup.md +33 -0
  200. package/skills/frontend-dev/references/minimax-cli-reference.md +133 -0
  201. package/skills/frontend-dev/references/minimax-image-guide.md +65 -0
  202. package/skills/frontend-dev/references/minimax-music-guide.md +216 -0
  203. package/skills/frontend-dev/references/minimax-tts-guide.md +78 -0
  204. package/skills/frontend-dev/references/minimax-video-guide.md +82 -0
  205. package/skills/frontend-dev/references/minimax-voice-catalog.md +685 -0
  206. package/skills/frontend-dev/references/motion-recipes.md +407 -0
  207. package/skills/frontend-dev/references/troubleshooting.md +85 -0
  208. package/skills/frontend-dev/scripts/minimax_image.py +137 -0
  209. package/skills/frontend-dev/scripts/minimax_music.py +157 -0
  210. package/skills/frontend-dev/scripts/minimax_tts.py +127 -0
  211. package/skills/frontend-dev/scripts/minimax_video.py +187 -0
  212. package/skills/frontend-dev/templates/generator_template.js +223 -0
  213. package/skills/frontend-dev/templates/viewer.html +599 -0
  214. package/skills/frontend-ui-engineering/SKILL.md +367 -0
  215. package/skills/fullstack-dev/SKILL.md +819 -0
  216. package/skills/fullstack-dev/references/api-design.md +444 -0
  217. package/skills/fullstack-dev/references/auth-flow.md +165 -0
  218. package/skills/fullstack-dev/references/db-schema.md +706 -0
  219. package/skills/fullstack-dev/references/django-best-practices.md +466 -0
  220. package/skills/fullstack-dev/references/environment-management.md +78 -0
  221. package/skills/fullstack-dev/references/release-checklist.md +278 -0
  222. package/skills/fullstack-dev/references/technology-selection.md +254 -0
  223. package/skills/fullstack-dev/references/testing-strategy.md +404 -0
  224. package/skills/git-workflow-and-versioning/SKILL.md +446 -0
  225. package/skills/github-evidence-triage/SKILL.md +115 -0
  226. package/skills/harness-evolution/SKILL.md +59 -0
  227. package/skills/harness-evolution/references/activation-matrix.md +13 -0
  228. package/skills/harness-evolution/references/approval-policy.md +20 -0
  229. package/skills/harness-evolution/references/change-report-template.md +33 -0
  230. package/skills/harness-evolution/references/component-taxonomy.md +19 -0
  231. package/skills/harness-evolution/references/verdict-policy.md +13 -0
  232. package/skills/harness-issue-triage/SKILL.md +72 -0
  233. package/skills/harness-issue-triage/references/component-diagnosis.md +25 -0
  234. package/skills/harness-issue-triage/references/triage-report-template.md +34 -0
  235. package/skills/harness-optimization-audit/SKILL.md +125 -0
  236. package/skills/idea-refine/SKILL.md +191 -0
  237. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  238. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  239. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/SKILL.upstream.md +178 -0
  240. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/examples.md +238 -0
  241. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/frameworks.md +99 -0
  242. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/refinement-criteria.md +113 -0
  243. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/scripts/idea-refine.upstream.sh +15 -0
  244. package/skills/incremental-implementation/SKILL.md +252 -0
  245. package/skills/ios-application-dev/SKILL.md +212 -0
  246. package/skills/ios-application-dev/references/accessibility.md +259 -0
  247. package/skills/ios-application-dev/references/graphics-animation.md +350 -0
  248. package/skills/ios-application-dev/references/layout-system.md +199 -0
  249. package/skills/ios-application-dev/references/metal-shader.md +178 -0
  250. package/skills/ios-application-dev/references/navigation-patterns.md +175 -0
  251. package/skills/ios-application-dev/references/swift-coding-standards.md +757 -0
  252. package/skills/ios-application-dev/references/swiftui-design-guidelines.md +1167 -0
  253. package/skills/ios-application-dev/references/system-integration.md +401 -0
  254. package/skills/ios-application-dev/references/uikit-components.md +297 -0
  255. package/skills/local-review-gate/SKILL.md +207 -0
  256. package/skills/local-review-gate/references/addyosmani-code-review-rubric.md +33 -0
  257. package/skills/local-review-gate/references/codex-github-compatibility.md +26 -0
  258. package/skills/local-review-gate/references/ecc-code-review-adaptation.md +40 -0
  259. package/skills/local-review-gate/references/graphify-local-review.md +33 -0
  260. package/skills/local-review-gate/references/orchestration-adaptation.md +51 -0
  261. package/skills/local-review-gate/references/review-repair-lane-adaptation.md +46 -0
  262. package/skills/local-review-gate/references/upstream-provenance.md +61 -0
  263. package/skills/mature-project-pattern-research/SKILL.md +155 -0
  264. package/skills/mature-project-pattern-research/references/research-rubric.md +45 -0
  265. package/skills/minimax-docx/LICENSE +21 -0
  266. package/skills/minimax-docx/SKILL.md +273 -0
  267. package/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
  268. package/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
  269. package/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
  270. package/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
  271. package/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
  272. package/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
  273. package/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
  274. package/skills/minimax-docx/references/cjk_typography.md +357 -0
  275. package/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
  276. package/skills/minimax-docx/references/comments_guide.md +191 -0
  277. package/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
  278. package/skills/minimax-docx/references/design_principles.md +819 -0
  279. package/skills/minimax-docx/references/openxml_element_order.md +308 -0
  280. package/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
  281. package/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
  282. package/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
  283. package/skills/minimax-docx/references/openxml_namespaces.md +82 -0
  284. package/skills/minimax-docx/references/openxml_units.md +72 -0
  285. package/skills/minimax-docx/references/scenario_a_create.md +284 -0
  286. package/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
  287. package/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
  288. package/skills/minimax-docx/references/track_changes_guide.md +200 -0
  289. package/skills/minimax-docx/references/troubleshooting.md +506 -0
  290. package/skills/minimax-docx/references/typography_guide.md +294 -0
  291. package/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
  292. package/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
  293. package/skills/minimax-docx/scripts/docx_preview.sh +37 -0
  294. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
  295. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
  296. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
  297. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
  298. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
  299. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
  300. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
  301. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
  302. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
  303. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
  304. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
  305. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
  306. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
  307. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
  308. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
  309. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
  310. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
  311. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
  312. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
  313. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
  314. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
  315. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
  316. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
  317. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
  318. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
  319. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
  320. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
  321. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
  322. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
  323. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
  324. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
  325. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
  326. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
  327. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
  328. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
  329. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
  330. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
  331. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
  332. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
  333. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
  334. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
  335. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
  336. package/skills/minimax-docx/scripts/env_check.sh +196 -0
  337. package/skills/minimax-docx/scripts/setup.ps1 +274 -0
  338. package/skills/minimax-docx/scripts/setup.sh +504 -0
  339. package/skills/minimax-pdf/README.md +222 -0
  340. package/skills/minimax-pdf/SKILL.md +218 -0
  341. package/skills/minimax-pdf/design/design.md +381 -0
  342. package/skills/minimax-pdf/scripts/cover.py +1579 -0
  343. package/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
  344. package/skills/minimax-pdf/scripts/fill_write.py +242 -0
  345. package/skills/minimax-pdf/scripts/make.sh +491 -0
  346. package/skills/minimax-pdf/scripts/merge.py +112 -0
  347. package/skills/minimax-pdf/scripts/palette.py +521 -0
  348. package/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
  349. package/skills/minimax-pdf/scripts/render_body.py +1052 -0
  350. package/skills/minimax-pdf/scripts/render_cover.js +111 -0
  351. package/skills/minimax-xlsx/SKILL.md +156 -0
  352. package/skills/minimax-xlsx/references/create.md +691 -0
  353. package/skills/minimax-xlsx/references/edit.md +684 -0
  354. package/skills/minimax-xlsx/references/fix.md +37 -0
  355. package/skills/minimax-xlsx/references/format.md +768 -0
  356. package/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
  357. package/skills/minimax-xlsx/references/read-analyze.md +97 -0
  358. package/skills/minimax-xlsx/references/validate.md +772 -0
  359. package/skills/minimax-xlsx/scripts/formula_check.py +422 -0
  360. package/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
  361. package/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
  362. package/skills/minimax-xlsx/scripts/style_audit.py +575 -0
  363. package/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
  364. package/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
  365. package/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
  366. package/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
  367. package/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
  368. package/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
  369. package/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
  370. package/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
  371. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
  372. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
  373. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
  374. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
  375. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
  376. package/skills/newsletter-generation/SKILL.md +71 -0
  377. package/skills/oss-release-readiness/SKILL.md +47 -0
  378. package/skills/parallel-subagent-dispatch/SKILL.md +58 -0
  379. package/skills/performance-optimization/SKILL.md +364 -0
  380. package/skills/planning-and-task-breakdown/SKILL.md +290 -0
  381. package/skills/pptx-generator/SKILL.md +275 -0
  382. package/skills/pptx-generator/references/design-system.md +392 -0
  383. package/skills/pptx-generator/references/editing.md +162 -0
  384. package/skills/pptx-generator/references/pitfalls.md +112 -0
  385. package/skills/pptx-generator/references/pptxgenjs.md +420 -0
  386. package/skills/pptx-generator/references/slide-types.md +413 -0
  387. package/skills/pr-test-analysis/SKILL.md +24 -0
  388. package/skills/react-native-dev/SKILL.md +180 -0
  389. package/skills/react-native-dev/references/animations.md +254 -0
  390. package/skills/react-native-dev/references/components.md +124 -0
  391. package/skills/react-native-dev/references/engineering.md +527 -0
  392. package/skills/react-native-dev/references/forms.md +300 -0
  393. package/skills/react-native-dev/references/native-capabilities.md +163 -0
  394. package/skills/react-native-dev/references/navigation.md +271 -0
  395. package/skills/react-native-dev/references/networking.md +346 -0
  396. package/skills/react-native-dev/references/performance.md +215 -0
  397. package/skills/react-native-dev/references/state-management.md +230 -0
  398. package/skills/react-native-dev/references/styling.md +117 -0
  399. package/skills/react-native-dev/references/testing.md +341 -0
  400. package/skills/requirements-grilling/SKILL.md +447 -0
  401. package/skills/requirements-grilling/references/ADR-FORMAT.md +46 -0
  402. package/skills/requirements-grilling/references/CONTEXT-FORMAT.md +41 -0
  403. package/skills/requirements-grilling/references/INTERVIEW-PACKET-FORMAT.md +62 -0
  404. package/skills/requirements-grilling/references/MIT-LICENSE-MATT-POCOCK.md +25 -0
  405. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  406. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  407. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/interview-me/SKILL.upstream.md +225 -0
  408. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/LICENSE +21 -0
  409. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/NOTICE.md +12 -0
  410. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/batch-grill-me/SKILL.upstream.md +15 -0
  411. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/grill-me/SKILL.upstream.md +7 -0
  412. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/grilling/SKILL.upstream.md +12 -0
  413. package/skills/review-pipeline/SKILL.md +38 -0
  414. package/skills/rose-memory/SKILL.md +210 -0
  415. package/skills/rose-memory/references/README.md +48 -0
  416. package/skills/rose-memory/references/memory_cli.py +2384 -0
  417. package/skills/rose-memory/references/schema.sql +315 -0
  418. package/skills/security-and-hardening/SKILL.md +369 -0
  419. package/skills/session-handoff/SKILL.md +88 -0
  420. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/LICENSE +21 -0
  421. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/NOTICE.md +9 -0
  422. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/handoff/SKILL.upstream.md +16 -0
  423. package/skills/session-handoff/scripts/session_handoff.py +764 -0
  424. package/skills/shader-dev/SKILL.md +316 -0
  425. package/skills/shader-dev/reference/ambient-occlusion.md +382 -0
  426. package/skills/shader-dev/reference/analytic-ray-tracing.md +651 -0
  427. package/skills/shader-dev/reference/anti-aliasing.md +71 -0
  428. package/skills/shader-dev/reference/atmospheric-scattering.md +571 -0
  429. package/skills/shader-dev/reference/camera-effects.md +80 -0
  430. package/skills/shader-dev/reference/cellular-automata.md +635 -0
  431. package/skills/shader-dev/reference/color-palette.md +481 -0
  432. package/skills/shader-dev/reference/csg-boolean-operations.md +466 -0
  433. package/skills/shader-dev/reference/domain-repetition.md +436 -0
  434. package/skills/shader-dev/reference/domain-warping.md +419 -0
  435. package/skills/shader-dev/reference/fluid-simulation.md +425 -0
  436. package/skills/shader-dev/reference/fractal-rendering.md +525 -0
  437. package/skills/shader-dev/reference/lighting-model.md +639 -0
  438. package/skills/shader-dev/reference/matrix-transform.md +535 -0
  439. package/skills/shader-dev/reference/multipass-buffer.md +571 -0
  440. package/skills/shader-dev/reference/normal-estimation.md +418 -0
  441. package/skills/shader-dev/reference/particle-system.md +589 -0
  442. package/skills/shader-dev/reference/path-tracing-gi.md +602 -0
  443. package/skills/shader-dev/reference/polar-uv-manipulation.md +521 -0
  444. package/skills/shader-dev/reference/post-processing.md +375 -0
  445. package/skills/shader-dev/reference/procedural-2d-pattern.md +439 -0
  446. package/skills/shader-dev/reference/procedural-noise.md +551 -0
  447. package/skills/shader-dev/reference/ray-marching.md +396 -0
  448. package/skills/shader-dev/reference/sdf-2d.md +724 -0
  449. package/skills/shader-dev/reference/sdf-3d.md +805 -0
  450. package/skills/shader-dev/reference/sdf-tricks.md +63 -0
  451. package/skills/shader-dev/reference/shadow-techniques.md +476 -0
  452. package/skills/shader-dev/reference/simulation-physics.md +644 -0
  453. package/skills/shader-dev/reference/sound-synthesis.md +578 -0
  454. package/skills/shader-dev/reference/terrain-rendering.md +839 -0
  455. package/skills/shader-dev/reference/texture-mapping-advanced.md +87 -0
  456. package/skills/shader-dev/reference/texture-sampling.md +553 -0
  457. package/skills/shader-dev/reference/volumetric-rendering.md +608 -0
  458. package/skills/shader-dev/reference/voronoi-cellular-noise.md +486 -0
  459. package/skills/shader-dev/reference/voxel-rendering.md +701 -0
  460. package/skills/shader-dev/reference/water-ocean.md +445 -0
  461. package/skills/shader-dev/reference/webgl-pitfalls.md +41 -0
  462. package/skills/shader-dev/techniques/ambient-occlusion.md +364 -0
  463. package/skills/shader-dev/techniques/analytic-ray-tracing.md +542 -0
  464. package/skills/shader-dev/techniques/anti-aliasing.md +124 -0
  465. package/skills/shader-dev/techniques/atmospheric-scattering.md +522 -0
  466. package/skills/shader-dev/techniques/camera-effects.md +115 -0
  467. package/skills/shader-dev/techniques/cellular-automata.md +531 -0
  468. package/skills/shader-dev/techniques/color-palette.md +380 -0
  469. package/skills/shader-dev/techniques/csg-boolean-operations.md +491 -0
  470. package/skills/shader-dev/techniques/domain-repetition.md +333 -0
  471. package/skills/shader-dev/techniques/domain-warping.md +414 -0
  472. package/skills/shader-dev/techniques/fluid-simulation.md +1175 -0
  473. package/skills/shader-dev/techniques/fractal-rendering.md +436 -0
  474. package/skills/shader-dev/techniques/lighting-model.md +527 -0
  475. package/skills/shader-dev/techniques/matrix-transform.md +455 -0
  476. package/skills/shader-dev/techniques/multipass-buffer.md +922 -0
  477. package/skills/shader-dev/techniques/normal-estimation.md +318 -0
  478. package/skills/shader-dev/techniques/particle-system.md +1203 -0
  479. package/skills/shader-dev/techniques/path-tracing-gi.md +623 -0
  480. package/skills/shader-dev/techniques/polar-uv-manipulation.md +373 -0
  481. package/skills/shader-dev/techniques/post-processing.md +788 -0
  482. package/skills/shader-dev/techniques/procedural-2d-pattern.md +346 -0
  483. package/skills/shader-dev/techniques/procedural-noise.md +554 -0
  484. package/skills/shader-dev/techniques/ray-marching.md +467 -0
  485. package/skills/shader-dev/techniques/sdf-2d.md +631 -0
  486. package/skills/shader-dev/techniques/sdf-3d.md +589 -0
  487. package/skills/shader-dev/techniques/sdf-tricks.md +100 -0
  488. package/skills/shader-dev/techniques/shadow-techniques.md +776 -0
  489. package/skills/shader-dev/techniques/simulation-physics.md +1542 -0
  490. package/skills/shader-dev/techniques/sound-synthesis.md +490 -0
  491. package/skills/shader-dev/techniques/terrain-rendering.md +408 -0
  492. package/skills/shader-dev/techniques/texture-mapping-advanced.md +121 -0
  493. package/skills/shader-dev/techniques/texture-sampling.md +382 -0
  494. package/skills/shader-dev/techniques/volumetric-rendering.md +375 -0
  495. package/skills/shader-dev/techniques/voronoi-cellular-noise.md +458 -0
  496. package/skills/shader-dev/techniques/voxel-rendering.md +985 -0
  497. package/skills/shader-dev/techniques/water-ocean.md +490 -0
  498. package/skills/shader-dev/techniques/webgl-pitfalls.md +170 -0
  499. package/skills/shipping-and-launch/SKILL.md +334 -0
  500. package/skills/silent-failure-hunting/SKILL.md +24 -0
  501. package/skills/source-driven-development/SKILL.md +223 -0
  502. package/skills/spec-driven-development/SKILL.md +235 -0
  503. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  504. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  505. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/spec-driven-development/SKILL.upstream.md +206 -0
  506. package/skills/strategy-stress-test/SKILL.md +185 -0
  507. package/skills/systematic-literature-review/SKILL.md +77 -0
  508. package/skills/test-document-generator/SKILL.md +152 -0
  509. package/skills/test-document-generator/references/test-document-template.md +45 -0
  510. package/skills/test-driven-development/SKILL.md +399 -0
  511. package/skills/write-skills/GLOSSARY.md +113 -0
  512. package/skills/write-skills/SKILL.md +133 -0
  513. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/LICENSE +21 -0
  514. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/NOTICE.md +9 -0
  515. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/writing-great-skills/GLOSSARY.md +201 -0
  516. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/writing-great-skills/SKILL.upstream.md +83 -0
  517. package/src/runtime/child-guard.ts +70 -0
  518. package/src/runtime/conflicts.ts +25 -0
  519. package/src/runtime/contracts.ts +9 -0
  520. package/src/runtime/doctor.ts +134 -0
  521. package/src/runtime/global-resources.ts +152 -0
  522. package/src/runtime/index.ts +23 -0
  523. package/src/runtime/lifecycle.ts +19 -0
  524. package/src/runtime/native-integrations.ts +44 -0
  525. package/src/runtime/path-boundaries.ts +69 -0
  526. package/src/runtime/registry.ts +213 -0
  527. package/src/runtime/roles.ts +64 -0
  528. package/src/runtime/rose-context.ts +24 -0
  529. package/src/runtime/subagents.ts +426 -0
  530. package/templates/APPEND_SYSTEM.md +12 -0
  531. package/upstream/aili-workflows.lock.json +3226 -0
@@ -0,0 +1,1052 @@
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
+ def _render_math_png(expr: str, dpi: int = 180) -> bytes | None:
306
+ """
307
+ Render a LaTeX math expression via matplotlib mathtext.
308
+ No LaTeX binary required — uses matplotlib's built-in math parser.
309
+ Supports: fractions (\\frac), integrals (\\int), sums (\\sum),
310
+ Greek letters, sub/superscripts, etc.
311
+ """
312
+ try:
313
+ import matplotlib
314
+ matplotlib.use("Agg")
315
+ import matplotlib.pyplot as plt
316
+
317
+ fig = plt.figure(figsize=(8, 1.2))
318
+ fig.patch.set_facecolor("white")
319
+ ax = fig.add_axes([0, 0, 1, 1])
320
+ ax.set_axis_off()
321
+ ax.set_facecolor("white")
322
+ ax.text(0.5, 0.5, f"${expr}$",
323
+ fontsize=16, ha="center", va="center",
324
+ transform=ax.transAxes)
325
+ buf = io.BytesIO()
326
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
327
+ facecolor="white", pad_inches=0.1)
328
+ plt.close(fig)
329
+ buf.seek(0)
330
+ return buf.read()
331
+ except Exception:
332
+ return None
333
+
334
+
335
+ def _render_chart_png(item: dict, accent: str, dpi: int = 150) -> bytes | None:
336
+ """
337
+ Render bar / line / pie chart to PNG using matplotlib.
338
+
339
+ Required fields:
340
+ chart_type "bar" | "line" | "pie" (default "bar")
341
+ labels list of category strings
342
+ datasets list of {label?, values: list[number]}
343
+
344
+ Optional fields:
345
+ title chart title
346
+ x_label X-axis label
347
+ y_label Y-axis label
348
+ """
349
+ try:
350
+ import matplotlib
351
+ matplotlib.use("Agg")
352
+ import matplotlib.pyplot as plt
353
+ import matplotlib.colors as mcolors
354
+ import colorsys
355
+ import numpy as np
356
+
357
+ chart_type = item.get("chart_type", "bar")
358
+ title_text = item.get("title", "")
359
+ labels = item.get("labels", [])
360
+ datasets = item.get("datasets", [])
361
+
362
+ # Derive a consistent palette from the document accent color
363
+ r, g, b = mcolors.to_rgb(accent)
364
+ h, s, v = colorsys.rgb_to_hsv(r, g, b)
365
+ palette = [
366
+ colorsys.hsv_to_rgb(
367
+ (h + i * 0.13) % 1.0,
368
+ max(0.35, s - i * 0.08),
369
+ min(0.92, v + i * 0.04),
370
+ )
371
+ for i in range(max(len(datasets), 1))
372
+ ]
373
+
374
+ fig, ax = plt.subplots(figsize=(7, 3.6), dpi=dpi)
375
+ fig.patch.set_facecolor("white")
376
+ ax.set_facecolor("white")
377
+
378
+ if chart_type == "bar":
379
+ x = np.arange(len(labels))
380
+ n = max(len(datasets), 1)
381
+ width = 0.68 / n
382
+ for i, ds in enumerate(datasets):
383
+ offset = (i - (n - 1) / 2) * width
384
+ ax.bar(x + offset, ds.get("values", []), width * 0.88,
385
+ label=ds.get("label", f"Series {i+1}"),
386
+ color=palette[i % len(palette)], edgecolor="none")
387
+ ax.set_xticks(x)
388
+ ax.set_xticklabels(labels, fontsize=8.5)
389
+ ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
390
+ ax.set_axisbelow(True)
391
+ if item.get("x_label"):
392
+ ax.set_xlabel(item["x_label"], fontsize=8.5)
393
+ if item.get("y_label"):
394
+ ax.set_ylabel(item["y_label"], fontsize=8.5)
395
+
396
+ elif chart_type == "line":
397
+ x = np.arange(len(labels))
398
+ for i, ds in enumerate(datasets):
399
+ ax.plot(x, ds.get("values", []), marker="o", markersize=3.5,
400
+ label=ds.get("label", f"Series {i+1}"),
401
+ color=palette[i % len(palette)], linewidth=1.8)
402
+ ax.set_xticks(x)
403
+ ax.set_xticklabels(labels, fontsize=8.5)
404
+ ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
405
+ ax.set_axisbelow(True)
406
+ if item.get("x_label"):
407
+ ax.set_xlabel(item["x_label"], fontsize=8.5)
408
+ if item.get("y_label"):
409
+ ax.set_ylabel(item["y_label"], fontsize=8.5)
410
+
411
+ elif chart_type == "pie":
412
+ vals = datasets[0].get("values", []) if datasets else []
413
+ colors = [
414
+ colorsys.hsv_to_rgb(
415
+ (h + i * 0.11) % 1.0,
416
+ max(0.30, s - i * 0.06),
417
+ min(0.92, v + i * 0.03),
418
+ )
419
+ for i in range(len(vals))
420
+ ]
421
+ ax.pie(vals, labels=labels, colors=colors,
422
+ autopct="%1.1f%%", pctdistance=0.82,
423
+ wedgeprops=dict(edgecolor="white", linewidth=1.4),
424
+ textprops=dict(fontsize=8.5))
425
+
426
+ # Shared styling
427
+ for spine in ax.spines.values():
428
+ spine.set_linewidth(0.5)
429
+ spine.set_color("#CCCCCC")
430
+ ax.tick_params(axis="both", length=0, labelsize=8.5)
431
+ if title_text:
432
+ ax.set_title(title_text, fontsize=10, pad=8,
433
+ color="#333333", fontweight="bold")
434
+ if len(datasets) > 1 and chart_type != "pie":
435
+ ax.legend(frameon=False, fontsize=8, loc="upper right")
436
+
437
+ plt.tight_layout(pad=0.4)
438
+ buf = io.BytesIO()
439
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
440
+ facecolor="white", pad_inches=0.06)
441
+ plt.close(fig)
442
+ buf.seek(0)
443
+ return buf.read()
444
+ except Exception:
445
+ return None
446
+
447
+
448
+ def _render_flowchart_png(item: dict, accent: str, dark: str,
449
+ muted: str, dpi: int = 130) -> bytes | None:
450
+ """
451
+ Render a top-to-bottom flowchart using matplotlib patches and arrows.
452
+
453
+ Node schema: {id, label, shape?}
454
+ shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
455
+
456
+ Edge schema: {from, to, label?}
457
+ Forward edges (to a later node) draw straight arrows.
458
+ Back edges (to an earlier node) draw a curved arc to the right.
459
+ """
460
+ try:
461
+ import matplotlib
462
+ matplotlib.use("Agg")
463
+ import matplotlib.pyplot as plt
464
+ import matplotlib.patches as mpatch
465
+ from matplotlib.patches import FancyBboxPatch
466
+ import matplotlib.colors as mcolors
467
+
468
+ nodes_list = item.get("nodes", [])
469
+ edges = item.get("edges", [])
470
+ if not nodes_list:
471
+ return None
472
+
473
+ nodes = {n["id"]: n for n in nodes_list}
474
+ order = {n["id"]: i for i, n in enumerate(nodes_list)}
475
+
476
+ n_nodes = len(nodes_list)
477
+ BOX_W = 4.2
478
+ BOX_H = 0.58
479
+ STEP_Y = 1.25
480
+ CX = 5.0
481
+
482
+ fig_h = max(3.5, n_nodes * STEP_Y + 0.8)
483
+ fig, ax = plt.subplots(figsize=(6, fig_h), dpi=dpi)
484
+ fig.patch.set_facecolor("white")
485
+ ax.set_facecolor("white")
486
+ ax.set_xlim(0, 10)
487
+ ax.set_ylim(-0.6, n_nodes * STEP_Y + 0.2)
488
+ ax.invert_yaxis()
489
+ ax.axis("off")
490
+
491
+ acc_rgb = mcolors.to_rgb(accent)
492
+ dark_rgb = mcolors.to_rgb(dark)
493
+ muted_rgb = mcolors.to_rgb(muted)
494
+
495
+ # Node positions (cx, cy) — preserves input order
496
+ pos = {nid: (CX, i * STEP_Y) for nid, i in order.items()}
497
+
498
+ # ── Draw edges (behind nodes) ──────────────────────────────────────────
499
+ for edge in edges:
500
+ src, dst = edge.get("from"), edge.get("to")
501
+ if src not in pos or dst not in pos:
502
+ continue
503
+ x1, y1 = pos[src]
504
+ x2, y2 = pos[dst]
505
+ lbl = edge.get("label", "")
506
+
507
+ src_shape = nodes.get(src, {}).get("shape", "rect")
508
+ dst_shape = nodes.get(dst, {}).get("shape", "rect")
509
+ dy_src = BOX_H * (0.80 if src_shape == "diamond" else 0.50)
510
+ dy_dst = BOX_H * (0.80 if dst_shape == "diamond" else 0.50)
511
+
512
+ y_start = y1 + dy_src
513
+ y_end = y2 - dy_dst
514
+
515
+ # Forward edge: straight; back-edge: curved arc
516
+ conn = "arc3,rad=0.0" if y_end > y_start + 0.01 else "arc3,rad=0.42"
517
+
518
+ ax.annotate("",
519
+ xy=(x2, y_end), xytext=(x1, y_start),
520
+ arrowprops=dict(
521
+ arrowstyle="-|>", color=muted_rgb,
522
+ lw=1.0, mutation_scale=10,
523
+ connectionstyle=conn,
524
+ ),
525
+ )
526
+ if lbl:
527
+ mid_x = (x1 + x2) / 2 + 0.28
528
+ mid_y = (y_start + y_end) / 2
529
+ ax.text(mid_x, mid_y, lbl, fontsize=7.5,
530
+ color=muted_rgb, ha="left", va="center")
531
+
532
+ # ── Draw nodes (in front of edges) ────────────────────────────────────
533
+ for nid, (cx, cy) in pos.items():
534
+ node = nodes[nid]
535
+ shape = node.get("shape", "rect")
536
+ label = node.get("label", nid)
537
+ left = cx - BOX_W / 2
538
+ bot = cy - BOX_H / 2
539
+
540
+ if shape in ("oval", "terminal"):
541
+ el = mpatch.Ellipse(
542
+ (cx, cy), BOX_W * 0.78, BOX_H * 1.15,
543
+ facecolor=acc_rgb, edgecolor=acc_rgb, linewidth=0,
544
+ )
545
+ ax.add_patch(el)
546
+ ax.text(cx, cy, label, ha="center", va="center",
547
+ fontsize=8.5, fontweight="bold", color="white")
548
+
549
+ elif shape == "diamond":
550
+ d = BOX_W * 0.44
551
+ diamond = plt.Polygon(
552
+ [(cx, cy - d * 0.72), (cx + d, cy),
553
+ (cx, cy + d * 0.72), (cx - d, cy)],
554
+ facecolor="#FFFCF0",
555
+ edgecolor=accent, linewidth=1.2,
556
+ )
557
+ ax.add_patch(diamond)
558
+ ax.text(cx, cy, label, ha="center", va="center",
559
+ fontsize=8, color=dark_rgb)
560
+
561
+ elif shape == "parallelogram":
562
+ skew = 0.30
563
+ para = plt.Polygon(
564
+ [(left + skew, bot), (left + BOX_W + skew, bot),
565
+ (left + BOX_W, bot + BOX_H), (left, bot + BOX_H)],
566
+ facecolor="white",
567
+ edgecolor=accent, linewidth=1.2,
568
+ )
569
+ ax.add_patch(para)
570
+ ax.text(cx, cy, label, ha="center", va="center",
571
+ fontsize=8.5, color=dark_rgb)
572
+
573
+ else: # rect (default)
574
+ rect = FancyBboxPatch(
575
+ (left, bot), BOX_W, BOX_H,
576
+ boxstyle="round,pad=0.04",
577
+ facecolor="white",
578
+ edgecolor=accent, linewidth=1.2,
579
+ )
580
+ ax.add_patch(rect)
581
+ ax.text(cx, cy, label, ha="center", va="center",
582
+ fontsize=8.5, color=dark_rgb)
583
+
584
+ plt.tight_layout(pad=0.2)
585
+ buf = io.BytesIO()
586
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
587
+ facecolor="white", pad_inches=0.08)
588
+ plt.close(fig)
589
+ buf.seek(0)
590
+ return buf.read()
591
+ except Exception:
592
+ return None
593
+
594
+
595
+ # ══════════════════════════════════════════════════════════════════════════════
596
+ # Block renderers
597
+ #
598
+ # All functions share the same signature:
599
+ # _add_XXX(story: list, item: dict, ctx: dict)
600
+ #
601
+ # ctx keys:
602
+ # tokens dict design tokens from palette.py
603
+ # styles dict ParagraphStyle objects from make_styles()
604
+ # usable_w float usable page width in points
605
+ # acc str accent hex color
606
+ # acc_lt str light accent hex color
607
+ # mu str muted hex color
608
+ # dark str dark hex color
609
+ # figure_n int auto-incrementing figure counter (mutable)
610
+ # numbered_n int auto-incrementing list counter (mutable)
611
+ # ══════════════════════════════════════════════════════════════════════════════
612
+
613
+ def _add_heading(story: list, item: dict, ctx: dict, level: int):
614
+ key = f"h{level}"
615
+ para = Paragraph(item["text"], ctx["styles"][key])
616
+ if level == 1:
617
+ story.append(KeepTogether([para, _divider(ctx["acc"])]))
618
+ else:
619
+ story.append(para)
620
+
621
+
622
+ def _add_body(story: list, item: dict, ctx: dict):
623
+ story.append(Paragraph(item["text"], ctx["styles"]["body"]))
624
+
625
+
626
+ def _add_bullet(story: list, item: dict, ctx: dict):
627
+ story.append(Paragraph(
628
+ f"\u2022\u2002{item['text']}", ctx["styles"]["bullet"]
629
+ ))
630
+
631
+
632
+ def _add_numbered(story: list, item: dict, ctx: dict):
633
+ ctx["numbered_n"] += 1
634
+ story.append(Paragraph(
635
+ f"{ctx['numbered_n']}.\u2002{item['text']}",
636
+ ctx["styles"]["numbered"],
637
+ ))
638
+
639
+
640
+ def _add_callout(story: list, item: dict, ctx: dict):
641
+ story.append(Spacer(1, 8))
642
+ story.append(CalloutBox(
643
+ item["text"], ctx["styles"]["callout"], ctx["acc"], ctx["acc_lt"]
644
+ ))
645
+ story.append(Spacer(1, 8))
646
+
647
+
648
+ def _add_table(story: list, item: dict, ctx: dict):
649
+ t = ctx["tokens"]
650
+ styles = ctx["styles"]
651
+ usable_w = ctx["usable_w"]
652
+ acc = ctx["acc"]
653
+ acc_lt = ctx["acc_lt"]
654
+
655
+ headers = [Paragraph(h, styles["table_header"]) for h in item["headers"]]
656
+ rows = [
657
+ [Paragraph(str(c), styles["table_cell"]) for c in row]
658
+ for row in item.get("rows", [])
659
+ ]
660
+ n_cols = len(item["headers"])
661
+
662
+ # Optional col_widths as fractions summing to 1.0
663
+ if "col_widths" in item and len(item["col_widths"]) == n_cols:
664
+ col_w = [usable_w * f for f in item["col_widths"]]
665
+ else:
666
+ col_w = [usable_w / n_cols] * n_cols
667
+
668
+ tbl = Table([headers] + rows, colWidths=col_w)
669
+ tbl.setStyle(TableStyle([
670
+ ("BACKGROUND", (0, 0), (-1, 0), HexColor(acc)),
671
+ ("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#FFFFFF")),
672
+ ("FONTNAME", (0, 0), (-1, 0), t["font_body_b_rl"]),
673
+ ("FONTSIZE", (0, 0), (-1, 0), 9.5),
674
+ ("TOPPADDING", (0, 0), (-1, 0), 7),
675
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
676
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1),
677
+ [HexColor("#FFFFFF"), HexColor(acc_lt)]),
678
+ ("FONTNAME", (0, 1), (-1, -1), t["font_body_rl"]),
679
+ ("FONTSIZE", (0, 1), (-1, -1), 9.5),
680
+ ("TOPPADDING", (0, 1), (-1, -1), 6),
681
+ ("BOTTOMPADDING", (0, 1), (-1, -1), 6),
682
+ ("LEFTPADDING", (0, 0), (-1, -1), 10),
683
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
684
+ ("BOX", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
685
+ ("LINEBELOW", (0, 0), (-1, 0), 1.2, HexColor(acc)),
686
+ ("TEXTCOLOR", (0, 1), (-1, -1), HexColor(t["body_text"])),
687
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
688
+ ]))
689
+ story.append(tbl)
690
+ if item.get("caption"):
691
+ story.append(Spacer(1, 4))
692
+ story.append(Paragraph(item["caption"], styles["caption"]))
693
+ story.append(Spacer(1, 12))
694
+
695
+
696
+ def _add_image(story: list, item: dict, ctx: dict):
697
+ path = str(item.get("path", item.get("src", "")))
698
+ if not os.path.exists(path):
699
+ story.append(Paragraph(
700
+ f"[Image not found: {path}]", ctx["styles"]["caption"]
701
+ ))
702
+ return
703
+ try:
704
+ img = RLImage(path)
705
+ uw = ctx["usable_w"]
706
+ if img.drawWidth > uw:
707
+ scale = uw / img.drawWidth
708
+ img.drawWidth = uw
709
+ img.drawHeight = img.drawHeight * scale
710
+ story.append(img)
711
+ except Exception as e:
712
+ story.append(Paragraph(f"[Image error: {e}]", ctx["styles"]["caption"]))
713
+ return
714
+ if item.get("caption"):
715
+ story.append(Spacer(1, 4))
716
+ story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
717
+ story.append(Spacer(1, 8))
718
+
719
+
720
+ def _add_figure(story: list, item: dict, ctx: dict):
721
+ """Like image but auto-numbers the caption as 'Figure N: ...'."""
722
+ ctx["figure_n"] += 1
723
+ raw_cap = item.get("caption", "")
724
+ caption = f"Figure {ctx['figure_n']}: {raw_cap}" if raw_cap \
725
+ else f"Figure {ctx['figure_n']}"
726
+ _add_image(story, {**item, "caption": caption}, ctx)
727
+
728
+
729
+ def _add_code(story: list, item: dict, ctx: dict):
730
+ acc = ctx["acc"]
731
+ acc_lt = ctx["acc_lt"]
732
+ mu = ctx["mu"]
733
+ uw = ctx["usable_w"]
734
+ lang = item.get("language", "")
735
+
736
+ pre = Preformatted(item.get("text", ""), ctx["styles"]["code"])
737
+ tbl = Table([[pre]], colWidths=[uw])
738
+ tbl.setStyle(TableStyle([
739
+ ("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
740
+ ("LINEBEFORE", (0, 0), ( 0, -1), 3, HexColor(acc)),
741
+ ("BOX", (0, 0), (-1, -1), 0.5, HexColor(mu)),
742
+ ("LEFTPADDING", (0, 0), (-1, -1), 14),
743
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
744
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
745
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
746
+ ]))
747
+ story.append(Spacer(1, 6))
748
+ if lang:
749
+ story.append(Paragraph(lang.upper(), ctx["styles"]["code_lang"]))
750
+ story.append(tbl)
751
+ story.append(Spacer(1, 6))
752
+
753
+
754
+ def _add_math(story: list, item: dict, ctx: dict):
755
+ """
756
+ Display math block.
757
+
758
+ Fields:
759
+ text LaTeX math expression (without enclosing $)
760
+ label optional equation label, e.g. "(1)" — displayed right-aligned
761
+ caption optional caption below the formula
762
+
763
+ Example:
764
+ {"type": "math", "text": "E = mc^2", "label": "(1)"}
765
+ {"type": "math", "text": "\\\\int_0^\\\\infty e^{-x^2}\\\\,dx = \\\\frac{\\\\sqrt{\\\\pi}}{2}"}
766
+ """
767
+ acc = ctx["acc"]
768
+ acc_lt = ctx["acc_lt"]
769
+ uw = ctx["usable_w"]
770
+ expr = item.get("text", "").strip()
771
+ label = item.get("label", "").strip()
772
+
773
+ png = _render_math_png(expr)
774
+
775
+ if png is None:
776
+ # Graceful text fallback if matplotlib unavailable
777
+ story.append(Spacer(1, 6))
778
+ pre = Preformatted(f" {expr}", ctx["styles"]["math_fallback"])
779
+ tbl = Table([[pre]], colWidths=[uw])
780
+ tbl.setStyle(TableStyle([
781
+ ("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
782
+ ("LEFTPADDING", (0, 0), (-1, -1), 14),
783
+ ("RIGHTPADDING", (0, 0), (-1, -1), 14),
784
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
785
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
786
+ ]))
787
+ story.append(tbl)
788
+ story.append(Spacer(1, 6))
789
+ return
790
+
791
+ img = _image_from_bytes(png, uw, max_frac=0.72)
792
+ story.append(Spacer(1, 10))
793
+
794
+ if label:
795
+ label_w = 44
796
+ formula_w = uw - label_w
797
+ lbl_para = Paragraph(label, ctx["styles"]["eq_label"])
798
+ row_tbl = Table([[img, lbl_para]], colWidths=[formula_w, label_w])
799
+ row_tbl.setStyle(TableStyle([
800
+ ("ALIGN", (0, 0), (0, 0), "CENTER"),
801
+ ("ALIGN", (1, 0), (1, 0), "RIGHT"),
802
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
803
+ ]))
804
+ story.append(row_tbl)
805
+ else:
806
+ row_tbl = Table([[img]], colWidths=[uw])
807
+ row_tbl.setStyle(TableStyle([
808
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
809
+ ]))
810
+ story.append(row_tbl)
811
+
812
+ if item.get("caption"):
813
+ story.append(Spacer(1, 4))
814
+ story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
815
+ story.append(Spacer(1, 10))
816
+
817
+
818
+ def _add_chart(story: list, item: dict, ctx: dict):
819
+ """
820
+ Render a chart (bar / line / pie) via matplotlib.
821
+
822
+ Fields:
823
+ chart_type "bar" | "line" | "pie" (default "bar")
824
+ title chart title
825
+ labels list of category strings
826
+ datasets list of {label?, values: list[number]}
827
+ x_label X-axis label (bar/line)
828
+ y_label Y-axis label (bar/line)
829
+ caption caption text below chart
830
+ figure bool (default true) — prefix caption with "Figure N:"
831
+ """
832
+ uw = ctx["usable_w"]
833
+ png = _render_chart_png(item, ctx["acc"])
834
+
835
+ if png is None:
836
+ story.append(Paragraph(
837
+ "[Chart: install matplotlib to render — pip install matplotlib]",
838
+ ctx["styles"]["caption"],
839
+ ))
840
+ return
841
+
842
+ img = _image_from_bytes(png, uw, max_frac=0.95)
843
+ story.append(Spacer(1, 8))
844
+ row_tbl = Table([[img]], colWidths=[uw])
845
+ row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
846
+ story.append(row_tbl)
847
+
848
+ raw_cap = item.get("caption", "")
849
+ use_fig = item.get("figure", True)
850
+ if raw_cap or use_fig:
851
+ ctx["figure_n"] += 1
852
+ prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
853
+ story.append(Spacer(1, 4))
854
+ story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
855
+ story.append(Spacer(1, 10))
856
+
857
+
858
+ def _add_flowchart(story: list, item: dict, ctx: dict):
859
+ """
860
+ Render a flowchart via matplotlib.
861
+
862
+ Fields:
863
+ nodes list of {id, label, shape?}
864
+ shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
865
+ edges list of {from, to, label?}
866
+ caption caption below the diagram
867
+ figure bool (default true) — prefix caption with "Figure N:"
868
+ """
869
+ uw = ctx["usable_w"]
870
+ png = _render_flowchart_png(item, ctx["acc"], ctx["dark"], ctx["mu"])
871
+
872
+ if png is None:
873
+ story.append(Paragraph(
874
+ "[Flowchart: install matplotlib to render — pip install matplotlib]",
875
+ ctx["styles"]["caption"],
876
+ ))
877
+ return
878
+
879
+ img = _image_from_bytes(png, uw, max_frac=0.78)
880
+ story.append(Spacer(1, 8))
881
+ row_tbl = Table([[img]], colWidths=[uw])
882
+ row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
883
+ story.append(row_tbl)
884
+
885
+ raw_cap = item.get("caption", "")
886
+ use_fig = item.get("figure", True)
887
+ if raw_cap or use_fig:
888
+ ctx["figure_n"] += 1
889
+ prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
890
+ story.append(Spacer(1, 4))
891
+ story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
892
+ story.append(Spacer(1, 10))
893
+
894
+
895
+ def _add_bibliography(story: list, item: dict, ctx: dict):
896
+ """
897
+ Numbered reference list with hanging indent.
898
+
899
+ Fields:
900
+ title section heading (default "References"); set "" to suppress
901
+ items list of {id, text}
902
+
903
+ Example:
904
+ {"type": "bibliography",
905
+ "items": [
906
+ {"id": "1", "text": "Smith, J. (2023). Title. Journal, 10(2), 1–15."},
907
+ {"id": "2", "text": "Doe, A. (2022). Another title. Publisher."}
908
+ ]}
909
+ """
910
+ heading = item.get("title", "References")
911
+ if heading:
912
+ story.append(KeepTogether([
913
+ Paragraph(heading, ctx["styles"]["bib_title"]),
914
+ _divider(ctx["acc"]),
915
+ ]))
916
+
917
+ for ref in item.get("items", []):
918
+ story.append(Spacer(1, 4))
919
+ story.append(BibliographyItem(
920
+ str(ref.get("id", "")),
921
+ ref.get("text", ""),
922
+ ctx["styles"]["bib"],
923
+ ctx["dark"],
924
+ ))
925
+
926
+
927
+ # ══════════════════════════════════════════════════════════════════════════════
928
+ # Story builder
929
+ # ══════════════════════════════════════════════════════════════════════════════
930
+
931
+ # Block types that break a numbered list sequence
932
+ _RESETS_NUMBERED = frozenset({
933
+ "h1", "h2", "h3", "body", "bullet", "callout", "table",
934
+ "image", "figure", "code", "math", "chart", "flowchart",
935
+ "bibliography", "divider", "caption", "pagebreak", "spacer",
936
+ })
937
+
938
+
939
+ def build_story(content: list, tokens: dict, styles: dict) -> list:
940
+ usable_w = A4[0] - tokens["margin_left"] - tokens["margin_right"]
941
+
942
+ ctx: dict = {
943
+ "tokens": tokens,
944
+ "styles": styles,
945
+ "usable_w": usable_w,
946
+ "acc": tokens["accent"],
947
+ "acc_lt": tokens["accent_lt"],
948
+ "mu": tokens["muted"],
949
+ "dark": tokens["dark"],
950
+ "figure_n": 0,
951
+ "numbered_n": 0,
952
+ }
953
+
954
+ story: list = []
955
+
956
+ for item in content:
957
+ kind = item.get("type", "body")
958
+
959
+ if kind in _RESETS_NUMBERED:
960
+ ctx["numbered_n"] = 0
961
+
962
+ if kind == "h1": _add_heading(story, item, ctx, 1)
963
+ elif kind == "h2": _add_heading(story, item, ctx, 2)
964
+ elif kind == "h3": _add_heading(story, item, ctx, 3)
965
+ elif kind == "body": _add_body(story, item, ctx)
966
+ elif kind == "bullet": _add_bullet(story, item, ctx)
967
+ elif kind == "numbered": _add_numbered(story, item, ctx)
968
+ elif kind == "callout": _add_callout(story, item, ctx)
969
+ elif kind == "table": _add_table(story, item, ctx)
970
+ elif kind == "image": _add_image(story, item, ctx)
971
+ elif kind == "figure": _add_figure(story, item, ctx)
972
+ elif kind == "code": _add_code(story, item, ctx)
973
+ elif kind == "math": _add_math(story, item, ctx)
974
+ elif kind == "chart": _add_chart(story, item, ctx)
975
+ elif kind == "flowchart": _add_flowchart(story, item, ctx)
976
+ elif kind == "bibliography": _add_bibliography(story, item, ctx)
977
+ elif kind == "divider": story.append(_divider(ctx["acc"]))
978
+ elif kind == "caption":
979
+ story.append(Paragraph(item["text"], styles["caption"]))
980
+ elif kind == "pagebreak": story.append(PageBreak())
981
+ elif kind == "spacer": story.append(Spacer(1, item.get("pt", 12)))
982
+
983
+ return story
984
+
985
+
986
+ # ══════════════════════════════════════════════════════════════════════════════
987
+ # Main build
988
+ # ══════════════════════════════════════════════════════════════════════════════
989
+
990
+ def build(tokens: dict, content: list, out_path: str) -> dict:
991
+ register_fonts(tokens)
992
+ styles = make_styles(tokens)
993
+
994
+ doc = BeautifulDoc(
995
+ out_path, tokens,
996
+ pagesize=A4,
997
+ leftMargin=tokens["margin_left"],
998
+ rightMargin=tokens["margin_right"],
999
+ topMargin=tokens["margin_top"],
1000
+ bottomMargin=tokens["margin_bottom"],
1001
+ )
1002
+ doc.build(build_story(content, tokens, styles))
1003
+
1004
+ size = os.path.getsize(out_path)
1005
+ return {"status": "ok", "out": out_path, "size_kb": size // 1024}
1006
+
1007
+
1008
+ # ══════════════════════════════════════════════════════════════════════════════
1009
+ # CLI
1010
+ # ══════════════════════════════════════════════════════════════════════════════
1011
+
1012
+ def main():
1013
+ parser = argparse.ArgumentParser(
1014
+ description="Render body PDF from tokens.json + content.json"
1015
+ )
1016
+ parser.add_argument("--tokens", default="tokens.json")
1017
+ parser.add_argument("--content", default="content.json")
1018
+ parser.add_argument("--out", default="body.pdf")
1019
+ args = parser.parse_args()
1020
+
1021
+ for fpath in (args.tokens, args.content):
1022
+ if not os.path.exists(fpath):
1023
+ print(
1024
+ json.dumps({"status": "error",
1025
+ "error": f"File not found: {fpath}"}),
1026
+ file=sys.stderr,
1027
+ )
1028
+ sys.exit(1)
1029
+
1030
+ with open(args.tokens, encoding="utf-8") as f:
1031
+ tokens = json.load(f)
1032
+ with open(args.content, encoding="utf-8") as f:
1033
+ content = json.load(f)
1034
+
1035
+ try:
1036
+ result = build(tokens, content, args.out)
1037
+ print(json.dumps(result))
1038
+ except Exception as e:
1039
+ import traceback
1040
+ print(
1041
+ json.dumps({
1042
+ "status": "error",
1043
+ "error": str(e),
1044
+ "trace": traceback.format_exc(),
1045
+ }),
1046
+ file=sys.stderr,
1047
+ )
1048
+ sys.exit(3)
1049
+
1050
+
1051
+ if __name__ == "__main__":
1052
+ main()