zefyr

format_rules.dart 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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 'package:notus/notus.dart';
  5. import 'package:quill_delta/quill_delta.dart';
  6. /// A heuristic rule for format (retain) operations.
  7. abstract class FormatRule {
  8. /// Constant constructor allows subclasses to declare constant constructors.
  9. const FormatRule();
  10. /// Applies heuristic rule to a retain (format) operation on a [document] and
  11. /// returns resulting [Delta].
  12. Delta apply(Delta document, int index, int length, NotusAttribute attribute);
  13. }
  14. /// Produces Delta with line-level attributes applied strictly to
  15. /// line-break characters.
  16. class ResolveLineFormatRule extends FormatRule {
  17. const ResolveLineFormatRule() : super();
  18. @override
  19. Delta apply(Delta document, int index, int length, NotusAttribute attribute) {
  20. if (attribute.scope != NotusAttributeScope.line) return null;
  21. var result = Delta()..retain(index);
  22. final iter = DeltaIterator(document);
  23. iter.skip(index);
  24. // Apply line styles to all line-break characters within range of this
  25. // retain operation.
  26. var current = 0;
  27. while (current < length && iter.hasNext) {
  28. final op = iter.next(length - current);
  29. if (op.data.contains('\n')) {
  30. final delta = _applyAttribute(op.data, attribute);
  31. result = result.concat(delta);
  32. } else {
  33. result.retain(op.length);
  34. }
  35. current += op.length;
  36. }
  37. // And include extra line-break after retain
  38. while (iter.hasNext) {
  39. final op = iter.next();
  40. final lf = op.data.indexOf('\n');
  41. if (lf == -1) {
  42. result..retain(op.length);
  43. continue;
  44. }
  45. result..retain(lf)..retain(1, attribute.toJson());
  46. break;
  47. }
  48. return result;
  49. }
  50. Delta _applyAttribute(String text, NotusAttribute attribute) {
  51. final result = Delta();
  52. var offset = 0;
  53. var lf = text.indexOf('\n');
  54. while (lf >= 0) {
  55. result..retain(lf - offset)..retain(1, attribute.toJson());
  56. offset = lf + 1;
  57. lf = text.indexOf('\n', offset);
  58. }
  59. // Retain any remaining characters in text
  60. result.retain(text.length - offset);
  61. return result;
  62. }
  63. }
  64. /// Produces Delta with inline-level attributes applied too all characters
  65. /// except line-breaks.
  66. class ResolveInlineFormatRule extends FormatRule {
  67. const ResolveInlineFormatRule();
  68. @override
  69. Delta apply(Delta document, int index, int length, NotusAttribute attribute) {
  70. if (attribute.scope != NotusAttributeScope.inline) return null;
  71. final result = Delta()..retain(index);
  72. final iter = DeltaIterator(document);
  73. iter.skip(index);
  74. // Apply inline styles to all non-line-break characters within range of this
  75. // retain operation.
  76. var current = 0;
  77. while (current < length && iter.hasNext) {
  78. final op = iter.next(length - current);
  79. var lf = op.data.indexOf('\n');
  80. if (lf != -1) {
  81. var pos = 0;
  82. while (lf != -1) {
  83. result..retain(lf - pos, attribute.toJson())..retain(1);
  84. pos = lf + 1;
  85. lf = op.data.indexOf('\n', pos);
  86. }
  87. if (pos < op.length) result.retain(op.length - pos, attribute.toJson());
  88. } else {
  89. result.retain(op.length, attribute.toJson());
  90. }
  91. current += op.length;
  92. }
  93. return result;
  94. }
  95. }
  96. /// Allows updating link format with collapsed selection.
  97. class FormatLinkAtCaretPositionRule extends FormatRule {
  98. const FormatLinkAtCaretPositionRule();
  99. @override
  100. Delta apply(Delta document, int index, int length, NotusAttribute attribute) {
  101. if (attribute.key != NotusAttribute.link.key) return null;
  102. // If user selection is not collapsed we let it fallback to default rule
  103. // which simply applies the attribute to selected range.
  104. // This may still not be a bulletproof approach as selection can span
  105. // multiple lines or be a subset of existing link-formatted text.
  106. // So certain improvements can be made in the future to account for such
  107. // edge cases.
  108. if (length != 0) return null;
  109. final result = Delta();
  110. final iter = DeltaIterator(document);
  111. final before = iter.skip(index);
  112. final after = iter.next();
  113. var startIndex = index;
  114. var retain = 0;
  115. if (before != null && before.hasAttribute(attribute.key)) {
  116. startIndex -= before.length;
  117. retain = before.length;
  118. }
  119. if (after != null && after.hasAttribute(attribute.key)) {
  120. retain += after.length;
  121. }
  122. // There is no link-styled text around `index` position so it becomes a
  123. // no-op action.
  124. if (retain == 0) return null;
  125. result..retain(startIndex)..retain(retain, attribute.toJson());
  126. return result;
  127. }
  128. }
  129. /// Handles all format operations which manipulate embeds.
  130. class FormatEmbedsRule extends FormatRule {
  131. const FormatEmbedsRule();
  132. @override
  133. Delta apply(Delta document, int index, int length, NotusAttribute attribute) {
  134. // We are only interested in embed attributes
  135. if (attribute is! EmbedAttribute) return null;
  136. EmbedAttribute embed = attribute;
  137. if (length == 1 && embed.isUnset) {
  138. // Remove the embed.
  139. return Delta()
  140. ..retain(index)
  141. ..delete(length);
  142. } else {
  143. // If length is 0 we treat it as an insert at specified [index].
  144. // If length is non-zero we treat it as a replace of selected range
  145. // with the embed.
  146. assert(!embed.isUnset);
  147. return _insertEmbed(document, index, length, embed);
  148. }
  149. }
  150. Delta _insertEmbed(
  151. Delta document, int index, int length, EmbedAttribute embed) {
  152. final result = Delta()..retain(index);
  153. final iter = DeltaIterator(document);
  154. final previous = iter.skip(index);
  155. iter.skip(length); // ignore deleted part.
  156. final target = iter.next();
  157. // Check if [index] is on an empty line already.
  158. final isNewlineBefore = previous == null || previous.data.endsWith('\n');
  159. final isNewlineAfter = target.data.startsWith('\n');
  160. final isOnEmptyLine = isNewlineBefore && isNewlineAfter;
  161. if (isOnEmptyLine) {
  162. return result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
  163. }
  164. // We are on a non-empty line, split it (preserving style if needed)
  165. // and insert our embed.
  166. final lineStyle = _getLineStyle(iter, target);
  167. if (!isNewlineBefore) {
  168. result..insert('\n', lineStyle);
  169. }
  170. result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
  171. if (!isNewlineAfter) {
  172. result..insert('\n');
  173. }
  174. result.delete(length);
  175. return result;
  176. }
  177. Map<String, dynamic> _getLineStyle(
  178. DeltaIterator iterator, Operation current) {
  179. if (current.data.contains('\n')) {
  180. return current.attributes;
  181. }
  182. // Continue looking for line-break.
  183. Map<String, dynamic> attributes;
  184. while (iterator.hasNext) {
  185. final op = iterator.next();
  186. final lf = op.data.indexOf('\n');
  187. if (lf >= 0) {
  188. attributes = op.attributes;
  189. break;
  190. }
  191. }
  192. return attributes;
  193. }
  194. }