@projectwallace/css-parser 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/css-node.js CHANGED
@@ -1,5 +1,5 @@
1
- import { DIMENSION, NUMBER, DECLARATION, FLAG_IMPORTANT, FLAG_VENDOR_PREFIXED, FLAG_HAS_ERROR, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, STYLE_RULE, BLOCK, AT_RULE, COMMENT, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, FLAG_HAS_PARENS, NTH_SELECTOR, NTH_OF_SELECTOR, SELECTOR_LIST, SELECTOR, COMBINATOR, ATTRIBUTE_SELECTOR, PRELUDE_OPERATOR, LAYER_NAME, SUPPORTS_QUERY, CONTAINER_QUERY, MEDIA_TYPE, MEDIA_FEATURE, MEDIA_QUERY, LANG_SELECTOR, NESTING_SELECTOR, UNIVERSAL_SELECTOR, ID_SELECTOR, CLASS_SELECTOR, TYPE_SELECTOR, URL, PARENTHESIS, OPERATOR, FUNCTION, HASH, STRING, IDENTIFIER, STYLESHEET } from './arena.js';
2
- import { is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
1
+ import { URL, STRING, DIMENSION, NUMBER, DECLARATION, FLAG_IMPORTANT, FLAG_VENDOR_PREFIXED, FLAG_HAS_ERROR, FLAG_HAS_BLOCK, FLAG_HAS_DECLARATIONS, STYLE_RULE, BLOCK, AT_RULE, COMMENT, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, FLAG_HAS_PARENS, NTH_SELECTOR, NTH_OF_SELECTOR, SELECTOR_LIST, SELECTOR, COMBINATOR, ATTRIBUTE_SELECTOR, PRELUDE_OPERATOR, LAYER_NAME, SUPPORTS_QUERY, CONTAINER_QUERY, MEDIA_TYPE, MEDIA_FEATURE, MEDIA_QUERY, LANG_SELECTOR, NESTING_SELECTOR, UNIVERSAL_SELECTOR, ID_SELECTOR, CLASS_SELECTOR, TYPE_SELECTOR, PARENTHESIS, OPERATOR, FUNCTION, HASH, IDENTIFIER, STYLESHEET } from './arena.js';
2
+ import { str_starts_with, is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
3
3
  import { parse_dimension } from './parse-utils.js';
4
4
 
5
5
  const TYPE_NAMES = {
@@ -82,7 +82,26 @@ class CSSNode {
82
82
  // Get the value text (for declarations: "blue" in "color: blue")
83
83
  // For dimension/number nodes: returns the numeric value as a number
84
84
  // For string nodes: returns the string content without quotes
85
+ // For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
86
+ // For URL nodes with unquoted URL: returns the URL content without quotes
85
87
  get value() {
88
+ if (this.type === URL) {
89
+ const firstChild = this.first_child;
90
+ if (firstChild && firstChild.type === STRING) {
91
+ return firstChild.text;
92
+ }
93
+ const text = this.text;
94
+ if (str_starts_with(text, "url(")) {
95
+ const openParen = text.indexOf("(");
96
+ const closeParen = text.lastIndexOf(")");
97
+ if (openParen !== -1 && closeParen !== -1 && closeParen > openParen) {
98
+ let content = text.substring(openParen + 1, closeParen).trim();
99
+ return content;
100
+ }
101
+ } else if (text.startsWith('"') || text.startsWith("'")) {
102
+ return text;
103
+ }
104
+ }
86
105
  if (this.type === DIMENSION || this.type === NUMBER) {
87
106
  return parse_dimension(this.text).value;
88
107
  }
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from './lexer.js';
2
2
  import { NTH_SELECTOR, CSSDataArena } from './arena.js';
3
3
  import { TOKEN_IDENT, TOKEN_DELIM, TOKEN_DIMENSION, TOKEN_NUMBER } from './token-types.js';
4
- import { CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils.js';
4
+ import { str_equals, CHAR_MINUS_HYPHEN, CHAR_PLUS, str_index_of } from './string-utils.js';
5
5
  import { skip_whitespace_forward } from './parse-utils.js';
6
6
  import { CSSNode } from './css-node.js';
7
7
 
@@ -36,8 +36,8 @@ class ANplusBParser {
36
36
  }
37
37
  this.lexer.next_token_fast(true);
38
38
  if (this.lexer.token_type === TOKEN_IDENT) {
39
- const text = this.source.substring(this.lexer.token_start, this.lexer.token_end).toLowerCase();
40
- if (text === "odd" || text === "even") {
39
+ const text = this.source.substring(this.lexer.token_start, this.lexer.token_end);
40
+ if (str_equals("odd", text) || str_equals("even", text)) {
41
41
  a_start = this.lexer.token_start;
42
42
  a_end = this.lexer.token_end;
43
43
  return this.create_anplusb_node(node_start, a_start, a_end, 0, 0);
@@ -118,7 +118,7 @@ class ANplusBParser {
118
118
  }
119
119
  if (this.lexer.token_type === TOKEN_DIMENSION) {
120
120
  const token_text = this.source.substring(this.lexer.token_start, this.lexer.token_end);
121
- const n_index = token_text.toLowerCase().indexOf("n");
121
+ const n_index = str_index_of(token_text, "n");
122
122
  if (n_index !== -1) {
123
123
  a_start = this.lexer.token_start;
124
124
  a_end = this.lexer.token_start + n_index + 1;
@@ -2,7 +2,7 @@ import { Lexer } from './lexer.js';
2
2
  import { CSSDataArena, SELECTOR_LIST, SELECTOR, COMBINATOR, NESTING_SELECTOR, ID_SELECTOR, TYPE_SELECTOR, UNIVERSAL_SELECTOR, CLASS_SELECTOR, ATTRIBUTE_SELECTOR, ATTR_OPERATOR_NONE, ATTR_FLAG_NONE, ATTR_OPERATOR_EQUAL, ATTR_OPERATOR_TILDE_EQUAL, ATTR_OPERATOR_PIPE_EQUAL, ATTR_OPERATOR_CARET_EQUAL, ATTR_OPERATOR_DOLLAR_EQUAL, ATTR_OPERATOR_STAR_EQUAL, ATTR_FLAG_CASE_INSENSITIVE, ATTR_FLAG_CASE_SENSITIVE, PSEUDO_ELEMENT_SELECTOR, PSEUDO_CLASS_SELECTOR, FLAG_VENDOR_PREFIXED, FLAG_HAS_PARENS, LANG_SELECTOR, NTH_OF_SELECTOR } from './arena.js';
3
3
  import { TOKEN_COMMA, TOKEN_DELIM, TOKEN_EOF, TOKEN_WHITESPACE, TOKEN_FUNCTION, TOKEN_COLON, TOKEN_LEFT_BRACKET, TOKEN_HASH, TOKEN_IDENT, TOKEN_RIGHT_BRACKET, TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN, TOKEN_STRING } from './token-types.js';
4
4
  import { skip_whitespace_and_comments_forward, skip_whitespace_and_comments_backward, skip_whitespace_forward } from './parse-utils.js';
5
- import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, is_vendor_prefixed } from './string-utils.js';
5
+ import { CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_PERIOD, CHAR_ASTERISK, CHAR_AMPERSAND, CHAR_PIPE, CHAR_SPACE, CHAR_NEWLINE, CHAR_CARRIAGE_RETURN, CHAR_FORM_FEED, is_combinator, is_whitespace, CHAR_EQUALS, CHAR_CARET, CHAR_DOLLAR, CHAR_SINGLE_QUOTE, CHAR_DOUBLE_QUOTE, CHAR_COLON, is_vendor_prefixed, str_equals } from './string-utils.js';
6
6
  import { ANplusBParser } from './parse-anplusb.js';
7
7
  import { CSSNode } from './css-node.js';
8
8
 
@@ -466,19 +466,19 @@ class SelectorParser {
466
466
  this.arena.set_flag(node, FLAG_VENDOR_PREFIXED);
467
467
  }
468
468
  if (content_end > content_start) {
469
- let func_name = this.source.substring(func_name_start, func_name_end).toLowerCase();
470
- if (this.is_nth_pseudo(func_name)) {
469
+ let func_name_substr = this.source.substring(func_name_start, func_name_end);
470
+ if (this.is_nth_pseudo(func_name_substr)) {
471
471
  let child = this.parse_nth_expression(content_start, content_end);
472
472
  if (child !== null) {
473
473
  this.arena.set_first_child(node, child);
474
474
  this.arena.set_last_child(node, child);
475
475
  }
476
- } else if (func_name === "lang") {
476
+ } else if (str_equals("lang", func_name_substr)) {
477
477
  this.parse_lang_identifiers(content_start, content_end, node);
478
478
  } else {
479
479
  let saved_selector_end = this.selector_end;
480
480
  const saved = this.lexer.save_position();
481
- let allow_relative = func_name === "has";
481
+ let allow_relative = str_equals("has", func_name_substr);
482
482
  let child_selector = this.parse_selector(content_start, content_end, this.lexer.line, this.lexer.column, allow_relative);
483
483
  this.selector_end = saved_selector_end;
484
484
  this.lexer.restore_position(saved);
@@ -492,7 +492,7 @@ class SelectorParser {
492
492
  }
493
493
  // Check if pseudo-class name is an nth-* pseudo
494
494
  is_nth_pseudo(name) {
495
- return name === "nth-child" || name === "nth-last-child" || name === "nth-of-type" || name === "nth-last-of-type" || name === "nth-col" || name === "nth-last-col";
495
+ return str_equals("nth-child", name) || str_equals("nth-last-child", name) || str_equals("nth-of-type", name) || str_equals("nth-last-of-type", name) || str_equals("nth-col", name) || str_equals("nth-last-col", name);
496
496
  }
497
497
  // Parse :lang() content - comma-separated language identifiers
498
498
  // Accepts both quoted strings: :lang("en", "fr") and unquoted: :lang(en, fr)
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from './lexer.js';
2
2
  import { CSSDataArena, OPERATOR, HASH, STRING, DIMENSION, NUMBER, IDENTIFIER, URL, FUNCTION, PARENTHESIS } from './arena.js';
3
3
  import { TOKEN_EOF, TOKEN_LEFT_PAREN, TOKEN_COMMA, TOKEN_DELIM, TOKEN_FUNCTION, TOKEN_HASH, TOKEN_STRING, TOKEN_DIMENSION, TOKEN_PERCENTAGE, TOKEN_NUMBER, TOKEN_IDENT, TOKEN_RIGHT_PAREN } from './token-types.js';
4
- import { is_whitespace, CHAR_PLUS, CHAR_MINUS_HYPHEN, CHAR_ASTERISK, CHAR_FORWARD_SLASH } from './string-utils.js';
4
+ import { is_whitespace, CHAR_PLUS, CHAR_MINUS_HYPHEN, CHAR_ASTERISK, CHAR_FORWARD_SLASH, str_equals } from './string-utils.js';
5
5
  import { CSSNode } from './css-node.js';
6
6
 
7
7
  class ValueParser {
@@ -98,9 +98,9 @@ class ValueParser {
98
98
  }
99
99
  parse_function_node(start, end) {
100
100
  let name_end = end - 1;
101
- let func_name = this.source.substring(start, name_end).toLowerCase();
101
+ let func_name_substr = this.source.substring(start, name_end);
102
102
  let node = this.arena.create_node(
103
- func_name === "url" ? URL : FUNCTION,
103
+ str_equals("url", func_name_substr) ? URL : FUNCTION,
104
104
  start,
105
105
  0,
106
106
  // length unknown yet
@@ -109,7 +109,7 @@ class ValueParser {
109
109
  );
110
110
  this.arena.set_content_start_delta(node, 0);
111
111
  this.arena.set_content_length(node, name_end - start);
112
- if (func_name === "url" || func_name === "src") {
112
+ if (str_equals("url", func_name_substr) || str_equals("src", func_name_substr)) {
113
113
  let save_pos = this.lexer.save_position();
114
114
  this.lexer.next_token_fast(false);
115
115
  while (this.is_whitespace_inline() && this.lexer.pos < this.value_end) {
@@ -23,6 +23,27 @@ export declare const CHAR_COLON = 58;
23
23
  * @param b Compare string
24
24
  */
25
25
  export declare function str_equals(a: string, b: string): boolean;
26
+ /**
27
+ * Case-insensitive ASCII prefix check without allocations
28
+ * Returns true if string `str` starts with prefix (case-insensitive)
29
+ *
30
+ * IMPORTANT: prefix MUST be lowercase for correct comparison
31
+ *
32
+ * @param str - The string to check
33
+ * @param prefix - The lowercase prefix to match against
34
+ */
35
+ export declare function str_starts_with(str: string, prefix: string): boolean;
36
+ /**
37
+ * Case-insensitive character/substring search without allocations
38
+ * Returns the index of the first occurrence of searchChar (case-insensitive)
39
+ *
40
+ * IMPORTANT: searchChar MUST be lowercase for correct comparison
41
+ *
42
+ * @param str - The string to search in
43
+ * @param searchChar - The lowercase character/substring to find
44
+ * @returns The index of the first match, or -1 if not found
45
+ */
46
+ export declare function str_index_of(str: string, searchChar: string): number;
26
47
  /**
27
48
  * Check if a string range has a vendor prefix
28
49
  *
@@ -41,6 +41,52 @@ function str_equals(a, b) {
41
41
  }
42
42
  return true;
43
43
  }
44
+ function str_starts_with(str, prefix) {
45
+ if (str.length < prefix.length) {
46
+ return false;
47
+ }
48
+ for (let i = 0; i < prefix.length; i++) {
49
+ let ca = str.charCodeAt(i);
50
+ let cb = prefix.charCodeAt(i);
51
+ if (ca >= 65 && ca <= 90) ca |= 32;
52
+ if (ca !== cb) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ function str_index_of(str, searchChar) {
59
+ if (searchChar.length === 0) {
60
+ return -1;
61
+ }
62
+ if (searchChar.length === 1) {
63
+ const searchCode = searchChar.charCodeAt(0);
64
+ for (let i = 0; i < str.length; i++) {
65
+ let ca = str.charCodeAt(i);
66
+ if (ca >= 65 && ca <= 90) ca |= 32;
67
+ if (ca === searchCode) {
68
+ return i;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+ for (let i = 0; i <= str.length - searchChar.length; i++) {
74
+ let match = true;
75
+ for (let j = 0; j < searchChar.length; j++) {
76
+ let ca = str.charCodeAt(i + j);
77
+ let cb = searchChar.charCodeAt(j);
78
+ if (ca >= 65 && ca <= 90) ca |= 32;
79
+ if (ca !== cb) {
80
+ match = false;
81
+ break;
82
+ }
83
+ }
84
+ if (match) {
85
+ return i;
86
+ }
87
+ }
88
+ return -1;
89
+ }
44
90
  function is_vendor_prefixed(source, start, end) {
45
91
  if (source.charCodeAt(start) !== CHAR_MINUS_HYPHEN) {
46
92
  return false;
@@ -60,4 +106,4 @@ function is_vendor_prefixed(source, start, end) {
60
106
  return false;
61
107
  }
62
108
 
63
- export { CHAR_AMPERSAND, CHAR_ASTERISK, CHAR_CARET, CHAR_CARRIAGE_RETURN, CHAR_COLON, CHAR_DOLLAR, CHAR_DOUBLE_QUOTE, CHAR_EQUALS, CHAR_FORM_FEED, CHAR_FORWARD_SLASH, CHAR_GREATER_THAN, CHAR_MINUS_HYPHEN, CHAR_NEWLINE, CHAR_PERIOD, CHAR_PIPE, CHAR_PLUS, CHAR_SINGLE_QUOTE, CHAR_SPACE, CHAR_TAB, CHAR_TILDE, is_combinator, is_digit, is_vendor_prefixed, is_whitespace, str_equals };
109
+ export { CHAR_AMPERSAND, CHAR_ASTERISK, CHAR_CARET, CHAR_CARRIAGE_RETURN, CHAR_COLON, CHAR_DOLLAR, CHAR_DOUBLE_QUOTE, CHAR_EQUALS, CHAR_FORM_FEED, CHAR_FORWARD_SLASH, CHAR_GREATER_THAN, CHAR_MINUS_HYPHEN, CHAR_NEWLINE, CHAR_PERIOD, CHAR_PIPE, CHAR_PLUS, CHAR_SINGLE_QUOTE, CHAR_SPACE, CHAR_TAB, CHAR_TILDE, is_combinator, is_digit, is_vendor_prefixed, is_whitespace, str_equals, str_index_of, str_starts_with };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectwallace/css-parser",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "High-performance CSS lexer and parser, optimized for CSS inspection and analysis",
5
5
  "author": "Bart Veneman <bat@projectwallace.com>",
6
6
  "license": "MIT",