Skip to content

Commit

Permalink
PR Updates, also fixed AttributedText Markdown serialization bug
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll committed Jul 23, 2022
1 parent e0a42bf commit e2be77d
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ChangeSelectionCommand extends AttributedTextEditingValueCommand {
_previousComposingRange = previousValue.composingRegion;

return AttributedTextEditingValue(
text: previousValue.text,
text: previousValue.text.copy(),
selection: newSelection,
composingRegion: newComposingRange ?? TextRange.empty,
);
Expand All @@ -33,7 +33,7 @@ class ChangeSelectionCommand extends AttributedTextEditingValueCommand {
@override
AttributedTextEditingValue doUndo(AttributedTextEditingValue currentValue) {
return AttributedTextEditingValue(
text: currentValue.text,
text: currentValue.text.copy(),
selection: _previousSelection!,
composingRegion: _previousComposingRange!,
);
Expand Down Expand Up @@ -263,15 +263,6 @@ class InsertTextAtOffsetCommand extends AttributedTextEditingValueCommand {
}
}

extension on AttributedText {
AttributedText copy() {
return AttributedText(
text: text,
spans: spans.copy(),
);
}
}

/// Deletes the currently selected text, collapsing the selection to a caret
/// at the selection base.
class DeleteSelectedTextCommand extends AttributedTextEditingValueCommand {
Expand Down Expand Up @@ -517,3 +508,12 @@ int _moveCaretForDeletion({
return deleteFrom + (caretOffset - deleteTo);
}
}

extension on AttributedText {
AttributedText copy() {
return AttributedText(
text: text,
spans: spans.copy(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -833,11 +833,24 @@ void main() {

group("deletes text", () {
test("between the caret and the beginning of the line", () {
// TODO:
});
final controller = EventSourcedAttributedTextEditingController(
AttributedTextEditingValue(
text: AttributedText(text: "before the caret:after"),
selection: const TextSelection.collapsed(offset: 16),
),
);

controller.deleteTextOnLineBeforeCaret(textLayout: _FakeTextLayout(["before the caret:after"]));
expect(controller.text.text, ":after");
expect(controller.selection, const TextSelection.collapsed(offset: 0));

test("between the caret and the end of the line", () {
// TODO:
// Undo it.
controller.undo();
expect(controller.text.text, "before the caret:after");
expect(
controller.selection,
const TextSelection.collapsed(offset: 16),
);
});

test("when it's selected", () {
Expand Down Expand Up @@ -1138,10 +1151,6 @@ void main() {
));
});
});

test("pastes text from clipboard", () {
// TODO:
});
});
}

Expand Down
65 changes: 36 additions & 29 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 @@ -420,38 +421,29 @@ extension Markdown 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 e2be77d

Please sign in to comment.