Skip to content

Support lookbehind/lookahead in annotation patterns when using multiple annotations #81

@officialwhatsevr

Description

@officialwhatsevr

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*.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions