123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
- // for details. All rights reserved. Use of this source code is governed by a
- // BSD-style license that can be found in the LICENSE file.
- import 'dart:math' as math;
-
- import 'package:quill_delta/quill_delta.dart';
-
- import 'attributes.dart';
- import 'line.dart';
- import 'node.dart';
-
- /// A leaf node in Notus document tree.
- abstract class LeafNode extends Node
- with StyledNodeMixin
- implements StyledNode {
- /// Creates a new [LeafNode] with specified [value].
- LeafNode._([String value = ''])
- : assert(value != null && !value.contains('\n')),
- _value = value;
-
- factory LeafNode([String value = '']) {
- LeafNode node;
- if (value == kZeroWidthSpace) {
- // Zero-width space is reserved for embed nodes.
- node = new EmbedNode();
- } else {
- assert(
- !value.contains(kZeroWidthSpace),
- 'Zero-width space is reserved for embed leaf nodes and cannot be used '
- 'inside regular text nodes.');
- node = new TextNode(value);
- }
- return node;
- }
-
- /// Plain-text value of this node.
- String get value => _value;
- String _value;
-
- /// 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.
- LeafNode splitAt(int index) {
- assert(index >= 0 && index <= length);
- if (index == 0) return this;
- if (index == length && isLast) return null;
- if (index == length && !isLast) return next;
-
- String text = _value;
- _value = text.substring(0, index);
- final split = new LeafNode(text.substring(index));
- split.applyStyle(style);
- insertAfter(split);
- return split;
- }
-
- /// Cuts a leaf node 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`.
- LeafNode cutAt(int index) {
- assert(index >= 0 && index <= length);
- LeafNode cut = splitAt(index);
- cut?.unlink();
- return cut;
- }
-
- /// Isolates a new leaf node 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. Note that returned node may still be the same as this node
- /// if provided [index] is `0`.
- LeafNode isolate(int index, int length) {
- assert(
- index >= 0 && index < this.length && (index + length <= this.length),
- 'Index or length is out of bounds. Index: $index, length: $length. '
- 'Actual node length: ${this.length}.');
- // Since `index < this.length` (guarded by assert) below line
- // always returns a new node.
- LeafNode target = splitAt(index);
- target.splitAt(length);
- return target;
- }
-
- /// Formats this node and optimizes it with adjacent leaf nodes if needed.
- void formatAndOptimize(NotusStyle style) {
- if (style != null && style.isNotEmpty) {
- applyStyle(style);
- }
- optimize();
- }
-
- @override
- void applyStyle(NotusStyle value) {
- assert(value != null && (value.isInline || value.isEmpty),
- "Style cannot be applied to this leaf node: $value");
- assert(() {
- if (value.contains(NotusAttribute.embed)) {
- if (value.get(NotusAttribute.embed) == NotusAttribute.embed.unset) {
- throw 'Unsetting embed attribute is not allowed. '
- 'This operation means that the embed itself must be deleted from the document. '
- 'Make sure there is FormatEmbedsRule in your heuristics registry, '
- 'which is responsible for handling this scenario.';
- }
- if (this is! EmbedNode) {
- throw 'Embed style can only be applied to an EmbedNode.';
- }
- }
- return true;
- }());
-
- super.applyStyle(value);
- }
-
- @override
- LineNode get parent => super.parent;
-
- @override
- int get length => _value.length;
-
- @override
- Delta toDelta() {
- return new Delta()..insert(_value, style.toJson());
- }
-
- @override
- String toPlainText() => _value;
-
- @override
- void insert(int index, String value, NotusStyle style) {
- assert(index >= 0 && (index <= length), 'Index: $index, Length: $length.');
- assert(value.isNotEmpty);
- final node = new LeafNode(value);
- if (index == length) {
- insertAfter(node);
- } else {
- splitAt(index).insertBefore(node);
- }
- node.formatAndOptimize(style);
- }
-
- @override
- void retain(int index, int length, NotusStyle style) {
- if (style == null) return;
-
- final local = math.min(this.length - index, length);
- final node = isolate(index, local);
-
- int remaining = length - local;
- if (remaining > 0) {
- assert(node.next != null);
- node.next.retain(0, remaining, style);
- }
- // Optimize at the very end
- node.formatAndOptimize(style);
- }
-
- @override
- void delete(int index, int length) {
- assert(index < this.length);
-
- final local = math.min(this.length - index, length);
- final target = isolate(index, local);
- // Memorize siblings before un-linking.
- final needsOptimize = target.previous;
- final actualNext = target.next;
- target.unlink();
-
- int remaining = length - local;
- if (remaining > 0) {
- assert(actualNext != null);
- actualNext.delete(0, remaining);
- }
-
- if (needsOptimize != null) needsOptimize.optimize();
- }
-
- @override
- String toString() {
- final keys = style.keys.toList(growable: false)..sort();
- String styleKeys = keys.join();
- return "⟨$value⟩$styleKeys";
- }
-
- /// Optimizes this text node by merging it with adjacent nodes if they share
- /// the same style.
- @override
- void optimize() {
- LeafNode node = this;
- if (!node.isFirst) {
- LeafNode mergeWith = node.previous;
- if (mergeWith.style == node.style) {
- mergeWith._value += node.value;
- node.unlink();
- node = mergeWith;
- }
- }
- if (!node.isLast) {
- LeafNode mergeWith = node.next;
- if (mergeWith.style == node.style) {
- node._value += mergeWith._value;
- mergeWith.unlink();
- }
- }
- }
- }
-
- /// A span of formatted text within a line in a Notus document.
- ///
- /// TextNode is a leaf node of a document tree.
- ///
- /// Parent of a text node is always a [LineNode], and as a consequence text
- /// node's [value] cannot contain any line-break characters.
- ///
- /// See also:
- ///
- /// * [LineNode], a node representing a line of text.
- /// * [BlockNode], a node representing a group of lines.
- class TextNode extends LeafNode {
- TextNode([String content = '']) : super._(content);
- }
-
- final kZeroWidthSpace = new String.fromCharCode(0x200b);
-
- /// An embed node inside of a line in a Notus document.
- ///
- /// Embed node is a leaf node similar to [TextNode]. It represents an
- /// arbitrary piece of non-text content embedded into a document, such as,
- /// image, horizontal rule, video, or any other object with defined structure,
- /// like tweet, for instance.
- ///
- /// Embed node's length is always `1` character and it is represented with
- /// zero-width space 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 EmbedNode extends LeafNode {
- static final kPlainTextPlaceholder = new String.fromCharCode(0x200b);
-
- EmbedNode() : super._(kPlainTextPlaceholder);
- }
|