diff --git a/lib/index.js b/lib/index.js index 6b247b0..d083211 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,6 +12,53 @@ module.exports = { parser.parse(); + // To handle double-slash comments (`//`) we end up creating a new tokenizer + // in certain cases (see `lib/nodes/inline-comment.js`). However, this means + // that any following node in the AST will have incorrect start/end positions + // on the `source` property. To fix that, we'll walk the AST and compute + // updated positions for all nodes. + parser.root.walk((node) => { + const offset = input.css.lastIndexOf(node.source.input.css); + + if (offset === 0) { + // Short circuit - this node was processed with the original tokenizer + // and should therefore have correct position information. + return; + } + + // This ensures that the chunk of source we're processing corresponds + // strictly to a terminal substring of the input CSS. This should always + // be the case, but if it ever isn't, we prefer to fail instead of + // producing potentially invalid output. + // istanbul ignore next + if (offset + node.source.input.css.length !== input.css.length) { + throw new Error('Invalid state detected in postcss-less'); + } + + const newStartOffset = offset + node.source.start.offset; + const newStartPosition = input.fromOffset(offset + node.source.start.offset); + + // eslint-disable-next-line no-param-reassign + node.source.start = { + offset: newStartOffset, + line: newStartPosition.line, + column: newStartPosition.col + }; + + // Not all nodes have an `end` property. + if (node.source.end) { + const newEndOffset = offset + node.source.end.offset; + const newEndPosition = input.fromOffset(offset + node.source.end.offset); + + // eslint-disable-next-line no-param-reassign + node.source.end = { + offset: newEndOffset, + line: newEndPosition.line, + column: newEndPosition.col + }; + } + }); + return parser.root; }, diff --git a/lib/nodes/inline-comment.js b/lib/nodes/inline-comment.js index 6e5bc04..b933949 100644 --- a/lib/nodes/inline-comment.js +++ b/lib/nodes/inline-comment.js @@ -8,7 +8,7 @@ module.exports = { if (token[0] === 'word' && token[1].slice(0, 2) === '//') { const first = token; const bits = []; - let last; + let endOffset; let remainingInput; while (token) { @@ -20,7 +20,12 @@ module.exports = { // Get remaining input and retokenize remainingInput = token[1].substring(token[1].indexOf('\n')); - remainingInput += this.input.css.valueOf().substring(this.tokenizer.position()); + const untokenizedRemainingInput = this.input.css + .valueOf() + .substring(this.tokenizer.position()); + remainingInput += untokenizedRemainingInput; + + endOffset = token[3] + untokenizedRemainingInput.length - remainingInput.length; } else { // If the tokenizer went to the next line go back this.tokenizer.back(token); @@ -29,11 +34,12 @@ module.exports = { } bits.push(token[1]); - last = token; + // eslint-disable-next-line prefer-destructuring + endOffset = token[2]; token = this.tokenizer.nextToken({ ignoreUnclosed: true }); } - const newToken = ['comment', bits.join(''), first[2], last[2]]; + const newToken = ['comment', bits.join(''), first[2], endOffset]; this.inlineComment(newToken); // Replace tokenizer to retokenize the rest of the string diff --git a/test/parser/comments.test.js b/test/parser/comments.test.js index e3be6dc..6f7fdc1 100644 --- a/test/parser/comments.test.js +++ b/test/parser/comments.test.js @@ -179,3 +179,36 @@ test('inline comments with asterisk are persisted (#135)', (t) => { t.is(first.text, '*batman'); t.is(nodeToString(root), less); }); + +test('handles single quotes in comments (#163)', (t) => { + const less = `a {\n // '\n color: pink;\n}\n\n/** ' */`; + + const root = parse(less); + + const [ruleNode, commentNode] = root.nodes; + + t.is(ruleNode.type, 'rule'); + t.is(commentNode.type, 'comment'); + + t.is(commentNode.source.start.line, 6); + t.is(commentNode.source.start.column, 1); + t.is(commentNode.source.end.line, 6); + t.is(commentNode.source.end.column, 8); + + const [innerCommentNode, declarationNode] = ruleNode.nodes; + + t.is(innerCommentNode.type, 'comment'); + t.is(declarationNode.type, 'decl'); + + t.is(innerCommentNode.source.start.line, 2); + t.is(innerCommentNode.source.start.column, 3); + t.is(innerCommentNode.source.end.line, 2); + t.is(innerCommentNode.source.end.column, 6); + + t.is(declarationNode.source.start.line, 3); + t.is(declarationNode.source.start.column, 3); + t.is(declarationNode.source.end.line, 3); + t.is(declarationNode.source.end.column, 14); + + t.is(nodeToString(root), less); +});