@pfern/elements 0.1.11 → 0.2.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/src/mathml.js ADDED
@@ -0,0 +1,340 @@
1
+ /**
2
+ * MathML tag helpers (curated).
3
+ *
4
+ * This module is intentionally small and focused on the subset that’s useful
5
+ * for building expression visualizers and teaching tools (e.g. Content MathML
6
+ * trees with `<apply>`, plus a handful of Presentation MathML helpers).
7
+ *
8
+ * The Elements.js runtime supports rendering *any* MathML tag name as a vnode
9
+ * (e.g. `['apply', {}, ...]`). These helpers exist for ergonomics and docs.
10
+ */
11
+
12
+ const isPropsObject = x =>
13
+ typeof x === 'object'
14
+ && x !== null
15
+ && !Array.isArray(x)
16
+ && !(typeof Node !== 'undefined' && x instanceof Node)
17
+
18
+ /**
19
+ * @param {string} tag
20
+ * @returns {import('./core/types.js').ElementsElementHelper<any>}
21
+ */
22
+ const createTagHelper = tag => (...args) => {
23
+ const hasFirstArg = args.length > 0
24
+ const [propsOrChild, ...children] = args
25
+ const props = hasFirstArg && isPropsObject(propsOrChild) ? propsOrChild : {}
26
+ const actualChildren = !hasFirstArg
27
+ ? []
28
+ : props === propsOrChild
29
+ ? children
30
+ : [propsOrChild, ...children]
31
+ return /** @type {import('./core/types.js').ElementsVNode} */ (
32
+ [tag, props, ...actualChildren]
33
+ )
34
+ }
35
+
36
+ /**
37
+ * <math>
38
+ * The root element of a MathML expression.
39
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/math
40
+ */
41
+ export const math = createTagHelper('math')
42
+
43
+ /**
44
+ * <mrow>
45
+ * Groups sub-expressions without introducing visible separators.
46
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mrow
47
+ */
48
+ export const mrow = createTagHelper('mrow')
49
+
50
+ /**
51
+ * <mi>
52
+ * Identifier (variable/function name).
53
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mi
54
+ */
55
+ export const mi = createTagHelper('mi')
56
+
57
+ /**
58
+ * <mo>
59
+ * Operator (e.g. "+", "→", parentheses).
60
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo
61
+ */
62
+ export const mo = createTagHelper('mo')
63
+
64
+ /**
65
+ * <mn>
66
+ * Number literal.
67
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mn
68
+ */
69
+ export const mn = createTagHelper('mn')
70
+
71
+ /**
72
+ * <mtext>
73
+ * Text inside math layout.
74
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtext
75
+ */
76
+ export const mtext = createTagHelper('mtext')
77
+
78
+ /**
79
+ * <mspace>
80
+ * Spacing control.
81
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mspace
82
+ */
83
+ export const mspace = createTagHelper('mspace')
84
+
85
+ /**
86
+ * <mstyle>
87
+ * Styling wrapper for MathML subtree.
88
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mstyle
89
+ */
90
+ export const mstyle = createTagHelper('mstyle')
91
+
92
+ /**
93
+ * <menclose>
94
+ * Encloses content (e.g. boxes, circles).
95
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/menclose
96
+ */
97
+ export const menclose = createTagHelper('menclose')
98
+
99
+ /**
100
+ * <mfenced>
101
+ * Convenience fencing wrapper (parentheses, brackets).
102
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfenced
103
+ */
104
+ export const mfenced = createTagHelper('mfenced')
105
+
106
+ /**
107
+ * <msup>
108
+ * Superscript.
109
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msup
110
+ */
111
+ export const msup = createTagHelper('msup')
112
+
113
+ /**
114
+ * <msub>
115
+ * Subscript.
116
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msub
117
+ */
118
+ export const msub = createTagHelper('msub')
119
+
120
+ /**
121
+ * <msubsup>
122
+ * Subscript + superscript.
123
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msubsup
124
+ */
125
+ export const msubsup = createTagHelper('msubsup')
126
+
127
+ /**
128
+ * <mfrac>
129
+ * Fraction.
130
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mfrac
131
+ */
132
+ export const mfrac = createTagHelper('mfrac')
133
+
134
+ /**
135
+ * <msqrt>
136
+ * Square root.
137
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/msqrt
138
+ */
139
+ export const msqrt = createTagHelper('msqrt')
140
+
141
+ /**
142
+ * <mroot>
143
+ * Root with explicit index.
144
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mroot
145
+ */
146
+ export const mroot = createTagHelper('mroot')
147
+
148
+ /**
149
+ * <munder>
150
+ * Under-script.
151
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/munder
152
+ */
153
+ export const munder = createTagHelper('munder')
154
+
155
+ /**
156
+ * <mover>
157
+ * Over-script.
158
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mover
159
+ */
160
+ export const mover = createTagHelper('mover')
161
+
162
+ /**
163
+ * <munderover>
164
+ * Under + over script.
165
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/munderover
166
+ */
167
+ export const munderover = createTagHelper('munderover')
168
+
169
+ /**
170
+ * <mtable>
171
+ * Table layout.
172
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtable
173
+ */
174
+ export const mtable = createTagHelper('mtable')
175
+
176
+ /**
177
+ * <mtr>
178
+ * Table row.
179
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtr
180
+ */
181
+ export const mtr = createTagHelper('mtr')
182
+
183
+ /**
184
+ * <mtd>
185
+ * Table cell.
186
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mtd
187
+ */
188
+ export const mtd = createTagHelper('mtd')
189
+
190
+ /**
191
+ * <semantics>
192
+ * Attach semantic annotations to a presentation tree.
193
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/semantics
194
+ */
195
+ export const semantics = createTagHelper('semantics')
196
+
197
+ /**
198
+ * <annotation>
199
+ * A textual annotation inside <semantics>.
200
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/annotation
201
+ */
202
+ export const annotation = createTagHelper('annotation')
203
+
204
+ /**
205
+ * <annotation-xml>
206
+ * An XML annotation inside <semantics>.
207
+ *
208
+ * Exported as `annotationXml` because of the dash in the tag name.
209
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/annotation-xml
210
+ */
211
+ export const annotationXml = createTagHelper('annotation-xml')
212
+
213
+ /**
214
+ * <apply>
215
+ * Content MathML application node: first child is the operator, remaining
216
+ * children are arguments.
217
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/apply
218
+ */
219
+ export const apply = createTagHelper('apply')
220
+
221
+ /**
222
+ * <ci>
223
+ * Content MathML identifier.
224
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/ci
225
+ */
226
+ export const ci = createTagHelper('ci')
227
+
228
+ /**
229
+ * <cn>
230
+ * Content MathML number.
231
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/cn
232
+ */
233
+ export const cn = createTagHelper('cn')
234
+
235
+ /**
236
+ * <csymbol>
237
+ * Content MathML symbol (often with a content dictionary via `cd`).
238
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/csymbol
239
+ */
240
+ export const csymbol = createTagHelper('csymbol')
241
+
242
+ /**
243
+ * <bind>
244
+ * Content MathML binding node (e.g. lambda).
245
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/bind
246
+ */
247
+ export const bind = createTagHelper('bind')
248
+
249
+ /**
250
+ * <bvar>
251
+ * Bound variable (used under <bind>).
252
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/bvar
253
+ */
254
+ export const bvar = createTagHelper('bvar')
255
+
256
+ /**
257
+ * <lambda>
258
+ * Lambda binder operator (typically inside <bind>).
259
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/lambda
260
+ */
261
+ export const lambda = createTagHelper('lambda')
262
+
263
+ /**
264
+ * <set>
265
+ * Content MathML set constructor (often under <apply>).
266
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/set
267
+ */
268
+ export const set = createTagHelper('set')
269
+
270
+ /**
271
+ * <list>
272
+ * Content MathML list constructor (often under <apply>).
273
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/list
274
+ */
275
+ export const list = createTagHelper('list')
276
+
277
+ /**
278
+ * <vector>
279
+ * Content MathML vector constructor.
280
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/vector
281
+ */
282
+ export const vector = createTagHelper('vector')
283
+
284
+ /**
285
+ * <matrix>
286
+ * Content MathML matrix constructor.
287
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/matrix
288
+ */
289
+ export const matrix = createTagHelper('matrix')
290
+
291
+ /**
292
+ * <matrixrow>
293
+ * Content MathML matrix row.
294
+ * https://developer.mozilla.org/en-US/docs/Web/MathML/Element/matrixrow
295
+ */
296
+ export const matrixrow = createTagHelper('matrixrow')
297
+
298
+ /**
299
+ * A map of curated MathML helpers.
300
+ */
301
+ export const mathml = /** @type {const} */ ({
302
+ math,
303
+ mrow,
304
+ mi,
305
+ mo,
306
+ mn,
307
+ mtext,
308
+ mspace,
309
+ mstyle,
310
+ menclose,
311
+ mfenced,
312
+ msup,
313
+ msub,
314
+ msubsup,
315
+ mfrac,
316
+ msqrt,
317
+ mroot,
318
+ munder,
319
+ mover,
320
+ munderover,
321
+ mtable,
322
+ mtr,
323
+ mtd,
324
+ semantics,
325
+ annotation,
326
+ annotationXml,
327
+ apply,
328
+ ci,
329
+ cn,
330
+ csymbol,
331
+ bind,
332
+ bvar,
333
+ lambda,
334
+ set,
335
+ list,
336
+ vector,
337
+ matrix,
338
+ matrixrow
339
+ })
340
+
package/src/router.js ADDED
@@ -0,0 +1,51 @@
1
+ // Router utilities.
2
+ //
3
+ // `onNavigate(fn)` registers a callback that should re-render your app when the
4
+ // URL changes (via History API navigation or back/forward).
5
+ //
6
+ // The callback is invoked on a microtask after `popstate` so it can safely run
7
+ // outside of any current declarative event update.
8
+
9
+ let current = null
10
+ let installed = false
11
+ let scheduled = false
12
+
13
+ const defer = fn =>
14
+ typeof window.queueMicrotask === 'function'
15
+ ? window.queueMicrotask(fn)
16
+ : Promise.resolve().then(fn)
17
+
18
+ const schedule = () => {
19
+ if (typeof current !== 'function') return
20
+ if (scheduled) return
21
+ scheduled = true
22
+ defer(() => {
23
+ scheduled = false
24
+ typeof current === 'function' && current()
25
+ })
26
+ }
27
+
28
+ const onPopState = () => schedule()
29
+
30
+ export const hasNavigateHandler = () => typeof current === 'function'
31
+
32
+ export const onNavigate = (fn, { immediate = false } = {}) => {
33
+ current = fn
34
+
35
+ if (typeof window !== 'undefined' && !installed) {
36
+ window.addEventListener('popstate', onPopState)
37
+ installed = true
38
+ }
39
+
40
+ immediate && schedule()
41
+
42
+ return () => {
43
+ if (current !== fn) return
44
+ current = null
45
+
46
+ if (typeof window !== 'undefined' && installed) {
47
+ window.removeEventListener('popstate', onPopState)
48
+ installed = false
49
+ }
50
+ }
51
+ }
package/src/ssr.js ADDED
@@ -0,0 +1,175 @@
1
+ /** @fileoverview Server-side rendering / static site generation helpers.
2
+ *
3
+ * Elements.js treats UI as a pure function tree that produces declarative
4
+ * “vnode” arrays:
5
+ *
6
+ * ['div', { id: 'x' }, 'Hello']
7
+ *
8
+ * In the browser, `render()` evaluates this AST into real DOM nodes. For
9
+ * build-time prerendering (SSG) or server-side rendering (SSR),
10
+ * `toHtmlString()` walks the same AST and prints HTML.
11
+ *
12
+ * Design notes:
13
+ * - This is intentionally “dumb printing”, not hydration. Event handlers and
14
+ * other imperative props (functions) are dropped.
15
+ * - `innerHTML` is treated as an explicit escape hatch: it is inserted verbatim
16
+ * and children are ignored.
17
+ * - Boolean attribute rules mirror DOM assignment in `core/props.js`: -
18
+ * `aria-*` / `data-*` booleans become `"true"` / `"false"`. - other booleans
19
+ * are present when true, omitted when false.
20
+ *
21
+ * The long-term goal is that vnode arrays act as a small, Lisp-like
22
+ * cons-structure / AST: they can be evaluated into DOM, or serialized back into
23
+ * markup, or translated into other equivalent encodings.
24
+ */
25
+
26
+ const voidTags = new Set([
27
+ 'area',
28
+ 'base',
29
+ 'br',
30
+ 'col',
31
+ 'embed',
32
+ 'hr',
33
+ 'img',
34
+ 'input',
35
+ 'link',
36
+ 'meta',
37
+ 'param',
38
+ 'source',
39
+ 'track',
40
+ 'wbr'
41
+ ])
42
+
43
+ const isObject = x =>
44
+ typeof x === 'object'
45
+ && x !== null
46
+ && !Array.isArray(x)
47
+
48
+ const escapeText = s =>
49
+ String(s)
50
+ .replaceAll('&', '&amp;')
51
+ .replaceAll('<', '&lt;')
52
+ .replaceAll('>', '&gt;')
53
+
54
+ const escapeAttr = s =>
55
+ String(s)
56
+ .replaceAll('&', '&amp;')
57
+ .replaceAll('<', '&lt;')
58
+ .replaceAll('>', '&gt;')
59
+ .replaceAll('"', '&quot;')
60
+ .replaceAll('\'', '&#39;')
61
+
62
+ const toKebab = s =>
63
+ String(s)
64
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
65
+ .replaceAll('_', '-')
66
+ .toLowerCase()
67
+
68
+ const styleToString = style => {
69
+ if (!isObject(style)) return ''
70
+ const keys = Object.keys(style)
71
+ if (keys.length === 0) return ''
72
+
73
+ return keys
74
+ .filter(k => style[k] != null && style[k] !== false)
75
+ .map(k => `${k.startsWith('--') ? k : toKebab(k)}:${String(style[k])}`)
76
+ .join(';')
77
+ }
78
+
79
+ const shouldDropProp = (key, value) => {
80
+ if (key == null) return true
81
+ if (key === 'innerHTML') return true
82
+ if (String(key).startsWith('__')) return true
83
+ if (typeof value === 'function') return true
84
+ if (key === 'ontick') return true
85
+ return false
86
+ }
87
+
88
+ const attrsToString = props => {
89
+ if (!isObject(props)) return ''
90
+
91
+ const normalized = props.class == null && props.className != null
92
+ ? { ...props, class: props.className }
93
+ : props
94
+
95
+ const keys = Object.keys(normalized)
96
+ .filter(k => !shouldDropProp(k, normalized[k]))
97
+ .sort()
98
+
99
+ let out = ''
100
+ for (const key of keys) {
101
+ const value = normalized[key]
102
+ if (value == null || value === false) {
103
+ // Preserve `aria-*` / `data-*` boolean semantics for false.
104
+ if (typeof value === 'boolean'
105
+ && (key.startsWith('aria-') || key.startsWith('data-')))
106
+ out += ` ${key}="false"`
107
+ continue
108
+ }
109
+
110
+ if (key === 'style' && isObject(value)) {
111
+ const cssText = styleToString(value)
112
+ cssText && (out += ` style="${escapeAttr(cssText)}"`)
113
+ continue
114
+ }
115
+
116
+ if (typeof value === 'boolean') {
117
+ if (key.startsWith('aria-') || key.startsWith('data-')) {
118
+ out += ` ${key}="${value ? 'true' : 'false'}"`
119
+ } else if (value) {
120
+ out += ` ${key}`
121
+ }
122
+ continue
123
+ }
124
+
125
+ out += ` ${key}="${escapeAttr(value)}"`
126
+ }
127
+ return out
128
+ }
129
+
130
+ const toHtmlStringInner = vnode => {
131
+ const type = typeof vnode
132
+ if (vnode == null || vnode === false) return ''
133
+ if (type === 'string' || type === 'number') return escapeText(vnode)
134
+
135
+ if (!Array.isArray(vnode) || vnode.length === 0) return ''
136
+
137
+ const tag = vnode[0]
138
+
139
+ // Explicit fragments render children without a wrapper tag.
140
+ if (tag === 'fragment') {
141
+ let out = ''
142
+ for (let i = 2; i < vnode.length; i++) out += toHtmlStringInner(vnode[i])
143
+ return out
144
+ }
145
+
146
+ if (typeof tag !== 'string') return ''
147
+
148
+ /** @type {Record<string, any>} */
149
+ const props = isObject(vnode[1]) ? vnode[1] : {}
150
+ const attrs = attrsToString(props)
151
+
152
+ if (voidTags.has(tag)) return `<${tag}${attrs}>`
153
+
154
+ const hasInnerHtml = 'innerHTML' in props
155
+ const inner = hasInnerHtml
156
+ ? String(props.innerHTML ?? '')
157
+ : (() => {
158
+ let out = ''
159
+ for (let i = 2; i < vnode.length; i++) out += toHtmlStringInner(vnode[i])
160
+ return out
161
+ })()
162
+
163
+ return `<${tag}${attrs}>${inner}</${tag}>`
164
+ }
165
+
166
+ /**
167
+ * Serialize a vnode tree to an HTML string (SSR/SSG).
168
+ *
169
+ * @param {import('./core/types.js').ElementsVNode | string | number | null | undefined | false} vnode
170
+ * @param {{ doctype?: boolean }} [options]
171
+ * @returns {string}
172
+ */
173
+ export const toHtmlString = (vnode, { doctype = false } = {}) =>
174
+ `${doctype ? '<!doctype html>' : ''}${toHtmlStringInner(vnode)}`
175
+