From 121a1da29d54ad37a4d73c5d639b1b23358b995a Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:43:29 +0530 Subject: [PATCH 1/4] Add source offsets to selector nodes --- postcss-selector-parser.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/postcss-selector-parser.d.ts b/postcss-selector-parser.d.ts index 9f0a233..f364830 100644 --- a/postcss-selector-parser.d.ts +++ b/postcss-selector-parser.d.ts @@ -144,11 +144,15 @@ declare namespace parser { interface NodeSource { start?: { line: number, - column: number + column: number, + /** Zero-based UTF-16 offset into the original selector. */ + offset?: number }, end?: { line: number, - column: number + column: number, + /** Zero-based, inclusive UTF-16 offset into the original selector. */ + offset?: number } } interface SpaceAround { From 968cdb65bfff8a78a3c0a0cc86120676643aabcd Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:43:34 +0530 Subject: [PATCH 2/4] Add type assertions for node offsets in tests --- postcss-selector-parser.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/postcss-selector-parser.test.ts b/postcss-selector-parser.test.ts index 5ba3fa7..8294b02 100644 --- a/postcss-selector-parser.test.ts +++ b/postcss-selector-parser.test.ts @@ -8,6 +8,8 @@ parser((root) => { root.walk((node, index) => { node satisfies parser.Node; index satisfies number; + node.source?.start?.offset satisfies number | undefined; + node.source?.end?.offset satisfies number | undefined; if (node.type === 'selector') { node satisfies parser.Selector; From 1609bfc297b732c20488cce452a08ba6c392b7f4 Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:43:39 +0530 Subject: [PATCH 3/4] Add source offsets to selector nodes --- src/__tests__/sourceIndex.mjs | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/__tests__/sourceIndex.mjs b/src/__tests__/sourceIndex.mjs index 916458d..b91ebd3 100644 --- a/src/__tests__/sourceIndex.mjs +++ b/src/__tests__/sourceIndex.mjs @@ -333,3 +333,70 @@ test("pseudo with arguments spanning multiple lines", "h1:not(\n\t.one,\n\t.two\ t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].source.end.column, 5, ".two end column"); t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].sourceIndex, 16, ".two sourceIndex"); }); + +test( + "source offsets for multiline and nested selectors", + "h1:not(\n\t.one,\n\t.two\n)", + (t, tree) => { + const [selector] = tree.nodes; + const [, pseudo] = selector.nodes; + t.deepEqual([tree.source.start.offset, tree.source.end.offset], [0, 21]); + t.deepEqual([selector.source.start.offset, selector.source.end.offset], [0, 21]); + t.deepEqual([pseudo.source.start.offset, pseudo.source.end.offset], [2, 21]); + t.deepEqual([pseudo.nodes[0].source.start.offset, pseudo.nodes[0].source.end.offset], [7, 12]); + t.deepEqual( + [pseudo.nodes[0].nodes[0].source.start.offset, pseudo.nodes[0].nodes[0].source.end.offset], + [9, 12], + ); + t.deepEqual([pseudo.nodes[1].source.start.offset, pseudo.nodes[1].source.end.offset], [14, 21]); + t.deepEqual( + [pseudo.nodes[1].nodes[0].source.start.offset, pseudo.nodes[1].nodes[0].source.end.offset], + [16, 19], + ); + }, +); + +test("source offsets for CRLF and split words", "#one,\r\n#two,\r\ndiv#id.class", (t, tree) => { + t.deepEqual( + tree.nodes.map((node) => node.source.end.offset), + [3, 10, 25], + ); + t.deepEqual( + tree.nodes.map((node) => node.nodes[0].source.start.offset), + [0, 7, 14], + ); + t.deepEqual( + tree.nodes[2].nodes.map((node) => [node.source.start.offset, node.source.end.offset]), + [ + [14, 16], + [17, 19], + [20, 25], + ], + ); +}); + +test( + "source offsets for attributes, named combinators, and empty pseudos", + "[foo=bar] /deep/ :not()", + (t, tree) => { + const [attr, comb, pseudo] = tree.nodes[0].nodes; + t.deepEqual([attr.source.start.offset, attr.source.end.offset], [0, 8]); + t.deepEqual([comb.source.start.offset, comb.source.end.offset], [10, 15]); + t.deepEqual([pseudo.source.start.offset, pseudo.source.end.offset], [17, 22]); + t.deepEqual([pseudo.nodes[0].source.start.offset, pseudo.nodes[0].source.end.offset], [22, 22]); + }, +); + +test("source offsets span words joined from multiple tokens", "a=b.class", (t, tree) => { + t.deepEqual( + tree.nodes[0].nodes.map((node) => [node.source.start.offset, node.source.end.offset]), + [ + [0, 2], + [3, 8], + ], + ); + t.deepEqual( + tree.nodes[0].nodes.map((node) => node.sourceIndex), + [0, 3], + ); +}); From f9a1de558bf98453c95e3468662038975318ea54 Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 27 Sep 2026 03:43:49 +0530 Subject: [PATCH 4/4] Add source offsets to selector nodes --- src/parser.js | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/parser.js b/src/parser.js index edf5330..e5e4722 100644 --- a/src/parser.js +++ b/src/parser.js @@ -35,6 +35,7 @@ function tokenStart(token) { return { line: token[TOKEN.START_LINE], column: token[TOKEN.START_COL], + offset: token[TOKEN.START_POS], }; } @@ -42,18 +43,21 @@ function tokenEnd(token) { return { line: token[TOKEN.END_LINE], column: token[TOKEN.END_COL], + offset: token[TOKEN.END_POS] - 1, }; } -function getSource(startLine, startColumn, endLine, endColumn) { +function getSource(startLine, startColumn, endLine, endColumn, startOffset, endOffset) { return { start: { line: startLine, column: startColumn, + offset: startOffset, }, end: { line: endLine, column: endColumn, + offset: endOffset, }, }; } @@ -64,6 +68,8 @@ function getTokenSource(token) { token[TOKEN.START_COL], token[TOKEN.END_LINE], token[TOKEN.END_COL], + token[TOKEN.START_POS], + token[TOKEN.END_POS] - 1, ); } @@ -76,6 +82,8 @@ function getTokenSourceSpan(startToken, endToken) { startToken[TOKEN.START_COL], endToken[TOKEN.END_LINE], endToken[TOKEN.END_COL], + startToken[TOKEN.START_POS], + endToken[TOKEN.END_POS] - 1, ); } @@ -132,7 +140,7 @@ export default class Parser { this.root.errorGenerator = this._errorGenerator(); const selector = new Selector({ - source: { start: { line: 1, column: 1 } }, + source: { start: { line: 1, column: 1, offset: 0 } }, sourceIndex: 0, }); this.root.append(selector); @@ -172,7 +180,7 @@ export default class Parser { const len = attr.length; const node = { - source: getSource(startingToken[1], startingToken[2], this.currToken[3], this.currToken[4]), + source: getTokenSourceSpan(startingToken, this.currToken), sourceIndex: startingToken[TOKEN.START_POS], }; @@ -471,12 +479,7 @@ export default class Parser { nodes.push( new Str({ value: "", - source: getSource( - firstToken[TOKEN.START_LINE], - firstToken[TOKEN.START_COL], - lastToken[TOKEN.END_LINE], - lastToken[TOKEN.END_COL], - ), + source: getTokenSourceSpan(firstToken, lastToken), sourceIndex: firstToken[TOKEN.START_POS], spaces: { before: space, after: "" }, }), @@ -530,12 +533,7 @@ export default class Parser { } let node = new Combinator({ value: `/${name}/`, - source: getSource( - this.currToken[TOKEN.START_LINE], - this.currToken[TOKEN.START_COL], - this.tokens[this.position + 2][TOKEN.END_LINE], - this.tokens[this.position + 2][TOKEN.END_COL], - ), + source: getTokenSourceSpan(this.currToken, this.tokens[this.position + 2]), sourceIndex: this.currToken[TOKEN.START_POS], raws, }); @@ -814,12 +812,7 @@ export default class Parser { this.newNode( new Str({ value: parenValue, - source: getSource( - parenStart[TOKEN.START_LINE], - parenStart[TOKEN.START_COL], - parenEnd[TOKEN.END_LINE], - parenEnd[TOKEN.END_COL], - ), + source: getTokenSourceSpan(parenStart, parenEnd), sourceIndex: parenStart[TOKEN.START_POS], }), ); @@ -920,6 +913,7 @@ export default class Parser { } splitWord(namespace, firstCallback) { + const firstToken = this.currToken; let nextToken = this.nextToken; let word = this.content(); while ( @@ -966,8 +960,15 @@ export default class Parser { } let node; const current = this.currToken; - const sourceIndex = current[TOKEN.START_POS] + indices[i]; - const source = getSource(current[1], current[2] + ind, current[3], current[2] + (index - 1)); + const sourceIndex = firstToken[TOKEN.START_POS] + indices[i]; + const source = getSource( + current[TOKEN.START_LINE], + current[TOKEN.START_COL] + ind, + current[TOKEN.END_LINE], + current[TOKEN.START_COL] + index - 1, + sourceIndex, + firstToken[TOKEN.START_POS] + index - 1, + ); if (classIndexes.has(ind)) { let classNameOpts = { value: value.slice(1),