Search code examples
javascriptjqueryrangeselectionrangy

Convert tokens into selection ranges


How would I convert a set of token ranges inside a jQuery selection, to a set of rangy ranges?

For example I have this:

<div class="test-input">
    <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas
        convallis dui id erat pellentesque et rhoncus nunc semper. Suspendisse
        malesuada {hendrerit velit nec }tristique. Aliq{uam gravida mauris at
        ligula venenatis rhoncus. Suspendisse inter}dum, nisi nec consectetur
        pulvinar, lorem augue ornare felis, vel lacinia erat nibh in ve{lit.
    </p>
    <p>
        Hendr}erit, felis ac fringilla lobortis, massa ligula aliquet justo, sit
        amet tincidunt enim quam {sollicitudin} nisi. Maecenas ipsum augue,
        commodo sit amet aliquet ut, laoreet ut nunc. Vestibulum ante ipsum
        primis in {fauc}ibus orci luctus et ultrices posuere cubilia Curae;
        Pellentesque tincidunt eros quis tellus laoreet ac dignissim turpis
        luctus. Integer nunc est, {pulvinar ac tempor ac, pretium ut odio.
    </p>
    <p>
        Pellentesque in arcu sit amet} odio scelerisque tincidunt. Lorem ipsum
        dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi
        tristique senectus et netus et malesuada fames ac turpis egestas.
    </p>
</div>

And I want to convert the text between { and } into ranges (and remove the tokens).

I tried using this:

function tokensToRanges(element) {
    element = $(element);
    var node = element.get(0);
    var ranges = [];
    do {
        var text = $(node).text(),
            start = text.indexOf('{'),
            end = text.indexOf('}') - 1,
            input = null;
        input = node.innerHTML.replace('{', '').replace('}', '');
        element.html(input);
        var range = rangy.createRange();
        range.selectCharacters(node, start, end);
        ranges.push(range);
    } while ($(node).text().indexOf('{') != -1);
    return ranges;
}

But it the ranges are not correct. I think the selectCharacters method ignores whitespace.

Also I would prefer not to use the TextRangeModule if possible.


Solution

  • selectCharacters() does not ignore all white space but it does ignore collapsed white space. For example, if a text node contains three consecutive space characters, only the first contributes to the character count. I may add an option to that method to switch that behaviour off.

    In answer to your question, Rangy's test suite has a function that does something a bit like what you want, so I've adapted it below. Start and end range markers may appear in different nodes.

    Demo: http://jsfiddle.net/timdown/DdeFr/

    Code:

    function RangeInfo() {}
    
    RangeInfo.prototype = {
        setStart: function(node, offset) {
            this.sc = node;
            this.so = offset;
        },
        setEnd: function(node, offset) {
            this.ec = node;
            this.eo = offset;
        },
        toRange: function() {
            var range = rangy.createRange();
            range.setStart(this.sc, this.so);
            range.setEnd(this.ec, this.eo);
            return range;
        }
    };
    
    function getTextNodesIn(node) {
        var textNodes = [];
        function getTextNodes(node) {
            if (node.nodeType === 3) {
                textNodes.push(node);
            } else {
                for (var i = 0, l = node.childNodes.length; i < l; i++) {
                    getTextNodes(node.childNodes[i]);
                }
            }
        }
    
        getTextNodes(node);
        return textNodes;
    }
    
    function tokensToRanges(el) {
        var rangeInfos = [];
        var currentRangeInfo;
        var textNodes = getTextNodesIn(el);
    
        $.each(textNodes, function() {
            var searchStartIndex = 0;
            var searchIndex;
            while ( (searchIndex = this.data.indexOf(currentRangeInfo ? "}" : "{", searchStartIndex)) != -1 ) {
                // Remove the marker. Doing this breaks existing ranges
                // in this node, which is why we use RangeInfo objects
                // instead of ranges
                this.data = this.data.slice(0, searchIndex) + this.data.slice(searchIndex + 1);
                if (currentRangeInfo) {
                    currentRangeInfo.setEnd(this, searchIndex);
                    rangeInfos.push(currentRangeInfo);
                    currentRangeInfo = null;
                } else {
                    currentRangeInfo = new RangeInfo();
                    currentRangeInfo.setStart(this, searchIndex);
                }
                searchStartIndex = searchIndex;
            }
        });
    
        // Convert RangeInfos into ranges
        var ranges = [];
        $.each(rangeInfos, function() {
            ranges.push(this.toRange());
        });
    
        return ranges;
    }
    
    var ranges = tokensToRanges(document.body);
    var applier = rangy.createCssClassApplier("highlight");
    applier.applyToRanges(ranges);