Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_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
27
28#include "nilorea/n_log.h"
29#include "nilorea/n_markdown.h"
30
31#include <string.h>
32
33static int failures = 0;
34
35#define CHECK(cond, msg) \
36 do { \
37 if (!(cond)) { \
38 n_log(LOG_ERR, "FAIL: %s", (msg)); \
39 failures++; \
40 } \
41 } while (0)
42
43/* Parse a NUL-terminated literal and return the doc. */
44static n_markdown_doc parse(const char* s) {
46 n_markdown_parse(s, strlen(s), &d);
47 return d;
48}
49
50/* The same, with the optional block kinds asked for. */
51static n_markdown_doc parse_ex(const char* s, unsigned flags) {
53 n_markdown_parse_ex(s, strlen(s), flags, &d);
54 return d;
55}
56
57/* The text of one inline flow, concatenated; the caller frees nothing. */
58static void flow_text(const n_markdown_inline* inl, char* out, size_t cap) {
59 size_t i, w = 0;
60 out[0] = '\0';
61 for (i = 0; i < inl->n_spans; i++) {
62 const char* t = inl->spans[i].text;
63 size_t n = t ? strlen(t) : 0;
64 if (w + n + 1 > cap)
65 break;
66 memcpy(out + w, t, n);
67 w += n;
68 out[w] = '\0';
69 }
70}
71
72/* Cell (row, col) of a table block, as text. */
73static void cell_text(const n_markdown_block* b, size_t row, size_t col, char* out, size_t cap) {
74 size_t at = row * b->n_cols + col;
75 out[0] = '\0';
76 if (!b->n_cols || at >= b->n_items)
77 return;
78 flow_text(&b->items[at], out, cap);
79}
80
81int main(void) {
83
84 /* inline styles fold into per-span flags */
85 {
86 n_markdown_doc d = parse("a **b** c *i* `k` end");
87 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_PARA, "para");
88 if (d.n_blocks == 1) {
89 unsigned seen = 0;
90 size_t i;
91 for (i = 0; i < d.blocks[0].inl.n_spans; i++)
92 seen |= d.blocks[0].inl.spans[i].style;
93 CHECK((seen & N_MARKDOWN_BOLD) && (seen & N_MARKDOWN_ITALIC) &&
94 (seen & N_MARKDOWN_CODE),
95 "bold+italic+code seen");
96 }
98 }
99
100 /* a valid http(s) link carries the url; a non-http scheme is literal */
101 {
102 n_markdown_doc d = parse("see [x](https://e.org) and [y](javascript:1)");
103 int link = 0, js_literal = 1;
104 size_t i;
105 if (d.n_blocks == 1)
106 for (i = 0; i < d.blocks[0].inl.n_spans; i++) {
107 n_markdown_span* s = &d.blocks[0].inl.spans[i];
108 if (s->style & N_MARKDOWN_LINK) {
109 link = 1;
110 CHECK(s->url && strcmp(s->url, "https://e.org") == 0,
111 "http url captured");
112 }
113 if (s->url && strstr(s->url, "javascript"))
114 js_literal = 0; /* must NOT become a link url */
115 }
116 CHECK(link, "http link recognised");
117 CHECK(js_literal, "javascript: scheme not turned into a link");
118 n_markdown_free(&d);
119 }
120
121 /* block kinds: quote, code fence, lists */
122 {
123 n_markdown_doc d = parse("> quoted\n\n```\ncode\n```\n\n- one\n- two");
124 int quote = 0, code = 0, ul = 0;
125 size_t i;
126 for (i = 0; i < d.n_blocks; i++) {
127 if (d.blocks[i].kind == N_MARKDOWN_QUOTE)
128 quote = 1;
130 code = 1;
131 if (d.blocks[i].kind == N_MARKDOWN_UL)
132 ul = d.blocks[i].n_items == 2;
133 }
134 CHECK(quote && code && ul, "quote + code block + 2-item list");
135 n_markdown_free(&d);
136 }
137
138 /* an ordered list */
139 {
140 n_markdown_doc d = parse("1. a\n2. b\n3. c");
141 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_OL &&
142 d.blocks[0].n_items == 3,
143 "ordered list of 3");
144 n_markdown_free(&d);
145 }
146
147 /* safety: an unclosed emphasis marker stays literal, no markup detected */
148 {
149 CHECK(n_markdown_has_markup("**bold**", 8), "has_markup true for bold");
150 CHECK(!n_markdown_has_markup("plain text", 10),
151 "has_markup false for plain");
152 CHECK(!n_markdown_has_markup("an *unclosed marker", 19),
153 "unclosed marker is not markup");
154 }
155
156 /* NULL / empty input must not crash */
157 {
159 n_markdown_parse(NULL, 0, &d);
160 CHECK(d.n_blocks == 0, "NULL input -> empty doc");
161 n_markdown_free(&d);
162 n_markdown_free(NULL);
163 }
164
165 /* ---- the optional block kinds, and that they stay optional ---------- */
166
167 /* a heading is ordinary text until it is asked for */
168 {
169 n_markdown_doc d = parse("# Title\n\nbody");
170 CHECK(d.n_blocks == 2 && d.blocks[0].kind == N_MARKDOWN_PARA,
171 "heading is a paragraph in the default subset");
172 n_markdown_free(&d);
173 }
174 {
175 char buf[64];
176 n_markdown_doc d = parse_ex("# Title\n\nbody", N_MARKDOWN_HEADINGS);
177 int ok = d.n_blocks == 2 && d.blocks[0].kind == N_MARKDOWN_HEADING &&
178 d.blocks[0].level == 1 && d.blocks[1].kind == N_MARKDOWN_PARA;
179 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
180 CHECK(ok && strcmp(buf, "Title") == 0, "flagged heading, level 1, hashes stripped");
181 n_markdown_free(&d);
182 }
183 {
184 n_markdown_doc d = parse_ex("###### six\n", N_MARKDOWN_HEADINGS);
185 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_HEADING &&
186 d.blocks[0].level == 6,
187 "six hashes is a level 6 heading");
188 n_markdown_free(&d);
189 }
190 {
191 /* seven is past the deepest heading, and a hash with no space after it
192 is a hash tag or a preprocessor line, not a heading */
193 n_markdown_doc d = parse_ex("####### seven\n", N_MARKDOWN_HEADINGS);
194 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_PARA, "seven hashes stays text");
195 n_markdown_free(&d);
196 d = parse_ex("#hashtag\n", N_MARKDOWN_HEADINGS);
197 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_PARA, "#hashtag stays text");
198 n_markdown_free(&d);
199 }
200
201 /* a table is ordinary text until it is asked for */
202 {
203 n_markdown_doc d = parse("| a | b |\n|---|---|\n| 1 | 2 |\n");
204 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_PARA,
205 "table is a paragraph in the default subset");
206 n_markdown_free(&d);
207 }
208 {
209 char buf[64];
210 n_markdown_doc d = parse_ex("| Key | Effect |\n|:--|--:|\n| G | grid |\n| H | help |\n",
212 const n_markdown_block* b = &d.blocks[0];
213 int ok = d.n_blocks == 1 && b->kind == N_MARKDOWN_TABLE && b->n_cols == 2 &&
214 b->n_items == 6;
215 CHECK(ok, "flagged table: 2 columns, a header and two body rows");
216 if (ok) {
217 CHECK(b->align && b->align[0] == N_MARKDOWN_ALIGN_LEFT &&
219 "alignment read from the rule row");
220 cell_text(b, 0, 1, buf, sizeof(buf));
221 CHECK(strcmp(buf, "Effect") == 0, "header cell (0,1)");
222 cell_text(b, 2, 0, buf, sizeof(buf));
223 CHECK(strcmp(buf, "H") == 0, "body cell (2,0)");
224 }
225 n_markdown_free(&d);
226 }
227 {
228 /* the pipes at the ends are optional, and ":-:" is centred */
229 n_markdown_doc d = parse_ex("a | b\n--- | :-:\n1 | 2\n", N_MARKDOWN_ALL);
230 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_TABLE &&
231 d.blocks[0].n_cols == 2 && d.blocks[0].align &&
233 "table without edge pipes, centred second column");
234 n_markdown_free(&d);
235 }
236 {
237 /* a pipe decides nothing on its own: without the rule row underneath
238 it, this is a paragraph -- and the parser must still make progress,
239 which is what this case was written for */
240 n_markdown_doc d = parse_ex("a | b\nc | d\n", N_MARKDOWN_ALL);
241 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_PARA,
242 "pipes with no rule row stay a paragraph");
243 n_markdown_free(&d);
244 }
245 {
246 /* every row is padded or truncated to the header's width, so a
247 renderer can index items[row * n_cols + col] without checking */
248 n_markdown_doc d = parse_ex("| a | b |\n|---|---|\n| 1 |\n| 1 | 2 | 3 |\n",
250 CHECK(d.n_blocks == 1 && d.blocks[0].n_cols == 2 && d.blocks[0].n_items == 6,
251 "short and long rows are squared off to the header's width");
252 n_markdown_free(&d);
253 }
254 {
255 /* hostile: nothing but separators, and a table with no content at all */
256 n_markdown_doc d = parse_ex("|||||\n|---|\n||||\n", N_MARKDOWN_ALL);
257 CHECK(d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_TABLE,
258 "a row of bare pipes parses and does not crash");
259 n_markdown_free(&d);
260 d = parse_ex("|\n|-|\n", N_MARKDOWN_ALL);
261 CHECK(d.n_blocks == 1 && d.blocks[0].n_cols == 1, "an empty table has one column");
262 n_markdown_free(&d);
263 }
264
265 /* a list item long enough to wrap stays one item */
266 {
267 char buf[128];
268 n_markdown_doc d = parse("- one\n- two carries on\n onto a second line\n- three\n");
269 int ok = d.n_blocks == 1 && d.blocks[0].kind == N_MARKDOWN_UL &&
270 d.blocks[0].n_items == 3;
271 CHECK(ok, "a wrapped list item does not end its list");
272 if (ok) {
273 flow_text(&d.blocks[0].items[1], buf, sizeof(buf));
274 CHECK(strcmp(buf, "two carries on onto a second line") == 0,
275 "the continuation joins with a space, indent stripped");
276 }
277 n_markdown_free(&d);
278 }
279 {
280 /* a continuation that is really the next block must not be swallowed */
281 n_markdown_doc d = parse_ex("- one\n still one\n## a heading\n", N_MARKDOWN_HEADINGS);
282 CHECK(d.n_blocks == 2 && d.blocks[0].kind == N_MARKDOWN_UL &&
283 d.blocks[0].n_items == 1 && d.blocks[1].kind == N_MARKDOWN_HEADING,
284 "a block start ends the item even when indented text preceded it");
285 n_markdown_free(&d);
286 }
287
288 /* ---- inline options: autolinks, strikethrough, escapes ------------- */
289 {
290#define IA N_MARKDOWN_INLINE_ALL
291 char buf[160];
292 /* one link span in a one-paragraph doc: its text and url, or "" */
293 struct {
294 const char* in;
295 const char* url; /* NULL: no link expected */
296 } al[] = {
297 {"see https://x.org/a_b_c today", "https://x.org/a_b_c"},
298 {"see https://x.org.", "https://x.org"},
299 {"(https://x.org/w)", "https://x.org/w"},
300 {"https://en.wikipedia.org/wiki/C_(lang)", "https://en.wikipedia.org/wiki/C_(lang)"},
301 {"wow https://x.org!?", "https://x.org"},
302 {"<http://a>", "http://a"},
303 {"<https://x.org/q?a=1&b=2>", "https://x.org/q?a=1&b=2"},
304 {"HTTPS://X.ORG/P", "HTTPS://X.ORG/P"},
305 {"xhttps://x.org", NULL}, /* not at a word boundary */
306 {"https://", NULL}, /* nothing after the scheme */
307 {"<https://>", NULL},
308 {"ftp://x.org", NULL}, /* http(s) only */
309 {"`https://x.org`", NULL}, /* code is code */
310 {"<javascript:alert(1)>", NULL},
311 };
312 size_t k;
313 for (k = 0; k < sizeof(al) / sizeof(al[0]); k++) {
314 n_markdown_doc d = parse_ex(al[k].in, IA);
315 const char* got = NULL;
316 size_t i;
317 if (d.n_blocks == 1)
318 for (i = 0; i < d.blocks[0].inl.n_spans; i++)
319 if (d.blocks[0].inl.spans[i].style & N_MARKDOWN_LINK) {
320 got = d.blocks[0].inl.spans[i].url;
321 CHECK(strcmp(d.blocks[0].inl.spans[i].text, got) == 0,
322 "an autolink's text is its url");
323 }
324 if (al[k].url)
325 CHECK(got && strcmp(got, al[k].url) == 0, al[k].in);
326 else
327 CHECK(got == NULL, al[k].in);
328 n_markdown_free(&d);
329 }
330 /* trailing punctuation goes back to the text */
331 {
332 n_markdown_doc d = parse_ex("see https://x.org.", IA);
333 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
334 CHECK(strcmp(buf, "see https://x.org.") == 0,
335 "no character is lost around an autolink");
336 n_markdown_free(&d);
337 }
338 /* [text](url) still wins over the bare URL inside it */
339 {
340 n_markdown_doc d = parse_ex("[site](https://x.org)", IA);
341 CHECK(d.n_blocks == 1 && d.blocks[0].inl.n_spans == 1 &&
342 strcmp(d.blocks[0].inl.spans[0].text, "site") == 0,
343 "an explicit link is not re-linked");
344 n_markdown_free(&d);
345 }
346 /* strikethrough */
347 {
348 n_markdown_doc d = parse_ex("a ~~gone~~ b", IA);
349 CHECK(d.n_blocks == 1 && d.blocks[0].inl.n_spans == 3 &&
351 strcmp(d.blocks[0].inl.spans[1].text, "gone") == 0,
352 "~~x~~ is a strike span");
353 n_markdown_free(&d);
354 d = parse_ex("~~**both**~~", IA);
355 CHECK(d.n_blocks == 1 && d.blocks[0].inl.n_spans == 1 &&
356 d.blocks[0].inl.spans[0].style ==
358 "strike nests with bold");
359 n_markdown_free(&d);
360 d = parse_ex("~~~~ and ~one~", IA);
361 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
362 CHECK(strcmp(buf, "~~~~ and ~one~") == 0 &&
363 d.blocks[0].inl.spans[0].style == 0,
364 "an empty or single-tilde strike is text");
365 n_markdown_free(&d);
366 d = parse("a ~~gone~~ b");
367 CHECK(d.blocks[0].inl.n_spans == 1, "no strike unless asked for");
368 n_markdown_free(&d);
369 }
370 /* escapes */
371 {
372 n_markdown_doc d = parse_ex("\\*not italic\\* and \\# not \\a", IA);
373 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
374 CHECK(d.blocks[0].inl.n_spans == 1 &&
375 d.blocks[0].inl.spans[0].style == 0 &&
376 strcmp(buf, "*not italic* and # not \\a") == 0,
377 "a backslash makes punctuation literal, and only punctuation");
378 n_markdown_free(&d);
379 CHECK(n_markdown_has_markup_ex("a\\*b", 4, IA),
380 "an escape is markup: the plain path would show it");
381 CHECK(!n_markdown_has_markup("a\\*b", 4),
382 "but not unless asked for");
383 }
384 /* the default subset is unchanged, byte for byte */
385 {
386 n_markdown_doc d = parse("see https://x.org ~~a~~ \\*b\\*");
387 CHECK(d.n_blocks == 1 && d.blocks[0].inl.n_spans >= 1 &&
388 !(d.blocks[0].inl.spans[0].style & N_MARKDOWN_LINK),
389 "the default parse links nothing bare");
390 n_markdown_free(&d);
391 }
392#undef IA
393 }
394
395 /* an emphasis or a code span needs content: an empty one is literal */
396 {
397 char buf[96];
398 size_t k;
399 const char* lit[] = {"an **unclosed run", "a ** b", "a **** b",
400 "a `` b", "x __y"};
401 for (k = 0; k < sizeof(lit) / sizeof(lit[0]); k++) {
402 n_markdown_doc d = parse(lit[k]);
403 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
404 CHECK(d.n_blocks == 1 && strcmp(buf, lit[k]) == 0 &&
405 d.blocks[0].inl.n_spans == 1 &&
406 d.blocks[0].inl.spans[0].style == 0,
407 lit[k]);
408 n_markdown_free(&d);
409 }
410 {
411 n_markdown_doc d = parse("**a** and *b*");
412 CHECK(d.n_blocks == 1 && d.blocks[0].inl.n_spans == 3 &&
413 d.blocks[0].inl.spans[0].style == N_MARKDOWN_BOLD &&
415 "non-empty emphasis still works");
416 n_markdown_free(&d);
417 }
418 }
419 /* a table ends at a line that starts a block of its own */
420 {
421 n_markdown_doc d = parse_ex("a | b\n--|--\n1 | 2\n# x | y\n- c|d\n> e|f\n",
423 CHECK(d.n_blocks == 4 && d.blocks[0].kind == N_MARKDOWN_TABLE &&
424 d.blocks[0].n_items == 4 && d.blocks[1].kind == N_MARKDOWN_HEADING &&
426 "a heading, a list item or a quote after a table is not a row");
427 n_markdown_free(&d);
428 d = parse_ex("a | b\n--|--\n1 | 2\nplain | row\n", N_MARKDOWN_ALL);
429 CHECK(d.n_blocks == 1 && d.blocks[0].n_items == 6,
430 "an ordinary line with a pipe is still a row");
431 n_markdown_free(&d);
432 }
433
434 /* underscores inside a word neither open nor close emphasis
435 * (CommonMark): shortcodes and snake_case stay literal */
436 {
437 char buf[128];
438 size_t k;
439 const char* lit[] = {":throwing_kiss: :wrapped_present: :two_hearts:",
440 "snake_case_name and other_name",
441 "café_au_lait_x", "a__b__c", "x_y _z"};
442 for (k = 0; k < sizeof(lit) / sizeof(lit[0]); k++) {
443 n_markdown_doc d = parse(lit[k]);
444 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
445 CHECK(d.n_blocks == 1 && strcmp(buf, lit[k]) == 0 &&
446 d.blocks[0].inl.n_spans == 1 &&
447 d.blocks[0].inl.spans[0].style == 0,
448 lit[k]);
449 n_markdown_free(&d);
450 CHECK(!n_markdown_has_markup(lit[k], strlen(lit[k])), lit[k]);
451 }
452 {
453 n_markdown_doc d = parse("_a_b c_ and __x_y__ and a*b*c");
454 flow_text(&d.blocks[0].inl, buf, sizeof(buf));
455 CHECK(strcmp(buf, "a_b c and x_y and abc") == 0 &&
458 "word-boundary underscores still emphasise; '*' is unchanged");
459 n_markdown_free(&d);
460 }
461 {
462 n_markdown_doc d = parse("(_paren_) _a_.");
463 CHECK(d.blocks[0].inl.n_spans == 5 &&
466 "punctuation is a boundary");
467 n_markdown_free(&d);
468 }
469 }
470
471 /* has_markup follows the flags it is given */
472 {
473 CHECK(!n_markdown_has_markup("# Title", 7), "a heading is not markup by default");
475 "a heading is markup when asked for");
477 "the default subset still counts under the flags");
478 }
479
480 if (failures) {
481 n_log(LOG_ERR, "ex_markdown: %d failure(s)", failures);
482 return 1;
483 }
484 n_log(LOG_NOTICE, "ex_markdown: all checks passed");
485 return 0;
486}
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
#define CHECK(cond, msg)
static const char * row(N_GUI_TREE *tree, int index)
the text of a visible row, "" past the end
static n_http_req_status parse(const char *s, n_http_request *out)
Definition ex_http_req.c:36
static n_markdown_doc parse_ex(const char *s, unsigned flags)
Definition ex_markdown.c:51
static void flow_text(const n_markdown_inline *inl, char *out, size_t cap)
Definition ex_markdown.c:58
static void cell_text(const n_markdown_block *b, size_t row, size_t col, char *out, size_t cap)
Definition ex_markdown.c:73
#define IA
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
Generic log system.
void n_markdown_free(n_markdown_doc *doc)
free everything a n_markdown_doc owns and zero it
Definition n_markdown.c:918
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
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
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
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_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_ALL
every block kind this parser knows.
Definition n_markdown.h:90
#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_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