Skip to content

Commit

Permalink
Add sass-parser support for the @use rule (#2389)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos (Goodwine) <[email protected]>
  • Loading branch information
nex3 and Goodwine authored Oct 23, 2024
1 parent 84e281e commit 473ddf9
Show file tree
Hide file tree
Showing 21 changed files with 2,453 additions and 42 deletions.
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.
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
/// [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 "
"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

0 comments on commit 473ddf9

Please sign in to comment.