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