@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.
Files changed (136) hide show
  1. package/README.md +192 -0
  2. package/package.json +54 -0
  3. package/src/blocks/board.js +210 -0
  4. package/src/blocks/callout.js +63 -0
  5. package/src/blocks/code.js +28 -0
  6. package/src/blocks/deck.js +76 -0
  7. package/src/blocks/diagram.js +265 -0
  8. package/src/blocks/element.js +51 -0
  9. package/src/blocks/graph.js +264 -0
  10. package/src/blocks/grid.js +156 -0
  11. package/src/blocks/index.js +106 -0
  12. package/src/blocks/list.js +50 -0
  13. package/src/blocks/placement.js +119 -0
  14. package/src/blocks/prose.js +28 -0
  15. package/src/blocks/quote.js +25 -0
  16. package/src/blocks/raw.js +47 -0
  17. package/src/blocks/registry.js +158 -0
  18. package/src/blocks/schema-parts.js +19 -0
  19. package/src/blocks/section.js +53 -0
  20. package/src/blocks/slide.js +104 -0
  21. package/src/blocks/stat.js +83 -0
  22. package/src/blocks/table.js +50 -0
  23. package/src/blocks/timeline.js +80 -0
  24. package/src/cli/commands/close.js +51 -0
  25. package/src/cli/commands/comments.js +73 -0
  26. package/src/cli/commands/debug.js +34 -0
  27. package/src/cli/commands/example.js +22 -0
  28. package/src/cli/commands/export.js +10 -0
  29. package/src/cli/commands/init.js +53 -0
  30. package/src/cli/commands/lint.js +93 -0
  31. package/src/cli/commands/list.js +54 -0
  32. package/src/cli/commands/open.js +31 -0
  33. package/src/cli/commands/promote.js +38 -0
  34. package/src/cli/commands/render.js +31 -0
  35. package/src/cli/commands/replay.js +49 -0
  36. package/src/cli/commands/schema.js +10 -0
  37. package/src/cli/commands/serve.js +178 -0
  38. package/src/cli/commands/setup.js +64 -0
  39. package/src/cli/commands/snapshot.js +29 -0
  40. package/src/cli/commands/templates.js +75 -0
  41. package/src/cli/deliver.js +54 -0
  42. package/src/cli/emit.js +29 -0
  43. package/src/cli/format.js +153 -0
  44. package/src/cli/index.js +365 -0
  45. package/src/cli/registry.js +18 -0
  46. package/src/core/blocks.js +147 -0
  47. package/src/core/diagram.js +282 -0
  48. package/src/core/errors.js +156 -0
  49. package/src/core/example.js +109 -0
  50. package/src/core/ir.js +62 -0
  51. package/src/core/lint-gates.js +427 -0
  52. package/src/core/lint.js +281 -0
  53. package/src/core/scan.js +84 -0
  54. package/src/core/schema.js +88 -0
  55. package/src/core/spec-check.js +33 -0
  56. package/src/core/validate.js +44 -0
  57. package/src/core/version.js +18 -0
  58. package/src/core/vocabulary.js +140 -0
  59. package/src/delivery/atomic.js +71 -0
  60. package/src/delivery/comments.js +180 -0
  61. package/src/delivery/home.js +70 -0
  62. package/src/delivery/open.js +31 -0
  63. package/src/delivery/project.js +141 -0
  64. package/src/delivery/read.js +109 -0
  65. package/src/delivery/run.js +95 -0
  66. package/src/delivery/scaffold-blueprints.js +728 -0
  67. package/src/delivery/store.js +219 -0
  68. package/src/delivery/template-extensions.js +183 -0
  69. package/src/delivery/template-format.js +112 -0
  70. package/src/delivery/template-package.js +376 -0
  71. package/src/delivery/template-promote.js +240 -0
  72. package/src/delivery/template-scaffold.js +181 -0
  73. package/src/delivery/templates.js +192 -0
  74. package/src/delivery/toml.js +195 -0
  75. package/src/delivery/write.js +35 -0
  76. package/src/export/browser.js +130 -0
  77. package/src/export/index.js +96 -0
  78. package/src/export/pdf.js +25 -0
  79. package/src/export/png.js +40 -0
  80. package/src/export/pptx.js +48 -0
  81. package/src/export/slides.js +33 -0
  82. package/src/export/snapshot.js +33 -0
  83. package/src/layouts/article.js +103 -0
  84. package/src/layouts/canvas.js +144 -0
  85. package/src/layouts/card.js +128 -0
  86. package/src/layouts/deck.js +88 -0
  87. package/src/layouts/index.js +90 -0
  88. package/src/layouts/one-page.js +161 -0
  89. package/src/layouts/registry.js +251 -0
  90. package/src/layouts/resume.js +172 -0
  91. package/src/layouts/template-index.js +78 -0
  92. package/src/parser/artifact.js +38 -0
  93. package/src/parser/container.js +103 -0
  94. package/src/parser/index.js +223 -0
  95. package/src/parser/tokens.js +265 -0
  96. package/src/render/board-filter.client.js +80 -0
  97. package/src/render/compile.js +29 -0
  98. package/src/render/context.js +98 -0
  99. package/src/render/element.js +32 -0
  100. package/src/render/fonts.js +129 -0
  101. package/src/render/graph-hover.client.js +148 -0
  102. package/src/render/html.js +52 -0
  103. package/src/render/index.js +241 -0
  104. package/src/render/measure.js +60 -0
  105. package/src/render/placement.js +136 -0
  106. package/src/render/playback.client.js +74 -0
  107. package/src/render/scale-to-fit.client.js +136 -0
  108. package/src/render/scale.js +41 -0
  109. package/src/render/skeleton.js +131 -0
  110. package/src/render/ssr.js +24 -0
  111. package/src/render/styles.js +56 -0
  112. package/src/render/templates.js +191 -0
  113. package/src/serve/daemon.js +117 -0
  114. package/src/serve/overlay.js +213 -0
  115. package/src/serve/protocol.js +36 -0
  116. package/src/serve/server.js +264 -0
  117. package/templates/kami/cards/components.js +40 -0
  118. package/templates/kami/cards/index.js +25 -0
  119. package/templates/kami/cards/manifest.js +67 -0
  120. package/templates/kami/cards/styles.css +389 -0
  121. package/templates/kami/long-form/components.js +102 -0
  122. package/templates/kami/long-form/index.js +25 -0
  123. package/templates/kami/long-form/manifest.js +87 -0
  124. package/templates/kami/long-form/styles.css +481 -0
  125. package/templates/kami/one-page/components.js +48 -0
  126. package/templates/kami/one-page/index.js +27 -0
  127. package/templates/kami/one-page/manifest.js +65 -0
  128. package/templates/kami/one-page/styles.css +375 -0
  129. package/templates/kami/resume/components.js +51 -0
  130. package/templates/kami/resume/index.js +27 -0
  131. package/templates/kami/resume/manifest.js +65 -0
  132. package/templates/kami/resume/styles.css +424 -0
  133. package/templates/kami/slides/components.js +41 -0
  134. package/templates/kami/slides/index.js +26 -0
  135. package/templates/kami/slides/manifest.js +64 -0
  136. package/templates/kami/slides/styles.css +406 -0
@@ -0,0 +1,156 @@
1
+ import { el } from './element.js'
2
+ import { children } from './schema-parts.js'
3
+ import {
4
+ GRID_COLUMNS,
5
+ PLACEMENT_KEYS,
6
+ hasPlacement,
7
+ placementProblems,
8
+ placementStyle,
9
+ } from './placement.js'
10
+
11
+ /**
12
+ * `grid` — 「像在外面包一層 `display:grid` 的 div」(wayfinder issue 14 裁決 2).
13
+ *
14
+ * The twelfth core type, and the first one added through the F1 registry rather
15
+ * than by editing the SDK: everything a grid *is* — its schema branch, its
16
+ * example, its Markdown-superset syntax, its refusal wording, its markup — is
17
+ * in this file, and `src/blocks/index.js` gains one line.
18
+ *
19
+ * It is a container and nothing more. It has no columns option (24 is
20
+ * vocabulary, not configuration), no gap, no alignment: appearance never enters
21
+ * the IR. Content that is *not* wrapped in one stays flow content exactly as
22
+ * before, which is what makes the whole type opt-in — 「不包=預設流式」.
23
+ */
24
+
25
+ /** Verbatim — the `:::` fence names this module claims in the Markdown superset. */
26
+ const GRID_FENCE = 'grid'
27
+ const PLACE_FENCE = 'place'
28
+
29
+ const INTEGER_LITERAL = /^-?\d+$/
30
+
31
+ /**
32
+ * `col=1 colSpan=12` → `{col: 1, colSpan: 12}`.
33
+ *
34
+ * A key nobody recognises is kept rather than dropped, and an unparseable value
35
+ * is kept as the string it was. Both survive into the block, where
36
+ * `unevaluatedProperties: false` and the placement bounds refuse them with a
37
+ * block path — because the failure mode that matters here is the *typo*
38
+ * (`colspan=8`), and a parser that quietly discards a typo produces a document
39
+ * whose layout is silently not the one the author wrote (seal F7 的同一條).
40
+ */
41
+ export function parsePlacementParams(params) {
42
+ const out = {}
43
+ for (const token of String(params ?? '').trim().split(/\s+/)) {
44
+ if (token.length === 0) continue
45
+ const at = token.indexOf('=')
46
+ if (at <= 0) continue
47
+ const key = token.slice(0, at)
48
+ const raw = token.slice(at + 1)
49
+ out[key] = INTEGER_LITERAL.test(raw) ? Number(raw) : raw
50
+ }
51
+ return out
52
+ }
53
+
54
+ /** Attach one placement to every block a `:::place` fence contained. */
55
+ const place = (params, kids) => {
56
+ const placement = parsePlacementParams(params)
57
+ return kids.map((kid) => ({ ...kid, ...placement }))
58
+ }
59
+
60
+ /**
61
+ * Every relational placement problem among a grid's direct children.
62
+ *
63
+ * Bounds are the schema's job; what no schema keyword can say is that a span
64
+ * has to *end* inside the 24 columns. Reporting it here — from the module, as
65
+ * plain data — is what keeps a third-party container able to refuse its own
66
+ * broken geometry instead of drawing something the author did not write.
67
+ */
68
+ export function gridProblems(block) {
69
+ const kids = Array.isArray(block?.children) ? block.children : []
70
+ return kids.flatMap((kid, index) => placementProblems(`children[${index}]`, kid))
71
+ }
72
+
73
+ export default Object.freeze({
74
+ type: 'grid',
75
+
76
+ schema: Object.freeze({ required: ['children'], properties: { children } }),
77
+
78
+ example: Object.freeze({
79
+ block: {
80
+ id: 'b19',
81
+ type: 'grid',
82
+ children: [
83
+ { id: 'b20', type: 'prose', html: '主欄:占 24 格中的 16 格。', col: 1, colSpan: 16 },
84
+ { id: 'b21', type: 'prose', html: '側欄:占 24 格中的 8 格。', col: 17, colSpan: 8 },
85
+ ],
86
+ },
87
+ }),
88
+
89
+ nesting: Object.freeze({ arrays: Object.freeze(['children']) }),
90
+
91
+ /**
92
+ * The Markdown superset, declared rather than wired into the parser.
93
+ *
94
+ * ````
95
+ * ::::grid
96
+ * :::place col=1 colSpan=16
97
+ * 主欄。
98
+ * :::
99
+ * :::place col=17 colSpan=8
100
+ * 側欄。
101
+ * :::
102
+ * ::::
103
+ * ````
104
+ *
105
+ * The outer fence wears one more colon than the inner ones, the same rule
106
+ * markdown-it-container uses for nesting. `place` builds no block of its own:
107
+ * placement belongs to the *child*, exactly as `grid-column` does in CSS, so
108
+ * the fence hands its coordinates to whatever blocks it contained and leaves.
109
+ */
110
+ syntax: Object.freeze({
111
+ containers: Object.freeze([GRID_FENCE, PLACE_FENCE]),
112
+ build: ({ name, params, children: kids }) =>
113
+ name === GRID_FENCE ? { type: GRID_FENCE, children: kids } : place(params, kids),
114
+ }),
115
+
116
+ styleHooks: Object.freeze(['grid', 'grid-cell']),
117
+
118
+ create: ({ children: kids }) => Object.freeze({ type: GRID_FENCE, children: kids }),
119
+
120
+ validate: gridProblems,
121
+
122
+ invalid: Object.freeze({
123
+ code: 'KSB_PLACEMENT_INVALID',
124
+ message: (problems) =>
125
+ `grid 子項擺位非法:${problems.join(';')}。` +
126
+ `${GRID_COLUMNS} 格座標是整數詞彙,越界即報錯——` +
127
+ '靜默夾持會讓版面悄悄不是作者寫的那一個。',
128
+ }),
129
+
130
+ /**
131
+ * The 24 columns are declared inline, on the container itself.
132
+ *
133
+ * A stylesheet could carry them, but then a grid would only work in templates
134
+ * that had heard of grids — and every existing template would have to ship a
135
+ * new rule, which is a byte change to every artifact those templates ever
136
+ * drew. Geometry is what a grid *is*; the paint (gaps, rules, colour) stays
137
+ * the template's, through the two hooks above.
138
+ */
139
+ render: (block, ctx) => {
140
+ const kids = ctx.drawableChildren(block.children)
141
+ return el(
142
+ 'div',
143
+ {
144
+ class: 'grid',
145
+ style: `display: grid; grid-template-columns: repeat(${GRID_COLUMNS}, minmax(0, 1fr))`,
146
+ },
147
+ kids.map((kid, index) =>
148
+ el('div', { class: 'grid-cell', style: placementStyle(kid) }, [
149
+ ctx.renderBlock(kid, { index, total: kids.length }),
150
+ ]),
151
+ ),
152
+ )
153
+ },
154
+ })
155
+
156
+ export { GRID_COLUMNS, PLACEMENT_KEYS, hasPlacement }
@@ -0,0 +1,106 @@
1
+ import { blockModules, registerBlockModule, sealCoreRegistry } from './registry.js'
2
+
3
+ import section from './section.js'
4
+ import prose from './prose.js'
5
+ import quote from './quote.js'
6
+ import callout from './callout.js'
7
+ import code from './code.js'
8
+ import table from './table.js'
9
+ import raw from './raw.js'
10
+ import list from './list.js'
11
+ import deck from './deck.js'
12
+ import slide from './slide.js'
13
+ import diagram from './diagram.js'
14
+ import grid from './grid.js'
15
+ import stat from './stat.js'
16
+ import timeline from './timeline.js'
17
+ import board from './board.js'
18
+ import graph from './graph.js'
19
+
20
+ /**
21
+ * The core block vocabulary (SPEC §3), in canonical order.
22
+ *
23
+ * Order is load-bearing: it is the order of the `type` enum and of the branch
24
+ * list in the generated JSON Schema, and therefore of the bytes `kamishibai
25
+ * schema` prints. It is also the only place the core types are listed —
26
+ * every other list in the SDK (schema, examples, the renderer table, the
27
+ * template admission rule) is derived from the registry rather than repeated.
28
+ *
29
+ * A *seventeenth* type does not belong here: a plugin calls `registerBlockModule`
30
+ * from outside and needs no edit to any file inside `src/` (CONTRACT A4).
31
+ *
32
+ * `grid` is appended rather than slotted in beside the other containers, and
33
+ * that is a byte decision, not a taste one: this order is the order of the
34
+ * `type` enum and of the schema's branch list, so inserting anywhere but the
35
+ * end would move bytes the F2a slice is not allowed to move (CONTRACT C5).
36
+ *
37
+ * The four data types F6a landed (`stat`, `timeline`, `board`, `graph` —
38
+ * wayfinder issue 11 P1「資料」) are appended after `grid` for the same reason,
39
+ * and among themselves they sit in the order that ticket lists them. Anywhere
40
+ * else in this array is a moved byte in `kamishibai schema --json` for a type
41
+ * that has nothing to do with this slice.
42
+ */
43
+ const CORE_MODULES = Object.freeze([
44
+ section,
45
+ prose,
46
+ quote,
47
+ callout,
48
+ code,
49
+ table,
50
+ raw,
51
+ list,
52
+ deck,
53
+ slide,
54
+ diagram,
55
+ grid,
56
+ stat,
57
+ timeline,
58
+ board,
59
+ graph,
60
+ ])
61
+
62
+ for (const mod of CORE_MODULES) registerBlockModule(mod)
63
+ sealCoreRegistry()
64
+
65
+ export {
66
+ BLOCK_MODULE_FIELDS,
67
+ PLUGIN_TYPE_PREFIX,
68
+ PLUGIN_TYPE_WILDCARD,
69
+ blockModule,
70
+ blockModules,
71
+ blockTypes,
72
+ pluginBlockTypes,
73
+ registerBlockModule,
74
+ registryVersion,
75
+ resetBlockRegistry,
76
+ } from './registry.js'
77
+
78
+ export { el, rawEl, isElement } from './element.js'
79
+
80
+ export {
81
+ GRID_COLUMNS,
82
+ PLACEMENT_KEYS,
83
+ PLACEMENT_PROPERTIES,
84
+ ROW_KEYS,
85
+ hasPlacement,
86
+ hasRowVocabulary,
87
+ placedWidth,
88
+ placementProblems,
89
+ placementStyle,
90
+ } from './placement.js'
91
+
92
+ /**
93
+ * The `:::` fence names the registered vocabulary claims, name → module.
94
+ *
95
+ * The parser used to hold this list itself (`CALLOUT_VARIANTS`, hard-coded in
96
+ * `parser/container.js`), which meant a new container type was a parser edit —
97
+ * the eleven-file problem in its last hiding place. Now a module declares
98
+ * `syntax.containers` and the parser asks.
99
+ */
100
+ export function containerSyntax() {
101
+ const table = new Map()
102
+ for (const mod of blockModules()) {
103
+ for (const name of mod.syntax?.containers ?? []) table.set(name, mod)
104
+ }
105
+ return table
106
+ }
@@ -0,0 +1,50 @@
1
+ import { el } from './element.js'
2
+ import { itemGroups } from './schema-parts.js'
3
+
4
+ /**
5
+ * `list` — keeps its own frame and hands each item's blocks straight back to
6
+ * the renderer, so a callout inside a list item is the very same callout module
7
+ * as one at document level (CONTRACT C1).
8
+ */
9
+ export default Object.freeze({
10
+ type: 'list',
11
+
12
+ schema: Object.freeze({
13
+ required: ['ordered', 'items'],
14
+ properties: { ordered: { type: 'boolean' }, items: itemGroups },
15
+ }),
16
+
17
+ example: Object.freeze({
18
+ block: {
19
+ id: 'b11',
20
+ type: 'list',
21
+ ordered: false,
22
+ items: [
23
+ [{ id: 'b12', type: 'prose', html: '項目一:每個項目是一組 block。' }],
24
+ [
25
+ { id: 'b13', type: 'prose', html: '項目二:所以項目內可以放別的 block。' },
26
+ {
27
+ id: 'b14',
28
+ type: 'callout',
29
+ variant: 'note',
30
+ children: [{ id: 'b15', type: 'prose', html: '像這個 callout。' }],
31
+ },
32
+ ],
33
+ ],
34
+ },
35
+ }),
36
+
37
+ /** `items` is an array *of arrays* — the only matrix shape in the vocabulary. */
38
+ nesting: Object.freeze({ matrices: Object.freeze(['items']) }),
39
+
40
+ styleHooks: Object.freeze(['list']),
41
+
42
+ create: ({ ordered, items }) => Object.freeze({ type: 'list', ordered, items }),
43
+
44
+ render: (block, ctx) =>
45
+ el(
46
+ block.ordered ? 'ol' : 'ul',
47
+ { class: 'list' },
48
+ (block.items ?? []).map((item) => el('li', null, ctx.renderChildren(item))),
49
+ ),
50
+ })
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The 24-column placement vocabulary — the *only* coordinates the IR carries.
3
+ *
4
+ * It lives beside `element.js` rather than inside `grid.js` or inside a layout
5
+ * because three different layers have to agree on it and none of them may own
6
+ * it (wayfinder issue 14 裁決 2):
7
+ *
8
+ * - the `grid` block module validates its children against it,
9
+ * - `core/schema.js` widens `$defs.block` with it, so *any* block may carry a
10
+ * placement without every block module having to declare four fields,
11
+ * - the render layer divides the available width by it before a diagram is
12
+ * measured (issue 14 裁決 3「diagram 一律收實際可用寬」).
13
+ *
14
+ * Four integers and nothing else. No units, no gaps, no alignment: appearance
15
+ * never enters the IR (issue 13 裁決 5), so what an author writes is *which
16
+ * cells this block occupies*, and what that looks like stays a template's
17
+ * business. A block with no placement is flow content, exactly as before —
18
+ * 「不包=預設流式」.
19
+ */
20
+
21
+ /** Verbatim — the grid is 24 columns wide. Never configurable: it is vocabulary. */
22
+ export const GRID_COLUMNS = 24
23
+
24
+ /** Verbatim — the four placement field names, in declaration order. */
25
+ export const PLACEMENT_KEYS = Object.freeze(['col', 'row', 'colSpan', 'rowSpan'])
26
+
27
+ /** The two field names that only mean something where rows are fixed. */
28
+ export const ROW_KEYS = Object.freeze(['row', 'rowSpan'])
29
+
30
+ const gridInteger = Object.freeze({ type: 'integer', minimum: 1, maximum: GRID_COLUMNS })
31
+
32
+ /**
33
+ * The placement fields as JSON Schema, for merging into `$defs.block`.
34
+ *
35
+ * Bounds are expressible here, so they are checked here — that is what puts a
36
+ * bad `col` in front of `lint` with a block path attached, without the document
37
+ * ever reaching a renderer. What a schema keyword *cannot* say is that
38
+ * `col + colSpan - 1` must still land inside 24 columns; that relational rule
39
+ * belongs to the `grid` module's own `validate`.
40
+ */
41
+ export const PLACEMENT_PROPERTIES = Object.freeze(
42
+ Object.fromEntries(PLACEMENT_KEYS.map((key) => [key, gridInteger])),
43
+ )
44
+
45
+ /** Whether a block carries any placement at all (absence means flow content). */
46
+ export const hasPlacement = (block) =>
47
+ PLACEMENT_KEYS.some((key) => block?.[key] !== undefined && block[key] !== null)
48
+
49
+ /** Whether a block carries row vocabulary (legal only where rows are fixed). */
50
+ export const hasRowVocabulary = (block) =>
51
+ ROW_KEYS.some((key) => block?.[key] !== undefined && block[key] !== null)
52
+
53
+ const isGridInteger = (value) =>
54
+ Number.isInteger(value) && value >= 1 && value <= GRID_COLUMNS
55
+
56
+ /**
57
+ * The span rule for one axis, as human-readable problems.
58
+ *
59
+ * Out-of-range values are *not* re-reported here: the schema already refuses
60
+ * them, and saying it twice in two wordings would give an author two different
61
+ * sentences for one mistake. What is reported is only the relation the schema
62
+ * cannot express — a span whose far edge falls outside the 24 columns.
63
+ */
64
+ const axisProblems = (where, block, startKey, spanKey, edgeWord) => {
65
+ const start = block?.[startKey]
66
+ const span = block?.[spanKey]
67
+ if (!isGridInteger(start) || !isGridInteger(span)) return []
68
+ const edge = start + span - 1
69
+ if (edge <= GRID_COLUMNS) return []
70
+ return [
71
+ `${where} 的 ${startKey} ${start} + ${spanKey} ${span} 讓${edgeWord}落在第 ${edge} 格,` +
72
+ `超出 ${GRID_COLUMNS} 格`,
73
+ ]
74
+ }
75
+
76
+ /**
77
+ * Every relational placement problem of one block, as human-readable messages.
78
+ * Empty array means the block's placement is expressible on a 24-column grid.
79
+ *
80
+ * @param {string} where how to name this block in a message (e.g. `children[2]`)
81
+ *
82
+ * The edge word is per axis. It used to be 「右緣」 for both, so a `row 20 +
83
+ * rowSpan 8` was reported as a *right* edge falling outside — a sentence that
84
+ * sends the reader to look at the wrong axis of their own layout. Nobody hit it
85
+ * before F2f because rows were only legal inside an explicit grid; the
86
+ * grid-addressed page made row coordinates ordinary, so the wording had to
87
+ * become true.
88
+ */
89
+ export const placementProblems = (where, block) => [
90
+ ...axisProblems(where, block, 'col', 'colSpan', '右緣'),
91
+ ...axisProblems(where, block, 'row', 'rowSpan', '下緣'),
92
+ ]
93
+
94
+ /**
95
+ * The CSS a placement means, as an inline `style` value — or `null` for none.
96
+ *
97
+ * Geometry is inline rather than in a stylesheet because it is a property of
98
+ * *what a grid is*, not of who painted it: a template that never heard of grids
99
+ * must still draw one correctly, and a template that has opinions still owns
100
+ * every colour, gap and border through the `grid`/`grid-cell` hooks. The same
101
+ * reasoning that makes deck playback a skeleton asset makes this one inline.
102
+ */
103
+ export function placementStyle(block) {
104
+ const span = isGridInteger(block?.colSpan) ? block.colSpan : GRID_COLUMNS
105
+ const col = isGridInteger(block?.col) ? block.col : 'auto'
106
+ const parts = [`grid-column: ${col} / span ${span}`]
107
+ if (hasRowVocabulary(block)) {
108
+ const rowSpan = isGridInteger(block?.rowSpan) ? block.rowSpan : 1
109
+ const row = isGridInteger(block?.row) ? block.row : 'auto'
110
+ parts.push(`grid-row: ${row} / span ${rowSpan}`)
111
+ }
112
+ return parts.join('; ')
113
+ }
114
+
115
+ /** How much of `available` px a block occupies, given its column span. */
116
+ export const placedWidth = (available, block) => {
117
+ const span = isGridInteger(block?.colSpan) ? block.colSpan : GRID_COLUMNS
118
+ return Math.floor((available * span) / GRID_COLUMNS)
119
+ }
@@ -0,0 +1,28 @@
1
+ import { rawEl } from './element.js'
2
+ import { str } from './schema-parts.js'
3
+
4
+ /**
5
+ * Block-level flow content that `<p>` may not contain. A `<p>` wrapping a
6
+ * `<ul>` is invalid HTML: the parser implicitly closes the paragraph, so the
7
+ * real DOM becomes an empty `<p>`, a list that has escaped the `.prose`
8
+ * styling scope, and another empty `<p>`. Pick the container to match.
9
+ */
10
+ const BLOCK_LEVEL_CONTENT = /<(?:ul|ol|div|pre|table|blockquote|figure|hr|h[1-6])\b/i
11
+
12
+ /** `prose` — one run of inline Markdown, already compiled to HTML by the parser. */
13
+ export default Object.freeze({
14
+ type: 'prose',
15
+
16
+ schema: Object.freeze({ required: ['html'], properties: { html: str } }),
17
+
18
+ example: Object.freeze({
19
+ block: { id: 'b3', type: 'prose', html: '散文段落,行內 Markdown 已編譯為 HTML。' },
20
+ }),
21
+
22
+ styleHooks: Object.freeze(['prose']),
23
+
24
+ create: (html) => Object.freeze({ type: 'prose', html }),
25
+
26
+ render: (block) =>
27
+ rawEl(BLOCK_LEVEL_CONTENT.test(block.html) ? 'div' : 'p', { class: 'prose' }, block.html),
28
+ })
@@ -0,0 +1,25 @@
1
+ import { el } from './element.js'
2
+ import { children } from './schema-parts.js'
3
+
4
+ /** `quote` — a block quotation whose contents are ordinary child blocks. */
5
+ export default Object.freeze({
6
+ type: 'quote',
7
+
8
+ schema: Object.freeze({ required: ['children'], properties: { children } }),
9
+
10
+ example: Object.freeze({
11
+ block: {
12
+ id: 'b4',
13
+ type: 'quote',
14
+ children: [{ id: 'b5', type: 'prose', html: '被引用的一段話。' }],
15
+ },
16
+ }),
17
+
18
+ nesting: Object.freeze({ arrays: Object.freeze(['children']) }),
19
+
20
+ styleHooks: Object.freeze(['quote']),
21
+
22
+ create: (kids) => Object.freeze({ type: 'quote', children: kids }),
23
+
24
+ render: (block, ctx) => el('blockquote', { class: 'quote' }, ctx.renderChildren(block.children)),
25
+ })
@@ -0,0 +1,47 @@
1
+ import { rawEl } from './element.js'
2
+ import { str } from './schema-parts.js'
3
+
4
+ /** The two island flavours: arbitrary markup, or a drawing. */
5
+ export const RAW_SUBTYPES = Object.freeze(['html', 'svg'])
6
+
7
+ /**
8
+ * `raw` — the escape hatch (SPEC §11 島嶼).
9
+ *
10
+ * `intent` is required and non-empty on purpose: an island is a confession that
11
+ * the vocabulary could not say something, and the confession has to record
12
+ * *what*. That record is what later promotes a recurring island into a real
13
+ * plugin block instead of leaving hand-written HTML to accumulate forever.
14
+ */
15
+ export default Object.freeze({
16
+ type: 'raw',
17
+
18
+ schema: Object.freeze({
19
+ required: ['subtype', 'intent', 'html'],
20
+ properties: {
21
+ subtype: { enum: [...RAW_SUBTYPES] },
22
+ intent: { ...str, minLength: 1 },
23
+ html: str,
24
+ },
25
+ }),
26
+
27
+ example: Object.freeze({
28
+ block: {
29
+ id: 'b10',
30
+ type: 'raw',
31
+ subtype: 'html',
32
+ intent: '為何需要手寫這塊',
33
+ html: '<div class="island">手寫內容</div>',
34
+ },
35
+ }),
36
+
37
+ styleHooks: Object.freeze(['island']),
38
+
39
+ create: ({ subtype, intent, html }) => Object.freeze({ type: 'raw', subtype, intent, html }),
40
+
41
+ render: (block) =>
42
+ rawEl(
43
+ block.subtype === 'svg' ? 'figure' : 'div',
44
+ { class: 'island', 'data-intent': block.intent },
45
+ block.html,
46
+ ),
47
+ })