Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/block-comment-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rics": patch
---

Skip `/* */` and `//` comments when scanning for the end of an `@if`, `@else`, `@mixin`, `@for`, or `@each` block. Previously `readBlock` tracked braces and quotes through comment text, so a lone apostrophe (`it's`, `user's`) or a stray brace inside a comment desynced the brace count, producing a false `UNCLOSED_BLOCK` error or silently swallowing rules. The same block-comment blind spot in expression-value scanning (`readUntil`) is fixed too. Comment text inside blocks is preserved unchanged.
35 changes: 35 additions & 0 deletions packages/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ class Compiler {
return "";
}

private readRawComment(): string {
const start = this.pos;
if (this.peek() === "/" && this.peek(1) === "*") {
this.advance(2);
while (this.pos < this.input.length && !(this.peek() === "*" && this.peek(1) === "/")) {
this.advance();
}
if (this.pos < this.input.length) this.advance(2);
} else {
this.advance(2);
while (this.pos < this.input.length && this.peek() !== "\n") {
this.advance();
}
}
return this.input.slice(start, this.pos);
}

private parseStylesheet(): void {
while (this.pos < this.input.length) {
this.checkLimits();
Expand Down Expand Up @@ -1862,6 +1879,19 @@ class Compiler {
continue;
}

if (ch === "/" && this.peek(1) === "*") {
content += this.readRawComment();
continue;
}

if (ch === "/" && this.peek(1) === "/") {
const last = content.length > 0 ? content[content.length - 1] : "";
if (last === "" || last === " " || last === "\t" || last === "\n" || last === "\r") {
content += this.readRawComment();
continue;
}
}

if (ch === '"' || ch === "'") {
inString = ch;
content += this.advance();
Expand Down Expand Up @@ -1928,6 +1958,11 @@ class Compiler {
continue;
}

if (ch === "/" && this.peek(1) === "*") {
result += this.readRawComment();
continue;
}

if (ch === "/" && this.peek(1) === "/") {
const last = result.length > 0 ? result[result.length - 1] : "";
if (last === "" || last === " " || last === "\t" || last === "\n" || last === "\r") {
Expand Down
93 changes: 93 additions & 0 deletions packages/core/tests/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,99 @@ describe("compiler", () => {
});
});

describe("block-boundary comments (issue #1)", () => {
it("should not break @if block on comment with an apostrophe", () => {
const { css, errors } = compileWithDetails(`
$mode: on;
@if $mode == on {
/* the cat's toy fell */
.a { color: red; }
.b { color: blue; }
}
`);
expect(errors).toEqual([]);
expect(css).toContain(".a");
expect(css).toContain(".b");
expect(css).toContain("/* the cat's toy fell */");
});

it("should not break @mixin block on comment with an apostrophe", () => {
const { css, errors } = compileWithDetails(`
@mixin extras() {
/* the cat's toy fell */
.a { color: red; }
.b { color: blue; }
}
@include extras();
`);
expect(errors).toEqual([]);
expect(css).toContain(".a");
expect(css).toContain(".b");
});

it("should not swallow rules when a block comment contains a brace", () => {
const { css, errors } = compileWithDetails(`
@mixin m() {
/* use } carefully */
.a { color: red; }
}
@include m();
`);
expect(errors).toEqual([]);
expect(css).toContain(".a");
expect(css).toContain("color: red");
expect(css).toContain("/* use } carefully */");
});

it("should not break @for block on comment with an apostrophe and brace", () => {
const { css, errors } = compileWithDetails(`
@for $i from 1 through 2 {
/* it's item #{$i} { row } */
.col-#{$i} { width: $i; }
}
`);
expect(errors).toEqual([]);
expect(css).toContain(".col-1");
expect(css).toContain(".col-2");
});

it("should not break @each block on comment with an apostrophe", () => {
const { css, errors } = compileWithDetails(`
@each $c in red, blue {
/* author's note */
.#{$c} { color: $c; }
}
`);
expect(errors).toEqual([]);
expect(css).toContain(".red");
expect(css).toContain(".blue");
});

it("should not break @mixin block on a // line comment with apostrophe and brace", () => {
const { css, errors } = compileWithDetails(`
@mixin m() {
// it's a { comment
.a { color: red; }
}
@include m();
`);
expect(errors).toEqual([]);
expect(css).toContain(".a");
expect(css).toContain("color: red");
expect(css).not.toContain("comment");
});

it("should not desync a value on a block comment containing a brace or apostrophe", () => {
const { css, errors } = compileWithDetails(`
.a { margin: 1px /* it's } here */ 2px; }
.b { color: green; }
`);
expect(errors).toEqual([]);
expect(css).toContain(".b");
expect(css).toContain("color: green");
});
});

describe("error handling", () => {
it("should report undefined variables", () => {
const input = `.box { color: $undefined; }`;
Expand Down
Loading