Skip to content

Commit 4a86f30

Browse files
Fix: PreserveInlineStylesRule (singerdmx#1980)
1 parent 4d56bf0 commit 4a86f30

File tree

4 files changed

+93
-12
lines changed

4 files changed

+93
-12
lines changed

lib/src/models/documents/attribute.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ class Attribute<T> extends Equatable {
116116

117117
static const VideoAttribute video = VideoAttribute(null);
118118

119-
static final Set<String> inlineKeys = {
119+
static final registeredAttributeKeys = Set.unmodifiable(_registry.keys);
120+
121+
static final inlineKeys = Set.unmodifiable(<String>{
120122
Attribute.bold.key,
121123
Attribute.subscript.key,
122124
Attribute.superscript.key,
@@ -128,7 +130,17 @@ class Attribute<T> extends Equatable {
128130
Attribute.color.key,
129131
Attribute.background.key,
130132
Attribute.placeholder.key,
131-
};
133+
Attribute.font.key,
134+
Attribute.size.key,
135+
Attribute.inlineCode.key,
136+
});
137+
138+
static final ignoreKeys = Set.unmodifiable(<String>{
139+
Attribute.width.key,
140+
Attribute.height.key,
141+
Attribute.style.key,
142+
Attribute.token.key,
143+
});
132144

133145
static final Set<String> blockKeys = LinkedHashSet.of({
134146
Attribute.header.key,

lib/src/models/rules/insert.dart

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -556,26 +556,47 @@ class PreserveInlineStylesRule extends InsertRule {
556556
return null;
557557
}
558558

559-
final itr = DeltaIterator(document.toDelta());
559+
final documentDelta = document.toDelta();
560+
final itr = DeltaIterator(documentDelta);
560561
var prev = itr.skip(len == 0 ? index : index + 1);
561562

562563
if (prev == null || prev.data is! String) return null;
563564

564-
if ((prev.data as String).endsWith('\n')) {
565-
if (prev.attributes != null) {
566-
for (final key in prev.attributes!.keys) {
567-
if (!Attribute.inlineKeys.contains(key)) {
568-
return null;
565+
/// Trap for simple insertions at start of line
566+
if (len == 0) {
567+
final prevData = prev.data as String;
568+
if (prevData.endsWith('\n')) {
569+
/// If current line is empty get attributes from a prior line
570+
final currLine = itr.next();
571+
final currData = currLine.data as String?;
572+
if (currData != null && (currData.isEmpty || currData[0] == '\n')) {
573+
if (prevData.trimRight().isEmpty) {
574+
final back =
575+
DeltaIterator(documentDelta).skip(index - prevData.length);
576+
if (back != null && back.data is String) {
577+
prev = back;
578+
}
569579
}
580+
} else {
581+
prev = currLine;
570582
}
571583
}
572-
prev = itr
573-
.next(); // at the start of a line, apply the style for the current line and not the style for the preceding line
574584
}
575585

576-
final attributes = prev.attributes;
586+
final attributes = <String, dynamic>{};
587+
if (prev.attributes != null) {
588+
for (final entry in prev.attributes!.entries) {
589+
if (Attribute.inlineKeys.contains(entry.key)) {
590+
attributes[entry.key] = entry.value;
591+
}
592+
}
593+
}
594+
if (attributes.isEmpty) {
595+
return null;
596+
}
597+
577598
final text = data;
578-
if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
599+
if (attributes.isEmpty || !attributes.containsKey(Attribute.link.key)) {
579600
return Delta()
580601
..retain(index + (len ?? 0))
581602
..insert(text, attributes);

lib/src/widgets/raw_editor/raw_editor_actions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ class QuillEditorOpenSearchAction extends ContextAction<OpenSearchIntent> {
460460
);
461461
}
462462
await showDialog<String>(
463+
barrierColor: Colors.transparent,
463464
context: context,
464465
builder: (_) => FlutterQuillLocalizationsWidget(
465466
child: QuillToolbarSearchDialog(

test/utils/attributes_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:flutter_quill/flutter_quill.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
/// Attributes are assigned an AttributeScope to define how they are used.
6+
/// Collections of Attribute keys are used to allow quick iteration by type of scope.
7+
group('collections of keys', () {
8+
test('unmodifiable inlineKeys', () {
9+
expect(() => Attribute.inlineKeys.add('value'),
10+
throwsA(const TypeMatcher<UnsupportedError>()));
11+
});
12+
13+
/// All registered attributes should be listed in collections of keys.
14+
test('collections of keys', () {
15+
final all = <String>{}..addAll(Attribute.registeredAttributeKeys);
16+
for (final key in Attribute.inlineKeys) {
17+
expect(all.remove(key), true);
18+
}
19+
for (final key in Attribute.blockKeys) {
20+
expect(all.remove(key), true);
21+
}
22+
for (final key in Attribute.embedKeys) {
23+
expect(all.remove(key), true);
24+
}
25+
for (final key in Attribute.ignoreKeys) {
26+
expect(all.remove(key), true);
27+
}
28+
expect(all, <String>{});
29+
});
30+
31+
/// verify collections contain the correct AttributeScope.
32+
test('collections of scope', () {
33+
for (final key in Attribute.inlineKeys) {
34+
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.inline);
35+
}
36+
for (final key in Attribute.blockKeys) {
37+
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.block);
38+
}
39+
for (final key in Attribute.embedKeys) {
40+
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.embeds);
41+
}
42+
for (final key in Attribute.ignoreKeys) {
43+
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.ignore);
44+
}
45+
});
46+
});
47+
}

0 commit comments

Comments
 (0)