Skip to content

Commit 18ffc24

Browse files
committed
Merge branch 'release/v0.15.2'
2 parents cd062f9 + 7743be5 commit 18ffc24

11 files changed

Lines changed: 209 additions & 106 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ tmp
77
.out*
88
test
99
coverage
10+
Icon?

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import antfu from '@antfu/eslint-config'
2-
import { eslintIgnoreDefaults, eslintRulesDefaults } from 'zeed'
2+
import { eslintIgnoreDefaults, eslintRulesDefaults } from 'zeed/eslint'
33

44
export default antfu(
55
{

package.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zeed-dom",
33
"type": "module",
4-
"version": "0.15.1",
4+
"version": "0.15.2",
55
"description": "🌱 Lightweight offline DOM",
66
"author": {
77
"name": "Dirk Holtwick",
@@ -67,21 +67,21 @@
6767
"watch": "npm run build:tsup -- --watch"
6868
},
6969
"dependencies": {
70-
"css-what": "^6.1.0",
71-
"entities": "^5.0.0"
70+
"css-what": "^7.0.0",
71+
"entities": "^6.0.1"
7272
},
7373
"devDependencies": {
74-
"@antfu/eslint-config": "^3.7.1",
75-
"@antfu/ni": "^0.23.0",
76-
"@types/node": "^22.5.5",
77-
"@vitest/coverage-v8": "^2.1.1",
78-
"c8": "^10.1.2",
79-
"eslint": "^9.11.0",
80-
"tsup": "^8.3.0",
81-
"typedoc": "^0.26.7",
82-
"typescript": "^5.6.2",
83-
"vite": "^5.4.7",
84-
"vitest": "^2.1.1",
85-
"zeed": "^0.24.21"
74+
"@antfu/eslint-config": "^4.17.0",
75+
"@antfu/ni": "^25.0.0",
76+
"@types/node": "^24.0.14",
77+
"@vitest/coverage-v8": "^3.2.4",
78+
"c8": "^10.1.3",
79+
"eslint": "^9.31.0",
80+
"tsup": "^8.5.0",
81+
"typedoc": "^0.28.7",
82+
"typescript": "^5.8.3",
83+
"vite": "^7.0.5",
84+
"vitest": "^3.2.4",
85+
"zeed": "^1.0.1"
8686
}
8787
}

src/html.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function markup(
4545
|| (Array.isArray(children)
4646
&& (children.length === 0
4747
|| (children.length === 1 && children[0] === '')))
48-
|| children == null
48+
|| children == null
4949
)
5050

5151
const parts: string[] = []

src/htmlparser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class HtmlParser {
121121
}
122122
}
123123

124-
parseStartTag(input: string, tagName: string, match: any) {
124+
private parseStartTag(input: string, tagName: string, match: any) {
125125
const isSelfColse = selfCloseTagRe.test(input)
126126
let attrInput = match[2]
127127
if (isSelfColse)
@@ -131,11 +131,11 @@ export class HtmlParser {
131131
return tagName.toLocaleLowerCase()
132132
}
133133

134-
parseEndTag(input: string, tagName: string) {
134+
private parseEndTag(input: string, tagName: string) {
135135
this.scanner.endElement(tagName)
136136
}
137137

138-
parseAttributes(tagName: string, input: string) {
138+
private parseAttributes(tagName: string, input: string) {
139139
const attrs: Record<string, any> = {}
140140
input.replace(this.attrRe, (...m: any[]) => {
141141
const [_attr, name, _c2, value, _c4, valueInQuote, _c6, valueInSingleQuote] = m

src/jsx-runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { h } from './html'
55
// https://www.typescriptlang.org/tsconfig/#jsxImportSource
66

77
export {
8+
h,
89
h as jsx,
9-
h as jsxs,
1010
h as jsxDEV,
11-
h,
11+
h as jsxs,
1212
}

src/tidy.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function level(element: VNode): string {
1010
indent += ' '
1111
element = element.parentNode
1212
}
13-
return indent.substr(2)
13+
return indent.slice(2)
1414
}
1515

1616
export function tidyDOM(document: VDocument) {
@@ -23,34 +23,31 @@ export function tidyDOM(document: VDocument) {
2323
ee = ee.parentNode
2424
}
2525

26+
const parent = e.parentNode
27+
if (!parent)
28+
return
29+
2630
const prev = e.previousSibling
27-
if (
28-
!prev
29-
|| prev.nodeType !== VNode.TEXT_NODE
30-
|| !prev.nodeValue?.endsWith('\n')
31-
) {
32-
e.parentNode?.insertBefore(new VTextNode('\n'), e)
31+
if (!prev || prev.nodeType !== VNode.TEXT_NODE || !prev.nodeValue?.endsWith('\n')) {
32+
parent.insertBefore(new VTextNode('\n'), e)
3333
}
34-
35-
e.parentNode?.insertBefore(new VTextNode(level(e)), e)
34+
parent.insertBefore(new VTextNode(level(e)), e)
3635

3736
const next = e.nextSibling
38-
if (
39-
!next
40-
|| next.nodeType !== VNode.TEXT_NODE
41-
|| !next.nodeValue?.startsWith('\n')
42-
) {
43-
if (next)
44-
e.parentNode?.insertBefore(new VTextNode('\n'), next)
45-
else
46-
e.parentNode?.appendChild(new VTextNode('\n'))
37+
if (!next || next.nodeType !== VNode.TEXT_NODE || !next.nodeValue?.startsWith('\n')) {
38+
if (next) {
39+
parent.insertBefore(new VTextNode('\n'), next)
40+
}
41+
else {
42+
parent.appendChild(new VTextNode('\n'))
43+
}
4744
}
4845

4946
if (e.childNodes.length) {
5047
const first = e.firstChild
51-
if (first.nodeType === VNode.TEXT_NODE)
52-
e.insertBefore(new VTextNode(`\n${level(e)} `))
53-
48+
if (first.nodeType === VNode.TEXT_NODE) {
49+
e.insertBefore(new VTextNode(`\n${level(e)} `), first)
50+
}
5451
e.appendChild(new VTextNode(`\n${level(e)}`))
5552
}
5653
})

src/vcss.spec.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createHTMLDocument, h } from './vdom'
33

44
const _keepH = h
55

6-
describe('cSS', () => {
6+
describe('css', () => {
77
it('should parse', () => {
88
const element = (
99
<div id="foo" className="foo bar" foo="bar" data-lang="en">
@@ -168,6 +168,64 @@ describe('cSS', () => {
168168
expect(!document.querySelector('meta[charset]')).toBe(false)
169169
})
170170

171+
it('should handle descendant combinator', () => {
172+
const element = (
173+
<div id="root">
174+
<section>
175+
<span id="target">Hello</span>
176+
</section>
177+
</div>
178+
)
179+
expect(matchSelector('div section', element)).toBe(true)
180+
expect(matchSelector('div span', element)).toBe(true)
181+
expect(matchSelector('section span', element.querySelector('section'))).toBe(true)
182+
expect(matchSelector('div > section', element)).toBe(false) // Not implemented, should be false
183+
expect(matchSelector('div + section', element)).toBe(false) // Not implemented, should be false
184+
})
185+
186+
it('should handle edge cases and invalid selectors', () => {
187+
const element = <div id="foo"></div>
188+
expect(matchSelector('', element)).toBe(false)
189+
expect(() => matchSelector('!!!', element)).toThrow()
190+
})
191+
192+
it('should handle universal selector in combination', () => {
193+
const element = <div id="foo"></div>
194+
expect(matchSelector('*#foo', element)).toBe(true)
195+
expect(matchSelector('*.bar', element)).toBe(false)
196+
})
197+
198+
it('should handle case sensitivity', () => {
199+
const element = <div id="foo"></div>
200+
expect(matchSelector('div', element)).toBe(true)
201+
expect(matchSelector('DIV', element)).toBe(true)
202+
})
203+
204+
it('should handle :not pseudo-class', () => {
205+
const element = <div id="foo" className="bar"></div>
206+
expect(matchSelector(':not(span)', element)).toBe(true)
207+
expect(matchSelector(':not(div)', element)).toBe(false)
208+
expect(matchSelector('div:not(.bar)', element)).toBe(false)
209+
expect(matchSelector('div:not(.baz)', element)).toBe(true)
210+
expect(matchSelector('div:not([id])', element)).toBe(false)
211+
expect(matchSelector('div:not([data-unknown])', element)).toBe(true)
212+
})
213+
214+
it('should handle multiple simple selectors', () => {
215+
const element = <div id="foo" className="bar"></div>
216+
expect(matchSelector('div#foo.bar', element)).toBe(true)
217+
expect(matchSelector('div.bar#foo', element)).toBe(true)
218+
expect(matchSelector('div#bar.bar', element)).toBe(false)
219+
expect(matchSelector('span#foo.bar', element)).toBe(false)
220+
})
221+
222+
it('should handle whitespace and empty selectors', () => {
223+
const element = <div id="foo"></div>
224+
expect(matchSelector(' ', element)).toBe(false)
225+
expect(matchSelector(' div ', element)).toBe(true)
226+
expect(matchSelector(' #foo ', element)).toBe(true)
227+
})
228+
171229
// it('should be single fail', () => {
172230
// let element = <h1>...</h1>
173231
// expect(matchSelector('[id]', element, { debug: true })).toBe(false)

src/vcss.ts

Lines changed: 76 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -30,64 +30,70 @@ export function matchSelector(
3030
log('Element:', element)
3131
}
3232

33-
const handleRules = (element: VElement, rules: any[]) => {
34-
// let pos = 0
33+
const handleRules = (element: VElement, rules: any[], ruleIndex = 0): boolean => {
34+
if (!element || ruleIndex >= rules.length)
35+
return false
36+
const part = rules[ruleIndex]
37+
const { type, name, action, value, _ignoreCase = true, data } = part
3538
let success = false
36-
for (const part of rules) {
37-
const { type, name, action, value, _ignoreCase = true, data } = part
38-
if (type === 'attribute') {
39-
if (action === 'equals') {
40-
success = element.getAttribute(name) === value
41-
if (debug)
42-
log('Attribute equals', success)
43-
}
44-
else if (action === 'start') {
45-
success = !!element.getAttribute(name)?.startsWith(value)
46-
if (debug)
47-
log('Attribute start', success)
48-
}
49-
else if (action === 'end') {
50-
success = !!element.getAttribute(name)?.endsWith(value)
51-
if (debug)
52-
log('Attribute start', success)
53-
}
54-
else if (action === 'element') {
55-
if (name === 'class') {
56-
success = element.classList.contains(value)
39+
switch (type) {
40+
case 'attribute': {
41+
const attrValue = element.getAttribute(name)
42+
switch (action) {
43+
case 'equals':
44+
success = attrValue === value
5745
if (debug)
58-
log('Attribute class', success)
59-
}
60-
else {
61-
success = !!element.getAttribute(name)?.includes(value)
46+
log('Attribute equals', success)
47+
break
48+
case 'start':
49+
success = !!attrValue?.startsWith(value)
6250
if (debug)
63-
log('Attribute element', success)
64-
}
65-
}
66-
else if (action === 'exists') {
67-
success = element.hasAttribute(name)
68-
if (debug)
69-
log('Attribute exists', success)
70-
}
71-
else if (action === 'any') {
72-
success = !!element.getAttribute(name)?.includes(value)
73-
if (debug)
74-
log('Attribute any', success)
75-
}
76-
else {
77-
console.warn('Unknown CSS selector action', action)
51+
log('Attribute start', success)
52+
break
53+
case 'end':
54+
success = !!attrValue?.endsWith(value)
55+
if (debug)
56+
log('Attribute end', success)
57+
break
58+
case 'element':
59+
if (name === 'class') {
60+
success = element.classList.contains(value)
61+
if (debug)
62+
log('Attribute class', success)
63+
}
64+
else {
65+
success = !!attrValue?.includes(value)
66+
if (debug)
67+
log('Attribute element', success)
68+
}
69+
break
70+
case 'exists':
71+
success = element.hasAttribute(name)
72+
if (debug)
73+
log('Attribute exists', success)
74+
break
75+
case 'any':
76+
success = !!attrValue?.includes(value)
77+
if (debug)
78+
log('Attribute any', success)
79+
break
80+
default:
81+
if (debug)
82+
console.warn('Unknown CSS selector action', action)
7883
}
84+
break
7985
}
80-
else if (type === 'tag') {
86+
case 'tag':
8187
success = element.tagName === name.toUpperCase()
8288
if (debug)
8389
log('Is tag', success)
84-
}
85-
else if (type === 'universal') {
90+
break
91+
case 'universal':
8692
success = true
8793
if (debug)
8894
log('Is universal', success)
89-
}
90-
else if (type === 'pseudo') {
95+
break
96+
case 'pseudo':
9197
if (name === 'not') {
9298
let ok = true
9399
data.forEach((rules: any) => {
@@ -98,21 +104,30 @@ export function matchSelector(
98104
}
99105
if (debug)
100106
log('Is :not', success)
101-
// } else if (type === 'descendant') {
102-
// element = element.
103-
}
104-
// else if (type === 'descendant') {
105-
// for (const child of element.childNodes)
106-
// handleRules(child, rules.slice(pos))
107-
// }
108-
else {
109-
console.warn('Unknown CSS selector type', type, selector, rules)
110-
}
111-
// log(success, selector, part, element)
112-
if (!success)
113107
break
114-
115-
// pos += 1
108+
case 'descendant':
109+
// Try to match the next rule part in any descendant
110+
for (const child of element.childNodes || []) {
111+
if (handleRules(child, rules, ruleIndex + 1)) {
112+
success = true
113+
break
114+
}
115+
}
116+
if (debug)
117+
log('Is descendant', success)
118+
break
119+
default:
120+
if (debug)
121+
console.warn('Unknown CSS selector type', type, selector, rules)
122+
}
123+
if (!success)
124+
return false
125+
// If this was a combinator, we already advanced ruleIndex
126+
if (type === 'descendant')
127+
return success
128+
// Move to next rule part
129+
if (ruleIndex + 1 < rules.length) {
130+
return handleRules(element, rules, ruleIndex + 1)
116131
}
117132
return success
118133
}

0 commit comments

Comments
 (0)