Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4f5da2a

Browse files
committedApr 1, 2025·
WIP: 2590 - serialize quill inline embeds for inline placeholders
1 parent 652f8ee commit 4f5da2a

File tree

3 files changed

+261
-19
lines changed

3 files changed

+261
-19
lines changed
 

‎super_editor_quill/lib/src/serializing/serializers.dart

+60-15
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

+1-1
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

+200-3
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ void main() {
227227
});
228228

229229
group("custom serializers >", () {
230-
test("can serialize inline embeds", () {
230+
test("can serialize inline embeds from attributions", () {
231231
const userMentionAttribution = _UserTagAttribution("123456");
232232

233233
final deltas = MutableDocument(
@@ -280,6 +280,164 @@ void main() {
280280
expect(deltas, quillDocumentEquivalentTo(expectedDeltas));
281281
});
282282

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

335-
const _inlineEmbedSerializers = [_UserTagInlineEmbedSerializer()];
493+
const _inlineEmbedSerializers = [
494+
_InlineImageEmbedSerializer(),
495+
_UserTagInlineEmbedSerializer(),
496+
];
497+
498+
class _InlineImageEmbedSerializer implements InlineEmbedDeltaSerializer {
499+
const _InlineImageEmbedSerializer();
500+
501+
@override
502+
bool serializeText(String text, Set<Attribution> attributions, Delta deltas) => false;
503+
504+
@override
505+
bool serializeInlinePlaceholder(Object placeholder, Map<String, dynamic> attributes, Delta deltas) {
506+
if (placeholder is! _InlineImage) {
507+
return false;
508+
}
509+
510+
deltas.operations.add(
511+
Operation.insert(
512+
{
513+
"image": {
514+
"url": placeholder.url,
515+
},
516+
},
517+
attributes.isNotEmpty ? attributes : null,
518+
),
519+
);
520+
521+
return true;
522+
}
523+
}
524+
525+
class _InlineImage {
526+
const _InlineImage(this.url);
527+
528+
final String url;
529+
}
336530

337531
class _UserTagInlineEmbedSerializer implements InlineEmbedDeltaSerializer {
338532
const _UserTagInlineEmbedSerializer();
339533

340534
@override
341-
bool serialize(String text, Set<Attribution> attributions, Delta deltas) {
535+
bool serializeText(String text, Set<Attribution> attributions, Delta deltas) {
342536
final userTag = attributions.whereType<_UserTagAttribution>().firstOrNull;
343537
if (userTag == null) {
344538
return false;
@@ -356,6 +550,9 @@ class _UserTagInlineEmbedSerializer implements InlineEmbedDeltaSerializer {
356550

357551
return true;
358552
}
553+
554+
@override
555+
bool serializeInlinePlaceholder(Object placeholder, Map<String, dynamic> attributes, Delta deltas) => false;
359556
}
360557

361558
class _UserTagAttribution implements Attribution {

0 commit comments

Comments
 (0)
Please sign in to comment.