Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sass-parser support for the @use rule #2389

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
24 changes: 24 additions & 0 deletions lib/src/js/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';

import '../ast/sass.dart';
import '../exception.dart';
import '../parse/parser.dart';
import '../syntax.dart';
import '../util/nullable.dart';
import '../util/span.dart';
import '../util/string.dart';
import '../visitor/interface/expression.dart';
import '../visitor/interface/statement.dart';
import 'reflection.dart';
Expand All @@ -24,10 +27,14 @@ import 'visitor/statement.dart';
class ParserExports {
external factory ParserExports(
{required Function parse,
required Function parseIdentifier,
required Function toCssIdentifier,
required Function createExpressionVisitor,
required Function createStatementVisitor});

external set parse(Function function);
external set parseIdentifier(Function function);
external set toCssIdentifier(Function function);
external set createStatementVisitor(Function function);
external set createExpressionVisitor(Function function);
}
Expand All @@ -45,6 +52,8 @@ ParserExports loadParserExports() {
_updateAstPrototypes();
return ParserExports(
parse: allowInterop(_parse),
parseIdentifier: allowInterop(_parseIdentifier),
toCssIdentifier: allowInterop(_toCssIdentifier),
createExpressionVisitor: allowInterop(
(JSExpressionVisitorObject inner) => JSExpressionVisitor(inner)),
createStatementVisitor: allowInterop(
Expand Down Expand Up @@ -117,3 +126,18 @@ Stylesheet _parse(String css, String syntax, String? path) => Stylesheet.parse(
_ => throw UnsupportedError('Unknown syntax "$syntax"')
},
url: path.andThen(p.toUri));

/// A JavaScript-friendly method to parse an identifier to its semantic value.
///
/// Returns null if [identifier] isn't a valid identifier.
String? _parseIdentifier(String identifier) {
try {
return Parser.parseIdentifier(identifier);
} on SassFormatException {
return null;
}
}

/// A JavaScript-friendly method to convert text to a valid CSS identifier with
/// the same contents.
String _toCssIdentifier(String text) => text.toCssIdentifier();
11 changes: 11 additions & 0 deletions lib/src/util/character.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import 'package:charcode/charcode.dart';
/// lowercase equivalents.
const _asciiCaseBit = 0x20;

/// The highest character allowed in CSS.
///
/// See https://drafts.csswg.org/css-syntax-3/#maximum-allowed-code-point
const maxAllowedCharacter = 0x10FFFF;

// Define these checks as extension getters so they can be used in pattern
// matches.
extension CharacterExtension on int {
Expand All @@ -35,6 +40,12 @@ extension CharacterExtension on int {
// 0x36 == 0b110110.
this >> 10 == 0x36;

/// Returns whether [character] is the end of a UTF-16 surrogate pair.
bool get isLowSurrogate =>
// A character is a high surrogate exactly if it matches 0b110111XXXXXXXXXX.
// 0x36 == 0b110111.
nex3 marked this conversation as resolved.
Show resolved Hide resolved
this >> 10 == 0x37;

/// Returns whether [character] is a Unicode private-use code point in the Basic
/// Multilingual Plane.
///
Expand Down
108 changes: 108 additions & 0 deletions lib/src/util/string.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:charcode/charcode.dart';
import 'package:string_scanner/string_scanner.dart';

import 'character.dart';

extension StringExtension on String {
/// Returns a minimally-escaped CSS identifiers whose contents evaluates to
nex3 marked this conversation as resolved.
Show resolved Hide resolved
/// [text].
///
/// Throws a [FormatException] if [text] cannot be represented as a CSS
/// identifier (such as the empty string).
String toCssIdentifier() {
var buffer = StringBuffer();
var scanner = SpanScanner(this);

void writeEscape(int character) {
buffer.writeCharCode($backslash);
buffer.write(character.toRadixString(16));
if (scanner.peekChar() case int(isHex: true)) {
buffer.writeCharCode($space);
}
}

void consumeSurrogatePair(int character) {
if (scanner.peekChar(1) case null || int(isLowSurrogate: false)) {
scanner.error(
"An individual surrogates can't be represented as a CSS "
nex3 marked this conversation as resolved.
Show resolved Hide resolved
"identifier.",
length: 1);
} else if (character.isPrivateUseHighSurrogate) {
writeEscape(combineSurrogates(scanner.readChar(), scanner.readChar()));
} else {
buffer.writeCharCode(scanner.readChar());
buffer.writeCharCode(scanner.readChar());
}
}

var doubleDash = false;
if (scanner.scanChar($dash)) {
if (scanner.isDone) return '\\2d';

buffer.writeCharCode($dash);

if (scanner.scanChar($dash)) {
buffer.writeCharCode($dash);
doubleDash = true;
}
}

if (!doubleDash) {
switch (scanner.peekChar()) {
case null:
scanner.error(
"The empty string can't be represented as a CSS identifier.");

case 0:
scanner.error("The U+0000 can't be represented as a CSS identifier.");

case int character when character.isHighSurrogate:
consumeSurrogatePair(character);

case int(isLowSurrogate: true):
scanner.error(
"An individual surrogate can't be represented as a CSS "
"identifier.",
length: 1);

case int(isNameStart: true, isPrivateUseBMP: false):
buffer.writeCharCode(scanner.readChar());

case _:
writeEscape(scanner.readChar());
}
}

loop:
while (true) {
switch (scanner.peekChar()) {
case null:
break loop;

case 0:
scanner.error("The U+0000 can't be represented as a CSS identifier.");

case int character when character.isHighSurrogate:
consumeSurrogatePair(character);

case int(isLowSurrogate: true):
scanner.error(
"An individual surrogate can't be represented as a CSS "
"identifier.",
length: 1);

case int(isName: true, isPrivateUseBMP: false):
buffer.writeCharCode(scanner.readChar());

case _:
writeEscape(scanner.readChar());
}
}

return buffer.toString();
}
}
2 changes: 1 addition & 1 deletion lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ int consumeEscapedCharacter(StringScanner scanner) {
if (scanner.peekChar().isWhitespace) scanner.readChar();

return switch (value) {
0 || (>= 0xD800 && <= 0xDFFF) || >= 0x10FFFF => 0xFFFD,
0 || (>= 0xD800 && <= 0xDFFF) || >= maxAllowedCharacter => 0xFFFD,
_ => value
};
case _:
Expand Down
2 changes: 2 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Add `BooleanExpression` and `NumberExpression`.

* Add support for parsing the `@use` rule.

## 0.4.0

* **Breaking change:** Warnings are no longer emitted during parsing, so the
Expand Down
14 changes: 14 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ import {Root} from './src/statement/root';
import * as sassInternal from './src/sass-internal';
import {Stringifier} from './src/stringifier';

export {
Configuration,
ConfigurationProps,
ConfigurationRaws,
} from './src/configuration';
export {
ConfiguredVariable,
ConfiguredVariableObjectProps,
ConfiguredVariableExpressionProps,
ConfiguredVariableProps,
ConfiguredVariableRaws,
} from './src/configured-variable';
export {AnyNode, Node, NodeProps, NodeType} from './src/node';
export {RawWithValue} from './src/raw-with-value';
export {
AnyExpression,
Expression,
Expand Down Expand Up @@ -71,6 +84,7 @@ export {
SassCommentProps,
SassCommentRaws,
} from './src/statement/sass-comment';
export {UseRule, UseRuleProps, UseRuleRaws} from './src/statement/use-rule';
export {
AnyStatement,
AtRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a configured variable toJSON 1`] = `
{
"expression": <"qux">,
"guarded": false,
"inputs": [
{
"css": "@use "foo" with ($baz: "qux")",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "configured-variable",
"source": <1:18-1:29 in 0>,
"variableName": "baz",
}
`;
Loading
Loading