ebe8b358 by Adam Heath

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

1 parent 2b0bc0ee
......@@ -5,3 +5,7 @@ import Node from './node.astro'
import Replace from './replace.astro'
export { Children, Custom, Match, Node, Replace }
import type { NodeType } from 'ultrahtml'
type SlotHandler = (string, NodeType) => Promise<Any>
type Replacements = Record<string, string>
......
......@@ -32,71 +32,94 @@ export const appendClasses = (node, extraClasses) => {
}
// TODO: implement a parent/child/element cache
const filterChildElements = (parent) => parent?.children?.filter(n => n.type === ELEMENT_NODE) || []
const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node);
const filterChildElementsMatcher = (context, child, parent, i) => child.type === ELEMENT_NODE
type Matcher = (context, node, parent, i, debug) => boolean
type AttrValueMatch = (string) => string
type ParentFilter = (context, node, parent, i) => boolean
const makeNthChildPosMatcher = (ast: AST): Matcher => {
const { argument } = ast
const n = Number(argument)
if (!Number.isNaN(n)) {
return (context, node, parent, i, debug) => {
return i === n
}
const compileMatcher = (ast: AST, selector: string): Matcher => {
let counter = 0
const neededContext = []
const selectorCacheCounter = counter++
neededContext[ selectorCacheCounter ] = () => new WeakMap()
const findChildren = (context, parent: NodeType, selector: string, matcher: Matcher): array[NodeType] => {
if (parent === null) console.log('null parent', new Error())
if (parent === null) return []
let selectorCache = context[ selectorCacheCounter ].get(parent)
if (!selectorCache) context[ selectorCacheCounter ].set(parent, selectorCache = {})
const selectorResult = selectorCache[ selector ]
if (selectorResult) return selectorResult
const newResult = parent?.children?.filter((child, index) => matcher(context, child, parent, index)) || []
selectorCache[ selector ] = newResult
return newResult
}
switch (argument) {
case 'odd':
return (context, node, parent, i, debug) => Math.abs(i % 2) === 1
case 'even':
return (context, node, parent, i) => i % 2 === 0
default: {
if (!argument) throw new Error(`Unsupported empty nth-child selector!`)
let [_, A = '1', B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? []
if (A.length === 0) A = '1'
const a = Number.parseInt(A === '-' ? '-1' : A)
const b = Number.parseInt(B)
const nth = (index) => (a * n) + b
return (context, node, parent, i) => {
const elements = filterChildElements(parent)
for (let index = 0; index < elements.length; index++) {
const n = nth(index)
if (n > elements.length) return false
if (n === i) return true
const makeNthChildPosMatcher = (argument: string): Matcher => {
const n = Number(argument)
if (!Number.isNaN(n)) {
// Simple variant, just a number
return (context, node, parent, i, debug) => {
return i === n
}
}
switch (argument) {
case 'odd':
return (context, node, parent, i, debug) => Math.abs(i % 2) === 1
case 'even':
return (context, node, parent, i) => i % 2 === 0
default: {
if (!argument) throw new Error(`Unsupported empty nth-child selector!`)
let [_, A, B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? []
const b = Number.parseInt(B)
// (index) => (index - b) / a
let nMatch
if (A === undefined || A === '0' || A === '-0') {
nMatch = (i) => (i - b) === 0
} else {
const a = A === '' ? 1 : A === '-' ? -1 : Number.parseInt(A)
if (a < 0) {
nMatch = (i) => { const n = -(i - b) / a; return n !== 0 && Math.floor(n) === n }
} else {
nMatch = (i) => { const n = (i - b) / a; return n !== 0 && Math.floor(n) === n }
}
}
return (context, node, parent, i, debug) => {
const r = nMatch(i)
console.log('foo', {argument, A, B, debug, i, r})
return r
}
return false
}
}
}
}
const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: boolean): AttrValueMatch => {
if (value === undefined) return (attrValue) => attrValue !== undefined
const isCaseInsenstive = caseSensitive === 'i'
if (isCaseInsensitive) value = value.toLowerCase()
const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher
switch (operator) {
case '=': return (attrValue) => value === attrValue
case '~=': {
const keys = value.split(/\s+/g).reduce((keys, item) => {
keys[ item ] = true
return keys
}, {})
return adjustMatcher((attrValue) => keys[ attrValue ])
const getAttrValueMatch = (value: string, operator: string = '=', caseSenstive: boolean): AttrValueMatch => {
if (value === undefined) return (attrValue) => attrValue !== undefined
const isCaseInsenstive = caseSensitive === 'i'
if (isCaseInsensitive) value = value.toLowerCase()
const adjustMatcher = (matcher) => isCaseInsensitive ? (attrValue) => matcher(attrValue.toLowerCase()) : matcher
switch (operator) {
case '=': return (attrValue) => value === attrValue
case '~=': {
const keys = value.split(/\s+/g).reduce((keys, item) => {
keys[ item ] = true
return keys
}, {})
return adjustMatcher((attrValue) => keys[ attrValue ])
}
case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-'))
case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1)
case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue))
case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue))
}
case '|=': return adjustMatcher((attrValue) => value.startsWith(attrValue + '-'))
case '*=': return adjustMatcher((attrValue) => value.indexOf(attrValue) > -1)
case '$=': return adjustMatcher((attrValue) => value.endsWith(attrValue))
case '^=': return adjustMatcher((attrValue) => value.startsWith(attrValue))
return (attrValue) => false
}
return (attrValue) => false
}
const compileMatcher = (ast: AST, selector: string): Matcher => {
let counter = 0
const neededContext = []
const makeMatcher = (ast: AST) => {
//console.log('makeMatcher', ast)
switch (ast.type) {
......@@ -215,16 +238,38 @@ const compileMatcher = (ast: AST, selector: string): Matcher => {
case 'only-child':
return (context, node, parent, i, debug) => {
// TODO: This can break-early after it finds the second element
return filterChildElements(parent).length === 1
return findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher).length === 1
}
// case 'nth-of-type':
// case 'nth-last-of-type':
// case 'nth-last-child':
case 'nth-child': {
const nthChildMatcher = makeNthChildPosMatcher(ast)
console.log('nth-child:ast', ast)
const argument = ast.subtree ? ast.argument.replace(/\s*of\s+.*$/, '') : ast.argument
const nthChildMatcher = makeNthChildPosMatcher(argument)
let subDebug, childSelector, childMatcher
if (ast.subtree) {
subDebug = true
childSelector = ast.content
childMatcher = makeMatcher(ast.subtree)
} else {
subDebug = false
childSelector = 'ELEMENT'
childMatcher = filterChildElementsMatcher
}
return (context, node, parent, i, debug) => {
const pos = nthChildPos(node, parent) + 1
return nthChildMatcher(context, node, parent, pos, debug)
const children = findChildren(context, parent, childSelector, childMatcher)
const pos = children.indexOf(node)
if (parent?.name === 'body' && pos !== -1) {
console.log('nth-child:debug', {parent, childSelector, children, node, pos})
}
if (pos === -1) return false
return nthChildMatcher(context, node, parent, pos + 1, debug || parent?.name === 'body')
}
}
default:
console.error('pseudo-class', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
throw new Error(`Unknown pseudo-class: ${ast.name}`)
}
case 'attribute':
......@@ -256,9 +301,9 @@ const compileMatcher = (ast: AST, selector: string): Matcher => {
export const createMatcher = (selector: string) => {
const matcherCreater = selectorCache.get(selector)
if (false && matcherCreater) return matcherCreater()
if (matcherCreater) return matcherCreater()
const ast = elParse(selector)
console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
//console.log('createMatcher', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
const newMatcherCreater = compileMatcher(ast, selector)
selectorCache.set(selector, newMatcherCreater)
return newMatcherCreater()
......
---
import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import type { NodeType } from 'ultrahtml'
import Children from './children.astro'
interface Props {
html?: string,
debug?: boolean,
xpath?: string,
replacements?: Replacements,
slotHandler
parent?: NodeType,
node: NodeType,
index?: number,
}
const { props: { parent = null, node, index = 0, debug = false, replacers, slotHandler } } = Astro
const { name: Name, attributes } = node
......
......@@ -3,6 +3,14 @@ import html from '@resources/provider-portal.html?raw'
import { walkSync } from 'ultrahtml'
import { parseHtml, createMatcher, findNode } from './html.ts'
import Match from './match.astro'
import type { SlotHandler, Replacements } from './astro.ts'
interface Props {
html?: string,
debug?: boolean,
xpath?: string,
replacements?: Replacements,
}
const { props } = Astro
const { html, debug = false, xpath, replacements = {} } = props
......@@ -11,7 +19,7 @@ const doc = props.node ? props.node : parseHtml(props.html)
const node = xpath ? findNode(doc, xpath) : doc
const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ])
const slotHandler = (slotName, node) => {
const slotHandler: SlotHandler = (slotName, node) => {
return Astro.slots.render(slotName, [ node, { slotHandler, replacers } ] )
}
---
......
......@@ -10,9 +10,10 @@ const layoutReplacements = {
['.hero-section']: 'content',
['.content-section']: 'DELETE',
['.content-section-4']: 'DELETE',
//['a.w-webflow-badge']: 'DELETE',
//['.navbar.w-nav:nth-child(n + 1)']: 'DELETE',
// The following 2 are not implemented yet
//[':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE',
[':nth-child(n + 1 of .navbar.w-nav)']: 'DELETE',
// The following is not implemented yet
//['.navbar.w-nav:nth-of-type(n + 1)']: 'DELETE',
}
---
......
......@@ -4,7 +4,6 @@ import { Replace } from '@lib/astro.ts'
import BaseLayout from '@layouts/base.astro'
import { Demographics } from '@components/EventDetails.jsx'
const { data: layoutHtml } = await getSitePage('msd', '/')
const { data: pageHtml } = await getSitePage('msd', '/provider-portal')
const pageReplacements = {
['#w-node-_3615b991-cc00-f776-58c2-a728a0fba1a9-70cc1f5d']: 'Demographics',
......
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"alwaysStrict": true,
"noImplicitAny": true,
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/*"],
......