@litfamily/lithermes 1.0.7 → 1.0.9

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 (652) hide show
  1. package/README.md +299 -13
  2. package/README_Ko-KR.md +300 -14
  3. package/assets/lithermes-plugin/README.md +17 -6
  4. package/assets/lithermes-plugin/__init__.py +96 -21
  5. package/assets/lithermes-plugin/core.py +15 -6
  6. package/assets/lithermes-plugin/core_commands.py +140 -28
  7. package/assets/lithermes-plugin/core_contexts.py +88 -15
  8. package/assets/lithermes-plugin/core_contract.py +118 -9
  9. package/assets/lithermes-plugin/core_routing.py +197 -8
  10. package/assets/lithermes-plugin/deliverable_hedge_guard.py +378 -357
  11. package/assets/lithermes-plugin/diagnostics.py +76 -8
  12. package/assets/lithermes-plugin/humanizer_detector.py +348 -0
  13. package/assets/lithermes-plugin/humanizer_ko_metrics.py +108 -0
  14. package/assets/lithermes-plugin/humanizer_office.py +18 -0
  15. package/assets/lithermes-plugin/lit_mark.py +1 -1
  16. package/assets/lithermes-plugin/motion_receipts.py +80 -0
  17. package/assets/lithermes-plugin/payload-version.json +2240 -64
  18. package/assets/lithermes-plugin/plugin.yaml +1 -1
  19. package/assets/lithermes-plugin/rules/bundled-rules/lit-humanizer.md +14 -0
  20. package/assets/lithermes-plugin/skills/autoconference/assets/report_template.md +2 -1
  21. package/assets/lithermes-plugin/skills/autoconference/modes/ship/SKILL.md +11 -8
  22. package/assets/lithermes-plugin/skills/autoresearch/modes/ship/type-checklists.md +2 -2
  23. package/assets/lithermes-plugin/skills/browser-drive/ORIGIN.json +17 -0
  24. package/assets/lithermes-plugin/skills/browser-drive/SKILL.md +41 -16
  25. package/assets/lithermes-plugin/skills/browser-drive/scripts/capability_probe.py +122 -10
  26. package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +45 -13
  27. package/assets/lithermes-plugin/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +51 -0
  28. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/README.md +44 -0
  29. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/adaptive-layout.md +34 -62
  30. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/brand-and-imagery.md +22 -70
  31. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +18 -4
  32. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/composition.md +27 -78
  33. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/craft-floor.md +74 -0
  34. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/creative-directions.md +25 -69
  35. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/evidence-review.md +18 -97
  36. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/implementation-platforms.md +23 -83
  37. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/inclusive-interface.md +22 -70
  38. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/interaction-motion.md +34 -68
  39. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/motion-guide.md +62 -0
  40. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/operating-lanes.md +14 -78
  41. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/performance-delivery.md +22 -71
  42. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/product-direction.md +17 -70
  43. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/production.md +19 -7
  44. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/redesign-playbook.md +23 -76
  45. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/slop-register.md +69 -0
  46. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/system-foundations.md +22 -67
  47. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-language.md +16 -72
  48. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-reconstruction.md +16 -81
  49. package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/THIRD-PARTY-NOTICE.txt +15 -0
  50. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-page.js +311 -0
  51. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-rules.json +46 -0
  52. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +231 -0
  53. package/assets/lithermes-plugin/skills/lit-comprehend/SKILL.md +36 -25
  54. package/assets/lithermes-plugin/skills/lit-comprehend/references/artifact-template.md +45 -87
  55. package/assets/lithermes-plugin/skills/lit-comprehend/scripts/verify-explainer.py +3 -27
  56. package/assets/lithermes-plugin/skills/lit-diagram-drawer/NOTICE +33 -0
  57. package/assets/lithermes-plugin/skills/lit-diagram-drawer/SKILL.md +308 -0
  58. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-dark.html +1 -0
  59. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-full.html +1 -0
  60. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-light.html +1 -0
  61. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-dark.html +1 -0
  62. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-full.html +1 -0
  63. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-light.html +1 -0
  64. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-dark.html +1 -0
  65. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-full.html +1 -0
  66. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-light.html +1 -0
  67. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-dark.html +1 -0
  68. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-full.html +1 -0
  69. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-light.html +1 -0
  70. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-dark.html +1 -0
  71. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-full.html +1 -0
  72. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-light.html +1 -0
  73. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-dark.html +1 -0
  74. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-full.html +1 -0
  75. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-light.html +1 -0
  76. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-dark.html +1 -0
  77. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-full.html +1 -0
  78. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-light.html +1 -0
  79. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-dark.html +1 -0
  80. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-full.html +1 -0
  81. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-light.html +1 -0
  82. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-dark.html +1 -0
  83. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-full.html +1 -0
  84. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-light.html +1 -0
  85. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-dark.html +1 -0
  86. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-full.html +1 -0
  87. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-light.html +1 -0
  88. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-dark.html +1 -0
  89. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-full.html +1 -0
  90. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-light.html +1 -0
  91. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-dark.html +1 -0
  92. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-full.html +1 -0
  93. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-light.html +1 -0
  94. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-dark.html +1 -0
  95. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-full.html +1 -0
  96. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-light.html +1 -0
  97. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-dark.html +1 -0
  98. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-full.html +1 -0
  99. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-light.html +1 -0
  100. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-dark.html +1 -0
  101. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-full.html +1 -0
  102. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-light.html +1 -0
  103. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-dark.html +1 -0
  104. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-full.html +1 -0
  105. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-light.html +1 -0
  106. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-dark.html +1 -0
  107. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-full.html +1 -0
  108. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-light.html +1 -0
  109. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-dark.html +1 -0
  110. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-full.html +1 -0
  111. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-light.html +1 -0
  112. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-dark.html +1 -0
  113. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-full.html +1 -0
  114. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-light.html +1 -0
  115. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-dark.html +1 -0
  116. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-full.html +1 -0
  117. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-light.html +1 -0
  118. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-dark.html +1 -0
  119. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-full.html +1 -0
  120. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-light.html +1 -0
  121. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-dark.html +1 -0
  122. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-full.html +1 -0
  123. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-light.html +1 -0
  124. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-dark.html +1 -0
  125. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-full.html +1 -0
  126. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-light.html +1 -0
  127. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-dark.html +1 -0
  128. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-full.html +1 -0
  129. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-light.html +1 -0
  130. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-dark.html +1 -0
  131. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-full.html +1 -0
  132. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-light.html +1 -0
  133. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-dark.html +1 -0
  134. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-full.html +1 -0
  135. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-light.html +1 -0
  136. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-dark.html +1 -0
  137. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-full.html +1 -0
  138. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-light.html +1 -0
  139. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-dark.html +1 -0
  140. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-full.html +1 -0
  141. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-light.html +1 -0
  142. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-dark.html +1 -0
  143. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-full.html +1 -0
  144. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-light.html +1 -0
  145. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-dark.html +1 -0
  146. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-full.html +1 -0
  147. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-light.html +1 -0
  148. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-dark.html +1 -0
  149. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-full.html +1 -0
  150. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-light.html +1 -0
  151. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-dark.html +1 -0
  152. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-full.html +1 -0
  153. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-light.html +1 -0
  154. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-dark.html +1 -0
  155. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-full.html +1 -0
  156. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-light.html +1 -0
  157. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-dark.html +1 -0
  158. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-full.html +1 -0
  159. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-light.html +1 -0
  160. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-dark.html +1 -0
  161. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-full.html +1 -0
  162. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-light.html +1 -0
  163. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-dark.html +1 -0
  164. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-full.html +1 -0
  165. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-light.html +1 -0
  166. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-dark.html +1 -0
  167. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-full.html +1 -0
  168. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-light.html +1 -0
  169. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-dark.html +1 -0
  170. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-full.html +1 -0
  171. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-light.html +1 -0
  172. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-dark.html +1 -0
  173. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-full.html +1 -0
  174. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-light.html +1 -0
  175. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-dark.html +1 -0
  176. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-full.html +1 -0
  177. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-light.html +1 -0
  178. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-dark.html +1 -0
  179. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-full.html +1 -0
  180. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-light.html +1 -0
  181. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-dark.html +1 -0
  182. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-full.html +1 -0
  183. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-light.html +1 -0
  184. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-dark.html +1 -0
  185. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-full.html +1 -0
  186. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-light.html +1 -0
  187. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-dark.html +1 -0
  188. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-full.html +1 -0
  189. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-light.html +1 -0
  190. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-dark.html +1 -0
  191. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-full.html +1 -0
  192. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-light.html +1 -0
  193. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-dark.html +1 -0
  194. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-full.html +1 -0
  195. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-light.html +1 -0
  196. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-dark.html +1 -0
  197. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-full.html +1 -0
  198. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-light.html +1 -0
  199. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-dark.html +1 -0
  200. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-full.html +1 -0
  201. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-light.html +1 -0
  202. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-dark.html +1 -0
  203. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-full.html +1 -0
  204. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-dark.html +1 -0
  205. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-full.html +1 -0
  206. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-light.html +1 -0
  207. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-light.html +1 -0
  208. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-dark.html +1 -0
  209. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-full.html +1 -0
  210. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-light.html +1 -0
  211. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-dark.html +1 -0
  212. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-full.html +1 -0
  213. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-light.html +1 -0
  214. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-dark.html +1 -0
  215. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-full.html +1 -0
  216. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-light.html +1 -0
  217. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-dark.html +1 -0
  218. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-full.html +1 -0
  219. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-light.html +1 -0
  220. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-dark.html +1 -0
  221. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-full.html +1 -0
  222. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-light.html +1 -0
  223. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-dark.html +1 -0
  224. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-full.html +1 -0
  225. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-light.html +1 -0
  226. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-dark.html +1 -0
  227. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-full.html +1 -0
  228. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-light.html +1 -0
  229. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-dark.html +1 -0
  230. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-full.html +1 -0
  231. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-light.html +1 -0
  232. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-dark.html +1 -0
  233. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-full.html +1 -0
  234. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-light.html +1 -0
  235. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-dark.html +1 -0
  236. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-full.html +1 -0
  237. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-light.html +1 -0
  238. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-dark.html +1 -0
  239. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-full.html +1 -0
  240. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-light.html +1 -0
  241. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/OFL.txt +94 -0
  242. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/PretendardVariable.woff2 +0 -0
  243. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/provenance.json +22 -0
  244. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/icons.html +231 -0
  245. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/index.html +1 -0
  246. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/devicon-MIT.txt +20 -0
  247. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/diagram-design-MIT.txt +21 -0
  248. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/log-z-MIT.txt +21 -0
  249. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/simple-icons-CC0.md +30 -0
  250. package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/tabler-MIT.txt +21 -0
  251. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/01-service-architecture/after.html +1 -0
  252. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/01-service-architecture/brief.md +33 -0
  253. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/02-release-decision/after.html +1 -0
  254. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/02-release-decision/brief.md +18 -0
  255. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/03-data-retention/after.html +1 -0
  256. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/03-data-retention/brief.md +19 -0
  257. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/04-oauth-sequence/after.html +1 -0
  258. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/04-oauth-sequence/brief.md +17 -0
  259. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/05-review-lanes/after.html +1 -0
  260. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/05-review-lanes/brief.md +17 -0
  261. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/06-rollout-gates/after.html +1 -0
  262. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/06-rollout-gates/brief.md +17 -0
  263. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/07-deployment-boundary/after.html +1 -0
  264. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/07-deployment-boundary/brief.md +19 -0
  265. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/08-policy-traces/after.html +1 -0
  266. package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/08-policy-traces/brief.md +20 -0
  267. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/accessibility.md +37 -0
  268. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/export.md +29 -0
  269. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-drawio.md +21 -0
  270. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-excalidraw.md +14 -0
  271. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-mermaid.md +18 -0
  272. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-schema.md +28 -0
  273. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/korean-typography.md +34 -0
  274. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/motion.md +29 -0
  275. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/office-pptx-docx.md +40 -0
  276. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/output-spec.md +48 -0
  277. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-annotation.md +12 -0
  278. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-icons.md +21 -0
  279. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-sketchy.md +11 -0
  280. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-terminal.md +11 -0
  281. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/profiles.md +24 -0
  282. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/semantic-patterns.md +27 -0
  283. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/style-guide.md +67 -0
  284. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-architecture.md +51 -0
  285. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bar.md +51 -0
  286. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-beeswarm.md +49 -0
  287. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bubble.md +49 -0
  288. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bump.md +49 -0
  289. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-catalog.json +1433 -0
  290. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-data-flow.md +51 -0
  291. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-datalake.md +49 -0
  292. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-db-schema.md +49 -0
  293. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dependency.md +49 -0
  294. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-deployment.md +49 -0
  295. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dp-integration.md +49 -0
  296. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dp-security-matrix.md +49 -0
  297. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-er.md +49 -0
  298. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-fishbone.md +49 -0
  299. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-flowchart.md +49 -0
  300. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-gantt.md +49 -0
  301. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-heatmap.md +53 -0
  302. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-high-level-vertical.md +53 -0
  303. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-high-level.md +53 -0
  304. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-drawio.md +53 -0
  305. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-excalidraw.md +53 -0
  306. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-mermaid.md +53 -0
  307. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-it-state.md +54 -0
  308. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-journey.md +54 -0
  309. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-kanban.md +54 -0
  310. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-layers.md +54 -0
  311. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-line.md +54 -0
  312. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-loop-terminal.md +53 -0
  313. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-loop.md +54 -0
  314. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-marimekko.md +54 -0
  315. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-medallion.md +54 -0
  316. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-nested.md +59 -0
  317. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-org-chart.md +58 -0
  318. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-paved-road-animated.md +72 -0
  319. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-polar.md +67 -0
  320. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-policy-trace-animated.md +69 -0
  321. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-process.md +58 -0
  322. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-pyramid.md +59 -0
  323. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-quadrant-consultant.md +60 -0
  324. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-quadrant.md +57 -0
  325. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-queue-animated.md +63 -0
  326. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-radar.md +57 -0
  327. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-ridgeline.md +63 -0
  328. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sankey.md +68 -0
  329. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-scatter.md +60 -0
  330. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sequence-oauth.md +66 -0
  331. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sequence.md +63 -0
  332. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-slopegraph.md +65 -0
  333. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-state-lifecycle.md +53 -0
  334. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-state.md +59 -0
  335. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-story-map.md +52 -0
  336. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-streamgraph.md +53 -0
  337. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-swimlane.md +52 -0
  338. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-timeline.md +52 -0
  339. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree-block-decomposition.md +57 -0
  340. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree.md +54 -0
  341. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-treemap.md +54 -0
  342. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-uml-class.md +52 -0
  343. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-venn.md +51 -0
  344. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-wardley.md +52 -0
  345. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-waterfall.md +54 -0
  346. package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/verifier-guide.md +31 -0
  347. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/block-registry.mjs +43 -0
  348. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/browser-version.mjs +12 -0
  349. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/check-visible-text.py +12 -0
  350. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_brief.py +192 -0
  351. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_catalog.py +135 -0
  352. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_checks.py +123 -0
  353. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_cli.py +94 -0
  354. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_geometry.py +130 -0
  355. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_metrics.py +166 -0
  356. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality.py +293 -0
  357. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality_geometry.py +200 -0
  358. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_records.py +205 -0
  359. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_sequence.py +55 -0
  360. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/doctor.mjs +24 -0
  361. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/drawio_extract.py +209 -0
  362. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/drawio_source.py +188 -0
  363. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/excalidraw_extract.py +233 -0
  364. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/excalidraw_support.py +64 -0
  365. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/export.mjs +125 -0
  366. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/mermaid_extract.py +240 -0
  367. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/mermaid_source.py +39 -0
  368. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/office-safety.mjs +54 -0
  369. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-all.py +12 -0
  370. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-brief.py +12 -0
  371. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-diagram.py +12 -0
  372. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-motion.py +12 -0
  373. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-type.py +12 -0
  374. package/assets/lithermes-plugin/skills/lit-docx/NOTICE +1 -0
  375. package/assets/lithermes-plugin/skills/lit-docx/SKILL.md +97 -0
  376. package/assets/lithermes-plugin/skills/lit-docx/references/frontmatter_schema.md +42 -0
  377. package/assets/lithermes-plugin/skills/lit-docx/references/journal_style_spec.md +17 -0
  378. package/assets/lithermes-plugin/skills/lit-docx/references/markdown_quality_checklist.md +37 -0
  379. package/assets/lithermes-plugin/skills/lit-docx/references/slop_phrase_list.yaml +58 -0
  380. package/assets/lithermes-plugin/skills/lit-docx/references/slop_rules.md +35 -0
  381. package/assets/lithermes-plugin/skills/lit-docx/scripts/clean_markdown.py +95 -0
  382. package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_docx.py +1041 -0
  383. package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_pdf.py +281 -0
  384. package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_pdf.py +130 -0
  385. package/assets/lithermes-plugin/skills/lit-docx/scripts/edit_docx.py +297 -0
  386. package/assets/lithermes-plugin/skills/lit-docx/scripts/embed_images.py +78 -0
  387. package/assets/lithermes-plugin/skills/lit-docx/scripts/generate_docx_templates.py +491 -0
  388. package/assets/lithermes-plugin/skills/lit-docx/scripts/slop_lint.py +536 -0
  389. package/assets/lithermes-plugin/skills/lit-docx/scripts/visual_audit.py +41 -0
  390. package/assets/lithermes-plugin/skills/lit-docx/templates/docx/acs.docx +0 -0
  391. package/assets/lithermes-plugin/skills/lit-docx/templates/docx/elsevier.docx +0 -0
  392. package/assets/lithermes-plugin/skills/lit-docx/templates/docx/ieee.docx +0 -0
  393. package/assets/lithermes-plugin/skills/lit-docx/templates/docx/korean-generic.docx +0 -0
  394. package/assets/lithermes-plugin/skills/lit-docx/templates/docx/nature.docx +0 -0
  395. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/README.md +5 -0
  396. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/template.tex +37 -0
  397. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/README.md +5 -0
  398. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/template.tex +38 -0
  399. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/README.md +5 -0
  400. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/template.tex +37 -0
  401. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/README.md +5 -0
  402. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/template.tex +37 -0
  403. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/README.md +5 -0
  404. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/template.tex +37 -0
  405. package/assets/lithermes-plugin/skills/lit-docx/templates/registry.yaml +1366 -0
  406. package/assets/lithermes-plugin/skills/lit-humanizer/NOTICE +170 -0
  407. package/assets/lithermes-plugin/skills/lit-humanizer/SKILL.md +125 -0
  408. package/assets/lithermes-plugin/skills/lit-humanizer/assets/report-skeleton.md +26 -0
  409. package/assets/lithermes-plugin/skills/lit-humanizer/assets/slide-text-skeleton.md +30 -0
  410. package/assets/lithermes-plugin/skills/lit-humanizer/examples/accessibility-note/after.md +7 -0
  411. package/assets/lithermes-plugin/skills/lit-humanizer/examples/accessibility-note/before.md +7 -0
  412. package/assets/lithermes-plugin/skills/lit-humanizer/examples/api-reference/after.md +9 -0
  413. package/assets/lithermes-plugin/skills/lit-humanizer/examples/api-reference/before.md +15 -0
  414. package/assets/lithermes-plugin/skills/lit-humanizer/examples/changelog/after.md +5 -0
  415. package/assets/lithermes-plugin/skills/lit-humanizer/examples/changelog/before.md +12 -0
  416. package/assets/lithermes-plugin/skills/lit-humanizer/examples/code-comments/after.md +10 -0
  417. package/assets/lithermes-plugin/skills/lit-humanizer/examples/code-comments/before.md +19 -0
  418. package/assets/lithermes-plugin/skills/lit-humanizer/examples/commit/after.md +5 -0
  419. package/assets/lithermes-plugin/skills/lit-humanizer/examples/commit/before.md +7 -0
  420. package/assets/lithermes-plugin/skills/lit-humanizer/examples/data-release/after.md +7 -0
  421. package/assets/lithermes-plugin/skills/lit-humanizer/examples/data-release/before.md +9 -0
  422. package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/ko-metrics-golden.json +49 -0
  423. package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/pos-real.txt +26 -0
  424. package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/rule-cases.json +306 -0
  425. package/assets/lithermes-plugin/skills/lit-humanizer/examples/english-essay/after.md +5 -0
  426. package/assets/lithermes-plugin/skills/lit-humanizer/examples/english-essay/before.md +5 -0
  427. package/assets/lithermes-plugin/skills/lit-humanizer/examples/incident-update/after.md +7 -0
  428. package/assets/lithermes-plugin/skills/lit-humanizer/examples/incident-update/before.md +7 -0
  429. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-essay/after.md +7 -0
  430. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-essay/before.md +9 -0
  431. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-report/after.md +9 -0
  432. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-report/before.md +13 -0
  433. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-slide/after.md +19 -0
  434. package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-slide/before.md +21 -0
  435. package/assets/lithermes-plugin/skills/lit-humanizer/examples/meeting-brief/after.md +9 -0
  436. package/assets/lithermes-plugin/skills/lit-humanizer/examples/meeting-brief/before.md +11 -0
  437. package/assets/lithermes-plugin/skills/lit-humanizer/examples/policy-faq/after.md +9 -0
  438. package/assets/lithermes-plugin/skills/lit-humanizer/examples/policy-faq/before.md +9 -0
  439. package/assets/lithermes-plugin/skills/lit-humanizer/examples/pr/after.md +15 -0
  440. package/assets/lithermes-plugin/skills/lit-humanizer/examples/pr/before.md +17 -0
  441. package/assets/lithermes-plugin/skills/lit-humanizer/examples/project-update/after.md +7 -0
  442. package/assets/lithermes-plugin/skills/lit-humanizer/examples/project-update/before.md +9 -0
  443. package/assets/lithermes-plugin/skills/lit-humanizer/examples/readme/after.md +12 -0
  444. package/assets/lithermes-plugin/skills/lit-humanizer/examples/readme/before.md +14 -0
  445. package/assets/lithermes-plugin/skills/lit-humanizer/examples/report/after.md +13 -0
  446. package/assets/lithermes-plugin/skills/lit-humanizer/examples/report/before.md +11 -0
  447. package/assets/lithermes-plugin/skills/lit-humanizer/examples/research-abstract/after.md +5 -0
  448. package/assets/lithermes-plugin/skills/lit-humanizer/examples/research-abstract/before.md +5 -0
  449. package/assets/lithermes-plugin/skills/lit-humanizer/examples/slides/after.md +19 -0
  450. package/assets/lithermes-plugin/skills/lit-humanizer/examples/slides/before.md +19 -0
  451. package/assets/lithermes-plugin/skills/lit-humanizer/examples/troubleshooting/after.md +11 -0
  452. package/assets/lithermes-plugin/skills/lit-humanizer/examples/troubleshooting/before.md +11 -0
  453. package/assets/lithermes-plugin/skills/lit-humanizer/references/README.md +16 -0
  454. package/assets/lithermes-plugin/skills/lit-humanizer/references/always-on-rule.md +9 -0
  455. package/assets/lithermes-plugin/skills/lit-humanizer/references/code-patterns.md +468 -0
  456. package/assets/lithermes-plugin/skills/lit-humanizer/references/deliverable-channels.md +111 -0
  457. package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-checklist.md +43 -0
  458. package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-content.md +243 -0
  459. package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-structure.md +375 -0
  460. package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns.md +57 -0
  461. package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-metrics.md +33 -0
  462. package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns-a-d.md +659 -0
  463. package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns-e-j.md +387 -0
  464. package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns.md +31 -0
  465. package/assets/lithermes-plugin/skills/lit-humanizer/references/rewrite-playbook.md +54 -0
  466. package/assets/lithermes-plugin/skills/lit-humanizer/references/taxonomy.md +153 -0
  467. package/assets/lithermes-plugin/skills/lit-humanizer/rules.json +884 -0
  468. package/assets/lithermes-plugin/skills/lit-humanizer/scripts/detect.py +17 -0
  469. package/assets/lithermes-plugin/skills/lit-humanizer/scripts/extract_office_text.py +114 -0
  470. package/assets/lithermes-plugin/skills/lit-humanizer/scripts/ko_metrics.py +35 -0
  471. package/assets/lithermes-plugin/skills/lit-pptx/NOTICE +1 -0
  472. package/assets/lithermes-plugin/skills/lit-pptx/SKILL.md +109 -0
  473. package/assets/lithermes-plugin/skills/lit-pptx/agents/analyst.md +49 -0
  474. package/assets/lithermes-plugin/skills/lit-pptx/agents/antithesis.md +65 -0
  475. package/assets/lithermes-plugin/skills/lit-pptx/agents/architect.md +44 -0
  476. package/assets/lithermes-plugin/skills/lit-pptx/agents/critic.md +42 -0
  477. package/assets/lithermes-plugin/skills/lit-pptx/agents/synthesizer.md +47 -0
  478. package/assets/lithermes-plugin/skills/lit-pptx/agents/thesis.md +33 -0
  479. package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-lg.png +0 -0
  480. package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-md.png +0 -0
  481. package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-sm.png +0 -0
  482. package/assets/lithermes-plugin/skills/lit-pptx/bin/office.mjs +77 -0
  483. package/assets/lithermes-plugin/skills/lit-pptx/fonts/PRETENDARD-OFL.txt +94 -0
  484. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Black.otf +0 -0
  485. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Bold.otf +0 -0
  486. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-ExtraBold.otf +0 -0
  487. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-ExtraLight.otf +0 -0
  488. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Light.otf +0 -0
  489. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Medium.otf +0 -0
  490. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Regular.otf +0 -0
  491. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-SemiBold.otf +0 -0
  492. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Thin.otf +0 -0
  493. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/OFL.txt +94 -0
  494. package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/SOURCE.md +29 -0
  495. package/assets/lithermes-plugin/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Bold.otf +0 -0
  496. package/assets/lithermes-plugin/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Regular.otf +0 -0
  497. package/assets/lithermes-plugin/skills/lit-pptx/references/anti-slop-checklist.md +71 -0
  498. package/assets/lithermes-plugin/skills/lit-pptx/references/authoring-guide.md +139 -0
  499. package/assets/lithermes-plugin/skills/lit-pptx/references/consensus-protocol.md +125 -0
  500. package/assets/lithermes-plugin/skills/lit-pptx/references/data-display-cookbook.md +45 -0
  501. package/assets/lithermes-plugin/skills/lit-pptx/references/design-system-azure.md +58 -0
  502. package/assets/lithermes-plugin/skills/lit-pptx/runtime/package-lock.json +750 -0
  503. package/assets/lithermes-plugin/skills/lit-pptx/runtime/package.json +15 -0
  504. package/assets/lithermes-plugin/skills/lit-pptx/runtime/requirements.in +7 -0
  505. package/assets/lithermes-plugin/skills/lit-pptx/runtime/requirements.lock +391 -0
  506. package/assets/lithermes-plugin/skills/lit-pptx/scripts/compile-deck.js +447 -0
  507. package/assets/lithermes-plugin/skills/lit-pptx/scripts/craft_extras.py +215 -0
  508. package/assets/lithermes-plugin/skills/lit-pptx/scripts/embed_fonts.py +199 -0
  509. package/assets/lithermes-plugin/skills/lit-pptx/scripts/integrity.py +60 -0
  510. package/assets/lithermes-plugin/skills/lit-pptx/scripts/inventory.py +64 -0
  511. package/assets/lithermes-plugin/skills/lit-pptx/scripts/learn_template.py +433 -0
  512. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/derive-palette.js +176 -0
  513. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/directive-parser.js +244 -0
  514. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/font-map.js +79 -0
  515. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/gen-bg.js +85 -0
  516. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/gen-gradient-assets.js +36 -0
  517. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +736 -0
  518. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/markdown-loader.js +136 -0
  519. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-adapter-html.js +357 -0
  520. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +1864 -0
  521. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/slide-ast.js +539 -0
  522. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/spec-validator.js +94 -0
  523. package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/template-registry.js +355 -0
  524. package/assets/lithermes-plugin/skills/lit-pptx/scripts/qa_deck.py +791 -0
  525. package/assets/lithermes-plugin/skills/lit-pptx/scripts/validate_pptx.py +206 -0
  526. package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/markdown-slide-spec-v1.md +251 -0
  527. package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/markdown-slide-spec-v2.md +221 -0
  528. package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/slide-ast-v1.schema.json +229 -0
  529. package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/slide-ast-v2.schema.json +144 -0
  530. package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/template-enrollment-contract.md +228 -0
  531. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/capabilities.yaml +146 -0
  532. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +384 -0
  533. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +37 -0
  534. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +154 -0
  535. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +405 -0
  536. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +37 -0
  537. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/capabilities.yaml +179 -0
  538. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/layout-mapping.yaml +299 -0
  539. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/template.yaml +45 -0
  540. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +181 -0
  541. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/layout-mapping.yaml +299 -0
  542. package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +44 -0
  543. package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
  544. package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +71 -0
  545. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/audio.py +45 -0
  546. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +690 -0
  547. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +299 -0
  548. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/NOTICE +23 -0
  549. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +55 -0
  550. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/browser.mjs +169 -0
  551. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/completion.mjs +52 -0
  552. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/constants.mjs +162 -0
  553. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/director.mjs +33 -0
  554. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/encode.mjs +191 -0
  555. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/flash.mjs +163 -0
  556. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/frame.mjs +428 -0
  557. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/gate.mjs +483 -0
  558. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/look.mjs +223 -0
  559. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/page.js +622 -0
  560. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/presets.mjs +170 -0
  561. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/scenes.mjs +242 -0
  562. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +382 -0
  563. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-gate.mjs +97 -0
  564. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-init.js +186 -0
  565. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-kit.js +389 -0
  566. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-qa.mjs +358 -0
  567. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-worker.mjs +33 -0
  568. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +691 -0
  569. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stills.mjs +71 -0
  570. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stroke.mjs +231 -0
  571. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/timeline.mjs +177 -0
  572. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +244 -0
  573. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/type.mjs +183 -0
  574. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/util.mjs +152 -0
  575. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/craft-loop.md +175 -0
  576. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/engine-contract.md +166 -0
  577. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/runtime.md +122 -0
  578. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +261 -0
  579. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/style-bibles.md +122 -0
  580. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +164 -0
  581. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/type-craft.md +92 -0
  582. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/fonts.json +154 -0
  583. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/licenses/CREDITS +20 -0
  584. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/licenses/EMS-OFL.txt +91 -0
  585. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/package-lock.json +88 -0
  586. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/package.json +12 -0
  587. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.in +1 -0
  588. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.lock +727 -0
  589. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/word-timing.json +9 -0
  590. package/assets/lithermes-plugin/skills/litresearch/SKILL.md +19 -18
  591. package/assets/lithermes-plugin/skills/litwork/SKILL.md +28 -0
  592. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
  593. package/package.json +1 -1
  594. package/readme-assets/ab-simple/s11-ui-baseline-desktop.webp +0 -0
  595. package/readme-assets/ab-simple/s11-ui-lithermes-desktop.webp +0 -0
  596. package/readme-assets/ab-simple/s3-ui-baseline-desktop.webp +0 -0
  597. package/readme-assets/ab-simple/s3-ui-lithermes-desktop.webp +0 -0
  598. package/readme-assets/ab-simple/s4-ui-baseline-desktop.webp +0 -0
  599. package/readme-assets/ab-simple/s4-ui-lithermes-desktop.webp +0 -0
  600. package/readme-assets/ab-simple/s5-office-baseline-slides.webp +0 -0
  601. package/readme-assets/ab-simple/s5-office-lithermes-slides.webp +0 -0
  602. package/readme-assets/ab-simple/s7-lithermes-diagram.webp +0 -0
  603. package/readme-assets/ab-simple/s8-office-baseline-slides.webp +0 -0
  604. package/readme-assets/ab-simple/s8-office-lithermes-slides.webp +0 -0
  605. package/readme-assets/ab-simple/s9-office-lithermes-pages.webp +0 -0
  606. package/readme-assets/badge-version.svg +1 -1
  607. package/readme-assets/cover-motion-still.webp +0 -0
  608. package/readme-assets/cover-motion.webp +0 -0
  609. package/readme-assets/skills/autoconference.webp +0 -0
  610. package/readme-assets/skills/autoresearch.webp +0 -0
  611. package/readme-assets/skills/browser-drive.webp +0 -0
  612. package/readme-assets/skills/comment-checker.webp +0 -0
  613. package/readme-assets/skills/debugging.webp +0 -0
  614. package/readme-assets/skills/deep-interview.webp +0 -0
  615. package/readme-assets/skills/frontend-ui-ux.webp +0 -0
  616. package/readme-assets/skills/lit-burnoff-file.webp +0 -0
  617. package/readme-assets/skills/lit-burnoff.webp +0 -0
  618. package/readme-assets/skills/lit-code.webp +0 -0
  619. package/readme-assets/skills/lit-commit.webp +0 -0
  620. package/readme-assets/skills/lit-comprehend.webp +0 -0
  621. package/readme-assets/skills/lit-crucible.webp +0 -0
  622. package/readme-assets/skills/lit-diagram-drawer.webp +0 -0
  623. package/readme-assets/skills/lit-docx.webp +0 -0
  624. package/readme-assets/skills/lit-handoff.webp +0 -0
  625. package/readme-assets/skills/lit-humanizer.webp +0 -0
  626. package/readme-assets/skills/lit-init.webp +0 -0
  627. package/readme-assets/skills/lit-plan.webp +0 -0
  628. package/readme-assets/skills/lit-pptx.webp +0 -0
  629. package/readme-assets/skills/lit-recap.webp +0 -0
  630. package/readme-assets/skills/lit-scientific-visualization.webp +0 -0
  631. package/readme-assets/skills/lit-typographic-motion.webp +0 -0
  632. package/readme-assets/skills/litgoal.webp +0 -0
  633. package/readme-assets/skills/litresearch.webp +0 -0
  634. package/readme-assets/skills/litwork.webp +0 -0
  635. package/readme-assets/skills/lsp-setup.webp +0 -0
  636. package/readme-assets/skills/lsp.webp +0 -0
  637. package/readme-assets/skills/readme-studio.webp +0 -0
  638. package/readme-assets/skills/refactor.webp +0 -0
  639. package/readme-assets/skills/review-work.webp +0 -0
  640. package/readme-assets/skills/rules.webp +0 -0
  641. package/readme-assets/skills/start-work.webp +0 -0
  642. package/readme-assets/skills/structural-search.webp +0 -0
  643. package/readme-assets/skills/visual-qa.webp +0 -0
  644. package/readme-assets/skills/wikify.webp +0 -0
  645. package/src/cli.js +40 -1
  646. package/src/lib/check.js +21 -0
  647. package/src/lib/install.js +262 -7
  648. package/src/lib/skillPayload.js +3 -3
  649. package/assets/lithermes-plugin/skills/lit-korean/SKILL.md +0 -237
  650. package/assets/lithermes-plugin/skills/lit-korean/references/quick-rules.md +0 -24
  651. package/assets/lithermes-plugin/skills/lit-korean/references/safety-checklist.md +0 -23
  652. package/readme-assets/cover.webp +0 -0
@@ -0,0 +1,1864 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * render-adapter-pptx.js — Render a resolved slide spec to a PPTX file.
5
+ *
6
+ * Architecture: Each text line is its own separate shape (individual addText()
7
+ * call). Bullet items use manual "• " prefix in green (#00AE41) Bold 16pt.
8
+ * Section headers are separate shapes in Bold 18pt black.
9
+ *
10
+ * Input: resolved spec from layout-resolver, templateObj from registry, output path.
11
+ * Output: writes a .pptx file via pptxgenjs.
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+ const PptxGenJS = require("pptxgenjs");
18
+ const { extractContent, looksLikeKpi } = require("./layout-resolver");
19
+
20
+ // Narrowest a KPI badge can be and still read as one glance.
21
+ const KPI_MIN_CARD_WIDTH_IN = 1.9;
22
+
23
+ const TOOLKIT_ROOT = path.resolve(__dirname, "../..");
24
+ const ASSETS_MEDIA = path.join(TOOLKIT_ROOT, "assets", "media");
25
+ const ASSETS_AZURE = path.join(TOOLKIT_ROOT, "assets", "azure");
26
+
27
+ /** Resolve a decoration asset path: explicit absolute override (custom-accent
28
+ * regenerated gradients), then d.dir "azure" → assets/azure, else assets/media. */
29
+ function decorAssetPath(d) {
30
+ if (d.assetPath) return d.assetPath;
31
+ if (d.dir === "azure") return path.join(ASSETS_AZURE, d.asset);
32
+ return path.join(ASSETS_MEDIA, d.asset);
33
+ }
34
+ const hex = (c) => (c || "#000000").replace("#", "");
35
+
36
+ function linkedTableCell(text, options) {
37
+ const original = cleanMd(text);
38
+ let display = original;
39
+ let url = null;
40
+ const markdown = original.match(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/i);
41
+ if (markdown) {
42
+ display = original.replace(markdown[0], markdown[1]);
43
+ url = markdown[2];
44
+ } else {
45
+ const rawUrl = original.match(/https?:\/\/[^\s<>\])}]+/i);
46
+ const doi = original.match(/\b10\.\d{4,9}\/[^\s<>\])}]+/i);
47
+ if (rawUrl) url = rawUrl[0].replace(/[.,;:]+$/, "");
48
+ else if (doi) url = `https://doi.org/${doi[0].replace(/[.,;:]+$/, "")}`;
49
+ }
50
+ return {
51
+ text: display,
52
+ options: {
53
+ ...options,
54
+ ...(url ? { hyperlink: { url } } : {}),
55
+ },
56
+ };
57
+ }
58
+
59
+ // ── Text style constants (brand-derived; defaults are the neutral template).
60
+ // These are reassigned from the template at the start of render() so the same
61
+ // adapter serves any enrolled template (e.g. BOILERPLATE-PRETENDARD).
62
+ let FONT_BOLD = "Pretendard";
63
+ let FONT_MEDIUM = "Pretendard";
64
+ let FONT_LIGHT = "Pretendard";
65
+ let CHAR_SPACING = -0.7;
66
+
67
+ function expandHome(p) {
68
+ if (p === "~") return os.homedir();
69
+ if (p && p.startsWith(`~${path.sep}`)) return path.join(os.homedir(), p.slice(2));
70
+ return p;
71
+ }
72
+
73
+ function resolveImagePath(src, sourceDir) {
74
+ if (!src) throw new Error("Image source is empty");
75
+ const expanded = expandHome(src);
76
+ const candidates = [];
77
+ if (path.isAbsolute(expanded)) candidates.push(expanded);
78
+ else {
79
+ if (sourceDir) candidates.push(path.resolve(sourceDir, expanded));
80
+ candidates.push(path.resolve(process.cwd(), expanded));
81
+ candidates.push(path.join(ASSETS_MEDIA, expanded));
82
+ candidates.push(path.resolve(TOOLKIT_ROOT, expanded));
83
+ }
84
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
85
+ if (!found) {
86
+ throw new Error(`Image not found: ${src}. Checked: ${candidates.join(", ")}`);
87
+ }
88
+ return found;
89
+ }
90
+
91
+ function readImageSize(filePath) {
92
+ const buf = fs.readFileSync(filePath);
93
+ if (buf.length >= 24 && buf.toString("ascii", 1, 4) === "PNG") {
94
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
95
+ }
96
+ if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8) {
97
+ let offset = 2;
98
+ while (offset + 9 < buf.length) {
99
+ if (buf[offset] !== 0xff) { offset++; continue; }
100
+ const marker = buf[offset + 1];
101
+ const length = buf.readUInt16BE(offset + 2);
102
+ if (length < 2) break;
103
+ if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) ||
104
+ (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
105
+ return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) };
106
+ }
107
+ offset += 2 + length;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function containBox(x, y, w, h, imageSize) {
114
+ if (!imageSize || !imageSize.width || !imageSize.height) return { x, y, w, h };
115
+ const boxRatio = w / h;
116
+ const imageRatio = imageSize.width / imageSize.height;
117
+ if (imageRatio > boxRatio) {
118
+ const fittedH = w / imageRatio;
119
+ return { x, y: y + (h - fittedH) / 2, w, h: fittedH };
120
+ }
121
+ const fittedW = h * imageRatio;
122
+ return { x: x + (w - fittedW) / 2, y, w: fittedW, h };
123
+ }
124
+
125
+ // Bullet items
126
+ let BULLET_FONT = FONT_BOLD;
127
+ const BULLET_SIZE = 16;
128
+ let BULLET_COLOR = "00AE41"; // brand bullet color (default green; reassigned per-template)
129
+ let SECTION_ACCENT = "00AE41"; // brand section-card / accent color (default green)
130
+ const BULLET_PREFIX = "\u2022 "; // "• " (bullet + two spaces)
131
+ const BULLET_X = 0.59;
132
+ const BULLET_W = 8.8;
133
+ const BULLET_H = 0.38;
134
+ const BULLET_Y_SPACING = 0.42;
135
+ const BULLET_LINE_SPACING = 1.25;
136
+
137
+ // Section headers
138
+ let SEC_HEADER_FONT = FONT_BOLD;
139
+ const SEC_HEADER_SIZE = 18;
140
+ const SEC_HEADER_COLOR = "000000";
141
+ const SEC_HEADER_X = 0.39;
142
+ const SEC_HEADER_W = 9.0;
143
+ const SEC_HEADER_H = 0.5;
144
+
145
+ // Summary (narrow width)
146
+ const SUMMARY_HEADER_W = 4.3;
147
+ const SUMMARY_BULLET_W = 4.8;
148
+ const SUMMARY_BULLET_SIZE = 14;
149
+ const SUMMARY_BULLET_H = 0.34;
150
+ const SUMMARY_BULLET_Y_SPACING = 0.38;
151
+ const SUMMARY_BULLET_LINE_SPACING = 1.05;
152
+
153
+ // TOC items (separate shapes)
154
+ let TOC_FONT = FONT_BOLD;
155
+ const TOC_SIZE = 18;
156
+ const TOC_COLOR = "000000";
157
+ const TOC_X = 0.390;
158
+ const TOC_W = 7.500;
159
+ const TOC_H = 0.550;
160
+ const TOC_Y_START = 0.957;
161
+ const TOC_Y_SPACING = 0.650;
162
+
163
+ function weightedTextLength(text) {
164
+ let total = 0;
165
+ for (const ch of String(text || "")) {
166
+ if (/\s/.test(ch)) total += 0.35;
167
+ else if (/^[\x00-\x7F]$/.test(ch)) total += 0.58;
168
+ else total += 1.0;
169
+ }
170
+ return total;
171
+ }
172
+
173
+ function estimateWrappedLines(text, widthIn, fontSize) {
174
+ const usablePt = Math.max(12, (widthIn || 4) * 72 - 10);
175
+ // Conservative on purpose: PowerPoint wraps dense mixed
176
+ // Korean-English strings earlier than a naive ASCII width estimate.
177
+ const unitsPerLine = Math.max(4, usablePt / Math.max(4, fontSize * 0.95));
178
+ return Math.max(1, Math.ceil(weightedTextLength(text) / unitsPerLine));
179
+ }
180
+
181
+ function estimateTextHeight(text, widthIn, fontSize, lineSpacingMultiple) {
182
+ const lines = estimateWrappedLines(text, widthIn, fontSize);
183
+ return Math.max(0.14, ((lines * fontSize * lineSpacingMultiple) / 72) * 1.4 + 0.08);
184
+ }
185
+
186
+ function bodyElements(items) {
187
+ const elements = [];
188
+ let numberedCounter = 0;
189
+ for (const item of items) {
190
+ if (item.type === "section") {
191
+ elements.push({ kind: "section", text: item.heading || "" });
192
+ numberedCounter = 0;
193
+ for (const child of item.children || []) {
194
+ numberedCounter++;
195
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${child.text || ""}` });
196
+ }
197
+ } else if (item.type === "bullet") {
198
+ elements.push({ kind: "bullet", text: item.text || "" });
199
+ numberedCounter = 0;
200
+ for (const child of item.children || []) {
201
+ numberedCounter++;
202
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${child.text || ""}` });
203
+ }
204
+ } else if (item.type === "numbered") {
205
+ numberedCounter++;
206
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${item.text || ""}` });
207
+ } else if (item.type === "text") {
208
+ elements.push({ kind: "text", text: item.text || "" });
209
+ }
210
+ }
211
+ return elements;
212
+ }
213
+
214
+ function chooseBodyStyle(items, widthIn, startY, bottomY) {
215
+ const available = Math.max(0.5, (bottomY || 7.0) - startY);
216
+ const sizes = [16, 15, 14, 13];
217
+ for (const bodySize of sizes) {
218
+ const sectionSize = Math.min(18, bodySize + 2);
219
+ const lineSpacing = bodySize <= 10 ? 1.05 : 1.18;
220
+ const gap = bodySize <= 10 ? 0.035 : 0.05;
221
+ const total = bodyElements(items).reduce((sum, el) => {
222
+ const size = el.kind === "section" ? sectionSize : bodySize;
223
+ const w = el.kind === "section" ? Math.min(widthIn, SEC_HEADER_W) : widthIn;
224
+ return sum + estimateTextHeight(el.text, w, size, lineSpacing) + gap;
225
+ }, 0);
226
+ if (total <= available) return { bodySize, sectionSize, lineSpacing, gap };
227
+ }
228
+ return { bodySize: 13, sectionSize: 15, lineSpacing: 1.08, gap: 0.04 };
229
+ }
230
+
231
+ function estimateSectionHeight(item, secW, bulletW, style) {
232
+ let total = estimateTextHeight(item.heading || "", secW, style.sectionSize, style.lineSpacing) + style.gap;
233
+ let numberedCounter = 0;
234
+ for (const child of item.children || []) {
235
+ numberedCounter++;
236
+ total += estimateTextHeight(`(${numberedCounter}) ${child.text || ""}`, bulletW, style.bodySize, style.lineSpacing) + style.gap;
237
+ }
238
+ return total + 0.12;
239
+ }
240
+
241
+ function addSectionCard(slide, x, y, w, h) {
242
+ slide.addShape("rect", {
243
+ x,
244
+ y: y - 0.035,
245
+ w,
246
+ h: Math.max(0.28, h),
247
+ fill: { color: "F7F9F8", transparency: 12 },
248
+ line: { color: "D8E2DD", width: 0.5, transparency: 20 },
249
+ });
250
+ }
251
+
252
+ function bodyItemsToDiagnosticLines(items) {
253
+ const lines = [];
254
+ let numberedCounter = 0;
255
+ for (const item of items || []) {
256
+ if (item.type === "section") {
257
+ lines.push(`■ ${item.heading || ""}`);
258
+ numberedCounter = 0;
259
+ for (const child of item.children || []) {
260
+ numberedCounter++;
261
+ lines.push(`(${numberedCounter}) ${child.text || ""}`);
262
+ }
263
+ } else if (item.type === "bullet") {
264
+ lines.push(`• ${item.text || ""}`);
265
+ numberedCounter = 0;
266
+ for (const child of item.children || []) {
267
+ numberedCounter++;
268
+ lines.push(`(${numberedCounter}) ${child.text || ""}`);
269
+ }
270
+ } else if (item.type === "numbered") {
271
+ numberedCounter++;
272
+ lines.push(`(${numberedCounter}) ${item.text || ""}`);
273
+ } else if (item.type === "text") {
274
+ lines.push(item.text || "");
275
+ }
276
+ }
277
+ return lines.filter(Boolean);
278
+ }
279
+
280
+ function bodyItemsToDiagnosticCards(items) {
281
+ const cards = [];
282
+ let numberedCounter = 0;
283
+ for (const item of items || []) {
284
+ if (item.type === "section" || item.type === "bullet") {
285
+ const title = item.heading || item.text || "";
286
+ const body = [];
287
+ numberedCounter = 0;
288
+ for (const child of item.children || []) {
289
+ numberedCounter++;
290
+ body.push(`(${numberedCounter}) ${child.text || ""}`);
291
+ }
292
+ if (title || body.length) cards.push({ title, body });
293
+ } else if (item.type === "numbered") {
294
+ numberedCounter++;
295
+ cards.push({ title: `(${numberedCounter})`, body: [item.text || ""] });
296
+ } else if (item.type === "text" && item.text) {
297
+ cards.push({ title: "", body: [item.text] });
298
+ }
299
+ }
300
+ return cards;
301
+ }
302
+
303
+ function splitLinesByWeight(lines, columns) {
304
+ const groups = Array.from({ length: columns }, () => []);
305
+ const weights = Array(columns).fill(0);
306
+ for (const line of lines) {
307
+ let target = 0;
308
+ for (let i = 1; i < columns; i++) {
309
+ if (weights[i] < weights[target]) target = i;
310
+ }
311
+ groups[target].push(line);
312
+ weights[target] += Math.max(20, weightedTextLength(line));
313
+ }
314
+ return groups;
315
+ }
316
+
317
+ function splitCardsByWeight(cards, columns) {
318
+ const groups = Array.from({ length: columns }, () => []);
319
+ const weights = Array(columns).fill(0);
320
+ for (const card of cards) {
321
+ let target = 0;
322
+ for (let i = 1; i < columns; i++) {
323
+ if (weights[i] < weights[target]) target = i;
324
+ }
325
+ groups[target].push(card);
326
+ const cardWeight = weightedTextLength(card.title || "") * 1.4
327
+ + (card.body || []).reduce((sum, line) => sum + weightedTextLength(line), 0);
328
+ weights[target] += Math.max(80, cardWeight);
329
+ }
330
+ return groups;
331
+ }
332
+
333
+ function renderDiagnosticCard(slide, card, x, y, w, h, idx) {
334
+ slide.addShape("rect", {
335
+ x,
336
+ y,
337
+ w,
338
+ h,
339
+ fill: { color: idx % 2 ? "F8FAF9" : "F3F6F4", transparency: 2 },
340
+ line: { color: "D6E2DB", width: 0.35, transparency: 12 },
341
+ });
342
+ slide.addShape("rect", {
343
+ x,
344
+ y,
345
+ w: 0.028,
346
+ h,
347
+ fill: { color: SECTION_ACCENT, transparency: 0 },
348
+ line: { color: SECTION_ACCENT, transparency: 100 },
349
+ });
350
+
351
+ const padX = 0.055;
352
+ const padY = 0.05;
353
+ const titleH = card.title ? Math.min(0.28, Math.max(0.18, h * 0.16)) : 0;
354
+ if (card.title) {
355
+ slide.addText(card.title, {
356
+ x: x + padX,
357
+ y: y + padY,
358
+ w: w - padX * 1.45,
359
+ h: titleH,
360
+ fontFace: FONT_BOLD,
361
+ fontSize: 7.4,
362
+ color: "002554",
363
+ charSpacing: -0.25,
364
+ fit: "shrink",
365
+ margin: 0.01,
366
+ breakLine: false,
367
+ valign: "top",
368
+ });
369
+ }
370
+
371
+ const bodyText = (card.body || []).join("\n");
372
+ if (bodyText) {
373
+ slide.addText(bodyText, {
374
+ x: x + padX,
375
+ y: y + padY + titleH + 0.015,
376
+ w: w - padX * 1.45,
377
+ h: Math.max(0.16, h - titleH - padY * 1.6),
378
+ fontFace: FONT_MEDIUM,
379
+ fontSize: 5.85,
380
+ color: "1F1F1F",
381
+ charSpacing: -0.18,
382
+ fit: "shrink",
383
+ margin: 0.008,
384
+ paraSpaceAfterPt: 0,
385
+ lineSpacingMultiple: 0.96,
386
+ breakLine: false,
387
+ valign: "top",
388
+ });
389
+ }
390
+ }
391
+
392
+ function estimateDiagnosticCardHeight(card, w) {
393
+ const padY = 0.05;
394
+ const titleH = card.title ? 0.24 : 0;
395
+ const bodyText = (card.body || []).join("\n");
396
+ const bodyH = bodyText ? estimateTextHeight(bodyText, Math.max(0.8, w - 0.12), 5.85, 1.02) : 0;
397
+ return Math.max(0.56, Math.min(2.35, padY * 2 + titleH + bodyH));
398
+ }
399
+
400
+ function renderCramStressBody(slide, items, pageW, pageH) {
401
+ const cards = bodyItemsToDiagnosticCards(items);
402
+ const columns = 3;
403
+ const groups = cards.length ? splitCardsByWeight(cards, columns) : splitLinesByWeight(bodyItemsToDiagnosticLines(items), columns).map((group) => [{ title: "", body: group }]);
404
+ const x = 0.28;
405
+ const y = 0.98;
406
+ const w = 7.78;
407
+ const h = pageH - y - 0.34;
408
+ const gap = 0.105;
409
+ const colW = (w - gap * (columns - 1)) / columns;
410
+ groups.forEach((group, idx) => {
411
+ const colX = x + idx * (colW + gap);
412
+ const cardGap = 0.075;
413
+ const availableH = h - cardGap * Math.max(0, group.length - 1);
414
+ const desiredHeights = group.map((card) => estimateDiagnosticCardHeight(card, colW));
415
+ const desiredTotal = desiredHeights.reduce((sum, value) => sum + value, 0) || 1;
416
+ const scale = desiredTotal > availableH ? availableH / desiredTotal : 1;
417
+ let cursorY = y;
418
+ group.forEach((card, cardIdx) => {
419
+ const remainingCards = group.length - cardIdx - 1;
420
+ const rawH = desiredHeights[cardIdx] * scale;
421
+ const cardH = Math.max(0.62, Math.min(rawH, h - (cursorY - y) - remainingCards * (0.62 + cardGap)));
422
+ renderDiagnosticCard(slide, card, colX, cursorY, colW, cardH, idx + cardIdx);
423
+ cursorY += cardH + cardGap;
424
+ });
425
+ });
426
+ }
427
+
428
+ function renderCramStressImageSheet(slide, content, sourceDir, pageW, pageH) {
429
+ if (!content) return;
430
+ const images = content.type === "image-grid"
431
+ ? (Array.isArray(content.images) ? content.images : [])
432
+ : content.type === "image" ? [content] : [];
433
+ if (images.length === 0) return;
434
+ const x = 8.24;
435
+ const y = 0.98;
436
+ const w = pageW - x - 0.25;
437
+ const h = pageH - y - 0.34;
438
+ const cols = images.length >= 8 ? 2 : 1;
439
+ const rows = Math.ceil(images.length / cols);
440
+ const gap = 0.04;
441
+ const cellW = (w - gap * (cols - 1)) / cols;
442
+ const headerH = 0.28;
443
+ const sheetH = Math.min(h - 0.9, rows * 0.58 + gap * (rows - 1));
444
+ const cellH = (sheetH - gap * (rows - 1)) / rows;
445
+ const noteY = y + headerH + sheetH + 0.16;
446
+ const noteH = Math.min(1.0, h - (noteY - y) - 0.08);
447
+ const panelH = noteY - y + noteH;
448
+ slide.addShape("rect", {
449
+ x: x - 0.04,
450
+ y: y - 0.04,
451
+ w: w + 0.08,
452
+ h: panelH + 0.08,
453
+ fill: { color: "F5F8F6", transparency: 0 },
454
+ line: { color: "CAD4CE", width: 0.5 },
455
+ });
456
+ slide.addText("RAW EVIDENCE CONTACT SHEET", {
457
+ x,
458
+ y: y + 0.02,
459
+ w,
460
+ h: 0.16,
461
+ fontFace: FONT_BOLD,
462
+ fontSize: 5.8,
463
+ color: "002554",
464
+ charSpacing: -0.15,
465
+ fit: "shrink",
466
+ margin: 0,
467
+ });
468
+ slide.addShape("rect", {
469
+ x,
470
+ y: y + 0.22,
471
+ w,
472
+ h: 0,
473
+ line: { color: SECTION_ACCENT, width: 0.45 },
474
+ });
475
+ images.forEach((image, idx) => {
476
+ const imgPath = resolveImagePath(image.src, sourceDir);
477
+ const col = idx % cols;
478
+ const row = Math.floor(idx / cols);
479
+ const fit = containBox(
480
+ x + col * (cellW + gap),
481
+ y + headerH + row * (cellH + gap),
482
+ cellW,
483
+ cellH,
484
+ readImageSize(imgPath),
485
+ );
486
+ slide.addImage({
487
+ path: imgPath,
488
+ ...fit,
489
+ altText: image.caption || path.basename(image.src || imgPath),
490
+ });
491
+ });
492
+ slide.addShape("rect", {
493
+ x,
494
+ y: noteY,
495
+ w,
496
+ h: noteH,
497
+ fill: { color: "FFFFFF", transparency: 0 },
498
+ line: { color: "D6E2DB", width: 0.35, transparency: 20 },
499
+ });
500
+ slide.addText("diagnostic cram-stress\nmicrotext is intentional\nproduction decks must synthesize\noverflow / marker / missing media = fail", {
501
+ x: x + 0.06,
502
+ y: noteY + 0.07,
503
+ w: w - 0.12,
504
+ h: Math.max(0.24, noteH - 0.14),
505
+ fontFace: FONT_MEDIUM,
506
+ fontSize: 5.3,
507
+ color: "333333",
508
+ charSpacing: -0.15,
509
+ fit: "shrink",
510
+ margin: 0.01,
511
+ breakLine: false,
512
+ lineSpacingMultiple: 1.02,
513
+ valign: "mid",
514
+ });
515
+ }
516
+
517
+ function isCramStressDeck(resolved) {
518
+ const meta = (resolved && resolved.deck && resolved.deck.metadata) || {};
519
+ return String(meta.render_mode || meta.renderMode || "").toLowerCase() === "cram-stress";
520
+ }
521
+
522
+ function renderCramStressSlide(slide, slideSpec, pageW, pageH, sourceDir) {
523
+ const regions = slideSpec.regions || {};
524
+ const title = regions.title && regions.title.content;
525
+ if (title) {
526
+ slide.addText(String(title), {
527
+ x: 0.24,
528
+ y: 0.24,
529
+ w: pageW - 1.72,
530
+ h: 0.46,
531
+ fontFace: FONT_BOLD,
532
+ fontSize: 13.5,
533
+ color: "000000",
534
+ charSpacing: CHAR_SPACING,
535
+ fit: "shrink",
536
+ });
537
+ slide.addShape("rect", {
538
+ x: 0.24,
539
+ y: 0.76,
540
+ w: pageW - 0.48,
541
+ h: 0,
542
+ line: { color: SECTION_ACCENT, width: 1.2 },
543
+ });
544
+ }
545
+ if (regions.body && regions.body.content && regions.body.content.type === "body") {
546
+ renderCramStressBody(slide, regions.body.content.items || [], pageW, pageH);
547
+ }
548
+ if (regions.image && regions.image.content) {
549
+ renderCramStressImageSheet(slide, regions.image.content, sourceDir, pageW, pageH);
550
+ }
551
+ }
552
+
553
+ function shouldUseDenseBodyFlow(items, widthIn, startY, bottomY) {
554
+ const elements = bodyElements(items);
555
+ const charCount = elements.reduce((sum, el) => sum + String(el.text || "").length, 0);
556
+ const available = Math.max(0.5, (bottomY || 7.0) - startY);
557
+ const estimatedAtMin = elements.reduce((sum, el) => {
558
+ const size = el.kind === "section" ? 12.8 : 11.5;
559
+ return sum + estimateTextHeight(el.text, Math.max(1.8, widthIn / 2), size, 1.02) + 0.025;
560
+ }, 0);
561
+ return widthIn >= 4.4 && (elements.length >= 18 || charCount >= 2400 || estimatedAtMin > available * 1.35);
562
+ }
563
+
564
+ function chooseDenseBodyStyle() {
565
+ return { bodySize: 11.5, sectionSize: 12.8, lineSpacing: 1.02, gap: 0.025 };
566
+ }
567
+
568
+ // ── Font role helpers ───────────────────────────────────────────────────────
569
+
570
+ /**
571
+ * Build font role map from template capabilities.
572
+ * Maps YAML field names to pptxgenjs option names.
573
+ */
574
+ function buildFontRoles(templateObj) {
575
+ if (!templateObj || !templateObj.capabilities || !templateObj.capabilities.font_roles) {
576
+ return {};
577
+ }
578
+
579
+ const roles = {};
580
+ for (const [name, def] of Object.entries(templateObj.capabilities.font_roles)) {
581
+ roles[name] = {
582
+ font: def.font,
583
+ size: def.size,
584
+ color: (def.color || "#000000").replace("#", ""),
585
+ align: def.align || "left",
586
+ charSpacing: def.char_spacing,
587
+ lineSpacing: def.line_spacing,
588
+ bold: def.bold,
589
+ spaceAfter: def.space_after,
590
+ bulletType: def.bullet_type,
591
+ bulletChar: def.bullet_char,
592
+ bulletFont: def.bullet_font,
593
+ numberType: def.number_type,
594
+ marL: def.marL,
595
+ indent: def.indent,
596
+ fill: def.fill ? def.fill.replace("#", "") : undefined,
597
+ borderColor: def.border_color ? def.border_color.replace("#", "") : undefined,
598
+ borderWidth: def.border_width,
599
+ };
600
+ }
601
+ return roles;
602
+ }
603
+
604
+ // ── Decoration rendering ────────────────────────────────────────────────────
605
+
606
+ /**
607
+ * Add decoration elements to a pptxgenjs slide.
608
+ * Note: slide_number is intentionally omitted — the reference template has none.
609
+ */
610
+ function addDecorations(slide, decorations, slideIdx, fontRoles) {
611
+ if (!decorations || !Array.isArray(decorations)) return;
612
+
613
+ for (const d of decorations) {
614
+ if (d.type === "image") {
615
+ slide.addImage({
616
+ path: decorAssetPath(d),
617
+ x: d.x || 0,
618
+ y: d.y || 0,
619
+ w: d.w || 1,
620
+ h: d.h || 1,
621
+ rotate: d.rotate || undefined,
622
+ altText: d.asset || "template-image",
623
+ });
624
+
625
+ } else if (d.type === "rect" || d.type === "roundRect" || d.type === "pill") {
626
+ // Solid color block / rounded card / full-radius pill. Optional text label.
627
+ const shape = d.type === "rect" ? "rect" : "roundRect";
628
+ const opts = {
629
+ x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || 0.5,
630
+ };
631
+ if (d.fill) opts.fill = { color: hex(d.fill), transparency: d.alpha || 0 };
632
+ else opts.fill = { type: "none" };
633
+ if (d.line) opts.line = { color: hex(d.line), width: d.line_width || 1 };
634
+ else opts.line = { type: "none" };
635
+ if (shape === "roundRect") opts.rectRadius = d.type === "pill" ? (d.h || 0.5) / 2 : (d.radius != null ? d.radius : 0.12);
636
+ slide.addShape(shape, opts);
637
+ if (d.text) {
638
+ slide.addText(d.text, {
639
+ x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || 0.5,
640
+ fontFace: d.font || FONT_BOLD, fontSize: d.size || 12,
641
+ color: hex(d.text_color || "#FFFFFF"), bold: !!d.bold,
642
+ align: d.align || "center", valign: "middle",
643
+ charSpacing: d.char_spacing != null ? d.char_spacing : CHAR_SPACING,
644
+ });
645
+ }
646
+
647
+ } else if (d.type === "ellipse" || d.type === "ring") {
648
+ const opts = { x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || (d.w || 1) };
649
+ if (d.type === "ring" || !d.fill) {
650
+ opts.fill = { type: "none" };
651
+ opts.line = { color: hex(d.line || d.color || "#FFFFFF"), width: d.line_width || 2 };
652
+ } else {
653
+ opts.fill = { color: hex(d.fill), transparency: d.alpha || 0 };
654
+ opts.line = { type: "none" };
655
+ }
656
+ slide.addShape("ellipse", opts);
657
+
658
+ } else if (d.type === "text") {
659
+ slide.addText(d.text || "", {
660
+ x: d.x || 0, y: d.y || 0, w: d.w || 4, h: d.h || 1,
661
+ fontFace: d.font || FONT_BOLD, fontSize: d.size || 14,
662
+ color: hex(d.color || "#000000"), bold: !!d.bold,
663
+ align: d.align || "left", valign: d.valign || "top",
664
+ charSpacing: d.char_spacing != null ? d.char_spacing : CHAR_SPACING,
665
+ transparency: d.transparency != null ? d.transparency : undefined,
666
+ });
667
+
668
+ } else if (d.type === "line") {
669
+ slide.addShape("rect", {
670
+ x: d.x || 0,
671
+ y: d.y || 0,
672
+ w: d.w || 1,
673
+ h: 0,
674
+ line: { color: (d.color || "#000000").replace("#", ""), width: d.width || 1 },
675
+ });
676
+
677
+ } else if (d.type === "confidential_mark") {
678
+ const role = fontRoles.confidential_mark || {};
679
+ const mX = d.x || 0, mY = d.y || 0, mW = d.w || 1, mH = d.h || 0.3;
680
+ const bColor = role.borderColor || "C00000";
681
+ const bPt = role.borderWidth || 1;
682
+ slide.addShape("rect", {
683
+ x: mX, y: mY, w: mW, h: mH,
684
+ line: { color: bColor, width: bPt },
685
+ });
686
+ slide.addText(typeof d.text === "string" && d.text.trim() ? d.text : "대외비", {
687
+ x: mX, y: mY, w: mW, h: mH,
688
+ fontFace: role.font || FONT_MEDIUM,
689
+ fontSize: role.size || 14,
690
+ color: role.color || "C00000",
691
+ align: role.align || "center",
692
+ valign: "middle",
693
+ });
694
+
695
+ } else if (d.type === "disclaimer") {
696
+ const role = fontRoles.disclaimer || {};
697
+ slide.addText(
698
+ typeof d.text === "string" && d.text.trim() ? d.text : "※ 본 문서는 대외비입니다.",
699
+ {
700
+ x: d.x || 0,
701
+ y: d.y || 0,
702
+ w: d.w || 1,
703
+ h: d.h || 0.2,
704
+ fontFace: role.font || FONT_LIGHT,
705
+ fontSize: role.size || 6,
706
+ color: role.color || "666666",
707
+ align: role.align || "left",
708
+ }
709
+ );
710
+ }
711
+ // slide_number intentionally removed — reference template has none
712
+ }
713
+ }
714
+
715
+ // ── Body shape rendering (individual shapes per line) ────────────────────
716
+
717
+ /**
718
+ * Add body items as individual shapes to a slide.
719
+ * Each section header and bullet item is a separate positioned shape.
720
+ *
721
+ * @param {object} slide - pptxgenjs slide
722
+ * @param {Array} items - AST items (section, bullet, numbered, text)
723
+ * @param {number} startY - Starting y position
724
+ * @param {number} maxX - Max width for positioning
725
+ * @returns {number} The y position after the last item
726
+ */
727
+ function addBodyShapes(slide, items, startY, maxX, bottomY, originX) {
728
+ const denseFlow = shouldUseDenseBodyFlow(items, maxX, startY, bottomY || 7.0);
729
+ const columns = denseFlow ? 2 : 1;
730
+ const colGap = denseFlow ? 0.16 : 0;
731
+ const colW = (maxX - colGap * (columns - 1)) / columns;
732
+ let col = 0;
733
+ let y = startY;
734
+ // Template regions have always drawn body text at the template's own left
735
+ // margin. A placed box carries its own, and passes it here.
736
+ const baseX = originX != null ? originX : SEC_HEADER_X;
737
+ const bodyIndent = denseFlow ? 0.13 : (BULLET_X - SEC_HEADER_X);
738
+ const BODY_FULL_W = 10.188;
739
+ const scale = colW / BODY_FULL_W;
740
+ const secW = denseFlow ? Math.max(1.4, colW - 0.04) : SEC_HEADER_W * scale;
741
+ const bulletW = denseFlow ? Math.max(1.2, colW - bodyIndent - 0.02) : BULLET_W * scale;
742
+ let numberedCounter = 0;
743
+ const style = denseFlow ? chooseDenseBodyStyle() : chooseBodyStyle(items, bulletW, startY, bottomY || 7.0);
744
+
745
+ function colX() { return baseX + col * (colW + colGap); }
746
+ function secX() { return colX(); }
747
+ function bulletX() { return colX() + bodyIndent; }
748
+ function maybeAdvanceColumn(nextHeight) {
749
+ if (!denseFlow || col >= columns - 1) return;
750
+ if (y + nextHeight > (bottomY || 7.0)) {
751
+ col++;
752
+ y = startY;
753
+ }
754
+ }
755
+
756
+ for (const item of items) {
757
+ if (item.type === "section") {
758
+ const sectionH = estimateTextHeight(item.heading, secW, style.sectionSize, style.lineSpacing);
759
+ const cardH = estimateSectionHeight(item, secW, bulletW, style);
760
+ maybeAdvanceColumn(Math.min(cardH, sectionH + 0.2));
761
+ addSectionCard(slide, secX() - 0.035, y - 0.03, Math.max(secW, bulletW + bodyIndent) + 0.07, cardH);
762
+ // Section header — separate shape, black, 1.5 line spacing
763
+ slide.addText(mdRuns(item.heading, {
764
+ fontFace: SEC_HEADER_FONT,
765
+ fontSize: style.sectionSize,
766
+ color: SEC_HEADER_COLOR,
767
+ charSpacing: CHAR_SPACING,
768
+ }), {
769
+ x: secX(),
770
+ y: y,
771
+ w: secW,
772
+ h: sectionH,
773
+ lineSpacingMultiple: style.lineSpacing,
774
+ });
775
+ y += sectionH + style.gap;
776
+
777
+ // Numbered children with (1)(2)(3) format
778
+ if (item.children) {
779
+ numberedCounter = 0;
780
+ for (const child of item.children) {
781
+ numberedCounter++;
782
+ const numPrefix = `(${numberedCounter}) `;
783
+ const text = numPrefix + cleanMd(child.text);
784
+ const h = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
785
+ maybeAdvanceColumn(h);
786
+ slide.addText([
787
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
788
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
789
+ ], {
790
+ x: bulletX(),
791
+ y: y,
792
+ w: bulletW,
793
+ h,
794
+ lineSpacingMultiple: style.lineSpacing,
795
+ });
796
+ y += h + style.gap;
797
+ }
798
+ }
799
+ y += 0.08;
800
+
801
+ } else if (item.type === "bullet") {
802
+ const h = estimateTextHeight(item.text, bulletW, style.bodySize, style.lineSpacing);
803
+ maybeAdvanceColumn(h);
804
+ // Bullet item — green prefix + black text
805
+ slide.addText([
806
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: style.bodySize, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
807
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
808
+ ], {
809
+ x: bulletX(),
810
+ y: y,
811
+ w: bulletW,
812
+ h,
813
+ lineSpacingMultiple: style.lineSpacing,
814
+ });
815
+ y += h + style.gap;
816
+
817
+ // Children (numbered sub-items)
818
+ if (item.children) {
819
+ numberedCounter = 0;
820
+ for (const child of item.children) {
821
+ numberedCounter++;
822
+ const numPrefix = `(${numberedCounter}) `;
823
+ const text = numPrefix + cleanMd(child.text);
824
+ const childH = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
825
+ maybeAdvanceColumn(childH);
826
+ slide.addText([
827
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
828
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
829
+ ], {
830
+ x: bulletX(),
831
+ y: y,
832
+ w: bulletW,
833
+ h: childH,
834
+ lineSpacingMultiple: style.lineSpacing,
835
+ });
836
+ y += childH + style.gap;
837
+ }
838
+ }
839
+
840
+ } else if (item.type === "numbered") {
841
+ numberedCounter++;
842
+ const numPrefix = `(${numberedCounter}) `;
843
+ const text = numPrefix + cleanMd(item.text);
844
+ const h = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
845
+ maybeAdvanceColumn(h);
846
+ slide.addText([
847
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
848
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
849
+ ], {
850
+ x: bulletX(),
851
+ y: y,
852
+ w: bulletW,
853
+ h,
854
+ lineSpacingMultiple: style.lineSpacing,
855
+ });
856
+ y += h + style.gap;
857
+
858
+ } else if (item.type === "text") {
859
+ const h = estimateTextHeight(item.text, bulletW, style.bodySize, style.lineSpacing);
860
+ maybeAdvanceColumn(h);
861
+ slide.addText(mdRuns(item.text, {
862
+ fontFace: FONT_MEDIUM,
863
+ fontSize: style.bodySize,
864
+ color: "000000",
865
+ charSpacing: CHAR_SPACING,
866
+ }), {
867
+ x: bulletX(),
868
+ y: y,
869
+ w: bulletW,
870
+ h,
871
+ lineSpacingMultiple: style.lineSpacing,
872
+ });
873
+ y += h + style.gap;
874
+ }
875
+ }
876
+
877
+ return y;
878
+ }
879
+
880
+ /**
881
+ * Add summary group content as individual shapes.
882
+ * Uses narrow width (~4.3" for headers, ~4.1" for bullets).
883
+ */
884
+ function addSummaryShapes(slide, content, region) {
885
+ let y = region.y || 1.04;
886
+
887
+ // Group heading
888
+ if (content.heading) {
889
+ slide.addText(mdRuns(content.heading, {
890
+ fontFace: SEC_HEADER_FONT,
891
+ fontSize: SEC_HEADER_SIZE,
892
+ color: SEC_HEADER_COLOR,
893
+ charSpacing: CHAR_SPACING,
894
+ }), {
895
+ x: region.x || 0.39,
896
+ y: y,
897
+ w: SUMMARY_HEADER_W,
898
+ h: SEC_HEADER_H,
899
+ });
900
+ y += SEC_HEADER_H + 0.08;
901
+ }
902
+
903
+ // Items
904
+ if (content.items) {
905
+ for (const item of content.items) {
906
+ if (item.type === "section") {
907
+ // Section header
908
+ slide.addText(mdRuns(item.heading, {
909
+ fontFace: SEC_HEADER_FONT,
910
+ fontSize: SEC_HEADER_SIZE,
911
+ color: SEC_HEADER_COLOR,
912
+ charSpacing: CHAR_SPACING,
913
+ }), {
914
+ x: region.x || 0.39,
915
+ y: y,
916
+ w: SUMMARY_HEADER_W,
917
+ h: SEC_HEADER_H,
918
+ });
919
+ y += SEC_HEADER_H + 0.05;
920
+
921
+ // Children as bullet items
922
+ if (item.children) {
923
+ for (const child of item.children) {
924
+ slide.addText([
925
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
926
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
927
+ ], {
928
+ x: 0.59,
929
+ y: y,
930
+ w: SUMMARY_BULLET_W,
931
+ h: SUMMARY_BULLET_H,
932
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
933
+ });
934
+ y += SUMMARY_BULLET_Y_SPACING;
935
+ }
936
+ }
937
+
938
+ } else if (item.type === "bullet") {
939
+ slide.addText([
940
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
941
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
942
+ ], {
943
+ x: 0.59,
944
+ y: y,
945
+ w: SUMMARY_BULLET_W,
946
+ h: SUMMARY_BULLET_H,
947
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
948
+ });
949
+ y += SUMMARY_BULLET_Y_SPACING;
950
+
951
+ if (item.children) {
952
+ for (const child of item.children) {
953
+ slide.addText([
954
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
955
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
956
+ ], {
957
+ x: 0.59,
958
+ y: y,
959
+ w: SUMMARY_BULLET_W,
960
+ h: SUMMARY_BULLET_H,
961
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
962
+ });
963
+ y += SUMMARY_BULLET_Y_SPACING;
964
+ }
965
+ }
966
+
967
+ } else if (item.type === "numbered") {
968
+ slide.addText([
969
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
970
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
971
+ ], {
972
+ x: 0.59,
973
+ y: y,
974
+ w: SUMMARY_BULLET_W,
975
+ h: SUMMARY_BULLET_H,
976
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
977
+ });
978
+ y += SUMMARY_BULLET_Y_SPACING;
979
+ }
980
+ }
981
+ }
982
+ }
983
+
984
+ // ── TOC rendering (separate shapes per item) ─────────────────────────────
985
+
986
+ /**
987
+ * Add TOC items as separate text shapes, matching TEMPLATE-PPTX.pptx exactly.
988
+ * Each item is: "N. text" in Pretendard 18pt, w=7.5, h=0.55, spaced 0.65" apart.
989
+ */
990
+ function addTocShapes(slide, items) {
991
+ let y = TOC_Y_START;
992
+ let num = 1;
993
+
994
+ for (const item of items) {
995
+ const text = `${num}. ${cleanMd(item.text || item.heading || "")}`;
996
+ slide.addText(text, {
997
+ x: TOC_X,
998
+ y: y,
999
+ w: TOC_W,
1000
+ h: TOC_H,
1001
+ fontFace: TOC_FONT,
1002
+ fontSize: TOC_SIZE,
1003
+ color: TOC_COLOR,
1004
+ charSpacing: CHAR_SPACING,
1005
+ });
1006
+ y += TOC_Y_SPACING;
1007
+ num++;
1008
+ }
1009
+ }
1010
+
1011
+ /**
1012
+ * Check if a slide is a TOC slide by inspecting the title region content.
1013
+ */
1014
+ function isTocSlide(regions) {
1015
+ const title = regions.title;
1016
+ if (!title || typeof title.content !== "string") return false;
1017
+ return title.content.includes("목차");
1018
+ }
1019
+
1020
+ function adjustedRegionsForImageColumn(regions) {
1021
+ const body = regions.body;
1022
+ const image = regions.image;
1023
+ if (!body || !image || !body.content || !image.content) return regions;
1024
+ if (body.content.type !== "body" || !["image", "image-grid"].includes(image.content.type)) return regions;
1025
+
1026
+ const bodyX = body.x || 0;
1027
+ const bodyW = body.w || 10.188;
1028
+ const imageX = image.x || 0;
1029
+ const gap = 0.25;
1030
+ if (imageX <= bodyX || imageX >= bodyX + bodyW) return regions;
1031
+
1032
+ const denseImageGrid = image.content.type === "image-grid" && Array.isArray(image.content.images) && image.content.images.length >= 6;
1033
+ return {
1034
+ ...regions,
1035
+ body: {
1036
+ ...body,
1037
+ w: Math.max(0.5, imageX - bodyX - gap),
1038
+ },
1039
+ image: denseImageGrid ? {
1040
+ ...image,
1041
+ y: Math.min(image.y || 1.5, 1.08),
1042
+ h: Math.max(image.h || 3.0, 5.85),
1043
+ } : image,
1044
+ figure_caption: denseImageGrid && regions.figure_caption ? {
1045
+ ...regions.figure_caption,
1046
+ y: Math.max(regions.figure_caption.y || 4.55, 7.02),
1047
+ } : regions.figure_caption,
1048
+ };
1049
+ }
1050
+
1051
+ // ── Region rendering ────────────────────────────────────────────────────────
1052
+
1053
+ /**
1054
+ * Add a region as a positioned element on the slide.
1055
+ *
1056
+ * Content types:
1057
+ * - string: title, main-box, notes
1058
+ * - { type: "body" } bullet/section/numbered items → individual shapes
1059
+ * - { type: "kpi-table" } table with headers and rows
1060
+ * - { type: "image" } image element
1061
+ * - { type: "figure-caption" } figure caption (auto-numbered "도 N.")
1062
+ * - { type: "table-caption" } table caption (auto-numbered "표 N.")
1063
+ * - { type: "summary-group" } heading + items → individual shapes
1064
+ */
1065
+ function addRegion(slide, name, region, fontRoles, pageH, regionBottom, isToc, sourceDir, palette) {
1066
+ if (!region) return;
1067
+
1068
+ const content = region.content;
1069
+ if (content === undefined || content === null) return;
1070
+ if (typeof content === "string" && !content.trim()) return;
1071
+ if (typeof content === "object" && !content.type) return;
1072
+
1073
+ const roleName = region.font_role || region.heading_role || region.sub_item_role;
1074
+ const role = fontRoles[roleName] || {};
1075
+
1076
+ // ── String content (title, main-box, notes) ────────────────────────
1077
+ if (typeof content === "string") {
1078
+ const opts = {
1079
+ x: region.x || 0,
1080
+ y: region.y || 0,
1081
+ w: region.w || 4,
1082
+ h: region.h || ((regionBottom || (pageH - 0.5)) - (region.y || 0)),
1083
+ fontFace: role.font || FONT_BOLD,
1084
+ fontSize: role.size || 16,
1085
+ color: (role.color || "000000").replace("#", ""),
1086
+ align: region.align || role.align || "left",
1087
+ valign: region.valign || "top",
1088
+ };
1089
+
1090
+ if (role.charSpacing != null) opts.charSpacing = role.charSpacing;
1091
+ if (role.lineSpacing) opts.lineSpacingMultiple = role.lineSpacing;
1092
+
1093
+ // Border (main_box)
1094
+ if (region.border) {
1095
+ opts.line = {
1096
+ color: (region.border.color || "#000000").replace("#", ""),
1097
+ width: region.border.width || 1,
1098
+ };
1099
+ }
1100
+
1101
+ // Autofit
1102
+ if (region.autofit === "resize") opts.autoFit = true;
1103
+ else if (region.autofit === "shrink") opts.shrinkText = true;
1104
+
1105
+ slide.addText(mdRuns(content, {
1106
+ fontFace: opts.fontFace,
1107
+ fontSize: opts.fontSize,
1108
+ color: opts.color,
1109
+ ...(opts.charSpacing != null ? { charSpacing: opts.charSpacing } : {}),
1110
+ }), opts);
1111
+ return;
1112
+ }
1113
+
1114
+ // ── Body content → TOC shapes or individual shapes ──────────────────
1115
+ if (content.type === "body") {
1116
+ if (isToc) {
1117
+ addTocShapes(slide, content.items || []);
1118
+ } else {
1119
+ const startY = region.y || 1.044;
1120
+ addBodyShapes(slide, content.items || [], startY, region.w || 10.188, regionBottom || pageH - 0.5, region.originX);
1121
+ }
1122
+ return;
1123
+ }
1124
+
1125
+ // Native PowerPoint chart: the data stays editable in the deck workbook.
1126
+ if (content.type === "chart") {
1127
+ const ink = hex((palette && palette.ink) || "#0E1B2C");
1128
+ const primary = hex((palette && palette.primary) || "#1D4ED8");
1129
+ slide.addChart(content.kind, content.series, {
1130
+ x: region.x || 0.84, y: region.y || 2.05,
1131
+ w: region.w || 11.65, h: region.h || 3.7,
1132
+ showLegend: content.series.length > 1,
1133
+ legendPos: "b",
1134
+ showTitle: false, showValue: false,
1135
+ showCatName: false, showSerName: false,
1136
+ catAxisLabelFontFace: FONT_MEDIUM, catAxisLabelFontSize: 11,
1137
+ valAxisLabelFontFace: FONT_MEDIUM, valAxisLabelFontSize: 11,
1138
+ chartColors: [primary, hex((palette && palette.azure_soft) || "#93C5FD"), ink],
1139
+ showMarker: content.kind === "line",
1140
+ showLine: content.kind === "line",
1141
+ showShadow: false,
1142
+ });
1143
+ return;
1144
+ }
1145
+
1146
+ // ── KPI Table ──────────────────────────────────────────────────────
1147
+ if (content.type === "kpi-table") {
1148
+ const headerRole = fontRoles[region.font_role_header] || fontRoles.table_header;
1149
+ const dataRole = fontRoles[region.font_role_data] || fontRoles.table_data;
1150
+
1151
+ const tableData = [];
1152
+
1153
+ if (content.headers && content.headers.length > 0) {
1154
+ tableData.push(
1155
+ content.headers.map((h) => ({
1156
+ text: cleanMd(h),
1157
+ options: {
1158
+ bold: false,
1159
+ fontSize: headerRole ? headerRole.size : 14,
1160
+ fontFace: headerRole ? headerRole.font : FONT_BOLD,
1161
+ color: headerRole ? headerRole.color : "000000",
1162
+ align: headerRole ? headerRole.align : "left",
1163
+ valign: "middle",
1164
+ ...(headerRole && headerRole.charSpacing != null
1165
+ ? { charSpacing: headerRole.charSpacing }
1166
+ : {}),
1167
+ ...(headerRole && headerRole.fill
1168
+ ? { fill: { color: headerRole.fill.replace("#", "") } }
1169
+ : {}),
1170
+ },
1171
+ }))
1172
+ );
1173
+ }
1174
+
1175
+ if (content.rows) {
1176
+ for (const row of content.rows) {
1177
+ tableData.push(
1178
+ row.map((cell) => linkedTableCell(cell, {
1179
+ fontSize: dataRole ? dataRole.size : 14,
1180
+ fontFace: dataRole ? dataRole.font : FONT_MEDIUM,
1181
+ color: dataRole ? dataRole.color : "000000",
1182
+ align: dataRole ? dataRole.align : "left",
1183
+ valign: "middle",
1184
+ ...(dataRole && dataRole.charSpacing != null
1185
+ ? { charSpacing: dataRole.charSpacing }
1186
+ : {}),
1187
+ }))
1188
+ );
1189
+ }
1190
+ }
1191
+
1192
+ const numCols = (content.headers && content.headers.length) || 1;
1193
+ const tblX = region.x || 0;
1194
+ const tblY = region.y || 0;
1195
+ const tblW = region.w || 4;
1196
+ slide.addTable(tableData, {
1197
+ x: tblX,
1198
+ y: tblY,
1199
+ w: tblW,
1200
+ border: { pt: 1.5, color: "000000" },
1201
+ colW: Array(numCols).fill(tblW / numCols),
1202
+ margin: [3.6, 7.2, 3.6, 7.2],
1203
+ rowH: 0.35,
1204
+ });
1205
+ return;
1206
+ }
1207
+
1208
+ // ── Image ──────────────────────────────────────────────────────────
1209
+ if (content.type === "image") {
1210
+ const imgPath = resolveImagePath(content.src, sourceDir);
1211
+ const fit = containBox(
1212
+ region.x || 0,
1213
+ region.y || 0,
1214
+ region.w || 2,
1215
+ region.h || 1,
1216
+ readImageSize(imgPath),
1217
+ );
1218
+ slide.addImage({
1219
+ path: imgPath,
1220
+ ...fit,
1221
+ altText: content.caption || path.basename(content.src || imgPath),
1222
+ });
1223
+ return;
1224
+ }
1225
+
1226
+ if (content.type === "image-grid") {
1227
+ const images = Array.isArray(content.images) ? content.images : [];
1228
+ const count = images.length;
1229
+ if (count === 0) return;
1230
+ const cols = count <= 2 ? count : 2;
1231
+ const rows = Math.ceil(count / cols);
1232
+ const gap = 0.06;
1233
+ const baseX = region.x || 0;
1234
+ const baseY = region.y || 0;
1235
+ const baseW = region.w || 4;
1236
+ const baseH = region.h || 3;
1237
+ const cellW = (baseW - gap * (cols - 1)) / cols;
1238
+ const cellH = (baseH - gap * (rows - 1)) / rows;
1239
+ images.forEach((image, idx) => {
1240
+ const imgPath = resolveImagePath(image.src, sourceDir);
1241
+ const col = idx % cols;
1242
+ const row = Math.floor(idx / cols);
1243
+ const fit = containBox(
1244
+ baseX + col * (cellW + gap),
1245
+ baseY + row * (cellH + gap),
1246
+ cellW,
1247
+ cellH,
1248
+ readImageSize(imgPath),
1249
+ );
1250
+ slide.addImage({
1251
+ path: imgPath,
1252
+ ...fit,
1253
+ altText: image.caption || path.basename(image.src || imgPath),
1254
+ });
1255
+ });
1256
+ return;
1257
+ }
1258
+
1259
+ // ── Figure Caption ────────────────────────────────────────────────
1260
+ if (content.type === "figure-caption") {
1261
+ const num = content.number || 1;
1262
+ const prefix = content.prefix || "도";
1263
+ const captionText = content.caption ? `${prefix} ${num}. ${cleanMd(content.caption)}` : `${prefix} ${num}.`;
1264
+ const captionRole = fontRoles.figure_caption || {};
1265
+ const captionW = region.w || 4;
1266
+ const captionSize = captionRole.size || 16;
1267
+ const captionSpacing = captionRole.lineSpacing || 1.3;
1268
+ slide.addText(captionText, {
1269
+ x: region.x || 0,
1270
+ y: region.y || 0,
1271
+ w: captionW,
1272
+ // A caption that wraps needs a box tall enough to hold every line, or it
1273
+ // overflows its frame. The declared region height is the floor, not the cap.
1274
+ h: Math.max(
1275
+ region.h || 0.456,
1276
+ estimateTextHeight(captionText, captionW, captionSize, captionSpacing)
1277
+ ),
1278
+ fontFace: captionRole.font || FONT_MEDIUM,
1279
+ fontSize: captionSize,
1280
+ color: (captionRole.color || "000000").replace("#", ""),
1281
+ align: region.align || captionRole.align || "center",
1282
+ charSpacing: captionRole.charSpacing != null ? captionRole.charSpacing : CHAR_SPACING,
1283
+ lineSpacingMultiple: captionRole.lineSpacing || 1.3,
1284
+ });
1285
+ return;
1286
+ }
1287
+
1288
+ // ── Table Caption ─────────────────────────────────────────────────
1289
+ if (content.type === "table-caption") {
1290
+ const num = content.number || 1;
1291
+ const prefix = content.prefix || "표";
1292
+ const captionText = content.caption ? `${prefix} ${num}. ${cleanMd(content.caption)}` : `${prefix} ${num}.`;
1293
+ const captionRole = fontRoles.table_caption || {};
1294
+ const captionW = region.w || 4;
1295
+ const captionSize = captionRole.size || 16;
1296
+ const captionSpacing = captionRole.lineSpacing || 1.3;
1297
+ slide.addText(captionText, {
1298
+ x: region.x || 0,
1299
+ y: region.y || 0,
1300
+ w: captionW,
1301
+ // Same rule as the figure caption: the declared region height is the
1302
+ // floor, not the cap, or a wrapping caption overflows its frame.
1303
+ h: Math.max(
1304
+ region.h || 0.456,
1305
+ estimateTextHeight(captionText, captionW, captionSize, captionSpacing)
1306
+ ),
1307
+ fontFace: captionRole.font || FONT_MEDIUM,
1308
+ fontSize: captionSize,
1309
+ color: (captionRole.color || "000000").replace("#", ""),
1310
+ align: region.align || captionRole.align || "center",
1311
+ charSpacing: captionRole.charSpacing != null ? captionRole.charSpacing : CHAR_SPACING,
1312
+ lineSpacingMultiple: captionRole.lineSpacing || 1.3,
1313
+ });
1314
+ return;
1315
+ }
1316
+
1317
+ // ── Summary Group → individual shapes ──────────────────────────────
1318
+ if (content.type === "summary-group") {
1319
+ addSummaryShapes(slide, content, region);
1320
+ return;
1321
+ }
1322
+ }
1323
+
1324
+ // ── AZURE rich rendering (isolated; only used when template.render_style==="azure") ──
1325
+
1326
+ /** Split a title string into runs, emphasizing **word** in the primary color. */
1327
+ function azureTitleRuns(text, P) {
1328
+ return text.split(/(\*\*[^*]+\*\*)/g).filter(Boolean).map((p) =>
1329
+ p.startsWith("**") && p.endsWith("**")
1330
+ ? { text: p.slice(2, -2), options: { color: hex(P.primary) } }
1331
+ : { text: p, options: {} }
1332
+ );
1333
+ }
1334
+
1335
+ /** Strip leftover markdown markers (### heading, list dashes, **bold**, `code`)
1336
+ * from inline text, returning a clean plain string. */
1337
+ function cleanMd(s) {
1338
+ return String(s || "")
1339
+ .replace(/^#{1,6}\s+/, "")
1340
+ .replace(/^\s*[*-]\s+/, "")
1341
+ .replace(/\*\*(.+?)\*\*/g, "$1")
1342
+ .replace(/`/g, "")
1343
+ .trim();
1344
+ }
1345
+
1346
+ /** Convert inline markdown in `text` to pptxgenjs styled runs: **x** → a bold
1347
+ * run, plain segments → normal runs. Leading heading/list markers and backticks
1348
+ * are stripped. `runOpts` styles every run; bold runs additionally set bold.
1349
+ * Guarantees no raw "**", "###", or backtick reaches the rendered <a:t>. */
1350
+ function mdRuns(text, runOpts = {}) {
1351
+ const clean = String(text || "")
1352
+ .replace(/^#{1,6}\s+/, "")
1353
+ .replace(/^\s*[*-]\s+/, "")
1354
+ .replace(/`/g, "");
1355
+ const parts = clean.split(/(\*\*[^*]+\*\*)/g).filter((p) => p !== "");
1356
+ if (parts.length === 0) return [{ text: "", options: { ...runOpts } }];
1357
+ return parts.map((p) =>
1358
+ p.startsWith("**") && p.endsWith("**")
1359
+ ? { text: p.slice(2, -2), options: { ...runOpts, bold: true } }
1360
+ : { text: p, options: { ...runOpts } }
1361
+ );
1362
+ }
1363
+
1364
+ /** Convert body items into up to 4 cards: {heading, lines[]}.
1365
+ * Handles both authoring styles:
1366
+ * - `### Title` (parsed as `text`) followed by `- **point**` (a `section`) → one card
1367
+ * titled by the heading, the section folded into the body.
1368
+ * - bare `- **Title**` sections → one card each (heading = bold title). */
1369
+ function bodyToCards(items) {
1370
+ const cards = [];
1371
+ let cur = null;
1372
+ for (const item of items || []) {
1373
+ if (item.type === "text" && item.text) {
1374
+ cur = { heading: cleanMd(item.text), lines: [] };
1375
+ cards.push(cur);
1376
+ } else if (item.type === "section" || item.type === "bullet") {
1377
+ const head = cleanMd(item.heading || item.text || "");
1378
+ const kids = (item.children || []).map((c) => cleanMd(c.text)).filter(Boolean);
1379
+ if (cur && cur.lines.length === 0 && cur.heading) {
1380
+ // fold this section under the preceding heading card
1381
+ if (head) cur.lines.push(head);
1382
+ kids.forEach((k) => cur.lines.push(" " + k));
1383
+ } else {
1384
+ cur = { heading: head, lines: kids };
1385
+ cards.push(cur);
1386
+ }
1387
+ } else if (item.type === "numbered" && item.text) {
1388
+ if (cur) cur.lines.push(cleanMd(item.text));
1389
+ else { cur = { heading: "", lines: [cleanMd(item.text)] }; cards.push(cur); }
1390
+ }
1391
+ }
1392
+ return cards.slice(0, 4);
1393
+ }
1394
+
1395
+ /** Draw one rounded card with a numbered chip, heading, and body lines. */
1396
+ function drawAzureCard(slide, x, y, w, h, card, idx, P, emphasized) {
1397
+ const bg = emphasized ? hex(P.primary_deep) : hex(P.tint);
1398
+ const headColor = emphasized ? "FFFFFF" : hex(P.ink);
1399
+ const bodyColor = emphasized ? hex(P.emphasisTextColor) : hex(P.ink_muted);
1400
+ slide.addShape("roundRect", { x, y, w, h, rectRadius: 0.16, fill: { color: bg }, line: { type: "none" } });
1401
+ const pad = 0.30;
1402
+ // numbered chip
1403
+ const chip = 0.52;
1404
+ slide.addShape("roundRect", {
1405
+ x: x + pad, y: y + pad, w: chip, h: chip, rectRadius: 0.12,
1406
+ fill: { color: emphasized ? "FFFFFF" : hex(P.primary) }, line: { type: "none" },
1407
+ });
1408
+ // Number sits in a text box whose rect is identical to the chip rect, with
1409
+ // margin:0 and tight line spacing so the numeral reads dead-centre.
1410
+ slide.addText(String(idx + 1).padStart(2, "0"), {
1411
+ x: x + pad, y: y + pad, w: chip, h: chip, align: "center", valign: "middle",
1412
+ fontFace: P.titleFont, fontSize: 15, bold: true,
1413
+ color: emphasized ? hex(P.primary_deep) : "FFFFFF",
1414
+ margin: 0, lineSpacingMultiple: 1,
1415
+ });
1416
+ if (card.heading) {
1417
+ slide.addText(card.heading, {
1418
+ x: x + pad, y: y + pad + chip + 0.18, w: w - 2 * pad, h: 0.4,
1419
+ fontFace: P.titleFont, fontSize: 15, bold: true, color: headColor, align: "left", valign: "top",
1420
+ charSpacing: -0.3,
1421
+ });
1422
+ }
1423
+ const bodyY = y + pad + chip + (card.heading ? 0.62 : 0.18);
1424
+ const bodyH = h - (bodyY - y) - 0.18;
1425
+ if (card.bodyRuns && card.bodyRuns.length) {
1426
+ slide.addText(card.bodyRuns, {
1427
+ x: x + pad, y: bodyY, w: w - 2 * pad, h: bodyH,
1428
+ color: bodyColor, align: "left", valign: "top", lineSpacingMultiple: card.lineSpacing || 1.28, charSpacing: -0.2,
1429
+ });
1430
+ } else if (card.lines && card.lines.length) {
1431
+ // Shrink body font when there are many lines so it never exceeds the frame.
1432
+ const fs = card.lines.length > 4 ? 10.5 : 11.5;
1433
+ slide.addText(card.lines.join("\n"), {
1434
+ x: x + pad, y: bodyY, w: w - 2 * pad, h: bodyH,
1435
+ fontFace: P.bodyFont, fontSize: fs, color: bodyColor, align: "left", valign: "top",
1436
+ lineSpacingMultiple: 1.3, charSpacing: -0.2,
1437
+ });
1438
+ }
1439
+ }
1440
+
1441
+ /** Render content/main body as a row of cards. */
1442
+ function renderAzureCards(slide, items, region, P) {
1443
+ const cards = bodyToCards(items);
1444
+ if (!cards.length) return;
1445
+ const n = cards.length;
1446
+ const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1;
1447
+ const h = region.h || 3.0;
1448
+ const gap = 0.28;
1449
+ const cardW = (w - (n - 1) * gap) / n;
1450
+ cards.forEach((c, i) => {
1451
+ drawAzureCard(slide, x0 + i * (cardW + gap), y0, cardW, h, c, i, P, n >= 3 && i === n - 1);
1452
+ });
1453
+ }
1454
+
1455
+ /** Flatten body/summary items (sections + children + bullets) into styled runs.
1456
+ * `scale` (≤1) shrinks fonts to fit a fixed card height. */
1457
+ function itemsToRuns(items, P, scale = 1) {
1458
+ const s = (v) => Math.max(8, Math.round(v * scale * 10) / 10);
1459
+ const runs = [];
1460
+ for (const it of items || []) {
1461
+ if (it.type === "section") {
1462
+ runs.push({ text: cleanMd(it.heading || ""), options: { bold: true, fontFace: P.titleFont, fontSize: s(12.5), color: hex(P.ink), breakLine: true } });
1463
+ for (const c of it.children || []) {
1464
+ runs.push({ text: " " + cleanMd(c.text || ""), options: { fontFace: P.bodyFont, fontSize: s(11), color: hex(P.ink_muted), breakLine: true } });
1465
+ }
1466
+ } else if (it.text) {
1467
+ runs.push({ text: (it.type === "numbered" ? " " : "• ") + cleanMd(it.text), options: { fontFace: P.bodyFont, fontSize: s(11.5), color: hex(P.ink_muted), breakLine: true } });
1468
+ }
1469
+ }
1470
+ return runs;
1471
+ }
1472
+
1473
+ /** Estimate the rendered height of a group's items at a given font scale,
1474
+ * using the same conservative metric inventory respects (estimateTextHeight). */
1475
+ function estimateGroupHeight(items, innerW, scale, lineSpacing) {
1476
+ let hsum = 0;
1477
+ for (const it of items || []) {
1478
+ if (it.type === "section") {
1479
+ hsum += estimateTextHeight(it.heading || "", innerW, 12.5 * scale, lineSpacing);
1480
+ for (const c of it.children || []) hsum += estimateTextHeight(" " + (c.text || ""), innerW - 0.1, 11 * scale, lineSpacing);
1481
+ } else if (it.text) {
1482
+ hsum += estimateTextHeight(it.text, innerW, 11.5 * scale, lineSpacing);
1483
+ }
1484
+ }
1485
+ return hsum;
1486
+ }
1487
+
1488
+ /** Render a single summary-group as one card (heading + items, iterative shrink-to-fit). */
1489
+ function renderAzureGroupCard(slide, content, region, P, idx) {
1490
+ const w = region.w || 5.6, h = region.h || 2.4;
1491
+ const innerW = w - 0.60;
1492
+ const innerH = h - 0.30 - 0.52 - (content.heading ? 0.62 : 0.18) - 0.18;
1493
+ let scale = 0.55, ls = 1.12;
1494
+ for (const s of [1, 0.92, 0.85, 0.78, 0.72, 0.66, 0.6, 0.55]) {
1495
+ if (estimateGroupHeight(content.items, innerW, s, s < 1 ? 1.12 : 1.28) <= innerH) { scale = s; ls = s < 1 ? 1.12 : 1.28; break; }
1496
+ }
1497
+ const card = {
1498
+ heading: cleanMd(content.heading || ""),
1499
+ bodyRuns: itemsToRuns(content.items, P, scale),
1500
+ lineSpacing: ls,
1501
+ };
1502
+ drawAzureCard(slide, region.x || 0.6, region.y || 1.6, w, h, card, idx, P, false);
1503
+ }
1504
+
1505
+ /** Render a general data table (palette-styled): primary header, zebra tint rows. */
1506
+ function renderAzureTable(slide, content, region, P) {
1507
+ const headers = content.headers || [];
1508
+ const rows = content.rows || [];
1509
+ if (!headers.length) return;
1510
+ const x = region.x || 0.6, y = region.y || 1.6, w = region.w || 6.0;
1511
+ const numericColumns = headers.map((_, ci) => ci > 0 && rows.length > 0 &&
1512
+ rows.filter((row) => /^[-−–—]?$|^-?[\d,]+(?:\.\d+)?%?$/.test(cleanMd(row[ci] || "").trim())).length >= Math.ceil(rows.length * 0.6));
1513
+ const tableRows = [];
1514
+ tableRows.push(headers.map((h, ci) => ({
1515
+ text: cleanMd(h),
1516
+ options: { bold: true, color: "FFFFFF", fill: { color: hex(P.primary) }, fontFace: P.titleFont, fontSize: 12, align: numericColumns[ci] ? "right" : "left", valign: "middle" },
1517
+ })));
1518
+ rows.forEach((r, ri) => {
1519
+ tableRows.push(r.map((c, ci) => linkedTableCell(c, {
1520
+ color: hex(P.ink), fill: { color: ri % 2 ? "FFFFFF" : hex(P.tint) }, fontFace: P.bodyFont, fontSize: 11.5, align: numericColumns[ci] ? "right" : "left", valign: "middle",
1521
+ })));
1522
+ });
1523
+ slide.addTable(tableRows, {
1524
+ x, y, w, colW: Array(headers.length).fill(w / headers.length),
1525
+ border: { type: "solid", color: hex(P.line), pt: 0.5 }, rowH: 0.4, margin: 0.06, autoPage: false,
1526
+ });
1527
+ }
1528
+
1529
+ /** Render a kpi-table as a row of KPI cards (value + label); first card filled. */
1530
+ function renderAzureKpi(slide, content, region, P) {
1531
+ const cards = kpiCards(content);
1532
+ const values = cards.map((c) => c.value);
1533
+ const labels = cards.map((c) => c.label);
1534
+ const n = Math.min(values.length, 4);
1535
+ if (!n) return;
1536
+ const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1, h = region.h || 1.7;
1537
+ const gap = 0.26;
1538
+ const cardW = (w - (n - 1) * gap) / n;
1539
+ for (let i = 0; i < n; i++) {
1540
+ const fill = i === 0;
1541
+ const x = x0 + i * (cardW + gap);
1542
+ slide.addShape("roundRect", {
1543
+ x, y: y0, w: cardW, h, rectRadius: 0.16,
1544
+ fill: { color: fill ? hex(P.primary) : "FFFFFF" },
1545
+ line: fill ? { type: "none" } : { color: hex(P.line), width: 1 },
1546
+ });
1547
+ slide.addText(cleanMd(values[i]), {
1548
+ x: x + 0.28, y: y0 + 0.20, w: cardW - 0.56, h: 0.8,
1549
+ fontFace: P.titleFont, fontSize: 30, bold: true,
1550
+ color: fill ? "FFFFFF" : hex(P.primary), align: "left", valign: "middle", charSpacing: -1, margin: 0,
1551
+ });
1552
+ slide.addText(cleanMd(labels[i]).toUpperCase(), {
1553
+ x: x + 0.30, y: y0 + h - 0.5, w: cardW - 0.56, h: 0.32,
1554
+ fontFace: P.bodyFont, fontSize: 9.5, bold: false,
1555
+ color: fill ? hex(P.emphasisTextColor) : hex(P.ink_muted), align: "left", valign: "middle", charSpacing: 0.4, margin: 0,
1556
+ });
1557
+ }
1558
+ }
1559
+
1560
+ /** A pipe table reads as KPI in two shapes:
1561
+ * - TALL: 2 cols of value|label pairs, ≤4 pairs, value cell short/metric-like.
1562
+ * - WIDE: 3–4 cols, a single data row of short metric values (labels = header).
1563
+ * Anything else (multi-row, long cells) is a real data table. */
1564
+
1565
+
1566
+ /** Normalize a kpi-table into [{value,label}] cards for both KPI shapes. */
1567
+ /**
1568
+ * Is there room to draw this as KPI badges?
1569
+ *
1570
+ * A badge stacks a value over a label and reads as one glance, which stops
1571
+ * working the moment the card is narrower than its own words: in a column half
1572
+ * the slide wide, four badges leave 1.4in each and a label like "영업이익" wraps
1573
+ * to one character per line. Below the floor the same data is drawn as a table,
1574
+ * which narrows gracefully.
1575
+ */
1576
+ function kpiFits(content, widthIn) {
1577
+ const cards = kpiCards(content).length;
1578
+ return cards > 0 && (widthIn || 0) / cards >= KPI_MIN_CARD_WIDTH_IN;
1579
+ }
1580
+
1581
+ function kpiCards(content) {
1582
+ const headers = content.headers || [];
1583
+ const rows = content.rows || [];
1584
+ if (headers.length === 2) {
1585
+ return [headers, ...rows].slice(0, 4).map((p) => ({ value: p[0], label: p[1] }));
1586
+ }
1587
+ const row = rows[0] || [];
1588
+ return headers.slice(0, 4).map((lab, i) => ({ value: row[i] != null ? row[i] : "", label: lab }));
1589
+ }
1590
+
1591
+ /** Azure region dispatch. Returns true if it fully handled the region.
1592
+ * opts.hasImage = slide also has a populated image region (→ never card the body). */
1593
+ function renderAzureRegion(slide, layout, name, region, fontRoles, P, opts = {}) {
1594
+ const content = region.content;
1595
+ if (content == null) return false;
1596
+ if (layout === "cover" && name === "title" && typeof content === "string" && content.includes("**")) {
1597
+ const role = fontRoles.cover_title || {};
1598
+ slide.addText(azureTitleRuns(content, P), {
1599
+ x: region.x || 0, y: region.y || 0, w: region.w || 8, h: region.h || 1,
1600
+ fontFace: role.font || P.titleFont, fontSize: role.size || 48, bold: role.bold !== false,
1601
+ align: role.align || "left", valign: "middle",
1602
+ charSpacing: role.charSpacing != null ? role.charSpacing : -1, lineSpacingMultiple: role.lineSpacing || 1.08,
1603
+ });
1604
+ return true;
1605
+ }
1606
+ if (typeof content === "object") {
1607
+ if (content.type === "body" && (layout === "content" || layout === "main")) {
1608
+ // Cards ONLY for section-structured bodies (≥2 ### sections) with no image.
1609
+ // Prose bullets, or any body sharing the slide with an image, render as a
1610
+ // normal text column (fall through to addRegion) so nothing is cramped.
1611
+ const sections = (content.items || []).filter((i) => i.type === "section");
1612
+ if (sections.length >= 2 && !opts.hasImage) {
1613
+ renderAzureCards(slide, content.items, region, P);
1614
+ return true;
1615
+ }
1616
+ return false;
1617
+ }
1618
+ if (content.type === "kpi-table") {
1619
+ if (looksLikeKpi(content) && kpiFits(content, region.w)) {
1620
+ renderAzureKpi(slide, content, region, P);
1621
+ }
1622
+ else renderAzureTable(slide, content, region, P);
1623
+ return true;
1624
+ }
1625
+ if (content.type === "summary-group") {
1626
+ const idx = name === "group_bottom" ? 1 : 0;
1627
+ renderAzureGroupCard(slide, content, region, P, idx);
1628
+ return true;
1629
+ }
1630
+ }
1631
+ return false;
1632
+ }
1633
+
1634
+ // ── Placements (author-positioned boxes and shapes) ─────────────────────────
1635
+
1636
+ /** Region name a placed block should be rendered as. */
1637
+ function partRegionName(blockType) {
1638
+ switch (blockType) {
1639
+ case "title": return "title";
1640
+ case "kpi-table": return "table";
1641
+ case "image": return "image";
1642
+ case "figure-caption": return "figure_caption";
1643
+ case "table-caption": return "table_caption";
1644
+ default: return "body";
1645
+ }
1646
+ }
1647
+
1648
+ /** font_role a placed block falls back to when its box names none. */
1649
+ function defaultPartRole(blockType) {
1650
+ switch (blockType) {
1651
+ case "title": return "section_title";
1652
+ case "figure-caption": return "figure_caption";
1653
+ case "table-caption": return "table_caption";
1654
+ case "notes": return "disclaimer";
1655
+ default: return "body_text";
1656
+ }
1657
+ }
1658
+
1659
+ function estimatePartHeight(content, widthIn, role) {
1660
+ const size = role.size || 13;
1661
+ const spacing = role.lineSpacing || 1.3;
1662
+ if (typeof content === "string") {
1663
+ return estimateTextHeight(content, widthIn, size, spacing) + 0.08;
1664
+ }
1665
+ if (content && content.type === "body") {
1666
+ const text = (content.items || []).map((i) => i.heading || i.text || "").join("\n");
1667
+ return estimateTextHeight(text, widthIn, size, spacing) + 0.12;
1668
+ }
1669
+ return 0.5;
1670
+ }
1671
+
1672
+ /**
1673
+ * Turn a shape placement into the decoration element addDecorations reads.
1674
+ *
1675
+ * A rule is drawn as a zero-height rect whose stroke is the visible mark, so it
1676
+ * reads its colour and thickness from `color`/`width` while every other
1677
+ * primitive reads `line`/`line_width`. Authors write one spelling — `line=` and
1678
+ * `lw=` — and the difference is absorbed here.
1679
+ */
1680
+ function shapeToDecoration(placement, sourceDir) {
1681
+ const isRule = placement.shape === "line";
1682
+ const element = {
1683
+ type: placement.shape,
1684
+ x: placement.x, y: placement.y, w: placement.w, h: placement.h,
1685
+ };
1686
+ for (const [key, value] of Object.entries(placement)) {
1687
+ if (["kind", "z", "shape", "x", "y", "w", "h"].includes(key)) continue;
1688
+ if (key === "lw") element[isRule ? "width" : "line_width"] = value;
1689
+ else if (key === "line" && isRule) element.color = value;
1690
+ else if (key === "src") element.assetPath = resolveImagePath(value, sourceDir);
1691
+ else element[key] = value;
1692
+ }
1693
+ return element;
1694
+ }
1695
+
1696
+ /**
1697
+ * Render one placed box by flowing its blocks down the box.
1698
+ *
1699
+ * Every block but the last takes the height its content needs; the last takes
1700
+ * whatever is left, so a heading followed by body fills the box rather than
1701
+ * floating in its top edge. Nothing here consults the auto-nudge rules: a box
1702
+ * lands exactly where the author put it, and the QA gate reports the consequence.
1703
+ */
1704
+ function renderPlacementBox(slide, placement, ctx) {
1705
+ const blocks = placement.blocks || [];
1706
+ if (blocks.length === 0) return;
1707
+
1708
+ // Columns replace the body region, so they render in the template's own idiom.
1709
+ // An explicit box is literal: the author asked for this content, here.
1710
+ const useRich = ctx.rich && placement.z === "content";
1711
+ const bottom = placement.y + (placement.h != null ? placement.h : 1.0);
1712
+ let y = placement.y;
1713
+
1714
+ blocks.forEach((block, i) => {
1715
+ const remaining = Math.max(0.2, bottom - y);
1716
+ const roleName = placement.role || defaultPartRole(block.type);
1717
+ const role = ctx.fontRoles[roleName] || {};
1718
+ const content = extractContent(block);
1719
+ const isLast = i === blocks.length - 1;
1720
+ const h = isLast
1721
+ ? remaining
1722
+ : Math.min(remaining, Math.max(0.2, estimatePartHeight(content, placement.w, role)));
1723
+
1724
+ const region = {
1725
+ x: placement.x, y, w: placement.w, h,
1726
+ content,
1727
+ font_role: roleName,
1728
+ originX: placement.x,
1729
+ ...(placement.align ? { align: placement.align } : {}),
1730
+ ...(placement.valign ? { valign: placement.valign } : {}),
1731
+ };
1732
+
1733
+ const name = partRegionName(block.type);
1734
+ const rendered = useRich &&
1735
+ renderAzureRegion(slide, ctx.layout, name, region, ctx.fontRoles, ctx.P, {});
1736
+ if (!rendered) {
1737
+ addRegion(slide, name, region, ctx.fontRoles, ctx.pageH, y + h, false, ctx.sourceDir, ctx.P);
1738
+ }
1739
+ y += h;
1740
+ });
1741
+ }
1742
+
1743
+ function renderPlacements(slide, placements, band, ctx) {
1744
+ for (const placement of placements) {
1745
+ if (placement.z !== band) continue;
1746
+ if (placement.kind === "shape") {
1747
+ addDecorations(slide, [shapeToDecoration(placement, ctx.sourceDir)], ctx.slideIndex, ctx.fontRoles);
1748
+ } else {
1749
+ renderPlacementBox(slide, placement, ctx);
1750
+ }
1751
+ }
1752
+ }
1753
+
1754
+ // ── Main render function ────────────────────────────────────────────────────
1755
+
1756
+ /**
1757
+ * Render the full resolved spec to a PPTX file.
1758
+ *
1759
+ * @param {object} resolved - Resolved spec from layout-resolver
1760
+ * @param {object} templateObj - Template object from template-registry
1761
+ * @param {string} outputPath - Where to write the .pptx file
1762
+ * @returns {Promise<string>} Resolves with the output path
1763
+ */
1764
+ async function render(resolved, templateObj, outputPath) {
1765
+ const fontRoles = buildFontRoles(templateObj);
1766
+
1767
+ // Brand-neutralize: derive font/spacing/accent from the template, falling back
1768
+ // to the neutral template defaults so enrolled templates are unchanged.
1769
+ const tpl = (templateObj && templateObj.template) || {};
1770
+ const tplFonts = tpl.fonts || {};
1771
+ const tplType = tpl.global_typography || {};
1772
+ FONT_BOLD = tplFonts.title || "Pretendard";
1773
+ FONT_MEDIUM = tplFonts.body || "Pretendard";
1774
+ FONT_LIGHT = tplFonts.light || "Pretendard";
1775
+ CHAR_SPACING = tplType.char_spacing != null ? tplType.char_spacing : -0.7;
1776
+ BULLET_FONT = FONT_BOLD;
1777
+ SEC_HEADER_FONT = FONT_BOLD;
1778
+ TOC_FONT = FONT_BOLD;
1779
+ BULLET_COLOR = (tplType.bullet_color || "00AE41").replace("#", "");
1780
+ SECTION_ACCENT = (tplType.section_accent || "00AE41").replace("#", "");
1781
+
1782
+ // Rich card/KPI/divider rendering — palette-driven, shared by render_style
1783
+ // "azure" (blue) and "green" (green). Tokens come from the template palette.
1784
+ const rich = ["azure", "green"].includes(tpl.render_style);
1785
+ const pal = tpl.palette || {};
1786
+ const P = {
1787
+ titleFont: FONT_BOLD, bodyFont: FONT_MEDIUM,
1788
+ primary: pal.primary || "#1D4ED8", primary_deep: pal.primary_deep || "#0B2E6F",
1789
+ ink: pal.ink || "#0E1B2C", ink_muted: pal.ink_muted || "#51607A",
1790
+ tint: pal.tint || "#EEF4FF", line: pal.line || "#D5DEEC", azure_soft: pal.azure_soft || "#93C5FD",
1791
+ emphasisTextColor: pal.emphasis_text || "#C9D8F5",
1792
+ };
1793
+
1794
+ const dims =
1795
+ (templateObj && templateObj.template && templateObj.template.dimensions) || {};
1796
+ const pageW = dims.width || 10.833;
1797
+ const pageH = dims.height || 7.5;
1798
+
1799
+ const pptx = new PptxGenJS();
1800
+ pptx.defineLayout({ name: "A4_LANDSCAPE", width: pageW, height: pageH });
1801
+ pptx.layout = "A4_LANDSCAPE";
1802
+
1803
+ const title = (resolved.deck && resolved.deck.title) || "Presentation";
1804
+ pptx.title = title;
1805
+ pptx.subject = `LitHermes template:${resolved.deck?.template || "unknown"}`;
1806
+ const sourceDir = resolved.sourceDir;
1807
+ const cramStress = isCramStressDeck(resolved);
1808
+
1809
+ for (const slideSpec of resolved.slides || []) {
1810
+ const slide = pptx.addSlide();
1811
+
1812
+ // Draw order is a band at a time: template decorations, then anything the
1813
+ // author put under the content, then the template's regions, then the
1814
+ // columns that replaced the body, then anything sitting on top.
1815
+ addDecorations(slide, slideSpec.decorations, slideSpec.index, fontRoles);
1816
+
1817
+ const placements = slideSpec.placements || [];
1818
+ const placementCtx = {
1819
+ rich, P, fontRoles, pageH, sourceDir,
1820
+ layout: slideSpec.layout,
1821
+ slideIndex: slideSpec.index,
1822
+ };
1823
+ renderPlacements(slide, placements, "under", placementCtx);
1824
+
1825
+ const regions = adjustedRegionsForImageColumn(slideSpec.regions || {});
1826
+ if (cramStress && ["content", "main"].includes(slideSpec.layout) && regions.body) {
1827
+ renderCramStressSlide(slide, { ...slideSpec, regions }, pageW, pageH, sourceDir);
1828
+ continue;
1829
+ }
1830
+
1831
+ // Calculate bottom boundary for regions without explicit h
1832
+ const bottomBoundary = {};
1833
+ for (const [name, region] of Object.entries(regions)) {
1834
+ let bottom = pageH - 0.5;
1835
+ for (const [otherName, otherRegion] of Object.entries(regions)) {
1836
+ if (otherName === name || otherName === "title") continue;
1837
+ if (name === "body" && ["image", "figure_caption"].includes(otherName)) continue;
1838
+ if (otherRegion.y && otherRegion.y > (region.y || 0) && otherRegion.h) {
1839
+ const candidateBottom = otherRegion.y - 0.15;
1840
+ if (candidateBottom < bottom) bottom = candidateBottom;
1841
+ }
1842
+ }
1843
+ bottomBoundary[name] = bottom;
1844
+ }
1845
+
1846
+ const tocSlide = isTocSlide(regions);
1847
+ const hasImage = Object.entries(regions).some(([n, r]) =>
1848
+ (n === "image" || n === "figure") && r.content &&
1849
+ (r.content.type === "image" || r.content.type === "image-grid"));
1850
+
1851
+ for (const [name, region] of Object.entries(regions)) {
1852
+ if (rich && renderAzureRegion(slide, slideSpec.layout, name, region, fontRoles, P, { hasImage })) continue;
1853
+ addRegion(slide, name, region, fontRoles, pageH, bottomBoundary[name], tocSlide, sourceDir, P);
1854
+ }
1855
+
1856
+ renderPlacements(slide, placements, "content", placementCtx);
1857
+ renderPlacements(slide, placements, "over", placementCtx);
1858
+ }
1859
+
1860
+ await pptx.writeFile({ fileName: outputPath });
1861
+ return outputPath;
1862
+ }
1863
+
1864
+ module.exports = { render };