@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,282 @@
|
|
|
1
|
+
import { DIAGRAM_KINDS, diagramProblems } from '../blocks/diagram.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `diagram` geometry — the layout half of the block (SPEC §3.3).
|
|
5
|
+
*
|
|
6
|
+
* What a diagram *is* (its spec, its schema, its markup) lives in
|
|
7
|
+
* `src/blocks/diagram.js`; what it *measures out to* lives here, because layout
|
|
8
|
+
* is not a block-level fact: it depends on how much width the page gives the
|
|
9
|
+
* block, which only the layer above knows (wayfinder issue 13 試金石).
|
|
10
|
+
*
|
|
11
|
+
* **Layout is pure integer arithmetic.** No clock, no randomness, no measured
|
|
12
|
+
* text: the same spec and the same available width must yield byte-identical
|
|
13
|
+
* SVG (CONTRACT D1), because that is what makes `replay` and the export chain
|
|
14
|
+
* verifiable. The cost is honest ugliness — a long label overflows its box
|
|
15
|
+
* rather than being re-flowed by something we cannot reproduce.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { DIAGRAM_KINDS, diagramProblems }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Geometry, in SVG user units. Even numbers keep every midpoint an integer.
|
|
22
|
+
*
|
|
23
|
+
* Layers run **downwards**, siblings run across. The direction is not a taste
|
|
24
|
+
* call: an artifact's SVG is scaled to the text measure, so a left-to-right
|
|
25
|
+
* chain of six nodes produced a 1500-unit canvas squeezed to half size — a
|
|
26
|
+
* picture whose labels could no longer be read. Stacking layers vertically
|
|
27
|
+
* keeps a chain one column wide and therefore legible at 1:1, and it is the
|
|
28
|
+
* conventional reading direction for a flow anyway.
|
|
29
|
+
*/
|
|
30
|
+
const NODE_W = 180
|
|
31
|
+
const NODE_H = 56
|
|
32
|
+
/** Vertical distance between one layer and the next. */
|
|
33
|
+
const LAYER_GAP = 48
|
|
34
|
+
/** Horizontal distance between two nodes sharing a layer. */
|
|
35
|
+
const ROW_GAP = 28
|
|
36
|
+
const PAD = 24
|
|
37
|
+
/** How far a non-forward edge bulges out to the right of the boxes it joins. */
|
|
38
|
+
const BACK_DIP = 44
|
|
39
|
+
/** Extra canvas reserved on the right when non-forward edges exist. */
|
|
40
|
+
const BACK_BAND = BACK_DIP + 40
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Available width, in CSS pixels, when nobody says otherwise.
|
|
44
|
+
*
|
|
45
|
+
* `NODE_W` was tuned against the long-form text measure of 740px, and that
|
|
46
|
+
* assumption used to be invisible — the layout took no width at all, so a
|
|
47
|
+
* diagram placed in a narrower column was simply squeezed until its labels
|
|
48
|
+
* stopped being readable (量測報告 §4.2). The assumption is now a parameter
|
|
49
|
+
* with the old value as its default, which is what keeps every existing
|
|
50
|
+
* artifact byte-identical while making the narrow case expressible.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_MEASURE = 740
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* How many boxes fit across the available width — at least one, because a
|
|
56
|
+
* single box cannot be narrower than a box.
|
|
57
|
+
*/
|
|
58
|
+
const rowCapacity = (measure) =>
|
|
59
|
+
Math.max(1, Math.floor((measure - PAD * 2 + ROW_GAP) / (NODE_W + ROW_GAP)))
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Edge indices that close a cycle, found by an iterative DFS in declaration
|
|
63
|
+
* order (so the answer is a deterministic function of the spec).
|
|
64
|
+
*
|
|
65
|
+
* Cycles must be *removed before layering*, not merely survived. A `store →
|
|
66
|
+
* render` back edge — which every real architecture diagram has — makes plain
|
|
67
|
+
* longest-path relaxation push its whole cycle one layer further on every
|
|
68
|
+
* pass: capping the passes terminates, but the picture it terminates on is a
|
|
69
|
+
* nonsense 5000-unit-wide canvas. Breaking the cycle first keeps the layering
|
|
70
|
+
* a property of the structure rather than of the iteration count.
|
|
71
|
+
*
|
|
72
|
+
* The DFS is iterative because the stack depth would otherwise be the node
|
|
73
|
+
* count, and a spec is user input.
|
|
74
|
+
*/
|
|
75
|
+
const findBackEdges = (nodes, edges) => {
|
|
76
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]))
|
|
77
|
+
for (const [index, edge] of edges.entries()) {
|
|
78
|
+
if (edge.from !== edge.to) outgoing.get(edge.from).push({ index, to: edge.to })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const UNSEEN = 0
|
|
82
|
+
const OPEN = 1
|
|
83
|
+
const DONE = 2
|
|
84
|
+
const state = new Map(nodes.map((node) => [node.id, UNSEEN]))
|
|
85
|
+
const back = new Set()
|
|
86
|
+
|
|
87
|
+
for (const root of nodes) {
|
|
88
|
+
if (state.get(root.id) !== UNSEEN) continue
|
|
89
|
+
const stack = [{ id: root.id, next: 0 }]
|
|
90
|
+
state.set(root.id, OPEN)
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const frame = stack[stack.length - 1]
|
|
93
|
+
const edgesOut = outgoing.get(frame.id)
|
|
94
|
+
if (frame.next >= edgesOut.length) {
|
|
95
|
+
state.set(frame.id, DONE)
|
|
96
|
+
stack.pop()
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const { index, to } = edgesOut[frame.next]
|
|
100
|
+
frame.next += 1
|
|
101
|
+
const seen = state.get(to)
|
|
102
|
+
if (seen === OPEN) back.add(index)
|
|
103
|
+
else if (seen === UNSEEN) {
|
|
104
|
+
state.set(to, OPEN)
|
|
105
|
+
stack.push({ id: to, next: 0 })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return back
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Longest-path layering over the acyclic remainder of the spec. */
|
|
113
|
+
const assignLayers = (nodes, edges) => {
|
|
114
|
+
const back = findBackEdges(nodes, edges)
|
|
115
|
+
const forward = edges.filter((edge, index) => edge.from !== edge.to && !back.has(index))
|
|
116
|
+
const layer = new Map(nodes.map((node) => [node.id, 0]))
|
|
117
|
+
for (let pass = 0; pass < nodes.length; pass += 1) {
|
|
118
|
+
let changed = false
|
|
119
|
+
for (const edge of forward) {
|
|
120
|
+
const want = layer.get(edge.from) + 1
|
|
121
|
+
if (want > layer.get(edge.to)) {
|
|
122
|
+
layer.set(edge.to, want)
|
|
123
|
+
changed = true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!changed) break
|
|
127
|
+
}
|
|
128
|
+
return layer
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const half = (value) => Math.round(value / 2)
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Labels sit *beside* their connector, anchored at the start, never centred on
|
|
135
|
+
* it: a centred label lands exactly on the line it describes.
|
|
136
|
+
*/
|
|
137
|
+
const LABEL_ANCHOR = 'start'
|
|
138
|
+
const LABEL_INSET = 8
|
|
139
|
+
|
|
140
|
+
/** `M …` path for an edge that goes strictly downwards, layer to layer. */
|
|
141
|
+
const forwardPath = (a, b) => {
|
|
142
|
+
const x1 = a.cx
|
|
143
|
+
const y1 = a.y + NODE_H
|
|
144
|
+
const x2 = b.cx
|
|
145
|
+
const y2 = b.y
|
|
146
|
+
const bend = half(y2 - y1)
|
|
147
|
+
return {
|
|
148
|
+
d: `M ${x1} ${y1} C ${x1} ${y1 + bend} ${x2} ${y2 - bend} ${x2} ${y2}`,
|
|
149
|
+
labelX: half(x1 + x2) + LABEL_INSET,
|
|
150
|
+
labelY: half(y1 + y2) + 4,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `M …` path for a back edge or a same-layer edge: bulge out to the right. */
|
|
155
|
+
const backPath = (a, b) => {
|
|
156
|
+
const x1 = a.x + NODE_W
|
|
157
|
+
const y1 = a.cy
|
|
158
|
+
const x2 = b.x + NODE_W
|
|
159
|
+
const y2 = b.cy
|
|
160
|
+
const bulge = Math.max(x1, x2) + BACK_DIP
|
|
161
|
+
return {
|
|
162
|
+
d: `M ${x1} ${y1} C ${bulge} ${y1} ${bulge} ${y2} ${x2} ${y2}`,
|
|
163
|
+
labelX: bulge + LABEL_INSET,
|
|
164
|
+
labelY: half(y1 + y2) + 4,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `M …` path for a self-loop: a lobe off the node's right edge. */
|
|
169
|
+
const selfPath = (a) => {
|
|
170
|
+
const x1 = a.x + NODE_W
|
|
171
|
+
const y1 = a.y + Math.round(NODE_H / 3)
|
|
172
|
+
const y2 = a.y + Math.round((NODE_H * 2) / 3)
|
|
173
|
+
const out = x1 + BACK_DIP
|
|
174
|
+
return {
|
|
175
|
+
d: `M ${x1} ${y1} C ${out} ${y1} ${out} ${y2} ${x1} ${y2}`,
|
|
176
|
+
labelX: out + LABEL_INSET,
|
|
177
|
+
labelY: a.cy + 4,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Place every node on a (layer, row) grid, wrapping a layer whose siblings do
|
|
183
|
+
* not fit across `capacity` columns onto further rows of the same layer.
|
|
184
|
+
*
|
|
185
|
+
* Wrapping rather than shrinking is the whole point of taking a width: a
|
|
186
|
+
* narrower column must not make the boxes — and therefore the labels — smaller,
|
|
187
|
+
* or the picture stops being readable at exactly the moment it gets tight.
|
|
188
|
+
*/
|
|
189
|
+
const gridPositions = (nodes, layer, capacity) => {
|
|
190
|
+
const seenPerLayer = new Map()
|
|
191
|
+
const cells = new Map()
|
|
192
|
+
const rowsBefore = new Map()
|
|
193
|
+
let totalRows = 0
|
|
194
|
+
for (const node of nodes) {
|
|
195
|
+
const l = layer.get(node.id)
|
|
196
|
+
const seen = seenPerLayer.get(l) ?? 0
|
|
197
|
+
seenPerLayer.set(l, seen + 1)
|
|
198
|
+
cells.set(node.id, { layer: l, column: seen % capacity, wrap: Math.floor(seen / capacity) })
|
|
199
|
+
}
|
|
200
|
+
// Layers are laid out top to bottom, and a wrapped layer occupies more than
|
|
201
|
+
// one row, so a layer's vertical offset is the number of rows above it.
|
|
202
|
+
for (const l of [...seenPerLayer.keys()].sort((a, b) => a - b)) {
|
|
203
|
+
rowsBefore.set(l, totalRows)
|
|
204
|
+
totalRows += Math.ceil(seenPerLayer.get(l) / capacity)
|
|
205
|
+
}
|
|
206
|
+
const widestRow = Math.max(
|
|
207
|
+
...[...seenPerLayer.values()].map((count) => Math.min(count, capacity)),
|
|
208
|
+
)
|
|
209
|
+
return { cells, rowsBefore, totalRows, widestRow }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Deterministic layered layout for a `kind: "graph"` diagram.
|
|
214
|
+
*
|
|
215
|
+
* Callers must have passed `diagramProblems` / `assertDiagrams` first; this
|
|
216
|
+
* function assumes a valid spec and does no error reporting of its own.
|
|
217
|
+
*
|
|
218
|
+
* @param {object} block a validated `diagram` block
|
|
219
|
+
* @param {{measure?: number}} [options] available width in CSS pixels
|
|
220
|
+
* @returns {{width: number, height: number,
|
|
221
|
+
* nodes: Array<{id: string, label: string, x: number, y: number, w: number, h: number,
|
|
222
|
+
* cx: number, cy: number}>,
|
|
223
|
+
* edges: Array<{from: string, to: string, label: string|undefined, d: string,
|
|
224
|
+
* labelX: number, labelY: number}>}}
|
|
225
|
+
*/
|
|
226
|
+
export function layoutGraph(block, options = {}) {
|
|
227
|
+
const measure = Number.isFinite(options.measure) ? options.measure : DEFAULT_MEASURE
|
|
228
|
+
const nodes = block.nodes
|
|
229
|
+
const edges = block.edges ?? []
|
|
230
|
+
const layer = assignLayers(nodes, edges)
|
|
231
|
+
const { cells, rowsBefore, totalRows, widestRow } = gridPositions(
|
|
232
|
+
nodes,
|
|
233
|
+
layer,
|
|
234
|
+
rowCapacity(measure),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const placed = nodes.map((node) => {
|
|
238
|
+
const cell = cells.get(node.id)
|
|
239
|
+
const x = PAD + cell.column * (NODE_W + ROW_GAP)
|
|
240
|
+
const y = PAD + (rowsBefore.get(cell.layer) + cell.wrap) * (NODE_H + LAYER_GAP)
|
|
241
|
+
return {
|
|
242
|
+
id: node.id,
|
|
243
|
+
label: node.label,
|
|
244
|
+
x,
|
|
245
|
+
y,
|
|
246
|
+
w: NODE_W,
|
|
247
|
+
h: NODE_H,
|
|
248
|
+
cx: x + half(NODE_W),
|
|
249
|
+
cy: y + half(NODE_H),
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
const byId = new Map(placed.map((node) => [node.id, node]))
|
|
253
|
+
|
|
254
|
+
let hasBackEdge = false
|
|
255
|
+
const drawn = edges.map((edge) => {
|
|
256
|
+
const a = byId.get(edge.from)
|
|
257
|
+
const b = byId.get(edge.to)
|
|
258
|
+
let geometry
|
|
259
|
+
if (edge.from === edge.to) {
|
|
260
|
+
geometry = selfPath(a)
|
|
261
|
+
hasBackEdge = true
|
|
262
|
+
} else if (b.y > a.y) geometry = forwardPath(a, b)
|
|
263
|
+
else {
|
|
264
|
+
geometry = backPath(a, b)
|
|
265
|
+
hasBackEdge = true
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
from: edge.from,
|
|
269
|
+
to: edge.to,
|
|
270
|
+
label: edge.label,
|
|
271
|
+
labelAnchor: LABEL_ANCHOR,
|
|
272
|
+
...geometry,
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
width: PAD * 2 + widestRow * (NODE_W + ROW_GAP) - ROW_GAP + (hasBackEdge ? BACK_BAND : 0),
|
|
278
|
+
height: PAD * 2 + totalRows * (NODE_H + LAYER_GAP) - LAYER_GAP,
|
|
279
|
+
nodes: placed,
|
|
280
|
+
edges: drawn,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error vocabulary. Every user-facing failure carries a KSB_ code and a block
|
|
3
|
+
* path so Agents get machine-actionable feedback (SPEC §10.3).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const EXIT = Object.freeze({
|
|
7
|
+
OK: 0,
|
|
8
|
+
VALIDATION: 1,
|
|
9
|
+
USAGE: 2,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const CODES = Object.freeze({
|
|
13
|
+
INPUT_NOT_FOUND: 'KSB_INPUT_NOT_FOUND',
|
|
14
|
+
INPUT_EMPTY: 'KSB_INPUT_EMPTY',
|
|
15
|
+
PARSE_FAILED: 'KSB_PARSE_FAILED',
|
|
16
|
+
TEMPLATE_NOT_FOUND: 'KSB_TEMPLATE_NOT_FOUND',
|
|
17
|
+
TEMPLATE_BLOCK_UNSUPPORTED: 'KSB_TEMPLATE_BLOCK_UNSUPPORTED',
|
|
18
|
+
WRITE_FAILED: 'KSB_WRITE_FAILED',
|
|
19
|
+
USAGE: 'KSB_USAGE',
|
|
20
|
+
ARTIFACT_UNREADABLE: 'KSB_ARTIFACT_UNREADABLE',
|
|
21
|
+
ARTIFACT_NOT_FOUND: 'KSB_ARTIFACT_NOT_FOUND',
|
|
22
|
+
OPEN_FAILED: 'KSB_OPEN_FAILED',
|
|
23
|
+
IR_MISSING: 'KSB_IR_MISSING',
|
|
24
|
+
IR_DUPLICATE: 'KSB_IR_DUPLICATE',
|
|
25
|
+
IR_UNPARSABLE: 'KSB_IR_UNPARSABLE',
|
|
26
|
+
IR_SCHEMA: 'KSB_IR_SCHEMA',
|
|
27
|
+
EXTERNAL_SCRIPT: 'KSB_EXTERNAL_SCRIPT',
|
|
28
|
+
EXTERNAL_STYLESHEET: 'KSB_EXTERNAL_STYLESHEET',
|
|
29
|
+
EXTERNAL_IMAGE: 'KSB_EXTERNAL_IMAGE',
|
|
30
|
+
EXTERNAL_CSS_IMPORT: 'KSB_EXTERNAL_CSS_IMPORT',
|
|
31
|
+
EXTERNAL_CSS_URL: 'KSB_EXTERNAL_CSS_URL',
|
|
32
|
+
BANNED_FONT: 'KSB_BANNED_FONT',
|
|
33
|
+
DIAGRAM_INVALID: 'KSB_DIAGRAM_INVALID',
|
|
34
|
+
/** F6a — a board card pointing at a lane nobody declared (silent card loss). */
|
|
35
|
+
BOARD_INVALID: 'KSB_BOARD_INVALID',
|
|
36
|
+
/** F6a — a graph edge pointing at a node nobody declared (silent edge loss). */
|
|
37
|
+
GRAPH_INVALID: 'KSB_GRAPH_INVALID',
|
|
38
|
+
EXPORT_FORMAT_MISMATCH: 'KSB_EXPORT_FORMAT_MISMATCH',
|
|
39
|
+
BROWSER_MISSING: 'KSB_BROWSER_MISSING',
|
|
40
|
+
EXPORT_FAILED: 'KSB_EXPORT_FAILED',
|
|
41
|
+
/** S5 — a comment anchored to a block the artifact's IR does not contain. */
|
|
42
|
+
BLOCK_NOT_FOUND: 'KSB_BLOCK_NOT_FOUND',
|
|
43
|
+
/** S5 — `comments resolve` naming a comment id no record carries. */
|
|
44
|
+
COMMENT_NOT_FOUND: 'KSB_COMMENT_NOT_FOUND',
|
|
45
|
+
/** S5 — the dev preview server could not be started (port taken, no handshake). */
|
|
46
|
+
SERVE_START_FAILED: 'KSB_SERVE_START_FAILED',
|
|
47
|
+
/** S5 — `--port` that is not a legal TCP port. */
|
|
48
|
+
SERVE_PORT_INVALID: 'KSB_SERVE_PORT_INVALID',
|
|
49
|
+
/** S5 — a recorded server could not be terminated. */
|
|
50
|
+
SERVE_CLOSE_FAILED: 'KSB_SERVE_CLOSE_FAILED',
|
|
51
|
+
/** F4 — a command module could not be loaded (broken or partial install). */
|
|
52
|
+
COMMAND_LOAD_FAILED: 'KSB_COMMAND_LOAD_FAILED',
|
|
53
|
+
/** F2a — a 24-grid placement that cannot exist on a 24-column grid. */
|
|
54
|
+
PLACEMENT_INVALID: 'KSB_PLACEMENT_INVALID',
|
|
55
|
+
/** F2a — a template naming a 文體 nobody registered. */
|
|
56
|
+
LAYOUT_NOT_FOUND: 'KSB_LAYOUT_NOT_FOUND',
|
|
57
|
+
/** F2a — canvas geometry that contradicts itself (measure wider than maxWidth). */
|
|
58
|
+
LAYOUT_CANVAS_INVALID: 'KSB_LAYOUT_CANVAS_INVALID',
|
|
59
|
+
/** F2a — a template filling a chrome slot its layout does not offer. */
|
|
60
|
+
LAYOUT_SLOT_UNKNOWN: 'KSB_LAYOUT_SLOT_UNKNOWN',
|
|
61
|
+
/** F2a — a template restating a root form its 文體 does not require. */
|
|
62
|
+
LAYOUT_ROOT_CONFLICT: 'KSB_LAYOUT_ROOT_CONFLICT',
|
|
63
|
+
/** F3 — a store template package that is incomplete or declares an illegal field. */
|
|
64
|
+
TEMPLATE_PACKAGE_INVALID: 'KSB_TEMPLATE_PACKAGE_INVALID',
|
|
65
|
+
/** F3 — `init` aimed at a package directory that already holds a package. */
|
|
66
|
+
TEMPLATE_PACKAGE_EXISTS: 'KSB_TEMPLATE_PACKAGE_EXISTS',
|
|
67
|
+
/**
|
|
68
|
+
* F3 — a package's JS extension (rotor override / plugin block) would not load
|
|
69
|
+
* or would not pass the same shape check the core registries apply.
|
|
70
|
+
*
|
|
71
|
+
* It gets its own code for the reason F4 B9 names: falling back to
|
|
72
|
+
* `KSB_PARSE_FAILED` would tell an agent its *document* is broken when the
|
|
73
|
+
* truth is a package on disk, and it would go rewrite a document that was
|
|
74
|
+
* never at fault.
|
|
75
|
+
*/
|
|
76
|
+
TEMPLATE_EXTENSION_FAILED: 'KSB_TEMPLATE_EXTENSION_FAILED',
|
|
77
|
+
/** F3 — a store package squatting on a namespace the factory templates own. */
|
|
78
|
+
TEMPLATE_NAMESPACE_RESERVED: 'KSB_TEMPLATE_NAMESPACE_RESERVED',
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* F7b core — a `src` the browser would have to *fetch*.
|
|
82
|
+
*
|
|
83
|
+
* `KSB_EXTERNAL_IMAGE` only ever caught `src="http`, because CONTRACT A3's
|
|
84
|
+
* verbatim 禁形 list names that form. A **relative** `src="./fig.png"` breaks
|
|
85
|
+
* the same promise and passed every gate: the artifact renders on the machine
|
|
86
|
+
* that drew it and comes up broken everywhere else, which is the failure mode
|
|
87
|
+
* 「離線單檔」 exists to make impossible.
|
|
88
|
+
*/
|
|
89
|
+
EXTERNAL_ASSET_SRC: 'KSB_EXTERNAL_ASSET_SRC',
|
|
90
|
+
/** F7b core — a `<script>` inside an `<svg>`: a figure that executes. */
|
|
91
|
+
SVG_SCRIPT: 'KSB_SVG_SCRIPT',
|
|
92
|
+
/** F7b core — an `on…=` event attribute inside an `<svg>`. */
|
|
93
|
+
SVG_EVENT_HANDLER: 'KSB_SVG_EVENT_HANDLER',
|
|
94
|
+
/** F7b core — a `javascript:` URL inside an `<svg>`. */
|
|
95
|
+
SVG_JAVASCRIPT_URL: 'KSB_SVG_JAVASCRIPT_URL',
|
|
96
|
+
/** F7b core — `rgba()` inside an `<svg>`: alpha the export chain drops. */
|
|
97
|
+
SVG_RGBA: 'KSB_SVG_RGBA',
|
|
98
|
+
/** F7b core — a marker reference no `<marker id>` answers (silent arrowhead loss). */
|
|
99
|
+
SVG_MARKER_DANGLING: 'KSB_SVG_MARKER_DANGLING',
|
|
100
|
+
|
|
101
|
+
/** F7b — a manifest `[gates]` section that declares something illegal. */
|
|
102
|
+
GATE_DECLARATION_INVALID: 'KSB_GATE_DECLARATION_INVALID',
|
|
103
|
+
/** F7b gate — body line-height above the ceiling the package declared. */
|
|
104
|
+
GATE_LINE_HEIGHT: 'KSB_GATE_LINE_HEIGHT',
|
|
105
|
+
/** F7b gate — a heading rule heavier than the package declared. */
|
|
106
|
+
GATE_HEADING_WEIGHT: 'KSB_GATE_HEADING_WEIGHT',
|
|
107
|
+
/** F7b gate — a box-shadow blurrier-than-declared-minimum (a hard shadow). */
|
|
108
|
+
GATE_SHADOW_BLUR: 'KSB_GATE_SHADOW_BLUR',
|
|
109
|
+
/** F7b gate — `font-style: italic` in a package that declared italic off. */
|
|
110
|
+
GATE_ITALIC: 'KSB_GATE_ITALIC',
|
|
111
|
+
/** F7b gate — a colour the package's own palette put on its banned list. */
|
|
112
|
+
GATE_BANNED_HEX: 'KSB_GATE_BANNED_HEX',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/** Root path token used when a finding cannot be tied to a single block. */
|
|
116
|
+
export const ROOT_PATH = '$'
|
|
117
|
+
|
|
118
|
+
export class KsbError extends Error {
|
|
119
|
+
constructor({ code, message, path = ROOT_PATH, exitCode = EXIT.VALIDATION }) {
|
|
120
|
+
super(message)
|
|
121
|
+
this.name = 'KsbError'
|
|
122
|
+
this.code = code
|
|
123
|
+
this.path = path
|
|
124
|
+
this.exitCode = exitCode
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Serialisable finding, shape pinned by CONTRACT lint --json surface. */
|
|
128
|
+
toFinding() {
|
|
129
|
+
return { path: this.path, code: this.code, message: this.message }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function usageError(message, code = CODES.USAGE) {
|
|
134
|
+
return new KsbError({ code, message, exitCode: EXIT.USAGE })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function validationError(message, code, path = ROOT_PATH) {
|
|
138
|
+
return new KsbError({ code, message, path, exitCode: EXIT.VALIDATION })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A command module that would not load — a broken or partial install, never a
|
|
143
|
+
* problem with what the user typed.
|
|
144
|
+
*
|
|
145
|
+
* It gets its own code because the fallback, `KSB_PARSE_FAILED`, means "your
|
|
146
|
+
* input could not be parsed": an agent reading that after a half-installed
|
|
147
|
+
* package would go and rewrite a document that was never at fault. The
|
|
148
|
+
* resolver's own words are kept, since they name the missing file.
|
|
149
|
+
*/
|
|
150
|
+
export function commandLoadError(command, cause) {
|
|
151
|
+
return new KsbError({
|
|
152
|
+
code: CODES.COMMAND_LOAD_FAILED,
|
|
153
|
+
message: `指令模組 \`${command}\` 載入失敗(安裝可能不完整):${cause?.message ?? String(cause)}`,
|
|
154
|
+
exitCode: EXIT.VALIDATION,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { blockModule, blockTypes } from '../blocks/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canonical Markdown-superset example. It must always be valid input to
|
|
5
|
+
* `render` — that round-trip is the SDK's anti-trial-and-error guarantee
|
|
6
|
+
* (SPEC §4「CLI 必須提供 example/schema,讓 Agent 免反覆試錯」).
|
|
7
|
+
*
|
|
8
|
+
* `doc` is the one kind with no block module: it is the envelope every block
|
|
9
|
+
* sits in, not a block. Every other example comes from its own module.
|
|
10
|
+
*/
|
|
11
|
+
export const EXAMPLE_DOC = `---
|
|
12
|
+
title: 範例文件
|
|
13
|
+
kicker: KAMISHIBAI EXAMPLE
|
|
14
|
+
author: kamishibai
|
|
15
|
+
template: kami/long-form
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# 第一章 散文與引言
|
|
19
|
+
|
|
20
|
+
這是 **prose** 區塊,支援行內 Markdown:粗體、\`行內程式碼\`、以及[連結](https://example.com/)。
|
|
21
|
+
|
|
22
|
+
> 這是 quote 區塊:引用一段話。
|
|
23
|
+
|
|
24
|
+
## 第一節 程式碼、表格與提示框
|
|
25
|
+
|
|
26
|
+
\`\`\`js
|
|
27
|
+
export const hello = (name) => \`你好,\${name}\`
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
| block | 用途 |
|
|
31
|
+
|-------|------|
|
|
32
|
+
| prose | 散文段落 |
|
|
33
|
+
| table | 表格資料 |
|
|
34
|
+
|
|
35
|
+
清單是真正的 \`list\` block——項目內容是子 block,所以清單裡放 callout 不會拆掉外框:
|
|
36
|
+
|
|
37
|
+
- 項目一
|
|
38
|
+
- 項目二
|
|
39
|
+
:::note
|
|
40
|
+
項目內的 callout 仍走同一個 Callout 元件。
|
|
41
|
+
:::
|
|
42
|
+
- 項目三
|
|
43
|
+
|
|
44
|
+
1. 有序項目一
|
|
45
|
+
2. 有序項目二
|
|
46
|
+
|
|
47
|
+
:::note
|
|
48
|
+
這是 callout(note 型)。另有 warn 型。
|
|
49
|
+
:::
|
|
50
|
+
|
|
51
|
+
:::warn
|
|
52
|
+
警示型 callout 長這樣。
|
|
53
|
+
:::
|
|
54
|
+
|
|
55
|
+
# 第二章 結構化圖表
|
|
56
|
+
|
|
57
|
+
下面這個 diagram fence 的內容是 **JSON**,欄位就是 IR 的 diagram block;版面由 SDK
|
|
58
|
+
算出,不必手畫座標。v1 只有一種 kind:有向的節點邊圖。
|
|
59
|
+
|
|
60
|
+
\`\`\`diagram
|
|
61
|
+
{
|
|
62
|
+
"kind": "graph",
|
|
63
|
+
"nodes": [
|
|
64
|
+
{ "id": "input", "label": "來源語料" },
|
|
65
|
+
{ "id": "ir", "label": "block tree" },
|
|
66
|
+
{ "id": "artifact", "label": "離線產物" }
|
|
67
|
+
],
|
|
68
|
+
"edges": [
|
|
69
|
+
{ "from": "input", "to": "ir", "label": "parse" },
|
|
70
|
+
{ "from": "ir", "to": "artifact", "label": "render" },
|
|
71
|
+
{ "from": "artifact", "to": "ir", "label": "replay" }
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
# 第三章 raw 島嶼
|
|
77
|
+
|
|
78
|
+
島嶼是逃生艙:模板敘述不出來的東西才手寫,並以 intent 記錄原因。
|
|
79
|
+
|
|
80
|
+
\`\`\`raw-html intent="示範 raw 島嶼的宣告方式"
|
|
81
|
+
<div class="example-island">手寫 HTML 島嶼</div>
|
|
82
|
+
\`\`\`
|
|
83
|
+
`
|
|
84
|
+
|
|
85
|
+
/** Re-exported for callers that want the deck teaching source by name. */
|
|
86
|
+
export { EXAMPLE_DECK } from '../blocks/deck.js'
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `doc` plus every registered block type.
|
|
90
|
+
*
|
|
91
|
+
* A function, not a frozen array: the vocabulary grows when a plugin registers,
|
|
92
|
+
* and a list snapshotted at import would answer `unknown example kind "x-…"`
|
|
93
|
+
* for a block the renderer had just drawn.
|
|
94
|
+
*/
|
|
95
|
+
export const exampleKinds = () => ['doc', ...blockTypes()]
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} kind `doc`/`deck` for the Markdown superset, or a block type name
|
|
99
|
+
* @returns {{kind: string, example: string}|null} null when the kind is unknown
|
|
100
|
+
*/
|
|
101
|
+
export function exampleFor(kind) {
|
|
102
|
+
if (kind === 'doc') return { kind, example: EXAMPLE_DOC }
|
|
103
|
+
const mod = blockModule(kind)
|
|
104
|
+
if (mod === undefined) return null
|
|
105
|
+
// A module may teach with a whole source file (the kinds that can be piped
|
|
106
|
+
// straight back into `render -`) or with one block, never both.
|
|
107
|
+
if (typeof mod.example.source === 'string') return { kind, example: mod.example.source }
|
|
108
|
+
return { kind, example: JSON.stringify(mod.example.block, null, 2) }
|
|
109
|
+
}
|
package/src/core/ir.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { engineVersion } from './version.js'
|
|
2
|
+
|
|
3
|
+
export const IR_VERSION = '1'
|
|
4
|
+
export const DEFAULT_GENERATOR = 'kamishibai-cli'
|
|
5
|
+
export const BUILD_TIME_ENV = 'KAMISHIBAI_BUILD_TIME'
|
|
6
|
+
/** SPEC §7.3 / issues/11 P4 — the embedding media type, verbatim. */
|
|
7
|
+
export const IR_SCRIPT_TYPE = 'application/kamishibai+json'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic build timestamp: KAMISHIBAI_BUILD_TIME wins so that repeated
|
|
11
|
+
* renders of the same input are byte-identical (CONTRACT A8).
|
|
12
|
+
*/
|
|
13
|
+
export function resolveCreatedAt(env = {}, now = () => new Date()) {
|
|
14
|
+
const pinned = env[BUILD_TIME_ENV]
|
|
15
|
+
if (typeof pinned === 'string' && pinned.trim().length > 0) return pinned.trim()
|
|
16
|
+
return now().toISOString()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Timestamp for a replay. A pinned `KAMISHIBAI_BUILD_TIME` still wins, but the
|
|
21
|
+
* fallback is the *original* artifact's `createdAt` rather than "now": replay
|
|
22
|
+
* reproduces a document from its own embedded record, so an unpinned replay
|
|
23
|
+
* that stamped the current clock would silently break byte-identity (B6).
|
|
24
|
+
*/
|
|
25
|
+
export function replayCreatedAt(env = {}, ir = {}) {
|
|
26
|
+
const pinned = env[BUILD_TIME_ENV]
|
|
27
|
+
if (typeof pinned === 'string' && pinned.trim().length > 0) return pinned.trim()
|
|
28
|
+
if (typeof ir.createdAt === 'string' && ir.createdAt.length > 0) return ir.createdAt
|
|
29
|
+
return resolveCreatedAt(env)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `<namespace>/<name>` — the template key an IR envelope was rendered with. */
|
|
33
|
+
export function templateKeyOf(ir) {
|
|
34
|
+
const t = ir?.template
|
|
35
|
+
if (!t || typeof t.namespace !== 'string' || typeof t.name !== 'string') return undefined
|
|
36
|
+
return `${t.namespace}/${t.name}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `<namespace>/<name>@<version>` — the display form used by `list`. */
|
|
40
|
+
export function templateLabelOf(ir) {
|
|
41
|
+
const key = templateKeyOf(ir)
|
|
42
|
+
return key === undefined ? '' : `${key}@${ir.template.version}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assemble the embedded IR envelope (SPEC §7.3). Key order is fixed so that
|
|
47
|
+
* JSON.stringify output is stable.
|
|
48
|
+
*/
|
|
49
|
+
export function buildIr({ doc, template, createdAt, generator = DEFAULT_GENERATOR }) {
|
|
50
|
+
return {
|
|
51
|
+
irVersion: IR_VERSION,
|
|
52
|
+
engine: engineVersion(),
|
|
53
|
+
template: {
|
|
54
|
+
namespace: template.namespace,
|
|
55
|
+
name: template.name,
|
|
56
|
+
version: template.version,
|
|
57
|
+
},
|
|
58
|
+
doc,
|
|
59
|
+
createdAt,
|
|
60
|
+
generator,
|
|
61
|
+
}
|
|
62
|
+
}
|