diff --git a/packages/parchment/lib/src/document/attributes.dart b/packages/parchment/lib/src/document/attributes.dart index 2e9cc650..120ee711 100644 --- a/packages/parchment/lib/src/document/attributes.dart +++ b/packages/parchment/lib/src/document/attributes.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; -import 'package:quiver/core.dart'; /// Scope of a style attribute, defines context in which an attribute can be /// applied. @@ -254,7 +253,7 @@ class ParchmentAttribute implements ParchmentAttributeBuilder { } @override - int get hashCode => hash3(key, scope, value); + int get hashCode => Object.hash(key, scope, value); @override String toString() => '$key: $value'; @@ -397,8 +396,9 @@ class ParchmentStyle { @override int get hashCode { - final hashes = _data.entries.map((entry) => hash2(entry.key, entry.value)); - return hashObjects(hashes); + final hashes = + _data.entries.map((entry) => Object.hash(entry.key, entry.value)); + return Object.hashAll(hashes); } @override diff --git a/packages/parchment/lib/src/document/embeds.dart b/packages/parchment/lib/src/document/embeds.dart index 1e4ec98a..a836afe8 100644 --- a/packages/parchment/lib/src/document/embeds.dart +++ b/packages/parchment/lib/src/document/embeds.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:quiver/core.dart'; const _dataEquality = DeepCollectionEquality(); @@ -60,12 +59,11 @@ class EmbeddableObject { @override int get hashCode { - if (_data.isEmpty) return hash2(type, inline); + if (_data.isEmpty) return Object.hash(type, inline); - final dataHash = hashObjects( - _data.entries.map((e) => hash2(e.key, e.value)), - ); - return hash3(type, inline, dataHash); + final dataHash = + Object.hashAll(_data.entries.map((e) => Object.hash(e.key, e.value))); + return Object.hash(type, inline, dataHash); } Map toJson() { diff --git a/packages/parchment/lib/src/document/leaf.dart b/packages/parchment/lib/src/document/leaf.dart index 8964107b..dc5cbc1d 100644 --- a/packages/parchment/lib/src/document/leaf.dart +++ b/packages/parchment/lib/src/document/leaf.dart @@ -29,6 +29,12 @@ abstract base class LeafNode extends Node with StyledNode { Object get value => _value; Object _value; + void _setValue(Object newValue) { + _value = newValue; + parent.invalidateLength(); + next?.invalidateOffset(); + } + /// Splits this leaf node at [index] and returns new node. /// /// If this is the last node in its list and [index] equals this node's @@ -48,7 +54,7 @@ abstract base class LeafNode extends Node with StyledNode { if (this is TextNode) { final text = _value as String; - _value = text.substring(0, index); + _setValue(text.substring(0, index)); final split = LeafNode(text.substring(index)); split.applyStyle(style); insertAfter(split); @@ -205,7 +211,7 @@ abstract base class LeafNode extends Node with StyledNode { var mergeWith = node.previous as TextNode; if (mergeWith.style == node.style) { final combinedValue = mergeWith.value + node.value; - mergeWith._value = combinedValue; + mergeWith._setValue(combinedValue); node.unlink(); node = mergeWith; } @@ -214,7 +220,7 @@ abstract base class LeafNode extends Node with StyledNode { var mergeWith = node.next as TextNode; if (mergeWith.style == node.style) { final combinedValue = node.value + mergeWith.value; - node._value = combinedValue; + node._setValue(combinedValue); mergeWith.unlink(); } } diff --git a/packages/parchment/lib/src/document/line.dart b/packages/parchment/lib/src/document/line.dart index 3d330a5c..25c19818 100644 --- a/packages/parchment/lib/src/document/line.dart +++ b/packages/parchment/lib/src/document/line.dart @@ -155,7 +155,6 @@ final class LineNode extends ContainerNode with StyledNode { @override LeafNode get defaultChild => TextNode(); - // TODO: should be able to cache length and invalidate on any child-related operation @override int get length => super.length + 1; diff --git a/packages/parchment/lib/src/document/node.dart b/packages/parchment/lib/src/document/node.dart index 3aaec730..47848b45 100644 --- a/packages/parchment/lib/src/document/node.dart +++ b/packages/parchment/lib/src/document/node.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:meta/meta.dart'; import 'package:parchment_delta/parchment_delta.dart'; import 'attributes.dart'; @@ -33,24 +34,32 @@ abstract base class Node extends LinkedListEntry { /// `null`. bool get mounted => _parent != null; + int? _offsetCache; + /// Offset in characters of this node relative to [parent] node. /// /// To get offset of this node in the document see [documentOffset]. int get offset { - if (isFirst) return 0; + if (_offsetCache != null) return _offsetCache!; + + if (isFirst) return _offsetCache = 0; var offset = 0; var node = this; do { node = node.previous!; offset += node.length; } while (!node.isFirst); - return offset; + return _offsetCache = offset; } + int? _documentOffsetCache; + /// Offset in characters of this node in the document. int get documentOffset { + if (_documentOffsetCache != null) return _documentOffsetCache!; + final parentOffset = (_parent is! RootNode) ? _parent!.documentOffset : 0; - return parentOffset + offset; + return _documentOffsetCache = parentOffset + offset; } /// Returns `true` if this node contains character at specified [offset] in @@ -86,6 +95,8 @@ abstract base class Node extends LinkedListEntry { assert(entry._parent == null && _parent != null); entry._parent = _parent; super.insertBefore(entry); + _parent?.invalidateLength(); + invalidateOffset(); } @override @@ -93,13 +104,31 @@ abstract base class Node extends LinkedListEntry { assert(entry._parent == null && _parent != null); entry._parent = _parent; super.insertAfter(entry); + parent?.invalidateLength(); + entry.invalidateOffset(); } @override void unlink() { assert(_parent != null); + final oldParent = _parent; + final oldNext = next; _parent = null; super.unlink(); + oldNext?.invalidateOffset(); + oldParent?.invalidateLength(); + } + + @mustCallSuper + void invalidateOffset() { + _offsetCache = null; + invalidateDocumentOffset(); + next?.invalidateOffset(); + } + + @mustCallSuper + void invalidateDocumentOffset() { + _documentOffsetCache = null; } } @@ -163,6 +192,8 @@ abstract base class ContainerNode extends Node { assert(node._parent == null); node._parent = this; _children.add(node); + node.invalidateOffset(); + invalidateLength(); } /// Adds [node] to the beginning of this container children list. @@ -170,13 +201,20 @@ abstract base class ContainerNode extends Node { assert(node._parent == null); node._parent = this; _children.addFirst(node); + node.invalidateOffset(); + invalidateLength(); } /// Removes [node] from this container. void remove(T node) { assert(node._parent == this); node._parent = null; - _children.remove(node); + final oldNext = node.next; + final removed = _children.remove(node); + if (removed) { + invalidateLength(); + oldNext?.invalidateOffset(); + } } /// Moves children of this node to [newParent]. @@ -222,10 +260,13 @@ abstract base class ContainerNode extends Node { @override String toPlainText() => children.map((child) => child.toPlainText()).join(); + int? _length; + /// Content length of this node's children. To get number of children in this /// node use [childCount]. @override - int get length => _children.fold(0, (current, node) => current + node.length); + int get length => _length ??= + _children.fold(0, (current, node) => current + node.length); @override void insert(int index, Object data, ParchmentStyle? style) { @@ -258,6 +299,20 @@ abstract base class ContainerNode extends Node { @override String toString() => _children.join('\n'); + + @override + void invalidateDocumentOffset() { + super.invalidateDocumentOffset(); + for (var child in children) { + child.invalidateDocumentOffset(); + } + } + + void invalidateLength() { + _length = null; + next?.invalidateOffset(); + parent?.invalidateLength(); + } } /// Mixin used by nodes that wish to implement [StyledNode] interface. diff --git a/packages/parchment/pubspec.yaml b/packages/parchment/pubspec.yaml index 3b94c377..039d9001 100644 --- a/packages/parchment/pubspec.yaml +++ b/packages/parchment/pubspec.yaml @@ -11,9 +11,9 @@ environment: dependencies: collection: ^1.18.0 parchment_delta: ^1.0.0 - quiver: ^3.2.1 html: ^0.15.4 + meta: ^1.12.0 dev_dependencies: - test: ^1.25.5 + test: ^1.25.8 lints: ^4.0.0 diff --git a/packages/parchment/test/document/leaf_test.dart b/packages/parchment/test/document/leaf_test.dart index 23b197aa..78406c96 100644 --- a/packages/parchment/test/document/leaf_test.dart +++ b/packages/parchment/test/document/leaf_test.dart @@ -96,8 +96,10 @@ void main() { test('insert in formatted node', () { line.retain(0, 6, boldStyle); expect(line.childCount, 2); + expect(line.children.last.offset, 6); line.insert(3, 'don', null); expect(line.childCount, 4); + expect(line.children.last.offset, 9); final b = boldStyle.toJson(); expect( line.children.elementAt(0).toDelta(), diff --git a/packages/parchment/test/document/line_test.dart b/packages/parchment/test/document/line_test.dart index b6a3328e..778e19ce 100644 --- a/packages/parchment/test/document/line_test.dart +++ b/packages/parchment/test/document/line_test.dart @@ -59,7 +59,10 @@ void main() { root.retain(0, 4, boldStyle); root.retain(16, 6, boldStyle); final line = root.first as LineNode; + final lastTextSegment = line.children.last; + expect(lastTextSegment.offset, 16); final newLine = line.splitAt(10); + expect(lastTextSegment.offset, 6); expect(line.toPlainText(), 'This house\n'); expect(newLine.toPlainText(), ' is a circus\n'); }); @@ -124,10 +127,17 @@ void main() { }); test('format line', () { - root.insert(0, 'Hello world', null); + root.insert(0, 'Hello world\n', null); + root.insert(12, 'Second headline\n', null); root.retain(11, 1, h1Style); root.retain(11, 1, rightStyle); + final secondHeadline = root.first.next!; + expect(secondHeadline.offset, 12); + + root.retain(27, 1, ParchmentStyle().merge(ParchmentAttribute.cl)); + expect(secondHeadline.offset, 0); + final line = root.first as LineNode; expect(line, hasLength(12)); diff --git a/packages/parchment/test/document/node_test.dart b/packages/parchment/test/document/node_test.dart index e7d418b5..a81a7b64 100644 --- a/packages/parchment/test/document/node_test.dart +++ b/packages/parchment/test/document/node_test.dart @@ -23,11 +23,20 @@ void main() { }); test('documentOffset', () { - root.insert(0, 'First line\nSecond line', null); - final line = root.children.last as LineNode; - final text = line.first as TextNode; - expect(line.documentOffset, 11); - expect(text.documentOffset, 11); + root.insert(0, 'First line\nSecond line\nThird line', null); + final secondLine = root.children.first.next as LineNode; + final thirdLine = root.children.last as LineNode; + expect(thirdLine.documentOffset, 23); + secondLine.insert(6, ' styled', ParchmentStyle.fromJson({'b': true})); + final styledText = secondLine.first.next as TextNode; + final lastText = secondLine.last as TextNode; + expect(secondLine.documentOffset, 11); + expect(thirdLine.documentOffset, 30); + expect(styledText.documentOffset, 17); + expect(lastText.documentOffset, 24); + secondLine.remove(styledText); + expect(lastText.documentOffset, 17); + expect(thirdLine.documentOffset, 23); }); test('containsOffset', () {