master
1//! Markdown parsing and rendering support.
2//!
3//! A Markdown document consists of a series of blocks. Depending on its type,
4//! each block may contain other blocks, inline content, or nothing. The
5//! supported blocks are as follows:
6//!
7//! - **List** - a sequence of list items of the same type.
8//!
9//! - **List item** - unordered list items start with `-`, `*`, or `+` followed
10//! by a space. Ordered list items start with a number between 0 and
11//! 999,999,999, followed by a `.` or `)` and a space. The number of an
12//! ordered list item only matters for the first item in the list (to
13//! determine the starting number of the list). All subsequent ordered list
14//! items will have sequentially increasing numbers.
15//!
16//! All list items may contain block content. Any content indented at least as
17//! far as the end of the list item marker (including the space after it) is
18//! considered part of the list item.
19//!
20//! Lists which have no blank lines between items or between direct children
21//! of items are considered _tight_, and direct child paragraphs of tight list
22//! items are rendered without `<p>` tags.
23//!
24//! - **Table** - a sequence of adjacent table row lines, where each line starts
25//! and ends with a `|`, and cells within the row are delimited by `|`s.
26//!
27//! The first or second row of a table may be a _header delimiter row_, which
28//! is a row consisting of cells of the pattern `---` (for unset column
29//! alignment), `:--` (for left alignment), `:-:` (for center alignment), or
30//! `--:` (for right alignment). The number of `-`s must be at least one, but
31//! is otherwise arbitrary. If there is a row just before the header delimiter
32//! row, it becomes the header row for the table (a table need not have a
33//! header row at all).
34//!
35//! - **Heading** - a sequence of between 1 and 6 `#` characters, followed by a
36//! space and further inline content on the same line.
37//!
38//! - **Code block** - a sequence of at least 3 `` ` `` characters (a _fence_),
39//! optionally followed by a "tag" on the same line, and continuing until a
40//! line consisting only of a closing fence whose length matches the opening
41//! fence, or until the end of the containing block.
42//!
43//! The content of a code block is not parsed as inline content. It is
44//! included verbatim in the output document (minus leading indentation up to
45//! the position of the opening fence).
46//!
47//! - **Blockquote** - a sequence of lines preceded by `>` characters.
48//!
49//! - **Paragraph** - ordinary text, parsed as inline content, ending with a
50//! blank line or the end of the containing block.
51//!
52//! Paragraphs which are part of another block may be "lazily" continued by
53//! subsequent paragraph lines even if those lines would not ordinarily be
54//! considered part of the containing block. For example, this is a single
55//! list item, not a list item followed by a paragraph:
56//!
57//! ```markdown
58//! - First line of content.
59//! This content is still part of the paragraph,
60//! even though it isn't indented far enough.
61//! ```
62//!
63//! - **Thematic break** - a line consisting of at least three matching `-`,
64//! `_`, or `*` characters and, optionally, spaces.
65//!
66//! Indentation may consist of spaces and tabs. The use of tabs is not
67//! recommended: a tab is treated the same as a single space for the purpose of
68//! determining the indentation level, and is not recognized as a space for
69//! block starters which require one (for example, `-` followed by a tab is not
70//! a valid list item).
71//!
72//! The supported inlines are as follows:
73//!
74//! - **Link** - of the format `[text](target)`. `text` may contain inline
75//! content. `target` may contain `\`-escaped characters and balanced
76//! parentheses.
77//!
78//! - **Autolink** - an abbreviated link, of the format `<target>`, where
79//! `target` serves as both the link target and text. `target` may not
80//! contain spaces or `<`, and any `\` in it are interpreted literally (not as
81//! escapes). `target` is expected to be an absolute URI: an autolink will not
82//! be recognized unless `target` starts with a URI scheme followed by a `:`.
83//!
84//! For convenience, autolinks may also be recognized in plain text without
85//! any `<>` delimiters. Such autolinks are restricted to start with `http://`
86//! or `https://` followed by at least one other character, not including any
87//! trailing punctuation after the link.
88//!
89//! - **Image** - a link directly preceded by a `!`. The link text is
90//! interpreted as the alt text of the image.
91//!
92//! - **Emphasis** - a run of `*` or `_` characters may be an emphasis opener,
93//! closer, or both. For `*` characters, the run may be an opener as long as
94//! it is not directly followed by a whitespace character (or the end of the
95//! inline content) and a closer as long as it is not directly preceded by
96//! one. For `_` characters, this rule is strengthened by requiring that the
97//! run also be preceded by a whitespace or punctuation character (for
98//! openers) or followed by one (for closers), to avoid mangling `snake_case`
99//! words.
100//!
101//! The rule for emphasis handling is greedy: any run that can close existing
102//! emphasis will do so, otherwise it will open emphasis. A single run may
103//! serve both functions: the middle `**` in the following example both closes
104//! the initial emphasis and opens a new one:
105//!
106//! ```markdown
107//! *one**two*
108//! ```
109//!
110//! A single `*` or `_` is used for normal emphasis (HTML `<em>`), and a
111//! double `**` or `__` is used for strong emphasis (HTML `<strong>`). Even
112//! longer runs may be used to produce further nested emphasis (though only
113//! `***` and `___` to produce `<em><strong>` is really useful).
114//!
115//! - **Code span** - a run of `` ` `` characters, terminated by a matching run
116//! or the end of inline content. The content of a code span is not parsed
117//! further.
118//!
119//! - **Text** - normal text is interpreted as-is, except that `\` may be used
120//! to escape any punctuation character, preventing it from being interpreted
121//! according to other syntax rules. A `\` followed by a line break within a
122//! paragraph is interpreted as a hard line break.
123//!
124//! Any null bytes or invalid UTF-8 bytes within text are replaced with Unicode
125//! replacement characters, `U+FFFD`.
126
127const std = @import("std");
128const testing = std.testing;
129
130pub const Document = @import("markdown/Document.zig");
131pub const Parser = @import("markdown/Parser.zig");
132pub const Renderer = @import("markdown/renderer.zig").Renderer;
133pub const renderNodeInlineText = @import("markdown/renderer.zig").renderNodeInlineText;
134pub const fmtHtml = @import("markdown/renderer.zig").fmtHtml;
135
136// Avoid exposing main to other files merely importing this one.
137pub const main = if (@import("root") == @This())
138 mainImpl
139else
140 @compileError("only available as root source file");
141
142fn mainImpl() !void {
143 const gpa = std.heap.c_allocator;
144
145 var parser = try Parser.init(gpa);
146 defer parser.deinit();
147
148 var stdin_buffer: [1024]u8 = undefined;
149 var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer);
150
151 while (stdin_reader.takeDelimiterExclusive('\n')) |line| {
152 const trimmed = std.mem.trimRight(u8, line, '\r');
153 try parser.feedLine(trimmed);
154 } else |err| switch (err) {
155 error.EndOfStream => {},
156 else => |e| return e,
157 }
158
159 var doc = try parser.endInput();
160 defer doc.deinit(gpa);
161
162 var stdout_buffer: [1024]u8 = undefined;
163 var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
164 try doc.render(&stdout_writer.interface);
165 try stdout_writer.interface.flush();
166}
167
168test "empty document" {
169 try testRender("", "");
170 try testRender(" ", "");
171 try testRender("\n \n\t\n \n", "");
172}
173
174test "unordered lists" {
175 try testRender(
176 \\- Spam
177 \\- Spam
178 \\- Spam
179 \\- Eggs
180 \\- Bacon
181 \\- Spam
182 \\
183 \\* Spam
184 \\* Spam
185 \\* Spam
186 \\* Eggs
187 \\* Bacon
188 \\* Spam
189 \\
190 \\+ Spam
191 \\+ Spam
192 \\+ Spam
193 \\+ Eggs
194 \\+ Bacon
195 \\+ Spam
196 \\
197 ,
198 \\<ul>
199 \\<li>Spam</li>
200 \\<li>Spam</li>
201 \\<li>Spam</li>
202 \\<li>Eggs</li>
203 \\<li>Bacon</li>
204 \\<li>Spam</li>
205 \\</ul>
206 \\<ul>
207 \\<li>Spam</li>
208 \\<li>Spam</li>
209 \\<li>Spam</li>
210 \\<li>Eggs</li>
211 \\<li>Bacon</li>
212 \\<li>Spam</li>
213 \\</ul>
214 \\<ul>
215 \\<li>Spam</li>
216 \\<li>Spam</li>
217 \\<li>Spam</li>
218 \\<li>Eggs</li>
219 \\<li>Bacon</li>
220 \\<li>Spam</li>
221 \\</ul>
222 \\
223 );
224}
225
226test "ordered lists" {
227 try testRender(
228 \\1. Breakfast
229 \\2. Second breakfast
230 \\3. Lunch
231 \\2. Afternoon snack
232 \\1. Dinner
233 \\6. Dessert
234 \\7. Midnight snack
235 \\
236 \\1) Breakfast
237 \\2) Second breakfast
238 \\3) Lunch
239 \\2) Afternoon snack
240 \\1) Dinner
241 \\6) Dessert
242 \\7) Midnight snack
243 \\
244 \\1001. Breakfast
245 \\2. Second breakfast
246 \\3. Lunch
247 \\2. Afternoon snack
248 \\1. Dinner
249 \\6. Dessert
250 \\7. Midnight snack
251 \\
252 \\1001) Breakfast
253 \\2) Second breakfast
254 \\3) Lunch
255 \\2) Afternoon snack
256 \\1) Dinner
257 \\6) Dessert
258 \\7) Midnight snack
259 \\
260 ,
261 \\<ol>
262 \\<li>Breakfast</li>
263 \\<li>Second breakfast</li>
264 \\<li>Lunch</li>
265 \\<li>Afternoon snack</li>
266 \\<li>Dinner</li>
267 \\<li>Dessert</li>
268 \\<li>Midnight snack</li>
269 \\</ol>
270 \\<ol>
271 \\<li>Breakfast</li>
272 \\<li>Second breakfast</li>
273 \\<li>Lunch</li>
274 \\<li>Afternoon snack</li>
275 \\<li>Dinner</li>
276 \\<li>Dessert</li>
277 \\<li>Midnight snack</li>
278 \\</ol>
279 \\<ol start="1001">
280 \\<li>Breakfast</li>
281 \\<li>Second breakfast</li>
282 \\<li>Lunch</li>
283 \\<li>Afternoon snack</li>
284 \\<li>Dinner</li>
285 \\<li>Dessert</li>
286 \\<li>Midnight snack</li>
287 \\</ol>
288 \\<ol start="1001">
289 \\<li>Breakfast</li>
290 \\<li>Second breakfast</li>
291 \\<li>Lunch</li>
292 \\<li>Afternoon snack</li>
293 \\<li>Dinner</li>
294 \\<li>Dessert</li>
295 \\<li>Midnight snack</li>
296 \\</ol>
297 \\
298 );
299}
300
301test "nested lists" {
302 try testRender(
303 \\- - Item 1.
304 \\ - Item 2.
305 \\Item 2 continued.
306 \\ * New list.
307 \\
308 ,
309 \\<ul>
310 \\<li><ul>
311 \\<li>Item 1.</li>
312 \\<li>Item 2.
313 \\Item 2 continued.</li>
314 \\</ul>
315 \\<ul>
316 \\<li>New list.</li>
317 \\</ul>
318 \\</li>
319 \\</ul>
320 \\
321 );
322}
323
324test "lists with block content" {
325 try testRender(
326 \\1. Item 1.
327 \\2. Item 2.
328 \\
329 \\ This one has another paragraph.
330 \\3. Item 3.
331 \\
332 \\- > Blockquote.
333 \\- - Sub-list.
334 \\ - Sub-list continued.
335 \\ * Different sub-list.
336 \\- ## Heading.
337 \\
338 \\ Some contents below the heading.
339 \\ 1. Item 1.
340 \\ 2. Item 2.
341 \\ 3. Item 3.
342 \\
343 ,
344 \\<ol>
345 \\<li><p>Item 1.</p>
346 \\</li>
347 \\<li><p>Item 2.</p>
348 \\<p>This one has another paragraph.</p>
349 \\</li>
350 \\<li><p>Item 3.</p>
351 \\</li>
352 \\</ol>
353 \\<ul>
354 \\<li><blockquote>
355 \\<p>Blockquote.</p>
356 \\</blockquote>
357 \\</li>
358 \\<li><ul>
359 \\<li>Sub-list.</li>
360 \\<li>Sub-list continued.</li>
361 \\</ul>
362 \\<ul>
363 \\<li>Different sub-list.</li>
364 \\</ul>
365 \\</li>
366 \\<li><h2>Heading.</h2>
367 \\<p>Some contents below the heading.</p>
368 \\<ol>
369 \\<li>Item 1.</li>
370 \\<li>Item 2.</li>
371 \\<li>Item 3.</li>
372 \\</ol>
373 \\</li>
374 \\</ul>
375 \\
376 );
377}
378
379test "indented lists" {
380 try testRender(
381 \\Test:
382 \\ * a1
383 \\ * a2
384 \\ * b1
385 \\ * b2
386 \\
387 \\---
388 \\
389 \\ Test:
390 \\ - One
391 \\Two
392 \\ - Three
393 \\Four
394 \\ Five
395 \\Six
396 \\
397 \\---
398 \\
399 \\None of these items are indented far enough from the previous one to
400 \\start a nested list:
401 \\ - One
402 \\ - Two
403 \\ - Three
404 \\ - Four
405 \\ - Five
406 \\ - Six
407 \\ - Seven
408 \\ - Eight
409 \\ - Nine
410 \\
411 \\---
412 \\
413 \\ - One
414 \\ - Two
415 \\ - Three
416 \\ - Four
417 \\ - Five
418 \\ - Six
419 \\- Seven
420 \\
421 ,
422 \\<p>Test:</p>
423 \\<ul>
424 \\<li>a1</li>
425 \\<li>a2<ul>
426 \\<li>b1</li>
427 \\<li>b2</li>
428 \\</ul>
429 \\</li>
430 \\</ul>
431 \\<hr />
432 \\<p>Test:</p>
433 \\<ul>
434 \\<li>One
435 \\Two<ul>
436 \\<li>Three
437 \\Four
438 \\Five
439 \\Six</li>
440 \\</ul>
441 \\</li>
442 \\</ul>
443 \\<hr />
444 \\<p>None of these items are indented far enough from the previous one to
445 \\start a nested list:</p>
446 \\<ul>
447 \\<li>One</li>
448 \\<li>Two</li>
449 \\<li>Three</li>
450 \\<li>Four</li>
451 \\<li>Five</li>
452 \\<li>Six</li>
453 \\<li>Seven</li>
454 \\<li>Eight</li>
455 \\<li>Nine</li>
456 \\</ul>
457 \\<hr />
458 \\<ul>
459 \\<li>One<ul>
460 \\<li>Two<ul>
461 \\<li>Three<ul>
462 \\<li>Four</li>
463 \\</ul>
464 \\</li>
465 \\</ul>
466 \\</li>
467 \\<li>Five<ul>
468 \\<li>Six</li>
469 \\</ul>
470 \\</li>
471 \\</ul>
472 \\</li>
473 \\<li>Seven</li>
474 \\</ul>
475 \\
476 );
477}
478
479test "tables" {
480 try testRender(
481 \\| Operator | Meaning |
482 \\| :------: | ---------------- |
483 \\| `+` | Add |
484 \\| `-` | Subtract |
485 \\| `*` | Multiply |
486 \\| `/` | Divide |
487 \\| `??` | **Not sure yet** |
488 \\
489 \\| Item 1 | Value 1 |
490 \\| Item 2 | Value 2 |
491 \\| Item 3 | Value 3 |
492 \\| Item 4 | Value 4 |
493 \\
494 \\| :--- | :----: | ----: |
495 \\| Left | Center | Right |
496 \\
497 \\ | One | Two |
498 \\ | Three | Four |
499 \\ | Five | Six |
500 \\
501 ,
502 \\<table>
503 \\<tr>
504 \\<th style="text-align: center">Operator</th>
505 \\<th>Meaning</th>
506 \\</tr>
507 \\<tr>
508 \\<td style="text-align: center"><code>+</code></td>
509 \\<td>Add</td>
510 \\</tr>
511 \\<tr>
512 \\<td style="text-align: center"><code>-</code></td>
513 \\<td>Subtract</td>
514 \\</tr>
515 \\<tr>
516 \\<td style="text-align: center"><code>*</code></td>
517 \\<td>Multiply</td>
518 \\</tr>
519 \\<tr>
520 \\<td style="text-align: center"><code>/</code></td>
521 \\<td>Divide</td>
522 \\</tr>
523 \\<tr>
524 \\<td style="text-align: center"><code>??</code></td>
525 \\<td><strong>Not sure yet</strong></td>
526 \\</tr>
527 \\</table>
528 \\<table>
529 \\<tr>
530 \\<td>Item 1</td>
531 \\<td>Value 1</td>
532 \\</tr>
533 \\<tr>
534 \\<td>Item 2</td>
535 \\<td>Value 2</td>
536 \\</tr>
537 \\<tr>
538 \\<td>Item 3</td>
539 \\<td>Value 3</td>
540 \\</tr>
541 \\<tr>
542 \\<td>Item 4</td>
543 \\<td>Value 4</td>
544 \\</tr>
545 \\</table>
546 \\<table>
547 \\<tr>
548 \\<td style="text-align: left">Left</td>
549 \\<td style="text-align: center">Center</td>
550 \\<td style="text-align: right">Right</td>
551 \\</tr>
552 \\</table>
553 \\<table>
554 \\<tr>
555 \\<td>One</td>
556 \\<td>Two</td>
557 \\</tr>
558 \\<tr>
559 \\<td>Three</td>
560 \\<td>Four</td>
561 \\</tr>
562 \\<tr>
563 \\<td>Five</td>
564 \\<td>Six</td>
565 \\</tr>
566 \\</table>
567 \\
568 );
569}
570
571test "table with uneven number of columns" {
572 try testRender(
573 \\| One |
574 \\| :-- | :--: |
575 \\| One | Two | Three |
576 \\
577 ,
578 \\<table>
579 \\<tr>
580 \\<th style="text-align: left">One</th>
581 \\</tr>
582 \\<tr>
583 \\<td style="text-align: left">One</td>
584 \\<td style="text-align: center">Two</td>
585 \\<td>Three</td>
586 \\</tr>
587 \\</table>
588 \\
589 );
590}
591
592test "table with escaped pipes" {
593 try testRender(
594 \\| One \| Two |
595 \\| --- | --- |
596 \\| One \| Two |
597 \\
598 ,
599 \\<table>
600 \\<tr>
601 \\<th>One | Two</th>
602 \\</tr>
603 \\<tr>
604 \\<td>One | Two</td>
605 \\</tr>
606 \\</table>
607 \\
608 );
609}
610
611test "table with pipes in code spans" {
612 try testRender(
613 \\| `|` | Bitwise _OR_ |
614 \\| `||` | Combines error sets |
615 \\| `` `||` `` | Escaped version |
616 \\| ` ``||`` ` | Another escaped version |
617 \\| `Oops unterminated code span |
618 \\
619 ,
620 \\<table>
621 \\<tr>
622 \\<td><code>|</code></td>
623 \\<td>Bitwise <em>OR</em></td>
624 \\</tr>
625 \\<tr>
626 \\<td><code>||</code></td>
627 \\<td>Combines error sets</td>
628 \\</tr>
629 \\<tr>
630 \\<td><code>`||`</code></td>
631 \\<td>Escaped version</td>
632 \\</tr>
633 \\<tr>
634 \\<td><code>``||``</code></td>
635 \\<td>Another escaped version</td>
636 \\</tr>
637 \\</table>
638 \\<p>| <code>Oops unterminated code span |</code></p>
639 \\
640 );
641}
642
643test "tables require leading and trailing pipes" {
644 try testRender(
645 \\Not | a | table
646 \\
647 \\| But | this | is |
648 \\
649 \\Also not a table:
650 \\|
651 \\ |
652 \\
653 ,
654 \\<p>Not | a | table</p>
655 \\<table>
656 \\<tr>
657 \\<td>But</td>
658 \\<td>this</td>
659 \\<td>is</td>
660 \\</tr>
661 \\</table>
662 \\<p>Also not a table:
663 \\|
664 \\|</p>
665 \\
666 );
667}
668
669test "headings" {
670 try testRender(
671 \\# Level one
672 \\## Level two
673 \\### Level three
674 \\#### Level four
675 \\##### Level five
676 \\###### Level six
677 \\####### Not a heading
678 \\
679 ,
680 \\<h1>Level one</h1>
681 \\<h2>Level two</h2>
682 \\<h3>Level three</h3>
683 \\<h4>Level four</h4>
684 \\<h5>Level five</h5>
685 \\<h6>Level six</h6>
686 \\<p>####### Not a heading</p>
687 \\
688 );
689}
690
691test "headings with inline content" {
692 try testRender(
693 \\# Outline of `std.zig`
694 \\## **Important** notes
695 \\### ***Nested* inline content**
696 \\
697 ,
698 \\<h1>Outline of <code>std.zig</code></h1>
699 \\<h2><strong>Important</strong> notes</h2>
700 \\<h3><strong><em>Nested</em> inline content</strong></h3>
701 \\
702 );
703}
704
705test "code blocks" {
706 try testRender(
707 \\```
708 \\Hello, world!
709 \\This is some code.
710 \\```
711 \\``` zig test
712 \\const std = @import("std");
713 \\
714 \\test {
715 \\ try std.testing.expect(2 + 2 == 4);
716 \\}
717 \\```
718 \\ ```
719 \\ Indentation up to the fence is removed.
720 \\ Like this.
721 \\ Doesn't need to be fully indented.
722 \\ ```
723 \\```
724 \\Overly indented closing fence is fine:
725 \\ ```
726 \\
727 ,
728 \\<pre><code>Hello, world!
729 \\This is some code.
730 \\</code></pre>
731 \\<pre><code>const std = @import("std");
732 \\
733 \\test {
734 \\ try std.testing.expect(2 + 2 == 4);
735 \\}
736 \\</code></pre>
737 \\<pre><code>Indentation up to the fence is removed.
738 \\ Like this.
739 \\Doesn't need to be fully indented.
740 \\</code></pre>
741 \\<pre><code>Overly indented closing fence is fine:
742 \\</code></pre>
743 \\
744 );
745}
746
747test "blockquotes" {
748 try testRender(
749 \\> > You miss 100% of the shots you don't take.
750 \\> >
751 \\> > ~ Wayne Gretzky
752 \\>
753 \\> ~ Michael Scott
754 \\
755 ,
756 \\<blockquote>
757 \\<blockquote>
758 \\<p>You miss 100% of the shots you don't take.</p>
759 \\<p>~ Wayne Gretzky</p>
760 \\</blockquote>
761 \\<p>~ Michael Scott</p>
762 \\</blockquote>
763 \\
764 );
765}
766
767test "blockquote lazy continuation lines" {
768 try testRender(
769 \\>>>>Deeply nested blockquote
770 \\>>which continues on another line
771 \\and then yet another one.
772 \\>>
773 \\>> But now two of them have been closed.
774 \\
775 \\And then there were none.
776 \\
777 ,
778 \\<blockquote>
779 \\<blockquote>
780 \\<blockquote>
781 \\<blockquote>
782 \\<p>Deeply nested blockquote
783 \\which continues on another line
784 \\and then yet another one.</p>
785 \\</blockquote>
786 \\</blockquote>
787 \\<p>But now two of them have been closed.</p>
788 \\</blockquote>
789 \\</blockquote>
790 \\<p>And then there were none.</p>
791 \\
792 );
793}
794
795test "paragraphs" {
796 try testRender(
797 \\Paragraph one.
798 \\
799 \\Paragraph two.
800 \\Still in the paragraph.
801 \\ So is this.
802 \\
803 \\
804 \\
805 \\
806 \\ Last paragraph.
807 \\
808 ,
809 \\<p>Paragraph one.</p>
810 \\<p>Paragraph two.
811 \\Still in the paragraph.
812 \\So is this.</p>
813 \\<p>Last paragraph.</p>
814 \\
815 );
816}
817
818test "thematic breaks" {
819 try testRender(
820 \\---
821 \\***
822 \\___
823 \\ ---
824 \\ - - - - - - - - - - -
825 \\
826 ,
827 \\<hr />
828 \\<hr />
829 \\<hr />
830 \\<hr />
831 \\<hr />
832 \\
833 );
834}
835
836test "links" {
837 try testRender(
838 \\[Link](https://example.com)
839 \\[Link *with inlines*](https://example.com)
840 \\[Nested parens](https://example.com/nested(parens(inside)))
841 \\[Escaped parens](https://example.com/\)escaped\()
842 \\[Line break in target](test\
843 \\target)
844 \\
845 ,
846 \\<p><a href="https://example.com">Link</a>
847 \\<a href="https://example.com">Link <em>with inlines</em></a>
848 \\<a href="https://example.com/nested(parens(inside))">Nested parens</a>
849 \\<a href="https://example.com/)escaped(">Escaped parens</a>
850 \\<a href="test\
851 \\target">Line break in target</a></p>
852 \\
853 );
854}
855
856test "autolinks" {
857 try testRender(
858 \\<https://example.com>
859 \\**This is important: <https://example.com/strong>**
860 \\<https://example.com?query=abc.123#page(parens)>
861 \\<placeholder>
862 \\<data:>
863 \\1 < 2
864 \\4 > 3
865 \\Unclosed: <
866 \\
867 ,
868 \\<p><a href="https://example.com">https://example.com</a>
869 \\<strong>This is important: <a href="https://example.com/strong">https://example.com/strong</a></strong>
870 \\<a href="https://example.com?query=abc.123#page(parens)">https://example.com?query=abc.123#page(parens)</a>
871 \\<placeholder>
872 \\<a href="data:">data:</a>
873 \\1 < 2
874 \\4 > 3
875 \\Unclosed: <</p>
876 \\
877 );
878}
879
880test "text autolinks" {
881 try testRender(
882 \\Text autolinks must start with http:// or https://.
883 \\This doesn't count: ftp://example.com.
884 \\Example: https://ziglang.org.
885 \\Here is an important link: **http://example.com**
886 \\(Links may be in parentheses: https://example.com/?q=(parens))
887 \\Escaping a link so it's plain text: https\://example.com
888 \\
889 ,
890 \\<p>Text autolinks must start with http:// or https://.
891 \\This doesn't count: ftp://example.com.
892 \\Example: <a href="https://ziglang.org">https://ziglang.org</a>.
893 \\Here is an important link: <strong><a href="http://example.com">http://example.com</a></strong>
894 \\(Links may be in parentheses: <a href="https://example.com/?q=(parens)">https://example.com/?q=(parens)</a>)
895 \\Escaping a link so it's plain text: https://example.com</p>
896 \\
897 );
898}
899
900test "images" {
901 try testRender(
902 \\
903 \\
904 \\).png)
905 \\escaped\(.png)
906 \\
908 \\
909 ,
910 \\<p><img src="https://example.com/image.png" alt="Alt text" />
911 \\<img src="https://example.com/image.png" alt="Alt text with inlines" />
912 \\<img src="https://example.com/nested(parens(inside)).png" alt="Nested parens" />
913 \\<img src="https://example.com/)escaped(.png" alt="Escaped parens" />
914 \\<img src="test\
915 \\target" alt="Line break in target" /></p>
916 \\
917 );
918}
919
920test "emphasis" {
921 try testRender(
922 \\*Emphasis.*
923 \\**Strong.**
924 \\***Strong emphasis.***
925 \\****More...****
926 \\*****MORE...*****
927 \\******Even more...******
928 \\*******OK, this is enough.*******
929 \\
930 ,
931 \\<p><em>Emphasis.</em>
932 \\<strong>Strong.</strong>
933 \\<em><strong>Strong emphasis.</strong></em>
934 \\<em><strong><em>More...</em></strong></em>
935 \\<em><strong><strong>MORE...</strong></strong></em>
936 \\<em><strong><em><strong>Even more...</strong></em></strong></em>
937 \\<em><strong><em><strong><em>OK, this is enough.</em></strong></em></strong></em></p>
938 \\
939 );
940 try testRender(
941 \\_Emphasis._
942 \\__Strong.__
943 \\___Strong emphasis.___
944 \\____More...____
945 \\_____MORE..._____
946 \\______Even more...______
947 \\_______OK, this is enough._______
948 \\
949 ,
950 \\<p><em>Emphasis.</em>
951 \\<strong>Strong.</strong>
952 \\<em><strong>Strong emphasis.</strong></em>
953 \\<em><strong><em>More...</em></strong></em>
954 \\<em><strong><strong>MORE...</strong></strong></em>
955 \\<em><strong><em><strong>Even more...</strong></em></strong></em>
956 \\<em><strong><em><strong><em>OK, this is enough.</em></strong></em></strong></em></p>
957 \\
958 );
959}
960
961test "nested emphasis" {
962 try testRender(
963 \\**Hello, *world!***
964 \\*Hello, **world!***
965 \\**Hello, _world!_**
966 \\_Hello, **world!**_
967 \\*Hello, **nested** *world!**
968 \\***Hello,* world!**
969 \\__**Hello, world!**__
970 \\****Hello,** world!**
971 \\__Hello,_ world!_
972 \\*Test**123*
973 \\__Test____123__
974 \\
975 ,
976 \\<p><strong>Hello, <em>world!</em></strong>
977 \\<em>Hello, <strong>world!</strong></em>
978 \\<strong>Hello, <em>world!</em></strong>
979 \\<em>Hello, <strong>world!</strong></em>
980 \\<em>Hello, <strong>nested</strong> <em>world!</em></em>
981 \\<strong><em>Hello,</em> world!</strong>
982 \\<strong><strong>Hello, world!</strong></strong>
983 \\<strong><strong>Hello,</strong> world!</strong>
984 \\<em><em>Hello,</em> world!</em>
985 \\<em>Test</em><em>123</em>
986 \\<strong>Test____123</strong></p>
987 \\
988 );
989}
990
991test "emphasis precedence" {
992 try testRender(
993 \\*First one _wins*_.
994 \\_*No other __rule matters.*_
995 \\
996 ,
997 \\<p><em>First one _wins</em>_.
998 \\<em><em>No other __rule matters.</em></em></p>
999 \\
1000 );
1001}
1002
1003test "emphasis open and close" {
1004 try testRender(
1005 \\Cannot open: *
1006 \\Cannot open: _
1007 \\*Cannot close: *
1008 \\_Cannot close: _
1009 \\
1010 \\foo*bar*baz
1011 \\foo_bar_baz
1012 \\foo**bar**baz
1013 \\foo__bar__baz
1014 \\
1015 ,
1016 \\<p>Cannot open: *
1017 \\Cannot open: _
1018 \\*Cannot close: *
1019 \\_Cannot close: _</p>
1020 \\<p>foo<em>bar</em>baz
1021 \\foo_bar_baz
1022 \\foo<strong>bar</strong>baz
1023 \\foo__bar__baz</p>
1024 \\
1025 );
1026}
1027
1028test "code spans" {
1029 try testRender(
1030 \\`Hello, world!`
1031 \\```Multiple `backticks` can be used.```
1032 \\`**This** does not produce emphasis.`
1033 \\`` `Backtick enclosed string.` ``
1034 \\`Delimiter lengths ```must``` match.`
1035 \\
1036 \\Unterminated ``code...
1037 \\
1038 \\Weird empty code span: `
1039 \\
1040 \\**Very important code: `hi`**
1041 \\
1042 ,
1043 \\<p><code>Hello, world!</code>
1044 \\<code>Multiple `backticks` can be used.</code>
1045 \\<code>**This** does not produce emphasis.</code>
1046 \\<code>`Backtick enclosed string.`</code>
1047 \\<code>Delimiter lengths ```must``` match.</code></p>
1048 \\<p>Unterminated <code>code...</code></p>
1049 \\<p>Weird empty code span: <code></code></p>
1050 \\<p><strong>Very important code: <code>hi</code></strong></p>
1051 \\
1052 );
1053}
1054
1055test "backslash escapes" {
1056 try testRender(
1057 \\Not \*emphasized\*.
1058 \\Literal \\backslashes\\.
1059 \\Not code: \`hi\`.
1060 \\\# Not a title.
1061 \\#\# Also not a title.
1062 \\\> Not a blockquote.
1063 \\\- Not a list item.
1064 \\\| Not a table. |
1065 \\| Also not a table. \|
1066 \\Any \punctuation\ characte\r can be escaped:
1067 \\\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~
1068 \\
1069 ,
1070 \\<p>Not *emphasized*.
1071 \\Literal \backslashes\.
1072 \\Not code: `hi`.
1073 \\# Not a title.
1074 \\## Also not a title.
1075 \\> Not a blockquote.
1076 \\- Not a list item.
1077 \\| Not a table. |
1078 \\| Also not a table. |
1079 \\Any \punctuation\ characte\r can be escaped:
1080 \\!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~</p>
1081 \\
1082 );
1083}
1084
1085test "hard line breaks" {
1086 try testRender(
1087 \\The iguana sits\
1088 \\Perched atop a short desk chair\
1089 \\Writing code in Zig
1090 \\
1091 ,
1092 \\<p>The iguana sits<br />
1093 \\Perched atop a short desk chair<br />
1094 \\Writing code in Zig</p>
1095 \\
1096 );
1097}
1098
1099test "Unicode handling" {
1100 // Null bytes must be replaced.
1101 try testRender("\x00\x00\x00", "<p>\u{FFFD}\u{FFFD}\u{FFFD}</p>\n");
1102
1103 // Invalid UTF-8 must be replaced.
1104 try testRender("\xC0\x80\xE0\x80\x80\xF0\x80\x80\x80", "<p>\u{FFFD}\u{FFFD}\u{FFFD}</p>\n");
1105 try testRender("\xED\xA0\x80\xED\xBF\xBF", "<p>\u{FFFD}\u{FFFD}</p>\n");
1106
1107 // Incomplete UTF-8 must be replaced.
1108 try testRender("\xE2\x82", "<p>\u{FFFD}</p>\n");
1109}
1110
1111fn testRender(input: []const u8, expected: []const u8) !void {
1112 var parser = try Parser.init(testing.allocator);
1113 defer parser.deinit();
1114
1115 var lines = std.mem.splitScalar(u8, input, '\n');
1116 while (lines.next()) |line| {
1117 try parser.feedLine(line);
1118 }
1119 var doc = try parser.endInput();
1120 defer doc.deinit(testing.allocator);
1121
1122 var actual = std.array_list.Managed(u8).init(testing.allocator);
1123 defer actual.deinit();
1124 try doc.render(actual.writer());
1125
1126 try testing.expectEqualStrings(expected, actual.items);
1127}