dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
10 KiB
380 lines
10 KiB
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; |
|
} |
|
|
|
// retain(1) should be '\n', set it with no attribute (default to null) |
|
return Delta()..retain(index)..retain(1); |
|
} |
|
} |
|
|
|
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); |
|
}
|
|
|