一个相当失败的基于html和js的sql编辑器


前言

本来想在干活儿的工具中加入一个基于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("&nbsp;"," ")
                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("&nbsp;"," ")
                    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>

声明:一代明君的小屋|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 一个相当失败的基于html和js的sql编辑器


欢迎来到我的小屋