diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index 5f0abfbf1d..6138c79c0e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -8,7 +8,7 @@ export 'src/core/document/path.dart'; export 'src/document/position.dart'; export 'src/document/selection.dart'; export 'src/document/state_tree.dart'; -export 'src/document/text_delta.dart'; +export 'src/core/document/text_delta.dart'; export 'src/core/document/attributes.dart'; export 'src/document/built_in_attribute_keys.dart'; export 'src/editor_state.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart index 9095bc1478..68c27cd55b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/src/core/document/path.dart'; import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; class Node extends ChangeNotifier with LinkedListEntry { Node({ @@ -232,7 +232,7 @@ class TextNode extends Node { ); TextNode.empty({Attributes? attributes}) - : _delta = Delta([TextInsert('')]), + : _delta = Delta(operations: [TextInsert('')]), super( type: 'text', attributes: attributes ?? {}, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart similarity index 68% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart index d509a1ef14..5bf1832f73 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart @@ -1,170 +1,472 @@ import 'dart:collection'; import 'dart:math'; -import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:flutter/foundation.dart'; +import 'package:appflowy_editor/src/core/document/attributes.dart'; + // constant number: 2^53 - 1 const int _maxInt = 9007199254740991; -abstract class TextOperation { - bool get isEmpty => length == 0; +List stringIndexes(String text) { + final indexes = List.filled(text.length, 0); + final iterator = text.runes.iterator; + while (iterator.moveNext()) { + for (var i = 0; i < iterator.currentSize; i++) { + indexes[iterator.rawIndex + i] = iterator.rawIndex; + } + } + + return indexes; +} + +abstract class TextOperation { + Attributes? get attributes; int get length; - Attributes? get attributes => null; + bool get isEmpty => length == 0; Map toJson(); } class TextInsert extends TextOperation { - String content; + TextInsert( + this.text, { + Attributes? attributes, + }) : _attributes = attributes; + + String text; final Attributes? _attributes; - TextInsert(this.content, [Attributes? attrs]) : _attributes = attrs; + @override + int get length => text.length; @override - int get length { - return content.length; - } + Attributes? get attributes => _attributes != null ? {..._attributes!} : null; @override - Attributes? get attributes { - return _attributes; + Map toJson() { + final result = { + 'insert': text, + }; + if (_attributes != null) { + result['attributes'] = attributes; + } + return result; } @override bool operator ==(Object other) { - if (other is! TextInsert) { - return false; - } - return content == other.content && + if (identical(this, other)) return true; + + return other is TextInsert && + other.text == text && mapEquals(_attributes, other._attributes); } @override - int get hashCode { - final contentHash = content.hashCode; - final attrs = _attributes; - return Object.hash( - contentHash, - attrs != null ? hashAttributes(attrs) : null, - ); - } - - @override - Map toJson() { - final result = { - 'insert': content, - }; - final attrs = _attributes; - if (attrs != null) { - result['attributes'] = {...attrs}; - } - return result; - } + int get hashCode => text.hashCode ^ _attributes.hashCode; } class TextRetain extends TextOperation { - int _length; + TextRetain( + this.length, { + Attributes? attributes, + }) : _attributes = attributes; + + @override + int length; final Attributes? _attributes; - TextRetain(length, [Attributes? attributes]) - : _length = length, - _attributes = attributes; - @override - bool get isEmpty { - return length == 0; - } - - @override - int get length { - return _length; - } - - set length(int v) { - _length = v; - } - - @override - Attributes? get attributes { - return _attributes; - } - - @override - bool operator ==(Object other) { - if (other is! TextRetain) { - return false; - } - return _length == other.length && mapEquals(_attributes, other._attributes); - } - - @override - int get hashCode { - final attrs = _attributes; - return Object.hash( - _length, - attrs != null ? hashAttributes(attrs) : null, - ); - } + Attributes? get attributes => _attributes != null ? {..._attributes!} : null; @override Map toJson() { final result = { - 'retain': _length, + 'retain': length, }; - final attrs = _attributes; - if (attrs != null) { - result['attributes'] = {...attrs}; + if (_attributes != null) { + result['attributes'] = attributes; } return result; } -} - -class TextDelete extends TextOperation { - int _length; - - TextDelete(int length) : _length = length; - - @override - int get length { - return _length; - } - - set length(int v) { - _length = v; - } @override bool operator ==(Object other) { - if (other is! TextDelete) { - return false; - } - return _length == other.length; + if (identical(this, other)) return true; + + return other is TextRetain && + other.length == length && + mapEquals(_attributes, other._attributes); } @override - int get hashCode { - return _length.hashCode; - } + int get hashCode => length.hashCode ^ _attributes.hashCode; +} + +class TextDelete extends TextOperation { + TextDelete({ + required this.length, + }); + + @override + int length; + + @override + Attributes? get attributes => null; @override Map toJson() { return { - 'delete': _length, + 'delete': length, }; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TextDelete && other.length == length; + } + + @override + int get hashCode => length.hashCode; +} + +/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. +/// The format is JSON based, and is human readable, yet easily parsible by machines. +/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. +/// + +/// Basically borrowed from: https://github.com/quilljs/delta +class Delta extends Iterable { + Delta({ + List? operations, + }) : _operations = operations ?? []; + + factory Delta.fromJson(List list) { + final operations = []; + + for (final value in list) { + if (value is Map) { + final op = _textOperationFromJson(value); + if (op != null) { + operations.add(op); + } + } + } + + return Delta(operations: operations); + } + + final List _operations; + String? _plainText; + List? _runeIndexes; + + void addAll(Iterable textOperations) { + textOperations.forEach(add); + } + + void add(TextOperation textOperation) { + if (textOperation.isEmpty) { + return; + } + _plainText = null; + + if (_operations.isNotEmpty) { + final lastOp = _operations.last; + if (lastOp is TextDelete && textOperation is TextDelete) { + lastOp.length += textOperation.length; + return; + } + if (mapEquals(lastOp.attributes, textOperation.attributes)) { + if (lastOp is TextInsert && textOperation is TextInsert) { + lastOp.text += textOperation.text; + return; + } + // if there is an delete before the insert + // swap the order + if (lastOp is TextDelete && textOperation is TextInsert) { + _operations.removeLast(); + _operations.add(textOperation); + _operations.add(lastOp); + return; + } + if (lastOp is TextRetain && textOperation is TextRetain) { + lastOp.length += textOperation.length; + return; + } + } + } + + _operations.add(textOperation); + } + + /// The slice() method does not change the original string. + /// The start and end parameters specifies the part of the string to extract. + /// The end position is optional. + Delta slice(int start, [int? end]) { + final result = Delta(); + final iterator = _OpIterator(_operations); + int index = 0; + + while ((end == null || index < end) && iterator.hasNext) { + TextOperation? nextOp; + if (index < start) { + nextOp = iterator._next(start - index); + } else { + nextOp = iterator._next(end == null ? null : end - index); + result.add(nextOp); + } + + index += nextOp.length; + } + + return result; + } + + /// Insert operations have an `insert` key defined. + /// A String value represents inserting text. + void insert(String text, {Attributes? attributes}) => + add(TextInsert(text, attributes: attributes)); + + /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). + /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. + /// A value of `null` in the `attributes` Object represents removal of that key. + /// + /// *Note: It is not necessary to retain the last characters of a document as this is implied.* + void retain(int length, {Attributes? attributes}) => + add(TextRetain(length, attributes: attributes)); + + /// Delete operations have a Number `delete` key defined representing the number of characters to delete. + void delete(int length) => add(TextDelete(length: length)); + + /// The length of the string fo the [Delta]. + @override + int get length { + return _operations.fold( + 0, (previousValue, element) => previousValue + element.length); + } + + /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. + Delta compose(Delta other) { + final thisIter = _OpIterator(_operations); + final otherIter = _OpIterator(other._operations); + final operations = []; + + final firstOther = otherIter.peek(); + if (firstOther != null && + firstOther is TextRetain && + firstOther.attributes == null) { + int firstLeft = firstOther.length; + while ( + thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { + firstLeft -= thisIter.peekLength(); + final next = thisIter._next(); + operations.add(next); + } + if (firstOther.length - firstLeft > 0) { + otherIter._next(firstOther.length - firstLeft); + } + } + + final delta = Delta(operations: operations); + while (thisIter.hasNext || otherIter.hasNext) { + if (otherIter.peek() is TextInsert) { + final next = otherIter._next(); + delta.add(next); + } else if (thisIter.peek() is TextDelete) { + final next = thisIter._next(); + delta.add(next); + } else { + // otherIs + final length = min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter._next(length); + final otherOp = otherIter._next(length); + final attributes = composeAttributes( + thisOp.attributes, + otherOp.attributes, + keepNull: thisOp is TextRetain, + ); + + if (otherOp is TextRetain && otherOp.length > 0) { + TextOperation? newOp; + if (thisOp is TextRetain) { + newOp = TextRetain(length, attributes: attributes); + } else if (thisOp is TextInsert) { + newOp = TextInsert(thisOp.text, attributes: attributes); + } + + if (newOp != null) { + delta.add(newOp); + } + + // Optimization if rest of other is just retain + if (!otherIter.hasNext && + delta._operations.isNotEmpty && + delta._operations.last == newOp) { + final rest = Delta(operations: thisIter.rest()); + return (delta + rest)..chop(); + } + } else if (otherOp is TextDelete && (thisOp is TextRetain)) { + delta.add(otherOp); + } + } + } + + return delta..chop(); + } + + /// This method joins two Delta together. + Delta operator +(Delta other) { + var operations = [..._operations]; + if (other._operations.isNotEmpty) { + operations.add(other._operations[0]); + operations.addAll(other._operations.sublist(1)); + } + return Delta(operations: operations); + } + + void chop() { + if (_operations.isEmpty) { + return; + } + _plainText = null; + final lastOp = _operations.last; + if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { + _operations.removeLast(); + } + } + + @override + bool operator ==(Object other) { + if (other is! Delta) { + return false; + } + return listEquals(_operations, other._operations); + } + + @override + int get hashCode { + return Object.hashAll(_operations); + } + + /// Returned an inverted delta that has the opposite effect of against a base document delta. + Delta invert(Delta base) { + final inverted = Delta(); + _operations.fold(0, (int previousValue, op) { + if (op is TextInsert) { + inverted.delete(op.length); + } else if (op is TextRetain && op.attributes == null) { + inverted.retain(op.length); + return previousValue + op.length; + } else if (op is TextDelete || op is TextRetain) { + final length = op.length; + final slice = base.slice(previousValue, previousValue + length); + for (final baseOp in slice._operations) { + if (op is TextDelete) { + inverted.add(baseOp); + } else if (op is TextRetain && op.attributes != null) { + inverted.retain( + baseOp.length, + attributes: invertAttributes(baseOp.attributes, op.attributes), + ); + } + } + return previousValue + length; + } + return previousValue; + }); + return inverted..chop(); + } + + List toJson() { + return _operations.map((e) => e.toJson()).toList(); + } + + /// This method will return the position of the previous rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position - 1` simply. + /// + /// This method can help you to compute the position of the previous character. + int prevRunePosition(int pos) { + if (pos == 0) { + return pos - 1; + } + _plainText ??= + _operations.whereType().map((op) => op.text).join(); + _runeIndexes ??= stringIndexes(_plainText!); + return _runeIndexes![pos - 1]; + } + + /// This method will return the position of the next rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position + 1` simply. + /// + /// This method can help you to compute the position of the next character. + int nextRunePosition(int pos) { + final stringContent = toPlainText(); + if (pos >= stringContent.length - 1) { + return stringContent.length; + } + _runeIndexes ??= stringIndexes(_plainText!); + + for (var i = pos + 1; i < _runeIndexes!.length; i++) { + if (_runeIndexes![i] != pos) { + return _runeIndexes![i]; + } + } + + return stringContent.length; + } + + String toPlainText() { + _plainText ??= + _operations.whereType().map((op) => op.text).join(); + return _plainText!; + } + + @override + Iterator get iterator => _operations.iterator; + + static TextOperation? _textOperationFromJson(Map json) { + TextOperation? operation; + + if (json['insert'] is String) { + final attributes = json['attributes'] as Map?; + operation = TextInsert( + json['insert'] as String, + attributes: attributes != null ? {...attributes} : null, + ); + } else if (json['retain'] is int) { + final attrs = json['attributes'] as Map?; + operation = TextRetain( + json['retain'] as int, + attributes: attrs != null ? {...attrs} : null, + ); + } else if (json['delete'] is int) { + operation = TextDelete(length: json['delete'] as int); + } + + return operation; + } } class _OpIterator { + _OpIterator( + Iterable operations, + ) : _operations = UnmodifiableListView(operations); + final UnmodifiableListView _operations; int _index = 0; int _offset = 0; - _OpIterator(List operations) - : _operations = UnmodifiableListView(operations); - bool get hasNext { return peekLength() < _maxInt; } @@ -204,20 +506,17 @@ class _OpIterator { _offset += length; } if (nextOp is TextDelete) { - return TextDelete(length); + return TextDelete(length: length); } if (nextOp is TextRetain) { - return TextRetain( - length, - nextOp.attributes, - ); + return TextRetain(length, attributes: nextOp.attributes); } if (nextOp is TextInsert) { return TextInsert( - nextOp.content.substring(offset, offset + length), - nextOp.attributes, + nextOp.text.substring(offset, offset + length), + attributes: nextOp.attributes, ); } @@ -240,331 +539,3 @@ class _OpIterator { } } } - -TextOperation? _textOperationFromJson(Map json) { - TextOperation? result; - - if (json['insert'] is String) { - final attrs = json['attributes'] as Map?; - result = - TextInsert(json['insert'] as String, attrs == null ? null : {...attrs}); - } else if (json['retain'] is int) { - final attrs = json['attributes'] as Map?; - result = - TextRetain(json['retain'] as int, attrs == null ? null : {...attrs}); - } else if (json['delete'] is int) { - result = TextDelete(json['delete'] as int); - } - - return result; -} - -/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. -/// The format is JSON based, and is human readable, yet easily parsible by machines. -/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. -/// - -/// Basically borrowed from: https://github.com/quilljs/delta -class Delta extends Iterable { - final List _operations; - String? _rawString; - List? _runeIndexes; - - factory Delta.fromJson(List list) { - final operations = []; - - for (final obj in list) { - final op = _textOperationFromJson(obj as Map); - if (op != null) { - operations.add(op); - } - } - - return Delta(operations); - } - - Delta([List? ops]) : _operations = ops ?? []; - - void addAll(Iterable textOps) { - textOps.forEach(add); - } - - void add(TextOperation textOp) { - if (textOp.isEmpty) { - return; - } - _rawString = null; - - if (_operations.isNotEmpty) { - final lastOp = _operations.last; - if (lastOp is TextDelete && textOp is TextDelete) { - lastOp.length += textOp.length; - return; - } - if (mapEquals(lastOp.attributes, textOp.attributes)) { - if (lastOp is TextInsert && textOp is TextInsert) { - lastOp.content += textOp.content; - return; - } - // if there is an delete before the insert - // swap the order - if (lastOp is TextDelete && textOp is TextInsert) { - _operations.removeLast(); - _operations.add(textOp); - _operations.add(lastOp); - return; - } - if (lastOp is TextRetain && textOp is TextRetain) { - lastOp.length += textOp.length; - return; - } - } - } - - _operations.add(textOp); - } - - /// The slice() method does not change the original string. - /// The start and end parameters specifies the part of the string to extract. - /// The end position is optional. - Delta slice(int start, [int? end]) { - final result = Delta(); - final iterator = _OpIterator(_operations); - int index = 0; - - while ((end == null || index < end) && iterator.hasNext) { - TextOperation? nextOp; - if (index < start) { - nextOp = iterator._next(start - index); - } else { - nextOp = iterator._next(end == null ? null : end - index); - result.add(nextOp); - } - - index += nextOp.length; - } - - return result; - } - - /// Insert operations have an `insert` key defined. - /// A String value represents inserting text. - void insert(String content, [Attributes? attributes]) => - add(TextInsert(content, attributes)); - - /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). - /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. - /// A value of `null` in the `attributes` Object represents removal of that key. - /// - /// *Note: It is not necessary to retain the last characters of a document as this is implied.* - void retain(int length, [Attributes? attributes]) => - add(TextRetain(length, attributes)); - - /// Delete operations have a Number `delete` key defined representing the number of characters to delete. - void delete(int length) => add(TextDelete(length)); - - /// The length of the string fo the [Delta]. - @override - int get length { - return _operations.fold( - 0, (previousValue, element) => previousValue + element.length); - } - - /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. - Delta compose(Delta other) { - final thisIter = _OpIterator(_operations); - final otherIter = _OpIterator(other._operations); - final ops = []; - - final firstOther = otherIter.peek(); - if (firstOther != null && - firstOther is TextRetain && - firstOther.attributes == null) { - int firstLeft = firstOther.length; - while ( - thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { - firstLeft -= thisIter.peekLength(); - final next = thisIter._next(); - ops.add(next); - } - if (firstOther.length - firstLeft > 0) { - otherIter._next(firstOther.length - firstLeft); - } - } - - final delta = Delta(ops); - while (thisIter.hasNext || otherIter.hasNext) { - if (otherIter.peek() is TextInsert) { - final next = otherIter._next(); - delta.add(next); - } else if (thisIter.peek() is TextDelete) { - final next = thisIter._next(); - delta.add(next); - } else { - // otherIs - final length = min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter._next(length); - final otherOp = otherIter._next(length); - final attributes = composeAttributes( - thisOp.attributes, - otherOp.attributes, - keepNull: thisOp is TextRetain, - ); - - if (otherOp is TextRetain && otherOp.length > 0) { - TextOperation? newOp; - if (thisOp is TextRetain) { - newOp = TextRetain(length, attributes); - } else if (thisOp is TextInsert) { - newOp = TextInsert(thisOp.content, attributes); - } - - if (newOp != null) { - delta.add(newOp); - } - - // Optimization if rest of other is just retain - if (!otherIter.hasNext && - delta._operations.isNotEmpty && - delta._operations.last == newOp) { - final rest = Delta(thisIter.rest()); - return (delta + rest)..chop(); - } - } else if (otherOp is TextDelete && (thisOp is TextRetain)) { - delta.add(otherOp); - } - } - } - - return delta..chop(); - } - - /// This method joins two Delta together. - Delta operator +(Delta other) { - var ops = [..._operations]; - if (other._operations.isNotEmpty) { - ops.add(other._operations[0]); - ops.addAll(other._operations.sublist(1)); - } - return Delta(ops); - } - - void chop() { - if (_operations.isEmpty) { - return; - } - _rawString = null; - final lastOp = _operations.last; - if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { - _operations.removeLast(); - } - } - - @override - bool operator ==(Object other) { - if (other is! Delta) { - return false; - } - return listEquals(_operations, other._operations); - } - - @override - int get hashCode { - return Object.hashAll(_operations); - } - - /// Returned an inverted delta that has the opposite effect of against a base document delta. - Delta invert(Delta base) { - final inverted = Delta(); - _operations.fold(0, (int previousValue, op) { - if (op is TextInsert) { - inverted.delete(op.length); - } else if (op is TextRetain && op.attributes == null) { - inverted.retain(op.length); - return previousValue + op.length; - } else if (op is TextDelete || op is TextRetain) { - final length = op.length; - final slice = base.slice(previousValue, previousValue + length); - for (final baseOp in slice._operations) { - if (op is TextDelete) { - inverted.add(baseOp); - } else if (op is TextRetain && op.attributes != null) { - inverted.retain( - baseOp.length, - invertAttributes(baseOp.attributes, op.attributes), - ); - } - } - return previousValue + length; - } - return previousValue; - }); - return inverted..chop(); - } - - List toJson() { - return _operations.map((e) => e.toJson()).toList(); - } - - /// This method will return the position of the previous rune. - /// - /// Since the encoding of the [String] in Dart is UTF-16. - /// If you want to find the previous character of a position, - /// you can' just use the `position - 1` simply. - /// - /// This method can help you to compute the position of the previous character. - int prevRunePosition(int pos) { - if (pos == 0) { - return pos - 1; - } - _rawString ??= - _operations.whereType().map((op) => op.content).join(); - _runeIndexes ??= stringIndexes(_rawString!); - return _runeIndexes![pos - 1]; - } - - /// This method will return the position of the next rune. - /// - /// Since the encoding of the [String] in Dart is UTF-16. - /// If you want to find the previous character of a position, - /// you can' just use the `position + 1` simply. - /// - /// This method can help you to compute the position of the next character. - int nextRunePosition(int pos) { - final stringContent = toPlainText(); - if (pos >= stringContent.length - 1) { - return stringContent.length; - } - _runeIndexes ??= stringIndexes(_rawString!); - - for (var i = pos + 1; i < _runeIndexes!.length; i++) { - if (_runeIndexes![i] != pos) { - return _runeIndexes![i]; - } - } - - return stringContent.length; - } - - String toPlainText() { - _rawString ??= - _operations.whereType().map((op) => op.content).join(); - return _rawString!; - } - - @override - Iterator get iterator => _operations.iterator; -} - -List stringIndexes(String content) { - final indexes = List.filled(content.length, 0); - final iterator = content.runes.iterator; - - while (iterator.moveNext()) { - for (var i = 0; i < iterator.currentSize; i++) { - indexes[iterator.rawIndex + i] = iterator.rawIndex; - } - } - - return indexes; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart index bdc4333c57..77996d2024 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/path.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import '../core/document/attributes.dart'; class StateTree { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index d5604aa938..e671b47645 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -2,7 +2,7 @@ import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/path.dart'; import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; extension TextNodeExtension on TextNode { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart index 70f3884e4e..2d00d369ce 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/src/core/document/node.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; @@ -218,24 +218,29 @@ class HTMLToNodesConverter { _handleRichTextElement(Delta delta, html.Element element) { if (element.localName == HTMLTag.span) { - delta.insert(element.text, - _getDeltaAttributesFromHtmlAttributes(element.attributes)); + delta.insert( + element.text, + attributes: _getDeltaAttributesFromHtmlAttributes(element.attributes), + ); } else if (element.localName == HTMLTag.anchor) { final hyperLink = element.attributes["href"]; Map? attributes; if (hyperLink != null) { attributes = {"href": hyperLink}; } - delta.insert(element.text, attributes); + delta.insert(element.text, attributes: attributes); } else if (element.localName == HTMLTag.strong || element.localName == HTMLTag.bold) { - delta.insert(element.text, {BuiltInAttributeKey.bold: true}); + delta.insert(element.text, attributes: {BuiltInAttributeKey.bold: true}); } else if (element.localName == HTMLTag.underline) { - delta.insert(element.text, {BuiltInAttributeKey.underline: true}); + delta.insert(element.text, + attributes: {BuiltInAttributeKey.underline: true}); } else if (element.localName == HTMLTag.italic) { - delta.insert(element.text, {BuiltInAttributeKey.italic: true}); + delta + .insert(element.text, attributes: {BuiltInAttributeKey.italic: true}); } else if (element.localName == HTMLTag.del) { - delta.insert(element.text, {BuiltInAttributeKey.strikethrough: true}); + delta.insert(element.text, + attributes: {BuiltInAttributeKey.strikethrough: true}); } else { delta.insert(element.text); } @@ -535,22 +540,22 @@ class NodesToHTMLConverter { if (attributes.length == 1 && attributes[BuiltInAttributeKey.bold] == true) { final strong = html.Element.tag(HTMLTag.strong); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.underline] == true) { final strong = html.Element.tag(HTMLTag.underline); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.italic] == true) { final strong = html.Element.tag(HTMLTag.italic); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[BuiltInAttributeKey.strikethrough] == true) { final strong = html.Element.tag(HTMLTag.del); - strong.append(html.Text(op.content)); + strong.append(html.Text(op.text)); childNodes.add(strong); } else { final span = html.Element.tag(HTMLTag.span); @@ -558,11 +563,11 @@ class NodesToHTMLConverter { if (cssString.isNotEmpty) { span.attributes["style"] = cssString; } - span.append(html.Text(op.content)); + span.append(html.Text(op.text)); childNodes.add(span); } } else { - childNodes.add(html.Text(op.content)); + childNodes.add(html.Text(op.text)); } } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 1c3ded6dcf..c1dce826dd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/path.dart'; import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/transaction.dart'; @@ -134,7 +134,7 @@ class TransactionBuilder { ..retain(index) ..insert( content, - newAttributes, + attributes: newAttributes, ), ); afterSelection = Selection.collapsed( @@ -148,7 +148,7 @@ class TransactionBuilder { node, () => Delta() ..retain(index) - ..retain(length, attributes)); + ..retain(length, attributes: attributes)); afterSelection = beforeSelection; } @@ -177,7 +177,7 @@ class TransactionBuilder { () => Delta() ..retain(index) ..delete(length) - ..insert(content, newAttributes), + ..insert(content, attributes: newAttributes), ); afterSelection = Selection.collapsed( Position( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 600047074f..c6bf271e5d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -9,7 +9,7 @@ import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/path.dart'; import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; @@ -257,7 +257,7 @@ class _FlowyRichTextState extends State with SelectableMixin { offset += textInsert.length; textSpans.add( TextSpan( - text: textInsert.content, + text: textInsert.text, style: textStyle, recognizer: recognizer, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index b901ad626d..6d35dddb8c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -222,7 +222,7 @@ Delta _lineContentToDelta(String lineContent) { delta.insert(lineContent.substring(lastUrlEndOffset, match.start)); } final linkContent = lineContent.substring(match.start, match.end); - delta.insert(linkContent, {"href": linkContent}); + delta.insert(linkContent, attributes: {"href": linkContent}); lastUrlEndOffset = match.end; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart new file mode 100644 index 0000000000..c0c3946636 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/core/document/text_delta_test.dart @@ -0,0 +1,332 @@ +import 'package:appflowy_editor/src/core/document/attributes.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/core/document/text_delta.dart'; + +void main() { + group('text_delta.dart', () { + group('compose', () { + test('test delta', () { + final delta = Delta(operations: [ + TextInsert('Gandalf', attributes: { + 'bold': true, + }), + TextInsert(' the '), + TextInsert('Grey', attributes: { + 'color': '#ccc', + }) + ]); + + final death = Delta() + ..retain(12) + ..insert("White", attributes: { + 'color': '#fff', + }) + ..delete(4); + + final restores = delta.compose(death); + expect(restores.toList(), [ + TextInsert('Gandalf', attributes: {'bold': true}), + TextInsert(' the '), + TextInsert('White', attributes: {'color': '#fff'}), + ]); + }); + test('compose()', () { + final a = Delta()..insert('A'); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..insert('A'); + expect(a.compose(b), expected); + }); + test('insert + retain', () { + final a = Delta()..insert('A'); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..insert('A', attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('insert + delete', () { + final a = Delta()..insert('A'); + final b = Delta()..delete(1); + final expected = Delta(); + expect(a.compose(b), expected); + }); + test('delete + insert', () { + final a = Delta()..delete(1); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..delete(1); + expect(a.compose(b), expected); + }); + test('delete + retain', () { + final a = Delta()..delete(1); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..delete(1) + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('delete + delete', () { + final a = Delta()..delete(1); + final b = Delta()..delete(1); + final expected = Delta()..delete(2); + expect(a.compose(b), expected); + }); + test('retain + insert', () { + final a = Delta()..retain(1, attributes: {'color': 'blue'}); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..retain(1, attributes: { + 'color': 'blue', + }); + expect(a.compose(b), expected); + }); + test('retain + retain', () { + final a = Delta() + ..retain(1, attributes: { + 'color': 'blue', + }); + final b = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..retain(1, attributes: { + 'bold': true, + 'color': 'red', + }); + expect(a.compose(b), expected); + }); + test('retain + delete', () { + final a = Delta() + ..retain(1, attributes: { + 'color': 'blue', + }); + final b = Delta()..delete(1); + final expected = Delta()..delete(1); + expect(a.compose(b), expected); + }); + test('insert in middle of text', () { + final a = Delta()..insert('Hello'); + final b = Delta() + ..retain(3) + ..insert('X'); + final expected = Delta()..insert('HelXlo'); + expect(a.compose(b), expected); + }); + test('insert and delete ordering', () { + final a = Delta()..insert('Hello'); + final b = Delta()..insert('Hello'); + final insertFirst = Delta() + ..retain(3) + ..insert('X') + ..delete(1); + final deleteFirst = Delta() + ..retain(3) + ..delete(1) + ..insert('X'); + final expected = Delta()..insert('HelXo'); + expect(a.compose(insertFirst), expected); + expect(b.compose(deleteFirst), expected); + }); + test('delete entire text', () { + final a = Delta() + ..retain(4) + ..insert('Hello'); + final b = Delta()..delete(9); + final expected = Delta()..delete(4); + expect(a.compose(b), expected); + }); + test('retain more than length of text', () { + final a = Delta()..insert('Hello'); + final b = Delta()..retain(10); + final expected = Delta()..insert('Hello'); + expect(a.compose(b), expected); + }); + test('retain start optimization', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..delete(1); + final b = Delta() + ..retain(3) + ..insert('D'); + final expected = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..insert('D') + ..delete(1); + expect(a.compose(b), expected); + }); + test('retain end optimization', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}); + final b = Delta()..delete(1); + final expected = Delta() + ..insert('B') + ..insert('C', attributes: {'bold': true}); + expect(a.compose(b), expected); + }); + test('retain end optimization join', () { + final a = Delta() + ..insert('A', attributes: {'bold': true}) + ..insert('B') + ..insert('C', attributes: {'bold': true}) + ..insert('D') + ..insert('E', attributes: {'bold': true}) + ..insert('F'); + final b = Delta() + ..retain(1) + ..delete(1); + final expected = Delta() + ..insert('AC', attributes: {'bold': true}) + ..insert('D') + ..insert('E', attributes: {'bold': true}) + ..insert('F'); + expect(a.compose(b), expected); + }); + }); + group('invert', () { + test('insert', () { + final delta = Delta() + ..retain(2) + ..insert('A'); + final base = Delta()..insert('12346'); + final expected = Delta() + ..retain(2) + ..delete(1); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('delete', () { + final delta = Delta() + ..retain(2) + ..delete(3); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..insert('345'); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); + test('retain', () { + final delta = Delta() + ..retain(2) + ..retain(3, attributes: {'bold': true}); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..retain(3, attributes: {'bold': null}); + final inverted = delta.invert(base); + expect(expected, inverted); + final t = base.compose(delta).compose(inverted); + expect(t, base); + }); + }); + group('json', () { + test('toJson()', () { + final delta = Delta() + ..retain(2) + ..insert('A') + ..delete(3); + expect(delta.toJson(), [ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3} + ]); + }); + test('attributes', () { + final delta = Delta() + ..retain(2, attributes: {'bold': true}) + ..insert('A', attributes: {'italic': true}); + expect(delta.toJson(), [ + { + 'retain': 2, + 'attributes': {'bold': true}, + }, + { + 'insert': 'A', + 'attributes': {'italic': true}, + }, + ]); + }); + test('fromJson()', () { + final delta = Delta.fromJson([ + {'retain': 2}, + {'insert': 'A'}, + {'delete': 3}, + ]); + final expected = Delta() + ..retain(2) + ..insert('A') + ..delete(3); + expect(delta, expected); + }); + }); + group('runes', () { + test("stringIndexes", () { + final indexes = stringIndexes('😊'); + expect(indexes[0], 0); + expect(indexes[1], 0); + }); + test("next rune 1", () { + final delta = Delta()..insert('😊'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 2", () { + final delta = Delta()..insert('😊a'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 3", () { + final delta = Delta()..insert('😊陈'); + expect(delta.nextRunePosition(2), 3); + }); + test("prev rune 1", () { + final delta = Delta()..insert('😊陈'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 2", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 3", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(0), -1); + }); + }); + group("attributes", () { + test("compose", () { + final attrs = + composeAttributes({'a': null}, {'b': null}, keepNull: true); + expect(attrs != null, true); + expect(attrs?.containsKey("a"), true); + expect(attrs?.containsKey("b"), true); + expect(attrs?["a"], null); + expect(attrs?["b"], null); + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index e0395fe420..09860c9346 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -63,7 +63,7 @@ class EditorWidgetTester { void insertTextNode(String? text, {Attributes? attributes, Delta? delta}) { insert( TextNode( - delta: delta ?? Delta([TextInsert(text ?? 'Test')]), + delta: delta ?? Delta(operations: [TextInsert(text ?? 'Test')]), attributes: attributes, ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart deleted file mode 100644 index 3c8d4a3dfe..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/delta_test.dart +++ /dev/null @@ -1,329 +0,0 @@ -import 'package:appflowy_editor/src/core/document/attributes.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/document/text_delta.dart'; - -void main() { - group('compose', () { - test('test delta', () { - final delta = Delta([ - TextInsert('Gandalf', { - 'bold': true, - }), - TextInsert(' the '), - TextInsert('Grey', { - 'color': '#ccc', - }) - ]); - - final death = Delta() - ..retain(12) - ..insert("White", { - 'color': '#fff', - }) - ..delete(4); - - final restores = delta.compose(death); - expect(restores.toList(), [ - TextInsert('Gandalf', {'bold': true}), - TextInsert(' the '), - TextInsert('White', {'color': '#fff'}), - ]); - }); - test('compose()', () { - final a = Delta()..insert('A'); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..insert('A'); - expect(a.compose(b), expected); - }); - test('insert + retain', () { - final a = Delta()..insert('A'); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..insert('A', { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('insert + delete', () { - final a = Delta()..insert('A'); - final b = Delta()..delete(1); - final expected = Delta(); - expect(a.compose(b), expected); - }); - test('delete + insert', () { - final a = Delta()..delete(1); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..delete(1); - expect(a.compose(b), expected); - }); - test('delete + retain', () { - final a = Delta()..delete(1); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..delete(1) - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('delete + delete', () { - final a = Delta()..delete(1); - final b = Delta()..delete(1); - final expected = Delta()..delete(2); - expect(a.compose(b), expected); - }); - test('retain + insert', () { - final a = Delta()..retain(1, {'color': 'blue'}); - final b = Delta()..insert('B'); - final expected = Delta() - ..insert('B') - ..retain(1, { - 'color': 'blue', - }); - expect(a.compose(b), expected); - }); - test('retain + retain', () { - final a = Delta() - ..retain(1, { - 'color': 'blue', - }); - final b = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta() - ..retain(1, { - 'bold': true, - 'color': 'red', - }); - expect(a.compose(b), expected); - }); - test('retain + delete', () { - final a = Delta() - ..retain(1, { - 'color': 'blue', - }); - final b = Delta()..delete(1); - final expected = Delta()..delete(1); - expect(a.compose(b), expected); - }); - test('insert in middle of text', () { - final a = Delta()..insert('Hello'); - final b = Delta() - ..retain(3) - ..insert('X'); - final expected = Delta()..insert('HelXlo'); - expect(a.compose(b), expected); - }); - test('insert and delete ordering', () { - final a = Delta()..insert('Hello'); - final b = Delta()..insert('Hello'); - final insertFirst = Delta() - ..retain(3) - ..insert('X') - ..delete(1); - final deleteFirst = Delta() - ..retain(3) - ..delete(1) - ..insert('X'); - final expected = Delta()..insert('HelXo'); - expect(a.compose(insertFirst), expected); - expect(b.compose(deleteFirst), expected); - }); - test('delete entire text', () { - final a = Delta() - ..retain(4) - ..insert('Hello'); - final b = Delta()..delete(9); - final expected = Delta()..delete(4); - expect(a.compose(b), expected); - }); - test('retain more than length of text', () { - final a = Delta()..insert('Hello'); - final b = Delta()..retain(10); - final expected = Delta()..insert('Hello'); - expect(a.compose(b), expected); - }); - test('retain start optimization', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..delete(1); - final b = Delta() - ..retain(3) - ..insert('D'); - final expected = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..insert('D') - ..delete(1); - expect(a.compose(b), expected); - }); - test('retain end optimization', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}); - final b = Delta()..delete(1); - final expected = Delta() - ..insert('B') - ..insert('C', {'bold': true}); - expect(a.compose(b), expected); - }); - test('retain end optimization join', () { - final a = Delta() - ..insert('A', {'bold': true}) - ..insert('B') - ..insert('C', {'bold': true}) - ..insert('D') - ..insert('E', {'bold': true}) - ..insert('F'); - final b = Delta() - ..retain(1) - ..delete(1); - final expected = Delta() - ..insert('AC', {'bold': true}) - ..insert('D') - ..insert('E', {'bold': true}) - ..insert('F'); - expect(a.compose(b), expected); - }); - }); - group('invert', () { - test('insert', () { - final delta = Delta() - ..retain(2) - ..insert('A'); - final base = Delta()..insert('12346'); - final expected = Delta() - ..retain(2) - ..delete(1); - final inverted = delta.invert(base); - expect(expected, inverted); - expect(base.compose(delta).compose(inverted), base); - }); - test('delete', () { - final delta = Delta() - ..retain(2) - ..delete(3); - final base = Delta()..insert('123456'); - final expected = Delta() - ..retain(2) - ..insert('345'); - final inverted = delta.invert(base); - expect(expected, inverted); - expect(base.compose(delta).compose(inverted), base); - }); - test('retain', () { - final delta = Delta() - ..retain(2) - ..retain(3, {'bold': true}); - final base = Delta()..insert('123456'); - final expected = Delta() - ..retain(2) - ..retain(3, {'bold': null}); - final inverted = delta.invert(base); - expect(expected, inverted); - final t = base.compose(delta).compose(inverted); - expect(t, base); - }); - }); - group('json', () { - test('toJson()', () { - final delta = Delta() - ..retain(2) - ..insert('A') - ..delete(3); - expect(delta.toJson(), [ - {'retain': 2}, - {'insert': 'A'}, - {'delete': 3} - ]); - }); - test('attributes', () { - final delta = Delta() - ..retain(2, {'bold': true}) - ..insert('A', {'italic': true}); - expect(delta.toJson(), [ - { - 'retain': 2, - 'attributes': {'bold': true}, - }, - { - 'insert': 'A', - 'attributes': {'italic': true}, - }, - ]); - }); - test('fromJson()', () { - final delta = Delta.fromJson([ - {'retain': 2}, - {'insert': 'A'}, - {'delete': 3}, - ]); - final expected = Delta() - ..retain(2) - ..insert('A') - ..delete(3); - expect(delta, expected); - }); - }); - group('runes', () { - test("stringIndexes", () { - final indexes = stringIndexes('😊'); - expect(indexes[0], 0); - expect(indexes[1], 0); - }); - test("next rune 1", () { - final delta = Delta()..insert('😊'); - expect(delta.nextRunePosition(0), 2); - }); - test("next rune 2", () { - final delta = Delta()..insert('😊a'); - expect(delta.nextRunePosition(0), 2); - }); - test("next rune 3", () { - final delta = Delta()..insert('😊陈'); - expect(delta.nextRunePosition(2), 3); - }); - test("prev rune 1", () { - final delta = Delta()..insert('😊陈'); - expect(delta.prevRunePosition(2), 0); - }); - test("prev rune 2", () { - final delta = Delta()..insert('😊'); - expect(delta.prevRunePosition(2), 0); - }); - test("prev rune 3", () { - final delta = Delta()..insert('😊'); - expect(delta.prevRunePosition(0), -1); - }); - }); - group("attributes", () { - test("compose", () { - final attrs = composeAttributes({'a': null}, {'b': null}, keepNull: true); - expect(attrs != null, true); - expect(attrs?.containsKey("a"), true); - expect(attrs?.containsKey("b"), true); - expect(attrs?["a"], null); - expect(attrs?["b"], null); - }); - }); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index 29dfbba136..b13fa456c9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -26,8 +26,8 @@ void main() async { BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, BuiltInAttributeKey.checkbox: false, }, - delta: Delta([ - TextInsert(text, { + delta: Delta(operations: [ + TextInsert(text, attributes: { BuiltInAttributeKey.bold: true, BuiltInAttributeKey.italic: true, BuiltInAttributeKey.underline: true, diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index 1e8f2f1f07..2388507003 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -63,9 +63,9 @@ void main() async { ..insertTextNode(text) ..insertTextNode( null, - delta: Delta([ + delta: Delta(operations: [ TextInsert(text), - TextInsert(text, attributes), + TextInsert(text, attributes: attributes), TextInsert(text), ]), ); @@ -171,8 +171,8 @@ void main() async { BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, BuiltInAttributeKey.heading: BuiltInAttributeKey.h1, }, - delta: Delta([ - TextInsert(text, { + delta: Delta(operations: [ + TextInsert(text, attributes: { BuiltInAttributeKey.bold: true, }) ]),