|
@@ -12,95 +12,107 @@ abstract class InsertRule extends Rule {
|
|
|
RuleType get type => RuleType.INSERT;
|
|
|
|
|
|
@override
|
|
|
- void validateArgs(int? length, Object? data, Attribute? attribute) {
|
|
|
+ void validateArgs(int? len, Object? data, Attribute? attribute) {
|
|
|
assert(data != null);
|
|
|
assert(attribute == null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* -------------------------------- Rule Impl ------------------------------- */
|
|
|
-
|
|
|
class PreserveLineStyleOnSplitRule extends InsertRule {
|
|
|
const PreserveLineStyleOnSplitRule();
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || data != '\n') {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final before = it.skip(index);
|
|
|
+ final itr = DeltaIterator(document);
|
|
|
+ final before = itr.skip(index);
|
|
|
if (before == null ||
|
|
|
before.data is! String ||
|
|
|
(before.data as String).endsWith('\n')) {
|
|
|
return null;
|
|
|
}
|
|
|
- final after = it.next();
|
|
|
+ final after = itr.next();
|
|
|
if (after.data is! String || (after.data as String).startsWith('\n')) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
final text = after.data as String;
|
|
|
- final delta = Delta()..retain(index + (length ?? 0));
|
|
|
+
|
|
|
+ final delta = Delta()..retain(index + (len ?? 0));
|
|
|
if (text.contains('\n')) {
|
|
|
assert(after.isPlain);
|
|
|
delta.insert('\n');
|
|
|
return delta;
|
|
|
}
|
|
|
-
|
|
|
- final nextNewLine = _getNextNewLine(it);
|
|
|
+ final nextNewLine = _getNextNewLine(itr);
|
|
|
final attributes = nextNewLine.item1?.attributes;
|
|
|
|
|
|
return delta..insert('\n', attributes);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// Preserves block style when user inserts text containing newlines.
|
|
|
+///
|
|
|
+/// This rule handles:
|
|
|
+///
|
|
|
+/// * inserting a new line in a block
|
|
|
+/// * pasting text containing multiple lines of text in a block
|
|
|
+///
|
|
|
+/// This rule may also be activated for changes triggered by auto-correct.
|
|
|
class PreserveBlockStyleOnInsertRule extends InsertRule {
|
|
|
const PreserveBlockStyleOnInsertRule();
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || !data.contains('\n')) {
|
|
|
+ // Only interested in text containing at least one newline character.
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final it = DeltaIterator(document)..skip(index);
|
|
|
+ final itr = DeltaIterator(document)..skip(index);
|
|
|
|
|
|
- final nextNewLine = _getNextNewLine(it);
|
|
|
- final lineStyle = Style.fromJson(
|
|
|
- nextNewLine.item1?.attributes ?? <String, dynamic>{},
|
|
|
- );
|
|
|
+ // Look for the next newline.
|
|
|
+ final nextNewLine = _getNextNewLine(itr);
|
|
|
+ final lineStyle =
|
|
|
+ Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
|
|
|
|
|
|
- final attribute = lineStyle.getBlockExceptHeader();
|
|
|
- if (attribute == null) {
|
|
|
+ final blockStyle = lineStyle.getBlocksExceptHeader();
|
|
|
+ // Are we currently in a block? If not then ignore.
|
|
|
+ if (blockStyle.isEmpty) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final blockStyle = <String, dynamic>{attribute.key: attribute.value};
|
|
|
-
|
|
|
Map<String, dynamic>? resetStyle;
|
|
|
-
|
|
|
+ // If current line had heading style applied to it we'll need to move this
|
|
|
+ // style to the newly inserted line before it and reset style of the
|
|
|
+ // original line.
|
|
|
if (lineStyle.containsKey(Attribute.header.key)) {
|
|
|
resetStyle = Attribute.header.toJson();
|
|
|
}
|
|
|
|
|
|
+ // Go over each inserted line and ensure block style is applied.
|
|
|
final lines = data.split('\n');
|
|
|
- final delta = Delta()..retain(index + (length ?? 0));
|
|
|
+ final delta = Delta()..retain(index + (len ?? 0));
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
final line = lines[i];
|
|
|
if (line.isNotEmpty) {
|
|
|
delta.insert(line);
|
|
|
}
|
|
|
if (i == 0) {
|
|
|
+ // The first line should inherit the lineStyle entirely.
|
|
|
delta.insert('\n', lineStyle.toJson());
|
|
|
} else if (i < lines.length - 1) {
|
|
|
+ // we don't want to insert a newline after the last chunk of text, so -1
|
|
|
delta.insert('\n', blockStyle);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Reset style of the original newline character if needed.
|
|
|
if (resetStyle != null) {
|
|
|
delta
|
|
|
..retain(nextNewLine.item2!)
|
|
@@ -112,6 +124,12 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// Heuristic rule to exit current block when user inserts two consecutive
|
|
|
+/// newlines.
|
|
|
+///
|
|
|
+/// This rule is only applied when the cursor is on the last line of a block.
|
|
|
+/// When the cursor is in the middle of a block we allow adding empty lines
|
|
|
+/// and preserving the block's style.
|
|
|
class AutoExitBlockRule extends InsertRule {
|
|
|
const AutoExitBlockRule();
|
|
|
|
|
@@ -127,40 +145,55 @@ class AutoExitBlockRule extends InsertRule {
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || data != '\n') {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final prev = it.skip(index), cur = it.next();
|
|
|
+ final itr = DeltaIterator(document);
|
|
|
+ final prev = itr.skip(index), cur = itr.next();
|
|
|
final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader();
|
|
|
+ // We are not in a block, ignore.
|
|
|
if (cur.isPlain || blockStyle == null) {
|
|
|
return null;
|
|
|
}
|
|
|
+ // We are not on an empty line, ignore.
|
|
|
if (!_isEmptyLine(prev, cur)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // We are on an empty line. Now we need to determine if we are on the
|
|
|
+ // last line of a block.
|
|
|
+ // First check if `cur` length is greater than 1, this would indicate
|
|
|
+ // that it contains multiple newline characters which share the same style.
|
|
|
+ // This would mean we are not on the last line yet.
|
|
|
+ // `cur.value as String` is safe since we already called isEmptyLine and
|
|
|
+ // know it contains a newline
|
|
|
if ((cur.value as String).length > 1) {
|
|
|
+ // We are not on the last line of this block, ignore.
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final nextNewLine = _getNextNewLine(it);
|
|
|
+ // Keep looking for the next newline character to see if it shares the same
|
|
|
+ // block style as `cur`.
|
|
|
+ final nextNewLine = _getNextNewLine(itr);
|
|
|
if (nextNewLine.item1 != null &&
|
|
|
nextNewLine.item1!.attributes != null &&
|
|
|
Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
|
|
|
blockStyle) {
|
|
|
+ // We are not at the end of this block, ignore.
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // Here we now know that the line after `cur` is not in the same block
|
|
|
+ // therefore we can exit this block.
|
|
|
final attributes = cur.attributes ?? <String, dynamic>{};
|
|
|
- final k = attributes.keys
|
|
|
- .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k));
|
|
|
+ final k =
|
|
|
+ attributes.keys.firstWhere(Attribute.blockKeysExceptHeader.contains);
|
|
|
attributes[k] = null;
|
|
|
// retain(1) should be '\n', set it with no attribute
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..retain(1, attributes);
|
|
|
}
|
|
|
}
|
|
@@ -170,7 +203,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || data != '\n') {
|
|
|
return null;
|
|
|
}
|
|
@@ -187,7 +220,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
|
|
|
resetStyle = Attribute.header.toJson();
|
|
|
}
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..insert('\n', cur.attributes)
|
|
|
..retain(1, resetStyle)
|
|
|
..trim();
|
|
@@ -199,14 +232,14 @@ class InsertEmbedsRule extends InsertRule {
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is String) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final delta = Delta()..retain(index + (length ?? 0));
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final prev = it.skip(index), cur = it.next();
|
|
|
+ final delta = Delta()..retain(index + (len ?? 0));
|
|
|
+ final itr = DeltaIterator(document);
|
|
|
+ final prev = itr.skip(index), cur = itr.next();
|
|
|
|
|
|
final textBefore = prev?.data is String ? prev!.data as String? : '';
|
|
|
final textAfter = cur.data is String ? (cur.data as String?)! : '';
|
|
@@ -222,8 +255,8 @@ class InsertEmbedsRule extends InsertRule {
|
|
|
if (textAfter.contains('\n')) {
|
|
|
lineStyle = cur.attributes;
|
|
|
} else {
|
|
|
- while (it.hasNext) {
|
|
|
- final op = it.next();
|
|
|
+ while (itr.hasNext) {
|
|
|
+ final op = itr.next();
|
|
|
if ((op.data is String ? op.data as String? : '')!.contains('\n')) {
|
|
|
lineStyle = op.attributes;
|
|
|
break;
|
|
@@ -242,52 +275,18 @@ class InsertEmbedsRule extends InsertRule {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
|
|
|
- const ForceNewlineForInsertsAroundEmbedRule();
|
|
|
-
|
|
|
- @override
|
|
|
- Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
- if (data is! String) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- final text = data;
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final prev = it.skip(index), cur = it.next();
|
|
|
- final cursorBeforeEmbed = cur.data is! String;
|
|
|
- final cursorAfterEmbed = prev != null && prev.data is! String;
|
|
|
-
|
|
|
- if (!cursorBeforeEmbed && !cursorAfterEmbed) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- final delta = Delta()..retain(index + (length ?? 0));
|
|
|
- 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? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || data != ' ') {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final prev = it.skip(index);
|
|
|
+ final itr = DeltaIterator(document);
|
|
|
+ final prev = itr.skip(index);
|
|
|
if (prev == null || prev.data is! String) {
|
|
|
return null;
|
|
|
}
|
|
@@ -306,7 +305,7 @@ class AutoFormatLinksRule extends InsertRule {
|
|
|
|
|
|
attributes.addAll(LinkAttribute(link.toString()).toJson());
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0) - cand.length)
|
|
|
+ ..retain(index + (len ?? 0) - cand.length)
|
|
|
..retain(cand.length, attributes)
|
|
|
..insert(data, prev.attributes);
|
|
|
} on FormatException {
|
|
@@ -320,13 +319,13 @@ class PreserveInlineStylesRule extends InsertRule {
|
|
|
|
|
|
@override
|
|
|
Delta? applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
if (data is! String || data.contains('\n')) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- final it = DeltaIterator(document);
|
|
|
- final prev = it.skip(index);
|
|
|
+ final itr = DeltaIterator(document);
|
|
|
+ final prev = itr.skip(index);
|
|
|
if (prev == null ||
|
|
|
prev.data is! String ||
|
|
|
(prev.data as String).contains('\n')) {
|
|
@@ -337,15 +336,15 @@ class PreserveInlineStylesRule extends InsertRule {
|
|
|
final text = data;
|
|
|
if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..insert(text, attributes);
|
|
|
}
|
|
|
|
|
|
attributes.remove(Attribute.link.key);
|
|
|
final delta = Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..insert(text, attributes.isEmpty ? null : attributes);
|
|
|
- final next = it.next();
|
|
|
+ final next = itr.next();
|
|
|
|
|
|
final nextAttributes = next.attributes ?? const <String, dynamic>{};
|
|
|
if (!nextAttributes.containsKey(Attribute.link.key)) {
|
|
@@ -353,7 +352,7 @@ class PreserveInlineStylesRule extends InsertRule {
|
|
|
}
|
|
|
if (attributes[Attribute.link.key] == nextAttributes[Attribute.link.key]) {
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..insert(text, attributes);
|
|
|
}
|
|
|
return delta;
|
|
@@ -365,15 +364,13 @@ class CatchAllInsertRule extends InsertRule {
|
|
|
|
|
|
@override
|
|
|
Delta applyRule(Delta document, int index,
|
|
|
- {int? length, Object? data, Attribute? attribute}) {
|
|
|
+ {int? len, Object? data, Attribute? attribute}) {
|
|
|
return Delta()
|
|
|
- ..retain(index + (length ?? 0))
|
|
|
+ ..retain(index + (len ?? 0))
|
|
|
..insert(data);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* --------------------------------- Helper --------------------------------- */
|
|
|
-
|
|
|
Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) {
|
|
|
Operation op;
|
|
|
for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
|