Rich text editor for Flutter
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.
 
 
 
 
 

381 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 = Attribute.header.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 = Attribute.header.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);
final prev = itr.skip(index);
final 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);
}