Skip to content

Commit 473ddf9

Browse files
nex3Goodwine
andauthored
Add sass-parser support for the @use rule (#2389)
Co-authored-by: Carlos (Goodwine) <[email protected]>
1 parent 84e281e commit 473ddf9

21 files changed

+2453
-42
lines changed

lib/src/js/parser.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import 'package:path/path.dart' as p;
1010
import 'package:source_span/source_span.dart';
1111

1212
import '../ast/sass.dart';
13+
import '../exception.dart';
14+
import '../parse/parser.dart';
1315
import '../syntax.dart';
1416
import '../util/nullable.dart';
1517
import '../util/span.dart';
18+
import '../util/string.dart';
1619
import '../visitor/interface/expression.dart';
1720
import '../visitor/interface/statement.dart';
1821
import 'reflection.dart';
@@ -24,10 +27,14 @@ import 'visitor/statement.dart';
2427
class ParserExports {
2528
external factory ParserExports(
2629
{required Function parse,
30+
required Function parseIdentifier,
31+
required Function toCssIdentifier,
2732
required Function createExpressionVisitor,
2833
required Function createStatementVisitor});
2934

3035
external set parse(Function function);
36+
external set parseIdentifier(Function function);
37+
external set toCssIdentifier(Function function);
3138
external set createStatementVisitor(Function function);
3239
external set createExpressionVisitor(Function function);
3340
}
@@ -45,6 +52,8 @@ ParserExports loadParserExports() {
4552
_updateAstPrototypes();
4653
return ParserExports(
4754
parse: allowInterop(_parse),
55+
parseIdentifier: allowInterop(_parseIdentifier),
56+
toCssIdentifier: allowInterop(_toCssIdentifier),
4857
createExpressionVisitor: allowInterop(
4958
(JSExpressionVisitorObject inner) => JSExpressionVisitor(inner)),
5059
createStatementVisitor: allowInterop(
@@ -117,3 +126,18 @@ Stylesheet _parse(String css, String syntax, String? path) => Stylesheet.parse(
117126
_ => throw UnsupportedError('Unknown syntax "$syntax"')
118127
},
119128
url: path.andThen(p.toUri));
129+
130+
/// A JavaScript-friendly method to parse an identifier to its semantic value.
131+
///
132+
/// Returns null if [identifier] isn't a valid identifier.
133+
String? _parseIdentifier(String identifier) {
134+
try {
135+
return Parser.parseIdentifier(identifier);
136+
} on SassFormatException {
137+
return null;
138+
}
139+
}
140+
141+
/// A JavaScript-friendly method to convert text to a valid CSS identifier with
142+
/// the same contents.
143+
String _toCssIdentifier(String text) => text.toCssIdentifier();

lib/src/util/character.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import 'package:charcode/charcode.dart';
1010
/// lowercase equivalents.
1111
const _asciiCaseBit = 0x20;
1212

13+
/// The highest character allowed in CSS.
14+
///
15+
/// See https://drafts.csswg.org/css-syntax-3/#maximum-allowed-code-point
16+
const maxAllowedCharacter = 0x10FFFF;
17+
1318
// Define these checks as extension getters so they can be used in pattern
1419
// matches.
1520
extension CharacterExtension on int {
@@ -35,6 +40,12 @@ extension CharacterExtension on int {
3540
// 0x36 == 0b110110.
3641
this >> 10 == 0x36;
3742

43+
/// Returns whether [character] is the end of a UTF-16 surrogate pair.
44+
bool get isLowSurrogate =>
45+
// A character is a high surrogate exactly if it matches 0b110111XXXXXXXXXX.
46+
// 0x36 == 0b110111.
47+
this >> 10 == 0x37;
48+
3849
/// Returns whether [character] is a Unicode private-use code point in the Basic
3950
/// Multilingual Plane.
4051
///

lib/src/util/string.dart

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:charcode/charcode.dart';
6+
import 'package:string_scanner/string_scanner.dart';
7+
8+
import 'character.dart';
9+
10+
extension StringExtension on String {
11+
/// Returns a minimally-escaped CSS identifiers whose contents evaluates to
12+
/// [text].
13+
///
14+
/// Throws a [FormatException] if [text] cannot be represented as a CSS
15+
/// identifier (such as the empty string).
16+
String toCssIdentifier() {
17+
var buffer = StringBuffer();
18+
var scanner = SpanScanner(this);
19+
20+
void writeEscape(int character) {
21+
buffer.writeCharCode($backslash);
22+
buffer.write(character.toRadixString(16));
23+
if (scanner.peekChar() case int(isHex: true)) {
24+
buffer.writeCharCode($space);
25+
}
26+
}
27+
28+
void consumeSurrogatePair(int character) {
29+
if (scanner.peekChar(1) case null || int(isLowSurrogate: false)) {
30+
scanner.error(
31+
"An individual surrogates can't be represented as a CSS "
32+
"identifier.",
33+
length: 1);
34+
} else if (character.isPrivateUseHighSurrogate) {
35+
writeEscape(combineSurrogates(scanner.readChar(), scanner.readChar()));
36+
} else {
37+
buffer.writeCharCode(scanner.readChar());
38+
buffer.writeCharCode(scanner.readChar());
39+
}
40+
}
41+
42+
var doubleDash = false;
43+
if (scanner.scanChar($dash)) {
44+
if (scanner.isDone) return '\\2d';
45+
46+
buffer.writeCharCode($dash);
47+
48+
if (scanner.scanChar($dash)) {
49+
buffer.writeCharCode($dash);
50+
doubleDash = true;
51+
}
52+
}
53+
54+
if (!doubleDash) {
55+
switch (scanner.peekChar()) {
56+
case null:
57+
scanner.error(
58+
"The empty string can't be represented as a CSS identifier.");
59+
60+
case 0:
61+
scanner.error("The U+0000 can't be represented as a CSS identifier.");
62+
63+
case int character when character.isHighSurrogate:
64+
consumeSurrogatePair(character);
65+
66+
case int(isLowSurrogate: true):
67+
scanner.error(
68+
"An individual surrogate can't be represented as a CSS "
69+
"identifier.",
70+
length: 1);
71+
72+
case int(isNameStart: true, isPrivateUseBMP: false):
73+
buffer.writeCharCode(scanner.readChar());
74+
75+
case _:
76+
writeEscape(scanner.readChar());
77+
}
78+
}
79+
80+
loop:
81+
while (true) {
82+
switch (scanner.peekChar()) {
83+
case null:
84+
break loop;
85+
86+
case 0:
87+
scanner.error("The U+0000 can't be represented as a CSS identifier.");
88+
89+
case int character when character.isHighSurrogate:
90+
consumeSurrogatePair(character);
91+
92+
case int(isLowSurrogate: true):
93+
scanner.error(
94+
"An individual surrogate can't be represented as a CSS "
95+
"identifier.",
96+
length: 1);
97+
98+
case int(isName: true, isPrivateUseBMP: false):
99+
buffer.writeCharCode(scanner.readChar());
100+
101+
case _:
102+
writeEscape(scanner.readChar());
103+
}
104+
}
105+
106+
return buffer.toString();
107+
}
108+
}

lib/src/utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ int consumeEscapedCharacter(StringScanner scanner) {
409409
if (scanner.peekChar().isWhitespace) scanner.readChar();
410410

411411
return switch (value) {
412-
0 || (>= 0xD800 && <= 0xDFFF) || >= 0x10FFFF => 0xFFFD,
412+
0 || (>= 0xD800 && <= 0xDFFF) || >= maxAllowedCharacter => 0xFFFD,
413413
_ => value
414414
};
415415
case _:

pkg/sass-parser/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Add `BooleanExpression` and `NumberExpression`.
44

5+
* Add support for parsing the `@use` rule.
6+
57
## 0.4.0
68

79
* **Breaking change:** Warnings are no longer emitted during parsing, so the

pkg/sass-parser/lib/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@ import {Root} from './src/statement/root';
88
import * as sassInternal from './src/sass-internal';
99
import {Stringifier} from './src/stringifier';
1010

11+
export {
12+
Configuration,
13+
ConfigurationProps,
14+
ConfigurationRaws,
15+
} from './src/configuration';
16+
export {
17+
ConfiguredVariable,
18+
ConfiguredVariableObjectProps,
19+
ConfiguredVariableExpressionProps,
20+
ConfiguredVariableProps,
21+
ConfiguredVariableRaws,
22+
} from './src/configured-variable';
1123
export {AnyNode, Node, NodeProps, NodeType} from './src/node';
24+
export {RawWithValue} from './src/raw-with-value';
1225
export {
1326
AnyExpression,
1427
Expression,
@@ -71,6 +84,7 @@ export {
7184
SassCommentProps,
7285
SassCommentRaws,
7386
} from './src/statement/sass-comment';
87+
export {UseRule, UseRuleProps, UseRuleRaws} from './src/statement/use-rule';
7488
export {
7589
AnyStatement,
7690
AtRule,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a configured variable toJSON 1`] = `
4+
{
5+
"expression": <"qux">,
6+
"guarded": false,
7+
"inputs": [
8+
{
9+
"css": "@use "foo" with ($baz: "qux")",
10+
"hasBOM": false,
11+
"id": "<input css _____>",
12+
},
13+
],
14+
"raws": {},
15+
"sassType": "configured-variable",
16+
"source": <1:18-1:29 in 0>,
17+
"variableName": "baz",
18+
}
19+
`;

0 commit comments

Comments
 (0)