相比于上一篇文章,关键字列表完善了一下,支持多行注释,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>