|
@@ -1,262 +1,138 @@
|
|
|
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<int> stringIndexes(String text) {
|
|
|
+ final indexes = List<int>.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<String, dynamic> toJson();
|
|
|
}
|
|
|
|
|
|
class TextInsert extends TextOperation {
|
|
|
- String content;
|
|
|
- final Attributes? _attributes;
|
|
|
-
|
|
|
- TextInsert(this.content, [Attributes? attrs]) : _attributes = attrs;
|
|
|
-
|
|
|
- @override
|
|
|
- int get length {
|
|
|
- return content.length;
|
|
|
- }
|
|
|
+ TextInsert(
|
|
|
+ this.text, {
|
|
|
+ Attributes? attributes,
|
|
|
+ }) : _attributes = attributes;
|
|
|
|
|
|
- @override
|
|
|
- Attributes? get attributes {
|
|
|
- return _attributes;
|
|
|
- }
|
|
|
+ String text;
|
|
|
+ final Attributes? _attributes;
|
|
|
|
|
|
@override
|
|
|
- bool operator ==(Object other) {
|
|
|
- if (other is! TextInsert) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return content == other.content &&
|
|
|
- mapEquals(_attributes, other._attributes);
|
|
|
- }
|
|
|
+ int get length => text.length;
|
|
|
|
|
|
@override
|
|
|
- int get hashCode {
|
|
|
- final contentHash = content.hashCode;
|
|
|
- final attrs = _attributes;
|
|
|
- return Object.hash(
|
|
|
- contentHash,
|
|
|
- attrs != null ? hashAttributes(attrs) : null,
|
|
|
- );
|
|
|
- }
|
|
|
+ Attributes? get attributes => _attributes != null ? {..._attributes!} : null;
|
|
|
|
|
|
@override
|
|
|
Map<String, dynamic> toJson() {
|
|
|
final result = <String, dynamic>{
|
|
|
- 'insert': content,
|
|
|
+ 'insert': text,
|
|
|
};
|
|
|
- final attrs = _attributes;
|
|
|
- if (attrs != null) {
|
|
|
- result['attributes'] = {...attrs};
|
|
|
+ if (_attributes != null) {
|
|
|
+ result['attributes'] = attributes;
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-class TextRetain extends TextOperation {
|
|
|
- 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;
|
|
|
- }
|
|
|
+ bool operator ==(Object other) {
|
|
|
+ if (identical(this, other)) return true;
|
|
|
|
|
|
- set length(int v) {
|
|
|
- _length = v;
|
|
|
+ return other is TextInsert &&
|
|
|
+ other.text == text &&
|
|
|
+ mapEquals(_attributes, other._attributes);
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
- Attributes? get attributes {
|
|
|
- return _attributes;
|
|
|
- }
|
|
|
+ int get hashCode => text.hashCode ^ _attributes.hashCode;
|
|
|
+}
|
|
|
+
|
|
|
+class TextRetain extends TextOperation {
|
|
|
+ TextRetain(
|
|
|
+ this.length, {
|
|
|
+ Attributes? attributes,
|
|
|
+ }) : _attributes = attributes;
|
|
|
|
|
|
@override
|
|
|
- bool operator ==(Object other) {
|
|
|
- if (other is! TextRetain) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return _length == other.length && mapEquals(_attributes, other._attributes);
|
|
|
- }
|
|
|
+ int length;
|
|
|
+ final Attributes? _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<String, dynamic> toJson() {
|
|
|
final result = <String, dynamic>{
|
|
|
- '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;
|
|
|
+ @override
|
|
|
+ bool operator ==(Object other) {
|
|
|
+ if (identical(this, other)) return true;
|
|
|
|
|
|
- TextDelete(int length) : _length = length;
|
|
|
+ return other is TextRetain &&
|
|
|
+ other.length == length &&
|
|
|
+ mapEquals(_attributes, other._attributes);
|
|
|
+ }
|
|
|
|
|
|
@override
|
|
|
- int get length {
|
|
|
- return _length;
|
|
|
- }
|
|
|
+ int get hashCode => length.hashCode ^ _attributes.hashCode;
|
|
|
+}
|
|
|
|
|
|
- set length(int v) {
|
|
|
- _length = v;
|
|
|
- }
|
|
|
+class TextDelete extends TextOperation {
|
|
|
+ TextDelete({
|
|
|
+ required this.length,
|
|
|
+ });
|
|
|
|
|
|
@override
|
|
|
- bool operator ==(Object other) {
|
|
|
- if (other is! TextDelete) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return _length == other.length;
|
|
|
- }
|
|
|
+ int length;
|
|
|
|
|
|
@override
|
|
|
- int get hashCode {
|
|
|
- return _length.hashCode;
|
|
|
- }
|
|
|
+ Attributes? get attributes => null;
|
|
|
|
|
|
@override
|
|
|
Map<String, dynamic> toJson() {
|
|
|
return {
|
|
|
- 'delete': _length,
|
|
|
+ 'delete': length,
|
|
|
};
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-class _OpIterator {
|
|
|
- final UnmodifiableListView<TextOperation> _operations;
|
|
|
- int _index = 0;
|
|
|
- int _offset = 0;
|
|
|
-
|
|
|
- _OpIterator(List<TextOperation> operations)
|
|
|
- : _operations = UnmodifiableListView(operations);
|
|
|
-
|
|
|
- bool get hasNext {
|
|
|
- return peekLength() < _maxInt;
|
|
|
- }
|
|
|
-
|
|
|
- TextOperation? peek() {
|
|
|
- if (_index >= _operations.length) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return _operations[_index];
|
|
|
- }
|
|
|
-
|
|
|
- int peekLength() {
|
|
|
- if (_index < _operations.length) {
|
|
|
- final op = _operations[_index];
|
|
|
- return op.length - _offset;
|
|
|
- }
|
|
|
- return _maxInt;
|
|
|
- }
|
|
|
-
|
|
|
- TextOperation _next([int? length]) {
|
|
|
- length ??= _maxInt;
|
|
|
-
|
|
|
- if (_index >= _operations.length) {
|
|
|
- return TextRetain(_maxInt);
|
|
|
- }
|
|
|
-
|
|
|
- final nextOp = _operations[_index];
|
|
|
|
|
|
- final offset = _offset;
|
|
|
- final opLength = nextOp.length;
|
|
|
- if (length >= opLength - offset) {
|
|
|
- length = opLength - offset;
|
|
|
- _index += 1;
|
|
|
- _offset = 0;
|
|
|
- } else {
|
|
|
- _offset += length;
|
|
|
- }
|
|
|
- if (nextOp is TextDelete) {
|
|
|
- return TextDelete(length);
|
|
|
- }
|
|
|
-
|
|
|
- if (nextOp is TextRetain) {
|
|
|
- return TextRetain(
|
|
|
- length,
|
|
|
- nextOp.attributes,
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- if (nextOp is TextInsert) {
|
|
|
- return TextInsert(
|
|
|
- nextOp.content.substring(offset, offset + length),
|
|
|
- nextOp.attributes,
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- return TextRetain(_maxInt);
|
|
|
- }
|
|
|
-
|
|
|
- List<TextOperation> rest() {
|
|
|
- if (!hasNext) {
|
|
|
- return [];
|
|
|
- } else if (_offset == 0) {
|
|
|
- return _operations.sublist(_index);
|
|
|
- } else {
|
|
|
- final offset = _offset;
|
|
|
- final index = _index;
|
|
|
- final next = _next();
|
|
|
- final rest = _operations.sublist(_index);
|
|
|
- _offset = offset;
|
|
|
- _index = index;
|
|
|
- return [next] + rest;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+ @override
|
|
|
+ bool operator ==(Object other) {
|
|
|
+ if (identical(this, other)) return true;
|
|
|
|
|
|
-TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
|
|
|
- TextOperation? result;
|
|
|
-
|
|
|
- if (json['insert'] is String) {
|
|
|
- final attrs = json['attributes'] as Map<String, dynamic>?;
|
|
|
- result =
|
|
|
- TextInsert(json['insert'] as String, attrs == null ? null : {...attrs});
|
|
|
- } else if (json['retain'] is int) {
|
|
|
- final attrs = json['attributes'] as Map<String, dynamic>?;
|
|
|
- result =
|
|
|
- TextRetain(json['retain'] as int, attrs == null ? null : {...attrs});
|
|
|
- } else if (json['delete'] is int) {
|
|
|
- result = TextDelete(json['delete'] as int);
|
|
|
+ return other is TextDelete && other.length == length;
|
|
|
}
|
|
|
|
|
|
- return result;
|
|
|
+ @override
|
|
|
+ int get hashCode => length.hashCode;
|
|
|
}
|
|
|
|
|
|
/// Deltas are a simple, yet expressive format that can be used to describe contents and changes.
|
|
@@ -266,62 +142,66 @@ TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
/// Basically borrowed from: https://github.com/quilljs/delta
|
|
|
class Delta extends Iterable<TextOperation> {
|
|
|
- final List<TextOperation> _operations;
|
|
|
- String? _rawString;
|
|
|
- List<int>? _runeIndexes;
|
|
|
+ Delta({
|
|
|
+ List<TextOperation>? operations,
|
|
|
+ }) : _operations = operations ?? <TextOperation>[];
|
|
|
|
|
|
factory Delta.fromJson(List<dynamic> list) {
|
|
|
final operations = <TextOperation>[];
|
|
|
|
|
|
- for (final obj in list) {
|
|
|
- final op = _textOperationFromJson(obj as Map<String, dynamic>);
|
|
|
- if (op != null) {
|
|
|
- operations.add(op);
|
|
|
+ for (final value in list) {
|
|
|
+ if (value is Map<String, dynamic>) {
|
|
|
+ final op = _textOperationFromJson(value);
|
|
|
+ if (op != null) {
|
|
|
+ operations.add(op);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return Delta(operations);
|
|
|
+ return Delta(operations: operations);
|
|
|
}
|
|
|
|
|
|
- Delta([List<TextOperation>? ops]) : _operations = ops ?? <TextOperation>[];
|
|
|
+ final List<TextOperation> _operations;
|
|
|
+ String? _plainText;
|
|
|
+ List<int>? _runeIndexes;
|
|
|
|
|
|
- void addAll(Iterable<TextOperation> textOps) {
|
|
|
- textOps.forEach(add);
|
|
|
+ void addAll(Iterable<TextOperation> textOperations) {
|
|
|
+ textOperations.forEach(add);
|
|
|
}
|
|
|
|
|
|
- void add(TextOperation textOp) {
|
|
|
- if (textOp.isEmpty) {
|
|
|
+ void add(TextOperation textOperation) {
|
|
|
+ if (textOperation.isEmpty) {
|
|
|
return;
|
|
|
}
|
|
|
- _rawString = null;
|
|
|
+ _plainText = null;
|
|
|
|
|
|
if (_operations.isNotEmpty) {
|
|
|
final lastOp = _operations.last;
|
|
|
- if (lastOp is TextDelete && textOp is TextDelete) {
|
|
|
- lastOp.length += textOp.length;
|
|
|
+ if (lastOp is TextDelete && textOperation is TextDelete) {
|
|
|
+ lastOp.length += textOperation.length;
|
|
|
return;
|
|
|
}
|
|
|
- if (mapEquals(lastOp.attributes, textOp.attributes)) {
|
|
|
- if (lastOp is TextInsert && textOp is TextInsert) {
|
|
|
- lastOp.content += textOp.content;
|
|
|
+ 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 && textOp is TextInsert) {
|
|
|
+ if (lastOp is TextDelete && textOperation is TextInsert) {
|
|
|
_operations.removeLast();
|
|
|
- _operations.add(textOp);
|
|
|
+ _operations.add(textOperation);
|
|
|
_operations.add(lastOp);
|
|
|
return;
|
|
|
}
|
|
|
- if (lastOp is TextRetain && textOp is TextRetain) {
|
|
|
- lastOp.length += textOp.length;
|
|
|
+ if (lastOp is TextRetain && textOperation is TextRetain) {
|
|
|
+ lastOp.length += textOperation.length;
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- _operations.add(textOp);
|
|
|
+ _operations.add(textOperation);
|
|
|
}
|
|
|
|
|
|
/// The slice() method does not change the original string.
|
|
@@ -349,19 +229,19 @@ class Delta extends Iterable<TextOperation> {
|
|
|
|
|
|
/// Insert operations have an `insert` key defined.
|
|
|
/// A String value represents inserting text.
|
|
|
- void insert(String content, [Attributes? attributes]) =>
|
|
|
- add(TextInsert(content, attributes));
|
|
|
+ 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));
|
|
|
+ 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));
|
|
|
+ void delete(int length) => add(TextDelete(length: length));
|
|
|
|
|
|
/// The length of the string fo the [Delta].
|
|
|
@override
|
|
@@ -374,7 +254,7 @@ class Delta extends Iterable<TextOperation> {
|
|
|
Delta compose(Delta other) {
|
|
|
final thisIter = _OpIterator(_operations);
|
|
|
final otherIter = _OpIterator(other._operations);
|
|
|
- final ops = <TextOperation>[];
|
|
|
+ final operations = <TextOperation>[];
|
|
|
|
|
|
final firstOther = otherIter.peek();
|
|
|
if (firstOther != null &&
|
|
@@ -385,14 +265,14 @@ class Delta extends Iterable<TextOperation> {
|
|
|
thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) {
|
|
|
firstLeft -= thisIter.peekLength();
|
|
|
final next = thisIter._next();
|
|
|
- ops.add(next);
|
|
|
+ operations.add(next);
|
|
|
}
|
|
|
if (firstOther.length - firstLeft > 0) {
|
|
|
otherIter._next(firstOther.length - firstLeft);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- final delta = Delta(ops);
|
|
|
+ final delta = Delta(operations: operations);
|
|
|
while (thisIter.hasNext || otherIter.hasNext) {
|
|
|
if (otherIter.peek() is TextInsert) {
|
|
|
final next = otherIter._next();
|
|
@@ -414,9 +294,9 @@ class Delta extends Iterable<TextOperation> {
|
|
|
if (otherOp is TextRetain && otherOp.length > 0) {
|
|
|
TextOperation? newOp;
|
|
|
if (thisOp is TextRetain) {
|
|
|
- newOp = TextRetain(length, attributes);
|
|
|
+ newOp = TextRetain(length, attributes: attributes);
|
|
|
} else if (thisOp is TextInsert) {
|
|
|
- newOp = TextInsert(thisOp.content, attributes);
|
|
|
+ newOp = TextInsert(thisOp.text, attributes: attributes);
|
|
|
}
|
|
|
|
|
|
if (newOp != null) {
|
|
@@ -427,7 +307,7 @@ class Delta extends Iterable<TextOperation> {
|
|
|
if (!otherIter.hasNext &&
|
|
|
delta._operations.isNotEmpty &&
|
|
|
delta._operations.last == newOp) {
|
|
|
- final rest = Delta(thisIter.rest());
|
|
|
+ final rest = Delta(operations: thisIter.rest());
|
|
|
return (delta + rest)..chop();
|
|
|
}
|
|
|
} else if (otherOp is TextDelete && (thisOp is TextRetain)) {
|
|
@@ -441,19 +321,19 @@ class Delta extends Iterable<TextOperation> {
|
|
|
|
|
|
/// This method joins two Delta together.
|
|
|
Delta operator +(Delta other) {
|
|
|
- var ops = [..._operations];
|
|
|
+ var operations = [..._operations];
|
|
|
if (other._operations.isNotEmpty) {
|
|
|
- ops.add(other._operations[0]);
|
|
|
- ops.addAll(other._operations.sublist(1));
|
|
|
+ operations.add(other._operations[0]);
|
|
|
+ operations.addAll(other._operations.sublist(1));
|
|
|
}
|
|
|
- return Delta(ops);
|
|
|
+ return Delta(operations: operations);
|
|
|
}
|
|
|
|
|
|
void chop() {
|
|
|
if (_operations.isEmpty) {
|
|
|
return;
|
|
|
}
|
|
|
- _rawString = null;
|
|
|
+ _plainText = null;
|
|
|
final lastOp = _operations.last;
|
|
|
if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) {
|
|
|
_operations.removeLast();
|
|
@@ -491,7 +371,7 @@ class Delta extends Iterable<TextOperation> {
|
|
|
} else if (op is TextRetain && op.attributes != null) {
|
|
|
inverted.retain(
|
|
|
baseOp.length,
|
|
|
- invertAttributes(baseOp.attributes, op.attributes),
|
|
|
+ attributes: invertAttributes(baseOp.attributes, op.attributes),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
@@ -517,9 +397,9 @@ class Delta extends Iterable<TextOperation> {
|
|
|
if (pos == 0) {
|
|
|
return pos - 1;
|
|
|
}
|
|
|
- _rawString ??=
|
|
|
- _operations.whereType<TextInsert>().map((op) => op.content).join();
|
|
|
- _runeIndexes ??= stringIndexes(_rawString!);
|
|
|
+ _plainText ??=
|
|
|
+ _operations.whereType<TextInsert>().map((op) => op.text).join();
|
|
|
+ _runeIndexes ??= stringIndexes(_plainText!);
|
|
|
return _runeIndexes![pos - 1];
|
|
|
}
|
|
|
|
|
@@ -535,7 +415,7 @@ class Delta extends Iterable<TextOperation> {
|
|
|
if (pos >= stringContent.length - 1) {
|
|
|
return stringContent.length;
|
|
|
}
|
|
|
- _runeIndexes ??= stringIndexes(_rawString!);
|
|
|
+ _runeIndexes ??= stringIndexes(_plainText!);
|
|
|
|
|
|
for (var i = pos + 1; i < _runeIndexes!.length; i++) {
|
|
|
if (_runeIndexes![i] != pos) {
|
|
@@ -547,24 +427,115 @@ class Delta extends Iterable<TextOperation> {
|
|
|
}
|
|
|
|
|
|
String toPlainText() {
|
|
|
- _rawString ??=
|
|
|
- _operations.whereType<TextInsert>().map((op) => op.content).join();
|
|
|
- return _rawString!;
|
|
|
+ _plainText ??=
|
|
|
+ _operations.whereType<TextInsert>().map((op) => op.text).join();
|
|
|
+ return _plainText!;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
Iterator<TextOperation> get iterator => _operations.iterator;
|
|
|
+
|
|
|
+ static TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
|
|
|
+ TextOperation? operation;
|
|
|
+
|
|
|
+ if (json['insert'] is String) {
|
|
|
+ final attributes = json['attributes'] as Map<String, dynamic>?;
|
|
|
+ operation = TextInsert(
|
|
|
+ json['insert'] as String,
|
|
|
+ attributes: attributes != null ? {...attributes} : null,
|
|
|
+ );
|
|
|
+ } else if (json['retain'] is int) {
|
|
|
+ final attrs = json['attributes'] as Map<String, dynamic>?;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-List<int> stringIndexes(String content) {
|
|
|
- final indexes = List<int>.filled(content.length, 0);
|
|
|
- final iterator = content.runes.iterator;
|
|
|
+class _OpIterator {
|
|
|
+ _OpIterator(
|
|
|
+ Iterable<TextOperation> operations,
|
|
|
+ ) : _operations = UnmodifiableListView(operations);
|
|
|
|
|
|
- while (iterator.moveNext()) {
|
|
|
- for (var i = 0; i < iterator.currentSize; i++) {
|
|
|
- indexes[iterator.rawIndex + i] = iterator.rawIndex;
|
|
|
+ final UnmodifiableListView<TextOperation> _operations;
|
|
|
+ int _index = 0;
|
|
|
+ int _offset = 0;
|
|
|
+
|
|
|
+ bool get hasNext {
|
|
|
+ return peekLength() < _maxInt;
|
|
|
+ }
|
|
|
+
|
|
|
+ TextOperation? peek() {
|
|
|
+ if (_index >= _operations.length) {
|
|
|
+ return null;
|
|
|
}
|
|
|
+
|
|
|
+ return _operations[_index];
|
|
|
}
|
|
|
|
|
|
- return indexes;
|
|
|
+ int peekLength() {
|
|
|
+ if (_index < _operations.length) {
|
|
|
+ final op = _operations[_index];
|
|
|
+ return op.length - _offset;
|
|
|
+ }
|
|
|
+ return _maxInt;
|
|
|
+ }
|
|
|
+
|
|
|
+ TextOperation _next([int? length]) {
|
|
|
+ length ??= _maxInt;
|
|
|
+
|
|
|
+ if (_index >= _operations.length) {
|
|
|
+ return TextRetain(_maxInt);
|
|
|
+ }
|
|
|
+
|
|
|
+ final nextOp = _operations[_index];
|
|
|
+
|
|
|
+ final offset = _offset;
|
|
|
+ final opLength = nextOp.length;
|
|
|
+ if (length >= opLength - offset) {
|
|
|
+ length = opLength - offset;
|
|
|
+ _index += 1;
|
|
|
+ _offset = 0;
|
|
|
+ } else {
|
|
|
+ _offset += length;
|
|
|
+ }
|
|
|
+ if (nextOp is TextDelete) {
|
|
|
+ return TextDelete(length: length);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nextOp is TextRetain) {
|
|
|
+ return TextRetain(length, attributes: nextOp.attributes);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nextOp is TextInsert) {
|
|
|
+ return TextInsert(
|
|
|
+ nextOp.text.substring(offset, offset + length),
|
|
|
+ attributes: nextOp.attributes,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return TextRetain(_maxInt);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<TextOperation> rest() {
|
|
|
+ if (!hasNext) {
|
|
|
+ return [];
|
|
|
+ } else if (_offset == 0) {
|
|
|
+ return _operations.sublist(_index);
|
|
|
+ } else {
|
|
|
+ final offset = _offset;
|
|
|
+ final index = _index;
|
|
|
+ final next = _next();
|
|
|
+ final rest = _operations.sublist(_index);
|
|
|
+ _offset = offset;
|
|
|
+ _index = index;
|
|
|
+ return [next] + rest;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|