From 85537417ffed2484f0fd3869c2d1102ceac60fdc Mon Sep 17 00:00:00 2001
From: singerdmx <singerdmx@gmail.com>
Date: Wed, 16 Dec 2020 22:56:55 -0800
Subject: [PATCH] Add document class

---
 lib/models/documents/attribute.dart   |  27 +-
 lib/models/documents/document.dart    | 143 ++++++++++
 lib/models/documents/nodes/embed.dart |  26 ++
 lib/models/documents/nodes/line.dart  |   2 +-
 lib/models/documents/style.dart       |   3 +-
 lib/models/rules/delete.dart          | 122 ++++++++
 lib/models/rules/format.dart          | 134 +++++++++
 lib/models/rules/insert.dart          | 383 ++++++++++++++++++++++++++
 lib/models/rules/rule.dart            |  67 +++++
 pubspec.lock                          |  14 +
 pubspec.yaml                          |   1 +
 11 files changed, 917 insertions(+), 5 deletions(-)
 create mode 100644 lib/models/documents/document.dart
 create mode 100644 lib/models/rules/delete.dart
 create mode 100644 lib/models/rules/format.dart
 create mode 100644 lib/models/rules/insert.dart
 create mode 100644 lib/models/rules/rule.dart

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<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> {
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<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,
+}
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<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);
 }
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<Leaf> {
 
     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<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);
+  }
+}
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<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);
+}
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<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');
+  }
+}
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: