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

mojolicious / dom.js / 25563801595

08 May 2026 03:21PM UTC coverage: 96.501% (-0.7%) from 97.231%
25563801595

push

github

kraih
Backported bug fixes from Perl version

524 of 560 branches covered (93.57%)

Branch coverage included in aggregate %.

193 of 209 new or added lines in 2 files covered. (92.34%)

1 existing line in 1 file now uncovered.

2041 of 2098 relevant lines covered (97.28%)

512.62 hits per line

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

94.93
/src/css.ts
1
import type {ElementNode} from './nodes/element.js';
1✔
2
import type {Parent} from './types.js';
1✔
3
import {cssUnescape, escapeRegExp, stickyMatch} from '@mojojs/util';
1✔
4

1✔
5
interface Attribute {
1✔
6
  name: RegExp;
1✔
7
  type: 'attr';
1✔
8
  value: RegExp | null;
1✔
9
}
1✔
10

1✔
11
interface Tag {
1✔
12
  name: RegExp | null;
1✔
13
  type: 'tag';
1✔
14
}
1✔
15

1✔
16
interface PseudoClassIsNot {
1✔
17
  class: 'is' | 'not';
1✔
18
  type: 'pc';
1✔
19
  value: SelectorList;
1✔
20
}
1✔
21

1✔
22
interface PseudoClassNth {
1✔
23
  class: 'nth-child' | 'nth-last-child' | 'nth-of-type' | 'nth-last-of-type';
1✔
24
  type: 'pc';
1✔
25
  value: [number, number];
1✔
26
}
1✔
27

1✔
28
interface PseudoClassPlain {
1✔
29
  class: 'any-link' | 'checked' | 'empty' | 'link' | 'only-child' | 'only-of-type' | 'root' | 'visited';
1✔
30
  type: 'pc';
1✔
31
}
1✔
32

1✔
33
interface PseudoClassText {
1✔
34
  class: 'text';
1✔
35
  type: 'pc';
1✔
36
  value: RegExp;
1✔
37
}
1✔
38

1✔
39
type PseudoClass = PseudoClassIsNot | PseudoClassNth | PseudoClassPlain | PseudoClassText;
1✔
40

1✔
41
type SimpleSelector = Attribute | Tag | PseudoClass;
1✔
42

1✔
43
interface Combinator {
1✔
44
  type: 'combinator';
1✔
45
  value: string;
1✔
46
}
1✔
47

1✔
48
interface CompoundSelector {
1✔
49
  type: 'compound';
1✔
50
  value: SimpleSelector[];
1✔
51
}
1✔
52

1✔
53
type ComplexSelector = Array<Combinator | CompoundSelector>;
1✔
54
type SelectorList = ComplexSelector[];
1✔
55

1✔
56
const ESCAPE_RE = '\\\\[^0-9a-fA-F]|\\\\[0-9a-fA-F]{1,6}';
1✔
57
const SEPARATOR_RE = new RegExp(`\\s*,\\s*`, 'y');
1✔
58
const COMBINATOR_RE = new RegExp(`\\s*([>+~])\\s*`, 'y');
1✔
59
const DESCENDANT_RE = new RegExp(`\\s+`, 'y');
1✔
60
const PC_RE = new RegExp(`:([\\w\\-]+)(?:\\(((?:\\([^)]+\\)|[^)])+)\\))?`, 'y');
1✔
61
const TAG_RE = new RegExp(`((?:${ESCAPE_RE}\\s|\\\\[\\s\\S]|[^,.#:[\\s>~+])+)`, 'y');
1✔
62
const CLASS_ID_RE = new RegExp(`([.#])((?:${ESCAPE_RE}\\s?|[^,.#:[\\s>~+])+)`, 'y');
1✔
63
const ATTR_RE = new RegExp(
1✔
64
  `\\[` +
1✔
65
    `((?:${ESCAPE_RE}|[\\w\\-])+)` +
1✔
66
    `(?:` +
1✔
67
    `(\\W)?=` +
1✔
68
    `(?:"((?:\\\\"|[^"])*)"|'((?:\\\\'|[^'])*)'|([^\\]]+?))` +
1✔
69
    `(?:\\s+(?:(i|I)|s|S))?` +
1✔
70
    `)?` +
1✔
71
    `\\]`,
1✔
72
  'y'
1✔
73
);
1✔
74

1✔
75
export class Selector {
1✔
76
  _ast: SelectorList;
1✔
77

1✔
78
  constructor(selector: string) {
1✔
79
    this._ast = compileSelector(selector);
630✔
80
  }
630✔
81

1✔
82
  all(tree: Parent): ElementNode[] {
1✔
83
    return selectElements(false, tree, this._ast);
255✔
84
  }
255✔
85

1✔
86
  first(tree: Parent): ElementNode | null {
1✔
87
    return selectElements(true, tree, this._ast)[0] ?? null;
336✔
88
  }
336✔
89

1✔
90
  matches(tree: ElementNode): boolean {
1✔
91
    return matchList(this._ast, tree, tree, tree.root() as Parent);
39✔
92
  }
39✔
93
}
1✔
94

1✔
95
function allTags(tree: Parent): ElementNode[] {
1,538✔
96
  const tags: ElementNode[] = [];
1,538✔
97
  const queue = [...tree.childNodes];
1,538✔
98
  let current;
1,538✔
99
  while ((current = queue.shift()) !== undefined) {
1,538✔
100
    if (current.nodeType !== '#element') continue;
133,364✔
101
    tags.push(current);
44,881✔
102
    queue.unshift(...current.childNodes);
44,881✔
103
  }
44,881✔
104
  return tags;
1,538✔
105
}
1,538✔
106

1✔
107
function compileAttrValue(op: string, value: string | undefined, insensitive: boolean): RegExp | null {
229✔
108
  if (value === undefined) return null;
229✔
109
  const flags = insensitive === true ? 'i' : undefined;
229!
110
  value = escapeRegExp(cssUnescape(value));
229✔
111

229✔
112
  // "~=" (word)
229✔
113
  if (op === '~') return new RegExp(`(?:^|\\s+)${value}(?:\\s+|$)`, flags);
229✔
114

144✔
115
  // "|=" (hyphen-separated)
144✔
116
  if (op === '|') return new RegExp(`^${value}(?:-|$)`, flags);
229!
117

144✔
118
  // "*=" (contains)
144✔
119
  if (op === '*') return new RegExp(value, flags);
229✔
120

140✔
121
  // "^=" (begins with)
140✔
122
  if (op === '^') return new RegExp(`^${value}`, flags);
229✔
123

117✔
124
  // "$=" (ends with)
117✔
125
  if (op === '$') return new RegExp(`${value}$`, flags);
229✔
126

95✔
127
  // Everything else
95✔
128
  return new RegExp(`^${value}$`, flags);
95✔
129
}
95✔
130

1✔
131
function compileEquation(values: string | undefined): [number, number] {
95✔
132
  if (values === undefined) return [0, 0];
95✔
133

94✔
134
  // "even"
94✔
135
  if (/^\s*even\s*$/i.test(values)) return [2, 0];
95✔
136

89✔
137
  // "odd"
89✔
138
  if (/^\s*odd\s*$/i.test(values)) return [2, 1];
95✔
139

77✔
140
  // "4", "+4" or "-4"
77✔
141
  const numMatch = values.match(/^\s*((?:\+|-)?\d+)\s*$/);
77✔
142
  if (numMatch !== null) return [0, parseInt(numMatch[1])];
95✔
143

48✔
144
  // "n", "4n", "+4n", "-4n", "n+1", "4n-1", "+4n-1" (and other variations)
48✔
145
  const complexMatch = values.match(/^\s*((?:\+|-)?(?:\d+)?)?n\s*((?:\+|-)\s*\d+)?\s*$/i);
48✔
146
  if (complexMatch === null) return [0, 0];
95✔
147

47✔
148
  const first = complexMatch[1] ?? '1';
95✔
149
  const second = complexMatch[2]?.replaceAll(' ', '') ?? '0';
95✔
150
  return [first === '-' ? -1 : parseInt(first), parseInt(second)];
95✔
151
}
95✔
152

1✔
153
function compileName(value: string): RegExp {
1,094✔
154
  return new RegExp('(?:^|\\:)' + escapeRegExp(cssUnescape(value)) + '$');
1,094✔
155
}
1,094✔
156

1✔
157
function compilePseudoClass(name: string, args: string): PseudoClass {
219✔
158
  // ":text"
219✔
159
  if (name === 'text') {
219✔
160
    const textMatch = args.match(/^\/(.+)\/(i)?$/);
22✔
161
    const regex =
22✔
162
      textMatch === null ? new RegExp(escapeRegExp(args), 'i') : new RegExp(textMatch[1], textMatch[2] ?? '');
22✔
163
    return {type: 'pc', class: 'text', value: regex};
22✔
164
  }
22✔
165

197✔
166
  // ":is" and ":not" (contains more selectors)
197✔
167
  if (name === 'not' || name === 'is') {
219✔
168
    return {type: 'pc', class: name, value: compileSelector(args)};
38✔
169
  }
38✔
170

159✔
171
  // ":nth-*" (with An+B notation)
159✔
172
  else if (name === 'nth-child' || name === 'nth-last-child' || name === 'nth-of-type' || name === 'nth-last-of-type') {
159✔
173
    return {type: 'pc', class: name, value: compileEquation(args)};
95✔
174
  }
95✔
175

64✔
176
  // ":first-child"
64✔
177
  else if (name === 'first-child') {
64✔
178
    return {type: 'pc', class: 'nth-child', value: [0, 1]};
5✔
179
  }
5✔
180

59✔
181
  // ":first-of-type"
59✔
182
  else if (name === 'first-of-type') {
59✔
183
    return {type: 'pc', class: 'nth-of-type', value: [0, 1]};
3✔
184
  }
3✔
185

56✔
186
  // ":last-child"
56✔
187
  else if (name === 'last-child') {
56✔
188
    return {type: 'pc', class: 'nth-last-child', value: [0, 1]};
4✔
189
  }
4✔
190

52✔
191
  // ":last-of-type"
52✔
192
  else if (name === 'last-of-type') {
52✔
193
    return {type: 'pc', class: 'nth-last-of-type', value: [0, 1]};
2✔
194
  }
2✔
195

50✔
196
  // ":checked", ":empty", ":only-child", ":only-of-type", ":root"
50✔
197
  else if (
50✔
198
    name === 'any-link' ||
50✔
199
    name === 'checked' ||
50✔
200
    name === 'empty' ||
50✔
201
    name === 'link' ||
50✔
202
    name === 'only-child' ||
50✔
203
    name === 'only-of-type' ||
50✔
204
    name === 'root' ||
50✔
205
    name === 'visited'
6✔
206
  ) {
50✔
207
    return {type: 'pc', class: name};
48✔
208
  }
48✔
209

2✔
210
  // Unknown
2✔
211
  return {type: 'pc', class: 'nth-child', value: [0, 0]};
2✔
212
}
2✔
213

1✔
214
function compileSelector(selector: string): SelectorList {
668✔
215
  const group: SelectorList = [[]];
668✔
216

668✔
217
  const trimmed = selector.trim();
668✔
218
  const sticky = {offset: 0, value: trimmed};
668✔
219
  while (trimmed.length > sticky.offset) {
668✔
220
    const complex = group[group.length - 1];
1,776✔
221
    if (complex.length === 0 || complex[complex.length - 1].type !== 'compound') {
1,776✔
222
      complex.push({type: 'compound', value: []});
1,096✔
223
    }
1,096✔
224
    const last = (complex[complex.length - 1] as CompoundSelector).value;
1,776✔
225

1,776✔
226
    // Separator
1,776✔
227
    const separatorMatch = stickyMatch(sticky, SEPARATOR_RE);
1,776✔
228
    if (separatorMatch !== null) {
1,776✔
229
      group.push([]);
16✔
230
      continue;
16✔
231
    }
16✔
232

1,760✔
233
    // Combinator
1,760✔
234
    const combinatorMatch = stickyMatch(sticky, COMBINATOR_RE);
1,760✔
235
    if (combinatorMatch !== null) {
1,776✔
236
      complex.push({type: 'combinator', value: combinatorMatch[1]});
267✔
237
      continue;
267✔
238
    }
267✔
239

1,493✔
240
    // Descendant combinator
1,493✔
241
    const descendantMatch = stickyMatch(sticky, DESCENDANT_RE);
1,493✔
242
    if (descendantMatch !== null) {
1,776✔
243
      complex.push({type: 'combinator', value: ' '});
145✔
244
      continue;
145✔
245
    }
145✔
246

1,348✔
247
    // Class or ID
1,348✔
248
    const classMatch = stickyMatch(sticky, CLASS_ID_RE);
1,348✔
249
    if (classMatch !== null) {
1,776✔
250
      if (classMatch[1] === '#') {
103✔
251
        last.push({type: 'attr', name: compileName('id'), value: compileAttrValue('', classMatch[2], false)});
67✔
252
      } else {
103✔
253
        last.push({type: 'attr', name: compileName('class'), value: compileAttrValue('~', classMatch[2], false)});
36✔
254
      }
36✔
255
      continue;
103✔
256
    }
103✔
257

1,245✔
258
    // Attribute
1,245✔
259
    const attrMatch = stickyMatch(sticky, ATTR_RE);
1,245✔
260
    if (attrMatch !== null) {
1,776✔
261
      const insensitive = attrMatch[6] === undefined ? false : true;
126!
262
      last.push({
126✔
263
        type: 'attr',
126✔
264
        name: compileName(attrMatch[1]),
126✔
265
        value: compileAttrValue(attrMatch[2] ?? '', attrMatch[3] ?? attrMatch[4] ?? attrMatch[5], insensitive)
126✔
266
      });
126✔
267
      continue;
126✔
268
    }
126✔
269

1,119✔
270
    // Pseudo-class
1,119✔
271
    const pcMatch = stickyMatch(sticky, PC_RE);
1,119✔
272
    if (pcMatch !== null) {
1,776✔
273
      last.push(compilePseudoClass(pcMatch[1], pcMatch[2]));
219✔
274
      continue;
219✔
275
    }
219✔
276

900✔
277
    // Tag
900✔
278
    const tagMatch = stickyMatch(sticky, TAG_RE);
900✔
279
    if (tagMatch !== null) {
900✔
280
      const tag = tagMatch[0];
900✔
281
      last.push({type: 'tag', name: tag === '*' ? null : compileName(tag)});
900✔
282
      continue;
900✔
283
    }
900✔
284

×
NEW
285
    throw new Error(`Unknown CSS selector: ${trimmed}`);
×
286
  }
×
287

668✔
288
  return group;
668✔
289
}
668✔
290

1✔
291
function evaluate(group: SelectorList, tree: Parent, scope: Parent, pool: ElementNode[]): ElementNode[] {
591✔
292
  const results: ElementNode[] = [];
591✔
293
  const seen = new Set<ElementNode>();
591✔
294
  for (const selector of group) {
591✔
295
    for (const node of evaluateOne(selector, tree, scope, pool)) {
592✔
296
      if (seen.has(node) === true) continue;
1,092!
297
      seen.add(node);
1,092✔
298
      results.push(node);
1,092✔
299
    }
1,092✔
300
  }
592✔
301
  return results;
591✔
302
}
591✔
303

1✔
304
function evaluateOne(selector: ComplexSelector, tree: Parent, scope: Parent, pool: ElementNode[]): ElementNode[] {
592✔
305
  const parts = [...selector];
592✔
306
  const compound = parts.shift();
592✔
307
  if (compound === undefined || compound.type !== 'compound') return [];
592!
308
  let candidates = pool.filter(node => matchSelector(compound, node, tree, scope) === true);
592✔
309

592✔
310
  while (parts.length > 0) {
592✔
311
    const combinator = parts.shift() as Combinator;
409✔
312
    const next = parts.shift();
409✔
313
    if (next === undefined || next.type !== 'compound') return [];
409!
314

409✔
315
    const seen = new Set<ElementNode>();
409✔
316
    const newCandidates: ElementNode[] = [];
409✔
317
    for (const node of candidates) {
409✔
318
      for (const candidate of stepForward(combinator.value, node)) {
1,316✔
319
        if (seen.has(candidate) === true) continue;
40,157✔
320
        seen.add(candidate);
2,513✔
321
        if (matchSelector(next, candidate, tree, scope) === true) newCandidates.push(candidate);
40,157✔
322
      }
40,157✔
323
    }
1,316✔
324
    candidates = newCandidates;
409✔
325
  }
409✔
326

592✔
327
  return candidates;
592✔
328
}
592✔
329

1✔
330
function matchAttribute(selector: Attribute, current: ElementNode): boolean {
1,251✔
331
  const nameRegex = selector.name;
1,251✔
332
  const valueRegex = selector.value;
1,251✔
333

1,251✔
334
  for (const [name, value] of Object.entries(current.attributes)) {
1,251✔
335
    if (nameRegex.test(name) === false) continue;
767✔
336
    if (valueRegex === null || valueRegex.test(value) === true) return true;
767✔
337
  }
767✔
338

1,037✔
339
  return false;
1,037✔
340
}
1,037✔
341

1✔
342
function matchList(group: SelectorList, current: ElementNode, tree: Parent, scope: Parent): boolean {
121✔
343
  for (const selector of group) {
121✔
344
    if (matchOne(selector, current, tree, scope) === true) return true;
141✔
345
  }
141✔
346
  return false;
78✔
347
}
78✔
348

1✔
349
function matchOne(selector: ComplexSelector, current: ElementNode, tree: Parent, scope: Parent): boolean {
141✔
350
  const parts = [...selector].reverse();
141✔
351
  const compound = parts.shift();
141✔
352
  if (compound === undefined || compound.type !== 'compound') return false;
141!
353
  if (matchSelector(compound, current, tree, scope) === false) return false;
141✔
354

43✔
355
  let candidates: ElementNode[] = [current];
43✔
356
  while (parts.length > 0) {
141✔
357
    const combinator = parts.shift() as Combinator;
1✔
358
    const next = parts.shift();
1✔
359
    if (next === undefined || next.type !== 'compound') return false;
1!
360

1✔
361
    const seen = new Set<ElementNode>();
1✔
362
    const newCandidates: ElementNode[] = [];
1✔
363
    for (const node of candidates) {
1✔
364
      for (const candidate of stepBack(combinator.value, node, scope)) {
1✔
365
        if (seen.has(candidate) === true) continue;
1!
366
        seen.add(candidate);
1✔
367
        if (matchSelector(next, candidate, tree, scope) === true) newCandidates.push(candidate);
1✔
368
      }
1✔
369
    }
1✔
370
    if (newCandidates.length === 0) return false;
1!
371
    candidates = newCandidates;
1✔
372
  }
1✔
373

43✔
374
  return true;
43✔
375
}
43✔
376

1✔
377
function matchPseudoClass(simple: PseudoClass, current: ElementNode, tree: Parent, scope: Parent): boolean {
1,257✔
378
  const name = simple.class;
1,257✔
379

1,257✔
380
  // ":not"
1,257✔
381
  if (name === 'not') {
1,257✔
382
    if (matchList(simple.value, current, tree, scope) === false) return true;
70✔
383
  }
70✔
384

1,187✔
385
  // ":is"
1,187✔
386
  else if (name === 'is') {
1,187✔
387
    if (matchList(simple.value, current, tree, scope) === true) return true;
12✔
388
  }
12✔
389

1,175✔
390
  // ":root"
1,175✔
391
  else if (name === 'root') {
1,175✔
392
    if (current.parentNode === current.root()) return true;
46✔
393
  }
46✔
394

1,129✔
395
  // ":empty"
1,129✔
396
  else if (name === 'empty') {
1,129✔
397
    if (current.childNodes.filter(node => node.nodeType !== '#comment' && node.nodeType !== '#pi').length === 0) {
36✔
398
      return true;
15✔
399
    }
15✔
400
  }
36✔
401

1,093✔
402
  // ":checked"
1,093✔
403
  else if (name === 'checked') {
1,093✔
404
    const attrs = current.attributes;
205✔
405
    if (attrs.checked !== undefined || attrs.selected !== undefined) return true;
205✔
406
  }
205✔
407

888✔
408
  // ":text"
888✔
409
  else if (name === 'text') {
888✔
410
    const regex = simple.value;
103✔
411
    for (const node of current.childNodes) {
103✔
412
      if (node.nodeType === '#text' && regex.test(node.value.toString()) === true) return true;
269✔
413
    }
269✔
414
  }
85✔
415

785✔
416
  // ":any-link", ":link", ":visited"
785✔
417
  else if (name === 'any-link' || name === 'link' || name === 'visited') {
785✔
418
    const tag = current.tagName;
39✔
419
    if ((tag === 'a' || tag === 'area' || tag === 'link') && current.attributes.href !== undefined) return true;
39✔
420
  }
39✔
421

746✔
422
  // ":only-*"
746✔
423
  else if (name === 'only-child' || name === 'only-of-type') {
746✔
424
    let nodes = current.parentNode?.childNodes.filter(node => node.nodeType === '#element') ?? [];
26!
425
    if (name === 'only-of-type') nodes = nodes.filter(el => (el as ElementNode).tagName === current.tagName);
26✔
426
    if (nodes.length === 1) return true;
26✔
427
  }
26✔
428

720✔
429
  // ":nth-*"
720✔
430
  else if (name === 'nth-child' || name === 'nth-last-child' || name === 'nth-of-type' || name === 'nth-last-of-type') {
720✔
431
    const equation = simple.value;
720✔
432
    let nodes = current.parentNode?.childNodes.filter(node => node.nodeType === '#element') ?? [];
720!
433

720✔
434
    // ":*-of-type"
720✔
435
    if (name === 'nth-of-type' || name === 'nth-last-of-type') {
720✔
436
      nodes = nodes.filter(el => (el as ElementNode).tagName === current.tagName);
57✔
437
    }
57✔
438

720✔
439
    let index = 0;
720✔
440
    for (let i = 0; i < nodes.length; i++) {
720✔
441
      if (nodes[i] !== current) continue;
3,110✔
442
      index = i;
720✔
443
      break;
720✔
444
    }
720✔
445
    if (name === 'nth-last-child' || name === 'nth-last-of-type') index = nodes.length - index - 1;
720✔
446
    index++;
720✔
447

720✔
448
    const delta = index - equation[1];
720✔
449
    if (delta === 0) return true;
720✔
450
    return equation[0] !== 0 && delta < 0 === equation[0] < 0 && delta % equation[0] === 0;
720✔
451
  }
720✔
452

366✔
453
  // Everything else
366✔
454
  return false;
366✔
455
}
366✔
456

1✔
457
function matchSelector(compound: CompoundSelector, current: ElementNode, tree: Parent, scope: Parent): boolean {
8,224✔
458
  for (const selector of compound.value) {
8,224✔
459
    const type = selector.type;
9,107✔
460

9,107✔
461
    // Tag
9,107✔
462
    if (type === 'tag') {
9,107✔
463
      const name = selector.name;
6,599✔
464
      if (name !== null && name.test(current.tagName) === false) return false;
6,599✔
465
    }
6,599✔
466

2,508✔
467
    // Attribute
2,508✔
468
    else if (type === 'attr') {
2,508✔
469
      if (matchAttribute(selector, current) === false) return false;
1,251✔
470
    }
1,251✔
471

1,257✔
472
    // Pseudo-class
1,257✔
473
    else if (type === 'pc') {
1,257✔
474
      if (matchPseudoClass(selector, current, tree, scope) === false) return false;
1,257✔
475
    }
1,257✔
476

×
477
    // No match
×
478
    else {
×
479
      return false;
×
480
    }
×
481
  }
9,107✔
482

2,452✔
483
  return true;
2,452✔
484
}
2,452✔
485

1✔
486
function selectElements(one: boolean, scope: Parent, group: SelectorList): ElementNode[] {
591✔
487
  const tags = allTags(scope);
591✔
488
  const matches = new Set(evaluate(group, scope, scope, tags));
591✔
489

591✔
490
  const results: ElementNode[] = [];
591✔
491
  for (const node of tags) {
591✔
492
    if (matches.has(node) === false) continue;
4,715✔
493
    if (one === true) return [node];
4,715✔
494
    results.push(node);
730✔
495
  }
730✔
496

278✔
497
  return results;
278✔
498
}
278✔
499

1✔
500
function stepBack(combinator: string, node: ElementNode, scope: Parent): ElementNode[] {
1✔
501
  // " " (ancestors) and ">" (parent only)
1✔
502
  if (combinator === ' ' || combinator === '>') {
1✔
503
    const ancestors: ElementNode[] = [];
1✔
504
    let current: Parent | null = node;
1✔
505
    while (current !== scope && current.parentNode !== null) {
1✔
506
      current = current.parentNode;
1✔
507
      if (current.nodeType !== '#element') break;
1!
508
      ancestors.push(current);
1✔
509
      if (combinator === '>' || current === scope) break;
1!
510
    }
1✔
511
    return ancestors;
1✔
512
  }
1✔
513

×
NEW
514
  // "~" (preceding siblings) and "+" (immediately preceding)
×
NEW
515
  const parent = node.parentNode;
×
NEW
516
  if (parent === null) return [];
×
UNCOV
517

×
NEW
518
  const preceding: ElementNode[] = [];
×
NEW
519
  for (const child of parent.childNodes) {
×
NEW
520
    if (child.nodeType !== '#element') continue;
×
NEW
521
    if (child === node) break;
×
NEW
522
    preceding.push(child);
×
NEW
523
  }
×
NEW
524
  if (combinator === '+') return preceding.length === 0 ? [] : [preceding[preceding.length - 1]];
×
NEW
525
  return preceding;
×
NEW
526
}
×
527

1✔
528
function stepForward(combinator: string, node: ElementNode): ElementNode[] {
1,316✔
529
  // " " (descendants)
1,316✔
530
  if (combinator === ' ') return allTags(node);
1,316✔
531

369✔
532
  // ">" (children only)
369✔
533
  if (combinator === '>') {
1,316✔
534
    const children: ElementNode[] = [];
356✔
535
    for (const child of node.childNodes) {
356✔
536
      if (child.nodeType !== '#element') continue;
1,527✔
537
      children.push(child);
834✔
538
    }
834✔
539
    return children;
356✔
540
  }
356✔
541

13✔
542
  // "~" (following siblings) and "+" (immediately following)
13✔
543
  const parent = node.parentNode;
13✔
544
  if (parent === null) return [];
1,316!
545

13✔
546
  const following: ElementNode[] = [];
13✔
547
  let found = false;
13✔
548
  for (const child of parent.childNodes) {
1,316✔
549
    if (child.nodeType !== '#element') continue;
83✔
550
    if (found === true) following.push(child);
83✔
551
    if (child === node) found = true;
83✔
552
  }
83✔
553
  if (combinator === '+') return following.length === 0 ? [] : [following[0]];
1,316✔
554
  return following;
9✔
555
}
9✔
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