Improved types, nth-child(n of selector).
Showing
7 changed files
with
104 additions
and
31 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,20 +32,42 @@ export const appendClasses = (node, extraClasses) => { | ... | @@ -32,20 +32,42 @@ 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 | ||
| 42 | |||
| 43 | const compileMatcher = (ast: AST, selector: string): Matcher => { | ||
| 44 | let counter = 0 | ||
| 45 | |||
| 46 | const neededContext = [] | ||
| 47 | const selectorCacheCounter = counter++ | ||
| 48 | neededContext[ selectorCacheCounter ] = () => new WeakMap() | ||
| 40 | 49 | ||
| 41 | const makeNthChildPosMatcher = (ast: AST): Matcher => { | 50 | const findChildren = (context, parent: NodeType, selector: string, matcher: Matcher): array[NodeType] => { |
| 42 | const { argument } = ast | 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 | ||
| 60 | } | ||
| 61 | |||
| 62 | const makeNthChildPosMatcher = (argument: string): Matcher => { | ||
| 43 | const n = Number(argument) | 63 | const n = Number(argument) |
| 44 | if (!Number.isNaN(n)) { | 64 | if (!Number.isNaN(n)) { |
| 65 | // Simple variant, just a number | ||
| 45 | return (context, node, parent, i, debug) => { | 66 | return (context, node, parent, i, debug) => { |
| 46 | return i === n | 67 | return i === n |
| 47 | } | 68 | } |
| 48 | } | 69 | } |
| 70 | |||
| 49 | switch (argument) { | 71 | switch (argument) { |
| 50 | case 'odd': | 72 | case 'odd': |
| 51 | return (context, node, parent, i, debug) => Math.abs(i % 2) === 1 | 73 | return (context, node, parent, i, debug) => Math.abs(i % 2) === 1 |
| ... | @@ -53,25 +75,30 @@ const makeNthChildPosMatcher = (ast: AST): Matcher => { | ... | @@ -53,25 +75,30 @@ const makeNthChildPosMatcher = (ast: AST): Matcher => { |
| 53 | return (context, node, parent, i) => i % 2 === 0 | 75 | return (context, node, parent, i) => i % 2 === 0 |
| 54 | default: { | 76 | default: { |
| 55 | if (!argument) throw new Error(`Unsupported empty nth-child selector!`) | 77 | if (!argument) throw new Error(`Unsupported empty nth-child selector!`) |
| 56 | let [_, A = '1', B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? [] | 78 | let [_, A, B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? [] |
| 57 | if (A.length === 0) A = '1' | ||
| 58 | const a = Number.parseInt(A === '-' ? '-1' : A) | ||
| 59 | const b = Number.parseInt(B) | 79 | const b = Number.parseInt(B) |
| 60 | const nth = (index) => (a * n) + b | 80 | // (index) => (index - b) / a |
| 61 | return (context, node, parent, i) => { | 81 | let nMatch |
| 62 | const elements = filterChildElements(parent) | 82 | if (A === undefined || A === '0' || A === '-0') { |
| 63 | for (let index = 0; index < elements.length; index++) { | 83 | nMatch = (i) => (i - b) === 0 |
| 64 | const n = nth(index) | 84 | } else { |
| 65 | if (n > elements.length) return false | 85 | const a = A === '' ? 1 : A === '-' ? -1 : Number.parseInt(A) |
| 66 | if (n === i) return true | 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() |
| ... | @@ -91,12 +118,8 @@ const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: | ... | @@ -91,12 +118,8 @@ const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: |
| 91 | case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue)) | 118 | case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue)) |
| 92 | } | 119 | } |
| 93 | return (attrValue) => false | 120 | return (attrValue) => false |
| 94 | } | 121 | } |
| 95 | |||
| 96 | const compileMatcher = (ast: AST, selector: string): Matcher => { | ||
| 97 | let counter = 0 | ||
| 98 | 122 | ||
| 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