zefyr

insert_rules.dart 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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 insert operations.
  7. abstract class InsertRule {
  8. /// Constant constructor allows subclasses to declare constant constructors.
  9. const InsertRule();
  10. /// Applies heuristic rule to an insert operation on a [document] and returns
  11. /// resulting [Delta].
  12. Delta apply(Delta document, int index, String text);
  13. }
  14. /// Fallback rule which simply inserts text as-is without any special handling.
  15. class CatchAllInsertRule extends InsertRule {
  16. const CatchAllInsertRule();
  17. @override
  18. Delta apply(Delta document, int index, String text) {
  19. return Delta()
  20. ..retain(index)
  21. ..insert(text);
  22. }
  23. }
  24. /// Preserves line format when user splits the line into two.
  25. ///
  26. /// This rule ignores scenarios when the line is split on its edge, meaning
  27. /// a line-break is inserted at the beginning or the end of the line.
  28. class PreserveLineStyleOnSplitRule extends InsertRule {
  29. const PreserveLineStyleOnSplitRule();
  30. bool isEdgeLineSplit(Operation before, Operation after) {
  31. if (before == null) return true; // split at the beginning of a doc
  32. return before.data.endsWith('\n') || after.data.startsWith('\n');
  33. }
  34. @override
  35. Delta apply(Delta document, int index, String text) {
  36. if (text != '\n') return null;
  37. final iter = DeltaIterator(document);
  38. final before = iter.skip(index);
  39. final after = iter.next();
  40. if (isEdgeLineSplit(before, after)) return null;
  41. final result = Delta()..retain(index);
  42. if (after.data.contains('\n')) {
  43. // It is not allowed to combine line and inline styles in insert
  44. // operation containing line-break together with other characters.
  45. // The only scenario we get such operation is when the text is plain.
  46. assert(after.isPlain);
  47. // No attributes to apply so we simply create a new line.
  48. result.insert('\n');
  49. return result;
  50. }
  51. // Continue looking for line-break.
  52. Map<String, dynamic> attributes;
  53. while (iter.hasNext) {
  54. final op = iter.next();
  55. final lf = op.data.indexOf('\n');
  56. if (lf >= 0) {
  57. attributes = op.attributes;
  58. break;
  59. }
  60. }
  61. result.insert('\n', attributes);
  62. return result;
  63. }
  64. }
  65. /// of a line (right before a line-break).
  66. /// Resets format for a newly inserted line when insert occurred at the end
  67. class ResetLineFormatOnNewLineRule extends InsertRule {
  68. const ResetLineFormatOnNewLineRule();
  69. @override
  70. Delta apply(Delta document, int index, String text) {
  71. if (text != '\n') return null;
  72. final iter = DeltaIterator(document);
  73. iter.skip(index);
  74. final target = iter.next();
  75. if (target.data.startsWith('\n')) {
  76. Map<String, dynamic> resetStyle;
  77. if (target.attributes != null &&
  78. target.attributes.containsKey(NotusAttribute.heading.key)) {
  79. resetStyle = NotusAttribute.heading.unset.toJson();
  80. }
  81. return Delta()
  82. ..retain(index)
  83. ..insert('\n', target.attributes)
  84. ..retain(1, resetStyle)
  85. ..trim();
  86. }
  87. return null;
  88. }
  89. }
  90. /// Heuristic rule to exit current block when user inserts two consecutive
  91. /// line-breaks.
  92. // TODO: update this rule to handle code blocks differently, at least allow 3 consecutive line-breaks before exiting.
  93. class AutoExitBlockRule extends InsertRule {
  94. const AutoExitBlockRule();
  95. bool isEmptyLine(Operation previous, Operation target) {
  96. return (previous == null || previous.data.endsWith('\n')) &&
  97. target.data.startsWith('\n');
  98. }
  99. @override
  100. Delta apply(Delta document, int index, String text) {
  101. if (text != '\n') return null;
  102. final iter = DeltaIterator(document);
  103. final previous = iter.skip(index);
  104. final target = iter.next();
  105. final isInBlock = target.isNotPlain &&
  106. target.attributes.containsKey(NotusAttribute.block.key);
  107. if (isEmptyLine(previous, target) && isInBlock) {
  108. // We reset block style even if this line is not the last one in it's
  109. // block which effectively splits the block into two.
  110. // TODO: For code blocks this should not split the block but allow inserting as many lines as needed.
  111. var attributes;
  112. if (target.attributes != null) {
  113. attributes = target.attributes;
  114. } else {
  115. attributes = <String, dynamic>{};
  116. }
  117. attributes.addAll(NotusAttribute.block.unset.toJson());
  118. return Delta()..retain(index)..retain(1, attributes);
  119. }
  120. return null;
  121. }
  122. }
  123. /// Preserves inline styles when user inserts text inside formatted segment.
  124. class PreserveInlineStylesRule extends InsertRule {
  125. const PreserveInlineStylesRule();
  126. @override
  127. Delta apply(Delta document, int index, String text) {
  128. // This rule is only applicable to characters other than line-break.
  129. if (text.contains('\n')) return null;
  130. final iter = DeltaIterator(document);
  131. final previous = iter.skip(index);
  132. // If there is a line-break in previous chunk, there should be no inline
  133. // styles. Also if there is no previous operation we are at the beginning
  134. // of the document so no styles to inherit from.
  135. if (previous == null || previous.data.contains('\n')) return null;
  136. final attributes = previous.attributes;
  137. final hasLink =
  138. (attributes != null && attributes.containsKey(NotusAttribute.link.key));
  139. if (!hasLink) {
  140. return Delta()
  141. ..retain(index)
  142. ..insert(text, attributes);
  143. }
  144. // Special handling needed for inserts inside fragments with link attribute.
  145. // Link style should only be preserved if insert occurs inside the fragment.
  146. // Link style should NOT be preserved on the boundaries.
  147. var noLinkAttributes = previous.attributes;
  148. noLinkAttributes.remove(NotusAttribute.link.key);
  149. final noLinkResult = Delta()
  150. ..retain(index)
  151. ..insert(text, noLinkAttributes.isEmpty ? null : noLinkAttributes);
  152. final next = iter.next();
  153. if (next == null) {
  154. // Nothing after us, we are not inside link-styled fragment.
  155. return noLinkResult;
  156. }
  157. final nextAttributes = next.attributes ?? <String, dynamic>{};
  158. if (!nextAttributes.containsKey(NotusAttribute.link.key)) {
  159. // Next fragment is not styled as link.
  160. return noLinkResult;
  161. }
  162. // We must make sure links are identical in previous and next operations.
  163. if (attributes[NotusAttribute.link.key] ==
  164. nextAttributes[NotusAttribute.link.key]) {
  165. return Delta()
  166. ..retain(index)
  167. ..insert(text, attributes);
  168. } else {
  169. return noLinkResult;
  170. }
  171. }
  172. }
  173. /// Applies link format to text segment (which looks like a link) when user
  174. /// inserts space character after it.
  175. class AutoFormatLinksRule extends InsertRule {
  176. const AutoFormatLinksRule();
  177. @override
  178. Delta apply(Delta document, int index, String text) {
  179. // This rule applies to a space inserted after a link, so we can ignore
  180. // everything else.
  181. if (text != ' ') return null;
  182. final iter = DeltaIterator(document);
  183. final previous = iter.skip(index);
  184. // No previous operation means no link.
  185. if (previous == null) return null;
  186. // Split text of previous operation in lines and words and take last word to test.
  187. final candidate = previous.data.split('\n').last.split(' ').last;
  188. try {
  189. final link = Uri.parse(candidate);
  190. if (!['https', 'http'].contains(link.scheme)) {
  191. // TODO: might need a more robust way of validating links here.
  192. return null;
  193. }
  194. final attributes = previous.attributes ?? <String, dynamic>{};
  195. // Do nothing if already formatted as link.
  196. if (attributes.containsKey(NotusAttribute.link.key)) return null;
  197. attributes
  198. .addAll(NotusAttribute.link.fromString(link.toString()).toJson());
  199. return Delta()
  200. ..retain(index - candidate.length)
  201. ..retain(candidate.length, attributes)
  202. ..insert(text, previous.attributes);
  203. } on FormatException {
  204. return null; // Our candidate is not a link.
  205. }
  206. }
  207. }
  208. /// Forces text inserted on the same line with an embed (before or after it)
  209. /// to be moved to a new line adjacent to the original line.
  210. ///
  211. /// This rule assumes that a line is only allowed to have single embed child.
  212. class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
  213. const ForceNewlineForInsertsAroundEmbedRule();
  214. @override
  215. Delta apply(Delta document, int index, String text) {
  216. final iter = DeltaIterator(document);
  217. final previous = iter.skip(index);
  218. final target = iter.next();
  219. final beforeEmbed = target.data == EmbedNode.kPlainTextPlaceholder;
  220. final afterEmbed = previous?.data == EmbedNode.kPlainTextPlaceholder;
  221. if (beforeEmbed || afterEmbed) {
  222. final delta = Delta()..retain(index);
  223. if (beforeEmbed && !text.endsWith('\n')) {
  224. return delta..insert(text)..insert('\n');
  225. }
  226. if (afterEmbed && !text.startsWith('\n')) {
  227. return delta..insert('\n')..insert(text);
  228. }
  229. return delta..insert(text);
  230. }
  231. return null;
  232. }
  233. }
  234. /// Preserves block style when user pastes text containing line-breaks.
  235. /// This rule may also be activated for changes triggered by auto-correct.
  236. class PreserveBlockStyleOnPasteRule extends InsertRule {
  237. const PreserveBlockStyleOnPasteRule();
  238. bool isEdgeLineSplit(Operation before, Operation after) {
  239. if (before == null) return true; // split at the beginning of a doc
  240. return before.data.endsWith('\n') || after.data.startsWith('\n');
  241. }
  242. @override
  243. Delta apply(Delta document, int index, String text) {
  244. if (!text.contains('\n') || text.length == 1) {
  245. // Only interested in text containing at least one line-break and at least
  246. // one more character.
  247. return null;
  248. }
  249. final iter = DeltaIterator(document);
  250. iter.skip(index);
  251. // Look for next line-break.
  252. Map<String, dynamic> lineStyle;
  253. while (iter.hasNext) {
  254. final op = iter.next();
  255. final lf = op.data.indexOf('\n');
  256. if (lf >= 0) {
  257. lineStyle = op.attributes;
  258. break;
  259. }
  260. }
  261. Map<String, dynamic> resetStyle;
  262. Map<String, dynamic> blockStyle;
  263. if (lineStyle != null) {
  264. if (lineStyle.containsKey(NotusAttribute.heading.key)) {
  265. resetStyle = NotusAttribute.heading.unset.toJson();
  266. }
  267. if (lineStyle.containsKey(NotusAttribute.block.key)) {
  268. blockStyle = <String, dynamic>{
  269. NotusAttribute.block.key: lineStyle[NotusAttribute.block.key]
  270. };
  271. }
  272. }
  273. final lines = text.split('\n');
  274. final result = Delta()..retain(index);
  275. for (var i = 0; i < lines.length; i++) {
  276. final line = lines[i];
  277. if (line.isNotEmpty) {
  278. result.insert(line);
  279. }
  280. if (i == 0) {
  281. result.insert('\n', lineStyle);
  282. } else if (i == lines.length - 1) {
  283. if (resetStyle != null) result.retain(1, resetStyle);
  284. } else {
  285. result.insert('\n', blockStyle);
  286. }
  287. }
  288. return result;
  289. }
  290. }