romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 11m ago
src/markdown.c
#include "markdown.h" #include "util.h" #include <ctype.h> #include <string.h> /* ---- small helpers -------------------------------------------------- */ static const char *find2(const char *s, size_t n, const char *nd, size_t nn) { if (!nn || n < nn) return NULL; for (size_t i = 0; i + nn <= n; i++) if (memcmp(s + i, nd, nn) == 0) return s + i; return NULL; } /* prefix test on a bounded (non-NUL-terminated) run */ static int pfx(const char *s, size_t n, const char *p) { size_t m = strlen(p); return n >= m && memcmp(s, p, m) == 0; } /* URLs may only carry inert schemes or be relative — anything else * renders as plain text, so the renderer can't mint javascript:/data: */ static int url_ok(const char *u, size_t n) { if (pfx(u, n, "http://") || pfx(u, n, "https://") || pfx(u, n, "mailto:")) return 1; if (n && (u[0] == '/' || u[0] == '#' || u[0] == '.')) return 1; return memchr(u, ':', n) == NULL; } /* ---- inline ---------------------------------------------------------- * depth caps nested-marker recursion on adversarial input ("[[[[["). */ static void inl(struct buf *b, const char *s, size_t n, int depth) { size_t i = 0; while (i < n) { char c = s[i]; size_t adv = 0; if (c == '`') { const char *e = find2(s + i + 1, n - i - 1, "`", 1); if (e) { buf_puts(b, "<code>"); buf_put_escaped(b, s + i + 1, (size_t)(e - s - i - 1)); buf_puts(b, "</code>"); adv = (size_t)(e - s - i) + 1; } } else if ((c == '*' || c == '_') && i + 1 < n && s[i + 1] == c && depth < 8) { char nd[2] = { c, c }; const char *e = find2(s + i + 2, n - i - 2, nd, 2); if (e && e > s + i + 2) { buf_puts(b, "<strong>"); inl(b, s + i + 2, (size_t)(e - s - i - 2), depth + 1); buf_puts(b, "</strong>"); adv = (size_t)(e - s - i) + 2; } } else if ((c == '*' || c == '_') && depth < 8 && (i + 1 < n && !isspace((unsigned char)s[i + 1])) && (c == '*' || i == 0 || !isalnum((unsigned char)s[i - 1]))) { /* _ requires an opening word boundary (snake_case stays * literal); * just needs a non-space right neighbour */ char nd[1] = { c }; const char *e = find2(s + i + 1, n - i - 1, nd, 1); while (e && e + 1 < s + n && e[1] == c) /* skip ** pair */ e = find2(e + 2, (size_t)(s + n - e - 2), nd, 1); if (e && e > s + i + 1 && !isspace((unsigned char)e[-1])) { buf_puts(b, "<em>"); inl(b, s + i + 1, (size_t)(e - s - i - 1), depth + 1); buf_puts(b, "</em>"); adv = (size_t)(e - s - i) + 1; } } else if ((c == '[' || (c == '!' && i + 1 < n && s[i + 1] == '[')) && depth < 8) { size_t t0 = i + (c == '!' ? 2 : 1); const char *cb = find2(s + t0, n - t0, "]", 1); if (cb && cb + 1 < s + n && cb[1] == '(') { const char *cp = find2(cb + 2, (size_t)(s + n - cb - 2), ")", 1); if (cp && url_ok(cb + 2, (size_t)(cp - cb - 2))) { buf_puts(b, "<a href=\""); buf_put_escaped(b, cb + 2, (size_t)(cp - cb - 2)); buf_puts(b, "\">"); inl(b, s + t0, (size_t)(cb - s - t0), depth + 1); buf_puts(b, "</a>"); adv = (size_t)(cp - s - i) + 1; } } } else if ((c == 'h' || c == 'm') && depth < 8 && (i == 0 || isspace((unsigned char)s[i - 1])) && (pfx(s + i, n - i, "http://") || pfx(s + i, n - i, "https://") || pfx(s + i, n - i, "mailto:"))) { /* bare autolink: consume non-space, trim trailing punct */ size_t j = i; while (j < n && !isspace((unsigned char)s[j]) && s[j] != '<') j++; while (j > i && strchr(".,;:!?'", s[j - 1])) j--; if (url_ok(s + i, j - i)) { buf_puts(b, "<a href=\""); buf_put_escaped(b, s + i, j - i); buf_puts(b, "\">"); buf_put_escaped(b, s + i, j - i); buf_puts(b, "</a>"); adv = j - i; } } if (adv) { i += adv; continue; } buf_put_escaped(b, s + i, 1); i++; } } /* ---- block level ----------------------------------------------------- */ /* leading spaces (up to 3) are insignificant for block starts */ static size_t skip_indent(const char *s, size_t n) { size_t i = 0; while (i < n && s[i] == ' ' && i < 3) i++; return i; } /* opening fence: run >=3 of ` or ~ ; returns run length, 0 else. * The info string after the run is ignored. */ static size_t fence_run(const char *s, size_t n, char *fc) { size_t i = skip_indent(s, n); if (i >= n || (s[i] != '`' && s[i] != '~')) return 0; size_t k = i; while (k < n && s[k] == s[i]) k++; if (k - i < 3) return 0; *fc = s[i]; return k - i; } /* closing fence per CommonMark: same char, run >= the opening run, * and nothing but spaces after it — so a ``` line inside a ```` * block stays literal. */ static int fence_close(const char *s, size_t n, char fc, size_t flen) { size_t i = skip_indent(s, n); if (i >= n || s[i] != fc) return 0; size_t k = i; while (k < n && s[k] == fc) k++; if (k - i < flen) return 0; for (; k < n; k++) if (!isspace((unsigned char)s[k])) return 0; return 1; } static int is_hr(const char *s, size_t n) { size_t i = skip_indent(s, n); if (i >= n || (s[i] != '-' && s[i] != '*' && s[i] != '_')) return 0; char c = s[i]; int cnt = 0; for (; i < n; i++) { if (s[i] == c) cnt++; else if (!isspace((unsigned char)s[i])) return 0; } return cnt >= 3; } /* run of one char (spaces allowed): "==="-style setext underlines */ static int is_run(const char *s, size_t n, char c, int min) { int cnt = 0; for (size_t i = skip_indent(s, n); i < n; i++) { if (s[i] == c) cnt++; else if (!isspace((unsigned char)s[i])) return 0; } return cnt >= min; } /* " - item" / "1. item" -> list kind: 1 ul, 2 ol, 0 not-a-list */ static int list_item(const char *s, size_t n, size_t *body) { size_t i = skip_indent(s, n); if (i + 1 < n && (s[i] == '-' || s[i] == '*' || s[i] == '+') && s[i + 1] == ' ') { *body = i + 2; return 1; } size_t k = i; while (k < n && isdigit((unsigned char)s[k])) k++; if (k > i && k + 1 < n && s[k] == '.' && s[k + 1] == ' ') { *body = k + 2; return 2; } return 0; } void md_emit(struct buf *b, const char *data, size_t len) { struct buf para; /* pending paragraph lines */ int fence = 0, list = 0, quote = 0; /* open block state */ char fc = '`'; size_t flen = 0; /* opening fence run length */ buf_init(¶); size_t i = 0; while (i < len) { const char *ls = data + i; const char *le = memchr(ls, '\n', len - i); size_t ll = le ? (size_t)(le - ls) : len - i; i += ll + (le ? 1 : 0); while (ll && ls[ll - 1] == '\r') ll--; if (fence) { if (fence_close(ls, ll, fc, flen)) { buf_puts(b, "</code></pre>"); fence = 0; } else { buf_put_escaped(b, ls, ll); buf_puts(b, "\n"); } continue; } if ((flen = fence_run(ls, ll, &fc))) { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } if (para.len) { buf_printf(b, "<p>%s</p>", para.data); para.len = 0; } buf_puts(b, "<pre><code>"); fence = 1; continue; } int blank = 1; for (size_t k = 0; k < ll; k++) if (!isspace((unsigned char)ls[k])) { blank = 0; break; } if (blank) { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } if (para.len) { buf_printf(b, "<p>%s</p>", para.data); para.len = 0; } continue; } size_t ind = skip_indent(ls, ll); if (is_run(ls, ll, '=', 2) && para.len) { buf_printf(b, "<h1>%s</h1>", para.data); para.len = 0; continue; } if (is_run(ls, ll, '-', 3) && para.len) { buf_printf(b, "<h2>%s</h2>", para.data); para.len = 0; continue; } /* any non-continuation block start closes paragraph/list state */ int hd = 0; while (ind + (size_t)hd < ll && ls[ind + hd] == '#' && hd < 7) hd++; int is_h = hd >= 1 && hd <= 6 && (ind + (size_t)hd == ll || ls[ind + hd] == ' '); size_t ibody = 0; int lkind = list_item(ls, ll, &ibody); int is_q = ind < ll && ls[ind] == '>'; if (is_h || is_hr(ls, ll) || lkind || is_q) { if (para.len) { buf_printf(b, "<p>%s</p>", para.data); para.len = 0; } } if (is_h) { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } buf_printf(b, "<h%d>", hd); size_t t0 = ind + hd; while (t0 < ll && ls[t0] == ' ') t0++; inl(b, ls + t0, ll - t0, 0); buf_printf(b, "</h%d>", hd); } else if (is_hr(ls, ll)) { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } buf_puts(b, "<hr>"); } else if (lkind) { if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } if (list != lkind) { if (list) buf_puts(b, list == 1 ? "</ul>" : "</ol>"); buf_puts(b, lkind == 1 ? "<ul>" : "<ol>"); list = lkind; } buf_puts(b, "<li>"); inl(b, ls + ibody, ll - ibody, 0); buf_puts(b, "</li>"); } else if (is_q) { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (!quote) { buf_puts(b, "<blockquote><p>"); quote = 1; } else { buf_puts(b, "\n"); } size_t t0 = ind + 1; if (t0 < ll && ls[t0] == ' ') t0++; inl(b, ls + t0, ll - t0, 0); } else { if (list) { buf_puts(b, list == 1 ? "</ul>" : "</ol>"); list = 0; } if (quote) { buf_puts(b, "</p></blockquote>"); quote = 0; } if (para.len) buf_puts(¶, "\n"); inl(¶, ls + ind, ll - ind, 0); } } if (fence) buf_puts(b, "</code></pre>"); if (list) buf_puts(b, list == 1 ? "</ul>" : "</ol>"); if (quote) buf_puts(b, "</p></blockquote>"); if (para.len) buf_printf(b, "<p>%s</p>", para.data); buf_free(¶); }