@litfamily/litopencode 1.0.16 → 1.0.18
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 +13 -0
- package/README-Ko-KR.md +11 -11
- package/README.md +11 -11
- package/dist/activation-routing.js +1 -1
- package/dist/deliverable-hedge-guard.js +3 -1
- package/dist/features.js +2 -2
- package/dist/hooks.js +2 -1
- package/dist/skills.js +6 -4
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +1 -1
- package/package.json +1 -1
- package/skills/frontend-ui-ux/data/THIRD-PARTY-NOTICE.txt +1 -3
- package/skills/lit-docx/SKILL.md +80 -19
- package/skills/lit-docx/references/components.md +253 -0
- package/skills/lit-docx/references/direction-step.md +234 -0
- package/skills/lit-docx/references/examples/01-report-ko.md +160 -0
- package/skills/lit-docx/references/examples/02-brief-ko.md +58 -0
- package/skills/lit-docx/references/examples/03-manual-ko.md +107 -0
- package/skills/lit-docx/references/examples/04-proposal-ko.md +93 -0
- package/skills/lit-docx/references/examples/05-memo-ko.md +42 -0
- package/skills/lit-docx/references/examples/06-journal-en.md +84 -0
- package/skills/lit-docx/references/examples/07-report-en.md +137 -0
- package/skills/lit-docx/references/examples/08-journal-ko.md +93 -0
- package/skills/lit-docx/references/examples/09-brief-en.md +64 -0
- package/skills/lit-docx/references/page-composition.md +306 -0
- package/skills/lit-docx/references/tonalities/brief.md +104 -0
- package/skills/lit-docx/references/tonalities/journal.md +101 -0
- package/skills/lit-docx/references/tonalities/manual.md +111 -0
- package/skills/lit-docx/references/tonalities/memo.md +105 -0
- package/skills/lit-docx/references/tonalities/proposal.md +101 -0
- package/skills/lit-docx/references/tonalities/report.md +126 -0
- package/skills/lit-docx/run.py +2 -1
- package/skills/lit-docx/scripts/convert_md_to_docx.py +384 -67
- package/skills/lit-docx/scripts/craft_docx.py +204 -1
- package/skills/lit-docx/scripts/docx_design.py +2445 -0
- package/skills/lit-docx/scripts/docx_layout.py +959 -0
- package/skills/lit-docx/scripts/slop_lint.py +23 -6
- package/skills/lit-docx/templates/tonalities/brief.yaml +28 -0
- package/skills/lit-docx/templates/tonalities/journal.yaml +25 -0
- package/skills/lit-docx/templates/tonalities/manual.yaml +26 -0
- package/skills/lit-docx/templates/tonalities/memo.yaml +26 -0
- package/skills/lit-docx/templates/tonalities/proposal.yaml +26 -0
- package/skills/lit-docx/templates/tonalities/report.yaml +26 -0
- package/skills/lit-pptx/NOTICE.md +1 -1
- package/skills/lit-pptx/SKILL.md +108 -19
- package/skills/lit-pptx/pretendard-font/public/static/Pretendard-Bold.otf +0 -0
- package/skills/lit-pptx/pretendard-font/public/static/Pretendard-Regular.otf +0 -0
- package/skills/lit-pptx/references/density-and-fill.md +283 -0
- package/skills/lit-pptx/references/direction-step.md +249 -0
- package/skills/lit-pptx/references/examples/01-ops-review-ledger-ko.md +173 -0
- package/skills/lit-pptx/references/examples/02-launch-pitch-signal-en.md +153 -0
- package/skills/lit-pptx/references/examples/03-field-survey-atlas-ko.md +120 -0
- package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +132 -0
- package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +162 -0
- package/skills/lit-pptx/references/examples/06-policy-briefing-gazette-ko.md +176 -0
- package/skills/lit-pptx/references/examples/07-annual-story-studio-en.md +131 -0
- package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +182 -0
- package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +147 -0
- package/skills/lit-pptx/references/examples/10-investor-pitch-signal-ko.md +136 -0
- package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +177 -0
- package/skills/lit-pptx/references/examples/12-exhibition-proposal-studio-ko.md +149 -0
- package/skills/lit-pptx/references/layout-families.md +470 -0
- package/skills/lit-pptx/references/title-treatments.md +271 -0
- package/skills/lit-pptx/references/tonalities/atlas.md +131 -0
- package/skills/lit-pptx/references/tonalities/chalk.md +132 -0
- package/skills/lit-pptx/references/tonalities/gazette.md +137 -0
- package/skills/lit-pptx/references/tonalities/ledger.md +150 -0
- package/skills/lit-pptx/references/tonalities/night.md +148 -0
- package/skills/lit-pptx/references/tonalities/paper.md +135 -0
- package/skills/lit-pptx/references/tonalities/signal.md +147 -0
- package/skills/lit-pptx/references/tonalities/studio.md +149 -0
- package/skills/lit-pptx/scripts/compile-deck.js +68 -21
- package/skills/lit-pptx/scripts/craft-thresholds.json +45 -1
- package/skills/lit-pptx/scripts/craft_extras.py +37 -8
- package/skills/lit-pptx/scripts/deck_output.py +654 -0
- package/skills/lit-pptx/scripts/embed_fonts.py +2 -2
- package/skills/lit-pptx/scripts/layout_inventory.py +35 -2
- package/skills/lit-pptx/scripts/lib/directive-parser.js +2 -2
- package/skills/lit-pptx/scripts/lib/font-map.js +4 -4
- package/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
- package/skills/lit-pptx/scripts/lib/layout-resolver.js +403 -21
- package/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
- package/skills/lit-pptx/scripts/lib/render-adapter-html.js +3 -9
- package/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +500 -113
- package/skills/lit-pptx/scripts/lib/render-pack.js +3558 -0
- package/skills/lit-pptx/scripts/lib/slide-ast.js +55 -24
- package/skills/lit-pptx/scripts/lib/spec-validator.js +2 -2
- package/skills/lit-pptx/scripts/lib/template-registry.js +335 -11
- package/skills/lit-pptx/scripts/qa_deck.py +48 -13
- package/skills/lit-pptx/scripts/validate_pptx.py +25 -7
- package/skills/lit-pptx/specs/template-enrollment-contract.md +4 -4
- package/skills/lit-pptx/templates/boilerplate_professional.css +13 -13
- package/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
- package/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +5 -2
- package/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +20 -20
- package/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -11
- package/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +10 -7
- package/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +17 -17
- package/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +8 -8
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +104 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +100 -0
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +101 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +100 -0
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +105 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +101 -0
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +99 -0
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +2 -2
- package/skills/lit-typographic-motion/engine/treatment.mjs +1 -1
- package/skills/lit-typographic-motion/fonts/pins.json +2 -2
- package/skills/lit-typographic-motion/gate.mjs +1 -1
- package/skills/lit-typographic-motion/references/stage.md +1 -1
- package/skills/lit-typographic-motion/references/style-bibles.md +2 -2
- package/skills/lit-typographic-motion/references/treatment.md +1 -1
- package/skills/lit-typographic-motion/references/type-craft.md +3 -3
- package/skills/managed-skill-manifest.json +288 -44
- 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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Studio
|
|
2
|
+
|
|
3
|
+
Studio is a magazine spread. The 12-column grid is visible as fine column hairlines, titles live in
|
|
4
|
+
a side rail or open a page at large size, splits are asymmetric, real numbers are set prominently on
|
|
5
|
+
tinted panels, and a dark ochre accent marks the one thing to look at. Pack file:
|
|
6
|
+
`templates/tonalities/studio/pack.yaml`.
|
|
7
|
+
|
|
8
|
+
## Use it for
|
|
9
|
+
|
|
10
|
+
- Annual reviews and impact reports told as a story with photographs and a few charts.
|
|
11
|
+
- Exhibition, programme and place proposals (전시 기획, 공간 제안).
|
|
12
|
+
- Public lectures and editorial narratives where images and figures alternate.
|
|
13
|
+
|
|
14
|
+
## Wrong for
|
|
15
|
+
|
|
16
|
+
- Dense data reviews: editorial white space starves tables. Studio lists no table family.
|
|
17
|
+
- Korean reading briefs: gazette.
|
|
18
|
+
- Decks with no photographs and few numbers: the editorial grid then frames nothing.
|
|
19
|
+
|
|
20
|
+
## Palette roles
|
|
21
|
+
|
|
22
|
+
| Role | Colour | Used for |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| ground / surface | white / warm pale grey `#F2F2F0` | background, rail, number tint |
|
|
25
|
+
| ink / ink-muted | near-black `#141414` / grey `#525252` | text |
|
|
26
|
+
| line | warm grey `#D2D2CE` | column hairlines, rules |
|
|
27
|
+
| accent | dark ochre `#8A5A00` | the one thing to look at |
|
|
28
|
+
| accent-deep / accent-tint | deep brown `#4D3200` / pale sand `#F5ECD9` | deep text / number panel tint |
|
|
29
|
+
| field / on-field | near-black / white | closing field, caption panel |
|
|
30
|
+
| positive / negative | green `#1E7A3F` / red `#B3261E` | signed deltas |
|
|
31
|
+
| series | ochre, ink, grey, slate blue | charts |
|
|
32
|
+
|
|
33
|
+
## Type
|
|
34
|
+
|
|
35
|
+
Pretendard throughout. Compact ramp at density 10; display tracks tight (−2).
|
|
36
|
+
|
|
37
|
+
## Dials
|
|
38
|
+
|
|
39
|
+
Density 10, variance 8: up to five treatments, all five of the pack's. Studio decks show the most
|
|
40
|
+
title variety of the eight.
|
|
41
|
+
|
|
42
|
+
## Title treatments by role
|
|
43
|
+
|
|
44
|
+
| Role | Default treatment |
|
|
45
|
+
|---|---|
|
|
46
|
+
| content | side-rail |
|
|
47
|
+
| data | top-plain-large |
|
|
48
|
+
| data-takeaway | side-rail |
|
|
49
|
+
| sequence | side-rail |
|
|
50
|
+
| image | side-rail |
|
|
51
|
+
| statement | statement |
|
|
52
|
+
| reference | side-rail |
|
|
53
|
+
| definition | side-rail |
|
|
54
|
+
|
|
55
|
+
An image slide takes the side rail: the picture across columns 5-12 and its facts and source in the
|
|
56
|
+
rail under the title, an asymmetric split that differs from the bottom title Atlas or Paper would
|
|
57
|
+
give the same slide.
|
|
58
|
+
|
|
59
|
+
Treatments: side-rail, statement, top-plain-large, bottom-anchor, overlay. The statement is drawn
|
|
60
|
+
`rules`: the sentence on the open page between a hairline above it and one under its support line
|
|
61
|
+
(the earlier offset rail beside it stood empty). Studio has no
|
|
62
|
+
kicker-numeral; sequences take the side rail.
|
|
63
|
+
|
|
64
|
+
## Families and variants
|
|
65
|
+
|
|
66
|
+
Families: asymmetric-feature, big-number, image-split, image-full, photo-grid, text-two-column,
|
|
67
|
+
chart-insight, kpi-row, timeline, comparison, statement.
|
|
68
|
+
|
|
69
|
+
Covers: cover-split-image (takes the first image in the deck), cover-rail, cover-numeral. Sections:
|
|
70
|
+
section-numeral, section-rail. Closings: closing-contact-split, closing-summary-list.
|
|
71
|
+
|
|
72
|
+
Display devices: no cover device of its own (covers draw their variant plainly), statement `rules`,
|
|
73
|
+
big number on a tint, closing step in a `band`.
|
|
74
|
+
|
|
75
|
+
## Tables and charts
|
|
76
|
+
|
|
77
|
+
- Tables `open`: no header fill, one rule under the header, no banding, numbers right-aligned,
|
|
78
|
+
totals bold. Studio lists no table family, so tables appear only as KPI rows and closing asks.
|
|
79
|
+
- Charts: no gridlines, direct labels, the accent on one series. Column hairlines run
|
|
80
|
+
in the gutter beside a chart's takeaways.
|
|
81
|
+
- Images: no frame.
|
|
82
|
+
|
|
83
|
+
## Decoration vocabulary
|
|
84
|
+
|
|
85
|
+
column-hairlines, numeral, rail-fill, colour-field, caption-band.
|
|
86
|
+
|
|
87
|
+
## Fill behaviour
|
|
88
|
+
|
|
89
|
+
`fill-order`: change-family, step-up (off at density 10), anchor-visual, distribute.
|
|
90
|
+
|
|
91
|
+
## Writing for Studio
|
|
92
|
+
|
|
93
|
+
- Alternate: an image slide, a numbers slide, a text slide. Studio's variance rewards a deck whose
|
|
94
|
+
slides genuinely differ in job.
|
|
95
|
+
- `text-two-column` needs four to six groups with two or three sub-points each to fill both columns
|
|
96
|
+
at density 10.
|
|
97
|
+
- Large numerals must be real figures with a basis row; the engine caps them at title size anyway.
|
|
98
|
+
- Timeline events get a sixth of the width each; keep them short (`Homework help in all 25 rooms`
|
|
99
|
+
rather than a full clause).
|
|
100
|
+
- When OF-111 reports a repeated layout, the usual fix is a `title:` key on one repeated slide
|
|
101
|
+
(`top-plain-large` on a chart slide, `bottom-anchor` on a timeline, `side-rail` on a KPI row).
|
|
102
|
+
|
|
103
|
+
## Avoid
|
|
104
|
+
|
|
105
|
+
- Large numerals that are not real figures.
|
|
106
|
+
- Column hairlines through text.
|
|
107
|
+
- Two side-rail slides with the same split in a row.
|
|
108
|
+
- Centred body text.
|
|
109
|
+
|
|
110
|
+
## A sample fragment
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
---
|
|
114
|
+
tonality: studio
|
|
115
|
+
title: Harbour promenade proposal 2027
|
|
116
|
+
date: 2026-10-06
|
|
117
|
+
notice: Sample data and stand-in photos
|
|
118
|
+
---
|
|
119
|
+
layout: asymmetric-feature
|
|
120
|
+
|
|
121
|
+
## The promenade today
|
|
122
|
+
|
|
123
|
+

|
|
124
|
+
|
|
125
|
+
- **Length** 2.4 km from the ferry terminal to the old customs house
|
|
126
|
+
- **Visitors** about 9,000 on a summer weekend day, 1,200 on a winter weekday
|
|
127
|
+
- **Seating** 46 benches, none shaded, none with backs
|
|
128
|
+
- **Lighting** 31% of the route under 10 lx after dark
|
|
129
|
+
- Source: Example City visitor counts and lighting survey, 2026
|
|
130
|
+
---
|
|
131
|
+
layout: kpi-row
|
|
132
|
+
title: side-rail
|
|
133
|
+
|
|
134
|
+
## What the first phase delivers
|
|
135
|
+
|
|
136
|
+
| Shaded seats | Lit route | Trees | Cost |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| 180 | 100% | 64 | 3.8M EUR |
|
|
139
|
+
| was 0 | above 10 lx | native species | phase 1 of 2 |
|
|
140
|
+
|
|
141
|
+
- Phase 1 covers the 1.1 km between the terminal and the market hall
|
|
142
|
+
- Source: Example City outline design, September 2026
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
- `examples/07-annual-story-studio-en.md`: English reading-rooms annual review, 9 slides.
|
|
148
|
+
- `examples/12-exhibition-proposal-studio-ko.md`: Korean architecture photo exhibition proposal,
|
|
149
|
+
9 slides.
|
|
@@ -86,8 +86,8 @@ function compileDeck(source) {
|
|
|
86
86
|
// registering the file and reading its internal name, which is the failure this
|
|
87
87
|
// export exists to prevent.
|
|
88
88
|
const BUNDLED_FONTS = [
|
|
89
|
-
{ match: /
|
|
90
|
-
files: ["
|
|
89
|
+
{ match: /Pretendard/i, dir: "pretendard-font/public/static",
|
|
90
|
+
files: ["Pretendard-Regular.otf", "Pretendard-Bold.otf"] },
|
|
91
91
|
{ match: /에이투지체/, dir: "fonts/a2z-font",
|
|
92
92
|
files: ["A2Z-Regular.otf", "A2Z-Bold.otf"] },
|
|
93
93
|
];
|
|
@@ -143,11 +143,23 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
|
|
|
143
143
|
throw new Error("layout-resolver module not available. Cannot resolve layouts.");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// A tonality (flag or frontmatter) is a design direction read from a pack; without one the named
|
|
147
|
+
// template loads as a legacy pack and keeps its own look.
|
|
148
|
+
const meta = (ast.deck && ast.deck.metadata) || {};
|
|
149
|
+
const tonality = options.tonality || meta.tonality || null;
|
|
150
|
+
const templateObj = tonality
|
|
151
|
+
? templateRegistry.loadTonality(tonality, { density: meta.density, variance: meta.variance, canvas: meta.canvas, faces: options.faces || meta.faces })
|
|
152
|
+
: templateRegistry.loadTemplate(templateName);
|
|
153
|
+
if (tonality) {
|
|
154
|
+
const d = templateObj.pack.dials;
|
|
155
|
+
console.log(`Tonality ${templateObj.pack.id} · density ${d.density} · variance ${d.variance} · ${d.canvas} ${templateObj.pack.grid.name} grid` +
|
|
156
|
+
(templateName && templateName !== tonality ? ` (template ${templateName} not used)` : ""));
|
|
157
|
+
}
|
|
158
|
+
const can = (capability) => templateRegistry.hasCapability(templateObj, capability);
|
|
147
159
|
|
|
148
|
-
// Font override (
|
|
149
|
-
// of which template was chosen. Applied before accent (independent of colour).
|
|
150
|
-
if (options.font &&
|
|
160
|
+
// Font override (templates with the font-swap capability): swap the whole font system
|
|
161
|
+
// regardless of which template was chosen. Applied before accent (independent of colour).
|
|
162
|
+
if (options.font && can("font-swap")) {
|
|
151
163
|
const { applyFont, KEYS } = require("./lib/font-map");
|
|
152
164
|
if (KEYS.includes(options.font)) {
|
|
153
165
|
applyFont(templateObj, options.font);
|
|
@@ -158,10 +170,10 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
|
|
|
158
170
|
const resolved = layoutResolver.resolve(ast, templateObj);
|
|
159
171
|
if (options.sourceDir) resolved.sourceDir = options.sourceDir;
|
|
160
172
|
|
|
161
|
-
// Custom accent recolor (
|
|
173
|
+
// Custom accent recolor (templates with the recolor capability): derive a WCAG-safe
|
|
162
174
|
// palette from the accent hex, override template tokens + hardcoded decoration
|
|
163
175
|
// colors, and regenerate the gradient circle PNGs in the new hue.
|
|
164
|
-
if (options.accent &&
|
|
176
|
+
if (options.accent && can("recolor")) {
|
|
165
177
|
const { derivePalette, defaultToDerivedMap } = require("./lib/derive-palette");
|
|
166
178
|
const { genGradientAssets } = require("./lib/gen-gradient-assets");
|
|
167
179
|
const os = require("os");
|
|
@@ -199,11 +211,11 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
|
|
|
199
211
|
console.log(`Accent ${options.accent} → primary ${derived.primary} (white-contrast ${derived._ratios.whiteOnPrimary}:1)`);
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
// Blurred gradient-mesh background (
|
|
214
|
+
// Blurred gradient-mesh background (mesh-background capability): ambient wash on cover/closing/
|
|
203
215
|
// content (light) and a deep textured wash on section dividers (dark). Uses the
|
|
204
216
|
// ACTIVE palette (so it respects --accent). The dark mesh sits ABOVE the base
|
|
205
217
|
// section fill rect, so qa_deck still resolves the (dark) fill behind white text.
|
|
206
|
-
if (options.bg === "mesh" &&
|
|
218
|
+
if (options.bg === "mesh" && can("mesh-background")) {
|
|
207
219
|
const { genMeshBg } = require("./lib/gen-bg");
|
|
208
220
|
const os = require("os");
|
|
209
221
|
const dims = templateObj.template.dimensions || {};
|
|
@@ -277,8 +289,36 @@ if (require.main === module) {
|
|
|
277
289
|
}
|
|
278
290
|
process.exit(0);
|
|
279
291
|
}
|
|
292
|
+
if (args[0] === "--list-tonalities") {
|
|
293
|
+
const reg = require("./lib/template-registry");
|
|
294
|
+
for (const id of reg.listTonalities()) {
|
|
295
|
+
const p = reg.loadPack(id);
|
|
296
|
+
console.log(`${id} — ${p.intent} [density ${p.density}, variance ${p.variance}; ${p.treatments.join(", ")}]`);
|
|
297
|
+
}
|
|
298
|
+
for (const name of reg.listTemplates()) console.log(`${name} (legacy template)`);
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
280
301
|
if (args[0] === "--list-layouts" && args[1]) {
|
|
281
302
|
const reg = require("./lib/template-registry");
|
|
303
|
+
if (reg.listTonalities().includes(String(args[1]).toLowerCase())) {
|
|
304
|
+
// A tonality lists its families with the titles each can take there, and its variants.
|
|
305
|
+
const G = require("./lib/grid-resolver");
|
|
306
|
+
const p = reg.loadPack(args[1]);
|
|
307
|
+
console.log(`${p.id} — ${p.intent}`);
|
|
308
|
+
console.log(`treatments: ${p.treatments.join(", ")}`);
|
|
309
|
+
console.log("layout families:");
|
|
310
|
+
for (const f of p["layout-families"]) {
|
|
311
|
+
console.log(` ${f} [${G.familyAllows(f).filter((t) => p.treatments.includes(t)).join(", ")}]`);
|
|
312
|
+
}
|
|
313
|
+
console.log(`covers: ${p.covers.join(", ")}`);
|
|
314
|
+
console.log(`sections: ${p.sections.join(", ")}`);
|
|
315
|
+
console.log(`closings: ${p.closings.join(", ")}`);
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
if (!reg.listTemplates().includes(args[1])) {
|
|
319
|
+
console.error(`Unknown tonality or template "${args[1]}". Tonalities: ${reg.listTonalities().join(", ")}; legacy templates: ${reg.listTemplates().join(", ")}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
282
322
|
const t = reg.loadTemplate(args[1]);
|
|
283
323
|
const layouts = (t.mapping && t.mapping.layouts) || {};
|
|
284
324
|
const caps = (t.capabilities && t.capabilities.supported_blocks) || {};
|
|
@@ -294,13 +334,16 @@ if (require.main === module) {
|
|
|
294
334
|
if (args.length === 0) {
|
|
295
335
|
console.error("Usage:");
|
|
296
336
|
console.error(" node compile-deck.js --list-templates");
|
|
297
|
-
console.error(" node compile-deck.js --list-
|
|
337
|
+
console.error(" node compile-deck.js --list-tonalities");
|
|
338
|
+
console.error(" node compile-deck.js --list-layouts <template or tonality>");
|
|
298
339
|
console.error(" node compile-deck.js <input.md> [--ast out.json]");
|
|
299
340
|
console.error(" node compile-deck.js <input.md> --template <name> --html out.html");
|
|
300
341
|
console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx");
|
|
342
|
+
console.error(" node compile-deck.js <input.md> --tonality <id> --pptx out.pptx (or tonality: in the frontmatter)");
|
|
343
|
+
console.error(" node compile-deck.js <input.md> --tonality <id> --faces a2z --pptx out.pptx (A2Z faces on request; Pretendard by default)");
|
|
301
344
|
console.error(" node compile-deck.js <input.md> --template <name> --ast out.json --html out.html --pptx out.pptx");
|
|
302
|
-
console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx --boilerplate");
|
|
303
345
|
console.error(" node compile-deck.js <input.md> --template <name> --export-viz-context ctx.json");
|
|
346
|
+
console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx --boilerplate");
|
|
304
347
|
process.exit(1);
|
|
305
348
|
}
|
|
306
349
|
|
|
@@ -309,12 +352,14 @@ if (require.main === module) {
|
|
|
309
352
|
let templateName = null;
|
|
310
353
|
let htmlPath = null;
|
|
311
354
|
let pptxPath = null;
|
|
312
|
-
let boilerplate = false;
|
|
313
355
|
let embedFonts = false;
|
|
356
|
+
let facesFlag = null;
|
|
314
357
|
let accent = null;
|
|
315
358
|
let font = null;
|
|
316
359
|
let bg = null;
|
|
317
360
|
let vizContextPath = null;
|
|
361
|
+
let tonalityFlag = null;
|
|
362
|
+
let boilerplate = false;
|
|
318
363
|
|
|
319
364
|
for (let i = 1; i < args.length; i++) {
|
|
320
365
|
if (args[i] === "--embed-fonts") { embedFonts = true; continue; }
|
|
@@ -322,6 +367,8 @@ if (require.main === module) {
|
|
|
322
367
|
if (args[i] === "--font" && args[i + 1]) { font = args[i + 1]; i++; continue; }
|
|
323
368
|
if (args[i] === "--bg" && args[i + 1]) { bg = args[i + 1]; i++; continue; }
|
|
324
369
|
if (args[i] === "--export-viz-context" && args[i + 1]) { vizContextPath = args[i + 1]; i++; continue; }
|
|
370
|
+
if (args[i] === "--tonality" && args[i + 1]) { tonalityFlag = args[i + 1]; i++; continue; }
|
|
371
|
+
if (args[i] === "--faces" && args[i + 1]) { facesFlag = args[i + 1]; i++; continue; }
|
|
325
372
|
if (args[i] === "--ast" && args[i + 1]) {
|
|
326
373
|
astPath = args[i + 1];
|
|
327
374
|
i++;
|
|
@@ -339,9 +386,12 @@ if (require.main === module) {
|
|
|
339
386
|
}
|
|
340
387
|
}
|
|
341
388
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
389
|
+
const source = fs.readFileSync(inputPath, "utf-8");
|
|
390
|
+
|
|
391
|
+
// A render needs a design: --template, --tonality, or a tonality: line in the frontmatter.
|
|
392
|
+
const frontTonality = (source.match(/^---[ \t]*\n([\s\S]*?)\n---/) || ["", ""])[1].match(/^tonality:[ \t]*(\S+)/m);
|
|
393
|
+
if ((htmlPath || pptxPath || vizContextPath) && !templateName && !tonalityFlag && !frontTonality) {
|
|
394
|
+
console.error("Error: --template or --tonality (or tonality: in the frontmatter) is required when using --html, --pptx or --export-viz-context");
|
|
345
395
|
process.exit(1);
|
|
346
396
|
}
|
|
347
397
|
|
|
@@ -350,8 +400,6 @@ if (require.main === module) {
|
|
|
350
400
|
console.error("Error: --template is required when using --boilerplate with --pptx");
|
|
351
401
|
process.exit(1);
|
|
352
402
|
}
|
|
353
|
-
|
|
354
|
-
const source = fs.readFileSync(inputPath, "utf-8");
|
|
355
403
|
const sourceDir = path.dirname(path.resolve(inputPath));
|
|
356
404
|
|
|
357
405
|
(async () => {
|
|
@@ -380,7 +428,7 @@ if (require.main === module) {
|
|
|
380
428
|
}
|
|
381
429
|
|
|
382
430
|
// Resolve and render if template + output formats specified
|
|
383
|
-
if (
|
|
431
|
+
if (htmlPath || pptxPath || vizContextPath) {
|
|
384
432
|
// When --boilerplate flag is set, use Python boilerplate engine for PPTX
|
|
385
433
|
if (boilerplate && pptxPath) {
|
|
386
434
|
// Write AST to a temp file, then invoke Python compile_boilerplate
|
|
@@ -403,7 +451,6 @@ if (require.main === module) {
|
|
|
403
451
|
// Run Python boilerplate engine for PPTX
|
|
404
452
|
await new Promise((resolve, reject) => {
|
|
405
453
|
const pyArgs = [pyScript, tmpAstPath, "--output", pptxPath];
|
|
406
|
-
const scriptRoot = path.resolve(scriptDir, "..");
|
|
407
454
|
execFile("python3", pyArgs, { cwd: scriptDir }, (error, stdout, stderr) => {
|
|
408
455
|
// Clean up temp file
|
|
409
456
|
try { fs.unlinkSync(tmpAstPath); } catch (_) {}
|
|
@@ -422,7 +469,7 @@ if (require.main === module) {
|
|
|
422
469
|
html: htmlPath,
|
|
423
470
|
pptx: pptxPath,
|
|
424
471
|
vizContext: vizContextPath,
|
|
425
|
-
}, { sourceDir, accent: effectiveAccent, font: effectiveFont, bg: effectiveBg });
|
|
472
|
+
}, { sourceDir, accent: effectiveAccent, font: effectiveFont, bg: effectiveBg, tonality: tonalityFlag, faces: facesFlag });
|
|
426
473
|
}
|
|
427
474
|
|
|
428
475
|
// Optional: embed bundled fonts into the PPTX for a self-contained deck.
|
|
@@ -22,5 +22,49 @@
|
|
|
22
22
|
"groupRatioMin": 2,
|
|
23
23
|
"equalRowToleranceIn": 0.05,
|
|
24
24
|
"emptyBandMedium": 0.25,
|
|
25
|
-
"emptyBandHigh": 0.35
|
|
25
|
+
"emptyBandHigh": 0.35,
|
|
26
|
+
"outputMinSlides": 8,
|
|
27
|
+
"outputMinTreatments": 3,
|
|
28
|
+
"outputVarietyShare": 0.6,
|
|
29
|
+
"outputVarietyCap": 5,
|
|
30
|
+
"outputMaxShare": 0.4,
|
|
31
|
+
"outputBandFloorPt": 486,
|
|
32
|
+
"outputFooterTopPt": 492,
|
|
33
|
+
"outputBandDeckMax": 0.2,
|
|
34
|
+
"outputBandCaps": [
|
|
35
|
+
[
|
|
36
|
+
2,
|
|
37
|
+
0.28
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
4,
|
|
41
|
+
0.24
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
6,
|
|
45
|
+
0.2
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
8,
|
|
49
|
+
0.16
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
10,
|
|
53
|
+
0.12
|
|
54
|
+
]
|
|
55
|
+
],
|
|
56
|
+
"outputGeometryTolPt": 2,
|
|
57
|
+
"outputSizeTolPt": 0.5,
|
|
58
|
+
"outputAnchorTolPt": 1,
|
|
59
|
+
"outputWideBlockShare": 0.7,
|
|
60
|
+
"outputEvenColumns": 0.85,
|
|
61
|
+
"outputRegionFloorGapPt": 12,
|
|
62
|
+
"outputRegionEmptyShare": 0.4,
|
|
63
|
+
"outputSiblingMinDiffer": 1,
|
|
64
|
+
"outputSiblingMinDifferLong": 2,
|
|
65
|
+
"outputSiblingLongDeck": 4,
|
|
66
|
+
"figureGroundLuminanceMax": 0.25,
|
|
67
|
+
"figureNearWhite": 235,
|
|
68
|
+
"figureLightShare": 0.5,
|
|
69
|
+
"figureMinArea": 0.03
|
|
26
70
|
}
|
|
@@ -11,11 +11,15 @@ from pptx import Presentation
|
|
|
11
11
|
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
12
12
|
from pptx.enum.text import PP_ALIGN
|
|
13
13
|
|
|
14
|
+
from deck_output import check_figures, check_labels, check_output, check_run_ins
|
|
15
|
+
|
|
14
16
|
T = json.loads(Path(__file__).with_name("craft-thresholds.json").read_text())
|
|
15
17
|
EMU = 914400
|
|
16
18
|
NUMERIC = re.compile(r"^[\d\s,.%+\-±()$€£¥₩▲▼]+$")
|
|
17
19
|
HANGUL = re.compile(r"[가-힣]")
|
|
18
20
|
EMOJI = re.compile(r"[\U0001F000-\U0001FAFF\u2600-\u27BF]")
|
|
21
|
+
# Families the tonality compiler draws as display pages; every other `family@` name is a content slide.
|
|
22
|
+
DISPLAY_FAMILY = re.compile(r"(?:cover|section)(?:-|$)|(?:statement|quote|closing-statement|image-full)$")
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
def _box(shape):
|
|
@@ -62,6 +66,9 @@ def _text_size(shape):
|
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
def _display(slide):
|
|
69
|
+
family = next((shape.name.split("@", 1)[1] for shape in slide.shapes if shape.name.startswith("family@")), None)
|
|
70
|
+
if family is not None:
|
|
71
|
+
return bool(DISPLAY_FAMILY.match(family))
|
|
65
72
|
text = [shape for shape in slide.shapes if getattr(shape, "has_text_frame", False) and shape.text.strip()]
|
|
66
73
|
visual = [shape for shape in slide.shapes if getattr(shape, "has_table", False) or getattr(shape, "has_chart", False)]
|
|
67
74
|
return bool(text) and max(map(_text_size, text)) >= T["displayPt"] and len(text) <= 2 and not visual
|
|
@@ -79,6 +86,12 @@ def _radius(shape):
|
|
|
79
86
|
return None
|
|
80
87
|
|
|
81
88
|
|
|
89
|
+
def _page_furniture(box, width, height, tolerance=0.03):
|
|
90
|
+
"""A fill on two or more canvas edges (a band, a rail, a colour field) frames the page, not content."""
|
|
91
|
+
x, y, w, h = box
|
|
92
|
+
return sum([x <= tolerance, y <= tolerance, x + w >= width - tolerance, y + h >= height - tolerance]) >= 2
|
|
93
|
+
|
|
94
|
+
|
|
82
95
|
def _vertical_gaps(top, bottom, boxes):
|
|
83
96
|
cursor = top
|
|
84
97
|
gaps = []
|
|
@@ -92,14 +105,16 @@ def _vertical_gaps(top, bottom, boxes):
|
|
|
92
105
|
def analyse(path):
|
|
93
106
|
deck = Presentation(str(path))
|
|
94
107
|
canvas = deck.slide_width * deck.slide_height
|
|
95
|
-
findings =
|
|
108
|
+
findings = check_output(deck) + check_labels(deck) + check_figures(deck) + check_run_ins(deck)
|
|
96
109
|
|
|
97
110
|
def add(rule, severity, slide, value, threshold, tier="measured", shape=None):
|
|
98
111
|
findings.append({"rule": rule, "severity": severity, "tier": tier, "slide": slide,
|
|
99
112
|
"shape": shape, "value": value, "threshold": threshold})
|
|
100
113
|
|
|
101
114
|
for slide_no, slide in enumerate(deck.slides, 1):
|
|
102
|
-
|
|
115
|
+
# The sample-data tag is a deliberate label on every slide, not part of the design.
|
|
116
|
+
shapes = [shape for shape in slide.shapes if isinstance(shape.width, int) and isinstance(shape.height, int)
|
|
117
|
+
and not shape.name.startswith("lit-notice")]
|
|
103
118
|
display = _display(slide)
|
|
104
119
|
hues = []
|
|
105
120
|
for index, shape in enumerate(shapes, 1):
|
|
@@ -185,10 +200,24 @@ def analyse(path):
|
|
|
185
200
|
add("OF-105", "MEDIUM", slide_no, round(inter / intra, 2), T["groupRatioMin"])
|
|
186
201
|
|
|
187
202
|
if not display:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
# The compiler names its title frame; a title in the lower half closes the body from below.
|
|
204
|
+
title = next((shape for shape in shapes if shape.name.startswith("title@")), None) or max(
|
|
205
|
+
(shape for shape in shapes if getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) >= T["displayPt"]), key=_text_size, default=None)
|
|
206
|
+
if title is not None and title.top > deck.slide_height / 2:
|
|
207
|
+
top, bottom = .8, title.top / EMU - .1
|
|
208
|
+
else:
|
|
209
|
+
top = (title.top + title.height) / EMU + .1 if title else .8
|
|
210
|
+
bottom = deck.slide_height / EMU - .55
|
|
211
|
+
held = [_box(shape) for shape in shapes if shape is not title and (getattr(shape, "has_table", False) or getattr(shape, "has_chart", False) or shape.shape_type == MSO_SHAPE_TYPE.PICTURE or (getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) > T["captionPt"]))]
|
|
212
|
+
# A title in a side rail: the body stands beside the rail from the title's top, and what stands
|
|
213
|
+
# in the rail under the title (criteria labels, takeaways, the source) belongs to the rail.
|
|
214
|
+
if title is not None and title.left < .1 * deck.slide_width and title.width < .35 * deck.slide_width and title.top < deck.slide_height / 4:
|
|
215
|
+
rx, ry, rw, rh = _box(title)
|
|
216
|
+
beside = [box for box in held if box[0] >= rx + rw - .05 and box[1] < bottom]
|
|
217
|
+
under = [box for box in held if box[0] + box[2] <= rx + rw + .05 and box[1] >= ry + rh - .05]
|
|
218
|
+
if beside and len(beside) + len(under) == len([box for box in held if box[1] < bottom]):
|
|
219
|
+
top, held = ry, beside
|
|
220
|
+
content = [box for box in held if box[1] < bottom and box[1] + box[3] > top]
|
|
192
221
|
if content and bottom > top:
|
|
193
222
|
gaps = _vertical_gaps(top, bottom, content)
|
|
194
223
|
fraction = max(gaps) / (bottom - top)
|
|
@@ -196,11 +225,11 @@ def analyse(path):
|
|
|
196
225
|
add("OF-109", "HIGH" if fraction > T["emptyBandHigh"] else "MEDIUM", slide_no, round(fraction, 3), {"medium": T["emptyBandMedium"], "high": T["emptyBandHigh"]})
|
|
197
226
|
|
|
198
227
|
card_gaps = []
|
|
199
|
-
for index, card in enumerate(shapes, 1):
|
|
228
|
+
for index, card in enumerate([] if display else shapes, 1):
|
|
200
229
|
if _fill(card) is None or card.width * card.height / canvas >= T["fullBleedFraction"]:
|
|
201
230
|
continue
|
|
202
231
|
x, y, width, height = _box(card)
|
|
203
|
-
if width <= 0 or height <= 0:
|
|
232
|
+
if width <= 0 or height <= 0 or _page_furniture((x, y, width, height), deck.slide_width / EMU, deck.slide_height / EMU):
|
|
204
233
|
continue
|
|
205
234
|
frame = getattr(card, "text_frame", None) if getattr(card, "has_text_frame", False) else None
|
|
206
235
|
top = y + (frame.margin_top / EMU if frame else 0)
|