zefyr

leaf.dart 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'dart:math' as math;
  5. import 'package:quill_delta/quill_delta.dart';
  6. import 'attributes.dart';
  7. import 'line.dart';
  8. import 'node.dart';
  9. /// A leaf node in Notus document tree.
  10. abstract class LeafNode extends Node
  11. with StyledNodeMixin
  12. implements StyledNode {
  13. /// Creates a new [LeafNode] with specified [value].
  14. LeafNode._([String value = ''])
  15. : assert(value != null && !value.contains('\n')),
  16. _value = value;
  17. factory LeafNode([String value = '']) {
  18. LeafNode node;
  19. if (value == kZeroWidthSpace) {
  20. // Zero-width space is reserved for embed nodes.
  21. node = EmbedNode();
  22. } else {
  23. assert(
  24. !value.contains(kZeroWidthSpace),
  25. 'Zero-width space is reserved for embed leaf nodes and cannot be used '
  26. 'inside regular text nodes.');
  27. node = TextNode(value);
  28. }
  29. return node;
  30. }
  31. /// Plain-text value of this node.
  32. String get value => _value;
  33. String _value;
  34. /// Splits this leaf node at [index] and returns new node.
  35. ///
  36. /// If this is the last node in its list and [index] equals this node's
  37. /// length then this method returns `null` as there is nothing left to split.
  38. /// If there is another leaf node after this one and [index] equals this
  39. /// node's length then the next leaf node is returned.
  40. ///
  41. /// If [index] equals to `0` then this node itself is returned unchanged.
  42. ///
  43. /// In case a new node is actually split from this one, it inherits this
  44. /// node's style.
  45. LeafNode splitAt(int index) {
  46. assert(index >= 0 && index <= length);
  47. if (index == 0) return this;
  48. if (index == length && isLast) return null;
  49. if (index == length && !isLast) return next as LeafNode;
  50. final text = _value;
  51. _value = text.substring(0, index);
  52. final split = LeafNode(text.substring(index));
  53. split.applyStyle(style);
  54. insertAfter(split);
  55. return split;
  56. }
  57. /// Cuts a leaf node from [index] to the end of this node and returns new node
  58. /// in detached state (e.g. [mounted] returns `false`).
  59. ///
  60. /// Splitting logic is identical to one described in [splitAt], meaning this
  61. /// method may return `null`.
  62. LeafNode cutAt(int index) {
  63. assert(index >= 0 && index <= length);
  64. final cut = splitAt(index);
  65. cut?.unlink();
  66. return cut;
  67. }
  68. /// Isolates a new leaf node starting at [index] with specified [length].
  69. ///
  70. /// Splitting logic is identical to one described in [splitAt], with one
  71. /// exception that it is required for [index] to always be less than this
  72. /// node's length. As a result this method always returns a [LeafNode]
  73. /// instance. Note that returned node may still be the same as this node
  74. /// if provided [index] is `0`.
  75. LeafNode isolate(int index, int length) {
  76. assert(
  77. index >= 0 && index < this.length && (index + length <= this.length),
  78. 'Index or length is out of bounds. Index: $index, length: $length. '
  79. 'Actual node length: ${this.length}.');
  80. // Since `index < this.length` (guarded by assert) below line
  81. // always returns a new node.
  82. final target = splitAt(index);
  83. target.splitAt(length);
  84. return target;
  85. }
  86. /// Formats this node and optimizes it with adjacent leaf nodes if needed.
  87. void formatAndOptimize(NotusStyle style) {
  88. if (style != null && style.isNotEmpty) {
  89. applyStyle(style);
  90. }
  91. optimize();
  92. }
  93. @override
  94. void applyStyle(NotusStyle value) {
  95. assert(value != null && (value.isInline || value.isEmpty),
  96. 'Style cannot be applied to this leaf node: $value');
  97. assert(() {
  98. if (value.contains(NotusAttribute.embed)) {
  99. if (value.get(NotusAttribute.embed) == NotusAttribute.embed.unset) {
  100. throw 'Unsetting embed attribute is not allowed. '
  101. 'This operation means that the embed itself must be deleted from the document. '
  102. 'Make sure there is FormatEmbedsRule in your heuristics registry, '
  103. 'which is responsible for handling this scenario.';
  104. }
  105. if (this is! EmbedNode) {
  106. throw 'Embed style can only be applied to an EmbedNode.';
  107. }
  108. }
  109. return true;
  110. }());
  111. super.applyStyle(value);
  112. }
  113. @override
  114. LineNode get parent => super.parent as LineNode;
  115. @override
  116. int get length => _value.length;
  117. @override
  118. Delta toDelta() {
  119. return Delta()..insert(_value, style.toJson());
  120. }
  121. @override
  122. String toPlainText() => _value;
  123. @override
  124. void insert(int index, String value, NotusStyle style) {
  125. assert(index >= 0 && (index <= length), 'Index: $index, Length: $length.');
  126. assert(value.isNotEmpty);
  127. final node = LeafNode(value);
  128. if (index == length) {
  129. insertAfter(node);
  130. } else {
  131. splitAt(index).insertBefore(node);
  132. }
  133. node.formatAndOptimize(style);
  134. }
  135. @override
  136. void retain(int index, int length, NotusStyle style) {
  137. if (style == null) return;
  138. final local = math.min(this.length - index, length);
  139. final node = isolate(index, local);
  140. final remaining = length - local;
  141. if (remaining > 0) {
  142. assert(node.next != null);
  143. node.next.retain(0, remaining, style);
  144. }
  145. // Optimize at the very end
  146. node.formatAndOptimize(style);
  147. }
  148. @override
  149. void delete(int index, int length) {
  150. assert(index < this.length);
  151. final local = math.min(this.length - index, length);
  152. final target = isolate(index, local);
  153. // Memorize siblings before un-linking.
  154. final needsOptimize = target.previous;
  155. final actualNext = target.next;
  156. target.unlink();
  157. final remaining = length - local;
  158. if (remaining > 0) {
  159. assert(actualNext != null);
  160. actualNext.delete(0, remaining);
  161. }
  162. if (needsOptimize != null) needsOptimize.optimize();
  163. }
  164. @override
  165. String toString() {
  166. final keys = style.keys.toList(growable: false)..sort();
  167. final styleKeys = keys.join();
  168. return '⟨$value⟩$styleKeys';
  169. }
  170. /// Optimizes this text node by merging it with adjacent nodes if they share
  171. /// the same style.
  172. @override
  173. void optimize() {
  174. var node = this;
  175. if (!node.isFirst) {
  176. LeafNode mergeWith = node.previous;
  177. if (mergeWith.style == node.style) {
  178. mergeWith._value += node.value;
  179. node.unlink();
  180. node = mergeWith;
  181. }
  182. }
  183. if (!node.isLast) {
  184. LeafNode mergeWith = node.next;
  185. if (mergeWith.style == node.style) {
  186. node._value += mergeWith._value;
  187. mergeWith.unlink();
  188. }
  189. }
  190. }
  191. }
  192. /// A span of formatted text within a line in a Notus document.
  193. ///
  194. /// TextNode is a leaf node of a document tree.
  195. ///
  196. /// Parent of a text node is always a [LineNode], and as a consequence text
  197. /// node's [value] cannot contain any line-break characters.
  198. ///
  199. /// See also:
  200. ///
  201. /// * [LineNode], a node representing a line of text.
  202. /// * [BlockNode], a node representing a group of lines.
  203. class TextNode extends LeafNode {
  204. TextNode([String content = '']) : super._(content);
  205. }
  206. final kZeroWidthSpace = String.fromCharCode(0x200b);
  207. /// An embed node inside of a line in a Notus document.
  208. ///
  209. /// Embed node is a leaf node similar to [TextNode]. It represents an
  210. /// arbitrary piece of non-text content embedded into a document, such as,
  211. /// image, horizontal rule, video, or any other object with defined structure,
  212. /// like tweet, for instance.
  213. ///
  214. /// Embed node's length is always `1` character and it is represented with
  215. /// zero-width space in the document text.
  216. ///
  217. /// Any inline style can be applied to an embed, however this does not
  218. /// necessarily mean the embed will look according to that style. For instance,
  219. /// applying "bold" style to an image gives no effect, while adding a "link" to
  220. /// an image actually makes the image react to user's action.
  221. class EmbedNode extends LeafNode {
  222. static final kPlainTextPlaceholder = String.fromCharCode(0x200b);
  223. EmbedNode() : super._(kPlainTextPlaceholder);
  224. }