Skip to content

Commit c375f50

Browse files
bartvenemanclaude
andauthored
Breaking: Add zero-cost TypeScript node type narrowing layer (#205)
## Summary This PR introduces a comprehensive TypeScript type narrowing system for CSS AST nodes, enabling type-safe node handling without runtime overhead. The implementation provides both type predicate functions and a discriminated union type for exhaustive narrowing patterns. ## Key Changes - **New `src/node-types.ts`**: Defines 39 specialized node interfaces that extend `CSSNode` with precise property types, removing spurious `undefined` values from type-specific properties - Each interface overrides property types to reflect what a specific node type actually returns - Examples: `DeclarationNode.property` is `string` (not `string | undefined`), `DimensionNode.unit` is `string` (not `string | undefined`) - **Type predicate functions**: 39 individual `is_*()` functions for runtime type narrowing - Each compiles to a single integer comparison (zero heap allocation) - All are individually tree-shakeable named exports - Enable idiomatic TypeScript patterns: `if (is_declaration(node)) { node.property /* string */ }` - **`AnyCssNode` discriminated union**: Enables exhaustive switch-based narrowing without explicit type predicates - Allows automatic type narrowing in switch statements based on `node.type` - Useful for walk callbacks and visitor patterns - **Comprehensive test suite** (`src/node-types.test.ts`): - Runtime behavior tests for all major type predicates - Compile-time type assertion tests using `expectTypeOf` to verify narrowing works correctly - Tests for selector subtypes and complex nested structures - **Updated exports** in `src/index.ts`: All node type interfaces and predicate functions are now publicly exported - **Updated `src/walk.ts`**: Walk callback parameter type changed from `CSSNode` to `AnyCssNode` for better type inference in user code - **Updated `src/constants.ts`**: Added missing `UNICODE_RANGE` export ## Implementation Details - All interfaces are erased at compile time — zero bytes in the JS output - Type predicates compile to single integer comparisons with no runtime overhead - The design follows TypeScript best practices for discriminated unions and type guards - Comprehensive documentation in comments explains usage patterns and benefits https://claude.ai/code/session_01AutHjiFtdrfyBLJ64JwXnM --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4064188 commit c375f50

22 files changed

+2445
-1423
lines changed

API.md

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,6 @@ The `CommentInfo` object passed to `on_comment` contains:
9494
- `nth_a` - The 'a' coefficient from An+B expressions like `"2n"` from `:nth-child(2n+1)` (for NTH_SELECTOR)
9595
- `nth_b` - The 'b' coefficient from An+B expressions like `"+1"` from `:nth-child(2n+1)` (for NTH_SELECTOR)
9696

97-
**Attribute Selector Properties:**
98-
99-
- `attr_operator` - Attribute operator constant (for ATTRIBUTE_SELECTOR): `ATTR_OPERATOR_NONE`, `ATTR_OPERATOR_EQUAL`, etc.
100-
- `attr_flags` - Attribute flags constant (for ATTRIBUTE_SELECTOR): `ATTR_FLAG_NONE`, `ATTR_FLAG_CASE_INSENSITIVE`, `ATTR_FLAG_CASE_SENSITIVE`
101-
10297
**Methods:**
10398

10499
- `clone(options?)` - Clone node as a mutable plain object with children as arrays
@@ -902,48 +897,6 @@ const pseudoClass2 = ast2.first_child.first_child
902897
console.log(pseudoClass2.has_children) // false - no function syntax
903898
```
904899

905-
## Attribute Selector Constants
906-
907-
### Attribute Selector Operators
908-
909-
Use these constants with the `node.attr_operator` property to identify the operator in attribute selectors:
910-
911-
- `ATTR_OPERATOR_NONE` (0) - No operator (e.g., `[disabled]`)
912-
- `ATTR_OPERATOR_EQUAL` (1) - Exact match (e.g., `[type="text"]`)
913-
- `ATTR_OPERATOR_TILDE_EQUAL` (2) - Whitespace-separated list contains (e.g., `[class~="active"]`)
914-
- `ATTR_OPERATOR_PIPE_EQUAL` (3) - Starts with or is followed by hyphen (e.g., `[lang|="en"]`)
915-
- `ATTR_OPERATOR_CARET_EQUAL` (4) - Starts with (e.g., `[href^="https"]`)
916-
- `ATTR_OPERATOR_DOLLAR_EQUAL` (5) - Ends with (e.g., `[href$=".pdf"]`)
917-
- `ATTR_OPERATOR_STAR_EQUAL` (6) - Contains substring (e.g., `[href*="example"]`)
918-
919-
### Attribute Selector Flags
920-
921-
Use these constants with the `node.attr_flags` property to identify case sensitivity flags in attribute selectors:
922-
923-
- `ATTR_FLAG_NONE` (0) - No flag specified (default case sensitivity)
924-
- `ATTR_FLAG_CASE_INSENSITIVE` (1) - Case-insensitive matching (e.g., `[type="text" i]`)
925-
- `ATTR_FLAG_CASE_SENSITIVE` (2) - Case-sensitive matching (e.g., `[type="text" s]`)
926-
927-
#### Example
928-
929-
```javascript
930-
import {
931-
parse_selector,
932-
ATTRIBUTE_SELECTOR,
933-
ATTR_OPERATOR_EQUAL,
934-
ATTR_FLAG_CASE_INSENSITIVE,
935-
} from '@projectwallace/css-parser'
936-
937-
const ast = parse_selector('[type="text" i]')
938-
939-
for (let node of ast) {
940-
if (node.type === ATTRIBUTE_SELECTOR) {
941-
console.log(node.attr_operator === ATTR_OPERATOR_EQUAL) // true
942-
console.log(node.attr_flags === ATTR_FLAG_CASE_INSENSITIVE) // true
943-
}
944-
}
945-
```
946-
947900
---
948901

949902
## `@projectwallace/css-parser/string-utils`

0 commit comments

Comments
 (0)