mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
update flowy_editor models
This commit is contained in:
parent
e4db7222f8
commit
2a42fd108c
@ -1,3 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
enum AttributeScope {
|
||||
@ -14,9 +16,10 @@ class Attribute<T> {
|
||||
final AttributeScope scope;
|
||||
final T value;
|
||||
|
||||
static final Map<String, Attribute> _registry = {
|
||||
static final Map<String, Attribute> _registry = LinkedHashMap.of({
|
||||
Attribute.bold.key: Attribute.bold,
|
||||
Attribute.italic.key: Attribute.italic,
|
||||
Attribute.small.key: Attribute.small,
|
||||
Attribute.underline.key: Attribute.underline,
|
||||
Attribute.strikeThrough.key: Attribute.strikeThrough,
|
||||
Attribute.font.key: Attribute.font,
|
||||
@ -26,23 +29,23 @@ class Attribute<T> {
|
||||
Attribute.background.key: Attribute.background,
|
||||
Attribute.placeholder.key: Attribute.placeholder,
|
||||
Attribute.header.key: Attribute.header,
|
||||
Attribute.indent.key: Attribute.indent,
|
||||
Attribute.align.key: Attribute.align,
|
||||
Attribute.list.key: Attribute.list,
|
||||
Attribute.codeBlock.key: Attribute.codeBlock,
|
||||
Attribute.quoteBlock.key: Attribute.quoteBlock,
|
||||
Attribute.indent.key: Attribute.indent,
|
||||
Attribute.width.key: Attribute.width,
|
||||
Attribute.height.key: Attribute.height,
|
||||
Attribute.style.key: Attribute.style,
|
||||
Attribute.token.key: Attribute.token,
|
||||
};
|
||||
|
||||
// Attribute Properties
|
||||
});
|
||||
|
||||
static final BoldAttribute bold = BoldAttribute();
|
||||
|
||||
static final ItalicAttribute italic = ItalicAttribute();
|
||||
|
||||
static final SmallAttribute small = SmallAttribute();
|
||||
|
||||
static final UnderlineAttribute underline = UnderlineAttribute();
|
||||
|
||||
static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute();
|
||||
@ -79,67 +82,10 @@ class Attribute<T> {
|
||||
|
||||
static final TokenAttribute token = TokenAttribute('');
|
||||
|
||||
static Attribute<int?> get h1 => HeaderAttribute(level: 1);
|
||||
|
||||
static Attribute<int?> get h2 => HeaderAttribute(level: 2);
|
||||
|
||||
static Attribute<int?> get h3 => HeaderAttribute(level: 3);
|
||||
|
||||
static Attribute<int?> get h4 => HeaderAttribute(level: 4);
|
||||
|
||||
static Attribute<int?> get h5 => HeaderAttribute(level: 5);
|
||||
|
||||
static Attribute<int?> get h6 => HeaderAttribute(level: 6);
|
||||
|
||||
static Attribute<String?> get leftAlignment => AlignAttribute('left');
|
||||
|
||||
static Attribute<String?> get centerAlignment => AlignAttribute('center');
|
||||
|
||||
static Attribute<String?> get rightAlignment => AlignAttribute('right');
|
||||
|
||||
static Attribute<String?> get justifyAlignment => AlignAttribute('justify');
|
||||
|
||||
static Attribute<String?> get bullet => ListAttribute('bullet');
|
||||
|
||||
static Attribute<String?> get ordered => ListAttribute('ordered');
|
||||
|
||||
static Attribute<String?> get checked => ListAttribute('checked');
|
||||
|
||||
static Attribute<String?> get unchecked => ListAttribute('unchecked');
|
||||
|
||||
static Attribute<int?> get indentL1 => IndentAttribute(level: 1);
|
||||
|
||||
static Attribute<int?> get indentL2 => IndentAttribute(level: 2);
|
||||
|
||||
static Attribute<int?> get indentL3 => IndentAttribute(level: 3);
|
||||
|
||||
static Attribute<int?> get indentL4 => IndentAttribute(level: 4);
|
||||
|
||||
static Attribute<int?> get indentL5 => IndentAttribute(level: 5);
|
||||
|
||||
static Attribute<int?> get indentL6 => IndentAttribute(level: 6);
|
||||
|
||||
static Attribute<int?> getIndentLevel(int? level) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return indentL1;
|
||||
case 2:
|
||||
return indentL2;
|
||||
case 3:
|
||||
return indentL3;
|
||||
case 4:
|
||||
return indentL4;
|
||||
case 5:
|
||||
return indentL5;
|
||||
default:
|
||||
return indentL6;
|
||||
}
|
||||
}
|
||||
|
||||
// Keys Container
|
||||
static final Set<String> inlineKeys = {
|
||||
Attribute.bold.key,
|
||||
Attribute.italic.key,
|
||||
Attribute.small.key,
|
||||
Attribute.underline.key,
|
||||
Attribute.strikeThrough.key,
|
||||
Attribute.link.key,
|
||||
@ -148,37 +94,109 @@ class Attribute<T> {
|
||||
Attribute.placeholder.key,
|
||||
};
|
||||
|
||||
static final Set<String> blockKeys = {
|
||||
static final Set<String> blockKeys = LinkedHashSet.of({
|
||||
Attribute.header.key,
|
||||
Attribute.indent.key,
|
||||
Attribute.align.key,
|
||||
Attribute.list.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.quoteBlock.key,
|
||||
};
|
||||
Attribute.indent.key,
|
||||
});
|
||||
|
||||
static final Set<String> blockKeysExceptHeader = blockKeys
|
||||
..remove(Attribute.header.key);
|
||||
static final Set<String> blockKeysExceptHeader = LinkedHashSet.of({
|
||||
Attribute.list.key,
|
||||
Attribute.align.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.quoteBlock.key,
|
||||
Attribute.indent.key,
|
||||
});
|
||||
|
||||
// Utils
|
||||
static final Set<String> exclusiveBlockKeys = LinkedHashSet.of({
|
||||
Attribute.header.key,
|
||||
Attribute.list.key,
|
||||
Attribute.codeBlock.key,
|
||||
Attribute.quoteBlock.key,
|
||||
});
|
||||
|
||||
bool get isInline => AttributeScope.INLINE == scope;
|
||||
static Attribute<int?> get h1 => HeaderAttribute(level: 1);
|
||||
|
||||
bool get isIgnored => AttributeScope.IGNORE == scope;
|
||||
static Attribute<int?> get h2 => HeaderAttribute(level: 2);
|
||||
|
||||
static Attribute<int?> get h3 => HeaderAttribute(level: 3);
|
||||
|
||||
// "attributes":{"align":"left"}
|
||||
static Attribute<String?> get leftAlignment => AlignAttribute('left');
|
||||
|
||||
// "attributes":{"align":"center"}
|
||||
static Attribute<String?> get centerAlignment => AlignAttribute('center');
|
||||
|
||||
// "attributes":{"align":"right"}
|
||||
static Attribute<String?> get rightAlignment => AlignAttribute('right');
|
||||
|
||||
// "attributes":{"align":"justify"}
|
||||
static Attribute<String?> get justifyAlignment => AlignAttribute('justify');
|
||||
|
||||
// "attributes":{"list":"bullet"}
|
||||
static Attribute<String?> get bullet => ListAttribute('bullet');
|
||||
|
||||
// "attributes":{"list":"ordered"}
|
||||
static Attribute<String?> get ordered => ListAttribute('ordered');
|
||||
|
||||
// "attributes":{"list":"checked"}
|
||||
static Attribute<String?> get checked => ListAttribute('checked');
|
||||
|
||||
// "attributes":{"list":"unchecked"}
|
||||
static Attribute<String?> get unchecked => ListAttribute('unchecked');
|
||||
|
||||
// "attributes":{"indent":1"}
|
||||
static Attribute<int?> get indentL1 => IndentAttribute(level: 1);
|
||||
|
||||
// "attributes":{"indent":2"}
|
||||
static Attribute<int?> get indentL2 => IndentAttribute(level: 2);
|
||||
|
||||
// "attributes":{"indent":3"}
|
||||
static Attribute<int?> get indentL3 => IndentAttribute(level: 3);
|
||||
|
||||
static Attribute<int?> getIndentLevel(int? level) {
|
||||
if (level == 1) {
|
||||
return indentL1;
|
||||
}
|
||||
if (level == 2) {
|
||||
return indentL2;
|
||||
}
|
||||
if (level == 3) {
|
||||
return indentL3;
|
||||
}
|
||||
return IndentAttribute(level: level);
|
||||
}
|
||||
|
||||
bool get isInline => scope == AttributeScope.INLINE;
|
||||
|
||||
bool get isBlockExceptHeader => blockKeysExceptHeader.contains(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.');
|
||||
static Attribute? fromKeyValue(String key, dynamic value) {
|
||||
final origin = _registry[key];
|
||||
if (origin == null) {
|
||||
return null;
|
||||
}
|
||||
final origin = _registry[key]!;
|
||||
final attribute = clone(origin, value);
|
||||
return attribute;
|
||||
}
|
||||
|
||||
static int getRegistryOrder(Attribute attribute) {
|
||||
var order = 0;
|
||||
for (final attr in _registry.values) {
|
||||
if (attr.key == attribute.key) {
|
||||
break;
|
||||
}
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
static Attribute clone(Attribute origin, dynamic value) {
|
||||
return Attribute(origin.key, origin.scope, value);
|
||||
}
|
||||
@ -186,7 +204,7 @@ class Attribute<T> {
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! Attribute<T>) return false;
|
||||
if (other is! Attribute) return false;
|
||||
final typedOther = other;
|
||||
return key == typedOther.key &&
|
||||
scope == typedOther.scope &&
|
||||
@ -202,12 +220,6 @@ class Attribute<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Attributes Impl */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* --------------------------------- INLINE --------------------------------- */
|
||||
|
||||
class BoldAttribute extends Attribute<bool> {
|
||||
BoldAttribute() : super('bold', AttributeScope.INLINE, true);
|
||||
}
|
||||
@ -216,42 +228,44 @@ class ItalicAttribute extends Attribute<bool> {
|
||||
ItalicAttribute() : super('italic', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class SmallAttribute extends Attribute<bool> {
|
||||
SmallAttribute() : super('small', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class UnderlineAttribute extends Attribute<bool> {
|
||||
UnderlineAttribute() : super('underline', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class StrikeThroughAttribute extends Attribute<bool> {
|
||||
StrikeThroughAttribute()
|
||||
: super('strikethrough', AttributeScope.INLINE, true);
|
||||
StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
class FontAttribute extends Attribute<String?> {
|
||||
FontAttribute(String? value) : super('font', AttributeScope.INLINE, value);
|
||||
FontAttribute(String? val) : super('font', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class SizeAttribute extends Attribute<String?> {
|
||||
SizeAttribute(String? value) : super('size', AttributeScope.INLINE, value);
|
||||
SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class LinkAttribute extends Attribute<String?> {
|
||||
LinkAttribute(String? value) : super('link', AttributeScope.INLINE, value);
|
||||
LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class ColorAttribute extends Attribute<String?> {
|
||||
ColorAttribute(String? value) : super('color', AttributeScope.INLINE, value);
|
||||
ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class BackgroundAttribute extends Attribute<String?> {
|
||||
BackgroundAttribute(String? value)
|
||||
: super('background', AttributeScope.INLINE, value);
|
||||
BackgroundAttribute(String? val)
|
||||
: super('background', AttributeScope.INLINE, val);
|
||||
}
|
||||
|
||||
class PlaceholderAttribute extends Attribute<bool?> {
|
||||
/// This is custom attribute for hint
|
||||
class PlaceholderAttribute extends Attribute<bool> {
|
||||
PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true);
|
||||
}
|
||||
|
||||
/* ---------------------------------- BLOCK --------------------------------- */
|
||||
|
||||
class HeaderAttribute extends Attribute<int?> {
|
||||
HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level);
|
||||
}
|
||||
@ -261,36 +275,33 @@ class IndentAttribute extends Attribute<int?> {
|
||||
}
|
||||
|
||||
class AlignAttribute extends Attribute<String?> {
|
||||
AlignAttribute(String? value) : super('align', AttributeScope.BLOCK, value);
|
||||
AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val);
|
||||
}
|
||||
|
||||
class ListAttribute extends Attribute<String?> {
|
||||
ListAttribute(String? value) : super('list', AttributeScope.BLOCK, value);
|
||||
ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val);
|
||||
}
|
||||
|
||||
class CodeBlockAttribute extends Attribute<bool?> {
|
||||
CodeBlockAttribute() : super('code_block', AttributeScope.BLOCK, true);
|
||||
class CodeBlockAttribute extends Attribute<bool> {
|
||||
CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true);
|
||||
}
|
||||
|
||||
class QuoteBlockAttribute extends Attribute<bool?> {
|
||||
QuoteBlockAttribute() : super('quote_block', AttributeScope.BLOCK, true);
|
||||
}
|
||||
|
||||
/* --------------------------------- IGNORE --------------------------------- */
|
||||
|
||||
class WidthAttribute extends Attribute<String?> {
|
||||
WidthAttribute(String? value) : super('width', AttributeScope.IGNORE, value);
|
||||
WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class HeightAttribute extends Attribute<String?> {
|
||||
HeightAttribute(String? value)
|
||||
: super('height', AttributeScope.IGNORE, value);
|
||||
HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class StyleAttribute extends Attribute<String?> {
|
||||
StyleAttribute(String? value) : super('style', AttributeScope.IGNORE, value);
|
||||
StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
||||
class TokenAttribute extends Attribute<String?> {
|
||||
TokenAttribute(String? value) : super('token', AttributeScope.IGNORE, value);
|
||||
class TokenAttribute extends Attribute<String> {
|
||||
TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ abstract class EditorChangesetSender {
|
||||
/// The rich text document
|
||||
class Document {
|
||||
EditorChangesetSender? sender;
|
||||
|
||||
Document({this.sender}) : _delta = Delta()..insert('\n') {
|
||||
_loadDocument(_delta);
|
||||
}
|
||||
@ -31,7 +30,7 @@ class Document {
|
||||
}
|
||||
|
||||
Document.fromDelta(Delta delta) : _delta = delta {
|
||||
_loadDocument(_delta);
|
||||
_loadDocument(delta);
|
||||
}
|
||||
|
||||
/// The root node of the document tree
|
||||
@ -47,6 +46,10 @@ class Document {
|
||||
|
||||
final Rules _rules = Rules.getInstance();
|
||||
|
||||
void setCustomRules(List<Rule> customRules) {
|
||||
_rules.setCustomRules(customRules);
|
||||
}
|
||||
|
||||
final StreamController<Tuple3<Delta, Delta, ChangeSource>> _observer =
|
||||
StreamController.broadcast();
|
||||
|
||||
@ -54,12 +57,7 @@ class Document {
|
||||
|
||||
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
|
||||
|
||||
bool get hasUndo => _history.hasUndo;
|
||||
|
||||
bool get hasRedo => _history.hasRedo;
|
||||
|
||||
Delta insert(int index, Object? data, {int replaceLength = 0}) {
|
||||
Log.trace('insert $data at $index');
|
||||
assert(index >= 0);
|
||||
assert(data is String || data is Embeddable);
|
||||
if (data is Embeddable) {
|
||||
@ -68,79 +66,71 @@ class Document {
|
||||
return Delta();
|
||||
}
|
||||
|
||||
final delta = _rules.apply(
|
||||
RuleType.INSERT,
|
||||
this,
|
||||
index,
|
||||
data: data,
|
||||
length: replaceLength,
|
||||
);
|
||||
|
||||
final delta = _rules.apply(RuleType.INSERT, this, index,
|
||||
data: data, len: replaceLength);
|
||||
compose(delta, ChangeSource.LOCAL);
|
||||
Log.trace('current document $_delta');
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta delete(int index, int length) {
|
||||
Log.trace('delete $length at $index');
|
||||
assert(index >= 0 && length > 0);
|
||||
final delta = _rules.apply(RuleType.DELETE, this, index, length: length);
|
||||
Delta delete(int index, int len) {
|
||||
assert(index >= 0 && len > 0);
|
||||
final delta = _rules.apply(RuleType.DELETE, this, index, len: len);
|
||||
if (delta.isNotEmpty) {
|
||||
compose(delta, ChangeSource.LOCAL);
|
||||
}
|
||||
Log.trace('current document $_delta');
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta replace(int index, int length, Object? data) {
|
||||
Log.trace('replace $length at $index with $data');
|
||||
Delta replace(int index, int len, Object? data) {
|
||||
assert(index >= 0);
|
||||
assert(data is String || data is Embeddable);
|
||||
|
||||
final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
|
||||
assert(dataIsNotEmpty || length > 0);
|
||||
|
||||
assert(dataIsNotEmpty || len > 0);
|
||||
|
||||
var delta = Delta();
|
||||
|
||||
// We have to insert before applying delete rules
|
||||
// Otherwise delete would be operating on stale document snapshot.
|
||||
if (dataIsNotEmpty) {
|
||||
delta = insert(index, data, replaceLength: length);
|
||||
delta = insert(index, data, replaceLength: len);
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
final deleteDelta = delete(index, length);
|
||||
if (len > 0) {
|
||||
final deleteDelta = delete(index, len);
|
||||
delta = delta.compose(deleteDelta);
|
||||
}
|
||||
|
||||
Log.trace('current document $delta');
|
||||
return delta;
|
||||
}
|
||||
|
||||
Delta format(int index, int length, Attribute? attribute) {
|
||||
assert(index >= 0 && length >= 0 && attribute != null);
|
||||
Log.trace('format $length at $index with $attribute');
|
||||
Delta format(int index, int len, Attribute? attribute) {
|
||||
assert(index >= 0 && len >= 0 && attribute != null);
|
||||
|
||||
var delta = Delta();
|
||||
|
||||
final formatDelta = _rules.apply(
|
||||
RuleType.FORMAT,
|
||||
this,
|
||||
index,
|
||||
length: length,
|
||||
attribute: attribute,
|
||||
);
|
||||
final formatDelta = _rules.apply(RuleType.FORMAT, this, index,
|
||||
len: len, attribute: attribute);
|
||||
if (formatDelta.isNotEmpty) {
|
||||
compose(formatDelta, ChangeSource.LOCAL);
|
||||
Log.trace('current document $_delta');
|
||||
delta = delta.compose(formatDelta);
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
Style collectStyle(int index, int length) {
|
||||
/// Only attributes applied to all characters within this range are
|
||||
/// included in the result.
|
||||
Style collectStyle(int index, int len) {
|
||||
final res = queryChild(index);
|
||||
return (res.node as Line).collectStyle(res.offset, length);
|
||||
return (res.node as Line).collectStyle(res.offset, len);
|
||||
}
|
||||
|
||||
/// Returns all styles for any character within the specified text range.
|
||||
List<Style> collectAllStyles(int index, int len) {
|
||||
final res = queryChild(index);
|
||||
return (res.node as Line).collectAllStyles(res.offset, len);
|
||||
}
|
||||
|
||||
ChildQuery queryChild(int offset) {
|
||||
@ -152,10 +142,6 @@ class Document {
|
||||
return block.queryChild(res.offset, true);
|
||||
}
|
||||
|
||||
Tuple2 undo() => _history.undo(this);
|
||||
|
||||
Tuple2 redo() => _history.redo(this);
|
||||
|
||||
void compose(Delta delta, ChangeSource changeSource) {
|
||||
assert(!_observer.isClosed);
|
||||
delta.trim();
|
||||
@ -163,7 +149,7 @@ class Document {
|
||||
|
||||
var offset = 0;
|
||||
delta = _transform(delta);
|
||||
final originDelta = toDelta();
|
||||
final originalDelta = toDelta();
|
||||
for (final op in delta.toList()) {
|
||||
final style =
|
||||
op.attributes != null ? Style.fromJson(op.attributes) : null;
|
||||
@ -180,7 +166,6 @@ class Document {
|
||||
offset += op.length!;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final changeset = delta;
|
||||
|
||||
@ -194,37 +179,55 @@ class Document {
|
||||
if (_delta != _root.toDelta()) {
|
||||
throw 'Compose failed';
|
||||
}
|
||||
final change = Tuple3(originDelta, delta, changeSource);
|
||||
final change = Tuple3(originalDelta, delta, changeSource);
|
||||
_observer.add(change);
|
||||
_history.handleDocChange(change);
|
||||
}
|
||||
|
||||
Tuple2 undo() {
|
||||
return _history.undo(this);
|
||||
}
|
||||
|
||||
Tuple2 redo() {
|
||||
return _history.redo(this);
|
||||
}
|
||||
|
||||
bool get hasUndo => _history.hasUndo;
|
||||
|
||||
bool get hasRedo => _history.hasRedo;
|
||||
|
||||
static Delta _transform(Delta delta) {
|
||||
final res = Delta();
|
||||
final ops = delta.toList();
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
final op = ops[i];
|
||||
res.push(op);
|
||||
_handleImageInsert(i, ops, op, res);
|
||||
_autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'video');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void _handleImageInsert(
|
||||
int i, List<Operation> ops, Operation op, Delta res) {
|
||||
final nextOpIsImage =
|
||||
i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String;
|
||||
if (nextOpIsImage && !(op.data as String).endsWith('\n')) {
|
||||
static void _autoAppendNewlineAfterEmbeddable(
|
||||
int i, List<Operation> ops, Operation op, Delta res, String type) {
|
||||
final nextOpIsEmbed = i + 1 < ops.length &&
|
||||
ops[i + 1].isInsert &&
|
||||
ops[i + 1].data is Map &&
|
||||
(ops[i + 1].data as Map).containsKey(type);
|
||||
if (nextOpIsEmbed &&
|
||||
op.data is String &&
|
||||
(op.data as String).isNotEmpty &&
|
||||
!(op.data as String).endsWith('\n')) {
|
||||
res.push(Operation.insert('\n'));
|
||||
}
|
||||
// Currently embed is equivalent to image and hence `is! String`
|
||||
final opInsertImage = op.isInsert && op.data is! String;
|
||||
// embed could be image or video
|
||||
final opInsertEmbed =
|
||||
op.isInsert && op.data is Map && (op.data as Map).containsKey(type);
|
||||
final nextOpIsLineBreak = i + 1 < ops.length &&
|
||||
ops[i + 1].isInsert &&
|
||||
ops[i + 1].data is String &&
|
||||
(ops[i + 1].data as String).startsWith('\n');
|
||||
if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
|
||||
// automatically append '\n' for image
|
||||
if (opInsertEmbed && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
|
||||
// automatically append '\n' for embeddable
|
||||
res.push(Operation.insert('\n'));
|
||||
}
|
||||
}
|
||||
@ -245,14 +248,20 @@ class Document {
|
||||
_history.clear();
|
||||
}
|
||||
|
||||
void _loadDocument(Delta delta) {
|
||||
assert((delta.last.data as String).endsWith('\n'),
|
||||
'Delta must ends with a line break.');
|
||||
String toPlainText() => _root.children.map((e) => e.toPlainText()).join();
|
||||
|
||||
void _loadDocument(Delta doc) {
|
||||
if (doc.isEmpty) {
|
||||
throw ArgumentError.value(doc, 'Document Delta cannot be empty.');
|
||||
}
|
||||
|
||||
assert((doc.last.data as String).endsWith('\n'));
|
||||
|
||||
var offset = 0;
|
||||
for (final op in delta.toList()) {
|
||||
for (final op in doc.toList()) {
|
||||
if (!op.isInsert) {
|
||||
throw ArgumentError.value(delta,
|
||||
'Document Delta can only contain insert operations but ${op.key} found.');
|
||||
throw ArgumentError.value(doc,
|
||||
'Document can only contain insert operations but ${op.key} found.');
|
||||
}
|
||||
final style =
|
||||
op.attributes != null ? Style.fromJson(op.attributes) : null;
|
||||
@ -282,11 +291,7 @@ class Document {
|
||||
final delta = node.toDelta();
|
||||
return delta.length == 1 &&
|
||||
delta.first.data == '\n' &&
|
||||
delta.first.key == Operation.insertKey;
|
||||
}
|
||||
|
||||
String toPlainText() {
|
||||
return root.children.map((child) => child.toPlainText()).join();
|
||||
delta.first.key == 'insert';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import 'node.dart';
|
||||
/// Represents a group of adjacent [Line]s with the same block style.
|
||||
///
|
||||
/// Block elements are:
|
||||
/// - Quoteblock
|
||||
/// - Blockquote
|
||||
/// - Header
|
||||
/// - Indent
|
||||
/// - List
|
||||
@ -14,14 +14,12 @@ import 'node.dart';
|
||||
/// - Text Direction
|
||||
/// - Code Block
|
||||
class Block extends Container<Line?> {
|
||||
@override
|
||||
Line get defaultChild => Line();
|
||||
|
||||
/// Creates new unmounted [Block].
|
||||
@override
|
||||
Node newInstance() {
|
||||
return Block();
|
||||
}
|
||||
Node newInstance() => Block();
|
||||
|
||||
@override
|
||||
Line get defaultChild => Line();
|
||||
|
||||
@override
|
||||
Delta toDelta() {
|
||||
@ -63,7 +61,7 @@ class Block extends Container<Line?> {
|
||||
@override
|
||||
String toString() {
|
||||
final block = style.attributes.toString();
|
||||
final buffer = StringBuffer(' {$block}\n');
|
||||
final buffer = StringBuffer('§ {$block}\n');
|
||||
for (final child in children) {
|
||||
final tree = child.isLast ? '└' : '├';
|
||||
buffer.write(' $tree $child');
|
||||
|
@ -40,12 +40,6 @@ abstract class Container<T extends Node?> extends Node {
|
||||
/// Always returns fresh instance.
|
||||
T get defaultChild;
|
||||
|
||||
/// Content length of this node's children.
|
||||
///
|
||||
/// To get number of children in this node use [childCount].
|
||||
@override
|
||||
int get length => _children.fold(0, (curr, node) => curr + node.length);
|
||||
|
||||
/// Adds [node] to the end of this container children list.
|
||||
void add(T node) {
|
||||
assert(node?.parent == null);
|
||||
@ -69,7 +63,9 @@ abstract class Container<T extends Node?> extends Node {
|
||||
|
||||
/// Moves children of this node to [newParent].
|
||||
void moveChildToNewParent(Container? newParent) {
|
||||
if (isEmpty) return;
|
||||
if (isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final last = newParent!.isEmpty ? null : newParent.last as T?;
|
||||
while (isNotEmpty) {
|
||||
@ -83,7 +79,7 @@ abstract class Container<T extends Node?> extends Node {
|
||||
if (last != null) last.adjust();
|
||||
}
|
||||
|
||||
/// Queries the child [Node] at specified character [offset] in this container.
|
||||
/// Queries the child [Node] at [offset] in this container.
|
||||
///
|
||||
/// The result may contain the found node or `null` if no node is found
|
||||
/// at specified offset.
|
||||
@ -96,17 +92,25 @@ abstract class Container<T extends Node?> extends Node {
|
||||
return ChildQuery(null, 0);
|
||||
}
|
||||
|
||||
for (final child in children) {
|
||||
final childLen = child.length;
|
||||
if (offset < childLen ||
|
||||
(inclusive && offset == childLen && (child.isLast))) {
|
||||
return ChildQuery(child, offset);
|
||||
for (final node in children) {
|
||||
final len = node.length;
|
||||
if (offset < len || (inclusive && offset == len && node.isLast)) {
|
||||
return ChildQuery(node, offset);
|
||||
}
|
||||
offset -= childLen;
|
||||
offset -= len;
|
||||
}
|
||||
return ChildQuery(null, 0);
|
||||
}
|
||||
|
||||
@override
|
||||
String toPlainText() => children.map((child) => child.toPlainText()).join();
|
||||
|
||||
/// Content length of this node's children.
|
||||
///
|
||||
/// To get number of children in this node use [childCount].
|
||||
@override
|
||||
int get length => _children.fold(0, (cur, node) => cur + node.length);
|
||||
|
||||
@override
|
||||
void insert(int index, Object data, Style? style) {
|
||||
assert(index == 0 || (index > 0 && index < length));
|
||||
@ -138,9 +142,6 @@ abstract class Container<T extends Node?> extends Node {
|
||||
child.node!.delete(child.offset, length);
|
||||
}
|
||||
|
||||
@override
|
||||
String toPlainText() => children.map((child) => child.toPlainText()).join();
|
||||
|
||||
@override
|
||||
String toString() => _children.join('\n');
|
||||
}
|
||||
|
@ -4,21 +4,25 @@
|
||||
///
|
||||
/// * [BlockEmbed] which represents a block embed.
|
||||
class Embeddable {
|
||||
Map<String, dynamic> toJson() => <String, String>{type: data};
|
||||
|
||||
static Embeddable fromJson(Map<String, dynamic> json) {
|
||||
final mp = Map<String, dynamic>.from(json);
|
||||
assert(mp.length == 1, 'Embeddable map has one key');
|
||||
return BlockEmbed(mp.keys.first, mp.values.first);
|
||||
}
|
||||
const Embeddable(this.type, this.data);
|
||||
|
||||
/// The type of this object.
|
||||
final String type;
|
||||
|
||||
/// The data payload of this object
|
||||
/// The data payload of this object.
|
||||
final dynamic data;
|
||||
|
||||
Embeddable(this.type, this.data);
|
||||
Map<String, dynamic> toJson() {
|
||||
final m = <String, String>{type: data};
|
||||
return m;
|
||||
}
|
||||
|
||||
static Embeddable fromJson(Map<String, dynamic> json) {
|
||||
final m = Map<String, dynamic>.from(json);
|
||||
assert(m.length == 1, 'Embeddable map has one key');
|
||||
|
||||
return BlockEmbed(m.keys.first, m.values.first);
|
||||
}
|
||||
}
|
||||
|
||||
/// An object which occupies an entire line in a document and cannot co-exist
|
||||
@ -28,9 +32,14 @@ class Embeddable {
|
||||
/// the document model itself does not make any assumptions about the types
|
||||
/// of embedded objects and allows users to define their own types.
|
||||
class BlockEmbed extends Embeddable {
|
||||
BlockEmbed(String type, String data) : super(type, data);
|
||||
const BlockEmbed(String type, String data) : super(type, data);
|
||||
|
||||
static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
|
||||
static const String horizontalRuleType = 'divider';
|
||||
static BlockEmbed horizontalRule = const BlockEmbed(horizontalRuleType, 'hr');
|
||||
|
||||
static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
|
||||
static const String imageType = 'image';
|
||||
static BlockEmbed image(String imageUrl) => BlockEmbed(imageType, imageUrl);
|
||||
|
||||
static const String videoType = 'video';
|
||||
static BlockEmbed video(String videoUrl) => BlockEmbed(videoType, videoUrl);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'embed.dart';
|
||||
import 'line.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// A leaf in Quill document tree.
|
||||
abstract class Leaf extends Node {
|
||||
/// Creates a new [Leaf] with specified [data].
|
||||
factory Leaf(Object data) {
|
||||
@ -194,8 +193,6 @@ abstract class Leaf extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------- Impl ---------------------------------- */
|
||||
|
||||
/// A span of formatted text within a line in a Quill document.
|
||||
///
|
||||
/// Text is a leaf node of a document tree.
|
||||
@ -213,7 +210,7 @@ class Text extends Leaf {
|
||||
super.val(text);
|
||||
|
||||
@override
|
||||
Node newInstance() => Text();
|
||||
Node newInstance() => Text(value);
|
||||
|
||||
@override
|
||||
String get value => _value as String;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import '../../quill_delta.dart';
|
||||
import '../attribute.dart';
|
||||
import '../style.dart';
|
||||
@ -24,10 +24,7 @@ class Line extends Container<Leaf?> {
|
||||
|
||||
/// Returns `true` if this line contains an embedded object.
|
||||
bool get hasEmbed {
|
||||
if (childCount != 1) {
|
||||
return false;
|
||||
}
|
||||
return children.single is Embed;
|
||||
return children.any((child) => child is Embed);
|
||||
}
|
||||
|
||||
/// Returns next [Line] or `null` if this is the last line in the document.
|
||||
@ -202,23 +199,44 @@ class Line extends Container<Leaf?> {
|
||||
} // No block-level changes
|
||||
|
||||
if (parent is Block) {
|
||||
final parentStyle = (parent as Block).style.getBlockExceptHeader();
|
||||
if (blockStyle.value == null) {
|
||||
final parentStyle = (parent as Block).style.getBlocksExceptHeader();
|
||||
// Ensure that we're only unwrapping the block only if we unset a single
|
||||
// block format in the `parentStyle` and there are no more block formats
|
||||
// left to unset.
|
||||
if (blockStyle.value == null &&
|
||||
parentStyle.containsKey(blockStyle.key) &&
|
||||
parentStyle.length == 1) {
|
||||
_unwrap();
|
||||
} else if (blockStyle != parentStyle) {
|
||||
} else if (!const MapEquality()
|
||||
.equals(newStyle.getBlocksExceptHeader(), parentStyle)) {
|
||||
_unwrap();
|
||||
final block = Block()..applyAttribute(blockStyle);
|
||||
_wrap(block);
|
||||
block.adjust();
|
||||
// Block style now can contain multiple attributes
|
||||
if (newStyle.attributes.keys
|
||||
.any(Attribute.exclusiveBlockKeys.contains)) {
|
||||
parentStyle.removeWhere(
|
||||
(key, attr) => Attribute.exclusiveBlockKeys.contains(key));
|
||||
}
|
||||
parentStyle.removeWhere(
|
||||
(key, attr) => newStyle?.attributes.keys.contains(key) ?? false);
|
||||
final parentStyleToMerge = Style.attr(parentStyle);
|
||||
newStyle = newStyle.mergeAll(parentStyleToMerge);
|
||||
_applyBlockStyles(newStyle);
|
||||
} // else the same style, no-op.
|
||||
} else if (blockStyle.value != null) {
|
||||
// Only wrap with a new block if this is not an unset
|
||||
final block = Block()..applyAttribute(blockStyle);
|
||||
_wrap(block);
|
||||
block.adjust();
|
||||
_applyBlockStyles(newStyle);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBlockStyles(Style newStyle) {
|
||||
var block = Block();
|
||||
for (final style in newStyle.getBlocksExceptHeader().values) {
|
||||
block = block..applyAttribute(style);
|
||||
}
|
||||
_wrap(block);
|
||||
block.adjust();
|
||||
}
|
||||
|
||||
/// Wraps this line with new parent [block].
|
||||
///
|
||||
/// This line can not be in a [Block] when this method is called.
|
||||
@ -359,4 +377,36 @@ class Line extends Container<Leaf?> {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns all styles for any character within the specified text range.
|
||||
List<Style> collectAllStyles(int offset, int len) {
|
||||
final local = math.min(length - offset, len);
|
||||
final result = <Style>[];
|
||||
|
||||
final data = queryChild(offset, true);
|
||||
var node = data.node as Leaf?;
|
||||
if (node != null) {
|
||||
result.add(node.style);
|
||||
var pos = node.length - data.offset;
|
||||
while (!node!.isLast && pos < local) {
|
||||
node = node.next as Leaf?;
|
||||
result.add(node!.style);
|
||||
pos += node.length;
|
||||
}
|
||||
}
|
||||
|
||||
result.add(style);
|
||||
if (parent is Block) {
|
||||
final block = parent as Block;
|
||||
result.add(block.style);
|
||||
}
|
||||
|
||||
final remaining = len - local;
|
||||
if (remaining > 0) {
|
||||
final rest = nextLine!.collectAllStyles(0, remaining);
|
||||
result.addAll(rest);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -38,19 +38,24 @@ abstract class Node extends LinkedListEntry<Node> {
|
||||
/// To get offset of this node in the document see [documentOffset].
|
||||
int get offset {
|
||||
var offset = 0;
|
||||
|
||||
if (list == null || isFirst) {
|
||||
return offset;
|
||||
}
|
||||
var curr = this;
|
||||
|
||||
var cur = this;
|
||||
do {
|
||||
curr = curr.previous!;
|
||||
offset += curr.length;
|
||||
} while (!curr.isFirst);
|
||||
cur = cur.previous!;
|
||||
offset += cur.length;
|
||||
} while (!cur.isFirst);
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// Offset in characters of this node in the document.
|
||||
int get documentOffset {
|
||||
if (parent == null) {
|
||||
return offset;
|
||||
}
|
||||
final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
|
||||
return parentOffset + offset;
|
||||
}
|
||||
@ -58,16 +63,16 @@ abstract class Node extends LinkedListEntry<Node> {
|
||||
/// Returns `true` if this node contains character at specified [offset] in
|
||||
/// the document.
|
||||
bool containsOffset(int offset) {
|
||||
final docOffset = documentOffset;
|
||||
return docOffset <= offset && offset < docOffset + length;
|
||||
final o = documentOffset;
|
||||
return o <= offset && offset < o + length;
|
||||
}
|
||||
|
||||
void applyAttribute(Attribute attribute) {
|
||||
_style = _style.merge(attribute);
|
||||
}
|
||||
|
||||
void applyStyle(Style otherStyle) {
|
||||
_style = _style.mergeAll(otherStyle);
|
||||
void applyStyle(Style value) {
|
||||
_style = _style.mergeAll(value);
|
||||
}
|
||||
|
||||
void clearStyle() {
|
||||
@ -97,7 +102,7 @@ abstract class Node extends LinkedListEntry<Node> {
|
||||
|
||||
void adjust() {/* no-op */}
|
||||
|
||||
// Subclass overridden method
|
||||
/// abstract methods begin
|
||||
|
||||
Node newInstance();
|
||||
|
||||
@ -107,9 +112,11 @@ abstract class Node extends LinkedListEntry<Node> {
|
||||
|
||||
void insert(int index, Object data, Style? style);
|
||||
|
||||
void retain(int index, int? length, Style? style);
|
||||
void retain(int index, int? len, Style? style);
|
||||
|
||||
void delete(int index, int? length);
|
||||
void delete(int index, int? len);
|
||||
|
||||
/// abstract methods end
|
||||
}
|
||||
|
||||
/// Root node of document tree.
|
||||
|
@ -3,6 +3,7 @@ import 'package:quiver/core.dart';
|
||||
|
||||
import 'attribute.dart';
|
||||
|
||||
/* Collection of style attributes */
|
||||
class Style {
|
||||
Style() : _attributes = <String, Attribute>{};
|
||||
|
||||
@ -14,49 +15,63 @@ class Style {
|
||||
if (attributes == null) {
|
||||
return Style();
|
||||
}
|
||||
final result = attributes.map((key, value) {
|
||||
|
||||
final result = attributes.map((key, dynamic value) {
|
||||
final attr = Attribute.fromKeyValue(key, value);
|
||||
return MapEntry<String, Attribute>(key, attr);
|
||||
return MapEntry<String, Attribute>(
|
||||
key, attr ?? Attribute(key, AttributeScope.IGNORE, value));
|
||||
});
|
||||
return Style.attr(result);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? toJson() => _attributes.isEmpty
|
||||
? null
|
||||
: _attributes.map<String, dynamic>((_, attr) {
|
||||
return MapEntry<String, dynamic>(attr.key, attr.value);
|
||||
});
|
||||
|
||||
// Properties
|
||||
|
||||
Map<String, Attribute> get attributes => _attributes;
|
||||
: _attributes.map<String, dynamic>((_, attribute) =>
|
||||
MapEntry<String, dynamic>(attribute.key, attribute.value));
|
||||
|
||||
Iterable<String> get keys => _attributes.keys;
|
||||
|
||||
Iterable<Attribute> get values => _attributes.values;
|
||||
Iterable<Attribute> get values => _attributes.values.sorted(
|
||||
(a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b));
|
||||
|
||||
Map<String, Attribute> get attributes => _attributes;
|
||||
|
||||
bool get isEmpty => _attributes.isEmpty;
|
||||
|
||||
bool get isNotEmpty => _attributes.isNotEmpty;
|
||||
|
||||
bool get isInline => isNotEmpty && values.every((ele) => ele.isInline);
|
||||
bool get isInline => isNotEmpty && values.every((item) => item.isInline);
|
||||
|
||||
bool get isIgnored => isNotEmpty && values.every((ele) => ele.isIgnored);
|
||||
bool get isIgnored =>
|
||||
isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE);
|
||||
|
||||
Attribute get single => values.single;
|
||||
Attribute get single => _attributes.values.single;
|
||||
|
||||
bool containsKey(String key) => _attributes.containsKey(key);
|
||||
|
||||
Attribute? getBlockExceptHeader() {
|
||||
for (final value in values) {
|
||||
if (value.isBlockExceptHeader) {
|
||||
return value;
|
||||
for (final val in values) {
|
||||
if (val.isBlockExceptHeader && val.value != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
for (final val in values) {
|
||||
if (val.isBlockExceptHeader) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Operators
|
||||
Map<String, Attribute> getBlocksExceptHeader() {
|
||||
final m = <String, Attribute>{};
|
||||
attributes.forEach((key, value) {
|
||||
if (Attribute.blockKeysExceptHeader.contains(key)) {
|
||||
m[key] = value;
|
||||
}
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
Style merge(Attribute attribute) {
|
||||
final merged = Map<String, Attribute>.from(_attributes);
|
||||
@ -70,22 +85,22 @@ class Style {
|
||||
|
||||
Style mergeAll(Style other) {
|
||||
var result = Style.attr(_attributes);
|
||||
other.values.forEach((attr) {
|
||||
result = result.merge(attr);
|
||||
});
|
||||
for (final attribute in other.values) {
|
||||
result = result.merge(attribute);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Style removeAll(Set<Attribute> attributes) {
|
||||
final merged = Map<String, Attribute>.from(_attributes);
|
||||
attributes.map((ele) => ele.key).forEach(merged.remove);
|
||||
attributes.map((item) => item.key).forEach(merged.remove);
|
||||
return Style.attr(merged);
|
||||
}
|
||||
|
||||
Style put(Attribute attribute) {
|
||||
final merged = Map<String, Attribute>.from(_attributes);
|
||||
merged[attribute.key] = attribute;
|
||||
return Style.attr(merged);
|
||||
final m = Map<String, Attribute>.from(attributes);
|
||||
m[attribute.key] = attribute;
|
||||
return Style.attr(m);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -9,8 +9,8 @@ abstract class DeleteRule extends Rule {
|
||||
RuleType get type => RuleType.DELETE;
|
||||
|
||||
@override
|
||||
void validateArgs(int? length, Object? data, Attribute? attribute) {
|
||||
assert(length != null);
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute) {
|
||||
assert(len != null);
|
||||
assert(data == null);
|
||||
assert(attribute == null);
|
||||
}
|
||||
@ -21,10 +21,10 @@ class CatchAllDeleteRule extends DeleteRule {
|
||||
|
||||
@override
|
||||
Delta applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
return Delta()
|
||||
..retain(index)
|
||||
..delete(length!);
|
||||
..delete(len!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,9 +33,9 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
final it = DeltaIterator(document)..skip(index);
|
||||
var op = it.next(1);
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
var op = itr.next(1);
|
||||
if (op.data != '\n') {
|
||||
return null;
|
||||
}
|
||||
@ -43,13 +43,13 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
|
||||
final isNotPlain = op.isNotPlain;
|
||||
final attrs = op.attributes;
|
||||
|
||||
it.skip(length! - 1);
|
||||
itr.skip(len! - 1);
|
||||
final delta = Delta()
|
||||
..retain(index)
|
||||
..delete(length);
|
||||
..delete(len);
|
||||
|
||||
while (it.hasNext) {
|
||||
op = it.next();
|
||||
while (itr.hasNext) {
|
||||
op = itr.next();
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
final lineBreak = text.indexOf('\n');
|
||||
if (lineBreak == -1) {
|
||||
@ -66,7 +66,9 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
|
||||
attributes ??= <String, dynamic>{};
|
||||
attributes.addAll(attrs!);
|
||||
}
|
||||
delta..retain(lineBreak)..retain(1, attributes);
|
||||
delta
|
||||
..retain(lineBreak)
|
||||
..retain(1, attributes);
|
||||
break;
|
||||
}
|
||||
return delta;
|
||||
@ -78,23 +80,23 @@ class EnsureEmbedLineRule extends DeleteRule {
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
final it = DeltaIterator(document);
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final itr = DeltaIterator(document);
|
||||
|
||||
var op = it.skip(index);
|
||||
int? indexDelta = 0, lengthDelta = 0, remain = length;
|
||||
var op = itr.skip(index);
|
||||
int? indexDelta = 0, lengthDelta = 0, remain = len;
|
||||
var embedFound = op != null && op.data is! String;
|
||||
final hasLineBreakBefore =
|
||||
!embedFound && (op == null || (op.data as String).endsWith('\n'));
|
||||
if (embedFound) {
|
||||
var candidate = it.next(1);
|
||||
var candidate = itr.next(1);
|
||||
if (remain != null) {
|
||||
remain--;
|
||||
if (candidate.data == '\n') {
|
||||
indexDelta++;
|
||||
lengthDelta--;
|
||||
|
||||
candidate = it.next(1);
|
||||
candidate = itr.next(1);
|
||||
remain--;
|
||||
if (candidate.data == '\n') {
|
||||
lengthDelta++;
|
||||
@ -103,10 +105,10 @@ class EnsureEmbedLineRule extends DeleteRule {
|
||||
}
|
||||
}
|
||||
|
||||
op = it.skip(remain!);
|
||||
op = itr.skip(remain!);
|
||||
if (op != null &&
|
||||
(op.data is String ? op.data as String? : '')!.endsWith('\n')) {
|
||||
final candidate = it.next(1);
|
||||
final candidate = itr.next(1);
|
||||
if (candidate.data is! String && !hasLineBreakBefore) {
|
||||
embedFound = true;
|
||||
lengthDelta--;
|
||||
@ -119,6 +121,6 @@ class EnsureEmbedLineRule extends DeleteRule {
|
||||
|
||||
return Delta()
|
||||
..retain(index + indexDelta)
|
||||
..delete(length! + lengthDelta);
|
||||
..delete(len! + lengthDelta);
|
||||
}
|
||||
}
|
||||
|
@ -9,30 +9,28 @@ abstract class FormatRule extends Rule {
|
||||
RuleType get type => RuleType.FORMAT;
|
||||
|
||||
@override
|
||||
void validateArgs(int? length, Object? data, Attribute? attribute) {
|
||||
assert(length != null);
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute) {
|
||||
assert(len != null);
|
||||
assert(data == null);
|
||||
assert(attribute != null);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------- Rule Impl ------------------------------- */
|
||||
|
||||
class ResolveLineFormatRule extends FormatRule {
|
||||
const ResolveLineFormatRule();
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.scope != AttributeScope.BLOCK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var delta = Delta()..retain(index);
|
||||
final it = DeltaIterator(document)..skip(index);
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
Operation op;
|
||||
for (var cur = 0; cur < length! && it.hasNext; cur += op.length!) {
|
||||
op = it.next(length - cur);
|
||||
for (var 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;
|
||||
@ -41,29 +39,50 @@ class ResolveLineFormatRule extends FormatRule {
|
||||
final tmp = Delta();
|
||||
var offset = 0;
|
||||
|
||||
// Enforce Block Format exclusivity by rule
|
||||
final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key)
|
||||
? op.attributes?.keys
|
||||
.where((key) =>
|
||||
Attribute.exclusiveBlockKeys.contains(key) &&
|
||||
attribute.key != key &&
|
||||
attribute.value != null)
|
||||
.map((key) => MapEntry<String, dynamic>(key, null)) ??
|
||||
[]
|
||||
: <MapEntry<String, dynamic>>[];
|
||||
|
||||
for (var lineBreak = text.indexOf('\n');
|
||||
lineBreak >= 0;
|
||||
lineBreak = text.indexOf('\n', offset)) {
|
||||
tmp
|
||||
..retain(lineBreak - offset)
|
||||
..retain(1, attribute.toJson());
|
||||
..retain(1, attribute.toJson()..addEntries(removedBlocks));
|
||||
offset = lineBreak + 1;
|
||||
}
|
||||
tmp.retain(text.length - offset);
|
||||
delta = delta.concat(tmp);
|
||||
}
|
||||
|
||||
while (it.hasNext) {
|
||||
op = it.next();
|
||||
while (itr.hasNext) {
|
||||
op = itr.next();
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
final lineBreak = text.indexOf('\n');
|
||||
if (lineBreak < 0) {
|
||||
delta.retain(op.length!);
|
||||
continue;
|
||||
}
|
||||
// Enforce Block Format exclusivity by rule
|
||||
final removedBlocks = Attribute.exclusiveBlockKeys.contains(attribute.key)
|
||||
? op.attributes?.keys
|
||||
.where((key) =>
|
||||
Attribute.exclusiveBlockKeys.contains(key) &&
|
||||
attribute.key != key &&
|
||||
attribute.value != null)
|
||||
.map((key) => MapEntry<String, dynamic>(key, null)) ??
|
||||
[]
|
||||
: <MapEntry<String, dynamic>>[];
|
||||
delta
|
||||
..retain(lineBreak)
|
||||
..retain(1, attribute.toJson());
|
||||
..retain(1, attribute.toJson()..addEntries(removedBlocks));
|
||||
break;
|
||||
}
|
||||
return delta;
|
||||
@ -75,14 +94,14 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.key != Attribute.link.key || length! > 0) {
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.key != Attribute.link.key || len! > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final delta = Delta();
|
||||
final it = DeltaIterator(document);
|
||||
final before = it.skip(index), after = it.next();
|
||||
final itr = DeltaIterator(document);
|
||||
final before = itr.skip(index), after = itr.next();
|
||||
int? beg = index, retain = 0;
|
||||
if (before != null && before.hasAttribute(attribute.key)) {
|
||||
beg -= before.length!;
|
||||
@ -107,17 +126,17 @@ class ResolveInlineFormatRule extends FormatRule {
|
||||
|
||||
@override
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
if (attribute!.scope != AttributeScope.INLINE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final delta = Delta()..retain(index);
|
||||
final it = DeltaIterator(document)..skip(index);
|
||||
final itr = DeltaIterator(document)..skip(index);
|
||||
|
||||
Operation op;
|
||||
for (var cur = 0; cur < length! && it.hasNext; cur += op.length!) {
|
||||
op = it.next(length - cur);
|
||||
for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
|
||||
op = itr.next(len - cur);
|
||||
final text = op.data is String ? (op.data as String?)! : '';
|
||||
var lineBreak = text.indexOf('\n');
|
||||
if (lineBreak < 0) {
|
||||
|
@ -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!) {
|
||||
|
@ -6,46 +6,37 @@ import 'delete.dart';
|
||||
import 'format.dart';
|
||||
import 'package:flowy_log/flowy_log.dart';
|
||||
|
||||
enum RuleType {
|
||||
INSERT,
|
||||
DELETE,
|
||||
FORMAT,
|
||||
}
|
||||
enum RuleType { INSERT, DELETE, FORMAT }
|
||||
|
||||
abstract class Rule {
|
||||
const Rule();
|
||||
|
||||
RuleType get type;
|
||||
|
||||
Delta? apply(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
validateArgs(length, data, attribute);
|
||||
return applyRule(
|
||||
document,
|
||||
index,
|
||||
length: length,
|
||||
data: data,
|
||||
attribute: attribute,
|
||||
);
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
validateArgs(len, data, attribute);
|
||||
return applyRule(document, index,
|
||||
len: len, data: data, attribute: attribute);
|
||||
}
|
||||
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? length, Object? data, Attribute? attribute});
|
||||
void validateArgs(int? len, Object? data, Attribute? attribute);
|
||||
|
||||
void validateArgs(int? length, Object? data, Attribute? attribute);
|
||||
Delta? applyRule(Delta document, int index,
|
||||
{int? len, Object? data, Attribute? attribute});
|
||||
|
||||
RuleType get type;
|
||||
}
|
||||
|
||||
class Rules {
|
||||
Rules(this._rules);
|
||||
|
||||
final List<Rule> _rules;
|
||||
List<Rule> _customRules = [];
|
||||
|
||||
final List<Rule> _rules;
|
||||
static final Rules _instance = Rules([
|
||||
const FormatLinkAtCaretPositionRule(),
|
||||
const ResolveLineFormatRule(),
|
||||
const ResolveInlineFormatRule(),
|
||||
const InsertEmbedsRule(),
|
||||
// const ForceNewlineForInsertsAroundEmbedRule(),
|
||||
const AutoExitBlockRule(),
|
||||
const PreserveBlockStyleOnInsertRule(),
|
||||
const PreserveLineStyleOnSplitRule(),
|
||||
@ -53,23 +44,27 @@ class Rules {
|
||||
const AutoFormatLinksRule(),
|
||||
const PreserveInlineStylesRule(),
|
||||
const CatchAllInsertRule(),
|
||||
// const EnsureEmbedLineRule(),
|
||||
const EnsureEmbedLineRule(),
|
||||
const PreserveLineStyleOnMergeRule(),
|
||||
const CatchAllDeleteRule(),
|
||||
]);
|
||||
|
||||
static Rules getInstance() => _instance;
|
||||
|
||||
void setCustomRules(List<Rule> customRules) {
|
||||
_customRules = customRules;
|
||||
}
|
||||
|
||||
Delta apply(RuleType ruleType, Document document, int index,
|
||||
{int? length, Object? data, Attribute? attribute}) {
|
||||
{int? len, Object? data, Attribute? attribute}) {
|
||||
final delta = document.toDelta();
|
||||
for (final rule in _rules) {
|
||||
for (final rule in _customRules + _rules) {
|
||||
if (rule.type != ruleType) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final result = rule.apply(delta, index,
|
||||
length: length, data: data, attribute: attribute);
|
||||
len: len, data: data, attribute: attribute);
|
||||
if (result != null) {
|
||||
Log.trace('apply rule: $rule, result: $result');
|
||||
return result..trim();
|
||||
|
@ -529,9 +529,6 @@ class _HeaderStyleButtonState extends State<HeaderStyleButton> {
|
||||
Attribute.h1: 'H1',
|
||||
Attribute.h2: 'H2',
|
||||
Attribute.h3: 'H3',
|
||||
Attribute.h4: 'H4',
|
||||
Attribute.h5: 'H5',
|
||||
Attribute.h6: 'H6',
|
||||
};
|
||||
final headerStyles = headerTextMapping.keys.toList(growable: false);
|
||||
final headerTexts = headerTextMapping.values.toList(growable: false);
|
||||
|
@ -97,9 +97,6 @@ class TextLine extends StatelessWidget {
|
||||
Attribute.h1: defaultStyles.h1!.style,
|
||||
Attribute.h2: defaultStyles.h2!.style,
|
||||
Attribute.h3: defaultStyles.h3!.style,
|
||||
Attribute.h4: defaultStyles.h4!.style,
|
||||
Attribute.h5: defaultStyles.h5!.style,
|
||||
Attribute.h6: defaultStyles.h6!.style,
|
||||
};
|
||||
textStyle =
|
||||
textStyle.merge(headerStyles[header] ?? defaultStyles.paragraph!.style);
|
||||
|
Loading…
Reference in New Issue
Block a user