Improved types, nth-child(n of selector).
Showing
7 changed files
with
134 additions
and
61 deletions
... | @@ -5,3 +5,7 @@ import Node from './node.astro' | ... | @@ -5,3 +5,7 @@ import Node from './node.astro' |
5 | import Replace from './replace.astro' | 5 | import Replace from './replace.astro' |
6 | 6 | ||
7 | export { Children, Custom, Match, Node, Replace } | 7 | export { Children, Custom, Match, Node, Replace } |
8 | |||
9 | import type { NodeType } from 'ultrahtml' | ||
10 | type SlotHandler = (string, NodeType) => Promise<Any> | ||
11 | type Replacements = Record<string, string> | ... | ... |
... | @@ -32,71 +32,94 @@ export const appendClasses = (node, extraClasses) => { | ... | @@ -32,71 +32,94 @@ export const appendClasses = (node, extraClasses) => { |
32 | } | 32 | } |
33 | 33 | ||
34 | // TODO: implement a parent/child/element cache | 34 | // TODO: implement a parent/child/element cache |
35 | const filterChildElements = (parent) => parent?.children?.filter(n => n.type === ELEMENT_NODE) || [] | ||
36 | const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node); | 35 | const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node); |
37 | 36 | ||
37 | const filterChildElementsMatcher = (context, child, parent, i) => child.type === ELEMENT_NODE | ||
38 | |||
38 | type Matcher = (context, node, parent, i, debug) => boolean | 39 | type Matcher = (context, node, parent, i, debug) => boolean |
39 | type AttrValueMatch = (string) => string | 40 | type AttrValueMatch = (string) => string |
41 | type ParentFilter = (context, node, parent, i) => boolean | ||
40 | 42 | ||
41 | const makeNthChildPosMatcher = (ast: AST): Matcher => { | 43 | const compileMatcher = (ast: AST, selector: string): Matcher => { |
42 | const { argument } = ast | 44 | let counter = 0 |
43 | const n = Number(argument) | 45 | |
44 | if (!Number.isNaN(n)) { | 46 | const neededContext = [] |
45 | return (context, node, parent, i, debug) => { | 47 | const selectorCacheCounter = counter++ |
46 | return i === n | 48 | neededContext[ selectorCacheCounter ] = () => new WeakMap() |
47 | } | 49 | |
50 | const findChildren = (context, parent: NodeType, selector: string, matcher: Matcher): array[NodeType] => { | ||
51 | if (parent === null) console.log('null parent', new Error()) | ||
52 | if (parent === null) return [] | ||
53 | let selectorCache = context[ selectorCacheCounter ].get(parent) | ||
54 | if (!selectorCache) context[ selectorCacheCounter ].set(parent, selectorCache = {}) | ||
55 | const selectorResult = selectorCache[ selector ] | ||
56 | if (selectorResult) return selectorResult | ||
57 | const newResult = parent?.children?.filter((child, index) => matcher(context, child, parent, index)) || [] | ||
58 | selectorCache[ selector ] = newResult | ||
59 | return newResult | ||
48 | } | 60 | } |
49 | switch (argument) { | 61 | |
50 | case 'odd': | 62 | const makeNthChildPosMatcher = (argument: string): Matcher => { |
51 | return (context, node, parent, i, debug) => Math.abs(i % 2) === 1 | 63 | const n = Number(argument) |
52 | case 'even': | 64 | if (!Number.isNaN(n)) { |
53 | return (context, node, parent, i) => i % 2 === 0 | 65 | // Simple variant, just a number |
54 | default: { | 66 | return (context, node, parent, i, debug) => { |
55 | if (!argument) throw new Error(`Unsupported empty nth-child selector!`) | 67 | return i === n |
56 | let [_, A = '1', B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? [] | 68 | } |
57 | if (A.length === 0) A = '1' | 69 | } |
58 | const a = Number.parseInt(A === '-' ? '-1' : A) | 70 | |
59 | const b = Number.parseInt(B) | 71 | switch (argument) { |
60 | const nth = (index) => (a * n) + b | 72 | case 'odd': |
61 | return (context, node, parent, i) => { | 73 | return (context, node, parent, i, debug) => Math.abs(i % 2) === 1 |
62 | const elements = filterChildElements(parent) | 74 | case 'even': |
63 | for (let index = 0; index < elements.length; index++) { | 75 | return (context, node, parent, i) => i % 2 === 0 |
64 | const n = nth(index) | 76 | default: { |
65 | if (n > elements.length) return false | 77 | if (!argument) throw new Error(`Unsupported empty nth-child selector!`) |
66 | if (n === i) return true | 78 | let [_, A, B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? [] |
79 | const b = Number.parseInt(B) | ||
80 | // (index) => (index - b) / a | ||
81 | let nMatch | ||
82 | if (A === undefined || A === '0' || A === '-0') { | ||
83 | nMatch = (i) => (i - b) === 0 | ||
84 | } else { | ||
85 | const a = A === '' ? 1 : A === '-' ? -1 : Number.parseInt(A) | ||
86 | if (a < 0) { | ||
87 | nMatch = (i) => { const n = -(i - b) / a; return n !== 0 && Math.floor(n) === n } | ||
88 | } else { | ||
89 | nMatch = (i) => { const n = (i - b) / a; return n !== 0 && Math.floor(n) === n } | ||
90 | } | ||
91 | } | ||
92 | return (context, node, parent, i, debug) => { | ||
93 | const r = nMatch(i) | ||
94 | console.log('foo', {argument, A, B, debug, i, r}) | ||
95 | return r | ||
67 | } | 96 | } |
68 | return false | ||
69 | } | 97 | } |
70 | } | 98 | } |
71 | } | 99 | } |
72 | } | ||
73 | 100 | ||
74 | const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: boolean): AttrValueMatch => { | 101 | const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: boolean): AttrValueMatch => { |
75 | if (value === undefined) return (attrValue) => attrValue !== undefined | 102 | if (value === undefined) return (attrValue) => attrValue !== undefined |
76 | const isCaseInsenstive = caseSensitive === 'i' | 103 | const isCaseInsenstive = caseSensitive === 'i' |
77 | if (isCaseInsensitive) value = value.toLowerCase() | 104 | if (isCaseInsensitive) value = value.toLowerCase() |
78 | const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher | 105 | const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher |
79 | switch (operator) { | 106 | switch (operator) { |
80 | case '=': return (attrValue) => value === attrValue | 107 | case '=': return (attrValue) => value === attrValue |
81 | case '~=': { | 108 | case '~=': { |
82 | const keys = value.split(/\s+/g).reduce((keys, item) => { | 109 | const keys = value.split(/\s+/g).reduce((keys, item) => { |
83 | keys[ item ] = true | 110 | keys[ item ] = true |
84 | return keys | 111 | return keys |
85 | }, {}) | 112 | }, {}) |
86 | return adjustMatcher((attrValue) => keys[ attrValue ]) | 113 | return adjustMatcher((attrValue) => keys[ attrValue ]) |
114 | } | ||
115 | case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-')) | ||
116 | case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1) | ||
117 | case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue)) | ||
118 | case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue)) | ||
87 | } | 119 | } |
88 | case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-')) | 120 | return (attrValue) => false |
89 | case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1) | ||
90 | case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue)) | ||
91 | case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue)) | ||
92 | } | 121 | } |
93 | return (attrValue) => false | ||
94 | } | ||
95 | 122 | ||
96 | const compileMatcher = (ast: AST, selector: string): Matcher => { | ||
97 | let counter = 0 | ||
98 | |||
99 | const neededContext = [] | ||
100 | const makeMatcher = (ast: AST) => { | 123 | const makeMatcher = (ast: AST) => { |
101 | //console.log('makeMatcher', ast) | 124 | //console.log('makeMatcher', ast) |
102 | switch (ast.type) { | 125 | switch (ast.type) { |
... | @@ -215,16 +238,38 @@ const compileMatcher = (ast: AST, selector: string): Matcher => { | ... | @@ -215,16 +238,38 @@ const compileMatcher = (ast: AST, selector: string): Matcher => { |
215 | case 'only-child': | 238 | case 'only-child': |
216 | return (context, node, parent, i, debug) => { | 239 | return (context, node, parent, i, debug) => { |
217 | // TODO: This can break-early after it finds the second element | 240 | // TODO: This can break-early after it finds the second element |
218 | return filterChildElements(parent).length === 1 | 241 | return findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher).length === 1 |
219 | } | 242 | } |
243 | // case 'nth-of-type': | ||
244 | // case 'nth-last-of-type': | ||
245 | // case 'nth-last-child': | ||
220 | case 'nth-child': { | 246 | case 'nth-child': { |
221 | const nthChildMatcher = makeNthChildPosMatcher(ast) | 247 | console.log('nth-child:ast', ast) |
248 | const argument = ast.subtree ? ast.argument.replace(/\s*of\s+.*$/, '') : ast.argument | ||
249 | const nthChildMatcher = makeNthChildPosMatcher(argument) | ||
250 | let subDebug, childSelector, childMatcher | ||
251 | if (ast.subtree) { | ||
252 | subDebug = true | ||
253 | childSelector = ast.content | ||
254 | childMatcher = makeMatcher(ast.subtree) | ||
255 | } else { | ||
256 | subDebug = false | ||
257 | childSelector = 'ELEMENT' | ||
258 | childMatcher = filterChildElementsMatcher | ||
259 | } | ||
260 | |||
222 | return (context, node, parent, i, debug) => { | 261 | return (context, node, parent, i, debug) => { |
223 | const pos = nthChildPos(node, parent) + 1 | 262 | const children = findChildren(context, parent, childSelector, childMatcher) |
224 | return nthChildMatcher(context, node, parent, pos, debug) | 263 | const pos = children.indexOf(node) |
264 | if (parent?.name === 'body' && pos !== -1) { | ||
265 | console.log('nth-child:debug', {parent, childSelector, children, node, pos}) | ||
266 | } | ||
267 | if (pos === -1) return false | ||
268 | return nthChildMatcher(context, node, parent, pos + 1, debug || parent?.name === 'body') | ||
225 | } | 269 | } |
226 | } | 270 | } |
227 | default: | 271 | default: |
272 | console.error('pseudo-class', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true })) | ||
228 | throw new Error(`Unknown pseudo-class: ${ast.name}`) | 273 | throw new Error(`Unknown pseudo-class: ${ast.name}`) |
229 | } | 274 | } |
230 | case 'attribute': | 275 | case 'attribute': |
... | @@ -256,9 +301,9 @@ const compileMatcher = (ast: AST, selector: string): Matcher => { | ... | @@ -256,9 +301,9 @@ const compileMatcher = (ast: AST, selector: string): Matcher => { |
256 | 301 | ||
257 | export const createMatcher = (selector: string) => { | 302 | export const createMatcher = (selector: string) => { |
258 | const matcherCreater = selectorCache.get(selector) | 303 | const matcherCreater = selectorCache.get(selector) |
259 | if (false && matcherCreater) return matcherCreater() | 304 | if (matcherCreater) return matcherCreater() |
260 | const ast = elParse(selector) | 305 | const ast = elParse(selector) |
261 | console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true })) | 306 | //console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true })) |
262 | const newMatcherCreater = compileMatcher(ast, selector) | 307 | const newMatcherCreater = compileMatcher(ast, selector) |
263 | selectorCache.set(selector, newMatcherCreater) | 308 | selectorCache.set(selector, newMatcherCreater) |
264 | return newMatcherCreater() | 309 | return newMatcherCreater() | ... | ... |
1 | --- | 1 | --- |
2 | import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml' | 2 | import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml' |
3 | import type { NodeType } from 'ultrahtml' | ||
3 | import Children from './children.astro' | 4 | import Children from './children.astro' |
4 | 5 | ||
6 | interface Props { | ||
7 | html?: string, | ||
8 | debug?: boolean, | ||
9 | xpath?: string, | ||
10 | replacements?: Replacements, | ||
11 | |||
12 | slotHandler | ||
13 | parent?: NodeType, | ||
14 | node: NodeType, | ||
15 | index?: number, | ||
16 | } | ||
17 | |||
18 | |||
5 | const { props: { parent = null, node, index = 0, debug = false, replacers, slotHandler } } = Astro | 19 | const { props: { parent = null, node, index = 0, debug = false, replacers, slotHandler } } = Astro |
6 | const { name: Name, attributes } = node | 20 | const { name: Name, attributes } = node |
7 | 21 | ... | ... |
... | @@ -3,6 +3,14 @@ import html from '@resources/provider-portal.html?raw' | ... | @@ -3,6 +3,14 @@ import html from '@resources/provider-portal.html?raw' |
3 | import { walkSync } from 'ultrahtml' | 3 | import { walkSync } from 'ultrahtml' |
4 | import { parseHtml, createMatcher, findNode } from './html.ts' | 4 | import { parseHtml, createMatcher, findNode } from './html.ts' |
5 | import Match from './match.astro' | 5 | import Match from './match.astro' |
6 | import type { SlotHandler, Replacements } from './astro.ts' | ||
7 | |||
8 | interface Props { | ||
9 | html?: string, | ||
10 | debug?: boolean, | ||
11 | xpath?: string, | ||
12 | replacements?: Replacements, | ||
13 | } | ||
6 | 14 | ||
7 | const { props } = Astro | 15 | const { props } = Astro |
8 | const { html, debug = false, xpath, replacements = {} } = props | 16 | const { html, debug = false, xpath, replacements = {} } = props |
... | @@ -11,7 +19,7 @@ const doc = props.node ? props.node : parseHtml(props.html) | ... | @@ -11,7 +19,7 @@ const doc = props.node ? props.node : parseHtml(props.html) |
11 | const node = xpath ? findNode(doc, xpath) : doc | 19 | const node = xpath ? findNode(doc, xpath) : doc |
12 | const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ]) | 20 | const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ]) |
13 | 21 | ||
14 | const slotHandler = (slotName, node) => { | 22 | const slotHandler: SlotHandler = (slotName, node) => { |
15 | return Astro.slots.render(slotName, [ node, { slotHandler, replacers } ] ) | 23 | return Astro.slots.render(slotName, [ node, { slotHandler, replacers } ] ) |
16 | } | 24 | } |
17 | --- | 25 | --- | ... | ... |
... | @@ -10,9 +10,10 @@ const layoutReplacements = { | ... | @@ -10,9 +10,10 @@ const layoutReplacements = { |
10 | ['.hero-section']: 'content', | 10 | ['.hero-section']: 'content', |
11 | ['.content-section']: 'DELETE', | 11 | ['.content-section']: 'DELETE', |
12 | ['.content-section-4']: 'DELETE', | 12 | ['.content-section-4']: 'DELETE', |
13 | //['a.w-webflow-badge']: 'DELETE', | ||
13 | //['.navbar.w-nav:nth-child(n + 1)']: 'DELETE', | 14 | //['.navbar.w-nav:nth-child(n + 1)']: 'DELETE', |
14 | // The following 2 are not implemented yet | 15 | [':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE', |
15 | //[':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE', | 16 | // The following is not implemented yet |
16 | //['.navbar.w-nav:nth-of-type(n + 1)']: 'DELETE', | 17 | //['.navbar.w-nav:nth-of-type(n + 1)']: 'DELETE', |
17 | } | 18 | } |
18 | --- | 19 | --- | ... | ... |
... | @@ -4,7 +4,6 @@ import { Replace } from '@lib/astro.ts' | ... | @@ -4,7 +4,6 @@ import { Replace } from '@lib/astro.ts' |
4 | import BaseLayout from '@layouts/base.astro' | 4 | import BaseLayout from '@layouts/base.astro' |
5 | import { Demographics } from '@components/EventDetails.jsx' | 5 | import { Demographics } from '@components/EventDetails.jsx' |
6 | 6 | ||
7 | const { data: layoutHtml } = await getSitePage('msd', '/') | ||
8 | const { data: pageHtml } = await getSitePage('msd', '/provider-portal') | 7 | const { data: pageHtml } = await getSitePage('msd', '/provider-portal') |
9 | const pageReplacements = { | 8 | const pageReplacements = { |
10 | ['#w-node-_3615b991-cc00-f776-58c2-a728a0fba1a9-70cc1f5d']: 'Demographics', | 9 | ['#w-node-_3615b991-cc00-f776-58c2-a728a0fba1a9-70cc1f5d']: 'Demographics', | ... | ... |
-
Please register or sign in to post a comment