move csslib into dart svn
deleted files that duplicate those in the Dart repository (LICENSE, pubspec.yaml, codereview.settings, .gitignore)
Otherwise, just changed pkg.status and pubspec
R=terry@google.com
Review URL: https://codereview.chromium.org//23168002
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/csslib@26155 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b84dae4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+csslib in Pure Dart
+===================
+
+This is a pure [Dart][dart] [CSS parser][cssparse]. Since it's 100%
+Dart you can use it safely from a script or server side app.
+
+[![Build Status](https://drone.io/github.com/dart-lang/csslib/status.png)](https://drone.io/github.com/dart-lang/csslib/latest)
+
+Installation
+------------
+
+Add this to your `pubspec.yaml` (or create it):
+```yaml
+dependencies:
+ csslib: any
+```
+Then run the [Pub Package Manager][pub] (comes with the Dart SDK):
+
+ pub install
+
+Usage
+-----
+
+Parsing CSS is easy!
+```dart
+import 'package:csslib/parser.dart' show parse;
+import 'package:csslib/css.dart';
+
+main() {
+ var stylesheet = parse(
+ '.foo { color: red; left: 20px; top: 20px; width: 100px; height:200px }');
+ print(stylesheet.toString());
+}
+```
+
+You can pass a String or list of bytes to `parse`.
+
+
+Updating
+--------
+
+You can upgrade the library with:
+
+ pub update
+
+Disclaimer: the APIs are not finished. Updating may break your code. If that
+happens, you can check the
+[commit log](https://github.com/dart-lang/csslib/commits/master), to figure
+out what the change was.
+
+If you want to avoid breakage, you can also put the version constraint in your
+`pubspec.yaml` in place of the word `any`.
+
+Running Tests
+-------------
+
+All tests (both canary and suite) should be passing. Canary are quick test
+verifies that basic CSS is working. The suite tests are a comprehensive set of
+~11,000 tests.
+
+```bash
+export DART_SDK=path/to/dart/sdk
+
+# Make sure dependencies are installed
+pub install
+
+# Run command both canary and the suite tests
+test/run.sh
+```
+
+ Run only the canary test:
+
+```bash
+ test/run.sh canary
+```
+
+ Run only the suite tests:
+
+```bash
+ test/run.sh suite
+```
+
+[dart]: http://www.dartlang.org/
+[pub]: http://www.dartlang.org/docs/pub-package-manager/
diff --git a/bin/css.dart b/bin/css.dart
new file mode 100644
index 0000000..08f2930
--- /dev/null
+++ b/bin/css.dart
@@ -0,0 +1,8 @@
+#!/usr/bin/env dart
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:csslib/css.dart' as css;
+
+void main() => css.main();
diff --git a/example/call_parser.dart b/example/call_parser.dart
new file mode 100644
index 0000000..6271057
--- /dev/null
+++ b/example/call_parser.dart
@@ -0,0 +1,90 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+
+import 'package:csslib/parser.dart' as css;
+import 'package:csslib/visitor.dart';
+
+/**
+ * Spin-up CSS parser in checked mode to detect any problematic CSS. Normally,
+ * CSS will allow any property/value pairs regardless of validity; all of our
+ * tests (by default) will ensure that the CSS is really valid.
+ */
+StyleSheet parseCss(String cssInput, {List errors, List opts}) =>
+ css.parse(cssInput, errors: errors, options: opts == null ?
+ ['--no-colors', '--checked', '--warnings_as_errors', 'memory'] : opts);
+
+// Pretty printer for CSS.
+var emitCss = new CssPrinter();
+String prettyPrint(StyleSheet ss) =>
+ (emitCss..visitTree(ss, pretty: true)).toString();
+
+main() {
+ var errors = [];
+
+ // Parse a simple stylesheet.
+ print('1. Good CSS, parsed CSS emitted:');
+ print(' =============================');
+ var stylesheet = parseCss(
+ '@import "support/at-charset-019.css"; div { color: red; }'
+ 'button[type] { background-color: red; }'
+ '.foo { '
+ 'color: red; left: 20px; top: 20px; width: 100px; height:200px'
+ '}'
+ '#div {'
+ 'color : #00F578; border-color: #878787;'
+ '}', errors: errors);
+
+ if (!errors.isEmpty) {
+ print("Got ${errors.length} errors.\n");
+ for (var error in errors) {
+ print(error);
+ }
+ } else {
+ print(prettyPrint(stylesheet));
+ }
+
+ // Parse a stylesheet with errors
+ print('2. Catch severe syntax errors:');
+ print(' ===========================');
+ var stylesheetError = parseCss(
+ '.foo #%^&*asdf{ '
+ 'color: red; left: 20px; top: 20px; width: 100px; height:200px'
+ '}', errors: errors);
+
+ if (!errors.isEmpty) {
+ print("Got ${errors.length} errors.\n");
+ for (var error in errors) {
+ print(error);
+ }
+ } else {
+ print(stylesheetError.toString());
+ }
+
+ // Parse a stylesheet that warns (checks) problematic CSS.
+ print('3. Detect CSS problem with checking on:');
+ print(' ===================================');
+ stylesheetError = parseCss( '# div1 { color: red; }', errors: errors);
+
+ if (!errors.isEmpty) {
+ print("Detected ${errors.length} problem in checked mode.\n");
+ for (var error in errors) {
+ print(error);
+ }
+ } else {
+ print(stylesheetError.toString());
+ }
+
+ // Parse a CSS selector.
+ print('4. Parse a selector only:');
+ print(' ======================');
+ var selectorAst = css.selector('#div .foo', errors: errors);
+ if (!errors.isEmpty) {
+ print("Got ${errors.length} errors.\n");
+ for (var error in errors) {
+ print(error);
+ }
+ } else {
+ print(prettyPrint(selectorAst));
+ }
+
+}
diff --git a/example/call_parser.html b/example/call_parser.html
new file mode 100644
index 0000000..8dc02e4
--- /dev/null
+++ b/example/call_parser.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Call CSS Parser from Browser (Dart2JS and Dartium)</title>
+ </head>
+ <body>
+ <h1>Dartium/Dart2JS Test</h1>
+ <script type="application/dart" src="call_parser.dart"></script>
+ <script src="packages/browser/dart.js"></script>
+ </body>
+</html>
diff --git a/example/test.css b/example/test.css
new file mode 100644
index 0000000..42aa048
--- /dev/null
+++ b/example/test.css
@@ -0,0 +1,6 @@
+.foo #abc {
+ color: red;
+ width: 300px;
+ left: 50px;
+ right: 50px;
+}
\ No newline at end of file
diff --git a/lib/css.dart b/lib/css.dart
new file mode 100644
index 0000000..ebfc675
--- /dev/null
+++ b/lib/css.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library css;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+import 'dart:math' as Math;
+
+import 'package:path/path.dart' as path;
+import 'package:source_maps/span.dart' show SourceFile;
+
+import 'parser.dart';
+import 'visitor.dart';
+import 'src/messages.dart';
+import 'src/options.dart';
+
+void main() {
+ // TODO(jmesserly): fix this to return a proper exit code
+ var options = PreprocessorOptions.parse(new Options().arguments);
+ if (options == null) return;
+
+ messages = new Messages(options: options);
+
+ _time('Total time spent on ${options.inputFile}', () {
+ _compile(options.inputFile, options.verbose);
+ }, true);
+}
+
+void _compile(String inputPath, bool verbose) {
+ var ext = path.extension(inputPath);
+ if (ext != '.css' && ext != '.scss') {
+ messages.error("Please provide a CSS/Sass file", null);
+ return;
+ }
+ try {
+ // Read the file.
+ var filename = path.basename(inputPath);
+ var contents = new File(inputPath).readAsStringSync();
+ var file = new SourceFile.text(inputPath, contents);
+
+ // Parse the CSS.
+ var tree = _time('Parse $filename',
+ () => new Parser(file, contents).parse(), verbose);
+
+ _time('Analyzer $filename',
+ () => new Analyzer([tree], messages), verbose).run();
+
+ // Emit the processed CSS.
+ var emitter = new CssPrinter();
+ _time('Codegen $filename',
+ () => emitter.visitTree(tree, pretty: true), verbose);
+
+ // Write the contents to a file.
+ var outPath = path.join(path.dirname(inputPath), '_$filename');
+ new File(outPath).writeAsStringSync(emitter.toString());
+ } catch (e) {
+ messages.error('error processing $inputPath. Original message:\n $e', null);
+ }
+}
+
+_time(String message, callback(), bool printTime) {
+ if (!printTime) return callback();
+ final watch = new Stopwatch();
+ watch.start();
+ var result = callback();
+ watch.stop();
+ final duration = watch.elapsedMilliseconds;
+ _printMessage(message, duration);
+ return result;
+}
+
+void _printMessage(String message, int duration) {
+ var buf = new StringBuffer();
+ buf.write(message);
+ for (int i = message.length; i < 60; i++) buf.write(' ');
+ buf.write(' -- ');
+ if (duration < 10) buf.write(' ');
+ if (duration < 100) buf.write(' ');
+ buf..write(duration)..write(' ms');
+ print(buf.toString());
+}
diff --git a/lib/parser.dart b/lib/parser.dart
new file mode 100644
index 0000000..a1cd715
--- /dev/null
+++ b/lib/parser.dart
@@ -0,0 +1,2402 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.parser;
+
+import 'dart:math' as math;
+
+import 'package:source_maps/span.dart' show SourceFile, Span, FileSpan;
+
+import "visitor.dart";
+import 'src/messages.dart';
+import 'src/options.dart';
+
+part 'src/analyzer.dart';
+part 'src/property.dart';
+part 'src/token.dart';
+part 'src/tokenizer_base.dart';
+part 'src/tokenizer.dart';
+part 'src/tokenkind.dart';
+
+
+/** Used for parser lookup ahead (used for nested selectors Less support). */
+class ParserState extends TokenizerState {
+ final Token peekToken;
+ final Token previousToken;
+
+ ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer)
+ : super(tokenizer);
+}
+
+void _createMessages({List errors, List options}) {
+ if (errors == null) errors = [];
+
+ if (options == null) {
+ options = ['--no-colors', 'memory'];
+ }
+ var opt = PreprocessorOptions.parse(options);
+ messages = new Messages(options: opt, printHandler: errors.add);
+}
+
+/** CSS checked mode enabled. */
+bool get isChecked => messages.options.checked;
+
+// TODO(terry): Remove nested name parameter.
+/** Parse and analyze the CSS file. */
+StyleSheet compile(var input, {List errors, List options, bool nested: true}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors, options: options);
+
+ var file = new SourceFile.text(null, source);
+
+ var tree = new Parser(file, source).parse();
+
+ analyze([tree], errors: errors, options: options);
+
+ return tree;
+}
+
+/** Analyze the CSS file. */
+void analyze(List<StyleSheet> styleSheets, {List errors, List options}) {
+ _createMessages(errors: errors, options: options);
+ new Analyzer(styleSheets, messages).run();
+}
+
+/**
+ * Parse the [input] CSS stylesheet into a tree. The [input] can be a [String],
+ * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional
+ * [errors] list will contain each error/warning as a [Message].
+ */
+StyleSheet parse(var input, {List errors, List options}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors, options: options);
+
+ var file = new SourceFile.text(null, source);
+
+ return new Parser(file, source).parse();
+}
+
+/**
+ * Parse the [input] CSS selector into a tree. The [input] can be a [String],
+ * or [List<int>] of bytes and returns a [StyleSheet] AST. The optional
+ * [errors] list will contain each error/warning as a [Message].
+ */
+StyleSheet selector(var input, {List errors}) {
+ var source = _inputAsString(input);
+
+ _createMessages(errors: errors);
+
+ var file = new SourceFile.text(null, source);
+
+ return new Parser(file, source).parseSelector();
+}
+
+String _inputAsString(var input) {
+ String source;
+
+ if (input is String) {
+ source = input;
+ } else if (input is List<int>) {
+ // TODO(terry): The parse function needs an "encoding" argument and will
+ // default to whatever encoding CSS defaults to.
+ //
+ // Here's some info about CSS encodings:
+ // http://www.w3.org/International/questions/qa-css-charset.en.php
+ //
+ // As JMesserly suggests it will probably need a "preparser" html5lib
+ // (encoding_parser.dart) that interprets the bytes as ASCII and scans for
+ // @charset. But for now an "encoding" argument would work. Often the
+ // HTTP header will indicate the correct encoding.
+ //
+ // See encoding helpers at: package:html5lib/lib/src/char_encodings.dart
+ // These helpers can decode in different formats given an encoding name
+ // (mostly unicode, ascii, windows-1252 which is html5 default encoding).
+ source = new String.fromCharCodes(input);
+ } else {
+ // TODO(terry): Support RandomAccessFile using console.
+ throw new ArgumentError("'source' must be a String or "
+ "List<int> (of bytes). RandomAccessFile not supported from this "
+ "simple interface");
+ }
+
+ return source;
+}
+
+/** A simple recursive descent parser for CSS. */
+class Parser {
+ Tokenizer tokenizer;
+
+ /** Base url of CSS file. */
+ final String _baseUrl;
+
+ /**
+ * File containing the source being parsed, used to report errors with
+ * source-span locations.
+ */
+ final SourceFile file;
+
+ Token _previousToken;
+ Token _peekToken;
+
+ Parser(SourceFile file, String text, {int start: 0, String baseUrl})
+ : this.file = file,
+ _baseUrl = baseUrl,
+ tokenizer = new Tokenizer(file, text, true, start) {
+ _peekToken = tokenizer.next();
+ }
+
+ /** Main entry point for parsing an entire CSS file. */
+ StyleSheet parse() {
+ List<TreeNode> productions = [];
+
+ int start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
+ // TODO(terry): Need to handle charset.
+ var directive = processDirective();
+ if (directive != null) {
+ productions.add(directive);
+ _maybeEat(TokenKind.SEMICOLON);
+ } else {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset != null) {
+ productions.add(ruleset);
+ } else {
+ break;
+ }
+ }
+ }
+
+ checkEndOfFile();
+
+ return new StyleSheet(productions, _makeSpan(start));
+ }
+
+ /** Main entry point for parsing a simple selector sequence. */
+ StyleSheet parseSelector() {
+ List<TreeNode> productions = [];
+
+ int start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
+ var selector = processSelector();
+ if (selector != null) {
+ productions.add(selector);
+ }
+ }
+
+ checkEndOfFile();
+
+ return new StyleSheet.selector(productions, _makeSpan(start));
+ }
+
+ /** Generate an error if [file] has not been completely consumed. */
+ void checkEndOfFile() {
+ if (!(_peekKind(TokenKind.END_OF_FILE) ||
+ _peekKind(TokenKind.INCOMPLETE_COMMENT))) {
+ _error('premature end of file unknown CSS', _peekToken.span);
+ }
+ }
+
+ /** Guard to break out of parser when an unexpected end of file is found. */
+ // TODO(jimhug): Failure to call this method can lead to inifinite parser
+ // loops. Consider embracing exceptions for more errors to reduce
+ // the danger here.
+ bool isPrematureEndOfFile() {
+ if (_maybeEat(TokenKind.END_OF_FILE)) {
+ _error('unexpected end of file', _peekToken.span);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // Basic support methods
+ ///////////////////////////////////////////////////////////////////
+ int _peek() {
+ return _peekToken.kind;
+ }
+
+ Token _next({unicodeRange : false}) {
+ _previousToken = _peekToken;
+ _peekToken = tokenizer.next(unicodeRange: unicodeRange);
+ return _previousToken;
+ }
+
+ bool _peekKind(int kind) {
+ return _peekToken.kind == kind;
+ }
+
+ /* Is the next token a legal identifier? This includes pseudo-keywords. */
+ bool _peekIdentifier() {
+ return TokenKind.isIdentifier(_peekToken.kind);
+ }
+
+ /** Marks the parser/tokenizer look ahead to support Less nested selectors. */
+ ParserState get _mark =>
+ new ParserState(_peekToken, _previousToken, tokenizer);
+
+ /** Restores the parser/tokenizer state to state remembered by _mark. */
+ void _restore(ParserState markedData) {
+ tokenizer.restore(markedData);
+ _peekToken = markedData.peekToken;
+ _previousToken = markedData.previousToken;
+ }
+
+ bool _maybeEat(int kind, {unicodeRange : false}) {
+ if (_peekToken.kind == kind) {
+ _previousToken = _peekToken;
+ _peekToken = tokenizer.next(unicodeRange: unicodeRange);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void _eat(int kind, {unicodeRange : false}) {
+ if (!_maybeEat(kind, unicodeRange: unicodeRange)) {
+ _errorExpected(TokenKind.kindToString(kind));
+ }
+ }
+
+ void _eatSemicolon() {
+ _eat(TokenKind.SEMICOLON);
+ }
+
+ void _errorExpected(String expected) {
+ var tok = _next();
+ var message;
+ try {
+ message = 'expected $expected, but found $tok';
+ } catch (e) {
+ message = 'parsing error expected $expected';
+ }
+ _error(message, tok.span);
+ }
+
+ void _error(String message, Span location) {
+ if (location == null) {
+ location = _peekToken.span;
+ }
+ messages.error(message, location);
+ }
+
+ void _warning(String message, Span location) {
+ if (location == null) {
+ location = _peekToken.span;
+ }
+ messages.warning(message, location);
+ }
+
+ Span _makeSpan(int start) {
+ // TODO(terry): there are places where we are creating spans before we eat
+ // the tokens, so using _previousToken.end is not always valid.
+ var end = _previousToken != null && _previousToken.end >= start
+ ? _previousToken.end : _peekToken.end;
+ return file.span(start, end);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // Top level productions
+ ///////////////////////////////////////////////////////////////////
+
+ /**
+ * The media_query_list production below replaces the media_list production
+ * from CSS2 the new grammar is:
+ *
+ * media_query_list
+ * : S* [media_query [ ',' S* media_query ]* ]?
+ * media_query
+ * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
+ * | expression [ AND S* expression ]*
+ * media_type
+ * : IDENT
+ * expression
+ * : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ * media_feature
+ * : IDENT
+ */
+ List<MediaQuery> processMediaQueryList() {
+ var mediaQueries = [];
+
+ bool firstTime = true;
+ var mediaQuery;
+ do {
+ mediaQuery = processMediaQuery(firstTime == true);
+ if (mediaQuery != null) {
+ mediaQueries.add(mediaQuery);
+ firstTime = false;
+ continue;
+ }
+
+ // Any more more media types separated by comma.
+ if (!_maybeEat(TokenKind.COMMA)) break;
+
+ // Yep more media types start again.
+ firstTime = true;
+ } while ((!firstTime && mediaQuery != null) || firstTime);
+
+ return mediaQueries;
+ }
+
+ MediaQuery processMediaQuery([bool startQuery = true]) {
+ // Grammar: [ONLY | NOT]? S* media_type S*
+ // [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*
+
+ int start = _peekToken.start;
+
+ // Is it a unary media operator?
+ var op = _peekToken.text;
+ var opLen = op.length;
+ var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
+ if (unaryOp != -1) {
+ if (isChecked) {
+ if (startQuery &&
+ unaryOp != TokenKind.MEDIA_OP_NOT ||
+ unaryOp != TokenKind.MEDIA_OP_ONLY) {
+ _warning("Only the unary operators NOT and ONLY allowed",
+ _makeSpan(start));
+ }
+ if (!startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
+ _warning("Only the binary AND operator allowed", _makeSpan(start));
+ }
+ }
+ _next();
+ start = _peekToken.start;
+ }
+
+ var type;
+ if (startQuery && unaryOp != TokenKind.MEDIA_OP_AND) {
+ // Get the media type.
+ if (_peekIdentifier()) type = identifier();
+ }
+
+ var exprs = [];
+
+ if (unaryOp == -1 || unaryOp == TokenKind.MEDIA_OP_AND) {
+ var andOp = false;
+ while (true) {
+ var expr = processMediaExpression(andOp);
+ if (expr == null) break;
+
+ exprs.add(expr);
+ op = _peekToken.text;
+ opLen = op.length;
+ andOp = TokenKind.matchMediaOperator(op, 0, opLen) ==
+ TokenKind.MEDIA_OP_AND;
+ if (!andOp) break;
+ _next();
+ }
+ }
+
+ if (unaryOp != -1 || type != null || exprs.length > 0) {
+ return new MediaQuery(unaryOp, type, exprs, _makeSpan(start));
+ }
+ }
+
+ MediaExpression processMediaExpression([bool andOperator = false]) {
+ int start = _peekToken.start;
+
+ // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ if (_maybeEat(TokenKind.LPAREN)) {
+ if (_peekIdentifier()) {
+ var feature = identifier(); // Media feature.
+ while (_maybeEat(TokenKind.COLON)) {
+ int startExpr = _peekToken.start;
+ var exprs = processExpr();
+ if (_maybeEat(TokenKind.RPAREN)) {
+ return new MediaExpression(andOperator, feature, exprs,
+ _makeSpan(startExpr));
+ } else if (isChecked) {
+ _warning("Missing parenthesis around media expression",
+ _makeSpan(start));
+ return null;
+ }
+ }
+ } else if (isChecked) {
+ _warning("Missing media feature in media expression", _makeSpan(start));
+ return null;
+ }
+ }
+ }
+
+ // Directive grammar:
+ //
+ // import: '@import' [string | URI] media_list?
+ // media: '@media' media_query_list '{' ruleset '}'
+ // page: '@page' [':' IDENT]? '{' declarations '}'
+ // stylet: '@stylet' IDENT '{' ruleset '}'
+ // media_query_list: IDENT [',' IDENT]
+ // keyframes: '@-webkit-keyframes ...' (see grammar below).
+ // font_face: '@font-face' '{' declarations '}'
+ // namespace: '@namespace name url("xmlns")
+ // host: '@host '{' ruleset '}'
+ processDirective() {
+ int start = _peekToken.start;
+
+ var tokId = _peek();
+ // Handle case for @ directive (where there's a whitespace between the @
+ // sign and the directive name. Technically, it's not valid grammar but
+ // a number of CSS tests test for whitespace between @ and name.
+ if (tokId == TokenKind.AT) {
+ Token tok = _next();
+ tokId = _peek();
+ if (_peekIdentifier()) {
+ // Is it a directive?
+ var directive = _peekToken.text;
+ var directiveLen = directive.length;
+ tokId = TokenKind.matchDirectives(directive, 0, directiveLen);
+ if (tokId == -1) {
+ tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen);
+ }
+ }
+
+ if (tokId == -1) {
+ if (messages.options.lessSupport) {
+ // Less compatibility:
+ // @name: value; => var-name: value; (VarDefinition)
+ // property: @name; => property: var(name); (VarUsage)
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr();
+
+ var span = _makeSpan(start);
+ return new VarDefinitionDirective(
+ new VarDefinition(name, exprs, span), span);
+ } else if (isChecked) {
+ _error('unexpected directive @$_peekToken', _peekToken.span);
+ }
+ }
+ }
+
+ switch (tokId) {
+ case TokenKind.DIRECTIVE_IMPORT:
+ _next();
+
+ // @import "uri_string" or @import url("uri_string") are identical; only
+ // a url can follow an @import.
+ String importStr;
+ if (_peekIdentifier()) {
+ var func = processFunction(identifier());
+ if (func is UriTerm) {
+ importStr = func.text;
+ }
+ } else {
+ importStr = processQuotedString(false);
+ }
+
+ // Any medias?
+ var medias = processMediaQueryList();
+
+ if (importStr == null) {
+ _error('missing import string', _peekToken.span);
+ }
+
+ return new ImportDirective(importStr.trim(), medias, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_MEDIA:
+ _next();
+
+ // Any medias?
+ var media = processMediaQueryList();
+
+ List<TreeNode> rulesets = [];
+ if (_maybeEat(TokenKind.LBRACE)) {
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) break;
+ rulesets.add(ruleset);
+ }
+
+ if (!_maybeEat(TokenKind.RBRACE)) {
+ _error('expected } after ruleset for @media', _peekToken.span);
+ }
+ } else {
+ _error('expected { after media before ruleset', _peekToken.span);
+ }
+ return new MediaDirective(media, rulesets, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_HOST:
+ _next();
+
+ List<TreeNode> rulesets = [];
+ if (_maybeEat(TokenKind.LBRACE)) {
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) break;
+ rulesets.add(ruleset);
+ }
+
+ if (!_maybeEat(TokenKind.RBRACE)) {
+ _error('expected } after ruleset for @host', _peekToken.span);
+ }
+ } else {
+ _error('expected { after host before ruleset', _peekToken.span);
+ }
+ return new HostDirective(rulesets, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_PAGE:
+ /*
+ * @page S* IDENT? pseudo_page?
+ * S* '{' S*
+ * [ declaration | margin ]?
+ * [ ';' S* [ declaration | margin ]? ]* '}' S*
+ *
+ * pseudo_page :
+ * ':' [ "left" | "right" | "first" ]
+ *
+ * margin :
+ * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ *
+ * margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
+ *
+ * See http://www.w3.org/TR/css3-page/#CSS21
+ */
+ _next();
+
+ // Page name
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ // Any pseudo page?
+ var pseudoPage;
+ if (_maybeEat(TokenKind.COLON)) {
+ if (_peekIdentifier()) {
+ pseudoPage = identifier();
+ // TODO(terry): Normalize pseudoPage to lowercase.
+ if (isChecked &&
+ !(pseudoPage.name == 'left' ||
+ pseudoPage.name == 'right' ||
+ pseudoPage.name == 'first')) {
+ _warning("Pseudo page must be left, top or first",
+ pseudoPage.span);
+ return;
+ }
+ }
+ }
+
+ String pseudoName = pseudoPage is Identifier ? pseudoPage.name : '';
+ String ident = name is Identifier ? name.name : '';
+ return new PageDirective(ident, pseudoName,
+ processMarginsDeclarations(), _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_CHARSET:
+ // @charset S* STRING S* ';'
+ _next();
+
+ var charEncoding = processQuotedString(false);
+ if (isChecked && charEncoding == null) {
+ // Missing character encoding.
+ _warning('missing character encoding string', _makeSpan(start));
+ }
+
+ return new CharsetDirective(charEncoding, _makeSpan(start));
+
+ // TODO(terry): Workaround Dart2js bug continue not implemented in switch
+ // see https://code.google.com/p/dart/issues/detail?id=8270
+ /*
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ // TODO(terry): For now only IE 10 (are base level) supports @keyframes,
+ // -moz- has only been optional since Oct 2012 release of Firefox, not
+ // all versions of webkit support @keyframes and opera doesn't yet
+ // support w/o -o- prefix. Add more warnings for other prefixes when
+ // they become optional.
+ if (isChecked) {
+ _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
+ }
+ continue keyframeDirective;
+
+ keyframeDirective:
+ */
+ case TokenKind.DIRECTIVE_KEYFRAMES:
+ case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES:
+ case TokenKind.DIRECTIVE_MOZ_KEYFRAMES:
+ case TokenKind.DIRECTIVE_O_KEYFRAMES:
+ // TODO(terry): Remove workaround when bug 8270 is fixed.
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ if (tokId == TokenKind.DIRECTIVE_MS_KEYFRAMES && isChecked) {
+ _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
+ }
+ // TODO(terry): End of workaround.
+
+ /* Key frames grammar:
+ *
+ * @[browser]? keyframes [IDENT|STRING] '{' keyframes-blocks '}';
+ *
+ * browser: [-webkit-, -moz-, -ms-, -o-]
+ *
+ * keyframes-blocks:
+ * [keyframe-selectors '{' declarations '}']* ;
+ *
+ * keyframe-selectors:
+ * ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ;
+ */
+ _next();
+
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.LBRACE);
+
+ var keyframe = new KeyFrameDirective(tokId, name, _makeSpan(start));
+
+ do {
+ Expressions selectors = new Expressions(_makeSpan(start));
+
+ do {
+ var term = processTerm();
+
+ // TODO(terry): Only allow from, to and PERCENTAGE ...
+
+ selectors.add(term);
+ } while (_maybeEat(TokenKind.COMMA));
+
+ keyframe.add(new KeyFrameBlock(selectors, processDeclarations(),
+ _makeSpan(start)));
+
+ } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());
+
+ return keyframe;
+
+ case TokenKind.DIRECTIVE_FONTFACE:
+ _next();
+ return new FontFaceDirective(processDeclarations(), _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_STYLET:
+ /* Stylet grammar:
+ *
+ * @stylet IDENT '{'
+ * ruleset
+ * '}'
+ */
+ _next();
+
+ var name;
+ if (_peekIdentifier()) {
+ name = identifier();
+ }
+
+ _eat(TokenKind.LBRACE);
+
+ List<TreeNode> productions = [];
+
+ start = _peekToken.start;
+ while (!_maybeEat(TokenKind.END_OF_FILE)) {
+ RuleSet ruleset = processRuleSet();
+ if (ruleset == null) {
+ break;
+ }
+ productions.add(ruleset);
+ }
+
+ _eat(TokenKind.RBRACE);
+
+ return new StyletDirective(name, productions, _makeSpan(start));
+
+ case TokenKind.DIRECTIVE_NAMESPACE:
+ /* Namespace grammar:
+ *
+ * @namespace S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
+ * namespace_prefix : IDENT
+ *
+ */
+ _next();
+
+ var prefix;
+ if (_peekIdentifier()) {
+ prefix = identifier();
+ }
+
+ // The namespace URI can be either a quoted string url("uri_string")
+ // are identical.
+ String namespaceUri;
+ if (_peekIdentifier()) {
+ var func = processFunction(identifier());
+ if (func is UriTerm) {
+ namespaceUri = func.text;
+ }
+ } else {
+ if (prefix != null && prefix.name == 'url') {
+ var func = processFunction(prefix);
+ if (func is UriTerm) {
+ // @namespace url("");
+ namespaceUri = func.text;
+ prefix = null;
+ }
+ } else {
+ namespaceUri = processQuotedString(false);
+ }
+ }
+
+ return new NamespaceDirective(prefix != null ? prefix.name : '',
+ namespaceUri, _makeSpan(start));
+ }
+ }
+
+ RuleSet processRuleSet([SelectorGroup selectorGroup]) {
+ if (selectorGroup == null) {
+ selectorGroup = processSelectorGroup();
+ }
+ if (selectorGroup != null) {
+ return new RuleSet(selectorGroup, processDeclarations(),
+ selectorGroup.span);
+ }
+ }
+
+ /**
+ * Look ahead to see if what should be a declaration is really a selector.
+ * If it's a selector than it's a nested selector. This support's Less'
+ * nested selector syntax (requires a look ahead). E.g.,
+ *
+ * div {
+ * width : 20px;
+ * span {
+ * color: red;
+ * }
+ * }
+ *
+ * Two tag name selectors div and span equivalent to:
+ *
+ * div {
+ * width: 20px;
+ * }
+ * div span {
+ * color: red;
+ * }
+ *
+ * Return [null] if no selector or [SelectorGroup] if a selector was parsed.
+ */
+ SelectorGroup _nestedSelector() {
+ Messages oldMessages = messages;
+ _createMessages();
+
+ var markedData = _mark;
+
+ // Look a head do we have a nested selector instead of a declaration?
+ SelectorGroup selGroup = processSelectorGroup();
+
+ var nestedSelector = selGroup != null && _peekKind(TokenKind.LBRACE) &&
+ messages.messages.isEmpty;
+
+ 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;
+ }
+ }
+
+ processDeclarations({bool checkBrace: true}) {
+ int start = _peekToken.start;
+
+ if (checkBrace) _eat(TokenKind.LBRACE);
+
+ List decls = [];
+ List dartStyles = []; // List of latest styles exposed to Dart.
+
+ do {
+ var selectorGroup = _nestedSelector();
+ while (selectorGroup != null) {
+ // Nested selector so process as a ruleset.
+ var ruleset = processRuleSet(selectorGroup);
+ decls.add(ruleset);
+ selectorGroup = _nestedSelector();
+ }
+
+ Declaration decl = processDeclaration(dartStyles);
+ if (decl != null) {
+ if (decl.hasDartStyle) {
+ var newDartStyle = decl.dartStyle;
+
+ // Replace or add latest Dart style.
+ bool replaced = false;
+ for (var i = 0; i < dartStyles.length; i++) {
+ var dartStyle = dartStyles[i];
+ if (dartStyle.isSame(newDartStyle)) {
+ dartStyles[i] = newDartStyle;
+ replaced = true;
+ break;
+ }
+ }
+ if (!replaced) {
+ dartStyles.add(newDartStyle);
+ }
+ }
+ decls.add(decl);
+ }
+ } while (_maybeEat(TokenKind.SEMICOLON));
+
+ if (checkBrace) _eat(TokenKind.RBRACE);
+
+ // Fixup declaration to only have dartStyle that are live for this set of
+ // declarations.
+ for (var decl in decls) {
+ if (decl is Declaration) {
+ if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) {
+ // Dart style not live, ignore these styles in this Declarations.
+ decl.dartStyle = null;
+ }
+ }
+ }
+
+ return new DeclarationGroup(decls, _makeSpan(start));
+ }
+
+ List<DeclarationGroup> processMarginsDeclarations() {
+ List groups = [];
+
+ int start = _peekToken.start;
+
+ _eat(TokenKind.LBRACE);
+
+ List<Declaration> decls = [];
+ List dartStyles = []; // List of latest styles exposed to Dart.
+
+ do {
+ switch (_peek()) {
+ case TokenKind.MARGIN_DIRECTIVE_TOPLEFTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_TOPLEFT:
+ case TokenKind.MARGIN_DIRECTIVE_TOPCENTER:
+ case TokenKind.MARGIN_DIRECTIVE_TOPRIGHT:
+ case TokenKind.MARGIN_DIRECTIVE_TOPRIGHTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFT:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMCENTER:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHT:
+ case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTTOP:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTMIDDLE:
+ case TokenKind.MARGIN_DIRECTIVE_LEFTBOTTOM:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTTOP:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTMIDDLE:
+ case TokenKind.MARGIN_DIRECTIVE_RIGHTBOTTOM:
+ // Margin syms processed.
+ // margin :
+ // margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
+ //
+ // margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
+ var marginSym = _peek();
+
+ _next();
+
+ var declGroup = processDeclarations();
+ if (declGroup != null) {
+ groups.add(new MarginGroup(marginSym, declGroup.declarations,
+ _makeSpan(start)));
+ }
+ break;
+ default:
+ Declaration decl = processDeclaration(dartStyles);
+ if (decl != null) {
+ if (decl.hasDartStyle) {
+ var newDartStyle = decl.dartStyle;
+
+ // Replace or add latest Dart style.
+ bool replaced = false;
+ for (var i = 0; i < dartStyles.length; i++) {
+ var dartStyle = dartStyles[i];
+ if (dartStyle.isSame(newDartStyle)) {
+ dartStyles[i] = newDartStyle;
+ replaced = true;
+ break;
+ }
+ }
+ if (!replaced) {
+ dartStyles.add(newDartStyle);
+ }
+ }
+ decls.add(decl);
+ }
+ _maybeEat(TokenKind.SEMICOLON);
+ break;
+ }
+ } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());
+
+ // Fixup declaration to only have dartStyle that are live for this set of
+ // declarations.
+ for (var decl in decls) {
+ if (decl.hasDartStyle && dartStyles.indexOf(decl.dartStyle) < 0) {
+ // Dart style not live, ignore these styles in this Declarations.
+ decl.dartStyle = null;
+ }
+ }
+
+ if (decls.length > 0) {
+ groups.add(new DeclarationGroup(decls, _makeSpan(start)));
+ }
+
+ return groups;
+ }
+
+ SelectorGroup processSelectorGroup() {
+ List<Selector> selectors = [];
+ int start = _peekToken.start;
+ do {
+ Selector selector = processSelector();
+ if (selector != null) {
+ selectors.add(selector);
+ }
+ } while (_maybeEat(TokenKind.COMMA));
+
+ if (selectors.length > 0) {
+ return new SelectorGroup(selectors, _makeSpan(start));
+ }
+ }
+
+ /**
+ * Return list of selectors
+ */
+ processSelector() {
+ List<SimpleSelectorSequence> simpleSequences = [];
+ int start = _peekToken.start;
+ while (true) {
+ // First item is never descendant make sure it's COMBINATOR_NONE.
+ var selectorItem = simpleSelectorSequence(simpleSequences.length == 0);
+ if (selectorItem != null) {
+ simpleSequences.add(selectorItem);
+ } else {
+ break;
+ }
+ }
+
+ if (simpleSequences.length > 0) {
+ return new Selector(simpleSequences, _makeSpan(start));
+ }
+ }
+
+ simpleSelectorSequence(bool forceCombinatorNone) {
+ var start = _peekToken.start;
+ var combinatorType = TokenKind.COMBINATOR_NONE;
+ var thisOperator = false;
+
+ switch (_peek()) {
+ case TokenKind.PLUS:
+ _eat(TokenKind.PLUS);
+ combinatorType = TokenKind.COMBINATOR_PLUS;
+ break;
+ case TokenKind.GREATER:
+ _eat(TokenKind.GREATER);
+ combinatorType = TokenKind.COMBINATOR_GREATER;
+ break;
+ case TokenKind.TILDE:
+ _eat(TokenKind.TILDE);
+ combinatorType = TokenKind.COMBINATOR_TILDE;
+ break;
+ case TokenKind.AMPERSAND:
+ _eat(TokenKind.AMPERSAND);
+ thisOperator = true;
+ break;
+ }
+
+ // Check if WHITESPACE existed between tokens if so we're descendent.
+ if (combinatorType == TokenKind.COMBINATOR_NONE && !forceCombinatorNone) {
+ if (this._previousToken != null &&
+ this._previousToken.end != this._peekToken.start) {
+ combinatorType = TokenKind.COMBINATOR_DESCENDANT;
+ }
+ }
+
+ var span = _makeSpan(start);
+ var simpleSel = thisOperator ?
+ new ElementSelector(new ThisOperator(span), span) : simpleSelector();
+ if (simpleSel == null &&
+ (combinatorType == TokenKind.COMBINATOR_PLUS ||
+ combinatorType == TokenKind.COMBINATOR_GREATER ||
+ combinatorType == TokenKind.COMBINATOR_TILDE)) {
+ // For "+ &", "~ &" or "> &" a selector sequence with no name is needed
+ // so that the & will have a combinator too. This is needed to
+ // disambiguate selector expressions:
+ // .foo&:hover combinator before & is NONE
+ // .foo & combinator before & is DESCDENDANT
+ // .foo > & combinator before & is GREATER
+ simpleSel = new ElementSelector(new Identifier("", span), span);
+ }
+ if (simpleSel != null) {
+ return new SimpleSelectorSequence(simpleSel, span, combinatorType);
+ }
+ }
+
+ /**
+ * Simple selector grammar:
+ *
+ * simple_selector_sequence
+ * : [ type_selector | universal ]
+ * [ HASH | class | attrib | pseudo | negation ]*
+ * | [ HASH | class | attrib | pseudo | negation ]+
+ * type_selector
+ * : [ namespace_prefix ]? element_name
+ * namespace_prefix
+ * : [ IDENT | '*' ]? '|'
+ * element_name
+ * : IDENT
+ * universal
+ * : [ namespace_prefix ]? '*'
+ * class
+ * : '.' IDENT
+ */
+ simpleSelector() {
+ // TODO(terry): Nathan makes a good point parsing of namespace and element
+ // are essentially the same (asterisk or identifier) other
+ // than the error message for element. Should consolidate the
+ // code.
+ // TODO(terry): Need to handle attribute namespace too.
+ var first;
+ int start = _peekToken.start;
+ switch (_peek()) {
+ case TokenKind.ASTERISK:
+ // Mark as universal namespace.
+ var tok = _next();
+ first = new Wildcard(_makeSpan(tok.start));
+ break;
+ case TokenKind.IDENTIFIER:
+ first = identifier();
+ break;
+ default:
+ // Expecting simple selector.
+ // TODO(terry): Could be a synthesized token like value, etc.
+ if (TokenKind.isKindIdentifier(_peek())) {
+ first = identifier();
+ } else if (_peekKind(TokenKind.SEMICOLON)) {
+ // Can't be a selector if we found a semi-colon.
+ return null;
+ }
+ break;
+ }
+
+ if (_maybeEat(TokenKind.NAMESPACE)) {
+ var element;
+ switch (_peek()) {
+ case TokenKind.ASTERISK:
+ // Mark as universal element
+ var tok = _next();
+ element = new Wildcard(_makeSpan(tok.start));
+ break;
+ case TokenKind.IDENTIFIER:
+ element = identifier();
+ break;
+ default:
+ _error('expected element name or universal(*), but found $_peekToken',
+ _peekToken.span);
+ break;
+ }
+
+ return new NamespaceSelector(first,
+ new ElementSelector(element, element.span), _makeSpan(start));
+ } else if (first != null) {
+ return new ElementSelector(first, _makeSpan(start));
+ } else {
+ // Check for HASH | class | attrib | pseudo | negation
+ return simpleSelectorTail();
+ }
+ }
+
+ bool _anyWhiteSpaceBeforePeekToken(int kind) {
+ if (_previousToken != null && _peekToken != null &&
+ _previousToken.kind == kind) {
+ // If end of previous token isn't same as the start of peek token then
+ // there's something between these tokens probably whitespace.
+ return _previousToken.end != _peekToken.start;
+ }
+
+ return false;
+ }
+
+ /**
+ * type_selector | universal | HASH | class | attrib | pseudo
+ */
+ simpleSelectorTail() {
+ // Check for HASH | class | attrib | pseudo | negation
+ int start = _peekToken.start;
+ switch (_peek()) {
+ case TokenKind.HASH:
+ _eat(TokenKind.HASH);
+
+ bool hasWhiteSpace = false;
+ if (_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
+ _warning("Not a valid ID selector expected #id", _makeSpan(start));
+ hasWhiteSpace = true;
+ }
+ if (_peekIdentifier()) {
+ var id = identifier();
+ if (hasWhiteSpace) {
+ // Generate bad selector id (normalized).
+ id.name = " ${id.name}";
+ }
+ return new IdSelector(id, _makeSpan(start));
+ }
+ return null;
+ case TokenKind.DOT:
+ _eat(TokenKind.DOT);
+
+ bool hasWhiteSpace = false;
+ if (_anyWhiteSpaceBeforePeekToken(TokenKind.DOT)) {
+ _warning("Not a valid class selector expected .className",
+ _makeSpan(start));
+ hasWhiteSpace = true;
+ }
+ var id = identifier();
+ if (hasWhiteSpace) {
+ // Generate bad selector class (normalized).
+ id.name = " ${id.name}";
+ }
+ return new ClassSelector(id, _makeSpan(start));
+ case TokenKind.COLON:
+ // :pseudo-class ::pseudo-element
+ // TODO(terry): '::' should be token.
+ _eat(TokenKind.COLON);
+ bool pseudoElement = _maybeEat(TokenKind.COLON);
+
+ // TODO(terry): If no identifier specified consider optimizing out the
+ // : or :: and making this a normal selector. For now,
+ // create an empty pseudoName.
+ var pseudoName;
+ if (_peekIdentifier()) {
+ pseudoName = identifier();
+ } else {
+ return null;
+ }
+
+ // Functional pseudo?
+ if (_maybeEat(TokenKind.LPAREN)) {
+ if (!pseudoElement && pseudoName.name.toLowerCase() == 'not') {
+ // Negation : ':NOT(' S* negation_arg S* ')'
+ var negArg = simpleSelector();
+
+ _eat(TokenKind.RPAREN);
+ return new NegationSelector(negArg, _makeSpan(start));
+ } else {
+ // Handle function expression.
+ var span = _makeSpan(start);
+ var expr = processSelectorExpression();
+
+ // Used during selector look-a-head if not a SelectorExpression is
+ // bad.
+ if (expr is! SelectorExpression) {
+ _errorExpected("CSS expression");
+ return null;
+ }
+
+ _eat(TokenKind.RPAREN);
+ return (pseudoElement) ?
+ new PseudoElementFunctionSelector(pseudoName, expr, span) :
+ new PseudoClassFunctionSelector(pseudoName, expr, span);
+ }
+ }
+
+ // TODO(terry): Need to handle specific pseudo class/element name and
+ // backward compatible names that are : as well as :: as well as
+ // parameters. Current, spec uses :: for pseudo-element and : for
+ // pseudo-class. However, CSS2.1 allows for : to specify old
+ // pseudo-elements (:first-line, :first-letter, :before and :after) any
+ // new pseudo-elements defined would require a ::.
+ return pseudoElement ?
+ new PseudoElementSelector(pseudoName, _makeSpan(start)) :
+ new PseudoClassSelector(pseudoName, _makeSpan(start));
+ case TokenKind.LBRACK:
+ return processAttribute();
+ case TokenKind.DOUBLE:
+ _error('name must start with a alpha character, but found a number',
+ _peekToken.span);
+ _next();
+ break;
+ }
+ }
+
+ /**
+ * In CSS3, the expressions are identifiers, strings, or of the form "an+b".
+ *
+ * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
+ *
+ * num [0-9]+|[0-9]*\.[0-9]+
+ * PLUS '+'
+ * DIMENSION {num}{ident}
+ * NUMBER {num}
+ */
+ processSelectorExpression() {
+ int start = _peekToken.start;
+
+ var expression = new SelectorExpression(_makeSpan(start));
+
+ Token termToken;
+ var value;
+
+ // Special parsing for expressions in pseudo functions. Minus is used as
+ // operator not identifier.
+ tokenizer.selectorExpression = true;
+
+ bool keepParsing = true;
+ while (keepParsing) {
+ switch (_peek()) {
+ case TokenKind.PLUS:
+ start = _peekToken.start;
+ termToken = _next();
+ expression.add(new OperatorPlus(_makeSpan(start)));
+ break;
+ case TokenKind.MINUS:
+ start = _peekToken.start;
+ termToken = _next();
+ expression.add(new OperatorMinus(_makeSpan(start)));
+ break;
+ case TokenKind.INTEGER:
+ termToken = _next();
+ value = int.parse(termToken.text);
+ break;
+ case TokenKind.DOUBLE:
+ termToken = _next();
+ value = double.parse(termToken.text);
+ break;
+ case TokenKind.SINGLE_QUOTE:
+ case TokenKind.DOUBLE_QUOTE:
+ value = processQuotedString(false);
+ value = '"${_escapeString(value)}"';
+ return new LiteralTerm(value, value, _makeSpan(start));
+ case TokenKind.IDENTIFIER:
+ value = identifier(); // Snarf up the ident we'll remap, maybe.
+ break;
+ default:
+ keepParsing = false;
+ }
+
+ if (keepParsing && value != null) {
+ var unitTerm;
+ // Don't process the dimension if MINUS or PLUS is next.
+ if (_peek() != TokenKind.MINUS && _peek() != TokenKind.PLUS) {
+ unitTerm = processDimension(termToken, value, _makeSpan(start));
+ }
+ if (unitTerm == null) {
+ unitTerm = new LiteralTerm(value, value.name, _makeSpan(start));
+ }
+ expression.add(unitTerm);
+
+ value = null;
+ }
+ }
+
+ tokenizer.selectorExpression = false;
+
+ return expression;
+ }
+
+ // Attribute grammar:
+ //
+ // attributes :
+ // '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']'
+ //
+ // ATTRIB_MATCHES :
+ // [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ]
+ //
+ // INCLUDES: '~='
+ //
+ // DASHMATCH: '|='
+ //
+ // PREFIXMATCH: '^='
+ //
+ // SUFFIXMATCH: '$='
+ //
+ // SUBSTRMATCH: '*='
+ //
+ //
+ processAttribute() {
+ int start = _peekToken.start;
+
+ if (_maybeEat(TokenKind.LBRACK)) {
+ var attrName = identifier();
+
+ int op;
+ switch (_peek()) {
+ case TokenKind.EQUALS:
+ case TokenKind.INCLUDES: // ~=
+ case TokenKind.DASH_MATCH: // |=
+ case TokenKind.PREFIX_MATCH: // ^=
+ case TokenKind.SUFFIX_MATCH: // $=
+ case TokenKind.SUBSTRING_MATCH: // *=
+ op = _peek();
+ _next();
+ break;
+ default:
+ op = TokenKind.NO_MATCH;
+ }
+
+ var value;
+ if (op != TokenKind.NO_MATCH) {
+ // Operator hit so we require a value too.
+ if (_peekIdentifier()) {
+ value = identifier();
+ } else {
+ value = processQuotedString(false);
+ }
+
+ if (value == null) {
+ _error('expected attribute value string or ident', _peekToken.span);
+ }
+ }
+
+ _eat(TokenKind.RBRACK);
+
+ return new AttributeSelector(attrName, op, value, _makeSpan(start));
+ }
+ }
+
+ // Declaration grammar:
+ //
+ // declaration: property ':' expr prio?
+ //
+ // property: IDENT [or IE hacks]
+ // prio: !important
+ // expr: (see processExpr)
+ //
+ // Here are the ugly IE hacks we need to support:
+ // property: expr prio? \9; - IE8 and below property, /9 before semi-colon
+ // *IDENT - IE7 or below
+ // _IDENT - IE6 property (automatically a valid ident)
+ //
+ processDeclaration(List dartStyles) {
+ Declaration decl;
+
+ int start = _peekToken.start;
+
+ // IE7 hack of * before property name if so the property is IE7 or below.
+ var ie7 = _peekKind(TokenKind.ASTERISK);
+ if (ie7) {
+ _next();
+ }
+
+ // IDENT ':' expr '!important'?
+ if (TokenKind.isIdentifier(_peekToken.kind)) {
+ var propertyIdent = identifier();
+
+ var ieFilterProperty = propertyIdent.name.toLowerCase() == 'filter';
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr(ieFilterProperty);
+
+ var dartComposite = _styleForDart(propertyIdent, exprs, dartStyles);
+
+ // Handle !important (prio)
+ var importantPriority = _maybeEat(TokenKind.IMPORTANT);
+
+ decl = new Declaration(propertyIdent, exprs, dartComposite,
+ _makeSpan(start), important: importantPriority, ie7: ie7);
+ } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) {
+ _next();
+ var definedName;
+ if (_peekIdentifier()) definedName = identifier();
+
+ _eat(TokenKind.COLON);
+
+ Expressions exprs = processExpr();
+
+ decl = new VarDefinition(definedName, exprs, _makeSpan(start));
+ }
+
+ return decl;
+ }
+
+ /** List of styles exposed to the Dart UI framework. */
+ static const int _fontPartFont= 0;
+ static const int _fontPartVariant = 1;
+ static const int _fontPartWeight = 2;
+ static const int _fontPartSize = 3;
+ static const int _fontPartFamily = 4;
+ static const int _fontPartStyle = 5;
+ static const int _marginPartMargin = 6;
+ static const int _marginPartLeft = 7;
+ static const int _marginPartTop = 8;
+ static const int _marginPartRight = 9;
+ static const int _marginPartBottom = 10;
+ static const int _lineHeightPart = 11;
+ static const int _borderPartBorder = 12;
+ static const int _borderPartLeft = 13;
+ static const int _borderPartTop = 14;
+ static const int _borderPartRight = 15;
+ static const int _borderPartBottom = 16;
+ static const int _borderPartWidth = 17;
+ static const int _borderPartLeftWidth = 18;
+ static const int _borderPartTopWidth = 19;
+ static const int _borderPartRightWidth = 20;
+ static const int _borderPartBottomWidth = 21;
+ static const int _heightPart = 22;
+ static const int _widthPart = 23;
+ static const int _paddingPartPadding = 24;
+ static const int _paddingPartLeft = 25;
+ static const int _paddingPartTop = 26;
+ static const int _paddingPartRight = 27;
+ static const int _paddingPartBottom = 28;
+
+ static const Map<String, int> _stylesToDart = const {
+ 'font': _fontPartFont,
+ 'font-family': _fontPartFamily,
+ 'font-size': _fontPartSize,
+ 'font-style': _fontPartStyle,
+ 'font-variant': _fontPartVariant,
+ 'font-weight': _fontPartWeight,
+ 'line-height': _lineHeightPart,
+ 'margin': _marginPartMargin,
+ 'margin-left': _marginPartLeft,
+ 'margin-right': _marginPartRight,
+ 'margin-top': _marginPartTop,
+ 'margin-bottom': _marginPartBottom,
+ 'border': _borderPartBorder,
+ 'border-left': _borderPartLeft,
+ 'border-right': _borderPartRight,
+ 'border-top': _borderPartTop,
+ 'border-bottom': _borderPartBottom,
+ 'border-width': _borderPartWidth,
+ 'border-left-width': _borderPartLeftWidth,
+ 'border-top-width': _borderPartTopWidth,
+ 'border-right-width': _borderPartRightWidth,
+ 'border-bottom-width': _borderPartBottomWidth,
+ 'height': _heightPart,
+ 'width': _widthPart,
+ 'padding': _paddingPartPadding,
+ 'padding-left': _paddingPartLeft,
+ 'padding-top': _paddingPartTop,
+ 'padding-right': _paddingPartRight,
+ 'padding-bottom': _paddingPartBottom
+ };
+
+ static const Map<String, int> _nameToFontWeight = const {
+ 'bold' : FontWeight.bold,
+ 'normal' : FontWeight.normal
+ };
+
+ static _findStyle(String styleName) {
+ if (_stylesToDart.containsKey(styleName)) {
+ return _stylesToDart[styleName];
+ }
+ }
+
+ _styleForDart(Identifier property, Expressions exprs, List dartStyles) {
+ int styleType = _findStyle(property.name.toLowerCase());
+ if (styleType != null) {
+ return buildDartStyleNode(styleType, exprs, dartStyles);
+ }
+ }
+
+ FontExpression _mergeFontStyles(FontExpression fontExpr, List dartStyles) {
+ // Merge all font styles for this class selector.
+ for (var dartStyle in dartStyles) {
+ if (dartStyle.isFont) {
+ fontExpr = new FontExpression.merge(dartStyle, fontExpr);
+ }
+ }
+
+ return fontExpr;
+ }
+
+ buildDartStyleNode(int styleType, Expressions exprs, List dartStyles) {
+ switch (styleType) {
+ /*
+ * Properties in order:
+ *
+ * font-style font-variant font-weight font-size/line-height font-family
+ *
+ * The font-size and font-family values are required. If other values are
+ * missing; a default, if it exist, will be used.
+ */
+ case _fontPartFont:
+ var processor = new ExpressionsProcessor(exprs);
+ return _mergeFontStyles(processor.processFont(), dartStyles);
+ case _fontPartFamily:
+ var processor = new ExpressionsProcessor(exprs);
+
+ try {
+ return _mergeFontStyles(processor.processFontFamily(), dartStyles);
+ } catch (fontException) {
+ _error(fontException, _peekToken.span);
+ }
+ break;
+ case _fontPartSize:
+ var processor = new ExpressionsProcessor(exprs);
+ return _mergeFontStyles(processor.processFontSize(), dartStyles);
+ case _fontPartStyle:
+ /* Possible style values:
+ * normal [default]
+ * italic
+ * oblique
+ * inherit
+ */
+ // TODO(terry): TBD
+ break;
+ case _fontPartVariant:
+ /* Possible variant values:
+ * normal [default]
+ * small-caps
+ * inherit
+ */
+ // TODO(terry): TBD
+ break;
+ case _fontPartWeight:
+ /* Possible weight values:
+ * normal [default]
+ * bold
+ * bolder
+ * lighter
+ * 100 - 900
+ * inherit
+ */
+ // TODO(terry): Only 'normal', 'bold', or values of 100-900 supoorted
+ // need to handle bolder, lighter, and inherit. See
+ // https://github.com/dart-lang/csslib/issues/1
+ var expr = exprs.expressions[0];
+ if (expr is NumberTerm) {
+ var fontExpr = new FontExpression(expr.span,
+ weight: expr.value);
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (expr is LiteralTerm) {
+ int weight = _nameToFontWeight[expr.value.toString()];
+ if (weight != null) {
+ var fontExpr = new FontExpression(expr.span, weight: weight);
+ return _mergeFontStyles(fontExpr, dartStyles);
+ }
+ }
+ break;
+ case _lineHeightPart:
+ num lineHeight;
+ if (exprs.expressions.length == 1) {
+ var expr = exprs.expressions[0];
+ if (expr is UnitTerm) {
+ UnitTerm unitTerm = expr;
+ // TODO(terry): Need to handle other units and LiteralTerm normal
+ // See https://github.com/dart-lang/csslib/issues/2.
+ if (unitTerm.unit == TokenKind.UNIT_LENGTH_PX ||
+ unitTerm.unit == TokenKind.UNIT_LENGTH_PT) {
+ var fontExpr = new FontExpression(expr.span,
+ lineHeight: new LineHeight(expr.value, inPixels: true));
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (isChecked) {
+ _warning("Unexpected unit for line-height", expr.span);
+ }
+ } else if (expr is NumberTerm) {
+ var fontExpr = new FontExpression(expr.span,
+ lineHeight: new LineHeight(expr.value, inPixels: false));
+ return _mergeFontStyles(fontExpr, dartStyles);
+ } else if (isChecked) {
+ _warning("Unexpected value for line-height", expr.span);
+ }
+ }
+ break;
+ case _marginPartMargin:
+ return new MarginExpression.boxEdge(exprs.span, processFourNums(exprs));
+ case _borderPartBorder:
+ for (var expr in exprs.expressions) {
+ var v = marginValue(expr);
+ if (v != null) {
+ final box = new BoxEdge.uniform(v);
+ return new BorderExpression.boxEdge(exprs.span, box);
+ }
+ }
+ break;
+ case _borderPartWidth:
+ var v = marginValue(exprs.expressions[0]);
+ if (v != null) {
+ final box = new BoxEdge.uniform(v);
+ return new BorderExpression.boxEdge(exprs.span, box);
+ }
+ break;
+ case _paddingPartPadding:
+ return new PaddingExpression.boxEdge(exprs.span,
+ processFourNums(exprs));
+ case _marginPartLeft:
+ case _marginPartTop:
+ case _marginPartRight:
+ case _marginPartBottom:
+ case _borderPartLeft:
+ case _borderPartTop:
+ case _borderPartRight:
+ case _borderPartBottom:
+ case _borderPartLeftWidth:
+ case _borderPartTopWidth:
+ case _borderPartRightWidth:
+ case _borderPartBottomWidth:
+ case _heightPart:
+ case _widthPart:
+ case _paddingPartLeft:
+ case _paddingPartTop:
+ case _paddingPartRight:
+ case _paddingPartBottom:
+ if (exprs.expressions.length > 0) {
+ return processOneNumber(exprs, styleType);
+ }
+ break;
+ default:
+ // Don't handle it.
+ return;
+ }
+ }
+
+ // TODO(terry): Look at handling width of thin, thick, etc. any none numbers
+ // to convert to a number.
+ processOneNumber(Expressions exprs, int part) {
+ var value = marginValue(exprs.expressions[0]);
+ if (value != null) {
+ switch (part) {
+ case _marginPartLeft:
+ return new MarginExpression(exprs.span, left: value);
+ case _marginPartTop:
+ return new MarginExpression(exprs.span, top: value);
+ case _marginPartRight:
+ return new MarginExpression(exprs.span, right: value);
+ case _marginPartBottom:
+ return new MarginExpression(exprs.span, bottom: value);
+ case _borderPartLeft:
+ case _borderPartLeftWidth:
+ return new BorderExpression(exprs.span, left: value);
+ case _borderPartTop:
+ case _borderPartTopWidth:
+ return new BorderExpression(exprs.span, top: value);
+ case _borderPartRight:
+ case _borderPartRightWidth:
+ return new BorderExpression(exprs.span, right: value);
+ case _borderPartBottom:
+ case _borderPartBottomWidth:
+ return new BorderExpression(exprs.span, bottom: value);
+ case _heightPart:
+ return new HeightExpression(exprs.span, value);
+ case _widthPart:
+ return new WidthExpression(exprs.span, value);
+ case _paddingPartLeft:
+ return new PaddingExpression(exprs.span, left: value);
+ case _paddingPartTop:
+ return new PaddingExpression(exprs.span, top: value);
+ case _paddingPartRight:
+ return new PaddingExpression(exprs.span, right: value);
+ case _paddingPartBottom:
+ return new PaddingExpression(exprs.span, bottom: value);
+ }
+ }
+ }
+
+ /**
+ * Margins are of the format:
+ *
+ * top,right,bottom,left (4 parameters)
+ * top,right/left, bottom (3 parameters)
+ * top/bottom,right/left (2 parameters)
+ * top/right/bottom/left (1 parameter)
+ *
+ * The values of the margins can be a unit or unitless or auto.
+ */
+ processFourNums(Expressions exprs) {
+ num top;
+ num right;
+ num bottom;
+ num left;
+
+ int totalExprs = exprs.expressions.length;
+ switch (totalExprs) {
+ case 1:
+ top = marginValue(exprs.expressions[0]);
+ right = top;
+ bottom = top;
+ left = top;
+ break;
+ case 2:
+ top = marginValue(exprs.expressions[0]);
+ bottom = top;
+ right = marginValue(exprs.expressions[1]);
+ left = right;
+ break;
+ case 3:
+ top = marginValue(exprs.expressions[0]);
+ right = marginValue(exprs.expressions[1]);
+ left = right;
+ bottom = marginValue(exprs.expressions[2]);
+ break;
+ case 4:
+ top = marginValue(exprs.expressions[0]);
+ right = marginValue(exprs.expressions[1]);
+ bottom = marginValue(exprs.expressions[2]);
+ left = marginValue(exprs.expressions[3]);
+ break;
+ default:
+ return;
+ }
+
+ return new BoxEdge.clockwiseFromTop(top, right, bottom, left);
+ }
+
+ // TODO(terry): Need to handle auto.
+ marginValue(var exprTerm) {
+ if (exprTerm is UnitTerm || exprTerm is NumberTerm) {
+ return exprTerm.value;
+ }
+ }
+
+ // Expression grammar:
+ //
+ // expression: term [ operator? term]*
+ //
+ // operator: '/' | ','
+ // term: (see processTerm)
+ //
+ processExpr([bool ieFilter = false]) {
+ int start = _peekToken.start;
+ Expressions expressions = new Expressions(_makeSpan(start));
+
+ bool keepGoing = true;
+ var expr;
+ while (keepGoing && (expr = processTerm(ieFilter)) != null) {
+ var op;
+
+ int opStart = _peekToken.start;
+
+ switch (_peek()) {
+ case TokenKind.SLASH:
+ op = new OperatorSlash(_makeSpan(opStart));
+ break;
+ case TokenKind.COMMA:
+ op = new OperatorComma(_makeSpan(opStart));
+ break;
+ case TokenKind.BACKSLASH:
+ // Backslash outside of string; detected IE8 or older signaled by \9 at
+ // end of an expression.
+ var ie8Start = _peekToken.start;
+
+ _next();
+ if (_peekKind(TokenKind.INTEGER)) {
+ var numToken = _next();
+ var value = int.parse(numToken.text);
+ if (value == 9) {
+ op = new IE8Term(_makeSpan(ie8Start));
+ } else if (isChecked) {
+ _warning("\$value is not valid in an expression", _makeSpan(start));
+ }
+ }
+ break;
+ }
+
+ if (expr != null) {
+ expressions.add(expr);
+ } else {
+ keepGoing = false;
+ }
+
+ if (op != null) {
+ expressions.add(op);
+ if (op is IE8Term) {
+ keepGoing = false;
+ } else {
+ _next();
+ }
+ }
+ }
+
+ return expressions;
+ }
+
+ static int MAX_UNICODE = int.parse('0x10FFFF');
+
+ // Term grammar:
+ //
+ // term:
+ // unary_operator?
+ // [ term_value ]
+ // | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
+ //
+ // term_value:
+ // NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
+ // TIME S* | FREQ S* | function
+ //
+ // NUMBER: {num}
+ // PERCENTAGE: {num}%
+ // LENGTH: {num}['px' | 'cm' | 'mm' | 'in' | 'pt' | 'pc']
+ // EMS: {num}'em'
+ // EXS: {num}'ex'
+ // ANGLE: {num}['deg' | 'rad' | 'grad']
+ // TIME: {num}['ms' | 's']
+ // FREQ: {num}['hz' | 'khz']
+ // function: IDENT '(' expr ')'
+ //
+ processTerm([bool ieFilter = false]) {
+ int start = _peekToken.start;
+ Token t; // token for term's value
+ var value; // value of term (numeric values)
+
+ var unary = "";
+ switch (_peek()) {
+ case TokenKind.HASH:
+ this._eat(TokenKind.HASH);
+ if (!_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
+ String hexText;
+ if (_peekKind(TokenKind.INTEGER)) {
+ String hexText1 = _peekToken.text;
+ _next();
+ if (_peekIdentifier()) {
+ hexText = '$hexText1${identifier().name}';
+ } else {
+ hexText = hexText1;
+ }
+ } else if (_peekIdentifier()) {
+ hexText = identifier().name;
+ }
+ if (hexText != null) {
+ return _parseHex(hexText, _makeSpan(start));
+ }
+ }
+
+ if (isChecked) {
+ _warning("Expected hex number", _makeSpan(start));
+ }
+ // Construct the bad hex value with a #<space>number.
+ return _parseHex(" ${processTerm().text}", _makeSpan(start));
+ case TokenKind.INTEGER:
+ t = _next();
+ value = int.parse("${unary}${t.text}");
+ break;
+ case TokenKind.DOUBLE:
+ t = _next();
+ value = double.parse("${unary}${t.text}");
+ break;
+ case TokenKind.SINGLE_QUOTE:
+ case TokenKind.DOUBLE_QUOTE:
+ value = processQuotedString(false);
+ value = '"${_escapeString(value)}"';
+ return new LiteralTerm(value, value, _makeSpan(start));
+ case TokenKind.LPAREN:
+ _next();
+
+ GroupTerm group = new GroupTerm(_makeSpan(start));
+
+ var term;
+ do {
+ term = processTerm();
+ if (term != null && term is LiteralTerm) {
+ group.add(term);
+ }
+ } while (term != null && !_maybeEat(TokenKind.RPAREN) &&
+ !isPrematureEndOfFile());
+
+ return group;
+ case TokenKind.LBRACK:
+ _next();
+
+ var term = processTerm();
+ if (!(term is NumberTerm)) {
+ _error('Expecting a positive number', _makeSpan(start));
+ }
+
+ _eat(TokenKind.RBRACK);
+
+ return new ItemTerm(term.value, term.text, _makeSpan(start));
+ case TokenKind.IDENTIFIER:
+ var nameValue = identifier(); // Snarf up the ident we'll remap, maybe.
+
+ if (!ieFilter && _maybeEat(TokenKind.LPAREN)) {
+ // FUNCTION
+ return processFunction(nameValue);
+ } if (ieFilter) {
+ if (_maybeEat(TokenKind.COLON) &&
+ nameValue.name.toLowerCase() == 'progid') {
+ // IE filter:progid:
+ return processIEFilter(start);
+ } else {
+ // Handle filter:<name> where name is any filter e.g., alpha, chroma,
+ // Wave, blur, etc.
+ return processIEFilter(start);
+ }
+ }
+
+ // TODO(terry): Need to have a list of known identifiers today only
+ // 'from' is special.
+ if (nameValue.name == 'from') {
+ return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
+ }
+
+ // What kind of identifier is it, named color?
+ var colorEntry = TokenKind.matchColorName(nameValue.name);
+ if (colorEntry == null) {
+ if (isChecked) {
+ var propName = nameValue.name;
+ var errMsg = TokenKind.isPredefinedName(propName) ?
+ "Improper use of property value ${propName}" :
+ "Unknown property value ${propName}";
+ _warning(errMsg, _makeSpan(start));
+ }
+ return new LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
+ }
+
+ // Yes, process the color as an RGB value.
+ String rgbColor = TokenKind.decimalToHex(
+ TokenKind.colorValue(colorEntry), 6);
+ return _parseHex(rgbColor, _makeSpan(start));
+ case TokenKind.UNICODE_RANGE:
+ var first;
+ var second;
+ var firstNumber;
+ var secondNumber;
+ _eat(TokenKind.UNICODE_RANGE, unicodeRange: true);
+ if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
+ first = _previousToken.text;
+ firstNumber = int.parse('0x$first');
+ if (firstNumber > MAX_UNICODE) {
+ _error("unicode range must be less than 10FFFF", _makeSpan(start));
+ }
+ if (_maybeEat(TokenKind.MINUS, unicodeRange: true)) {
+ if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
+ second = _previousToken.text;
+ secondNumber = int.parse('0x$second');
+ if (secondNumber > MAX_UNICODE) {
+ _error("unicode range must be less than 10FFFF",
+ _makeSpan(start));
+ }
+ if (firstNumber > secondNumber) {
+ _error("unicode first range can not be greater than last",
+ _makeSpan(start));
+ }
+ }
+ }
+ } else if (_maybeEat(TokenKind.HEX_RANGE, unicodeRange: true)) {
+ first = _previousToken.text;
+ }
+
+ return new UnicodeRangeTerm(first, second, _makeSpan(start));
+ case TokenKind.AT:
+ if (messages.options.lessSupport) {
+ _next();
+
+ var expr = processExpr();
+ if (isChecked && expr.expressions.length > 1) {
+ _error("only @name for Less syntax", _peekToken.span);
+ }
+
+ var param = expr.expressions[0];
+ return new VarUsage(param.text, [], _makeSpan(start));
+ }
+ break;
+ }
+
+ return processDimension(t, value, _makeSpan(start));
+ }
+
+ /** Process all dimension units. */
+ processDimension(Token t, var value, Span span) {
+ var term;
+ var unitType = this._peek();
+
+ switch (unitType) {
+ case TokenKind.UNIT_EM:
+ term = new EmTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_EX:
+ term = new ExTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_LENGTH_PX:
+ case TokenKind.UNIT_LENGTH_CM:
+ case TokenKind.UNIT_LENGTH_MM:
+ case TokenKind.UNIT_LENGTH_IN:
+ case TokenKind.UNIT_LENGTH_PT:
+ case TokenKind.UNIT_LENGTH_PC:
+ term = new LengthTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_ANGLE_DEG:
+ case TokenKind.UNIT_ANGLE_RAD:
+ case TokenKind.UNIT_ANGLE_GRAD:
+ case TokenKind.UNIT_ANGLE_TURN:
+ term = new AngleTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_TIME_MS:
+ case TokenKind.UNIT_TIME_S:
+ term = new TimeTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_FREQ_HZ:
+ case TokenKind.UNIT_FREQ_KHZ:
+ term = new FreqTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.PERCENT:
+ term = new PercentageTerm(value, t.text, span);
+ _next(); // Skip the %
+ break;
+ case TokenKind.UNIT_FRACTION:
+ term = new FractionTerm(value, t.text, span);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_RESOLUTION_DPI:
+ case TokenKind.UNIT_RESOLUTION_DPCM:
+ case TokenKind.UNIT_RESOLUTION_DPPX:
+ term = new ResolutionTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_CH:
+ term = new ChTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_REM:
+ term = new RemTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ case TokenKind.UNIT_VIEWPORT_VW:
+ case TokenKind.UNIT_VIEWPORT_VH:
+ case TokenKind.UNIT_VIEWPORT_VMIN:
+ case TokenKind.UNIT_VIEWPORT_VMAX:
+ term = new ViewportTerm(value, t.text, span, unitType);
+ _next(); // Skip the unit
+ break;
+ default:
+ if (value != null && t != null) {
+ term = (value is Identifier)
+ ? new LiteralTerm(value, value.name, span)
+ : new NumberTerm(value, t.text, span);
+ }
+ break;
+ }
+
+ return term;
+ }
+
+ processQuotedString([bool urlString = false]) {
+ int start = _peekToken.start;
+
+ // URI term sucks up everything inside of quotes(' or ") or between parens
+ int stopToken = urlString ? TokenKind.RPAREN : -1;
+ switch (_peek()) {
+ case TokenKind.SINGLE_QUOTE:
+ stopToken = TokenKind.SINGLE_QUOTE;
+ start = _peekToken.start + 1; // Skip the quote might have whitespace.
+ _next(); // Skip the SINGLE_QUOTE.
+ break;
+ case TokenKind.DOUBLE_QUOTE:
+ stopToken = TokenKind.DOUBLE_QUOTE;
+ start = _peekToken.start + 1; // Skip the quote might have whitespace.
+ _next(); // Skip the DOUBLE_QUOTE.
+ break;
+ default:
+ if (urlString) {
+ if (_peek() == TokenKind.LPAREN) {
+ _next(); // Skip the LPAREN.
+ start = _peekToken.start;
+ }
+ stopToken = TokenKind.RPAREN;
+ } else {
+ _error('unexpected string', _makeSpan(start));
+ }
+ break;
+ }
+
+ // Gobble up everything until we hit our stop token.
+ int runningStart = _peekToken.start;
+ while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) {
+ var tok = _next();
+ }
+
+ // All characters between quotes is the string.
+ int end = _peekToken.end;
+ var stringValue = (_peekToken.span as FileSpan).file.getText(start,
+ end - 1);
+
+ if (stopToken != TokenKind.RPAREN) {
+ _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE;
+ }
+
+ return stringValue;
+ }
+
+ // TODO(terry): Should probably understand IE's non-standard filter syntax to
+ // fully support calc, var(), etc.
+ /**
+ * IE's filter property breaks CSS value parsing. IE's format can be:
+ *
+ * filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83');
+ *
+ * We'll just parse everything after the 'progid:' look for the left paren
+ * then parse to the right paren ignoring everything in between.
+ */
+ processIEFilter(int startAfterProgidColon) {
+ int parens = 0;
+
+ while (_peek() != TokenKind.END_OF_FILE) {
+ switch (_peek()) {
+ case TokenKind.LPAREN:
+ _eat(TokenKind.LPAREN);
+ parens++;
+ break;
+ case TokenKind.RPAREN:
+ _eat(TokenKind.RPAREN);
+ if (--parens == 0) {
+ var tok = tokenizer.makeIEFilter(startAfterProgidColon,
+ _peekToken.start);
+ return new LiteralTerm(tok.text, tok.text, tok.span);
+ }
+ break;
+ default:
+ _eat(_peek());
+ }
+ }
+ }
+
+ // Function grammar:
+ //
+ // function: IDENT '(' expr ')'
+ //
+ processFunction(Identifier func) {
+ int start = _peekToken.start;
+
+ String name = func.name;
+
+ switch (name) {
+ case 'url':
+ // URI term sucks up everything inside of quotes(' or ") or between parens
+ String urlParam = processQuotedString(true);
+
+ // TODO(terry): Better error messge and checking for mismatched quotes.
+ if (_peek() == TokenKind.END_OF_FILE) {
+ _error("problem parsing URI", _peekToken.span);
+ }
+
+ if (_peek() == TokenKind.RPAREN) {
+ _next();
+ }
+
+ return new UriTerm(urlParam, _makeSpan(start));
+ case 'calc':
+ // TODO(terry): Implement expression handling...
+ break;
+ case 'var':
+ // TODO(terry): Consider handling var in IE specific filter/progid. This
+ // will require parsing entire IE specific syntax e.g.,
+ // param = value or progid:com_id, etc. for example:
+ //
+ // var-blur: Blur(Add = 0, Direction = 225, Strength = 10);
+ // var-gradient: progid:DXImageTransform.Microsoft.gradient"
+ // (GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
+ var expr = processExpr();
+ if (!_maybeEat(TokenKind.RPAREN)) {
+ _error("problem parsing var expected ), ", _peekToken.span);
+ }
+ if (isChecked &&
+ expr.expressions.where((e) => e is OperatorComma).length > 1) {
+ _error("too many parameters to var()", _peekToken.span);
+ }
+
+ var paramName = expr.expressions[0].text;
+
+ // [0] - var name, [1] - OperatorComma, [2] - default value.
+ var defaultValues = expr.expressions.length >= 3
+ ? expr.expressions.sublist(2) : [];
+ return new VarUsage(paramName, defaultValues, _makeSpan(start));
+ default:
+ var expr = processExpr();
+ if (!_maybeEat(TokenKind.RPAREN)) {
+ _error("problem parsing function expected ), ", _peekToken.span);
+ }
+
+ return new FunctionTerm(name, name, expr, _makeSpan(start));
+ }
+
+ return null;
+ }
+
+ identifier() {
+ var tok = _next();
+
+ if (!TokenKind.isIdentifier(tok.kind) &&
+ !TokenKind.isKindIdentifier(tok.kind)) {
+ if (isChecked) {
+ _warning('expected identifier, but found $tok', tok.span);
+ }
+ return new Identifier("", _makeSpan(tok.start));
+ }
+
+ return new Identifier(tok.text, _makeSpan(tok.start));
+ }
+
+ // TODO(terry): Move this to base <= 36 and into shared code.
+ static int _hexDigit(int c) {
+ if(c >= 48/*0*/ && c <= 57/*9*/) {
+ return c - 48;
+ } else if (c >= 97/*a*/ && c <= 102/*f*/) {
+ return c - 87;
+ } else if (c >= 65/*A*/ && c <= 70/*F*/) {
+ return c - 55;
+ } else {
+ return -1;
+ }
+ }
+
+ HexColorTerm _parseHex(String hexText, Span span) {
+ int hexValue = 0;
+
+ for (int i = 0; i < hexText.length; i++) {
+ var digit = _hexDigit(hexText.codeUnitAt(i));
+ if (digit < 0) {
+ _warning('Bad hex number', span);
+ return new HexColorTerm(new BAD_HEX_VALUE(), hexText, span);
+ }
+ hexValue = (hexValue << 4) + digit;
+ }
+
+ // Make 3 character hex value #RRGGBB => #RGB iff:
+ // high/low nibble of RR is the same, high/low nibble of GG is the same and
+ // high/low nibble of BB is the same.
+ if (hexText.length == 6 &&
+ hexText[0] == hexText[1] &&
+ hexText[2] == hexText[3] &&
+ hexText[4] == hexText[5]) {
+ hexText = '${hexText[0]}${hexText[2]}${hexText[4]}';
+ } else if (hexText.length == 4 &&
+ hexText[0] == hexText[1] &&
+ hexText[2] == hexText[3]) {
+ hexText = '${hexText[0]}${hexText[2]}';
+ } else if (hexText.length == 2 && hexText[0] == hexText[1]) {
+ hexText = '${hexText[0]}';
+ }
+ return new HexColorTerm(hexValue, hexText, span);
+ }
+}
+
+class ExpressionsProcessor {
+ final Expressions _exprs;
+ int _index = 0;
+
+ ExpressionsProcessor(this._exprs);
+
+ // TODO(terry): Only handles ##px unit.
+ processFontSize() {
+ /* font-size[/line-height]
+ *
+ * Possible size values:
+ * xx-small
+ * small
+ * medium [default]
+ * large
+ * x-large
+ * xx-large
+ * smaller
+ * larger
+ * ##length in px, pt, etc.
+ * ##%, percent of parent elem's font-size
+ * inherit
+ */
+ LengthTerm size;
+ LineHeight lineHt;
+ bool nextIsLineHeight = false;
+ for (; _index < _exprs.expressions.length; _index++) {
+ var expr = _exprs.expressions[_index];
+ if (size == null && expr is LengthTerm) {
+ // font-size part.
+ size = expr;
+ } else if (size != null) {
+ if (expr is OperatorSlash) {
+ // LineHeight could follow?
+ nextIsLineHeight = true;
+ } else if (nextIsLineHeight && expr is LengthTerm) {
+ assert(expr.unit == TokenKind.UNIT_LENGTH_PX);
+ lineHt = new LineHeight(expr.value, inPixels: true);
+ nextIsLineHeight = false;
+ _index++;
+ break;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return new FontExpression(_exprs.span, size: size, lineHeight: lineHt);
+ }
+
+ processFontFamily() {
+ final List<String> family = <String>[];
+
+ /* Possible family values:
+ * font-family: arial, Times new roman ,Lucida Sans Unicode,Courier;
+ * font-family: "Times New Roman", arial, Lucida Sans Unicode, Courier;
+ */
+ bool moreFamilies = false;
+
+ for (; _index < _exprs.expressions.length; _index++) {
+ Expression expr = _exprs.expressions[_index];
+ if (expr is LiteralTerm) {
+ if (family.length == 0 || moreFamilies) {
+ // It's font-family now.
+ family.add(expr.toString());
+ moreFamilies = false;
+ } else if (isChecked) {
+ messages.warning('Only font-family can be a list', _exprs.span);
+ }
+ } else if (expr is OperatorComma && family.length > 0) {
+ moreFamilies = true;
+ } else {
+ break;
+ }
+ }
+
+ return new FontExpression(_exprs.span, family: family);
+ }
+
+ processFont() {
+ var family;
+
+ // Process all parts of the font expression.
+ FontExpression fontSize;
+ FontExpression fontFamily;
+ for (; _index < _exprs.expressions.length; _index++) {
+ var expr = _exprs.expressions[_index];
+ // Order is font-size font-family
+ if (fontSize == null) {
+ fontSize = processFontSize();
+ }
+ if (fontFamily == null) {
+ fontFamily = processFontFamily();
+ }
+ //TODO(terry): Handle font-weight, font-style, and font-variant. See
+ // https://github.com/dart-lang/csslib/issues/3
+ // https://github.com/dart-lang/csslib/issues/4
+ // https://github.com/dart-lang/csslib/issues/5
+ }
+
+ return new FontExpression(_exprs.span,
+ size: fontSize.font.size,
+ lineHeight: fontSize.font.lineHeight,
+ family: fontFamily.font.family);
+ }
+}
+
+/**
+ * Escapes [text] for use in a CSS string.
+ * [single] specifies single quote `'` vs double quote `"`.
+ */
+String _escapeString(String text, {bool single: false}) {
+ StringBuffer result = null;
+
+ for (int i = 0; i < text.length; i++) {
+ int code = text.codeUnitAt(i);
+ var replace = null;
+ switch (code) {
+ case 34/*'"'*/: if (!single) replace = r'\"'; break;
+ case 39/*"'"*/: if (single) replace = r"\'"; break;
+ }
+
+ if (replace != null && result == null) {
+ result = new StringBuffer(text.substring(0, i));
+ }
+
+ if (result != null) result.write(replace != null ? replace : text[i]);
+ }
+
+ return result == null ? text : result.toString();
+}
diff --git a/lib/src/analyzer.dart b/lib/src/analyzer.dart
new file mode 100644
index 0000000..9e366a5
--- /dev/null
+++ b/lib/src/analyzer.dart
@@ -0,0 +1,513 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.parser;
+
+
+// TODO(terry): Detect invalid directive usage. All @imports must occur before
+// all rules other than @charset directive. Any @import directive
+// after any non @charset or @import directive are ignored. e.g.,
+// @import "a.css";
+// div { color: red; }
+// @import "b.css";
+// becomes:
+// @import "a.css";
+// div { color: red; }
+// <http://www.w3.org/TR/css3-syntax/#at-rules>
+
+/**
+ * Analysis phase will validate/fixup any new CSS feature or any SASS style
+ * feature.
+ */
+class Analyzer {
+ final List<StyleSheet> _styleSheets;
+ final Messages _messages;
+ VarDefinitions varDefs;
+
+ Analyzer(this._styleSheets, this._messages);
+
+ void run() {
+ varDefs = new VarDefinitions(_styleSheets);
+
+ // Any cycles?
+ var cycles = findAllCycles();
+ for (var cycle in cycles) {
+ _messages.warning("var cycle detected var-${cycle.definedName}",
+ cycle.span);
+ // TODO(terry): What if no var definition for a var usage an error?
+ // TODO(terry): Ensure a var definition imported from a different style
+ // sheet works.
+ }
+
+ // Remove any var definition from the stylesheet that has a cycle.
+ _styleSheets.forEach((styleSheet) =>
+ new RemoveVarDefinitions(cycles).visitStyleSheet(styleSheet));
+
+ // Expand any nested selectors using selector desendant combinator to
+ // signal CSS inheritance notation.
+ _styleSheets.forEach((styleSheet) => new ExpandNestedSelectors()
+ ..visitStyleSheet(styleSheet)
+ ..flatten(styleSheet));
+ }
+
+ List<VarDefinition> findAllCycles() {
+ var cycles = [];
+
+ varDefs.map.values.forEach((value) {
+ if (hasCycle(value.property)) cycles.add(value);
+ });
+
+ // Update our local list of known varDefs remove any varDefs with a cycle.
+ // So the same varDef cycle isn't reported for each style sheet processed.
+ for (var cycle in cycles) {
+ varDefs.map.remove(cycle.property);
+ }
+
+ return cycles;
+ }
+
+ Iterable<VarUsage> variablesOf(Expressions exprs) =>
+ exprs.expressions.where((e) => e is VarUsage);
+
+ bool hasCycle(String varName, {Set<String> visiting, Set<String> visited}) {
+ if (visiting == null) visiting = new Set();
+ if (visited == null) visited = new Set();
+ if (visiting.contains(varName)) return true;
+ if (visited.contains(varName)) return false;
+ visiting.add(varName);
+ visited.add(varName);
+ bool cycleDetected = false;
+ if (varDefs.map[varName] != null) {
+ for (var usage in variablesOf(varDefs.map[varName].expression)) {
+ if (hasCycle(usage.name, visiting: visiting, visited: visited)) {
+ cycleDetected = true;
+ break;
+ }
+ }
+ }
+ visiting.remove(varName);
+ return cycleDetected;
+ }
+
+ // TODO(terry): Need to start supporting @host, custom pseudo elements,
+ // composition, intrinsics, etc.
+}
+
+
+/** Find all var definitions from a list of stylesheets. */
+class VarDefinitions extends Visitor {
+ /** Map of variable name key to it's definition. */
+ final Map<String, VarDefinition> map = new Map<String, VarDefinition>();
+
+ VarDefinitions(List<StyleSheet> styleSheets) {
+ for (var styleSheet in styleSheets) {
+ visitTree(styleSheet);
+ }
+ }
+
+ void visitVarDefinition(VarDefinition node) {
+ // Replace with latest variable definition.
+ map[node.definedName] = node;
+ super.visitVarDefinition(node);
+ }
+
+ void visitVarDefinitionDirective(VarDefinitionDirective node) {
+ visitVarDefinition(node.def);
+ }
+}
+
+/**
+ * Remove the var definition from the stylesheet where it is defined; if it is
+ * a definition from the list to delete.
+ */
+class RemoveVarDefinitions extends Visitor {
+ final List<VarDefinition> _varDefsToRemove;
+
+ RemoveVarDefinitions(this._varDefsToRemove);
+
+ void visitStyleSheet(StyleSheet ss) {
+ var idx = ss.topLevels.length;
+ while(--idx >= 0) {
+ var topLevel = ss.topLevels[idx];
+ if (topLevel is VarDefinitionDirective &&
+ _varDefsToRemove.contains(topLevel.def)) {
+ ss.topLevels.removeAt(idx);
+ }
+ }
+
+ super.visitStyleSheet(ss);
+ }
+
+ void visitDeclarationGroup(DeclarationGroup node) {
+ var idx = node.declarations.length;
+ while (--idx >= 0) {
+ var decl = node.declarations[idx];
+ if (decl is VarDefinition && _varDefsToRemove.contains(decl)) {
+ node.declarations.removeAt(idx);
+ }
+ }
+
+ super.visitDeclarationGroup(node);
+ }
+}
+
+/**
+ * Traverse all rulesets looking for nested ones. If a ruleset is in a
+ * declaration group (implies nested selector) then generate new ruleset(s) at
+ * level 0 of CSS using selector inheritance syntax (flattens the nesting).
+ *
+ * How the AST works for a rule [RuleSet] and nested rules. First of all a
+ * CSS rule [RuleSet] consist of a selector and a declaration e.g.,
+ *
+ * selector {
+ * declaration
+ * }
+ *
+ * AST structure of a [RuleSet] is:
+ *
+ * RuleSet
+ * SelectorGroup
+ * List<Selector>
+ * List<SimpleSelectorSequence>
+ * Combinator // +, >, ~, DESCENDENT, or NONE
+ * SimpleSelector // class, id, element, namespace, attribute
+ * DeclarationGroup
+ * List // Declaration or RuleSet
+ *
+ * For the simple rule:
+ *
+ * div + span { color: red; }
+ *
+ * the AST [RuleSet] is:
+ *
+ * RuleSet
+ * SelectorGroup
+ * List<Selector>
+ * [0]
+ * List<SimpleSelectorSequence>
+ * [0] Combinator = COMBINATOR_NONE
+ * ElementSelector (name = div)
+ * [1] Combinator = COMBINATOR_PLUS
+ * ElementSelector (name = span)
+ * DeclarationGroup
+ * List // Declarations or RuleSets
+ * [0]
+ * Declaration (property = color, expression = red)
+ *
+ * Usually a SelectorGroup contains 1 Selector. Consider the selectors:
+ *
+ * div { color: red; }
+ * a { color: red; }
+ *
+ * are equivalent to
+ *
+ * div, a { color : red; }
+ *
+ * In the above the RuleSet would have a SelectorGroup with 2 selectors e.g.,
+ *
+ * RuleSet
+ * SelectorGroup
+ * List<Selector>
+ * [0]
+ * List<SimpleSelectorSequence>
+ * [0] Combinator = COMBINATOR_NONE
+ * ElementSelector (name = div)
+ * [1]
+ * List<SimpleSelectorSequence>
+ * [0] Combinator = COMBINATOR_NONE
+ * ElementSelector (name = a)
+ * DeclarationGroup
+ * List // Declarations or RuleSets
+ * [0]
+ * Declaration (property = color, expression = red)
+ *
+ * For a nested rule e.g.,
+ *
+ * div {
+ * color : blue;
+ * a { color : red; }
+ * }
+ *
+ * Would map to the follow CSS rules:
+ *
+ * div { color: blue; }
+ * div a { color: red; }
+ *
+ * The AST for the former nested rule is:
+ *
+ * RuleSet
+ * SelectorGroup
+ * List<Selector>
+ * [0]
+ * List<SimpleSelectorSequence>
+ * [0] Combinator = COMBINATOR_NONE
+ * ElementSelector (name = div)
+ * DeclarationGroup
+ * List // Declarations or RuleSets
+ * [0]
+ * Declaration (property = color, expression = blue)
+ * [1]
+ * RuleSet
+ * SelectorGroup
+ * List<Selector>
+ * [0]
+ * List<SimpleSelectorSequence>
+ * [0] Combinator = COMBINATOR_NONE
+ * ElementSelector (name = a)
+ * DeclarationGroup
+ * List // Declarations or RuleSets
+ * [0]
+ * Declaration (property = color, expression = red)
+ *
+ * Nested rules is a terse mechanism to describe CSS inheritance. The analyzer
+ * will flatten and expand the nested rules to it's flatten strucure. Using the
+ * all parent [RuleSets] (selector expressions) and applying each nested
+ * [RuleSet] to the list of [Selectors] in a [SelectorGroup].
+ *
+ * Then result is a style sheet where all nested rules have been flatten and
+ * expanded.
+ */
+class ExpandNestedSelectors extends Visitor {
+ /** Parent [RuleSet] if a nested rule otherwise [null]. */
+ RuleSet _parentRuleSet;
+
+ /** Top-most rule if nested rules. */
+ SelectorGroup _topLevelSelectorGroup;
+
+ /** SelectorGroup at each nesting level. */
+ SelectorGroup _nestedSelectorGroup;
+
+ /** Declaration (sans the nested selectors). */
+ DeclarationGroup _flatDeclarationGroup;
+
+ /** Each nested selector get's a flatten RuleSet. */
+ List<RuleSet> _expandedRuleSets = [];
+
+ /** Maping of a nested rule set to the fully expanded list of RuleSet(s). */
+ final Map<RuleSet, List<RuleSet>> _expansions = new Map();
+
+ void visitRuleSet(RuleSet node) {
+ final oldParent = _parentRuleSet;
+
+ var oldNestedSelectorGroups = _nestedSelectorGroup;
+
+ if (_nestedSelectorGroup == null) {
+ // Create top-level selector (may have nested rules).
+ final newSelectors = node.selectorGroup.selectors.toList();
+ _topLevelSelectorGroup = new SelectorGroup(newSelectors, node.span);
+ _nestedSelectorGroup = _topLevelSelectorGroup;
+ } else {
+ // Generate new selector groups from the nested rules.
+ _nestedSelectorGroup = _mergeToFlatten(node);
+ }
+
+ _parentRuleSet = node;
+
+ super.visitRuleSet(node);
+
+ _parentRuleSet = oldParent;
+
+ // Remove nested rules; they're all flatten and in the _expandedRuleSets.
+ node.declarationGroup.declarations.removeWhere((declaration) =>
+ declaration is RuleSet);
+
+ _nestedSelectorGroup = oldNestedSelectorGroups;
+
+ // If any expandedRuleSets and we're back at the top-level rule set then
+ // there were nested rule set(s).
+ if (_parentRuleSet == null) {
+ if (!_expandedRuleSets.isEmpty) {
+ // Remember ruleset to replace with these flattened rulesets.
+ _expansions[node] = _expandedRuleSets;
+ _expandedRuleSets = [];
+ }
+ assert(_flatDeclarationGroup == null);
+ assert(_nestedSelectorGroup == null);
+ }
+ }
+
+ /**
+ * Build up the list of all inherited sequences from the parent selector
+ * [node] is the current nested selector and it's parent is the last entry in
+ * the [_nestedSelectorGroup].
+ */
+ SelectorGroup _mergeToFlatten(RuleSet node) {
+ // Create a new SelectorGroup for this nesting level.
+ var nestedSelectors = _nestedSelectorGroup.selectors;
+ var selectors = node.selectorGroup.selectors;
+
+ // Create a merged set of previous parent selectors and current selectors.
+ var newSelectors = [];
+ for (Selector selector in selectors) {
+ for (Selector nestedSelector in nestedSelectors) {
+ var seq = _mergeNestedSelector(nestedSelector.simpleSelectorSequences,
+ selector.simpleSelectorSequences);
+ newSelectors.add(new Selector(seq, node.span));
+ }
+ }
+
+ return new SelectorGroup(newSelectors, node.span);
+ }
+
+ /**
+ * 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 = [];
+
+ 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.isEmpty &&
+ !newSequence.last.simpleSelector.name.isEmpty;
+ 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 = [];
+ var first = sequences.first;
+ newSequences.add(new SimpleSelectorSequence(first.simpleSelector,
+ first.span, TokenKind.COMBINATOR_DESCENDANT));
+ newSequences.addAll(sequences.skip(1));
+
+ return newSequences;
+ }
+
+ void visitDeclarationGroup(DeclarationGroup node) {
+ var span = node.span;
+
+ var currentGroup = new DeclarationGroup([], span);
+
+ var oldGroup = _flatDeclarationGroup;
+ _flatDeclarationGroup = currentGroup;
+
+ var expandedLength = _expandedRuleSets.length;
+
+ super.visitDeclarationGroup(node);
+
+ // We're done with the group.
+ _flatDeclarationGroup = oldGroup;
+
+ // No nested rule to process it's a top-level rule.
+ if (_nestedSelectorGroup == _topLevelSelectorGroup) return;
+
+ // If flatten selector's declaration is empty skip this selector, no need
+ // to emit an empty nested selector.
+ if (currentGroup.declarations.isEmpty) return;
+
+ var selectorGroup = _nestedSelectorGroup;
+
+ // Build new rule set from the nested selectors and declarations.
+ var newRuleSet = new RuleSet(selectorGroup, currentGroup, span);
+
+ // Place in order so outer-most rule is first.
+ if (expandedLength == _expandedRuleSets.length) {
+ _expandedRuleSets.add(newRuleSet);
+ } else {
+ _expandedRuleSets.insert(expandedLength, newRuleSet);
+ }
+ }
+
+ // Record all declarations in a nested selector (Declaration, VarDefinition
+ // and MarginGroup) but not the nested rule in the Declaration.
+
+ void visitDeclaration(Declaration node) {
+ if (_parentRuleSet != null) {
+ _flatDeclarationGroup.declarations.add(node);
+ }
+ super.visitDeclaration(node);
+ }
+
+ void visitVarDefinition(VarDefinition node) {
+ if (_parentRuleSet != null) {
+ _flatDeclarationGroup.declarations.add(node);
+ }
+ super.visitVarDefinition(node);
+ }
+
+ void visitMarginGroup(MarginGroup node) {
+ if (_parentRuleSet != null) {
+ _flatDeclarationGroup.declarations.add(node);
+ }
+ super.visitMarginGroup(node);
+ }
+
+ /**
+ * Replace the rule set that contains nested rules with the flatten rule sets.
+ */
+ void flatten(StyleSheet styleSheet) {
+ // TODO(terry): Iterate over topLevels instead of _expansions it's already
+ // a map (this maybe quadratic).
+ _expansions.forEach((RuleSet ruleSet, List<RuleSet> newRules) {
+ var index = styleSheet.topLevels.indexOf(ruleSet);
+ if (index == -1) {
+ // Check any @media directives for nested rules and replace them.
+ var found = _MediaRulesReplacer.replace(styleSheet, ruleSet, newRules);
+ assert(found);
+ } else {
+ styleSheet.topLevels.insertAll(index + 1, newRules);
+ }
+ });
+ _expansions.clear();
+ }
+}
+
+class _MediaRulesReplacer extends Visitor {
+ RuleSet _ruleSet;
+ List<RuleSet> _newRules;
+ bool _foundAndReplaced = false;
+
+ /**
+ * Look for the [ruleSet] inside of an @media directive; if found then replace
+ * with the [newRules]. If [ruleSet] is found and replaced return true.
+ */
+ static bool replace(StyleSheet styleSheet, RuleSet ruleSet,
+ List<RuleSet>newRules) {
+ var visitor = new _MediaRulesReplacer(ruleSet, newRules);
+ visitor.visitStyleSheet(styleSheet);
+ return visitor._foundAndReplaced;
+ }
+
+ _MediaRulesReplacer(this._ruleSet, this._newRules);
+
+ visitMediaDirective(MediaDirective node) {
+ var index = node.rulesets.indexOf(_ruleSet);
+ if (index != -1) {
+ node.rulesets.insertAll(index + 1, _newRules);
+ _foundAndReplaced = true;
+ }
+ }
+}
diff --git a/lib/src/css_printer.dart b/lib/src/css_printer.dart
new file mode 100644
index 0000000..b8000b7
--- /dev/null
+++ b/lib/src/css_printer.dart
@@ -0,0 +1,476 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.visitor;
+
+/**
+ * Visitor that produces a formatted string representation of the CSS tree.
+ */
+class CssPrinter extends Visitor {
+ StringBuffer _buff = new StringBuffer();
+ bool prettyPrint = true;
+
+ /**
+ * Walk the [tree] Stylesheet. [pretty] if true emits line breaks, extra
+ * spaces, friendly property values, etc., if false emits compacted output.
+ */
+ void visitTree(StyleSheet tree, {bool pretty: false}) {
+ prettyPrint = pretty;
+ _buff = new StringBuffer();
+ visitStyleSheet(tree);
+ }
+
+ /** Appends [str] to the output buffer. */
+ void emit(String str) {
+ _buff.write(str);
+ }
+
+ /** Returns the output buffer. */
+ String toString() => _buff.toString().trim();
+
+ String get _newLine => prettyPrint ? '\n' : ' ';
+ String get _sp => prettyPrint ? ' ' : '';
+
+ // TODO(terry): When adding obfuscation we'll need isOptimized (compact w/
+ // obufuscation) and have isTesting (compact no obfuscation) and
+ // isCompact would be !prettyPrint. We'll need another boolean
+ // flag for obfuscation.
+ bool get _isTesting => !prettyPrint;
+
+ void visitCssComment(CssComment node) {
+ emit('/* ${node.comment} */');
+ }
+
+ void visitCommentDefinition(CommentDefinition node) {
+ emit('<!-- ${node.comment} -->');
+ }
+
+ void visitMediaExpression(MediaExpression node) {
+ emit(node.andOperator ? ' AND ' : ' ');
+ emit('(${node.mediaFeature}:');
+ visitExpressions(node.exprs);
+ emit(')');
+ }
+
+ void visitMediaQuery(MediaQuery query) {
+ var unary = query.hasUnary ? ' ${query.unary}' : '';
+ var mediaType = query.hasMediaType ? ' ${query.mediaType}' : '';
+ emit('$unary$mediaType');
+ for (var expression in query.expressions) {
+ visitMediaExpression(expression);
+ }
+ }
+
+ void emitMediaQueries(queries) {
+ var queriesLen = queries.length;
+ for (var i = 0; i < queriesLen; i++) {
+ var query = queries[i];
+ if (query.hasMediaType && i > 0) emit(',');
+ visitMediaQuery(query);
+ }
+ }
+
+ void visitMediaDirective(MediaDirective node) {
+ emit(' @media');
+ emitMediaQueries(node.mediaQueries);
+ emit(' {');
+ for (var ruleset in node.rulesets) {
+ ruleset.visit(this);
+ }
+ emit('$_newLine\}');
+ }
+
+ void visitHostDirective(HostDirective node) {
+ emit('\n@host {');
+ for (var ruleset in node.rulesets) {
+ ruleset.visit(this);
+ }
+ emit('$_newLine\}');
+ }
+
+ /**
+ * @page : pseudoPage {
+ * decls
+ * }
+ */
+ void visitPageDirective(PageDirective node) {
+ emit('$_newLine@page');
+ if (node.hasIdent || node.hasPseudoPage) {
+ if (node.hasIdent) emit(' ');
+ emit(node._ident);
+ emit(node.hasPseudoPage ? ':${node._pseudoPage}' : '');
+ }
+ emit(' ');
+
+ var declsMargin = node._declsMargin;
+ int declsMarginLength = declsMargin.length;
+ for (var i = 0; i < declsMarginLength; i++) {
+ if (i > 0) emit(_newLine);
+ emit('{$_newLine');
+ declsMargin[i].visit(this);
+ emit('}');
+ }
+ }
+
+ /** @charset "charset encoding" */
+ void visitCharsetDirective(CharsetDirective node) {
+ emit('$_newLine@charset "${node.charEncoding}";');
+ }
+
+ void visitImportDirective(ImportDirective node) {
+ bool isStartingQuote(String ch) => ('\'"'.indexOf(ch[0]) >= 0);
+
+ if (_isTesting) {
+ // Emit assuming url() was parsed; most suite tests use url function.
+ emit(' @import url(${node.import})');
+ } else if (isStartingQuote(node.import)) {
+ emit(' @import ${node.import}');
+ } else {
+ // url(...) isn't needed only a URI can follow an @import directive; emit
+ // url as a string.
+ emit(' @import "${node.import}"');
+ }
+ emitMediaQueries(node.mediaQueries);
+ emit(';');
+ }
+
+ void visitKeyFrameDirective(KeyFrameDirective node) {
+ emit('$_newLine${node.keyFrameName} ');
+ node._name.visit(this);
+ emit('$_sp{$_newLine');
+ for (final block in node._blocks) {
+ block.visit(this);
+ }
+ emit('}');
+ }
+
+ void visitFontFaceDirective(FontFaceDirective node) {
+ emit('$_newLine@font-face ');
+ emit('$_sp{$_newLine');
+ node._declarations.visit(this);
+ emit('}');
+ }
+
+ void visitKeyFrameBlock(KeyFrameBlock node) {
+ emit('$_sp$_sp');
+ node._blockSelectors.visit(this);
+ emit('$_sp{$_newLine');
+ node._declarations.visit(this);
+ emit('$_sp$_sp}$_newLine');
+ }
+
+ void visitStyletDirective(StyletDirective node) {
+ emit('/* @stylet export as ${node._dartClassName} */\n');
+ }
+
+ void visitNamespaceDirective(NamespaceDirective node) {
+ bool isStartingQuote(String ch) => ('\'"'.indexOf(ch) >= 0);
+
+ if (isStartingQuote(node._uri)) {
+ emit(' @namespace ${node.prefix}"${node._uri}"');
+ } else {
+ if (_isTesting) {
+ // Emit exactly was we parsed.
+ emit(' @namespace ${node.prefix}url(${node._uri})');
+ } else {
+ // url(...) isn't needed only a URI can follow a:
+ // @namespace prefix directive.
+ emit(' @namespace ${node.prefix}${node._uri}');
+ }
+ }
+ emit(';');
+ }
+
+ void visitVarDefinitionDirective(VarDefinitionDirective node) {
+ visitVarDefinition(node.def);
+ emit(';$_newLine');
+ }
+
+ void visitRuleSet(RuleSet node) {
+ emit("$_newLine");
+ node._selectorGroup.visit(this);
+ emit(" {$_newLine");
+ node._declarationGroup.visit(this);
+ emit("}");
+ }
+
+ void visitDeclarationGroup(DeclarationGroup node) {
+ var declarations = node._declarations;
+ var declarationsLength = declarations.length;
+ for (var i = 0; i < declarationsLength; i++) {
+ if (i > 0) emit(_newLine);
+ emit("$_sp$_sp");
+ declarations[i].visit(this);
+ emit(";");
+ }
+ if (declarationsLength > 0) emit(_newLine);
+ }
+
+ void visitMarginGroup(MarginGroup node) {
+ var margin_sym_name =
+ TokenKind.idToValue(TokenKind.MARGIN_DIRECTIVES, node.margin_sym);
+
+ emit("@$margin_sym_name {$_newLine");
+
+ visitDeclarationGroup(node);
+
+ emit("}$_newLine");
+ }
+
+ void visitDeclaration(Declaration node) {
+ String importantAsString() => node.important ? '$_sp!important' : '';
+
+ emit("${node.property}: ");
+ node._expression.visit(this);
+
+ emit("${importantAsString()}");
+ }
+
+ void visitVarDefinition(VarDefinition node) {
+ emit("var-${node.definedName}: ");
+ node._expression.visit(this);
+ }
+
+ void visitSelectorGroup(SelectorGroup node) {
+ var selectors = node._selectors;
+ var selectorsLength = selectors.length;
+ for (var i = 0; i < selectorsLength; i++) {
+ if (i > 0) emit(',$_sp');
+ selectors[i].visit(this);
+ }
+ }
+
+ void visitSimpleSelectorSequence(SimpleSelectorSequence node) {
+ emit('${node._combinatorToString}');
+ node._selector.visit(this);
+ }
+
+ void visitSimpleSelector(SimpleSelector node) {
+ emit(node.name);
+ }
+
+ void visitNamespaceSelector(NamespaceSelector node) {
+ emit("${node.namespace}|${node.nameAsSimpleSelector.name}");
+ }
+
+ void visitElementSelector(ElementSelector node) {
+ emit("${node.name}");
+ }
+
+ void visitAttributeSelector(AttributeSelector node) {
+ emit("[${node.name}${node.matchOperator()}${node.valueToString()}]");
+ }
+
+ void visitIdSelector(IdSelector node) {
+ emit("#${node.name}");
+ }
+
+ void visitClassSelector(ClassSelector node) {
+ emit(".${node.name}");
+ }
+
+ void visitPseudoClassSelector(PseudoClassSelector node) {
+ emit(":${node.name}");
+ }
+
+ void visitPseudoElementSelector(PseudoElementSelector node) {
+ emit("::${node.name}");
+ }
+
+ void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node) {
+ emit(":${node.name}(");
+ node.expression.visit(this);
+ emit(')');
+ }
+
+ void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node) {
+ emit("::${node.name}(");
+ node.expression.visit(this);
+ emit(')');
+ }
+
+ void visitNegationSelector(NegationSelector node) {
+ emit(':not(');
+ node.negationArg.visit(this);
+ emit(')');
+ }
+
+ void visitSelectorExpression(SelectorExpression node) {
+ var expressions = node._expressions;
+ var expressionsLength = expressions.length;
+ for (var i = 0; i < expressionsLength; i++) {
+ // Add space seperator between terms without an operator.
+ var expression = expressions[i];
+ expression.visit(this);
+ }
+ }
+
+ void visitUnicodeRangeTerm(UnicodeRangeTerm node) {
+ if (node.hasSecond) {
+ emit("U+${node.first}-${node.second}");
+ } else {
+ emit("U+${node.first}");
+ }
+ }
+
+ void visitLiteralTerm(LiteralTerm node) {
+ emit(node.text);
+ }
+
+ void visitHexColorTerm(HexColorTerm node) {
+ var mappedName;
+ if (_isTesting && (node.value is! BAD_HEX_VALUE)) {
+ mappedName = TokenKind.hexToColorName(node.value);
+ }
+ if (mappedName == null) {
+ mappedName = '#${node.text}';
+ }
+
+ emit(mappedName);
+ }
+
+ void visitNumberTerm(NumberTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitUnitTerm(UnitTerm node) {
+ emit(node.toString());
+ }
+
+ void visitLengthTerm(LengthTerm node) {
+ emit(node.toString());
+ }
+
+ void visitPercentageTerm(PercentageTerm node) {
+ emit('${node.text}%');
+ }
+
+ void visitEmTerm(EmTerm node) {
+ emit('${node.text}em');
+ }
+
+ void visitExTerm(ExTerm node) {
+ emit('${node.text}ex');
+ }
+
+ void visitAngleTerm(AngleTerm node) {
+ emit(node.toString());
+ }
+
+ void visitTimeTerm(TimeTerm node) {
+ emit(node.toString());
+ }
+
+ void visitFreqTerm(FreqTerm node) {
+ emit(node.toString());
+ }
+
+ void visitFractionTerm(FractionTerm node) {
+ emit('${node.text}fr');
+ }
+
+ void visitUriTerm(UriTerm node) {
+ emit('url("${node.text}")');
+ }
+
+ void visitResolutionTerm(ResolutionTerm node) {
+ emit(node.toString());
+ }
+
+ void visitViewportTerm(ViewportTerm node) {
+ emit(node.toString());
+ }
+
+ void visitFunctionTerm(FunctionTerm node) {
+ // TODO(terry): Optimize rgb to a hexcolor.
+ emit('${node.text}(');
+ node._params.visit(this);
+ emit(')');
+ }
+
+ void visitGroupTerm(GroupTerm node) {
+ emit('(');
+ var terms = node._terms;
+ var termsLength = terms.length;
+ for (var i = 0; i < termsLength; i++) {
+ if (i > 0) emit('$_sp');
+ terms[i].visit(this);
+ }
+ emit(')');
+ }
+
+ void visitItemTerm(ItemTerm node) {
+ emit('[${node.text}]');
+ }
+
+ void visitIE8Term(IE8Term node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitOperatorSlash(OperatorSlash node) {
+ emit('/');
+ }
+
+ void visitOperatorComma(OperatorComma node) {
+ emit(',');
+ }
+
+ void visitOperatorPlus(OperatorPlus node) {
+ emit('+');
+ }
+
+ void visitOperatorMinus(OperatorMinus node) {
+ emit('-');
+ }
+
+ void visitVarUsage(VarUsage node) {
+ emit('var(${node.name}');
+ if (!node.defaultValues.isEmpty) {
+ emit(',');
+ for (var defaultValue in node.defaultValues) {
+ emit(' ');
+ defaultValue.visit(this);
+ }
+ }
+ emit(')');
+ }
+
+ void visitExpressions(Expressions node) {
+ var expressions = node.expressions;
+ var expressionsLength = expressions.length;
+ for (var i = 0; i < expressionsLength; i++) {
+ // Add space seperator between terms without an operator.
+ // TODO(terry): Should have a BinaryExpression to solve this problem.
+ var expression = expressions[i];
+ if (i > 0 &&
+ !(expression is OperatorComma || expression is OperatorSlash)) {
+ emit(' ');
+ }
+ expression.visit(this);
+ }
+ }
+
+ void visitBinaryExpression(BinaryExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitUnaryExpression(UnaryExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitIdentifier(Identifier node) {
+ emit(node.name);
+ }
+
+ void visitWildcard(Wildcard node) {
+ emit('*');
+ }
+
+ void visitDartStyleExpression(DartStyleExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+}
diff --git a/lib/src/messages.dart b/lib/src/messages.dart
new file mode 100644
index 0000000..3c9f3fc
--- /dev/null
+++ b/lib/src/messages.dart
@@ -0,0 +1,129 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.src.messages;
+
+import 'package:logging/logging.dart' show Level;
+import 'package:source_maps/span.dart' show Span;
+
+import 'package:csslib/parser.dart';
+
+import 'options.dart';
+
+// TODO(terry): Remove the global messages, use some object that tracks
+// compilation state.
+
+/** The global [Messages] for tracking info/warnings/messages. */
+Messages messages;
+
+// Color constants used for generating messages.
+final String GREEN_COLOR = '\u001b[32m';
+final String RED_COLOR = '\u001b[31m';
+final String MAGENTA_COLOR = '\u001b[35m';
+final String NO_COLOR = '\u001b[0m';
+
+/** Map between error levels and their display color. */
+final Map<Level, String> _ERROR_COLORS = (() {
+ var colorsMap = new Map<Level, String>();
+ colorsMap[Level.SEVERE] = RED_COLOR;
+ colorsMap[Level.WARNING] = MAGENTA_COLOR;
+ colorsMap[Level.INFO] = GREEN_COLOR;
+ return colorsMap;
+})();
+
+/** Map between error levels and their friendly name. */
+final Map<Level, String> _ERROR_LABEL = (() {
+ var labels = new Map<Level, String>();
+ labels[Level.SEVERE] = 'error';
+ labels[Level.WARNING] = 'warning';
+ labels[Level.INFO] = 'info';
+ return labels;
+})();
+
+/** A single message from the compiler. */
+class Message {
+ final Level level;
+ final String message;
+ final Span span;
+ final bool useColors;
+
+ Message(this.level, this.message, {Span span, bool useColors: false})
+ : this.span = span, this.useColors = useColors;
+
+ String toString() {
+ var output = new StringBuffer();
+ bool colors = useColors && _ERROR_COLORS.containsKey(level);
+ var levelColor = _ERROR_COLORS[level];
+ if (colors) output.write(levelColor);
+ output..write(_ERROR_LABEL[level])..write(' ');
+ if (colors) output.write(NO_COLOR);
+
+ if (span == null) {
+ output.write(message);
+ } else {
+ output.write(span.getLocationMessage(message, useColors: colors,
+ color: levelColor));
+ }
+
+ return output.toString();
+ }
+}
+
+typedef void PrintHandler(Object obj);
+
+/**
+ * This class tracks and prints information, warnings, and errors emitted by the
+ * compiler.
+ */
+class Messages {
+ /** Called on every error. Set to blank function to supress printing. */
+ final PrintHandler printHandler;
+
+ final PreprocessorOptions options;
+
+ final List<Message> messages = <Message>[];
+
+ Messages({PreprocessorOptions options, this.printHandler: print})
+ : options = options != null ? options : new PreprocessorOptions();
+
+ /** Report a compile-time CSS error. */
+ void error(String message, Span span) {
+ var msg = new Message(Level.SEVERE, message, span: span,
+ useColors: options.useColors);
+
+ messages.add(msg);
+
+ printHandler(msg);
+ }
+
+ /** Report a compile-time CSS warning. */
+ void warning(String message, Span span) {
+ if (options.warningsAsErrors) {
+ error(message, span);
+ } else {
+ var msg = new Message(Level.WARNING, message, span: span,
+ useColors: options.useColors);
+
+ messages.add(msg);
+ }
+ }
+
+ /** Report and informational message about what the compiler is doing. */
+ void info(String message, Span span) {
+ var msg = new Message(Level.INFO, message, span: span,
+ useColors: options.useColors);
+
+ messages.add(msg);
+
+ if (options.verbose) printHandler(msg);
+ }
+
+ /** Merge [newMessages] to this message lsit. */
+ void mergeMessages(Messages newMessages) {
+ messages.addAll(newMessages.messages);
+ newMessages.messages.where((message) =>
+ message.level.value == Level.SEVERE || options.verbose)
+ .forEach((message) { printHandler(message); });
+ }
+}
diff --git a/lib/src/options.dart b/lib/src/options.dart
new file mode 100644
index 0000000..a62ef71
--- /dev/null
+++ b/lib/src/options.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.src.options;
+
+import 'package:args/args.dart';
+
+class PreprocessorOptions {
+ /** Report warnings as errors. */
+ final bool warningsAsErrors;
+
+ /** Throw an exception on warnings (not used by command line tool). */
+ final bool throwOnWarnings;
+
+ /** Throw an exception on errors (not used by command line tool). */
+ final bool throwOnErrors;
+
+ /** True to show informational messages. The `--verbose` flag. */
+ final bool verbose;
+
+ /** True to show warning messages for bad CSS. The '--checked' flag. */
+ final bool checked;
+
+ // TODO(terry): Add mixin support and nested rules.
+ /**
+ * Subset of Less commands enabled; disable with '--no-less'.
+ * Less syntax supported:
+ * - @name at root level statically defines variables resolved at compilation
+ * time. Essentially a directive e.g., @var-name.
+ */
+ final bool lessSupport;
+
+ /** Whether to use colors to print messages on the terminal. */
+ final bool useColors;
+
+ /** File to process by the compiler. */
+ String inputFile;
+
+ // We could make this faster, if it ever matters.
+ factory PreprocessorOptions() => parse(['']);
+
+ PreprocessorOptions.fromArgs(ArgResults args)
+ : warningsAsErrors = args['warnings_as_errors'],
+ throwOnWarnings = args['throw_on_warnings'],
+ throwOnErrors = args['throw_on_errors'],
+ verbose = args['verbose'],
+ checked = args['checked'],
+ lessSupport = args['less'],
+ useColors = args['colors'],
+ inputFile = args.rest.length > 0 ? args.rest[0] : null;
+
+ // tool.dart [options...] <css file>
+ static PreprocessorOptions parse(List<String> arguments) {
+ var parser = new ArgParser()
+ ..addFlag('verbose', abbr: 'v', defaultsTo: false, negatable: false,
+ help: 'Display detail info')
+ ..addFlag('checked', defaultsTo: false, negatable: false,
+ help: 'Validate CSS values invalid value display a warning message')
+ ..addFlag('less', defaultsTo: true, negatable: true,
+ help: 'Supports subset of Less syntax')
+ ..addFlag('suppress_warnings', defaultsTo: true,
+ help: 'Warnings not displayed')
+ ..addFlag('warnings_as_errors', defaultsTo: false,
+ help: 'Warning handled as errors')
+ ..addFlag('throw_on_errors', defaultsTo: false,
+ help: 'Throw on errors encountered')
+ ..addFlag('throw_on_warnings', defaultsTo: false,
+ help: 'Throw on warnings encountered')
+ ..addFlag('colors', defaultsTo: true,
+ help: 'Display errors/warnings in colored text')
+ ..addFlag('help', abbr: 'h', defaultsTo: false, negatable: false,
+ help: 'Displays this help message');
+
+ try {
+ var results = parser.parse(arguments);
+ if (results['help'] || results.rest.length == 0) {
+ showUsage(parser);
+ return null;
+ }
+ return new PreprocessorOptions.fromArgs(results);
+ } on FormatException catch (e) {
+ print(e.message);
+ showUsage(parser);
+ return null;
+ }
+ }
+
+ static showUsage(parser) {
+ print('Usage: css [options...] input.css');
+ print(parser.getUsage());
+ }
+
+}
diff --git a/lib/src/property.dart b/lib/src/property.dart
new file mode 100644
index 0000000..8dc4dcb
--- /dev/null
+++ b/lib/src/property.dart
@@ -0,0 +1,1250 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/** Representations of CSS styles. */
+
+part of csslib.parser;
+
+// TODO(terry): Prune down this file we do need some of the code in this file
+// for darker, lighter, how to represent a Font, etc but alot of
+// the complexity can be removed.
+// See https://github.com/dart-lang/csslib/issues/7
+
+/**
+ * Base for all style properties (e.g., Color, Font, Border, Margin, etc.)
+ */
+abstract class _StyleProperty {
+ /**
+ * Returns the expression part of a CSS declaration. Declaration is:
+ *
+ * property:expression;
+ *
+ * E.g., if property is color then expression could be rgba(255,255,0) the
+ * CSS declaration would be 'color:rgba(255,255,0);'.
+ *
+ * then _cssExpression would return 'rgba(255,255,0)'. See
+ * <http://www.w3.org/TR/CSS21/grammar.html>
+ */
+ String get cssExpression;
+}
+
+
+/**
+ * Base interface for Color, HSL and RGB.
+ */
+abstract class ColorBase {
+ /**
+ * Canonical form for color #rrggbb with alpha blending (0.0 == full
+ * transparency and 1.0 == fully opaque). If _argb length is 6 it's an
+ * rrggbb otherwise it's aarrggbb.
+ */
+ String toHexArgbString();
+
+ /**
+ * Return argb as a value (int).
+ */
+ int get argbValue;
+}
+
+
+/**
+ * General purpse Color class. Represent a color as an ARGB value that can be
+ * converted to and from num, hex string, hsl, hsla, rgb, rgba and SVG pre-
+ * defined color constant.
+ */
+class Color implements _StyleProperty, ColorBase {
+ // If _argb length is 6 it's an rrggbb otherwise it's aarrggbb.
+ final String _argb;
+
+ // TODO(terry): Look at reducing Rgba and Hsla classes as factories for
+ // converting from Color to an Rgba or Hsla for reading only.
+ // Usefulness of creating an Rgba or Hsla is limited.
+
+ /**
+ * Create a color with an integer representing the rgb value of red, green,
+ * and blue. The value 0xffffff is the color white #ffffff (CSS style).
+ * The [rgb] value of 0xffd700 would map to #ffd700 or the constant
+ * Color.gold, where ff is red intensity, d7 is green intensity, and 00 is
+ * blue intensity.
+ */
+ Color(int rgb, [num alpha]) :
+ this._argb = Color._rgbToArgbString(rgb, alpha);
+
+ /**
+ * RGB takes three values. The [red], [green], and [blue] parameters are
+ * the intensity of those components where '0' is the least and '256' is the
+ * greatest.
+ *
+ * If [alpha] is provided, it is the level of translucency which ranges from
+ * '0' (completely transparent) to '1.0' (completely opaque). It will
+ * internally be mapped to an int between '0' and '255' like the other color
+ * components.
+ */
+ Color.createRgba(int red, int green, int blue, [num alpha]) :
+ this._argb = Color.convertToHexString(Color._clamp(red, 0, 255),
+ Color._clamp(green, 0, 255),
+ Color._clamp(blue, 0, 255),
+ alpha != null ? Color._clamp(alpha, 0, 1) : alpha);
+
+ /**
+ * Creates a new color from a CSS color string. For more information, see
+ * <https://developer.mozilla.org/en/CSS/color>.
+ */
+ Color.css(String color) :
+ this._argb = Color._convertCssToArgb(color);
+
+ // TODO(jmesserly): I found the use of percents a bit suprising.
+ /**
+ * HSL takes three values. The [hueDegree] degree on the color wheel; '0' is
+ * the least and '100' is the greatest. The value '0' or '360' is red, '120'
+ * is green, '240' is blue. Numbers in between reflect different shades.
+ * The [saturationPercent] percentage; where'0' is the least and '100' is the
+ * greatest (100 represents full color). The [lightnessPercent] percentage;
+ * where'0' is the least and '100' is the greatest. The value 0 is dark or
+ * black, 100 is light or white and 50 is a medium lightness.
+ *
+ * If [alpha] is provided, it is the level of translucency which ranges from
+ * '0' (completely transparent foreground) to '1.0' (completely opaque
+ * foreground).
+ */
+ Color.createHsla(num hueDegree, num saturationPercent, num lightnessPercent,
+ [num alpha]) :
+ this._argb = new Hsla(Color._clamp(hueDegree, 0, 360) / 360,
+ Color._clamp(saturationPercent, 0, 100) / 100,
+ Color._clamp(lightnessPercent, 0, 100) / 100,
+ alpha != null ? Color._clamp(alpha, 0, 1) : alpha).toHexArgbString();
+
+ /**
+ * The hslaRaw takes three values. The [hue] degree on the color wheel; '0'
+ * is the least and '1' is the greatest. The value '0' or '1' is red, the
+ * ratio of 120/360 is green, and the ratio of 240/360 is blue. Numbers in
+ * between reflect different shades. The [saturation] is a percentage; '0'
+ * is the least and '1' is the greatest. The value of '1' is equivalent to
+ * 100% (full colour). The [lightness] is a percentage; '0' is the least and
+ * '1' is the greatest. The value of '0' is dark (black), the value of '1'
+ * is light (white), and the value of '.50' is a medium lightness.
+ *
+ * The fourth optional parameter is:
+ * [alpha] level of translucency range of values is 0..1, zero is a
+ * completely transparent foreground and 1 is a completely
+ * opaque foreground.
+ */
+ Color.hslaRaw(num hue, num saturation, num lightness, [num alpha]) :
+ this._argb = new Hsla(Color._clamp(hue, 0, 1),
+ Color._clamp(saturation, 0, 1),
+ Color._clamp(lightness, 0, 1),
+ alpha != null ? Color._clamp(alpha, 0, 1) : alpha).toHexArgbString();
+
+ /**
+ * Generate a real constant for pre-defined colors (no leading #).
+ */
+ const Color.hex(this._argb);
+
+ // TODO(jmesserly): this is needed by the example so leave it exposed for now.
+ String toString() => cssExpression;
+
+ // TODO(terry): Regardless of how color is set (rgb, num, css or hsl) we'll
+ // always return a rgb or rgba loses fidelity when debugging in
+ // CSS if user uses hsl and would like to edit as hsl, etc. If
+ // this is an issue we should keep the original value and not re-
+ // create the CSS from the normalized value.
+ String get cssExpression {
+ if (_argb.length == 6) {
+ return "#$_argb"; // RGB only, no alpha blending.
+ } else {
+ num alpha = Color.hexToInt(_argb.substring(0, 2));
+ String a = (alpha / 255).toStringAsPrecision(2);
+ int r = Color.hexToInt(_argb.substring(2, 4));
+ int g = Color.hexToInt(_argb.substring(4, 6));
+ int b = Color.hexToInt(_argb.substring(6, 8));
+ return "rgba($r,$g,$b,$a)";
+ }
+ }
+
+ Rgba get rgba {
+ int nextIndex = 0;
+ num a;
+ if (_argb.length == 8) {
+ // Get alpha blending value 0..255
+ int alpha = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
+ // Convert to value from 0..1
+ a = double.parse((alpha / 255).toStringAsPrecision(2));
+ nextIndex += 2;
+ }
+ int r = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
+ nextIndex += 2;
+ int g = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
+ nextIndex += 2;
+ int b = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
+ return new Rgba(r, g, b, a);
+ }
+
+ Hsla get hsla => new Hsla.fromRgba(rgba);
+
+ int get argbValue => Color.hexToInt(_argb);
+
+ bool operator ==(Object other) => Color.equal(this, other);
+
+ String toHexArgbString() => _argb;
+
+ Color darker(num amount) {
+ Rgba newRgba = Color._createNewTintShadeFromRgba(rgba, -amount);
+ return new Color.hex("${newRgba.toHexArgbString()}");
+ }
+
+ Color lighter(num amount) {
+ Rgba newRgba = Color._createNewTintShadeFromRgba(rgba, amount);
+ return new Color.hex("${newRgba.toHexArgbString()}");
+ }
+
+ static bool equal(ColorBase curr, Object other) {
+ if (other is Color) {
+ Color o = other;
+ return o.toHexArgbString() == curr.toHexArgbString();
+ } else if (other is Rgba) {
+ Rgba rgb = other;
+ return rgb.toHexArgbString() == curr.toHexArgbString();
+ } else if (other is Hsla) {
+ Hsla hsla = other;
+ return hsla.toHexArgbString() == curr.toHexArgbString();
+ } else {
+ return false;
+ }
+ }
+
+ int get hashCode => _argb.hashCode;
+
+ // Conversion routines:
+
+ static String _rgbToArgbString(int rgba, num alpha) {
+ int a;
+ // If alpha is defined then adjust from 0..1 to 0..255 value, if not set
+ // then a is left as undefined and passed to convertToHexString.
+ if (alpha != null) {
+ a = (Color._clamp(alpha, 0, 1) * 255).round();
+ }
+
+ int r = (rgba & 0xff0000) >> 0x10;
+ int g = (rgba & 0xff00) >> 8;
+ int b = rgba & 0xff;
+
+ return Color.convertToHexString(r, g, b, a);
+ }
+
+ static const int _rgbCss = 1;
+ static const int _rgbaCss = 2;
+ static const int _hslCss = 3;
+ static const int _hslaCss = 4;
+ /**
+ * Parse CSS expressions of the from #rgb, rgb(r,g,b), rgba(r,g,b,a),
+ * hsl(h,s,l), hsla(h,s,l,a) and SVG colors (e.g., darkSlateblue, etc.) and
+ * convert to argb.
+ */
+ static String _convertCssToArgb(String value) {
+ // TODO(terry): Better parser/regex for converting CSS properties.
+ String color = value.trim().replaceAll("\\s", "");
+ if (color[0] == '#') {
+ String v = color.substring(1);
+ Color.hexToInt(v); // Valid hexadecimal, throws if not.
+ return v;
+ } else if (color.length > 0 && color[color.length - 1] == ')') {
+ int type;
+ if (color.indexOf("rgb(") == 0 || color.indexOf("RGB(") == 0) {
+ color = color.substring(4);
+ type = _rgbCss;
+ } else if (color.indexOf("rgba(") == 0 || color.indexOf("RGBA(") == 0) {
+ type = _rgbaCss;
+ color = color.substring(5);
+ } else if (color.indexOf("hsl(") == 0 || color.indexOf("HSL(") == 0) {
+ type = _hslCss;
+ color = color.substring(4);
+ } else if (color.indexOf("hsla(") == 0 || color.indexOf("HSLA(") == 0) {
+ type = _hslaCss;
+ color = color.substring(5);
+ } else {
+ throw new UnsupportedError('CSS property not implemented');
+ }
+
+ color = color.substring(0, color.length - 1); // Strip close paren.
+
+ var args = <num>[];
+ List<String> params = color.split(",");
+ for (String param in params) {
+ args.add(double.parse(param));
+ }
+ switch (type) {
+ case _rgbCss:
+ return Color.convertToHexString(args[0], args[1], args[2]);
+ case _rgbaCss:
+ return Color.convertToHexString(args[0], args[1], args[2], args[3]);
+ case _hslCss:
+ return new Hsla(args[0], args[1], args[2]).toHexArgbString();
+ case _hslaCss:
+ return new Hsla(args[0], args[1], args[2],
+ args[3]).toHexArgbString();
+ default:
+ // Type not defined UnsupportedOperationException should have thrown.
+ assert(true);
+ break;
+ }
+ }
+ }
+
+ /**
+ * [hex] hexadecimal string to convert to scalar.
+ * returns hexadecimal number as an integer.
+ * throws BadNumberFormatException if [hex] isn't a valid hexadecimal number.
+ */
+ // TODO(terry): Should be part of Dart standard library see bug
+ // <http://code.google.com/p/dart/issues/detail?id=2624>
+ static int hexToInt(String hex) {
+ int val = 0;
+
+ int len = hex.length;
+ for (int i = 0; i < len; i++) {
+ int hexDigit = hex.codeUnitAt(i);
+ if (hexDigit >= 48 && hexDigit <= 57) {
+ val += (hexDigit - 48) * (1 << (4 * (len - 1 - i)));
+ } else if (hexDigit >= 65 && hexDigit <= 70) {
+ // A..F
+ val += (hexDigit - 55) * (1 << (4 * (len - 1 - i)));
+ } else if (hexDigit >= 97 && hexDigit <= 102) {
+ // a..f
+ val += (hexDigit - 87) * (1 << (4 * (len - 1 - i)));
+ } else {
+ throw throw new FormatException("Bad hexadecimal value");
+ }
+ }
+
+ return val;
+ }
+
+ static String convertToHexString(int r, int g, int b, [num a]) {
+ String rHex = Color._numAs2DigitHex(Color._clamp(r, 0, 255));
+ String gHex = Color._numAs2DigitHex(Color._clamp(g, 0, 255));
+ String bHex = Color._numAs2DigitHex(Color._clamp(b, 0, 255));
+ String aHex = (a != null) ?
+ Color._numAs2DigitHex((Color._clamp(a, 0, 1) * 255).round()) : "";
+
+ // TODO(terry) 15.toRadixString(16) return 'F' on Dartium not f as in JS.
+ // bug: <http://code.google.com/p/dart/issues/detail?id=2670>
+ return "$aHex$rHex$gHex$bHex".toLowerCase();
+ }
+
+ static String _numAs2DigitHex(num v) {
+ // TODO(terry): v.toInt().toRadixString instead of v.toRadixString
+ // Bug <http://code.google.com/p/dart/issues/detail?id=2671>.
+ String hex = v.toInt().toRadixString(16);
+ if (hex.length == 1) {
+ hex = "0${hex}";
+ }
+ return hex;
+ }
+
+ static num _clamp(num value, num min, num max) =>
+ math.max(math.min(max, value), min);
+
+ /**
+ * Change the tint (make color lighter) or shade (make color darker) of all
+ * parts of [rgba] (r, g and b). The [amount] is percentage darker between
+ * -1 to 0 for darker and 0 to 1 for lighter; '0' is no change. The [amount]
+ * will darken or lighten the rgb values; it will not change the alpha value.
+ * If [amount] is outside of the value -1 to +1 then [amount] is changed to
+ * either the min or max direction -1 or 1.
+ *
+ * Darker will approach the color #000000 (black) and lighter will approach
+ * the color #ffffff (white).
+ */
+ static Rgba _createNewTintShadeFromRgba(Rgba rgba, num amount) {
+ int r, g, b;
+ num tintShade = Color._clamp(amount, -1, 1);
+ if (amount < 0 && rgba.r == 255 && rgba.g == 255 && rgba.b == 255) {
+ // TODO(terry): See TODO in _changeTintShadeColor; eliminate this test
+ // by converting to HSL and adjust lightness although this
+ // is fastest lighter/darker algorithm.
+ // Darkening white special handling.
+ r = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
+ g = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
+ b = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
+ } else {
+ // All other colors then darkening white go here.
+ r = Color._changeTintShadeColor(rgba.r, tintShade).round().toInt();
+ g = Color._changeTintShadeColor(rgba.g, tintShade).round().toInt();
+ b = Color._changeTintShadeColor(rgba.b, tintShade).round().toInt();
+ }
+ return new Rgba(r, g, b, rgba.a);
+ }
+
+ // TODO(terry): This does an okay lighter/darker; better would be convert to
+ // HSL then change the lightness.
+ /**
+ * The parameter [v] is the color to change (r, g, or b) in the range '0' to
+ * '255'. The parameter [delta] is a number between '-1' and '1'. A value
+ * between '-1' and '0' is darker and a value between '0' and '1' is lighter
+ * ('0' imples no change).
+ */
+ static num _changeTintShadeColor(num v, num delta) =>
+ Color._clamp(((1 - delta) * v + (delta * 255)).round(), 0, 255);
+
+ // Predefined CSS colors see <http://www.w3.org/TR/css3-color/>
+ static final Color transparent = const Color.hex("00ffffff"); // Alpha 0.0
+ static final Color aliceBlue = const Color.hex("0f08ff");
+ static final Color antiqueWhite = const Color.hex("0faebd7");
+ static final Color aqua = const Color.hex("00ffff");
+ static final Color aquaMarine = const Color.hex("7fffd4");
+ static final Color azure = const Color.hex("f0ffff");
+ static final Color beige = const Color.hex("f5f5dc");
+ static final Color bisque = const Color.hex("ffe4c4");
+ static final Color black = const Color.hex("000000");
+ static final Color blanchedAlmond = const Color.hex("ffebcd");
+ static final Color blue = const Color.hex("0000ff");
+ static final Color blueViolet = const Color.hex("8a2be2");
+ static final Color brown = const Color.hex("a52a2a");
+ static final Color burlyWood = const Color.hex("deb887");
+ static final Color cadetBlue = const Color.hex("5f9ea0");
+ static final Color chartreuse = const Color.hex("7fff00");
+ static final Color chocolate = const Color.hex("d2691e");
+ static final Color coral = const Color.hex("ff7f50");
+ static final Color cornFlowerBlue = const Color.hex("6495ed");
+ static final Color cornSilk = const Color.hex("fff8dc");
+ static final Color crimson = const Color.hex("dc143c");
+ static final Color cyan = const Color.hex("00ffff");
+ static final Color darkBlue = const Color.hex("00008b");
+ static final Color darkCyan = const Color.hex("008b8b");
+ static final Color darkGoldenRod = const Color.hex("b8860b");
+ static final Color darkGray = const Color.hex("a9a9a9");
+ static final Color darkGreen = const Color.hex("006400");
+ static final Color darkGrey = const Color.hex("a9a9a9");
+ static final Color darkKhaki = const Color.hex("bdb76b");
+ static final Color darkMagenta = const Color.hex("8b008b");
+ static final Color darkOliveGreen = const Color.hex("556b2f");
+ static final Color darkOrange = const Color.hex("ff8c00");
+ static final Color darkOrchid = const Color.hex("9932cc");
+ static final Color darkRed = const Color.hex("8b0000");
+ static final Color darkSalmon = const Color.hex("e9967a");
+ static final Color darkSeaGreen = const Color.hex("8fbc8f");
+ static final Color darkSlateBlue = const Color.hex("483d8b");
+ static final Color darkSlateGray = const Color.hex("2f4f4f");
+ static final Color darkSlateGrey = const Color.hex("2f4f4f");
+ static final Color darkTurquoise = const Color.hex("00ced1");
+ static final Color darkViolet = const Color.hex("9400d3");
+ static final Color deepPink = const Color.hex("ff1493");
+ static final Color deepSkyBlue = const Color.hex("00bfff");
+ static final Color dimGray = const Color.hex("696969");
+ static final Color dimGrey = const Color.hex("696969");
+ static final Color dodgerBlue = const Color.hex("1e90ff");
+ static final Color fireBrick = const Color.hex("b22222");
+ static final Color floralWhite = const Color.hex("fffaf0");
+ static final Color forestGreen = const Color.hex("228b22");
+ static final Color fuchsia = const Color.hex("ff00ff");
+ static final Color gainsboro = const Color.hex("dcdcdc");
+ static final Color ghostWhite = const Color.hex("f8f8ff");
+ static final Color gold = const Color.hex("ffd700");
+ static final Color goldenRod = const Color.hex("daa520");
+ static final Color gray = const Color.hex("808080");
+ static final Color green = const Color.hex("008000");
+ static final Color greenYellow = const Color.hex("adff2f");
+ static final Color grey = const Color.hex("808080");
+ static final Color honeydew = const Color.hex("f0fff0");
+ static final Color hotPink = const Color.hex("ff69b4");
+ static final Color indianRed = const Color.hex("cd5c5c");
+ static final Color indigo = const Color.hex("4b0082");
+ static final Color ivory = const Color.hex("fffff0");
+ static final Color khaki = const Color.hex("f0e68c");
+ static final Color lavender = const Color.hex("e6e6fa");
+ static final Color lavenderBlush = const Color.hex("fff0f5");
+ static final Color lawnGreen = const Color.hex("7cfc00");
+ static final Color lemonChiffon = const Color.hex("fffacd");
+ static final Color lightBlue = const Color.hex("add8e6");
+ static final Color lightCoral = const Color.hex("f08080");
+ static final Color lightCyan = const Color.hex("e0ffff");
+ static final Color lightGoldenRodYellow = const Color.hex("fafad2");
+ static final Color lightGray = const Color.hex("d3d3d3");
+ static final Color lightGreen = const Color.hex("90ee90");
+ static final Color lightGrey = const Color.hex("d3d3d3");
+ static final Color lightPink = const Color.hex("ffb6c1");
+ static final Color lightSalmon = const Color.hex("ffa07a");
+ static final Color lightSeaGreen = const Color.hex("20b2aa");
+ static final Color lightSkyBlue = const Color.hex("87cefa");
+ static final Color lightSlateGray = const Color.hex("778899");
+ static final Color lightSlateGrey = const Color.hex("778899");
+ static final Color lightSteelBlue = const Color.hex("b0c4de");
+ static final Color lightYellow = const Color.hex("ffffe0");
+ static final Color lime = const Color.hex("00ff00");
+ static final Color limeGreen = const Color.hex("32cd32");
+ static final Color linen = const Color.hex("faf0e6");
+ static final Color magenta = const Color.hex("ff00ff");
+ static final Color maroon = const Color.hex("800000");
+ static final Color mediumAquaMarine = const Color.hex("66cdaa");
+ static final Color mediumBlue = const Color.hex("0000cd");
+ static final Color mediumOrchid = const Color.hex("ba55d3");
+ static final Color mediumPurple = const Color.hex("9370db");
+ static final Color mediumSeaGreen = const Color.hex("3cb371");
+ static final Color mediumSlateBlue = const Color.hex("7b68ee");
+ static final Color mediumSpringGreen = const Color.hex("00fa9a");
+ static final Color mediumTurquoise = const Color.hex("48d1cc");
+ static final Color mediumVioletRed = const Color.hex("c71585");
+ static final Color midnightBlue = const Color.hex("191970");
+ static final Color mintCream = const Color.hex("f5fffa");
+ static final Color mistyRose = const Color.hex("ffe4e1");
+ static final Color moccasin = const Color.hex("ffe4b5");
+ static final Color navajoWhite = const Color.hex("ffdead");
+ static final Color navy = const Color.hex("000080");
+ static final Color oldLace = const Color.hex("fdf5e6");
+ static final Color olive = const Color.hex("808000");
+ static final Color oliveDrab = const Color.hex("6b8e23");
+ static final Color orange = const Color.hex("ffa500");
+ static final Color orangeRed = const Color.hex("ff4500");
+ static final Color orchid = const Color.hex("da70d6");
+ static final Color paleGoldenRod = const Color.hex("eee8aa");
+ static final Color paleGreen = const Color.hex("98fb98");
+ static final Color paleTurquoise = const Color.hex("afeeee");
+ static final Color paleVioletRed = const Color.hex("db7093");
+ static final Color papayaWhip = const Color.hex("ffefd5");
+ static final Color peachPuff = const Color.hex("ffdab9");
+ static final Color peru = const Color.hex("cd85ef");
+ static final Color pink = const Color.hex("ffc0cb");
+ static final Color plum = const Color.hex("dda0dd");
+ static final Color powderBlue = const Color.hex("b0e0e6");
+ static final Color purple = const Color.hex("800080");
+ static final Color red = const Color.hex("ff0000");
+ static final Color rosyBrown = const Color.hex("bc8f8f");
+ static final Color royalBlue = const Color.hex("4169e1");
+ static final Color saddleBrown = const Color.hex("8b4513");
+ static final Color salmon = const Color.hex("fa8072");
+ static final Color sandyBrown = const Color.hex("f4a460");
+ static final Color seaGreen = const Color.hex("2e8b57");
+ static final Color seashell = const Color.hex("fff5ee");
+ static final Color sienna = const Color.hex("a0522d");
+ static final Color silver = const Color.hex("c0c0c0");
+ static final Color skyBlue = const Color.hex("87ceeb");
+ static final Color slateBlue = const Color.hex("6a5acd");
+ static final Color slateGray = const Color.hex("708090");
+ static final Color slateGrey = const Color.hex("708090");
+ static final Color snow = const Color.hex("fffafa");
+ static final Color springGreen = const Color.hex("00ff7f");
+ static final Color steelBlue = const Color.hex("4682b4");
+ static final Color tan = const Color.hex("d2b48c");
+ static final Color teal = const Color.hex("008080");
+ static final Color thistle = const Color.hex("d8bfd8");
+ static final Color tomato = const Color.hex("ff6347");
+ static final Color turquoise = const Color.hex("40e0d0");
+ static final Color violet = const Color.hex("ee82ee");
+ static final Color wheat = const Color.hex("f5deb3");
+ static final Color white = const Color.hex("ffffff");
+ static final Color whiteSmoke = const Color.hex("f5f5f5");
+ static final Color yellow = const Color.hex("ffff00");
+ static final Color yellowGreen = const Color.hex("9acd32");
+}
+
+
+/**
+ * Rgba class for users that want to interact with a color as a RGBA value.
+ */
+class Rgba implements _StyleProperty, ColorBase {
+ // TODO(terry): Consider consolidating rgba to a single 32-bit int, make sure
+ // it works under JS and Dart VM.
+ final int r;
+ final int g;
+ final int b;
+ final num a;
+
+ Rgba(int red, int green, int blue, [num alpha]) :
+ this.r = Color._clamp(red, 0, 255),
+ this.g = Color._clamp(green, 0, 255),
+ this.b = Color._clamp(blue, 0, 255),
+ this.a = (alpha != null) ? Color._clamp(alpha, 0, 1) : alpha;
+
+ factory Rgba.fromString(String hexValue) =>
+ new Color.css("#${Color._convertCssToArgb(hexValue)}").rgba;
+
+ factory Rgba.fromColor(Color color) => color.rgba;
+
+ factory Rgba.fromArgbValue(num value) {
+ return new Rgba(((value.toInt() & 0xff000000) >> 0x18), /* a */
+ ((value.toInt() & 0xff0000) >> 0x10), /* r */
+ ((value.toInt() & 0xff00) >> 8), /* g */
+ ((value.toInt() & 0xff))); /* b */
+ }
+
+ factory Rgba.fromHsla(Hsla hsla) {
+ // Convert to Rgba.
+ // See site <http://easyrgb.com/index.php?X=MATH> for good documentation
+ // and color conversion routines.
+
+ num h = hsla.hue;
+ num s = hsla.saturation;
+ num l = hsla.lightness;
+ num a = hsla.alpha;
+
+ int r;
+ int g;
+ int b;
+
+ if (s == 0) {
+ r = (l * 255).round().toInt();
+ g = r;
+ b = r;
+ } else {
+ num var2;
+
+ if (l < 0.5) {
+ var2 = l * (1 + s);
+ } else {
+ var2 = (l + s) - (s * l);
+ }
+ num var1 = 2 * l - var2;
+
+ r = (255 * Rgba._hueToRGB(var1, var2, h + (1/3))).round().toInt();
+ g = (255 * Rgba._hueToRGB(var1, var2, h)).round().toInt();
+ b = (255 * Rgba._hueToRGB(var1, var2, h - (1/3))).round().toInt();
+ }
+
+ return new Rgba(r, g, b, a);
+ }
+
+ static num _hueToRGB(num v1, num v2, num vH) {
+ if (vH < 0) {
+ vH += 1;
+ }
+
+ if (vH > 1) {
+ vH -= 1;
+ }
+
+ if ((6 * vH) < 1) {
+ return (v1 + (v2 - v1) * 6 * vH);
+ }
+
+ if ((2 * vH) < 1) {
+ return v2;
+ }
+
+ if ((3 * vH) < 2) {
+ return (v1 + (v2 - v1) * ((2 / 3 - vH) * 6));
+ }
+
+ return v1;
+ }
+
+ bool operator ==(Object other) => Color.equal(this, other);
+
+ String get cssExpression {
+ if (a == null) {
+ return "#${Color.convertToHexString(r, g, b)}";
+ } else {
+ return "rgba($r,$g,$b,$a)";
+ }
+ }
+
+ String toHexArgbString() => Color.convertToHexString(r, g, b, a);
+
+ int get argbValue {
+ int value = 0;
+ if (a != null) {
+ value = (a.toInt() << 0x18);
+ }
+ value += (r << 0x10);
+ value += (g << 0x08);
+ value += b;
+ }
+
+ Color get color => new Color.createRgba(r, g, b, a);
+ Hsla get hsla => new Hsla.fromRgba(this);
+
+ Rgba darker(num amount) => Color._createNewTintShadeFromRgba(this, -amount);
+ Rgba lighter(num amount) => Color._createNewTintShadeFromRgba(this, amount);
+
+ int get hashCode => toHexArgbString().hashCode;
+}
+
+
+/**
+ * Hsl class support to interact with a color as a hsl with hue, saturation, and
+ * lightness with optional alpha blending. The hue is a ratio of 360 degrees
+ * 360° = 1 or 0, (1° == (1/360)), saturation and lightness is a 0..1 fraction
+ * (1 == 100%) and alpha is a 0..1 fraction.
+ */
+class Hsla implements _StyleProperty, ColorBase {
+ final num _h; // Value from 0..1
+ final num _s; // Value from 0..1
+ final num _l; // Value from 0..1
+ final num _a; // Value from 0..1
+
+ /**
+ * [hue] is a 0..1 fraction of 360 degrees (360 == 0).
+ * [saturation] is a 0..1 fraction (100% == 1).
+ * [lightness] is a 0..1 fraction (100% == 1).
+ * [alpha] is a 0..1 fraction, alpha blending between 0..1, 1 == 100% opaque.
+ */
+ Hsla(num hue, num saturation, num lightness, [num alpha]) :
+ this._h = (hue == 1) ? 0 : Color._clamp(hue, 0, 1),
+ this._s = Color._clamp(saturation, 0, 1),
+ this._l = Color._clamp(lightness, 0, 1),
+ this._a = (alpha != null) ? Color._clamp(alpha, 0, 1) : alpha;
+
+ factory Hsla.fromString(String hexValue) {
+ Rgba rgba = new Color.css("#${Color._convertCssToArgb(hexValue)}").rgba;
+ return _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);
+ }
+
+ factory Hsla.fromColor(Color color) {
+ Rgba rgba = color.rgba;
+ return _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);
+ }
+
+ factory Hsla.fromArgbValue(num value) {
+ num a = (value.toInt() & 0xff000000) >> 0x18;
+ int r = (value.toInt() & 0xff0000) >> 0x10;
+ int g = (value.toInt() & 0xff00) >> 8;
+ int b = value.toInt() & 0xff;
+
+ // Convert alpha to 0..1 from (0..255).
+ if (a != null) {
+ a = double.parse((a / 255).toStringAsPrecision(2));
+ }
+
+ return _createFromRgba(r, g, b, a);
+ }
+
+ factory Hsla.fromRgba(Rgba rgba) =>
+ _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);
+
+ static Hsla _createFromRgba(num r, num g, num b, num a) {
+ // Convert RGB to hsl.
+ // See site <http://easyrgb.com/index.php?X=MATH> for good documentation
+ // and color conversion routines.
+ r /= 255;
+ g /= 255;
+ b /= 255;
+
+ // Hue, saturation and lightness.
+ num h;
+ num s;
+ num l;
+
+ num minRgb = math.min(r, math.min(g, b));
+ num maxRgb = math.max(r, math.max(g, b));
+ l = (maxRgb + minRgb) / 2;
+ if (l <= 0) {
+ return new Hsla(0, 0, l); // Black;
+ }
+
+ num vm = maxRgb - minRgb;
+ s = vm;
+ if (s > 0) {
+ s /= (l < 0.5) ? (maxRgb + minRgb) : (2 - maxRgb - minRgb);
+ } else {
+ return new Hsla(0, 0, l); // White
+ }
+
+ num r2, g2, b2;
+ r2 = (maxRgb - r) / vm;
+ g2 = (maxRgb - g) / vm;
+ b2 = (maxRgb - b) / vm;
+ if (r == maxRgb) {
+ h = (g == minRgb) ? 5.0 + b2 : 1 - g2;
+ } else if (g == maxRgb) {
+ h = (b == minRgb) ? 1 + r2 : 3 - b2;
+ } else {
+ h = (r == minRgb) ? 3 + g2 : 5 - r2;
+ }
+ h /= 6;
+
+ return new Hsla(h, s, l, a);
+ }
+
+ /**
+ * Returns 0..1 fraction (ratio of 360°, e.g. 1° == 1/360).
+ */
+ num get hue => _h;
+
+ /**
+ * Returns 0..1 fraction (1 == 100%)
+ */
+ num get saturation => _s;
+
+ /**
+ * Returns 0..1 fraction (1 == 100%).
+ */
+ num get lightness => _l;
+
+ /**
+ * Returns number as degrees 0..360.
+ */
+ num get hueDegrees => (_h * 360).round();
+
+ /**
+ * Returns number as percentage 0..100
+ */
+ num get saturationPercentage => (_s * 100).round();
+
+ /**
+ * Returns number as percentage 0..100.
+ */
+ num get lightnessPercentage => (_l * 100).round();
+
+ /**
+ * Returns number as 0..1
+ */
+ num get alpha => _a;
+
+ bool operator ==(Object other) => Color.equal(this, other);
+
+ String get cssExpression => (_a == null) ?
+ "hsl($hueDegrees,$saturationPercentage,$lightnessPercentage)" :
+ "hsla($hueDegrees,$saturationPercentage,$lightnessPercentage,$_a)";
+
+ String toHexArgbString() => new Rgba.fromHsla(this).toHexArgbString();
+
+ int get argbValue => Color.hexToInt(this.toHexArgbString());
+
+ Color get color => new Color.createHsla(_h, _s, _l, _a);
+ Rgba get rgba => new Rgba.fromHsla(this);
+
+ Hsla darker(num amount) =>
+ new Hsla.fromRgba(new Rgba.fromHsla(this).darker(amount));
+
+ Hsla lighter(num amount) =>
+ new Hsla.fromRgba(new Rgba.fromHsla(this).lighter(amount));
+
+ int get hashCode => toHexArgbString().hashCode;
+}
+
+
+/** X,Y position. */
+class PointXY implements _StyleProperty {
+ final num x, y;
+ const PointXY(this.x, this.y);
+
+ String get cssExpression {
+ // TODO(terry): TBD
+ }
+}
+
+
+// TODO(terry): Implement style and color.
+/**
+ * Supports border for measuring with layout.
+ */
+class Border implements _StyleProperty {
+ final int top, left, bottom, right;
+
+ // TODO(terry): Just like CSS, 1-arg -> set all properties, 2-args -> top and
+ // bottom are first arg, left and right are second, 3-args, and
+ // 4-args -> tlbr or trbl.
+ const Border([this.top, this.left, this.bottom, this.right]);
+
+ // TODO(terry): Consider using Size or width and height.
+ Border.uniform(num amount) :
+ top = amount, left = amount, bottom = amount, right = amount;
+
+ int get width => left + right;
+ int get height => top + bottom;
+
+ String get cssExpression {
+ return (top == left && bottom == right && top == right) ? "${left}px" :
+ "${top != null ? '$top' : '0'}px ${
+ right != null ? '$right' : '0'}px ${
+ bottom != null ? '$bottom' : '0'}px ${
+ left != null ? '$left' : '0'}px";
+ }
+}
+
+
+/** Font style constants. */
+class FontStyle {
+ /** Font style [normal] default. */
+ static const String normal = "normal";
+ /**
+ * Font style [italic] use explicity crafted italic font otherwise inclined
+ * on the fly like oblique.
+ */
+ static const String italic = "italic";
+ /**
+ * Font style [oblique] is rarely used. The normal style of a font is inclined
+ * on the fly to the right by 8-12 degrees.
+ */
+ static const String oblique = "oblique";
+}
+
+
+/** Font variant constants. */
+class FontVariant {
+ /** Font style [normal] default. */
+ static const String normal = "normal";
+ /** Font variant [smallCaps]. */
+ static const String smallCaps = "small-caps";
+}
+
+
+/** Font weight constants values 100, 200, 300, 400, 500, 600, 700, 800, 900. */
+class FontWeight {
+ /** Font weight normal [default] */
+ static const int normal = 400;
+ /** Font weight bold */
+ static const int bold = 700;
+
+ static const int wt100 = 100;
+ static const int wt200 = 200;
+ static const int wt300 = 300;
+ static const int wt400 = 400;
+ static const int wt500 = 500;
+ static const int wt600 = 600;
+ static const int wt700 = 700;
+ static const int wt800 = 800;
+ static const int wt900 = 900;
+}
+
+
+/** Generic font family names. */
+class FontGeneric {
+ /** Generic family sans-serif font (w/o serifs). */
+ static const String sansSerif = "sans-serif";
+ /** Generic family serif font. */
+ static const String serif = "serif";
+ /** Generic family fixed-width font. */
+ static const monospace = "monospace";
+ /** Generic family emulate handwriting font. */
+ static const String cursive = "cursive";
+ /** Generic family decorative font. */
+ static const String fantasy = "fantasy";
+}
+
+
+/**
+ * List of most common font families across different platforms. Use the
+ * collection names in the Font class (e.g., Font.SANS_SERIF, Font.FONT_SERIF,
+ * Font.MONOSPACE, Font.CURSIVE or Font.FANTASY). These work best on all
+ * platforms using the fonts that best match availability on each platform.
+ * See <http://www.angelfire.com/al4/rcollins/style/fonts.html> for a good
+ * description of fonts available between platforms and browsers.
+ */
+class FontFamily {
+ /** Sans-Serif font for Windows similar to Helvetica on Mac bold/italic. */
+ static const String arial = "arial";
+ /** Sans-Serif font for Windows less common already bolded. */
+ static const String arialBlack = "arial black";
+ /** Sans-Serif font for Mac since 1984, similar to Arial/Helvetica. */
+ static const String geneva = "geneva";
+ /** Sans-Serif font for Windows most readable sans-serif font for displays. */
+ static const String verdana = "verdana";
+ /** Sans-Serif font for Mac since 1984 is identical to Arial. */
+ static const String helvetica = "helvetica";
+
+ /** Serif font for Windows traditional font with “old-style” numerals. */
+ static const String georgia = "georgia";
+ /**
+ * Serif font for Mac. PCs may have the non-scalable Times use Times New
+ * Roman instead. Times is more compact than Times New Roman.
+ */
+ static const String times = "times";
+ /**
+ * Serif font for Windows most common serif font and default serif font for
+ * most browsers.
+ */
+ static const String timesNewRoman = "times new roman";
+
+ /**
+ * Monospace font for Mac/Windows most common. Scalable on Mac not scalable
+ * on Windows.
+ */
+ static const String courier = "courier";
+ /** Monospace font for Mac/Windows scalable on both platforms. */
+ static const String courierNew = "courier new";
+
+ /** Cursive font for Windows and default cursive font for IE. */
+ static const String comicSansMs = "comic sans ms";
+ /** Cursive font for Mac on Macs 2000 and newer. */
+ static const String textile = "textile";
+ /** Cursive font for older Macs. */
+ static const String appleChancery = "apple chancery";
+ /** Cursive font for some PCs. */
+ static const String zaphChancery = "zaph chancery";
+
+ /** Fantasy font on most Mac/Windows/Linux platforms. */
+ static const String impact = "impact";
+ /** Fantasy font for Windows. */
+ static const String webdings = "webdings";
+}
+
+class LineHeight {
+ final num height;
+ final bool inPixels;
+ const LineHeight(this.height, {this.inPixels : true});
+}
+
+// TODO(terry): Support @font-face fule.
+/**
+ * Font style support for size, family, weight, style, variant, and lineheight.
+ */
+class Font implements _StyleProperty {
+ /** Collection of most common sans-serif fonts in order. */
+ static const List<String> sansSerif = const [FontFamily.arial,
+ FontFamily.verdana,
+ FontFamily.geneva,
+ FontFamily.helvetica,
+ FontGeneric.sansSerif];
+
+ /** Collection of most common serif fonts in order. */
+ static const List<String> serif = const [FontFamily.georgia,
+ FontFamily.timesNewRoman,
+ FontFamily.times,
+ FontGeneric.serif];
+ /** Collection of most common monospace fonts in order. */
+ static const List<String> monospace = const [FontFamily.courierNew,
+ FontFamily.courier,
+ FontGeneric.monospace];
+ /** Collection of most common cursive fonts in order. */
+ static const List<String> cursive = const [FontFamily.textile,
+ FontFamily.appleChancery,
+ FontFamily.zaphChancery,
+ FontGeneric.fantasy];
+ /** Collection of most common fantasy fonts in order. */
+ static const List<String> fantasy = const [FontFamily.comicSansMs,
+ FontFamily.impact,
+ FontFamily.webdings,
+ FontGeneric.fantasy];
+
+ // TODO(terry): Should support the values xx-small, small, large, xx-large,
+ // etc. (mapped to a pixel sized font)?
+ /** Font size in pixels. */
+ final num size;
+
+ // TODO(terry): _family should be an immutable list, wrapper class to do this
+ // should exist in Dart.
+ /**
+ * Family specifies a list of fonts, the browser will sequentially select the
+ * the first known/supported font. There are two types of font families the
+ * family-name (e.g., arial, times, courier, etc) or the generic-family (e.g.,
+ * serif, sans-seric, etc.)
+ */
+ final List<String> family;
+
+ /** Font weight from 100, 200, 300, 400, 500, 600, 700, 800, 900 */
+ final int weight;
+
+ /** Style of a font normal, italic, oblique. */
+ final String style;
+
+ /**
+ * Font variant NORMAL (default) or SMALL_CAPS. Different set of font glyph
+ * lower case letters designed to have to fit within the font-height and
+ * weight of the corresponding lowercase letters.
+ */
+ final String variant;
+
+ final LineHeight lineHeight;
+
+ // TODO(terry): Size and computedLineHeight are in pixels. Need to figure out
+ // how to handle in other units (specified in other units) like
+ // points, inches, etc. Do we have helpers like Units.Points(12)
+ // where 12 is in points and that's converted to pixels?
+ // TODO(terry): lineHeight is computed as 1.2 although CSS_RESET is 1.0 we
+ // need to be consistent some browsers use 1 others 1.2.
+ // TODO(terry): There is a school of thought "Golden Ratio Typography".
+ // Where width to display the text is also important in computing the line
+ // height. Classic typography suggest the ratio be 1.5. See
+ // <http://www.pearsonified.com/2011/12/golden-ratio-typography.php> and
+ // <http://meyerweb.com/eric/thoughts/2008/05/06/line-height-abnormal/>.
+ /**
+ * Create a font using [size] of font in pixels, [family] name of font(s)
+ * using [FontFamily], [style] of the font using [FontStyle], [variant] using
+ * [FontVariant], and [lineHeight] extra space (leading) around the font in
+ * pixels, if not specified it's 1.2 the font size.
+ */
+ const Font({this.size, this.family, this.weight, this.style, this.variant,
+ this.lineHeight});
+
+ /**
+ * Merge the two fonts and return the result. See [Style.merge] for
+ * more information.
+ */
+ factory Font.merge(Font a, Font b) {
+ if (a == null) return b;
+ if (b == null) return a;
+ return new Font._merge(a, b);
+ }
+
+ Font._merge(Font a, Font b)
+ : size = _mergeVal(a.size, b.size),
+ family = _mergeVal(a.family, b.family),
+ weight = _mergeVal(a.weight, b.weight),
+ style = _mergeVal(a.style, b.style),
+ variant = _mergeVal(a.variant, b.variant),
+ lineHeight = _mergeVal(a.lineHeight, b.lineHeight);
+
+ /**
+ * Shorthand CSS format for font is:
+ *
+ * font-style font-variant font-weight font-size/line-height font-family
+ *
+ * The font-size and font-family values are required. If any of the other
+ * values are missing the default value is used.
+ */
+ String get cssExpression {
+ // TODO(jimhug): include variant, style, other options
+ if (weight != null) {
+ // TODO(jacobr): is this really correct for lineHeight?
+ if (lineHeight != null) {
+ return "$weight ${size}px/$lineHeightInPixels $_fontsAsString";
+ }
+ return '$weight ${size}px $_fontsAsString';
+ }
+
+ return '${size}px $_fontsAsString';
+ }
+
+ Font scale(num ratio) =>
+ new Font(size: size * ratio, family: family, weight: weight, style: style,
+ variant: variant);
+
+ /**
+ * The lineHeight, provides an indirect means to specify the leading. The
+ * leading is the difference between the font-size height and the (used)
+ * value of line height in pixels. If lineHeight is not specified it's
+ * automatically computed as 1.2 of the font size. Firefox is 1.2, Safari is
+ * ~1.2, and CSS suggest a ration from 1 to 1.2 of the font-size when
+ * computing line-height. The Font class constructor has the computation for
+ * _lineHeight.
+ */
+ num get lineHeightInPixels {
+ if (lineHeight != null) {
+ if (lineHeight.inPixels) {
+ return lineHeight.height;
+ } else {
+ return (size != null) ? lineHeight.height * size : null;
+ }
+ } else {
+ return (size != null) ? size * 1.2 : null;
+ }
+ }
+
+ int get hashCode {
+ // TODO(jimhug): Lot's of potential collisions here. List of fonts, etc.
+ return size.toInt() % family[0].hashCode;
+ }
+
+ bool operator ==(Object other) {
+ if (other is! Font) return false;
+ Font o = other;
+ return o.size == size && o.family == family && o.weight == weight &&
+ o.lineHeight == lineHeight && o.style == style && o.variant == variant;
+ }
+
+ // TODO(terry): This is fragile should probably just iterate through the list
+ // of fonts construction the font-family string.
+ /** Return fonts as a comma seperated list sans the square brackets. */
+ String get _fontsAsString {
+ String fonts = family.toString();
+ return fonts.length > 2 ? fonts.substring(1, fonts.length - 1) : "";
+ }
+}
+
+/**
+ * This class stores the sizes of the box edges in the CSS [box model][]. Each
+ * edge area is placed around the sides of the content box. The innermost area
+ * is the [Style.padding] area which has a background and surrounds the content.
+ * The content and padding area is surrounded by the [Style.border], which
+ * itself is surrounded by the transparent [Style.margin]. This box represents
+ * the eges of padding, border, or margin depending on which accessor was used
+ * to retrieve it.
+ *
+ * [box model]: https://developer.mozilla.org/en/CSS/box_model
+ */
+class BoxEdge {
+ /** The size of the left edge, or null if the style has no edge. */
+ final num left;
+
+ /** The size of the top edge, or null if the style has no edge. */
+ final num top;
+
+ /** The size of the right edge, or null if the style has no edge. */
+ final num right;
+
+ /** The size of the bottom edge, or null if the style has no edge. */
+ final num bottom;
+
+ /**
+ * Creates a box edge with the specified [left], [top], [right], and
+ * [bottom] width.
+ */
+ const BoxEdge([this.left, this.top, this.right, this.bottom]);
+
+ /**
+ * Creates a box edge with the specified [top], [right], [bottom], and
+ * [left] width. This matches the typical CSS order:
+ * <https://developer.mozilla.org/en/CSS/margin>
+ * <https://developer.mozilla.org/en/CSS/border-width>
+ * <https://developer.mozilla.org/en/CSS/padding>.
+ */
+ const BoxEdge.clockwiseFromTop(this.top, this.right, this.bottom, this.left);
+
+ /**
+ * This is a helper to creates a box edge with the same [left], [top]
+ * [right], and [bottom] widths.
+ */
+ const BoxEdge.uniform(num size)
+ : top = size, left = size, bottom = size, right = size;
+
+ /**
+ * Takes a possibly null box edge, with possibly null metrics, and fills
+ * them in with 0 instead.
+ */
+ factory BoxEdge.nonNull(BoxEdge other) {
+ if (other == null) return const BoxEdge(0, 0, 0, 0);
+ num left = other.left;
+ num top = other.top;
+ num right = other.right;
+ num bottom = other.bottom;
+ bool make = false;
+ if (left == null) {
+ make = true;
+ left = 0;
+ }
+ if (top == null) {
+ make = true;
+ top = 0;
+ }
+ if (right == null) {
+ make = true;
+ right = 0;
+ }
+ if (bottom == null) {
+ make = true;
+ bottom = 0;
+ }
+ return make ? new BoxEdge(left, top, right, bottom) : other;
+ }
+
+ /**
+ * Merge the two box edge sizes and return the result. See [Style.merge] for
+ * more information.
+ */
+ factory BoxEdge.merge(BoxEdge x, BoxEdge y) {
+ if (x == null) return y;
+ if (y == null) return x;
+ return new BoxEdge._merge(x, y);
+ }
+
+ BoxEdge._merge(BoxEdge x, BoxEdge y)
+ : left = _mergeVal(x.left, y.left),
+ top = _mergeVal(x.top, y.top),
+ right = _mergeVal(x.right, y.right),
+ bottom = _mergeVal(x.bottom, y.bottom);
+
+ /**
+ * The total size of the horizontal edges. Equal to [left] + [right], where
+ * null is interpreted as 0px.
+ */
+ num get width => (left != null ? left : 0) + (right != null ? right : 0);
+
+ /**
+ * The total size of the vertical edges. Equal to [top] + [bottom], where
+ * null is interpreted as 0px.
+ */
+ num get height => (top != null ? top : 0) + (bottom != null ? bottom : 0);
+}
+
+_mergeVal(x, y) => y != null ? y : x;
diff --git a/lib/src/token.dart b/lib/src/token.dart
new file mode 100644
index 0000000..cf9e376
--- /dev/null
+++ b/lib/src/token.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.parser;
+
+/**
+ * A single token in the Dart language.
+ */
+class Token {
+ /** A member of [TokenKind] specifying what kind of token this is. */
+ final int kind;
+
+ /** The location where this token was parsed from. */
+ final Span span;
+
+ /** The start offset of this token. */
+ int get start => span.start.offset;
+
+ /** The end offset of this token. */
+ int get end => span.end.offset;
+
+ /** Returns the source text corresponding to this [Token]. */
+ String get text => span.text;
+
+ Token(this.kind, this.span);
+
+ /** Returns a pretty representation of this token for error messages. **/
+ String toString() {
+ var kindText = TokenKind.kindToString(kind);
+ var actualText = text;
+ if (kindText != actualText) {
+ if (actualText.length > 10) {
+ actualText = '${actualText.substring(0, 8)}...';
+ }
+ return '$kindText($actualText)';
+ } else {
+ return kindText;
+ }
+ }
+}
+
+/** A token containing a parsed literal value. */
+class LiteralToken extends Token {
+ var value;
+ LiteralToken(int kind, Span span, this.value) : super(kind, span);
+}
+
+/** A token containing error information. */
+class ErrorToken extends Token {
+ String message;
+ ErrorToken(int kind, Span span, this.message) : super(kind, span);
+}
diff --git a/lib/src/tokenizer.dart b/lib/src/tokenizer.dart
new file mode 100644
index 0000000..e3cdfdc
--- /dev/null
+++ b/lib/src/tokenizer.dart
@@ -0,0 +1,409 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.parser;
+
+class Tokenizer extends TokenizerBase {
+ /** U+ prefix for unicode characters. */
+ final UNICODE_U = 'U'.codeUnitAt(0);
+ final UNICODE_LOWER_U = 'u'.codeUnitAt(0);
+ final UNICODE_PLUS = '+'.codeUnitAt(0);
+
+ final QUESTION_MARK = '?'.codeUnitAt(0);
+
+ /** CDATA keyword. */
+ final List CDATA_NAME = 'CDATA'.codeUnits;
+
+ Tokenizer(SourceFile file, String text, bool skipWhitespace,
+ [int index = 0])
+ : super(file, text, skipWhitespace, index);
+
+ Token next({unicodeRange: false}) {
+ // keep track of our starting position
+ _startIndex = _index;
+
+ int ch;
+ ch = _nextChar();
+ switch (ch) {
+ case TokenChar.NEWLINE:
+ case TokenChar.RETURN:
+ case TokenChar.SPACE:
+ case TokenChar.TAB:
+ return finishWhitespace();
+ case TokenChar.END_OF_FILE:
+ return _finishToken(TokenKind.END_OF_FILE);
+ case TokenChar.AT:
+ int peekCh = _peekChar();
+ if (TokenizerHelpers.isIdentifierStart(peekCh)) {
+ var oldIndex = _index;
+ var oldStartIndex = _startIndex;
+
+ _startIndex = _index;
+ ch = _nextChar();
+ Token ident = this.finishIdentifier(ch);
+
+ // Is it a directive?
+ int tokId = TokenKind.matchDirectives(_text, _startIndex,
+ _index - _startIndex);
+ if (tokId == -1) {
+ // No, is it a margin directive?
+ tokId = TokenKind.matchMarginDirectives(_text, _startIndex,
+ _index - _startIndex);
+ }
+
+ if (tokId != -1) {
+ return _finishToken(tokId);
+ } else {
+ // Didn't find a CSS directive or margin directive so the @name is
+ // probably the Less definition '@name: value_variable_definition'.
+ _startIndex = oldStartIndex;
+ _index = oldIndex;
+ }
+ }
+ return _finishToken(TokenKind.AT);
+ case TokenChar.DOT:
+ int start = _startIndex; // Start where the dot started.
+ if (maybeEatDigit()) {
+ // looks like a number dot followed by digit(s).
+ Token number = finishNumber();
+ if (number.kind == TokenKind.INTEGER) {
+ // It's a number but it's preceeded by a dot, so make it a double.
+ _startIndex = start;
+ return _finishToken(TokenKind.DOUBLE);
+ } else {
+ // Don't allow dot followed by a double (e.g, '..1').
+ return _errorToken();
+ }
+ }
+ // It's really a dot.
+ return _finishToken(TokenKind.DOT);
+ case TokenChar.LPAREN:
+ return _finishToken(TokenKind.LPAREN);
+ case TokenChar.RPAREN:
+ return _finishToken(TokenKind.RPAREN);
+ case TokenChar.LBRACE:
+ return _finishToken(TokenKind.LBRACE);
+ case TokenChar.RBRACE:
+ return _finishToken(TokenKind.RBRACE);
+ case TokenChar.LBRACK:
+ return _finishToken(TokenKind.LBRACK);
+ case TokenChar.RBRACK:
+ if (_maybeEatChar(TokenChar.RBRACK) &&
+ _maybeEatChar(TokenChar.GREATER)) {
+ // ]]>
+ return next();
+ }
+ return _finishToken(TokenKind.RBRACK);
+ case TokenChar.HASH:
+ return _finishToken(TokenKind.HASH);
+ case TokenChar.PLUS:
+ if (maybeEatDigit()) return finishNumber();
+ return _finishToken(TokenKind.PLUS);
+ case TokenChar.MINUS:
+ if (selectorExpression || unicodeRange) {
+ // If parsing in pseudo function expression then minus is an operator
+ // not part of identifier e.g., interval value range (e.g. U+400-4ff)
+ // or minus operator in selector expression.
+ return _finishToken(TokenKind.MINUS);
+ } else if (maybeEatDigit()) {
+ return finishNumber();
+ } else if (TokenizerHelpers.isIdentifierStart(ch)) {
+ return this.finishIdentifier(ch);
+ }
+ return _finishToken(TokenKind.MINUS);
+ case TokenChar.GREATER:
+ return _finishToken(TokenKind.GREATER);
+ case TokenChar.TILDE:
+ if (_maybeEatChar(TokenChar.EQUALS)) {
+ return _finishToken(TokenKind.INCLUDES); // ~=
+ }
+ return _finishToken(TokenKind.TILDE);
+ case TokenChar.ASTERISK:
+ if (_maybeEatChar(TokenChar.EQUALS)) {
+ return _finishToken(TokenKind.SUBSTRING_MATCH); // *=
+ }
+ return _finishToken(TokenKind.ASTERISK);
+ case TokenChar.AMPERSAND:
+ return _finishToken(TokenKind.AMPERSAND);
+ case TokenChar.NAMESPACE:
+ return _finishToken(TokenKind.NAMESPACE);
+ case TokenChar.COLON:
+ return _finishToken(TokenKind.COLON);
+ case TokenChar.COMMA:
+ return _finishToken(TokenKind.COMMA);
+ case TokenChar.SEMICOLON:
+ return _finishToken(TokenKind.SEMICOLON);
+ case TokenChar.PERCENT:
+ return _finishToken(TokenKind.PERCENT);
+ case TokenChar.SINGLE_QUOTE:
+ return _finishToken(TokenKind.SINGLE_QUOTE);
+ case TokenChar.DOUBLE_QUOTE:
+ return _finishToken(TokenKind.DOUBLE_QUOTE);
+ case TokenChar.SLASH:
+ if (_maybeEatChar(TokenChar.ASTERISK)) return finishMultiLineComment();
+ return _finishToken(TokenKind.SLASH);
+ case TokenChar.LESS: // <!--
+ if (_maybeEatChar(TokenChar.BANG)) {
+ if (_maybeEatChar(TokenChar.MINUS) &&
+ _maybeEatChar(TokenChar.MINUS)) {
+ return finishMultiLineComment();
+ } else if (_maybeEatChar(TokenChar.LBRACK) &&
+ _maybeEatChar(CDATA_NAME[0]) &&
+ _maybeEatChar(CDATA_NAME[1]) &&
+ _maybeEatChar(CDATA_NAME[2]) &&
+ _maybeEatChar(CDATA_NAME[3]) &&
+ _maybeEatChar(CDATA_NAME[4]) &&
+ _maybeEatChar(TokenChar.LBRACK)) {
+ // <![CDATA[
+ return next();
+ }
+ }
+ return _finishToken(TokenKind.LESS);
+ case TokenChar.EQUALS:
+ return _finishToken(TokenKind.EQUALS);
+ case TokenChar.OR:
+ if (_maybeEatChar(TokenChar.EQUALS)) {
+ return _finishToken(TokenKind.DASH_MATCH); // |=
+ }
+ return _finishToken(TokenKind.OR);
+ case TokenChar.CARET:
+ if (_maybeEatChar(TokenChar.EQUALS)) {
+ return _finishToken(TokenKind.PREFIX_MATCH); // ^=
+ }
+ return _finishToken(TokenKind.CARET);
+ case TokenChar.DOLLAR:
+ if (_maybeEatChar(TokenChar.EQUALS)) {
+ return _finishToken(TokenKind.SUFFIX_MATCH); // $=
+ }
+ return _finishToken(TokenKind.DOLLAR);
+ case TokenChar.BANG:
+ Token tok = finishIdentifier(ch);
+ return (tok == null) ? _finishToken(TokenKind.BANG) : tok;
+ case TokenChar.BACKSLASH:
+ return _finishToken(TokenKind.BACKSLASH);
+ default:
+ if (unicodeRange) {
+ // Three types of unicode ranges:
+ // - single code point (e.g. U+416)
+ // - interval value range (e.g. U+400-4ff)
+ // - range where trailing ‘?’ characters imply ‘any digit value’
+ // (e.g. U+4??)
+ if (maybeEatHexDigit()) {
+ var t = finishHexNumber();
+ // Any question marks then it's a HEX_RANGE not HEX_NUMBER.
+ if (maybeEatQuestionMark()) finishUnicodeRange();
+ return t;
+ } else if (maybeEatQuestionMark()) {
+ // HEX_RANGE U+N???
+ return finishUnicodeRange();
+ } else {
+ return _errorToken();
+ }
+ } else if ((ch == UNICODE_U || ch == UNICODE_LOWER_U) &&
+ (_peekChar() == UNICODE_PLUS)) {
+ // Unicode range: U+uNumber[-U+uNumber]
+ // uNumber = 0..10FFFF
+ _nextChar(); // Skip +
+ _startIndex = _index; // Starts at the number
+ return _finishToken(TokenKind.UNICODE_RANGE);
+ } else if (varDef(ch)) {
+ return _finishToken(TokenKind.VAR_DEFINITION);
+ } else if (varUsage(ch)) {
+ return _finishToken(TokenKind.VAR_USAGE);
+ } else if (TokenizerHelpers.isIdentifierStart(ch)) {
+ return finishIdentifier(ch);
+ } else if (TokenizerHelpers.isDigit(ch)) {
+ return finishNumber();
+ }
+ return _errorToken();
+ }
+ }
+
+ bool varDef(int ch) {
+ return ch == 'v'.codeUnitAt(0) && _maybeEatChar('a'.codeUnitAt(0)) &&
+ _maybeEatChar('r'.codeUnitAt(0)) && _maybeEatChar('-'.codeUnitAt(0));
+ }
+
+ bool varUsage(int ch) {
+ return ch == 'v'.codeUnitAt(0) && _maybeEatChar('a'.codeUnitAt(0)) &&
+ _maybeEatChar('r'.codeUnitAt(0)) && (_peekChar() == '-'.codeUnitAt(0));
+ }
+
+ Token _errorToken([String message = null]) {
+ return _finishToken(TokenKind.ERROR);
+ }
+
+ int getIdentifierKind() {
+ // Is the identifier a unit type?
+ int tokId = TokenKind.matchUnits(_text, _startIndex, _index - _startIndex);
+ if (tokId == -1) {
+ tokId = (_text.substring(_startIndex, _index) == '!important') ?
+ TokenKind.IMPORTANT : -1;
+ }
+
+ return tokId >= 0 ? tokId : TokenKind.IDENTIFIER;
+ }
+
+ // Need to override so CSS version of isIdentifierPart is used.
+ Token finishIdentifier(int ch) {
+ while (_index < _text.length) {
+ // If parsing in pseudo function expression then minus is an operator
+ // not part of identifier.
+ var isIdentifier = selectorExpression
+ ? TokenizerHelpers.isIdentifierPartExpr(_text.codeUnitAt(_index))
+ : TokenizerHelpers.isIdentifierPart(_text.codeUnitAt(_index));
+ if (!isIdentifier) {
+ break;
+ } else {
+ _index += 1;
+ }
+ }
+
+ int kind = getIdentifierKind();
+ if (kind == TokenKind.IDENTIFIER) {
+ return _finishToken(TokenKind.IDENTIFIER);
+ } else {
+ return _finishToken(kind);
+ }
+ }
+
+ Token finishImportant() {
+
+ }
+
+ Token finishNumber() {
+ eatDigits();
+
+ if (_peekChar() == 46/*.*/) {
+ // Handle the case of 1.toString().
+ _nextChar();
+ if (TokenizerHelpers.isDigit(_peekChar())) {
+ eatDigits();
+ return _finishToken(TokenKind.DOUBLE);
+ } else {
+ _index -= 1;
+ }
+ }
+
+ return _finishToken(TokenKind.INTEGER);
+ }
+
+ bool maybeEatDigit() {
+ if (_index < _text.length
+ && TokenizerHelpers.isDigit(_text.codeUnitAt(_index))) {
+ _index += 1;
+ return true;
+ }
+ return false;
+ }
+
+ Token finishHexNumber() {
+ eatHexDigits();
+ return _finishToken(TokenKind.HEX_INTEGER);
+ }
+
+ void eatHexDigits() {
+ while (_index < _text.length) {
+ if (TokenizerHelpers.isHexDigit(_text.codeUnitAt(_index))) {
+ _index += 1;
+ } else {
+ return;
+ }
+ }
+ }
+
+ bool maybeEatHexDigit() {
+ if (_index < _text.length
+ && TokenizerHelpers.isHexDigit(_text.codeUnitAt(_index))) {
+ _index += 1;
+ return true;
+ }
+ return false;
+ }
+
+ bool maybeEatQuestionMark() {
+ if (_index < _text.length &&
+ _text.codeUnitAt(_index) == QUESTION_MARK) {
+ _index += 1;
+ return true;
+ }
+ return false;
+ }
+
+ void eatQuestionMarks() {
+ while (_index < _text.length) {
+ if (_text.codeUnitAt(_index) == QUESTION_MARK) {
+ _index += 1;
+ } else {
+ return;
+ }
+ }
+ }
+
+ Token finishUnicodeRange() {
+ eatQuestionMarks();
+ return _finishToken(TokenKind.HEX_RANGE);
+ }
+
+ Token finishMultiLineComment() {
+ while (true) {
+ int ch = _nextChar();
+ if (ch == 0) {
+ return _finishToken(TokenKind.INCOMPLETE_COMMENT);
+ } else if (ch == 42/*'*'*/) {
+ if (_maybeEatChar(47/*'/'*/)) {
+ if (_skipWhitespace) {
+ return next();
+ } else {
+ return _finishToken(TokenKind.COMMENT);
+ }
+ }
+ } else if (ch == TokenChar.MINUS) {
+ /* Check if close part of Comment Definition --> (CDC). */
+ if (_maybeEatChar(TokenChar.MINUS)) {
+ if (_maybeEatChar(TokenChar.GREATER)) {
+ if (_skipWhitespace) {
+ return next();
+ } else {
+ return _finishToken(TokenKind.HTML_COMMENT);
+ }
+ }
+ }
+ }
+ }
+ return _errorToken();
+ }
+
+}
+
+/** Static helper methods. */
+class TokenizerHelpers {
+ static bool isIdentifierStart(int c) {
+ return isIdentifierStartExpr(c) || c == 45 /*-*/;
+ }
+
+ static bool isDigit(int c) {
+ return (c >= 48/*0*/ && c <= 57/*9*/);
+ }
+
+ static bool isHexDigit(int c) {
+ return (isDigit(c) || (c >= 97/*a*/ && c <= 102/*f*/)
+ || (c >= 65/*A*/ && c <= 70/*F*/));
+ }
+
+ static bool isIdentifierPart(int c) {
+ return isIdentifierPartExpr(c) || c == 45 /*-*/;
+ }
+
+ /** Pseudo function expressions identifiers can't have a minus sign. */
+ static bool isIdentifierStartExpr(int c) {
+ return ((c >= 97/*a*/ && c <= 122/*z*/) || (c >= 65/*A*/ && c <= 90/*Z*/) ||
+ c == 95/*_*/);
+ }
+
+ /** Pseudo function expressions identifiers can't have a minus sign. */
+ static bool isIdentifierPartExpr(int c) {
+ return (isIdentifierStartExpr(c) || isDigit(c));
+ }
+}
diff --git a/lib/src/tokenizer_base.dart b/lib/src/tokenizer_base.dart
new file mode 100644
index 0000000..f33a362
--- /dev/null
+++ b/lib/src/tokenizer_base.dart
@@ -0,0 +1,445 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+// Generated by scripts/tokenizer_gen.py.
+
+part of csslib.parser;
+
+/** Tokenizer state to support look ahead for Less' nested selectors. */
+class TokenizerState {
+ final int index;
+ final int startIndex;
+ final bool selectorExpression;
+
+ TokenizerState(TokenizerBase base) :
+ this.index = base._index,
+ this.startIndex = base._startIndex,
+ this.selectorExpression = base.selectorExpression;
+}
+
+/**
+ * The base class for our tokenizer. The hand coded parts are in this file, with
+ * the generated parts in the subclass Tokenizer.
+ */
+abstract class TokenizerBase {
+ final SourceFile _file;
+ final bool _skipWhitespace;
+ final String _text;
+
+ /**
+ * Changes tokenization when in a pseudo function expression. If true then
+ * minus signs are handled as operators instead of identifiers.
+ */
+ bool selectorExpression = false;
+
+ int _index;
+ int _startIndex;
+
+ static const String _CDATA_START = '<![CDATA[';
+ static const String _CDATA_END = ']]>';
+
+ TokenizerBase(this._file, this._text, this._skipWhitespace,
+ [this._index = 0]);
+
+ Token next();
+ int getIdentifierKind();
+
+ /** Snapshot of Tokenizer scanning state. */
+ TokenizerState get mark => new TokenizerState(this);
+
+ /** Restore Tokenizer scanning state. */
+ void restore(TokenizerState markedData) {
+ _index = markedData.index;
+ _startIndex = markedData.startIndex;
+ selectorExpression = markedData.selectorExpression;
+ }
+
+ int _nextChar() {
+ if (_index < _text.length) {
+ return _text.codeUnitAt(_index++);
+ } else {
+ return 0;
+ }
+ }
+
+ int _peekChar() {
+ if (_index < _text.length) {
+ return _text.codeUnitAt(_index);
+ } else {
+ return 0;
+ }
+ }
+
+ bool _maybeEatChar(int ch) {
+ if (_index < _text.length) {
+ if (_text.codeUnitAt(_index) == ch) {
+ _index++;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ String _tokenText() {
+ if (_index < _text.length) {
+ return _text.substring(_startIndex, _index);
+ } else {
+ return _text.substring(_startIndex, _text.length);
+ }
+ }
+
+ Token _finishToken(int kind) {
+ return new Token(kind, _file.span(_startIndex, _index));
+ }
+
+ Token _errorToken([String message = null]) {
+ return new ErrorToken(
+ TokenKind.ERROR, _file.span(_startIndex, _index), message);
+ }
+
+ Token finishWhitespace() {
+ _index--;
+ while (_index < _text.length) {
+ final ch = _text.codeUnitAt(_index++);
+ if (ch == TokenChar.SPACE ||
+ ch == TokenChar.TAB ||
+ ch == TokenChar.RETURN) {
+ // do nothing
+ } else if (ch == TokenChar.NEWLINE) {
+ if (!_skipWhitespace) {
+ return _finishToken(TokenKind.WHITESPACE); // note the newline?
+ }
+ } else {
+ _index--;
+ if (_skipWhitespace) {
+ return next();
+ } else {
+ return _finishToken(TokenKind.WHITESPACE);
+ }
+ }
+
+ }
+ return _finishToken(TokenKind.END_OF_FILE);
+ }
+
+ Token finishSingleLineComment() {
+ while (true) {
+ int ch = _nextChar();
+ if (ch == 0 || ch == TokenChar.NEWLINE || ch == TokenChar.RETURN) {
+ if (_skipWhitespace) {
+ return next();
+ } else {
+ return _finishToken(TokenKind.COMMENT);
+ }
+ }
+ }
+ }
+
+ Token finishMultiLineComment() {
+ int nesting = 1;
+ do {
+ int ch = _nextChar();
+ if (ch == 0) {
+ return _errorToken();
+ } else if (ch == TokenChar.ASTERISK) {
+ if (_maybeEatChar(TokenChar.SLASH)) {
+ nesting--;
+ }
+ } else if (ch == TokenChar.SLASH) {
+ if (_maybeEatChar(TokenChar.ASTERISK)) {
+ nesting++;
+ }
+ }
+ } while (nesting > 0);
+
+ if (_skipWhitespace) {
+ return next();
+ } else {
+ return _finishToken(TokenKind.COMMENT);
+ }
+ }
+
+ void eatDigits() {
+ while (_index < _text.length) {
+ if (TokenizerHelpers.isDigit(_text.codeUnitAt(_index))) {
+ _index++;
+ } else {
+ return;
+ }
+ }
+ }
+
+ static int _hexDigit(int c) {
+ if(c >= 48/*0*/ && c <= 57/*9*/) {
+ return c - 48;
+ } else if (c >= 97/*a*/ && c <= 102/*f*/) {
+ return c - 87;
+ } else if (c >= 65/*A*/ && c <= 70/*F*/) {
+ return c - 55;
+ } else {
+ return -1;
+ }
+ }
+
+ int readHex([int hexLength]) {
+ int maxIndex;
+ if (hexLength == null) {
+ maxIndex = _text.length - 1;
+ } else {
+ // TODO(jimhug): What if this is too long?
+ maxIndex = _index + hexLength;
+ if (maxIndex >= _text.length) return -1;
+ }
+ var result = 0;
+ while (_index < maxIndex) {
+ final digit = _hexDigit(_text.codeUnitAt(_index));
+ if (digit == -1) {
+ if (hexLength == null) {
+ return result;
+ } else {
+ return -1;
+ }
+ }
+ _hexDigit(_text.codeUnitAt(_index));
+ // Multiply by 16 rather than shift by 4 since that will result in a
+ // correct value for numbers that exceed the 32 bit precision of JS
+ // 'integers'.
+ // TODO: Figure out a better solution to integer truncation. Issue 638.
+ result = (result * 16) + digit;
+ _index++;
+ }
+
+ return result;
+ }
+
+ Token finishNumber() {
+ eatDigits();
+
+ if (_peekChar() == TokenChar.DOT) {
+ // Handle the case of 1.toString().
+ _nextChar();
+ if (TokenizerHelpers.isDigit(_peekChar())) {
+ eatDigits();
+ return finishNumberExtra(TokenKind.DOUBLE);
+ } else {
+ _index--;
+ }
+ }
+
+ return finishNumberExtra(TokenKind.INTEGER);
+ }
+
+ Token finishNumberExtra(int kind) {
+ if (_maybeEatChar(101/*e*/) || _maybeEatChar(69/*E*/)) {
+ kind = TokenKind.DOUBLE;
+ _maybeEatChar(TokenKind.MINUS);
+ _maybeEatChar(TokenKind.PLUS);
+ eatDigits();
+ }
+ if (_peekChar() != 0 && TokenizerHelpers.isIdentifierStart(_peekChar())) {
+ _nextChar();
+ return _errorToken("illegal character in number");
+ }
+
+ return _finishToken(kind);
+ }
+
+ Token _makeStringToken(List<int> buf, bool isPart) {
+ final s = new String.fromCharCodes(buf);
+ final kind = isPart ? TokenKind.STRING_PART : TokenKind.STRING;
+ return new LiteralToken(kind, _file.span(_startIndex, _index), s);
+ }
+
+ Token makeIEFilter(int start, int end) {
+ var filter = _text.substring(start, end);
+ return new LiteralToken(TokenKind.STRING, _file.span(start, end), filter);
+ }
+
+ Token _makeRawStringToken(bool isMultiline) {
+ var s;
+ if (isMultiline) {
+ // Skip initial newline in multiline strings
+ int start = _startIndex + 4;
+ if (_text[start] == '\n') start++;
+ s = _text.substring(start, _index - 3);
+ } else {
+ s = _text.substring(_startIndex + 2, _index - 1);
+ }
+ return new LiteralToken(TokenKind.STRING,
+ _file.span(_startIndex, _index), s);
+ }
+
+ Token finishMultilineString(int quote) {
+ var buf = <int>[];
+ while (true) {
+ int ch = _nextChar();
+ if (ch == 0) {
+ return _errorToken();
+ } else if (ch == quote) {
+ if (_maybeEatChar(quote)) {
+ if (_maybeEatChar(quote)) {
+ return _makeStringToken(buf, false);
+ }
+ buf.add(quote);
+ }
+ buf.add(quote);
+ } else if (ch == TokenChar.BACKSLASH) {
+ var escapeVal = readEscapeSequence();
+ if (escapeVal == -1) {
+ return _errorToken("invalid hex escape sequence");
+ } else {
+ buf.add(escapeVal);
+ }
+ } else {
+ buf.add(ch);
+ }
+ }
+ }
+
+ Token _finishOpenBrace() {
+ return _finishToken(TokenKind.LBRACE);
+ }
+
+ Token _finishCloseBrace() {
+ return _finishToken(TokenKind.RBRACE);
+ }
+
+ Token finishString(int quote) {
+ if (_maybeEatChar(quote)) {
+ if (_maybeEatChar(quote)) {
+ // skip an initial newline
+ _maybeEatChar(TokenChar.NEWLINE);
+ return finishMultilineString(quote);
+ } else {
+ return _makeStringToken(new List<int>(), false);
+ }
+ }
+ return finishStringBody(quote);
+ }
+
+ Token finishRawString(int quote) {
+ if (_maybeEatChar(quote)) {
+ if (_maybeEatChar(quote)) {
+ return finishMultilineRawString(quote);
+ } else {
+ return _makeStringToken(<int>[], false);
+ }
+ }
+ while (true) {
+ int ch = _nextChar();
+ if (ch == quote) {
+ return _makeRawStringToken(false);
+ } else if (ch == 0) {
+ return _errorToken();
+ }
+ }
+ }
+
+ Token finishMultilineRawString(int quote) {
+ while (true) {
+ int ch = _nextChar();
+ if (ch == 0) {
+ return _errorToken();
+ } else if (ch == quote && _maybeEatChar(quote) && _maybeEatChar(quote)) {
+ return _makeRawStringToken(true);
+ }
+ }
+ }
+
+ Token finishStringBody(int quote) {
+ var buf = new List<int>();
+ while (true) {
+ int ch = _nextChar();
+ if (ch == quote) {
+ return _makeStringToken(buf, false);
+ } else if (ch == 0) {
+ return _errorToken();
+ } else if (ch == TokenChar.BACKSLASH) {
+ var escapeVal = readEscapeSequence();
+ if (escapeVal == -1) {
+ return _errorToken("invalid hex escape sequence");
+ } else {
+ buf.add(escapeVal);
+ }
+ } else {
+ buf.add(ch);
+ }
+ }
+ }
+
+ int readEscapeSequence() {
+ final ch = _nextChar();
+ int hexValue;
+ switch (ch) {
+ case 110/*n*/:
+ return TokenChar.NEWLINE;
+ case 114/*r*/:
+ return TokenChar.RETURN;
+ case 102/*f*/:
+ return TokenChar.FF;
+ case 98/*b*/:
+ return TokenChar.BACKSPACE;
+ case 116/*t*/:
+ return TokenChar.TAB;
+ case 118/*v*/:
+ return TokenChar.FF;
+ case 120/*x*/:
+ hexValue = readHex(2);
+ break;
+ case 117/*u*/:
+ if (_maybeEatChar(TokenChar.LBRACE)) {
+ hexValue = readHex();
+ if (!_maybeEatChar(TokenChar.RBRACE)) {
+ return -1;
+ }
+ } else {
+ hexValue = readHex(4);
+ }
+ break;
+ default: return ch;
+ }
+
+ if (hexValue == -1) return -1;
+
+ // According to the Unicode standard the high and low surrogate halves
+ // used by UTF-16 (U+D800 through U+DFFF) and values above U+10FFFF
+ // are not legal Unicode values.
+ if (hexValue < 0xD800 || hexValue > 0xDFFF && hexValue <= 0xFFFF) {
+ return hexValue;
+ } else if (hexValue <= 0x10FFFF){
+ messages.error('unicode values greater than 2 bytes not implemented yet',
+ _file.span(_startIndex, _startIndex + 1));
+ return -1;
+ } else {
+ return -1;
+ }
+ }
+
+ Token finishDot() {
+ if (TokenizerHelpers.isDigit(_peekChar())) {
+ eatDigits();
+ return finishNumberExtra(TokenKind.DOUBLE);
+ } else {
+ return _finishToken(TokenKind.DOT);
+ }
+ }
+
+ Token finishIdentifier(int ch) {
+ while (_index < _text.length) {
+ if (!TokenizerHelpers.isIdentifierPart(_text.codeUnitAt(_index++))) {
+ _index--;
+ break;
+ }
+ }
+ int kind = getIdentifierKind();
+ if (kind == TokenKind.IDENTIFIER) {
+ return _finishToken(TokenKind.IDENTIFIER);
+ } else {
+ return _finishToken(kind);
+ }
+ }
+}
+
diff --git a/lib/src/tokenkind.dart b/lib/src/tokenkind.dart
new file mode 100644
index 0000000..ebe0615
--- /dev/null
+++ b/lib/src/tokenkind.dart
@@ -0,0 +1,756 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.parser;
+
+// TODO(terry): Need to be consistent with tokens either they're ASCII tokens
+// e.g., ASTERISK or they're CSS e.g., PSEUDO, COMBINATOR_*.
+class TokenKind {
+ // Common shared tokens used in TokenizerBase.
+ static const int UNUSED = 0; // Unused place holder...
+ static const int END_OF_FILE = 1; // EOF
+ static const int LPAREN = 2; // (
+ static const int RPAREN = 3; // )
+ static const int LBRACK = 4; // [
+ static const int RBRACK = 5; // ]
+ static const int LBRACE = 6; // {
+ static const int RBRACE = 7; // }
+ static const int DOT = 8; // .
+ static const int SEMICOLON = 9; // ;
+
+ // Unique tokens for CSS.
+ static const int AT = 10; // @
+ static const int HASH = 11; // #
+ static const int PLUS = 12; // +
+ static const int GREATER = 13; // >
+ static const int TILDE = 14; // ~
+ static const int ASTERISK = 15; // *
+ static const int NAMESPACE = 16; // |
+ static const int COLON = 17; // :
+ static const int PRIVATE_NAME = 18; // _ prefix private class or id
+ static const int COMMA = 19; // ,
+ static const int SPACE = 20;
+ static const int TAB = 21; // /t
+ static const int NEWLINE = 22; // /n
+ static const int RETURN = 23; // /r
+ static const int PERCENT = 24; // %
+ static const int SINGLE_QUOTE = 25; // '
+ static const int DOUBLE_QUOTE = 26; // "
+ static const int SLASH = 27; // /
+ static const int EQUALS = 28; // =
+ static const int OR = 29; // |
+ static const int CARET = 30; // ^
+ static const int DOLLAR = 31; // $
+ static const int LESS = 32; // <
+ static const int BANG = 33; // !
+ static const int MINUS = 34; // -
+ static const int BACKSLASH = 35; // \
+ static const int AMPERSAND = 36; // &
+
+ // WARNING: Tokens from this point and above must have the corresponding ASCII
+ // character in the TokenChar list at the bottom of this file. The
+ // order of the above tokens should be the same order as TokenChar.
+
+ /** [TokenKind] representing integer tokens. */
+ static const int INTEGER = 60;
+
+ /** [TokenKind] representing hex integer tokens. */
+ static const int HEX_INTEGER = 61;
+
+ /** [TokenKind] representing double tokens. */
+ static const int DOUBLE = 62;
+
+ /** [TokenKind] representing whitespace tokens. */
+ static const int WHITESPACE = 63;
+
+ /** [TokenKind] representing comment tokens. */
+ static const int COMMENT = 64;
+
+ /** [TokenKind] representing error tokens. */
+ static const int ERROR = 65;
+
+ /** [TokenKind] representing incomplete string tokens. */
+ static const int INCOMPLETE_STRING = 66;
+
+ /** [TokenKind] representing incomplete comment tokens. */
+ static const int INCOMPLETE_COMMENT = 67;
+
+ static const int VAR_DEFINITION = 400; // var-NNN-NNN
+ static const int VAR_USAGE = 401; // var(NNN-NNN [,default])
+
+ // Synthesized Tokens (no character associated with TOKEN).
+ static const int STRING = 500;
+ static const int STRING_PART = 501;
+ static const int NUMBER = 502;
+ static const int HEX_NUMBER = 503;
+ static const int HTML_COMMENT = 504; // <!--
+ static const int IMPORTANT = 505; // !important
+ static const int CDATA_START = 506; // <![CDATA[
+ static const int CDATA_END = 507; // ]]>
+ // U+uNumber[-U+uNumber]
+ // uNumber = 0..10FFFF | ?[?]*
+ static const int UNICODE_RANGE = 508;
+ static const int HEX_RANGE = 509; // ? in the hex range
+ static const int IDENTIFIER = 511;
+
+ // Uniquely synthesized tokens for CSS.
+ static const int SELECTOR_EXPRESSION = 512;
+ static const int COMBINATOR_NONE = 513;
+ static const int COMBINATOR_DESCENDANT = 514; // Space combinator
+ static const int COMBINATOR_PLUS = 515; // + combinator
+ static const int COMBINATOR_GREATER = 516; // > combinator
+ static const int COMBINATOR_TILDE = 517; // ~ combinator
+
+ static const int UNARY_OP_NONE = 518; // No unary operator present.
+
+ // Attribute match types:
+ static const int INCLUDES = 530; // '~='
+ static const int DASH_MATCH = 531; // '|='
+ static const int PREFIX_MATCH = 532; // '^='
+ static const int SUFFIX_MATCH = 533; // '$='
+ static const int SUBSTRING_MATCH = 534; // '*='
+ static const int NO_MATCH = 535; // No operator.
+
+ // Unit types:
+ static const int UNIT_EM = 600;
+ static const int UNIT_EX = 601;
+ static const int UNIT_LENGTH_PX = 602;
+ static const int UNIT_LENGTH_CM = 603;
+ static const int UNIT_LENGTH_MM = 604;
+ static const int UNIT_LENGTH_IN = 605;
+ static const int UNIT_LENGTH_PT = 606;
+ static const int UNIT_LENGTH_PC = 607;
+ static const int UNIT_ANGLE_DEG = 608;
+ static const int UNIT_ANGLE_RAD = 609;
+ static const int UNIT_ANGLE_GRAD = 610;
+ static const int UNIT_ANGLE_TURN = 611;
+ static const int UNIT_TIME_MS = 612;
+ static const int UNIT_TIME_S = 613;
+ static const int UNIT_FREQ_HZ = 614;
+ static const int UNIT_FREQ_KHZ = 615;
+ static const int UNIT_PERCENT = 616;
+ static const int UNIT_FRACTION = 617;
+ static const int UNIT_RESOLUTION_DPI = 618;
+ static const int UNIT_RESOLUTION_DPCM = 619;
+ static const int UNIT_RESOLUTION_DPPX = 620;
+ static const int UNIT_CH = 621; // Measure of "0" U+0030 glyph.
+ static const int UNIT_REM = 622; // computed value ‘font-size’ on root elem.
+ static const int UNIT_VIEWPORT_VW = 623;
+ static const int UNIT_VIEWPORT_VH = 624;
+ static const int UNIT_VIEWPORT_VMIN = 625;
+ static const int UNIT_VIEWPORT_VMAX = 626;
+
+ // Directives (@nnnn)
+ static const int DIRECTIVE_NONE = 650;
+ static const int DIRECTIVE_IMPORT = 651;
+ static const int DIRECTIVE_MEDIA = 652;
+ static const int DIRECTIVE_PAGE = 653;
+ static const int DIRECTIVE_CHARSET = 654;
+ static const int DIRECTIVE_STYLET = 655;
+ static const int DIRECTIVE_KEYFRAMES = 656;
+ static const int DIRECTIVE_WEB_KIT_KEYFRAMES = 657;
+ static const int DIRECTIVE_MOZ_KEYFRAMES = 658;
+ static const int DIRECTIVE_MS_KEYFRAMES = 659;
+ static const int DIRECTIVE_O_KEYFRAMES = 660;
+ static const int DIRECTIVE_FONTFACE = 661;
+ static const int DIRECTIVE_NAMESPACE = 662;
+ static const int DIRECTIVE_HOST = 663;
+
+ // Media query operators
+ static const int MEDIA_OP_ONLY = 665; // Unary.
+ static const int MEDIA_OP_NOT = 666; // Unary.
+ static const int MEDIA_OP_AND = 667; // Binary.
+
+ // Directives inside of a @page (margin sym).
+ static const int MARGIN_DIRECTIVE_TOPLEFTCORNER = 670;
+ static const int MARGIN_DIRECTIVE_TOPLEFT = 671;
+ static const int MARGIN_DIRECTIVE_TOPCENTER = 672;
+ static const int MARGIN_DIRECTIVE_TOPRIGHT = 673;
+ static const int MARGIN_DIRECTIVE_TOPRIGHTCORNER = 674;
+ static const int MARGIN_DIRECTIVE_BOTTOMLEFTCORNER = 675;
+ static const int MARGIN_DIRECTIVE_BOTTOMLEFT = 676;
+ static const int MARGIN_DIRECTIVE_BOTTOMCENTER = 677;
+ static const int MARGIN_DIRECTIVE_BOTTOMRIGHT = 678;
+ static const int MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER = 679;
+ static const int MARGIN_DIRECTIVE_LEFTTOP = 680;
+ static const int MARGIN_DIRECTIVE_LEFTMIDDLE = 681;
+ static const int MARGIN_DIRECTIVE_LEFTBOTTOM = 682;
+ static const int MARGIN_DIRECTIVE_RIGHTTOP = 683;
+ static const int MARGIN_DIRECTIVE_RIGHTMIDDLE = 684;
+ static const int MARGIN_DIRECTIVE_RIGHTBOTTOM = 685;
+
+ // Simple selector type.
+ static const int CLASS_NAME = 700; // .class
+ static const int ELEMENT_NAME = 701; // tagName
+ static const int HASH_NAME = 702; // #elementId
+ static const int ATTRIBUTE_NAME = 703; // [attrib]
+ static const int PSEUDO_ELEMENT_NAME = 704; // ::pseudoElement
+ static const int PSEUDO_CLASS_NAME = 705; // :pseudoClass
+ static const int NEGATION = 706; // NOT
+
+ static const List<Map<int, String>> _DIRECTIVES = const [
+ const {'type': TokenKind.DIRECTIVE_IMPORT, 'value' : 'import'},
+ const {'type': TokenKind.DIRECTIVE_MEDIA, 'value' : 'media'},
+ const {'type': TokenKind.DIRECTIVE_PAGE, 'value' : 'page'},
+ const {'type': TokenKind.DIRECTIVE_CHARSET, 'value' : 'charset'},
+ const {'type': TokenKind.DIRECTIVE_STYLET, 'value' : 'stylet'},
+ const {'type': TokenKind.DIRECTIVE_KEYFRAMES, 'value' : 'keyframes'},
+ const {'type': TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES,
+ 'value' : '-webkit-keyframes'},
+ const {'type': TokenKind.DIRECTIVE_MOZ_KEYFRAMES,
+ 'value' : '-moz-keyframes'},
+ const {'type': TokenKind.DIRECTIVE_MS_KEYFRAMES, 'value' : '-ms-keyframes'},
+ const {'type': TokenKind.DIRECTIVE_O_KEYFRAMES, 'value' : '-o-keyframes'},
+ const {'type': TokenKind.DIRECTIVE_FONTFACE, 'value' : 'font-face'},
+ const {'type': TokenKind.DIRECTIVE_NAMESPACE, 'value' : 'namespace'},
+ const {'type': TokenKind.DIRECTIVE_HOST, 'value' : 'host'},
+ ];
+
+ static const List<Map<int, String>> MEDIA_OPERATORS = const [
+ const {'type': TokenKind.MEDIA_OP_ONLY, 'value' : 'only'},
+ const {'type': TokenKind.MEDIA_OP_NOT, 'value' : 'not'},
+ const {'type': TokenKind.MEDIA_OP_AND, 'value' : 'and'},
+];
+
+ static const List<Map<int, String>> MARGIN_DIRECTIVES = const [
+ const {'type': TokenKind.MARGIN_DIRECTIVE_TOPLEFTCORNER,
+ 'value' : 'top-left-corner'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_TOPLEFT,
+ 'value' : 'top-left'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_TOPCENTER,
+ 'value' : 'top-center'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_TOPRIGHT,
+ 'value' : 'top-right'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_TOPRIGHTCORNER,
+ 'value' : 'top-right-corner'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFTCORNER,
+ 'value' : 'bottom-left-corner'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFT,
+ 'value' : 'bottom-left'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_BOTTOMCENTER,
+ 'value' : 'bottom-center'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHT,
+ 'value' : 'bottom-right'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER,
+ 'value' : 'bottom-right-corner'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_LEFTTOP,
+ 'value' : 'left-top'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_LEFTMIDDLE,
+ 'value' : 'left-middle'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_LEFTBOTTOM,
+ 'value' : 'right-bottom'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_RIGHTTOP,
+ 'value' : 'right-top'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_RIGHTMIDDLE,
+ 'value' : 'right-middle'},
+ const {'type': TokenKind.MARGIN_DIRECTIVE_RIGHTBOTTOM,
+ 'value' : 'right-bottom'},
+ ];
+
+ static const List<Map> _UNITS = const [
+ const {'unit': TokenKind.UNIT_EM, 'value' : 'em'},
+ const {'unit': TokenKind.UNIT_EX, 'value' : 'ex'},
+ const {'unit': TokenKind.UNIT_LENGTH_PX, 'value' : 'px'},
+ const {'unit': TokenKind.UNIT_LENGTH_CM, 'value' : 'cm'},
+ const {'unit': TokenKind.UNIT_LENGTH_MM, 'value' : 'mm'},
+ const {'unit': TokenKind.UNIT_LENGTH_IN, 'value' : 'in'},
+ const {'unit': TokenKind.UNIT_LENGTH_PT, 'value' : 'pt'},
+ const {'unit': TokenKind.UNIT_LENGTH_PC, 'value' : 'pc'},
+ const {'unit': TokenKind.UNIT_ANGLE_DEG, 'value' : 'deg'},
+ const {'unit': TokenKind.UNIT_ANGLE_RAD, 'value' : 'rad'},
+ const {'unit': TokenKind.UNIT_ANGLE_GRAD, 'value' : 'grad'},
+ const {'unit': TokenKind.UNIT_ANGLE_TURN, 'value' : 'turn'},
+ const {'unit': TokenKind.UNIT_TIME_MS, 'value' : 'ms'},
+ const {'unit': TokenKind.UNIT_TIME_S, 'value' : 's'},
+ const {'unit': TokenKind.UNIT_FREQ_HZ, 'value' : 'hz'},
+ const {'unit': TokenKind.UNIT_FREQ_KHZ, 'value' : 'khz'},
+ const {'unit': TokenKind.UNIT_FRACTION, 'value' : 'fr'},
+ const {'unit': TokenKind.UNIT_RESOLUTION_DPI, 'value' : 'dpi'},
+ const {'unit': TokenKind.UNIT_RESOLUTION_DPCM, 'value' : 'dpcm'},
+ const {'unit': TokenKind.UNIT_RESOLUTION_DPPX, 'value' : 'dppx'},
+ const {'unit': TokenKind.UNIT_CH, 'value' : 'ch'},
+ const {'unit': TokenKind.UNIT_REM, 'value' : 'rem'},
+ const {'unit': TokenKind.UNIT_VIEWPORT_VW, 'value' : 'vw'},
+ const {'unit': TokenKind.UNIT_VIEWPORT_VH, 'value' : 'vh'},
+ const {'unit': TokenKind.UNIT_VIEWPORT_VMIN, 'value' : 'vmin'},
+ const {'unit': TokenKind.UNIT_VIEWPORT_VMAX, 'value' : 'vmax'},
+ ];
+
+ // Some more constants:
+ static const int ASCII_UPPER_A = 65; // ASCII value for uppercase A
+ static const int ASCII_UPPER_Z = 90; // ASCII value for uppercase Z
+
+ // Extended color keywords:
+ static const List<Map> _EXTENDED_COLOR_NAMES = const [
+ const {'name' : 'aliceblue', 'value' : 0xF08FF},
+ const {'name' : 'antiquewhite', 'value' : 0xFAEBD7},
+ const {'name' : 'aqua', 'value' : 0x00FFFF},
+ const {'name' : 'aquamarine', 'value' : 0x7FFFD4},
+ const {'name' : 'azure', 'value' : 0xF0FFFF},
+ const {'name' : 'beige', 'value' : 0xF5F5DC},
+ const {'name' : 'bisque', 'value' : 0xFFE4C4},
+ const {'name' : 'black', 'value' : 0x000000},
+ const {'name' : 'blanchedalmond', 'value' : 0xFFEBCD},
+ const {'name' : 'blue', 'value' : 0x0000FF},
+ const {'name' : 'blueviolet', 'value' : 0x8A2BE2},
+ const {'name' : 'brown', 'value' : 0xA52A2A},
+ const {'name' : 'burlywood', 'value' : 0xDEB887},
+ const {'name' : 'cadetblue', 'value' : 0x5F9EA0},
+ const {'name' : 'chartreuse', 'value' : 0x7FFF00},
+ const {'name' : 'chocolate', 'value' : 0xD2691E},
+ const {'name' : 'coral', 'value' : 0xFF7F50},
+ const {'name' : 'cornflowerblue', 'value' : 0x6495ED},
+ const {'name' : 'cornsilk', 'value' : 0xFFF8DC},
+ const {'name' : 'crimson', 'value' : 0xDC143C},
+ const {'name' : 'cyan', 'value' : 0x00FFFF},
+ const {'name' : 'darkblue', 'value' : 0x00008B},
+ const {'name' : 'darkcyan', 'value' : 0x008B8B},
+ const {'name' : 'darkgoldenrod', 'value' : 0xB8860B},
+ const {'name' : 'darkgray', 'value' : 0xA9A9A9},
+ const {'name' : 'darkgreen', 'value' : 0x006400},
+ const {'name' : 'darkgrey', 'value' : 0xA9A9A9},
+ const {'name' : 'darkkhaki', 'value' : 0xBDB76B},
+ const {'name' : 'darkmagenta', 'value' : 0x8B008B},
+ const {'name' : 'darkolivegreen', 'value' : 0x556B2F},
+ const {'name' : 'darkorange', 'value' : 0xFF8C00},
+ const {'name' : 'darkorchid', 'value' : 0x9932CC},
+ const {'name' : 'darkred', 'value' : 0x8B0000},
+ const {'name' : 'darksalmon', 'value' : 0xE9967A},
+ const {'name' : 'darkseagreen', 'value' : 0x8FBC8F},
+ const {'name' : 'darkslateblue', 'value' : 0x483D8B},
+ const {'name' : 'darkslategray', 'value' : 0x2F4F4F},
+ const {'name' : 'darkslategrey', 'value' : 0x2F4F4F},
+ const {'name' : 'darkturquoise', 'value' : 0x00CED1},
+ const {'name' : 'darkviolet', 'value' : 0x9400D3},
+ const {'name' : 'deeppink', 'value' : 0xFF1493},
+ const {'name' : 'deepskyblue', 'value' : 0x00BFFF},
+ const {'name' : 'dimgray', 'value' : 0x696969},
+ const {'name' : 'dimgrey', 'value' : 0x696969},
+ const {'name' : 'dodgerblue', 'value' : 0x1E90FF},
+ const {'name' : 'firebrick', 'value' : 0xB22222},
+ const {'name' : 'floralwhite', 'value' : 0xFFFAF0},
+ const {'name' : 'forestgreen', 'value' : 0x228B22},
+ const {'name' : 'fuchsia', 'value' : 0xFF00FF},
+ const {'name' : 'gainsboro', 'value' : 0xDCDCDC},
+ const {'name' : 'ghostwhite', 'value' : 0xF8F8FF},
+ const {'name' : 'gold', 'value' : 0xFFD700},
+ const {'name' : 'goldenrod', 'value' : 0xDAA520},
+ const {'name' : 'gray', 'value' : 0x808080},
+ const {'name' : 'green', 'value' : 0x008000},
+ const {'name' : 'greenyellow', 'value' : 0xADFF2F},
+ const {'name' : 'grey', 'value' : 0x808080},
+ const {'name' : 'honeydew', 'value' : 0xF0FFF0},
+ const {'name' : 'hotpink', 'value' : 0xFF69B4},
+ const {'name' : 'indianred', 'value' : 0xCD5C5C},
+ const {'name' : 'indigo', 'value' : 0x4B0082},
+ const {'name' : 'ivory', 'value' : 0xFFFFF0},
+ const {'name' : 'khaki', 'value' : 0xF0E68C},
+ const {'name' : 'lavender', 'value' : 0xE6E6FA},
+ const {'name' : 'lavenderblush', 'value' : 0xFFF0F5},
+ const {'name' : 'lawngreen', 'value' : 0x7CFC00},
+ const {'name' : 'lemonchiffon', 'value' : 0xFFFACD},
+ const {'name' : 'lightblue', 'value' : 0xADD8E6},
+ const {'name' : 'lightcoral', 'value' : 0xF08080},
+ const {'name' : 'lightcyan', 'value' : 0xE0FFFF},
+ const {'name' : 'lightgoldenrodyellow', 'value' : 0xFAFAD2},
+ const {'name' : 'lightgray', 'value' : 0xD3D3D3},
+ const {'name' : 'lightgreen', 'value' : 0x90EE90},
+ const {'name' : 'lightgrey', 'value' : 0xD3D3D3},
+ const {'name' : 'lightpink', 'value' : 0xFFB6C1},
+ const {'name' : 'lightsalmon', 'value' : 0xFFA07A},
+ const {'name' : 'lightseagreen', 'value' : 0x20B2AA},
+ const {'name' : 'lightskyblue', 'value' : 0x87CEFA},
+ const {'name' : 'lightslategray', 'value' : 0x778899},
+ const {'name' : 'lightslategrey', 'value' : 0x778899},
+ const {'name' : 'lightsteelblue', 'value' : 0xB0C4DE},
+ const {'name' : 'lightyellow', 'value' : 0xFFFFE0},
+ const {'name' : 'lime', 'value' : 0x00FF00},
+ const {'name' : 'limegreen', 'value' : 0x32CD32},
+ const {'name' : 'linen', 'value' : 0xFAF0E6},
+ const {'name' : 'magenta', 'value' : 0xFF00FF},
+ const {'name' : 'maroon', 'value' : 0x800000},
+ const {'name' : 'mediumaquamarine', 'value' : 0x66CDAA},
+ const {'name' : 'mediumblue', 'value' : 0x0000CD},
+ const {'name' : 'mediumorchid', 'value' : 0xBA55D3},
+ const {'name' : 'mediumpurple', 'value' : 0x9370DB},
+ const {'name' : 'mediumseagreen', 'value' : 0x3CB371},
+ const {'name' : 'mediumslateblue', 'value' : 0x7B68EE},
+ const {'name' : 'mediumspringgreen', 'value' : 0x00FA9A},
+ const {'name' : 'mediumturquoise', 'value' : 0x48D1CC},
+ const {'name' : 'mediumvioletred', 'value' : 0xC71585},
+ const {'name' : 'midnightblue', 'value' : 0x191970},
+ const {'name' : 'mintcream', 'value' : 0xF5FFFA},
+ const {'name' : 'mistyrose', 'value' : 0xFFE4E1},
+ const {'name' : 'moccasin', 'value' : 0xFFE4B5},
+ const {'name' : 'navajowhite', 'value' : 0xFFDEAD},
+ const {'name' : 'navy', 'value' : 0x000080},
+ const {'name' : 'oldlace', 'value' : 0xFDF5E6},
+ const {'name' : 'olive', 'value' : 0x808000},
+ const {'name' : 'olivedrab', 'value' : 0x6B8E23},
+ const {'name' : 'orange', 'value' : 0xFFA500},
+ const {'name' : 'orangered', 'value' : 0xFF4500},
+ const {'name' : 'orchid', 'value' : 0xDA70D6},
+ const {'name' : 'palegoldenrod', 'value' : 0xEEE8AA},
+ const {'name' : 'palegreen', 'value' : 0x98FB98},
+ const {'name' : 'paleturquoise', 'value' : 0xAFEEEE},
+ const {'name' : 'palevioletred', 'value' : 0xDB7093},
+ const {'name' : 'papayawhip', 'value' : 0xFFEFD5},
+ const {'name' : 'peachpuff', 'value' : 0xFFDAB9},
+ const {'name' : 'peru', 'value' : 0xCD853F},
+ const {'name' : 'pink', 'value' : 0xFFC0CB},
+ const {'name' : 'plum', 'value' : 0xDDA0DD},
+ const {'name' : 'powderblue', 'value' : 0xB0E0E6},
+ const {'name' : 'purple', 'value' : 0x800080},
+ const {'name' : 'red', 'value' : 0xFF0000},
+ const {'name' : 'rosybrown', 'value' : 0xBC8F8F},
+ const {'name' : 'royalblue', 'value' : 0x4169E1},
+ const {'name' : 'saddlebrown', 'value' : 0x8B4513},
+ const {'name' : 'salmon', 'value' : 0xFA8072},
+ const {'name' : 'sandybrown', 'value' : 0xF4A460},
+ const {'name' : 'seagreen', 'value' : 0x2E8B57},
+ const {'name' : 'seashell', 'value' : 0xFFF5EE},
+ const {'name' : 'sienna', 'value' : 0xA0522D},
+ const {'name' : 'silver', 'value' : 0xC0C0C0},
+ const {'name' : 'skyblue', 'value' : 0x87CEEB},
+ const {'name' : 'slateblue', 'value' : 0x6A5ACD},
+ const {'name' : 'slategray', 'value' : 0x708090},
+ const {'name' : 'slategrey', 'value' : 0x708090},
+ const {'name' : 'snow', 'value' : 0xFFFAFA},
+ const {'name' : 'springgreen', 'value' : 0x00FF7F},
+ const {'name' : 'steelblue', 'value' : 0x4682B4},
+ const {'name' : 'tan', 'value' : 0xD2B48C},
+ const {'name' : 'teal', 'value' : 0x008080},
+ const {'name' : 'thistle', 'value' : 0xD8BFD8},
+ const {'name' : 'tomato', 'value' : 0xFF6347},
+ const {'name' : 'turquoise', 'value' : 0x40E0D0},
+ const {'name' : 'violet', 'value' : 0xEE82EE},
+ const {'name' : 'wheat', 'value' : 0xF5DEB3},
+ const {'name' : 'white', 'value' : 0xFFFFFF},
+ const {'name' : 'whitesmoke', 'value' : 0xF5F5F5},
+ const {'name' : 'yellow', 'value' : 0xFFFF00},
+ const {'name' : 'yellowgreen', 'value' : 0x9ACD32},
+ ];
+
+ // TODO(terry): Should used Dart mirroring for parameter values and types
+ // especially for enumeration (e.g., counter's second parameter
+ // is list-style-type which is an enumerated list for ordering
+ // of a list 'circle', 'decimal', 'lower-roman', 'square', etc.
+ // see http://www.w3schools.com/cssref/pr_list-style-type.asp
+ // for list of possible values.
+
+ // List of valid CSS functions:
+ static const List<Map<String, Object>> _FUNCTIONS = const [
+ const {'name' : 'counter', 'info' : const {'params' : 2, 'expr' : false}},
+ const {'name' : 'attr', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'calc', 'info' : const {'params' : 1, 'expr' : true}},
+ const {'name' : 'min', 'info' : const {'params' : 2, 'expr' : true}},
+ const {'name' : 'max', 'info' : const {'params' : 2, 'expr' : true}},
+
+ // 2D functions:
+ const {'name' : 'translateX',
+ 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'translateY',
+ 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'translate', 'info' : const {'params' : 2, 'expr' : false}},
+ const {'name' : 'rotate', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'scaleX', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'scaleY', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'scale', 'info' : const {'params' : 2, 'expr' : false}},
+ const {'name' : 'skewX', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'skewY', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'skew', 'info' : const {'params' : 2, 'expr' : false}},
+ const {'name' : 'matrix', 'info' : const {'params' : 6, 'expr' : false}},
+
+ // 3D functions:
+ const {'name' : 'matrix3d', 'info' : const {'params' : 16, 'expr' : false}},
+ const {'name' : 'translate3d',
+ 'info' : const {'params' : 3, 'expr' : false}},
+ const {'name' : 'translateZ',
+ 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'scale3d', 'info' : const {'params' : 3, 'expr' : false}},
+ const {'name' : 'scaleZ', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'rotate3d', 'info' : const {'params' : 3, 'expr' : false}},
+ const {'name' : 'rotateX', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'rotateY', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'rotateZ', 'info' : const {'params' : 1, 'expr' : false}},
+ const {'name' : 'perspective',
+ 'info' : const {'params' : 1, 'expr' : false}},
+ ];
+
+ /**
+ * Check if name is a pre-defined CSS name. Used by error handler to report
+ * if name is unknown or used improperly.
+ */
+ static bool isPredefinedName(String name) {
+ var nameLen = name.length;
+ // TODO(terry): Add more pre-defined names (hidden, bolder, inherit, etc.).
+ if (matchUnits(name, 0, nameLen) == -1 ||
+ matchDirectives(name, 0, nameLen) == -1 ||
+ matchMarginDirectives(name, 0, nameLen) == -1 ||
+ matchColorName(name) == null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Return the token that matches the unit ident found. */
+ static int matchList(var identList, String tokenField, String text,
+ int offset, int length) {
+ for (final entry in identList) {
+ String ident = entry['value'];
+
+ if (length == ident.length) {
+ int idx = offset;
+ bool match = true;
+ for (int i = 0; i < ident.length; i++) {
+ int identChar = ident.codeUnitAt(i);
+ int char = text.codeUnitAt(idx++);
+ // Compare lowercase to lowercase then check if char is uppercase.
+ match = match && (char == identChar ||
+ ((char >= ASCII_UPPER_A && char <= ASCII_UPPER_Z) &&
+ (char + 32) == identChar));
+ if (!match) {
+ break;
+ }
+ }
+
+ if (match) {
+ // Completely matched; return the token for this unit.
+ return entry[tokenField];
+ }
+ }
+ }
+
+ return -1; // Not a unit token.
+ }
+
+ /** Return the token that matches the unit ident found. */
+ static int matchUnits(String text, int offset, int length) {
+ return matchList(_UNITS, 'unit', text, offset, length);
+ }
+
+ /** Return the token that matches the directive name found. */
+ static int matchDirectives(String text, int offset, int length) {
+ return matchList(_DIRECTIVES, 'type', text, offset, length);
+ }
+
+ /** Return the token that matches the margin directive name found. */
+ static int matchMarginDirectives(String text, int offset, int length) {
+ return matchList(MARGIN_DIRECTIVES, 'type', text, offset, length);
+ }
+
+ /** Return the token that matches the media operator found. */
+ static int matchMediaOperator(String text, int offset, int length) {
+ return matchList(MEDIA_OPERATORS, 'type', text, offset, length);
+ }
+
+ static String idToValue(var identList, int tokenId) {
+ for (var entry in identList) {
+ if (tokenId == entry['type']) {
+ return entry['value'];
+ }
+ }
+
+ return null;
+ }
+
+
+ /** Return the unit token as its pretty name. */
+ static String unitToString(int unitTokenToFind) {
+ if (unitTokenToFind == TokenKind.PERCENT) {
+ return '%';
+ } else {
+ for (final entry in _UNITS) {
+ int unit = entry['unit'];
+ if (unit == unitTokenToFind) {
+ return entry['value'];
+ }
+ }
+ }
+
+ return '<BAD UNIT>'; // Not a unit token.
+ }
+
+ /**
+ * Match color name, case insensitive match and return the associated color
+ * entry from _EXTENDED_COLOR_NAMES list, return [null] if not found.
+ */
+ static Map matchColorName(String text) {
+ var name = text.toLowerCase();
+ return _EXTENDED_COLOR_NAMES.
+ firstWhere((e) => e['name'] == name, orElse: () => null);
+ }
+
+ /** Return RGB value as [int] from a color entry in _EXTENDED_COLOR_NAMES. */
+ static int colorValue(Map entry) {
+ assert(entry != null);
+ return entry['value'];
+ }
+
+ static String hexToColorName(hexValue) {
+ for (final entry in _EXTENDED_COLOR_NAMES) {
+ if (entry['value'] == hexValue) {
+ return entry['name'];
+ }
+ }
+
+ return null;
+ }
+
+ static String decimalToHex(int number, [int minDigits = 1]) {
+ final String _HEX_DIGITS = '0123456789abcdef';
+
+ List<String> result = new List<String>();
+
+ int dividend = number >> 4;
+ int remain = number % 16;
+ result.add(_HEX_DIGITS[remain]);
+ while (dividend != 0) {
+ remain = dividend % 16;
+ dividend >>= 4;
+ result.add(_HEX_DIGITS[remain]);
+ }
+
+ StringBuffer invertResult = new StringBuffer();
+ int paddings = minDigits - result.length;
+ while (paddings-- > 0) {
+ invertResult.write('0');
+ }
+ for (int i = result.length - 1; i >= 0; i--) {
+ invertResult.write(result[i]);
+ }
+
+ return invertResult.toString();
+ }
+
+ static String kindToString(int kind) {
+ switch(kind) {
+ case TokenKind.UNUSED: return "ERROR";
+ case TokenKind.END_OF_FILE: return "end of file";
+ case TokenKind.LPAREN: return "(";
+ case TokenKind.RPAREN: return ")";
+ case TokenKind.LBRACK: return "[";
+ case TokenKind.RBRACK: return "]";
+ case TokenKind.LBRACE: return "{";
+ case TokenKind.RBRACE: return "}";
+ case TokenKind.DOT: return ".";
+ case TokenKind.SEMICOLON: return ";";
+ case TokenKind.AT: return "@";
+ case TokenKind.HASH: return "#";
+ case TokenKind.PLUS: return "+";
+ case TokenKind.GREATER: return ">";
+ case TokenKind.TILDE: return "~";
+ case TokenKind.ASTERISK: return "*";
+ case TokenKind.NAMESPACE: return "|";
+ case TokenKind.COLON: return ":";
+ case TokenKind.PRIVATE_NAME: return "_";
+ case TokenKind.COMMA: return ",";
+ case TokenKind.SPACE: return " ";
+ case TokenKind.TAB: return "\t";
+ case TokenKind.NEWLINE: return "\n";
+ case TokenKind.RETURN: return "\r";
+ case TokenKind.PERCENT: return "%";
+ case TokenKind.SINGLE_QUOTE: return "'";
+ case TokenKind.DOUBLE_QUOTE: return "\"";
+ case TokenKind.SLASH: return "/";
+ case TokenKind.EQUALS: return '=';
+ case TokenKind.OR: return '|';
+ case TokenKind.CARET: return '^';
+ case TokenKind.DOLLAR: return '\$';
+ case TokenKind.LESS: return '<';
+ case TokenKind.BANG: return '!';
+ case TokenKind.MINUS: return '-';
+ case TokenKind.BACKSLASH: return '\\';
+ default:
+ throw "Unknown TOKEN";
+ }
+ }
+
+ static bool isKindIdentifier(int kind) {
+ switch(kind) {
+ // Synthesized tokens.
+ case TokenKind.DIRECTIVE_IMPORT:
+ case TokenKind.DIRECTIVE_MEDIA:
+ case TokenKind.DIRECTIVE_PAGE:
+ case TokenKind.DIRECTIVE_CHARSET:
+ case TokenKind.DIRECTIVE_STYLET:
+ case TokenKind.DIRECTIVE_KEYFRAMES:
+ case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES:
+ case TokenKind.DIRECTIVE_MOZ_KEYFRAMES:
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ case TokenKind.DIRECTIVE_O_KEYFRAMES:
+ case TokenKind.DIRECTIVE_FONTFACE:
+ case TokenKind.DIRECTIVE_NAMESPACE:
+ case TokenKind.DIRECTIVE_HOST:
+ case TokenKind.UNIT_EM:
+ case TokenKind.UNIT_EX:
+ case TokenKind.UNIT_LENGTH_PX:
+ case TokenKind.UNIT_LENGTH_CM:
+ case TokenKind.UNIT_LENGTH_MM:
+ case TokenKind.UNIT_LENGTH_IN:
+ case TokenKind.UNIT_LENGTH_PT:
+ case TokenKind.UNIT_LENGTH_PC:
+ case TokenKind.UNIT_ANGLE_DEG:
+ case TokenKind.UNIT_ANGLE_RAD:
+ case TokenKind.UNIT_ANGLE_GRAD:
+ case TokenKind.UNIT_TIME_MS:
+ case TokenKind.UNIT_TIME_S:
+ case TokenKind.UNIT_FREQ_HZ:
+ case TokenKind.UNIT_FREQ_KHZ:
+ case TokenKind.UNIT_FRACTION:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool isIdentifier(int kind) {
+ return kind == IDENTIFIER ;
+ }
+}
+
+// Note: these names should match TokenKind names
+class TokenChar {
+ static const int UNUSED = -1;
+ static const int END_OF_FILE = 0;
+ static const int LPAREN = 0x28; // "(".codeUnitAt(0)
+ static const int RPAREN = 0x29; // ")".codeUnitAt(0)
+ static const int LBRACK = 0x5b; // "[".codeUnitAt(0)
+ static const int RBRACK = 0x5d; // "]".codeUnitAt(0)
+ static const int LBRACE = 0x7b; // "{".codeUnitAt(0)
+ static const int RBRACE = 0x7d; // "}".codeUnitAt(0)
+ static const int DOT = 0x2e; // ".".codeUnitAt(0)
+ static const int SEMICOLON = 0x3b; // ";".codeUnitAt(0)
+ static const int AT = 0x40; // "@".codeUnitAt(0)
+ static const int HASH = 0x23; // "#".codeUnitAt(0)
+ static const int PLUS = 0x2b; // "+".codeUnitAt(0)
+ static const int GREATER = 0x3e; // ">".codeUnitAt(0)
+ static const int TILDE = 0x7e; // "~".codeUnitAt(0)
+ static const int ASTERISK = 0x2a; // "*".codeUnitAt(0)
+ static const int NAMESPACE = 0x7c; // "|".codeUnitAt(0)
+ static const int COLON = 0x3a; // ":".codeUnitAt(0)
+ static const int PRIVATE_NAME = 0x5f; // "_".codeUnitAt(0)
+ static const int COMMA = 0x2c; // ",".codeUnitAt(0)
+ static const int SPACE = 0x20; // " ".codeUnitAt(0)
+ static const int TAB = 0x9; // "\t".codeUnitAt(0)
+ static const int NEWLINE = 0xa; // "\n".codeUnitAt(0)
+ static const int RETURN = 0xd; // "\r".codeUnitAt(0)
+ static const int BACKSPACE = 0x8; // "/b".codeUnitAt(0)
+ static const int FF = 0xc; // "/f".codeUnitAt(0)
+ static const int VT = 0xb; // "/v".codeUnitAt(0)
+ static const int PERCENT = 0x25; // "%".codeUnitAt(0)
+ static const int SINGLE_QUOTE = 0x27; // "'".codeUnitAt(0)
+ static const int DOUBLE_QUOTE = 0x22; // '"'.codeUnitAt(0)
+ static const int SLASH = 0x2f; // "/".codeUnitAt(0)
+ static const int EQUALS = 0x3d; // "=".codeUnitAt(0)
+ static const int OR = 0x7c; // "|".codeUnitAt(0)
+ static const int CARET = 0x5e; // "^".codeUnitAt(0)
+ static const int DOLLAR = 0x24; // "\$".codeUnitAt(0)
+ static const int LESS = 0x3c; // "<".codeUnitAt(0)
+ static const int BANG = 0x21; // "!".codeUnitAt(0)
+ static const int MINUS = 0x2d; // "-".codeUnitAt(0)
+ static const int BACKSLASH = 0x5c; // "\".codeUnitAt(0)
+ static const int AMPERSAND = 0x26; // "&".codeUnitAt(0)
+}
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
new file mode 100644
index 0000000..e8411d3
--- /dev/null
+++ b/lib/src/tree.dart
@@ -0,0 +1,1054 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.visitor;
+
+/////////////////////////////////////////////////////////////////////////
+// CSS specific types:
+/////////////////////////////////////////////////////////////////////////
+
+class Identifier extends TreeNode {
+ String name;
+
+ Identifier(this.name, Span span): super(span);
+
+ visit(VisitorBase visitor) => visitor.visitIdentifier(this);
+
+ String toString() => name;
+}
+
+class Wildcard extends TreeNode {
+ Wildcard(Span span): super(span);
+ visit(VisitorBase visitor) => visitor.visitWildcard(this);
+}
+
+class ThisOperator extends TreeNode {
+ ThisOperator(Span span): super(span);
+ visit(VisitorBase visitor) => visitor.visitThisOperator(this);
+}
+
+class Negation extends TreeNode {
+ Negation(Span span): super(span);
+ visit(VisitorBase visitor) => visitor.visitNegation(this);
+}
+
+// /* .... */
+class CssComment extends TreeNode {
+ final String comment;
+
+ CssComment(this.comment, Span span): super(span);
+ visit(VisitorBase visitor) => visitor.visitCssComment(this);
+}
+
+// CDO/CDC (Comment Definition Open <!-- and Comment Definition Close -->).
+class CommentDefinition extends CssComment {
+ CommentDefinition(String comment, Span span): super(comment, span);
+ visit(VisitorBase visitor) => visitor.visitCommentDefinition(this);
+}
+
+class SelectorGroup extends TreeNode {
+ List<Selector> _selectors;
+
+ SelectorGroup(this._selectors, Span span): super(span);
+
+ List<Selector> get selectors => _selectors;
+
+ visit(VisitorBase visitor) => visitor.visitSelectorGroup(this);
+}
+
+class Selector extends TreeNode {
+ final List<SimpleSelectorSequence> _simpleSelectorSequences;
+
+ Selector(this._simpleSelectorSequences, Span span) : super(span);
+
+ List<SimpleSelectorSequence> get simpleSelectorSequences =>
+ _simpleSelectorSequences;
+
+ add(SimpleSelectorSequence seq) => _simpleSelectorSequences.add(seq);
+
+ int get length => _simpleSelectorSequences.length;
+
+ visit(VisitorBase visitor) => visitor.visitSelector(this);
+}
+
+class SimpleSelectorSequence extends TreeNode {
+ /** +, >, ~, NONE */
+ final int _combinator;
+ final SimpleSelector _selector;
+
+ SimpleSelectorSequence(this._selector, Span span,
+ [int combinator = TokenKind.COMBINATOR_NONE])
+ : _combinator = combinator, super(span);
+
+ get simpleSelector => _selector;
+
+ bool get isCombinatorNone => _combinator == TokenKind.COMBINATOR_NONE;
+ bool get isCombinatorPlus => _combinator == TokenKind.COMBINATOR_PLUS;
+ bool get isCombinatorGreater => _combinator == TokenKind.COMBINATOR_GREATER;
+ bool get isCombinatorTilde => _combinator == TokenKind.COMBINATOR_TILDE;
+ bool get isCombinatorDescendant =>
+ _combinator == TokenKind.COMBINATOR_DESCENDANT;
+
+ String get _combinatorToString =>
+ isCombinatorDescendant ? ' ' :
+ isCombinatorPlus ? ' + ' :
+ isCombinatorGreater ? ' > ' :
+ isCombinatorTilde ? ' ~ ' : '';
+
+ visit(VisitorBase visitor) => visitor.visitSimpleSelectorSequence(this);
+}
+
+/* All other selectors (element, #id, .class, attribute, pseudo, negation,
+ * namespace, *) are derived from this selector.
+ */
+class SimpleSelector extends TreeNode {
+ final _name;
+
+ SimpleSelector(this._name, Span span) : super(span);
+
+ // Name can be an Identifier or WildCard we'll return either the name or '*'.
+ String get name => isWildcard ? '*' : isThis ? '&' : _name.name;
+
+ bool get isWildcard => _name is Wildcard;
+
+ bool get isThis => _name is ThisOperator;
+
+ visit(VisitorBase visitor) => visitor.visitSimpleSelector(this);
+}
+
+// element name
+class ElementSelector extends SimpleSelector {
+ ElementSelector(name, Span span) : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitElementSelector(this);
+}
+
+// namespace|element
+class NamespaceSelector extends SimpleSelector {
+ final _namespace; // null, Wildcard or Identifier
+
+ NamespaceSelector(this._namespace, var name, Span span) : super(name, span);
+
+ String get namespace =>
+ _namespace is Wildcard ? '*' : _namespace == null ? '' : _namespace.name;
+
+ bool get isNamespaceWildcard => _namespace is Wildcard;
+
+ SimpleSelector get nameAsSimpleSelector => _name;
+
+ visit(VisitorBase visitor) => visitor.visitNamespaceSelector(this);
+}
+
+// [attr op value]
+class AttributeSelector extends SimpleSelector {
+ final int _op;
+ final _value;
+
+ AttributeSelector(Identifier name, this._op, this._value,
+ Span span) : super(name, span);
+
+ String matchOperator() {
+ switch (_op) {
+ case TokenKind.EQUALS:
+ return '=';
+ case TokenKind.INCLUDES:
+ return '~=';
+ case TokenKind.DASH_MATCH:
+ return '|=';
+ case TokenKind.PREFIX_MATCH:
+ return '^=';
+ case TokenKind.SUFFIX_MATCH:
+ return '\$=';
+ case TokenKind.SUBSTRING_MATCH:
+ return '*=';
+ case TokenKind.NO_MATCH:
+ return '';
+ }
+ }
+
+ // Return the TokenKind for operator used by visitAttributeSelector.
+ String matchOperatorAsTokenString() {
+ switch (_op) {
+ case TokenKind.EQUALS:
+ return 'EQUALS';
+ case TokenKind.INCLUDES:
+ return 'INCLUDES';
+ case TokenKind.DASH_MATCH:
+ return 'DASH_MATCH';
+ case TokenKind.PREFIX_MATCH:
+ return 'PREFIX_MATCH';
+ case TokenKind.SUFFIX_MATCH:
+ return 'SUFFIX_MATCH';
+ case TokenKind.SUBSTRING_MATCH:
+ return 'SUBSTRING_MATCH';
+ }
+ }
+
+ String valueToString() {
+ if (_value != null) {
+ if (_value is Identifier) {
+ return _value.name;
+ } else {
+ return '"${_value}"';
+ }
+ } else {
+ return '';
+ }
+ }
+
+ visit(VisitorBase visitor) => visitor.visitAttributeSelector(this);
+}
+
+// #id
+class IdSelector extends SimpleSelector {
+ IdSelector(Identifier name, Span span) : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitIdSelector(this);
+}
+
+// .class
+class ClassSelector extends SimpleSelector {
+ ClassSelector(Identifier name, Span span) : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitClassSelector(this);
+}
+
+// :pseudoClass
+class PseudoClassSelector extends SimpleSelector {
+ PseudoClassSelector(Identifier name, Span span) : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitPseudoClassSelector(this);
+}
+
+// ::pseudoElement
+class PseudoElementSelector extends SimpleSelector {
+ PseudoElementSelector(Identifier name, Span span) : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitPseudoElementSelector(this);
+}
+
+// :pseudoClassFunction(expression)
+class PseudoClassFunctionSelector extends PseudoClassSelector {
+ SelectorExpression expression;
+
+ PseudoClassFunctionSelector(Identifier name, this.expression, Span span)
+ : super(name, span);
+ visit(VisitorBase visitor) => visitor.visitPseudoClassFunctionSelector(this);
+}
+
+// ::pseudoElementFunction(expression)
+class PseudoElementFunctionSelector extends PseudoElementSelector {
+ SelectorExpression expression;
+
+ PseudoElementFunctionSelector(Identifier name, this.expression, Span span)
+ : super(name, span);
+ visit(VisitorBase visitor) =>
+ visitor.visitPseudoElementFunctionSelector(this);
+}
+
+class SelectorExpression extends TreeNode {
+ final List<Expression> _expressions = [];
+
+ SelectorExpression(Span span): super(span);
+
+ add(Expression expression) {
+ _expressions.add(expression);
+ }
+
+ List<Expression> get expressions => _expressions;
+
+ visit(VisitorBase visitor) => visitor.visitSelectorExpression(this);
+}
+
+// :NOT(negation_arg)
+class NegationSelector extends SimpleSelector {
+ SimpleSelector negationArg;
+
+ NegationSelector(this.negationArg, Span span)
+ : super(new Negation(span), span);
+
+ visit(VisitorBase visitor) => visitor.visitNegationSelector(this);
+}
+
+class StyleSheet extends TreeNode {
+ /**
+ * Contains charset, ruleset, directives (media, page, etc.), and selectors.
+ */
+ final topLevels;
+
+ StyleSheet(this.topLevels, Span span) : super(span) {
+ for (final node in topLevels) {
+ assert(node is TopLevelProduction || node is Directive);
+ }
+ }
+
+ /** Selectors only in this tree. */
+ StyleSheet.selector(this.topLevels, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitStyleSheet(this);
+}
+
+class TopLevelProduction extends TreeNode {
+ TopLevelProduction(Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitTopLevelProduction(this);
+}
+
+class RuleSet extends TopLevelProduction {
+ final SelectorGroup _selectorGroup;
+ final DeclarationGroup _declarationGroup;
+
+ RuleSet(this._selectorGroup, this._declarationGroup, Span span) : super(span);
+
+ SelectorGroup get selectorGroup => _selectorGroup;
+ DeclarationGroup get declarationGroup => _declarationGroup;
+
+ visit(VisitorBase visitor) => visitor.visitRuleSet(this);
+}
+
+class Directive extends TreeNode {
+ Directive(Span span) : super(span);
+
+ bool get isBuiltIn => true; // Known CSS directive?
+ bool get isExtension => false; // SCSS extension?
+
+ visit(VisitorBase visitor) => visitor.visitDirective(this);
+}
+
+class ImportDirective extends Directive {
+ /** import name specified. */
+ final String import;
+
+ /** Any media queries for this import. */
+ final List<MediaQuery> mediaQueries;
+
+ ImportDirective(this.import, this.mediaQueries, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitImportDirective(this);
+}
+
+/**
+ * MediaExpression grammar:
+ * '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ */
+class MediaExpression extends TreeNode {
+ final bool andOperator;
+ final Identifier _mediaFeature;
+ final Expressions exprs;
+
+ MediaExpression(this.andOperator, this._mediaFeature, this.exprs, Span span)
+ : super(span);
+
+ String get mediaFeature => _mediaFeature.name;
+
+ visit(VisitorBase visitor) => visitor.visitMediaExpression(this);
+}
+
+/**
+ * MediaQuery grammar:
+ * : [ONLY | NOT]? S* media_type S* [ AND S* media_expression ]*
+ * | media_expression [ AND S* media_expression ]*
+ * media_type
+ * : IDENT
+ * media_expression
+ * : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ * media_feature
+ * : IDENT
+ */
+class MediaQuery extends TreeNode {
+ /** not, only or no operator. */
+ final int _mediaUnary;
+ final Identifier _mediaType;
+ final List<MediaExpression> expressions;
+
+ MediaQuery(this._mediaUnary, this._mediaType, this.expressions, Span span)
+ : super(span);
+
+ bool get hasMediaType => _mediaType != null;
+ String get mediaType => _mediaType.name;
+
+ bool get hasUnary => _mediaUnary != -1;
+ String get unary =>
+ TokenKind.idToValue(TokenKind.MEDIA_OPERATORS, _mediaUnary).toUpperCase();
+
+ visit(VisitorBase visitor) => visitor.visitMediaQuery(this);
+}
+
+class MediaDirective extends Directive {
+ List<MediaQuery> mediaQueries;
+ List<RuleSet> rulesets;
+
+ MediaDirective(this.mediaQueries, this.rulesets, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitMediaDirective(this);
+}
+
+class HostDirective extends Directive {
+ List<RuleSet> rulesets;
+
+ HostDirective(this.rulesets, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitHostDirective(this);
+}
+
+class PageDirective extends Directive {
+ final String _ident;
+ final String _pseudoPage;
+ List<DeclarationGroup> _declsMargin;
+
+ PageDirective(this._ident, this._pseudoPage, this._declsMargin,
+ Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitPageDirective(this);
+
+ bool get hasIdent => _ident != null && _ident.length > 0;
+ bool get hasPseudoPage => _pseudoPage != null && _pseudoPage.length > 0;
+}
+
+class CharsetDirective extends Directive {
+ final String charEncoding;
+
+ CharsetDirective(this.charEncoding, Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitCharsetDirective(this);
+}
+
+class KeyFrameDirective extends Directive {
+ /*
+ * Either @keyframe or keyframe prefixed with @-webkit-, @-moz-, @-ms-, @-o-.
+ */
+ final int _keyframeName;
+ final _name;
+ final List<KeyFrameBlock> _blocks;
+
+ KeyFrameDirective(this._keyframeName, this._name, Span span)
+ : _blocks = [], super(span);
+
+ add(KeyFrameBlock block) {
+ _blocks.add(block);
+ }
+
+ String get keyFrameName {
+ switch (_keyframeName) {
+ case TokenKind.DIRECTIVE_KEYFRAMES:
+ case TokenKind.DIRECTIVE_MS_KEYFRAMES:
+ return '@keyframes';
+ case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES: return '@-webkit-keyframes';
+ case TokenKind.DIRECTIVE_MOZ_KEYFRAMES: return '@-moz-keyframes';
+ case TokenKind.DIRECTIVE_O_KEYFRAMES: return '@-o-keyframes';
+ }
+ }
+
+ String get name => _name;
+
+ visit(VisitorBase visitor) => visitor.visitKeyFrameDirective(this);
+}
+
+class KeyFrameBlock extends Expression {
+ final Expressions _blockSelectors;
+ final DeclarationGroup _declarations;
+
+ KeyFrameBlock(this._blockSelectors, this._declarations, Span span)
+ : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitKeyFrameBlock(this);
+}
+
+class FontFaceDirective extends Directive {
+ final DeclarationGroup _declarations;
+
+ FontFaceDirective(this._declarations, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitFontFaceDirective(this);
+}
+
+class StyletDirective extends Directive {
+ final String _dartClassName;
+ final List<RuleSet> _rulesets;
+
+ StyletDirective(this._dartClassName, this._rulesets, Span span) : super(span);
+
+ bool get isBuiltIn => false;
+ bool get isExtension => true;
+
+ String get dartClassName => _dartClassName;
+ List<RuleSet> get rulesets => _rulesets;
+
+ visit(VisitorBase visitor) => visitor.visitStyletDirective(this);
+}
+
+class NamespaceDirective extends Directive {
+ /** Namespace prefix. */
+ final String _prefix;
+
+ /** URI associated with this namespace. */
+ final String _uri;
+
+ NamespaceDirective(this._prefix, this._uri, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitNamespaceDirective(this);
+
+ String get prefix => _prefix.length > 0 ? '$_prefix ' : '';
+}
+
+/** To support Less syntax @name: expression */
+class VarDefinitionDirective extends Directive {
+ final VarDefinition def;
+
+ VarDefinitionDirective(this.def, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitVarDefinitionDirective(this);
+}
+
+class Declaration extends TreeNode {
+ final Identifier _property;
+ final Expression _expression;
+ /** Style exposed to Dart. */
+ var _dart;
+ final bool important;
+
+ /**
+ * IE CSS hacks that can only be read by a particular IE version.
+ * 7 implies IE 7 or older property (e.g., *background: blue;)
+ * Note: IE 8 or older property (e.g., background: green\9;) is handled
+ * by IE8Term in declaration expression handling.
+ * Note: IE 6 only property with a leading underscore is a valid IDENT
+ * since an ident can start with underscore (e.g., _background: red;)
+ */
+ final bool isIE7;
+
+ Declaration(this._property, this._expression, this._dart, Span span,
+ {important: false, ie7: false})
+ : this.important = important, this.isIE7 = ie7, super(span);
+
+ String get property => isIE7 ? '*${_property.name}' : _property.name;
+ Expression get expression => _expression;
+
+ bool get hasDartStyle => _dart != null;
+ get dartStyle => _dart;
+ set dartStyle(dStyle) {
+ _dart = dStyle;
+ }
+
+ visit(VisitorBase visitor) => visitor.visitDeclaration(this);
+}
+
+// TODO(terry): Consider 2 kinds of VarDefinitions static at top-level and
+// dynamic when in a declaration. Currently, Less syntax
+// '@foo: expression' and 'var-foo: expression' in a declaration
+// are statically resolved. Better solution, if @foo or var-foo
+// are top-level are then statically resolved and var-foo in a
+// declaration group (surrounded by a selector) would be dynamic.
+class VarDefinition extends Declaration {
+ VarDefinition(Identifier definedName, Expression expr, Span span)
+ : super(definedName, expr, null, span);
+
+ String get definedName => _property.name;
+
+ set dartStyle(dStyle) { }
+
+ visit(VisitorBase visitor) => visitor.visitVarDefinition(this);
+}
+
+class DeclarationGroup extends TreeNode {
+ /** Can be either Declaration or RuleSet (if nested selector). */
+ final List _declarations;
+
+ DeclarationGroup(this._declarations, Span span) : super(span);
+
+ List get declarations => _declarations;
+
+ visit(VisitorBase visitor) => visitor.visitDeclarationGroup(this);
+}
+
+class MarginGroup extends DeclarationGroup {
+ final int margin_sym; // TokenType for for @margin sym.
+
+ MarginGroup(this.margin_sym, List<Declaration> decls, Span span)
+ : super(decls, span);
+ visit(VisitorBase visitor) => visitor.visitMarginGroup(this);
+}
+
+class VarUsage extends Expression {
+ final String name;
+ final List<Expression> defaultValues;
+
+ VarUsage(this.name, this.defaultValues, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitVarUsage(this);
+}
+
+class OperatorSlash extends Expression {
+ OperatorSlash(Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitOperatorSlash(this);
+}
+
+class OperatorComma extends Expression {
+ OperatorComma(Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitOperatorComma(this);
+}
+
+class OperatorPlus extends Expression {
+ OperatorPlus(Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitOperatorPlus(this);
+}
+
+class OperatorMinus extends Expression {
+ OperatorMinus(Span span) : super(span);
+ visit(VisitorBase visitor) => visitor.visitOperatorMinus(this);
+}
+
+class UnicodeRangeTerm extends Expression {
+ final String first;
+ final String second;
+
+ UnicodeRangeTerm(this.first, this.second, Span span) : super(span);
+
+ bool get hasSecond => second != null;
+
+ visit(VisitorBase visitor) => visitor.visitUnicodeRangeTerm(this);
+}
+
+class LiteralTerm extends Expression {
+ // TODO(terry): value and text fields can be made final once all CSS resources
+ // are copied/symlink'd in the build tool and UriVisitor in
+ // web_ui is removed.
+ var value;
+ String text;
+
+ LiteralTerm(this.value, this.text, Span span) : super(span);
+
+ visit(VisitorBase visitor) => visitor.visitLiteralTerm(this);
+}
+
+class NumberTerm extends LiteralTerm {
+ NumberTerm(value, String t, Span span) : super(value, t, span);
+ visit(VisitorBase visitor) => visitor.visitNumberTerm(this);
+}
+
+class UnitTerm extends LiteralTerm {
+ final int _unit;
+
+ UnitTerm(value, String t, Span span, this._unit) : super(value, t, span);
+
+ int get unit => _unit;
+
+ visit(VisitorBase visitor) => visitor.visitUnitTerm(this);
+
+ String unitToString() => TokenKind.unitToString(_unit);
+
+ String toString() => '$text${unitToString()}';
+}
+
+class LengthTerm extends UnitTerm {
+ LengthTerm(value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(this._unit == TokenKind.UNIT_LENGTH_PX ||
+ this._unit == TokenKind.UNIT_LENGTH_CM ||
+ this._unit == TokenKind.UNIT_LENGTH_MM ||
+ this._unit == TokenKind.UNIT_LENGTH_IN ||
+ this._unit == TokenKind.UNIT_LENGTH_PT ||
+ this._unit == TokenKind.UNIT_LENGTH_PC);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitLengthTerm(this);
+}
+
+class PercentageTerm extends LiteralTerm {
+ PercentageTerm(value, String t, Span span) : super(value, t, span);
+ visit(VisitorBase visitor) => visitor.visitPercentageTerm(this);
+}
+
+class EmTerm extends LiteralTerm {
+ EmTerm(value, String t, Span span) : super(value, t, span);
+ visit(VisitorBase visitor) => visitor.visitEmTerm(this);
+}
+
+class ExTerm extends LiteralTerm {
+ ExTerm(value, String t, Span span) : super(value, t, span);
+ visit(VisitorBase visitor) => visitor.visitExTerm(this);
+}
+
+class AngleTerm extends UnitTerm {
+ AngleTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(this._unit == TokenKind.UNIT_ANGLE_DEG ||
+ this._unit == TokenKind.UNIT_ANGLE_RAD ||
+ this._unit == TokenKind.UNIT_ANGLE_GRAD ||
+ this._unit == TokenKind.UNIT_ANGLE_TURN);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitAngleTerm(this);
+}
+
+class TimeTerm extends UnitTerm {
+ TimeTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(this._unit == TokenKind.UNIT_ANGLE_DEG ||
+ this._unit == TokenKind.UNIT_TIME_MS ||
+ this._unit == TokenKind.UNIT_TIME_S);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitTimeTerm(this);
+}
+
+class FreqTerm extends UnitTerm {
+ FreqTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(_unit == TokenKind.UNIT_FREQ_HZ || _unit == TokenKind.UNIT_FREQ_KHZ);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitFreqTerm(this);
+}
+
+class FractionTerm extends LiteralTerm {
+ FractionTerm(var value, String t, Span span) : super(value, t, span);
+
+ visit(VisitorBase visitor) => visitor.visitFractionTerm(this);
+}
+
+class UriTerm extends LiteralTerm {
+ UriTerm(String value, Span span) : super(value, value, span);
+
+ visit(VisitorBase visitor) => visitor.visitUriTerm(this);
+}
+
+class ResolutionTerm extends UnitTerm {
+ ResolutionTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(_unit == TokenKind.UNIT_RESOLUTION_DPI ||
+ _unit == TokenKind.UNIT_RESOLUTION_DPCM ||
+ _unit == TokenKind.UNIT_RESOLUTION_DPPX);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitResolutionTerm(this);
+}
+
+class ChTerm extends UnitTerm {
+ ChTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(_unit == TokenKind.UNIT_CH);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitChTerm(this);
+}
+
+class RemTerm extends UnitTerm {
+ RemTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(_unit == TokenKind.UNIT_REM);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitRemTerm(this);
+}
+
+class ViewportTerm extends UnitTerm {
+ ViewportTerm(var value, String t, Span span,
+ [int unit = TokenKind.UNIT_LENGTH_PX]) : super(value, t, span, unit) {
+ assert(_unit == TokenKind.UNIT_VIEWPORT_VW ||
+ _unit == TokenKind.UNIT_VIEWPORT_VH ||
+ _unit == TokenKind.UNIT_VIEWPORT_VMIN ||
+ _unit == TokenKind.UNIT_VIEWPORT_VMAX);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitViewportTerm(this);
+}
+
+/** Type to signal a bad hex value for HexColorTerm.value. */
+class BAD_HEX_VALUE { }
+
+class HexColorTerm extends LiteralTerm {
+ HexColorTerm(var value, String t, Span span) : super(value, t, span);
+
+ visit(VisitorBase visitor) => visitor.visitHexColorTerm(this);
+}
+
+class FunctionTerm extends LiteralTerm {
+ final Expressions _params;
+
+ FunctionTerm(var value, String t, this._params, Span span)
+ : super(value, t, span);
+
+ visit(VisitorBase visitor) => visitor.visitFunctionTerm(this);
+}
+
+/**
+ * A "\9" was encountered at the end of the expression and before a semi-colon.
+ * This is an IE trick to ignore a property or value except by IE 8 and older
+ * browsers.
+ */
+class IE8Term extends LiteralTerm {
+ IE8Term(Span span) : super('\\9', '\\9', span);
+ visit(VisitorBase visitor) => visitor.visitIE8Term(this);
+}
+
+class GroupTerm extends Expression {
+ final List<LiteralTerm> _terms;
+
+ GroupTerm(Span span) : _terms = [], super(span);
+
+ add(LiteralTerm term) {
+ _terms.add(term);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitGroupTerm(this);
+}
+
+class ItemTerm extends NumberTerm {
+ ItemTerm(var value, String t, Span span) : super(value, t, span);
+
+ visit(VisitorBase visitor) => visitor.visitItemTerm(this);
+}
+
+class Expressions extends Expression {
+ final List<Expression> expressions = [];
+
+ Expressions(Span span): super(span);
+
+ add(Expression expression) {
+ expressions.add(expression);
+ }
+
+ visit(VisitorBase visitor) => visitor.visitExpressions(this);
+}
+
+class BinaryExpression extends Expression {
+ final Token op;
+ final Expression x;
+ final Expression y;
+
+ BinaryExpression(this.op, this.x, this.y, Span span): super(span);
+
+ visit(VisitorBase visitor) => visitor.visitBinaryExpression(this);
+}
+
+class UnaryExpression extends Expression {
+ final Token op;
+ final Expression self;
+
+ UnaryExpression(this.op, this.self, Span span): super(span);
+
+ visit(VisitorBase visitor) => visitor.visitUnaryExpression(this);
+}
+
+abstract class DartStyleExpression extends TreeNode {
+ static final int unknownType = 0;
+ static final int fontStyle = 1;
+ static final int marginStyle = 2;
+ static final int borderStyle = 3;
+ static final int paddingStyle = 4;
+ static final int heightStyle = 5;
+ static final int widthStyle = 6;
+
+ final int _styleType;
+ int priority;
+
+ DartStyleExpression(this._styleType, Span span) : super(span);
+
+ /*
+ * Merges give 2 DartStyleExpression (or derived from DartStyleExpression,
+ * e.g., FontExpression, etc.) will merge if the two expressions are of the
+ * same property name (implies same exact type e.g, FontExpression).
+ */
+ merged(DartStyleExpression newDartExpr);
+
+ bool get isUnknown => _styleType == 0 || _styleType == null;
+ bool get isFont => _styleType == fontStyle;
+ bool get isMargin => _styleType == marginStyle;
+ bool get isBorder => _styleType == borderStyle;
+ bool get isPadding => _styleType == paddingStyle;
+ bool get isHeight => _styleType == heightStyle;
+ bool get isWidth => _styleType == widthStyle;
+ bool get isBoxExpression => isMargin || isBorder || isPadding;
+
+ bool isSame(DartStyleExpression other) => this._styleType == other._styleType;
+
+ visit(VisitorBase visitor) => visitor.visitDartStyleExpression(this);
+}
+
+class FontExpression extends DartStyleExpression {
+ Font font;
+
+ // font-style font-variant font-weight font-size/line-height font-family
+ FontExpression(Span span, {var size, List<String>family,
+ int weight, String style, String variant, LineHeight lineHeight})
+ : super(DartStyleExpression.fontStyle, span) {
+ // TODO(terry): Only px/pt for now need to handle all possible units to
+ // support calc expressions on units.
+ font = new Font(size : size is LengthTerm ? size.value : size,
+ family: family, weight: weight, style: style, variant: variant,
+ lineHeight: lineHeight);
+ }
+
+ merged(FontExpression newFontExpr) {
+ if (this.isFont && newFontExpr.isFont) {
+ return new FontExpression.merge(this, newFontExpr);
+ }
+
+ return null;
+ }
+
+ /**
+ * Merge the two FontExpression and return the result.
+ */
+ factory FontExpression.merge(FontExpression x, FontExpression y) {
+ return new FontExpression._merge(x, y, y.span);
+ }
+
+ FontExpression._merge(FontExpression x, FontExpression y, Span span)
+ : super(DartStyleExpression.fontStyle, span),
+ font = new Font.merge(x.font, y.font);
+
+ visit(VisitorBase visitor) => visitor.visitFontExpression(this);
+}
+
+abstract class BoxExpression extends DartStyleExpression {
+ final BoxEdge box;
+
+ BoxExpression(int styleType, Span span, this.box)
+ : super(styleType, span);
+
+ /*
+ * Merges give 2 DartStyleExpression (or derived from DartStyleExpression,
+ * e.g., FontExpression, etc.) will merge if the two expressions are of the
+ * same property name (implies same exact type e.g, FontExpression).
+ */
+ merged(BoxExpression newDartExpr);
+
+ visit(VisitorBase visitor) => visitor.visitBoxExpression(this);
+
+ String get formattedBoxEdge {
+ if (box.top == box.left && box.top == box.bottom &&
+ box.top== box.right) {
+ return '.uniform(${box.top})';
+ } else {
+ var left = box.left == null ? 0 : box.left;
+ var top = box.top == null ? 0 : box.top;
+ var right = box.right == null ? 0 : box.right;
+ var bottom = box.bottom == null ? 0 : box.bottom;
+ return '.clockwiseFromTop($top,$right,$bottom,$left)';
+ }
+ }
+}
+
+class MarginExpression extends BoxExpression {
+ // TODO(terry): Does auto for margin need to be exposed to Dart UI framework?
+ /** Margin expression ripped apart. */
+ MarginExpression(Span span, {num top, num right, num bottom, num left})
+ : super(DartStyleExpression.marginStyle, span,
+ new BoxEdge(left, top, right, bottom));
+
+ MarginExpression.boxEdge(Span span, BoxEdge box)
+ : super(DartStyleExpression.marginStyle, span, box);
+
+ merged(MarginExpression newMarginExpr) {
+ if (this.isMargin && newMarginExpr.isMargin) {
+ return new MarginExpression.merge(this, newMarginExpr);
+ }
+
+ return null;
+ }
+
+ /**
+ * Merge the two MarginExpressions and return the result.
+ */
+ factory MarginExpression.merge(MarginExpression x, MarginExpression y) {
+ return new MarginExpression._merge(x, y, y.span);
+ }
+
+ MarginExpression._merge(MarginExpression x, MarginExpression y, Span span)
+ : super(x._styleType, span, new BoxEdge.merge(x.box, y.box));
+
+ visit(VisitorBase visitor) => visitor.visitMarginExpression(this);
+}
+
+class BorderExpression extends BoxExpression {
+ /** Border expression ripped apart. */
+ BorderExpression(Span span, {num top, num right, num bottom, num left})
+ : super(DartStyleExpression.borderStyle, span,
+ new BoxEdge(left, top, right, bottom));
+
+ BorderExpression.boxEdge(Span span, BoxEdge box)
+ : super(DartStyleExpression.borderStyle, span, box);
+
+ merged(BorderExpression newBorderExpr) {
+ if (this.isBorder && newBorderExpr.isBorder) {
+ return new BorderExpression.merge(this, newBorderExpr);
+ }
+
+ return null;
+ }
+
+ /**
+ * Merge the two BorderExpression and return the result.
+ */
+ factory BorderExpression.merge(BorderExpression x, BorderExpression y) {
+ return new BorderExpression._merge(x, y, y.span);
+ }
+
+ BorderExpression._merge(BorderExpression x, BorderExpression y,
+ Span span)
+ : super(DartStyleExpression.borderStyle, span,
+ new BoxEdge.merge(x.box, y.box));
+
+ visit(VisitorBase visitor) => visitor.visitBorderExpression(this);
+}
+
+class HeightExpression extends DartStyleExpression {
+ final height;
+
+ HeightExpression(Span span, this.height)
+ : super(DartStyleExpression.heightStyle, span);
+
+ merged(HeightExpression newHeightExpr) {
+ if (this.isHeight && newHeightExpr.isHeight) {
+ return newHeightExpr;
+ }
+
+ return null;
+ }
+
+ visit(VisitorBase visitor) => visitor.visitHeightExpression(this);
+}
+
+class WidthExpression extends DartStyleExpression {
+ final width;
+
+ WidthExpression(Span span, this.width)
+ : super(DartStyleExpression.widthStyle, span);
+
+ merged(WidthExpression newWidthExpr) {
+ if (this.isWidth && newWidthExpr.isWidth) {
+ return newWidthExpr;
+ }
+
+ return null;
+ }
+
+ visit(VisitorBase visitor) => visitor.visitWidthExpression(this);
+}
+
+class PaddingExpression extends BoxExpression {
+ /** Padding expression ripped apart. */
+ PaddingExpression(Span span, {num top, num right, num bottom, num left})
+ : super(DartStyleExpression.paddingStyle, span,
+ new BoxEdge(left, top, right, bottom));
+
+ PaddingExpression.boxEdge(Span span, BoxEdge box)
+ : super(DartStyleExpression.paddingStyle, span, box);
+
+ merged(PaddingExpression newPaddingExpr) {
+ if (this.isPadding && newPaddingExpr.isPadding) {
+ return new PaddingExpression.merge(this, newPaddingExpr);
+ }
+
+ return null;
+ }
+
+ /**
+ * Merge the two PaddingExpression and return the result.
+ */
+ factory PaddingExpression.merge(PaddingExpression x, PaddingExpression y) {
+ return new PaddingExpression._merge(x, y, y.span);
+ }
+
+ PaddingExpression._merge(PaddingExpression x, PaddingExpression y, Span span)
+ : super(DartStyleExpression.paddingStyle, span,
+ new BoxEdge.merge(x.box, y.box));
+
+ visit(VisitorBase visitor) => visitor.visitPaddingExpression(this);
+}
diff --git a/lib/src/tree_base.dart b/lib/src/tree_base.dart
new file mode 100644
index 0000000..31411d6
--- /dev/null
+++ b/lib/src/tree_base.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.visitor;
+
+/**
+ * The base type for all nodes in a CSS abstract syntax tree.
+ */
+abstract class TreeNode {
+ /** The source code this [TreeNode] represents. */
+ Span span;
+
+ TreeNode(this.span) {}
+
+ /** Classic double-dispatch visitor for implementing passes. */
+ visit(VisitorBase visitor);
+
+ /** A multiline string showing the node and its children. */
+ String toDebugString() {
+ var to = new TreeOutput();
+ var tp = new _TreePrinter(to, true);
+ this.visit(tp);
+ return to.buf.toString();
+ }
+}
+
+/** The base type for expressions. */
+abstract class Expression extends TreeNode {
+ Expression(Span span): super(span);
+}
+
+/** Simple class to provide a textual dump of trees for debugging. */
+class TreeOutput {
+ int depth = 0;
+ final StringBuffer buf = new StringBuffer();
+ var printer;
+
+ void write(String s) {
+ for (int i=0; i < depth; i++) {
+ buf.write(' ');
+ }
+ buf.write(s);
+ }
+
+ void writeln(String s) {
+ write(s);
+ buf.write('\n');
+ }
+
+ void heading(String name, [span]) {
+ write(name);
+ if (span != null) {
+ buf.write(' (${span.getLocationMessage('')})');
+ }
+ buf.write('\n');
+ }
+
+ String toValue(value) {
+ if (value == null) return 'null';
+ else if (value is Identifier) return value.name;
+ else return value.toString();
+ }
+
+ void writeNode(String label, TreeNode node) {
+ write('${label}: ');
+ depth += 1;
+ if (node != null) node.visit(printer);
+ else writeln('null');
+ depth -= 1;
+ }
+
+ void writeValue(String label, value) {
+ var v = toValue(value);
+ writeln('${label}: ${v}');
+ }
+
+ void writeList(String label, List list) {
+ write('${label}: ');
+ if (list == null) {
+ buf.write('null');
+ buf.write('\n');
+ } else {
+ for (var item in list) {
+ buf.write(item.toString());
+ buf.write(', ');
+ }
+ buf.write('\n');
+ }
+ }
+
+ void writeNodeList(String label, List list) {
+ writeln('${label} [');
+ if (list != null) {
+ depth += 1;
+ for (var node in list) {
+ if (node != null) {
+ node.visit(printer);
+ } else {
+ writeln('null');
+ }
+ }
+ depth -= 1;
+ writeln(']');
+ }
+ }
+
+ String toString() => buf.toString();
+}
diff --git a/lib/src/tree_printer.dart b/lib/src/tree_printer.dart
new file mode 100644
index 0000000..24cca64
--- /dev/null
+++ b/lib/src/tree_printer.dart
@@ -0,0 +1,512 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of csslib.visitor;
+
+// TODO(terry): Enable class for debug only; when conditional imports enabled.
+
+/** Helper function to dump the CSS AST. */
+String treeToDebugString(styleSheet, [bool useSpan = false]) {
+ var to = new TreeOutput();
+ new _TreePrinter(to, useSpan)..visitTree(styleSheet);
+ return to.toString();
+}
+
+/** Tree dump for debug output of the CSS AST. */
+class _TreePrinter extends Visitor {
+ var output;
+ final bool useSpan;
+ _TreePrinter(this.output, this.useSpan) { output.printer = this; }
+
+ void visitTree(tree) => visitStylesheet(tree);
+
+ void heading(String heading, node) {
+ if (useSpan) {
+ output.heading(heading, node.span);
+ } else {
+ output.heading(heading);
+ }
+ }
+
+ void visitStylesheet(StyleSheet node) {
+ heading('Stylesheet', node);
+ output.depth++;
+ super.visitStyleSheet(node);
+ output.depth--;
+ }
+
+ void visitTopLevelProduction(TopLevelProduction node) {
+ heading('TopLevelProduction', node);
+ }
+
+ void visitDirective(Directive node) {
+ heading('Directive', node);
+ }
+
+ void visitCssComment(CssComment node) {
+ heading('Comment', node);
+ output.depth++;
+ output.writeValue('comment value', node.comment);
+ output.depth--;
+ }
+
+ void visitCommentDefinition(CommentDefinition node) {
+ heading('CommentDefinition (CDO/CDC)', node);
+ output.depth++;
+ output.writeValue('comment value', node.comment);
+ output.depth--;
+ }
+
+ void visitMediaExpression(MediaExpression node) {
+ heading('MediaExpression', node);
+ output.writeValue('feature', node.mediaFeature);
+ if (node.andOperator) output.writeValue('AND operator', '');
+ visitExpressions(node.exprs);
+ }
+
+ void visitMediaQueries(MediaQuery query) {
+ output.headeing('MediaQueries');
+ output.writeValue('unary', query.unary);
+ output.writeValue('media type', query.mediaType);
+ output.writeNodeList('media expressions', query.expressions);
+ }
+
+ void visitMediaDirective(MediaDirective node) {
+ heading('MediaDirective', node);
+ output.depth++;
+ output.writeNodeList('media queries', node.mediaQueries);
+ output.writeNodeList('rule sets', node.rulesets);
+ super.visitMediaDirective(node);
+ output.depth--;
+ }
+
+ void visitPageDirective(PageDirective node) {
+ heading('PageDirective', node);
+ output.depth++;
+ output.writeValue('pseudo page', node._pseudoPage);
+ super.visitPageDirective(node);
+ output.depth;
+ }
+
+ void visitCharsetDirective(CharsetDirective node) {
+ heading('Charset Directive', node);
+ output.writeValue('charset encoding', node.charEncoding);
+ }
+
+ void visitImportDirective(ImportDirective node) {
+ heading('ImportDirective', node);
+ output.depth++;
+ output.writeValue('import', node.import);
+ super.visitImportDirective(node);
+ output.writeNodeList('media', node.mediaQueries);
+ output.depth--;
+ }
+
+ void visitKeyFrameDirective(KeyFrameDirective node) {
+ heading('KeyFrameDirective', node);
+ output.depth++;
+ output.writeValue('keyframe', node.keyFrameName);
+ output.writeValue('name', node._name);
+ output.writeNodeList('blocks', node._blocks);
+ output.depth--;
+ }
+
+ void visitKeyFrameBlock(KeyFrameBlock node) {
+ heading('KeyFrameBlock', node);
+ output.depth++;
+ super.visitKeyFrameBlock(node);
+ output.depth--;
+ }
+
+ void visitFontFaceDirective(FontFaceDirective node) {
+ // TODO(terry): To Be Implemented
+ }
+
+ void visitStyletDirective(StyletDirective node) {
+ heading('StyletDirective', node);
+ output.writeValue('dartClassName', node._dartClassName);
+ output.depth++;
+ output.writeNodeList('rulesets', node._rulesets);
+ output.depth--;
+ }
+
+ void visitNamespaceDirective(NamespaceDirective node) {
+ heading('NamespaceDirective', node);
+ output.depth++;
+ output.writeValue('prefix', node._prefix);
+ output.writeValue('uri', node._uri);
+ output.depth--;
+ }
+
+ void visitVarDefinitionDirective(VarDefinitionDirective node) {
+ heading('Less variable definition', node);
+ visitVarDefinition(node.def);
+ }
+
+ void visitRuleSet(RuleSet node) {
+ heading('Ruleset', node);
+ output.depth++;
+ super.visitRuleSet(node);
+ output.depth--;
+ }
+
+ void visitDeclarationGroup(DeclarationGroup node) {
+ heading('DeclarationGroup', node);
+ output.depth++;
+ output.writeNodeList('declarations', node._declarations);
+ output.depth--;
+ }
+
+ void visitMarginGroup(MarginGroup node) {
+ heading('MarginGroup', node);
+ output.depth++;
+ output.writeValue('@directive', node.margin_sym);
+ output.writeNodeList('declarations', node._declarations);
+ output.depth--;
+ }
+
+ void visitDeclaration(Declaration node) {
+ heading('Declaration', node);
+ output.depth++;
+ if (node.isIE7) output.write('IE7 property');
+ output.write('property');
+ super.visitDeclaration(node);
+ output.writeNode('expression', node._expression);
+ if (node.important) {
+ output.writeValue('!important', 'true');
+ }
+ output.depth--;
+ }
+
+ void visitVarDefinition(VarDefinition node) {
+ heading('Var', node);
+ output.depth++;
+ output.write('defintion');
+ super.visitVarDefinition(node);
+ output.writeNode('expression', node._expression);
+ output.depth--;
+ }
+
+ void visitSelectorGroup(SelectorGroup node) {
+ heading('Selector Group', node);
+ output.depth++;
+ output.writeNodeList('selectors', node.selectors);
+ output.depth--;
+ }
+
+ void visitSelector(Selector node) {
+ heading('Selector', node);
+ output.depth++;
+ output.writeNodeList('simpleSelectorsSequences',
+ node._simpleSelectorSequences);
+ output.depth--;
+ }
+
+ void visitSimpleSelectorSequence(SimpleSelectorSequence node) {
+ heading('SimpleSelectorSequence', node);
+ output.depth++;
+ if (node.isCombinatorNone) {
+ output.writeValue('combinator', "NONE");
+ } else if (node.isCombinatorDescendant) {
+ output.writeValue('combinator', "descendant");
+ } else if (node.isCombinatorPlus) {
+ output.writeValue('combinator', "+");
+ } else if (node.isCombinatorGreater) {
+ output.writeValue('combinator', ">");
+ } else if (node.isCombinatorTilde) {
+ output.writeValue('combinator', "~");
+ } else {
+ output.writeValue('combinator', "ERROR UNKNOWN");
+ }
+
+ super.visitSimpleSelectorSequence(node);
+
+ output.depth--;
+ }
+
+ void visitNamespaceSelector(NamespaceSelector node) {
+ heading('Namespace Selector', node);
+ output.depth++;
+
+ super.visitNamespaceSelector(node);
+
+ visitSimpleSelector(node.nameAsSimpleSelector);
+ output.depth--;
+ }
+
+ void visitElementSelector(ElementSelector node) {
+ heading('Element Selector', node);
+ output.depth++;
+ super.visitElementSelector(node);
+ output.depth--;
+ }
+
+ void visitAttributeSelector(AttributeSelector node) {
+ heading('AttributeSelector', node);
+ output.depth++;
+ super.visitAttributeSelector(node);
+ String tokenStr = node.matchOperatorAsTokenString();
+ output.writeValue('operator', '${node.matchOperator()} (${tokenStr})');
+ output.writeValue('value', node.valueToString());
+ output.depth--;
+ }
+
+ void visitIdSelector(IdSelector node) {
+ heading('Id Selector', node);
+ output.depth++;
+ super.visitIdSelector(node);
+ output.depth--;
+ }
+
+ void visitClassSelector(ClassSelector node) {
+ heading('Class Selector', node);
+ output.depth++;
+ super.visitClassSelector(node);
+ output.depth--;
+ }
+
+ void visitPseudoClassSelector(PseudoClassSelector node) {
+ heading('Pseudo Class Selector', node);
+ output.depth++;
+ super.visitPseudoClassSelector(node);
+ output.depth--;
+ }
+
+ void visitPseudoElementSelector(PseudoElementSelector node) {
+ heading('Pseudo Element Selector', node);
+ output.depth++;
+ super.visitPseudoElementSelector(node);
+ output.depth--;
+ }
+
+ void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node) {
+ heading('Pseudo Class Function Selector', node);
+ output.depth++;
+ visitSelectorExpression(node.expression);
+ super.visitPseudoClassFunctionSelector(node);
+ output.depth--;
+ }
+
+ void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node) {
+ heading('Pseudo Element Function Selector', node);
+ output.depth++;
+ visitSelectorExpression(node.expression);
+ super.visitPseudoElementFunctionSelector(node);
+ output.depth--;
+ }
+
+ void visitSelectorExpression(SelectorExpression node) {
+ heading('Selector Expression', node);
+ output.depth++;
+ output.writeNodeList('expressions', node.expressions);
+ output.depth--;
+ }
+
+ void visitNegationSelector(NegationSelector node) {
+ super.visitNegationSelector(node);
+ output.depth++;
+ heading('Negation Selector', node);
+ output.writeNode('Negation arg', node.negationArg);
+ output.depth--;
+ }
+
+ void visitUnicodeRangeTerm(UnicodeRangeTerm node) {
+ heading('UnicodeRangeTerm', node);
+ output.depth++;
+ output.writeValue('1st value', node.first);
+ output.writeValue('2nd value', node.second);
+ output.depth--;
+ }
+
+ void visitLiteralTerm(LiteralTerm node) {
+ heading('LiteralTerm', node);
+ output.depth++;
+ output.writeValue('value', node.text);
+ output.depth--;
+ }
+
+ void visitHexColorTerm(HexColorTerm node) {
+ heading('HexColorTerm', node);
+ output.depth++;
+ output.writeValue('hex value', node.text);
+ output.writeValue('decimal value', node.value);
+ output.depth--;
+ }
+
+ void visitNumberTerm(NumberTerm node) {
+ heading('NumberTerm', node);
+ output.depth++;
+ output.writeValue('value', node.text);
+ output.depth--;
+ }
+
+ void visitUnitTerm(UnitTerm node) {
+ String unitValue;
+
+ output.depth++;
+ output.writeValue('value', node.text);
+ output.writeValue('unit', node.unitToString());
+ output.depth--;
+ }
+
+ void visitLengthTerm(LengthTerm node) {
+ heading('LengthTerm', node);
+ super.visitLengthTerm(node);
+ }
+
+ void visitPercentageTerm(PercentageTerm node) {
+ heading('PercentageTerm', node);
+ output.depth++;
+ super.visitPercentageTerm(node);
+ output.depth--;
+ }
+
+ void visitEmTerm(EmTerm node) {
+ heading('EmTerm', node);
+ output.depth++;
+ super.visitEmTerm(node);
+ output.depth--;
+ }
+
+ void visitExTerm(ExTerm node) {
+ heading('ExTerm', node);
+ output.depth++;
+ super.visitExTerm(node);
+ output.depth--;
+ }
+
+ void visitAngleTerm(AngleTerm node) {
+ heading('AngleTerm', node);
+ super.visitAngleTerm(node);
+ }
+
+ void visitTimeTerm(TimeTerm node) {
+ heading('TimeTerm', node);
+ super.visitTimeTerm(node);
+ }
+
+ void visitFreqTerm(FreqTerm node) {
+ heading('FreqTerm', node);
+ super.visitFreqTerm(node);
+ }
+
+ void visitFractionTerm(FractionTerm node) {
+ heading('FractionTerm', node);
+ output.depth++;
+ super.visitFractionTerm(node);
+ output.depth--;
+ }
+
+ void visitUriTerm(UriTerm node) {
+ heading('UriTerm', node);
+ output.depth++;
+ super.visitUriTerm(node);
+ output.depth--;
+ }
+
+ void visitFunctionTerm(FunctionTerm node) {
+ heading('FunctionTerm', node);
+ output.depth++;
+ super.visitFunctionTerm(node);
+ output.depth--;
+ }
+
+ void visitGroupTerm(GroupTerm node) {
+ heading('GroupTerm', node);
+ output.depth++;
+ output.writeNodeList('grouped terms', node._terms);
+ output.depth--;
+ }
+
+ void visitItemTerm(ItemTerm node) {
+ heading('ItemTerm', node);
+ super.visitItemTerm(node);
+ }
+
+ void visitIE8Term(IE8Term node) {
+ heading('IE8Term', node);
+ visitLiteralTerm(node);
+ }
+
+ void visitOperatorSlash(OperatorSlash node) {
+ heading('OperatorSlash', node);
+ }
+
+ void visitOperatorComma(OperatorComma node) {
+ heading('OperatorComma', node);
+ }
+
+ void visitOperatorPlus(OperatorPlus node) {
+ heading('OperatorPlus', node);
+ }
+
+ void visitOperatorMinus(OperatorMinus node) {
+ heading('OperatorMinus', node);
+ }
+
+ void visitVarUsage(VarUsage node) {
+ heading('Var', node);
+ output.depth++;
+ output.write('usage ${node.name}');
+ output.writeNodeList('default values', node.defaultValues);
+ output.depth--;
+ }
+
+ void visitExpressions(Expressions node) {
+ heading('Expressions', node);
+ output.depth++;
+ output.writeNodeList('expressions', node.expressions);
+ output.depth--;
+ }
+
+ void visitBinaryExpression(BinaryExpression node) {
+ heading('BinaryExpression', node);
+ // TODO(terry): TBD
+ }
+
+ void visitUnaryExpression(UnaryExpression node) {
+ heading('UnaryExpression', node);
+ // TODO(terry): TBD
+ }
+
+ void visitIdentifier(Identifier node) {
+ heading('Identifier(${output.toValue(node.name)})', node);
+ }
+
+ void visitWildcard(Wildcard node) {
+ heading('Wildcard(*)', node);
+ }
+
+ void visitDartStyleExpression(DartStyleExpression node) {
+ heading('DartStyleExpression', node);
+ }
+
+ void visitFontExpression(FontExpression node) {
+ heading('Dart Style FontExpression', node);
+ }
+
+ void visitBoxExpression(BoxExpression node) {
+ heading('Dart Style BoxExpression', node);
+ }
+
+ void visitMarginExpression(MarginExpression node) {
+ heading('Dart Style MarginExpression', node);
+ }
+
+ void visitBorderExpression(BorderExpression node) {
+ heading('Dart Style BorderExpression', node);
+ }
+
+ void visitHeightExpression(HeightExpression node) {
+ heading('Dart Style HeightExpression', node);
+ }
+
+ void visitPaddingExpression(PaddingExpression node) {
+ heading('Dart Style PaddingExpression', node);
+ }
+
+ void visitWidthExpression(WidthExpression node) {
+ heading('Dart Style WidthExpression', node);
+ }
+}
diff --git a/lib/src/validate.dart b/lib/src/validate.dart
new file mode 100644
index 0000000..d2301d7
--- /dev/null
+++ b/lib/src/validate.dart
@@ -0,0 +1,135 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.src.validate;
+
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'package:source_maps/span.dart' show Span;
+
+/** Can be thrown on any Css runtime problem includes source location. */
+class CssSelectorException implements Exception {
+ final String _message;
+ final Span _span;
+
+ CssSelectorException(this._message, [this._span]);
+
+ String toString() {
+ var msg = _span == null ? _message : _span.getLocationMessage(_message);
+ return 'CssSelectorException: $msg';
+ }
+}
+
+List<String> classes = [];
+List<String> ids = [];
+
+class Validate {
+ static int _classNameCheck(var selector, int matches) {
+ if (selector.isCombinatorDescendant() ||
+ (selector.isCombinatorNone() && matches == 0)) {
+ if (matches < 0) {
+ String tooMany = selector.simpleSelector.toString();
+ throw new CssSelectorException(
+ 'Can not mix Id selector with class selector(s). Id '
+ 'selector must be singleton too many starting at $tooMany');
+ }
+
+ return matches + 1;
+ } else {
+ String error = selector.toString();
+ throw new CssSelectorException(
+ 'Selectors can not have combinators (>, +, or ~) before $error');
+ }
+ }
+
+ static int _elementIdCheck(var selector, int matches) {
+ if (selector.isCombinatorNone() && matches == 0) {
+ // Perfect just one element id returns matches of -1.
+ return -1;
+ } else if (selector.isCombinatorDescendant()) {
+ String tooMany = selector.simpleSelector.toString();
+ throw new CssSelectorException(
+ 'Use of Id selector must be singleton starting at $tooMany');
+ } else {
+ String error = selector.simpleSelector.toString();
+ throw new CssSelectorException(
+ 'Selectors can not have combinators (>, +, or ~) before $error');
+ }
+ }
+
+ // Validate the @{css expression} only .class and #elementId are valid inside
+ // of @{...}.
+ static template(List<Selector> selectors) {
+ var errorSelector; // signal which selector didn't match.
+ bool found = false; // signal if a selector is matched.
+ int matches = 0; // < 0 IdSelectors, > 0 ClassSelector
+
+ // At most one selector group (any number of simple selector sequences).
+ assert(selectors.length <= 1);
+
+ for (final sels in selectors) {
+ for (final selector in sels.simpleSelectorSequences) {
+ found = false;
+ var simpleSelector = selector.simpleSelector;
+ if (simpleSelector is ClassSelector) {
+ // Any class name starting with an underscore is a private class name
+ // that doesn't have to match the world of known classes.
+ if (!simpleSelector.name.startsWith('_')) {
+ // TODO(terry): For now iterate through all classes look for faster
+ // mechanism hash map, etc.
+ for (final className in classes) {
+ if (selector.simpleSelector.name == className) {
+ matches = _classNameCheck(selector, matches);
+ found = true; // .class found.
+ break;
+ }
+ for (final className2 in classes) {
+ print(className2);
+ }
+ }
+
+ } else {
+ // Don't check any class name that is prefixed with an underscore.
+ // However, signal as found and bump up matches; it's a valid class
+ // name.
+ matches = _classNameCheck(selector, matches);
+ found = true; // ._class are always okay.
+ }
+ } else if (simpleSelector is IdSelector) {
+ // Any element id starting with an underscore is a private element id
+ // that doesn't have to match the world of known elemtn ids.
+ if (!simpleSelector.name.startsWith('_')) {
+ for (final id in ids) {
+ if (simpleSelector.name == id) {
+ matches = _elementIdCheck(selector, matches);
+ found = true; // #id found.
+ break;
+ }
+ }
+ } else {
+ // Don't check any element ID that is prefixed with an underscore.
+ // Signal as found and bump up matches; it's a valid element ID.
+ matches = _elementIdCheck(selector, matches);
+ found = true; // #_id are always okay
+ }
+ } else {
+ String badSelector = simpleSelector.toString();
+ throw new CssSelectorException(
+ 'Invalid template selector $badSelector');
+ }
+
+ if (!found) {
+ String unknownName = simpleSelector.toString();
+ throw new CssSelectorException('Unknown selector name $unknownName');
+ }
+ }
+ }
+
+ // Every selector must match.
+ Selector selector = selectors[0];
+ assert((matches >= 0 ? matches : -matches) ==
+ selector.simpleSelectorSequences.length);
+ }
+}
+
diff --git a/lib/visitor.dart b/lib/visitor.dart
new file mode 100644
index 0000000..d2f24c9
--- /dev/null
+++ b/lib/visitor.dart
@@ -0,0 +1,445 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library csslib.visitor;
+
+import 'package:source_maps/span.dart' show Span;
+import 'parser.dart';
+
+part 'src/css_printer.dart';
+part 'src/tree.dart';
+part 'src/tree_base.dart';
+part 'src/tree_printer.dart';
+
+abstract class VisitorBase {
+ void visitCssComment(CssComment node);
+ void visitCommentDefinition(CommentDefinition node);
+ void visitStyleSheet(StyleSheet node);
+ void visitTopLevelProduction(TopLevelProduction node);
+ void visitDirective(Directive node);
+ void visitMediaExpression(MediaExpression node);
+ void visitMediaQuery(MediaQuery node);
+ void visitMediaDirective(MediaDirective node);
+ void visitHostDirective(HostDirective node);
+ void visitPageDirective(PageDirective node);
+ void visitCharsetDirective(CharsetDirective node);
+ void visitImportDirective(ImportDirective node);
+ void visitKeyFrameDirective(KeyFrameDirective node);
+ void visitKeyFrameBlock(KeyFrameBlock node);
+ void visitFontFaceDirective(FontFaceDirective node);
+ void visitStyletDirective(StyletDirective node);
+ void visitNamespaceDirective(NamespaceDirective node);
+ void visitVarDefinitionDirective(VarDefinitionDirective node);
+
+ void visitRuleSet(RuleSet node);
+ void visitDeclarationGroup(DeclarationGroup node);
+ void visitMarginGroup(DeclarationGroup node);
+ void visitDeclaration(Declaration node);
+ void visitVarDefinition(VarDefinition node);
+ void visitSelectorGroup(SelectorGroup node);
+ void visitSelector(Selector node);
+ void visitSimpleSelectorSequence(SimpleSelectorSequence node);
+ void visitSimpleSelector(SimpleSelector node);
+ void visitElementSelector(ElementSelector node);
+ void visitNamespaceSelector(NamespaceSelector node);
+ void visitAttributeSelector(AttributeSelector node);
+ void visitIdSelector(IdSelector node);
+ void visitClassSelector(ClassSelector node);
+ void visitPseudoClassSelector(PseudoClassSelector node);
+ void visitPseudoElementSelector(PseudoElementSelector node);
+ void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node);
+ void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node);
+ void visitNegationSelector(NegationSelector node);
+ void visitSelectorExpression(SelectorExpression node);
+
+ void visitUnicodeRangeTerm(UnicodeRangeTerm node);
+ void visitLiteralTerm(LiteralTerm node);
+ void visitHexColorTerm(HexColorTerm node);
+ void visitNumberTerm(NumberTerm node);
+ void visitUnitTerm(UnitTerm node);
+ void visitLengthTerm(LengthTerm node);
+ void visitPercentageTerm(PercentageTerm node);
+ void visitEmTerm(EmTerm node);
+ void visitExTerm(ExTerm node);
+ void visitAngleTerm(AngleTerm node);
+ void visitTimeTerm(TimeTerm node);
+ void visitFreqTerm(FreqTerm node);
+ void visitFractionTerm(FractionTerm node);
+ void visitUriTerm(UriTerm node);
+ void visitResolutionTerm(ResolutionTerm node);
+ void visitChTerm(ChTerm node);
+ void visitRemTerm(RemTerm node);
+ void visitViewportTerm(ViewportTerm node);
+ void visitFunctionTerm(FunctionTerm node);
+ void visitGroupTerm(GroupTerm node);
+ void visitItemTerm(ItemTerm node);
+ void visitIE8Term(IE8Term node);
+ void visitOperatorSlash(OperatorSlash node);
+ void visitOperatorComma(OperatorComma node);
+ void visitOperatorPlus(OperatorPlus node);
+ void visitOperatorMinus(OperatorMinus node);
+ void visitVarUsage(VarUsage node);
+
+ void visitExpressions(Expressions node);
+ void visitBinaryExpression(BinaryExpression node);
+ void visitUnaryExpression(UnaryExpression node);
+
+ void visitIdentifier(Identifier node);
+ void visitWildcard(Wildcard node);
+ void visitThisOperator(ThisOperator node);
+ void visitNegation(Negation node);
+
+ void visitDartStyleExpression(DartStyleExpression node);
+ void visitFontExpression(FontExpression node);
+ void visitBoxExpression(BoxExpression node);
+ void visitMarginExpression(MarginExpression node);
+ void visitBorderExpression(BorderExpression node);
+ void visitHeightExpression(HeightExpression node);
+ void visitPaddingExpression(PaddingExpression node);
+ void visitWidthExpression(WidthExpression node);
+}
+
+/** Base vistor class for the style sheet AST. */
+class Visitor implements VisitorBase {
+ /** Helper function to walk a list of nodes. */
+ void _visitNodeList(list) {
+ // Don't use iterable otherwise the list can't grow while using Visitor.
+ // It certainly can't have items deleted before the index being iterated
+ // but items could be added after the index.
+ for (var index = 0; index < list.length; index++) {
+ list[index].visit(this);
+ }
+ }
+
+ void visitTree(StyleSheet tree) => visitStyleSheet(tree);
+
+ void visitStyleSheet(StyleSheet ss) {
+ _visitNodeList(ss.topLevels);
+ }
+
+ void visitTopLevelProduction(TopLevelProduction node) { }
+
+ void visitDirective(Directive node) { }
+
+ void visitCssComment(CssComment node) { }
+
+ void visitCommentDefinition(CommentDefinition node) { }
+
+ void visitMediaExpression(MediaExpression node) {
+ visitExpressions(node.exprs);
+ }
+
+ void visitMediaQuery(MediaQuery node) {
+ for (var mediaExpr in node.expressions) {
+ visitMediaExpression(mediaExpr);
+ }
+ }
+
+ void visitMediaDirective(MediaDirective node) {
+ for (var mediaQuery in node.mediaQueries) {
+ visitMediaQuery(mediaQuery);
+ }
+ for (var ruleset in node.rulesets) {
+ visitRuleSet(ruleset);
+ }
+ }
+
+ void visitHostDirective(HostDirective node) {
+ for (var ruleset in node.rulesets) {
+ visitRuleSet(ruleset);
+ }
+ }
+
+ void visitPageDirective(PageDirective node) {
+ for (var declGroup in node._declsMargin) {
+ if (declGroup is MarginGroup) {
+ visitMarginGroup(declGroup);
+ } else {
+ visitDeclarationGroup(declGroup);
+ }
+ }
+ }
+
+ void visitCharsetDirective(CharsetDirective node) { }
+
+ void visitImportDirective(ImportDirective node) {
+ for (var mediaQuery in node.mediaQueries) {
+ visitMediaQuery(mediaQuery);
+ }
+ }
+
+ void visitKeyFrameDirective(KeyFrameDirective node) {
+ visitIdentifier(node._name);
+ _visitNodeList(node._blocks);
+ }
+
+ void visitKeyFrameBlock(KeyFrameBlock node) {
+ visitExpressions(node._blockSelectors);
+ visitDeclarationGroup(node._declarations);
+ }
+
+ void visitFontFaceDirective(FontFaceDirective node) {
+ visitDeclarationGroup(node._declarations);
+ }
+
+ void visitStyletDirective(StyletDirective node) {
+ _visitNodeList(node._rulesets);
+ }
+
+ void visitNamespaceDirective(NamespaceDirective node) { }
+
+ void visitVarDefinitionDirective(VarDefinitionDirective node) {
+ visitVarDefinition(node.def);
+ }
+
+ void visitRuleSet(RuleSet node) {
+ visitSelectorGroup(node._selectorGroup);
+ visitDeclarationGroup(node._declarationGroup);
+ }
+
+ void visitDeclarationGroup(DeclarationGroup node) {
+ _visitNodeList(node._declarations);
+ }
+
+ void visitMarginGroup(MarginGroup node) => visitDeclarationGroup(node);
+
+ void visitDeclaration(Declaration node) {
+ visitIdentifier(node._property);
+ if (node._expression != null) node._expression.visit(this);
+ }
+
+ void visitVarDefinition(VarDefinition node) {
+ visitIdentifier(node._property);
+ if (node._expression != null) node._expression.visit(this);
+ }
+
+ void visitSelectorGroup(SelectorGroup node) {
+ _visitNodeList(node.selectors);
+ }
+
+ void visitSelector(Selector node) {
+ _visitNodeList(node._simpleSelectorSequences);
+ }
+
+ void visitSimpleSelectorSequence(SimpleSelectorSequence node) {
+ var selector = node._selector;
+ if (selector is NamespaceSelector) {
+ visitNamespaceSelector(selector);
+ } else if (selector is ElementSelector) {
+ visitElementSelector(selector);
+ } else if (selector is IdSelector) {
+ visitIdSelector(selector);
+ } else if (selector is ClassSelector) {
+ visitClassSelector(selector);
+ } else if (selector is PseudoClassFunctionSelector) {
+ visitPseudoClassFunctionSelector(selector);
+ } else if (selector is PseudoElementFunctionSelector) {
+ visitPseudoElementFunctionSelector(selector);
+ } else if (selector is PseudoClassSelector) {
+ visitPseudoClassSelector(selector);
+ } else if (selector is PseudoElementSelector) {
+ visitPseudoElementSelector(selector);
+ } else if (selector is NegationSelector) {
+ visitNegationSelector(selector);
+ } else if (selector is SelectorExpression) {
+ visitSelectorExpression(selector);
+ } else if (selector is AttributeSelector) {
+ visitAttributeSelector(selector);
+ } else {
+ visitSimpleSelector(selector);
+ }
+ }
+
+ void visitSimpleSelector(SimpleSelector node) => node._name.visit(this);
+
+ void visitNamespaceSelector(NamespaceSelector node) {
+ var namespace = node._namespace;
+ if (namespace is Identifier) {
+ visitIdentifier(namespace);
+ } else if (namespace is Wildcard) {
+ visitWildcard(namespace);
+ }
+
+ visitSimpleSelector(node.nameAsSimpleSelector);
+ }
+
+ void visitElementSelector(ElementSelector node) => visitSimpleSelector(node);
+
+ void visitAttributeSelector(AttributeSelector node) {
+ visitSimpleSelector(node);
+ }
+
+ void visitIdSelector(IdSelector node) => visitSimpleSelector(node);
+
+ void visitClassSelector(ClassSelector node) => visitSimpleSelector(node);
+
+ void visitPseudoClassSelector(PseudoClassSelector node) =>
+ visitSimpleSelector(node);
+
+ void visitPseudoElementSelector(PseudoElementSelector node) =>
+ visitSimpleSelector(node);
+
+ void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node) =>
+ visitSimpleSelector(node);
+
+ void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node) =>
+ visitSimpleSelector(node);
+
+ void visitNegationSelector(NegationSelector node) =>
+ visitSimpleSelector(node);
+
+ void visitSelectorExpression(SelectorExpression node) {
+ _visitNodeList(node._expressions);
+ }
+
+ void visitUnicodeRangeTerm(UnicodeRangeTerm node) { }
+
+ void visitLiteralTerm(LiteralTerm node) { }
+
+ void visitHexColorTerm(HexColorTerm node) { }
+
+ void visitNumberTerm(NumberTerm node) { }
+
+ void visitUnitTerm(UnitTerm node) { }
+
+ void visitLengthTerm(LengthTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitPercentageTerm(PercentageTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitEmTerm(EmTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitExTerm(ExTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitAngleTerm(AngleTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitTimeTerm(TimeTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitFreqTerm(FreqTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitFractionTerm(FractionTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitUriTerm(UriTerm node) {
+ visitLiteralTerm(node);
+ }
+
+ void visitResolutionTerm(ResolutionTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitChTerm(ChTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitRemTerm(RemTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitViewportTerm(ViewportTerm node) {
+ visitUnitTerm(node);
+ }
+
+ void visitFunctionTerm(FunctionTerm node) {
+ visitLiteralTerm(node);
+ visitExpressions(node._params);
+ }
+
+ void visitGroupTerm(GroupTerm node) {
+ for (var term in node._terms) {
+ term.visit(this);
+ }
+ }
+
+ void visitItemTerm(ItemTerm node) {
+ visitNumberTerm(node);
+ }
+
+ void visitIE8Term(IE8Term node) { }
+
+ void visitOperatorSlash(OperatorSlash node) { }
+
+ void visitOperatorComma(OperatorComma node) { }
+
+ void visitOperatorPlus(OperatorPlus node) { }
+
+ void visitOperatorMinus(OperatorMinus node) { }
+
+ void visitVarUsage(VarUsage node) {
+ _visitNodeList(node.defaultValues);
+ }
+
+ void visitExpressions(Expressions node) {
+ _visitNodeList(node.expressions);
+ }
+
+ void visitBinaryExpression(BinaryExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitUnaryExpression(UnaryExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitIdentifier(Identifier node) { }
+
+ void visitWildcard(Wildcard node) { }
+
+ void visitThisOperator(ThisOperator node) { }
+
+ void visitNegation(Negation node) { }
+
+ void visitDartStyleExpression(DartStyleExpression node) { }
+
+ void visitFontExpression(FontExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitBoxExpression(BoxExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitMarginExpression(MarginExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitBorderExpression(BorderExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitHeightExpression(HeightExpression node) {
+ // TODO(terry): TB
+ throw UnimplementedError;
+ }
+
+ void visitPaddingExpression(PaddingExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+
+ void visitWidthExpression(WidthExpression node) {
+ // TODO(terry): TBD
+ throw UnimplementedError;
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..b821941
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,12 @@
+name: csslib
+author: "Web UI Team <web-ui-dev@dartlang.org>"
+description: A library for parsing CSS.
+homepage: https://www.dartlang.org
+dependencies:
+ args: any
+ logging: any
+ path: any
+ source_maps: any
+dev_dependencies:
+ browser: any
+ unittest: any
diff --git a/test/compiler_test.dart b/test/compiler_test.dart
new file mode 100644
index 0000000..ee37f29
--- /dev/null
+++ b/test/compiler_test.dart
@@ -0,0 +1,728 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library compiler_test;
+
+import 'dart:utf';
+import 'package:unittest/unittest.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'testing.dart';
+
+void testClass() {
+ var errors = [];
+ var input = ".foobar {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var selectorSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+ expect(selectorSeqs.length, 1);
+ final simpSelector = selectorSeqs[0].simpleSelector;
+ expect(simpSelector is ClassSelector, true);
+ expect(selectorSeqs[0].isCombinatorNone, true);
+ expect(simpSelector.name, "foobar");
+}
+
+void testClass2() {
+ var errors = [];
+ var input = ".foobar .bar .no-story {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+ expect(simpleSeqs.length, 3);
+
+ var simpSelector0 = simpleSeqs[0].simpleSelector;
+ expect(simpSelector0 is ClassSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector0.name, "foobar");
+
+ var simpSelector1 = simpleSeqs[1].simpleSelector;
+ expect(simpSelector1 is ClassSelector, true);
+ expect(simpleSeqs[1].isCombinatorDescendant, true);
+ expect(simpSelector1.name, "bar");
+
+ var simpSelector2 = simpleSeqs[2].simpleSelector;
+ expect(simpSelector2 is ClassSelector, true);
+ expect(simpleSeqs[2].isCombinatorDescendant, true);
+ expect(simpSelector2.name, "no-story");
+}
+
+void testId() {
+ var errors = [];
+ var input = "#elemId {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 1);
+ var simpSelector = simpleSeqs[0].simpleSelector;
+ expect(simpSelector is IdSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector.name, "elemId");
+}
+
+void testElement() {
+ var errors = [];
+ var input = "div {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 1);
+
+ final simpSelector = simpleSeqs[0].simpleSelector;
+ expect(simpSelector is ElementSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector.name, "div");
+
+ input = "div div span {}";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 3);
+
+ var simpSelector0 = simpleSeqs[0].simpleSelector;
+ expect(simpSelector0 is ElementSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector0.name, "div");
+
+ var simpSelector1 = simpleSeqs[1].simpleSelector;
+ expect(simpSelector1 is ElementSelector, true);
+ expect(simpleSeqs[1].isCombinatorDescendant, true);
+ expect(simpSelector1.name, "div");
+
+ var simpSelector2 = simpleSeqs[2].simpleSelector;
+ expect(simpSelector2 is ElementSelector, true);
+ expect(simpleSeqs[2].isCombinatorDescendant, true);
+ expect(simpSelector2.name, "span");
+}
+
+void testNamespace() {
+ var errors = [];
+ var input = "ns1|div {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 1);
+ var simpSelector = simpleSeqs[0].simpleSelector;
+ expect(simpSelector is NamespaceSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector.isNamespaceWildcard, false);
+ expect(simpSelector.namespace, "ns1");
+ var elementSelector = simpSelector.nameAsSimpleSelector;
+ expect(elementSelector is ElementSelector, true);
+ expect(elementSelector.isWildcard, false);
+ expect(elementSelector.name, "div");
+}
+
+void testNamespace2() {
+ var errors = [];
+ var input = "ns1|div div ns2|span .foobar {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 4);
+
+ var simpSelector0 = simpleSeqs[0].simpleSelector;
+ expect(simpSelector0 is NamespaceSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector0.namespace, "ns1");
+ var elementSelector0 = simpSelector0.nameAsSimpleSelector;
+ expect(elementSelector0 is ElementSelector, true);
+ expect(elementSelector0.isWildcard, false);
+ expect(elementSelector0.name, "div");
+
+ var simpSelector1 = simpleSeqs[1].simpleSelector;
+ expect(simpSelector1 is ElementSelector, true);
+ expect(simpleSeqs[1].isCombinatorDescendant, true);
+ expect(simpSelector1.name, "div");
+
+ var simpSelector2 = simpleSeqs[2].simpleSelector;
+ expect(simpSelector2 is NamespaceSelector, true);
+ expect(simpleSeqs[2].isCombinatorDescendant, true);
+ expect(simpSelector2.namespace, "ns2");
+ var elementSelector2 = simpSelector2.nameAsSimpleSelector;
+ expect(elementSelector2 is ElementSelector, true);
+ expect(elementSelector2.isWildcard, false);
+ expect(elementSelector2.name, "span");
+
+ var simpSelector3 = simpleSeqs[3].simpleSelector;
+ expect(simpSelector3 is ClassSelector, true);
+ expect(simpleSeqs[3].isCombinatorDescendant, true);
+ expect(simpSelector3.name, "foobar");
+}
+
+void testSelectorGroups() {
+ var errors = [];
+ var input =
+ "div, .foobar ,#elemId, .xyzzy .test, ns1|div div #elemId .foobar {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 5);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var groupSelector0 = ruleset.selectorGroup.selectors[0];
+ expect(groupSelector0.simpleSelectorSequences.length, 1);
+ var selector0 = groupSelector0.simpleSelectorSequences[0];
+ var simpleSelector0 = selector0.simpleSelector;
+ expect(simpleSelector0 is ElementSelector, true);
+ expect(selector0.isCombinatorNone, true);
+ expect(simpleSelector0.name, "div");
+
+ var groupSelector1 = ruleset.selectorGroup.selectors[1];
+ expect(groupSelector1.simpleSelectorSequences.length, 1);
+ var selector1 = groupSelector1.simpleSelectorSequences[0];
+ var simpleSelector1 = selector1.simpleSelector;
+ expect(simpleSelector1 is ClassSelector, true);
+ expect(selector1.isCombinatorNone, true);
+ expect(simpleSelector1.name, "foobar");
+
+ var groupSelector2 = ruleset.selectorGroup.selectors[2];
+ expect(groupSelector2.simpleSelectorSequences.length, 1);
+ var selector2 = groupSelector2.simpleSelectorSequences[0];
+ var simpleSelector2 = selector2.simpleSelector;
+ expect(simpleSelector2 is IdSelector, true);
+ expect(selector2.isCombinatorNone, true);
+ expect(simpleSelector2.name, "elemId");
+
+ var groupSelector3 = ruleset.selectorGroup.selectors[3];
+ expect(groupSelector3.simpleSelectorSequences.length, 2);
+
+ var selector30 = groupSelector3.simpleSelectorSequences[0];
+ var simpleSelector30 = selector30.simpleSelector;
+ expect(simpleSelector30 is ClassSelector, true);
+ expect(selector30.isCombinatorNone, true);
+ expect(simpleSelector30.name, "xyzzy");
+
+ var selector31 = groupSelector3.simpleSelectorSequences[1];
+ var simpleSelector31 = selector31.simpleSelector;
+ expect(simpleSelector31 is ClassSelector, true);
+ expect(selector31.isCombinatorDescendant, true);
+ expect(simpleSelector31.name, "test");
+
+ var groupSelector4 = ruleset.selectorGroup.selectors[4];
+ expect(groupSelector4.simpleSelectorSequences.length, 4);
+
+ var selector40 = groupSelector4.simpleSelectorSequences[0];
+ var simpleSelector40 = selector40.simpleSelector;
+ expect(simpleSelector40 is NamespaceSelector, true);
+ expect(selector40.isCombinatorNone, true);
+ expect(simpleSelector40.namespace, "ns1");
+ var elementSelector = simpleSelector40.nameAsSimpleSelector;
+ expect(elementSelector is ElementSelector, true);
+ expect(elementSelector.isWildcard, false);
+ expect(elementSelector.name, "div");
+
+ var selector41 = groupSelector4.simpleSelectorSequences[1];
+ var simpleSelector41 = selector41.simpleSelector;
+ expect(simpleSelector41 is ElementSelector, true);
+ expect(selector41.isCombinatorDescendant, true);
+ expect(simpleSelector41.name, "div");
+
+ var selector42 = groupSelector4.simpleSelectorSequences[2];
+ var simpleSelector42 = selector42.simpleSelector;
+ expect(simpleSelector42 is IdSelector, true);
+ expect(selector42.isCombinatorDescendant, true);
+ expect(simpleSelector42.name, "elemId");
+
+ var selector43 = groupSelector4.simpleSelectorSequences[3];
+ var simpleSelector43 = selector43.simpleSelector;
+ expect(selector43.isCombinatorDescendant, true);
+ expect(simpleSelector43.name, "foobar");
+}
+
+void testCombinator() {
+ var errors = [];
+ var input = ".foobar > .bar + .no-story ~ myNs|div #elemId {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 5);
+
+ var selector0 = simpleSeqs[0];
+ var simpleSelector0 = selector0.simpleSelector;
+ expect(simpleSelector0 is ClassSelector, true);
+ expect(selector0.isCombinatorNone, true);
+ expect(simpleSelector0.name, "foobar");
+
+ var selector1 = simpleSeqs[1];
+ var simpleSelector1 = selector1.simpleSelector;
+ expect(simpleSelector1 is ClassSelector, true);
+ expect(selector1.isCombinatorGreater, true);
+ expect(simpleSelector1.name, "bar");
+
+ var selector2 = simpleSeqs[2];
+ var simpleSelector2 = selector2.simpleSelector;
+ expect(simpleSelector2 is ClassSelector, true);
+ expect(selector2.isCombinatorPlus, true);
+ expect(simpleSelector2.name, "no-story");
+
+ var selector3 = simpleSeqs[3];
+ var simpleSelector3 = selector3.simpleSelector;
+ expect(simpleSelector3 is NamespaceSelector, true);
+ expect(selector3.isCombinatorTilde, true);
+ expect(simpleSelector3.namespace, "myNs");
+ var elementSelector = simpleSelector3.nameAsSimpleSelector;
+ expect(elementSelector is ElementSelector, true);
+ expect(elementSelector.isWildcard, false);
+ expect(elementSelector.name, "div");
+
+ var selector4 = simpleSeqs[4];
+ var simpleSelector4 = selector4.simpleSelector;
+ expect(simpleSelector4 is IdSelector, true);
+ expect(selector4.isCombinatorDescendant, true);
+ expect(simpleSelector4.name, "elemId");
+}
+
+void testWildcard() {
+ var errors = [];
+ var input = "* {}";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ var ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ var simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 1);
+ var simpSelector = simpleSeqs[0].simpleSelector;
+ expect(simpSelector is ElementSelector, true);
+ expect(simpleSeqs[0].isCombinatorNone, true);
+ expect(simpSelector.isWildcard, true);
+ expect(simpSelector.name, "*");
+
+ input = "*.foobar {}";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 2);
+
+ var selector0 = simpleSeqs[0];
+ var simpleSelector0 = selector0.simpleSelector;
+ expect(simpleSelector0 is ElementSelector, true);
+ expect(selector0.isCombinatorNone, true);
+ expect(simpleSelector0.isWildcard, true);
+ expect(simpleSelector0.name, "*");
+
+ var selector1 = simpleSeqs[1];
+ var simpleSelector1 = selector1.simpleSelector;
+ expect(simpleSelector1 is ClassSelector, true);
+ expect(selector1.isCombinatorNone, true);
+ expect(simpleSelector1.name, "foobar");
+
+ input = "myNs|*.foobar {}";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels.length, 1);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 2);
+
+ selector0 = simpleSeqs[0];
+ simpleSelector0 = selector0.simpleSelector;
+ expect(simpleSelector0 is NamespaceSelector, true);
+ expect(selector0.isCombinatorNone, true);
+ expect(simpleSelector0.isNamespaceWildcard, false);
+ var elementSelector = simpleSelector0.nameAsSimpleSelector;
+ expect("myNs", simpleSelector0.namespace);
+ expect(elementSelector.isWildcard, true);
+ expect("*", elementSelector.name);
+
+ selector1 = simpleSeqs[1];
+ simpleSelector1 = selector1.simpleSelector;
+ expect(simpleSelector1 is ClassSelector, true);
+ expect(selector1.isCombinatorNone, true);
+ expect("foobar", simpleSelector1.name);
+
+ input = "*|*.foobar {}";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(stylesheet.topLevels[0] is RuleSet, true);
+ ruleset = stylesheet.topLevels[0];
+ expect(ruleset.selectorGroup.selectors.length, 1);
+ expect(ruleset.declarationGroup.declarations.length, 0);
+
+ simpleSeqs = ruleset.selectorGroup.selectors[0].simpleSelectorSequences;
+
+ expect(simpleSeqs.length, 2);
+
+ selector0 = simpleSeqs[0];
+ simpleSelector0 = selector0.simpleSelector;
+ expect(simpleSelector0 is NamespaceSelector, true);
+ expect(selector0.isCombinatorNone, true);
+ expect(simpleSelector0.isNamespaceWildcard, true);
+ expect("*", simpleSelector0.namespace);
+ elementSelector = simpleSelector0.nameAsSimpleSelector;
+ expect(elementSelector.isWildcard, true);
+ expect("*", elementSelector.name);
+
+ selector1 = simpleSeqs[1];
+ simpleSelector1 = selector1.simpleSelector;
+ expect(simpleSelector1 is ClassSelector, true);
+ expect(selector1.isCombinatorNone, true);
+ expect("foobar", simpleSelector1.name);
+}
+
+/** Test List<int> as input to parser. */
+void testArrayOfChars() {
+ var errors = [];
+ var input = '<![CDATA[.foo { '
+ 'color: red; left: 20px; top: 20px; width: 100px; height:200px'
+ '}'
+ '#div {'
+ 'color : #00F578; border-color: #878787;'
+ '}]]>';
+
+ var stylesheet = parse(encodeUtf8(input), errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ expect(prettyPrint(stylesheet), r'''
+.foo {
+ color: #f00;
+ left: 20px;
+ top: 20px;
+ width: 100px;
+ height: 200px;
+}
+#div {
+ color: #00F578;
+ border-color: #878787;
+}''');
+}
+
+void testPseudo() {
+ var errors = [];
+
+ final input = r'''
+html:lang(fr-ca) { quotes: '" ' ' "' }
+zoom: { }
+
+a:link { color: red }
+:link { color: blue }
+
+a:focus { background: yellow }
+a:focus:hover { background: white }
+
+p.special:first-letter {color: #ffd800}
+
+p:not(#example){
+ background-color: yellow;
+}
+
+input:not([DISABLED]){
+ background-color: yellow;
+}
+
+html|*:not(:link):not(:visited) {
+ border: 1px solid black;
+}
+
+*:not(FOO) {
+ height: 20px;
+}
+
+*|*:not(*) {
+ color: orange;
+}
+
+*|*:not(:hover) {
+ color: magenta;
+}
+
+p:nth-child(3n-3) { }
+
+div:nth-child(2n) { color : red; }
+''';
+
+ var stylesheet = parseCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), r'''
+html:lang(fr-ca) {
+ quotes: "\" " " \"";
+}
+zoom {
+}
+a:link {
+ color: #f00;
+}
+:link {
+ color: #00f;
+}
+a:focus {
+ background: #ff0;
+}
+a:focus:hover {
+ background: #fff;
+}
+p.special:first-letter {
+ color: #ffd800;
+}
+p:not(#example) {
+ background-color: #ff0;
+}
+input:not([DISABLED]) {
+ background-color: #ff0;
+}
+html|*:not(:link):not(:visited) {
+ border: 1px solid #000;
+}
+*:not(FOO) {
+ height: 20px;
+}
+*|*:not(*) {
+ color: #ffa500;
+}
+*|*:not(:hover) {
+ color: #f0f;
+}
+p:nth-child(3n-3) {
+}
+div:nth-child(2n) {
+ color: #f00;
+}''');
+}
+
+void testAttribute() {
+ // TODO(terry): Implement
+}
+
+void testNegation() {
+ // TODO(terry): Implement
+}
+
+void testHost() {
+ var errors = [];
+ var input = '@host { '
+ ':scope {'
+ 'white-space: nowrap;'
+ 'overflow-style: marquee-line;'
+ 'overflow-x: marquee;'
+ '}'
+ '* { color: red; }'
+ '*:hover { font-weight: bold; }'
+ ':nth-child(odd) { color: blue; }'
+ '}';
+ var stylesheet = parseCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), r'''
+@host {
+:scope {
+ white-space: nowrap;
+ overflow-style: marquee-line;
+ overflow-x: marquee;
+}
+* {
+ color: #f00;
+}
+*:hover {
+ font-weight: bold;
+}
+:nth-child(odd) {
+ color: #00f;
+}
+}''');
+}
+
+// TODO(terry): Move to emitter_test.dart when real emitter exist.
+void testEmitter() {
+ var errors = [];
+ var input = '.foo { '
+ 'color: red; left: 20px; top: 20px; width: 100px; height:200px'
+ '}'
+ '#div {'
+ 'color : #00F578; border-color: #878787;'
+ '}';
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ walkTree(stylesheet);
+
+ expect(prettyPrint(stylesheet), r'''
+.foo {
+ color: #f00;
+ left: 20px;
+ top: 20px;
+ width: 100px;
+ height: 200px;
+}
+#div {
+ color: #00F578;
+ border-color: #878787;
+}''');
+}
+
+main() {
+ test('Classes', testClass);
+ test('Classes 2', testClass2);
+ test('Ids', testId);
+ test('Elements', testElement);
+ test('Namespace', testNamespace);
+ test('Namespace 2', testNamespace2);
+ test('Selector Groups', testSelectorGroups);
+ test('Combinator', testCombinator);
+ test('Wildcards', testWildcard);
+ test('Pseudo', testPseudo);
+ test('Attributes', testAttribute);
+ test('Negation', testNegation);
+ test('@host', testHost);
+ test('Parse List<int> as input', testArrayOfChars);
+ test('Simple Emitter', testEmitter);
+}
diff --git a/test/declaration_test.dart b/test/declaration_test.dart
new file mode 100644
index 0000000..3d6912c
--- /dev/null
+++ b/test/declaration_test.dart
@@ -0,0 +1,1036 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library declaration_test;
+
+import 'package:unittest/unittest.dart';
+import 'testing.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+
+
+/** CSS compiler options no checks in in memory style sheet. */
+List options = ['--no-colors', 'memory'];
+
+void testSimpleTerms() {
+ var errors = [];
+ final String input = r'''
+@ import url("test.css");
+.foo {
+ background-color: #191919;
+ width: 10PX;
+ height: 22mM !important;
+ border-width: 20cm;
+ margin-width: 33%;
+ border-height: 30EM;
+ width: .6in;
+ length: 1.2in;
+ -web-stuff: -10Px;
+}''';
+ final String generated = r'''
+@import "test.css";
+.foo {
+ background-color: #191919;
+ width: 10px;
+ height: 22mm !important;
+ border-width: 20cm;
+ margin-width: 33%;
+ border-height: 30em;
+ width: .6in;
+ length: 1.2in;
+ -web-stuff: -10px;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ final String input2 = r'''
+* {
+ border-color: green;
+}''';
+ final String generated2 = r'''
+* {
+ border-color: #008000;
+}''';
+
+ stylesheet = parseCss(input2, errors: errors..clear());
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+}
+
+/**
+ * Declarations with comments, references with single-quotes, double-quotes,
+ * no quotes. Hex values with # and letters, and functions (rgba, url, etc.)
+ */
+void testDeclarations() {
+ var errors = [];
+ final String input = r'''
+.more {
+ color: white;
+ color: black;
+ color: cyan;
+ color: red;
+ color: #aabbcc; /* test -- 3 */
+ color: blue;
+ background-image: url(http://test.jpeg);
+ background-image: url("http://double_quote.html");
+ background-image: url('http://single_quote.html');
+ color: rgba(10,20,255); <!-- test CDO/CDC -->
+ color: #123aef; /* hex # part integer and part identifier */
+}''';
+ final String generated = r'''
+.more {
+ color: #fff;
+ color: #000;
+ color: #0ff;
+ color: #f00;
+ color: #abc;
+ color: #00f;
+ background-image: url("http://test.jpeg");
+ background-image: url("http://double_quote.html");
+ background-image: url("http://single_quote.html");
+ color: rgba(10, 20, 255);
+ color: #123aef;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testIdentifiers() {
+ var errors = [];
+ final String input = r'''
+#da {
+ height: 100px;
+}
+#foo {
+ width: 10px;
+ color: #ff00cc;
+}
+''';
+ final String generated = r'''
+#da {
+ height: 100px;
+}
+#foo {
+ width: 10px;
+ color: #f0c;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testComposites() {
+ var errors = [];
+ final String input = r'''
+.xyzzy {
+ border: 10px 80px 90px 100px;
+ width: 99%;
+}
+@-webkit-keyframes pulsate {
+ 0% {
+ -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+ }
+}''';
+ final String generated = r'''
+.xyzzy {
+ border: 10px 80px 90px 100px;
+ width: 99%;
+}
+@-webkit-keyframes pulsate {
+ 0% {
+ -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+ }
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testUnits() {
+ var errors = [];
+ final String input = r'''
+#id-1 {
+ transition: color 0.4s;
+ animation-duration: 500ms;
+ top: 1em;
+ left: 200ex;
+ right: 300px;
+ bottom: 400cm;
+ border-width: 2.5mm;
+ margin-top: .5in;
+ margin-left: 5pc;
+ margin-right: 5ex;
+ margin-bottom: 5ch;
+ font-size: 10pt;
+ padding-top: 22rem;
+ padding-left: 33vw;
+ padding-right: 34vh;
+ padding-bottom: 3vmin;
+ transform: rotate(20deg);
+ voice-pitch: 10hz;
+}
+#id-2 {
+ left: 2fr;
+ font-size: 10vmax;
+ transform: rotatex(20rad);
+ voice-pitch: 10khz;
+ -web-kit-resolution: 2dpi; /* Bogus property name testing dpi unit. */
+}
+#id-3 {
+ -web-kit-resolution: 3dpcm; /* Bogus property name testing dpi unit. */
+ transform: rotatey(20grad);
+}
+#id-4 {
+ -web-kit-resolution: 4dppx; /* Bogus property name testing dpi unit. */
+ transform: rotatez(20turn);
+}
+''';
+
+ final String generated = r'''
+#id-1 {
+ transition: color 0.4s;
+ animation-duration: 500ms;
+ top: 1em;
+ left: 200ex;
+ right: 300px;
+ bottom: 400cm;
+ border-width: 2.5mm;
+ margin-top: .5in;
+ margin-left: 5pc;
+ margin-right: 5ex;
+ margin-bottom: 5ch;
+ font-size: 10pt;
+ padding-top: 22rem;
+ padding-left: 33vw;
+ padding-right: 34vh;
+ padding-bottom: 3vmin;
+ transform: rotate(20deg);
+ voice-pitch: 10hz;
+}
+#id-2 {
+ left: 2fr;
+ font-size: 10vmax;
+ transform: rotatex(20rad);
+ voice-pitch: 10khz;
+ -web-kit-resolution: 2dpi;
+}
+#id-3 {
+ -web-kit-resolution: 3dpcm;
+ transform: rotatey(20grad);
+}
+#id-4 {
+ -web-kit-resolution: 4dppx;
+ transform: rotatez(20turn);
+}''';
+
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testUnicode() {
+ var errors = [];
+ final String input = r'''
+.toggle:after {
+ content: '✔';
+ line-height: 43px;
+ font-size: 20px;
+ color: #d9d9d9;
+ text-shadow: 0 -1px 0 #bfbfbf;
+}
+''';
+
+ final String generated = r'''
+.toggle:after {
+ content: "✔";
+ line-height: 43px;
+ font-size: 20px;
+ color: #d9d9d9;
+ text-shadow: 0 -1px 0 #bfbfbf;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testNewerCss() {
+ var errors = [];
+ final String input = r'''
+@media screen,print {
+ .foobar_screen {
+ width: 10px;
+ }
+}
+@page {
+ height: 22px;
+ size: 3in 3in;
+}
+@page : left {
+ width: 10px;
+}
+@page bar : left { @top-left { margin: 8px; } }
+@charset "ISO-8859-1";
+@charset 'ASCII';''';
+
+ final String generated = r'''
+@media screen, print {
+.foobar_screen {
+ width: 10px;
+}
+}
+@page {
+ height: 22px;
+ size: 3in 3in;
+}
+@page:left {
+ width: 10px;
+}
+@page bar:left {
+@top-left {
+ margin: 8px;
+}
+}
+@charset "ISO-8859-1";
+@charset "ASCII";''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testMediaQueries() {
+ var errors = [];
+ String input = '''
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .todo-item .toggle {
+ background: none;
+ }
+ #todo-item .toggle {
+ height: 40px;
+ }
+}''';
+ String generated = '''
+@media screen AND (-webkit-min-device-pixel-ratio:0) {
+.todo-item .toggle {
+ background: none;
+}
+#todo-item .toggle {
+ height: 40px;
+}
+}''';
+
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ input = '''
+ @media handheld and (min-width: 20em),
+ screen and (min-width: 20em) {
+ #id { color: red; }
+ .myclass { height: 20px; }
+ }
+ @media print and (min-resolution: 300dpi) {
+ #anotherId {
+ color: #fff;
+ }
+ }
+ @media print and (min-resolution: 280dpcm) {
+ #finalId {
+ color: #aaa;
+ }
+ .class2 {
+ border: 20px;
+ }
+ }''';
+ generated =
+ '''@media handheld AND (min-width:20em), screen AND (min-width:20em) {
+#id {
+ color: #f00;
+}
+.myclass {
+ height: 20px;
+}
+} @media print AND (min-resolution:300dpi) {
+#anotherId {
+ color: #fff;
+}
+} @media print AND (min-resolution:280dpcm) {
+#finalId {
+ color: #aaa;
+}
+.class2 {
+ border: 20px;
+}
+}''';
+
+ stylesheet = parseCss(input, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ input = '''
+@media only screen and (min-device-width: 4000px) and
+ (min-device-height: 2000px), screen (another: 100px) {
+ html {
+ font-size: 10em;
+ }
+ }''';
+ generated = '@media ONLY screen AND (min-device-width:4000px) '
+ 'AND (min-device-height:2000px), screen (another:100px) {\n'
+ 'html {\n font-size: 10em;\n}\n}';
+
+ stylesheet = parseCss(input, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ input = '''
+@media screen,print (min-device-width: 4000px) and
+ (min-device-height: 2000px), screen (another: 100px) {
+ html {
+ font-size: 10em;
+ }
+ }''';
+ generated = '@media screen, print (min-device-width:4000px) AND '
+ '(min-device-height:2000px), screen (another:100px) {\n'
+ 'html {\n font-size: 10em;\n}\n}';
+
+ stylesheet = parseCss(input, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ input = '''
+@import "test.css" ONLY screen, NOT print (min-device-width: 4000px);''';
+ generated =
+ '@import "test.css" ONLY screen, NOT print (min-device-width:4000px);';
+
+ stylesheet = parseCss(input, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testFontFace() {
+ var errors = [];
+
+ final String input = '''
+@font-face {
+ font-family: BBCBengali;
+ src: url(fonts/BBCBengali.ttf) format("opentype");
+ unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
+}''';
+ final String generated = '''@font-face {
+ font-family: BBCBengali;
+ src: url("fonts/BBCBengali.ttf") format("opentype");
+ unicode-range: U+0A-FF, U+980-9FF, U+????, U+3???;
+}''';
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ final String input1 = '''
+@font-face {
+ font-family: Gentium;
+ src: url(http://example.com/fonts/Gentium.ttf);
+ src: url(http://example.com/fonts/Gentium.ttf);
+}''';
+ final String generated1 = '''@font-face {
+ font-family: Gentium;
+ src: url("http://example.com/fonts/Gentium.ttf");
+ src: url("http://example.com/fonts/Gentium.ttf");
+}''';
+
+ stylesheet = parseCss(input1, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated1);
+
+ final String input2 = '''
+@font-face {
+src: url(ideal-sans-serif.woff) format("woff"),
+ url(basic-sans-serif.ttf) format("opentype"),
+ local(Gentium Bold);
+}''';
+ final String generated2 =
+ '@font-face {\n'
+ ' src: url("ideal-sans-serif.woff") '
+ 'format("woff"), url("basic-sans-serif.ttf") '
+ 'format("opentype"), local(Gentium Bold);\n}';
+
+ stylesheet = parseCss(input2, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+
+ final String input3 = '''@font-face {
+ font-family: MyGentium Text Ornaments;
+ src: local(Gentium Bold), /* full font name */
+ local(Gentium-Bold), /* Postscript name */
+ url(GentiumBold.ttf); /* otherwise, download it */
+ font-weight: bold;
+}''';
+ final String generated3 = '''@font-face {
+ font-family: MyGentium Text Ornaments;
+ src: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
+ font-weight: bold;
+}''';
+
+ stylesheet = parseCss(input3, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated3);
+
+ final String input4 = '''
+@font-face {
+ font-family: STIXGeneral;
+ src: local(STIXGeneral), url(/stixfonts/STIXGeneral.otf);
+ unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+}''';
+ final String generated4 = '''@font-face {
+ font-family: STIXGeneral;
+ src: local(STIXGeneral), url("/stixfonts/STIXGeneral.otf");
+ unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+}''';
+ stylesheet = parseCss(input4, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated4);
+}
+
+void testCssFile() {
+ var errors = [];
+ final String input = r'''
+@import 'simple.css'
+@import "test.css" print
+@import url(test.css) screen, print
+@import url(http://google.com/maps/maps.css);
+
+div[href^='test'] {
+ height: 10px;
+}
+
+@-webkit-keyframes pulsate {
+ from {
+ -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+ }
+ 10% {
+ -webkit-transform: translate3d(0, 0, 0) scale(1.0);
+ }
+ 30% {
+ -webkit-transform: translate3d(0, 2, 0) scale(1.0);
+ }
+}
+
+.foobar {
+ grid-columns: 10px ("content" 1fr 10px)[4];
+}
+
+.test-background {
+ background: url(http://www.foo.com/bar.png);
+}
+''';
+
+ final String generated =
+ '@import "simple.css"; '
+ '@import "test.css" print; '
+ '@import "test.css" screen, print; '
+ '@import "http://google.com/maps/maps.css";\n'
+ 'div[href^="test"] {\n'
+ ' height: 10px;\n'
+ '}\n'
+ '@-webkit-keyframes pulsate {\n'
+ ' from {\n'
+ ' -webkit-transform: translate3d(0, 0, 0) scale(1.0);\n'
+ ' }\n'
+ ' 10% {\n'
+ ' -webkit-transform: translate3d(0, 0, 0) scale(1.0);\n'
+ ' }\n'
+ ' 30% {\n'
+ ' -webkit-transform: translate3d(0, 2, 0) scale(1.0);\n'
+ ' }\n'
+ '}\n'
+ '.foobar {\n'
+ ' grid-columns: 10px ("content" 1fr 10px) [4];\n'
+ '}\n'
+ '.test-background {\n'
+ ' background: url("http://www.foo.com/bar.png");\n'
+ '}';
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testCompactEmitter() {
+ var errors = [];
+
+ // Check !import compactly emitted.
+ final String input = r'''
+div {
+ color: green !important;
+}
+''';
+ final String generated = "div { color: green!important; }";
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(compactOuptut(stylesheet), generated);
+
+ // Check namespace directive compactly emitted.
+ final String input2 = "@namespace a url(http://www.example.org/a);";
+ final String generated2 = "@namespace a url(http://www.example.org/a);";
+
+ var stylesheet2 = parseCss(input2, errors: errors..clear());
+
+ expect(stylesheet2 != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(compactOuptut(stylesheet2), generated2);
+}
+
+void testNotSelectors() {
+ var errors = [];
+
+ final String input = r'''
+.details:not(.open-details) x-element,
+.details:not(.open-details) .summary {
+ overflow: hidden;
+}
+
+.details:not(.open-details) x-icon {
+ margin-left: 99px;
+}
+
+.kind-class .details:not(.open-details) x-icon {
+ margin-left: 0px;
+}
+
+.name {
+ margin-left: 0px;
+}
+
+.details:not(.open-details) .the-class {
+ width: 80px;
+}
+
+*:focus
+{
+ outline: none;
+}
+
+body > h2:not(:first-of-type):not(:last-of-type) {
+ color: red;
+}
+
+.details-1:not([DISABLED]) {
+ outline: none;
+}
+
+html|*:not(:link):not(:visited) {
+ width: 92%;
+}
+
+*|*:not(*) {
+ font-weight: bold;
+}
+
+*:not(:not([disabled])) { color: blue; }
+''';
+ final String generated = r'''
+.details:not(.open-details) x-element, .details:not(.open-details) .summary {
+ overflow: hidden;
+}
+.details:not(.open-details) x-icon {
+ margin-left: 99px;
+}
+.kind-class .details:not(.open-details) x-icon {
+ margin-left: 0px;
+}
+.name {
+ margin-left: 0px;
+}
+.details:not(.open-details) .the-class {
+ width: 80px;
+}
+*:focus {
+ outline: none;
+}
+body > h2:not(:first-of-type):not(:last-of-type) {
+ color: #f00;
+}
+.details-1:not([DISABLED]) {
+ outline: none;
+}
+html|*:not(:link):not(:visited) {
+ width: 92%;
+}
+*|*:not(*) {
+ font-weight: bold;
+}
+*:not(:not([disabled])) {
+ color: #00f;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testIE() {
+ var errors = [];
+ final String input =
+".test {\n"
+" filter: progid:DXImageTransform.Microsoft.gradient"
+"(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');\n"
+"}";
+ final String generated =
+".test {\n"
+" filter: progid:DXImageTransform.Microsoft.gradient"
+"(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');\n"
+"}";
+
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ final String input2 =
+".test {\n"
+" filter: progid:DXImageTransform.Microsoft.gradient"
+"(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670')\n"
+" progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);\n"
+"}";
+
+ final String generated2 =
+".test {\n"
+" filter: progid:DXImageTransform.Microsoft.gradient"
+"(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670')\n"
+" progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);\n"
+"}";
+
+ stylesheet = parseCss(input2, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+
+ final String input3 = '''
+div {
+ filter: alpha(opacity=80); /* IE7 and under */
+ -ms-filter: "Alpha(Opacity=40)"; /* IE8 and newer */
+
+ Filter: Blur(Add = 0, Direction = 225, Strength = 10);
+ Filter: FlipV;
+ Filter: Gray;
+ FILTER: Chroma(Color = #000000) Mask(Color=#00FF00);
+ Filter: Alpha(Opacity=100, FinishOpacity=0, Style=2, StartX=20, StartY=40,
+ FinishX=0, FinishY=0) Wave(Add=0, Freq=5, LightStrength=20,
+ Phase=220, Strength=10);
+}
+''';
+ final String generated3 = 'div {\n filter: alpha(opacity=80);\n'
+ ' -ms-filter: "Alpha(Opacity=40)";\n'
+ ' Filter: Blur(Add = 0, Direction = 225, Strength = 10);\n'
+ ' Filter: FlipV;\n Filter: Gray;\n'
+ ' FILTER: Chroma(Color = #000000) Mask(Color=#00FF00);\n'
+ ' Filter: Alpha(Opacity=100, FinishOpacity=0, Style=2, '
+ 'StartX=20, StartY=40, \n'
+ ' FinishX=0, FinishY=0) Wave(Add=0, Freq=5, LightStrength=20, \n'
+ ' Phase=220, Strength=10);\n}';
+
+ stylesheet = parseCss(input3, errors: errors..clear(), opts: options);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated3);
+}
+
+/**
+ * Test IE specific declaration syntax:
+ * IE6 property name prefixed with _ (normal CSS property name can start
+ * with an underscore).
+ *
+ * IE7 or below property add asterisk before the CSS property.
+ *
+ * IE8 or below add \9 at end of declaration expression e.g.,
+ * background: red\9;
+ */
+void testIEDeclaration() {
+ var errors = [];
+
+ final input = '''
+.testIE-6 {
+ _zoom : 5;
+}
+.clearfix {
+ *zoom: 1;
+}
+audio, video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+.uneditable-input:focus {
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ outline: thin dotted \\9; /* IE6-9 */
+}
+
+input[type="radio"], input[type="checkbox"] {
+ margin-top: 1px \\9;
+ *margin-top: 0;
+}
+
+input.search-query {
+ padding-right: 14px;
+ padding-right: 4px \\9;
+ padding-left: 14px;
+ padding-left: 4px \\9; /* IE7-8 no border-radius, don't indent padding. */
+}
+
+.btn.active {
+ background-color: #cccccc \\9;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+from {
+background-position: 40px 0;
+}
+to {
+background-position: 0 0;
+}
+}
+
+@-moz-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+@-ms-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+@-o-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}''';
+
+ final generated = '''.testIE-6 {
+ _zoom: 5;
+}
+.clearfix {
+ *zoom: 1;
+}
+audio, video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+.uneditable-input:focus {
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ outline: thin dotted \\9;
+}
+input[type="radio"], input[type="checkbox"] {
+ margin-top: 1px \\9;
+ *margin-top: 0;
+}
+input.search-query {
+ padding-right: 14px;
+ padding-right: 4px \\9;
+ padding-left: 14px;
+ padding-left: 4px \\9;
+}
+.btn.active {
+ background-color: #ccc \\9;
+}
+@-webkit-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+@-moz-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+@-o-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}''';
+
+ var stylesheet = parseCss(input, errors: errors, opts: options);
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void testHangs() {
+ final optionErrors = ['--no-colors', '--warnings_as_errors', 'memory'];
+ var errors = [];
+
+ // Bad hexvalue had caused a hang in processTerm.
+ final input = r'''#a { color: #ebebeburl(0/IE8+9+); }''';
+ var stylesheet = parseCss(input, errors: errors, opts: optionErrors);
+
+ expect(stylesheet != null, true);
+ expect(errors.length, 3, reason: errors.toString());
+
+ var errorMessage = errors[0];
+ expect(errorMessage.message, contains('Bad hex number'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 0);
+ expect(errorMessage.span.start.column, 12);
+ expect(errorMessage.span.text, '#ebebeburl');
+
+ errorMessage = errors[1];
+ expect(errorMessage.message, contains('expected }, but found +'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 0);
+ expect(errorMessage.span.start.column, 30);
+ expect(errorMessage.span.text, '+');
+
+ errorMessage = errors[2];
+ expect(errorMessage.message, contains('premature end of file unknown CSS'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 0);
+ expect(errorMessage.span.start.column, 31);
+ expect(errorMessage.span.text, ')');
+
+ // Missing closing parenthesis for keyframes.
+ final input2 = r'''@-ms-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+''';
+
+ stylesheet = parseCss(input2, errors: errors..clear(), opts: optionErrors);
+
+ expect(stylesheet != null, true);
+
+ expect(errors.length, 1, reason: errors.toString());
+
+ errorMessage = errors[0];
+ expect(errorMessage.message, contains('unexpected end of file'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 7);
+ expect(errorMessage.span.start.column, 0);
+ expect(errorMessage.span.text, '');
+}
+
+main() {
+ test('Simple Terms', testSimpleTerms);
+ test('Declarations', testDeclarations);
+ test('Identifiers', testIdentifiers);
+ test('Composites', testComposites);
+ test('Units', testUnits);
+ test('Unicode', testUnicode);
+ test('Newer CSS', testNewerCss);
+ test('Media Queries', testMediaQueries);
+ test('Font-Face', testFontFace);
+ test('CSS file', testCssFile);
+ test('Compact Emitter', testCompactEmitter);
+ test('Selector Negation', testNotSelectors);
+ test('IE stuff', testIE);
+ test('IE declaration syntax', testIEDeclaration);
+ test('Hanging bugs', testHangs);
+}
diff --git a/test/error_test.dart b/test/error_test.dart
new file mode 100644
index 0000000..98acffc
--- /dev/null
+++ b/test/error_test.dart
@@ -0,0 +1,355 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library error_test;
+
+import 'package:unittest/unittest.dart';
+import 'testing.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'package:csslib/src/messages.dart';
+
+/**
+ * Test for unsupported font-weights values of bolder, lighter and inherit.
+ */
+void testUnsupportedFontWeights() {
+ var errors = [];
+
+ // TODO(terry): Need to support bolder.
+ // font-weight value bolder.
+ var input = ".foobar { font-weight: bolder; }";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unknown property value bolder
+.foobar { font-weight: bolder; }
+ ^^^^^^''');
+ expect(stylesheet != null, true);
+
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ font-weight: bolder;
+}''');
+
+ // TODO(terry): Need to support lighter.
+ // font-weight value lighter.
+ input = ".foobar { font-weight: lighter; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unknown property value lighter
+.foobar { font-weight: lighter; }
+ ^^^^^^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ font-weight: lighter;
+}''');
+
+ // TODO(terry): Need to support inherit.
+ // font-weight value inherit.
+ input = ".foobar { font-weight: inherit; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unknown property value inherit
+.foobar { font-weight: inherit; }
+ ^^^^^^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ font-weight: inherit;
+}''');
+}
+
+/**
+ * Test for unsupported line-height values of units other than px, pt and
+ * inherit.
+ */
+void testUnsupportedLineHeights() {
+ var errors = [];
+
+ // line-height value in percentge unit.
+ var input = ".foobar { line-height: 120%; }";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unexpected value for line-height
+.foobar { line-height: 120%; }
+ ^^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ line-height: 120%;
+}''');
+
+ // TODO(terry): Need to support all units.
+ // line-height value in cm unit.
+ input = ".foobar { line-height: 20cm; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unexpected unit for line-height
+.foobar { line-height: 20cm; }
+ ^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ line-height: 20cm;
+}''');
+
+ // TODO(terry): Need to support inherit.
+ // line-height value inherit.
+ input = ".foobar { line-height: inherit; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:24: Unknown property value inherit
+.foobar { line-height: inherit; }
+ ^^^^^^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ line-height: inherit;
+}''');
+}
+
+/** Test for bad selectors. */
+void testBadSelectors() {
+ var errors = [];
+
+ // Invalid id selector.
+ var input = "# foo { color: #ff00ff; }";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:1: Not a valid ID selector expected #id
+# foo { color: #ff00ff; }
+^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+# foo {
+ color: #f0f;
+}''');
+
+ // Invalid class selector.
+ input = ". foo { color: #ff00ff; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:1: Not a valid class selector expected .className
+. foo { color: #ff00ff; }
+^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+. foo {
+ color: #f0f;
+}''');
+}
+
+/** Test for bad hex values. */
+void testBadHexValues() {
+ var errors = [];
+
+ // Invalid hex value.
+ var input = ".foobar { color: #AH787; }";
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:18: Bad hex number
+.foobar { color: #AH787; }
+ ^^^^^^''');
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ color: #AH787;
+}''');
+
+ // Bad color constant.
+ input = ".foobar { color: redder; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:18: Unknown property value redder
+.foobar { color: redder; }
+ ^^^^^^''');
+
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ color: redder;
+}''');
+
+ // Bad hex color #<space>ffffff.
+ input = ".foobar { color: # ffffff; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:18: Expected hex number
+.foobar { color: # ffffff; }
+ ^''');
+
+ expect(stylesheet != null, true);
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ color: # ffffff;
+}''');
+
+ // Bad hex color #<space>123fff.
+ input = ".foobar { color: # 123fff; }";
+ stylesheet = parseCss(input, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:18: Expected hex number
+.foobar { color: # 123fff; }
+ ^''');
+
+ expect(stylesheet != null, true);
+
+ // Formating is off with an extra space. However, the entire value is bad
+ // and isn't processed anyway.
+ expect(prettyPrint(stylesheet), r'''
+.foobar {
+ color: # 123 fff;
+}''');
+
+}
+
+void testBadUnicode() {
+ var errors = [];
+ final String input = '''
+@font-face {
+ src: url(fonts/BBCBengali.ttf) format("opentype");
+ unicode-range: U+400-200;
+}''';
+
+ var stylesheet = parseCss(input, errors: errors);
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(),
+ 'error :3:20: unicode first range can not be greater than last\n'
+ ' unicode-range: U+400-200;\n'
+ ' ^^^^^^^');
+
+ final String input2 = '''
+@font-face {
+ src: url(fonts/BBCBengali.ttf) format("opentype");
+ unicode-range: U+12FFFF;
+}''';
+
+ stylesheet = parseCss(input2, errors: errors..clear());
+
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(),
+ 'error :3:20: unicode range must be less than 10FFFF\n'
+ ' unicode-range: U+12FFFF;\n'
+ ' ^^^^^^');
+}
+
+void testBadNesting() {
+ var errors = [];
+
+ // Test for bad declaration in a nested rule.
+ final String input = '''
+div {
+ width: 20px;
+ span + ul { color: blue; }
+ span + ul > #aaaa {
+ color: #ffghghgh;
+ }
+ background-color: red;
+}
+''';
+
+ var stylesheet = parseCss(input, errors: errors);
+ expect(errors.length, 1);
+ var errorMessage = messages.messages[0];
+ expect(errorMessage.message, contains('Bad hex number'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 4);
+ expect(errorMessage.span.start.column, 11);
+ expect(errorMessage.span.text, '#ffghghgh');
+
+ // Test for bad selector syntax.
+ final String input2 = '''
+div {
+ span + ul #aaaa > (3333) {
+ color: #ffghghgh;
+ }
+}
+''';
+ var stylesheet2 = parseCss(input2, errors: errors..clear());
+ expect(errors.length, 4);
+ errorMessage = messages.messages[0];
+ expect(errorMessage.message, contains(':, but found +'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 1);
+ expect(errorMessage.span.start.column, 7);
+ expect(errorMessage.span.text, '+');
+
+ errorMessage = messages.messages[1];
+ expect(errorMessage.message, contains('Unknown property value ul'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 1);
+ expect(errorMessage.span.start.column, 9);
+ expect(errorMessage.span.text, 'ul');
+
+ errorMessage = messages.messages[2];
+ expect(errorMessage.message, contains('expected }, but found >'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 1);
+ expect(errorMessage.span.start.column, 18);
+ expect(errorMessage.span.text, '>');
+
+ errorMessage = messages.messages[3];
+ expect(errorMessage.message, contains('premature end of file unknown CSS'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 1);
+ expect(errorMessage.span.start.column, 20);
+ expect(errorMessage.span.text, '(');
+
+ // Test for missing close braces and bad declaration.
+ final String input3 = '''
+div {
+ span {
+ color: #green;
+}
+''';
+ var stylesheet3 = parseCss(input3, errors: errors..clear());
+ expect(errors.length, 2);
+ errorMessage = messages.messages[0];
+ expect(errorMessage.message, contains('Bad hex number'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 2);
+ expect(errorMessage.span.start.column, 11);
+ expect(errorMessage.span.text, '#green');
+
+ errorMessage = messages.messages[1];
+ expect(errorMessage.message, contains('expected }, but found end of file'));
+ expect(errorMessage.span, isNotNull);
+ expect(errorMessage.span.start.line, 3);
+ expect(errorMessage.span.start.column, 1);
+ expect(errorMessage.span.text, '\n');
+}
+
+main() {
+ test('font-weight value errors', testUnsupportedFontWeights);
+ test('line-height value errors', testUnsupportedLineHeights);
+ test('bad selectors', testBadSelectors);
+ test('bad Hex values', testBadHexValues);
+ test('bad unicode ranges', testBadUnicode);
+ test('nested rules', testBadNesting);
+}
diff --git a/test/nested_test.dart b/test/nested_test.dart
new file mode 100644
index 0000000..e6b0025
--- /dev/null
+++ b/test/nested_test.dart
@@ -0,0 +1,614 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library nested_test;
+
+import 'dart:utf';
+import 'package:unittest/unittest.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'testing.dart';
+
+List optionsCss = ['--no-colors', 'memory'];
+
+compileAndValidate(String input, String generated) {
+ var errors = [];
+ var stylesheet = compileCss(input, errors: errors, opts: optionsCss);
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+selectorVariations() {
+ final input1 = r'''html { color: red; }''';
+ final generated1 = r'''html {
+ color: #f00;
+}''';
+ compileAndValidate(input1, generated1);
+
+ final input2 = r'''button { span { height: 200 } }''';
+ final generated2 = r'''button {
+}
+button span {
+ height: 200;
+}''';
+ compileAndValidate(input2, generated2);
+
+ final input3 = r'''div { color: red; } button { span { height: 200 } }''';
+ final generated3 = r'''div {
+ color: #f00;
+}
+button {
+}
+button span {
+ height: 200;
+}''';
+ compileAndValidate(input3, generated3);
+
+ final input4 = r'''#header { color: red; h1 { font-size: 26px; } }''';
+ final generated4 = r'''#header {
+ color: #f00;
+}
+#header h1 {
+ font-size: 26px;
+}''';
+ compileAndValidate(input4, generated4);
+
+ final input5 = r'''
+#header {
+ color: red;
+ h1 { font-size: 26px; }
+ background-color: blue;
+}''';
+ final generated5 = r'''#header {
+ color: #f00;
+ background-color: #00f;
+}
+#header h1 {
+ font-size: 26px;
+}''';
+ compileAndValidate(input5, generated5);
+
+ final input6 = r'''html { body {color: red; }}''';
+ final generated6 = r'''html {
+}
+html body {
+ color: #f00;
+}''';
+ compileAndValidate(input6, generated6);
+
+ final input7 = r'''html body {color: red; }''';
+ final generated7 = r'''html body {
+ color: #f00;
+}''';
+ compileAndValidate(input7, generated7);
+
+ final input8 = r'''
+html, body { color: red; }
+button { height: 200 }
+body { width: 300px; }''';
+ final generated8 = r'''html, body {
+ color: #f00;
+}
+button {
+ height: 200;
+}
+body {
+ width: 300px;
+}''';
+ compileAndValidate(input8, generated8);
+
+ final input9 = '''
+html, body {
+ color: red;
+ button { height: 200 }
+ div { width: 300px; }
+}''';
+ final generated9 = r'''html, body {
+ color: #f00;
+}
+html button, body button {
+ height: 200;
+}
+html div, body div {
+ width: 300px;
+}''';
+ compileAndValidate(input9, generated9);
+
+ final input10 = '''
+html {
+ color: red;
+ button, div { height: 200 }
+ body { width: 300px; }
+}''';
+ final generated10 = r'''html {
+ color: #f00;
+}
+html button, html div {
+ height: 200;
+}
+html body {
+ width: 300px;
+}''';
+ compileAndValidate(input10, generated10);
+
+ final input11 = '''
+html, body {
+ color: red;
+ button, div { height: 200 }
+ table { width: 300px; }
+}''';
+ final generated11 = r'''html, body {
+ color: #f00;
+}
+html button, body button, html div, body div {
+ height: 200;
+}
+html table, body table {
+ width: 300px;
+}''';
+ compileAndValidate(input11, generated11);
+
+ final input12 = '''
+html, body {
+ color: red;
+ button, div {
+ span, a, ul { height: 200 }
+ }
+ table { width: 300px; }
+}''';
+ final generated12 = r'''html, body {
+ color: #f00;
+}
+'''
+'html button span, body button span, html div span, body div span, '
+'html button a, body button a, html div a, body div a, html button ul, '
+r'''body button ul, html div ul, body div ul {
+ height: 200;
+}
+html table, body table {
+ width: 300px;
+}''';
+ compileAndValidate(input12, generated12);
+
+ final input13 = r'''
+#header {
+ div {
+ width: 100px;
+ a { height: 200px; }
+ }
+ color: blue;
+}
+span { color: #1f1f1f; }
+''';
+ final generated13 = r'''#header {
+ color: #00f;
+}
+#header div {
+ width: 100px;
+}
+#header div a {
+ height: 200px;
+}
+span {
+ color: #1f1f1f;
+}''';
+ compileAndValidate(input13, generated13);
+}
+
+void simpleNest() {
+ final errors = [];
+ final input = '''
+div span { color: green; }
+#header {
+ color: red;
+ h1 {
+ font-size: 26px;
+ font-weight: bold;
+ }
+ p {
+ font-size: 12px;
+ a {
+ text-decoration: none;
+ }
+ }
+ background-color: blue;
+}
+div > span[attr="foo"] { color: yellow; }
+''';
+
+ final generated =
+r'''div span {
+ color: #008000;
+}
+#header {
+ color: #f00;
+ background-color: #00f;
+}
+#header h1 {
+ font-size: 26px;
+ font-weight: bold;
+}
+#header p {
+ font-size: 12px;
+}
+#header p a {
+ text-decoration: none;
+}
+div > span[attr="foo"] {
+ color: #ff0;
+}''';
+ compileAndValidate(input, generated);
+}
+
+void complexNest() {
+ final errors = [];
+ final input = '''
+@font-face { font-family: arial; }
+div { color: #f0f0f0; }
+#header + div {
+ color: url(abc.png);
+ *[attr="bar"] {
+ font-size: 26px;
+ font-weight: bold;
+ }
+ p~ul {
+ font-size: 12px;
+ :not(p) {
+ text-decoration: none;
+ div > span[attr="foo"] { color: yellow; }
+ }
+ }
+ background-color: blue;
+ span {
+ color: red;
+ .one { color: blue; }
+ .two { color: green; }
+ .three { color: yellow; }
+ .four {
+ .four-1 { background-color: #00000f; }
+ .four-2 { background-color: #0000ff; }
+ .four-3 { background-color: #000fff; }
+ .four-4 {
+ height: 44px;
+ .four-4-1 { height: 10px; }
+ .four-4-2 { height: 20px; }
+ .four-4-3 { height: 30px; }
+ width: 44px;
+ }
+ }
+ }
+}
+span { color: #1f1f2f; }
+''';
+
+ final generated = r'''@font-face {
+ font-family: arial;
+}
+div {
+ color: #f0f0f0;
+}
+#header + div {
+ color: url("abc.png");
+ background-color: #00f;
+}
+#header + div *[attr="bar"] {
+ font-size: 26px;
+ font-weight: bold;
+}
+#header + div p ~ ul {
+ font-size: 12px;
+}
+#header + div p ~ ul :not(p) {
+ text-decoration: none;
+}
+#header + div p ~ ul :not(p) div > span[attr="foo"] {
+ color: #ff0;
+}
+#header + div span {
+ color: #f00;
+}
+#header + div span .one {
+ color: #00f;
+}
+#header + div span .two {
+ color: #008000;
+}
+#header + div span .three {
+ color: #ff0;
+}
+#header + div span .four .four-1 {
+ background-color: #00000f;
+}
+#header + div span .four .four-2 {
+ background-color: #00f;
+}
+#header + div span .four .four-3 {
+ background-color: #000fff;
+}
+#header + div span .four .four-4 {
+ height: 44px;
+ width: 44px;
+}
+#header + div span .four .four-4 .four-4-1 {
+ height: 10px;
+}
+#header + div span .four .four-4 .four-4-2 {
+ height: 20px;
+}
+#header + div span .four .four-4 .four-4-3 {
+ height: 30px;
+}
+span {
+ color: #1f1f2f;
+}''';
+
+ compileAndValidate(input, generated);
+}
+
+void mediaNesting() {
+ var errors = [];
+
+ final input = r'''
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ #toggle-all {
+ image: url(test.jpb);
+ div, table {
+ background: none;
+ a { width: 100px; }
+ }
+ color: red;
+ }
+}
+''';
+ final generated = r'''@media screen AND (-webkit-min-device-pixel-ratio:0) {
+#toggle-all {
+ image: url("test.jpb");
+ color: #f00;
+}
+#toggle-all div, #toggle-all table {
+ background: none;
+}
+#toggle-all div a, #toggle-all table a {
+ width: 100px;
+}
+}''';
+
+ compileAndValidate(input, generated);
+}
+
+void simpleThis() {
+ final errors = [];
+ final input = '''#header {
+ h1 {
+ font-size: 26px;
+ font-weight: bold;
+ }
+ p { font-size: 12px;
+ a { text-decoration: none;
+ &:hover { border-width: 1px }
+ }
+ }
+}
+''';
+
+ final generated = r'''#header {
+}
+#header h1 {
+ font-size: 26px;
+ font-weight: bold;
+}
+#header p {
+ font-size: 12px;
+}
+#header p a {
+ text-decoration: none;
+}
+#header p a:hover {
+ border-width: 1px;
+}''';
+
+ compileAndValidate(input, generated);
+}
+
+void complexThis() {
+ var errors = [];
+
+ final input1 = r'''
+.light {
+ .leftCol {
+ .textLink {
+ color: fooL1;
+ &:hover { color: barL1;}
+ }
+ .picLink {
+ background-image: url(/fooL1.jpg);
+ &:hover { background-image: url(/barL1.jpg);}
+ }
+ .textWithIconLink {
+ color: fooL2;
+ background-image: url(/fooL2.jpg);
+ &:hover { color: barL2; background-image: url(/barL2.jpg);}
+ }
+ }
+}''';
+
+ final generated1 = r'''.light {
+}
+.light .leftCol .textLink {
+ color: fooL1;
+}
+.light .leftCol .textLink:hover {
+ color: barL1;
+}
+.light .leftCol .picLink {
+ background-image: url("/fooL1.jpg");
+}
+.light .leftCol .picLink:hover {
+ background-image: url("/barL1.jpg");
+}
+.light .leftCol .textWithIconLink {
+ color: fooL2;
+ background-image: url("/fooL2.jpg");
+}
+.light .leftCol .textWithIconLink:hover {
+ color: barL2;
+ background-image: url("/barL2.jpg");
+}''';
+
+ compileAndValidate(input1, generated1);
+
+ final input2 = r'''
+.textLink {
+ .light .leftCol & {
+ color: fooL1;
+ &:hover { color: barL1; }
+ }
+ .light .rightCol & {
+ color: fooL3;
+ &:hover { color: barL3; }
+ }
+}''';
+
+ final generated2 = r'''
+.textLink {
+}
+.light .leftCol .textLink {
+ color: fooL1;
+}
+.light .leftCol .textLink:hover {
+ color: barL1;
+}
+.light .rightCol .textLink {
+ color: fooL3;
+}
+.light .rightCol .textLink:hover {
+ color: barL3;
+}''';
+
+ compileAndValidate(input2, generated2);
+}
+
+variationsThis() {
+ var errors = [];
+
+ final input1 = r'''
+.textLink {
+ a {
+ light .leftCol & {
+ color: red;
+ }
+ }
+}''';
+ final generated1 = r'''.textLink {
+}
+light .leftCol .textLink a {
+ color: #f00;
+}''';
+
+ compileAndValidate(input1, generated1);
+
+ final input2 = r'''.textLink {
+ a {
+ & light .leftCol & {
+ color: red;
+ }
+ }
+}''';
+ final generated2 = r'''.textLink {
+}
+.textLink a light .leftCol .textLink a {
+ color: #f00;
+}''';
+ compileAndValidate(input2, generated2);
+
+ final input3 = r'''
+.textLink {
+ a {
+ & light .leftCol { color: red; }
+ }
+}''';
+ final generated3 = r'''.textLink {
+}
+.textLink a light .leftCol {
+ color: #f00;
+}''';
+ compileAndValidate(input3, generated3);
+
+ final input4 = r'''
+.textLink {
+ a {
+ & light .leftCol { color: red; }
+ &:hover { width: 100px; }
+ }
+}''';
+ final generated4 = r'''.textLink {
+}
+.textLink a light .leftCol {
+ color: #f00;
+}
+.textLink a:hover {
+ width: 100px;
+}''';
+ compileAndValidate(input4, generated4);
+
+ final input5 = r'''.textLink { a { &:hover { color: red; } } }''';
+ final generated5 = r'''.textLink {
+}
+.textLink a:hover {
+ color: #f00;
+}''';
+
+ compileAndValidate(input5, generated5);
+
+ final input6 = r'''.textLink { &:hover { color: red; } }''';
+ final generated6 = r'''.textLink {
+}
+.textLink:hover {
+ color: #f00;
+}''';
+ compileAndValidate(input6, generated6);
+
+ final input7 = r'''.textLink { a { & + & { color: red; } } }''';
+ final generated7 = r'''.textLink {
+}
+.textLink a + .textLink a {
+ color: #f00;
+}''';
+ compileAndValidate(input7, generated7);
+
+ final input8 = r'''.textLink { a { & { color: red; } } }''';
+ final generated8 = r'''.textLink {
+}
+.textLink a {
+ color: #f00;
+}''';
+ compileAndValidate(input8, generated8);
+
+ final input9 = r'''.textLink { a { & ~ & { color: red; } } }''';
+ final generated9 = r'''.textLink {
+}
+.textLink a ~ .textLink a {
+ color: #f00;
+}''';
+ compileAndValidate(input9, generated9);
+
+ final input10 = r'''.textLink { a { & & { color: red; } } }''';
+ final generated10 = r'''.textLink {
+}
+.textLink a .textLink a {
+ color: #f00;
+}''';
+ compileAndValidate(input10, generated10);
+}
+
+main() {
+ test('Selector and Nested Variations', selectorVariations);
+ test('Simple nesting', simpleNest);
+ test('Complex nesting', complexNest);
+ test('@media nesting', mediaNesting);
+ test('Simple &', simpleThis);
+ test("Variations &", variationsThis);
+ test('Complex &', complexThis);
+}
diff --git a/test/run.sh b/test/run.sh
new file mode 100755
index 0000000..7d26209
--- /dev/null
+++ b/test/run.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Usage: call directly in the commandline as test/run.sh ensuring that you have
+# 'dart' in your path. Filter tests by passing a pattern as an argument to this
+# script.
+
+# TODO(sigmund): replace with a real test runner
+
+# bail on error
+set -e
+
+# print commands executed by this script
+# set -x
+
+DIR=$( cd $( dirname "${BASH_SOURCE[0]}" ) && pwd )
+DART_FLAGS="--checked"
+TEST_PATTERN=$1
+
+if [[ ($TEST_PATTERN == "") ]]; then
+ # Note: dart_analyzer needs to be run from the root directory for proper path
+ # canonicalization.
+ pushd $DIR/.. &>/dev/null
+ echo Analyzing compiler for warnings or type errors
+ dartanalyzer --fatal-warnings --fatal-type-errors bin/css.dart
+ popd &>/dev/null
+fi
+
+pushd $DIR &>/dev/null
+if [[ ($TEST_PATTERN == "canary") || ($TEST_PATTERN = "") ]]; then
+ dart $DART_FLAGS run_all.dart
+else
+ dart $DART_FLAGS run_all.dart $TEST_PATTERN
+fi
+popd &>/dev/null
+
+echo All tests completed.
diff --git a/test/run_all.dart b/test/run_all.dart
new file mode 100644
index 0000000..517d5a0
--- /dev/null
+++ b/test/run_all.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/**
+ * This is a helper for run.sh. We try to run all of the Dart code in one
+ * instance of the Dart VM to reduce warm-up time.
+ */
+library run_impl;
+
+import 'dart:io';
+import 'package:unittest/unittest.dart';
+import 'package:unittest/compact_vm_config.dart';
+import 'testing.dart';
+
+import 'compiler_test.dart' as compiler_test;
+import 'declaration_test.dart' as declaration_test;
+import 'var_test.dart' as var_test;
+import 'nested_test.dart' as nested_test;
+import 'error_test.dart' as error_test;
+import 'selector_test.dart' as selector_test;
+import 'visitor_test.dart' as visitor_test;
+
+main() {
+ var args = new Options().arguments;
+
+ var pattern = new RegExp(args.length > 0 ? args[0] : '.');
+
+ useCompactVMConfiguration();
+ useMockMessages();
+
+ if (pattern.hasMatch('compiler_test.dart')) compiler_test.main();
+ if (pattern.hasMatch('declaration_test.dart')) declaration_test.main();
+ if (pattern.hasMatch('var_test.dart')) var_test.main();
+ if (pattern.hasMatch('nested_test.dart')) nested_test.main();
+ if (pattern.hasMatch('selector_test.dart')) selector_test.main();
+ if (pattern.hasMatch('visitor_test.dart')) visitor_test.main();
+ if (pattern.hasMatch('error_test.dart')) error_test.main();
+}
diff --git a/test/selector_test.dart b/test/selector_test.dart
new file mode 100644
index 0000000..ad015bb
--- /dev/null
+++ b/test/selector_test.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library selector_test;
+
+import 'package:unittest/unittest.dart';
+import 'testing.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+
+void testSelectorSuccesses() {
+ var errors = [];
+ var selectorAst = selector('#div .foo', errors: errors);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('#div .foo', compactOuptut(selectorAst));
+
+ // Valid selectors for class names.
+ selectorAst = selector('.foo', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('.foo', compactOuptut(selectorAst));
+
+ selectorAst = selector('.foobar .xyzzy', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('.foobar .xyzzy', compactOuptut(selectorAst));
+
+ selectorAst = selector('.foobar .a-story .xyzzy', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('.foobar .a-story .xyzzy', compactOuptut(selectorAst));
+
+ selectorAst = selector('.foobar .xyzzy .a-story .b-story',
+ errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('.foobar .xyzzy .a-story .b-story', compactOuptut(selectorAst));
+
+ // Valid selectors for element IDs.
+ selectorAst = selector('#id1', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('#id1', compactOuptut(selectorAst));
+
+ selectorAst = selector('#id-number-3', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('#id-number-3', compactOuptut(selectorAst));
+
+ selectorAst = selector('#_privateId', errors: errors..clear());
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect('#_privateId', compactOuptut(selectorAst));
+}
+
+// TODO(terry): Move this failure case to a failure_test.dart when the analyzer
+// and validator exit then they'll be a bunch more checks.
+void testSelectorFailures() {
+ var errors = [];
+
+ // Test for invalid class name (can't start with number).
+ var selectorAst = selector('.foobar .1a-story .xyzzy', errors: errors);
+ expect(errors.isEmpty, false);
+ expect(errors[0].toString(), r'''
+error :1:9: name must start with a alpha character, but found a number
+.foobar .1a-story .xyzzy
+ ^^''');
+}
+
+main() {
+ test('Valid Selectors', testSelectorSuccesses);
+ test('Invalid Selectors', testSelectorFailures);
+}
diff --git a/test/testing.dart b/test/testing.dart
new file mode 100644
index 0000000..e7b14bd
--- /dev/null
+++ b/test/testing.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/** Common definitions used for setting up the test environment. */
+library testing;
+
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'package:csslib/src/messages.dart';
+
+useMockMessages() {
+ messages = new Messages(printHandler: (message) {});
+}
+
+/**
+ * Spin-up CSS parser in checked mode to detect any problematic CSS. Normally,
+ * CSS will allow any property/value pairs regardless of validity; all of our
+ * tests (by default) will ensure that the CSS is really valid.
+ */
+StyleSheet parseCss(String cssInput, {List errors, List opts}) =>
+ parse(cssInput, errors: errors, options: opts == null ?
+ ['--no-colors', '--checked', '--warnings_as_errors', 'memory'] : opts);
+
+/**
+ * Spin-up CSS parser in checked mode to detect any problematic CSS. Normally,
+ * CSS will allow any property/value pairs regardless of validity; all of our
+ * tests (by default) will ensure that the CSS is really valid.
+ */
+StyleSheet compileCss(String cssInput, {List errors, List opts}) =>
+ compile(cssInput, errors: errors, options: opts == null ?
+ ['--no-colors', '--checked', '--warnings_as_errors', 'memory'] : opts);
+
+/** CSS emitter walks the style sheet tree and emits readable CSS. */
+var _emitCss = new CssPrinter();
+
+/** Simple Visitor does nothing but walk tree. */
+var _cssVisitor = new Visitor();
+
+/** Pretty printer for CSS. */
+String prettyPrint(StyleSheet ss) {
+ // Walk the tree testing basic Vistor class.
+ walkTree(ss);
+ return (_emitCss..visitTree(ss, pretty: true)).toString();
+}
+
+/**
+ * Helper function to emit compact (non-pretty printed) CSS for suite test
+ * comparsions. Spaces, new lines, etc. are reduced for easier comparsions of
+ * expected suite test results.
+ */
+String compactOuptut(StyleSheet ss) {
+ walkTree(ss);
+ return (_emitCss..visitTree(ss, pretty: false)).toString();
+}
+
+/** Walks the style sheet tree does nothing; insures the basic walker works. */
+void walkTree(StyleSheet ss) {
+ _cssVisitor..visitTree(ss);
+}
+
+String dumpTree(StyleSheet ss) => treeToDebugString(ss);
+
+
diff --git a/test/var_test.dart b/test/var_test.dart
new file mode 100644
index 0000000..fb870c7
--- /dev/null
+++ b/test/var_test.dart
@@ -0,0 +1,629 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library var_test;
+
+import 'dart:utf';
+import 'package:unittest/unittest.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'testing.dart';
+
+void simpleVar() {
+ final errors = [];
+ final input = ''':root {
+ var-color-background: red;
+ var-color-foreground: blue;
+
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #00ff00;
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(color-background);
+}
+''';
+
+ final generated = ''':root {
+ var-color-background: #f00;
+ var-color-foreground: #00f;
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #0f0;
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(color-background);
+}''';
+
+ var stylesheet = compileCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void expressionsVar() {
+ final errors = [];
+ final input = ''':root {
+ var-color-background: red;
+ var-color-foreground: blue;
+
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #00ff00;
+
+ var-image: url(test.png);
+
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30EM;
+ var-width: .6in;
+ var-length: 1.2in;
+ var-web-stuff: -10Px;
+ var-rgba: rgba(10,20,255);
+ var-transition: color 0.4s;
+ var-transform: rotate(20deg);
+ var-content: "✔";
+ var-text-shadow: 0 -1px 0 #bfbfbf;
+ var-font-family: Gentium;
+ var-src: url("http://example.com/fonts/Gentium.ttf");
+ var-src-1: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
+ var-unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+ var-unicode-range-1: U+0A-FF, U+980-9FF, U+????, U+3???;
+ var-grid-columns: 10px ("content" 1fr 10px) [4];
+}
+
+.testIt {
+ color: var(color-foreground);
+ background: var(c);
+ background-image: var(image);
+
+ border-width: var(b-width);
+ margin-width: var(m-width);
+ border-height: var(b-height);
+ width: var(width);
+ length: var(length);
+ -web-stuff: var(web-stuff);
+ background-color: var(rgba);
+
+ transition: var(transition);
+ transform: var(transform);
+ content: var(content);
+ text-shadow: var(text-shadow);
+}
+
+@font-face {
+ font-family: var(font-family);
+ src: var(src);
+ unicode-range: var(unicode-range);
+}
+
+@font-face {
+ font-family: var(font-family);
+ src: var(src-1);
+ unicode-range: var(unicode-range-1);
+}
+
+.foobar {
+ grid-columns: var(grid-columns);
+}
+''';
+
+ final generated = ''':root {
+ var-color-background: #f00;
+ var-color-foreground: #00f;
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #0f0;
+ var-image: url("test.png");
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30em;
+ var-width: .6in;
+ var-length: 1.2in;
+ var-web-stuff: -10px;
+ var-rgba: rgba(10, 20, 255);
+ var-transition: color 0.4s;
+ var-transform: rotate(20deg);
+ var-content: "✔";
+ var-text-shadow: 0 -1px 0 #bfbfbf;
+ var-font-family: Gentium;
+ var-src: url("http://example.com/fonts/Gentium.ttf");
+ var-src-1: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
+ var-unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+ var-unicode-range-1: U+0A-FF, U+980-9FF, U+????, U+3???;
+ var-grid-columns: 10px ("content" 1fr 10px) [4];
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(c);
+ background-image: var(image);
+ border-width: var(b-width);
+ margin-width: var(m-width);
+ border-height: var(b-height);
+ width: var(width);
+ length: var(length);
+ -web-stuff: var(web-stuff);
+ background-color: var(rgba);
+ transition: var(transition);
+ transform: var(transform);
+ content: var(content);
+ text-shadow: var(text-shadow);
+}
+@font-face {
+ font-family: var(font-family);
+ src: var(src);
+ unicode-range: var(unicode-range);
+}
+@font-face {
+ font-family: var(font-family);
+ src: var(src-1);
+ unicode-range: var(unicode-range-1);
+}
+.foobar {
+ grid-columns: var(grid-columns);
+}''';
+
+ var stylesheet = compileCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void defaultVar() {
+ final errors = [];
+ final input = '''
+:root {
+ var-color-background: red;
+ var-color-foreground: blue;
+
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #00ff00;
+
+ var-image: url(test.png);
+
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30EM;
+}
+
+.test {
+ background-color: var(test, orange);
+}
+
+body {
+ background: var(a) var(image) no-repeat right top;
+}
+
+div {
+ background: var(color-background) url('img_tree.png') no-repeat right top;
+}
+
+.test-2 {
+ background: var(color-background) var(image-2, url('img_1.png'))
+ no-repeat right top;
+}
+
+.test-3 {
+ background: var(color-background) var(image-2) no-repeat right top;
+}
+
+.test-4 {
+ background: #ffff00 var(image) no-repeat right top;
+}
+
+.test-5 {
+ background: var(test-color, var(a)) var(image) no-repeat right top;
+}
+
+.test-6 {
+ border: red var(a-1, solid 20px);
+}
+''';
+
+ final generated = ''':root {
+ var-color-background: #f00;
+ var-color-foreground: #00f;
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #0f0;
+ var-image: url("test.png");
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30em;
+}
+.test {
+ background-color: var(test, #ffa500);
+}
+body {
+ background: var(a) var(image) no-repeat right top;
+}
+div {
+ background: var(color-background) url("img_tree.png") no-repeat right top;
+}
+.test-2 {
+ background: var(color-background) var(image-2, url("img_1.png")) no-repeat right top;
+}
+.test-3 {
+ background: var(color-background) var(image-2) no-repeat right top;
+}
+.test-4 {
+ background: #ff0 var(image) no-repeat right top;
+}
+.test-5 {
+ background: var(test-color, var(a)) var(image) no-repeat right top;
+}
+.test-6 {
+ border: #f00 var(a-1, solid 20px);
+}''';
+
+ var stylesheet = compileCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+void cyclesVar() {
+ final errors = [];
+ final input = ''':root {
+ var-color-background: red;
+ var-color-foreground: blue;
+
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #00ff00;
+
+ var-one: var(two);
+ var-two: var(one);
+
+ var-four: var(five);
+ var-five: var(six);
+ var-six: var(four);
+
+ var-def-1: var(def-2);
+ var-def-2: var(def-3);
+ var-def-3: var(def-2);
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(color-background);
+}
+.test-2 {
+ color: var(one);
+}
+''';
+
+ final generated = ''':root {
+ var-color-background: #f00;
+ var-color-foreground: #00f;
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #0f0;
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(color-background);
+}
+.test-2 {
+ color: var(one);
+}''';
+
+ var stylesheet = compileCss(input, errors: errors,
+ opts: ['--no-colors', '--warnings_as_errors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.length, 8, reason: errors.toString());
+ expect(errors[0].toString(),
+ 'error :14:3: var cycle detected var-six\n'
+ ' var-six: var(four);\n'
+ ' ^^^^^^^^^^^^^^^^^^');
+ expect(errors[1].toString(),
+ 'error :18:3: var cycle detected var-def-3\n'
+ ' var-def-3: var(def-2);\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^');
+ expect(errors[2].toString(),
+ 'error :10:3: var cycle detected var-two\n'
+ ' var-two: var(one);\n'
+ ' ^^^^^^^^^^^^^^^^^');
+ expect(errors[3].toString(),
+ 'error :17:3: var cycle detected var-def-2\n'
+ ' var-def-2: var(def-3);\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^');
+ expect(errors[4].toString(),
+ 'error :16:3: var cycle detected var-def-1\n'
+ ' var-def-1: var(def-2);\n'
+ ' ^^^^^^^^^^^^^^^^^^^^^');
+ expect(errors[5].toString(),
+ 'error :13:3: var cycle detected var-five\n'
+ ' var-five: var(six);\n'
+ ' ^^^^^^^^^^^^^^^^^^');
+ expect(errors[6].toString(),
+ 'error :9:3: var cycle detected var-one\n'
+ ' var-one: var(two);\n'
+ ' ^^^^^^^^^^^^^^^^^');
+ expect(errors[7].toString(),
+ 'error :12:3: var cycle detected var-four\n'
+ ' var-four: var(five);\n'
+ ' ^^^^^^^^^^^^^^^^^^^');
+ expect(prettyPrint(stylesheet), generated);
+}
+
+parserVar() {
+ final errors = [];
+ final input = ''':root {
+ var-color-background: red;
+ var-color-foreground: blue;
+
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #00ff00;
+
+ var-image: url(test.png);
+
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30EM;
+ var-width: .6in;
+ var-length: 1.2in;
+ var-web-stuff: -10Px;
+ var-rgba: rgba(10,20,255);
+ var-transition: color 0.4s;
+ var-transform: rotate(20deg);
+ var-content: "✔";
+ var-text-shadow: 0 -1px 0 #bfbfbf;
+ var-font-family: Gentium;
+ var-src: url("http://example.com/fonts/Gentium.ttf");
+ var-src-1: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
+ var-unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+ var-unicode-range-1: U+0A-FF, U+980-9FF, U+????, U+3???;
+ var-grid-columns: 10px ("content" 1fr 10px) [4];
+}
+
+.testIt {
+ color: var(color-foreground);
+ background: var(c);
+ background-image: var(image);
+
+ border-width: var(b-width);
+ margin-width: var(m-width);
+ border-height: var(b-height);
+ width: var(width);
+ length: var(length);
+ -web-stuff: var(web-stuff);
+ background-color: var(rgba);
+
+ transition: var(transition);
+ transform: var(transform);
+ content: var(content);
+ text-shadow: var(text-shadow);
+}
+
+@font-face {
+ font-family: var(font-family);
+ src: var(src);
+ unicode-range: var(unicode-range);
+}
+
+@font-face {
+ font-family: var(font-family);
+ src: var(src-1);
+ unicode-range: var(unicode-range-1);
+}
+
+.foobar {
+ grid-columns: var(grid-columns);
+}
+''';
+
+ final generated = ''':root {
+ var-color-background: #f00;
+ var-color-foreground: #00f;
+ var-a: var(b);
+ var-b: var(c);
+ var-c: #0f0;
+ var-image: url("test.png");
+ var-b-width: 20cm;
+ var-m-width: 33%;
+ var-b-height: 30em;
+ var-width: .6in;
+ var-length: 1.2in;
+ var-web-stuff: -10px;
+ var-rgba: rgba(10, 20, 255);
+ var-transition: color 0.4s;
+ var-transform: rotate(20deg);
+ var-content: "✔";
+ var-text-shadow: 0 -1px 0 #bfbfbf;
+ var-font-family: Gentium;
+ var-src: url("http://example.com/fonts/Gentium.ttf");
+ var-src-1: local(Gentium Bold), local(Gentium-Bold), url("GentiumBold.ttf");
+ var-unicode-range: U+000-49F, U+2000-27FF, U+2900-2BFF, U+1D400-1D7FF;
+ var-unicode-range-1: U+0A-FF, U+980-9FF, U+????, U+3???;
+ var-grid-columns: 10px ("content" 1fr 10px) [4];
+}
+.testIt {
+ color: var(color-foreground);
+ background: var(c);
+ background-image: var(image);
+ border-width: var(b-width);
+ margin-width: var(m-width);
+ border-height: var(b-height);
+ width: var(width);
+ length: var(length);
+ -web-stuff: var(web-stuff);
+ background-color: var(rgba);
+ transition: var(transition);
+ transform: var(transform);
+ content: var(content);
+ text-shadow: var(text-shadow);
+}
+@font-face {
+ font-family: var(font-family);
+ src: var(src);
+ unicode-range: var(unicode-range);
+}
+@font-face {
+ font-family: var(font-family);
+ src: var(src-1);
+ unicode-range: var(unicode-range-1);
+}
+.foobar {
+ grid-columns: var(grid-columns);
+}''';
+
+ var stylesheet = parseCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+}
+
+testVar() {
+ final errors = [];
+ final input = '''
+@color-background: red;
+@color-foreground: blue;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}
+''';
+ final generated = '''
+var-color-background: #f00;
+var-color-foreground: #00f;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}''';
+
+ var stylesheet = parseCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ stylesheet = compileCss(input, errors: errors..clear(),
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ final input2 = '''
+@color-background: red;
+@color-foreground: blue;
+
+.test {
+ background-color: @color-background;
+ color: @color-foreground;
+}
+''';
+ final generated2 = '''var-color-background: #f00;
+var-color-foreground: #00f;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}''';
+
+ stylesheet = parseCss(input, errors: errors..clear(),
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+
+ stylesheet = compileCss(input2, errors: errors..clear(),
+ opts: ['--no-colors', 'memory', '--no-less']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+}
+
+testLess() {
+ final errors = [];
+ final input = '''
+@color-background: red;
+@color-foreground: blue;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}
+''';
+ final generated = '''var-color-background: #f00;
+var-color-foreground: #00f;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}''';
+
+ var stylesheet = parseCss(input, errors: errors,
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ stylesheet = compileCss(input, errors: errors..clear(),
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated);
+
+ final input2 = '''
+@color-background: red;
+@color-foreground: blue;
+
+.test {
+ background-color: @color-background;
+ color: @color-foreground;
+}
+''';
+ final generated2 = '''var-color-background: #f00;
+var-color-foreground: #00f;
+
+.test {
+ background-color: var(color-background);
+ color: var(color-foreground);
+}''';
+
+ stylesheet = parseCss(input, errors: errors..clear(),
+ opts: ['--no-colors', 'memory']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+
+ stylesheet = compileCss(input2, errors: errors..clear(),
+ opts: ['--no-colors', 'memory', '--no-less']);
+
+ expect(stylesheet != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+ expect(prettyPrint(stylesheet), generated2);
+}
+
+main() {
+ test('Simple var', simpleVar);
+ test('Expressions var', expressionsVar);
+ test('Default value in var()', defaultVar);
+ test('CSS Parser only var', parserVar);
+ test('Var syntax', testVar);
+ test('Cycles var', cyclesVar);
+ test('Less syntax', testLess);
+}
diff --git a/test/visitor_test.dart b/test/visitor_test.dart
new file mode 100644
index 0000000..e60dbc5
--- /dev/null
+++ b/test/visitor_test.dart
@@ -0,0 +1,114 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library visitor_test;
+
+import 'dart:utf';
+import 'package:unittest/unittest.dart';
+import 'package:csslib/parser.dart';
+import 'package:csslib/visitor.dart';
+import 'testing.dart';
+
+class ClassVisitor extends Visitor {
+ final List expectedClasses;
+ final Set<String> foundClasses = new Set();
+
+ ClassVisitor(this.expectedClasses);
+
+ void visitClassSelector(ClassSelector node) {
+ foundClasses.add(node.name);
+ }
+
+ bool get matches {
+ bool match = true;
+ foundClasses.forEach((value) {
+ match = match && expectedClasses.contains(value);
+ });
+ expectedClasses.forEach((value) {
+ match = match && foundClasses.contains(value);
+ });
+
+ return match;
+ }
+}
+
+void testClassVisitors() {
+ var errors = [];
+ var in1 = '.foobar { }';
+
+ var s = parseCss(in1, errors: errors);
+
+ expect(s != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ var clsVisits = new ClassVisitor(['foobar'])..visitTree(s);
+ expect(clsVisits.matches, true);
+
+ in1= '''
+ .foobar1 { }
+ .xyzzy .foo #my-div { color: red; }
+ div.hello { font: arial; }
+ ''';
+
+ s = parseCss(in1, errors: errors..clear(), opts: ['--no-colors', 'memory']);
+
+ expect(s != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ clsVisits =
+ new ClassVisitor(['foobar1', 'xyzzy', 'foo', 'hello'])..visitTree(s);
+ expect(clsVisits.matches, true);
+
+ expect(prettyPrint(s), r'''
+.foobar1 {
+}
+.xyzzy .foo #my-div {
+ color: #f00;
+}
+div.hello {
+ font: arial;
+}''');
+}
+
+class PolyfillEmitter extends CssPrinter {
+ final String _prefix;
+
+ PolyfillEmitter(this._prefix);
+
+ void visitClassSelector(ClassSelector node) {
+ emit('.${_prefix}_${node.name}');
+ }
+}
+
+String polyfillPrint(String prefix, StyleSheet ss) =>
+ (new PolyfillEmitter(prefix)..visitTree(ss, pretty: true)).toString();
+
+void testPolyFill() {
+ var errors = [];
+ final input = r'''
+.foobar { }
+div.xyzzy { }
+#foo .foo .bar .foobar { }
+''';
+
+ final generated = r'''
+.myComponent_foobar {
+}
+div.myComponent_xyzzy {
+}
+#foo .myComponent_foo .myComponent_bar .myComponent_foobar {
+}''';
+
+ var s = parseCss(input, errors: errors);
+ expect(s != null, true);
+ expect(errors.isEmpty, true, reason: errors.toString());
+
+ final emitted = polyfillPrint('myComponent', s);
+ expect(emitted, generated);
+}
+
+main() {
+ test('Class Visitors', testClassVisitors);
+ test('Polyfill', testPolyFill);
+}