Browse Source

Added first embed: horizontal rule

Anatoly Pulyaevskiy 6 years ago
parent
commit
26359edb42

+ 0
- 1
packages/notus/analysis_options.yaml View File

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

+ 0
- 1
packages/notus/lib/notus.dart View File

11
 export 'src/document/leaf.dart';
11
 export 'src/document/leaf.dart';
12
 export 'src/document/line.dart';
12
 export 'src/document/line.dart';
13
 export 'src/document/node.dart';
13
 export 'src/document/node.dart';
14
-export 'src/embed.dart';
15
 export 'src/heuristics.dart';
14
 export 'src/heuristics.dart';
16
 export 'src/heuristics/delete_rules.dart';
15
 export 'src/heuristics/delete_rules.dart';
17
 export 'src/heuristics/format_rules.dart';
16
 export 'src/heuristics/format_rules.dart';

+ 15
- 38
packages/notus/lib/src/document.dart View File

10
 import 'document/leaf.dart';
10
 import 'document/leaf.dart';
11
 import 'document/line.dart';
11
 import 'document/line.dart';
12
 import 'document/node.dart';
12
 import 'document/node.dart';
13
-import 'embed.dart';
14
 import 'heuristics.dart';
13
 import 'heuristics.dart';
15
 
14
 
16
 /// Source of a [NotusChange].
15
 /// Source of a [NotusChange].
95
     _controller.close();
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
   /// This method applies heuristic rules before modifying this document and
99
   /// This method applies heuristic rules before modifying this document and
102
   /// produces a [NotusChange] with source set to [ChangeSource.local].
100
   /// produces a [NotusChange] with source set to [ChangeSource.local].
103
   ///
101
   ///
104
   /// Returns an instance of [Delta] actually composed into this document.
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
     assert(index >= 0);
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
     compose(change, ChangeSource.local);
109
     compose(change, ChangeSource.local);
120
     return change;
110
     return change;
121
   }
111
   }
130
     assert(index >= 0 && length > 0);
120
     assert(index >= 0 && length > 0);
131
     // TODO: need a heuristic rule to ensure last line-break.
121
     // TODO: need a heuristic rule to ensure last line-break.
132
     final change = _heuristics.applyDeleteRules(this, index, length);
122
     final change = _heuristics.applyDeleteRules(this, index, length);
133
-    // Delete rules are allowed to prevent the edit so it may be empty.
134
     if (change.isNotEmpty) {
123
     if (change.isNotEmpty) {
124
+      // Delete rules are allowed to prevent the edit so it may be empty.
135
       compose(change, ChangeSource.local);
125
       compose(change, ChangeSource.local);
136
     }
126
     }
137
     return change;
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
   /// This method applies heuristic rules before modifying this document and
132
   /// This method applies heuristic rules before modifying this document and
144
   /// produces a [NotusChange] with source set to [ChangeSource.local].
133
   /// produces a [NotusChange] with source set to [ChangeSource.local].
145
   ///
134
   ///
146
   /// Returns an instance of [Delta] actually composed into this document.
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
     Delta delta = new Delta();
139
     Delta delta = new Delta();
156
-
157
     // We have to compose before applying delete rules
140
     // We have to compose before applying delete rules
158
     // Otherwise delete would be operating on stale document snapshot.
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
       index = delta.transformPosition(index);
144
       index = delta.transformPosition(index);
162
     }
145
     }
163
 
146
 
168
     return delta;
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
   /// Applies heuristic rules before modifying this document and
156
   /// Applies heuristic rules before modifying this document and
174
   /// produces a [NotusChange] with source set to [ChangeSource.local].
157
   /// produces a [NotusChange] with source set to [ChangeSource.local].
178
   /// unchanged and no [NotusChange] is published to [changes] stream.
161
   /// unchanged and no [NotusChange] is published to [changes] stream.
179
   Delta format(int index, int length, NotusAttribute attribute) {
162
   Delta format(int index, int length, NotusAttribute attribute) {
180
     assert(index >= 0 && length >= 0 && attribute != null);
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
     if (change.isNotEmpty) {
165
     if (change.isNotEmpty) {
189
       compose(change, ChangeSource.local);
166
       compose(change, ChangeSource.local);
190
     }
167
     }

+ 0
- 21
packages/notus/lib/src/embed.dart View File

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 View File

6
 import 'package:quill_delta/quill_delta.dart';
6
 import 'package:quill_delta/quill_delta.dart';
7
 
7
 
8
 import 'heuristics/delete_rules.dart';
8
 import 'heuristics/delete_rules.dart';
9
-import 'heuristics/embed_rules.dart';
10
 import 'heuristics/format_rules.dart';
9
 import 'heuristics/format_rules.dart';
11
 import 'heuristics/insert_rules.dart';
10
 import 'heuristics/insert_rules.dart';
12
 
11
 
16
   /// Default set of heuristic rules.
15
   /// Default set of heuristic rules.
17
   static const NotusHeuristics fallback = NotusHeuristics(
16
   static const NotusHeuristics fallback = NotusHeuristics(
18
     formatRules: [
17
     formatRules: [
18
+      FormatEmbedsRule(),
19
       FormatLinkAtCaretPositionRule(),
19
       FormatLinkAtCaretPositionRule(),
20
       ResolveLineFormatRule(),
20
       ResolveLineFormatRule(),
21
       ResolveInlineFormatRule(),
21
       ResolveInlineFormatRule(),
36
       PreserveLineStyleOnMergeRule(),
36
       PreserveLineStyleOnMergeRule(),
37
       CatchAllDeleteRule(),
37
       CatchAllDeleteRule(),
38
     ],
38
     ],
39
-    embedRules: [
40
-      FormatEmbedsRule(),
41
-    ],
42
   );
39
   );
43
 
40
 
44
   const NotusHeuristics({
41
   const NotusHeuristics({
45
     this.formatRules,
42
     this.formatRules,
46
     this.insertRules,
43
     this.insertRules,
47
     this.deleteRules,
44
     this.deleteRules,
48
-    this.embedRules,
49
   });
45
   });
50
 
46
 
51
   /// List of format rules in this registry.
47
   /// List of format rules in this registry.
57
   /// List of delete rules in this registry.
53
   /// List of delete rules in this registry.
58
   final List<DeleteRule> deleteRules;
54
   final List<DeleteRule> deleteRules;
59
 
55
 
60
-  /// List of embed rules in this registry.
61
-  final List<EmbedRule> embedRules;
62
-
63
   /// Applies heuristic rules to specified insert operation based on current
56
   /// Applies heuristic rules to specified insert operation based on current
64
   /// state of Notus [document].
57
   /// state of Notus [document].
65
   Delta applyInsertRules(NotusDocument document, int index, String insert) {
58
   Delta applyInsertRules(NotusDocument document, int index, String insert) {
96
     throw new StateError(
89
     throw new StateError(
97
         'Failed to apply delete heuristic rules: none applied.');
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 View File

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 View File

144
     return result;
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 View File

240
 
240
 
241
     test('insert embed after line-break', () {
241
     test('insert embed after line-break', () {
242
       final doc = dartconfDoc();
242
       final doc = dartconfDoc();
243
-      doc.insert(9, const HorizontalRuleEmbed());
243
+      doc.format(9, 0, NotusAttribute.embed.horizontalRule);
244
       expect(doc.root.children, hasLength(3));
244
       expect(doc.root.children, hasLength(3));
245
       expect(doc.root.first.toPlainText(), 'DartConf\n');
245
       expect(doc.root.first.toPlainText(), 'DartConf\n');
246
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
246
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
253
 
253
 
254
     test('insert embed before line-break', () {
254
     test('insert embed before line-break', () {
255
       final doc = dartconfDoc();
255
       final doc = dartconfDoc();
256
-      doc.insert(8, const HorizontalRuleEmbed());
256
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
257
       expect(doc.root.children, hasLength(3));
257
       expect(doc.root.children, hasLength(3));
258
       expect(doc.root.first.toPlainText(), 'DartConf\n');
258
       expect(doc.root.first.toPlainText(), 'DartConf\n');
259
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
259
       expect(doc.root.last.toPlainText(), 'Los Angeles\n');
266
 
266
 
267
     test('insert embed in the middle of a line', () {
267
     test('insert embed in the middle of a line', () {
268
       final doc = dartconfDoc();
268
       final doc = dartconfDoc();
269
-      doc.insert(4, const HorizontalRuleEmbed());
269
+      doc.format(4, 0, NotusAttribute.embed.horizontalRule);
270
       expect(doc.root.children, hasLength(4));
270
       expect(doc.root.children, hasLength(4));
271
       expect(doc.root.children.elementAt(0).toPlainText(), 'Dart\n');
271
       expect(doc.root.children.elementAt(0).toPlainText(), 'Dart\n');
272
       expect(doc.root.children.elementAt(1).toPlainText(),
272
       expect(doc.root.children.elementAt(1).toPlainText(),
282
 
282
 
283
     test('delete embed', () {
283
     test('delete embed', () {
284
       final doc = dartconfDoc();
284
       final doc = dartconfDoc();
285
-      doc.insert(8, const HorizontalRuleEmbed());
285
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
286
       expect(doc.root.children, hasLength(3));
286
       expect(doc.root.children, hasLength(3));
287
       doc.delete(9, 1);
287
       doc.delete(9, 1);
288
       expect(doc.root.children, hasLength(3));
288
       expect(doc.root.children, hasLength(3));
299
 
299
 
300
     test('insert text before embed', () {
300
     test('insert text before embed', () {
301
       final doc = dartconfDoc();
301
       final doc = dartconfDoc();
302
-      doc.insert(8, const HorizontalRuleEmbed());
302
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
303
       expect(doc.root.children, hasLength(3));
303
       expect(doc.root.children, hasLength(3));
304
       doc.insert(9, 'text');
304
       doc.insert(9, 'text');
305
       expect(doc.root.children, hasLength(4));
305
       expect(doc.root.children, hasLength(4));
310
 
310
 
311
     test('insert text after embed', () {
311
     test('insert text after embed', () {
312
       final doc = dartconfDoc();
312
       final doc = dartconfDoc();
313
-      doc.insert(8, const HorizontalRuleEmbed());
313
+      doc.format(8, 0, NotusAttribute.embed.horizontalRule);
314
       expect(doc.root.children, hasLength(3));
314
       expect(doc.root.children, hasLength(3));
315
       doc.insert(10, 'text');
315
       doc.insert(10, 'text');
316
       expect(doc.root.children, hasLength(4));
316
       expect(doc.root.children, hasLength(4));
318
           '${EmbedNode.kPlainTextPlaceholder}\n');
318
           '${EmbedNode.kPlainTextPlaceholder}\n');
319
       expect(doc.root.children.elementAt(2).toPlainText(), 'text\n');
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 View File

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

+ 5
- 1
packages/zefyr/lib/src/widgets/toolbar.dart View File

28
   numberList,
28
   numberList,
29
   code,
29
   code,
30
   quote,
30
   quote,
31
+  horizontalRule,
31
   hideKeyboard,
32
   hideKeyboard,
32
   close,
33
   close,
33
   confirm,
34
   confirm,
44
   ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList,
45
   ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList,
45
   ZefyrToolbarAction.numberList: NotusAttribute.block.numberList,
46
   ZefyrToolbarAction.numberList: NotusAttribute.block.numberList,
46
   ZefyrToolbarAction.code: NotusAttribute.block.code,
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
 /// Allows customizing appearance of [ZefyrToolbar].
52
 /// Allows customizing appearance of [ZefyrToolbar].
258
       buildButton(context, ZefyrToolbarAction.numberList),
260
       buildButton(context, ZefyrToolbarAction.numberList),
259
       buildButton(context, ZefyrToolbarAction.quote),
261
       buildButton(context, ZefyrToolbarAction.quote),
260
       buildButton(context, ZefyrToolbarAction.code),
262
       buildButton(context, ZefyrToolbarAction.code),
263
+      buildButton(context, ZefyrToolbarAction.horizontalRule),
261
     ];
264
     ];
262
     return buttons;
265
     return buttons;
263
   }
266
   }
344
     ZefyrToolbarAction.numberList: Icons.format_list_numbered,
347
     ZefyrToolbarAction.numberList: Icons.format_list_numbered,
345
     ZefyrToolbarAction.code: Icons.code,
348
     ZefyrToolbarAction.code: Icons.code,
346
     ZefyrToolbarAction.quote: Icons.format_quote,
349
     ZefyrToolbarAction.quote: Icons.format_quote,
350
+    ZefyrToolbarAction.horizontalRule: Icons.remove,
347
     ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide,
351
     ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide,
348
     ZefyrToolbarAction.close: Icons.close,
352
     ZefyrToolbarAction.close: Icons.close,
349
     ZefyrToolbarAction.confirm: Icons.check,
353
     ZefyrToolbarAction.confirm: Icons.check,