create-pathfinder 4.2.0 → 4.3.0
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/CLAUDE.md +2 -0
- package/package.json +1 -1
- package/skills/learn-codebase/SKILL.md +188 -17
- package/skills/learn-feature/SKILL.md +136 -15
- package/skills/map-system/SKILL.md +293 -0
- package/skills/render-artifact/SKILL.md +187 -0
- package/skills/render-artifact/engine/bin/render.mjs +225 -0
- package/skills/render-artifact/engine/deliver.mjs +197 -0
- package/skills/render-artifact/engine/doctor.mjs +96 -0
- package/skills/render-artifact/engine/examples/diagram.json +223 -0
- package/skills/render-artifact/engine/examples/lesson.json +242 -0
- package/skills/render-artifact/engine/references/determinism.md +71 -0
- package/skills/render-artifact/engine/references/specification.md +149 -0
- package/skills/render-artifact/engine/references/validation.md +268 -0
- package/skills/render-artifact/engine/render/behavior.mjs +128 -0
- package/skills/render-artifact/engine/render/diagram.mjs +342 -0
- package/skills/render-artifact/engine/render/escape.mjs +34 -0
- package/skills/render-artifact/engine/render/graph/behavior.mjs +394 -0
- package/skills/render-artifact/engine/render/graph/draw.mjs +204 -0
- package/skills/render-artifact/engine/render/graph/interaction.mjs +174 -0
- package/skills/render-artifact/engine/render/graph/layout.mjs +698 -0
- package/skills/render-artifact/engine/render/graph/style.mjs +200 -0
- package/skills/render-artifact/engine/render/graph/width.mjs +204 -0
- package/skills/render-artifact/engine/render/index.mjs +50 -0
- package/skills/render-artifact/engine/render/lesson.mjs +294 -0
- package/skills/render-artifact/engine/render/shell.mjs +275 -0
- package/skills/render-artifact/engine/render/theme.mjs +592 -0
- package/skills/render-artifact/engine/schemas/common.schema.json +101 -0
- package/skills/render-artifact/engine/schemas/diagram.schema.json +176 -0
- package/skills/render-artifact/engine/schemas/lesson.schema.json +210 -0
- package/skills/render-artifact/engine/validate/composition.mjs +395 -0
- package/skills/render-artifact/engine/validate/diagnostics.mjs +83 -0
- package/skills/render-artifact/engine/validate/diagram-parts.mjs +68 -0
- package/skills/render-artifact/engine/validate/evidence.mjs +302 -0
- package/skills/render-artifact/engine/validate/index.mjs +132 -0
- package/skills/render-artifact/engine/validate/jsonschema.mjs +312 -0
- package/skills/render-artifact/engine/validate/structural.mjs +241 -0
- package/skills/render-artifact/engine/verification.mjs +76 -0
- package/skills/render-artifact/engine/version.mjs +24 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a diagram looks, in terms of the identity that already exists.
|
|
3
|
+
*
|
|
4
|
+
* Not one colour is defined here. Every value below resolves to a `--pf-*`
|
|
5
|
+
* token from `theme.mjs`, so a diagram inherits both themes, the measured
|
|
6
|
+
* contrast behind them, and any future change to either, without restating a
|
|
7
|
+
* single one. That is what "adding a kind must not require restating the
|
|
8
|
+
* identity" means when the kind is a picture rather than a page.
|
|
9
|
+
*
|
|
10
|
+
* What *is* defined here is shape: stroke weights, corner treatment, the grid
|
|
11
|
+
* the canvas sits on. Those are diagram vocabulary and have no meaning for a
|
|
12
|
+
* lesson, which is why they live beside the diagram renderer rather than in the
|
|
13
|
+
* shared theme.
|
|
14
|
+
*
|
|
15
|
+
* Two deliberate choices, both of which read as design and are really
|
|
16
|
+
* accessibility:
|
|
17
|
+
*
|
|
18
|
+
* - **Role is never carried by colour alone.** Shape and a printed role word
|
|
19
|
+
* carry it; the accent only reinforces it.
|
|
20
|
+
* - **Labels are monospace.** Diagram labels are technical tokens — endpoint
|
|
21
|
+
* names, queue names, service names — and mono is the honest typography for
|
|
22
|
+
* them. It is also what keeps a character-budget line break close to the
|
|
23
|
+
* truth while the width-aware model is still being decided.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const GRAPH_CSS = `
|
|
27
|
+
.pf-canvas {
|
|
28
|
+
margin: var(--pf-space-5) 0 var(--pf-space-7);
|
|
29
|
+
padding: var(--pf-space-4);
|
|
30
|
+
background: var(--pf-surface);
|
|
31
|
+
border: 1px solid var(--pf-line);
|
|
32
|
+
border-radius: var(--pf-radius);
|
|
33
|
+
box-shadow: var(--pf-shadow);
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
}
|
|
36
|
+
.pf-graph {
|
|
37
|
+
display: block;
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: auto;
|
|
40
|
+
font-family: var(--pf-mono);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.pf-group-box {
|
|
44
|
+
fill: var(--pf-surface-2);
|
|
45
|
+
stroke: var(--pf-line-strong);
|
|
46
|
+
stroke-width: 1;
|
|
47
|
+
stroke-dasharray: 4 4;
|
|
48
|
+
}
|
|
49
|
+
.pf-group[data-pf-depth="1"] .pf-group-box { stroke-dasharray: 2 3; }
|
|
50
|
+
.pf-group-label {
|
|
51
|
+
fill: var(--pf-muted);
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
letter-spacing: .06em;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.pf-node-box {
|
|
58
|
+
fill: var(--pf-surface);
|
|
59
|
+
stroke: var(--pf-line-strong);
|
|
60
|
+
stroke-width: 2;
|
|
61
|
+
}
|
|
62
|
+
.pf-node[data-pf-role="store"] .pf-node-box,
|
|
63
|
+
.pf-node[data-pf-role="queue"] .pf-node-box { stroke-dasharray: 7 3; }
|
|
64
|
+
.pf-node[data-pf-role="external"] .pf-node-box,
|
|
65
|
+
.pf-node[data-pf-role="actor"] .pf-node-box { stroke: var(--pf-accent); }
|
|
66
|
+
.pf-node[data-pf-role="terminal"] .pf-node-box { stroke: var(--pf-ok); }
|
|
67
|
+
.pf-node[data-pf-role="decision"] .pf-node-box { stroke: var(--pf-accent); stroke-dasharray: 3 3; }
|
|
68
|
+
|
|
69
|
+
.pf-node-label {
|
|
70
|
+
fill: var(--pf-ink);
|
|
71
|
+
font-size: 13px;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
.pf-node-role {
|
|
75
|
+
fill: var(--pf-muted);
|
|
76
|
+
font-size: 10px;
|
|
77
|
+
letter-spacing: .1em;
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.pf-edge-line {
|
|
82
|
+
fill: none;
|
|
83
|
+
stroke: var(--pf-line-strong);
|
|
84
|
+
stroke-width: 2;
|
|
85
|
+
stroke-linejoin: round;
|
|
86
|
+
stroke-linecap: round;
|
|
87
|
+
}
|
|
88
|
+
.pf-edge-on-path .pf-edge-line { stroke: var(--pf-accent); stroke-width: 3; }
|
|
89
|
+
.pf-edge[data-pf-relation="depends_on"] .pf-edge-line { stroke-dasharray: 5 4; }
|
|
90
|
+
.pf-edge[data-pf-relation="publishes"] .pf-edge-line,
|
|
91
|
+
.pf-edge[data-pf-relation="consumes"] .pf-edge-line { stroke-dasharray: 2 4; }
|
|
92
|
+
.pf-edge-label {
|
|
93
|
+
fill: var(--pf-muted);
|
|
94
|
+
font-size: 11px;
|
|
95
|
+
}
|
|
96
|
+
.pf-edge-on-path .pf-edge-label { fill: var(--pf-link); }
|
|
97
|
+
|
|
98
|
+
/* ---- the reading controls, and what a selection looks like ----
|
|
99
|
+
|
|
100
|
+
Three states, and the distinction is deliberate. "on" is what the reader
|
|
101
|
+
asked about. "near" is what it touches — still readable, because a component
|
|
102
|
+
whose neighbours have been greyed out tells you less than one whose
|
|
103
|
+
neighbours are merely quieter. "off" is the rest of the graph, dimmed rather
|
|
104
|
+
than removed: the document keeps every fact whatever is selected, and a
|
|
105
|
+
reader who dislikes the dimming can turn scripting off and read all of it.
|
|
106
|
+
|
|
107
|
+
No colour is defined here either. Selection reads through the accent that
|
|
108
|
+
already carries emphasis, and through opacity, so it works in both themes
|
|
109
|
+
without a second palette. Opacity alone would be a colour-only signal, so
|
|
110
|
+
the focused node also thickens its stroke. */
|
|
111
|
+
.pf-graph-tools { margin: var(--pf-space-5) 0 calc(var(--pf-space-3) * -1); }
|
|
112
|
+
.pf-toolbar {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: var(--pf-space-2);
|
|
117
|
+
}
|
|
118
|
+
.pf-tool {
|
|
119
|
+
padding: 4px 10px;
|
|
120
|
+
background: var(--pf-surface);
|
|
121
|
+
color: var(--pf-ink);
|
|
122
|
+
border: 1px solid var(--pf-line-strong);
|
|
123
|
+
border-radius: var(--pf-radius-sm);
|
|
124
|
+
font-family: var(--pf-mono);
|
|
125
|
+
font-size: .78rem;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
.pf-tool:hover:not(:disabled) { border-color: var(--pf-accent); color: var(--pf-link); }
|
|
129
|
+
.pf-tool:disabled { opacity: .45; cursor: default; }
|
|
130
|
+
.pf-tool-sep {
|
|
131
|
+
width: 1px;
|
|
132
|
+
height: 1.2em;
|
|
133
|
+
background: var(--pf-line);
|
|
134
|
+
}
|
|
135
|
+
.pf-graph-status {
|
|
136
|
+
margin: var(--pf-space-2) 0 0;
|
|
137
|
+
color: var(--pf-muted);
|
|
138
|
+
font-family: var(--pf-mono);
|
|
139
|
+
font-size: .78rem;
|
|
140
|
+
}
|
|
141
|
+
/* Inline and free to wrap. A float here escaped its heading at narrow widths,
|
|
142
|
+
which is exactly where this gets read. */
|
|
143
|
+
.pf-pick { margin-left: var(--pf-space-2); white-space: nowrap; }
|
|
144
|
+
|
|
145
|
+
.pf-canvas:focus-visible { outline: 3px solid var(--pf-accent); outline-offset: 2px; }
|
|
146
|
+
.pf-canvas[data-pf-zoom="in"] .pf-graph { cursor: grab; }
|
|
147
|
+
.pf-canvas[data-pf-dragging] .pf-graph { cursor: grabbing; }
|
|
148
|
+
|
|
149
|
+
/* Selection. Keyed off attributes the script sets and nothing else, so the
|
|
150
|
+
delivered document is identical whether or not a browser ever runs it. */
|
|
151
|
+
.pf-node[data-pf-state="off"],
|
|
152
|
+
.pf-edge[data-pf-state="off"] { opacity: .22; }
|
|
153
|
+
.pf-node[data-pf-state="near"],
|
|
154
|
+
.pf-edge[data-pf-state="near"] { opacity: .75; }
|
|
155
|
+
.pf-node[data-pf-state="on"] .pf-node-box {
|
|
156
|
+
stroke: var(--pf-accent);
|
|
157
|
+
stroke-width: 4;
|
|
158
|
+
}
|
|
159
|
+
.pf-edge[data-pf-state="on"] .pf-edge-line {
|
|
160
|
+
stroke: var(--pf-accent);
|
|
161
|
+
stroke-width: 3;
|
|
162
|
+
}
|
|
163
|
+
.pf-edge[data-pf-state="on"] .pf-edge-label { fill: var(--pf-link); }
|
|
164
|
+
|
|
165
|
+
/* The written entry the selection points at. A left rule rather than a fill:
|
|
166
|
+
the card already has a surface, and stacking another on it would flatten
|
|
167
|
+
the hierarchy the evidence block sits in. */
|
|
168
|
+
[data-pf-entry][aria-current] {
|
|
169
|
+
border-left: 3px solid var(--pf-accent);
|
|
170
|
+
padding-left: var(--pf-space-3);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
174
|
+
.pf-node, .pf-edge { transition: opacity 120ms linear; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The reading list below the canvas: the same facts, as text, for a reader who
|
|
178
|
+
is not looking at a picture. */
|
|
179
|
+
.pf-legend { display: grid; gap: var(--pf-space-4); }
|
|
180
|
+
.pf-legend-role {
|
|
181
|
+
display: inline-block;
|
|
182
|
+
margin-left: var(--pf-space-2);
|
|
183
|
+
padding: 1px 6px;
|
|
184
|
+
border: 1px solid var(--pf-line);
|
|
185
|
+
border-radius: var(--pf-radius-sm);
|
|
186
|
+
color: var(--pf-muted);
|
|
187
|
+
font-family: var(--pf-mono);
|
|
188
|
+
font-size: .72rem;
|
|
189
|
+
letter-spacing: .08em;
|
|
190
|
+
text-transform: uppercase;
|
|
191
|
+
vertical-align: middle;
|
|
192
|
+
}
|
|
193
|
+
.pf-relation {
|
|
194
|
+
font-family: var(--pf-mono);
|
|
195
|
+
font-size: .82rem;
|
|
196
|
+
color: var(--pf-muted);
|
|
197
|
+
}
|
|
198
|
+
.pf-walk { margin: 0; padding-left: var(--pf-space-5); }
|
|
199
|
+
.pf-walk li { margin-bottom: var(--pf-space-1); }
|
|
200
|
+
`.trim();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many columns a code point occupies. Generated, pinned, and never derived
|
|
3
|
+
* at run time.
|
|
4
|
+
*
|
|
5
|
+
* GENERATED FILE — do not edit by hand.
|
|
6
|
+
* Regenerate with `node scripts/generate-width-table.mjs` in
|
|
7
|
+
* `packages/render-artifact`, deliberately, as an adopted Unicode upgrade.
|
|
8
|
+
*
|
|
9
|
+
* Pinned to **Unicode 17.0**, the version the generating engine carried.
|
|
10
|
+
* The renderer does not consult the engine it runs on: `\p{Mn}` and friends
|
|
11
|
+
* resolve against whatever Unicode data that engine was compiled with, and that
|
|
12
|
+
* moves between Node releases. Two machines would then lay the same label out
|
|
13
|
+
* differently, which is the ambient dependency the determinism invariant
|
|
14
|
+
* forbids. So the answer is frozen here as literal ranges.
|
|
15
|
+
*
|
|
16
|
+
* Three widths, and a total function:
|
|
17
|
+
*
|
|
18
|
+
* 0 non-spacing and enclosing marks, and format characters — including
|
|
19
|
+
* U+200D ZERO WIDTH JOINER and the variation selectors
|
|
20
|
+
* 2 East Asian Wide and Fullwidth blocks, and default-emoji-presentation
|
|
21
|
+
* 1 everything else, including every code point not named below
|
|
22
|
+
*
|
|
23
|
+
* The default of 1 is what makes this total. A code point Unicode assigns after
|
|
24
|
+
* this table was pinned is measured as one column rather than throwing or
|
|
25
|
+
* returning undefined, so a label in a script newer than the renderer still
|
|
26
|
+
* lays out — narrower than it should be if that character is actually wide,
|
|
27
|
+
* which the box's headroom absorbs.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The Unicode version this table was generated from. */
|
|
31
|
+
export const UNICODE_VERSION = "17.0";
|
|
32
|
+
|
|
33
|
+
/** Code points that occupy no column. */
|
|
34
|
+
const ZERO_WIDTH = [
|
|
35
|
+
[0xAD, 0xAD], [0x300, 0x36F], [0x483, 0x489], [0x591, 0x5BD],
|
|
36
|
+
[0x5BF, 0x5BF], [0x5C1, 0x5C2], [0x5C4, 0x5C5], [0x5C7, 0x5C7],
|
|
37
|
+
[0x600, 0x605], [0x610, 0x61A], [0x61C, 0x61C], [0x64B, 0x65F],
|
|
38
|
+
[0x670, 0x670], [0x6D6, 0x6DD], [0x6DF, 0x6E4], [0x6E7, 0x6E8],
|
|
39
|
+
[0x6EA, 0x6ED], [0x70F, 0x70F], [0x711, 0x711], [0x730, 0x74A],
|
|
40
|
+
[0x7A6, 0x7B0], [0x7EB, 0x7F3], [0x7FD, 0x7FD], [0x816, 0x819],
|
|
41
|
+
[0x81B, 0x823], [0x825, 0x827], [0x829, 0x82D], [0x859, 0x85B],
|
|
42
|
+
[0x890, 0x891], [0x897, 0x89F], [0x8CA, 0x902], [0x93A, 0x93A],
|
|
43
|
+
[0x93C, 0x93C], [0x941, 0x948], [0x94D, 0x94D], [0x951, 0x957],
|
|
44
|
+
[0x962, 0x963], [0x981, 0x981], [0x9BC, 0x9BC], [0x9C1, 0x9C4],
|
|
45
|
+
[0x9CD, 0x9CD], [0x9E2, 0x9E3], [0x9FE, 0x9FE], [0xA01, 0xA02],
|
|
46
|
+
[0xA3C, 0xA3C], [0xA41, 0xA42], [0xA47, 0xA48], [0xA4B, 0xA4D],
|
|
47
|
+
[0xA51, 0xA51], [0xA70, 0xA71], [0xA75, 0xA75], [0xA81, 0xA82],
|
|
48
|
+
[0xABC, 0xABC], [0xAC1, 0xAC5], [0xAC7, 0xAC8], [0xACD, 0xACD],
|
|
49
|
+
[0xAE2, 0xAE3], [0xAFA, 0xAFF], [0xB01, 0xB01], [0xB3C, 0xB3C],
|
|
50
|
+
[0xB3F, 0xB3F], [0xB41, 0xB44], [0xB4D, 0xB4D], [0xB55, 0xB56],
|
|
51
|
+
[0xB62, 0xB63], [0xB82, 0xB82], [0xBC0, 0xBC0], [0xBCD, 0xBCD],
|
|
52
|
+
[0xC00, 0xC00], [0xC04, 0xC04], [0xC3C, 0xC3C], [0xC3E, 0xC40],
|
|
53
|
+
[0xC46, 0xC48], [0xC4A, 0xC4D], [0xC55, 0xC56], [0xC62, 0xC63],
|
|
54
|
+
[0xC81, 0xC81], [0xCBC, 0xCBC], [0xCBF, 0xCBF], [0xCC6, 0xCC6],
|
|
55
|
+
[0xCCC, 0xCCD], [0xCE2, 0xCE3], [0xD00, 0xD01], [0xD3B, 0xD3C],
|
|
56
|
+
[0xD41, 0xD44], [0xD4D, 0xD4D], [0xD62, 0xD63], [0xD81, 0xD81],
|
|
57
|
+
[0xDCA, 0xDCA], [0xDD2, 0xDD4], [0xDD6, 0xDD6], [0xE31, 0xE31],
|
|
58
|
+
[0xE34, 0xE3A], [0xE47, 0xE4E], [0xEB1, 0xEB1], [0xEB4, 0xEBC],
|
|
59
|
+
[0xEC8, 0xECE], [0xF18, 0xF19], [0xF35, 0xF35], [0xF37, 0xF37],
|
|
60
|
+
[0xF39, 0xF39], [0xF71, 0xF7E], [0xF80, 0xF84], [0xF86, 0xF87],
|
|
61
|
+
[0xF8D, 0xF97], [0xF99, 0xFBC], [0xFC6, 0xFC6], [0x102D, 0x1030],
|
|
62
|
+
[0x1032, 0x1037], [0x1039, 0x103A], [0x103D, 0x103E], [0x1058, 0x1059],
|
|
63
|
+
[0x105E, 0x1060], [0x1071, 0x1074], [0x1082, 0x1082], [0x1085, 0x1086],
|
|
64
|
+
[0x108D, 0x108D], [0x109D, 0x109D], [0x135D, 0x135F], [0x1712, 0x1714],
|
|
65
|
+
[0x1732, 0x1733], [0x1752, 0x1753], [0x1772, 0x1773], [0x17B4, 0x17B5],
|
|
66
|
+
[0x17B7, 0x17BD], [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
|
67
|
+
[0x180B, 0x180F], [0x1885, 0x1886], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
|
68
|
+
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], [0x1A17, 0x1A18],
|
|
69
|
+
[0x1A1B, 0x1A1B], [0x1A56, 0x1A56], [0x1A58, 0x1A5E], [0x1A60, 0x1A60],
|
|
70
|
+
[0x1A62, 0x1A62], [0x1A65, 0x1A6C], [0x1A73, 0x1A7C], [0x1A7F, 0x1A7F],
|
|
71
|
+
[0x1AB0, 0x1ADD], [0x1AE0, 0x1AEB], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
|
72
|
+
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B6B, 0x1B73],
|
|
73
|
+
[0x1B80, 0x1B81], [0x1BA2, 0x1BA5], [0x1BA8, 0x1BA9], [0x1BAB, 0x1BAD],
|
|
74
|
+
[0x1BE6, 0x1BE6], [0x1BE8, 0x1BE9], [0x1BED, 0x1BED], [0x1BEF, 0x1BF1],
|
|
75
|
+
[0x1C2C, 0x1C33], [0x1C36, 0x1C37], [0x1CD0, 0x1CD2], [0x1CD4, 0x1CE0],
|
|
76
|
+
[0x1CE2, 0x1CE8], [0x1CED, 0x1CED], [0x1CF4, 0x1CF4], [0x1CF8, 0x1CF9],
|
|
77
|
+
[0x1DC0, 0x1DFF], [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2064],
|
|
78
|
+
[0x2066, 0x206F], [0x20D0, 0x20F0], [0x2CEF, 0x2CF1], [0x2D7F, 0x2D7F],
|
|
79
|
+
[0x2DE0, 0x2DFF], [0x302A, 0x302D], [0x3099, 0x309A], [0xA66F, 0xA672],
|
|
80
|
+
[0xA674, 0xA67D], [0xA69E, 0xA69F], [0xA6F0, 0xA6F1], [0xA802, 0xA802],
|
|
81
|
+
[0xA806, 0xA806], [0xA80B, 0xA80B], [0xA825, 0xA826], [0xA82C, 0xA82C],
|
|
82
|
+
[0xA8C4, 0xA8C5], [0xA8E0, 0xA8F1], [0xA8FF, 0xA8FF], [0xA926, 0xA92D],
|
|
83
|
+
[0xA947, 0xA951], [0xA980, 0xA982], [0xA9B3, 0xA9B3], [0xA9B6, 0xA9B9],
|
|
84
|
+
[0xA9BC, 0xA9BD], [0xA9E5, 0xA9E5], [0xAA29, 0xAA2E], [0xAA31, 0xAA32],
|
|
85
|
+
[0xAA35, 0xAA36], [0xAA43, 0xAA43], [0xAA4C, 0xAA4C], [0xAA7C, 0xAA7C],
|
|
86
|
+
[0xAAB0, 0xAAB0], [0xAAB2, 0xAAB4], [0xAAB7, 0xAAB8], [0xAABE, 0xAABF],
|
|
87
|
+
[0xAAC1, 0xAAC1], [0xAAEC, 0xAAED], [0xAAF6, 0xAAF6], [0xABE5, 0xABE5],
|
|
88
|
+
[0xABE8, 0xABE8], [0xABED, 0xABED], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
|
89
|
+
[0xFE20, 0xFE2F], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], [0x101FD, 0x101FD],
|
|
90
|
+
[0x102E0, 0x102E0], [0x10376, 0x1037A], [0x10A01, 0x10A03], [0x10A05, 0x10A06],
|
|
91
|
+
[0x10A0C, 0x10A0F], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x10AE5, 0x10AE6],
|
|
92
|
+
[0x10D24, 0x10D27], [0x10D69, 0x10D6D], [0x10EAB, 0x10EAC], [0x10EFA, 0x10EFF],
|
|
93
|
+
[0x10F46, 0x10F50], [0x10F82, 0x10F85], [0x11001, 0x11001], [0x11038, 0x11046],
|
|
94
|
+
[0x11070, 0x11070], [0x11073, 0x11074], [0x1107F, 0x11081], [0x110B3, 0x110B6],
|
|
95
|
+
[0x110B9, 0x110BA], [0x110BD, 0x110BD], [0x110C2, 0x110C2], [0x110CD, 0x110CD],
|
|
96
|
+
[0x11100, 0x11102], [0x11127, 0x1112B], [0x1112D, 0x11134], [0x11173, 0x11173],
|
|
97
|
+
[0x11180, 0x11181], [0x111B6, 0x111BE], [0x111C9, 0x111CC], [0x111CF, 0x111CF],
|
|
98
|
+
[0x1122F, 0x11231], [0x11234, 0x11234], [0x11236, 0x11237], [0x1123E, 0x1123E],
|
|
99
|
+
[0x11241, 0x11241], [0x112DF, 0x112DF], [0x112E3, 0x112EA], [0x11300, 0x11301],
|
|
100
|
+
[0x1133B, 0x1133C], [0x11340, 0x11340], [0x11366, 0x1136C], [0x11370, 0x11374],
|
|
101
|
+
[0x113BB, 0x113C0], [0x113CE, 0x113CE], [0x113D0, 0x113D0], [0x113D2, 0x113D2],
|
|
102
|
+
[0x113E1, 0x113E2], [0x11438, 0x1143F], [0x11442, 0x11444], [0x11446, 0x11446],
|
|
103
|
+
[0x1145E, 0x1145E], [0x114B3, 0x114B8], [0x114BA, 0x114BA], [0x114BF, 0x114C0],
|
|
104
|
+
[0x114C2, 0x114C3], [0x115B2, 0x115B5], [0x115BC, 0x115BD], [0x115BF, 0x115C0],
|
|
105
|
+
[0x115DC, 0x115DD], [0x11633, 0x1163A], [0x1163D, 0x1163D], [0x1163F, 0x11640],
|
|
106
|
+
[0x116AB, 0x116AB], [0x116AD, 0x116AD], [0x116B0, 0x116B5], [0x116B7, 0x116B7],
|
|
107
|
+
[0x1171D, 0x1171D], [0x1171F, 0x1171F], [0x11722, 0x11725], [0x11727, 0x1172B],
|
|
108
|
+
[0x1182F, 0x11837], [0x11839, 0x1183A], [0x1193B, 0x1193C], [0x1193E, 0x1193E],
|
|
109
|
+
[0x11943, 0x11943], [0x119D4, 0x119D7], [0x119DA, 0x119DB], [0x119E0, 0x119E0],
|
|
110
|
+
[0x11A01, 0x11A0A], [0x11A33, 0x11A38], [0x11A3B, 0x11A3E], [0x11A47, 0x11A47],
|
|
111
|
+
[0x11A51, 0x11A56], [0x11A59, 0x11A5B], [0x11A8A, 0x11A96], [0x11A98, 0x11A99],
|
|
112
|
+
[0x11B60, 0x11B60], [0x11B62, 0x11B64], [0x11B66, 0x11B66], [0x11C30, 0x11C36],
|
|
113
|
+
[0x11C38, 0x11C3D], [0x11C3F, 0x11C3F], [0x11C92, 0x11CA7], [0x11CAA, 0x11CB0],
|
|
114
|
+
[0x11CB2, 0x11CB3], [0x11CB5, 0x11CB6], [0x11D31, 0x11D36], [0x11D3A, 0x11D3A],
|
|
115
|
+
[0x11D3C, 0x11D3D], [0x11D3F, 0x11D45], [0x11D47, 0x11D47], [0x11D90, 0x11D91],
|
|
116
|
+
[0x11D95, 0x11D95], [0x11D97, 0x11D97], [0x11EF3, 0x11EF4], [0x11F00, 0x11F01],
|
|
117
|
+
[0x11F36, 0x11F3A], [0x11F40, 0x11F40], [0x11F42, 0x11F42], [0x11F5A, 0x11F5A],
|
|
118
|
+
[0x13430, 0x13440], [0x13447, 0x13455], [0x1611E, 0x16129], [0x1612D, 0x1612F],
|
|
119
|
+
[0x16AF0, 0x16AF4], [0x16B30, 0x16B36], [0x16F4F, 0x16F4F], [0x16F8F, 0x16F92],
|
|
120
|
+
[0x16FE4, 0x16FE4], [0x1BC9D, 0x1BC9E], [0x1BCA0, 0x1BCA3], [0x1CF00, 0x1CF2D],
|
|
121
|
+
[0x1CF30, 0x1CF46], [0x1D167, 0x1D169], [0x1D173, 0x1D182], [0x1D185, 0x1D18B],
|
|
122
|
+
[0x1D1AA, 0x1D1AD], [0x1D242, 0x1D244], [0x1DA00, 0x1DA36], [0x1DA3B, 0x1DA6C],
|
|
123
|
+
[0x1DA75, 0x1DA75], [0x1DA84, 0x1DA84], [0x1DA9B, 0x1DA9F], [0x1DAA1, 0x1DAAF],
|
|
124
|
+
[0x1E000, 0x1E006], [0x1E008, 0x1E018], [0x1E01B, 0x1E021], [0x1E023, 0x1E024],
|
|
125
|
+
[0x1E026, 0x1E02A], [0x1E08F, 0x1E08F], [0x1E130, 0x1E136], [0x1E2AE, 0x1E2AE],
|
|
126
|
+
[0x1E2EC, 0x1E2EF], [0x1E4EC, 0x1E4EF], [0x1E5EE, 0x1E5EF], [0x1E6E3, 0x1E6E3],
|
|
127
|
+
[0x1E6E6, 0x1E6E6], [0x1E6EE, 0x1E6EF], [0x1E6F5, 0x1E6F5], [0x1E8D0, 0x1E8D6],
|
|
128
|
+
[0x1E944, 0x1E94A], [0xE0001, 0xE0001], [0xE0020, 0xE007F], [0xE0100, 0xE01EF],
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/** Code points that occupy two columns. */
|
|
132
|
+
const WIDE = [
|
|
133
|
+
[0x1100, 0x115F], [0x231A, 0x231B], [0x2329, 0x232A], [0x23E9, 0x23EC],
|
|
134
|
+
[0x23F0, 0x23F0], [0x23F3, 0x23F3], [0x25FD, 0x25FE], [0x2614, 0x2615],
|
|
135
|
+
[0x2648, 0x2653], [0x267F, 0x267F], [0x2693, 0x2693], [0x26A1, 0x26A1],
|
|
136
|
+
[0x26AA, 0x26AB], [0x26BD, 0x26BE], [0x26C4, 0x26C5], [0x26CE, 0x26CE],
|
|
137
|
+
[0x26D4, 0x26D4], [0x26EA, 0x26EA], [0x26F2, 0x26F3], [0x26F5, 0x26F5],
|
|
138
|
+
[0x26FA, 0x26FA], [0x26FD, 0x26FD], [0x2705, 0x2705], [0x270A, 0x270B],
|
|
139
|
+
[0x2728, 0x2728], [0x274C, 0x274C], [0x274E, 0x274E], [0x2753, 0x2755],
|
|
140
|
+
[0x2757, 0x2757], [0x2795, 0x2797], [0x27B0, 0x27B0], [0x27BF, 0x27BF],
|
|
141
|
+
[0x2B1B, 0x2B1C], [0x2B50, 0x2B50], [0x2B55, 0x2B55], [0x2E80, 0x2E99],
|
|
142
|
+
[0x2E9B, 0x2EF3], [0x2F00, 0x2FD5], [0x2FF0, 0x2FFB], [0x3000, 0x303E],
|
|
143
|
+
[0x3041, 0x3096], [0x3099, 0x30FF], [0x3105, 0x312F], [0x3131, 0x318E],
|
|
144
|
+
[0x3190, 0x31E3], [0x31F0, 0x321E], [0x3220, 0x3247], [0x3250, 0x4DBF],
|
|
145
|
+
[0x4E00, 0xA48C], [0xA490, 0xA4C6], [0xA960, 0xA97C], [0xAC00, 0xD7A3],
|
|
146
|
+
[0xF900, 0xFAFF], [0xFE10, 0xFE19], [0xFE30, 0xFE52], [0xFE54, 0xFE66],
|
|
147
|
+
[0xFE68, 0xFE6B], [0xFF01, 0xFF60], [0xFFE0, 0xFFE6], [0x16FE0, 0x16FE4],
|
|
148
|
+
[0x17000, 0x187F7], [0x18800, 0x18CD5], [0x1B000, 0x1B122], [0x1B150, 0x1B152],
|
|
149
|
+
[0x1B164, 0x1B167], [0x1B170, 0x1B2FB], [0x1F004, 0x1F004], [0x1F0CF, 0x1F0CF],
|
|
150
|
+
[0x1F18E, 0x1F18E], [0x1F191, 0x1F19A], [0x1F1E6, 0x1F1FF], [0x1F201, 0x1F201],
|
|
151
|
+
[0x1F21A, 0x1F21A], [0x1F22F, 0x1F22F], [0x1F232, 0x1F236], [0x1F238, 0x1F23A],
|
|
152
|
+
[0x1F250, 0x1F251], [0x1F300, 0x1F320], [0x1F32D, 0x1F335], [0x1F337, 0x1F37C],
|
|
153
|
+
[0x1F37E, 0x1F393], [0x1F3A0, 0x1F3CA], [0x1F3CF, 0x1F3D3], [0x1F3E0, 0x1F3F0],
|
|
154
|
+
[0x1F3F4, 0x1F3F4], [0x1F3F8, 0x1F43E], [0x1F440, 0x1F440], [0x1F442, 0x1F4FC],
|
|
155
|
+
[0x1F4FF, 0x1F53D], [0x1F54B, 0x1F54E], [0x1F550, 0x1F567], [0x1F57A, 0x1F57A],
|
|
156
|
+
[0x1F595, 0x1F596], [0x1F5A4, 0x1F5A4], [0x1F5FB, 0x1F64F], [0x1F680, 0x1F6C5],
|
|
157
|
+
[0x1F6CC, 0x1F6CC], [0x1F6D0, 0x1F6D2], [0x1F6D5, 0x1F6D8], [0x1F6DC, 0x1F6DF],
|
|
158
|
+
[0x1F6EB, 0x1F6EC], [0x1F6F4, 0x1F6FC], [0x1F7E0, 0x1F7EB], [0x1F7F0, 0x1F7F0],
|
|
159
|
+
[0x1F90C, 0x1F93A], [0x1F93C, 0x1F945], [0x1F947, 0x1F9FF], [0x1FA70, 0x1FA7C],
|
|
160
|
+
[0x1FA80, 0x1FA8A], [0x1FA8E, 0x1FAC6], [0x1FAC8, 0x1FAC8], [0x1FACD, 0x1FADC],
|
|
161
|
+
[0x1FADF, 0x1FAEA], [0x1FAEF, 0x1FAF8], [0x20000, 0x2FFFD], [0x30000, 0x3FFFD],
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
/** Binary search over sorted, non-overlapping, inclusive ranges. */
|
|
165
|
+
function inRanges(ranges, cp) {
|
|
166
|
+
let low = 0;
|
|
167
|
+
let high = ranges.length - 1;
|
|
168
|
+
while (low <= high) {
|
|
169
|
+
const mid = (low + high) >> 1;
|
|
170
|
+
if (cp < ranges[mid][0]) { high = mid - 1; continue; }
|
|
171
|
+
if (cp > ranges[mid][1]) { low = mid + 1; continue; }
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The width of one code point, in columns.
|
|
179
|
+
*
|
|
180
|
+
* @param {number} cp a Unicode code point
|
|
181
|
+
* @returns {0|1|2}
|
|
182
|
+
*/
|
|
183
|
+
export function codePointWidth(cp) {
|
|
184
|
+
if (inRanges(ZERO_WIDTH, cp)) return 0;
|
|
185
|
+
if (inRanges(WIDE, cp)) return 2;
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The width of a string, in columns.
|
|
191
|
+
*
|
|
192
|
+
* Iterates by code point — `for...of` over a string yields code points, not
|
|
193
|
+
* UTF-16 units — so an astral character is measured once rather than twice.
|
|
194
|
+
* No locale API, no segmenter, no normalization: each is either engine-version
|
|
195
|
+
* dependent or locale dependent, and both are forbidden here.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} text
|
|
198
|
+
* @returns {number} columns
|
|
199
|
+
*/
|
|
200
|
+
export function cellWidth(text) {
|
|
201
|
+
let width = 0;
|
|
202
|
+
for (const character of String(text)) width += codePointWidth(character.codePointAt(0));
|
|
203
|
+
return width;
|
|
204
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer registry: `kind` in, HTML out.
|
|
3
|
+
*
|
|
4
|
+
* Two entries and no placeholder for anything else. A registry with a stub in
|
|
5
|
+
* it invites a caller to render a kind that does not exist yet, and the result
|
|
6
|
+
* would be an artifact carrying the Pathfinder identity around content nothing
|
|
7
|
+
* checked. An unsupported kind is refused in the structural layer, before
|
|
8
|
+
* anything reaches here.
|
|
9
|
+
*
|
|
10
|
+
* Both entries render into the same shell and inherit the same identity. That
|
|
11
|
+
* is the whole claim of a shared renderer, and it is checkable: nothing below
|
|
12
|
+
* this line knows which kind it is looking at.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { renderLesson } from "./lesson.mjs";
|
|
16
|
+
import { renderDiagram } from "./diagram.mjs";
|
|
17
|
+
|
|
18
|
+
export const RENDERERS = Object.freeze({
|
|
19
|
+
lesson: renderLesson,
|
|
20
|
+
diagram: renderDiagram,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a validated specification.
|
|
25
|
+
*
|
|
26
|
+
* Deterministic by construction: the only input is the specification and this
|
|
27
|
+
* module's code. Nothing here reads a clock, an environment variable, a
|
|
28
|
+
* hostname, a working directory, or a locale, and no ordering comes from a
|
|
29
|
+
* directory listing or a hash table — every list is emitted in the order the
|
|
30
|
+
* specification wrote it.
|
|
31
|
+
*
|
|
32
|
+
* Rendering does not validate and does not pretend to. Called without an
|
|
33
|
+
* attestation — the ordinary case, and the only one available to an outside
|
|
34
|
+
* caller — the artifact carries its provenance rows and no claim to have been
|
|
35
|
+
* checked. Only `deliver()`, which validates first, can supply one.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} spec a specification whose `kind` this engine renders
|
|
38
|
+
* @param {object} [verification] an attestation minted from a passing
|
|
39
|
+
* validation. Cannot be forged: see `verification.mjs`.
|
|
40
|
+
* @returns {string} a complete, self-contained HTML document
|
|
41
|
+
*/
|
|
42
|
+
export function render(spec, verification) {
|
|
43
|
+
const renderer = RENDERERS[spec.kind];
|
|
44
|
+
if (!renderer) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`no renderer for kind ${JSON.stringify(spec.kind)}; this should have been ` +
|
|
47
|
+
`refused by the structural layer`);
|
|
48
|
+
}
|
|
49
|
+
return renderer(spec, verification);
|
|
50
|
+
}
|