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
Open
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.79.7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it is intended, 1.79.6 has not been released.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks


* No user-visible changes.

## 1.79.6

* Fix a bug where Sass would add an extra `*/` after loud comments with
Expand Down
25 changes: 25 additions & 0 deletions lib/src/js/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';

import '../ast/sass.dart';
import '../exception.dart';
import '../logger.dart';
import '../logger/js_to_dart.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 'logger.dart';
Expand All @@ -27,10 +30,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 @@ -48,6 +55,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 @@ -122,3 +131,19 @@ Stylesheet _parse(String css, String syntax, String? path, JSLogger? logger) =>
},
url: path.andThen(p.toUri),
logger: JSToDartLogger(logger, Logger.stderr()));

/// 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, [JSLogger? logger]) {
try {
return Parser.parseIdentifier(identifier,
logger: JSToDartLogger(logger, Logger.stderr()));
} 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();
9 changes: 9 additions & 0 deletions lib/src/util/character.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import 'package:charcode/charcode.dart';
/// lowercase equivalents.
const _asciiCaseBit = 0x20;

/// The highest character allowed in a
nex3 marked this conversation as resolved.
Show resolved Hide resolved
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 +38,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
110 changes: 110 additions & 0 deletions lib/src/util/string.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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) || null) {
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());
}
}

void consumeBody() {
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());
}
}
}

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

buffer.writeCharCode($dash);

if (scanner.scanChar($dash)) {
buffer.writeCharCode($dash);
consumeBody();
return buffer.toString();
}
}

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());
}

consumeBody();
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
4 changes: 4 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.7

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

## 0.2.6

* No user-visible changes.
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 @@ -9,7 +9,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,
ConfiguredVariableValueProps,
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 @@ -62,6 +75,7 @@ export {
SassCommentProps,
SassCommentRaws,
} from './src/statement/sass-comment';
export {UseRule, UseRuleProps, UseRuleRaws} from './src/statement/use-rule';
export {
AnyStatement,
AtRule,
Expand Down
Loading