hyper-html-api 0.8.1 → 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.
Files changed (217) hide show
  1. package/README.md +28 -0
  2. package/conformance/MANIFEST.json +2 -2
  3. package/conformance/cases/no-data-list-skipped.expected.json +6 -0
  4. package/conformance/cases/no-data-list-skipped.html +1 -0
  5. package/conformance/cases/no-data-list-skipped.meta +4 -0
  6. package/conformance/cases/no-data-list-skipped.parsed.json +3 -0
  7. package/conformance/cases/no-data-list-skipped.rules +1 -0
  8. package/conformance/cases/no-data-text-projected.expected.json +3 -0
  9. package/conformance/cases/no-data-text-projected.html +1 -0
  10. package/conformance/cases/no-data-text-projected.meta +4 -0
  11. package/conformance/cases/no-data-text-projected.parsed.json +3 -0
  12. package/conformance/cases/no-data-text-projected.rules +1 -0
  13. package/conformance/cases/props-type-is-attribute.expected.json +5 -0
  14. package/conformance/cases/props-type-is-attribute.meta +4 -0
  15. package/conformance/cases/write-attr-app-scheme.after.html +1 -0
  16. package/conformance/cases/write-attr-app-scheme.after.json +3 -0
  17. package/conformance/cases/write-attr-app-scheme.data.json +1 -0
  18. package/conformance/cases/write-attr-app-scheme.html +1 -0
  19. package/conformance/cases/write-attr-app-scheme.meta +4 -0
  20. package/conformance/cases/write-attr-app-scheme.write.json +4 -0
  21. package/conformance/cases/write-attr-href.after.html +1 -0
  22. package/conformance/cases/write-attr-href.after.json +3 -0
  23. package/conformance/cases/write-attr-href.data.json +1 -0
  24. package/conformance/cases/write-attr-href.html +1 -0
  25. package/conformance/cases/write-attr-href.meta +4 -0
  26. package/conformance/cases/write-attr-href.write.json +4 -0
  27. package/conformance/cases/write-checked-false.after.html +1 -0
  28. package/conformance/cases/write-checked-false.after.json +3 -0
  29. package/conformance/cases/write-checked-false.data.json +1 -0
  30. package/conformance/cases/write-checked-false.html +1 -0
  31. package/conformance/cases/write-checked-false.meta +4 -0
  32. package/conformance/cases/write-checked-false.write.json +4 -0
  33. package/conformance/cases/write-checked-true.after.html +1 -0
  34. package/conformance/cases/write-checked-true.after.json +3 -0
  35. package/conformance/cases/write-checked-true.data.json +1 -0
  36. package/conformance/cases/write-checked-true.html +1 -0
  37. package/conformance/cases/write-checked-true.meta +4 -0
  38. package/conformance/cases/write-checked-true.write.json +4 -0
  39. package/conformance/cases/write-crlf-multi.after.html +12 -0
  40. package/conformance/cases/write-crlf-multi.after.json +8 -0
  41. package/conformance/cases/write-crlf-multi.data.json +1 -0
  42. package/conformance/cases/write-crlf-multi.html +12 -0
  43. package/conformance/cases/write-crlf-multi.meta +4 -0
  44. package/conformance/cases/write-crlf-multi.write.json +4 -0
  45. package/conformance/cases/write-fallback-implied.after.html +1 -0
  46. package/conformance/cases/write-fallback-implied.after.json +3 -0
  47. package/conformance/cases/write-fallback-implied.data.json +1 -0
  48. package/conformance/cases/write-fallback-implied.html +1 -0
  49. package/conformance/cases/write-fallback-implied.meta +4 -0
  50. package/conformance/cases/write-fallback-implied.write.json +4 -0
  51. package/conformance/cases/write-flatten.after.html +1 -0
  52. package/conformance/cases/write-flatten.after.json +3 -0
  53. package/conformance/cases/write-flatten.data.json +1 -0
  54. package/conformance/cases/write-flatten.html +1 -0
  55. package/conformance/cases/write-flatten.meta +4 -0
  56. package/conformance/cases/write-flatten.write.json +4 -0
  57. package/conformance/cases/write-list-empty-no-seed.data.json +1 -0
  58. package/conformance/cases/write-list-empty-no-seed.error.json +8 -0
  59. package/conformance/cases/write-list-empty-no-seed.html +1 -0
  60. package/conformance/cases/write-list-empty-no-seed.meta +4 -0
  61. package/conformance/cases/write-list-grow-from-template.after.html +1 -0
  62. package/conformance/cases/write-list-grow-from-template.after.json +5 -0
  63. package/conformance/cases/write-list-grow-from-template.data.json +1 -0
  64. package/conformance/cases/write-list-grow-from-template.html +1 -0
  65. package/conformance/cases/write-list-grow-from-template.meta +4 -0
  66. package/conformance/cases/write-list-grow-from-template.write.json +4 -0
  67. package/conformance/cases/write-list-object-clone.after.html +1 -0
  68. package/conformance/cases/write-list-object-clone.after.json +12 -0
  69. package/conformance/cases/write-list-object-clone.data.json +1 -0
  70. package/conformance/cases/write-list-object-clone.html +1 -0
  71. package/conformance/cases/write-list-object-clone.meta +4 -0
  72. package/conformance/cases/write-list-object-clone.write.json +4 -0
  73. package/conformance/cases/write-list-scalar-grow.after.html +4 -0
  74. package/conformance/cases/write-list-scalar-grow.after.json +7 -0
  75. package/conformance/cases/write-list-scalar-grow.data.json +1 -0
  76. package/conformance/cases/write-list-scalar-grow.html +4 -0
  77. package/conformance/cases/write-list-scalar-grow.meta +4 -0
  78. package/conformance/cases/write-list-scalar-grow.write.json +4 -0
  79. package/conformance/cases/write-list-scalar-shrink.after.html +4 -0
  80. package/conformance/cases/write-list-scalar-shrink.after.json +5 -0
  81. package/conformance/cases/write-list-scalar-shrink.data.json +1 -0
  82. package/conformance/cases/write-list-scalar-shrink.html +4 -0
  83. package/conformance/cases/write-list-scalar-shrink.meta +4 -0
  84. package/conformance/cases/write-list-scalar-shrink.write.json +4 -0
  85. package/conformance/cases/write-list-shrink-zero.after.html +1 -0
  86. package/conformance/cases/write-list-shrink-zero.after.json +3 -0
  87. package/conformance/cases/write-list-shrink-zero.data.json +1 -0
  88. package/conformance/cases/write-list-shrink-zero.html +1 -0
  89. package/conformance/cases/write-list-shrink-zero.meta +4 -0
  90. package/conformance/cases/write-list-shrink-zero.write.json +4 -0
  91. package/conformance/cases/write-no-data-kept.after.html +1 -0
  92. package/conformance/cases/write-no-data-kept.after.json +3 -0
  93. package/conformance/cases/write-no-data-kept.data.json +1 -0
  94. package/conformance/cases/write-no-data-kept.html +1 -0
  95. package/conformance/cases/write-no-data-kept.meta +4 -0
  96. package/conformance/cases/write-no-data-kept.write.json +4 -0
  97. package/conformance/cases/write-no-rules-tag.data.json +1 -0
  98. package/conformance/cases/write-no-rules-tag.error.json +6 -0
  99. package/conformance/cases/write-no-rules-tag.html +1 -0
  100. package/conformance/cases/write-no-rules-tag.meta +4 -0
  101. package/conformance/cases/write-noop.after.html +3 -0
  102. package/conformance/cases/write-noop.after.json +3 -0
  103. package/conformance/cases/write-noop.data.json +1 -0
  104. package/conformance/cases/write-noop.html +3 -0
  105. package/conformance/cases/write-noop.meta +4 -0
  106. package/conformance/cases/write-noop.write.json +4 -0
  107. package/conformance/cases/write-outerhtml-unchanged.after.html +1 -0
  108. package/conformance/cases/write-outerhtml-unchanged.after.json +3 -0
  109. package/conformance/cases/write-outerhtml-unchanged.data.json +1 -0
  110. package/conformance/cases/write-outerhtml-unchanged.html +1 -0
  111. package/conformance/cases/write-outerhtml-unchanged.meta +4 -0
  112. package/conformance/cases/write-outerhtml-unchanged.write.json +4 -0
  113. package/conformance/cases/write-readonly-changed.data.json +1 -0
  114. package/conformance/cases/write-readonly-changed.error.json +6 -0
  115. package/conformance/cases/write-readonly-changed.html +1 -0
  116. package/conformance/cases/write-readonly-changed.meta +4 -0
  117. package/conformance/cases/write-readonly-unchanged.after.html +1 -0
  118. package/conformance/cases/write-readonly-unchanged.after.json +4 -0
  119. package/conformance/cases/write-readonly-unchanged.data.json +1 -0
  120. package/conformance/cases/write-readonly-unchanged.html +1 -0
  121. package/conformance/cases/write-readonly-unchanged.meta +4 -0
  122. package/conformance/cases/write-readonly-unchanged.write.json +4 -0
  123. package/conformance/cases/write-refused-animate.data.json +1 -0
  124. package/conformance/cases/write-refused-animate.error.json +11 -0
  125. package/conformance/cases/write-refused-animate.html +1 -0
  126. package/conformance/cases/write-refused-animate.meta +4 -0
  127. package/conformance/cases/write-refused-attr-name.data.json +1 -0
  128. package/conformance/cases/write-refused-attr-name.error.json +11 -0
  129. package/conformance/cases/write-refused-attr-name.html +1 -0
  130. package/conformance/cases/write-refused-attr-name.meta +4 -0
  131. package/conformance/cases/write-refused-data-url.data.json +1 -0
  132. package/conformance/cases/write-refused-data-url.error.json +11 -0
  133. package/conformance/cases/write-refused-data-url.html +1 -0
  134. package/conformance/cases/write-refused-data-url.meta +4 -0
  135. package/conformance/cases/write-refused-innerhtml.data.json +1 -0
  136. package/conformance/cases/write-refused-innerhtml.error.json +11 -0
  137. package/conformance/cases/write-refused-innerhtml.html +1 -0
  138. package/conformance/cases/write-refused-innerhtml.meta +4 -0
  139. package/conformance/cases/write-refused-javascript-url.data.json +1 -0
  140. package/conformance/cases/write-refused-javascript-url.error.json +11 -0
  141. package/conformance/cases/write-refused-javascript-url.html +1 -0
  142. package/conformance/cases/write-refused-javascript-url.meta +4 -0
  143. package/conformance/cases/write-refused-list-removes-rules-tag.data.json +1 -0
  144. package/conformance/cases/write-refused-list-removes-rules-tag.error.json +11 -0
  145. package/conformance/cases/write-refused-list-removes-rules-tag.html +1 -0
  146. package/conformance/cases/write-refused-list-removes-rules-tag.meta +4 -0
  147. package/conformance/cases/write-refused-multiple.data.json +1 -0
  148. package/conformance/cases/write-refused-multiple.error.json +15 -0
  149. package/conformance/cases/write-refused-multiple.html +1 -0
  150. package/conformance/cases/write-refused-multiple.meta +4 -0
  151. package/conformance/cases/write-refused-onclick.data.json +1 -0
  152. package/conformance/cases/write-refused-onclick.error.json +11 -0
  153. package/conformance/cases/write-refused-onclick.html +1 -0
  154. package/conformance/cases/write-refused-onclick.meta +4 -0
  155. package/conformance/cases/write-refused-rules-tag-ancestor.data.json +1 -0
  156. package/conformance/cases/write-refused-rules-tag-ancestor.error.json +11 -0
  157. package/conformance/cases/write-refused-rules-tag-ancestor.html +1 -0
  158. package/conformance/cases/write-refused-rules-tag-ancestor.meta +4 -0
  159. package/conformance/cases/write-refused-script.data.json +1 -0
  160. package/conformance/cases/write-refused-script.error.json +11 -0
  161. package/conformance/cases/write-refused-script.html +1 -0
  162. package/conformance/cases/write-refused-script.meta +4 -0
  163. package/conformance/cases/write-shape-mismatch.data.json +1 -0
  164. package/conformance/cases/write-shape-mismatch.error.json +12 -0
  165. package/conformance/cases/write-shape-mismatch.html +1 -0
  166. package/conformance/cases/write-shape-mismatch.meta +4 -0
  167. package/conformance/cases/write-text-basic.after.html +3 -0
  168. package/conformance/cases/write-text-basic.after.json +3 -0
  169. package/conformance/cases/write-text-basic.data.json +1 -0
  170. package/conformance/cases/write-text-basic.html +3 -0
  171. package/conformance/cases/write-text-basic.meta +4 -0
  172. package/conformance/cases/write-text-basic.write.json +4 -0
  173. package/conformance/cases/write-text-escape.after.html +1 -0
  174. package/conformance/cases/write-text-escape.after.json +3 -0
  175. package/conformance/cases/write-text-escape.data.json +1 -0
  176. package/conformance/cases/write-text-escape.html +1 -0
  177. package/conformance/cases/write-text-escape.meta +4 -0
  178. package/conformance/cases/write-text-escape.write.json +4 -0
  179. package/conformance/cases/write-text-null-clears.after.html +1 -0
  180. package/conformance/cases/write-text-null-clears.after.json +3 -0
  181. package/conformance/cases/write-text-null-clears.data.json +1 -0
  182. package/conformance/cases/write-text-null-clears.html +1 -0
  183. package/conformance/cases/write-text-null-clears.meta +4 -0
  184. package/conformance/cases/write-text-null-clears.write.json +4 -0
  185. package/conformance/cases/write-type-attribute.after.html +1 -0
  186. package/conformance/cases/write-type-attribute.after.json +3 -0
  187. package/conformance/cases/write-type-attribute.data.json +1 -0
  188. package/conformance/cases/write-type-attribute.html +1 -0
  189. package/conformance/cases/write-type-attribute.meta +4 -0
  190. package/conformance/cases/write-type-attribute.write.json +4 -0
  191. package/conformance/cases/write-unknown-key.data.json +1 -0
  192. package/conformance/cases/write-unknown-key.error.json +11 -0
  193. package/conformance/cases/write-unknown-key.html +3 -0
  194. package/conformance/cases/write-unknown-key.meta +4 -0
  195. package/conformance/cases/write-unmatched.data.json +1 -0
  196. package/conformance/cases/write-unmatched.error.json +14 -0
  197. package/conformance/cases/write-unmatched.html +1 -0
  198. package/conformance/cases/write-unmatched.meta +4 -0
  199. package/conformance/write-policy.json +78 -0
  200. package/dist/hyper-html-api.engine.min.js +1 -1
  201. package/dist/hyper-html-api.min.js +5 -4
  202. package/dist/hyper-html-api.upgrade.min.js +2 -2
  203. package/package.json +4 -3
  204. package/packed-contract.json +1 -1
  205. package/src/adapters/cheerio.js +105 -10
  206. package/src/engine/apply.js +2 -0
  207. package/src/engine/errors.js +25 -0
  208. package/src/engine/index.js +3 -0
  209. package/src/engine/splice.js +118 -0
  210. package/src/engine/write-plan.js +52 -0
  211. package/src/engine/write-policy.js +136 -0
  212. package/src/write.js +29 -0
  213. package/conformance/cases/props-type-is-node-type.expected.json +0 -5
  214. package/conformance/cases/props-type-is-node-type.meta +0 -11
  215. /package/conformance/cases/{props-type-is-node-type.html → props-type-is-attribute.html} +0 -0
  216. /package/conformance/cases/{props-type-is-node-type.parsed.json → props-type-is-attribute.parsed.json} +0 -0
  217. /package/conformance/cases/{props-type-is-node-type.rules → props-type-is-attribute.rules} +0 -0
@@ -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
+ }
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,5 +0,0 @@
1
- {
2
- "input": "tag",
3
- "script": "script",
4
- "style": "style"
5
- }
@@ -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.