tungsten 0.2.24 → 0.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/dist/ContentNode.d.ts +7 -0
- package/dist/ContentNode.d.ts.map +1 -1
- package/dist/Instance/Instance.d.ts.map +1 -1
- package/dist/Instance/Instance.js +7 -5
- package/dist/Instance/gotoRedirect.sxyt.d.ts +2 -0
- package/dist/Instance/gotoRedirect.sxyt.d.ts.map +1 -0
- package/dist/Instance/gotoRedirect.sxyt.js +148 -0
- package/dist/Servers/DevServer.d.ts +7 -0
- package/dist/Servers/DevServer.d.ts.map +1 -1
- package/dist/Servers/DevServer.js +38 -1
- package/dist/Servers/routing.d.ts.map +1 -1
- package/dist/Servers/routing.js +10 -1
- package/dist/build/compileJavascript.d.ts.map +1 -1
- package/dist/build/compileJavascript.js +86 -1
- package/dist/build/compileStyles.d.ts +1 -1
- package/dist/build/compileStyles.d.ts.map +1 -1
- package/dist/build/compileStyles.js +10 -4
- package/dist/build/compileUserApp.d.ts.map +1 -1
- package/dist/build/compileUserApp.js +7 -2
- package/dist/components/RawHtml.d.ts +36 -0
- package/dist/components/RawHtml.d.ts.map +1 -0
- package/dist/components/RawHtml.js +30 -0
- package/dist/components/RawHtml.sxyt.d.ts +2 -0
- package/dist/components/RawHtml.sxyt.d.ts.map +1 -0
- package/dist/components/RawHtml.sxyt.js +104 -0
- package/dist/dev/liveReloadHub.d.ts +6 -0
- package/dist/dev/liveReloadHub.d.ts.map +1 -0
- package/dist/dev/liveReloadHub.js +12 -0
- package/dist/helpers/getFetchCompatibleHeaders.d.ts.map +1 -1
- package/dist/hydrate/hydrate.d.ts.map +1 -1
- package/dist/hydrate/hydrate.js +11 -4
- package/dist/hydrate/reconcileHydratedAttributes.d.ts +24 -0
- package/dist/hydrate/reconcileHydratedAttributes.d.ts.map +1 -0
- package/dist/hydrate/reconcileHydratedAttributes.js +73 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.d.ts +2 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.d.ts.map +1 -0
- package/dist/hydrate/reconcileHydratedAttributes.sxyt.js +173 -0
- package/dist/indexInner.d.ts +4 -0
- package/dist/indexInner.d.ts.map +1 -1
- package/dist/indexInner.js +3 -0
- package/dist/innerHtmlFromProps.d.ts +16 -0
- package/dist/innerHtmlFromProps.d.ts.map +1 -0
- package/dist/innerHtmlFromProps.js +31 -0
- package/dist/innerHtmlFromProps.sxyt.d.ts +2 -0
- package/dist/innerHtmlFromProps.sxyt.d.ts.map +1 -0
- package/dist/innerHtmlFromProps.sxyt.js +99 -0
- package/dist/postcssPlugins/TransformCssNames.d.ts +14 -1
- package/dist/postcssPlugins/TransformCssNames.d.ts.map +1 -1
- package/dist/postcssPlugins/TransformCssNames.js +97 -95
- package/dist/rawHtml/HtmlAllowList.d.ts +17 -0
- package/dist/rawHtml/HtmlAllowList.d.ts.map +1 -0
- package/dist/rawHtml/HtmlAllowList.js +1 -0
- package/dist/rawHtml/decodeHtmlEntities.d.ts +8 -0
- package/dist/rawHtml/decodeHtmlEntities.d.ts.map +1 -0
- package/dist/rawHtml/decodeHtmlEntities.js +105 -0
- package/dist/rawHtml/htmlParsingTags.d.ts +23 -0
- package/dist/rawHtml/htmlParsingTags.d.ts.map +1 -0
- package/dist/rawHtml/htmlParsingTags.js +55 -0
- package/dist/rawHtml/parseHtml.d.ts +35 -0
- package/dist/rawHtml/parseHtml.d.ts.map +1 -0
- package/dist/rawHtml/parseHtml.js +214 -0
- package/dist/rawHtml/parseHtml.sxyt.d.ts +2 -0
- package/dist/rawHtml/parseHtml.sxyt.d.ts.map +1 -0
- package/dist/rawHtml/parseHtml.sxyt.js +239 -0
- package/dist/rawHtml/safeHtmlAllowList.d.ts +18 -0
- package/dist/rawHtml/safeHtmlAllowList.d.ts.map +1 -0
- package/dist/rawHtml/safeHtmlAllowList.js +47 -0
- package/dist/rawHtml/tokenizeHtml.d.ts +25 -0
- package/dist/rawHtml/tokenizeHtml.d.ts.map +1 -0
- package/dist/rawHtml/tokenizeHtml.js +148 -0
- package/dist/render/processHead.d.ts.map +1 -1
- package/dist/render/processHead.js +10 -0
- package/dist/render/processProps.d.ts.map +1 -1
- package/dist/render/processProps.js +14 -2
- package/dist/render/processProps.sxyt.d.ts +2 -0
- package/dist/render/processProps.sxyt.d.ts.map +1 -0
- package/dist/render/processProps.sxyt.js +90 -0
- package/dist/render/renderingTreeToHtml.d.ts.map +1 -1
- package/dist/render/renderingTreeToHtml.js +8 -0
- package/dist/routing/RouteTypes.d.ts +1 -1
- package/dist/routing/RouteTypes.d.ts.map +1 -1
- package/dist/routing/routeSymbols.d.ts +9 -0
- package/dist/routing/routeSymbols.d.ts.map +1 -0
- package/dist/routing/routeSymbols.js +4 -0
- package/dist/update/integrateUpdateVdom.d.ts.map +1 -1
- package/dist/update/integrateUpdateVdom.js +28 -7
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe } from 'sxy-test-runner';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { innerHtmlFromProps } from "./innerHtmlFromProps.js";
|
|
4
|
+
import { renderingNodeToHtml } from "./render/renderingTreeToHtml.js";
|
|
5
|
+
import { $props, $type } from "./contentNodeSymbols.js";
|
|
6
|
+
const props = value => value;
|
|
7
|
+
// `found: true` short-circuits the <head> handling, which is all these
|
|
8
|
+
// element-level tests need from HeadData.
|
|
9
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
10
|
+
const noHeadData = {
|
|
11
|
+
found: true
|
|
12
|
+
};
|
|
13
|
+
const renderHtml = (type, nodeProps) => renderingNodeToHtml(
|
|
14
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
15
|
+
{
|
|
16
|
+
[$type]: type,
|
|
17
|
+
[$props]: nodeProps
|
|
18
|
+
}, false, noHeadData, null);
|
|
19
|
+
describe('innerHtmlFromProps()', ({
|
|
20
|
+
it
|
|
21
|
+
}) => {
|
|
22
|
+
it('returns null when the element does not use dangerouslySetInnerHTML', () => {
|
|
23
|
+
expect(innerHtmlFromProps('div', props({
|
|
24
|
+
children: 'hello'
|
|
25
|
+
}), true)).to.equal(null);
|
|
26
|
+
});
|
|
27
|
+
it('returns the raw markup', () => {
|
|
28
|
+
expect(innerHtmlFromProps('div', props({
|
|
29
|
+
dangerouslySetInnerHTML: {
|
|
30
|
+
__html: '<b>hi</b>'
|
|
31
|
+
}
|
|
32
|
+
}), false)).to.equal('<b>hi</b>');
|
|
33
|
+
});
|
|
34
|
+
it('rejects an element carrying both raw markup and children', () => {
|
|
35
|
+
expect(() => innerHtmlFromProps('div', props({
|
|
36
|
+
dangerouslySetInnerHTML: {
|
|
37
|
+
__html: '<b>hi</b>'
|
|
38
|
+
},
|
|
39
|
+
children: 'hello'
|
|
40
|
+
}), true)).to.throw(/both dangerouslySetInnerHTML and children/);
|
|
41
|
+
});
|
|
42
|
+
it('rejects a prop that is not of the form { __html }', () => {
|
|
43
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
44
|
+
const bad = {
|
|
45
|
+
dangerouslySetInnerHTML: '<b>hi</b>'
|
|
46
|
+
};
|
|
47
|
+
expect(() => innerHtmlFromProps('div', bad, false)).to.throw(/{ __html: '...' }/);
|
|
48
|
+
});
|
|
49
|
+
it('rejects a null or undefined __html', () => {
|
|
50
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
51
|
+
const nulled = {
|
|
52
|
+
dangerouslySetInnerHTML: {
|
|
53
|
+
__html: null
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
expect(() => innerHtmlFromProps('div', nulled, false)).to.throw(/__html is null/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('dangerouslySetInnerHTML rendering', ({
|
|
60
|
+
it
|
|
61
|
+
}) => {
|
|
62
|
+
it('writes the markup into the element unescaped', async () => {
|
|
63
|
+
expect(await renderHtml('div', {
|
|
64
|
+
dangerouslySetInnerHTML: {
|
|
65
|
+
__html: '<p>Bonuses.</p>'
|
|
66
|
+
}
|
|
67
|
+
})).to.equal('<div><p>Bonuses.</p></div>');
|
|
68
|
+
});
|
|
69
|
+
it('does not leak the prop into the markup as an attribute', async () => {
|
|
70
|
+
const html = await renderHtml('div', {
|
|
71
|
+
id: 'tip',
|
|
72
|
+
dangerouslySetInnerHTML: {
|
|
73
|
+
__html: '<b>hi</b>'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
expect(html).to.not.include('dangerouslySetInnerHTML');
|
|
77
|
+
expect(html).to.not.include('[object Object]');
|
|
78
|
+
expect(html).to.equal('<div id="tip"><b>hi</b></div>');
|
|
79
|
+
});
|
|
80
|
+
it('escapes ordinary children, so the switch is per element', async () => {
|
|
81
|
+
expect(await renderHtml('div', {
|
|
82
|
+
children: '<p>Bonuses.</p>'
|
|
83
|
+
})).to.equal('<div><p>Bonuses.</p></div>');
|
|
84
|
+
});
|
|
85
|
+
it('rejects raw markup on a void element', async () => {
|
|
86
|
+
let thrown = null;
|
|
87
|
+
try {
|
|
88
|
+
await renderHtml('br', {
|
|
89
|
+
dangerouslySetInnerHTML: {
|
|
90
|
+
__html: '<b>hi</b>'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
thrown = error;
|
|
95
|
+
}
|
|
96
|
+
expect(thrown).to.be.an('error');
|
|
97
|
+
expect(thrown.message).to.include('void type');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Root } from 'postcss';
|
|
2
|
+
import type { CssNames } from '../CssNames.js';
|
|
3
|
+
type Opts = {
|
|
4
|
+
moduleName: string;
|
|
5
|
+
outNames: CssNames;
|
|
6
|
+
};
|
|
7
|
+
export declare const TransformCssNames: {
|
|
8
|
+
(opts: Opts): {
|
|
9
|
+
postcssPlugin: string;
|
|
10
|
+
Once(root: Root): void;
|
|
11
|
+
};
|
|
12
|
+
postcss: boolean;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
2
15
|
//# sourceMappingURL=TransformCssNames.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransformCssNames.d.ts","sourceRoot":"","sources":["../../src/postcssPlugins/TransformCssNames.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TransformCssNames.d.ts","sourceRoot":"","sources":["../../src/postcssPlugins/TransformCssNames.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,IAAI,EAAQ,MAAM,SAAS,CAAA;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAyD9C,KAAK,IAAI,GAAG;IACR,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,QAAQ,CAAA;CACrB,CAAA;AAKD,eAAO,MAAM,iBAAiB;WAAU,IAAI;;mBAezB,IAAI;;;CAoHtB,CAAA"}
|
|
@@ -9,8 +9,6 @@ extract classes -> localise
|
|
|
9
9
|
.navItem -> NavItem_navItemf
|
|
10
10
|
.span -> NavItem_span
|
|
11
11
|
*/
|
|
12
|
-
//const path = require('path')
|
|
13
|
-
import postcss from 'postcss';
|
|
14
12
|
import { parseSelectorsRich } from "./parseSelectorsRich.js";
|
|
15
13
|
import { componentBumpClass } from "../componentClassName.js";
|
|
16
14
|
import { throwError } from "../throwError.js";
|
|
@@ -45,13 +43,13 @@ function transformTag(moduleName, tag) {
|
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
45
|
//Page-poll#_div
|
|
46
|
+
// why is this unused?
|
|
48
47
|
function separatePseudo(selector) {
|
|
49
48
|
const colonIndex = selector.indexOf(':');
|
|
50
49
|
if (colonIndex === -1) return [selector, ''];else return [selector.slice(0, colonIndex), selector.slice(colonIndex)];
|
|
51
50
|
}
|
|
52
51
|
const bump = '.' + componentBumpClass + '.' + componentBumpClass + '.' + componentBumpClass;
|
|
53
|
-
|
|
54
|
-
export const TransformCssNames = postcss.plugin('postcss-transform-names', opts => {
|
|
52
|
+
export const TransformCssNames = opts => {
|
|
55
53
|
//console.log('TransformCssNames')
|
|
56
54
|
if (!('moduleName' in opts)) {
|
|
57
55
|
throw new Error(`PostcssTransformNames error - pass a moduleName prop to the` + ` plugin to receive transformed names map`);
|
|
@@ -59,100 +57,104 @@ export const TransformCssNames = postcss.plugin('postcss-transform-names', opts
|
|
|
59
57
|
if (!('outNames' in opts)) {
|
|
60
58
|
throw new Error(`PostcssTransformNames error - pass an outNames object to the` + ` plugin to receive transformed names map`);
|
|
61
59
|
}
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
60
|
+
return {
|
|
61
|
+
postcssPlugin: 'postcss-transform-names',
|
|
62
|
+
Once(root) {
|
|
63
|
+
const moduleName = opts.moduleName;
|
|
64
|
+
const names = opts.outNames;
|
|
65
|
+
names.moduleName = moduleName;
|
|
66
|
+
names.tags = {};
|
|
67
|
+
names.classes = {};
|
|
68
|
+
// Transform CSS AST here
|
|
69
|
+
root.walkRules(rule => {
|
|
70
|
+
const selectorLine = rule.selector;
|
|
71
|
+
const selectorGroups = selectorLine.split(',').map(x => x.trim());
|
|
72
|
+
const newSelectorGroups = [];
|
|
73
|
+
for (const selectorGroup of selectorGroups) {
|
|
74
|
+
debug('selectorGroup', selectorGroup);
|
|
75
|
+
//// Always explain your regex's
|
|
76
|
+
//// Split the string before a space, after a space, before a . or # within a sequence (e.g. foo.bar)
|
|
77
|
+
//// This splits the string into tags (foo), classes (.foo), IDs (#foo), and spaces ( )
|
|
78
|
+
//// The spaces are retained so that we can rebuild the string with the spaces as they were found
|
|
79
|
+
//const selectors = selectorGroup.split(/(?= )|(?<= )|(?<=\S)(?=[.#]\S)/)
|
|
80
|
+
// replace the regex with a parser. Easier
|
|
81
|
+
const selectors = parseSelectorsRich(selectorGroup);
|
|
82
|
+
const newSelectors = [];
|
|
83
|
+
for (const {
|
|
84
|
+
type,
|
|
85
|
+
content
|
|
86
|
+
} of selectors) {
|
|
87
|
+
debug('Sel: ', type + ':', content);
|
|
88
|
+
if (type === 'whitespace') {
|
|
89
|
+
newSelectors.push(' ');
|
|
90
|
+
} else if (type === 'grammar') {
|
|
91
|
+
newSelectors.push(content);
|
|
92
|
+
} else if (type === 'id') {
|
|
93
|
+
newSelectors.push(content);
|
|
94
|
+
} else if (type === 'class') {
|
|
95
|
+
const className = content.slice(1);
|
|
96
|
+
const newClassName = transformClass(moduleName, className);
|
|
97
|
+
newSelectors.push('.' + newClassName);
|
|
98
|
+
names.classes[className] = newClassName;
|
|
99
|
+
} else if (type === 'tag') {
|
|
100
|
+
const newName = transformTag(moduleName, content);
|
|
101
|
+
// newName.css === escaped version for css
|
|
102
|
+
// newName.jsx === unescaped ver for jsx
|
|
103
|
+
if (/^[A-Z]/.test(content)) {
|
|
104
|
+
// starts with uppercase - React el
|
|
105
|
+
newSelectors.push(bump + '.' + newName.css);
|
|
106
|
+
} else {
|
|
107
|
+
// lowercase - html el
|
|
108
|
+
newSelectors.push('.' + newName.css);
|
|
109
|
+
}
|
|
110
|
+
names.tags[content] = newName.jsx;
|
|
111
|
+
} else if (type === 'pseudo-class') {
|
|
112
|
+
// // REGEX:
|
|
113
|
+
// // :
|
|
114
|
+
// // multiple letters/numbers/-/_ [match group 1]
|
|
115
|
+
// // (
|
|
116
|
+
// // any chars (greedy / as many as possible) [match group 2]
|
|
117
|
+
// // )
|
|
118
|
+
// // e.g. :Match1(Match2)
|
|
119
|
+
// const pseudoClassRegex = /:([a-zA-Z0-9-_]+)\((.*)\)/
|
|
120
|
+
// REGEX:
|
|
121
|
+
// :
|
|
122
|
+
// multiple letters/numbers/-/_ [match group 1]
|
|
123
|
+
// optional section, non matching
|
|
124
|
+
// (
|
|
125
|
+
// any chars (greedy / as many as possible) [match group 2]
|
|
126
|
+
// )
|
|
127
|
+
// e.g. :Match1(Match2)
|
|
128
|
+
const pseudoClassRegex = /:([a-zA-Z0-9-_]+)(?:\((.*)\))?/;
|
|
129
|
+
const results = pseudoClassRegex.exec(content);
|
|
130
|
+
if (results === null) throwError('TransformCssNames -> walkRules', 'pseudo-class selector did not match pseudo-class regex');
|
|
131
|
+
const label = results[1]; // eslint-disable-line no-type-assertion/no-type-assertion -- typings are incorrect
|
|
132
|
+
// if it is :i(xxx) or :ignore(xxx), pass through the internal part
|
|
133
|
+
// otherwise pass all through unaffected
|
|
134
|
+
if (label === 'i' || label === 'ignore') {
|
|
135
|
+
const inner = results[2]; // eslint-disable-line no-type-assertion/no-type-assertion -- typings are incorrect
|
|
136
|
+
if (inner !== undefined && inner !== '') {
|
|
137
|
+
newSelectors.push(inner);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
newSelectors.push(content);
|
|
138
141
|
}
|
|
139
|
-
} else {
|
|
142
|
+
} else if (type === 'pseudo-element') {
|
|
143
|
+
newSelectors.push(content);
|
|
144
|
+
} else if (type === 'attribute') {
|
|
140
145
|
newSelectors.push(content);
|
|
141
146
|
}
|
|
142
|
-
} else if (type === 'pseudo-element') {
|
|
143
|
-
newSelectors.push(content);
|
|
144
|
-
} else if (type === 'attribute') {
|
|
145
|
-
newSelectors.push(content);
|
|
146
147
|
}
|
|
148
|
+
newSelectorGroups.push(newSelectors.join(''));
|
|
147
149
|
}
|
|
148
|
-
newSelectorGroups.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//opts.outNames = names
|
|
150
|
+
const newSelectorLine = newSelectorGroups.join(', ');
|
|
151
|
+
debug('new selector line', newSelectorLine);
|
|
152
|
+
debug('name for above line', names);
|
|
153
|
+
rule.selector = newSelectorLine;
|
|
154
|
+
});
|
|
155
|
+
//console.log("names", names)
|
|
156
|
+
//opts.outNames = names
|
|
157
|
+
}
|
|
157
158
|
};
|
|
158
|
-
}
|
|
159
|
+
};
|
|
160
|
+
TransformCssNames.postcss = true;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What raw markup is permitted to contain.
|
|
3
|
+
*
|
|
4
|
+
* Anything outside the list is dropped. A disallowed *element* is unwrapped
|
|
5
|
+
* rather than deleted — its children take its place — so filtering never
|
|
6
|
+
* silently loses someone's words. The exception is elements whose content is
|
|
7
|
+
* program text rather than prose (`script`, `style`), which go entirely.
|
|
8
|
+
*/
|
|
9
|
+
export type HtmlAllowList = {
|
|
10
|
+
/** Tag names that may be rendered. */
|
|
11
|
+
tags: readonly string[];
|
|
12
|
+
/** Attributes allowed on any allowed tag. */
|
|
13
|
+
attributes: readonly string[];
|
|
14
|
+
/** Further attributes allowed only on the named tags. */
|
|
15
|
+
tagAttributes?: Readonly<Record<string, readonly string[]>> | undefined;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=HtmlAllowList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HtmlAllowList.d.ts","sourceRoot":"","sources":["../../src/rawHtml/HtmlAllowList.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG;IACxB,sCAAsC;IACtC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,6CAA6C;IAC7C,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,yDAAyD;IACzD,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;CAC1E,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes HTML character references in text taken from raw markup.
|
|
3
|
+
*
|
|
4
|
+
* Never throws. An entity this does not recognise is left as written rather
|
|
5
|
+
* than dropped, so no content is ever silently lost.
|
|
6
|
+
*/
|
|
7
|
+
export declare function decodeHtmlEntities(text: string): string;
|
|
8
|
+
//# sourceMappingURL=decodeHtmlEntities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decodeHtmlEntities.d.ts","sourceRoot":"","sources":["../../src/rawHtml/decodeHtmlEntities.ts"],"names":[],"mappings":"AA0CA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOvD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The named entities that turn up in real authored content. This is not the
|
|
2
|
+
// full HTML5 table — that is thousands of entries — and it does not need to be:
|
|
3
|
+
// anything unrecognised is left exactly as written, which is what a reader
|
|
4
|
+
// sees today anyway.
|
|
5
|
+
const namedEntities = {
|
|
6
|
+
amp: '&',
|
|
7
|
+
lt: '<',
|
|
8
|
+
gt: '>',
|
|
9
|
+
quot: '"',
|
|
10
|
+
apos: '\'',
|
|
11
|
+
nbsp: '\u00a0',
|
|
12
|
+
ensp: '\u2002',
|
|
13
|
+
emsp: '\u2003',
|
|
14
|
+
thinsp: '\u2009',
|
|
15
|
+
shy: '\u00ad',
|
|
16
|
+
pound: '£',
|
|
17
|
+
euro: '€',
|
|
18
|
+
yen: '¥',
|
|
19
|
+
cent: '¢',
|
|
20
|
+
curren: '¤',
|
|
21
|
+
copy: '©',
|
|
22
|
+
reg: '®',
|
|
23
|
+
trade: '™',
|
|
24
|
+
sect: '§',
|
|
25
|
+
para: '¶',
|
|
26
|
+
dagger: '†',
|
|
27
|
+
Dagger: '‡',
|
|
28
|
+
deg: '°',
|
|
29
|
+
plusmn: '±',
|
|
30
|
+
micro: 'µ',
|
|
31
|
+
middot: '·',
|
|
32
|
+
bull: '•',
|
|
33
|
+
hellip: '…',
|
|
34
|
+
mdash: '—',
|
|
35
|
+
ndash: '–',
|
|
36
|
+
lsquo: '‘',
|
|
37
|
+
rsquo: '’',
|
|
38
|
+
sbquo: '‚',
|
|
39
|
+
ldquo: '“',
|
|
40
|
+
rdquo: '”',
|
|
41
|
+
bdquo: '„',
|
|
42
|
+
laquo: '«',
|
|
43
|
+
raquo: '»',
|
|
44
|
+
times: '×',
|
|
45
|
+
divide: '÷',
|
|
46
|
+
minus: '−',
|
|
47
|
+
frac12: '½',
|
|
48
|
+
frac14: '¼',
|
|
49
|
+
frac34: '¾',
|
|
50
|
+
sup1: '¹',
|
|
51
|
+
sup2: '²',
|
|
52
|
+
sup3: '³',
|
|
53
|
+
ordm: 'º',
|
|
54
|
+
ordf: 'ª',
|
|
55
|
+
larr: '←',
|
|
56
|
+
uarr: '↑',
|
|
57
|
+
rarr: '→',
|
|
58
|
+
darr: '↓',
|
|
59
|
+
harr: '↔',
|
|
60
|
+
ne: '≠',
|
|
61
|
+
le: '≤',
|
|
62
|
+
ge: '≥',
|
|
63
|
+
infin: '∞',
|
|
64
|
+
asymp: '≈',
|
|
65
|
+
prime: '′',
|
|
66
|
+
Prime: '″',
|
|
67
|
+
alpha: 'α',
|
|
68
|
+
beta: 'β',
|
|
69
|
+
gamma: 'γ',
|
|
70
|
+
delta: 'δ',
|
|
71
|
+
pi: 'π',
|
|
72
|
+
mu: 'μ',
|
|
73
|
+
omega: 'ω'
|
|
74
|
+
};
|
|
75
|
+
// REGEX:
|
|
76
|
+
// &
|
|
77
|
+
// then one of
|
|
78
|
+
// # followed by digits -- decimal numeric reference
|
|
79
|
+
// #x followed by hex digits (either case) -- hex numeric reference
|
|
80
|
+
// a letter followed by letters/digits -- named reference
|
|
81
|
+
// then ;
|
|
82
|
+
const entityPattern = /&(#[0-9]+|#[xX][0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g;
|
|
83
|
+
function decodeNumeric(body) {
|
|
84
|
+
const hex = body[1] === 'x' || body[1] === 'X';
|
|
85
|
+
const codePoint = parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
86
|
+
if (Number.isNaN(codePoint)) return null;
|
|
87
|
+
// Surrogates and out-of-range values make fromCodePoint throw, and a
|
|
88
|
+
// malformed entity must never take the page down.
|
|
89
|
+
if (codePoint <= 0 || codePoint > 0x10ffff) return null;
|
|
90
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdfff) return null;
|
|
91
|
+
return String.fromCodePoint(codePoint);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decodes HTML character references in text taken from raw markup.
|
|
95
|
+
*
|
|
96
|
+
* Never throws. An entity this does not recognise is left as written rather
|
|
97
|
+
* than dropped, so no content is ever silently lost.
|
|
98
|
+
*/
|
|
99
|
+
export function decodeHtmlEntities(text) {
|
|
100
|
+
if (!text.includes('&')) return text;
|
|
101
|
+
return text.replace(entityPattern, (match, body) => {
|
|
102
|
+
if (body.startsWith('#')) return decodeNumeric(body) ?? match;
|
|
103
|
+
return namedEntities[body] ?? match;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elements whose content is text, not markup, up to their closing tag.
|
|
3
|
+
*
|
|
4
|
+
* `escapable` ones hold ordinary text, so their character references are
|
|
5
|
+
* decoded; `literal` ones hold program text, which is taken exactly as written.
|
|
6
|
+
*/
|
|
7
|
+
export declare const htmlRawTextTags: Record<string, 'literal' | 'escapable'>;
|
|
8
|
+
/**
|
|
9
|
+
* Table-structural elements, which may not hold text directly.
|
|
10
|
+
*
|
|
11
|
+
* A browser moves text out of these to just before the table it is in — the
|
|
12
|
+
* spec calls it foster parenting — so the parser has to do the same, or the
|
|
13
|
+
* tree and the DOM built from the rendered markup disagree. Whitespace alone
|
|
14
|
+
* is left where it is, which is also what a browser does.
|
|
15
|
+
*
|
|
16
|
+
* Only tables behave this way. `ul`, `ol`, `dl` and `select` keep stray text
|
|
17
|
+
* exactly where it was written.
|
|
18
|
+
*/
|
|
19
|
+
export declare const htmlTableStructureTags: Record<string, true>;
|
|
20
|
+
export declare const htmlImpliedEndTags: Record<string, string[]>;
|
|
21
|
+
/** Whether an open `openTag` is implicitly closed by a start tag `startTag`. */
|
|
22
|
+
export declare function impliedEndTag(openTag: string, startTag: string): boolean;
|
|
23
|
+
//# sourceMappingURL=htmlParsingTags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"htmlParsingTags.d.ts","sourceRoot":"","sources":["../../src/rawHtml/htmlParsingTags.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,CAKnE,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAMvD,CAAA;AAYD,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAcvD,CAAA;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAExE"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Tag data the raw-HTML parser needs, kept together because the rules only
|
|
2
|
+
// make sense as a set.
|
|
3
|
+
/**
|
|
4
|
+
* Elements whose content is text, not markup, up to their closing tag.
|
|
5
|
+
*
|
|
6
|
+
* `escapable` ones hold ordinary text, so their character references are
|
|
7
|
+
* decoded; `literal` ones hold program text, which is taken exactly as written.
|
|
8
|
+
*/
|
|
9
|
+
export const htmlRawTextTags = {
|
|
10
|
+
script: 'literal',
|
|
11
|
+
style: 'literal',
|
|
12
|
+
textarea: 'escapable',
|
|
13
|
+
title: 'escapable'
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Table-structural elements, which may not hold text directly.
|
|
17
|
+
*
|
|
18
|
+
* A browser moves text out of these to just before the table it is in — the
|
|
19
|
+
* spec calls it foster parenting — so the parser has to do the same, or the
|
|
20
|
+
* tree and the DOM built from the rendered markup disagree. Whitespace alone
|
|
21
|
+
* is left where it is, which is also what a browser does.
|
|
22
|
+
*
|
|
23
|
+
* Only tables behave this way. `ul`, `ol`, `dl` and `select` keep stray text
|
|
24
|
+
* exactly where it was written.
|
|
25
|
+
*/
|
|
26
|
+
export const htmlTableStructureTags = {
|
|
27
|
+
table: true,
|
|
28
|
+
thead: true,
|
|
29
|
+
tbody: true,
|
|
30
|
+
tfoot: true,
|
|
31
|
+
tr: true
|
|
32
|
+
};
|
|
33
|
+
// Elements a start tag implicitly closes. The browser applies these rules
|
|
34
|
+
// whatever the markup says, so the parser has to as well — otherwise the tree
|
|
35
|
+
// nests where the DOM does not, which is the mismatch that breaks hydration.
|
|
36
|
+
const closedByBlock = ['address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'];
|
|
37
|
+
export const htmlImpliedEndTags = {
|
|
38
|
+
p: closedByBlock,
|
|
39
|
+
li: ['li'],
|
|
40
|
+
dt: ['dt', 'dd'],
|
|
41
|
+
dd: ['dt', 'dd'],
|
|
42
|
+
option: ['option', 'optgroup'],
|
|
43
|
+
optgroup: ['optgroup'],
|
|
44
|
+
tr: ['tr', 'tbody', 'tfoot', 'thead'],
|
|
45
|
+
td: ['td', 'th', 'tr', 'tbody', 'tfoot', 'thead'],
|
|
46
|
+
th: ['td', 'th', 'tr', 'tbody', 'tfoot', 'thead'],
|
|
47
|
+
thead: ['tbody', 'tfoot'],
|
|
48
|
+
tbody: ['tbody', 'tfoot'],
|
|
49
|
+
tfoot: ['tbody'],
|
|
50
|
+
a: ['a']
|
|
51
|
+
};
|
|
52
|
+
/** Whether an open `openTag` is implicitly closed by a start tag `startTag`. */
|
|
53
|
+
export function impliedEndTag(openTag, startTag) {
|
|
54
|
+
return htmlImpliedEndTags[openTag]?.includes(startTag) ?? false;
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ContentNode } from '../ContentNode.js';
|
|
2
|
+
import type { HtmlAllowList } from './HtmlAllowList.js';
|
|
3
|
+
export type ParseHtmlOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Drop `<script>` elements and `javascript:` URLs. On by default, because
|
|
6
|
+
* raw markup usually comes from outside the application.
|
|
7
|
+
*
|
|
8
|
+
* `on*` handler attributes are always dropped, sanitizing or not: Tungsten
|
|
9
|
+
* reads those props as event listeners, and a string where a function is
|
|
10
|
+
* expected breaks hydration.
|
|
11
|
+
*/
|
|
12
|
+
sanitize?: boolean | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Keep only the tags and attributes on this list. Off by default — without
|
|
15
|
+
* one, everything parsed is kept, subject to `sanitize`.
|
|
16
|
+
*
|
|
17
|
+
* See `safeHtmlAllowList` for a ready-made list for authored rich text.
|
|
18
|
+
*/
|
|
19
|
+
allowList?: HtmlAllowList | undefined;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Parses raw HTML into a Tungsten content tree.
|
|
23
|
+
*
|
|
24
|
+
* Never throws, whatever it is given. Unclosed tags are closed, stray end tags
|
|
25
|
+
* are ignored, and anything that is not markup is kept as text — the result is
|
|
26
|
+
* always a renderable tree.
|
|
27
|
+
*
|
|
28
|
+
* The point of parsing rather than injecting the markup is that the result is
|
|
29
|
+
* an ordinary tree: it renders, hydrates and reconciles like any other content,
|
|
30
|
+
* with no opaque region the framework cannot see into. That means the parser
|
|
31
|
+
* has to apply the browser's own nesting rules, or the tree it produces and the
|
|
32
|
+
* DOM the browser builds from the rendered markup would disagree.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseHtml(html: string, options?: ParseHtmlOptions): ContentNode;
|
|
35
|
+
//# sourceMappingURL=parseHtml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseHtml.d.ts","sourceRoot":"","sources":["../../src/rawHtml/parseHtml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,mBAAmB,CAAA;AAKhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvD,MAAM,MAAM,gBAAgB,GAAG;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;CACxC,CAAA;AAsID;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,CAqGnF"}
|