Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions postcss-selector-parser.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions postcss-selector-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
67 changes: 67 additions & 0 deletions src/__tests__/sourceIndex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
);
});
47 changes: 24 additions & 23 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,29 @@ function tokenStart(token) {
return {
line: token[TOKEN.START_LINE],
column: token[TOKEN.START_COL],
offset: token[TOKEN.START_POS],
};
}

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,
},
};
}
Expand All @@ -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,
);
}

Expand All @@ -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,
);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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],
};

Expand Down Expand Up @@ -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: "" },
}),
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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],
}),
);
Expand Down Expand Up @@ -920,6 +913,7 @@ export default class Parser {
}

splitWord(namespace, firstCallback) {
const firstToken = this.currToken;
let nextToken = this.nextToken;
let word = this.content();
while (
Expand Down Expand Up @@ -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),
Expand Down