import dbg from "../../debug/debug.js";import type { Token } from "../../ast/mod.ts";
const debug = dbg("lex");
export function lex(css: string): Token[] { let start = 0;
let buffer = ""; let ch: string; let column = 0; let cursor = -1; let depth = 0; let line = 1; let state = "before-selector"; const stack = [state]; let token: Token = {}; const tokens: Token[] = [];
const atRules: any = [ "media", "keyframes", { name: "-webkit-keyframes", type: "keyframes", prefix: "-webkit-" }, { name: "-moz-keyframes", type: "keyframes", prefix: "-moz-" }, { name: "-ms-keyframes", type: "keyframes", prefix: "-ms-" }, { name: "-o-keyframes", type: "keyframes", prefix: "-o-" }, "font-face", { name: "import", state: "before-at-value" }, { name: "charset", state: "before-at-value" }, "supports", "viewport", { name: "namespace", state: "before-at-value" }, "document", { name: "-moz-document", type: "document", prefix: "-moz-" }, "page", ];
function getCh(): string { skip(); return css[cursor]; }
function getState(index?: number): string { return index ? stack[stack.length - 1 - index] : state; }
function isNextString(str: string): boolean { let start = cursor + 1; return str === css.slice(start, start + str.length); }
function find(str: string): number | boolean { let pos = css.slice(cursor).indexOf(str);
return pos > 0 ? pos : false; }
function isNextChar(ch: string): boolean { return ch === peek(1); }
function peek(offset: number): string { return css[cursor + (offset || 1)]; }
function popState(): string | undefined { let removed = stack.pop(); state = stack[stack.length - 1];
return removed; }
function pushState(newState: string): number { state = newState; stack.push(state);
return stack.length; }
function replaceState(newState: string): string { let previousState = state; stack[stack.length - 1] = state = newState;
return previousState; }
function skip(n?: number) { if ((n || 1) == 1) { if (css[cursor] == "\n") { line++; column = 1; } else { column++; } cursor++; } else { let skipStr = css.slice(cursor, cursor + (n || 0)).split("\n"); if (skipStr.length > 1) { line += skipStr.length - 1; column = 1; } column += skipStr[skipStr.length - 1].length; cursor = cursor + (n || 0); } }
function addToken() { token.end = { line: line, col: column, };
debug("addToken:", JSON.stringify(token, null, 2));
tokens.push(token);
buffer = ""; token = {}; }
function initializeToken(type: string) { token = { type: type, start: { line: line, col: column, }, }; }
start = Date.now();
while ((ch = getCh())) { debug(ch, getState());
switch (ch) { case " ": switch (getState()) { case "selector": case "value": case "value-paren": case "at-group": case "at-value": case "comment": case "double-string": case "single-string": buffer += ch; break; } break;
case "\n": case "\t": case "\r": case "\f": switch (getState()) { case "value": case "value-paren": case "at-group": case "comment": case "single-string": case "double-string": case "selector": buffer += ch; break;
case "at-value": if ("\n" === ch) { token.value = buffer.trim(); addToken(); popState(); } break; }
break;
case ":": switch (getState()) { case "name": token.name = buffer.trim(); buffer = "";
replaceState("before-value"); break;
case "before-selector": buffer += ch;
initializeToken("selector"); pushState("selector"); break;
case "before-value": replaceState("value"); buffer += ch; break;
default: buffer += ch; break; } break;
case ";": switch (getState()) { case "name": case "before-value": case "value": if (buffer.trim().length > 0) { (token.value = buffer.trim()), addToken(); } replaceState("before-name"); break;
case "value-paren": buffer += ch; break;
case "at-value": token.value = buffer.trim(); addToken(); popState(); break;
case "before-name": break;
default: buffer += ch; break; } break;
case "{": switch (getState()) { case "selector": if (peek(-1) === "\\") { buffer += ch; break; }
token.text = buffer.trim(); addToken(); replaceState("before-name"); depth = depth + 1; break;
case "at-group": token.name = buffer.trim(); switch (token.type) { case "font-face": case "viewport": case "page": pushState("before-name"); break;
default: pushState("before-selector"); }
addToken(); depth = depth + 1; break;
case "name": case "at-rule": token.name = buffer.trim(); addToken(); pushState("before-name"); depth = depth + 1; break;
case "comment": case "double-string": case "single-string": buffer += ch; break; case "before-value": replaceState("value"); buffer += ch; break; }
break;
case "}": switch (getState()) { case "before-name": case "name": case "before-value": case "value": if (buffer) { token.value = buffer.trim(); }
if (token.name && token.value) { addToken(); }
initializeToken("end"); addToken(); popState();
if ("at-group" === getState()) { initializeToken("at-group-end"); addToken(); popState(); }
if (depth > 0) { depth = depth - 1; }
break;
case "at-group": case "before-selector": case "selector": if (peek(-1) === "\\") { buffer += ch; break; }
if (depth > 0) { if ("at-group" === getState(1)) { initializeToken("at-group-end"); addToken(); } }
if (depth > 1) { popState(); }
if (depth > 0) { depth = depth - 1; } break;
case "double-string": case "single-string": case "comment": buffer += ch; break; }
break;
case '"': case "'": switch (getState()) { case "double-string": if ('"' === ch && "\\" !== peek(-1)) { popState(); } break;
case "single-string": if ("'" === ch && "\\" !== peek(-1)) { popState(); } break;
case "before-at-value": replaceState("at-value"); pushState('"' === ch ? "double-string" : "single-string"); break;
case "before-value": replaceState("value"); pushState('"' === ch ? "double-string" : "single-string"); break;
case "comment": break;
default: if ("\\" !== peek(-1)) { pushState('"' === ch ? "double-string" : "single-string"); } }
buffer += ch; break;
case "/": switch (getState()) { case "comment": case "double-string": case "single-string": buffer += ch; break;
case "before-value": case "selector": case "name": case "value": if (isNextChar("*")) { let pos = find("*/");
if (pos && typeof pos !== "boolean") { skip(pos + 1); } } else { if (getState() == "before-value") replaceState("value"); buffer += ch; } break;
default: if (isNextChar("*")) { initializeToken("comment"); pushState("comment"); skip(); } else { buffer += ch; } break; } break;
case "*": switch (getState()) { case "comment": if (isNextChar("/")) { token.text = buffer; skip(); addToken(); popState(); } else { buffer += ch; } break;
case "before-selector": buffer += ch; initializeToken("selector"); pushState("selector"); break;
case "before-value": replaceState("value"); buffer += ch; break;
default: buffer += ch; } break;
case "@": switch (getState()) { case "comment": case "double-string": case "single-string": buffer += ch; break; case "before-value": replaceState("value"); buffer += ch; break;
default: let tokenized = false; let name; let rule;
for (let j = 0, len = atRules.length; !tokenized && j < len; ++j) { rule = atRules[j]; name = rule.name || rule;
if (!isNextString(name)) continue;
tokenized = true;
initializeToken(name); pushState(rule.state || "at-group"); skip(name.length);
if (rule.prefix) { token.prefix = rule.prefix; }
if (rule.type) { token.type = rule.type; } }
if (!tokenized) { buffer += ch; } break; } break;
case "(": switch (getState()) { case "value": pushState("value-paren"); break; case "before-value": replaceState("value"); break; }
buffer += ch; break;
case ")": switch (getState()) { case "value-paren": popState(); break; case "before-value": replaceState("value"); break; }
buffer += ch; break;
default: switch (getState()) { case "before-selector": initializeToken("selector"); pushState("selector"); break;
case "before-name": initializeToken("property"); replaceState("name"); break;
case "before-value": replaceState("value"); break;
case "before-at-value": replaceState("at-value"); break; }
buffer += ch; break; } }
debug("ran in", Date.now() - start + "ms");
return tokens;}