@litfamily/litclaude 1.0.19 → 1.0.21
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 +12 -0
- package/README.md +14 -8
- package/README_ko-KR.md +13 -8
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +3 -2
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-hook.js +4 -4
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +91 -31
- package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +3 -6
- package/plugins/litclaude/skills/lit-docx/SKILL.md +50 -19
- package/plugins/litclaude/skills/lit-docx/references/components.md +207 -0
- package/plugins/litclaude/skills/lit-docx/references/direction-step.md +277 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/01-report-ko.md +166 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/02-brief-ko.md +105 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/03-manual-ko.md +164 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/04-proposal-ko.md +138 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/05-memo-en.md +55 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/06-journal-en.md +130 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/07-report-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/08-proposal-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/09-memo-ko.md +45 -0
- package/plugins/litclaude/skills/lit-docx/references/page-composition.md +221 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/brief.md +143 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/journal.md +142 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/manual.md +148 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/memo.md +137 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/proposal.md +140 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/report.md +156 -0
- package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +274 -73
- package/plugins/litclaude/skills/lit-docx/scripts/docx_design.py +2449 -0
- package/plugins/litclaude/skills/lit-docx/scripts/docx_layout.py +959 -0
- package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +66 -9
- package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +18 -6
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/brief.yaml +28 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/journal.yaml +25 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/manual.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/memo.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/proposal.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/report.yaml +26 -0
- package/plugins/litclaude/skills/lit-pptx/SKILL.md +105 -48
- package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +59 -30
- package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +119 -42
- package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +13 -6
- package/plugins/litclaude/skills/lit-pptx/references/density-and-fill.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +7 -4
- package/plugins/litclaude/skills/lit-pptx/references/direction-step.md +186 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/01-pitch-signal-ko.md +210 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/02-pitch-studio-ko.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/03-business-review-ledger-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/04-business-review-night-en.md +203 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/06-lecture-chalk-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/07-data-review-night-en.md +194 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/08-image-led-atlas-ko.md +175 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/09-briefing-gazette-ko.md +204 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/10-status-update-ledger-ko.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/11-portfolio-studio-en.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/12-research-tutorial-chalk-en.md +174 -0
- package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/layout-families.md +198 -0
- package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +2 -2
- package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/title-treatments.md +109 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/atlas.md +139 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/chalk.md +148 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/gazette.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/ledger.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/night.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/paper.md +151 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/signal.md +150 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/studio.md +132 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +64 -16
- package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +664 -3
- package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +48 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +362 -11
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +211 -65
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-pack.js +3559 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +10 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +286 -1
- package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +39 -10
- package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +22 -7
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +92 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +97 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +94 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/night/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/paper/pack.yaml +98 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/signal/pack.yaml +94 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/studio/pack.yaml +92 -0
|
@@ -16,9 +16,11 @@ const os = require("os");
|
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const { runtimeRequire } = require("./runtime-require");
|
|
18
18
|
const { extractContent, looksLikeKpi } = require("./layout-resolver");
|
|
19
|
+
const { LEGACY_TOKENS } = require("./template-registry");
|
|
19
20
|
|
|
20
|
-
// Narrowest a KPI badge can be and still read as one glance.
|
|
21
|
-
|
|
21
|
+
// Narrowest a KPI badge can be and still read as one glance (template token kpi.minCardWidth).
|
|
22
|
+
let KPI_MIN_CARD_WIDTH_IN = LEGACY_TOKENS.kpi.minCardWidth;
|
|
23
|
+
let KPI_GAP = LEGACY_TOKENS.kpi.gap;
|
|
22
24
|
|
|
23
25
|
const TOOLKIT_ROOT = path.resolve(__dirname, "../..");
|
|
24
26
|
const ASSETS_MEDIA = path.join(TOOLKIT_ROOT, "assets", "media");
|
|
@@ -129,43 +131,52 @@ function containBox(x, y, w, h, imageSize) {
|
|
|
129
131
|
return { x: x + (w - fittedW) / 2, y, w: fittedW, h };
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
// Bullet items
|
|
133
|
-
|
|
134
|
-
const BULLET_SIZE = 16;
|
|
134
|
+
// Bullet items. Geometry comes from the template's tokens (bullet.*, secHeader.*, summary.*,
|
|
135
|
+
// toc.*), reassigned in render(); these defaults are the legacy values.
|
|
135
136
|
let BULLET_COLOR = "1D4ED8"; // bullet color (reassigned per template)
|
|
136
137
|
let SECTION_ACCENT = "1D4ED8"; // section-card / accent color (reassigned per template)
|
|
137
138
|
const BULLET_PREFIX = "\u2022 "; // "• " (bullet + two spaces)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const BULLET_H = 0.38;
|
|
141
|
-
const BULLET_Y_SPACING = 0.42;
|
|
142
|
-
const BULLET_LINE_SPACING = 1.25;
|
|
139
|
+
let BULLET_X = LEGACY_TOKENS.bullet.x;
|
|
140
|
+
let BULLET_W = LEGACY_TOKENS.bullet.w;
|
|
143
141
|
|
|
144
142
|
// Section headers
|
|
145
143
|
let SEC_HEADER_FONT = FONT_BOLD;
|
|
146
|
-
|
|
144
|
+
let SEC_HEADER_SIZE = LEGACY_TOKENS.secHeader.size;
|
|
147
145
|
const SEC_HEADER_COLOR = "000000";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
let SEC_HEADER_X = LEGACY_TOKENS.secHeader.x;
|
|
147
|
+
let SEC_HEADER_W = LEGACY_TOKENS.secHeader.w;
|
|
148
|
+
let SEC_HEADER_H = LEGACY_TOKENS.secHeader.h;
|
|
151
149
|
|
|
152
150
|
// Summary (narrow width)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
let SUMMARY_HEADER_W = LEGACY_TOKENS.summary.headerW;
|
|
152
|
+
let SUMMARY_BULLET_W = LEGACY_TOKENS.summary.bulletW;
|
|
153
|
+
let SUMMARY_BULLET_SIZE = LEGACY_TOKENS.summary.bulletSize;
|
|
154
|
+
let SUMMARY_BULLET_H = LEGACY_TOKENS.summary.bulletH;
|
|
155
|
+
let SUMMARY_BULLET_Y_SPACING = LEGACY_TOKENS.summary.ySpacing;
|
|
156
|
+
let SUMMARY_BULLET_LINE_SPACING = LEGACY_TOKENS.summary.lineSpacing;
|
|
159
157
|
|
|
160
158
|
// TOC items (separate shapes)
|
|
161
159
|
let TOC_FONT = FONT_BOLD;
|
|
162
|
-
|
|
160
|
+
let TOC_SIZE = LEGACY_TOKENS.toc.size;
|
|
163
161
|
const TOC_COLOR = "000000";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
let TOC_X = LEGACY_TOKENS.toc.x;
|
|
163
|
+
let TOC_W = LEGACY_TOKENS.toc.w;
|
|
164
|
+
let TOC_H = LEGACY_TOKENS.toc.h;
|
|
165
|
+
let TOC_Y_START = LEGACY_TOKENS.toc.yStart;
|
|
166
|
+
let TOC_Y_SPACING = LEGACY_TOKENS.toc.ySpacing;
|
|
167
|
+
|
|
168
|
+
/** Reassign the token-driven geometry from the template's pack (legacy values when absent). */
|
|
169
|
+
function applyTokens(tokens) {
|
|
170
|
+
const t = tokens || LEGACY_TOKENS;
|
|
171
|
+
({ x: BULLET_X, w: BULLET_W } = t.bullet);
|
|
172
|
+
({ size: SEC_HEADER_SIZE, x: SEC_HEADER_X, w: SEC_HEADER_W, h: SEC_HEADER_H } = t.secHeader);
|
|
173
|
+
({ headerW: SUMMARY_HEADER_W, bulletW: SUMMARY_BULLET_W, bulletSize: SUMMARY_BULLET_SIZE, bulletH: SUMMARY_BULLET_H,
|
|
174
|
+
ySpacing: SUMMARY_BULLET_Y_SPACING, lineSpacing: SUMMARY_BULLET_LINE_SPACING } = t.summary);
|
|
175
|
+
({ size: TOC_SIZE, x: TOC_X, w: TOC_W, h: TOC_H, yStart: TOC_Y_START, ySpacing: TOC_Y_SPACING } = t.toc);
|
|
176
|
+
({ pad: CARD_PAD, chip: CARD_CHIP, minH: CARD_MIN_H, gap: CARD_GAP, radius: CARD_RADIUS } = t.card);
|
|
177
|
+
({ minCardWidth: KPI_MIN_CARD_WIDTH_IN, gap: KPI_GAP } = t.kpi);
|
|
178
|
+
NOTICE = { ...t.notice };
|
|
179
|
+
}
|
|
169
180
|
|
|
170
181
|
function weightedTextLength(text) {
|
|
171
182
|
let total = 0;
|
|
@@ -404,7 +415,7 @@ function estimateDiagnosticCardHeight(card, w) {
|
|
|
404
415
|
return Math.max(0.56, Math.min(2.35, padY * 2 + titleH + bodyH));
|
|
405
416
|
}
|
|
406
417
|
|
|
407
|
-
function renderCramStressBody(slide, items,
|
|
418
|
+
function renderCramStressBody(slide, items, pageH) {
|
|
408
419
|
const cards = bodyItemsToDiagnosticCards(items);
|
|
409
420
|
const columns = 3;
|
|
410
421
|
const groups = cards.length ? splitCardsByWeight(cards, columns) : splitLinesByWeight(bodyItemsToDiagnosticLines(items), columns).map((group) => [{ title: "", body: group }]);
|
|
@@ -550,7 +561,7 @@ function renderCramStressSlide(slide, slideSpec, pageW, pageH, sourceDir) {
|
|
|
550
561
|
});
|
|
551
562
|
}
|
|
552
563
|
if (regions.body && regions.body.content && regions.body.content.type === "body") {
|
|
553
|
-
renderCramStressBody(slide, regions.body.content.items || [],
|
|
564
|
+
renderCramStressBody(slide, regions.body.content.items || [], pageH);
|
|
554
565
|
}
|
|
555
566
|
if (regions.image && regions.image.content) {
|
|
556
567
|
renderCramStressImageSheet(slide, regions.image.content, sourceDir, pageW, pageH);
|
|
@@ -614,7 +625,7 @@ function buildFontRoles(templateObj) {
|
|
|
614
625
|
* Add decoration elements to a pptxgenjs slide.
|
|
615
626
|
* Note: slide_number is intentionally omitted — the reference template has none.
|
|
616
627
|
*/
|
|
617
|
-
function addDecorations(slide, decorations,
|
|
628
|
+
function addDecorations(slide, decorations, fontRoles) {
|
|
618
629
|
if (!decorations || !Array.isArray(decorations)) return;
|
|
619
630
|
|
|
620
631
|
for (const d of decorations) {
|
|
@@ -1113,7 +1124,7 @@ function addRegion(slide, name, region, fontRoles, pageH, regionBottom, isToc, s
|
|
|
1113
1124
|
fontSize: opts.fontSize,
|
|
1114
1125
|
color: opts.color,
|
|
1115
1126
|
...(opts.charSpacing != null ? { charSpacing: opts.charSpacing } : {}),
|
|
1116
|
-
}), opts);
|
|
1127
|
+
}), name === "title" ? { ...opts, objectName: "title@legacy" } : opts);
|
|
1117
1128
|
return;
|
|
1118
1129
|
}
|
|
1119
1130
|
|
|
@@ -1313,7 +1324,7 @@ function addRegion(slide, name, region, fontRoles, pageH, regionBottom, isToc, s
|
|
|
1313
1324
|
}
|
|
1314
1325
|
}
|
|
1315
1326
|
|
|
1316
|
-
// ──
|
|
1327
|
+
// ── Rich card, KPI and table rendering (templates with the rich-blocks capability) ──
|
|
1317
1328
|
|
|
1318
1329
|
/** Split a title string into runs, emphasizing **word** in the primary color. */
|
|
1319
1330
|
function azureTitleRuns(text, P) {
|
|
@@ -1331,6 +1342,7 @@ function cleanMd(s) {
|
|
|
1331
1342
|
.replace(/^#{1,6}\s+/, "")
|
|
1332
1343
|
.replace(/^\s*[*-]\s+/, "")
|
|
1333
1344
|
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
1345
|
+
.replace(/(?<![\w*])\*([^*\s](?:[^*]*[^*\s])?)\*(?![\w*])/gu, "$1")
|
|
1334
1346
|
.replace(/`/g, "")
|
|
1335
1347
|
.trim();
|
|
1336
1348
|
}
|
|
@@ -1344,12 +1356,15 @@ function mdRuns(text, runOpts = {}) {
|
|
|
1344
1356
|
.replace(/^#{1,6}\s+/, "")
|
|
1345
1357
|
.replace(/^\s*[*-]\s+/, "")
|
|
1346
1358
|
.replace(/`/g, "");
|
|
1347
|
-
|
|
1359
|
+
// **x** is bold; *x* (a journal or book name) is italic.
|
|
1360
|
+
const parts = clean.split(/(\*\*[^*]+\*\*|(?<![\w*])\*[^*\s](?:[^*]*[^*\s])?\*(?![\w*]))/gu).filter((p) => p !== "");
|
|
1348
1361
|
if (parts.length === 0) return [{ text: "", options: { ...runOpts } }];
|
|
1349
1362
|
return parts.map((p) =>
|
|
1350
1363
|
p.startsWith("**") && p.endsWith("**")
|
|
1351
1364
|
? { text: p.slice(2, -2), options: { ...runOpts, bold: true } }
|
|
1352
|
-
:
|
|
1365
|
+
: p.length > 2 && p.startsWith("*") && p.endsWith("*")
|
|
1366
|
+
? { text: p.slice(1, -1), options: { ...runOpts, italic: true } }
|
|
1367
|
+
: { text: p, options: { ...runOpts } }
|
|
1353
1368
|
);
|
|
1354
1369
|
}
|
|
1355
1370
|
|
|
@@ -1384,10 +1399,13 @@ function bodyToCards(items) {
|
|
|
1384
1399
|
return cards.slice(0, 4);
|
|
1385
1400
|
}
|
|
1386
1401
|
|
|
1387
|
-
// Card geometry shared by drawing and sizing, so a card is exactly as tall as its text
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1402
|
+
// Card geometry shared by drawing and sizing, so a card is exactly as tall as its text
|
|
1403
|
+
// (template tokens card.*; reassigned in render()).
|
|
1404
|
+
let CARD_PAD = LEGACY_TOKENS.card.pad;
|
|
1405
|
+
let CARD_CHIP = LEGACY_TOKENS.card.chip;
|
|
1406
|
+
let CARD_MIN_H = LEGACY_TOKENS.card.minH;
|
|
1407
|
+
let CARD_GAP = LEGACY_TOKENS.card.gap;
|
|
1408
|
+
let CARD_RADIUS = LEGACY_TOKENS.card.radius;
|
|
1391
1409
|
|
|
1392
1410
|
// Primary reading text stays at or above the 12 pt floor of the design laws.
|
|
1393
1411
|
function cardBodySize(card) {
|
|
@@ -1433,7 +1451,7 @@ function drawAzureCard(slide, x, y, w, h, card, idx, P, emphasized) {
|
|
|
1433
1451
|
const bg = emphasized ? hex(P.primary_deep) : hex(P.tint);
|
|
1434
1452
|
const headColor = emphasized ? "FFFFFF" : hex(P.ink);
|
|
1435
1453
|
const bodyColor = emphasized ? hex(P.emphasisTextColor) : hex(P.ink_muted);
|
|
1436
|
-
slide.addShape("roundRect", { x, y, w, h, rectRadius:
|
|
1454
|
+
slide.addShape("roundRect", { x, y, w, h, rectRadius: CARD_RADIUS, fill: { color: bg }, line: { type: "none" } });
|
|
1437
1455
|
const pad = CARD_PAD;
|
|
1438
1456
|
const innerW = w - 2 * pad;
|
|
1439
1457
|
// numbered chip
|
|
@@ -1482,7 +1500,7 @@ function renderAzureCards(slide, items, region, P) {
|
|
|
1482
1500
|
if (!cards.length) return;
|
|
1483
1501
|
const n = cards.length;
|
|
1484
1502
|
const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1;
|
|
1485
|
-
const gap =
|
|
1503
|
+
const gap = CARD_GAP;
|
|
1486
1504
|
const cardW = (w - (n - 1) * gap) / n;
|
|
1487
1505
|
const h = fittedCardHeight(cards, cardW, region.h || 3.0);
|
|
1488
1506
|
cards.forEach((c, i) => {
|
|
@@ -1569,13 +1587,13 @@ function renderAzureGroupCard(slide, content, region, P, idx, height) {
|
|
|
1569
1587
|
// ── Tables and charts that fill their region ────────────────────────────────
|
|
1570
1588
|
|
|
1571
1589
|
// A cell reads as a number when, after units and signs, only digits remain.
|
|
1572
|
-
const NUMERIC_CELL = /^[
|
|
1590
|
+
const NUMERIC_CELL = /^(?:[▲▼△▽]\s*)?[+\-−±]?\s*[\d.,]+\s*(?:%p?|배|x|pt|bp|[가-힣]{1,3}|[A-Za-z]{1,3})?$/u;
|
|
1573
1591
|
const isNumericCell = (text) => NUMERIC_CELL.test(cleanMd(String(text)).trim());
|
|
1574
1592
|
|
|
1575
|
-
/** Columns whose data cells are all numbers are right-aligned. */
|
|
1593
|
+
/** Columns whose data cells are all numbers are right-aligned; a dash for a missing value is blank. */
|
|
1576
1594
|
function numericColumns(headers, rows) {
|
|
1577
1595
|
return headers.map((_, ci) => {
|
|
1578
|
-
const cells = rows.map((r) => r[ci]).filter((c) => c != null && String(c)
|
|
1596
|
+
const cells = rows.map((r) => r[ci]).filter((c) => c != null && !/^[\s—–\-]*$/u.test(cleanMd(String(c))));
|
|
1579
1597
|
return cells.length > 0 && cells.every(isNumericCell);
|
|
1580
1598
|
});
|
|
1581
1599
|
}
|
|
@@ -1650,13 +1668,47 @@ function renderChart(slide, content, region, style, bottom) {
|
|
|
1650
1668
|
const series = headers.slice(1).map((name, si) => ({
|
|
1651
1669
|
name: cleanMd(String(name)),
|
|
1652
1670
|
labels,
|
|
1653
|
-
|
|
1671
|
+
// A missing cell stays null: pptxgenjs writes it as a blank point, and the line breaks there instead of dropping to zero.
|
|
1672
|
+
values: rows.map((r) => chartNumber(r[si + 1])),
|
|
1654
1673
|
}));
|
|
1674
|
+
// Data labels keep the decimals the table writes: "18.0" stays 18.0 beside a table that says 18.0.
|
|
1675
|
+
const decimals = Math.min(3, Math.max(0, ...rows.flatMap((r) => r.slice(1).map((v) => ((/\.(\d+)/u.exec(cleanMd(String(v == null ? "" : v)).replace(/,/gu, "")) || [, ""])[1]).length))));
|
|
1655
1676
|
const x = region.x || 0.6, y = region.y || 1.6, w = region.w || 8;
|
|
1656
1677
|
const h = Math.max(1.6, ((bottom || y + (region.h || 3.2)) - y));
|
|
1657
1678
|
const colors = style.series.map((c) => hex(c));
|
|
1658
1679
|
const round = kind === "pie" || kind === "doughnut";
|
|
1659
1680
|
const data = round ? series.slice(0, 1) : series;
|
|
1681
|
+
// Crowding, estimated in points: a category label wider than its slot is slanted; side-by-side
|
|
1682
|
+
// value labels wider than their bars collide, so the values are read off a shown axis instead.
|
|
1683
|
+
const ems = (t) => [...String(t)].reduce((a, ch) => a + (/[\u3131-\uD7A3]/u.test(ch) ? 0.94 : /\s/u.test(ch) ? 0.28 : 0.56), 0);
|
|
1684
|
+
const labelPt = style.labelSize || 11;
|
|
1685
|
+
const slot = (w * 72 * 0.85) / Math.max(1, labels.length);
|
|
1686
|
+
// A renderer adds space between Hangul and digits ("25 년 2Q"), so a label needs a quarter of its slot spare.
|
|
1687
|
+
// A horizontal bar chart lists its categories down the left side; slanted there, LibreOffice skips some of them.
|
|
1688
|
+
const slanted = !round && kind !== "line" && spec.type !== "bar" && Math.max(0, ...labels.map(ems)) * labelPt > slot * 0.75;
|
|
1689
|
+
const clustered = kind === "bar" && spec.type !== "stacked" && data.length > 1;
|
|
1690
|
+
const valueW = Math.max(0, ...data.flatMap((d) => d.values.map((v) => ems(Number(v).toLocaleString("en-US"))))) * labelPt;
|
|
1691
|
+
// Several lines share one band of the plot, so a value printed over each point lands on its neighbours'.
|
|
1692
|
+
const crowded = !round && ((clustered && valueW > (slot / 1.6 / data.length) * 1.1) || (kind === "line" && data.length > 1));
|
|
1693
|
+
const values = data.flatMap((d) => d.values).filter((v) => v !== null);
|
|
1694
|
+
// Horizontal bars of several series stack thin bars down each row; once a bar is thinner than its value label, the labels touch.
|
|
1695
|
+
const horizontal = !round && spec.type === "bar";
|
|
1696
|
+
const dataPt = style.labelSize || (round ? 12 : 10);
|
|
1697
|
+
const barPt = (h * 72 - 54) / Math.max(1, labels.length) / (data.length + 0.6);
|
|
1698
|
+
const thin = horizontal && data.length > 1 && spec.labels !== false && barPt < dataPt * 1.2;
|
|
1699
|
+
// A narrow horizontal bar chart gets few axis numbers, each with room to stand level; left to itself, LibreOffice slants a crowded value axis.
|
|
1700
|
+
const top = Math.max(0, ...values), low = Math.min(0, ...values);
|
|
1701
|
+
const tickW = ems(Math.round(Math.max(top, -low)).toLocaleString("en-US")) * dataPt;
|
|
1702
|
+
const catW = Math.min(w * 72 * 0.4, Math.max(0, ...labels.map(ems)) * labelPt + 8);
|
|
1703
|
+
const ticks = Math.floor((w * 72 - catW - 16) / (tickW * 2));
|
|
1704
|
+
const nice = (step) => { const p = 10 ** Math.floor(Math.log10(step)); return [1, 2, 2.5, 5, 10].find((m) => m * p >= step) * p; };
|
|
1705
|
+
const majorUnit = horizontal && top > low && ticks < 6 ? nice((top - low) / Math.max(1, ticks - 1)) : null;
|
|
1706
|
+
const zeroBased = !round && values.length > 0 && values.every((v) => v >= 0);
|
|
1707
|
+
// The unit stands once: a legend whose series names carry it already says it, so the axis title goes;
|
|
1708
|
+
// otherwise it is set level (a rotated title stacks Hangul a glyph a line and reads tiny; 360 degrees, since
|
|
1709
|
+
// pptxgenjs writes no angle for 0 and the axis default is vertical). Amendments 8.
|
|
1710
|
+
const unitShown = spec.unit && data.length > 1 && data.every((d) => String(d.name || "").includes(spec.unit));
|
|
1711
|
+
const axisTitle = spec.unit && !unitShown && !(Boolean(style.valAxisHidden) && !crowded && !thin);
|
|
1660
1712
|
slide.addChart(kind, data, {
|
|
1661
1713
|
x, y, w, h,
|
|
1662
1714
|
barDir: spec.type === "bar" ? "bar" : "col",
|
|
@@ -1666,22 +1718,31 @@ function renderChart(slide, content, region, style, bottom) {
|
|
|
1666
1718
|
showLegend: round || data.length > 1,
|
|
1667
1719
|
legendPos: round ? "r" : "t",
|
|
1668
1720
|
legendFontFace: style.bodyFont, legendFontSize: 11, legendColor: hex(style.ink),
|
|
1669
|
-
showValue: !round && spec.labels !== false,
|
|
1721
|
+
showValue: !round && spec.labels !== false && !crowded && !thin,
|
|
1670
1722
|
showPercent: round && spec.labels !== false, showLabel: false,
|
|
1671
|
-
dataLabelFontFace: style.bodyFont, dataLabelFontSize: round ? 12 : 10,
|
|
1723
|
+
dataLabelFontFace: style.bodyFont, dataLabelFontSize: style.labelSize || (round ? 12 : 10),
|
|
1672
1724
|
dataLabelColor: round ? "FFFFFF" : hex(style.ink),
|
|
1673
1725
|
dataLabelPosition: round ? "ctr" : kind === "line" ? "t" : "outEnd",
|
|
1674
|
-
dataLabelFormatCode: round ? "0%" : "#,##0
|
|
1675
|
-
catAxisLabelFontFace: style.bodyFont, catAxisLabelFontSize: 11, catAxisLabelColor: hex(style.ink),
|
|
1676
|
-
valAxisLabelFontFace: style.bodyFont, valAxisLabelFontSize: 10, valAxisLabelColor: hex(style.ink_muted),
|
|
1726
|
+
dataLabelFormatCode: round ? "0%" : decimals ? `#,##0.${"0".repeat(decimals)}` : "#,##0",
|
|
1727
|
+
catAxisLabelFontFace: style.bodyFont, catAxisLabelFontSize: style.labelSize || 11, catAxisLabelColor: hex(style.ink),
|
|
1728
|
+
valAxisLabelFontFace: style.bodyFont, valAxisLabelFontSize: style.labelSize || 10, valAxisLabelColor: hex(style.ink_muted),
|
|
1677
1729
|
valAxisLabelFormatCode: "#,##0",
|
|
1678
|
-
|
|
1730
|
+
// A pack with direct labels and no gridlines drops the value axis: the labels carry the values.
|
|
1731
|
+
valAxisHidden: Boolean(style.valAxisHidden) && !crowded && !thin,
|
|
1732
|
+
...(majorUnit ? { valAxisMajorUnit: majorUnit } : {}),
|
|
1733
|
+
// With negative values the zero line runs through the plot, so the category labels move to the axis minimum, off the bars.
|
|
1734
|
+
...(horizontal && low < 0 ? { catAxisLabelPos: "low" } : {}),
|
|
1735
|
+
// Blank points leave a gap, never a joined or zero point.
|
|
1736
|
+
displayBlanksAs: "gap",
|
|
1737
|
+
...(style.valMax ? { valAxisMaxVal: style.valMax } : {}),
|
|
1738
|
+
...(zeroBased ? { valAxisMinVal: 0 } : {}),
|
|
1739
|
+
...(slanted ? { catAxisLabelRotate: -45 } : {}),
|
|
1679
1740
|
catAxisLineShow: true, catAxisLineColor: hex(style.line),
|
|
1680
|
-
valGridLine: { color: hex(style.line), size: 0.5 },
|
|
1741
|
+
valGridLine: style.grid === false && !crowded && !thin ? { style: "none" } : { color: hex(style.line), size: 0.5 },
|
|
1681
1742
|
valAxisLineShow: false,
|
|
1682
1743
|
lineSize: 2.5, lineDataSymbol: "circle", lineDataSymbolSize: 7,
|
|
1683
1744
|
holeSize: 58,
|
|
1684
|
-
...(
|
|
1745
|
+
...(axisTitle ? { showValAxisTitle: true, valAxisTitle: spec.unit, valAxisTitleRotate: 360, valAxisTitleFontSize: style.labelSize || 10, valAxisTitleColor: hex(style.ink_muted), valAxisTitleFontFace: style.bodyFont } : {}),
|
|
1685
1746
|
...(spec.title ? { showTitle: true, title: spec.title, titleFontFace: style.titleFont, titleFontSize: 13, titleColor: hex(style.ink) } : {}),
|
|
1686
1747
|
});
|
|
1687
1748
|
}
|
|
@@ -1706,7 +1767,7 @@ function renderAzureKpi(slide, content, region, P) {
|
|
|
1706
1767
|
const n = Math.min(values.length, 4);
|
|
1707
1768
|
if (!n) return;
|
|
1708
1769
|
const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1, h = region.h || 1.7;
|
|
1709
|
-
const gap =
|
|
1770
|
+
const gap = KPI_GAP;
|
|
1710
1771
|
const cardW = (w - (n - 1) * gap) / n;
|
|
1711
1772
|
for (let i = 0; i < n; i++) {
|
|
1712
1773
|
const fill = i === 0;
|
|
@@ -1757,7 +1818,9 @@ function kpiCards(content) {
|
|
|
1757
1818
|
return [headers, ...rows].slice(0, 4).map((p) => ({ value: p[0], label: p[1] }));
|
|
1758
1819
|
}
|
|
1759
1820
|
const row = rows[0] || [];
|
|
1760
|
-
|
|
1821
|
+
// A second body row is each figure's basis or comparison ("전년 대비 +12%", "목표 50%").
|
|
1822
|
+
const basis = rows[1] || [];
|
|
1823
|
+
return headers.slice(0, 6).map((lab, i) => ({ value: row[i] != null ? row[i] : "", label: lab, ...(basis[i] ? { note: basis[i] } : {}) }));
|
|
1761
1824
|
}
|
|
1762
1825
|
|
|
1763
1826
|
/** Azure region dispatch. Returns true if it fully handled the region.
|
|
@@ -1775,6 +1838,7 @@ function renderAzureRegion(slide, layout, name, region, fontRoles, P, opts = {})
|
|
|
1775
1838
|
if (layout === "cover" && name === "title" && typeof content === "string" && content.includes("**")) {
|
|
1776
1839
|
const role = fontRoles.cover_title || {};
|
|
1777
1840
|
slide.addText(azureTitleRuns(content, P), {
|
|
1841
|
+
objectName: "title@legacy",
|
|
1778
1842
|
x: region.x || 0, y: region.y || 0, w: region.w || 8, h: region.h || 1,
|
|
1779
1843
|
fontFace: role.font || P.titleFont, fontSize: role.size || 48, bold: role.bold !== false,
|
|
1780
1844
|
align: role.align || "left", valign: "middle",
|
|
@@ -1930,7 +1994,7 @@ function renderPlacements(slide, placements, band, ctx) {
|
|
|
1930
1994
|
for (const placement of placements) {
|
|
1931
1995
|
if (placement.z !== band) continue;
|
|
1932
1996
|
if (placement.kind === "shape") {
|
|
1933
|
-
addDecorations(slide, [shapeToDecoration(placement, ctx.sourceDir)], ctx.
|
|
1997
|
+
addDecorations(slide, [shapeToDecoration(placement, ctx.sourceDir)], ctx.fontRoles);
|
|
1934
1998
|
} else {
|
|
1935
1999
|
renderPlacementBox(slide, placement, ctx);
|
|
1936
2000
|
}
|
|
@@ -1956,14 +2020,15 @@ function renderPlacements(slide, placements, band, ctx) {
|
|
|
1956
2020
|
*/
|
|
1957
2021
|
const NOTICE_FILL = "FDECEA";
|
|
1958
2022
|
const NOTICE_INK = "A21B12";
|
|
2023
|
+
let NOTICE = { ...LEGACY_TOKENS.notice };
|
|
1959
2024
|
|
|
1960
2025
|
function addNotice(slide, text, pageW, pageH, pal) {
|
|
1961
2026
|
const label = cleanMd(text);
|
|
1962
|
-
const fontSize =
|
|
2027
|
+
const fontSize = NOTICE.size;
|
|
1963
2028
|
const textW = (weightedTextLength(label) * fontSize * 0.98) / 72;
|
|
1964
2029
|
const w = Math.min(pageW * 0.55, textW + 0.46);
|
|
1965
|
-
const h =
|
|
1966
|
-
const x =
|
|
2030
|
+
const h = NOTICE.h;
|
|
2031
|
+
const x = NOTICE.x, y = pageH - h - NOTICE.bottom;
|
|
1967
2032
|
const fill = ((pal && pal.notice_fill) || NOTICE_FILL).replace("#", "");
|
|
1968
2033
|
const ink = ((pal && pal.notice_ink) || NOTICE_INK).replace("#", "");
|
|
1969
2034
|
slide.addShape("roundRect", { x, y, w, h, rectRadius: 0.15, fill: { color: fill }, line: { type: "none" }, objectName: "lit-notice tag" });
|
|
@@ -1975,7 +2040,34 @@ function addNotice(slide, text, pageW, pageH, pal) {
|
|
|
1975
2040
|
});
|
|
1976
2041
|
}
|
|
1977
2042
|
|
|
2043
|
+
/**
|
|
2044
|
+
* Give the first object drawn on a slide that has no name of its own the name `family@<id>`, so a
|
|
2045
|
+
* checker can read the layout family the source asked for and compare it with what it measures.
|
|
2046
|
+
*/
|
|
2047
|
+
function stampFamily(slide, family) {
|
|
2048
|
+
const at = { addText: 1, addShape: 1, addImage: 0, addTable: 1, addChart: 2 };
|
|
2049
|
+
const originals = {};
|
|
2050
|
+
let done = false;
|
|
2051
|
+
const restore = () => { for (const m of Object.keys(originals)) slide[m] = originals[m]; };
|
|
2052
|
+
for (const [method, index] of Object.entries(at)) {
|
|
2053
|
+
originals[method] = slide[method];
|
|
2054
|
+
slide[method] = (...args) => {
|
|
2055
|
+
if (!(args[index] && args[index].objectName)) {
|
|
2056
|
+
restore();
|
|
2057
|
+
done = true;
|
|
2058
|
+
args[index] = { ...(args[index] || {}), objectName: `family@${family}` };
|
|
2059
|
+
}
|
|
2060
|
+
return originals[method].apply(slide, args);
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
/** Whether a shape took the name; unwraps the slide either way. */
|
|
2064
|
+
return () => { restore(); return done; };
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1978
2067
|
async function render(resolved, templateObj, outputPath) {
|
|
2068
|
+
const pack = (templateObj && templateObj.pack) || null;
|
|
2069
|
+
if (pack && !pack.legacy) return require("./render-pack").renderPack(resolved, templateObj, outputPath);
|
|
2070
|
+
applyTokens(pack && pack.tokens);
|
|
1979
2071
|
const fontRoles = buildFontRoles(templateObj);
|
|
1980
2072
|
|
|
1981
2073
|
// Brand-neutralize: derive font/spacing/accent from the template, falling back
|
|
@@ -1987,15 +2079,14 @@ async function render(resolved, templateObj, outputPath) {
|
|
|
1987
2079
|
FONT_MEDIUM = tplFonts.body || "Pretendard";
|
|
1988
2080
|
FONT_LIGHT = tplFonts.light || "Pretendard";
|
|
1989
2081
|
CHAR_SPACING = tplType.char_spacing != null ? tplType.char_spacing : -0.7;
|
|
1990
|
-
BULLET_FONT = FONT_BOLD;
|
|
1991
2082
|
SEC_HEADER_FONT = FONT_BOLD;
|
|
1992
2083
|
TOC_FONT = FONT_BOLD;
|
|
1993
2084
|
BULLET_COLOR = (tplType.bullet_color || "1D4ED8").replace("#", "");
|
|
1994
2085
|
SECTION_ACCENT = (tplType.section_accent || "1D4ED8").replace("#", "");
|
|
1995
2086
|
|
|
1996
|
-
// Rich card/KPI/divider rendering — palette-driven,
|
|
1997
|
-
//
|
|
1998
|
-
const rich =
|
|
2087
|
+
// Rich card/KPI/divider rendering — palette-driven, for templates with the rich-blocks
|
|
2088
|
+
// capability. Tokens come from the template palette.
|
|
2089
|
+
const rich = Boolean(pack && (pack.capabilities || []).includes("rich-blocks"));
|
|
1999
2090
|
const pal = tpl.palette || {};
|
|
2000
2091
|
PLAIN_CHART_STYLE = {
|
|
2001
2092
|
series: [pal.ink || "#3C3836", pal.accent_green_2 || pal.primary || "#689D6A", pal.accent_amber || "#D79921", pal.accent_purple || "#B16286"],
|
|
@@ -2030,17 +2121,17 @@ async function render(resolved, templateObj, outputPath) {
|
|
|
2030
2121
|
|
|
2031
2122
|
for (const slideSpec of resolved.slides || []) {
|
|
2032
2123
|
const slide = pptx.addSlide();
|
|
2124
|
+
stampFamily(slide, slideSpec.family || slideSpec.layout);
|
|
2033
2125
|
|
|
2034
2126
|
// Draw order is a band at a time: template decorations, then anything the
|
|
2035
2127
|
// author put under the content, then the template's regions, then the
|
|
2036
2128
|
// columns that replaced the body, then anything sitting on top.
|
|
2037
|
-
addDecorations(slide, slideSpec.decorations,
|
|
2129
|
+
addDecorations(slide, slideSpec.decorations, fontRoles);
|
|
2038
2130
|
|
|
2039
2131
|
const placements = slideSpec.placements || [];
|
|
2040
2132
|
const placementCtx = {
|
|
2041
2133
|
rich, P, fontRoles, pageH, sourceDir,
|
|
2042
2134
|
layout: slideSpec.layout,
|
|
2043
|
-
slideIndex: slideSpec.index,
|
|
2044
2135
|
};
|
|
2045
2136
|
renderPlacements(slide, placements, "under", placementCtx);
|
|
2046
2137
|
|
|
@@ -2087,8 +2178,63 @@ async function render(resolved, templateObj, outputPath) {
|
|
|
2087
2178
|
if (notice) addNotice(slide, notice, pageW, pageH, pal);
|
|
2088
2179
|
}
|
|
2089
2180
|
|
|
2090
|
-
await pptx
|
|
2181
|
+
await writeTagged(pptx, outputPath);
|
|
2091
2182
|
return outputPath;
|
|
2092
2183
|
}
|
|
2093
2184
|
|
|
2094
|
-
|
|
2185
|
+
const HANGUL = /[\u1100-\u11FF\u3130-\u318F\uAC00-\uD7A3]/u;
|
|
2186
|
+
|
|
2187
|
+
/**
|
|
2188
|
+
* Korean text runs carry their language: pptxgenjs writes lang="en-US" on every run, and a renderer then
|
|
2189
|
+
* applies Latin rules to Hangul (LibreOffice on a host whose system locale is not Korean adds space
|
|
2190
|
+
* between Hangul and digits: "주 2일" set as "주 2 일"). A run that holds Hangul is ko-KR with en-US as its
|
|
2191
|
+
* alternate language; a chart that holds Hangul names ko-KR for its text. Latin-only runs keep en-US.
|
|
2192
|
+
*/
|
|
2193
|
+
function tagLanguages(name, xml) {
|
|
2194
|
+
if (/^ppt\/slides\/slide\d+\.xml$/u.test(name)) {
|
|
2195
|
+
return xml.replace(/<a:r><a:rPr\b([^>]*?)(\/?>)([\s\S]*?)<a:t>([^<]*)<\/a:t><\/a:r>/gu, (m, attrs, close, mid, text) => {
|
|
2196
|
+
if (!HANGUL.test(text)) return m;
|
|
2197
|
+
const rest = attrs.replace(/\s(?:lang|altLang)="[^"]*"/gu, "");
|
|
2198
|
+
return `<a:r><a:rPr lang="ko-KR" altLang="en-US"${rest}${close}${mid}<a:t>${text}</a:t></a:r>`;
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
if (/^ppt\/charts\/chart\d+\.xml$/u.test(name) && HANGUL.test(xml)) {
|
|
2202
|
+
let out = xml.replace(/<a:defRPr\b(?![^>]*\blang=)/gu, '<a:defRPr lang="ko-KR" altLang="en-US"')
|
|
2203
|
+
.replace(/<a:endParaRPr lang="en-US"/gu, '<a:endParaRPr lang="ko-KR" altLang="en-US"');
|
|
2204
|
+
if (!/<c:lang\b/u.test(out)) out = out.replace(/(<c:date1904 val="[01]"\/>)/u, '$1<c:lang val="ko-KR"/>');
|
|
2205
|
+
return out;
|
|
2206
|
+
}
|
|
2207
|
+
return xml;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/** Write a deck with its Korean runs and charts tagged (see tagLanguages); `edit` may rewrite a part first. */
|
|
2211
|
+
async function writeTagged(pptx, outputPath, edit = (name, xml) => xml) {
|
|
2212
|
+
const JSZip = runtimeRequire("jszip");
|
|
2213
|
+
const zip = await JSZip.loadAsync(await pptx.write({ outputType: "nodebuffer" }));
|
|
2214
|
+
for (const name of Object.keys(zip.files).filter((n) => /^ppt\/(slides\/slide|charts\/chart|theme\/theme)\d+\.xml$/u.test(n))) {
|
|
2215
|
+
const xml = await zip.file(name).async("string");
|
|
2216
|
+
zip.file(name, tagLanguages(name, edit(name, xml)));
|
|
2217
|
+
}
|
|
2218
|
+
fs.writeFileSync(outputPath, await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" }));
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// ── Tonality packs ──────────────────────────────────────────────────────────
|
|
2222
|
+
//
|
|
2223
|
+
// A pack deck is drawn by render-pack.js. It reuses the helpers below, and the font globals the
|
|
2224
|
+
// free placement boxes read are switched to the pack's faces before it draws.
|
|
2225
|
+
|
|
2226
|
+
function usePackFonts(titleFont, bodyFont, tokens) {
|
|
2227
|
+
FONT_BOLD = titleFont;
|
|
2228
|
+
FONT_MEDIUM = bodyFont;
|
|
2229
|
+
FONT_LIGHT = bodyFont;
|
|
2230
|
+
CHAR_SPACING = 0;
|
|
2231
|
+
applyTokens(tokens);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
module.exports = {
|
|
2235
|
+
render,
|
|
2236
|
+
shared: {
|
|
2237
|
+
hex, cleanMd, mdRuns, linkedTableCell, resolveImagePath, readImageSize, containBox,
|
|
2238
|
+
kpiCards, numericColumns, proportionalWidths, renderChart, stampFamily, renderPlacements, usePackFonts, writeTagged,
|
|
2239
|
+
},
|
|
2240
|
+
};
|