dvFindMatch function
Which of the page's rendered paragraphs section mirrors.
beforematch names the element the browser matched in, not the words or
the offset, so the paragraph is the finest this can be. candidates are
the page's paragraphs as they are rendered now, in order; hint is the
index the mirror was written with, which is exact while the page has not
changed and breaks ties when it has.
In order of confidence: the same text; a paragraph the section is part of (the build wrote one sentence of a longer rendered paragraph); a run of paragraphs that makes up most of the section (the build wrote a whole list, the page drew an item at a time); a long paragraph inside the section; and, last, the paragraph sharing most of its words, for text that changed a little since the mirror was written. Null when nothing is close, because scrolling somewhere wrong is worse than not scrolling.
Implementation
int? dvFindMatch(String section, List<String> candidates, {int? hint}) {
final String target = dvFindNormalize(section);
if (target.isEmpty || candidates.isEmpty) return null;
final List<String> normalized = candidates.map(dvFindNormalize).toList();
int? nearest(Iterable<int> indexes) {
int? best;
for (final int i in indexes) {
if (best == null) {
best = i;
continue;
}
if (hint == null) continue;
if ((i - hint).abs() < (best - hint).abs()) best = i;
}
return best;
}
final Iterable<int> all = Iterable<int>.generate(normalized.length);
final int? exact = nearest(all.where((int i) => normalized[i] == target));
if (exact != null) return exact;
final int? within = nearest(all.where(
(int i) => normalized[i].isNotEmpty && normalized[i].contains(target)));
if (within != null) return within;
// A run of paragraphs that is most of the section: a list the build wrote
// whole, drawn one short item at a time. Most of it, because a one-word
// label at the start of a long section is not what the section says.
bool opensRun(int i) {
if (normalized[i].isEmpty || !target.startsWith(normalized[i])) {
return false;
}
String joined = normalized[i];
for (int j = i + 1; j < normalized.length; j++) {
final String next = '$joined ${normalized[j]}';
if (!target.startsWith(next)) break;
joined = next;
}
return joined.length >= target.length * 0.6;
}
final int? run = nearest(all.where(opensRun));
if (run != null) return run;
// A paragraph inside the section. Short ones are left out: a one-word
// label is inside every section that mentions it.
final int? inside = nearest(all.where((int i) =>
normalized[i].length >= 12 && target.contains(normalized[i])));
if (inside != null) return inside;
final Set<String> words = target.split(' ').toSet();
double bestScore = 0;
int? best;
for (final int i in all) {
final Set<String> theirs = normalized[i].split(' ').toSet();
if (theirs.isEmpty) continue;
final int shared = words.intersection(theirs).length;
final double score = shared / words.union(theirs).length;
final bool closer = best != null &&
hint != null &&
score == bestScore &&
(i - hint).abs() < (best - hint).abs();
if (score > bestScore || closer) {
bestScore = score;
best = i;
}
}
return bestScore >= 0.5 ? best : null;
}