Javascript文本突出显示功能

时间:2013-05-13 14:18:59

标签: javascript algorithm substring highlight

方案

我正在尝试开发一个Javascript文本高亮功能。 它在输入中接收一个要在里面搜索的文本,一个要搜索的标记数组,一个用于包装找到的匹配的类:

var fmk = fmk || {};

fmk.highlight = function (target, tokens, cls) {
    var token, re;
    if (tokens.length > 0) {
        token = tokens.pop();
        re = new RegExp(token, "gi");
        return this.highlight(
            target.replace(re, function (matched) {
                return "<span class=\"" + cls + "\">" + matched + "</span>";
            }), tokens, cls);
    }
    else { return target; }
};

它基于递归替换,在找到的匹配项周围包含<span>标记。

JsFiddle demo

问题

  1. 如果有两个令牌,后者是前一个的子串,那么只有后一个令牌会被高亮显示。在jsFiddle示例中尝试这些标记:'ab b'。

  2. 如果标记包含包装序列的子字符串(即<span class="[className]"></span>)和另一个匹配标记,则然后突出显示失败并返回脏结果。在jsFiddle示例中尝试这些标记:'red ab'。

  3. 注意在实际应用程序中允许使用单个字符标记。

    问题

    如何避免这些错误?我想出了这些方法:

    • 要预处理令牌,请删除作为其他子串的令牌。缺点:在n个令牌的情况下,它需要在预处理阶段进行O(n^2)次搜索;好的比赛被切断了。

    • 在应用包装器之前预处理匹配,以便仅切断子串匹配。缺点:再次,需要进一步的计算。无论如何,我不知道从哪里开始实现这个替换回调函数。

2 个答案:

答案 0 :(得分:2)

我认为处理这个问题的方法是循环遍历元素的所有后代,检查它是否是文本节点,并替换用span / class包装的相应内容。

var MyApp = {};

MyApp.highlighter = (function () {
    "use strict";

    var checkAndReplace, func,
        id = {
            container: "container",
            tokens: "tokens",
            all: "all",
            token: "token",
            className: "className",
            sensitiveSearch: "sensitiveSearch"
        };

    checkAndReplace = function (node, tokenArr, classNameAll, sensitiveSearchAll) {
        var nodeVal = node.nodeValue, parentNode = node.parentNode,
            i, j, curToken, myToken, myClassName, mySensitiveSearch,
            finalClassName, finalSensitiveSearch,
            foundIndex, begin, matched, end,
            textNode, span;

        for (i = 0, j = tokenArr.length; i < j; i++) {
            curToken = tokenArr[i];
            myToken = curToken[id.token];
            myClassName = curToken[id.className];
            mySensitiveSearch = curToken[id.sensitiveSearch];

            finalClassName = (classNameAll ? myClassName + " " + classNameAll : myClassName);

            finalSensitiveSearch = (typeof sensitiveSearchAll !== "undefined" ? sensitiveSearchAll : mySensitiveSearch);
            if (finalSensitiveSearch) {
                foundIndex = nodeVal.indexOf(myToken);
            } else {
                foundIndex = nodeVal.toLowerCase().indexOf(myToken.toLowerCase());
            }

            if (foundIndex > -1) {
                begin = nodeVal.substring(0, foundIndex);
                matched = nodeVal.substr(foundIndex, myToken.length);
                end = nodeVal.substring(foundIndex + myToken.length, nodeVal.length);

                if (begin) {
                    textNode = document.createTextNode(begin);
                    parentNode.insertBefore(textNode, node);
                }

                span = document.createElement("span");
                span.className += finalClassName;
                span.appendChild(document.createTextNode(matched));
                parentNode.insertBefore(span, node);

                if (end) {
                    textNode = document.createTextNode(end);
                    parentNode.insertBefore(textNode, node);
                }

                parentNode.removeChild(node);
            }
        }
    };

    func = function (options) {
        var iterator,
            tokens = options[id.tokens],
            allClassName = options[id.all][id.className],
            allSensitiveSearch = options[id.all][id.sensitiveSearch];

        iterator = function (p) {
            var children = Array.prototype.slice.call(p.childNodes),
                i, cur;

            if (children.length) {
                for (i = 0; i < children.length; i++) {
                    cur = children[i];
                    if (cur.nodeType === 3) {
                        checkAndReplace(cur, tokens, allClassName, allSensitiveSearch);
                    } else if (cur.nodeType === 1) {
                        iterator(cur);
                    }
                }
            }
        };

        iterator(options[id.container]);
    };

    return func;
})();

window.onload = function () {
    var container = document.getElementById("container");
    MyApp.highlighter({
        container: container,
        all: {
            className: "highlighter"
        },
        tokens: [{
            token: "sd",
            className: "highlight-sd",
            sensitiveSearch: false
        }, {
            token: "SA",
            className: "highlight-SA",
            sensitiveSearch: true
        }]
    });
};

DEMO: http://jsfiddle.net/UWQ6r/1/

我进行了设置,因此您可以更改id中的值,以便在传递给{}的{​​{1}}中使用不同的键。

highlighter对象中的两个设置指的是无论如何都要添加的类,以及区分大小写的搜索覆盖。对于每个令牌,您可以指定令牌,类以及匹配是否区分大小写。

参考文献:

答案 1 :(得分:1)

这似乎对我有用:

JsFiddle demo中的第17行)

问题1:var tokens = [['ab','b'].join("|")];

问题2:var tokens = ['<span'.replace(/</g,"&lt;")];

所有在一起,然后:

var tokens = [[..my tokens..].sort().join("|").replace(/</g,"&lt;")]; 

(顺便说一下,我测试了'"''"s''span'这样的令牌,但它们似乎工作得很好。另外,我不确定为什么{{1}这里很重要,但是因为我喜欢接近原始代码而离开了它。)