document.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import 'dart:async';
  2. import 'package:tuple/tuple.dart';
  3. import '../quill_delta.dart';
  4. import '../heuristic/rule.dart';
  5. import '../document/style.dart';
  6. import 'history.dart';
  7. import 'attribute.dart';
  8. import 'node/block.dart';
  9. import 'node/container.dart';
  10. import 'node/embed.dart';
  11. import 'node/line.dart';
  12. import 'node/node.dart';
  13. /// The rich text document
  14. class Document {
  15. Document() : _delta = Delta()..insert('\n') {
  16. _loadDocument(_delta);
  17. }
  18. Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
  19. _loadDocument(_delta);
  20. }
  21. Document.fromDelta(Delta delta) : _delta = delta {
  22. _loadDocument(_delta);
  23. }
  24. /// The root node of the document tree
  25. final Root _root = Root();
  26. Root get root => _root;
  27. int get length => _root.length;
  28. Delta _delta;
  29. Delta toDelta() => Delta.from(_delta);
  30. final Rules _rules = Rules.getInstance();
  31. final StreamController<Tuple3<Delta, Delta, ChangeSource>> _observer =
  32. StreamController.broadcast();
  33. final History _history = History();
  34. Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
  35. bool get hasUndo => _history.hasUndo;
  36. bool get hasRedo => _history.hasRedo;
  37. Delta insert(int index, Object? data, {int replaceLength = 0}) {
  38. assert(index >= 0);
  39. assert(data is String || data is Embeddable);
  40. if (data is Embeddable) {
  41. data = data.toJson();
  42. } else if ((data as String).isEmpty) {
  43. return Delta();
  44. }
  45. final delta = _rules.apply(
  46. RuleType.INSERT,
  47. this,
  48. index,
  49. data: data,
  50. length: replaceLength,
  51. );
  52. compose(delta, ChangeSource.LOCAL);
  53. return delta;
  54. }
  55. Delta delete(int index, int length) {
  56. assert(index >= 0 && length > 0);
  57. final delta = _rules.apply(RuleType.DELETE, this, index, length: length);
  58. if (delta.isNotEmpty) {
  59. compose(delta, ChangeSource.LOCAL);
  60. }
  61. return delta;
  62. }
  63. Delta replace(int index, int length, Object? data) {
  64. assert(index >= 0);
  65. assert(data is String || data is Embeddable);
  66. final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
  67. assert(dataIsNotEmpty || length > 0);
  68. var delta = Delta();
  69. // We have to insert before applying delete rules
  70. // Otherwise delete would be operating on stale document snapshot.
  71. if (dataIsNotEmpty) {
  72. delta = insert(index, data, replaceLength: length);
  73. }
  74. if (length > 0) {
  75. final deleteDelta = delete(index, length);
  76. delta = delta.compose(deleteDelta);
  77. }
  78. return delta;
  79. }
  80. Delta format(int index, int length, Attribute? attribute) {
  81. assert(index >= 0 && length >= 0 && attribute != null);
  82. var delta = Delta();
  83. final formatDelta = _rules.apply(
  84. RuleType.FORMAT,
  85. this,
  86. index,
  87. length: length,
  88. attribute: attribute,
  89. );
  90. if (formatDelta.isNotEmpty) {
  91. compose(formatDelta, ChangeSource.LOCAL);
  92. delta = delta.compose(formatDelta);
  93. }
  94. return delta;
  95. }
  96. Style collectStyle(int index, int length) {
  97. final res = queryChild(index);
  98. return (res.node as Line).collectStyle(res.offset, length);
  99. }
  100. ChildQuery queryChild(int offset) {
  101. final res = _root.queryChild(offset, true);
  102. if (res.node is Line) {
  103. return res;
  104. }
  105. final block = res.node as Block;
  106. return block.queryChild(res.offset, true);
  107. }
  108. Tuple2 undo() => _history.undo(this);
  109. Tuple2 redo() => _history.redo(this);
  110. void compose(Delta delta, ChangeSource changeSource) {
  111. assert(!_observer.isClosed);
  112. delta.trim();
  113. assert(delta.isNotEmpty);
  114. var offset = 0;
  115. delta = _transform(delta);
  116. final originDelta = toDelta();
  117. for (final op in delta.toList()) {
  118. final style =
  119. op.attributes != null ? Style.fromJson(op.attributes) : null;
  120. if (op.isInsert) {
  121. _root.insert(offset, _normalize(op.data), style);
  122. } else if (op.isDelete) {
  123. _root.delete(offset, op.length);
  124. } else if (op.attributes != null) {
  125. _root.retain(offset, op.length, style);
  126. }
  127. if (!op.isDelete) {
  128. offset += op.length!;
  129. }
  130. }
  131. try {
  132. _delta = _delta.compose(delta);
  133. } catch (e) {
  134. throw '_delta compose failed';
  135. }
  136. if (_delta != _root.toDelta()) {
  137. throw 'Compose failed';
  138. }
  139. final change = Tuple3(originDelta, delta, changeSource);
  140. _observer.add(change);
  141. _history.handleDocChange(change);
  142. }
  143. static Delta _transform(Delta delta) {
  144. final res = Delta();
  145. final ops = delta.toList();
  146. for (var i = 0; i < ops.length; i++) {
  147. final op = ops[i];
  148. res.push(op);
  149. _handleImageInsert(i, ops, op, res);
  150. }
  151. return res;
  152. }
  153. static void _handleImageInsert(
  154. int i, List<Operation> ops, Operation op, Delta res) {
  155. final nextOpIsImage =
  156. i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String;
  157. if (nextOpIsImage && !(op.data as String).endsWith('\n')) {
  158. res.push(Operation.insert('\n'));
  159. }
  160. // Currently embed is equivalent to image and hence `is! String`
  161. final opInsertImage = op.isInsert && op.data is! String;
  162. final nextOpIsLineBreak = i + 1 < ops.length &&
  163. ops[i + 1].isInsert &&
  164. ops[i + 1].data is String &&
  165. (ops[i + 1].data as String).startsWith('\n');
  166. if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
  167. // automatically append '\n' for image
  168. res.push(Operation.insert('\n'));
  169. }
  170. }
  171. Object _normalize(Object? data) {
  172. if (data is String) {
  173. return data;
  174. }
  175. if (data is Embeddable) {
  176. return data;
  177. }
  178. return Embeddable.fromJson(data as Map<String, dynamic>);
  179. }
  180. void close() {
  181. _observer.close();
  182. _history.clear();
  183. }
  184. void _loadDocument(Delta delta) {
  185. assert((delta.last.data as String).endsWith('\n'),
  186. 'Delta must ends with a line break.');
  187. var offset = 0;
  188. for (final op in delta.toList()) {
  189. if (!op.isInsert) {
  190. throw ArgumentError.value(delta,
  191. 'Document Delta can only contain insert operations but ${op.key} found.');
  192. }
  193. final style =
  194. op.attributes != null ? Style.fromJson(op.attributes) : null;
  195. final data = _normalize(op.data);
  196. _root.insert(offset, data, style);
  197. offset += op.length!;
  198. }
  199. final node = _root.last;
  200. if (node is Line &&
  201. node.parent is! Block &&
  202. node.style.isEmpty &&
  203. _root.childCount > 1) {
  204. _root.remove(node);
  205. }
  206. }
  207. bool isEmpty() {
  208. if (root.children.length != 1) {
  209. return false;
  210. }
  211. final node = root.children.first;
  212. if (!node.isLast) {
  213. return false;
  214. }
  215. final delta = node.toDelta();
  216. return delta.length == 1 &&
  217. delta.first.data == '\n' &&
  218. delta.first.key == Operation.insertKey;
  219. }
  220. String toPlainText() {
  221. return root.children.map((child) => child.toPlainText()).join();
  222. }
  223. }
  224. enum ChangeSource {
  225. LOCAL,
  226. REMOTE,
  227. }