The WebKit HTML preview previously rendered fenced code blocks monochrome. Wire the existing tokenizer into the HTML renderer so code blocks are highlighted like GitHub, and extend language coverage. - markdown.c: accumulate each code block's text, tokenize it on block leave, and emit <span class="pl-*"> per token with HTML-escaped gaps and token text; unknown languages fall back to plain escaped text. - Add GitHub Primer syntax colors (light/dark) for keywords, constants, strings, and comments. - code_highlight.c: tokenize comments (//, /* */, #), which were previously skipped and left uncolored. - Add languages: javascript, kotlin, and bash (aliases js, kt, sh, shell). Bash uses a dedicated scanner whose '#' comments are word-position aware so $#, x=$#, and a.com#frag are not comments. - Create the code_comment tag so the source editor colors comments too. - README: document the highlighted languages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1084 lines
29 KiB
C
1084 lines
29 KiB
C
#include "code_highlight.h"
|
|
#include <string.h>
|
|
|
|
#define MARKYD_SCAN_FLAG_BLOCK_COMMENT (1u << 0)
|
|
#define MARKYD_SCAN_FLAG_JAVA_TEXT_BLOCK (1u << 1)
|
|
#define MARKYD_SCAN_FLAG_PY_TRIPLE_SINGLE (1u << 2)
|
|
#define MARKYD_SCAN_FLAG_PY_TRIPLE_DOUBLE (1u << 3)
|
|
|
|
static const gchar *const c_kw_group_a[] = {
|
|
"break", "case", "continue", "default", "do", "else",
|
|
"for", "goto", "if", "return", "switch", "while",
|
|
};
|
|
|
|
static const gchar *const c_kw_group_b[] = {
|
|
"auto", "const", "extern", "inline", "register",
|
|
"restrict","static", "typedef", "volatile", "_Alignas",
|
|
"_Atomic", "_Noreturn", "_Static_assert", "_Thread_local",
|
|
};
|
|
|
|
static const gchar *const c_kw_group_c[] = {
|
|
"char", "double", "enum", "float", "int", "long",
|
|
"short", "signed", "sizeof", "struct", "union", "unsigned",
|
|
"void", "_Alignof", "_Bool", "_Complex", "_Generic", "_Imaginary",
|
|
};
|
|
|
|
static const MarkydKeywordGroup c_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, c_kw_group_a, G_N_ELEMENTS(c_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, c_kw_group_b, G_N_ELEMENTS(c_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, c_kw_group_c, G_N_ELEMENTS(c_kw_group_c)},
|
|
};
|
|
|
|
static const gchar *const java_kw_group_a[] = {
|
|
"assert", "break", "case", "catch", "continue", "default",
|
|
"do", "else", "finally","for", "if", "return",
|
|
"switch", "throw", "throws", "try", "while",
|
|
};
|
|
|
|
static const gchar *const java_kw_group_b[] = {
|
|
"abstract", "class", "const", "enum", "extends",
|
|
"final", "goto", "implements", "import", "instanceof",
|
|
"interface","native", "new", "package", "private",
|
|
"protected","public", "static", "strictfp", "super",
|
|
"synchronized", "this", "transient", "volatile", "_",
|
|
};
|
|
|
|
static const gchar *const java_kw_group_c[] = {
|
|
"boolean", "byte", "char", "double", "float",
|
|
"int", "long", "short", "void",
|
|
};
|
|
|
|
static const MarkydKeywordGroup java_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, java_kw_group_a, G_N_ELEMENTS(java_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, java_kw_group_b, G_N_ELEMENTS(java_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, java_kw_group_c, G_N_ELEMENTS(java_kw_group_c)},
|
|
};
|
|
|
|
static const gchar *const py_kw_group_a[] = {
|
|
"and", "assert", "async", "await", "break", "case", "continue",
|
|
"elif", "else", "except","finally","for", "if", "in",
|
|
"is", "match", "not", "or", "raise", "return","try",
|
|
"while", "with", "yield",
|
|
};
|
|
|
|
static const gchar *const py_kw_group_b[] = {
|
|
"as", "class", "def", "del", "from", "global",
|
|
"import", "lambda", "nonlocal", "pass", "_",
|
|
};
|
|
|
|
static const gchar *const py_kw_group_c[] = {
|
|
"False", "None", "True",
|
|
};
|
|
|
|
static const MarkydKeywordGroup py_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, py_kw_group_a, G_N_ELEMENTS(py_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, py_kw_group_b, G_N_ELEMENTS(py_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, py_kw_group_c, G_N_ELEMENTS(py_kw_group_c)},
|
|
};
|
|
|
|
static const gchar *const js_kw_group_a[] = {
|
|
"await", "break", "case", "catch", "continue", "default",
|
|
"do", "else", "finally", "for", "if", "in",
|
|
"instanceof", "of", "return", "switch", "throw", "try",
|
|
"while", "with", "yield",
|
|
};
|
|
|
|
static const gchar *const js_kw_group_b[] = {
|
|
"async", "class", "const", "debugger", "delete", "export",
|
|
"extends","function","import", "let", "new", "static",
|
|
"super", "this", "typeof", "var", "void",
|
|
};
|
|
|
|
static const gchar *const js_kw_group_c[] = {
|
|
"false", "Infinity", "NaN", "null", "true", "undefined",
|
|
};
|
|
|
|
static const MarkydKeywordGroup js_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, js_kw_group_a, G_N_ELEMENTS(js_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, js_kw_group_b, G_N_ELEMENTS(js_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, js_kw_group_c, G_N_ELEMENTS(js_kw_group_c)},
|
|
};
|
|
|
|
static const gchar *const kotlin_kw_group_a[] = {
|
|
"break", "catch", "continue", "do", "else", "finally",
|
|
"for", "if", "in", "is", "return","throw",
|
|
"try", "when", "while",
|
|
};
|
|
|
|
static const gchar *const kotlin_kw_group_b[] = {
|
|
"abstract", "actual", "annotation", "as", "by",
|
|
"class", "companion", "const", "constructor","crossinline",
|
|
"data", "enum", "expect", "external", "final",
|
|
"fun", "import", "infix", "init", "inline",
|
|
"inner", "interface", "internal", "lateinit", "noinline",
|
|
"object", "open", "operator", "out", "override",
|
|
"package", "private", "protected", "public", "reified",
|
|
"sealed", "super", "suspend", "tailrec", "this",
|
|
"typealias", "val", "var", "vararg", "where",
|
|
};
|
|
|
|
static const gchar *const kotlin_kw_group_c[] = {
|
|
"false", "null", "true", "Any", "Boolean", "Byte", "Char",
|
|
"Double","Float","Int", "Long", "Nothing", "Short", "String",
|
|
"Unit",
|
|
};
|
|
|
|
static const MarkydKeywordGroup kotlin_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, kotlin_kw_group_a, G_N_ELEMENTS(kotlin_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, kotlin_kw_group_b, G_N_ELEMENTS(kotlin_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, kotlin_kw_group_c, G_N_ELEMENTS(kotlin_kw_group_c)},
|
|
};
|
|
|
|
static const gchar *const bash_kw_group_a[] = {
|
|
"case", "do", "done", "elif", "else", "esac", "fi",
|
|
"for", "function","if", "in", "select","then", "time",
|
|
"until","while",
|
|
};
|
|
|
|
static const gchar *const bash_kw_group_b[] = {
|
|
"alias", "bg", "break", "cd", "command", "continue",
|
|
"declare", "echo", "eval", "exec", "exit", "export",
|
|
"getopts", "hash", "jobs", "kill", "let", "local",
|
|
"printf", "pwd", "read", "readonly","return", "set",
|
|
"shift", "source", "test", "trap", "type", "typeset",
|
|
"ulimit", "umask", "unalias", "unset", "wait",
|
|
};
|
|
|
|
static const gchar *const bash_kw_group_c[] = {
|
|
"false", "true",
|
|
};
|
|
|
|
static const MarkydKeywordGroup bash_groups[] = {
|
|
{MARKYD_TAG_CODE_KW_A, bash_kw_group_a, G_N_ELEMENTS(bash_kw_group_a)},
|
|
{MARKYD_TAG_CODE_KW_B, bash_kw_group_b, G_N_ELEMENTS(bash_kw_group_b)},
|
|
{MARKYD_TAG_CODE_KW_C, bash_kw_group_c, G_N_ELEMENTS(bash_kw_group_c)},
|
|
};
|
|
|
|
static gboolean is_ascii_identifier_char(gchar c) {
|
|
return (c == '_') || g_ascii_isalnum((guchar)c);
|
|
}
|
|
|
|
static gboolean starts_with_triple_quote(const gchar *p) {
|
|
return p && p[0] == '"' && p[1] == '"' && p[2] == '"';
|
|
}
|
|
|
|
static gboolean is_identifier_start(gunichar c) {
|
|
return (c == '_') || (c < 128 && g_ascii_isalpha((gchar)c));
|
|
}
|
|
|
|
static gboolean is_identifier_char(gunichar c) {
|
|
return (c == '_') || (c < 128 && g_ascii_isalnum((gchar)c));
|
|
}
|
|
|
|
static void advance_utf8_char(const gchar **p, gint *char_index) {
|
|
if (!p || !*p || !**p) {
|
|
return;
|
|
}
|
|
*p = g_utf8_next_char(*p);
|
|
(*char_index)++;
|
|
}
|
|
|
|
static void skip_quoted_literal(const gchar **p, gint *char_index,
|
|
gchar quote_char) {
|
|
if (!p || !*p || !**p) {
|
|
return;
|
|
}
|
|
|
|
/* Consume opening quote. */
|
|
(*p)++;
|
|
(*char_index)++;
|
|
|
|
while (**p) {
|
|
if ((*p)[0] == '\\') {
|
|
(*p)++;
|
|
(*char_index)++;
|
|
if (**p) {
|
|
*p = g_utf8_next_char(*p);
|
|
(*char_index)++;
|
|
}
|
|
continue;
|
|
}
|
|
if ((*p)[0] == quote_char) {
|
|
(*p)++;
|
|
(*char_index)++;
|
|
break;
|
|
}
|
|
advance_utf8_char(p, char_index);
|
|
}
|
|
}
|
|
|
|
static const gchar *lookup_keyword_tag(const MarkydLanguageHighlight *language,
|
|
const gchar *token, gsize token_len) {
|
|
if (!language || !token || token_len == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
for (gsize i = 0; i < language->group_count; i++) {
|
|
const MarkydKeywordGroup *group = &language->groups[i];
|
|
for (gsize k = 0; k < group->keyword_count; k++) {
|
|
const gchar *kw = group->keywords[k];
|
|
if (strlen(kw) == token_len && strncmp(kw, token, token_len) == 0) {
|
|
return group->tag_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void consume_integer_suffix_c(const gchar **s) {
|
|
const gchar *p;
|
|
gboolean saw_u = FALSE;
|
|
gint long_count = 0;
|
|
|
|
if (!s || !*s) {
|
|
return;
|
|
}
|
|
|
|
p = *s;
|
|
while (*p) {
|
|
if ((p[0] == 'u' || p[0] == 'U') && !saw_u) {
|
|
saw_u = TRUE;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (p[0] == 'l' || p[0] == 'L') {
|
|
if (p[1] == p[0]) {
|
|
if (long_count >= 2) {
|
|
break;
|
|
}
|
|
long_count += 2;
|
|
p += 2;
|
|
} else {
|
|
if (long_count >= 2) {
|
|
break;
|
|
}
|
|
long_count += 1;
|
|
p++;
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
*s = p;
|
|
}
|
|
|
|
static gboolean starts_number_c(const gchar *line, const gchar *p) {
|
|
gchar prev = '\0';
|
|
|
|
if (!line || !p || *p == '\0') {
|
|
return FALSE;
|
|
}
|
|
|
|
if (!(g_ascii_isdigit((guchar)p[0]) ||
|
|
(p[0] == '.' && g_ascii_isdigit((guchar)p[1])))) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (p > line) {
|
|
prev = p[-1];
|
|
if (is_ascii_identifier_char(prev) || prev == '.') {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gint scan_number_c(const gchar *p) {
|
|
const gchar *s;
|
|
const gchar *end;
|
|
gboolean is_float = FALSE;
|
|
gboolean saw_digits = FALSE;
|
|
|
|
if (!p || *p == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
s = p;
|
|
if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
|
gboolean saw_hex_digit = FALSE;
|
|
s += 2;
|
|
while (g_ascii_isxdigit((guchar)*s)) {
|
|
s++;
|
|
saw_hex_digit = TRUE;
|
|
}
|
|
|
|
if (*s == '.') {
|
|
is_float = TRUE;
|
|
s++;
|
|
while (g_ascii_isxdigit((guchar)*s)) {
|
|
s++;
|
|
saw_hex_digit = TRUE;
|
|
}
|
|
}
|
|
|
|
if (!saw_hex_digit) {
|
|
return 0;
|
|
}
|
|
|
|
if (*s == 'p' || *s == 'P') {
|
|
const gchar *exp = s + 1;
|
|
if (*exp == '+' || *exp == '-') {
|
|
exp++;
|
|
}
|
|
if (!g_ascii_isdigit((guchar)*exp)) {
|
|
return 0;
|
|
}
|
|
while (g_ascii_isdigit((guchar)*exp)) {
|
|
exp++;
|
|
}
|
|
s = exp;
|
|
is_float = TRUE;
|
|
}
|
|
|
|
if (is_float) {
|
|
if (*s == 'f' || *s == 'F' || *s == 'l' || *s == 'L') {
|
|
s++;
|
|
}
|
|
} else {
|
|
consume_integer_suffix_c(&s);
|
|
}
|
|
} else if (*s == '0' && (s[1] == 'b' || s[1] == 'B')) {
|
|
s += 2;
|
|
while (*s == '0' || *s == '1') {
|
|
s++;
|
|
saw_digits = TRUE;
|
|
}
|
|
if (!saw_digits) {
|
|
return 0;
|
|
}
|
|
consume_integer_suffix_c(&s);
|
|
} else {
|
|
if (*s == '.') {
|
|
is_float = TRUE;
|
|
s++;
|
|
while (g_ascii_isdigit((guchar)*s)) {
|
|
s++;
|
|
saw_digits = TRUE;
|
|
}
|
|
if (!saw_digits) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
while (g_ascii_isdigit((guchar)*s)) {
|
|
s++;
|
|
saw_digits = TRUE;
|
|
}
|
|
if (*s == '.') {
|
|
is_float = TRUE;
|
|
s++;
|
|
while (g_ascii_isdigit((guchar)*s)) {
|
|
s++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (*s == 'e' || *s == 'E') {
|
|
const gchar *exp = s + 1;
|
|
if (*exp == '+' || *exp == '-') {
|
|
exp++;
|
|
}
|
|
if (g_ascii_isdigit((guchar)*exp)) {
|
|
while (g_ascii_isdigit((guchar)*exp)) {
|
|
exp++;
|
|
}
|
|
s = exp;
|
|
is_float = TRUE;
|
|
}
|
|
}
|
|
|
|
if (is_float) {
|
|
if (*s == 'f' || *s == 'F' || *s == 'l' || *s == 'L') {
|
|
s++;
|
|
}
|
|
} else {
|
|
consume_integer_suffix_c(&s);
|
|
}
|
|
}
|
|
|
|
end = s;
|
|
if (is_ascii_identifier_char(*end)) {
|
|
return 0;
|
|
}
|
|
|
|
return (gint)(end - p);
|
|
}
|
|
|
|
static gboolean is_dec_digit_char(gchar c) {
|
|
return g_ascii_isdigit((guchar)c);
|
|
}
|
|
|
|
static gboolean is_hex_digit_char(gchar c) {
|
|
return g_ascii_isxdigit((guchar)c);
|
|
}
|
|
|
|
static gboolean is_oct_digit_char(gchar c) { return (c >= '0' && c <= '7'); }
|
|
|
|
static gboolean is_bin_digit_char(gchar c) { return (c == '0' || c == '1'); }
|
|
|
|
static gboolean consume_digits_with_underscores(const gchar **s,
|
|
gboolean (*is_digit)(gchar)) {
|
|
const gchar *p;
|
|
gboolean saw_digit = FALSE;
|
|
gboolean prev_underscore = FALSE;
|
|
|
|
if (!s || !*s || !is_digit) {
|
|
return FALSE;
|
|
}
|
|
|
|
p = *s;
|
|
while (*p) {
|
|
if (*p == '_') {
|
|
if (!saw_digit || prev_underscore) {
|
|
break;
|
|
}
|
|
prev_underscore = TRUE;
|
|
p++;
|
|
continue;
|
|
}
|
|
if (!is_digit(*p)) {
|
|
break;
|
|
}
|
|
saw_digit = TRUE;
|
|
prev_underscore = FALSE;
|
|
p++;
|
|
}
|
|
|
|
if (!saw_digit || prev_underscore) {
|
|
return FALSE;
|
|
}
|
|
|
|
*s = p;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean starts_number_python(const gchar *line, const gchar *p) {
|
|
gchar prev = '\0';
|
|
|
|
if (!line || !p || *p == '\0') {
|
|
return FALSE;
|
|
}
|
|
if (!(g_ascii_isdigit((guchar)p[0]) ||
|
|
(p[0] == '.' && g_ascii_isdigit((guchar)p[1])))) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (p > line) {
|
|
prev = p[-1];
|
|
if (is_ascii_identifier_char(prev) || prev == '.') {
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gint scan_number_python(const gchar *p) {
|
|
const gchar *s;
|
|
gboolean is_float = FALSE;
|
|
gboolean is_decimal_number = FALSE;
|
|
|
|
if (!p || *p == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
s = p;
|
|
if (*s == '.') {
|
|
s++;
|
|
if (!consume_digits_with_underscores(&s, is_dec_digit_char)) {
|
|
return 0;
|
|
}
|
|
is_float = TRUE;
|
|
is_decimal_number = TRUE;
|
|
} else if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
|
s += 2;
|
|
if (!consume_digits_with_underscores(&s, is_hex_digit_char)) {
|
|
return 0;
|
|
}
|
|
} else if (*s == '0' && (s[1] == 'o' || s[1] == 'O')) {
|
|
s += 2;
|
|
if (!consume_digits_with_underscores(&s, is_oct_digit_char)) {
|
|
return 0;
|
|
}
|
|
} else if (*s == '0' && (s[1] == 'b' || s[1] == 'B')) {
|
|
s += 2;
|
|
if (!consume_digits_with_underscores(&s, is_bin_digit_char)) {
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (!consume_digits_with_underscores(&s, is_dec_digit_char)) {
|
|
return 0;
|
|
}
|
|
is_decimal_number = TRUE;
|
|
|
|
if (*s == '.') {
|
|
const gchar *after_dot = s + 1;
|
|
s = after_dot;
|
|
/* '1.' is valid; digits after dot are optional. */
|
|
if (g_ascii_isdigit((guchar)*s)) {
|
|
if (!consume_digits_with_underscores(&s, is_dec_digit_char)) {
|
|
return 0;
|
|
}
|
|
}
|
|
is_float = TRUE;
|
|
}
|
|
|
|
if (*s == 'e' || *s == 'E') {
|
|
const gchar *exp = s + 1;
|
|
if (*exp == '+' || *exp == '-') {
|
|
exp++;
|
|
}
|
|
if (!consume_digits_with_underscores(&exp, is_dec_digit_char)) {
|
|
return 0;
|
|
}
|
|
s = exp;
|
|
is_float = TRUE;
|
|
}
|
|
}
|
|
|
|
if (*s == 'j' || *s == 'J') {
|
|
if (!is_float && !is_decimal_number) {
|
|
return 0;
|
|
}
|
|
s++;
|
|
}
|
|
|
|
if (is_ascii_identifier_char(*s)) {
|
|
return 0;
|
|
}
|
|
return (gint)(s - p);
|
|
}
|
|
|
|
static gboolean is_python_string_prefix_char(gchar c) {
|
|
switch (c) {
|
|
case 'r':
|
|
case 'R':
|
|
case 'b':
|
|
case 'B':
|
|
case 'u':
|
|
case 'U':
|
|
case 'f':
|
|
case 'F':
|
|
return TRUE;
|
|
default:
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static gboolean parse_python_string_start(const gchar *p, gint *prefix_len,
|
|
gchar *quote_char,
|
|
gboolean *is_triple,
|
|
gboolean *is_raw) {
|
|
gint plen = 0;
|
|
gboolean raw = FALSE;
|
|
|
|
if (!p || !prefix_len || !quote_char || !is_triple || !is_raw) {
|
|
return FALSE;
|
|
}
|
|
|
|
while (plen < 3 && is_python_string_prefix_char(p[plen])) {
|
|
if (p[plen] == 'r' || p[plen] == 'R') {
|
|
raw = TRUE;
|
|
}
|
|
plen++;
|
|
}
|
|
|
|
if (p[plen] != '\'' && p[plen] != '"') {
|
|
return FALSE;
|
|
}
|
|
|
|
*prefix_len = plen;
|
|
*quote_char = p[plen];
|
|
*is_triple = (p[plen + 1] == p[plen] && p[plen + 2] == p[plen]);
|
|
*is_raw = raw;
|
|
return TRUE;
|
|
}
|
|
|
|
static void scan_line_c_like(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token,
|
|
gpointer user_data,
|
|
gboolean allow_java_text_blocks) {
|
|
const gchar *p;
|
|
gint char_index = 0;
|
|
gboolean in_block_comment = FALSE;
|
|
gboolean in_java_text_block = FALSE;
|
|
|
|
if (!language || !line || !state || !on_token) {
|
|
return;
|
|
}
|
|
|
|
in_block_comment = (state->flags & MARKYD_SCAN_FLAG_BLOCK_COMMENT) != 0;
|
|
if (allow_java_text_blocks) {
|
|
in_java_text_block =
|
|
(state->flags & MARKYD_SCAN_FLAG_JAVA_TEXT_BLOCK) != 0;
|
|
}
|
|
p = line;
|
|
while (*p) {
|
|
if (allow_java_text_blocks && in_java_text_block) {
|
|
gint start_char_index = char_index;
|
|
while (*p) {
|
|
if (starts_with_triple_quote(p)) {
|
|
p += 3;
|
|
char_index += 3;
|
|
in_java_text_block = FALSE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
continue;
|
|
}
|
|
|
|
if (in_block_comment) {
|
|
gint start_char_index = char_index;
|
|
while (*p) {
|
|
if (p[0] == '*' && p[1] == '/') {
|
|
p += 2;
|
|
char_index += 2;
|
|
in_block_comment = FALSE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
|
|
continue;
|
|
}
|
|
|
|
if (p[0] == '/' && p[1] == '/') {
|
|
gint start_char_index = char_index;
|
|
while (*p) {
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
|
|
break;
|
|
}
|
|
if (p[0] == '/' && p[1] == '*') {
|
|
gint start_char_index = char_index;
|
|
p += 2;
|
|
char_index += 2;
|
|
in_block_comment = TRUE;
|
|
while (*p) {
|
|
if (p[0] == '*' && p[1] == '/') {
|
|
p += 2;
|
|
char_index += 2;
|
|
in_block_comment = FALSE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
|
|
continue;
|
|
}
|
|
if (allow_java_text_blocks && starts_with_triple_quote(p)) {
|
|
gint start_char_index = char_index;
|
|
p += 3;
|
|
char_index += 3;
|
|
in_java_text_block = TRUE;
|
|
while (*p) {
|
|
if (starts_with_triple_quote(p)) {
|
|
p += 3;
|
|
char_index += 3;
|
|
in_java_text_block = FALSE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
continue;
|
|
}
|
|
if (p[0] == '"' || p[0] == '\'') {
|
|
gint start_char_index = char_index;
|
|
skip_quoted_literal(&p, &char_index, p[0]);
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
continue;
|
|
}
|
|
|
|
gunichar c = g_utf8_get_char(p);
|
|
const gchar *next = g_utf8_next_char(p);
|
|
|
|
if (is_identifier_start(c)) {
|
|
const gchar *token_start = p;
|
|
gint start_char_index = char_index;
|
|
|
|
p = next;
|
|
char_index++;
|
|
|
|
while (*p) {
|
|
gunichar tc = g_utf8_get_char(p);
|
|
const gchar *tnext = g_utf8_next_char(p);
|
|
if (!is_identifier_char(tc)) {
|
|
break;
|
|
}
|
|
p = tnext;
|
|
char_index++;
|
|
}
|
|
|
|
gsize token_len = (gsize)(p - token_start);
|
|
const gchar *tag_name = lookup_keyword_tag(language, token_start, token_len);
|
|
if (tag_name) {
|
|
on_token(start_char_index, char_index, tag_name, user_data);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (starts_number_c(line, p)) {
|
|
gint number_chars = scan_number_c(p);
|
|
if (number_chars > 0) {
|
|
on_token(char_index, char_index + number_chars, MARKYD_TAG_CODE_LITERAL,
|
|
user_data);
|
|
p += number_chars;
|
|
char_index += number_chars;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
p = next;
|
|
char_index++;
|
|
}
|
|
|
|
if (in_block_comment) {
|
|
state->flags |= MARKYD_SCAN_FLAG_BLOCK_COMMENT;
|
|
} else {
|
|
state->flags &= ~MARKYD_SCAN_FLAG_BLOCK_COMMENT;
|
|
}
|
|
if (allow_java_text_blocks && in_java_text_block) {
|
|
state->flags |= MARKYD_SCAN_FLAG_JAVA_TEXT_BLOCK;
|
|
} else {
|
|
state->flags &= ~MARKYD_SCAN_FLAG_JAVA_TEXT_BLOCK;
|
|
}
|
|
}
|
|
|
|
static void scan_line_c(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token, gpointer user_data) {
|
|
scan_line_c_like(language, line, state, on_token, user_data, FALSE);
|
|
}
|
|
|
|
static void scan_line_java(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token,
|
|
gpointer user_data) {
|
|
scan_line_c_like(language, line, state, on_token, user_data, TRUE);
|
|
}
|
|
|
|
static void scan_line_python(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token,
|
|
gpointer user_data) {
|
|
const gchar *p;
|
|
gint char_index = 0;
|
|
gboolean in_triple_single = FALSE;
|
|
gboolean in_triple_double = FALSE;
|
|
|
|
if (!language || !line || !state || !on_token) {
|
|
return;
|
|
}
|
|
|
|
in_triple_single = (state->flags & MARKYD_SCAN_FLAG_PY_TRIPLE_SINGLE) != 0;
|
|
in_triple_double = (state->flags & MARKYD_SCAN_FLAG_PY_TRIPLE_DOUBLE) != 0;
|
|
|
|
p = line;
|
|
while (*p) {
|
|
if (in_triple_single || in_triple_double) {
|
|
gint start_char_index = char_index;
|
|
gchar quote = in_triple_single ? '\'' : '"';
|
|
|
|
while (*p) {
|
|
if (p[0] == quote && p[1] == quote && p[2] == quote) {
|
|
p += 3;
|
|
char_index += 3;
|
|
in_triple_single = FALSE;
|
|
in_triple_double = FALSE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
continue;
|
|
}
|
|
|
|
if (p[0] == '#') {
|
|
gint start_char_index = char_index;
|
|
while (*p) {
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
|
|
break;
|
|
}
|
|
|
|
{
|
|
gint prefix_len = 0;
|
|
gchar quote_char = '\0';
|
|
gboolean is_triple = FALSE;
|
|
gboolean is_raw = FALSE;
|
|
|
|
if (parse_python_string_start(p, &prefix_len, "e_char, &is_triple,
|
|
&is_raw)) {
|
|
gint start_char_index = char_index;
|
|
|
|
p += prefix_len;
|
|
char_index += prefix_len;
|
|
|
|
if (is_triple) {
|
|
gboolean closed = FALSE;
|
|
p += 3;
|
|
char_index += 3;
|
|
|
|
while (*p) {
|
|
if (p[0] == quote_char && p[1] == quote_char && p[2] == quote_char) {
|
|
p += 3;
|
|
char_index += 3;
|
|
closed = TRUE;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
|
|
if (!closed) {
|
|
if (quote_char == '\'') {
|
|
in_triple_single = TRUE;
|
|
} else {
|
|
in_triple_double = TRUE;
|
|
}
|
|
}
|
|
} else {
|
|
/* Consume opening quote. */
|
|
p++;
|
|
char_index++;
|
|
while (*p) {
|
|
if (!is_raw && p[0] == '\\') {
|
|
p++;
|
|
char_index++;
|
|
if (*p) {
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
continue;
|
|
}
|
|
if (p[0] == quote_char) {
|
|
p++;
|
|
char_index++;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
}
|
|
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL,
|
|
user_data);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
{
|
|
gunichar c = g_utf8_get_char(p);
|
|
const gchar *next = g_utf8_next_char(p);
|
|
|
|
if (is_identifier_start(c)) {
|
|
const gchar *token_start = p;
|
|
gint start_char_index = char_index;
|
|
|
|
p = next;
|
|
char_index++;
|
|
|
|
while (*p) {
|
|
gunichar tc = g_utf8_get_char(p);
|
|
const gchar *tnext = g_utf8_next_char(p);
|
|
if (!is_identifier_char(tc)) {
|
|
break;
|
|
}
|
|
p = tnext;
|
|
char_index++;
|
|
}
|
|
|
|
gsize token_len = (gsize)(p - token_start);
|
|
const gchar *tag_name =
|
|
lookup_keyword_tag(language, token_start, token_len);
|
|
if (tag_name) {
|
|
on_token(start_char_index, char_index, tag_name, user_data);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (starts_number_python(line, p)) {
|
|
gint number_chars = scan_number_python(p);
|
|
if (number_chars > 0) {
|
|
on_token(char_index, char_index + number_chars, MARKYD_TAG_CODE_LITERAL,
|
|
user_data);
|
|
p += number_chars;
|
|
char_index += number_chars;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
|
|
if (in_triple_single) {
|
|
state->flags |= MARKYD_SCAN_FLAG_PY_TRIPLE_SINGLE;
|
|
} else {
|
|
state->flags &= ~MARKYD_SCAN_FLAG_PY_TRIPLE_SINGLE;
|
|
}
|
|
if (in_triple_double) {
|
|
state->flags |= MARKYD_SCAN_FLAG_PY_TRIPLE_DOUBLE;
|
|
} else {
|
|
state->flags &= ~MARKYD_SCAN_FLAG_PY_TRIPLE_DOUBLE;
|
|
}
|
|
}
|
|
|
|
/* Bash: '#' comments (position-sensitive), single/double-quoted strings, and
|
|
* keyword/builtin identifiers. No multi-line state is tracked. */
|
|
static void scan_line_bash(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token,
|
|
gpointer user_data) {
|
|
const gchar *p;
|
|
gint char_index = 0;
|
|
gchar prev = '\0';
|
|
|
|
if (!language || !line || !state || !on_token) {
|
|
return;
|
|
}
|
|
|
|
p = line;
|
|
while (*p) {
|
|
/* A '#' starts a comment only at the start of a word (line start or after
|
|
* whitespace), so tokens like "$#" and "a#b" are left alone. */
|
|
if (p[0] == '#' && (p == line || prev == ' ' || prev == '\t')) {
|
|
gint start_char_index = char_index;
|
|
while (*p) {
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
|
|
break;
|
|
}
|
|
|
|
if (p[0] == '\'') {
|
|
gint start_char_index = char_index;
|
|
p++;
|
|
char_index++;
|
|
while (*p && p[0] != '\'') {
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
if (*p == '\'') {
|
|
p++;
|
|
char_index++;
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
prev = '\'';
|
|
continue;
|
|
}
|
|
|
|
if (p[0] == '"') {
|
|
gint start_char_index = char_index;
|
|
p++;
|
|
char_index++;
|
|
while (*p) {
|
|
if (p[0] == '\\' && p[1]) {
|
|
p++;
|
|
char_index++;
|
|
advance_utf8_char(&p, &char_index);
|
|
continue;
|
|
}
|
|
if (p[0] == '"') {
|
|
p++;
|
|
char_index++;
|
|
break;
|
|
}
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
|
|
prev = '"';
|
|
continue;
|
|
}
|
|
|
|
{
|
|
gunichar c = g_utf8_get_char(p);
|
|
const gchar *next = g_utf8_next_char(p);
|
|
|
|
if (is_identifier_start(c)) {
|
|
const gchar *token_start = p;
|
|
gint start_char_index = char_index;
|
|
|
|
p = next;
|
|
char_index++;
|
|
while (*p) {
|
|
gunichar tc = g_utf8_get_char(p);
|
|
const gchar *tnext = g_utf8_next_char(p);
|
|
if (!is_identifier_char(tc)) {
|
|
break;
|
|
}
|
|
p = tnext;
|
|
char_index++;
|
|
}
|
|
|
|
gsize token_len = (gsize)(p - token_start);
|
|
const gchar *tag_name =
|
|
lookup_keyword_tag(language, token_start, token_len);
|
|
if (tag_name) {
|
|
on_token(start_char_index, char_index, tag_name, user_data);
|
|
}
|
|
prev = 'a';
|
|
continue;
|
|
}
|
|
}
|
|
|
|
prev = *p;
|
|
advance_utf8_char(&p, &char_index);
|
|
}
|
|
}
|
|
|
|
static const MarkydLanguageHighlight languages[] = {
|
|
{"c", c_groups, G_N_ELEMENTS(c_groups), scan_line_c},
|
|
{"java", java_groups, G_N_ELEMENTS(java_groups), scan_line_java},
|
|
{"python", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
|
|
{"py", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
|
|
{"javascript", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
|
|
{"js", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
|
|
{"kotlin", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
|
|
{"kt", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
|
|
{"bash", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
|
|
{"sh", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
|
|
{"shell", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
|
|
};
|
|
|
|
const MarkydLanguageHighlight *
|
|
markyd_code_lookup_language(const gchar *language) {
|
|
if (!language || !*language) {
|
|
return NULL;
|
|
}
|
|
|
|
for (gsize i = 0; i < G_N_ELEMENTS(languages); i++) {
|
|
if (g_ascii_strcasecmp(language, languages[i].language) == 0) {
|
|
return &languages[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void markyd_code_scan_state_reset(MarkydCodeScanState *state) {
|
|
if (!state) {
|
|
return;
|
|
}
|
|
state->flags = 0;
|
|
}
|
|
|
|
void markyd_code_scan_line(const MarkydLanguageHighlight *language,
|
|
const gchar *line, MarkydCodeScanState *state,
|
|
MarkydCodeTokenCallback on_token,
|
|
gpointer user_data) {
|
|
if (!language || !line || !state || !on_token) {
|
|
return;
|
|
}
|
|
if (!language->scan_line) {
|
|
return;
|
|
}
|
|
|
|
language->scan_line(language, line, state, on_token, user_data);
|
|
}
|