rikiki-deck 0.7.1 → 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/README.md +1 -1
- package/bin/lib/browser.mjs +24 -1
- 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 +369 -14
- package/bin/lib/component-deps.mjs +69 -0
- package/bin/lib/export-pdf.mjs +28 -5
- package/bin/lib/inline.mjs +70 -35
- package/bin/lib/narrative.mjs +77 -0
- package/bin/lib/prune-icons.mjs +24 -1
- package/bin/rikiki.mjs +45 -7
- package/dist/{plugins/click-stages.d.ts → click-stages.d.ts} +1 -1
- package/dist/{layouts/deck-cover.d.ts → deck-cover.d.ts} +2 -0
- package/dist/deck-cover.js +9 -6
- package/dist/{atoms/deck-kicker.d.ts → deck-kicker.d.ts} +5 -0
- package/dist/deck-kicker.js +1 -1
- package/dist/deck-mermaid.js +4 -4
- package/dist/deck-overview.js +41 -40
- package/dist/deck-presenter.js +89 -46
- package/dist/{runtime/deck-root.d.ts → deck-root.d.ts} +14 -3
- package/dist/deck-root.js +5 -5
- package/dist/{extras/deck-timeline.d.ts → deck-timeline.d.ts} +6 -0
- package/dist/deck-timeline.js +4 -4
- package/dist/deck-versus.js +2 -2
- package/dist/index.d.ts +37 -37
- package/dist/index.js +55 -52
- package/dist/standalone.js +229 -182
- package/dist/vendor/THIRD-PARTY-NOTICES.txt +3 -3
- package/dist/vendor/inventory.json +7 -7
- package/dist/vendor/mermaid.min.js +95 -95
- package/docs/llms/rikiki-reference.md +31 -4
- package/llms.txt +1 -1
- package/package.json +13 -6
- package/dist/contrast.js +0 -1
- package/dist/shared/contrast.d.ts +0 -52
- /package/dist/{shared/annotation-marks.d.ts → annotation-marks.d.ts} +0 -0
- /package/dist/{shared/bar-segments.d.ts → bar-segments.d.ts} +0 -0
- /package/dist/{infrastructure/browser-location.d.ts → browser-location.d.ts} +0 -0
- /package/dist/{shared/cards-syntax.d.ts → cards-syntax.d.ts} +0 -0
- /package/dist/{runtime/color.d.ts → color.d.ts} +0 -0
- /package/dist/{extras/deck-agenda.d.ts → deck-agenda.d.ts} +0 -0
- /package/dist/{extras/deck-annotate.d.ts → deck-annotate.d.ts} +0 -0
- /package/dist/{atoms/deck-badge.d.ts → deck-badge.d.ts} +0 -0
- /package/dist/{extras/deck-bar.d.ts → deck-bar.d.ts} +0 -0
- /package/dist/{layouts/deck-bento.d.ts → deck-bento.d.ts} +0 -0
- /package/dist/{molecules/deck-callout.d.ts → deck-callout.d.ts} +0 -0
- /package/dist/{molecules/deck-card.d.ts → deck-card.d.ts} +0 -0
- /package/dist/{molecules/deck-cell.d.ts → deck-cell.d.ts} +0 -0
- /package/dist/{extras/deck-checklist.d.ts → deck-checklist.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/{molecules/deck-csv.d.ts → deck-csv.d.ts} +0 -0
- /package/dist/{layouts/deck-feature-cards.d.ts → deck-feature-cards.d.ts} +0 -0
- /package/dist/{layouts/deck-feature.d.ts → deck-feature.d.ts} +0 -0
- /package/dist/{extras/deck-figure.d.ts → deck-figure.d.ts} +0 -0
- /package/dist/{molecules/deck-fit.d.ts → deck-fit.d.ts} +0 -0
- /package/dist/{extras/deck-flow.d.ts → deck-flow.d.ts} +0 -0
- /package/dist/{extras/deck-graph.d.ts → deck-graph.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/{extras/deck-icon.d.ts → deck-icon.d.ts} +0 -0
- /package/dist/{extras/deck-kpi-grid.d.ts → deck-kpi-grid.d.ts} +0 -0
- /package/dist/{domain/deck-link.d.ts → deck-link.d.ts} +0 -0
- /package/dist/{molecules/deck-md.d.ts → deck-md.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/{domain/deck-outline.d.ts → deck-outline.d.ts} +0 -0
- /package/dist/{runtime/deck-overview.d.ts → deck-overview.d.ts} +0 -0
- /package/dist/{extras/deck-persona.d.ts → deck-persona.d.ts} +0 -0
- /package/dist/{layouts/deck-photo.d.ts → deck-photo.d.ts} +0 -0
- /package/dist/{molecules/deck-point.d.ts → deck-point.d.ts} +0 -0
- /package/dist/{runtime/deck-presenter.d.ts → deck-presenter.d.ts} +0 -0
- /package/dist/{extras/deck-pull.d.ts → deck-pull.d.ts} +0 -0
- /package/dist/{atoms/deck-punch.d.ts → deck-punch.d.ts} +0 -0
- /package/dist/{extras/deck-quote.d.ts → deck-quote.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/{atoms/deck-source.d.ts → deck-source.d.ts} +0 -0
- /package/dist/{layouts/deck-split.d.ts → deck-split.d.ts} +0 -0
- /package/dist/{molecules/deck-stack.d.ts → deck-stack.d.ts} +0 -0
- /package/dist/{molecules/deck-stat.d.ts → deck-stat.d.ts} +0 -0
- /package/dist/{molecules/deck-step-list.d.ts → deck-step-list.d.ts} +0 -0
- /package/dist/{extras/deck-table.d.ts → deck-table.d.ts} +0 -0
- /package/dist/{layouts/deck-takeaway.d.ts → deck-takeaway.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/dist/{extras/deck-versus.d.ts → deck-versus.d.ts} +0 -0
- /package/dist/{application/deep-link.d.ts → deep-link.d.ts} +0 -0
- /package/dist/{shared/escape-html.d.ts → escape-html.d.ts} +0 -0
- /package/dist/{shared/fit-controller.d.ts → fit-controller.d.ts} +0 -0
- /package/dist/{shared/graph-layout.d.ts → graph-layout.d.ts} +0 -0
- /package/dist/{shared/grid-tracks.d.ts → grid-tracks.d.ts} +0 -0
- /package/dist/{shared/icon-set.d.ts → icon-set.d.ts} +0 -0
- /package/dist/{application/keymap.d.ts → keymap.d.ts} +0 -0
- /package/dist/{application/mouse-nav.d.ts → mouse-nav.d.ts} +0 -0
- /package/dist/{domain/navigation.d.ts → navigation.d.ts} +0 -0
- /package/dist/{shared/parse-csv.d.ts → parse-csv.d.ts} +0 -0
- /package/dist/{plugins/shiki.d.ts → shiki.d.ts} +0 -0
- /package/dist/{extras/signature.d.ts → signature.d.ts} +0 -0
- /package/dist/{shared/slide-fill.d.ts → slide-fill.d.ts} +0 -0
- /package/dist/{domain/viewport.d.ts → viewport.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
A tiny **Lit Web Components** framework for technical presentations. Drop a folder anywhere, open `index.html`, give the talk. No build step on the consumer side · the framework itself is built from TypeScript, but the output is plain ES modules you import directly.
|
|
8
8
|
|
|
9
|
-
This documentation tracks rikiki v0.7.
|
|
9
|
+
This documentation tracks rikiki v0.7.2.
|
|
10
10
|
|
|
11
11
|
## TL;DR
|
|
12
12
|
|
package/bin/lib/browser.mjs
CHANGED
|
@@ -52,6 +52,29 @@ export async function loadChromium() {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Launch the full Chromium build rather than Playwright's default headless shell.
|
|
57
|
+
*
|
|
58
|
+
* The shell positions glyphs differently: a paragraph that wraps onto six lines
|
|
59
|
+
* in Chrome can fit on five there, so a card that spills in every real browser
|
|
60
|
+
* measured clean and `CONTENT_ESCAPES_BOX` stayed silent. The full build wraps
|
|
61
|
+
* like the browser the deck is actually opened in. When it is not installed the
|
|
62
|
+
* default build still runs, and `note` says the measurements may be off.
|
|
63
|
+
*/
|
|
64
|
+
export async function launchChromium(chromium, note = (line) => console.error(line)) {
|
|
65
|
+
try {
|
|
66
|
+
return await chromium.launch({ channel: 'chromium' });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
69
|
+
if (!message.includes("Executable doesn't exist")) throw e;
|
|
70
|
+
note(
|
|
71
|
+
'rikiki · note · the full Chromium build is not installed · falling back to the headless shell, ' +
|
|
72
|
+
'whose text wrapping differs from a real browser · run `npx playwright install chromium`',
|
|
73
|
+
);
|
|
74
|
+
return chromium.launch();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/** True when `abs` is inside `root` · `/srv/deck` must not admit `/srv/deck-x`.
|
|
56
79
|
* Exported because a path check nobody can test is a path check nobody trusts. */
|
|
57
80
|
export function isInside(root, abs) {
|
|
@@ -248,7 +271,7 @@ export async function withDeck(deckPath, fn, { timeoutMs = PAGE_LOAD_TIMEOUT_MS,
|
|
|
248
271
|
const chromium = await loadChromium();
|
|
249
272
|
const { rootDir, urlPath } = deckLocation(deckPath);
|
|
250
273
|
const server = await serveDir(rootDir);
|
|
251
|
-
const browser = await chromium
|
|
274
|
+
const browser = await launchChromium(chromium);
|
|
252
275
|
const missing = [];
|
|
253
276
|
const errors = [];
|
|
254
277
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface CheckFinding {
|
|
2
|
+
element?: Element;
|
|
3
|
+
key?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
suggestion?: string;
|
|
6
|
+
measurement?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface CheckContext {
|
|
9
|
+
readonly slide: number;
|
|
10
|
+
readonly state: number;
|
|
11
|
+
readonly profile: string;
|
|
12
|
+
query<T extends Element = HTMLElement>(selector: string, options?: { shadow?: boolean }): T[];
|
|
13
|
+
report(finding: CheckFinding): void;
|
|
14
|
+
}
|
|
15
|
+
export interface CheckRule {
|
|
16
|
+
code: string;
|
|
17
|
+
scope: 'document' | 'slide' | 'state';
|
|
18
|
+
severity: 'error' | 'warning';
|
|
19
|
+
profiles?: string[];
|
|
20
|
+
run(ctx: CheckContext): void | Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface CheckPlugin {
|
|
23
|
+
apiVersion: 1;
|
|
24
|
+
id: string;
|
|
25
|
+
version: string;
|
|
26
|
+
rules: CheckRule[];
|
|
27
|
+
}
|
|
28
|
+
export function defineChecks(plugin: CheckPlugin): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Register only while `rikiki check` has installed its browser bridge. */
|
|
2
|
+
export function defineChecks(plugin) {
|
|
3
|
+
const bridge = globalThis[Symbol.for('rikiki.checks.v1')];
|
|
4
|
+
if (!bridge) throw new Error('CheckPlugin must be loaded by rikiki check (API 1)');
|
|
5
|
+
bridge.define(plugin);
|
|
6
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const CHECK_API_VERSION = 1;
|
|
7
|
+
const failure = (plugin, code, message) => ({ plugin, code, severity: 'error', message });
|
|
8
|
+
|
|
9
|
+
export function findCheckConfig(deckPath, explicit) {
|
|
10
|
+
if (explicit) return resolve(explicit);
|
|
11
|
+
let dir = dirname(resolve(deckPath));
|
|
12
|
+
for (;;) {
|
|
13
|
+
const file = join(dir, 'rikiki.config.json');
|
|
14
|
+
if (existsSync(file)) return file;
|
|
15
|
+
const parent = dirname(dir);
|
|
16
|
+
if (parent === dir) return null;
|
|
17
|
+
dir = parent;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Data only: no dependency's Node entry point is evaluated. */
|
|
22
|
+
export function resolveCheckPlugins(deckPath, { config, plugins = [], noPlugins = false } = {}) {
|
|
23
|
+
const result = { plugins: [], diagnostics: [], notChecked: [], config: {} };
|
|
24
|
+
if (noPlugins) { result.notChecked.push('plugin checks · explicitly disabled'); return result; }
|
|
25
|
+
let file;
|
|
26
|
+
try {
|
|
27
|
+
file = findCheckConfig(deckPath, config);
|
|
28
|
+
result.config = file ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
29
|
+
if (!result.config || typeof result.config !== 'object' || Array.isArray(result.config)
|
|
30
|
+
|| (result.config.plugins !== undefined && !Array.isArray(result.config.plugins))) throw new Error('plugins must be an array');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
result.diagnostics.push(failure('rikiki', 'CHECK_CONFIG_INVALID', `Cannot load check configuration: ${error.message}`));
|
|
33
|
+
result.notChecked.push('plugin checks · invalid configuration');
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
const base = file ? dirname(file) : dirname(resolve(deckPath));
|
|
37
|
+
const require = createRequire(pathToFileURL(join(base, 'package.json')));
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
const namespaces = new Set();
|
|
40
|
+
for (const entry of [...(result.config.plugins ?? []), ...plugins]) {
|
|
41
|
+
const spec = typeof entry === 'string' ? { package: entry } : entry;
|
|
42
|
+
let id = spec?.package ?? spec?.manifest ?? 'unknown';
|
|
43
|
+
try {
|
|
44
|
+
if (!spec || typeof spec !== 'object') throw new Error('expected a package name or a plugin descriptor');
|
|
45
|
+
const manifestPath = spec.manifest
|
|
46
|
+
? resolve(base, spec.manifest)
|
|
47
|
+
: require.resolve(`${spec.package}/rikiki.module.json`);
|
|
48
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
49
|
+
id = manifest.id;
|
|
50
|
+
if (manifest.schemaVersion !== 1 || typeof id !== 'string' || !/^[a-zA-Z0-9@/._-]+$/.test(id)
|
|
51
|
+
|| typeof manifest.version !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(manifest.namespace ?? '')
|
|
52
|
+
|| manifest.checks?.apiVersion !== CHECK_API_VERSION) throw new Error('unsupported module schema or check API');
|
|
53
|
+
const profile = spec.profile ?? 'recommended';
|
|
54
|
+
if (!manifest.checks.profiles?.includes(profile)) throw new Error(`unknown profile: ${profile}`);
|
|
55
|
+
const entryPath = manifest.checks.entry;
|
|
56
|
+
if (typeof entryPath !== 'string' || isAbsolute(entryPath)) throw new Error('checks.entry must be package-relative');
|
|
57
|
+
const root = realpathSync(dirname(manifestPath));
|
|
58
|
+
const script = realpathSync(resolve(root, entryPath));
|
|
59
|
+
const rel = relative(root, script);
|
|
60
|
+
if (rel.startsWith('..') || isAbsolute(rel)) throw new Error('checks.entry escapes the module directory');
|
|
61
|
+
const identity = JSON.stringify([realpathSync(manifestPath), manifest.version, profile]);
|
|
62
|
+
if (seen.has(id)) {
|
|
63
|
+
if (seen.get(id) !== identity) throw new Error(`conflicting versions or profiles of ${id}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (namespaces.has(manifest.namespace)) throw new Error(`duplicate namespace ${manifest.namespace}`);
|
|
67
|
+
seen.set(id, identity);
|
|
68
|
+
namespaces.add(manifest.namespace);
|
|
69
|
+
const skills = (manifest.skills ?? []).map(skill => {
|
|
70
|
+
if (!skill || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.name) || typeof skill.path !== 'string') throw new Error('invalid skill descriptor');
|
|
71
|
+
const src = realpathSync(resolve(root, skill.path));
|
|
72
|
+
const rel = relative(root, src);
|
|
73
|
+
if (rel.startsWith('..') || isAbsolute(rel) || !existsSync(join(src, 'SKILL.md'))) throw new Error('invalid skill path');
|
|
74
|
+
return { name: skill.name, src };
|
|
75
|
+
});
|
|
76
|
+
result.plugins.push({ id, version: manifest.version, namespace: manifest.namespace, apiVersion: CHECK_API_VERSION,
|
|
77
|
+
profile, script, skills, status: 'pending', rules: [], executed: [] });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
result.diagnostics.push(failure(String(id), 'CHECK_PLUGIN_LOAD_FAILED', `Cannot load ${id}: ${error.message}`));
|
|
80
|
+
result.notChecked.push(`plugin ${id} · loading failed`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!result.plugins.length && !result.diagnostics.length) result.notChecked.push('plugin checks · no plugin configured');
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Serialized into the browser. This registry has no imports or component side effects. */
|
|
88
|
+
export function installCheckBridge() {
|
|
89
|
+
const registered = new Map();
|
|
90
|
+
const key = Symbol.for('rikiki.checks.v1');
|
|
91
|
+
const query = (root, selector, shadow = false) => {
|
|
92
|
+
const found = [...root.querySelectorAll(selector)];
|
|
93
|
+
if (root.matches?.(selector)) found.unshift(root);
|
|
94
|
+
if (shadow) for (const el of root.querySelectorAll('*')) {
|
|
95
|
+
if (el.shadowRoot) found.push(...query(el.shadowRoot, selector, true));
|
|
96
|
+
}
|
|
97
|
+
return [...new Set(found)];
|
|
98
|
+
};
|
|
99
|
+
const pathOf = (element) => {
|
|
100
|
+
const parts = [];
|
|
101
|
+
for (let el = element; el && el !== document.body;) {
|
|
102
|
+
const tag = el.localName;
|
|
103
|
+
if (!tag) break;
|
|
104
|
+
const siblings = el.parentElement ? [...el.parentElement.children].filter(n => n.localName === tag) : [];
|
|
105
|
+
parts.unshift(el.id ? `${tag}#${CSS.escape(el.id)}` : `${tag}:nth-of-type(${Math.max(1, siblings.indexOf(el) + 1)})`);
|
|
106
|
+
const host = el.getRootNode().host;
|
|
107
|
+
if (!el.parentElement && host) { parts.unshift('::shadow'); el = host; }
|
|
108
|
+
else el = el.parentElement;
|
|
109
|
+
}
|
|
110
|
+
return parts.join(' > ');
|
|
111
|
+
};
|
|
112
|
+
const api = {
|
|
113
|
+
define(plugin) {
|
|
114
|
+
if (plugin.apiVersion !== 1 || !plugin.id || !plugin.version || !Array.isArray(plugin.rules)) throw new Error('Invalid CheckPlugin');
|
|
115
|
+
if (registered.has(plugin.id)) throw new Error(`Duplicate CheckPlugin: ${plugin.id}`);
|
|
116
|
+
const codes = new Set();
|
|
117
|
+
for (const rule of plugin.rules) {
|
|
118
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(rule.code) || codes.has(rule.code) || typeof rule.run !== 'function'
|
|
119
|
+
|| !['document', 'slide', 'state'].includes(rule.scope) || !['error', 'warning'].includes(rule.severity)) throw new Error('Invalid or duplicate check rule');
|
|
120
|
+
codes.add(rule.code);
|
|
121
|
+
}
|
|
122
|
+
registered.set(plugin.id, plugin);
|
|
123
|
+
},
|
|
124
|
+
describe(id) {
|
|
125
|
+
const p = registered.get(id);
|
|
126
|
+
return p ? { id: p.id, version: p.version, apiVersion: p.apiVersion,
|
|
127
|
+
rules: p.rules.map(({ code, scope, severity, profiles }) => ({ code, scope, severity, profiles })) } : null;
|
|
128
|
+
},
|
|
129
|
+
async run({ id, profile, slide, state, documentPass }) {
|
|
130
|
+
const plugin = registered.get(id);
|
|
131
|
+
const root = document.querySelector('deck-root');
|
|
132
|
+
const slides = root ? [...root.children].filter(e => e.localName.startsWith('deck-') && e.localName !== 'deck-root') : [];
|
|
133
|
+
const active = slides[slide - 1];
|
|
134
|
+
const out = [], executed = [], failures = [];
|
|
135
|
+
for (const rule of plugin.rules) {
|
|
136
|
+
if (rule.profiles && !rule.profiles.includes(profile)) continue;
|
|
137
|
+
if (rule.scope === 'document' ? !documentPass : rule.scope === 'slide' ? state !== 0 || !active : !active) continue;
|
|
138
|
+
const scope = rule.scope === 'document' ? root ?? document : active;
|
|
139
|
+
try {
|
|
140
|
+
for (const el of query(scope, '*', true)) if (el.updateComplete) await el.updateComplete;
|
|
141
|
+
await rule.run({ slide, state, profile,
|
|
142
|
+
query: (selector, options = {}) => query(scope, selector, options.shadow ?? false),
|
|
143
|
+
report: (finding) => {
|
|
144
|
+
if (!finding || typeof finding.message !== 'string' || !finding.message.trim()
|
|
145
|
+
|| (finding.key !== undefined && typeof finding.key !== 'string')
|
|
146
|
+
|| (finding.suggestion !== undefined && typeof finding.suggestion !== 'string')
|
|
147
|
+
|| (finding.element !== undefined && !(finding.element instanceof Element))) throw new Error('Invalid diagnostic');
|
|
148
|
+
const el = finding.element;
|
|
149
|
+
const owner = el ? slides.findIndex(s => s === el || s.contains(el) || query(s, '*', true).includes(el)) : -1;
|
|
150
|
+
const measurement = finding.measurement === undefined ? undefined : JSON.parse(JSON.stringify(finding.measurement));
|
|
151
|
+
out.push({ code: rule.code, severity: rule.severity, message: finding.message, suggestion: finding.suggestion,
|
|
152
|
+
element: el ? pathOf(el) : undefined, key: finding.key,
|
|
153
|
+
slide: owner >= 0 ? owner + 1 : rule.scope !== 'document' ? slide : undefined,
|
|
154
|
+
state: rule.scope === 'document' ? undefined : state, measurement });
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
executed.push(rule.code);
|
|
158
|
+
} catch (error) { failures.push({ code: rule.code, message: String(error?.message ?? error) }); }
|
|
159
|
+
}
|
|
160
|
+
return { diagnostics: out, executed, failures };
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
Object.defineProperty(globalThis, key, { value: api, configurable: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A Node timer closes the page even when synchronous plugin code blocks its JS thread. */
|
|
167
|
+
async function bounded(page, operation, timeoutMs) {
|
|
168
|
+
let timer;
|
|
169
|
+
try {
|
|
170
|
+
return await Promise.race([operation(), new Promise((_, reject) => {
|
|
171
|
+
timer = setTimeout(() => {
|
|
172
|
+
reject(new Error('Check plugin execution timed out'));
|
|
173
|
+
void page.close({ runBeforeUnload: false }).catch(() => {});
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
})]);
|
|
176
|
+
} finally { clearTimeout(timer); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function startCheckPlugins(page, resolution, timeoutMs = 5000) {
|
|
180
|
+
if (!resolution.plugins.length) return;
|
|
181
|
+
await page.evaluate(installCheckBridge);
|
|
182
|
+
for (const plugin of resolution.plugins) {
|
|
183
|
+
try {
|
|
184
|
+
await bounded(page, () => page.addScriptTag({ content: readFileSync(plugin.script, 'utf8') }), timeoutMs);
|
|
185
|
+
const described = await bounded(page, () => page.evaluate(id => globalThis[Symbol.for('rikiki.checks.v1')].describe(id), plugin.id), timeoutMs);
|
|
186
|
+
if (!described || described.version !== plugin.version || described.apiVersion !== CHECK_API_VERSION
|
|
187
|
+
|| described.rules.some(r => !r.code.startsWith(plugin.namespace + '_'))) throw new Error('Registration does not match the manifest');
|
|
188
|
+
plugin.rules = described.rules.filter(r => !r.profiles || r.profiles.includes(plugin.profile));
|
|
189
|
+
plugin.status = 'loaded';
|
|
190
|
+
} catch (error) {
|
|
191
|
+
plugin.status = 'failed';
|
|
192
|
+
resolution.diagnostics.push(failure(plugin.id, 'CHECK_PLUGIN_LOAD_FAILED', error.message));
|
|
193
|
+
resolution.notChecked.push(`plugin ${plugin.id} · registration failed`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function runCheckPlugins(page, resolution, { slide, state, documentPass }, timeoutMs = 5000) {
|
|
199
|
+
const diagnostics = [];
|
|
200
|
+
for (const plugin of resolution.plugins.filter(p => p.status === 'loaded')) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await bounded(page, () => page.evaluate(args => globalThis[Symbol.for('rikiki.checks.v1')].run(args),
|
|
203
|
+
{ id: plugin.id, profile: plugin.profile, slide, state, documentPass }), timeoutMs);
|
|
204
|
+
if (!result || !Array.isArray(result.diagnostics) || !Array.isArray(result.failures) || !Array.isArray(result.executed)) throw new Error('Invalid plugin result');
|
|
205
|
+
for (const d of result.diagnostics) {
|
|
206
|
+
const rule = plugin.rules.find(r => r.code === d.code);
|
|
207
|
+
if (!rule || d.severity !== rule.severity || typeof d.message !== 'string') throw new Error('Invalid plugin diagnostic');
|
|
208
|
+
diagnostics.push({ ...d, plugin: plugin.id });
|
|
209
|
+
}
|
|
210
|
+
plugin.executed = [...new Set([...plugin.executed, ...result.executed])];
|
|
211
|
+
for (const err of result.failures) diagnostics.push(failure(plugin.id, 'CHECK_RULE_FAILED', `${err.code}: ${err.message}`));
|
|
212
|
+
if (result.failures.length) {
|
|
213
|
+
plugin.status = 'failed';
|
|
214
|
+
resolution.notChecked.push(`plugin ${plugin.id} · rule execution failed`);
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
plugin.status = 'failed';
|
|
218
|
+
diagnostics.push(failure(plugin.id, 'CHECK_PLUGIN_FAILED', error.message));
|
|
219
|
+
resolution.notChecked.push(`plugin ${plugin.id} · execution incomplete`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return diagnostics;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function pluginReport(resolution) {
|
|
226
|
+
return resolution.plugins.map(({ script, skills, ...plugin }) => ({ ...plugin,
|
|
227
|
+
status: plugin.status === 'loaded' ? 'completed' : plugin.status }));
|
|
228
|
+
}
|