jQuery实现代码高亮编辑器

我在写一个网页版navicat,其中的一个小插曲。只是一种思路,肯定会有不完善的地方,我最近正在完善。目前词法分析只针对Mysql。
用jquery是因为方便,后期会去掉。除了jquery以外没有任何第三方插件。有个codemirror插件,没有细细地研究过。因为后期想封装成vue组件,所以codemirror这种感觉有点老了。
原理:h5 div有contenteditable属性,为true时div可以编辑。如果只有一个div开启编辑模式,同时触发事件去处理输入内容,并回显到这个div上,光标会自动回到0的位置。如果这个div内部有很多span标签,那么光标实际作用的对象是这些span,而不是这个div本身。所以要将光标强行改为原来位置,需要进行计算,而且退格和删除还要另外判断,所以很麻烦。这个想法暂时告一段落。
换一种思路,如果让这个用来编辑的div只用来响应编辑的动作,再用一个div专门用来渲染高亮后的代码标签,这样就简单多了。可以把显示用的div放在下层,编辑的div放在上层(字体颜色设为透明)。编辑会触发input事件,经过计算后将标签渲染到显示div上面(内容不变,只是样式不一样),造成一种实时高亮的假象。

运行效果:
运行效果

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>语法自动高亮编辑器</title>
    <link rel="stylesheet" href="index.css">
    <!--    可以通过引入不同的css来切换主题-->
    <link rel="stylesheet" href="theme.css">
    <script src="../lib/jquery.js"></script>
	<script src="codeParser.js"></script>
	<script src="render.js"></script>
</head>
<body>
    <!--    外部容器-->
    <div id="container">
        <!--    这个是真实接受输入的div,覆盖在显示div上方-->
        <div class="innerDiv innerDiv-edit" contenteditable="true" id="editorDiv"></div>
        <!--这个div只用来显示渲染出的结果-->
        <div class="innerDiv innerDiv-display" id="displayDiv"></div>
    </div>
</body>
</html>

index.css

/**
清除浏览器默认margin和padding
 */
* {
    margin: 0;
    padding: 0;
}

/**
外部容器的样式
 */
#container {
    width: 800px;
    height: 400px;
    margin: 30px auto;
    position: relative;
    border: 2px solid red;
    font-size: 25px;
    border-radius: 5px;
}

/**
编辑和显示div的宽高都随父div
*/
.innerDiv {
    width: 100%;
    height: 100%;
    position: absolute;/*绝对定位*/
}

/**
显示div的样式可以自定义
 */
.innerDiv-display {

}

/**
编辑div的样式
 */
.innerDiv-edit {
    color: transparent;/*编辑字体颜色设为透明不可见*/
    position: absolute;/*绝对定位*/
    caret-color:black;/*光标颜色设为默认黑色*/
    z-index: 999;/*提高层级使编辑div永远覆盖在最上方*/
    outline: none;/*去掉默认编辑元素的outline*/
}

theme.css
这个分离出来方便扩展不同主题。

/*高亮主题颜色,可以自定义某个类型的样式*/

/**
单词
 */
.word {
    color: black;
}

/**
空白符
 */
.blank {

}

/**
字符串
 */
.str {
    color: green;
}

/**
整型
 */
.int {
    color: blue;
}

/**
系统关键字
 */
.keyword {
    color: red;
}

/**
注释
 */
.comment {
    color: pink;
}

codeParser.js

/**
 * 根据当前片段第一个字符判断类型
 * @param char 需要判断的字符
 * @returns {string} token类型
 */
function getType(char) {
    if(/\s/.test(char)) {
        return 'blank';
    }else if(char === '\'') {
        return 'singleStr';
    }else if(char === '\"') {
        return 'doubleStr';
    }else if(/\d/.test(char)) {
        return 'int';
    }else if(char === '#') {
        return 'comment';
    }
}

/**
 * 读取空白符
 * @param str 原字符串
 */
function readBlankChar(str) {
    endIndex = -1;
    for (let i = index+1; i < str.length; i++) {
        char = str.charAt(i);
        //读到非空白符时停止
        if(/\S/.test(char)) {
            endIndex = i;
            break
        }
    }
    //防止读到内容结尾还没结束的情况
    if(endIndex === -1) endIndex = str.length;
    words.push({content:str.substring(index,endIndex),type:'blank'});
    index = endIndex;
}

/**
 * 读取单词
 * @param str 原字符串
 */
function readWord(str) {
    endIndex = -1;
    for (let i = index+1; i < str.length; i++) {
        char = str.charAt(i);
        //读到空白符时停止
        if(/\s/.test(char)) {
            endIndex = i;
            break;
        }
    }
    //防止读到内容结尾还没结束的情况
    if(endIndex === -1) endIndex = str.length;
    let content = str.substring(index,endIndex);
    //如果单词在关键字列表中,标记类型为关键字
    if(keywords.includes(content)) {
        words.push({content: content,type:'keyword'});
    }else {
        //默认类型为普通单词
        words.push({content: content,type:'word'});
    }
    index = endIndex;
}

/**
 * 读取单词
 * @param str 原字符串
 */
function readComment(str) {
    endIndex = -1;
    for (let i = index+1; i < str.length; i++) {
        char = str.charAt(i);
        //读到换行时停止
        if(char === '\n') {
            endIndex = i;
            break;
        }
    }
    //防止读到内容结尾还没结束的情况
    if(endIndex === -1) endIndex = str.length;
    let content = str.substring(index,endIndex);
    words.push({content: content,type:'comment'});
    index = endIndex;
}

/**
 * 读取字符串
 * @param str 原字符串
 * @param stopStr 结束符 可以是单引号或者双引号字符
 */
function readStr(str,stopStr) {
    endIndex = -1;
    for (let i = index+1; i < str.length; i++) {
        char = str.charAt(i);
        //读到结束符时停止
        if(char === stopStr) {
            endIndex = i;
            break;
        }
    }
    //防止读到内容结尾还没结束的情况
    if(endIndex === -1) endIndex = str.length;
    //MYSQL不区分单双引号,所以都记为str
    words.push({content:str.substring(index,endIndex+1),type:'str'});
    index = endIndex+1;
}

/**
 * 读取整型
 * @param str 原字符串
 */
function readInt(str) {
    endIndex = -1;
    for (let i = index+1; i < str.length; i++) {
        char = str.charAt(i);
        if(!/\d/.test(char)) {
            endIndex = i;
            break;
        }
    }
    //防止读到内容结尾还没结束的情况
    if(endIndex === -1) endIndex = str.length;
    words.push({content:str.substring(index,endIndex),type:'int'});
    index = endIndex;
}

/**
 * 原始文本转为token对象数组
 * @param str
 */
function toWords(str) {
    while(index < str.length) {
        char = str.charAt(index);
        let type = getType(char);
        //根据不同的类型去调用不同的处理函数
        switch (type) {
            case 'blank':
                readBlankChar(str);
                break;
            case 'singleStr':
                readStr(str,'\'');
                break;
            case 'doubleStr':
                readStr(str,'\"');
                break;
            case 'int':
                readInt(str);
                break;
            case 'comment':
                readComment(str);
                break;
            default:
                readWord(str);
        }
    }
}

/**
 * token对象数组转为node str
 * @returns {string}
 */
function toNodes() {
    let nodeStr = '';
    //遍历token数组根据不同的类型添加对应的类名
    words.forEach(item => {
        nodeStr += `<span class="${item.type}">${item.content}</span>`;
    });
    return nodeStr;
}

let index = 0;//当前指针
let endIndex;//结束指针
let char;//当前字符
let words = [];//token对象数组
const keywords = ['select','as','where','from'];//关键字数组

render.js

$(function () {

    /**
     * div 输入事件
     */
    $('#editorDiv').on('input',function (e) {
        index = 0;//索引回到0的位置
        words = [];//单词表清空
        let content = $(this).text();//获取原始内容
        toWords(content);//解析单词表
        let nodeStr = toNodes();//获取将要被渲染的元素
        $('#displayDiv').html(nodeStr);//渲染到元素中
    });

});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值