代码高亮编辑器封装成vue组件

相比于上一篇文章,关键字列表完善了一下,支持多行注释,js面向过程改为面向对象,去掉jQuery依赖,封装为vue组件。
效果图:
在这里插入图片描述
App.vue

<template>
    <div>
        <code-high-light :width="800" :height="400" class="code-hl"/>
    </div>
</template>

<script>
    import CodeHighLight from "./components/CodeHighLight";

    export default {
        name: 'App',
        components: {CodeHighLight},
    }
</script>

<style scoped>
    .code-hl {
        border: 2px solid darkcyan;
        margin: 30px auto;
        font-size: 25px;
        border-radius: 5px;
    }
</style>

theme.css见上一篇文章

CodeParser.js

/**
 * 面向对象形式
 */
export class CodeParser {
    constructor(isMultiComment) {
        this.index = 0;//当前指针
        this.endIndex = null;//结束指针
        this.isComment1 = false;//# 注释
        this.isComment2 = false;// -- 注释
        this.words = [];//token对象数组
        this.isMultiComment = isMultiComment;
        this.keywords = ["ADD", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC", "ASENSITIVE",
            "BEFORE", "BETWEEN", "BIGINT", "BINARY", "BLOB", "BOTH", "BY",
            "CALL", "CASCADE", "CASE", "CHANGE", "CHAR", "CHARACTER", "CHECK", "COLLATE",
            "COLUMN", "CONDITION", "CONNECTION", "CONSTRAINT", "CONTINUE", "CONVERT",
            "CREATE", "CROSS", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
            "CURRENT_USER", "CURSOR",
            "DATABASE", "DATABASES", "DAY_HOUR", "DAY_MICROSECOND", "DAY_MINUTE",
            "DAY_SECOND", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DELAYED", "DELETE",
            "DESC", "DESCRIBE", "DETERMINISTIC", "DISTINCT", "DISTINCTROW", "DIV", "DOUBLE",
            "DROP", "DUAL",
            "EACH", "ELSE", "ELSEIF", "ENCLOSED", "ESCAPED", "EXISTS", "EXIT", "EXPLAIN",
            "FALSE", "FETCH", "FLOAT", "FLOAT4", "FLOAT8", "FOR", "FORCE", "FOREIGN", "FROM", "FULLTEXT",
            "GOTO", "GRANT", "GROUP",
            "HAVING", "HIGH_PRIORITY", "HOUR_MICROSECOND", "HOUR_MINUTE", "HOUR_SECOND",
            "IF", "IGNORE", "IN", "INDEX", "INFILE", "INNER", "INOUT", "INSENSITIVE", "INSERT", "INT",
            "INT1", "INT2", "INT3", "INT4", "INT8", "INTEGER", "INTERVAL", "INTO", "IS", "ITERATE",
            "JOIN",
            "KEY", "KEYS", "KILL",
            "LABEL", "LEADING", "LEAVE", "LEFT", "LIKE", "LIMIT", "LINEAR", "LINES", "LOAD", "LOCALTIME",
            "LOCALTIMESTAMP", "LOCK", "LONG", "LONGBLOB", "LONGTEXT", "LOOP", "LOW_PRIORITY",
            "MATCH", "MEDIUMBLOB", "MEDIUMINT", "MEDIUMTEXT", "MIDDLEINT", "MINUTE_MICROSECOND",
            "MINUTE_SECOND", "MOD", "MODIFIES",
            "NATURAL", "NOT", "NO_WRITE_TO_BINLOG", "NULL", "NUMERIC",
            "ON", "OPTIMIZE", "OPTION", "OPTIONALLY", "OR", "ORDER", "OUT", "OUTER", "OUTFILE",
            "PRECISION", "PRIMARY", "PROCEDURE", "PURGE",
            "RAID0", "RANGE", "READ", "READS", "REAL", "REFERENCES", "REGEXP", "RELEASE", "RENAME",
            "REPEAT", "REPLACE", "REQUIRE", "RESTRICT", "RETURN", "REVOKE", "RIGHT", "RLIKE",
            "SCHEMA", "SCHEMAS", "SECOND_MICROSECOND", "SELECT", "SENSITIVE", "SEPARATOR", "SET",
            "SHOW", "SMALLINT", "SPATIAL", "SPECIFIC", "SQL", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING",
            "SQL_BIG_RESULT", "SQL_CALC_FOUND_ROWS", "SQL_SMALL_RESULT", "SSL", "STARTING", "STRAIGHT_JOIN",
            "TABLE", "TERMINATED", "THEN", "TINYBLOB", "TINYINT", "TINYTEXT", "TO", "TRAILING", "TRIGGER", "TRUE",
            "UNDO", "UNION", "UNIQUE", "UNLOCK", "UNSIGNED", "UPDATE", "USAGE", "USE", "USING",
            "UTC_DATE", "UTC_TIME", "UTC_TIMESTAMP",
            "VALUES", "VARBINARY", "VARCHAR", "VARCHARACTER", "VARYING",
            "WHEN", "WHERE", "WHILE", "WITH", "WRITE",
            "X509", "XOR",
            "YEAR_MONTH",
            "ZEROFILL"];//关键词数组
    }

    /**
     * 解析并渲染
     * @param str
     * @returns {string}
     */
    parseAndRender(str) {
        this.toWords(str);//解析单词表
        return this.toNodes();//获取将要被渲染的元素
    }

    /**
     * 初始化一些类变量
     */
    initGlobalData(isMultiComment) {
        this.index = 0;//索引回到0的位置
        this.endIndex = null;
        this.words = [];//单词表清空
        this.isComment1 = false;
        this.isComment2 = false;
        this.isMultiComment = isMultiComment;
    }
    /**
     * 原始文本转为token对象数组
     * @param str
     */
    toWords(str) {
        while(this.index < str.length) {
            if(this.isComment1) {
                this.readComment(str);
            }
            if(this.isComment2) {
                this.readBlankChar(str);
                this.readComment(str);
            }else if(this.isMultiComment) {
                this.readMultiComment(str);
            }else {
                let type = this.getType(str);
                //根据不同的类型去调用不同的处理函数
                switch (type) {
                    case 'blank':
                        this.readBlankChar(str);
                        break;
                    case 'singleStr':
                        this.readStr(str,'\'');
                        break;
                    case 'doubleStr':
                        this.readStr(str,'\"');
                        break;
                    case 'int':
                        this.readInt(str);
                        break;
                    case 'comment':
                        this.readComment(str);
                        break;
                    case 'multiComment':
                        this.readMultiComment(str);
                        break;
                    default:
                        this.readWord(str);
                }
            }
        }
    }
    /**
     * token对象数组转为node str
     * @returns {string}
     */
    toNodes() {
        let nodeStr = `<div class="line">`;
        //遍历token数组根据不同的类型添加对应的类名
        this.words.forEach(item => {
            nodeStr += `<span class="${item.type}">${item.content}</span>`;
        });
        nodeStr += `</div>`;
        return nodeStr;
    }
    /**
     * 根据当前片段第一个字符判断类型
     * @param str
     * @returns {string} token类型
     */
    getType(str) {
        let char = str.charAt(this.index);
        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';
        }else if(char === '/' && str.charAt(this.index+1) === '*') {
            this.isMultiComment = true;
            return 'multiComment';
        }
    }

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

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

    /**
     * 读取注释
     * @param str 原字符串
     */
    readComment(str) {
        //一直读到该行结尾(换行h5内部已经做了处理)
        this.endIndex = str.length;
        let content = str.substring(this.index,this.endIndex);
        this.words.push({content: content,type:'comment'});
        this.index = this.endIndex;
    }

    /**
     * 读取多行注释
     * @param str 原字符串
     */
    readMultiComment(str) {
        this.endIndex = -1;
        for (let i = this.index; i < str.length; i++) {
            //读到*/时停止
            // if(str.substring(i,i+1) === '*/') {
            if(str.charAt(i) === '*' && str.charAt(i+1) === '/') {
                this.endIndex = i;
                this.isMultiComment = false;
                break;
            }
        }
        //防止读到内容结尾还没结束的情况
        if(this.endIndex === -1) this.endIndex = str.length;
        let content = str.substring(this.index,this.endIndex+2);
        this.words.push({content: content,type:'comment'});
        this.index = this.endIndex+2;
    }

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

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

CodeHighLight.vue

<template>
    <div :style="{width:width+'px',height:height+'px'}">
        <div class="hl-div hl-div-edit" contenteditable="true" @input="handleInput($event.target)"></div>
        <div class="hl-div hl-div-display" v-html="nodeStr"></div>
    </div>
</template>

<script>
    import {CodeParser} from './CodeParser';

    export default {
        name: "CodeHighLight",
        data() {
            return {
                nodeStr: ''
            }
        },
        props: {
            width: {
                type: Number,
                default: 800
            },
            height: {
                type: Number,
                default: 400
            }
        },
        methods: {
            handleInput(target) {
                const firstLine = target.firstChild ? target.firstChild.textContent : '';//当文本框被清空时,this.firstChild为undefined
                let isMultiComment = false;
                const codeParser = new CodeParser(isMultiComment);
                this.nodeStr = codeParser.parseAndRender(firstLine);
                isMultiComment = codeParser.isMultiComment;
                let children = target.children;
                for (let i = 0; i < children.length; i++) {
                    let childLine = children[i].textContent;
                    if (childLine.length === 0) {//只是换行没有内容
                        this.nodeStr += `<div><br/></div>`;
                    } else {
                        codeParser.initGlobalData(isMultiComment);
                        this.nodeStr += codeParser.parseAndRender(childLine);//获取将要被渲染的元素
                        isMultiComment = codeParser.isMultiComment;
                    }
                }
                console.log(this.nodeStr);
            },
        },
    }
</script>

<style scoped>
    /*导入主题css*/
    @import "../assets/theme.css";

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

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

    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值