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
+1288
-1376
No files matched your search
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 184
|
||||
#define MONTAUK_BUILD_NUMBER 185
|
||||
@@ -1,680 +0,0 @@
|
||||
/*
|
||||
* syntax_ansi.hpp
|
||||
* ANSI terminal syntax highlighting for C, C++ and Lua
|
||||
* Used by the MontaukOS CLI text editor
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// ============================================================================
|
||||
// Token types
|
||||
// ============================================================================
|
||||
|
||||
enum SynLanguage : uint8_t {
|
||||
SYN_LANG_NONE,
|
||||
SYN_LANG_C,
|
||||
SYN_LANG_CPP,
|
||||
SYN_LANG_LUA,
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 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';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyword / type classification
|
||||
// ============================================================================
|
||||
|
||||
inline SynToken syn_classify_c_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"auto", "break", "case", "const", "continue", "default", "do",
|
||||
"else", "enum", "extern", "for", "goto", "if", "inline",
|
||||
"register", "restrict", "return", "sizeof", "static", "struct",
|
||||
"switch", "typedef", "union", "volatile", "while",
|
||||
"NULL", "true", "false", "nullptr",
|
||||
};
|
||||
static const char* types[] = {
|
||||
"void", "char", "short", "int", "long", "float", "double",
|
||||
"signed", "unsigned", "bool", "_Bool",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t",
|
||||
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
|
||||
"FILE",
|
||||
};
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
inline SynToken syn_classify_lua_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"and", "break", "do", "else", "elseif", "end", "false",
|
||||
"for", "function", "goto", "if", "in", "local", "nil",
|
||||
"not", "or", "repeat", "return", "then", "true",
|
||||
"until", "while",
|
||||
};
|
||||
static const char* builtins[] = {
|
||||
"_ENV", "_G", "_VERSION",
|
||||
"assert", "collectgarbage", "coroutine", "debug", "dofile",
|
||||
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
|
||||
"math", "next", "os", "package", "pairs", "pcall",
|
||||
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
|
||||
"select", "setmetatable", "string", "table", "tonumber",
|
||||
"tostring", "type", "utf8", "warn", "xpcall",
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
|
||||
// C++ keywords (on top of the C set, which is checked first)
|
||||
static const char* keywords[] = {
|
||||
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
|
||||
"catch", "class", "compl", "concept", "consteval", "constexpr",
|
||||
"constinit", "const_cast", "co_await", "co_return", "co_yield",
|
||||
"decltype", "delete", "dynamic_cast", "explicit", "export",
|
||||
"final", "friend", "mutable", "namespace", "new", "noexcept",
|
||||
"not", "not_eq", "operator", "or", "or_eq", "override",
|
||||
"private", "protected", "public", "reinterpret_cast", "requires",
|
||||
"static_assert", "static_cast", "template", "this", "thread_local",
|
||||
"throw", "try", "typeid", "typename", "using", "virtual",
|
||||
"xor", "xor_eq",
|
||||
};
|
||||
// C++ library / builtin types
|
||||
static const char* types[] = {
|
||||
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
|
||||
"std", "string", "string_view", "vector", "array", "span",
|
||||
"map", "set", "unordered_map", "unordered_set", "deque", "list",
|
||||
"pair", "tuple", "optional", "variant", "function",
|
||||
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
|
||||
};
|
||||
|
||||
SynToken c = syn_classify_c_word(buf, len);
|
||||
if (c != SYN_NORMAL) return c;
|
||||
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
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);
|
||||
default: return SYN_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return state;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lua long bracket matching
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
|
||||
int& eqs, int& span) {
|
||||
if (i >= len || line[i] != '[') return false;
|
||||
int j = i + 1;
|
||||
while (j < len && line[j] == '=') j++;
|
||||
if (j < len && line[j] == '[') {
|
||||
eqs = j - i - 1;
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
|
||||
int eqs, int& span) {
|
||||
if (i >= len || line[i] != ']') return false;
|
||||
int j = i + 1;
|
||||
for (int k = 0; k < eqs; k++) {
|
||||
if (j >= len || line[j] != '=') return false;
|
||||
j++;
|
||||
}
|
||||
if (j < len && line[j] == ']') {
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// C highlighting
|
||||
// ============================================================================
|
||||
// C / C++ raw string helpers
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
|
||||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
|
||||
syn_streq(buf, len, "u8R");
|
||||
}
|
||||
|
||||
inline bool syn_is_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
|
||||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
|
||||
}
|
||||
|
||||
// Colors a raw-string body from `i` up to and including the closing )delim".
|
||||
// If the terminator is not on this line, leaves state.in_raw_string set so the
|
||||
// next line continues the literal.
|
||||
inline void syn_consume_raw_body(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
while (i < len) {
|
||||
if (line[i] == ')') {
|
||||
int j = i + 1;
|
||||
int k = 0;
|
||||
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
|
||||
j++;
|
||||
k++;
|
||||
}
|
||||
if (k == state.raw_delim_len && j < len && line[j] == '"') {
|
||||
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
|
||||
i = j + 1;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim_len = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Enters a raw string; `line[i]` must be the opening quote.
|
||||
inline void syn_consume_raw_string(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.raw_delim_len = 0;
|
||||
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
|
||||
state.raw_delim[state.raw_delim_len++] = line[i];
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
if (i < len && line[i] == '(') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.in_raw_string = true;
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
return;
|
||||
}
|
||||
// Malformed (or an over-long delimiter): treat the rest of the line as string
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
|
||||
i = len;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// C / C++ highlighting
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state, SynLanguage lang = SYN_LANG_C) {
|
||||
const bool cpp = (lang == SYN_LANG_CPP);
|
||||
int i = 0;
|
||||
|
||||
while (i < len) {
|
||||
// ---- Raw string continuation ----
|
||||
if (state.in_raw_string) {
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Block comment continuation ----
|
||||
if (state.in_block_comment) {
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, 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];
|
||||
|
||||
// ---- Line comment ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Block comment start ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Preprocessor directive ----
|
||||
if (c == '#') {
|
||||
// Check that only whitespace precedes the #
|
||||
bool is_pp = true;
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
|
||||
}
|
||||
if (is_pp) {
|
||||
while (i < len) {
|
||||
// Handle line-comment inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
// Handle block comment start inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
// Continue consuming as comment
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- String literal ----
|
||||
if (c == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Character literal ----
|
||||
if (c == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
syn_set_token(out, out_len, i + 1, SYN_CHAR);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Numbers ----
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, true, cpp);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Identifiers / keywords / types / literal prefixes ----
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_raw_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
syn_consume_raw_string(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
// The quote itself is handled on the next iteration
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '\'' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
|
||||
continue;
|
||||
}
|
||||
SynToken tok = syn_classify_word(lang, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Operators ----
|
||||
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;
|
||||
}
|
||||
|
||||
// ---- Everything else (whitespace, braces, parens, etc.) ----
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state) {
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
if (state.long_token != SYN_NORMAL) {
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
|
||||
i += span;
|
||||
state.long_token = SYN_NORMAL;
|
||||
state.long_bracket_eqs = -1;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, state.long_token);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
char c = line[i];
|
||||
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
|
||||
i += 2 + span;
|
||||
state.long_token = SYN_COMMENT;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
|
||||
i += span;
|
||||
state.long_token = SYN_STRING;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, 0);
|
||||
continue;
|
||||
}
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
|
||||
c == '^' || 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++;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Per-line highlighter
|
||||
// ============================================================================
|
||||
|
||||
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;
|
||||
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;
|
||||
return SYN_LANG_NONE;
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* lang_c.hpp
|
||||
* C and C++ highlighting
|
||||
* 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
|
||||
|
||||
// ============================================================================
|
||||
// Keyword / type classification
|
||||
// ============================================================================
|
||||
|
||||
inline SynToken syn_classify_c_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"auto", "break", "case", "const", "continue", "default", "do",
|
||||
"else", "enum", "extern", "for", "goto", "if", "inline",
|
||||
"register", "restrict", "return", "sizeof", "static", "struct",
|
||||
"switch", "typedef", "union", "volatile", "while",
|
||||
"NULL", "true", "false", "nullptr",
|
||||
};
|
||||
static const char* types[] = {
|
||||
"void", "char", "short", "int", "long", "float", "double",
|
||||
"signed", "unsigned", "bool", "_Bool",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t",
|
||||
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
|
||||
"FILE",
|
||||
};
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
|
||||
// C++ keywords (on top of the C set, which is checked first)
|
||||
static const char* keywords[] = {
|
||||
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
|
||||
"catch", "class", "compl", "concept", "consteval", "constexpr",
|
||||
"constinit", "const_cast", "co_await", "co_return", "co_yield",
|
||||
"decltype", "delete", "dynamic_cast", "explicit", "export",
|
||||
"final", "friend", "mutable", "namespace", "new", "noexcept",
|
||||
"not", "not_eq", "operator", "or", "or_eq", "override",
|
||||
"private", "protected", "public", "reinterpret_cast", "requires",
|
||||
"static_assert", "static_cast", "template", "this", "thread_local",
|
||||
"throw", "try", "typeid", "typename", "using", "virtual",
|
||||
"xor", "xor_eq",
|
||||
};
|
||||
// C++ library / builtin types
|
||||
static const char* types[] = {
|
||||
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
|
||||
"std", "string", "string_view", "vector", "array", "span",
|
||||
"map", "set", "unordered_map", "unordered_set", "deque", "list",
|
||||
"pair", "tuple", "optional", "variant", "function",
|
||||
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
|
||||
};
|
||||
|
||||
SynToken c = syn_classify_c_word(buf, len);
|
||||
if (c != SYN_NORMAL) return c;
|
||||
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// C highlighting
|
||||
// ============================================================================
|
||||
// C / C++ raw string helpers
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
|
||||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
|
||||
syn_streq(buf, len, "u8R");
|
||||
}
|
||||
|
||||
inline bool syn_is_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
|
||||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
|
||||
}
|
||||
|
||||
// Colors a raw-string body from `i` up to and including the closing )delim".
|
||||
// If the terminator is not on this line, leaves state.in_raw_string set so the
|
||||
// next line continues the literal.
|
||||
inline void syn_consume_raw_body(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
while (i < len) {
|
||||
if (line[i] == ')') {
|
||||
int j = i + 1;
|
||||
int k = 0;
|
||||
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
|
||||
j++;
|
||||
k++;
|
||||
}
|
||||
if (k == state.raw_delim_len && j < len && line[j] == '"') {
|
||||
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
|
||||
i = j + 1;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim_len = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Enters a raw string; `line[i]` must be the opening quote.
|
||||
inline void syn_consume_raw_string(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.raw_delim_len = 0;
|
||||
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
|
||||
state.raw_delim[state.raw_delim_len++] = line[i];
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
if (i < len && line[i] == '(') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.in_raw_string = true;
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
return;
|
||||
}
|
||||
// Malformed (or an over-long delimiter): treat the rest of the line as string
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
|
||||
i = len;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// C / C++ highlighting
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state, SynLanguage lang = SYN_LANG_C) {
|
||||
const bool cpp = (lang == SYN_LANG_CPP);
|
||||
int i = 0;
|
||||
|
||||
while (i < len) {
|
||||
// ---- Raw string continuation ----
|
||||
if (state.in_raw_string) {
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Block comment continuation ----
|
||||
if (state.in_block_comment) {
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, 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];
|
||||
|
||||
// ---- Line comment ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Block comment start ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Preprocessor directive ----
|
||||
if (c == '#') {
|
||||
// Check that only whitespace precedes the #
|
||||
bool is_pp = true;
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
|
||||
}
|
||||
if (is_pp) {
|
||||
while (i < len) {
|
||||
// Handle line-comment inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
// Handle block comment start inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
// Continue consuming as comment
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- String literal ----
|
||||
if (c == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Character literal ----
|
||||
if (c == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
syn_set_token(out, out_len, i + 1, SYN_CHAR);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Numbers ----
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, true, cpp);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Identifiers / keywords / types / literal prefixes ----
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_raw_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
syn_consume_raw_string(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
// The quote itself is handled on the next iteration
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '\'' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
|
||||
continue;
|
||||
}
|
||||
SynToken tok = syn_classify_word(lang, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Operators ----
|
||||
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;
|
||||
}
|
||||
|
||||
// ---- Everything else (whitespace, braces, parens, etc.) ----
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* lang_lua.hpp
|
||||
* Lua highlighting
|
||||
* 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
|
||||
|
||||
// ============================================================================
|
||||
// Keyword / builtin classification
|
||||
// ============================================================================
|
||||
|
||||
inline SynToken syn_classify_lua_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"and", "break", "do", "else", "elseif", "end", "false",
|
||||
"for", "function", "goto", "if", "in", "local", "nil",
|
||||
"not", "or", "repeat", "return", "then", "true",
|
||||
"until", "while",
|
||||
};
|
||||
static const char* builtins[] = {
|
||||
"_ENV", "_G", "_VERSION",
|
||||
"assert", "collectgarbage", "coroutine", "debug", "dofile",
|
||||
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
|
||||
"math", "next", "os", "package", "pairs", "pcall",
|
||||
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
|
||||
"select", "setmetatable", "string", "table", "tonumber",
|
||||
"tostring", "type", "utf8", "warn", "xpcall",
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lua long bracket matching
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
|
||||
int& eqs, int& span) {
|
||||
if (i >= len || line[i] != '[') return false;
|
||||
int j = i + 1;
|
||||
while (j < len && line[j] == '=') j++;
|
||||
if (j < len && line[j] == '[') {
|
||||
eqs = j - i - 1;
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
|
||||
int eqs, int& span) {
|
||||
if (i >= len || line[i] != ']') return false;
|
||||
int j = i + 1;
|
||||
for (int k = 0; k < eqs; k++) {
|
||||
if (j >= len || line[j] != '=') return false;
|
||||
j++;
|
||||
}
|
||||
if (j < len && line[j] == ']') {
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Per-line highlighter
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state) {
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
if (state.long_token != SYN_NORMAL) {
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
|
||||
i += span;
|
||||
state.long_token = SYN_NORMAL;
|
||||
state.long_bracket_eqs = -1;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, state.long_token);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
char c = line[i];
|
||||
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
|
||||
i += 2 + span;
|
||||
state.long_token = SYN_COMMENT;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
|
||||
i += span;
|
||||
state.long_token = SYN_STRING;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, 0);
|
||||
continue;
|
||||
}
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
|
||||
c == '^' || 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++;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+28
-13
@@ -12,7 +12,7 @@ using montauk::slen;
|
||||
using montauk::memcpy;
|
||||
using montauk::memmove;
|
||||
|
||||
#include <edit/syntax_ansi.hpp>
|
||||
#include <syntax/syntax.hpp>
|
||||
|
||||
// ---- Integer to string ----
|
||||
|
||||
@@ -66,6 +66,8 @@ static void dim_text() { esc("2m"); }
|
||||
// ---- Syntax highlighting state ----
|
||||
|
||||
static SynState synState = {};
|
||||
#define SYN_TOKEN_MAX 1024
|
||||
|
||||
static SynLanguage synLang = SYN_LANG_NONE;
|
||||
|
||||
// ANSI color for token type (must be called before each colored character)
|
||||
@@ -513,22 +515,27 @@ static void draw_line(int screenRow, int docLine) {
|
||||
if (visibleLen > maxChars) visibleLen = maxChars;
|
||||
}
|
||||
|
||||
if (synLang != SYN_LANG_NONE && visibleLen > 0) {
|
||||
// Tokenize visible portion for syntax highlighting
|
||||
static SynToken tokens[256];
|
||||
int tokCount = visibleLen;
|
||||
if (tokCount > 256) tokCount = 256;
|
||||
if (synLang != SYN_LANG_NONE) {
|
||||
// Tokenize the WHOLE line, not just the visible window: the
|
||||
// highlighter carries multi-line state (comments, HTML tags,
|
||||
// script bodies) that only stays correct if every character of
|
||||
// every line is seen in order.
|
||||
static SynToken tokens[SYN_TOKEN_MAX];
|
||||
int tokCount = ln->len;
|
||||
if (tokCount > SYN_TOKEN_MAX) tokCount = SYN_TOKEN_MAX;
|
||||
syn_fill_tokens(tokens, tokCount, 0, tokCount, SYN_NORMAL);
|
||||
syn_highlight_line(ln->data + startCol, visibleLen, tokens, tokCount, synLang, synState);
|
||||
syn_highlight_line(ln->data, ln->len, tokens, tokCount, synLang, synState);
|
||||
|
||||
// Output with ANSI colors
|
||||
// Output the visible window with ANSI colors
|
||||
SynToken prevTok = SYN_NORMAL;
|
||||
for (int c = 0; c < tokCount; c++) {
|
||||
if (c == 0 || tokens[c] != prevTok) {
|
||||
syn_ansi_color(tokens[c]);
|
||||
prevTok = tokens[c];
|
||||
for (int c = 0; c < visibleLen; c++) {
|
||||
int ti = startCol + c;
|
||||
SynToken tok = (ti < tokCount) ? tokens[ti] : SYN_NORMAL;
|
||||
if (c == 0 || tok != prevTok) {
|
||||
syn_ansi_color(tok);
|
||||
prevTok = tok;
|
||||
}
|
||||
putch(ln->data[startCol + c]);
|
||||
putch(ln->data[ti]);
|
||||
}
|
||||
syn_ansi_reset();
|
||||
} else {
|
||||
@@ -552,6 +559,14 @@ static void render() {
|
||||
draw_status_bar();
|
||||
|
||||
if (fullRedraw) {
|
||||
// Replay the lines above the viewport so multi-line syntax state
|
||||
// (block comments, HTML tags, <script> bodies) is correct at topLine
|
||||
synState = syn_make_state();
|
||||
if (synLang != SYN_LANG_NONE) {
|
||||
for (int i = 0; i < topLine && i < numLines; i++)
|
||||
syn_highlight_line(lines[i].data, lines[i].len, nullptr, 0,
|
||||
synLang, synState);
|
||||
}
|
||||
for (int i = 0; i < editorRows; i++) {
|
||||
draw_line(i, topLine + i);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* main.cpp
|
||||
* MontaukOS Text Editor - standalone Window Server app
|
||||
* Single-buffer text editor with line numbers, cursor, scrolling, file I/O,
|
||||
* syntax highlighting for C, C++ and Lua files
|
||||
* syntax highlighting for C, C++, Lua, HTML, CSS and JS files
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,50 +1,17 @@
|
||||
/*
|
||||
* syntax_highlight.hpp
|
||||
* C, C++ and Lua syntax highlighting for the MontaukOS text editor
|
||||
* Activated for .c/.h, C++ (.cpp/.hpp/...), and .lua files
|
||||
* Token colors for the MontaukOS text editor
|
||||
* The highlighters themselves live in <syntax/syntax.hpp>
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/gui.hpp>
|
||||
#include <syntax/syntax.hpp>
|
||||
|
||||
using namespace gui;
|
||||
|
||||
enum SynLanguage : uint8_t {
|
||||
SYN_LANG_NONE,
|
||||
SYN_LANG_C,
|
||||
SYN_LANG_CPP,
|
||||
SYN_LANG_LUA,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Token types
|
||||
// ============================================================================
|
||||
|
||||
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
|
||||
|
||||
struct SynState {
|
||||
bool in_block_comment;
|
||||
SynToken long_token;
|
||||
int long_bracket_eqs;
|
||||
bool in_raw_string;
|
||||
char raw_delim[SYN_RAW_DELIM_MAX];
|
||||
int raw_delim_len;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Colors for each token type
|
||||
// ============================================================================
|
||||
@@ -62,648 +29,3 @@ inline Color syn_color(SynToken tok) {
|
||||
default: return Color::from_rgb(0x1E, 0x1E, 0x1E); // near-black
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyword / type lookup
|
||||
// ============================================================================
|
||||
|
||||
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 SynToken syn_classify_c_word(const char* buf, int len) {
|
||||
// C keywords
|
||||
static const char* keywords[] = {
|
||||
"auto", "break", "case", "const", "continue", "default", "do",
|
||||
"else", "enum", "extern", "for", "goto", "if", "inline",
|
||||
"register", "restrict", "return", "sizeof", "static", "struct",
|
||||
"switch", "typedef", "union", "volatile", "while",
|
||||
"NULL", "true", "false", "nullptr",
|
||||
};
|
||||
// C types
|
||||
static const char* types[] = {
|
||||
"void", "char", "short", "int", "long", "float", "double",
|
||||
"signed", "unsigned", "bool", "_Bool",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t",
|
||||
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t",
|
||||
"FILE",
|
||||
};
|
||||
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
inline SynToken syn_classify_lua_word(const char* buf, int len) {
|
||||
static const char* keywords[] = {
|
||||
"and", "break", "do", "else", "elseif", "end", "false",
|
||||
"for", "function", "goto", "if", "in", "local", "nil",
|
||||
"not", "or", "repeat", "return", "then", "true",
|
||||
"until", "while",
|
||||
};
|
||||
static const char* builtins[] = {
|
||||
"_ENV", "_G", "_VERSION",
|
||||
"assert", "collectgarbage", "coroutine", "debug", "dofile",
|
||||
"error", "getmetatable", "io", "ipairs", "load", "loadfile",
|
||||
"math", "next", "os", "package", "pairs", "pcall",
|
||||
"print", "rawequal", "rawget", "rawlen", "rawset", "require",
|
||||
"select", "setmetatable", "string", "table", "tonumber",
|
||||
"tostring", "type", "utf8", "warn", "xpcall",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
inline SynToken syn_classify_cpp_word(const char* buf, int len) {
|
||||
// C++ keywords (on top of the C set, which is checked first)
|
||||
static const char* keywords[] = {
|
||||
"alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor",
|
||||
"catch", "class", "compl", "concept", "consteval", "constexpr",
|
||||
"constinit", "const_cast", "co_await", "co_return", "co_yield",
|
||||
"decltype", "delete", "dynamic_cast", "explicit", "export",
|
||||
"final", "friend", "mutable", "namespace", "new", "noexcept",
|
||||
"not", "not_eq", "operator", "or", "or_eq", "override",
|
||||
"private", "protected", "public", "reinterpret_cast", "requires",
|
||||
"static_assert", "static_cast", "template", "this", "thread_local",
|
||||
"throw", "try", "typeid", "typename", "using", "virtual",
|
||||
"xor", "xor_eq",
|
||||
};
|
||||
// C++ library / builtin types
|
||||
static const char* types[] = {
|
||||
"char8_t", "char16_t", "char32_t", "wchar_t", "nullptr_t",
|
||||
"std", "string", "string_view", "vector", "array", "span",
|
||||
"map", "set", "unordered_map", "unordered_set", "deque", "list",
|
||||
"pair", "tuple", "optional", "variant", "function",
|
||||
"unique_ptr", "shared_ptr", "weak_ptr", "initializer_list",
|
||||
};
|
||||
|
||||
SynToken c = syn_classify_c_word(buf, len);
|
||||
if (c != SYN_NORMAL) return c;
|
||||
|
||||
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(types) / sizeof(types[0])); i++) {
|
||||
if (syn_streq(buf, len, types[i])) return SYN_TYPE;
|
||||
}
|
||||
return SYN_NORMAL;
|
||||
}
|
||||
|
||||
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);
|
||||
default: return SYN_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
inline SynState syn_make_state() {
|
||||
SynState state = {};
|
||||
state.in_block_comment = false;
|
||||
state.long_token = SYN_NORMAL;
|
||||
state.long_bracket_eqs = -1;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim_len = 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i,
|
||||
int& eqs, int& span) {
|
||||
if (i >= len || line[i] != '[') return false;
|
||||
int j = i + 1;
|
||||
while (j < len && line[j] == '=') j++;
|
||||
if (j < len && line[j] == '[') {
|
||||
eqs = j - i - 1;
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i,
|
||||
int eqs, int& span) {
|
||||
if (i >= len || line[i] != ']') return false;
|
||||
int j = i + 1;
|
||||
for (int k = 0; k < eqs; k++) {
|
||||
if (j >= len || line[j] != '=') return false;
|
||||
j++;
|
||||
}
|
||||
if (j < len && line[j] == ']') {
|
||||
span = j - i + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Per-line highlighter
|
||||
// ============================================================================
|
||||
//
|
||||
// Fills `out[]` with SynToken values for each character in the line.
|
||||
// `state` carries the multi-line syntax state across lines:
|
||||
// - pass in the state from the previous line
|
||||
// - on return, updated for the next line
|
||||
//
|
||||
// `line` points to the first char of the line, `len` is its length
|
||||
// (excluding the newline). `out` can be null if only state tracking is needed.
|
||||
|
||||
// ============================================================================
|
||||
// C / C++ raw string helpers
|
||||
// ============================================================================
|
||||
|
||||
inline bool syn_is_raw_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "R") || syn_streq(buf, len, "LR") ||
|
||||
syn_streq(buf, len, "uR") || syn_streq(buf, len, "UR") ||
|
||||
syn_streq(buf, len, "u8R");
|
||||
}
|
||||
|
||||
inline bool syn_is_string_prefix(const char* buf, int len) {
|
||||
return syn_streq(buf, len, "L") || syn_streq(buf, len, "u") ||
|
||||
syn_streq(buf, len, "U") || syn_streq(buf, len, "u8");
|
||||
}
|
||||
|
||||
// Colors a raw-string body from `i` up to and including the closing )delim".
|
||||
// If the terminator is not on this line, leaves state.in_raw_string set so the
|
||||
// next line continues the literal.
|
||||
inline void syn_consume_raw_body(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
while (i < len) {
|
||||
if (line[i] == ')') {
|
||||
int j = i + 1;
|
||||
int k = 0;
|
||||
while (k < state.raw_delim_len && j < len && line[j] == state.raw_delim[k]) {
|
||||
j++;
|
||||
k++;
|
||||
}
|
||||
if (k == state.raw_delim_len && j < len && line[j] == '"') {
|
||||
syn_fill_tokens(out, out_len, i, j + 1, SYN_STRING);
|
||||
i = j + 1;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim_len = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Enters a raw string; `line[i]` must be the opening quote.
|
||||
inline void syn_consume_raw_string(const char* line, int len, int& i,
|
||||
SynToken* out, int out_len, SynState& state) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.raw_delim_len = 0;
|
||||
while (i < len && line[i] != '(' && state.raw_delim_len < SYN_RAW_DELIM_MAX) {
|
||||
state.raw_delim[state.raw_delim_len++] = line[i];
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
if (i < len && line[i] == '(') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
state.in_raw_string = true;
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
return;
|
||||
}
|
||||
// Malformed (or an over-long delimiter): treat the rest of the line as string
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_STRING);
|
||||
i = len;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// C / C++ highlighting
|
||||
// ============================================================================
|
||||
|
||||
inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state, SynLanguage lang = SYN_LANG_C) {
|
||||
const bool cpp = (lang == SYN_LANG_CPP);
|
||||
int i = 0;
|
||||
|
||||
while (i < len) {
|
||||
// ---- Raw string continuation ----
|
||||
if (state.in_raw_string) {
|
||||
syn_consume_raw_body(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Block comment continuation ----
|
||||
if (state.in_block_comment) {
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, 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];
|
||||
|
||||
// ---- Line comment ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Block comment start ----
|
||||
if (c == '/' && i + 1 < len && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Preprocessor directive ----
|
||||
if (c == '#') {
|
||||
// Check that only whitespace precedes the #
|
||||
bool is_pp = true;
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; }
|
||||
}
|
||||
if (is_pp) {
|
||||
while (i < len) {
|
||||
// Handle line-comment inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') {
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
// Handle block comment start inside preprocessor
|
||||
if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') {
|
||||
state.in_block_comment = true;
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
// Continue consuming as comment
|
||||
while (i < len) {
|
||||
if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') {
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
syn_set_token(out, out_len, i + 1, SYN_COMMENT);
|
||||
i += 2;
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_COMMENT);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_PREPROCESSOR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- String literal ----
|
||||
if (c == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '"') {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Character literal ----
|
||||
if (c == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
syn_set_token(out, out_len, i + 1, SYN_CHAR);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (line[i] == '\'') {
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_CHAR);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Numbers ----
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, true, cpp);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Identifiers / keywords / types / literal prefixes ----
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_raw_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
syn_consume_raw_string(line, len, i, out, out_len, state);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '"' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
// The quote itself is handled on the next iteration
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_STRING);
|
||||
continue;
|
||||
}
|
||||
if (cpp && i < len && line[i] == '\'' &&
|
||||
syn_is_string_prefix(line + start, i - start)) {
|
||||
syn_fill_tokens(out, out_len, start, i, SYN_CHAR);
|
||||
continue;
|
||||
}
|
||||
SynToken tok = syn_classify_word(lang, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Operators ----
|
||||
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;
|
||||
}
|
||||
|
||||
// ---- Everything else (whitespace, braces, parens, etc.) ----
|
||||
syn_set_token(out, out_len, i, SYN_NORMAL);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len,
|
||||
SynState& state) {
|
||||
int i = 0;
|
||||
|
||||
while (i < len) {
|
||||
if (state.long_token != SYN_NORMAL) {
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, state.long_token);
|
||||
i += span;
|
||||
state.long_token = SYN_NORMAL;
|
||||
state.long_bracket_eqs = -1;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, state.long_token);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = line[i];
|
||||
|
||||
if (c == '-' && i + 1 < len && line[i + 1] == '-') {
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT);
|
||||
i += 2 + span;
|
||||
state.long_token = SYN_COMMENT;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
syn_fill_tokens(out, out_len, i, len, SYN_COMMENT);
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
i++;
|
||||
while (i < len) {
|
||||
if (line[i] == '\\' && i + 1 < len) {
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
syn_set_token(out, out_len, i + 1, SYN_STRING);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
syn_set_token(out, out_len, i, SYN_STRING);
|
||||
if (line[i] == quote) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int eqs = 0;
|
||||
int span = 0;
|
||||
if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) {
|
||||
syn_fill_tokens(out, out_len, i, i + span, SYN_STRING);
|
||||
i += span;
|
||||
state.long_token = SYN_STRING;
|
||||
state.long_bracket_eqs = eqs;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) {
|
||||
syn_consume_number(line, len, i, out, out_len, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syn_is_alpha(c)) {
|
||||
int start = i;
|
||||
while (i < len && syn_is_alnum(line[i])) i++;
|
||||
SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start);
|
||||
syn_fill_tokens(out, out_len, start, i, tok);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
|
||||
c == '^' || 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++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Extension check
|
||||
// ============================================================================
|
||||
|
||||
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;
|
||||
return SYN_LANG_NONE;
|
||||
}
|
||||
Reference in new issue
Block a user