feat: refactor syntax higlighting support in edit.elf/Text Editor, add highlighting for html, css, js
This commit is contained in:
1 parent
5b90869101
commit
0a18b2cf2d
9 files changed
+1289
-1377
No files matched your search
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* lang_html.hpp
|
||||
* HTML highlighting, with embedded CSS and JS
|
||||
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
// Included by <syntax/syntax.hpp>; not standalone.
|
||||
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// JavaScript keyword / builtin classification
|
||||
// ============================================================================
|
||||
|
||||
inline SynToken syn_classify_js_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"async", "await", "break", "case", "catch", "class", "const",
|
||||
"continue", "debugger", "default", "delete", "do", "else",
|
||||
"export", "extends", "finally", "for", "function", "if",
|
||||
"import", "in", "instanceof", "let", "new", "of", "return",
|
||||
"static", "super", "switch", "this", "throw", "try", "typeof",
|
||||
"var", "void", "while", "with", "yield",
|
||||
"true", "false", "null", "undefined",
|
||||
};
|
||||
static const char* builtins[] = {
|
||||
"Array", "Boolean", "Date", "Error", "Infinity", "JSON", "Map",
|
||||
"Math", "NaN", "Number", "Object", "Promise", "Proxy", "RegExp",
|
||||
"Set", "String", "Symbol", "WeakMap", "WeakSet",
|
||||
"console", "document", "globalThis", "localStorage", "navigator",
|
||||
"window", "fetch", "setTimeout", "setInterval",
|
||||
};
|
||||
for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) {
|
||||
if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD;
|
||||
}
|
||||
for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) {
|
||||
if (syn_streq(buf, len, builtins[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JavaScript (also used for <script> bodies inside HTML)
|
||||
// ============================================================================
|
||||
//
|
||||
// Highlights line[i .. limit) - `limit` is the end of the JS region on this
|
||||
// line, which for an embedded script is where the closing </script> begins.
|
||||
|
||||
inline void syn_highlight_js_range(const char* line, int limit, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
while (i < limit) {
|
||||
// ---- Block comment continuation ----
|
||||
if (state.in_block_comment) {
|
||||
while (i < limit) {
|
||||
if (i + 1 < limit && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
|
||||
i += 2;
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = line[i];
|
||||
|
||||
if (c == '/' && i + 1 < limit && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, limit, SYN_COMMENT);
|
||||
i = limit;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == '/' && i + 1 < limit && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- String / template literal (single line only) ----
|
||||
if (c == '"' || c == '\'' || c == '`') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < limit) {
|
||||
if (line[i] == '\\' && i + 1 < limit) {
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < limit && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, limit, i, out, out_len, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_alpha(c) || c == '$') {
|
||||
int start = i;
|
||||
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '$')) i++;
|
||||
syn_fill_tokens(out, out_len, start, i,
|
||||
syn_classify_js_word(line + start, i - start));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
|
||||
c == '=' || c == '!' || c == '<' || c == '>' || c == '&' ||
|
||||
c == '|' || c == '^' || c == '~' || c == '?' || c == ':') {
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CSS (also used for <style> bodies inside HTML)
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_css_range(const char* line, int limit, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
while (i < limit) {
|
||||
// ---- Block comment continuation ----
|
||||
if (state.in_block_comment) {
|
||||
while (i < limit) {
|
||||
if (i + 1 < limit && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
|
||||
i += 2;
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = line[i];
|
||||
|
||||
if (c == '/' && i + 1 < limit && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_COMMENT);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < limit) {
|
||||
if (line[i] == '\\' && i + 1 < limit) {
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- At-rule (@media, @import, ...) ----
|
||||
if (c == '@') {
|
||||
int start = i;
|
||||
i++;
|
||||
while (i < limit && syn_is_alnum(line[i])) i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_PREPROCESSOR);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Hex color inside a declaration, id selector outside ----
|
||||
if (c == '#') {
|
||||
int start = i;
|
||||
i++;
|
||||
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
|
||||
syn_fill_tokens(out, out_len, start, i,
|
||||
state.css_in_value ? SYN_NUMBER : SYN_TYPE);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Class selector ----
|
||||
if (c == '.' && !state.css_in_value && i + 1 < limit && syn_is_alpha(line[i + 1])) {
|
||||
int start = i;
|
||||
i++;
|
||||
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_TYPE);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < limit && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, limit, i, out, out_len, false);
|
||||
// Unit suffix (px, em, %, ...)
|
||||
int unit = i;
|
||||
while (i < limit && (syn_is_alpha(line[i]) || line[i] == '%')) i++;
|
||||
syn_fill_tokens(out, out_len, unit, i, SYN_NUMBER);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_alpha(c) || c == '-') {
|
||||
int start = i;
|
||||
while (i < limit && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
|
||||
SynToken tok;
|
||||
if (state.css_in_value) {
|
||||
tok = SYN_NORMAL;
|
||||
} else if (state.css_depth > 0) {
|
||||
tok = SYN_KEYWORD; // property name
|
||||
} else {
|
||||
tok = SYN_TYPE; // element selector
|
||||
}
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '{') {
|
||||
if (state.css_depth < 255) state.css_depth++;
|
||||
state.css_in_value = 0;
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '}') {
|
||||
if (state.css_depth > 0) state.css_depth--;
|
||||
state.css_in_value = 0;
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == ':') {
|
||||
// Inside a block this starts a value; outside it is a pseudo-class
|
||||
if (state.css_depth > 0) state.css_in_value = 1;
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') {
|
||||
state.css_in_value = 0;
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '>' || c == '~' || c == '+' || c == '*' || c == ',' || c == '=') {
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTML
|
||||
// ============================================================================
|
||||
//
|
||||
// Tag names are keywords, attribute names types, attribute values strings,
|
||||
// entities numbers, and <!DOCTYPE>/processing instructions preprocessor.
|
||||
// <script> and <style> bodies are handed to the JS and CSS highlighters.
|
||||
|
||||
inline void syn_highlight_line_html(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state) {
|
||||
int i = 0;
|
||||
|
||||
while (i < len) {
|
||||
// ---- Comment continuation ----
|
||||
if (state.html_ctx == SYN_HTML_COMMENT) {
|
||||
while (i < len) {
|
||||
if (syn_match_ci(line, len, i, "-->")) {
|
||||
syn_fill_tokens(out, out_len, i, i + 3, SYN_COMMENT);
|
||||
i += 3;
|
||||
state.html_ctx = SYN_HTML_TEXT;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Raw text body: <script> / <style> ----
|
||||
if (state.html_ctx == SYN_HTML_SCRIPT || state.html_ctx == SYN_HTML_STYLE) {
|
||||
const bool script = (state.html_ctx == SYN_HTML_SCRIPT);
|
||||
const char* endTag = script ? "</script" : "</style";
|
||||
int limit = i;
|
||||
while (limit < len && !syn_match_ci(line, len, limit, endTag)) limit++;
|
||||
|
||||
if (script)
|
||||
syn_highlight_js_range(line, limit, i, out, out_len, state);
|
||||
else
|
||||
syn_highlight_css_range(line, limit, i, out, out_len, state);
|
||||
i = limit;
|
||||
|
||||
if (i < len) {
|
||||
// Closing tag reached: back to ordinary tag parsing
|
||||
state.in_block_comment = false;
|
||||
state.css_in_value = 0;
|
||||
state.css_depth = 0;
|
||||
state.html_ctx = SYN_HTML_TAG;
|
||||
state.html_pending = SYN_HTML_TEXT;
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_OPERATOR);
|
||||
i += 2;
|
||||
int start = i;
|
||||
while (i < len && (syn_is_alnum(line[i]) || line[i] == '-')) i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_KEYWORD);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Inside a tag: attributes ----
|
||||
if (state.html_ctx == SYN_HTML_TAG) {
|
||||
// Attribute value continued from a previous line
|
||||
if (state.html_quote) {
|
||||
while (i < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == state.html_quote) {
|
||||
state.html_quote = 0;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = line[i];
|
||||
|
||||
if (c == '>') {
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
state.html_ctx = state.html_pending ? (SynHtmlCtx)state.html_pending
|
||||
: SYN_HTML_TEXT;
|
||||
state.html_pending = SYN_HTML_TEXT;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '>') {
|
||||
syn_fill_tokens(out, out_len, i, i + 2, SYN_OPERATOR);
|
||||
i += 2;
|
||||
state.html_ctx = SYN_HTML_TEXT;
|
||||
state.html_pending = SYN_HTML_TEXT;
|
||||
continue;
|
||||
}
|
||||
if (c == '=') {
|
||||
syn_set_token(out, out_len, i, SYN_OPERATOR);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
bool closed = false;
|
||||
while (i < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!closed) state.html_quote = quote;
|
||||
continue;
|
||||
}
|
||||
if (syn_is_alpha(c) || c == '-' || c == ':' || c == '@') {
|
||||
int start = i;
|
||||
i++;
|
||||
while (i < len && (syn_is_alnum(line[i]) || line[i] == '-' ||
|
||||
line[i] == ':' || line[i] == '.'))
|
||||
i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_TYPE);
|
||||
continue;
|
||||
}
|
||||
// Unquoted attribute value or stray punctuation
|
||||
if (c != ' ' && c != '\t') {
|
||||
int start = i;
|
||||
while (i < len && line[i] != ' ' && line[i] != '\t' &&
|
||||
line[i] != '>' && line[i] != '"' && line[i] != '\'')
|
||||
i++;
|
||||
if (i == start) i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Text content ----
|
||||
char c = line[i];
|
||||
|
||||
if (c == '<') {
|
||||
if (syn_match_ci(line, len, i, "<!--")) {
|
||||
state.html_ctx = SYN_HTML_COMMENT;
|
||||
syn_fill_tokens(out, out_len, i, i + 4, SYN_COMMENT);
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
if (i + 1 < len && (line[i + 1] == '!' || line[i + 1] == '?')) {
|
||||
// <!DOCTYPE ...> or a processing instruction
|
||||
int start = i;
|
||||
while (i < len && line[i] != '>') i++;
|
||||
if (i < len) i++;
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_PREPROCESSOR);
|
||||
continue;
|
||||
}
|
||||
|
||||
int op_end = i + 1;
|
||||
bool closing = false;
|
||||
if (op_end < len && line[op_end] == '/') {
|
||||
closing = true;
|
||||
op_end++;
|
||||
}
|
||||
syn_fill_tokens(out, out_len, i, op_end, SYN_OPERATOR);
|
||||
int start = op_end;
|
||||
int j = op_end;
|
||||
while (j < len && (syn_is_alnum(line[j]) || line[j] == '-' || line[j] == ':')) j++;
|
||||
if (j == start) {
|
||||
// A bare '<' in text, not a tag
|
||||
i = op_end;
|
||||
continue;
|
||||
}
|
||||
syn_fill_tokens(out, out_len, start, j, SYN_KEYWORD);
|
||||
state.html_ctx = SYN_HTML_TAG;
|
||||
state.html_pending = SYN_HTML_TEXT;
|
||||
if (!closing) {
|
||||
if (syn_streq(line + start, j - start, "script") ||
|
||||
syn_streq(line + start, j - start, "SCRIPT"))
|
||||
state.html_pending = SYN_HTML_SCRIPT;
|
||||
else if (syn_streq(line + start, j - start, "style") ||
|
||||
syn_streq(line + start, j - start, "STYLE"))
|
||||
state.html_pending = SYN_HTML_STYLE;
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Entity reference ----
|
||||
if (c == '&') {
|
||||
int j = i + 1;
|
||||
if (j < len && line[j] == '#') j++;
|
||||
int nameStart = j;
|
||||
while (j < len && syn_is_alnum(line[j])) j++;
|
||||
if (j > nameStart && j < len && line[j] == ';') {
|
||||
syn_fill_tokens(out, out_len, i, j + 1, SYN_NUMBER);
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user