Support parsing identifiers with escape codes in them

The output of `CssPrinter` will now also retain escape codes in identifiers.
This ensures they remain valid identifiers, as the escaped values may not parse
as valid identifiers.

The parser will also no longer accept an ID or class selector with space between
the first token (`#` or `.` respectively) and the identifier. The parser will
now fail immediately on these selector errors instead of attempting to recover.
Recovering in a robust manner is difficult given that this parser immediately
attempts to parse at the selector granularity, rather than first parsing the
style sheet into rules as described [here][parse-rule].

[parse-rule]: https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule

Fixes https://github.com/dart-lang/csslib/issues/58.
diff --git a/lib/parser.dart b/lib/parser.dart
index ca1f465..099240d 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -1307,12 +1307,14 @@
     var selectors = <Selector>[];
     var start = _peekToken.span;
 
+    tokenizer.inSelector = true;
     do {
       var selector = processSelector();
       if (selector != null) {
         selectors.add(selector);
       }
     } while (_maybeEat(TokenKind.COMMA));
+    tokenizer.inSelector = false;
 
     if (selectors.isNotEmpty) {
       return SelectorGroup(selectors, _makeSpan(start));
@@ -1501,35 +1503,20 @@
       case TokenKind.HASH:
         _eat(TokenKind.HASH);
 
-        var hasWhiteSpace = false;
         if (_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
-          _warning('Not a valid ID selector expected #id', _makeSpan(start));
-          hasWhiteSpace = true;
+          _error('Not a valid ID selector expected #id', _makeSpan(start));
+          return null;
         }
-        if (_peekIdentifier()) {
-          var id = identifier();
-          if (hasWhiteSpace) {
-            // Generate bad selector id (normalized).
-            id.name = ' ${id.name}';
-          }
-          return IdSelector(id, _makeSpan(start));
-        }
-        return null;
+        return IdSelector(identifier(), _makeSpan(start));
       case TokenKind.DOT:
         _eat(TokenKind.DOT);
 
-        var hasWhiteSpace = false;
         if (_anyWhiteSpaceBeforePeekToken(TokenKind.DOT)) {
-          _warning('Not a valid class selector expected .className',
+          _error('Not a valid class selector expected .className',
               _makeSpan(start));
-          hasWhiteSpace = true;
+          return null;
         }
-        var id = identifier();
-        if (hasWhiteSpace) {
-          // Generate bad selector class (normalized).
-          id.name = ' ${id.name}';
-        }
-        return ClassSelector(id, _makeSpan(start));
+        return ClassSelector(identifier(), _makeSpan(start));
       case TokenKind.COLON:
         // :pseudo-class ::pseudo-element
         return processPseudoSelector(start);
diff --git a/lib/src/tree.dart b/lib/src/tree.dart
index 8a17b53..2961b6f 100644
--- a/lib/src/tree.dart
+++ b/lib/src/tree.dart
@@ -20,7 +20,12 @@
   dynamic visit(VisitorBase visitor) => visitor.visitIdentifier(this);
 
   @override
-  String toString() => name;
+  String toString() {
+    // Try to use the identifier's original lexeme to preserve any escape codes
+    // as authored. The name, which may include escaped values, may no longer be
+    // a valid identifier.
+    return span?.text ?? name;
+  }
 }
 
 class Wildcard extends TreeNode {
@@ -274,7 +279,7 @@
   String valueToString() {
     if (value != null) {
       if (value is Identifier) {
-        return value.name;
+        return value.toString();
       } else {
         return '"$value"';
       }
diff --git a/test/error_test.dart b/test/error_test.dart
index d242176..5822c4c 100644
--- a/test/error_test.dart
+++ b/test/error_test.dart
@@ -134,37 +134,27 @@
 
   // Invalid id selector.
   var input = '# foo { color: #ff00ff; }';
-  var stylesheet = parseCss(input, errors: errors);
+  parseCss(input, errors: errors);
 
-  expect(errors.isEmpty, false);
+  expect(errors, isNotEmpty);
   expect(errors[0].toString(), r'''
 error on line 1, column 1: Not a valid ID selector expected #id

 1 │ # 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());
+  parseCss(input, errors: errors..clear());
 
-  expect(errors.isEmpty, false);
+  expect(errors, isNotEmpty);
   expect(errors[0].toString(), r'''
 error on line 1, column 1: Not a valid class selector expected .className

 1 │ . foo { color: #ff00ff; }
   │ ^
   ╵''');
-  expect(stylesheet != null, true);
-  expect(prettyPrint(stylesheet), r'''
-. foo {
-  color: #f0f;
-}''');
 }
 
 /// Test for bad hex values.
diff --git a/test/escape_codes_test.dart b/test/escape_codes_test.dart
new file mode 100644
index 0000000..af66469
--- /dev/null
+++ b/test/escape_codes_test.dart
@@ -0,0 +1,28 @@
+import 'package:csslib/parser.dart';
+import 'package:test/test.dart';
+
+import 'testing.dart';
+
+void main() {
+  final errors = <Message>[];
+
+  tearDown(() {
+    errors.clear();
+  });
+
+  group('handles escape codes', () {
+    group('in an identifier', () {
+      test('with trailing space', () {
+        final selectorAst = selector(r'.\35 00px', errors: errors);
+        expect(errors, isEmpty);
+        expect(compactOuptut(selectorAst), r'.\35 00px');
+      });
+
+      test('in an attribute selector value', () {
+        final selectorAst = selector(r'[elevation=\31]', errors: errors);
+        expect(errors, isEmpty);
+        expect(compactOuptut(selectorAst), r'[elevation=\31]');
+      });
+    });
+  });
+}