Added yaml_edit (#52)

* Added yaml_edit
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..76170e6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## v1.0.0
+
+- Initial release.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5264c46
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# Yaml Editor
+
+A library for [YAML](https://yaml.org) manipulation while preserving comments.
+
+**Disclaimer:** This is not an officially supported Google product.
+
+## Usage
+
+A simple usage example:
+
+```dart
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  final yamlEditor = YamlEditor('{YAML: YAML}');
+  yamlEditor.assign(['YAML'], "YAML Ain't Markup Language");
+  print(yamlEditor);
+  // Expected output:
+  // {YAML: YAML Ain't Markup Language}
+}
+```
+
+## Testing
+
+Testing is done in two strategies: Unit testing (`/test/editor_test.dart`) and
+Golden testing (`/test/golden_test.dart`). More information on Golden testing
+and the input/output format can be found at `/test/testdata/README.md`.
+
+These tests are automatically run with `pub run test`.
+
+## Limitations
+
+1. Users are not allowed to define tags in the modifications.
+2. Map keys will always be added in the flow style.
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:pedantic/analysis_options.yaml
diff --git a/example/example.dart b/example/example.dart
new file mode 100644
index 0000000..d49c39b
--- /dev/null
+++ b/example/example.dart
@@ -0,0 +1,12 @@
+import 'package:yaml_edit/yaml_edit.dart';
+
+void main() {
+  final doc = YamlEditor('''
+- 0 # comment 0
+- 1 # comment 1
+- 2 # comment 2
+''');
+  doc.remove([1]);
+
+  print(doc);
+}
diff --git a/lib/src/editor.dart b/lib/src/editor.dart
new file mode 100644
index 0000000..9e16598
--- /dev/null
+++ b/lib/src/editor.dart
@@ -0,0 +1,642 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'errors.dart';
+import 'list_mutations.dart';
+import 'map_mutations.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// An interface for modififying [YAML][1] documents while preserving comments
+/// and whitespaces.
+///
+/// YAML parsing is supported by `package:yaml`, and modifications are performed
+/// as string operations. An error will be thrown if internal assertions fail -
+/// such a situation should be extremely rare, and should only occur with
+/// degenerate formatting.
+///
+/// Most modification methods require the user to pass in an [Iterable<Object>]
+/// path that holds the keys/indices to navigate to the element.
+///
+/// **Example:**
+/// ```yaml
+/// a: 1
+/// b: 2
+/// c:
+///   - 3
+///   - 4
+///   - {e: 5, f: [6, 7]}
+/// ```
+///
+/// To get to `7`, our path will be `['c', 2, 'f', 1]`. The path for the base
+/// object is the empty array `[]`. All modification methods will throw a
+/// [Argu,emtError] if the path provided is invalid. Note also that that the order
+/// of elements in the path is important, and it should be arranged in order of
+/// calling, with the first element being the first key or index to be called.
+///
+/// In most modification methods, users are required to pass in a value to be
+/// used for updating the YAML tree. This value is only allowed to either be a
+/// valid scalar that is recognizable by YAML (i.e. `bool`, `String`, `List`,
+/// `Map`, `num`, `null`) or a [YamlNode]. Should the user want to specify
+/// the style to be applied to the value passed in, the user may wrap the value
+/// using [wrapAsYamlNode] while passing in the appropriate `scalarStyle` or
+/// `collectionStyle`. While we try to respect the style that is passed in,
+/// there will be instances where the formatting will not result in valid YAML,
+/// and as such we will fallback to a default formatting while preserving the
+/// content.
+///
+/// To dump the YAML after all the modifications have been completed, simply
+/// call [toString()].
+///
+/// [1]: https://yaml.org/
+@sealed
+class YamlEditor {
+  final List<SourceEdit> _edits = [];
+
+  /// List of [SourceEdit]s that have been applied to [_yaml] since the creation
+  /// of this instance, in chronological order. Intended to be compatible with
+  /// `package:analysis_server`.
+  ///
+  /// The [SourceEdit] objects can be serialized to JSON using the `toJSON`
+  /// function, deserialized using [SourceEdit.fromJSON], and applied to a
+  /// string using the `apply` function. Multiple [SourceEdit]s can be applied
+  /// to a string using [SourceEdit.applyAll].
+  ///
+  /// For more information, refer to the [SourceEdit] class.
+  List<SourceEdit> get edits => [..._edits];
+
+  /// Current YAML string.
+  String _yaml;
+
+  /// Root node of YAML AST.
+  YamlNode _contents;
+
+  /// Stores the list of nodes in [_contents] that are connected by aliases.
+  ///
+  /// When a node is anchored with an alias and subsequently referenced,
+  /// the full content of the anchored node is thought to be copied in the
+  /// following references.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// a: &SS Sammy Sosa
+  /// b: *SS
+  /// ```
+  ///
+  /// is equivalent to
+  ///
+  /// ```dart
+  /// a: Sammy Sosa
+  /// b: Sammy Sosa
+  /// ```
+  ///
+  /// As such, aliased nodes have to be treated with special caution when
+  /// any modification is taking place.
+  ///
+  /// See 7.1 Alias Nodes: https://yaml.org/spec/1.2/spec.html#id2786196
+  Set<YamlNode> _aliases = {};
+
+  /// Returns the current YAML string.
+  @override
+  String toString() => _yaml;
+
+  factory YamlEditor(String yaml) => YamlEditor._(yaml);
+
+  YamlEditor._(this._yaml) {
+    ArgumentError.checkNotNull(_yaml);
+    _initialize();
+  }
+
+  /// Loads [_contents] from [_yaml], and traverses the YAML tree formed to
+  /// detect alias nodes.
+  void _initialize() {
+    _contents = loadYamlNode(_yaml);
+    _aliases = {};
+
+    /// Performs a DFS on [_contents] to detect alias nodes.
+    final visited = <YamlNode>{};
+    void collectAliases(YamlNode node) {
+      if (visited.add(node)) {
+        if (node is YamlMap) {
+          node.nodes.forEach((key, value) {
+            collectAliases(key);
+            collectAliases(value);
+          });
+        } else if (node is YamlList) {
+          node.nodes.forEach(collectAliases);
+        }
+      } else {
+        _aliases.add(node);
+      }
+    }
+
+    collectAliases(_contents);
+  }
+
+  /// Parses the document to return [YamlNode] currently present at [path].
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [ArgumentError].
+  ///
+  /// To get `null` when [path] does not point to a value in the [YamlNode]-tree,
+  /// simply pass `orElse: () => null`.
+  ///
+  /// **Example:** (using orElse)
+  /// ```dart
+  /// final myYamlEditor('{"key": "value"}');
+  /// final value = myYamlEditor.valueAt(['invalid', 'path'], orElse: () => null);
+  /// print(value) // null
+  /// ```
+  ///
+  /// **Example:** (common usage)
+  /// ```dart
+  ///   final doc = YamlEditor('''
+  /// a: 1
+  /// b:
+  ///   d: 4
+  ///   e: [5, 6, 7]
+  /// c: 3
+  /// ''');
+  /// print(doc.parseAt(['b', 'e', 2])); // 7
+  /// ```
+  /// The value returned by [parseAt] is invalidated when the documented is
+  /// mutated, as illustrated below:
+  ///
+  /// **Example:** (old [parseAt] value is invalidated)
+  /// ```dart
+  /// final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+  /// final node = doc.parseAt(['YAML']);
+  ///
+  /// print(node.value); // Expected output: "YAML Ain't Markup Language"
+  ///
+  /// doc.update(['YAML'], 'YAML');
+  ///
+  /// final newNode = doc.parseAt(['YAML']);
+  ///
+  /// // Note that the value does not change
+  /// print(newNode.value); // "YAML"
+  /// print(node.value); // "YAML Ain't Markup Language"
+  /// ```
+  YamlNode parseAt(Iterable<Object> path, {YamlNode Function() orElse}) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    return _traverse(path, orElse: orElse);
+  }
+
+  /// Sets [value] in the [path].
+  ///
+  /// There is a subtle difference between [update] and [remove] followed by
+  /// an [insertIntoList], because [update] preserves comments at the same level.
+  ///
+  /// Throws a [ArgumentError] if [path] is invalid.
+  ///
+  /// **Example:** (using [update])
+  /// ```dart
+  /// final doc = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc.update([1], 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test # comment
+  ///   - 2
+  /// ```
+  ///
+  /// **Example:** (using [remove] and [insertIntoList])
+  /// ```dart
+  /// final doc2 = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc2.remove([1]);
+  /// doc2.insertIntoList([], 1, 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test
+  ///   - 2
+  /// ```
+  void update(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    final valueNode = wrapAsYamlNode(value);
+
+    if (path.isEmpty) {
+      final start = _contents.span.start.offset;
+      final end = getContentSensitiveEnd(_contents);
+      final lineEnding = getLineEnding(_yaml);
+      final edit = SourceEdit(
+          start, end - start, yamlEncodeBlockString(valueNode, 0, lineEnding));
+
+      return _performEdit(edit, path, valueNode);
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath, checkAlias: true);
+
+    if (parentNode is YamlList) {
+      final expected = wrapAsYamlNode(
+        [...parentNode.nodes]..[keyOrIndex] = valueNode,
+      );
+
+      return _performEdit(updateInList(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expected);
+    }
+
+    if (parentNode is YamlMap) {
+      final expectedMap =
+          updatedYamlMap(parentNode, (nodes) => nodes[keyOrIndex] = valueNode);
+      return _performEdit(updateInMap(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expectedMap);
+    }
+
+    throw PathError.unexpected(
+        path, 'Scalar $parentNode does not have key $keyOrIndex');
+  }
+
+  /// Appends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 1]');
+  /// doc.appendToList([], 2); // [0, 1, 2]
+  /// ```
+  void appendToList(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+    final yamlList = _traverseToList(path);
+
+    insertIntoList(path, yamlList.length, value);
+  }
+
+  /// Prepends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[1, 2]');
+  /// doc.prependToList([], 0); // [0, 1, 2]
+  /// ```
+  void prependToList(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    insertIntoList(path, 0, value);
+  }
+
+  /// Inserts [value] into the list at [path].
+  ///
+  /// [index] must be non-negative and no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 2]');
+  /// doc.insertIntoList([], 1, 1); // [0, 1, 2]
+  /// ```
+  void insertIntoList(Iterable<Object> path, int index, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+    final valueNode = wrapAsYamlNode(value);
+
+    final list = _traverseToList(path, checkAlias: true);
+    RangeError.checkValueInInterval(index, 0, list.length);
+
+    final edit = insertInList(this, list, index, valueNode);
+    final expected = wrapAsYamlNode(
+      [...list.nodes]..insert(index, valueNode),
+    );
+
+    _performEdit(edit, path, expected);
+  }
+
+  /// Changes the contents of the list at [path] by removing [deleteCount] items
+  /// at [index], and inserting [values] in-place. Returns the elements that
+  /// are deleted.
+  ///
+  /// [index] and [deleteCount] must be non-negative and [index] + [deleteCount]
+  /// must be no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[Jan, March, April, June]');
+  /// doc.spliceList([], 1, 0, ['Feb']); // [Jan, Feb, March, April, June]
+  /// doc.spliceList([], 4, 1, ['May']); // [Jan, Feb, March, April, May]
+  /// ```
+  Iterable<YamlNode> spliceList(Iterable<Object> path, int index,
+      int deleteCount, Iterable<Object> values) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(index, 'index');
+    ArgumentError.checkNotNull(deleteCount, 'deleteCount');
+    ArgumentError.checkNotNull(values, 'values');
+
+    final list = _traverseToList(path, checkAlias: true);
+
+    RangeError.checkValueInInterval(index, 0, list.length);
+    RangeError.checkValueInInterval(index + deleteCount, 0, list.length);
+
+    final nodesToRemove = list.nodes.getRange(index, index + deleteCount);
+
+    /// Perform addition of elements before removal to avoid scenarioes where
+    /// a block list gets emptied out to {} to avoid changing collection styles
+    /// where possible.
+
+    /// Reverse [values] and insert them.
+    final reversedValues = values.toList().reversed;
+    for (final value in reversedValues) {
+      insertIntoList(path, index, value);
+    }
+
+    for (var i = 0; i < deleteCount; i++) {
+      remove([...path, index + values.length]);
+    }
+
+    return nodesToRemove;
+  }
+
+  /// Removes the node at [path]. Comments "belonging" to the node will be
+  /// removed while surrounding comments will be left untouched.
+  ///
+  /// Throws a [ArgumentError] if [path] is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('''
+  /// - 0 # comment 0
+  /// # comment A
+  /// - 1 # comment 1
+  /// # comment B
+  /// - 2 # comment 2
+  /// ''');
+  /// doc.remove([1]);
+  /// ```
+  ///
+  /// **Expected Result:**
+  /// ```dart
+  /// '''
+  /// - 0 # comment 0
+  /// # comment A
+  /// # comment B
+  /// - 2 # comment 2
+  /// '''
+  /// ```
+  YamlNode remove(Iterable<Object> path) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    SourceEdit edit;
+    YamlNode expectedNode;
+    final nodeToRemove = _traverse(path, checkAlias: true);
+
+    if (path.isEmpty) {
+      edit = SourceEdit(0, _yaml.length, '');
+
+      /// Parsing an empty YAML document returns `null`.
+      _performEdit(edit, path, expectedNode);
+      return nodeToRemove;
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath);
+
+    if (parentNode is YamlList) {
+      edit = removeInList(this, parentNode, keyOrIndex);
+      expectedNode = wrapAsYamlNode(
+        [...parentNode.nodes]..removeAt(keyOrIndex),
+      );
+    } else if (parentNode is YamlMap) {
+      edit = removeInMap(this, parentNode, keyOrIndex);
+
+      expectedNode =
+          updatedYamlMap(parentNode, (nodes) => nodes.remove(keyOrIndex));
+    }
+
+    _performEdit(edit, collectionPath, expectedNode);
+
+    return nodeToRemove;
+  }
+
+  /// Traverses down [path] to return the [YamlNode] at [path] if successful.
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [PathError].
+  ///
+  /// If [checkAlias] is `true`, throw [AliasError] if an aliased node is
+  /// encountered.
+  YamlNode _traverse(Iterable<Object> path,
+      {bool checkAlias = false, YamlNode Function() orElse}) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(checkAlias, 'checkAlias');
+
+    if (path.isEmpty) return _contents;
+
+    var currentNode = _contents;
+    final pathList = path.toList();
+
+    for (var i = 0; i < pathList.length; i++) {
+      final keyOrIndex = pathList[i];
+
+      if (checkAlias && _aliases.contains(currentNode)) {
+        throw AliasError(path, currentNode);
+      }
+
+      if (currentNode is YamlList) {
+        final list = currentNode as YamlList;
+        if (!isValidIndex(keyOrIndex, list.length)) {
+          return _pathErrorOrElse(path, path.take(i + 1), list, orElse);
+        }
+
+        currentNode = list.nodes[keyOrIndex];
+      } else if (currentNode is YamlMap) {
+        final map = currentNode as YamlMap;
+
+        if (!containsKey(map, keyOrIndex)) {
+          return _pathErrorOrElse(path, path.take(i + 1), map, orElse);
+        }
+        final keyNode = getKeyNode(map, keyOrIndex);
+
+        if (checkAlias) {
+          if (_aliases.contains(keyNode)) throw AliasError(path, keyNode);
+        }
+
+        currentNode = map.nodes[keyNode];
+      } else {
+        return _pathErrorOrElse(path, path.take(i + 1), currentNode, orElse);
+      }
+    }
+
+    if (checkAlias) _assertNoChildAlias(path, currentNode);
+
+    return currentNode;
+  }
+
+  /// Throws a [PathError] if [orElse] is not provided, returns the result
+  /// of invoking the [orElse] function otherwise.
+  YamlNode _pathErrorOrElse(Iterable<Object> path, Iterable<Object> subPath,
+      YamlNode parent, YamlNode Function() orElse) {
+    if (orElse == null) throw PathError(path, subPath, parent);
+    return orElse();
+  }
+
+  /// Asserts that [node] and none its children are aliases
+  void _assertNoChildAlias(Iterable<Object> path, [YamlNode node]) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    if (node == null) return _assertNoChildAlias(path, _traverse(path));
+    if (_aliases.contains(node)) throw AliasError(path, node);
+
+    if (node is YamlScalar) return;
+
+    if (node is YamlList) {
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, i];
+        _assertNoChildAlias(updatedPath, node.nodes[i]);
+      }
+    }
+
+    if (node is YamlMap) {
+      final keyList = node.keys.toList();
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, keyList[i]];
+        if (_aliases.contains(keyList[i])) throw AliasError(path, keyList[i]);
+        _assertNoChildAlias(updatedPath, node.nodes[keyList[i]]);
+      }
+    }
+  }
+
+  /// Traverses down the provided [path] to return the [YamlList] at [path].
+  ///
+  /// Convenience function to ensure that a [YamlList] is returned.
+  ///
+  /// Throws [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid. If [checkAlias] is `true`, and an aliased node
+  /// is encountered along [path], an [AliasError] will be thrown.
+  YamlList _traverseToList(Iterable<Object> path, {bool checkAlias = false}) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(checkAlias, 'checkAlias');
+
+    final possibleList = _traverse(path, checkAlias: true);
+
+    if (possibleList is YamlList) {
+      return possibleList;
+    } else {
+      throw PathError.unexpected(
+          path, 'Path $path does not point to a YamlList!');
+    }
+  }
+
+  /// Utility method to replace the substring of [_yaml] according to [edit].
+  ///
+  /// When [_yaml] is modified with this method, the resulting string is parsed
+  /// and reloaded and traversed down [path] to ensure that the reloaded YAML
+  /// tree is equal to our expectations by deep equality of values. Throws an
+  /// [AssertionError] if the two trees do not match.
+  void _performEdit(
+      SourceEdit edit, Iterable<Object> path, YamlNode expectedNode) {
+    ArgumentError.checkNotNull(edit, 'edit');
+    ArgumentError.checkNotNull(path, 'path');
+
+    final expectedTree = _deepModify(_contents, path, [], expectedNode);
+    _yaml = edit.apply(_yaml);
+    _initialize();
+
+    final actualTree = loadYamlNode(_yaml);
+    if (!deepEquals(actualTree, expectedTree)) {
+      throw AssertionError('''
+Modification did not result in expected result! 
+
+Obtained: 
+$actualTree
+
+Expected: 
+$expectedTree''');
+    }
+
+    _contents = actualTree;
+    _edits.add(edit);
+  }
+
+  /// Utility method to produce an updated YAML tree equivalent to converting
+  /// the [YamlNode] at [path] to be [expectedNode]. [subPath] holds the portion
+  /// of [path] that has been traversed thus far.
+  ///
+  /// Throws a [PathError] if path is invalid.
+  ///
+  /// When called, it creates a new [YamlNode] of the same type as [tree], and
+  /// copies its children over, except for the child that is on the path. Doing
+  /// so allows us to "update" the immutable [YamlNode] without having to clone
+  /// the whole tree.
+  ///
+  /// [SourceSpan]s in this new tree are not guaranteed to be accurate.
+  YamlNode _deepModify(YamlNode tree, Iterable<Object> path,
+      Iterable<Object> subPath, YamlNode expectedNode) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(tree, 'tree');
+    RangeError.checkValueInInterval(subPath.length, 0, path.length);
+
+    if (path.length == subPath.length) return expectedNode;
+
+    final keyOrIndex = path.elementAt(subPath.length);
+
+    if (tree is YamlList) {
+      if (!isValidIndex(keyOrIndex, tree.length)) {
+        throw PathError(path, subPath, tree);
+      }
+
+      return wrapAsYamlNode([...tree.nodes]..[keyOrIndex] = _deepModify(
+          tree.nodes[keyOrIndex],
+          path,
+          path.take(subPath.length + 1),
+          expectedNode));
+    }
+
+    if (tree is YamlMap) {
+      return updatedYamlMap(
+          tree,
+          (nodes) => nodes[keyOrIndex] = _deepModify(nodes[keyOrIndex], path,
+              path.take(subPath.length + 1), expectedNode));
+    }
+
+    /// Should not ever reach here.
+    throw PathError(path, subPath, tree);
+  }
+}
diff --git a/lib/src/equality.dart b/lib/src/equality.dart
new file mode 100644
index 0000000..91fa191
--- /dev/null
+++ b/lib/src/equality.dart
@@ -0,0 +1,126 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+import 'package:yaml/yaml.dart';
+
+/// Creates a map that uses our custom [deepEquals] and [deepHashCode] functions
+/// to determine equality.
+Map<K, V> deepEqualsMap<K, V>() =>
+    LinkedHashMap(equals: deepEquals, hashCode: deepHashCode);
+
+/// Compares two [Object]s for deep equality. This implementation differs from
+/// `package:yaml`'s deep equality notation by allowing for comparison of
+/// non-scalar map keys.
+bool deepEquals(dynamic obj1, dynamic obj2) {
+  if (obj1 is YamlNode) obj1 = obj1.value;
+  if (obj2 is YamlNode) obj2 = obj2.value;
+
+  if (obj1 is Map && obj2 is Map) {
+    return mapDeepEquals(obj1, obj2);
+  }
+
+  if (obj1 is List && obj2 is List) {
+    return listDeepEquals(obj1, obj2);
+  }
+
+  return obj1 == obj2;
+}
+
+/// Compares two [List]s for deep equality.
+bool listDeepEquals(List list1, List list2) {
+  if (list1.length != list2.length) return false;
+
+  if (list1 is YamlList) list1 = (list1 as YamlList).nodes;
+  if (list2 is YamlList) list2 = (list2 as YamlList).nodes;
+
+  for (var i = 0; i < list1.length; i++) {
+    if (!deepEquals(list1[i], list2[i])) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+/// Compares two [Map]s for deep equality. Differs from `package:yaml`'s deep
+/// equality notation by allowing for comparison of non-scalar map keys.
+bool mapDeepEquals(Map map1, Map map2) {
+  if (map1.length != map2.length) return false;
+
+  if (map1 is YamlList) map1 = (map1 as YamlMap).nodes;
+  if (map2 is YamlList) map2 = (map2 as YamlMap).nodes;
+
+  return map1.keys.every((key) {
+    if (!containsKey(map2, key)) return false;
+
+    /// Because two keys may be equal by deep equality but using one key on the
+    /// other map might not get a hit since they may not be both using our
+    /// [deepEqualsMap].
+    final key2 = getKey(map2, key);
+
+    if (!deepEquals(map1[key], map2[key2])) {
+      return false;
+    }
+
+    return true;
+  });
+}
+
+/// Returns a hashcode for [value] such that structures that are equal by
+/// [deepEquals] will have the same hash code.
+int deepHashCode(Object value) {
+  if (value is Map) {
+    const equality = UnorderedIterableEquality();
+    return equality.hash(value.keys.map(deepHashCode)) ^
+        equality.hash(value.values.map(deepHashCode));
+  } else if (value is Iterable) {
+    return const IterableEquality().hash(value.map(deepHashCode));
+  } else if (value is YamlScalar) {
+    return value.value.hashCode;
+  }
+
+  return value.hashCode;
+}
+
+/// 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;
+}
+
+/// Returns the [YamlNode] after the [YamlNode] corresponding to the provided
+/// [key].
+YamlNode getNextKeyNode(YamlMap map, Object key) {
+  final keyIterator = map.nodes.keys.iterator;
+  while (keyIterator.moveNext()) {
+    if (deepEquals(keyIterator.current, key) && keyIterator.moveNext()) {
+      return keyIterator.current;
+    }
+  }
+
+  return null;
+}
+
+/// Returns the key in [map] that is equal to the provided [key] by the notion
+/// of deep equality.
+Object getKey(Map map, Object key) {
+  return map.keys.firstWhere((k) => deepEquals(k, key));
+}
+
+/// Checks if [map] has any keys equal to the provided [key] by deep equality.
+bool containsKey(Map map, Object key) {
+  return map.keys.where((node) => deepEquals(node, key)).isNotEmpty;
+}
diff --git a/lib/src/errors.dart b/lib/src/errors.dart
new file mode 100644
index 0000000..83241cc
--- /dev/null
+++ b/lib/src/errors.dart
@@ -0,0 +1,78 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+/// Error thrown when a function is passed an invalid path.
+@sealed
+class PathError extends ArgumentError {
+  /// The full path that caused the error
+  final Iterable<Object> path;
+
+  /// The subpath that caused the error
+  final Iterable<Object> subPath;
+
+  /// The last element of [path] that could be traversed.
+  YamlNode parent;
+
+  PathError(this.path, this.subPath, this.parent, [String message])
+      : super.value(subPath, 'path', message);
+
+  PathError.unexpected(this.path, String message)
+      : subPath = path,
+        super(message);
+
+  @override
+  String toString() {
+    if (message == null) {
+      var errorMessage = 'Failed to traverse to subpath $subPath!';
+
+      if (subPath.isNotEmpty) {
+        errorMessage +=
+            ' Parent $parent does not contain key or index ${subPath.last}';
+      }
+
+      return 'Invalid path: $path. $errorMessage.';
+    }
+
+    return 'Invalid path: $path. $message';
+  }
+}
+
+/// Error thrown when the path contains an alias along the way.
+///
+/// When a path contains an aliased node, the behavior becomes less well-defined
+/// because we cannot be certain if the user wishes for the change to
+/// propagate throughout all the other aliased nodes, or if the user wishes
+/// for only that particular node to be modified. As such, [AliasError] reflects
+/// the detection that our change will impact an alias, and we do not intend
+/// on supporting such changes for the foreseeable future.
+@sealed
+class AliasError extends UnsupportedError {
+  /// The path that caused the error
+  final Iterable<Object> path;
+
+  /// The anchor node of the alias
+  final YamlNode anchor;
+
+  AliasError(this.path, this.anchor)
+      : super('Encountered an alias node along $path! '
+            'Alias nodes are nodes that refer to a previously serialized nodes, '
+            'and are denoted by either the "*" or the "&" indicators in the '
+            'original YAML. As the resulting behavior of mutations on these '
+            'nodes is not well-defined, the operation will not be supported '
+            'by this library.\n\n'
+            '${anchor.span.message('The alias was first defined here.')}');
+}
diff --git a/lib/src/list_mutations.dart b/lib/src/list_mutations.dart
new file mode 100644
index 0000000..3c8e361
--- /dev/null
+++ b/lib/src/list_mutations.dart
@@ -0,0 +1,312 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of setting the element at [index] to [newValue] when re-parsed.
+SourceEdit updateInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final currValue = list.nodes[index];
+  final offset = currValue.span.start.offset;
+  final yaml = yamlEdit.toString();
+  String valueString;
+
+  /// We do not use [_formatNewBlock] since we want to only replace the contents
+  /// of this node while preserving comments/whitespace, while [_formatNewBlock]
+  /// produces a string represnetation of a new node.
+  if (list.style == CollectionStyle.BLOCK) {
+    final listIndentation = getListIndentation(yaml, list);
+    final indentation = listIndentation + getIndentation(yamlEdit);
+    final lineEnding = getLineEnding(yaml);
+    valueString = yamlEncodeBlockString(
+        wrapAsYamlNode(newValue), indentation, lineEnding);
+
+    /// We prefer the compact nested notation for collections.
+    ///
+    /// By virtue of [yamlEncodeBlockString], 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 (isCollection(currValue) &&
+        getStyle(currValue) == CollectionStyle.BLOCK) {
+      valueString += lineEnding;
+    }
+
+    final end = getContentSensitiveEnd(currValue);
+
+    return SourceEdit(offset, end - offset, valueString);
+  } else {
+    valueString = yamlEncodeFlowString(newValue);
+    return SourceEdit(offset, currValue.span.length, valueString);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of appending [item] to the list.
+SourceEdit appendIntoList(YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  if (list.style == CollectionStyle.FLOW) {
+    return _appendToFlowList(yamlEdit, list, item);
+  } else {
+    return _appendToBlockList(yamlEdit, list, item);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] to the list at [index].
+SourceEdit insertInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  /// We call the append method if the user wants to append it to the end of the
+  /// list because appending requires different techniques.
+  if (index == list.length) {
+    return appendIntoList(yamlEdit, list, item);
+  } else {
+    if (list.style == CollectionStyle.FLOW) {
+      return _insertInFlowList(yamlEdit, list, index, item);
+    } else {
+      return _insertInBlockList(yamlEdit, list, index, item);
+    }
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing the element at [index] when re-parsed.
+SourceEdit removeInList(YamlEditor yamlEdit, YamlList list, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final nodeToRemove = list.nodes[index];
+
+  if (list.style == CollectionStyle.FLOW) {
+    return _removeFromFlowList(yamlEdit, list, nodeToRemove, index);
+  } else {
+    return _removeFromBlockList(yamlEdit, list, nodeToRemove, index);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of addition [item] into [nodes], noting that this is a flow list.
+SourceEdit _appendToFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final valueString = _formatNewFlow(list, item, true);
+  return SourceEdit(list.span.end.offset - 1, 0, valueString);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of addition [item] into [nodes], noting that this is a block list.
+SourceEdit _appendToBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  var formattedValue = _formatNewBlock(yamlEdit, list, item);
+  final yaml = yamlEdit.toString();
+  var offset = list.span.end.offset;
+
+  // Adjusts offset to after the trailing newline of the last entry, if it exists
+  if (list.isNotEmpty) {
+    final lastValueSpanEnd = list.nodes.last.span.end.offset;
+    final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd);
+    if (nextNewLineIndex == -1) {
+      formattedValue = getLineEnding(yaml) + formattedValue;
+    } else {
+      offset = nextNewLineIndex + 1;
+    }
+  }
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Formats [item] into a new node for block lists.
+String _formatNewBlock(YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final yaml = yamlEdit.toString();
+  final listIndentation = getListIndentation(yaml, list);
+  final newIndentation = listIndentation + getIndentation(yamlEdit);
+  final lineEnding = getLineEnding(yaml);
+
+  var valueString = yamlEncodeBlockString(item, newIndentation, lineEnding);
+  if (isCollection(item) && !isFlowYamlCollectionNode(item) && !isEmpty(item)) {
+    valueString = valueString.substring(newIndentation);
+  }
+  final indentedHyphen = ' ' * listIndentation + '- ';
+
+  return '$indentedHyphen$valueString$lineEnding';
+}
+
+/// Formats [item] into a new node for flow lists.
+String _formatNewFlow(YamlList list, YamlNode item, [bool isLast = false]) {
+  ArgumentError.checkNotNull(list, 'list');
+  ArgumentError.checkNotNull(isLast, 'isLast');
+
+  var valueString = yamlEncodeFlowString(item);
+  if (list.isNotEmpty) {
+    if (isLast) {
+      valueString = ', $valueString';
+    } else {
+      valueString += ', ';
+    }
+  }
+
+  return valueString;
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] into [nodes] at [index], noting that this is
+/// a block list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _insertInBlockList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToBlockList(yamlEdit, list, item);
+
+  final formattedValue = _formatNewBlock(yamlEdit, list, item);
+
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  final yaml = yamlEdit.toString();
+  final start = yaml.lastIndexOf('\n', currNodeStart) + 1;
+
+  return SourceEdit(start, 0, formattedValue);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] into [nodes] at [index], noting that this is
+/// a flow list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _insertInFlowList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToFlowList(yamlEdit, list, item);
+
+  final formattedValue = _formatNewFlow(list, item);
+
+  final yaml = yamlEdit.toString();
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  var start = yaml.lastIndexOf(RegExp(r',|\['), currNodeStart - 1) + 1;
+  if (yaml[start] == ' ') start++;
+
+  return SourceEdit(start, 0, formattedValue);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing [nodeToRemove] from [nodes], noting that this is a
+/// block list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _removeFromBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  /// If we are removing the last element in a block list, convert it into a
+  /// flow empty list.
+  if (list.length == 1) {
+    final start = list.span.start.offset;
+    final end = getContentSensitiveEnd(nodeToRemove);
+
+    return SourceEdit(start, end - start, '[]');
+  }
+
+  final yaml = yamlEdit.toString();
+  final span = nodeToRemove.span;
+
+  /// The general removal strategy is to remove everything that starts from
+  /// [nodeToRemove]'s dash to the next node's dash.
+  ///
+  /// -1 accounts for the fact that the content can start with a dash
+  var start = yaml.lastIndexOf('-', span.start.offset - 1);
+  var end = yaml.lastIndexOf('\n', list.span.end.offset) + 1;
+
+  if (index < list.length - 1) {
+    final nextNode = list.nodes[index + 1];
+    end = yaml.lastIndexOf('-', nextNode.span.start.offset - 1);
+  } else {
+    /// If there is a possibility that there is a `-` or `\n` before the node
+    if (start > 0) {
+      final lastHyphen = yaml.lastIndexOf('-', start - 1);
+      final lastNewLine = yaml.lastIndexOf('\n', start - 1);
+      if (lastHyphen > lastNewLine) {
+        start = lastHyphen + 2;
+      } else if (lastNewLine > lastHyphen) {
+        start = lastNewLine + 1;
+      }
+    }
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing [nodeToRemove] from [nodes], noting that this is a
+/// flow list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _removeFromFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final span = nodeToRemove.span;
+  final yaml = yamlEdit.toString();
+  var start = span.start.offset;
+  var end = span.end.offset;
+
+  if (index == 0) {
+    start = yaml.lastIndexOf('[', start - 1) + 1;
+    if (index == list.length - 1) {
+      end = yaml.indexOf(']', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/lib/src/map_mutations.dart b/lib/src/map_mutations.dart
new file mode 100644
index 0000000..b0dec1f
--- /dev/null
+++ b/lib/src/map_mutations.dart
@@ -0,0 +1,258 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'equality.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Performs the string operation on [yaml] to achieve the effect of setting
+/// the element at [key] to [newValue] when re-parsed.
+SourceEdit updateInMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (!containsKey(map, key)) {
+    final keyNode = wrapAsYamlNode(key);
+
+    if (map.style == CollectionStyle.FLOW) {
+      return _addToFlowMap(yamlEdit, map, keyNode, newValue);
+    } else {
+      return _addToBlockMap(yamlEdit, map, keyNode, newValue);
+    }
+  } else {
+    if (map.style == CollectionStyle.FLOW) {
+      return _replaceInFlowMap(yamlEdit, map, key, newValue);
+    } else {
+      return _replaceInBlockMap(yamlEdit, map, key, newValue);
+    }
+  }
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the element at [key] when re-parsed.
+SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object key) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (!containsKey(map, key)) return null;
+
+  final keyNode = getKeyNode(map, key);
+  final valueNode = map.nodes[keyNode];
+
+  if (map.style == CollectionStyle.FLOW) {
+    return _removeFromFlowMap(yamlEdit, map, keyNode, valueNode);
+  } else {
+    return _removeFromBlockMap(yamlEdit, map, keyNode, valueNode);
+  }
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of adding
+/// the [key]:[newValue] pair when reparsed, bearing in mind that this is a
+/// block map.
+SourceEdit _addToBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final yaml = yamlEdit.toString();
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+  final keyString = yamlEncodeFlowString(wrapAsYamlNode(key));
+  final lineEnding = getLineEnding(yaml);
+
+  var valueString = yamlEncodeBlockString(newValue, newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    valueString = '$lineEnding$valueString';
+  }
+
+  var formattedValue = ' ' * getMapIndentation(yaml, map) + '$keyString: ';
+  var offset = map.span.end.offset;
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (map.isNotEmpty) {
+    /// Adjusts offset to after the trailing newline of the last entry, if it
+    /// exists
+    if (insertionIndex == map.length) {
+      final lastValueSpanEnd = getContentSensitiveEnd(map.nodes.values.last);
+      final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd);
+
+      if (nextNewLineIndex != -1) {
+        offset = nextNewLineIndex + 1;
+      } else {
+        formattedValue = lineEnding + formattedValue;
+      }
+    } else {
+      final keyAtIndex = map.nodes.keys.toList()[insertionIndex] as YamlNode;
+      final keySpanStart = keyAtIndex.span.start.offset;
+      final prevNewLineIndex = yaml.lastIndexOf('\n', keySpanStart);
+
+      offset = prevNewLineIndex + 1;
+    }
+  }
+
+  formattedValue += valueString + lineEnding;
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of adding
+/// the [key]:[newValue] pair when reparsed, bearing in mind that this is a flow
+/// map.
+SourceEdit _addToFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final keyString = yamlEncodeFlowString(keyNode);
+  final valueString = yamlEncodeFlowString(newValue);
+
+  // The -1 accounts for the closing bracket.
+  if (map.isEmpty) {
+    return SourceEdit(map.span.end.offset - 1, 0, '$keyString: $valueString');
+  }
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (insertionIndex == map.length) {
+    return SourceEdit(map.span.end.offset - 1, 0, ', $keyString: $valueString');
+  }
+
+  final insertionOffset =
+      (map.nodes.keys.toList()[insertionIndex] as YamlNode).span.start.offset;
+
+  return SourceEdit(insertionOffset, 0, '$keyString: $valueString, ');
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of replacing
+/// the value at [key] with [newValue] when reparsed, bearing in mind that this
+/// is a block map.
+SourceEdit _replaceInBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final yaml = yamlEdit.toString();
+  final lineEnding = getLineEnding(yaml);
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+
+  final keyNode = getKeyNode(map, key);
+  var valueAsString = yamlEncodeBlockString(
+      wrapAsYamlNode(newValue), newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    valueAsString = lineEnding + valueAsString;
+  }
+
+  /// +2 accounts for the colon
+  final start = keyNode.span.end.offset + 1;
+  final end = getContentSensitiveEnd(map.nodes[key]);
+
+  return SourceEdit(start, end - start, ' ' + valueAsString);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of replacing
+/// the value at [key] with [newValue] when reparsed, bearing in mind that this
+/// is a flow map.
+SourceEdit _replaceInFlowMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final valueSpan = map.nodes[key].span;
+  final valueString = yamlEncodeFlowString(newValue);
+
+  return SourceEdit(valueSpan.start.offset, valueSpan.length, valueString);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the [key] from the map, bearing in mind that this is a block map.
+SourceEdit _removeFromBlockMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+  ArgumentError.checkNotNull(keyNode, 'keyNode');
+  ArgumentError.checkNotNull(valueNode, 'valueNode');
+
+  final keySpan = keyNode.span;
+  var end = getContentSensitiveEnd(valueNode);
+  final yaml = yamlEdit.toString();
+
+  if (map.length == 1) {
+    final start = map.span.start.offset;
+    return SourceEdit(start, end - start, '{}');
+  }
+
+  var start = keySpan.start.offset;
+
+  final nextNode = getNextKeyNode(map, keyNode);
+  if (nextNode == null) {
+    /// If there is a possibility that there is a `-` or `\n` before the node
+    if (start > 0) {
+      final lastHyphen = yaml.lastIndexOf('-', start - 1);
+      final lastNewLine = yaml.lastIndexOf('\n', start - 1);
+      if (lastHyphen > lastNewLine) {
+        start = lastHyphen + 2;
+      } else if (lastNewLine > lastHyphen) {
+        start = lastNewLine + 1;
+      }
+    }
+    final nextNewLine = yaml.indexOf('\n', end);
+    if (nextNewLine != -1) {
+      end = nextNewLine + 1;
+    }
+  } else {
+    end = nextNode.span.start.offset;
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the [key] from the map, bearing in mind that this is a flow map.
+SourceEdit _removeFromFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+  ArgumentError.checkNotNull(keyNode, 'keyNode');
+  ArgumentError.checkNotNull(valueNode, 'valueNode');
+
+  var start = keyNode.span.start.offset;
+  var end = valueNode.span.end.offset;
+  final yaml = yamlEdit.toString();
+
+  if (deepEquals(keyNode, map.keys.first)) {
+    start = yaml.lastIndexOf('{', start - 1) + 1;
+
+    if (deepEquals(keyNode, map.keys.last)) {
+      end = yaml.indexOf('}', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/lib/src/source_edit.dart b/lib/src/source_edit.dart
new file mode 100644
index 0000000..aa8c1d3
--- /dev/null
+++ b/lib/src/source_edit.dart
@@ -0,0 +1,153 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:meta/meta.dart';
+
+/// A class representing a change on a [String], intended to be compatible with
+/// `package:analysis_server`'s [SourceEdit].
+///
+/// For example, changing a string from
+/// ```
+/// foo: foobar
+/// ```
+/// to
+/// ```
+/// foo: barbar
+/// ```
+/// will be represented by `SourceEdit(offset: 4, length: 3, replacement: 'bar')`
+@sealed
+class SourceEdit {
+  /// The offset from the start of the string where the modification begins.
+  final int offset;
+
+  /// The length of the substring to be replaced.
+  final int length;
+
+  /// The replacement string to be used.
+  final String replacement;
+
+  /// Creates a new [SourceEdit] instance. [offset], [length] and [replacement]
+  /// must be non-null, and [offset] and [length] must be non-negative.
+  factory SourceEdit(int offset, int length, String replacement) =>
+      SourceEdit._(offset, length, replacement);
+
+  SourceEdit._(this.offset, this.length, this.replacement) {
+    ArgumentError.checkNotNull(offset, 'offset');
+    ArgumentError.checkNotNull(length, 'length');
+    ArgumentError.checkNotNull(replacement, 'replacement');
+    RangeError.checkNotNegative(offset);
+    RangeError.checkNotNegative(length);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is SourceEdit) {
+      return offset == other.offset &&
+          length == other.length &&
+          replacement == other.replacement;
+    }
+
+    return false;
+  }
+
+  @override
+  int get hashCode => offset.hashCode ^ length.hashCode ^ replacement.hashCode;
+
+  /// Constructs a SourceEdit from JSON.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = {
+  ///   'offset': 1,
+  ///   'length': 2,
+  ///   'replacement': 'replacement string'
+  /// };
+  ///
+  /// final sourceEdit = SourceEdit.fromJson(edit);
+  /// ```
+  factory SourceEdit.fromJson(Map<String, dynamic> json) {
+    ArgumentError.checkNotNull(json, 'json');
+
+    if (json is Map) {
+      final offset = json['offset'];
+      final length = json['length'];
+      final replacement = json['replacement'];
+
+      if (offset is int && length is int && replacement is String) {
+        return SourceEdit(offset, length, replacement);
+      }
+    }
+    throw FormatException('Invalid JSON passed to SourceEdit');
+  }
+
+  /// Encodes this object as JSON-compatible structure.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// import 'dart:convert' show jsonEncode;
+  ///
+  /// final edit = SourceEdit(offset, length, 'replacement string');
+  /// final jsonString = jsonEncode(edit.toJson());
+  /// print(jsonString);
+  /// ```
+  Map<String, dynamic> toJson() {
+    return {'offset': offset, 'length': length, 'replacement': replacement};
+  }
+
+  @override
+  String toString() => 'SourceEdit($offset, $length, "$replacement")';
+
+  /// Applies a series of [SourceEdit]s to an original string, and return the
+  /// final output.
+  ///
+  /// [edits] should be in order i.e. the first [SourceEdit] in [edits] should
+  /// be the first edit applied to [original].
+  ///
+  /// **Example:**
+  /// ```dart
+  /// const original = 'YAML: YAML';
+  /// final sourceEdits = [
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(0, 4, "YAML Ain't Markup Language")
+  ///      ];
+  /// final result = SourceEdit.applyAll(original, sourceEdits);
+  /// ```
+  /// **Expected result:**
+  /// ```dart
+  /// "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup
+  /// Language"
+  /// ```
+  static String applyAll(String original, Iterable<SourceEdit> edits) {
+    ArgumentError.checkNotNull(original, 'original');
+    ArgumentError.checkNotNull(edits, 'edits');
+
+    return edits.fold(original, (current, edit) => edit.apply(current));
+  }
+
+  /// Applies one [SourceEdit]s to an original string, and return the final
+  /// output.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = SourceEdit(4, 3, 'bar');
+  /// final originalString = 'foo: foobar';
+  /// print(edit.apply(originalString)); // 'foo: barbar'
+  /// ```
+  String apply(String original) {
+    ArgumentError.checkNotNull(original, 'original');
+
+    return original.replaceRange(offset, offset + length, replacement);
+  }
+}
diff --git a/lib/src/strings.dart b/lib/src/strings.dart
new file mode 100644
index 0000000..6f7a155
--- /dev/null
+++ b/lib/src/strings.dart
@@ -0,0 +1,321 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml/yaml.dart';
+import 'utils.dart';
+
+/// Given [value], tries to format it into a plain string recognizable by YAML.
+/// If it fails, it defaults to returning a double-quoted string.
+///
+/// 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').
+String _tryYamlEncodePlain(Object value) {
+  if (value is YamlNode) {
+    AssertionError(
+        'YamlNodes should not be passed directly into getSafeString!');
+  }
+
+  assertValidScalar(value);
+
+  if (value is String) {
+    /// 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
+    if (isDangerousString(value)) {
+      return _yamlEncodeDoubleQuoted(value);
+    }
+
+    return value;
+  }
+
+  return value.toString();
+}
+
+/// Checks if [string] has unprintable characters according to
+/// [unprintableCharCodes].
+bool _hasUnprintableCharacters(String string) {
+  ArgumentError.checkNotNull(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) {
+  ArgumentError.checkNotNull(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"';
+}
+
+/// Generates a YAML-safe single-quoted string. Automatically escapes
+/// single-quotes.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeSingleQuoted(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  final result = string.replaceAll('\'', '\'\'');
+  return '\'$result\'';
+}
+
+/// Generates a YAML-safe folded string.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(string, 'string');
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  String result;
+
+  final trimmedString = string.trimRight();
+  final removedPortion = string.substring(trimmedString.length);
+
+  if (removedPortion.contains('\n')) {
+    result = '>+\n' + ' ' * indentation;
+  } else {
+    result = '>-\n' + ' ' * indentation;
+  }
+
+  /// Duplicating the newline for folded strings preserves it in YAML.
+  /// Assumes the user did not try to account for windows documents by using
+  /// `\r\n` already
+  return result +
+      trimmedString.replaceAll('\n', lineEnding * 2 + ' ' * indentation) +
+      removedPortion;
+}
+
+/// Generates a YAML-safe literal string.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeLiteral(
+    String string, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(string, 'string');
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  final result = '|-\n$string';
+
+  /// Assumes the user did not try to account for windows documents by using
+  /// `\r\n` already
+  return result.replaceAll('\n', lineEnding + ' ' * indentation);
+}
+
+/// Returns [value] with the necessary formatting applied in a flow context
+/// if possible.
+///
+/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
+/// possible. Certain cases make this impossible (e.g. a plain string scalar that
+/// starts with '>'), in which case we will produce [value] with default styling
+/// options.
+String _yamlEncodeFlowScalar(YamlNode value) {
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+
+    if (value.value is String) {
+      if (_hasUnprintableCharacters(value.value) ||
+          value.style == ScalarStyle.DOUBLE_QUOTED) {
+        return _yamlEncodeDoubleQuoted(value.value);
+      }
+
+      if (value.style == ScalarStyle.SINGLE_QUOTED) {
+        return _tryYamlEncodeSingleQuoted(value.value);
+      }
+    }
+
+    return _tryYamlEncodePlain(value.value);
+  }
+
+  assertValidScalar(value);
+  return _tryYamlEncodePlain(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a block context
+/// if possible.
+///
+/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
+/// possible. Certain cases make this impossible (e.g. a folded string scalar
+/// 'null'), in which case we will produce [value] with default styling
+/// options.
+String yamlEncodeBlockScalar(
+    YamlNode value, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+
+    if (value.value is String) {
+      if (_hasUnprintableCharacters(value.value)) {
+        return _yamlEncodeDoubleQuoted(value.value);
+      }
+
+      if (value.style == ScalarStyle.SINGLE_QUOTED) {
+        return _tryYamlEncodeSingleQuoted(value.value);
+      }
+
+      // Strings with only white spaces will cause a misparsing
+      if (value.value.trim().length == value.value.length &&
+          value.value.length != 0) {
+        if (value.style == ScalarStyle.FOLDED) {
+          return _tryYamlEncodeFolded(value.value, indentation, lineEnding);
+        }
+
+        if (value.style == ScalarStyle.LITERAL) {
+          return _tryYamlEncodeLiteral(value.value, indentation, lineEnding);
+        }
+      }
+    }
+
+    return _tryYamlEncodePlain(value.value);
+  }
+
+  assertValidScalar(value);
+
+  /// The remainder of the possibilities are similar to how [getFlowScalar]
+  /// treats [value].
+  return _yamlEncodeFlowScalar(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a flow context.
+///
+/// If [value] is a [YamlNode], we try to respect its [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 yamlEncodeFlowString(YamlNode value) {
+  if (value is YamlList) {
+    final list = value.nodes;
+
+    final safeValues = list.map(yamlEncodeFlowString);
+    return '[' + safeValues.join(', ') + ']';
+  } else if (value is YamlMap) {
+    final safeEntries = value.nodes.entries.map((entry) {
+      final safeKey = yamlEncodeFlowString(entry.key);
+      final safeValue = yamlEncodeFlowString(entry.value);
+      return '$safeKey: $safeValue';
+    });
+
+    return '{' + safeEntries.join(', ') + '}';
+  }
+
+  return _yamlEncodeFlowScalar(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a block context.
+///
+/// If [value] is a [YamlNode], we respect its [style] parameter.
+String yamlEncodeBlockString(
+    YamlNode value, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  const additionalIndentation = 2;
+
+  if (!isBlockNode(value)) return yamlEncodeFlowString(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 =
+          yamlEncodeBlockString(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 safeKey = yamlEncodeFlowString(entry.key);
+      final formattedKey = ' ' * indentation + safeKey;
+      final formattedValue =
+          yamlEncodeBlockString(entry.value, newIndentation, lineEnding);
+
+      if (isCollection(entry.value)) {
+        return formattedKey + ':\n' + formattedValue;
+      }
+
+      return formattedKey + ': ' + formattedValue;
+    }).join(lineEnding);
+  }
+
+  return yamlEncodeBlockScalar(value, 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. In particular, \x32 is not included because it
+/// can be processed normally.
+///
+/// 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).
+};
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..e5572e6
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,279 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+
+/// Determines if [string] is dangerous by checking if parsing the plain string can
+/// return a result different from [string].
+///
+/// This function is also capable of detecting if non-printable characters are in
+/// [string].
+bool isDangerousString(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  try {
+    if (loadYamlNode(string).value != string) {
+      return true;
+    }
+
+    /// [string] should also not contain the `[`, `]`, `,`, `{` and `}` indicator characters.
+    return string.contains(RegExp(r'\{|\[|\]|\}|,'));
+  } catch (e) {
+    /// This catch statement catches [ArgumentError] in `loadYamlNode` when
+    /// a string can be interpreted as a URI tag, but catches for other
+    /// [YamlException]s
+    return true;
+  }
+}
+
+/// Asserts that [value] is a valid scalar according to YAML.
+///
+/// A valid scalar is a number, String, boolean, or null.
+void assertValidScalar(Object value) {
+  if (value is num || value is String || value is bool || value == null) {
+    return;
+  }
+
+  throw ArgumentError.value(value, 'value', 'Not a valid scalar type!');
+}
+
+/// Checks if [node] is a [YamlNode] with block styling.
+///
+/// [ScalarStyle.ANY] and [CollectionStyle.ANY] are considered to be block styling
+/// by default for maximum flexibility.
+bool isBlockNode(YamlNode node) {
+  ArgumentError.checkNotNull(node, 'node');
+
+  if (node is YamlScalar) {
+    if (node.style == ScalarStyle.LITERAL ||
+        node.style == ScalarStyle.FOLDED ||
+        node.style == ScalarStyle.ANY) {
+      return true;
+    }
+  }
+
+  if (node is YamlList &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) return true;
+  if (node is YamlMap &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) return true;
+
+  return false;
+}
+
+/// Returns the content sensitive ending offset of [yamlNode] (i.e. where the last
+/// meaningful content happens)
+int getContentSensitiveEnd(YamlNode yamlNode) {
+  ArgumentError.checkNotNull(yamlNode, 'yamlNode');
+
+  if (yamlNode is YamlList) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.last);
+    }
+  } else if (yamlNode is YamlMap) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.values.last);
+    }
+  }
+
+  return yamlNode.span.end.offset;
+}
+
+/// Checks if the item is a Map or a List
+bool isCollection(Object item) => item is Map || item is List;
+
+/// Checks if [index] is [int], >=0, < [length]
+bool isValidIndex(Object index, int length) {
+  return index is int && index >= 0 && index < length;
+}
+
+/// Checks if the item is empty, if it is a List or a Map.
+///
+/// Returns `false` if [item] is not a List or Map.
+bool isEmpty(Object item) {
+  if (item is Map) return item.isEmpty;
+  if (item is List) return item.isEmpty;
+
+  return false;
+}
+
+/// Creates a [SourceSpan] from [sourceUrl] with no meaningful location
+/// information.
+///
+/// Mainly used with [wrapAsYamlNode] to allow for a reasonable
+/// implementation of [SourceSpan.message].
+SourceSpan shellSpan(Object sourceUrl) {
+  final shellSourceLocation = SourceLocation(0, sourceUrl: sourceUrl);
+  return SourceSpanBase(shellSourceLocation, shellSourceLocation, '');
+}
+
+/// Returns if [value] is a [YamlList] or [YamlMap] with [CollectionStyle.FLOW].
+bool isFlowYamlCollectionNode(Object value) {
+  if (value is YamlList || value is YamlMap) {
+    return (value as dynamic).style == CollectionStyle.FLOW;
+  }
+
+  return false;
+}
+
+/// Determines the index where [newKey] will be inserted if the keys in [map] are in
+/// alphabetical order when converted to strings.
+///
+/// Returns the length of [map] if the keys in [map] are not in alphabetical order.
+int getMapInsertionIndex(YamlMap map, Object newKey) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  final keys = map.nodes.keys.map((k) => k.toString()).toList();
+
+  for (var i = 1; i < keys.length; i++) {
+    if (keys[i].compareTo(keys[i - 1]) < 0) {
+      return map.length;
+    }
+  }
+
+  final insertionIndex = keys.indexWhere((key) => key.compareTo(newKey) > 0);
+
+  if (insertionIndex != -1) return insertionIndex;
+
+  return map.length;
+}
+
+/// Returns the [style] property of [target], if it is a [YamlNode]. Otherwise return null.
+Object getStyle(Object target) {
+  if (target is YamlNode) {
+    return (target as dynamic).style;
+  }
+
+  return null;
+}
+
+/// Returns the detected indentation step used in [yaml], or
+/// defaults to a value of `2` if no indentation step can be detected.
+///
+/// Indentation step is determined by the difference in indentation of the
+/// first block-styled yaml collection in the second level as compared to the
+/// top-level elements. In the case where there are multiple possible
+/// candidates, we choose the candidate closest to the start of [yaml].
+int getIndentation(YamlEditor editor) {
+  final node = editor.parseAt([]);
+  Iterable<YamlNode> children;
+  var indentation = 2;
+
+  if (node is YamlMap && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes.values;
+  } else if (node is YamlList && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes;
+  }
+
+  if (children != null) {
+    for (final child in children) {
+      var indent = 0;
+      if (child is YamlList) {
+        indent = getListIndentation(editor.toString(), child);
+      } else if (child is YamlMap) {
+        indent = getMapIndentation(editor.toString(), child);
+      }
+
+      if (indent != 0) indentation = indent;
+    }
+  }
+  return indentation;
+}
+
+/// Gets the indentation level of [list]. This is 0 if it is a flow list,
+/// but returns the number of spaces before the hyphen of elements for
+/// block lists.
+///
+/// Throws [UnsupportedError] if an empty block map is passed in.
+int getListIndentation(String yaml, YamlList list) {
+  ArgumentError.checkNotNull(list, 'list');
+
+  if (list.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (list.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block list');
+  }
+
+  final lastSpanOffset = list.nodes.last.span.start.offset;
+  final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset - 1);
+  final lastHyphen = yaml.lastIndexOf('-', lastSpanOffset - 1);
+
+  if (lastNewLine == -1) return lastHyphen;
+
+  return lastHyphen - lastNewLine - 1;
+}
+
+/// Gets the indentation level of [map]. This is 0 if it is a flow map,
+/// but returns the number of spaces before the keys for block maps.
+int getMapIndentation(String yaml, YamlMap map) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (map.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (map.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block map');
+  }
+
+  /// Use the number of spaces between the last key and the newline as
+  /// indentation.
+  final lastKey = map.nodes.keys.last as YamlNode;
+  final lastSpanOffset = lastKey.span.start.offset;
+  final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset);
+  final lastQuestionMark = yaml.lastIndexOf('?', lastSpanOffset);
+
+  if (lastQuestionMark == -1) {
+    if (lastNewLine == -1) return lastSpanOffset;
+    return lastSpanOffset - lastNewLine - 1;
+  }
+
+  /// If there is a question mark, it might be a complex key. Check if it
+  /// is on the same line as the key node to verify.
+  if (lastNewLine == -1) return lastQuestionMark;
+  if (lastQuestionMark > lastNewLine) {
+    return lastQuestionMark - lastNewLine - 1;
+  }
+
+  return lastSpanOffset - lastNewLine - 1;
+}
+
+/// Returns the detected line ending used in [yaml], more specifically, whether
+/// [yaml] appears to use Windows `\r\n` or Unix `\n` line endings.
+///
+/// The heuristic used is to count all `\n` in the text and if stricly more
+/// than half of them are preceded by `\r` we report that windows line endings
+/// are used.
+String getLineEnding(String yaml) {
+  var index = -1;
+  var unixNewlines = 0;
+  var windowsNewlines = 0;
+  while ((index = yaml.indexOf('\n', index + 1)) != -1) {
+    if (index != 0 && yaml[index - 1] == '\r') {
+      windowsNewlines++;
+    } else {
+      unixNewlines++;
+    }
+  }
+
+  return windowsNewlines > unixNewlines ? '\r\n' : '\n';
+}
diff --git a/lib/src/wrap.dart b/lib/src/wrap.dart
new file mode 100644
index 0000000..76ea452
--- /dev/null
+++ b/lib/src/wrap.dart
@@ -0,0 +1,195 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:collection' as collection;
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'utils.dart';
+
+/// Returns a new [YamlMap] constructed by applying [update] onto the [nodes]
+/// of this [YamlMap].
+YamlMap updatedYamlMap(YamlMap map, Function(Map) update) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  final dummyMap = deepEqualsMap();
+  dummyMap.addAll(map.nodes);
+
+  update(dummyMap);
+
+  return wrapAsYamlNode(dummyMap);
+}
+
+/// Wraps [value] into a [YamlNode].
+///
+/// [Map]s, [List]s and Scalars will be wrapped as [YamlMap]s, [YamlList]s,
+/// and [YamlScalar]s respectively. If [collectionStyle]/[scalarStyle] is
+/// defined, and [value] is a collection or scalar, the wrapped [YamlNode] will
+/// have the respective style, otherwise it defaults to the ANY style.
+///
+/// If a [YamlNode] is passed in, no further wrapping will be done, and the
+/// [collectionStyle]/[scalarStyle] will not be applied.
+YamlNode wrapAsYamlNode(Object value,
+    {CollectionStyle collectionStyle = CollectionStyle.ANY,
+    ScalarStyle scalarStyle = ScalarStyle.ANY}) {
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+    return value;
+  } else if (value is YamlList) {
+    for (final item in value.nodes) {
+      wrapAsYamlNode(item);
+    }
+
+    return value;
+  } else if (value is YamlMap) {
+    /// Both [entry.key] and [entry.values] are guaranteed to be [YamlNode]s,
+    /// so running this will just assert that they are valid scalars.
+    for (final entry in value.nodes.entries) {
+      wrapAsYamlNode(entry.key);
+      wrapAsYamlNode(entry.value);
+    }
+
+    return value;
+  } else if (value is Map) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+    return YamlMapWrap(value, collectionStyle: collectionStyle);
+  } else if (value is List) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+    return YamlListWrap(value, collectionStyle: collectionStyle);
+  } else {
+    assertValidScalar(value);
+
+    ArgumentError.checkNotNull(scalarStyle, 'scalarStyle');
+    return YamlScalarWrap(value, style: scalarStyle);
+  }
+}
+
+/// Internal class that allows us to define a constructor on [YamlScalar]
+/// which takes in [style] as an argument.
+class YamlScalarWrap implements YamlScalar {
+  /// The [ScalarStyle] to be used for the scalar.
+  @override
+  final ScalarStyle style;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final dynamic value;
+
+  YamlScalarWrap(this.value, {this.style = ScalarStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl) {
+    ArgumentError.checkNotNull(style, 'scalarStyle');
+  }
+
+  @override
+  String toString() => value.toString();
+}
+
+/// Internal class that allows us to define a constructor on [YamlMap]
+/// which takes in [style] as an argument.
+class YamlMapWrap
+    with collection.MapMixin, UnmodifiableMapMixin
+    implements YamlMap {
+  /// The [CollectionStyle] to be used for the map.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final Map<dynamic, YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  factory YamlMapWrap(Map dartMap,
+      {CollectionStyle collectionStyle = CollectionStyle.ANY,
+      Object sourceUrl}) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+
+    final wrappedMap = deepEqualsMap<dynamic, YamlNode>();
+
+    for (final entry in dartMap.entries) {
+      final wrappedKey = wrapAsYamlNode(entry.key);
+      final wrappedValue = wrapAsYamlNode(entry.value);
+      wrappedMap[wrappedKey] = wrappedValue;
+    }
+
+    return YamlMapWrap._(wrappedMap,
+        style: collectionStyle, sourceUrl: sourceUrl);
+  }
+
+  YamlMapWrap._(this.nodes,
+      {CollectionStyle style = CollectionStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](Object key) => nodes[key]?.value;
+
+  @override
+  Iterable get keys => nodes.keys.map((node) => node.value);
+
+  @override
+  Map get value => this;
+}
+
+/// Internal class that allows us to define a constructor on [YamlList]
+/// which takes in [style] as an argument.
+class YamlListWrap with collection.ListMixin implements YamlList {
+  /// The [CollectionStyle] to be used for the list.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final List<YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  int get length => nodes.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  factory YamlListWrap(List dartList,
+      {CollectionStyle collectionStyle = CollectionStyle.ANY,
+      Object sourceUrl}) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+
+    final wrappedList = dartList.map(wrapAsYamlNode).toList();
+    return YamlListWrap._(wrappedList,
+        style: collectionStyle, sourceUrl: sourceUrl);
+  }
+
+  YamlListWrap._(this.nodes,
+      {CollectionStyle style = CollectionStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](int index) => nodes[index].value;
+
+  @override
+  operator []=(int index, value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  @override
+  List get value => this;
+}
diff --git a/lib/yaml_edit.dart b/lib/yaml_edit.dart
new file mode 100644
index 0000000..2ff8049
--- /dev/null
+++ b/lib/yaml_edit.dart
@@ -0,0 +1,37 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// YAML parsing is supported by `package:yaml`, and each time a change is
+/// made, the resulting YAML AST is compared against our expected output
+/// with deep equality to ensure that the output conforms to our expectations.
+///
+/// **Example**
+/// ```dart
+/// import 'package:yaml_edit/yaml_edit.dart';
+///
+/// void main() {
+///  final yamlEditor = YamlEditor('{YAML: YAML}');
+///  yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+///  print(yamlEditor);
+///  // Expected Output:
+///  // {YAML: YAML Ain't Markup Language}
+/// }
+/// ```
+///
+/// [1]: https://yaml.org/
+library yaml_edit;
+
+export 'src/editor.dart';
+export 'src/source_edit.dart';
+export 'src/wrap.dart' show wrapAsYamlNode;
diff --git a/mono_pkg.yaml b/mono_pkg.yaml
new file mode 100644
index 0000000..db15a21
--- /dev/null
+++ b/mono_pkg.yaml
@@ -0,0 +1,8 @@
+dart:
+- stable
+stages:
+- analyze:
+  - dartanalyzer
+  - dartfmt
+- tests:
+  - test
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..42e314a
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,21 @@
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8
+  quiver_hashcode: ^2.0.0
+  yaml: ^2.2.1
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
diff --git a/test/alias_test.dart b/test/alias_test.dart
new file mode 100644
index 0000000..8f87d95
--- /dev/null
+++ b/test/alias_test.dart
@@ -0,0 +1,149 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+/// This test suite is a temporary measure until we are able to better handle
+/// aliases.
+void main() {
+  group('list ', () {
+    test('removing an alias anchor results in AliasError', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+''');
+      expect(() => doc.remove([0]), throwsAliasError);
+    });
+
+    test('removing an alias reference results in AliasError', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+''');
+
+      expect(() => doc.remove([1]), throwsAliasError);
+    });
+
+    test('it is okay to remove a non-alias node', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- *SS
+- Sammy Sosa
+''');
+
+      doc.remove([2]);
+      expect(doc.toString(), equals('''
+- &SS Sammy Sosa
+- *SS
+'''));
+    });
+  });
+
+  group('map', () {
+    test('removing an alias anchor value results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['a']), throwsAliasError);
+    });
+
+    test('removing an alias reference value results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['b']), throwsAliasError);
+    });
+
+    test('removing an alias anchor key results in AliasError', () {
+      final doc = YamlEditor('''
+&SS Sammy Sosa: a
+b: *SS
+''');
+
+      expect(() => doc.remove(['Sammy Sosa']), throwsAliasError);
+    });
+
+    test('removing an alias reference key results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+*SS : b
+''');
+
+      expect(() => doc.remove(['Sammy Sosa']), throwsAliasError);
+    });
+
+    test('it is okay to remove a non-alias node', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: *SS
+c: Sammy Sosa
+''');
+
+      doc.remove(['c']);
+      expect(doc.toString(), equals('''
+a: &SS Sammy Sosa
+b: *SS
+'''));
+    });
+  });
+
+  group('nested alias', () {
+    test('nested list alias anchors are detected too', () {
+      final doc = YamlEditor('''
+- 
+  - &SS Sammy Sosa
+- *SS
+''');
+
+      expect(() => doc.remove([0]), throwsAliasError);
+    });
+
+    test('nested list alias references are detected too', () {
+      final doc = YamlEditor('''
+- &SS Sammy Sosa
+- 
+  - *SS
+''');
+
+      expect(() => doc.remove([1]), throwsAliasError);
+    });
+
+    test('removing nested map alias anchor results in AliasError', () {
+      final doc = YamlEditor('''
+a: 
+  c: &SS Sammy Sosa
+b: *SS
+''');
+
+      expect(() => doc.remove(['a']), throwsAliasError);
+    });
+
+    test('removing nested map alias reference results in AliasError', () {
+      final doc = YamlEditor('''
+a: &SS Sammy Sosa
+b: 
+  c: *SS
+''');
+
+      expect(() => doc.remove(['b']), throwsAliasError);
+    });
+  });
+}
diff --git a/test/append_test.dart b/test/append_test.dart
new file mode 100644
index 0000000..05eaba2
--- /dev/null
+++ b/test/append_test.dart
@@ -0,0 +1,157 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.appendToList([], 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.appendToList([], 4), throwsPathError);
+    });
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.appendToList([], 4);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+- 3
+- 4
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2, 3, 4]);
+    });
+
+    test('element to simple block list ', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.appendToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+- 3
+- - 4
+  - 5
+  - 6
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        1,
+        2,
+        3,
+        [4, 5, 6]
+      ]);
+    });
+
+    test('nested', () {
+      final doc = YamlEditor('''
+- 0
+- - 1
+  - 2
+''');
+      doc.appendToList([1], 3);
+      expect(doc.toString(), equals('''
+- 0
+- - 1
+  - 2
+  - 3
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        [1, 2, 3]
+      ]);
+    });
+
+    test('block list element to nested block list ', () {
+      final doc = YamlEditor('''
+- 0
+- - 1
+  - 2
+''');
+      doc.appendToList([1], [3, 4, 5]);
+
+      expect(doc.toString(), equals('''
+- 0
+- - 1
+  - 2
+  - - 3
+    - 4
+    - 5
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        [
+          1,
+          2,
+          [3, 4, 5]
+        ]
+      ]);
+    });
+
+    test('nested', () {
+      final yamlEditor = YamlEditor('''
+a:
+  1: 
+    - null
+  2: null
+''');
+      yamlEditor.appendToList(['a', 1], false);
+
+      expect(yamlEditor.toString(), equals('''
+a:
+  1: 
+    - null
+    - false
+  2: null
+'''));
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[0, 1, 2]');
+      doc.appendToList([], 3);
+      expect(doc.toString(), equals('[0, 1, 2, 3]'));
+      expectYamlBuilderValue(doc, [0, 1, 2, 3]);
+    });
+
+    test('empty ', () {
+      final doc = YamlEditor('[]');
+      doc.appendToList([], 0);
+      expect(doc.toString(), equals('[0]'));
+      expectYamlBuilderValue(doc, [0]);
+    });
+  });
+}
diff --git a/test/editor_test.dart b/test/editor_test.dart
new file mode 100644
index 0000000..4cdb205
--- /dev/null
+++ b/test/editor_test.dart
@@ -0,0 +1,66 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('YamlEditor records edits', () {
+    test('returns empty list at start', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+
+      expect(yamlEditor.edits, []);
+    });
+
+    test('after one change', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+
+      expect(
+          yamlEditor.edits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+    });
+
+    test('after multiple changes', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+      yamlEditor.update(['XML'], 'Extensible Markup Language');
+      yamlEditor.remove(['YAML']);
+
+      expect(yamlEditor.edits, [
+        SourceEdit(5, 5, " YAML Ain't Markup Language"),
+        SourceEdit(0, 0, 'XML: Extensible Markup Language\n'),
+        SourceEdit(32, 32, '')
+      ]);
+    });
+
+    test('that do not automatically update with internal list', () {
+      final yamlEditor = YamlEditor('YAML: YAML');
+      yamlEditor.update(['YAML'], "YAML Ain't Markup Language");
+
+      final firstEdits = yamlEditor.edits;
+
+      expect(firstEdits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+
+      yamlEditor.update(['XML'], 'Extensible Markup Language');
+      yamlEditor.remove(['YAML']);
+
+      expect(firstEdits, [SourceEdit(5, 5, " YAML Ain't Markup Language")]);
+      expect(yamlEditor.edits, [
+        SourceEdit(5, 5, " YAML Ain't Markup Language"),
+        SourceEdit(0, 0, 'XML: Extensible Markup Language\n'),
+        SourceEdit(32, 32, '')
+      ]);
+    });
+  });
+}
diff --git a/test/fuzz_test.dart b/test/fuzz_test.dart
new file mode 100644
index 0000000..1d35a15
--- /dev/null
+++ b/test/fuzz_test.dart
@@ -0,0 +1,308 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:math';
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:yaml_edit/src/wrap.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import 'problem_strings.dart';
+import 'test_utils.dart';
+
+/// Performs naive fuzzing on an initial YAML file based on an initial seed.
+///
+/// Starting with a template YAML, we randomly generate modifications and their
+/// inputs (boolean, null, strings, or numbers) to modify the YAML and assert
+/// that the change produced was expected.
+void main() {
+  const seed = 0;
+  final generator = _Generator(seed: seed);
+
+  const roundsOfTesting = 40;
+  const modificationsPerRound = 1000;
+
+  for (var i = 0; i < roundsOfTesting; i++) {
+    test('fuzz test $i', () {
+      final editor = YamlEditor('''
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 0.0.1-dev
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8
+  quiver_hashcode: ^2.0.0
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+''');
+
+      for (var j = 0; j < modificationsPerRound; j++) {
+        expect(
+            () => generator.performNextModification(editor), returnsNormally);
+      }
+    });
+  }
+}
+
+/// Generates the random variables we need for fuzzing.
+class _Generator {
+  final Random r;
+
+  /// 2^32
+  static const int maxInt = 4294967296;
+
+  /// Maximum depth of random YAML collection generated.
+  final int maxDepth;
+
+  _Generator({int seed, this.maxDepth = 5}) : r = Random(seed ?? 42);
+
+  int nextInt([int max = maxInt]) => r.nextInt(max);
+
+  double nextDouble() => r.nextDouble();
+
+  bool nextBool() => r.nextBool();
+
+  /// Generates a new string by individually generating characters and
+  /// appending them to a buffer. Currently only generates strings from
+  /// ascii 32 - 127.
+  String nextString() {
+    if (nextBool()) {
+      return problemStrings[nextInt(problemStrings.length)];
+    }
+
+    final length = nextInt(100);
+    final buffer = StringBuffer();
+
+    for (var i = 0; i < length; i++) {
+      final charCode = nextInt(95) + 32;
+      buffer.writeCharCode(charCode);
+    }
+
+    return buffer.toString();
+  }
+
+  /// Generates a new scalar recognizable by YAML.
+  Object nextScalar() {
+    final typeIndex = nextInt(5);
+
+    switch (typeIndex) {
+      case 0:
+        return nextBool();
+      case 1:
+        return nextDouble();
+      case 2:
+        return nextInt();
+      case 3:
+        return null;
+      default:
+        return nextString();
+    }
+  }
+
+  YamlScalar nextYamlScalar() {
+    return wrapAsYamlNode(nextScalar(), scalarStyle: nextScalarStyle());
+  }
+
+  /// Generates the next [YamlList], with the current [depth].
+  YamlList nextYamlList(int depth) {
+    final length = nextInt(9);
+    final list = [];
+
+    for (var i = 0; i < length; i++) {
+      list.add(nextYamlNode(depth + 1));
+    }
+
+    return wrapAsYamlNode(list, collectionStyle: nextCollectionStyle());
+  }
+
+  /// Generates the next [YamlList], with the current [depth].
+  YamlMap nextYamlMap(int depth) {
+    final length = nextInt(9);
+    final nodes = {};
+
+    for (var i = 0; i < length; i++) {
+      nodes[nextYamlNode(depth + 1)] = nextYamlScalar();
+    }
+
+    return wrapAsYamlNode(nodes, collectionStyle: nextCollectionStyle());
+  }
+
+  /// Returns a [YamlNode], with it being a [YamlScalar] 80% of the time, a
+  /// [YamlList] 10% of the time, and a [YamlMap] 10% of the time.
+  ///
+  /// If [depth] is greater than [maxDepth], we instantly return a [YamlScalar]
+  /// to prevent the parent from growing any further, to improve our speeds.
+  YamlNode nextYamlNode([int depth = 0]) {
+    if (depth >= maxDepth) {
+      return nextYamlScalar();
+    }
+
+    final roll = nextInt(10);
+
+    if (roll < 8) {
+      return nextYamlScalar();
+    } else if (roll == 8) {
+      return nextYamlList(depth);
+    } else {
+      return nextYamlMap(depth);
+    }
+  }
+
+  /// Performs a random modification
+  void performNextModification(YamlEditor editor) {
+    final path = findPath(editor);
+    final node = editor.parseAt(path);
+    final initialString = editor.toString();
+    final args = [];
+    var method = YamlModificationMethod.remove;
+
+    try {
+      if (node is YamlScalar) {
+        editor.remove(path);
+        return;
+      }
+
+      if (node is YamlList) {
+        final methodIndex = nextInt(YamlModificationMethod.values.length);
+        method = YamlModificationMethod.values[methodIndex];
+
+        switch (method) {
+          case YamlModificationMethod.remove:
+            editor.remove(path);
+            break;
+          case YamlModificationMethod.update:
+            if (node.isEmpty) break;
+            final index = nextInt(node.length);
+            args.add(nextYamlNode());
+            path.add(index);
+            editor.update(path, args[0]);
+            break;
+          case YamlModificationMethod.appendTo:
+            args.add(nextYamlNode());
+            editor.appendToList(path, args[0]);
+            break;
+          case YamlModificationMethod.prependTo:
+            args.add(nextYamlNode());
+            editor.prependToList(path, args[0]);
+            break;
+          case YamlModificationMethod.insert:
+            args.add(nextInt(node.length + 1));
+            args.add(nextYamlNode());
+            editor.insertIntoList(path, args[0], args[1]);
+            break;
+          case YamlModificationMethod.splice:
+            args.add(nextInt(node.length + 1));
+            args.add(nextInt(node.length + 1 - args[0]));
+            args.add(nextYamlList(0));
+            editor.spliceList(path, args[0], args[1], args[2]);
+            break;
+        }
+        return;
+      }
+
+      if (node is YamlMap) {
+        final replace = nextBool();
+        method = YamlModificationMethod.update;
+
+        if (replace && node.isNotEmpty) {
+          final keyList = node.keys.toList();
+          path.add(keyList[nextInt(keyList.length)]);
+        } else {
+          path.add(nextScalar());
+        }
+        final value = nextYamlNode();
+        args.add(value);
+        editor.update(path, value);
+        return;
+      }
+    } catch (error) {
+      print('''
+Failed to call $method on:
+$initialString
+with the following arguments:
+$args
+and path:
+$path
+
+Error Details:
+${error.message}
+''');
+      rethrow;
+    }
+
+    throw AssertionError('Got invalid node');
+  }
+
+  /// Obtains a random path by traversing [editor].
+  ///
+  /// At every node, we return the path to the node if the node has no children.
+  /// Otherwise, we return at a 50% chance, or traverse to one random child.
+  List<Object> findPath(YamlEditor editor) {
+    final path = [];
+
+    // 50% chance of stopping at the collection
+    while (nextBool()) {
+      final node = editor.parseAt(path);
+
+      if (node is YamlList && node.isNotEmpty) {
+        path.add(nextInt(node.length));
+      } else if (node is YamlMap && node.isNotEmpty) {
+        final keyList = node.keys.toList();
+        path.add(keyList[nextInt(keyList.length)]);
+      } else {
+        break;
+      }
+    }
+
+    return path;
+  }
+
+  ScalarStyle nextScalarStyle() {
+    final seed = nextInt(6);
+
+    switch (seed) {
+      case 0:
+        return ScalarStyle.DOUBLE_QUOTED;
+      case 1:
+        return ScalarStyle.FOLDED;
+      case 2:
+        return ScalarStyle.LITERAL;
+      case 3:
+        return ScalarStyle.PLAIN;
+      case 4:
+        return ScalarStyle.SINGLE_QUOTED;
+      default:
+        return ScalarStyle.ANY;
+    }
+  }
+
+  CollectionStyle nextCollectionStyle() {
+    final seed = nextInt(3);
+
+    switch (seed) {
+      case 0:
+        return CollectionStyle.BLOCK;
+      case 1:
+        return CollectionStyle.FLOW;
+      default:
+        return CollectionStyle.ANY;
+    }
+  }
+}
diff --git a/test/golden_test.dart b/test/golden_test.dart
new file mode 100644
index 0000000..f4c5db1
--- /dev/null
+++ b/test/golden_test.dart
@@ -0,0 +1,45 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+import 'dart:isolate';
+
+import './test_case.dart';
+
+/// This script performs snapshot testing of the inputs in the testing directory
+/// against golden files if they exist, and creates the golden files otherwise.
+///
+/// Input directory should be in `test/test_cases`, while the golden files should
+/// be in `test/test_cases_golden`.
+///
+/// For more information on the expected input and output, refer to the README
+/// in the testdata folder
+Future<void> main() async {
+  final packageUri = await Isolate.resolvePackageUri(
+      Uri.parse('package:yaml_edit/yaml_edit.dart'));
+
+  final testdataUri = packageUri.resolve('../test/testdata/');
+  final inputDirectory = Directory.fromUri(testdataUri.resolve('input/'));
+  final goldDirectoryUri = testdataUri.resolve('output/');
+
+  if (!inputDirectory.existsSync()) {
+    throw FileSystemException(
+        'Testing Directory does not exist!', inputDirectory.path);
+  }
+
+  final testCases =
+      await TestCases.getTestCases(inputDirectory.uri, goldDirectoryUri);
+
+  testCases.test();
+}
diff --git a/test/insert_test.dart b/test/insert_test.dart
new file mode 100644
index 0000000..4672e3f
--- /dev/null
+++ b/test/insert_test.dart
@@ -0,0 +1,142 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.insertIntoList([], 0, 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.insertIntoList([], 0, 4), throwsPathError);
+    });
+  });
+
+  test('throws RangeError if index is out of range', () {
+    final doc = YamlEditor('[1, 2]');
+    expect(() => doc.insertIntoList([], -1, 0), throwsRangeError);
+    expect(() => doc.insertIntoList([], 3, 0), throwsRangeError);
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.insertIntoList([], 0, 0);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.insertIntoList([], 1, 3);
+      expect(doc.toString(), equals('''
+- 1
+- 3
+- 2'''));
+      expectYamlBuilderValue(doc, [1, 3, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+''');
+      doc.insertIntoList([], 2, 3);
+      expect(doc.toString(), equals('''
+- 1
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [1, 2, 3]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('''
+- 1
+- 3
+''');
+      doc.insertIntoList([], 1, [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 1
+- - 4
+  - 5
+  - 6
+- 3
+'''));
+      expectYamlBuilderValue(doc, [
+        1,
+        [4, 5, 6],
+        3
+      ]);
+    });
+
+    test(' with comments', () {
+      final doc = YamlEditor('''
+- 0 # comment a
+- 2 # comment b
+''');
+      doc.insertIntoList([], 1, 1);
+      expect(doc.toString(), equals('''
+- 0 # comment a
+- 1
+- 2 # comment b
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 0, 0);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 1, 3);
+      expect(doc.toString(), equals('[1, 3, 2]'));
+      expectYamlBuilderValue(doc, [1, 3, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.insertIntoList([], 2, 3);
+      expect(doc.toString(), equals('[1, 2, 3]'));
+      expectYamlBuilderValue(doc, [1, 2, 3]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('["[],", "[],"]');
+      doc.insertIntoList([], 1, 'test');
+      expect(doc.toString(), equals('["[],", test, "[],"]'));
+      expectYamlBuilderValue(doc, ['[],', 'test', '[],']);
+    });
+  });
+}
diff --git a/test/naughty_test.dart b/test/naughty_test.dart
new file mode 100644
index 0000000..6e4e64b
--- /dev/null
+++ b/test/naughty_test.dart
@@ -0,0 +1,31 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import 'problem_strings.dart';
+
+void main() {
+  for (final string in problemStrings) {
+    test('expect string $string', () {
+      final doc = YamlEditor('');
+
+      expect(() => doc.update([], string), returnsNormally);
+      final value = doc.parseAt([]).value;
+      expect(value, isA<String>());
+      expect(value, equals(string));
+    });
+  }
+}
diff --git a/test/parse_test.dart b/test/parse_test.dart
new file mode 100644
index 0000000..a23c37f
--- /dev/null
+++ b/test/parse_test.dart
@@ -0,0 +1,147 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('PathError if key does not exist', () {
+      final doc = YamlEditor('{a: 4}');
+      final path = ['b'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if path tries to go deeper into a scalar', () {
+      final doc = YamlEditor('{a: 4}');
+      final path = ['a', 'b'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if index is out of bounds', () {
+      final doc = YamlEditor('[0,1]');
+      final path = [2];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+
+    test('PathError if index is not an integer', () {
+      final doc = YamlEditor('[0,1]');
+      final path = ['2'];
+
+      expect(() => doc.parseAt(path), throwsPathError);
+    });
+  });
+
+  group('orElse provides a default value', () {
+    test('simple example with null return ', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => null);
+
+      expect(result, equals(null));
+    });
+
+    test('simple example with map return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result =
+          doc.parseAt(['b'], orElse: () => wrapAsYamlNode({'a': 42}));
+
+      expect(result, isA<YamlMap>());
+      expect(result.value, equals({'a': 42}));
+    });
+
+    test('simple example with scalar return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => wrapAsYamlNode(42));
+
+      expect(result, isA<YamlScalar>());
+      expect(result.value, equals(42));
+    });
+
+    test('simple example with list return', () {
+      final doc = YamlEditor('{a: {d: 4}, c: ~}');
+      final result = doc.parseAt(['b'], orElse: () => wrapAsYamlNode([42]));
+
+      expect(result, isA<YamlList>());
+      expect(result.value, equals([42]));
+    });
+  });
+
+  group('returns a YamlNode', () {
+    test('with the correct type', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlScalar = doc.parseAt(['YAML']);
+
+      expect(expectedYamlScalar, isA<YamlScalar>());
+    });
+
+    test('with the correct value', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+
+      expect(doc.parseAt(['YAML']).value, "YAML Ain't Markup Language");
+    });
+
+    test('with the correct value in nested collection', () {
+      final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: [5, 6, 7]
+c: 3
+''');
+
+      expect(doc.parseAt(['b', 'e', 2]).value, 7);
+    });
+
+    test('with the correct type (2)', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(expectedYamlMap is YamlMap, equals(true));
+    });
+
+    test('that is immutable', () {
+      final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(() => (expectedYamlMap as YamlMap)['YAML'] = 'test',
+          throwsUnsupportedError);
+    });
+
+    test('that has immutable children', () {
+      final doc = YamlEditor("YAML: ['Y', 'A', 'M', 'L']");
+      final expectedYamlMap = doc.parseAt([]);
+
+      expect(() => (expectedYamlMap as YamlMap)['YAML'][0] = 'X',
+          throwsUnsupportedError);
+    });
+  });
+
+  test('works with map keys', () {
+    final doc = YamlEditor('{a: {{[1, 2]: 3}: 4}}');
+    expect(
+        doc.parseAt([
+          'a',
+          {
+            [1, 2]: 3
+          }
+        ]).value,
+        equals(4));
+  });
+}
diff --git a/test/prepend_test.dart b/test/prepend_test.dart
new file mode 100644
index 0000000..fa9d2e6
--- /dev/null
+++ b/test/prepend_test.dart
@@ -0,0 +1,172 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws PathError', () {
+    test('if it is a map', () {
+      final doc = YamlEditor('a:1');
+      expect(() => doc.prependToList([], 4), throwsPathError);
+    });
+
+    test('if it is a scalar', () {
+      final doc = YamlEditor('1');
+      expect(() => doc.prependToList([], 4), throwsPathError);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2]');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('with spaces (1)', () {
+      final doc = YamlEditor('[ 1 , 2 ]');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('[ 0, 1 , 2 ]'));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 1
+- 2''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    /// Regression testing for no trailing spaces.
+    test('(2)', () {
+      final doc = YamlEditor('''- 1
+- 2''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''- 0
+- 1
+- 2'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+''');
+      doc.prependToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- - 4
+  - 5
+  - 6
+- 1
+- 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [4, 5, 6],
+        1,
+        2
+      ]);
+    });
+
+    test('(4)', () {
+      final doc = YamlEditor('''
+a:
+ - b
+ - - c
+   - d
+''');
+      doc.prependToList(
+          ['a'], wrapAsYamlNode({1: 2}, collectionStyle: CollectionStyle.FLOW));
+
+      expect(doc.toString(), equals('''
+a:
+ - {1: 2}
+ - b
+ - - c
+   - d
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [
+          {1: 2},
+          'b',
+          ['c', 'd']
+        ]
+      });
+    });
+
+    test('with comments ', () {
+      final doc = YamlEditor('''
+# comments
+- 1 # comments
+- 2
+''');
+      doc.prependToList([], 0);
+      expect(doc.toString(), equals('''
+# comments
+- 0
+- 1 # comments
+- 2
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('nested in map', () {
+      final doc = YamlEditor('''
+a:
+  - 1
+  - 2
+''');
+      doc.prependToList(['a'], 0);
+      expect(doc.toString(), equals('''
+a:
+  - 0
+  - 1
+  - 2
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [0, 1, 2]
+      });
+    });
+
+    test('nested in map with comments ', () {
+      final doc = YamlEditor('''
+a: # comments
+  - 1 # comments
+  - 2
+''');
+      doc.prependToList(['a'], 0);
+      expect(doc.toString(), equals('''
+a: # comments
+  - 0
+  - 1 # comments
+  - 2
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': [0, 1, 2]
+      });
+    });
+  });
+}
diff --git a/test/preservation_test.dart b/test/preservation_test.dart
new file mode 100644
index 0000000..1b3b655
--- /dev/null
+++ b/test/preservation_test.dart
@@ -0,0 +1,71 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('preserves original yaml: ', () {
+    test('number', expectLoadPreservesYAML('2'));
+    test('number with leading and trailing lines', expectLoadPreservesYAML('''
+      
+      2
+      
+      '''));
+    test('octal numbers', expectLoadPreservesYAML('0o14'));
+    test('negative numbers', expectLoadPreservesYAML('-345'));
+    test('hexadecimal numbers', expectLoadPreservesYAML('0x123abc'));
+    test('floating point numbers', expectLoadPreservesYAML('345.678'));
+    test('exponential numbers', expectLoadPreservesYAML('12.3015e+02'));
+    test('string', expectLoadPreservesYAML('a string'));
+    test('string with control characters',
+        expectLoadPreservesYAML('a string \\n'));
+    test('string with control characters',
+        expectLoadPreservesYAML('a string \n\r'));
+    test('string with hex escapes',
+        expectLoadPreservesYAML('\\x0d\\x0a is \\r\\n'));
+    test('flow map', expectLoadPreservesYAML('{a: 2}'));
+    test('flow list', expectLoadPreservesYAML('[1, 2]'));
+    test('flow list with different types of elements',
+        expectLoadPreservesYAML('[1, a]'));
+    test('flow list with weird spaces',
+        expectLoadPreservesYAML('[ 1 ,      2]'));
+    test('multiline string', expectLoadPreservesYAML('''
+      Mark set a major league
+      home run record in 1998.'''));
+    test('tilde', expectLoadPreservesYAML('~'));
+    test('false', expectLoadPreservesYAML('false'));
+
+    test('block map', expectLoadPreservesYAML('''a: 
+    b: 1
+    '''));
+    test('block list', expectLoadPreservesYAML('''a: 
+    - 1
+    '''));
+    test('complicated example', () {
+      expectLoadPreservesYAML('''verb: RecommendCafes
+map:
+  a: 
+    b: 1
+recipe:
+  - verb: Score
+    outputs: ["DishOffering[]/Scored", "Suggestions"]
+    name: Hotpot
+  - verb: Rate
+    inputs: Dish
+    ''');
+    });
+  });
+}
diff --git a/test/problem_strings.dart b/test/problem_strings.dart
new file mode 100644
index 0000000..74de28f
--- /dev/null
+++ b/test/problem_strings.dart
@@ -0,0 +1,99 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const problemStrings = [
+  '[]',
+  '{}',
+  '',
+  ',',
+  '~',
+  'undefined',
+  'undef',
+  'null',
+  'NULL',
+  '(null)',
+  'nil',
+  'NIL',
+  'true',
+  'false',
+  'True',
+  'False',
+  'TRUE',
+  'FALSE',
+  'None',
+  '\\',
+  '\\\\',
+  '0',
+  '1',
+  '\$1.00',
+  '1/2',
+  '1E2',
+  '-\$1.00',
+  '-1/2',
+  '-1E+02',
+  '1/0',
+  '0/0',
+  '-0',
+  '+0.0',
+  '0..0',
+  '.',
+  '0.0.0',
+  '0,00',
+  ',',
+  '0.0/0',
+  '1.0/0.0',
+  '0.0/0.0',
+  '--1',
+  '-',
+  '-.',
+  '-,',
+  '999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999',
+  'NaN',
+  'Infinity',
+  '-Infinity',
+  'INF',
+  '1#INF',
+  '0x0',
+  '0xffffffffffffffff',
+  "1'000.00",
+  '1,000,000.00',
+  '1.000,00',
+  "1'000,00",
+  '1.000.000,00',
+  ",./;'[]\\-=",
+  '<>?:\"{}|_+',
+  '!@#\$%^&*()`~',
+  '\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f',
+  '\t\u000b\f …             ​

   ',
+  'ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็',
+  "'",
+  '"',
+  "''",
+  '\'\"',
+  "'\"\'",
+  '社會科學院語學研究所',
+  'Ⱥ',
+  'ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ',
+  '❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙',
+  '𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘',
+  ' ',
+  '%',
+  '%d',
+  '%s%s%s%s%s',
+  '{0}',
+  '%*.*s',
+  '%@',
+  '%n',
+  'The quic\b\b\b\b\b\bk brown fo\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007\u0007x... [Beeeep]',
+];
diff --git a/test/remove_test.dart b/test/remove_test.dart
new file mode 100644
index 0000000..7362d0d
--- /dev/null
+++ b/test/remove_test.dart
@@ -0,0 +1,440 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('PathError if collectionPath points to a scalar', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+
+      expect(() => doc.remove(['a', 0]), throwsPathError);
+    });
+
+    test('PathError if collectionPath is invalid', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+
+      expect(() => doc.remove(['d']), throwsPathError);
+    });
+
+    test('PathError if collectionPath is invalid - list', () {
+      final doc = YamlEditor('''
+[1, 2, 3]
+''');
+
+      expect(() => doc.remove([4]), throwsPathError);
+    });
+  });
+
+  test('empty path should clear string', () {
+    final doc = YamlEditor('''
+a: 1
+b: 2
+c: [3, 4]
+''');
+    doc.remove([]);
+    expect(doc.toString(), equals(''));
+  });
+
+  group('block map', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1
+c: 3
+'''));
+    });
+
+    test('final element in map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1
+'''));
+    });
+
+    test('final element in nested map', () {
+      final doc = YamlEditor('''
+a: 
+  aa: 11
+  bb: 22
+b: 2
+''');
+      doc.remove(['a', 'bb']);
+      expect(doc.toString(), equals('''
+a: 
+  aa: 11
+b: 2
+'''));
+    });
+
+    test('last element should return flow empty map', () {
+      final doc = YamlEditor('''
+a: 1
+''');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('''
+{}
+'''));
+    });
+
+    test('last element should return flow empty map (2)', () {
+      final doc = YamlEditor('''
+- a: 1
+- b: 2
+''');
+      doc.remove([0, 'a']);
+      expect(doc.toString(), equals('''
+- {}
+- b: 2
+'''));
+    });
+
+    test('nested', () {
+      final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+''');
+      doc.remove(['b', 'd']);
+      expect(doc.toString(), equals('''
+a: 1
+b: 
+  e: 5
+c: 3
+'''));
+    });
+  });
+
+  group('block list', () {
+    test('last element should return flow empty list', () {
+      final doc = YamlEditor('''
+- 0
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+[]
+'''));
+    });
+
+    test('last element should return flow empty list (2)', () {
+      final doc = YamlEditor('''
+a: [1]
+b: [3]
+''');
+      doc.remove(['a', 0]);
+      expect(doc.toString(), equals('''
+a: []
+b: [3]
+'''));
+    });
+
+    test('last element should return flow empty list (3)', () {
+      final doc = YamlEditor('''
+a: 
+  - 1
+b: 
+  - 3
+''');
+      doc.remove(['a', 0]);
+      expect(doc.toString(), equals('''
+a: 
+  []
+b: 
+  - 3
+'''));
+    });
+  });
+
+  group('flow map', () {
+    test('(1)', () {
+      final doc = YamlEditor('{a: 1, b: 2, c: 3}');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('{a: 1, c: 3}'));
+    });
+
+    test('(2) ', () {
+      final doc = YamlEditor('{a: 1}');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('{}'));
+    });
+
+    test('(3) ', () {
+      final doc = YamlEditor('{a: 1, b: 2}');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('{ b: 2}'));
+    });
+
+    test('(4) ', () {
+      final doc =
+          YamlEditor('{"{}[],": {"{}[],": 1, b: "}{[]},", "}{[],": 3}}');
+      doc.remove(['{}[],', 'b']);
+      expect(doc.toString(), equals('{"{}[],": {"{}[],": 1, "}{[],": 3}}'));
+    });
+
+    test('nested flow map ', () {
+      final doc = YamlEditor('{a: 1, b: {d: 4, e: 5}, c: 3}');
+      doc.remove(['b', 'd']);
+      expect(doc.toString(), equals('{a: 1, b: { e: 5}, c: 3}'));
+    });
+
+    test('nested flow map (2)', () {
+      final doc = YamlEditor('{a: {{[1] : 2}: 3, b: 2}}');
+      doc.remove([
+        'a',
+        {
+          [1]: 2
+        }
+      ]);
+      expect(doc.toString(), equals('{a: { b: 2}}'));
+    });
+  });
+
+  group('block list', () {
+    test('(1) ', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 0
+- [1,2,3]
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- 0
+- {a: 1, b: 2}
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('last element', () {
+      final doc = YamlEditor('''
+- 0
+- 1
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+'''));
+      expectYamlBuilderValue(doc, [0]);
+    });
+
+    test('with comments', () {
+      final doc = YamlEditor('''
+- 0
+- 1 # comments
+- 2
+- 3
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0
+- 2
+- 3
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('nested', () {
+      final doc = YamlEditor('''
+- - - 0
+    - 1
+''');
+      doc.remove([0, 0, 0]);
+      expect(doc.toString(), equals('''
+- - - 1
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          [1]
+        ]
+      ]);
+    });
+
+    test('nested list', () {
+      final doc = YamlEditor('''
+- - 0
+  - 1
+- 2
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+- 2
+'''));
+      expectYamlBuilderValue(doc, [2]);
+    });
+
+    test('nested list (2)', () {
+      final doc = YamlEditor('''
+- - 0
+  - 1
+- 2
+''');
+      doc.remove([0, 1]);
+      expect(doc.toString(), equals('''
+- - 0
+- 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [0],
+        2
+      ]);
+    });
+
+    test('nested list (3)', () {
+      final doc = YamlEditor('''
+-
+  - - 0
+    - 1
+  - 2
+''');
+      doc.remove([0, 0, 1]);
+      expect(doc.toString(), equals('''
+-
+  - - 0
+  - 2
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          [0],
+          2
+        ]
+      ]);
+    });
+    test('nested map', () {
+      final doc = YamlEditor('''
+- - a: b
+    c: d
+''');
+      doc.remove([0, 0, 'a']);
+      expect(doc.toString(), equals('''
+- - c: d
+'''));
+      expectYamlBuilderValue(doc, [
+        [
+          {'c': 'd'}
+        ]
+      ]);
+    });
+
+    test('nested map (2)', () {
+      final doc = YamlEditor('''
+- a: 
+    - 0
+    - 1
+  c: d
+''');
+      doc.remove([0, 'a', 1]);
+      expect(doc.toString(), equals('''
+- a: 
+    - 0
+  c: d
+'''));
+      expectYamlBuilderValue(doc, [
+        {
+          'a': [0],
+          'c': 'd'
+        }
+      ]);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[1, 2, 3]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('[1, 3]'));
+      expectYamlBuilderValue(doc, [1, 3]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[1, "b", "c"]');
+      doc.remove([0]);
+      expect(doc.toString(), equals('[ "b", "c"]'));
+      expectYamlBuilderValue(doc, ['b', 'c']);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('[1, {a: 1}, "c"]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('[1, "c"]'));
+      expectYamlBuilderValue(doc, [1, 'c']);
+    });
+
+    test('(4) ', () {
+      final doc = YamlEditor('["{}", b, "}{"]');
+      doc.remove([1]);
+      expect(doc.toString(), equals('["{}", "}{"]'));
+    });
+
+    test('(5) ', () {
+      final doc = YamlEditor('["{}[],", [test, "{}[],", "{}[],"], "{}[],"]');
+      doc.remove([1, 0]);
+      expect(doc.toString(), equals('["{}[],", [ "{}[],", "{}[],"], "{}[],"]'));
+    });
+  });
+}
diff --git a/test/source_edit_test.dart b/test/source_edit_test.dart
new file mode 100644
index 0000000..8552a22
--- /dev/null
+++ b/test/source_edit_test.dart
@@ -0,0 +1,146 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('SourceEdit', () {
+    group('fromJson', () {
+      test('converts from jsonEncode', () {
+        final sourceEditMap = {
+          'offset': 1,
+          'length': 2,
+          'replacement': 'replacement string'
+        };
+        final sourceEdit = SourceEdit.fromJson(sourceEditMap);
+
+        expect(sourceEdit.offset, 1);
+        expect(sourceEdit.length, 2);
+        expect(sourceEdit.replacement, 'replacement string');
+      });
+
+      test('throws formatException if offset is non-int', () {
+        final sourceEditJson = {
+          'offset': '1',
+          'length': 2,
+          'replacement': 'replacement string'
+        };
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if length is non-int', () {
+        final sourceEditJson = {
+          'offset': 1,
+          'length': '2',
+          'replacement': 'replacement string'
+        };
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if replacement is non-string', () {
+        final sourceEditJson = {'offset': 1, 'length': 2, 'replacement': 3};
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+
+      test('throws formatException if a field is not present', () {
+        final sourceEditJson = {'offset': 1, 'length': 2};
+
+        expect(
+            () => SourceEdit.fromJson(sourceEditJson), throwsFormatException);
+      });
+    });
+
+    test('toString returns a nice string representation', () {
+      final sourceEdit = SourceEdit(1, 2, 'replacement string');
+      expect(sourceEdit.toString(),
+          equals('SourceEdit(1, 2, "replacement string")'));
+    });
+
+    group('hashCode', () {
+      test('returns same value for equal SourceEdits', () {
+        final sourceEdit1 = SourceEdit(1, 2, 'replacement string');
+        final sourceEdit2 = SourceEdit(1, 2, 'replacement string');
+        expect(sourceEdit1.hashCode, equals(sourceEdit2.hashCode));
+      });
+
+      test('returns different value for equal SourceEdits', () {
+        final sourceEdit1 = SourceEdit(1, 2, 'replacement string');
+        final sourceEdit2 = SourceEdit(1, 3, 'replacement string');
+        expect(sourceEdit1.hashCode == sourceEdit2.hashCode, equals(false));
+      });
+    });
+
+    group('toJson', () {
+      test('behaves as expected', () {
+        final sourceEdit = SourceEdit(1, 2, 'replacement string');
+        final sourceEditJson = sourceEdit.toJson();
+
+        expect(
+            sourceEditJson,
+            equals({
+              'offset': 1,
+              'length': 2,
+              'replacement': 'replacement string'
+            }));
+      });
+
+      test('is compatible with fromJson', () {
+        final sourceEdit = SourceEdit(1, 2, 'replacement string');
+        final sourceEditJson = sourceEdit.toJson();
+        final newSourceEdit = SourceEdit.fromJson(sourceEditJson);
+
+        expect(newSourceEdit.offset, 1);
+        expect(newSourceEdit.length, 2);
+        expect(newSourceEdit.replacement, 'replacement string');
+      });
+    });
+
+    group('applyAll', () {
+      test('returns original string when empty list is passed in', () {
+        const original = 'YAML: YAML';
+        final result = SourceEdit.applyAll(original, []);
+
+        expect(result, original);
+      });
+      test('works with list of one SourceEdit', () {
+        const original = 'YAML: YAML';
+        final sourceEdits = [SourceEdit(6, 4, 'YAML Ain\'t Markup Language')];
+
+        final result = SourceEdit.applyAll(original, sourceEdits);
+
+        expect(result, "YAML: YAML Ain't Markup Language");
+      });
+      test('works with list of multiple SourceEdits', () {
+        const original = 'YAML: YAML';
+        final sourceEdits = [
+          SourceEdit(6, 4, "YAML Ain't Markup Language"),
+          SourceEdit(6, 4, "YAML Ain't Markup Language"),
+          SourceEdit(0, 4, "YAML Ain't Markup Language")
+        ];
+
+        final result = SourceEdit.applyAll(original, sourceEdits);
+
+        expect(result,
+            "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup Language");
+      });
+    });
+  });
+}
diff --git a/test/special_test.dart b/test/special_test.dart
new file mode 100644
index 0000000..d322d2c
--- /dev/null
+++ b/test/special_test.dart
@@ -0,0 +1,121 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  test('test if "No" is recognized as false', () {
+    final doc = YamlEditor('''
+~: null
+false: false
+No: No
+true: true
+''');
+    doc.update([null], 'tilde');
+    doc.update([false], false);
+    doc.update(['No'], 'no');
+    doc.update([true], 'true');
+
+    expect(doc.toString(), equals('''
+~: tilde
+false: false
+No: no
+true: "true"
+'''));
+
+    expectYamlBuilderValue(
+        doc, {null: 'tilde', false: false, 'No': 'no', true: 'true'});
+  });
+
+  test('array keys are recognized', () {
+    final doc = YamlEditor('{[1,2,3]: a}');
+    doc.update([
+      [1, 2, 3]
+    ], 'sums to 6');
+
+    expect(doc.toString(), equals('{[1,2,3]: sums to 6}'));
+    expectYamlBuilderValue(doc, {
+      [1, 2, 3]: 'sums to 6'
+    });
+  });
+
+  test('map keys are recognized', () {
+    final doc = YamlEditor('{{a: 1}: a}');
+    doc.update([
+      {'a': 1}
+    ], 'sums to 6');
+
+    expect(doc.toString(), equals('{{a: 1}: sums to 6}'));
+    expectYamlBuilderValue(doc, {
+      {'a': 1}: 'sums to 6'
+    });
+  });
+
+  test('documents can have directives', () {
+    final doc = YamlEditor('''%YAML 1.2
+--- text''');
+    doc.update([], 'test');
+
+    expect(doc.toString(), equals('%YAML 1.2\n--- test'));
+    expectYamlBuilderValue(doc, 'test');
+  });
+
+  test('tags should be removed if value is changed', () {
+    final doc = YamlEditor('''
+ - !!str a
+ - b
+ - !!int 42
+ - d
+''');
+    doc.update([2], 'test');
+
+    expect(doc.toString(), equals('''
+ - !!str a
+ - b
+ - test
+ - d
+'''));
+    expectYamlBuilderValue(doc, ['a', 'b', 'test', 'd']);
+  });
+
+  test('tags should be removed if key is changed', () {
+    final doc = YamlEditor('''
+!!str a: b
+c: !!int 42
+e: !!str f
+g: h
+!!str 23: !!bool false
+''');
+    doc.remove(['23']);
+
+    expect(doc.toString(), equals('''
+!!str a: b
+c: !!int 42
+e: !!str f
+g: h
+'''));
+    expectYamlBuilderValue(doc, {'a': 'b', 'c': 42, 'e': 'f', 'g': 'h'});
+  });
+
+  test('detect invalid extra closing bracket', () {
+    final doc = YamlEditor('''[ a, b ]''');
+    doc.appendToList([], 'c ]');
+
+    expect(doc.toString(), equals('''[ a, b , "c ]"]'''));
+    expectYamlBuilderValue(doc, ['a', 'b', 'c ]']);
+  });
+}
diff --git a/test/splice_test.dart b/test/splice_test.dart
new file mode 100644
index 0000000..13ccd06
--- /dev/null
+++ b/test/splice_test.dart
@@ -0,0 +1,107 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  test(
+      'throws RangeError if invalid index + deleteCount combination is passed in',
+      () {
+    final doc = YamlEditor('[0, 0]');
+    expect(() => doc.spliceList([], 1, 5, [1, 2]), throwsRangeError);
+  });
+
+  group('block list', () {
+    test('(1)', () {
+      final doc = YamlEditor('''
+- 0
+- 0
+''');
+      final nodes = doc.spliceList([], 1, 1, [1, 2]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+'''));
+
+      expectDeepEquals(nodes.toList(), [0]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('''
+- 0
+- 0
+''');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('''
+- 0
+- 1
+- 2
+'''));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+
+    test('(3)', () {
+      final doc = YamlEditor('''
+- Jan
+- March
+- April
+- June
+''');
+      final nodes = doc.spliceList([], 1, 0, ['Feb']);
+      expect(doc.toString(), equals('''
+- Jan
+- Feb
+- March
+- April
+- June
+'''));
+
+      expectDeepEquals(nodes.toList(), []);
+
+      final nodes2 = doc.spliceList([], 4, 1, ['May']);
+      expect(doc.toString(), equals('''
+- Jan
+- Feb
+- March
+- April
+- May
+'''));
+
+      expectDeepEquals(nodes2.toList(), ['June']);
+    });
+  });
+
+  group('flow list', () {
+    test('(1)', () {
+      final doc = YamlEditor('[0, 0]');
+      final nodes = doc.spliceList([], 1, 1, [1, 2]);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+
+      expectDeepEquals(nodes.toList(), [0]);
+    });
+
+    test('(2)', () {
+      final doc = YamlEditor('[0, 0]');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('[0, 1, 2]'));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+  });
+}
diff --git a/test/test_case.dart b/test/test_case.dart
new file mode 100644
index 0000000..849bdf8
--- /dev/null
+++ b/test/test_case.dart
@@ -0,0 +1,300 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:yaml_edit/src/utils.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import 'test_utils.dart';
+
+/// Interface for creating golden Test cases
+class TestCases {
+  final List<_TestCase> testCases;
+
+  /// Creates a [TestCases] object based on test directory and golden directory
+  /// path.
+  static Future<TestCases> getTestCases(Uri testDirUri, Uri goldDirUri) async {
+    final testDir = Directory.fromUri(testDirUri);
+
+    if (!testDir.existsSync()) return TestCases([]);
+
+    /// Recursively grab all the files in the testing directory.
+    return TestCases(await testDir
+        .list(recursive: true, followLinks: false)
+        .where((entity) => entity.path.endsWith('.test'))
+        .map((entity) => entity.uri)
+        .map((inputUri) {
+      final inputWithoutExtension =
+          p.basenameWithoutExtension(inputUri.toFilePath());
+      final goldenUri = goldDirUri.resolve('./$inputWithoutExtension.golden');
+
+      return _TestCase(inputUri, goldenUri);
+    }).toList());
+  }
+
+  /// Tests all the [_TestCase]s if the golden files exist, create the golden
+  /// files otherwise.
+  void test() {
+    var tested = 0;
+    var created = 0;
+
+    for (final testCase in testCases) {
+      testCase.testOrCreate();
+      if (testCase.state == _TestCaseStates.testedGoldenFile) {
+        tested++;
+      } else if (testCase.state == _TestCaseStates.createdGoldenFile) {
+        created++;
+      }
+    }
+
+    print(
+        'Successfully tested $tested inputs against golden files, created $created golden files');
+  }
+
+  TestCases(this.testCases);
+
+  int get length => testCases.length;
+}
+
+/// Enum representing the different states of [_TestCase]s.
+enum _TestCaseStates { initialized, createdGoldenFile, testedGoldenFile }
+
+/// Interface for a golden test case. Handles the logic for test conduct/golden
+/// test update accordingly.
+class _TestCase {
+  final Uri inputUri;
+  final Uri goldenUri;
+  final List<String> states = [];
+
+  String info;
+  YamlEditor yamlBuilder;
+  List<_YamlModification> modifications;
+
+  String inputLineEndings = '\n';
+
+  _TestCaseStates state = _TestCaseStates.initialized;
+
+  _TestCase(this.inputUri, this.goldenUri) {
+    final inputFile = File.fromUri(inputUri);
+    if (!inputFile.existsSync()) {
+      throw Exception('Input File does not exist!');
+    }
+
+    _initialize(inputFile);
+  }
+
+  /// Initializes the [_TestCase] by reading the corresponding [inputFile] and
+  /// parsing the different portions, and then running the input yaml against
+  /// the specified modifications.
+  ///
+  /// Precondition: [inputFile] must exist, and inputs must be well-formatted.
+  void _initialize(File inputFile) {
+    final input = inputFile.readAsStringSync();
+
+    final inputLineEndings = getLineEnding(input);
+    final inputElements = input.split('---$inputLineEndings');
+
+    if (inputElements.length != 3) {
+      throw AssertionError('File ${inputFile.path} is not properly formatted.');
+    }
+
+    info = inputElements[0];
+    yamlBuilder = YamlEditor(inputElements[1]);
+    final rawModifications = _getValueFromYamlNode(loadYaml(inputElements[2]));
+    modifications = _parseModifications(rawModifications);
+
+    /// Adds the initial state as well, so we can check that the simplest
+    /// parse -> immediately dump does not affect the string.
+    states.add(yamlBuilder.toString());
+
+    _performModifications();
+  }
+
+  void _performModifications() {
+    for (final mod in modifications) {
+      _performModification(mod);
+      states.add(yamlBuilder.toString());
+    }
+  }
+
+  void _performModification(_YamlModification mod) {
+    switch (mod.method) {
+      case YamlModificationMethod.update:
+        yamlBuilder.update(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.remove:
+        yamlBuilder.remove(mod.path);
+        return;
+      case YamlModificationMethod.appendTo:
+        yamlBuilder.appendToList(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.prependTo:
+        yamlBuilder.prependToList(mod.path, mod.value);
+        return;
+      case YamlModificationMethod.insert:
+        yamlBuilder.insertIntoList(mod.path, mod.index, mod.value);
+        return;
+      case YamlModificationMethod.splice:
+        yamlBuilder.spliceList(mod.path, mod.index, mod.deleteCount, mod.value);
+        return;
+    }
+  }
+
+  void testOrCreate() {
+    final goldenFile = File.fromUri(goldenUri);
+    if (!goldenFile.existsSync()) {
+      createGoldenFile(goldenFile);
+    } else {
+      testGoldenFile(goldenFile);
+    }
+  }
+
+  void createGoldenFile(File goldenFile) {
+    /// Assumes user wants the golden file to have the same line endings as
+    /// the input file.
+    final goldenOutput = states.join('---$inputLineEndings');
+
+    goldenFile.writeAsStringSync(goldenOutput);
+    state = _TestCaseStates.createdGoldenFile;
+  }
+
+  /// Tests the golden file. Ensures that the number of states are the same, and
+  /// that the individual states are the same.
+  void testGoldenFile(File goldenFile) {
+    final inputFileName = p.basename(inputUri.toFilePath());
+    final golden = goldenFile.readAsStringSync();
+    final goldenStates = golden.split('---${getLineEnding(golden)}');
+
+    group('testing $inputFileName - input and golden files have', () {
+      test('same number of states', () {
+        expect(states.length, equals(goldenStates.length));
+      });
+
+      for (var i = 0; i < states.length; i++) {
+        test('same state $i', () {
+          expect(states[i], equals(goldenStates[i]));
+        });
+      }
+    });
+
+    state = _TestCaseStates.testedGoldenFile;
+  }
+}
+
+/// Converts a [YamlList] into a Dart list.
+List _getValueFromYamlList(YamlList node) {
+  return node.value.map((n) {
+    if (n is YamlNode) return _getValueFromYamlNode(n);
+    return n;
+  }).toList();
+}
+
+/// Converts a [YamlMap] into a Dart Map.
+Map _getValueFromYamlMap(YamlMap node) {
+  final keys = node.keys;
+  final result = {};
+  for (final key in keys) {
+    result[key.value] = result[key].value;
+  }
+
+  return result;
+}
+
+/// Converts a [YamlNode] into a Dart object.
+dynamic _getValueFromYamlNode(YamlNode node) {
+  switch (node.runtimeType) {
+    case YamlList:
+      return _getValueFromYamlList(node);
+    case YamlMap:
+      return _getValueFromYamlMap(node);
+    default:
+      return node.value;
+  }
+}
+
+/// Converts the list of modifications from the raw input to [_YamlModification]
+/// objects.
+List<_YamlModification> _parseModifications(List<dynamic> modifications) {
+  return modifications.map((mod) {
+    Object value;
+    int index;
+    int deleteCount;
+    final method = _getModificationMethod(mod[0] as String);
+
+    final path = mod[1] as List;
+
+    if (method == YamlModificationMethod.appendTo ||
+        method == YamlModificationMethod.update ||
+        method == YamlModificationMethod.prependTo) {
+      value = mod[2];
+    } else if (method == YamlModificationMethod.insert) {
+      index = mod[2];
+      value = mod[3];
+    } else if (method == YamlModificationMethod.splice) {
+      index = mod[2];
+      deleteCount = mod[3];
+
+      if (mod[4] is! List) {
+        throw ArgumentError('Invalid array ${mod[4]} used in splice');
+      }
+
+      value = mod[4];
+    }
+
+    return _YamlModification(method, path, index, value, deleteCount);
+  }).toList();
+}
+
+/// Gets the YAML modification method corresponding to [method]
+YamlModificationMethod _getModificationMethod(String method) {
+  switch (method) {
+    case 'update':
+      return YamlModificationMethod.update;
+    case 'remove':
+      return YamlModificationMethod.remove;
+    case 'append':
+    case 'appendTo':
+      return YamlModificationMethod.appendTo;
+    case 'prepend':
+    case 'prependTo':
+      return YamlModificationMethod.prependTo;
+    case 'insert':
+    case 'insertIn':
+      return YamlModificationMethod.insert;
+    case 'splice':
+      return YamlModificationMethod.splice;
+    default:
+      throw Exception('$method not recognized!');
+  }
+}
+
+/// Class representing an abstract YAML modification to be performed
+class _YamlModification {
+  final YamlModificationMethod method;
+  final List<dynamic> path;
+  final int index;
+  final dynamic value;
+  final int deleteCount;
+
+  _YamlModification(
+      this.method, this.path, this.index, this.value, this.deleteCount);
+
+  @override
+  String toString() =>
+      'method: $method, path: $path, index: $index, value: $value, deleteCount: $deleteCount';
+}
diff --git a/test/test_utils.dart b/test/test_utils.dart
new file mode 100644
index 0000000..4110f2b
--- /dev/null
+++ b/test/test_utils.dart
@@ -0,0 +1,56 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+
+import 'package:yaml_edit/src/equality.dart';
+import 'package:yaml_edit/src/errors.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+/// Asserts that a string containing a single YAML document is unchanged
+/// when dumped right after loading.
+void Function() expectLoadPreservesYAML(String source) {
+  final doc = YamlEditor(source);
+  return () => expect(doc.toString(), equals(source));
+}
+
+/// Asserts that [builder] has the same internal value as [expected].
+void expectYamlBuilderValue(YamlEditor builder, Object expected) {
+  final builderValue = builder.parseAt([]);
+  expectDeepEquals(builderValue, expected);
+}
+
+/// Asserts that [builder] has the same internal value as [expected].
+void expectDeepEquals(Object actual, Object expected) {
+  expect(
+      actual, predicate((actual) => deepEquals(actual, expected), '$expected'));
+}
+
+Matcher notEquals(dynamic expected) => isNot(equals(expected));
+
+/// A matcher for functions that throw [PathError].
+Matcher throwsPathError = throwsA(isA<PathError>());
+
+/// A matcher for functions that throw [AliasError].
+Matcher throwsAliasError = throwsA(isA<AliasError>());
+
+/// Enum to hold the possible modification methods.
+enum YamlModificationMethod {
+  appendTo,
+  insert,
+  prependTo,
+  remove,
+  splice,
+  update,
+}
diff --git a/test/testdata/README.md b/test/testdata/README.md
new file mode 100644
index 0000000..c3dade3
--- /dev/null
+++ b/test/testdata/README.md
@@ -0,0 +1,79 @@
+# Golden Testing
+
+This folder contains the files used for Golden testing performed by [golden_test.dart](../golden_test.dart).
+
+With golden testing, we are able to quickly ensure that our output conforms to our expectations given input parameters, which are extremely valuable especially on complex test cases not easily captured by unit tests.
+
+When the tests are run (see [Running Tests](#Running-Tests)), the series of specified modifications will be performed on the input, and the various output states will be compared against the `.golden` files if they exist. Othweise, if the `.golden` files do not exist (such as in the case of a new test case), they will be created.
+
+## Table of Contents
+
+1. [Running Tests](#Running-Tests)
+1. [Input Format](#Input-Format)
+1. [Adding Test Cases](#Adding-Test-Cases)
+1. [Output Format](#Output-Format)
+
+## Running Tests
+
+By default, golden testing is performed with `pub run test`. If we only wanted to
+performed golden testing, simply do: `pub run test test/golden_test.dart`.
+
+## Input Format
+
+Input files have the following format:
+
+```
+INFORMATION (e.g. description) - parsed as text
+---
+INPUT - parsed as YAML
+---
+Modifications - parsed as YAML, must be a list.
+```
+
+The information section is meant for a brief description of your test, and other further elaboration on what your test is targeted at (e.g. modification of complex keys). The Input section should be the YAML that you wish to parse, and the modifications section should be a list of modification operations, formatted as a YAML list. The valid list of modifications are as follows:
+
+- [update, [ path ], newValue]
+- [remove, [ path ], keyOrIndex]
+- [append, [ collectionPath ], newValue]
+
+An example of what an input file might look like is:
+
+```
+BASIC LIST TEST - Ensures that the basic list operations work.
+---
+- 0
+- 1
+- 2
+- 3
+---
+- [remove, [1]]
+- [append, [], 4]
+```
+
+Note that the parser uses `\n---\n` as the delimiter to separate the different sections.
+
+## Adding Test Cases
+
+To add test cases, simple create `<your-test-name>.test` files in `/test/testdata/input` in the format explained in [Input Format](#Input-Format). When the test script is first run, the respective `.golden` files will be created in `/test/testdata/output`, you should check to ensure that the output is as expected since future collaborators will be counting on your output!
+
+## Output Format
+
+The output `.golden` files contain a series of YAML strings representing the state of the YAML after each specified modification, with the first string being the inital state as specified in the ouput. These states are separated by `\n---\n` as a delimiter. For example, the output file for the sample input file above is:
+
+```
+- 0
+- 1
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+- 4
+```
+
+The first state is the input, the second state is the first state with the removal of the element at index 1, and the last state is the result of the second state with the insertion of the element 4 into the list.
diff --git a/test/testdata/input/allowed_characters_in_keys.test b/test/testdata/input/allowed_characters_in_keys.test
new file mode 100644
index 0000000..1057df0
--- /dev/null
+++ b/test/testdata/input/allowed_characters_in_keys.test
@@ -0,0 +1,9 @@
+ALLOWED CHARACTERS IN KEYS
+---
+test: test
+---
+- [update, ["a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~"], safe]
+- [update, ['?foo'], safe question mark]
+- [update, [':foo'], safe colon]
+- [update, ['-foo'], safe dash]
+- [update, ['this is#not'], a comment]
\ No newline at end of file
diff --git a/test/testdata/input/basic.test b/test/testdata/input/basic.test
new file mode 100644
index 0000000..02c082d
--- /dev/null
+++ b/test/testdata/input/basic.test
@@ -0,0 +1,5 @@
+BASIC TEST
+---
+YAML: YAML Ain't Markup Language
+---
+- [update, ['YAML'], hi]
\ No newline at end of file
diff --git a/test/testdata/input/basic_list.test b/test/testdata/input/basic_list.test
new file mode 100644
index 0000000..3ba942b
--- /dev/null
+++ b/test/testdata/input/basic_list.test
@@ -0,0 +1,12 @@
+BASIC LIST TEST - Ensures that the basic list operations work.
+---
+- 0
+- 1
+- 2
+- 3
+---
+- [remove, [1]]
+- [append, [], 4]
+- [update, [2], 5]
+- [prepend, [], 6]
+- [splice, [], 2, 3, [7, 8]]
\ No newline at end of file
diff --git a/test/testdata/input/nested_block_map.test b/test/testdata/input/nested_block_map.test
new file mode 100644
index 0000000..69e8b23
--- /dev/null
+++ b/test/testdata/input/nested_block_map.test
@@ -0,0 +1,11 @@
+NESTED BLOCK MAP
+---
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+---
+- [update, ['b', 'e'], 6]
+- [update, ['b', 'e'], [1,2,3]]
+- [update, ['b', 'f'], 6]
\ No newline at end of file
diff --git a/test/testdata/input/pubspec.test b/test/testdata/input/pubspec.test
new file mode 100644
index 0000000..eafc6b4
--- /dev/null
+++ b/test/testdata/input/pubspec.test
@@ -0,0 +1,27 @@
+TESTING WITH A SAMPLE PUBSPEC
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  yaml: ^2.2.1 # For YAML
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+---
+- [update, ['dependencies', 'yaml'], ^3.2.0]
+- [update, ['dependencies', 'retry'], ^3.0.1]
+- [remove, ['dependencies', 'meta']]
\ No newline at end of file
diff --git a/test/testdata/input/spaces.test b/test/testdata/input/spaces.test
new file mode 100644
index 0000000..d74fe73
--- /dev/null
+++ b/test/testdata/input/spaces.test
@@ -0,0 +1,8 @@
+SPACE IN STRING - update a string that starts and ends with space
+---
+a: ' space around me '
+c: 'hello world'
+---
+- [update, ['b'], ' also spaced ']
+- [update, ['d'], ' ']
+- [update, ["\ne"], with newline]
\ No newline at end of file
diff --git a/test/testdata/input/special_keywords.test b/test/testdata/input/special_keywords.test
new file mode 100644
index 0000000..0b3b81f
--- /dev/null
+++ b/test/testdata/input/special_keywords.test
@@ -0,0 +1,11 @@
+SPECIAL KEYWORDS 
+---
+~: null
+false: false
+No: No
+true: true
+---
+- [update, [null], tilde]
+- [update, [false], false]
+- [update, [No], no]
+- [update, [true], True]
\ No newline at end of file
diff --git a/test/testdata/output/allowed_characters_in_keys.golden b/test/testdata/output/allowed_characters_in_keys.golden
new file mode 100644
index 0000000..4cfa06f
--- /dev/null
+++ b/test/testdata/output/allowed_characters_in_keys.golden
@@ -0,0 +1,26 @@
+test: test
+---
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+test: test
+---
+?foo: safe question mark
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+test: test
+---
+:foo: safe colon
+?foo: safe question mark
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+test: test
+---
+-foo: safe dash
+:foo: safe colon
+?foo: safe question mark
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+test: test
+---
+-foo: safe dash
+:foo: safe colon
+?foo: safe question mark
+"a!\"#$%&'()*+,-.\/09:;<=>?@AZ[\\]^_`az{|}~": safe
+test: test
+this is#not: a comment
diff --git a/test/testdata/output/basic.golden b/test/testdata/output/basic.golden
new file mode 100644
index 0000000..e200f97
--- /dev/null
+++ b/test/testdata/output/basic.golden
@@ -0,0 +1,3 @@
+YAML: YAML Ain't Markup Language
+---
+YAML: hi
diff --git a/test/testdata/output/basic_list.golden b/test/testdata/output/basic_list.golden
new file mode 100644
index 0000000..c3a2b65
--- /dev/null
+++ b/test/testdata/output/basic_list.golden
@@ -0,0 +1,29 @@
+- 0
+- 1
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+---
+- 0
+- 2
+- 3
+- 4
+---
+- 0
+- 2
+- 5
+- 4
+---
+- 6
+- 0
+- 2
+- 5
+- 4
+---
+- 6
+- 0
+- 7
+- 8
diff --git a/test/testdata/output/nested_block_map.golden b/test/testdata/output/nested_block_map.golden
new file mode 100644
index 0000000..94c4742
--- /dev/null
+++ b/test/testdata/output/nested_block_map.golden
@@ -0,0 +1,30 @@
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+---
+a: 1
+b: 
+  d: 4
+  e: 6
+c: 3
+---
+a: 1
+b: 
+  d: 4
+  e: 
+    - 1
+    - 2
+    - 3
+c: 3
+---
+a: 1
+b: 
+  d: 4
+  e: 
+    - 1
+    - 2
+    - 3
+  f: 6
+c: 3
diff --git a/test/testdata/output/pubspec.golden b/test/testdata/output/pubspec.golden
new file mode 100644
index 0000000..f058bb8
--- /dev/null
+++ b/test/testdata/output/pubspec.golden
@@ -0,0 +1,88 @@
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  yaml: ^2.2.1 # For YAML
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  meta: ^1.1.8 # To annotate
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  retry: ^3.0.1
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
+---
+name: yaml_edit
+description: A library for YAML manipulation with comment and whitespace preservation.
+version: 1.0.0
+
+homepage: https://github.com/google/dart-neats/tree/master/yaml_edit
+
+repository: https://github.com/google/dart-neats.git
+
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:yaml_edit
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  quiver_hashcode: ^2.0.0 # For hashcodes
+  retry: ^3.0.1
+  yaml: ^3.2.0 # For YAML
+
+dev_dependencies:
+  pedantic: ^1.9.0
+  test: ^1.14.4
diff --git a/test/testdata/output/spaces.golden b/test/testdata/output/spaces.golden
new file mode 100644
index 0000000..7ee3372
--- /dev/null
+++ b/test/testdata/output/spaces.golden
@@ -0,0 +1,17 @@
+a: ' space around me '
+c: 'hello world'
+---
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+---
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+d: " "
+---
+"\ne": with newline
+a: ' space around me '
+b: " also spaced "
+c: 'hello world'
+d: " "
diff --git a/test/testdata/output/special_keywords.golden b/test/testdata/output/special_keywords.golden
new file mode 100644
index 0000000..e0c179d
--- /dev/null
+++ b/test/testdata/output/special_keywords.golden
@@ -0,0 +1,24 @@
+~: null
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: No
+true: true
+---
+~: tilde
+false: false
+No: no
+true: true
+---
+~: tilde
+false: false
+No: no
+true: true
diff --git a/test/update_test.dart b/test/update_test.dart
new file mode 100644
index 0000000..f8492a1
--- /dev/null
+++ b/test/update_test.dart
@@ -0,0 +1,773 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('throws', () {
+    test('RangeError in list if index is negative', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([-1], 'test'), throwsRangeError);
+    });
+
+    test('RangeError in list if index is larger than list length', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([2], 'test'), throwsRangeError);
+    });
+
+    test('PathError in list if attempting to set a key of a scalar', () {
+      final doc = YamlEditor("- YAML Ain't Markup Language");
+      expect(() => doc.update([0, 'a'], 'a'), throwsPathError);
+    });
+  });
+
+  group('works on top-level', () {
+    test('empty document', () {
+      final doc = YamlEditor('');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces string in document containing only a string', () {
+      final doc = YamlEditor('test');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level string to map', () {
+      final doc = YamlEditor('test');
+      doc.update([], {'a': 1});
+
+      expect(doc.toString(), equals('a: 1'));
+      expectYamlBuilderValue(doc, {'a': 1});
+    });
+
+    test('replaces top-level list', () {
+      final doc = YamlEditor('- 1');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level map', () {
+      final doc = YamlEditor('a: 1');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+
+    test('replaces top-level map with comment', () {
+      final doc = YamlEditor('a: 1 # comment');
+      doc.update([], 'replacement');
+
+      expect(doc.toString(), equals('replacement # comment'));
+      expectYamlBuilderValue(doc, 'replacement');
+    });
+  });
+
+  group('replaces in', () {
+    group('block map', () {
+      test('(1)', () {
+        final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML: test'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor('test: test');
+        doc.update(['test'], []);
+
+        expect(doc.toString(), equals('test: []'));
+        expectYamlBuilderValue(doc, {'test': []});
+      });
+
+      test('with comment', () {
+        final doc = YamlEditor("YAML: YAML Ain't Markup Language # comment");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('YAML: test # comment'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('nested', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 
+  d: 4
+  e: 6
+c: 3
+'''));
+
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested (2)', () {
+        final doc = YamlEditor('''
+a: 1
+b: {d: 4, e: 5}
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: {d: 4, e: 6}
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested (3)', () {
+        final doc = YamlEditor('''
+a:
+ b: 4
+''');
+        doc.update(['a'], true);
+
+        expect(doc.toString(), equals('''
+a: true
+'''));
+
+        expectYamlBuilderValue(doc, {'a': true});
+      });
+
+      test('nested (4)', () {
+        final doc = YamlEditor('''
+a: 1
+''');
+        doc.update([
+          'a'
+        ], [
+          {'a': true, 'b': false}
+        ]);
+
+        expectYamlBuilderValue(doc, {
+          'a': [
+            {'a': true, 'b': false}
+          ]
+        });
+      });
+
+      test('nested (5)', () {
+        final doc = YamlEditor('''
+a: 
+  - a: 1
+    b: 2
+  - null
+''');
+        doc.update(['a', 0], false);
+        expect(doc.toString(), equals('''
+a: 
+  - false
+
+  - null
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [false, null]
+        });
+      });
+
+      test('nested (6)', () {
+        final doc = YamlEditor('''
+a: 
+  - - 1
+    - 2
+  - null
+''');
+        doc.update(['a', 0], false);
+        expect(doc.toString(), equals('''
+a: 
+  - false
+
+  - null
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [false, null]
+        });
+      });
+
+      test('nested (7)', () {
+        final doc = YamlEditor('''
+a:
+  - - 0
+b: false
+''');
+        doc.update(['a', 0], true);
+
+        expect(doc.toString(), equals('''
+a:
+  - true
+
+b: false
+'''));
+      });
+
+      test('nested scalar -> flow list', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], [1, 2, 3]);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 
+  d: 4
+  e: 
+    - 1
+    - 2
+    - 3
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {
+            'd': 4,
+            'e': [1, 2, 3]
+          },
+          'c': 3
+        });
+      });
+
+      test('nested block map -> scalar', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b'], 2);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3});
+      });
+
+      test('nested block map -> scalar with comments', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+
+
+# comment
+''');
+        doc.update(['b'], 2);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+
+
+# comment
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': 2,
+        });
+      });
+
+      test('nested scalar -> block map', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5
+c: 3
+''');
+        doc.update(['b', 'e'], {'x': 3, 'y': 4});
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 
+  d: 4
+  e: 
+    x: 3
+    y: 4
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {
+            'd': 4,
+            'e': {'x': 3, 'y': 4}
+          },
+          'c': 3
+        });
+      });
+
+      test('nested block map with comments', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4
+  e: 5 # comment
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 
+  d: 4
+  e: 6 # comment
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+
+      test('nested block map with comments (2)', () {
+        final doc = YamlEditor('''
+a: 1
+b: 
+  d: 4 # comment
+# comment
+  e: 5 # comment
+# comment
+c: 3
+''');
+        doc.update(['b', 'e'], 6);
+
+        expect(doc.toString(), equals('''
+a: 1
+b: 
+  d: 4 # comment
+# comment
+  e: 6 # comment
+# comment
+c: 3
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': 1,
+          'b': {'d': 4, 'e': 6},
+          'c': 3
+        });
+      });
+    });
+
+    group('flow map', () {
+      test('(1)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'], 'test');
+
+        expect(doc.toString(), equals('{YAML: test}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'], 'd9]zH`FoYC/>]');
+
+        expect(doc.toString(), equals('{YAML: "d9]zH`FoYC\\/>]"}'));
+        expectYamlBuilderValue(doc, {'YAML': 'd9]zH`FoYC/>]'});
+      });
+
+      test('with spacing', () {
+        final doc = YamlEditor(
+            "{ YAML:  YAML Ain't Markup Language , XML: Extensible Markup Language , HTML: Hypertext Markup Language }");
+        doc.update(['XML'], 'XML Markup Language');
+
+        expect(
+            doc.toString(),
+            equals(
+                "{ YAML:  YAML Ain't Markup Language , XML: XML Markup Language, HTML: Hypertext Markup Language }"));
+        expectYamlBuilderValue(doc, {
+          'YAML': "YAML Ain't Markup Language",
+          'XML': 'XML Markup Language',
+          'HTML': 'Hypertext Markup Language'
+        });
+      });
+    });
+
+    group('block list', () {
+      test('(1)', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('- test'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('nested (1)', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language");
+        doc.update([0], [1, 2]);
+
+        expect(doc.toString(), equals('- - 1\n  - 2'));
+        expectYamlBuilderValue(doc, [
+          [1, 2]
+        ]);
+      });
+
+      test('with comment', () {
+        final doc = YamlEditor("- YAML Ain't Markup Language # comment");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('- test # comment'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('with comment and spaces', () {
+        final doc = YamlEditor("-  YAML Ain't Markup Language  # comment");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('-  test  # comment'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('nested (2)', () {
+        final doc = YamlEditor('''
+- 0
+- - 0
+  - 1
+  - 2
+- 2
+- 3
+''');
+        doc.update([1, 1], 4);
+        expect(doc.toString(), equals('''
+- 0
+- - 0
+  - 4
+  - 2
+- 2
+- 3
+'''));
+
+        expectYamlBuilderValue(doc, [
+          0,
+          [0, 4, 2],
+          2,
+          3
+        ]);
+      });
+
+      test('nested (3)', () {
+        final doc = YamlEditor('''
+- 0
+- 1
+''');
+        doc.update([0], {'item': 'Super Hoop', 'quantity': 1});
+        doc.update([1], {'item': 'BasketBall', 'quantity': 4});
+        expect(doc.toString(), equals('''
+- item: Super Hoop
+  quantity: 1
+- item: BasketBall
+  quantity: 4
+'''));
+
+        expectYamlBuilderValue(doc, [
+          {'item': 'Super Hoop', 'quantity': 1},
+          {'item': 'BasketBall', 'quantity': 4}
+        ]);
+      });
+
+      test('nested list flow map -> scalar', () {
+        final doc = YamlEditor('''
+- 0
+- {a: 1, b: 2}
+- 2
+- 3
+''');
+        doc.update([1], 4);
+        expect(doc.toString(), equals('''
+- 0
+- 4
+- 2
+- 3
+'''));
+        expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+      });
+
+      test('nested list-map-list-number update', () {
+        final doc = YamlEditor('''
+- 0
+- a:
+   - 1
+   - 2
+   - 3
+- 2
+- 3
+''');
+        doc.update([1, 'a', 0], 15);
+        expect(doc.toString(), equals('''
+- 0
+- a:
+   - 15
+   - 2
+   - 3
+- 2
+- 3
+'''));
+        expectYamlBuilderValue(doc, [
+          0,
+          {
+            'a': [15, 2, 3]
+          },
+          2,
+          3
+        ]);
+      });
+    });
+
+    group('flow list', () {
+      test('(1)', () {
+        final doc = YamlEditor("[YAML Ain't Markup Language]");
+        doc.update([0], 'test');
+
+        expect(doc.toString(), equals('[test]'));
+        expectYamlBuilderValue(doc, ['test']);
+      });
+
+      test('(2)', () {
+        final doc = YamlEditor("[YAML Ain't Markup Language]");
+        doc.update([0], [1, 2, 3]);
+
+        expect(doc.toString(), equals('[[1, 2, 3]]'));
+        expectYamlBuilderValue(doc, [
+          [1, 2, 3]
+        ]);
+      });
+
+      test('with spacing (1)', () {
+        final doc = YamlEditor('[ 0 , 1 , 2 , 3 ]');
+        doc.update([1], 4);
+
+        expect(doc.toString(), equals('[ 0 , 4, 2 , 3 ]'));
+        expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+      });
+    });
+  });
+
+  group('adds to', () {
+    group('flow map', () {
+      test('that is empty ', () {
+        final doc = YamlEditor('{}');
+        doc.update(['a'], 1);
+        expect(doc.toString(), equals('{a: 1}'));
+        expectYamlBuilderValue(doc, {'a': 1});
+      });
+
+      test('that is empty (2)', () {
+        final doc = YamlEditor('''
+- {}
+- []
+''');
+        doc.update([0, 'a'], [1]);
+        expect(doc.toString(), equals('''
+- {a: [1]}
+- []
+'''));
+        expectYamlBuilderValue(doc, [
+          {
+            'a': [1]
+          },
+          []
+        ]);
+      });
+
+      test('(1)', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['XML'], 'Extensible Markup Language');
+
+        expect(
+            doc.toString(),
+            equals(
+                "{XML: Extensible Markup Language, YAML: YAML Ain't Markup Language}"));
+        expectYamlBuilderValue(doc, {
+          'XML': 'Extensible Markup Language',
+          'YAML': "YAML Ain't Markup Language",
+        });
+      });
+    });
+
+    group('block map', () {
+      test('(1)', () {
+        final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+''');
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+d: 4
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      });
+
+      /// Regression testing to ensure it works without leading wtesttespace
+      test('(2)', () {
+        final doc = YamlEditor('a: 1');
+        doc.update(['b'], 2);
+        expect(doc.toString(), equals('''a: 1
+b: 2
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2});
+      });
+
+      test('(3)', () {
+        final doc = YamlEditor('''
+a:
+  aa: 1
+  zz: 1
+''');
+        doc.update([
+          'a',
+          'bb'
+        ], {
+          'aaa': {'dddd': 'c'},
+          'bbb': [0, 1, 2]
+        });
+
+        expect(doc.toString(), equals('''
+a:
+  aa: 1
+  bb: 
+    aaa:
+      dddd: c
+    bbb:
+      - 0
+      - 1
+      - 2
+  zz: 1
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': {
+            'aa': 1,
+            'bb': {
+              'aaa': {'dddd': 'c'},
+              'bbb': [0, 1, 2]
+            },
+            'zz': 1
+          }
+        });
+      });
+
+      test('(4)', () {
+        final doc = YamlEditor('''
+a:
+  aa: 1
+  zz: 1
+''');
+        doc.update([
+          'a',
+          'bb'
+        ], [
+          0,
+          [1, 2],
+          {'aaa': 'b', 'bbb': 'c'}
+        ]);
+
+        expect(doc.toString(), equals('''
+a:
+  aa: 1
+  bb: 
+    - 0
+    - - 1
+      - 2
+    - aaa: b
+      bbb: c
+  zz: 1
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': {
+            'aa': 1,
+            'bb': [
+              0,
+              [1, 2],
+              {'aaa': 'b', 'bbb': 'c'}
+            ],
+            'zz': 1
+          }
+        });
+      });
+
+      test('with complex keys', () {
+        final doc = YamlEditor('''
+? Sammy Sosa
+? Ken Griff''');
+        doc.update(['Mark McGwire'], null);
+        expect(doc.toString(), equals('''
+? Sammy Sosa
+? Ken Griff
+Mark McGwire: null
+'''));
+        expectYamlBuilderValue(
+            doc, {'Sammy Sosa': null, 'Ken Griff': null, 'Mark McGwire': null});
+      });
+
+      test('with trailing newline', () {
+        final doc = YamlEditor('''
+a: 1
+b: 2
+c: 3
+
+
+''');
+        doc.update(['d'], 4);
+        expect(doc.toString(), equals('''
+a: 1
+b: 2
+c: 3
+d: 4
+
+
+'''));
+        expectYamlBuilderValue(doc, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      });
+    });
+  });
+}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..aaea08d
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,457 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+
+import 'package:yaml_edit/src/utils.dart';
+import 'package:yaml_edit/src/wrap.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:yaml/yaml.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('indentation', () {
+    test('returns 2 for empty strings', () {
+      final doc = YamlEditor('');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('returns 2 for strings consisting only scalars', () {
+      final doc = YamlEditor('foo');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('returns 2 if only top-level elements are present', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 3''');
+      expect(getIndentation(doc), equals(2));
+    });
+
+    test('detects the indentation used in nested list', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+   - 3
+   - 4''');
+      expect(getIndentation(doc), equals(3));
+    });
+
+    test('detects the indentation used in nested map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c:
+   d: 4
+   e: 5''');
+      expect(getIndentation(doc), equals(3));
+    });
+
+    test('detects the indentation used in nested map in list', () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+    d: 4
+    e: 5''');
+      expect(getIndentation(doc), equals(4));
+    });
+
+    test('detects the indentation used in nested map in list with complex keys',
+        () {
+      final doc = YamlEditor('''
+- 1
+- 2
+- 
+    ? d
+    : 4''');
+      expect(getIndentation(doc), equals(4));
+    });
+
+    test('detects the indentation used in nested list in map', () {
+      final doc = YamlEditor('''
+a: 1
+b: 2
+c:
+  - 4
+  - 5''');
+      expect(getIndentation(doc), equals(2));
+    });
+  });
+
+  group('styling options', () {
+    group('update', () {
+      test('flow map with style', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(['YAML'],
+            wrapAsYamlNode('hi', scalarStyle: ScalarStyle.DOUBLE_QUOTED));
+
+        expect(doc.toString(), equals('{YAML: "hi"}'));
+        expectYamlBuilderValue(doc, {'YAML': 'hi'});
+      });
+
+      test('prevents block scalars in flow map', () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(
+            ['YAML'], wrapAsYamlNode('test', scalarStyle: ScalarStyle.FOLDED));
+
+        expect(doc.toString(), equals('{YAML: test}'));
+        expectYamlBuilderValue(doc, {'YAML': 'test'});
+      });
+
+      test('wraps string in double-quotes if it contains dangerous characters',
+          () {
+        final doc = YamlEditor("{YAML: YAML Ain't Markup Language}");
+        doc.update(
+            ['YAML'], wrapAsYamlNode('> test', scalarStyle: ScalarStyle.PLAIN));
+
+        expect(doc.toString(), equals('{YAML: "> test"}'));
+        expectYamlBuilderValue(doc, {'YAML': '> test'});
+      });
+
+      test('list in map', () {
+        final doc = YamlEditor('''YAML: YAML Ain't Markup Language''');
+        doc.update(['YAML'],
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW));
+
+        expect(doc.toString(), equals('YAML: [1, 2, 3]'));
+        expectYamlBuilderValue(doc, {
+          'YAML': [1, 2, 3]
+        });
+      });
+
+      test('nested map', () {
+        final doc = YamlEditor('''YAML: YAML Ain't Markup Language''');
+        doc.update(
+            ['YAML'],
+            wrapAsYamlNode({'YAML': "YAML Ain't Markup Language"},
+                collectionStyle: CollectionStyle.FLOW));
+
+        expect(
+            doc.toString(), equals("YAML: {YAML: YAML Ain't Markup Language}"));
+        expectYamlBuilderValue(doc, {
+          'YAML': {'YAML': "YAML Ain't Markup Language"}
+        });
+      });
+
+      test('nested list', () {
+        final doc = YamlEditor('- 0');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              1,
+              2,
+              wrapAsYamlNode([3, 4], collectionStyle: CollectionStyle.FLOW),
+              5
+            ]));
+
+        expect(doc.toString(), equals('''
+- - 1
+  - 2
+  - [3, 4]
+  - 5'''));
+        expectYamlBuilderValue(doc, [
+          [
+            1,
+            2,
+            [3, 4],
+            5
+          ]
+        ]);
+      });
+
+      test('different scalars in block list!', () {
+        final doc = YamlEditor('- 0');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              wrapAsYamlNode('plain string', scalarStyle: ScalarStyle.PLAIN),
+              wrapAsYamlNode('folded string', scalarStyle: ScalarStyle.FOLDED),
+              wrapAsYamlNode('single-quoted string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              wrapAsYamlNode('literal string',
+                  scalarStyle: ScalarStyle.LITERAL),
+              wrapAsYamlNode('double-quoted string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            ]));
+
+        expect(doc.toString(), equals('''
+- - plain string
+  - >-
+      folded string
+  - 'single-quoted string'
+  - |-
+      literal string
+  - "double-quoted string"'''));
+        expectYamlBuilderValue(doc, [
+          [
+            'plain string',
+            'folded string',
+            'single-quoted string',
+            'literal string',
+            'double-quoted string',
+          ]
+        ]);
+      });
+
+      test('different scalars in block map!', () {
+        final doc = YamlEditor('strings: strings');
+        doc.update(
+            ['strings'],
+            wrapAsYamlNode({
+              'plain': wrapAsYamlNode('string', scalarStyle: ScalarStyle.PLAIN),
+              'folded':
+                  wrapAsYamlNode('string', scalarStyle: ScalarStyle.FOLDED),
+              'single-quoted': wrapAsYamlNode('string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              'literal':
+                  wrapAsYamlNode('string', scalarStyle: ScalarStyle.LITERAL),
+              'double-quoted': wrapAsYamlNode('string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            }));
+
+        expect(doc.toString(), equals('''
+strings: 
+  plain: string
+  folded: >-
+      string
+  single-quoted: 'string'
+  literal: |-
+      string
+  double-quoted: "string"'''));
+        expectYamlBuilderValue(doc, {
+          'strings': {
+            'plain': 'string',
+            'folded': 'string',
+            'single-quoted': 'string',
+            'literal': 'string',
+            'double-quoted': 'string',
+          }
+        });
+      });
+
+      test('different scalars in flow list!', () {
+        final doc = YamlEditor('[0]');
+        doc.update(
+            [0],
+            wrapAsYamlNode([
+              wrapAsYamlNode('plain string', scalarStyle: ScalarStyle.PLAIN),
+              wrapAsYamlNode('folded string', scalarStyle: ScalarStyle.FOLDED),
+              wrapAsYamlNode('single-quoted string',
+                  scalarStyle: ScalarStyle.SINGLE_QUOTED),
+              wrapAsYamlNode('literal string',
+                  scalarStyle: ScalarStyle.LITERAL),
+              wrapAsYamlNode('double-quoted string',
+                  scalarStyle: ScalarStyle.DOUBLE_QUOTED),
+            ]));
+
+        expect(
+            doc.toString(),
+            equals(
+                '[[plain string, folded string, \'single-quoted string\', literal string, "double-quoted string"]]'));
+        expectYamlBuilderValue(doc, [
+          [
+            'plain string',
+            'folded string',
+            'single-quoted string',
+            'literal string',
+            'double-quoted string',
+          ]
+        ]);
+      });
+
+      test('wraps non-printable strings in double-quotes in flow context', () {
+        final doc = YamlEditor('[0]');
+        doc.update([0], '\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"');
+        expect(
+            doc.toString(), equals('["\\0\\a\\b\\v\\f\\r\\e\\N\\_\\L\\P\\""]'));
+        expectYamlBuilderValue(
+            doc, ['\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"']);
+      });
+
+      test('wraps non-printable strings in double-quotes in block context', () {
+        final doc = YamlEditor('- 0');
+        doc.update([0], '\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"');
+        expect(
+            doc.toString(), equals('- "\\0\\a\\b\\v\\f\\r\\e\\N\\_\\L\\P\\""'));
+        expectYamlBuilderValue(
+            doc, ['\x00\x07\x08\x0b\x0c\x0d\x1b\x85\xa0\u2028\u2029"']);
+      });
+
+      test('generates folded strings properly', () {
+        final doc = YamlEditor('');
+        doc.update(
+            [], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('>-\n  test\n\n  test'));
+      });
+
+      test('rewrites folded strings properly', () {
+        final doc = YamlEditor('''
+- >
+    folded string
+''');
+        doc.update(
+            [0], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('''
+- >-
+    test
+
+    test
+'''));
+      });
+
+      test('rewrites folded strings properly (1)', () {
+        final doc = YamlEditor('''
+- >
+    folded string''');
+        doc.update(
+            [0], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.FOLDED));
+        expect(doc.toString(), equals('''
+- >-
+    test
+
+    test'''));
+      });
+
+      test('generates literal strings properly', () {
+        final doc = YamlEditor('');
+        doc.update(
+            [], wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.LITERAL));
+        expect(doc.toString(), equals('|-\n  test\n  test'));
+      });
+
+      test('rewrites literal strings properly', () {
+        final doc = YamlEditor('''
+- |
+    literal string
+''');
+        doc.update([0],
+            wrapAsYamlNode('test\ntest', scalarStyle: ScalarStyle.LITERAL));
+        expect(doc.toString(), equals('''
+- |-
+    test
+    test
+'''));
+      });
+
+      test('prevents literal strings in flow maps, even if nested', () {
+        final doc = YamlEditor('''
+{1: 1}
+''');
+        doc.update([
+          1
+        ], [
+          wrapAsYamlNode('d9]zH`FoYC/>]', scalarStyle: ScalarStyle.LITERAL)
+        ]);
+
+        expect(doc.toString(), equals('''
+{1: ["d9]zH`FoYC\\/>]"]}
+'''));
+        expect((doc.parseAt([1, 0]) as dynamic).style,
+            equals(ScalarStyle.DOUBLE_QUOTED));
+      });
+
+      test('prevents literal empty strings', () {
+        final doc = YamlEditor('''
+a:
+  c: 1
+''');
+        doc.update([
+          'a'
+        ], {
+          'f': wrapAsYamlNode('', scalarStyle: ScalarStyle.LITERAL),
+          'g': 1
+        });
+
+        expect(doc.toString(), equals('''
+a: 
+  f: ""
+  g: 1
+'''));
+      });
+
+      test('prevents literal strings with leading spaces', () {
+        final doc = YamlEditor('''
+a:
+  c: 1
+''');
+        doc.update([
+          'a'
+        ], {
+          'f': wrapAsYamlNode(' a', scalarStyle: ScalarStyle.LITERAL),
+          'g': 1
+        });
+
+        expect(doc.toString(), equals('''
+a: 
+  f: " a"
+  g: 1
+'''));
+      });
+
+      test(
+          'flow collection structure does not get substringed when added to block structure',
+          () {
+        final doc = YamlEditor('''
+a:
+  - false
+''');
+        doc.prependToList(['a'],
+            wrapAsYamlNode([1234], collectionStyle: CollectionStyle.FLOW));
+        expect(doc.toString(), equals('''
+a:
+  - [1234]
+  - false
+'''));
+        expectYamlBuilderValue(doc, {
+          'a': [
+            [1234],
+            false
+          ]
+        });
+      });
+    });
+  });
+
+  group('assertValidScalar', () {
+    test('does nothing with a boolean', () {
+      expect(() => assertValidScalar(true), returnsNormally);
+    });
+
+    test('does nothing with a number', () {
+      expect(() => assertValidScalar(1.12), returnsNormally);
+    });
+    test('does nothing with infinity', () {
+      expect(() => assertValidScalar(double.infinity), returnsNormally);
+    });
+    test('does nothing with a String', () {
+      expect(() => assertValidScalar('test'), returnsNormally);
+    });
+
+    test('does nothing with null', () {
+      expect(() => assertValidScalar(null), returnsNormally);
+    });
+
+    test('throws on map', () {
+      expect(() => assertValidScalar({'a': 1}), throwsArgumentError);
+    });
+
+    test('throws on list', () {
+      expect(() => assertValidScalar([1]), throwsArgumentError);
+    });
+  });
+}
diff --git a/test/windows_test.dart b/test/windows_test.dart
new file mode 100644
index 0000000..d36c450
--- /dev/null
+++ b/test/windows_test.dart
@@ -0,0 +1,219 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:yaml_edit/yaml_edit.dart';
+import 'package:yaml_edit/src/utils.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('windows line ending detection', () {
+    test('empty string gives not windows', () {
+      final doc = YamlEditor('');
+      expect(getLineEnding(doc.toString()), equals('\n'));
+    });
+
+    test('accurately detects windows documents', () {
+      final doc = YamlEditor('\r\n');
+      expect(getLineEnding(doc.toString()), equals('\r\n'));
+    });
+
+    test('accurately detects windows documents (2)', () {
+      final doc = YamlEditor('''
+a:\r
+  b:\r
+    - 1\r
+    - 2\r
+c: 3\r
+''');
+      expect(getLineEnding(doc.toString()), equals('\r\n'));
+    });
+  });
+
+  group('modification with windows line endings', () {
+    test('append element to simple block list ', () {
+      final doc = YamlEditor('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+''');
+      doc.appendToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+- - 4\r
+  - 5\r
+  - 6\r
+'''));
+      expectYamlBuilderValue(doc, [
+        0,
+        1,
+        2,
+        3,
+        [4, 5, 6]
+      ]);
+    });
+
+    test('update nested scalar -> flow list', () {
+      final doc = YamlEditor('''
+a: 1\r
+b: \r
+  d: 4\r
+  e: 5\r
+c: 3\r
+''');
+      doc.update(['b', 'e'], [1, 2, 3]);
+
+      expect(doc.toString(), equals('''
+a: 1\r
+b: \r
+  d: 4\r
+  e: \r
+    - 1\r
+    - 2\r
+    - 3\r
+c: 3\r
+'''));
+      expectYamlBuilderValue(doc, {
+        'a': 1,
+        'b': {
+          'd': 4,
+          'e': [1, 2, 3]
+        },
+        'c': 3
+      });
+    });
+
+    test('update in nested list flow map -> scalar', () {
+      final doc = YamlEditor('''
+- 0\r
+- {a: 1, b: 2}\r
+- 2\r
+- 3\r
+''');
+      doc.update([1], 4);
+      expect(doc.toString(), equals('''
+- 0\r
+- 4\r
+- 2\r
+- 3\r
+'''));
+      expectYamlBuilderValue(doc, [0, 4, 2, 3]);
+    });
+
+    test('insert into a list with comments', () {
+      final doc = YamlEditor('''
+- 0 # comment a\r
+- 2 # comment b\r
+''');
+      doc.insertIntoList([], 1, 1);
+      expect(doc.toString(), equals('''
+- 0 # comment a\r
+- 1\r
+- 2 # comment b\r
+'''));
+      expectYamlBuilderValue(doc, [0, 1, 2]);
+    });
+
+    test('prepend into a list', () {
+      final doc = YamlEditor('''
+- 1\r
+- 2\r
+''');
+      doc.prependToList([], [4, 5, 6]);
+      expect(doc.toString(), equals('''
+- - 4\r
+  - 5\r
+  - 6\r
+- 1\r
+- 2\r
+'''));
+      expectYamlBuilderValue(doc, [
+        [4, 5, 6],
+        1,
+        2
+      ]);
+    });
+
+    test('remove from block list ', () {
+      final doc = YamlEditor('''
+- 0\r
+- 1\r
+- 2\r
+- 3\r
+''');
+      doc.remove([1]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 2\r
+- 3\r
+'''));
+      expectYamlBuilderValue(doc, [0, 2, 3]);
+    });
+
+    test('remove from block list (2)', () {
+      final doc = YamlEditor('''
+- 0\r
+''');
+      doc.remove([0]);
+      expect(doc.toString(), equals('''
+[]\r
+'''));
+      expectYamlBuilderValue(doc, []);
+    });
+
+    test('remove from block map', () {
+      final doc = YamlEditor('''
+a: 1\r
+b: 2\r
+c: 3\r
+''');
+      doc.remove(['b']);
+      expect(doc.toString(), equals('''
+a: 1\r
+c: 3\r
+'''));
+    });
+
+    test('remove from block map (2)', () {
+      final doc = YamlEditor('''
+a: 1\r
+''');
+      doc.remove(['a']);
+      expect(doc.toString(), equals('''
+{}\r
+'''));
+      expectYamlBuilderValue(doc, {});
+    });
+
+    test('splice block list', () {
+      final doc = YamlEditor('''
+- 0\r
+- 0\r
+''');
+      final nodes = doc.spliceList([], 0, 2, [0, 1, 2]);
+      expect(doc.toString(), equals('''
+- 0\r
+- 1\r
+- 2\r
+'''));
+
+      expectDeepEquals(nodes.toList(), [0, 0]);
+    });
+  });
+}
diff --git a/test/wrap_test.dart b/test/wrap_test.dart
new file mode 100644
index 0000000..3aed89c
--- /dev/null
+++ b/test/wrap_test.dart
@@ -0,0 +1,342 @@
+// Copyright 2020 Garett Tok Ern Liang
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:yaml_edit/src/equality.dart';
+import 'package:yaml_edit/src/wrap.dart';
+import 'package:yaml/yaml.dart';
+
+import 'test_utils.dart';
+
+void main() {
+  group('wrapAsYamlNode', () {
+    group('checks for invalid scalars', () {
+      test('fails to wrap an invalid scalar', () {
+        expect(() => wrapAsYamlNode(File('test.dart')), throwsArgumentError);
+      });
+
+      test('fails to wrap an invalid map', () {
+        expect(() => wrapAsYamlNode({'a': File('test.dart')}),
+            throwsArgumentError);
+      });
+
+      test('fails to wrap an invalid list', () {
+        expect(
+            () => wrapAsYamlNode([
+                  'a',
+                  [File('test.dart')]
+                ]),
+            throwsArgumentError);
+      });
+
+      test('checks YamlScalar for invalid scalar value', () {
+        expect(() => wrapAsYamlNode(YamlScalar.wrap(File('test.dart'))),
+            throwsArgumentError);
+      });
+
+      test('checks YamlMap for deep invalid scalar value', () {
+        expect(
+            () => wrapAsYamlNode(YamlMap.wrap({
+                  'a': {'b': File('test.dart')}
+                })),
+            throwsArgumentError);
+      });
+
+      test('checks YamlList for deep invalid scalar value', () {
+        expect(
+            () => wrapAsYamlNode(YamlList.wrap([
+                  'a',
+                  [File('test.dart')]
+                ])),
+            throwsArgumentError);
+      });
+    });
+
+    test('wraps scalars', () {
+      final scalar = wrapAsYamlNode('foo');
+
+      expect((scalar as YamlScalar).style, equals(ScalarStyle.ANY));
+      expect(scalar.value, equals('foo'));
+    });
+
+    test('wraps scalars with style', () {
+      final scalar =
+          wrapAsYamlNode('foo', scalarStyle: ScalarStyle.DOUBLE_QUOTED);
+
+      expect((scalar as YamlScalar).style, equals(ScalarStyle.DOUBLE_QUOTED));
+      expect(scalar.value, equals('foo'));
+    });
+
+    test('wraps lists', () {
+      final list = wrapAsYamlNode([
+        [1, 2, 3],
+        {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'value'
+      ]);
+
+      expect(
+          list,
+          equals([
+            [1, 2, 3],
+            {
+              'foo': 'bar',
+              'nested': [4, 5, 6]
+            },
+            'value'
+          ]));
+      expect((list as YamlList).style, equals(CollectionStyle.ANY));
+      expect((list as YamlList)[0].style, equals(CollectionStyle.ANY));
+      expect((list as YamlList)[1].style, equals(CollectionStyle.ANY));
+    });
+
+    test('wraps lists with collectionStyle', () {
+      final list = wrapAsYamlNode([
+        [1, 2, 3],
+        {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'value'
+      ], collectionStyle: CollectionStyle.BLOCK);
+
+      expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+      expect((list as YamlList)[0].style, equals(CollectionStyle.ANY));
+      expect((list as YamlList)[1].style, equals(CollectionStyle.ANY));
+    });
+
+    test('wraps nested lists while preserving style', () {
+      final list = wrapAsYamlNode([
+        wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        wrapAsYamlNode({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }, collectionStyle: CollectionStyle.FLOW),
+        'value'
+      ], collectionStyle: CollectionStyle.BLOCK);
+
+      expect((list as YamlList).style, equals(CollectionStyle.BLOCK));
+      expect((list as YamlList)[0].style, equals(CollectionStyle.FLOW));
+      expect((list as YamlList)[1].style, equals(CollectionStyle.FLOW));
+    });
+
+    test('wraps maps', () {
+      final map = wrapAsYamlNode({
+        'list': [1, 2, 3],
+        'map': {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'scalar': 'value'
+      });
+
+      expect(
+          map,
+          equals({
+            'list': [1, 2, 3],
+            'map': {
+              'foo': 'bar',
+              'nested': [4, 5, 6]
+            },
+            'scalar': 'value'
+          }));
+
+      expect((map as YamlMap).style, equals(CollectionStyle.ANY));
+    });
+
+    test('wraps maps with collectionStyle', () {
+      final map = wrapAsYamlNode({
+        'list': [1, 2, 3],
+        'map': {
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        },
+        'scalar': 'value'
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+    });
+
+    test('wraps nested maps while preserving style', () {
+      final map = wrapAsYamlNode({
+        'list':
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        'map': wrapAsYamlNode({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }, collectionStyle: CollectionStyle.BLOCK),
+        'scalar': 'value'
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+      expect((map as YamlMap)['list'].style, equals(CollectionStyle.FLOW));
+      expect((map as YamlMap)['map'].style, equals(CollectionStyle.BLOCK));
+    });
+
+    test('works with YamlMap.wrap', () {
+      final map = wrapAsYamlNode({
+        'list':
+            wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW),
+        'map': YamlMap.wrap({
+          'foo': 'bar',
+          'nested': [4, 5, 6]
+        }),
+      }, collectionStyle: CollectionStyle.BLOCK);
+
+      expect((map as YamlMap).style, equals(CollectionStyle.BLOCK));
+      expect((map as YamlMap)['list'].style, equals(CollectionStyle.FLOW));
+      expect((map as YamlMap)['map'].style, equals(CollectionStyle.ANY));
+    });
+  });
+
+  group('deepHashCode', () {
+    test('returns the same result for scalar and its value', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 = deepHashCode(wrapAsYamlNode('foo'));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns different results for different values', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 = deepHashCode(wrapAsYamlNode('bar'));
+
+      expect(hashCode1, notEquals(hashCode2));
+    });
+
+    test('returns the same result for YamlScalar with style and its value', () {
+      final hashCode1 = deepHashCode('foo');
+      final hashCode2 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.LITERAL));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlScalars with same value but different styles',
+        () {
+      final hashCode1 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.PLAIN));
+      final hashCode2 =
+          deepHashCode(wrapAsYamlNode('foo', scalarStyle: ScalarStyle.LITERAL));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and its value', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode(wrapAsYamlNode([1, 2, 3]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and the YamlList.wrap() value', () {
+      final hashCode1 = deepHashCode([
+        1,
+        [1, 2],
+        3
+      ]);
+      final hashCode2 = deepHashCode(YamlList.wrap([
+        1,
+        YamlList.wrap([1, 2]),
+        3
+      ]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the different results for different lists', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode([1, 2, 4]);
+      final hashCode3 = deepHashCode([1, 2, 3, 4]);
+
+      expect(hashCode1, notEquals(hashCode2));
+      expect(hashCode2, notEquals(hashCode3));
+      expect(hashCode3, notEquals(hashCode1));
+    });
+
+    test('returns the same result for YamlList with style and its value', () {
+      final hashCode1 = deepHashCode([1, 2, 3]);
+      final hashCode2 = deepHashCode(
+          wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlLists with same value but different styles',
+        () {
+      final hashCode1 = deepHashCode(
+          wrapAsYamlNode([1, 2, 3], collectionStyle: CollectionStyle.BLOCK));
+      final hashCode2 = deepHashCode(wrapAsYamlNode([1, 2, 3]));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for a map and its value', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2}));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the same result for list and the YamlList.wrap() value', () {
+      final hashCode1 = deepHashCode({
+        'a': 1,
+        'b': 2,
+        'c': {'d': 4, 'e': 5}
+      });
+      final hashCode2 = deepHashCode(YamlMap.wrap({
+        'a': 1,
+        'b': 2,
+        'c': YamlMap.wrap({'d': 4, 'e': 5})
+      }));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test('returns the different results for different maps', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode({'a': 1, 'b': 3});
+      final hashCode3 = deepHashCode({'a': 1, 'b': 2, 'c': 3});
+
+      expect(hashCode1, notEquals(hashCode2));
+      expect(hashCode2, notEquals(hashCode3));
+      expect(hashCode3, notEquals(hashCode1));
+    });
+
+    test('returns the same result for YamlMap with style and its value', () {
+      final hashCode1 = deepHashCode({'a': 1, 'b': 2});
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+
+    test(
+        'returns the same result for two YamlMaps with same value but different styles',
+        () {
+      final hashCode1 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.BLOCK));
+      final hashCode2 = deepHashCode(wrapAsYamlNode({'a': 1, 'b': 2},
+          collectionStyle: CollectionStyle.FLOW));
+
+      expect(hashCode1, equals(hashCode2));
+    });
+  });
+}