Skip to content

Commit 51bf98d

Browse files
authored
feat: add is_custm(str: string): boolean helper (#87)
closes #85
1 parent 757909c commit 51bf98d

File tree

4 files changed

+170
-9
lines changed

4 files changed

+170
-9
lines changed

API.md

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ function parse(source: string, options?: ParserOptions): CSSNode
3232
`CSSNode` - Root stylesheet node with the following properties:
3333

3434
**Core Properties:**
35+
3536
- `type` - Node type constant (e.g., `STYLESHEET`, `STYLE_RULE`)
3637
- `type_name` - CSSTree-compatible type name (e.g., `'StyleSheet'`, `'Rule'`)
3738
- `text` - Full text of the node from source
3839

3940
**Content Properties:**
41+
4042
- `name` - Property name for declarations, at-rule name for at-rules, layer name for import layers
4143
- `property` - Alias for `name` (for declarations, more semantic)
4244
- `value` - Value text (for declarations), numeric value (for NUMBER/DIMENSION), string content without quotes (for STRING), URL content (for URL), or `null`
@@ -45,13 +47,15 @@ function parse(source: string, options?: ParserOptions): CSSNode
4547
- `prelude` - At-rule prelude text or `null`
4648

4749
**Location Properties:**
50+
4851
- `line` - Starting line number (1-based)
4952
- `column` - Starting column number (1-based)
5053
- `start` - Starting offset in source (0-based)
5154
- `length` - Length in source
5255
- `end` - End offset in source (calculated as `start + length`)
5356

5457
**Flags:**
58+
5559
- `is_important` - Whether declaration has `!important` (DECLARATION only)
5660
- `is_vendor_prefixed` - Whether node has vendor prefix (checks name/text based on type)
5761
- `has_error` - Whether node has syntax error
@@ -62,34 +66,40 @@ function parse(source: string, options?: ParserOptions): CSSNode
6266
- `has_next` - Whether node has a next sibling
6367

6468
**Tree Structure:**
69+
6570
- `first_child` - First child node or `null`
6671
- `next_sibling` - Next sibling node or `null`
6772
- `children` - Array of all child nodes
6873
- `block` - Block node containing declarations/nested rules (for style rules and at-rules with blocks)
6974
- `is_empty` - Whether block has no declarations or rules (only comments allowed)
7075

7176
**Value Access (Declarations):**
77+
7278
- `values` - Array of value nodes (for declarations)
7379

7480
**Selector Properties:**
81+
7582
- `selector_list` - Selector list from pseudo-classes like `:is()`, `:not()`, `:has()`, `:where()`, or `:nth-child(of)`
7683
- `nth` - An+B formula node from `:nth-child(of)` wrapper (for NTH_OF_SELECTOR nodes)
7784
- `selector` - Selector list from `:nth-child(of)` wrapper (for NTH_OF_SELECTOR nodes)
7885
- `nth_a` - The 'a' coefficient from An+B expressions like `"2n"` from `:nth-child(2n+1)` (for NTH_SELECTOR)
7986
- `nth_b` - The 'b' coefficient from An+B expressions like `"+1"` from `:nth-child(2n+1)` (for NTH_SELECTOR)
8087

8188
**Attribute Selector Properties:**
89+
8290
- `attr_operator` - Attribute operator constant (for ATTRIBUTE_SELECTOR): `ATTR_OPERATOR_NONE`, `ATTR_OPERATOR_EQUAL`, etc.
8391
- `attr_flags` - Attribute flags constant (for ATTRIBUTE_SELECTOR): `ATTR_FLAG_NONE`, `ATTR_FLAG_CASE_INSENSITIVE`, `ATTR_FLAG_CASE_SENSITIVE`
8492

8593
**Compound Selector Helpers (SELECTOR nodes):**
94+
8695
- `compound_parts()` - Iterator over first compound selector parts (zero allocation)
8796
- `first_compound` - Array of parts before first combinator
8897
- `all_compounds` - Array of compound arrays split by combinators
8998
- `is_compound` - Whether selector has no combinators
9099
- `first_compound_text` - Text of first compound selector (no node allocation)
91100

92101
**Methods:**
102+
93103
- `clone(options?)` - Clone node as a mutable plain object with children as arrays
94104

95105
### Example 1: Basic Parsing
@@ -613,11 +623,7 @@ console.log(nodes[0].text) // "(min-width: 768px)"
613623
Walk the AST in depth-first order, calling the callback for each node.
614624

615625
```typescript
616-
function walk(
617-
node: CSSNode,
618-
callback: (node: CSSNode, depth: number) => void | typeof SKIP | typeof BREAK,
619-
depth?: number
620-
): boolean
626+
function walk(node: CSSNode, callback: (node: CSSNode, depth: number) => void | typeof SKIP | typeof BREAK, depth?: number): boolean
621627
```
622628

623629
### Parameters
@@ -696,7 +702,7 @@ function traverse(
696702
options?: {
697703
enter?: (node: CSSNode) => void | typeof SKIP | typeof BREAK
698704
leave?: (node: CSSNode) => void | typeof SKIP | typeof BREAK
699-
}
705+
},
700706
): boolean
701707
```
702708

@@ -730,7 +736,7 @@ traverse(ast, {
730736
leave(node) {
731737
console.log(`${' '.repeat(depth)}Leaving ${node.type_name}`)
732738
depth--
733-
}
739+
},
734740
})
735741
```
736742

@@ -755,7 +761,7 @@ traverse(ast, {
755761
console.log('Leaving media query at depth', depth)
756762
}
757763
depth--
758-
}
764+
},
759765
})
760766
// Output:
761767
// Entering media query at depth 2
@@ -790,7 +796,7 @@ traverse(ast, {
790796
if (node.type === AT_RULE) {
791797
context.pop()
792798
}
793-
}
799+
},
794800
})
795801
// Output:
796802
// Rule: .top
@@ -962,3 +968,91 @@ for (let node of ast) {
962968
}
963969
}
964970
```
971+
972+
---
973+
974+
## `@projectwallace/css-parser/string-utils`
975+
976+
### `is_custom(str)`
977+
978+
Check if a string is a CSS custom property (starts with `--`).
979+
980+
```typescript
981+
import { parse } from '@projectwallace/css-parser'
982+
import { is_custom } from '@projectwallace/css-parser/string-utils'
983+
984+
const ast = parse(':root { --primary: blue; color: red; }')
985+
const block = ast.first_child.block
986+
987+
for (const decl of block.children) {
988+
if (is_custom(decl.name)) {
989+
console.log('Custom property:', decl.name) // Logs: "--primary"
990+
}
991+
}
992+
993+
// Direct usage
994+
is_custom('--primary-color') // true
995+
is_custom('--my-var') // true
996+
is_custom('color') // false
997+
is_custom('-webkit-transform') // false (vendor prefix, not custom)
998+
```
999+
1000+
### `is_vendor_prefixed(str)`
1001+
1002+
Check if a string has a vendor prefix (`-webkit-`, `-moz-`, `-ms-`, `-o-`).
1003+
1004+
```typescript
1005+
import { is_vendor_prefixed } from '@projectwallace/css-parser/string-utils'
1006+
1007+
// Detect vendor prefixes
1008+
is_vendor_prefixed('-webkit-transform') // true
1009+
is_vendor_prefixed('-moz-appearance') // true
1010+
is_vendor_prefixed('-ms-filter') // true
1011+
is_vendor_prefixed('-o-border-image') // true
1012+
1013+
// Not vendor prefixes
1014+
is_vendor_prefixed('--custom-property') // false (custom property)
1015+
is_vendor_prefixed('border-radius') // false (standard property)
1016+
is_vendor_prefixed('transform') // false (no prefix)
1017+
```
1018+
1019+
### `str_equals(a, b)`
1020+
1021+
Case-insensitive string equality check without allocations. The first parameter must be lowercase.
1022+
1023+
```typescript
1024+
import { str_equals } from '@projectwallace/css-parser/string-utils'
1025+
1026+
// First parameter MUST be lowercase
1027+
str_equals('media', 'MEDIA') // true
1028+
str_equals('media', 'Media') // true
1029+
str_equals('media', 'media') // true
1030+
str_equals('media', 'print') // false
1031+
```
1032+
1033+
### `str_starts_with(str, prefix)`
1034+
1035+
Case-insensitive prefix check without allocations. The prefix parameter must be lowercase.
1036+
1037+
```typescript
1038+
import { str_starts_with } from '@projectwallace/css-parser/string-utils'
1039+
1040+
// prefix MUST be lowercase
1041+
str_starts_with('WEBKIT-transform', 'webkit') // true
1042+
str_starts_with('Mozilla', 'moz') // true
1043+
str_starts_with('transform', 'trans') // true
1044+
str_starts_with('color', 'border') // false
1045+
```
1046+
1047+
### `str_index_of(str, search)`
1048+
1049+
Case-insensitive substring search without allocations. Returns the index of the first occurrence. The search parameter must be lowercase.
1050+
1051+
```typescript
1052+
import { str_index_of } from '@projectwallace/css-parser/string-utils'
1053+
1054+
// search MUST be lowercase
1055+
str_index_of('background-COLOR', 'color') // 11
1056+
str_index_of('HELLO', 'e') // 1
1057+
str_index_of('transform', 'x') // -1 (not found)
1058+
```

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { parse_declaration } from './parse-declaration'
88
export { parse_value } from './parse-value'
99
export { tokenize } from './tokenize'
1010
export { walk, traverse, SKIP, BREAK } from './walk'
11+
export { is_custom, is_vendor_prefixed, str_equals, str_starts_with, str_index_of } from './string-utils'
1112

1213
// Advanced/class-based API
1314
export { type ParserOptions } from './parse'

src/string-utils.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
str_starts_with,
66
str_index_of,
77
is_vendor_prefixed,
8+
is_custom,
89
CHAR_SPACE,
910
CHAR_TAB,
1011
CHAR_NEWLINE,
@@ -350,6 +351,52 @@ describe('string-utils', () => {
350351
expect(is_vendor_prefixed(source, 6, 20)).toBe(true) // "-webkit-suffix"
351352
})
352353
})
354+
355+
describe('is_custom', () => {
356+
it('should detect custom property with --', () => {
357+
expect(is_custom('--primary-color')).toBe(true)
358+
})
359+
360+
it('should detect another custom property', () => {
361+
expect(is_custom('--my-var')).toBe(true)
362+
})
363+
364+
it('should detect shortest valid custom property', () => {
365+
expect(is_custom('--x')).toBe(true)
366+
})
367+
368+
it('should not detect exactly two hyphens', () => {
369+
expect(is_custom('--')).toBe(false)
370+
})
371+
372+
it('should not detect vendor prefix as custom', () => {
373+
expect(is_custom('-webkit-transform')).toBe(false)
374+
})
375+
376+
it('should not detect -moz- vendor prefix as custom', () => {
377+
expect(is_custom('-moz-appearance')).toBe(false)
378+
})
379+
380+
it('should not detect standard property with hyphen as custom', () => {
381+
expect(is_custom('border-radius')).toBe(false)
382+
})
383+
384+
it('should not detect standard property as custom', () => {
385+
expect(is_custom('color')).toBe(false)
386+
})
387+
388+
it('should not detect single hyphen as custom', () => {
389+
expect(is_custom('-')).toBe(false)
390+
})
391+
392+
it('should not detect empty string as custom', () => {
393+
expect(is_custom('')).toBe(false)
394+
})
395+
396+
it('should not detect single character as custom', () => {
397+
expect(is_custom('a')).toBe(false)
398+
})
399+
})
353400
})
354401

355402
describe('str_index_of', () => {

src/string-utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,22 @@ export function is_vendor_prefixed(source: string, start?: number, end?: number)
199199
}
200200
return false
201201
}
202+
203+
/**
204+
* Check if a string is a CSS custom property (starts with --)
205+
*
206+
* @param str - The string to check
207+
* @returns true if the string starts with -- (custom property)
208+
*
209+
* Examples:
210+
* - `--primary-color` → true
211+
* - `--my-var` → true
212+
* - `-webkit-transform` → false (vendor prefix, not custom)
213+
* - `border-radius` → false (standard property)
214+
* - `color` → false
215+
*/
216+
export function is_custom(str: string): boolean {
217+
// Must start with two hyphens and have at least one character after
218+
if (str.length < 3) return false
219+
return str.charCodeAt(0) === CHAR_MINUS_HYPHEN && str.charCodeAt(1) === CHAR_MINUS_HYPHEN
220+
}

0 commit comments

Comments
 (0)