Nesting/overlapping ranges and RegExp capturing groups
The markRanges() method with wrapAllRanges option, can highlight nesting/overlapping ranges.
With this option, all ranges that have indexes within 0 and context length be wrapped.
The markRegExp() method with RegExp having the d flag, with separateGroups and wrapAllRanges options can highlight the following:
- capturing groups regardless of nested level. You need to filter out unwanted groups
Without thewrapAllRangesoption - if a group has been wrapped, all nested groups are ignored. - capturing groups inside positive lookaround assertions
It practically removes all restrictions.
See Playground examples that demonstrate cases of using wrapAllRanges option:
- Playground - Examples -> Overlapped groups
- Playground - Examples -> Overlapped matches
- Playground - Examples -> Random groups
Note: the
wrapAllRanges option can cause performance degradation when highlighting a very large number of overlapping matches.
This is because with each wrapping, two more objects are inserted into the array, which require a lot of copying, memory allocation ...The 1MB file containing 20800 text nodes:
Warning: this performance tests were run on slow processor (important is ratio than actual time)
| option | marked groups 2500 | marked groups 29000 |
|---|---|---|
| wrapAllRanges: true | 120 ms | 710 ms |
| wrapAllRanges: false | 70 ms | 310 ms |
To mark nesting/overlapping ranges.
const ranges = [{ start: 0, length: 50 }, { start: 10, length: 20, nested: true }, ..];
instance.markRanges(ranges, {
'wrapAllRanges': true,
'each': (markElement, range) => {
// to distinguish ranges you can add some property to it
if (range.nested) {
markElement.className = 'nested';
}
}
});
To mark nesting groups with the acrossElements option and the d flag.
instance.markRegExp(/\w+\s((nested group)\s+\w+)/dg, {
'acrossElements': true,
'separateGroups': true,
'wrapAllRanges': true,
'each': (markElement, info) => {
if (info.groupIndex === 2) {
markElement.className = 'nested';
}
}
});
To mark nesting groups with acrossElements option and RegExp without d flag
It treats the whole match as a group 0, and all child groups, in this case 'group1, group2', as nested ones.
It's an only way to wrap nested groups without the `d` flag:
let regex = /\w+\s(group1).+?(group2).*/gi;
instance.markRegExp(regex, {
'acrossElements': true,
'separateGroups': true,
'wrapAllRanges': true,
'each': (markElement, info) => {
if (info.groupIndex === 0) {
markElement.className = 'main-group';
}
if (info.groupIndex > 0) {
markElement.className = 'nested-group';
}
}
});
Simple example with next/previous buttons and wrapAllRanges: true
It uses numbers as unique match identifiers in continuous ascending order. The code example with next/previous buttons which uses 'start elements' doesn't work correctly with nesting/overlapping matches.
let currentIndex = 0,
matchCount,
marks,
// highlight 3 words in sentences in any order, e.g. 'word word2 word word3 word word1.'
regex = /(?=[^.]*?(word1))(?=[^.]*?(word2))(?=[^.]*?(word3))/dgi;
instance.markRegExp(regex, {
'acrossElements': true,
'separateGroups': true,
'wrapAllRanges': true,
'each': (markElement, info) => {
// info.count as a match identifier
markElement.setAttribute('data-markjs', info.count);
},
'done': (totalMarks, totalMatches) => {
marks = $('mark');
matchCount = totalMatches;
}
});
prevButton.click(function() {
if (--currentIndex <= 0) currentIndex = 0;
highlightMatchGroups();
});
nextButton.click(function() {
if (++currentIndex > matchCount) currentIndex = matchCount;
highlightMatchGroups();
});
function highlightMatchGroups() {
marks.removeClass('current');
const elems = marks.filter((i, elem) => $(elem).data('markjs') === currentIndex).addClass('current');
elems.find('*[data-markjs]').addClass('current'); // add class to all descendant too
}