Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_markdown.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
30#include "nilorea/n_markdown.h"
31
32#include <stdlib.h>
33#include <string.h>
34
35#define MD_MAX_BLOCKS 1024u /* blocks per document */
36#define MD_MAX_SPANS 2048u /* spans per inline flow */
37#define MD_MAX_ITEMS 1024u /* rows per list, cells per table */
38#define MD_MAX_DEPTH 16 /* bold/italic nesting */
39#define MD_MAX_COLS 32u /* columns per table */
40#define MD_MAX_HEADING 6 /* deepest "######" heading */
41
42/* ---- small helpers ------------------------------------------------------ */
43
44static int is_ws(char c) {
45 return c == ' ' || c == '\t' || c == '\r' || c == '\f' || c == '\v';
46}
47
48static int is_digit(char c) {
49 return c >= '0' && c <= '9';
50}
51
52/* Own a NUL-terminated copy of s[0..n); NULL on OOM (treated as truncation). */
53static char* dup_range(const char* s, size_t n) {
54 char* out = malloc(n + 1);
55 if (!out)
56 return NULL;
57 if (n)
58 memcpy(out, s, n);
59 out[n] = '\0';
60 return out;
61}
62
63/* A growable byte buffer for the pending plain-text run. */
64typedef struct {
65 char* b;
66 size_t len, cap;
67} tbuf;
68
69static int tbuf_add(tbuf* t, char c) {
70 if (t->len + 1 > t->cap) {
71 size_t nc = t->cap ? t->cap * 2 : 32;
72 char* nb = realloc(t->b, nc);
73 if (!nb)
74 return 0;
75 t->b = nb;
76 t->cap = nc;
77 }
78 t->b[t->len++] = c;
79 return 1;
80}
81
82/* ---- inline parsing ----------------------------------------------------- */
83
84/* Append an owned span (takes ownership of text/url). Drops it (freeing) on a
85 * cap overflow or a NULL text. Returns 1 if stored. */
86static int inl_push(n_markdown_inline* a, char* text, char* url, unsigned style) {
88 if (!text) {
89 free(url);
90 return 0;
91 }
92 if (a->n_spans >= MD_MAX_SPANS) {
93 free(text);
94 free(url);
95 return 0;
96 }
97 ns = realloc(a->spans, (a->n_spans + 1) * sizeof(*ns));
98 if (!ns) {
99 free(text);
100 free(url);
101 return 0;
102 }
103 a->spans = ns;
104 ns[a->n_spans].text = text;
105 ns[a->n_spans].url = url;
106 ns[a->n_spans].style = style;
107 a->n_spans++;
108 return 1;
109}
110
111/* Flush the pending plain text (if any) as one span with \p style. */
112static void flush_text(n_markdown_inline* a, tbuf* t, unsigned style) {
113 if (t->len == 0)
114 return;
115 (void)inl_push(a, dup_range(t->b, t->len), NULL, style);
116 t->len = 0;
117}
118
119/* Case-insensitive "http://" / "https://" prefix test on s[0..n). */
120static int is_http_url(const char* s, size_t n) {
121 static const char* h = "http";
122 size_t i = 0;
123 for (; i < 4; i++)
124 if (i >= n || (s[i] | 0x20) != h[i])
125 return 0;
126 if (i < n && (s[i] | 0x20) == 's')
127 i++;
128 return i + 3 <= n && s[i] == ':' && s[i + 1] == '/' && s[i + 2] == '/';
129}
130
131static void parse_inline(n_markdown_inline* a, const char* s, size_t n, unsigned inherited, int depth, unsigned flags);
132
133/* Try to match `code` at s[i]; on success emit the span and set *ni past it. */
134static int try_code(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
135 size_t j;
136 if (s[i] != '`')
137 return 0;
138 for (j = i + 1; j < n && s[j] != '`'; j++);
139 if (j >= n || j == i + 1) /* no closing backtick, or "``": literal */
140 return 0;
141 flush_text(a, t, inherited);
142 (void)inl_push(a, dup_range(s + i + 1, j - (i + 1)), NULL,
143 inherited | N_MARKDOWN_CODE);
144 *ni = j + 1;
145 return 1;
146}
147
148/* Try to match [text](url) at s[i]. */
149static int try_link(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
150 size_t close, url0, k;
151 if (s[i] != '[')
152 return 0;
153 for (close = i + 1; close < n && s[close] != ']'; close++);
154 if (close >= n || close + 1 >= n || s[close + 1] != '(')
155 return 0;
156 url0 = close + 2;
157 for (k = url0; k < n && s[k] != ')' && !is_ws(s[k]); k++);
158 if (k >= n || s[k] != ')' || k == url0) /* empty url or ws/EOF: no match */
159 return 0;
160 flush_text(a, t, inherited);
161 if (is_http_url(s + url0, k - url0)) {
162 (void)inl_push(a, dup_range(s + i + 1, close - (i + 1)),
163 dup_range(s + url0, k - url0),
164 inherited | N_MARKDOWN_LINK);
165 } else { /* non-http scheme: render the whole [..](..) literally */
166 (void)inl_push(a, dup_range(s + i, (k + 1) - i), NULL, inherited);
167 }
168 *ni = k + 1;
169 return 1;
170}
171
172/* A character that makes an underscore intraword: an ASCII letter or digit,
173 * or any byte of a multi-byte UTF-8 sequence (so "café_au_lait" counts). */
174static int md_word_char(unsigned char c) {
175 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
176 (c >= 'A' && c <= 'Z') || c >= 0x80;
177}
178
179/* Underscore emphasis does not open or close INSIDE a word (CommonMark):
180 * "snake_case_name" and ":throwing_kiss: :wrapped_present:" stay literal
181 * instead of italicising what lies between two of them. An opener at s[i]
182 * needs no word character before its run of underscores, a closer of length
183 * len at s[j] none after its run (so "a__b__c" is literal too). Asterisks
184 * are unaffected. */
185static int md_us_opens(const char* s, size_t i) {
186 while (i > 0 && s[i - 1] == '_')
187 i--;
188 return i == 0 || !md_word_char((unsigned char)s[i - 1]);
189}
190static int md_us_closes(const char* s, size_t n, size_t j, size_t len) {
191 size_t k = j + len;
192 while (k < n && s[k] == '_')
193 k++;
194 return k >= n || !md_word_char((unsigned char)s[k]);
195}
196
197/* Try bold or italic emphasis at s[i]; recurses on the inner span. */
198static int try_emphasis(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t* ni) {
199 char c = s[i];
200 size_t j;
201 if (c != '*' && c != '_')
202 return 0;
203 if (depth >= MD_MAX_DEPTH) /* too deep: the marker is literal text */
204 return 0;
205 if (c == '_' && !md_us_opens(s, i)) /* intraword: literal */
206 return 0;
207 if (i + 1 < n && s[i + 1] == c) { /* bold: find the next "cc" */
208 for (j = i + 2; j + 1 < n; j++)
209 if (s[j] == c && s[j + 1] == c) {
210 if (c == '_' && !md_us_closes(s, n, j, 2))
211 continue;
212 if (j == i + 2) /* "****" is not an empty bold */
213 break;
214 flush_text(a, t, inherited);
215 parse_inline(a, s + i + 2, j - (i + 2),
216 inherited | N_MARKDOWN_BOLD, depth + 1, flags);
217 *ni = j + 2;
218 return 1;
219 }
220 }
221 /* italic: find the next single c. An empty one ("**" left over from a
222 * bold that never closed) is not italic: it used to swallow both
223 * markers, so "an **unclosed run" lost its asterisks. */
224 for (j = i + 1; j < n; j++)
225 if (s[j] == c) {
226 if (c == '_' && j > i + 1 && !md_us_closes(s, n, j, 1))
227 continue;
228 if (j == i + 1)
229 return 0;
230 flush_text(a, t, inherited);
231 parse_inline(a, s + i + 1, j - (i + 1),
232 inherited | N_MARKDOWN_ITALIC, depth + 1, flags);
233 *ni = j + 1;
234 return 1;
235 }
236 return 0;
237}
238
239/* ~~strike~~ (N_MARKDOWN_STRIKETHROUGH): two tildes, then the next "~~".
240 * Recurses like bold, so ~~**both**~~ carries both styles. */
241static int try_strike(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t* ni) {
242 size_t j;
243 if (!(flags & N_MARKDOWN_STRIKETHROUGH) || s[i] != '~' || i + 1 >= n ||
244 s[i + 1] != '~' || depth >= MD_MAX_DEPTH)
245 return 0;
246 for (j = i + 2; j + 1 < n; j++)
247 if (s[j] == '~' && s[j + 1] == '~') {
248 if (j == i + 2) /* "~~~~" is not an empty strike */
249 return 0;
250 flush_text(a, t, inherited);
251 parse_inline(a, s + i + 2, j - (i + 2),
252 inherited | N_MARKDOWN_STRIKE, depth + 1, flags);
253 *ni = j + 2;
254 return 1;
255 }
256 return 0;
257}
258
259/* Where a bare URL starting at s[i] ends: at white space or a character a URL
260 * in running text does not carry, then less the trailing punctuation that
261 * belongs to the sentence ("see https://x.org." ends at the g) and a closing
262 * parenthesis with no opening one inside the URL ("(https://x.org)"). */
263static size_t url_end(const char* s, size_t n, size_t i) {
264 size_t e = i, k;
265 long depth = 0;
266 while (e < n && !is_ws(s[e]) && s[e] != '\n' && s[e] != '<' &&
267 s[e] != '>' && s[e] != '"' && s[e] != '`')
268 e++;
269 for (;;) {
270 char c;
271 if (e <= i)
272 return e;
273 c = s[e - 1];
274 if (c == '.' || c == ',' || c == ';' || c == ':' || c == '!' ||
275 c == '?' || c == '\'' || c == '"' || c == '*' || c == '_' ||
276 c == '~') {
277 e--;
278 continue;
279 }
280 if (c == ')') {
281 depth = 0;
282 for (k = i; k < e; k++)
283 depth += s[k] == '(' ? 1 : s[k] == ')' ? -1
284 : 0;
285 if (depth < 0) {
286 e--;
287 continue;
288 }
289 }
290 return e;
291 }
292}
293
294/* Bare http(s) URLs and <http(s)://...> (N_MARKDOWN_AUTOLINKS): the URL is
295 * both the text and the link. A bare one starts only at a word boundary, so
296 * "xhttp://" is text, and needs something after "://". */
297static int try_autolink(n_markdown_inline* a, tbuf* t, const char* s, size_t n, size_t i, unsigned inherited, size_t* ni) {
298 size_t e;
299 if (s[i] == '<') {
300 size_t k, u0;
301 if (!is_http_url(s + i + 1, n - (i + 1)))
302 return 0;
303 for (k = i + 1; k < n && s[k] != '>' && !is_ws(s[k]) && s[k] != '\n'; k++);
304 if (k >= n || s[k] != '>')
305 return 0;
306 u0 = i + 1;
307 e = k;
308 if (e - u0 <= (((s[u0 + 4] | 0x20) == 's') ? 8u : 7u))
309 return 0; /* nothing after the "://" */
310 flush_text(a, t, inherited);
311 (void)inl_push(a, dup_range(s + u0, e - u0), dup_range(s + u0, e - u0),
312 inherited | N_MARKDOWN_LINK);
313 *ni = k + 1;
314 return 1;
315 }
316 if ((s[i] | 0x20) != 'h' || !is_http_url(s + i, n - i))
317 return 0;
318 if (i > 0 && (is_digit(s[i - 1]) || ((s[i - 1] | 0x20) >= 'a' && (s[i - 1] | 0x20) <= 'z') ||
319 s[i - 1] == '/' || (unsigned char)s[i - 1] >= 0x80))
320 return 0;
321 e = url_end(s, n, i);
322 {
323 size_t scheme = ((s[i + 4] | 0x20) == 's') ? 8 : 7; /* https:// */
324 if (e <= i + scheme)
325 return 0; /* nothing after the "://" */
326 }
327 flush_text(a, t, inherited);
328 (void)inl_push(a, dup_range(s + i, e - i), dup_range(s + i, e - i),
329 inherited | N_MARKDOWN_LINK);
330 *ni = e;
331 return 1;
332}
333
334/* A backslash before ASCII punctuation (N_MARKDOWN_ESCAPES) is that character,
335 * literally: "\\*not italic\\*". A backslash before anything else is a
336 * backslash. */
337static int is_md_punct(char c) {
338 return (c >= '!' && c <= '/') || (c >= ':' && c <= '@') ||
339 (c >= '[' && c <= '`') || (c >= '{' && c <= '~');
340}
341
342/* Parse s[0..n) into inline spans appended to \p a, each carrying \p inherited
343 * plus whatever emphasis/code/link applies. \p flags are the parse_ex flags;
344 * only the inline ones matter here. */
345static void parse_inline(n_markdown_inline* a, const char* s, size_t n, unsigned inherited, int depth, unsigned flags) {
346 tbuf t = {0};
347 size_t i = 0, ni;
348 while (i < n) {
349 if ((flags & N_MARKDOWN_ESCAPES) && s[i] == '\\' && i + 1 < n &&
350 is_md_punct(s[i + 1])) {
351 if (!tbuf_add(&t, s[i + 1]))
352 break;
353 i += 2;
354 continue;
355 }
356 if (try_code(a, &t, s, n, i, inherited, &ni) ||
357 try_link(a, &t, s, n, i, inherited, &ni) ||
358 ((flags & N_MARKDOWN_AUTOLINKS) &&
359 try_autolink(a, &t, s, n, i, inherited, &ni)) ||
360 try_strike(a, &t, s, n, i, inherited, depth, flags, &ni) ||
361 try_emphasis(a, &t, s, n, i, inherited, depth, flags, &ni)) {
362 i = ni;
363 continue;
364 }
365 if (!tbuf_add(&t, s[i])) /* OOM: stop, keep what we have */
366 break;
367 i++;
368 }
369 flush_text(a, &t, inherited);
370 free(t.b);
371}
372
373/* ---- block parsing ------------------------------------------------------ */
374
375/* One line = {ptr, len} into the normalized buffer. */
376typedef struct {
377 const char* p;
378 size_t n;
379} mline;
380
381static int line_blank(const char* s, size_t n) {
382 size_t i;
383 for (i = 0; i < n; i++)
384 if (!is_ws(s[i]) && s[i] != '\n')
385 return 0;
386 return 1;
387}
388
389static int starts_fence(const char* s, size_t n) {
390 return n >= 3 && s[0] == '`' && s[1] == '`' && s[2] == '`';
391}
392
393/* > blockquote: on match, set *off to the start of the content (past "> "). */
394static int match_quote(const char* s, size_t n, size_t* off) {
395 if (n == 0 || s[0] != '>')
396 return 0;
397 *off = 1;
398 if (*off < n && is_ws(s[*off]))
399 (*off)++;
400 return 1;
401}
402
403/* "- " / "* " list row: set *off past the marker and its following space(s). */
404static int match_ul(const char* s, size_t n, size_t* off) {
405 if (n < 2 || (s[0] != '-' && s[0] != '*') || !is_ws(s[1]))
406 return 0;
407 *off = 1;
408 while (*off < n && is_ws(s[*off]))
409 (*off)++;
410 return 1;
411}
412
413/* "1. " list row: set *off past the digits, dot and following space(s). */
414static int match_ol(const char* s, size_t n, size_t* off) {
415 size_t i = 0;
416 while (i < n && is_digit(s[i]))
417 i++;
418 if (i == 0 || i >= n || s[i] != '.')
419 return 0;
420 i++;
421 if (i >= n || !is_ws(s[i]))
422 return 0;
423 while (i < n && is_ws(s[i]))
424 i++;
425 *off = i;
426 return 1;
427}
428
429/* "## " heading: one to six hashes, then a space, then the text. Without the
430 * space it is not a heading, which is what keeps a hash tag out. */
431static int match_heading(const char* s, size_t n, int* level, size_t* off) {
432 size_t i = 0, h = 0;
433 while (i + h < n && s[i + h] == '#')
434 h++;
435 if (h == 0 || h > (size_t)MD_MAX_HEADING)
436 return 0;
437 i += h;
438 if (i >= n || s[i] != ' ')
439 return 0;
440 while (i < n && is_ws(s[i]))
441 i++;
442 *level = (int)h;
443 *off = i;
444 return 1;
445}
446
447/* The cells of one table row, as [a, b) bounds into the line. */
448typedef struct {
449 size_t a[MD_MAX_COLS];
450 size_t b[MD_MAX_COLS];
451 size_t n;
452} md_cells;
453
454/* Split a table row on its pipes. A leading and a trailing pipe are both
455 * optional, which is what GitHub accepts and what the documentation in this
456 * tree is written with; dropping the trailing one here is what stops the blank
457 * after it counting as an extra column. There is no escape for a pipe inside a
458 * cell, because the subset has no backslash escapes at all and inventing one
459 * here alone would be a rule nothing else in the parser follows. */
460static void row_cells(const char* s, size_t n, md_cells* out) {
461 size_t i = 0;
462 out->n = 0;
463 while (i < n && is_ws(s[i]))
464 i++;
465 if (i < n && s[i] == '|')
466 i++;
467 while (n > i && is_ws(s[n - 1]))
468 n--;
469 if (n > i && s[n - 1] == '|')
470 n--;
471 for (;;) {
472 size_t j = i;
473 while (j < n && s[j] != '|')
474 j++;
475 if (out->n >= MD_MAX_COLS)
476 return;
477 out->a[out->n] = i;
478 out->b[out->n] = j;
479 out->n++;
480 if (j >= n)
481 return;
482 i = j + 1;
483 }
484}
485
486/* Is this the "|---|:-:|" row under a table's header? Every cell has to be
487 * dashes, with an optional colon at either end and at least one dash. That is
488 * the whole test, and it is what tells a table apart from a paragraph that
489 * happens to contain pipes. Split by row_cells, so the rule row and the header
490 * row can never disagree about how many columns there are. */
491static int is_table_rule(const char* s, size_t n) {
492 md_cells c;
493 size_t k;
494 row_cells(s, n, &c);
495 if (c.n == 0)
496 return 0;
497 for (k = 0; k < c.n; k++) {
498 size_t a = c.a[k], b = c.b[k];
499 while (a < b && is_ws(s[a]))
500 a++;
501 while (b > a && is_ws(s[b - 1]))
502 b--;
503 if (a < b && s[a] == ':')
504 a++;
505 if (b > a && s[b - 1] == ':')
506 b--;
507 if (a >= b)
508 return 0;
509 while (a < b) {
510 if (s[a] != '-')
511 return 0;
512 a++;
513 }
514 }
515 return 1;
516}
517
518/* Alignment asked for by one cell of the rule row. */
519static unsigned char rule_align(const char* s, size_t a, size_t b) {
520 int left, right;
521 while (a < b && is_ws(s[a]))
522 a++;
523 while (b > a && is_ws(s[b - 1]))
524 b--;
525 left = (a < b && s[a] == ':');
526 right = (b > a && s[b - 1] == ':');
527 if (left && right)
529 if (right)
532}
533
534/* Does a line hold a cell separator at all: the cheap half of "is this a
535 * table", asked of the header row before the rule row below it is looked at. */
536static int has_pipe(const char* s, size_t n) {
537 size_t i;
538 for (i = 0; i < n; i++)
539 if (s[i] == '|')
540 return 1;
541 return 0;
542}
543
544/* Does a table start at line @p li: a row with a pipe in it, and the rule row
545 * underneath. Both halves are needed. A paragraph may well contain a pipe, and
546 * a line that has one decides nothing on its own. */
547static int starts_table(const mline* lines, size_t li, size_t nlines) {
548 return has_pipe(lines[li].p, lines[li].n) && li + 1 < nlines &&
549 is_table_rule(lines[li + 1].p, lines[li + 1].n);
550}
551
552/* Would line @p li start a block of its own rather than continue a paragraph?
553 *
554 * Takes the whole array because a table cannot be recognised from one line,
555 * and @p flags because a heading or a table is only a block when it was asked
556 * for: without the flag those lines are ordinary text and have to keep being
557 * swallowed by the paragraph around them, exactly as they were before the
558 * flags existed. */
559static int line_starts_block(const mline* lines, size_t li, size_t nlines, unsigned flags) {
560 const char* s = lines[li].p;
561 size_t n = lines[li].n, off;
562 int level;
563 if ((flags & N_MARKDOWN_HEADINGS) && match_heading(s, n, &level, &off))
564 return 1;
565 if ((flags & N_MARKDOWN_TABLES) && starts_table(lines, li, nlines))
566 return 1;
567 return starts_fence(s, n) || match_quote(s, n, &off) ||
568 match_ul(s, n, &off) || match_ol(s, n, &off);
569}
570
571/* Append a block; returns a pointer to it, or NULL on a cap/OOM. */
574 if (d->n_blocks >= MD_MAX_BLOCKS)
575 return NULL;
576 nb = realloc(d->blocks, (d->n_blocks + 1) * sizeof(*nb));
577 if (!nb)
578 return NULL;
579 d->blocks = nb;
580 memset(&nb[d->n_blocks], 0, sizeof(nb[d->n_blocks]));
581 nb[d->n_blocks].kind = kind;
582 return &d->blocks[d->n_blocks++];
583}
584
585/* Join lines [a..b) with '\n' into an owned buffer, applying \p strip to each
586 * line to skip a prefix (quote/list marker). \p strip may be NULL. */
587typedef size_t (*strip_fn)(const char* s, size_t n);
588
589static char* join_lines(const mline* lines, size_t a, size_t b, strip_fn strip, size_t* out_len) {
590 size_t total = 0, i, w = 0;
591 char* buf;
592 for (i = a; i < b; i++) {
593 size_t off = strip ? strip(lines[i].p, lines[i].n) : 0;
594 total += lines[i].n - off + (i + 1 < b ? 1 : 0);
595 }
596 buf = malloc(total + 1);
597 if (!buf) {
598 *out_len = 0;
599 return NULL;
600 }
601 for (i = a; i < b; i++) {
602 size_t off = strip ? strip(lines[i].p, lines[i].n) : 0;
603 size_t seg = lines[i].n - off;
604 memcpy(buf + w, lines[i].p + off, seg);
605 w += seg;
606 if (i + 1 < b)
607 buf[w++] = '\n';
608 }
609 buf[w] = '\0';
610 *out_len = w;
611 return buf;
612}
613
614static size_t strip_quote(const char* s, size_t n) {
615 size_t off;
616 return match_quote(s, n, &off) ? off : 0;
617}
618
619/* Is this line indented under the marker above it? A wrapped list item is
620 * written that way, and GitHub calls taking it as part of the item above a
621 * lazy continuation. Without it a list item long enough to wrap ends its own
622 * list and the rest of the sentence becomes a paragraph. */
623static int line_indented(const char* s, size_t n) {
624 return n > 0 && is_ws(s[0]) && !line_blank(s, n);
625}
626
627/* Add one table cell (parse its inline spans). Cells are appended row by row,
628 * so the array stays row-major with blk->n_cols to a row. */
629static int table_add_cell(n_markdown_block* blk, const char* s, size_t a, size_t b, unsigned flags) {
631 if (blk->n_items >= MD_MAX_ITEMS)
632 return 0;
633 while (a < b && is_ws(s[a]))
634 a++;
635 while (b > a && is_ws(s[b - 1]))
636 b--;
637 ni = realloc(blk->items, (blk->n_items + 1) * sizeof(*ni));
638 if (!ni)
639 return 0;
640 blk->items = ni;
641 memset(&ni[blk->n_items], 0, sizeof(ni[blk->n_items]));
642 parse_inline(&blk->items[blk->n_items], s + a, b - a, 0, 0, flags);
643 blk->n_items++;
644 return 1;
645}
646
647/* One row of cells, padded or truncated to blk->n_cols so that every row is
648 * the same length and cell (r, c) is always items[r * n_cols + c]. A row of a
649 * hand-written table is short or long often enough that a renderer should not
650 * have to ask. */
651static void table_add_row(n_markdown_block* blk, const char* s, const md_cells* c, unsigned flags) {
652 size_t k;
653 for (k = 0; k < blk->n_cols; k++) {
654 if (k < c->n) {
655 if (!table_add_cell(blk, s, c->a[k], c->b[k], flags))
656 return;
657 } else if (!table_add_cell(blk, s, 0, 0, flags)) {
658 return;
659 }
660 }
661}
662
663/* Add one list item that may run over several lines: the marker is stripped
664 * from the first and the indent from the rest, and the lines are joined with a
665 * space rather than a newline, because they are one sentence that the renderer
666 * will wrap to whatever width it has. */
667static int list_add_wrapped(n_markdown_block* blk, const mline* lines, size_t a, size_t b, size_t off, unsigned flags) {
669 char* buf;
670 size_t total = 0, i, w = 0;
671
672 if (blk->n_items >= MD_MAX_ITEMS)
673 return 0;
674 for (i = a; i < b; i++) {
675 size_t skip = (i == a) ? off : 0;
676 while (i != a && skip < lines[i].n && is_ws(lines[i].p[skip]))
677 skip++;
678 total += lines[i].n - skip + 1;
679 }
680 buf = malloc(total + 1);
681 if (!buf)
682 return 0;
683 for (i = a; i < b; i++) {
684 size_t skip = (i == a) ? off : 0;
685 while (i != a && skip < lines[i].n && is_ws(lines[i].p[skip]))
686 skip++;
687 if (w && lines[i].n > skip)
688 buf[w++] = ' ';
689 memcpy(buf + w, lines[i].p + skip, lines[i].n - skip);
690 w += lines[i].n - skip;
691 }
692 buf[w] = '\0';
693 ni = realloc(blk->items, (blk->n_items + 1) * sizeof(*ni));
694 if (!ni) {
695 free(buf);
696 return 0;
697 }
698 blk->items = ni;
699 memset(&ni[blk->n_items], 0, sizeof(ni[blk->n_items]));
700 parse_inline(&blk->items[blk->n_items], buf, w, 0, 0, flags);
701 blk->n_items++;
702 free(buf);
703 return 1;
704}
705
706void n_markdown_parse(const char* text, size_t len, n_markdown_doc* out) {
707 n_markdown_parse_ex(text, len, 0, out);
708}
709
710void n_markdown_parse_ex(const char* text, size_t len, unsigned flags, n_markdown_doc* out) {
711 char* norm;
712 mline* lines = NULL;
713 size_t nlines = 0, i, li, start;
714
715 memset(out, 0, sizeof(*out));
716 if (!text)
717 return;
718
719 /* Normalize CRLF / lone CR to LF. */
720 norm = malloc(len + 1);
721 if (!norm)
722 return;
723 {
724 size_t w = 0;
725 for (i = 0; i < len; i++) {
726 if (text[i] == '\r') {
727 norm[w++] = '\n';
728 if (i + 1 < len && text[i + 1] == '\n')
729 i++;
730 } else {
731 norm[w++] = text[i];
732 }
733 }
734 norm[w] = '\0';
735 len = w;
736 }
737
738 /* Split into lines. */
739 lines = malloc((len + 1) * sizeof(*lines));
740 if (!lines) {
741 free(norm);
742 return;
743 }
744 start = 0;
745 for (i = 0; i <= len; i++) {
746 if (i == len || norm[i] == '\n') {
747 lines[nlines].p = norm + start;
748 lines[nlines].n = i - start;
749 nlines++;
750 start = i + 1;
751 }
752 }
753
754 li = 0;
755 while (li < nlines) {
756 const char* s = lines[li].p;
757 size_t n = lines[li].n, off;
758 n_markdown_block* blk;
759
760 if (line_blank(s, n)) {
761 li++;
762 continue;
763 }
764 if (flags & N_MARKDOWN_HEADINGS) { /* "## " heading */
765 int level = 0;
766 if (match_heading(s, n, &level, &off)) {
767 blk = doc_push(out, N_MARKDOWN_HEADING);
768 if (blk) {
769 blk->level = level;
770 parse_inline(&blk->inl, s + off, n - off, 0, 0, flags);
771 }
772 li++;
773 continue;
774 }
775 }
776 if ((flags & N_MARKDOWN_TABLES) && starts_table(lines, li, nlines)) { /* pipe table */
777 md_cells head, rule;
778
779 row_cells(s, n, &head);
780 row_cells(lines[li + 1].p, lines[li + 1].n, &rule);
781 blk = doc_push(out, N_MARKDOWN_TABLE);
782 if (!blk) {
783 /* out of blocks: skip the whole table rather than leave its
784 rows to be read as paragraphs */
785 li += 2;
786 while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
787 has_pipe(lines[li].p, lines[li].n) &&
788 !line_starts_block(lines, li, nlines, flags))
789 li++;
790 continue;
791 }
792 /* The header decides the width. A rule row of a different length
793 is a malformed table, and taking the header's count keeps the
794 cells and the alignments in step whichever way it differs. */
795 blk->n_cols = head.n ? head.n : 1;
796 blk->align = malloc(blk->n_cols);
797 if (blk->align) {
798 size_t k;
799 for (k = 0; k < blk->n_cols; k++)
800 blk->align[k] = k < rule.n
801 ? rule_align(lines[li + 1].p, rule.a[k], rule.b[k])
802 : (unsigned char)N_MARKDOWN_ALIGN_LEFT;
803 }
804 table_add_row(blk, s, &head, flags);
805 li += 2;
806 /* a row is a non-blank line with a pipe that does not start a
807 block of its own: "# x | y" or "- a|b" after a table used to be
808 taken as rows */
809 while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
810 has_pipe(lines[li].p, lines[li].n) &&
811 !line_starts_block(lines, li, nlines, flags)) {
812 md_cells body;
813 row_cells(lines[li].p, lines[li].n, &body);
814 table_add_row(blk, lines[li].p, &body, flags);
815 li++;
816 }
817 continue;
818 }
819 if (starts_fence(s, n)) { /* ``` fenced code block */
820 size_t body_a, body_b;
821 char* code;
822 size_t clen;
823 li++;
824 body_a = li;
825 while (li < nlines && !starts_fence(lines[li].p, lines[li].n))
826 li++;
827 body_b = li;
828 if (li < nlines)
829 li++; /* closing fence */
830 code = join_lines(lines, body_a, body_b, NULL, &clen);
832 if (blk)
833 blk->code = code ? code : dup_range("", 0);
834 else
835 free(code);
836 continue;
837 }
838 if (match_quote(s, n, &off)) { /* > blockquote */
839 size_t a = li, joined_len;
840 char* joined;
841 while (li < nlines && match_quote(lines[li].p, lines[li].n, &off))
842 li++;
843 joined = join_lines(lines, a, li, strip_quote, &joined_len);
844 blk = doc_push(out, N_MARKDOWN_QUOTE);
845 if (blk && joined)
846 parse_inline(&blk->inl, joined, joined_len, 0, 0, flags);
847 free(joined);
848 continue;
849 }
850 if (match_ul(s, n, &off)) { /* unordered list */
851 blk = doc_push(out, N_MARKDOWN_UL);
852 while (li < nlines && match_ul(lines[li].p, lines[li].n, &off)) {
853 size_t item_a = li;
854 /* off is the marker width of THIS item, set by the match in
855 the condition above; nothing below writes to it, so the
856 continuation scan can run before it is used. */
857 li++;
858 while (li < nlines && line_indented(lines[li].p, lines[li].n) &&
859 !line_starts_block(lines, li, nlines, flags))
860 li++;
861 if (blk)
862 (void)list_add_wrapped(blk, lines, item_a, li, off, flags);
863 }
864 continue;
865 }
866 if (match_ol(s, n, &off)) { /* ordered list */
867 blk = doc_push(out, N_MARKDOWN_OL);
868 while (li < nlines && match_ol(lines[li].p, lines[li].n, &off)) {
869 size_t item_a = li;
870 /* off is the marker width of THIS item, set by the match in
871 the condition above; nothing below writes to it, so the
872 continuation scan can run before it is used. */
873 li++;
874 while (li < nlines && line_indented(lines[li].p, lines[li].n) &&
875 !line_starts_block(lines, li, nlines, flags))
876 li++;
877 if (blk)
878 (void)list_add_wrapped(blk, lines, item_a, li, off, flags);
879 }
880 continue;
881 }
882 { /* paragraph: consecutive non-blank, non-special lines */
883 size_t a = li, joined_len;
884 char* joined;
885 /* The first line is taken whatever it is. Every block start is
886 tested above, so a line reaching here is paragraph text by
887 elimination -- and consuming it unconditionally is what
888 guarantees this loop advances, whatever a future matcher
889 decides. A predicate that says "block starts here" about a line
890 the branches above declined would otherwise spin. */
891 li++;
892 while (li < nlines && !line_blank(lines[li].p, lines[li].n) &&
893 !line_starts_block(lines, li, nlines, flags))
894 li++;
895 joined = join_lines(lines, a, li, NULL, &joined_len);
896 blk = doc_push(out, N_MARKDOWN_PARA);
897 if (blk && joined)
898 parse_inline(&blk->inl, joined, joined_len, 0, 0, flags);
899 free(joined);
900 }
901 }
902
903 free(lines);
904 free(norm);
905}
906
908 size_t i;
909 for (i = 0; i < inl->n_spans; i++) {
910 free(inl->spans[i].text);
911 free(inl->spans[i].url);
912 }
913 free(inl->spans);
914 inl->spans = NULL;
915 inl->n_spans = 0;
916}
917
919 size_t i, j;
920 if (!doc || !doc->blocks)
921 return;
922 for (i = 0; i < doc->n_blocks; i++) {
923 n_markdown_block* b = &doc->blocks[i];
924 free_inline(&b->inl);
925 free(b->code);
926 for (j = 0; j < b->n_items; j++)
927 free_inline(&b->items[j]);
928 free(b->items);
929 free(b->align);
930 }
931 free(doc->blocks);
932 doc->blocks = NULL;
933 doc->n_blocks = 0;
934}
935
936int n_markdown_has_markup(const char* text, size_t len) {
937 return n_markdown_has_markup_ex(text, len, 0);
938}
939
940int n_markdown_has_markup_ex(const char* text, size_t len, unsigned flags) {
942 int markup = 0;
943 n_markdown_parse_ex(text, len, flags, &d);
944 if (d.n_blocks > 1) {
945 markup = 1;
946 } else if (d.n_blocks == 1) {
947 const n_markdown_block* b = &d.blocks[0];
948 if (b->kind != N_MARKDOWN_PARA) {
949 markup = 1;
950 } else {
951 size_t i;
952 for (i = 0; i < b->inl.n_spans; i++)
953 if (b->inl.spans[i].style != 0) {
954 markup = 1;
955 break;
956 }
957 }
958 }
959 n_markdown_free(&d);
960 if (!markup && (flags & N_MARKDOWN_ESCAPES) && text) {
961 size_t i;
962 for (i = 0; i + 1 < len; i++)
963 if (text[i] == '\\' && is_md_punct(text[i + 1]))
964 return 1; /* the plain path would show the backslash */
965 }
966 return markup;
967}
static void right(N_GUI_CTX *gui, float cx, float cy)
right press
size_t len
Definition n_markdown.c:66
static int try_emphasis(n_markdown_inline *a, tbuf *t, const char *s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t *ni)
Definition n_markdown.c:198
#define MD_MAX_HEADING
Definition n_markdown.c:40
static int is_md_punct(char c)
Definition n_markdown.c:337
#define MD_MAX_BLOCKS
Definition n_markdown.c:35
static int is_digit(char c)
Definition n_markdown.c:48
static int starts_fence(const char *s, size_t n)
Definition n_markdown.c:389
static int try_link(n_markdown_inline *a, tbuf *t, const char *s, size_t n, size_t i, unsigned inherited, size_t *ni)
Definition n_markdown.c:149
size_t n
Definition n_markdown.c:378
const char * p
Definition n_markdown.c:377
static int starts_table(const mline *lines, size_t li, size_t nlines)
Definition n_markdown.c:547
static int md_us_closes(const char *s, size_t n, size_t j, size_t len)
Definition n_markdown.c:190
static int tbuf_add(tbuf *t, char c)
Definition n_markdown.c:69
static void flush_text(n_markdown_inline *a, tbuf *t, unsigned style)
Definition n_markdown.c:112
size_t(* strip_fn)(const char *s, size_t n)
Definition n_markdown.c:587
static size_t url_end(const char *s, size_t n, size_t i)
Definition n_markdown.c:263
static void row_cells(const char *s, size_t n, md_cells *out)
Definition n_markdown.c:460
static int try_autolink(n_markdown_inline *a, tbuf *t, const char *s, size_t n, size_t i, unsigned inherited, size_t *ni)
Definition n_markdown.c:297
static int line_blank(const char *s, size_t n)
Definition n_markdown.c:381
static int inl_push(n_markdown_inline *a, char *text, char *url, unsigned style)
Definition n_markdown.c:86
static int list_add_wrapped(n_markdown_block *blk, const mline *lines, size_t a, size_t b, size_t off, unsigned flags)
Definition n_markdown.c:667
void n_markdown_free(n_markdown_doc *doc)
free everything a n_markdown_doc owns and zero it
Definition n_markdown.c:918
static void table_add_row(n_markdown_block *blk, const char *s, const md_cells *c, unsigned flags)
Definition n_markdown.c:651
#define MD_MAX_COLS
Definition n_markdown.c:39
size_t b[32u]
Definition n_markdown.c:450
#define MD_MAX_ITEMS
Definition n_markdown.c:37
static char * dup_range(const char *s, size_t n)
Definition n_markdown.c:53
static int is_ws(char c)
Definition n_markdown.c:44
static int is_http_url(const char *s, size_t n)
Definition n_markdown.c:120
static int match_quote(const char *s, size_t n, size_t *off)
Definition n_markdown.c:394
static int md_word_char(unsigned char c)
Definition n_markdown.c:174
#define MD_MAX_SPANS
Definition n_markdown.c:36
static unsigned char rule_align(const char *s, size_t a, size_t b)
Definition n_markdown.c:519
static size_t strip_quote(const char *s, size_t n)
Definition n_markdown.c:614
size_t cap
Definition n_markdown.c:66
static int has_pipe(const char *s, size_t n)
Definition n_markdown.c:536
static int match_heading(const char *s, size_t n, int *level, size_t *off)
Definition n_markdown.c:431
static int try_strike(n_markdown_inline *a, tbuf *t, const char *s, size_t n, size_t i, unsigned inherited, int depth, unsigned flags, size_t *ni)
Definition n_markdown.c:241
static int line_starts_block(const mline *lines, size_t li, size_t nlines, unsigned flags)
Definition n_markdown.c:559
int n_markdown_has_markup_ex(const char *text, size_t len, unsigned flags)
1 if text contains any markup the given flags would render, else 0
Definition n_markdown.c:940
void n_markdown_parse(const char *text, size_t len, n_markdown_doc *out)
parse text (len bytes, plain UTF-8) into out
Definition n_markdown.c:706
static int match_ul(const char *s, size_t n, size_t *off)
Definition n_markdown.c:404
static int line_indented(const char *s, size_t n)
Definition n_markdown.c:623
static int md_us_opens(const char *s, size_t i)
Definition n_markdown.c:185
void n_markdown_parse_ex(const char *text, size_t len, unsigned flags, n_markdown_doc *out)
parse text into out, choosing which optional block kinds to recognise
Definition n_markdown.c:710
static int match_ol(const char *s, size_t n, size_t *off)
Definition n_markdown.c:414
static n_markdown_block * doc_push(n_markdown_doc *d, int kind)
Definition n_markdown.c:572
static char * join_lines(const mline *lines, size_t a, size_t b, strip_fn strip, size_t *out_len)
Definition n_markdown.c:589
static int try_code(n_markdown_inline *a, tbuf *t, const char *s, size_t n, size_t i, unsigned inherited, size_t *ni)
Definition n_markdown.c:134
#define MD_MAX_DEPTH
Definition n_markdown.c:38
static int is_table_rule(const char *s, size_t n)
Definition n_markdown.c:491
char * b
Definition n_markdown.c:65
size_t n
Definition n_markdown.c:451
static void free_inline(n_markdown_inline *inl)
Definition n_markdown.c:907
int n_markdown_has_markup(const char *text, size_t len)
1 if text contains any renderable markup, else 0
Definition n_markdown.c:936
size_t a[32u]
Definition n_markdown.c:449
static void parse_inline(n_markdown_inline *a, const char *s, size_t n, unsigned inherited, int depth, unsigned flags)
Definition n_markdown.c:345
static int table_add_cell(n_markdown_block *blk, const char *s, size_t a, size_t b, unsigned flags)
Definition n_markdown.c:629
Parse a SAFE, strict subset of Markdown into a block / styled-span model.
#define N_MARKDOWN_CODE
inline code style flag (carries no emphasis)
Definition n_markdown.h:67
#define N_MARKDOWN_ESCAPES
n_markdown_parse_ex flag: a backslash before ASCII punctuation makes that character literal ("\\*not ...
Definition n_markdown.h:86
#define N_MARKDOWN_ALIGN_CENTER
what "|:---:|" asks for.
Definition n_markdown.h:98
#define N_MARKDOWN_ALIGN_RIGHT
what "|---:|" asks for.
Definition n_markdown.h:100
#define N_MARKDOWN_ITALIC
italic inline style flag
Definition n_markdown.h:65
#define N_MARKDOWN_LINK
[text](http(s)://url) style flag; url is set only when this is present
Definition n_markdown.h:69
#define N_MARKDOWN_BOLD
bold inline style flag
Definition n_markdown.h:63
n_markdown_span * spans
Definition n_markdown.h:125
n_markdown_inline * items
Definition n_markdown.h:142
n_markdown_inline inl
Definition n_markdown.h:140
#define N_MARKDOWN_TABLES
n_markdown_parse_ex flag: recognise GitHub-style pipe tables.
Definition n_markdown.h:76
@ N_MARKDOWN_HEADING
"## " heading (inl, level)
Definition n_markdown.h:110
@ N_MARKDOWN_OL
ordered list (items)
Definition n_markdown.h:109
@ N_MARKDOWN_QUOTE
Definition n_markdown.h:106
@ N_MARKDOWN_TABLE
pipe table (items, n_cols)
Definition n_markdown.h:111
@ N_MARKDOWN_UL
unordered list (items)
Definition n_markdown.h:108
@ N_MARKDOWN_CODE_BLOCK
``` fenced code (raw code)
Definition n_markdown.h:107
@ N_MARKDOWN_PARA
paragraph
Definition n_markdown.h:105
#define N_MARKDOWN_STRIKE
strike inline style flag (only with N_MARKDOWN_STRIKETHROUGH)
Definition n_markdown.h:71
#define N_MARKDOWN_STRIKETHROUGH
n_markdown_parse_ex flag: "~~text~~" carries N_MARKDOWN_STRIKE.
Definition n_markdown.h:83
#define N_MARKDOWN_AUTOLINKS
n_markdown_parse_ex flag: a bare http(s) URL, or one in <angle brackets>, is a link whose text is the...
Definition n_markdown.h:81
#define N_MARKDOWN_HEADINGS
n_markdown_parse_ex flag: recognise "# " to "###### " headings.
Definition n_markdown.h:74
int kind
enum n_markdown_block_kind
Definition n_markdown.h:139
unsigned char * align
TABLE: N_MARKDOWN_ALIGN_* per column, n_cols entries, owned.
Definition n_markdown.h:150
#define N_MARKDOWN_ALIGN_LEFT
table column alignment: the default, and what "|:---|" asks for.
Definition n_markdown.h:96
n_markdown_block * blocks
Definition n_markdown.h:155
size_t n_cols
TABLE: cells per row, always at least 1.
Definition n_markdown.h:147
int level
HEADING: 1 to 6.
Definition n_markdown.h:145
A parsed document: an owned array of blocks.
Definition n_markdown.h:154
One flow of inline runs (a paragraph, a quote, or one list item).
Definition n_markdown.h:124
One inline run: visible text, a style bitmask, and (for a link) an already-http(s)-validated url.
Definition n_markdown.h:117