zefyr

line.dart 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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 'block.dart';
  8. import 'leaf.dart';
  9. import 'node.dart';
  10. /// A line of rich text in a Notus document.
  11. ///
  12. /// LineNode serves as a container for [LeafNode]s, like [TextNode] and
  13. /// [EmbedNode].
  14. ///
  15. /// When a line contains an embed, it fully occupies the line, no other embeds
  16. /// or text nodes are allowed.
  17. class LineNode extends ContainerNode<LeafNode>
  18. with StyledNodeMixin
  19. implements StyledNode {
  20. /// Returns `true` if this line contains an embed.
  21. bool get hasEmbed {
  22. if (childCount == 1) {
  23. return children.single is EmbedNode;
  24. }
  25. assert(children.every((child) => child is TextNode));
  26. return false;
  27. }
  28. /// Returns next [LineNode] or `null` if this is the last line in the document.
  29. LineNode get nextLine {
  30. if (isLast) {
  31. if (parent is BlockNode) {
  32. if (parent.isLast) return null;
  33. LineNode line = (parent.next is BlockNode)
  34. ? (parent.next as BlockNode).first
  35. : parent.next;
  36. return line;
  37. } else {
  38. return null;
  39. }
  40. } else {
  41. LineNode line = (next is BlockNode) ? (next as BlockNode).first : next;
  42. return line;
  43. }
  44. }
  45. /// Creates new empty [LineNode] with the same style.
  46. LineNode clone() {
  47. final node = LineNode();
  48. node.applyStyle(style);
  49. return node;
  50. }
  51. /// Splits this line into two at specified character [index].
  52. ///
  53. /// This is an equivalent of inserting a line-break character at [index].
  54. LineNode splitAt(int index) {
  55. assert(index == 0 || (index > 0 && index < length),
  56. 'Index is out of bounds. Index: $index. Actual node length: ${length}.');
  57. final line = clone();
  58. insertAfter(line);
  59. if (index == length - 1) return line;
  60. final split = lookup(index);
  61. while (!split.node.isLast) {
  62. LeafNode child = last;
  63. child.unlink();
  64. line.addFirst(child);
  65. }
  66. LeafNode child = split.node;
  67. line.addFirst(child.cutAt(split.offset));
  68. return line;
  69. }
  70. /// Unwraps this line from it's parent [BlockNode].
  71. ///
  72. /// This method asserts if current [parent] of this line is not a [BlockNode].
  73. void unwrap() {
  74. assert(parent is BlockNode);
  75. BlockNode block = parent;
  76. block.unwrapLine(this);
  77. }
  78. /// Wraps this line with new parent [block].
  79. ///
  80. /// This line can not be in a [BlockNode] when this method is called.
  81. void wrap(BlockNode block) {
  82. assert(parent != null && parent is! BlockNode);
  83. insertAfter(block);
  84. unlink();
  85. block.add(this);
  86. }
  87. /// Returns style for specified text range.
  88. ///
  89. /// Only attributes applied to all characters within this range are
  90. /// included in the result. Inline and line level attributes are
  91. /// handled separately, e.g.:
  92. ///
  93. /// - line attribute X is included in the result only if it exists for
  94. /// every line within this range (partially included lines are counted).
  95. /// - inline attribute X is included in the result only if it exists
  96. /// for every character within this range (line-break characters excluded).
  97. NotusStyle collectStyle(int offset, int length) {
  98. final local = math.min(this.length - offset, length);
  99. var result = NotusStyle();
  100. final excluded = <NotusAttribute>{};
  101. void _handle(NotusStyle style) {
  102. if (result.isEmpty) {
  103. excluded.addAll(style.values);
  104. } else {
  105. for (var attr in result.values) {
  106. if (!style.contains(attr)) {
  107. excluded.add(attr);
  108. }
  109. }
  110. }
  111. final remaining = style.removeAll(excluded);
  112. result = result.removeAll(excluded);
  113. result = result.mergeAll(remaining);
  114. }
  115. final data = lookup(offset, inclusive: true);
  116. LeafNode node = data.node;
  117. if (node != null) {
  118. result = result.mergeAll(node.style);
  119. var pos = node.length - data.offset;
  120. while (!node.isLast && pos < local) {
  121. node = node.next as LeafNode;
  122. _handle(node.style);
  123. pos += node.length;
  124. }
  125. }
  126. result = result.mergeAll(style);
  127. if (parent is BlockNode) {
  128. BlockNode block = parent;
  129. result = result.mergeAll(block.style);
  130. }
  131. final remaining = length - local;
  132. if (remaining > 0) {
  133. final rest = nextLine.collectStyle(0, remaining);
  134. _handle(rest);
  135. }
  136. return result;
  137. }
  138. @override
  139. LeafNode get defaultChild => TextNode();
  140. // TODO: should be able to cache length and invalidate on any child-related operation
  141. @override
  142. int get length => super.length + 1;
  143. @override
  144. Delta toDelta() {
  145. final delta = children
  146. .map((text) => text.toDelta())
  147. .fold(Delta(), (a, b) => a.concat(b));
  148. var attributes = style;
  149. if (parent is BlockNode) {
  150. BlockNode block = parent;
  151. attributes = attributes.mergeAll(block.style);
  152. }
  153. delta.insert('\n', attributes.toJson());
  154. return delta;
  155. }
  156. @override
  157. String toPlainText() => super.toPlainText() + '\n';
  158. @override
  159. String toString() {
  160. final body = children.join(' → ');
  161. final styleString = style.isNotEmpty ? ' $style' : '';
  162. return '¶ $body ⏎$styleString';
  163. }
  164. @override
  165. void optimize() {
  166. // No-op, line merging is done in insert/delete operations
  167. }
  168. @override
  169. void insert(int index, String text, NotusStyle style) {
  170. final lf = text.indexOf('\n');
  171. if (lf == -1) {
  172. _insertSafe(index, text, style);
  173. // No need to update line or block format since those attributes can only
  174. // be attached to `\n` character and we already know it's not present.
  175. return;
  176. }
  177. final substring = text.substring(0, lf);
  178. _insertSafe(index, substring, style);
  179. if (substring.isNotEmpty) index += substring.length;
  180. final nextLine = splitAt(index); // Next line inherits our format.
  181. // Reset our format and unwrap from a block if needed.
  182. clearStyle();
  183. if (parent is BlockNode) unwrap();
  184. // Now we can apply new format and re-layout.
  185. _formatAndOptimize(style);
  186. // Continue with remaining part.
  187. final remaining = text.substring(lf + 1);
  188. nextLine.insert(0, remaining, style);
  189. }
  190. @override
  191. void retain(int index, int length, NotusStyle style) {
  192. if (style == null) return;
  193. final thisLength = this.length;
  194. final local = math.min(thisLength - index, length);
  195. // If index is at line-break character this is line/block format update.
  196. final isLineFormat = (index + local == thisLength) && local == 1;
  197. if (isLineFormat) {
  198. assert(
  199. style.values.every((attr) => attr.scope == NotusAttributeScope.line),
  200. 'It is not allowed to apply inline attributes to line itself.');
  201. _formatAndOptimize(style);
  202. } else {
  203. // otherwise forward to children as it's inline format update.
  204. assert(index + local != thisLength,
  205. 'It is not allowed to apply inline attributes to line itself.');
  206. assert(style.values
  207. .every((attr) => attr.scope == NotusAttributeScope.inline));
  208. super.retain(index, local, style);
  209. }
  210. final remaining = length - local;
  211. if (remaining > 0) {
  212. assert(nextLine != null);
  213. nextLine.retain(0, remaining, style);
  214. }
  215. }
  216. @override
  217. void delete(int index, int length) {
  218. final local = math.min(this.length - index, length);
  219. final isLFDeleted = (index + local == this.length);
  220. if (isLFDeleted) {
  221. // Our line-break deleted with all style information.
  222. clearStyle();
  223. if (local > 1) {
  224. // Exclude line-break from delete range for children.
  225. super.delete(index, local - 1);
  226. }
  227. } else {
  228. super.delete(index, local);
  229. }
  230. final remaining = length - local;
  231. if (remaining > 0) {
  232. assert(nextLine != null);
  233. nextLine.delete(0, remaining);
  234. }
  235. if (isLFDeleted && isNotEmpty) {
  236. // Since we lost our line-break and still have child text nodes those must
  237. // migrate to the next line.
  238. // nextLine might have been unmounted since last assert so we need to
  239. // check again we still have a line after us.
  240. assert(nextLine != null);
  241. // Move remaining stuff in this line to next line so that all attributes
  242. // of nextLine are preserved.
  243. nextLine.moveChildren(this); // TODO: avoid double move
  244. moveChildren(nextLine);
  245. }
  246. if (isLFDeleted) {
  247. // Now we can remove this line.
  248. final block = parent; // remember reference before un-linking.
  249. unlink();
  250. block.optimize();
  251. }
  252. }
  253. /// Formats this line and optimizes layout afterwards.
  254. void _formatAndOptimize(NotusStyle newStyle) {
  255. if (newStyle == null || newStyle.isEmpty) return;
  256. applyStyle(newStyle);
  257. if (!newStyle.contains(NotusAttribute.block)) {
  258. return;
  259. } // no block-level changes
  260. final blockStyle = newStyle.get(NotusAttribute.block);
  261. if (parent is BlockNode) {
  262. final parentStyle = (parent as BlockNode).style.get(NotusAttribute.block);
  263. if (blockStyle == NotusAttribute.block.unset) {
  264. unwrap();
  265. } else if (blockStyle != parentStyle) {
  266. unwrap();
  267. final block = BlockNode();
  268. block.applyAttribute(blockStyle);
  269. wrap(block);
  270. block.optimize();
  271. } // else the same style, no-op.
  272. } else if (blockStyle != NotusAttribute.block.unset) {
  273. // Only wrap with a new block if this is not an unset
  274. final block = BlockNode();
  275. block.applyAttribute(blockStyle);
  276. wrap(block);
  277. block.optimize();
  278. }
  279. }
  280. void _insertSafe(int index, String text, NotusStyle style) {
  281. assert(index == 0 || (index > 0 && index < length));
  282. assert(text.contains('\n') == false);
  283. if (text.isEmpty) return;
  284. if (isEmpty) {
  285. final child = LeafNode(text);
  286. add(child);
  287. child.formatAndOptimize(style);
  288. } else {
  289. final result = lookup(index, inclusive: true);
  290. result.node.insert(result.offset, text, style);
  291. }
  292. }
  293. }