Skip to content

Commit

Permalink
[super_editor_markdown] - serialization fixes (Resolves #712) (#713)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll authored Jul 25, 2022
1 parent a4884bb commit 8cfab36
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 30 deletions.
67 changes: 37 additions & 30 deletions super_editor_markdown/lib/src/markdown.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:convert';

import 'package:flutter/widgets.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:super_editor/super_editor.dart';

Expand Down Expand Up @@ -411,7 +412,7 @@ class _InlineMarkdownToDocument implements md.NodeVisitor {
}
}

extension on AttributedText {
extension Markdown on AttributedText {
String toMarkdown() {
final serializer = AttributedTextMarkdownSerializer();
return serializer.serialize(this);
Expand All @@ -420,38 +421,29 @@ extension on AttributedText {

/// Serializes an [AttributedText] into markdown format
class AttributedTextMarkdownSerializer extends AttributionVisitor {
late String _text;
int _spanStart = 0;
final _buffer = StringBuffer();
late String _fullText;
late StringBuffer _buffer;
late int _bufferCursor;

String serialize(AttributedText attributedText) {
_text = attributedText.text;
_fullText = attributedText.text;
_buffer = StringBuffer();
_bufferCursor = 0;
attributedText.visitAttributions(this);
return _buffer.toString();
}

@override
void visitAttributions(AttributedText fullText, int index, Set<Attribution> startingAttributions, Set<Attribution> endingAttributions) {
// Add end markers.
if (endingAttributions.isNotEmpty) {
final markdownStyles = _sortAndSerializeAttributions(endingAttributions, AttributionVisitEvent.end);
// Links are different from the plain styles since they are both not NamedAttributions (and therefore
// can't be checked using equality comparison) and asymmetrical in markdown.
final linkMarker = _encodeLinkMarker(endingAttributions, AttributionVisitEvent.end);

// +1 on end index because this visitor has inclusive indices
// whereas substring() expects an exclusive ending index.
_buffer
..write(fullText.text.substring(_spanStart, index + 1))
..write(markdownStyles)
..write(linkMarker);

// When we reach the end of an attribution we need to hold the start of the next span,
// because if the last span has no attributions we will not visit any other index with
// a start marker.
// After we visit all the indexes we add the remaining text to the buffer.
_spanStart = index + 1;
}
void visitAttributions(
AttributedText fullText,
int index,
Set<Attribution> startingAttributions,
Set<Attribution> endingAttributions,
) {
// Write out the text between the end of the last markers, and these new markers.
_buffer.write(
fullText.text.substring(_bufferCursor, index),
);

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

_buffer
..write(fullText.text.substring(_spanStart, index))
..write(linkMarker)
..write(markdownStyles);
}

// Write out the character at this index.
_buffer.write(_fullText[index]);
_bufferCursor = index + 1;

// Add end markers.
if (endingAttributions.isNotEmpty) {
final markdownStyles = _sortAndSerializeAttributions(endingAttributions, AttributionVisitEvent.end);
// Links are different from the plain styles since they are both not NamedAttributions (and therefore
// can't be checked using equality comparison) and asymmetrical in markdown.
final linkMarker = _encodeLinkMarker(endingAttributions, AttributionVisitEvent.end);

_spanStart = index;
// +1 on end index because this visitor has inclusive indices
// whereas substring() expects an exclusive ending index.
_buffer
..write(markdownStyles)
..write(linkMarker);
}
}

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

Expand Down
95 changes: 95 additions & 0 deletions super_editor_markdown/test/attributed_text_markdown_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor_markdown/src/markdown.dart';

void main() {
group("AttributedText markdown serializes", () {
test("un-styled text", () {
expect(
AttributedText(text: "This is unstyled text.").toMarkdown(),
"This is unstyled text.",
);
});

test("single character styles", () {
expect(
AttributedText(
text: "This is single character styles.",
spans: AttributedSpans(
attributions: [
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.end),
SpanMarker(attribution: italicsAttribution, offset: 23, markerType: SpanMarkerType.start),
SpanMarker(attribution: italicsAttribution, offset: 23, markerType: SpanMarkerType.end),
],
),
).toMarkdown(),
"This is **s**ingle characte*r* styles.",
);
});

test("bold text", () {
expect(
AttributedText(
text: "This is bold text.",
spans: AttributedSpans(
attributions: [
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.end),
],
),
).toMarkdown(),
"This is **bold** text.",
);
});

test("italics text", () {
expect(
AttributedText(
text: "This is italics text.",
spans: AttributedSpans(
attributions: [
SpanMarker(attribution: italicsAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: italicsAttribution, offset: 14, markerType: SpanMarkerType.end),
],
),
).toMarkdown(),
"This is *italics* text.",
);
});

test("multiple styles across the same span", () {
expect(
AttributedText(
text: "This is multiple styled text.",
spans: AttributedSpans(
attributions: [
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: boldAttribution, offset: 22, markerType: SpanMarkerType.end),
SpanMarker(attribution: italicsAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: italicsAttribution, offset: 22, markerType: SpanMarkerType.end),
],
),
).toMarkdown(),
"This is ***multiple styled*** text.",
);
});

test("partially overlapping styles", () {
expect(
AttributedText(
text: "This is overlapping styles.",
spans: AttributedSpans(
attributions: [
SpanMarker(attribution: boldAttribution, offset: 8, markerType: SpanMarkerType.start),
SpanMarker(attribution: boldAttribution, offset: 13, markerType: SpanMarkerType.end),
SpanMarker(attribution: italicsAttribution, offset: 11, markerType: SpanMarkerType.start),
SpanMarker(attribution: italicsAttribution, offset: 18, markerType: SpanMarkerType.end),
],
),
).toMarkdown(),
"This is **ove*rla**pping* styles.",
);
});
});
}

0 comments on commit 8cfab36

Please sign in to comment.