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,299 @@
|
||||
/*
|
||||
* syntax.hpp
|
||||
* Syntax highlighting core: token types, state, shared scanners
|
||||
* Shared by the MontaukOS text editor (GUI) and edit.elf (CLI)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
// The highlighter turns one line of text into a SynToken per character while
|
||||
// carrying multi-line state (block comments, HTML tag context, ...) forward
|
||||
// in a SynState. Frontends map SynToken to their own colors: the GUI editor
|
||||
// to a Color, edit.elf to an ANSI escape.
|
||||
//
|
||||
// Callers include THIS header only; the per-language lang_*.hpp files below
|
||||
// are not standalone (they rely on the declarations above their include).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// ============================================================================
|
||||
// Token types
|
||||
// ============================================================================
|
||||
|
||||
enum SynLanguage : uint8_t {
|
||||
SYN_LANG_NONE,
|
||||
SYN_LANG_C,
|
||||
SYN_LANG_CPP,
|
||||
SYN_LANG_LUA,
|
||||
SYN_LANG_HTML,
|
||||
SYN_LANG_CSS,
|
||||
SYN_LANG_JS,
|
||||
};
|
||||
|
||||
enum SynToken : uint8_t {
|
||||
SYN_NORMAL,
|
||||
SYN_KEYWORD,
|
||||
SYN_TYPE,
|
||||
SYN_PREPROCESSOR,
|
||||
SYN_STRING,
|
||||
SYN_CHAR,
|
||||
SYN_COMMENT,
|
||||
SYN_NUMBER,
|
||||
SYN_OPERATOR,
|
||||
};
|
||||
|
||||
#define SYN_RAW_DELIM_MAX 16
|
||||
|
||||
// Where an HTML line starts: plain text, inside a tag, inside a comment, or
|
||||
// inside a <script>/<style> raw-text body.
|
||||
enum SynHtmlCtx : uint8_t {
|
||||
SYN_HTML_TEXT,
|
||||
SYN_HTML_TAG,
|
||||
SYN_HTML_COMMENT,
|
||||
SYN_HTML_SCRIPT,
|
||||
SYN_HTML_STYLE,
|
||||
};
|
||||
|
||||
struct SynState {
|
||||
int in_block_comment; // 0 or 1 (bool stored as int for freestanding)
|
||||
SynToken long_token;
|
||||
int long_bracket_eqs;
|
||||
bool in_raw_string;
|
||||
char raw_delim[SYN_RAW_DELIM_MAX];
|
||||
int raw_delim_len;
|
||||
uint8_t html_ctx; // SynHtmlCtx
|
||||
uint8_t html_pending; // context to enter once the open tag closes
|
||||
char html_quote; // attribute value quote left open on this line
|
||||
uint8_t css_depth; // brace nesting, to tell selectors from properties
|
||||
uint8_t css_in_value; // between ':' and ';' inside a declaration
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_is_alpha(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
|
||||
}
|
||||
|
||||
inline bool syn_is_alnum(char c) {
|
||||
return syn_is_alpha(c) || (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
inline bool syn_is_digit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
inline bool syn_is_hex(char c) {
|
||||
return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
|
||||
inline void syn_set_token(SynToken* out, int out_len, int idx, SynToken tok) {
|
||||
if (out && idx >= 0 && idx < out_len)
|
||||
out[idx] = tok;
|
||||
}
|
||||
|
||||
inline void syn_fill_tokens(SynToken* out, int out_len, int start, int end, SynToken tok) {
|
||||
if (!out) return;
|
||||
if (start < 0) start = 0;
|
||||
if (end > out_len) end = out_len;
|
||||
for (int i = start; i < end; i++)
|
||||
out[i] = tok;
|
||||
}
|
||||
|
||||
inline bool syn_streq(const char* buf, int len, const char* kw) {
|
||||
int i = 0;
|
||||
while (i < len && kw[i]) {
|
||||
if (buf[i] != kw[i]) return false;
|
||||
i++;
|
||||
}
|
||||
return i == len && kw[i] == '\0';
|
||||
}
|
||||
|
||||
inline char syn_lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
|
||||
}
|
||||
|
||||
// Case-insensitive literal match at line[i]
|
||||
inline bool syn_match_ci(const char* line, int len, int i, const char* s) {
|
||||
int k = 0;
|
||||
while (s[k]) {
|
||||
if (i + k >= len) return false;
|
||||
if (syn_lower(line[i + k]) != syn_lower(s[k])) return false;
|
||||
k++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline SynState syn_make_state() {
|
||||
SynState state = {};
|
||||
state.in_block_comment = 0;
|
||||
state.long_token = SYN_NORMAL;
|
||||
state.long_bracket_eqs = -1;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim_len = 0;
|
||||
state.html_ctx = SYN_HTML_TEXT;
|
||||
state.html_pending = SYN_HTML_TEXT;
|
||||
state.html_quote = 0;
|
||||
state.css_depth = 0;
|
||||
state.css_in_value = 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Number consumption
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_scan_digits(const char* line, int len, int& i, bool sep, bool hex) {
|
||||
while (i < len) {
|
||||
if (hex ? syn_is_hex(line[i]) : syn_is_digit(line[i])) { i++; continue; }
|
||||
// C++14 digit separator: a quote wedged between two digits
|
||||
if (sep && line[i] == '\'' && i + 1 < len &&
|
||||
(hex ? syn_is_hex(line[i + 1]) : syn_is_digit(line[i + 1]))) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline void syn_consume_number(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, bool c_style_suffixes,
|
||||
bool digit_sep = false) {
|
||||
int start = i;
|
||||
|
||||
if (line[i] == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) {
|
||||
i += 2;
|
||||
syn_scan_digits(line, len, i, digit_sep, true);
|
||||
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
|
||||
i++;
|
||||
syn_scan_digits(line, len, i, digit_sep, true);
|
||||
}
|
||||
if (i < len && (line[i] == 'p' || line[i] == 'P')) {
|
||||
int exp = i + 1;
|
||||
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
|
||||
if (exp < len && syn_is_digit(line[exp])) {
|
||||
i = exp;
|
||||
syn_scan_digits(line, len, i, digit_sep, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (line[i] == '.') i++;
|
||||
syn_scan_digits(line, len, i, digit_sep, false);
|
||||
if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) {
|
||||
i++;
|
||||
syn_scan_digits(line, len, i, digit_sep, false);
|
||||
}
|
||||
if (i < len && (line[i] == 'e' || line[i] == 'E')) {
|
||||
int exp = i + 1;
|
||||
if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++;
|
||||
if (exp < len && syn_is_digit(line[exp])) {
|
||||
i = exp;
|
||||
syn_scan_digits(line, len, i, digit_sep, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (c_style_suffixes) {
|
||||
while (i < len && (line[i] == 'u' || line[i] == 'U' ||
|
||||
line[i] == 'l' || line[i] == 'L' ||
|
||||
line[i] == 'f' || line[i] == 'F' ||
|
||||
line[i] == 'z' || line[i] == 'Z'))
|
||||
i++;
|
||||
}
|
||||
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_NUMBER);
|
||||
}
|
||||
|
||||
// Defined below, once every language's classifier is in scope
|
||||
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len);
|
||||
|
||||
#include <syntax/lang_c.hpp>
|
||||
#include <syntax/lang_lua.hpp>
|
||||
#include <syntax/lang_html.hpp>
|
||||
|
||||
// ============================================================================
|
||||
// Word classification
|
||||
// ============================================================================
|
||||
|
||||
inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len) {
|
||||
switch (lang) {
|
||||
case SYN_LANG_C: return syn_classify_c_word(buf, len);
|
||||
case SYN_LANG_CPP: return syn_classify_cpp_word(buf, len);
|
||||
case SYN_LANG_LUA: return syn_classify_lua_word(buf, len);
|
||||
case SYN_LANG_JS: return syn_classify_js_word(buf, len);
|
||||
default: return SYN_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
inline void syn_highlight_line(const char* line, int len, SynToken* out, int out_len,
|
||||
SynLanguage lang, SynState& state) {
|
||||
if (!line || len <= 0) return;
|
||||
switch (lang) {
|
||||
case SYN_LANG_C:
|
||||
case SYN_LANG_CPP:
|
||||
syn_highlight_line_c(line, len, out, out_len, state, lang);
|
||||
break;
|
||||
case SYN_LANG_LUA:
|
||||
syn_highlight_line_lua(line, len, out, out_len, state);
|
||||
break;
|
||||
case SYN_LANG_HTML:
|
||||
syn_highlight_line_html(line, len, out, out_len, state);
|
||||
break;
|
||||
case SYN_LANG_CSS: {
|
||||
int i = 0;
|
||||
syn_highlight_css_range(line, len, i, out, out_len, state);
|
||||
break;
|
||||
}
|
||||
case SYN_LANG_JS: {
|
||||
int i = 0;
|
||||
syn_highlight_js_range(line, len, i, out, out_len, state);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Language detection
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_path_ends_with(const char* path, const char* suffix) {
|
||||
if (!path || !suffix) return false;
|
||||
int path_len = 0;
|
||||
while (path[path_len]) path_len++;
|
||||
int suffix_len = 0;
|
||||
while (suffix[suffix_len]) suffix_len++;
|
||||
if (path_len < suffix_len) return false;
|
||||
for (int i = 0; i < suffix_len; i++) {
|
||||
if (path[path_len - suffix_len + i] != suffix[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline SynLanguage syn_detect_language(const char* filepath) {
|
||||
if (!filepath || filepath[0] == '\0') return SYN_LANG_NONE;
|
||||
if (syn_path_ends_with(filepath, ".cpp") || syn_path_ends_with(filepath, ".cc") ||
|
||||
syn_path_ends_with(filepath, ".cxx") || syn_path_ends_with(filepath, ".c++") ||
|
||||
syn_path_ends_with(filepath, ".hpp") || syn_path_ends_with(filepath, ".hh") ||
|
||||
syn_path_ends_with(filepath, ".hxx") || syn_path_ends_with(filepath, ".h++") ||
|
||||
syn_path_ends_with(filepath, ".ipp") || syn_path_ends_with(filepath, ".tpp") ||
|
||||
syn_path_ends_with(filepath, ".inl"))
|
||||
return SYN_LANG_CPP;
|
||||
if (syn_path_ends_with(filepath, ".c") || syn_path_ends_with(filepath, ".h"))
|
||||
return SYN_LANG_C;
|
||||
if (syn_path_ends_with(filepath, ".lua"))
|
||||
return SYN_LANG_LUA;
|
||||
if (syn_path_ends_with(filepath, ".html") || syn_path_ends_with(filepath, ".htm") ||
|
||||
syn_path_ends_with(filepath, ".xhtml"))
|
||||
return SYN_LANG_HTML;
|
||||
if (syn_path_ends_with(filepath, ".css"))
|
||||
return SYN_LANG_CSS;
|
||||
if (syn_path_ends_with(filepath, ".js") || syn_path_ends_with(filepath, ".mjs"))
|
||||
return SYN_LANG_JS;
|
||||
return SYN_LANG_NONE;
|
||||
}
|
||||
Reference in new issue
Block a user