1use crate::lexer::{Tok, lex_all};
4
5const BLOCK_KEYWORDS: &[&str] = &[
6 "table",
7 "mixin",
8 "blueprint",
9 "naming",
10 "constraints",
11 "nouns",
12];
13
14const STATEMENT_KEYWORDS: &[&str] = &[
15 "column",
16 "pk",
17 "index",
18 "use",
19 "override",
20 "except",
21 "belongs_to",
22 "unique_belongs_to",
23 "has_many",
24 "has_one",
25 "unique",
26 "comment",
27 "name",
28];
29
30const NAMES_A_TYPE: &[&str] = &[
32 "table",
33 "mixin",
34 "blueprint",
35 "use",
36 "belongs_to",
37 "unique_belongs_to",
38 "has_many",
39 "has_one",
40];
41
42const MACROS: &[&str] = &["associate", "apply_blueprint"];
43const FUNCTIONS: &[&str] = &["eval", "noun", "singular", "plural", "short", "desc"];
44const CONSTANTS: &[&str] = &[
45 "true",
46 "false",
47 "cascade",
48 "restrict",
49 "set_null",
50 "no_action",
51 "plural",
52 "singular",
53];
54
55pub fn to_html(src: &str) -> String {
59 let (tokens, _) = lex_all(src);
60 let mut out = String::new();
61 let mut cursor = 0usize;
62 let mut prev_ident: Option<String> = None;
63 let mut line_head: Option<String> = None;
65
66 for (i, tok) in tokens.iter().enumerate() {
67 if tok.value == Tok::Eof {
68 break;
69 }
70 if tok.span.start > cursor {
72 out.push_str(&escape(&src[cursor..tok.span.start]));
73 }
74 let text = &src[tok.span.start..tok.span.end];
75 let next_is_eq = matches!(tokens.get(i + 1).map(|t| &t.value), Some(Tok::Eq));
76 let next_is_paren = matches!(tokens.get(i + 1).map(|t| &t.value), Some(Tok::LParen));
77
78 let class = match &tok.value {
79 Tok::Comment(_) => Some("c"),
80 Tok::Str(_) => Some("s"),
81 Tok::Num(_) => Some("n"),
82 Tok::Eval(_) => Some("fn"),
83 Tok::Ident(name) => classify(name, &prev_ident, &line_head, next_is_eq, next_is_paren),
84 _ => None,
85 };
86
87 match &tok.value {
88 Tok::Eval(body) => {
89 out.push_str("<span class=\"fn\">eval</span>(");
90 out.push_str(&format!("<span class=\"raw\">{}</span>", escape(body)));
91 out.push(')');
92 }
93 Tok::Str(_) => out.push_str(&string_html(text)),
94 _ => match class {
95 Some(c) => out.push_str(&format!("<span class=\"{c}\">{}</span>", escape(text))),
96 None => out.push_str(&escape(text)),
97 },
98 }
99
100 match &tok.value {
101 Tok::Ident(name) => {
102 if line_head.is_none() {
103 line_head = Some(name.clone());
104 }
105 prev_ident = Some(name.clone());
106 }
107 Tok::Newline | Tok::LBrace | Tok::RBrace => {
108 prev_ident = None;
109 line_head = None;
110 }
111 Tok::Eq | Tok::Comment(_) => {}
113 _ => prev_ident = None,
114 }
115 cursor = tok.span.end;
116 }
117 if cursor < src.len() {
118 out.push_str(&escape(&src[cursor..]));
119 }
120 out
121}
122
123fn classify(
124 name: &str,
125 prev: &Option<String>,
126 line_head: &Option<String>,
127 next_is_eq: bool,
128 next_is_paren: bool,
129) -> Option<&'static str> {
130 if next_is_paren {
131 if MACROS.contains(&name) {
132 return Some("macro");
133 }
134 if FUNCTIONS.contains(&name) {
135 return Some("fn");
136 }
137 }
138 if next_is_eq {
139 return Some("attr");
140 }
141 if let Some(p) = prev.as_deref() {
143 if NAMES_A_TYPE.contains(&p) {
144 return Some("ty");
145 }
146 match p {
147 "column" | "override" => return Some("member"),
148 "type" => return Some("t"),
149 _ => {}
150 }
151 }
152 if BLOCK_KEYWORDS.contains(&name) || STATEMENT_KEYWORDS.contains(&name) {
153 return Some("k");
154 }
155 if CONSTANTS.contains(&name) {
156 return Some("cn");
157 }
158 match line_head.as_deref() {
160 Some("pk" | "index" | "except") => Some("member"),
161 Some(head) if MACROS.contains(&head) => Some("ty"),
162 Some("blueprint") => Some("var"),
163 _ => None,
164 }
165}
166
167fn string_html(text: &str) -> String {
169 let mut out = String::from("<span class=\"s\">");
170 let mut rest = text;
171 while let Some(i) = rest.find("${") {
172 let Some(close) = rest[i..].find('}') else {
173 break;
174 };
175 out.push_str(&escape(&rest[..i]));
176 out.push_str(&format!(
177 "<span class=\"tpl\">{}</span>",
178 escape(&rest[i..i + close + 1])
179 ));
180 rest = &rest[i + close + 1..];
181 }
182 out.push_str(&escape(rest));
183 out.push_str("</span>");
184 out
185}
186
187pub fn escape(s: &str) -> String {
188 s.replace('&', "&")
189 .replace('<', "<")
190 .replace('>', ">")
191}
192
193const SQL_KEYWORDS: &[&str] = &[
194 "CREATE",
195 "TABLE",
196 "ALTER",
197 "ADD",
198 "CONSTRAINT",
199 "PRIMARY",
200 "KEY",
201 "FOREIGN",
202 "REFERENCES",
203 "ON",
204 "DELETE",
205 "UPDATE",
206 "CASCADE",
207 "RESTRICT",
208 "SET",
209 "NULL",
210 "NOT",
211 "DEFAULT",
212 "UNIQUE",
213 "INDEX",
214 "COMMENT",
215 "IS",
216 "OR",
217 "REPLACE",
218 "FUNCTION",
219 "RETURNS",
220 "TRIGGER",
221 "BEFORE",
222 "FOR",
223 "EACH",
224 "ROW",
225 "EXECUTE",
226 "LANGUAGE",
227 "BEGIN",
228 "END",
229 "RETURN",
230 "AS",
231];
232
233pub fn sql_to_html(src: &str) -> String {
235 let mut out = String::new();
236 for token in split_keep(src) {
237 if SQL_KEYWORDS.contains(&token.to_ascii_uppercase().as_str()) {
238 out.push_str(&format!("<span class=\"k\">{}</span>", escape(token)));
239 } else if token.starts_with('\'') {
240 out.push_str(&format!("<span class=\"s\">{}</span>", escape(token)));
241 } else {
242 out.push_str(&escape(token));
243 }
244 }
245 out
246}
247
248fn split_keep(src: &str) -> Vec<&str> {
250 let mut parts = Vec::new();
251 let mut start = 0;
252 let mut in_word = false;
253 for (i, ch) in src.char_indices() {
254 let word = ch.is_alphanumeric() || ch == '_';
255 if word != in_word {
256 if i > start {
257 parts.push(&src[start..i]);
258 }
259 start = i;
260 in_word = word;
261 }
262 }
263 if start < src.len() {
264 parts.push(&src[start..]);
265 }
266 parts
267}