From 7501cb4540999936dca7c8d52dae4d2e2dbe496d Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:06:09 +0100 Subject: [PATCH 01/11] Add function to skip and/or extract comments --- lib/src/utils.dart | 112 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index c1e1755..32f279f 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -273,6 +273,118 @@ String getLineEnding(String yaml) { return windowsNewlines > unixNewlines ? '\r\n' : '\n'; } +/// Extracts comments for a node that is replaced within a [YamlMap] or +/// [YamlList] or a top-level [YamlScalar] of the [yaml] string provided. +/// +/// [currentEndOffset] represents the end offset of [YamlScalar] or [YamlList] +/// or [YamlMap] being replaced, that is, `end + 1`. +/// +/// [nextStartOffset] represents the start offset of the next [YamlNode]. +/// May be null if the current [YamlNode] being replaced is the last node +/// in a [YamlScalar] or [YamlList] or if its the only top-level [YamlScalar]. +/// If not sure of the next [YamlNode]'s [nextStartOffset] pass in null and +/// allow this function to handle that manually. +/// +/// Do note that this function has no context of the structure of the [yaml] +/// but assumes the caller does and requires comments based on the offsets +/// provided and thus, may be erroneus since it exclusively scans for `#` +/// delimiter or extracts the comments between the [currentEndOffset] and +/// [nextStartOffset] if both are provided. +/// +/// Returns the `endOffset` of the last comment extracted that is `end + 1` +/// and a `List comments`. It is recommended (but not necessary) that +/// the caller checks the `endOffset` is still within the bounds of the [yaml]. +(int endOffset, List comments) skipAndExtractCommentsInBlock( + String yaml, + int currentEndOffset, + int? nextStartOffset, [ + String lineEnding = '\n', +]) { + /// If [nextStartOffset] is null, this may be the last element in a collection + /// and thus we have to check and extract comments manually. + /// + /// Also, the caller may not be sure where the next node starts. + if (nextStartOffset == null) { + final comments = []; + + /// Skips white-space while extracting comments. + /// + /// Returns [null] if the end of the [yaml] was encountered while + /// skipping any white-space. Otherwise, returns the [index] of the next + /// non-white-space character. + int? skipWhitespace(int index) { + var nextIndex = index; + + while (true) { + if (nextIndex == yaml.length) return null; + if (yaml[nextIndex].trim().isNotEmpty) return nextIndex; + ++nextIndex; + } + } + + var currentOffset = currentEndOffset; + + externalLoop: + while (true) { + if (currentOffset == yaml.length) break; + + var leadingChar = yaml[currentOffset].trim(); + var indexOfCommentStart = -1; + + if (leadingChar.isEmpty) { + switch (skipWhitespace(currentOffset)) { + case final int nextIndex: + currentOffset = nextIndex; + leadingChar = yaml[currentOffset]; + break; + + default: + currentOffset = yaml.length; + break externalLoop; // Exit loop entirely! + } + } + + /// We need comments only, nothing else. This may be pointless but will + /// help us avoid extracting comments when provided random offsets + /// within a string. + if (leadingChar == '#') indexOfCommentStart = currentOffset; + + /// This is a mindless assumption that the last character was either + /// `\n` or [white-space] or the last erroneus offset provided. + if (indexOfCommentStart == -1) break; + + final indexOfLineBreak = yaml.indexOf(lineEnding, currentOffset); + final isEnd = indexOfLineBreak == -1; + + final comment = yaml + .substring(indexOfCommentStart, isEnd ? null : indexOfLineBreak) + .trim(); + + if (comment.isNotEmpty) comments.add(comment); + + if (isEnd) { + currentOffset += comment.length; + break; + } + currentOffset = indexOfLineBreak + 1; // Skip line-break eagerly + } + + return (currentOffset, comments); + } + + return ( + nextStartOffset, + yaml.substring(currentEndOffset, nextStartOffset).split(lineEnding).fold( + [], + (buffer, current) { + final comment = current.trim(); + if (comment.isNotEmpty) buffer.add(comment); + return buffer; + }, + ) + ); +} + extension YamlNodeExtension on YamlNode { /// Returns the [CollectionStyle] of `this` if `this` is [YamlMap] or /// [YamlList]. From 647329c33aa7fce42fb9a7f48ce1ca826ddc1164 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:21:53 +0100 Subject: [PATCH 02/11] Add function to normalize trailing line breaks in encode block --- lib/src/utils.dart | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 32f279f..05451c3 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -385,6 +385,84 @@ String getLineEnding(String yaml) { ); } +/// Normalizes an encoded [YamlNode] encoded as a string by pruning any +/// dangling line-breaks. +/// +/// This function checks the last `YamlNode` of the [update] that is a +/// `YamlScalar` and removes any unwanted line-break within the +/// [updateAsString]. +/// +/// This is achieved by obtaining the chunk of the [yaml] that is after the +/// current node being replaced using its [nodeToReplaceEndOffset]. If: +/// 1. The chunk has any trailing line-break then the it is left untouched. +/// 2. The node being replaced with [update] is not the last node, then it +/// is left untouched. +/// 3. The terminal node in [update] is a `YamlScalar`, that is, +/// the last [YamlNode] within the [update] that is not a collection. +String normalizeEncodedBlock( + String yaml, + String lineEnding, + int nodeToReplaceEndOffset, + YamlNode update, + String updateAsString, +) { + var terminalNode = update; + + if (terminalNode is! YamlScalar) { + loop: + while (terminalNode is! YamlScalar) { + switch (terminalNode) { + case YamlList list: + { + if (list.isEmpty) { + terminalNode = list; + break loop; + } + + terminalNode = list.nodes.last; + } + + case YamlMap map: + { + if (map.isEmpty) { + terminalNode = map; + break loop; + } + + terminalNode = map.nodes.entries.last.value; + } + } + } + } + + /// The node may end up being an empty [YamlMap] or [YamlList] or + /// [YamlScalar]. We never normalize a literal/folded string irrespective of + /// its position + if (terminalNode case YamlScalar(style: var style) + when style == ScalarStyle.LITERAL || style == ScalarStyle.FOLDED) { + return updateAsString; + } + + var normalizedString = updateAsString; + + /// We need to be methodical as we only want to strip it if at the end of the + /// yaml. If not at the end, this `\n` acts as a line break. + final trailing = yaml.substring(nodeToReplaceEndOffset); + + /// We trim it since `package: yaml` only includes an offset with meaningful + /// content. A further check for the trailing `\n` ensures we respect its + /// initial state. + if (trailing.trimRight().isEmpty && !trailing.endsWith(lineEnding)) { + final size = lineEnding == '\r\n' ? 2 : 1; + normalizedString = updateAsString.substring( + 0, + updateAsString.length - size, + ); + } + + return normalizedString; +} + extension YamlNodeExtension on YamlNode { /// Returns the [CollectionStyle] of `this` if `this` is [YamlMap] or /// [YamlList]. From 502fa0af51de4cab831b53313eee0c4cadde7d30 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:22:34 +0100 Subject: [PATCH 03/11] Return index getting key node in map --- lib/src/equality.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/equality.dart b/lib/src/equality.dart index 0c6a952..fae0ed3 100644 --- a/lib/src/equality.dart +++ b/lib/src/equality.dart @@ -87,8 +87,10 @@ int deepHashCode(Object? value) { } /// Returns the [YamlNode] corresponding to the provided [key]. -YamlNode getKeyNode(YamlMap map, Object? key) { - return map.nodes.keys.firstWhere((node) => deepEquals(node, key)) as YamlNode; +(int index, YamlNode keyNode) getKeyNode(YamlMap map, Object? key) { + return map.nodes.keys.indexed.firstWhere( + (value) => deepEquals(value.$2, key), + ) as (int, YamlNode); } /// Returns the [YamlNode] after the [YamlNode] corresponding to the provided From c3056c81e2000b794303bdad053b876b25a32881 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:32:03 +0100 Subject: [PATCH 04/11] Apply line-break after each encoded yaml block --- lib/src/strings.dart | 59 ++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/lib/src/strings.dart b/lib/src/strings.dart index dcb1b72..649cc8a 100644 --- a/lib/src/strings.dart +++ b/lib/src/strings.dart @@ -276,64 +276,81 @@ String yamlEncodeFlow(YamlNode value) { } /// Returns [value] with the necessary formatting applied in a block context. +/// +/// It is recommended that callers of this method also make a call to +/// [normalizeEncodedBlock] with this [value] as the `update` and output +/// of this call as the `updateAsString` to prune any dangling line-break. String yamlEncodeBlock( YamlNode value, int indentation, String lineEnding, +) { + return _encodeBlockRecursively(value, indentation, lineEnding).$2; +} + +(bool addedLineBreak, String value) _encodeBlockRecursively( + YamlNode value, + int indentation, + String lineEnding, ) { const additionalIndentation = 2; - if (!isBlockNode(value)) return yamlEncodeFlow(value); + if (!isBlockNode(value)) return (true, yamlEncodeFlow(value) + lineEnding); final newIndentation = indentation + additionalIndentation; if (value is YamlList) { - if (value.isEmpty) return '${' ' * indentation}[]'; + if (value.isEmpty) return (true, '${' ' * indentation}[]$lineEnding'); - Iterable safeValues; - - final children = value.nodes; + final encodedList = value.nodes.fold('', (string, element) { + var (addedLineBreak, valueString) = _encodeBlockRecursively( + element, + newIndentation, + lineEnding, + ); - safeValues = children.map((child) { - var valueString = yamlEncodeBlock(child, newIndentation, lineEnding); - if (isCollection(child) && !isFlowYamlCollectionNode(child)) { + if (isCollection(element) && !isFlowYamlCollectionNode(element)) { valueString = valueString.substring(newIndentation); } - return '${' ' * indentation}- $valueString'; + final appended = '$string${' ' * indentation}- $valueString'; + return addedLineBreak ? appended : '$appended$lineEnding'; }); - return safeValues.join(lineEnding); + return (true, encodedList); } else if (value is YamlMap) { - if (value.isEmpty) return '${' ' * indentation}{}'; + if (value.isEmpty) return (true, '${' ' * indentation}{}$lineEnding'); - return value.nodes.entries.map((entry) { + final encodedMap = value.nodes.entries.fold('', (string, entry) { final MapEntry(:key, :value) = entry; final safeKey = yamlEncodeFlow(key as YamlNode); - final formattedKey = ' ' * indentation + safeKey; + var formattedKey = ' ' * indentation + safeKey; - final formattedValue = yamlEncodeBlock( + final (addedLineBreak, formattedValue) = _encodeBlockRecursively( 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'; - } + /// be avoided. Otherwise, begin the collection on a new line. + formattedKey = '$formattedKey:' + '${isCollection(value) && !isEmpty(value) ? lineEnding : " "}'; - return '$formattedKey: $formattedValue'; - }).join(lineEnding); + final appended = '$string$formattedKey$formattedValue'; + return addedLineBreak ? appended : '$appended$lineEnding'; + }); + return (true, encodedMap); } - return _yamlEncodeBlockScalar( + final encodedScalar = _yamlEncodeBlockScalar( value as YamlScalar, newIndentation, lineEnding, ); + + return (true, encodedScalar + lineEnding); } /// List of unprintable characters. From e610993314654a3f0ed3a64a61971c65fa5a325b Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:33:44 +0100 Subject: [PATCH 05/11] Encode folded/literal strings based on c3056c8 --- lib/src/strings.dart | 72 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/src/strings.dart b/lib/src/strings.dart index 649cc8a..139bc8a 100644 --- a/lib/src/strings.dart +++ b/lib/src/strings.dart @@ -106,7 +106,7 @@ String? _tryYamlEncodeFolded(String string, int indentSize, String lineEnding) { /// Remove trailing `\n` & white-space to ease string folding var trimmed = string.trimRight(); - final stripped = string.substring(trimmed.length); + var stripped = string.substring(trimmed.length); final trimmedSplit = trimmed.replaceAll('\n', lineEnding + indent).split(lineEnding); @@ -137,9 +137,30 @@ String? _tryYamlEncodeFolded(String string, int indentSize, String lineEnding) { return previous + lineEnding + updated; }); - return '>-\n' + stripped = stripped.replaceAll('\n', lineEnding); // Mild paranoia + final ignoreTrailingLineBreak = stripped.endsWith(lineEnding); + + // We ignore it with conviction as explained below. + if (ignoreTrailingLineBreak) { + stripped = stripped.substring(0, stripped.length - 1); + } + + /// If indeed we have a trailing line, we apply a `chomping hack`. We use a + /// `clip indicator` (no chomping indicator) if we need to ignore the `\n` + /// and `strip indicator` if not to remove any trailing indents. + /// + /// The caller of this method, that is, [yamlEncodeBlock] will apply a + /// dangling `\n` that will\should be normalized by + /// [normalizeEncodedBlock] which allows trailing `\n` for [folded] + /// strings such that: + /// * If we had a string `"my string \n"`: + /// 1. This function excludes it and it becomes `>\nmy string ` + /// 2. [yamlEncodeBlock] applies `\n` that we skipped. + /// 2. [normalizeEncodedBlock] ignores the trailing `\n` for folded + /// string by default. + return '>${ignoreTrailingLineBreak ? '' : '-'}\n' '$indent$trimmed' - '${stripped.replaceAll('\n', lineEnding + indent)}'; + '${stripped.replaceAll(lineEnding, lineEnding + indent)}'; } /// Attempts to encode a [string] as a _YAML literal string_ and apply the @@ -170,13 +191,41 @@ String? _tryYamlEncodeLiteral( // encoded in literal mode. if (_hasUnprintableCharacters(string)) return null; + final indent = ' ' * indentSize; + // TODO: Are there other strings we can't encode in literal mode? + final trimmed = string.trimRight(); - final indent = ' ' * indentSize; + // Mild paranoia + var stripped = string + .substring( + trimmed.length, + ) + .replaceAll('\n', lineEnding); + + final ignoreTrailingLineBreak = stripped.endsWith(lineEnding); - /// Simplest block style. - /// * https://yaml.org/spec/1.2.2/#812-literal-style - return '|-\n$indent${string.replaceAll('\n', lineEnding + indent)}'; + // We ignore it with conviction as explained below. + if (ignoreTrailingLineBreak) { + stripped = stripped.substring(0, stripped.length - 1); + } + + /// If indeed we have a trailing line, we apply a `chomping hack`. We use a + /// `clip indicator` (no chomping indicator) if we need to ignore the `\n` + /// and `strip indicator` if not to remove any trailing indents. + /// + /// The caller of this method, that is, [yamlEncodeBlock] will apply a + /// dangling `\n` that will\should be normalized by + /// [normalizeEncodedBlock] which allows trailing `\n` for [literal] + /// strings such that: + /// * If we had a string `"my string \n"`: + /// 1. This function excludes it and it becomes `|\nmy string ` + /// 2. [yamlEncodeBlock] applies `\n` that we skipped. + /// 2. [normalizeEncodedBlock] ignores the trailing `\n` for literal + /// string by default. + return '|${ignoreTrailingLineBreak ? '' : '-'}\n' + '$indent${trimmed.replaceAll('\n', lineEnding + indent)}' + '${stripped.replaceAll(lineEnding, lineEnding + indent)}'; } /// Encodes a flow [YamlScalar] based on the provided [YamlScalar.style]. @@ -280,13 +329,8 @@ String yamlEncodeFlow(YamlNode value) { /// It is recommended that callers of this method also make a call to /// [normalizeEncodedBlock] with this [value] as the `update` and output /// of this call as the `updateAsString` to prune any dangling line-break. -String yamlEncodeBlock( - YamlNode value, - int indentation, - String lineEnding, -) { - return _encodeBlockRecursively(value, indentation, lineEnding).$2; -} +String yamlEncodeBlock(YamlNode value, int indentation, String lineEnding) => + _encodeBlockRecursively(value, indentation, lineEnding).$2; (bool addedLineBreak, String value) _encodeBlockRecursively( YamlNode value, From e659cb91e554ae5d25dad7eb2ad1595d0c6238b0 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:36:25 +0100 Subject: [PATCH 06/11] Skip comments and include `\n` in map mutations --- lib/src/map_mutations.dart | 65 +++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/lib/src/map_mutations.dart b/lib/src/map_mutations.dart index 67665d9..4dac29e 100644 --- a/lib/src/map_mutations.dart +++ b/lib/src/map_mutations.dart @@ -36,7 +36,7 @@ SourceEdit updateInMap( /// removing the element at [key] when re-parsed. SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object? key) { assert(containsKey(map, key)); - final keyNode = getKeyNode(map, key); + final (_, keyNode) = getKeyNode(map, key); final valueNode = map.nodes[keyNode]!; if (map.style == CollectionStyle.FLOW) { @@ -83,13 +83,14 @@ SourceEdit _addToBlockMap( } } - var valueString = yamlEncodeBlock(newValue, newIndentation, lineEnding); + final valueString = yamlEncodeBlock(newValue, newIndentation, lineEnding); + if (isCollection(newValue) && !isFlowYamlCollectionNode(newValue) && !isEmpty(newValue)) { - formattedValue += '$keyString:$lineEnding$valueString$lineEnding'; + formattedValue += '$keyString:$lineEnding$valueString'; } else { - formattedValue += '$keyString: $valueString$lineEnding'; + formattedValue += '$keyString: $valueString'; } return SourceEdit(offset, 0, formattedValue); @@ -127,12 +128,18 @@ SourceEdit _replaceInBlockMap( YamlEditor yamlEdit, YamlMap map, Object? key, YamlNode newValue) { final yaml = yamlEdit.toString(); final lineEnding = getLineEnding(yaml); - final newIndentation = - getMapIndentation(yaml, map) + getIndentation(yamlEdit); + final mapIndentation = getMapIndentation(yaml, map); + final newIndentation = mapIndentation + getIndentation(yamlEdit); + + // TODO: Compensate for the indent eaten up + final (keyIndex, keyNode) = getKeyNode(map, key); + + var valueAsString = yamlEncodeBlock( + wrapAsYamlNode(newValue), + newIndentation, + lineEnding, + ); - final keyNode = getKeyNode(map, key); - var valueAsString = - yamlEncodeBlock(wrapAsYamlNode(newValue), newIndentation, lineEnding); if (isCollection(newValue) && !isFlowYamlCollectionNode(newValue) && !isEmpty(newValue)) { @@ -150,9 +157,43 @@ SourceEdit _replaceInBlockMap( var end = getContentSensitiveEnd(map.nodes[key]!); /// `package:yaml` parses empty nodes in a way where the start/end of the - /// empty value node is the end of the key node, so we have to adjust for - /// this. - if (end < start) end = start; + /// empty value node is the end of the key node. + /// + /// In our case, we need to ensure that any line-breaks are included in the + /// edit such that: + /// 1. We account for `\n` after a key within other keys or at the start + /// Example.. + /// a: + /// b: value + /// + /// or.. + /// a: value + /// b: + /// c: value + /// + /// 2. We don't suggest edits that are not within the string bounds because + /// of the `\n` we need to account for in Rule 1 above. This could be a + /// key: + /// * At the index `0` but it's the only key + /// * At the end in a map with more than one key + end = start == yaml.length + ? start + : end < start + ? start + 1 + : end; + + // Aggressively skip all comments + final (offsetOfLastComment, _) = + skipAndExtractCommentsInBlock(yaml, end, null, lineEnding); + end = offsetOfLastComment; + + valueAsString = + normalizeEncodedBlock(yaml, lineEnding, end, newValue, valueAsString); + + /// [skipAndExtractCommentsInBlock] is greedy and eats up any whitespace + /// it encounters in search of comments. Compensate indent lost in the + /// current edit + if (keyIndex != map.length - 1) valueAsString += ' ' * mapIndentation; return SourceEdit(start, end - start, valueAsString); } From 53f9637a9671d7fcf2fea220ae26c32466c18b0d Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:37:39 +0100 Subject: [PATCH 07/11] Skip comments and remove additional `\n` added in list mutations --- lib/src/list_mutations.dart | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/src/list_mutations.dart b/lib/src/list_mutations.dart index 17da6dd..de808bc 100644 --- a/lib/src/list_mutations.dart +++ b/lib/src/list_mutations.dart @@ -29,18 +29,21 @@ SourceEdit updateInList( final listIndentation = getListIndentation(yaml, list); final indentation = listIndentation + getIndentation(yamlEdit); final lineEnding = getLineEnding(yaml); - valueString = - yamlEncodeBlock(wrapAsYamlNode(newValue), indentation, lineEnding); + + final encoded = yamlEncodeBlock( + wrapAsYamlNode(newValue), + indentation, + lineEnding, + ); + valueString = encoded; /// We prefer the compact nested notation for collections. /// - /// By virtue of [yamlEncodeBlockString], collections automatically + /// By virtue of [yamlEncodeBlock], collections automatically /// have the necessary line endings. if ((newValue is List && (newValue as List).isNotEmpty) || (newValue is Map && (newValue as Map).isNotEmpty)) { valueString = valueString.substring(indentation); - } else if (currValue.collectionStyle == CollectionStyle.BLOCK) { - valueString += lineEnding; } var end = getContentSensitiveEnd(currValue); @@ -50,6 +53,19 @@ SourceEdit updateInList( valueString = ' $valueString'; } + // Aggressively skip all comments + final (offsetOfLastComment, _) = + skipAndExtractCommentsInBlock(yaml, end, null, lineEnding); + end = offsetOfLastComment; + + valueString = + normalizeEncodedBlock(yaml, lineEnding, end, newValue, valueString); + + /// [skipAndExtractCommentsInBlock] is greedy and eats up any whitespace + /// it encounters in search of comments. Compensate indent lost in the + /// current edit + if (index != list.length - 1) valueString += ' ' * listIndentation; + return SourceEdit(offset, end - offset, valueString); } else { valueString = yamlEncodeFlow(newValue); @@ -146,7 +162,7 @@ SourceEdit _appendToBlockList( valueString = valueString.substring(newIndentation); } - return (listIndentation, '- $valueString$lineEnding'); + return (listIndentation, '- $valueString'); } /// Formats [item] into a new node for flow lists. From edd8d384b4a6b6eb057270cd5c034366940fde42 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 10:48:10 +0100 Subject: [PATCH 08/11] Normalize top level edits --- lib/src/editor.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/editor.dart b/lib/src/editor.dart index 54775cc..c31717d 100644 --- a/lib/src/editor.dart +++ b/lib/src/editor.dart @@ -243,9 +243,10 @@ class YamlEditor { final start = _contents.span.start.offset; final end = getContentSensitiveEnd(_contents); final lineEnding = getLineEnding(_yaml); - final edit = SourceEdit( - start, end - start, yamlEncodeBlock(valueNode, 0, lineEnding)); - + var encoded = yamlEncodeBlock(valueNode, 0, lineEnding); + encoded = + normalizeEncodedBlock(_yaml, lineEnding, end, valueNode, encoded); + final edit = SourceEdit(start, end - start, encoded); return _performEdit(edit, path, valueNode); } @@ -483,7 +484,7 @@ class YamlEditor { if (!containsKey(map, keyOrIndex)) { return _pathErrorOrElse(path, path.take(i + 1), map, orElse); } - final keyNode = getKeyNode(map, keyOrIndex); + final (_, keyNode) = getKeyNode(map, keyOrIndex); if (checkAlias) { if (_aliases.contains(keyNode)) throw AliasException(path, keyNode); From f5a259ba5bf4d8766c11af2455ac1e838b7c9809 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:06:24 +0100 Subject: [PATCH 09/11] Remove defensive encoding function after fix in e659cb9 and 53f9637 --- lib/src/strings.dart | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/lib/src/strings.dart b/lib/src/strings.dart index 139bc8a..c3fe9e7 100644 --- a/lib/src/strings.dart +++ b/lib/src/strings.dart @@ -329,63 +329,43 @@ String yamlEncodeFlow(YamlNode value) { /// It is recommended that callers of this method also make a call to /// [normalizeEncodedBlock] with this [value] as the `update` and output /// of this call as the `updateAsString` to prune any dangling line-break. -String yamlEncodeBlock(YamlNode value, int indentation, String lineEnding) => - _encodeBlockRecursively(value, indentation, lineEnding).$2; - -(bool addedLineBreak, String value) _encodeBlockRecursively( - YamlNode value, - int indentation, - String lineEnding, -) { +String yamlEncodeBlock(YamlNode value, int indentation, String lineEnding) { const additionalIndentation = 2; - if (!isBlockNode(value)) return (true, yamlEncodeFlow(value) + lineEnding); + if (!isBlockNode(value)) return yamlEncodeFlow(value) + lineEnding; final newIndentation = indentation + additionalIndentation; if (value is YamlList) { - if (value.isEmpty) return (true, '${' ' * indentation}[]$lineEnding'); + if (value.isEmpty) return '${' ' * indentation}[]$lineEnding'; - final encodedList = value.nodes.fold('', (string, element) { - var (addedLineBreak, valueString) = _encodeBlockRecursively( - element, - newIndentation, - lineEnding, - ); + return value.nodes.fold('', (string, element) { + var valueString = yamlEncodeBlock(element, newIndentation, lineEnding); if (isCollection(element) && !isFlowYamlCollectionNode(element)) { valueString = valueString.substring(newIndentation); } - final appended = '$string${' ' * indentation}- $valueString'; - return addedLineBreak ? appended : '$appended$lineEnding'; + return '$string${' ' * indentation}- $valueString'; }); - - return (true, encodedList); } else if (value is YamlMap) { - if (value.isEmpty) return (true, '${' ' * indentation}{}$lineEnding'); + if (value.isEmpty) return '${' ' * indentation}{}$lineEnding'; - final encodedMap = value.nodes.entries.fold('', (string, entry) { + return value.nodes.entries.fold('', (string, entry) { final MapEntry(:key, :value) = entry; final safeKey = yamlEncodeFlow(key as YamlNode); var formattedKey = ' ' * indentation + safeKey; - final (addedLineBreak, formattedValue) = _encodeBlockRecursively( - value, - newIndentation, - lineEnding, - ); + final formattedValue = yamlEncodeBlock(value, newIndentation, lineEnding); /// Empty collections are always encoded in flow-style, so new-line must /// be avoided. Otherwise, begin the collection on a new line. formattedKey = '$formattedKey:' '${isCollection(value) && !isEmpty(value) ? lineEnding : " "}'; - final appended = '$string$formattedKey$formattedValue'; - return addedLineBreak ? appended : '$appended$lineEnding'; + return '$string$formattedKey$formattedValue'; }); - return (true, encodedMap); } final encodedScalar = _yamlEncodeBlockScalar( @@ -394,7 +374,7 @@ String yamlEncodeBlock(YamlNode value, int indentation, String lineEnding) => lineEnding, ); - return (true, encodedScalar + lineEnding); + return encodedScalar + lineEnding; } /// List of unprintable characters. From 5a51cbebc7f68fee7ed2d89650491039195276f3 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:43:14 +0100 Subject: [PATCH 10/11] Run dart format --- lib/src/list_mutations.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/list_mutations.dart b/lib/src/list_mutations.dart index de808bc..25d19ad 100644 --- a/lib/src/list_mutations.dart +++ b/lib/src/list_mutations.dart @@ -57,7 +57,7 @@ SourceEdit updateInList( final (offsetOfLastComment, _) = skipAndExtractCommentsInBlock(yaml, end, null, lineEnding); end = offsetOfLastComment; - + valueString = normalizeEncodedBlock(yaml, lineEnding, end, newValue, valueString); From c7aec859e55c184522f9ef28ae0c038ae2521ab5 Mon Sep 17 00:00:00 2001 From: Kelvin Kavisi <68240897+kekavc24@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:50:30 +0100 Subject: [PATCH 11/11] Skip comments for top-level edits --- lib/src/editor.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/editor.dart b/lib/src/editor.dart index c31717d..4a01217 100644 --- a/lib/src/editor.dart +++ b/lib/src/editor.dart @@ -241,8 +241,9 @@ class YamlEditor { if (path.isEmpty) { final start = _contents.span.start.offset; - final end = getContentSensitiveEnd(_contents); + var end = getContentSensitiveEnd(_contents); final lineEnding = getLineEnding(_yaml); + end = skipAndExtractCommentsInBlock(_yaml, end, null, lineEnding).$1; var encoded = yamlEncodeBlock(valueNode, 0, lineEnding); encoded = normalizeEncodedBlock(_yaml, lineEnding, end, valueNode, encoded);