前言
本来想在干活儿的工具中加入一个基于html的编辑器,但是开发过程在光标定位,编辑器渲染等地方遇到了相当的问题。
虽然编辑器的开发相当失败。不过也学习到了很多知识。值得记录。
源代码
<!DOCTYPE html>
<html>
<head>
<title>实时SQL语法高亮示例</title>
<style>
.sql-editor {
font-family: monospace;
background-color: rgb(1, 50, 70);
width: 50%;
height: 800px;
padding: 8px 8px 8px 8px;
color: white;
font-size: 23px;
}
.menu_box{
background-color: rgb(0, 33, 46);
width: 50%;
height: 30px;
color: white;
padding: 8px 8px 8px 8px;
font-size: 23px;
display: flex;
align-items: center;
}
.menu_box button{
background-color: rgba(59, 59, 59, 1);
border-style: none;
color: rgb(224, 224, 224);
cursor: pointer;
}
.menu_box button:hover{
background-color: rgba(0,0,0,0);
border-style: none;
color: white;
cursor: pointer;
}
.sql-keyword {
color: blue;
font-weight: bold;
}
.sql-variable {
color: orange;
}
</style>
</head>
<body>
<div class="menu_box">
<div></div>
<div><button id="execute_sql">△执行sql</button></div>
</div>
<div id="sql-input" contenteditable="true" class="sql-editor" rows="10" cols="50">
<span></span>
</div>
<!-- 编辑器 -->
<script>
const TokenTypes = {
SELECT: "SELECT",
FROM: "FROM",
WHERE: "WHERE",
INNER: "INNER",
JOIN: "JOIN",
LEFT: "LEFT",
RIGHT: "RIGHT",
ON: "ON",
GROUP:"GROUP",
ORDER:"ORDER",
BY: "BY",
AS:"AS" ,
COMMENT:"COMMENT",
HAVING:"HAVING",
STRING:"STRING",
NUMBER:"NUMBER",
LEFT_PAREN:"LEFT_PAREN", //(
RIGHT_PAREN:"RIGHT_PAREN", //)
COMMA:"COMMA", // ,
DOT:"DOT", // .
SEMICOLON:"SEMICOLON", //;
SPACE:"SPACE", // 空格
IDENTIFIER:"IDENTIFIER",
ENTER:"ENTER",
XOA:"XOA",
EOF:"EOF"
};
const keywords = {
'SELECT':TokenTypes.SELECT,
'FROM':TokenTypes.FROM,
'WHERE':TokenTypes.WHERE,
'INNER':TokenTypes.INNER,
'JOIN':TokenTypes.JOIN,
'LEFT':TokenTypes.LEFT,
'RIGHT':TokenTypes.RIGHT,
'ON':TokenTypes.ON,
'GROUP':TokenTypes.GROUP,
'ORDER':TokenTypes.ORDER,
'BY':TokenTypes.BY,
'AS':TokenTypes.AS,
'COMMENT':TokenTypes.COMMENT,
'HAVING':TokenTypes.HAVING,
'LEFT_PAREN':TokenTypes.LEFT_PAREN,
'RIGHT_PAREN':TokenTypes.RIGHT_PAREN,
'COMMA':TokenTypes.COMMA,
'DOT':TokenTypes.DOT,
'SEMICOLON':TokenTypes.SEMICOLON,
'EOF':TokenTypes.EOF
};
class Token {
/** @type {TokenTypes} */
type;
/** @type {string} */
lexeme;
/** @type {Object} */
literal;
/** @type {int} */
line;
constructor(type, lexeme, literal, line) {
this.type = type;
this.lexeme = lexeme;
this.literal = literal;
this.line = line;
}
toString() {
return this.type + " " + this.lexeme + " " + line;
}
}
/**
* Scanner
* 词法扫描器
*/
class Scanner {
start = 0;
current = 0;
line = 0;
tokens = [];
/** @type {String} */
source = null;
constructor(source) {
this.source = source;
}
scanTokens(){
while(!this.isAtEnd() ){
this.start = this.current;
this.scanToken();
}
this.tokens.push(new Token(TokenTypes.EOF,"",null,this.line))
return this.tokens
}
scanToken() {
let c;
c = this.advance();
if (c == "("){
this.addToken(TokenTypes.LEFT_PAREN,c);
}
else if (c == ")"){
this.addToken(TokenTypes.RIGHT_PAREN,c);
}
else if (c == "."){
this.addToken(TokenTypes.DOT,c);
}else if (c == ","){
this.addToken(TokenTypes.COMMA,c);
}
else if (c == ";"){
this.addToken(TokenTypes.SEMICOLON,c);
}
else if (["\"","'"].includes(c)){
this.string(c);
}
else if (c == " "){
this.addToken(TokenTypes.SPACE," ");
}
else if (c == '\xA0'){
this.addToken(TokenTypes.SPACE,'\xA0');
}
else if(c == "\n"){
this.match("\n")
this.addToken(TokenTypes.ENTER,'\n');
}
else if(c == "\x0A"){
this.match("\x0A")
this.addToken(TokenTypes.XOA,'\x0A');
}
else{
if(this.isDigit(c)){
this.number();
}else if(this.isAlphaNumeric(c)){
this.identifier();
}else if(c == ""){
}
else{
throw ""+this.line+" " + c +" 非法字符!"
}
}
}
// 移动指针
advance(){
this.current+=1;
return this.source.charAt(this.current-1);
}
// 结束
isAtEnd(){
return this.current > this.source.length;
}
// 添加token
addToken(type, literal = null) {
let text = this.source.slice(this.start,this.current);
this.tokens.push(new Token(type,text,literal,this.line))
}
// 取指针当前指向字符
peek(){
if(!this.isAtEnd()){
return this.source.charAt(this.current);
}
return "\0";
}
// 取指针下一位字符
nextPeek(){
if(!this.isAtEnd()){
return this.source.charAt(this.current+1);
}
return "\0";
}
// 判断连续字符
match(c){
if(!this.isAtEnd() && this.peek()==c){
this.advance()
return true
}
return false
}
// 判断是否为数值
isDigit(c){
return c >= "0" && c <= "9";
}
//判断是否为字符串(包含汉字)
isAlpha(c){
return ("a" <= c && c <= "z") || ("A" <= c && c <= "Z") || c == "*" || c == "_" || ("一"<=c && c <= "龥")
}
// 判断是否为字符串或数值
isAlphaNumeric(c){
return this.isAlpha(c) || this.isDigit(c);
}
// 处理字符串
string(c){
while(this.peek() != c && !this.isAtEnd()){
if(this.peek() == "\n"){
this.line += 1;
}
this.advance();
}
if(this.isAtEnd()){
throw "Unterminated string.";
}
this.advance();
this.addToken(TokenTypes.STRING, this.source.slice(this.start+1,this.current-1));
}
// 处理数值
number(){
while (this.isDigit(this.peek())){
this.advance();
}
if (this.peek() == "." && this.isDigit(this.peekNext())){
this.current+=1
while(this.isDigit(this.peek())){
this.advance()
}
} this.addToken(TokenTypes.NUMBER,this.source.slice(this.start,this.current))
}
identifier(){ while(this.isAlphaNumeric(this.peek())){
this.advance();
} if(keywords[this.source.slice(this.start,this.current).toUpperCase()] != undefined){ this.addToken(keywords[keywords[this.source.slice(this.start,this.current).toUpperCase()]],keywords[keywords[this.source.slice(this.start,this.current).toUpperCase()]]);
}else{ this.addToken(TokenTypes.IDENTIFIER,keywords[keywords[this.source.slice(this.start,this.current)]]);
}
}
}
/**
* Parser
* 句法分析器 暂时先不用
*/
class Parser {
tokens;
current;
constructor(tokens) {
this.tokens = tokens;
this.current = 0;
}
parser(){
while(!this.isAtEnd()){
this.expression();
}
}
isAtEnd(){
return this.current > this.tokens.length
}
}
/**
* Redrew
* 语句重绘
*/
class Redraw {
tokens;
current;
doms="";
line="";
lines=[];
color_key = {
"SELECT":"rgb(190, 46, 221)","FROM":"rgb(190, 46, 221)",
"WHERE":"rgb(190, 46, 221)","INNER":"rgb(190, 46, 221)",
"JOIN":"rgb(190, 46, 221)","LEFT":"rgb(190, 46, 221)",
"RIGHT":"rgb(190, 46, 221)","ON":"rgb(190, 46, 221)",
"GROUP":"rgb(190, 46, 221)","ORDER":"rgb(190, 46, 221)",
"BY":"rgb(190, 46, 221)","AS":"rgb(190, 46, 221)",
"COMMENT":"rgb(190, 46, 221)","HAVING":"rgb(190, 46, 221)",
"LEFT_PAREN":"rgb(190, 46, 221)","RIGHT_PAREN":"rgb(190, 46, 221)",
"COMMA":"rgb(190, 46, 221)","DOT":"rgb(190, 46, 221)",
"SEMICOLON":"rgb(190, 46, 221)","EOF":"rgb(190, 46, 221)"
}
constructor(tokens){
this.tokens = tokens;
this.current = 0;
this.init()
}
init(){
this.current = 0;
this.doms="";
this.line = "";
this.lines = [];
}
redraw(){
this.init()
while(!this.isAtEnd()){
this.draw();
}
this.lines.push(this.line)
for(let i in this.lines){
let dom = this.lines[i];
if(dom == "<span>\n\x0A</span>") dom = "<span>\n\x0A</span><br>"
this.doms+=("<div>"+dom+"</div>")
}
console.log(this.doms);
return this.doms;
}
draw(){
let token = this.advance()
if(token.type == TokenTypes.EOF){
return;
}
if(token.type == TokenTypes.IDENTIFIER){
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}else if(token.type == TokenTypes.NUMBER){
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}else if(token.type == TokenTypes.STRING){
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}else if(token.type == TokenTypes.IDENTIFIER){
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}else if(token.type == TokenTypes.ENTER){
this.lines.push(this.line)
this.line = "<span>\n\x0A</span>"
}else if(token.type == TokenTypes.XOA){
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}
else{
this.line+= this.createSpan(token.lexeme,this.color_key[token.type]);
}
}
createSpan(value,color){
return `<span contenteditable="true" style = "color:${color}">${value}</span>`
}
createDiv(value,color){
return `<div><span contenteditable="true" style = "color:${color}">${value}</span></div>`
}
advance(){
this.current+=1;
return this.tokens[this.current-1];
}
isAtEnd(){
return this.current >= this.tokens.length
}
}
// 计算当前光标位置
function getSelectionX(new_data,old_data){
let num = 0;
let offset = 0;
let left = false;
if (new_data.length == old_data.length){
for (let i = 0; i < new_data.length; i++) {
if(new_data[i] != old_data[i]){
console.log("?????????/");
num=i+2
break;
}
}
}
else if(new_data.length > old_data.length){
for (let i = 0; i < new_data.length; i++) {
if(new_data[i] != old_data[i] && old_data[i]!="\x0A"){
num=i
for(;new_data[i]!=old_data[num];i++){
}
if(old_data[num]=="\n"){
i--;
}
num=i
break;
}else{
num = i
}
}
}
else{
for (let i = 0; i < old_data.length; i++) {
if(new_data[i] != old_data[i]){
num=i;
break;
}
}
}
return num+offset;
}
// 获取当前需要设置的光标的位置
function setSelectionX(spans,s_set){
let text_size = 0;
let span = null;
let span_set = 0;
let span_len = spans.length;
for(let span_num in spans){
let tmp_span_text = spans[span_num].innerHTML
console.log(">>>>>>>> ",tmp_span_text);
tmp_span_text = tmp_span_text.replace(" "," ")
if(tmp_span_text == undefined) break;
if(tmp_span_text=="\n\n" && span_num+1 < span_len){
tmp_span_text="\n"
}
text_size += tmp_span_text.length
if(text_size >= s_set){
span = spans[span_num]
span_set = text_size - s_set
let span_html = span.innerHTML.replace(" "," ")
span_set = span_html.length - span_set
if(text_size == s_set){
}
break
}
}
return [span,span_set];
}
function stringToAsc(ss){
let s = ""
for(let i=0;i<ss.length;i++){
s += " - "+ss.charCodeAt(i);
}
return s
}
let sqlInput = document.getElementById('sql-input');
let chace_data = sqlInput.innerText;
sqlInput.addEventListener('input', (e) => {
data = sqlInput.innerText;
console.log(chace_data); console.log(stringToAsc(chace_data));
console.log(data);
console.log(stringToAsc(data));
let scanner = new Scanner(data);
let tokens = scanner.scanTokens()
let redraw = new Redraw(tokens)
console.log(tokens);
let selection = window.getSelection();
let range = document.createRange();
sqlInput.innerHTML = redraw.redraw()
setTimeout(async () => {
let s_set = getSelectionX(data,chace_data);
console.log(s_set);
let spans = sqlInput.getElementsByTagName("span");
let selectionX = await setSelectionX(spans,s_set);
span = await selectionX[0];
span_set = await selectionX[1];
console.log(">>>>>" ,span);
if(!span["firstChild"]){ range.setStart(span,span_set); range.setEnd(span,span_set);
}else{ range.setStart(span.firstChild,span_set); range.setEnd(span.firstChild,span_set);
}
await selection.removeAllRanges();
await selection.addRange(range);
await console.log(selection);
chace_data = sqlInput.innerText;
}, 3);
}
);
</script>
<!-- 执行sql -->
<script> document.querySelector("#execute_sql").addEventListener('click',function(event){
console.log(event.target.innerText);
let value = document.querySelector("#sql-input").innerText;
sendSql(value);
});
function sendSql(sql){
const options = {
method: 'POST',
headers: {
Dg_authorization_token: 'eyJTRVNTSU9OSUQiOiJmMzhkMGI1YWYxYmM0MWNlODEwMzI1Nzc0OGM0NjViNiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZW5KIiwiZXhwIjoxNzEwNTA3NTgyfQ.rp3dbseYIuu27pmEniDboORetpXJpjYZNSNTFiUOJ1U',
'content-type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
element: '{\n"WorkFlow": {\n"LineList": [],\n"ElementList": [\n{\n"name": "表输出1",\n"type": "e_tableoutput",\n"value": {\n"name": "表输出1",\n"commitSize": 10000,\n"copies": 1,\n"zookeeperHdfsDatanode": "/hdfs-ha/service/ActiveStandbyElectorLock",\n"insertColumns": [],\n"dbId": "23c959bec5054741974161426a506907"\n}\n}\n]\n}\n}',
stepName: '表输出1',
sql: sql
})
}; fetch('http://xxxx.xxx.xxx.xx/etl/trans/executeSQL', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
}
</script>
</body>
</html> 

Comments | NOTHING