UNPKG

khufu

Version:

A template language for incremental-dom or DSL for javascript views

286 lines (256 loc) 9.83 kB
import Lexer from './baselexer'; const TOPLEVEL = 0; const TOPLEVEL_KW = ["import", "from", "style", "view", "as"] const NEWLINE = 1; const VIEW_KW = [ "store", "link", "let", "and", "or", "not", "for", "in", "of", "key", "if", "else", "elif", "catch", ... TOPLEVEL_KW] // reserve them for better error reporting const STYLE = 2; const VIEW = 10; const VIEW_LINESTART = 12; // "<" at tag open const VIEW_TAG = 14; // "abc-def" identifiers const VIEW_TEMPLATE = 16; // ${vars} identifiers const MATCHING_BRACKET = { '<': '>', '(': ')', '[': ']', '{': '}' } function lex(value) { if(typeof value == 'string') { return lex(lexeme => value); } else { return function(lexeme) { this.yytext = lexeme; let old_state = this.state; let result = value.call(this, lexeme) if(result && result != 'NL') { this.newline = false; } if(old_state == this.state && this.state == VIEW_LINESTART) { this.state = VIEW; } return result; } } } function unquote(value) { return value.replace(/\\["'bfnrt/\\]|\\u[a-fA-F0-9]{4}/g, function(x) { switch(x.charAt(1)) { case '"': return '"'; case "'": return "'"; case "b": return "\b"; case "f": return "\f"; case "n": return "\n"; case "r": return "\r"; case "t": return "\t"; case "/": return "/"; case "\\": return "\\"; case "u": return String.fromCharCode(parseInt(x.substr(2), 16)); default: return x; } return x }) } export default function () { let lexer = new Lexer(); /********************* Common tokens *****************************/ lexer.addRule(/\/\/.*$/m, lex(() => {}), []); lexer.addRule(/^\s*\/\/.*\n/m, lex(() => {}), []); /// The order of whitespace rules matter lexer.addRule(/\s*$/g, lex(function(lexeme) { let tokens = ['NL'] while(this.indent.length > 1) { tokens.push("DEDENT") this.indent.shift() } tokens.push('EOF') return tokens }), []); lexer.addRule(/\n/g, lex(function (lexeme) { if(this.brackets.length == 0 && !this.newline) { this.newline = true; return 'NL'; } }), []); lexer.addRule(/^[\t ]*/gm, lex(function(lexeme) { if(this.brackets.length != 0) { return; } var indentation = lexeme.length; if(this.state == VIEW) { this.state = VIEW_LINESTART; } if(indentation > this.indent[0]) { this.indent.unshift(indentation); return "INDENT"; } var tokens = []; if(indentation < this.indent[0]) { while (indentation < this.indent[0]) { tokens.push("DEDENT"); this.indent.shift(); } if(indentation != this.indent[0]) { throw Error("Invalid indentation, expected " + `${this.indent[0]}, got ${indentation} ` + `at line ${this.yylineno}`) } } if(this.indent[0] == 0) { this.state = TOPLEVEL; } if (tokens.length) return tokens; }), []); lexer.addRule(/[ \t]+/, lex(() => {}), []); lexer.addRule(/[({\[<]/, lex(function (lexeme) { if(lexeme == '<') { if(this.brackets.length == 0 && this.state == VIEW_LINESTART) { this.brackets.unshift(lexeme); this.state = VIEW_TAG; } else { this.reject = true; } } else { this.brackets.unshift(lexeme); if(this.state == VIEW_TAG) { this.state = VIEW; } } return lexeme; }), []); lexer.addRule(/[)}\]>]/, lex(function (lexeme) { this.yytext = lexeme; if(MATCHING_BRACKET[this.brackets[0]] != lexeme) { this.reject = true; return; } if(lexeme == '>') { if(this.brackets.length == 1 && this.brackets[0] == '<') { this.brackets.shift(); this.state = VIEW; } else { this.reject = true; } } else { this.brackets.shift(); if(this.brackets[0] == '<') { this.state = VIEW_TAG; } } return lexeme; }), []); lexer.addRule(/"(?:\\["'bfnrt/\\]|\\u[a-fA-F0-9]{4}|[^"\\])*"/, lex(function (lex) { this.yytext = unquote(lex.substr(1, lex.length-2)); return 'STRING'; }), [TOPLEVEL, VIEW, VIEW_TAG, VIEW_LINESTART, VIEW_TEMPLATE]); lexer.addRule(/'(?:\\["'bfnrt/\\]|\\u[a-fA-F0-9]{4}|[^'\\])*'/, lex(function (lex) { this.yytext = unquote(lex.substr(1, lex.length-2)); return 'STRING'; }), [TOPLEVEL, VIEW, VIEW_TAG, VIEW_LINESTART, VIEW_TEMPLATE]); /********************* Toplevel tokens ***************************/ lexer.addRule(/[a-zA-Z_][a-zA-Z0-9_]*/, lex(function (lexeme) { if(TOPLEVEL_KW.indexOf(lexeme) >= 0) { switch(lexeme) { case "style": this.state = STYLE; break; case "view": this.state = VIEW; break; } return lexeme; } else { return 'IDENT'; } }), [TOPLEVEL]); lexer.addRule(/[/*+\\-^,\\.]/, lex(lexeme => lexeme), [TOPLEVEL]); /********************* Style tokens ***************************/ let css_esc = "\\\\[^\\n0-9a-fA-F]|\\\\[0-9a-fA-F]{1,6} ?" let css_ident = "-*" + `(?:[a-zA-Z_\\u0080-\\uffff]|${css_esc})` + `(?:[a-zA-Z_0-9\\u0080-\\uffff-]|${css_esc})*` let css_number = "[+-]?(?:[0-9]+\\.[0-9]+|[0-9]+|\\.[0-9]+)(?:[eE][+-]?[0-9]+)?" let css_unquoted = "(?:[^\"'()\\s\\u0000-\\u0008\\u000b\\u000e-\\u001f\\u007f]" + `|${css_esc})*` let css_string = `(?:"(?:[^"\\\\\\n]|${css_esc}|\\\\\\n)*"` + `|'(?:[^'\\\\\\n]|${css_esc}|\\\\\\n)*')` lexer.addRule(/[:.,]/, lex(x => x), [STYLE]); lexer.addRule(/@media/, lex("MEDIA"), [STYLE]); lexer.addRule(new RegExp(css_ident), lex("IDENT_TOKEN"), [STYLE]); lexer.addRule(new RegExp(css_number), lex("NUMBER"), [STYLE]); lexer.addRule(new RegExp(css_number + css_ident), lex("DIMENSION"), [STYLE]); lexer.addRule(new RegExp(css_number + '%'), lex("PERCENTAGE_TOKEN"), [STYLE]); lexer.addRule(new RegExp("url\\(\\s*(?:" + css_string + "|" + css_unquoted + ")\s*\\)"), lex("URL"), [STYLE]); // I'm not sure it's useful in normal css but it's used // in `composes: xx from "file"` in CSS Modules lexer.addRule(new RegExp(css_string), lex("CSS_STRING"), [STYLE]); lexer.addRule(new RegExp("#(?:[a-zA-Z_0-9\\u0080-\\uffff-]|${css_esc})*"), lex("HASH_TOKEN"), [STYLE]); /********************* View tokens ***************************/ lexer.addRule(/->/, lex(x => x), [VIEW, VIEW_LINESTART]); lexer.addRule(/<-/, lex(x => x), [VIEW, VIEW_LINESTART]); lexer.addRule(/[!=][=][=]/, lex(x => x), [VIEW, VIEW_LINESTART, VIEW_TEMPLATE]); lexer.addRule(/[><!=]\=/, lex(x => x), [VIEW, VIEW_LINESTART, VIEW_TEMPLATE]); lexer.addRule(/[|:,.*+/%=<>!?-]/, lex(x => x), [VIEW, VIEW_LINESTART, VIEW_TEMPLATE]); lexer.addRule(/[a-zA-Z_][a-zA-Z0-9_]*/, lex(function(lexeme) { if(VIEW_KW.indexOf(lexeme) >= 0) { return lexeme; } else { return 'IDENT'; } }), [VIEW, VIEW_LINESTART, VIEW_TEMPLATE]); lexer.addRule(/@[a-zA-Z_][a-zA-Z0-9_]*/, lex(function(lexeme) { this.yytext = lexeme.substr(1); return 'STORE'; }), [TOPLEVEL, VIEW, VIEW_LINESTART, VIEW_TAG, VIEW_TEMPLATE]); lexer.addRule(/[a-zA-Z_][a-zA-Z0-9_-]*/, lex('TAG_NAME'), [VIEW_TAG]); lexer.addRule(/[=.?+-]/, lex(x => x), [VIEW_TAG]); lexer.addRule(new RegExp( "(?:[0-9]|[1-9][0-9]+)" + // integer part "(?:\\.[0-9]+)?" + // fractional part "(?:[eE][-+]?[0-9]+)?\\b"), // exponent lex("NUMBER"), [VIEW, VIEW_TAG, VIEW_LINESTART, VIEW_TEMPLATE]) lexer.addRule( /`(?:\\["'bfnrt/\\]|\\u[a-fA-F0-9]{4}|\$(?!\{)|[^$`\\])*(?:`|\$\{)/, lex(function (lex) { if(lex.charAt(lex.length-1) == '`') { this.yytext = unquote(lex.substr(1, lex.length-2)); return 'TEMPLATE_STRING'; } else { this.yytext = unquote(lex.substr(1, lex.length-3)); this.templates += 1; this.state = VIEW_TEMPLATE; return 'TEMPLATE_BEGIN'; } }), [VIEW, VIEW_TAG, VIEW_LINESTART]); lexer.addRule( /}(?:\\["'bfnrt/\\]|\\u[a-fA-F0-9]{4}|\$(?!\{)|[^$`\\])*(?:`|\$\{)/, lex(function (lex) { if(lex.charAt(lex.length-1) == '`') { this.yytext = unquote(lex.substr(1, lex.length-2)); this.templates -= 1; if(!this.templates) { if(this.brackets[0] == '<') { this.state = VIEW_TAG; } else { this.state = VIEW; } } return 'TEMPLATE_END'; } else { this.yytext = unquote(lex.substr(1, lex.length-3)); return 'TEMPLATE_INTER'; } }), [VIEW_TEMPLATE]); return lexer.factory(); }