| // Copyright (c) 2014, the Dart project authors. | |
| // Copyright (c) 2006, Kirill Simonov. | |
| // | |
| // Use of this source code is governed by an MIT-style | |
| // license that can be found in the LICENSE file or at | |
| // https://opensource.org/licenses/MIT. | |
| import 'package:source_span/source_span.dart'; | |
| import 'charcodes.dart'; | |
| import 'equality.dart'; | |
| import 'error_listener.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 = <String, YamlNode>{}; | |
| /// Anchors that are currently being resolved. | |
| final _activeAnchors = <String>{}; | |
| /// The span of the entire stream emitted so far. | |
| FileSpan get span => _span; | |
| FileSpan _span; | |
| /// Creates a loader that loads [source]. | |
| factory Loader(String source, | |
| {Uri? sourceUrl, bool recover = false, ErrorListener? errorListener}) { | |
| var parser = Parser(source, | |
| sourceUrl: sourceUrl, recover: recover, errorListener: errorListener); | |
| var event = parser.parse(); | |
| assert(event.type == EventType.streamStart); | |
| return Loader._(parser, event.span); | |
| } | |
| Loader._(this._parser, this._span); | |
| /// 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.streamEnd) { | |
| _span = _span.expand(event.span); | |
| return null; | |
| } | |
| var document = _loadDocument(event as DocumentStartEvent); | |
| _span = _span.expand(document.span as FileSpan); | |
| _aliases.clear(); | |
| return document; | |
| } | |
| /// Composes a document object. | |
| YamlDocument _loadDocument(DocumentStartEvent firstEvent) { | |
| var contents = _loadNode(_parser.parse()); | |
| var lastEvent = _parser.parse() as DocumentEndEvent; | |
| assert(lastEvent.type == EventType.documentEnd); | |
| return 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) { | |
| EventType.alias => _loadAlias(firstEvent as AliasEvent), | |
| EventType.scalar => _loadScalar(firstEvent as ScalarEvent), | |
| EventType.sequenceStart => | |
| _loadSequence(firstEvent as SequenceStartEvent), | |
| EventType.mappingStart => _loadMapping(firstEvent as MappingStartEvent), | |
| _ => throw StateError('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) { | |
| if (_activeAnchors.contains(event.name)) { | |
| throw YamlException( | |
| 'Self-referential collections are not supported.', event.span); | |
| } | |
| return alias; | |
| } | |
| throw YamlException('Undefined alias.', event.span); | |
| } | |
| /// Composes a scalar node. | |
| YamlNode _loadScalar(ScalarEvent scalar) { | |
| YamlNode node; | |
| if (scalar.tag == '!') { | |
| node = YamlScalar.internal(scalar.value, scalar); | |
| } else if (scalar.tag != null) { | |
| node = _parseByTag(scalar); | |
| } else { | |
| node = _parseScalar(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 YamlException('Invalid tag for sequence.', firstEvent.span); | |
| } | |
| var children = <YamlNode>[]; | |
| var node = YamlList.internal(children, firstEvent.span, firstEvent.style); | |
| _registerAnchor(firstEvent.anchor, node); | |
| Event event; | |
| try { | |
| if (firstEvent.anchor case final anchor?) _activeAnchors.add(anchor); | |
| event = _parser.parse(); | |
| while (event.type != EventType.sequenceEnd) { | |
| children.add(_loadNode(event)); | |
| event = _parser.parse(); | |
| } | |
| } finally { | |
| if (firstEvent.anchor case final anchor?) _activeAnchors.remove(anchor); | |
| } | |
| 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 YamlException('Invalid tag for mapping.', firstEvent.span); | |
| } | |
| var children = deepEqualsMap<dynamic, YamlNode>(); | |
| var node = YamlMap.internal(children, firstEvent.span, firstEvent.style); | |
| _registerAnchor(firstEvent.anchor, node); | |
| Event event; | |
| try { | |
| if (firstEvent.anchor case final anchor?) _activeAnchors.add(anchor); | |
| event = _parser.parse(); | |
| while (event.type != EventType.mappingEnd) { | |
| var key = _loadNode(event); | |
| var value = _loadNode(_parser.parse()); | |
| if (children.containsKey(key)) { | |
| throw YamlException('Duplicate mapping key.', key.span); | |
| } | |
| children[key] = value; | |
| event = _parser.parse(); | |
| } | |
| } finally { | |
| if (firstEvent.anchor case final anchor?) _activeAnchors.remove(anchor); | |
| } | |
| 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': | |
| var result = _parseNull(scalar); | |
| if (result != null) return result; | |
| throw YamlException('Invalid null scalar.', scalar.span); | |
| case 'tag:yaml.org,2002:bool': | |
| var result = _parseBool(scalar); | |
| if (result != null) return result; | |
| throw YamlException('Invalid bool scalar.', scalar.span); | |
| case 'tag:yaml.org,2002:int': | |
| var result = _parseNumber(scalar, allowFloat: false); | |
| if (result != null) return result; | |
| throw YamlException('Invalid int scalar.', scalar.span); | |
| case 'tag:yaml.org,2002:float': | |
| var result = _parseNumber(scalar, allowInt: false); | |
| if (result != null) return result; | |
| throw YamlException('Invalid float scalar.', scalar.span); | |
| case 'tag:yaml.org,2002:str': | |
| return YamlScalar.internal(scalar.value, scalar); | |
| default: | |
| throw YamlException('Undefined tag: ${scalar.tag}.', scalar.span); | |
| } | |
| } | |
| /// Parses [scalar], which may be one of several types. | |
| YamlScalar _parseScalar(ScalarEvent scalar) => | |
| _tryParseScalar(scalar) ?? YamlScalar.internal(scalar.value, scalar); | |
| /// Tries to parse [scalar]. | |
| /// | |
| /// If parsing fails, this returns `null`, indicating that the scalar should | |
| /// be parsed as a string. | |
| YamlScalar? _tryParseScalar(ScalarEvent scalar) { | |
| // Quickly check for the empty string, which means null. | |
| var length = scalar.value.length; | |
| if (length == 0) return YamlScalar.internal(null, scalar); | |
| // Dispatch on the first character. | |
| var firstChar = scalar.value.codeUnitAt(0); | |
| return switch (firstChar) { | |
| $dot || $plus || $minus => _parseNumber(scalar), | |
| $n || $N => length == 4 ? _parseNull(scalar) : null, | |
| $t || $T => length == 4 ? _parseBool(scalar) : null, | |
| $f || $F => length == 5 ? _parseBool(scalar) : null, | |
| $tilde => length == 1 ? YamlScalar.internal(null, scalar) : null, | |
| _ => (firstChar >= $0 && firstChar <= $9) ? _parseNumber(scalar) : null | |
| }; | |
| } | |
| /// Parse a null scalar. | |
| /// | |
| /// Returns a Dart `null` if parsing fails. | |
| YamlScalar? _parseNull(ScalarEvent scalar) => switch (scalar.value) { | |
| '' || | |
| 'null' || | |
| 'Null' || | |
| 'NULL' || | |
| '~' => | |
| YamlScalar.internal(null, scalar), | |
| _ => null | |
| }; | |
| /// Parse a boolean scalar. | |
| /// | |
| /// Returns `null` if parsing fails. | |
| YamlScalar? _parseBool(ScalarEvent scalar) => switch (scalar.value) { | |
| 'true' || 'True' || 'TRUE' => YamlScalar.internal(true, scalar), | |
| 'false' || 'False' || 'FALSE' => YamlScalar.internal(false, scalar), | |
| _ => null | |
| }; | |
| /// Parses a numeric scalar. | |
| /// | |
| /// Returns `null` if parsing fails. | |
| YamlScalar? _parseNumber(ScalarEvent scalar, | |
| {bool allowInt = true, bool allowFloat = true}) { | |
| var value = _parseNumberValue(scalar.value, | |
| allowInt: allowInt, allowFloat: allowFloat); | |
| return value == null ? null : YamlScalar.internal(value, scalar); | |
| } | |
| /// Parses the value of a number. | |
| /// | |
| /// Returns the number if it's parsed successfully, or `null` if it's not. | |
| num? _parseNumberValue(String contents, | |
| {bool allowInt = true, bool allowFloat = true}) { | |
| assert(allowInt || allowFloat); | |
| var firstChar = contents.codeUnitAt(0); | |
| var length = contents.length; | |
| // Quick check for single digit integers. | |
| if (allowInt && length == 1) { | |
| var value = firstChar - $0; | |
| return value >= 0 && value <= 9 ? value : null; | |
| } | |
| var secondChar = contents.codeUnitAt(1); | |
| // Hexadecimal or octal integers. | |
| if (allowInt && firstChar == $0) { | |
| // int.tryParse supports 0x natively. | |
| if (secondChar == $x) return int.tryParse(contents); | |
| if (secondChar == $o) { | |
| var afterRadix = contents.substring(2); | |
| return int.tryParse(afterRadix, radix: 8); | |
| } | |
| } | |
| // Int or float starting with a digit or a +/- sign. | |
| if ((firstChar >= $0 && firstChar <= $9) || | |
| ((firstChar == $plus || firstChar == $minus) && | |
| secondChar >= $0 && | |
| secondChar <= $9)) { | |
| // Try to parse an int or, failing that, a double. | |
| num? result; | |
| if (allowInt) { | |
| // Pass "radix: 10" explicitly to ensure that "-0x10", which is valid | |
| // Dart but invalid YAML, doesn't get parsed. | |
| result = int.tryParse(contents, radix: 10); | |
| } | |
| if (allowFloat) result ??= double.tryParse(contents); | |
| return result; | |
| } | |
| if (!allowFloat) return null; | |
| // Now the only possibility is to parse a float starting with a dot or a | |
| // sign and a dot, or the signed/unsigned infinity values and not-a-numbers. | |
| if ((firstChar == $dot && secondChar >= $0 && secondChar <= $9) || | |
| (firstChar == $minus || firstChar == $plus) && secondChar == $dot) { | |
| // Starting with a . and a number or a sign followed by a dot. | |
| if (length == 5) { | |
| switch (contents) { | |
| case '+.inf': | |
| case '+.Inf': | |
| case '+.INF': | |
| return double.infinity; | |
| case '-.inf': | |
| case '-.Inf': | |
| case '-.INF': | |
| return -double.infinity; | |
| } | |
| } | |
| return double.tryParse(contents); | |
| } | |
| if (length == 4 && firstChar == $dot) { | |
| switch (contents) { | |
| case '.inf': | |
| case '.Inf': | |
| case '.INF': | |
| return double.infinity; | |
| case '.nan': | |
| case '.NaN': | |
| case '.NAN': | |
| return double.nan; | |
| } | |
| } | |
| return null; | |
| } | |
| } |