@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.
- package/dist/html-entities.d.ts +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +72 -0
- package/package.json +1 -1
- package/src/__tests__/html-entities.test.ts +54 -0
- package/src/html-entities.ts +123 -0
- package/src/index.ts +1 -0
|
@@ -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 &
|
|
6
|
+
* Chips</span>` means the TEXT `Fish & Chips`, and `©` 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 `©` 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 (`©` / `©`) decode fully.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Decode HTML character references in JSX literal text / static
|
|
26
|
+
* attribute values: numeric decimal (`©`), numeric hex
|
|
27
|
+
* (`©`), 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 `'`, 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
@@ -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 & Chips')).toBe('Fish & Chips')
|
|
7
|
+
expect(decodeEntities('a < b > c')).toBe('a < b > c')
|
|
8
|
+
expect(decodeEntities('"x" 'y'')).toBe(`"x" 'y'`)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('common typographic names decode', () => {
|
|
12
|
+
expect(decodeEntities('© 2026')).toBe('© 2026')
|
|
13
|
+
expect(decodeEntities('1 000')).toBe('1 000')
|
|
14
|
+
expect(decodeEntities('A…')).toBe('A…')
|
|
15
|
+
expect(decodeEntities('€5 / £4 / ¥3')).toBe('€5 / £4 / ¥3')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('numeric decimal and hex references decode', () => {
|
|
19
|
+
expect(decodeEntities('©')).toBe('©')
|
|
20
|
+
expect(decodeEntities('©')).toBe('©')
|
|
21
|
+
expect(decodeEntities('😀')).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('&')).toBe('&')
|
|
28
|
+
// Lone surrogate / out-of-range code points are refused, not
|
|
29
|
+
// replaced with garbage.
|
|
30
|
+
expect(decodeEntities('�')).toBe('�')
|
|
31
|
+
expect(decodeEntities('�')).toBe('�')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('double-escaped input decodes exactly one level', () => {
|
|
35
|
+
expect(decodeEntities('&copy;')).toBe('©')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('escapeHtml', () => {
|
|
40
|
+
test('escapes & < > " and leaves the rest', () => {
|
|
41
|
+
expect(escapeHtml('Fish & Chips')).toBe('Fish & Chips')
|
|
42
|
+
expect(escapeHtml('a < b > c')).toBe('a < b > c')
|
|
43
|
+
expect(escapeHtml('say "hi"')).toBe('say "hi"')
|
|
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 & Chips'))).toBe('Fish & Chips')
|
|
49
|
+
expect(escapeHtml(decodeEntities('a < b'))).toBe('a < b')
|
|
50
|
+
// Named references outside the escape set stay decoded — the
|
|
51
|
+
// literal character is the canonical emission (`©`, not `©`).
|
|
52
|
+
expect(escapeHtml(decodeEntities('© 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 &
|
|
6
|
+
* Chips</span>` means the TEXT `Fish & Chips`, and `©` 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 `©` 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 (`©` / `©`) 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 (`©`), numeric hex
|
|
84
|
+
* (`©`), 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 `'`, 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, '&')
|
|
120
|
+
.replace(/</g, '<')
|
|
121
|
+
.replace(/>/g, '>')
|
|
122
|
+
.replace(/"/g, '"')
|
|
123
|
+
}
|