@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.
- package/README.md +299 -13
- package/README_Ko-KR.md +300 -14
- package/assets/lithermes-plugin/README.md +17 -6
- package/assets/lithermes-plugin/__init__.py +96 -21
- package/assets/lithermes-plugin/core.py +15 -6
- package/assets/lithermes-plugin/core_commands.py +140 -28
- package/assets/lithermes-plugin/core_contexts.py +88 -15
- package/assets/lithermes-plugin/core_contract.py +118 -9
- package/assets/lithermes-plugin/core_routing.py +197 -8
- package/assets/lithermes-plugin/deliverable_hedge_guard.py +378 -357
- package/assets/lithermes-plugin/diagnostics.py +76 -8
- package/assets/lithermes-plugin/humanizer_detector.py +348 -0
- package/assets/lithermes-plugin/humanizer_ko_metrics.py +108 -0
- package/assets/lithermes-plugin/humanizer_office.py +18 -0
- package/assets/lithermes-plugin/lit_mark.py +1 -1
- package/assets/lithermes-plugin/motion_receipts.py +80 -0
- package/assets/lithermes-plugin/payload-version.json +2240 -64
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/rules/bundled-rules/lit-humanizer.md +14 -0
- package/assets/lithermes-plugin/skills/autoconference/assets/report_template.md +2 -1
- package/assets/lithermes-plugin/skills/autoconference/modes/ship/SKILL.md +11 -8
- package/assets/lithermes-plugin/skills/autoresearch/modes/ship/type-checklists.md +2 -2
- package/assets/lithermes-plugin/skills/browser-drive/ORIGIN.json +17 -0
- package/assets/lithermes-plugin/skills/browser-drive/SKILL.md +41 -16
- package/assets/lithermes-plugin/skills/browser-drive/scripts/capability_probe.py +122 -10
- package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +45 -13
- package/assets/lithermes-plugin/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +51 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/README.md +44 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/adaptive-layout.md +34 -62
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/brand-and-imagery.md +22 -70
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +18 -4
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/composition.md +27 -78
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/craft-floor.md +74 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/creative-directions.md +25 -69
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/evidence-review.md +18 -97
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/implementation-platforms.md +23 -83
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/inclusive-interface.md +22 -70
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/interaction-motion.md +34 -68
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/motion-guide.md +62 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/operating-lanes.md +14 -78
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/performance-delivery.md +22 -71
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/product-direction.md +17 -70
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/production.md +19 -7
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/redesign-playbook.md +23 -76
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/slop-register.md +69 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/system-foundations.md +22 -67
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-language.md +16 -72
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/visual-reconstruction.md +16 -81
- package/assets/lithermes-plugin/skills/frontend-ui-ux/resources/THIRD-PARTY-NOTICE.txt +15 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-page.js +311 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-rules.json +46 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +231 -0
- package/assets/lithermes-plugin/skills/lit-comprehend/SKILL.md +36 -25
- package/assets/lithermes-plugin/skills/lit-comprehend/references/artifact-template.md +45 -87
- package/assets/lithermes-plugin/skills/lit-comprehend/scripts/verify-explainer.py +3 -27
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/NOTICE +33 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/SKILL.md +308 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-architecture-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bar-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-beeswarm-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bubble-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-bump-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-data-flow-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-datalake-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-db-schema-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dependency-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-deployment-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-integration-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-er-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-fishbone-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-flowchart-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-gantt-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-heatmap-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-drawio-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-it-state-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-journey-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-kanban-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-layers-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-line-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-marimekko-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-medallion-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-nested-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-org-chart-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-polar-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-process-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-pyramid-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-quadrant-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-queue-animated-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-radar-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-ridgeline-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sankey-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-scatter-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-slopegraph-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-state-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-story-map-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-streamgraph-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-swimlane-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-timeline-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-tree-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-treemap-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-uml-class-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-venn-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-wardley-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-dark.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-full.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/examples/type-waterfall-light.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/OFL.txt +94 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/PretendardVariable.woff2 +0 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/fonts/provenance.json +22 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/icons.html +231 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/index.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/devicon-MIT.txt +20 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/diagram-design-MIT.txt +21 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/log-z-MIT.txt +21 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/simple-icons-CC0.md +30 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/assets/licenses/tabler-MIT.txt +21 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/01-service-architecture/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/01-service-architecture/brief.md +33 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/02-release-decision/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/02-release-decision/brief.md +18 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/03-data-retention/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/03-data-retention/brief.md +19 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/04-oauth-sequence/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/04-oauth-sequence/brief.md +17 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/05-review-lanes/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/05-review-lanes/brief.md +17 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/06-rollout-gates/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/06-rollout-gates/brief.md +17 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/07-deployment-boundary/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/07-deployment-boundary/brief.md +19 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/08-policy-traces/after.html +1 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/examples/08-policy-traces/brief.md +20 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/accessibility.md +37 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/export.md +29 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-drawio.md +21 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-excalidraw.md +14 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-mermaid.md +18 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/import-schema.md +28 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/korean-typography.md +34 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/motion.md +29 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/office-pptx-docx.md +40 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/output-spec.md +48 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-annotation.md +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-icons.md +21 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-sketchy.md +11 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/primitive-terminal.md +11 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/profiles.md +24 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/semantic-patterns.md +27 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/style-guide.md +67 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-architecture.md +51 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bar.md +51 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-beeswarm.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bubble.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-bump.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-catalog.json +1433 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-data-flow.md +51 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-datalake.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-db-schema.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dependency.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-deployment.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dp-integration.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dp-security-matrix.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-er.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-fishbone.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-flowchart.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-gantt.md +49 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-heatmap.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-high-level-vertical.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-high-level.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-drawio.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-excalidraw.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-import-mermaid.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-it-state.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-journey.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-kanban.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-layers.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-line.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-loop-terminal.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-loop.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-marimekko.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-medallion.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-nested.md +59 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-org-chart.md +58 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-paved-road-animated.md +72 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-polar.md +67 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-policy-trace-animated.md +69 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-process.md +58 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-pyramid.md +59 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-quadrant-consultant.md +60 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-quadrant.md +57 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-queue-animated.md +63 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-radar.md +57 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-ridgeline.md +63 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sankey.md +68 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-scatter.md +60 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sequence-oauth.md +66 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-sequence.md +63 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-slopegraph.md +65 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-state-lifecycle.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-state.md +59 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-story-map.md +52 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-streamgraph.md +53 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-swimlane.md +52 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-timeline.md +52 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree-block-decomposition.md +57 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-treemap.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-uml-class.md +52 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-venn.md +51 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-wardley.md +52 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-waterfall.md +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/verifier-guide.md +31 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/block-registry.mjs +43 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/browser-version.mjs +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/check-visible-text.py +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_brief.py +192 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_catalog.py +135 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_checks.py +123 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_cli.py +94 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_geometry.py +130 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_metrics.py +166 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality.py +293 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality_geometry.py +200 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_records.py +205 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_sequence.py +55 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/doctor.mjs +24 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/drawio_extract.py +209 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/drawio_source.py +188 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/excalidraw_extract.py +233 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/excalidraw_support.py +64 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/export.mjs +125 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/mermaid_extract.py +240 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/mermaid_source.py +39 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/office-safety.mjs +54 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-all.py +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-brief.py +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-diagram.py +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-motion.py +12 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/verify-type.py +12 -0
- package/assets/lithermes-plugin/skills/lit-docx/NOTICE +1 -0
- package/assets/lithermes-plugin/skills/lit-docx/SKILL.md +97 -0
- package/assets/lithermes-plugin/skills/lit-docx/references/frontmatter_schema.md +42 -0
- package/assets/lithermes-plugin/skills/lit-docx/references/journal_style_spec.md +17 -0
- package/assets/lithermes-plugin/skills/lit-docx/references/markdown_quality_checklist.md +37 -0
- package/assets/lithermes-plugin/skills/lit-docx/references/slop_phrase_list.yaml +58 -0
- package/assets/lithermes-plugin/skills/lit-docx/references/slop_rules.md +35 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/clean_markdown.py +95 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_docx.py +1041 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_pdf.py +281 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_pdf.py +130 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/edit_docx.py +297 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/embed_images.py +78 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/generate_docx_templates.py +491 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/slop_lint.py +536 -0
- package/assets/lithermes-plugin/skills/lit-docx/scripts/visual_audit.py +41 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/docx/acs.docx +0 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/docx/elsevier.docx +0 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/docx/ieee.docx +0 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/docx/korean-generic.docx +0 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/docx/nature.docx +0 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/README.md +5 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/template.tex +37 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/README.md +5 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/template.tex +38 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/README.md +5 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/template.tex +37 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/README.md +5 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/template.tex +37 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/README.md +5 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/template.tex +37 -0
- package/assets/lithermes-plugin/skills/lit-docx/templates/registry.yaml +1366 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/NOTICE +170 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/SKILL.md +125 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/assets/report-skeleton.md +26 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/assets/slide-text-skeleton.md +30 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/accessibility-note/after.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/accessibility-note/before.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/api-reference/after.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/api-reference/before.md +15 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/changelog/after.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/changelog/before.md +12 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/code-comments/after.md +10 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/code-comments/before.md +19 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/commit/after.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/commit/before.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/data-release/after.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/data-release/before.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/ko-metrics-golden.json +49 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/pos-real.txt +26 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/detector-validation/rule-cases.json +306 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/english-essay/after.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/english-essay/before.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/incident-update/after.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/incident-update/before.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-essay/after.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-essay/before.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-report/after.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-report/before.md +13 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-slide/after.md +19 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/ko-slide/before.md +21 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/meeting-brief/after.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/meeting-brief/before.md +11 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/policy-faq/after.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/policy-faq/before.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/pr/after.md +15 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/pr/before.md +17 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/project-update/after.md +7 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/project-update/before.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/readme/after.md +12 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/readme/before.md +14 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/report/after.md +13 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/report/before.md +11 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/research-abstract/after.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/research-abstract/before.md +5 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/slides/after.md +19 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/slides/before.md +19 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/troubleshooting/after.md +11 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/examples/troubleshooting/before.md +11 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/README.md +16 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/always-on-rule.md +9 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/code-patterns.md +468 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/deliverable-channels.md +111 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-checklist.md +43 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-content.md +243 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns-structure.md +375 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/en-patterns.md +57 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-metrics.md +33 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns-a-d.md +659 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns-e-j.md +387 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/ko-patterns.md +31 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/rewrite-playbook.md +54 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/references/taxonomy.md +153 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/rules.json +884 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/scripts/detect.py +17 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/scripts/extract_office_text.py +114 -0
- package/assets/lithermes-plugin/skills/lit-humanizer/scripts/ko_metrics.py +35 -0
- package/assets/lithermes-plugin/skills/lit-pptx/NOTICE +1 -0
- package/assets/lithermes-plugin/skills/lit-pptx/SKILL.md +109 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/analyst.md +49 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/antithesis.md +65 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/architect.md +44 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/critic.md +42 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/synthesizer.md +47 -0
- package/assets/lithermes-plugin/skills/lit-pptx/agents/thesis.md +33 -0
- package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-lg.png +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-md.png +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/assets/azure/circle-sm.png +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/bin/office.mjs +77 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/PRETENDARD-OFL.txt +94 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Black.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Bold.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-ExtraBold.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-ExtraLight.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Light.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Medium.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Regular.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-SemiBold.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/A2Z-Thin.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/OFL.txt +94 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/a2z-font/SOURCE.md +29 -0
- package/assets/lithermes-plugin/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Bold.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/pretendard-font/public/static/PretendardGOV-Regular.otf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/anti-slop-checklist.md +71 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/authoring-guide.md +139 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/consensus-protocol.md +125 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/data-display-cookbook.md +45 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/design-system-azure.md +58 -0
- package/assets/lithermes-plugin/skills/lit-pptx/runtime/package-lock.json +750 -0
- package/assets/lithermes-plugin/skills/lit-pptx/runtime/package.json +15 -0
- package/assets/lithermes-plugin/skills/lit-pptx/runtime/requirements.in +7 -0
- package/assets/lithermes-plugin/skills/lit-pptx/runtime/requirements.lock +391 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/compile-deck.js +447 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/craft_extras.py +215 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/embed_fonts.py +199 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/integrity.py +60 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/inventory.py +64 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/learn_template.py +433 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/derive-palette.js +176 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/directive-parser.js +244 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/font-map.js +79 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/gen-bg.js +85 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/gen-gradient-assets.js +36 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +736 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/markdown-loader.js +136 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-adapter-html.js +357 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +1864 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/slide-ast.js +539 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/spec-validator.js +94 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/template-registry.js +355 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/qa_deck.py +791 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/validate_pptx.py +206 -0
- package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/markdown-slide-spec-v1.md +251 -0
- package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/markdown-slide-spec-v2.md +221 -0
- package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/slide-ast-v1.schema.json +229 -0
- package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/slide-ast-v2.schema.json +144 -0
- package/assets/lithermes-plugin/skills/lit-pptx/slide-formats/template-enrollment-contract.md +228 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/capabilities.yaml +146 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +384 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +37 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +154 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +405 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +37 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/capabilities.yaml +179 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/layout-mapping.yaml +299 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/template.yaml +45 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +181 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/layout-mapping.yaml +299 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +44 -0
- package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +71 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/audio.py +45 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +690 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +299 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/NOTICE +23 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +55 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/browser.mjs +169 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/completion.mjs +52 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/constants.mjs +162 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/director.mjs +33 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/encode.mjs +191 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/flash.mjs +163 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/frame.mjs +428 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/gate.mjs +483 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/look.mjs +223 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/page.js +622 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/presets.mjs +170 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/scenes.mjs +242 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +382 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-gate.mjs +97 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-init.js +186 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-kit.js +389 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-qa.mjs +358 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-worker.mjs +33 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +691 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stills.mjs +71 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stroke.mjs +231 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/timeline.mjs +177 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +244 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/type.mjs +183 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/util.mjs +152 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/craft-loop.md +175 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/engine-contract.md +166 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/runtime.md +122 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +261 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/style-bibles.md +122 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +164 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/type-craft.md +92 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/fonts.json +154 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/licenses/CREDITS +20 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/licenses/EMS-OFL.txt +91 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/package-lock.json +88 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/package.json +12 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.in +1 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.lock +727 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/word-timing.json +9 -0
- package/assets/lithermes-plugin/skills/litresearch/SKILL.md +19 -18
- package/assets/lithermes-plugin/skills/litwork/SKILL.md +28 -0
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
- package/package.json +1 -1
- package/readme-assets/ab-simple/s11-ui-baseline-desktop.webp +0 -0
- package/readme-assets/ab-simple/s11-ui-lithermes-desktop.webp +0 -0
- package/readme-assets/ab-simple/s3-ui-baseline-desktop.webp +0 -0
- package/readme-assets/ab-simple/s3-ui-lithermes-desktop.webp +0 -0
- package/readme-assets/ab-simple/s4-ui-baseline-desktop.webp +0 -0
- package/readme-assets/ab-simple/s4-ui-lithermes-desktop.webp +0 -0
- package/readme-assets/ab-simple/s5-office-baseline-slides.webp +0 -0
- package/readme-assets/ab-simple/s5-office-lithermes-slides.webp +0 -0
- package/readme-assets/ab-simple/s7-lithermes-diagram.webp +0 -0
- package/readme-assets/ab-simple/s8-office-baseline-slides.webp +0 -0
- package/readme-assets/ab-simple/s8-office-lithermes-slides.webp +0 -0
- package/readme-assets/ab-simple/s9-office-lithermes-pages.webp +0 -0
- package/readme-assets/badge-version.svg +1 -1
- package/readme-assets/cover-motion-still.webp +0 -0
- package/readme-assets/cover-motion.webp +0 -0
- package/readme-assets/skills/autoconference.webp +0 -0
- package/readme-assets/skills/autoresearch.webp +0 -0
- package/readme-assets/skills/browser-drive.webp +0 -0
- package/readme-assets/skills/comment-checker.webp +0 -0
- package/readme-assets/skills/debugging.webp +0 -0
- package/readme-assets/skills/deep-interview.webp +0 -0
- package/readme-assets/skills/frontend-ui-ux.webp +0 -0
- package/readme-assets/skills/lit-burnoff-file.webp +0 -0
- package/readme-assets/skills/lit-burnoff.webp +0 -0
- package/readme-assets/skills/lit-code.webp +0 -0
- package/readme-assets/skills/lit-commit.webp +0 -0
- package/readme-assets/skills/lit-comprehend.webp +0 -0
- package/readme-assets/skills/lit-crucible.webp +0 -0
- package/readme-assets/skills/lit-diagram-drawer.webp +0 -0
- package/readme-assets/skills/lit-docx.webp +0 -0
- package/readme-assets/skills/lit-handoff.webp +0 -0
- package/readme-assets/skills/lit-humanizer.webp +0 -0
- package/readme-assets/skills/lit-init.webp +0 -0
- package/readme-assets/skills/lit-plan.webp +0 -0
- package/readme-assets/skills/lit-pptx.webp +0 -0
- package/readme-assets/skills/lit-recap.webp +0 -0
- package/readme-assets/skills/lit-scientific-visualization.webp +0 -0
- package/readme-assets/skills/lit-typographic-motion.webp +0 -0
- package/readme-assets/skills/litgoal.webp +0 -0
- package/readme-assets/skills/litresearch.webp +0 -0
- package/readme-assets/skills/litwork.webp +0 -0
- package/readme-assets/skills/lsp-setup.webp +0 -0
- package/readme-assets/skills/lsp.webp +0 -0
- package/readme-assets/skills/readme-studio.webp +0 -0
- package/readme-assets/skills/refactor.webp +0 -0
- package/readme-assets/skills/review-work.webp +0 -0
- package/readme-assets/skills/rules.webp +0 -0
- package/readme-assets/skills/start-work.webp +0 -0
- package/readme-assets/skills/structural-search.webp +0 -0
- package/readme-assets/skills/visual-qa.webp +0 -0
- package/readme-assets/skills/wikify.webp +0 -0
- package/src/cli.js +40 -1
- package/src/lib/check.js +21 -0
- package/src/lib/install.js +262 -7
- package/src/lib/skillPayload.js +3 -3
- package/assets/lithermes-plugin/skills/lit-korean/SKILL.md +0 -237
- package/assets/lithermes-plugin/skills/lit-korean/references/quick-rules.md +0 -24
- package/assets/lithermes-plugin/skills/lit-korean/references/safety-checklist.md +0 -23
- 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 };
|