Add document class

pull/13/head
singerdmx 4 years ago
parent 5373dc69bd
commit 85537417ff
  1. 27
      lib/models/documents/attribute.dart
  2. 143
      lib/models/documents/document.dart
  3. 26
      lib/models/documents/nodes/embed.dart
  4. 2
      lib/models/documents/nodes/line.dart
  5. 3
      lib/models/documents/style.dart
  6. 122
      lib/models/rules/delete.dart
  7. 134
      lib/models/rules/format.dart
  8. 383
      lib/models/rules/insert.dart
  9. 67
      lib/models/rules/rule.dart
  10. 14
      pubspec.lock
  11. 1
      pubspec.yaml

@ -1,3 +1,5 @@
import 'package:quiver_hashcode/hashcode.dart';
enum AttributeScope {
INLINE, // refer to https://quilljs.com/docs/formats/#inline
BLOCK, // refer to https://quilljs.com/docs/formats/#block
@ -31,7 +33,7 @@ class Attribute<T> {
static StrikeThroughAttribute strikeThrough = StrikeThroughAttribute();
static LinkAttribute link = LinkAttribute();
static LinkAttribute link = LinkAttribute('');
static HeaderAttribute header = HeaderAttribute(1);
@ -53,6 +55,11 @@ class Attribute<T> {
bool get isInline => scope == AttributeScope.INLINE;
bool get isBlockExceptHeader =>
scope == AttributeScope.BLOCK && key != Attribute.header.key;
Map<String, dynamic> toJson() => <String, dynamic>{key: value};
static Attribute fromKeyValue(String key, dynamic value) {
if (!_registry.containsKey(key)) {
throw ArgumentError.value(key, 'key "$key" not found.');
@ -63,6 +70,22 @@ class Attribute<T> {
}
return attribute;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Attribute<T>) return false;
Attribute<T> typedOther = other;
return key == typedOther.key &&
scope == typedOther.scope &&
value == typedOther.value;
}
@override
int get hashCode => hash3(key, scope, value);
@override
String toString() => '$key: $value';
}
class BoldAttribute extends Attribute<bool> {
@ -82,7 +105,7 @@ class StrikeThroughAttribute extends Attribute<bool> {
}
class LinkAttribute extends Attribute<String> {
LinkAttribute() : super('link', AttributeScope.INLINE, '');
LinkAttribute(String val) : super('link', AttributeScope.INLINE, val);
}
class HeaderAttribute extends Attribute<int> {

@ -0,0 +1,143 @@
import 'dart:async';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:tuple/tuple.dart';
import '../rules/rule.dart';
import 'attribute.dart';
import 'nodes/embed.dart';
import 'nodes/node.dart';
/// The rich text document
class Document {
/// The root node of the document tree
final Root _root = Root();
Root get root => _root;
int get length => _root.length;
Delta _delta;
Delta toDelta() => Delta.from(_delta);
final Rules _rules = Rules.getInstance();
final StreamController<Tuple3<Delta, Delta, ChangeSource>> _observer =
StreamController.broadcast();
Delta insert(int index, Object data) {
assert(index >= 0);
assert(data is String || data is Embeddable);
if (data is Embeddable) {
data = (data as Embeddable).toJson();
} else if ((data as String).isEmpty) {
return Delta();
}
Delta delta = _rules.apply(RuleType.INSERT, this, index, data: data);
compose(delta, ChangeSource.LOCAL);
return delta;
}
Delta delete(int index, int len) {
assert(index >= 0 && len > 0);
Delta delta = _rules.apply(RuleType.DELETE, this, index, len: len);
if (delta.isNotEmpty) {
compose(delta, ChangeSource.LOCAL);
}
return delta;
}
Delta replace(int index, int len, Object data) {
assert(index >= 0);
assert(data is String || data is Embeddable);
bool dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
assert(dataIsNotEmpty || len > 0);
Delta delta = Delta();
if (dataIsNotEmpty) {
delta = insert(index + length, data);
}
if (len > 0) {
Delta deleteDelta = delete(index, len);
delta = delta.compose(deleteDelta);
}
return delta;
}
Delta format(int index, int len, Attribute attribute) {
assert(index >= 0 && len >= 0 && attribute != null);
Delta delta = Delta();
Delta formatDelta = _rules.apply(RuleType.FORMAT, this, index,
len: len, attribute: attribute);
if (formatDelta.isNotEmpty) {
compose(formatDelta, ChangeSource.LOCAL);
delta = delta.compose(formatDelta);
}
return delta;
}
compose(Delta delta, ChangeSource changeSource) {
assert(!_observer.isClosed);
delta.trim();
assert(delta.isNotEmpty);
int offset = 0;
delta = _transform(delta);
Delta originalDelta = toDelta();
for (Operation op in delta.toList()) {
Style style =
op.attributes != null ? Style.fromJson(op.attributes) : null;
if (op.isInsert) {
_root.insert(offset, _normalize(op.data), style);
} else if (op.isDelete) {
_root.delete(offset, op.length);
} else if (op.attributes != null) {
_root.retain(offset, op.length, style);
}
if (!op.isDelete) {
offset += op.length;
}
}
_delta = _delta.compose(delta);
if (_delta != _root.toDelta()) {
throw ('Compose failed');
}
_observer.add(Tuple3(originalDelta, delta, changeSource));
}
static Delta _transform(Delta delta) {
Delta res = Delta();
for (Operation op in delta.toList()) {
// TODO
res.push(op);
}
return res;
}
Object _normalize(Object data) {
return data is String
? data
: data is Embeddable
? data
: Embeddable.fromJson(data);
}
}
enum ChangeSource {
LOCAL,
REMOTE,
}

@ -16,4 +16,30 @@ class Embeddable {
m[INLINE_KEY] = inline;
return m;
}
static Embeddable fromJson(Map<String, dynamic> json) {
String type = json[TYPE_KEY] as String;
bool inline = json[INLINE_KEY] as bool;
Map<String, dynamic> data = Map<String, dynamic>.from(json);
data.remove(TYPE_KEY);
data.remove(INLINE_KEY);
if (inline) {
return Span(type, data: data);
}
return BlockEmbed(type, data: data);
}
}
class Span extends Embeddable {
Span(
String type, {
Map<String, dynamic> data = const {},
}) : super(type, true, data);
}
class BlockEmbed extends Embeddable {
BlockEmbed(
String type, {
Map<String, dynamic> data = const {},
}) : super(type, false, data);
}

@ -57,7 +57,7 @@ class Line extends Container<Leaf> {
String text = data as String;
int lineBreak = text.indexOf('\n');
if (lineBreak == -1) {
if (lineBreak < 0) {
_insert(index, text, style);
return;
}

@ -41,8 +41,7 @@ class Style {
Attribute getBlockExceptHeader() {
for (Attribute val in values) {
if (val.scope == AttributeScope.BLOCK &&
val.key != Attribute.header.key) {
if (val.isBlockExceptHeader) {
return val;
}
}

@ -0,0 +1,122 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import 'package:quill_delta/quill_delta.dart';
abstract class DeleteRule extends Rule {
const DeleteRule();
@override
RuleType get type => RuleType.DELETE;
@override
validateArgs(int len, Object data, Attribute attribute) {
assert(len != null);
assert(data == null);
assert(attribute == null);
}
}
class CatchAllDeleteRule extends DeleteRule {
const CatchAllDeleteRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
return Delta()
..retain(index)
..delete(len);
}
}
class PreserveLineStyleOnMergeRule extends DeleteRule {
const PreserveLineStyleOnMergeRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation op = itr.next(1);
if (op.data != '\n') {
return null;
}
bool isNotPlain = op.isNotPlain;
Map<String, dynamic> attrs = op.attributes;
itr.skip(len - 1);
Delta delta = Delta()
..retain(index)
..delete(len);
while (itr.hasNext) {
op = itr.next();
String text = op.data is String ? op.data as String : '';
int lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
delta..retain(op.length);
continue;
}
Map<String, dynamic> attributes = op.attributes == null
? null
: op.attributes.map<String, dynamic>((String key, dynamic value) =>
MapEntry<String, dynamic>(key, null));
if (isNotPlain) {
attributes ??= <String, dynamic>{};
attributes.addAll(attrs);
}
delta..retain(lineBreak)..retain(1, attributes);
}
return delta;
}
}
class EnsureEmbedLineRule extends DeleteRule {
const EnsureEmbedLineRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
DeltaIterator itr = DeltaIterator(document);
Operation op = itr.skip(index);
int indexDelta = 0, lengthDelta = 0, remain = len;
bool embedFound = op != null && op.data is! String;
bool hasLineBreakBefore =
!embedFound && (op == null || (op?.data as String).endsWith('\n'));
if (embedFound) {
Operation candidate = itr.next(1);
remain--;
if (candidate.data == '\n') {
indexDelta++;
lengthDelta--;
candidate = itr.next(1);
remain--;
if (candidate.data == '\n') {
lengthDelta++;
}
}
}
op = itr.skip(remain);
if (op != null &&
(op?.data is String ? op.data as String : '').endsWith('\n')) {
Operation candidate = itr.next(1);
if (candidate.data is! String && !hasLineBreakBefore) {
embedFound = true;
lengthDelta--;
}
}
if (!embedFound) {
return null;
}
return Delta()
..retain(index + indexDelta)
..delete(len + lengthDelta);
}
}

@ -0,0 +1,134 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import 'package:quill_delta/quill_delta.dart';
abstract class FormatRule extends Rule {
const FormatRule();
@override
RuleType get type => RuleType.FORMAT;
@override
validateArgs(int len, Object data, Attribute attribute) {
assert(len != null);
assert(data == null);
assert(attribute != null);
}
}
class ResolveLineFormatRule extends FormatRule {
const ResolveLineFormatRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (attribute.scope != AttributeScope.BLOCK) {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation op;
for (int cur = 0; cur < len && itr.hasNext; cur += op.length) {
op = itr.next(len - cur);
if (op.data is! String || !(op.data as String).contains('\n')) {
delta.retain(op.length);
continue;
}
String text = op.data;
Delta tmp = Delta();
int offset = 0;
for (int lineBreak = text.indexOf('\n');
lineBreak >= 0;
lineBreak = text.indexOf('\n', offset)) {
tmp..retain(lineBreak - offset)..retain(1, attribute.toJson());
offset = lineBreak + 1;
}
tmp.retain(text.length - offset);
delta = delta.concat(tmp);
}
while (itr.hasNext) {
op = itr.next();
String text = op.data is String ? op.data as String : '';
int lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
delta..retain(op.length);
continue;
}
delta..retain(lineBreak)..retain(1, attribute.toJson());
break;
}
return delta;
}
}
class FormatLinkAtCaretPositionRule extends FormatRule {
const FormatLinkAtCaretPositionRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (attribute.key != Attribute.link.key || len > 0) {
return null;
}
Delta delta = Delta();
DeltaIterator itr = DeltaIterator(document);
Operation before = itr.skip(index), after = itr.next();
int beg = index, retain = 0;
if (before != null && before.hasAttribute(attribute.key)) {
beg -= before.length;
retain = before.length;
}
if (after != null && after.hasAttribute(attribute.key)) {
retain += after.length;
}
if (retain == 0) {
return null;
}
delta..retain(beg)..retain(retain, attribute.toJson());
return delta;
}
}
class ResolveInlineFormatRule extends FormatRule {
const ResolveInlineFormatRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (attribute.scope != AttributeScope.BLOCK) {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation op;
for (int cur = 0; cur < len && itr.hasNext; cur += op.length) {
op = itr.next(len - cur);
String text = op.data is String ? op.data as String : '';
int lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
delta.retain(op.length, attribute.toJson());
continue;
}
int pos = 0;
while (lineBreak >= 0) {
delta..retain(lineBreak - pos, attribute.toJson())..retain(1);
pos = lineBreak + 1;
lineBreak = text.indexOf('\n', pos);
}
if (pos < op.length) {
delta.retain(op.length - pos, attribute.toJson());
}
}
return delta;
}
}

@ -0,0 +1,383 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:tuple/tuple.dart';
abstract class InsertRule extends Rule {
const InsertRule();
@override
RuleType get type => RuleType.INSERT;
@override
validateArgs(int len, Object data, Attribute attribute) {
assert(len == null);
assert(data != null);
assert(attribute == null);
}
}
class PreserveLineStyleOnSplitRule extends InsertRule {
const PreserveLineStyleOnSplitRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || (data as String) != '\n') {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation before = itr.skip(index);
if (before == null ||
before.data is! String ||
(before.data as String).endsWith('\n')) {
return null;
}
Operation after = itr.next();
if (after == null ||
after.data is! String ||
(after.data as String).startsWith('\n')) {
return null;
}
final text = after.data as String;
Delta delta = Delta()..retain(index);
if (text.contains('\n')) {
assert(after.isPlain);
delta..insert('\n');
return delta;
}
Tuple2<Operation, int> nextNewLine = _getNextNewLine(itr);
Map<String, dynamic> attributes = nextNewLine?.item1?.attributes;
return delta..insert('\n', attributes);
}
}
class PreserveBlockStyleOnInsertRule extends InsertRule {
const PreserveBlockStyleOnInsertRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || !(data as String).contains('\n')) {
return null;
}
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Tuple2<Operation, int> nextNewLine = _getNextNewLine(itr);
Style lineStyle =
Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
Attribute attribute = lineStyle.getBlockExceptHeader();
if (attribute != null) {
return null;
}
var blockStyle = <String, dynamic>{attribute.key: attribute.value};
Map<String, dynamic> resetStyle;
if (lineStyle.containsKey(Attribute.header.key)) {
resetStyle = HeaderAttribute(null).toJson();
}
List<String> lines = (data as String).split('\n');
Delta delta = Delta()..retain(index);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.isNotEmpty) {
delta.insert(line);
}
if (i == 0) {
delta.insert('\n', lineStyle.toJson());
} else if (i < lines.length - 1) {
delta.insert('\n', blockStyle);
}
}
if (resetStyle != null) {
delta.retain(nextNewLine.item2);
delta
..retain((nextNewLine.item1.data as String).indexOf('\n'))
..retain(1, resetStyle);
}
return delta;
}
}
class AutoExitBlockRule extends InsertRule {
const AutoExitBlockRule();
bool _isEmptyLine(Operation before, Operation after) {
if (before == null) {
return true;
}
return before.data is String &&
(before.data as String).endsWith('\n') &&
after.data is String &&
(after.data as String).startsWith('\n');
}
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || (data as String) != '\n') {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation prev = itr.skip(index), cur = itr.next();
Attribute blockStyle =
Style.fromJson(cur.attributes).getBlockExceptHeader();
if (cur.isPlain || blockStyle == null) {
return null;
}
if (!_isEmptyLine(prev, cur)) {
return null;
}
if ((cur.value as String).length > 1) {
return null;
}
Tuple2<Operation, int> nextNewLine = _getNextNewLine(itr);
if (nextNewLine.item1 != null &&
nextNewLine.item1.attributes != null &&
Style.fromJson(nextNewLine.item1.attributes).getBlockExceptHeader() ==
blockStyle) {
return null;
}
Map<String, dynamic> attributes = cur.attributes ?? <String, dynamic>{};
BlockQuoteAttribute blockQuoteAttribute = BlockQuoteAttribute();
blockQuoteAttribute.value = null;
attributes.addAll(blockQuoteAttribute.toJson()); // TODO
return Delta()..retain(index)..retain(1, attributes);
}
}
class ResetLineFormatOnNewLineRule extends InsertRule {
const ResetLineFormatOnNewLineRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || (data as String) != '\n') {
return null;
}
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation cur = itr.next();
if (cur.data is! String || !(cur.data as String).startsWith('\n')) {
return null;
}
Map<String, dynamic> resetStyle;
if (cur.attributes != null &&
cur.attributes.containsKey(Attribute.header.key)) {
resetStyle = HeaderAttribute(null).toJson();
}
return Delta()
..retain(index)
..insert('\n', cur.attributes)
..retain(1, resetStyle)
..trim();
}
}
class InsertEmbedsRule extends InsertRule {
const InsertEmbedsRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is String) {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
Operation prev = itr.skip(index), cur = itr.next();
String textBefore = prev?.data is String ? prev.data as String : '';
String textAfter = cur.data is String ? cur.data as String : '';
final isNewlineBefore = prev == null || textBefore.endsWith('\n');
final isNewlineAfter = textAfter.startsWith('\n');
if (isNewlineBefore && isNewlineAfter) {
return delta..insert(data);
}
Map<String, dynamic> lineStyle;
if (textAfter.contains('\n')) {
lineStyle = cur.attributes;
} else {
while (itr.hasNext) {
Operation op = itr.next();
if ((op.data is String ? op.data as String : '').indexOf('\n') >= 0) {
lineStyle = op.attributes;
break;
}
}
}
if (!isNewlineBefore) {
delta..insert('\n', lineStyle);
}
delta..insert(data);
if (!isNewlineAfter) {
delta..insert('\n');
}
return delta;
}
}
class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
const ForceNewlineForInsertsAroundEmbedRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String) {
return null;
}
String text = data as String;
DeltaIterator itr = DeltaIterator(document);
Operation prev = itr.skip(index), cur = itr.next();
bool cursorBeforeEmbed = cur.data is! String;
bool cursorAfterEmbed = prev != null && prev.data is! String;
if (!cursorBeforeEmbed && !cursorAfterEmbed) {
return null;
}
Delta delta = Delta()..retain(index);
if (cursorBeforeEmbed && !text.endsWith('\n')) {
return delta..insert(text)..insert('\n');
}
if (cursorAfterEmbed && !text.startsWith('\n')) {
return delta..insert('\n')..insert(text);
}
return delta..insert(text);
}
}
class AutoFormatLinksRule extends InsertRule {
const AutoFormatLinksRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || (data as String) != ' ') {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation prev = itr.skip(index);
if (prev == null || prev.data is! String) {
return null;
}
try {
String cand = (prev.data as String).split('\n').last.split(' ').last;
Uri link = Uri.parse(cand);
if (!['https', 'http'].contains(link.scheme)) {
return null;
}
Map<String, dynamic> attributes = prev.attributes ?? <String, dynamic>{};
if (attributes.containsKey(Attribute.link.key)) {
return null;
}
attributes.addAll(LinkAttribute(link.toString()).toJson());
return Delta()
..retain(index - cand.length)
..retain(cand.length, attributes)
..insert(data as String, prev.attributes);
} on FormatException {
return null;
}
}
}
class PreserveInlineStylesRule extends InsertRule {
const PreserveInlineStylesRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
if (data is! String || (data as String).contains('\n')) {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation prev = itr.skip(index);
if (prev == null ||
prev.data is! String ||
(prev.data as String).contains('\n')) {
return null;
}
Map<String, dynamic> attributes = prev.attributes;
String text = data as String;
if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
return Delta()
..retain(index)
..insert(text, attributes);
}
attributes.remove(Attribute.link.key);
Delta delta = Delta()
..retain(index)
..insert(text, attributes.isEmpty ? null : attributes);
Operation next = itr.next();
if (next == null) {
return delta;
}
Map<String, dynamic> nextAttributes =
next.attributes ?? const <String, dynamic>{};
if (!nextAttributes.containsKey(Attribute.link.key)) {
return delta;
}
if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) {
return Delta()
..retain(index)
..insert(text, attributes);
}
return delta;
}
}
class CatchAllInsertRule extends InsertRule {
const CatchAllInsertRule();
@override
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute}) {
return Delta()
..retain(index)
..insert(data);
}
}
Tuple2<Operation, int> _getNextNewLine(DeltaIterator iterator) {
Operation op;
for (int skipped = 0; iterator.hasNext; skipped += op.length) {
op = iterator.next();
int lineBreak = (op.data is String ? op.data as String : '').indexOf('\n');
if (lineBreak >= 0) {
return Tuple2(op, skipped);
}
}
return Tuple2(null, null);
}

@ -0,0 +1,67 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:quill_delta/quill_delta.dart';
import 'delete.dart';
import 'format.dart';
import 'insert.dart';
enum RuleType { INSERT, DELETE, FORMAT }
abstract class Rule {
const Rule();
Delta apply(Delta document, int index,
{int len, Object data, Attribute attribute}) {
assert(document != null);
assert(index != null);
validateArgs(len, data, attribute);
return applyRule(document, index,
len: len, data: data, attribute: attribute);
}
validateArgs(int len, Object data, Attribute attribute);
Delta applyRule(Delta document, int index,
{int len, Object data, Attribute attribute});
RuleType get type;
}
class Rules {
final List<Rule> _rules;
static final Rules _instance = Rules([
FormatLinkAtCaretPositionRule(),
ResolveLineFormatRule(),
ResolveInlineFormatRule(),
InsertEmbedsRule(),
ForceNewlineForInsertsAroundEmbedRule(),
AutoExitBlockRule(),
PreserveBlockStyleOnInsertRule(),
PreserveLineStyleOnSplitRule(),
ResetLineFormatOnNewLineRule(),
AutoFormatLinksRule(),
PreserveInlineStylesRule(),
CatchAllInsertRule(),
EnsureEmbedLineRule(),
PreserveLineStyleOnMergeRule(),
CatchAllDeleteRule(),
]);
Rules(this._rules);
static Rules getInstance() => _instance;
Delta apply(RuleType ruleType, Document document, int index,
{int len, Object data, Attribute attribute}) {
Delta delta = document.toDelta();
for (var rule in _rules) {
delta =
rule.apply(delta, index, len: len, data: data, attribute: attribute);
if (delta != null) {
return delta..trim();
}
}
throw('Apply rules failed');
}
}

@ -88,6 +88,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
quiver_hashcode:
dependency: "direct main"
description:
@ -142,6 +149,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
tuple:
dependency: "direct main"
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
typed_data:
dependency: transitive
description:

@ -15,6 +15,7 @@ dependencies:
quill_delta: ^2.0.0
quiver_hashcode: ^2.0.0
collection: ^1.14.13
tuple: ^1.0.3
dev_dependencies:
flutter_test:

Loading…
Cancel
Save