Skip to content

Commit

Permalink
feat: support nestedSelector & selectorText
Browse files Browse the repository at this point in the history
  • Loading branch information
devjiangzhou committed Aug 29, 2022
1 parent c0a42ba commit b62b91e
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 43 deletions.
15 changes: 7 additions & 8 deletions webf/lib/src/css/css_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import 'package:webf/css.dart';

/// https://drafts.csswg.org/cssom/#the-cssstylerule-interface
class CSSStyleRule extends CSSRule {
final SelectorTextVisitor _selectorTextVisitor = SelectorTextVisitor();

String get selectorText {
_selectorTextVisitor.visitSelectorGroup(selectorGroup);
return _selectorTextVisitor.toString();
}

@override
String get cssText => declaration.cssText;

Expand All @@ -21,14 +28,6 @@ class CSSStyleRule extends CSSRule {
SimpleSelector? get lastSimpleSelector {
return selectorGroup.selectors.last.simpleSelectorSequences.last.simpleSelector;
}

String get selectorText {
var sb = StringBuffer();
selectorGroup.selectors.forEach((selector) {
sb.write(selector.simpleSelectorSequences.map((ss) => ss.simpleSelector.name).join(' '));
});
return sb.toString();
}
}

class KeyFrameBlock {
Expand Down
63 changes: 42 additions & 21 deletions webf/lib/src/css/parser/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ class CSSParser {
List<CSSRule> parseRules({int startPosition = 0}) {
var rules = <CSSRule>[];
while (!_maybeEat(TokenKind.END_OF_FILE)) {
final rule = processRule();
if (rule != null) {
rules.add(rule);
final data = processRule();
if (data != null) {
rules.addAll(data);
} else {
_next();
}
Expand Down Expand Up @@ -378,7 +378,10 @@ class CSSParser {
selectors.add(selector);
} while (_maybeEat(TokenKind.COMMA));

keyframe.add(KeyFrameBlock(selectors, processDeclarations()));
final declarations = processDeclarations();
if (declarations.last is CSSStyleDeclaration) {
keyframe.add(KeyFrameBlock(selectors, declarations.last));
}
} while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());

return keyframe;
Expand Down Expand Up @@ -424,27 +427,45 @@ class CSSParser {
return null;
}

CSSRule? processRule([SelectorGroup? selectorGroup]) {
List<CSSRule>? processRule([SelectorGroup? selectorGroup]) {
if (selectorGroup == null) {
final directive = processDirective();
if (directive != null) {
_maybeEat(TokenKind.SEMICOLON);
return directive;
return [directive];
}
selectorGroup = processSelectorGroup();
}
if (selectorGroup != null) {
return CSSStyleRule(selectorGroup, processDeclarations());
final declarations = processDeclarations();
CSSStyleDeclaration declaration = declarations.where((element) => element is CSSStyleDeclaration).last!;
Iterable childRules = declarations.where((element) => element is CSSStyleRule);
CSSStyleRule rule = CSSStyleRule(selectorGroup, declaration);
List<CSSRule> rules = [rule];
for (CSSStyleRule childRule in childRules) {
// child Rule
for (Selector selector in childRule.selectorGroup.selectors) {
// parentRule
for (Selector parentSelector in selectorGroup.selectors) {
List<SimpleSelectorSequence> newSelectorSequences =
mergeNestedSelector(parentSelector.simpleSelectorSequences, selector.simpleSelectorSequences);
selector.simpleSelectorSequences.clear();
selector.simpleSelectorSequences.addAll(newSelectorSequences);
}
}
rules.add(childRule);
}
return rules;
}
return null;
}

List<CSSRule> processGroupRuleBody() {
var nodes = <CSSRule>[];
while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
var rule = processRule();
if (rule != null) {
nodes.add(rule);
var rules = processRule();
if (rules != null) {
nodes.addAll(rules);
continue;
}
break;
Expand Down Expand Up @@ -475,9 +496,6 @@ class CSSParser {
/// Return [:null:] if no selector or [SelectorGroup] if a selector was
/// parsed.
SelectorGroup? _nestedSelector() {
// var oldMessages = messages;
// _createMessages();

var markedData = _mark;

// Look a head do we have a nested selector instead of a declaration?
Expand All @@ -489,33 +507,32 @@ class CSSParser {
if (!nestedSelector) {
// Not a selector so restore the world.
_restore(markedData);
// messages = oldMessages;
return null;
} else {
// Remember any messages from look ahead.
// oldMessages.mergeMessages(messages);
// messages = oldMessages;
return selGroup;
}
}

CSSStyleDeclaration processDeclarations({bool checkBrace = true}) {
// return list of rule && CSSStyleDeclaration
List<dynamic> processDeclarations({bool checkBrace = true}) {
if (checkBrace) _eat(TokenKind.LBRACE);

var declaration = CSSStyleDeclaration();
List list = [declaration];
do {
var selectorGroup = _nestedSelector();
while (selectorGroup != null) {
// Nested selector so process as a ruleset.
processRule(selectorGroup)!;
List<CSSRule> rule = processRule(selectorGroup)!;
list.addAll(rule);
selectorGroup = _nestedSelector();
}
processDeclaration(declaration);
} while (_maybeEat(TokenKind.SEMICOLON));

if (checkBrace) _eat(TokenKind.RBRACE);

return declaration;
return list;
}

SelectorGroup? processSelectorGroup() {
Expand Down Expand Up @@ -732,9 +749,13 @@ class CSSParser {
// TODO(terry): If no identifier specified consider optimizing out the
// : or :: and making this a normal selector. For now,
// create an empty pseudoName.
// TODO(jiangzhou): Forced to evade
Identifier pseudoName;
if (_peekIdentifier() && _peekToken.text != 'var' && _peekToken.text != 'rgb' && _peekToken.text != 'rgba') {
if (_peekIdentifier()) {
pseudoName = identifier();
if (pseudoName.isFunction()) {
return null;
}
} else {
return null;
}
Expand Down
46 changes: 46 additions & 0 deletions webf/lib/src/css/parser/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,49 @@ class NegationSelector extends SimpleSelector {
@override
dynamic visit(Visitor visitor) => visitor.visitNegationSelector(this);
}

/// Merge the nested selector sequences [current] to the [parent] sequences or
/// substitue any & with the parent selector.
List<SimpleSelectorSequence> mergeNestedSelector(
List<SimpleSelectorSequence> parent, List<SimpleSelectorSequence> current) {
// If any & operator then the parent selector will be substituted otherwise
// the parent selector is pre-pended to the current selector.
var hasThis = current.any((s) => s.simpleSelector.isThis);

var newSequence = <SimpleSelectorSequence>[];

if (!hasThis) {
// If no & in the sector group then prefix with the parent selector.
newSequence.addAll(parent);
newSequence.addAll(_convertToDescendentSequence(current));
} else {
for (var sequence in current) {
if (sequence.simpleSelector.isThis) {
// Substitue the & with the parent selector and only use a combinator
// descendant if & is prefix by a sequence with an empty name e.g.,
// "... + &", "&", "... ~ &", etc.
var hasPrefix = newSequence.isNotEmpty && newSequence.last.simpleSelector.name.isNotEmpty;
newSequence.addAll(hasPrefix ? _convertToDescendentSequence(parent) : parent);
} else {
newSequence.add(sequence);
}
}
}

return newSequence;
}

/// Return selector sequences with first sequence combinator being a
/// descendant. Used for nested selectors when the parent selector needs to
/// be prefixed to a nested selector or to substitute the this (&) with the
/// parent selector.
List<SimpleSelectorSequence> _convertToDescendentSequence(List<SimpleSelectorSequence> sequences) {
if (sequences.isEmpty) return sequences;

var newSequences = <SimpleSelectorSequence>[];
var first = sequences.first;
newSequences.add(SimpleSelectorSequence(first.simpleSelector, TokenKind.COMBINATOR_DESCENDANT));
newSequences.addAll(sequences.skip(1));

return newSequences;
}
19 changes: 19 additions & 0 deletions webf/lib/src/css/parser/tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ class Identifier extends TreeNode {
// a valid identifier.
return name;
}

bool isFunction() {
return name == 'var' ||
name == 'rgb' ||
name == 'rgba' ||
name == 'translate' ||
name == 'rotate' ||
name == 'calc' ||
name == 'hsl' ||
name == 'hsla' ||
name == 'linear-gradient' ||
name == 'radial-gradient' ||
name == 'repeating-linear-gradient' ||
name == 'repeating-radial-gradient' ||
name == 'cubic-bezier' ||
name == 'attr' ||
name == 'url';
;
}
}

class Wildcard extends TreeNode {
Expand Down
91 changes: 91 additions & 0 deletions webf/lib/src/css/parser/visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,94 @@ class SelectorVisitor implements Visitor {
@override
dynamic visitNegationSelector(NegationSelector node) => visitSimpleSelector(node);
}

class SelectorTextVisitor extends Visitor {
StringBuffer _buff = StringBuffer();

void emit(String str) {
_buff.write(str);
}

@override
String toString() => _buff.toString().trim();

@override
dynamic visitSelectorGroup(SelectorGroup node) {
_buff = StringBuffer();
var selectors = node.selectors;
var selectorsLength = selectors.length;
for (var i = 0; i < selectorsLength; i++) {
if (i > 0) emit(',');
selectors[i].visit(this);
}
}

@override
void visitSelector(Selector node) {
for (var selectorSequences in node.simpleSelectorSequences) {
selectorSequences.visit(this);
}
}

@override
void visitSimpleSelectorSequence(SimpleSelectorSequence node) {
emit(node.combinatorToString);
node.simpleSelector.visit(this);
}

@override
void visitSimpleSelector(SimpleSelector node) {
emit(node.name);
}

@override
void visitElementSelector(ElementSelector node) {
emit(node.toString());
}

@override
void visitAttributeSelector(AttributeSelector node) {
emit(node.toString());
}

@override
void visitIdSelector(IdSelector node) {
emit(node.toString());
}

@override
void visitClassSelector(ClassSelector node) {
emit(node.toString());
}

@override
void visitPseudoClassSelector(PseudoClassSelector node) {
emit(node.toString());
}

@override
void visitPseudoElementSelector(PseudoElementSelector node) {
emit(node.toString());
}

@override
void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node) {
emit(':${node.name}(');
node.argument.visit(this);
emit(')');
}

@override
void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node) {
emit('::${node.name}(');
node.expression.join(' , ');
emit(')');
}

@override
void visitNegationSelector(NegationSelector node) {
emit(':not(');
node.negationArg!.visit(this);
emit(')');
}
}
1 change: 0 additions & 1 deletion webf/lib/src/css/rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:webf/css.dart';
abstract class CSSRule {
String cssText = '';
CSSStyleSheet? parentStyleSheet;
CSSRule? parentRule;

int position = -1;

Expand Down
19 changes: 19 additions & 0 deletions webf/test/src/css/style_rule_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,24 @@ void main() {
CSSStyleRule styleRule = rule as CSSStyleRule;
expect(styleRule.lastSimpleSelector?.name, 'foo');
});

test('19', () {
CSSRule? rule = parseSingleRule(' .foo { transform: translate(30px, 20px) rotate(20deg); }');
CSSStyleRule styleRule = rule as CSSStyleRule;
expect(styleRule.lastSimpleSelector?.name, 'foo');
});

test('20', () {
CSSStyleSheet sheet = CSSParser('#header { color: red; h1 { font-size: 26px; }}').parse();
CSSStyleRule styleRule1 = sheet.cssRules[0] as CSSStyleRule;
CSSStyleRule styleRule2 = sheet.cssRules[1] as CSSStyleRule;
final visitor = SelectorTextVisitor();
visitor.visitSelectorGroup(styleRule1.selectorGroup);
expect(visitor.toString(), '#header');
expect(styleRule1.declaration.getPropertyValue('color'), 'red');
visitor.visitSelectorGroup(styleRule2.selectorGroup);
expect(visitor.toString(), '#header h1');
expect(styleRule2.declaration.getPropertyValue('fontSize'), '26px');
});
});
}
Loading

0 comments on commit b62b91e

Please sign in to comment.