@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,136 @@
|
|
|
1
|
+
import { blockArrayKeys, blockMatrixKeys } from '../core/blocks.js'
|
|
2
|
+
import { CODES, validationError } from '../core/errors.js'
|
|
3
|
+
import { GRID_COLUMNS, PLACEMENT_KEYS, hasPlacement, placementProblems } from '../blocks/placement.js'
|
|
4
|
+
import { GRID_ADDRESSED_FIELD } from '../layouts/index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where a 24-grid coordinate is allowed to mean anything.
|
|
8
|
+
*
|
|
9
|
+
* There are exactly two such places: inside an explicit `grid` container, and
|
|
10
|
+
* inside a layout that has **declared** its whole page addressable by grid
|
|
11
|
+
* coordinates (`gridAddressed`). Everywhere else a coordinate names a cell that
|
|
12
|
+
* does not exist, and CSS auto-placement drops it without a word — so the
|
|
13
|
+
* artifact renders, `lint` returns 0, and the layout is quietly not the one the
|
|
14
|
+
* author wrote.
|
|
15
|
+
*
|
|
16
|
+
* `:::place col=1 colSpan=8` under `kami/long-form` was exactly that: it passed
|
|
17
|
+
* every gate and the coordinates evaporated. Columns are not gentler than rows
|
|
18
|
+
* here — a span nobody applies is as invisible as a row nobody has — so **any**
|
|
19
|
+
* placement outside those two places is refused, which is the failure mode
|
|
20
|
+
* `KSB_PLACEMENT_INVALID`'s own wording condemns.
|
|
21
|
+
*
|
|
22
|
+
* **`bounded` is not the licence.** An earlier version of this gate exempted
|
|
23
|
+
* every bounded canvas, on the reasoning that a fixed logical page "is" a grid.
|
|
24
|
+
* It was not one: `bounded` says only that the canvas has a fixed size and gets
|
|
25
|
+
* scaled to fit, and it says nothing about anyone laying down the 24 tracks a
|
|
26
|
+
* bare `col: 1` would have to resolve against. Exempting it would therefore
|
|
27
|
+
* have re-opened the hole in the one place hardest to notice — the artifact
|
|
28
|
+
* scales beautifully and the coordinate is gone. The licence stays a claim a
|
|
29
|
+
* layout makes on purpose. Since F2f exactly one factory layout makes it:
|
|
30
|
+
* `deck`, whose slides lay the tracks (`src/blocks/slide.js`) — the claim and
|
|
31
|
+
* the tracks landed in the same slice, which is the only way it may be made.
|
|
32
|
+
*
|
|
33
|
+
* The exemption is **whole-page**, as F2a defined it: a layout that says the
|
|
34
|
+
* page is addressable says it about the page, not about one container in it.
|
|
35
|
+
* What the exemption is not is a hole — the relational rule still runs on the
|
|
36
|
+
* exempt path (`assertRelations` below), which is the half F2a deferred here.
|
|
37
|
+
*
|
|
38
|
+
* No block can check any of this for itself: it depends on the enclosing
|
|
39
|
+
* container *and* on the artifact's layout, neither of which is visible from
|
|
40
|
+
* inside a block module. Hence a gate at the layer that knows both, run before
|
|
41
|
+
* a byte is drawn — the same shape as `assertBlockSpecs`, for the same reason.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const GRID_TYPE = 'grid'
|
|
45
|
+
|
|
46
|
+
const carried = (block) => PLACEMENT_KEYS.filter((key) => block?.[key] !== undefined && block[key] !== null)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walk every block in the tree, handing each one its own path.
|
|
50
|
+
*
|
|
51
|
+
* Shared by both gates below so they can never disagree about what a block's
|
|
52
|
+
* path is: a refusal that names `doc.children[0].slides[1].children[0]` is only
|
|
53
|
+
* actionable if it is the same string an author would get from the other gate.
|
|
54
|
+
*/
|
|
55
|
+
function visitBlocks(doc, arrays, matrices, visit) {
|
|
56
|
+
const walk = (node, path, inGrid) => {
|
|
57
|
+
if (node === null || typeof node !== 'object') return
|
|
58
|
+
visit(node, path, inGrid)
|
|
59
|
+
const childInGrid = node.type === GRID_TYPE
|
|
60
|
+
for (const key of matrices) {
|
|
61
|
+
if (!Array.isArray(node[key])) continue
|
|
62
|
+
for (const [group, blocks] of node[key].entries()) {
|
|
63
|
+
if (!Array.isArray(blocks)) continue
|
|
64
|
+
for (const [index, child] of blocks.entries()) {
|
|
65
|
+
walk(child, `${path}.${key}[${group}][${index}]`, childInGrid)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const key of arrays) {
|
|
70
|
+
if (!Array.isArray(node[key])) continue
|
|
71
|
+
for (const [index, child] of node[key].entries()) {
|
|
72
|
+
walk(child, `${path}.${key}[${index}]`, childInGrid)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
walk(doc, 'doc', false)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The **relational** rule on the exempt path — F2a's deferred half (終驗遞延).
|
|
81
|
+
*
|
|
82
|
+
* `col + colSpan - 1 ≤ 24` cannot be said in JSON Schema, so it is checked by
|
|
83
|
+
* the `grid` module's own `validate` — which only ever runs for `grid` blocks.
|
|
84
|
+
* The moment a layout declares `gridAddressed`, coordinates become legal
|
|
85
|
+
* *outside* any grid container, and that check stops running on them: F2a's
|
|
86
|
+
* gate returned at the first line and nothing downstream looked again. So on a
|
|
87
|
+
* grid-addressed page `col: 20 colSpan: 8` passed every gate and was then
|
|
88
|
+
* clamped by CSS to the 24th column — an artifact whose layout is silently not
|
|
89
|
+
* the one the author wrote, which is precisely what the refusal below condemns.
|
|
90
|
+
*
|
|
91
|
+
* Two defences, each pinned on its own: JSON Schema refuses a coordinate
|
|
92
|
+
* *outside* 1..24 with a block path (`KSB_IR_SCHEMA`), and this refuses a
|
|
93
|
+
* coordinate whose span *ends* outside them.
|
|
94
|
+
*/
|
|
95
|
+
function assertRelations(doc, canvas, arrays, matrices) {
|
|
96
|
+
visitBlocks(doc, arrays, matrices, (node, path, inGrid) => {
|
|
97
|
+
// Inside a grid container the grid module already said it, in its own
|
|
98
|
+
// wording. Saying it twice would give one mistake two sentences.
|
|
99
|
+
if (inGrid || !hasPlacement(node)) return
|
|
100
|
+
const problems = placementProblems(path, node)
|
|
101
|
+
if (problems.length === 0) return
|
|
102
|
+
throw validationError(
|
|
103
|
+
`擺位非法:${problems.join(';')}。` +
|
|
104
|
+
`layout \`${canvas?.layout}\` 宣告自己是格軌定址畫布(\`${GRID_ADDRESSED_FIELD}\`),` +
|
|
105
|
+
`整頁就是那 ${GRID_COLUMNS} 格——越界即報錯:` +
|
|
106
|
+
'靜默夾持會讓版面悄悄不是作者寫的那一個。',
|
|
107
|
+
CODES.PLACEMENT_INVALID,
|
|
108
|
+
path,
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @throws {import('../core/errors.js').KsbError} exit 1 / KSB_PLACEMENT_INVALID
|
|
115
|
+
*/
|
|
116
|
+
export function assertPlacement(doc, canvas) {
|
|
117
|
+
const arrays = blockArrayKeys()
|
|
118
|
+
const matrices = blockMatrixKeys()
|
|
119
|
+
if (canvas?.[GRID_ADDRESSED_FIELD] === true) {
|
|
120
|
+
assertRelations(doc, canvas, arrays, matrices)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
visitBlocks(doc, arrays, matrices, (node, path, inGrid) => {
|
|
125
|
+
if (inGrid || !hasPlacement(node)) return
|
|
126
|
+
throw validationError(
|
|
127
|
+
`${path} 帶擺位(${carried(node).join('/')}),但它既不在 grid 容器內,` +
|
|
128
|
+
`所屬 layout \`${canvas?.layout}\` 也沒有宣告自己是格軌定址畫布` +
|
|
129
|
+
`(\`${GRID_ADDRESSED_FIELD}\`)。` +
|
|
130
|
+
'24 格座標只有在版面真的鋪了那些格軌時才指得到東西——沒鋪的版面裡它會被 ' +
|
|
131
|
+
'CSS 自動排版靜默忽略,於是作者寫的擺位與看到的版面不是同一個。',
|
|
132
|
+
CODES.PLACEMENT_INVALID,
|
|
133
|
+
path,
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Kami slides playback (SPEC section 7.4, CONTRACT C5).
|
|
3
|
+
*
|
|
4
|
+
* A template-layer asset, not a raw island: it ships with the template and is
|
|
5
|
+
* inlined by the render layer, which is what lets SPEC section 11 ban script
|
|
6
|
+
* elements *inside islands* -- interaction is a component concern, never
|
|
7
|
+
* hand-written escape-hatch markup.
|
|
8
|
+
*
|
|
9
|
+
* Every character here is ASCII, and that is a size decision rather than a
|
|
10
|
+
* stylistic one: the skeleton assets are fed to `collectCodepoints`, so one
|
|
11
|
+
* section sign or one em dash in a comment pulls a whole font subset (~8.5KB)
|
|
12
|
+
* into every artifact that carries this file. Pinned across all six scanned
|
|
13
|
+
* surfaces by `test_f2g_skeleton_assets_cannot_derail_the_parser`.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately ES5-shaped and dependency-free: it is embedded verbatim into one
|
|
16
|
+
* offline file, so it must parse in any evergreen browser without a build step.
|
|
17
|
+
* Comparison operators mean a bare left angle bracket does appear below. What
|
|
18
|
+
* must never appear -- in the code or, as this very sentence once proved, in a
|
|
19
|
+
* comment warning against it -- is any of the four sequences banned by
|
|
20
|
+
* `test_f2g_skeleton_assets_cannot_derail_the_parser`: naming two of them here
|
|
21
|
+
* and in the sibling skeleton asset was enough to make the parser read every
|
|
22
|
+
* deck artifact as one script element running to the end of the file, so that
|
|
23
|
+
* neither asset ever ran (CONTRACT F2g H2). Without JS every slide simply stays
|
|
24
|
+
* visible and the deck reads as a scrolling document.
|
|
25
|
+
*/
|
|
26
|
+
(function () {
|
|
27
|
+
var deck = document.querySelector('.deck')
|
|
28
|
+
if (!deck) return
|
|
29
|
+
var slides = Array.prototype.slice.call(deck.querySelectorAll('.slide'))
|
|
30
|
+
if (slides.length === 0) return
|
|
31
|
+
|
|
32
|
+
var progress = deck.querySelector('.deck-progress')
|
|
33
|
+
var index = 0
|
|
34
|
+
|
|
35
|
+
function show(next) {
|
|
36
|
+
index = Math.max(0, Math.min(slides.length - 1, next))
|
|
37
|
+
for (var i = 0; i < slides.length; i += 1) {
|
|
38
|
+
var current = i === index
|
|
39
|
+
slides[i].classList.toggle('is-current', current)
|
|
40
|
+
slides[i].setAttribute('aria-hidden', current ? 'false' : 'true')
|
|
41
|
+
}
|
|
42
|
+
if (progress) progress.textContent = index + 1 + ' / ' + slides.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function enterFullscreen() {
|
|
46
|
+
var target = document.documentElement
|
|
47
|
+
if (document.fullscreenElement) return
|
|
48
|
+
if (target.requestFullscreen) target.requestFullscreen()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function exitFullscreen() {
|
|
52
|
+
if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener('keydown', function (event) {
|
|
56
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return
|
|
57
|
+
var key = event.key
|
|
58
|
+
if (key === 'ArrowRight' || key === ' ' || key === 'Spacebar' || key === 'PageDown') {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
show(index + 1)
|
|
61
|
+
} else if (key === 'ArrowLeft' || key === 'PageUp') {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
show(index - 1)
|
|
64
|
+
} else if (key === 'f' || key === 'F') {
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
enterFullscreen()
|
|
67
|
+
} else if (key === 'Escape' || key === 'Esc') {
|
|
68
|
+
exitFullscreen()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
deck.classList.add('is-live')
|
|
73
|
+
show(0)
|
|
74
|
+
})()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton asset: keep every bounded canvas exactly as large as it can be.
|
|
3
|
+
*
|
|
4
|
+
* Embedded verbatim into any artifact whose layout declares a bounded canvas,
|
|
5
|
+
* so a single edited character moves that artifact's bytes. It assumes nothing
|
|
6
|
+
* about the page beyond the canvas attribute -- a bounded layout this file has
|
|
7
|
+
* never heard of is scaled by it all the same, and gets the same flow geometry
|
|
8
|
+
* and the same print behaviour without having to ask for either.
|
|
9
|
+
*
|
|
10
|
+
* ## Nothing written here may perturb the HTML tokenizer (CONTRACT F2g H2)
|
|
11
|
+
*
|
|
12
|
+
* The four sequences banned by `test_f2g_skeleton_assets_cannot_derail_the_parser`
|
|
13
|
+
* are banned in comments too, and that is not pedantry. This file used to spell
|
|
14
|
+
* one of them out in this very paragraph, inside a sentence warning against it,
|
|
15
|
+
* while the sibling skeleton asset spelled out another in its own header. Apart
|
|
16
|
+
* they were harmless; together they walked the parser into its double-escaped
|
|
17
|
+
* state, where the end tag that should have closed the script is ordinary text.
|
|
18
|
+
* Every artifact carrying both assets therefore shipped a single script element
|
|
19
|
+
* that swallowed the rest of the document, embedded IR and all -- so neither
|
|
20
|
+
* asset ran at all, at any window size, and the artifact still looked plausible
|
|
21
|
+
* because a canvas that is never scaled is simply a canvas at scale 1.
|
|
22
|
+
*
|
|
23
|
+
* ## One scalar for both axes
|
|
24
|
+
*
|
|
25
|
+
* Fitting each axis separately would fill the window and stretch the type,
|
|
26
|
+
* which is the one outcome worse than a black border.
|
|
27
|
+
*
|
|
28
|
+
* ## A transform does not move the flow box (CONTRACT F2g H1)
|
|
29
|
+
*
|
|
30
|
+
* `transform` changes what is painted, never what the layout reserves, so a
|
|
31
|
+
* canvas scaled by k goes on occupying its unscaled height. Where one canvas
|
|
32
|
+
* fills the window that is invisible; where several are stacked in one host it
|
|
33
|
+
* is the whole geometry. Measured on a real browser before this was fixed: at
|
|
34
|
+
* k = 1.2 each canvas overflowed 180px into the next one and the content
|
|
35
|
+
* underneath became unclickable, and at k = 0.27 the same arithmetic opened
|
|
36
|
+
* 656px of dead space instead. So the advance owes the difference,
|
|
37
|
+
* `h * scale - h`, and margin-bottom is where that debt is paid: positive when
|
|
38
|
+
* the canvas grew, negative when it shrank, zero at k = 1. What is left is a
|
|
39
|
+
* gap of exactly nothing, which is the only gap that does not change meaning
|
|
40
|
+
* when everything beside it is scaled.
|
|
41
|
+
*
|
|
42
|
+
* ## Where a canvas centres itself
|
|
43
|
+
*
|
|
44
|
+
* In the space it is given -- and it is given the whole window only when it is
|
|
45
|
+
* the one canvas rendered in its host. Horizontally that always holds, so the
|
|
46
|
+
* horizontal offset is unconditional. Vertically it holds for a paged artifact
|
|
47
|
+
* (one page shown, the others not rendered) and fails for a stack, where
|
|
48
|
+
* centring each canvas in the window *is* the dead space above it. The test is
|
|
49
|
+
* structural on purpose: this file may not learn the name of a layout in order
|
|
50
|
+
* to lay it out.
|
|
51
|
+
*/
|
|
52
|
+
;(function () {
|
|
53
|
+
var ATTR = 'data-ksb-canvas'
|
|
54
|
+
var SCALE_ATTR = 'data-ksb-scale'
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* On paper the canvas is drawn at its natural size, so the transform and the
|
|
58
|
+
* flow correction that pays for it stand down together -- keeping one without
|
|
59
|
+
* the other would space the canvases by the very difference it corrects. The
|
|
60
|
+
* rule is written from here, not left to the stylesheet, because a skin that
|
|
61
|
+
* never wrote a print block is exactly the case this file exists to cover.
|
|
62
|
+
*/
|
|
63
|
+
var standDown = document.createElement('style')
|
|
64
|
+
standDown.textContent =
|
|
65
|
+
'@media print{[' + ATTR + ']{transform:none !important;margin-bottom:0 !important}}'
|
|
66
|
+
;(document.head || document.documentElement).appendChild(standDown)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What every canvas on the page should become -- read in full before any of
|
|
70
|
+
* them moves, because moving one can take a scrollbar with it and change what
|
|
71
|
+
* the next one would have measured.
|
|
72
|
+
*/
|
|
73
|
+
function plan() {
|
|
74
|
+
var nodes = document.querySelectorAll('[' + ATTR + ']')
|
|
75
|
+
var steps = []
|
|
76
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
77
|
+
var node = nodes[i]
|
|
78
|
+
var size = String(node.getAttribute(ATTR) || '').split('x')
|
|
79
|
+
var w = Number(size[0])
|
|
80
|
+
var h = Number(size[1])
|
|
81
|
+
if (!(w > 0) || !(h > 0)) continue
|
|
82
|
+
var host = node.parentNode
|
|
83
|
+
var vw = host && host.clientWidth ? host.clientWidth : window.innerWidth
|
|
84
|
+
var vh = window.innerHeight
|
|
85
|
+
var scale = Math.min(vw / w, vh / h)
|
|
86
|
+
steps.push({
|
|
87
|
+
node: node,
|
|
88
|
+
host: host,
|
|
89
|
+
/* Empty exactly when the box is not rendered: how a paged artifact says
|
|
90
|
+
that its other pages are not on screen at the moment. */
|
|
91
|
+
shown: node.getClientRects().length > 0,
|
|
92
|
+
scale: scale,
|
|
93
|
+
offsetX: (vw - w * scale) / 2,
|
|
94
|
+
offsetY: (vh - h * scale) / 2,
|
|
95
|
+
advance: h * scale - h,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return steps
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether no other rendered canvas shares this one's host. */
|
|
102
|
+
function alone(steps, step) {
|
|
103
|
+
for (var i = 0; i < steps.length; i += 1) {
|
|
104
|
+
if (steps[i] !== step && steps[i].host === step.host && steps[i].shown) return false
|
|
105
|
+
}
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fit() {
|
|
110
|
+
var steps = plan()
|
|
111
|
+
for (var i = 0; i < steps.length; i += 1) {
|
|
112
|
+
var step = steps[i]
|
|
113
|
+
var scale = step.scale
|
|
114
|
+
var x = step.offsetX
|
|
115
|
+
var y = alone(steps, step) ? step.offsetY : 0
|
|
116
|
+
step.node.style.transformOrigin = 'top left'
|
|
117
|
+
step.node.style.transform = 'translate(' + x + 'px, ' + y + 'px) scale(' + scale + ')'
|
|
118
|
+
step.node.style.marginBottom = step.advance + 'px'
|
|
119
|
+
step.node.setAttribute(SCALE_ATTR, String(scale))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
window.addEventListener('resize', fit)
|
|
124
|
+
document.addEventListener('fullscreenchange', fit)
|
|
125
|
+
if (document.readyState === 'loading') {
|
|
126
|
+
document.addEventListener('DOMContentLoaded', fit)
|
|
127
|
+
} else {
|
|
128
|
+
fit()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
document.addEventListener('keydown', function (event) {
|
|
132
|
+
if (event.key !== 'f' && event.key !== 'F') return
|
|
133
|
+
if (document.fullscreenElement) document.exitFullscreen()
|
|
134
|
+
else if (document.documentElement.requestFullscreen) document.documentElement.requestFullscreen()
|
|
135
|
+
})
|
|
136
|
+
})()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale-to-fit — what a **bounded** 文體 gets because of what it is.
|
|
3
|
+
*
|
|
4
|
+
* A bounded artifact (a slide, a card, a one-page site, a résumé) has a *fixed
|
|
5
|
+
* logical canvas*: the author lays out 1280×720 once and the window shows all
|
|
6
|
+
* of it, whatever size the window happens to be (wayfinder issue 14 裁決 4).
|
|
7
|
+
* This is also the answer to v1's 「fullscreen 只多黑框」 — going fullscreen made
|
|
8
|
+
* the page bigger and the artifact exactly as small as before, because nothing
|
|
9
|
+
* was ever scaled.
|
|
10
|
+
*
|
|
11
|
+
* The arithmetic lives here, apart from the browser asset, so it can be
|
|
12
|
+
* asserted without a browser. **One scalar for both axes** is the whole
|
|
13
|
+
* correctness claim: two would fit the canvas exactly and distort every glyph
|
|
14
|
+
* in it, which is the failure mode that looks fine in a screenshot and is
|
|
15
|
+
* unreadable on a projector.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The attribute and the attribute builder are **defined one layer down**, with
|
|
20
|
+
* the resolved canvas (`src/layouts/canvas.js`), and re-exported here so this
|
|
21
|
+
* module stays the one place the render layer asks about scaling. The reason
|
|
22
|
+
* for the move is in that file: the block that draws a bounded canvas element
|
|
23
|
+
* must be able to spell the attribute without importing the render layer.
|
|
24
|
+
*/
|
|
25
|
+
export { CANVAS_ATTR, canvasAttrs } from '../layouts/canvas.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The uniform scale and the offsets that centre the canvas in the viewport.
|
|
29
|
+
*
|
|
30
|
+
* @param {{canvas: {width: number, height: number},
|
|
31
|
+
* viewport: {width: number, height: number}}} input
|
|
32
|
+
* @returns {{scale: number, offsetX: number, offsetY: number}}
|
|
33
|
+
*/
|
|
34
|
+
export function scaleToFit({ canvas, viewport }) {
|
|
35
|
+
const scale = Math.min(viewport.width / canvas.width, viewport.height / canvas.height)
|
|
36
|
+
return {
|
|
37
|
+
scale,
|
|
38
|
+
offsetX: (viewport.width - canvas.width * scale) / 2,
|
|
39
|
+
offsetY: (viewport.height - canvas.height * scale) / 2,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { CANVAS_BOUNDED } from '../layouts/index.js'
|
|
4
|
+
import { collectBlockTypes } from '../core/vocabulary.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client assets — the behaviour an artifact gets because of *what it is*,
|
|
8
|
+
* not because of which template drew it.
|
|
9
|
+
*
|
|
10
|
+
* None of them is keyed on a template name, so a third-party template inherits
|
|
11
|
+
* the behaviour without asking and without being able to decline: a slide
|
|
12
|
+
* artifact that cannot be paged through is not a slide artifact, and a bounded
|
|
13
|
+
* canvas that does not scale is a black border.
|
|
14
|
+
*
|
|
15
|
+
* Two keys, two reasons:
|
|
16
|
+
*
|
|
17
|
+
* - **layout-keyed (skeleton)** — `playback` for a deck-rooted layout
|
|
18
|
+
* (wayfinder issue 13 P3), `scale-to-fit` for a bounded canvas
|
|
19
|
+
* (issue 14 裁決 4).
|
|
20
|
+
* - **block-keyed (interaction)** — `board-filter` and `graph-hover`
|
|
21
|
+
* (issue 11 P5), embedded only into artifacts that carry the block the
|
|
22
|
+
* interaction belongs to. That condition is what keeps every artifact drawn
|
|
23
|
+
* before F6a byte for byte unchanged: a document with no `board` and no
|
|
24
|
+
* `graph` gets exactly the script it got before, and a document with no
|
|
25
|
+
* behaviour at all still gets no script element at all.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `playback.client.js` used to live in `templates/kami/slides/`, which made
|
|
30
|
+
* "a deck can be paged through" a property of one template rather than of
|
|
31
|
+
* decks. Its own header still describes itself as a template asset: the file is
|
|
32
|
+
* embedded into every deck artifact **verbatim**, so a single edited character
|
|
33
|
+
* moves the artifact's bytes (CONTRACT A2). The header is corrected together
|
|
34
|
+
* with SPEC §7.4, in the slice that is allowed to move those bytes *and* to
|
|
35
|
+
* edit the SPEC — F2f was allowed the first and not the second, so it left the
|
|
36
|
+
* pair alone rather than making the file and the SPEC disagree.
|
|
37
|
+
*/
|
|
38
|
+
const PLAYBACK_PATH = fileURLToPath(new URL('./playback.client.js', import.meta.url))
|
|
39
|
+
const SCALE_PATH = fileURLToPath(new URL('./scale-to-fit.client.js', import.meta.url))
|
|
40
|
+
const BOARD_FILTER_PATH = fileURLToPath(new URL('./board-filter.client.js', import.meta.url))
|
|
41
|
+
const GRAPH_HOVER_PATH = fileURLToPath(new URL('./graph-hover.client.js', import.meta.url))
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every asset this module can embed, by role — the **single source of the list**.
|
|
45
|
+
*
|
|
46
|
+
* Exported because the list has judges: the scan that keeps these files free of
|
|
47
|
+
* the sequences that would derail the HTML tokenizer, and free of non-ASCII,
|
|
48
|
+
* has to run over *all* of them. A hand-copied list in the test file is a list
|
|
49
|
+
* that silently misses the third asset the day one arrives — the same shape as
|
|
50
|
+
* the "至少" thresholds F2d's seal replaced with set equality. So the test
|
|
51
|
+
* derives its scan targets from here, and separately asserts this object's
|
|
52
|
+
* value set equals the `*.client.js` files actually sitting in this directory:
|
|
53
|
+
* adding a file without wiring it, or wiring one that does not exist, is red
|
|
54
|
+
* either way (CONTRACT F2g H4).
|
|
55
|
+
*/
|
|
56
|
+
export const SKELETON_ASSETS = Object.freeze({
|
|
57
|
+
playback: PLAYBACK_PATH,
|
|
58
|
+
scale: SCALE_PATH,
|
|
59
|
+
boardFilter: BOARD_FILTER_PATH,
|
|
60
|
+
graphHover: GRAPH_HOVER_PATH,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** Verbatim — the root form that makes an artifact a deck. */
|
|
64
|
+
const DECK_ROOT = 'deck'
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which block type pulls in which interaction asset (issue 11 P5).
|
|
68
|
+
*
|
|
69
|
+
* Order is load-bearing in the same, dull way the module list is: it is the
|
|
70
|
+
* order the assets are concatenated in, and therefore part of the artifact's
|
|
71
|
+
* bytes. It follows the canonical block order, so it never has to be argued
|
|
72
|
+
* about again.
|
|
73
|
+
*/
|
|
74
|
+
const INTERACTION_ASSETS = Object.freeze([
|
|
75
|
+
Object.freeze({ type: 'board', path: BOARD_FILTER_PATH }),
|
|
76
|
+
Object.freeze({ type: 'graph', path: GRAPH_HOVER_PATH }),
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The inline script for an artifact, or `undefined` when the skeleton has
|
|
81
|
+
* nothing to add (a layout without behaviour emits no element at all, which is
|
|
82
|
+
* what keeps its artifacts byte-identical).
|
|
83
|
+
*
|
|
84
|
+
* @param {{rootForm?: string|null, canvas?: string}} layout
|
|
85
|
+
* @returns {string|undefined}
|
|
86
|
+
*/
|
|
87
|
+
export function skeletonScript(layout) {
|
|
88
|
+
const assets = []
|
|
89
|
+
if (layout?.rootForm === DECK_ROOT) assets.push(readFileSync(PLAYBACK_PATH, 'utf8'))
|
|
90
|
+
if (layout?.canvas === CANVAS_BOUNDED) assets.push(readFileSync(SCALE_PATH, 'utf8'))
|
|
91
|
+
return assets.length === 0 ? undefined : assets.join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The inline script the *blocks in this document* ask for, or `undefined` when
|
|
96
|
+
* none of them does.
|
|
97
|
+
*
|
|
98
|
+
* Keyed on the document rather than on the template's declared vocabulary: a
|
|
99
|
+
* template may admit `board` and this document may contain none, and shipping
|
|
100
|
+
* the filter into an artifact with nothing to filter is dead weight in a file
|
|
101
|
+
* whose whole promise is that it is self-contained. It also makes the byte
|
|
102
|
+
* claim exact — every artifact rendered before F6a contains neither block, so
|
|
103
|
+
* every one of them still gets `undefined` here.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} doc the canonical block tree
|
|
106
|
+
* @returns {string|undefined}
|
|
107
|
+
*/
|
|
108
|
+
export function interactionScript(doc) {
|
|
109
|
+
const present = collectBlockTypes(doc)
|
|
110
|
+
const assets = INTERACTION_ASSETS.filter(({ type }) => present.has(type)).map(({ path }) =>
|
|
111
|
+
readFileSync(path, 'utf8'),
|
|
112
|
+
)
|
|
113
|
+
return assets.length === 0 ? undefined : assets.join('\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Everything that goes into the artifact's one behaviour script, in order:
|
|
118
|
+
* skeleton first (it decides what the page *is*), interaction after.
|
|
119
|
+
*
|
|
120
|
+
* `undefined` rather than an empty string when there is nothing to say — a
|
|
121
|
+
* layout without behaviour emits no script element at all, which is what keeps
|
|
122
|
+
* its artifacts byte-identical.
|
|
123
|
+
*
|
|
124
|
+
* @returns {string|undefined}
|
|
125
|
+
*/
|
|
126
|
+
export function artifactScript(layout, doc) {
|
|
127
|
+
const parts = [skeletonScript(layout), interactionScript(doc)].filter(
|
|
128
|
+
(part) => typeof part === 'string',
|
|
129
|
+
)
|
|
130
|
+
return parts.length === 0 ? undefined : parts.join('\n')
|
|
131
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSSRApp } from 'vue'
|
|
2
|
+
import { renderToString } from 'vue/server-renderer'
|
|
3
|
+
import { toVNode } from './element.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server-render a layout's root form against a doc block tree.
|
|
7
|
+
*
|
|
8
|
+
* The root is a plain function returning a neutral element tree, so Vue never
|
|
9
|
+
* sees the layout, the template or any block module — it only sees the vnodes
|
|
10
|
+
* `toVNode` builds. That is what makes the SSR backend replaceable and both a
|
|
11
|
+
* layout and a template package pure data (wayfinder issue 13 裁決 3).
|
|
12
|
+
*
|
|
13
|
+
* The root arrives as a function rather than as a package, because since F2a it
|
|
14
|
+
* comes from the **layout**: a template supplies skin, config and chrome, and
|
|
15
|
+
* the shape of the page is the 文體's (CONTRACT C1).
|
|
16
|
+
*
|
|
17
|
+
* @param {(doc: object, ctx: object) => object} root the layout's root form
|
|
18
|
+
*/
|
|
19
|
+
export async function renderBody(root, doc, ctx) {
|
|
20
|
+
const app = createSSRApp({
|
|
21
|
+
render: () => toVNode(root(doc, ctx)),
|
|
22
|
+
})
|
|
23
|
+
return await renderToString(app)
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout geometry flows **into** the stylesheet, never out of it.
|
|
3
|
+
*
|
|
4
|
+
* `--measure: 740px` used to be a constant typed into
|
|
5
|
+
* `templates/kami/long-form/styles.css`, while `manifest.measure: 740` restated
|
|
6
|
+
* the same number as data so a diagram could be measured against it (量測報告
|
|
7
|
+
* §4.2). Two copies of one number, in two languages, with nothing keeping them
|
|
8
|
+
* in step — and the CSS copy was the one the reader actually saw.
|
|
9
|
+
*
|
|
10
|
+
* So the stylesheet now declares *where* a geometry value goes and the config
|
|
11
|
+
* says *what* it is: inside `:root`, a CSS comment reading `ksb:measure
|
|
12
|
+
* --measure` is replaced by the declaration `--measure: 740px;`.
|
|
13
|
+
*
|
|
14
|
+
* The marker is a CSS comment, so a stylesheet stays a valid stylesheet and can
|
|
15
|
+
* be opened, linted and edited by anything that understands CSS. It names the
|
|
16
|
+
* canvas knob it wants and the custom property to write it into, which is what
|
|
17
|
+
* lets `kami/slides` fill `--slide-measure` from `maxWidth` without the SDK
|
|
18
|
+
* knowing either name.
|
|
19
|
+
*
|
|
20
|
+
* Substituting in place (rather than prepending a `:root` block) is also what
|
|
21
|
+
* keeps CONTRACT C5 satisfiable at all: the emitted CSS is byte-for-byte the
|
|
22
|
+
* text that was there before the marker replaced the constant.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A CSS comment reading `ksb:<knob> --<custom-property>` — the one legal form. */
|
|
26
|
+
const MARKER = /\/\*\s*ksb:([A-Za-z][A-Za-z0-9]*)\s+(--[A-Za-z0-9-]+)\s*\*\//g
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fill every geometry marker in a template's stylesheet.
|
|
30
|
+
*
|
|
31
|
+
* What a marker may name is **whatever the resolved canvas holds as a number** —
|
|
32
|
+
* the two canvas dimensions every 文體 has, plus whichever knobs that 文體
|
|
33
|
+
* declared for itself (`article` declares `railWidth`; CONTRACT F2b G1). A
|
|
34
|
+
* hard-coded list here would mean a third-party layout could define a knob, a
|
|
35
|
+
* template could turn it, and the stylesheet still had no way to read it
|
|
36
|
+
* without an edit inside the SDK — which is the one thing the layout axis
|
|
37
|
+
* exists to make unnecessary.
|
|
38
|
+
*
|
|
39
|
+
* An unknown knob, or one that is not a number (a boolean switch, a side), is
|
|
40
|
+
* left exactly as written rather than replaced with `undefined`: a typo must
|
|
41
|
+
* not silently become a broken declaration that still parses.
|
|
42
|
+
* `test_f2a_style_markers_resolved` asserts no marker survives in the factory
|
|
43
|
+
* templates, so a typo is loud in the suite instead of in the artifact.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} css the template's raw stylesheet
|
|
46
|
+
* @param {{maxWidth: number, measure: number}} canvas the resolved canvas
|
|
47
|
+
*/
|
|
48
|
+
export function injectLayoutVars(css, canvas) {
|
|
49
|
+
return String(css ?? '').replace(MARKER, (whole, knob, property) => {
|
|
50
|
+
if (typeof canvas?.[knob] !== 'number') return whole
|
|
51
|
+
return `${property}: ${canvas[knob]}px;`
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether any geometry marker is still unfilled — for the suite, not the pipeline. */
|
|
56
|
+
export const unresolvedMarkers = (css) => String(css ?? '').match(MARKER) ?? []
|