hyper-html-api 0.8.0 → 0.9.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/README.md +38 -0
- package/conformance/MANIFEST.json +2 -2
- package/conformance/cases/no-data-list-skipped.expected.json +6 -0
- package/conformance/cases/no-data-list-skipped.html +1 -0
- package/conformance/cases/no-data-list-skipped.meta +4 -0
- package/conformance/cases/no-data-list-skipped.parsed.json +3 -0
- package/conformance/cases/no-data-list-skipped.rules +1 -0
- package/conformance/cases/no-data-text-projected.expected.json +3 -0
- package/conformance/cases/no-data-text-projected.html +1 -0
- package/conformance/cases/no-data-text-projected.meta +4 -0
- package/conformance/cases/no-data-text-projected.parsed.json +3 -0
- package/conformance/cases/no-data-text-projected.rules +1 -0
- package/conformance/cases/props-type-is-attribute.expected.json +5 -0
- package/conformance/cases/props-type-is-attribute.meta +4 -0
- package/conformance/cases/write-attr-app-scheme.after.html +1 -0
- package/conformance/cases/write-attr-app-scheme.after.json +3 -0
- package/conformance/cases/write-attr-app-scheme.data.json +1 -0
- package/conformance/cases/write-attr-app-scheme.html +1 -0
- package/conformance/cases/write-attr-app-scheme.meta +4 -0
- package/conformance/cases/write-attr-app-scheme.write.json +4 -0
- package/conformance/cases/write-attr-href.after.html +1 -0
- package/conformance/cases/write-attr-href.after.json +3 -0
- package/conformance/cases/write-attr-href.data.json +1 -0
- package/conformance/cases/write-attr-href.html +1 -0
- package/conformance/cases/write-attr-href.meta +4 -0
- package/conformance/cases/write-attr-href.write.json +4 -0
- package/conformance/cases/write-checked-false.after.html +1 -0
- package/conformance/cases/write-checked-false.after.json +3 -0
- package/conformance/cases/write-checked-false.data.json +1 -0
- package/conformance/cases/write-checked-false.html +1 -0
- package/conformance/cases/write-checked-false.meta +4 -0
- package/conformance/cases/write-checked-false.write.json +4 -0
- package/conformance/cases/write-checked-true.after.html +1 -0
- package/conformance/cases/write-checked-true.after.json +3 -0
- package/conformance/cases/write-checked-true.data.json +1 -0
- package/conformance/cases/write-checked-true.html +1 -0
- package/conformance/cases/write-checked-true.meta +4 -0
- package/conformance/cases/write-checked-true.write.json +4 -0
- package/conformance/cases/write-crlf-multi.after.html +12 -0
- package/conformance/cases/write-crlf-multi.after.json +8 -0
- package/conformance/cases/write-crlf-multi.data.json +1 -0
- package/conformance/cases/write-crlf-multi.html +12 -0
- package/conformance/cases/write-crlf-multi.meta +4 -0
- package/conformance/cases/write-crlf-multi.write.json +4 -0
- package/conformance/cases/write-fallback-implied.after.html +1 -0
- package/conformance/cases/write-fallback-implied.after.json +3 -0
- package/conformance/cases/write-fallback-implied.data.json +1 -0
- package/conformance/cases/write-fallback-implied.html +1 -0
- package/conformance/cases/write-fallback-implied.meta +4 -0
- package/conformance/cases/write-fallback-implied.write.json +4 -0
- package/conformance/cases/write-flatten.after.html +1 -0
- package/conformance/cases/write-flatten.after.json +3 -0
- package/conformance/cases/write-flatten.data.json +1 -0
- package/conformance/cases/write-flatten.html +1 -0
- package/conformance/cases/write-flatten.meta +4 -0
- package/conformance/cases/write-flatten.write.json +4 -0
- package/conformance/cases/write-list-empty-no-seed.data.json +1 -0
- package/conformance/cases/write-list-empty-no-seed.error.json +8 -0
- package/conformance/cases/write-list-empty-no-seed.html +1 -0
- package/conformance/cases/write-list-empty-no-seed.meta +4 -0
- package/conformance/cases/write-list-grow-from-template.after.html +1 -0
- package/conformance/cases/write-list-grow-from-template.after.json +5 -0
- package/conformance/cases/write-list-grow-from-template.data.json +1 -0
- package/conformance/cases/write-list-grow-from-template.html +1 -0
- package/conformance/cases/write-list-grow-from-template.meta +4 -0
- package/conformance/cases/write-list-grow-from-template.write.json +4 -0
- package/conformance/cases/write-list-object-clone.after.html +1 -0
- package/conformance/cases/write-list-object-clone.after.json +12 -0
- package/conformance/cases/write-list-object-clone.data.json +1 -0
- package/conformance/cases/write-list-object-clone.html +1 -0
- package/conformance/cases/write-list-object-clone.meta +4 -0
- package/conformance/cases/write-list-object-clone.write.json +4 -0
- package/conformance/cases/write-list-scalar-grow.after.html +4 -0
- package/conformance/cases/write-list-scalar-grow.after.json +7 -0
- package/conformance/cases/write-list-scalar-grow.data.json +1 -0
- package/conformance/cases/write-list-scalar-grow.html +4 -0
- package/conformance/cases/write-list-scalar-grow.meta +4 -0
- package/conformance/cases/write-list-scalar-grow.write.json +4 -0
- package/conformance/cases/write-list-scalar-shrink.after.html +4 -0
- package/conformance/cases/write-list-scalar-shrink.after.json +5 -0
- package/conformance/cases/write-list-scalar-shrink.data.json +1 -0
- package/conformance/cases/write-list-scalar-shrink.html +4 -0
- package/conformance/cases/write-list-scalar-shrink.meta +4 -0
- package/conformance/cases/write-list-scalar-shrink.write.json +4 -0
- package/conformance/cases/write-list-shrink-zero.after.html +1 -0
- package/conformance/cases/write-list-shrink-zero.after.json +3 -0
- package/conformance/cases/write-list-shrink-zero.data.json +1 -0
- package/conformance/cases/write-list-shrink-zero.html +1 -0
- package/conformance/cases/write-list-shrink-zero.meta +4 -0
- package/conformance/cases/write-list-shrink-zero.write.json +4 -0
- package/conformance/cases/write-no-data-kept.after.html +1 -0
- package/conformance/cases/write-no-data-kept.after.json +3 -0
- package/conformance/cases/write-no-data-kept.data.json +1 -0
- package/conformance/cases/write-no-data-kept.html +1 -0
- package/conformance/cases/write-no-data-kept.meta +4 -0
- package/conformance/cases/write-no-data-kept.write.json +4 -0
- package/conformance/cases/write-no-rules-tag.data.json +1 -0
- package/conformance/cases/write-no-rules-tag.error.json +6 -0
- package/conformance/cases/write-no-rules-tag.html +1 -0
- package/conformance/cases/write-no-rules-tag.meta +4 -0
- package/conformance/cases/write-noop.after.html +3 -0
- package/conformance/cases/write-noop.after.json +3 -0
- package/conformance/cases/write-noop.data.json +1 -0
- package/conformance/cases/write-noop.html +3 -0
- package/conformance/cases/write-noop.meta +4 -0
- package/conformance/cases/write-noop.write.json +4 -0
- package/conformance/cases/write-outerhtml-unchanged.after.html +1 -0
- package/conformance/cases/write-outerhtml-unchanged.after.json +3 -0
- package/conformance/cases/write-outerhtml-unchanged.data.json +1 -0
- package/conformance/cases/write-outerhtml-unchanged.html +1 -0
- package/conformance/cases/write-outerhtml-unchanged.meta +4 -0
- package/conformance/cases/write-outerhtml-unchanged.write.json +4 -0
- package/conformance/cases/write-readonly-changed.data.json +1 -0
- package/conformance/cases/write-readonly-changed.error.json +6 -0
- package/conformance/cases/write-readonly-changed.html +1 -0
- package/conformance/cases/write-readonly-changed.meta +4 -0
- package/conformance/cases/write-readonly-unchanged.after.html +1 -0
- package/conformance/cases/write-readonly-unchanged.after.json +4 -0
- package/conformance/cases/write-readonly-unchanged.data.json +1 -0
- package/conformance/cases/write-readonly-unchanged.html +1 -0
- package/conformance/cases/write-readonly-unchanged.meta +4 -0
- package/conformance/cases/write-readonly-unchanged.write.json +4 -0
- package/conformance/cases/write-refused-animate.data.json +1 -0
- package/conformance/cases/write-refused-animate.error.json +11 -0
- package/conformance/cases/write-refused-animate.html +1 -0
- package/conformance/cases/write-refused-animate.meta +4 -0
- package/conformance/cases/write-refused-attr-name.data.json +1 -0
- package/conformance/cases/write-refused-attr-name.error.json +11 -0
- package/conformance/cases/write-refused-attr-name.html +1 -0
- package/conformance/cases/write-refused-attr-name.meta +4 -0
- package/conformance/cases/write-refused-data-url.data.json +1 -0
- package/conformance/cases/write-refused-data-url.error.json +11 -0
- package/conformance/cases/write-refused-data-url.html +1 -0
- package/conformance/cases/write-refused-data-url.meta +4 -0
- package/conformance/cases/write-refused-innerhtml.data.json +1 -0
- package/conformance/cases/write-refused-innerhtml.error.json +11 -0
- package/conformance/cases/write-refused-innerhtml.html +1 -0
- package/conformance/cases/write-refused-innerhtml.meta +4 -0
- package/conformance/cases/write-refused-javascript-url.data.json +1 -0
- package/conformance/cases/write-refused-javascript-url.error.json +11 -0
- package/conformance/cases/write-refused-javascript-url.html +1 -0
- package/conformance/cases/write-refused-javascript-url.meta +4 -0
- package/conformance/cases/write-refused-list-removes-rules-tag.data.json +1 -0
- package/conformance/cases/write-refused-list-removes-rules-tag.error.json +11 -0
- package/conformance/cases/write-refused-list-removes-rules-tag.html +1 -0
- package/conformance/cases/write-refused-list-removes-rules-tag.meta +4 -0
- package/conformance/cases/write-refused-multiple.data.json +1 -0
- package/conformance/cases/write-refused-multiple.error.json +15 -0
- package/conformance/cases/write-refused-multiple.html +1 -0
- package/conformance/cases/write-refused-multiple.meta +4 -0
- package/conformance/cases/write-refused-onclick.data.json +1 -0
- package/conformance/cases/write-refused-onclick.error.json +11 -0
- package/conformance/cases/write-refused-onclick.html +1 -0
- package/conformance/cases/write-refused-onclick.meta +4 -0
- package/conformance/cases/write-refused-rules-tag-ancestor.data.json +1 -0
- package/conformance/cases/write-refused-rules-tag-ancestor.error.json +11 -0
- package/conformance/cases/write-refused-rules-tag-ancestor.html +1 -0
- package/conformance/cases/write-refused-rules-tag-ancestor.meta +4 -0
- package/conformance/cases/write-refused-script.data.json +1 -0
- package/conformance/cases/write-refused-script.error.json +11 -0
- package/conformance/cases/write-refused-script.html +1 -0
- package/conformance/cases/write-refused-script.meta +4 -0
- package/conformance/cases/write-shape-mismatch.data.json +1 -0
- package/conformance/cases/write-shape-mismatch.error.json +12 -0
- package/conformance/cases/write-shape-mismatch.html +1 -0
- package/conformance/cases/write-shape-mismatch.meta +4 -0
- package/conformance/cases/write-text-basic.after.html +3 -0
- package/conformance/cases/write-text-basic.after.json +3 -0
- package/conformance/cases/write-text-basic.data.json +1 -0
- package/conformance/cases/write-text-basic.html +3 -0
- package/conformance/cases/write-text-basic.meta +4 -0
- package/conformance/cases/write-text-basic.write.json +4 -0
- package/conformance/cases/write-text-escape.after.html +1 -0
- package/conformance/cases/write-text-escape.after.json +3 -0
- package/conformance/cases/write-text-escape.data.json +1 -0
- package/conformance/cases/write-text-escape.html +1 -0
- package/conformance/cases/write-text-escape.meta +4 -0
- package/conformance/cases/write-text-escape.write.json +4 -0
- package/conformance/cases/write-text-null-clears.after.html +1 -0
- package/conformance/cases/write-text-null-clears.after.json +3 -0
- package/conformance/cases/write-text-null-clears.data.json +1 -0
- package/conformance/cases/write-text-null-clears.html +1 -0
- package/conformance/cases/write-text-null-clears.meta +4 -0
- package/conformance/cases/write-text-null-clears.write.json +4 -0
- package/conformance/cases/write-type-attribute.after.html +1 -0
- package/conformance/cases/write-type-attribute.after.json +3 -0
- package/conformance/cases/write-type-attribute.data.json +1 -0
- package/conformance/cases/write-type-attribute.html +1 -0
- package/conformance/cases/write-type-attribute.meta +4 -0
- package/conformance/cases/write-type-attribute.write.json +4 -0
- package/conformance/cases/write-unknown-key.data.json +1 -0
- package/conformance/cases/write-unknown-key.error.json +11 -0
- package/conformance/cases/write-unknown-key.html +3 -0
- package/conformance/cases/write-unknown-key.meta +4 -0
- package/conformance/cases/write-unmatched.data.json +1 -0
- package/conformance/cases/write-unmatched.error.json +14 -0
- package/conformance/cases/write-unmatched.html +1 -0
- package/conformance/cases/write-unmatched.meta +4 -0
- package/conformance/write-policy.json +78 -0
- package/dist/hyper-html-api.engine.min.js +1 -1
- package/dist/hyper-html-api.min.js +5 -4
- package/dist/hyper-html-api.upgrade.min.js +2 -2
- package/package.json +5 -3
- package/packed-contract.json +1 -0
- package/src/adapters/cheerio.js +109 -10
- package/src/adapters/dom.js +191 -0
- package/src/dom-api.js +17 -6
- package/src/engine/apply.js +16 -2
- package/src/engine/errors.js +25 -0
- package/src/engine/extract.js +3 -0
- package/src/engine/index.js +4 -1
- package/src/engine/splice.js +118 -0
- package/src/engine/write-plan.js +52 -0
- package/src/engine/write-policy.js +136 -0
- package/src/lib/content-dom.js +110 -0
- package/src/lib/region-capabilities.js +71 -0
- package/src/write.js +29 -0
- package/conformance/cases/props-type-is-node-type.expected.json +0 -5
- package/conformance/cases/props-type-is-node-type.meta +0 -11
- /package/conformance/cases/{props-type-is-node-type.html → props-type-is-attribute.html} +0 -0
- /package/conformance/cases/{props-type-is-node-type.parsed.json → props-type-is-attribute.parsed.json} +0 -0
- /package/conformance/cases/{props-type-is-node-type.rules → props-type-is-attribute.rules} +0 -0
package/src/engine/errors.js
CHANGED
|
@@ -49,3 +49,28 @@ export class RuleTargetReadOnly extends Error {
|
|
|
49
49
|
this.target = name
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export class WriteRefused extends Error {
|
|
54
|
+
constructor(refusals) {
|
|
55
|
+
super(`write refused: ${refusals.length} write(s) broke the content-only policy`)
|
|
56
|
+
this.name = 'WriteRefused'
|
|
57
|
+
this.refusals = refusals
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class NoRulesTag extends Error {
|
|
62
|
+
constructor(token) {
|
|
63
|
+
super(`no <script data-rules-name~="${token}"> rules tag in this document`)
|
|
64
|
+
this.name = 'NoRulesTag'
|
|
65
|
+
this.token = token
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class WriteRejected extends Error {
|
|
70
|
+
constructor(unknownKeys, unmatched) {
|
|
71
|
+
super(`write rejected: ${unknownKeys.length} unknown key(s), ${unmatched.length} rule(s) with no matching element`)
|
|
72
|
+
this.name = 'WriteRejected'
|
|
73
|
+
this.unknownKeys = unknownKeys
|
|
74
|
+
this.unmatched = unmatched
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/engine/extract.js
CHANGED
|
@@ -3,6 +3,9 @@ import { MaxRuleDepthExceeded, MAX_RULE_DEPTH } from './errors.js'
|
|
|
3
3
|
import { ruleAttrIndex } from './rule-syntax.js'
|
|
4
4
|
|
|
5
5
|
export function extract(adapter, root, rules, opts = {}) {
|
|
6
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'exclude') && adapter.semanticExclude === false) {
|
|
7
|
+
throw new Error('This adapter does not support semantic exclude queries; omit exclude or use the DOM adapter.')
|
|
8
|
+
}
|
|
6
9
|
return extractAt(adapter, root, rules, { depth: 0, path: [] }, opts)
|
|
7
10
|
}
|
|
8
11
|
|
package/src/engine/index.js
CHANGED
|
@@ -8,7 +8,10 @@ export { findRulesIn } from './rules-tag.js'
|
|
|
8
8
|
export { parseStrict, parseRelaxed } from './rules.js'
|
|
9
9
|
export { ruleAttrIndex, splitRule } from './rule-syntax.js'
|
|
10
10
|
export * as errors from './errors.js'
|
|
11
|
-
export { DOM_PROPERTIES, DOM_PROPERTIES_SET } from './dom-properties.js'
|
|
11
|
+
export { DOM_PROPERTIES, DOM_PROPERTIES_SET, DOM_PROPERTIES_READ_ONLY_SET } from './dom-properties.js'
|
|
12
|
+
export { WRITE_POLICY, guardAdapter, checkAttribute, urlScheme } from './write-policy.js'
|
|
13
|
+
export { planWrite } from './write-plan.js'
|
|
14
|
+
export { trackAdapter, spliceDocument } from './splice.js'
|
|
12
15
|
|
|
13
16
|
// source: object literal → use as-is (no tag); string → look up by token.
|
|
14
17
|
// For an element root, search the whole document (root.ownerDocument) so a
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
function raw(node) {
|
|
2
|
+
return node && node[0] ? node[0] : node
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Records which nodes a write touched. Structural changes mark the parent,
|
|
6
|
+
// since the parent's children are what changed. Clones are recorded so their
|
|
7
|
+
// copied source locations are never trusted.
|
|
8
|
+
export function trackAdapter(adapter) {
|
|
9
|
+
const dirty = new Set()
|
|
10
|
+
const cloned = new WeakSet()
|
|
11
|
+
const mark = (node) => {
|
|
12
|
+
const r = raw(node)
|
|
13
|
+
if (r) dirty.add(r)
|
|
14
|
+
}
|
|
15
|
+
const markTree = (r) => {
|
|
16
|
+
if (!r) return
|
|
17
|
+
cloned.add(r)
|
|
18
|
+
for (const c of r.children || []) markTree(c)
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...adapter,
|
|
22
|
+
text(node, value) {
|
|
23
|
+
if (value === undefined) return adapter.text(node)
|
|
24
|
+
mark(node)
|
|
25
|
+
return adapter.text(node, value)
|
|
26
|
+
},
|
|
27
|
+
attr(node, name, value) {
|
|
28
|
+
if (value === undefined) return adapter.attr(node, name)
|
|
29
|
+
mark(node)
|
|
30
|
+
return adapter.attr(node, name, value)
|
|
31
|
+
},
|
|
32
|
+
removeAttr(node, name) {
|
|
33
|
+
mark(node)
|
|
34
|
+
return adapter.removeAttr(node, name)
|
|
35
|
+
},
|
|
36
|
+
prop(node, name, value) {
|
|
37
|
+
if (value === undefined) return adapter.prop(node, name)
|
|
38
|
+
mark(node)
|
|
39
|
+
return adapter.prop(node, name, value)
|
|
40
|
+
},
|
|
41
|
+
replaceWith(node, html) {
|
|
42
|
+
const p = adapter.parent(node)
|
|
43
|
+
if (p) mark(p)
|
|
44
|
+
return adapter.replaceWith(node, html)
|
|
45
|
+
},
|
|
46
|
+
clone(node) {
|
|
47
|
+
const c = adapter.clone(node)
|
|
48
|
+
markTree(raw(c))
|
|
49
|
+
return c
|
|
50
|
+
},
|
|
51
|
+
insertAt(parent, node, index) {
|
|
52
|
+
mark(parent)
|
|
53
|
+
const from = adapter.parent(node)
|
|
54
|
+
if (from) mark(from)
|
|
55
|
+
return adapter.insertAt(parent, node, index)
|
|
56
|
+
},
|
|
57
|
+
remove(node) {
|
|
58
|
+
const p = adapter.parent(node)
|
|
59
|
+
if (p) mark(p)
|
|
60
|
+
return adapter.remove(node)
|
|
61
|
+
},
|
|
62
|
+
dirty,
|
|
63
|
+
cloned,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function attached(r, docRoot) {
|
|
68
|
+
let n = r
|
|
69
|
+
while (n && n.parent) n = n.parent
|
|
70
|
+
return n === docRoot
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function located(r, cloned) {
|
|
74
|
+
let n = r
|
|
75
|
+
while (n && n.type !== 'root') {
|
|
76
|
+
const loc = n.sourceCodeLocation
|
|
77
|
+
if (!cloned.has(n) && loc && loc.startOffset != null && loc.endOffset != null) return n
|
|
78
|
+
n = n.parent
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasAncestorIn(r, set) {
|
|
84
|
+
let n = r.parent
|
|
85
|
+
while (n) {
|
|
86
|
+
if (set.has(n)) return true
|
|
87
|
+
n = n.parent
|
|
88
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Replaces the source span of each topmost changed element with its fresh
|
|
93
|
+
// serialization, then proves the result by reparsing: the spliced text must
|
|
94
|
+
// render exactly like the mutated tree. Anything unprovable falls back to the
|
|
95
|
+
// whole-document render.
|
|
96
|
+
export function spliceDocument(load, src, $, tracker) {
|
|
97
|
+
const full = $.html()
|
|
98
|
+
const fallback = { html: full, spliced: false }
|
|
99
|
+
const docRoot = raw($.root())
|
|
100
|
+
const targets = new Set()
|
|
101
|
+
for (const r of tracker.dirty) {
|
|
102
|
+
if (!attached(r, docRoot)) continue
|
|
103
|
+
const at = located(r, tracker.cloned)
|
|
104
|
+
if (!at) return fallback
|
|
105
|
+
targets.add(at)
|
|
106
|
+
}
|
|
107
|
+
const top = [...targets].filter((r) => !hasAncestorIn(r, targets))
|
|
108
|
+
top.sort((a, b) => b.sourceCodeLocation.startOffset - a.sourceCodeLocation.startOffset)
|
|
109
|
+
let out = src
|
|
110
|
+
let prevStart = Infinity
|
|
111
|
+
for (const r of top) {
|
|
112
|
+
const { startOffset, endOffset } = r.sourceCodeLocation
|
|
113
|
+
if (endOffset > prevStart) return fallback
|
|
114
|
+
out = out.slice(0, startOffset) + $.html(r) + out.slice(endOffset)
|
|
115
|
+
prevStart = startOffset
|
|
116
|
+
}
|
|
117
|
+
return load(out).html() === full ? { html: out, spliced: true } : fallback
|
|
118
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ruleAttrIndex } from './rule-syntax.js'
|
|
2
|
+
|
|
3
|
+
function isPlainObject(v) {
|
|
4
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function label(path) {
|
|
8
|
+
return path.reduce((out, part) => (typeof part === 'number' ? `${out}[${part}]` : out ? `${out}.${part}` : part), '')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Walks rules against data without touching the document. Reports data keys
|
|
12
|
+
// the rules do not define, and scalar rules whose selector matches nothing.
|
|
13
|
+
// Shape problems are left to apply(), which already reports them. Inside list
|
|
14
|
+
// items only unknown keys are checked, because new rows do not exist yet.
|
|
15
|
+
export function planWrite(adapter, root, rules, data, opts = {}) {
|
|
16
|
+
const unknownKeys = []
|
|
17
|
+
const unmatched = []
|
|
18
|
+
|
|
19
|
+
function checkKeys(rule, value, path) {
|
|
20
|
+
if (Array.isArray(rule)) {
|
|
21
|
+
const [, shape] = rule
|
|
22
|
+
if (!Array.isArray(value) || !isPlainObject(shape)) return
|
|
23
|
+
value.forEach((item, i) => checkKeys(shape, item, [...path, i]))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if (!isPlainObject(rule) || !isPlainObject(value)) return
|
|
27
|
+
for (const key of Object.keys(value)) {
|
|
28
|
+
if (!Object.prototype.hasOwnProperty.call(rule, key)) unknownKeys.push(label([...path, key]))
|
|
29
|
+
else checkKeys(rule[key], value[key], [...path, key])
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkTargets(ctx, rule, value, path) {
|
|
34
|
+
if (value === undefined) return
|
|
35
|
+
if (typeof rule === 'string') {
|
|
36
|
+
if (rule.endsWith('[]') || rule === '.' || rule.startsWith('@')) return
|
|
37
|
+
const at = ruleAttrIndex(rule)
|
|
38
|
+
const selector = at === -1 ? rule : rule.slice(0, at)
|
|
39
|
+
if (!selector) return
|
|
40
|
+
if (adapter.find(ctx, selector, opts).length === 0) unmatched.push({ path: label(path), selector })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(rule)) return
|
|
44
|
+
if (isPlainObject(rule) && isPlainObject(value)) {
|
|
45
|
+
for (const [key, sub] of Object.entries(rule)) checkTargets(ctx, sub, value[key], [...path, key])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
checkKeys(rules, data, [])
|
|
50
|
+
checkTargets(root, rules, data, [])
|
|
51
|
+
return { unknownKeys, unmatched }
|
|
52
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { POLICY_TOKENS } from '../lib/region-capabilities.js'
|
|
2
|
+
import { WriteRefused } from './errors.js'
|
|
3
|
+
|
|
4
|
+
export const WRITE_POLICY = Object.freeze({
|
|
5
|
+
refusedRuleForms: ['innerHTML', 'outerHTML'],
|
|
6
|
+
refusedTargets: ['script', 'style', 'template', 'noscript', 'iframe', 'object', 'embed', 'xmp', 'plaintext', 'noembed', 'noframes', 'meta', 'link', 'base', 'animate', 'set', 'animatemotion', 'animatetransform'],
|
|
7
|
+
refusedAttributes: [
|
|
8
|
+
'style', 'srcdoc', 'is', 'http-equiv', 'sandbox', 'allow', 'nonce', 'integrity', 'charset',
|
|
9
|
+
'htmlclaytoken', 'htmlclayid', 'data-rules-name', 'data-rules-version', 'clay', 'contenteditable',
|
|
10
|
+
...POLICY_TOKENS,
|
|
11
|
+
],
|
|
12
|
+
refusedAttributePrefixes: ['on'],
|
|
13
|
+
urlAttributes: ['href', 'src', 'srcset', 'action', 'formaction', 'poster', 'cite', 'data', 'ping', 'background', 'longdesc', 'usemap', 'xlink:href', 'manifest', 'codebase', 'icon'],
|
|
14
|
+
refusedSchemes: ['javascript', 'vbscript', 'data'],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const TEXT_PROPS = new Set(['textContent', 'innerText'])
|
|
18
|
+
|
|
19
|
+
export function urlScheme(value) {
|
|
20
|
+
const cleaned = String(value).replace(/^[\u0000- ]+|[\u0000- ]+$/g, '').replace(/[\t\n\r]/g, '')
|
|
21
|
+
const m = cleaned.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)
|
|
22
|
+
return m ? m[1].toLowerCase() : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VALID_ATTR_NAME = /^[^\t\n\f\r "'>/=\u0000-\u001f\u007f]+$/
|
|
26
|
+
|
|
27
|
+
export function checkAttribute(name, value, policy = WRITE_POLICY) {
|
|
28
|
+
if (!VALID_ATTR_NAME.test(String(name))) return `attribute name ${JSON.stringify(String(name))} is not valid`
|
|
29
|
+
const lower = String(name).toLowerCase()
|
|
30
|
+
if (policy.refusedAttributePrefixes.some((p) => lower.startsWith(p))) return `attribute "${lower}" can run script`
|
|
31
|
+
if (policy.refusedAttributes.includes(lower)) return `attribute "${lower}" is not content`
|
|
32
|
+
if (policy.urlAttributes.includes(lower) && value != null) {
|
|
33
|
+
const urls = lower === 'srcset'
|
|
34
|
+
? String(value).split(',').map((c) => c.trim().split(/\s+/)[0]).filter(Boolean)
|
|
35
|
+
: [String(value)]
|
|
36
|
+
for (const url of urls) {
|
|
37
|
+
const scheme = urlScheme(url)
|
|
38
|
+
if (scheme && policy.refusedSchemes.includes(scheme)) return `"${scheme}:" URLs are not allowed in "${lower}"`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tagOf(adapter, node) {
|
|
45
|
+
const t = adapter.prop(node, 'tagName')
|
|
46
|
+
return t ? String(t).toLowerCase() : ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function describe(adapter, node) {
|
|
50
|
+
const tag = tagOf(adapter, node) || 'node'
|
|
51
|
+
const id = adapter.attr(node, 'id')
|
|
52
|
+
return id ? `<${tag}#${id}>` : `<${tag}>`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function refusedAncestor(adapter, node, policy) {
|
|
56
|
+
let cur = node
|
|
57
|
+
while (cur) {
|
|
58
|
+
const tag = tagOf(adapter, cur)
|
|
59
|
+
if (tag && policy.refusedTargets.includes(tag)) return tag
|
|
60
|
+
cur = adapter.parent(cur)
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function holdsRulesTag(adapter, node) {
|
|
66
|
+
return adapter.find(node, 'script[data-rules-name]', { includeRulesTag: true, templateAttr: null }).length > 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isOrHoldsRulesTag(adapter, node) {
|
|
70
|
+
if (tagOf(adapter, node) === 'script' && adapter.attr(node, 'data-rules-name') != null) return true
|
|
71
|
+
return holdsRulesTag(adapter, node)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Wraps an adapter so every write is checked against the policy first. A write
|
|
75
|
+
// that breaks it is skipped and recorded; call assertClean() after apply() to
|
|
76
|
+
// throw one WriteRefused listing every violation.
|
|
77
|
+
export function guardAdapter(adapter, policy = WRITE_POLICY) {
|
|
78
|
+
const refusals = []
|
|
79
|
+
const refuse = (node, reason) => {
|
|
80
|
+
refusals.push({ target: describe(adapter, node), reason })
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
const allowTarget = (node) => {
|
|
84
|
+
const bad = refusedAncestor(adapter, node, policy)
|
|
85
|
+
return bad ? refuse(node, `inside <${bad}>, which is not content`) : true
|
|
86
|
+
}
|
|
87
|
+
const allowText = (node) => {
|
|
88
|
+
if (!allowTarget(node)) return false
|
|
89
|
+
if (holdsRulesTag(adapter, node)) return refuse(node, 'a text write here would delete the data rules tag')
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
const allowAttr = (node, name, value) => {
|
|
93
|
+
if (!allowTarget(node)) return false
|
|
94
|
+
const reason = checkAttribute(name, value, policy)
|
|
95
|
+
return reason ? refuse(node, reason) : true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const guarded = {
|
|
99
|
+
...adapter,
|
|
100
|
+
text(node, value) {
|
|
101
|
+
if (value === undefined) return adapter.text(node)
|
|
102
|
+
if (allowText(node)) adapter.text(node, value)
|
|
103
|
+
},
|
|
104
|
+
attr(node, name, value) {
|
|
105
|
+
if (value === undefined) return adapter.attr(node, name)
|
|
106
|
+
if (allowAttr(node, name, value)) adapter.attr(node, name, value)
|
|
107
|
+
},
|
|
108
|
+
prop(node, name, value) {
|
|
109
|
+
if (value === undefined) return adapter.prop(node, name)
|
|
110
|
+
if (policy.refusedRuleForms.includes(name)) return refuse(node, `"@${name}" writes HTML, only text is allowed`)
|
|
111
|
+
if (TEXT_PROPS.has(name)) {
|
|
112
|
+
if (allowText(node)) adapter.prop(node, name, value)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const attrName = name === 'className' ? 'class' : name
|
|
116
|
+
if (allowAttr(node, attrName, value)) adapter.prop(node, name, value)
|
|
117
|
+
},
|
|
118
|
+
remove(node) {
|
|
119
|
+
if (isOrHoldsRulesTag(adapter, node)) {
|
|
120
|
+
refuse(node, 'removing this would delete the data rules tag')
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
adapter.remove(node)
|
|
124
|
+
},
|
|
125
|
+
replaceWith(node, html) {
|
|
126
|
+
if (html === adapter.prop(node, 'outerHTML')) return node
|
|
127
|
+
refuse(node, '"@outerHTML" writes HTML, only text is allowed')
|
|
128
|
+
return node
|
|
129
|
+
},
|
|
130
|
+
refusals,
|
|
131
|
+
assertClean() {
|
|
132
|
+
if (refusals.length) throw new WriteRefused(refusals.slice())
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
return guarded
|
|
136
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// GENERATED from clayjs/src/lib/content-dom.js. Edit ClayJS and run `npm run propagate:foundation`.
|
|
2
|
+
|
|
3
|
+
import { capabilitySelector, hasCapability } from './region-capabilities.js'
|
|
4
|
+
|
|
5
|
+
const UNSUPPORTED_SELECTOR = /:(?:focus(?:-within|-visible)?|hover|active|visited|defined)\b/i
|
|
6
|
+
|
|
7
|
+
function copyControlState(source, copy) {
|
|
8
|
+
if (!/^(INPUT|TEXTAREA|SELECT|OPTION)$/.test(source.tagName || '')) return
|
|
9
|
+
const type = (source.getAttribute?.('type') || '').toLowerCase()
|
|
10
|
+
if ('value' in source && 'value' in copy && source.tagName !== 'OPTION' && type !== 'checkbox' && type !== 'radio') copy.value = source.value
|
|
11
|
+
if ('checked' in source && 'checked' in copy) copy.checked = source.checked
|
|
12
|
+
if ('selected' in source && 'selected' in copy) copy.selected = source.selected
|
|
13
|
+
if (source.tagName === 'SELECT') {
|
|
14
|
+
for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected
|
|
15
|
+
}
|
|
16
|
+
if ('indeterminate' in source && 'indeterminate' in copy) copy.indeterminate = source.indeterminate
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function matchesWithin(node, selector, boundary, inherit) {
|
|
20
|
+
if (inherit) return !!node.closest?.(selector)
|
|
21
|
+
let current = node
|
|
22
|
+
while (current?.nodeType === 1) {
|
|
23
|
+
if (current.matches(selector)) return true
|
|
24
|
+
if (current === boundary) break
|
|
25
|
+
current = current.parentElement
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
|
|
31
|
+
if (source.nodeType === 1 && (
|
|
32
|
+
(inherit ? hasCapability(source, capability) : matchesWithin(source, capabilityMatch, boundary, false)) ||
|
|
33
|
+
(exclude && matchesWithin(source, exclude, boundary, inherit))
|
|
34
|
+
)) return null
|
|
35
|
+
const copy = targetDocument.importNode(source, false)
|
|
36
|
+
maps.cloneToLive.set(copy, source)
|
|
37
|
+
maps.liveToClone.set(source, copy)
|
|
38
|
+
const sourceChildren = source.nodeType === 1 && source.tagName === 'TEMPLATE' ? source.content : source
|
|
39
|
+
const copyChildren = copy.nodeType === 1 && copy.tagName === 'TEMPLATE' ? copy.content : copy
|
|
40
|
+
if (sourceChildren !== source) {
|
|
41
|
+
maps.cloneToLive.set(copyChildren, sourceChildren)
|
|
42
|
+
maps.liveToClone.set(sourceChildren, copyChildren)
|
|
43
|
+
}
|
|
44
|
+
for (const child of sourceChildren.childNodes || []) {
|
|
45
|
+
const childCopy = importTree(child, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps)
|
|
46
|
+
if (childCopy) {
|
|
47
|
+
copyChildren.appendChild(childCopy)
|
|
48
|
+
if (child.nodeType === 1) copyControlState(child, childCopy)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (source.nodeType === 1) copyControlState(source, copy)
|
|
52
|
+
return copy
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createContentView(context, { capability = 'data', exclude = null, inherit = true } = {}) {
|
|
56
|
+
if (!context) throw new TypeError('createContentView requires a DOM context')
|
|
57
|
+
const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument
|
|
58
|
+
if (!sourceDocument?.implementation?.createHTMLDocument) {
|
|
59
|
+
throw new TypeError('createContentView requires an HTML DOM implementation')
|
|
60
|
+
}
|
|
61
|
+
const inertDocument = sourceDocument.implementation.createHTMLDocument('')
|
|
62
|
+
const cloneToLive = new WeakMap()
|
|
63
|
+
const liveToClone = new WeakMap()
|
|
64
|
+
const sourceRoot = context.nodeType === 9 ? context.documentElement : context
|
|
65
|
+
if (exclude) inertDocument.documentElement.matches(exclude)
|
|
66
|
+
const capabilityMatch = capabilitySelector(capability)
|
|
67
|
+
const root = importTree(sourceRoot, inertDocument, capability, capabilityMatch, exclude, sourceRoot, inherit, { cloneToLive, liveToClone })
|
|
68
|
+
if (context.nodeType === 9 && root) {
|
|
69
|
+
inertDocument.replaceChild(root, inertDocument.documentElement)
|
|
70
|
+
liveToClone.set(context, inertDocument)
|
|
71
|
+
cloneToLive.set(inertDocument, context)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const assertSelector = selector => {
|
|
75
|
+
if (UNSUPPORTED_SELECTOR.test(selector)) {
|
|
76
|
+
throw new Error(`Filtered content queries do not support stateful selector: ${selector}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
root,
|
|
82
|
+
document: inertDocument,
|
|
83
|
+
capability,
|
|
84
|
+
selector: capabilitySelector(capability),
|
|
85
|
+
cloneToLive,
|
|
86
|
+
liveToClone,
|
|
87
|
+
original(node) { return cloneToLive.get(node) || null },
|
|
88
|
+
cloneOf(node) { return liveToClone.get(node) || null },
|
|
89
|
+
query(selector, queryRoot = root) {
|
|
90
|
+
assertSelector(selector)
|
|
91
|
+
return Array.from(queryRoot.querySelectorAll(selector), node => cloneToLive.get(node) || node)
|
|
92
|
+
},
|
|
93
|
+
text(node = root) {
|
|
94
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
95
|
+
return projected?.textContent || ''
|
|
96
|
+
},
|
|
97
|
+
html(node = root) {
|
|
98
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
99
|
+
return projected?.innerHTML ?? ''
|
|
100
|
+
},
|
|
101
|
+
clone(node = root) {
|
|
102
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
103
|
+
return projected ? inertDocument.importNode(projected, true) : null
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function cleanContentClone(node, options) {
|
|
109
|
+
return createContentView(node, options).root
|
|
110
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// GENERATED from clayjs/src/lib/region-capabilities.js. Edit ClayJS and run `npm run propagate:foundation`.
|
|
2
|
+
|
|
3
|
+
export const CAPABILITIES = Object.freeze({
|
|
4
|
+
data: Object.freeze({ tokens: ['no-data'], bundles: ['editor-ui'] }),
|
|
5
|
+
save: Object.freeze({ tokens: ['no-save'], bundles: ['editor-ui'] }),
|
|
6
|
+
snapshot: Object.freeze({ tokens: ['no-snapshot'], bundles: ['editor-ui'] }),
|
|
7
|
+
watch: Object.freeze({ tokens: ['no-watch'], bundles: ['editor-ui'] }),
|
|
8
|
+
undo: Object.freeze({ tokens: ['no-undo'], bundles: ['editor-ui'] }),
|
|
9
|
+
history: Object.freeze({ tokens: [], bundles: ['editor-ui'] }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const BUNDLES = Object.freeze({
|
|
13
|
+
'editor-ui': Object.freeze(['no-data', 'no-save', 'no-snapshot', 'no-watch', 'no-undo']),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const POLICY_TOKENS = Object.freeze([
|
|
17
|
+
'no-save',
|
|
18
|
+
'no-snapshot',
|
|
19
|
+
'no-trigger-autosave',
|
|
20
|
+
'no-dirty',
|
|
21
|
+
'no-watch',
|
|
22
|
+
'no-undo',
|
|
23
|
+
'no-data',
|
|
24
|
+
'freeze',
|
|
25
|
+
'editor-ui',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
export function hasPolicyToken(element, token) {
|
|
29
|
+
if (!element || element.nodeType !== 1) return false
|
|
30
|
+
const clay = element.getAttribute?.('clay')
|
|
31
|
+
return !!(
|
|
32
|
+
(clay && clay.split(/\s+/).includes(token)) ||
|
|
33
|
+
element.hasAttribute?.(token)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function expandsTo(element, token) {
|
|
38
|
+
if (hasPolicyToken(element, token)) return true
|
|
39
|
+
for (const [bundle, members] of Object.entries(BUNDLES)) {
|
|
40
|
+
if (members.includes(token) && hasPolicyToken(element, bundle)) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function capabilitySelector(capability) {
|
|
46
|
+
const definition = CAPABILITIES[capability]
|
|
47
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
48
|
+
return [...definition.tokens, ...definition.bundles]
|
|
49
|
+
.flatMap(token => [`[clay~="${token}"]`, `[${token}]`])
|
|
50
|
+
.join(', ')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function closestWithCapability(node, capability) {
|
|
54
|
+
let element = node && node.nodeType === 1 ? node : node?.parentElement
|
|
55
|
+
while (element && element.nodeType === 1) {
|
|
56
|
+
const definition = CAPABILITIES[capability]
|
|
57
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
58
|
+
if (definition.tokens.some(token => expandsTo(element, token)) ||
|
|
59
|
+
definition.bundles.some(bundle => hasPolicyToken(element, bundle))) return element
|
|
60
|
+
element = element.parentElement
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasCapability(node, capability) {
|
|
66
|
+
return !!closestWithCapability(node, capability)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isPolicyAttribute(name) {
|
|
70
|
+
return name === 'clay' || POLICY_TOKENS.includes(name)
|
|
71
|
+
}
|
package/src/write.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import cheerioAdapter from './adapters/cheerio.js'
|
|
2
|
+
import { apply } from './engine/apply.js'
|
|
3
|
+
import { findRulesIn } from './engine/rules-tag.js'
|
|
4
|
+
import { guardAdapter } from './engine/write-policy.js'
|
|
5
|
+
import { planWrite } from './engine/write-plan.js'
|
|
6
|
+
import { trackAdapter, spliceDocument } from './engine/splice.js'
|
|
7
|
+
import { NoRulesTag, WriteRejected } from './engine/errors.js'
|
|
8
|
+
|
|
9
|
+
// Applies a JSON body to a document's source through the document's own rules
|
|
10
|
+
// tag, under the content-only policy. `load` is cheerio.load, passed in so this
|
|
11
|
+
// package keeps cheerio out of its browser bundles. Returns the new source and
|
|
12
|
+
// whether anything changed. Throws NoRulesTag, WriteRejected, WriteRefused,
|
|
13
|
+
// ShapeMismatch or EmptyListInsert, always before producing any output.
|
|
14
|
+
export function writeDocument(load, src, data, { token = 'api' } = {}) {
|
|
15
|
+
const $ = load(src, { sourceCodeLocationInfo: true })
|
|
16
|
+
const root = $.root()
|
|
17
|
+
const found = findRulesIn(cheerioAdapter, root, token)
|
|
18
|
+
if (!found) throw new NoRulesTag(token)
|
|
19
|
+
const { rules } = found
|
|
20
|
+
const plan = planWrite(cheerioAdapter, root, rules, data)
|
|
21
|
+
if (plan.unknownKeys.length || plan.unmatched.length) throw new WriteRejected(plan.unknownKeys, plan.unmatched)
|
|
22
|
+
const tracker = trackAdapter(cheerioAdapter)
|
|
23
|
+
const guarded = guardAdapter(tracker)
|
|
24
|
+
apply(guarded, root, rules, data, { templateAttr: 'cms-template' })
|
|
25
|
+
guarded.assertClean()
|
|
26
|
+
if (tracker.dirty.size === 0) return { html: src, changed: false, spliced: true }
|
|
27
|
+
const { html, spliced } = spliceDocument(load, src, $, tracker)
|
|
28
|
+
return { html, changed: html !== src, spliced }
|
|
29
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
tier: 3
|
|
2
|
-
face: query
|
|
3
|
-
expect: ok
|
|
4
|
-
skip: htmlclay=ships @type fixed; fixed behavior asserted in htmlclay dataapi/props_test.go
|
|
5
|
-
note: @type is NOT the type attribute. `type` is on the DOM property whitelist, and domhandler
|
|
6
|
-
note: nodes already carry a `type` field naming the node kind, so cheerio's prop() returns that
|
|
7
|
-
note: instead: "tag" for a normal element, "script" for a script, "style" for a style. Three
|
|
8
|
-
note: values, none of them the author's type="text".
|
|
9
|
-
note: DIVERGENCE, direction JS-only. This case pins the WART so an upstream fix gets noticed.
|
|
10
|
-
note: htmlclay deliberately does not reproduce it and returns the type attribute, because that API
|
|
11
|
-
note: has no existing htmlclay users and preserving the bug would buy nothing.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|