forked from WHC/Hermit
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
338 lines
10 KiB
C
338 lines
10 KiB
C
#include "block_scan.h"
|
|
#include <string.h>
|
|
|
|
int block_count_heading(const char *input, size_t len, size_t pos, size_t *content_start)
|
|
{
|
|
size_t i = pos;
|
|
int level = 0;
|
|
while (i < len && input[i] == '#') {
|
|
level++;
|
|
i++;
|
|
}
|
|
if (level == 0 || level > 6) return 0;
|
|
if (i < len && input[i] != ' ' && input[i] != '\n') return 0;
|
|
while (i < len && input[i] == ' ') i++;
|
|
*content_start = i;
|
|
return level;
|
|
}
|
|
|
|
int block_is_fence(const char *input, size_t len, size_t pos,
|
|
int *fence_len, char *fence_char)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i + 2 >= len) return 0;
|
|
char ch = input[i];
|
|
if (ch != '`' && ch != '~') return 0;
|
|
if (input[i + 1] != ch || input[i + 2] != ch) return 0;
|
|
int fl = 3;
|
|
i += 3;
|
|
while (i < len && input[i] == ch) { fl++; i++; }
|
|
*fence_len = fl;
|
|
if (fence_char)
|
|
*fence_char = ch;
|
|
return 1;
|
|
}
|
|
|
|
void block_fence_info_range(const char *input, size_t len, size_t pos,
|
|
int fence_len, char fence_char,
|
|
size_t *info_start, size_t *info_end)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
i += (size_t)fence_len;
|
|
(void)fence_char;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
*info_start = i;
|
|
size_t end = i;
|
|
while (end < len && input[end] != '\n' && input[end] != '\r')
|
|
end++;
|
|
while (end > *info_start &&
|
|
(input[end - 1] == ' ' || input[end - 1] == '\t'))
|
|
end--;
|
|
*info_end = end;
|
|
}
|
|
|
|
int block_is_thematic_break(const char *input, size_t pos, size_t le)
|
|
{
|
|
size_t i = pos;
|
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i >= le) return 0;
|
|
|
|
char marker = input[i];
|
|
if (marker != '*' && marker != '-' && marker != '_') return 0;
|
|
|
|
int count = 0;
|
|
for (; i < le; i++) {
|
|
char c = input[i];
|
|
if (c == marker) {
|
|
count++;
|
|
} else if (c == ' ' || c == '\t') {
|
|
continue;
|
|
} else if (c == '\r') {
|
|
continue;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
return count >= 3;
|
|
}
|
|
|
|
int block_is_setext_underline(const char *input, size_t pos, size_t le, int *level)
|
|
{
|
|
size_t i = pos;
|
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i >= le) return 0;
|
|
|
|
char marker = input[i];
|
|
if (marker != '=' && marker != '-') return 0;
|
|
|
|
size_t start = i;
|
|
while (i < le && input[i] == marker) i++;
|
|
if (i == start) return 0;
|
|
while (i < le && (input[i] == ' ' || input[i] == '\t' || input[i] == '\r'))
|
|
i++;
|
|
if (i < le) return 0;
|
|
|
|
/* Thematic-break-looking '---' is still a valid setext underline when
|
|
* paired with a preceding text line (caller decides context). */
|
|
if (level)
|
|
*level = (marker == '=') ? 1 : 2;
|
|
return 1;
|
|
}
|
|
|
|
int block_is_blockquote(const char *input, size_t len, size_t pos, size_t *content_start)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i >= len || input[i] != '>') return 0;
|
|
i++;
|
|
if (i < len && input[i] == ' ') i++;
|
|
*content_start = i;
|
|
return 1;
|
|
}
|
|
|
|
int block_is_ul(const char *input, size_t len, size_t pos, size_t *content_start)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i >= len) return 0;
|
|
if (input[i] != '-' && input[i] != '*' && input[i] != '+') return 0;
|
|
if (i + 1 < len && input[i + 1] != ' ') return 0;
|
|
i += 2;
|
|
*content_start = i;
|
|
return 1;
|
|
}
|
|
|
|
int block_parse_task_marker(const char *input, size_t len, size_t content_start,
|
|
int *task, size_t *text_start)
|
|
{
|
|
size_t i = content_start;
|
|
if (i + 3 > len) return 0;
|
|
if (input[i] != '[') return 0;
|
|
char mark = input[i + 1];
|
|
if (input[i + 2] != ']') return 0;
|
|
if (mark != ' ' && mark != 'x' && mark != 'X') return 0;
|
|
i += 3;
|
|
if (i < len && input[i] == ' ') i++;
|
|
*task = (mark == ' ') ? 1 : 2;
|
|
*text_start = i;
|
|
return 1;
|
|
}
|
|
|
|
int block_is_ol(const char *input, size_t len, size_t pos, size_t *content_start, int *num)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
size_t num_start = i;
|
|
{ unsigned char c = (unsigned char)input[i];
|
|
if (i >= len || c < '0' || c > '9') return 0; }
|
|
while (i < len) {
|
|
unsigned char c = (unsigned char)input[i];
|
|
if (c < '0' || c > '9') break;
|
|
i++;
|
|
}
|
|
if (i >= len || input[i] != '.') return 0;
|
|
if (i + 1 < len && input[i + 1] != ' ') return 0;
|
|
*num = 0;
|
|
for (size_t j = num_start; j < i; j++)
|
|
*num = *num * 10 + (input[j] - '0');
|
|
i += 2;
|
|
*content_start = i;
|
|
return 1;
|
|
}
|
|
|
|
int block_line_starts_table(const char *input, size_t len, size_t pos)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i >= len) return 0;
|
|
/* GFM: leading '|' OR a pipe somewhere on the line with a separator below.
|
|
* Cheap reject: must contain '|' before newline. */
|
|
const char *nl = memchr(input + i, '\n', len - i);
|
|
size_t line_len = nl ? (size_t)(nl - (input + i)) : (len - i);
|
|
return memchr(input + i, '|', line_len) != NULL;
|
|
}
|
|
|
|
int block_is_latex_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t pos, size_t *content_start, size_t *content_end)
|
|
{
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
if (i + 1 >= len || input[i] != '$' || input[i + 1] != '$') return 0;
|
|
i += 2;
|
|
*content_start = i;
|
|
size_t le = block_line_end(ctx, input, len, pos);
|
|
if (i + 1 < le && input[le - 2] == '$' && input[le - 1] == '$') {
|
|
*content_end = le - 2;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int block_try_setext(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t pos, size_t le, size_t stop, int is_final,
|
|
line_info_t *info, size_t *end_pos)
|
|
{
|
|
if (pos == le || (pos + 1 == le && input[pos] == '\r'))
|
|
return 0;
|
|
|
|
/* Next line starts after the newline at `le` (line_end points at '\n'). */
|
|
if (le >= len || input[le] != '\n')
|
|
return is_final ? 0 : -1;
|
|
|
|
size_t next = le + 1;
|
|
if (next >= len)
|
|
return is_final ? 0 : -1;
|
|
if (next >= stop && !is_final)
|
|
return -1;
|
|
|
|
/* Cheap reject before full underline scan. */
|
|
size_t u = next;
|
|
while (u < len && (input[u] == ' ' || input[u] == '\t'))
|
|
u++;
|
|
if (u >= len || (input[u] != '=' && input[u] != '-'))
|
|
return 0;
|
|
|
|
size_t nle = block_line_end(ctx, input, len, next);
|
|
int level = 0;
|
|
if (!block_is_setext_underline(input, next, nle, &level))
|
|
return 0;
|
|
|
|
info->kind = LK_HEADING;
|
|
info->heading_level = level;
|
|
info->content_start = pos;
|
|
while (info->content_start < le &&
|
|
(input[info->content_start] == ' ' || input[info->content_start] == '\t'))
|
|
info->content_start++;
|
|
info->content_end = le;
|
|
while (info->content_end > info->content_start &&
|
|
(input[info->content_end - 1] == ' ' ||
|
|
input[info->content_end - 1] == '\t' ||
|
|
input[info->content_end - 1] == '\r'))
|
|
info->content_end--;
|
|
info->fence_len = 0;
|
|
info->fence_char = 0;
|
|
info->info_start = 0;
|
|
info->info_end = 0;
|
|
info->task = 0;
|
|
*end_pos = block_skip_line(ctx, input, len, next);
|
|
return 1;
|
|
}
|
|
|
|
line_info_t block_classify_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
|
size_t pos, size_t le)
|
|
{
|
|
line_info_t info;
|
|
memset(&info, 0, sizeof(info));
|
|
info.kind = LK_PARA;
|
|
info.content_start = pos;
|
|
info.content_end = le;
|
|
|
|
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
|
|
info.kind = LK_EMPTY;
|
|
return info;
|
|
}
|
|
|
|
size_t i = pos;
|
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
|
unsigned char first = (i < len) ? (unsigned char)input[i] : 0;
|
|
|
|
/* Thematic breaks only possible for - / * / _ lines. */
|
|
if ((first == '*' || first == '-' || first == '_') &&
|
|
block_is_thematic_break(input, pos, le)) {
|
|
info.kind = LK_HR;
|
|
return info;
|
|
}
|
|
|
|
switch (first) {
|
|
case '#':
|
|
info.heading_level = block_count_heading(input, len, pos, &info.content_start);
|
|
if (info.heading_level > 0) {
|
|
info.kind = LK_HEADING;
|
|
info.content_end = le;
|
|
return info;
|
|
}
|
|
break;
|
|
case '`':
|
|
case '~':
|
|
if (block_is_fence(input, len, pos, &info.fence_len, &info.fence_char)) {
|
|
info.kind = LK_FENCE;
|
|
block_fence_info_range(input, len, pos, info.fence_len, info.fence_char,
|
|
&info.info_start, &info.info_end);
|
|
return info;
|
|
}
|
|
break;
|
|
case '>':
|
|
if (block_is_blockquote(input, len, pos, &info.content_start)) {
|
|
info.kind = LK_BQ;
|
|
return info;
|
|
}
|
|
break;
|
|
case '-': case '*': case '+':
|
|
if (block_is_ul(input, len, pos, &info.content_start)) {
|
|
info.kind = LK_UL;
|
|
size_t text = info.content_start;
|
|
int task = 0;
|
|
if (block_parse_task_marker(input, le, info.content_start, &task, &text)) {
|
|
info.task = task;
|
|
info.content_start = text;
|
|
}
|
|
return info;
|
|
}
|
|
break;
|
|
case '0': case '1': case '2': case '3': case '4':
|
|
case '5': case '6': case '7': case '8': case '9':
|
|
{
|
|
int ol_num = 0;
|
|
if (block_is_ol(input, len, pos, &info.content_start, &ol_num)) {
|
|
info.kind = LK_OL;
|
|
return info;
|
|
}
|
|
break;
|
|
}
|
|
case '$':
|
|
{
|
|
size_t cs, ce;
|
|
if (i + 1 < len && input[i] == '$' && input[i + 1] == '$') {
|
|
if (i == pos) {
|
|
info.kind = LK_LATEX_BLOCK;
|
|
return info;
|
|
}
|
|
if (block_is_latex_line(ctx, input, len, pos, &cs, &ce)) {
|
|
info.kind = LK_LATEX_LINE;
|
|
info.content_start = cs;
|
|
info.content_end = ce;
|
|
return info;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
info.kind = LK_PARA;
|
|
return info;
|
|
}
|