import 'package:meta/meta.dart' show immutable; import '../../../quill_delta.dart'; import '../documents/attribute.dart'; import 'rule.dart'; /// A heuristic rule for format (retain) operations. @immutable abstract class FormatRule extends Rule { const FormatRule(); @override RuleType get type => RuleType.format; @override void validateArgs(int? len, Object? data, Attribute? attribute) { assert(len != null); assert(data == null); assert(attribute != null); } } /// Produces Delta with line-level attributes applied strictly to /// newline characters. @immutable class ResolveLineFormatRule extends FormatRule { const ResolveLineFormatRule(); @override Delta? applyRule( Delta document, int index, { int? len, Object? data, Attribute? attribute, }) { if (attribute!.scope != AttributeScope.block) { return null; } // Apply line styles to all newline characters within range of this // retain operation. var result = Delta()..retain(index); final itr = DeltaIterator(document)..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); final opText = op.data is String ? op.data as String : ''; if (!opText.contains('\n')) { result.retain(op.length!); continue; } final delta = _applyAttribute(opText, op, attribute); result = result.concat(delta); } // And include extra newline after retain while (itr.hasNext) { op = itr.next(); final opText = op.data is String ? op.data as String : ''; final lf = opText.indexOf('\n'); if (lf < 0) { result.retain(op.length!); continue; } final delta = _applyAttribute(opText, op, attribute, firstOnly: true); result = result.concat(delta); break; } return result; } Delta _applyAttribute(String text, Operation op, Attribute attribute, {bool firstOnly = false}) { final result = Delta(); var offset = 0; var lf = text.indexOf('\n'); final removedBlocks = _getRemovedBlocks(attribute, op); while (lf >= 0) { final actualStyle = attribute.toJson()..addEntries(removedBlocks); result ..retain(lf - offset) ..retain(1, actualStyle); if (firstOnly) { return result; } offset = lf + 1; lf = text.indexOf('\n', offset); } // Retain any remaining characters in text result.retain(text.length - offset); return result; } Iterable> _getRemovedBlocks( Attribute attribute, Operation op) { // Enforce Block Format exclusivity by rule if (!Attribute.exclusiveBlockKeys.contains(attribute.key)) { return >[]; } return op.attributes?.keys .where((key) => Attribute.exclusiveBlockKeys.contains(key) && attribute.key != key && attribute.value != null) .map((key) => MapEntry(key, null)) ?? []; } } /// Allows updating link format with collapsed selection. @immutable class FormatLinkAtCaretPositionRule extends FormatRule { const FormatLinkAtCaretPositionRule(); @override Delta? applyRule( Delta document, int index, { int? len, Object? data, Attribute? attribute, }) { if (attribute!.key != Attribute.link.key || len! > 0) { return null; } final delta = Delta(); final itr = DeltaIterator(document); final before = itr.skip(index), after = itr.next(); int? beg = index, retain = 0; if (before != null && before.hasAttribute(attribute.key)) { beg -= before.length!; retain = before.length; } if (after.hasAttribute(attribute.key)) { if (retain != null) retain += after.length!; } if (retain == 0) { return null; } delta ..retain(beg) ..retain(retain!, attribute.toJson()); return delta; } } /// Produces Delta with inline-level attributes applied to all characters /// except newlines. @immutable class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); @override Delta? applyRule( Delta document, int index, { int? len, Object? data, Attribute? attribute, }) { if (attribute!.scope != AttributeScope.inline) { return null; } final delta = Delta()..retain(index); final itr = DeltaIterator(document)..skip(index); Operation op; for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) { op = itr.next(len - cur); final text = op.data is String ? (op.data as String?)! : ''; var lineBreak = text.indexOf('\n'); if (lineBreak < 0) { delta.retain(op.length!, attribute.toJson()); continue; } var pos = 0; while (lineBreak >= 0) { delta ..retain(lineBreak - pos, attribute.toJson()) ..retain(1); pos = lineBreak + 1; lineBreak = text.indexOf('\n', pos); } if (pos < op.length!) { delta.retain(op.length! - pos, attribute.toJson()); } } return delta; } } /// Produces Delta with attributes applied to image leaf node @immutable class ResolveImageFormatRule extends FormatRule { const ResolveImageFormatRule(); @override Delta? applyRule( Delta document, int index, { int? len, Object? data, Attribute? attribute, }) { if (attribute == null || attribute.key != Attribute.style.key) { return null; } assert(len == 1 && data == null); final delta = Delta() ..retain(index) ..retain(1, attribute.toJson()); return delta; } }