// 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 = {}; /// 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) 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 = []; var node = YamlList.internal(children, firstEvent.span, firstEvent.style); _registerAnchor(firstEvent.anchor, node); var event = _parser.parse(); while (event.type != EventType.sequenceEnd) { 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 YamlException('Invalid tag for mapping.', firstEvent.span); } var children = deepEqualsMap(); var node = YamlMap.internal(children, firstEvent.span, firstEvent.style); _registerAnchor(firstEvent.anchor, node); var 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(); } 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; } }