use crate::lexer::{Tok, lex_all};
const BLOCK_KEYWORDS: &[&str] = &[
"table",
"mixin",
"blueprint",
"naming",
"constraints",
"nouns",
];
const STATEMENT_KEYWORDS: &[&str] = &[
"column",
"pk",
"index",
"use",
"override",
"except",
"belongs_to",
"unique_belongs_to",
"has_many",
"has_one",
"unique",
"comment",
"name",
];
const NAMES_A_TYPE: &[&str] = &[
"table",
"mixin",
"blueprint",
"use",
"belongs_to",
"unique_belongs_to",
"has_many",
"has_one",
];
const MACROS: &[&str] = &["associate", "apply_blueprint"];
const FUNCTIONS: &[&str] = &["eval", "noun", "singular", "plural", "short", "desc"];
const CONSTANTS: &[&str] = &[
"true",
"false",
"cascade",
"restrict",
"set_null",
"no_action",
"plural",
"singular",
];
pub fn to_html(src: &str) -> String {
let (tokens, _) = lex_all(src);
let mut out = String::new();
let mut cursor = 0usize;
let mut prev_ident: Option<String> = None;
let mut line_head: Option<String> = None;
for (i, tok) in tokens.iter().enumerate() {
if tok.value == Tok::Eof {
break;
}
if tok.span.start > cursor {
out.push_str(&escape(&src[cursor..tok.span.start]));
}
let text = &src[tok.span.start..tok.span.end];
let next_is_eq = matches!(tokens.get(i + 1).map(|t| &t.value), Some(Tok::Eq));
let next_is_paren = matches!(tokens.get(i + 1).map(|t| &t.value), Some(Tok::LParen));
let class = match &tok.value {
Tok::Comment(_) => Some("c"),
Tok::Str(_) => Some("s"),
Tok::Num(_) => Some("n"),
Tok::Eval(_) => Some("fn"),
Tok::Ident(name) => classify(name, &prev_ident, &line_head, next_is_eq, next_is_paren),
_ => None,
};
match &tok.value {
Tok::Eval(body) => {
out.push_str("<span class=\"fn\">eval</span>(");
out.push_str(&format!("<span class=\"raw\">{}</span>", escape(body)));
out.push(')');
}
Tok::Str(_) => out.push_str(&string_html(text)),
_ => match class {
Some(c) => out.push_str(&format!("<span class=\"{c}\">{}</span>", escape(text))),
None => out.push_str(&escape(text)),
},
}
match &tok.value {
Tok::Ident(name) => {
if line_head.is_none() {
line_head = Some(name.clone());
}
prev_ident = Some(name.clone());
}
Tok::Newline | Tok::LBrace | Tok::RBrace => {
prev_ident = None;
line_head = None;
}
Tok::Eq | Tok::Comment(_) => {}
_ => prev_ident = None,
}
cursor = tok.span.end;
}
if cursor < src.len() {
out.push_str(&escape(&src[cursor..]));
}
out
}
fn classify(
name: &str,
prev: &Option<String>,
line_head: &Option<String>,
next_is_eq: bool,
next_is_paren: bool,
) -> Option<&'static str> {
if next_is_paren {
if MACROS.contains(&name) {
return Some("macro");
}
if FUNCTIONS.contains(&name) {
return Some("fn");
}
}
if next_is_eq {
return Some("attr");
}
if let Some(p) = prev.as_deref() {
if NAMES_A_TYPE.contains(&p) {
return Some("ty");
}
match p {
"column" | "override" => return Some("member"),
"type" => return Some("t"),
_ => {}
}
}
if BLOCK_KEYWORDS.contains(&name) || STATEMENT_KEYWORDS.contains(&name) {
return Some("k");
}
if CONSTANTS.contains(&name) {
return Some("cn");
}
match line_head.as_deref() {
Some("pk" | "index" | "except") => Some("member"),
Some(head) if MACROS.contains(&head) => Some("ty"),
Some("blueprint") => Some("var"),
_ => None,
}
}
fn string_html(text: &str) -> String {
let mut out = String::from("<span class=\"s\">");
let mut rest = text;
while let Some(i) = rest.find("${") {
let Some(close) = rest[i..].find('}') else {
break;
};
out.push_str(&escape(&rest[..i]));
out.push_str(&format!(
"<span class=\"tpl\">{}</span>",
escape(&rest[i..i + close + 1])
));
rest = &rest[i + close + 1..];
}
out.push_str(&escape(rest));
out.push_str("</span>");
out
}
pub fn escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
const SQL_KEYWORDS: &[&str] = &[
"CREATE",
"TABLE",
"ALTER",
"ADD",
"CONSTRAINT",
"PRIMARY",
"KEY",
"FOREIGN",
"REFERENCES",
"ON",
"DELETE",
"UPDATE",
"CASCADE",
"RESTRICT",
"SET",
"NULL",
"NOT",
"DEFAULT",
"UNIQUE",
"INDEX",
"COMMENT",
"IS",
"OR",
"REPLACE",
"FUNCTION",
"RETURNS",
"TRIGGER",
"BEFORE",
"FOR",
"EACH",
"ROW",
"EXECUTE",
"LANGUAGE",
"BEGIN",
"END",
"RETURN",
"AS",
];
pub fn sql_to_html(src: &str) -> String {
let mut out = String::new();
for token in split_keep(src) {
if SQL_KEYWORDS.contains(&token.to_ascii_uppercase().as_str()) {
out.push_str(&format!("<span class=\"k\">{}</span>", escape(token)));
} else if token.starts_with('\'') {
out.push_str(&format!("<span class=\"s\">{}</span>", escape(token)));
} else {
out.push_str(&escape(token));
}
}
out
}
fn split_keep(src: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_word = false;
for (i, ch) in src.char_indices() {
let word = ch.is_alphanumeric() || ch == '_';
if word != in_word {
if i > start {
parts.push(&src[start..i]);
}
start = i;
in_word = word;
}
}
if start < src.len() {
parts.push(&src[start..]);
}
parts
}