@kamishibai/sdk 0.1.1
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 +192 -0
- package/package.json +54 -0
- package/src/blocks/board.js +210 -0
- package/src/blocks/callout.js +63 -0
- package/src/blocks/code.js +28 -0
- package/src/blocks/deck.js +76 -0
- package/src/blocks/diagram.js +265 -0
- package/src/blocks/element.js +51 -0
- package/src/blocks/graph.js +264 -0
- package/src/blocks/grid.js +156 -0
- package/src/blocks/index.js +106 -0
- package/src/blocks/list.js +50 -0
- package/src/blocks/placement.js +119 -0
- package/src/blocks/prose.js +28 -0
- package/src/blocks/quote.js +25 -0
- package/src/blocks/raw.js +47 -0
- package/src/blocks/registry.js +158 -0
- package/src/blocks/schema-parts.js +19 -0
- package/src/blocks/section.js +53 -0
- package/src/blocks/slide.js +104 -0
- package/src/blocks/stat.js +83 -0
- package/src/blocks/table.js +50 -0
- package/src/blocks/timeline.js +80 -0
- package/src/cli/commands/close.js +51 -0
- package/src/cli/commands/comments.js +73 -0
- package/src/cli/commands/debug.js +34 -0
- package/src/cli/commands/example.js +22 -0
- package/src/cli/commands/export.js +10 -0
- package/src/cli/commands/init.js +53 -0
- package/src/cli/commands/lint.js +93 -0
- package/src/cli/commands/list.js +54 -0
- package/src/cli/commands/open.js +31 -0
- package/src/cli/commands/promote.js +38 -0
- package/src/cli/commands/render.js +31 -0
- package/src/cli/commands/replay.js +49 -0
- package/src/cli/commands/schema.js +10 -0
- package/src/cli/commands/serve.js +178 -0
- package/src/cli/commands/setup.js +64 -0
- package/src/cli/commands/snapshot.js +29 -0
- package/src/cli/commands/templates.js +75 -0
- package/src/cli/deliver.js +54 -0
- package/src/cli/emit.js +29 -0
- package/src/cli/format.js +153 -0
- package/src/cli/index.js +365 -0
- package/src/cli/registry.js +18 -0
- package/src/core/blocks.js +147 -0
- package/src/core/diagram.js +282 -0
- package/src/core/errors.js +156 -0
- package/src/core/example.js +109 -0
- package/src/core/ir.js +62 -0
- package/src/core/lint-gates.js +427 -0
- package/src/core/lint.js +281 -0
- package/src/core/scan.js +84 -0
- package/src/core/schema.js +88 -0
- package/src/core/spec-check.js +33 -0
- package/src/core/validate.js +44 -0
- package/src/core/version.js +18 -0
- package/src/core/vocabulary.js +140 -0
- package/src/delivery/atomic.js +71 -0
- package/src/delivery/comments.js +180 -0
- package/src/delivery/home.js +70 -0
- package/src/delivery/open.js +31 -0
- package/src/delivery/project.js +141 -0
- package/src/delivery/read.js +109 -0
- package/src/delivery/run.js +95 -0
- package/src/delivery/scaffold-blueprints.js +728 -0
- package/src/delivery/store.js +219 -0
- package/src/delivery/template-extensions.js +183 -0
- package/src/delivery/template-format.js +112 -0
- package/src/delivery/template-package.js +376 -0
- package/src/delivery/template-promote.js +240 -0
- package/src/delivery/template-scaffold.js +181 -0
- package/src/delivery/templates.js +192 -0
- package/src/delivery/toml.js +195 -0
- package/src/delivery/write.js +35 -0
- package/src/export/browser.js +130 -0
- package/src/export/index.js +96 -0
- package/src/export/pdf.js +25 -0
- package/src/export/png.js +40 -0
- package/src/export/pptx.js +48 -0
- package/src/export/slides.js +33 -0
- package/src/export/snapshot.js +33 -0
- package/src/layouts/article.js +103 -0
- package/src/layouts/canvas.js +144 -0
- package/src/layouts/card.js +128 -0
- package/src/layouts/deck.js +88 -0
- package/src/layouts/index.js +90 -0
- package/src/layouts/one-page.js +161 -0
- package/src/layouts/registry.js +251 -0
- package/src/layouts/resume.js +172 -0
- package/src/layouts/template-index.js +78 -0
- package/src/parser/artifact.js +38 -0
- package/src/parser/container.js +103 -0
- package/src/parser/index.js +223 -0
- package/src/parser/tokens.js +265 -0
- package/src/render/board-filter.client.js +80 -0
- package/src/render/compile.js +29 -0
- package/src/render/context.js +98 -0
- package/src/render/element.js +32 -0
- package/src/render/fonts.js +129 -0
- package/src/render/graph-hover.client.js +148 -0
- package/src/render/html.js +52 -0
- package/src/render/index.js +241 -0
- package/src/render/measure.js +60 -0
- package/src/render/placement.js +136 -0
- package/src/render/playback.client.js +74 -0
- package/src/render/scale-to-fit.client.js +136 -0
- package/src/render/scale.js +41 -0
- package/src/render/skeleton.js +131 -0
- package/src/render/ssr.js +24 -0
- package/src/render/styles.js +56 -0
- package/src/render/templates.js +191 -0
- package/src/serve/daemon.js +117 -0
- package/src/serve/overlay.js +213 -0
- package/src/serve/protocol.js +36 -0
- package/src/serve/server.js +264 -0
- package/templates/kami/cards/components.js +40 -0
- package/templates/kami/cards/index.js +25 -0
- package/templates/kami/cards/manifest.js +67 -0
- package/templates/kami/cards/styles.css +389 -0
- package/templates/kami/long-form/components.js +102 -0
- package/templates/kami/long-form/index.js +25 -0
- package/templates/kami/long-form/manifest.js +87 -0
- package/templates/kami/long-form/styles.css +481 -0
- package/templates/kami/one-page/components.js +48 -0
- package/templates/kami/one-page/index.js +27 -0
- package/templates/kami/one-page/manifest.js +65 -0
- package/templates/kami/one-page/styles.css +375 -0
- package/templates/kami/resume/components.js +51 -0
- package/templates/kami/resume/index.js +27 -0
- package/templates/kami/resume/manifest.js +65 -0
- package/templates/kami/resume/styles.css +424 -0
- package/templates/kami/slides/components.js +41 -0
- package/templates/kami/slides/index.js +26 -0
- package/templates/kami/slides/manifest.js +64 -0
- package/templates/kami/slides/styles.css +406 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { h } from 'vue'
|
|
2
|
+
import { isElement } from '../blocks/element.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Neutral element tree → Vue vnodes.
|
|
6
|
+
*
|
|
7
|
+
* This is the *only* place the two representations meet, and it is why Vue no
|
|
8
|
+
* longer appears in `src/core`, in `src/blocks`, or in any template: a block
|
|
9
|
+
* module hands over `{tag, attrs, children}` data and the render layer decides
|
|
10
|
+
* what backend draws it. Swapping the backend, or serialising the same tree to
|
|
11
|
+
* something other than HTML, becomes a change to this file alone.
|
|
12
|
+
*
|
|
13
|
+
* Three pass-through rules keep the bytes identical to the hand-written vnodes
|
|
14
|
+
* this replaced:
|
|
15
|
+
*
|
|
16
|
+
* - `attrs` are handed to `h` verbatim, key order and all — a `null` value is
|
|
17
|
+
* how Vue is told to omit the attribute.
|
|
18
|
+
* - a `null` child stays `null`, so it still serialises to the empty comment
|
|
19
|
+
* node that separates "no element here" from "nothing at all".
|
|
20
|
+
* - `html` becomes `innerHTML`, the one prop that carries raw content.
|
|
21
|
+
*/
|
|
22
|
+
export function toVNode(node) {
|
|
23
|
+
if (node === null || node === undefined) return null
|
|
24
|
+
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
|
25
|
+
// Anything else is not a node this contract defines. Drawing it would put a
|
|
26
|
+
// third party's mistake into the artifact as markup nobody can account for.
|
|
27
|
+
if (!isElement(node)) return null
|
|
28
|
+
const { tag, attrs, children, html } = node
|
|
29
|
+
if (typeof html === 'string') return h(tag, { ...(attrs ?? undefined), innerHTML: html })
|
|
30
|
+
if (children === undefined || children === null) return h(tag, attrs ?? null)
|
|
31
|
+
return h(tag, attrs ?? null, Array.isArray(children) ? children.map(toVNode) : toVNode(children))
|
|
32
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url)
|
|
6
|
+
|
|
7
|
+
const FACE_RE = /@font-face\s*\{([^}]*)\}/g
|
|
8
|
+
const DECL = (body, prop) => {
|
|
9
|
+
const m = new RegExp(`${prop}\\s*:\\s*([^;]+);`).exec(body)
|
|
10
|
+
return m ? m[1].trim() : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parseUnicodeRange = (spec) =>
|
|
14
|
+
spec
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((chunk) => chunk.trim().replace(/^U\+/i, ''))
|
|
17
|
+
.filter((chunk) => chunk.length > 0)
|
|
18
|
+
.map((chunk) => {
|
|
19
|
+
const [lo, hi] = chunk.split('-')
|
|
20
|
+
return { lo: parseInt(lo, 16), hi: parseInt(hi ?? lo, 16) }
|
|
21
|
+
})
|
|
22
|
+
.filter((r) => Number.isFinite(r.lo) && Number.isFinite(r.hi))
|
|
23
|
+
|
|
24
|
+
/** Parse a @fontsource stylesheet into ordered face descriptors. */
|
|
25
|
+
export function parseFontFaces(cssPath) {
|
|
26
|
+
const css = readFileSync(cssPath, 'utf8')
|
|
27
|
+
const baseDir = dirname(cssPath)
|
|
28
|
+
const faces = []
|
|
29
|
+
let m
|
|
30
|
+
while ((m = FACE_RE.exec(css)) !== null) {
|
|
31
|
+
const body = m[1]
|
|
32
|
+
const unicodeRange = DECL(body, 'unicode-range')
|
|
33
|
+
const src = DECL(body, 'src')
|
|
34
|
+
if (unicodeRange === null || src === null) continue
|
|
35
|
+
const woff2 = /url\(([^)]+\.woff2)\)/.exec(src)
|
|
36
|
+
if (woff2 === null) continue
|
|
37
|
+
faces.push({
|
|
38
|
+
index: faces.length,
|
|
39
|
+
family: (DECL(body, 'font-family') ?? "'sans-serif'").replace(/^['"]|['"]$/g, ''),
|
|
40
|
+
style: DECL(body, 'font-style') ?? 'normal',
|
|
41
|
+
weight: DECL(body, 'font-weight') ?? '400',
|
|
42
|
+
display: DECL(body, 'font-display') ?? 'swap',
|
|
43
|
+
unicodeRange,
|
|
44
|
+
ranges: parseUnicodeRange(unicodeRange),
|
|
45
|
+
file: resolve(baseDir, woff2[1]),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
return faces
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Distinct code points used by a rendered document. */
|
|
52
|
+
export function collectCodepoints(...texts) {
|
|
53
|
+
const points = new Set()
|
|
54
|
+
for (const text of texts) {
|
|
55
|
+
if (typeof text !== 'string') continue
|
|
56
|
+
for (const char of text) points.add(char.codePointAt(0))
|
|
57
|
+
}
|
|
58
|
+
return points
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const buildIndex = (faces) => {
|
|
62
|
+
const entries = []
|
|
63
|
+
for (const face of faces) {
|
|
64
|
+
for (const range of face.ranges) entries.push({ ...range, face: face.index })
|
|
65
|
+
}
|
|
66
|
+
return entries.sort((a, b) => a.lo - b.lo || a.hi - b.hi)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lookup = (index, cp) => {
|
|
70
|
+
let lo = 0
|
|
71
|
+
let hi = index.length - 1
|
|
72
|
+
while (lo <= hi) {
|
|
73
|
+
const mid = (lo + hi) >> 1
|
|
74
|
+
if (cp < index[mid].lo) hi = mid - 1
|
|
75
|
+
else if (cp > index[mid].hi) lo = mid + 1
|
|
76
|
+
else return index[mid].face
|
|
77
|
+
}
|
|
78
|
+
return -1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Faces whose unicode-range intersects the used code points, in file order. */
|
|
82
|
+
export function selectFaces(faces, codepoints) {
|
|
83
|
+
const index = buildIndex(faces)
|
|
84
|
+
const used = new Set()
|
|
85
|
+
for (const cp of codepoints) {
|
|
86
|
+
const hit = lookup(index, cp)
|
|
87
|
+
if (hit !== -1) used.add(hit)
|
|
88
|
+
}
|
|
89
|
+
return faces.filter((face) => used.has(face.index))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const faceCss = (face) => {
|
|
93
|
+
const data = readFileSync(face.file).toString('base64')
|
|
94
|
+
return [
|
|
95
|
+
'@font-face{',
|
|
96
|
+
`font-family:'${face.family}';`,
|
|
97
|
+
`font-style:${face.style};`,
|
|
98
|
+
`font-display:${face.display};`,
|
|
99
|
+
`font-weight:${face.weight};`,
|
|
100
|
+
`src:url(data:font/woff2;base64,${data}) format('woff2');`,
|
|
101
|
+
`unicode-range:${face.unicodeRange};`,
|
|
102
|
+
'}',
|
|
103
|
+
].join('')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the @font-face CSS for a template's font specs, embedding only the
|
|
108
|
+
* subset chunks whose unicode-range intersects the document's characters.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<{package: string, weights: string[]}>} specs
|
|
111
|
+
* @param {Set<number>} codepoints
|
|
112
|
+
* @returns {{css: string, embedded: number, available: number}}
|
|
113
|
+
*/
|
|
114
|
+
export function embedFontSubset(specs, codepoints) {
|
|
115
|
+
const parts = []
|
|
116
|
+
let embedded = 0
|
|
117
|
+
let available = 0
|
|
118
|
+
for (const spec of specs) {
|
|
119
|
+
for (const weight of spec.weights) {
|
|
120
|
+
const cssPath = require.resolve(`${spec.package}/${weight}.css`)
|
|
121
|
+
const faces = parseFontFaces(cssPath)
|
|
122
|
+
available += faces.length
|
|
123
|
+
const selected = selectFaces(faces, codepoints)
|
|
124
|
+
embedded += selected.length
|
|
125
|
+
for (const face of selected) parts.push(faceCss(face))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { css: parts.join('\n'), embedded, available }
|
|
129
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Graph dependency highlight (SPEC section 3.4, wayfinder issue 11 P5
|
|
3
|
+
* "graph hover highlight").
|
|
4
|
+
*
|
|
5
|
+
* A block-keyed client asset: the render layer embeds it only into artifacts
|
|
6
|
+
* that actually contain a `graph` block, which is what keeps every artifact
|
|
7
|
+
* drawn before this existed byte for byte unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Every character here is ASCII, and that is a size decision rather than a
|
|
10
|
+
* stylistic one: client assets are fed to `collectCodepoints`, so one section
|
|
11
|
+
* sign or one em dash in a comment pulls a whole font subset (~8.5KB) into
|
|
12
|
+
* every artifact that carries this file. The same scan also bans the four
|
|
13
|
+
* sequences that would put the HTML tokenizer into its double-escaped state,
|
|
14
|
+
* where the closing tag of the script element stops closing it and the rest of
|
|
15
|
+
* the artifact -- the embedded IR included -- is swallowed as script data
|
|
16
|
+
* (CONTRACT F2g H2). Both rules are pinned by
|
|
17
|
+
* `test_f2g_skeleton_assets_cannot_derail_the_parser`.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately ES5-shaped and dependency-free: it is embedded verbatim into one
|
|
20
|
+
* offline file, so it must parse in any evergreen browser without a build step.
|
|
21
|
+
*
|
|
22
|
+
* The DOM contract is stated by `src/blocks/graph.js` and read here: the
|
|
23
|
+
* adjacency is already in the markup (`data-node-id` on a node,
|
|
24
|
+
* `data-edge-from` / `data-edge-to` on an edge), so this file computes no graph
|
|
25
|
+
* of its own -- there is no second model that could disagree with the picture.
|
|
26
|
+
* Keyboard focus is wired alongside the pointer because a highlight only
|
|
27
|
+
* pointing devices can reach is a highlight half the readers never get.
|
|
28
|
+
* Without JS every node and edge simply stays at full strength.
|
|
29
|
+
*
|
|
30
|
+
* The leading semicolon is the same guard `scale-to-fit.client.js` carries and
|
|
31
|
+
* for the same reason: assets are concatenated with a newline between them, and
|
|
32
|
+
* `})()` followed by a line starting with `(` is one expression to a JS parser --
|
|
33
|
+
* the previous asset's return value would be called as a function and this whole
|
|
34
|
+
* file would never run. Pinned by `test_f6a_assets_survive_concatenation`.
|
|
35
|
+
*/
|
|
36
|
+
;(function () {
|
|
37
|
+
/*
|
|
38
|
+
* The floor: what an unpainted graph has to look like to be a graph at all.
|
|
39
|
+
*
|
|
40
|
+
* SVG presentation defaults are `fill: black; stroke: none`, and every one of
|
|
41
|
+
* them is wrong for this picture: the node box is painted solid black, the
|
|
42
|
+
* label is painted black *inside* that box (black on black -- the label rect
|
|
43
|
+
* sits entirely within the box rect), and an edge, which is an open bezier,
|
|
44
|
+
* is filled rather than stroked, so it draws as a black blob instead of a
|
|
45
|
+
* line. Measured in real Chromium on the article path: box fill rgb(0,0,0),
|
|
46
|
+
* label fill rgb(0,0,0), edge-line fill rgb(0,0,0) / stroke none. None of the
|
|
47
|
+
* five shipped Kami skins matches a single one of these selectors.
|
|
48
|
+
*
|
|
49
|
+
* So this is the same sentence the dimming rules below already carry, applied
|
|
50
|
+
* one level down: when the skin has painted nothing, "invisible" and "never
|
|
51
|
+
* ran" are the same picture -- and here it is the *content* that disappears,
|
|
52
|
+
* not just the interaction.
|
|
53
|
+
*
|
|
54
|
+
* Two disciplines make this a floor rather than a ceiling:
|
|
55
|
+
*
|
|
56
|
+
* - `:where()` zeroes the specificity, so any template rule on the same hook
|
|
57
|
+
* wins no matter that this stylesheet is appended last. Paint stays the
|
|
58
|
+
* template's (wayfinder issue 13, the three-way split of template duty);
|
|
59
|
+
* this only refuses to leave the page blank when nobody painted.
|
|
60
|
+
* - `currentColor` rather than a colour of its own, so the graph inherits
|
|
61
|
+
* whatever ink the surrounding page already uses. The SDK names no colour.
|
|
62
|
+
*/
|
|
63
|
+
var BASE_RULES =
|
|
64
|
+
':where(.graph-edge-line){fill:none;stroke:currentColor}' +
|
|
65
|
+
':where(.graph-arrow){fill:currentColor;stroke:none}' +
|
|
66
|
+
':where(.graph-edge-label){fill:currentColor;stroke:none}' +
|
|
67
|
+
':where(.graph-node-box){fill:none;stroke:currentColor}' +
|
|
68
|
+
':where(.graph-node-label){fill:currentColor;stroke:none}'
|
|
69
|
+
var DIM_RULES =
|
|
70
|
+
'.graph.is-hovering .graph-edge:not(.is-linked){opacity:0.2}' +
|
|
71
|
+
'.graph.is-hovering .graph-node:not(.is-active):not(.is-linked){opacity:0.3}'
|
|
72
|
+
var graphs = Array.prototype.slice.call(document.querySelectorAll('.graph[data-graph]'))
|
|
73
|
+
if (graphs.length === 0) return
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The dimming ships with the behaviour, not with the skin.
|
|
77
|
+
*
|
|
78
|
+
* A template is free to paint `.is-active` and `.is-linked` however it likes,
|
|
79
|
+
* but if nobody paints anything the class flip is invisible -- and an
|
|
80
|
+
* invisible highlight is indistinguishable from a script that never ran.
|
|
81
|
+
*
|
|
82
|
+
* Both blocks are injected from the same element and behind the same
|
|
83
|
+
* condition (a document with no graph gets neither, and no style element at
|
|
84
|
+
* all), so readability and behaviour can never arrive separately.
|
|
85
|
+
*/
|
|
86
|
+
var rules = document.createElement('style')
|
|
87
|
+
rules.textContent = BASE_RULES + DIM_RULES
|
|
88
|
+
document.head.appendChild(rules)
|
|
89
|
+
|
|
90
|
+
function clear(graph) {
|
|
91
|
+
graph.classList.remove('is-hovering')
|
|
92
|
+
var marked = Array.prototype.slice.call(graph.querySelectorAll('.is-active, .is-linked'))
|
|
93
|
+
for (var i = 0; i < marked.length; i += 1) {
|
|
94
|
+
marked[i].classList.remove('is-active')
|
|
95
|
+
marked[i].classList.remove('is-linked')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function highlight(graph, id) {
|
|
100
|
+
clear(graph)
|
|
101
|
+
/*
|
|
102
|
+
* A list, not an object used as a set: a node whose id happens to be
|
|
103
|
+
* `constructor` or `toString` would test truthy against a bare object
|
|
104
|
+
* literal without anyone ever having put it there, and would then light up
|
|
105
|
+
* as a neighbour of every node in the graph.
|
|
106
|
+
*/
|
|
107
|
+
var neighbours = []
|
|
108
|
+
var edges = Array.prototype.slice.call(graph.querySelectorAll('.graph-edge'))
|
|
109
|
+
for (var i = 0; i < edges.length; i += 1) {
|
|
110
|
+
var from = edges[i].getAttribute('data-edge-from')
|
|
111
|
+
var to = edges[i].getAttribute('data-edge-to')
|
|
112
|
+
if (from !== id && to !== id) continue
|
|
113
|
+
edges[i].classList.add('is-linked')
|
|
114
|
+
neighbours.push(from)
|
|
115
|
+
neighbours.push(to)
|
|
116
|
+
}
|
|
117
|
+
var nodes = Array.prototype.slice.call(graph.querySelectorAll('.graph-node'))
|
|
118
|
+
for (var j = 0; j < nodes.length; j += 1) {
|
|
119
|
+
var name = nodes[j].getAttribute('data-node-id')
|
|
120
|
+
if (name === id) nodes[j].classList.add('is-active')
|
|
121
|
+
else if (neighbours.indexOf(name) !== -1) nodes[j].classList.add('is-linked')
|
|
122
|
+
}
|
|
123
|
+
graph.classList.add('is-hovering')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (var g = 0; g < graphs.length; g += 1) {
|
|
127
|
+
(function (graph) {
|
|
128
|
+
var nodes = Array.prototype.slice.call(graph.querySelectorAll('.graph-node'))
|
|
129
|
+
for (var k = 0; k < nodes.length; k += 1) {
|
|
130
|
+
(function (node) {
|
|
131
|
+
var id = node.getAttribute('data-node-id')
|
|
132
|
+
node.addEventListener('mouseenter', function () {
|
|
133
|
+
highlight(graph, id)
|
|
134
|
+
})
|
|
135
|
+
node.addEventListener('focus', function () {
|
|
136
|
+
highlight(graph, id)
|
|
137
|
+
})
|
|
138
|
+
node.addEventListener('mouseleave', function () {
|
|
139
|
+
clear(graph)
|
|
140
|
+
})
|
|
141
|
+
node.addEventListener('blur', function () {
|
|
142
|
+
clear(graph)
|
|
143
|
+
})
|
|
144
|
+
})(nodes[k])
|
|
145
|
+
}
|
|
146
|
+
})(graphs[g])
|
|
147
|
+
}
|
|
148
|
+
})()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { IR_SCRIPT_TYPE } from '../core/ir.js'
|
|
2
|
+
|
|
3
|
+
export { IR_SCRIPT_TYPE }
|
|
4
|
+
|
|
5
|
+
const ESCAPES = Object.freeze({ '&': '&', '<': '<', '>': '>', '"': '"' })
|
|
6
|
+
|
|
7
|
+
export const escapeHtml = (value) => String(value ?? '').replace(/[&<>"]/g, (c) => ESCAPES[c])
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialise the IR for embedding inside a <script> element.
|
|
11
|
+
*
|
|
12
|
+
* Both `</` (ends the element early) and `<!--` (opens an HTML comment state)
|
|
13
|
+
* are hazards, and every one of them starts with `<`. Escaping `<` itself as
|
|
14
|
+
* `<` neutralises the whole class at once — and unlike `\/` or `\!`, it is
|
|
15
|
+
* a legal JSON escape in *any* position, so the payload always parses back.
|
|
16
|
+
* `\!` is not a valid escape at all and silently corrupted the IR (CONTRACT A2).
|
|
17
|
+
*/
|
|
18
|
+
export const serializeIr = (ir) => JSON.stringify(ir).replace(/</g, '\\u003c')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Assemble the single-file artifact. Nothing here depends on file paths (A8).
|
|
22
|
+
*
|
|
23
|
+
* `script` is the template's own inlined behaviour (playback chrome, SPEC §7.4).
|
|
24
|
+
* It is emitted without a `src`, so the零外部請求 rules see nothing to flag, and
|
|
25
|
+
* it sits outside the IR payload so `liveMarkup` still judges it as live markup.
|
|
26
|
+
* Templates without behaviour emit no element at all, which is what keeps their
|
|
27
|
+
* artifacts byte-identical to the pre-S3a bytes.
|
|
28
|
+
*/
|
|
29
|
+
export function assembleDocument({ language, title, styles, fontCss, body, script, ir }) {
|
|
30
|
+
const behaviour = typeof script === 'string' && script.length > 0 ? [`<script>${script}</script>`] : []
|
|
31
|
+
return [
|
|
32
|
+
'<!doctype html>',
|
|
33
|
+
`<html lang="${escapeHtml(language)}">`,
|
|
34
|
+
'<head>',
|
|
35
|
+
'<meta charset="utf-8">',
|
|
36
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
37
|
+
`<meta name="generator" content="${escapeHtml(`kamishibai ${ir.engine}`)}">`,
|
|
38
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
39
|
+
'<style>',
|
|
40
|
+
fontCss,
|
|
41
|
+
styles,
|
|
42
|
+
'</style>',
|
|
43
|
+
'</head>',
|
|
44
|
+
'<body>',
|
|
45
|
+
body,
|
|
46
|
+
...behaviour,
|
|
47
|
+
`<script type="${IR_SCRIPT_TYPE}">${serializeIr(ir)}</script>`,
|
|
48
|
+
'</body>',
|
|
49
|
+
'</html>',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n')
|
|
52
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { buildIr } from '../core/ir.js'
|
|
2
|
+
import { findDeck, walkBlocks } from '../core/blocks.js'
|
|
3
|
+
import { assertTemplateVocabulary } from '../core/vocabulary.js'
|
|
4
|
+
import { layoutGraph } from '../core/diagram.js'
|
|
5
|
+
import { assertBlockSpecs } from '../core/spec-check.js'
|
|
6
|
+
import { CODES, validationError } from '../core/errors.js'
|
|
7
|
+
import {
|
|
8
|
+
canvasProblems,
|
|
9
|
+
knownLayouts,
|
|
10
|
+
resolveCanvas,
|
|
11
|
+
resolveLayout,
|
|
12
|
+
} from '../layouts/index.js'
|
|
13
|
+
import { resolveTemplate } from './templates.js'
|
|
14
|
+
import { createRenderContext } from './context.js'
|
|
15
|
+
import { renderBody } from './ssr.js'
|
|
16
|
+
import { artifactScript } from './skeleton.js'
|
|
17
|
+
import { availableWidths } from './measure.js'
|
|
18
|
+
import { assertPlacement } from './placement.js'
|
|
19
|
+
import { injectLayoutVars } from './styles.js'
|
|
20
|
+
import { collectCodepoints, embedFontSubset } from './fonts.js'
|
|
21
|
+
import { assembleDocument } from './html.js'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
availableTemplateKeys,
|
|
25
|
+
builtinManifests,
|
|
26
|
+
isReservedKey,
|
|
27
|
+
listTemplateKeys,
|
|
28
|
+
registerTemplateSource,
|
|
29
|
+
reservedNamespaces,
|
|
30
|
+
resetTemplateSources,
|
|
31
|
+
resolveTemplate,
|
|
32
|
+
} from './templates.js'
|
|
33
|
+
export { IR_SCRIPT_TYPE } from './html.js'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The block types whose picture is laid out by the SDK rather than by the
|
|
37
|
+
* block: `diagram` (SPEC §3.1) and, since F6a, the core `graph` (issue 11 P1).
|
|
38
|
+
*
|
|
39
|
+
* Both draw a node-and-edge picture from the same engine, which is the whole
|
|
40
|
+
* reason the list exists here rather than as a flag on the module: layout needs
|
|
41
|
+
* the width the page gives the block, so it cannot be a block-level fact, and a
|
|
42
|
+
* *second* list of "who needs measuring" living inside the blocks layer would
|
|
43
|
+
* be a fact the render layer could disagree with.
|
|
44
|
+
*/
|
|
45
|
+
const LAID_OUT_TYPES = Object.freeze(['diagram', 'graph'])
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Measure every laid-out block in the document before anything is drawn.
|
|
49
|
+
*
|
|
50
|
+
* Layout is not a block-level fact — it needs the width the page gives the
|
|
51
|
+
* block, which is a layout-layer concern the block cannot see (wayfinder issue
|
|
52
|
+
* 13 試金石). Computing it here, once, is also what keeps the two Kami
|
|
53
|
+
* templates drawing the *same* picture from the same spec: a layout computed
|
|
54
|
+
* inside the renderer would be one layout per template.
|
|
55
|
+
*
|
|
56
|
+
* Since F2a the width is per block rather than per document: a diagram sitting
|
|
57
|
+
* in 8 of 24 columns is measured against those 8 columns, so it is drawn narrow
|
|
58
|
+
* instead of being drawn wide and then squeezed (量測報告 §4.2).
|
|
59
|
+
*
|
|
60
|
+
* @returns {Map<string, object>} block id → layout
|
|
61
|
+
*/
|
|
62
|
+
function measureLaidOutGraphs(doc, widths, canvas) {
|
|
63
|
+
const layouts = new Map()
|
|
64
|
+
for (const { block } of walkBlocks(doc)) {
|
|
65
|
+
if (!LAID_OUT_TYPES.includes(block?.type)) continue
|
|
66
|
+
const measure = widths.get(block.id) ?? canvas.measure
|
|
67
|
+
layouts.set(block.id, layoutGraph(block, { measure }))
|
|
68
|
+
}
|
|
69
|
+
return layouts
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The 文體 this template is drawn in, refused loudly when nobody registered it.
|
|
74
|
+
*
|
|
75
|
+
* A missing layout cannot be defaulted away: the root form, the canvas and the
|
|
76
|
+
* slot vocabulary all come from it, so guessing would draw *some* artifact and
|
|
77
|
+
* call it this template's.
|
|
78
|
+
*/
|
|
79
|
+
function layoutFor(manifest, key) {
|
|
80
|
+
const layout = resolveLayout(manifest)
|
|
81
|
+
if (layout === undefined) {
|
|
82
|
+
throw validationError(
|
|
83
|
+
`template "${key}" 宣告的 layout \`${manifest.layout}\` 沒有人註冊;` +
|
|
84
|
+
`已註冊的文體:${knownLayouts().join(', ')}。`,
|
|
85
|
+
CODES.LAYOUT_NOT_FOUND,
|
|
86
|
+
'doc',
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
return layout
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A template may only fill slots its layout offers.
|
|
94
|
+
*
|
|
95
|
+
* An unfilled slot is silence and a *misspelled* one is also silence — the
|
|
96
|
+
* chrome simply never appears, the artifact renders, and `lint` says nothing.
|
|
97
|
+
* That is the same invisible data-loss shape `manifest.blocks` exists to
|
|
98
|
+
* refuse, so it is refused the same way.
|
|
99
|
+
*/
|
|
100
|
+
function assertChromeSlots(template, layout, key) {
|
|
101
|
+
const filled = Object.keys(template.chrome ?? {})
|
|
102
|
+
const unknown = filled.filter((slot) => !layout.slots.includes(slot))
|
|
103
|
+
if (unknown.length === 0) return
|
|
104
|
+
throw validationError(
|
|
105
|
+
`template "${key}" 填了 layout \`${layout.name}\` 沒有的槽位:${unknown.join(', ')};` +
|
|
106
|
+
`該文體提供:${layout.slots.join(', ') || '(無)'}。` +
|
|
107
|
+
'拼錯的槽位不會報錯、只會不出現——所以在這裡報。',
|
|
108
|
+
CODES.LAYOUT_SLOT_UNKNOWN,
|
|
109
|
+
'doc',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A template may restate its 文體's root form, and may not disagree with it.
|
|
115
|
+
*
|
|
116
|
+
* `manifest.root` predates layouts: it was where `kami/slides` said "this
|
|
117
|
+
* template only draws decks". Since that requirement belongs to the 文體, the
|
|
118
|
+
* field is now a restatement — and a restatement that disagrees is the most
|
|
119
|
+
* dangerous shape available here, because it *reads* like a template being
|
|
120
|
+
* careful. A `root: 'section'` on the `deck` layout would satisfy every guard
|
|
121
|
+
* and then hand a section-rooted document to a root form that looks for a deck
|
|
122
|
+
* and finds nothing: an artifact with an empty body and a playback script
|
|
123
|
+
* bolted to it, exit 0, `lint` 0.
|
|
124
|
+
*
|
|
125
|
+
* So the two must be the same sentence, in both directions — a template may not
|
|
126
|
+
* loosen its 文體's requirement, and may not invent one its 文體 does not have.
|
|
127
|
+
*/
|
|
128
|
+
function assertLayoutRootForm(manifest, layout, key) {
|
|
129
|
+
const declared = manifest.root ?? null
|
|
130
|
+
const required = layout.rootForm ?? null
|
|
131
|
+
if (declared === required) return
|
|
132
|
+
const say = (value) => (value === null ? '無根形要求' : `\`${value}\``)
|
|
133
|
+
throw validationError(
|
|
134
|
+
`template "${key}" 宣告根形 ${say(declared)},但所屬 layout \`${layout.name}\` ` +
|
|
135
|
+
`要求 ${say(required)}。根形是文體的,模板只能複述、不能改寫或放寬——` +
|
|
136
|
+
'改寫得逞的產物會是「body 空白但帶著播放腳本」的殭屍,而且 exit 0。',
|
|
137
|
+
CODES.LAYOUT_ROOT_CONFLICT,
|
|
138
|
+
'doc',
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The applied canvas geometry, refused loudly when it contradicts itself (C3). */
|
|
143
|
+
function canvasFor(layout, manifest) {
|
|
144
|
+
const canvas = resolveCanvas(layout, manifest)
|
|
145
|
+
const problems = canvasProblems(canvas, layout, manifest)
|
|
146
|
+
if (problems.length > 0) {
|
|
147
|
+
throw validationError(problems.join(';'), CODES.LAYOUT_CANVAS_INVALID, 'doc')
|
|
148
|
+
}
|
|
149
|
+
return canvas
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render a canonical doc block tree with an already-resolved template package.
|
|
154
|
+
*
|
|
155
|
+
* The seam exists because a template is not always something `resolveTemplate`
|
|
156
|
+
* can find: a store-loaded package, a third-party layout's own built-in
|
|
157
|
+
* template, or a test's synthetic one all have to reach the same pipeline —
|
|
158
|
+
* the *same* one, not a re-implementation that could disagree with it.
|
|
159
|
+
*/
|
|
160
|
+
export async function renderWithTemplatePackage({ template, doc, createdAt, generator }) {
|
|
161
|
+
const key = `${template.manifest.namespace}/${template.manifest.name}`
|
|
162
|
+
const layout = layoutFor(template.manifest, key)
|
|
163
|
+
assertLayoutRootForm(template.manifest, layout, key)
|
|
164
|
+
assertChromeSlots(template, layout, key)
|
|
165
|
+
const canvas = canvasFor(layout, template.manifest)
|
|
166
|
+
|
|
167
|
+
// Before a single byte is drawn: a template that cannot express this document
|
|
168
|
+
// must fail loudly. Skipping unknown types is what a renderer does; doing it
|
|
169
|
+
// as the *only* answer turns a wrong `-t` into a silently empty artifact (C4).
|
|
170
|
+
assertTemplateVocabulary({ doc, manifest: template.manifest, layout })
|
|
171
|
+
// A block whose spec cannot be drawn is rejected here rather than turned into
|
|
172
|
+
// an empty SVG: a picture that silently disagrees with its spec is the one
|
|
173
|
+
// failure a reader cannot detect (CONTRACT D2). Every block checks its own.
|
|
174
|
+
assertBlockSpecs(doc)
|
|
175
|
+
// …and a coordinate that cannot mean anything where it sits is refused too,
|
|
176
|
+
// because no block can see where it sits (CONTRACT C2).
|
|
177
|
+
assertPlacement(doc, canvas)
|
|
178
|
+
|
|
179
|
+
const ir = buildIr({ doc, template: template.manifest, createdAt, generator })
|
|
180
|
+
|
|
181
|
+
const widths = availableWidths(doc, canvas)
|
|
182
|
+
const layouts = measureLaidOutGraphs(doc, widths, canvas)
|
|
183
|
+
const ctx = createRenderContext({
|
|
184
|
+
template,
|
|
185
|
+
doc,
|
|
186
|
+
layouts,
|
|
187
|
+
canvas,
|
|
188
|
+
// 轉子 override is *this* package's answer for a type the SDK already has
|
|
189
|
+
// (CONTRACT D3). It arrives here rather than in the block registry because
|
|
190
|
+
// the registry is global: an installed package that overrode `callout`
|
|
191
|
+
// there would silently change how every other template draws one.
|
|
192
|
+
overrides: template.overrides,
|
|
193
|
+
})
|
|
194
|
+
const body = await renderBody(layout.root, doc, ctx)
|
|
195
|
+
// Geometry flows from config into the stylesheet, never the other way (C3).
|
|
196
|
+
const styles = injectLayoutVars(template.styles(), canvas)
|
|
197
|
+
// Behaviour comes from the skeleton and from the blocks themselves, never
|
|
198
|
+
// from the template: a deck that could opt out of paging would ship as an
|
|
199
|
+
// unusable deck (issue 13 P3), and a board whose lanes could not be filtered
|
|
200
|
+
// would be a board missing the one interaction its ticket promised (11 P5).
|
|
201
|
+
const script = artifactScript(layout, doc)
|
|
202
|
+
|
|
203
|
+
const codepoints = collectCodepoints(body, styles, script, doc.meta?.title, doc.meta?.kicker)
|
|
204
|
+
const { css: fontCss, embedded, available } = embedFontSubset(template.fonts, codepoints)
|
|
205
|
+
|
|
206
|
+
const html = assembleDocument({
|
|
207
|
+
language: template.language,
|
|
208
|
+
title: doc.meta?.title ?? 'Untitled',
|
|
209
|
+
styles,
|
|
210
|
+
fontCss,
|
|
211
|
+
body,
|
|
212
|
+
script,
|
|
213
|
+
ir,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const deck = findDeck(doc)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
html,
|
|
220
|
+
ir,
|
|
221
|
+
fonts: { embedded, available },
|
|
222
|
+
slides: deck === undefined ? undefined : deck.slides.length,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render a canonical doc block tree into a self-contained single-file artifact.
|
|
228
|
+
* Pure with respect to the filesystem destination — nothing about the output
|
|
229
|
+
* path enters the artifact, which is what keeps renders byte-identical (A8).
|
|
230
|
+
*
|
|
231
|
+
* `slides` is present only for deck artifacts, and is read off the block tree
|
|
232
|
+
* rather than off the parse: `replay` rebuilds from the embedded IR alone, so a
|
|
233
|
+
* count derived from the source would simply vanish on the replay path.
|
|
234
|
+
*
|
|
235
|
+
* @returns {Promise<{html: string, ir: object, fonts: {embedded: number, available: number},
|
|
236
|
+
* slides: number|undefined}>}
|
|
237
|
+
*/
|
|
238
|
+
export async function renderArtifact({ doc, templateKey, createdAt, generator }) {
|
|
239
|
+
const template = await resolveTemplate(templateKey)
|
|
240
|
+
return await renderWithTemplatePackage({ template, doc, createdAt, generator })
|
|
241
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { blockArrayKeys, blockMatrixKeys } from '../core/blocks.js'
|
|
2
|
+
import { hasPlacement, placedWidth } from '../blocks/placement.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How much width the page actually gives each block.
|
|
6
|
+
*
|
|
7
|
+
* `core/diagram.js` was tuned against 740px and could not see that number: a
|
|
8
|
+
* diagram dropped into a narrow column was squeezed until its labels stopped
|
|
9
|
+
* being readable, and nothing in the pipeline could tell (量測報告 §4.2). F1
|
|
10
|
+
* turned the assumption into a parameter; this is the layer that finally knows
|
|
11
|
+
* what to pass — 「diagram 一律收實際可用寬」 (wayfinder issue 14 裁決 3).
|
|
12
|
+
*
|
|
13
|
+
* The rule is one line of arithmetic and deliberately stays that way: a block
|
|
14
|
+
* that **carries a placement** gets its span's worth of the width around it;
|
|
15
|
+
* every other block gets that width whole. No text measurement, no percentages
|
|
16
|
+
* resolved against a viewport — the same document must produce the same bytes
|
|
17
|
+
* on any machine (CONTRACT D1).
|
|
18
|
+
*
|
|
19
|
+
* The rule used to be phrased as 「inside a `grid` container」 instead, which was
|
|
20
|
+
* the same rule while a `grid` was the only place a coordinate could exist: an
|
|
21
|
+
* unplaced child of a grid spans all 24 columns, so `placedWidth` already
|
|
22
|
+
* returned the full width for it. Since F2f a `gridAddressed` layout lays tracks
|
|
23
|
+
* of its own (a slide's content region), and a container test would have gone
|
|
24
|
+
* blind exactly there — the diagram in 8 of 24 columns of a slide would be
|
|
25
|
+
* measured against the whole page and then squeezed, which is 量測報告 §4.2
|
|
26
|
+
* happening a second time in a new place.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Block id → available width in CSS pixels, for every block in the tree.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} doc the canonical doc block tree
|
|
33
|
+
* @param {{measure: number}} canvas the resolved canvas of this artifact
|
|
34
|
+
* @returns {Map<string, number>}
|
|
35
|
+
*/
|
|
36
|
+
export function availableWidths(doc, { measure }) {
|
|
37
|
+
const arrays = blockArrayKeys()
|
|
38
|
+
const matrices = blockMatrixKeys()
|
|
39
|
+
const widths = new Map()
|
|
40
|
+
|
|
41
|
+
const visit = (node, available) => {
|
|
42
|
+
if (node === null || typeof node !== 'object') return
|
|
43
|
+
if (typeof node.id === 'string') widths.set(node.id, available)
|
|
44
|
+
const childWidth = (child) => (hasPlacement(child) ? placedWidth(available, child) : available)
|
|
45
|
+
for (const key of matrices) {
|
|
46
|
+
if (!Array.isArray(node[key])) continue
|
|
47
|
+
for (const group of node[key]) {
|
|
48
|
+
if (!Array.isArray(group)) continue
|
|
49
|
+
for (const child of group) visit(child, childWidth(child))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const key of arrays) {
|
|
53
|
+
if (!Array.isArray(node[key])) continue
|
|
54
|
+
for (const child of node[key]) visit(child, childWidth(child))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
visit(doc, measure)
|
|
59
|
+
return widths
|
|
60
|
+
}
|