diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 57b7117f..129a7da8 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -1,3 +1,4 @@ +import 'package:flutter_quill/flutter_quill.dart'; import 'package:tuple/tuple.dart'; import '../documents/attribute.dart'; @@ -286,44 +287,107 @@ class InsertEmbedsRule extends InsertRule { } } -/// Applies link format to text segment (which looks like a link) when user -/// inserts space character after it. +/// Applies link format to text segments within the inserted text that matches +/// the URL pattern. +/// +/// The link attribute is applied as the user types. class AutoFormatLinksRule extends InsertRule { const AutoFormatLinksRule(); + /// Link pattern. + /// + /// This pattern is used to match a links within a text segment. + static const _linkPattern = + r'(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?'; + static final _linkRegExp = RegExp(_linkPattern); + @override - Delta? applyRule(Delta document, int index, - {int? len, Object? data, Attribute? attribute}) { - if (data is! String || data != ' ') { - return null; - } + Delta? applyRule( + Delta document, + int index, { + int? len, + Object? data, + Attribute? attribute, + }) { + // Only format when inserting text. + if (data is! String) return null; + + // Get current text. + final entireText = Document.fromDelta(document).toPlainText(); + + // Get word before insertion. + final leftWordPart = entireText + // Keep all text before insertion. + .substring(0, index) + // Keep last paragraph. + .split('\n') + .last + // Keep last word. + .split(' ') + .last + .trimLeft(); + + // Get word after insertion. + final rightWordPart = entireText + // Keep all text after insertion. + .substring(index) + // Keep first paragraph. + .split('\n') + .first + // Keep first word. + .split(' ') + .first + .trimRight(); + + // Build the segment of affected words. + final affectedWords = '$leftWordPart$data$rightWordPart'; + + // Check for URL pattern. + final matches = _linkRegExp.allMatches(affectedWords); + + // If there are no matches, do not apply any format. + if (matches.isEmpty) return null; + + // Build base delta. + // The base delta is a simple insertion delta. + final baseDelta = Delta() + ..retain(index) + ..insert(data); - final itr = DeltaIterator(document); - final prev = itr.skip(index); - if (prev == null || prev.data is! String) { - return null; - } + // Get unchanged text length. + final unmodifiedLength = index - leftWordPart.length; - try { - final cand = (prev.data as String).split('\n').last.split(' ').last; - final link = Uri.parse(cand); - if (!['https', 'http'].contains(link.scheme)) { - return null; - } - final attributes = prev.attributes ?? {}; + // Create formatter delta. + // The formatter delta will only include links formatting when needed. + final formatterDelta = Delta()..retain(unmodifiedLength); - if (attributes.containsKey(Attribute.link.key)) { - return null; - } + var previousLinkEndRelativeIndex = 0; + for (final match in matches) { + // Get the size of the leading segment of text that is not part of the + // link. + final separationLength = match.start - previousLinkEndRelativeIndex; - attributes.addAll(LinkAttribute(link.toString()).toJson()); - return Delta() - ..retain(index + (len ?? 0) - cand.length) - ..retain(cand.length, attributes) - ..insert(data, prev.attributes); - } on FormatException { - return null; + // Get the identified link. + final link = affectedWords.substring(match.start, match.end); + + // Keep the leading segment of text and add link with its proper + // attribute. + formatterDelta + ..retain(separationLength, LinkAttribute(null).toJson()) + ..retain(link.length, LinkAttribute(link).toJson()); + + // Update reference index. + previousLinkEndRelativeIndex = match.end; } + + // Get remaining text length. + final remainingLength = affectedWords.length - previousLinkEndRelativeIndex; + + // Remove links from remaining non-link text. + formatterDelta.retain(remainingLength, LinkAttribute(null).toJson()); + + // Build and return resulting change delta. + return baseDelta.compose(formatterDelta); } }