Skip to content

Commit ce0d3ed

Browse files
PR updates
1 parent a9c0c4f commit ce0d3ed

File tree

2 files changed

+145
-86
lines changed

2 files changed

+145
-86
lines changed

super_editor/lib/src/infrastructure/attributed_text_styles.dart

+45-62
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension ComputeTextSpan on AttributedText {
1919
/// The given [styleBuilder] interprets the meaning of every attribution
2020
/// and constructs [TextStyle]s accordingly.
2121
///
22-
/// The given [inlineWidgetBuilder] interprets every placeholder `Object`
22+
/// The given [inlineWidgetBuilders] interprets every placeholder `Object`
2323
/// and builds a corresponding inline widget.
2424
InlineSpan computeInlineSpan(
2525
BuildContext context,
@@ -34,80 +34,63 @@ extension ComputeTextSpan on AttributedText {
3434
final inlineSpans = <InlineSpan>[];
3535

3636
final collapsedSpans = spans.collapseSpans(contentLength: length);
37-
var spanIndex = 0;
38-
var span = collapsedSpans.first;
39-
40-
int start = 0;
41-
while (start < length) {
42-
late int contentEnd;
43-
if (placeholders[start] != null) {
44-
// This section is a placeholder.
45-
contentEnd = start + 1;
46-
47-
final textStyle = styleBuilder({});
48-
Widget? inlineWidget;
49-
for (final builder in inlineWidgetBuilders) {
50-
inlineWidget = builder(context, textStyle, placeholders[start]!);
51-
if (inlineWidget != null) {
52-
break;
37+
38+
for (final span in collapsedSpans) {
39+
final textStyle = styleBuilder(span.attributions);
40+
41+
// A single span might be divided in multiple inline spans if there are placeholders.
42+
// Keep track of the start of the current inline span.
43+
int startOfInlineSpan = span.start;
44+
45+
// Look for placeholders within the current span and split the span accordingly.
46+
int characterIndex = span.start;
47+
while (characterIndex <= span.end) {
48+
if (placeholders[characterIndex] != null) {
49+
// We found a placeholder. Build a widget for it.
50+
51+
if (characterIndex > startOfInlineSpan) {
52+
// There is text before the placeholder.
53+
inlineSpans.add(
54+
TextSpan(
55+
text: substring(startOfInlineSpan, characterIndex),
56+
style: textStyle,
57+
),
58+
);
5359
}
54-
}
5560

56-
if (inlineWidget != null) {
57-
inlineSpans.add(
58-
WidgetSpan(
59-
alignment: PlaceholderAlignment.middle,
60-
child: inlineWidget,
61-
),
62-
);
63-
}
64-
} else {
65-
// This section is text. The end of this text is either the
66-
// end of the current span, or the index of the next placeholder.
67-
contentEnd = span.end + 1;
68-
69-
final nextSpan = spanIndex + 1 < collapsedSpans.length //
70-
? collapsedSpans[spanIndex + 1]
71-
: null;
72-
for (final entry in placeholders.entries) {
73-
if (entry.key <= start) {
74-
// This placeholder is before the current span.
75-
continue;
61+
Widget? inlineWidget;
62+
for (final builder in inlineWidgetBuilders) {
63+
inlineWidget = builder(context, textStyle, placeholders[characterIndex]!);
64+
if (inlineWidget != null) {
65+
break;
66+
}
7667
}
7768

78-
if (nextSpan != null && entry.key >= nextSpan.start) {
79-
// This placeholder is beyond the next span.
80-
break;
69+
if (inlineWidget != null) {
70+
inlineSpans.add(
71+
WidgetSpan(
72+
alignment: PlaceholderAlignment.middle,
73+
child: inlineWidget,
74+
),
75+
);
8176
}
8277

83-
// This placeholder is within the current span.
84-
contentEnd = entry.key;
85-
break;
78+
// Start another inline span after the placeholder.
79+
startOfInlineSpan = characterIndex + 1;
8680
}
8781

82+
characterIndex += 1;
83+
}
84+
85+
if (startOfInlineSpan <= span.end) {
86+
// There is text after the last placeholder or there is no placeholder at all.
8887
inlineSpans.add(
8988
TextSpan(
90-
text: substring(start, contentEnd),
91-
style: styleBuilder(span.attributions),
89+
text: substring(startOfInlineSpan, span.end + 1),
90+
style: textStyle,
9291
),
9392
);
9493
}
95-
96-
if (contentEnd == span.end + 1) {
97-
// The content and span end at the same place.
98-
start = contentEnd;
99-
} else if (contentEnd < span.end + 1) {
100-
// The content ends before the span.
101-
start = contentEnd;
102-
} else {
103-
// The span ends before the content.
104-
start = span.end + 1;
105-
}
106-
107-
if (start > span.end && start < length) {
108-
spanIndex += 1;
109-
span = collapsedSpans[spanIndex];
110-
}
11194
}
11295

11396
return TextSpan(

super_editor/test/infrastructure/inline_span_test.dart

+100-24
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:super_editor/super_editor.dart';
44

55
void main() {
66
group('SuperEditor > computeInlineSpan >', () {
7-
testWidgets('does not modify text with attributions and a placeholder at the beginning', (tester) async {
7+
testWidgets('computes inlineSpan for text with attributions and a placeholder at the beginning', (tester) async {
88
// Pump a widget because we need a BuildContext to compute the InlineSpan.
99
await tester.pumpWidget(
1010
const MaterialApp(),
@@ -15,10 +15,10 @@ void main() {
1515
'Welcome to SuperEditor',
1616
AttributedSpans(
1717
attributions: [
18-
const SpanMarker(attribution: boldAttribution, offset: 0, markerType: SpanMarkerType.start),
19-
const SpanMarker(attribution: boldAttribution, offset: 6, markerType: SpanMarkerType.end),
20-
const SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.start),
21-
const SpanMarker(attribution: boldAttribution, offset: 21, markerType: SpanMarkerType.end),
18+
const SpanMarker(attribution: boldAttribution, offset: 1, markerType: SpanMarkerType.start),
19+
const SpanMarker(attribution: boldAttribution, offset: 7, markerType: SpanMarkerType.end),
20+
const SpanMarker(attribution: boldAttribution, offset: 12, markerType: SpanMarkerType.start),
21+
const SpanMarker(attribution: boldAttribution, offset: 22, markerType: SpanMarkerType.end),
2222
],
2323
),
2424
{0: const _ExamplePlaceholder()},
@@ -30,14 +30,34 @@ void main() {
3030
[_inlineWidgetBuilder],
3131
);
3232

33-
// Ensure the text was not modified.
34-
expect(
35-
inlineSpan.toPlainText(includePlaceholders: false),
36-
'Welcome to SuperEditor',
37-
);
33+
final spanList = _flattenInlineSpan(inlineSpan);
34+
expect(spanList.length, equals(5));
35+
36+
// Ensure that the first span is an empty TextSpan with the default fontWeight.
37+
expect(spanList[0], isA<TextSpan>());
38+
expect((spanList[0] as TextSpan).text, equals(''));
39+
expect((spanList[0] as TextSpan).style!.fontWeight, isNull);
40+
41+
// Expect that the second span is the widget rendered using the placeholder.
42+
expect(spanList[1], isA<WidgetSpan>());
43+
44+
// Ensure that the third span is a TextSpan with the text "Welcome" in bold.
45+
expect(spanList[2], isA<TextSpan>());
46+
expect((spanList[2] as TextSpan).text, equals('Welcome'));
47+
expect((spanList[2] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
48+
49+
// Ensure that the fourth span is a TextSpan with the text " to " with the default fontWeight.
50+
expect(spanList[3], isA<TextSpan>());
51+
expect((spanList[3] as TextSpan).text, equals(' to '));
52+
expect((spanList[3] as TextSpan).style!.fontWeight, isNull);
53+
54+
// Ensure that the fifth span is a TextSpan with the text "SuperEditor" in bold.
55+
expect(spanList[4], isA<TextSpan>());
56+
expect((spanList[4] as TextSpan).text, equals('SuperEditor'));
57+
expect((spanList[4] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
3858
});
3959

40-
testWidgets('does not modify text with attributions and a placeholder at the middle', (tester) async {
60+
testWidgets('computes inlineSpan for text with attributions and a placeholder at the middle', (tester) async {
4161
// Pump a widget because we need a BuildContext to compute the InlineSpan.
4262
await tester.pumpWidget(
4363
const MaterialApp(),
@@ -51,8 +71,8 @@ void main() {
5171
attributions: [
5272
const SpanMarker(attribution: boldAttribution, offset: 0, markerType: SpanMarkerType.start),
5373
const SpanMarker(attribution: boldAttribution, offset: 6, markerType: SpanMarkerType.end),
54-
const SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.start),
55-
const SpanMarker(attribution: boldAttribution, offset: 21, markerType: SpanMarkerType.end),
74+
const SpanMarker(attribution: boldAttribution, offset: 12, markerType: SpanMarkerType.start),
75+
const SpanMarker(attribution: boldAttribution, offset: 22, markerType: SpanMarkerType.end),
5676
],
5777
),
5878
{10: const _ExamplePlaceholder()},
@@ -64,14 +84,39 @@ void main() {
6484
[_inlineWidgetBuilder],
6585
);
6686

67-
// Ensure the text was not modified.
68-
expect(
69-
inlineSpan.toPlainText(includePlaceholders: false),
70-
'Welcome to SuperEditor',
71-
);
87+
final spanList = _flattenInlineSpan(inlineSpan);
88+
expect(spanList.length, equals(6));
89+
90+
// Ensure that the first span is an empty TextSpan with the default fontWeight.
91+
expect(spanList[0], isA<TextSpan>());
92+
expect((spanList[0] as TextSpan).text, equals(''));
93+
expect((spanList[0] as TextSpan).style!.fontWeight, isNull);
94+
95+
// Expect that the second span is a TextSpan with the text "Welcome" in bold.
96+
expect(spanList[1], isA<TextSpan>());
97+
expect((spanList[1] as TextSpan).text, equals('Welcome'));
98+
expect((spanList[1] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
99+
100+
// Ensure that the third span is a TextSpan with the text " to" with the default fontWeight.
101+
expect(spanList[2], isA<TextSpan>());
102+
expect((spanList[2] as TextSpan).text, equals(' to'));
103+
expect((spanList[2] as TextSpan).style!.fontWeight, isNull);
104+
105+
// Expect that the fourth span is the widget rendered using the placeholder.
106+
expect(spanList[3], isA<WidgetSpan>());
107+
108+
// Ensure that the fifth span is a TextSpan with the text " " with the default fontWeight.
109+
expect(spanList[4], isA<TextSpan>());
110+
expect((spanList[4] as TextSpan).text, equals(' '));
111+
expect((spanList[4] as TextSpan).style!.fontWeight, isNull);
112+
113+
// Ensure that the sixth span is a TextSpan with the text "SuperEditor" in bold.
114+
expect(spanList[5], isA<TextSpan>());
115+
expect((spanList[5] as TextSpan).text, equals('SuperEditor'));
116+
expect((spanList[5] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
72117
});
73118

74-
testWidgets('does not modify text with attributions and a placeholder at the end', (tester) async {
119+
testWidgets('computes inlineSpan for text with attributions and a placeholder at the end', (tester) async {
75120
// Pump a widget because we need a BuildContext to compute the InlineSpan.
76121
await tester.pumpWidget(
77122
const MaterialApp(),
@@ -97,15 +142,46 @@ void main() {
97142
[_inlineWidgetBuilder],
98143
);
99144

100-
// Ensure the text was not modified.
101-
expect(
102-
inlineSpan.toPlainText(includePlaceholders: false),
103-
'Welcome to SuperEditor',
104-
);
145+
final spanList = _flattenInlineSpan(inlineSpan);
146+
expect(spanList.length, equals(5));
147+
148+
// Ensure that the first span is an empty TextSpan with the default fontWeight.
149+
expect(spanList[0], isA<TextSpan>());
150+
expect((spanList[0] as TextSpan).text, equals(''));
151+
expect((spanList[0] as TextSpan).style!.fontWeight, isNull);
152+
153+
// Ensure that the second span is a TextSpan with the text "Welcome" in bold.
154+
expect(spanList[1], isA<TextSpan>());
155+
expect((spanList[1] as TextSpan).text, equals('Welcome'));
156+
expect((spanList[1] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
157+
158+
// Ensure that the third span is a TextSpan with the text " to " with the default fontWeight.
159+
expect(spanList[2], isA<TextSpan>());
160+
expect((spanList[2] as TextSpan).text, equals(' to '));
161+
expect((spanList[2] as TextSpan).style!.fontWeight, isNull);
162+
163+
// Ensure that the fourth span is a TextSpan with the text "SuperEditor" in bold.
164+
expect(spanList[3], isA<TextSpan>());
165+
expect((spanList[3] as TextSpan).text, equals('SuperEditor'));
166+
expect((spanList[3] as TextSpan).style!.fontWeight, equals(FontWeight.bold));
167+
168+
// Expect that the fifth span is the widget rendered using the placeholder.
169+
expect(spanList[4], isA<WidgetSpan>());
105170
});
106171
});
107172
}
108173

174+
List<InlineSpan> _flattenInlineSpan(InlineSpan inlineSpan) {
175+
final flatList = <InlineSpan>[];
176+
177+
inlineSpan.visitChildren((child) {
178+
flatList.add(child);
179+
return true;
180+
});
181+
182+
return flatList;
183+
}
184+
109185
class _ExamplePlaceholder {
110186
const _ExamplePlaceholder();
111187
}

0 commit comments

Comments
 (0)