From 5825f0d4955786bdd3930a0bf77c7b09cff59274 Mon Sep 17 00:00:00 2001
From: Juan Cruz Viotti kiwi mango kiwi ++++ = = = = ** __ Paragraph text\n"
+ "___ x______ x -*- alpha beta beta ######## title #1 fan ##tag # title text\n"
+ "## more one line next line Title\n"
+ "=== Title\n"
+ "== == ` Title\n"
+ "=== quote\n"
+ "continued == === Intro Body One\n"
+ "two three One\n"
+ "two three One\n"
+ "two\n"
+ "===\n"
+ "three Text\n"
+ "code text text text more ```js`\n"
+ "code [docs]: /manual "first second" [docs] [docs]: text [docs] See docs. [docs]: /manual 'Manual' trailing 'Manual' trailing [docs] [docs] Intro\n"
+ "[docs]: /manual [docs] quoted ---\n"
+ "docs []: /manual [] text [ ]: /manual [ ] one\n"
+ "two one\n"
+ "two\n"
+ "three first\n"
+ "second second one one text\n"
+ "more one\n"
+ "two\n"
+ "three one\n"
+ "two one\n"
+ "two\n"
+ "three one code one\n"
+ "* two text one two one two text quote one two one two one\n"
+ "two one\n"
+ "two three text two one two one two one two two *one 1)two one two one two quote one 9999999999) over -5) negative text text text text one text\n"
+ "1) one\n"
+ "two one\n"
+ "two quoted\n"
+ "lazy line Text We counted\n"
+ "42. items in total We counted one two b c a b a b c a b a b c a b b c b c b y a c a d text more text more one\n"
+ "two\n"
+ "three\n"
+ "four hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world after quote | a | b | c |\n"
+ "| - | - | intro\n"
+ "a | b a | b | a |\n"
+ "| :-:x | x | y | z x ~~~y~~~ z x ~~y x ~~ y ~~ z ~~x~~ see www.sourcemeta.com/docs/intro Go to www.sourcemeta.com/"
+ "x.y! https://"
+ "sourcemeta.com/wiki/Thing_(disambiguation) https://"
+ "sourcemeta.com/a_(b))) (see https://"
+ "sourcemeta.com/a_(b)) https://"
+ "sourcemeta.com/?x=1& https://"
+ "sourcemeta.com/?x=1&y=2
\n");
+}
+
+TEST(tab_after_space_indented_code_block) {
+ const auto result{sourcemeta::core::markdown_to_html(" \tlime\tpear")};
+ EXPECT_EQ(result, "lime\tpear\t\tplum\n"
+ "
\n");
+}
+
+TEST(tab_list_item_continuation) {
+ const auto result{sourcemeta::core::markdown_to_html(" - kiwi\n\n\tmango")};
+ EXPECT_EQ(result, "lime\tpear\n"
+ "\n"
+ "
\n");
+}
+
+TEST(tab_list_item_indented_code) {
+ const auto result{sourcemeta::core::markdown_to_html("* kiwi\n\n\t\tmango")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(tab_after_blockquote_marker) {
+ const auto result{sourcemeta::core::markdown_to_html(">\t\tcedar\n>\t\toak")};
+ EXPECT_EQ(result, "
\n"
+ " mango\n"
+ "\n"
+ "
\n");
+}
+
+TEST(tab_after_list_marker) {
+ const auto result{sourcemeta::core::markdown_to_html("+\t\tcedar")};
+ EXPECT_EQ(result, "
\n"
+ " cedar\n"
+ " oak\n"
+ "\n"
+ "
\n");
+}
+
+TEST(tab_after_heading_marker) {
+ const auto result{sourcemeta::core::markdown_to_html("##\tTitle")};
+ EXPECT_EQ(result, "
\n"
+ " cedar\n"
+ "Title
\n");
+}
+
+TEST(tab_between_thematic_break_characters) {
+ const auto result{sourcemeta::core::markdown_to_html("-\t-\t-")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(list_marker_takes_precedence_over_code_span) {
+ const auto result{sourcemeta::core::markdown_to_html("* `alpha\n* beta`")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(thematic_break_with_trailing_whitespace) {
+ const auto result{sourcemeta::core::markdown_to_html("***\t ")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(thematic_break_plus_signs_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("++++")};
+ EXPECT_EQ(result, "
\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(thematic_break_indented_four_spaces_is_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" ---")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(thematic_break_indented_four_spaces_continues_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("Paragraph text\n ___")};
+ EXPECT_EQ(result, "---\n"
+ "
\n");
+}
+
+TEST(thematic_break_spaces_between_characters) {
+ const auto result{sourcemeta::core::markdown_to_html("* * * *")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(thematic_break_with_other_characters_is_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("- - - x\n\nx______\n\n***x***")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "
\n"
+ "alpha
\n"
+ "\n"
+ "
\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(thematic_break_inside_list_item) {
+ const auto result{sourcemeta::core::markdown_to_html("* one\n* - - -")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(atx_heading_eight_hashes_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("######## title")};
+ EXPECT_EQ(result, "
\n"
+ "plain em *raw*
\n");
+}
+
+TEST(atx_heading_surrounding_spaces_ignored) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("### spaced out ")};
+ EXPECT_EQ(result, "spaced out
\n");
+}
+
+TEST(atx_heading_indented_up_to_three_spaces) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" #### deep\n # shallow")};
+ EXPECT_EQ(result, "deep
\n"
+ "shallow
\n");
+}
+
+TEST(atx_heading_indented_four_spaces_is_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" ## code")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(atx_heading_indented_four_spaces_continues_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("text\n ## more")};
+ EXPECT_EQ(result, "## code\n"
+ "title
\n"
+ "title
\n");
+}
+
+TEST(atx_heading_closing_sequence_followed_by_spaces) {
+ const auto result{sourcemeta::core::markdown_to_html("## title ## ")};
+ EXPECT_EQ(result, "title
\n");
+}
+
+TEST(atx_heading_hashes_followed_by_text_are_content) {
+ const auto result{sourcemeta::core::markdown_to_html("## title ## tail")};
+ EXPECT_EQ(result, "title ## tail
\n");
+}
+
+TEST(atx_heading_closing_sequence_needs_preceding_space) {
+ const auto result{sourcemeta::core::markdown_to_html("## title##")};
+ EXPECT_EQ(result, "title##
\n");
+}
+
+TEST(atx_heading_escaped_closing_hashes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("## title \\##\n# title #\\#")};
+ EXPECT_EQ(result, "title ##
\n"
+ "title ##
\n");
+}
+
+TEST(atx_heading_between_thematic_breaks) {
+ const auto result{sourcemeta::core::markdown_to_html("---\n# title\n---")};
+ EXPECT_EQ(result, "
\n"
+ "title
\n"
+ "
\n");
+}
+
+TEST(atx_heading_interrupts_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("one line\n## heading\nnext line")};
+ EXPECT_EQ(result, "heading
\n"
+ "Title em
\n");
+}
+
+TEST(setext_heading_multiline_content) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("First *line\nsecond* line\n---")};
+ EXPECT_EQ(result, "First line\n"
+ "second line
\n");
+}
+
+TEST(setext_heading_content_surrounding_spaces_stripped) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" Title *em\ntext* \n===")};
+ EXPECT_EQ(result, "Title em\n"
+ "text
\n");
+}
+
+TEST(setext_heading_indented_content) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" Title\n ===\n\n Other\n-")};
+ EXPECT_EQ(result, "Title
\n"
+ "Other
\n");
+}
+
+TEST(setext_heading_content_indented_four_spaces_is_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" Title\n ===")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(setext_heading_underline_indented_up_to_three_spaces) {
+ const auto result{sourcemeta::core::markdown_to_html("Title\n === ")};
+ EXPECT_EQ(result, "Title\n"
+ "===\n"
+ "Title
\n");
+}
+
+TEST(setext_heading_underline_indented_four_spaces_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("Title\n ===")};
+ EXPECT_EQ(result, "Title
\n");
+}
+
+TEST(setext_heading_trailing_backslash_is_literal) {
+ const auto result{sourcemeta::core::markdown_to_html("Title\\\n===")};
+ EXPECT_EQ(result, "Title\\
\n");
+}
+
+TEST(setext_heading_underline_breaks_code_span) {
+ const auto result{sourcemeta::core::markdown_to_html("`code\n===\n`")};
+ EXPECT_EQ(result, "`code
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(setext_heading_dashes_after_lazy_line_are_thematic_break) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> quote\ncontinued\n---")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(setext_heading_equals_after_list_item_is_lazy_continuation) {
+ const auto result{sourcemeta::core::markdown_to_html("* Item\n===")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(setext_heading_multiline_paragraph_content) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("Line one\nLine two\n===")};
+ EXPECT_EQ(result, "Line one\n"
+ "Line two
\n");
+}
+
+TEST(setext_heading_equals_line_then_heading) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("===\nTitle\n===\nText\n---")};
+ EXPECT_EQ(result, "===\n"
+ "Title
\n"
+ "Text
\n");
+}
+
+TEST(setext_heading_underline_without_content_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("\n==")};
+ EXPECT_EQ(result, "
\n"
+ "
\n");
+}
+
+TEST(setext_heading_dashes_after_list_item) {
+ const auto result{sourcemeta::core::markdown_to_html("* item\n---")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(setext_heading_equals_after_indented_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" code\n===")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "- not a list
\n");
+}
+
+TEST(setext_heading_after_blank_line_separated_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("Intro\n\nTitle\n===\nBody")};
+ EXPECT_EQ(result, "Title
\n"
+ "
\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(indented_code_keeps_extra_indentation_on_blank_lines) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ " first\n \n second")};
+ EXPECT_EQ(result, "first\n"
+ "\n"
+ "second\n"
+ "\n"
+ "\n"
+ "third\n"
+ "
\n");
+}
+
+TEST(indented_code_cannot_interrupt_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("Text\n code")};
+ EXPECT_EQ(result, "first\n"
+ " \n"
+ " second\n"
+ "
\n"
+ "code\n"
+ "Top
\n"
+ "
\n"
+ "code\n"
+ "Bottom
\n"
+ "
\n"
+ "code\n"
+ "
\n");
+}
+
+TEST(indented_code_first_line_extra_indentation) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" alpha\n beta")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(indented_code_surrounding_blank_lines_dropped) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" \n\n code\n\n \n")};
+ EXPECT_EQ(result, " alpha\n"
+ "beta\n"
+ "
\n");
+}
+
+TEST(indented_code_trailing_whitespace_kept) {
+ const auto result{sourcemeta::core::markdown_to_html(" code\t ")};
+ EXPECT_EQ(result, "code\n"
+ "
\n");
+}
+
+TEST(indented_code_content_is_not_parsed) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ " **bold**\n [link](/x)\n\n 1. item")};
+ EXPECT_EQ(result, "code\t \n"
+ "
\n");
+}
+
+TEST(fenced_code_closing_fence_longer_than_opening) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n~~~~~")};
+ EXPECT_EQ(result, "**bold**\n"
+ "[link](/x)\n"
+ "\n"
+ "1. item\n"
+ "
\n");
+}
+
+TEST(fenced_code_tildes_not_closed_by_backticks) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("~~~~\ncode\n````\n~~~~")};
+ EXPECT_EQ(result, "code\n"
+ "
\n");
+}
+
+TEST(fenced_code_backticks_not_closed_by_tildes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("````\ncode\n~~~~\n````")};
+ EXPECT_EQ(result, "code\n"
+ "````\n"
+ "
\n");
+}
+
+TEST(fenced_code_unclosed_empty) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~")};
+ EXPECT_EQ(result, "code\n"
+ "~~~~\n"
+ "
\n");
+}
+
+TEST(fenced_code_unclosed_runs_to_end_of_document) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~~\n\n~~~\ntext")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(fenced_code_unclosed_ends_with_blockquote) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> ~~~\n> code\n\ntext")};
+ EXPECT_EQ(result, "\n"
+ "~~~\n"
+ "text\n"
+ "\n"
+ "
\n"
+ "
\n"
+ "code\n"
+ "
\n");
+}
+
+TEST(fenced_code_empty) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~\n~~~")};
+ EXPECT_EQ(result, " \n"
+ "\n"
+ "
\n");
+}
+
+TEST(fenced_code_indentation_removed_from_content) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" ~~~\n one\n two\nthree\n ~~~")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(fenced_code_indentation_removed_up_to_fence_indentation) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" ```\n one\n two\n```")};
+ EXPECT_EQ(result, "one\n"
+ "two\n"
+ "three\n"
+ "
\n");
+}
+
+TEST(fenced_code_fence_indented_four_spaces_is_indented_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" ~~~\n code\n ~~~")};
+ EXPECT_EQ(result, " one\n"
+ " two\n"
+ "
\n");
+}
+
+TEST(fenced_code_closing_fence_indented_differently) {
+ const auto result{sourcemeta::core::markdown_to_html(" ~~~\ncode\n ~~~")};
+ EXPECT_EQ(result, "~~~\n"
+ "code\n"
+ "~~~\n"
+ "
\n");
+}
+
+TEST(fenced_code_closing_fence_indented_four_spaces_is_content) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n ~~~")};
+ EXPECT_EQ(result, "code\n"
+ "
\n");
+}
+
+TEST(fenced_code_closing_fence_with_internal_space_is_content) {
+ const auto result{sourcemeta::core::markdown_to_html("```\ncode\n`` ``")};
+ EXPECT_EQ(result, "code\n"
+ " ~~~\n"
+ "
\n");
+}
+
+TEST(fenced_code_backtick_info_with_backticks_is_code_span) {
+ const auto result{sourcemeta::core::markdown_to_html("``` x ```\ncode")};
+ EXPECT_EQ(result, "code\n"
+ "`` ``\n"
+ "x\n"
+ "code
\n"
+ "code\n"
+ "
\n");
+}
+
+TEST(fenced_code_info_string_of_symbols) {
+ const auto result{sourcemeta::core::markdown_to_html("```#!\n```")};
+ EXPECT_EQ(result, "print(1)\n"
+ "
\n");
+}
+
+TEST(fenced_code_backtick_fence_info_cannot_contain_backticks) {
+ const auto result{sourcemeta::core::markdown_to_html("```js`\ncode")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(fenced_code_closing_fence_cannot_have_info) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("~~~\n~~~ closing\n~~~")};
+ EXPECT_EQ(result, "code\n"
+ "
\n");
+}
+
+TEST(fenced_code_info_string_entities_decoded) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("```c++\ncode\n```")};
+ EXPECT_EQ(result, "~~~ closing\n"
+ "
\n");
+}
+
+TEST(fenced_code_info_string_backslash_escapes) {
+ const auto result{sourcemeta::core::markdown_to_html("```c\\#\ncode\n```")};
+ EXPECT_EQ(result, "code\n"
+ "
\n");
+}
+
+TEST(link_reference_definition_title_on_next_line) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "[docs]: /manual\n\"The manual\"\n\n[docs]")};
+ EXPECT_EQ(result,
+ "\n");
+}
+
+TEST(link_reference_definition_spread_over_lines) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ " [docs]:\n /manual\n (Manual) \n\n[docs]")};
+ EXPECT_EQ(result, "\n");
+}
+
+TEST(link_reference_definition_multiline_title) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "[docs]: /manual \"\nfirst\nsecond\n\"\n\n[docs]")};
+ EXPECT_EQ(result, "\n");
+}
+
+TEST(link_reference_definition_title_with_blank_line_is_invalid) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "[docs]: /manual \"first\n\nsecond\"\n\n[docs]")};
+ EXPECT_EQ(result, "code\n"
+ "
\n"
+ "[docs]: /manual\n"
+ "
\n"
+ "[docs]: /manual\n"
+ "Docs
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(link_reference_definition_followed_by_dashes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("[docs]: /manual\n---\n[docs]")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n");
+}
+
+TEST(link_reference_definition_label_whitespace_normalized) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "[the long\n label]: /manual\n\n[The long label]")};
+ EXPECT_EQ(result, "\n");
+}
+
+TEST(link_reference_definition_empty_label_is_invalid) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("[]: /manual\n\n[] text")};
+ EXPECT_EQ(result, "
\n"
+ " first\n"
+ "
\n"
+ "twotwo
\n");
+}
+
+TEST(blockquote_marker_without_following_space) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(">## Title\n>text\n> more")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_marker_indented_up_to_three_spaces) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" > one\n > two\n > three")};
+ EXPECT_EQ(result, "Title
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_marker_indented_four_spaces_is_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" > one\n > two")};
+ EXPECT_EQ(result, "
\n");
+}
+
+TEST(blockquote_lazy_paragraph_continuation) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\ntwo")};
+ EXPECT_EQ(result, "> one\n"
+ "> two\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_lazy_continuation_between_marked_lines) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\ntwo\n> three")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_laziness_does_not_apply_to_thematic_break) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n***")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(blockquote_laziness_does_not_apply_to_list) {
+ const auto result{sourcemeta::core::markdown_to_html("> * one\n* two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_laziness_does_not_apply_to_indented_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> code\n more")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n"
+ "code\n"
+ "
\n");
+}
+
+TEST(blockquote_laziness_does_not_apply_to_fenced_code) {
+ const auto result{sourcemeta::core::markdown_to_html("> ~~~\ncode\n~~~")};
+ EXPECT_EQ(result, " more\n"
+ "\n"
+ "
\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(blockquote_lazy_line_that_looks_like_list_item) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n * two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_empty_indented_marker) {
+ const auto result{sourcemeta::core::markdown_to_html(" >")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_empty_multiple_lines) {
+ const auto result{sourcemeta::core::markdown_to_html(">\n>\n> ")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_blank_first_and_last_lines) {
+ const auto result{sourcemeta::core::markdown_to_html("> \n> text\n>")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_blank_line_separates_blockquotes) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n\n> two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_empty_marker_line_joins_paragraphs) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n> \n> two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_interrupts_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("text\n>quote")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(blockquote_ended_by_thematic_break) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n- - -\n> two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_lazy_line_after_blank_marker_line) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\n>\ntwo")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_nested_markers_without_spaces) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(">>one\n>two\n>>>three")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(blockquote_indented_code_needs_five_spaces) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> code\n\n> text")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "
\n"
+ " code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_content_needs_marker_width_indentation) {
+ const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_wide_marker_content_not_indented_enough) {
+ const auto result{sourcemeta::core::markdown_to_html("1. one\n\n two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n");
+}
+
+TEST(list_item_wide_marker_content_indented_enough) {
+ const auto result{sourcemeta::core::markdown_to_html("1. one\n\n two")};
+ EXPECT_EQ(result, "two\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_inside_nested_blockquotes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> > - one\n> >\n> > two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_item_inside_nested_blockquotes_not_indented_enough) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> > - one\n> >\n> > two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_marker_needs_following_space) {
+ const auto result{sourcemeta::core::markdown_to_html("*one\n\n1)two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_with_several_block_types) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "2. one\n\n ~~~\n code\n ~~~\n\n two\n\n > quote")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_item_code_block_with_blank_lines) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "* one\n\n code\n\n\n more")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(ordered_list_nine_digit_number) {
+ const auto result{sourcemeta::core::markdown_to_html("999999999) max")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "\n"
+ "more\n"
+ "\n"
+ "
\n");
+}
+
+TEST(ordered_list_ten_digit_number_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("9999999999) over")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_number_with_leading_zeros) {
+ const auto result{sourcemeta::core::markdown_to_html("0007. seven")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_negative_number_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("-5) negative")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_item_indented_code_after_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" 100. text\n\n code")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_starting_with_indented_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* code\n\n text\n\n more")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_starting_with_indented_code_and_extra_space) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* code\n\n text")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "
\n"
+ "more\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_starting_with_blank_line) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("*\n one\n*\n ~~~\n code\n ~~~")};
+ EXPECT_EQ(result, "
\n"
+ " code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_starting_with_blank_line_and_trailing_spaces) {
+ const auto result{sourcemeta::core::markdown_to_html("1. \n one")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_at_most_one_starting_blank_line) {
+ const auto result{sourcemeta::core::markdown_to_html("*\n\n one")};
+ EXPECT_EQ(result, "\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_empty_with_trailing_spaces) {
+ const auto result{sourcemeta::core::markdown_to_html("* one\n* \n* two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_item_empty_between_items) {
+ const auto result{sourcemeta::core::markdown_to_html("1) one\n2)\n3) two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_consisting_of_single_empty_item) {
+ const auto result{sourcemeta::core::markdown_to_html("-")};
+ EXPECT_EQ(result, "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_empty_cannot_interrupt_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("text\n-\n\ntext\n1)")};
+ EXPECT_EQ(result, "text
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_indented_four_spaces_is_code) {
+ const auto result{sourcemeta::core::markdown_to_html(" - one\n two")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "
\n");
+}
+
+TEST(list_item_lazy_continuation_lines) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" * one\ntwo\n\n code")};
+ EXPECT_EQ(result, "- one\n"
+ " two\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_partially_lazy_continuation) {
+ const auto result{sourcemeta::core::markdown_to_html(" * one\n two\nthree")};
+ EXPECT_EQ(result, "
\n"
+ "code\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_laziness_inside_nested_containers) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> * > quoted\nlazy line")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_nested_with_sufficient_indentation) {
+ const auto result{sourcemeta::core::markdown_to_html("* a\n * b\n * c")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_not_nested_with_one_space_increments) {
+ const auto result{sourcemeta::core::markdown_to_html("* a\n * b\n * c")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(ordered_list_nested_needs_marker_width_indentation) {
+ const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_not_nested_with_insufficient_indentation) {
+ const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_containing_only_list) {
+ const auto result{sourcemeta::core::markdown_to_html("+ + one")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_item_containing_nested_ordered_list) {
+ const auto result{sourcemeta::core::markdown_to_html("3) * 4) deep")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_item_containing_headings) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* ## Title\n* Text\n ===\n more")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_changing_bullet_character_starts_new_list) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* one\n* two\n- three")};
+ EXPECT_EQ(result, "Title
\n"
+ "Text
\n"
+ "more\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_changing_delimiter_starts_new_list) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("1) one\n2) two\n3. three")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_interrupts_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("Text\n* one\n* two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_not_starting_at_one_cannot_interrupt_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("We counted\n42. items in total")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_items_separated_by_multiple_blank_lines) {
+ const auto result{sourcemeta::core::markdown_to_html("* one\n\n\n* two")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_nested_item_with_blank_lines) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n * b\n\n\n c")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_separated_by_html_comment) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* one\n\n\n\n* two")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_items_with_varying_marker_indentation) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n * b\n * c\n * d\n* e")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_item_indented_four_spaces_continues_previous_item) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n * b\n * c\n * d")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(ordered_list_item_indented_five_spaces_is_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("1) a\n\n 2) b\n\n 3) c")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_loose_with_blank_line_between_items) {
+ const auto result{sourcemeta::core::markdown_to_html("* a\n\n* b\n* c")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_loose_with_empty_item) {
+ const auto result{sourcemeta::core::markdown_to_html("- a\n-\n\n- b")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_loose_with_blank_line_inside_item) {
+ const auto result{sourcemeta::core::markdown_to_html("* a\n\n b\n* c")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_loose_with_blank_line_before_reference_definition) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n\n [r]: /r\n* b")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_tight_with_blank_lines_inside_code_block) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n* ~~~\n x\n\n ~~~\n* b")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(list_tight_with_loose_sublist) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n * b\n\n * c\n* d")};
+ EXPECT_EQ(result, "
\n"
+ "x\n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_tight_with_blank_blockquote_line) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("- a\n > b\n >\n > c\n- d")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_tight_with_blockquote_and_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n > b\n ~~~\n c\n ~~~\n* d")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_single_tight_nested_ordered_item) {
+ const auto result{sourcemeta::core::markdown_to_html("1. a\n 1. b")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "
\n"
+ "c\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_loose_single_item_with_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* ~~~\n x\n ~~~\n\n y")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_outer_loose_inner_tight) {
+ const auto result{sourcemeta::core::markdown_to_html("1. a\n - b\n\n c")};
+ EXPECT_EQ(result, "
\n"
+ "x\n"
+ "\n"
+ "
\n");
+}
+
+TEST(list_tight_sublist_inside_loose_list) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a\n 1. b\n 2. c\n\n* d")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(crlf_line_endings) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("## Title\r\ntext\r\n\r\nmore")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "Title
\n"
+ "Title
\n"
+ "\n"
+ "
\n");
+}
diff --git a/test/markdown/markdown_concurrency_test.cc b/test/markdown/markdown_concurrency_test.cc
new file mode 100644
index 0000000000..36d35822f0
--- /dev/null
+++ b/test/markdown/markdown_concurrency_test.cc
@@ -0,0 +1,42 @@
+#include \n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_ends_at_blank_line) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "| a | b |\n|---|---|\n| 1 | 2 |\n\nafter")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "code \n"
+ "\n"
+ " \n"
+ " \n"
+ "a|b\n"
+ " \n"
+ "\n"
+ "c|d \n"
+ "\n"
+ "\n"
+ "
\n"
+ "\n"
+ " \n"
+ "\n"
+ "\n"
+ "a \n"
+ "b \n"
+ "\n"
+ " \n"
+ "\n"
+ "1 \n"
+ "2 \n"
+ "\n"
+ "\n"
+ "
\n"
+ "\n"
+ " \n"
+ "\n"
+ "\n"
+ "a \n"
+ "\n"
+ " \n"
+ "\n"
+ "1 \n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_line_without_pipes_is_row) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| a | b |\n|---|---|\ntext row")};
+ EXPECT_EQ(result, "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_delimiter_count_mismatch_is_not_table) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| a | b | c |\n| - | - |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "a \n"
+ "b \n"
+ "\n"
+ " \n"
+ "\n"
+ "text row \n"
+ "\n"
+ " \n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_without_outer_pipes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("x | y\n:-- | --:\n1 | 2")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "a \n"
+ "b \n"
+ "\n"
+ " \n"
+ "1 \n"
+ "\n"
+ " \n"
+ " \n"
+ "\n"
+ "1 \n"
+ "2 \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_single_column_with_pipes) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| only |\n| :-: |\n| one |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "x \n"
+ "y \n"
+ "\n"
+ " \n"
+ "\n"
+ "1 \n"
+ "2 \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_every_alignment) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "| l | r | c | n |\n|:--|--:|:-:|---|\n| 1 | 2 | 3 | 4 |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "only \n"
+ "\n"
+ " \n"
+ "\n"
+ "one \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_delimiter_row_with_surrounding_spaces) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("x|y\n --- | :---: ")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "l \n"
+ "r \n"
+ "c \n"
+ "n \n"
+ "\n"
+ " \n"
+ "\n"
+ "1 \n"
+ "2 \n"
+ "3 \n"
+ "4 \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_header_cell_with_escaped_pipe) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| a \\| b | c |\n| - | - |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "x \n"
+ "y \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_cell_with_reference_link) {
+ const auto result{sourcemeta::core::markdown_to_html(
+ "| [manual] |\n| --- |\n\n[manual]: /manual")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "a | b \n"
+ "c \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_interrupts_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("intro\na | b\n- | -")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "manual \n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_inside_list_item) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* a | b\n - | -\n 1 | 2")};
+ EXPECT_EQ(result, "\n"
+ "
\n");
+}
+
+TEST(table_inside_blockquote) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("> a | b\n> - | -\n> 1 | 2")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_cell_with_entities) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| <tag> |\n| --- |")};
+ EXPECT_EQ(result, "\n"
+ "
\n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_empty_header_cells) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| | |\n|--|--|\n| x | y |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "<tag> \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_emphasis_does_not_span_cells) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| _a | b_ |\n|---|---|")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "\n"
+ " \n"
+ " \n"
+ " \n"
+ "\n"
+ "x \n"
+ "y \n"
+ "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_invalid_delimiter_row_is_paragraph) {
+ const auto result{sourcemeta::core::markdown_to_html("| a |\n| :-:x |")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "_a \n"
+ "b_ \n"
+ "\n"
+ "
\n");
+}
+
+TEST(table_leading_pipe_without_trailing_pipe) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("| x | y\n|---|---\n| 1 | 2")};
+ EXPECT_EQ(result, "\n"
+ "\n"
+ "
\n");
+}
+
+TEST(strikethrough_spanning_lines) {
+ const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")};
+ EXPECT_EQ(result, "\n"
+ " \n"
+ "\n"
+ "\n"
+ "x \n"
+ "y \n"
+ "\n"
+ " \n"
+ "\n"
+ "1 \n"
+ "2 \n"
+ "one\n"
+ "two_x_
www.sourcemeta._com
\n" + "www.source_meta.com
\n"); +} + +TEST(extended_autolink_underscore_in_earlier_domain_segment) { + const auto result{ + sourcemeta::core::markdown_to_html("www.docs_site.sourcemeta.com")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(extended_autolink_http_with_query_and_fragment) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/search?q=json+schema#results")}; + EXPECT_EQ( + result, + "https://" + "sourcemeta.com/search?q=json+schema#results
\n"); +} + +TEST(extended_autolink_email_local_and_domain_symbols) { + const auto result{sourcemeta::core::markdown_to_html( + "team@sourcemeta.com\n\nteam@source+meta.com\n\nteam+tag@sourcemeta." + "com")}; + EXPECT_EQ( + result, + "\n" + "team@source+meta.com
\n" + "team+tag@sourcemeta.com" + "p>\n"); +} + +TEST(extended_autolink_email_trailing_symbols) { + const auto result{sourcemeta::core::markdown_to_html( + "first.last@sourcemeta.com.\n\nfirst.last@sourcemeta.com-\n\nfirst.last@" + "sourcemeta.com_")}; + EXPECT_EQ(result, "
first.last@" + "sourcemeta.com.
\n" + "first.last@sourcemeta.com-
\n" + "first.last@sourcemeta.com_
\n"); +} + +TEST(extended_autolink_mailto_and_xmpp) { + const auto result{sourcemeta::core::markdown_to_html( + "mailto:team@sourcemeta.com\n\nxmpp:team@sourcemeta.com/resource")}; + EXPECT_EQ( + result, + "\n" + "xmpp:team@sourcemeta.com/" + "resource
\n"); +} + +TEST(extended_autolink_not_after_word_character) { + const auto result{sourcemeta::core::markdown_to_html("xwww.sourcemeta.com")}; + EXPECT_EQ(result, "xwww.sourcemeta.com
\n"); +} + +TEST(extended_autolink_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_https://sourcemeta.com_")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(extended_autolink_ftp_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("ftp://files.sourcemeta.com/archive")}; + EXPECT_EQ(result, "ftp://" + "files.sourcemeta.com/archive
\n"); +} + +TEST(extended_autolink_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[https://sourcemeta.com](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(extended_autolink_inside_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("`https://sourcemeta.com`")}; + EXPECT_EQ(result, "https://sourcemeta.com
a
\n" + "b
\n" + "body
\n"); +} + +TEST(footnote_reference_without_definition) { + const auto result{sourcemeta::core::markdown_to_html("body[^ghost]")}; + EXPECT_EQ(result, "body[^ghost]
\n"); +} + +TEST(footnote_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^LABEL]\n\n[^label]: text")}; + EXPECT_EQ(result, + "body1
\n" + "text \xe2\x86\xa9
\n" + "body1
\n" + "first
\n" + "\n" + "\n" + "quoted
\n" + "
body1
\n" + "text \xe2\x86\xa9
\n" + "| h | \n" + "
|---|
| c1 | \n" + "
table note \xe2\x86\xa9
\n" + "body1
\n" + "refers2 \xe2\x86\xa9
\n" + "nested \xe2\x86\xa9
\n" + "body1
\n" + "\n" + "\n" + "
inside quote \xe2\x86\xa9
\n" + "body^two words
\n"); +} diff --git a/test/markdown/markdown_inlines_test.cc b/test/markdown/markdown_inlines_test.cc new file mode 100644 index 0000000000..47d30d13dd --- /dev/null +++ b/test/markdown/markdown_inlines_test.cc @@ -0,0 +1,1039 @@ +#includea~b}c|d{e`f_g^h]i\\j[k@l?m>n=o<p;q:r/" + "s.t-u,v+w*x)y(z'1&2%3$4#5"6!
\n"); +} + +TEST(backslash_before_other_characters_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("\\Z \\9 \\é \\€ \\\t")}; + EXPECT_EQ(result, "\\Z \\9 \\\xc3\xa9 \\\xe2\x82\xac \\
\n"); +} + +TEST(backslash_escaped_markup_is_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "\\_no emphasis_\n\\ no tag\n\\\n2\\) no " + "list\n\\+ no list\n\\> no quote\n\\& no entity")}; + EXPECT_EQ(result, "_no emphasis_\n" + "<em> no tag\n" + "!no image\n" + "2) no list\n" + "+ no list\n" + "> no quote\n" + "& no entity
\n"); +} + +TEST(backslash_escaped_backslash_before_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("\\\\_emphasis_")}; + EXPECT_EQ(result, "\\emphasis
\n"); +} + +TEST(backslash_at_end_of_line_is_hard_break) { + const auto result{sourcemeta::core::markdown_to_html("one\\\ntwo")}; + EXPECT_EQ(result, "one
\n"
+ "two
\\*\\_
\\*\\_\n"
+ "\n");
+}
+
+TEST(backslash_inside_fenced_code_is_literal) {
+ const auto result{sourcemeta::core::markdown_to_html("```\n\\*\\_\n```")};
+ EXPECT_EQ(result, "\\*\\_\n"
+ "\n");
+}
+
+TEST(backslash_inside_autolink_is_literal) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("https://" + "sourcemeta.com/\\_\\*
\n"); +} + +TEST(backslash_escapes_in_link_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/a\\_b 'c\\_d')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(backslash_escapes_in_reference_definition) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]\n\n[docs]: /a\\_b 'c\\_d'")}; + EXPECT_EQ(result, "\n"); +} + +TEST(entity_named_references) { + const auto result{sourcemeta::core::markdown_to_html( + "< > " ' € …\n→ " + "⪡̸ ★")}; + EXPECT_EQ(result, "< > " ' \xe2\x82\xac \xe2\x80\xa6\n" + "\xe2\x86\x92 \xe2\xaa\xa1\xcc\xb8 \xe2\x98\x85
\n"); +} + +TEST(entity_decimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("A € 😀")}; + EXPECT_EQ(result, "A \xe2\x82\xac \xf0\x9f\x98\x80
\n"); +} + +TEST(entity_hexadecimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("A € 😀")}; + EXPECT_EQ(result, "A \xe2\x82\xac \xf0\x9f\x98\x80
\n"); +} + +TEST(entity_invalid_references_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "&euro &y; z; z;\n\n\n&NotARealEntity;")}; + EXPECT_EQ(result, "&euro &y; &#z; &#xz;\n" + "\xef\xbf\xbd\n" + "\xef\xbf\xbd\n" + "&NotARealEntity;
\n"); +} + +TEST(entity_without_semicolon_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("&")}; + EXPECT_EQ(result, "&
\n"); +} + +TEST(entity_unknown_name_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("¬real;")}; + EXPECT_EQ(result, "¬real;
\n"); +} + +TEST(entity_code_point_beyond_unicode_range) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "\xef\xbf\xbd
\n"); +} + +TEST(entity_surrogate_code_point) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "\xef\xbf\xbd
\n"); +} + +TEST(entity_in_link_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/café 'café')")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(entity_in_reference_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /café 'café'")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(entity_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`é`")}; + EXPECT_EQ(result, "é
é&\n"
+ "\n");
+}
+
+TEST(entity_cannot_create_emphasis) {
+ const auto result{sourcemeta::core::markdown_to_html("_word_")};
+ EXPECT_EQ(result, "_word_
\n"); +} + +TEST(entity_cannot_create_list) { + const auto result{sourcemeta::core::markdown_to_html("- item")}; + EXPECT_EQ(result, "- item
\n"); +} + +TEST(entity_newline_is_not_line_break) { + const auto result{sourcemeta::core::markdown_to_html("one two")}; + EXPECT_EQ(result, "one\n" + "two
\n"); +} + +TEST(entity_tabs_are_not_indentation) { + const auto result{sourcemeta::core::markdown_to_html(" code")}; + EXPECT_EQ(result, "\t\tcode
\n"); +} + +TEST(entity_quotes_do_not_delimit_link_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/manual "Title")")}; + EXPECT_EQ(result, "[docs](/manual "Title")
\n"); +} + +TEST(code_span_with_double_backticks) { + const auto result{sourcemeta::core::markdown_to_html("``a`b``")}; + EXPECT_EQ(result, "a`b
`x`
x
x
\xc2\xa0x
one two three
a b c
a\\b
``x```
\n"); +} + +TEST(code_span_unmatched_opening_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("``x")}; + EXPECT_EQ(result, "``x
\n"); +} + +TEST(code_span_longer_closing_run_does_not_close) { + const auto result{sourcemeta::core::markdown_to_html("``x```y``")}; + EXPECT_EQ(result, "x```y
_x_
[text code](/url)
<https://sourcemeta.com/x>`
https://" + "sourcemeta.com/`x`
\n"); +} + +TEST(emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x _ y z_")}; + EXPECT_EQ(result, "x _ y z_
\n"); +} + +TEST(emphasis_opening_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x*(y)*")}; + EXPECT_EQ(result, "x*(y)*
\n"); +} + +TEST(emphasis_opening_followed_by_non_breaking_space) { + const auto result{sourcemeta::core::markdown_to_html("_\xC2\xA0" + "x\xC2\xA0_")}; + EXPECT_EQ(result, "_\xc2\xa0x\xc2\xa0_
\n"); +} + +TEST(emphasis_intraword_with_asterisks) { + const auto result{ + sourcemeta::core::markdown_to_html("un*frigging*believable")}; + EXPECT_EQ(result, "unfriggingbelievable
\n"); +} + +TEST(emphasis_intraword_with_underscores_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("snake_case_name")}; + EXPECT_EQ(result, "snake_case_name
\n"); +} + +TEST(emphasis_intraword_with_underscores_in_cyrillic) { + const auto result{sourcemeta::core::markdown_to_html("река_течёт_быстро")}; + EXPECT_EQ(result, "\xd1\x80\xd0\xb5\xd0\xba\xd0\xb0_" + "\xd1\x82\xd0\xb5\xd1\x87\xd1\x91\xd1\x82_" + "\xd0\xb1\xd1\x8b\xd1\x81\xd1\x82\xd1\x80\xd0\xbe
\n"); +} + +TEST(emphasis_between_digits) { + const auto result{sourcemeta::core::markdown_to_html("1*2*3\n\n1_2_3")}; + EXPECT_EQ(result, "123
\n" + "1_2_3
\n"); +} + +TEST(emphasis_underscore_between_letters_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x_(y)_z")}; + EXPECT_EQ(result, "x_(y)_z
\n"); +} + +TEST(emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x._[y]_")}; + EXPECT_EQ(result, "x.[y]
\n"); +} + +TEST(emphasis_mismatched_delimiter_characters) { + const auto result{sourcemeta::core::markdown_to_html("*x_")}; + EXPECT_EQ(result, "*x_
\n"); +} + +TEST(emphasis_closing_preceded_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("_x y _")}; + EXPECT_EQ(result, "_x y _
\n"); +} + +TEST(emphasis_closing_on_new_line) { + const auto result{sourcemeta::core::markdown_to_html("_x y\n_")}; + EXPECT_EQ(result, "_x y\n" + "_
\n"); +} + +TEST(emphasis_closing_not_right_flanking) { + const auto result{sourcemeta::core::markdown_to_html("_[_x]")}; + EXPECT_EQ(result, "_[_x]
\n"); +} + +TEST(emphasis_nested_with_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[_x_]_")}; + EXPECT_EQ(result, "[x]
\n"); +} + +TEST(emphasis_closing_followed_by_letter) { + const auto result{sourcemeta::core::markdown_to_html("*x*y")}; + EXPECT_EQ(result, "xy
\n"); +} + +TEST(emphasis_underscore_closing_followed_by_letter) { + const auto result{sourcemeta::core::markdown_to_html("_x_y\n\n_x_y_z_")}; + EXPECT_EQ(result, "_x_y
\n" + "x_y_z
\n"); +} + +TEST(emphasis_underscore_followed_by_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[x]_!")}; + EXPECT_EQ(result, "[x]!
\n"); +} + +TEST(strong_emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("__ x y__")}; + EXPECT_EQ(result, "__ x y__
\n"); +} + +TEST(strong_emphasis_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x__(y)__")}; + EXPECT_EQ(result, "x__(y)__
\n"); +} + +TEST(strong_emphasis_intraword_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("x**y**z")}; + EXPECT_EQ(result, "xyz
\n"); +} + +TEST(strong_emphasis_nested_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("**x, **y**, z**")}; + EXPECT_EQ(result, "x, y, z
\n"); +} + +TEST(strong_emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x.__[y]__")}; + EXPECT_EQ(result, "x.[y]
\n"); +} + +TEST(emphasis_rule_of_three_nested_strong) { + const auto result{sourcemeta::core::markdown_to_html("_x__y__z_")}; + EXPECT_EQ(result, "x__y__z
\n"); +} + +TEST(emphasis_rule_of_three_unmatched) { + const auto result{sourcemeta::core::markdown_to_html("_x__y_")}; + EXPECT_EQ(result, "x__y
\n"); +} + +TEST(emphasis_rule_of_three_triple_opening) { + const auto result{sourcemeta::core::markdown_to_html("___x__ y_")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(emphasis_rule_of_three_triple_closing) { + const auto result{sourcemeta::core::markdown_to_html("_x __y___")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(emphasis_rule_of_three_intraword) { + const auto result{sourcemeta::core::markdown_to_html("a***b***c")}; + EXPECT_EQ(result, "abc
\n"); +} + +TEST(emphasis_rule_of_three_long_runs) { + const auto result{sourcemeta::core::markdown_to_html("a****b*******c")}; + EXPECT_EQ(result, "ab***c
\n"); +} + +TEST(emphasis_containing_link) { + const auto result{ + sourcemeta::core::markdown_to_html("_see [docs](/manual)_")}; + EXPECT_EQ(result, "see docs
\n"); +} + +TEST(emphasis_empty_delimiters_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "__ empty strong\n\n____ empty double")}; + EXPECT_EQ(result, "__ empty strong
\n" + "____ empty double
\n"); +} + +TEST(emphasis_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("_one\ntwo_")}; + EXPECT_EQ(result, "one\n" + "two
\n"); +} + +TEST(strong_emphasis_nested_inside_itself) { + const auto result{sourcemeta::core::markdown_to_html("**x **y** z**")}; + EXPECT_EQ(result, "x y z
\n"); +} + +TEST(strong_emphasis_quadruple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("****x****")}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(strong_emphasis_quintuple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("*****x*****")}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(emphasis_with_escaped_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("x _\\__\n\nx __\\___")}; + EXPECT_EQ(result, "x _
\n" + "x _
\n"); +} + +TEST(emphasis_with_excess_delimiters) { + const auto result{ + sourcemeta::core::markdown_to_html("_x__\n\n__x_\n\n_x____")}; + EXPECT_EQ(result, "x_
\n" + "_x
\n" + "x___
\n"); +} + +TEST(emphasis_overlapping_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("_x *y_ z*")}; + EXPECT_EQ(result, "x *y z*
\n"); +} + +TEST(emphasis_overlapping_strong_and_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x **y _z w** v_")}; + EXPECT_EQ(result, "x y _z w v
\n"); +} + +TEST(emphasis_unclosed_outer_strong) { + const auto result{sourcemeta::core::markdown_to_html("__x __y z__")}; + EXPECT_EQ(result, "__x y z
\n"); +} + +TEST(emphasis_unclosed_outer_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x _y z_")}; + EXPECT_EQ(result, "_x y z
\n"); +} + +TEST(emphasis_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("_[x_](/url)")}; + EXPECT_EQ(result, "_x_
\n"); +} + +TEST(emphasis_asterisk_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("*x [y*](/url)")}; + EXPECT_EQ(result, "*x y*
\n"); +} + +TEST(strong_emphasis_does_not_span_code_span) { + const auto result{sourcemeta::core::markdown_to_html("**x `**` y**")}; + EXPECT_EQ(result, "x ** y
_xhttps://" + "sourcemeta.com/?a=_
\n" + "**xhttps://" + "sourcemeta.com/?b=**
\n"); +} + +TEST(link_destination_single_space) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( )")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_empty_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( <> )")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_with_space_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a b)")}; + EXPECT_EQ(result, "[docs](/a b)
\n"); +} + +TEST(link_destination_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a\n/b)")}; + EXPECT_EQ(result, "[docs](/a\n" + "/b)
\n"); +} + +TEST(link_destination_angle_brackets_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "[docs](</a\n" + "b>)
\n"); +} + +TEST(link_destination_angle_brackets_with_closing_paren) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_unmatched_angle_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "[docs](<a>)
\n"); +} + +TEST(link_destination_invalid_angle_bracket_forms) { + const auto result{sourcemeta::core::markdown_to_html("[x]([x](<y)z\n" + "[x](z)
\n"); +} + +TEST(link_destination_escaped_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](\\(a\\)b)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)e)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_unbalanced_parentheses_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)")}; + EXPECT_EQ(result, "[docs](a(b(c)d)
\n"); +} + +TEST(link_destination_escaped_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\(b\\(c)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_angle_brackets_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\]\\!)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_fragments_and_queries) { + const auto result{sourcemeta::core::markdown_to_html( + "[x](#top)\n\n[y](https://sourcemeta.com/?q=1#top)")}; + EXPECT_EQ(result, + "\n" + "\n"); +} + +TEST(link_destination_backslash_before_letter) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\zb)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_percent_encoding_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](caf%C3%A9é)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_that_looks_like_title) { + const auto result{sourcemeta::core::markdown_to_html("[docs]('Title')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_title_quote_styles) { + const auto result{sourcemeta::core::markdown_to_html( + "[a](/x 'one') [b](/y (two)) [c](/z \"three\")")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(link_title_escaped_quotes_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x 'it\\'s & more')")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(link_title_separated_by_non_breaking_space) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x\xC2\xA0'title')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_title_with_unescaped_inner_quote_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/x 'a'b')")}; + EXPECT_EQ(result, "[docs](/x 'a'b')
\n"); +} + +TEST(link_title_parentheses_with_quotes_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs](/x (with 'single' and \"double\"))")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_whitespace_around_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](\n /x\n 'title'\n)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_spaces_before_parentheses_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs] (/x)")}; + EXPECT_EQ(result, "[docs] (/x)
\n"); +} + +TEST(link_text_with_balanced_brackets) { + const auto result{sourcemeta::core::markdown_to_html("[a [b] [c [d]]](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_unbalanced_closing_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a] b](/x)")}; + EXPECT_EQ(result, "[a] b](/x)
\n"); +} + +TEST(link_text_with_unbalanced_opening_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a [b](/x)")}; + EXPECT_EQ(result, "[a b
\n"); +} + +TEST(link_text_with_escaped_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a \\] b](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("[_a_ __b__ `c` ~~d~~](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_image) { + const auto result{ + sourcemeta::core::markdown_to_html("[](/home)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_inside_link_text_is_not_allowed) { + const auto result{ + sourcemeta::core::markdown_to_html("[a [b](/inner) c](/outer)")}; + EXPECT_EQ(result, "[a b c](/outer)
\n"); +} + +TEST(link_inside_emphasis_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[a _[b](/inner)_](/outer)")}; + EXPECT_EQ(result, "[a b](/outer)
\n"); +} + +TEST(image_description_containing_link) { + const auto result{ + sourcemeta::core::markdown_to_html("](/image.png)")}; + EXPECT_EQ(result, "
_a_
\n"); +} + +TEST(link_brackets_take_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("[a _b](/x_)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(emphasis_does_not_span_link_text_brackets) { + const auto result{sourcemeta::core::markdown_to_html("_a [b_ c]")}; + EXPECT_EQ(result, "a [b c]
\n"); +} + +TEST(link_code_span_takes_precedence) { + const auto result{sourcemeta::core::markdown_to_html("[a`](/x)`")}; + EXPECT_EQ(result, "[a](/x)
[ahttps://" + "sourcemeta.com/?q=](/x)
\n"); +} + +TEST(reference_link_full) { + const auto result{sourcemeta::core::markdown_to_html( + "[the docs][manual]\n\n[manual]: /manual 'Manual'")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(reference_link_text_with_nested_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("[a [b] c][ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_case_folding) { + const auto result{sourcemeta::core::markdown_to_html( + "[text][MaNuAl]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_titlecase_digraph_folding) { + const auto result{sourcemeta::core::markdown_to_html("[Dž]\n\n[dž]: /digraph")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_space_between_labels_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a] [ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "[a] ref
\n"); +} + +TEST(reference_link_newline_between_labels_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a]\n[ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "[a]\n" + "ref
\n"); +} + +TEST(reference_link_uses_first_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[a][ref]\n\n[ref]: /first\n[ref]: /second")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_escapes_are_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][ref\\*]\n\n[ref*]: /ref")}; + EXPECT_EQ(result, "[a][ref*]
\n"); +} + +TEST(reference_link_label_with_unescaped_bracket_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b]c]\n\n[b]c]: /x")}; + EXPECT_EQ(result, "[a][b]c]
\n" + "[b]c]: /x
\n"); +} + +TEST(reference_link_label_with_escaped_bracket) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b\\]c]\n\n[b\\]c]: /x")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_with_escaped_backslash) { + const auto result{ + sourcemeta::core::markdown_to_html("[a\\\\b]: /x\n\n[a\\\\b]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_empty_brackets_are_literal) { + const auto result{sourcemeta::core::markdown_to_html("[][]\n\n[]: /x")}; + EXPECT_EQ(result, "[][]
\n" + "[]: /x
\n"); +} + +TEST(collapsed_reference_link_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[MANUAL][]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(collapsed_reference_link_across_lines_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual]\n[]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "manual\n" + "[]
\n"); +} + +TEST(shortcut_reference_link_with_emphasis) { + const auto result{sourcemeta::core::markdown_to_html( + "[_the_ manual]\n\n[_the_ manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(shortcut_reference_link_escaped) { + const auto result{ + sourcemeta::core::markdown_to_html("\\[manual\\]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "[manual]
\n"); +} + +TEST(shortcut_reference_link_label_with_underscore) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual_]: /manual\n\n_[manual_]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_full_takes_precedence_over_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b][c]\n\n[b]: /b\n[c]: /c")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_undefined_label_falls_back) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b][c]\n\n[a]: /a\n[c]: /c")}; + EXPECT_EQ(result, "[a]b
\n"); +} + +TEST(inline_link_takes_precedence_over_reference) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual]()\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(inline_link_invalid_falls_back_to_reference) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual](a b)\n\n[manual]: /manual")}; + EXPECT_EQ(result, "manual(a b)
\n"); +} + +TEST(image_alt_text_is_plain_text) { + const auto result{ + sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "




![logo]
\n"); +} + +TEST(image_escaped_exclamation_is_link) { + const auto result{ + sourcemeta::core::markdown_to_html("\\![logo]\n\n[logo]: /logo.png")}; + EXPECT_EQ(result, "!logo
\n"); +} + +TEST(image_angle_bracket_destination_with_space) { + const auto result{sourcemeta::core::markdown_to_html("

<z:thing>
\n"); +} + +TEST(autolink_uri_with_space_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/" + "a b>
\n"); +} + +TEST(autolink_uri_backslash_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("https://" + "sourcemeta.com/\\_
\n"); +} + +TEST(autolink_email) { + const auto result{ + sourcemeta::core::markdown_to_html("" + "first.last+tag@sub-domain.sourcemeta.com
\n"); +} + +TEST(autolink_email_with_backslash_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("< >
\n" + "<https://sourcemeta.com " + ">
\n"); +} + +TEST(autolink_without_scheme_is_not_autolink) { + const auto result{sourcemeta::core::markdown_to_html("<sourcemeta.com>
\n"); +} + +TEST(hard_break_with_many_spaces) { + const auto result{sourcemeta::core::markdown_to_html("one \ntwo")}; + EXPECT_EQ(result, "one
\n"
+ "two
one
\n"
+ "two
one
\n"
+ "two
one
\n"
+ "two
one two
text\\
\n" + "text
\n" + "one\n" + "two
\n"); +} + +TEST(text_with_special_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("cost: $5 + 10% ; done?")}; + EXPECT_EQ(result, "cost: $5 + 10% ; done?
\n"); +} + +TEST(text_with_greek_characters) { + const auto result{sourcemeta::core::markdown_to_html("λέξη και φράση")}; + EXPECT_EQ(result, + "\xce\xbb\xce\xad\xce\xbe\xce\xb7 \xce\xba\xce\xb1\xce\xb9 " + "\xcf\x86\xcf\x81\xce\xac\xcf\x83\xce\xb7
\n"); +} + +TEST(text_internal_spaces_preserved) { + const auto result{sourcemeta::core::markdown_to_html("wide gap")}; + EXPECT_EQ(result, "wide gap
\n"); +} diff --git a/test/markdown/markdown_pathological_test.cc b/test/markdown/markdown_pathological_test.cc new file mode 100644 index 0000000000..95aa906f17 --- /dev/null +++ b/test/markdown/markdown_pathological_test.cc @@ -0,0 +1,251 @@ +#include" + repeat("x y ", 5000) + "z" + + repeat(" y x", 5000) + "
\n"); +} + +TEST(underscore_closers_before_punctuation_without_openers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("x_. ", 10000))}; + EXPECT_EQ(result, "" + repeat("x_. ", 9999) + "x_.
\n"); +} + +TEST(double_underscore_openers_without_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("__y ", 10000))}; + EXPECT_EQ(result, "" + repeat("__y ", 9999) + "__y
\n"); +} + +TEST(link_closers_without_openers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("]b", 10000))}; + EXPECT_EQ(result, "" + repeat("]b", 10000) + "
\n"); +} + +TEST(image_openers_without_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat(")}; + EXPECT_EQ(result, "" + repeat("[ x](", 10000) + "
\n"); +} + +TEST(nested_image_brackets) { + const auto input{repeat("![", 5000) + "x" + repeat("]", 5000)}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "" + input + "
\n"); +} + +TEST(unclosed_image_angle_bracket_destinations) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat(" + "
\n"); +} + +TEST(backtick_runs_of_increasing_length) { + const auto input{increasing_backtick_runs(2000)}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "" + input + "
\n"); +} + +TEST(code_span_with_eighty_backticks) { + const auto result{sourcemeta::core::markdown_to_html( + std::string(80, '`') + "x" + std::string(80, '`'))}; + EXPECT_EQ(result, "x
" + input + "
\n"); +} + +TEST(alternating_blockquote_and_list_nesting_on_one_line) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat("> - ", 30) + "deep")}; + EXPECT_EQ(result, repeat("\n\n", 29)); +} + +TEST(alternating_blockquote_and_list_nesting_on_one_line_beyond_depth_limit) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat("> - ", 50) + "deep")}; + EXPECT_EQ(result, repeat("\n
\n- \n", 29) + + "
\n\n\n" + + repeat("\n
\n" + "- deep
\n
\n\n", 49)); +} + +TEST(staircase_list_of_one_hundred_fifty_levels) { + const auto result{sourcemeta::core::markdown_to_html(staircase_list(150))}; + EXPECT_EQ(result, repeat("\n
\n- \n", 49) + + "
\n\n\n" + + repeat("- deep
\n
| a | \nb | \nc | \n" + "
|---|---|---|
| x | \ny | \nz | \n
| h | \n", 1000) + + "
|---|
| c | \n", 1000) + + "
one\xef\xbf\xbdtwo\xef\xbf\xbd
\n"); +} + +TEST(link_reference_label_of_one_thousand_characters) { + const std::string label(1000, 'y'); + const auto result{sourcemeta::core::markdown_to_html( + "[" + label + "]: /x\n\n[" + label + "]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_reference_label_of_one_thousand_one_characters) { + const std::string label(1001, 'y'); + const auto result{sourcemeta::core::markdown_to_html( + "[" + label + "]: /x\n\n[" + label + "]")}; + EXPECT_EQ(result, "[" + label + "]: /x
\n[" + label + "]
\n"); +} + +TEST(single_paragraph_line_of_two_hundred_thousand_characters) { + const std::string input(200000, 'w'); + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "" + input + "
\n"); +} + +TEST(two_thousand_reference_definitions_and_uses) { + const auto result{ + sourcemeta::core::markdown_to_html(numbered_reference_definitions(2000) + + "\n" + numbered_reference_uses(2000))}; + EXPECT_EQ(result, "" + numbered_reference_anchors(2000) + "
\n"); +} diff --git a/test/markdown/markdown_safety_test.cc b/test/markdown/markdown_safety_test.cc new file mode 100644 index 0000000000..93e57f94da --- /dev/null +++ b/test/markdown/markdown_safety_test.cc @@ -0,0 +1,151 @@ +#includebefore after
\n"); +} + +TEST(safe_mode_omits_processing_instruction) { + const auto result{sourcemeta::core::markdown_to_html( + "before after")}; + EXPECT_EQ(result, "before after
\n"); +} + +TEST(safe_mode_omits_cdata_section) { + const auto result{ + sourcemeta::core::markdown_to_html("before after")}; + EXPECT_EQ(result, "before after
\n"); +} + +TEST(safe_mode_omits_declaration_block) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "\n"); +} + +TEST(safe_mode_omits_html_block_but_parses_markdown_between_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "strong
\n" + "\n"); +} + +TEST(safe_mode_omits_html_block_with_blank_line_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "\n__text__\n\n_more_\n
\nmore
\n" + "\n"); +} + +TEST(safe_mode_omits_inline_tag_with_attributes) { + const auto result{ + sourcemeta::core::markdown_to_html("x y")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(safe_mode_escapes_markup_in_link_title) { + const auto result{sourcemeta::core::markdown_to_html("[x](/y '&\"')")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(safe_mode_escapes_quotes_in_image_title) { + const auto result{ + sourcemeta::core::markdown_to_html(R"MD()MD")}; + EXPECT_EQ( + result, + "
x
\n"); +} + +TEST(safe_mode_omits_raw_html_taking_precedence_over_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[x ")}; + EXPECT_EQ(result, "[x
\n"); +} diff --git a/test/markdown/markdown_test.cc b/test/markdown/markdown_test.cc index ad16573f35..b56d9c8a71 100644 --- a/test/markdown/markdown_test.cc +++ b/test/markdown/markdown_test.cc @@ -2,45 +2,6 @@ #includehello world
\n"); - EXPECT_EQ(results[1], "hello world
\n"); - EXPECT_EQ(results[2], "hello world
\n"); - EXPECT_EQ(results[3], "hello world
\n"); - EXPECT_EQ(results[4], "hello world
\n"); - EXPECT_EQ(results[5], "hello world
\n"); - EXPECT_EQ(results[6], "hello world
\n"); - EXPECT_EQ(results[7], "hello world
\n"); - EXPECT_EQ(results[8], "hello world
\n"); - EXPECT_EQ(results[9], "hello world
\n"); - EXPECT_EQ(results[10], "hello world
\n"); - EXPECT_EQ(results[11], "hello world
\n"); - EXPECT_EQ(results[12], "hello world
\n"); - EXPECT_EQ(results[13], "hello world
\n"); - EXPECT_EQ(results[14], "hello world
\n"); - EXPECT_EQ(results[15], "hello world
\n"); -} - TEST(simple_paragraph) { const auto result{sourcemeta::core::markdown_to_html("Hello world")}; EXPECT_EQ(result, "Hello world
\n"); @@ -64,24 +25,12 @@ TEST(default_omits_raw_html) { EXPECT_EQ(result, "\n"); } -TEST(unsafe_mode_passes_raw_html) { - const auto result{ - sourcemeta::core::markdown_to_html("after
\n"); +} + +TEST(html_block_pre_keeps_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nline one\n\n*not emphasis*\n\nafter", false)}; + EXPECT_EQ(result, "
\n" + "line one\n" + "\n" + "*not emphasis*\n" + "\n" + "
after
\n"); +} + +TEST(html_block_style_with_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "<style>\n" + "\n" + ".x { color: red; }\n" + "\n" + "</style>\n" + "after
\n"); +} + +TEST(html_block_comment_with_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_processing_instruction) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_declaration) { + const auto result{ + sourcemeta::core::markdown_to_html("", false)}; + EXPECT_EQ(result, "\n"); +} + +TEST(html_block_cdata_section) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_ends_at_blank_line) { + const auto result{sourcemeta::core::markdown_to_html( + "two
\n" + "Text\n" + "\n" + "more
\n"); +} + +TEST(html_block_section_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html( + "Text\nText
\n" + "<div>code</div>\n"
+ "\n");
+}
+
+TEST(html_block_inside_list_item) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* \n"); +} + +TEST(inline_html_invalid_tag_names) { + const auto result{sourcemeta::core::markdown_to_html("<1x> <-x>", false)}; + EXPECT_EQ(result, "
<1x> <-x>
\n"); +} + +TEST(inline_html_invalid_attribute_name) { + const auto result{ + sourcemeta::core::markdown_to_html("", false)}; + EXPECT_EQ(result, "<span 1x="y">
\n"); +} + +TEST(inline_html_invalid_attribute_values) { + const auto result{ + sourcemeta::core::markdown_to_html(" \n"); +} + +TEST(inline_html_invalid_whitespace) { + const auto result{sourcemeta::core::markdown_to_html( + "< span> ", false)}; + EXPECT_EQ(result, "< span> <span/ > <span a=b\n" + "c!d>
\n"); +} + +TEST(inline_html_closing_tags) { + const auto result{ + sourcemeta::core::markdown_to_html("</span class="x">
\n"); +} + +TEST(inline_html_comment_with_double_hyphen) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_comment_edge_forms) { + const auto result{sourcemeta::core::markdown_to_html( + "x y -->\n\nx y -->", false)}; + EXPECT_EQ(result, "x y -->
\n" + "x y -->
\n"); +} + +TEST(inline_html_processing_instruction) { + const auto result{sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_declaration) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_cdata_section) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_entity_in_attribute_is_preserved) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_backslash_in_attribute_is_preserved) { + const auto result{ + sourcemeta::core::markdown_to_html(R"MD(x )MD", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(tagfilter_every_disallowed_tag) { + const auto result{sourcemeta::core::markdown_to_html( + "deleted
deleted bold text
This is wrong correct
| h | \n
|---|
use old_func()
all three
~not deleted~
\n"); +} + +TEST(strikethrough_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")}; + EXPECT_EQ(result, "one\n"
+ "two
x ~~~y~~~ z
\n"); +} + +TEST(strikethrough_unclosed) { + const auto result{sourcemeta::core::markdown_to_html("x ~~y")}; + EXPECT_EQ(result, "x ~~y
\n"); +} + +TEST(strikethrough_with_surrounding_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x ~~ y ~~ z")}; + EXPECT_EQ(result, "x ~~ y ~~ z
\n"); +} + +TEST(strikethrough_and_emphasis_overlap) { + const auto result{sourcemeta::core::markdown_to_html("~~_x~~_")}; + EXPECT_EQ(result, "_x_
~~x~~
\n"); +} + +TEST(strikethrough_inside_link_text) { + const auto result{sourcemeta::core::markdown_to_html("[~~old~~ new](/x)")}; + EXPECT_EQ(result, "\n"); +} diff --git a/test/markdown/markdown_tables_test.cc b/test/markdown/markdown_tables_test.cc new file mode 100644 index 0000000000..920ed27a96 --- /dev/null +++ b/test/markdown/markdown_tables_test.cc @@ -0,0 +1,417 @@ +#include| a | \nb | \n
|---|---|
| 1 | \n2 | \n
| left | \n" + "center | \n" + "right | \n" + "
|---|---|---|
| a | \n" + "b | \n" + "c | \n" + "
| h | \n
|---|
| bold | \n
| x | \ny | \n
|---|---|
| 1 | \n2 | \n
| 3 | \n4 | \n
| 5 | \n6 | \n
| h | \n
|---|
| a | b | \n
| h | \n
|---|
| click" + " | \n
| h | \n
|---|
code | \n
| a | \nb | \n
|---|---|
| \n | \n |
| a | \nb | \n
|---|
Here is a table:
\n" + "| a | \n
|---|
| 1 | \n
key | \n"
+ "value | \n
|---|---|
| a | \nb | \n
| code | \n" + "
|---|
a|b | \n"
+ "
| c|d | \n" + "
| a | \n" + "b | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
after
\n"); +} + +TEST(table_ends_at_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n|---|\n| 1 |\n> quote")}; + EXPECT_EQ(result, "| a | \n" + "
|---|
| 1 | \n" + "
\n" + "\n"); +} + +TEST(table_line_without_pipes_is_row) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b |\n|---|---|\ntext row")}; + EXPECT_EQ(result, "quote
\n" + "
| a | \n" + "b | \n" + "
|---|---|
| text row | \n" + "\n" + " |
| a | b | c |\n" + "| - | - |
\n"); +} + +TEST(table_rows_with_fewer_and_more_cells) { + const auto result{sourcemeta::core::markdown_to_html( + "| a | b |\n| - | - |\n| 1 |\n| 1 | 2 | 3 |")}; + EXPECT_EQ(result, "| a | \n" + "b | \n" + "
|---|---|
| 1 | \n" + "\n" + " |
| 1 | \n" + "2 | \n" + "
| x | \n" + "y | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
| only | \n" + "
|---|
| one | \n" + "
| l | \n" + "r | \n" + "c | \n" + "n | \n" + "
|---|---|---|---|
| 1 | \n" + "2 | \n" + "3 | \n" + "4 | \n" + "
| x | \n" + "y | \n" + "
|---|
| a | b | \n" + "c | \n" + "
|---|
| manual | \n" + "
|---|
intro\n" + "a | b
\n" + "\n" + "\n"); +} + +TEST(table_cell_with_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("| <tag> |\n| --- |")}; + EXPECT_EQ(result, "a | b
\n" + "\n" + "
\n" + "- | -\n" + "1 | 2
\n" + "
| <tag> | \n" + "
|---|
| \n" + " | \n" + " |
|---|---|
| x | \n" + "y | \n" + "
| _a | \n" + "b_ | \n" + "
|---|
| a |\n" + "| :-:x |
\n"); +} + +TEST(table_delimiter_count_mismatch_without_outer_pipes) { + const auto result{sourcemeta::core::markdown_to_html("x | y | z\n- | -")}; + EXPECT_EQ(result, "x | y | z
\n" + "| x | \n" + "y | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
buga
\n" + "b
\n" + "Hello world
\n"); -} - -TEST(safe_mode_renders_plain_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Hello **world**", true)}; - EXPECT_EQ(result, "Hello world
\n"); -} - -TEST(safe_mode_omits_raw_html) { - const auto result{ - sourcemeta::core::markdown_to_html("First paragraph
\nSecond paragraph
\n"); -} - -TEST(three_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html("One\n\nTwo\n\nThree")}; - EXPECT_EQ(result, "One
\nTwo
\nThree
\n"); -} - -TEST(empty_input) { - const auto result{sourcemeta::core::markdown_to_html("")}; - EXPECT_TRUE(result.empty()); -} - -TEST(whitespace_only) { - const auto result{sourcemeta::core::markdown_to_html(" \n\n ")}; - EXPECT_TRUE(result.empty()); -} - -TEST(soft_line_break) { - const auto result{sourcemeta::core::markdown_to_html("line one\nline two")}; - EXPECT_EQ(result, "line one\nline two
\n"); -} - -TEST(hard_line_break_with_two_spaces) { - const auto result{sourcemeta::core::markdown_to_html("line one \nline two")}; - EXPECT_EQ(result, "line one
\nline two
line one
\nline two
italic
\n"); -} - -TEST(italic_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("_italic_")}; - EXPECT_EQ(result, "italic
\n"); -} - -TEST(bold_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("**bold**")}; - EXPECT_EQ(result, "bold
\n"); -} - -TEST(bold_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("__bold__")}; - EXPECT_EQ(result, "bold
\n"); -} - -TEST(bold_and_italic) { - const auto result{sourcemeta::core::markdown_to_html("***bold italic***")}; - EXPECT_EQ(result, "bold italic
\n"); -} - -TEST(bold_inside_italic) { - const auto result{ - sourcemeta::core::markdown_to_html("*this is **bold** inside italic*")}; - EXPECT_EQ(result, - "this is bold inside italic
\n"); -} - -TEST(italic_inside_bold) { - const auto result{ - sourcemeta::core::markdown_to_html("**this is *italic* inside bold**")}; - EXPECT_EQ(result, - "this is italic inside bold
\n"); -} - -TEST(inline_code) { - const auto result{sourcemeta::core::markdown_to_html("Use `printf()`")}; - EXPECT_EQ(result, "Use printf()
there is a ` here
<div class="foo">
code here\n\n");
-}
-
-TEST(fenced_code_block_tildes) {
- const auto result{sourcemeta::core::markdown_to_html("~~~\ncode here\n~~~")};
- EXPECT_EQ(result, "code here\n\n");
-}
-
-TEST(fenced_code_block_with_language) {
- const auto result{
- sourcemeta::core::markdown_to_html("```cpp\nint x = 0;\n```")};
- EXPECT_EQ(result, "int x = 0;\n\n");
-}
-
-TEST(fenced_code_block_html_escaped) {
- const auto result{
- sourcemeta::core::markdown_to_html("```\n<div>&</div>\n\n");
-}
-
-TEST(indented_code_block) {
- const auto result{
- sourcemeta::core::markdown_to_html(" int x = 0;\n return x;")};
- EXPECT_EQ(result, "int x = 0;\nreturn x;\n\n");
-}
-
-TEST(inline_link) {
- const auto result{
- sourcemeta::core::markdown_to_html("[click here](https://example.com)")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(inline_link_with_title) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click](https://example.com \"My Title\")")};
- EXPECT_EQ(
- result,
- "\n");
-}
-
-TEST(reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click][ref]\n\n[ref]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(reference_link_with_title) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click][ref]\n\n[ref]: https://example.com \"Title\"")};
- EXPECT_EQ(
- result,
- "\n");
-}
-
-TEST(collapsed_reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[example][]\n\n[example]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(shortcut_reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[example]\n\n[example]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(link_with_emphasis_inside) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[**bold link**](https://example.com)")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(image) {
- const auto result{
- sourcemeta::core::markdown_to_html("")};
- EXPECT_EQ(result, "

\n\n"); -} - -TEST(multiline_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("> line one\n> line two")}; - EXPECT_EQ(result, "quoted text
\n
\n\n"); -} - -TEST(nested_blockquote) { - const auto result{sourcemeta::core::markdown_to_html("> outer\n>> inner")}; - EXPECT_EQ(result, "line one\nline two
\n
\n\n"); -} - -TEST(blockquote_with_formatting) { - const auto result{ - sourcemeta::core::markdown_to_html("> **bold** and *italic*")}; - EXPECT_EQ(result, - "outer
\n\n\ninner
\n" - "
\n\n"); -} - -TEST(unordered_list_with_dashes) { - const auto result{ - sourcemeta::core::markdown_to_html("- one\n- two\n- three")}; - EXPECT_EQ(result, - "bold and italic
\n" - "
one
\ntwo
\nthree
\n*not italic*
\n"); -} - -TEST(escape_hash) { - const auto result{sourcemeta::core::markdown_to_html("\\# not a heading")}; - EXPECT_EQ(result, "# not a heading
\n"); -} - -TEST(escape_brackets) { - const auto result{ - sourcemeta::core::markdown_to_html("\\[not a link\\](url)")}; - EXPECT_EQ(result, "[not a link](url)
\n"); -} - -TEST(escape_backtick) { - const auto result{sourcemeta::core::markdown_to_html("\\`not code\\`")}; - EXPECT_EQ(result, "`not code`
\n"); -} - -TEST(html_entity_named) { - const auto result{sourcemeta::core::markdown_to_html("© 2025")}; - EXPECT_EQ(result, "\xC2\xA9 2025
\n"); -} - -TEST(html_entity_numeric) { - const auto result{sourcemeta::core::markdown_to_html("© 2025")}; - EXPECT_EQ(result, "\xC2\xA9 2025
\n"); -} - -TEST(ampersand_in_text) { - const auto result{sourcemeta::core::markdown_to_html("AT&T")}; - EXPECT_EQ(result, "AT&T
\n"); -} - -TEST(angle_brackets_in_text) { - const auto result{sourcemeta::core::markdown_to_html("1 < 2 and 3 > 2")}; - EXPECT_EQ(result, "1 < 2 and 3 > 2
\n"); -} - -TEST(unicode_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Hello \xC3\xA9\xC3\xA0\xC3\xBC")}; - EXPECT_EQ(result, "Hello \xC3\xA9\xC3\xA0\xC3\xBC
\n"); -} - -TEST(cjk_characters) { - const auto result{ - sourcemeta::core::markdown_to_html("\xE4\xBD\xA0\xE5\xA5\xBD")}; - EXPECT_EQ(result, "\xE4\xBD\xA0\xE5\xA5\xBD
\n"); -} - -TEST(emoji) { - const auto result{sourcemeta::core::markdown_to_html("\xF0\x9F\x98\x80")}; - EXPECT_EQ(result, "\xF0\x9F\x98\x80
\n"); -} - -TEST(inline_html_is_sanitized) { - const auto result{sourcemeta::core::markdown_to_html("Hello world")}; - EXPECT_EQ( - result, - "Hello world
\n"); -} - -TEST(raw_html_block_is_sanitized) { - const auto result{ - sourcemeta::core::markdown_to_html("| a | \nb | \n
|---|---|
| 1 | \n2 | \n
| left | \n" - "center | \n" - "right | \n" - "
|---|---|---|
| a | \n" - "b | \n" - "c | \n" - "
| h | \n
|---|
| bold | \n
| x | \ny | \n
|---|---|
| 1 | \n2 | \n
| 3 | \n4 | \n
| 5 | \n6 | \n
| h | \n
|---|
| a | b | \n
Visit https://example.com" - " today
\n"); -} - -TEST(autolink_http) { - const auto result{sourcemeta::core::markdown_to_html("http://example.com")}; - EXPECT_EQ(result, - "\n"); -} - -TEST(autolink_www) { - const auto result{sourcemeta::core::markdown_to_html("www.example.com")}; - EXPECT_EQ(result, - "\n"); -} - -TEST(autolink_email) { - const auto result{ - sourcemeta::core::markdown_to_html("Contact user@example.com")}; - EXPECT_EQ(result, "Contact " - "user@example.com
\n"); -} - -TEST(angle_bracket_autolink) { - const auto result{ - sourcemeta::core::markdown_to_html("deleted
deleted bold text
This is wrong correct
Some text here.
\n"); -} - -TEST(blockquote_with_list) { - const auto result{ - sourcemeta::core::markdown_to_html("> items:\n> - one\n> - two")}; - EXPECT_EQ(result, - "\n\n"); -} - -TEST(list_with_code_block) { - const auto result{sourcemeta::core::markdown_to_html( - "- item:\n\n ```\n code\n ```\n\n- next")}; - EXPECT_EQ(result, "items:
\n\n
\n- one
\n- two
\n" - "
item:
\ncode\n\n"
- "next
\nthis is all bold here
\n"); -} - -TEST(paragraph_with_inline_code_and_link) { - const auto result{sourcemeta::core::markdown_to_html( - "Use `foo()` from [the docs](https://docs.com).")}; - EXPECT_EQ(result, "Use foo() from "
- "the docs.
| h | \n
|---|
| h | \n
|---|
| click" - " | \n
| h | \n
|---|
code | \n
| a | \nb | \n
|---|---|
| \n | \n |
| a | \nb | \n
|---|
bugmain functionuse old_func()
\n\n"); -} - -TEST(blockquote_with_heading) { - const auto result{sourcemeta::core::markdown_to_html("> ## Quoted heading")}; - EXPECT_EQ(result, "example:
\n" - "\ncode here\n
\n\n"); -} - -TEST(deeply_nested_list) { - const auto result{ - sourcemeta::core::markdown_to_html("- a\n - b\n - c\n - d")}; - EXPECT_EQ(result, "Quoted heading
\n
\n\n"); -} - -TEST(consecutive_headings) { - const auto result{ - sourcemeta::core::markdown_to_html("# One\n\n## Two\n\n### Three")}; - EXPECT_EQ(result, "a
\n\n\n" - "b
\n" - "\n\nc
\n

line one\n\nline three\n\n");
-}
-
-TEST(ordered_list_with_paragraphs) {
- const auto result{sourcemeta::core::markdown_to_html(
- "1. first\n\n more text\n\n2. second")};
- EXPECT_EQ(result, "first
\nmore text
\nsecond
\nContent
\n"); -} - -TEST(table_after_paragraph) { - const auto result{sourcemeta::core::markdown_to_html( - "Here is a table:\n\n| a |\n| - |\n| 1 |")}; - EXPECT_EQ(result, "Here is a table:
\n" - "| a | \n
|---|
| 1 | \n
\n\n" - "quote
\n
See https://one.com and " - "https://two.com
\n"); -} - -TEST(emphasis_does_not_cross_code_span) { - const auto result{sourcemeta::core::markdown_to_html("*start `code* end`")}; - EXPECT_EQ(result, "*start code* end
a
\n"); -} - -TEST(list_item_with_multiple_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html( - "- paragraph one\n\n paragraph two\n\n paragraph three")}; - EXPECT_EQ(result, "paragraph one
\nparagraph two
\n" - "paragraph three
\nkey | \n"
- "value | \n
|---|---|
| a | \nb | \n
all three
See https://example.com.
\n"); -} - -TEST(autolink_in_parentheses) { - const auto result{ - sourcemeta::core::markdown_to_html("(see https://example.com)")}; - EXPECT_EQ( - result, - "(see https://example.com)
\n"); -} - -TEST(blockquote_with_strikethrough) { - const auto result{sourcemeta::core::markdown_to_html("> ~~deleted~~")}; - EXPECT_EQ(result, "\n\n"); -} - -TEST(heading_with_bold_and_code) { - const auto result{ - sourcemeta::core::markdown_to_html("## **Bold** and `code`")}; - EXPECT_EQ(result, "\n
deleted
codeText1
\n" - "Footnote content " - "\xe2\x86\xa9
\n" - "First " - "\xe2\x86\xa9
\n" - "Second " - "\xe2\x86\xa9
\n" - "~not deleted~
\n"); -} - -TEST(github_pre_lang_format) { - const auto result{ - sourcemeta::core::markdown_to_html("```python\nprint()\n```")}; - EXPECT_EQ(result, "print()\n\n");
-}
-
-TEST(invalid_utf8_is_replaced) {
- const auto result{sourcemeta::core::markdown_to_html("hello \xFF world")};
- EXPECT_EQ(result, "hello \xEF\xBF\xBD world
\n"); -} diff --git a/test/markdown/markdown_thematic_breaks_test.cc b/test/markdown/markdown_thematic_breaks_test.cc new file mode 100644 index 0000000000..45c31cc640 --- /dev/null +++ b/test/markdown/markdown_thematic_breaks_test.cc @@ -0,0 +1,131 @@ +#include++++
\n"); +} + +TEST(thematic_break_equals_signs_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("= = = =")}; + EXPECT_EQ(result, "= = = =
\n"); +} + +TEST(thematic_break_two_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("**\n\n__")}; + EXPECT_EQ(result, "**
\n" + "__
\n"); +} + +TEST(thematic_break_indented_one_to_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html(" ___\n ---\n ***")}; + EXPECT_EQ(result, "---\n"
+ "\n");
+}
+
+TEST(thematic_break_indented_four_spaces_continues_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("Paragraph text\n ___")};
+ EXPECT_EQ(result, "Paragraph text\n" + "___
\n"); +} + +TEST(thematic_break_many_characters) { + const auto result{sourcemeta::core::markdown_to_html( + "-------------------------------------------------")}; + EXPECT_EQ(result, "x______
\n" + "x
\n"); +} + +TEST(thematic_break_mixed_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("-*-")}; + EXPECT_EQ(result, "-*-
\n"); +} + +TEST(thematic_break_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("alpha\n___\nbeta")}; + EXPECT_EQ(result, "alpha
\n" + "beta
\n"); +} + +TEST(thematic_break_takes_precedence_over_list_item) { + const auto result{sourcemeta::core::markdown_to_html("- one\n- - -\n- two")}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(unsafe_mode_nul_character_inside_inline_html) { + const auto result{ + sourcemeta::core::markdown_to_html("x "sv, false)}; + EXPECT_EQ(result, "x
\n"); +}