476 lines
17 KiB
C++
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++;
|
|
}
|
|
}
|