Skip to content

Commit 7dedafd

Browse files
committed
feat: add is_vendor_prefixed to property nodes
1 parent 711e039 commit 7dedafd

File tree

4 files changed

+164
-13
lines changed

4 files changed

+164
-13
lines changed

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const FLAG_IMPORTANT = 1 << 0 // Has !important
6969
export const FLAG_HAS_ERROR = 1 << 1 // Syntax error
7070
export const FLAG_LENGTH_OVERFLOW = 1 << 2 // Node > 65k chars
7171
export const FLAG_HAS_BLOCK = 1 << 3 // Has { } block (for style rules and at-rules)
72+
export const FLAG_VENDOR_PREFIXED = 1 << 4 // Has vendor prefix (-webkit-, -moz-, -ms-, -o-)
7273

7374
export class CSSDataArena {
7475
private buffer: ArrayBuffer

src/css-node.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
FLAG_IMPORTANT,
3939
FLAG_HAS_ERROR,
4040
FLAG_HAS_BLOCK,
41+
FLAG_VENDOR_PREFIXED,
4142
} from './arena'
4243

4344
// Node type constants (numeric for performance)
@@ -138,11 +139,9 @@ export class CSSNode {
138139
return this.arena.has_flag(this.index, FLAG_IMPORTANT)
139140
}
140141

141-
// Check if this has a vendor prefix (lazy computation for performance)
142+
// Check if this has a vendor prefix (flag-based for performance)
142143
get is_vendor_prefixed(): boolean {
143-
const name = this.name
144-
if (!name) return false
145-
return name.startsWith('-webkit-') || name.startsWith('-moz-') || name.startsWith('-ms-') || name.startsWith('-o-')
144+
return this.arena.has_flag(this.index, FLAG_VENDOR_PREFIXED)
146145
}
147146

148147
// Check if this node has an error

src/parser.test.ts

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { describe, test, expect } from 'vitest'
2-
import { Parser } from './parser'
3-
// TODO: the node types should be re-exported from parser
4-
import { NODE_STYLESHEET, NODE_STYLE_RULE, NODE_AT_RULE, NODE_DECLARATION } from './arena'
2+
import { Parser, NODE_STYLESHEET, NODE_STYLE_RULE, NODE_AT_RULE, NODE_DECLARATION } from './parser'
53

64
describe('Parser', () => {
75
describe('basic parsing', () => {
@@ -201,12 +199,12 @@ describe('Parser', () => {
201199
test('should parse @import', () => {
202200
const source = '@import url("style.css");'
203201
const parser = new Parser(source, { parse_atrule_preludes: false })
204-
const root = parser.parse()
202+
const root = parser.parse()
205203

206-
const atRule = root.first_child!
207-
expect(atRule.type).toBe(NODE_AT_RULE)
208-
expect(atRule.name).toBe('import')
209-
expect(atRule.has_children).toBe(false)
204+
const atRule = root.first_child!
205+
expect(atRule.type).toBe(NODE_AT_RULE)
206+
expect(atRule.name).toBe('import')
207+
expect(atRule.has_children).toBe(false)
210208
})
211209

212210
test('should parse @namespace', () => {
@@ -708,6 +706,138 @@ describe('Parser', () => {
708706
})
709707
})
710708

709+
describe('vendor prefix detection', () => {
710+
test('should detect -webkit- vendor prefix', () => {
711+
let source = '.box { -webkit-transform: scale(1); }'
712+
let parser = new Parser(source)
713+
let root = parser.parse()
714+
715+
let rule = root.first_child!
716+
let [_selector, decl] = rule.children
717+
expect(decl.name).toBe('-webkit-transform')
718+
expect(decl.is_vendor_prefixed).toBe(true)
719+
})
720+
721+
test('should detect -moz- vendor prefix', () => {
722+
let source = '.box { -moz-transform: scale(1); }'
723+
let parser = new Parser(source)
724+
let root = parser.parse()
725+
726+
let rule = root.first_child!
727+
let [_selector, decl] = rule.children
728+
expect(decl.name).toBe('-moz-transform')
729+
expect(decl.is_vendor_prefixed).toBe(true)
730+
})
731+
732+
test('should detect -ms- vendor prefix', () => {
733+
let source = '.box { -ms-transform: scale(1); }'
734+
let parser = new Parser(source)
735+
let root = parser.parse()
736+
737+
let rule = root.first_child!
738+
let [_selector, decl] = rule.children
739+
expect(decl.name).toBe('-ms-transform')
740+
expect(decl.is_vendor_prefixed).toBe(true)
741+
})
742+
743+
test('should detect -o- vendor prefix', () => {
744+
let source = '.box { -o-transform: scale(1); }'
745+
let parser = new Parser(source)
746+
let root = parser.parse()
747+
748+
let rule = root.first_child!
749+
let [_selector, decl] = rule.children
750+
expect(decl.name).toBe('-o-transform')
751+
expect(decl.is_vendor_prefixed).toBe(true)
752+
})
753+
754+
test('should not detect vendor prefix for standard properties', () => {
755+
let source = '.box { transform: scale(1); }'
756+
let parser = new Parser(source)
757+
let root = parser.parse()
758+
759+
let rule = root.first_child!
760+
let [_selector, decl] = rule.children
761+
expect(decl.name).toBe('transform')
762+
expect(decl.is_vendor_prefixed).toBe(false)
763+
})
764+
765+
test('should not detect vendor prefix for properties with hyphens', () => {
766+
let source = '.box { background-color: red; }'
767+
let parser = new Parser(source)
768+
let root = parser.parse()
769+
770+
let rule = root.first_child!
771+
let [_selector, decl] = rule.children
772+
expect(decl.name).toBe('background-color')
773+
expect(decl.is_vendor_prefixed).toBe(false)
774+
})
775+
776+
test('should not detect vendor prefix for custom properties', () => {
777+
let source = ':root { --primary-color: blue; }'
778+
let parser = new Parser(source)
779+
let root = parser.parse()
780+
781+
let rule = root.first_child!
782+
let [_selector, decl] = rule.children
783+
expect(decl.name).toBe('--primary-color')
784+
expect(decl.is_vendor_prefixed).toBe(false)
785+
})
786+
787+
test('should detect vendor prefix with multiple vendor-prefixed properties', () => {
788+
let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }'
789+
let parser = new Parser(source)
790+
let root = parser.parse()
791+
792+
let rule = root.first_child!
793+
let [_selector, webkit, moz, standard] = rule.children
794+
795+
expect(webkit.name).toBe('-webkit-transform')
796+
expect(webkit.is_vendor_prefixed).toBe(true)
797+
798+
expect(moz.name).toBe('-moz-transform')
799+
expect(moz.is_vendor_prefixed).toBe(true)
800+
801+
expect(standard.name).toBe('transform')
802+
expect(standard.is_vendor_prefixed).toBe(false)
803+
})
804+
805+
test('should detect vendor prefix for complex property names', () => {
806+
let source = '.box { -webkit-border-top-left-radius: 5px; }'
807+
let parser = new Parser(source)
808+
let root = parser.parse()
809+
810+
let rule = root.first_child!
811+
let [_selector, decl] = rule.children
812+
expect(decl.name).toBe('-webkit-border-top-left-radius')
813+
expect(decl.is_vendor_prefixed).toBe(true)
814+
})
815+
816+
test('should not detect vendor prefix for similar but non-vendor properties', () => {
817+
// Edge case: property that starts with hyphen but isn't a vendor prefix
818+
let source = '.box { border-radius: 5px; }'
819+
let parser = new Parser(source)
820+
let root = parser.parse()
821+
822+
let rule = root.first_child!
823+
let [_selector, decl] = rule.children
824+
expect(decl.name).toBe('border-radius')
825+
expect(decl.is_vendor_prefixed).toBe(false)
826+
})
827+
828+
test('should return false for nodes without names', () => {
829+
// Nodes like selectors or at-rules without property names
830+
let source = 'body { }'
831+
let parser = new Parser(source)
832+
let root = parser.parse()
833+
834+
let rule = root.first_child!
835+
let selector = rule.first_child!
836+
// Selectors have text but checking is_vendor_prefixed should be safe
837+
expect(selector.is_vendor_prefixed).toBe(false)
838+
})
839+
})
840+
711841
describe('complex real-world scenarios', () => {
712842
test('should parse complex nested structure', () => {
713843
let source = `

src/parser.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
// CSS Parser - Builds AST using the arena
22
import { Lexer } from './lexer'
3-
import { CSSDataArena, NODE_STYLESHEET, NODE_STYLE_RULE, NODE_SELECTOR, NODE_DECLARATION, NODE_AT_RULE, FLAG_IMPORTANT, FLAG_HAS_BLOCK } from './arena'
3+
import {
4+
CSSDataArena,
5+
NODE_STYLESHEET,
6+
NODE_STYLE_RULE,
7+
NODE_SELECTOR,
8+
NODE_DECLARATION,
9+
NODE_AT_RULE,
10+
FLAG_IMPORTANT,
11+
FLAG_HAS_BLOCK,
12+
FLAG_VENDOR_PREFIXED,
13+
} from './arena'
414
import { CSSNode } from './css-node'
515
import { ValueParser } from './value-parser'
616
import { SelectorParser } from './selector-parser'
@@ -17,6 +27,8 @@ import {
1727
} from './token-types'
1828
import { trim_boundaries } from './string-utils'
1929

30+
const MINUS_HYPHEN = 45
31+
2032
export interface ParserOptions {
2133
skip_comments?: boolean
2234
parse_values?: boolean
@@ -272,6 +284,15 @@ export class Parser {
272284
this.arena.set_content_start(declaration, prop_start)
273285
this.arena.set_content_length(declaration, prop_end - prop_start)
274286

287+
// Check for vendor prefix and set flag if detected
288+
if (this.source.charCodeAt(prop_start) === MINUS_HYPHEN && this.source.charCodeAt(prop_start + 1) !== MINUS_HYPHEN) {
289+
let prop_length = prop_end - prop_start
290+
// Check for -webkit- (8 chars min), -moz- (5 chars), -ms- (4 chars), -o- (3 chars)
291+
if (prop_length >= 3 && this.source.indexOf('-', prop_start + 2) !== -1) {
292+
this.arena.set_flag(declaration, FLAG_VENDOR_PREFIXED)
293+
}
294+
}
295+
275296
// Track value start (after colon, skipping whitespace)
276297
let value_start = this.lexer.token_start
277298
let value_end = value_start

0 commit comments

Comments
 (0)