update flowy_editor models

This commit is contained in:
appflowy 2021-09-15 22:57:29 +08:00
parent e4db7222f8
commit 2a42fd108c
15 changed files with 522 additions and 422 deletions

View File

@ -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);
}

View File

@ -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';
}
}

View File

@ -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');

View File

@ -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');
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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!) {

View File

@ -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();

View File

@ -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);

View File

@ -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);