Files

476 lines
17 KiB
C++

/*
* 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++;
}
}