ebe8b358 by Adam Heath

Improved types, nth-child(n of selector).

1 parent 2b0bc0ee
...@@ -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',
......
1 { 1 {
2 "extends": "astro/tsconfigs/base", 2 "extends": "astro/tsconfigs/base",
3 "compilerOptions": { 3 "compilerOptions": {
4 "alwaysStrict": true,
5 "noImplicitAny": true,
4 "baseUrl": ".", 6 "baseUrl": ".",
5 "paths": { 7 "paths": {
6 "@lib/*": ["lib/*"], 8 "@lib/*": ["lib/*"],
......