0d873159 by Adam Heath

Implement pseudo-elements after/before.

1 parent acf23c75
...@@ -19,6 +19,12 @@ import { ...@@ -19,6 +19,12 @@ import {
19 19
20 import { parsedHtmlCache, selectorCache, findNodeCache } from './cache.js' 20 import { parsedHtmlCache, selectorCache, findNodeCache } from './cache.js'
21 21
22 export const PSEUDO_ELEMENT = Symbol('PSEUDO_ELEMENT')
23 export const PSEUDO_ELEMENTS = {
24 after: { type: PSEUDO_ELEMENT, name: '::after' },
25 before: { type: PSEUDO_ELEMENT, name: '::before' },
26 }
27
22 export const fixAttributes = (attributes, options = { mapClassname: true }) => { 28 export const fixAttributes = (attributes, options = { mapClassname: true }) => {
23 const { mapClassname } = options 29 const { mapClassname } = options
24 if (!mapClassname) return attributes 30 if (!mapClassname) return attributes
...@@ -35,10 +41,10 @@ export const appendClasses = (node, extraClasses) => { ...@@ -35,10 +41,10 @@ export const appendClasses = (node, extraClasses) => {
35 // TODO: implement a parent/child/element cache 41 // TODO: implement a parent/child/element cache
36 const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node); 42 const nthChildPos = (node, parent) => filterChildElements(parent).findIndex((child) => child === node);
37 43
38 const filterChildElementsMatcher = (context, child, parent, i) => child.type === ELEMENT_NODE 44 const filterChildElementsMatcher = (context, child, parent, i, debug, special) => child.type === ELEMENT_NODE
39 45
40 type NodePosition = number | 'before' | 'after' 46 type NodePosition = number | 'before' | 'after'
41 type Matcher = (context, node: NodeType, parent?: NodeType, i: NodePosition, debug: number) => boolean 47 type Matcher = (context, node: NodeType, parent?: NodeType, i: NodePosition, debug: number, special: any) => boolean
42 type MatcherProducer = () => Matcher 48 type MatcherProducer = () => Matcher
43 type AttrValueMatch = (string) => string 49 type AttrValueMatch = (string) => string
44 50
...@@ -64,16 +70,16 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -64,16 +70,16 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
64 const n = Number(argument) 70 const n = Number(argument)
65 if (!Number.isNaN(n)) { 71 if (!Number.isNaN(n)) {
66 // Simple variant, just a number 72 // Simple variant, just a number
67 return (context, node, parent, i, debug) => { 73 return (context, node, parent, i, debug, special) => {
68 return i === n 74 return i === n
69 } 75 }
70 } 76 }
71 77
72 switch (argument) { 78 switch (argument) {
73 case 'odd': 79 case 'odd':
74 return (context, node, parent, i, debug) => Math.abs(i % 2) === 1 80 return (context, node, parent, i, debug, special) => Math.abs(i % 2) === 1
75 case 'even': 81 case 'even':
76 return (context, node, parent, i) => i % 2 === 0 82 return (context, node, parent, i, debug, special) => i % 2 === 0
77 default: { 83 default: {
78 if (!argument) throw new Error(`Unsupported empty nth-child selector!`) 84 if (!argument) throw new Error(`Unsupported empty nth-child selector!`)
79 let [_, A, B = '0'] = /^\s*(?:(-?(?:\d+)?)n)?\s*\+?\s*(\d+)?\s*$/gm.exec(argument) ?? [] 85 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 => { ...@@ -90,7 +96,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
90 nMatch = (i) => { const n = (i - b) / a; return n !== 0 && Math.floor(n) === n } 96 nMatch = (i) => { const n = (i - b) / a; return n !== 0 && Math.floor(n) === n }
91 } 97 }
92 } 98 }
93 return (context, node, parent, i, debug) => { 99 return (context, node, parent, i, debug, special) => {
94 return nMatch(i) 100 return nMatch(i)
95 } 101 }
96 } 102 }
...@@ -133,18 +139,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -133,18 +139,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
133 switch (ast.type) { 139 switch (ast.type) {
134 case 'list': { 140 case 'list': {
135 const matchers = ast.list.map(s => makeMatcher(s)) 141 const matchers = ast.list.map(s => makeMatcher(s))
136 return (context, node, parent, i, debug) => { 142 return (context, node, parent, i, debug, special) => {
137 for (const matcher of matchers) { 143 for (const matcher of matchers) {
138 if (!matcher(context, node, parent, i)) return false 144 if (!matcher(context, node, parent, i, debug, special)) return false
139 } 145 }
140 return true 146 return true
141 } 147 }
142 } 148 }
143 case 'compound': { 149 case 'compound': {
144 const matchers = ast.list.map(s => makeMatcher(s)) 150 const matchers = ast.list.map(s => makeMatcher(s))
145 return (context, node, parent, i, debug) => { 151 return (context, node, parent, i, debug, special) => {
146 for (const matcher of matchers) { 152 for (const matcher of matchers) {
147 if (!matcher(context, node, parent, i)) return false 153 if (!matcher(context, node, parent, i, debug, special)) return false
148 } 154 }
149 return true 155 return true
150 } 156 }
...@@ -155,7 +161,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -155,7 +161,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
155 const leftCounter = counter++ 161 const leftCounter = counter++
156 neededContext[ leftCounter ] = () => new WeakSet() 162 neededContext[ leftCounter ] = () => new WeakSet()
157 const rightMatcher = makeMatcher(right) 163 const rightMatcher = makeMatcher(right)
158 return (context, node, parent, i, debug) => { 164 return (context, node, parent, i, debug, special) => {
159 const { [ leftCounter ]: leftMatches } = context 165 const { [ leftCounter ]: leftMatches } = context
160 if (leftMatcher(context, node, parent, i, debug)) { 166 if (leftMatcher(context, node, parent, i, debug)) {
161 if (debug) console.log('matched on left', { left, right, combinator, pos, parent }) 167 if (debug) console.log('matched on left', { left, right, combinator, pos, parent })
...@@ -204,15 +210,15 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -204,15 +210,15 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
204 case 'type': { 210 case 'type': {
205 const { name, content } = ast 211 const { name, content } = ast
206 if (content === '*') return (context, node, parent, i) => true 212 if (content === '*') return (context, node, parent, i) => true
207 return (context, node, parent, i, debug) => node.name === name 213 return (context, node, parent, i, debug, special) => node.name === name
208 } 214 }
209 case 'class': { 215 case 'class': {
210 const { name } = ast 216 const { name } = ast
211 return (context, node, parent, i, debug) => node.attributes?.['class']?.split(/\s+/g).includes(name) 217 return (context, node, parent, i, debug, special) => node.attributes?.['class']?.split(/\s+/g).includes(name)
212 } 218 }
213 case 'id': { 219 case 'id': {
214 const { name } = ast 220 const { name } = ast
215 return (context, node, parent, i, debug) => node.attributes?.id === name 221 return (context, node, parent, i, debug, special) => node.attributes?.id === name
216 } 222 }
217 case 'pseudo-class': 223 case 'pseudo-class':
218 switch (ast.name) { 224 switch (ast.name) {
...@@ -227,26 +233,26 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -227,26 +233,26 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
227 case 'where': 233 case 'where':
228 return makeMatcher(ast.subtree) 234 return makeMatcher(ast.subtree)
229 case 'root': 235 case 'root':
230 return (context, node, parent, i) => !node.parent 236 return (context, node, parent, i, debug, special) => !node.parent
231 case 'empty': 237 case 'empty':
232 return (context, node, parent, i, debug) => { 238 return (context, node, parent, i, debug, special) => {
233 if (node.type !== ELEMENT_NODE) return false 239 if (node.type !== ELEMENT_NODE) return false
234 const { children } = node 240 const { children } = node
235 if (children.length === 0) return false 241 if (children.length === 0) return false
236 return children.every(child => child.type === TEXT_NODE && child.value.trim() === '') 242 return children.every(child => child.type === TEXT_NODE && child.value.trim() === '')
237 } 243 }
238 case 'first-child': 244 case 'first-child':
239 return (context, node, parent, i, debug) => { 245 return (context, node, parent, i, debug, special) => {
240 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher) 246 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
241 return children[ 0 ] == node 247 return children[ 0 ] == node
242 } 248 }
243 case 'last-child': 249 case 'last-child':
244 return (context, node, parent, i, debug) => { 250 return (context, node, parent, i, debug, special) => {
245 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher) 251 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
246 return children[ children.length - 1 ] == node 252 return children[ children.length - 1 ] == node
247 } 253 }
248 case 'only-child': 254 case 'only-child':
249 return (context, node, parent, i, debug) => { 255 return (context, node, parent, i, debug, special) => {
250 // TODO: This can break-early after it finds the second element 256 // TODO: This can break-early after it finds the second element
251 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher) 257 const children = findChildren(context, parent, 'ELEMENT', filterChildElementsMatcher)
252 return children.length === 1 258 return children.length === 1
...@@ -269,7 +275,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -269,7 +275,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
269 childMatcher = filterChildElementsMatcher 275 childMatcher = filterChildElementsMatcher
270 } 276 }
271 277
272 return (context, node, parent, i, debug) => { 278 return (context, node, parent, i, debug, special) => {
273 const children = findChildren(context, parent, childSelector, childMatcher) 279 const children = findChildren(context, parent, childSelector, childMatcher)
274 const pos = children.indexOf(node) 280 const pos = children.indexOf(node)
275 if (parent?.name === 'body' && pos !== -1) { 281 if (parent?.name === 'body' && pos !== -1) {
...@@ -281,7 +287,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -281,7 +287,7 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
281 } 287 }
282 case 'contains': { 288 case 'contains': {
283 const contentValueMatch = getAttrValueMatch(ast.argument, '*=', false) 289 const contentValueMatch = getAttrValueMatch(ast.argument, '*=', false)
284 return (context, node, parent, i, debug) => { 290 return (context, node, parent, i, debug, special) => {
285 const nodeText = getNodeText(node) 291 const nodeText = getNodeText(node)
286 return contentValueMatch(nodeText) 292 return contentValueMatch(nodeText)
287 } 293 }
...@@ -293,18 +299,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -293,18 +299,18 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
293 case 'attribute': 299 case 'attribute':
294 const { caseSensitive, name, value, operator } = ast 300 const { caseSensitive, name, value, operator } = ast
295 const attrValueMatch = getAttrValueMatch(value, operator, caseSensitive) 301 const attrValueMatch = getAttrValueMatch(value, operator, caseSensitive)
296 return (context, node, parent, i, debug) => { 302 return (context, node, parent, i, debug, special) => {
297 const { attributes: { [ name ]: attrValue } = {} } = node 303 const { attributes: { [ name ]: attrValue } = {} } = node
298 return attrValueMatch(attrValue) 304 return attrValueMatch(attrValue)
299 } 305 }
300 case 'universal': 306 case 'universal':
301 return (context, node, parent, i, debug) => true 307 return (context, node, parent, i, debug, special) => true
302 case 'pseudo-element': 308 case 'pseudo-element':
303 switch (ast.name) { 309 switch (ast.name) {
304 case 'after': 310 case 'after':
305 return (context, node, parent, i, debug) => i === 'after' 311 return (context, node, parent, i, debug, special) => special === PSEUDO_ELEMENTS.after
306 case 'before': 312 case 'before':
307 return (context, node, parent, i, debug) => i === 'before' 313 return (context, node, parent, i, debug, special) => special === PSEUDO_ELEMENTS.before
308 default: 314 default:
309 console.error('pseudo-class', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true })) 315 console.error('pseudo-class', nodeUtil.inspect({ selector, ast }, { depth: null, colors: true }))
310 throw new Error(`Unknown pseudo-class: ${ast.name}`) 316 throw new Error(`Unknown pseudo-class: ${ast.name}`)
...@@ -316,9 +322,9 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => { ...@@ -316,9 +322,9 @@ const compileMatcher = (ast: AST, selector: string): MatcherProducer => {
316 const matcher = makeMatcher(ast) 322 const matcher = makeMatcher(ast)
317 return () => { 323 return () => {
318 const context = neededContext.map(item => item()) 324 const context = neededContext.map(item => item())
319 const nodeMatcher = (node, parent, i, debug) => { 325 const nodeMatcher = (node, parent, i, debug, special) => {
320 //if (debug) console.log('starting to match', {node, context}) 326 //if (debug) console.log('starting to match', {node, context})
321 return matcher(context, node, parent, i, debug) 327 return matcher(context, node, parent, i, debug, special)
322 } 328 }
323 nodeMatcher.toString = () => { 329 nodeMatcher.toString = () => {
324 return '[matcher:' + selector + ']' 330 return '[matcher:' + selector + ']'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml' 2 import { ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
3 import Node from './node.astro' 3 import Node from './node.astro'
4 4
5 const { props: { parent = null, node, index = 0, debug = 0, replacers, slotHandler } } = Astro 5 const { props: { parent = null, node, index = 0, special, debug = 0, replacers, slotHandler } } = Astro
6 const { name: Name, attributes } = node 6 const { name: Name, attributes } = node
7 7
8 if (debug) { 8 if (debug) {
...@@ -11,7 +11,7 @@ if (debug) { ...@@ -11,7 +11,7 @@ if (debug) {
11 let slotName 11 let slotName
12 for (const [ matcher, handler ] of replacers) { 12 for (const [ matcher, handler ] of replacers) {
13 if (debug) console.log('attempting matcher', matcher.toString()) 13 if (debug) console.log('attempting matcher', matcher.toString())
14 if (matcher(node, parent, index, false)) { 14 if (matcher(node, parent, index, false, special)) {
15 if (debug) console.log('matched') 15 if (debug) console.log('matched')
16 slotName = handler 16 slotName = handler
17 } 17 }
...@@ -22,6 +22,6 @@ const [ Component, componentArgs ] = Array.isArray(slotName) ? slotName : [] ...@@ -22,6 +22,6 @@ const [ Component, componentArgs ] = Array.isArray(slotName) ? slotName : []
22 --- 22 ---
23 { 23 {
24 Component ? (<Component {...componentArgs} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>) 24 Component ? (<Component {...componentArgs} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>)
25 : slotName ? slotHandler(slotName, node) 25 : slotName ? slotHandler(slotName, node, special)
26 : <Node parent={parent} node={node} index={index} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/> 26 : <Node parent={parent} node={node} index={index} debug={nextDebug} replacers={replacers} slotHandler={slotHandler} special={special}/>
27 } 27 }
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
2 import { COMMENT_NODE, DOCTYPE_NODE, DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE } from 'ultrahtml' 2 import { COMMENT_NODE, DOCTYPE_NODE, DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE } from 'ultrahtml'
3 import type { NodeType } from 'ultrahtml' 3 import type { NodeType } from 'ultrahtml'
4 import Children from './children.astro' 4 import Children from './children.astro'
5 import Match from './match.astro'
6 import {
7 PSEUDO_ELEMENT,
8 PSEUDO_ELEMENTS,
9 } from './html.ts'
5 10
6 interface Props { 11 interface Props {
7 html?: string, 12 html?: string,
...@@ -16,7 +21,7 @@ interface Props { ...@@ -16,7 +21,7 @@ interface Props {
16 } 21 }
17 22
18 23
19 const { props: { parent = null, node, index = 0, debug = 0, replacers, slotHandler } } = Astro 24 const { props: { parent = null, node, special, index = 0, debug = 0, replacers, slotHandler } } = Astro
20 const { name: Name, attributes } = node 25 const { name: Name, attributes } = node
21 26
22 if (debug) { 27 if (debug) {
...@@ -25,12 +30,13 @@ if (debug) { ...@@ -25,12 +30,13 @@ if (debug) {
25 const nextDebug = debug ? debug - 1 : 0 30 const nextDebug = debug ? debug - 1 : 0
26 --- 31 ---
27 { 32 {
28 node.type === DOCTYPE_NODE ? '' 33 special?.type === PSEUDO_ELEMENT ? ''
34 : node.type === DOCTYPE_NODE ? ''
29 : node.type === DOCUMENT_NODE ? <Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/> 35 : node.type === DOCUMENT_NODE ? <Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/>
30 : node.type === COMMENT_NODE ? <Fragment set:html={'<!-- ' + node.value + ' -->'}/> 36 : node.type === COMMENT_NODE ? <Fragment set:html={'<!-- ' + node.value + ' -->'}/>
31 : node.type === TEXT_NODE ? <Fragment set:html={node.value}/> 37 : node.type === TEXT_NODE ? <Fragment set:html={node.value}/>
32 : node.type === ELEMENT_NODE ? ( 38 : node.type === ELEMENT_NODE ? (
33 node.isSelfClosingTag ? <Name {...attributes}/> 39 node.isSelfClosingTag ? <Name {...attributes}/>
34 : <Name {...attributes}><Children parent={node} children={node.children} debug={nextDebug} replacers={replacers} slotHandler={slotHandler}/></Name> 40 : <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>
35 ) : '' 41 ) : ''
36 } 42 }
......
...@@ -19,8 +19,8 @@ const { debug = 0, replacements = {}, ...rest } = props ...@@ -19,8 +19,8 @@ const { debug = 0, replacements = {}, ...rest } = props
19 19
20 const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ]) 20 const replacers = Object.entries(replacements).map(([ selector, handler ]) => [ createMatcher(selector), handler ])
21 21
22 const slotHandler: SlotHandler = (slotName, node) => { 22 const slotHandler: SlotHandler = (slotName, node, special) => {
23 return Astro.slots.render(slotName, [ node, { slotHandler, replacers } ] ) 23 return Astro.slots.render(slotName, [ node, { slotHandler, replacers, special } ] )
24 } 24 }
25 const nextDebug = debug ? debug - 1 : 0 25 const nextDebug = debug ? debug - 1 : 0
26 --- 26 ---
......