0d873159 by Adam Heath

Implement pseudo-elements after/before.

1 parent acf23c75
......@@ -19,6 +19,12 @@ import {
import { parsedHtmlCache, selectorCache, findNodeCache } from './cache.js'
export const PSEUDO_ELEMENT = Symbol('PSEUDO_ELEMENT')
export const PSEUDO_ELEMENTS = {
after: { type: PSEUDO_ELEMENT, name: '::after' },
before: { type: PSEUDO_ELEMENT, name: '::before' },
}
export const fixAttributes = (attributes, options = { mapClassname: true }) => {
const { mapClassname } = options
if (!mapClassname) return attributes
......@@ -35,10 +41,10 @@ export const appendClasses = (node, extraClasses) => {
// TODO: implement a parent/child/element cache
const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node);
const filterChildElementsMatcher = (context, child, parent, i) => child.type === ELEMENT_NODE
const filterChildElementsMatcher = (context, child, parent, i, debug, special) => child.type === ELEMENT_NODE
type NodePosition = number | 'before' | 'after'
type Matcher = (context, node: NodeType, parent?: NodeType, i: NodePosition, debug: number) => boolean
type Matcher = (context, node: NodeType, parent?: NodeType, i: NodePosition, debug: number, special: any) => boolean
type MatcherProducer = () => Matcher
type AttrValueMatch = (string) => string
......@@ -64,16 +70,16 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
const n = Number(argument)
if (!Number.isNaN(n)) {
// Simple variant, just a number
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
return i === n
}
}
switch (argument) {
case 'odd':
return (context, node, parent, i, debug) => Math.abs(i % 2) === 1
return (context, node, parent, i, debug, special) => Math.abs(i % 2) === 1
case 'even':
return (context, node, parent, i) => i % 2 === 0
return (context, node, parent, i, debug, special) => 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) ?? []
......@@ -90,7 +96,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
nMatch = (i) => { const n = (i - b) / a; return n !== 0 && Math.floor(n) === n }
}
}
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
return nMatch(i)
}
}
......@@ -133,18 +139,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
switch (ast.type) {
case 'list': {
const matchers = ast.list.map(s => makeMatcher(s))
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
for (const matcher of matchers) {
if (!matcher(context, node, parent, i)) return false
if (!matcher(context, node, parent, i, debug, special)) return false
}
return true
}
}
case 'compound': {
const matchers = ast.list.map(s => makeMatcher(s))
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
for (const matcher of matchers) {
if (!matcher(context, node, parent, i)) return false
if (!matcher(context, node, parent, i, debug, special)) return false
}
return true
}
......@@ -155,7 +161,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
const leftCounter = counter++
neededContext[ leftCounter ] = () => new WeakSet()
const rightMatcher = makeMatcher(right)
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const { [ leftCounter ]: leftMatches } = context
if (leftMatcher(context, node, parent, i, debug)) {
if (debug) console.log('matched on left', { left, right, combinator, pos, parent })
......@@ -204,15 +210,15 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
case 'type': {
const { name, content } = ast
if (content === '*') return (context, node, parent, i) => true
return (context, node, parent, i, debug) => node.name === name
return (context, node, parent, i, debug, special) => node.name === name
}
case 'class': {
const { name } = ast
return (context, node, parent, i, debug) => node.attributes?.['class']?.split(/\s+/g).includes(name)
return (context, node, parent, i, debug, special) => node.attributes?.['class']?.split(/\s+/g).includes(name)
}
case 'id': {
const { name } = ast
return (context, node, parent, i, debug) => node.attributes?.id === name
return (context, node, parent, i, debug, special) => node.attributes?.id === name
}
case 'pseudo-class':
switch (ast.name) {
......@@ -227,26 +233,26 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
case 'where':
return makeMatcher(ast.subtree)
case 'root':
return (context, node, parent, i) => !node.parent
return (context, node, parent, i, debug, special) => !node.parent
case 'empty':
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
if (node.type !== ELEMENT_NODE) return false
const { children } = node
if (children.length === 0) return false
return children.every(child => child.type === TEXT_NODE && child.value.trim() === '')
}
case 'first-child':
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
return children[ 0 ] == node
}
case 'last-child':
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
return children[ children.length - 1 ] == node
}
case 'only-child':
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
// TODO: This can break-early after it finds the second element
const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
return children.length === 1
......@@ -269,7 +275,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
childMatcher = filterChildElementsMatcher
}
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const children = findChildren(context, parent, childSelector, childMatcher)
const pos = children.indexOf(node)
if (parent?.name === 'body' && pos !== -1) {
......@@ -281,7 +287,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
}
case 'contains': {
const contentValueMatch = getAttrValueMatch(ast.argument, '*=', false)
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const nodeText = getNodeText(node)
return contentValueMatch(nodeText)
}
......@@ -293,18 +299,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
case 'attribute':
const { caseSensitive, name, value, operator } = ast
const attrValueMatch = getAttrValueMatch(value, operator, caseSensitive)
return (context, node, parent, i, debug) => {
return (context, node, parent, i, debug, special) => {
const { attributes: { [ name ]: attrValue } = {} } = node
return attrValueMatch(attrValue)
}
case 'universal':
return (context, node, parent, i, debug) => true
return (context, node, parent, i, debug, special) => true
case 'pseudo-element':
switch (ast.name) {
case 'after':
return (context, node, parent, i, debug) => i === 'after'
return (context, node, parent, i, debug, special) => special === PSEUDO_ELEMENTS.after
case 'before':
return (context, node, parent, i, debug) => i === 'before'
return (context, node, parent, i, debug, special) => special === PSEUDO_ELEMENTS.before
default:
console.error('pseudo-class', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
throw new Error(`Unknown pseudo-class: ${ast.name}`)
......@@ -316,9 +322,9 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
const matcher = makeMatcher(ast)
return () => {
const context = neededContext.map(item => item())
const nodeMatcher = (node, parent, i, debug) => {
const nodeMatcher = (node, parent, i, debug, special) => {
//if (debug) console.log('starting to match', {node, context})
return matcher(context, node, parent, i, debug)
return matcher(context, node, parent, i, debug, special)
}
nodeMatcher.toString = () => {
return '[matcher:' + selector + ']'
......
......@@ -2,7 +2,7 @@
import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import Node from './node.astro'
const { props: { parent = null, node, index = 0, debug = 0, replacers, slotHandler } } = Astro
const { props: { parent = null, node, index = 0, special, debug = 0, replacers, slotHandler } } = Astro
const { name: Name, attributes } = node
if (debug) {
......@@ -11,7 +11,7 @@ if (debug) {
let slotName
for (const [ matcher, handler ] of replacers) {
if (debug) console.log('attempting matcher', matcher.toString())
if (matcher(node, parent, index, false)) {
if (matcher(node, parent, index, false, special)) {
if (debug) console.log('matched')
slotName = handler
}
......@@ -22,6 +22,6 @@ const [ Component, componentArgs ] = Array.isArray(slotName) ? slotName : []
---
{
Component ? (<Component {...componentArgs} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>)
: slotName ? slotHandler(slotName, node)
: <Node parent={parent} node={node} index={index} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>
: slotName ? slotHandler(slotName, node, special)
: <Node parent={parent} node={node} index={index} debug={nextDebug} replacers={replacers} slotHandler={slotHandler} special={special}/>
}
......
......@@ -2,6 +2,11 @@
import { COMMENT_NODE, DOCTYPE_NODE, DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
import type { NodeType } from 'ultrahtml'
import Children from './children.astro'
import Match from './match.astro'
import {
PSEUDO_ELEMENT,
PSEUDO_ELEMENTS,
} from './html.ts'
interface Props {
html?: string,
......@@ -16,7 +21,7 @@ interface Props {
}
const { props: { parent = null, node, index = 0, debug = 0, replacers, slotHandler } } = Astro
const { props: { parent = null, node, special, index = 0, debug = 0, replacers, slotHandler } } = Astro
const { name: Name, attributes } = node
if (debug) {
......@@ -25,12 +30,13 @@ if (debug) {
const nextDebug = debug ? debug - 1 : 0
---
{
node.type === DOCTYPE_NODE ? ''
special?.type === PSEUDO_ELEMENT ? ''
: node.type === DOCTYPE_NODE ? ''
: node.type === DOCUMENT_NODE ? <Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>
: node.type === COMMENT_NODE ? <Fragment set:html={'<!-- ' + node.value + ' -->'}/>
: node.type === TEXT_NODE ? <Fragment set:html={node.value}/>
: node.type === ELEMENT_NODE ? (
node.isSelfClosingTag ? <Name {...attributes}/>
: <Name {...attributes}><Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/></Name>
: <Name {...attributes}><Match parent={parent} node={node} special={PSEUDO_ELEMENTS.before} index={0} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/><Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/><Match parent={parent} node={node} special={PSEUDO_ELEMENTS.after} index={0} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/></Name>
) : ''
}
......
......@@ -19,8 +19,8 @@ const { debug = 0, replacements = {}, ...rest } = props
const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ])
const slotHandler: SlotHandler = (slotName, node) => {
return Astro.slots.render(slotName, [ node, { slotHandler, replacers } ] )
const slotHandler: SlotHandler = (slotName, node, special) => {
return Astro.slots.render(slotName, [ node, { slotHandler, replacers, special } ] )
}
const nextDebug = debug ? debug - 1 : 0
---
......