dvHighlight function

String dvHighlight(
  1. String text,
  2. Iterable<String> terms, {
  3. String pre = '<mark>',
  4. String post = '</mark>',
})

Wraps each occurrence of any of terms in text with pre and post.

Matching is case-insensitive and the original casing is preserved, so a highlighted result still reads as what was stored.

Spans are collected and merged before anything is inserted. Replacing terms one after another would wrap the markers of an earlier pass -- 'love' and 'lovelace' both match "Lovelace" -- and the corrupted output still looks like text.

Implementation

String dvHighlight(
  String text,
  Iterable<String> terms, {
  String pre = '<mark>',
  String post = '</mark>',
}) {
  final String haystack = text.toLowerCase();
  final List<List<int>> spans = <List<int>>[];

  for (final String term in terms) {
    if (term.isEmpty) continue;
    final String needle = term.toLowerCase();
    int at = haystack.indexOf(needle);
    while (at != -1) {
      spans.add(<int>[at, at + needle.length]);
      at = haystack.indexOf(needle, at + 1);
    }
  }
  if (spans.isEmpty) return text;

  spans.sort((List<int> x, List<int> y) => x[0].compareTo(y[0]));
  final List<List<int>> merged = <List<int>>[spans.first];
  for (final List<int> span in spans.skip(1)) {
    final List<int> last = merged.last;
    if (span[0] <= last[1]) {
      if (span[1] > last[1]) last[1] = span[1];
    } else {
      merged.add(span);
    }
  }

  final StringBuffer out = StringBuffer();
  int cursor = 0;
  for (final List<int> span in merged) {
    out
      ..write(text.substring(cursor, span[0]))
      ..write(pre)
      ..write(text.substring(span[0], span[1]))
      ..write(post);
    cursor = span[1];
  }
  out.write(text.substring(cursor));
  return out.toString();
}