| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:collection/collection.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'utils.dart'; |
| |
| /// Given [value], tries to format it into a plain string recognizable by YAML. |
| /// |
| /// Not all values can be formatted into a plain string. If the string contains |
| /// an escape sequence, it can only be detected when in a double-quoted |
| /// sequence. Plain strings may also be misinterpreted by the YAML parser (e.g. |
| /// ' null'). |
| /// |
| /// Returns `null` if [value] cannot be encoded as a plain string. |
| String? _tryYamlEncodePlain(String value) { |
| /// If it contains a dangerous character we want to wrap the result with |
| /// double quotes because the double quoted style allows for arbitrary |
| /// strings with "\" escape sequences. |
| /// |
| /// See 7.3.1 Double-Quoted Style |
| /// https://yaml.org/spec/1.2/spec.html#id2787109 |
| return isDangerousString(value) ? null : value; |
| } |
| |
| /// Checks if [string] has unprintable characters according to |
| /// [unprintableCharCodes]. |
| bool _hasUnprintableCharacters(String string) { |
| final codeUnits = string.codeUnits; |
| |
| for (final key in unprintableCharCodes.keys) { |
| if (codeUnits.contains(key)) return true; |
| } |
| |
| return false; |
| } |
| |
| /// Generates a YAML-safe double-quoted string based on [string], escaping the |
| /// list of characters as defined by the YAML 1.2 spec. |
| /// |
| /// See 5.7 Escaped Characters https://yaml.org/spec/1.2/spec.html#id2776092 |
| String _yamlEncodeDoubleQuoted(String string) { |
| final buffer = StringBuffer(); |
| for (final codeUnit in string.codeUnits) { |
| if (doubleQuoteEscapeChars[codeUnit] != null) { |
| buffer.write(doubleQuoteEscapeChars[codeUnit]); |
| } else { |
| buffer.writeCharCode(codeUnit); |
| } |
| } |
| |
| return '"$buffer"'; |
| } |
| |
| /// Encodes [string] as YAML single quoted string. |
| /// |
| /// Returns `null`, if the [string] can't be encoded as single-quoted string. |
| /// This might happen if it contains line-breaks or [_hasUnprintableCharacters]. |
| /// |
| /// See: https://yaml.org/spec/1.2.2/#732-single-quoted-style |
| String? _tryYamlEncodeSingleQuoted(String string) { |
| // If [string] contains a newline we'll use double quoted strings instead. |
| // Single quoted strings can represent newlines, but then we have to use an |
| // empty line (replace \n with \n\n). But since leading spaces following |
| // line breaks are ignored, we can't represent "\n ". |
| // Thus, if the string contains `\n` and we're asked to do single quoted, |
| // we'll fallback to a double quoted string. |
| if (_hasUnprintableCharacters(string) || string.contains('\n')) return null; |
| |
| final result = string.replaceAll('\'', '\'\''); |
| return '\'$result\''; |
| } |
| |
| /// Attempts to encode a [string] as a _YAML folded string_ and apply the |
| /// appropriate _chomping indicator_. |
| /// |
| /// Returns `null`, if the [string] cannot be encoded as a _YAML folded |
| /// string_. |
| /// |
| /// **Examples** of folded strings. |
| /// ```yaml |
| /// # With the "strip" chomping indicator |
| /// key: >- |
| /// my folded |
| /// string |
| /// |
| /// # With the "keep" chomping indicator |
| /// key: >+ |
| /// my folded |
| /// string |
| /// ``` |
| /// |
| /// See: https://yaml.org/spec/1.2.2/#813-folded-style |
| String? _tryYamlEncodeFolded(String string, int indentSize, String lineEnding) { |
| // A string that starts with space or newline followed by space can't be |
| // encoded in folded mode. |
| if (string.isEmpty || string.trim().length != string.length) return null; |
| |
| if (_hasUnprintableCharacters(string)) return null; |
| |
| // TODO: Are there other strings we can't encode in folded mode? |
| |
| final indent = ' ' * indentSize; |
| |
| /// Remove trailing `\n` & white-space to ease string folding |
| var trimmed = string.trimRight(); |
| final stripped = string.substring(trimmed.length); |
| |
| final trimmedSplit = |
| trimmed.replaceAll('\n', lineEnding + indent).split(lineEnding); |
| |
| /// Try folding to match specification: |
| /// * https://yaml.org/spec/1.2.2/#65-line-folding |
| trimmed = trimmedSplit.reduceIndexed((index, previous, current) { |
| var updated = current; |
| |
| /// If initially empty, this line holds only `\n` or white-space. This |
| /// tells us we don't need to apply an additional `\n`. |
| /// |
| /// See https://yaml.org/spec/1.2.2/#64-empty-lines |
| /// |
| /// If this line is not empty, we need to apply an additional `\n` if and |
| /// only if: |
| /// 1. The preceding line was non-empty too |
| /// 2. If the current line doesn't begin with white-space |
| /// |
| /// Such that we apply `\n` for `foo\nbar` but not `foo\n bar`. |
| if (current.trim().isNotEmpty && |
| trimmedSplit[index - 1].trim().isNotEmpty && |
| !current.replaceFirst(indent, '').startsWith(' ')) { |
| updated = lineEnding + updated; |
| } |
| |
| /// Apply a `\n` by default. |
| return previous + lineEnding + updated; |
| }); |
| |
| return '>-\n' |
| '$indent$trimmed' |
| '${stripped.replaceAll('\n', lineEnding + indent)}'; |
| } |
| |
| /// Attempts to encode a [string] as a _YAML literal string_ and apply the |
| /// appropriate _chomping indicator_. |
| /// |
| /// Returns `null`, if the [string] cannot be encoded as a _YAML literal |
| /// string_. |
| /// |
| /// **Examples** of literal strings. |
| /// ```yaml |
| /// # With the "strip" chomping indicator |
| /// key: |- |
| /// my literal |
| /// string |
| /// |
| /// # Without chomping indicator |
| /// key: | |
| /// my literal |
| /// string |
| /// ``` |
| /// |
| /// See: https://yaml.org/spec/1.2.2/#812-literal-style |
| String? _tryYamlEncodeLiteral( |
| String string, int indentSize, String lineEnding) { |
| if (string.isEmpty || string.trim().length != string.length) return null; |
| |
| // A string that starts with space or newline followed by space can't be |
| // encoded in literal mode. |
| if (_hasUnprintableCharacters(string)) return null; |
| |
| // TODO: Are there other strings we can't encode in literal mode? |
| |
| final indent = ' ' * indentSize; |
| |
| /// Simplest block style. |
| /// * https://yaml.org/spec/1.2.2/#812-literal-style |
| return '|-\n$indent${string.replaceAll('\n', lineEnding + indent)}'; |
| } |
| |
| /// Encodes a flow [YamlScalar] based on the provided [YamlScalar.style]. |
| /// |
| /// Falls back to [ScalarStyle.DOUBLE_QUOTED] if the [yamlScalar] cannot be |
| /// encoded with the [YamlScalar.style] or with [ScalarStyle.PLAIN] when the |
| /// [yamlScalar] is not a [String]. |
| String _yamlEncodeFlowScalar(YamlScalar yamlScalar) { |
| final YamlScalar(:value, :style) = yamlScalar; |
| |
| if (value is! String) { |
| return value.toString(); |
| } |
| |
| switch (style) { |
| /// Only encode as double-quoted if it's a string. |
| case ScalarStyle.DOUBLE_QUOTED: |
| return _yamlEncodeDoubleQuoted(value); |
| |
| case ScalarStyle.SINGLE_QUOTED: |
| return _tryYamlEncodeSingleQuoted(value) ?? |
| _yamlEncodeDoubleQuoted(value); |
| |
| /// Cast into [String] if [null] as this condition only returns [null] |
| /// for a [String] that can't be encoded. |
| default: |
| return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value); |
| } |
| } |
| |
| /// Encodes a block [YamlScalar] based on the provided [YamlScalar.style]. |
| /// |
| /// Falls back to [ScalarStyle.DOUBLE_QUOTED] if the [yamlScalar] cannot be |
| /// encoded with the [YamlScalar.style] provided. |
| String _yamlEncodeBlockScalar( |
| YamlScalar yamlScalar, |
| int indentation, |
| String lineEnding, |
| ) { |
| final YamlScalar(:value, :style) = yamlScalar; |
| assertValidScalar(value); |
| |
| if (value is! String) { |
| return value.toString(); |
| } |
| |
| switch (style) { |
| /// Prefer 'plain', fallback to "double quoted" |
| case ScalarStyle.PLAIN: |
| return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value); |
| |
| // Prefer 'single quoted', fallback to "double quoted" |
| case ScalarStyle.SINGLE_QUOTED: |
| return _tryYamlEncodeSingleQuoted(value) ?? |
| _yamlEncodeDoubleQuoted(value); |
| |
| /// Prefer folded string, fallback to "double quoted" |
| case ScalarStyle.FOLDED: |
| return _tryYamlEncodeFolded(value, indentation, lineEnding) ?? |
| _yamlEncodeDoubleQuoted(value); |
| |
| /// Prefer literal string, fallback to "double quoted" |
| case ScalarStyle.LITERAL: |
| return _tryYamlEncodeLiteral(value, indentation, lineEnding) ?? |
| _yamlEncodeDoubleQuoted(value); |
| |
| /// Prefer plain, fallback to "double quoted" |
| default: |
| return _tryYamlEncodePlain(value) ?? _yamlEncodeDoubleQuoted(value); |
| } |
| } |
| |
| /// Returns [value] with the necessary formatting applied in a flow context. |
| /// |
| /// If [value] is a [YamlNode], we try to respect its [YamlScalar.style] |
| /// parameter where possible. Certain cases make this impossible (e.g. a plain |
| /// string scalar that starts with '>', a child having a block style |
| /// parameters), in which case we will produce [value] with default styling |
| /// options. |
| String yamlEncodeFlow(YamlNode value) { |
| if (value is YamlList) { |
| final list = value.nodes; |
| |
| final safeValues = list.map(yamlEncodeFlow); |
| return '[${safeValues.join(', ')}]'; |
| } else if (value is YamlMap) { |
| final safeEntries = value.nodes.entries.map((entry) { |
| final safeKey = yamlEncodeFlow(entry.key as YamlNode); |
| final safeValue = yamlEncodeFlow(entry.value); |
| return '$safeKey: $safeValue'; |
| }); |
| |
| return '{${safeEntries.join(', ')}}'; |
| } |
| |
| return _yamlEncodeFlowScalar(value as YamlScalar); |
| } |
| |
| /// Returns [value] with the necessary formatting applied in a block context. |
| String yamlEncodeBlock( |
| YamlNode value, |
| int indentation, |
| String lineEnding, |
| ) { |
| const additionalIndentation = 2; |
| |
| if (!isBlockNode(value)) return yamlEncodeFlow(value); |
| |
| final newIndentation = indentation + additionalIndentation; |
| |
| if (value is YamlList) { |
| if (value.isEmpty) return '${' ' * indentation}[]'; |
| |
| Iterable<String> safeValues; |
| |
| final children = value.nodes; |
| |
| safeValues = children.map((child) { |
| var valueString = yamlEncodeBlock(child, newIndentation, lineEnding); |
| if (isCollection(child) && !isFlowYamlCollectionNode(child)) { |
| valueString = valueString.substring(newIndentation); |
| } |
| |
| return '${' ' * indentation}- $valueString'; |
| }); |
| |
| return safeValues.join(lineEnding); |
| } else if (value is YamlMap) { |
| if (value.isEmpty) return '${' ' * indentation}{}'; |
| |
| return value.nodes.entries.map((entry) { |
| final MapEntry(:key, :value) = entry; |
| |
| final safeKey = yamlEncodeFlow(key as YamlNode); |
| final formattedKey = ' ' * indentation + safeKey; |
| |
| final formattedValue = yamlEncodeBlock( |
| value, |
| newIndentation, |
| lineEnding, |
| ); |
| |
| /// Empty collections are always encoded in flow-style, so new-line must |
| /// be avoided |
| if (isCollection(value) && !isEmpty(value)) { |
| return '$formattedKey:$lineEnding$formattedValue'; |
| } |
| |
| return '$formattedKey: $formattedValue'; |
| }).join(lineEnding); |
| } |
| |
| return _yamlEncodeBlockScalar( |
| value as YamlScalar, |
| newIndentation, |
| lineEnding, |
| ); |
| } |
| |
| /// List of unprintable characters. |
| /// |
| /// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092 |
| final Map<int, String> unprintableCharCodes = { |
| 0: '\\0', // Escaped ASCII null (#x0) character. |
| 7: '\\a', // Escaped ASCII bell (#x7) character. |
| 8: '\\b', // Escaped ASCII backspace (#x8) character. |
| 11: '\\v', // Escaped ASCII vertical tab (#xB) character. |
| 12: '\\f', // Escaped ASCII form feed (#xC) character. |
| 13: '\\r', // Escaped ASCII carriage return (#xD) character. Line Break. |
| 27: '\\e', // Escaped ASCII escape (#x1B) character. |
| 133: '\\N', // Escaped Unicode next line (#x85) character. |
| 160: '\\_', // Escaped Unicode non-breaking space (#xA0) character. |
| 8232: '\\L', // Escaped Unicode line separator (#x2028) character. |
| 8233: '\\P', // Escaped Unicode paragraph separator (#x2029) character. |
| }; |
| |
| /// List of escape characters. |
| /// |
| /// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092 |
| final Map<int, String> doubleQuoteEscapeChars = { |
| ...unprintableCharCodes, |
| 9: '\\t', // Escaped ASCII horizontal tab (#x9) character. Printable |
| 10: '\\n', // Escaped ASCII line feed (#xA) character. Line Break. |
| 34: '\\"', // Escaped ASCII double quote (#x22). |
| 47: '\\/', // Escaped ASCII slash (#x2F), for JSON compatibility. |
| 92: '\\\\', // Escaped ASCII back slash (#x5C). |
| }; |