Vlight & Vejis 开发笔记 2010.8.18

Github https://github.com/vilic/vlight

同志们可能已经发现了, 现在有代码高亮了.

这个属于心血来潮, 突然想到写一个, 然后就写了一个, 不过后来兼容IE6还是费了点事. 最后在样式上也动用了JS. 不过顺道, 也进一步完善了Vejis (Vlight需要Vejis). 代码不多, 去掉注释的话只有80+行. 所以直接贴出来, 也顺道测试下.

/*
Vlight JS Code Highlight (Style from Visual Studio 2010)

Version 0.3
Vejis JavaScript Library 0.0.0.5 is needed.

By Vilic Vane
http://www.vilic.info/

©2010 Groinup Studio
All rights reserved.

Redistribution and use in source and binary forms,
with or without modification, are permitted provided
that the following conditions are met:
Redistributions of source code must retain the above
copyright notice, this list of conditions and the
following disclaimer.
Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and
the following disclaimer in the documentation and/or
other materials provided with the distribution.
Neither the name of the Groinup Studio nor the names
of its contributors may be used to endorse or promote
products derived from this software without specific
prior written permission.
*/

vejis.ready(function () {
    var className = 'vlight', //自定义类名
        maxLineCount = 20, //最多同时显示的行数
        lineHeight = 16, //行高
        scrollBarWidth = 24, //预计滚动条宽度
        cssText = //CSS内容
        'div.vlight { position: relative; margin: 5px 0px; border: solid 1px #FFF3D0; line-height: ' + lineHeight + 'px; color: #000000; font-size: 12px; font-family: Courier New, monospace; white-space: nowrap; overflow: hidden; }' +
        'div.vlight div.vlight_top { height: 5px; background-color: #FFE8A6; font-size: 0px; }' +
        'div.vlight div.vlight_left { position: absolute; width: 65px; left: 0px; text-align: right; color: #2B91AF; overflow: hidden; }' +
        'div.vlight div.vlight_left div { position: relative; width: 40px; left: 0px; padding-right: 5px; border-left: solid 16px #F0F0F0; border-right: solid 4px #6CE26C; }' +
        'div.vlight div.vlight_main { position: relative; margin-left: 65px; padding-left: 5px; overflow-x: scroll; overflow-y: auto; }' +
        'div.vlight span.vlight_comm { color: #008000; }' +
        'div.vlight span.vlight_re { color: #000000; }' +
        'div.vlight span.vlight_str { color: #800000; }' +
        'div.vlight span.vlight_key { color: #0000FF; }';

    cssText = cssText.replace(/vlight/g, className); //替换类名
    vejis.html.createStyleSheet(cssText); //创建样式

    var eles = vejis.html.getElementsByClassName(className); //获取元素
    var spanl = '<span>',
        spanr = '</span>';

    //处理每一个类名符合的元素
    vejis.foreach(eles, function (ele) {
        var div = document.createElement('div');
        div.className = className;
        div.innerHTML =
        '<div></div>' +
        '<div></div>';

        var top = div.childNodes[0],
            left = div.childNodes[1];

        var main = document.createElement('div');
        main.className = className + '_main';

        var oText;
        if (ele.tagName == 'TEXTAREA') oText = ele.value; //如果是textarea则直接取value
        else if (ele.tagName == 'PRE') oText = ele.innerText || ele.innerHTML;
        else oText = ele.innerHTML.replace(/\r?\n/g, '').replace(/<p( [^>]*)?>([\s\S]*?)<\/p>/gi, '$2\r\n\r\n').replace(/<div( [^>]*)?>([\s\S]*?)<\/div>/gi, '$2\r\n').replace(/<([a-z]+)( [^>]*)?>([\s\S]*?)<\/\1>/gi, '$3').replace(/<br[^>]*>/gi, '\r\n').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');

        var result = convert(oText); //转换文本到高亮的HTML
        main.innerHTML = result.html;
        div.appendChild(main);

        //创建行号
        var lines = ''
        for (var i = 1; i <= result.count; i++)
            lines += i + '<br />';
        left.innerHTML = '<div>' + lines + '</div>';

        //将原来的元素替换成代码高亮区域
        ele.parentNode.replaceChild(div, ele);

        //设置高宽
        left.style.height = main.style.height = lineHeight * (result.count > maxLineCount ? maxLineCount : result.count) + scrollBarWidth + 'px';
        left.childNodes[0].style.height = result.count * lineHeight + scrollBarWidth + 'px';

        //绑定事件
        vejis._event.add(window, 'resize', resize);
        vejis._event.add(main, 'scroll', scroll);

        resize(); //初始化

        function resize() {
            try {
                main.style.width = top.offsetWidth - left.offsetWidth - 5 + 'px';
            } catch (e) { }
        }

        function scroll() {
            left.childNodes[0].style.marginTop = -main.scrollTop + 'px';
        }
    });

    function convert(text) {
        var names = ['comm', 're', 'str', 'key']; //类名的后缀

        //全局正则表达式
        var globalRE = /((\/\*[\s\S]*?\*\/|\/\/.*)|('('|.*?([^\\]'|\\\\'))|"("|.*?([^\\]"|\\\\")))|\/(\/|.*?([^\\]\/|\\\\\/))[gim]{0,3}|([^\w]|^)(break|delete|function|return|typeof|case|do|if|switch|var|catch|else|in|this|void|continue|false|instanceof|throw|while|debugger|finally|new|true|with|default|for|null|try)(?=[^\w]|$))/g;
       
        //拆分开的正则表达式
        var res = [
            /^(\/\*[\s\S]*?\*\/|\/\/.*)$/,
            /^\/(\/|.*?([^\\]\/|\\\\\/))[gim]{0,3}$/,
            /^'('|.*?([^\\]'|\\\\'))|"("|.*?([^\\]"|\\\\"))$/,
            /(break|delete|function|return|typeof|case|do|if|switch|var|catch|else|in|this|void|continue|false|instanceof|throw|while|debugger|finally|new|true|with|default|for|null|try)$/
        ];

        text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); //符号处理

        //匹配
        text = text.replace(globalRE, function (s) {
            for (var i = 0; i < res.length; i++) {
                if (!res[i].test(s)) continue;
                var spanl2m = spanl + names[i] + spanm;

                s = s.replace(res[i], function (s) {
                    return spanl2m + s + spanr; //加标签
                });
                return s;
            }
        });

        var count = 1; //行数

        //字符处理
        text = text.replace(/\t/g, '    ').replace(/  /g, '&nbsp; ').replace(/  /g, ' &nbsp;').replace(/(\r?\n)+$/g, '').replace(/\r?\n/g, function () { count++; return '<br />'; });

        return { html: text, count: count };
    }
});

Vejis主要改进是在事件处理上(没有完全测试过, 所以可能还有大量bug), 琐碎的就不说了, 上代码:

var _event = this._event = new function () {
    var objects = [];
    var holders = [];

    this.add = _(Object, String, Function, function (obj, name, callback) {
        name = name.toLowerCase();

        var index = array.indexOf(objects, obj),
            subHolder;

        if (index < 0) {
            index = objects.length;
            objects.push(obj);
            var holder = {};
            holders.push(holder);
            createListener(holder);
        }
        else {
            var holder = holders[index];
            subHolder = holder[name];
            if (subHolder) {
                if (array.exists(subHolder, callback)) return false;
                subHolder.push(callback);
            }
            else createListener(holder);
        }

        return true;

        function createListener(holder) {
            var subHolder = holder[name] = [callback];
            subHolder.handler = handler;
            if (obj.addEventListener) obj.addEventListener(name, handler, false);
            else if (obj.attachEvent) obj.attachEvent('on' + name, handler);
        }

        function handler(ev) {
            var e = window.event || ev;
            onEvent(e, obj, name);
        }
    });

    this.remove = _(Object, String, Function, function (obj, name, callback) {
        name = name.toLowerCase();
        var index = array.indexOf(objects, obj);
        if (index < 0) return false;
        else {
            var holder = holders[index],
                subHolder;
            if (holder && (subHolder = holder[name])) {
                var idx = array.indexOf(subHolder, callback);
                if (idx < 0) return false;
                else {
                    array.removeByIndex(subHolder, idx);
                    if (!subHolder.length) {
                        var handler = subHolder.handler;
                        if (obj.removeEventListener) obj.removeEventListener(name, handler);
                        else if (obj.detachEvent) obj.detachEvent('on' + name, handler);
                        delete holder[name];
                        if (object.isEmpty(holder)) {
                            array.remove(holders, index);
                            array.remove(objects, index);
                        }
                    }
                    return true;
                }
            }
            return false;
        }
    });

    function onEvent(e, obj, name) {
        var index = array.indexOf(objects, obj);
        var callbacks = holders[index][name];

        foreach(callbacks, function (callback) {
            callback(e);
        });
    }
} ();