diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md
index ff759a4b..077b59c0 100644
--- a/.github/ISSUE_TEMPLATE/issue-template.md
+++ b/.github/ISSUE_TEMPLATE/issue-template.md
@@ -13,4 +13,4 @@ My issue is about [Desktop]
I have tried running `example` directory successfully before creating an issue here.
-Please note that we are using stable channel. If you are using beta or master channel, those are not supported.
+Please note that we are using stable channel on branch master. If you are using beta or master channel, use branch dev.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d96c8386..601b3bf2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+## [1.3.3]
+* Upgrade file_picker version.
+
+## [1.3.2]
+* Fix copy/paste bug.
+
+## [1.3.1]
+* New logo.
+
+## [1.3.0]
+* Support flutter 2.2.0.
+
+## [1.2.2]
+* Checkbox supports tapping.
+
## [1.2.1]
* Indented position not holding while editing.
diff --git a/README.md b/README.md
index b7c1a247..bbedb500 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,35 @@
+
+
+
+A rich text editor for Flutter
+
+[![MIT License][license-badge]][license-link]
+[![PRs Welcome][prs-badge]][prs-link]
+[![Watch on GitHub][github-watch-badge]][github-watch-link]
+[![Star on GitHub][github-star-badge]][github-star-link]
+[![Watch on GitHub][github-forks-badge]][github-forks-link]
+
+[license-badge]: https://img.shields.io/github/license/singerdmx/flutter-quill.svg?style=for-the-badge
+[license-link]: https://github.com/singerdmx/flutter-quill/blob/master/LICENSE
+[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
+[prs-link]: https://github.com/singerdmx/flutter-quill/issues
+[github-watch-badge]: https://img.shields.io/github/watchers/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-watch-link]: https://github.com/singerdmx/flutter-quill/watchers
+[github-star-badge]: https://img.shields.io/github/stars/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-star-link]: https://github.com/singerdmx/flutter-quill/stargazers
+[github-forks-badge]: https://img.shields.io/github/forks/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff
+[github-forks-link]: https://github.com/singerdmx/flutter-quill/network/members
+
+
+FlutterQuill is a rich text editor and a [Quill] component for [Flutter].
-
-
-
-# FlutterQuill
-
-FlutterQuill is a rich text editor and a [Quill] component for [Flutter].
-
This library is a WYSIWYG editor built for the modern mobile platform, with web compatibility under development. You can join our [Slack Group] for discussion.
Demo App: https://bulletjournal.us/home/index.html
Pub: https://pub.dev/packages/flutter_quill
-## Usage
+## Usage
See the `example` directory for a minimal example of how to use FlutterQuill. You typically just need to instantiate a controller:
@@ -79,17 +94,30 @@ It is required to provide EmbedBuilder, e.g. [defaultEmbedBuilderWeb](https://gi
## Migrate Zefyr Data
Check out [code](https://github.com/jwehrle/zefyr_quill_convert) and [doc](https://docs.google.com/document/d/1FUSrpbarHnilb7uDN5J5DDahaI0v1RMXBjj4fFSpSuY/edit?usp=sharing).
-
----
-
-
-
-
-
-
+
+---
+
+
+
+
+
+
+
+
+
+
+
+
+## Sponsors
+
+
+
+
[Quill]: https://quilljs.com/docs/formats
-[Flutter]: https://github.com/flutter/flutter
-[FlutterQuill]: https://pub.dev/packages/flutter_quill
-[ReactQuill]: https://github.com/zenoamaro/react-quill
+[Flutter]: https://github.com/flutter/flutter
+[FlutterQuill]: https://pub.dev/packages/flutter_quill
+[ReactQuill]: https://github.com/zenoamaro/react-quill
[Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g
[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 2fc4fec6..7749c861 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -17,6 +17,7 @@ linter:
- avoid_void_async
- cascade_invocations
- directives_ordering
+ - lines_longer_than_80_chars
- omit_local_variable_types
- prefer_const_constructors
- prefer_const_constructors_in_immutables
@@ -31,5 +32,6 @@ linter:
- prefer_single_quotes
- sort_constructors_first
- sort_unnamed_constructors_first
+ - unnecessary_lambdas
- unnecessary_parenthesis
- unnecessary_string_interpolations
diff --git a/example/ios/Podfile b/example/ios/Podfile
index f7d6a5e6..1e8c3c90 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -28,6 +28,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
diff --git a/example/lib/main.dart b/example/lib/main.dart
index f3ec0666..5b4feb2b 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,4 +1,3 @@
-// import 'package:app/pages/home_page.dart';
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart
index 075f3f9b..e0f436f8 100644
--- a/example/lib/pages/home_page.dart
+++ b/example/lib/pages/home_page.dart
@@ -5,12 +5,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_quill/models/documents/attribute.dart';
-import 'package:flutter_quill/models/documents/document.dart';
-import 'package:flutter_quill/widgets/controller.dart';
-import 'package:flutter_quill/widgets/default_styles.dart';
-import 'package:flutter_quill/widgets/editor.dart';
-import 'package:flutter_quill/widgets/toolbar.dart';
+import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tuple/tuple.dart';
@@ -175,7 +170,8 @@ class _HomePageState extends State {
}
// Renders the image picked by imagePicker from local file storage
- // You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL
+ // You can also upload the picked image to any server (eg : AWS s3
+ // or Firebase) and then return the uploaded image URL.
Future _onImagePickCallback(File file) async {
// Copies the picked file from temporary cache to applications directory
final appDocDir = await getApplicationDocumentsDirectory();
diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart
index 42957b52..594d6123 100644
--- a/example/lib/pages/read_only_page.dart
+++ b/example/lib/pages/read_only_page.dart
@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_quill/widgets/controller.dart';
-import 'package:flutter_quill/widgets/editor.dart';
+import 'package:flutter_quill/flutter_quill.dart' hide Text;
import '../universal_ui/universal_ui.dart';
import '../widgets/demo_scaffold.dart';
diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart
index b242af34..95047a31 100644
--- a/example/lib/universal_ui/universal_ui.dart
+++ b/example/lib/universal_ui/universal_ui.dart
@@ -2,10 +2,10 @@ library universal_ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
-import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
-import 'package:flutter_quill/widgets/responsive_widget.dart';
+import 'package:flutter_quill/flutter_quill.dart';
import 'package:universal_html/html.dart' as html;
+import '../widgets/responsive_widget.dart';
import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance;
class PlatformViewRegistryFix {
@@ -25,7 +25,7 @@ class UniversalUI {
var ui = UniversalUI();
-Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) {
+Widget defaultEmbedBuilderWeb(BuildContext context, Embed node) {
switch (node.value.type) {
case 'image':
final String imageUrl = node.value.data;
@@ -50,8 +50,9 @@ Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) {
default:
throw UnimplementedError(
- 'Embeddable type "${node.value.type}" is not supported by default embed '
- 'builder of QuillEditor. You must pass your own builder function to '
- 'embedBuilder property of QuillEditor or QuillField widgets.');
+ 'Embeddable type "${node.value.type}" is not supported by default '
+ 'embed builder of QuillEditor. You must pass your own builder function '
+ 'to embedBuilder property of QuillEditor or QuillField widgets.',
+ );
}
}
diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart
index 4098a5f6..944b7fe9 100644
--- a/example/lib/widgets/demo_scaffold.dart
+++ b/example/lib/widgets/demo_scaffold.dart
@@ -2,9 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_quill/models/documents/document.dart';
-import 'package:flutter_quill/widgets/controller.dart';
-import 'package:flutter_quill/widgets/toolbar.dart';
+import 'package:flutter_quill/flutter_quill.dart' hide Text;
typedef DemoContentBuilder = Widget Function(
BuildContext context, QuillController? controller);
diff --git a/lib/widgets/responsive_widget.dart b/example/lib/widgets/responsive_widget.dart
similarity index 100%
rename from lib/widgets/responsive_widget.dart
rename to example/lib/widgets/responsive_widget.dart
diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart
index 92e481c7..6b6754ce 100644
--- a/lib/flutter_quill.dart
+++ b/lib/flutter_quill.dart
@@ -1 +1,11 @@
library flutter_quill;
+
+export 'src/models/documents/attribute.dart';
+export 'src/models/documents/document.dart';
+export 'src/models/documents/nodes/embed.dart';
+export 'src/models/documents/nodes/leaf.dart';
+export 'src/models/quill_delta.dart';
+export 'src/widgets/controller.dart';
+export 'src/widgets/default_styles.dart';
+export 'src/widgets/editor.dart';
+export 'src/widgets/toolbar.dart';
diff --git a/lib/models/documents/attribute.dart b/lib/models/documents/attribute.dart
index 1b9043b9..e106383e 100644
--- a/lib/models/documents/attribute.dart
+++ b/lib/models/documents/attribute.dart
@@ -1,292 +1,3 @@
-import 'dart:collection';
-
-import 'package:quiver/core.dart';
-
-enum AttributeScope {
- INLINE, // refer to https://quilljs.com/docs/formats/#inline
- BLOCK, // refer to https://quilljs.com/docs/formats/#block
- EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds
- IGNORE, // attributes that can be ignored
-}
-
-class Attribute {
- Attribute(this.key, this.scope, this.value);
-
- final String key;
- final AttributeScope scope;
- final T value;
-
- static final Map _registry = LinkedHashMap.of({
- Attribute.bold.key: Attribute.bold,
- Attribute.italic.key: Attribute.italic,
- Attribute.underline.key: Attribute.underline,
- Attribute.strikeThrough.key: Attribute.strikeThrough,
- Attribute.font.key: Attribute.font,
- Attribute.size.key: Attribute.size,
- Attribute.link.key: Attribute.link,
- Attribute.color.key: Attribute.color,
- Attribute.background.key: Attribute.background,
- Attribute.placeholder.key: Attribute.placeholder,
- Attribute.header.key: Attribute.header,
- Attribute.align.key: Attribute.align,
- Attribute.list.key: Attribute.list,
- Attribute.codeBlock.key: Attribute.codeBlock,
- Attribute.blockQuote.key: Attribute.blockQuote,
- 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,
- });
-
- static final BoldAttribute bold = BoldAttribute();
-
- static final ItalicAttribute italic = ItalicAttribute();
-
- static final UnderlineAttribute underline = UnderlineAttribute();
-
- static final StrikeThroughAttribute strikeThrough = StrikeThroughAttribute();
-
- static final FontAttribute font = FontAttribute(null);
-
- static final SizeAttribute size = SizeAttribute(null);
-
- static final LinkAttribute link = LinkAttribute(null);
-
- static final ColorAttribute color = ColorAttribute(null);
-
- static final BackgroundAttribute background = BackgroundAttribute(null);
-
- static final PlaceholderAttribute placeholder = PlaceholderAttribute();
-
- static final HeaderAttribute header = HeaderAttribute();
-
- static final IndentAttribute indent = IndentAttribute();
-
- static final AlignAttribute align = AlignAttribute(null);
-
- static final ListAttribute list = ListAttribute(null);
-
- static final CodeBlockAttribute codeBlock = CodeBlockAttribute();
-
- static final BlockQuoteAttribute blockQuote = BlockQuoteAttribute();
-
- static final WidthAttribute width = WidthAttribute(null);
-
- static final HeightAttribute height = HeightAttribute(null);
-
- static final StyleAttribute style = StyleAttribute(null);
-
- static final TokenAttribute token = TokenAttribute('');
-
- static final Set inlineKeys = {
- Attribute.bold.key,
- Attribute.italic.key,
- Attribute.underline.key,
- Attribute.strikeThrough.key,
- Attribute.link.key,
- Attribute.color.key,
- Attribute.background.key,
- Attribute.placeholder.key,
- };
-
- static final Set blockKeys = LinkedHashSet.of({
- Attribute.header.key,
- Attribute.align.key,
- Attribute.list.key,
- Attribute.codeBlock.key,
- Attribute.blockQuote.key,
- Attribute.indent.key,
- });
-
- static final Set blockKeysExceptHeader = LinkedHashSet.of({
- Attribute.list.key,
- Attribute.align.key,
- Attribute.codeBlock.key,
- Attribute.blockQuote.key,
- Attribute.indent.key,
- });
-
- static Attribute get h1 => HeaderAttribute(level: 1);
-
- 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 ul => ListAttribute('bullet');
-
- // "attributes":{"list":"ordered"}
- static Attribute get ol => 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.');
- }
- 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);
- }
-
- @override
- bool operator ==(Object other) {
- if (identical(this, other)) return true;
- if (other is! Attribute) return false;
- final typedOther = other;
- return key == typedOther.key &&
- scope == typedOther.scope &&
- value == typedOther.value;
- }
-
- @override
- int get hashCode => hash3(key, scope, value);
-
- @override
- String toString() {
- return 'Attribute{key: $key, scope: $scope, value: $value}';
- }
-}
-
-class BoldAttribute extends Attribute {
- BoldAttribute() : super('bold', AttributeScope.INLINE, true);
-}
-
-class ItalicAttribute extends Attribute {
- ItalicAttribute() : super('italic', AttributeScope.INLINE, true);
-}
-
-class UnderlineAttribute extends Attribute {
- UnderlineAttribute() : super('underline', AttributeScope.INLINE, true);
-}
-
-class StrikeThroughAttribute extends Attribute {
- StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true);
-}
-
-class FontAttribute extends Attribute {
- FontAttribute(String? val) : super('font', AttributeScope.INLINE, val);
-}
-
-class SizeAttribute extends Attribute {
- SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val);
-}
-
-class LinkAttribute extends Attribute {
- LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val);
-}
-
-class ColorAttribute extends Attribute {
- ColorAttribute(String? val) : super('color', AttributeScope.INLINE, val);
-}
-
-class BackgroundAttribute extends Attribute {
- BackgroundAttribute(String? val)
- : super('background', AttributeScope.INLINE, val);
-}
-
-/// This is custom attribute for hint
-class PlaceholderAttribute extends Attribute {
- PlaceholderAttribute() : super('placeholder', AttributeScope.INLINE, true);
-}
-
-class HeaderAttribute extends Attribute {
- HeaderAttribute({int? level}) : super('header', AttributeScope.BLOCK, level);
-}
-
-class IndentAttribute extends Attribute {
- IndentAttribute({int? level}) : super('indent', AttributeScope.BLOCK, level);
-}
-
-class AlignAttribute extends Attribute {
- AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val);
-}
-
-class ListAttribute extends Attribute {
- ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val);
-}
-
-class CodeBlockAttribute extends Attribute {
- CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true);
-}
-
-class BlockQuoteAttribute extends Attribute {
- BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true);
-}
-
-class WidthAttribute extends Attribute {
- WidthAttribute(String? val) : super('width', AttributeScope.IGNORE, val);
-}
-
-class HeightAttribute extends Attribute {
- HeightAttribute(String? val) : super('height', AttributeScope.IGNORE, val);
-}
-
-class StyleAttribute extends Attribute {
- StyleAttribute(String? val) : super('style', AttributeScope.IGNORE, val);
-}
-
-class TokenAttribute extends Attribute {
- TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val);
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../src/models/documents/attribute.dart';
diff --git a/lib/models/documents/document.dart b/lib/models/documents/document.dart
index 68dbee4d..a187d19d 100644
--- a/lib/models/documents/document.dart
+++ b/lib/models/documents/document.dart
@@ -1,273 +1,3 @@
-import 'dart:async';
-
-import 'package:tuple/tuple.dart';
-
-import '../quill_delta.dart';
-import '../rules/rule.dart';
-import 'attribute.dart';
-import 'history.dart';
-import 'nodes/block.dart';
-import 'nodes/container.dart';
-import 'nodes/embed.dart';
-import 'nodes/line.dart';
-import 'nodes/node.dart';
-import 'style.dart';
-
-/// The rich text document
-class Document {
- Document() : _delta = Delta()..insert('\n') {
- _loadDocument(_delta);
- }
-
- Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
- _loadDocument(_delta);
- }
-
- Document.fromDelta(Delta delta) : _delta = delta {
- _loadDocument(delta);
- }
-
- /// The root node of the document tree
- final Root _root = Root();
-
- Root get root => _root;
-
- int get length => _root.length;
-
- Delta _delta;
-
- Delta toDelta() => Delta.from(_delta);
-
- final Rules _rules = Rules.getInstance();
-
- final StreamController> _observer =
- StreamController.broadcast();
-
- final History _history = History();
-
- Stream> get changes => _observer.stream;
-
- Delta insert(int index, Object? data, {int replaceLength = 0}) {
- assert(index >= 0);
- assert(data is String || data is Embeddable);
- if (data is Embeddable) {
- data = data.toJson();
- } else if ((data as String).isEmpty) {
- return Delta();
- }
-
- final delta = _rules.apply(RuleType.INSERT, this, index,
- data: data, len: replaceLength);
- compose(delta, ChangeSource.LOCAL);
- return delta;
- }
-
- 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);
- }
- return delta;
- }
-
- 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 || 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: len);
- }
-
- if (len > 0) {
- final deleteDelta = delete(index, len);
- delta = delta.compose(deleteDelta);
- }
-
- return delta;
- }
-
- Delta format(int index, int len, Attribute? attribute) {
- assert(index >= 0 && len >= 0 && attribute != null);
-
- var delta = Delta();
-
- final formatDelta = _rules.apply(RuleType.FORMAT, this, index,
- len: len, attribute: attribute);
- if (formatDelta.isNotEmpty) {
- compose(formatDelta, ChangeSource.LOCAL);
- delta = delta.compose(formatDelta);
- }
-
- return delta;
- }
-
- Style collectStyle(int index, int len) {
- final res = queryChild(index);
- return (res.node as Line).collectStyle(res.offset, len);
- }
-
- ChildQuery queryChild(int offset) {
- final res = _root.queryChild(offset, true);
- if (res.node is Line) {
- return res;
- }
- final block = res.node as Block;
- return block.queryChild(res.offset, true);
- }
-
- void compose(Delta delta, ChangeSource changeSource) {
- assert(!_observer.isClosed);
- delta.trim();
- assert(delta.isNotEmpty);
-
- var offset = 0;
- delta = _transform(delta);
- final originalDelta = toDelta();
- for (final op in delta.toList()) {
- final style =
- op.attributes != null ? Style.fromJson(op.attributes) : null;
-
- if (op.isInsert) {
- _root.insert(offset, _normalize(op.data), style);
- } else if (op.isDelete) {
- _root.delete(offset, op.length);
- } else if (op.attributes != null) {
- _root.retain(offset, op.length, style);
- }
-
- if (!op.isDelete) {
- offset += op.length!;
- }
- }
- try {
- _delta = _delta.compose(delta);
- } catch (e) {
- throw '_delta compose failed';
- }
-
- if (_delta != _root.toDelta()) {
- throw 'Compose failed';
- }
- 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);
- }
- return res;
- }
-
- static void _handleImageInsert(
- int i, List 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')) {
- res.push(Operation.insert('\n'));
- }
- // Currently embed is equivalent to image and hence `is! String`
- final opInsertImage = op.isInsert && op.data is! String;
- 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
- res.push(Operation.insert('\n'));
- }
- }
-
- Object _normalize(Object? data) {
- if (data is String) {
- return data;
- }
-
- if (data is Embeddable) {
- return data;
- }
- return Embeddable.fromJson(data as Map);
- }
-
- void close() {
- _observer.close();
- _history.clear();
- }
-
- 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 doc.toList()) {
- if (!op.isInsert) {
- throw ArgumentError.value(doc,
- 'Document Delta can only contain insert operations but ${op.key} found.');
- }
- final style =
- op.attributes != null ? Style.fromJson(op.attributes) : null;
- final data = _normalize(op.data);
- _root.insert(offset, data, style);
- offset += op.length!;
- }
- final node = _root.last;
- if (node is Line &&
- node.parent is! Block &&
- node.style.isEmpty &&
- _root.childCount > 1) {
- _root.remove(node);
- }
- }
-
- bool isEmpty() {
- if (root.children.length != 1) {
- return false;
- }
-
- final node = root.children.first;
- if (!node.isLast) {
- return false;
- }
-
- final delta = node.toDelta();
- return delta.length == 1 &&
- delta.first.data == '\n' &&
- delta.first.key == 'insert';
- }
-}
-
-enum ChangeSource {
- LOCAL,
- REMOTE,
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../src/models/documents/document.dart';
diff --git a/lib/models/documents/history.dart b/lib/models/documents/history.dart
index d406505e..b07c8e33 100644
--- a/lib/models/documents/history.dart
+++ b/lib/models/documents/history.dart
@@ -1,134 +1,3 @@
-import 'package:tuple/tuple.dart';
-
-import '../quill_delta.dart';
-import 'document.dart';
-
-class History {
- History({
- this.ignoreChange = false,
- this.interval = 400,
- this.maxStack = 100,
- this.userOnly = false,
- this.lastRecorded = 0,
- });
-
- final HistoryStack stack = HistoryStack.empty();
-
- bool get hasUndo => stack.undo.isNotEmpty;
-
- bool get hasRedo => stack.redo.isNotEmpty;
-
- /// used for disable redo or undo function
- bool ignoreChange;
-
- int lastRecorded;
-
- /// Collaborative editing's conditions should be true
- final bool userOnly;
-
- ///max operation count for undo
- final int maxStack;
-
- ///record delay
- final int interval;
-
- void handleDocChange(Tuple3 change) {
- if (ignoreChange) return;
- if (!userOnly || change.item3 == ChangeSource.LOCAL) {
- record(change.item2, change.item1);
- } else {
- transform(change.item2);
- }
- }
-
- void clear() {
- stack.clear();
- }
-
- void record(Delta change, Delta before) {
- if (change.isEmpty) return;
- stack.redo.clear();
- var undoDelta = change.invert(before);
- final timeStamp = DateTime.now().millisecondsSinceEpoch;
-
- if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
- final lastDelta = stack.undo.removeLast();
- undoDelta = undoDelta.compose(lastDelta);
- } else {
- lastRecorded = timeStamp;
- }
-
- if (undoDelta.isEmpty) return;
- stack.undo.add(undoDelta);
-
- if (stack.undo.length > maxStack) {
- stack.undo.removeAt(0);
- }
- }
-
- ///
- ///It will override pre local undo delta,replaced by remote change
- ///
- void transform(Delta delta) {
- transformStack(stack.undo, delta);
- transformStack(stack.redo, delta);
- }
-
- void transformStack(List stack, Delta delta) {
- for (var i = stack.length - 1; i >= 0; i -= 1) {
- final oldDelta = stack[i];
- stack[i] = delta.transform(oldDelta, true);
- delta = oldDelta.transform(delta, false);
- if (stack[i].length == 0) {
- stack.removeAt(i);
- }
- }
- }
-
- Tuple2 _change(Document doc, List source, List dest) {
- if (source.isEmpty) {
- return const Tuple2(false, 0);
- }
- final delta = source.removeLast();
- // look for insert or delete
- int? len = 0;
- final ops = delta.toList();
- for (var i = 0; i < ops.length; i++) {
- if (ops[i].key == Operation.insertKey) {
- len = ops[i].length;
- } else if (ops[i].key == Operation.deleteKey) {
- len = ops[i].length! * -1;
- }
- }
- final base = Delta.from(doc.toDelta());
- final inverseDelta = delta.invert(base);
- dest.add(inverseDelta);
- lastRecorded = 0;
- ignoreChange = true;
- doc.compose(delta, ChangeSource.LOCAL);
- ignoreChange = false;
- return Tuple2(true, len);
- }
-
- Tuple2 undo(Document doc) {
- return _change(doc, stack.undo, stack.redo);
- }
-
- Tuple2 redo(Document doc) {
- return _change(doc, stack.redo, stack.undo);
- }
-}
-
-class HistoryStack {
- HistoryStack.empty()
- : undo = [],
- redo = [];
-
- final List undo;
- final List redo;
-
- void clear() {
- undo.clear();
- redo.clear();
- }
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../src/models/documents/history.dart';
diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart
index 095f1183..6de6f743 100644
--- a/lib/models/documents/nodes/block.dart
+++ b/lib/models/documents/nodes/block.dart
@@ -1,72 +1,3 @@
-import '../../quill_delta.dart';
-import 'container.dart';
-import 'line.dart';
-import 'node.dart';
-
-/// Represents a group of adjacent [Line]s with the same block style.
-///
-/// Block elements are:
-/// - Blockquote
-/// - Header
-/// - Indent
-/// - List
-/// - Text Alignment
-/// - Text Direction
-/// - Code Block
-class Block extends Container {
- /// Creates new unmounted [Block].
- @override
- Node newInstance() => Block();
-
- @override
- Line get defaultChild => Line();
-
- @override
- Delta toDelta() {
- return children
- .map((child) => child.toDelta())
- .fold(Delta(), (a, b) => a.concat(b));
- }
-
- @override
- void adjust() {
- if (isEmpty) {
- final sibling = previous;
- unlink();
- if (sibling != null) {
- sibling.adjust();
- }
- return;
- }
-
- var block = this;
- final prev = block.previous;
- // merging it with previous block if style is the same
- if (!block.isFirst &&
- block.previous is Block &&
- prev!.style == block.style) {
- block
- ..moveChildToNewParent(prev as Container?)
- ..unlink();
- block = prev as Block;
- }
- final next = block.next;
- // merging it with next block if style is the same
- if (!block.isLast && block.next is Block && next!.style == block.style) {
- (next as Block).moveChildToNewParent(block);
- next.unlink();
- }
- }
-
- @override
- String toString() {
- final block = style.attributes.toString();
- final buffer = StringBuffer('§ {$block}\n');
- for (final child in children) {
- final tree = child.isLast ? '└' : '├';
- buffer.write(' $tree $child');
- if (!child.isLast) buffer.writeln();
- }
- return buffer.toString();
- }
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/block.dart';
diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart
index dbdd12d1..d9a54451 100644
--- a/lib/models/documents/nodes/container.dart
+++ b/lib/models/documents/nodes/container.dart
@@ -1,160 +1,3 @@
-import 'dart:collection';
-
-import '../style.dart';
-import 'leaf.dart';
-import 'line.dart';
-import 'node.dart';
-
-/// Container can accommodate other nodes.
-///
-/// Delegates insert, retain and delete operations to children nodes. For each
-/// operation container looks for a child at specified index position and
-/// forwards operation to that child.
-///
-/// Most of the operation handling logic is implemented by [Line] and [Text].
-abstract class Container extends Node {
- final LinkedList _children = LinkedList();
-
- /// List of children.
- LinkedList get children => _children;
-
- /// Returns total number of child nodes in this container.
- ///
- /// To get text length of this container see [length].
- int get childCount => _children.length;
-
- /// Returns the first child [Node].
- Node get first => _children.first;
-
- /// Returns the last child [Node].
- Node get last => _children.last;
-
- /// Returns `true` if this container has no child nodes.
- bool get isEmpty => _children.isEmpty;
-
- /// Returns `true` if this container has at least 1 child.
- bool get isNotEmpty => _children.isNotEmpty;
-
- /// Returns an instance of default child for this container node.
- ///
- /// Always returns fresh instance.
- T get defaultChild;
-
- /// Adds [node] to the end of this container children list.
- void add(T node) {
- assert(node?.parent == null);
- node?.parent = this;
- _children.add(node as Node);
- }
-
- /// Adds [node] to the beginning of this container children list.
- void addFirst(T node) {
- assert(node?.parent == null);
- node?.parent = this;
- _children.addFirst(node as Node);
- }
-
- /// Removes [node] from this container.
- void remove(T node) {
- assert(node?.parent == this);
- node?.parent = null;
- _children.remove(node as Node);
- }
-
- /// Moves children of this node to [newParent].
- void moveChildToNewParent(Container? newParent) {
- if (isEmpty) {
- return;
- }
-
- final last = newParent!.isEmpty ? null : newParent.last as T?;
- while (isNotEmpty) {
- final child = first as T;
- child?.unlink();
- newParent.add(child);
- }
-
- /// In case [newParent] already had children we need to make sure
- /// combined list is optimized.
- if (last != null) last.adjust();
- }
-
- /// Queries the child [Node] at specified character [offset] in this container.
- ///
- /// The result may contain the found node or `null` if no node is found
- /// at specified offset.
- ///
- /// [ChildQuery.offset] is set to relative offset within returned child node
- /// which points at the same character position in the document as the
- /// original [offset].
- ChildQuery queryChild(int offset, bool inclusive) {
- if (offset < 0 || offset > length) {
- return ChildQuery(null, 0);
- }
-
- for (final node in children) {
- final len = node.length;
- if (offset < len || (inclusive && offset == len && (node.isLast))) {
- return ChildQuery(node, offset);
- }
- 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));
-
- if (isNotEmpty) {
- final child = queryChild(index, false);
- child.node!.insert(child.offset, data, style);
- return;
- }
-
- // empty
- assert(index == 0);
- final node = defaultChild;
- add(node);
- node?.insert(index, data, style);
- }
-
- @override
- void retain(int index, int? length, Style? attributes) {
- assert(isNotEmpty);
- final child = queryChild(index, false);
- child.node!.retain(child.offset, length, attributes);
- }
-
- @override
- void delete(int index, int? length) {
- assert(isNotEmpty);
- final child = queryChild(index, false);
- child.node!.delete(child.offset, length);
- }
-
- @override
- String toString() => _children.join('\n');
-}
-
-/// Result of a child query in a [Container].
-class ChildQuery {
- ChildQuery(this.node, this.offset);
-
- /// The child node if found, otherwise `null`.
- final Node? node;
-
- /// Starting offset within the child [node] which points at the same
- /// character in the document as the original offset passed to
- /// [Container.queryChild] method.
- final int offset;
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/container.dart';
diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart
index d6fe628a..01dc357b 100644
--- a/lib/models/documents/nodes/embed.dart
+++ b/lib/models/documents/nodes/embed.dart
@@ -1,40 +1,3 @@
-/// An object which can be embedded into a Quill document.
-///
-/// See also:
-///
-/// * [BlockEmbed] which represents a block embed.
-class Embeddable {
- Embeddable(this.type, this.data);
-
- /// The type of this object.
- final String type;
-
- /// The data payload of this object.
- final dynamic data;
-
- Map toJson() {
- final m = {type: data};
- return m;
- }
-
- static Embeddable fromJson(Map json) {
- final m = Map.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
-/// inline with regular text.
-///
-/// There are two built-in embed types supported by Quill documents, however
-/// 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);
-
- static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
-
- static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/embed.dart';
diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart
index bd9292f5..cc2808f2 100644
--- a/lib/models/documents/nodes/leaf.dart
+++ b/lib/models/documents/nodes/leaf.dart
@@ -1,252 +1,3 @@
-import 'dart:math' as math;
-
-import '../../quill_delta.dart';
-import '../style.dart';
-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) {
- if (data is Embeddable) {
- return Embed(data);
- }
- final text = data as String;
- assert(text.isNotEmpty);
- return Text(text);
- }
-
- Leaf.val(Object val) : _value = val;
-
- /// Contents of this node, either a String if this is a [Text] or an
- /// [Embed] if this is an [BlockEmbed].
- Object get value => _value;
- Object _value;
-
- @override
- void applyStyle(Style value) {
- assert(value.isInline || value.isIgnored || value.isEmpty,
- 'Unable to apply Style to leaf: $value');
- super.applyStyle(value);
- }
-
- @override
- Line? get parent => super.parent as Line?;
-
- @override
- int get length {
- if (_value is String) {
- return (_value as String).length;
- }
- // return 1 for embedded object
- return 1;
- }
-
- @override
- Delta toDelta() {
- final data =
- _value is Embeddable ? (_value as Embeddable).toJson() : _value;
- return Delta()..insert(data, style.toJson());
- }
-
- @override
- void insert(int index, Object data, Style? style) {
- assert(index >= 0 && index <= length);
- final node = Leaf(data);
- if (index < length) {
- splitAt(index)!.insertBefore(node);
- } else {
- insertAfter(node);
- }
- node.format(style);
- }
-
- @override
- void retain(int index, int? len, Style? style) {
- if (style == null) {
- return;
- }
-
- final local = math.min(length - index, len!);
- final remain = len - local;
- final node = _isolate(index, local);
-
- if (remain > 0) {
- assert(node.next != null);
- node.next!.retain(0, remain, style);
- }
- node.format(style);
- }
-
- @override
- void delete(int index, int? len) {
- assert(index < length);
-
- final local = math.min(length - index, len!);
- final target = _isolate(index, local);
- final prev = target.previous as Leaf?;
- final next = target.next as Leaf?;
- target.unlink();
-
- final remain = len - local;
- if (remain > 0) {
- assert(next != null);
- next!.delete(0, remain);
- }
-
- if (prev != null) {
- prev.adjust();
- }
- }
-
- /// Adjust this text node by merging it with adjacent nodes if they share
- /// the same style.
- @override
- void adjust() {
- if (this is Embed) {
- // Embed nodes cannot be merged with text nor other embeds (in fact,
- // there could be no two adjacent embeds on the same line since an
- // embed occupies an entire line).
- return;
- }
-
- // This is a text node and it can only be merged with other text nodes.
- var node = this as Text;
-
- // Merging it with previous node if style is the same.
- final prev = node.previous;
- if (!node.isFirst && prev is Text && prev.style == node.style) {
- prev._value = prev.value + node.value;
- node.unlink();
- node = prev;
- }
-
- // Merging it with next node if style is the same.
- final next = node.next;
- if (!node.isLast && next is Text && next.style == node.style) {
- node._value = node.value + next.value;
- next.unlink();
- }
- }
-
- /// Splits this leaf node at [index] and returns new node.
- ///
- /// If this is the last node in its list and [index] equals this node's
- /// length then this method returns `null` as there is nothing left to split.
- /// If there is another leaf node after this one and [index] equals this
- /// node's length then the next leaf node is returned.
- ///
- /// If [index] equals to `0` then this node itself is returned unchanged.
- ///
- /// In case a new node is actually split from this one, it inherits this
- /// node's style.
- Leaf? splitAt(int index) {
- assert(index >= 0 && index <= length);
- if (index == 0) {
- return this;
- }
- if (index == length) {
- return isLast ? null : next as Leaf?;
- }
-
- assert(this is Text);
- final text = _value as String;
- _value = text.substring(0, index);
- final split = Leaf(text.substring(index))..applyStyle(style);
- insertAfter(split);
- return split;
- }
-
- /// Cuts a leaf from [index] to the end of this node and returns new node
- /// in detached state (e.g. [mounted] returns `false`).
- ///
- /// Splitting logic is identical to one described in [splitAt], meaning this
- /// method may return `null`.
- Leaf? cutAt(int index) {
- assert(index >= 0 && index <= length);
- final cut = splitAt(index);
- cut?.unlink();
- return cut;
- }
-
- /// Formats this node and optimizes it with adjacent leaf nodes if needed.
- void format(Style? style) {
- if (style != null && style.isNotEmpty) {
- applyStyle(style);
- }
- adjust();
- }
-
- /// Isolates a new leaf starting at [index] with specified [length].
- ///
- /// Splitting logic is identical to one described in [splitAt], with one
- /// exception that it is required for [index] to always be less than this
- /// node's length. As a result this method always returns a [LeafNode]
- /// instance. Returned node may still be the same as this node
- /// if provided [index] is `0`.
- Leaf _isolate(int index, int length) {
- assert(
- index >= 0 && index < this.length && (index + length <= this.length));
- final target = splitAt(index)!..splitAt(length);
- return target;
- }
-}
-
-/// A span of formatted text within a line in a Quill document.
-///
-/// Text is a leaf node of a document tree.
-///
-/// Parent of a text node is always a [Line], and as a consequence text
-/// node's [value] cannot contain any line-break characters.
-///
-/// See also:
-///
-/// * [Embed], a leaf node representing an embeddable object.
-/// * [Line], a node representing a line of text.
-class Text extends Leaf {
- Text([String text = ''])
- : assert(!text.contains('\n')),
- super.val(text);
-
- @override
- Node newInstance() => Text();
-
- @override
- String get value => _value as String;
-
- @override
- String toPlainText() => value;
-}
-
-/// An embed node inside of a line in a Quill document.
-///
-/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
-/// piece of non-textual content embedded into a document, such as, image,
-/// horizontal rule, video, or any other object with defined structure,
-/// like a tweet, for instance.
-///
-/// Embed node's length is always `1` character and it is represented with
-/// unicode object replacement character in the document text.
-///
-/// Any inline style can be applied to an embed, however this does not
-/// necessarily mean the embed will look according to that style. For instance,
-/// applying "bold" style to an image gives no effect, while adding a "link" to
-/// an image actually makes the image react to user's action.
-class Embed extends Leaf {
- Embed(Embeddable data) : super.val(data);
-
- static const kObjectReplacementCharacter = '\uFFFC';
-
- @override
- Node newInstance() => throw UnimplementedError();
-
- @override
- Embeddable get value => super.value as Embeddable;
-
- /// // Embed nodes are represented as unicode object replacement character in
- // plain text.
- @override
- String toPlainText() => kObjectReplacementCharacter;
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/leaf.dart';
diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart
index fabfad4d..7ca2016e 100644
--- a/lib/models/documents/nodes/line.dart
+++ b/lib/models/documents/nodes/line.dart
@@ -1,371 +1,3 @@
-import 'dart:math' as math;
-
-import 'package:collection/collection.dart';
-
-import '../../quill_delta.dart';
-import '../attribute.dart';
-import '../style.dart';
-import 'block.dart';
-import 'container.dart';
-import 'embed.dart';
-import 'leaf.dart';
-import 'node.dart';
-
-/// A line of rich text in a Quill document.
-///
-/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
-///
-/// When a line contains an embed, it fully occupies the line, no other embeds
-/// or text nodes are allowed.
-class Line extends Container {
- @override
- Leaf get defaultChild => Text();
-
- @override
- int get length => super.length + 1;
-
- /// Returns `true` if this line contains an embedded object.
- bool get hasEmbed {
- if (childCount != 1) {
- return false;
- }
-
- return children.single is Embed;
- }
-
- /// Returns next [Line] or `null` if this is the last line in the document.
- Line? get nextLine {
- if (!isLast) {
- return next is Block ? (next as Block).first as Line? : next as Line?;
- }
- if (parent is! Block) {
- return null;
- }
-
- if (parent!.isLast) {
- return null;
- }
- return parent!.next is Block
- ? (parent!.next as Block).first as Line?
- : parent!.next as Line?;
- }
-
- @override
- Node newInstance() => Line();
-
- @override
- Delta toDelta() {
- final delta = children
- .map((child) => child.toDelta())
- .fold(Delta(), (dynamic a, b) => a.concat(b));
- var attributes = style;
- if (parent is Block) {
- final block = parent as Block;
- attributes = attributes.mergeAll(block.style);
- }
- delta.insert('\n', attributes.toJson());
- return delta;
- }
-
- @override
- String toPlainText() => '${super.toPlainText()}\n';
-
- @override
- String toString() {
- final body = children.join(' → ');
- final styleString = style.isNotEmpty ? ' $style' : '';
- return '¶ $body ⏎$styleString';
- }
-
- @override
- void insert(int index, Object data, Style? style) {
- if (data is Embeddable) {
- // We do not check whether this line already has any children here as
- // inserting an embed into a line with other text is acceptable from the
- // Delta format perspective.
- // We rely on heuristic rules to ensure that embeds occupy an entire line.
- _insertSafe(index, data, style);
- return;
- }
-
- final text = data as String;
- final lineBreak = text.indexOf('\n');
- if (lineBreak < 0) {
- _insertSafe(index, text, style);
- // No need to update line or block format since those attributes can only
- // be attached to `\n` character and we already know it's not present.
- return;
- }
-
- final prefix = text.substring(0, lineBreak);
- _insertSafe(index, prefix, style);
- if (prefix.isNotEmpty) {
- index += prefix.length;
- }
-
- // Next line inherits our format.
- final nextLine = _getNextLine(index);
-
- // Reset our format and unwrap from a block if needed.
- clearStyle();
- if (parent is Block) {
- _unwrap();
- }
-
- // Now we can apply new format and re-layout.
- _format(style);
-
- // Continue with remaining part.
- final remain = text.substring(lineBreak + 1);
- nextLine.insert(0, remain, style);
- }
-
- @override
- void retain(int index, int? len, Style? style) {
- if (style == null) {
- return;
- }
- final thisLength = length;
-
- final local = math.min(thisLength - index, len!);
- // If index is at newline character then this is a line/block style update.
- final isLineFormat = (index + local == thisLength) && local == 1;
-
- if (isLineFormat) {
- assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
- 'It is not allowed to apply inline attributes to line itself.');
- _format(style);
- } else {
- // Otherwise forward to children as it's an inline format update.
- assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
- assert(index + local != thisLength);
- super.retain(index, local, style);
- }
-
- final remain = len - local;
- if (remain > 0) {
- assert(nextLine != null);
- nextLine!.retain(0, remain, style);
- }
- }
-
- @override
- void delete(int index, int? len) {
- final local = math.min(length - index, len!);
- final isLFDeleted = index + local == length; // Line feed
- if (isLFDeleted) {
- // Our newline character deleted with all style information.
- clearStyle();
- if (local > 1) {
- // Exclude newline character from delete range for children.
- super.delete(index, local - 1);
- }
- } else {
- super.delete(index, local);
- }
-
- final remaining = len - local;
- if (remaining > 0) {
- assert(nextLine != null);
- nextLine!.delete(0, remaining);
- }
-
- if (isLFDeleted && isNotEmpty) {
- // Since we lost our line-break and still have child text nodes those must
- // migrate to the next line.
-
- // nextLine might have been unmounted since last assert so we need to
- // check again we still have a line after us.
- assert(nextLine != null);
-
- // Move remaining children in this line to the next line so that all
- // attributes of nextLine are preserved.
- nextLine!.moveChildToNewParent(this);
- moveChildToNewParent(nextLine);
- }
-
- if (isLFDeleted) {
- // Now we can remove this line.
- final block = parent!; // remember reference before un-linking.
- unlink();
- block.adjust();
- }
- }
-
- /// Formats this line.
- void _format(Style? newStyle) {
- if (newStyle == null || newStyle.isEmpty) {
- return;
- }
-
- applyStyle(newStyle);
- final blockStyle = newStyle.getBlockExceptHeader();
- if (blockStyle == null) {
- return;
- } // No block-level changes
-
- if (parent is Block) {
- final parentStyle = (parent as Block).style.getBlocksExceptHeader();
- if (blockStyle.value == null) {
- _unwrap();
- } else if (!const MapEquality()
- .equals(newStyle.getBlocksExceptHeader(), parentStyle)) {
- _unwrap();
- _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
- _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.
- void _wrap(Block block) {
- assert(parent != null && parent is! Block);
- insertAfter(block);
- unlink();
- block.add(this);
- }
-
- /// Unwraps this line from it's parent [Block].
- ///
- /// This method asserts if current [parent] of this line is not a [Block].
- void _unwrap() {
- if (parent is! Block) {
- throw ArgumentError('Invalid parent');
- }
- final block = parent as Block;
-
- assert(block.children.contains(this));
-
- if (isFirst) {
- unlink();
- block.insertBefore(this);
- } else if (isLast) {
- unlink();
- block.insertAfter(this);
- } else {
- final before = block.clone() as Block;
- block.insertBefore(before);
-
- var child = block.first as Line;
- while (child != this) {
- child.unlink();
- before.add(child);
- child = block.first as Line;
- }
- unlink();
- block.insertBefore(this);
- }
- block.adjust();
- }
-
- Line _getNextLine(int index) {
- assert(index == 0 || (index > 0 && index < length));
-
- final line = clone() as Line;
- insertAfter(line);
- if (index == length - 1) {
- return line;
- }
-
- final query = queryChild(index, false);
- while (!query.node!.isLast) {
- final next = (last as Leaf)..unlink();
- line.addFirst(next);
- }
- final child = query.node as Leaf;
- final cut = child.splitAt(query.offset);
- cut?.unlink();
- line.addFirst(cut);
- return line;
- }
-
- void _insertSafe(int index, Object data, Style? style) {
- assert(index == 0 || (index > 0 && index < length));
-
- if (data is String) {
- assert(!data.contains('\n'));
- if (data.isEmpty) {
- return;
- }
- }
-
- if (isEmpty) {
- final child = Leaf(data);
- add(child);
- child.format(style);
- } else {
- final result = queryChild(index, true);
- result.node!.insert(result.offset, data, style);
- }
- }
-
- /// Returns style for specified text range.
- ///
- /// Only attributes applied to all characters within this range are
- /// included in the result. Inline and line level attributes are
- /// handled separately, e.g.:
- ///
- /// - line attribute X is included in the result only if it exists for
- /// every line within this range (partially included lines are counted).
- /// - inline attribute X is included in the result only if it exists
- /// for every character within this range (line-break characters excluded).
- Style collectStyle(int offset, int len) {
- final local = math.min(length - offset, len);
- var result = Style();
- final excluded = {};
-
- void _handle(Style style) {
- if (result.isEmpty) {
- excluded.addAll(style.values);
- } else {
- for (final attr in result.values) {
- if (!style.containsKey(attr.key)) {
- excluded.add(attr);
- }
- }
- }
- final remaining = style.removeAll(excluded);
- result = result.removeAll(excluded);
- result = result.mergeAll(remaining);
- }
-
- final data = queryChild(offset, true);
- var node = data.node as Leaf?;
- if (node != null) {
- result = result.mergeAll(node.style);
- var pos = node.length - data.offset;
- while (!node!.isLast && pos < local) {
- node = node.next as Leaf?;
- _handle(node!.style);
- pos += node.length;
- }
- }
-
- result = result.mergeAll(style);
- if (parent is Block) {
- final block = parent as Block;
- result = result.mergeAll(block.style);
- }
-
- final remaining = len - local;
- if (remaining > 0) {
- final rest = nextLine!.collectStyle(0, remaining);
- _handle(rest);
- }
-
- return result;
- }
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/line.dart';
diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart
index 6bb0fb97..210c1672 100644
--- a/lib/models/documents/nodes/node.dart
+++ b/lib/models/documents/nodes/node.dart
@@ -1,131 +1,3 @@
-import 'dart:collection';
-
-import '../../quill_delta.dart';
-import '../attribute.dart';
-import '../style.dart';
-import 'container.dart';
-import 'line.dart';
-
-/// An abstract node in a document tree.
-///
-/// Represents a segment of a Quill document with specified [offset]
-/// and [length].
-///
-/// The [offset] property is relative to [parent]. See also [documentOffset]
-/// which provides absolute offset of this node within the document.
-///
-/// The current parent node is exposed by the [parent] property.
-abstract class Node extends LinkedListEntry {
- /// Current parent of this node. May be null if this node is not mounted.
- Container? parent;
-
- Style get style => _style;
- Style _style = Style();
-
- /// Returns `true` if this node is the first node in the [parent] list.
- bool get isFirst => list!.first == this;
-
- /// Returns `true` if this node is the last node in the [parent] list.
- bool get isLast => list!.last == this;
-
- /// Length of this node in characters.
- int get length;
-
- Node clone() => newInstance()..applyStyle(style);
-
- /// Offset in characters of this node relative to [parent] 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 cur = this;
- do {
- cur = cur.previous!;
- offset += cur.length;
- } while (!cur.isFirst);
- return offset;
- }
-
- /// Offset in characters of this node in the document.
- int get documentOffset {
- final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
- return parentOffset + offset;
- }
-
- /// Returns `true` if this node contains character at specified [offset] in
- /// the document.
- bool containsOffset(int offset) {
- final o = documentOffset;
- return o <= offset && offset < o + length;
- }
-
- void applyAttribute(Attribute attribute) {
- _style = _style.merge(attribute);
- }
-
- void applyStyle(Style value) {
- _style = _style.mergeAll(value);
- }
-
- void clearStyle() {
- _style = Style();
- }
-
- @override
- void insertBefore(Node entry) {
- assert(entry.parent == null && parent != null);
- entry.parent = parent;
- super.insertBefore(entry);
- }
-
- @override
- void insertAfter(Node entry) {
- assert(entry.parent == null && parent != null);
- entry.parent = parent;
- super.insertAfter(entry);
- }
-
- @override
- void unlink() {
- assert(parent != null);
- parent = null;
- super.unlink();
- }
-
- void adjust() {/* no-op */}
-
- /// abstract methods begin
-
- Node newInstance();
-
- String toPlainText();
-
- Delta toDelta();
-
- void insert(int index, Object data, Style? style);
-
- void retain(int index, int? len, Style? style);
-
- void delete(int index, int? len);
-
- /// abstract methods end
-}
-
-/// Root node of document tree.
-class Root extends Container> {
- @override
- Node newInstance() => Root();
-
- @override
- Container get defaultChild => Line();
-
- @override
- Delta toDelta() => children
- .map((child) => child.toDelta())
- .fold(Delta(), (a, b) => a.concat(b));
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../../src/models/documents/nodes/node.dart';
diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart
index fade1bb5..6df9412b 100644
--- a/lib/models/documents/style.dart
+++ b/lib/models/documents/style.dart
@@ -1,127 +1,3 @@
-import 'package:collection/collection.dart';
-import 'package:quiver/core.dart';
-
-import 'attribute.dart';
-
-/* Collection of style attributes */
-class Style {
- Style() : _attributes = {};
-
- Style.attr(this._attributes);
-
- final Map _attributes;
-
- static Style fromJson(Map? attributes) {
- if (attributes == null) {
- return Style();
- }
-
- final result = attributes.map((key, dynamic value) {
- final attr = Attribute.fromKeyValue(key, value);
- return MapEntry(key, attr);
- });
- return Style.attr(result);
- }
-
- Map? toJson() => _attributes.isEmpty
- ? null
- : _attributes.map((_, attribute) =>
- MapEntry(attribute.key, attribute.value));
-
- Iterable get keys => _attributes.keys;
-
- Iterable get values => _attributes.values.sorted(
- (a, b) => Attribute.getRegistryOrder(a) - Attribute.getRegistryOrder(b));
-
- Map get attributes => _attributes;
-
- bool get isEmpty => _attributes.isEmpty;
-
- bool get isNotEmpty => _attributes.isNotEmpty;
-
- bool get isInline => isNotEmpty && values.every((item) => item.isInline);
-
- bool get isIgnored =>
- isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE);
-
- Attribute get single => _attributes.values.single;
-
- bool containsKey(String key) => _attributes.containsKey(key);
-
- Attribute? getBlockExceptHeader() {
- 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;
- }
-
- Map getBlocksExceptHeader() {
- final m = {};
- attributes.forEach((key, value) {
- if (Attribute.blockKeysExceptHeader.contains(key)) {
- m[key] = value;
- }
- });
- return m;
- }
-
- Style merge(Attribute attribute) {
- final merged = Map.from(_attributes);
- if (attribute.value == null) {
- merged.remove(attribute.key);
- } else {
- merged[attribute.key] = attribute;
- }
- return Style.attr(merged);
- }
-
- Style mergeAll(Style other) {
- var result = Style.attr(_attributes);
- for (final attribute in other.values) {
- result = result.merge(attribute);
- }
- return result;
- }
-
- Style removeAll(Set attributes) {
- final merged = Map.from(_attributes);
- attributes.map((item) => item.key).forEach(merged.remove);
- return Style.attr(merged);
- }
-
- Style put(Attribute attribute) {
- final m = Map.from(attributes);
- m[attribute.key] = attribute;
- return Style.attr(m);
- }
-
- @override
- bool operator ==(Object other) {
- if (identical(this, other)) {
- return true;
- }
- if (other is! Style) {
- return false;
- }
- final typedOther = other;
- const eq = MapEquality();
- return eq.equals(_attributes, typedOther._attributes);
- }
-
- @override
- int get hashCode {
- final hashes =
- _attributes.entries.map((entry) => hash2(entry.key, entry.value));
- return hashObjects(hashes);
- }
-
- @override
- String toString() => "{${_attributes.values.join(', ')}}";
-}
+/// TODO: Remove this file in the next breaking release, because implementation
+/// files should be located in the src folder, https://bit.ly/3fA23Yz.
+export '../../src/models/documents/style.dart';
diff --git a/lib/models/quill_delta.dart b/lib/models/quill_delta.dart
index a0e608be..796d68ca 100644
--- a/lib/models/quill_delta.dart
+++ b/lib/models/quill_delta.dart
@@ -1,684 +1,3 @@
-// Copyright (c) 2018, Anatoly Pulyaevskiy. All rights reserved. Use of this source code
-// is governed by a BSD-style license that can be found in the LICENSE file.
-
-/// Implementation of Quill Delta format in Dart.
-library quill_delta;
-
-import 'dart:math' as math;
-
-import 'package:collection/collection.dart';
-import 'package:quiver/core.dart';
-
-const _attributeEquality = DeepCollectionEquality();
-const _valueEquality = DeepCollectionEquality();
-
-/// Decoder function to convert raw `data` object into a user-defined data type.
-///
-/// Useful with embedded content.
-typedef DataDecoder = Object? Function(Object data);
-
-/// Default data decoder which simply passes through the original value.
-Object? _passThroughDataDecoder(Object? data) => data;
-
-/// Operation performed on a rich-text document.
-class Operation {
- Operation._(this.key, this.length, this.data, Map? attributes)
- : assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
- assert(() {
- if (key != Operation.insertKey) return true;
- return data is String ? data.length == length : length == 1;
- }(), 'Length of insert operation must be equal to the data length.'),
- _attributes =
- attributes != null ? Map.from(attributes) : null;
-
- /// Creates operation which deletes [length] of characters.
- factory Operation.delete(int length) =>
- Operation._(Operation.deleteKey, length, '', null);
-
- /// Creates operation which inserts [text] with optional [attributes].
- factory Operation.insert(dynamic data, [Map? attributes]) =>
- Operation._(Operation.insertKey, data is String ? data.length : 1, data,
- attributes);
-
- /// Creates operation which retains [length] of characters and optionally
- /// applies attributes.
- factory Operation.retain(int? length, [Map? attributes]) =>
- Operation._(Operation.retainKey, length, '', attributes);
-
- /// Key of insert operations.
- static const String insertKey = 'insert';
-
- /// Key of delete operations.
- static const String deleteKey = 'delete';
-
- /// Key of retain operations.
- static const String retainKey = 'retain';
-
- /// Key of attributes collection.
- static const String attributesKey = 'attributes';
-
- static const List _validKeys = [insertKey, deleteKey, retainKey];
-
- /// Key of this operation, can be "insert", "delete" or "retain".
- final String key;
-
- /// Length of this operation.
- final int? length;
-
- /// Payload of "insert" operation, for other types is set to empty string.
- final Object? data;
-
- /// Rich-text attributes set by this operation, can be `null`.
- Map? get attributes =>
- _attributes == null ? null : Map.from(_attributes!);
- final Map? _attributes;
-
- /// Creates new [Operation] from JSON payload.
- ///
- /// If `dataDecoder` parameter is not null then it is used to additionally
- /// decode the operation's data object. Only applied to insert operations.
- static Operation fromJson(Map data, {DataDecoder? dataDecoder}) {
- dataDecoder ??= _passThroughDataDecoder;
- final map = Map.from(data);
- if (map.containsKey(Operation.insertKey)) {
- final data = dataDecoder(map[Operation.insertKey]);
- final dataLength = data is String ? data.length : 1;
- return Operation._(
- Operation.insertKey, dataLength, data, map[Operation.attributesKey]);
- } else if (map.containsKey(Operation.deleteKey)) {
- final int? length = map[Operation.deleteKey];
- return Operation._(Operation.deleteKey, length, '', null);
- } else if (map.containsKey(Operation.retainKey)) {
- final int? length = map[Operation.retainKey];
- return Operation._(
- Operation.retainKey, length, '', map[Operation.attributesKey]);
- }
- throw ArgumentError.value(data, 'Invalid data for Delta operation.');
- }
-
- /// Returns JSON-serializable representation of this operation.
- Map toJson() {
- final json = {key: value};
- if (_attributes != null) json[Operation.attributesKey] = attributes;
- return json;
- }
-
- /// Returns value of this operation.
- ///
- /// For insert operations this returns text, for delete and retain - length.
- dynamic get value => (key == Operation.insertKey) ? data : length;
-
- /// Returns `true` if this is a delete operation.
- bool get isDelete => key == Operation.deleteKey;
-
- /// Returns `true` if this is an insert operation.
- bool get isInsert => key == Operation.insertKey;
-
- /// Returns `true` if this is a retain operation.
- bool get isRetain => key == Operation.retainKey;
-
- /// Returns `true` if this operation has no attributes, e.g. is plain text.
- bool get isPlain => _attributes == null || _attributes!.isEmpty;
-
- /// Returns `true` if this operation sets at least one attribute.
- bool get isNotPlain => !isPlain;
-
- /// Returns `true` is this operation is empty.
- ///
- /// An operation is considered empty if its [length] is equal to `0`.
- bool get isEmpty => length == 0;
-
- /// Returns `true` is this operation is not empty.
- bool get isNotEmpty => length! > 0;
-
- @override
- bool operator ==(other) {
- if (identical(this, other)) return true;
- if (other is! Operation) return false;
- final typedOther = other;
- return key == typedOther.key &&
- length == typedOther.length &&
- _valueEquality.equals(data, typedOther.data) &&
- hasSameAttributes(typedOther);
- }
-
- /// Returns `true` if this operation has attribute specified by [name].
- bool hasAttribute(String name) =>
- isNotPlain && _attributes!.containsKey(name);
-
- /// Returns `true` if [other] operation has the same attributes as this one.
- bool hasSameAttributes(Operation other) {
- return _attributeEquality.equals(_attributes, other._attributes);
- }
-
- @override
- int get hashCode {
- if (_attributes != null && _attributes!.isNotEmpty) {
- final attrsHash =
- hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value)));
- return hash3(key, value, attrsHash);
- }
- return hash2(key, value);
- }
-
- @override
- String toString() {
- final attr = attributes == null ? '' : ' + $attributes';
- final text = isInsert
- ? (data is String
- ? (data as String).replaceAll('\n', '⏎')
- : data.toString())
- : '$length';
- return '$key⟨ $text ⟩$attr';
- }
-}
-
-/// Delta represents a document or a modification of a document as a sequence of
-/// insert, delete and retain operations.
-///
-/// Delta consisting of only "insert" operations is usually referred to as
-/// "document delta". When delta includes also "retain" or "delete" operations
-/// it is a "change delta".
-class Delta {
- /// Creates new empty [Delta].
- factory Delta() => Delta._([]);
-
- Delta._(List operations) : _operations = operations;
-
- /// Creates new [Delta] from [other].
- factory Delta.from(Delta other) =>
- Delta._(List.from(other._operations));
-
- /// Transforms two attribute sets.
- static Map? transformAttributes(
- Map? a, Map? b, bool priority) {
- if (a == null) return b;
- if (b == null) return null;
-
- if (!priority) return b;
-
- final result = b.keys.fold