Skip to content

Commit 8cfab36

Browse files
[super_editor_markdown] - serialization fixes (Resolves #712) (#713)
1 parent a4884bb commit 8cfab36

File tree

2 files changed

+132
-30
lines changed

2 files changed

+132
-30
lines changed

super_editor_markdown/lib/src/markdown.dart

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:convert';
22

3+
import 'package:flutter/widgets.dart';
34
import 'package:markdown/markdown.dart' as md;
45
import 'package:super_editor/super_editor.dart';
56

@@ -411,7 +412,7 @@ class _InlineMarkdownToDocument implements md.NodeVisitor {
411412
}
412413
}
413414

414-
extension on AttributedText {
415+
extension Markdown on AttributedText {
415416
String toMarkdown() {
416417
final serializer = AttributedTextMarkdownSerializer();
417418
return serializer.serialize(this);
@@ -420,38 +421,29 @@ extension on AttributedText {
420421

421422
/// Serializes an [AttributedText] into markdown format
422423
class AttributedTextMarkdownSerializer extends AttributionVisitor {
423-
late String _text;
424-
int _spanStart = 0;
425-
final _buffer = StringBuffer();
424+
late String _fullText;
425+
late StringBuffer _buffer;
426+
late int _bufferCursor;
426427

427428
String serialize(AttributedText attributedText) {
428-
_text = attributedText.text;
429+
_fullText = attributedText.text;
430+
_buffer = StringBuffer();
431+
_bufferCursor = 0;
429432
attributedText.visitAttributions(this);
430433
return _buffer.toString();
431434
}
432435

433436
@override
434-
void visitAttributions(AttributedText fullText, int index, Set<Attribution> startingAttributions, Set<Attribution> endingAttributions) {
435-
// Add end markers.
436-
if (endingAttributions.isNotEmpty) {
437-
final markdownStyles = _sortAndSerializeAttributions(endingAttributions, AttributionVisitEvent.end);
438-
// Links are different from the plain styles since they are both not NamedAttributions (and therefore
439-
// can't be checked using equality comparison) and asymmetrical in markdown.
440-
final linkMarker = _encodeLinkMarker(endingAttributions, AttributionVisitEvent.end);
441-
442-
// +1 on end index because this visitor has inclusive indices
443-
// whereas substring() expects an exclusive ending index.
444-
_buffer
445-
..write(fullText.text.substring(_spanStart, index + 1))
446-
..write(markdownStyles)
447-
..write(linkMarker);
448-
449-
// When we reach the end of an attribution we need to hold the start of the next span,
450-
// because if the last span has no attributions we will not visit any other index with
451-
// a start marker.
452-
// After we visit all the indexes we add the remaining text to the buffer.
453-
_spanStart = index + 1;
454-
}
437+
void visitAttributions(
438+
AttributedText fullText,
439+
int index,
440+
Set<Attribution> startingAttributions,
441+
Set<Attribution> endingAttributions,
442+
) {
443+
// Write out the text between the end of the last markers, and these new markers.
444+
_buffer.write(
445+
fullText.text.substring(_bufferCursor, index),
446+
);
455447

456448
// Add start markers.
457449
if (startingAttributions.isNotEmpty) {
@@ -461,19 +453,34 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor {
461453
final linkMarker = _encodeLinkMarker(startingAttributions, AttributionVisitEvent.start);
462454

463455
_buffer
464-
..write(fullText.text.substring(_spanStart, index))
465456
..write(linkMarker)
466457
..write(markdownStyles);
458+
}
459+
460+
// Write out the character at this index.
461+
_buffer.write(_fullText[index]);
462+
_bufferCursor = index + 1;
463+
464+
// Add end markers.
465+
if (endingAttributions.isNotEmpty) {
466+
final markdownStyles = _sortAndSerializeAttributions(endingAttributions, AttributionVisitEvent.end);
467+
// Links are different from the plain styles since they are both not NamedAttributions (and therefore
468+
// can't be checked using equality comparison) and asymmetrical in markdown.
469+
final linkMarker = _encodeLinkMarker(endingAttributions, AttributionVisitEvent.end);
467470

468-
_spanStart = index;
471+
// +1 on end index because this visitor has inclusive indices
472+
// whereas substring() expects an exclusive ending index.
473+
_buffer
474+
..write(markdownStyles)
475+
..write(linkMarker);
469476
}
470477
}
471478

472479
@override
473480
void onVisitEnd() {
474481
// When the last span has no attributions, we still have text that wasn't added to the buffer yet.
475-
if (_spanStart <= _text.length - 1) {
476-
_buffer.write(_text.substring(_spanStart));
482+
if (_bufferCursor <= _fullText.length - 1) {
483+
_buffer.write(_fullText.substring(_bufferCursor));
477484
}
478485
}
479486

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:super_editor/super_editor.dart';
3+
import 'package:super_editor_markdown/src/markdown.dart';
4+
5+
void main() {
6+
group("AttributedText markdown serializes", () {
7+
test("un-styled text", () {
8+
expect(
9+
AttributedText(text: "This is unstyled text.").toMarkdown(),
10+
"This is unstyled text.",
11+
);
12+
});
13+
14+
test("single character styles", () {
15+
expect(
16+
AttributedText(
17+
text: "This is single character styles.",
18+
spans: AttributedSpans(
19+
attributions: [
20+
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
21+
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.end),
22+
SpanMarker(attribution: italicsAttribution, offset: 23, markerType: SpanMarkerType.start),
23+
SpanMarker(attribution: italicsAttribution, offset: 23, markerType: SpanMarkerType.end),
24+
],
25+
),
26+
).toMarkdown(),
27+
"This is **s**ingle characte*r* styles.",
28+
);
29+
});
30+
31+
test("bold text", () {
32+
expect(
33+
AttributedText(
34+
text: "This is bold text.",
35+
spans: AttributedSpans(
36+
attributions: [
37+
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
38+
SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.end),
39+
],
40+
),
41+
).toMarkdown(),
42+
"This is **bold** text.",
43+
);
44+
});
45+
46+
test("italics text", () {
47+
expect(
48+
AttributedText(
49+
text: "This is italics text.",
50+
spans: AttributedSpans(
51+
attributions: [
52+
SpanMarker(attribution: italicsAttribution, offset: 8, markerType: SpanMarkerType.start),
53+
SpanMarker(attribution: italicsAttribution, offset: 14, markerType: SpanMarkerType.end),
54+
],
55+
),
56+
).toMarkdown(),
57+
"This is *italics* text.",
58+
);
59+
});
60+
61+
test("multiple styles across the same span", () {
62+
expect(
63+
AttributedText(
64+
text: "This is multiple styled text.",
65+
spans: AttributedSpans(
66+
attributions: [
67+
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
68+
SpanMarker(attribution: boldAttribution, offset: 22, markerType: SpanMarkerType.end),
69+
SpanMarker(attribution: italicsAttribution, offset: 8, markerType: SpanMarkerType.start),
70+
SpanMarker(attribution: italicsAttribution, offset: 22, markerType: SpanMarkerType.end),
71+
],
72+
),
73+
).toMarkdown(),
74+
"This is ***multiple styled*** text.",
75+
);
76+
});
77+
78+
test("partially overlapping styles", () {
79+
expect(
80+
AttributedText(
81+
text: "This is overlapping styles.",
82+
spans: AttributedSpans(
83+
attributions: [
84+
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
85+
SpanMarker(attribution: boldAttribution, offset: 13, markerType: SpanMarkerType.end),
86+
SpanMarker(attribution: italicsAttribution, offset: 11, markerType: SpanMarkerType.start),
87+
SpanMarker(attribution: italicsAttribution, offset: 18, markerType: SpanMarkerType.end),
88+
],
89+
),
90+
).toMarkdown(),
91+
"This is **ove*rla**pping* styles.",
92+
);
93+
});
94+
});
95+
}

0 commit comments

Comments
 (0)