@taloswork/ose-shared 0.0.4 → 0.1.0
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/mcp/.gitkeep +1 -1
- package/mcp/gen/mcp.json +2 -2
- package/package.json +3 -3
- package/skills/.gitkeep +1 -1
- package/skills/document/SKILL.md +7 -1
- package/skills/document/bin/deck-build.cjs +257 -23
- package/skills/document/bin/doc-gen.cjs +17 -1
- package/skills/document/bin/doc-scan.cjs +131 -18
- package/skills/document/bin/doc-text.cjs +146 -10
- package/skills/document/bin/html2pptx.cjs +17 -1
- package/skills/document/deck-design.md +5 -1
- package/skills/document/layouts/README.md +43 -10
- package/skills/document/layouts/brand-values.tpl +6 -8
- package/skills/document/layouts/color-palette.tpl +6 -10
- package/skills/document/layouts/gantt.tpl +4 -4
- package/skills/document/layouts/logo-guidelines.tpl +8 -8
- package/skills/document/layouts/map-distribution.tpl +5 -5
- package/skills/document/layouts/map-quad.tpl +7 -7
- package/skills/document/layouts/radar.tpl +4 -4
- package/skills/document/layouts/ring-progress.tpl +4 -4
- package/skills/document/layouts/slots.json +1 -763
- package/skills/document/layouts/steps-vertical.tpl +4 -4
- package/skills/document/layouts/team-row.tpl +5 -11
- package/skills/document/layouts/toc-panel.tpl +4 -7
- package/skills/document/layouts/type-specimen.tpl +9 -9
- package/skills/document/styles/anthropic/skin.css +1 -4
- package/skills/document/styles/apple/skin.css +1 -4
- package/skills/document/styles/black-gold/skin.css +1 -4
- package/skills/document/styles/branding/skin.css +1 -4
- package/skills/document/styles/cinereel/skin.css +1 -4
- package/skills/document/styles/cool/skin.css +1 -4
- package/skills/document/styles/hand-drawn/skin.css +1 -4
- package/skills/document/styles/ivory-gold/skin.css +1 -4
- package/skills/document/styles/magazine-ink/skin.css +0 -3
- package/skills/document/styles/mckinsey/skin.css +1 -4
- package/skills/document/styles/mono/skin.css +1 -4
- package/skills/document/styles/olive-gold/skin.css +1 -4
- package/skills/document/styles/swiss-grid/skin.css +0 -3
package/mcp/.gitkeep
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Each shared MCP package lives at `mcp/<pkg>/mcp.json` — the client extracts
|
|
4
4
|
# this directory tree into `~/.talos/bundled/shared/mcp/` and every OSE
|
|
5
|
-
# auto-merges the servers in without opt-in (P12b / docs/
|
|
5
|
+
# auto-merges the servers in without opt-in (P12b / docs/02-ose-and-prompt.md). The mcp.json
|
|
6
6
|
# shape matches per-OSE `bundled/<id>/mcp.json`: `{ version, servers: { ... } }`.
|
|
7
7
|
#
|
|
8
8
|
# Delete this file once at least one real shared MCP package lands here.
|
package/mcp/gen/mcp.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
],
|
|
10
10
|
"env": {
|
|
11
11
|
"ELECTRON_RUN_AS_NODE": "1",
|
|
12
|
-
"GENERATION_VENDOR": "talos",
|
|
13
12
|
"TALOS_API_KEY": "${TALOS_GATEWAY_API_KEY}",
|
|
14
13
|
"TALOS_GATEWAY_BASE_URL": "${TALOS_GATEWAY_BASE_URL}"
|
|
15
14
|
},
|
|
@@ -17,9 +16,10 @@
|
|
|
17
16
|
"connectTimeoutMs": 30000,
|
|
18
17
|
"supportsParallelToolCalls": false,
|
|
19
18
|
"metadata": {
|
|
19
|
+
"provides": "talos/gen",
|
|
20
20
|
"title": "文生图/视频 · Talos 计费",
|
|
21
21
|
"source": "talos",
|
|
22
|
-
"note": "无需厂商 API Key——登录 Talos 账号即可,按张/按秒扣积分(走 Talos-Backend gateway)
|
|
22
|
+
"note": "无需厂商 API Key——登录 Talos 账号即可,按张/按秒扣积分(走 Talos-Backend gateway)。模型目录由 gateway GET /v1/media/models 下发。"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taloswork/ose-shared",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Talos cross-OSE shared library — skills + MCP servers auto-loaded into every OSE (P93 2026-07-27).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
|
-
"_talosPublishHash": "sha256-
|
|
14
|
-
"_talosPublishedAt": "2026-08-
|
|
13
|
+
"_talosPublishHash": "sha256-_TBER93S6Gcnoc6VaJyOr1oERYK8cA1G_ifT0tdC3ZQ",
|
|
14
|
+
"_talosPublishedAt": "2026-08-27T14:52:48.964Z"
|
|
15
15
|
}
|
package/skills/.gitkeep
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Each shared skill lives at `skills/<name>/SKILL.md` — the client extracts
|
|
4
4
|
# this directory tree into `~/.talos/bundled/shared/skills/` and every OSE
|
|
5
|
-
# auto-loads the skills without opt-in (P12b / docs/
|
|
5
|
+
# auto-loads the skills without opt-in (P12b / docs/02-ose-and-prompt.md).
|
|
6
6
|
#
|
|
7
7
|
# Delete this file once at least one real shared skill lands here.
|
package/skills/document/SKILL.md
CHANGED
|
@@ -119,6 +119,12 @@ script only when:
|
|
|
119
119
|
(leave color to the skin; `html2pptx` warns on hardcoded colors). 8 roles:
|
|
120
120
|
cover/title/toc/section/transition/content/ending + special charts
|
|
121
121
|
(gantt/radar/funnel/ring/org-tree/map/data-table/stats-chart).
|
|
122
|
+
**A layout with a `{{#rows}}` block (gantt · radar · ring-progress ·
|
|
123
|
+
map-distribution · team-row · brand-values · color-palette · steps-vertical ·
|
|
124
|
+
toc-panel) must go through `deck-build`** — it repeats the block per row AND
|
|
125
|
+
computes the geometry (bar spans, ring arcs, pin positions, the radar chart)
|
|
126
|
+
from your numbers. Hand-pasting one leaves `{{#…}}` markers in the deck, or a
|
|
127
|
+
drawing that contradicts its own labels.
|
|
122
128
|
3. **Render:** `bin/html2pptx.cjs deck.html out.pptx --style <skin> [--backdrop
|
|
123
129
|
<variant>]` — DEFAULT hybrid (no `--editable`/`--rasterize`): pixel-perfect
|
|
124
130
|
visuals baked into a backdrop + native EDITABLE text and native `data-chart`
|
|
@@ -212,7 +218,7 @@ Fidelity-decision self-correct, but pointed at your OWN output:
|
|
|
212
218
|
- `bin/xlsx-recalc.cjs <in.xlsx> <out.xlsx>` — re-evaluate all formulas (HyperFormula) and cache the results, keeping the formulas. Use when a spreadsheet's formula cells show stale/blank values.
|
|
213
219
|
- `bin/doc-run.cjs <script.js> [args…]` — run a custom Node script with the engine's libraries bundled in (`require("pptxgenjs"|"docx"|"xlsx"|"pdf-lib"|"jszip"|"pizzip"|"hyperformula")` guaranteed on any machine; `--list` prints the registry). The long-tail escape hatch when no verb fits — see rewrite.md "Custom scripts"; ALWAYS doc-validate the output.
|
|
214
220
|
- `bin/html2pptx.cjs <deck.html|snapshot.layout.json> <out.pptx> [--style <skin>] [--backdrop <variant>] [--theme <theme.json>] [--editable] [--rasterize]` — **THE deck path**: write slide HTML (`<section class="slide" data-layout="…">` on a 1280×720 canvas), Chromium lays it out → NATIVE editable pptx. **Compose from `layouts/*.tpl` (structure) + `--style <skin>` (paint)** — the preferred structure/skin split (`styles/README.md`); `--style` injects the skin's CSS, fills each `.slide-backdrop`, colors `data-chart` from the skin. `--backdrop <variant>` picks a signature backdrop. **DEFAULT = hybrid**: each slide's visuals bake into a full-fidelity raster backdrop with native EDITABLE text on top; auto-falls back to plain native if backdrop capture is unavailable. `--editable` = plain native only (effects flatten). `--rasterize` = full image slides (ZERO editability — tell the user). Advisory warnings on hardcoded brand colors + repeated layouts (P3). Runs in the app's Chromium OR bundled headless Chromium (Playwright) — works off-app (CI/cloud); layout JSON works anywhere. **Tailwind + `size="1".."7"` scale available** (vendored offline). See deck-design.md + layouts/README.md. `[--theme <theme.json>]` is the legacy path (superseded by `--style`). **Don't confuse the two `--style` flags:** html2pptx's `--style` = the whole deck **skin** (backgrounds, cards, the lot); doc-gen's `--style` on **docx/pdf** is a different, narrower flag that borrows only the skin's ONE accent color onto black-text paper (a document, not a deck).
|
|
215
|
-
- `bin/deck-build.cjs <spec.json> <deck.html>` — **THE FAST deck path** for catalog slides: emit a COMPACT spec (per slide a `layout` id + its content `slots` `{…}`
|
|
221
|
+
- `bin/deck-build.cjs <spec.json> <deck.html>` — **THE FAST deck path** for catalog slides: emit a COMPACT spec (per slide a `layout` id + its content `slots` `{…}`, plus `rows` for list/figure layouts and an optional `chart`) and this reads each `layouts/<id>.tpl`, fills it, and assembles `deck.html` — so you stream ~content only, not ~40KB of boilerplate HTML (much faster). Then `html2pptx deck.html out.pptx --style <spec.style>` as usual. **What each layout takes is in `layouts/slots.json`** (`slots` + `rows` per layout; `"chart":"derived"` means the layout BUILDS its chart from its rows — pass rows, never `chart`). Un-filled slots fall back to the template default, but a spec the catalog can't satisfy (unknown layout/slot/field, a `rows` block with no data, a figure row missing its numbers) **exits 1 and writes nothing** — fix the spec rather than shipping demo data. For a **bespoke** slide the catalog can't express, hand-author that ONE `<section>` and append it to deck.html (deck-build handles the catalog slides). spec: `{ "style":"cool", "slides":[ {"layout":"cover","slots":{"title":"…"}}, {"layout":"chart-kpi","slots":{…},"chart":{…}}, {"layout":"gantt","slots":{…},"rows":{"cols":[{"label":"Q1"}],"tasks":[{"name":"调研","from":1,"to":2}]}} ] }`.
|
|
216
222
|
|
|
217
223
|
All scripts are Node and print a machine-readable result plus a human note. On a
|
|
218
224
|
missing dependency or unreadable file they exit non-zero with a clear message —
|
|
@@ -6,56 +6,285 @@ var import_promises = require("node:fs/promises");
|
|
|
6
6
|
var import_node_fs = require("node:fs");
|
|
7
7
|
var import_node_path = require("node:path");
|
|
8
8
|
|
|
9
|
+
// packages/core/dist/fs/deck-figures.js
|
|
10
|
+
var FIGURE_SPECS = {
|
|
11
|
+
radar: {
|
|
12
|
+
metrics: { required: ["name", "score"], derived: [] }
|
|
13
|
+
},
|
|
14
|
+
gantt: {
|
|
15
|
+
cols: { required: ["label"], derived: [] },
|
|
16
|
+
tasks: { required: ["name", "from", "to"], derived: ["_left", "_width"] }
|
|
17
|
+
},
|
|
18
|
+
"ring-progress": {
|
|
19
|
+
rings: { required: ["label", "pct"], derived: ["_pct", "_dashoffset"] }
|
|
20
|
+
},
|
|
21
|
+
"map-distribution": {
|
|
22
|
+
regions: { required: ["name", "pct"], derived: [] },
|
|
23
|
+
pins: { required: ["x", "y"], derived: ["_left", "_top"] }
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var DERIVED_CHART_LAYOUTS = /* @__PURE__ */ new Set(["radar"]);
|
|
27
|
+
var RING_CIRCUMFERENCE = 339.3;
|
|
28
|
+
var SLIDE_W = 1280;
|
|
29
|
+
var SLIDE_H = 720;
|
|
30
|
+
var PIN_SIZE = 16;
|
|
31
|
+
var isFigureLayout = (layout) => Object.hasOwn(FIGURE_SPECS, layout);
|
|
32
|
+
function num(v) {
|
|
33
|
+
if (typeof v === "number")
|
|
34
|
+
return Number.isFinite(v) ? v : null;
|
|
35
|
+
if (typeof v !== "string")
|
|
36
|
+
return null;
|
|
37
|
+
const n = Number(v.trim().replace(/%$/, ""));
|
|
38
|
+
return Number.isFinite(n) ? n : null;
|
|
39
|
+
}
|
|
40
|
+
var fmt = (n) => String(Math.round(n * 1e4) / 1e4);
|
|
41
|
+
function clamp(n, lo, hi) {
|
|
42
|
+
return n < lo ? lo : n > hi ? hi : n;
|
|
43
|
+
}
|
|
44
|
+
function normalizeRow(raw) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
47
|
+
if (v === void 0 || v === null)
|
|
48
|
+
continue;
|
|
49
|
+
out[k] = String(v);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function prepareRows(layout, rows) {
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const errors = [];
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [key, value] of Object.entries(rows ?? {})) {
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
errors.push(`${layout}: rows.${key} must be an array of objects`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const normalized = [];
|
|
63
|
+
value.forEach((raw, i) => {
|
|
64
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
65
|
+
errors.push(`${layout}: rows.${key}[${i}] must be an object`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
normalized.push(normalizeRow(raw));
|
|
69
|
+
});
|
|
70
|
+
out[key] = normalized;
|
|
71
|
+
}
|
|
72
|
+
if (!isFigureLayout(layout))
|
|
73
|
+
return { rows: out, warnings, errors };
|
|
74
|
+
const spec = FIGURE_SPECS[layout];
|
|
75
|
+
for (const key of Object.keys(spec)) {
|
|
76
|
+
if (!out[key]?.length)
|
|
77
|
+
errors.push(`${layout}: rows.${key} is required (${spec[key].required.join(", ")})`);
|
|
78
|
+
}
|
|
79
|
+
for (const [key, fieldSpec] of Object.entries(spec)) {
|
|
80
|
+
(out[key] ?? []).forEach((row, i) => {
|
|
81
|
+
for (const f of fieldSpec.required) {
|
|
82
|
+
if (row[f] === void 0 || row[f] === "")
|
|
83
|
+
errors.push(`${layout}: rows.${key}[${i}] is missing "${f}"`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (errors.length)
|
|
88
|
+
return { rows: out, warnings, errors };
|
|
89
|
+
if (layout === "radar")
|
|
90
|
+
return { rows: out, chart: radarChart(out.metrics, warnings), warnings, errors };
|
|
91
|
+
if (layout === "gantt")
|
|
92
|
+
deriveGantt(out, warnings, errors);
|
|
93
|
+
else if (layout === "ring-progress")
|
|
94
|
+
deriveRings(out, warnings);
|
|
95
|
+
else if (layout === "map-distribution")
|
|
96
|
+
derivePins(out, warnings);
|
|
97
|
+
return { rows: out, warnings, errors };
|
|
98
|
+
}
|
|
99
|
+
function radarChart(metrics, warnings) {
|
|
100
|
+
const labels = [];
|
|
101
|
+
const values = [];
|
|
102
|
+
metrics.forEach((m, i) => {
|
|
103
|
+
const score = num(m.score);
|
|
104
|
+
if (score === null)
|
|
105
|
+
warnings.push(`radar: rows.metrics[${i}] "score" is not a number \u2014 plotted as 0`);
|
|
106
|
+
labels.push(m.name);
|
|
107
|
+
values.push(score ?? 0);
|
|
108
|
+
});
|
|
109
|
+
return { type: "radar", labels, series: [{ name: "", values }], colors: ["__ACCENT__"], axisColor: "__AXIS__" };
|
|
110
|
+
}
|
|
111
|
+
function deriveGantt(rows, warnings, errors) {
|
|
112
|
+
const total = rows.cols.length;
|
|
113
|
+
rows.tasks.forEach((task, i) => {
|
|
114
|
+
const from = num(task.from);
|
|
115
|
+
const to = num(task.to);
|
|
116
|
+
if (from === null || to === null) {
|
|
117
|
+
errors.push(`gantt: rows.tasks[${i}] "from"/"to" must be column numbers 1..${total}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let a = Math.round(from);
|
|
121
|
+
let b = Math.round(to);
|
|
122
|
+
if (a < 1 || b > total || a > b) {
|
|
123
|
+
warnings.push(`gantt: rows.tasks[${i}] spans ${a}..${b}, clamped to 1..${total}`);
|
|
124
|
+
a = clamp(a, 1, total);
|
|
125
|
+
b = clamp(b, a, total);
|
|
126
|
+
}
|
|
127
|
+
task._left = fmt((a - 1) / total * 100);
|
|
128
|
+
task._width = fmt((b - a + 1) / total * 100);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function deriveRings(rows, warnings) {
|
|
132
|
+
rows.rings.forEach((ring, i) => {
|
|
133
|
+
let pct = num(ring.pct);
|
|
134
|
+
if (pct === null) {
|
|
135
|
+
warnings.push(`ring-progress: rows.rings[${i}] "pct" is not a number \u2014 drawn as 0%`);
|
|
136
|
+
pct = 0;
|
|
137
|
+
} else if (pct < 0 || pct > 100) {
|
|
138
|
+
warnings.push(`ring-progress: rows.rings[${i}] "pct" ${pct} is outside 0..100 \u2014 clamped`);
|
|
139
|
+
pct = clamp(pct, 0, 100);
|
|
140
|
+
}
|
|
141
|
+
ring._pct = `${fmt(pct)}%`;
|
|
142
|
+
ring._dashoffset = fmt(RING_CIRCUMFERENCE * (1 - pct / 100));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function derivePins(rows, warnings) {
|
|
146
|
+
rows.pins.forEach((pin, i) => {
|
|
147
|
+
const x = num(pin.x);
|
|
148
|
+
const y = num(pin.y);
|
|
149
|
+
if (x === null || y === null) {
|
|
150
|
+
warnings.push(`map-distribution: rows.pins[${i}] "x"/"y" must be slide px \u2014 pin dropped to the origin`);
|
|
151
|
+
}
|
|
152
|
+
const cx = clamp(x ?? 0, 0, SLIDE_W);
|
|
153
|
+
const cy = clamp(y ?? 0, 0, SLIDE_H);
|
|
154
|
+
if (x !== null && y !== null && (x !== cx || y !== cy)) {
|
|
155
|
+
warnings.push(`map-distribution: rows.pins[${i}] (${x}, ${y}) is off-canvas \u2014 clamped to the slide`);
|
|
156
|
+
}
|
|
157
|
+
pin._left = fmt(cx - PIN_SIZE / 2);
|
|
158
|
+
pin._top = fmt(cy - PIN_SIZE / 2);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
9
162
|
// packages/core/dist/fs/deck-build.js
|
|
10
163
|
var SLOT_RE = /\{\{(?![#/])([^{}]*)\}\}/g;
|
|
164
|
+
var BLOCK_RE = /\{\{#([A-Za-z_][\w-]*)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
|
|
11
165
|
function escapeHtml(s) {
|
|
12
166
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13
167
|
}
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
168
|
+
var slotName = (inner) => {
|
|
169
|
+
const bar = inner.indexOf("|");
|
|
170
|
+
return (bar >= 0 ? inner.slice(0, bar) : inner).trim();
|
|
171
|
+
};
|
|
172
|
+
var withoutComments = (tpl) => tpl.replace(/<!--[\s\S]*?-->/g, "");
|
|
173
|
+
var withoutBlocks = (tpl) => tpl.replace(BLOCK_RE, "");
|
|
174
|
+
function templateSlots(tpl) {
|
|
175
|
+
const names = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const m of withoutBlocks(withoutComments(tpl)).matchAll(SLOT_RE))
|
|
177
|
+
names.add(slotName(m[1]));
|
|
178
|
+
return [...names];
|
|
179
|
+
}
|
|
180
|
+
function fillSlots(tpl, values) {
|
|
181
|
+
return tpl.replace(SLOT_RE, (_full, inner) => {
|
|
17
182
|
const bar = inner.indexOf("|");
|
|
18
|
-
const name = (
|
|
183
|
+
const name = slotName(inner);
|
|
19
184
|
const def = bar >= 0 ? inner.slice(bar + 1) : "";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
185
|
+
const v = values[name];
|
|
186
|
+
if (v === void 0)
|
|
187
|
+
return def;
|
|
188
|
+
return name.startsWith("_") ? v : escapeHtml(v);
|
|
23
189
|
});
|
|
190
|
+
}
|
|
191
|
+
function fillLayout(tpl, slots, layout, problems) {
|
|
192
|
+
const declared = new Set(templateSlots(tpl));
|
|
193
|
+
const authorable = [...declared].filter((s) => !s.startsWith("_"));
|
|
24
194
|
for (const k of Object.keys(slots)) {
|
|
25
|
-
if (
|
|
26
|
-
|
|
195
|
+
if (k.startsWith("_"))
|
|
196
|
+
problems.errors.push(`${layout}: "${k}" is a derived slot \u2014 it is computed from rows, not authored`);
|
|
197
|
+
else if (!declared.has(k)) {
|
|
198
|
+
problems.errors.push(`${layout}: unknown slot "${k}" \u2014 declared slots: ${authorable.join(", ") || "(none)"}`);
|
|
199
|
+
}
|
|
27
200
|
}
|
|
28
|
-
return
|
|
201
|
+
return fillSlots(tpl, slots);
|
|
29
202
|
}
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
203
|
+
function expandRepeats(tpl, rows, layout, problems, figureInputs = {}) {
|
|
204
|
+
const seen = /* @__PURE__ */ new Set();
|
|
205
|
+
const html = tpl.replace(BLOCK_RE, (_full, key, body) => {
|
|
206
|
+
seen.add(key);
|
|
207
|
+
const items = rows[key];
|
|
208
|
+
if (!items?.length) {
|
|
209
|
+
problems.errors.push(`${layout}: {{#${key}}} needs rows.${key} \u2014 without it the block renders empty`);
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
const declared = /* @__PURE__ */ new Set([...templateSlots(body), ...figureInputs[key]?.required ?? []]);
|
|
213
|
+
const authorable = [...declared].filter((f) => !f.startsWith("_"));
|
|
214
|
+
return items.map((row, i) => {
|
|
215
|
+
for (const k of Object.keys(row)) {
|
|
216
|
+
if (!declared.has(k)) {
|
|
217
|
+
problems.errors.push(`${layout}: rows.${key}[${i}] has unknown field "${k}" \u2014 fields: ${authorable.join(", ") || "(none)"}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const n = i + 1;
|
|
221
|
+
return fillSlots(body, { ...row, _n: String(n), _nn: String(n).padStart(2, "0") });
|
|
222
|
+
}).join("");
|
|
223
|
+
});
|
|
224
|
+
for (const key of Object.keys(rows)) {
|
|
225
|
+
if (!seen.has(key))
|
|
226
|
+
problems.errors.push(`${layout}: rows.${key} has no {{#${key}}} block in this layout`);
|
|
227
|
+
}
|
|
228
|
+
return html;
|
|
229
|
+
}
|
|
230
|
+
function injectCharts(html, chart, layout, problems) {
|
|
231
|
+
const specs = Array.isArray(chart) ? chart : [chart];
|
|
232
|
+
let i = 0;
|
|
233
|
+
const out = html.replace(/data-chart=(['"])[\s\S]*?\1/g, (full) => {
|
|
234
|
+
const spec = specs[i++];
|
|
235
|
+
if (spec === void 0)
|
|
236
|
+
return full;
|
|
237
|
+
return `data-chart='${JSON.stringify(spec).replace(/'/g, "'")}'`;
|
|
238
|
+
});
|
|
239
|
+
if (i === 0)
|
|
240
|
+
problems.errors.push(`${layout}: a chart was supplied but this layout has no data-chart`);
|
|
241
|
+
else if (specs.length > i)
|
|
242
|
+
problems.errors.push(`${layout}: ${specs.length} charts supplied but the layout has ${i}`);
|
|
243
|
+
else if (specs.length < i)
|
|
244
|
+
problems.warnings.push(`${layout}: ${i} charts in the layout, only ${specs.length} supplied \u2014 the rest keep their demo data`);
|
|
245
|
+
return out;
|
|
33
246
|
}
|
|
34
247
|
function stripLeadComment(tpl) {
|
|
35
248
|
return tpl.replace(/^\s*<!--[\s\S]*?-->\s*/, "");
|
|
36
249
|
}
|
|
37
250
|
function assembleDeck(spec, readLayout) {
|
|
38
|
-
const
|
|
251
|
+
const problems = { warnings: [], errors: [] };
|
|
39
252
|
const sections = [];
|
|
40
253
|
const slides = Array.isArray(spec?.slides) ? spec.slides : [];
|
|
41
254
|
if (!slides.length)
|
|
42
|
-
|
|
255
|
+
problems.errors.push("spec has no slides");
|
|
43
256
|
slides.forEach((slide, i) => {
|
|
257
|
+
const at = `slide ${i + 1}`;
|
|
44
258
|
if (!slide || typeof slide.layout !== "string" || !slide.layout) {
|
|
45
|
-
|
|
259
|
+
problems.errors.push(`${at}: missing "layout"`);
|
|
46
260
|
return;
|
|
47
261
|
}
|
|
48
262
|
const raw = readLayout(slide.layout);
|
|
49
263
|
if (raw == null) {
|
|
50
|
-
|
|
264
|
+
problems.errors.push(`${at}: unknown layout "${slide.layout}"`);
|
|
51
265
|
return;
|
|
52
266
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
267
|
+
const local = { warnings: [], errors: [] };
|
|
268
|
+
const where = `${at} (${slide.layout})`;
|
|
269
|
+
const figures = prepareRows(slide.layout, slide.rows);
|
|
270
|
+
local.warnings.push(...figures.warnings.map((m) => `${at}: ${m}`));
|
|
271
|
+
local.errors.push(...figures.errors.map((m) => `${at}: ${m}`));
|
|
272
|
+
let html = stripLeadComment(raw);
|
|
273
|
+
if (!local.errors.length)
|
|
274
|
+
html = expandRepeats(html, figures.rows, where, local, FIGURE_SPECS[slide.layout] ?? {});
|
|
275
|
+
html = fillLayout(html, slide.slots ?? {}, where, local);
|
|
276
|
+
if (slide.chart !== void 0 && DERIVED_CHART_LAYOUTS.has(slide.layout)) {
|
|
277
|
+
local.errors.push(`${where}: this layout builds its chart from rows \u2014 drop "chart" from the spec`);
|
|
278
|
+
}
|
|
279
|
+
const chart = figures.chart ?? slide.chart;
|
|
280
|
+
if (chart !== void 0 && !local.errors.length)
|
|
281
|
+
html = injectCharts(html, chart, where, local);
|
|
282
|
+
problems.warnings.push(...local.warnings);
|
|
283
|
+
problems.errors.push(...local.errors);
|
|
284
|
+
if (!local.errors.length)
|
|
285
|
+
sections.push(html.trim());
|
|
57
286
|
});
|
|
58
|
-
return { html: sections.join("\n"), warnings };
|
|
287
|
+
return { html: sections.join("\n"), warnings: problems.warnings, errors: problems.errors };
|
|
59
288
|
}
|
|
60
289
|
|
|
61
290
|
// apps/admin/data/skills.lib/document/bin-src/deck-build.mjs
|
|
@@ -80,9 +309,14 @@ async function main() {
|
|
|
80
309
|
const p = (0, import_node_path.join)(layoutsDir, `${id}.tpl`);
|
|
81
310
|
return (0, import_node_fs.existsSync)(p) ? (0, import_node_fs.readFileSync)(p, "utf8") : null;
|
|
82
311
|
};
|
|
83
|
-
const { html, warnings } = assembleDeck(spec, readLayout);
|
|
312
|
+
const { html, warnings, errors } = assembleDeck(spec, readLayout);
|
|
84
313
|
for (const w of warnings) process.stderr.write(`deck-build: ${w}
|
|
85
314
|
`);
|
|
315
|
+
if (errors.length) {
|
|
316
|
+
for (const e of errors) process.stderr.write(`deck-build: ERROR ${e}
|
|
317
|
+
`);
|
|
318
|
+
die(`${errors.length} spec error(s) \u2014 nothing written (see layouts/slots.json for each layout's slots + rows)`, 1);
|
|
319
|
+
}
|
|
86
320
|
await (0, import_promises.writeFile)(outPath, html, "utf8");
|
|
87
321
|
const style = typeof spec?.style === "string" ? spec.style : null;
|
|
88
322
|
process.stdout.write(JSON.stringify({
|
|
@@ -154534,13 +154534,26 @@ async function convertLayoutToPptx(snapshot) {
|
|
|
154534
154534
|
warnings.push(`${where} (chart): needs labels + a series with values \u2014 dropped`);
|
|
154535
154535
|
break;
|
|
154536
154536
|
}
|
|
154537
|
+
const points = series.map((s, si) => {
|
|
154538
|
+
const name = s.name ?? "";
|
|
154539
|
+
const raw = s.values;
|
|
154540
|
+
const at = `${where} (chart): series ${si + 1}${name ? ` "${name}"` : ""}`;
|
|
154541
|
+
if (raw.length !== labels.length) {
|
|
154542
|
+
warnings.push(`${at} has ${raw.length} value(s) for ${labels.length} label(s) \u2014 ${raw.length < labels.length ? "the trailing categories will be blank" : "the extra values are ignored"}`);
|
|
154543
|
+
}
|
|
154544
|
+
const values = raw.map(Number);
|
|
154545
|
+
const bad = values.filter((v) => !Number.isFinite(v)).length;
|
|
154546
|
+
if (bad)
|
|
154547
|
+
warnings.push(`${at} has ${bad} non-numeric value(s) \u2014 plotted as 0`);
|
|
154548
|
+
return { name, labels, values: values.map((v) => Number.isFinite(v) ? v : 0) };
|
|
154549
|
+
});
|
|
154537
154550
|
const horizontal = spec.type === "barH" || spec.horizontal === true;
|
|
154538
154551
|
const rawType = spec.type === "barH" ? "bar" : spec.type;
|
|
154539
154552
|
const type = ["bar", "line", "pie", "doughnut", "area", "radar", "scatter"].includes(rawType) ? rawType : "bar";
|
|
154540
154553
|
const ramp = (Array.isArray(spec.colors) && spec.colors.length ? spec.colors : DEFAULT_CHART_RAMP).map((c) => cssColorToHex(c) ?? String(c).replace("#", "")).filter(Boolean);
|
|
154541
154554
|
const axis = cssColorToHex(spec.axisColor) ?? "44474F";
|
|
154542
154555
|
const multi = series.length > 1;
|
|
154543
|
-
slide.addChart(type,
|
|
154556
|
+
slide.addChart(type, points, {
|
|
154544
154557
|
x: frame.x,
|
|
154545
154558
|
y: frame.y,
|
|
154546
154559
|
w: frame.w,
|
|
@@ -154554,6 +154567,9 @@ async function convertLayoutToPptx(snapshot) {
|
|
|
154554
154567
|
valAxisLineColor: "C9D0DA",
|
|
154555
154568
|
valGridLine: { color: "E2E8F0", style: "dash", size: 0.5 },
|
|
154556
154569
|
...horizontal ? { barDir: "bar" } : {},
|
|
154570
|
+
// `showValues: true` in the data-chart prints each point's number on
|
|
154571
|
+
// the mark (opt-in: on a dense series the labels collide).
|
|
154572
|
+
...spec.showValues === true ? { showValue: true } : {},
|
|
154557
154573
|
...multi || type === "pie" || type === "doughnut" ? { showLegend: true, legendPos: "b" } : {}
|
|
154558
154574
|
});
|
|
154559
154575
|
recordShape(el);
|
|
@@ -47,27 +47,140 @@ async function openDoc(bytes) {
|
|
|
47
47
|
}
|
|
48
48
|
return { mupdf, doc };
|
|
49
49
|
}
|
|
50
|
-
async function
|
|
50
|
+
async function analyzePdfDocument(bytes, options = {}) {
|
|
51
|
+
const wantText = options.text !== false;
|
|
52
|
+
const wantSignals = options.signals === true;
|
|
51
53
|
const { doc } = await openDoc(bytes);
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const pageCount = doc.countPages();
|
|
55
|
+
const signals = [];
|
|
56
|
+
let out = "";
|
|
57
|
+
for (let i = 0; i < pageCount; i++) {
|
|
55
58
|
const page = doc.loadPage(i);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
if (wantText) {
|
|
60
|
+
out += `
|
|
61
|
+
[page ${i + 1}]
|
|
62
|
+
${page.toStructuredText().asText()}`;
|
|
63
|
+
}
|
|
64
|
+
if (wantSignals) {
|
|
65
|
+
signals.push(pageSignalsFrom(page, i + 1));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { text: wantText ? out.trim() : "", pageCount, signals };
|
|
69
|
+
}
|
|
70
|
+
function countVectorOperators(content) {
|
|
71
|
+
let ops = 0;
|
|
72
|
+
let curves = 0;
|
|
73
|
+
let fills = 0;
|
|
74
|
+
let i = 0;
|
|
75
|
+
const n = content.length;
|
|
76
|
+
while (i < n) {
|
|
77
|
+
const ch = content[i];
|
|
78
|
+
if (ch === "(") {
|
|
79
|
+
let depth = 1;
|
|
80
|
+
i++;
|
|
81
|
+
while (i < n && depth > 0) {
|
|
82
|
+
if (content[i] === "\\")
|
|
83
|
+
i++;
|
|
84
|
+
else if (content[i] === "(")
|
|
85
|
+
depth++;
|
|
86
|
+
else if (content[i] === ")")
|
|
87
|
+
depth--;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (ch === "<" && content[i + 1] !== "<") {
|
|
93
|
+
while (i < n && content[i] !== ">")
|
|
94
|
+
i++;
|
|
95
|
+
i++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "I" && content[i + 1] === "D" && isDelimiter(content[i + 2])) {
|
|
99
|
+
const end = content.indexOf("EI", i + 2);
|
|
100
|
+
i = end === -1 ? n : end + 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!isDelimiter(ch)) {
|
|
104
|
+
const start = i;
|
|
105
|
+
while (i < n && !isDelimiter(content[i]))
|
|
106
|
+
i++;
|
|
107
|
+
const token = content.slice(start, i);
|
|
108
|
+
switch (token) {
|
|
109
|
+
case "m":
|
|
110
|
+
case "l":
|
|
111
|
+
case "re":
|
|
112
|
+
ops++;
|
|
113
|
+
break;
|
|
114
|
+
case "c":
|
|
115
|
+
case "v":
|
|
116
|
+
case "y":
|
|
117
|
+
ops++;
|
|
118
|
+
curves++;
|
|
119
|
+
break;
|
|
120
|
+
case "f":
|
|
121
|
+
case "F":
|
|
122
|
+
case "f*":
|
|
123
|
+
fills++;
|
|
124
|
+
break;
|
|
125
|
+
case "B":
|
|
126
|
+
case "B*":
|
|
127
|
+
case "b":
|
|
128
|
+
case "b*":
|
|
129
|
+
fills++;
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
return { ops, curves, fills };
|
|
139
|
+
}
|
|
140
|
+
function isDelimiter(ch) {
|
|
141
|
+
return ch === void 0 || ch === " " || ch === "\n" || ch === "\r" || ch === " " || ch === "\f" || ch === "\0" || ch === "[" || ch === "]" || ch === "/" || ch === "{" || ch === "}";
|
|
142
|
+
}
|
|
143
|
+
function pageContentStream(page) {
|
|
144
|
+
try {
|
|
145
|
+
const obj = page.getObject?.();
|
|
146
|
+
const contents = obj?.get?.("Contents");
|
|
147
|
+
if (!contents)
|
|
148
|
+
return "";
|
|
149
|
+
const streams = contents.isArray?.() && contents.get ? Array.from({ length: contents.length ?? 0 }, (_, i) => contents.get(i)) : [contents];
|
|
150
|
+
let out = "";
|
|
151
|
+
for (const stream of streams) {
|
|
152
|
+
const buf = stream?.readStream?.();
|
|
153
|
+
const bytes = buf?.asUint8Array?.();
|
|
154
|
+
if (bytes)
|
|
155
|
+
out += Buffer.from(bytes).toString("latin1");
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
} catch {
|
|
159
|
+
return "";
|
|
69
160
|
}
|
|
70
|
-
|
|
161
|
+
}
|
|
162
|
+
function pageSignalsFrom(page, pageNumber) {
|
|
163
|
+
const json = JSON.parse(page.toStructuredText("preserve-images").asJSON());
|
|
164
|
+
const blocks = json.blocks ?? [];
|
|
165
|
+
const text = blocks.flatMap((b) => (b.lines ?? []).map((l) => l.text ?? "")).join(" ");
|
|
166
|
+
const [x0, y0, x1, y1] = page.getBounds();
|
|
167
|
+
const pageArea = Math.max((x1 - x0) * (y1 - y0), 1);
|
|
168
|
+
const imgArea = blocks.filter((b) => b.type === "image").reduce((a, b) => a + Math.max((b.bbox?.w ?? 0) * (b.bbox?.h ?? 0), 0), 0);
|
|
169
|
+
const vector = countVectorOperators(pageContentStream(page));
|
|
170
|
+
return {
|
|
171
|
+
page: pageNumber,
|
|
172
|
+
textChars: text.trim().length,
|
|
173
|
+
imageCoverage: Math.min(imgArea / pageArea, 1),
|
|
174
|
+
vectorOps: vector.ops,
|
|
175
|
+
vectorCurves: vector.curves,
|
|
176
|
+
vectorFills: vector.fills,
|
|
177
|
+
mathHint: textMathHint(text),
|
|
178
|
+
tableHint: textTableHint(text)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function scanPdfSignals(bytes) {
|
|
182
|
+
const { signals } = await analyzePdfDocument(bytes, { text: false, signals: true });
|
|
183
|
+
return signals;
|
|
71
184
|
}
|
|
72
185
|
|
|
73
186
|
// apps/admin/data/skills.lib/document/bin-src/doc-scan.mjs
|