@@ -22,38 +22,75 @@ import 'package:super_editor_quill/src/parsing/inline_formats.dart';
22
22
/// and a [MutableComposer] . The document must be empty.
23
23
/// {@endtemplate}
24
24
///
25
+ /// {@template merge_consecutive_blocks}
26
+ /// ### Merging consecutive blocks
27
+ ///
28
+ /// The Delta format creates some ambiguity around when multiple lines should
29
+ /// be combined into a single block vs one block per line. E.g., a code block
30
+ /// with multiple lines of code vs a series of independent code blocks.
31
+ ///
32
+ /// [blockMergeRules] explicitly tells the parser which consecutive
33
+ /// [DocumentNode] s should be merged together when not separated by an unstyled
34
+ /// newline in the given deltas.
35
+ ///
36
+ /// Example of consecutive code blocks that would be merged (if requested):
37
+ ///
38
+ /// [
39
+ /// { "insert": "Code line one" },
40
+ /// { "insert": "\n", "attributed": { "code-block": "plain"} },
41
+ /// { "insert": "Code line two" },
42
+ /// { "insert": "\n", "attributed": { "code-block": "plain"} },
43
+ /// ]
44
+ ///
45
+ /// Example of code blocks, separated by an unstyled newline, that wouldn't be merged:
46
+ ///
47
+ /// [
48
+ /// { "insert": "Code line one" },
49
+ /// { "insert": "\n", "attributed": { "code-block": "plain"} },
50
+ /// { "insert": "\n" },
51
+ /// { "insert": "Code line two" },
52
+ /// { "insert": "\n", "attributed": { "code-block": "plain"} },
53
+ /// ]
54
+ ///
55
+ /// {@endtemplate}
56
+ ///
25
57
/// For more information about the Quill Delta format, see the official
26
58
/// documentation: https://quilljs.com/docs/delta/
27
59
MutableDocument parseQuillDeltaDocument (
28
60
Map <String , dynamic > deltaDocument, {
29
61
Editor ? customEditor,
30
62
List <BlockDeltaFormat > blockFormats = defaultBlockFormats,
63
+ List <DeltaBlockMergeRule > blockMergeRules = defaultBlockMergeRules,
31
64
List <InlineDeltaFormat > inlineFormats = defaultInlineFormats,
32
65
List <InlineEmbedFormat > inlineEmbedFormats = const [],
33
66
List <BlockDeltaFormat > embedBlockFormats = defaultEmbedBockFormats,
34
67
}) {
35
68
return parseQuillDeltaOps (
36
69
deltaDocument["ops" ],
37
70
customEditor: customEditor,
71
+ blockMergeRules: blockMergeRules,
38
72
blockFormats: blockFormats,
39
73
inlineFormats: inlineFormats,
40
74
inlineEmbedFormats: inlineEmbedFormats,
41
75
embedBlockFormats: embedBlockFormats,
42
76
);
43
77
}
44
78
45
- /// Parses a list Quill Delta operations (as JSON) into a [MutableDocument] .
79
+ /// Parses a list of Quill Delta operations (as JSON) into a [MutableDocument] .
46
80
///
47
81
/// This parser is the same as [parseQuillDeltaDocument] except that this method
48
82
/// directly accepts the operations list instead of the whole document map. This
49
83
/// method is provided for convenience because in some situations only the
50
84
/// operations are exchanged, rather than the whole document object.
51
85
///
52
86
/// {@macro parse_deltas_custom_editor}
87
+ ///
88
+ /// {@macro merge_consecutive_blocks}
53
89
MutableDocument parseQuillDeltaOps (
54
90
List <dynamic > deltaOps, {
55
91
Editor ? customEditor,
56
92
List <BlockDeltaFormat > blockFormats = defaultBlockFormats,
93
+ List <DeltaBlockMergeRule > blockMergeRules = defaultBlockMergeRules,
57
94
List <InlineDeltaFormat > inlineFormats = defaultInlineFormats,
58
95
List <InlineEmbedFormat > inlineEmbedFormats = const [],
59
96
List <BlockDeltaFormat > embedBlockFormats = defaultEmbedBockFormats,
@@ -117,6 +154,7 @@ MutableDocument parseQuillDeltaOps(
117
154
delta.applyToDocument (
118
155
editor,
119
156
blockFormats: blockFormats,
157
+ blockMergeRules: blockMergeRules,
120
158
inlineFormats: inlineFormats,
121
159
inlineEmbedFormats: inlineEmbedFormats,
122
160
embedBlockFormats: embedBlockFormats,
@@ -179,6 +217,7 @@ extension OperationParser on Operation {
179
217
void applyToDocument (
180
218
Editor editor, {
181
219
required List <BlockDeltaFormat > blockFormats,
220
+ List <DeltaBlockMergeRule > blockMergeRules = defaultBlockMergeRules,
182
221
required List <InlineDeltaFormat > inlineFormats,
183
222
required List <InlineEmbedFormat > inlineEmbedFormats,
184
223
required List <BlockDeltaFormat > embedBlockFormats,
@@ -197,44 +236,65 @@ extension OperationParser on Operation {
197
236
_doInsertMedia (editor, composer, inlineEmbedFormats, embedBlockFormats);
198
237
}
199
238
200
- // Deduplicate all back-to-back code blocks .
239
+ // Merge consecutive blocks as desired by the given node types .
201
240
final document = editor.context.find <MutableDocument >(Editor .documentKey);
202
241
if (document.nodeCount < 3 ) {
203
- // Minimum of 3 nodes: code, code , newline.
242
+ // Minimum of 3 nodes: block, block , newline.
204
243
break ;
205
244
}
206
245
207
- var codeBlocks = < ParagraphNode > [];
246
+ // Beginning with the last non-empty node, move backwards, collecting all
247
+ // nodes that should be merged into one.
248
+ final nodeBeforeTrailingNewline = document.getNodeBefore (document.last)! ;
249
+ final blockTypeToMerge = nodeBeforeTrailingNewline.getMetadataValue (NodeMetadata .blockType);
250
+ var blocksToMerge = < ParagraphNode > [];
208
251
for (int i = document.nodeCount - 2 ; i >= 0 ; i -= 1 ) {
209
252
final node = document.getNodeAt (i)! ;
210
253
if (node is ! ParagraphNode ) {
211
254
break ;
212
255
}
213
- if (node.getMetadataValue ("blockType" ) != codeAttribution) {
256
+
257
+ var shouldMerge = false ;
258
+ for (final rule in blockMergeRules) {
259
+ final ruleShouldMerge = rule.shouldMerge (blockTypeToMerge, node.getMetadataValue (NodeMetadata .blockType));
260
+ if (ruleShouldMerge == true ) {
261
+ // The rule says we definitely want to merge.
262
+ shouldMerge = true ;
263
+ break ;
264
+ }
265
+ if (ruleShouldMerge == false ) {
266
+ // The rule says we definitely don't want to merge.
267
+ shouldMerge = false ;
268
+ break ;
269
+ }
270
+ }
271
+ if (! shouldMerge) {
272
+ // Our merge rules don't want us to merge this node.
214
273
break ;
215
274
}
216
275
217
- codeBlocks .add (node);
276
+ blocksToMerge .add (node);
218
277
}
219
278
220
- if (codeBlocks .length < 2 ) {
279
+ if (blocksToMerge .length < 2 ) {
221
280
break ;
222
281
}
223
282
224
- codeBlocks = codeBlocks.reversed.toList ();
225
- final mergeNode = codeBlocks.first;
226
- var codeToMove = codeBlocks[1 ].text.insertString (textToInsert: "\n " , startOffset: 0 );
227
- for (int i = 2 ; i < codeBlocks.length; i += 1 ) {
228
- codeToMove = codeToMove.copyAndAppend (codeBlocks[i].text.insertString (textToInsert: "\n " , startOffset: 0 ));
283
+ blocksToMerge = blocksToMerge.reversed.toList ();
284
+ final mergeNode = blocksToMerge.first;
285
+ var nodeContentToMove = blocksToMerge[1 ].text.insertString (textToInsert: "\n " , startOffset: 0 );
286
+ for (int i = 2 ; i < blocksToMerge.length; i += 1 ) {
287
+ nodeContentToMove =
288
+ nodeContentToMove.copyAndAppend (blocksToMerge[i].text.insertString (textToInsert: "\n " , startOffset: 0 ));
229
289
}
230
290
231
291
editor.execute ([
232
292
InsertAttributedTextRequest (
233
293
DocumentPosition (nodeId: mergeNode.id, nodePosition: mergeNode.endPosition),
234
- codeToMove ,
294
+ nodeContentToMove ,
235
295
),
236
- for (int i = 1 ; i < codeBlocks .length; i += 1 ) //
237
- DeleteNodeRequest (nodeId: codeBlocks [i].id),
296
+ for (int i = 1 ; i < blocksToMerge .length; i += 1 ) //
297
+ DeleteNodeRequest (nodeId: blocksToMerge [i].id),
238
298
]);
239
299
240
300
case DeltaOperationType .retain:
@@ -457,7 +517,7 @@ extension OperationParser on Operation {
457
517
458
518
// The caret wants to move beyond this paragraph.
459
519
unitsToMove -= selectedNode.text.length - currentPosition.offset;
460
- selectedNode = document.getNodeAfter (selectedNode)! ;
520
+ selectedNode = document.getNodeAfterById (selectedNode.id )! ;
461
521
caretPosition = DocumentPosition (
462
522
nodeId: selectedNode.id,
463
523
nodePosition: selectedNode.beginningPosition,
@@ -476,7 +536,7 @@ extension OperationParser on Operation {
476
536
477
537
// The deltas want to retain more beyond this node.
478
538
unitsToMove -= 1 ;
479
- selectedNode = document.getNodeAfter (selectedNode)! ;
539
+ selectedNode = document.getNodeAfterById (selectedNode.id )! ;
480
540
caretPosition = DocumentPosition (
481
541
nodeId: selectedNode.id,
482
542
nodePosition: selectedNode.beginningPosition,
@@ -510,3 +570,39 @@ enum DeltaOperationType {
510
570
retain,
511
571
delete,
512
572
}
573
+
574
+ /// The standard set of [DeltaBlockMergeRule] s used when parsing Quill Deltas.
575
+ const defaultBlockMergeRules = [
576
+ MergeBlock (blockquoteAttribution),
577
+ MergeBlock (codeAttribution),
578
+ ];
579
+
580
+ /// A rule that decides whether a given [DocumentNode] should be merged into
581
+ /// the node before it, when creating a [Document] from Quill Deltas.
582
+ ///
583
+ /// This is useful, for example, to place multiple lines of code within a
584
+ /// single code block.
585
+ abstract interface class DeltaBlockMergeRule {
586
+ /// Returns `true` if two consecutive blocks with the given types should merge,
587
+ /// `false` if they shouldn't, or `null` if this rule has no opinion about the merge.
588
+ bool ? shouldMerge (Attribution block1, Attribution block2);
589
+ }
590
+
591
+ /// A [DeltaBlockMergeRule] that chooses to merge blocks whose type `==`
592
+ /// the given block type.
593
+ class MergeBlock implements DeltaBlockMergeRule {
594
+ const MergeBlock (this ._blockType);
595
+
596
+ final Attribution _blockType;
597
+
598
+ @override
599
+ bool ? shouldMerge (Attribution block1, Attribution block2) {
600
+ if (block1 == _blockType && block2 == _blockType) {
601
+ // Yes, try to merge them.
602
+ return true ;
603
+ }
604
+
605
+ // This isn't our block type. We don't have an opinion.
606
+ return null ;
607
+ }
608
+ }
0 commit comments