Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Highlight text based on index in textContent

Bounty

The bounty will go to the fastest solution, as demonstrated by jsPerf, across the latest release versions of Firefox, Chrome and Internet Explorer at time of testing or the answer most useful in creating such a solution at my discretion. Mwahahaha!

I'll be mostly satisfied with a solution that takes all of the offsets and an unprocessed <span> and adds the highlighting to that, so that parent.textContent = parent.textContent followed by running the solution on an updated list of offsets will re-highlight, but this has unfavourable time complexity so is not preferred.


Related questions

  • Overlapping of one string into another string highlighting issue
    (doesn't feature overlapping)
  • How to get the parent element in an HTML string at a given offset?
    (evolved from a variant of this question)

I have an element containing nothing but text, which I would like to highlight. I also have an array of [startline, startcol, endline, endcol] which, knowing the lengths of each line from .textContent, I can normalise to [startoffset, endoffset]. How can I highlight between each pair of offsets?

This problem is harder than it seems because:

  • the content is not guaranteed to have no repeats (so no find / replace), and
  • highlighting must ultimately be performed on already highlighted text, sometimes intersecting with text that has already been highlighted, and
  • highlighting must be performed based on the index of the parent element's .textContent property.

Definitions

  • highlight: to place a subset of the text from an element's textContent in one or more <span class="highlighted"> without changing the parent element's textContent value, such that text that is highlighted n times is within n nested <span class="highlighted"> elements.
  • offset: a non-negative integer representing the number of characters before a certain point (which is between two characters).
  • character: an instance of whatever JavaScript gives you as the value at a given index of a .textContent string (including whitespace).

MCVE

function highlight(parent, startoff, endoff) {
  // Erm...
  parent.textContent;
}

// Test cases

var starts = [
  5,  44, 0, 50, 6,  100, 99,  50, 51, 52
];
var ends = [
  20, 62, 4, 70, 10, 100, 101, 54, 53, 53
];
for (var i = 0; i < 10; i += 1) {
  highlight(document.getElementById("target"),
            starts[i], ends[i]);
}
#target {
  white-space: pre-wrap;
}
<span id="target">
'Twas brillig, and the slithy toves
  Did gyre and gimble in the wabe:
All mimsy were the borogoves,
  And the mome raths outgrabe.

"Beware the Jabberwock, my son!
  The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
  The frumious Bandersnatch!"

He took his vorpal sword in hand:
  Long time the manxome foe he sought --
So rested he by the Tumtum tree,
  And stood awhile in thought.

And, as in uffish thought he stood,
  The Jabberwock, with eyes of flame,
Came whiffling through the tulgey wood,
  And burbled as it came!

One, two! One, two! And through and through
  The vorpal blade went snicker-snack!
He left it dead, and with its head
  He went galumphing back.

"And, has thou slain the Jabberwock?
  Come to my arms, my beamish boy!
O frabjous day! Callooh! Callay!'
  He chortled in his joy.

'Twas brillig, and the slithy toves
  Did gyre and gimble in the wabe;
All mimsy were the borogoves,
  And the mome raths outgrabe.
</span>
like image 565
wizzwizz4 Avatar asked Apr 08 '18 19:04

wizzwizz4


1 Answers

Make normalization to start/end positions to avoid overlapping.

  1. Merge starting and ending positions to single list with opposite values(say, -1 and 1)
  2. Sort list by position value and then - by marker value(and based on second level sorting you can either distinguish sequential ranges or merge them)
  3. go through list of positions and add current position's value marker to current sum; once it's "0" - it means you have just found ending for some set nested/intersected sections;

This way you will get positions for highlighting without nested/overlapped ranges.

To replace text node with mix of text nodes and HTML elements(like <span>) documentFragment and .replaceChild() will help:

let starts = [
    5,  44, 0, 50, 6,  100, 99,  50, 51, 52
];
let ends = [
    20, 62, 4, 70, 10, 100, 101, 54, 53, 53
];

let positions = [];
let normalizedPositions = [];
starts.forEach(function(position) {
    positions.push({position, value: 1});
});
ends.forEach(function(position) {
    positions.push({position, value: -1});
});
positions = positions.sort(function(a, b) {
    return a.position - b.position || 
        b.value - a.value
});

var currentSection = {from: 0, counter: 0};

for(position of positions) {
    if (!currentSection.counter) {
        if (position.value === -1) {
            throw `inconsistent boundaries: closing before opening ${position.position}`;
        }
        currentSection.from = position.position;  
    }
    currentSection.counter += position.value;

    if (!currentSection.counter) { 
        normalizedPositions.push({
            from: currentSection.from, 
            to: position.position
        });
    }
}
if (currentSection.counter) {
    throw "last section has not been closed properly";   
}


let parentNode = document.querySelector('p');
let textNodeToReplace = parentNode.childNodes[0];
let sourceText = textNodeToReplace.nodeValue;

let documentFragment = document.createDocumentFragment();
let withoutHighlightingStart = 0;

normalizedPositions.forEach(function (highlightRange) {
    if (highlightRange.from> withoutHighlightingStart) {
      let notHighlighted = createTextNode(sourceText.slice(withoutHighlightingStart, highlightRange.from));
      documentFragment.appendChild(notHighlighted);
    }
    let highlighted = createHighlighted(sourceText.slice(highlightRange.from, highlightRange.to));
    documentFragment.appendChild(highlighted);
    withoutHighlightingStart = highlightRange.to;
});
let lastNotHighlighted = createTextNode(sourceText.slice(withoutHighlightingStart));
documentFragment.appendChild(lastNotHighlighted);

parentNode.replaceChild(documentFragment, textNodeToReplace);

function createTextNode(str) {
   return document.createTextNode(str);
}

function createHighlighted(str) {
   let span = document.createElement('span');
   span.classList.add('highlight');
   span.appendChild(createTextNode(str));
   return span;
}
.highlight {
    background-color: yellow;
    color: dark-blue;
}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
like image 119
skyboyer Avatar answered Oct 21 '22 09:10

skyboyer