Rewrite the pkg/yaml parser.
This increases the speed by about 10x. It also adds a number of new featurse.
BUG=21317, 20859
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//689513002
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/yaml@41649 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68af5f5..f454255 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## 2.1.0
+
+* Rewrite the parser for a 10x speed improvement.
+
+* Support anchors and aliases (`&foo` and `*foo`).
+
+* Support explicit tags (e.g. `!!str`). Note that user-defined tags are still
+ not fully supported.
+
+* `%YAML` and `%TAG` directives are now parsed, although again user-defined tags
+ are not fully supported.
+
+* `YamlScalar`, `YamlList`, and `YamlMap` now expose the styles in which they
+ were written (for example plain vs folded, block vs flow).
+
+* A `yamlWarningCallback` field is exposed. This field can be used to customize
+ how YAML warnings are displayed.
+
## 2.0.1+1
* Fix an import in a test.
diff --git a/lib/src/composer.dart b/lib/src/composer.dart
deleted file mode 100644
index 8612067..0000000
--- a/lib/src/composer.dart
+++ /dev/null
@@ -1,183 +0,0 @@
-// 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 yaml.composer;
-
-import 'model.dart';
-import 'visitor.dart';
-import 'yaml_exception.dart';
-
-/// Takes a parsed YAML document (what the spec calls the "serialization tree")
-/// and resolves aliases, resolves tags, and parses scalars to produce the
-/// "representation graph".
-class Composer extends Visitor {
- /// The root node of the serialization tree.
- final Node _root;
-
- /// Map from anchor names to the most recent representation graph node with
- /// that anchor.
- final _anchors = <String, Node>{};
-
- /// The next id to use for the represenation graph's anchors.
- ///
- /// The spec doesn't use anchors in the representation graph, but we do so
- /// that the constructor can ensure that the same node in the representation
- /// graph produces the same native object.
- var _idCounter = 0;
-
- Composer(this._root);
-
- /// Runs the Composer to produce a representation graph.
- Node compose() => _root.visit(this);
-
- /// Returns the anchor to which an alias node refers.
- Node visitAlias(AliasNode alias) {
- if (!_anchors.containsKey(alias.anchor)) {
- throw new YamlException("No anchor for alias ${alias.anchor}.",
- alias.span);
- }
- return _anchors[alias.anchor];
- }
-
- /// Parses a scalar node according to its tag, or auto-detects the type if no
- /// tag exists.
- ///
- /// Currently this only supports the YAML core type schema.
- Node visitScalar(ScalarNode scalar) {
- if (scalar.tag.name == "!") {
- return setAnchor(scalar, parseString(scalar));
- } else if (scalar.tag.name == "?") {
- for (var fn in [parseNull, parseBool, parseInt, parseFloat]) {
- var result = fn(scalar);
- if (result != null) return result;
- }
- return setAnchor(scalar, parseString(scalar));
- }
-
- var result = _parseByTag(scalar);
- if (result != null) return setAnchor(scalar, result);
- throw new YamlException('Invalid literal for ${scalar.tag}.',
- scalar.span);
- }
-
- ScalarNode _parseByTag(ScalarNode scalar) {
- switch (scalar.tag.name) {
- case "null": return parseNull(scalar);
- case "bool": return parseBool(scalar);
- case "int": return parseInt(scalar);
- case "float": return parseFloat(scalar);
- case "str": return parseString(scalar);
- }
- throw new YamlException('Undefined tag: ${scalar.tag}.', scalar.span);
- }
-
- /// Assigns a tag to the sequence and recursively composes its contents.
- Node visitSequence(SequenceNode seq) {
- var tagName = seq.tag.name;
- if (tagName != "!" && tagName != "?" && tagName != Tag.yaml("seq")) {
- throw new YamlException("Invalid tag for sequence: ${seq.tag}.",
- seq.span);
- }
-
- var result = setAnchor(seq,
- new SequenceNode(Tag.yaml('seq'), null, seq.span));
- result.content = super.visitSequence(seq);
- return result;
- }
-
- /// Assigns a tag to the mapping and recursively composes its contents.
- Node visitMapping(MappingNode map) {
- var tagName = map.tag.name;
- if (tagName != "!" && tagName != "?" && tagName != Tag.yaml("map")) {
- throw new YamlException("Invalid tag for mapping: ${map.tag}.",
- map.span);
- }
-
- var result = setAnchor(map,
- new MappingNode(Tag.yaml('map'), null, map.span));
- result.content = super.visitMapping(map);
- return result;
- }
-
- /// If the serialization tree node [anchored] has an anchor, records that
- /// that anchor is pointing to the representation graph node [result].
- Node setAnchor(Node anchored, Node result) {
- if (anchored.anchor == null) return result;
- result.anchor = '${_idCounter++}';
- _anchors[anchored.anchor] = result;
- return result;
- }
-
- /// Parses a null scalar.
- ScalarNode parseNull(ScalarNode scalar) {
- if (new RegExp(r"^(null|Null|NULL|~|)$").hasMatch(scalar.content)) {
- return new ScalarNode(Tag.yaml("null"), scalar.span, value: null);
- } else {
- return null;
- }
- }
-
- /// Parses a boolean scalar.
- ScalarNode parseBool(ScalarNode scalar) {
- var match = new RegExp(r"^(?:(true|True|TRUE)|(false|False|FALSE))$").
- firstMatch(scalar.content);
- if (match == null) return null;
- return new ScalarNode(Tag.yaml("bool"), scalar.span,
- value: match.group(1) != null);
- }
-
- /// Parses an integer scalar.
- ScalarNode parseInt(ScalarNode scalar) {
- var match = new RegExp(r"^[-+]?[0-9]+$").firstMatch(scalar.content);
- if (match != null) {
- return new ScalarNode(Tag.yaml("int"), scalar.span,
- value: int.parse(match.group(0)));
- }
-
- match = new RegExp(r"^0o([0-7]+)$").firstMatch(scalar.content);
- if (match != null) {
- int n = int.parse(match.group(1), radix: 8);
- return new ScalarNode(Tag.yaml("int"), scalar.span, value: n);
- }
-
- match = new RegExp(r"^0x[0-9a-fA-F]+$").firstMatch(scalar.content);
- if (match != null) {
- return new ScalarNode(Tag.yaml("int"), scalar.span,
- value: int.parse(match.group(0)));
- }
-
- return null;
- }
-
- /// Parses a floating-point scalar.
- ScalarNode parseFloat(ScalarNode scalar) {
- var match = new RegExp(
- r"^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$").
- firstMatch(scalar.content);
- if (match != null) {
- // YAML allows floats of the form "0.", but Dart does not. Fix up those
- // floats by removing the trailing dot.
- var matchStr = match.group(0).replaceAll(new RegExp(r"\.$"), "");
- return new ScalarNode(Tag.yaml("float"), scalar.span,
- value: double.parse(matchStr));
- }
-
- match = new RegExp(r"^([+-]?)\.(inf|Inf|INF)$").firstMatch(scalar.content);
- if (match != null) {
- var value = match.group(1) == "-" ? -double.INFINITY : double.INFINITY;
- return new ScalarNode(Tag.yaml("float"), scalar.span, value: value);
- }
-
- match = new RegExp(r"^\.(nan|NaN|NAN)$").firstMatch(scalar.content);
- if (match != null) {
- return new ScalarNode(Tag.yaml("float"), scalar.span, value: double.NAN);
- }
-
- return null;
- }
-
- /// Parses a string scalar.
- ScalarNode parseString(ScalarNode scalar) =>
- new ScalarNode(Tag.yaml("str"), scalar.span, value: scalar.content);
-}
diff --git a/lib/src/constructor.dart b/lib/src/constructor.dart
deleted file mode 100644
index 5931dae..0000000
--- a/lib/src/constructor.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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 yaml.constructor;
-
-import 'equality.dart';
-import 'model.dart';
-import 'visitor.dart';
-import 'yaml_node.dart';
-
-/// Takes a parsed and composed YAML document (what the spec calls the
-/// "representation graph") and creates native Dart objects that represent that
-/// document.
-class Constructor extends Visitor {
- /// The root node of the representation graph.
- final Node _root;
-
- /// Map from anchor names to the most recent Dart node with that anchor.
- final _anchors = <String, YamlNode>{};
-
- Constructor(this._root);
-
- /// Runs the Constructor to produce a Dart object.
- YamlNode construct() => _root.visit(this);
-
- /// Returns the value of a scalar.
- YamlScalar visitScalar(ScalarNode scalar) =>
- new YamlScalar.internal(scalar.value, scalar.span);
-
- /// Converts a sequence into a List of Dart objects.
- YamlList visitSequence(SequenceNode seq) {
- var anchor = getAnchor(seq);
- if (anchor != null) return anchor;
- var nodes = [];
- var dartSeq = setAnchor(seq, new YamlList.internal(nodes, seq.span));
- nodes.addAll(super.visitSequence(seq));
- return dartSeq;
- }
-
- /// Converts a mapping into a [Map] of Dart objects.
- YamlMap visitMapping(MappingNode map) {
- var anchor = getAnchor(map);
- if (anchor != null) return anchor;
- var nodes = deepEqualsMap();
- var dartMap = setAnchor(map, new YamlMap.internal(nodes, map.span));
- super.visitMapping(map).forEach((k, v) => nodes[k] = v);
- return dartMap;
- }
-
- /// Returns a new Dart object wrapping the object that already represents
- /// [anchored], if such a thing exists.
- YamlNode getAnchor(Node anchored) {
- if (anchored.anchor == null) return null;
- var value = _anchors[anchored.anchor];
- if (value == null) return null;
-
- // Re-wrap [value]'s contents so that it's associated with the span of the
- // anchor rather than its original definition.
- if (value is YamlMap) {
- return new YamlMap.internal(value.nodes, anchored.span);
- } else if (value is YamlList) {
- return new YamlList.internal(value.nodes, anchored.span);
- } else {
- assert(value is YamlScalar);
- return new YamlScalar.internal(value.value, anchored.span);
- }
- }
-
- /// Records that [value] is the Dart object representing [anchored].
- YamlNode setAnchor(Node anchored, YamlNode value) {
- if (anchored.anchor == null) return value;
- _anchors[anchored.anchor] = value;
- return value;
- }
-}
diff --git a/lib/src/event.dart b/lib/src/event.dart
new file mode 100644
index 0000000..96e2f16
--- /dev/null
+++ b/lib/src/event.dart
@@ -0,0 +1,157 @@
+// Copyright (c) 2014, 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 yaml.event;
+
+import 'package:source_span/source_span.dart';
+
+import 'style.dart';
+import 'yaml_document.dart';
+
+/// An event emitted by a [Parser].
+class Event {
+ /// The event type.
+ final EventType type;
+
+ /// The span associated with the event.
+ final FileSpan span;
+
+ Event(this.type, this.span);
+
+ String toString() => type.toString();
+}
+
+/// An event indicating the beginning of a YAML document.
+class DocumentStartEvent implements Event {
+ get type => EventType.DOCUMENT_START;
+ final FileSpan span;
+
+ /// The document's `%YAML` directive, or `null` if there was none.
+ final VersionDirective versionDirective;
+
+ /// The document's `%TAG` directives, if any.
+ final List<TagDirective> tagDirectives;
+
+ /// Whether the document started implicitly (that is, without an explicit
+ /// `===` sequence).
+ final bool isImplicit;
+
+ DocumentStartEvent(this.span, {this.versionDirective,
+ List<TagDirective> tagDirectives, this.isImplicit: true})
+ : tagDirectives = tagDirectives == null ? [] : tagDirectives;
+
+ String toString() => "DOCUMENT_START";
+}
+
+/// An event indicating the end of a YAML document.
+class DocumentEndEvent implements Event {
+ get type => EventType.DOCUMENT_END;
+ final FileSpan span;
+
+ /// Whether the document ended implicitly (that is, without an explicit
+ /// `...` sequence).
+ final bool isImplicit;
+
+ DocumentEndEvent(this.span, {this.isImplicit: true});
+
+ String toString() => "DOCUMENT_END";
+}
+
+/// An event indicating that an alias was referenced.
+class AliasEvent implements Event {
+ get type => EventType.ALIAS;
+ final FileSpan span;
+
+ /// The name of the anchor.
+ final String name;
+
+ AliasEvent(this.span, this.name);
+
+ String toString() => "ALIAS $name";
+}
+
+/// A base class for events that can have anchor and tag properties associated
+/// with them.
+abstract class _ValueEvent implements Event {
+ /// The name of the value's anchor, or `null` if it wasn't anchored.
+ String get anchor;
+
+ /// The text of the value's tag, or `null` if it wasn't tagged.
+ String get tag;
+
+ String toString() {
+ var buffer = new StringBuffer('$type');
+ if (anchor != null) buffer.write(" &$anchor");
+ if (tag != null) buffer.write(" $tag");
+ return buffer.toString();
+ }
+}
+
+/// An event indicating a single scalar value.
+class ScalarEvent extends _ValueEvent {
+ get type => EventType.SCALAR;
+ final FileSpan span;
+ final String anchor;
+ final String tag;
+
+ /// The contents of the scalar.
+ final String value;
+
+ /// The style of the scalar in the original source.
+ final ScalarStyle style;
+
+ ScalarEvent(this.span, this.value, this.style, {this.anchor, this.tag});
+
+ String toString() => "${super.toString()} \"$value\"";
+}
+
+/// An event indicating the beginning of a sequence.
+class SequenceStartEvent extends _ValueEvent {
+ get type => EventType.SEQUENCE_START;
+ final FileSpan span;
+ final String anchor;
+ final String tag;
+
+ /// The style of the collection in the original source.
+ final CollectionStyle style;
+
+ SequenceStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// An event indicating the beginning of a mapping.
+class MappingStartEvent extends _ValueEvent {
+ get type => EventType.MAPPING_START;
+ final FileSpan span;
+ final String anchor;
+ final String tag;
+
+ /// The style of the collection in the original source.
+ final CollectionStyle style;
+
+ MappingStartEvent(this.span, this.style, {this.anchor, this.tag});
+}
+
+/// An enum of types of [Event] object.
+class EventType {
+ static const STREAM_START = const EventType._("STREAM_START");
+ static const STREAM_END = const EventType._("STREAM_END");
+
+ static const DOCUMENT_START = const EventType._("DOCUMENT_START");
+ static const DOCUMENT_END = const EventType._("DOCUMENT_END");
+
+ static const ALIAS = const EventType._("ALIAS");
+ static const SCALAR = const EventType._("SCALAR");
+
+ static const SEQUENCE_START = const EventType._("SEQUENCE_START");
+ static const SEQUENCE_END = const EventType._("SEQUENCE_END");
+
+ static const MAPPING_START = const EventType._("MAPPING_START");
+ static const MAPPING_END = const EventType._("MAPPING_END");
+
+ final String name;
+
+ const EventType._(this.name);
+
+ String toString() => name;
+}
diff --git a/lib/src/loader.dart b/lib/src/loader.dart
new file mode 100644
index 0000000..d80578f
--- /dev/null
+++ b/lib/src/loader.dart
@@ -0,0 +1,257 @@
+// Copyright (c) 2014, 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 yaml.loader;
+
+import 'package:source_span/source_span.dart';
+
+import 'equality.dart';
+import 'event.dart';
+import 'parser.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
+import 'yaml_node.dart';
+
+/// A loader that reads [Event]s emitted by a [Parser] and emits
+/// [YamlDocument]s.
+///
+/// This is based on the libyaml loader, available at
+/// https://github.com/yaml/libyaml/blob/master/src/loader.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Loader {
+ /// The underlying [Parser] that generates [Event]s.
+ final Parser _parser;
+
+ /// Aliases by the alias name.
+ final _aliases = new Map<String, YamlNode>();
+
+ /// The span of the entire stream emitted so far.
+ FileSpan get span => _span;
+ FileSpan _span;
+
+ /// Creates a loader that loads [source].
+ ///
+ /// [sourceUrl] can be a String or a [Uri].
+ Loader(String source, {sourceUrl})
+ : _parser = new Parser(source, sourceUrl: sourceUrl) {
+ var event = _parser.parse();
+ _span = event.span;
+ assert(event.type == EventType.STREAM_START);
+ }
+
+ /// Loads the next document from the stream.
+ ///
+ /// If there are no more documents, returns `null`.
+ YamlDocument load() {
+ if (_parser.isDone) return null;
+
+ var event = _parser.parse();
+ if (event.type == EventType.STREAM_END) {
+ _span = _span.expand(event.span);
+ return null;
+ }
+
+ var document = _loadDocument(event);
+ _span = _span.expand(document.span);
+ _aliases.clear();
+ return document;
+ }
+
+ /// Composes a document object.
+ YamlDocument _loadDocument(DocumentStartEvent firstEvent) {
+ var contents = _loadNode(_parser.parse());
+
+ var lastEvent = _parser.parse();
+ assert(lastEvent.type == EventType.DOCUMENT_END);
+
+ return new YamlDocument.internal(
+ contents,
+ firstEvent.span.expand(lastEvent.span),
+ firstEvent.versionDirective,
+ firstEvent.tagDirectives,
+ startImplicit: firstEvent.isImplicit,
+ endImplicit: lastEvent.isImplicit);
+ }
+
+ /// Composes a node.
+ YamlNode _loadNode(Event firstEvent) {
+ switch (firstEvent.type) {
+ case EventType.ALIAS: return _loadAlias(firstEvent);
+ case EventType.SCALAR: return _loadScalar(firstEvent);
+ case EventType.SEQUENCE_START: return _loadSequence(firstEvent);
+ case EventType.MAPPING_START: return _loadMapping(firstEvent);
+ default: throw "Unreachable";
+ }
+ }
+
+ /// Registers an anchor.
+ void _registerAnchor(String anchor, YamlNode node) {
+ if (anchor == null) return;
+
+ // libyaml throws an error for duplicate anchors, but example 7.1 makes it
+ // clear that they should be overridden:
+ // http://yaml.org/spec/1.2/spec.html#id2786448.
+
+ _aliases[anchor] = node;
+ }
+
+ /// Composes a node corresponding to an alias.
+ YamlNode _loadAlias(AliasEvent event) {
+ var alias = _aliases[event.name];
+ if (alias != null) return alias;
+
+ throw new YamlException("Undefined alias.", event.span);
+ }
+
+ /// Composes a scalar node.
+ YamlNode _loadScalar(ScalarEvent scalar) {
+ var node;
+ if (scalar.tag == "!") {
+ node = _parseString(scalar);
+ } else if (scalar.tag != null) {
+ node = _parseByTag(scalar);
+ } else {
+ node = _parseNull(scalar);
+ if (node == null) node = _parseBool(scalar);
+ if (node == null) node = _parseInt(scalar);
+ if (node == null) node = _parseFloat(scalar);
+ if (node == null) node = _parseString(scalar);
+ }
+
+ _registerAnchor(scalar.anchor, node);
+ return node;
+ }
+
+ /// Composes a sequence node.
+ YamlNode _loadSequence(SequenceStartEvent firstEvent) {
+ if (firstEvent.tag != "!" && firstEvent.tag != null &&
+ firstEvent.tag != "tag:yaml.org,2002:seq") {
+ throw new YamlException("Invalid tag for sequence.", firstEvent.span);
+ }
+
+ var children = [];
+ var node = new YamlList.internal(
+ children, firstEvent.span, firstEvent.style);
+ _registerAnchor(firstEvent.anchor, node);
+
+ var event = _parser.parse();
+ while (event.type != EventType.SEQUENCE_END) {
+ children.add(_loadNode(event));
+ event = _parser.parse();
+ }
+
+ setSpan(node, firstEvent.span.expand(event.span));
+ return node;
+ }
+
+ /// Composes a mapping node.
+ YamlNode _loadMapping(MappingStartEvent firstEvent) {
+ if (firstEvent.tag != "!" && firstEvent.tag != null &&
+ firstEvent.tag != "tag:yaml.org,2002:map") {
+ throw new YamlException("Invalid tag for mapping.", firstEvent.span);
+ }
+
+ var children = deepEqualsMap();
+ var node = new YamlMap.internal(
+ children, firstEvent.span, firstEvent.style);
+ _registerAnchor(firstEvent.anchor, node);
+
+ var event = _parser.parse();
+ while (event.type != EventType.MAPPING_END) {
+ var key = _loadNode(event);
+ var value = _loadNode(_parser.parse());
+ children[key] = value;
+ event = _parser.parse();
+ }
+
+ setSpan(node, firstEvent.span.expand(event.span));
+ return node;
+ }
+
+ /// Parses a scalar according to its tag name.
+ YamlScalar _parseByTag(ScalarEvent scalar) {
+ switch (scalar.tag) {
+ case "tag:yaml.org,2002:null": return _parseNull(scalar);
+ case "tag:yaml.org,2002:bool": return _parseBool(scalar);
+ case "tag:yaml.org,2002:int": return _parseInt(scalar);
+ case "tag:yaml.org,2002:float": return _parseFloat(scalar);
+ case "tag:yaml.org,2002:str": return _parseString(scalar);
+ }
+ throw new YamlException('Undefined tag: ${scalar.tag}.', scalar.span);
+ }
+
+ /// Parses a null scalar.
+ YamlScalar _parseNull(ScalarEvent scalar) {
+ // TODO(nweiz): stop using regexps.
+ // TODO(nweiz): add ScalarStyle and implicit metadata to the scalars.
+ if (new RegExp(r"^(null|Null|NULL|~|)$").hasMatch(scalar.value)) {
+ return new YamlScalar.internal(null, scalar.span, scalar.style);
+ } else {
+ return null;
+ }
+ }
+
+ /// Parses a boolean scalar.
+ YamlScalar _parseBool(ScalarEvent scalar) {
+ var match = new RegExp(r"^(?:(true|True|TRUE)|(false|False|FALSE))$").
+ firstMatch(scalar.value);
+ if (match == null) return null;
+ return new YamlScalar.internal(
+ match.group(1) != null, scalar.span, scalar.style);
+ }
+
+ /// Parses an integer scalar.
+ YamlScalar _parseInt(ScalarEvent scalar) {
+ var match = new RegExp(r"^[-+]?[0-9]+$").firstMatch(scalar.value);
+ if (match != null) {
+ return new YamlScalar.internal(
+ int.parse(match.group(0)), scalar.span, scalar.style);
+ }
+
+ match = new RegExp(r"^0o([0-7]+)$").firstMatch(scalar.value);
+ if (match != null) {
+ var n = int.parse(match.group(1), radix: 8);
+ return new YamlScalar.internal(n, scalar.span, scalar.style);
+ }
+
+ match = new RegExp(r"^0x[0-9a-fA-F]+$").firstMatch(scalar.value);
+ if (match != null) {
+ return new YamlScalar.internal(
+ int.parse(match.group(0)), scalar.span, scalar.style);
+ }
+
+ return null;
+ }
+
+ /// Parses a floating-point scalar.
+ YamlScalar _parseFloat(ScalarEvent scalar) {
+ var match = new RegExp(
+ r"^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$").
+ firstMatch(scalar.value);
+ if (match != null) {
+ // YAML allows floats of the form "0.", but Dart does not. Fix up those
+ // floats by removing the trailing dot.
+ var matchStr = match.group(0).replaceAll(new RegExp(r"\.$"), "");
+ return new YamlScalar.internal(
+ double.parse(matchStr), scalar.span, scalar.style);
+ }
+
+ match = new RegExp(r"^([+-]?)\.(inf|Inf|INF)$").firstMatch(scalar.value);
+ if (match != null) {
+ var value = match.group(1) == "-" ? -double.INFINITY : double.INFINITY;
+ return new YamlScalar.internal(value, scalar.span, scalar.style);
+ }
+
+ match = new RegExp(r"^\.(nan|NaN|NAN)$").firstMatch(scalar.value);
+ if (match != null) {
+ return new YamlScalar.internal(double.NAN, scalar.span, scalar.style);
+ }
+
+ return null;
+ }
+
+ /// Parses a string scalar.
+ YamlScalar _parseString(ScalarEvent scalar) =>
+ new YamlScalar.internal(scalar.value, scalar.span, scalar.style);
+}
diff --git a/lib/src/model.dart b/lib/src/model.dart
deleted file mode 100644
index 93cb49c..0000000
--- a/lib/src/model.dart
+++ /dev/null
@@ -1,259 +0,0 @@
-// 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 file contains the node classes for the internal representations of YAML
-/// documents. These nodes are used for both the serialization tree and the
-/// representation graph.
-library yaml.model;
-
-import 'package:source_span/source_span.dart';
-
-import 'equality.dart';
-import 'parser.dart';
-import 'visitor.dart';
-import 'yaml_exception.dart';
-
-/// The prefix for tag types defined by the YAML spec.
-const _YAML_URI_PREFIX = "tag:yaml.org,2002:";
-
-/// A tag that indicates the type of a YAML node.
-class Tag {
- /// The name of the tag, either a URI or a local tag beginning with "!".
- final String name;
-
- /// The kind of the tag.
- final TagKind kind;
-
- /// Returns the standard YAML tag URI for [type].
- static String yaml(String type) => "tag:yaml.org,2002:$type";
-
- const Tag(this.name, this.kind);
-
- const Tag.scalar(String name)
- : this(name, TagKind.SCALAR);
-
- const Tag.sequence(String name)
- : this(name, TagKind.SEQUENCE);
-
- const Tag.mapping(String name)
- : this(name, TagKind.MAPPING);
-
- /// Two tags are equal if their URIs are equal.
- operator ==(other) {
- if (other is! Tag) return false;
- return name == other.name;
- }
-
- String toString() {
- if (name.startsWith(_YAML_URI_PREFIX)) {
- return '!!${name.substring(_YAML_URI_PREFIX.length)}';
- } else {
- return '!<$name>';
- }
- }
-
- int get hashCode => name.hashCode;
-}
-
-/// An enum for kinds of tags.
-class TagKind {
- /// A tag indicating that the value is a scalar.
- static const SCALAR = const TagKind._("scalar");
-
- /// A tag indicating that the value is a sequence.
- static const SEQUENCE = const TagKind._("sequence");
-
- /// A tag indicating that the value is a mapping.
- static const MAPPING = const TagKind._("mapping");
-
- final String name;
-
- const TagKind._(this.name);
-
- String toString() => name;
-}
-
-/// The abstract class for YAML nodes.
-abstract class Node {
- /// Every YAML node has a tag that describes its type.
- Tag tag;
-
- /// Any YAML node can have an anchor associated with it.
- String anchor;
-
- /// The source span for this node.
- SourceSpan span;
-
- Node(this.tag, this.span, [this.anchor]);
-
- bool operator ==(other) {
- if (other is! Node) return false;
- return tag == other.tag;
- }
-
- int get hashCode => tag.hashCode ^ anchor.hashCode;
-
- visit(Visitor v);
-}
-
-/// A sequence node represents an ordered list of nodes.
-class SequenceNode extends Node {
- /// The nodes in the sequence.
- List<Node> content;
-
- SequenceNode(String tagName, this.content, SourceSpan span)
- : super(new Tag.sequence(tagName), span);
-
- /// Two sequences are equal if their tags and contents are equal.
- bool operator ==(other) {
- // Should be super != other; bug 2554
- if (!(super == other) || other is! SequenceNode) return false;
- if (content.length != other.content.length) return false;
- for (var i = 0; i < content.length; i++) {
- if (content[i] != other.content[i]) return false;
- }
- return true;
- }
-
- String toString() => '$tag [${content.map((e) => '$e').join(', ')}]';
-
- int get hashCode => super.hashCode ^ deepHashCode(content);
-
- visit(Visitor v) => v.visitSequence(this);
-}
-
-/// An alias node is a reference to an anchor.
-class AliasNode extends Node {
- AliasNode(String anchor, SourceSpan span)
- : super(new Tag.scalar(Tag.yaml("str")), span, anchor);
-
- visit(Visitor v) => v.visitAlias(this);
-}
-
-/// A scalar node represents all YAML nodes that have a single value.
-class ScalarNode extends Node {
- /// The string value of the scalar node, if it was created by the parser.
- final String _content;
-
- /// The Dart value of the scalar node, if it was created by the composer.
- final value;
-
- /// Creates a new Scalar node.
- ///
- /// Exactly one of [content] and [value] should be specified. Content should
- /// be specified for a newly-parsed scalar that hasn't yet been composed.
- /// Value should be specified for a composed scalar, although `null` is a
- /// valid value.
- ScalarNode(String tagName, SourceSpan span, {String content, this.value})
- : _content = content,
- super(new Tag.scalar(tagName), span);
-
- /// Two scalars are equal if their string representations are equal.
- bool operator ==(other) {
- // Should be super != other; bug 2554
- if (!(super == other) || other is! ScalarNode) return false;
- return content == other.content;
- }
-
- /// Returns the string representation of the scalar. After composition, this
- /// is equal to the canonical serialization of the value of the scalar.
- String get content => _content != null ? _content : canonicalContent;
-
- /// Returns the canonical serialization of the value of the scalar. If the
- /// value isn't given, the result of this will be "null".
- String get canonicalContent {
- if (value == null || value is bool || value is int) return '$value';
-
- if (value is num) {
- // 20 is the maximum value for this argument, which we use since YAML
- // doesn't specify a maximum.
- return value.toStringAsExponential(20).
- replaceFirst(new RegExp("0+e"), "e");
- }
-
- if (value is String) {
- // TODO(nweiz): This could be faster if we used a RegExp to check for
- // special characters and short-circuited if they didn't exist.
-
- var escapedValue = value.codeUnits.map((c) {
- switch (c) {
- case Parser.TAB: return "\\t";
- case Parser.LF: return "\\n";
- case Parser.CR: return "\\r";
- case Parser.DOUBLE_QUOTE: return '\\"';
- case Parser.NULL: return "\\0";
- case Parser.BELL: return "\\a";
- case Parser.BACKSPACE: return "\\b";
- case Parser.VERTICAL_TAB: return "\\v";
- case Parser.FORM_FEED: return "\\f";
- case Parser.ESCAPE: return "\\e";
- case Parser.BACKSLASH: return "\\\\";
- case Parser.NEL: return "\\N";
- case Parser.NBSP: return "\\_";
- case Parser.LINE_SEPARATOR: return "\\L";
- case Parser.PARAGRAPH_SEPARATOR: return "\\P";
- default:
- if (c < 0x20 || (c >= 0x7f && c < 0x100)) {
- return "\\x${zeroPad(c.toRadixString(16).toUpperCase(), 2)}";
- } else if (c >= 0x100 && c < 0x10000) {
- return "\\u${zeroPad(c.toRadixString(16).toUpperCase(), 4)}";
- } else if (c >= 0x10000) {
- return "\\u${zeroPad(c.toRadixString(16).toUpperCase(), 8)}";
- } else {
- return new String.fromCharCodes([c]);
- }
- }
- });
- return '"${escapedValue.join()}"';
- }
-
- throw new YamlException('Unknown scalar value.', span);
- }
-
- String toString() => '$tag "$content"';
-
- /// Left-pads [str] with zeros so that it's at least [length] characters
- /// long.
- String zeroPad(String str, int length) {
- assert(length >= str.length);
- var prefix = new List.filled(length - str.length, '0');
- return '${prefix.join()}$str';
- }
-
- int get hashCode => super.hashCode ^ content.hashCode;
-
- visit(Visitor v) => v.visitScalar(this);
-}
-
-/// A mapping node represents an unordered map of nodes to nodes.
-class MappingNode extends Node {
- /// The node map.
- Map<Node, Node> content;
-
- MappingNode(String tagName, this.content, SourceSpan span)
- : super(new Tag.mapping(tagName), span);
-
- /// Two mappings are equal if their tags and contents are equal.
- bool operator ==(other) {
- // Should be super != other; bug 2554
- if (!(super == other) || other is! MappingNode) return false;
- if (content.length != other.content.length) return false;
- for (var key in content.keys) {
- if (!other.content.containsKey(key)) return false;
- if (content[key] != other.content[key]) return false;
- }
- return true;
- }
-
- String toString() {
- var strContent = content.keys
- .map((k) => '${k}: ${content[k]}')
- .join(', ');
- return '$tag {$strContent}';
- }
-
- int get hashCode => super.hashCode ^ deepHashCode(content);
-
- visit(Visitor v) => v.visitMapping(this);
-}
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 94f551f..72c01dc 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -1,1900 +1,817 @@
-// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// Copyright (c) 2014, 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 yaml.parser;
-import 'dart:collection';
-
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
-import 'equality.dart';
-import 'model.dart';
+import 'event.dart';
+import 'scanner.dart';
+import 'style.dart';
+import 'token.dart';
import 'utils.dart';
+import 'yaml_document.dart';
+import 'yaml_exception.dart';
-/// Translates a string of characters into a YAML serialization tree.
+/// A parser that reads [Token]s emitted by a [Scanner] and emits [Event]s.
///
-/// This parser is designed to closely follow the spec. All productions in the
-/// spec are numbered, and the corresponding methods in the parser have the same
-/// numbers. This is certainly not the most efficient way of parsing YAML, but
-/// it is the easiest to write and read in the context of the spec.
-///
-/// Methods corresponding to productions are also named as in the spec,
-/// translating the name of the method (although not the annotation characters)
-/// into camel-case for dart style.. For example, the spec has a production
-/// named `nb-ns-plain-in-line`, and the method implementing it is named
-/// `nb_ns_plainInLine`. The exception to that rule is methods that just
-/// recognize character classes; these are named `is*`.
+/// This is based on the libyaml parser, available at
+/// https://github.com/yaml/libyaml/blob/master/src/parser.c. The license for
+/// that is available in ../../libyaml-license.txt.
class Parser {
- static const TAB = 0x9;
- static const LF = 0xA;
- static const CR = 0xD;
- static const SP = 0x20;
- static const TILDE = 0x7E;
- static const NEL = 0x85;
- static const PLUS = 0x2B;
- static const HYPHEN = 0x2D;
- static const QUESTION_MARK = 0x3F;
- static const COLON = 0x3A;
- static const COMMA = 0x2C;
- static const LEFT_BRACKET = 0x5B;
- static const RIGHT_BRACKET = 0x5D;
- static const LEFT_BRACE = 0x7B;
- static const RIGHT_BRACE = 0x7D;
- static const HASH = 0x23;
- static const AMPERSAND = 0x26;
- static const ASTERISK = 0x2A;
- static const EXCLAMATION = 0x21;
- static const VERTICAL_BAR = 0x7C;
- static const GREATER_THAN = 0x3E;
- static const SINGLE_QUOTE = 0x27;
- static const DOUBLE_QUOTE = 0x22;
- static const PERCENT = 0x25;
- static const AT = 0x40;
- static const GRAVE_ACCENT = 0x60;
+ /// The underlying [Scanner] that generates [Token]s.
+ final Scanner _scanner;
- static const NULL = 0x0;
- static const BELL = 0x7;
- static const BACKSPACE = 0x8;
- static const VERTICAL_TAB = 0xB;
- static const FORM_FEED = 0xC;
- static const ESCAPE = 0x1B;
- static const SLASH = 0x2F;
- static const BACKSLASH = 0x5C;
- static const UNDERSCORE = 0x5F;
- static const NBSP = 0xA0;
- static const LINE_SEPARATOR = 0x2028;
- static const PARAGRAPH_SEPARATOR = 0x2029;
+ /// The stack of parse states for nested contexts.
+ final _states = new List<_State>();
- static const NUMBER_0 = 0x30;
- static const NUMBER_9 = 0x39;
+ /// The current parse state.
+ var _state = _State.STREAM_START;
- static const LETTER_A = 0x61;
- static const LETTER_B = 0x62;
- static const LETTER_E = 0x65;
- static const LETTER_F = 0x66;
- static const LETTER_N = 0x6E;
- static const LETTER_R = 0x72;
- static const LETTER_T = 0x74;
- static const LETTER_U = 0x75;
- static const LETTER_V = 0x76;
- static const LETTER_X = 0x78;
+ /// The custom tag directives, by tag handle.
+ final _tagDirectives = new Map<String, TagDirective>();
- static const LETTER_CAP_A = 0x41;
- static const LETTER_CAP_F = 0x46;
- static const LETTER_CAP_L = 0x4C;
- static const LETTER_CAP_N = 0x4E;
- static const LETTER_CAP_P = 0x50;
- static const LETTER_CAP_U = 0x55;
- static const LETTER_CAP_X = 0x58;
+ /// Whether the parser has finished parsing.
+ bool get isDone => _state == _State.END;
- static const C_SEQUENCE_ENTRY = 4;
- static const C_MAPPING_KEY = 5;
- static const C_MAPPING_VALUE = 6;
- static const C_COLLECT_ENTRY = 7;
- static const C_SEQUENCE_START = 8;
- static const C_SEQUENCE_END = 9;
- static const C_MAPPING_START = 10;
- static const C_MAPPING_END = 11;
- static const C_COMMENT = 12;
- static const C_ANCHOR = 13;
- static const C_ALIAS = 14;
- static const C_TAG = 15;
- static const C_LITERAL = 16;
- static const C_FOLDED = 17;
- static const C_SINGLE_QUOTE = 18;
- static const C_DOUBLE_QUOTE = 19;
- static const C_DIRECTIVE = 20;
- static const C_RESERVED = 21;
-
- static const BLOCK_OUT = 0;
- static const BLOCK_IN = 1;
- static const FLOW_OUT = 2;
- static const FLOW_IN = 3;
- static const BLOCK_KEY = 4;
- static const FLOW_KEY = 5;
-
- static const CHOMPING_STRIP = 0;
- static const CHOMPING_KEEP = 1;
- static const CHOMPING_CLIP = 2;
-
- /// The scanner that's used to scan through the document.
- final SpanScanner _scanner;
-
- /// Whether we're parsing a bare document (that is, one that doesn't begin
- /// with `---`). Bare documents don't allow `%` immediately following
- /// newlines.
- bool _inBareDocument = false;
-
- /// The state of the scanner when it was the farthest in the document it's
- /// been.
- LineScannerState _farthestState;
-
- /// The name of the context of the farthest position that has been parsed
- /// successfully before backtracking. Used for error reporting.
- String _farthestContext = "document";
-
- /// A stack of the names of parse contexts. Used for error reporting.
- final _contextStack = <String>["document"];
-
- /// Annotations attached to ranges of the source string that add extra
- /// information to any errors that occur in the annotated range.
- final _errorAnnotations = new _RangeMap<String>();
-
- /// The buffer containing the string currently being captured.
- StringBuffer _capturedString;
-
- /// The beginning of the current section of the captured string.
- int _captureStart;
-
- /// Whether the current string capture is being overridden.
- bool _capturingAs = false;
-
- Parser(String yaml, sourceUrl)
- : _scanner = new SpanScanner(yaml, sourceUrl: sourceUrl) {
- _farthestState = _scanner.state;
- }
-
- /// Returns the character at the current position, then moves that position
- /// forward one character.
- int next() => _scanner.readChar();
-
- /// Returns the code unit at the current position, or the character [i]
- /// characters after the current position.
- int peek([int i = 0]) => _scanner.peekChar(i);
-
- /// The truthiness operator. Returns `false` if [obj] is `null` or `false`,
- /// `true` otherwise.
- bool truth(obj) => obj != null && obj != false;
-
- /// Consumes the current character if it matches [matcher]. Returns the result
- /// of [matcher].
- bool consume(bool matcher(int)) {
- if (matcher(peek())) {
- next();
- return true;
- }
- return false;
- }
-
- /// Consumes the current character if it equals [char].
- bool consumeChar(int char) => consume((c) => c == char);
-
- /// Calls [consumer] until it returns a falsey value. Returns a list of all
- /// truthy return values of [consumer], or null if it didn't consume anything.
+ /// Creates a parser that parses [source].
///
- /// Conceptually, repeats a production one or more times.
- List oneOrMore(consumer()) {
- var first = consumer();
- if (!truth(first)) return null;
- var out = [first];
- while (true) {
- var el = consumer();
- if (!truth(el)) return out;
- out.add(el);
- }
- return null; // Unreachable.
- }
+ /// [sourceUrl] can be a String or a [Uri].
+ Parser(String source, {sourceUrl})
+ : _scanner = new Scanner(source, sourceUrl: sourceUrl);
- /// Calls [consumer] until it returns a falsey value. Returns a list of all
- /// truthy return values of [consumer], or the empty list if it didn't consume
- /// anything.
- ///
- /// Conceptually, repeats a production any number of times.
- List zeroOrMore(consumer()) {
- var out = [];
- var oldPos = _scanner.position;
- while (true) {
- var el = consumer();
- if (!truth(el) || oldPos == _scanner.position) return out;
- oldPos = _scanner.position;
- out.add(el);
- }
- return null; // Unreachable.
- }
-
- /// Just calls [consumer] and returns its result. Used to make it explicit
- /// that a production is intended to be optional.
- zeroOrOne(consumer()) => consumer();
-
- /// Calls each function in [consumers] until one returns a truthy value, then
- /// returns that.
- or(List<Function> consumers) {
- for (var c in consumers) {
- var res = c();
- if (truth(res)) return res;
- }
- return null;
- }
-
- /// Calls [consumer] and returns its result, but rolls back the parser state
- /// if [consumer] returns a falsey value.
- transaction(consumer()) {
- var oldState = _scanner.state;
- var oldCaptureStart = _captureStart;
- String capturedSoFar = _capturedString == null ? null :
- _capturedString.toString();
- var res = consumer();
- _refreshFarthestState();
- if (truth(res)) return res;
-
- _scanner.state = oldState;
- _captureStart = oldCaptureStart;
- _capturedString = capturedSoFar == null ? null :
- new StringBuffer(capturedSoFar);
- return res;
- }
-
- /// Consumes [n] characters matching [matcher], or none if there isn't a
- /// complete match. The first argument to [matcher] is the character code, the
- /// second is the index (from 0 to [n] - 1).
- ///
- /// Returns whether or not the characters were consumed.
- bool nAtOnce(int n, bool matcher(int c, int i)) => transaction(() {
- for (int i = 0; i < n; i++) {
- if (!consume((c) => matcher(c, i))) return false;
- }
- return true;
- });
-
- /// Consumes the exact characters in [str], or nothing.
- ///
- /// Returns whether or not the string was consumed.
- bool rawString(String str) =>
- nAtOnce(str.length, (c, i) => str.codeUnitAt(i) == c);
-
- /// Consumes and returns a string of characters matching [matcher], or null if
- /// there are no such characters.
- String stringOf(bool matcher(int)) =>
- captureString(() => oneOrMore(() => consume(matcher)));
-
- /// Calls [consumer] and returns the string that was consumed while doing so,
- /// or null if [consumer] returned a falsey value. Automatically wraps
- /// [consumer] in `transaction`.
- String captureString(consumer()) {
- // captureString calls may not be nested
- assert(_capturedString == null);
-
- _captureStart = _scanner.position;
- _capturedString = new StringBuffer();
- var res = transaction(consumer);
- if (!truth(res)) {
- _captureStart = null;
- _capturedString = null;
- return null;
- }
-
- flushCapture();
- var result = _capturedString.toString();
- _captureStart = null;
- _capturedString = null;
- return result;
- }
-
- captureAs(String replacement, consumer()) =>
- captureAndTransform(consumer, (_) => replacement);
-
- captureAndTransform(consumer(), String transformation(String captured)) {
- if (_capturedString == null) return consumer();
- if (_capturingAs) return consumer();
-
- flushCapture();
- _capturingAs = true;
- var res = consumer();
- _capturingAs = false;
- if (!truth(res)) return res;
-
- _capturedString.write(transformation(
- _scanner.string.substring(_captureStart, _scanner.position)));
- _captureStart = _scanner.position;
- return res;
- }
-
- void flushCapture() {
- _capturedString.write(_scanner.string.substring(
- _captureStart, _scanner.position));
- _captureStart = _scanner.position;
- }
-
- /// Adds a tag and an anchor to [node], if they're defined.
- Node addProps(Node node, Pair<Tag, String> props) {
- if (props == null || node == null) return node;
- if (truth(props.first)) node.tag = props.first;
- if (truth(props.last)) node.anchor = props.last;
- return node;
- }
-
- /// Creates a MappingNode from [pairs].
- MappingNode map(List<Pair<Node, Node>> pairs, SourceSpan span) {
- var content = new Map<Node, Node>();
- pairs.forEach((pair) => content[pair.first] = pair.last);
- return new MappingNode("?", content, span);
- }
-
- /// Runs [fn] in a context named [name]. Used for error reporting.
- context(String name, fn()) {
+ /// Consumes and returns the next event.
+ Event parse() {
try {
- _contextStack.add(name);
- return fn();
- } finally {
- var popped = _contextStack.removeLast();
- assert(popped == name);
+ if (isDone) throw new StateError("No more events.");
+ var event = _stateMachine();
+ return event;
+ } on StringScannerException catch (error) {
+ throw new YamlException(error.message, error.span);
}
}
- /// Adds [message] as extra information to any errors that occur between the
- /// current position and the position of the cursor after running [fn]. The
- /// cursor is reset after [fn] is run.
- annotateError(String message, fn()) {
- var start = _scanner.position;
- var end;
- transaction(() {
- fn();
- end = _scanner.position;
- return false;
- });
- _errorAnnotations[new _Range(start, end)] = message;
+ /// Dispatches parsing based on the current state.
+ Event _stateMachine() {
+ switch (_state) {
+ case _State.STREAM_START:
+ return _parseStreamStart();
+ case _State.DOCUMENT_START:
+ return _parseDocumentStart();
+ case _State.DOCUMENT_CONTENT:
+ return _parseDocumentContent();
+ case _State.DOCUMENT_END:
+ return _parseDocumentEnd();
+ case _State.BLOCK_NODE:
+ return _parseNode(block: true);
+ case _State.BLOCK_NODE_OR_INDENTLESS_SEQUENCE:
+ return _parseNode(block: true, indentlessSequence: true);
+ case _State.FLOW_NODE:
+ return _parseNode();
+ case _State.BLOCK_SEQUENCE_FIRST_ENTRY:
+ // Scan past the `BLOCK-SEQUENCE-FIRST-ENTRY` token to the
+ // `BLOCK-SEQUENCE-ENTRY` token.
+ _scanner.scan();
+ return _parseBlockSequenceEntry();
+ case _State.BLOCK_SEQUENCE_ENTRY:
+ return _parseBlockSequenceEntry();
+ case _State.INDENTLESS_SEQUENCE_ENTRY:
+ return _parseIndentlessSequenceEntry();
+ case _State.BLOCK_MAPPING_FIRST_KEY:
+ // Scan past the `BLOCK-MAPPING-FIRST-KEY` token to the
+ // `BLOCK-MAPPING-KEY` token.
+ _scanner.scan();
+ return _parseBlockMappingKey();
+ case _State.BLOCK_MAPPING_KEY:
+ return _parseBlockMappingKey();
+ case _State.BLOCK_MAPPING_VALUE:
+ return _parseBlockMappingValue();
+ case _State.FLOW_SEQUENCE_FIRST_ENTRY:
+ return _parseFlowSequenceEntry(first: true);
+ case _State.FLOW_SEQUENCE_ENTRY:
+ return _parseFlowSequenceEntry();
+ case _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY:
+ return _parseFlowSequenceEntryMappingKey();
+ case _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE:
+ return _parseFlowSequenceEntryMappingValue();
+ case _State.FLOW_SEQUENCE_ENTRY_MAPPING_END:
+ return _parseFlowSequenceEntryMappingEnd();
+ case _State.FLOW_MAPPING_FIRST_KEY:
+ return _parseFlowMappingKey(first: true);
+ case _State.FLOW_MAPPING_KEY:
+ return _parseFlowMappingKey();
+ case _State.FLOW_MAPPING_VALUE:
+ return _parseFlowMappingValue();
+ case _State.FLOW_MAPPING_EMPTY_VALUE:
+ return _parseFlowMappingValue(empty: true);
+ default:
+ throw "Unreachable";
+ }
}
- /// Throws an error with additional context information.
- void error(String message) =>
- _scanner.error("$message (in $_farthestContext).");
-
- /// If [result] is falsey, throws an error saying that [expected] was
- /// expected.
- expect(result, String expected) {
- if (truth(result)) return result;
- error("Expected $expected");
- }
-
- /// Throws an error saying that the parse failed.
+ /// Parses the production:
///
- /// Uses [_farthestState] and [_farthestContext] to provide additional
- /// information.
- parseFailed() {
- var message = "Invalid YAML in $_farthestContext";
- _refreshFarthestState();
- _scanner.state = _farthestState;
+ /// stream ::=
+ /// STREAM-START implicit_document? explicit_document* STREAM-END
+ /// ************
+ Event _parseStreamStart() {
+ var token = _scanner.scan();
+ assert(token.type == TokenType.STREAM_START);
- var extraError = _errorAnnotations[_scanner.position];
- if (extraError != null) message = "$message ($extraError)";
- _scanner.error("$message.");
+ _state = _State.DOCUMENT_START;
+ return new Event(EventType.STREAM_START, token.span);
}
- /// Update [_farthestState] if the scanner is farther than it's been before.
- void _refreshFarthestState() {
- if (_scanner.position <= _farthestState.position) return;
- _farthestState = _scanner.state;
- }
+ /// Parses the productions:
+ ///
+ /// implicit_document ::= block_node DOCUMENT-END*
+ /// *
+ /// explicit_document ::=
+ /// DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+ /// *************************
+ Event _parseDocumentStart() {
+ var token = _scanner.peek();
- /// Returns the number of spaces after the current position.
- int countIndentation() {
- var i = 0;
- while (peek(i) == SP) i++;
- return i;
- }
+ // libyaml requires any document beyond the first in the stream to have an
+ // explicit document start indicator, but the spec allows it to be omitted
+ // as long as there was an end indicator.
- /// Returns the indentation for a block scalar.
- int blockScalarAdditionalIndentation(_BlockHeader header, int indent) {
- if (!header.autoDetectIndent) return header.additionalIndent;
-
- var maxSpaces = 0;
- var spaces = 0;
- transaction(() {
- do {
- spaces = captureString(() => zeroOrMore(() => consumeChar(SP))).length;
- if (spaces > maxSpaces) maxSpaces = spaces;
- } while (b_break());
- return false;
- });
-
- // If the next non-empty line isn't indented further than the start of the
- // block scalar, that means the scalar is going to be empty. Returning any
- // value > 0 will cause the parser not to consume any text.
- if (spaces <= indent) return 1;
-
- // It's an error for a leading empty line to be indented more than the first
- // non-empty line.
- if (maxSpaces > spaces) {
- _scanner.error("Leading empty lines may not be indented more than the "
- "first non-empty line.");
+ // Parse extra document end indicators.
+ while (token.type == TokenType.DOCUMENT_END) {
+ token = _scanner.advance();
}
- return spaces - indent;
+ if (token.type != TokenType.VERSION_DIRECTIVE &&
+ token.type != TokenType.TAG_DIRECTIVE &&
+ token.type != TokenType.DOCUMENT_START &&
+ token.type != TokenType.STREAM_END) {
+ // Parse an implicit document.
+ _processDirectives();
+ _states.add(_State.DOCUMENT_END);
+ _state = _State.BLOCK_NODE;
+ return new DocumentStartEvent(token.span.start.pointSpan());
+ }
+
+ if (token.type == TokenType.STREAM_END) {
+ _state = _State.END;
+ _scanner.scan();
+ return new Event(EventType.STREAM_END, token.span);
+ }
+
+ // Parse an explicit document.
+ var start = token.span;
+ var pair = _processDirectives();
+ var versionDirective = pair.first;
+ var tagDirectives = pair.last;
+ token = _scanner.peek();
+ if (token.type != TokenType.DOCUMENT_START) {
+ throw new YamlException("Expected document start.", token.span);
+ }
+
+ _states.add(_State.DOCUMENT_END);
+ _state = _State.DOCUMENT_CONTENT;
+ _scanner.scan();
+ return new DocumentStartEvent(start.expand(token.span),
+ versionDirective: versionDirective,
+ tagDirectives: tagDirectives,
+ isImplicit: false);
}
- /// Returns whether the current position is at the beginning of a line.
- bool get atStartOfLine => _scanner.column == 0;
+ /// Parses the productions:
+ ///
+ /// explicit_document ::=
+ /// DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+ /// ***********
+ Event _parseDocumentContent() {
+ var token = _scanner.peek();
- /// Given an indicator character, returns the type of that indicator (or null
- /// if the indicator isn't found.
- int indicatorType(int char) {
- switch (char) {
- case HYPHEN: return C_SEQUENCE_ENTRY;
- case QUESTION_MARK: return C_MAPPING_KEY;
- case COLON: return C_MAPPING_VALUE;
- case COMMA: return C_COLLECT_ENTRY;
- case LEFT_BRACKET: return C_SEQUENCE_START;
- case RIGHT_BRACKET: return C_SEQUENCE_END;
- case LEFT_BRACE: return C_MAPPING_START;
- case RIGHT_BRACE: return C_MAPPING_END;
- case HASH: return C_COMMENT;
- case AMPERSAND: return C_ANCHOR;
- case ASTERISK: return C_ALIAS;
- case EXCLAMATION: return C_TAG;
- case VERTICAL_BAR: return C_LITERAL;
- case GREATER_THAN: return C_FOLDED;
- case SINGLE_QUOTE: return C_SINGLE_QUOTE;
- case DOUBLE_QUOTE: return C_DOUBLE_QUOTE;
- case PERCENT: return C_DIRECTIVE;
- case AT:
- case GRAVE_ACCENT:
- return C_RESERVED;
- default: return null;
+ switch (token.type) {
+ case TokenType.VERSION_DIRECTIVE:
+ case TokenType.TAG_DIRECTIVE:
+ case TokenType.DOCUMENT_START:
+ case TokenType.DOCUMENT_END:
+ case TokenType.STREAM_END:
+ _state = _states.removeLast();
+ return _processEmptyScalar(token.span.start);
+ default:
+ return _parseNode(block: true);
}
}
- // 1
- bool isPrintable(int char) {
- if (char == null) return false;
- return char == TAB ||
- char == LF ||
- char == CR ||
- (char >= SP && char <= TILDE) ||
- char == NEL ||
- (char >= 0xA0 && char <= 0xD7FF) ||
- (char >= 0xE000 && char <= 0xFFFD) ||
- (char >= 0x10000 && char <= 0x10FFFF);
- }
+ /// Parses the productions:
+ ///
+ /// implicit_document ::= block_node DOCUMENT-END*
+ /// *************
+ /// explicit_document ::=
+ /// DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END*
+ /// *************
+ Event _parseDocumentEnd() {
+ _tagDirectives.clear();
+ _state = _State.DOCUMENT_START;
- // 2
- bool isJson(int char) => char != null &&
- (char == TAB || (char >= SP && char <= 0x10FFFF));
-
- // 22
- bool c_indicator(int type) => consume((c) => indicatorType(c) == type);
-
- // 23
- bool isFlowIndicator(int char) {
- var indicator = indicatorType(char);
- return indicator == C_COLLECT_ENTRY ||
- indicator == C_SEQUENCE_START ||
- indicator == C_SEQUENCE_END ||
- indicator == C_MAPPING_START ||
- indicator == C_MAPPING_END;
- }
-
- // 26
- bool isBreak(int char) => char == LF || char == CR;
-
- // 27
- bool isNonBreak(int char) => isPrintable(char) && !isBreak(char);
-
- // 28
- bool b_break() {
- if (consumeChar(CR)) {
- zeroOrOne(() => consumeChar(LF));
- return true;
- }
- return consumeChar(LF);
- }
-
- // 29
- bool b_asLineFeed() => captureAs("\n", () => b_break());
-
- // 30
- bool b_nonContent() => captureAs("", () => b_break());
-
- // 33
- bool isSpace(int char) => char == SP || char == TAB;
-
- // 34
- bool isNonSpace(int char) => isNonBreak(char) && !isSpace(char);
-
- // 35
- bool isDecDigit(int char) => char != null && char >= NUMBER_0 &&
- char <= NUMBER_9;
-
- // 36
- bool isHexDigit(int char) {
- if (char == null) return false;
- return isDecDigit(char) ||
- (char >= LETTER_A && char <= LETTER_F) ||
- (char >= LETTER_CAP_A && char <= LETTER_CAP_F);
- }
-
- // 41
- bool c_escape() => captureAs("", () => consumeChar(BACKSLASH));
-
- // 42
- bool ns_escNull() => captureAs("\x00", () => consumeChar(NUMBER_0));
-
- // 43
- bool ns_escBell() => captureAs("\x07", () => consumeChar(LETTER_A));
-
- // 44
- bool ns_escBackspace() => captureAs("\b", () => consumeChar(LETTER_B));
-
- // 45
- bool ns_escHorizontalTab() => captureAs("\t", () {
- return consume((c) => c == LETTER_T || c == TAB);
- });
-
- // 46
- bool ns_escLineFeed() => captureAs("\n", () => consumeChar(LETTER_N));
-
- // 47
- bool ns_escVerticalTab() => captureAs("\v", () => consumeChar(LETTER_V));
-
- // 48
- bool ns_escFormFeed() => captureAs("\f", () => consumeChar(LETTER_F));
-
- // 49
- bool ns_escCarriageReturn() => captureAs("\r", () => consumeChar(LETTER_R));
-
- // 50
- bool ns_escEscape() => captureAs("\x1B", () => consumeChar(LETTER_E));
-
- // 51
- bool ns_escSpace() => consumeChar(SP);
-
- // 52
- bool ns_escDoubleQuote() => consumeChar(DOUBLE_QUOTE);
-
- // 53
- bool ns_escSlash() => consumeChar(SLASH);
-
- // 54
- bool ns_escBackslash() => consumeChar(BACKSLASH);
-
- // 55
- bool ns_escNextLine() => captureAs("\x85", () => consumeChar(LETTER_CAP_N));
-
- // 56
- bool ns_escNonBreakingSpace() =>
- captureAs("\xA0", () => consumeChar(UNDERSCORE));
-
- // 57
- bool ns_escLineSeparator() =>
- captureAs("\u2028", () => consumeChar(LETTER_CAP_L));
-
- // 58
- bool ns_escParagraphSeparator() =>
- captureAs("\u2029", () => consumeChar(LETTER_CAP_P));
-
- // 59
- bool ns_esc8Bit() => ns_escNBit(LETTER_X, 2);
-
- // 60
- bool ns_esc16Bit() => ns_escNBit(LETTER_U, 4);
-
- // 61
- bool ns_esc32Bit() => ns_escNBit(LETTER_CAP_U, 8);
-
- // Helper method for 59 - 61
- bool ns_escNBit(int char, int digits) {
- if (!captureAs('', () => consumeChar(char))) return false;
- var captured = captureAndTransform(
- () => nAtOnce(digits, (c, _) => isHexDigit(c)),
- (hex) => new String.fromCharCodes([int.parse("0x$hex")]));
- return expect(captured, "$digits hexidecimal digits");
- }
-
- // 62
- bool c_ns_escChar() => context('escape sequence', () => transaction(() {
- if (!truth(c_escape())) return false;
- return truth(or([
- ns_escNull, ns_escBell, ns_escBackspace, ns_escHorizontalTab,
- ns_escLineFeed, ns_escVerticalTab, ns_escFormFeed, ns_escCarriageReturn,
- ns_escEscape, ns_escSpace, ns_escDoubleQuote, ns_escSlash,
- ns_escBackslash, ns_escNextLine, ns_escNonBreakingSpace,
- ns_escLineSeparator, ns_escParagraphSeparator, ns_esc8Bit, ns_esc16Bit,
- ns_esc32Bit
- ]));
- }));
-
- // 63
- bool s_indent(int indent) {
- var result = nAtOnce(indent, (c, i) => c == SP);
- if (peek() == TAB) {
- annotateError("tab characters are not allowed as indentation in YAML",
- () => zeroOrMore(() => consume(isSpace)));
- }
- return result;
- }
-
- // 64
- bool s_indentLessThan(int indent) {
- for (int i = 0; i < indent - 1; i++) {
- if (!consumeChar(SP)) {
- if (peek() == TAB) {
- annotateError("tab characters are not allowed as indentation in YAML",
- () {
- for (; i < indent - 1; i++) {
- if (!consume(isSpace)) break;
- }
- });
- }
- break;
- }
- }
- return true;
- }
-
- // 65
- bool s_indentLessThanOrEqualTo(int indent) => s_indentLessThan(indent + 1);
-
- // 66
- bool s_separateInLine() => transaction(() {
- return captureAs('', () =>
- truth(oneOrMore(() => consume(isSpace))) || atStartOfLine);
- });
-
- // 67
- bool s_linePrefix(int indent, int ctx) => captureAs("", () {
- switch (ctx) {
- case BLOCK_OUT:
- case BLOCK_IN:
- return s_blockLinePrefix(indent);
- case FLOW_OUT:
- case FLOW_IN:
- return s_flowLinePrefix(indent);
- }
- });
-
- // 68
- bool s_blockLinePrefix(int indent) => s_indent(indent);
-
- // 69
- bool s_flowLinePrefix(int indent) => captureAs('', () {
- if (!truth(s_indent(indent))) return false;
- zeroOrOne(s_separateInLine);
- return true;
- });
-
- // 70
- bool l_empty(int indent, int ctx) => transaction(() {
- var start = or([
- () => s_linePrefix(indent, ctx),
- () => s_indentLessThan(indent)
- ]);
- if (!truth(start)) return false;
- return b_asLineFeed();
- });
-
- // 71
- bool b_asSpace() => captureAs(" ", () => consume(isBreak));
-
- // 72
- bool b_l_trimmed(int indent, int ctx) => transaction(() {
- if (!truth(b_nonContent())) return false;
- return truth(oneOrMore(() => captureAs("\n", () => l_empty(indent, ctx))));
- });
-
- // 73
- bool b_l_folded(int indent, int ctx) =>
- or([() => b_l_trimmed(indent, ctx), b_asSpace]);
-
- // 74
- bool s_flowFolded(int indent) => transaction(() {
- zeroOrOne(s_separateInLine);
- if (!truth(b_l_folded(indent, FLOW_IN))) return false;
- return s_flowLinePrefix(indent);
- });
-
- // 75
- bool c_nb_commentText() {
- if (!truth(c_indicator(C_COMMENT))) return false;
- zeroOrMore(() => consume(isNonBreak));
- return true;
- }
-
- // 76
- bool b_comment() => _scanner.isDone || b_nonContent();
-
- // 77
- bool s_b_comment() {
- if (truth(s_separateInLine())) {
- zeroOrOne(c_nb_commentText);
- }
- return b_comment();
- }
-
- // 78
- bool l_comment() => transaction(() {
- if (!truth(s_separateInLine())) return false;
- zeroOrOne(c_nb_commentText);
- return b_comment();
- });
-
- // 79
- bool s_l_comments() {
- if (!truth(s_b_comment()) && !atStartOfLine) return false;
- zeroOrMore(l_comment);
- return true;
- }
-
- // 80
- bool s_separate(int indent, int ctx) {
- switch (ctx) {
- case BLOCK_OUT:
- case BLOCK_IN:
- case FLOW_OUT:
- case FLOW_IN:
- return s_separateLines(indent);
- case BLOCK_KEY:
- case FLOW_KEY:
- return s_separateInLine();
- default: throw 'Invalid context "$ctx".';
+ var token = _scanner.peek();
+ if (token.type == TokenType.DOCUMENT_END) {
+ _scanner.scan();
+ return new DocumentEndEvent(token.span, isImplicit: false);
+ } else {
+ return new DocumentEndEvent(
+ token.span.start.pointSpan(), isImplicit: true);
}
}
- // 81
- bool s_separateLines(int indent) {
- return transaction(() => s_l_comments() && s_flowLinePrefix(indent)) ||
- s_separateInLine();
- }
+ /// Parses the productions:
+ ///
+ /// block_node_or_indentless_sequence ::=
+ /// ALIAS
+ /// *****
+ /// | properties (block_content | indentless_block_sequence)?
+ /// ********** *
+ /// | block_content | indentless_block_sequence
+ /// *
+ /// block_node ::= ALIAS
+ /// *****
+ /// | properties block_content?
+ /// ********** *
+ /// | block_content
+ /// *
+ /// flow_node ::= ALIAS
+ /// *****
+ /// | properties flow_content?
+ /// ********** *
+ /// | flow_content
+ /// *
+ /// properties ::= TAG ANCHOR? | ANCHOR TAG?
+ /// *************************
+ /// block_content ::= block_collection | flow_collection | SCALAR
+ /// ******
+ /// flow_content ::= flow_collection | SCALAR
+ /// ******
+ Event _parseNode({bool block: false, bool indentlessSequence: false}) {
+ var token = _scanner.peek();
- // 82
- bool l_directive() => false; // TODO(nweiz): implement
-
- // 96
- Pair<Tag, String> c_ns_properties(int indent, int ctx) {
- var tag, anchor;
- tag = c_ns_tagProperty();
- if (truth(tag)) {
- anchor = transaction(() {
- if (!truth(s_separate(indent, ctx))) return null;
- return c_ns_anchorProperty();
- });
- return new Pair<Tag, String>(tag, anchor);
+ if (token is AliasToken) {
+ _scanner.scan();
+ _state = _states.removeLast();
+ return new AliasEvent(token.span, token.name);
}
- anchor = c_ns_anchorProperty();
- if (truth(anchor)) {
- tag = transaction(() {
- if (!truth(s_separate(indent, ctx))) return null;
- return c_ns_tagProperty();
- });
- return new Pair<Tag, String>(tag, anchor);
+ var anchor;
+ var tagToken;
+ var span = token.span.start.pointSpan();
+ parseAnchor() {
+ anchor = token.name;
+ span = span.expand(token.span);
+ token = _scanner.advance();
}
- return null;
- }
-
- // 97
- Tag c_ns_tagProperty() => null; // TODO(nweiz): implement
-
- // 101
- String c_ns_anchorProperty() => null; // TODO(nweiz): implement
-
- // 102
- bool isAnchorChar(int char) => isNonSpace(char) && !isFlowIndicator(char);
-
- // 103
- String ns_anchorName() =>
- captureString(() => oneOrMore(() => consume(isAnchorChar)));
-
- // 104
- Node c_ns_aliasNode() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_ALIAS))) return null;
- var name = expect(ns_anchorName(), 'anchor name');
- return new AliasNode(name, _scanner.spanFrom(start));
- }
-
- // 105
- ScalarNode e_scalar() => new ScalarNode("?", _scanner.emptySpan, content: "");
-
- // 106
- ScalarNode e_node() => e_scalar();
-
- // 107
- bool nb_doubleChar() => or([
- c_ns_escChar,
- () => consume((c) => isJson(c) && c != BACKSLASH && c != DOUBLE_QUOTE)
- ]);
-
- // 108
- bool ns_doubleChar() => !isSpace(peek()) && truth(nb_doubleChar());
-
- // 109
- Node c_doubleQuoted(int indent, int ctx) => context('string', () {
- return transaction(() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_DOUBLE_QUOTE))) return null;
- var contents = nb_doubleText(indent, ctx);
- if (!truth(c_indicator(C_DOUBLE_QUOTE))) return null;
- return new ScalarNode("!", _scanner.spanFrom(start), content: contents);
- });
- });
-
- // 110
- String nb_doubleText(int indent, int ctx) => captureString(() {
- switch (ctx) {
- case FLOW_OUT:
- case FLOW_IN:
- nb_doubleMultiLine(indent);
- break;
- case BLOCK_KEY:
- case FLOW_KEY:
- nb_doubleOneLine();
- break;
- }
- return true;
- });
-
- // 111
- void nb_doubleOneLine() {
- zeroOrMore(nb_doubleChar);
- }
-
- // 112
- bool s_doubleEscaped(int indent) => transaction(() {
- zeroOrMore(() => consume(isSpace));
- if (!captureAs("", () => consumeChar(BACKSLASH))) return false;
- if (!truth(b_nonContent())) return false;
- zeroOrMore(() => captureAs("\n", () => l_empty(indent, FLOW_IN)));
- return s_flowLinePrefix(indent);
- });
-
- // 113
- bool s_doubleBreak(int indent) => or([
- () => s_doubleEscaped(indent),
- () => s_flowFolded(indent)
- ]);
-
- // 114
- void nb_ns_doubleInLine() {
- zeroOrMore(() => transaction(() {
- zeroOrMore(() => consume(isSpace));
- return ns_doubleChar();
- }));
- }
-
- // 115
- bool s_doubleNextLine(int indent) {
- if (!truth(s_doubleBreak(indent))) return false;
- zeroOrOne(() {
- if (!truth(ns_doubleChar())) return;
- nb_ns_doubleInLine();
- or([
- () => s_doubleNextLine(indent),
- () => zeroOrMore(() => consume(isSpace))
- ]);
- });
- return true;
- }
-
- // 116
- void nb_doubleMultiLine(int indent) {
- nb_ns_doubleInLine();
- or([
- () => s_doubleNextLine(indent),
- () => zeroOrMore(() => consume(isSpace))
- ]);
- }
-
- // 117
- bool c_quotedQuote() => captureAs("'", () => rawString("''"));
-
- // 118
- bool nb_singleChar() => or([
- c_quotedQuote,
- () => consume((c) => isJson(c) && c != SINGLE_QUOTE)
- ]);
-
- // 119
- bool ns_singleChar() => !isSpace(peek()) && truth(nb_singleChar());
-
- // 120
- Node c_singleQuoted(int indent, int ctx) => context('string', () {
- return transaction(() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_SINGLE_QUOTE))) return null;
- var contents = nb_singleText(indent, ctx);
- if (!truth(c_indicator(C_SINGLE_QUOTE))) return null;
- return new ScalarNode("!", _scanner.spanFrom(start), content: contents);
- });
- });
-
- // 121
- String nb_singleText(int indent, int ctx) => captureString(() {
- switch (ctx) {
- case FLOW_OUT:
- case FLOW_IN:
- nb_singleMultiLine(indent);
- break;
- case BLOCK_KEY:
- case FLOW_KEY:
- nb_singleOneLine(indent);
- break;
- }
- return true;
- });
-
- // 122
- void nb_singleOneLine(int indent) {
- zeroOrMore(nb_singleChar);
- }
-
- // 123
- void nb_ns_singleInLine() {
- zeroOrMore(() => transaction(() {
- zeroOrMore(() => consume(isSpace));
- return ns_singleChar();
- }));
- }
-
- // 124
- bool s_singleNextLine(int indent) {
- if (!truth(s_flowFolded(indent))) return false;
- zeroOrOne(() {
- if (!truth(ns_singleChar())) return;
- nb_ns_singleInLine();
- or([
- () => s_singleNextLine(indent),
- () => zeroOrMore(() => consume(isSpace))
- ]);
- });
- return true;
- }
-
- // 125
- void nb_singleMultiLine(int indent) {
- nb_ns_singleInLine();
- or([
- () => s_singleNextLine(indent),
- () => zeroOrMore(() => consume(isSpace))
- ]);
- }
-
- // 126
- bool ns_plainFirst(int ctx) {
- var char = peek();
- var indicator = indicatorType(char);
- if (indicator == C_RESERVED) {
- error("Reserved indicators can't start a plain scalar");
- }
- var match = (isNonSpace(char) && indicator == null) ||
- ((indicator == C_MAPPING_KEY ||
- indicator == C_MAPPING_VALUE ||
- indicator == C_SEQUENCE_ENTRY) &&
- isPlainSafe(ctx, peek(1)));
-
- if (match) next();
- return match;
- }
-
- // 127
- bool isPlainSafe(int ctx, int char) {
- switch (ctx) {
- case FLOW_OUT:
- case BLOCK_KEY:
- // 128
- return isNonSpace(char);
- case FLOW_IN:
- case FLOW_KEY:
- // 129
- return isNonSpace(char) && !isFlowIndicator(char);
- default: throw 'Invalid context "$ctx".';
- }
- }
-
- // 130
- bool ns_plainChar(int ctx) {
- var char = peek();
- var indicator = indicatorType(char);
- var safeChar = isPlainSafe(ctx, char) && indicator != C_MAPPING_VALUE &&
- indicator != C_COMMENT;
- var nonCommentHash = isNonSpace(peek(-1)) && indicator == C_COMMENT;
- var nonMappingColon = indicator == C_MAPPING_VALUE &&
- isPlainSafe(ctx, peek(1));
- var match = safeChar || nonCommentHash || nonMappingColon;
-
- if (match) next();
- return match;
- }
-
- // 131
- String ns_plain(int indent, int ctx) => context('plain scalar', () {
- return captureString(() {
- switch (ctx) {
- case FLOW_OUT:
- case FLOW_IN:
- return ns_plainMultiLine(indent, ctx);
- case BLOCK_KEY:
- case FLOW_KEY:
- return ns_plainOneLine(ctx);
- default: throw 'Invalid context "$ctx".';
- }
- });
- });
-
- // 132
- void nb_ns_plainInLine(int ctx) {
- zeroOrMore(() => transaction(() {
- zeroOrMore(() => consume(isSpace));
- return ns_plainChar(ctx);
- }));
- }
-
- // 133
- bool ns_plainOneLine(int ctx) {
- if (truth(c_forbidden())) return false;
- if (!truth(ns_plainFirst(ctx))) return false;
- nb_ns_plainInLine(ctx);
- return true;
- }
-
- // 134
- bool s_ns_plainNextLine(int indent, int ctx) => transaction(() {
- if (!truth(s_flowFolded(indent))) return false;
- if (truth(c_forbidden())) return false;
- if (!truth(ns_plainChar(ctx))) return false;
- nb_ns_plainInLine(ctx);
- return true;
- });
-
- // 135
- bool ns_plainMultiLine(int indent, int ctx) {
- if (!truth(ns_plainOneLine(ctx))) return false;
- zeroOrMore(() => s_ns_plainNextLine(indent, ctx));
- return true;
- }
-
- // 136
- int inFlow(int ctx) {
- switch (ctx) {
- case FLOW_OUT:
- case FLOW_IN:
- return FLOW_IN;
- case BLOCK_KEY:
- case FLOW_KEY:
- return FLOW_KEY;
- }
- throw "unreachable";
- }
-
- // 137
- SequenceNode c_flowSequence(int indent, int ctx) => transaction(() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_SEQUENCE_START))) return null;
- zeroOrOne(() => s_separate(indent, ctx));
- var content = zeroOrOne(() => ns_s_flowSeqEntries(indent, inFlow(ctx)));
- if (!truth(c_indicator(C_SEQUENCE_END))) return null;
- return new SequenceNode("?", new List<Node>.from(content),
- _scanner.spanFrom(start));
- });
-
- // 138
- Iterable<Node> ns_s_flowSeqEntries(int indent, int ctx) {
- var first = ns_flowSeqEntry(indent, ctx);
- if (!truth(first)) return new Queue<Node>();
- zeroOrOne(() => s_separate(indent, ctx));
-
- var rest;
- if (truth(c_indicator(C_COLLECT_ENTRY))) {
- zeroOrOne(() => s_separate(indent, ctx));
- rest = zeroOrOne(() => ns_s_flowSeqEntries(indent, ctx));
+ parseTag() {
+ tagToken = token;
+ span = span.expand(token.span);
+ token = _scanner.advance();
}
- if (rest == null) rest = new Queue<Node>();
- rest.addFirst(first);
-
- return rest;
- }
-
- // 139
- Node ns_flowSeqEntry(int indent, int ctx) => or([
- () => ns_flowPair(indent, ctx),
- () => ns_flowNode(indent, ctx)
- ]);
-
- // 140
- Node c_flowMapping(int indent, int ctx) {
- var start = _scanner.state;
- if (!truth(c_indicator(C_MAPPING_START))) return null;
- zeroOrOne(() => s_separate(indent, ctx));
- var content = zeroOrOne(() => ns_s_flowMapEntries(indent, inFlow(ctx)));
- if (!truth(c_indicator(C_MAPPING_END))) return null;
- return new MappingNode("?", content, _scanner.spanFrom(start));
- }
-
- // 141
- Map ns_s_flowMapEntries(int indent, int ctx) {
- var first = ns_flowMapEntry(indent, ctx);
- if (!truth(first)) return deepEqualsMap();
- zeroOrOne(() => s_separate(indent, ctx));
-
- var rest;
- if (truth(c_indicator(C_COLLECT_ENTRY))) {
- zeroOrOne(() => s_separate(indent, ctx));
- rest = ns_s_flowMapEntries(indent, ctx);
+ if (token is AnchorToken) {
+ parseAnchor();
+ if (token is TagToken) parseTag();
+ } else if (token is TagToken) {
+ parseTag();
+ if (token is AnchorToken) parseAnchor();
}
- if (rest == null) rest = deepEqualsMap();
-
- // TODO(nweiz): Duplicate keys should be an error. This includes keys with
- // different representations but the same value (e.g. 10 vs 0xa). To make
- // this user-friendly we'll probably also want to associate nodes with a
- // source range.
- if (!rest.containsKey(first.first)) rest[first.first] = first.last;
-
- return rest;
- }
-
- // 142
- Pair<Node, Node> ns_flowMapEntry(int indent, int ctx) => or([
- () => transaction(() {
- if (!truth(c_indicator(C_MAPPING_KEY))) return false;
- if (!truth(s_separate(indent, ctx))) return false;
- return ns_flowMapExplicitEntry(indent, ctx);
- }),
- () => ns_flowMapImplicitEntry(indent, ctx)
- ]);
-
- // 143
- Pair<Node, Node> ns_flowMapExplicitEntry(int indent, int ctx) => or([
- () => ns_flowMapImplicitEntry(indent, ctx),
- () => new Pair<Node, Node>(e_node(), e_node())
- ]);
-
- // 144
- Pair<Node, Node> ns_flowMapImplicitEntry(int indent, int ctx) => or([
- () => ns_flowMapYamlKeyEntry(indent, ctx),
- () => c_ns_flowMapEmptyKeyEntry(indent, ctx),
- () => c_ns_flowMapJsonKeyEntry(indent, ctx)
- ]);
-
- // 145
- Pair<Node, Node> ns_flowMapYamlKeyEntry(int indent, int ctx) {
- var key = ns_flowYamlNode(indent, ctx);
- if (!truth(key)) return null;
- var value = or([
- () => transaction(() {
- zeroOrOne(() => s_separate(indent, ctx));
- return c_ns_flowMapSeparateValue(indent, ctx);
- }),
- e_node
- ]);
- return new Pair<Node, Node>(key, value);
- }
-
- // 146
- Pair<Node, Node> c_ns_flowMapEmptyKeyEntry(int indent, int ctx) {
- var value = c_ns_flowMapSeparateValue(indent, ctx);
- if (!truth(value)) return null;
- return new Pair<Node, Node>(e_node(), value);
- }
-
- // 147
- Node c_ns_flowMapSeparateValue(int indent, int ctx) => transaction(() {
- if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
- if (isPlainSafe(ctx, peek())) return null;
-
- return or([
- () => transaction(() {
- if (!s_separate(indent, ctx)) return null;
- return ns_flowNode(indent, ctx);
- }),
- e_node
- ]);
- });
-
- // 148
- Pair<Node, Node> c_ns_flowMapJsonKeyEntry(int indent, int ctx) {
- var key = c_flowJsonNode(indent, ctx);
- if (!truth(key)) return null;
- var value = or([
- () => transaction(() {
- zeroOrOne(() => s_separate(indent, ctx));
- return c_ns_flowMapAdjacentValue(indent, ctx);
- }),
- e_node
- ]);
- return new Pair<Node, Node>(key, value);
- }
-
- // 149
- Node c_ns_flowMapAdjacentValue(int indent, int ctx) {
- if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
- return or([
- () => transaction(() {
- zeroOrOne(() => s_separate(indent, ctx));
- return ns_flowNode(indent, ctx);
- }),
- e_node
- ]);
- }
-
- // 150
- Node ns_flowPair(int indent, int ctx) {
- var start = _scanner.state;
- var pair = or([
- () => transaction(() {
- if (!truth(c_indicator(C_MAPPING_KEY))) return null;
- if (!truth(s_separate(indent, ctx))) return null;
- return ns_flowMapExplicitEntry(indent, ctx);
- }),
- () => ns_flowPairEntry(indent, ctx)
- ]);
- if (!truth(pair)) return null;
-
- return map([pair], _scanner.spanFrom(start));
- }
-
- // 151
- Pair<Node, Node> ns_flowPairEntry(int indent, int ctx) => or([
- () => ns_flowPairYamlKeyEntry(indent, ctx),
- () => c_ns_flowMapEmptyKeyEntry(indent, ctx),
- () => c_ns_flowPairJsonKeyEntry(indent, ctx)
- ]);
-
- // 152
- Pair<Node, Node> ns_flowPairYamlKeyEntry(int indent, int ctx) =>
- transaction(() {
- var key = ns_s_implicitYamlKey(FLOW_KEY);
- if (!truth(key)) return null;
- var value = c_ns_flowMapSeparateValue(indent, ctx);
- if (!truth(value)) return null;
- return new Pair<Node, Node>(key, value);
- });
-
- // 153
- Pair<Node, Node> c_ns_flowPairJsonKeyEntry(int indent, int ctx) =>
- transaction(() {
- var key = c_s_implicitJsonKey(FLOW_KEY);
- if (!truth(key)) return null;
- var value = c_ns_flowMapAdjacentValue(indent, ctx);
- if (!truth(value)) return null;
- return new Pair<Node, Node>(key, value);
- });
-
- // 154
- Node ns_s_implicitYamlKey(int ctx) => transaction(() {
- // TODO(nweiz): this is supposed to be limited to 1024 characters.
-
- // The indentation parameter is "null" since it's unused in this path
- var node = ns_flowYamlNode(null, ctx);
- if (!truth(node)) return null;
- zeroOrOne(s_separateInLine);
- return node;
- });
-
- // 155
- Node c_s_implicitJsonKey(int ctx) => transaction(() {
- // TODO(nweiz): this is supposed to be limited to 1024 characters.
-
- // The indentation parameter is "null" since it's unused in this path
- var node = c_flowJsonNode(null, ctx);
- if (!truth(node)) return null;
- zeroOrOne(s_separateInLine);
- return node;
- });
-
- // 156
- Node ns_flowYamlContent(int indent, int ctx) {
- var start = _scanner.state;
- var str = ns_plain(indent, ctx);
- if (!truth(str)) return null;
- return new ScalarNode("?", _scanner.spanFrom(start), content: str);
- }
-
- // 157
- Node c_flowJsonContent(int indent, int ctx) => or([
- () => c_flowSequence(indent, ctx),
- () => c_flowMapping(indent, ctx),
- () => c_singleQuoted(indent, ctx),
- () => c_doubleQuoted(indent, ctx)
- ]);
-
- // 158
- Node ns_flowContent(int indent, int ctx) => or([
- () => ns_flowYamlContent(indent, ctx),
- () => c_flowJsonContent(indent, ctx)
- ]);
-
- // 159
- Node ns_flowYamlNode(int indent, int ctx) => or([
- c_ns_aliasNode,
- () => ns_flowYamlContent(indent, ctx),
- () {
- var props = c_ns_properties(indent, ctx);
- if (!truth(props)) return null;
- var node = or([
- () => transaction(() {
- if (!truth(s_separate(indent, ctx))) return null;
- return ns_flowYamlContent(indent, ctx);
- }),
- e_scalar
- ]);
- return addProps(node, props);
- }
- ]);
-
- // 160
- Node c_flowJsonNode(int indent, int ctx) => transaction(() {
- var props;
- zeroOrOne(() => transaction(() {
- props = c_ns_properties(indent, ctx);
- if (!truth(props)) return null;
- return s_separate(indent, ctx);
- }));
-
- return addProps(c_flowJsonContent(indent, ctx), props);
- });
-
- // 161
- Node ns_flowNode(int indent, int ctx) => or([
- c_ns_aliasNode,
- () => ns_flowContent(indent, ctx),
- () => transaction(() {
- var props = c_ns_properties(indent, ctx);
- if (!truth(props)) return null;
- var node = or([
- () => transaction(() => s_separate(indent, ctx) ?
- ns_flowContent(indent, ctx) : null),
- e_scalar]);
- return addProps(node, props);
- })
- ]);
-
- // 162
- _BlockHeader c_b_blockHeader() => transaction(() {
- var indentation = c_indentationIndicator();
- var chomping = c_chompingIndicator();
- if (!truth(indentation)) indentation = c_indentationIndicator();
- if (!truth(s_b_comment())) return null;
-
- return new _BlockHeader(indentation, chomping);
- });
-
- // 163
- int c_indentationIndicator() {
- if (!isDecDigit(peek())) return null;
- return next() - NUMBER_0;
- }
-
- // 164
- int c_chompingIndicator() {
- switch (peek()) {
- case HYPHEN:
- next();
- return CHOMPING_STRIP;
- case PLUS:
- next();
- return CHOMPING_KEEP;
- default:
- return CHOMPING_CLIP;
- }
- }
-
- // 165
- bool b_chompedLast(int chomping) {
- if (_scanner.isDone) return true;
- switch (chomping) {
- case CHOMPING_STRIP:
- return b_nonContent();
- case CHOMPING_CLIP:
- case CHOMPING_KEEP:
- return b_asLineFeed();
- }
- throw "unreachable";
- }
-
- // 166
- void l_chompedEmpty(int indent, int chomping) {
- switch (chomping) {
- case CHOMPING_STRIP:
- case CHOMPING_CLIP:
- l_stripEmpty(indent);
- break;
- case CHOMPING_KEEP:
- l_keepEmpty(indent);
- break;
- }
- }
-
- // 167
- void l_stripEmpty(int indent) {
- captureAs('', () {
- zeroOrMore(() => transaction(() {
- if (!truth(s_indentLessThanOrEqualTo(indent))) return false;
- return b_nonContent();
- }));
- zeroOrOne(() => l_trailComments(indent));
- return true;
- });
- }
-
- // 168
- void l_keepEmpty(int indent) {
- zeroOrMore(() => captureAs('\n', () => l_empty(indent, BLOCK_IN)));
- zeroOrOne(() => captureAs('', () => l_trailComments(indent)));
- }
-
- // 169
- bool l_trailComments(int indent) => transaction(() {
- if (!truth(s_indentLessThanOrEqualTo(indent))) return false;
- if (!truth(c_nb_commentText())) return false;
- if (!truth(b_comment())) return false;
- zeroOrMore(l_comment);
- return true;
- });
-
- // 170
- Node c_l_literal(int indent) => transaction(() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_LITERAL))) return null;
- var header = c_b_blockHeader();
- if (!truth(header)) return null;
-
- var additionalIndent = blockScalarAdditionalIndentation(header, indent);
- var content = l_literalContent(indent + additionalIndent, header.chomping);
- if (!truth(content)) return null;
-
- return new ScalarNode("!", _scanner.spanFrom(start), content: content);
- });
-
- // 171
- bool l_nb_literalText(int indent) => transaction(() {
- zeroOrMore(() => captureAs("\n", () => l_empty(indent, BLOCK_IN)));
- if (!truth(captureAs("", () => s_indent(indent)))) return false;
- return truth(oneOrMore(() => consume(isNonBreak)));
- });
-
- // 172
- bool b_nb_literalNext(int indent) => transaction(() {
- if (!truth(b_asLineFeed())) return false;
- return l_nb_literalText(indent);
- });
-
- // 173
- String l_literalContent(int indent, int chomping) => captureString(() {
- transaction(() {
- if (!truth(l_nb_literalText(indent))) return false;
- zeroOrMore(() => b_nb_literalNext(indent));
- return b_chompedLast(chomping);
- });
- l_chompedEmpty(indent, chomping);
- return true;
- });
-
- // 174
- Node c_l_folded(int indent) => transaction(() {
- var start = _scanner.state;
- if (!truth(c_indicator(C_FOLDED))) return null;
- var header = c_b_blockHeader();
- if (!truth(header)) return null;
-
- var additionalIndent = blockScalarAdditionalIndentation(header, indent);
- var content = l_foldedContent(indent + additionalIndent, header.chomping);
- if (!truth(content)) return null;
-
- return new ScalarNode("!", _scanner.spanFrom(start), content: content);
- });
-
- // 175
- bool s_nb_foldedText(int indent) => transaction(() {
- if (!truth(captureAs('', () => s_indent(indent)))) return false;
- if (!truth(consume(isNonSpace))) return false;
- zeroOrMore(() => consume(isNonBreak));
- return true;
- });
-
- // 176
- bool l_nb_foldedLines(int indent) {
- if (!truth(s_nb_foldedText(indent))) return false;
- zeroOrMore(() => transaction(() {
- if (!truth(b_l_folded(indent, BLOCK_IN))) return false;
- return s_nb_foldedText(indent);
- }));
- return true;
- }
-
- // 177
- bool s_nb_spacedText(int indent) => transaction(() {
- if (!truth(captureAs('', () => s_indent(indent)))) return false;
- if (!truth(consume(isSpace))) return false;
- zeroOrMore(() => consume(isNonBreak));
- return true;
- });
-
- // 178
- bool b_l_spaced(int indent) {
- if (!truth(b_asLineFeed())) return false;
- zeroOrMore(() => captureAs("\n", () => l_empty(indent, BLOCK_IN)));
- return true;
- }
-
- // 179
- bool l_nb_spacedLines(int indent) {
- if (!truth(s_nb_spacedText(indent))) return false;
- zeroOrMore(() => transaction(() {
- if (!truth(b_l_spaced(indent))) return false;
- return s_nb_spacedText(indent);
- }));
- return true;
- }
-
- // 180
- bool l_nb_sameLines(int indent) => transaction(() {
- zeroOrMore(() => captureAs('\n', () => l_empty(indent, BLOCK_IN)));
- return or([
- () => l_nb_foldedLines(indent),
- () => l_nb_spacedLines(indent)
- ]);
- });
-
- // 181
- bool l_nb_diffLines(int indent) {
- if (!truth(l_nb_sameLines(indent))) return false;
- zeroOrMore(() => transaction(() {
- if (!truth(b_asLineFeed())) return false;
- return l_nb_sameLines(indent);
- }));
- return true;
- }
-
- // 182
- String l_foldedContent(int indent, int chomping) => captureString(() {
- transaction(() {
- if (!truth(l_nb_diffLines(indent))) return false;
- return b_chompedLast(chomping);
- });
- l_chompedEmpty(indent, chomping);
- return true;
- });
-
- // 183
- SequenceNode l_blockSequence(int indent) => context('sequence', () {
- var additionalIndent = countIndentation() - indent;
- if (additionalIndent <= 0) return null;
-
- var start = _scanner.state;
- var content = oneOrMore(() => transaction(() {
- if (!truth(s_indent(indent + additionalIndent))) return null;
- return c_l_blockSeqEntry(indent + additionalIndent);
- }));
- if (!truth(content)) return null;
-
- return new SequenceNode("?", content, _scanner.spanFrom(start));
- });
-
- // 184
- Node c_l_blockSeqEntry(int indent) => transaction(() {
- if (!truth(c_indicator(C_SEQUENCE_ENTRY))) return null;
- if (isNonSpace(peek())) return null;
-
- return s_l_blockIndented(indent, BLOCK_IN);
- });
-
- // 185
- Node s_l_blockIndented(int indent, int ctx) {
- var additionalIndent = countIndentation();
- return or([
- () => transaction(() {
- if (!truth(s_indent(additionalIndent))) return null;
- return or([
- () => ns_l_compactSequence(indent + 1 + additionalIndent),
- () => ns_l_compactMapping(indent + 1 + additionalIndent)]);
- }),
- () => s_l_blockNode(indent, ctx),
- () => s_l_comments() ? e_node() : null]);
- }
-
- // 186
- Node ns_l_compactSequence(int indent) => context('sequence', () {
- var start = _scanner.state;
- var first = c_l_blockSeqEntry(indent);
- if (!truth(first)) return null;
-
- var content = zeroOrMore(() => transaction(() {
- if (!truth(s_indent(indent))) return null;
- return c_l_blockSeqEntry(indent);
- }));
- content.insert(0, first);
-
- return new SequenceNode("?", content, _scanner.spanFrom(start));
- });
-
- // 187
- Node l_blockMapping(int indent) => context('mapping', () {
- var additionalIndent = countIndentation() - indent;
- if (additionalIndent <= 0) return null;
-
- var start = _scanner.state;
- var pairs = oneOrMore(() => transaction(() {
- if (!truth(s_indent(indent + additionalIndent))) return null;
- return ns_l_blockMapEntry(indent + additionalIndent);
- }));
- if (!truth(pairs)) return null;
-
- return map(pairs, _scanner.spanFrom(start));
- });
-
- // 188
- Pair<Node, Node> ns_l_blockMapEntry(int indent) => or([
- () => c_l_blockMapExplicitEntry(indent),
- () => ns_l_blockMapImplicitEntry(indent)
- ]);
-
- // 189
- Pair<Node, Node> c_l_blockMapExplicitEntry(int indent) {
- var key = c_l_blockMapExplicitKey(indent);
- if (!truth(key)) return null;
-
- var value = or([
- () => l_blockMapExplicitValue(indent),
- e_node
- ]);
-
- return new Pair<Node, Node>(key, value);
- }
-
- // 190
- Node c_l_blockMapExplicitKey(int indent) => transaction(() {
- if (!truth(c_indicator(C_MAPPING_KEY))) return null;
- return s_l_blockIndented(indent, BLOCK_OUT);
- });
-
- // 191
- Node l_blockMapExplicitValue(int indent) => transaction(() {
- if (!truth(s_indent(indent))) return null;
- if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
- return s_l_blockIndented(indent, BLOCK_OUT);
- });
-
- // 192
- Pair<Node, Node> ns_l_blockMapImplicitEntry(int indent) => transaction(() {
- var key = or([ns_s_blockMapImplicitKey, e_node]);
- var value = c_l_blockMapImplicitValue(indent);
- return truth(value) ? new Pair<Node, Node>(key, value) : null;
- });
-
- // 193
- Node ns_s_blockMapImplicitKey() => context('mapping key', () => or([
- () => c_s_implicitJsonKey(BLOCK_KEY),
- () => ns_s_implicitYamlKey(BLOCK_KEY)
- ]));
-
- // 194
- Node c_l_blockMapImplicitValue(int indent) => context('mapping value', () =>
- transaction(() {
- if (!truth(c_indicator(C_MAPPING_VALUE))) return null;
- return or([
- () => s_l_blockNode(indent, BLOCK_OUT),
- () => s_l_comments() ? e_node() : null
- ]);
- }));
-
- // 195
- Node ns_l_compactMapping(int indent) => context('mapping', () {
- var start = _scanner.state;
- var first = ns_l_blockMapEntry(indent);
- if (!truth(first)) return null;
-
- var pairs = zeroOrMore(() => transaction(() {
- if (!truth(s_indent(indent))) return null;
- return ns_l_blockMapEntry(indent);
- }));
- pairs.insert(0, first);
-
- return map(pairs, _scanner.spanFrom(start));
- });
-
- // 196
- Node s_l_blockNode(int indent, int ctx) => or([
- () => s_l_blockInBlock(indent, ctx),
- () => s_l_flowInBlock(indent)
- ]);
-
- // 197
- Node s_l_flowInBlock(int indent) => transaction(() {
- if (!truth(s_separate(indent + 1, FLOW_OUT))) return null;
- var node = ns_flowNode(indent + 1, FLOW_OUT);
- if (!truth(node)) return null;
- if (!truth(s_l_comments())) return null;
- return node;
- });
-
- // 198
- Node s_l_blockInBlock(int indent, int ctx) => or([
- () => s_l_blockScalar(indent, ctx),
- () => s_l_blockCollection(indent, ctx)
- ]);
-
- // 199
- Node s_l_blockScalar(int indent, int ctx) => transaction(() {
- if (!truth(s_separate(indent + 1, ctx))) return null;
- var props = transaction(() {
- var innerProps = c_ns_properties(indent + 1, ctx);
- if (!truth(innerProps)) return null;
- if (!truth(s_separate(indent + 1, ctx))) return null;
- return innerProps;
- });
-
- var node = or([() => c_l_literal(indent), () => c_l_folded(indent)]);
- if (!truth(node)) return null;
- return addProps(node, props);
- });
-
- // 200
- Node s_l_blockCollection(int indent, int ctx) => transaction(() {
- var props = transaction(() {
- if (!truth(s_separate(indent + 1, ctx))) return null;
- return c_ns_properties(indent + 1, ctx);
- });
-
- if (!truth(s_l_comments())) return null;
- return or([
- () => l_blockSequence(seqSpaces(indent, ctx)),
- () => l_blockMapping(indent)]);
- });
-
- // 201
- int seqSpaces(int indent, int ctx) => ctx == BLOCK_OUT ? indent - 1 : indent;
-
- // 202
- void l_documentPrefix() {
- zeroOrMore(l_comment);
- }
-
- // 203
- bool c_directivesEnd() => rawString("---");
-
- // 204
- bool c_documentEnd() => rawString("...");
-
- // 205
- bool l_documentSuffix() => transaction(() {
- if (!truth(c_documentEnd())) return false;
- return s_l_comments();
- });
-
- // 206
- bool c_forbidden() {
- if (!_inBareDocument || !atStartOfLine) return false;
- var forbidden = false;
- transaction(() {
- if (!truth(or([c_directivesEnd, c_documentEnd]))) return;
- var char = peek();
- forbidden = isBreak(char) || isSpace(char) || _scanner.isDone;
- return;
- });
- return forbidden;
- }
-
- // 207
- Node l_bareDocument() {
- try {
- _inBareDocument = true;
- return s_l_blockNode(-1, BLOCK_IN);
- } finally {
- _inBareDocument = false;
- }
- }
-
- // 208
- Node l_explicitDocument() {
- if (!truth(c_directivesEnd())) return null;
- var doc = l_bareDocument();
- if (truth(doc)) return doc;
-
- doc = e_node();
- s_l_comments();
- return doc;
- }
-
- // 209
- Node l_directiveDocument() {
- if (!truth(oneOrMore(l_directive))) return null;
- var doc = l_explicitDocument();
- if (doc != null) return doc;
- parseFailed();
- return null; // Unreachable.
- }
-
- // 210
- Node l_anyDocument() =>
- or([l_directiveDocument, l_explicitDocument, l_bareDocument]);
-
- // 211
- Pair<List<Node>, SourceSpan> l_yamlStream() {
- var start = _scanner.state;
- var docs = [];
- zeroOrMore(l_documentPrefix);
- var first = zeroOrOne(l_anyDocument);
- if (!truth(first)) first = e_node();
- docs.add(first);
-
- zeroOrMore(() {
- var doc;
- if (truth(oneOrMore(l_documentSuffix))) {
- zeroOrMore(l_documentPrefix);
- doc = zeroOrOne(l_anyDocument);
+ var tag;
+ if (tagToken != null) {
+ if (tagToken.handle == null) {
+ tag = tagToken.suffix;
} else {
- zeroOrMore(l_documentPrefix);
- doc = zeroOrOne(l_explicitDocument);
+ var tagDirective = _tagDirectives[tagToken.handle];
+ if (tagDirective == null) {
+ throw new YamlException("Undefined tag handle.", tagToken.span);
+ }
+
+ tag = tagDirective.prefix + tagToken.suffix;
}
- if (truth(doc)) docs.add(doc);
- return doc;
- });
-
- if (!_scanner.isDone) parseFailed();
- return new Pair(docs, _scanner.spanFrom(start));
- }
-}
-
-/// The information in the header for a block scalar.
-class _BlockHeader {
- final int additionalIndent;
- final int chomping;
-
- _BlockHeader(this.additionalIndent, this.chomping);
-
- bool get autoDetectIndent => additionalIndent == null;
-}
-
-/// A range of characters in the YAML document, from [start] to [end]
-/// (inclusive).
-class _Range {
- /// The first character in the range.
- final int start;
-
- /// The last character in the range.
- final int end;
-
- _Range(this.start, this.end);
-
- /// Returns whether or not [pos] lies within this range.
- bool contains(int pos) => pos >= start && pos <= end;
-}
-
-/// A map that associates [E] values with [_Range]s. It's efficient to create
-/// new associations, but finding the value associated with a position is more
-/// expensive.
-class _RangeMap<E> {
- /// The ranges and their associated elements.
- final List<Pair<_Range, E>> _contents = <Pair<_Range, E>>[];
-
- _RangeMap();
-
- /// Returns the value associated with the range in which [pos] lies, or null
- /// if there is no such range. If there's more than one such range, the most
- /// recently set one is used.
- E operator[](int pos) {
- // Iterate backwards through contents so the more recent range takes
- // precedence.
- for (var pair in _contents.reversed) {
- if (pair.first.contains(pos)) return pair.last;
}
- return null;
+
+ if (indentlessSequence && token.type == TokenType.BLOCK_ENTRY) {
+ _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+ return new SequenceStartEvent(
+ span.expand(token.span), CollectionStyle.BLOCK,
+ anchor: anchor, tag: tag);
+ }
+
+ if (token is ScalarToken) {
+ // All non-plain scalars have the "!" tag by default.
+ if (tag == null && token.style != ScalarStyle.PLAIN) tag = "!";
+
+ _state = _states.removeLast();
+ _scanner.scan();
+ return new ScalarEvent(
+ span.expand(token.span), token.value, token.style,
+ anchor: anchor, tag: tag);
+ }
+
+ if (token.type == TokenType.FLOW_SEQUENCE_START) {
+ _state = _State.FLOW_SEQUENCE_FIRST_ENTRY;
+ return new SequenceStartEvent(
+ span.expand(token.span), CollectionStyle.FLOW,
+ anchor: anchor, tag: tag);
+ }
+
+ if (token.type == TokenType.FLOW_MAPPING_START) {
+ _state = _State.FLOW_MAPPING_FIRST_KEY;
+ return new MappingStartEvent(
+ span.expand(token.span), CollectionStyle.FLOW,
+ anchor: anchor, tag: tag);
+ }
+
+ if (block && token.type == TokenType.BLOCK_SEQUENCE_START) {
+ _state = _State.BLOCK_SEQUENCE_FIRST_ENTRY;
+ return new SequenceStartEvent(
+ span.expand(token.span), CollectionStyle.BLOCK,
+ anchor: anchor, tag: tag);
+ }
+
+
+ if (block && token.type == TokenType.BLOCK_MAPPING_START) {
+ _state = _State.BLOCK_MAPPING_FIRST_KEY;
+ return new MappingStartEvent(
+ span.expand(token.span), CollectionStyle.BLOCK,
+ anchor: anchor, tag: tag);
+ }
+
+ if (anchor != null || tag != null) {
+ _state = _states.removeLast();
+ return new ScalarEvent(
+ span, '', ScalarStyle.PLAIN,
+ anchor: anchor, tag: tag);
+ }
+
+ throw new YamlException("Expected node content.", span);
}
- /// Associates [value] with [range].
- operator[]=(_Range range, E value) =>
- _contents.add(new Pair<_Range, E>(range, value));
+ /// Parses the productions:
+ ///
+ /// block_sequence ::=
+ /// BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END
+ /// ******************** *********** * *********
+ Event _parseBlockSequenceEntry() {
+ var token = _scanner.peek();
+
+ if (token.type == TokenType.BLOCK_ENTRY) {
+ token = _scanner.advance();
+
+ if (token.type == TokenType.BLOCK_ENTRY ||
+ token.type == TokenType.BLOCK_END) {
+ _state = _State.BLOCK_SEQUENCE_ENTRY;
+ return _processEmptyScalar(token.span.end);
+ } else {
+ _states.add(_State.BLOCK_SEQUENCE_ENTRY);
+ return _parseNode(block: true);
+ }
+ }
+
+ if (token.type == TokenType.BLOCK_END) {
+ _scanner.scan();
+ _state = _states.removeLast();
+ return new Event(EventType.SEQUENCE_END, token.span);
+ }
+
+ throw new YamlException("While parsing a block collection, expected '-'.",
+ token.span.start.pointSpan());
+ }
+
+ /// Parses the productions:
+ ///
+ /// indentless_sequence ::= (BLOCK-ENTRY block_node?)+
+ /// *********** *
+ Event _parseIndentlessSequenceEntry() {
+ var token = _scanner.peek();
+
+ if (token.type != TokenType.BLOCK_ENTRY) {
+ _state = _states.removeLast();
+ return new Event(EventType.SEQUENCE_END, token.span.start.pointSpan());
+ }
+
+ var start = token.span.start;
+ token = _scanner.advance();
+
+ if (token.type == TokenType.BLOCK_ENTRY ||
+ token.type == TokenType.KEY ||
+ token.type == TokenType.VALUE ||
+ token.type == TokenType.BLOCK_END) {
+ _state = _State.INDENTLESS_SEQUENCE_ENTRY;
+ return _processEmptyScalar(start);
+ } else {
+ _states.add(_State.INDENTLESS_SEQUENCE_ENTRY);
+ return _parseNode(block: true);
+ }
+ }
+
+ /// Parses the productions:
+ ///
+ /// block_mapping ::= BLOCK-MAPPING_START
+ /// *******************
+ /// ((KEY block_node_or_indentless_sequence?)?
+ /// *** *
+ /// (VALUE block_node_or_indentless_sequence?)?)*
+ ///
+ /// BLOCK-END
+ /// *********
+ Event _parseBlockMappingKey() {
+ var token = _scanner.peek();
+ if (token.type == TokenType.KEY) {
+ var start = token.span.start;
+ token = _scanner.advance();
+
+ if (token.type == TokenType.KEY ||
+ token.type == TokenType.VALUE ||
+ token.type == TokenType.BLOCK_END) {
+ _state = _State.BLOCK_MAPPING_VALUE;
+ return _processEmptyScalar(start);
+ } else {
+ _states.add(_State.BLOCK_MAPPING_VALUE);
+ return _parseNode(block: true, indentlessSequence: true);
+ }
+ }
+
+ // libyaml doesn't allow empty keys without an explicit key indicator, but
+ // the spec does. See example 8.18:
+ // http://yaml.org/spec/1.2/spec.html#id2798896.
+ if (token.type == TokenType.VALUE) {
+ _state = _State.BLOCK_MAPPING_VALUE;
+ return _processEmptyScalar(token.span.start);
+ }
+
+ if (token.type == TokenType.BLOCK_END) {
+ _scanner.scan();
+ _state = _states.removeLast();
+ return new Event(EventType.MAPPING_END, token.span);
+ }
+
+ throw new YamlException("Expected a key while parsing a block mapping.",
+ token.span.start.pointSpan());
+ }
+
+ /// Parses the productions:
+ ///
+ /// block_mapping ::= BLOCK-MAPPING_START
+ ///
+ /// ((KEY block_node_or_indentless_sequence?)?
+ ///
+ /// (VALUE block_node_or_indentless_sequence?)?)*
+ /// ***** *
+ /// BLOCK-END
+ ///
+ Event _parseBlockMappingValue() {
+ var token = _scanner.peek();
+
+ if (token.type != TokenType.VALUE) {
+ _state = _State.BLOCK_MAPPING_KEY;
+ return _processEmptyScalar(token.span.start);
+ }
+
+ var start = token.span.start;
+ token = _scanner.advance();
+ if (token.type == TokenType.KEY ||
+ token.type == TokenType.VALUE ||
+ token.type == TokenType.BLOCK_END) {
+ _state = _State.BLOCK_MAPPING_KEY;
+ return _processEmptyScalar(start);
+ } else {
+ _states.add(_State.BLOCK_MAPPING_KEY);
+ return _parseNode(block: true, indentlessSequence: true);
+ }
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_sequence ::= FLOW-SEQUENCE-START
+ /// *******************
+ /// (flow_sequence_entry FLOW-ENTRY)*
+ /// * **********
+ /// flow_sequence_entry?
+ /// *
+ /// FLOW-SEQUENCE-END
+ /// *****************
+ /// flow_sequence_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// *
+ Event _parseFlowSequenceEntry({bool first: false}) {
+ if (first) _scanner.scan();
+ var token = _scanner.peek();
+
+ if (token.type != TokenType.FLOW_SEQUENCE_END) {
+ if (!first) {
+ if (token.type != TokenType.FLOW_ENTRY) {
+ throw new YamlException(
+ "While parsing a flow sequence, expected ',' or ']'.",
+ token.span.start.pointSpan());
+ }
+
+ token = _scanner.advance();
+ }
+
+ if (token.type == TokenType.KEY) {
+ _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_KEY;
+ _scanner.scan();
+ return new MappingStartEvent(
+ token.span, CollectionStyle.FLOW);
+ } else if (token.type != TokenType.FLOW_SEQUENCE_END) {
+ _states.add(_State.FLOW_SEQUENCE_ENTRY);
+ return _parseNode();
+ }
+ }
+
+ _scanner.scan();
+ _state = _states.removeLast();
+ return new Event(EventType.SEQUENCE_END, token.span);
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_sequence_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// *** *
+ Event _parseFlowSequenceEntryMappingKey() {
+ var token = _scanner.peek();
+
+ if (token.type == TokenType.VALUE ||
+ token.type == TokenType.FLOW_ENTRY ||
+ token.type == TokenType.FLOW_SEQUENCE_END) {
+ // libyaml consumes the token here, but that seems like a bug, since it
+ // always causes [_parseFlowSequenceEntryMappingValue] to emit an empty
+ // scalar.
+
+ var start = token.span.start;
+ _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE;
+ return _processEmptyScalar(start);
+ } else {
+ _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_VALUE);
+ return _parseNode();
+ }
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_sequence_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// ***** *
+ Event _parseFlowSequenceEntryMappingValue() {
+ var token = _scanner.peek();
+
+ if (token.type == TokenType.VALUE) {
+ token = _scanner.advance();
+ if (token.type != TokenType.FLOW_ENTRY &&
+ token.type != TokenType.FLOW_SEQUENCE_END) {
+ _states.add(_State.FLOW_SEQUENCE_ENTRY_MAPPING_END);
+ return _parseNode();
+ }
+ }
+
+ _state = _State.FLOW_SEQUENCE_ENTRY_MAPPING_END;
+ return _processEmptyScalar(token.span.start);
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_sequence_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// *
+ Event _parseFlowSequenceEntryMappingEnd() {
+ _state = _State.FLOW_SEQUENCE_ENTRY;
+ return new Event(EventType.MAPPING_END,
+ _scanner.peek().span.start.pointSpan());
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_mapping ::= FLOW-MAPPING-START
+ /// ******************
+ /// (flow_mapping_entry FLOW-ENTRY)*
+ /// * **********
+ /// flow_mapping_entry?
+ /// ******************
+ /// FLOW-MAPPING-END
+ /// ****************
+ /// flow_mapping_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// * *** *
+ Event _parseFlowMappingKey({bool first: false}) {
+ if (first) _scanner.scan();
+ var token = _scanner.peek();
+
+ if (token.type != TokenType.FLOW_MAPPING_END) {
+ if (!first) {
+ if (token.type != TokenType.FLOW_ENTRY) {
+ throw new YamlException(
+ "While parsing a flow mapping, expected ',' or '}'.",
+ token.span.start.pointSpan());
+ }
+
+ token = _scanner.advance();
+ }
+
+ if (token.type == TokenType.KEY) {
+ token = _scanner.advance();
+ if (token.type != TokenType.VALUE &&
+ token.type != TokenType.FLOW_ENTRY &&
+ token.type != TokenType.FLOW_MAPPING_END) {
+ _states.add(_State.FLOW_MAPPING_VALUE);
+ return _parseNode();
+ } else {
+ _state = _State.FLOW_MAPPING_VALUE;
+ return _processEmptyScalar(token.span.start);
+ }
+ } else if (token.type != TokenType.FLOW_MAPPING_END) {
+ _states.add(_State.FLOW_MAPPING_EMPTY_VALUE);
+ return _parseNode();
+ }
+ }
+
+ _scanner.scan();
+ _state = _states.removeLast();
+ return new Event(EventType.MAPPING_END, token.span);
+ }
+
+ /// Parses the productions:
+ ///
+ /// flow_mapping_entry ::=
+ /// flow_node | KEY flow_node? (VALUE flow_node?)?
+ /// * ***** *
+ Event _parseFlowMappingValue({bool empty: false}) {
+ var token = _scanner.peek();
+
+ if (empty) {
+ _state = _State.FLOW_MAPPING_KEY;
+ return _processEmptyScalar(token.span.start);
+ }
+
+ if (token.type == TokenType.VALUE) {
+ token = _scanner.advance();
+ if (token.type != TokenType.FLOW_ENTRY &&
+ token.type != TokenType.FLOW_MAPPING_END) {
+ _states.add(_State.FLOW_MAPPING_KEY);
+ return _parseNode();
+ }
+ }
+
+ _state = _State.FLOW_MAPPING_KEY;
+ return _processEmptyScalar(token.span.start);
+ }
+
+ /// Generate an empty scalar event.
+ Event _processEmptyScalar(SourceLocation location) =>
+ new ScalarEvent(location.pointSpan(), '', ScalarStyle.PLAIN);
+
+ /// Parses directives.
+ Pair<VersionDirective, List<TagDirective>> _processDirectives() {
+ var token = _scanner.peek();
+
+ var versionDirective;
+ var tagDirectives = [];
+ var reservedDirectives = [];
+ while (token.type == TokenType.VERSION_DIRECTIVE ||
+ token.type == TokenType.TAG_DIRECTIVE) {
+ if (token is VersionDirectiveToken) {
+ if (versionDirective != null) {
+ throw new YamlException("Duplicate %YAML directive.", token.span);
+ }
+
+ if (token.major != 1 || token.minor == 0) {
+ throw new YamlException(
+ "Incompatible YAML document. This parser only supports YAML 1.1 "
+ "and 1.2.",
+ token.span);
+ } else if (token.minor > 2) {
+ // TODO(nweiz): Print to stderr when issue 6943 is fixed and dart:io
+ // is available.
+ warn("Warning: this parser only supports YAML 1.1 and 1.2.",
+ token.span);
+ }
+
+ versionDirective = new VersionDirective(token.major, token.minor);
+ } else if (token is TagDirectiveToken) {
+ var tagDirective = new TagDirective(token.handle, token.prefix);
+ _appendTagDirective(tagDirective, token.span);
+ tagDirectives.add(tagDirective);
+ }
+
+ token = _scanner.advance();
+ }
+
+ _appendTagDirective(
+ new TagDirective("!", "!"),
+ token.span.start.pointSpan(),
+ allowDuplicates: true);
+ _appendTagDirective(
+ new TagDirective("!!", "tag:yaml.org,2002:"),
+ token.span.start.pointSpan(),
+ allowDuplicates: true);
+
+ return new Pair(versionDirective, tagDirectives);
+ }
+
+ /// Adds a tag directive to the directives stack.
+ void _appendTagDirective(TagDirective newDirective, FileSpan span,
+ {bool allowDuplicates: false}) {
+ if (_tagDirectives.containsKey(newDirective.handle)) {
+ if (allowDuplicates) return;
+ throw new YamlException("Duplicate %TAG directive.", span);
+ }
+
+ _tagDirectives[newDirective.handle] = newDirective;
+ }
+}
+
+/// The possible states for the parser.
+class _State {
+ /// Expect [TokenType.STREAM_START].
+ static const STREAM_START = const _State("STREAM_START");
+
+ /// Expect the beginning of an implicit document.
+ static const IMPLICIT_DOCUMENT_START =
+ const _State("IMPLICIT_DOCUMENT_START");
+
+ /// Expect [TokenType.DOCUMENT_START].
+ static const DOCUMENT_START = const _State("DOCUMENT_START");
+
+ /// Expect the content of a document.
+ static const DOCUMENT_CONTENT = const _State("DOCUMENT_CONTENT");
+
+ /// Expect [TokenType.DOCUMENT_END].
+ static const DOCUMENT_END = const _State("DOCUMENT_END");
+
+ /// Expect a block node.
+ static const BLOCK_NODE = const _State("BLOCK_NODE");
+
+ /// Expect a block node or indentless sequence.
+ static const BLOCK_NODE_OR_INDENTLESS_SEQUENCE =
+ const _State("BLOCK_NODE_OR_INDENTLESS_SEQUENCE");
+
+ /// Expect a flow node.
+ static const FLOW_NODE = const _State("FLOW_NODE");
+
+ /// Expect the first entry of a block sequence.
+ static const BLOCK_SEQUENCE_FIRST_ENTRY =
+ const _State("BLOCK_SEQUENCE_FIRST_ENTRY");
+
+ /// Expect an entry of a block sequence.
+ static const BLOCK_SEQUENCE_ENTRY = const _State("BLOCK_SEQUENCE_ENTRY");
+
+ /// Expect an entry of an indentless sequence.
+ static const INDENTLESS_SEQUENCE_ENTRY =
+ const _State("INDENTLESS_SEQUENCE_ENTRY");
+
+ /// Expect the first key of a block mapping.
+ static const BLOCK_MAPPING_FIRST_KEY =
+ const _State("BLOCK_MAPPING_FIRST_KEY");
+
+ /// Expect a block mapping key.
+ static const BLOCK_MAPPING_KEY = const _State("BLOCK_MAPPING_KEY");
+
+ /// Expect a block mapping value.
+ static const BLOCK_MAPPING_VALUE = const _State("BLOCK_MAPPING_VALUE");
+
+ /// Expect the first entry of a flow sequence.
+ static const FLOW_SEQUENCE_FIRST_ENTRY =
+ const _State("FLOW_SEQUENCE_FIRST_ENTRY");
+
+ /// Expect an entry of a flow sequence.
+ static const FLOW_SEQUENCE_ENTRY = const _State("FLOW_SEQUENCE_ENTRY");
+
+ /// Expect a key of an ordered mapping.
+ static const FLOW_SEQUENCE_ENTRY_MAPPING_KEY =
+ const _State("FLOW_SEQUENCE_ENTRY_MAPPING_KEY");
+
+ /// Expect a value of an ordered mapping.
+ static const FLOW_SEQUENCE_ENTRY_MAPPING_VALUE =
+ const _State("FLOW_SEQUENCE_ENTRY_MAPPING_VALUE");
+
+ /// Expect the and of an ordered mapping entry.
+ static const FLOW_SEQUENCE_ENTRY_MAPPING_END =
+ const _State("FLOW_SEQUENCE_ENTRY_MAPPING_END");
+
+ /// Expect the first key of a flow mapping.
+ static const FLOW_MAPPING_FIRST_KEY = const _State("FLOW_MAPPING_FIRST_KEY");
+
+ /// Expect a key of a flow mapping.
+ static const FLOW_MAPPING_KEY = const _State("FLOW_MAPPING_KEY");
+
+ /// Expect a value of a flow mapping.
+ static const FLOW_MAPPING_VALUE = const _State("FLOW_MAPPING_VALUE");
+
+ /// Expect an empty value of a flow mapping.
+ static const FLOW_MAPPING_EMPTY_VALUE =
+ const _State("FLOW_MAPPING_EMPTY_VALUE");
+
+ /// Expect nothing.
+ static const END = const _State("END");
+
+ final String name;
+
+ const _State(this.name);
+
+ String toString() => name;
}
diff --git a/lib/src/scanner.dart b/lib/src/scanner.dart
new file mode 100644
index 0000000..0068553
--- /dev/null
+++ b/lib/src/scanner.dart
@@ -0,0 +1,1663 @@
+// Copyright (c) 2014, 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 yaml.scanner;
+
+import 'package:collection/collection.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:source_span/source_span.dart';
+
+import 'style.dart';
+import 'token.dart';
+import 'utils.dart';
+import 'yaml_exception.dart';
+
+/// A scanner that reads a string of Unicode characters and emits [Token]s.
+///
+/// This is based on the libyaml scanner, available at
+/// https://github.com/yaml/libyaml/blob/master/src/scanner.c. The license for
+/// that is available in ../../libyaml-license.txt.
+class Scanner {
+ static const TAB = 0x9;
+ static const LF = 0xA;
+ static const CR = 0xD;
+ static const SP = 0x20;
+ static const DOLLAR = 0x24;
+ static const LEFT_PAREN = 0x28;
+ static const RIGHT_PAREN = 0x29;
+ static const PLUS = 0x2B;
+ static const COMMA = 0x2C;
+ static const HYPHEN = 0x2D;
+ static const PERIOD = 0x2E;
+ static const QUESTION = 0x3F;
+ static const COLON = 0x3A;
+ static const SEMICOLON = 0x3B;
+ static const EQUALS = 0x3D;
+ static const LEFT_SQUARE = 0x5B;
+ static const RIGHT_SQUARE = 0x5D;
+ static const LEFT_CURLY = 0x7B;
+ static const RIGHT_CURLY = 0x7D;
+ static const HASH = 0x23;
+ static const AMPERSAND = 0x26;
+ static const ASTERISK = 0x2A;
+ static const EXCLAMATION = 0x21;
+ static const VERTICAL_BAR = 0x7C;
+ static const LEFT_ANGLE = 0x3C;
+ static const RIGHT_ANGLE = 0x3E;
+ static const SINGLE_QUOTE = 0x27;
+ static const DOUBLE_QUOTE = 0x22;
+ static const PERCENT = 0x25;
+ static const AT = 0x40;
+ static const GRAVE_ACCENT = 0x60;
+ static const TILDE = 0x7E;
+
+ static const NULL = 0x0;
+ static const BELL = 0x7;
+ static const BACKSPACE = 0x8;
+ static const VERTICAL_TAB = 0xB;
+ static const FORM_FEED = 0xC;
+ static const ESCAPE = 0x1B;
+ static const SLASH = 0x2F;
+ static const BACKSLASH = 0x5C;
+ static const UNDERSCORE = 0x5F;
+ static const NEL = 0x85;
+ static const NBSP = 0xA0;
+ static const LINE_SEPARATOR = 0x2028;
+ static const PARAGRAPH_SEPARATOR = 0x2029;
+ static const BOM = 0xFEFF;
+
+ static const NUMBER_0 = 0x30;
+ static const NUMBER_9 = 0x39;
+
+ static const LETTER_A = 0x61;
+ static const LETTER_B = 0x62;
+ static const LETTER_E = 0x65;
+ static const LETTER_F = 0x66;
+ static const LETTER_N = 0x6E;
+ static const LETTER_R = 0x72;
+ static const LETTER_T = 0x74;
+ static const LETTER_U = 0x75;
+ static const LETTER_V = 0x76;
+ static const LETTER_X = 0x78;
+ static const LETTER_Z = 0x7A;
+
+ static const LETTER_CAP_A = 0x41;
+ static const LETTER_CAP_F = 0x46;
+ static const LETTER_CAP_L = 0x4C;
+ static const LETTER_CAP_N = 0x4E;
+ static const LETTER_CAP_P = 0x50;
+ static const LETTER_CAP_U = 0x55;
+ static const LETTER_CAP_X = 0x58;
+ static const LETTER_CAP_Z = 0x5A;
+
+ /// The underlying [SpanScanner] used to read characters from the source text.
+ ///
+ /// This is also used to track line and column information and to generate
+ /// [SourceSpan]s.
+ final SpanScanner _scanner;
+
+ /// Whether this scanner has produced a [TokenType.STREAM_START] token
+ /// indicating the beginning of the YAML stream.
+ var _streamStartProduced = false;
+
+ /// Whether this scanner has produced a [TokenType.STREAM_END] token
+ /// indicating the end of the YAML stream.
+ var _streamEndProduced = false;
+
+ /// The queue of tokens yet to be emitted.
+ ///
+ /// These are queued up in advance so that [TokenType.KEY] tokens can be
+ /// inserted once the scanner determines that a series of tokens represents a
+ /// mapping key.
+ final _tokens = new QueueList<Token>();
+
+ /// The number of tokens that have been emitted.
+ ///
+ /// This doesn't count tokens in [tokens].
+ var _tokensParsed = 0;
+
+ /// Whether the next token in [_tokens] is ready to be returned.
+ ///
+ /// It might not be ready if there may still be a [TokenType.KEY] inserted
+ /// before it.
+ var _tokenAvailable = false;
+
+ /// The stack of indent levels for the current nested block contexts.
+ ///
+ /// The YAML spec specifies that the initial indentation level is -1 spaces.
+ final _indents = <int>[-1];
+
+ /// Whether a simple key is allowed in this context.
+ ///
+ /// A simple key refers to any mapping key that doesn't have an explicit "?".
+ var _simpleKeyAllowed = true;
+
+ /// The stack of potential simple keys for each level of flow nesting.
+ ///
+ /// Entries in this list may be `null`, indicating that there is no valid
+ /// simple key for the associated level of nesting.
+ ///
+ /// When a ":" is parsed and there's a simple key available, a [TokenType.KEY]
+ /// token is inserted in [_tokens] before that key's token. This allows the
+ /// parser to tell that the key is intended to be a mapping key.
+ final _simpleKeys = <_SimpleKey>[null];
+
+ /// The current indentation level.
+ int get _indent => _indents.last;
+
+ /// Whether the scanner's currently positioned in a block-level structure (as
+ /// opposed to flow-level).
+ bool get _inBlockContext => _simpleKeys.length == 1;
+
+ /// Whether the current character is a line break or the end of the source.
+ bool get _isBreakOrEnd => _scanner.isDone || _isBreak;
+
+ /// Whether the current character is a line break.
+ bool get _isBreak => _isBreakAt(0);
+
+ /// Whether the current character is whitespace or the end of the source.
+ bool get _isBlankOrEnd => _isBlankOrEndAt(0);
+
+ /// Whether the current character is whitespace.
+ bool get _isBlank => _isBlankAt(0);
+
+ /// Whether the current character is a valid tag name character.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#ns-tag-name.
+ bool get _isTagChar {
+ var char = _scanner.peekChar();
+ if (char == null) return false;
+ switch (char) {
+ case HYPHEN:
+ case SEMICOLON:
+ case SLASH:
+ case COLON:
+ case AT:
+ case AMPERSAND:
+ case EQUALS:
+ case PLUS:
+ case DOLLAR:
+ case PERIOD:
+ case TILDE:
+ case QUESTION:
+ case ASTERISK:
+ case SINGLE_QUOTE:
+ case LEFT_PAREN:
+ case RIGHT_PAREN:
+ case PERCENT:
+ return true;
+ default:
+ return (char >= NUMBER_0 && char <= NUMBER_9) ||
+ (char >= LETTER_A && char <= LETTER_Z) ||
+ (char >= LETTER_CAP_A && char <= LETTER_CAP_Z);
+ }
+ }
+
+ /// Whether the current character is a valid anchor name character.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#ns-anchor-name.
+ bool get _isAnchorChar {
+ if (!_isNonSpace) return false;
+
+ switch (_scanner.peekChar()) {
+ case COMMA:
+ case LEFT_SQUARE:
+ case RIGHT_SQUARE:
+ case LEFT_CURLY:
+ case RIGHT_CURLY:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /// Whether the character at the current position is a decimal digit.
+ bool get _isDigit {
+ var char = _scanner.peekChar();
+ return char != null && (char >= NUMBER_0 && char <= NUMBER_9);
+ }
+
+ /// Whether the character at the current position is a hexidecimal
+ /// digit.
+ bool get _isHex {
+ var char = _scanner.peekChar();
+ if (char == null) return false;
+ return (char >= NUMBER_0 && char <= NUMBER_9) ||
+ (char >= LETTER_A && char <= LETTER_F) ||
+ (char >= LETTER_CAP_A && char <= LETTER_CAP_F);
+ }
+
+ /// Whether the character at the current position is a plain character.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+ bool get _isPlainChar => _isPlainCharAt(0);
+
+ /// Whether the character at the current position is a printable character
+ /// other than a line break or byte-order mark.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+ bool get _isNonBreak {
+ var char = _scanner.peekChar();
+ if (char == null) return false;
+ switch (char) {
+ case LF:
+ case CR:
+ case BOM:
+ return false;
+ case TAB:
+ case NEL:
+ return true;
+ default:
+ return (char >= 0x00020 && char <= 0x00007E) ||
+ (char >= 0x000A0 && char <= 0x00D7FF) ||
+ (char >= 0x0E000 && char <= 0x00FFFD) ||
+ (char >= 0x10000 && char <= 0x10FFFF);
+ }
+ }
+
+ /// Whether the character at the current position is a printable character
+ /// other than whitespace.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#nb-char.
+ bool get _isNonSpace {
+ var char = _scanner.peekChar();
+ if (char == null) return false;
+ switch (char) {
+ case LF:
+ case CR:
+ case BOM:
+ case SP:
+ return false;
+ case NEL:
+ return true;
+ default:
+ return (char >= 0x00020 && char <= 0x00007E) ||
+ (char >= 0x000A0 && char <= 0x00D7FF) ||
+ (char >= 0x0E000 && char <= 0x00FFFD) ||
+ (char >= 0x10000 && char <= 0x10FFFF);
+ }
+ }
+
+ /// Returns Whether or not the current character begins a documentation
+ /// indicator.
+ ///
+ /// If so, this sets the scanner's last match to that indicator.
+ bool get _isDocumentIndicator {
+ return _scanner.column == 0 && _isBlankOrEndAt(3) &&
+ (_scanner.matches('---') || _scanner.matches('...'));
+ }
+
+ /// Creates a scanner that scans [source].
+ ///
+ /// [sourceUrl] can be a String or a [Uri].
+ Scanner(String source, {sourceUrl})
+ : _scanner = new SpanScanner(source, sourceUrl: sourceUrl);
+
+ /// Consumes and returns the next token.
+ Token scan() {
+ if (_streamEndProduced) throw new StateError("Out of tokens.");
+ if (!_tokenAvailable) _fetchMoreTokens();
+
+ var token = _tokens.removeFirst();
+ _tokenAvailable = false;
+ _tokensParsed++;
+ _streamEndProduced = token is Token &&
+ token.type == TokenType.STREAM_END;
+ return token;
+ }
+
+ /// Consumes the next token and returns the one after that.
+ Token advance() {
+ scan();
+ return peek();
+ }
+
+ /// Returns the next token without consuming it.
+ Token peek() {
+ if (_streamEndProduced) return null;
+ if (!_tokenAvailable) _fetchMoreTokens();
+ return _tokens.first;
+ }
+
+ /// Ensures that [_tokens] contains at least one token which can be returned.
+ void _fetchMoreTokens() {
+ while (true) {
+ if (_tokens.isNotEmpty) {
+ _staleSimpleKeys();
+
+ // If the current token could be a simple key, we need to scan more
+ // tokens until we determine whether it is or not. Otherwise we might
+ // not emit the `KEY` token before we emit the value of the key.
+ if (!_simpleKeys.any((key) =>
+ key != null && key.tokenNumber == _tokensParsed)) {
+ break;
+ }
+ }
+
+ _fetchNextToken();
+ }
+ _tokenAvailable = true;
+ }
+
+ /// The dispatcher for token fetchers.
+ void _fetchNextToken() {
+ if (!_streamStartProduced) {
+ _fetchStreamStart();
+ return;
+ }
+
+ _scanToNextToken();
+ _staleSimpleKeys();
+ _unrollIndent(_scanner.column);
+
+ if (_scanner.isDone) {
+ _fetchStreamEnd();
+ return;
+ }
+
+ if (_scanner.column == 0) {
+ if (_scanner.peekChar() == PERCENT) {
+ _fetchDirective();
+ return;
+ }
+
+ if (_isBlankOrEndAt(3)) {
+ if (_scanner.matches('---')) {
+ _fetchDocumentIndicator(TokenType.DOCUMENT_START);
+ return;
+ }
+
+ if (_scanner.matches('...')) {
+ _fetchDocumentIndicator(TokenType.DOCUMENT_END);
+ return;
+ }
+ }
+ }
+
+ switch (_scanner.peekChar()) {
+ case LEFT_SQUARE:
+ _fetchFlowCollectionStart(TokenType.FLOW_SEQUENCE_START);
+ return;
+ case LEFT_CURLY:
+ _fetchFlowCollectionStart(TokenType.FLOW_MAPPING_START);
+ return;
+ case RIGHT_SQUARE:
+ _fetchFlowCollectionEnd(TokenType.FLOW_SEQUENCE_END);
+ return;
+ case RIGHT_CURLY:
+ _fetchFlowCollectionEnd(TokenType.FLOW_MAPPING_END);
+ return;
+ case COMMA:
+ _fetchFlowEntry();
+ return;
+ case ASTERISK:
+ _fetchAnchor(anchor: false);
+ return;
+ case AMPERSAND:
+ _fetchAnchor(anchor: true);
+ return;
+ case EXCLAMATION:
+ _fetchTag();
+ return;
+ case SINGLE_QUOTE:
+ _fetchFlowScalar(singleQuote: true);
+ return;
+ case DOUBLE_QUOTE:
+ _fetchFlowScalar(singleQuote: false);
+ return;
+ case VERTICAL_BAR:
+ if (!_inBlockContext) _invalidScalarCharacter();
+ _fetchBlockScalar(literal: true);
+ return;
+ case RIGHT_ANGLE:
+ if (!_inBlockContext) _invalidScalarCharacter();
+ _fetchBlockScalar(literal: false);
+ return;
+ case PERCENT:
+ case AT:
+ case GRAVE_ACCENT:
+ _invalidScalarCharacter();
+ return;
+
+ // These characters may sometimes begin plain scalars.
+ case HYPHEN:
+ if (_isPlainCharAt(1)) {
+ _fetchPlainScalar();
+ } else {
+ _fetchBlockEntry();
+ }
+ return;
+ case QUESTION:
+ if (_isPlainCharAt(1)) {
+ _fetchPlainScalar();
+ } else {
+ _fetchKey();
+ }
+ return;
+ case COLON:
+ if (!_inBlockContext && _tokens.isNotEmpty) {
+ // If a colon follows a "JSON-like" value (an explicit map or list, or
+ // a quoted string) it isn't required to have whitespace after it
+ // since it unambiguously describes a map.
+ var token = _tokens.last;
+ if (token.type == TokenType.FLOW_SEQUENCE_END ||
+ token.type == TokenType.FLOW_MAPPING_END ||
+ (token.type == TokenType.SCALAR && token.style.isQuoted)) {
+ _fetchValue();
+ return;
+ }
+ }
+
+ if (_isPlainCharAt(1)) {
+ _fetchPlainScalar();
+ } else {
+ _fetchValue();
+ }
+ return;
+ default:
+ if (!_isNonBreak) _invalidScalarCharacter();
+
+ _fetchPlainScalar();
+ return;
+ }
+
+ throw 'Inaccessible';
+ }
+
+ /// Throws an error about a disallowed character.
+ void _invalidScalarCharacter() =>
+ _scanner.error("Unexpected character.", length: 1);
+
+ /// Checks the list of potential simple keys and remove the positions that
+ /// cannot contain simple keys anymore.
+ void _staleSimpleKeys() {
+ for (var i = 0; i < _simpleKeys.length; i++) {
+ var key = _simpleKeys[i];
+ if (key == null) continue;
+
+ // libyaml requires that all simple keys be a single line and no longer
+ // than 1024 characters. However, in section 7.4.2 of the spec
+ // (http://yaml.org/spec/1.2/spec.html#id2790832), these restrictions are
+ // only applied when the curly braces are omitted. It's difficult to
+ // retain enough context to know which keys need to have the restriction
+ // placed on them, so for now we go the other direction and allow
+ // everything but multiline simple keys in a block context.
+ if (!_inBlockContext) continue;
+
+ if (key.location.line == _scanner.line) continue;
+
+ if (key.required) {
+ throw new YamlException("Expected ':'.", _scanner.emptySpan);
+ }
+
+ _simpleKeys[i] = null;
+ }
+ }
+
+ /// Checks if a simple key may start at the current position and saves it if
+ /// so.
+ void _saveSimpleKey() {
+ // A simple key is required at the current position if the scanner is in the
+ // block context and the current column coincides with the indentation
+ // level.
+ var required = _inBlockContext && _indent == _scanner.column;
+
+ // A simple key is required only when it is the first token in the current
+ // line. Therefore it is always allowed. But we add a check anyway.
+ assert(_simpleKeyAllowed || !required);
+
+ if (!_simpleKeyAllowed) return;
+
+ // If the current position may start a simple key, save it.
+ _removeSimpleKey();
+ _simpleKeys[_simpleKeys.length - 1] = new _SimpleKey(
+ _tokensParsed + _tokens.length,
+ _scanner.location,
+ required: required);
+ }
+
+ /// Removes a potential simple key at the current flow level.
+ void _removeSimpleKey() {
+ var key = _simpleKeys.last;
+ if (key != null && key.required) {
+ throw new YamlException("Could not find expected ':' for simple key.",
+ key.location.pointSpan());
+ }
+
+ _simpleKeys[_simpleKeys.length - 1] = null;
+ }
+
+ /// Increases the flow level and resizes the simple key list.
+ void _increaseFlowLevel() {
+ _simpleKeys.add(null);
+ }
+
+ /// Decreases the flow level.
+ void _decreaseFlowLevel() {
+ if (_inBlockContext) return;
+ _simpleKeys.removeLast();
+ }
+
+ /// Pushes the current indentation level to the stack and sets the new level
+ /// if [column] is greater than [_indent].
+ ///
+ /// If it is, appends or inserts the specified token into [_tokens]. If
+ /// [tokenNumber] is provided, the corresponding token will be replaced;
+ /// otherwise, the token will be added at the end.
+ void _rollIndent(int column, TokenType type, SourceLocation location,
+ {int tokenNumber}) {
+ if (!_inBlockContext) return;
+ if (_indent != -1 && _indent >= column) return;
+
+ // Push the current indentation level to the stack and set the new
+ // indentation level.
+ _indents.add(column);
+
+ // Create a token and insert it into the queue.
+ var token = new Token(type, location.pointSpan());
+ if (tokenNumber == null) {
+ _tokens.add(token);
+ } else {
+ _tokens.insert(tokenNumber - _tokensParsed, token);
+ }
+ }
+
+ /// Pops indentation levels from [_indents] until the current level becomes
+ /// less than or equal to [column].
+ ///
+ /// For each indentation level, appends a [TokenType.BLOCK_END] token.
+ void _unrollIndent(int column) {
+ if (!_inBlockContext) return;
+
+ while (_indent > column) {
+ _tokens.add(new Token(TokenType.BLOCK_END, _scanner.emptySpan));
+ _indents.removeLast();
+ }
+ }
+
+ /// Pops indentation levels from [_indents] until the current level resets to
+ /// -1.
+ ///
+ /// For each indentation level, appends a [TokenType.BLOCK_END] token.
+ void _resetIndent() => _unrollIndent(-1);
+
+ /// Produces a [TokenType.STREAM_START] token.
+ void _fetchStreamStart() {
+ // Much of libyaml's initialization logic here is done in variable
+ // initializers instead.
+ _streamStartProduced = true;
+ _tokens.add(new Token(TokenType.STREAM_START, _scanner.emptySpan));
+ }
+
+ /// Produces a [TokenType.STREAM_END] token.
+ void _fetchStreamEnd() {
+ _resetIndent();
+ _removeSimpleKey();
+ _simpleKeyAllowed = false;
+ _tokens.add(new Token(TokenType.STREAM_END, _scanner.emptySpan));
+ }
+
+ /// Produces a [TokenType.VERSION_DIRECTIVE] or [TokenType.TAG_DIRECTIVE]
+ /// token.
+ void _fetchDirective() {
+ _resetIndent();
+ _removeSimpleKey();
+ _simpleKeyAllowed = false;
+ var directive = _scanDirective();
+ if (directive != null) _tokens.add(directive);
+ }
+
+ /// Produces a [TokenType.DOCUMENT_START] or [TokenType.DOCUMENT_END] token.
+ void _fetchDocumentIndicator(TokenType type) {
+ _resetIndent();
+ _removeSimpleKey();
+ _simpleKeyAllowed = false;
+
+ // Consume the indicator token.
+ var start = _scanner.state;
+ _scanner.readChar();
+ _scanner.readChar();
+ _scanner.readChar();
+
+ _tokens.add(new Token(type, _scanner.spanFrom(start)));
+ }
+
+ /// Produces a [TokenType.FLOW_SEQUENCE_START] or
+ /// [TokenType.FLOW_MAPPING_START] token.
+ void _fetchFlowCollectionStart(TokenType type) {
+ _saveSimpleKey();
+ _increaseFlowLevel();
+ _simpleKeyAllowed = true;
+ _addCharToken(type);
+ }
+
+ /// Produces a [TokenType.FLOW_SEQUENCE_END] or [TokenType.FLOW_MAPPING_END]
+ /// token.
+ void _fetchFlowCollectionEnd(TokenType type) {
+ _removeSimpleKey();
+ _decreaseFlowLevel();
+ _simpleKeyAllowed = false;
+ _addCharToken(type);
+ }
+
+ /// Produces a [TokenType.FLOW_ENTRY] token.
+ void _fetchFlowEntry() {
+ _removeSimpleKey();
+ _simpleKeyAllowed = true;
+ _addCharToken(TokenType.FLOW_ENTRY);
+ }
+
+ /// Produces a [TokenType.BLOCK_ENTRY] token.
+ void _fetchBlockEntry() {
+ if (_inBlockContext) {
+ if (!_simpleKeyAllowed) {
+ throw new YamlException(
+ "Block sequence entries are not allowed in this context.",
+ _scanner.emptySpan);
+ }
+
+ _rollIndent(
+ _scanner.column,
+ TokenType.BLOCK_SEQUENCE_START,
+ _scanner.emptySpan.start);
+ } else {
+ // It is an error for the '-' indicator to occur in the flow context, but
+ // we let the Parser detect and report it because it's able to point to
+ // the context.
+ }
+
+ _removeSimpleKey();
+ _simpleKeyAllowed = true;
+ _addCharToken(TokenType.BLOCK_ENTRY);
+ }
+
+ /// Produces the [TokenType.KEY] token.
+ void _fetchKey() {
+ if (_inBlockContext) {
+ if (!_simpleKeyAllowed) {
+ throw new YamlException("Mapping keys are not allowed in this context.",
+ _scanner.emptySpan);
+ }
+
+ _rollIndent(
+ _scanner.column,
+ TokenType.BLOCK_MAPPING_START,
+ _scanner.emptySpan.start);
+ }
+
+ // Simple keys are allowed after `?` in a block context.
+ _simpleKeyAllowed = _inBlockContext;
+ _addCharToken(TokenType.KEY);
+ }
+
+ /// Produces the [TokenType.VALUE] token.
+ void _fetchValue() {
+ var simpleKey = _simpleKeys.last;
+ if (simpleKey != null) {
+ // Add a [TokenType.KEY] directive before the first token of the simple
+ // key so the parser knows that it's part of a key/value pair.
+ _tokens.insert(simpleKey.tokenNumber - _tokensParsed,
+ new Token(TokenType.KEY, simpleKey.location.pointSpan()));
+
+ // In the block context, we may need to add the
+ // [TokenType.BLOCK_MAPPING_START] token.
+ _rollIndent(
+ simpleKey.location.column,
+ TokenType.BLOCK_MAPPING_START,
+ simpleKey.location,
+ tokenNumber: simpleKey.tokenNumber);
+
+ // Remove the simple key.
+ _simpleKeys[_simpleKeys.length - 1] = null;
+
+ // A simple key cannot follow another simple key.
+ _simpleKeyAllowed = false;
+ } else if (_inBlockContext) {
+ // If we're here, we've found the ':' indicator following a complex key.
+
+ if (!_simpleKeyAllowed) {
+ throw new YamlException(
+ "Mapping values are not allowed in this context.",
+ _scanner.emptySpan);
+ }
+
+ _rollIndent(
+ _scanner.column,
+ TokenType.BLOCK_MAPPING_START,
+ _scanner.location);
+ _simpleKeyAllowed = true;
+ } else if (_simpleKeyAllowed) {
+ // If we're here, we've found the ':' indicator with an empty key. This
+ // behavior differs from libyaml, which disallows empty implicit keys.
+ _simpleKeyAllowed = false;
+ _addCharToken(TokenType.KEY);
+ }
+
+ _addCharToken(TokenType.VALUE);
+ }
+
+ /// Adds a token with [type] to [_tokens].
+ ///
+ /// The span of the new token is the current character.
+ void _addCharToken(TokenType type) {
+ var start = _scanner.state;
+ _scanner.readChar();
+ _tokens.add(new Token(type, _scanner.spanFrom(start)));
+ }
+
+ /// Produces a [TokenType.ALIAS] or [TokenType.ANCHOR] token.
+ void _fetchAnchor({bool anchor: true}) {
+ _saveSimpleKey();
+ _simpleKeyAllowed = false;
+ _tokens.add(_scanAnchor(anchor: anchor));
+ }
+
+ /// Produces a [TokenType.TAG] token.
+ void _fetchTag() {
+ _saveSimpleKey();
+ _simpleKeyAllowed = false;
+ _tokens.add(_scanTag());
+ }
+
+ /// Produces a [TokenType.SCALAR] token with style [ScalarStyle.LITERAL] or
+ /// [ScalarStyle.FOLDED].
+ void _fetchBlockScalar({bool literal: false}) {
+ _removeSimpleKey();
+ _simpleKeyAllowed = true;
+ _tokens.add(_scanBlockScalar(literal: literal));
+ }
+
+ /// Produces a [TokenType.SCALAR] token with style [ScalarStyle.SINGLE_QUOTED]
+ /// or [ScalarStyle.DOUBLE_QUOTED].
+ void _fetchFlowScalar({bool singleQuote: false}) {
+ _saveSimpleKey();
+ _simpleKeyAllowed = false;
+ _tokens.add(_scanFlowScalar(singleQuote: singleQuote));
+ }
+
+ /// Produces a [TokenType.SCALAR] token with style [ScalarStyle.PLAIN].
+ void _fetchPlainScalar() {
+ _saveSimpleKey();
+ _simpleKeyAllowed = false;
+ _tokens.add(_scanPlainScalar());
+ }
+
+ /// Eats whitespace and comments until the next token is found.
+ void _scanToNextToken() {
+ var afterLineBreak = false;
+ while (true) {
+ // Allow the BOM to start a line.
+ if (_scanner.column == 0) _scanner.scan("\uFEFF");
+
+ // Eat whitespace.
+ //
+ // libyaml disallows tabs after "-", "?", or ":", but the spec allows
+ // them. See section 6.2: http://yaml.org/spec/1.2/spec.html#id2778241.
+ while (_scanner.peekChar() == SP ||
+ ((!_inBlockContext || !afterLineBreak) &&
+ _scanner.peekChar() == TAB)) {
+ _scanner.readChar();
+ }
+
+ if (_scanner.peekChar() == TAB) {
+ _scanner.error("Tab characters are not allowed as indentation.",
+ length: 1);
+ }
+
+ // Eat a comment until a line break.
+ _skipComment();
+
+ // If we're at a line break, eat it.
+ if (_isBreak) {
+ _skipLine();
+
+ // In the block context, a new line may start a simple key.
+ if (_inBlockContext) _simpleKeyAllowed = true;
+ afterLineBreak = true;
+ } else {
+ // Otherwise we've found a token.
+ break;
+ }
+ }
+ }
+
+ /// Scans a [TokenType.YAML_DIRECTIVE] or [TokenType.TAG_DIRECTIVE] token.
+ ///
+ /// %YAML 1.2 # a comment \n
+ /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ /// %TAG !yaml! tag:yaml.org,2002: \n
+ /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ Token _scanDirective() {
+ var start = _scanner.state;
+
+ // Eat '%'.
+ _scanner.readChar();
+
+ var token;
+ var name = _scanDirectiveName();
+ if (name == "YAML") {
+ token = _scanVersionDirectiveValue(start);
+ } else if (name == "TAG") {
+ token = _scanTagDirectiveValue(start);
+ } else {
+ warn("Warning: unknown directive.", _scanner.spanFrom(start));
+
+ // libyaml doesn't support unknown directives, but the spec says to ignore
+ // them and warn: http://yaml.org/spec/1.2/spec.html#id2781147.
+ while (!_isBreakOrEnd) {
+ _scanner.readChar();
+ }
+
+ return null;
+ }
+
+ // Eat the rest of the line, including any comments.
+ _skipBlanks();
+ _skipComment();
+
+ if (!_isBreakOrEnd) {
+ throw new YamlException(
+ "Expected comment or line break after directive.",
+ _scanner.spanFrom(start));
+ }
+
+ _skipLine();
+ return token;
+ }
+
+ /// Scans a directive name.
+ ///
+ /// %YAML 1.2 # a comment \n
+ /// ^^^^
+ /// %TAG !yaml! tag:yaml.org,2002: \n
+ /// ^^^
+ String _scanDirectiveName() {
+ // libyaml only allows word characters in directive names, but the spec
+ // disagrees: http://yaml.org/spec/1.2/spec.html#ns-directive-name.
+ var start = _scanner.position;
+ while (_isNonSpace) {
+ _scanner.readChar();
+ }
+
+ var name = _scanner.substring(start);
+ if (name.isEmpty) {
+ throw new YamlException("Expected directive name.", _scanner.emptySpan);
+ } else if (!_isBlankOrEnd) {
+ throw new YamlException(
+ "Unexpected character in directive name.", _scanner.emptySpan);
+ }
+
+ return name;
+ }
+
+ /// Scans the value of a version directive.
+ ///
+ /// %YAML 1.2 # a comment \n
+ /// ^^^^^^
+ Token _scanVersionDirectiveValue(LineScannerState start) {
+ _skipBlanks();
+
+ var major = _scanVersionDirectiveNumber();
+ _scanner.expect('.');
+ var minor = _scanVersionDirectiveNumber();
+
+ return new VersionDirectiveToken(_scanner.spanFrom(start), major, minor);
+ }
+
+ /// Scans the version number of a version directive.
+ ///
+ /// %YAML 1.2 # a comment \n
+ /// ^
+ /// %YAML 1.2 # a comment \n
+ /// ^
+ int _scanVersionDirectiveNumber() {
+ var start = _scanner.position;
+ while (_isDigit) {
+ _scanner.readChar();
+ }
+
+ var number = _scanner.substring(start);
+ if (number.isEmpty) {
+ throw new YamlException("Expected version number.", _scanner.emptySpan);
+ }
+
+ return int.parse(number);
+ }
+
+ /// Scans the value of a tag directive.
+ ///
+ /// %TAG !yaml! tag:yaml.org,2002: \n
+ /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ Token _scanTagDirectiveValue(LineScannerState start) {
+ _skipBlanks();
+
+ var handle = _scanTagHandle(directive: true);
+ if (!_isBlank) {
+ throw new YamlException("Expected whitespace.", _scanner.emptySpan);
+ }
+
+ _skipBlanks();
+
+ var prefix = _scanTagUri();
+ if (!_isBlankOrEnd) {
+ throw new YamlException("Expected whitespace.", _scanner.emptySpan);
+ }
+
+ return new TagDirectiveToken(_scanner.spanFrom(start), handle, prefix);
+ }
+
+ /// Scans a [TokenType.ANCHOR] token.
+ Token _scanAnchor({bool anchor: true}) {
+ var start = _scanner.state;
+
+ // Eat the indicator character.
+ _scanner.readChar();
+
+ // libyaml only allows word characters in anchor names, but the spec
+ // disagrees: http://yaml.org/spec/1.2/spec.html#ns-anchor-char.
+ var startPosition = _scanner.position;
+ while (_isAnchorChar) {
+ _scanner.readChar();
+ }
+ var name = _scanner.substring(startPosition);
+
+ var next = _scanner.peekChar();
+ if (name.isEmpty ||
+ (!_isBlankOrEnd && next != QUESTION && next != COLON &&
+ next != COMMA && next != RIGHT_SQUARE && next != RIGHT_CURLY &&
+ next != PERCENT && next != AT && next != GRAVE_ACCENT)) {
+ throw new YamlException("Expected alphanumeric character.",
+ _scanner.emptySpan);
+ }
+
+ if (anchor) {
+ return new AnchorToken(_scanner.spanFrom(start), name);
+ } else {
+ return new AliasToken(_scanner.spanFrom(start), name);
+ }
+ }
+
+ /// Scans a [TokenType.TAG] token.
+ Token _scanTag() {
+ var handle;
+ var suffix;
+ var start = _scanner.state;
+
+ // Check if the tag is in the canonical form.
+ if (_scanner.peekChar(1) == LEFT_ANGLE) {
+ // Eat '!<'.
+ _scanner.readChar();
+ _scanner.readChar();
+
+ handle = '';
+ suffix = _scanTagUri();
+
+ _scanner.expect('>');
+ } else {
+ // The tag has either the '!suffix' or the '!handle!suffix' form.
+
+ // First, try to scan a handle.
+ handle = _scanTagHandle();
+
+ if (handle.length > 1 && handle.startsWith('!') && handle.endsWith('!')) {
+ suffix = _scanTagUri(flowSeparators: false);
+ } else {
+ suffix = _scanTagUri(head: handle, flowSeparators: false);
+
+ // There was no explicit handle.
+ if (suffix.isEmpty) {
+ // This is the special '!' tag.
+ handle = null;
+ suffix = '!';
+ } else {
+ handle = '!';
+ }
+ }
+ }
+
+ // libyaml insists on whitespace after a tag, but example 7.2 indicates
+ // that it's not required: http://yaml.org/spec/1.2/spec.html#id2786720.
+
+ return new TagToken(_scanner.spanFrom(start), handle, suffix);
+ }
+
+ /// Scans a tag handle.
+ String _scanTagHandle({bool directive: false}) {
+ _scanner.expect('!');
+
+ var buffer = new StringBuffer('!');
+
+ // libyaml only allows word characters in tags, but the spec disagrees:
+ // http://yaml.org/spec/1.2/spec.html#ns-tag-char.
+ var start = _scanner.position;
+ while (_isTagChar) {
+ _scanner.readChar();
+ }
+ buffer.write(_scanner.substring(start));
+
+ if (_scanner.peekChar() == EXCLAMATION) {
+ buffer.writeCharCode(_scanner.readChar());
+ } else {
+ // It's either the '!' tag or not really a tag handle. If it's a %TAG
+ // directive, it's an error. If it's a tag token, it must be part of a
+ // URI.
+ if (directive && buffer.toString() != '!') _scanner.expect('!');
+ }
+
+ return buffer.toString();
+ }
+
+ /// Scans a tag URI.
+ ///
+ /// [head] is the initial portion of the tag that's already been scanned.
+ /// [flowSeparators] indicates whether the tag URI can contain flow
+ /// separators.
+ String _scanTagUri({String head, bool flowSeparators: true}) {
+ var length = head == null ? 0 : head.length;
+ var buffer = new StringBuffer();
+
+ // Copy the head if needed.
+ //
+ // Note that we don't copy the leading '!' character.
+ if (length > 1) buffer.write(head.substring(1));
+
+ // The set of characters that may appear in URI is as follows:
+ //
+ // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&',
+ // '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']',
+ // '%'.
+ //
+ // In a shorthand tag annotation, the flow separators ',', '[', and ']' are
+ // disallowed.
+ var start = _scanner.position;
+ var char = _scanner.peekChar();
+ while (_isTagChar || (flowSeparators &&
+ (char == COMMA || char == LEFT_SQUARE || char == RIGHT_SQUARE))) {
+ _scanner.readChar();
+ char = _scanner.peekChar();
+ }
+
+ // libyaml manually decodes the URL, but we don't have to do that.
+ return Uri.decodeFull(_scanner.substring(start));
+ }
+
+ /// Scans a block scalar.
+ Token _scanBlockScalar({bool literal: false}) {
+ var start = _scanner.state;
+
+ // Eat the indicator '|' or '>'.
+ _scanner.readChar();
+
+ // Check for a chomping indicator.
+ var chomping = _Chomping.CLIP;
+ var increment = 0;
+ var char = _scanner.peekChar();
+ if (char == PLUS || char == HYPHEN) {
+ chomping = char == PLUS ? _Chomping.KEEP : _Chomping.STRIP;
+ _scanner.readChar();
+
+ // Check for an indentation indicator.
+ if (_isDigit) {
+ // Check that the indentation is greater than 0.
+ if (_scanner.peekChar() == NUMBER_0) {
+ throw new YamlException(
+ "0 may not be used as an indentation indicator.",
+ _scanner.spanFrom(start));
+ }
+
+ increment = _scanner.readChar() - NUMBER_0;
+ }
+ } else if (_isDigit) {
+ // Do the same as above, but in the opposite order.
+ if (_scanner.peekChar() == NUMBER_0) {
+ throw new YamlException(
+ "0 may not be used as an indentation indicator.",
+ _scanner.spanFrom(start));
+ }
+
+ increment = _scanner.readChar() - NUMBER_0;
+
+ char = _scanner.peekChar();
+ if (char == PLUS || char == HYPHEN) {
+ chomping = char == PLUS ? _Chomping.KEEP : _Chomping.STRIP;
+ _scanner.readChar();
+ }
+ }
+
+ // Eat whitespace and comments to the end of the line.
+ _skipBlanks();
+ _skipComment();
+
+ // Check if we're at the end of the line.
+ if (!_isBreakOrEnd) {
+ throw new YamlException("Expected comment or line break.",
+ _scanner.emptySpan);
+ }
+
+ _skipLine();
+
+ // If the block scalar has an explicit indentation indicator, add that to
+ // the current indentation to get the indentation level for the scalar's
+ // contents.
+ var indent = 0;
+ if (increment != 0) {
+ indent = _indent >= 0 ? _indent + increment : increment;
+ }
+
+ // Scan the leading line breaks to determine the indentation level if
+ // needed.
+ var pair = _scanBlockScalarBreaks(indent);
+ indent = pair.first;
+ var trailingBreaks = pair.last;
+
+ // Scan the block scalar contents.
+ var buffer = new StringBuffer();
+ var leadingBreak = '';
+ var leadingBlank = false;
+ var trailingBlank = false;
+ while (_scanner.column == indent && !_scanner.isDone) {
+ // Check for a document indicator. libyaml doesn't do this, but the spec
+ // mandates it. See example 9.5:
+ // http://yaml.org/spec/1.2/spec.html#id2801606.
+ if (_isDocumentIndicator) break;
+
+ // We are at the beginning of a non-empty line.
+
+ // Is there trailing whitespace?
+ trailingBlank = _isBlank;
+
+ // Check if we need to fold the leading line break.
+ if (!literal && leadingBreak.isNotEmpty && !leadingBlank &&
+ !trailingBlank) {
+ // Do we need to join the lines with a space?
+ if (trailingBreaks.isEmpty) buffer.writeCharCode(SP);
+ } else {
+ buffer.write(leadingBreak);
+ }
+ leadingBreak = '';
+
+ // Append the remaining line breaks.
+ buffer.write(trailingBreaks);
+
+ // Is there leading whitespace?
+ leadingBlank = _isBlank;
+
+ var startPosition = _scanner.position;
+ while (!_isBreakOrEnd) {
+ _scanner.readChar();
+ }
+ buffer.write(_scanner.substring(startPosition));
+
+ // libyaml always reads a line here, but this breaks on block scalars at
+ // the end of the document that end without newlines. See example 8.1:
+ // http://yaml.org/spec/1.2/spec.html#id2793888.
+ if (!_scanner.isDone) leadingBreak = _readLine();
+
+ // Eat the following indentation and spaces.
+ var pair = _scanBlockScalarBreaks(indent);
+ indent = pair.first;
+ trailingBreaks = pair.last;
+ }
+
+ // Chomp the tail.
+ if (chomping != _Chomping.STRIP) buffer.write(leadingBreak);
+ if (chomping == _Chomping.KEEP) buffer.write(trailingBreaks);
+
+ return new ScalarToken(_scanner.spanFrom(start), buffer.toString(),
+ literal ? ScalarStyle.LITERAL : ScalarStyle.FOLDED);
+ }
+
+ /// Scans indentation spaces and line breaks for a block scalar.
+ ///
+ /// Determines the intendation level if needed. Returns the new indentation
+ /// level and the text of the line breaks.
+ Pair<int, String> _scanBlockScalarBreaks(int indent) {
+ var maxIndent = 0;
+ var breaks = new StringBuffer();
+
+ while (true) {
+ while ((indent == 0 || _scanner.column < indent) &&
+ _scanner.peekChar() == SP) {
+ _scanner.readChar();
+ }
+
+ if (_scanner.column > maxIndent) maxIndent = _scanner.column;
+
+ // libyaml throws an error here if a tab character is detected, but the
+ // spec treats tabs like any other non-space character. See example 8.2:
+ // http://yaml.org/spec/1.2/spec.html#id2794311.
+
+ if (!_isBreak) break;
+ breaks.write(_readLine());
+ }
+
+ if (indent == 0) {
+ indent = maxIndent;
+ if (indent < _indent + 1) indent = _indent + 1;
+
+ // libyaml forces indent to be at least 1 here, but that doesn't seem to
+ // be supported by the spec.
+ }
+
+ return new Pair(indent, breaks.toString());
+ }
+
+ // Scans a quoted scalar.
+ Token _scanFlowScalar({bool singleQuote: false}) {
+ var start = _scanner.state;
+ var buffer = new StringBuffer();
+
+ // Eat the left quote.
+ _scanner.readChar();
+
+ while (true) {
+ // Check that there are no document indicators at the beginning of the
+ // line.
+ if (_isDocumentIndicator) {
+ _scanner.error("Unexpected document indicator.");
+ }
+
+ if (_scanner.isDone) {
+ throw new YamlException("Unexpected end of file.", _scanner.emptySpan);
+ }
+
+ var leadingBlanks = false;
+ while (!_isBlankOrEnd) {
+ var char = _scanner.peekChar();
+ if (singleQuote && char == SINGLE_QUOTE &&
+ _scanner.peekChar(1) == SINGLE_QUOTE) {
+ // An escaped single quote.
+ _scanner.readChar();
+ _scanner.readChar();
+ buffer.writeCharCode(SINGLE_QUOTE);
+ } else if (char == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+ // The closing quote.
+ break;
+ } else if (!singleQuote && char == BACKSLASH && _isBreakAt(1)) {
+ // An escaped newline.
+ _scanner.readChar();
+ _skipLine();
+ leadingBlanks = true;
+ break;
+ } else if (!singleQuote && char == BACKSLASH) {
+ var escapeStart = _scanner.state;
+
+ // An escape sequence.
+ var codeLength = null;
+ switch (_scanner.peekChar(1)) {
+ case NUMBER_0:
+ buffer.writeCharCode(NULL);
+ break;
+ case LETTER_A:
+ buffer.writeCharCode(BELL);
+ break;
+ case LETTER_B:
+ buffer.writeCharCode(BACKSPACE);
+ break;
+ case LETTER_T:
+ case TAB:
+ buffer.writeCharCode(TAB);
+ break;
+ case LETTER_N:
+ buffer.writeCharCode(LF);
+ break;
+ case LETTER_V:
+ buffer.writeCharCode(VERTICAL_TAB);
+ break;
+ case LETTER_F:
+ buffer.writeCharCode(FORM_FEED);
+ break;
+ case LETTER_R:
+ buffer.writeCharCode(CR);
+ break;
+ case LETTER_E:
+ buffer.writeCharCode(ESCAPE);
+ break;
+ case SP:
+ case DOUBLE_QUOTE:
+ case SLASH:
+ case BACKSLASH:
+ // libyaml doesn't support an escaped forward slash, but it was
+ // added in YAML 1.2. See section 5.7:
+ // http://yaml.org/spec/1.2/spec.html#id2776092
+ buffer.writeCharCode(_scanner.peekChar(1));
+ break;
+ case LETTER_CAP_N:
+ buffer.writeCharCode(NEL);
+ break;
+ case UNDERSCORE:
+ buffer.writeCharCode(NBSP);
+ break;
+ case LETTER_CAP_L:
+ buffer.writeCharCode(LINE_SEPARATOR);
+ break;
+ case LETTER_CAP_P:
+ buffer.writeCharCode(PARAGRAPH_SEPARATOR);
+ break;
+ case LETTER_X:
+ codeLength = 2;
+ break;
+ case LETTER_U:
+ codeLength = 4;
+ break;
+ case LETTER_CAP_U:
+ codeLength = 8;
+ break;
+ default:
+ throw new YamlException("Unknown escape character.",
+ _scanner.spanFrom(escapeStart));
+ }
+
+ _scanner.readChar();
+ _scanner.readChar();
+
+ if (codeLength != null) {
+ var value = 0;
+ for (var i = 0; i < codeLength; i++) {
+ if (!_isHex) {
+ _scanner.readChar();
+ throw new YamlException(
+ "Expected $codeLength-digit hexidecimal number.",
+ _scanner.spanFrom(escapeStart));
+ }
+
+ value = (value << 4) + _asHex(_scanner.readChar());
+ }
+
+ // Check the value and write the character.
+ if ((value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF) {
+ throw new YamlException(
+ "Invalid Unicode character escape code.",
+ _scanner.spanFrom(escapeStart));
+ }
+
+ buffer.writeCharCode(value);
+ }
+ } else {
+ buffer.writeCharCode(_scanner.readChar());
+ }
+ }
+
+ // Check if we're at the end of a scalar.
+ if (_scanner.peekChar() == (singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE)) {
+ break;
+ }
+
+ var whitespace = new StringBuffer();
+ var leadingBreak = '';
+ var trailingBreaks = new StringBuffer();
+ while (_isBlank || _isBreak) {
+ if (_isBlank) {
+ // Consume a space or a tab.
+ if (!leadingBlanks) {
+ whitespace.writeCharCode(_scanner.readChar());
+ } else {
+ _scanner.readChar();
+ }
+ } else {
+ // Check if it's a first line break.
+ if (!leadingBlanks) {
+ whitespace.clear();
+ leadingBreak = _readLine();
+ leadingBlanks = true;
+ } else {
+ trailingBreaks.write(_readLine());
+ }
+ }
+ }
+
+ // Join the whitespace or fold line breaks.
+ if (leadingBlanks) {
+ if (leadingBreak.isNotEmpty && trailingBreaks.isEmpty) {
+ buffer.writeCharCode(SP);
+ } else {
+ buffer.write(trailingBreaks);
+ }
+ } else {
+ buffer.write(whitespace);
+ whitespace.clear();
+ }
+ }
+
+ // Eat the right quote.
+ _scanner.readChar();
+
+ return new ScalarToken(_scanner.spanFrom(start), buffer.toString(),
+ singleQuote ? ScalarStyle.SINGLE_QUOTED : ScalarStyle.DOUBLE_QUOTED);
+ }
+
+ /// Scans a plain scalar.
+ Token _scanPlainScalar() {
+ var start = _scanner.state;
+ var buffer = new StringBuffer();
+ var leadingBreak = '';
+ var trailingBreaks = '';
+ var whitespace = new StringBuffer();
+ var indent = _indent + 1;
+
+ while (true) {
+ // Check for a document indicator.
+ if (_isDocumentIndicator) break;
+
+ // Check for a comment.
+ if (_scanner.peekChar() == HASH) break;
+
+ if (_isPlainChar) {
+ // Join the whitespace or fold line breaks.
+ if (leadingBreak.isNotEmpty) {
+ if (trailingBreaks.isEmpty) {
+ buffer.writeCharCode(SP);
+ } else {
+ buffer.write(trailingBreaks);
+ }
+ leadingBreak = '';
+ trailingBreaks = '';
+ } else {
+ buffer.write(whitespace);
+ whitespace.clear();
+ }
+ }
+
+ // libyaml's notion of valid identifiers differs substantially from YAML
+ // 1.2's. We use [_isPlainChar] instead of libyaml's character here.
+ var startPosition = _scanner.position;
+ while (_isPlainChar) {
+ _scanner.readChar();
+ }
+ buffer.write(_scanner.substring(startPosition));
+
+ // Is it the end?
+ if (!_isBlank && !_isBreak) break;
+
+ while (_isBlank || _isBreak) {
+ if (_isBlank) {
+ // Check for a tab character messing up the intendation.
+ if (leadingBreak.isNotEmpty && _scanner.column < indent &&
+ _scanner.peekChar() == TAB) {
+ _scanner.error("Expected a space but found a tab.", length: 1);
+ }
+
+ if (leadingBreak.isEmpty) {
+ whitespace.writeCharCode(_scanner.readChar());
+ } else {
+ _scanner.readChar();
+ }
+ } else {
+ // Check if it's a first line break.
+ if (leadingBreak.isEmpty) {
+ leadingBreak = _readLine();
+ whitespace.clear();
+ } else {
+ trailingBreaks = _readLine();
+ }
+ }
+ }
+
+ // Check the indentation level.
+ if (_inBlockContext && _scanner.column < indent) break;
+ }
+
+ // Allow a simple key after a plain scalar with leading blanks.
+ if (leadingBreak.isNotEmpty) _simpleKeyAllowed = true;
+
+ return new ScalarToken(_scanner.spanFrom(start), buffer.toString(),
+ ScalarStyle.PLAIN);
+ }
+
+ /// Moves past the current line break, if there is one.
+ void _skipLine() {
+ var char = _scanner.peekChar();
+ if (char != CR && char != LF) return;
+ _scanner.readChar();
+ if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+ }
+
+ // Moves past the current line break and returns a newline.
+ String _readLine() {
+ var char = _scanner.peekChar();
+
+ // libyaml supports NEL, PS, and LS characters as line separators, but this
+ // is explicitly forbidden in section 5.4 of the YAML spec.
+ if (char != CR && char != LF) {
+ throw new YamlException("Expected newline.", _scanner.emptySpan);
+ }
+
+ _scanner.readChar();
+ // CR LF | CR | LF -> LF
+ if (char == CR && _scanner.peekChar() == LF) _scanner.readChar();
+ return "\n";
+ }
+
+ // Returns whether the character at [offset] is whitespace.
+ bool _isBlankAt(int offset) {
+ var char = _scanner.peekChar(offset);
+ return char == SP || char == TAB;
+ }
+
+ // Returns whether the character at [offset] is a line break.
+ bool _isBreakAt(int offset) {
+ // Libyaml considers NEL, LS, and PS to be line breaks as well, but that's
+ // contrary to the spec.
+ var char = _scanner.peekChar(offset);
+ return char == CR || char == LF;
+ }
+
+ // Returns whether the character at [offset] is whitespace or past the end of
+ // the source.
+ bool _isBlankOrEndAt(int offset) {
+ var char = _scanner.peekChar(offset);
+ return char == null || char == SP || char == TAB || char == CR ||
+ char == LF;
+ }
+
+ /// Returns whether the character at [offset] is a plain character.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#ns-plain-char(c).
+ bool _isPlainCharAt(int offset) {
+ switch (_scanner.peekChar(offset)) {
+ case COLON:
+ return _isPlainSafeAt(offset + 1);
+ case HASH:
+ var previous = _scanner.peekChar(offset - 1);
+ return previous != SP && previous != TAB;
+ default:
+ return _isPlainSafeAt(offset);
+ }
+ }
+
+ /// Returns whether the character at [offset] is a plain-safe character.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#ns-plain-safe(c).
+ bool _isPlainSafeAt(int offset) {
+ var char = _scanner.peekChar(offset);
+ switch (char) {
+ case COMMA:
+ case LEFT_SQUARE:
+ case RIGHT_SQUARE:
+ case LEFT_CURLY:
+ case RIGHT_CURLY:
+ // These characters are delimiters in a flow context and thus are only
+ // safe in a block context.
+ return _inBlockContext;
+ case SP:
+ case TAB:
+ case LF:
+ case CR:
+ case BOM:
+ return false;
+ case NEL:
+ return true;
+ default:
+ return char != null &&
+ ((char >= 0x00020 && char <= 0x00007E) ||
+ (char >= 0x000A0 && char <= 0x00D7FF) ||
+ (char >= 0x0E000 && char <= 0x00FFFD) ||
+ (char >= 0x10000 && char <= 0x10FFFF));
+ }
+ }
+
+ /// Returns the hexidecimal value of [char].
+ int _asHex(int char) {
+ if (char <= NUMBER_9) return char - NUMBER_0;
+ if (char <= LETTER_CAP_F) return 10 + char - LETTER_CAP_A;
+ return 10 + char - LETTER_A;
+ }
+
+ /// Moves the scanner past any blank characters.
+ void _skipBlanks() {
+ while (_isBlank) {
+ _scanner.readChar();
+ }
+ }
+
+ /// Moves the scanner past a comment, if one starts at the current position.
+ void _skipComment() {
+ if (_scanner.peekChar() != HASH) return;
+ while (!_isBreakOrEnd) {
+ _scanner.readChar();
+ }
+ }
+}
+
+/// A record of the location of a potential simple key.
+class _SimpleKey {
+ /// The index of the token that begins the simple key.
+ ///
+ /// This is the index relative to all tokens emitted, rather than relative to
+ /// [_tokens].
+ final int tokenNumber;
+
+ /// The source location of the beginning of the simple key.
+ ///
+ /// This is used for error reporting and for determining when a simple key is
+ /// no longer on the current line.
+ final SourceLocation location;
+
+ /// Whether this key must exist for the document to be scanned.
+ final bool required;
+
+ _SimpleKey(this.tokenNumber, this.location, {bool required})
+ : required = required;
+}
+
+/// An enum of chomping indicators that describe how to handle trailing
+/// whitespace for a block scalar.
+///
+/// See http://yaml.org/spec/1.2/spec.html#id2794534.
+class _Chomping {
+ /// All trailing whitespace is discarded.
+ static const STRIP = const _Chomping("STRIP");
+
+ /// A single trailing newline is retained.
+ static const CLIP = const _Chomping("CLIP");
+
+ /// All trailing whitespace is preserved.
+ static const KEEP = const _Chomping("KEEP");
+
+ final String name;
+
+ const _Chomping(this.name);
+
+ String toString() => name;
+}
diff --git a/lib/src/style.dart b/lib/src/style.dart
new file mode 100644
index 0000000..6305fce
--- /dev/null
+++ b/lib/src/style.dart
@@ -0,0 +1,73 @@
+// Copyright (c) 2014, 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 yaml.style;
+
+/// An enum of source scalar styles.
+class ScalarStyle {
+ /// No source style was specified.
+ ///
+ /// This usually indicates a scalar constructed with [YamlScalar.wrap].
+ static const ANY = const ScalarStyle._("ANY");
+
+ /// The plain scalar style, unquoted and without a prefix.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#style/flow/plain.
+ static const PLAIN = const ScalarStyle._("PLAIN");
+
+ /// The literal scalar style, with a `|` prefix.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#id2795688.
+ static const LITERAL = const ScalarStyle._("LITERAL");
+
+
+ /// The folded scalar style, with a `>` prefix.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#id2796251.
+ static const FOLDED = const ScalarStyle._("FOLDED");
+
+ /// The single-quoted scalar style.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#style/flow/single-quoted.
+ static const SINGLE_QUOTED = const ScalarStyle._("SINGLE_QUOTED");
+
+ /// The double-quoted scalar style.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#style/flow/double-quoted.
+ static const DOUBLE_QUOTED = const ScalarStyle._("DOUBLE_QUOTED");
+
+ final String name;
+
+ /// Whether this is a quoted style ([SINGLE_QUOTED] or [DOUBLE_QUOTED]).
+ bool get isQuoted => this == SINGLE_QUOTED || this == DOUBLE_QUOTED;
+
+ const ScalarStyle._(this.name);
+
+ String toString() => name;
+}
+
+/// An enum of collection styles.
+class CollectionStyle {
+ /// No source style was specified.
+ ///
+ /// This usually indicates a collection constructed with [YamlList.wrap] or
+ /// [YamlMap.wrap].
+ static const ANY = const CollectionStyle._("ANY");
+
+ /// The indentation-based block style.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#id2797293.
+ static const BLOCK = const CollectionStyle._("BLOCK");
+
+ /// The delimiter-based block style.
+ ///
+ /// See http://yaml.org/spec/1.2/spec.html#id2790088.
+ static const FLOW = const CollectionStyle._("FLOW");
+
+ final String name;
+
+ const CollectionStyle._(this.name);
+
+ String toString() => name;
+}
diff --git a/lib/src/token.dart b/lib/src/token.dart
new file mode 100644
index 0000000..20ae547
--- /dev/null
+++ b/lib/src/token.dart
@@ -0,0 +1,148 @@
+// Copyright (c) 2014, 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 yaml.token;
+
+import 'package:source_span/source_span.dart';
+
+import 'style.dart';
+
+/// A token emitted by a [Scanner].
+class Token {
+ /// The token type.
+ final TokenType type;
+
+ /// The span associated with the token.
+ final FileSpan span;
+
+ Token(this.type, this.span);
+
+ String toString() => type.toString();
+}
+
+/// A token representing a `%YAML` directive.
+class VersionDirectiveToken implements Token {
+ get type => TokenType.VERSION_DIRECTIVE;
+ final FileSpan span;
+
+ /// The declared major version of the document.
+ final int major;
+
+ /// The declared minor version of the document.
+ final int minor;
+
+ VersionDirectiveToken(this.span, this.major, this.minor);
+
+ String toString() => "VERSION_DIRECTIVE $major.$minor";
+}
+
+/// A token representing a `%TAG` directive.
+class TagDirectiveToken implements Token {
+ get type => TokenType.TAG_DIRECTIVE;
+ final FileSpan span;
+
+ /// The tag handle used in the document.
+ final String handle;
+
+ /// The tag prefix that the handle maps to.
+ final String prefix;
+
+ TagDirectiveToken(this.span, this.handle, this.prefix);
+
+ String toString() => "TAG_DIRECTIVE $handle $prefix";
+}
+
+/// A token representing an anchor (`&foo`).
+class AnchorToken implements Token {
+ get type => TokenType.ANCHOR;
+ final FileSpan span;
+
+ /// The name of the anchor.
+ final String name;
+
+ AnchorToken(this.span, this.name);
+
+ String toString() => "ANCHOR $name";
+}
+
+/// A token representing an alias (`*foo`).
+class AliasToken implements Token {
+ get type => TokenType.ALIAS;
+ final FileSpan span;
+
+ /// The name of the anchor.
+ final String name;
+
+ AliasToken(this.span, this.name);
+
+ String toString() => "ALIAS $name";
+}
+
+/// A token representing a tag (`!foo`).
+class TagToken implements Token {
+ get type => TokenType.TAG;
+ final FileSpan span;
+
+ /// The tag handle.
+ final String handle;
+
+ /// The tag suffix, or `null`.
+ final String suffix;
+
+ TagToken(this.span, this.handle, this.suffix);
+
+ String toString() => "TAG $handle $suffix";
+}
+
+/// A tkoen representing a scalar value.
+class ScalarToken implements Token {
+ get type => TokenType.SCALAR;
+ final FileSpan span;
+
+ /// The contents of the scalar.
+ final String value;
+
+ /// The style of the scalar in the original source.
+ final ScalarStyle style;
+
+ ScalarToken(this.span, this.value, this.style);
+
+ String toString() => "SCALAR $style \"$value\"";
+}
+
+/// An enum of types of [Token] object.
+class TokenType {
+ static const STREAM_START = const TokenType._("STREAM_START");
+ static const STREAM_END = const TokenType._("STREAM_END");
+
+ static const VERSION_DIRECTIVE = const TokenType._("VERSION_DIRECTIVE");
+ static const TAG_DIRECTIVE = const TokenType._("TAG_DIRECTIVE");
+ static const DOCUMENT_START = const TokenType._("DOCUMENT_START");
+ static const DOCUMENT_END = const TokenType._("DOCUMENT_END");
+
+ static const BLOCK_SEQUENCE_START = const TokenType._("BLOCK_SEQUENCE_START");
+ static const BLOCK_MAPPING_START = const TokenType._("BLOCK_MAPPING_START");
+ static const BLOCK_END = const TokenType._("BLOCK_END");
+
+ static const FLOW_SEQUENCE_START = const TokenType._("FLOW_SEQUENCE_START");
+ static const FLOW_SEQUENCE_END = const TokenType._("FLOW_SEQUENCE_END");
+ static const FLOW_MAPPING_START = const TokenType._("FLOW_MAPPING_START");
+ static const FLOW_MAPPING_END = const TokenType._("FLOW_MAPPING_END");
+
+ static const BLOCK_ENTRY = const TokenType._("BLOCK_ENTRY");
+ static const FLOW_ENTRY = const TokenType._("FLOW_ENTRY");
+ static const KEY = const TokenType._("KEY");
+ static const VALUE = const TokenType._("VALUE");
+
+ static const ALIAS = const TokenType._("ALIAS");
+ static const ANCHOR = const TokenType._("ANCHOR");
+ static const TAG = const TokenType._("TAG");
+ static const SCALAR = const TokenType._("SCALAR");
+
+ final String name;
+
+ const TokenType._(this.name);
+
+ String toString() => name;
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 84c1113..445221f 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -4,6 +4,8 @@
library yaml.utils;
+import 'package:source_span/source_span.dart';
+
/// A pair of values.
class Pair<E, F> {
final E first;
@@ -13,3 +15,30 @@
String toString() => '($first, $last)';
}
+
+/// Print a warning.
+///
+/// If [span] is passed, associates the warning with that span.
+void warn(String message, [SourceSpan span]) =>
+ yamlWarningCallback(message, span);
+
+/// A callback for emitting a warning.
+///
+/// [message] is the text of the warning. If [span] is passed, it's the portion
+/// of the document that the warning is associated with and should be included
+/// in the printed warning.
+typedef YamlWarningCallback(String message, [SourceSpan span]);
+
+/// A callback for emitting a warning.
+///
+/// In a very few cases, the YAML spec indicates that an implementation should
+/// emit a warning. To do so, it calls this callback. The default implementation
+/// prints a message using [print].
+YamlWarningCallback yamlWarningCallback = (message, [span]) {
+ // TODO(nweiz): Print to stderr with color when issue 6943 is fixed and
+ // dart:io is available.
+ if (span != null) message = span.message(message);
+ print(message);
+};
+
+
diff --git a/lib/src/visitor.dart b/lib/src/visitor.dart
deleted file mode 100644
index 3f4c455..0000000
--- a/lib/src/visitor.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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 yaml.visitor;
-
-import 'equality.dart';
-import 'model.dart';
-
-/// The visitor pattern for YAML documents.
-class Visitor {
- /// Returns [alias].
- visitAlias(AliasNode alias) => alias;
-
- /// Returns [scalar].
- visitScalar(ScalarNode scalar) => scalar;
-
- /// Visits each node in [seq] and returns a list of the results.
- visitSequence(SequenceNode seq)
- => seq.content.map((e) => e.visit(this)).toList();
-
- /// Visits each key and value in [map] and returns a map of the results.
- visitMapping(MappingNode map) {
- var out = deepEqualsMap();
- for (var key in map.content.keys) {
- out[key.visit(this)] = map.content[key].visit(this);
- }
- return out;
- }
-}
diff --git a/lib/src/yaml_document.dart b/lib/src/yaml_document.dart
new file mode 100644
index 0000000..4c249a0
--- /dev/null
+++ b/lib/src/yaml_document.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2014, 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 yaml.yaml_document;
+
+import 'dart:collection';
+
+import 'package:source_span/source_span.dart';
+
+import 'yaml_node.dart';
+
+/// A YAML document, complete with metadata.
+class YamlDocument {
+ /// The contents of the document.
+ final YamlNode contents;
+
+ /// The span covering the entire document.
+ final SourceSpan span;
+
+ /// The version directive for the document, if any.
+ final VersionDirective versionDirective;
+
+ /// The tag directives for the document.
+ final List<TagDirective> tagDirectives;
+
+ /// Whether the beginning of the document was implicit (versus explicit via
+ /// `===`).
+ final bool startImplicit;
+
+ /// Whether the end of the document was implicit (versus explicit via `...`).
+ final bool endImplicit;
+
+ /// Users of the library should not use this constructor.
+ YamlDocument.internal(this.contents, this.span, this.versionDirective,
+ List<TagDirective> tagDirectives, {this.startImplicit: false,
+ this.endImplicit: false})
+ : tagDirectives = new UnmodifiableListView(tagDirectives);
+
+ String toString() => contents.toString();
+}
+
+/// A directive indicating which version of YAML a document was written to.
+class VersionDirective {
+ /// The major version number.
+ final int major;
+
+ /// The minor version number.
+ final int minor;
+
+ VersionDirective(this.major, this.minor);
+
+ String toString() => "%YAML $major.$minor";
+}
+
+/// A directive describing a custom tag handle.
+class TagDirective {
+ /// The handle for use in the document.
+ final String handle;
+
+ /// The prefix that the handle maps to.
+ final String prefix;
+
+ TagDirective(this.handle, this.prefix);
+
+ String toString() => "%TAG $handle $prefix";
+}
diff --git a/lib/src/yaml_node.dart b/lib/src/yaml_node.dart
index 6320156..027bc1b 100644
--- a/lib/src/yaml_node.dart
+++ b/lib/src/yaml_node.dart
@@ -10,6 +10,7 @@
import 'package:source_span/source_span.dart';
import 'null_span.dart';
+import 'style.dart';
import 'yaml_node_wrapper.dart';
/// An interface for parsed nodes from a YAML source tree.
@@ -26,7 +27,9 @@
///
/// [SourceSpan.message] can be used to produce a human-friendly message about
/// this node.
- SourceSpan get span;
+ SourceSpan get span => _span;
+
+ SourceSpan _span;
/// The inner value of this node.
///
@@ -38,8 +41,6 @@
/// A read-only [Map] parsed from YAML.
class YamlMap extends YamlNode with collection.MapMixin, UnmodifiableMapMixin {
- final SourceSpan span;
-
/// A view of [this] where the keys and values are guaranteed to be
/// [YamlNode]s.
///
@@ -50,6 +51,9 @@
/// `dynamic` `map.nodes["foo"]` will still work.
final Map<dynamic, YamlNode> nodes;
+ /// The style used for the map in the original document.
+ final CollectionStyle style;
+
Map get value => this;
Iterable get keys => nodes.keys.map((node) => node.value);
@@ -77,8 +81,10 @@
new YamlMapWrapper(dartMap, sourceUrl);
/// Users of the library should not use this constructor.
- YamlMap.internal(Map<dynamic, YamlNode> nodes, this.span)
- : nodes = new UnmodifiableMapView<dynamic, YamlNode>(nodes);
+ YamlMap.internal(Map<dynamic, YamlNode> nodes, SourceSpan span, this.style)
+ : nodes = new UnmodifiableMapView<dynamic, YamlNode>(nodes) {
+ _span = span;
+ }
operator [](key) {
var node = nodes[key];
@@ -89,10 +95,11 @@
// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
/// A read-only [List] parsed from YAML.
class YamlList extends YamlNode with collection.ListMixin {
- final SourceSpan span;
-
final List<YamlNode> nodes;
+ /// The style used for the list in the original document.
+ final CollectionStyle style;
+
List get value => this;
int get length => nodes.length;
@@ -124,8 +131,10 @@
new YamlListWrapper(dartList, sourceUrl);
/// Users of the library should not use this constructor.
- YamlList.internal(List<YamlNode> nodes, this.span)
- : nodes = new UnmodifiableListView<YamlNode>(nodes);
+ YamlList.internal(List<YamlNode> nodes, SourceSpan span, this.style)
+ : nodes = new UnmodifiableListView<YamlNode>(nodes) {
+ _span = span;
+ }
operator [](int index) => nodes[index].value;
@@ -136,10 +145,11 @@
/// A wrapped scalar value parsed from YAML.
class YamlScalar extends YamlNode {
- final SourceSpan span;
-
final value;
+ /// The style used for the scalar in the original document.
+ final ScalarStyle style;
+
/// Wraps a Dart value in a [YamlScalar].
///
/// This scalar's [span] won't have useful location information. However, it
@@ -148,10 +158,21 @@
///
/// [sourceUrl] may be either a [String], a [Uri], or `null`.
YamlScalar.wrap(this.value, {sourceUrl})
- : span = new NullSpan(sourceUrl);
+ : style = ScalarStyle.ANY {
+ _span = new NullSpan(sourceUrl);
+ }
/// Users of the library should not use this constructor.
- YamlScalar.internal(this.value, this.span);
+ YamlScalar.internal(this.value, SourceSpan span, this.style) {
+ _span = span;
+ }
String toString() => value.toString();
}
+
+/// Sets the source span of a [YamlNode].
+///
+/// This method is not exposed publicly.
+void setSpan(YamlNode node, SourceSpan span) {
+ node._span = span;
+}
diff --git a/lib/src/yaml_node_wrapper.dart b/lib/src/yaml_node_wrapper.dart
index d000dcb..be96ba4 100644
--- a/lib/src/yaml_node_wrapper.dart
+++ b/lib/src/yaml_node_wrapper.dart
@@ -10,12 +10,15 @@
import 'package:source_span/source_span.dart';
import 'null_span.dart';
+import 'style.dart';
import 'yaml_node.dart';
/// A wrapper that makes a normal Dart map behave like a [YamlMap].
class YamlMapWrapper extends MapBase
with pkg_collection.UnmodifiableMapMixin
implements YamlMap {
+ final CollectionStyle style = CollectionStyle.ANY;
+
final Map _dartMap;
final SourceSpan span;
@@ -55,8 +58,8 @@
final SourceSpan _span;
- Iterable get keys =>
- _dartMap.keys.map((key) => new YamlScalar.internal(key, _span));
+ Iterable get keys => _dartMap.keys.map((key) =>
+ new YamlScalar.internal(key, _span, ScalarStyle.ANY));
_YamlMapNodes(this._dartMap, this._span);
@@ -76,6 +79,8 @@
// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
/// A wrapper that makes a normal Dart list behave like a [YamlList].
class YamlListWrapper extends ListBase implements YamlList {
+ final CollectionStyle style = CollectionStyle.ANY;
+
final List _dartList;
final SourceSpan span;
@@ -146,5 +151,5 @@
YamlNode _nodeForValue(value, SourceSpan span) {
if (value is Map) return new YamlMapWrapper._(value, span);
if (value is List) return new YamlListWrapper._(value, span);
- return new YamlScalar.internal(value, span);
+ return new YamlScalar.internal(value, span, ScalarStyle.ANY);
}
diff --git a/lib/yaml.dart b/lib/yaml.dart
index e45dd6e..9af329a 100644
--- a/lib/yaml.dart
+++ b/lib/yaml.dart
@@ -4,16 +4,17 @@
library yaml;
-import 'package:string_scanner/string_scanner.dart';
-
-import 'src/composer.dart';
-import 'src/constructor.dart';
-import 'src/parser.dart';
+import 'src/loader.dart';
+import 'src/style.dart';
+import 'src/yaml_document.dart';
import 'src/yaml_exception.dart';
import 'src/yaml_node.dart';
+export 'src/style.dart';
+export 'src/utils.dart' show YamlWarningCallback, yamlWarningCallback;
+export 'src/yaml_document.dart';
export 'src/yaml_exception.dart';
-export 'src/yaml_node.dart';
+export 'src/yaml_node.dart' hide setSpan;
/// Loads a single document from a YAML string.
///
@@ -39,13 +40,29 @@
/// This is just like [loadYaml], except that where [loadYaml] would return a
/// normal Dart value this returns a [YamlNode] instead. This allows the caller
/// to be confident that the return value will always be a [YamlNode].
-YamlNode loadYamlNode(String yaml, {sourceUrl}) {
- var stream = loadYamlStream(yaml, sourceUrl: sourceUrl);
- if (stream.length != 1) {
- throw new YamlException("Expected 1 document, were ${stream.length}.",
- stream.span);
+YamlNode loadYamlNode(String yaml, {sourceUrl}) =>
+ loadYamlDocument(yaml, sourceUrl: sourceUrl).contents;
+
+/// Loads a single document from a YAML string as a [YamlDocument].
+///
+/// This is just like [loadYaml], except that where [loadYaml] would return a
+/// normal Dart value this returns a [YamlDocument] instead. This allows the
+/// caller to access document metadata.
+YamlDocument loadYamlDocument(String yaml, {sourceUrl}) {
+ var loader = new Loader(yaml, sourceUrl: sourceUrl);
+ var document = loader.load();
+ if (document == null) {
+ return new YamlDocument.internal(
+ new YamlScalar.internal(null, loader.span, ScalarStyle.ANY),
+ loader.span, null, const []);
}
- return stream.nodes[0];
+
+ var nextDocument = loader.load();
+ if (nextDocument != null) {
+ throw new YamlException("Only expected one document.", nextDocument.span);
+ }
+
+ return document;
}
/// Loads a stream of documents from a YAML string.
@@ -62,15 +79,34 @@
/// If [sourceUrl] is passed, it's used as the URL from which the YAML
/// originated for error reporting. It can be a [String], a [Uri], or `null`.
YamlList loadYamlStream(String yaml, {sourceUrl}) {
- var pair;
- try {
- pair = new Parser(yaml, sourceUrl).l_yamlStream();
- } on StringScannerException catch (error) {
- throw new YamlException(error.message, error.span);
+ var loader = new Loader(yaml, sourceUrl: sourceUrl);
+
+ var documents = [];
+ var document = loader.load();
+ while (document != null) {
+ documents.add(document);
+ document = loader.load();
}
- var nodes = pair.first
- .map((doc) => new Constructor(new Composer(doc).compose()).construct())
- .toList();
- return new YamlList.internal(nodes, pair.last);
+ return new YamlList.internal(
+ documents.map((document) => document.contents).toList(),
+ loader.span,
+ CollectionStyle.ANY);
+}
+
+/// Loads a stream of documents from a YAML string.
+///
+/// This is like [loadYamlStream], except that it returns [YamlDocument]s with
+/// metadata wrapping the document contents.
+List<YamlDocument> loadYamlDocuments(String yaml, {sourceUrl}) {
+ var loader = new Loader(yaml, sourceUrl: sourceUrl);
+
+ var documents = [];
+ var document = loader.load();
+ while (document != null) {
+ documents.add(document);
+ document = loader.load();
+ }
+
+ return documents;
}
diff --git a/libyaml-license.txt b/libyaml-license.txt
new file mode 100644
index 0000000..050ced2
--- /dev/null
+++ b/libyaml-license.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2006 Kirill Simonov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pubspec.yaml b/pubspec.yaml
index 74194e5..d0d51c5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,12 +1,12 @@
name: yaml
-version: 2.0.1+1
+version: 2.1.0
author: "Dart Team <misc@dartlang.org>"
homepage: http://www.dartlang.org
description: A parser for YAML.
dependencies:
- collection: ">=0.9.2 <2.0.0"
+ collection: ">=1.1.0 <2.0.0"
path: ">=1.2.0 <2.0.0"
- string_scanner: ">=0.1.0 <0.2.0"
+ string_scanner: ">=0.1.2 <0.2.0"
source_span: ">=1.0.0 <2.0.0"
dev_dependencies:
unittest: ">=0.9.0 <0.12.0"
diff --git a/test/yaml_test.dart b/test/yaml_test.dart
index 4fbb108..2d9a0e8 100644
--- a/test/yaml_test.dart
+++ b/test/yaml_test.dart
@@ -4,7 +4,6 @@
library yaml.test;
-// TODO(rnystrom): rewrite tests so that they don't need "Expect".
import 'package:unittest/unittest.dart';
import 'package:yaml/yaml.dart';
@@ -16,7 +15,7 @@
group('has a friendly error message for', () {
var tabError = predicate((e) =>
- e.toString().contains('tab characters are not allowed as indentation'));
+ e.toString().contains('Tab characters are not allowed as indentation'));
test('using a tab as indentation', () {
expect(() => loadYaml('foo:\n\tbar'),
@@ -32,6 +31,29 @@
});
});
+ group("refuses documents that declare version", () {
+ test("1.0", () {
+ expectYamlFails("""
+ %YAML 1.0
+ --- text
+ """);
+ });
+
+ test("1.3", () {
+ expectYamlFails("""
+ %YAML 1.3
+ --- text
+ """);
+ });
+
+ test("2.0", () {
+ expectYamlFails("""
+ %YAML 2.0
+ --- text
+ """);
+ });
+ });
+
// The following tests are all taken directly from the YAML spec
// (http://www.yaml.org/spec/1.2/spec.html). Most of them are code examples
// that are directly included in the spec, but additional tests are derived
@@ -173,21 +195,21 @@
- Ken Griffey""");
});
- // test('[Example 2.10]', () {
- // expectYamlLoads({
- // "hr": ["Mark McGwire", "Sammy Sosa"],
- // "rbi": ["Sammy Sosa", "Ken Griffey"]
- // },
- // """
- // ---
- // hr:
- // - Mark McGwire
- // # Following node labeled SS
- // - &SS Sammy Sosa
- // rbi:
- // - *SS # Subsequent occurrence
- // - Ken Griffey""");
- // });
+ test('[Example 2.10]', () {
+ expectYamlLoads({
+ "hr": ["Mark McGwire", "Sammy Sosa"],
+ "rbi": ["Sammy Sosa", "Ken Griffey"]
+ },
+ """
+ ---
+ hr:
+ - Mark McGwire
+ # Following node labeled SS
+ - &SS Sammy Sosa
+ rbi:
+ - *SS # Subsequent occurrence
+ - Ken Griffey""");
+ });
test('[Example 2.11]', () {
var doc = deepEqualsMap();
@@ -536,12 +558,12 @@
""");
});
- // test('[Example 5.9]', () {
- // expectYamlLoads("text",
- // """
- // %YAML 1.2
- // --- text""");
- // });
+ test('[Example 5.9]', () {
+ expectYamlLoads("text",
+ """
+ %YAML 1.2
+ --- text""");
+ });
test('[Example 5.10]', () {
expectYamlFails("commercial-at: @text");
@@ -886,179 +908,179 @@
});
group('6.8: Directives', () {
- // // TODO(nweiz): assert that this produces a warning
- // test('[Example 6.13]', () {
- // expectYamlLoads("foo",
- // '''
- // %FOO bar baz # Should be ignored
- // # with a warning.
- // --- "foo"''');
- // });
+ // TODO(nweiz): assert that this produces a warning
+ test('[Example 6.13]', () {
+ expectYamlLoads("foo",
+ '''
+ %FOO bar baz # Should be ignored
+ # with a warning.
+ --- "foo"''');
+ });
- // // TODO(nweiz): assert that this produces a warning
- // test('[Example 6.14]', () {
- // expectYamlLoads("foo",
- // '''
- // %YAML 1.3 # Attempt parsing
- // # with a warning
- // ---
- // "foo"''');
- // });
+ // TODO(nweiz): assert that this produces a warning.
+ test('[Example 6.14]', () {
+ expectYamlLoads("foo",
+ '''
+ %YAML 1.3 # Attempt parsing
+ # with a warning
+ ---
+ "foo"''');
+ });
- // test('[Example 6.15]', () {
- // expectYamlFails(
- // """
- // %YAML 1.2
- // %YAML 1.1
- // foo""");
- // });
+ test('[Example 6.15]', () {
+ expectYamlFails(
+ """
+ %YAML 1.2
+ %YAML 1.1
+ foo""");
+ });
- // test('[Example 6.16]', () {
- // expectYamlLoads("foo",
- // '''
- // %TAG !yaml! tag:yaml.org,2002:
- // ---
- // !yaml!str "foo"''');
- // });
+ test('[Example 6.16]', () {
+ expectYamlLoads("foo",
+ '''
+ %TAG !yaml! tag:yaml.org,2002:
+ ---
+ !yaml!str "foo"''');
+ });
- // test('[Example 6.17]', () {
- // ExpectYamlFails(
- // """
- // %TAG ! !foo
- // %TAG ! !foo
- // bar""");
- // });
+ test('[Example 6.17]', () {
+ expectYamlFails(
+ """
+ %TAG ! !foo
+ %TAG ! !foo
+ bar""");
+ });
// Examples 6.18 through 6.22 test custom tag URIs, which this
// implementation currently doesn't plan to support.
});
group('6.9: Node Properties', () {
- // test('may be specified in any order', () {
- // expectYamlLoads(["foo", "bar"],
- // """
- // - !!str &a1 foo
- // - &a2 !!str bar""");
- // });
+ test('may be specified in any order', () {
+ expectYamlLoads(["foo", "bar"],
+ """
+ - !!str &a1 foo
+ - &a2 !!str bar""");
+ });
- // test('[Example 6.23]', () {
- // expectYamlLoads({
- // "foo": "bar",
- // "baz": "foo"
- // },
- // '''
- // !!str &a1 "foo":
- // !!str bar
- // &a2 baz : *a1''');
- // });
+ test('[Example 6.23]', () {
+ expectYamlLoads({
+ "foo": "bar",
+ "baz": "foo"
+ },
+ '''
+ !!str &a1 "foo":
+ !!str bar
+ &a2 baz : *a1''');
+ });
- // // Example 6.24 tests custom tag URIs, which this implementation currently
- // // doesn't plan to support.
+ // Example 6.24 tests custom tag URIs, which this implementation currently
+ // doesn't plan to support.
- // test('[Example 6.25]', () {
- // expectYamlFails("- !<!> foo");
- // expectYamlFails("- !<\$:?> foo");
- // });
+ test('[Example 6.25]', () {
+ expectYamlFails("- !<!> foo");
+ expectYamlFails("- !<\$:?> foo");
+ });
- // // Examples 6.26 and 6.27 test custom tag URIs, which this implementation
- // // currently doesn't plan to support.
+ // Examples 6.26 and 6.27 test custom tag URIs, which this implementation
+ // currently doesn't plan to support.
- // test('[Example 6.28]', () {
- // expectYamlLoads(["12", 12, "12"],
- // '''
- // # Assuming conventional resolution:
- // - "12"
- // - 12
- // - ! 12''');
- // });
+ test('[Example 6.28]', () {
+ expectYamlLoads(["12", 12, "12"],
+ '''
+ # Assuming conventional resolution:
+ - "12"
+ - 12
+ - ! 12''');
+ });
- // test('[Example 6.29]', () {
- // expectYamlLoads({
- // "First occurrence": "Value",
- // "Second occurrence": "anchor"
- // },
- // """
- // First occurrence: &anchor Value
- // Second occurrence: *anchor""");
- // });
+ test('[Example 6.29]', () {
+ expectYamlLoads({
+ "First occurrence": "Value",
+ "Second occurrence": "Value"
+ },
+ """
+ First occurrence: &anchor Value
+ Second occurrence: *anchor""");
+ });
});
// Chapter 7: Flow Styles
group('7.1: Alias Nodes', () {
- // test("must not use an anchor that doesn't previously occur", () {
- // expectYamlFails(
- // """
- // - *anchor
- // - &anchor foo""");
- // });
+ test("must not use an anchor that doesn't previously occur", () {
+ expectYamlFails(
+ """
+ - *anchor
+ - &anchor foo""");
+ });
- // test("don't have to exist for a given anchor node", () {
- // expectYamlLoads(["foo"], "- &anchor foo");
- // });
+ test("don't have to exist for a given anchor node", () {
+ expectYamlLoads(["foo"], "- &anchor foo");
+ });
- // group('must not specify', () {
- // test('tag properties', () => expectYamlFails(
- // """
- // - &anchor foo
- // - !str *anchor""");
+ group('must not specify', () {
+ test('tag properties', () => expectYamlFails(
+ """
+ - &anchor foo
+ - !str *anchor"""));
- // test('anchor properties', () => expectYamlFails(
- // """
- // - &anchor foo
- // - &anchor2 *anchor""");
+ test('anchor properties', () => expectYamlFails(
+ """
+ - &anchor foo
+ - &anchor2 *anchor"""));
- // test('content', () => expectYamlFails(
- // """
- // - &anchor foo
- // - *anchor bar""")));
- // });
+ test('content', () => expectYamlFails(
+ """
+ - &anchor foo
+ - *anchor bar"""));
+ });
- // test('must preserve structural equality', () {
- // var doc = loadYaml(cleanUpLiteral(
- // """
- // anchor: &anchor [a, b, c]
- // alias: *anchor""");
- // var anchorList = doc['anchor'];
- // var aliasList = doc['alias'];
- // expect(anchorList, same(aliasList));
+ test('must preserve structural equality', () {
+ var doc = loadYaml(cleanUpLiteral(
+ """
+ anchor: &anchor [a, b, c]
+ alias: *anchor"""));
+ var anchorList = doc['anchor'];
+ var aliasList = doc['alias'];
+ expect(anchorList, same(aliasList));
- // doc = loadYaml(cleanUpLiteral(
- // """
- // ? &anchor [a, b, c]
- // : ? *anchor
- // : bar""");
- // anchorList = doc.keys[0];
- // aliasList = doc[['a', 'b', 'c']].keys[0];
- // expect(anchorList, same(aliasList));
- // });
+ doc = loadYaml(cleanUpLiteral(
+ """
+ ? &anchor [a, b, c]
+ : ? *anchor
+ : bar"""));
+ anchorList = doc.keys.first;
+ aliasList = doc[['a', 'b', 'c']].keys.first;
+ expect(anchorList, same(aliasList));
+ });
- // test('[Example 7.1]', () {
- // expectYamlLoads({
- // "First occurence": "Foo",
- // "Second occurence": "Foo",
- // "Override anchor": "Bar",
- // "Reuse anchor": "Bar",
- // },
- // """
- // First occurrence: &anchor Foo
- // Second occurrence: *anchor
- // Override anchor: &anchor Bar
- // Reuse anchor: *anchor""");
- // });
+ test('[Example 7.1]', () {
+ expectYamlLoads({
+ "First occurrence": "Foo",
+ "Second occurrence": "Foo",
+ "Override anchor": "Bar",
+ "Reuse anchor": "Bar",
+ },
+ """
+ First occurrence: &anchor Foo
+ Second occurrence: *anchor
+ Override anchor: &anchor Bar
+ Reuse anchor: *anchor""");
+ });
});
group('7.2: Empty Nodes', () {
- // test('[Example 7.2]', () {
- // expectYamlLoads({
- // "foo": "",
- // "": "bar"
- // },
- // """
- // {
- // foo : !!str,
- // !!str : bar,
- // }""");
- // });
+ test('[Example 7.2]', () {
+ expectYamlLoads({
+ "foo": "",
+ "": "bar"
+ },
+ """
+ {
+ foo : !!str,
+ !!str : bar,
+ }""");
+ });
test('[Example 7.3]', () {
var doc = deepEqualsMap({"foo": null});
@@ -1284,17 +1306,18 @@
- [ {JSON: like}:adjacent ]""");
});
- test('[Example 7.22]', () {
- expectYamlFails(
- """
- [ foo
- bar: invalid ]""");
-
- // TODO(nweiz): enable this when we throw an error for long keys
- // var dotList = new List.filled(1024, ' ');
- // var dots = dotList.join();
- // expectYamlFails('[ "foo...$dots...bar": invalid ]');
- });
+ // TODO(nweiz): enable this when we throw an error for long or multiline
+ // keys.
+ // test('[Example 7.22]', () {
+ // expectYamlFails(
+ // """
+ // [ foo
+ // bar: invalid ]""");
+ //
+ // var dotList = new List.filled(1024, ' ');
+ // var dots = dotList.join();
+ // expectYamlFails('[ "foo...$dots...bar": invalid ]');
+ // });
});
group('7.5: Flow Nodes', () {
@@ -1308,15 +1331,15 @@
- c""");
});
- // test('[Example 7.24]', () {
- // expectYamlLoads(["a", "b", "c", "c", ""],
- // """
- // - !!str "a"
- // - 'b'
- // - &anchor "c"
- // - *anchor
- // - !!str""");
- // });
+ test('[Example 7.24]', () {
+ expectYamlLoads(["a", "b", "c", "c", ""],
+ """
+ - !!str "a"
+ - 'b'
+ - &anchor "c"
+ - *anchor
+ - !!str""");
+ });
});
// Chapter 8: Block Styles
@@ -1569,41 +1592,43 @@
: moon: white""");
});
- // test('[Example 8.20]', () {
- // expectYamlLoads(["flow in block", "Block scalar\n", {"foo": "bar"}],
- // '''
- // -
- // "flow in block"
- // - >
- // Block scalar
- // - !!map # Block collection
- // foo : bar''');
- // });
+ test('[Example 8.20]', () {
+ expectYamlLoads(["flow in block", "Block scalar\n", {"foo": "bar"}],
+ '''
+ -
+ "flow in block"
+ - >
+ Block scalar
+ - !!map # Block collection
+ foo : bar''');
+ });
- // test('[Example 8.21]', () {
- // expectYamlLoads({"literal": "value", "folded": "value"},
- // """
- // literal: |2
- // value
- // folded:
- // !!str
- // >1
- // value""");
- // });
+ test('[Example 8.21]', () {
+ // The spec doesn't include a newline after "value" in the parsed map, but
+ // the block scalar is clipped so it should be retained.
+ expectYamlLoads({"literal": "value\n", "folded": "value"},
+ """
+ literal: |2
+ value
+ folded:
+ !!str
+ >1
+ value""");
+ });
- // test('[Example 8.22]', () {
- // expectYamlLoads({
- // "sequence": ["entry", ["nested"]],
- // "mapping": {"foo": "bar"}
- // },
- // """
- // sequence: !!seq
- // - entry
- // - !!seq
- // - nested
- // mapping: !!map
- // foo: bar""");
- // });
+ test('[Example 8.22]', () {
+ expectYamlLoads({
+ "sequence": ["entry", ["nested"]],
+ "mapping": {"foo": "bar"}
+ },
+ """
+ sequence: !!seq
+ - entry
+ - !!seq
+ - nested
+ mapping: !!map
+ foo: bar""");
+ });
});
// Chapter 9: YAML Character Stream
@@ -1611,14 +1636,14 @@
// Example 9.1 tests the use of a BOM, which this implementation currently
// doesn't plan to support.
- // test('[Example 9.2]', () {
- // expectYamlLoads("Document",
- // """
- // %YAML 1.2
- // ---
- // Document
- // ... # Suffix""");
- // });
+ test('[Example 9.2]', () {
+ expectYamlLoads("Document",
+ """
+ %YAML 1.2
+ ---
+ Document
+ ... # Suffix""");
+ });
test('[Example 9.3]', () {
// The spec example indicates that the comment after "%!PS-Adobe-2.0"
@@ -1653,81 +1678,83 @@
...""");
});
- // test('[Example 9.5]', () {
- // expectYamlStreamLoads(["%!PS-Adobe-2.0\n", null],
- // """
- // %YAML 1.2
- // --- |
- // %!PS-Adobe-2.0
- // ...
- // %YAML1.2
- // ---
- // # Empty
- // ...""");
- // });
+ test('[Example 9.5]', () {
+ // The spec doesn't have a space between the second
+ // "YAML" and "1.2", but this seems to be a typo.
+ expectYamlStreamLoads(["%!PS-Adobe-2.0\n", null],
+ """
+ %YAML 1.2
+ --- |
+ %!PS-Adobe-2.0
+ ...
+ %YAML 1.2
+ ---
+ # Empty
+ ...""");
+ });
- // test('[Example 9.6]', () {
- // expectYamlStreamLoads(["Document", null, {"matches %": 20}],
- // """
- // Document
- // ---
- // # Empty
- // ...
- // %YAML 1.2
- // ---
- // matches %: 20""");
- // });
+ test('[Example 9.6]', () {
+ expectYamlStreamLoads(["Document", null, {"matches %": 20}],
+ """
+ Document
+ ---
+ # Empty
+ ...
+ %YAML 1.2
+ ---
+ matches %: 20""");
+ });
});
// Chapter 10: Recommended Schemas
group('10.1: Failsafe Schema', () {
- // test('[Example 10.1]', () {
- // expectYamlStreamLoads({
- // "Block style": {
- // "Clark": "Evans",
- // "Ingy": "döt Net",
- // "Oren": "Ben-Kiki"
- // },
- // "Flow style": {
- // "Clark": "Evans",
- // "Ingy": "döt Net",
- // "Oren": "Ben-Kiki"
- // }
- // },
- // """
- // Block style: !!map
- // Clark : Evans
- // Ingy : döt Net
- // Oren : Ben-Kiki
+ test('[Example 10.1]', () {
+ expectYamlLoads({
+ "Block style": {
+ "Clark": "Evans",
+ "Ingy": "döt Net",
+ "Oren": "Ben-Kiki"
+ },
+ "Flow style": {
+ "Clark": "Evans",
+ "Ingy": "döt Net",
+ "Oren": "Ben-Kiki"
+ }
+ },
+ """
+ Block style: !!map
+ Clark : Evans
+ Ingy : döt Net
+ Oren : Ben-Kiki
- // Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }""");
- // });
+ Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki }""");
+ });
- // test('[Example 10.2]', () {
- // expectYamlStreamLoads({
- // "Block style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"],
- // "Flow style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"]
- // },
- // """
- // Block style: !!seq
- // - Clark Evans
- // - Ingy döt Net
- // - Oren Ben-Kiki
+ test('[Example 10.2]', () {
+ expectYamlLoads({
+ "Block style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"],
+ "Flow style": ["Clark Evans", "Ingy döt Net", "Oren Ben-Kiki"]
+ },
+ """
+ Block style: !!seq
+ - Clark Evans
+ - Ingy döt Net
+ - Oren Ben-Kiki
- // Flow style: !!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ]""");
- // });
+ Flow style: !!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ]""");
+ });
- // test('[Example 10.3]', () {
- // expectYamlStreamLoads({
- // "Block style": "String: just a theory.",
- // "Flow style": "String: just a theory."
- // },
- // '''
- // Block style: !!str |-
- // String: just a theory.
+ test('[Example 10.3]', () {
+ expectYamlLoads({
+ "Block style": "String: just a theory.",
+ "Flow style": "String: just a theory."
+ },
+ '''
+ Block style: !!str |-
+ String: just a theory.
- // Flow style: !!str "String: just a theory."''');
- // });
+ Flow style: !!str "String: just a theory."''');
+ });
});
group('10.2: JSON Schema', () {