hyper-html-api 0.5.1 → 0.6.1
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/conformance/MANIFEST.json +6 -0
- package/conformance/cases/basic-scalar-and-list.expected.json +7 -0
- package/conformance/cases/basic-scalar-and-list.html +4 -0
- package/conformance/cases/basic-scalar-and-list.meta +4 -0
- package/conformance/cases/basic-scalar-and-list.parsed.json +4 -0
- package/conformance/cases/basic-scalar-and-list.rules +1 -0
- package/conformance/cases/cms-template-skipped-by-default.expected.json +7 -0
- package/conformance/cases/cms-template-skipped-by-default.html +7 -0
- package/conformance/cases/cms-template-skipped-by-default.meta +7 -0
- package/conformance/cases/cms-template-skipped-by-default.parsed.json +5 -0
- package/conformance/cases/cms-template-skipped-by-default.rules +1 -0
- package/conformance/cases/depth-at-limit-ok.expected.json +41 -0
- package/conformance/cases/depth-at-limit-ok.html +3 -0
- package/conformance/cases/depth-at-limit-ok.meta +6 -0
- package/conformance/cases/depth-at-limit-ok.parsed.json +41 -0
- package/conformance/cases/depth-at-limit-ok.rules +1 -0
- package/conformance/cases/depth-exceeded-path-contains-selector.error.json +5 -0
- package/conformance/cases/depth-exceeded-path-contains-selector.html +3 -0
- package/conformance/cases/depth-exceeded-path-contains-selector.meta +9 -0
- package/conformance/cases/depth-exceeded-path-contains-selector.parsed.json +43 -0
- package/conformance/cases/depth-exceeded-path-contains-selector.rules +1 -0
- package/conformance/cases/depth-exceeded.error.json +5 -0
- package/conformance/cases/depth-exceeded.html +3 -0
- package/conformance/cases/depth-exceeded.meta +6 -0
- package/conformance/cases/depth-exceeded.parsed.json +43 -0
- package/conformance/cases/depth-exceeded.rules +1 -0
- package/conformance/cases/props-boolean-on-any-element.expected.json +5 -0
- package/conformance/cases/props-boolean-on-any-element.html +3 -0
- package/conformance/cases/props-boolean-on-any-element.meta +7 -0
- package/conformance/cases/props-boolean-on-any-element.parsed.json +5 -0
- package/conformance/cases/props-boolean-on-any-element.rules +1 -0
- package/conformance/cases/props-empty-attr-vs-empty-prop.expected.json +4 -0
- package/conformance/cases/props-empty-attr-vs-empty-prop.html +3 -0
- package/conformance/cases/props-empty-attr-vs-empty-prop.meta +6 -0
- package/conformance/cases/props-empty-attr-vs-empty-prop.parsed.json +4 -0
- package/conformance/cases/props-empty-attr-vs-empty-prop.rules +1 -0
- package/conformance/cases/props-null-set.expected.json +9 -0
- package/conformance/cases/props-null-set.html +3 -0
- package/conformance/cases/props-null-set.meta +7 -0
- package/conformance/cases/props-null-set.parsed.json +9 -0
- package/conformance/cases/props-null-set.rules +1 -0
- package/conformance/cases/props-readonly-always-false.expected.json +4 -0
- package/conformance/cases/props-readonly-always-false.html +4 -0
- package/conformance/cases/props-readonly-always-false.meta +11 -0
- package/conformance/cases/props-readonly-always-false.parsed.json +4 -0
- package/conformance/cases/props-readonly-always-false.rules +1 -0
- package/conformance/cases/props-select-textarea-value-null.expected.json +5 -0
- package/conformance/cases/props-select-textarea-value-null.html +5 -0
- package/conformance/cases/props-select-textarea-value-null.meta +8 -0
- package/conformance/cases/props-select-textarea-value-null.parsed.json +5 -0
- package/conformance/cases/props-select-textarea-value-null.rules +1 -0
- package/conformance/cases/props-tagname-and-nodetype.expected.json +5 -0
- package/conformance/cases/props-tagname-and-nodetype.html +3 -0
- package/conformance/cases/props-tagname-and-nodetype.meta +6 -0
- package/conformance/cases/props-tagname-and-nodetype.parsed.json +5 -0
- package/conformance/cases/props-tagname-and-nodetype.rules +1 -0
- package/conformance/cases/props-type-is-node-type.expected.json +5 -0
- package/conformance/cases/props-type-is-node-type.html +5 -0
- package/conformance/cases/props-type-is-node-type.meta +11 -0
- package/conformance/cases/props-type-is-node-type.parsed.json +5 -0
- package/conformance/cases/props-type-is-node-type.rules +1 -0
- package/conformance/cases/rules-duplicate-keys-last-wins.expected.json +3 -0
- package/conformance/cases/rules-duplicate-keys-last-wins.html +4 -0
- package/conformance/cases/rules-duplicate-keys-last-wins.meta +5 -0
- package/conformance/cases/rules-duplicate-keys-last-wins.parsed.json +3 -0
- package/conformance/cases/rules-duplicate-keys-last-wins.rules +1 -0
- package/conformance/cases/rules-iterate-dot-leading-works.expected.json +10 -0
- package/conformance/cases/rules-iterate-dot-leading-works.html +3 -0
- package/conformance/cases/rules-iterate-dot-leading-works.meta +6 -0
- package/conformance/cases/rules-iterate-dot-leading-works.parsed.json +8 -0
- package/conformance/cases/rules-iterate-dot-leading-works.rules +1 -0
- package/conformance/cases/rules-iterate-lookahead-quoted.error.json +5 -0
- package/conformance/cases/rules-iterate-lookahead-quoted.html +3 -0
- package/conformance/cases/rules-iterate-lookahead-quoted.meta +7 -0
- package/conformance/cases/rules-iterate-lookahead-quoted.rules +1 -0
- package/conformance/cases/rules-iterate-lookahead-unquoted.error.json +5 -0
- package/conformance/cases/rules-iterate-lookahead-unquoted.html +3 -0
- package/conformance/cases/rules-iterate-lookahead-unquoted.meta +8 -0
- package/conformance/cases/rules-iterate-lookahead-unquoted.parsed.json +3 -0
- package/conformance/cases/rules-iterate-lookahead-unquoted.rules +1 -0
- package/conformance/cases/rules-lastindexof-at-split.expected.json +3 -0
- package/conformance/cases/rules-lastindexof-at-split.html +3 -0
- package/conformance/cases/rules-lastindexof-at-split.meta +11 -0
- package/conformance/cases/rules-lastindexof-at-split.parsed.json +3 -0
- package/conformance/cases/rules-lastindexof-at-split.rules +1 -0
- package/conformance/cases/rules-primitives-extract-null.expected.json +5 -0
- package/conformance/cases/rules-primitives-extract-null.html +3 -0
- package/conformance/cases/rules-primitives-extract-null.meta +6 -0
- package/conformance/cases/rules-primitives-extract-null.parsed.json +5 -0
- package/conformance/cases/rules-primitives-extract-null.rules +1 -0
- package/conformance/cases/rules-proto-key-omitted.expected.json +3 -0
- package/conformance/cases/rules-proto-key-omitted.html +3 -0
- package/conformance/cases/rules-proto-key-omitted.meta +7 -0
- package/conformance/cases/rules-proto-key-omitted.parsed.json +6 -0
- package/conformance/cases/rules-proto-key-omitted.rules +1 -0
- package/conformance/cases/rules-quoted-attr-in-unquoted-selector.error.json +5 -0
- package/conformance/cases/rules-quoted-attr-in-unquoted-selector.html +4 -0
- package/conformance/cases/rules-quoted-attr-in-unquoted-selector.meta +5 -0
- package/conformance/cases/rules-quoted-attr-in-unquoted-selector.rules +1 -0
- package/conformance/cases/rules-trailing-comma-dropped.expected.json +3 -0
- package/conformance/cases/rules-trailing-comma-dropped.html +3 -0
- package/conformance/cases/rules-trailing-comma-dropped.meta +5 -0
- package/conformance/cases/rules-trailing-comma-dropped.parsed.json +3 -0
- package/conformance/cases/rules-trailing-comma-dropped.rules +1 -0
- package/conformance/cases/rules-unwhitelisted-pseudo-breaks-tokenizer.error.json +5 -0
- package/conformance/cases/rules-unwhitelisted-pseudo-breaks-tokenizer.html +3 -0
- package/conformance/cases/rules-unwhitelisted-pseudo-breaks-tokenizer.meta +7 -0
- package/conformance/cases/rules-unwhitelisted-pseudo-breaks-tokenizer.rules +1 -0
- package/conformance/cases/rules-whitespace-clings.expected.json +4 -0
- package/conformance/cases/rules-whitespace-clings.html +4 -0
- package/conformance/cases/rules-whitespace-clings.meta +6 -0
- package/conformance/cases/rules-whitespace-clings.parsed.json +4 -0
- package/conformance/cases/rules-whitespace-clings.rules +1 -0
- package/conformance/cases/selector-unknown-pseudo-is-500.error.json +5 -0
- package/conformance/cases/selector-unknown-pseudo-is-500.html +4 -0
- package/conformance/cases/selector-unknown-pseudo-is-500.meta +7 -0
- package/conformance/cases/selector-unknown-pseudo-is-500.parsed.json +3 -0
- package/conformance/cases/selector-unknown-pseudo-is-500.rules +1 -0
- package/conformance/cases/tag-basic.expected.json +7 -0
- package/conformance/cases/tag-basic.html +5 -0
- package/conformance/cases/tag-basic.meta +6 -0
- package/conformance/cases/tag-basic.parsed.json +4 -0
- package/conformance/cases/tag-duplicate-names-first-wins.expected.json +3 -0
- package/conformance/cases/tag-duplicate-names-first-wins.html +6 -0
- package/conformance/cases/tag-duplicate-names-first-wins.meta +7 -0
- package/conformance/cases/tag-duplicate-names-first-wins.parsed.json +3 -0
- package/conformance/cases/tag-missing.error.json +5 -0
- package/conformance/cases/tag-missing.html +4 -0
- package/conformance/cases/tag-missing.meta +8 -0
- package/conformance/cases/tag-name-is-space-separated.expected.json +3 -0
- package/conformance/cases/tag-name-is-space-separated.html +4 -0
- package/conformance/cases/tag-name-is-space-separated.meta +6 -0
- package/conformance/cases/tag-name-is-space-separated.parsed.json +3 -0
- package/conformance/cases/tag-relaxed-body.expected.json +7 -0
- package/conformance/cases/tag-relaxed-body.html +5 -0
- package/conformance/cases/tag-relaxed-body.meta +7 -0
- package/conformance/cases/tag-relaxed-body.parsed.json +4 -0
- package/conformance/cases/tag-wrong-version.error.json +5 -0
- package/conformance/cases/tag-wrong-version.html +4 -0
- package/conformance/cases/tag-wrong-version.meta +6 -0
- package/conformance/cases/template-simple-vs-combinator.expected.json +7 -0
- package/conformance/cases/template-simple-vs-combinator.html +4 -0
- package/conformance/cases/template-simple-vs-combinator.meta +8 -0
- package/conformance/cases/template-simple-vs-combinator.parsed.json +4 -0
- package/conformance/cases/template-simple-vs-combinator.rules +1 -0
- package/conformance/cases/text-root-leaks-rules-tag.expected.json +4 -0
- package/conformance/cases/text-root-leaks-rules-tag.html +4 -0
- package/conformance/cases/text-root-leaks-rules-tag.meta +7 -0
- package/conformance/cases/text-root-leaks-rules-tag.parsed.json +4 -0
- package/conformance/cases/text-root-leaks-rules-tag.rules +1 -0
- package/conformance/cases/text-trim-asymmetry.expected.json +5 -0
- package/conformance/cases/text-trim-asymmetry.html +5 -0
- package/conformance/cases/text-trim-asymmetry.meta +6 -0
- package/conformance/cases/text-trim-asymmetry.parsed.json +5 -0
- package/conformance/cases/text-trim-asymmetry.rules +1 -0
- package/dist/hyper-html-api.engine.min.js +1 -1
- package/dist/hyper-html-api.min.js +4 -3
- package/dist/hyper-html-api.upgrade.min.js +2 -2
- package/package.json +13 -5
- package/src/cms/form-builder.js +3 -2
- package/src/dom-api.js +5 -0
- package/src/engine/apply.js +3 -2
- package/src/engine/diff.js +70 -13
- package/src/engine/extract.js +3 -2
- package/src/engine/index.js +1 -0
- package/src/engine/rule-syntax.js +43 -0
package/src/engine/diff.js
CHANGED
|
@@ -48,14 +48,18 @@ export function listDiff(adapter, parentCtx, selector, shape, newItems, trace, a
|
|
|
48
48
|
const anchorIdx = oldNodes.length > 0
|
|
49
49
|
? indexInParent(adapter, parent, referenceNode)
|
|
50
50
|
: 0
|
|
51
|
+
const contiguous = isContiguousRun(adapter, oldNodes)
|
|
51
52
|
|
|
52
53
|
const used = new Set()
|
|
54
|
+
const fresh = []
|
|
53
55
|
const finalNodes = newItems.map((_, i) => {
|
|
54
56
|
const oldIdx = matches[i]
|
|
55
57
|
if (oldIdx >= 0) {
|
|
56
58
|
used.add(oldIdx)
|
|
59
|
+
fresh.push(false)
|
|
57
60
|
return oldNodes[oldIdx]
|
|
58
61
|
}
|
|
62
|
+
fresh.push(true)
|
|
59
63
|
const cloned = adapter.clone(template)
|
|
60
64
|
adapter.stripIds(cloned)
|
|
61
65
|
return cloned
|
|
@@ -67,20 +71,36 @@ export function listDiff(adapter, parentCtx, selector, shape, newItems, trace, a
|
|
|
67
71
|
if (!used.has(i)) adapter.remove(n)
|
|
68
72
|
})
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
if (contiguous) {
|
|
75
|
+
// Place each final node at its target index. If the node is already
|
|
76
|
+
// there, do nothing (no-op apply on unchanged data drops zero state).
|
|
77
|
+
// Otherwise insertAt moves an attached node (DOM/cheerio both treat
|
|
78
|
+
// insertBefore on an attached node as "move to here").
|
|
79
|
+
finalNodes.forEach((node, i) => {
|
|
80
|
+
const targetIdx = anchorIdx + i
|
|
81
|
+
const siblings = adapter.children(parent)
|
|
82
|
+
const currentIdx = siblings.findIndex((s) => adapter.sameNode(s, node))
|
|
83
|
+
if (currentIdx === targetIdx) return
|
|
84
|
+
adapter.insertAt(parent, node, targetIdx)
|
|
85
|
+
})
|
|
86
|
+
writeItems(adapter, finalNodes, shape, newItems, trace, applyItem, opts)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The matched nodes are not a contiguous run of siblings, so this list has no
|
|
91
|
+
// DOM order to restore: any repositioning would move elements the list does
|
|
92
|
+
// not own. Anchoring every node to parent(oldNodes[0]) is what reparents a
|
|
93
|
+
// cross-parent list into its first container and compacts past unowned
|
|
94
|
+
// siblings, and both destroy content on a write that changed nothing. Leave
|
|
95
|
+
// every surviving node exactly where the author put it and only place the
|
|
96
|
+
// grown ones.
|
|
97
|
+
placeGrownItems(adapter, finalNodes, fresh, parent, anchorIdx)
|
|
98
|
+
writeItems(adapter, finalNodes, shape, newItems, trace, applyItem, opts)
|
|
99
|
+
}
|
|
81
100
|
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
// Apply per-item content. Skip when the existing value already matches
|
|
102
|
+
// — saves a write on no-op applies and avoids spurious mutation events.
|
|
103
|
+
function writeItems(adapter, finalNodes, shape, newItems, trace, applyItem, opts) {
|
|
84
104
|
finalNodes.forEach((node, i) => {
|
|
85
105
|
if (shape === null) {
|
|
86
106
|
const v = newItems[i]
|
|
@@ -167,3 +187,40 @@ function findFallbackTemplate(adapter, parentCtx, selector, opts) {
|
|
|
167
187
|
}
|
|
168
188
|
return null
|
|
169
189
|
}
|
|
190
|
+
|
|
191
|
+
// Repositioning only means something when the list occupies a contiguous run of
|
|
192
|
+
// element siblings under one parent. Two containers, or an unowned sibling
|
|
193
|
+
// sitting between two owned nodes, has no list order to restore.
|
|
194
|
+
function isContiguousRun(adapter, nodes) {
|
|
195
|
+
if (nodes.length <= 1) return true
|
|
196
|
+
const parent = adapter.parent(nodes[0])
|
|
197
|
+
if (!parent) return false
|
|
198
|
+
const siblings = adapter.children(parent)
|
|
199
|
+
const indices = []
|
|
200
|
+
for (const node of nodes) {
|
|
201
|
+
const i = siblings.findIndex((s) => adapter.sameNode(s, node))
|
|
202
|
+
if (i === -1) return false
|
|
203
|
+
indices.push(i)
|
|
204
|
+
}
|
|
205
|
+
indices.sort((a, b) => a - b)
|
|
206
|
+
return indices[indices.length - 1] - indices[0] === indices.length - 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Grown items follow the surviving node before them, so a new row lands next to
|
|
210
|
+
// its neighbour rather than being teleported into the first container. With no
|
|
211
|
+
// surviving node before it, fall back to where the list started.
|
|
212
|
+
function placeGrownItems(adapter, finalNodes, fresh, fallbackParent, fallbackIdx) {
|
|
213
|
+
let anchor = null
|
|
214
|
+
let nextFallbackIdx = fallbackIdx
|
|
215
|
+
for (let i = 0; i < finalNodes.length; i++) {
|
|
216
|
+
if (!fresh[i]) {
|
|
217
|
+
anchor = finalNodes[i]
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
const parent = anchor ? adapter.parent(anchor) : fallbackParent
|
|
221
|
+
if (!parent) continue
|
|
222
|
+
const at = anchor ? indexInParent(adapter, parent, anchor) + 1 : nextFallbackIdx++
|
|
223
|
+
adapter.insertAt(parent, finalNodes[i], at)
|
|
224
|
+
anchor = finalNodes[i]
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/engine/extract.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DOM_PROPERTIES_SET } from './dom-properties.js'
|
|
2
2
|
import { MaxRuleDepthExceeded, MAX_RULE_DEPTH } from './errors.js'
|
|
3
|
+
import { ruleAttrIndex } from './rule-syntax.js'
|
|
3
4
|
|
|
4
5
|
export function extract(adapter, root, rules, opts = {}) {
|
|
5
6
|
return extractAt(adapter, root, rules, { depth: 0, path: [] }, opts)
|
|
@@ -45,8 +46,8 @@ function extractScalar(adapter, ctx, rule, opts) {
|
|
|
45
46
|
return readPropOrAttr(adapter, ctx, rule.slice(1))
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const at = ruleAttrIndex(rule)
|
|
50
|
+
if (at !== -1) {
|
|
50
51
|
const selector = rule.slice(0, at)
|
|
51
52
|
const name = rule.slice(at + 1)
|
|
52
53
|
const matches = selector ? adapter.find(ctx, selector, opts) : [ctx]
|
package/src/engine/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { extract } from './extract.js'
|
|
|
6
6
|
export { apply } from './apply.js'
|
|
7
7
|
export { findRulesIn } from './rules-tag.js'
|
|
8
8
|
export { parseStrict, parseRelaxed } from './rules.js'
|
|
9
|
+
export { ruleAttrIndex, splitRule } from './rule-syntax.js'
|
|
9
10
|
export * as errors from './errors.js'
|
|
10
11
|
export { DOM_PROPERTIES, DOM_PROPERTIES_SET } from './dom-properties.js'
|
|
11
12
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The scalar rule grammar: "<selector>@<prop>", plus a trailing "[]" for a list
|
|
2
|
+
// of values, a leading "@" for a prop on the context node, and "." for its text.
|
|
3
|
+
//
|
|
4
|
+
// Locating the "@" is the only subtle part of it. It is the LAST one, because a
|
|
5
|
+
// prop name never contains "@" but a selector often does: a mailto link
|
|
6
|
+
// (a[href="mailto:hi@example.com"]), a container query, a Tailwind arbitrary
|
|
7
|
+
// variant. Every one of those carries its "@" inside brackets, parens or
|
|
8
|
+
// quotes, so a separator is an "@" at bracket depth zero and outside any quote.
|
|
9
|
+
// Scanning with a plain lastIndexOf finds the one in the mailto and splits the
|
|
10
|
+
// rule into `a[href="mailto:hi` and `example.com"]`, which reads downstream as
|
|
11
|
+
// an invalid selector when the rule the author wrote was correct.
|
|
12
|
+
|
|
13
|
+
// Index of the separator "@", or -1 when the rule has no prop part.
|
|
14
|
+
export function ruleAttrIndex(rule) {
|
|
15
|
+
let depth = 0
|
|
16
|
+
let quote = null
|
|
17
|
+
let found = -1
|
|
18
|
+
for (let i = 0; i < rule.length; i++) {
|
|
19
|
+
const ch = rule[i]
|
|
20
|
+
if (ch === '\\') {
|
|
21
|
+
i++
|
|
22
|
+
} else if (quote) {
|
|
23
|
+
if (ch === quote) quote = null
|
|
24
|
+
} else if (ch === '"' || ch === "'") {
|
|
25
|
+
quote = ch
|
|
26
|
+
} else if (ch === '[' || ch === '(') {
|
|
27
|
+
depth++
|
|
28
|
+
} else if (ch === ']' || ch === ')') {
|
|
29
|
+
if (depth > 0) depth--
|
|
30
|
+
} else if (ch === '@' && depth === 0) {
|
|
31
|
+
found = i
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return found
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// "<selector>@<prop>" split at that separator. selector is "" for a leading "@"
|
|
38
|
+
// (the context node itself); prop is null when the rule names no prop.
|
|
39
|
+
export function splitRule(rule) {
|
|
40
|
+
const at = ruleAttrIndex(rule)
|
|
41
|
+
if (at === -1) return { selector: rule, prop: null }
|
|
42
|
+
return { selector: rule.slice(0, at), prop: rule.slice(at + 1) || null }
|
|
43
|
+
}
|