diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart index 92874a2b85..484af2a4c3 100644 --- a/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart +++ b/app_flowy/packages/flowy_editor/lib/src/model/document/attribute.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:quiver/core.dart'; enum AttributeScope { @@ -14,9 +16,10 @@ class Attribute { final AttributeScope scope; final T value; - static final Map _registry = { + static final Map _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 { 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 { static final TokenAttribute token = TokenAttribute(''); - static Attribute get h1 => HeaderAttribute(level: 1); - - static Attribute get h2 => HeaderAttribute(level: 2); - - static Attribute get h3 => HeaderAttribute(level: 3); - - static Attribute get h4 => HeaderAttribute(level: 4); - - static Attribute get h5 => HeaderAttribute(level: 5); - - static Attribute get h6 => HeaderAttribute(level: 6); - - static Attribute get leftAlignment => AlignAttribute('left'); - - static Attribute get centerAlignment => AlignAttribute('center'); - - static Attribute get rightAlignment => AlignAttribute('right'); - - static Attribute get justifyAlignment => AlignAttribute('justify'); - - static Attribute get bullet => ListAttribute('bullet'); - - static Attribute get ordered => ListAttribute('ordered'); - - static Attribute get checked => ListAttribute('checked'); - - static Attribute get unchecked => ListAttribute('unchecked'); - - static Attribute get indentL1 => IndentAttribute(level: 1); - - static Attribute get indentL2 => IndentAttribute(level: 2); - - static Attribute get indentL3 => IndentAttribute(level: 3); - - static Attribute get indentL4 => IndentAttribute(level: 4); - - static Attribute get indentL5 => IndentAttribute(level: 5); - - static Attribute get indentL6 => IndentAttribute(level: 6); - - static Attribute 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 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 { Attribute.placeholder.key, }; - static final Set blockKeys = { + static final Set 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 blockKeysExceptHeader = blockKeys - ..remove(Attribute.header.key); + static final Set blockKeysExceptHeader = LinkedHashSet.of({ + Attribute.list.key, + Attribute.align.key, + Attribute.codeBlock.key, + Attribute.quoteBlock.key, + Attribute.indent.key, + }); - // Utils + static final Set exclusiveBlockKeys = LinkedHashSet.of({ + Attribute.header.key, + Attribute.list.key, + Attribute.codeBlock.key, + Attribute.quoteBlock.key, + }); - bool get isInline => AttributeScope.INLINE == scope; + static Attribute get h1 => HeaderAttribute(level: 1); - bool get isIgnored => AttributeScope.IGNORE == scope; + static Attribute get h2 => HeaderAttribute(level: 2); + + static Attribute get h3 => HeaderAttribute(level: 3); + + // "attributes":{"align":"left"} + static Attribute get leftAlignment => AlignAttribute('left'); + + // "attributes":{"align":"center"} + static Attribute get centerAlignment => AlignAttribute('center'); + + // "attributes":{"align":"right"} + static Attribute get rightAlignment => AlignAttribute('right'); + + // "attributes":{"align":"justify"} + static Attribute get justifyAlignment => AlignAttribute('justify'); + + // "attributes":{"list":"bullet"} + static Attribute get bullet => ListAttribute('bullet'); + + // "attributes":{"list":"ordered"} + static Attribute get ordered => ListAttribute('ordered'); + + // "attributes":{"list":"checked"} + static Attribute get checked => ListAttribute('checked'); + + // "attributes":{"list":"unchecked"} + static Attribute get unchecked => ListAttribute('unchecked'); + + // "attributes":{"indent":1"} + static Attribute get indentL1 => IndentAttribute(level: 1); + + // "attributes":{"indent":2"} + static Attribute get indentL2 => IndentAttribute(level: 2); + + // "attributes":{"indent":3"} + static Attribute get indentL3 => IndentAttribute(level: 3); + + static Attribute 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 toJson() => {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 { @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is! Attribute) return false; + if (other is! Attribute) return false; final typedOther = other; return key == typedOther.key && scope == typedOther.scope && @@ -202,12 +220,6 @@ class Attribute { } } -/* -------------------------------------------------------------------------- */ -/* Attributes Impl */ -/* -------------------------------------------------------------------------- */ - -/* --------------------------------- INLINE --------------------------------- */ - class BoldAttribute extends Attribute { BoldAttribute() : super('bold', AttributeScope.INLINE, true); } @@ -216,42 +228,44 @@ class ItalicAttribute extends Attribute { ItalicAttribute() : super('italic', AttributeScope.INLINE, true); } +class SmallAttribute extends Attribute { + SmallAttribute() : super('small', AttributeScope.INLINE, true); +} + class UnderlineAttribute extends Attribute { UnderlineAttribute() : super('underline', AttributeScope.INLINE, true); } class StrikeThroughAttribute extends Attribute { - StrikeThroughAttribute() - : super('strikethrough', AttributeScope.INLINE, true); + StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); } class FontAttribute extends Attribute { - FontAttribute(String? value) : super('font', AttributeScope.INLINE, value); + FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); } class SizeAttribute extends Attribute { - SizeAttribute(String? value) : super('size', AttributeScope.INLINE, value); + SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val); } class LinkAttribute extends Attribute { - LinkAttribute(String? value) : super('link', AttributeScope.INLINE, value); + LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val); } class ColorAttribute extends Attribute { - ColorAttribute(String? value) : super('color', AttributeScope.INLINE, value); + ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val); } class BackgroundAttribute extends Attribute { - BackgroundAttribute(String? value) - : super('background', AttributeScope.INLINE, value); + BackgroundAttribute(String? val) + : super('background', AttributeScope.INLINE, val); } -class PlaceholderAttribute extends Attribute { +/// This is custom attribute for hint +class PlaceholderAttribute extends Attribute { PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true); } -/* ---------------------------------- BLOCK --------------------------------- */ - class HeaderAttribute extends Attribute { HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level); } @@ -261,36 +275,33 @@ class IndentAttribute extends Attribute { } class AlignAttribute extends Attribute { - AlignAttribute(String? value) : super('align', AttributeScope.BLOCK, value); + AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val); } class ListAttribute extends Attribute { - ListAttribute(String? value) : super('list', AttributeScope.BLOCK, value); + ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val); } -class CodeBlockAttribute extends Attribute { - CodeBlockAttribute() : super('code_block', AttributeScope.BLOCK, true); +class CodeBlockAttribute extends Attribute { + CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true); } class QuoteBlockAttribute extends Attribute { QuoteBlockAttribute() : super('quote_block', AttributeScope.BLOCK, true); } -/* --------------------------------- IGNORE --------------------------------- */ - class WidthAttribute extends Attribute { - WidthAttribute(String? value) : super('width', AttributeScope.IGNORE, value); + WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val); } class HeightAttribute extends Attribute { - HeightAttribute(String? value) - : super('height', AttributeScope.IGNORE, value); + HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val); } class StyleAttribute extends Attribute { - StyleAttribute(String? value) : super('style', AttributeScope.IGNORE, value); + StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val); } -class TokenAttribute extends Attribute { - TokenAttribute(String? value) : super('token', AttributeScope.IGNORE, value); +class TokenAttribute extends Attribute { + TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); } diff --git a/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart b/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart index dd9bb625bb..221bd41483 100644 --- a/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart +++ b/app_flowy/packages/flowy_editor/lib/src/model/document/document.dart @@ -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 customRules) { + _rules.setCustomRules(customRules); + } + final StreamController> _observer = StreamController.broadcast(); @@ -54,12 +57,7 @@ class Document { Stream> 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