konpeki 0.3.0 → 0.4.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/AGENTS.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
package/html/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# HTML, files, theme, and CLI
|
|
2
|
+
|
|
3
|
+
This is the reference for Konpeki's source contract and runtime behavior. The
|
|
4
|
+
[skill](../skills/konpeki/SKILL.md) owns workflow, its [floor](../skills/konpeki/floor.md)
|
|
5
|
+
owns bans and defaults, and [AUTHORING.md](../AUTHORING.md) owns editorial and
|
|
6
|
+
visual judgment.
|
|
7
|
+
|
|
8
|
+
## Document and file contract
|
|
9
|
+
|
|
10
|
+
- Author static HTML/CSS and inline SVG. Do not author JavaScript, applications,
|
|
11
|
+
interactive controls, or remote resources.
|
|
12
|
+
- Each page is an explicit direct child of `<body>` with `data-page` and a
|
|
13
|
+
globally unique, stable `id`. Targetable HTML and SVG elements also need
|
|
14
|
+
globally unique, stable IDs.
|
|
15
|
+
- CSS defines fixed page width and height, at most 8192 CSS px per side. Pages
|
|
16
|
+
may differ in size. Page breaks are explicit; overflow is not paginated.
|
|
17
|
+
- Keep images, stylesheets, and licensed fonts beside the HTML with relative
|
|
18
|
+
paths, or embed them. The only remote exception is Google Fonts from
|
|
19
|
+
`fonts.googleapis.com/css2` and `fonts.gstatic.com`; it requires network access.
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8">
|
|
26
|
+
<link rel="stylesheet" href="theme.css">
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<main id="page-1" data-page data-size="link">
|
|
30
|
+
<h1 id="title">A clear headline</h1>
|
|
31
|
+
</main>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Themes
|
|
37
|
+
|
|
38
|
+
Link `theme.css` for the default light-blue theme. Read [Use a theme](theme.md)
|
|
39
|
+
for its `--kp-`-prefixed public tokens, complete type roles, page presets, and asset
|
|
40
|
+
requirements. A compatible entry point sets `--kp-theme: 1`; the base alone does
|
|
41
|
+
not. Keep the imported `theme-base.css` and referenced fonts beside the entry
|
|
42
|
+
point. Adapt a copy of the default when a brief brings its own look.
|
|
43
|
+
|
|
44
|
+
For deliberately unthemed content, set `data-theme="custom"` on its ancestor.
|
|
45
|
+
This skips theme-value checks in that subtree and definition checks on an
|
|
46
|
+
opted-out page, but keeps layout and resource checks. An alternative implementing
|
|
47
|
+
the contract does not need this opt-out.
|
|
48
|
+
|
|
49
|
+
## Diagnostics
|
|
50
|
+
|
|
51
|
+
`inspect` detects page and text overflow, clipped text, missing resources,
|
|
52
|
+
inter-element text line-box overlap, and opaque sRGB HTML text contrast against
|
|
53
|
+
solid ancestor backgrounds. Contrast thresholds are 3:1 for large text and
|
|
54
|
+
4.5:1 otherwise. Contrast inspection skips SVG text, gradients, images,
|
|
55
|
+
transparency, and compositing it cannot evaluate reliably.
|
|
56
|
+
|
|
57
|
+
Floor warnings use the rule ID from the [authoring floor](../skills/konpeki/floor.md)
|
|
58
|
+
as their code. `label-above-headline` flags a short, smaller line directly above
|
|
59
|
+
a title, display or section heading in the same column; running heads repeated
|
|
60
|
+
across A4 pages, full sentences and metric figures are not flagged, and
|
|
61
|
+
`data-kp-allow="label-above-headline"` on the label records a deliberate
|
|
62
|
+
exception. `stranded-word` flags a headline line that holds one word after a fuller
|
|
63
|
+
line, within each `<br>`-separated part; a line a `<br>` isolates is deliberate. The base balances headline roles and
|
|
64
|
+
sets `text-wrap: pretty` on pages to avoid most stranded words.
|
|
65
|
+
|
|
66
|
+
`small-text` warns below 24 CSS px on screen presets and 11 CSS px on A4,
|
|
67
|
+
including unthemed HTML and SVG text. The default preset is presentation.
|
|
68
|
+
These are computed font-size floors, not a guarantee of legibility after SVG
|
|
69
|
+
viewBox scaling, CSS transforms, or reduction to the final viewing size. Unknown
|
|
70
|
+
custom presets have no assumed floor. Review screen graphics at their intended
|
|
71
|
+
size and print pages at actual size; warnings do not block export.
|
|
72
|
+
|
|
73
|
+
For opted-in themes, it also checks the active definition and compares used
|
|
74
|
+
size/leading pairs, colors and shape treatments to its tokens. See the [theme diagnostics](theme-authoring.md#verify-a-theme)
|
|
75
|
+
for required roles and contrast pairs. `inspect` includes each page's resolved
|
|
76
|
+
type treatments under `theme.type`, so authors can plan with the actual sizes.
|
|
77
|
+
|
|
78
|
+
CLI inspection checks which fonts actually drew the text. Installed-font fallback
|
|
79
|
+
is an error, including on unthemed pages. Both CLI and preview check declared
|
|
80
|
+
weight/style coverage; only CLI can audit actual glyph fallback. Load the required
|
|
81
|
+
licensed faces and subsets from Google Fonts, local files, or embedded data
|
|
82
|
+
instead of relying on the machine. The default theme uses Google Fonts and needs
|
|
83
|
+
network access during preview and export; finished PNGs and PDFs do not.
|
|
84
|
+
|
|
85
|
+
HTML overlap uses estimated line-height boxes rather than raw font rectangles;
|
|
86
|
+
SVG, normal leading and transformed text retain range bounds. These are not glyph
|
|
87
|
+
ink measurements, and automated checks cannot establish factual
|
|
88
|
+
correctness or visual quality. Inspect rendered output. A render with diagnostic
|
|
89
|
+
errors exits nonzero and writes no output.
|
|
90
|
+
|
|
91
|
+
## CLI
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
konpeki validate document.html
|
|
95
|
+
konpeki inspect document.html [--page N] [--details]
|
|
96
|
+
konpeki check document.html
|
|
97
|
+
konpeki render document.html [--page N] [--format png|pdf] [--scale 2] [--output file]
|
|
98
|
+
konpeki preview document.html [--host host] [--port port] [--json]
|
|
99
|
+
konpeki browser install
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Pages are one-based. HTML export supports PNG and PDF only. PNG defaults to page
|
|
103
|
+
1; PDF includes all pages unless `--page N` is supplied and preserves mixed page
|
|
104
|
+
sizes. `--scale` affects PNG only. Outputs are exclusive and never overwritten.
|
|
105
|
+
|
|
106
|
+
Inspection and export require the pinned Chromium installed by `browser install`;
|
|
107
|
+
preview does not. Preview chooses another available port when its default is
|
|
108
|
+
busy, while an explicit `--port` is strict.
|
|
109
|
+
|
|
110
|
+
Preview comments remain browser-local. **Copy & clear** copies the full local
|
|
111
|
+
document path, comments, and target IDs for pasting into an agent conversation.
|
|
112
|
+
Small moves edit translation only; they do not reorder source. Deletion may
|
|
113
|
+
reflow content. Reread current source and preserve unrelated edits when applying
|
|
114
|
+
feedback.
|
|
115
|
+
|
|
116
|
+
## Keyboard review
|
|
117
|
+
|
|
118
|
+
Open **Keyboard shortcuts** (`?`) and enable **Vim review mode**. The preference
|
|
119
|
+
is remembered in this browser. With it off, the existing mouse and comment
|
|
120
|
+
shortcuts are unchanged.
|
|
121
|
+
|
|
122
|
+
| Context | Keys | Action |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| Canvas | `f`, then hint letters | Comment on a visible element of the current page |
|
|
125
|
+
| Canvas | `c` / `Shift+C` | Comment on the selection / entire current page |
|
|
126
|
+
| Canvas | `h j k l` | Pan left, down, up, right |
|
|
127
|
+
| Canvas | `[` / `]`, `gg` / `G` | Previous / next page, first / last page; keep zoom |
|
|
128
|
+
| Canvas | `+` or `=`, `-`, `0` | Zoom in, zoom out, fit and center current page |
|
|
129
|
+
| Canvas | `q` | Open all pending comments |
|
|
130
|
+
| Comment | `⌘/Ctrl+Enter` | Add and return to the canvas without changing the viewport |
|
|
131
|
+
| Pending comments | `j` / `k`, `Enter` | Select a comment, reveal its target and notes |
|
|
132
|
+
| Pending comments | `⌘/Ctrl+Enter` | Copy & clear; comments stay intact if copying fails |
|
|
133
|
+
| Review | `Esc` | Back one level, preserving unfinished drafts until reload |
|
|
134
|
+
|
|
135
|
+
The current page follows navigation and panning. Hints use stable element IDs,
|
|
136
|
+
exclude hidden/offscreen targets, and keep their codes while filtering.
|
|
137
|
+
`Backspace` corrects a hint prefix; `Esc` cancels. Moving the viewport or updating
|
|
138
|
+
the source cancels hints rather than reusing stale geometry. Letter shortcuts are
|
|
139
|
+
suspended inside text inputs and during IME composition. Review mode does not
|
|
140
|
+
add Vim text editing or bare-letter source-deletion commands.
|
package/html/browser.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { PDFDocument } from "pdf-lib";
|
|
3
|
+
import { documentServer } from "./server.ts";
|
|
4
|
+
import { inspectHTMLPage, type Diagnostic } from "./inspect.ts";
|
|
5
|
+
import { isGoogleFontResource } from "./document.ts";
|
|
6
|
+
|
|
7
|
+
export async function browserDocument(input: string, options: { page?: number; format?: string; scale?: number; details?: boolean } = {}) {
|
|
8
|
+
if (options.format !== undefined && !["png", "pdf"].includes(options.format)) throw new Error("HTML exports support PNG and PDF. Whole-page SVG export is not supported.");
|
|
9
|
+
const scale = options.scale ?? 2;
|
|
10
|
+
if (!Number.isFinite(scale) || scale <= 0 || scale > 8) throw new Error("Scale must be greater than 0 and at most 8.");
|
|
11
|
+
const server = await documentServer(input);
|
|
12
|
+
let browser;
|
|
13
|
+
try {
|
|
14
|
+
const snapshot = await server.store.read();
|
|
15
|
+
const selected = options.page;
|
|
16
|
+
if (selected !== undefined && (!Number.isInteger(selected) || selected < 1 || selected > snapshot.pages.length))
|
|
17
|
+
throw new Error(`Page must be an integer from 1 to ${snapshot.pages.length}.`);
|
|
18
|
+
try { browser = await chromium.launch({ headless: true }); }
|
|
19
|
+
catch { throw new Error("Chromium could not start. Run `konpeki browser install` to install the pinned browser; Linux also needs Chromium's system libraries."); }
|
|
20
|
+
const context = await browser.newContext({ viewport: { width: 794, height: 1123 }, deviceScaleFactor: scale, serviceWorkers: "block" });
|
|
21
|
+
// Local document assets plus Google Fonts; other external requests, including
|
|
22
|
+
// those hidden in CSS, remain blocked.
|
|
23
|
+
await context.route("**/*", route => {
|
|
24
|
+
const request = route.request();
|
|
25
|
+
return request.url().startsWith(`${server.origin}${server.prefix}/`) || isGoogleFontResource(request.url(), request.resourceType()) ? route.continue() : route.abort();
|
|
26
|
+
});
|
|
27
|
+
const tab = await context.newPage(), pages = [], diagnostics: Diagnostic[] = [];
|
|
28
|
+
const cdp = await context.newCDPSession(tab);
|
|
29
|
+
await cdp.send("DOM.enable"); await cdp.send("CSS.enable");
|
|
30
|
+
const failed = new Set<string>();
|
|
31
|
+
tab.on("requestfailed", request => failed.add(request.url()));
|
|
32
|
+
tab.on("response", response => { if (!response.ok()) failed.add(response.url()); });
|
|
33
|
+
const pdf = options.format === "pdf" ? await PDFDocument.create() : undefined;
|
|
34
|
+
let bytes: Uint8Array | undefined;
|
|
35
|
+
for (let index = 0; index < snapshot.pages.length; index++) {
|
|
36
|
+
if (selected !== undefined && selected !== index + 1) continue;
|
|
37
|
+
if (options.format === "png" && selected === undefined && index > 0) break;
|
|
38
|
+
const id = snapshot.pages[index];
|
|
39
|
+
await tab.goto("about:blank");
|
|
40
|
+
await tab.setViewportSize({ width: 794, height: 1123 });
|
|
41
|
+
failed.clear();
|
|
42
|
+
const response = await tab.goto(`${server.origin}${server.prefix}/document/?page=${encodeURIComponent(id)}&revision=${snapshot.revision}`);
|
|
43
|
+
if (!response?.ok()) throw new Error("The HTML changed while rendering. Inspect and retry the latest source.");
|
|
44
|
+
await tab.evaluate(async () => {
|
|
45
|
+
await document.fonts.ready;
|
|
46
|
+
await Promise.all(Array.from(document.images).map(image => image.decode().catch(() => {})));
|
|
47
|
+
});
|
|
48
|
+
const element = tab.locator("[data-konpeki-current]");
|
|
49
|
+
const initial = await element.boundingBox();
|
|
50
|
+
if (!initial || initial.width <= 0 || initial.height <= 0 || initial.width > 8192 || initial.height > 8192) throw new Error("Invalid page dimensions. Use an explicit width and height up to 8192 CSS px.");
|
|
51
|
+
await tab.setViewportSize({ width: Math.ceil(initial.width), height: Math.ceil(initial.height) });
|
|
52
|
+
await tab.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); });
|
|
53
|
+
await tab.waitForLoadState("networkidle");
|
|
54
|
+
const report = await element.evaluate(inspectHTMLPage);
|
|
55
|
+
// CSS font-family and FontFaceSet cannot tell which font supplied a glyph.
|
|
56
|
+
// Chromium can: inspect direct text children, retaining their nearest ID
|
|
57
|
+
// even when an inline span has no ID. Include generated text as well.
|
|
58
|
+
const { root } = await cdp.send("DOM.getDocument", { depth: -1, pierce: true });
|
|
59
|
+
const { nodeId } = await cdp.send("DOM.querySelector", { nodeId: root.nodeId, selector: "[data-konpeki-current]" });
|
|
60
|
+
const { node } = await cdp.send("DOM.describeNode", { nodeId, depth: -1, pierce: true });
|
|
61
|
+
const fallbacks = new Map<string, Set<string>>();
|
|
62
|
+
async function auditFonts(node: typeof root, target: string) {
|
|
63
|
+
if (node.nodeType !== 1) return;
|
|
64
|
+
const attrs = node.attributes ?? [], idIndex = attrs.indexOf("id");
|
|
65
|
+
if (idIndex >= 0) target = attrs[idIndex + 1];
|
|
66
|
+
const { computedStyle } = await cdp.send("CSS.getComputedStyleForNode", { nodeId: node.nodeId });
|
|
67
|
+
const style = Object.fromEntries(computedStyle.map(s => [s.name, s.value]));
|
|
68
|
+
if (style.display === "none" || Number(style.opacity) === 0) return;
|
|
69
|
+
if (style.visibility === "visible" && (node.pseudoType || node.children?.some(child => child.nodeType === 3 && child.nodeValue.trim()))) {
|
|
70
|
+
const { fonts } = await cdp.send("CSS.getPlatformFontsForNode", { nodeId: node.nodeId });
|
|
71
|
+
for (const font of fonts) if (font.glyphCount > 0 && !font.isCustomFont) {
|
|
72
|
+
if (!fallbacks.has(target)) fallbacks.set(target, new Set());
|
|
73
|
+
fallbacks.get(target)!.add(font.familyName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const child of [...node.children ?? [], ...node.pseudoElements ?? []]) await auditFonts(child, target);
|
|
77
|
+
}
|
|
78
|
+
await auditFonts(node, id);
|
|
79
|
+
for (const [target, fonts] of fallbacks) report.diagnostics.push({ code: "font-fallback", severity: "error", page: id, target,
|
|
80
|
+
message: `Text was drawn with installed fonts: ${[...fonts].join(", ")}. Load webfonts covering its families, styles, weights and glyphs from Google Fonts, local files or embedded data instead of relying on this machine.` });
|
|
81
|
+
if (Math.abs(report.width - initial.width) > .75 || Math.abs(report.height - initial.height) > .75)
|
|
82
|
+
throw new Error("Page dimensions depend on the viewport. Give pages a fixed CSS width and height; their content may use responsive layout.");
|
|
83
|
+
for (const resource of failed) report.diagnostics.push({ code: "missing-resource", severity: "error", page: id, target: id, message: `Resource did not load: ${resource.startsWith(`${server.origin}${server.prefix}/document/`) ? resource.slice(`${server.origin}${server.prefix}/document/`.length) : new URL(resource).origin + new URL(resource).pathname}. Use local assets or check access to Google Fonts.` });
|
|
84
|
+
diagnostics.push(...report.diagnostics);
|
|
85
|
+
pages.push({ pageNumber: index + 1, ...report, ...(options.details ? {} : { text: undefined }) });
|
|
86
|
+
if (options.format === "png") {
|
|
87
|
+
if (report.width * report.height * scale * scale > 64_000_000) throw new Error("PNG exceeds 64 million pixels; reduce --scale.");
|
|
88
|
+
bytes = await element.screenshot({ type: "png", animations: "disabled" });
|
|
89
|
+
} else if (pdf) {
|
|
90
|
+
// Print the screen layout on exactly one authored page, then merge pages
|
|
91
|
+
// to retain mixed paper sizes. Never invent document pagination.
|
|
92
|
+
await tab.emulateMedia({ media: "screen" });
|
|
93
|
+
await tab.addStyleTag({ content: `@page { size: ${report.width}px ${report.height}px; margin: 0; } body { print-color-adjust: exact; }` });
|
|
94
|
+
const part = await PDFDocument.load(await tab.pdf({ preferCSSPageSize: true, printBackground: true, margin: { top: 0, left: 0, right: 0, bottom: 0 } }));
|
|
95
|
+
if (part.getPageCount() !== 1) throw new Error(`Page ${index + 1} printed onto multiple sheets. Fix its overflow before exporting PDF.`);
|
|
96
|
+
for (const page of await pdf.copyPages(part, [0])) pdf.addPage(page);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if ((await server.store.read()).revision !== snapshot.revision) throw new Error("The source changed during rendering. Retry the latest revision.");
|
|
100
|
+
if (pdf) bytes = await pdf.save();
|
|
101
|
+
return { bytes, report: { schema: "konpeki-html-inspection/v1", revision: snapshot.revision, units: "css-pixels", ok: !diagnostics.some(d => d.severity === "error"), pages, diagnostics } };
|
|
102
|
+
} finally { await browser?.close(); await server.close(); }
|
|
103
|
+
}
|
package/html/document.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parse, parseFragment, serialize, type DefaultTreeAdapterMap } from "parse5";
|
|
2
|
+
|
|
3
|
+
export const documentPolicy = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data:; base-uri 'none'; form-action 'none'";
|
|
4
|
+
|
|
5
|
+
/** The only external resources permitted by CLI inspection/export. */
|
|
6
|
+
export function isGoogleFontResource(url: string, resourceType: string) {
|
|
7
|
+
const resource = new URL(url);
|
|
8
|
+
if (resource.protocol !== "https:" || resource.port || resource.username || resource.password) return false;
|
|
9
|
+
return resourceType === "stylesheet" && resource.hostname === "fonts.googleapis.com" && ["/css", "/css2"].includes(resource.pathname)
|
|
10
|
+
|| resourceType === "font" && resource.hostname === "fonts.gstatic.com" && resource.pathname.startsWith("/s/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A view of the source, never a serialization written back to it. Keep every
|
|
14
|
+
* page in the DOM so nth-child selectors and shared SVG definitions stay stable. */
|
|
15
|
+
export function documentHTML(source: string, pageId: string) {
|
|
16
|
+
const tree = parse(source);
|
|
17
|
+
const html = tree.childNodes.find(n => n.nodeName === "html") as DefaultTreeAdapterMap["element"];
|
|
18
|
+
const head = html.childNodes.find(n => n.nodeName === "head") as DefaultTreeAdapterMap["element"];
|
|
19
|
+
const body = html.childNodes.find(n => n.nodeName === "body") as DefaultTreeAdapterMap["element"];
|
|
20
|
+
for (const child of body.childNodes) {
|
|
21
|
+
if (!("attrs" in child)) continue;
|
|
22
|
+
child.attrs = child.attrs.filter(a => a.name !== "data-konpeki-current");
|
|
23
|
+
if (child.attrs.some(a => a.name === "data-page") && child.attrs.some(a => a.name === "id" && a.value === pageId))
|
|
24
|
+
child.attrs.push({ name: "data-konpeki-current", value: "" });
|
|
25
|
+
}
|
|
26
|
+
// Serve at a directory URL: relative assets and SVG #fragments then retain
|
|
27
|
+
// their native meaning without a <base> that redirects fragment references.
|
|
28
|
+
head.childNodes.unshift(...parseFragment(`<meta http-equiv="Content-Security-Policy" content="${documentPolicy}">`).childNodes);
|
|
29
|
+
head.childNodes.push(...parseFragment(`<style>
|
|
30
|
+
html, body { margin: 0 !important; padding: 0 !important; }
|
|
31
|
+
body { display: flex !important; flex-direction: column !important; align-items: flex-start !important; }
|
|
32
|
+
body > [data-page] { margin: 0 !important; flex: none !important; }
|
|
33
|
+
body > [data-page]:not([data-konpeki-current]) { display: none !important; }
|
|
34
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
35
|
+
@media print { body > [data-page] { break-before: auto !important; break-after: auto !important; } }
|
|
36
|
+
</style>`).childNodes);
|
|
37
|
+
return serialize(tree);
|
|
38
|
+
}
|
package/html/floor.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export type FloorRule = { ids: string[]; tier: "ban" | "default" | "verify"; check: "inspect" | "review"; text: string };
|
|
4
|
+
|
|
5
|
+
const tiers: Record<string, FloorRule["tier"]> = { Bans: "ban", Defaults: "default", Verify: "verify" };
|
|
6
|
+
|
|
7
|
+
/** The rules in skills/konpeki/floor.md, the single source for the review checklist. */
|
|
8
|
+
export function floorRules(source = readFileSync(new URL("../skills/konpeki/floor.md", import.meta.url), "utf8")) {
|
|
9
|
+
const rules: FloorRule[] = [];
|
|
10
|
+
let tier: FloorRule["tier"] | undefined;
|
|
11
|
+
for (const line of source.split("\n")) {
|
|
12
|
+
const heading = /^## (\w+)/.exec(line);
|
|
13
|
+
if (heading) { tier = tiers[heading[1]]; continue; }
|
|
14
|
+
const rule = /^- ((?:`[a-z0-9-]+`(?:, )?)+) \((inspect|review)\):(?: (.*))?$/.exec(line);
|
|
15
|
+
if (rule && tier) rules.push({ ids: rule[1].match(/[a-z0-9-]+/g)!, tier, check: rule[2] as FloorRule["check"], text: rule[3] ?? "" });
|
|
16
|
+
else if (/^ {2}\S/.test(line) && rules.length) rules.at(-1)!.text = `${rules.at(-1)!.text} ${line.trim()}`.trim();
|
|
17
|
+
}
|
|
18
|
+
return rules;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The first sentence, ignoring full stops inside quoted examples. */
|
|
22
|
+
function firstSentence(text: string) {
|
|
23
|
+
let quoted = false;
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
if (text[i] === '"') quoted = !quoted;
|
|
26
|
+
else if (!quoted && text[i] === "." && (i + 1 === text.length || text[i + 1] === " ")) return text.slice(0, i + 1);
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The first sentence of each rule that only a person looking at the page can check. */
|
|
32
|
+
export function reviewChecklist(rules = floorRules()) {
|
|
33
|
+
return rules.filter(rule => rule.check === "review").map(rule => `${rule.ids[0]}: ${firstSentence(rule.text)}`);
|
|
34
|
+
}
|
package/html/index.html
ADDED