@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,223 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it'
|
|
2
|
+
import matter from 'gray-matter'
|
|
3
|
+
import * as B from '../core/blocks.js'
|
|
4
|
+
import { CODES, validationError } from '../core/errors.js'
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LAYOUT,
|
|
7
|
+
defaultTemplateOf,
|
|
8
|
+
layoutModules,
|
|
9
|
+
templateLayoutEntries,
|
|
10
|
+
} from '../layouts/index.js'
|
|
11
|
+
import { containerPlugin } from './container.js'
|
|
12
|
+
import { walkTokens, nestSections } from './tokens.js'
|
|
13
|
+
|
|
14
|
+
/** Verbatim — the root form that makes an artifact a deck. */
|
|
15
|
+
const DECK_ROOT = 'deck'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Which template draws a document nobody named a template for.
|
|
19
|
+
*
|
|
20
|
+
* Derived, not typed: every layout ships a built-in generic template, so the
|
|
21
|
+
* resolution chain is 指名模板 → that 文體's default, and `render` can never fail
|
|
22
|
+
* for want of a template (CONTRACT C1 補丁). What used to be one global default
|
|
23
|
+
* is now one per 文體, and this is the flowing one.
|
|
24
|
+
*
|
|
25
|
+
* A function, not a constant, for the same reason `blockTypes()` is one: a list
|
|
26
|
+
* frozen at import is a list a third party can never appear in. Registration
|
|
27
|
+
* happens at whatever moment a plugin is loaded, which is not guaranteed to be
|
|
28
|
+
* before this module's first evaluation.
|
|
29
|
+
*/
|
|
30
|
+
export const defaultTemplateKey = () => defaultTemplateOf(DEFAULT_LAYOUT)
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The template keys under which `---` stops being decoration and cuts a slide.
|
|
34
|
+
*
|
|
35
|
+
* A page break is a *文體* fact, not a template's: it means something exactly
|
|
36
|
+
* where the artifact has pages. Read off the registry on every call, so a
|
|
37
|
+
* third-party deck layout's built-in template inherits it — the frozen version
|
|
38
|
+
* of this list could only ever contain `kami/slides`, which made the sentence
|
|
39
|
+
* below a claim the code did not keep.
|
|
40
|
+
*
|
|
41
|
+
* **Installed packages count too** (F2f, closing the F3 deferral). Until F2f a
|
|
42
|
+
* store package riding the `deck` 文體 got no `---` page break here, because
|
|
43
|
+
* resolving an arbitrary key to a manifest means reading the store — and this
|
|
44
|
+
* layer may not reach for it (issues/08). That deferral is why F3's `init` had
|
|
45
|
+
* to refuse `-l deck` outright: the alternative was scaffolding a deck package
|
|
46
|
+
* whose pages silently never split. The dependency is now inverted instead
|
|
47
|
+
* (`src/layouts/template-index.js`), so both halves land together: the key is
|
|
48
|
+
* read off an index the delivery layer fills, and this layer still imports
|
|
49
|
+
* nothing it did not import before.
|
|
50
|
+
*
|
|
51
|
+
* Read on every call, never frozen at import: a package installed one command
|
|
52
|
+
* ago is a package a frozen list could never contain.
|
|
53
|
+
*/
|
|
54
|
+
export const deckTemplateKeys = () => {
|
|
55
|
+
const decks = layoutModules().filter((layout) => layout.rootForm === DECK_ROOT)
|
|
56
|
+
const names = new Set(decks.map((layout) => layout.name))
|
|
57
|
+
return [
|
|
58
|
+
...new Set([
|
|
59
|
+
...decks.map((layout) => layout.defaultTemplate),
|
|
60
|
+
...templateLayoutEntries()
|
|
61
|
+
.filter((entry) => names.has(entry.layout))
|
|
62
|
+
.map((entry) => entry.key),
|
|
63
|
+
]),
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `<namespace>/<name>` — the version suffix is advisory (see render/templates.js). */
|
|
68
|
+
const bareKey = (key) => String(key ?? '').split('@')[0]
|
|
69
|
+
|
|
70
|
+
const createMd = () => {
|
|
71
|
+
const md = new MarkdownIt({ html: true, linkify: false, typographer: false })
|
|
72
|
+
md.use(containerPlugin)
|
|
73
|
+
return md
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pad2 = (value) => String(value).padStart(2, '0')
|
|
77
|
+
|
|
78
|
+
/** `YYYY-MM-DD` assembled from calendar *fields* — never sliced off an ISO string. */
|
|
79
|
+
const localCalendarDay = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
|
80
|
+
const utcCalendarDay = (d) =>
|
|
81
|
+
`${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* YAML resolves a bare `date: 2026-08-17` to exactly UTC midnight: that value is
|
|
85
|
+
* a *calendar day*, not an instant, so reading it back in UTC is what keeps it
|
|
86
|
+
* stable everywhere. Anything with a time component is a real instant.
|
|
87
|
+
*/
|
|
88
|
+
const isCalendarDayOnly = (d) =>
|
|
89
|
+
d.getUTCHours() === 0 &&
|
|
90
|
+
d.getUTCMinutes() === 0 &&
|
|
91
|
+
d.getUTCSeconds() === 0 &&
|
|
92
|
+
d.getUTCMilliseconds() === 0
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Coerce one frontmatter value into a meta string, or `undefined` to drop it.
|
|
96
|
+
*
|
|
97
|
+
* Two failures live here, both of them silent:
|
|
98
|
+
*
|
|
99
|
+
* - A *bare* `date:` is a Date object, so a `typeof === 'string'` gate threw
|
|
100
|
+
* the field away entirely — quoted dates shipped, unquoted ones vanished.
|
|
101
|
+
* - `toISOString().slice(0, 10)` re-reads the instant in UTC, so
|
|
102
|
+
* `2026-08-18 01:00:00 +08:00` displayed as 2026-08-17. A date the author
|
|
103
|
+
* wrote by hand must not come back a day earlier.
|
|
104
|
+
*
|
|
105
|
+
* So the day is assembled from calendar fields, picking the calendar the value
|
|
106
|
+
* was written in (SPEC-facing normal form: `YYYY-MM-DD`).
|
|
107
|
+
*/
|
|
108
|
+
const asMetaString = (value) => {
|
|
109
|
+
if (typeof value === 'string') return value.length > 0 ? value : undefined
|
|
110
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
111
|
+
return isCalendarDayOnly(value) ? utcCalendarDay(value) : localCalendarDay(value)
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Frontmatter → doc meta with a deterministic key order. */
|
|
117
|
+
const buildMeta = (data) => {
|
|
118
|
+
const meta = {}
|
|
119
|
+
for (const key of B.META_KEY_ORDER) {
|
|
120
|
+
const value = asMetaString(data[key])
|
|
121
|
+
if (value !== undefined) meta[key] = value
|
|
122
|
+
}
|
|
123
|
+
const extras = Object.keys(data)
|
|
124
|
+
.filter((k) => !B.META_KEY_ORDER.includes(k) && k !== 'template')
|
|
125
|
+
.sort()
|
|
126
|
+
for (const key of extras) {
|
|
127
|
+
const value = asMetaString(data[key])
|
|
128
|
+
if (value !== undefined) meta[key] = value
|
|
129
|
+
}
|
|
130
|
+
if (meta.title === undefined) meta.title = 'Untitled'
|
|
131
|
+
return meta
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Cut the flat item stream into slides on `hr` markers and wrap them in a deck.
|
|
136
|
+
* N separators yield N+1 slides — the segment count of the source, verbatim.
|
|
137
|
+
*/
|
|
138
|
+
const buildDeck = (items) => {
|
|
139
|
+
const groups = [[]]
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
if (item.kind === 'hr') groups.push([])
|
|
142
|
+
else groups[groups.length - 1].push(item)
|
|
143
|
+
}
|
|
144
|
+
return B.create.deck({ slides: groups.map((group) => B.create.slide({ children: nestSections(group) })) })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Compile the Markdown superset (frontmatter + fenced block types + `:::`
|
|
149
|
+
* callouts) into a canonical block tree.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} source
|
|
152
|
+
* @param {{template?: string}} [options] CLI `-t` override; it decides deck mode
|
|
153
|
+
* just as frontmatter `template:` does, so the two entry points cannot disagree.
|
|
154
|
+
* @returns {{doc: object, templateKey: string}}
|
|
155
|
+
*/
|
|
156
|
+
export function parseMarkdown(source, options = {}) {
|
|
157
|
+
if (typeof source !== 'string' || source.trim().length === 0) {
|
|
158
|
+
throw validationError('input is empty', CODES.INPUT_EMPTY, 'doc')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let front
|
|
162
|
+
try {
|
|
163
|
+
front = matter(source)
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
throw validationError(
|
|
166
|
+
`frontmatter is not valid YAML: ${cause.message}`,
|
|
167
|
+
CODES.PARSE_FAILED,
|
|
168
|
+
'doc.meta',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const md = createMd()
|
|
173
|
+
const env = {}
|
|
174
|
+
let tokens
|
|
175
|
+
try {
|
|
176
|
+
tokens = md.parse(front.content, env)
|
|
177
|
+
} catch (cause) {
|
|
178
|
+
throw validationError(
|
|
179
|
+
`markdown could not be compiled: ${cause.message}`,
|
|
180
|
+
CODES.PARSE_FAILED,
|
|
181
|
+
'doc',
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ctx = {
|
|
186
|
+
inline: (token) => (token ? md.renderer.renderInline(token.children ?? [], md.options, env) : ''),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const meta = buildMeta(front.data ?? {})
|
|
190
|
+
const declared =
|
|
191
|
+
typeof front.data?.template === 'string' && front.data.template.length > 0
|
|
192
|
+
? front.data.template
|
|
193
|
+
: defaultTemplateKey()
|
|
194
|
+
const templateKey =
|
|
195
|
+
typeof options.template === 'string' && options.template.length > 0
|
|
196
|
+
? options.template
|
|
197
|
+
: declared
|
|
198
|
+
|
|
199
|
+
const flat = walkTokens(tokens, ctx)
|
|
200
|
+
const children =
|
|
201
|
+
deckTemplateKeys().includes(bareKey(templateKey)) ? [buildDeck(flat)] : nestSections(flat)
|
|
202
|
+
|
|
203
|
+
return { doc: B.withIds(B.doc({ meta, children })), templateKey }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Parse a canonical block-tree JSON document (the data-view entry point). */
|
|
207
|
+
export function parseBlockTree(source) {
|
|
208
|
+
let parsed
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(source)
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
throw validationError(`input is not valid JSON: ${cause.message}`, CODES.PARSE_FAILED, 'doc')
|
|
213
|
+
}
|
|
214
|
+
const node = parsed?.type === 'doc' ? parsed : parsed?.doc
|
|
215
|
+
if (node?.type !== 'doc') {
|
|
216
|
+
throw validationError('JSON input has no `doc` block at its root', CODES.PARSE_FAILED, 'doc')
|
|
217
|
+
}
|
|
218
|
+
const templateKey =
|
|
219
|
+
typeof parsed?.template === 'string' && parsed.template.length > 0
|
|
220
|
+
? parsed.template
|
|
221
|
+
: defaultTemplateKey()
|
|
222
|
+
return { doc: B.withIds(node), templateKey }
|
|
223
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as B from '../core/blocks.js'
|
|
2
|
+
import { PLACEMENT_KEYS, containerSyntax } from '../blocks/index.js'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_INTENT = 'unspecified'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hand one `:::` fence back to the module that declared it.
|
|
8
|
+
*
|
|
9
|
+
* A module's `build` may answer with one block (`:::note` → a callout) or with
|
|
10
|
+
* a *list* of blocks (`:::place` → the blocks it contained, each now carrying a
|
|
11
|
+
* placement). The second shape is what lets placement live on the child, the
|
|
12
|
+
* way `grid-column` does in CSS, without inventing a cell block nobody asked
|
|
13
|
+
* for. An unclaimed name never reaches here: `container.js` refuses to open a
|
|
14
|
+
* fence the registry does not recognise.
|
|
15
|
+
*/
|
|
16
|
+
const buildContainer = (info, kids) => {
|
|
17
|
+
const raw = String(info ?? '').trim()
|
|
18
|
+
const name = raw.split(/\s+/)[0]
|
|
19
|
+
const mod = containerSyntax().get(name)
|
|
20
|
+
if (mod === undefined) return kids
|
|
21
|
+
const built = mod.syntax.build({ name, params: raw.slice(name.length).trim(), children: kids })
|
|
22
|
+
return Array.isArray(built) ? built : [built]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The placement fields a heading marker picked up from an enclosing `:::place`. */
|
|
26
|
+
const placementOf = (item) => {
|
|
27
|
+
const out = {}
|
|
28
|
+
for (const key of PLACEMENT_KEYS) {
|
|
29
|
+
if (item?.[key] !== undefined) out[key] = item[key]
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const findClose = (tokens, openIndex) => {
|
|
35
|
+
const openType = tokens[openIndex].type
|
|
36
|
+
const closeType = openType.replace(/_open$/, '_close')
|
|
37
|
+
let depth = 0
|
|
38
|
+
for (let i = openIndex; i < tokens.length; i += 1) {
|
|
39
|
+
if (tokens[i].type === openType) depth += 1
|
|
40
|
+
else if (tokens[i].type === closeType) {
|
|
41
|
+
depth -= 1
|
|
42
|
+
if (depth === 0) return i
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return tokens.length - 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rawSubtype = (info) => (info.startsWith('raw-svg') ? 'svg' : 'html')
|
|
49
|
+
|
|
50
|
+
const intentOf = (info, content) => {
|
|
51
|
+
const fromInfo = /intent=(?:"([^"]*)"|'([^']*)')/.exec(info)
|
|
52
|
+
if (fromInfo) return fromInfo[1] ?? fromInfo[2]
|
|
53
|
+
const fromAttr = /data-intent=(?:"([^"]*)"|'([^']*)')/.exec(content)
|
|
54
|
+
if (fromAttr) return fromAttr[1] ?? fromAttr[2]
|
|
55
|
+
return DEFAULT_INTENT
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ```` ```diagram ```` — the fence body is **JSON**, pinned (CONTRACT D2).
|
|
60
|
+
*
|
|
61
|
+
* JSON rather than YAML because the fence body and the IR block are then the
|
|
62
|
+
* same text: an author can paste `example diagram` straight into a fence, and
|
|
63
|
+
* the parser needs no second syntax whose failure modes differ from the IR's.
|
|
64
|
+
*
|
|
65
|
+
* A body that will not parse becomes an explicitly unrenderable diagram block
|
|
66
|
+
* rather than an exception here: by the time the render pipeline validates, the
|
|
67
|
+
* block has an id and therefore a block path to report (SPEC §10.3), which a
|
|
68
|
+
* throw from inside the token walk could not produce.
|
|
69
|
+
*/
|
|
70
|
+
const parseDiagram = (token) => {
|
|
71
|
+
let spec
|
|
72
|
+
try {
|
|
73
|
+
spec = JSON.parse(token.content)
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
return B.diagramParseError(`fence 內容不是合法 JSON:${cause.message}`)
|
|
76
|
+
}
|
|
77
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
78
|
+
return B.diagramParseError('fence 內容必須是一個 JSON 物件(diagram block 的欄位)')
|
|
79
|
+
}
|
|
80
|
+
return B.create.diagram({ kind: spec.kind, nodes: spec.nodes, edges: spec.edges })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parseFence = (token) => {
|
|
84
|
+
const info = (token.info ?? '').trim()
|
|
85
|
+
if (/^diagram\b/.test(info)) return parseDiagram(token)
|
|
86
|
+
if (/^raw(-html|-svg)?\b/.test(info)) {
|
|
87
|
+
return B.create.raw({
|
|
88
|
+
subtype: rawSubtype(info),
|
|
89
|
+
intent: intentOf(info, token.content),
|
|
90
|
+
html: token.content.replace(/\n+$/, ''),
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
return B.create.code({ lang: info.split(/\s+/)[0] ?? '', text: token.content.replace(/\n+$/, '') })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parseTable = (tokens, openIndex, inline) => {
|
|
97
|
+
const end = findClose(tokens, openIndex)
|
|
98
|
+
const head = []
|
|
99
|
+
const rows = []
|
|
100
|
+
let currentRow = null
|
|
101
|
+
let inHead = false
|
|
102
|
+
for (let i = openIndex + 1; i < end; i += 1) {
|
|
103
|
+
const t = tokens[i]
|
|
104
|
+
if (t.type === 'thead_open') inHead = true
|
|
105
|
+
else if (t.type === 'thead_close') inHead = false
|
|
106
|
+
else if (t.type === 'tr_open') currentRow = []
|
|
107
|
+
else if (t.type === 'tr_close') {
|
|
108
|
+
if (currentRow !== null && !inHead) rows.push(currentRow)
|
|
109
|
+
currentRow = null
|
|
110
|
+
} else if (t.type === 'th_open' || t.type === 'td_open') {
|
|
111
|
+
const cell = inline(tokens[i + 1])
|
|
112
|
+
if (inHead) head.push(cell)
|
|
113
|
+
else if (currentRow !== null) currentRow.push(cell)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { block: B.create.table({ head, rows }), next: end + 1 }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compile one `<ul>`/`<ol>` token run into a real `list` block.
|
|
121
|
+
*
|
|
122
|
+
* S1 flattened lists into a prose HTML string, which meant a callout inside a
|
|
123
|
+
* list item had to be rendered by markdown-it — a second rendering path that
|
|
124
|
+
* disagreed with the Callout component — unless the list frame was thrown away
|
|
125
|
+
* entirely. Both halves of that trade-off are gone once list items carry real
|
|
126
|
+
* blocks: the `<ul>` frame survives *and* every child stays in the block tree.
|
|
127
|
+
*/
|
|
128
|
+
const parseList = (tokens, openIndex, closeIndex, ctx, ordered) => {
|
|
129
|
+
const items = []
|
|
130
|
+
let i = openIndex + 1
|
|
131
|
+
while (i < closeIndex) {
|
|
132
|
+
if (tokens[i].type !== 'list_item_open') {
|
|
133
|
+
i += 1
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const itemClose = findClose(tokens, i)
|
|
137
|
+
items.push(walkTokens(tokens.slice(i + 1, itemClose), ctx))
|
|
138
|
+
i = itemClose + 1
|
|
139
|
+
}
|
|
140
|
+
return B.create.list({ ordered, items })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Walk a markdown-it token stream into flat block items. Headings become
|
|
145
|
+
* `{kind:'heading'}` markers and thematic breaks `{kind:'hr'}` markers;
|
|
146
|
+
* `nestSections` turns the former into section blocks and drops the latter,
|
|
147
|
+
* while deck compilation (parser/index.js) cuts slides on the latter.
|
|
148
|
+
*/
|
|
149
|
+
export function walkTokens(tokens, ctx) {
|
|
150
|
+
const { inline } = ctx
|
|
151
|
+
const out = []
|
|
152
|
+
let i = 0
|
|
153
|
+
|
|
154
|
+
while (i < tokens.length) {
|
|
155
|
+
const t = tokens[i]
|
|
156
|
+
switch (t.type) {
|
|
157
|
+
case 'heading_open': {
|
|
158
|
+
out.push({
|
|
159
|
+
kind: 'heading',
|
|
160
|
+
level: Number(t.tag.slice(1)),
|
|
161
|
+
title: tokens[i + 1].content,
|
|
162
|
+
})
|
|
163
|
+
i = findClose(tokens, i) + 1
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case 'paragraph_open': {
|
|
167
|
+
const close = findClose(tokens, i)
|
|
168
|
+
out.push(B.create.prose(inline(tokens[i + 1])))
|
|
169
|
+
i = close + 1
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
case 'fence': {
|
|
173
|
+
out.push(parseFence(t))
|
|
174
|
+
i += 1
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
case 'blockquote_open': {
|
|
178
|
+
const close = findClose(tokens, i)
|
|
179
|
+
out.push(B.create.quote(walkTokens(tokens.slice(i + 1, close), ctx)))
|
|
180
|
+
i = close + 1
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
case 'kami_container_open': {
|
|
184
|
+
const close = findClose(tokens, i)
|
|
185
|
+
out.push(...buildContainer(t.info, walkTokens(tokens.slice(i + 1, close), ctx)))
|
|
186
|
+
i = close + 1
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
case 'table_open': {
|
|
190
|
+
const { block, next } = parseTable(tokens, i, inline)
|
|
191
|
+
out.push(block)
|
|
192
|
+
i = next
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
case 'bullet_list_open':
|
|
196
|
+
case 'ordered_list_open': {
|
|
197
|
+
const close = findClose(tokens, i)
|
|
198
|
+
out.push(parseList(tokens, i, close, ctx, t.type === 'ordered_list_open'))
|
|
199
|
+
i = close + 1
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
case 'hr': {
|
|
203
|
+
out.push({ kind: 'hr' })
|
|
204
|
+
i += 1
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'html_block': {
|
|
208
|
+
out.push(
|
|
209
|
+
B.create.raw({
|
|
210
|
+
subtype: 'html',
|
|
211
|
+
intent: intentOf('', t.content),
|
|
212
|
+
html: t.content.replace(/\n+$/, ''),
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
i += 1
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
default:
|
|
219
|
+
i += 1
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Recurse into whichever nesting shape a container block uses. */
|
|
226
|
+
const nestChildren = (item) => {
|
|
227
|
+
if (Array.isArray(item.items)) return { ...item, items: item.items.map(nestSections) }
|
|
228
|
+
if (Array.isArray(item.children)) return { ...item, children: nestSections(item.children) }
|
|
229
|
+
return item
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Turn flat heading markers into a nested section tree (recursively).
|
|
234
|
+
*
|
|
235
|
+
* `hr` markers are dropped here: in document mode a thematic break carries no
|
|
236
|
+
* block meaning (a pre-existing, inventoried behaviour). Deck mode consumes
|
|
237
|
+
* them *before* calling this, which is the one place they mean anything.
|
|
238
|
+
*/
|
|
239
|
+
export function nestSections(items) {
|
|
240
|
+
const root = { children: [] }
|
|
241
|
+
const stack = [{ level: 0, node: root }]
|
|
242
|
+
|
|
243
|
+
for (const item of items) {
|
|
244
|
+
if (item.kind === 'hr') continue
|
|
245
|
+
if (item.kind === 'heading') {
|
|
246
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= item.level) stack.pop()
|
|
247
|
+
// A heading inside a `:::place` keeps the coordinates the fence gave it:
|
|
248
|
+
// `nestSections` builds a fresh node, so anything not copied across here
|
|
249
|
+
// is silently dropped — and a placement that vanishes leaves a document
|
|
250
|
+
// whose layout is not the one its author wrote.
|
|
251
|
+
const node = {
|
|
252
|
+
type: 'section',
|
|
253
|
+
...placementOf(item),
|
|
254
|
+
title: item.title,
|
|
255
|
+
level: item.level,
|
|
256
|
+
children: [],
|
|
257
|
+
}
|
|
258
|
+
stack[stack.length - 1].node.children.push(node)
|
|
259
|
+
stack.push({ level: item.level, node })
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
stack[stack.length - 1].node.children.push(nestChildren(item))
|
|
263
|
+
}
|
|
264
|
+
return root.children
|
|
265
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Board lane filter (SPEC section 3.4, wayfinder issue 11 P5 "board filter").
|
|
3
|
+
*
|
|
4
|
+
* A block-keyed client asset: the render layer embeds it only into artifacts
|
|
5
|
+
* that actually contain a `board` block, which is what keeps every artifact
|
|
6
|
+
* drawn before this existed byte for byte unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Every character here is ASCII, and that is a size decision rather than a
|
|
9
|
+
* stylistic one: client assets are fed to `collectCodepoints`, so one section
|
|
10
|
+
* sign or one em dash in a comment pulls a whole font subset (~8.5KB) into
|
|
11
|
+
* every artifact that carries this file. The same scan also bans the four
|
|
12
|
+
* sequences that would put the HTML tokenizer into its double-escaped state,
|
|
13
|
+
* where the closing tag of the script element stops closing it and the rest of
|
|
14
|
+
* the artifact -- the embedded IR included -- is swallowed as script data
|
|
15
|
+
* (CONTRACT F2g H2). Both rules are pinned by
|
|
16
|
+
* `test_f2g_skeleton_assets_cannot_derail_the_parser`.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately ES5-shaped and dependency-free: it is embedded verbatim into one
|
|
19
|
+
* offline file, so it must parse in any evergreen browser without a build step.
|
|
20
|
+
*
|
|
21
|
+
* The DOM contract is stated by `src/blocks/board.js` and read here: `data-board`
|
|
22
|
+
* scopes one board, and `data-board-lane` is carried by the button, by the lane
|
|
23
|
+
* and by every card, so this file never needs to know how a lane relates to its
|
|
24
|
+
* cards. Without JS every lane simply stays visible and the board reads as the
|
|
25
|
+
* full, unfiltered kanban -- the filter is an addition, never a prerequisite.
|
|
26
|
+
*
|
|
27
|
+
* The leading semicolon is the same guard `scale-to-fit.client.js` carries and
|
|
28
|
+
* for the same reason: assets are concatenated with a newline between them, and
|
|
29
|
+
* `})()` followed by a line starting with `(` is one expression to a JS parser --
|
|
30
|
+
* the previous asset's return value would be called as a function and this whole
|
|
31
|
+
* file would never run. Pinned by `test_f6a_assets_survive_concatenation`.
|
|
32
|
+
*/
|
|
33
|
+
;(function () {
|
|
34
|
+
var HIDE_RULE = '.board-lane[hidden]{display:none !important}'
|
|
35
|
+
var boards = Array.prototype.slice.call(document.querySelectorAll('.board[data-board]'))
|
|
36
|
+
if (boards.length === 0) return
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* The hiding rule ships with the behaviour, not with the skin.
|
|
40
|
+
*
|
|
41
|
+
* `hidden` is display:none in every UA stylesheet, and a template that gives
|
|
42
|
+
* `.board-lane` any display of its own silently wins over it -- the button
|
|
43
|
+
* would then toggle a class that changes nothing, which is worse than having
|
|
44
|
+
* no filter at all because it looks like one.
|
|
45
|
+
*/
|
|
46
|
+
var rules = document.createElement('style')
|
|
47
|
+
rules.textContent = HIDE_RULE
|
|
48
|
+
document.head.appendChild(rules)
|
|
49
|
+
|
|
50
|
+
function apply(board, lane) {
|
|
51
|
+
var lanes = Array.prototype.slice.call(board.querySelectorAll('.board-lane'))
|
|
52
|
+
for (var i = 0; i < lanes.length; i += 1) {
|
|
53
|
+
var name = lanes[i].getAttribute('data-board-lane')
|
|
54
|
+
lanes[i].hidden = lane !== null && name !== lane
|
|
55
|
+
}
|
|
56
|
+
var buttons = Array.prototype.slice.call(board.querySelectorAll('.board-filter'))
|
|
57
|
+
for (var j = 0; j < buttons.length; j += 1) {
|
|
58
|
+
var pressed = lane !== null && buttons[j].getAttribute('data-board-lane') === lane
|
|
59
|
+
buttons[j].setAttribute('aria-pressed', pressed ? 'true' : 'false')
|
|
60
|
+
}
|
|
61
|
+
board.setAttribute('data-board-filter', lane === null ? '' : lane)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (var b = 0; b < boards.length; b += 1) {
|
|
65
|
+
(function (board) {
|
|
66
|
+
var selected = null
|
|
67
|
+
var buttons = Array.prototype.slice.call(board.querySelectorAll('.board-filter'))
|
|
68
|
+
for (var k = 0; k < buttons.length; k += 1) {
|
|
69
|
+
(function (button) {
|
|
70
|
+
button.addEventListener('click', function () {
|
|
71
|
+
var lane = button.getAttribute('data-board-lane')
|
|
72
|
+
selected = selected === lane ? null : lane
|
|
73
|
+
apply(board, selected)
|
|
74
|
+
})
|
|
75
|
+
})(buttons[k])
|
|
76
|
+
}
|
|
77
|
+
apply(board, null)
|
|
78
|
+
})(boards[b])
|
|
79
|
+
}
|
|
80
|
+
})()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseBlockTree, parseMarkdown } from '../parser/index.js'
|
|
2
|
+
import { renderArtifact } from './index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Source text → finished artifact, with no filesystem in sight.
|
|
6
|
+
*
|
|
7
|
+
* `render` and `serve` must agree byte for byte on what a source file becomes —
|
|
8
|
+
* a preview that draws something the exported artifact does not is worse than
|
|
9
|
+
* no preview. Sharing one function is what makes that agreement structural
|
|
10
|
+
* rather than a matter of keeping two call sites in step.
|
|
11
|
+
*
|
|
12
|
+
* The template override reaches the *parser*, not just the renderer: whether
|
|
13
|
+
* `---` is a page break is a template decision, so resolving it later would
|
|
14
|
+
* compile a deck source as a document and then draw the empty result with the
|
|
15
|
+
* deck template.
|
|
16
|
+
*
|
|
17
|
+
* @param {{source: string, format: 'markdown'|'json', template?: string,
|
|
18
|
+
* generator?: string, createdAt: string}} input
|
|
19
|
+
* @returns {Promise<{html: string, ir: object, slides: number|undefined}>}
|
|
20
|
+
*/
|
|
21
|
+
export async function compileDocument({ source, format, template, generator, createdAt }) {
|
|
22
|
+
const parsed = format === 'json' ? parseBlockTree(source) : parseMarkdown(source, { template })
|
|
23
|
+
return await renderArtifact({
|
|
24
|
+
doc: parsed.doc,
|
|
25
|
+
templateKey: template ?? parsed.templateKey,
|
|
26
|
+
createdAt,
|
|
27
|
+
generator,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { blockModule } from '../blocks/index.js'
|
|
2
|
+
|
|
3
|
+
const EMPTY_CONFIG = Object.freeze({})
|
|
4
|
+
const ROOT_POSITION = Object.freeze({ index: 0, total: 1 })
|
|
5
|
+
|
|
6
|
+
const asNodes = (value) => (Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The render context a block module is handed.
|
|
10
|
+
*
|
|
11
|
+
* A block module knows what its own type *is*; it deliberately knows nothing
|
|
12
|
+
* about the page it lands on. Everything situational reaches it through here,
|
|
13
|
+
* which is what keeps a module portable enough to be published by a third party:
|
|
14
|
+
*
|
|
15
|
+
* - `templateKey` / `meta` — who is drawing, and the document's own identity
|
|
16
|
+
* - `config(type)` — the template's settings for a block type
|
|
17
|
+
* (`轉子/<型別>/config` in wayfinder issue 13)
|
|
18
|
+
* - `chrome(name, payload)`— a slot the template may fill; empty when it does not
|
|
19
|
+
* - `layoutOf(block)` — geometry measured by the layer above (A5)
|
|
20
|
+
* - `canvas` — the resolved canvas geometry of this artifact (C3)
|
|
21
|
+
* - `position` — where this block sits among its siblings
|
|
22
|
+
* - `renderBlock` / `renderChildren` / `drawableChildren` — recursion, so
|
|
23
|
+
* nesting stays uniform
|
|
24
|
+
*
|
|
25
|
+
* @param {{template: object, doc: object, layouts: Map<string, object>,
|
|
26
|
+
* canvas?: object, overrides?: Record<string, Function>}} input
|
|
27
|
+
*/
|
|
28
|
+
export function createRenderContext({
|
|
29
|
+
template,
|
|
30
|
+
doc,
|
|
31
|
+
layouts,
|
|
32
|
+
canvas = EMPTY_CONFIG,
|
|
33
|
+
overrides = EMPTY_CONFIG,
|
|
34
|
+
}) {
|
|
35
|
+
const { namespace, name } = template.manifest
|
|
36
|
+
const blockConfig = template.manifest.blockConfig ?? EMPTY_CONFIG
|
|
37
|
+
const chromeTable = template.chrome ?? EMPTY_CONFIG
|
|
38
|
+
|
|
39
|
+
const ctx = {
|
|
40
|
+
templateKey: `${namespace}/${name}`,
|
|
41
|
+
meta: doc.meta ?? EMPTY_CONFIG,
|
|
42
|
+
position: ROOT_POSITION,
|
|
43
|
+
config: (type) => blockConfig[type] ?? EMPTY_CONFIG,
|
|
44
|
+
chrome: (slot, payload) => {
|
|
45
|
+
const fill = chromeTable[slot]
|
|
46
|
+
return typeof fill === 'function' ? asNodes(fill(payload ?? EMPTY_CONFIG, ctx)) : []
|
|
47
|
+
},
|
|
48
|
+
layoutOf: (block) => layouts.get(block?.id),
|
|
49
|
+
canvas,
|
|
50
|
+
renderBlock,
|
|
51
|
+
renderChildren,
|
|
52
|
+
drawableChildren,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render one block, or `null` when no module claims its type.
|
|
57
|
+
*
|
|
58
|
+
* Skipping an unknown type is what a renderer does; it is not the *only*
|
|
59
|
+
* answer here, because `assertTemplateVocabulary` has already refused any
|
|
60
|
+
* document the template cannot express (CONTRACT C4). By this point a miss
|
|
61
|
+
* means a block nobody registered at all.
|
|
62
|
+
*
|
|
63
|
+
* A template package's 轉子 override wins over the registered module, and
|
|
64
|
+
* *only* for the artifact this context is drawing (CONTRACT D3). The
|
|
65
|
+
* admission check still runs against `manifest.blocks` first: an override is
|
|
66
|
+
* a different conversion for a type the template already declared, never a
|
|
67
|
+
* back door into a vocabulary it did not.
|
|
68
|
+
*/
|
|
69
|
+
function renderBlock(block, position = ROOT_POSITION) {
|
|
70
|
+
const type = block?.type
|
|
71
|
+
const mod = blockModule(type)
|
|
72
|
+
if (mod === undefined) return null
|
|
73
|
+
const render = overrides[type] ?? mod.render
|
|
74
|
+
return render(block, { ...ctx, position })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The children that will actually be drawn, in order.
|
|
79
|
+
*
|
|
80
|
+
* A container that has to pair each child with something of its own — a grid
|
|
81
|
+
* cell carrying that child's placement — cannot use `renderChildren` alone,
|
|
82
|
+
* because the returned array is already filtered and would silently pair the
|
|
83
|
+
* wrong child with the wrong coordinates.
|
|
84
|
+
*/
|
|
85
|
+
function drawableChildren(blocks) {
|
|
86
|
+
if (!Array.isArray(blocks)) return []
|
|
87
|
+
return blocks.filter((block) => blockModule(block?.type) !== undefined)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderChildren(blocks) {
|
|
91
|
+
const drawable = drawableChildren(blocks)
|
|
92
|
+
return drawable.map((block, index) =>
|
|
93
|
+
renderBlock(block, { index, total: drawable.length }),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return ctx
|
|
98
|
+
}
|