• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

david-luna / csset / 19246794314

10 Nov 2025 09:32PM UTC coverage: 95.408%. Remained the same
19246794314

push

github

david-luna
Merge branch 'main' of github.com:david-luna/csset

386 of 409 branches covered (94.38%)

Branch coverage included in aggregate %.

176 of 176 new or added lines in 8 files covered. (100.0%)

35 existing lines in 3 files now uncovered.

1754 of 1834 relevant lines covered (95.64%)

128.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.13
/lib/parser/tokenize.js
1
// MIT License
4✔
2

4✔
3
// Copyright (c) 2020 Lea Verou
4✔
4

4✔
5
// Permission is hereby granted, free of charge, to any person obtaining a copy
4✔
6
// of this software and associated documentation files (the "Software"), to deal
4✔
7
// in the Software without restriction, including without limitation the rights
4✔
8
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
4✔
9
// copies of the Software, and to permit persons to whom the Software is
4✔
10
// furnished to do so, subject to the following conditions:
4✔
11

4✔
12
// The above copyright notice and this permission notice shall be included in all
4✔
13
// copies or substantial portions of the Software.
4✔
14

4✔
15
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4✔
16
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4✔
17
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
4✔
18
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
4✔
19
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
4✔
20
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4✔
21
// SOFTWARE.
4✔
22

4✔
23
import { gobbleParens } from './gobbleParens.js';
4✔
24
import { TOKENS, TOKENS_FOR_RESTORE, TOKENS_WITH_PARENS, TOKENS_WITH_STRINGS, TRIM_TOKENS } from './tokens.js';
4✔
25

4✔
26
/**
4✔
27
 * @typedef {Record<string, RegExp>} Grammar
4✔
28
 * @typedef {{ str: string; start: number }} StringWithOffset
4✔
29
 */
4✔
30

4✔
31
/**
4✔
32
 * @param {unknown} input
4✔
33
 * @returns {input is Token}
4✔
34
 */
4✔
35
function isTokenType(input) {
1,030✔
36
  return typeof input == 'object';
1,030✔
37
}
1,030✔
38

4✔
39
/**
4✔
40
 * @param {Array<string | Token>} tokens
4✔
41
 * @param {StringWithOffset[]} strings
4✔
42
 * @param {RegExp} regex
4✔
43
 * @param {Set<string>} types
4✔
44
 */
4✔
45
function restoreNested(tokens, strings, regex, types) {
162✔
46
  for (const str of strings) {
162✔
47
    for (const token of tokens) {
26✔
48
      if (isTokenType(token) && types.has(token.type) && token.pos[0] < str.start && str.start < token.pos[1]) {
160✔
49
        const { content } = token;
26✔
50
        token.content = token.content.replace(regex, str.str);
26✔
51

26✔
52
        // actually changed?
26✔
53
        if (token.content !== content) {
26✔
54
          // Re-evaluate groups
26✔
55
          const groupsRegexp = TOKENS_FOR_RESTORE[token.type];
26✔
56
          groupsRegexp.lastIndex = 0;
26✔
57
          const match = groupsRegexp.exec(token.content);
26✔
58
          const groups = match && match.groups;
26✔
59
          Object.assign(token, groups);
26✔
60
        }
26✔
61
      }
26✔
62
    }
160✔
63
  }
26✔
64
}
162✔
65

4✔
66
/**
4✔
67
 *
4✔
68
 * @param {string} text
4✔
69
 * @param {Grammar} grammar
4✔
70
 * @returns {Array<Token | string>}
4✔
71
 */
4✔
72
export function tokenizeBy(text, grammar) {
4✔
73
  if (!text) {
81!
UNCOV
74
    return [];
×
UNCOV
75
  }
×
76

81✔
77
  /** @type {Array<Token | string>} */
81✔
78
  const strarr = [text];
81✔
79

81✔
80
  for (const token in grammar) {
81✔
81
    const pattern = grammar[token];
729✔
82

729✔
83
    // Don’t cache length as it changes during the loop
729✔
84
    for (let i = 0; i < strarr.length; i++) {
729✔
85
      const str = strarr[i];
2,964✔
86

2,964✔
87
      if (typeof str === 'string') {
2,964✔
88
        pattern.lastIndex = 0;
1,479✔
89

1,479✔
90
        const match = pattern.exec(str);
1,479✔
91

1,479✔
92
        if (match) {
1,479✔
93
          const from = match.index - 1;
435✔
94
          /** @type {Array<Token | string>} */
435✔
95
          const args = [];
435✔
96
          const content = match[0];
435✔
97

435✔
98
          const before = str.slice(0, from + 1);
435✔
99
          if (before) {
435✔
100
            args.push(before);
172✔
101
          }
172✔
102

435✔
103
          // @ts-ignore
435✔
104
          args.push({ type: token, content, ...match.groups });
435✔
105

435✔
106
          const after = str.slice(from + content.length + 1);
435✔
107
          if (after) {
435✔
108
            args.push(after);
182✔
109
          }
182✔
110

435✔
111
          strarr.splice(i, 1, ...args);
435✔
112
        }
435✔
113
      }
1,479✔
114
    }
2,964✔
115
  }
729✔
116

81✔
117
  let offset = 0;
81✔
118
  for (let i = 0; i < strarr.length; i++) {
81✔
119
    const token = strarr[i];
435✔
120
    const length = isTokenType(token) ? token.content.length : token.length;
435!
121

435✔
122
    if (isTokenType(token)) {
435✔
123
      token.pos = [offset, offset + length];
435✔
124

435✔
125
      if (TRIM_TOKENS.has(token.type)) {
435✔
126
        token.content = token.content.trim() || ' ';
142✔
127
      }
142✔
128
    }
435✔
129

435✔
130
    offset += length;
435✔
131
  }
435✔
132

81✔
133
  return strarr;
81✔
134
}
81✔
135

4✔
136
/**
4✔
137
 * @param {string} input
4✔
138
 * @returns {Array<Token | string> | null}
4✔
139
 */
4✔
140
export function tokenize(input) {
4✔
141
  if (!input) {
82✔
142
    return null;
1✔
143
  }
1✔
144

21✔
145
  let selector = input.trim(); // prevent leading/trailing whitespace be interpreted as combinators
21✔
146

21✔
147
  // Replace strings with whitespace strings (to preserve offsets)
21✔
148
  // https://github.com/LeaVerou/parsel/pull/16
21✔
149
  /** @type {StringWithOffset[]}*/
21✔
150
  const strings = [];
21✔
151
  selector = selector.replace(
21✔
152
    /(?:"((?:[^"\\]|\\.)*)")|(?:'((?:[^'\\]|\\.)*)')/g,
21✔
153
    (str, contentDouble, contentSingle, start) => {
21✔
154
      strings.push({ str, start });
15✔
155
      const content = contentDouble === void 0 ? contentSingle : contentDouble;
15✔
156
      // eslint-disable-next-line prettier/prettier
15✔
157
      const quote = (contentDouble === void 0) ? '\'' : '"';
15✔
158
      return quote + '§'.repeat(content.length) + quote;
15✔
159
    },
21✔
160
  );
21✔
161

21✔
162
  // Now that strings are out of the way, extract parens and replace them with parens with whitespace (to preserve offsets)
21✔
163
  /** @type {StringWithOffset[]}*/
21✔
164
  const parens = [];
21✔
165
  let offset = 0;
21✔
166
  let start;
21✔
167

21✔
168
  while ((start = selector.indexOf('(', offset)) > -1) {
82✔
169
    const str = gobbleParens(selector, start);
5✔
170
    parens.push({ str, start });
5✔
171
    selector =
5✔
172
      selector.substring(0, start) + '(' + '¶'.repeat(str.length - 2) + ')' + selector.substring(start + str.length);
5✔
173
    offset = start + str.length;
5✔
174
  }
5✔
175

21✔
176
  // Now we have no nested structures and we can parse with regexes
21✔
177
  const tokens = tokenizeBy(selector, TOKENS);
21✔
178

21✔
179
  // Now restore parens and strings in reverse order
21✔
180
  restoreNested(tokens, parens, /\(¶+\)/, TOKENS_WITH_PARENS);
21✔
181
  restoreNested(tokens, strings, /(['"])§+?\1/, TOKENS_WITH_STRINGS);
21✔
182

21✔
183
  return tokens;
21✔
184
}
82✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc