Implement pseudo-elements after/before.
Showing
4 changed files
with
48 additions
and
36 deletions
... | @@ -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 | --- | ... | ... |
-
Please register or sign in to post a comment