#include "inline_parse.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
void inline_runs_init(InlineRunList *list) {
list->runs = NULL;
list->count = 0;
list->cap = 0;
}
void inline_runs_free(InlineRunList *list) {
free(list->runs);
list->runs = NULL;
list->count = 0;
list->cap = 0;
}
static void push_run(InlineRunList *list, RunKind kind, int rawStart, int rawEnd, int contentStart, int contentEnd) {
if (list->count >= list->cap) {
list->cap = list->cap > 0 ? list->cap * 2 : 8;
list->runs = realloc(list->runs, (size_t)list->cap * sizeof(InlineRun));
}
InlineRun *r = &list->runs[list->count++];
r->kind = kind;
r->rawStart = rawStart;
r->rawEnd = rawEnd;
r->contentStart = contentStart;
r->contentEnd = contentEnd;
}
/* Finds the next unescaped occurrence of `c`, starting at `from`. -1 if not found. */
static int find_char(const char *text, int len, int from, char c) {
for (int i = from; i < len; i++) {
if (text[i] == '\\' && i + 1 < len) { i++; continue; }
if (text[i] == c) return i;
}
return -1;
}
/* Finds the next unescaped occurrence of the two-char delimiter `cc`. -1 if not found. */
static int find_double(const char *text, int len, int from, char c) {
for (int i = from; i < len - 1; i++) {
if (text[i] == '\\' && i + 1 < len) { i++; continue; }
if (text[i] == c && text[i + 1] == c) return i;
}
return -1;
}
/* If text[i..] starts with "http://" or "https://", returns the length of the URL run
(stopping at whitespace/'<'/'>', then trimming trailing punctuation that's usually not
meant to be part of the URL, e.g. the '.' ending a sentence). Returns 0 if no match. */
static int match_autolink_len(const char *text, int i, int len) {
static const char *const schemes[] = { "https://", "http://" };
for (int s = 0; s < 2; s++) {
int slen = (int)strlen(schemes[s]);
if (i + slen <= len && memcmp(text + i, schemes[s], (size_t)slen) == 0) {
int j = i + slen;
while (j < len && !isspace((unsigned char)text[j]) && text[j] != '<' && text[j] != '>') j++;
while (j > i + slen && strchr(".,;:!?)", text[j - 1])) j--;
return j - i;
}
}
return 0;
}
void inline_parse(const char *text, int len, InlineRunList *out) {
inline_runs_init(out);
int i = 0;
int plainStart = 0;
while (i < len) {
char c = text[i];
int matched = 0;
if (c == '`') {
int j = find_char(text, len, i + 1, '`');
if (j >= 0 && j > i + 1) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_CODE, i, j + 1, i + 1, j);
i = j + 1;
plainStart = i;
matched = 1;
}
} else if ((c == '*' || c == '_') && i + 2 < len && text[i + 1] == c && text[i + 2] == c) {
int j = -1;
for (int k = i + 3; k < len - 2; k++) {
if (text[k] == c && text[k + 1] == c && text[k + 2] == c) { j = k; break; }
}
if (j >= 0 && j > i + 3) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_BOLD_ITALIC, i, j + 3, i + 3, j);
i = j + 3;
plainStart = i;
matched = 1;
}
} else if ((c == '*' || c == '_') && i + 1 < len && text[i + 1] == c) {
int j = find_double(text, len, i + 2, c);
if (j >= 0 && j > i + 2) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_BOLD, i, j + 2, i + 2, j);
i = j + 2;
plainStart = i;
matched = 1;
}
} else if (c == '*' || c == '_') {
int j = find_char(text, len, i + 1, c);
if (j >= 0 && j > i + 1) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_ITALIC, i, j + 1, i + 1, j);
i = j + 1;
plainStart = i;
matched = 1;
}
} else if (c == '[') {
int closeBracket = find_char(text, len, i + 1, ']');
if (closeBracket >= 0 && closeBracket > i + 1 && closeBracket + 1 < len && text[closeBracket + 1] == '(') {
int closeParen = find_char(text, len, closeBracket + 2, ')');
if (closeParen >= 0) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_LINK, i, closeParen + 1, i + 1, closeBracket);
i = closeParen + 1;
plainStart = i;
matched = 1;
}
}
} else if (c == '~' && i + 1 < len && text[i + 1] == '~') {
int j = find_double(text, len, i + 2, '~');
if (j >= 0 && j > i + 2) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_STRIKE, i, j + 2, i + 2, j);
i = j + 2;
plainStart = i;
matched = 1;
}
} else if (c == '~') {
int j = find_char(text, len, i + 1, '~');
if (j >= 0 && j > i + 1) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_SUB, i, j + 1, i + 1, j);
i = j + 1;
plainStart = i;
matched = 1;
}
} else if (c == '=' && i + 1 < len && text[i + 1] == '=') {
int j = find_double(text, len, i + 2, '=');
if (j >= 0 && j > i + 2) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_HIGHLIGHT, i, j + 2, i + 2, j);
i = j + 2;
plainStart = i;
matched = 1;
}
} else if (c == '^') {
int j = find_char(text, len, i + 1, '^');
if (j >= 0 && j > i + 1) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
push_run(out, RUN_SUPER, i, j + 1, i + 1, j);
i = j + 1;
plainStart = i;
matched = 1;
}
} else if (c == 'h') {
int alen = match_autolink_len(text, i, len);
if (alen > 0) {
if (i > plainStart) push_run(out, RUN_PLAIN, plainStart, i, plainStart, i);
/* No markers to hide/reveal for a bare-URL autolink: raw range == content range. */
push_run(out, RUN_LINK, i, i + alen, i, i + alen);
i += alen;
plainStart = i;
matched = 1;
}
} else if (c == '\\' && i + 1 < len) {
i += 2;
continue;
}
if (!matched) i++;
}
if (len > plainStart) push_run(out, RUN_PLAIN, plainStart, len, plainStart, len);
}