parent
5373dc69bd
commit
85537417ff
11 changed files with 917 additions and 5 deletions
@ -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, |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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'); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue