rikiki-deck 0.6.0 → 0.7.2
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/.claude/skills/rikiki-debug/SKILL.md +17 -7
- package/.claude/skills/rikiki-deck/SKILL.md +362 -77
- package/.claude/skills/rikiki-theme/SKILL.md +1 -1
- package/README.md +69 -50
- package/bin/lib/assemble.mjs +154 -0
- package/bin/lib/box-geometry.mjs +66 -0
- package/bin/lib/browser.mjs +302 -0
- package/bin/lib/check-api.d.ts +28 -0
- package/bin/lib/check-api.mjs +6 -0
- package/bin/lib/check-plugins.mjs +228 -0
- package/bin/lib/check.mjs +1347 -0
- package/bin/lib/cli-error.mjs +26 -0
- package/bin/lib/component-deps.mjs +69 -0
- package/bin/lib/diff.mjs +275 -0
- package/bin/lib/export-pdf.mjs +65 -0
- package/bin/lib/graph-hit.mjs +86 -0
- package/bin/lib/inline.mjs +137 -39
- package/bin/lib/narrative.mjs +77 -0
- package/bin/lib/prune-icons.mjs +104 -0
- package/bin/lib/render.mjs +195 -0
- package/bin/lib/scan-external.mjs +126 -0
- package/bin/lib/starter.mjs +27 -14
- package/bin/lib/visual.mjs +120 -0
- package/bin/rikiki.mjs +420 -35
- package/dist/annotation-marks.d.ts +60 -0
- package/dist/annotation-marks.js +1 -0
- package/dist/bar-segments.d.ts +28 -0
- package/dist/bar-segments.js +1 -0
- package/dist/browser-location.d.ts +3 -0
- package/dist/browser-location.js +1 -0
- package/dist/cards-syntax.d.ts +31 -0
- package/dist/cards-syntax.js +6 -0
- package/dist/{plugins/click-stages.d.ts → click-stages.d.ts} +1 -1
- package/dist/deck-agenda.d.ts +25 -0
- package/dist/deck-agenda.js +6 -0
- package/dist/deck-annotate.d.ts +108 -0
- package/dist/deck-annotate.js +18 -0
- package/dist/deck-bar.d.ts +32 -0
- package/dist/deck-bar.js +19 -0
- package/dist/deck-bento.d.ts +38 -0
- package/dist/deck-bento.js +4 -0
- package/dist/{molecules/deck-callout.d.ts → deck-callout.d.ts} +2 -0
- package/dist/deck-callout.js +1 -1
- package/dist/deck-cell.d.ts +19 -0
- package/dist/deck-cell.js +1 -0
- package/dist/deck-checklist.d.ts +20 -0
- package/dist/deck-checklist.js +1 -0
- package/dist/{layouts/deck-cover.d.ts → deck-cover.d.ts} +8 -0
- package/dist/deck-cover.js +9 -6
- package/dist/deck-csv.d.ts +38 -0
- package/dist/deck-csv.js +15 -0
- package/dist/deck-feature-cards.js +2 -2
- package/dist/deck-feature.d.ts +18 -0
- package/dist/deck-feature.js +2 -2
- package/dist/deck-figure.d.ts +26 -0
- package/dist/deck-figure.js +8 -0
- package/dist/deck-fit.d.ts +14 -0
- package/dist/deck-fit.js +1 -0
- package/dist/deck-flow.d.ts +41 -0
- package/dist/deck-flow.js +7 -0
- package/dist/deck-graph.d.ts +92 -0
- package/dist/deck-graph.js +25 -0
- package/dist/deck-grid.js +1 -1
- package/dist/deck-icon.d.ts +20 -0
- package/dist/deck-icon.js +1 -0
- package/dist/{atoms/deck-kicker.d.ts → deck-kicker.d.ts} +5 -0
- package/dist/deck-kicker.js +1 -1
- package/dist/deck-kpi-grid.d.ts +26 -0
- package/dist/deck-kpi-grid.js +4 -0
- package/dist/deck-link.d.ts +21 -0
- package/dist/deck-link.js +1 -0
- package/dist/{molecules/deck-md.d.ts → deck-md.d.ts} +3 -0
- package/dist/deck-md.js +8 -3
- package/dist/deck-mermaid.js +15 -3
- package/dist/deck-outline.d.ts +50 -0
- package/dist/deck-outline.js +1 -0
- package/dist/deck-overview.js +53 -39
- package/dist/deck-persona.d.ts +31 -0
- package/dist/deck-persona.js +6 -0
- package/dist/deck-photo.js +1 -1
- package/dist/deck-point.d.ts +22 -0
- package/dist/deck-point.js +1 -0
- package/dist/deck-presenter.js +120 -48
- package/dist/deck-pull.d.ts +13 -0
- package/dist/deck-pull.js +1 -0
- package/dist/{atoms/deck-punch.d.ts → deck-punch.d.ts} +6 -0
- package/dist/deck-punch.js +1 -1
- package/dist/deck-quote.d.ts +28 -0
- package/dist/deck-quote.js +6 -0
- package/dist/{runtime/deck-root.d.ts → deck-root.d.ts} +88 -8
- package/dist/deck-root.js +17 -13
- package/dist/deck-section.js +2 -2
- package/dist/deck-source.d.ts +12 -0
- package/dist/deck-source.js +2 -0
- package/dist/deck-split.d.ts +30 -0
- package/dist/deck-split.js +5 -3
- package/dist/{molecules/deck-stat.d.ts → deck-stat.d.ts} +2 -0
- package/dist/deck-stat.js +2 -2
- package/dist/{molecules/deck-step-list.d.ts → deck-step-list.d.ts} +8 -0
- package/dist/deck-step-list.js +4 -2
- package/dist/deck-table.d.ts +26 -0
- package/dist/deck-table.js +1 -0
- package/dist/deck-takeaway.d.ts +18 -0
- package/dist/deck-takeaway.js +2 -2
- package/dist/deck-timeline.d.ts +37 -0
- package/dist/deck-timeline.js +5 -0
- package/dist/deck-transition.js +3 -3
- package/dist/deck-versus.d.ts +18 -0
- package/dist/deck-versus.js +9 -0
- package/dist/deep-link.d.ts +29 -0
- package/dist/deep-link.js +1 -0
- package/dist/escape-html.d.ts +3 -0
- package/dist/escape-html.js +1 -0
- package/dist/fit-controller.d.ts +27 -0
- package/dist/fit-controller.js +1 -0
- package/dist/graph-layout.d.ts +35 -0
- package/dist/graph-layout.js +1 -0
- package/dist/grid-tracks.d.ts +17 -0
- package/dist/grid-tracks.js +1 -0
- package/dist/icon-set.d.ts +6 -0
- package/dist/icon-set.js +1 -0
- package/dist/index.d.ts +37 -31
- package/dist/index.js +95 -49
- package/dist/keymap.d.ts +40 -0
- package/dist/keymap.js +1 -0
- package/dist/mouse-nav.d.ts +12 -0
- package/dist/mouse-nav.js +1 -0
- package/dist/navigation.d.ts +25 -0
- package/dist/navigation.js +1 -0
- package/dist/parse-csv.d.ts +9 -0
- package/dist/parse-csv.js +3 -0
- package/dist/shared-styles.js +1 -1
- package/dist/shiki.d.ts +8 -0
- package/dist/signature.d.ts +2 -0
- package/dist/signature.js +1 -0
- package/dist/slide-fill.d.ts +8 -0
- package/dist/slide-fill.js +1 -0
- package/dist/standalone.js +301 -169
- package/dist/vendor/THIRD-PARTY-NOTICES.txt +4347 -0
- package/dist/vendor/inventory.json +3029 -0
- package/dist/vendor/lit.js +62 -2
- package/dist/vendor/mermaid.min.js +95 -95
- package/dist/vendor/shiki.js +1 -57
- package/dist/viewport.d.ts +42 -0
- package/dist/viewport.js +1 -0
- package/docs/llms/rikiki-reference.md +955 -64
- package/docs/llms/rikiki-workflow.md +536 -0
- package/llms.txt +39 -12
- package/package.json +33 -12
- package/themes/rikiki.css +173 -47
- package/themes/siliceum.css +171 -51
- package/dist/layouts/deck-feature.d.ts +0 -11
- package/dist/layouts/deck-split.d.ts +0 -18
- package/dist/layouts/deck-takeaway.d.ts +0 -11
- package/dist/plugins/shiki.d.ts +0 -8
- /package/dist/{runtime/color.d.ts → color.d.ts} +0 -0
- /package/dist/{atoms/deck-badge.d.ts → deck-badge.d.ts} +0 -0
- /package/dist/{molecules/deck-card.d.ts → deck-card.d.ts} +0 -0
- /package/dist/{atoms/deck-code-highlighter.d.ts → deck-code-highlighter.d.ts} +0 -0
- /package/dist/{atoms/deck-code.d.ts → deck-code.d.ts} +0 -0
- /package/dist/{layouts/deck-feature-cards.d.ts → deck-feature-cards.d.ts} +0 -0
- /package/dist/{molecules/deck-grid.d.ts → deck-grid.d.ts} +0 -0
- /package/dist/{runtime/deck-help.d.ts → deck-help.d.ts} +0 -0
- /package/dist/{molecules/deck-mermaid.d.ts → deck-mermaid.d.ts} +0 -0
- /package/dist/{molecules/deck-metric.d.ts → deck-metric.d.ts} +0 -0
- /package/dist/{runtime/deck-notes.d.ts → deck-notes.d.ts} +0 -0
- /package/dist/{runtime/deck-overview.d.ts → deck-overview.d.ts} +0 -0
- /package/dist/{layouts/deck-photo.d.ts → deck-photo.d.ts} +0 -0
- /package/dist/{runtime/deck-presenter.d.ts → deck-presenter.d.ts} +0 -0
- /package/dist/{layouts/deck-section.d.ts → deck-section.d.ts} +0 -0
- /package/dist/{molecules/deck-shortcut.d.ts → deck-shortcut.d.ts} +0 -0
- /package/dist/{molecules/deck-stack.d.ts → deck-stack.d.ts} +0 -0
- /package/dist/{molecules/deck-tier-list.d.ts → deck-tier-list.d.ts} +0 -0
- /package/dist/{runtime/deck-transition.d.ts → deck-transition.d.ts} +0 -0
package/bin/rikiki.mjs
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
// ════════════════════════════════════════════════════════════════
|
|
3
3
|
// rikiki CLI
|
|
4
4
|
//
|
|
5
|
-
// rikiki init
|
|
6
|
-
//
|
|
7
|
-
// rikiki bundle <deck.html> [out.html|-] [--no-fonts]
|
|
5
|
+
// rikiki init [name.html] [--standalone] [--title "…"] [--theme rikiki|siliceum]
|
|
6
|
+
// [--with-mermaid] [--with-shiki] [--no-fonts] [--force]
|
|
7
|
+
// rikiki bundle <deck.html> [out.html|-] [--with-mermaid] [--with-shiki] [--no-fonts]
|
|
8
|
+
// rikiki assemble <deck.config.js> [out.html|-]
|
|
9
|
+
// rikiki render <deck.html> [--out dir] [--slides a,b] [--steps]
|
|
10
|
+
// [--baseline dir] [--threshold pct] [--json]
|
|
11
|
+
// rikiki check <deck.html> [--json] [--no-visual]
|
|
12
|
+
// rikiki export <deck.html> [--output deck.pdf]
|
|
8
13
|
// rikiki skills [--dir <path>] [--force]
|
|
9
14
|
//
|
|
15
|
+
// `init` writes an editable source deck plus the runtime it needs, next to it.
|
|
10
16
|
// `init --standalone` generates a self-contained, share-anywhere deck with no
|
|
11
17
|
// external links. `bundle` folds an existing deck into the same single file.
|
|
12
18
|
// Both use the rolldown-powered inliner in lib/inline.mjs. `skills` installs the
|
|
@@ -19,20 +25,53 @@ import { resolve, dirname, basename, join } from 'node:path';
|
|
|
19
25
|
import { fileURLToPath } from 'node:url';
|
|
20
26
|
import { inlineDeck } from './lib/inline.mjs';
|
|
21
27
|
import { starterHtml } from './lib/starter.mjs';
|
|
28
|
+
import { formatExternal, scanExternal } from './lib/scan-external.mjs';
|
|
29
|
+
import { pruneIcons } from './lib/prune-icons.mjs';
|
|
30
|
+
import { resolveCheckPlugins } from './lib/check-plugins.mjs';
|
|
31
|
+
import { exportPdf } from './lib/export-pdf.mjs';
|
|
32
|
+
import { ExpectedError, formatCliError } from './lib/cli-error.mjs';
|
|
33
|
+
import { assembleDeck } from './lib/assemble.mjs';
|
|
34
|
+
import { renderDeck } from './lib/render.mjs';
|
|
35
|
+
import { DEFAULT_THRESHOLD, diffFailed, formatDiff } from './lib/diff.mjs';
|
|
36
|
+
import { checkDeck, formatReport } from './lib/check.mjs';
|
|
22
37
|
|
|
23
38
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
39
|
|
|
25
40
|
const HELP = `rikiki · self-contained slide decks
|
|
26
41
|
|
|
27
|
-
rikiki init
|
|
42
|
+
rikiki init [name.html] [options] write an editable deck + its runtime
|
|
43
|
+
rikiki init --standalone [name.html] [options] generate a single self-contained file
|
|
44
|
+
rikiki assemble <deck.config.js> [out.html|-] build one deck from ordered partials
|
|
28
45
|
rikiki bundle <deck.html> [out.html|-] [options] fold an existing deck into one file
|
|
29
|
-
rikiki
|
|
46
|
+
rikiki render <deck.html> [options] one PNG per slide, plus a gallery and a manifest
|
|
47
|
+
rikiki check <deck.html> [--json] [--no-visual] [--steps] measure the deck and report what is wrong
|
|
48
|
+
rikiki export <deck.html> [--output deck.pdf] render the deck to PDF, one slide per page
|
|
49
|
+
rikiki skills [--agent codex|claude] [--plugin package] [--dir path] [--force]
|
|
50
|
+
install core and module agent skills
|
|
30
51
|
|
|
31
52
|
Options:
|
|
32
53
|
--title "…" deck title (init)
|
|
54
|
+
--standalone init: emit one self-contained file instead of a source deck
|
|
55
|
+
--force init: overwrite an existing deck
|
|
33
56
|
--theme rikiki|siliceum theme · siliceum inlines its local fonts (default: rikiki)
|
|
34
57
|
--with-mermaid inline the mermaid runtime (+~3 MB)
|
|
35
|
-
--with-shiki inline the Shiki highlighter (+~
|
|
58
|
+
--with-shiki inline the curated Shiki highlighter (+~0.7 MB raw)
|
|
59
|
+
--output, -o <file> PDF path (export · default <deck>.pdf)
|
|
60
|
+
--out <dir> picture directory (render · default <deck>.shots/)
|
|
61
|
+
--slides a,b render: which slides · numbers (1-based) or ids
|
|
62
|
+
--steps render: one picture per revealed state, not just the first
|
|
63
|
+
--baseline <dir> render: compare this render to an earlier one, slide by slide
|
|
64
|
+
--threshold <pct> render: percent of pixels that makes a slide changed (default 0.5)
|
|
65
|
+
--width, --height render/check: canvas size in pixels (default 1920×1080)
|
|
66
|
+
--json check / render --baseline: write the report to stdout as JSON
|
|
67
|
+
--no-visual check: skip the pixel pass (one screenshot per slide)
|
|
68
|
+
--config <file> check/skills: explicit rikiki.config.json
|
|
69
|
+
--plugin <package> check/skills: activate a module (repeatable)
|
|
70
|
+
--no-plugins check: disable module checks explicitly
|
|
71
|
+
--narrative-out <file> check: prepare review material for the current agent
|
|
72
|
+
--narrative-review <file> check: import the current agent's structured review
|
|
73
|
+
--plugin-timeout <ms> check: execution deadline per plugin call (default 5000)
|
|
74
|
+
--steps check: measure every revealed state of each slide, not just the first
|
|
36
75
|
--no-fonts drop fonts instead of inlining them (smaller, system fonts)
|
|
37
76
|
--all bundle every component (skip the used-only curation)
|
|
38
77
|
--include a,b force-include components used only from JS
|
|
@@ -46,6 +85,28 @@ The bundle is curated to the components the deck uses; HTML and CSS stay
|
|
|
46
85
|
readable so you can keep editing the file. Images are inlined as base64 and
|
|
47
86
|
SVGs as inline markup.`;
|
|
48
87
|
|
|
88
|
+
/** A pixel dimension from the flags · a size that is not a size is an error,
|
|
89
|
+
* not a silent fallback to the default. */
|
|
90
|
+
function pixels(values, flag, fallback) {
|
|
91
|
+
if (values[flag] === undefined) return fallback;
|
|
92
|
+
const n = Number(values[flag]);
|
|
93
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
94
|
+
throw new ExpectedError(`--${flag} must be a positive whole number of pixels`);
|
|
95
|
+
}
|
|
96
|
+
return n;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A percentage from the flags · anything that is not one is an error, not a
|
|
100
|
+
* silent fallback to the default. `0` is a legitimate value. */
|
|
101
|
+
function percent(values, flag, fallback) {
|
|
102
|
+
if (values[flag] === undefined) return fallback;
|
|
103
|
+
const n = Number(values[flag]);
|
|
104
|
+
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
105
|
+
throw new ExpectedError(`--${flag} must be a percentage between 0 and 100`);
|
|
106
|
+
}
|
|
107
|
+
return n;
|
|
108
|
+
}
|
|
109
|
+
|
|
49
110
|
/** Shared inlining options derived from the parsed flags. */
|
|
50
111
|
const inlineOpts = (v) => ({
|
|
51
112
|
all: v.all,
|
|
@@ -65,16 +126,81 @@ const SHARED_OPTIONS = {
|
|
|
65
126
|
'no-fonts': { type: 'boolean', default: false },
|
|
66
127
|
};
|
|
67
128
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
129
|
+
// References that appear in a bundle but are provably never fetched from it.
|
|
130
|
+
// Each one needs a reason, and the offline e2e test is what actually proves it.
|
|
131
|
+
const INERT_IN_BUNDLE = new Map([
|
|
132
|
+
[
|
|
133
|
+
'./index.js',
|
|
134
|
+
'deck-presenter computes it as a fallback bundle href · a bundled deck always ' +
|
|
135
|
+
'takes the inline branch instead (bundleInline is non-empty)',
|
|
136
|
+
],
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/** Report anything a "self-contained" file could still fetch at runtime.
|
|
140
|
+
* Returns true when the file really reaches for nothing.
|
|
141
|
+
*
|
|
142
|
+
* `inlined` names the heavy runtimes folded into this file. Their loaders keep
|
|
143
|
+
* a `new URL('./vendor/…')` in the code, but that branch is dead once the
|
|
144
|
+
* global is already set · which is exactly what inlining does. */
|
|
145
|
+
function checkSelfContained(html, inlined = {}) {
|
|
146
|
+
const dead = new Map(INERT_IN_BUNDLE);
|
|
147
|
+
if (inlined.mermaid) {
|
|
148
|
+
dead.set(
|
|
149
|
+
'./vendor/mermaid.min.js',
|
|
150
|
+
'the mermaid UMD is inlined above and sets window.mermaid, so the loader ' +
|
|
151
|
+
'returns before it builds this URL',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (inlined.shiki) {
|
|
155
|
+
dead.set(
|
|
156
|
+
'./vendor/shiki.js',
|
|
157
|
+
'the Shiki highlighter is inlined above and registered on globalThis',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const hits = scanExternal(html);
|
|
161
|
+
const real = hits.filter((h) => !dead.has(h.ref));
|
|
162
|
+
const inert = hits.filter((h) => dead.has(h.ref));
|
|
163
|
+
|
|
164
|
+
for (const h of inert) {
|
|
165
|
+
console.error(`rikiki · note · ${h.ref} stays in the file but is never fetched`);
|
|
166
|
+
console.error(` (${dead.get(h.ref)})`);
|
|
76
167
|
}
|
|
77
|
-
|
|
168
|
+
if (real.length === 0) return true;
|
|
169
|
+
|
|
170
|
+
console.error('rikiki · ERROR · the bundle is NOT self-contained · it still fetches:');
|
|
171
|
+
console.error(formatExternal(real));
|
|
172
|
+
const wantsMermaid = real.some((h) => h.ref.includes('mermaid'));
|
|
173
|
+
const wantsShiki = real.some((h) => h.ref.includes('shiki'));
|
|
174
|
+
if (wantsMermaid) console.error(' → this deck uses <deck-mermaid> · re-run with --with-mermaid');
|
|
175
|
+
if (wantsShiki) console.error(' → this deck uses Shiki · re-run with --with-shiki');
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Inject the heavy vendor runtimes the inliner then folds into the file.
|
|
180
|
+
* mermaid's UMD sets window.mermaid, so deck-mermaid skips its network load. */
|
|
181
|
+
function injectVendors(html, { withMermaid, withShiki }) {
|
|
182
|
+
// Shiki goes FIRST · it publishes globalThis.__rikikiShiki from a module, and
|
|
183
|
+
// modules run in document order, so a deck whose own <script> calls
|
|
184
|
+
// installShiki() must not be reached before the global exists.
|
|
185
|
+
const first = withShiki
|
|
186
|
+
? `<script type="module">\n` +
|
|
187
|
+
`import { createHighlighter } from '${PKG_ROOT}/dist/vendor/shiki.js';\n` +
|
|
188
|
+
`globalThis.__rikikiShiki = createHighlighter;\n` +
|
|
189
|
+
`</script>\n`
|
|
190
|
+
: '';
|
|
191
|
+
// mermaid goes LAST · it is a classic script, so it runs during parsing, well
|
|
192
|
+
// before any module, wherever it sits. Keeping it after the deck's own markup
|
|
193
|
+
// also keeps the inliner's asset passes away from its 3 MB of minified JS.
|
|
194
|
+
const last = withMermaid
|
|
195
|
+
? `<script src="${PKG_ROOT}/dist/vendor/mermaid.min.js"></script>\n`
|
|
196
|
+
: '';
|
|
197
|
+
if (!first && !last) return html;
|
|
198
|
+
|
|
199
|
+
let out = html;
|
|
200
|
+
const headOpen = out.match(/<head\b[^>]*>/i);
|
|
201
|
+
if (first) out = headOpen ? out.replace(headOpen[0], headOpen[0] + '\n' + first) : first + out;
|
|
202
|
+
if (last) out = out.includes('</head>') ? out.replace('</head>', last + '</head>') : out + last;
|
|
203
|
+
return out;
|
|
78
204
|
}
|
|
79
205
|
|
|
80
206
|
function writeOut(html, outputPath) {
|
|
@@ -84,12 +210,30 @@ function writeOut(html, outputPath) {
|
|
|
84
210
|
console.error(`rikiki · wrote ${outputPath} · ${kb} KB`);
|
|
85
211
|
}
|
|
86
212
|
|
|
213
|
+
// The runtime a source deck loads from next to itself. `dist/vendor` also
|
|
214
|
+
// holds lit and marked, which every deck needs · only the two heavy plugin
|
|
215
|
+
// payloads (~12 MB) wait until a deck asks for them.
|
|
216
|
+
const ASSET_DIR = 'rikiki';
|
|
217
|
+
const RUNTIME_ASSETS = ['dist', 'tokens.css', 'themes', 'fonts'];
|
|
218
|
+
const HEAVY_VENDORS = /[\\/]dist[\\/]vendor[\\/](mermaid\.min\.js|shiki\.js)$/;
|
|
219
|
+
|
|
220
|
+
/** Copy the runtime next to the deck. */
|
|
221
|
+
function copyRuntime(destRoot, { withVendor }) {
|
|
222
|
+
const skipVendor = (src) => withVendor || !HEAVY_VENDORS.test(src);
|
|
223
|
+
for (const asset of RUNTIME_ASSETS) {
|
|
224
|
+
const src = join(PKG_ROOT, asset);
|
|
225
|
+
if (!existsSync(src)) continue; // a trimmed install (e.g. no fonts) stays usable
|
|
226
|
+
cpSync(src, join(destRoot, asset), { recursive: true, filter: skipVendor });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
87
230
|
async function cmdInit(argv) {
|
|
88
231
|
const { values, positionals } = parseArgs({
|
|
89
232
|
args: argv,
|
|
90
233
|
allowPositionals: true,
|
|
91
234
|
options: {
|
|
92
235
|
standalone: { type: 'boolean', default: false },
|
|
236
|
+
force: { type: 'boolean', default: false },
|
|
93
237
|
title: { type: 'string' },
|
|
94
238
|
theme: { type: 'string', default: 'rikiki' },
|
|
95
239
|
'with-mermaid': { type: 'boolean', default: false },
|
|
@@ -98,31 +242,232 @@ async function cmdInit(argv) {
|
|
|
98
242
|
},
|
|
99
243
|
});
|
|
100
244
|
|
|
101
|
-
// `init` only produces standalone single-file decks for now · accept the flag
|
|
102
|
-
// explicitly but don't require it (the whole point is the self-contained file).
|
|
103
|
-
if (!values.standalone) {
|
|
104
|
-
console.error('rikiki · init currently generates standalone single-file decks · assuming --standalone');
|
|
105
|
-
}
|
|
106
245
|
const theme = values.theme === 'siliceum' ? 'siliceum' : 'rikiki';
|
|
107
246
|
const name = positionals[0] || 'slides.html';
|
|
108
247
|
const outputPath = name === '-' ? '-' : resolve(process.cwd(), name.endsWith('.html') ? name : name + '.html');
|
|
109
248
|
const title = values.title || basename(name, '.html').replace(/[-_]/g, ' ') || 'My deck';
|
|
110
249
|
|
|
111
|
-
|
|
250
|
+
// Someone's deck is not ours to replace · the second `init` in a directory is
|
|
251
|
+
// far more often a mistake than an intent.
|
|
252
|
+
if (outputPath !== '-' && existsSync(outputPath) && !values.force) {
|
|
253
|
+
throw new ExpectedError(`init · ${name} already exists · pass --force to overwrite it`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const starter = {
|
|
112
257
|
title, theme,
|
|
113
258
|
withMermaid: values['with-mermaid'],
|
|
114
259
|
withShiki: values['with-shiki'],
|
|
115
|
-
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Default: a source deck. It needs nothing but Node, stays readable, and is
|
|
263
|
+
// what `bundle` later folds into a single file.
|
|
264
|
+
if (!values.standalone) {
|
|
265
|
+
const html = starterHtml({ ...starter, assetBase: ASSET_DIR + '/' });
|
|
266
|
+
if (outputPath === '-') {
|
|
267
|
+
console.error(`rikiki · note · run \`rikiki init <name>.html\` to also copy the runtime into ./${ASSET_DIR}/`);
|
|
268
|
+
writeOut(html, outputPath);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
copyRuntime(join(dirname(outputPath), ASSET_DIR), {
|
|
272
|
+
withVendor: values['with-mermaid'] || values['with-shiki'],
|
|
273
|
+
});
|
|
274
|
+
writeOut(html, outputPath);
|
|
275
|
+
console.error(`rikiki · runtime copied to ./${ASSET_DIR}/ · serve this folder over HTTP, ES modules do not load from file://`);
|
|
276
|
+
console.error(`rikiki · next · edit ${basename(outputPath)} · then \`rikiki bundle ${basename(outputPath)}\` for one shareable file`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const html = starterHtml(starter);
|
|
116
281
|
const inlined = await inlineDeck({ html, baseDir: PKG_ROOT, pkgRoot: PKG_ROOT, ...inlineOpts(values) });
|
|
117
282
|
writeOut(inlined, outputPath);
|
|
118
|
-
|
|
283
|
+
// Same contract as `bundle` · a starter that would 404 offline is not a
|
|
284
|
+
// starter, so the exit code says so.
|
|
285
|
+
const ok = checkSelfContained(inlined, {
|
|
286
|
+
mermaid: values['with-mermaid'],
|
|
287
|
+
shiki: values['with-shiki'],
|
|
288
|
+
});
|
|
289
|
+
if (!ok) process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function cmdAssemble(argv) {
|
|
293
|
+
const { positionals } = parseArgs({ args: argv, allowPositionals: true, options: {} });
|
|
294
|
+
const config = positionals[0];
|
|
295
|
+
if (!config) throw new ExpectedError('assemble · missing <deck.config.{js,json}>\n\n' + HELP);
|
|
296
|
+
|
|
297
|
+
const { html, outputPath, slides, unbundleable } = await assembleDeck(config, positionals[1]);
|
|
298
|
+
for (const href of unbundleable) {
|
|
299
|
+
console.error(`rikiki · note · ${href} is not a \`rikiki/…\` path · it serves, but \`rikiki bundle\` will not inline it`);
|
|
300
|
+
}
|
|
301
|
+
if (!outputPath) { process.stdout.write(html); return; }
|
|
302
|
+
const kb = (Buffer.byteLength(html) / 1024).toFixed(0);
|
|
303
|
+
console.error(`rikiki · wrote ${outputPath} · ${slides} partial(s) · ${kb} KB`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function cmdRender(argv) {
|
|
307
|
+
const { values, positionals } = parseArgs({
|
|
308
|
+
args: argv,
|
|
309
|
+
allowPositionals: true,
|
|
310
|
+
options: {
|
|
311
|
+
out: { type: 'string' },
|
|
312
|
+
slides: { type: 'string' },
|
|
313
|
+
steps: { type: 'boolean', default: false },
|
|
314
|
+
width: { type: 'string' },
|
|
315
|
+
height: { type: 'string' },
|
|
316
|
+
baseline: { type: 'string' },
|
|
317
|
+
threshold: { type: 'string' },
|
|
318
|
+
json: { type: 'boolean', default: false },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const inputPath = deckArgument('render', positionals[0]);
|
|
322
|
+
const outDir = resolve(process.cwd(), values.out ?? basename(inputPath, '.html') + '.shots');
|
|
323
|
+
// Checked before the render, not after: a baseline that is not there is
|
|
324
|
+
// worth knowing before spending a browser on thirty screenshots.
|
|
325
|
+
const baseline = baselineArgument(values.baseline);
|
|
326
|
+
const { manifest, galleryPath, diff, diffPath } = await renderDeck(inputPath, {
|
|
327
|
+
outDir,
|
|
328
|
+
slides: values.slides,
|
|
329
|
+
steps: values.steps,
|
|
330
|
+
width: pixels(values, 'width', 1920),
|
|
331
|
+
height: pixels(values, 'height', 1080),
|
|
332
|
+
baseline,
|
|
333
|
+
threshold: percent(values, 'threshold', DEFAULT_THRESHOLD),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
for (const url of manifest.missing.slice(0, 5)) {
|
|
337
|
+
console.error(`rikiki · WARNING · the deck could not load: ${url}`);
|
|
338
|
+
}
|
|
339
|
+
console.error(
|
|
340
|
+
`rikiki · wrote ${manifest.captured} shot(s) to ${outDir} · ${manifest.canvas.width}×${manifest.canvas.height}`,
|
|
341
|
+
);
|
|
342
|
+
if (!manifest.stepsCaptured) {
|
|
343
|
+
console.error('rikiki · note · stepped slides are shown in their opening state · pass --steps for the rest');
|
|
344
|
+
}
|
|
345
|
+
console.error(`rikiki · gallery ${galleryPath}`);
|
|
346
|
+
|
|
347
|
+
if (!diff) return;
|
|
348
|
+
// In --json mode stdout carries the report and nothing else, so a caller can
|
|
349
|
+
// pipe it straight into a tool · same contract as `check --json`.
|
|
350
|
+
if (values.json) process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
|
|
351
|
+
else console.error(formatDiff(diff));
|
|
352
|
+
console.error(`rikiki · diff ${diffPath}`);
|
|
353
|
+
if (diffFailed(diff.summary)) process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Resolve `--baseline` · a directory that is not there is the one thing this
|
|
357
|
+
* command cannot work around, so it stops rather than render into silence. */
|
|
358
|
+
function baselineArgument(input) {
|
|
359
|
+
if (input === undefined) return undefined;
|
|
360
|
+
const dir = resolve(process.cwd(), input);
|
|
361
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
362
|
+
throw new ExpectedError(`render · baseline directory not found: ${dir}`, { exitCode: 2 });
|
|
363
|
+
}
|
|
364
|
+
return dir;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Resolve a deck argument · shared by the commands that read one. */
|
|
368
|
+
function deckArgument(command, input, { exitCode = 1 } = {}) {
|
|
369
|
+
if (!input) throw new ExpectedError(`${command} · missing <deck.html>\n\n` + HELP, { exitCode });
|
|
370
|
+
const inputPath = resolve(process.cwd(), input);
|
|
371
|
+
if (!existsSync(inputPath) || !statSync(inputPath).isFile()) {
|
|
372
|
+
throw new ExpectedError(`${command} · input not found: ` + inputPath, { exitCode });
|
|
373
|
+
}
|
|
374
|
+
return inputPath;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function cmdCheck(argv) {
|
|
378
|
+
const { values, positionals } = parseArgs({
|
|
379
|
+
args: argv,
|
|
380
|
+
allowPositionals: true,
|
|
381
|
+
options: {
|
|
382
|
+
json: { type: 'boolean', default: false },
|
|
383
|
+
'no-visual': { type: 'boolean', default: false },
|
|
384
|
+
config: { type: 'string' },
|
|
385
|
+
plugin: { type: 'string', multiple: true },
|
|
386
|
+
'no-plugins': { type: 'boolean', default: false },
|
|
387
|
+
'narrative-out': { type: 'string' },
|
|
388
|
+
'narrative-review': { type: 'string' },
|
|
389
|
+
'plugin-timeout': { type: 'string' },
|
|
390
|
+
steps: { type: 'boolean', default: false },
|
|
391
|
+
width: { type: 'string' },
|
|
392
|
+
height: { type: 'string' },
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
const inputPath = deckArgument('check', positionals[0], { exitCode: 2 });
|
|
396
|
+
const report = await checkDeck(inputPath, {
|
|
397
|
+
width: pixels(values, 'width', 1920),
|
|
398
|
+
height: pixels(values, 'height', 1080),
|
|
399
|
+
visual: !values['no-visual'],
|
|
400
|
+
steps: values.steps,
|
|
401
|
+
config: values.config,
|
|
402
|
+
plugins: values.plugin,
|
|
403
|
+
noPlugins: values['no-plugins'],
|
|
404
|
+
narrativeOut: values['narrative-out'],
|
|
405
|
+
narrativeReview: values['narrative-review'],
|
|
406
|
+
pluginTimeoutMs: values['plugin-timeout'] === undefined ? 5000 : Number(values['plugin-timeout']),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// In --json mode stdout carries the report and nothing else, so a caller can
|
|
410
|
+
// pipe it without stripping anything · including when the deck is broken.
|
|
411
|
+
if (values.json) process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
412
|
+
else console.error(formatReport(report));
|
|
413
|
+
|
|
414
|
+
if (report.summary.error > 0) process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function cmdExport(argv) {
|
|
418
|
+
const { values, positionals } = parseArgs({
|
|
419
|
+
args: argv,
|
|
420
|
+
allowPositionals: true,
|
|
421
|
+
options: { output: { type: 'string', short: 'o' } },
|
|
422
|
+
});
|
|
423
|
+
const input = positionals[0];
|
|
424
|
+
if (!input) {
|
|
425
|
+
console.error('rikiki export · missing <deck.html>\n\n' + HELP);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const inputPath = resolve(process.cwd(), input);
|
|
429
|
+
if (!existsSync(inputPath) || !statSync(inputPath).isFile()) {
|
|
430
|
+
console.error('rikiki export · input not found: ' + inputPath);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
const outputPath = resolve(
|
|
434
|
+
process.cwd(),
|
|
435
|
+
values.output ?? positionals[1] ?? basename(inputPath, '.html') + '.pdf',
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
let result;
|
|
439
|
+
try {
|
|
440
|
+
result = await exportPdf(inputPath, outputPath);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error('rikiki export · ' + (e instanceof Error ? e.message : String(e)));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
// A missing asset means a page printed without something the author put
|
|
446
|
+
// there · reporting it beats handing over a silently incomplete PDF.
|
|
447
|
+
if (result.missing.length) {
|
|
448
|
+
console.error('rikiki export · WARNING · the deck could not load:');
|
|
449
|
+
for (const url of [...new Set(result.missing)].slice(0, 10)) console.error(' · ' + url);
|
|
450
|
+
}
|
|
451
|
+
// One slide, one page · any gap either way means the paper does not match
|
|
452
|
+
// the deck, whether a slide was dropped or one spilled onto a second page.
|
|
453
|
+
if (result.pages !== result.slides) {
|
|
454
|
+
console.error(
|
|
455
|
+
`rikiki export · WARNING · ${result.slides} slides but ${result.pages} pages · ` +
|
|
456
|
+
'compare the PDF with the deck outline',
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
console.error(`rikiki · wrote ${outputPath} · ${result.pages} pages`);
|
|
119
460
|
}
|
|
120
461
|
|
|
121
462
|
async function cmdBundle(argv) {
|
|
122
463
|
const { values, positionals } = parseArgs({
|
|
123
464
|
args: argv,
|
|
124
465
|
allowPositionals: true,
|
|
125
|
-
options: {
|
|
466
|
+
options: {
|
|
467
|
+
'with-mermaid': { type: 'boolean', default: false },
|
|
468
|
+
'with-shiki': { type: 'boolean', default: false },
|
|
469
|
+
...SHARED_OPTIONS,
|
|
470
|
+
},
|
|
126
471
|
});
|
|
127
472
|
const input = positionals[0];
|
|
128
473
|
if (!input) { console.error('rikiki bundle · missing <deck.html>\n\n' + HELP); process.exit(1); }
|
|
@@ -135,10 +480,36 @@ async function cmdBundle(argv) {
|
|
|
135
480
|
: outArg ? resolve(process.cwd(), outArg)
|
|
136
481
|
: join(dirname(inputPath), basename(inputPath, '.html') + '.bundle.html');
|
|
137
482
|
|
|
138
|
-
const html = readFileSync(inputPath, 'utf8')
|
|
139
|
-
|
|
483
|
+
const html = injectVendors(readFileSync(inputPath, 'utf8'), {
|
|
484
|
+
withMermaid: values['with-mermaid'],
|
|
485
|
+
withShiki: values['with-shiki'],
|
|
486
|
+
});
|
|
487
|
+
let inlined = await inlineDeck({
|
|
488
|
+
html,
|
|
489
|
+
baseDir: dirname(inputPath),
|
|
490
|
+
pkgRoot: PKG_ROOT,
|
|
491
|
+
...inlineOpts(values),
|
|
492
|
+
});
|
|
493
|
+
// Curate the icon set the way components are already curated · a bundled deck
|
|
494
|
+
// carries the glyphs it writes and nothing else.
|
|
495
|
+
const icons = pruneIcons(inlined, html);
|
|
496
|
+
if (icons.pruned) {
|
|
497
|
+
inlined = icons.js;
|
|
498
|
+
console.error(
|
|
499
|
+
`rikiki · icons · kept ${icons.kept.length}, dropped ${icons.dropped} (${icons.saved} bytes)`,
|
|
500
|
+
);
|
|
501
|
+
} else if (icons.reason?.startsWith('unknown icon name')) {
|
|
502
|
+
// Worth saying out loud · the glyph will not render.
|
|
503
|
+
console.error(`rikiki · WARNING · ${icons.reason}`);
|
|
504
|
+
}
|
|
140
505
|
writeOut(inlined, outputPath);
|
|
141
|
-
|
|
506
|
+
// A bundle that still fetches is a broken deliverable, not a warning · the
|
|
507
|
+
// exit code is the only thing a CI job or a script can act on.
|
|
508
|
+
const ok = checkSelfContained(inlined, {
|
|
509
|
+
mermaid: values['with-mermaid'],
|
|
510
|
+
shiki: values['with-shiki'],
|
|
511
|
+
});
|
|
512
|
+
if (!ok) process.exit(1);
|
|
142
513
|
}
|
|
143
514
|
|
|
144
515
|
// Consumer-facing skills shipped in the npm tarball. `rikiki-component` and
|
|
@@ -149,12 +520,22 @@ const DISTRIBUTED_SKILLS = ['rikiki-deck', 'rikiki-theme', 'rikiki-debug'];
|
|
|
149
520
|
function cmdSkills(argv) {
|
|
150
521
|
const { values } = parseArgs({
|
|
151
522
|
args: argv,
|
|
152
|
-
options: { dir: { type: 'string' }, force: { type: 'boolean', default: false }
|
|
523
|
+
options: { dir: { type: 'string' }, force: { type: 'boolean', default: false },
|
|
524
|
+
agent: { type: 'string', default: 'claude' }, plugin: { type: 'string', multiple: true }, config: { type: 'string' } },
|
|
153
525
|
});
|
|
154
|
-
|
|
526
|
+
if (!['codex', 'claude'].includes(values.agent)) throw new ExpectedError('--agent must be codex or claude');
|
|
527
|
+
const targetRoot = resolve(process.cwd(), values.dir || (values.agent === 'codex' ? '.agents/skills' : '.claude/skills'));
|
|
528
|
+
const sources = DISTRIBUTED_SKILLS.map(name => ({ name, src: join(PKG_ROOT, '.claude', 'skills', name) }));
|
|
529
|
+
const resolution = resolveCheckPlugins(join(process.cwd(), 'deck.html'), { config: values.config, plugins: values.plugin });
|
|
530
|
+
if (resolution.diagnostics.length) throw new ExpectedError(resolution.diagnostics.map(d => d.message).join('\n'));
|
|
531
|
+
for (const plugin of resolution.plugins) for (const skill of plugin.skills ?? []) sources.push(skill);
|
|
532
|
+
const skillNames = new Set();
|
|
533
|
+
for (const { name } of sources) {
|
|
534
|
+
if (skillNames.has(name)) throw new ExpectedError(`duplicate skill name: ${name}`);
|
|
535
|
+
skillNames.add(name);
|
|
536
|
+
}
|
|
155
537
|
let copied = 0;
|
|
156
|
-
for (const name of
|
|
157
|
-
const src = join(PKG_ROOT, '.claude', 'skills', name);
|
|
538
|
+
for (const { name, src } of sources) {
|
|
158
539
|
if (!existsSync(src)) continue; // not in this install (e.g. running from a trimmed tarball)
|
|
159
540
|
const dest = join(targetRoot, name);
|
|
160
541
|
if (existsSync(dest) && !values.force) {
|
|
@@ -163,21 +544,25 @@ function cmdSkills(argv) {
|
|
|
163
544
|
}
|
|
164
545
|
mkdirSync(dirname(dest), { recursive: true });
|
|
165
546
|
cpSync(src, dest, { recursive: true });
|
|
166
|
-
console.error(`rikiki skills · installed ${name} → ${
|
|
547
|
+
console.error(`rikiki skills · installed ${name} → ${dest}`);
|
|
167
548
|
copied++;
|
|
168
549
|
}
|
|
169
|
-
if (copied) console.error(`rikiki skills · ${copied} skill(s) installed
|
|
550
|
+
if (copied) console.error(`rikiki skills · ${copied} skill(s) installed for ${values.agent} · reload the agent if needed`);
|
|
170
551
|
else console.error('rikiki skills · nothing installed');
|
|
171
552
|
}
|
|
172
553
|
|
|
173
554
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
174
555
|
try {
|
|
175
556
|
if (cmd === 'init') await cmdInit(rest);
|
|
557
|
+
else if (cmd === 'assemble') await cmdAssemble(rest);
|
|
176
558
|
else if (cmd === 'bundle') await cmdBundle(rest);
|
|
559
|
+
else if (cmd === 'render') await cmdRender(rest);
|
|
560
|
+
else if (cmd === 'check') await cmdCheck(rest);
|
|
561
|
+
else if (cmd === 'export') await cmdExport(rest);
|
|
177
562
|
else if (cmd === 'skills') cmdSkills(rest);
|
|
178
563
|
else if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') { console.log(HELP); }
|
|
179
564
|
else { console.error('rikiki · unknown command: ' + cmd + '\n\n' + HELP); process.exit(1); }
|
|
180
565
|
} catch (e) {
|
|
181
|
-
console.error(
|
|
182
|
-
process.exit(1);
|
|
566
|
+
console.error(formatCliError(e));
|
|
567
|
+
process.exit(e?.exitCode ?? 1);
|
|
183
568
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface Mark {
|
|
2
|
+
/** Horizontal position, 0..100, from the left edge of the image. */
|
|
3
|
+
readonly x: number;
|
|
4
|
+
/** Vertical position, 0..100, from the top edge. */
|
|
5
|
+
readonly y: number;
|
|
6
|
+
/** Caption shown in the legend · empty is allowed, the number still shows. */
|
|
7
|
+
readonly label: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PlacedMark extends Mark {
|
|
10
|
+
/** 1-based, in document order · what the reader sees in the badge. */
|
|
11
|
+
readonly n: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read the `marks` attribute · `x,y,label` triples separated by `|`.
|
|
15
|
+
*
|
|
16
|
+
* An attribute rather than JSON because a deck author writes HTML by hand, and
|
|
17
|
+
* `35,60,Latency spike` survives being retyped in five years better than an
|
|
18
|
+
* escaped JSON blob does.
|
|
19
|
+
*
|
|
20
|
+
* A mark with an unreadable coordinate is DROPPED rather than pinned to 0,0:
|
|
21
|
+
* a marker in the wrong corner of a screenshot is worse than a missing one,
|
|
22
|
+
* because the room believes it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseMarks(raw: string | null | undefined): Mark[];
|
|
25
|
+
/** Number the marks in document order. */
|
|
26
|
+
export declare function placeMarks(marks: readonly Mark[]): PlacedMark[];
|
|
27
|
+
/**
|
|
28
|
+
* How many marks are visible at `step`.
|
|
29
|
+
*
|
|
30
|
+
* Step 0 shows none, so the room looks at the screenshot before the speaker
|
|
31
|
+
* starts pointing at it. Each step reveals one more, and a step past the last
|
|
32
|
+
* mark keeps them all rather than wrapping.
|
|
33
|
+
*/
|
|
34
|
+
export declare function visibleCount(total: number, step: number): number;
|
|
35
|
+
/** Steps a slide needs to reveal every mark · the count the engine asks for. */
|
|
36
|
+
export declare const stepsForMarks: (total: number) => number;
|
|
37
|
+
/** A badge side name : the four directions a keyword offset can name. */
|
|
38
|
+
export type AnchorSide = 'above' | 'below' | 'left' | 'right';
|
|
39
|
+
/** A parsed `offset` / `offsets` entry, either raw pixels or a named side. */
|
|
40
|
+
export type Offset = {
|
|
41
|
+
readonly kind: 'px';
|
|
42
|
+
readonly dx: number;
|
|
43
|
+
readonly dy: number;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'anchor';
|
|
46
|
+
readonly side: AnchorSide;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Read one `offset` / `offsets` entry · either `x,y` CSS pixels or a keyword
|
|
50
|
+
* naming a side (`above`, `below`, `left`, `right`).
|
|
51
|
+
*
|
|
52
|
+
* A keyword lets the author state the intent instead of guessing pixels in a
|
|
53
|
+
* coordinate system they cannot see : the component turns it into a real
|
|
54
|
+
* displacement once it knows the rendered badge size.
|
|
55
|
+
*
|
|
56
|
+
* An unreadable entry falls back to `0,0`, same as an unreadable `x,y` pair
|
|
57
|
+
* always has : a badge pinned to its target is a smaller mistake than one
|
|
58
|
+
* thrown off-image by a typo.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseOffset(text: string | null | undefined): Offset;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var u=e=>Number.isFinite(e)?Math.max(0,Math.min(100,e)):0;function m(e){return e?e.split("|").map(r=>r.trim()).filter(Boolean).map(r=>{let[n="",a="",...i]=r.split(","),t=o=>o.trim()===""?Number.NaN:Number(o.trim());return{x:t(n),y:t(a),label:i.join(",").trim()}}).filter(r=>Number.isFinite(r.x)&&Number.isFinite(r.y)).map(r=>({x:u(r.x),y:u(r.y),label:r.label})):[]}function d(e){return e.map((r,n)=>({...r,n:n+1}))}function b(e,r){return e<=0?0:Math.max(0,Math.min(e,Math.floor(r)))}var c=e=>Math.max(0,e),l=["above","below","left","right"],s={kind:"px",dx:0,dy:0};function f(e){let r=e?.trim();if(!r)return s;if(l.includes(r))return{kind:"anchor",side:r};let[n,a]=r.split(","),i=Number(n?.trim()),t=Number(a?.trim());return Number.isFinite(i)&&Number.isFinite(t)?{kind:"px",dx:i,dy:t}:s}export{m as parseMarks,f as parseOffset,d as placeMarks,c as stepsForMarks,b as visibleCount};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** One slice of a bar · a share of the whole with a label and a tone. */
|
|
2
|
+
export interface Segment {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly value: number;
|
|
5
|
+
readonly tone?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SizedSegment extends Segment {
|
|
8
|
+
/** Share of the total, 0..100, rounded for display. */
|
|
9
|
+
readonly percent: number;
|
|
10
|
+
/** Exact share before rounding · used to lay the bar out without drift. */
|
|
11
|
+
readonly exact: number;
|
|
12
|
+
}
|
|
13
|
+
/** Parse the `segments` attribute · `label:value:tone` triples separated by
|
|
14
|
+
* `|`, because a deck author writes attributes, not JSON. The tone is
|
|
15
|
+
* optional; a malformed triple is skipped rather than rendered as garbage. */
|
|
16
|
+
export declare function parseSegments(raw: string | null | undefined): Segment[];
|
|
17
|
+
/**
|
|
18
|
+
* Size each segment against the total.
|
|
19
|
+
*
|
|
20
|
+
* `total` defaults to the sum, which is what a stacked bar wants. Passing a
|
|
21
|
+
* larger one is what makes "160 of 538" show the remaining 70% as empty track.
|
|
22
|
+
*
|
|
23
|
+
* Rounding is corrected on the largest segment so the displayed percentages
|
|
24
|
+
* always add to 100 · five segments of 16.67 must not print as 85.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sizeSegments(segments: readonly Segment[], total?: number): SizedSegment[];
|
|
27
|
+
/** The share of the track left empty · 0 for a stacked bar that fills it. */
|
|
28
|
+
export declare function remainder(sized: readonly SizedSegment[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function d(r){return r?r.split("|").map(e=>e.trim()).filter(Boolean).map(e=>{let[i="",u="",n=""]=e.split(":").map(c=>c.trim());return{label:i,value:u===""?Number.NaN:Number(u),tone:n||void 0}}).filter(e=>e.label.length>0&&Number.isFinite(e.value)&&e.value>=0):[]}function m(r,e){let i=r.reduce((t,a)=>t+Math.max(0,a.value),0),u=e!==void 0&&e>0?e:i;if(u<=0)return r.map(t=>({...t,percent:0,exact:0}));let n=r.map(t=>{let a=Math.max(0,t.value)/u*100;return{...t,exact:a,percent:Math.round(a)}});if(e!==void 0&&e>0)return n;let c=100-n.reduce((t,a)=>t+a.percent,0);if(c===0||n.length===0)return n;let l=0;for(let t=1;t<n.length;t++)n[t].exact>n[l].exact&&(l=t);return n[l]={...n[l],percent:n[l].percent+c},n}function o(r){let e=r.reduce((i,u)=>i+u.exact,0);return Math.max(0,100-e)}export{d as parseSegments,o as remainder,m as sizeSegments};
|