diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart index 79ad5174..1a936b4f 100644 --- a/lib/models/documents/attribute.dart +++ b/lib/models/documents/attribute.dart @@ -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 { static StrikeThroughAttribute strikeThrough = StrikeThroughAttribute(); - static LinkAttribute link = LinkAttribute(); + static LinkAttribute link = LinkAttribute(''); static HeaderAttribute header = HeaderAttribute(1); @@ -53,6 +55,11 @@ class Attribute { bool get isInline => scope == AttributeScope.INLINE; + bool get isBlockExceptHeader => + scope == AttributeScope.BLOCK && key != Attribute.header.key; + + Map toJson() => {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 { } return attribute; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Attribute) return false; + Attribute 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 { @@ -82,7 +105,7 @@ class StrikeThroughAttribute extends Attribute { } class LinkAttribute extends Attribute { - LinkAttribute() : super('link', AttributeScope.INLINE, ''); + LinkAttribute(String val) : super('link', AttributeScope.INLINE, val); } class HeaderAttribute extends Attribute { diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart new file mode 100644 index 00000000..c6652951 --- /dev/null +++ b/lib/models/documents/document.dart @@ -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> _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, +} diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart index 66dbb8ff..b3cdd0d7 100644 --- a/lib/models/documents/nodes/embed.dart +++ b/lib/models/documents/nodes/embed.dart @@ -16,4 +16,30 @@ class Embeddable { m[INLINE_KEY] = inline; return m; } + + static Embeddable fromJson(Map json) { + String type = json[TYPE_KEY] as String; + bool inline = json[INLINE_KEY] as bool; + Map data = Map.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 data = const {}, + }) : super(type, true, data); +} + +class BlockEmbed extends Embeddable { + BlockEmbed( + String type, { + Map data = const {}, + }) : super(type, false, data); } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 19e0ac8e..f133b89c 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -57,7 +57,7 @@ class Line extends Container { String text = data as String; int lineBreak = text.indexOf('\n'); - if (lineBreak == -1) { + if (lineBreak < 0) { _insert(index, text, style); return; } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index e4315063..6571ed0f 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -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; } } diff --git a/lib/models/rules/delete.dart b/lib/models/rules/delete.dart new file mode 100644 index 00000000..f255df3a --- /dev/null +++ b/lib/models/rules/delete.dart @@ -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 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 attributes = op.attributes == null + ? null + : op.attributes.map((String key, dynamic value) => + MapEntry(key, null)); + + if (isNotPlain) { + attributes ??= {}; + 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); + } +} diff --git a/lib/models/rules/format.dart b/lib/models/rules/format.dart new file mode 100644 index 00000000..4dfa866c --- /dev/null +++ b/lib/models/rules/format.dart @@ -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; + } +} diff --git a/lib/models/rules/insert.dart b/lib/models/rules/insert.dart new file mode 100644 index 00000000..8badac59 --- /dev/null +++ b/lib/models/rules/insert.dart @@ -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 nextNewLine = _getNextNewLine(itr); + Map 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 nextNewLine = _getNextNewLine(itr); + Style lineStyle = + Style.fromJson(nextNewLine.item1?.attributes ?? {}); + + Attribute attribute = lineStyle.getBlockExceptHeader(); + if (attribute != null) { + return null; + } + + var blockStyle = {attribute.key: attribute.value}; + + Map resetStyle; + + if (lineStyle.containsKey(Attribute.header.key)) { + resetStyle = HeaderAttribute(null).toJson(); + } + + List 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 nextNewLine = _getNextNewLine(itr); + if (nextNewLine.item1 != null && + nextNewLine.item1.attributes != null && + Style.fromJson(nextNewLine.item1.attributes).getBlockExceptHeader() == + blockStyle) { + return null; + } + + Map attributes = cur.attributes ?? {}; + 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 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 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 attributes = prev.attributes ?? {}; + + 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 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 nextAttributes = + next.attributes ?? const {}; + 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 _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); +} diff --git a/lib/models/rules/rule.dart b/lib/models/rules/rule.dart new file mode 100644 index 00000000..592fc2d7 --- /dev/null +++ b/lib/models/rules/rule.dart @@ -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 _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'); + } +} diff --git a/pubspec.lock b/pubspec.lock index e38e158f..695e46e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index bbded11c..6ef9e898 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: