原生JS实现代码高亮功能

分析如何实现该功能

  平时我们在使用一些代码编辑器或者Markdown时很好奇它的代码高亮是如何
实现的。其实原理也挺简单的,就是区分代码内容的不同token并加以颜色标识。
我们将以js规则为例来说明这一过程。
  在对js代码的编译处理中其实只要第一个过程词法法分析即可实现基本的代码
高亮, 这个过程被称为lexer或者tokenizer。
下面介绍一下词法语法结构。

了解词法结构

如何定义一门语言? 
  想要分析一门语言,必须要从它的结构入手提取出一般化的东西。
  比如说中文的句式(|是或者的关系):
       句子->主语 谓语 宾语
       主语->代词|名词|短语
       代词->你|我|他
   我们可以通过迭代的方式来描述该语言。计算机语言也是语言,可以用
   同样的方式来描述它,而且比自然语言更加的严格。
       计算机中定义该结构的式子就叫产生式,产生式是由终结符、非终结符、
   以及关系组成。终结符是无法往下扩展的符号,比如说代词中的“你”,“我”,
   “他”无法往下划分,非终结符由终结符、非终结符组成。
   产生式的写法:
       由巴克斯-若尔提出来的BNF(backs-Naur Form)产生式写法比较流行。
       上面的中文结构可以写成:
	        <中文>::=<句子>|<中文><句子>
		    <句子>::=<主语><谓语><宾语>|<主语><谓语>
		    <主语>::=<代词>|<名词>|<名词性短语>
		    <代词>::="你"|"我"|"他"
       还有扩展后的EBNF:
            中文::={句子}  //{}表示可以重复多次
		    句子::=主语 谓语 [宾语]    //方括号0-1个
		    主语::=代词 | 名词 | 名词性短语
		    代词::="你"|"我"|"他"
		    
了解了产生式的写法,再来了解形式化语言的类型。
       所有的计算机语言都属于形式化语言,根据其表达能力,划分了乔姆斯基谱
 系,从0-3规则越来越严格。他们也是从属关系,即3型必从属于2型,以此类推。
(下方产生式的?即可指代终结符亦可指代非终结符)
1.0-型文法(无限制文法或短语结构文法)所有的计算机语言都属于此范畴,计算机
语 言必须是图灵完备的,也就是说该语言能完整编写出一个图灵机实现所有功能。
产生式?::=?
2.1-型文法(上下文相关文法)自然语言文法  
 产生式<上文><A><下文>::=<上文><B><下文>
3.2-型文法生成上下文无关语言 为大多数编程语言提供理论基础 
 产生式<A>::=?
 4.3-型 正规语言 表达能力等同有限状态自动机
 产生式<A>::=<A>?

Javascript的产生式

资料参考来自
链接: acma262 standrad

   我们只要了解当中的Lexical Grammar部分就能完成token解析了,其它语言也
 相通。但是我们需要精简一下,毕竟对于新的es6语法,以及完全还原JavaScript
 的解析代码量很大,也要做许多复杂处理,导致主题的思路反而不明确,所以
 简化下它的Lexical产生式如下:
InputElement ::= WhiteSpace | LineTerminator | Comment | Token
WhiteSpace ::= " " | “ ” (包括Unicode的所有空格类,Effe零宽空格)
LineTerminator::= "\n" | "\r"
Comment::=SignleLineComment | MultilineComment
SignleLineComment::="/" "/" <any>*
MultilineComment::="/" "*" ( [^*]|"*"[^/])* "*" "/"
Token::= Literal | keywords | Identifier | Punctuator
Literal::=NumberLiteral | BooleanLiteral | StringLiteral | NullLiteral
keywords::="if" | "else" | "for" | "function" ......
Punctuator::= "+" | "-" | "*" | "/" | "{" | "}" | ......
我们需要辨识出注释,运算符,数字,变量,关键字这些即可,运用正则的
强大能力对字符串处理。

少废话,上代码

ok,先使用一个文件来管理以及配置要用的正则,通过命名分组方式来分辨不同的token类型。代码如下:

//文件名regExpCompiler.js
var jsLexer = jsLexer || (function(scope){
    let keywords = /(?:const|yield|import|get|set|async|await|break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|of|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|__parent__|__count__|escape|unescape|with|__proto__|class|enum|extends|super|export|implements|private|public|interface|package|protected|static)(?![a-zA-Z\d\$_\u00a1-\uffff])/;
    let xregexp = {
        InputElement:"<WhiteSpace>|<LineTerminator>|<Comments>|<Token>",
        WhiteSpace:/ /,//空格符
        //行终结符
        LineTerminator:/\n/,
        //匹配注释
        Comments:/\/\*(?:[^*]|\*[^\/])*\*\/|\/\/[^\n]*/,
        Token:"<Literal>|<Keywords>|<Constructors>|<Identifier>|<Punctuator>",
        Literal:"<NumberLiteral>|<BooleanLiteral>|<StringLiteral>|<NullLiteral>",
        //数字类型
        NumberLiteral:/(?:[1-9][0-9]*|0)(?:\.[0-9]*)?|\.[0-9]+/,
        //布尔类型
        BooleanLiteral:/true|false/,
        //匹配字符串、字符串模板、正则
        StringLiteral:/\"(?:[^"\n]|\\[\s\S])*\"|\'(?:[^'\n]|\\[\s\S])*\'|\`(?:[^'\n]|\\[\s\S])*\`|\/[^\/].*\//,
        NullLiteral:/null/,
        Keywords:keywords,
        //Global的构造函数
        Constructors:/Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|Error|EvalError|InternalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|JSON|Math/,
        //变量标识符
        Identifier:/[a-zA-Z\$_\u00a1-\uffff][a-zA-Z\d\$_\u00a1-\uffff]*/,
        //运算符
        Punctuator:/\+|\-|\*|\\|\{|\}|\<|\>|\+\+|\-\-|\=|\(|\)|;|\,|\[|\]|\./,
    };
    function compileRegExp(xregexp,name){//替换当中的字符串最后变成终结符中的正则
        if(xregexp[name] instanceof RegExp) return  `(?<${name}>${xregexp[name].source})`;
        let regexp = xregexp[name].replace(/\<([^>]+)\>/g,function(str,$1){
            return compileRegExp(xregexp,$1);
        });
        return regexp;
    }
    let  compiledRegExp = compileRegExp(xregexp,"InputElement");
    let  lexerRegExp = new RegExp(compiledRegExp,"g");
    return lexerRegExp;
})(typeof window !== "undefined" && window || this);

if(typeof module !== "undefined" && module.exports){
    module.exports = jsLexer;
}

然后写个函数处理html过来的字符串以及执行正则。
//lexer.js
function escapehtml(str) {
    console.log(str);
    var arrEntities={'lt':'<','gt':'>','nbsp':' ','amp':'&','quot':'"'};
    return str.replace(/&(lt|gt|nbsp|amp|quot);/ig,function(all,t){return arrEntities[t];});
}
function scan(htmlText,callback){
    let str = escapehtml(htmlText);
    console.log(str,jsLexer);
    jsLexer.lastIndex = 0;
    while(jsLexer.lastIndex < str.length){
        let r = jsLexer.exec(str);
        callback(r);
        // fs.appendFileSync('./data.txt',JSON.stringify(r)+"\n");
        if(!r[0].length)
            break;
    }
}

最后主要的html,带样例:

<html>
<head>
    <style>
        .code-area{
            padding: 4px;
            margin: 0 auto;
            width: 650px;
            height: auto;
            min-height: 96px;
            background: #000;
            border: 1px solid #ccc;
            color: #fff;
        }
        .code-area span{
            white-space: nowrap;
        }
        .keywords{
            color: #c678dd;
        }
        .identifier{
            color:#61aeee;
        }
        .white-space{
           display: inline-block;
           width: .5em;
        }
        .string-literal{
            color: #d19a66;
        }
        .punctuator{
            color: #fff;
        }
        .Constructors{
            color: #98c379;
        }
        .Comments{
           color:#5c6370;
        }
        .number-literal{
            color: #98c379;
        }
    </style>
</head>
<body>
<div class="code-area" id="textEl">
function show(){//render board
    let board=document.getElementById("board");
    board.innerHTML="";
    for(let i=0;i&lt;pattern.length;i++){
        for(let j=0;j&lt;pattern[i].length;j++){
            let cell=document.createElement("div");
            cell.classList.add("cell");
            cell.innerText=["","O","X"][pattern[i][j]];
            cell.addEventListener("click",()=&gt;{move(i,j)})
            board.appendChild(cell);
        }
        board.appendChild(document.createElement("br"))
    }
}
/*
* @param xregexp {string|regexp} 输入的要解析的自定义正则
* @param name {string} 在集合中查找的子串
*/
function compileRegExp(xregexp,name){//编译正则
    if(xregexp[name] instanceof RegExp) return  `(?<${name}>${xregexp[name].source})`;
    let regexp = xregexp[name].replace(/\<([^>]+)\>/g,function(str,$1){
        return compileRegExp(xregexp,$1);
    });
    return regexp;
}
let  compiledRegExp = compileRegExp(xregexp,"InputElement");
let  lexerRegExp = new RegExp(compiledRegExp,"g");
return lexerRegExp;
</div>
    <script src="./regExpCompiler.js"></script>
    <script src="./lexer.js"></script>
    <script>
        //just identify token in codetext and high light them.
        var elem = document.getElementById("textEl");
        function makeTag(className,text){
            let tagSpan = document.createElement("span");
            tagSpan.className = className;
            tagSpan.innerText = text;
            return tagSpan;
        }
        let htmlText = elem.innerHTML.trim();
        let fragement = document.createDocumentFragment();
        scan(htmlText,(r)=>{//将捕获的命名分组替换为带颜色的span标签
            let tag;
            console.log(r)
            const {
                WhiteSpace,
                Comments,
                Keywords,
                Constructors,
                Identifier,
                LineTerminator,
                Punctuator,
                NumberLiteral,
                StringLiteral
            } =  r.groups;
            switch(true){
                case WhiteSpace!== undefined:tag=makeTag("white-space",WhiteSpace);break;
                case Comments!== undefined:tag=makeTag("Comments",Comments);break;
                case Keywords!== undefined:tag=makeTag("keywords",Keywords);break;
                case Constructors!== undefined:tag=makeTag("Constructors",Constructors);break;
                case Identifier!== undefined:tag=makeTag("identifier",Identifier);break;
                case LineTerminator!== undefined:tag=makeTag("line-terminator",LineTerminator);break;
                case Punctuator!== undefined:tag=makeTag("punctuator",Punctuator);break;
                case NumberLiteral!== undefined:tag=makeTag("number-literal",NumberLiteral);break;
                case StringLiteral!== undefined:tag=makeTag("string-literal",StringLiteral);break;
            }
            fragement.append(tag);
        });
        elem.innerHTML = '';
        elem.append(fragement);
    </script>
</body>
</html>

完成后的效果图如下:
在这里插入图片描述
如要实现函数声明和调用颜色改变需要外加正则,但是对于字符串模板中 的${}却无能为力,因为其中包的是表达式Expression需要上下文判断超出了正则表达式的能力,只能在词法分析阶段细化。鄙人才疏学浅,如果有谬误还请大佬们指点指点。
最后,学海无涯时刻保持谦虚学习的心态。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值