Fix: PreserveInlineStylesRule (#1980)

pull/1983/head v9.5.7
AtlasAutocode 9 months ago committed by GitHub
parent 4d56bf0cd6
commit 4a86f3042f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      lib/src/models/documents/attribute.dart
  2. 41
      lib/src/models/rules/insert.dart
  3. 1
      lib/src/widgets/raw_editor/raw_editor_actions.dart
  4. 47
      test/utils/attributes_test.dart

@ -116,7 +116,9 @@ class Attribute<T> extends Equatable {
static const VideoAttribute video = VideoAttribute(null); static const VideoAttribute video = VideoAttribute(null);
static final Set<String> inlineKeys = { static final registeredAttributeKeys = Set.unmodifiable(_registry.keys);
static final inlineKeys = Set.unmodifiable(<String>{
Attribute.bold.key, Attribute.bold.key,
Attribute.subscript.key, Attribute.subscript.key,
Attribute.superscript.key, Attribute.superscript.key,
@ -128,7 +130,17 @@ class Attribute<T> extends Equatable {
Attribute.color.key, Attribute.color.key,
Attribute.background.key, Attribute.background.key,
Attribute.placeholder.key, Attribute.placeholder.key,
}; Attribute.font.key,
Attribute.size.key,
Attribute.inlineCode.key,
});
static final ignoreKeys = Set.unmodifiable(<String>{
Attribute.width.key,
Attribute.height.key,
Attribute.style.key,
Attribute.token.key,
});
static final Set<String> blockKeys = LinkedHashSet.of({ static final Set<String> blockKeys = LinkedHashSet.of({
Attribute.header.key, Attribute.header.key,

@ -556,26 +556,47 @@ class PreserveInlineStylesRule extends InsertRule {
return null; return null;
} }
final itr = DeltaIterator(document.toDelta()); final documentDelta = document.toDelta();
final itr = DeltaIterator(documentDelta);
var prev = itr.skip(len == 0 ? index : index + 1); var prev = itr.skip(len == 0 ? index : index + 1);
if (prev == null || prev.data is! String) return null; if (prev == null || prev.data is! String) return null;
if ((prev.data as String).endsWith('\n')) { /// Trap for simple insertions at start of line
if (prev.attributes != null) { if (len == 0) {
for (final key in prev.attributes!.keys) { final prevData = prev.data as String;
if (!Attribute.inlineKeys.contains(key)) { if (prevData.endsWith('\n')) {
return null; /// If current line is empty get attributes from a prior line
final currLine = itr.next();
final currData = currLine.data as String?;
if (currData != null && (currData.isEmpty || currData[0] == '\n')) {
if (prevData.trimRight().isEmpty) {
final back =
DeltaIterator(documentDelta).skip(index - prevData.length);
if (back != null && back.data is String) {
prev = back;
}
} }
} else {
prev = currLine;
} }
} }
prev = itr
.next(); // at the start of a line, apply the style for the current line and not the style for the preceding line
} }
final attributes = prev.attributes; final attributes = <String, dynamic>{};
if (prev.attributes != null) {
for (final entry in prev.attributes!.entries) {
if (Attribute.inlineKeys.contains(entry.key)) {
attributes[entry.key] = entry.value;
}
}
}
if (attributes.isEmpty) {
return null;
}
final text = data; final text = data;
if (attributes == null || !attributes.containsKey(Attribute.link.key)) { if (attributes.isEmpty || !attributes.containsKey(Attribute.link.key)) {
return Delta() return Delta()
..retain(index + (len ?? 0)) ..retain(index + (len ?? 0))
..insert(text, attributes); ..insert(text, attributes);

@ -460,6 +460,7 @@ class QuillEditorOpenSearchAction extends ContextAction<OpenSearchIntent> {
); );
} }
await showDialog<String>( await showDialog<String>(
barrierColor: Colors.transparent,
context: context, context: context,
builder: (_) => FlutterQuillLocalizationsWidget( builder: (_) => FlutterQuillLocalizationsWidget(
child: QuillToolbarSearchDialog( child: QuillToolbarSearchDialog(

@ -0,0 +1,47 @@
import 'package:flutter_quill/flutter_quill.dart';
import 'package:test/test.dart';
void main() {
/// Attributes are assigned an AttributeScope to define how they are used.
/// Collections of Attribute keys are used to allow quick iteration by type of scope.
group('collections of keys', () {
test('unmodifiable inlineKeys', () {
expect(() => Attribute.inlineKeys.add('value'),
throwsA(const TypeMatcher<UnsupportedError>()));
});
/// All registered attributes should be listed in collections of keys.
test('collections of keys', () {
final all = <String>{}..addAll(Attribute.registeredAttributeKeys);
for (final key in Attribute.inlineKeys) {
expect(all.remove(key), true);
}
for (final key in Attribute.blockKeys) {
expect(all.remove(key), true);
}
for (final key in Attribute.embedKeys) {
expect(all.remove(key), true);
}
for (final key in Attribute.ignoreKeys) {
expect(all.remove(key), true);
}
expect(all, <String>{});
});
/// verify collections contain the correct AttributeScope.
test('collections of scope', () {
for (final key in Attribute.inlineKeys) {
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.inline);
}
for (final key in Attribute.blockKeys) {
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.block);
}
for (final key in Attribute.embedKeys) {
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.embeds);
}
for (final key in Attribute.ignoreKeys) {
expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.ignore);
}
});
});
}
Loading…
Cancel
Save