@barefootjs/shared 0.18.4 → 0.18.5

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * HTML character-reference decoding and escaping for STATIC template
3
+ * content.
4
+ *
5
+ * JSX decodes character references at parse time: `<span>Fish &amp;
6
+ * Chips</span>` means the TEXT `Fish & Chips`, and `&copy;` means `©`
7
+ * (Babel/esbuild/TypeScript's JSX emit all decode). Phase 1
8
+ * (`jsx-to-ir`) applies `decodeEntities` once so `IRText.value` and
9
+ * static attribute values carry the DECODED text — the semantics —
10
+ * and every adapter re-escapes for its own emission context
11
+ * (`escapeHtml` for HTML template output; the Hono adapter
12
+ * re-encodes for JSX source). An adapter that emitted the raw entity
13
+ * text passed `&copy;` through as bytes while the reference decoded it
14
+ * (the `html-entity-text` divergence), and one that skipped
15
+ * re-escaping emitted a parse-corrupting `<`.
16
+ *
17
+ * The named table is the curated set below, not the full HTML5 list
18
+ * (~2,200 names): an unknown name (`&foo;`) is left as raw text, so
19
+ * BOTH the reference adapter and the template adapters receive the
20
+ * same undecoded string from the IR and stay byte-identical — the
21
+ * degradation is consistent, exactly how a browser treats an unknown
22
+ * reference. Numeric references (`&#169;` / `&#xA9;`) decode fully.
23
+ */
24
+ /**
25
+ * Decode HTML character references in JSX literal text / static
26
+ * attribute values: numeric decimal (`&#169;`), numeric hex
27
+ * (`&#xA9;`), and the curated named set above. Anything unrecognized
28
+ * (unknown name, malformed numeric, bare `&`) is left verbatim.
29
+ */
30
+ export declare function decodeEntities(text: string): string;
31
+ /**
32
+ * Escape decoded static text for direct HTML emission: `&` `<` `>`
33
+ * `"` to their named forms. Used for BOTH text nodes and double-quoted
34
+ * attribute values — one set, no context-dependent under-escaping.
35
+ *
36
+ * `'` is deliberately NOT escaped: the reference (Hono JSX) escapes it
37
+ * as `&#39;`, but raw `'` is valid everywhere outside single-quoted
38
+ * attributes (which no adapter emits), and the conformance harness's
39
+ * `normalizeHTML` canonicalises the raw and entity forms to one
40
+ * spelling on both sides — so leaving apostrophes raw keeps every
41
+ * existing template byte-stable instead of rewriting all prose text.
42
+ */
43
+ export declare function escapeHtml(text: string): string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { BF_SCOPE, BF_SLOT, BF_HOST, BF_AT, BF_ROOT, BF_PROPS, BF_COND, BF_ITEM, BF_PORTAL_OWNER, BF_PORTAL_ID, BF_PORTAL_PLACEHOLDER, BF_PARENT_OWNED_PREFIX, BF_SCOPE_COMMENT_PREFIX, BF_LOOP_START, BF_LOOP_END, BF_LOOP_ITEM, loopItemMarker, loopStartMarker, loopEndMarker, BF_KEY, BF_KEY_PREFIX, BF_PLACEHOLDER, BF_ASYNC, BF_ASYNC_RESOLVE, BF_REGION, BF_PARENT_SCOPE_PLACEHOLDER, BF_SEAM_HYDRATE, BF_SEAM_HYDRATE_WITHIN, BF_SEAM_DISPOSE_WITHIN, BF_SEAM_PUSH_SEARCH, BF_SEAM_NAV_SEARCH, } from './markers.ts';
2
2
  export { classifyDOMProp, toHTMLAttrName, toHTMLAttrNameRuntime, isBooleanAttr, isEventProp, BOOLEAN_ATTRS, } from './dom-prop.ts';
3
3
  export type { DOMPropKind, DOMPropClassification } from './dom-prop.ts';
4
+ export { decodeEntities, escapeHtml } from './html-entities.ts';
4
5
  export type { ProfilerEvent, ProfilerEventType, ProfilerSubscriberKind, } from './profiler-events.ts';
package/dist/index.js CHANGED
@@ -264,6 +264,76 @@ function toHTMLAttrNameRuntime(key) {
264
264
  function isBooleanAttr(name) {
265
265
  return BOOLEAN_ATTRS.has(name.toLowerCase());
266
266
  }
267
+ // src/html-entities.ts
268
+ var NAMED_ENTITIES = {
269
+ amp: "&",
270
+ lt: "<",
271
+ gt: ">",
272
+ quot: '"',
273
+ apos: "'",
274
+ nbsp: " ",
275
+ copy: "©",
276
+ reg: "®",
277
+ trade: "™",
278
+ deg: "°",
279
+ plusmn: "±",
280
+ times: "×",
281
+ divide: "÷",
282
+ middot: "·",
283
+ bull: "•",
284
+ hellip: "…",
285
+ ndash: "–",
286
+ mdash: "—",
287
+ lsquo: "‘",
288
+ rsquo: "’",
289
+ ldquo: "“",
290
+ rdquo: "”",
291
+ laquo: "«",
292
+ raquo: "»",
293
+ sect: "§",
294
+ para: "¶",
295
+ dagger: "†",
296
+ Dagger: "‡",
297
+ euro: "€",
298
+ pound: "£",
299
+ yen: "¥",
300
+ cent: "¢",
301
+ sup1: "¹",
302
+ sup2: "²",
303
+ sup3: "³",
304
+ frac12: "½",
305
+ frac14: "¼",
306
+ frac34: "¾",
307
+ larr: "←",
308
+ uarr: "↑",
309
+ rarr: "→",
310
+ darr: "↓",
311
+ harr: "↔",
312
+ minus: "−",
313
+ infin: "∞",
314
+ ne: "≠",
315
+ le: "≤",
316
+ ge: "≥"
317
+ };
318
+ function decodeEntities(text) {
319
+ return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body) => {
320
+ if (body[0] === "#") {
321
+ const isHex = body[1] === "x" || body[1] === "X";
322
+ const digits = body.slice(isHex ? 2 : 1);
323
+ if (!isHex && !/^[0-9]+$/.test(digits))
324
+ return match;
325
+ const code = parseInt(digits, isHex ? 16 : 10);
326
+ if (!Number.isFinite(code) || code > 1114111 || code >= 55296 && code <= 57343) {
327
+ return match;
328
+ }
329
+ return String.fromCodePoint(code);
330
+ }
331
+ return NAMED_ENTITIES[body] ?? match;
332
+ });
333
+ }
334
+ function escapeHtml(text) {
335
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
336
+ }
267
337
  export {
268
338
  toHTMLAttrNameRuntime,
269
339
  toHTMLAttrName,
@@ -272,6 +342,8 @@ export {
272
342
  loopEndMarker,
273
343
  isEventProp,
274
344
  isBooleanAttr,
345
+ escapeHtml,
346
+ decodeEntities,
275
347
  classifyDOMProp,
276
348
  BOOLEAN_ATTRS,
277
349
  BF_SLOT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/shared",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Shared constants for BarefootJS compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { decodeEntities, escapeHtml } from '../html-entities'
3
+
4
+ describe('decodeEntities', () => {
5
+ test('escaping-set names decode', () => {
6
+ expect(decodeEntities('Fish &amp; Chips')).toBe('Fish & Chips')
7
+ expect(decodeEntities('a &lt; b &gt; c')).toBe('a < b > c')
8
+ expect(decodeEntities('&quot;x&quot; &apos;y&apos;')).toBe(`"x" 'y'`)
9
+ })
10
+
11
+ test('common typographic names decode', () => {
12
+ expect(decodeEntities('&copy; 2026')).toBe('© 2026')
13
+ expect(decodeEntities('1&nbsp;000')).toBe('1 000')
14
+ expect(decodeEntities('A&hellip;')).toBe('A…')
15
+ expect(decodeEntities('&euro;5 / &pound;4 / &yen;3')).toBe('€5 / £4 / ¥3')
16
+ })
17
+
18
+ test('numeric decimal and hex references decode', () => {
19
+ expect(decodeEntities('&#169;')).toBe('©')
20
+ expect(decodeEntities('&#xA9;')).toBe('©')
21
+ expect(decodeEntities('&#x1F600;')).toBe('😀')
22
+ })
23
+
24
+ test('unknown / malformed references stay verbatim', () => {
25
+ expect(decodeEntities('&unknownname;')).toBe('&unknownname;')
26
+ expect(decodeEntities('a & b')).toBe('a & b')
27
+ expect(decodeEntities('&amp')).toBe('&amp')
28
+ // Lone surrogate / out-of-range code points are refused, not
29
+ // replaced with garbage.
30
+ expect(decodeEntities('&#xD800;')).toBe('&#xD800;')
31
+ expect(decodeEntities('&#x110000;')).toBe('&#x110000;')
32
+ })
33
+
34
+ test('double-escaped input decodes exactly one level', () => {
35
+ expect(decodeEntities('&amp;copy;')).toBe('&copy;')
36
+ })
37
+ })
38
+
39
+ describe('escapeHtml', () => {
40
+ test('escapes & < > " and leaves the rest', () => {
41
+ expect(escapeHtml('Fish & Chips')).toBe('Fish &amp; Chips')
42
+ expect(escapeHtml('a < b > c')).toBe('a &lt; b &gt; c')
43
+ expect(escapeHtml('say "hi"')).toBe('say &quot;hi&quot;')
44
+ expect(escapeHtml("it's © fine")).toBe("it's © fine")
45
+ })
46
+
47
+ test('decode → escape round-trips the entity text', () => {
48
+ expect(escapeHtml(decodeEntities('Fish &amp; Chips'))).toBe('Fish &amp; Chips')
49
+ expect(escapeHtml(decodeEntities('a &lt; b'))).toBe('a &lt; b')
50
+ // Named references outside the escape set stay decoded — the
51
+ // literal character is the canonical emission (`©`, not `&copy;`).
52
+ expect(escapeHtml(decodeEntities('&copy; 2026'))).toBe('© 2026')
53
+ })
54
+ })
@@ -0,0 +1,123 @@
1
+ /**
2
+ * HTML character-reference decoding and escaping for STATIC template
3
+ * content.
4
+ *
5
+ * JSX decodes character references at parse time: `<span>Fish &amp;
6
+ * Chips</span>` means the TEXT `Fish & Chips`, and `&copy;` means `©`
7
+ * (Babel/esbuild/TypeScript's JSX emit all decode). Phase 1
8
+ * (`jsx-to-ir`) applies `decodeEntities` once so `IRText.value` and
9
+ * static attribute values carry the DECODED text — the semantics —
10
+ * and every adapter re-escapes for its own emission context
11
+ * (`escapeHtml` for HTML template output; the Hono adapter
12
+ * re-encodes for JSX source). An adapter that emitted the raw entity
13
+ * text passed `&copy;` through as bytes while the reference decoded it
14
+ * (the `html-entity-text` divergence), and one that skipped
15
+ * re-escaping emitted a parse-corrupting `<`.
16
+ *
17
+ * The named table is the curated set below, not the full HTML5 list
18
+ * (~2,200 names): an unknown name (`&foo;`) is left as raw text, so
19
+ * BOTH the reference adapter and the template adapters receive the
20
+ * same undecoded string from the IR and stay byte-identical — the
21
+ * degradation is consistent, exactly how a browser treats an unknown
22
+ * reference. Numeric references (`&#169;` / `&#xA9;`) decode fully.
23
+ */
24
+
25
+ /**
26
+ * Named character references JSX authors actually write in literal
27
+ * text. `amp`/`lt`/`gt`/`quot`/`apos` are the escaping set itself;
28
+ * the rest are the common typographic/symbol names.
29
+ */
30
+ const NAMED_ENTITIES: Record<string, string> = {
31
+ amp: '&',
32
+ lt: '<',
33
+ gt: '>',
34
+ quot: '"',
35
+ apos: "'",
36
+ nbsp: ' ',
37
+ copy: '©',
38
+ reg: '®',
39
+ trade: '™',
40
+ deg: '°',
41
+ plusmn: '±',
42
+ times: '×',
43
+ divide: '÷',
44
+ middot: '·',
45
+ bull: '•',
46
+ hellip: '…',
47
+ ndash: '–',
48
+ mdash: '—',
49
+ lsquo: '‘',
50
+ rsquo: '’',
51
+ ldquo: '“',
52
+ rdquo: '”',
53
+ laquo: '«',
54
+ raquo: '»',
55
+ sect: '§',
56
+ para: '¶',
57
+ dagger: '†',
58
+ Dagger: '‡',
59
+ euro: '€',
60
+ pound: '£',
61
+ yen: '¥',
62
+ cent: '¢',
63
+ sup1: '¹',
64
+ sup2: '²',
65
+ sup3: '³',
66
+ frac12: '½',
67
+ frac14: '¼',
68
+ frac34: '¾',
69
+ larr: '←',
70
+ uarr: '↑',
71
+ rarr: '→',
72
+ darr: '↓',
73
+ harr: '↔',
74
+ minus: '−',
75
+ infin: '∞',
76
+ ne: '≠',
77
+ le: '≤',
78
+ ge: '≥',
79
+ }
80
+
81
+ /**
82
+ * Decode HTML character references in JSX literal text / static
83
+ * attribute values: numeric decimal (`&#169;`), numeric hex
84
+ * (`&#xA9;`), and the curated named set above. Anything unrecognized
85
+ * (unknown name, malformed numeric, bare `&`) is left verbatim.
86
+ */
87
+ export function decodeEntities(text: string): string {
88
+ return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body: string) => {
89
+ if (body[0] === '#') {
90
+ const isHex = body[1] === 'x' || body[1] === 'X'
91
+ const digits = body.slice(isHex ? 2 : 1)
92
+ if (!isHex && !/^[0-9]+$/.test(digits)) return match
93
+ const code = parseInt(digits, isHex ? 16 : 10)
94
+ // Reject out-of-range / lone-surrogate code points rather than
95
+ // producing replacement garbage — leave the reference raw.
96
+ if (!Number.isFinite(code) || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
97
+ return match
98
+ }
99
+ return String.fromCodePoint(code)
100
+ }
101
+ return NAMED_ENTITIES[body] ?? match
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Escape decoded static text for direct HTML emission: `&` `<` `>`
107
+ * `"` to their named forms. Used for BOTH text nodes and double-quoted
108
+ * attribute values — one set, no context-dependent under-escaping.
109
+ *
110
+ * `'` is deliberately NOT escaped: the reference (Hono JSX) escapes it
111
+ * as `&#39;`, but raw `'` is valid everywhere outside single-quoted
112
+ * attributes (which no adapter emits), and the conformance harness's
113
+ * `normalizeHTML` canonicalises the raw and entity forms to one
114
+ * spelling on both sides — so leaving apostrophes raw keeps every
115
+ * existing template byte-stable instead of rewriting all prose text.
116
+ */
117
+ export function escapeHtml(text: string): string {
118
+ return text
119
+ .replace(/&/g, '&amp;')
120
+ .replace(/</g, '&lt;')
121
+ .replace(/>/g, '&gt;')
122
+ .replace(/"/g, '&quot;')
123
+ }
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ export {
41
41
  BOOLEAN_ATTRS,
42
42
  } from './dom-prop.ts'
43
43
  export type { DOMPropKind, DOMPropClassification } from './dom-prop.ts'
44
+ export { decodeEntities, escapeHtml } from './html-entities.ts'
44
45
 
45
46
  export type {
46
47
  ProfilerEvent,