瀏覽代碼

Added first embed: horizontal rule

Anatoly Pulyaevskiy 6 年之前
父節點
當前提交
26359edb42

+ 0
- 1
packages/notus/analysis_options.yaml 查看文件

@@ -1,5 +1,4 @@
1 1
 analyzer:
2
-  strong-mode: true
3 2
   language:
4 3
     enableSuperMixins: true
5 4
 

+ 0
- 1
packages/notus/lib/notus.dart 查看文件

@@ -11,7 +11,6 @@ export 'src/document/block.dart';
11 11
 export 'src/document/leaf.dart';
12 12
 export 'src/document/line.dart';
13 13
 export 'src/document/node.dart';
14
-export 'src/embed.dart';
15 14
 export 'src/heuristics.dart';
16 15
 export 'src/heuristics/delete_rules.dart';
17 16
 export 'src/heuristics/format_rules.dart';

+ 15
- 38
packages/notus/lib/src/document.dart 查看文件

@@ -10,7 +10,6 @@ import 'document/block.dart';
10 10
 import 'document/leaf.dart';
11 11
 import 'document/line.dart';
12 12
 import 'document/node.dart';
13
-import 'embed.dart';
14 13
 import 'heuristics.dart';
15 14
 
16 15
 /// Source of a [NotusChange].
@@ -95,27 +94,18 @@ class NotusDocument {
95 94
     _controller.close();
96 95
   }
97 96
 
98
-  /// Inserts [value] in this document at specified [index]. Value must be a
99
-  /// [String] or an instance of [NotusEmbed].
97
+  /// Inserts [text] in this document at specified [index].
100 98
   ///
101 99
   /// This method applies heuristic rules before modifying this document and
102 100
   /// produces a [NotusChange] with source set to [ChangeSource.local].
103 101
   ///
104 102
   /// Returns an instance of [Delta] actually composed into this document.
105
-  Delta insert(int index, dynamic value) {
103
+  Delta insert(int index, String text) {
106 104
     assert(index >= 0);
107
-    assert(value is String || value is NotusEmbed,
108
-        'Value must be a string or a NotusEmbed.');
109
-    Delta change;
110
-    if (value is String) {
111
-      assert(value.isNotEmpty);
112
-      value = _sanitizeString(value);
113
-      if (value.isEmpty) return new Delta();
114
-      change = _heuristics.applyInsertRules(this, index, value);
115
-    } else {
116
-      NotusEmbed embed = value;
117
-      change = _heuristics.applyEmbedRules(this, index, embed.attribute);
118
-    }
105
+    assert(text.isNotEmpty);
106
+    text = _sanitizeString(text);
107
+    if (text.isEmpty) return new Delta();
108
+    final change = _heuristics.applyInsertRules(this, index, text);
119 109
     compose(change, ChangeSource.local);
120 110
     return change;
121 111
   }
@@ -130,34 +120,27 @@ class NotusDocument {
130 120
     assert(index >= 0 && length > 0);
131 121
     // TODO: need a heuristic rule to ensure last line-break.
132 122
     final change = _heuristics.applyDeleteRules(this, index, length);
133
-    // Delete rules are allowed to prevent the edit so it may be empty.
134 123
     if (change.isNotEmpty) {
124
+      // Delete rules are allowed to prevent the edit so it may be empty.
135 125
       compose(change, ChangeSource.local);
136 126
     }
137 127
     return change;
138 128
   }
139 129
 
140
-  /// Replaces [length] of characters starting at [index] with [value]. Value
141
-  /// must be a [String] or an instance of [NotusEmbed].
130
+  /// Replaces [length] of characters starting at [index] [text].
142 131
   ///
143 132
   /// This method applies heuristic rules before modifying this document and
144 133
   /// produces a [NotusChange] with source set to [ChangeSource.local].
145 134
   ///
146 135
   /// Returns an instance of [Delta] actually composed into this document.
147
-  Delta replace(int index, int length, dynamic value) {
148
-    assert(index >= 0 && (value.isNotEmpty || length > 0),
149
-        'With index $index, length $length and text "$value"');
150
-    assert(value is String || value is NotusEmbed,
151
-        'Value must be a string or a NotusEmbed.');
152
-
153
-    final hasInsert =
154
-        (value is NotusEmbed || (value is String && value.isNotEmpty));
136
+  Delta replace(int index, int length, String text) {
137
+    assert(index >= 0 && (text.isNotEmpty || length > 0),
138
+        'With index $index, length $length and text "$text"');
155 139
     Delta delta = new Delta();
156
-
157 140
     // We have to compose before applying delete rules
158 141
     // Otherwise delete would be operating on stale document snapshot.
159
-    if (hasInsert) {
160
-      delta = insert(index, value);
142
+    if (text.isNotEmpty) {
143
+      delta = insert(index, text);
161 144
       index = delta.transformPosition(index);
162 145
     }
163 146
 
@@ -168,7 +151,7 @@ class NotusDocument {
168 151
     return delta;
169 152
   }
170 153
 
171
-  /// Formats portion of this document with specified [attribute].
154
+  /// Formats segment of this document with specified [attribute].
172 155
   ///
173 156
   /// Applies heuristic rules before modifying this document and
174 157
   /// produces a [NotusChange] with source set to [ChangeSource.local].
@@ -178,13 +161,7 @@ class NotusDocument {
178 161
   /// unchanged and no [NotusChange] is published to [changes] stream.
179 162
   Delta format(int index, int length, NotusAttribute attribute) {
180 163
     assert(index >= 0 && length >= 0 && attribute != null);
181
-    Delta change;
182
-    if (attribute is EmbedAttribute) {
183
-      assert(length == 1);
184
-      change = _heuristics.applyEmbedRules(this, index, attribute);
185
-    } else {
186
-      change = _heuristics.applyFormatRules(this, index, length, attribute);
187
-    }
164
+    final change = _heuristics.applyFormatRules(this, index, length, attribute);
188 165
     if (change.isNotEmpty) {
189 166
       compose(change, ChangeSource.local);
190 167
     }

+ 0
- 21
packages/notus/lib/src/embed.dart 查看文件

@@ -1,21 +0,0 @@
1
-import 'document/attributes.dart';
2
-
3
-abstract class NotusEmbed {
4
-  NotusAttribute get attribute;
5
-}
6
-
7
-class HorizontalRuleEmbed implements NotusEmbed {
8
-  const HorizontalRuleEmbed();
9
-
10
-  @override
11
-  NotusAttribute<Map<String, dynamic>> get attribute =>
12
-      NotusAttribute.embed.horizontalRule;
13
-}
14
-
15
-class ImageEmbed implements NotusEmbed {
16
-  const ImageEmbed(this.source) : assert(source != null);
17
-  final String source;
18
-
19
-  @override
20
-  NotusAttribute get attribute => NotusAttribute.embed.image(source);
21
-}

+ 1
- 21
packages/notus/lib/src/heuristics.dart 查看文件

@@ -6,7 +6,6 @@ import 'package:notus/notus.dart';
6 6
 import 'package:quill_delta/quill_delta.dart';
7 7
 
8 8
 import 'heuristics/delete_rules.dart';
9
-import 'heuristics/embed_rules.dart';
10 9
 import 'heuristics/format_rules.dart';
11 10
 import 'heuristics/insert_rules.dart';
12 11
 
@@ -16,6 +15,7 @@ class NotusHeuristics {
16 15
   /// Default set of heuristic rules.
17 16
   static const NotusHeuristics fallback = NotusHeuristics(
18 17
     formatRules: [
18
+      FormatEmbedsRule(),
19 19
       FormatLinkAtCaretPositionRule(),
20 20
       ResolveLineFormatRule(),
21 21
       ResolveInlineFormatRule(),
@@ -36,16 +36,12 @@ class NotusHeuristics {
36 36
       PreserveLineStyleOnMergeRule(),
37 37
       CatchAllDeleteRule(),
38 38
     ],
39
-    embedRules: [
40
-      FormatEmbedsRule(),
41
-    ],
42 39
   );
43 40
 
44 41
   const NotusHeuristics({
45 42
     this.formatRules,
46 43
     this.insertRules,
47 44
     this.deleteRules,
48
-    this.embedRules,
49 45
   });
50 46
 
51 47
   /// List of format rules in this registry.
@@ -57,9 +53,6 @@ class NotusHeuristics {
57 53
   /// List of delete rules in this registry.
58 54
   final List<DeleteRule> deleteRules;
59 55
 
60
-  /// List of embed rules in this registry.
61
-  final List<EmbedRule> embedRules;
62
-
63 56
   /// Applies heuristic rules to specified insert operation based on current
64 57
   /// state of Notus [document].
65 58
   Delta applyInsertRules(NotusDocument document, int index, String insert) {
@@ -96,17 +89,4 @@ class NotusHeuristics {
96 89
     throw new StateError(
97 90
         'Failed to apply delete heuristic rules: none applied.');
98 91
   }
99
-
100
-  /// Applies heuristic rules to specified embed operation based on current
101
-  /// state of [document].
102
-  Delta applyEmbedRules(
103
-      NotusDocument document, int index, EmbedAttribute embed) {
104
-    final delta = document.toDelta();
105
-    for (var rule in embedRules) {
106
-      final result = rule.apply(delta, index, embed);
107
-      if (result != null) return result..trim();
108
-    }
109
-    throw new StateError(
110
-        'Failed to apply embed heuristic rules: none applied.');
111
-  }
112 92
 }

+ 0
- 86
packages/notus/lib/src/heuristics/embed_rules.dart 查看文件

@@ -1,86 +0,0 @@
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
-
5
-import 'package:quill_delta/quill_delta.dart';
6
-import 'package:notus/notus.dart';
7
-
8
-/// A heuristic rule for embed operations.
9
-abstract class EmbedRule {
10
-  /// Constant constructor allows subclasses to declare constant constructors.
11
-  const EmbedRule();
12
-
13
-  /// Applies heuristic rule to an embed operation on a [document] and returns
14
-  /// resulting [Delta].
15
-  Delta apply(Delta document, int index, EmbedAttribute embed);
16
-}
17
-
18
-/// Handles all operations which manipulate embeds.
19
-class FormatEmbedsRule extends EmbedRule {
20
-  const FormatEmbedsRule();
21
-
22
-  @override
23
-  Delta apply(Delta document, int index, EmbedAttribute embed) {
24
-    final iter = new DeltaIterator(document);
25
-    final previous = iter.skip(index);
26
-
27
-    final target = iter.next();
28
-    Delta result = new Delta()..retain(index);
29
-    if (target.length == 1 && target.data == EmbedNode.kPlainTextPlaceholder) {
30
-      assert(() {
31
-        final style = NotusStyle.fromJson(target.attributes);
32
-        return style.single.key == NotusAttribute.embed.key;
33
-      }());
34
-
35
-      // There is already embed here, simply update its style.
36
-      return result..retain(1, embed.toJson());
37
-    } else {
38
-      // Target is not an embed, need to insert.
39
-      // Embeds can be inserted only into an empty line.
40
-
41
-      // Check if [index] is on an empty line already.
42
-      final isNewlineBefore = previous == null || previous.data.endsWith('\n');
43
-      final isNewlineAfter = target.data.startsWith('\n');
44
-      final isOnEmptyLine = isNewlineBefore && isNewlineAfter;
45
-      if (isOnEmptyLine) {
46
-        return result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
47
-      }
48
-      // We are on a non-empty line, split it (preserving style if needed)
49
-      // and insert our embed.
50
-      final lineStyle = _getLineStyle(iter, target);
51
-      if (!isNewlineBefore) {
52
-        result..insert('\n', lineStyle);
53
-      }
54
-      result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
55
-      if (!isNewlineAfter) {
56
-        result..insert('\n');
57
-      }
58
-      return result;
59
-    }
60
-//
61
-//    if (embed == NotusAttribute.embed.unset) {
62
-//      // Convert into a delete operation.
63
-//      return result..delete(1);
64
-//    } else {
65
-//      return result..retain(1, embed.toJson());
66
-//    }
67
-  }
68
-
69
-  Map<String, dynamic> _getLineStyle(
70
-      DeltaIterator iterator, Operation current) {
71
-    if (current.data.indexOf('\n') >= 0) {
72
-      return current.attributes;
73
-    }
74
-    // Continue looking for line-break.
75
-    Map<String, dynamic> attributes;
76
-    while (iterator.hasNext) {
77
-      final op = iterator.next();
78
-      int lf = op.data.indexOf('\n');
79
-      if (lf >= 0) {
80
-        attributes = op.attributes;
81
-        break;
82
-      }
83
-    }
84
-    return attributes;
85
-  }
86
-}

+ 72
- 0
packages/notus/lib/src/heuristics/format_rules.dart 查看文件

@@ -144,3 +144,75 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
144 144
     return result;
145 145
   }
146 146
 }
147
+
148
+/// Handles all format operations which manipulate embeds.
149
+class FormatEmbedsRule extends FormatRule {
150
+  const FormatEmbedsRule();
151
+
152
+  @override
153
+  Delta apply(Delta document, int index, int length, NotusAttribute attribute) {
154
+    // We are only interested in embed attributes
155
+    if (attribute is! EmbedAttribute) return null;
156
+    EmbedAttribute embed = attribute;
157
+
158
+    if (length == 1 && embed.isUnset) {
159
+      // Remove the embed.
160
+      return new Delta()
161
+        ..retain(index)
162
+        ..delete(length);
163
+    } else {
164
+      // If length is 0 we treat it as an insert at specified [index].
165
+      // If length is non-zero we treat it as a replace of selected range
166
+      // with the embed.
167
+      assert(!embed.isUnset);
168
+      return _insertEmbed(document, index, length, embed);
169
+    }
170
+  }
171
+
172
+  Delta _insertEmbed(
173
+      Delta document, int index, int length, EmbedAttribute embed) {
174
+    Delta result = new Delta()..retain(index);
175
+    final iter = new DeltaIterator(document);
176
+    final previous = iter.skip(index);
177
+    iter.skip(length); // ignore deleted part.
178
+    final target = iter.next();
179
+
180
+    // Check if [index] is on an empty line already.
181
+    final isNewlineBefore = previous == null || previous.data.endsWith('\n');
182
+    final isNewlineAfter = target.data.startsWith('\n');
183
+    final isOnEmptyLine = isNewlineBefore && isNewlineAfter;
184
+    if (isOnEmptyLine) {
185
+      return result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
186
+    }
187
+    // We are on a non-empty line, split it (preserving style if needed)
188
+    // and insert our embed.
189
+    final lineStyle = _getLineStyle(iter, target);
190
+    if (!isNewlineBefore) {
191
+      result..insert('\n', lineStyle);
192
+    }
193
+    result..insert(EmbedNode.kPlainTextPlaceholder, embed.toJson());
194
+    if (!isNewlineAfter) {
195
+      result..insert('\n');
196
+    }
197
+    result.delete(length);
198
+    return result;
199
+  }
200
+
201
+  Map<String, dynamic> _getLineStyle(
202
+      DeltaIterator iterator, Operation current) {
203
+    if (current.data.indexOf('\n') >= 0) {
204
+      return current.attributes;
205
+    }
206
+    // Continue looking for line-break.
207
+    Map<String, dynamic> attributes;
208
+    while (iterator.hasNext) {
209
+      final op = iterator.next();
210
+      int lf = op.data.indexOf('\n');
211
+      if (lf >= 0) {
212
+        attributes = op.attributes;
213
+        break;
214
+      }
215
+    }
216
+    return attributes;
217
+  }
218
+}

+ 16
- 6
packages/notus/test/document_test.dart 查看文件

@@ -240,7 +240,7 @@ void main() {
240 240
 
241 241
     test('insert embed after line-break', () {
242 242
       final doc = dartconfDoc();
243
-      doc.insert(9, const HorizontalRuleEmbed());
243
+      doc.format(9, 0, NotusAttribute.embed.horizontalRule);
244 244
       expect(doc.root.children, hasLength(3));
245 245
       expect(doc.root.first.toPlainText(), 'DartConf\n');
246 246
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
@@ -253,7 +253,7 @@ void main() {
253 253
 
254 254
     test('insert embed before line-break', () {
255 255
       final doc = dartconfDoc();
256
-      doc.insert(8, const HorizontalRuleEmbed());
256
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
257 257
       expect(doc.root.children, hasLength(3));
258 258
       expect(doc.root.first.toPlainText(), 'DartConf\n');
259 259
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
@@ -266,7 +266,7 @@ void main() {
266 266
 
267 267
     test('insert embed in the middle of a line', () {
268 268
       final doc = dartconfDoc();
269
-      doc.insert(4, const HorizontalRuleEmbed());
269
+      doc.format(4, 0, NotusAttribute.embed.horizontalRule);
270 270
       expect(doc.root.children, hasLength(4));
271 271
       expect(doc.root.children.elementAt(0).toPlainText(), 'Dart\n');
272 272
       expect(doc.root.children.elementAt(1).toPlainText(),
@@ -282,7 +282,7 @@ void main() {
282 282
 
283 283
     test('delete embed', () {
284 284
       final doc = dartconfDoc();
285
-      doc.insert(8, const HorizontalRuleEmbed());
285
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
286 286
       expect(doc.root.children, hasLength(3));
287 287
       doc.delete(9, 1);
288 288
       expect(doc.root.children, hasLength(3));
@@ -299,7 +299,7 @@ void main() {
299 299
 
300 300
     test('insert text before embed', () {
301 301
       final doc = dartconfDoc();
302
-      doc.insert(8, const HorizontalRuleEmbed());
302
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
303 303
       expect(doc.root.children, hasLength(3));
304 304
       doc.insert(9, 'text');
305 305
       expect(doc.root.children, hasLength(4));
@@ -310,7 +310,7 @@ void main() {
310 310
 
311 311
     test('insert text after embed', () {
312 312
       final doc = dartconfDoc();
313
-      doc.insert(8, const HorizontalRuleEmbed());
313
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
314 314
       expect(doc.root.children, hasLength(3));
315 315
       doc.insert(10, 'text');
316 316
       expect(doc.root.children, hasLength(4));
@@ -318,5 +318,15 @@ void main() {
318 318
           '${EmbedNode.kPlainTextPlaceholder}\n');
319 319
       expect(doc.root.children.elementAt(2).toPlainText(), 'text\n');
320 320
     });
321
+
322
+    test('replace text with embed', () {
323
+      final doc = dartconfDoc();
324
+      doc.format(4, 4, NotusAttribute.embed.horizontalRule);
325
+      expect(doc.root.children, hasLength(3));
326
+      expect(doc.root.children.elementAt(0).toPlainText(), 'Dart\n');
327
+      expect(doc.root.children.elementAt(1).toPlainText(),
328
+          '${EmbedNode.kPlainTextPlaceholder}\n');
329
+      expect(doc.root.children.elementAt(2).toPlainText(), 'Los Angeles\n');
330
+    });
321 331
   });
322 332
 }

+ 1
- 1
packages/zefyr/lib/src/widgets/horizontal_rule.dart 查看文件

@@ -56,7 +56,7 @@ class HorizontalRule extends LeafRenderObjectWidget {
56 56
 }
57 57
 
58 58
 class RenderHorizontalRule extends RenderBox implements RenderEditableBox {
59
-  static const kPaddingBottom = 16.0;
59
+  static const kPaddingBottom = 24.0;
60 60
   static const kWidth = 3.0;
61 61
 
62 62
   RenderHorizontalRule({

+ 5
- 1
packages/zefyr/lib/src/widgets/toolbar.dart 查看文件

@@ -28,6 +28,7 @@ enum ZefyrToolbarAction {
28 28
   numberList,
29 29
   code,
30 30
   quote,
31
+  horizontalRule,
31 32
   hideKeyboard,
32 33
   close,
33 34
   confirm,
@@ -44,7 +45,8 @@ final kZefyrToolbarAttributeActions = <ZefyrToolbarAction, NotusAttributeKey>{
44 45
   ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList,
45 46
   ZefyrToolbarAction.numberList: NotusAttribute.block.numberList,
46 47
   ZefyrToolbarAction.code: NotusAttribute.block.code,
47
-  ZefyrToolbarAction.quote: NotusAttribute.block.quote
48
+  ZefyrToolbarAction.quote: NotusAttribute.block.quote,
49
+  ZefyrToolbarAction.horizontalRule: NotusAttribute.embed.horizontalRule,
48 50
 };
49 51
 
50 52
 /// Allows customizing appearance of [ZefyrToolbar].
@@ -258,6 +260,7 @@ class ZefyrToolbarState extends State<ZefyrToolbar>
258 260
       buildButton(context, ZefyrToolbarAction.numberList),
259 261
       buildButton(context, ZefyrToolbarAction.quote),
260 262
       buildButton(context, ZefyrToolbarAction.code),
263
+      buildButton(context, ZefyrToolbarAction.horizontalRule),
261 264
     ];
262 265
     return buttons;
263 266
   }
@@ -344,6 +347,7 @@ class _DefaultZefyrToolbarDelegate implements ZefyrToolbarDelegate {
344 347
     ZefyrToolbarAction.numberList: Icons.format_list_numbered,
345 348
     ZefyrToolbarAction.code: Icons.code,
346 349
     ZefyrToolbarAction.quote: Icons.format_quote,
350
+    ZefyrToolbarAction.horizontalRule: Icons.remove,
347 351
     ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide,
348 352
     ZefyrToolbarAction.close: Icons.close,
349 353
     ZefyrToolbarAction.confirm: Icons.check,