@litfamily/litopencode 1.0.15 → 1.0.17
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 +11 -0
- package/README-Ko-KR.md +11 -11
- package/README.md +11 -11
- package/dist/activation-routing.js +1 -1
- package/dist/auto-handoff.js +34 -1
- package/dist/features.js +2 -2
- 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 +29 -0
- package/skills/lit-docx/templates/tonalities/journal.yaml +26 -0
- package/skills/lit-docx/templates/tonalities/manual.yaml +27 -0
- package/skills/lit-docx/templates/tonalities/memo.yaml +27 -0
- package/skills/lit-docx/templates/tonalities/proposal.yaml +27 -0
- package/skills/lit-docx/templates/tonalities/report.yaml +27 -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 +132 -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 +151 -0
- package/skills/lit-pptx/references/tonalities/night.md +148 -0
- package/skills/lit-pptx/references/tonalities/paper.md +136 -0
- package/skills/lit-pptx/references/tonalities/signal.md +148 -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 +17 -5
- 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 +105 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +110 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +106 -0
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +106 -0
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +111 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +105 -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
|
@@ -223,7 +223,6 @@ function blockToRegion(blockType, block, layout) {
|
|
|
223
223
|
switch (blockType) {
|
|
224
224
|
case "title": return "title";
|
|
225
225
|
case "body": return "body";
|
|
226
|
-
case "chart": return "chart";
|
|
227
226
|
case "kpi-table":
|
|
228
227
|
// content/summary layout defines "table" region; main layout defines "kpi_table" region
|
|
229
228
|
return (layout === "main") ? "kpi_table" : "table";
|
|
@@ -247,21 +246,25 @@ function blockToRegion(blockType, block, layout) {
|
|
|
247
246
|
* much vertical room the block needs, so the rule lives here where the geometry
|
|
248
247
|
* is worked out and the renderer reads it from here.
|
|
249
248
|
*/
|
|
250
|
-
function looksLikeKpi(content) {
|
|
249
|
+
function looksLikeKpi(content, o = {}) {
|
|
250
|
+
if (content && content.chart) return false;
|
|
251
251
|
const headers = (content && content.headers) || [];
|
|
252
252
|
const rows = (content && content.rows) || [];
|
|
253
253
|
const cols = headers.length;
|
|
254
|
-
|
|
254
|
+
// Under a tonality a row may hold up to six figures; legacy cards stop at four.
|
|
255
|
+
if (cols < 2 || cols > (o.basis ? 6 : 4)) return false;
|
|
255
256
|
if (cols === 2) {
|
|
256
257
|
const pairs = [headers, ...rows];
|
|
257
258
|
return pairs.length <= 4 && pairs.every((p) => String(p[0]).length <= 12);
|
|
258
259
|
}
|
|
260
|
+
// Under a tonality a second row may give each figure's basis or comparison.
|
|
261
|
+
if (o.basis && rows.length === 2) return rows[0].every((c) => String(c).length <= 12) && rows[1].every((c) => String(c).length <= 24);
|
|
259
262
|
return rows.length === 1 && rows[0].every((c) => String(c).length <= 12);
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
// A KPI badge stacks a small label over a large value, so it stands roughly
|
|
263
|
-
//
|
|
264
|
-
const
|
|
265
|
+
// A KPI badge stacks a small label over a large value, so it stands roughly as tall as the
|
|
266
|
+
// template's kpi.blockHeight token (1.75in on the legacy templates) no matter how few rows fed it.
|
|
267
|
+
const grid = require("./grid-resolver");
|
|
265
268
|
|
|
266
269
|
// ---------------------------------------------------------------------------
|
|
267
270
|
// Capability validation
|
|
@@ -274,9 +277,9 @@ const KPI_BLOCK_HEIGHT_IN = 1.75;
|
|
|
274
277
|
*/
|
|
275
278
|
const CAPABILITY_RULES = {
|
|
276
279
|
cover: new Set(["title", "notes"]),
|
|
277
|
-
section: new Set(["title", "notes"]),
|
|
278
|
-
content: new Set(["title", "key-message", "body", "
|
|
279
|
-
main: new Set(["title", "key-message", "body", "
|
|
280
|
+
section: new Set(["title", "notes", "image", "figure-caption"]),
|
|
281
|
+
content: new Set(["title", "key-message", "body", "image", "notes", "kpi-table", "figure-caption", "table-caption"]),
|
|
282
|
+
main: new Set(["title", "key-message", "body", "main-box", "kpi-table", "image", "notes", "figure-caption", "table-caption"]),
|
|
280
283
|
summary: new Set(["title", "key-message", "summary-group", "image", "kpi-table", "figure-caption", "table-caption"]),
|
|
281
284
|
closing: new Set(["title", "notes"]),
|
|
282
285
|
};
|
|
@@ -295,7 +298,6 @@ const ALWAYS_ALLOWED = new Set(["notes", ...PLACEMENT_BLOCK_TYPES]);
|
|
|
295
298
|
const REGION_TO_BLOCKS = {
|
|
296
299
|
title: ["title"],
|
|
297
300
|
body: ["body"],
|
|
298
|
-
chart: ["chart"],
|
|
299
301
|
table: ["kpi-table", "table"],
|
|
300
302
|
kpi_table: ["kpi-table"],
|
|
301
303
|
image: ["image"],
|
|
@@ -435,6 +437,33 @@ function resolveColumns(slideIndex, layout, regionDefs, block) {
|
|
|
435
437
|
* }
|
|
436
438
|
* @returns {object} — Resolved deck spec with positioned regions
|
|
437
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* Caption prefixes in the deck's language: "도"/"표" for a Korean deck, "Figure"/
|
|
442
|
+
* "Table" otherwise. Frontmatter `lang: ko|en` decides; without it, a deck whose
|
|
443
|
+
* text is at least a tenth Hangul (one syllable weighs about two Latin letters)
|
|
444
|
+
* is Korean. English decks used to get Korean prefixes.
|
|
445
|
+
*/
|
|
446
|
+
function captionPrefixes(ast) {
|
|
447
|
+
const meta = (ast.deck && ast.deck.metadata) || {};
|
|
448
|
+
const lang = String(meta.lang || meta.language || "").toLowerCase();
|
|
449
|
+
let korean;
|
|
450
|
+
if (lang) korean = lang.startsWith("ko");
|
|
451
|
+
else {
|
|
452
|
+
const strings = [];
|
|
453
|
+
const walk = (node, key) => {
|
|
454
|
+
if (typeof node === "string") { if (!["type", "layout", "src", "variant", "kind", "align"].includes(key)) strings.push(node); }
|
|
455
|
+
else if (Array.isArray(node)) node.forEach((n) => walk(n, key));
|
|
456
|
+
else if (node && typeof node === "object") for (const [k, v] of Object.entries(node)) walk(v, k);
|
|
457
|
+
};
|
|
458
|
+
walk(ast.slides || [], "");
|
|
459
|
+
const text = strings.join(" ");
|
|
460
|
+
const hangul = (text.match(/[\uac00-\ud7a3]/gu) || []).length;
|
|
461
|
+
const latin = (text.match(/[A-Za-z]/gu) || []).length;
|
|
462
|
+
korean = hangul * 2 >= 0.1 * (hangul * 2 + latin) && hangul > 0;
|
|
463
|
+
}
|
|
464
|
+
return korean ? ["도", "표"] : ["Figure", "Table"];
|
|
465
|
+
}
|
|
466
|
+
|
|
438
467
|
function resolve(ast, templateObj) {
|
|
439
468
|
if (!ast || !ast.slides) {
|
|
440
469
|
throw new Error("resolve(): invalid AST — expected an object with a slides array");
|
|
@@ -442,6 +471,7 @@ function resolve(ast, templateObj) {
|
|
|
442
471
|
if (!templateObj) {
|
|
443
472
|
throw new Error("resolve(): templateObj is required");
|
|
444
473
|
}
|
|
474
|
+
if (templateObj.pack && !templateObj.pack.legacy) return resolvePack(ast, templateObj);
|
|
445
475
|
|
|
446
476
|
// Parse YAML strings from template object
|
|
447
477
|
const mapping = typeof templateObj.mapping === "string"
|
|
@@ -462,20 +492,34 @@ function resolve(ast, templateObj) {
|
|
|
462
492
|
let tableCount = 0;
|
|
463
493
|
|
|
464
494
|
const deckMeta = ast.deck || {};
|
|
495
|
+
const [FIGURE, TABLE] = captionPrefixes(ast);
|
|
465
496
|
|
|
497
|
+
const tokens = (templateObj.pack && templateObj.pack.tokens) || {};
|
|
498
|
+
const kpiBlockHeight = (tokens.kpi && tokens.kpi.blockHeight) || 1.75;
|
|
466
499
|
for (const slide of ast.slides) {
|
|
467
|
-
|
|
500
|
+
if (slide.meta && slide.meta.title) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`Slide ${slide.index + 1}: "title: ${slide.meta.title}" picks a title treatment, which needs a tonality; ` +
|
|
503
|
+
`${templateObj.name || "this template"} is a legacy template with one title look`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const resolved = resolveSlide(slide, mapping, capabilities, deckMeta, kpiBlockHeight);
|
|
468
507
|
// Auto-number captions across the deck
|
|
469
508
|
for (const [name, region] of Object.entries(resolved.regions || {})) {
|
|
470
509
|
if (region.content && region.content.type === "figure-caption") {
|
|
471
510
|
figureCount++;
|
|
472
511
|
region.content.number = figureCount;
|
|
473
|
-
region.content.prefix =
|
|
512
|
+
region.content.prefix = FIGURE;
|
|
474
513
|
}
|
|
475
|
-
if (region.content && region.content.type === "table-caption") {
|
|
514
|
+
if (region.content && region.content.type === "table-caption" && region.content.figure) {
|
|
515
|
+
// A chart's caption is a figure caption, numbered with the figures.
|
|
516
|
+
figureCount++;
|
|
517
|
+
region.content.number = figureCount;
|
|
518
|
+
region.content.prefix = FIGURE;
|
|
519
|
+
} else if (region.content && region.content.type === "table-caption") {
|
|
476
520
|
tableCount++;
|
|
477
521
|
region.content.number = tableCount;
|
|
478
|
-
region.content.prefix =
|
|
522
|
+
region.content.prefix = TABLE;
|
|
479
523
|
}
|
|
480
524
|
}
|
|
481
525
|
resolvedSlides.push(resolved);
|
|
@@ -495,10 +539,13 @@ function resolve(ast, templateObj) {
|
|
|
495
539
|
* @param {object} [capabilities] — Parsed capabilities.yaml
|
|
496
540
|
* @returns {object} — Resolved slide spec
|
|
497
541
|
*/
|
|
498
|
-
function resolveSlide(slide, mapping, capabilities, deckMeta) {
|
|
499
|
-
const { index,
|
|
542
|
+
function resolveSlide(slide, mapping, capabilities, deckMeta, kpiBlockHeight) {
|
|
543
|
+
const { index, blocks } = slide;
|
|
500
544
|
|
|
501
|
-
// 1. Look up layout configuration
|
|
545
|
+
// 1. Look up layout configuration. A layout family the template does not declare (kpi-row,
|
|
546
|
+
// chart-insight, …) is drawn on the template layout that hosts its kind of slide.
|
|
547
|
+
const family = familyOf(slide.layout, mapping, index);
|
|
548
|
+
const layout = (mapping.layouts[slide.layout] || slide.layout === "free") ? slide.layout : family.host;
|
|
502
549
|
const layoutConfig = mapping.layouts[layout] ||
|
|
503
550
|
(layout === "free" ? FREE_LAYOUT : null);
|
|
504
551
|
if (!layoutConfig) {
|
|
@@ -657,7 +704,7 @@ function resolveSlide(slide, mapping, capabilities, deckMeta) {
|
|
|
657
704
|
// lands on top of them.
|
|
658
705
|
const tableContent = tableRegion.content || {};
|
|
659
706
|
const needed = looksLikeKpi(tableContent)
|
|
660
|
-
?
|
|
707
|
+
? kpiBlockHeight
|
|
661
708
|
: rowCount * rowH;
|
|
662
709
|
const tableBottom = tblY + Math.max(needed, tableRegion.h || 0) + 0.3;
|
|
663
710
|
if ((regions.body.y || 1.044) < tableBottom) {
|
|
@@ -701,6 +748,7 @@ function resolveSlide(slide, mapping, capabilities, deckMeta) {
|
|
|
701
748
|
return {
|
|
702
749
|
index,
|
|
703
750
|
layout,
|
|
751
|
+
family: family.id,
|
|
704
752
|
layoutSource: layoutConfig.layout_source,
|
|
705
753
|
decorations,
|
|
706
754
|
regions,
|
|
@@ -721,15 +769,349 @@ function extractContent(block) {
|
|
|
721
769
|
if (block.type === "key-message") return block.content || "";
|
|
722
770
|
if (block.type === "notes") return block.content || "";
|
|
723
771
|
if (block.type === "body") return { type: "body", items: block.items || [] };
|
|
724
|
-
if (block.type === "
|
|
725
|
-
if (block.type === "kpi-table") return { type: "kpi-table", headers: block.headers || [], rows: block.rows || [], caption: block.caption || "" };
|
|
772
|
+
if (block.type === "kpi-table") return { type: "kpi-table", headers: block.headers || [], rows: block.rows || [], caption: block.caption || "", ...(block.chart ? { chart: block.chart } : {}) };
|
|
726
773
|
if (block.type === "image") return { type: "image", src: block.src || "", caption: block.caption || "" };
|
|
727
774
|
if (block.type === "figure-caption") return { type: "figure-caption", caption: block.caption || "" };
|
|
728
|
-
if (block.type === "table-caption") return { type: "table-caption", caption: block.caption || "" };
|
|
775
|
+
if (block.type === "table-caption") return { type: "table-caption", caption: block.caption || "", ...(block.figure ? { figure: true } : {}) };
|
|
729
776
|
if (block.type === "summary-group") return { type: "summary-group", heading: block.heading, items: block.items || [] };
|
|
730
777
|
return "";
|
|
731
778
|
}
|
|
732
779
|
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
// Layout families
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* The family a slide is stamped with, and the template layout that hosts it when the template
|
|
786
|
+
* does not declare the name itself. A name that is neither keeps the template's own error.
|
|
787
|
+
*/
|
|
788
|
+
function familyOf(layout, mapping, index) {
|
|
789
|
+
if (grid.LEGACY_LAYOUTS[layout]) return { id: grid.LEGACY_LAYOUTS[layout], host: layout };
|
|
790
|
+
if (mapping.layouts[layout]) return { id: layout, host: layout };
|
|
791
|
+
try {
|
|
792
|
+
const f = grid.resolveFamily(layout, index + 1);
|
|
793
|
+
return { id: f.family || layout, host: f.kind === "cover" || f.kind === "section" ? f.kind : "content" };
|
|
794
|
+
} catch (_) {
|
|
795
|
+
return { id: layout, host: layout };
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// Tonality packs: regions on the 12-column grid
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
const PACK_BLOCKS = {
|
|
804
|
+
cover: new Set(["title", "notes"]),
|
|
805
|
+
section: new Set(["title", "notes", "image", "figure-caption"]),
|
|
806
|
+
content: new Set(["title", "key-message", "body", "main-box", "kpi-table", "image", "notes", "figure-caption", "table-caption", "summary-group"]),
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const plain = (text) => String(text || "").replace(/\*\*/g, "").trim();
|
|
810
|
+
|
|
811
|
+
/** What a slide holds, as the treatment rules need it. */
|
|
812
|
+
function slideFacts(blocks) {
|
|
813
|
+
const all = blocks.flatMap((b) => (b.type === "columns" ? b.columns.flatMap((c) => c.blocks || []) : [b]));
|
|
814
|
+
const has = (type) => all.some((b) => b.type === type);
|
|
815
|
+
const textBlocks = all.filter((b) => ["body", "summary-group", "main-box", "key-message"].includes(b.type)).length +
|
|
816
|
+
(blocks.some((b) => b.type === "columns") ? 1 : 0);
|
|
817
|
+
const bodyItems = all.filter((b) => b.type === "body").reduce((n, b) => n + (b.items || []).length, 0);
|
|
818
|
+
return {
|
|
819
|
+
image: has("image"),
|
|
820
|
+
visual: has("image") || has("kpi-table"),
|
|
821
|
+
textBlocks: textBlocks + (bodyItems > 1 ? 1 : 0),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Comparison halves of numbers are data; halves of arguments are content. */
|
|
826
|
+
function comparisonRole(blocks) {
|
|
827
|
+
const text = JSON.stringify(blocks);
|
|
828
|
+
const digits = (text.match(/\d/g) || []).length;
|
|
829
|
+
return digits / Math.max(1, plain(text).length) >= 0.04 ? "data" : "content";
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** The ordered content of a pack slide, captions bound to the visual before them. */
|
|
833
|
+
// Families whose geometry is a chart: a table of numbers there is drawn as one.
|
|
834
|
+
const CHART_FAMILIES = new Set(["chart-insight", "full-chart", "kpi-over-chart", "dashboard-grid"]);
|
|
835
|
+
const NUMBER_CELL = /^[+\-−±▲▼△▽]?\s*[\d.,]+\s*(?:%p?|배|x|pt|bp|[가-힣]{1,3}|[A-Za-z]{1,3})?$/u;
|
|
836
|
+
|
|
837
|
+
/** A table whose data columns are all numbers, over three or more rows, can be drawn as a chart. */
|
|
838
|
+
function chartable(c) {
|
|
839
|
+
const rows = c.rows || [];
|
|
840
|
+
const headers = c.headers || [];
|
|
841
|
+
return headers.length >= 2 && rows.length >= 3 &&
|
|
842
|
+
headers.slice(1).every((_, ci) => rows.every((r) => r[ci + 1] != null && NUMBER_CELL.test(plain(r[ci + 1]))));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function packItems(blocks, numbering, charts = false) {
|
|
846
|
+
const items = [];
|
|
847
|
+
for (const block of blocks) {
|
|
848
|
+
if (["title", "notes", "key-message", "box", "shape"].includes(block.type)) continue;
|
|
849
|
+
if (block.type === "columns") {
|
|
850
|
+
items.push({ type: "columns", tracks: block.tracks, columns: block.columns.map((c) => packItems(c.blocks || [], numbering, charts)) });
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const content = extractContent(block);
|
|
854
|
+
if (charts && block.type === "kpi-table" && !content.chart && chartable(content)) {
|
|
855
|
+
content.chart = { type: (content.rows || []).length > 8 ? "line" : "column" };
|
|
856
|
+
}
|
|
857
|
+
if (block.type === "figure-caption" || block.type === "table-caption") {
|
|
858
|
+
// A table drawn as a chart is captioned as a figure.
|
|
859
|
+
const charted = [...items].reverse().find((i) => i.type === "kpi-table");
|
|
860
|
+
const figure = block.type === "figure-caption" || block.figure || Boolean(charted && charted.content.chart);
|
|
861
|
+
content.number = figure ? ++numbering.figure : ++numbering.table;
|
|
862
|
+
content.prefix = figure ? numbering.FIGURE : numbering.TABLE;
|
|
863
|
+
const target = [...items].reverse().find((i) => ["kpi-table", "image"].includes(i.type));
|
|
864
|
+
if (target && !target.caption) target.caption = content;
|
|
865
|
+
else items.push({ type: "caption", content });
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
items.push({ type: block.type, content });
|
|
869
|
+
}
|
|
870
|
+
return items;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* What covers and sections can draw from the deck itself: the first image, the part names (from
|
|
875
|
+
* the agenda heads, else the section titles), the first row of headline numbers and a real year.
|
|
876
|
+
*/
|
|
877
|
+
function deckFacts(ast) {
|
|
878
|
+
const facts = { image: null, index: [], figures: [], numeral: null };
|
|
879
|
+
const sections = [];
|
|
880
|
+
for (const slide of ast.slides) {
|
|
881
|
+
for (const b of slide.blocks) {
|
|
882
|
+
if (!facts.image && b.type === "image" && b.src) facts.image = { src: b.src, caption: b.caption };
|
|
883
|
+
if (!facts.figures.length && b.type === "kpi-table" && !b.chart && looksLikeKpi(b, { basis: true })) {
|
|
884
|
+
const headers = b.headers || [];
|
|
885
|
+
const row = (b.rows || [])[0] || [];
|
|
886
|
+
facts.figures = headers.slice(0, 4).map((h, i) => ({ label: plain(h), value: plain(row[i]) })).filter((f) => f.value);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (slide.layout === "agenda" && !facts.index.length) {
|
|
890
|
+
const body = slide.blocks.find((b) => b.type === "body");
|
|
891
|
+
const items = ((body && body.items) || []).filter((i) => plain(i.heading || i.text)).slice(0, 6);
|
|
892
|
+
facts.index = items.map((i) => plain(i.heading || String(i.text || "").replace(/^\*\*(.+?)\*\*.*$/u, "$1")));
|
|
893
|
+
// What each part is about, so a section titled with an entry's description finds its place too.
|
|
894
|
+
facts.indexAbout = items.map((i) => plain(String(i.text || "").replace(/^\*\*(.+?)\*\*/u, "")));
|
|
895
|
+
}
|
|
896
|
+
if (/^section/u.test(slide.layout)) {
|
|
897
|
+
const t = slide.blocks.find((b) => b.type === "title");
|
|
898
|
+
if (t) sections.push(plain(t.content));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (!facts.index.length && sections.length >= 2) facts.index = sections.slice(0, 6);
|
|
902
|
+
facts.indexAbout = facts.indexAbout || [];
|
|
903
|
+
const meta = (ast.deck && ast.deck.metadata) || {};
|
|
904
|
+
const year = `${ast.deck && ast.deck.title ? ast.deck.title : ""} ${meta.date || ""}`.match(/\b(19|20)\d{2}\b/u);
|
|
905
|
+
facts.numeral = year ? year[0] : null;
|
|
906
|
+
return facts;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Titles of the content slides a section opens, up to the next section or closing (at most three). */
|
|
910
|
+
function sectionContents(ast, from) {
|
|
911
|
+
const out = [];
|
|
912
|
+
for (const s of ast.slides.slice(from + 1)) {
|
|
913
|
+
if (/^(section|closing)/u.test(s.layout || "")) break;
|
|
914
|
+
const t = s.blocks.find((b) => b.type === "title");
|
|
915
|
+
if (t && plain(t.content)) out.push(plain(t.content));
|
|
916
|
+
}
|
|
917
|
+
return out.slice(0, 3);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* A preview line keeps a bold run-in label bold, followed by a colon ("**결과:** 대출 기간이 …"), unless the
|
|
922
|
+
* label already ends in its own separator; any other line is plain text.
|
|
923
|
+
*/
|
|
924
|
+
function runIn(text) {
|
|
925
|
+
const m = /^\*\*(.+?)\*\*\s*(.*)$/u.exec(String(text || "").trim());
|
|
926
|
+
if (!m || !plain(m[2])) return plain(text);
|
|
927
|
+
const label = plain(m[1]);
|
|
928
|
+
return `**${/[::.)\]–—-]$/u.test(label) ? label : `${label}:`}** ${plain(m[2])}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** The first body lines (bullets or text, up to five) of the slides a section opens, by title. */
|
|
932
|
+
function sectionDigest(ast, from) {
|
|
933
|
+
const out = [];
|
|
934
|
+
for (const s of ast.slides.slice(from + 1)) {
|
|
935
|
+
if (/^(section|closing)/u.test(s.layout || "")) break;
|
|
936
|
+
const t = s.blocks.find((b) => b.type === "title");
|
|
937
|
+
if (!t || !plain(t.content)) continue;
|
|
938
|
+
const lines = s.blocks.filter((b) => b.type === "body").flatMap((b) => b.items || [])
|
|
939
|
+
.filter((i) => ["bullet", "text", "numbered"].includes(i.type) && plain(i.text)).slice(0, 5).map((i) => runIn(i.text));
|
|
940
|
+
out.push({ title: plain(t.content), lines });
|
|
941
|
+
}
|
|
942
|
+
return out.slice(0, 3);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Position of a section title in the deck's part index (by entry or by its description), or -1. */
|
|
946
|
+
function indexOf(facts, title) {
|
|
947
|
+
const t = String(title || "").trim();
|
|
948
|
+
if (!t || facts.index.length < 2) return -1;
|
|
949
|
+
const at = facts.index.findIndex((name) => name === t);
|
|
950
|
+
return at >= 0 ? at : facts.indexAbout.findIndex((about) => about === t);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Can this cover, section or closing variant be drawn with what the deck and slide hold? true or the reason. */
|
|
954
|
+
function variantApplies(variant, facts, slide) {
|
|
955
|
+
const ownImage = slide.blocks.find((b) => b.type === "image");
|
|
956
|
+
if (["cover-split-image", "cover-full-image"].includes(variant) && !facts.image) return "the deck has no image";
|
|
957
|
+
if (variant === "section-image" && !ownImage) return "the section slide has no image";
|
|
958
|
+
if (variant === "cover-numeral" && !facts.numeral) return "the deck names no year or real figure";
|
|
959
|
+
if (variant === "cover-index" && facts.index.length < 2) return "the deck has no agenda or sections to list";
|
|
960
|
+
if (variant === "cover-figures" && facts.figures.length < 2) return "the deck has no row of headline numbers";
|
|
961
|
+
if (variant === "closing-statement" && slide.blocks.some((b) => ["kpi-table", "image"].includes(b.type))) return "a statement closing has no table or image";
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* The variant of a cover, section or closing: the one the source names when the pack lists it and
|
|
967
|
+
* it can be drawn, else the pack's first that can. A generic name (cover, section, closing) asks
|
|
968
|
+
* for the pack's own first choice.
|
|
969
|
+
*/
|
|
970
|
+
function chooseVariant(kind, named, pack, facts, slide, notes) {
|
|
971
|
+
const list = pack[kind === "cover" ? "covers" : kind === "section" ? "sections" : "closings"] || [];
|
|
972
|
+
const n = slide.index + 1;
|
|
973
|
+
if (named && list.includes(named)) {
|
|
974
|
+
const why = variantApplies(named, facts, slide);
|
|
975
|
+
if (why === true) return named;
|
|
976
|
+
notes.push(`slide ${n}: ${named} cannot be drawn (${why})`);
|
|
977
|
+
} else if (named && !["cover", "section", "closing"].includes(named)) {
|
|
978
|
+
notes.push(`slide ${n}: ${named} is not a ${pack.id} ${kind} variant (${list.join(", ")})`);
|
|
979
|
+
}
|
|
980
|
+
const pick = list.find((v) => v !== named && variantApplies(v, facts, slide) === true);
|
|
981
|
+
if (!pick) throw new Error(`Slide ${n}: no ${pack.id} ${kind} variant (${list.join(", ")}) can be drawn here`);
|
|
982
|
+
if (named) notes.push(`slide ${n}: drawn as ${pick}`);
|
|
983
|
+
return pick;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Resolve every slide of a deck against a tonality pack: its family, the title treatments it may
|
|
988
|
+
* take (the pack's role default first, then the others the family allows; the renderer picks among
|
|
989
|
+
* them by fill and the variance dial), the treatment geometry inputs and its content in reading
|
|
990
|
+
* order. Notes go to the build log.
|
|
991
|
+
*/
|
|
992
|
+
function resolvePack(ast, templateObj) {
|
|
993
|
+
const pack = templateObj.pack;
|
|
994
|
+
const sizes = pack.tok.sizes;
|
|
995
|
+
const decoration = new Set(pack.decoration || []);
|
|
996
|
+
const notes = [];
|
|
997
|
+
const [FIGURE, TABLE] = captionPrefixes(ast);
|
|
998
|
+
const numbering = { figure: 0, table: 0, FIGURE, TABLE };
|
|
999
|
+
const facts = deckFacts(ast);
|
|
1000
|
+
const families = pack["layout-families"] || [];
|
|
1001
|
+
const wide = { title: grid.faceWidth(pack.faces.title), display: grid.faceWidth(pack.faces.display) };
|
|
1002
|
+
let part = 0;
|
|
1003
|
+
|
|
1004
|
+
const slides = ast.slides.map((slide) => {
|
|
1005
|
+
const n = slide.index + 1;
|
|
1006
|
+
const meta = slide.meta || {};
|
|
1007
|
+
const fam = grid.resolveFamily(slide.layout, n);
|
|
1008
|
+
const kind = fam.kind === "free" ? "content" : fam.kind;
|
|
1009
|
+
const allowed = PACK_BLOCKS[kind];
|
|
1010
|
+
for (const block of slide.blocks) {
|
|
1011
|
+
if (["box", "shape", "columns"].includes(block.type) || allowed.has(block.type)) continue;
|
|
1012
|
+
throw new Error(`Slide ${n} (layout: ${slide.layout}): block type "${block.type}" is not allowed on a ${kind} slide. ` +
|
|
1013
|
+
`Allowed: ${[...allowed].join(", ")}`);
|
|
1014
|
+
}
|
|
1015
|
+
const titleBlock = slide.blocks.find((b) => b.type === "title");
|
|
1016
|
+
const title = titleBlock ? titleBlock.content : "";
|
|
1017
|
+
const lead = slide.blocks.find((b) => b.type === "key-message");
|
|
1018
|
+
const placements = slide.blocks
|
|
1019
|
+
.filter((b) => b.type === "box" || b.type === "shape")
|
|
1020
|
+
.map(({ type, ...rest }) => ({ kind: type === "shape" ? "shape" : "box", ...rest }));
|
|
1021
|
+
const base = { index: slide.index, layout: slide.layout, kind, title, lead: lead ? lead.content : null, placements, blocks: slide.blocks, meta, regions: {} };
|
|
1022
|
+
|
|
1023
|
+
if (kind === "cover" || kind === "section") {
|
|
1024
|
+
if (meta.title) throw new Error(`Slide ${n}: a ${kind} slide takes its variant's title, not "title: ${meta.title}"`);
|
|
1025
|
+
const variant = chooseVariant(kind, fam.family || fam.legacyLayout, pack, facts, slide, notes);
|
|
1026
|
+
let parts = null;
|
|
1027
|
+
if (kind === "section") {
|
|
1028
|
+
// A section numbers itself by its place in the deck's agenda when its title is an agenda
|
|
1029
|
+
// entry (or that entry's description); otherwise it counts the sections so far.
|
|
1030
|
+
const at = indexOf(facts, plain(title));
|
|
1031
|
+
part = at >= 0 ? at + 1 : part + 1;
|
|
1032
|
+
parts = at >= 0 ? facts.index.length : null;
|
|
1033
|
+
}
|
|
1034
|
+
const ownImage = slide.blocks.find((b) => b.type === "image");
|
|
1035
|
+
const cover = { ...facts, image: kind === "section" && ownImage ? { src: ownImage.src, caption: ownImage.caption } : facts.image };
|
|
1036
|
+
const contents = kind === "section" ? sectionContents(ast, slide.index) : [];
|
|
1037
|
+
const digest = kind === "section" ? sectionDigest(ast, slide.index) : [];
|
|
1038
|
+
return { ...base, family: variant, variant, part, parts, cover, contents, digest, regions: { title: { content: title } } };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
let family = fam.family === "free" ? "text-column" : fam.family;
|
|
1042
|
+
let role = family === "comparison" ? comparisonRole(slide.blocks) : fam.role;
|
|
1043
|
+
if (grid.VARIANTS.closing.includes(family) || fam.legacyLayout === "closing") {
|
|
1044
|
+
family = chooseVariant("closing", fam.legacyLayout === "closing" ? "closing" : family, pack, facts, slide, notes);
|
|
1045
|
+
role = family === "closing-statement" ? "statement" : "content";
|
|
1046
|
+
} else if (!families.includes(family)) {
|
|
1047
|
+
notes.push(`slide ${n}: ${family} is not a ${pack.id} layout family (${families.join(", ")}); drawn with its own geometry`);
|
|
1048
|
+
}
|
|
1049
|
+
let titleOnly = null;
|
|
1050
|
+
if (family === "statement" && !pack.treatments.includes("statement")) {
|
|
1051
|
+
// A pack without a statement title sets the sentence as a top title, the largest it has.
|
|
1052
|
+
titleOnly = ["top-plain-large", "band", "top-rule"].find((t) => pack.treatments.includes(t));
|
|
1053
|
+
if (!titleOnly) throw new Error(`Slide ${n}: ${pack.id} has no statement or top title to set a statement slide with; choose another tonality`);
|
|
1054
|
+
notes.push(`slide ${n}: ${pack.id} has no statement title, so the sentence is drawn as a ${titleOnly} title`);
|
|
1055
|
+
family = "text-column";
|
|
1056
|
+
role = "content";
|
|
1057
|
+
}
|
|
1058
|
+
const items = packItems(slide.blocks, numbering, CHART_FAMILIES.has(family));
|
|
1059
|
+
const paras = slide.blocks.filter((b) => b.type === "body").flatMap((b) => b.items || []);
|
|
1060
|
+
// A closing is not part of a section, so it never carries the part number.
|
|
1061
|
+
const closing = grid.VARIANTS.closing.includes(family);
|
|
1062
|
+
// An agenda numbers its own rows: a count beside its title reads as a stray section number.
|
|
1063
|
+
const numeral = family === "agenda" ? null : part > 0 && !closing ? part : null;
|
|
1064
|
+
const statementCols = (pack.display || {}).statement === "offset" ? [5, 12] : null;
|
|
1065
|
+
const ctx = { grid: pack.grid, sizes, title: plain(title), hero: pack.hero, decoration, numeral, rail: pack.rail, wide, statementCols, tight: Boolean((pack.tok || {}).tight) };
|
|
1066
|
+
const facts1 = slideFacts(slide.blocks);
|
|
1067
|
+
// A quote under a statement title says the quote; the source line becomes the support line.
|
|
1068
|
+
let statementText = null;
|
|
1069
|
+
let supportText = null;
|
|
1070
|
+
if (family === "quote") {
|
|
1071
|
+
const texts = paras.filter((i) => i.type === "text").map((i) => plain(i.text));
|
|
1072
|
+
const by = texts.find((t) => /^[—–-]\s*/u.test(t));
|
|
1073
|
+
statementText = texts.filter((t) => t !== by).join(" ") || null;
|
|
1074
|
+
supportText = by || null;
|
|
1075
|
+
}
|
|
1076
|
+
const applicable = (t) => {
|
|
1077
|
+
const c = t === "statement" && statementText ? { ...ctx, title: statementText } : ctx;
|
|
1078
|
+
const f = t === "statement" && family === "quote" ? { ...facts1, textBlocks: 1 } : facts1;
|
|
1079
|
+
return grid.treatmentApplies(t, f, c);
|
|
1080
|
+
};
|
|
1081
|
+
let allow = grid.familyAllows(family);
|
|
1082
|
+
let treatment;
|
|
1083
|
+
if (titleOnly) {
|
|
1084
|
+
treatment = titleOnly;
|
|
1085
|
+
} else if (!families.includes(family) && !meta.title && !pack.treatments.some((t) => allow.includes(t) && applicable(t) === true)) {
|
|
1086
|
+
// A family from outside the pack that takes none of its titles gets the pack's title for its role.
|
|
1087
|
+
allow = [pack["role-defaults"][role] || pack.treatments[0], ...grid.familyAllows("text-column")];
|
|
1088
|
+
treatment = pack.treatments.find((t) => allow.includes(t) && applicable(t) === true) || allow[0];
|
|
1089
|
+
notes.push(`slide ${n}: the ${family} family takes none of the ${pack.id} titles; drawn under ${treatment}`);
|
|
1090
|
+
} else {
|
|
1091
|
+
treatment = grid.pickTreatment({
|
|
1092
|
+
pack, family, role, override: meta.title, slideNumber: n, used: new Set(), max: Infinity,
|
|
1093
|
+
applicable, onNote: (note) => notes.push(note),
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
const candidates = meta.title || titleOnly ? [treatment] : [treatment, ...pack.treatments.filter((t) =>
|
|
1097
|
+
t !== treatment && allow.includes(t) && !["statement", "overlay"].includes(t) && applicable(t) === true)];
|
|
1098
|
+
return {
|
|
1099
|
+
...base,
|
|
1100
|
+
family,
|
|
1101
|
+
role,
|
|
1102
|
+
treatment,
|
|
1103
|
+
candidates,
|
|
1104
|
+
ctx,
|
|
1105
|
+
statementText,
|
|
1106
|
+
supportText,
|
|
1107
|
+
items,
|
|
1108
|
+
regions: { title: { content: title } },
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
return { deck: { ...ast.deck }, slides, notes };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
733
1115
|
// ---------------------------------------------------------------------------
|
|
734
1116
|
// Exports
|
|
735
1117
|
// ---------------------------------------------------------------------------
|
|
@@ -25,7 +25,8 @@ function parseSimpleYaml(raw) {
|
|
|
25
25
|
// Slide-local keys the engine acts on. This set is deliberately closed: a key
|
|
26
26
|
// the engine ignores has no effect a reader can see, so admitting one buys
|
|
27
27
|
// nothing and costs the body line it was indistinguishable from.
|
|
28
|
-
|
|
28
|
+
// `title` names the slide's title treatment when it should differ from the pack's role default.
|
|
29
|
+
const SLIDE_KEYS = new Set(["variant", "title"]);
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Split the text after a `layout:` line into slide-local keys and body content.
|
|
@@ -79,15 +80,16 @@ function load(source) {
|
|
|
79
80
|
|
|
80
81
|
const fm = parseSimpleYaml(fmRaw);
|
|
81
82
|
|
|
82
|
-
if (!fm.template) {
|
|
83
|
-
throw new Error("Frontmatter must declare a 'template' field");
|
|
83
|
+
if (!fm.template && !fm.tonality) {
|
|
84
|
+
throw new Error("Frontmatter must declare a 'template' or a 'tonality' field");
|
|
84
85
|
}
|
|
85
86
|
if (!fm.title) {
|
|
86
87
|
throw new Error("Frontmatter must declare a 'title' field");
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
const deck = {
|
|
90
|
-
|
|
91
|
+
// A tonality-only deck records its pack as the design it was written for.
|
|
92
|
+
template: fm.template || `tonality:${fm.tonality}`,
|
|
91
93
|
title: fm.title,
|
|
92
94
|
metadata: {},
|
|
93
95
|
};
|
|
@@ -16,10 +16,10 @@ const A2Z_WEIGHTS = ["Thin", "ExtraLight", "Light", "Regular", "Medium", "SemiBo
|
|
|
16
16
|
/** Map a font-family name to bundled OTF file(s): [{path, weight}]. */
|
|
17
17
|
function bundledFontFiles(family) {
|
|
18
18
|
const f = (family || "").trim();
|
|
19
|
-
if (f === "
|
|
19
|
+
if (f === "Pretendard") {
|
|
20
20
|
return [
|
|
21
|
-
{ file: "pretendard-font/public/static/
|
|
22
|
-
{ file: "pretendard-font/public/static/
|
|
21
|
+
{ file: "pretendard-font/public/static/Pretendard-Regular.otf", weight: 400 },
|
|
22
|
+
{ file: "pretendard-font/public/static/Pretendard-Bold.otf", weight: 700 },
|
|
23
23
|
]
|
|
24
24
|
.map((e) => ({ ...e, abs: path.join(REPO_ROOT, e.file) }))
|
|
25
25
|
.filter((e) => fs.existsSync(e.abs));
|
|
@@ -148,12 +148,6 @@ function renderContent(content) {
|
|
|
148
148
|
return html;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
if (typeof content === "object" && content.type === "chart") {
|
|
152
|
-
const header = `<tr><th>Category</th>${content.series.map((s) => `<th>${esc(s.name)}</th>`).join("")}</tr>`;
|
|
153
|
-
const rows = content.labels.map((label, i) => `<tr><td>${esc(label)}</td>${content.series.map((s) => `<td style="text-align:right">${esc(s.values[i])}</td>`).join("")}</tr>`).join("");
|
|
154
|
-
return `<table aria-label="Chart data" style="width:100%;border-collapse:collapse">${header}${rows}</table>`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
151
|
// KPI Table content
|
|
158
152
|
if (typeof content === "object" && content.type === "kpi-table") {
|
|
159
153
|
let html = '<table style="width:100%;border-collapse:collapse;margin:4px 0;">';
|