Skip to content

Commit 281fa02

Browse files
[Quill] - Serialize quill inline embeds for inline placeholders (Resolves #2590) (#2634)
1 parent de59dd0 commit 281fa02

File tree

3 files changed

+261
-19
lines changed

3 files changed

+261
-19
lines changed

super_editor_quill/lib/src/serializing/serializers.dart

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
44
import 'package:super_editor/super_editor.dart';
55
import 'package:super_editor_quill/src/content/formatting.dart';
66
import 'package:super_editor_quill/src/content/multimedia.dart';
7+
import 'package:super_editor_quill/src/parsing/inline_formats.dart';
78

89
/// A [DeltaSerializer] that serializes [ParagraphNode]s into deltas.
910
const paragraphDeltaSerializer = ParagraphDeltaSerializer();
@@ -189,12 +190,13 @@ class TextBlockDeltaSerializer implements DeltaSerializer {
189190

190191
for (int i = 0; i < spans.length; i += 1) {
191192
final span = spans[i];
192-
final text = line.toPlainText().substring(span.start, line.isNotEmpty ? span.end + 1 : span.end);
193+
final spanText = line.copyText(span.start, line.isNotEmpty ? span.end + 1 : span.end);
194+
final spanPlainText = line.toPlainText().substring(span.start, line.isNotEmpty ? span.end + 1 : span.end);
193195

194196
// Attempt to serialize this text span as an inline embed.
195197
bool didSerializeAsInlineEmbed = false;
196198
for (final inlineEmbedSerializer in inlineEmbedDeltaSerializers) {
197-
didSerializeAsInlineEmbed = inlineEmbedSerializer.serialize(text, span.attributions, deltas);
199+
didSerializeAsInlineEmbed = inlineEmbedSerializer.serializeText(spanPlainText, span.attributions, deltas);
198200
if (didSerializeAsInlineEmbed) {
199201
// This span was successfully serialized as an inline embed. Skip remaining
200202
// inline embed serializers.
@@ -209,19 +211,57 @@ class TextBlockDeltaSerializer implements DeltaSerializer {
209211

210212
// This span doesn't refer to an inline embed - it's just inline text with some styles.
211213
// Serialize the text and styles.
212-
final inlineAttributes = getInlineAttributesFor(span.attributions);
213-
final newDelta = Operation.insert(
214-
text,
215-
inlineAttributes.isNotEmpty ? inlineAttributes : null,
216-
);
217-
218-
final previousDelta = deltas.operations.lastOrNull;
219-
if (previousDelta != null && !previousDelta.hasBlockFormats && newDelta.canMergeWith(previousDelta)) {
220-
deltas.operations[deltas.operations.length - 1] = newDelta.mergeWith(previousDelta);
221-
continue;
214+
final placeholderIndices = spanText.placeholders.keys.toList();
215+
final textRunsAndPlaceholders = <Object>[];
216+
int start = 0;
217+
for (final placeholderIndex in placeholderIndices) {
218+
if (placeholderIndex >= spanText.length) {
219+
continue;
220+
}
221+
222+
final textRun = spanText.substring(start, placeholderIndex);
223+
if (textRun.isNotEmpty) {
224+
textRunsAndPlaceholders.add(textRun);
225+
}
226+
textRunsAndPlaceholders.add(spanText.placeholders[placeholderIndex]!);
227+
228+
start = placeholderIndex + 1;
222229
}
230+
if (start != spanText.length) {
231+
textRunsAndPlaceholders.add(spanText.substring(start));
232+
}
233+
234+
final inlineAttributes = getInlineAttributesFor(span.attributions);
235+
for (final item in textRunsAndPlaceholders) {
236+
if (item is! String) {
237+
// This is an inline placeholder. Try to embed it.
238+
for (final inlineSerializer in inlineEmbedDeltaSerializers) {
239+
final didSerialize = inlineSerializer.serializeInlinePlaceholder(item, inlineAttributes, deltas);
240+
if (didSerialize) {
241+
// We successfully serialized the placeholder. We're done with this item.
242+
continue;
243+
}
244+
}
245+
246+
// We failed to serialize this placeholder. Ignore it and continue
247+
// processing items.
248+
continue;
249+
}
250+
251+
// This is a text run.
252+
final newDelta = Operation.insert(
253+
item,
254+
inlineAttributes.isNotEmpty ? inlineAttributes : null,
255+
);
256+
257+
final previousDelta = deltas.operations.lastOrNull;
258+
if (previousDelta != null && !previousDelta.hasBlockFormats && newDelta.canMergeWith(previousDelta)) {
259+
deltas.operations[deltas.operations.length - 1] = newDelta.mergeWith(previousDelta);
260+
continue;
261+
}
223262

224-
deltas.operations.add(newDelta);
263+
deltas.operations.add(newDelta);
264+
}
225265
}
226266

227267
if (line.isNotEmpty && line.last == "\n") {
@@ -401,8 +441,13 @@ abstract interface class DeltaSerializer {
401441
abstract interface class InlineEmbedDeltaSerializer {
402442
/// Tries to serialize the given [text] into the given [deltas].
403443
///
404-
/// If this serializer doesn't apply to the given [text], the behavior is a no-op.
405-
bool serialize(String text, Set<Attribution> attributions, Delta deltas);
444+
/// If this serializer doesn't apply to the given [text], nothing happens.
445+
bool serializeText(String text, Set<Attribution> attributions, Delta deltas);
446+
447+
/// Tries to serialize the given inline [placeholder] into the given [deltas].
448+
///
449+
/// If this serialize doesn't apply to the given [placeholder], nothing happens.
450+
bool serializeInlinePlaceholder(Object placeholder, Map<String, dynamic> attributes, Delta deltas);
406451
}
407452

408453
extension DeltaSerialization on Operation {

super_editor_quill/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies:
2323
flutter:
2424
sdk: flutter
2525

26-
super_editor: ^0.3.0-dev.13
26+
super_editor: ^0.3.0-dev.18
2727
logging: ^1.3.0
2828
dart_quill_delta: ^9.4.1
2929
collection: ^1.18.0

super_editor_quill/test/serializing_test.dart

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ void main() {
244244
});
245245

246246
group("custom serializers >", () {
247-
test("can serialize inline embeds", () {
247+
test("can serialize inline embeds from attributions", () {
248248
const userMentionAttribution = _UserTagAttribution("123456");
249249

250250
final deltas = MutableDocument(
@@ -297,6 +297,164 @@ void main() {
297297
expect(deltas, quillDocumentEquivalentTo(expectedDeltas));
298298
});
299299

300+
group("inline placeholders >", () {
301+
test("in the middle of text", () {
302+
final deltas = MutableDocument(
303+
nodes: [
304+
ParagraphNode(
305+
id: "1",
306+
text: AttributedText(
307+
"Before images >< in between images >< after images.",
308+
null,
309+
{
310+
15: const _InlineImage("http://www.somedomain.com/image1.png"),
311+
37: const _InlineImage("http://www.somedomain.com/image2.png"),
312+
},
313+
),
314+
),
315+
],
316+
).toQuillDeltas(
317+
serializers: _serializersWithInlineEmbeds,
318+
);
319+
320+
final expectedDeltas = Delta.fromJson([
321+
{"insert": "Before images >"},
322+
{
323+
"insert": {
324+
"image": {
325+
"url": "http://www.somedomain.com/image1.png",
326+
},
327+
},
328+
},
329+
{"insert": "< in between images >"},
330+
{
331+
"insert": {
332+
"image": {
333+
"url": "http://www.somedomain.com/image2.png",
334+
},
335+
},
336+
},
337+
{"insert": "< after images.\n"},
338+
]);
339+
340+
expect(deltas, quillDocumentEquivalentTo(expectedDeltas));
341+
});
342+
343+
test("at the start and end of text", () {
344+
final deltas = MutableDocument(
345+
nodes: [
346+
ParagraphNode(
347+
id: "1",
348+
text: AttributedText(
349+
" < Text between images > ",
350+
null,
351+
{
352+
0: const _InlineImage("http://www.somedomain.com/image1.png"),
353+
26: const _InlineImage("http://www.somedomain.com/image2.png"),
354+
},
355+
),
356+
),
357+
],
358+
).toQuillDeltas(
359+
serializers: _serializersWithInlineEmbeds,
360+
);
361+
362+
final expectedDeltas = Delta.fromJson([
363+
{
364+
"insert": {
365+
"image": {
366+
"url": "http://www.somedomain.com/image1.png",
367+
},
368+
},
369+
},
370+
{"insert": " < Text between images > "},
371+
{
372+
"insert": {
373+
"image": {
374+
"url": "http://www.somedomain.com/image2.png",
375+
},
376+
},
377+
},
378+
{"insert": "\n"},
379+
]);
380+
381+
expect(deltas, quillDocumentEquivalentTo(expectedDeltas));
382+
});
383+
384+
test("within attribution spans", () {
385+
final deltas = MutableDocument(
386+
nodes: [
387+
ParagraphNode(
388+
id: "1",
389+
text: AttributedText(
390+
"Before attribution |< text >< text >| after attribution.",
391+
AttributedSpans(
392+
attributions: [
393+
const SpanMarker(
394+
attribution: boldAttribution,
395+
offset: 20,
396+
markerType: SpanMarkerType.start,
397+
),
398+
const SpanMarker(
399+
attribution: boldAttribution,
400+
offset: 38,
401+
markerType: SpanMarkerType.end,
402+
),
403+
],
404+
),
405+
{
406+
20: const _InlineImage("http://www.somedomain.com/image1.png"),
407+
29: const _InlineImage("http://www.somedomain.com/image2.png"),
408+
38: const _InlineImage("http://www.somedomain.com/image3.png"),
409+
},
410+
),
411+
),
412+
],
413+
).toQuillDeltas(
414+
serializers: _serializersWithInlineEmbeds,
415+
);
416+
417+
final expectedDeltas = Delta.fromJson([
418+
{"insert": "Before attribution |"},
419+
{
420+
"insert": {
421+
"image": {
422+
"url": "http://www.somedomain.com/image1.png",
423+
},
424+
},
425+
"attributes": {"bold": true},
426+
},
427+
{
428+
"insert": "< text >",
429+
"attributes": {"bold": true},
430+
},
431+
{
432+
"insert": {
433+
"image": {
434+
"url": "http://www.somedomain.com/image2.png",
435+
},
436+
},
437+
"attributes": {"bold": true},
438+
},
439+
{
440+
"insert": "< text >",
441+
"attributes": {"bold": true},
442+
},
443+
{
444+
"insert": {
445+
"image": {
446+
"url": "http://www.somedomain.com/image3.png",
447+
},
448+
},
449+
"attributes": {"bold": true},
450+
},
451+
{"insert": "| after attribution.\n"},
452+
]);
453+
454+
expect(deltas, quillDocumentEquivalentTo(expectedDeltas));
455+
});
456+
});
457+
300458
test("doesn't merge custom block with previous delta", () {
301459
final deltas = MutableDocument(
302460
nodes: [
@@ -349,13 +507,49 @@ const _serializersWithInlineEmbeds = [
349507
fileDeltaSerializer,
350508
];
351509

352-
const _inlineEmbedSerializers = [_UserTagInlineEmbedSerializer()];
510+
const _inlineEmbedSerializers = [
511+
_InlineImageEmbedSerializer(),
512+
_UserTagInlineEmbedSerializer(),
513+
];
514+
515+
class _InlineImageEmbedSerializer implements InlineEmbedDeltaSerializer {
516+
const _InlineImageEmbedSerializer();
517+
518+
@override
519+
bool serializeText(String text, Set<Attribution> attributions, Delta deltas) => false;
520+
521+
@override
522+
bool serializeInlinePlaceholder(Object placeholder, Map<String, dynamic> attributes, Delta deltas) {
523+
if (placeholder is! _InlineImage) {
524+
return false;
525+
}
526+
527+
deltas.operations.add(
528+
Operation.insert(
529+
{
530+
"image": {
531+
"url": placeholder.url,
532+
},
533+
},
534+
attributes.isNotEmpty ? attributes : null,
535+
),
536+
);
537+
538+
return true;
539+
}
540+
}
541+
542+
class _InlineImage {
543+
const _InlineImage(this.url);
544+
545+
final String url;
546+
}
353547

354548
class _UserTagInlineEmbedSerializer implements InlineEmbedDeltaSerializer {
355549
const _UserTagInlineEmbedSerializer();
356550

357551
@override
358-
bool serialize(String text, Set<Attribution> attributions, Delta deltas) {
552+
bool serializeText(String text, Set<Attribution> attributions, Delta deltas) {
359553
final userTag = attributions.whereType<_UserTagAttribution>().firstOrNull;
360554
if (userTag == null) {
361555
return false;
@@ -373,6 +567,9 @@ class _UserTagInlineEmbedSerializer implements InlineEmbedDeltaSerializer {
373567

374568
return true;
375569
}
570+
571+
@override
572+
bool serializeInlinePlaceholder(Object placeholder, Map<String, dynamic> attributes, Delta deltas) => false;
376573
}
377574

378575
class _UserTagAttribution implements Attribution {

0 commit comments

Comments
 (0)