Fix block map colon offset and support updating explicit keys without colons
diff --git a/pkgs/yaml_edit/CHANGELOG.md b/pkgs/yaml_edit/CHANGELOG.md index 7e14667..dd4350d 100644 --- a/pkgs/yaml_edit/CHANGELOG.md +++ b/pkgs/yaml_edit/CHANGELOG.md
@@ -1,6 +1,8 @@ ## 2.2.5-wip - Fix `YamlEditor.appendToList` and map updates when adding to flow collections with a trailing comma. +- Fix updating block map entries when keys have whitespace before the colon. +- Support updating the value of explicit keys that have no colon. ## 2.2.4
diff --git a/pkgs/yaml_edit/lib/src/map_mutations.dart b/pkgs/yaml_edit/lib/src/map_mutations.dart index 6fa71d6..3a374a2 100644 --- a/pkgs/yaml_edit/lib/src/map_mutations.dart +++ b/pkgs/yaml_edit/lib/src/map_mutations.dart
@@ -173,9 +173,27 @@ valueAsString = ' $valueAsString'; } - /// +1 accounts for the colon - // TODO: What if here is a whitespace following the key, before the colon? - final start = keyNode.span.end.offset + 1; + /// Find the association colon after the key, skipping whitespace and + /// comments. If no colon exists for this key (e.g. an explicit key without + /// a value), insert a new association line with the colon and value. + final colonIndex = _findAssociationColon(yaml, keyNode.span.end.offset); + if (colonIndex == -1) { + final mapIndent = getMapIndentation(yaml, map); + final nextNewLine = yaml.indexOf('\n', keyNode.span.end.offset); + if (nextNewLine == -1) { + return SourceEdit( + yaml.length, + 0, + '$lineEnding${' ' * mapIndent}:$valueAsString$lineEnding', + ); + } + return SourceEdit( + nextNewLine + 1, + 0, + '${' ' * mapIndent}:$valueAsString$lineEnding', + ); + } + final start = colonIndex + 1; var end = getContentSensitiveEnd(map.nodes[key]!); /// `package:yaml` parses empty nodes in a way where the start/end of the @@ -186,6 +204,36 @@ return SourceEdit(start, end - start, valueAsString); } +/// Finds the index of the association colon (`:`) belonging to a key in +/// [yaml], starting from [startOffset]. +/// +/// Skips whitespace and comments. Returns `-1` if no colon is found before +/// encountering another token or reaching the end of the input. +int _findAssociationColon(String yaml, int startOffset) { + var i = startOffset; + while (i < yaml.length) { + final c = yaml.codeUnitAt(i); + if (c == 0x20 /* space */ || + c == 0x09 /* tab */ || + c == 0x0A /* LF */ || + c == 0x0D /* CR */) { + i++; + } else if (c == 0x23 /* # */) { + i++; + while (i < yaml.length && + yaml.codeUnitAt(i) != 0x0A && + yaml.codeUnitAt(i) != 0x0D) { + i++; + } + } else if (c == 0x3A /* : */) { + return i; + } else { + return -1; + } + } + return -1; +} + /// Performs the string operation on [yamlEdit] to achieve the effect of /// replacing the value at [key] with [newValue] when reparsed, bearing in mind /// that this is a flow map.
diff --git a/pkgs/yaml_edit/test/update_test.dart b/pkgs/yaml_edit/test/update_test.dart index 20ab3b9..1746229 100644 --- a/pkgs/yaml_edit/test/update_test.dart +++ b/pkgs/yaml_edit/test/update_test.dart
@@ -961,6 +961,71 @@ doc, {'Sammy Sosa': null, 'Ken Griff': null, 'Mark McGwire': null}); }); + test('updating explicit key without colon succeeds', () { + final doc1 = YamlEditor('? key\n'); + doc1.update(['key'], 123); + expect(doc1.toString(), equals('? key\n: 123\n')); + expectYamlBuilderValue(doc1, {'key': 123}); + + final doc2 = YamlEditor('? key'); + doc2.update(['key'], 123); + expect(doc2.toString(), equals('? key\n: 123\n')); + expectYamlBuilderValue(doc2, {'key': 123}); + + final doc3 = YamlEditor(''' +? key +foo: bar +'''); + doc3.update(['key'], 123); + expect(doc3.toString(), equals(''' +? key +: 123 +foo: bar +''')); + expectYamlBuilderValue(doc3, {'key': 123, 'foo': 'bar'}); + + final doc4 = YamlEditor(''' +? key # comment with : here +foo: bar +'''); + doc4.update(['key'], 123); + expect(doc4.toString(), equals(''' +? key # comment with : here +: 123 +foo: bar +''')); + expectYamlBuilderValue(doc4, {'key': 123, 'foo': 'bar'}); + + final doc5 = YamlEditor(''' +parent: + ? key + foo: bar +'''); + doc5.update(['parent', 'key'], 123); + expect(doc5.toString(), equals(''' +parent: + ? key + : 123 + foo: bar +''')); + expectYamlBuilderValue(doc5, { + 'parent': {'key': 123, 'foo': 'bar'} + }); + }); + + test('updating explicit key with colon succeeds', () { + final doc = YamlEditor(''' +? key +: old +'''); + doc.update(['key'], 'new'); + expect(doc.toString(), equals(''' +? key +: new +''')); + expectYamlBuilderValue(doc, {'key': 'new'}); + }); + test('with trailing newline', () { final doc = YamlEditor(''' a: 1 @@ -1106,4 +1171,43 @@ }); }); }); + + group('quoted map keys with spaces before colon', () { + test('preserves colon when updating double-quoted key with space', () { + final doc = YamlEditor(''' +"key" : 123 +other: 456 +'''); + doc.update(['key'], 789); + expect(doc.toString(), equals(''' +"key" : 789 +other: 456 +''')); + expect(doc.parseAt(['key']).value, equals(789)); + }); + + test('preserves colon when updating single-quoted key with space', () { + final doc = YamlEditor(''' +'my quoted key' : 100 +'''); + doc.update(['my quoted key'], 200); + expect(doc.toString(), equals(''' +'my quoted key' : 200 +''')); + expect(doc.parseAt(['my quoted key']).value, equals(200)); + }); + + test('preserves colon when replacing with block collection', () { + final doc = YamlEditor(''' +"key" : 123 +'''); + doc.update(['key'], ['a', 'b']); + expect(doc.toString(), equals(''' +"key" : + - a + - b +''')); + expect(doc.parseAt(['key', 0]).value, equals('a')); + }); + }); }