import 'dart:async' show StreamController; import '../../widgets/quill/embeds.dart'; import '../quill_delta.dart'; import '../rules/rule.dart'; import '../structs/doc_change.dart'; import '../structs/history_changed.dart'; import '../structs/offset_value.dart'; import '../structs/segment_leaf_node.dart'; import 'attribute.dart'; import 'history.dart'; import 'nodes/block.dart'; import 'nodes/container.dart'; import 'nodes/embeddable.dart'; import 'nodes/leaf.dart'; import 'nodes/line.dart'; import 'nodes/node.dart'; import 'style.dart'; /// The rich text document class Document { /// Creates new empty document. Document() : _delta = Delta()..insert('\n') { _loadDocument(_delta); } /// Creates new document from provided JSON `data`. Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) { _loadDocument(_delta); } /// Creates new document from provided `delta`. Document.fromDelta(Delta delta) : _delta = delta { _loadDocument(delta); } /// The root node of the document tree final Root _root = Root(); Root get root => _root; /// Length of this document. int get length => _root.length; Delta _delta; /// Returns contents of this document as [Delta]. Delta toDelta() => Delta.from(_delta); final Rules _rules = Rules.getInstance(); void setCustomRules(List customRules) { _rules.setCustomRules(customRules); } final StreamController _observer = StreamController.broadcast(); final History _history = History(); /// Stream of [DocChange]s applied to this document. Stream get changes => _observer.stream; /// Inserts [data] in this document at specified [index]. /// /// The `data` parameter can be either a String or an instance of /// [Embeddable]. /// /// Applies heuristic rules before modifying this document and /// produces a change event with its source set to [ChangeSource.local]. /// /// Returns an instance of [Delta] actually composed into this document. 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; } /// Deletes [length] of characters from this document starting at [index]. /// /// This method applies heuristic rules before modifying this document and /// produces a [Change] with source set to [ChangeSource.local]. /// /// Returns an instance of [Delta] actually composed into this document. 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; } /// Replaces [length] of characters starting at [index] with [data]. /// /// This method applies heuristic rules before modifying this document and /// produces a change event with its source set to [ChangeSource.local]. /// /// Returns an instance of [Delta] actually composed into this document. 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; } /// Formats segment of this document with specified [attribute]. /// /// Applies heuristic rules before modifying this document and /// produces a change event with its source set to [ChangeSource.local]. /// /// Returns an instance of [Delta] actually composed into this document. /// The returned [Delta] may be empty in which case this document remains /// unchanged and no change event is published to the [changes] stream. 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; } /// Only attributes applied to all characters within this range are /// included in the result. Style collectStyle(int index, int len) { final res = queryChild(index); Style rangeStyle; if (len > 0) { return (res.node as Line).collectStyle(res.offset, len); } if (res.offset == 0) { rangeStyle = (res.node as Line).collectStyle(res.offset, len); return rangeStyle.removeAll({ for (final attr in rangeStyle.values) if (attr.isInline) attr }); } rangeStyle = (res.node as Line).collectStyle(res.offset - 1, len); final linkAttribute = rangeStyle.attributes[Attribute.link.key]; if ((linkAttribute != null) && (linkAttribute.value != (res.node as Line) .collectStyle(res.offset, len) .attributes[Attribute.link.key] ?.value)) { return rangeStyle.removeAll({linkAttribute}); } return rangeStyle; } /// Returns all styles and Embed for each node within selection List collectAllIndividualStyleAndEmbed(int index, int len) { final res = queryChild(index); return (res.node as Line) .collectAllIndividualStylesAndEmbed(res.offset, len); } /// Returns all styles for any character within the specified text range. List