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