-
Notifications
You must be signed in to change notification settings - Fork 92
Description
Problem
When using multiple annotations with patterns that contain lookbehind ((?<!...), (?<=...)) or lookahead ((?!...), (?=...)), the package throws a FormatException.
Example:
ReadMoreText(
'Check #hashtag and https://example.com/#section',
annotations: [
Annotation(
regExp: RegExp(r'(?<![/\w])#[a-zA-Z]\w*'), // Hashtag not part of URL
spanBuilder: ({required text, required textStyle}) => TextSpan(...),
),
Annotation(
regExp: RegExp(r'https?://[^\s]+'),
spanBuilder: ({required text, required textStyle}) => TextSpan(...),
),
],
)
Error:
FormatException: Lookbehind not valid in pattern at position X
Root Cause
In _mergeRegexPatterns(), all annotation patterns are joined with | into a single RegExp:
return RegExp(
annotations.map((a) => '(${a.regExp.pattern...})').join('|'),
);
This breaks when any pattern contains lookbehind/lookahead because Dart's RegExp engine cannot parse the combined pattern correctly.
Proposed Solution
Instead of merging patterns into one RegExp, process each annotation's RegExp independently and merge the resulting matches by position:
Run each annotation's RegExp separately against the input text
Collect all matches with their start/end positions and source annotation
Sort matches by position and resolve overlaps (e.g., longer match wins)
Build TextSpan children from the sorted, non-overlapping matches
This approach:
✅ Supports all valid RegExp features including lookbehind/lookahead
✅ Maintains backward compatibility (existing patterns work unchanged)
✅ Gives predictable overlap resolution
Use Case
I need to detect hashtags (#topic) but NOT match # that appears in URL fragments like https://example.com/#section. The only way to do this reliably is with negative lookbehind: (?<![/\w])#[a-zA-Z]\w*.