dvHighlight function
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();
}