From 5825f0d4955786bdd3930a0bf77c7b09cff59274 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 14 Sep 2026 16:56:25 -0300 Subject: [PATCH 1/2] Add more tests and benchmarks for the Markdown module Signed-off-by: Juan Cruz Viotti --- benchmark/CMakeLists.txt | 9 + benchmark/markdown.cc | 103 ++ test/markdown/CMakeLists.txt | 10 +- test/markdown/markdown_blocks_test.cc | 1834 +++++++++++++++++++ test/markdown/markdown_concurrency_test.cc | 42 + test/markdown/markdown_extensions_test.cc | 796 ++++++++ test/markdown/markdown_inlines_test.cc | 1039 +++++++++++ test/markdown/markdown_pathological_test.cc | 251 +++ test/markdown/markdown_safety_test.cc | 151 ++ test/markdown/markdown_test.cc | 51 - test/markdown/markdown_unsafe_test.cc | 323 ++++ 11 files changed, 4557 insertions(+), 52 deletions(-) create mode 100644 benchmark/markdown.cc create mode 100644 test/markdown/markdown_blocks_test.cc create mode 100644 test/markdown/markdown_concurrency_test.cc create mode 100644 test/markdown/markdown_extensions_test.cc create mode 100644 test/markdown/markdown_inlines_test.cc create mode 100644 test/markdown/markdown_pathological_test.cc create mode 100644 test/markdown/markdown_safety_test.cc create mode 100644 test/markdown/markdown_unsafe_test.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 728590c410..1f3e857f57 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -36,6 +36,10 @@ if(SOURCEMETA_CORE_HTML) list(APPEND BENCHMARK_SOURCES html.cc) endif() +if(SOURCEMETA_CORE_MARKDOWN) + list(APPEND BENCHMARK_SOURCES markdown.cc) +endif() + if(SOURCEMETA_CORE_GZIP) list(APPEND BENCHMARK_SOURCES gzip.cc) endif() @@ -102,6 +106,11 @@ if(BENCHMARK_SOURCES) PRIVATE sourcemeta::core::html) endif() + if(SOURCEMETA_CORE_MARKDOWN) + target_link_libraries(sourcemeta_core_benchmark + PRIVATE sourcemeta::core::markdown) + endif() + if(SOURCEMETA_CORE_GZIP) target_link_libraries(sourcemeta_core_benchmark PRIVATE sourcemeta::core::gzip sourcemeta::core::io) diff --git a/benchmark/markdown.cc b/benchmark/markdown.cc new file mode 100644 index 0000000000..ae3ed16d90 --- /dev/null +++ b/benchmark/markdown.cc @@ -0,0 +1,103 @@ +#include + +#include + +#include // std::size_t +#include // std::string, std::to_string + +static auto repeat(const std::string &pattern, const std::size_t count) + -> std::string { + std::string result; + result.reserve(pattern.size() * count); + for (std::size_t index = 0; index < count; ++index) { + result.append(pattern); + } + + return result; +} + +static auto increasing_backtick_runs(const std::size_t count) -> std::string { + std::string result; + for (std::size_t length = 1; length <= count; ++length) { + result.push_back('x'); + result.append(length, '`'); + } + + return result; +} + +static auto staircase_list(const std::size_t depth) -> std::string { + std::string result; + for (std::size_t level = 0; level < depth; ++level) { + result.append(level * 2, ' '); + result.append("- level\n"); + } + + return result; +} + +// NOLINTNEXTLINE(readability-identifier-naming) +static void Markdown_To_HTML_Realistic_Document(benchmark::State &state) { + std::string input; + for (std::size_t index = 0; index < 500; ++index) { + const auto number{std::to_string(index)}; + input.append("# Section ").append(number).append("\n\n"); + input.append("This paragraph has *emphasis*, **strong emphasis**, ") + .append("`inline code`, a [link](https://sourcemeta.com/") + .append(number) + .append(" \"Title\"), an autolink to www.sourcemeta.com, and ") + .append("~~deleted text~~ with an entity © and a footnote[^") + .append(number) + .append("].\n\n"); + input.append("> A block quote with a [reference link][ref") + .append(number) + .append("]\n> spanning two lines.\n\n"); + input.append("- First item\n - Nested *item*\n - Another `item`\n" + "- [x] Completed task\n- [ ] Pending task\n\n"); + input.append("1. One\n2. Two\n3. Three\n\n"); + input.append("| Name | Value | Notes |\n| :--- | ---: | :---: |\n" + "| alpha | 1 | *first* |\n| beta | 2 | `second` |\n" + "| gamma | 3 | [third](/third) |\n\n"); + input.append("```cpp\nauto main() -> int {\n return 0;\n}\n```\n\n"); + input.append(" indented code line\n\n---\n\n"); + input.append("[ref") + .append(number) + .append("]: https://sourcemeta.com/reference/") + .append(number) + .append("\n[^") + .append(number) + .append("]: Footnote ") + .append(number) + .append(".\n\n"); + } + + for (auto iteration : state) { + auto result{sourcemeta::core::markdown_to_html(input)}; + benchmark::DoNotOptimize(result); + } +} + +// Inputs that make naive delimiter, bracket, and container handling +// quadratic or worse, which a linear parser renders almost instantly +// NOLINTNEXTLINE(readability-identifier-naming) +static void Markdown_To_HTML_Pathological(benchmark::State &state) { + const std::string input{ + repeat("_x **y ", 5000) + "z" + repeat(" y** x_", 5000) + "\n\n" + + repeat("x_. ", 10000) + "\n\n" + repeat("__y ", 10000) + "\n\n" + + repeat("]b", 10000) + "\n\n" + repeat("![b", 10000) + "\n\n" + + repeat("_b* ", 10000) + "\n\n" + "x***y" + repeat("z* ", 10000) + "\n\n" + + repeat("[ b*", 10000) + "\n\n" + repeat("[ x](", 10000) + "\n\n" + + repeat("![", 5000) + "x" + repeat("]", 5000) + "\n\n" + + repeat("![x]( - ", 50) + "deep\n\n" + staircase_list(200) + "\n" + + "| a | b | c |\n|---|---|---|\n" + repeat("| x | y | z |\n", 5000)}; + + for (auto iteration : state) { + auto result{sourcemeta::core::markdown_to_html(input)}; + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(Markdown_To_HTML_Realistic_Document); +BENCHMARK(Markdown_To_HTML_Pathological); diff --git a/test/markdown/CMakeLists.txt b/test/markdown/CMakeLists.txt index 3c406d735f..a945aed1cb 100644 --- a/test/markdown/CMakeLists.txt +++ b/test/markdown/CMakeLists.txt @@ -1,5 +1,13 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME markdown - SOURCES markdown_test.cc) + SOURCES + markdown_blocks_test.cc + markdown_concurrency_test.cc + markdown_extensions_test.cc + markdown_inlines_test.cc + markdown_pathological_test.cc + markdown_safety_test.cc + markdown_test.cc + markdown_unsafe_test.cc) target_link_libraries(sourcemeta_core_markdown_unit PRIVATE sourcemeta::core::markdown) diff --git a/test/markdown/markdown_blocks_test.cc b/test/markdown/markdown_blocks_test.cc new file mode 100644 index 0000000000..90021b2efc --- /dev/null +++ b/test/markdown/markdown_blocks_test.cc @@ -0,0 +1,1834 @@ +#include + +#include + +TEST(tab_indented_code_block) { + const auto result{sourcemeta::core::markdown_to_html("\tlime\tpear\t\tplum")}; + EXPECT_EQ(result, "
lime\tpear\t\tplum\n"
+                    "
\n"); +} + +TEST(tab_after_space_indented_code_block) { + const auto result{sourcemeta::core::markdown_to_html(" \tlime\tpear")}; + EXPECT_EQ(result, "
lime\tpear\n"
+                    "
\n"); +} + +TEST(tab_list_item_continuation) { + const auto result{sourcemeta::core::markdown_to_html(" - kiwi\n\n\tmango")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "

    kiwi

    \n" + "

    mango

    \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" + "

    kiwi

    \n" + "
      mango\n"
    +                    "
    \n" + "
  • \n" + "
\n"); +} + +TEST(tab_after_blockquote_marker) { + const auto result{sourcemeta::core::markdown_to_html(">\t\tcedar\n>\t\toak")}; + EXPECT_EQ(result, "
\n" + "
  cedar\n"
+                    "  oak\n"
+                    "
\n" + "
\n"); +} + +TEST(tab_after_list_marker) { + const auto result{sourcemeta::core::markdown_to_html("+\t\tcedar")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "
      cedar\n"
    +                    "
    \n" + "
  • \n" + "
\n"); +} + +TEST(tab_after_heading_marker) { + const auto result{sourcemeta::core::markdown_to_html("##\tTitle")}; + EXPECT_EQ(result, "

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" + "
  • `alpha
  • \n" + "
  • beta`
  • \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"); +} + +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" + "
\n"); +} + +TEST(thematic_break_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" ---")}; + 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, "
\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" + "
      • x
      • \n" + "
      \n" + "
    • \n" + "
    \n" + "
  • \n" + "
\n" + "

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" + "
\n" + "

beta

\n"); +} + +TEST(dashes_after_paragraph_make_setext_heading) { + 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, "
    \n" + "
  • one
  • \n" + "
\n" + "
\n" + "
    \n" + "
  • two
  • \n" + "
\n"); +} + +TEST(thematic_break_inside_list_item) { + const auto result{sourcemeta::core::markdown_to_html("* one\n* - - -")}; + EXPECT_EQ(result, "
    \n" + "
  • one
  • \n" + "
  • \n" + "
    \n" + "
  • \n" + "
\n"); +} + +TEST(atx_heading_eight_hashes_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("######## title")}; + EXPECT_EQ(result, "

######## title

\n"); +} + +TEST(atx_heading_without_space_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("#1 fan\n\n##tag")}; + EXPECT_EQ(result, "

#1 fan

\n" + "

##tag

\n"); +} + +TEST(atx_heading_escaped_hash_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("\\# title")}; + EXPECT_EQ(result, "

# title

\n"); +} + +TEST(atx_heading_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("## plain *em* \\*raw\\*")}; + EXPECT_EQ(result, "

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, "
## code\n"
+                    "
\n"); +} + +TEST(atx_heading_indented_four_spaces_continues_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n ## more")}; + EXPECT_EQ(result, "

text\n" + "## more

\n"); +} + +TEST(atx_heading_closing_sequence_of_any_length) { + const auto result{ + sourcemeta::core::markdown_to_html("## title #######\n#### title #")}; + EXPECT_EQ(result, "

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, "

one line

\n" + "

heading

\n" + "

next line

\n"); +} + +TEST(atx_heading_empty) { + const auto result{sourcemeta::core::markdown_to_html("#\n## \n#### ####")}; + EXPECT_EQ(result, "

\n" + "

\n" + "

\n"); +} + +TEST(setext_heading_single_character_underline) { + const auto result{sourcemeta::core::markdown_to_html("Title *em*\n=")}; + EXPECT_EQ(result, "

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, "
Title\n"
+                    "===\n"
+                    "
\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"); +} + +TEST(setext_heading_underline_indented_four_spaces_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Title\n ===")}; + EXPECT_EQ(result, "

Title\n" + "===

\n"); +} + +TEST(setext_heading_underline_with_internal_space_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Title\n== ==")}; + EXPECT_EQ(result, "

Title\n" + "== ==

\n"); +} + +TEST(setext_heading_trailing_spaces_are_not_hard_break) { + 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"); +} + +TEST(setext_heading_equals_underline_is_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html("> Title\n===")}; + EXPECT_EQ(result, "
\n" + "

Title\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" + "

quote\n" + "continued

\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" + "
  • Item\n" + "===
  • \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"); +} + +TEST(setext_heading_dashes_after_thematic_break) { + 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" + "
  • item
  • \n" + "
\n" + "
\n"); +} + +TEST(setext_heading_equals_after_indented_code) { + const auto result{sourcemeta::core::markdown_to_html(" code\n===")}; + EXPECT_EQ(result, "
code\n"
+                    "
\n" + "

===

\n"); +} + +TEST(setext_heading_escaped_content) { + const auto result{sourcemeta::core::markdown_to_html("\\- not a list\n---")}; + EXPECT_EQ(result, "

- 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, "

Intro

\n" + "

Title

\n" + "

Body

\n"); +} + +TEST(setext_heading_spaced_dashes_after_blank_line) { + const auto result{ + sourcemeta::core::markdown_to_html("One\ntwo\n\n- - -\n\nthree")}; + EXPECT_EQ(result, "

One\n" + "two

\n" + "
\n" + "

three

\n"); +} + +TEST(setext_heading_underscores_are_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("One\ntwo\n___\nthree")}; + EXPECT_EQ(result, "

One\n" + "two

\n" + "
\n" + "

three

\n"); +} + +TEST(setext_heading_escaped_equals_are_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("One\ntwo\n\\===\nthree")}; + EXPECT_EQ(result, "

One\n" + "two\n" + "===\n" + "three

\n"); +} + +TEST(indented_code_chunks_separated_by_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + " first\n\n second\n \n \n third")}; + EXPECT_EQ(result, "
first\n"
+                    "\n"
+                    "second\n"
+                    "\n"
+                    "\n"
+                    "third\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"); +} + +TEST(indented_code_cannot_interrupt_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Text\n code")}; + EXPECT_EQ(result, "

Text\n" + "code

\n"); +} + +TEST(indented_code_ends_at_unindented_line) { + const auto result{sourcemeta::core::markdown_to_html(" code\ntext")}; + EXPECT_EQ(result, "
code\n"
+                    "
\n" + "

text

\n"); +} + +TEST(indented_code_between_headings) { + const auto result{sourcemeta::core::markdown_to_html( + "## Top\n code\nBottom\n===\n code\n***")}; + EXPECT_EQ(result, "

Top

\n" + "
code\n"
+                    "
\n" + "

Bottom

\n" + "
code\n"
+                    "
\n" + "
\n"); +} + +TEST(indented_code_first_line_extra_indentation) { + const auto result{ + sourcemeta::core::markdown_to_html(" alpha\n beta")}; + EXPECT_EQ(result, "
  alpha\n"
+                    "beta\n"
+                    "
\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, "
code\n"
+                    "
\n"); +} + +TEST(indented_code_trailing_whitespace_kept) { + const auto result{sourcemeta::core::markdown_to_html(" code\t ")}; + EXPECT_EQ(result, "
code\t  \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, "
**bold**\n"
+                    "[link](/x)\n"
+                    "\n"
+                    "1. item\n"
+                    "
\n"); +} + +TEST(fenced_code_closing_fence_longer_than_opening) { + const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n~~~~~")}; + EXPECT_EQ(result, "
code\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"
+                    "
\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, "
\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"
+                    "~~~\n"
+                    "text\n"
+                    "
\n"); +} + +TEST(fenced_code_unclosed_ends_with_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> ~~~\n> code\n\ntext")}; + EXPECT_EQ(result, "
\n" + "
code\n"
+                    "
\n" + "
\n" + "

text

\n"); +} + +TEST(fenced_code_only_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~~\n \n\n~~~")}; + EXPECT_EQ(result, "
 \n"
+                    "\n"
+                    "
\n"); +} + +TEST(fenced_code_empty) { + const auto result{sourcemeta::core::markdown_to_html("~~~\n~~~")}; + EXPECT_EQ(result, "
\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, "
one\n"
+                    "two\n"
+                    "three\n"
+                    "
\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"
+                    "
\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, "
~~~\n"
+                    "code\n"
+                    "~~~\n"
+                    "
\n"); +} + +TEST(fenced_code_closing_fence_indented_differently) { + const auto result{sourcemeta::core::markdown_to_html(" ~~~\ncode\n ~~~")}; + EXPECT_EQ(result, "
code\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"
+                    "
\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, "

x\n" + "code

\n"); +} + +TEST(fenced_code_interrupts_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("text\n~~~\ncode\n~~~\nmore")}; + EXPECT_EQ(result, "

text

\n" + "
code\n"
+                    "
\n" + "

more

\n"); +} + +TEST(fenced_code_info_string_first_word_is_language) { + const auto result{sourcemeta::core::markdown_to_html( + "~~~ python extra words here\nprint(1)\n~~~")}; + EXPECT_EQ(result, "
print(1)\n"
+                    "
\n"); +} + +TEST(fenced_code_info_string_of_symbols) { + const auto result{sourcemeta::core::markdown_to_html("```#!\n```")}; + EXPECT_EQ(result, "
\n"); +} + +TEST(fenced_code_backtick_fence_info_cannot_contain_backticks) { + const auto result{sourcemeta::core::markdown_to_html("```js`\ncode")}; + EXPECT_EQ(result, "

```js`\n" + "code

\n"); +} + +TEST(fenced_code_tilde_fence_info_can_contain_backticks) { + const auto result{ + sourcemeta::core::markdown_to_html("~~~ `js` ~~~\ncode\n~~~")}; + EXPECT_EQ(result, "
code\n"
+                    "
\n"); +} + +TEST(fenced_code_closing_fence_cannot_have_info) { + const auto result{ + sourcemeta::core::markdown_to_html("~~~\n~~~ closing\n~~~")}; + EXPECT_EQ(result, "
~~~ closing\n"
+                    "
\n"); +} + +TEST(fenced_code_info_string_entities_decoded) { + const auto result{ + sourcemeta::core::markdown_to_html("```c++\ncode\n```")}; + EXPECT_EQ(result, "
code\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, + "

docs

\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, "

docs

\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, "

docs

\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, "

[docs]: /manual "first

\n" + "

second"

\n" + "

[docs]

\n"); +} + +TEST(link_reference_definition_destination_on_next_line) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]:\n /manual\n\n[docs]")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_reference_definition_missing_destination) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]:\n\ntext [docs]")}; + EXPECT_EQ(result, "

[docs]:

\n" + "

text [docs]

\n"); +} + +TEST(link_reference_definition_empty_angle_destination) { + const auto result{sourcemeta::core::markdown_to_html("[docs]: <>\n\n[docs]")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_reference_definition_title_without_separating_space) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual\"title\"\n\n[docs]")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_reference_definition_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]: /a\\b\\#c 'it\\'s'\n\n[docs]")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_reference_definition_after_its_use) { + const auto result{ + sourcemeta::core::markdown_to_html("See [docs].\n\n[docs]: /manual")}; + EXPECT_EQ(result, "

See docs.

\n"); +} + +TEST(link_reference_definition_first_one_wins) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /one\n\n[docs]: /two")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_reference_definition_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[DoCs]: /manual\n\n[dOcS]")}; + EXPECT_EQ(result, "

dOcS

\n"); +} + +TEST(link_reference_definition_label_full_case_folding) { + const auto result{ + sourcemeta::core::markdown_to_html("[STRASSE]: /road\n\n[straße]")}; + EXPECT_EQ(result, "

stra\xc3\x9f" + "e

\n"); +} + +TEST(link_reference_definition_alone_renders_nothing) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual'")}; + EXPECT_EQ(result, ""); +} + +TEST(link_reference_definition_with_trailing_text_is_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual' trailing")}; + EXPECT_EQ(result, "

[docs]: /manual 'Manual' trailing

\n"); +} + +TEST(link_reference_definition_title_with_trailing_text_on_next_line) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]: /manual\n'Manual' trailing\n\n[docs]")}; + EXPECT_EQ(result, "

'Manual' trailing

\n" + "

docs

\n"); +} + +TEST(link_reference_definition_indented_four_spaces_is_code) { + const auto result{ + sourcemeta::core::markdown_to_html(" [docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "
[docs]: /manual\n"
+                    "
\n" + "

[docs]

\n"); +} + +TEST(link_reference_definition_inside_fenced_code_is_content) { + const auto result{sourcemeta::core::markdown_to_html( + "~~~\n[docs]: /manual\n~~~\n\n[docs]")}; + EXPECT_EQ(result, "
[docs]: /manual\n"
+                    "
\n" + "

[docs]

\n"); +} + +TEST(link_reference_definition_cannot_interrupt_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("Intro\n[docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "

Intro\n" + "[docs]: /manual

\n" + "

[docs]

\n"); +} + +TEST(link_reference_definition_between_heading_and_blockquote) { + const auto result{sourcemeta::core::markdown_to_html( + "## [Docs]\n[docs]: /manual\n> quoted")}; + EXPECT_EQ(result, "

Docs

\n" + "
\n" + "

quoted

\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" + "docs

\n"); +} + +TEST(link_reference_definitions_consecutive) { + const auto result{sourcemeta::core::markdown_to_html( + "[one]: /1 'One'\n[two]: /2\n 'Two'\n[three]: \n\n[one] [two] " + "[three]")}; + EXPECT_EQ(result, "

one two three

\n"); +} + +TEST(link_reference_definition_inside_blockquote_is_global) { + const auto result{ + sourcemeta::core::markdown_to_html("> [docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "
\n" + "
\n" + "

docs

\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, "

The long label

\n"); +} + +TEST(link_reference_definition_empty_label_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[]: /manual\n\n[] text")}; + EXPECT_EQ(result, "

[]: /manual

\n" + "

[] text

\n"); +} + +TEST(link_reference_definition_whitespace_label_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[ ]: /manual\n\n[ ]")}; + EXPECT_EQ(result, "

[ ]: /manual

\n" + "

[ ]

\n"); +} + +TEST(paragraph_leading_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html(" one\n two")}; + EXPECT_EQ(result, "

one\n" + "two

\n"); +} + +TEST(paragraph_continuation_leading_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html( + "one\n two\n three")}; + EXPECT_EQ(result, "

one\n" + "two\n" + "three

\n"); +} + +TEST(paragraph_first_line_indented_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; + EXPECT_EQ(result, "

first\n" + "second

\n"); +} + +TEST(paragraph_first_line_indented_five_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; + EXPECT_EQ(result, "
 first\n"
+                    "
\n" + "

second

\n"); +} + +TEST(paragraph_final_trailing_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html("one \ntwo ")}; + EXPECT_EQ(result, "

one
\n" + "two

\n"); +} + +TEST(blank_lines_at_start_and_end_ignored) { + const auto result{ + sourcemeta::core::markdown_to_html("\n \none\n\n \n## two\n\n\n")}; + EXPECT_EQ(result, "

one

\n" + "

two

\n"); +} + +TEST(blockquote_marker_without_following_space) { + const auto result{ + sourcemeta::core::markdown_to_html(">## Title\n>text\n> more")}; + EXPECT_EQ(result, "
\n" + "

Title

\n" + "

text\n" + "more

\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, "
\n" + "

one\n" + "two\n" + "three

\n" + "
\n"); +} + +TEST(blockquote_marker_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" > one\n > two")}; + EXPECT_EQ(result, "
> one\n"
+                    "> two\n"
+                    "
\n"); +} + +TEST(blockquote_lazy_paragraph_continuation) { + const auto result{sourcemeta::core::markdown_to_html("> one\ntwo")}; + EXPECT_EQ(result, "
\n" + "

one\n" + "two

\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" + "

one\n" + "two\n" + "three

\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" + "

one

\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" + "
  • one
  • \n" + "
\n" + "
\n" + "
    \n" + "
  • two
  • \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" + "
code\n"
+                    "
\n" + "
\n" + "
 more\n"
+                    "
\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_fenced_code) { + const auto result{sourcemeta::core::markdown_to_html("> ~~~\ncode\n~~~")}; + EXPECT_EQ(result, "
\n" + "
\n" + "
\n" + "

code

\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" + "

one\n" + "* two

\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" + "

text

\n" + "
\n"); +} + +TEST(blockquote_blank_line_separates_blockquotes) { + const auto result{sourcemeta::core::markdown_to_html("> one\n\n> two")}; + EXPECT_EQ(result, "
\n" + "

one

\n" + "
\n" + "
\n" + "

two

\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" + "

one

\n" + "

two

\n" + "
\n"); +} + +TEST(blockquote_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n>quote")}; + EXPECT_EQ(result, "

text

\n" + "
\n" + "

quote

\n" + "
\n"); +} + +TEST(blockquote_ended_by_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("> one\n- - -\n> two")}; + EXPECT_EQ(result, "
\n" + "

one

\n" + "
\n" + "
\n" + "
\n" + "

two

\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" + "

one

\n" + "
\n" + "

two

\n"); +} + +TEST(blockquote_nested_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html(">> one\ntwo")}; + EXPECT_EQ(result, "
\n" + "
\n" + "

one\n" + "two

\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" + "

one\n" + "two

\n" + "
\n" + "

three

\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" + "
 code\n"
+                    "
\n" + "
\n" + "
\n" + "

text

\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" + "
  • one
  • \n" + "
\n" + "

two

\n"); +} + +TEST(list_item_content_with_marker_width_indentation) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "

    one

    \n" + "

    two

    \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" + "
  1. one
  2. \n" + "
\n" + "
two\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, "
    \n" + "
  1. \n" + "

    one

    \n" + "

    two

    \n" + "
  2. \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" + "
    \n" + "
  • \n" + "

    one

    \n" + "

    two

    \n" + "
  • \n" + "
\n" + "
\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" + "
  • one
  • \n" + "
\n" + "

two

\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, "

*one

\n" + "

1)two

\n"); +} + +TEST(list_item_multiple_blank_lines_between_blocks) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n\n\n two")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "

    one

    \n" + "

    two

    \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" + "
  1. \n" + "

    one

    \n" + "
    code\n"
    +                    "
    \n" + "

    two

    \n" + "
    \n" + "

    quote

    \n" + "
    \n" + "
  2. \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" + "
  • \n" + "

    one

    \n" + "
    code\n"
    +                    "\n"
    +                    "\n"
    +                    "more\n"
    +                    "
    \n" + "
  • \n" + "
\n"); +} + +TEST(ordered_list_nine_digit_number) { + const auto result{sourcemeta::core::markdown_to_html("999999999) max")}; + EXPECT_EQ(result, "
    \n" + "
  1. max
  2. \n" + "
\n"); +} + +TEST(ordered_list_ten_digit_number_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("9999999999) over")}; + EXPECT_EQ(result, "

9999999999) over

\n"); +} + +TEST(ordered_list_starting_at_zero) { + const auto result{sourcemeta::core::markdown_to_html("0) zero")}; + EXPECT_EQ(result, "
    \n" + "
  1. zero
  2. \n" + "
\n"); +} + +TEST(ordered_list_number_with_leading_zeros) { + const auto result{sourcemeta::core::markdown_to_html("0007. seven")}; + EXPECT_EQ(result, "
    \n" + "
  1. seven
  2. \n" + "
\n"); +} + +TEST(ordered_list_negative_number_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("-5) negative")}; + EXPECT_EQ(result, "

-5) negative

\n"); +} + +TEST(list_item_indented_code_after_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("* text\n\n code")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "

    text

    \n" + "
    code\n"
    +                    "
    \n" + "
  • \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" + "
  1. \n" + "

    text

    \n" + "
    code\n"
    +                    "
    \n" + "
  2. \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" + "
  • \n" + "
    code\n"
    +                    "
    \n" + "

    text

    \n" + "
    more\n"
    +                    "
    \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" + "
  • \n" + "
     code\n"
    +                    "
    \n" + "

    text

    \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" + "
  • one
  • \n" + "
  • \n" + "
    code\n"
    +                    "
    \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" + "
  1. one
  2. \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" + "

one

\n"); +} + +TEST(list_item_empty_between_items) { + const auto result{sourcemeta::core::markdown_to_html("* one\n*\n* two")}; + EXPECT_EQ(result, "
    \n" + "
  • one
  • \n" + "
  • \n" + "
  • two
  • \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" + "
  • one
  • \n" + "
  • \n" + "
  • two
  • \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" + "
  1. one
  2. \n" + "
  3. \n" + "
  4. two
  5. \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" + "

text\n" + "1)

\n"); +} + +TEST(list_item_indented_one_space) { + const auto result{sourcemeta::core::markdown_to_html( + " 1. one\n two\n\n code")}; + EXPECT_EQ(result, "
    \n" + "
  1. \n" + "

    one\n" + "two

    \n" + "
    code\n"
    +                    "
    \n" + "
  2. \n" + "
\n"); +} + +TEST(list_item_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" - one\n two")}; + EXPECT_EQ(result, "
- one\n"
+                    "  two\n"
+                    "
\n"); +} + +TEST(list_item_lazy_continuation_lines) { + const auto result{ + sourcemeta::core::markdown_to_html(" * one\ntwo\n\n code")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "

    one\n" + "two

    \n" + "
    code\n"
    +                    "
    \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" + "
  • one\n" + "two\n" + "three
  • \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" + "
  • \n" + "
    \n" + "

    quoted\n" + "lazy line

    \n" + "
    \n" + "
  • \n" + "
\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" + "
  • a\n" + "
      \n" + "
    • b\n" + "
        \n" + "
      • c
      • \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" + "
  • a
  • \n" + "
  • b
  • \n" + "
  • c
  • \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" + "
  1. a\n" + "
      \n" + "
    • b
    • \n" + "
    \n" + "
  2. \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" + "
  1. a\n" + "* b
  2. \n" + "
\n"); +} + +TEST(list_item_containing_only_list) { + const auto result{sourcemeta::core::markdown_to_html("+ + one")}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "
      \n" + "
    • one
    • \n" + "
    \n" + "
  • \n" + "
\n"); +} + +TEST(list_item_containing_nested_ordered_list) { + const auto result{sourcemeta::core::markdown_to_html("3) * 4) deep")}; + EXPECT_EQ(result, "
    \n" + "
  1. \n" + "
      \n" + "
    • \n" + "
        \n" + "
      1. deep
      2. \n" + "
      \n" + "
    • \n" + "
    \n" + "
  2. \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" + "

    Title

    \n" + "
  • \n" + "
  • \n" + "

    Text

    \n" + "more
  • \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, "
    \n" + "
  • one
  • \n" + "
  • two
  • \n" + "
\n" + "
    \n" + "
  • three
  • \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" + "
  1. one
  2. \n" + "
  3. two
  4. \n" + "
\n" + "
    \n" + "
  1. three
  2. \n" + "
\n"); +} + +TEST(list_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Text\n* one\n* two")}; + EXPECT_EQ(result, "

Text

\n" + "
    \n" + "
  • one
  • \n" + "
  • two
  • \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, "

We counted\n" + "42. items in total

\n"); +} + +TEST(ordered_list_starting_at_one_interrupts_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("We counted\n1. items in total")}; + EXPECT_EQ(result, "

We counted

\n" + "
    \n" + "
  1. items in total
  2. \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" + "

    one

    \n" + "
  • \n" + "
  • \n" + "

    two

    \n" + "
  • \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" + "
  • a\n" + "
      \n" + "
    • \n" + "

      b

      \n" + "

      c

      \n" + "
    • \n" + "
    \n" + "
  • \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" + "
  • one
  • \n" + "
\n" + "\n" + "
    \n" + "
  • two
  • \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" + "
  • a
  • \n" + "
  • b
  • \n" + "
  • c
  • \n" + "
  • d
  • \n" + "
  • e
  • \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" + "
  • a
  • \n" + "
  • b
  • \n" + "
  • c\n" + "
      \n" + "
    • d
    • \n" + "
    \n" + "
  • \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" + "
  1. \n" + "

    a

    \n" + "
  2. \n" + "
  3. \n" + "

    b

    \n" + "
      \n" + "
    1. c
    2. \n" + "
    \n" + "
  4. \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" + "

    a

    \n" + "
  • \n" + "
  • \n" + "

    b

    \n" + "
  • \n" + "
  • \n" + "

    c

    \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" + "

    a

    \n" + "
  • \n" + "
  • \n" + "
  • \n" + "

    b

    \n" + "
  • \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" + "

    a

    \n" + "

    b

    \n" + "
  • \n" + "
  • \n" + "

    c

    \n" + "
  • \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" + "

    a

    \n" + "
  • \n" + "
  • \n" + "

    b

    \n" + "
  • \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" + "
  • a
  • \n" + "
  • \n" + "
    x\n"
    +                    "\n"
    +                    "
    \n" + "
  • \n" + "
  • b
  • \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" + "
  • a\n" + "
      \n" + "
    • \n" + "

      b

      \n" + "
    • \n" + "
    • \n" + "

      c

      \n" + "
    • \n" + "
    \n" + "
  • \n" + "
  • d
  • \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" + "
  • a\n" + "
    \n" + "

    b

    \n" + "

    c

    \n" + "
    \n" + "
  • \n" + "
  • d
  • \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" + "
  • a\n" + "
    \n" + "

    b

    \n" + "
    \n" + "
    c\n"
    +                    "
    \n" + "
  • \n" + "
  • d
  • \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" + "
  1. a\n" + "
      \n" + "
    1. b
    2. \n" + "
    \n" + "
  2. \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" + "
    x\n"
    +                    "
    \n" + "

    y

    \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" + "
  1. \n" + "

    a

    \n" + "
      \n" + "
    • b
    • \n" + "
    \n" + "

    c

    \n" + "
  2. \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" + "

    a

    \n" + "
      \n" + "
    1. b
    2. \n" + "
    3. c
    4. \n" + "
    \n" + "
  • \n" + "
  • \n" + "

    d

    \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, "

Title

\n" + "

text

\n" + "

more

\n"); +} + +TEST(cr_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("## Title\rtext\r\rmore")}; + EXPECT_EQ(result, "

Title

\n" + "

text

\n" + "

more

\n"); +} + +TEST(mixed_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("one\rtwo\r\nthree\nfour")}; + EXPECT_EQ(result, "

one\n" + "two\n" + "three\n" + "four

\n"); +} + +TEST(byte_order_mark_before_list) { + const auto result{sourcemeta::core::markdown_to_html("\xEF\xBB\xBF" + "* item")}; + EXPECT_EQ(result, "
    \n" + "
  • item
  • \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 + +#include + +#include // std::size_t +#include // std::string +#include // std::thread +#include // std::vector + +TEST(concurrent_calls) { + static constexpr std::size_t THREAD_COUNT{16}; + std::vector threads; + threads.reserve(THREAD_COUNT); + std::vector results(THREAD_COUNT); + + for (std::size_t index{0}; index < THREAD_COUNT; ++index) { + threads.emplace_back([&results, index]() { + results[index] = sourcemeta::core::markdown_to_html("**hello** world"); + }); + } + + for (auto &thread : threads) { + thread.join(); + } + + EXPECT_EQ(results[0], "

hello 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"); +} diff --git a/test/markdown/markdown_extensions_test.cc b/test/markdown/markdown_extensions_test.cc new file mode 100644 index 0000000000..811b7910ce --- /dev/null +++ b/test/markdown/markdown_extensions_test.cc @@ -0,0 +1,796 @@ +#include + +#include + +TEST(table_escaped_pipes_inside_inline_content) { + const auto result{sourcemeta::core::markdown_to_html( + "| code |\n| ---- |\n| `a\\|b` |\n| _c\\|d_ |")}; + EXPECT_EQ(result, "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
code
a|b
c|d
\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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
ab
12
\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, "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
a
1
\n" + "
\n" + "

quote

\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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
ab
text row
\n"); +} + +TEST(table_delimiter_count_mismatch_is_not_table) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b | c |\n| - | - |")}; + EXPECT_EQ(result, "

| 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, "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
ab
1
12
\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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
xy
12
\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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
only
one
\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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
lrcn
1234
\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" + "\n" + "\n" + "\n" + "
xy
\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" + "\n" + "\n" + "\n" + "\n" + "
a | bc
\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" + "\n" + "\n" + "\n" + "
manual
\n"); +} + +TEST(table_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("intro\na | b\n- | -")}; + EXPECT_EQ(result, "

intro\n" + "a | b

\n" + "
    \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" + "
  • a | b\n" + "
      \n" + "
    • | -\n" + "1 | 2
    • \n" + "
    \n" + "
  • \n" + "
\n"); +} + +TEST(table_inside_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> a | b\n> - | -\n> 1 | 2")}; + EXPECT_EQ(result, "
\n" + "

a | b

\n" + "
    \n" + "
  • | -\n" + "1 | 2
  • \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" + "\n" + "
<tag>
\n"); +} + +TEST(table_empty_header_cells) { + const auto result{ + sourcemeta::core::markdown_to_html("| | |\n|--|--|\n| x | y |")}; + EXPECT_EQ(result, "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
xy
\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" + "
_ab_
\n"); +} + +TEST(table_invalid_delimiter_row_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("| a |\n| :-:x |")}; + EXPECT_EQ(result, "

| 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" + "
    \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" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
xy
12
\n"); +} + +TEST(strikethrough_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")}; + EXPECT_EQ(result, "

one\n" + "two

\n"); +} + +TEST(strikethrough_triple_tildes_inline) { + const auto result{sourcemeta::core::markdown_to_html("x ~~~y~~~ z")}; + EXPECT_EQ(result, "

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_

\n"); +} + +TEST(strikethrough_escaped_closing) { + const auto result{sourcemeta::core::markdown_to_html("~~x\\~~")}; + EXPECT_EQ(result, "

~~x~~

\n"); +} + +TEST(strikethrough_inside_link_text) { + const auto result{sourcemeta::core::markdown_to_html("[~~old~~ new](/x)")}; + EXPECT_EQ(result, "

old new

\n"); +} + +TEST(extended_autolink_www_with_path) { + const auto result{ + sourcemeta::core::markdown_to_html("see www.sourcemeta.com/docs/intro")}; + EXPECT_EQ(result, "

see www.sourcemeta.com/docs/intro

\n"); +} + +TEST(extended_autolink_trailing_punctuation_excluded) { + const auto result{ + sourcemeta::core::markdown_to_html("Go to www.sourcemeta.com/x.y!")}; + EXPECT_EQ(result, "

Go to www.sourcemeta.com/" + "x.y!

\n"); +} + +TEST(extended_autolink_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/wiki/Thing_(disambiguation)")}; + EXPECT_EQ( + result, + "

https://" + "sourcemeta.com/wiki/Thing_(disambiguation)

\n"); +} + +TEST(extended_autolink_unbalanced_closing_parentheses) { + const auto result{ + sourcemeta::core::markdown_to_html("https://sourcemeta.com/a_(b)))")}; + EXPECT_EQ(result, "

https://" + "sourcemeta.com/a_(b)))

\n"); +} + +TEST(extended_autolink_inside_parentheses) { + const auto result{ + sourcemeta::core::markdown_to_html("(see https://sourcemeta.com/a_(b))")}; + EXPECT_EQ(result, "

(see https://" + "sourcemeta.com/a_(b))

\n"); +} + +TEST(extended_autolink_entity_like_suffix_excluded) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/?x=1&\n\nhttps://sourcemeta.com/?x=1&y=2")}; + EXPECT_EQ(result, "

https://" + "sourcemeta.com/?x=1&

\n" + "

https://" + "sourcemeta.com/?x=1&y=2

\n"); +} + +TEST(extended_autolink_ends_at_less_than) { + const auto result{ + sourcemeta::core::markdown_to_html("https://sourcemeta.com/ahttps://sourcemeta.com/" + "a<b

\n"); +} + +TEST(extended_autolink_underscore_in_final_domain_segments) { + const auto result{sourcemeta::core::markdown_to_html( + "www.sourcemeta._com\n\nwww.source_meta.com")}; + EXPECT_EQ(result, "

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, + "

www.docs_site.sourcemeta.com

\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, + "

team@sourcemeta.com

\n" + "

team@source+meta.com

\n" + "

team+tag@sourcemeta.com\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, + "

mailto:team@sourcemeta.com

\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, + "

https://sourcemeta.com

\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, "

https://sourcemeta.com

\n"); +} + +TEST(extended_autolink_inside_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("`https://sourcemeta.com`")}; + EXPECT_EQ(result, "

https://sourcemeta.com

\n"); +} + +TEST(task_list_uppercase_marker) { + const auto result{sourcemeta::core::markdown_to_html("* [X] finished")}; + EXPECT_EQ(result, "
    \n" + "
  • finished
  • \n" + "
\n"); +} + +TEST(task_list_marker_needs_following_space) { + const auto result{sourcemeta::core::markdown_to_html("* [ ]pending")}; + EXPECT_EQ(result, "
    \n" + "
  • [ ]pending
  • \n" + "
\n"); +} + +TEST(task_list_marker_with_other_character) { + const auto result{ + sourcemeta::core::markdown_to_html("* [-] partial\n* [x] complete")}; + EXPECT_EQ(result, "
    \n" + "
  • [-] partial
  • \n" + "
  • complete
  • \n" + "
\n"); +} + +TEST(task_list_nested) { + const auto result{sourcemeta::core::markdown_to_html( + "* [ ] parent\n * [x] child\n * [ ] grandchild")}; + EXPECT_EQ(result, + "
    \n" + "
  • parent\n" + "
      \n" + "
    • child\n" + "
        \n" + "
      • grandchild
      • \n" + "
      \n" + "
    • \n" + "
    \n" + "
  • \n" + "
\n"); +} + +TEST(task_list_in_ordered_list) { + const auto result{ + sourcemeta::core::markdown_to_html("1) [ ] first\n2) [X] second")}; + EXPECT_EQ( + result, + "
    \n" + "
  1. first
  2. \n" + "
  3. second
  4. \n" + "
\n"); +} + +TEST(task_list_marker_not_at_item_start) { + const auto result{sourcemeta::core::markdown_to_html("* text [ ] later")}; + EXPECT_EQ(result, "
    \n" + "
  • text [ ] later
  • \n" + "
\n"); +} + +TEST(task_list_marker_alone) { + const auto result{sourcemeta::core::markdown_to_html("* [x]")}; + EXPECT_EQ(result, "
    \n" + "
  • [x]
  • \n" + "
\n"); +} + +TEST(task_list_in_loose_list) { + const auto result{sourcemeta::core::markdown_to_html("* [x] a\n\n* [ ] b")}; + EXPECT_EQ(result, + "
    \n" + "
  • \n" + "

    a

    \n" + "
  • \n" + "
  • \n" + "

    b

    \n" + "
  • \n" + "
\n"); +} + +TEST(footnote_referenced_multiple_times) { + const auto result{sourcemeta::core::markdown_to_html( + "one[^n] two[^n] three[^n]\n\n[^n]: shared")}; + EXPECT_EQ( + result, + "

one1 two1 three1

\n" + "
\n" + "
    \n" + "
  1. \n" + "

    shared \xe2\x86\xa9 \xe2\x86\xa92 \xe2\x86\xa93

    \n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_unreferenced_definition_omitted) { + const auto result{ + sourcemeta::core::markdown_to_html("body\n\n[^orphan]: unused")}; + EXPECT_EQ(result, "

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" + "
\n" + "
    \n" + "
  1. \n" + "

    text \xe2\x86\xa9

    \n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_definition_with_multiple_blocks) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^m]\n\n[^m]: first\n\n > quoted\n\n * item")}; + EXPECT_EQ(result, "

body1

\n" + "
\n" + "
    \n" + "
  1. \n" + "

    first

    \n" + "
    \n" + "

    quoted

    \n" + "
    \n" + "
      \n" + "
    • item
    • \n" + "
    \n" + "\xe2\x86\xa9\n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_numbering_follows_references) { + const auto result{sourcemeta::core::markdown_to_html( + "x[^z] y[^y] z[^x]\n\n[^x]: X\n[^y]: Y\n[^z]: Z")}; + EXPECT_EQ(result, + "

x1 y2 z3

\n" + "
\n" + "
    \n" + "
  1. \n" + "

    Z \xe2\x86\xa9

    \n" + "
  2. \n" + "
  3. \n" + "

    Y \xe2\x86\xa9

    \n" + "
  4. \n" + "
  5. \n" + "

    X \xe2\x86\xa9

    \n" + "
  6. \n" + "
\n" + "
\n"); +} + +TEST(footnote_label_with_special_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^a&b]\n\n[^a&b]: text")}; + EXPECT_EQ( + result, + "

body1

\n" + "
\n" + "
    \n" + "
  1. \n" + "

    text \xe2\x86\xa9

    \n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_reference_in_table_cell) { + const auto result{sourcemeta::core::markdown_to_html( + "| h |\n| - |\n| c[^t] |\n\n[^t]: table note")}; + EXPECT_EQ(result, + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
h
c1
\n" + "
\n" + "
    \n" + "
  1. \n" + "

    table note \xe2\x86\xa9

    \n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_reference_inside_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^a]\n\n[^a]: refers[^b]\n\n[^b]: nested")}; + EXPECT_EQ(result, "

body1

\n" + "
\n" + "
    \n" + "
  1. \n" + "

    refers2 \xe2\x86\xa9

    \n" + "
  2. \n" + "
  3. \n" + "

    nested \xe2\x86\xa9

    \n" + "
  4. \n" + "
\n" + "
\n"); +} + +TEST(footnote_definition_inside_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^q]\n\n> [^q]: inside quote")}; + EXPECT_EQ(result, + "

body1

\n" + "
\n" + "
\n" + "
\n" + "
    \n" + "
  1. \n" + "

    inside quote \xe2\x86\xa9

    \n" + "
  2. \n" + "
\n" + "
\n"); +} + +TEST(footnote_label_with_space_is_not_footnote) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^two words]\n\n[^two words]: text")}; + EXPECT_EQ(result, "

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 @@ +#include + +#include + +TEST(backslash_escapes_every_ascii_punctuation) { + const auto result{sourcemeta::core::markdown_to_html( + "a\\~b\\}c\\|d\\{e\\`f\\_g\\^h\\]i\\\\j\\[k\\@l\\?m\\>n\\=o\\a~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\\![no image](/x.png)\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" + "&amp; 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"); +} + +TEST(backslash_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`\\*\\_`")}; + EXPECT_EQ(result, "

\\*\\_

\n"); +} + +TEST(backslash_inside_indented_code_is_literal) { + const auto result{sourcemeta::core::markdown_to_html(" \\*\\_")}; + EXPECT_EQ(result, "
\\*\\_\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("")}; + EXPECT_EQ(result, "

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, "

docs

\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, "

docs

\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; &#xz;\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, "

&amp

\n"); +} + +TEST(entity_unknown_name_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("¬real;")}; + EXPECT_EQ(result, "

&notreal;

\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, + "

docs

\n"); +} + +TEST(entity_in_reference_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /café 'café'")}; + EXPECT_EQ(result, + "

docs

\n"); +} + +TEST(entity_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`é`")}; + EXPECT_EQ(result, "

&eacute;

\n"); +} + +TEST(entity_inside_indented_code_is_literal) { + const auto result{sourcemeta::core::markdown_to_html(" é&")}; + EXPECT_EQ(result, "
&eacute;&amp;\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

\n"); +} + +TEST(code_span_strips_one_surrounding_space) { + const auto result{sourcemeta::core::markdown_to_html("`` `x` ``")}; + EXPECT_EQ(result, "

`x`

\n"); +} + +TEST(code_span_strips_only_one_space_per_side) { + const auto result{sourcemeta::core::markdown_to_html("`` x ``")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(code_span_space_on_one_side_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("`x `")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(code_span_non_breaking_space_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("`\xC2\xA0" + "x`")}; + EXPECT_EQ(result, "

\xc2\xa0x

\n"); +} + +TEST(code_span_of_only_spaces_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("` `")}; + EXPECT_EQ(result, "

\n"); +} + +TEST(code_span_line_endings_become_spaces) { + const auto result{sourcemeta::core::markdown_to_html("`one\ntwo\nthree`")}; + EXPECT_EQ(result, "

one two three

\n"); +} + +TEST(code_span_interior_spaces_are_kept) { + const auto result{sourcemeta::core::markdown_to_html("`a b \n c`")}; + EXPECT_EQ(result, "

a b c

\n"); +} + +TEST(code_span_backslash_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`a\\`b")}; + EXPECT_EQ(result, "

a\\b

\n"); +} + +TEST(code_span_backtick_run_lengths_must_match) { + const auto result{sourcemeta::core::markdown_to_html("``x```")}; + EXPECT_EQ(result, "

``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

\n"); +} + +TEST(code_span_takes_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x`_`")}; + EXPECT_EQ(result, "

_x_

\n"); +} + +TEST(code_span_takes_precedence_over_link) { + const auto result{sourcemeta::core::markdown_to_html("[text `code](/url`)")}; + EXPECT_EQ(result, "

[text code](/url)

\n"); +} + +TEST(code_span_takes_precedence_over_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("``")}; + EXPECT_EQ(result, "

<https://sourcemeta.com/x>`

\n"); +} + +TEST(autolink_takes_precedence_over_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("`")}; + EXPECT_EQ(result, "

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

\n"); +} + +TEST(emphasis_does_not_span_autolink) { + const auto result{sourcemeta::core::markdown_to_html( + "_x\n\n**x")}; + EXPECT_EQ(result, "

_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, "

docs

\n"); +} + +TEST(link_destination_empty_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( <> )")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_destination_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "

docs

\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, "

docs

\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](z)")}; + EXPECT_EQ(result, "

[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, "

docs

\n"); +} + +TEST(link_destination_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)e)")}; + EXPECT_EQ(result, "

docs

\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, "

docs

\n"); +} + +TEST(link_destination_angle_brackets_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_destination_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\]\\!)")}; + EXPECT_EQ(result, "

docs

\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, + "

x

\n" + "

y

\n"); +} + +TEST(link_destination_backslash_before_letter) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\zb)")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_destination_percent_encoding_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](caf%C3%A9é)")}; + EXPECT_EQ(result, "

docs

\n"); +} + +TEST(link_destination_that_looks_like_title) { + const auto result{sourcemeta::core::markdown_to_html("[docs]('Title')")}; + EXPECT_EQ(result, "

docs

\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, + "

a b c

\n"); +} + +TEST(link_title_escaped_quotes_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x 'it\\'s & more')")}; + EXPECT_EQ(result, + "

docs

\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, "

docs

\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, "

docs

\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, "

docs

\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, "

a [b] [c [d]]

\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, "

a ] b

\n"); +} + +TEST(link_text_with_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("[_a_ __b__ `c` ~~d~~](/x)")}; + EXPECT_EQ(result, "

a b " + "c d

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

\"logo\"

\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("![a [b](/inner)](/image.png)")}; + EXPECT_EQ(result, "

\"a

\n"); +} + +TEST(link_text_brackets_take_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_[a_](/x)")}; + 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, "

a _b

\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)

\n"); +} + +TEST(link_autolink_takes_precedence) { + const auto result{sourcemeta::core::markdown_to_html( + "[a")}; + EXPECT_EQ(result, "

[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, + "

the docs

\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, "

a [b] c

\n"); +} + +TEST(reference_link_label_case_folding) { + const auto result{sourcemeta::core::markdown_to_html( + "[text][MaNuAl]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "

text

\n"); +} + +TEST(reference_link_label_titlecase_digraph_folding) { + const auto result{sourcemeta::core::markdown_to_html("[Dž]\n\n[dž]: /digraph")}; + EXPECT_EQ(result, "

\xc7\x85

\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, "

a

\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, "

a

\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, "

a\\b

\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, "

MANUAL

\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, "

the manual

\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, "

_manual_

\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, "

ac

\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, "

manual

\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("![_italic_ and `code`](/i.png)")}; + EXPECT_EQ(result, "

\"italic

\n"); +} + +TEST(image_alt_text_from_nested_image) { + const auto result{sourcemeta::core::markdown_to_html( + "![outer ![inner](/in.png)](/out.png)")}; + EXPECT_EQ(result, "

\"outer

\n"); +} + +TEST(image_alt_text_from_nested_link) { + const auto result{ + sourcemeta::core::markdown_to_html("![outer [inner](/in)](/out.png)")}; + EXPECT_EQ(result, "

\"outer

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

\"logo\"

\n" + "

\"LOGO\"

\n"); +} + +TEST(image_escaped_bracket_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("!\\[logo]\n\n[logo]: /logo.png")}; + 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("![logo]()")}; + EXPECT_EQ(result, "

\"logo\"

\n"); +} + +TEST(image_title_with_escaped_quote) { + const auto result{ + sourcemeta::core::markdown_to_html("![logo](/l.png 'it\\'s')")}; + EXPECT_EQ(result, + "

\"logo\"

\n"); +} + +TEST(autolink_uri_schemes) { + const auto result{sourcemeta::core::markdown_to_html( + "\n\n\n\n")}; + EXPECT_EQ( + result, + "

irc://chat.example

\n" + "

x-custom+scheme.1:thing

\n" + "

ab:c

\n"); +} + +TEST(autolink_uri_scheme_too_short_is_not_autolink) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "

<z:thing>

\n"); +} + +TEST(autolink_uri_with_space_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "

<https://sourcemeta.com/" + "a b>

\n"); +} + +TEST(autolink_uri_backslash_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "

https://" + "sourcemeta.com/\\_

\n"); +} + +TEST(autolink_email) { + const auto result{ + sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ( + result, + "

team@sourcemeta.com

\n"); +} + +TEST(autolink_email_with_dots_plus_and_hyphen) { + const auto result{sourcemeta::core::markdown_to_html( + "")}; + EXPECT_EQ(result, "

" + "first.last+tag@sub-domain.sourcemeta.com

\n"); +} + +TEST(autolink_email_with_backslash_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ( + result, + "

<team@sourcemeta.com>

\n"); +} + +TEST(autolink_blank_or_trailing_space_angle_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("< >\n\n")}; + EXPECT_EQ( + result, + "

< >

\n" + "

<https://sourcemeta.com " + ">

\n"); +} + +TEST(autolink_without_scheme_is_not_autolink) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "

<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

\n"); +} + +TEST(hard_break_leading_spaces_on_next_line_ignored) { + const auto result{sourcemeta::core::markdown_to_html("one \n two")}; + EXPECT_EQ(result, "

one
\n" + "two

\n"); +} + +TEST(hard_break_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_one \ntwo_\n\n__one\\\ntwo__")}; + EXPECT_EQ(result, "

one
\n" + "two

\n" + "

one
\n" + "two

\n"); +} + +TEST(hard_break_not_inside_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`one \ntwo`")}; + EXPECT_EQ(result, "

one two

\n"); +} + +TEST(hard_break_not_at_end_of_block) { + const auto result{ + sourcemeta::core::markdown_to_html("text\\\n\ntext \n\n## title\\")}; + EXPECT_EQ(result, "

text\\

\n" + "

text

\n" + "

title\\

\n"); +} + +TEST(soft_break_surrounding_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html("one \n two")}; + EXPECT_EQ(result, "

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 + +#include + +#include // std::size_t +#include // std::string, std::to_string +#include // std::string_view + +using namespace std::literals::string_view_literals; + +static auto repeat(const std::string_view pattern, const std::size_t count) + -> std::string { + std::string result; + result.reserve(pattern.size() * count); + for (std::size_t index = 0; index < count; ++index) { + result.append(pattern); + } + + return result; +} + +static auto increasing_backtick_runs(const std::size_t count) -> std::string { + std::string result; + for (std::size_t length = 1; length <= count; ++length) { + result.push_back('x'); + result.append(length, '`'); + } + + return result; +} + +static auto staircase_list(const std::size_t depth) -> std::string { + std::string result; + for (std::size_t level = 0; level < depth; ++level) { + result.append(level * 2, ' '); + result.append("- level\n"); + } + + return result; +} + +static auto numbered_reference_definitions(const std::size_t count) + -> std::string { + std::string result; + for (std::size_t index = 0; index < count; ++index) { + const auto number{std::to_string(index)}; + result.append("[r").append(number).append("]: /").append(number).append( + "\n"); + } + + return result; +} + +static auto numbered_reference_uses(const std::size_t count) -> std::string { + std::string result; + for (std::size_t index = 0; index < count; ++index) { + if (index > 0) { + result.push_back(' '); + } + + result.append("[r").append(std::to_string(index)).append("]"); + } + + return result; +} + +static auto numbered_reference_anchors(const std::size_t count) -> std::string { + std::string result; + for (std::size_t index = 0; index < count; ++index) { + if (index > 0) { + result.push_back(' '); + } + + const auto number{std::to_string(index)}; + result.append("r") + .append(number) + .append(""); + } + + return result; +} + +TEST(alternating_nested_emphasis) { + const auto input{repeat("_x **y ", 5000) + "z" + repeat(" y** x_", 5000)}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "

" + 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("![b", 10000))}; + EXPECT_EQ(result, "

" + repeat("![b", 10000) + "

\n"); +} + +TEST(mismatched_underscore_openers_and_asterisk_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("_b* ", 10000))}; + EXPECT_EQ(result, "

" + repeat("_b* ", 9999) + "_b*

\n"); +} + +TEST(intraword_triple_asterisks_followed_by_single_closers) { + const auto result{ + sourcemeta::core::markdown_to_html("x***y" + repeat("z* ", 10000))}; + EXPECT_EQ(result, "

xyz z z " + + repeat("z* ", 9996) + "z*

\n"); +} + +TEST(link_openers_with_asterisk_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("[ b*", 10000))}; + EXPECT_EQ(result, "

" + repeat("[ b*", 10000) + "

\n"); +} + +TEST(unclosed_link_destinations_with_open_parentheses) { + const auto result{sourcemeta::core::markdown_to_html(repeat("[ x](", 10000))}; + 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("![x](" + repeat("![x](<y", 5000) + "

\n"); +} + +TEST(unclosed_inline_cdata_sections) { + const auto result{ + sourcemeta::core::markdown_to_html("x" + repeat("x" + repeat("<![CDATA[", 5000) + "

\n"); +} + +TEST(unclosed_inline_processing_instructions) { + const auto result{ + sourcemeta::core::markdown_to_html("x" + repeat("x" + repeat("<?", 5000) + "

\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

\n"); +} + +TEST(code_span_with_eighty_one_backticks) { + const auto input{std::string(81, '`') + "x" + std::string(81, '`')}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "

" + 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
  • \n", 29) + + "
    \n
      \n
    • deep
    • \n
    \n" + "
    \n" + + repeat("
  • \n
\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", 49) + + "
    \n

    - deep

    \n
    \n" + + repeat("
  • \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
  • level\n", 149) + + "
      \n
    • level
    • \n
    \n" + + repeat("
  • \n
\n", 149)); +} + +TEST(table_with_many_rows) { + const auto result{sourcemeta::core::markdown_to_html( + "| a | b | c |\n|---|---|---|\n" + repeat("| x | y | z |\n", 5000))}; + EXPECT_EQ( + result, + "\n\n\n\n\n\n" + "\n\n\n" + + repeat("\n\n\n\n\n", 5000) + + "\n
abc
xyz
\n"); +} + +TEST(table_with_many_columns) { + const auto result{sourcemeta::core::markdown_to_html( + "|" + repeat(" h |", 1000) + "\n|" + repeat("-|", 1000) + "\n|" + + repeat(" c |", 1000))}; + EXPECT_EQ(result, "\n\n\n" + repeat("\n", 1000) + + "\n\n\n\n" + + repeat("\n", 1000) + + "\n\n
h
c
\n"); +} + +TEST(nul_characters_are_replaced) { + const auto result{sourcemeta::core::markdown_to_html("one\0two\0"sv)}; + EXPECT_EQ(result, "

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, "

" + label + "

\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 @@ +#include + +#include + +TEST(safe_mode_strips_mixed_case_javascript_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("[click](JaVaScRiPt:void(0))")}; + EXPECT_EQ(result, "

click

\n"); +} + +TEST(safe_mode_strips_uppercase_file_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("[local](FILE://C:/secrets.txt)")}; + EXPECT_EQ(result, "

local

\n"); +} + +TEST(safe_mode_strips_data_text_plain) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](data:text/plain,hello)")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(safe_mode_keeps_data_image_gif) { + const auto result{sourcemeta::core::markdown_to_html( + "![pixel](data:image/gif;base64,R0lGODlhAQABAAAAACw=)")}; + EXPECT_EQ(result, + "

\n"); +} + +TEST(safe_mode_keeps_uppercase_data_image_jpeg_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](DATA:IMAGE/JPEG;base64,/9j/)")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(safe_mode_strips_data_image_bmp) { + const auto result{ + sourcemeta::core::markdown_to_html("![x](data:image/bmp;base64,Qk0=)")}; + EXPECT_EQ(result, "

\"x\"

\n"); +} + +TEST(safe_mode_strips_entity_encoded_javascript) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](javascript:void(0))")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(safe_mode_keeps_percent_encoded_colon) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](vbscript%3Amsgbox)")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(safe_mode_strips_dangerous_image_source) { + const auto result{ + sourcemeta::core::markdown_to_html("![x](vbscript:msgbox)")}; + EXPECT_EQ(result, "

\"x\"

\n"); +} + +TEST(safe_mode_strips_dangerous_reference_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[x][r]\n\n[r]: file:///tmp/a")}; + EXPECT_EQ(result, "

x

\n"); +} + +TEST(safe_mode_strips_dangerous_autolink) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "

vbscript:msgbox

\n"); +} + +TEST(safe_mode_keeps_telephone_relative_and_fragment_links) { + const auto result{sourcemeta::core::markdown_to_html( + "[a](tel:+123) [b](../up) [c](#anchor)")}; + EXPECT_EQ(result, "

a b c

\n"); +} + +TEST(safe_mode_omits_inline_html_comment) { + const auto result{ + sourcemeta::core::markdown_to_html("before after")}; + EXPECT_EQ(result, "

before 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( + "
\n\n__strong__\n\n
")}; + EXPECT_EQ(result, "\n" + "

strong

\n" + "\n"); +} + +TEST(safe_mode_omits_html_block_with_blank_line_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "
\n

\n__text__\n\n_more_\n

\n
")}; + EXPECT_EQ(result, "\n" + "

more

\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, + "

x

\n"); +} + +TEST(safe_mode_escapes_quotes_in_image_title) { + const auto result{ + sourcemeta::core::markdown_to_html(R"MD(![x](/y.png "say \"hi\""))MD")}; + EXPECT_EQ( + result, + "

\"x\"

\n"); +} + +TEST(safe_mode_omits_raw_html_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_x_")}; + 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 @@ #include -#include // std::size_t -#include // std::string -#include // std::thread -#include // std::vector - -TEST(concurrent_calls) { - static constexpr std::size_t THREAD_COUNT{16}; - std::vector threads; - threads.reserve(THREAD_COUNT); - std::vector results(THREAD_COUNT); - - for (std::size_t index{0}; index < THREAD_COUNT; ++index) { - threads.emplace_back([&results, index]() { - results[index] = sourcemeta::core::markdown_to_html("**hello** world"); - }); - } - - for (auto &thread : threads) { - thread.join(); - } - - EXPECT_EQ(results[0], "

hello 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("
hi
", false)}; - EXPECT_EQ(result, "
hi
\n"); -} - TEST(default_strips_dangerous_link) { const auto result{ sourcemeta::core::markdown_to_html("[click](javascript:alert(1))")}; EXPECT_EQ(result, "

click

\n"); } -TEST(unsafe_mode_keeps_dangerous_link) { - const auto result{sourcemeta::core::markdown_to_html( - "[click](javascript:alert(1))", false)}; - EXPECT_EQ(result, "

click

\n"); -} - TEST(multiple_paragraphs) { const auto result{sourcemeta::core::markdown_to_html( "First paragraph\n\nSecond paragraph")}; diff --git a/test/markdown/markdown_unsafe_test.cc b/test/markdown/markdown_unsafe_test.cc new file mode 100644 index 0000000000..a280d0b86b --- /dev/null +++ b/test/markdown/markdown_unsafe_test.cc @@ -0,0 +1,323 @@ +#include + +#include + +TEST(html_block_script_with_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "<script type=\"module\">\n" + "\n" + "const x = 1;\n" + "\n" + "</script>\n" + "

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( + "
\n*one*\n\n*two*\n
", false)}; + EXPECT_EQ(result, "
\n" + "*one*\n" + "

two

\n" + "
\n"); +} + +TEST(html_block_partial_opening_tag) { + const auto result{sourcemeta::core::markdown_to_html( + "
\n
", false)}; + EXPECT_EQ(result, "
\n" + "
\n"); +} + +TEST(html_block_starting_with_closing_tag) { + const auto result{ + sourcemeta::core::markdown_to_html("\n_text_", false)}; + EXPECT_EQ(result, "\n" + "_text_\n"); +} + +TEST(html_block_complete_inline_tag) { + const auto result{sourcemeta::core::markdown_to_html( + "\n_text_\n", false)}; + EXPECT_EQ(result, "\n" + "_text_\n" + "\n"); +} + +TEST(html_block_complete_tag_cannot_interrupt_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("Text\n\nmore", false)}; + EXPECT_EQ(result, "

Text\n" + "\n" + "more

\n"); +} + +TEST(html_block_section_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html( + "Text\n
\nmore\n
", false)}; + EXPECT_EQ(result, "

Text

\n" + "
\n" + "more\n" + "
\n"); +} + +TEST(html_block_indented_four_spaces_is_code) { + const auto result{ + sourcemeta::core::markdown_to_html("
code
", false)}; + EXPECT_EQ(result, "
<div>code</div>\n"
+                    "
\n"); +} + +TEST(html_block_inside_list_item) { + const auto result{ + sourcemeta::core::markdown_to_html("*
\n* text", false)}; + EXPECT_EQ(result, "
    \n" + "
  • \n" + "
    \n" + "
  • \n" + "
  • text
  • \n" + "
\n"); +} + +TEST(inline_html_open_tags) { + const auto result{sourcemeta::core::markdown_to_html("", false)}; + EXPECT_EQ(result, "

\n"); +} + +TEST(inline_html_empty_elements) { + const auto result{sourcemeta::core::markdown_to_html("

", false)}; + EXPECT_EQ(result, "



\n"); +} + +TEST(inline_html_attributes_across_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "", false)}; + EXPECT_EQ(result, "

\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("
", false)}; + EXPECT_EQ(result, "

\n"); +} + +TEST(inline_html_closing_tag_with_attributes_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("
", false)}; + EXPECT_EQ(result, "

</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( + " <script> <noframes> <noembed> <iframe> <xmp> <style> " + "<textarea> <title>", + false)}; + EXPECT_EQ(result, + "<p>&lt;plaintext> &lt;script> &lt;noframes> &lt;noembed> " + "&lt;iframe> &lt;xmp> &lt;style> &lt;textarea> &lt;title></p>\n"); +} + +TEST(tagfilter_mixed_allowed_and_disallowed_tags) { + const auto result{ + sourcemeta::core::markdown_to_html("<b> <iframe src=\"x\"> <i>", false)}; + EXPECT_EQ(result, "<p><b> &lt;iframe src=\"x\"> <i></p>\n"); +} + +TEST(tagfilter_closing_disallowed_tags) { + const auto result{ + sourcemeta::core::markdown_to_html("</textarea> </xmp>", false)}; + EXPECT_EQ(result, "<p>&lt;/textarea> &lt;/xmp></p>\n"); +} + +TEST(tagfilter_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("<IfRaMe src=\"x\"></iFrAmE>", false)}; + EXPECT_EQ(result, "&lt;IfRaMe src=\"x\">&lt;/iFrAmE>\n"); +} + +TEST(tagfilter_inside_html_block) { + const auto result{sourcemeta::core::markdown_to_html( + "<div>\n<noembed>x</noembed> <TITLE>y</TITLE>\n</div>", false)}; + EXPECT_EQ(result, "<div>\n" + "&lt;noembed>x&lt;/noembed> &lt;TITLE>y&lt;/TITLE>\n" + "</div>\n"); +} + +TEST(unsafe_mode_keeps_uppercase_file_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](FILE://C:/secrets.txt)", false)}; + EXPECT_EQ(result, "<p><a href=\"FILE://C:/secrets.txt\">x</a></p>\n"); +} + +TEST(unsafe_mode_keeps_dangerous_image_source) { + const auto result{ + sourcemeta::core::markdown_to_html("![x](vbscript:msgbox)", false)}; + EXPECT_EQ(result, "<p><img src=\"vbscript:msgbox\" alt=\"x\" /></p>\n"); +} + +TEST(unsafe_mode_keeps_raw_html_inside_emphasis) { + const auto result{sourcemeta::core::markdown_to_html( + "_<span title=\"_\">x</span>_", false)}; + EXPECT_EQ(result, "<p><em><span title=\"_\">x</span></em></p>\n"); +} + +TEST(unsafe_mode_raw_html_takes_precedence_over_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[x <span data=\"](/y)\">", false)}; + EXPECT_EQ(result, "<p>[x <span data=\"](/y)\"></p>\n"); +} + +TEST(unsafe_mode_list_separated_by_html_comment) { + const auto result{sourcemeta::core::markdown_to_html( + "* one\n\n<!-- split -->\n\n* two", false)}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "<!-- split -->\n" + "<ul>\n" + "<li>two</li>\n" + "</ul>\n"); +} + +TEST(unsafe_mode_passes_raw_html) { + const auto result{ + sourcemeta::core::markdown_to_html("<div onclick=\"x\">hi</div>", false)}; + EXPECT_EQ(result, "<div onclick=\"x\">hi</div>\n"); +} + +TEST(unsafe_mode_keeps_dangerous_link) { + const auto result{sourcemeta::core::markdown_to_html( + "[click](javascript:alert(1))", false)}; + EXPECT_EQ(result, "<p><a href=\"javascript:alert(1)\">click</a></p>\n"); +} From 8af23fcda5115c777079ffd5ab80ae24d9747036 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti <jv@jviotti.com> Date: Mon, 14 Sep 2026 17:16:04 -0300 Subject: [PATCH 2/2] More Signed-off-by: Juan Cruz Viotti <jv@jviotti.com> --- test/markdown/CMakeLists.txt | 23 +- test/markdown/markdown_autolinks_test.cc | 306 +++ test/markdown/markdown_blockquotes_test.cc | 259 +++ test/markdown/markdown_blocks_test.cc | 1834 ----------------- test/markdown/markdown_characters_test.cc | 487 +++++ test/markdown/markdown_code_blocks_test.cc | 284 +++ test/markdown/markdown_code_spans_test.cc | 97 + test/markdown/markdown_emphasis_test.cc | 312 +++ test/markdown/markdown_entities_test.cc | 114 + test/markdown/markdown_escapes_test.cc | 98 + test/markdown/markdown_extensions_test.cc | 796 ------- test/markdown/markdown_footnotes_test.cc | 241 +++ test/markdown/markdown_headings_test.cc | 360 ++++ test/markdown/markdown_images_test.cc | 85 + test/markdown/markdown_inlines_test.cc | 1039 ---------- test/markdown/markdown_line_breaks_test.cc | 58 + test/markdown/markdown_links_test.cc | 589 ++++++ test/markdown/markdown_lists_test.cc | 793 +++++++ test/markdown/markdown_paragraphs_test.cc | 76 + test/markdown/markdown_pathological_test.cc | 7 - test/markdown/markdown_safety_test.cc | 67 + test/markdown/markdown_strikethrough_test.cc | 86 + test/markdown/markdown_tables_test.cc | 417 ++++ test/markdown/markdown_task_lists_test.cc | 131 ++ test/markdown/markdown_test.cc | 938 --------- .../markdown/markdown_thematic_breaks_test.cc | 131 ++ test/markdown/markdown_unsafe_test.cc | 22 + 27 files changed, 5032 insertions(+), 4618 deletions(-) create mode 100644 test/markdown/markdown_autolinks_test.cc create mode 100644 test/markdown/markdown_blockquotes_test.cc delete mode 100644 test/markdown/markdown_blocks_test.cc create mode 100644 test/markdown/markdown_characters_test.cc create mode 100644 test/markdown/markdown_code_blocks_test.cc create mode 100644 test/markdown/markdown_code_spans_test.cc create mode 100644 test/markdown/markdown_emphasis_test.cc create mode 100644 test/markdown/markdown_entities_test.cc create mode 100644 test/markdown/markdown_escapes_test.cc delete mode 100644 test/markdown/markdown_extensions_test.cc create mode 100644 test/markdown/markdown_footnotes_test.cc create mode 100644 test/markdown/markdown_headings_test.cc create mode 100644 test/markdown/markdown_images_test.cc delete mode 100644 test/markdown/markdown_inlines_test.cc create mode 100644 test/markdown/markdown_line_breaks_test.cc create mode 100644 test/markdown/markdown_links_test.cc create mode 100644 test/markdown/markdown_lists_test.cc create mode 100644 test/markdown/markdown_paragraphs_test.cc create mode 100644 test/markdown/markdown_strikethrough_test.cc create mode 100644 test/markdown/markdown_tables_test.cc create mode 100644 test/markdown/markdown_task_lists_test.cc delete mode 100644 test/markdown/markdown_test.cc create mode 100644 test/markdown/markdown_thematic_breaks_test.cc diff --git a/test/markdown/CMakeLists.txt b/test/markdown/CMakeLists.txt index a945aed1cb..96f9fdd72d 100644 --- a/test/markdown/CMakeLists.txt +++ b/test/markdown/CMakeLists.txt @@ -1,12 +1,27 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME markdown SOURCES - markdown_blocks_test.cc + markdown_autolinks_test.cc + markdown_blockquotes_test.cc + markdown_characters_test.cc + markdown_code_blocks_test.cc + markdown_code_spans_test.cc markdown_concurrency_test.cc - markdown_extensions_test.cc - markdown_inlines_test.cc + markdown_emphasis_test.cc + markdown_entities_test.cc + markdown_escapes_test.cc + markdown_footnotes_test.cc + markdown_headings_test.cc + markdown_images_test.cc + markdown_line_breaks_test.cc + markdown_links_test.cc + markdown_lists_test.cc + markdown_paragraphs_test.cc markdown_pathological_test.cc markdown_safety_test.cc - markdown_test.cc + markdown_strikethrough_test.cc + markdown_tables_test.cc + markdown_task_lists_test.cc + markdown_thematic_breaks_test.cc markdown_unsafe_test.cc) target_link_libraries(sourcemeta_core_markdown_unit diff --git a/test/markdown/markdown_autolinks_test.cc b/test/markdown/markdown_autolinks_test.cc new file mode 100644 index 0000000000..bd2ebd78e7 --- /dev/null +++ b/test/markdown/markdown_autolinks_test.cc @@ -0,0 +1,306 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(autolink_url) { + const auto result{ + sourcemeta::core::markdown_to_html("Visit https://example.com today")}; + EXPECT_EQ(result, + "<p>Visit <a href=\"https://example.com\">https://example.com</a>" + " today</p>\n"); +} + +TEST(autolink_http) { + const auto result{sourcemeta::core::markdown_to_html("http://example.com")}; + EXPECT_EQ(result, + "<p><a href=\"http://example.com\">http://example.com</a></p>\n"); +} + +TEST(autolink_www) { + const auto result{sourcemeta::core::markdown_to_html("www.example.com")}; + EXPECT_EQ(result, + "<p><a href=\"http://www.example.com\">www.example.com</a></p>\n"); +} + +TEST(autolink_email) { + const auto result{ + sourcemeta::core::markdown_to_html("Contact user@example.com")}; + EXPECT_EQ(result, "<p>Contact <a href=\"mailto:user@example.com\">" + "user@example.com</a></p>\n"); +} + +TEST(angle_bracket_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://example.com>")}; + EXPECT_EQ(result, + "<p><a href=\"https://example.com\">https://example.com</a></p>\n"); +} + +TEST(autolink_inside_list) { + const auto result{sourcemeta::core::markdown_to_html( + "- visit https://example.com\n- done")}; + EXPECT_EQ(result, "<ul>\n<li>visit <a href=\"https://example.com\">" + "https://example.com</a></li>\n<li>done</li>\n</ul>\n"); +} + +TEST(multiple_autolinks_in_paragraph) { + const auto result{sourcemeta::core::markdown_to_html( + "See https://one.com and https://two.com")}; + EXPECT_EQ(result, + "<p>See <a href=\"https://one.com\">https://one.com</a> and " + "<a href=\"https://two.com\">https://two.com</a></p>\n"); +} + +TEST(autolink_with_trailing_punctuation) { + const auto result{ + sourcemeta::core::markdown_to_html("See https://example.com.")}; + EXPECT_EQ( + result, + "<p>See <a href=\"https://example.com\">https://example.com</a>.</p>\n"); +} + +TEST(autolink_in_parentheses) { + const auto result{ + sourcemeta::core::markdown_to_html("(see https://example.com)")}; + EXPECT_EQ( + result, + "<p>(see <a href=\"https://example.com\">https://example.com</a>)</p>\n"); +} + +TEST(autolink_takes_precedence_over_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/`x>`")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/%60x\">https://" + "sourcemeta.com/`x</a>`</p>\n"); +} + +TEST(autolink_uri_schemes) { + const auto result{sourcemeta::core::markdown_to_html( + "<irc://chat.example>\n\n<x-custom+scheme.1:thing>\n\n<ab:c>")}; + EXPECT_EQ( + result, + "<p><a href=\"irc://chat.example\">irc://chat.example</a></p>\n" + "<p><a href=\"x-custom+scheme.1:thing\">x-custom+scheme.1:thing</a></p>\n" + "<p><a href=\"ab:c\">ab:c</a></p>\n"); +} + +TEST(autolink_uri_scheme_too_short_is_not_autolink) { + const auto result{sourcemeta::core::markdown_to_html("<z:thing>")}; + EXPECT_EQ(result, "<p>&lt;z:thing&gt;</p>\n"); +} + +TEST(autolink_uri_with_space_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/a b>")}; + EXPECT_EQ(result, "<p>&lt;<a " + "href=\"https://sourcemeta.com/a\">https://sourcemeta.com/" + "a</a> b&gt;</p>\n"); +} + +TEST(autolink_uri_backslash_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/\\_>")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/%5C_\">https://" + "sourcemeta.com/\\_</a></p>\n"); +} + +TEST(autolink_email_in_angle_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("<team@sourcemeta.com>")}; + EXPECT_EQ( + result, + "<p><a " + "href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a></p>\n"); +} + +TEST(autolink_email_with_dots_plus_and_hyphen) { + const auto result{sourcemeta::core::markdown_to_html( + "<first.last+tag@sub-domain.sourcemeta.com>")}; + EXPECT_EQ(result, "<p><a " + "href=\"mailto:first.last+tag@sub-domain.sourcemeta.com\">" + "first.last+tag@sub-domain.sourcemeta.com</a></p>\n"); +} + +TEST(autolink_email_with_backslash_is_not_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("<team\\@sourcemeta.com>")}; + EXPECT_EQ( + result, + "<p>&lt;<a " + "href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a>&gt;</p>\n"); +} + +TEST(autolink_blank_or_trailing_space_angle_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("< >\n\n<https://sourcemeta.com >")}; + EXPECT_EQ( + result, + "<p>&lt; &gt;</p>\n" + "<p>&lt;<a href=\"https://sourcemeta.com\">https://sourcemeta.com</a> " + "&gt;</p>\n"); +} + +TEST(autolink_without_scheme_is_not_autolink) { + const auto result{sourcemeta::core::markdown_to_html("<sourcemeta.com>")}; + EXPECT_EQ(result, "<p>&lt;sourcemeta.com&gt;</p>\n"); +} + +TEST(extended_autolink_www_with_path) { + const auto result{ + sourcemeta::core::markdown_to_html("see www.sourcemeta.com/docs/intro")}; + EXPECT_EQ(result, "<p>see <a " + "href=\"http://www.sourcemeta.com/docs/" + "intro\">www.sourcemeta.com/docs/intro</a></p>\n"); +} + +TEST(extended_autolink_trailing_punctuation_excluded) { + const auto result{ + sourcemeta::core::markdown_to_html("Go to www.sourcemeta.com/x.y!")}; + EXPECT_EQ(result, "<p>Go to <a " + "href=\"http://www.sourcemeta.com/x.y\">www.sourcemeta.com/" + "x.y</a>!</p>\n"); +} + +TEST(extended_autolink_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/wiki/Thing_(disambiguation)")}; + EXPECT_EQ( + result, + "<p><a " + "href=\"https://sourcemeta.com/wiki/Thing_(disambiguation)\">https://" + "sourcemeta.com/wiki/Thing_(disambiguation)</a></p>\n"); +} + +TEST(extended_autolink_unbalanced_closing_parentheses) { + const auto result{ + sourcemeta::core::markdown_to_html("https://sourcemeta.com/a_(b)))")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/a_(b)\">https://" + "sourcemeta.com/a_(b)</a>))</p>\n"); +} + +TEST(extended_autolink_inside_parentheses) { + const auto result{ + sourcemeta::core::markdown_to_html("(see https://sourcemeta.com/a_(b))")}; + EXPECT_EQ(result, "<p>(see <a " + "href=\"https://sourcemeta.com/a_(b)\">https://" + "sourcemeta.com/a_(b)</a>)</p>\n"); +} + +TEST(extended_autolink_entity_like_suffix_excluded) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/?x=1&amp;\n\nhttps://sourcemeta.com/?x=1&y=2")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/?x=1\">https://" + "sourcemeta.com/?x=1</a>&amp;</p>\n" + "<p><a " + "href=\"https://sourcemeta.com/?x=1&amp;y=2\">https://" + "sourcemeta.com/?x=1&amp;y=2</a></p>\n"); +} + +TEST(extended_autolink_ends_at_less_than) { + const auto result{ + sourcemeta::core::markdown_to_html("https://sourcemeta.com/a<b")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/a\">https://sourcemeta.com/" + "a</a>&lt;b</p>\n"); +} + +TEST(extended_autolink_underscore_in_final_domain_segments) { + const auto result{sourcemeta::core::markdown_to_html( + "www.sourcemeta._com\n\nwww.source_meta.com")}; + EXPECT_EQ(result, "<p>www.sourcemeta._com</p>\n" + "<p>www.source_meta.com</p>\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, + "<p><a " + "href=\"http://" + "www.docs_site.sourcemeta.com\">www.docs_site.sourcemeta.com</a></p>\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, + "<p><a " + "href=\"https://sourcemeta.com/search?q=json+schema#results\">https://" + "sourcemeta.com/search?q=json+schema#results</a></p>\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, + "<p><a href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a></p>\n" + "<p>team@source+meta.com</p>\n" + "<p><a " + "href=\"mailto:team+tag@sourcemeta.com\">team+tag@sourcemeta.com</a></" + "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, "<p><a " + "href=\"mailto:first.last@sourcemeta.com\">first.last@" + "sourcemeta.com</a>.</p>\n" + "<p>first.last@sourcemeta.com-</p>\n" + "<p>first.last@sourcemeta.com_</p>\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, + "<p><a " + "href=\"mailto:team@sourcemeta.com\">mailto:team@sourcemeta.com</a></p>\n" + "<p><a " + "href=\"xmpp:team@sourcemeta.com/resource\">xmpp:team@sourcemeta.com/" + "resource</a></p>\n"); +} + +TEST(extended_autolink_not_after_word_character) { + const auto result{sourcemeta::core::markdown_to_html("xwww.sourcemeta.com")}; + EXPECT_EQ(result, "<p>xwww.sourcemeta.com</p>\n"); +} + +TEST(extended_autolink_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_https://sourcemeta.com_")}; + EXPECT_EQ( + result, + "<p><em><a " + "href=\"https://sourcemeta.com\">https://sourcemeta.com</a></em></p>\n"); +} + +TEST(extended_autolink_ftp_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("ftp://files.sourcemeta.com/archive")}; + EXPECT_EQ(result, "<p><a " + "href=\"ftp://files.sourcemeta.com/archive\">ftp://" + "files.sourcemeta.com/archive</a></p>\n"); +} + +TEST(extended_autolink_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[https://sourcemeta.com](/x)")}; + EXPECT_EQ(result, "<p><a href=\"/x\">https://sourcemeta.com</a></p>\n"); +} + +TEST(extended_autolink_inside_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("`https://sourcemeta.com`")}; + EXPECT_EQ(result, "<p><code>https://sourcemeta.com</code></p>\n"); +} diff --git a/test/markdown/markdown_blockquotes_test.cc b/test/markdown/markdown_blockquotes_test.cc new file mode 100644 index 0000000000..54dac695e9 --- /dev/null +++ b/test/markdown/markdown_blockquotes_test.cc @@ -0,0 +1,259 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(simple_blockquote) { + const auto result{sourcemeta::core::markdown_to_html("> quoted text")}; + EXPECT_EQ(result, "<blockquote>\n<p>quoted text</p>\n</blockquote>\n"); +} + +TEST(multiline_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> line one\n> line two")}; + EXPECT_EQ(result, "<blockquote>\n<p>line one\nline two</p>\n</blockquote>\n"); +} + +TEST(nested_blockquote) { + const auto result{sourcemeta::core::markdown_to_html("> outer\n>> inner")}; + EXPECT_EQ(result, "<blockquote>\n<p>outer</p>\n<blockquote>\n<p>inner</p>\n" + "</blockquote>\n</blockquote>\n"); +} + +TEST(blockquote_with_formatting) { + const auto result{ + sourcemeta::core::markdown_to_html("> **bold** and *italic*")}; + EXPECT_EQ(result, + "<blockquote>\n<p><strong>bold</strong> and <em>italic</em></p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_with_list) { + const auto result{ + sourcemeta::core::markdown_to_html("> items:\n> - one\n> - two")}; + EXPECT_EQ(result, + "<blockquote>\n<p>items:</p>\n<ul>\n<li>one</li>\n<li>two</li>\n" + "</ul>\n</blockquote>\n"); +} + +TEST(blockquote_with_code_block) { + const auto result{ + sourcemeta::core::markdown_to_html("> example:\n>\n> code here")}; + EXPECT_EQ(result, "<blockquote>\n<p>example:</p>\n" + "<pre><code>code here\n</code></pre>\n</blockquote>\n"); +} + +TEST(blockquote_with_heading) { + const auto result{sourcemeta::core::markdown_to_html("> ## Quoted heading")}; + EXPECT_EQ(result, "<blockquote>\n<h2>Quoted heading</h2>\n</blockquote>\n"); +} + +TEST(deeply_nested_blockquote) { + const auto result{sourcemeta::core::markdown_to_html("> a\n>> b\n>>> c")}; + EXPECT_EQ(result, "<blockquote>\n<p>a</p>\n<blockquote>\n<p>b</p>\n" + "<blockquote>\n<p>c</p>\n</blockquote>\n</blockquote>\n" + "</blockquote>\n"); +} + +TEST(blockquote_followed_by_list) { + const auto result{ + sourcemeta::core::markdown_to_html("> quote\n\n- item one\n- item two")}; + EXPECT_EQ(result, "<blockquote>\n<p>quote</p>\n</blockquote>\n" + "<ul>\n<li>item one</li>\n<li>item two</li>\n</ul>\n"); +} + +TEST(blockquote_with_strikethrough) { + const auto result{sourcemeta::core::markdown_to_html("> ~~deleted~~")}; + EXPECT_EQ(result, "<blockquote>\n<p><del>deleted</del></p>\n</blockquote>\n"); +} + +TEST(blockquote_marker_without_following_space) { + const auto result{ + sourcemeta::core::markdown_to_html(">## Title\n>text\n> more")}; + EXPECT_EQ(result, "<blockquote>\n" + "<h2>Title</h2>\n" + "<p>text\n" + "more</p>\n" + "</blockquote>\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, "<blockquote>\n" + "<p>one\n" + "two\n" + "three</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_marker_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" > one\n > two")}; + EXPECT_EQ(result, "<pre><code>&gt; one\n" + "&gt; two\n" + "</code></pre>\n"); +} + +TEST(blockquote_lazy_paragraph_continuation) { + const auto result{sourcemeta::core::markdown_to_html("> one\ntwo")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one\n" + "two</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_lazy_continuation_between_marked_lines) { + const auto result{sourcemeta::core::markdown_to_html("> one\ntwo\n> three")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one\n" + "two\n" + "three</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("> one\n***")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one</p>\n" + "</blockquote>\n" + "<hr />\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_list) { + const auto result{sourcemeta::core::markdown_to_html("> * one\n* two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "</blockquote>\n" + "<ul>\n" + "<li>two</li>\n" + "</ul>\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_indented_code) { + const auto result{ + sourcemeta::core::markdown_to_html("> code\n more")}; + EXPECT_EQ(result, "<blockquote>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</blockquote>\n" + "<pre><code> more\n" + "</code></pre>\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_fenced_code) { + const auto result{sourcemeta::core::markdown_to_html("> ~~~\ncode\n~~~")}; + EXPECT_EQ(result, "<blockquote>\n" + "<pre><code></code></pre>\n" + "</blockquote>\n" + "<p>code</p>\n" + "<pre><code></code></pre>\n"); +} + +TEST(blockquote_lazy_line_that_looks_like_list_item) { + const auto result{sourcemeta::core::markdown_to_html("> one\n * two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one\n" + "* two</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_empty_indented_marker) { + const auto result{sourcemeta::core::markdown_to_html(" >")}; + EXPECT_EQ(result, "<blockquote>\n" + "</blockquote>\n"); +} + +TEST(blockquote_empty_multiple_lines) { + const auto result{sourcemeta::core::markdown_to_html(">\n>\n> ")}; + EXPECT_EQ(result, "<blockquote>\n" + "</blockquote>\n"); +} + +TEST(blockquote_blank_first_and_last_lines) { + const auto result{sourcemeta::core::markdown_to_html("> \n> text\n>")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>text</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_blank_line_separates_blockquotes) { + const auto result{sourcemeta::core::markdown_to_html("> one\n\n> two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one</p>\n" + "</blockquote>\n" + "<blockquote>\n" + "<p>two</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_empty_marker_line_joins_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html("> one\n> \n> two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one</p>\n" + "<p>two</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n>quote")}; + EXPECT_EQ(result, "<p>text</p>\n" + "<blockquote>\n" + "<p>quote</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_ended_by_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("> one\n- - -\n> two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one</p>\n" + "</blockquote>\n" + "<hr />\n" + "<blockquote>\n" + "<p>two</p>\n" + "</blockquote>\n"); +} + +TEST(blockquote_lazy_line_after_blank_marker_line) { + const auto result{sourcemeta::core::markdown_to_html("> one\n>\ntwo")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>one</p>\n" + "</blockquote>\n" + "<p>two</p>\n"); +} + +TEST(blockquote_nested_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html(">> one\ntwo")}; + EXPECT_EQ(result, "<blockquote>\n" + "<blockquote>\n" + "<p>one\n" + "two</p>\n" + "</blockquote>\n" + "</blockquote>\n"); +} + +TEST(blockquote_nested_markers_without_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html(">>one\n>two\n>>>three")}; + EXPECT_EQ(result, "<blockquote>\n" + "<blockquote>\n" + "<p>one\n" + "two</p>\n" + "<blockquote>\n" + "<p>three</p>\n" + "</blockquote>\n" + "</blockquote>\n" + "</blockquote>\n"); +} + +TEST(blockquote_indented_code_needs_five_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html("> code\n\n> text")}; + EXPECT_EQ(result, "<blockquote>\n" + "<pre><code> code\n" + "</code></pre>\n" + "</blockquote>\n" + "<blockquote>\n" + "<p>text</p>\n" + "</blockquote>\n"); +} diff --git a/test/markdown/markdown_blocks_test.cc b/test/markdown/markdown_blocks_test.cc deleted file mode 100644 index 90021b2efc..0000000000 --- a/test/markdown/markdown_blocks_test.cc +++ /dev/null @@ -1,1834 +0,0 @@ -#include <sourcemeta/core/markdown.h> - -#include <sourcemeta/core/test.h> - -TEST(tab_indented_code_block) { - const auto result{sourcemeta::core::markdown_to_html("\tlime\tpear\t\tplum")}; - EXPECT_EQ(result, "<pre><code>lime\tpear\t\tplum\n" - "</code></pre>\n"); -} - -TEST(tab_after_space_indented_code_block) { - const auto result{sourcemeta::core::markdown_to_html(" \tlime\tpear")}; - EXPECT_EQ(result, "<pre><code>lime\tpear\n" - "</code></pre>\n"); -} - -TEST(tab_list_item_continuation) { - const auto result{sourcemeta::core::markdown_to_html(" - kiwi\n\n\tmango")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>kiwi</p>\n" - "<p>mango</p>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(tab_list_item_indented_code) { - const auto result{sourcemeta::core::markdown_to_html("* kiwi\n\n\t\tmango")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>kiwi</p>\n" - "<pre><code> mango\n" - "</code></pre>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(tab_after_blockquote_marker) { - const auto result{sourcemeta::core::markdown_to_html(">\t\tcedar\n>\t\toak")}; - EXPECT_EQ(result, "<blockquote>\n" - "<pre><code> cedar\n" - " oak\n" - "</code></pre>\n" - "</blockquote>\n"); -} - -TEST(tab_after_list_marker) { - const auto result{sourcemeta::core::markdown_to_html("+\t\tcedar")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<pre><code> cedar\n" - "</code></pre>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(tab_after_heading_marker) { - const auto result{sourcemeta::core::markdown_to_html("##\tTitle")}; - EXPECT_EQ(result, "<h2>Title</h2>\n"); -} - -TEST(tab_between_thematic_break_characters) { - const auto result{sourcemeta::core::markdown_to_html("-\t-\t-")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(list_marker_takes_precedence_over_code_span) { - const auto result{sourcemeta::core::markdown_to_html("* `alpha\n* beta`")}; - EXPECT_EQ(result, "<ul>\n" - "<li>`alpha</li>\n" - "<li>beta`</li>\n" - "</ul>\n"); -} - -TEST(thematic_break_with_trailing_whitespace) { - const auto result{sourcemeta::core::markdown_to_html("***\t ")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(thematic_break_plus_signs_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("++++")}; - EXPECT_EQ(result, "<p>++++</p>\n"); -} - -TEST(thematic_break_equals_signs_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("= = = =")}; - EXPECT_EQ(result, "<p>= = = =</p>\n"); -} - -TEST(thematic_break_two_characters_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("**\n\n__")}; - EXPECT_EQ(result, "<p>**</p>\n" - "<p>__</p>\n"); -} - -TEST(thematic_break_indented_one_to_three_spaces) { - const auto result{sourcemeta::core::markdown_to_html(" ___\n ---\n ***")}; - EXPECT_EQ(result, "<hr />\n" - "<hr />\n" - "<hr />\n"); -} - -TEST(thematic_break_indented_four_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" ---")}; - EXPECT_EQ(result, "<pre><code>---\n" - "</code></pre>\n"); -} - -TEST(thematic_break_indented_four_spaces_continues_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("Paragraph text\n ___")}; - EXPECT_EQ(result, "<p>Paragraph text\n" - "___</p>\n"); -} - -TEST(thematic_break_many_characters) { - const auto result{sourcemeta::core::markdown_to_html( - "-------------------------------------------------")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(thematic_break_spaces_between_characters) { - const auto result{sourcemeta::core::markdown_to_html("* * * *")}; - EXPECT_EQ(result, "<hr />\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, "<ul>\n" - "<li>\n" - "<ul>\n" - "<li>\n" - "<ul>\n" - "<li>x</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n" - "<p>x______</p>\n" - "<p><em><strong>x</strong></em></p>\n"); -} - -TEST(thematic_break_mixed_characters_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("-*-")}; - EXPECT_EQ(result, "<p>-*-</p>\n"); -} - -TEST(thematic_break_interrupts_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("alpha\n___\nbeta")}; - EXPECT_EQ(result, "<p>alpha</p>\n" - "<hr />\n" - "<p>beta</p>\n"); -} - -TEST(dashes_after_paragraph_make_setext_heading) { - const auto result{sourcemeta::core::markdown_to_html("alpha\n-----\nbeta")}; - EXPECT_EQ(result, "<h2>alpha</h2>\n" - "<p>beta</p>\n"); -} - -TEST(thematic_break_takes_precedence_over_list_item) { - const auto result{sourcemeta::core::markdown_to_html("- one\n- - -\n- two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "<hr />\n" - "<ul>\n" - "<li>two</li>\n" - "</ul>\n"); -} - -TEST(thematic_break_inside_list_item) { - const auto result{sourcemeta::core::markdown_to_html("* one\n* - - -")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "<li>\n" - "<hr />\n" - "</li>\n" - "</ul>\n"); -} - -TEST(atx_heading_eight_hashes_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("######## title")}; - EXPECT_EQ(result, "<p>######## title</p>\n"); -} - -TEST(atx_heading_without_space_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("#1 fan\n\n##tag")}; - EXPECT_EQ(result, "<p>#1 fan</p>\n" - "<p>##tag</p>\n"); -} - -TEST(atx_heading_escaped_hash_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("\\# title")}; - EXPECT_EQ(result, "<p># title</p>\n"); -} - -TEST(atx_heading_inline_content) { - const auto result{ - sourcemeta::core::markdown_to_html("## plain *em* \\*raw\\*")}; - EXPECT_EQ(result, "<h2>plain <em>em</em> *raw*</h2>\n"); -} - -TEST(atx_heading_surrounding_spaces_ignored) { - const auto result{ - sourcemeta::core::markdown_to_html("### spaced out ")}; - EXPECT_EQ(result, "<h3>spaced out</h3>\n"); -} - -TEST(atx_heading_indented_up_to_three_spaces) { - const auto result{ - sourcemeta::core::markdown_to_html(" #### deep\n # shallow")}; - EXPECT_EQ(result, "<h4>deep</h4>\n" - "<h1>shallow</h1>\n"); -} - -TEST(atx_heading_indented_four_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" ## code")}; - EXPECT_EQ(result, "<pre><code>## code\n" - "</code></pre>\n"); -} - -TEST(atx_heading_indented_four_spaces_continues_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("text\n ## more")}; - EXPECT_EQ(result, "<p>text\n" - "## more</p>\n"); -} - -TEST(atx_heading_closing_sequence_of_any_length) { - const auto result{ - sourcemeta::core::markdown_to_html("## title #######\n#### title #")}; - EXPECT_EQ(result, "<h2>title</h2>\n" - "<h4>title</h4>\n"); -} - -TEST(atx_heading_closing_sequence_followed_by_spaces) { - const auto result{sourcemeta::core::markdown_to_html("## title ## ")}; - EXPECT_EQ(result, "<h2>title</h2>\n"); -} - -TEST(atx_heading_hashes_followed_by_text_are_content) { - const auto result{sourcemeta::core::markdown_to_html("## title ## tail")}; - EXPECT_EQ(result, "<h2>title ## tail</h2>\n"); -} - -TEST(atx_heading_closing_sequence_needs_preceding_space) { - const auto result{sourcemeta::core::markdown_to_html("## title##")}; - EXPECT_EQ(result, "<h2>title##</h2>\n"); -} - -TEST(atx_heading_escaped_closing_hashes) { - const auto result{ - sourcemeta::core::markdown_to_html("## title \\##\n# title #\\#")}; - EXPECT_EQ(result, "<h2>title ##</h2>\n" - "<h1>title ##</h1>\n"); -} - -TEST(atx_heading_between_thematic_breaks) { - const auto result{sourcemeta::core::markdown_to_html("---\n# title\n---")}; - EXPECT_EQ(result, "<hr />\n" - "<h1>title</h1>\n" - "<hr />\n"); -} - -TEST(atx_heading_interrupts_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("one line\n## heading\nnext line")}; - EXPECT_EQ(result, "<p>one line</p>\n" - "<h2>heading</h2>\n" - "<p>next line</p>\n"); -} - -TEST(atx_heading_empty) { - const auto result{sourcemeta::core::markdown_to_html("#\n## \n#### ####")}; - EXPECT_EQ(result, "<h1></h1>\n" - "<h2></h2>\n" - "<h4></h4>\n"); -} - -TEST(setext_heading_single_character_underline) { - const auto result{sourcemeta::core::markdown_to_html("Title *em*\n=")}; - EXPECT_EQ(result, "<h1>Title <em>em</em></h1>\n"); -} - -TEST(setext_heading_multiline_content) { - const auto result{ - sourcemeta::core::markdown_to_html("First *line\nsecond* line\n---")}; - EXPECT_EQ(result, "<h2>First <em>line\n" - "second</em> line</h2>\n"); -} - -TEST(setext_heading_content_surrounding_spaces_stripped) { - const auto result{ - sourcemeta::core::markdown_to_html(" Title *em\ntext* \n===")}; - EXPECT_EQ(result, "<h1>Title <em>em\n" - "text</em></h1>\n"); -} - -TEST(setext_heading_indented_content) { - const auto result{ - sourcemeta::core::markdown_to_html(" Title\n ===\n\n Other\n-")}; - EXPECT_EQ(result, "<h1>Title</h1>\n" - "<h2>Other</h2>\n"); -} - -TEST(setext_heading_content_indented_four_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" Title\n ===")}; - EXPECT_EQ(result, "<pre><code>Title\n" - "===\n" - "</code></pre>\n"); -} - -TEST(setext_heading_underline_indented_up_to_three_spaces) { - const auto result{sourcemeta::core::markdown_to_html("Title\n === ")}; - EXPECT_EQ(result, "<h1>Title</h1>\n"); -} - -TEST(setext_heading_underline_indented_four_spaces_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("Title\n ===")}; - EXPECT_EQ(result, "<p>Title\n" - "===</p>\n"); -} - -TEST(setext_heading_underline_with_internal_space_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("Title\n== ==")}; - EXPECT_EQ(result, "<p>Title\n" - "== ==</p>\n"); -} - -TEST(setext_heading_trailing_spaces_are_not_hard_break) { - const auto result{sourcemeta::core::markdown_to_html("Title \n===")}; - EXPECT_EQ(result, "<h1>Title</h1>\n"); -} - -TEST(setext_heading_trailing_backslash_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("Title\\\n===")}; - EXPECT_EQ(result, "<h1>Title\\</h1>\n"); -} - -TEST(setext_heading_underline_breaks_code_span) { - const auto result{sourcemeta::core::markdown_to_html("`code\n===\n`")}; - EXPECT_EQ(result, "<h1>`code</h1>\n" - "<p>`</p>\n"); -} - -TEST(setext_heading_equals_underline_is_lazy_continuation) { - const auto result{sourcemeta::core::markdown_to_html("> Title\n===")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>Title\n" - "===</p>\n" - "</blockquote>\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, "<blockquote>\n" - "<p>quote\n" - "continued</p>\n" - "</blockquote>\n" - "<hr />\n"); -} - -TEST(setext_heading_equals_after_list_item_is_lazy_continuation) { - const auto result{sourcemeta::core::markdown_to_html("* Item\n===")}; - EXPECT_EQ(result, "<ul>\n" - "<li>Item\n" - "===</li>\n" - "</ul>\n"); -} - -TEST(setext_heading_multiline_paragraph_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Line one\nLine two\n===")}; - EXPECT_EQ(result, "<h1>Line one\n" - "Line two</h1>\n"); -} - -TEST(setext_heading_equals_line_then_heading) { - const auto result{ - sourcemeta::core::markdown_to_html("===\nTitle\n===\nText\n---")}; - EXPECT_EQ(result, "<h1>===\n" - "Title</h1>\n" - "<h2>Text</h2>\n"); -} - -TEST(setext_heading_underline_without_content_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("\n==")}; - EXPECT_EQ(result, "<p>==</p>\n"); -} - -TEST(setext_heading_dashes_after_thematic_break) { - const auto result{sourcemeta::core::markdown_to_html("***\n---")}; - EXPECT_EQ(result, "<hr />\n" - "<hr />\n"); -} - -TEST(setext_heading_dashes_after_list_item) { - const auto result{sourcemeta::core::markdown_to_html("* item\n---")}; - EXPECT_EQ(result, "<ul>\n" - "<li>item</li>\n" - "</ul>\n" - "<hr />\n"); -} - -TEST(setext_heading_equals_after_indented_code) { - const auto result{sourcemeta::core::markdown_to_html(" code\n===")}; - EXPECT_EQ(result, "<pre><code>code\n" - "</code></pre>\n" - "<p>===</p>\n"); -} - -TEST(setext_heading_escaped_content) { - const auto result{sourcemeta::core::markdown_to_html("\\- not a list\n---")}; - EXPECT_EQ(result, "<h2>- not a list</h2>\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, "<p>Intro</p>\n" - "<h1>Title</h1>\n" - "<p>Body</p>\n"); -} - -TEST(setext_heading_spaced_dashes_after_blank_line) { - const auto result{ - sourcemeta::core::markdown_to_html("One\ntwo\n\n- - -\n\nthree")}; - EXPECT_EQ(result, "<p>One\n" - "two</p>\n" - "<hr />\n" - "<p>three</p>\n"); -} - -TEST(setext_heading_underscores_are_thematic_break) { - const auto result{sourcemeta::core::markdown_to_html("One\ntwo\n___\nthree")}; - EXPECT_EQ(result, "<p>One\n" - "two</p>\n" - "<hr />\n" - "<p>three</p>\n"); -} - -TEST(setext_heading_escaped_equals_are_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("One\ntwo\n\\===\nthree")}; - EXPECT_EQ(result, "<p>One\n" - "two\n" - "===\n" - "three</p>\n"); -} - -TEST(indented_code_chunks_separated_by_blank_lines) { - const auto result{sourcemeta::core::markdown_to_html( - " first\n\n second\n \n \n third")}; - EXPECT_EQ(result, "<pre><code>first\n" - "\n" - "second\n" - "\n" - "\n" - "third\n" - "</code></pre>\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, "<pre><code>first\n" - " \n" - " second\n" - "</code></pre>\n"); -} - -TEST(indented_code_cannot_interrupt_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("Text\n code")}; - EXPECT_EQ(result, "<p>Text\n" - "code</p>\n"); -} - -TEST(indented_code_ends_at_unindented_line) { - const auto result{sourcemeta::core::markdown_to_html(" code\ntext")}; - EXPECT_EQ(result, "<pre><code>code\n" - "</code></pre>\n" - "<p>text</p>\n"); -} - -TEST(indented_code_between_headings) { - const auto result{sourcemeta::core::markdown_to_html( - "## Top\n code\nBottom\n===\n code\n***")}; - EXPECT_EQ(result, "<h2>Top</h2>\n" - "<pre><code>code\n" - "</code></pre>\n" - "<h1>Bottom</h1>\n" - "<pre><code>code\n" - "</code></pre>\n" - "<hr />\n"); -} - -TEST(indented_code_first_line_extra_indentation) { - const auto result{ - sourcemeta::core::markdown_to_html(" alpha\n beta")}; - EXPECT_EQ(result, "<pre><code> alpha\n" - "beta\n" - "</code></pre>\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, "<pre><code>code\n" - "</code></pre>\n"); -} - -TEST(indented_code_trailing_whitespace_kept) { - const auto result{sourcemeta::core::markdown_to_html(" code\t ")}; - EXPECT_EQ(result, "<pre><code>code\t \n" - "</code></pre>\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, "<pre><code>**bold**\n" - "[link](/x)\n" - "\n" - "1. item\n" - "</code></pre>\n"); -} - -TEST(fenced_code_closing_fence_longer_than_opening) { - const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n~~~~~")}; - EXPECT_EQ(result, "<pre><code>code\n" - "</code></pre>\n"); -} - -TEST(fenced_code_tildes_not_closed_by_backticks) { - const auto result{ - sourcemeta::core::markdown_to_html("~~~~\ncode\n````\n~~~~")}; - EXPECT_EQ(result, "<pre><code>code\n" - "````\n" - "</code></pre>\n"); -} - -TEST(fenced_code_backticks_not_closed_by_tildes) { - const auto result{ - sourcemeta::core::markdown_to_html("````\ncode\n~~~~\n````")}; - EXPECT_EQ(result, "<pre><code>code\n" - "~~~~\n" - "</code></pre>\n"); -} - -TEST(fenced_code_unclosed_empty) { - const auto result{sourcemeta::core::markdown_to_html("~~~")}; - EXPECT_EQ(result, "<pre><code></code></pre>\n"); -} - -TEST(fenced_code_unclosed_runs_to_end_of_document) { - const auto result{sourcemeta::core::markdown_to_html("~~~~\n\n~~~\ntext")}; - EXPECT_EQ(result, "<pre><code>\n" - "~~~\n" - "text\n" - "</code></pre>\n"); -} - -TEST(fenced_code_unclosed_ends_with_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("> ~~~\n> code\n\ntext")}; - EXPECT_EQ(result, "<blockquote>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</blockquote>\n" - "<p>text</p>\n"); -} - -TEST(fenced_code_only_blank_lines) { - const auto result{sourcemeta::core::markdown_to_html("~~~\n \n\n~~~")}; - EXPECT_EQ(result, "<pre><code> \n" - "\n" - "</code></pre>\n"); -} - -TEST(fenced_code_empty) { - const auto result{sourcemeta::core::markdown_to_html("~~~\n~~~")}; - EXPECT_EQ(result, "<pre><code></code></pre>\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, "<pre><code>one\n" - "two\n" - "three\n" - "</code></pre>\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, "<pre><code> one\n" - " two\n" - "</code></pre>\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, "<pre><code>~~~\n" - "code\n" - "~~~\n" - "</code></pre>\n"); -} - -TEST(fenced_code_closing_fence_indented_differently) { - const auto result{sourcemeta::core::markdown_to_html(" ~~~\ncode\n ~~~")}; - EXPECT_EQ(result, "<pre><code>code\n" - "</code></pre>\n"); -} - -TEST(fenced_code_closing_fence_indented_four_spaces_is_content) { - const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n ~~~")}; - EXPECT_EQ(result, "<pre><code>code\n" - " ~~~\n" - "</code></pre>\n"); -} - -TEST(fenced_code_closing_fence_with_internal_space_is_content) { - const auto result{sourcemeta::core::markdown_to_html("```\ncode\n`` ``")}; - EXPECT_EQ(result, "<pre><code>code\n" - "`` ``\n" - "</code></pre>\n"); -} - -TEST(fenced_code_backtick_info_with_backticks_is_code_span) { - const auto result{sourcemeta::core::markdown_to_html("``` x ```\ncode")}; - EXPECT_EQ(result, "<p><code>x</code>\n" - "code</p>\n"); -} - -TEST(fenced_code_interrupts_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("text\n~~~\ncode\n~~~\nmore")}; - EXPECT_EQ(result, "<p>text</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "<p>more</p>\n"); -} - -TEST(fenced_code_info_string_first_word_is_language) { - const auto result{sourcemeta::core::markdown_to_html( - "~~~ python extra words here\nprint(1)\n~~~")}; - EXPECT_EQ(result, "<pre lang=\"python\"><code>print(1)\n" - "</code></pre>\n"); -} - -TEST(fenced_code_info_string_of_symbols) { - const auto result{sourcemeta::core::markdown_to_html("```#!\n```")}; - EXPECT_EQ(result, "<pre lang=\"#!\"><code></code></pre>\n"); -} - -TEST(fenced_code_backtick_fence_info_cannot_contain_backticks) { - const auto result{sourcemeta::core::markdown_to_html("```js`\ncode")}; - EXPECT_EQ(result, "<p>```js`\n" - "code</p>\n"); -} - -TEST(fenced_code_tilde_fence_info_can_contain_backticks) { - const auto result{ - sourcemeta::core::markdown_to_html("~~~ `js` ~~~\ncode\n~~~")}; - EXPECT_EQ(result, "<pre lang=\"`js`\"><code>code\n" - "</code></pre>\n"); -} - -TEST(fenced_code_closing_fence_cannot_have_info) { - const auto result{ - sourcemeta::core::markdown_to_html("~~~\n~~~ closing\n~~~")}; - EXPECT_EQ(result, "<pre><code>~~~ closing\n" - "</code></pre>\n"); -} - -TEST(fenced_code_info_string_entities_decoded) { - const auto result{ - sourcemeta::core::markdown_to_html("```c&#43;&#43;\ncode\n```")}; - EXPECT_EQ(result, "<pre lang=\"c++\"><code>code\n" - "</code></pre>\n"); -} - -TEST(fenced_code_info_string_backslash_escapes) { - const auto result{sourcemeta::core::markdown_to_html("```c\\#\ncode\n```")}; - EXPECT_EQ(result, "<pre lang=\"c#\"><code>code\n" - "</code></pre>\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, - "<p><a href=\"/manual\" title=\"The manual\">docs</a></p>\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, "<p><a href=\"/manual\" title=\"Manual\">docs</a></p>\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, "<p><a href=\"/manual\" title=\"\n" - "first\n" - "second\n" - "\">docs</a></p>\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, "<p>[docs]: /manual &quot;first</p>\n" - "<p>second&quot;</p>\n" - "<p>[docs]</p>\n"); -} - -TEST(link_reference_definition_destination_on_next_line) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]:\n /manual\n\n[docs]")}; - EXPECT_EQ(result, "<p><a href=\"/manual\">docs</a></p>\n"); -} - -TEST(link_reference_definition_missing_destination) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]:\n\ntext [docs]")}; - EXPECT_EQ(result, "<p>[docs]:</p>\n" - "<p>text [docs]</p>\n"); -} - -TEST(link_reference_definition_empty_angle_destination) { - const auto result{sourcemeta::core::markdown_to_html("[docs]: <>\n\n[docs]")}; - EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); -} - -TEST(link_reference_definition_title_without_separating_space) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]: /manual\"title\"\n\n[docs]")}; - EXPECT_EQ(result, "<p><a href=\"/manual%22title%22\">docs</a></p>\n"); -} - -TEST(link_reference_definition_backslash_escapes) { - const auto result{sourcemeta::core::markdown_to_html( - "[docs]: /a\\b\\#c 'it\\'s'\n\n[docs]")}; - EXPECT_EQ(result, "<p><a href=\"/a%5Cb#c\" title=\"it's\">docs</a></p>\n"); -} - -TEST(link_reference_definition_after_its_use) { - const auto result{ - sourcemeta::core::markdown_to_html("See [docs].\n\n[docs]: /manual")}; - EXPECT_EQ(result, "<p>See <a href=\"/manual\">docs</a>.</p>\n"); -} - -TEST(link_reference_definition_first_one_wins) { - const auto result{sourcemeta::core::markdown_to_html( - "[docs]\n\n[docs]: /one\n\n[docs]: /two")}; - EXPECT_EQ(result, "<p><a href=\"/one\">docs</a></p>\n"); -} - -TEST(link_reference_definition_label_case_insensitive) { - const auto result{ - sourcemeta::core::markdown_to_html("[DoCs]: /manual\n\n[dOcS]")}; - EXPECT_EQ(result, "<p><a href=\"/manual\">dOcS</a></p>\n"); -} - -TEST(link_reference_definition_label_full_case_folding) { - const auto result{ - sourcemeta::core::markdown_to_html("[STRASSE]: /road\n\n[straße]")}; - EXPECT_EQ(result, "<p><a href=\"/road\">stra\xc3\x9f" - "e</a></p>\n"); -} - -TEST(link_reference_definition_alone_renders_nothing) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual'")}; - EXPECT_EQ(result, ""); -} - -TEST(link_reference_definition_with_trailing_text_is_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual' trailing")}; - EXPECT_EQ(result, "<p>[docs]: /manual 'Manual' trailing</p>\n"); -} - -TEST(link_reference_definition_title_with_trailing_text_on_next_line) { - const auto result{sourcemeta::core::markdown_to_html( - "[docs]: /manual\n'Manual' trailing\n\n[docs]")}; - EXPECT_EQ(result, "<p>'Manual' trailing</p>\n" - "<p><a href=\"/manual\" title=\"Manual\">docs</a></p>\n"); -} - -TEST(link_reference_definition_indented_four_spaces_is_code) { - const auto result{ - sourcemeta::core::markdown_to_html(" [docs]: /manual\n\n[docs]")}; - EXPECT_EQ(result, "<pre><code>[docs]: /manual\n" - "</code></pre>\n" - "<p>[docs]</p>\n"); -} - -TEST(link_reference_definition_inside_fenced_code_is_content) { - const auto result{sourcemeta::core::markdown_to_html( - "~~~\n[docs]: /manual\n~~~\n\n[docs]")}; - EXPECT_EQ(result, "<pre><code>[docs]: /manual\n" - "</code></pre>\n" - "<p>[docs]</p>\n"); -} - -TEST(link_reference_definition_cannot_interrupt_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("Intro\n[docs]: /manual\n\n[docs]")}; - EXPECT_EQ(result, "<p>Intro\n" - "[docs]: /manual</p>\n" - "<p>[docs]</p>\n"); -} - -TEST(link_reference_definition_between_heading_and_blockquote) { - const auto result{sourcemeta::core::markdown_to_html( - "## [Docs]\n[docs]: /manual\n> quoted")}; - EXPECT_EQ(result, "<h2><a href=\"/manual\">Docs</a></h2>\n" - "<blockquote>\n" - "<p>quoted</p>\n" - "</blockquote>\n"); -} - -TEST(link_reference_definition_followed_by_dashes) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs]: /manual\n---\n[docs]")}; - EXPECT_EQ(result, "<p>---\n" - "<a href=\"/manual\">docs</a></p>\n"); -} - -TEST(link_reference_definitions_consecutive) { - const auto result{sourcemeta::core::markdown_to_html( - "[one]: /1 'One'\n[two]: /2\n 'Two'\n[three]: </3>\n\n[one] [two] " - "[three]")}; - EXPECT_EQ(result, "<p><a href=\"/1\" title=\"One\">one</a> <a href=\"/2\" " - "title=\"Two\">two</a> <a href=\"/3\">three</a></p>\n"); -} - -TEST(link_reference_definition_inside_blockquote_is_global) { - const auto result{ - sourcemeta::core::markdown_to_html("> [docs]: /manual\n\n[docs]")}; - EXPECT_EQ(result, "<blockquote>\n" - "</blockquote>\n" - "<p><a href=\"/manual\">docs</a></p>\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, "<p><a href=\"/manual\">The long label</a></p>\n"); -} - -TEST(link_reference_definition_empty_label_is_invalid) { - const auto result{ - sourcemeta::core::markdown_to_html("[]: /manual\n\n[] text")}; - EXPECT_EQ(result, "<p>[]: /manual</p>\n" - "<p>[] text</p>\n"); -} - -TEST(link_reference_definition_whitespace_label_is_invalid) { - const auto result{ - sourcemeta::core::markdown_to_html("[ ]: /manual\n\n[ ]")}; - EXPECT_EQ(result, "<p>[ ]: /manual</p>\n" - "<p>[ ]</p>\n"); -} - -TEST(paragraph_leading_spaces_stripped) { - const auto result{sourcemeta::core::markdown_to_html(" one\n two")}; - EXPECT_EQ(result, "<p>one\n" - "two</p>\n"); -} - -TEST(paragraph_continuation_leading_spaces_stripped) { - const auto result{sourcemeta::core::markdown_to_html( - "one\n two\n three")}; - EXPECT_EQ(result, "<p>one\n" - "two\n" - "three</p>\n"); -} - -TEST(paragraph_first_line_indented_three_spaces) { - const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; - EXPECT_EQ(result, "<p>first\n" - "second</p>\n"); -} - -TEST(paragraph_first_line_indented_five_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; - EXPECT_EQ(result, "<pre><code> first\n" - "</code></pre>\n" - "<p>second</p>\n"); -} - -TEST(paragraph_final_trailing_spaces_stripped) { - const auto result{sourcemeta::core::markdown_to_html("one \ntwo ")}; - EXPECT_EQ(result, "<p>one<br />\n" - "two</p>\n"); -} - -TEST(blank_lines_at_start_and_end_ignored) { - const auto result{ - sourcemeta::core::markdown_to_html("\n \none\n\n \n## two\n\n\n")}; - EXPECT_EQ(result, "<p>one</p>\n" - "<h2>two</h2>\n"); -} - -TEST(blockquote_marker_without_following_space) { - const auto result{ - sourcemeta::core::markdown_to_html(">## Title\n>text\n> more")}; - EXPECT_EQ(result, "<blockquote>\n" - "<h2>Title</h2>\n" - "<p>text\n" - "more</p>\n" - "</blockquote>\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, "<blockquote>\n" - "<p>one\n" - "two\n" - "three</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_marker_indented_four_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" > one\n > two")}; - EXPECT_EQ(result, "<pre><code>&gt; one\n" - "&gt; two\n" - "</code></pre>\n"); -} - -TEST(blockquote_lazy_paragraph_continuation) { - const auto result{sourcemeta::core::markdown_to_html("> one\ntwo")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one\n" - "two</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_lazy_continuation_between_marked_lines) { - const auto result{sourcemeta::core::markdown_to_html("> one\ntwo\n> three")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one\n" - "two\n" - "three</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_laziness_does_not_apply_to_thematic_break) { - const auto result{sourcemeta::core::markdown_to_html("> one\n***")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one</p>\n" - "</blockquote>\n" - "<hr />\n"); -} - -TEST(blockquote_laziness_does_not_apply_to_list) { - const auto result{sourcemeta::core::markdown_to_html("> * one\n* two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "</blockquote>\n" - "<ul>\n" - "<li>two</li>\n" - "</ul>\n"); -} - -TEST(blockquote_laziness_does_not_apply_to_indented_code) { - const auto result{ - sourcemeta::core::markdown_to_html("> code\n more")}; - EXPECT_EQ(result, "<blockquote>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</blockquote>\n" - "<pre><code> more\n" - "</code></pre>\n"); -} - -TEST(blockquote_laziness_does_not_apply_to_fenced_code) { - const auto result{sourcemeta::core::markdown_to_html("> ~~~\ncode\n~~~")}; - EXPECT_EQ(result, "<blockquote>\n" - "<pre><code></code></pre>\n" - "</blockquote>\n" - "<p>code</p>\n" - "<pre><code></code></pre>\n"); -} - -TEST(blockquote_lazy_line_that_looks_like_list_item) { - const auto result{sourcemeta::core::markdown_to_html("> one\n * two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one\n" - "* two</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_empty_indented_marker) { - const auto result{sourcemeta::core::markdown_to_html(" >")}; - EXPECT_EQ(result, "<blockquote>\n" - "</blockquote>\n"); -} - -TEST(blockquote_empty_multiple_lines) { - const auto result{sourcemeta::core::markdown_to_html(">\n>\n> ")}; - EXPECT_EQ(result, "<blockquote>\n" - "</blockquote>\n"); -} - -TEST(blockquote_blank_first_and_last_lines) { - const auto result{sourcemeta::core::markdown_to_html("> \n> text\n>")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>text</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_blank_line_separates_blockquotes) { - const auto result{sourcemeta::core::markdown_to_html("> one\n\n> two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one</p>\n" - "</blockquote>\n" - "<blockquote>\n" - "<p>two</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_empty_marker_line_joins_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html("> one\n> \n> two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one</p>\n" - "<p>two</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_interrupts_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("text\n>quote")}; - EXPECT_EQ(result, "<p>text</p>\n" - "<blockquote>\n" - "<p>quote</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_ended_by_thematic_break) { - const auto result{sourcemeta::core::markdown_to_html("> one\n- - -\n> two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one</p>\n" - "</blockquote>\n" - "<hr />\n" - "<blockquote>\n" - "<p>two</p>\n" - "</blockquote>\n"); -} - -TEST(blockquote_lazy_line_after_blank_marker_line) { - const auto result{sourcemeta::core::markdown_to_html("> one\n>\ntwo")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>one</p>\n" - "</blockquote>\n" - "<p>two</p>\n"); -} - -TEST(blockquote_nested_lazy_continuation) { - const auto result{sourcemeta::core::markdown_to_html(">> one\ntwo")}; - EXPECT_EQ(result, "<blockquote>\n" - "<blockquote>\n" - "<p>one\n" - "two</p>\n" - "</blockquote>\n" - "</blockquote>\n"); -} - -TEST(blockquote_nested_markers_without_spaces) { - const auto result{ - sourcemeta::core::markdown_to_html(">>one\n>two\n>>>three")}; - EXPECT_EQ(result, "<blockquote>\n" - "<blockquote>\n" - "<p>one\n" - "two</p>\n" - "<blockquote>\n" - "<p>three</p>\n" - "</blockquote>\n" - "</blockquote>\n" - "</blockquote>\n"); -} - -TEST(blockquote_indented_code_needs_five_spaces) { - const auto result{ - sourcemeta::core::markdown_to_html("> code\n\n> text")}; - EXPECT_EQ(result, "<blockquote>\n" - "<pre><code> code\n" - "</code></pre>\n" - "</blockquote>\n" - "<blockquote>\n" - "<p>text</p>\n" - "</blockquote>\n"); -} - -TEST(list_item_content_needs_marker_width_indentation) { - const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "<p>two</p>\n"); -} - -TEST(list_item_content_with_marker_width_indentation) { - const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>one</p>\n" - "<p>two</p>\n" - "</li>\n" - "</ul>\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, "<ol>\n" - "<li>one</li>\n" - "</ol>\n" - "<pre><code>two\n" - "</code></pre>\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, "<ol>\n" - "<li>\n" - "<p>one</p>\n" - "<p>two</p>\n" - "</li>\n" - "</ol>\n"); -} - -TEST(list_item_inside_nested_blockquotes) { - const auto result{ - sourcemeta::core::markdown_to_html("> > - one\n> >\n> > two")}; - EXPECT_EQ(result, "<blockquote>\n" - "<blockquote>\n" - "<ul>\n" - "<li>\n" - "<p>one</p>\n" - "<p>two</p>\n" - "</li>\n" - "</ul>\n" - "</blockquote>\n" - "</blockquote>\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, "<blockquote>\n" - "<blockquote>\n" - "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "<p>two</p>\n" - "</blockquote>\n" - "</blockquote>\n"); -} - -TEST(list_item_marker_needs_following_space) { - const auto result{sourcemeta::core::markdown_to_html("*one\n\n1)two")}; - EXPECT_EQ(result, "<p>*one</p>\n" - "<p>1)two</p>\n"); -} - -TEST(list_item_multiple_blank_lines_between_blocks) { - const auto result{sourcemeta::core::markdown_to_html("* one\n\n\n\n two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>one</p>\n" - "<p>two</p>\n" - "</li>\n" - "</ul>\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, "<ol start=\"2\">\n" - "<li>\n" - "<p>one</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "<p>two</p>\n" - "<blockquote>\n" - "<p>quote</p>\n" - "</blockquote>\n" - "</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<p>one</p>\n" - "<pre><code>code\n" - "\n" - "\n" - "more\n" - "</code></pre>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(ordered_list_nine_digit_number) { - const auto result{sourcemeta::core::markdown_to_html("999999999) max")}; - EXPECT_EQ(result, "<ol start=\"999999999\">\n" - "<li>max</li>\n" - "</ol>\n"); -} - -TEST(ordered_list_ten_digit_number_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("9999999999) over")}; - EXPECT_EQ(result, "<p>9999999999) over</p>\n"); -} - -TEST(ordered_list_starting_at_zero) { - const auto result{sourcemeta::core::markdown_to_html("0) zero")}; - EXPECT_EQ(result, "<ol start=\"0\">\n" - "<li>zero</li>\n" - "</ol>\n"); -} - -TEST(ordered_list_number_with_leading_zeros) { - const auto result{sourcemeta::core::markdown_to_html("0007. seven")}; - EXPECT_EQ(result, "<ol start=\"7\">\n" - "<li>seven</li>\n" - "</ol>\n"); -} - -TEST(ordered_list_negative_number_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("-5) negative")}; - EXPECT_EQ(result, "<p>-5) negative</p>\n"); -} - -TEST(list_item_indented_code_after_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("* text\n\n code")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>text</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</li>\n" - "</ul>\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, "<ol start=\"100\">\n" - "<li>\n" - "<p>text</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<pre><code>code\n" - "</code></pre>\n" - "<p>text</p>\n" - "<pre><code>more\n" - "</code></pre>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>\n" - "<pre><code> code\n" - "</code></pre>\n" - "<p>text</p>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>one</li>\n" - "<li>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</li>\n" - "</ul>\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, "<ol>\n" - "<li>one</li>\n" - "</ol>\n"); -} - -TEST(list_item_at_most_one_starting_blank_line) { - const auto result{sourcemeta::core::markdown_to_html("*\n\n one")}; - EXPECT_EQ(result, "<ul>\n" - "<li></li>\n" - "</ul>\n" - "<p>one</p>\n"); -} - -TEST(list_item_empty_between_items) { - const auto result{sourcemeta::core::markdown_to_html("* one\n*\n* two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "<li></li>\n" - "<li>two</li>\n" - "</ul>\n"); -} - -TEST(list_item_empty_with_trailing_spaces) { - const auto result{sourcemeta::core::markdown_to_html("* one\n* \n* two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "<li></li>\n" - "<li>two</li>\n" - "</ul>\n"); -} - -TEST(ordered_list_item_empty_between_items) { - const auto result{sourcemeta::core::markdown_to_html("1) one\n2)\n3) two")}; - EXPECT_EQ(result, "<ol>\n" - "<li>one</li>\n" - "<li></li>\n" - "<li>two</li>\n" - "</ol>\n"); -} - -TEST(list_consisting_of_single_empty_item) { - const auto result{sourcemeta::core::markdown_to_html("-")}; - EXPECT_EQ(result, "<ul>\n" - "<li></li>\n" - "</ul>\n"); -} - -TEST(list_item_empty_cannot_interrupt_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("text\n-\n\ntext\n1)")}; - EXPECT_EQ(result, "<h2>text</h2>\n" - "<p>text\n" - "1)</p>\n"); -} - -TEST(list_item_indented_one_space) { - const auto result{sourcemeta::core::markdown_to_html( - " 1. one\n two\n\n code")}; - EXPECT_EQ(result, "<ol>\n" - "<li>\n" - "<p>one\n" - "two</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</li>\n" - "</ol>\n"); -} - -TEST(list_item_indented_four_spaces_is_code) { - const auto result{sourcemeta::core::markdown_to_html(" - one\n two")}; - EXPECT_EQ(result, "<pre><code>- one\n" - " two\n" - "</code></pre>\n"); -} - -TEST(list_item_lazy_continuation_lines) { - const auto result{ - sourcemeta::core::markdown_to_html(" * one\ntwo\n\n code")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>one\n" - "two</p>\n" - "<pre><code>code\n" - "</code></pre>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(list_item_partially_lazy_continuation) { - const auto result{sourcemeta::core::markdown_to_html(" * one\n two\nthree")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one\n" - "two\n" - "three</li>\n" - "</ul>\n"); -} - -TEST(list_item_laziness_inside_nested_containers) { - const auto result{ - sourcemeta::core::markdown_to_html("> * > quoted\nlazy line")}; - EXPECT_EQ(result, "<blockquote>\n" - "<ul>\n" - "<li>\n" - "<blockquote>\n" - "<p>quoted\n" - "lazy line</p>\n" - "</blockquote>\n" - "</li>\n" - "</ul>\n" - "</blockquote>\n"); -} - -TEST(list_nested_with_sufficient_indentation) { - const auto result{sourcemeta::core::markdown_to_html("* a\n * b\n * c")}; - EXPECT_EQ(result, "<ul>\n" - "<li>a\n" - "<ul>\n" - "<li>b\n" - "<ul>\n" - "<li>c</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>a</li>\n" - "<li>b</li>\n" - "<li>c</li>\n" - "</ul>\n"); -} - -TEST(ordered_list_nested_needs_marker_width_indentation) { - const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")}; - EXPECT_EQ(result, "<ol start=\"100\">\n" - "<li>a\n" - "<ul>\n" - "<li>b</li>\n" - "</ul>\n" - "</li>\n" - "</ol>\n"); -} - -TEST(ordered_list_not_nested_with_insufficient_indentation) { - const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")}; - EXPECT_EQ(result, "<ol start=\"100\">\n" - "<li>a\n" - "* b</li>\n" - "</ol>\n"); -} - -TEST(list_item_containing_only_list) { - const auto result{sourcemeta::core::markdown_to_html("+ + one")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(list_item_containing_nested_ordered_list) { - const auto result{sourcemeta::core::markdown_to_html("3) * 4) deep")}; - EXPECT_EQ(result, "<ol start=\"3\">\n" - "<li>\n" - "<ul>\n" - "<li>\n" - "<ol start=\"4\">\n" - "<li>deep</li>\n" - "</ol>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "</ol>\n"); -} - -TEST(list_item_containing_headings) { - const auto result{ - sourcemeta::core::markdown_to_html("* ## Title\n* Text\n ===\n more")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<h2>Title</h2>\n" - "</li>\n" - "<li>\n" - "<h1>Text</h1>\n" - "more</li>\n" - "</ul>\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, "<ul>\n" - "<li>one</li>\n" - "<li>two</li>\n" - "</ul>\n" - "<ul>\n" - "<li>three</li>\n" - "</ul>\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, "<ol>\n" - "<li>one</li>\n" - "<li>two</li>\n" - "</ol>\n" - "<ol start=\"3\">\n" - "<li>three</li>\n" - "</ol>\n"); -} - -TEST(list_interrupts_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("Text\n* one\n* two")}; - EXPECT_EQ(result, "<p>Text</p>\n" - "<ul>\n" - "<li>one</li>\n" - "<li>two</li>\n" - "</ul>\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, "<p>We counted\n" - "42. items in total</p>\n"); -} - -TEST(ordered_list_starting_at_one_interrupts_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("We counted\n1. items in total")}; - EXPECT_EQ(result, "<p>We counted</p>\n" - "<ol>\n" - "<li>items in total</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<p>one</p>\n" - "</li>\n" - "<li>\n" - "<p>two</p>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>a\n" - "<ul>\n" - "<li>\n" - "<p>b</p>\n" - "<p>c</p>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(list_separated_by_html_comment) { - const auto result{ - sourcemeta::core::markdown_to_html("* one\n\n<!-- split -->\n\n* two")}; - EXPECT_EQ(result, "<ul>\n" - "<li>one</li>\n" - "</ul>\n" - "<!-- raw HTML omitted -->\n" - "<ul>\n" - "<li>two</li>\n" - "</ul>\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, "<ul>\n" - "<li>a</li>\n" - "<li>b</li>\n" - "<li>c</li>\n" - "<li>d</li>\n" - "<li>e</li>\n" - "</ul>\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, "<ul>\n" - "<li>a</li>\n" - "<li>b</li>\n" - "<li>c\n" - "<ul>\n" - "<li>d</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\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, "<ol>\n" - "<li>\n" - "<p>a</p>\n" - "</li>\n" - "<li>\n" - "<p>b</p>\n" - "<ol start=\"3\">\n" - "<li>c</li>\n" - "</ol>\n" - "</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<p>a</p>\n" - "</li>\n" - "<li>\n" - "<p>b</p>\n" - "</li>\n" - "<li>\n" - "<p>c</p>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(list_loose_with_empty_item) { - const auto result{sourcemeta::core::markdown_to_html("- a\n-\n\n- b")}; - EXPECT_EQ(result, "<ul>\n" - "<li>\n" - "<p>a</p>\n" - "</li>\n" - "<li></li>\n" - "<li>\n" - "<p>b</p>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>\n" - "<p>a</p>\n" - "<p>b</p>\n" - "</li>\n" - "<li>\n" - "<p>c</p>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>\n" - "<p>a</p>\n" - "</li>\n" - "<li>\n" - "<p>b</p>\n" - "</li>\n" - "</ul>\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, "<ul>\n" - "<li>a</li>\n" - "<li>\n" - "<pre><code>x\n" - "\n" - "</code></pre>\n" - "</li>\n" - "<li>b</li>\n" - "</ul>\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, "<ul>\n" - "<li>a\n" - "<ul>\n" - "<li>\n" - "<p>b</p>\n" - "</li>\n" - "<li>\n" - "<p>c</p>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "<li>d</li>\n" - "</ul>\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, "<ul>\n" - "<li>a\n" - "<blockquote>\n" - "<p>b</p>\n" - "<p>c</p>\n" - "</blockquote>\n" - "</li>\n" - "<li>d</li>\n" - "</ul>\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, "<ul>\n" - "<li>a\n" - "<blockquote>\n" - "<p>b</p>\n" - "</blockquote>\n" - "<pre><code>c\n" - "</code></pre>\n" - "</li>\n" - "<li>d</li>\n" - "</ul>\n"); -} - -TEST(list_single_tight_nested_ordered_item) { - const auto result{sourcemeta::core::markdown_to_html("1. a\n 1. b")}; - EXPECT_EQ(result, "<ol>\n" - "<li>a\n" - "<ol>\n" - "<li>b</li>\n" - "</ol>\n" - "</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<pre><code>x\n" - "</code></pre>\n" - "<p>y</p>\n" - "</li>\n" - "</ul>\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, "<ol>\n" - "<li>\n" - "<p>a</p>\n" - "<ul>\n" - "<li>b</li>\n" - "</ul>\n" - "<p>c</p>\n" - "</li>\n" - "</ol>\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, "<ul>\n" - "<li>\n" - "<p>a</p>\n" - "<ol>\n" - "<li>b</li>\n" - "<li>c</li>\n" - "</ol>\n" - "</li>\n" - "<li>\n" - "<p>d</p>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(crlf_line_endings) { - const auto result{ - sourcemeta::core::markdown_to_html("## Title\r\ntext\r\n\r\nmore")}; - EXPECT_EQ(result, "<h2>Title</h2>\n" - "<p>text</p>\n" - "<p>more</p>\n"); -} - -TEST(cr_line_endings) { - const auto result{ - sourcemeta::core::markdown_to_html("## Title\rtext\r\rmore")}; - EXPECT_EQ(result, "<h2>Title</h2>\n" - "<p>text</p>\n" - "<p>more</p>\n"); -} - -TEST(mixed_line_endings) { - const auto result{ - sourcemeta::core::markdown_to_html("one\rtwo\r\nthree\nfour")}; - EXPECT_EQ(result, "<p>one\n" - "two\n" - "three\n" - "four</p>\n"); -} - -TEST(byte_order_mark_before_list) { - const auto result{sourcemeta::core::markdown_to_html("\xEF\xBB\xBF" - "* item")}; - EXPECT_EQ(result, "<ul>\n" - "<li>item</li>\n" - "</ul>\n"); -} diff --git a/test/markdown/markdown_characters_test.cc b/test/markdown/markdown_characters_test.cc new file mode 100644 index 0000000000..45f9059805 --- /dev/null +++ b/test/markdown/markdown_characters_test.cc @@ -0,0 +1,487 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +#include <string_view> // std::string_view + +using namespace std::literals::string_view_literals; + +TEST(ampersand_in_text) { + const auto result{sourcemeta::core::markdown_to_html("AT&T")}; + EXPECT_EQ(result, "<p>AT&amp;T</p>\n"); +} + +TEST(angle_brackets_in_text) { + const auto result{sourcemeta::core::markdown_to_html("1 < 2 and 3 > 2")}; + EXPECT_EQ(result, "<p>1 &lt; 2 and 3 &gt; 2</p>\n"); +} + +TEST(unicode_content) { + const auto result{ + sourcemeta::core::markdown_to_html("Hello \xC3\xA9\xC3\xA0\xC3\xBC")}; + EXPECT_EQ(result, "<p>Hello \xC3\xA9\xC3\xA0\xC3\xBC</p>\n"); +} + +TEST(cjk_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("\xE4\xBD\xA0\xE5\xA5\xBD")}; + EXPECT_EQ(result, "<p>\xE4\xBD\xA0\xE5\xA5\xBD</p>\n"); +} + +TEST(emoji) { + const auto result{sourcemeta::core::markdown_to_html("\xF0\x9F\x98\x80")}; + EXPECT_EQ(result, "<p>\xF0\x9F\x98\x80</p>\n"); +} + +TEST(invalid_utf8_is_replaced) { + const auto result{sourcemeta::core::markdown_to_html("hello \xFF world")}; + EXPECT_EQ(result, "<p>hello \xEF\xBF\xBD world</p>\n"); +} + +TEST(tab_indented_code_block) { + const auto result{sourcemeta::core::markdown_to_html("\tlime\tpear\t\tplum")}; + EXPECT_EQ(result, "<pre><code>lime\tpear\t\tplum\n" + "</code></pre>\n"); +} + +TEST(tab_after_space_indented_code_block) { + const auto result{sourcemeta::core::markdown_to_html(" \tlime\tpear")}; + EXPECT_EQ(result, "<pre><code>lime\tpear\n" + "</code></pre>\n"); +} + +TEST(tab_list_item_continuation) { + const auto result{sourcemeta::core::markdown_to_html(" - kiwi\n\n\tmango")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>kiwi</p>\n" + "<p>mango</p>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(tab_list_item_indented_code) { + const auto result{sourcemeta::core::markdown_to_html("* kiwi\n\n\t\tmango")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>kiwi</p>\n" + "<pre><code> mango\n" + "</code></pre>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(tab_after_blockquote_marker) { + const auto result{sourcemeta::core::markdown_to_html(">\t\tcedar\n>\t\toak")}; + EXPECT_EQ(result, "<blockquote>\n" + "<pre><code> cedar\n" + " oak\n" + "</code></pre>\n" + "</blockquote>\n"); +} + +TEST(tab_after_list_marker) { + const auto result{sourcemeta::core::markdown_to_html("+\t\tcedar")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<pre><code> cedar\n" + "</code></pre>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(tab_after_heading_marker) { + const auto result{sourcemeta::core::markdown_to_html("##\tTitle")}; + EXPECT_EQ(result, "<h2>Title</h2>\n"); +} + +TEST(tab_between_thematic_break_characters) { + const auto result{sourcemeta::core::markdown_to_html("-\t-\t-")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(crlf_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("## Title\r\ntext\r\n\r\nmore")}; + EXPECT_EQ(result, "<h2>Title</h2>\n" + "<p>text</p>\n" + "<p>more</p>\n"); +} + +TEST(cr_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("## Title\rtext\r\rmore")}; + EXPECT_EQ(result, "<h2>Title</h2>\n" + "<p>text</p>\n" + "<p>more</p>\n"); +} + +TEST(mixed_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("one\rtwo\r\nthree\nfour")}; + EXPECT_EQ(result, "<p>one\n" + "two\n" + "three\n" + "four</p>\n"); +} + +TEST(byte_order_mark_before_list) { + const auto result{sourcemeta::core::markdown_to_html("\xEF\xBB\xBF" + "* item")}; + EXPECT_EQ(result, "<ul>\n" + "<li>item</li>\n" + "</ul>\n"); +} + +TEST(text_with_special_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("cost: $5 + 10% ; done?")}; + EXPECT_EQ(result, "<p>cost: $5 + 10% ; done?</p>\n"); +} + +TEST(text_with_greek_characters) { + const auto result{sourcemeta::core::markdown_to_html("λέξη και φράση")}; + EXPECT_EQ(result, + "<p>\xce\xbb\xce\xad\xce\xbe\xce\xb7 \xce\xba\xce\xb1\xce\xb9 " + "\xcf\x86\xcf\x81\xce\xac\xcf\x83\xce\xb7</p>\n"); +} + +TEST(text_internal_spaces_preserved) { + const auto result{sourcemeta::core::markdown_to_html("wide gap")}; + EXPECT_EQ(result, "<p>wide gap</p>\n"); +} + +TEST(invalid_utf8_truncated_two_byte_sequence_at_end) { + const auto result{sourcemeta::core::markdown_to_html("caf\xC3")}; + EXPECT_EQ(result, "<p>caf\xef\xbf\xbd</p>\n"); +} + +TEST(invalid_utf8_truncated_three_byte_sequence_mid_text) { + const auto result{sourcemeta::core::markdown_to_html("x\xE2\x82" + "b")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbd" + "b</p>\n"); +} + +TEST(invalid_utf8_truncated_four_byte_sequence_before_newline) { + const auto result{sourcemeta::core::markdown_to_html("x\xF0\x9F\x98\ny")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbd\n" + "y</p>\n"); +} + +TEST(invalid_utf8_stray_continuation_byte) { + const auto result{sourcemeta::core::markdown_to_html("x\x80y")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbdy</p>\n"); +} + +TEST(invalid_utf8_consecutive_continuation_bytes) { + const auto result{sourcemeta::core::markdown_to_html("\x80\x81\xBF")}; + EXPECT_EQ(result, "<p>\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd</p>\n"); +} + +TEST(invalid_utf8_overlong_encoded_slash) { + const auto result{sourcemeta::core::markdown_to_html("path\xC0\xAF" + "etc")}; + EXPECT_EQ(result, "<p>path\xef\xbf\xbd" + "etc</p>\n"); +} + +TEST(invalid_utf8_overlong_encoded_nul) { + const auto result{sourcemeta::core::markdown_to_html("x\xE0\x80\x80y")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbdy</p>\n"); +} + +TEST(invalid_utf8_encoded_surrogate) { + const auto result{sourcemeta::core::markdown_to_html("x\xED\xA0\x80y")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbdy</p>\n"); +} + +TEST(invalid_utf8_code_point_above_unicode_range) { + const auto result{sourcemeta::core::markdown_to_html("x\xF4\x90\x80\x80y")}; + EXPECT_EQ(result, "<p>x\xef\xbf\xbdy</p>\n"); +} + +TEST(invalid_utf8_five_byte_sequence) { + const auto result{ + sourcemeta::core::markdown_to_html("x\xF8\x88\x80\x80\x80y")}; + EXPECT_EQ(result, + "<p>x\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbdy<" + "/p>\n"); +} + +TEST(invalid_utf8_lead_byte_followed_by_ascii) { + const auto result{sourcemeta::core::markdown_to_html("\xE4" + "abc")}; + EXPECT_EQ(result, "<p>\xef\xbf\xbd" + "abc</p>\n"); +} + +TEST(invalid_utf8_bytes_fe_and_ff) { + const auto result{sourcemeta::core::markdown_to_html("\xFE\xFF text")}; + EXPECT_EQ(result, "<p>\xef\xbf\xbd\xef\xbf\xbd text</p>\n"); +} + +TEST(invalid_utf8_in_heading) { + const auto result{sourcemeta::core::markdown_to_html("# t\xFFitle")}; + EXPECT_EQ(result, "<h1>t\xef\xbf\xbditle</h1>\n"); +} + +TEST(invalid_utf8_in_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`a\xFF" + "b`")}; + EXPECT_EQ(result, "<p><code>a\xef\xbf\xbd" + "b</code></p>\n"); +} + +TEST(invalid_utf8_in_fenced_code) { + const auto result{sourcemeta::core::markdown_to_html("```\n\xC3\n```")}; + EXPECT_EQ(result, "<pre><code>\xef\xbf\xbd\n" + "</code></pre>\n"); +} + +TEST(invalid_utf8_in_fenced_code_info_string) { + const auto result{sourcemeta::core::markdown_to_html("```l\xFF" + "ang\ncode\n```")}; + EXPECT_EQ(result, "<pre lang=\"l\xef\xbf\xbd" + "ang\"><code>code\n" + "</code></pre>\n"); +} + +TEST(invalid_utf8_in_indented_code) { + const auto result{ + sourcemeta::core::markdown_to_html(" \xE2\x82\n next")}; + EXPECT_EQ(result, "<pre><code>\xef\xbf\xbd\n" + "next\n" + "</code></pre>\n"); +} + +TEST(invalid_utf8_in_link_destination) { + const auto result{sourcemeta::core::markdown_to_html("[x](/a\xFF" + "b)")}; + EXPECT_EQ(result, "<p><a href=\"/a%EF%BF%BDb\">x</a></p>\n"); +} + +TEST(invalid_utf8_in_link_title) { + const auto result{sourcemeta::core::markdown_to_html("[x](/a 't\xC3')")}; + EXPECT_EQ(result, "<p><a href=\"/a\" title=\"t\xef\xbf\xbd\">x</a></p>\n"); +} + +TEST(invalid_utf8_in_reference_label) { + const auto result{ + sourcemeta::core::markdown_to_html("[l\xFF]\n\n[l\xFF]: /x")}; + EXPECT_EQ(result, "<p><a href=\"/x\">l\xef\xbf\xbd</a></p>\n"); +} + +TEST(invalid_utf8_in_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/\xFF>")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/%EF%BF%BD\">https://" + "sourcemeta.com/\xef\xbf\xbd</a></p>\n"); +} + +TEST(invalid_utf8_in_extended_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("www.sourcemeta.com/\xC3path")}; + EXPECT_EQ(result, + "<p><a " + "href=\"http://www.sourcemeta.com/" + "%EF%BF%BDpath\">www.sourcemeta.com/\xef\xbf\xbdpath</a></p>\n"); +} + +TEST(invalid_utf8_in_table_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n| - |\n| \xFF |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>\xef\xbf\xbd</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(invalid_utf8_in_image_description) { + const auto result{sourcemeta::core::markdown_to_html("![\xC3](/i.png)")}; + EXPECT_EQ(result, "<p><img src=\"/i.png\" alt=\"\xef\xbf\xbd\" /></p>\n"); +} + +TEST(invalid_utf8_between_emphasis_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("*\xFF*")}; + EXPECT_EQ(result, "<p><em>\xef\xbf\xbd</em></p>\n"); +} + +TEST(invalid_utf8_in_footnote_label) { + const auto result{ + sourcemeta::core::markdown_to_html("x[^\xFF]\n\n[^\xFF]: note")}; + EXPECT_EQ(result, + "<p>x<sup class=\"footnote-ref\"><a href=\"#fn-%EF%BF%BD\" " + "id=\"fnref-%EF%BF%BD\" data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-%EF%BF%BD\">\n" + "<p>note <a href=\"#fnref-%EF%BF%BD\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(invalid_utf8_inside_entity_like_text) { + const auto result{sourcemeta::core::markdown_to_html("&\xFF" + "amp;")}; + EXPECT_EQ(result, "<p>&amp;\xef\xbf\xbd" + "amp;</p>\n"); +} + +TEST(nul_character_at_start) { + const auto result{sourcemeta::core::markdown_to_html("\0text"sv)}; + EXPECT_EQ(result, "<p>\xef\xbf\xbdtext</p>\n"); +} + +TEST(nul_character_in_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`x\0y`"sv)}; + EXPECT_EQ(result, "<p><code>x\xef\xbf\xbdy</code></p>\n"); +} + +TEST(nul_character_in_fenced_code) { + const auto result{sourcemeta::core::markdown_to_html("```\nx\0y\n```"sv)}; + EXPECT_EQ(result, "<pre><code>x\xef\xbf\xbdy\n" + "</code></pre>\n"); +} + +TEST(nul_character_in_link_destination) { + const auto result{sourcemeta::core::markdown_to_html("[x](/a\0z)"sv)}; + EXPECT_EQ(result, "<p><a href=\"/a%EF%BF%BDz\">x</a></p>\n"); +} + +TEST(nul_character_in_heading) { + const auto result{sourcemeta::core::markdown_to_html("# a\0"sv)}; + EXPECT_EQ(result, "<h1>a\xef\xbf\xbd</h1>\n"); +} + +TEST(nul_character_in_table_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n| - |\n| \0 |"sv)}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>\xef\xbf\xbd</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(start_of_heading_control_character_preserved) { + const auto result{sourcemeta::core::markdown_to_html("x\x01y")}; + EXPECT_EQ(result, "<p>x\x01y</p>\n"); +} + +TEST(delete_character_preserved) { + const auto result{sourcemeta::core::markdown_to_html("x\x7Fy")}; + EXPECT_EQ(result, "<p>x\x7fy</p>\n"); +} + +TEST(form_feed_between_words) { + const auto result{sourcemeta::core::markdown_to_html("a\fz")}; + EXPECT_EQ(result, "<p>a\x0cz</p>\n"); +} + +TEST(vertical_tab_between_words) { + const auto result{sourcemeta::core::markdown_to_html("a\vz")}; + EXPECT_EQ(result, "<p>a\x0bz</p>\n"); +} + +TEST(form_feed_between_thematic_break_characters) { + const auto result{sourcemeta::core::markdown_to_html("-\f-\f-")}; + EXPECT_EQ(result, "<p>-\x0c-\x0c-</p>\n"); +} + +TEST(lone_carriage_return_at_end) { + const auto result{sourcemeta::core::markdown_to_html("text\r")}; + EXPECT_EQ(result, "<p>text</p>\n"); +} + +TEST(carriage_return_before_list_item) { + const auto result{sourcemeta::core::markdown_to_html("a\r* b")}; + EXPECT_EQ(result, "<p>a</p>\n" + "<ul>\n" + "<li>b</li>\n" + "</ul>\n"); +} + +TEST(crlf_after_trailing_spaces_is_hard_break) { + const auto result{sourcemeta::core::markdown_to_html("a \r\nb")}; + EXPECT_EQ(result, "<p>a<br />\n" + "b</p>\n"); +} + +TEST(non_breaking_space_after_list_marker_is_not_list) { + const auto result{sourcemeta::core::markdown_to_html("*\xC2\xA0item")}; + EXPECT_EQ(result, "<p>*\xc2\xa0item</p>\n"); +} + +TEST(em_spaces_do_not_indent_code) { + const auto result{sourcemeta::core::markdown_to_html( + "\xE2\x80\x83\xE2\x80\x83\xE2\x80\x83\xE2\x80\x83" + "code")}; + EXPECT_EQ(result, "<p>\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83" + "code</p>\n"); +} + +TEST(zero_width_joiner_sequence_preserved) { + const auto result{sourcemeta::core::markdown_to_html( + "\xF0\x9F\x91\xA8\xE2\x80\x8D\xF0\x9F\x92\xBB")}; + EXPECT_EQ(result, "<p>\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbb</p>\n"); +} + +TEST(combining_accent_preserved) { + const auto result{sourcemeta::core::markdown_to_html("e\xCC\x81")}; + EXPECT_EQ(result, "<p>e\xcc\x81</p>\n"); +} + +TEST(right_to_left_text_preserved) { + const auto result{ + sourcemeta::core::markdown_to_html("\xD7\xA9\xD7\x9C\xD7\x95\xD7\x9D")}; + EXPECT_EQ(result, "<p>\xd7\xa9\xd7\x9c\xd7\x95\xd7\x9d</p>\n"); +} + +TEST(byte_order_mark_after_first_line) { + const auto result{sourcemeta::core::markdown_to_html("a\n\xEF\xBB\xBF" + "b")}; + EXPECT_EQ(result, "<p>a\n" + "\xef\xbb\xbf" + "b</p>\n"); +} + +TEST(four_byte_character_in_link_destination) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](/\xF0\x9F\x98\x80)")}; + EXPECT_EQ(result, "<p><a href=\"/%F0%9F%98%80\">x</a></p>\n"); +} + +TEST(non_ascii_link_destination_is_percent_encoded) { + const auto result{sourcemeta::core::markdown_to_html("[x](/caf\xC3\xA9)")}; + EXPECT_EQ(result, "<p><a href=\"/caf%C3%A9\">x</a></p>\n"); +} + +TEST(quotes_in_text_are_escaped) { + const auto result{sourcemeta::core::markdown_to_html("say \"hi\" and 'bye'")}; + EXPECT_EQ(result, "<p>say &quot;hi&quot; and 'bye'</p>\n"); +} + +TEST(nul_characters_are_replaced) { + const auto result{sourcemeta::core::markdown_to_html("one\0two\0"sv)}; + EXPECT_EQ(result, "<p>one\xef\xbf\xbdtwo\xef\xbf\xbd</p>\n"); +} diff --git a/test/markdown/markdown_code_blocks_test.cc b/test/markdown/markdown_code_blocks_test.cc new file mode 100644 index 0000000000..b41f7eae28 --- /dev/null +++ b/test/markdown/markdown_code_blocks_test.cc @@ -0,0 +1,284 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(fenced_code_block_backticks) { + const auto result{sourcemeta::core::markdown_to_html("```\ncode here\n```")}; + EXPECT_EQ(result, "<pre><code>code here\n</code></pre>\n"); +} + +TEST(fenced_code_block_tildes) { + const auto result{sourcemeta::core::markdown_to_html("~~~\ncode here\n~~~")}; + EXPECT_EQ(result, "<pre><code>code here\n</code></pre>\n"); +} + +TEST(fenced_code_block_with_language) { + const auto result{ + sourcemeta::core::markdown_to_html("```cpp\nint x = 0;\n```")}; + EXPECT_EQ(result, "<pre lang=\"cpp\"><code>int x = 0;\n</code></pre>\n"); +} + +TEST(fenced_code_block_html_escaped) { + const auto result{ + sourcemeta::core::markdown_to_html("```\n<div>&amp;</div>\n```")}; + EXPECT_EQ(result, + "<pre><code>&lt;div&gt;&amp;amp;&lt;/div&gt;\n</code></pre>\n"); +} + +TEST(indented_code_block) { + const auto result{ + sourcemeta::core::markdown_to_html(" int x = 0;\n return x;")}; + EXPECT_EQ(result, "<pre><code>int x = 0;\nreturn x;\n</code></pre>\n"); +} + +TEST(fenced_code_block_with_empty_lines) { + const auto result{ + sourcemeta::core::markdown_to_html("```\nline one\n\nline three\n```")}; + EXPECT_EQ(result, "<pre><code>line one\n\nline three\n</code></pre>\n"); +} + +TEST(github_pre_lang_format) { + const auto result{ + sourcemeta::core::markdown_to_html("```python\nprint()\n```")}; + EXPECT_EQ(result, "<pre lang=\"python\"><code>print()\n</code></pre>\n"); +} + +TEST(indented_code_chunks_separated_by_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + " first\n\n second\n \n \n third")}; + EXPECT_EQ(result, "<pre><code>first\n" + "\n" + "second\n" + "\n" + "\n" + "third\n" + "</code></pre>\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, "<pre><code>first\n" + " \n" + " second\n" + "</code></pre>\n"); +} + +TEST(indented_code_cannot_interrupt_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Text\n code")}; + EXPECT_EQ(result, "<p>Text\n" + "code</p>\n"); +} + +TEST(indented_code_ends_at_unindented_line) { + const auto result{sourcemeta::core::markdown_to_html(" code\ntext")}; + EXPECT_EQ(result, "<pre><code>code\n" + "</code></pre>\n" + "<p>text</p>\n"); +} + +TEST(indented_code_between_headings) { + const auto result{sourcemeta::core::markdown_to_html( + "## Top\n code\nBottom\n===\n code\n***")}; + EXPECT_EQ(result, "<h2>Top</h2>\n" + "<pre><code>code\n" + "</code></pre>\n" + "<h1>Bottom</h1>\n" + "<pre><code>code\n" + "</code></pre>\n" + "<hr />\n"); +} + +TEST(indented_code_first_line_extra_indentation) { + const auto result{ + sourcemeta::core::markdown_to_html(" alpha\n beta")}; + EXPECT_EQ(result, "<pre><code> alpha\n" + "beta\n" + "</code></pre>\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, "<pre><code>code\n" + "</code></pre>\n"); +} + +TEST(indented_code_trailing_whitespace_kept) { + const auto result{sourcemeta::core::markdown_to_html(" code\t ")}; + EXPECT_EQ(result, "<pre><code>code\t \n" + "</code></pre>\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, "<pre><code>**bold**\n" + "[link](/x)\n" + "\n" + "1. item\n" + "</code></pre>\n"); +} + +TEST(fenced_code_closing_fence_longer_than_opening) { + const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n~~~~~")}; + EXPECT_EQ(result, "<pre><code>code\n" + "</code></pre>\n"); +} + +TEST(fenced_code_tildes_not_closed_by_backticks) { + const auto result{ + sourcemeta::core::markdown_to_html("~~~~\ncode\n````\n~~~~")}; + EXPECT_EQ(result, "<pre><code>code\n" + "````\n" + "</code></pre>\n"); +} + +TEST(fenced_code_backticks_not_closed_by_tildes) { + const auto result{ + sourcemeta::core::markdown_to_html("````\ncode\n~~~~\n````")}; + EXPECT_EQ(result, "<pre><code>code\n" + "~~~~\n" + "</code></pre>\n"); +} + +TEST(fenced_code_unclosed_empty) { + const auto result{sourcemeta::core::markdown_to_html("~~~")}; + EXPECT_EQ(result, "<pre><code></code></pre>\n"); +} + +TEST(fenced_code_unclosed_runs_to_end_of_document) { + const auto result{sourcemeta::core::markdown_to_html("~~~~\n\n~~~\ntext")}; + EXPECT_EQ(result, "<pre><code>\n" + "~~~\n" + "text\n" + "</code></pre>\n"); +} + +TEST(fenced_code_unclosed_ends_with_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> ~~~\n> code\n\ntext")}; + EXPECT_EQ(result, "<blockquote>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</blockquote>\n" + "<p>text</p>\n"); +} + +TEST(fenced_code_only_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~~\n \n\n~~~")}; + EXPECT_EQ(result, "<pre><code> \n" + "\n" + "</code></pre>\n"); +} + +TEST(fenced_code_empty) { + const auto result{sourcemeta::core::markdown_to_html("~~~\n~~~")}; + EXPECT_EQ(result, "<pre><code></code></pre>\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, "<pre><code>one\n" + "two\n" + "three\n" + "</code></pre>\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, "<pre><code> one\n" + " two\n" + "</code></pre>\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, "<pre><code>~~~\n" + "code\n" + "~~~\n" + "</code></pre>\n"); +} + +TEST(fenced_code_closing_fence_indented_differently) { + const auto result{sourcemeta::core::markdown_to_html(" ~~~\ncode\n ~~~")}; + EXPECT_EQ(result, "<pre><code>code\n" + "</code></pre>\n"); +} + +TEST(fenced_code_closing_fence_indented_four_spaces_is_content) { + const auto result{sourcemeta::core::markdown_to_html("~~~\ncode\n ~~~")}; + EXPECT_EQ(result, "<pre><code>code\n" + " ~~~\n" + "</code></pre>\n"); +} + +TEST(fenced_code_closing_fence_with_internal_space_is_content) { + const auto result{sourcemeta::core::markdown_to_html("```\ncode\n`` ``")}; + EXPECT_EQ(result, "<pre><code>code\n" + "`` ``\n" + "</code></pre>\n"); +} + +TEST(fenced_code_backtick_info_with_backticks_is_code_span) { + const auto result{sourcemeta::core::markdown_to_html("``` x ```\ncode")}; + EXPECT_EQ(result, "<p><code>x</code>\n" + "code</p>\n"); +} + +TEST(fenced_code_interrupts_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("text\n~~~\ncode\n~~~\nmore")}; + EXPECT_EQ(result, "<p>text</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "<p>more</p>\n"); +} + +TEST(fenced_code_info_string_first_word_is_language) { + const auto result{sourcemeta::core::markdown_to_html( + "~~~ python extra words here\nprint(1)\n~~~")}; + EXPECT_EQ(result, "<pre lang=\"python\"><code>print(1)\n" + "</code></pre>\n"); +} + +TEST(fenced_code_info_string_of_symbols) { + const auto result{sourcemeta::core::markdown_to_html("```#!\n```")}; + EXPECT_EQ(result, "<pre lang=\"#!\"><code></code></pre>\n"); +} + +TEST(fenced_code_backtick_fence_info_cannot_contain_backticks) { + const auto result{sourcemeta::core::markdown_to_html("```js`\ncode")}; + EXPECT_EQ(result, "<p>```js`\n" + "code</p>\n"); +} + +TEST(fenced_code_tilde_fence_info_can_contain_backticks) { + const auto result{ + sourcemeta::core::markdown_to_html("~~~ `js` ~~~\ncode\n~~~")}; + EXPECT_EQ(result, "<pre lang=\"`js`\"><code>code\n" + "</code></pre>\n"); +} + +TEST(fenced_code_closing_fence_cannot_have_info) { + const auto result{ + sourcemeta::core::markdown_to_html("~~~\n~~~ closing\n~~~")}; + EXPECT_EQ(result, "<pre><code>~~~ closing\n" + "</code></pre>\n"); +} + +TEST(fenced_code_info_string_entities_decoded) { + const auto result{ + sourcemeta::core::markdown_to_html("```c&#43;&#43;\ncode\n```")}; + EXPECT_EQ(result, "<pre lang=\"c++\"><code>code\n" + "</code></pre>\n"); +} + +TEST(fenced_code_info_string_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html("```c\\#\ncode\n```")}; + EXPECT_EQ(result, "<pre lang=\"c#\"><code>code\n" + "</code></pre>\n"); +} diff --git a/test/markdown/markdown_code_spans_test.cc b/test/markdown/markdown_code_spans_test.cc new file mode 100644 index 0000000000..6d1412d6ff --- /dev/null +++ b/test/markdown/markdown_code_spans_test.cc @@ -0,0 +1,97 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(inline_code) { + const auto result{sourcemeta::core::markdown_to_html("Use `printf()`")}; + EXPECT_EQ(result, "<p>Use <code>printf()</code></p>\n"); +} + +TEST(inline_code_with_backticks_inside) { + const auto result{ + sourcemeta::core::markdown_to_html("`` there is a ` here ``")}; + EXPECT_EQ(result, "<p><code>there is a ` here</code></p>\n"); +} + +TEST(inline_code_preserves_html) { + const auto result{ + sourcemeta::core::markdown_to_html("`<div class=\"foo\">`")}; + EXPECT_EQ(result, "<p><code>&lt;div class=&quot;foo&quot;&gt;</code></p>\n"); +} + +TEST(code_span_with_double_backticks) { + const auto result{sourcemeta::core::markdown_to_html("``a`b``")}; + EXPECT_EQ(result, "<p><code>a`b</code></p>\n"); +} + +TEST(code_span_strips_one_surrounding_space) { + const auto result{sourcemeta::core::markdown_to_html("`` `x` ``")}; + EXPECT_EQ(result, "<p><code>`x`</code></p>\n"); +} + +TEST(code_span_strips_only_one_space_per_side) { + const auto result{sourcemeta::core::markdown_to_html("`` x ``")}; + EXPECT_EQ(result, "<p><code> x </code></p>\n"); +} + +TEST(code_span_space_on_one_side_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("`x `")}; + EXPECT_EQ(result, "<p><code>x </code></p>\n"); +} + +TEST(code_span_non_breaking_space_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("`\xC2\xA0" + "x`")}; + EXPECT_EQ(result, "<p><code>\xc2\xa0x</code></p>\n"); +} + +TEST(code_span_of_only_spaces_is_kept) { + const auto result{sourcemeta::core::markdown_to_html("` `")}; + EXPECT_EQ(result, "<p><code> </code></p>\n"); +} + +TEST(code_span_line_endings_become_spaces) { + const auto result{sourcemeta::core::markdown_to_html("`one\ntwo\nthree`")}; + EXPECT_EQ(result, "<p><code>one two three</code></p>\n"); +} + +TEST(code_span_interior_spaces_are_kept) { + const auto result{sourcemeta::core::markdown_to_html("`a b \n c`")}; + EXPECT_EQ(result, "<p><code>a b c</code></p>\n"); +} + +TEST(code_span_backslash_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`a\\`b")}; + EXPECT_EQ(result, "<p><code>a\\</code>b</p>\n"); +} + +TEST(code_span_backtick_run_lengths_must_match) { + const auto result{sourcemeta::core::markdown_to_html("``x```")}; + EXPECT_EQ(result, "<p>``x```</p>\n"); +} + +TEST(code_span_unmatched_opening_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("``x")}; + EXPECT_EQ(result, "<p>``x</p>\n"); +} + +TEST(code_span_longer_closing_run_does_not_close) { + const auto result{sourcemeta::core::markdown_to_html("``x```y``")}; + EXPECT_EQ(result, "<p><code>x```y</code></p>\n"); +} + +TEST(code_span_takes_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x`_`")}; + EXPECT_EQ(result, "<p>_x<code>_</code></p>\n"); +} + +TEST(code_span_takes_precedence_over_link) { + const auto result{sourcemeta::core::markdown_to_html("[text `code](/url`)")}; + EXPECT_EQ(result, "<p>[text <code>code](/url</code>)</p>\n"); +} + +TEST(code_span_takes_precedence_over_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("`<https://sourcemeta.com/`x>`")}; + EXPECT_EQ(result, "<p><code>&lt;https://sourcemeta.com/</code>x&gt;`</p>\n"); +} diff --git a/test/markdown/markdown_emphasis_test.cc b/test/markdown/markdown_emphasis_test.cc new file mode 100644 index 0000000000..d0a7327967 --- /dev/null +++ b/test/markdown/markdown_emphasis_test.cc @@ -0,0 +1,312 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(italic_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("*italic*")}; + EXPECT_EQ(result, "<p><em>italic</em></p>\n"); +} + +TEST(italic_with_underscores) { + const auto result{sourcemeta::core::markdown_to_html("_italic_")}; + EXPECT_EQ(result, "<p><em>italic</em></p>\n"); +} + +TEST(bold_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("**bold**")}; + EXPECT_EQ(result, "<p><strong>bold</strong></p>\n"); +} + +TEST(bold_with_underscores) { + const auto result{sourcemeta::core::markdown_to_html("__bold__")}; + EXPECT_EQ(result, "<p><strong>bold</strong></p>\n"); +} + +TEST(bold_and_italic) { + const auto result{sourcemeta::core::markdown_to_html("***bold italic***")}; + EXPECT_EQ(result, "<p><em><strong>bold italic</strong></em></p>\n"); +} + +TEST(bold_inside_italic) { + const auto result{ + sourcemeta::core::markdown_to_html("*this is **bold** inside italic*")}; + EXPECT_EQ(result, + "<p><em>this is <strong>bold</strong> inside italic</em></p>\n"); +} + +TEST(italic_inside_bold) { + const auto result{ + sourcemeta::core::markdown_to_html("**this is *italic* inside bold**")}; + EXPECT_EQ(result, + "<p><strong>this is <em>italic</em> inside bold</strong></p>\n"); +} + +TEST(emphasis_across_words) { + const auto result{ + sourcemeta::core::markdown_to_html("this is **all bold** here")}; + EXPECT_EQ(result, "<p>this is <strong>all bold</strong> here</p>\n"); +} + +TEST(bold_and_italic_in_list_item) { + const auto result{ + sourcemeta::core::markdown_to_html("- ***bold italic item***\n- normal")}; + EXPECT_EQ(result, + "<ul>\n<li><em><strong>bold italic item</strong></em></li>\n" + "<li>normal</li>\n</ul>\n"); +} + +TEST(emphasis_does_not_cross_code_span) { + const auto result{sourcemeta::core::markdown_to_html("*start `code* end`")}; + EXPECT_EQ(result, "<p>*start <code>code* end</code></p>\n"); +} + +TEST(single_character_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("*a*")}; + EXPECT_EQ(result, "<p><em>a</em></p>\n"); +} + +TEST(emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x _ y z_")}; + EXPECT_EQ(result, "<p>x _ y z_</p>\n"); +} + +TEST(emphasis_opening_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x*(y)*")}; + EXPECT_EQ(result, "<p>x*(y)*</p>\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, "<p>_\xc2\xa0x\xc2\xa0_</p>\n"); +} + +TEST(emphasis_intraword_with_asterisks) { + const auto result{ + sourcemeta::core::markdown_to_html("un*frigging*believable")}; + EXPECT_EQ(result, "<p>un<em>frigging</em>believable</p>\n"); +} + +TEST(emphasis_intraword_with_underscores_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("snake_case_name")}; + EXPECT_EQ(result, "<p>snake_case_name</p>\n"); +} + +TEST(emphasis_intraword_with_underscores_in_cyrillic) { + const auto result{sourcemeta::core::markdown_to_html("река_течёт_быстро")}; + EXPECT_EQ(result, "<p>\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</p>\n"); +} + +TEST(emphasis_between_digits) { + const auto result{sourcemeta::core::markdown_to_html("1*2*3\n\n1_2_3")}; + EXPECT_EQ(result, "<p>1<em>2</em>3</p>\n" + "<p>1_2_3</p>\n"); +} + +TEST(emphasis_underscore_between_letters_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x_(y)_z")}; + EXPECT_EQ(result, "<p>x_(y)_z</p>\n"); +} + +TEST(emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x._[y]_")}; + EXPECT_EQ(result, "<p>x.<em>[y]</em></p>\n"); +} + +TEST(emphasis_mismatched_delimiter_characters) { + const auto result{sourcemeta::core::markdown_to_html("*x_")}; + EXPECT_EQ(result, "<p>*x_</p>\n"); +} + +TEST(emphasis_closing_preceded_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("_x y _")}; + EXPECT_EQ(result, "<p>_x y _</p>\n"); +} + +TEST(emphasis_closing_on_new_line) { + const auto result{sourcemeta::core::markdown_to_html("_x y\n_")}; + EXPECT_EQ(result, "<p>_x y\n" + "_</p>\n"); +} + +TEST(emphasis_closing_not_right_flanking) { + const auto result{sourcemeta::core::markdown_to_html("_[_x]")}; + EXPECT_EQ(result, "<p>_[_x]</p>\n"); +} + +TEST(emphasis_nested_with_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[_x_]_")}; + EXPECT_EQ(result, "<p><em>[<em>x</em>]</em></p>\n"); +} + +TEST(emphasis_closing_followed_by_letter) { + const auto result{sourcemeta::core::markdown_to_html("*x*y")}; + EXPECT_EQ(result, "<p><em>x</em>y</p>\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, "<p>_x_y</p>\n" + "<p><em>x_y_z</em></p>\n"); +} + +TEST(emphasis_underscore_followed_by_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[x]_!")}; + EXPECT_EQ(result, "<p><em>[x]</em>!</p>\n"); +} + +TEST(strong_emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("__ x y__")}; + EXPECT_EQ(result, "<p>__ x y__</p>\n"); +} + +TEST(strong_emphasis_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x__(y)__")}; + EXPECT_EQ(result, "<p>x__(y)__</p>\n"); +} + +TEST(strong_emphasis_intraword_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("x**y**z")}; + EXPECT_EQ(result, "<p>x<strong>y</strong>z</p>\n"); +} + +TEST(strong_emphasis_nested_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("**x, **y**, z**")}; + EXPECT_EQ(result, "<p><strong>x, y, z</strong></p>\n"); +} + +TEST(strong_emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x.__[y]__")}; + EXPECT_EQ(result, "<p>x.<strong>[y]</strong></p>\n"); +} + +TEST(emphasis_rule_of_three_nested_strong) { + const auto result{sourcemeta::core::markdown_to_html("_x__y__z_")}; + EXPECT_EQ(result, "<p><em>x__y__z</em></p>\n"); +} + +TEST(emphasis_rule_of_three_unmatched) { + const auto result{sourcemeta::core::markdown_to_html("_x__y_")}; + EXPECT_EQ(result, "<p><em>x__y</em></p>\n"); +} + +TEST(emphasis_rule_of_three_triple_opening) { + const auto result{sourcemeta::core::markdown_to_html("___x__ y_")}; + EXPECT_EQ(result, "<p><em><strong>x</strong> y</em></p>\n"); +} + +TEST(emphasis_rule_of_three_triple_closing) { + const auto result{sourcemeta::core::markdown_to_html("_x __y___")}; + EXPECT_EQ(result, "<p><em>x <strong>y</strong></em></p>\n"); +} + +TEST(emphasis_rule_of_three_intraword) { + const auto result{sourcemeta::core::markdown_to_html("a***b***c")}; + EXPECT_EQ(result, "<p>a<em><strong>b</strong></em>c</p>\n"); +} + +TEST(emphasis_rule_of_three_long_runs) { + const auto result{sourcemeta::core::markdown_to_html("a****b*******c")}; + EXPECT_EQ(result, "<p>a<strong>b</strong>***c</p>\n"); +} + +TEST(emphasis_containing_link) { + const auto result{ + sourcemeta::core::markdown_to_html("_see [docs](/manual)_")}; + EXPECT_EQ(result, "<p><em>see <a href=\"/manual\">docs</a></em></p>\n"); +} + +TEST(emphasis_empty_delimiters_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "__ empty strong\n\n____ empty double")}; + EXPECT_EQ(result, "<p>__ empty strong</p>\n" + "<p>____ empty double</p>\n"); +} + +TEST(emphasis_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("_one\ntwo_")}; + EXPECT_EQ(result, "<p><em>one\n" + "two</em></p>\n"); +} + +TEST(strong_emphasis_nested_inside_itself) { + const auto result{sourcemeta::core::markdown_to_html("**x **y** z**")}; + EXPECT_EQ(result, "<p><strong>x y z</strong></p>\n"); +} + +TEST(strong_emphasis_quadruple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("****x****")}; + EXPECT_EQ(result, "<p><strong>x</strong></p>\n"); +} + +TEST(strong_emphasis_quintuple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("*****x*****")}; + EXPECT_EQ(result, "<p><em><strong>x</strong></em></p>\n"); +} + +TEST(emphasis_with_escaped_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("x _\\__\n\nx __\\___")}; + EXPECT_EQ(result, "<p>x <em>_</em></p>\n" + "<p>x <strong>_</strong></p>\n"); +} + +TEST(emphasis_with_excess_delimiters) { + const auto result{ + sourcemeta::core::markdown_to_html("_x__\n\n__x_\n\n_x____")}; + EXPECT_EQ(result, "<p><em>x</em>_</p>\n" + "<p>_<em>x</em></p>\n" + "<p><em>x</em>___</p>\n"); +} + +TEST(emphasis_overlapping_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("_x *y_ z*")}; + EXPECT_EQ(result, "<p><em>x *y</em> z*</p>\n"); +} + +TEST(emphasis_overlapping_strong_and_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x **y _z w** v_")}; + EXPECT_EQ(result, "<p><em>x <strong>y _z w</strong> v</em></p>\n"); +} + +TEST(emphasis_unclosed_outer_strong) { + const auto result{sourcemeta::core::markdown_to_html("__x __y z__")}; + EXPECT_EQ(result, "<p>__x <strong>y z</strong></p>\n"); +} + +TEST(emphasis_unclosed_outer_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x _y z_")}; + EXPECT_EQ(result, "<p>_x <em>y z</em></p>\n"); +} + +TEST(emphasis_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("_[x_](/url)")}; + EXPECT_EQ(result, "<p>_<a href=\"/url\">x_</a></p>\n"); +} + +TEST(emphasis_asterisk_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("*x [y*](/url)")}; + EXPECT_EQ(result, "<p>*x <a href=\"/url\">y*</a></p>\n"); +} + +TEST(strong_emphasis_does_not_span_code_span) { + const auto result{sourcemeta::core::markdown_to_html("**x `**` y**")}; + EXPECT_EQ(result, "<p><strong>x <code>**</code> y</strong></p>\n"); +} + +TEST(emphasis_does_not_span_autolink) { + const auto result{sourcemeta::core::markdown_to_html( + "_x<https://sourcemeta.com/?a=_>\n\n**x<https://sourcemeta.com/?b=**>")}; + EXPECT_EQ(result, "<p>_x<a " + "href=\"https://sourcemeta.com/?a=_\">https://" + "sourcemeta.com/?a=_</a></p>\n" + "<p>**x<a " + "href=\"https://sourcemeta.com/?b=**\">https://" + "sourcemeta.com/?b=**</a></p>\n"); +} + +TEST(emphasis_does_not_span_link_text_brackets) { + const auto result{sourcemeta::core::markdown_to_html("_a [b_ c]")}; + EXPECT_EQ(result, "<p><em>a [b</em> c]</p>\n"); +} diff --git a/test/markdown/markdown_entities_test.cc b/test/markdown/markdown_entities_test.cc new file mode 100644 index 0000000000..b8159d413b --- /dev/null +++ b/test/markdown/markdown_entities_test.cc @@ -0,0 +1,114 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(html_entity_named) { + const auto result{sourcemeta::core::markdown_to_html("&copy; 2025")}; + EXPECT_EQ(result, "<p>\xC2\xA9 2025</p>\n"); +} + +TEST(html_entity_numeric) { + const auto result{sourcemeta::core::markdown_to_html("&#169; 2025")}; + EXPECT_EQ(result, "<p>\xC2\xA9 2025</p>\n"); +} + +TEST(entity_named_references) { + const auto result{sourcemeta::core::markdown_to_html( + "&lt; &gt; &quot; &apos; &euro; &hellip;\n&rightarrow; " + "&NotNestedLessLess; &bigstar;")}; + EXPECT_EQ(result, "<p>&lt; &gt; &quot; ' \xe2\x82\xac \xe2\x80\xa6\n" + "\xe2\x86\x92 \xe2\xaa\xa1\xcc\xb8 \xe2\x98\x85</p>\n"); +} + +TEST(entity_decimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("&#65; &#8364; &#128512;")}; + EXPECT_EQ(result, "<p>A \xe2\x82\xac \xf0\x9f\x98\x80</p>\n"); +} + +TEST(entity_hexadecimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("&#x41; &#X20AC; &#x1F600;")}; + EXPECT_EQ(result, "<p>A \xe2\x82\xac \xf0\x9f\x98\x80</p>\n"); +} + +TEST(entity_invalid_references_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "&euro &y; &#z; &#xz;\n&#12345678;\n&#x1234567;\n&NotARealEntity;")}; + EXPECT_EQ(result, "<p>&amp;euro &amp;y; &amp;#z; &amp;#xz;\n" + "\xef\xbf\xbd\n" + "\xef\xbf\xbd\n" + "&amp;NotARealEntity;</p>\n"); +} + +TEST(entity_without_semicolon_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("&amp")}; + EXPECT_EQ(result, "<p>&amp;amp</p>\n"); +} + +TEST(entity_unknown_name_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("&notreal;")}; + EXPECT_EQ(result, "<p>&amp;notreal;</p>\n"); +} + +TEST(entity_code_point_beyond_unicode_range) { + const auto result{sourcemeta::core::markdown_to_html("&#x110000;")}; + EXPECT_EQ(result, "<p>\xef\xbf\xbd</p>\n"); +} + +TEST(entity_surrogate_code_point) { + const auto result{sourcemeta::core::markdown_to_html("&#56320;")}; + EXPECT_EQ(result, "<p>\xef\xbf\xbd</p>\n"); +} + +TEST(entity_in_link_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/caf&eacute; 'caf&eacute;')")}; + EXPECT_EQ(result, + "<p><a href=\"/caf%C3%A9\" title=\"caf\xc3\xa9\">docs</a></p>\n"); +} + +TEST(entity_in_reference_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /caf&eacute; 'caf&eacute;'")}; + EXPECT_EQ(result, + "<p><a href=\"/caf%C3%A9\" title=\"caf\xc3\xa9\">docs</a></p>\n"); +} + +TEST(entity_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`&eacute;`")}; + EXPECT_EQ(result, "<p><code>&amp;eacute;</code></p>\n"); +} + +TEST(entity_inside_indented_code_is_literal) { + const auto result{sourcemeta::core::markdown_to_html(" &eacute;&amp;")}; + EXPECT_EQ(result, "<pre><code>&amp;eacute;&amp;amp;\n" + "</code></pre>\n"); +} + +TEST(entity_cannot_create_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("&#95;word&#95;")}; + EXPECT_EQ(result, "<p>_word_</p>\n"); +} + +TEST(entity_cannot_create_list) { + const auto result{sourcemeta::core::markdown_to_html("&#45; item")}; + EXPECT_EQ(result, "<p>- item</p>\n"); +} + +TEST(entity_newline_is_not_line_break) { + const auto result{sourcemeta::core::markdown_to_html("one&#xA;two")}; + EXPECT_EQ(result, "<p>one\n" + "two</p>\n"); +} + +TEST(entity_tabs_are_not_indentation) { + const auto result{sourcemeta::core::markdown_to_html("&#x9;&#x9;code")}; + EXPECT_EQ(result, "<p>\t\tcode</p>\n"); +} + +TEST(entity_quotes_do_not_delimit_link_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/manual &#34;Title&#34;)")}; + EXPECT_EQ(result, "<p>[docs](/manual &quot;Title&quot;)</p>\n"); +} diff --git a/test/markdown/markdown_escapes_test.cc b/test/markdown/markdown_escapes_test.cc new file mode 100644 index 0000000000..1ce9327360 --- /dev/null +++ b/test/markdown/markdown_escapes_test.cc @@ -0,0 +1,98 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(escape_asterisk) { + const auto result{sourcemeta::core::markdown_to_html("\\*not italic\\*")}; + EXPECT_EQ(result, "<p>*not italic*</p>\n"); +} + +TEST(escape_hash) { + const auto result{sourcemeta::core::markdown_to_html("\\# not a heading")}; + EXPECT_EQ(result, "<p># not a heading</p>\n"); +} + +TEST(escape_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("\\[not a link\\](url)")}; + EXPECT_EQ(result, "<p>[not a link](url)</p>\n"); +} + +TEST(escape_backtick) { + const auto result{sourcemeta::core::markdown_to_html("\\`not code\\`")}; + EXPECT_EQ(result, "<p>`not code`</p>\n"); +} + +TEST(backslash_escapes_every_ascii_punctuation) { + const auto result{sourcemeta::core::markdown_to_html( + "a\\~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\\!")}; + EXPECT_EQ(result, "<p>a~b}c|d{e`f_g^h]i\\j[k@l?m&gt;n=o&lt;p;q:r/" + "s.t-u,v+w*x)y(z'1&amp;2%3$4#5&quot;6!</p>\n"); +} + +TEST(backslash_before_other_characters_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("\\Z \\9 \\é \\€ \\\t")}; + EXPECT_EQ(result, "<p>\\Z \\9 \\\xc3\xa9 \\\xe2\x82\xac \\</p>\n"); +} + +TEST(backslash_escaped_markup_is_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "\\_no emphasis_\n\\<em> no tag\n\\![no image](/x.png)\n2\\) no " + "list\n\\+ no list\n\\> no quote\n\\&amp; no entity")}; + EXPECT_EQ(result, "<p>_no emphasis_\n" + "&lt;em&gt; no tag\n" + "!<a href=\"/x.png\">no image</a>\n" + "2) no list\n" + "+ no list\n" + "&gt; no quote\n" + "&amp;amp; no entity</p>\n"); +} + +TEST(backslash_escaped_backslash_before_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("\\\\_emphasis_")}; + EXPECT_EQ(result, "<p>\\<em>emphasis</em></p>\n"); +} + +TEST(backslash_at_end_of_line_is_hard_break) { + const auto result{sourcemeta::core::markdown_to_html("one\\\ntwo")}; + EXPECT_EQ(result, "<p>one<br />\n" + "two</p>\n"); +} + +TEST(backslash_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`\\*\\_`")}; + EXPECT_EQ(result, "<p><code>\\*\\_</code></p>\n"); +} + +TEST(backslash_inside_indented_code_is_literal) { + const auto result{sourcemeta::core::markdown_to_html(" \\*\\_")}; + EXPECT_EQ(result, "<pre><code>\\*\\_\n" + "</code></pre>\n"); +} + +TEST(backslash_inside_fenced_code_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("```\n\\*\\_\n```")}; + EXPECT_EQ(result, "<pre><code>\\*\\_\n" + "</code></pre>\n"); +} + +TEST(backslash_inside_autolink_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("<https://sourcemeta.com/\\_\\*>")}; + EXPECT_EQ(result, "<p><a " + "href=\"https://sourcemeta.com/%5C_%5C*\">https://" + "sourcemeta.com/\\_\\*</a></p>\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, "<p><a href=\"/a_b\" title=\"c_d\">docs</a></p>\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, "<p><a href=\"/a_b\" title=\"c_d\">docs</a></p>\n"); +} diff --git a/test/markdown/markdown_extensions_test.cc b/test/markdown/markdown_extensions_test.cc deleted file mode 100644 index 811b7910ce..0000000000 --- a/test/markdown/markdown_extensions_test.cc +++ /dev/null @@ -1,796 +0,0 @@ -#include <sourcemeta/core/markdown.h> - -#include <sourcemeta/core/test.h> - -TEST(table_escaped_pipes_inside_inline_content) { - const auto result{sourcemeta::core::markdown_to_html( - "| code |\n| ---- |\n| `a\\|b` |\n| _c\\|d_ |")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>code</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td><code>a|b</code></td>\n" - "</tr>\n" - "<tr>\n" - "<td><em>c|d</em></td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\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, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>a</th>\n" - "<th>b</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>1</td>\n" - "<td>2</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n" - "<p>after</p>\n"); -} - -TEST(table_ends_at_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("| a |\n|---|\n| 1 |\n> quote")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>a</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>1</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n" - "<blockquote>\n" - "<p>quote</p>\n" - "</blockquote>\n"); -} - -TEST(table_line_without_pipes_is_row) { - const auto result{ - sourcemeta::core::markdown_to_html("| a | b |\n|---|---|\ntext row")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>a</th>\n" - "<th>b</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>text row</td>\n" - "<td></td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(table_delimiter_count_mismatch_is_not_table) { - const auto result{ - sourcemeta::core::markdown_to_html("| a | b | c |\n| - | - |")}; - EXPECT_EQ(result, "<p>| a | b | c |\n" - "| - | - |</p>\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, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>a</th>\n" - "<th>b</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>1</td>\n" - "<td></td>\n" - "</tr>\n" - "<tr>\n" - "<td>1</td>\n" - "<td>2</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(table_without_outer_pipes) { - const auto result{ - sourcemeta::core::markdown_to_html("x | y\n:-- | --:\n1 | 2")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th align=\"left\">x</th>\n" - "<th align=\"right\">y</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td align=\"left\">1</td>\n" - "<td align=\"right\">2</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(table_single_column_with_pipes) { - const auto result{ - sourcemeta::core::markdown_to_html("| only |\n| :-: |\n| one |")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th align=\"center\">only</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td align=\"center\">one</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\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, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th align=\"left\">l</th>\n" - "<th align=\"right\">r</th>\n" - "<th align=\"center\">c</th>\n" - "<th>n</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td align=\"left\">1</td>\n" - "<td align=\"right\">2</td>\n" - "<td align=\"center\">3</td>\n" - "<td>4</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(table_delimiter_row_with_surrounding_spaces) { - const auto result{ - sourcemeta::core::markdown_to_html("x|y\n --- | :---: ")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>x</th>\n" - "<th align=\"center\">y</th>\n" - "</tr>\n" - "</thead>\n" - "</table>\n"); -} - -TEST(table_header_cell_with_escaped_pipe) { - const auto result{ - sourcemeta::core::markdown_to_html("| a \\| b | c |\n| - | - |")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>a | b</th>\n" - "<th>c</th>\n" - "</tr>\n" - "</thead>\n" - "</table>\n"); -} - -TEST(table_cell_with_reference_link) { - const auto result{sourcemeta::core::markdown_to_html( - "| [manual] |\n| --- |\n\n[manual]: /manual")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th><a href=\"/manual\">manual</a></th>\n" - "</tr>\n" - "</thead>\n" - "</table>\n"); -} - -TEST(table_interrupts_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("intro\na | b\n- | -")}; - EXPECT_EQ(result, "<p>intro\n" - "a | b</p>\n" - "<ul>\n" - "<li>| -</li>\n" - "</ul>\n"); -} - -TEST(table_inside_list_item) { - const auto result{ - sourcemeta::core::markdown_to_html("* a | b\n - | -\n 1 | 2")}; - EXPECT_EQ(result, "<ul>\n" - "<li>a | b\n" - "<ul>\n" - "<li>| -\n" - "1 | 2</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(table_inside_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("> a | b\n> - | -\n> 1 | 2")}; - EXPECT_EQ(result, "<blockquote>\n" - "<p>a | b</p>\n" - "<ul>\n" - "<li>| -\n" - "1 | 2</li>\n" - "</ul>\n" - "</blockquote>\n"); -} - -TEST(table_cell_with_entities) { - const auto result{ - sourcemeta::core::markdown_to_html("| &lt;tag&gt; |\n| --- |")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>&lt;tag&gt;</th>\n" - "</tr>\n" - "</thead>\n" - "</table>\n"); -} - -TEST(table_empty_header_cells) { - const auto result{ - sourcemeta::core::markdown_to_html("| | |\n|--|--|\n| x | y |")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th></th>\n" - "<th></th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>x</td>\n" - "<td>y</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(table_emphasis_does_not_span_cells) { - const auto result{ - sourcemeta::core::markdown_to_html("| _a | b_ |\n|---|---|")}; - EXPECT_EQ(result, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>_a</th>\n" - "<th>b_</th>\n" - "</tr>\n" - "</thead>\n" - "</table>\n"); -} - -TEST(table_invalid_delimiter_row_is_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("| a |\n| :-:x |")}; - EXPECT_EQ(result, "<p>| a |\n" - "| :-:x |</p>\n"); -} - -TEST(table_delimiter_count_mismatch_without_outer_pipes) { - const auto result{sourcemeta::core::markdown_to_html("x | y | z\n- | -")}; - EXPECT_EQ(result, "<p>x | y | z</p>\n" - "<ul>\n" - "<li>| -</li>\n" - "</ul>\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, "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>x</th>\n" - "<th>y</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>1</td>\n" - "<td>2</td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n"); -} - -TEST(strikethrough_spanning_lines) { - const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")}; - EXPECT_EQ(result, "<p><del>one\n" - "two</del></p>\n"); -} - -TEST(strikethrough_triple_tildes_inline) { - const auto result{sourcemeta::core::markdown_to_html("x ~~~y~~~ z")}; - EXPECT_EQ(result, "<p>x ~~~y~~~ z</p>\n"); -} - -TEST(strikethrough_unclosed) { - const auto result{sourcemeta::core::markdown_to_html("x ~~y")}; - EXPECT_EQ(result, "<p>x ~~y</p>\n"); -} - -TEST(strikethrough_with_surrounding_whitespace) { - const auto result{sourcemeta::core::markdown_to_html("x ~~ y ~~ z")}; - EXPECT_EQ(result, "<p>x ~~ y ~~ z</p>\n"); -} - -TEST(strikethrough_and_emphasis_overlap) { - const auto result{sourcemeta::core::markdown_to_html("~~_x~~_")}; - EXPECT_EQ(result, "<p><del>_x</del>_</p>\n"); -} - -TEST(strikethrough_escaped_closing) { - const auto result{sourcemeta::core::markdown_to_html("~~x\\~~")}; - EXPECT_EQ(result, "<p>~~x~~</p>\n"); -} - -TEST(strikethrough_inside_link_text) { - const auto result{sourcemeta::core::markdown_to_html("[~~old~~ new](/x)")}; - EXPECT_EQ(result, "<p><a href=\"/x\"><del>old</del> new</a></p>\n"); -} - -TEST(extended_autolink_www_with_path) { - const auto result{ - sourcemeta::core::markdown_to_html("see www.sourcemeta.com/docs/intro")}; - EXPECT_EQ(result, "<p>see <a " - "href=\"http://www.sourcemeta.com/docs/" - "intro\">www.sourcemeta.com/docs/intro</a></p>\n"); -} - -TEST(extended_autolink_trailing_punctuation_excluded) { - const auto result{ - sourcemeta::core::markdown_to_html("Go to www.sourcemeta.com/x.y!")}; - EXPECT_EQ(result, "<p>Go to <a " - "href=\"http://www.sourcemeta.com/x.y\">www.sourcemeta.com/" - "x.y</a>!</p>\n"); -} - -TEST(extended_autolink_balanced_parentheses) { - const auto result{sourcemeta::core::markdown_to_html( - "https://sourcemeta.com/wiki/Thing_(disambiguation)")}; - EXPECT_EQ( - result, - "<p><a " - "href=\"https://sourcemeta.com/wiki/Thing_(disambiguation)\">https://" - "sourcemeta.com/wiki/Thing_(disambiguation)</a></p>\n"); -} - -TEST(extended_autolink_unbalanced_closing_parentheses) { - const auto result{ - sourcemeta::core::markdown_to_html("https://sourcemeta.com/a_(b)))")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/a_(b)\">https://" - "sourcemeta.com/a_(b)</a>))</p>\n"); -} - -TEST(extended_autolink_inside_parentheses) { - const auto result{ - sourcemeta::core::markdown_to_html("(see https://sourcemeta.com/a_(b))")}; - EXPECT_EQ(result, "<p>(see <a " - "href=\"https://sourcemeta.com/a_(b)\">https://" - "sourcemeta.com/a_(b)</a>)</p>\n"); -} - -TEST(extended_autolink_entity_like_suffix_excluded) { - const auto result{sourcemeta::core::markdown_to_html( - "https://sourcemeta.com/?x=1&amp;\n\nhttps://sourcemeta.com/?x=1&y=2")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/?x=1\">https://" - "sourcemeta.com/?x=1</a>&amp;</p>\n" - "<p><a " - "href=\"https://sourcemeta.com/?x=1&amp;y=2\">https://" - "sourcemeta.com/?x=1&amp;y=2</a></p>\n"); -} - -TEST(extended_autolink_ends_at_less_than) { - const auto result{ - sourcemeta::core::markdown_to_html("https://sourcemeta.com/a<b")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/a\">https://sourcemeta.com/" - "a</a>&lt;b</p>\n"); -} - -TEST(extended_autolink_underscore_in_final_domain_segments) { - const auto result{sourcemeta::core::markdown_to_html( - "www.sourcemeta._com\n\nwww.source_meta.com")}; - EXPECT_EQ(result, "<p>www.sourcemeta._com</p>\n" - "<p>www.source_meta.com</p>\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, - "<p><a " - "href=\"http://" - "www.docs_site.sourcemeta.com\">www.docs_site.sourcemeta.com</a></p>\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, - "<p><a " - "href=\"https://sourcemeta.com/search?q=json+schema#results\">https://" - "sourcemeta.com/search?q=json+schema#results</a></p>\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, - "<p><a href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a></p>\n" - "<p>team@source+meta.com</p>\n" - "<p><a " - "href=\"mailto:team+tag@sourcemeta.com\">team+tag@sourcemeta.com</a></" - "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, "<p><a " - "href=\"mailto:first.last@sourcemeta.com\">first.last@" - "sourcemeta.com</a>.</p>\n" - "<p>first.last@sourcemeta.com-</p>\n" - "<p>first.last@sourcemeta.com_</p>\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, - "<p><a " - "href=\"mailto:team@sourcemeta.com\">mailto:team@sourcemeta.com</a></p>\n" - "<p><a " - "href=\"xmpp:team@sourcemeta.com/resource\">xmpp:team@sourcemeta.com/" - "resource</a></p>\n"); -} - -TEST(extended_autolink_not_after_word_character) { - const auto result{sourcemeta::core::markdown_to_html("xwww.sourcemeta.com")}; - EXPECT_EQ(result, "<p>xwww.sourcemeta.com</p>\n"); -} - -TEST(extended_autolink_inside_emphasis) { - const auto result{ - sourcemeta::core::markdown_to_html("_https://sourcemeta.com_")}; - EXPECT_EQ( - result, - "<p><em><a " - "href=\"https://sourcemeta.com\">https://sourcemeta.com</a></em></p>\n"); -} - -TEST(extended_autolink_ftp_scheme) { - const auto result{ - sourcemeta::core::markdown_to_html("ftp://files.sourcemeta.com/archive")}; - EXPECT_EQ(result, "<p><a " - "href=\"ftp://files.sourcemeta.com/archive\">ftp://" - "files.sourcemeta.com/archive</a></p>\n"); -} - -TEST(extended_autolink_inside_link_text) { - const auto result{ - sourcemeta::core::markdown_to_html("[https://sourcemeta.com](/x)")}; - EXPECT_EQ(result, "<p><a href=\"/x\">https://sourcemeta.com</a></p>\n"); -} - -TEST(extended_autolink_inside_code_span) { - const auto result{ - sourcemeta::core::markdown_to_html("`https://sourcemeta.com`")}; - EXPECT_EQ(result, "<p><code>https://sourcemeta.com</code></p>\n"); -} - -TEST(task_list_uppercase_marker) { - const auto result{sourcemeta::core::markdown_to_html("* [X] finished")}; - EXPECT_EQ(result, "<ul>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" " - "/> finished</li>\n" - "</ul>\n"); -} - -TEST(task_list_marker_needs_following_space) { - const auto result{sourcemeta::core::markdown_to_html("* [ ]pending")}; - EXPECT_EQ(result, "<ul>\n" - "<li>[ ]pending</li>\n" - "</ul>\n"); -} - -TEST(task_list_marker_with_other_character) { - const auto result{ - sourcemeta::core::markdown_to_html("* [-] partial\n* [x] complete")}; - EXPECT_EQ(result, "<ul>\n" - "<li>[-] partial</li>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" " - "/> complete</li>\n" - "</ul>\n"); -} - -TEST(task_list_nested) { - const auto result{sourcemeta::core::markdown_to_html( - "* [ ] parent\n * [x] child\n * [ ] grandchild")}; - EXPECT_EQ(result, - "<ul>\n" - "<li><input type=\"checkbox\" disabled=\"\" /> parent\n" - "<ul>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> child\n" - "<ul>\n" - "<li><input type=\"checkbox\" disabled=\"\" /> grandchild</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(task_list_in_ordered_list) { - const auto result{ - sourcemeta::core::markdown_to_html("1) [ ] first\n2) [X] second")}; - EXPECT_EQ( - result, - "<ol>\n" - "<li><input type=\"checkbox\" disabled=\"\" /> first</li>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> second</li>\n" - "</ol>\n"); -} - -TEST(task_list_marker_not_at_item_start) { - const auto result{sourcemeta::core::markdown_to_html("* text [ ] later")}; - EXPECT_EQ(result, "<ul>\n" - "<li>text [ ] later</li>\n" - "</ul>\n"); -} - -TEST(task_list_marker_alone) { - const auto result{sourcemeta::core::markdown_to_html("* [x]")}; - EXPECT_EQ(result, "<ul>\n" - "<li>[x]</li>\n" - "</ul>\n"); -} - -TEST(task_list_in_loose_list) { - const auto result{sourcemeta::core::markdown_to_html("* [x] a\n\n* [ ] b")}; - EXPECT_EQ(result, - "<ul>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> \n" - "<p>a</p>\n" - "</li>\n" - "<li><input type=\"checkbox\" disabled=\"\" /> \n" - "<p>b</p>\n" - "</li>\n" - "</ul>\n"); -} - -TEST(footnote_referenced_multiple_times) { - const auto result{sourcemeta::core::markdown_to_html( - "one[^n] two[^n] three[^n]\n\n[^n]: shared")}; - EXPECT_EQ( - result, - "<p>one<sup class=\"footnote-ref\"><a href=\"#fn-n\" id=\"fnref-n\" " - "data-footnote-ref>1</a></sup> two<sup class=\"footnote-ref\"><a " - "href=\"#fn-n\" id=\"fnref-n-2\" data-footnote-ref>1</a></sup> three<sup " - "class=\"footnote-ref\"><a href=\"#fn-n\" id=\"fnref-n-3\" " - "data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-n\">\n" - "<p>shared <a href=\"#fnref-n\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back " - "to reference 1\">\xe2\x86\xa9</a> <a href=\"#fnref-n-2\" " - "class=\"footnote-backref\" data-footnote-backref " - "data-footnote-backref-idx=\"1-2\" aria-label=\"Back to reference " - "1-2\">\xe2\x86\xa9<sup class=\"footnote-ref\">2</sup></a> <a " - "href=\"#fnref-n-3\" class=\"footnote-backref\" data-footnote-backref " - "data-footnote-backref-idx=\"1-3\" aria-label=\"Back to reference " - "1-3\">\xe2\x86\xa9<sup class=\"footnote-ref\">3</sup></a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_unreferenced_definition_omitted) { - const auto result{ - sourcemeta::core::markdown_to_html("body\n\n[^orphan]: unused")}; - EXPECT_EQ(result, "<p>body</p>\n"); -} - -TEST(footnote_reference_without_definition) { - const auto result{sourcemeta::core::markdown_to_html("body[^ghost]")}; - EXPECT_EQ(result, "<p>body[^ghost]</p>\n"); -} - -TEST(footnote_label_case_insensitive) { - const auto result{ - sourcemeta::core::markdown_to_html("body[^LABEL]\n\n[^label]: text")}; - EXPECT_EQ(result, - "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-label\" " - "id=\"fnref-label\" data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-label\">\n" - "<p>text <a href=\"#fnref-label\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_definition_with_multiple_blocks) { - const auto result{sourcemeta::core::markdown_to_html( - "body[^m]\n\n[^m]: first\n\n > quoted\n\n * item")}; - EXPECT_EQ(result, "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-m\" " - "id=\"fnref-m\" data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-m\">\n" - "<p>first</p>\n" - "<blockquote>\n" - "<p>quoted</p>\n" - "</blockquote>\n" - "<ul>\n" - "<li>item</li>\n" - "</ul>\n" - "<a href=\"#fnref-m\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_numbering_follows_references) { - const auto result{sourcemeta::core::markdown_to_html( - "x[^z] y[^y] z[^x]\n\n[^x]: X\n[^y]: Y\n[^z]: Z")}; - EXPECT_EQ(result, - "<p>x<sup class=\"footnote-ref\"><a href=\"#fn-z\" id=\"fnref-z\" " - "data-footnote-ref>1</a></sup> y<sup class=\"footnote-ref\"><a " - "href=\"#fn-y\" id=\"fnref-y\" data-footnote-ref>2</a></sup> z<sup " - "class=\"footnote-ref\"><a href=\"#fn-x\" id=\"fnref-x\" " - "data-footnote-ref>3</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-z\">\n" - "<p>Z <a href=\"#fnref-z\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "<li id=\"fn-y\">\n" - "<p>Y <a href=\"#fnref-y\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"2\" " - "aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "<li id=\"fn-x\">\n" - "<p>X <a href=\"#fnref-x\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"3\" " - "aria-label=\"Back to reference 3\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_label_with_special_characters) { - const auto result{ - sourcemeta::core::markdown_to_html("body[^a&b<c>]\n\n[^a&b<c>]: text")}; - EXPECT_EQ( - result, - "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-a&amp;b%3Cc%3E\" " - "id=\"fnref-a&amp;b%3Cc%3E\" data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-a&amp;b%3Cc%3E\">\n" - "<p>text <a href=\"#fnref-a&amp;b%3Cc%3E\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back " - "to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_reference_in_table_cell) { - const auto result{sourcemeta::core::markdown_to_html( - "| h |\n| - |\n| c[^t] |\n\n[^t]: table note")}; - EXPECT_EQ(result, - "<table>\n" - "<thead>\n" - "<tr>\n" - "<th>h</th>\n" - "</tr>\n" - "</thead>\n" - "<tbody>\n" - "<tr>\n" - "<td>c<sup class=\"footnote-ref\"><a href=\"#fn-t\" id=\"fnref-t\" " - "data-footnote-ref>1</a></sup></td>\n" - "</tr>\n" - "</tbody>\n" - "</table>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-t\">\n" - "<p>table note <a href=\"#fnref-t\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_reference_inside_definition) { - const auto result{sourcemeta::core::markdown_to_html( - "body[^a]\n\n[^a]: refers[^b]\n\n[^b]: nested")}; - EXPECT_EQ(result, "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-a\" " - "id=\"fnref-a\" data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-a\">\n" - "<p>refers<sup class=\"footnote-ref\"><a href=\"#fn-b\" " - "id=\"fnref-b\" data-footnote-ref>2</a></sup> <a " - "href=\"#fnref-a\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "<li id=\"fn-b\">\n" - "<p>nested <a href=\"#fnref-b\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"2\" " - "aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_definition_inside_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("body[^q]\n\n> [^q]: inside quote")}; - EXPECT_EQ(result, - "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-q\" " - "id=\"fnref-q\" data-footnote-ref>1</a></sup></p>\n" - "<blockquote>\n" - "</blockquote>\n" - "<section class=\"footnotes\" data-footnotes>\n" - "<ol>\n" - "<li id=\"fn-q\">\n" - "<p>inside quote <a href=\"#fnref-q\" class=\"footnote-backref\" " - "data-footnote-backref data-footnote-backref-idx=\"1\" " - "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "</ol>\n" - "</section>\n"); -} - -TEST(footnote_label_with_space_is_not_footnote) { - const auto result{sourcemeta::core::markdown_to_html( - "body[^two words]\n\n[^two words]: text")}; - EXPECT_EQ(result, "<p>body<a href=\"text\">^two words</a></p>\n"); -} diff --git a/test/markdown/markdown_footnotes_test.cc b/test/markdown/markdown_footnotes_test.cc new file mode 100644 index 0000000000..f253a34a26 --- /dev/null +++ b/test/markdown/markdown_footnotes_test.cc @@ -0,0 +1,241 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(footnote_basic) { + const auto result{ + sourcemeta::core::markdown_to_html("Text[^1]\n\n[^1]: Footnote content")}; + EXPECT_EQ( + result, + "<p>Text<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\"" + " data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n<ol>\n" + "<li id=\"fn-1\">\n<p>Footnote content " + "<a href=\"#fnref-1\" class=\"footnote-backref\"" + " data-footnote-backref data-footnote-backref-idx=\"1\"" + " aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n</ol>\n</section>\n"); +} + +TEST(footnote_multiple) { + const auto result{sourcemeta::core::markdown_to_html( + "A[^1] and B[^2]\n\n[^1]: First\n\n[^2]: Second")}; + EXPECT_EQ(result, + "<p>A<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\"" + " data-footnote-ref>1</a></sup> and " + "B<sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\"" + " data-footnote-ref>2</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n<ol>\n" + "<li id=\"fn-1\">\n<p>First " + "<a href=\"#fnref-1\" class=\"footnote-backref\"" + " data-footnote-backref data-footnote-backref-idx=\"1\"" + " aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "<li id=\"fn-2\">\n<p>Second " + "<a href=\"#fnref-2\" class=\"footnote-backref\"" + " data-footnote-backref data-footnote-backref-idx=\"2\"" + " aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" + "</li>\n</ol>\n</section>\n"); +} + +TEST(footnote_referenced_multiple_times) { + const auto result{sourcemeta::core::markdown_to_html( + "one[^n] two[^n] three[^n]\n\n[^n]: shared")}; + EXPECT_EQ( + result, + "<p>one<sup class=\"footnote-ref\"><a href=\"#fn-n\" id=\"fnref-n\" " + "data-footnote-ref>1</a></sup> two<sup class=\"footnote-ref\"><a " + "href=\"#fn-n\" id=\"fnref-n-2\" data-footnote-ref>1</a></sup> three<sup " + "class=\"footnote-ref\"><a href=\"#fn-n\" id=\"fnref-n-3\" " + "data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-n\">\n" + "<p>shared <a href=\"#fnref-n\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back " + "to reference 1\">\xe2\x86\xa9</a> <a href=\"#fnref-n-2\" " + "class=\"footnote-backref\" data-footnote-backref " + "data-footnote-backref-idx=\"1-2\" aria-label=\"Back to reference " + "1-2\">\xe2\x86\xa9<sup class=\"footnote-ref\">2</sup></a> <a " + "href=\"#fnref-n-3\" class=\"footnote-backref\" data-footnote-backref " + "data-footnote-backref-idx=\"1-3\" aria-label=\"Back to reference " + "1-3\">\xe2\x86\xa9<sup class=\"footnote-ref\">3</sup></a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_unreferenced_definition_omitted) { + const auto result{ + sourcemeta::core::markdown_to_html("body\n\n[^orphan]: unused")}; + EXPECT_EQ(result, "<p>body</p>\n"); +} + +TEST(footnote_reference_without_definition) { + const auto result{sourcemeta::core::markdown_to_html("body[^ghost]")}; + EXPECT_EQ(result, "<p>body[^ghost]</p>\n"); +} + +TEST(footnote_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^LABEL]\n\n[^label]: text")}; + EXPECT_EQ(result, + "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-label\" " + "id=\"fnref-label\" data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-label\">\n" + "<p>text <a href=\"#fnref-label\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_definition_with_multiple_blocks) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^m]\n\n[^m]: first\n\n > quoted\n\n * item")}; + EXPECT_EQ(result, "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-m\" " + "id=\"fnref-m\" data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-m\">\n" + "<p>first</p>\n" + "<blockquote>\n" + "<p>quoted</p>\n" + "</blockquote>\n" + "<ul>\n" + "<li>item</li>\n" + "</ul>\n" + "<a href=\"#fnref-m\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_numbering_follows_references) { + const auto result{sourcemeta::core::markdown_to_html( + "x[^z] y[^y] z[^x]\n\n[^x]: X\n[^y]: Y\n[^z]: Z")}; + EXPECT_EQ(result, + "<p>x<sup class=\"footnote-ref\"><a href=\"#fn-z\" id=\"fnref-z\" " + "data-footnote-ref>1</a></sup> y<sup class=\"footnote-ref\"><a " + "href=\"#fn-y\" id=\"fnref-y\" data-footnote-ref>2</a></sup> z<sup " + "class=\"footnote-ref\"><a href=\"#fn-x\" id=\"fnref-x\" " + "data-footnote-ref>3</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-z\">\n" + "<p>Z <a href=\"#fnref-z\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "<li id=\"fn-y\">\n" + "<p>Y <a href=\"#fnref-y\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"2\" " + "aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "<li id=\"fn-x\">\n" + "<p>X <a href=\"#fnref-x\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"3\" " + "aria-label=\"Back to reference 3\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_label_with_special_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^a&b<c>]\n\n[^a&b<c>]: text")}; + EXPECT_EQ( + result, + "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-a&amp;b%3Cc%3E\" " + "id=\"fnref-a&amp;b%3Cc%3E\" data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-a&amp;b%3Cc%3E\">\n" + "<p>text <a href=\"#fnref-a&amp;b%3Cc%3E\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back " + "to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_reference_in_table_cell) { + const auto result{sourcemeta::core::markdown_to_html( + "| h |\n| - |\n| c[^t] |\n\n[^t]: table note")}; + EXPECT_EQ(result, + "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>h</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>c<sup class=\"footnote-ref\"><a href=\"#fn-t\" id=\"fnref-t\" " + "data-footnote-ref>1</a></sup></td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-t\">\n" + "<p>table note <a href=\"#fnref-t\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_reference_inside_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^a]\n\n[^a]: refers[^b]\n\n[^b]: nested")}; + EXPECT_EQ(result, "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-a\" " + "id=\"fnref-a\" data-footnote-ref>1</a></sup></p>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-a\">\n" + "<p>refers<sup class=\"footnote-ref\"><a href=\"#fn-b\" " + "id=\"fnref-b\" data-footnote-ref>2</a></sup> <a " + "href=\"#fnref-a\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "<li id=\"fn-b\">\n" + "<p>nested <a href=\"#fnref-b\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"2\" " + "aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_definition_inside_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^q]\n\n> [^q]: inside quote")}; + EXPECT_EQ(result, + "<p>body<sup class=\"footnote-ref\"><a href=\"#fn-q\" " + "id=\"fnref-q\" data-footnote-ref>1</a></sup></p>\n" + "<blockquote>\n" + "</blockquote>\n" + "<section class=\"footnotes\" data-footnotes>\n" + "<ol>\n" + "<li id=\"fn-q\">\n" + "<p>inside quote <a href=\"#fnref-q\" class=\"footnote-backref\" " + "data-footnote-backref data-footnote-backref-idx=\"1\" " + "aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" + "</li>\n" + "</ol>\n" + "</section>\n"); +} + +TEST(footnote_label_with_space_is_not_footnote) { + const auto result{sourcemeta::core::markdown_to_html( + "body[^two words]\n\n[^two words]: text")}; + EXPECT_EQ(result, "<p>body<a href=\"text\">^two words</a></p>\n"); +} diff --git a/test/markdown/markdown_headings_test.cc b/test/markdown/markdown_headings_test.cc new file mode 100644 index 0000000000..401cf225d4 --- /dev/null +++ b/test/markdown/markdown_headings_test.cc @@ -0,0 +1,360 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(atx_heading_level_1) { + const auto result{sourcemeta::core::markdown_to_html("# Heading 1")}; + EXPECT_EQ(result, "<h1>Heading 1</h1>\n"); +} + +TEST(atx_heading_level_2) { + const auto result{sourcemeta::core::markdown_to_html("## Heading 2")}; + EXPECT_EQ(result, "<h2>Heading 2</h2>\n"); +} + +TEST(atx_heading_level_3) { + const auto result{sourcemeta::core::markdown_to_html("### Heading 3")}; + EXPECT_EQ(result, "<h3>Heading 3</h3>\n"); +} + +TEST(atx_heading_level_4) { + const auto result{sourcemeta::core::markdown_to_html("#### Heading 4")}; + EXPECT_EQ(result, "<h4>Heading 4</h4>\n"); +} + +TEST(atx_heading_level_5) { + const auto result{sourcemeta::core::markdown_to_html("##### Heading 5")}; + EXPECT_EQ(result, "<h5>Heading 5</h5>\n"); +} + +TEST(atx_heading_level_6) { + const auto result{sourcemeta::core::markdown_to_html("###### Heading 6")}; + EXPECT_EQ(result, "<h6>Heading 6</h6>\n"); +} + +TEST(atx_heading_with_closing_hashes) { + const auto result{sourcemeta::core::markdown_to_html("## Heading ##")}; + EXPECT_EQ(result, "<h2>Heading</h2>\n"); +} + +TEST(setext_heading_level_1) { + const auto result{sourcemeta::core::markdown_to_html("Heading 1\n=========")}; + EXPECT_EQ(result, "<h1>Heading 1</h1>\n"); +} + +TEST(setext_heading_level_2) { + const auto result{sourcemeta::core::markdown_to_html("Heading 2\n---------")}; + EXPECT_EQ(result, "<h2>Heading 2</h2>\n"); +} + +TEST(heading_followed_by_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("# Title\n\nSome text here.")}; + EXPECT_EQ(result, "<h1>Title</h1>\n<p>Some text here.</p>\n"); +} + +TEST(link_inside_heading) { + const auto result{ + sourcemeta::core::markdown_to_html("## [Link](https://example.com)")}; + EXPECT_EQ(result, "<h2><a href=\"https://example.com\">Link</a></h2>\n"); +} + +TEST(inline_code_inside_heading) { + const auto result{ + sourcemeta::core::markdown_to_html("## The `main` function")}; + EXPECT_EQ(result, "<h2>The <code>main</code> function</h2>\n"); +} + +TEST(consecutive_headings) { + const auto result{ + sourcemeta::core::markdown_to_html("# One\n\n## Two\n\n### Three")}; + EXPECT_EQ(result, "<h1>One</h1>\n<h2>Two</h2>\n<h3>Three</h3>\n"); +} + +TEST(heading_with_thematic_break) { + const auto result{ + sourcemeta::core::markdown_to_html("# Title\n\n---\n\nContent")}; + EXPECT_EQ(result, "<h1>Title</h1>\n<hr />\n<p>Content</p>\n"); +} + +TEST(heading_with_bold_and_code) { + const auto result{ + sourcemeta::core::markdown_to_html("## **Bold** and `code`")}; + EXPECT_EQ(result, "<h2><strong>Bold</strong> and <code>code</code></h2>\n"); +} + +TEST(dashes_after_paragraph_make_setext_heading) { + const auto result{sourcemeta::core::markdown_to_html("alpha\n-----\nbeta")}; + EXPECT_EQ(result, "<h2>alpha</h2>\n" + "<p>beta</p>\n"); +} + +TEST(atx_heading_eight_hashes_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("######## title")}; + EXPECT_EQ(result, "<p>######## title</p>\n"); +} + +TEST(atx_heading_without_space_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("#1 fan\n\n##tag")}; + EXPECT_EQ(result, "<p>#1 fan</p>\n" + "<p>##tag</p>\n"); +} + +TEST(atx_heading_escaped_hash_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("\\# title")}; + EXPECT_EQ(result, "<p># title</p>\n"); +} + +TEST(atx_heading_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("## plain *em* \\*raw\\*")}; + EXPECT_EQ(result, "<h2>plain <em>em</em> *raw*</h2>\n"); +} + +TEST(atx_heading_surrounding_spaces_ignored) { + const auto result{ + sourcemeta::core::markdown_to_html("### spaced out ")}; + EXPECT_EQ(result, "<h3>spaced out</h3>\n"); +} + +TEST(atx_heading_indented_up_to_three_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html(" #### deep\n # shallow")}; + EXPECT_EQ(result, "<h4>deep</h4>\n" + "<h1>shallow</h1>\n"); +} + +TEST(atx_heading_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" ## code")}; + EXPECT_EQ(result, "<pre><code>## code\n" + "</code></pre>\n"); +} + +TEST(atx_heading_indented_four_spaces_continues_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n ## more")}; + EXPECT_EQ(result, "<p>text\n" + "## more</p>\n"); +} + +TEST(atx_heading_closing_sequence_of_any_length) { + const auto result{ + sourcemeta::core::markdown_to_html("## title #######\n#### title #")}; + EXPECT_EQ(result, "<h2>title</h2>\n" + "<h4>title</h4>\n"); +} + +TEST(atx_heading_closing_sequence_followed_by_spaces) { + const auto result{sourcemeta::core::markdown_to_html("## title ## ")}; + EXPECT_EQ(result, "<h2>title</h2>\n"); +} + +TEST(atx_heading_hashes_followed_by_text_are_content) { + const auto result{sourcemeta::core::markdown_to_html("## title ## tail")}; + EXPECT_EQ(result, "<h2>title ## tail</h2>\n"); +} + +TEST(atx_heading_closing_sequence_needs_preceding_space) { + const auto result{sourcemeta::core::markdown_to_html("## title##")}; + EXPECT_EQ(result, "<h2>title##</h2>\n"); +} + +TEST(atx_heading_escaped_closing_hashes) { + const auto result{ + sourcemeta::core::markdown_to_html("## title \\##\n# title #\\#")}; + EXPECT_EQ(result, "<h2>title ##</h2>\n" + "<h1>title ##</h1>\n"); +} + +TEST(atx_heading_between_thematic_breaks) { + const auto result{sourcemeta::core::markdown_to_html("---\n# title\n---")}; + EXPECT_EQ(result, "<hr />\n" + "<h1>title</h1>\n" + "<hr />\n"); +} + +TEST(atx_heading_interrupts_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("one line\n## heading\nnext line")}; + EXPECT_EQ(result, "<p>one line</p>\n" + "<h2>heading</h2>\n" + "<p>next line</p>\n"); +} + +TEST(atx_heading_empty) { + const auto result{sourcemeta::core::markdown_to_html("#\n## \n#### ####")}; + EXPECT_EQ(result, "<h1></h1>\n" + "<h2></h2>\n" + "<h4></h4>\n"); +} + +TEST(setext_heading_single_character_underline) { + const auto result{sourcemeta::core::markdown_to_html("Title *em*\n=")}; + EXPECT_EQ(result, "<h1>Title <em>em</em></h1>\n"); +} + +TEST(setext_heading_multiline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("First *line\nsecond* line\n---")}; + EXPECT_EQ(result, "<h2>First <em>line\n" + "second</em> line</h2>\n"); +} + +TEST(setext_heading_content_surrounding_spaces_stripped) { + const auto result{ + sourcemeta::core::markdown_to_html(" Title *em\ntext* \n===")}; + EXPECT_EQ(result, "<h1>Title <em>em\n" + "text</em></h1>\n"); +} + +TEST(setext_heading_indented_content) { + const auto result{ + sourcemeta::core::markdown_to_html(" Title\n ===\n\n Other\n-")}; + EXPECT_EQ(result, "<h1>Title</h1>\n" + "<h2>Other</h2>\n"); +} + +TEST(setext_heading_content_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" Title\n ===")}; + EXPECT_EQ(result, "<pre><code>Title\n" + "===\n" + "</code></pre>\n"); +} + +TEST(setext_heading_underline_indented_up_to_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html("Title\n === ")}; + EXPECT_EQ(result, "<h1>Title</h1>\n"); +} + +TEST(setext_heading_underline_indented_four_spaces_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Title\n ===")}; + EXPECT_EQ(result, "<p>Title\n" + "===</p>\n"); +} + +TEST(setext_heading_underline_with_internal_space_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Title\n== ==")}; + EXPECT_EQ(result, "<p>Title\n" + "== ==</p>\n"); +} + +TEST(setext_heading_trailing_spaces_are_not_hard_break) { + const auto result{sourcemeta::core::markdown_to_html("Title \n===")}; + EXPECT_EQ(result, "<h1>Title</h1>\n"); +} + +TEST(setext_heading_trailing_backslash_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("Title\\\n===")}; + EXPECT_EQ(result, "<h1>Title\\</h1>\n"); +} + +TEST(setext_heading_underline_breaks_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`code\n===\n`")}; + EXPECT_EQ(result, "<h1>`code</h1>\n" + "<p>`</p>\n"); +} + +TEST(setext_heading_equals_underline_is_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html("> Title\n===")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>Title\n" + "===</p>\n" + "</blockquote>\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, "<blockquote>\n" + "<p>quote\n" + "continued</p>\n" + "</blockquote>\n" + "<hr />\n"); +} + +TEST(setext_heading_equals_after_list_item_is_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html("* Item\n===")}; + EXPECT_EQ(result, "<ul>\n" + "<li>Item\n" + "===</li>\n" + "</ul>\n"); +} + +TEST(setext_heading_multiline_paragraph_content) { + const auto result{ + sourcemeta::core::markdown_to_html("Line one\nLine two\n===")}; + EXPECT_EQ(result, "<h1>Line one\n" + "Line two</h1>\n"); +} + +TEST(setext_heading_equals_line_then_heading) { + const auto result{ + sourcemeta::core::markdown_to_html("===\nTitle\n===\nText\n---")}; + EXPECT_EQ(result, "<h1>===\n" + "Title</h1>\n" + "<h2>Text</h2>\n"); +} + +TEST(setext_heading_underline_without_content_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("\n==")}; + EXPECT_EQ(result, "<p>==</p>\n"); +} + +TEST(setext_heading_dashes_after_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("***\n---")}; + EXPECT_EQ(result, "<hr />\n" + "<hr />\n"); +} + +TEST(setext_heading_dashes_after_list_item) { + const auto result{sourcemeta::core::markdown_to_html("* item\n---")}; + EXPECT_EQ(result, "<ul>\n" + "<li>item</li>\n" + "</ul>\n" + "<hr />\n"); +} + +TEST(setext_heading_equals_after_indented_code) { + const auto result{sourcemeta::core::markdown_to_html(" code\n===")}; + EXPECT_EQ(result, "<pre><code>code\n" + "</code></pre>\n" + "<p>===</p>\n"); +} + +TEST(setext_heading_escaped_content) { + const auto result{sourcemeta::core::markdown_to_html("\\- not a list\n---")}; + EXPECT_EQ(result, "<h2>- not a list</h2>\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, "<p>Intro</p>\n" + "<h1>Title</h1>\n" + "<p>Body</p>\n"); +} + +TEST(setext_heading_spaced_dashes_after_blank_line) { + const auto result{ + sourcemeta::core::markdown_to_html("One\ntwo\n\n- - -\n\nthree")}; + EXPECT_EQ(result, "<p>One\n" + "two</p>\n" + "<hr />\n" + "<p>three</p>\n"); +} + +TEST(setext_heading_underscores_are_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("One\ntwo\n___\nthree")}; + EXPECT_EQ(result, "<p>One\n" + "two</p>\n" + "<hr />\n" + "<p>three</p>\n"); +} + +TEST(setext_heading_escaped_equals_are_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("One\ntwo\n\\===\nthree")}; + EXPECT_EQ(result, "<p>One\n" + "two\n" + "===\n" + "three</p>\n"); +} diff --git a/test/markdown/markdown_images_test.cc b/test/markdown/markdown_images_test.cc new file mode 100644 index 0000000000..bfb60d0864 --- /dev/null +++ b/test/markdown/markdown_images_test.cc @@ -0,0 +1,85 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(image) { + const auto result{ + sourcemeta::core::markdown_to_html("![alt text](image.png)")}; + EXPECT_EQ(result, "<p><img src=\"image.png\" alt=\"alt text\" /></p>\n"); +} + +TEST(image_with_title) { + const auto result{ + sourcemeta::core::markdown_to_html("![alt](image.png \"My Image\")")}; + EXPECT_EQ( + result, + "<p><img src=\"image.png\" alt=\"alt\" title=\"My Image\" /></p>\n"); +} + +TEST(image_as_link) { + const auto result{sourcemeta::core::markdown_to_html( + "[![logo](logo.png)](https://example.com)")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\">" + "<img src=\"logo.png\" alt=\"logo\" /></a></p>\n"); +} + +TEST(image_with_empty_alt) { + const auto result{sourcemeta::core::markdown_to_html("![](image.png)")}; + EXPECT_EQ(result, "<p><img src=\"image.png\" alt=\"\" /></p>\n"); +} + +TEST(image_description_containing_link) { + const auto result{ + sourcemeta::core::markdown_to_html("![a [b](/inner)](/image.png)")}; + EXPECT_EQ(result, "<p><img src=\"/image.png\" alt=\"a b\" /></p>\n"); +} + +TEST(image_alt_text_is_plain_text) { + const auto result{ + sourcemeta::core::markdown_to_html("![_italic_ and `code`](/i.png)")}; + EXPECT_EQ(result, "<p><img src=\"/i.png\" alt=\"italic and code\" /></p>\n"); +} + +TEST(image_alt_text_from_nested_image) { + const auto result{sourcemeta::core::markdown_to_html( + "![outer ![inner](/in.png)](/out.png)")}; + EXPECT_EQ(result, "<p><img src=\"/out.png\" alt=\"outer inner\" /></p>\n"); +} + +TEST(image_alt_text_from_nested_link) { + const auto result{ + sourcemeta::core::markdown_to_html("![outer [inner](/in)](/out.png)")}; + EXPECT_EQ(result, "<p><img src=\"/out.png\" alt=\"outer inner\" /></p>\n"); +} + +TEST(image_collapsed_and_shortcut_references) { + const auto result{sourcemeta::core::markdown_to_html( + "![logo][]\n\n![LOGO]\n\n[logo]: /logo.png 'Logo'")}; + EXPECT_EQ(result, + "<p><img src=\"/logo.png\" alt=\"logo\" title=\"Logo\" /></p>\n" + "<p><img src=\"/logo.png\" alt=\"LOGO\" title=\"Logo\" /></p>\n"); +} + +TEST(image_escaped_bracket_is_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("!\\[logo]\n\n[logo]: /logo.png")}; + EXPECT_EQ(result, "<p>![logo]</p>\n"); +} + +TEST(image_escaped_exclamation_is_link) { + const auto result{ + sourcemeta::core::markdown_to_html("\\![logo]\n\n[logo]: /logo.png")}; + EXPECT_EQ(result, "<p>!<a href=\"/logo.png\">logo</a></p>\n"); +} + +TEST(image_angle_bracket_destination_with_space) { + const auto result{sourcemeta::core::markdown_to_html("![logo](</a b.png>)")}; + EXPECT_EQ(result, "<p><img src=\"/a%20b.png\" alt=\"logo\" /></p>\n"); +} + +TEST(image_title_with_escaped_quote) { + const auto result{ + sourcemeta::core::markdown_to_html("![logo](/l.png 'it\\'s')")}; + EXPECT_EQ(result, + "<p><img src=\"/l.png\" alt=\"logo\" title=\"it's\" /></p>\n"); +} diff --git a/test/markdown/markdown_inlines_test.cc b/test/markdown/markdown_inlines_test.cc deleted file mode 100644 index 47d30d13dd..0000000000 --- a/test/markdown/markdown_inlines_test.cc +++ /dev/null @@ -1,1039 +0,0 @@ -#include <sourcemeta/core/markdown.h> - -#include <sourcemeta/core/test.h> - -TEST(backslash_escapes_every_ascii_punctuation) { - const auto result{sourcemeta::core::markdown_to_html( - "a\\~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\\!")}; - EXPECT_EQ(result, "<p>a~b}c|d{e`f_g^h]i\\j[k@l?m&gt;n=o&lt;p;q:r/" - "s.t-u,v+w*x)y(z'1&amp;2%3$4#5&quot;6!</p>\n"); -} - -TEST(backslash_before_other_characters_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("\\Z \\9 \\é \\€ \\\t")}; - EXPECT_EQ(result, "<p>\\Z \\9 \\\xc3\xa9 \\\xe2\x82\xac \\</p>\n"); -} - -TEST(backslash_escaped_markup_is_literal) { - const auto result{sourcemeta::core::markdown_to_html( - "\\_no emphasis_\n\\<em> no tag\n\\![no image](/x.png)\n2\\) no " - "list\n\\+ no list\n\\> no quote\n\\&amp; no entity")}; - EXPECT_EQ(result, "<p>_no emphasis_\n" - "&lt;em&gt; no tag\n" - "!<a href=\"/x.png\">no image</a>\n" - "2) no list\n" - "+ no list\n" - "&gt; no quote\n" - "&amp;amp; no entity</p>\n"); -} - -TEST(backslash_escaped_backslash_before_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("\\\\_emphasis_")}; - EXPECT_EQ(result, "<p>\\<em>emphasis</em></p>\n"); -} - -TEST(backslash_at_end_of_line_is_hard_break) { - const auto result{sourcemeta::core::markdown_to_html("one\\\ntwo")}; - EXPECT_EQ(result, "<p>one<br />\n" - "two</p>\n"); -} - -TEST(backslash_inside_code_span_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("`\\*\\_`")}; - EXPECT_EQ(result, "<p><code>\\*\\_</code></p>\n"); -} - -TEST(backslash_inside_indented_code_is_literal) { - const auto result{sourcemeta::core::markdown_to_html(" \\*\\_")}; - EXPECT_EQ(result, "<pre><code>\\*\\_\n" - "</code></pre>\n"); -} - -TEST(backslash_inside_fenced_code_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("```\n\\*\\_\n```")}; - EXPECT_EQ(result, "<pre><code>\\*\\_\n" - "</code></pre>\n"); -} - -TEST(backslash_inside_autolink_is_literal) { - const auto result{ - sourcemeta::core::markdown_to_html("<https://sourcemeta.com/\\_\\*>")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/%5C_%5C*\">https://" - "sourcemeta.com/\\_\\*</a></p>\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, "<p><a href=\"/a_b\" title=\"c_d\">docs</a></p>\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, "<p><a href=\"/a_b\" title=\"c_d\">docs</a></p>\n"); -} - -TEST(entity_named_references) { - const auto result{sourcemeta::core::markdown_to_html( - "&lt; &gt; &quot; &apos; &euro; &hellip;\n&rightarrow; " - "&NotNestedLessLess; &bigstar;")}; - EXPECT_EQ(result, "<p>&lt; &gt; &quot; ' \xe2\x82\xac \xe2\x80\xa6\n" - "\xe2\x86\x92 \xe2\xaa\xa1\xcc\xb8 \xe2\x98\x85</p>\n"); -} - -TEST(entity_decimal_references) { - const auto result{ - sourcemeta::core::markdown_to_html("&#65; &#8364; &#128512;")}; - EXPECT_EQ(result, "<p>A \xe2\x82\xac \xf0\x9f\x98\x80</p>\n"); -} - -TEST(entity_hexadecimal_references) { - const auto result{ - sourcemeta::core::markdown_to_html("&#x41; &#X20AC; &#x1F600;")}; - EXPECT_EQ(result, "<p>A \xe2\x82\xac \xf0\x9f\x98\x80</p>\n"); -} - -TEST(entity_invalid_references_are_literal) { - const auto result{sourcemeta::core::markdown_to_html( - "&euro &y; &#z; &#xz;\n&#12345678;\n&#x1234567;\n&NotARealEntity;")}; - EXPECT_EQ(result, "<p>&amp;euro &amp;y; &amp;#z; &amp;#xz;\n" - "\xef\xbf\xbd\n" - "\xef\xbf\xbd\n" - "&amp;NotARealEntity;</p>\n"); -} - -TEST(entity_without_semicolon_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("&amp")}; - EXPECT_EQ(result, "<p>&amp;amp</p>\n"); -} - -TEST(entity_unknown_name_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("&notreal;")}; - EXPECT_EQ(result, "<p>&amp;notreal;</p>\n"); -} - -TEST(entity_code_point_beyond_unicode_range) { - const auto result{sourcemeta::core::markdown_to_html("&#x110000;")}; - EXPECT_EQ(result, "<p>\xef\xbf\xbd</p>\n"); -} - -TEST(entity_surrogate_code_point) { - const auto result{sourcemeta::core::markdown_to_html("&#56320;")}; - EXPECT_EQ(result, "<p>\xef\xbf\xbd</p>\n"); -} - -TEST(entity_in_link_destination_and_title) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs](/caf&eacute; 'caf&eacute;')")}; - EXPECT_EQ(result, - "<p><a href=\"/caf%C3%A9\" title=\"caf\xc3\xa9\">docs</a></p>\n"); -} - -TEST(entity_in_reference_definition) { - const auto result{sourcemeta::core::markdown_to_html( - "[docs]\n\n[docs]: /caf&eacute; 'caf&eacute;'")}; - EXPECT_EQ(result, - "<p><a href=\"/caf%C3%A9\" title=\"caf\xc3\xa9\">docs</a></p>\n"); -} - -TEST(entity_inside_code_span_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("`&eacute;`")}; - EXPECT_EQ(result, "<p><code>&amp;eacute;</code></p>\n"); -} - -TEST(entity_inside_indented_code_is_literal) { - const auto result{sourcemeta::core::markdown_to_html(" &eacute;&amp;")}; - EXPECT_EQ(result, "<pre><code>&amp;eacute;&amp;amp;\n" - "</code></pre>\n"); -} - -TEST(entity_cannot_create_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("&#95;word&#95;")}; - EXPECT_EQ(result, "<p>_word_</p>\n"); -} - -TEST(entity_cannot_create_list) { - const auto result{sourcemeta::core::markdown_to_html("&#45; item")}; - EXPECT_EQ(result, "<p>- item</p>\n"); -} - -TEST(entity_newline_is_not_line_break) { - const auto result{sourcemeta::core::markdown_to_html("one&#xA;two")}; - EXPECT_EQ(result, "<p>one\n" - "two</p>\n"); -} - -TEST(entity_tabs_are_not_indentation) { - const auto result{sourcemeta::core::markdown_to_html("&#x9;&#x9;code")}; - EXPECT_EQ(result, "<p>\t\tcode</p>\n"); -} - -TEST(entity_quotes_do_not_delimit_link_title) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs](/manual &#34;Title&#34;)")}; - EXPECT_EQ(result, "<p>[docs](/manual &quot;Title&quot;)</p>\n"); -} - -TEST(code_span_with_double_backticks) { - const auto result{sourcemeta::core::markdown_to_html("``a`b``")}; - EXPECT_EQ(result, "<p><code>a`b</code></p>\n"); -} - -TEST(code_span_strips_one_surrounding_space) { - const auto result{sourcemeta::core::markdown_to_html("`` `x` ``")}; - EXPECT_EQ(result, "<p><code>`x`</code></p>\n"); -} - -TEST(code_span_strips_only_one_space_per_side) { - const auto result{sourcemeta::core::markdown_to_html("`` x ``")}; - EXPECT_EQ(result, "<p><code> x </code></p>\n"); -} - -TEST(code_span_space_on_one_side_is_kept) { - const auto result{sourcemeta::core::markdown_to_html("`x `")}; - EXPECT_EQ(result, "<p><code>x </code></p>\n"); -} - -TEST(code_span_non_breaking_space_is_kept) { - const auto result{sourcemeta::core::markdown_to_html("`\xC2\xA0" - "x`")}; - EXPECT_EQ(result, "<p><code>\xc2\xa0x</code></p>\n"); -} - -TEST(code_span_of_only_spaces_is_kept) { - const auto result{sourcemeta::core::markdown_to_html("` `")}; - EXPECT_EQ(result, "<p><code> </code></p>\n"); -} - -TEST(code_span_line_endings_become_spaces) { - const auto result{sourcemeta::core::markdown_to_html("`one\ntwo\nthree`")}; - EXPECT_EQ(result, "<p><code>one two three</code></p>\n"); -} - -TEST(code_span_interior_spaces_are_kept) { - const auto result{sourcemeta::core::markdown_to_html("`a b \n c`")}; - EXPECT_EQ(result, "<p><code>a b c</code></p>\n"); -} - -TEST(code_span_backslash_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("`a\\`b")}; - EXPECT_EQ(result, "<p><code>a\\</code>b</p>\n"); -} - -TEST(code_span_backtick_run_lengths_must_match) { - const auto result{sourcemeta::core::markdown_to_html("``x```")}; - EXPECT_EQ(result, "<p>``x```</p>\n"); -} - -TEST(code_span_unmatched_opening_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("``x")}; - EXPECT_EQ(result, "<p>``x</p>\n"); -} - -TEST(code_span_longer_closing_run_does_not_close) { - const auto result{sourcemeta::core::markdown_to_html("``x```y``")}; - EXPECT_EQ(result, "<p><code>x```y</code></p>\n"); -} - -TEST(code_span_takes_precedence_over_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("_x`_`")}; - EXPECT_EQ(result, "<p>_x<code>_</code></p>\n"); -} - -TEST(code_span_takes_precedence_over_link) { - const auto result{sourcemeta::core::markdown_to_html("[text `code](/url`)")}; - EXPECT_EQ(result, "<p>[text <code>code](/url</code>)</p>\n"); -} - -TEST(code_span_takes_precedence_over_autolink) { - const auto result{ - sourcemeta::core::markdown_to_html("`<https://sourcemeta.com/`x>`")}; - EXPECT_EQ(result, "<p><code>&lt;https://sourcemeta.com/</code>x&gt;`</p>\n"); -} - -TEST(autolink_takes_precedence_over_code_span) { - const auto result{ - sourcemeta::core::markdown_to_html("<https://sourcemeta.com/`x>`")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/%60x\">https://" - "sourcemeta.com/`x</a>`</p>\n"); -} - -TEST(emphasis_opening_followed_by_whitespace) { - const auto result{sourcemeta::core::markdown_to_html("x _ y z_")}; - EXPECT_EQ(result, "<p>x _ y z_</p>\n"); -} - -TEST(emphasis_opening_between_letter_and_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("x*(y)*")}; - EXPECT_EQ(result, "<p>x*(y)*</p>\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, "<p>_\xc2\xa0x\xc2\xa0_</p>\n"); -} - -TEST(emphasis_intraword_with_asterisks) { - const auto result{ - sourcemeta::core::markdown_to_html("un*frigging*believable")}; - EXPECT_EQ(result, "<p>un<em>frigging</em>believable</p>\n"); -} - -TEST(emphasis_intraword_with_underscores_is_literal) { - const auto result{sourcemeta::core::markdown_to_html("snake_case_name")}; - EXPECT_EQ(result, "<p>snake_case_name</p>\n"); -} - -TEST(emphasis_intraword_with_underscores_in_cyrillic) { - const auto result{sourcemeta::core::markdown_to_html("река_течёт_быстро")}; - EXPECT_EQ(result, "<p>\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</p>\n"); -} - -TEST(emphasis_between_digits) { - const auto result{sourcemeta::core::markdown_to_html("1*2*3\n\n1_2_3")}; - EXPECT_EQ(result, "<p>1<em>2</em>3</p>\n" - "<p>1_2_3</p>\n"); -} - -TEST(emphasis_underscore_between_letters_and_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("x_(y)_z")}; - EXPECT_EQ(result, "<p>x_(y)_z</p>\n"); -} - -TEST(emphasis_underscore_after_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("x._[y]_")}; - EXPECT_EQ(result, "<p>x.<em>[y]</em></p>\n"); -} - -TEST(emphasis_mismatched_delimiter_characters) { - const auto result{sourcemeta::core::markdown_to_html("*x_")}; - EXPECT_EQ(result, "<p>*x_</p>\n"); -} - -TEST(emphasis_closing_preceded_by_whitespace) { - const auto result{sourcemeta::core::markdown_to_html("_x y _")}; - EXPECT_EQ(result, "<p>_x y _</p>\n"); -} - -TEST(emphasis_closing_on_new_line) { - const auto result{sourcemeta::core::markdown_to_html("_x y\n_")}; - EXPECT_EQ(result, "<p>_x y\n" - "_</p>\n"); -} - -TEST(emphasis_closing_not_right_flanking) { - const auto result{sourcemeta::core::markdown_to_html("_[_x]")}; - EXPECT_EQ(result, "<p>_[_x]</p>\n"); -} - -TEST(emphasis_nested_with_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("_[_x_]_")}; - EXPECT_EQ(result, "<p><em>[<em>x</em>]</em></p>\n"); -} - -TEST(emphasis_closing_followed_by_letter) { - const auto result{sourcemeta::core::markdown_to_html("*x*y")}; - EXPECT_EQ(result, "<p><em>x</em>y</p>\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, "<p>_x_y</p>\n" - "<p><em>x_y_z</em></p>\n"); -} - -TEST(emphasis_underscore_followed_by_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("_[x]_!")}; - EXPECT_EQ(result, "<p><em>[x]</em>!</p>\n"); -} - -TEST(strong_emphasis_opening_followed_by_whitespace) { - const auto result{sourcemeta::core::markdown_to_html("__ x y__")}; - EXPECT_EQ(result, "<p>__ x y__</p>\n"); -} - -TEST(strong_emphasis_between_letter_and_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("x__(y)__")}; - EXPECT_EQ(result, "<p>x__(y)__</p>\n"); -} - -TEST(strong_emphasis_intraword_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("x**y**z")}; - EXPECT_EQ(result, "<p>x<strong>y</strong>z</p>\n"); -} - -TEST(strong_emphasis_nested_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("**x, **y**, z**")}; - EXPECT_EQ(result, "<p><strong>x, y, z</strong></p>\n"); -} - -TEST(strong_emphasis_underscore_after_punctuation) { - const auto result{sourcemeta::core::markdown_to_html("x.__[y]__")}; - EXPECT_EQ(result, "<p>x.<strong>[y]</strong></p>\n"); -} - -TEST(emphasis_rule_of_three_nested_strong) { - const auto result{sourcemeta::core::markdown_to_html("_x__y__z_")}; - EXPECT_EQ(result, "<p><em>x__y__z</em></p>\n"); -} - -TEST(emphasis_rule_of_three_unmatched) { - const auto result{sourcemeta::core::markdown_to_html("_x__y_")}; - EXPECT_EQ(result, "<p><em>x__y</em></p>\n"); -} - -TEST(emphasis_rule_of_three_triple_opening) { - const auto result{sourcemeta::core::markdown_to_html("___x__ y_")}; - EXPECT_EQ(result, "<p><em><strong>x</strong> y</em></p>\n"); -} - -TEST(emphasis_rule_of_three_triple_closing) { - const auto result{sourcemeta::core::markdown_to_html("_x __y___")}; - EXPECT_EQ(result, "<p><em>x <strong>y</strong></em></p>\n"); -} - -TEST(emphasis_rule_of_three_intraword) { - const auto result{sourcemeta::core::markdown_to_html("a***b***c")}; - EXPECT_EQ(result, "<p>a<em><strong>b</strong></em>c</p>\n"); -} - -TEST(emphasis_rule_of_three_long_runs) { - const auto result{sourcemeta::core::markdown_to_html("a****b*******c")}; - EXPECT_EQ(result, "<p>a<strong>b</strong>***c</p>\n"); -} - -TEST(emphasis_containing_link) { - const auto result{ - sourcemeta::core::markdown_to_html("_see [docs](/manual)_")}; - EXPECT_EQ(result, "<p><em>see <a href=\"/manual\">docs</a></em></p>\n"); -} - -TEST(emphasis_empty_delimiters_are_literal) { - const auto result{sourcemeta::core::markdown_to_html( - "__ empty strong\n\n____ empty double")}; - EXPECT_EQ(result, "<p>__ empty strong</p>\n" - "<p>____ empty double</p>\n"); -} - -TEST(emphasis_spanning_lines) { - const auto result{sourcemeta::core::markdown_to_html("_one\ntwo_")}; - EXPECT_EQ(result, "<p><em>one\n" - "two</em></p>\n"); -} - -TEST(strong_emphasis_nested_inside_itself) { - const auto result{sourcemeta::core::markdown_to_html("**x **y** z**")}; - EXPECT_EQ(result, "<p><strong>x y z</strong></p>\n"); -} - -TEST(strong_emphasis_quadruple_delimiters) { - const auto result{sourcemeta::core::markdown_to_html("****x****")}; - EXPECT_EQ(result, "<p><strong>x</strong></p>\n"); -} - -TEST(strong_emphasis_quintuple_delimiters) { - const auto result{sourcemeta::core::markdown_to_html("*****x*****")}; - EXPECT_EQ(result, "<p><em><strong>x</strong></em></p>\n"); -} - -TEST(emphasis_with_escaped_delimiters) { - const auto result{sourcemeta::core::markdown_to_html("x _\\__\n\nx __\\___")}; - EXPECT_EQ(result, "<p>x <em>_</em></p>\n" - "<p>x <strong>_</strong></p>\n"); -} - -TEST(emphasis_with_excess_delimiters) { - const auto result{ - sourcemeta::core::markdown_to_html("_x__\n\n__x_\n\n_x____")}; - EXPECT_EQ(result, "<p><em>x</em>_</p>\n" - "<p>_<em>x</em></p>\n" - "<p><em>x</em>___</p>\n"); -} - -TEST(emphasis_overlapping_delimiters) { - const auto result{sourcemeta::core::markdown_to_html("_x *y_ z*")}; - EXPECT_EQ(result, "<p><em>x *y</em> z*</p>\n"); -} - -TEST(emphasis_overlapping_strong_and_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("_x **y _z w** v_")}; - EXPECT_EQ(result, "<p><em>x <strong>y _z w</strong> v</em></p>\n"); -} - -TEST(emphasis_unclosed_outer_strong) { - const auto result{sourcemeta::core::markdown_to_html("__x __y z__")}; - EXPECT_EQ(result, "<p>__x <strong>y z</strong></p>\n"); -} - -TEST(emphasis_unclosed_outer_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("_x _y z_")}; - EXPECT_EQ(result, "<p>_x <em>y z</em></p>\n"); -} - -TEST(emphasis_does_not_span_link_brackets) { - const auto result{sourcemeta::core::markdown_to_html("_[x_](/url)")}; - EXPECT_EQ(result, "<p>_<a href=\"/url\">x_</a></p>\n"); -} - -TEST(emphasis_asterisk_does_not_span_link_brackets) { - const auto result{sourcemeta::core::markdown_to_html("*x [y*](/url)")}; - EXPECT_EQ(result, "<p>*x <a href=\"/url\">y*</a></p>\n"); -} - -TEST(strong_emphasis_does_not_span_code_span) { - const auto result{sourcemeta::core::markdown_to_html("**x `**` y**")}; - EXPECT_EQ(result, "<p><strong>x <code>**</code> y</strong></p>\n"); -} - -TEST(emphasis_does_not_span_autolink) { - const auto result{sourcemeta::core::markdown_to_html( - "_x<https://sourcemeta.com/?a=_>\n\n**x<https://sourcemeta.com/?b=**>")}; - EXPECT_EQ(result, "<p>_x<a " - "href=\"https://sourcemeta.com/?a=_\">https://" - "sourcemeta.com/?a=_</a></p>\n" - "<p>**x<a " - "href=\"https://sourcemeta.com/?b=**\">https://" - "sourcemeta.com/?b=**</a></p>\n"); -} - -TEST(link_destination_single_space) { - const auto result{sourcemeta::core::markdown_to_html("[docs]( )")}; - EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); -} - -TEST(link_destination_empty_angle_brackets_with_spaces) { - const auto result{sourcemeta::core::markdown_to_html("[docs]( <> )")}; - EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); -} - -TEST(link_destination_angle_brackets_with_spaces) { - const auto result{sourcemeta::core::markdown_to_html("[docs](<a b c>)")}; - EXPECT_EQ(result, "<p><a href=\"a%20b%20c\">docs</a></p>\n"); -} - -TEST(link_destination_with_space_is_not_link) { - const auto result{sourcemeta::core::markdown_to_html("[docs](/a b)")}; - EXPECT_EQ(result, "<p>[docs](/a b)</p>\n"); -} - -TEST(link_destination_with_newline_is_not_link) { - const auto result{sourcemeta::core::markdown_to_html("[docs](/a\n/b)")}; - EXPECT_EQ(result, "<p>[docs](/a\n" - "/b)</p>\n"); -} - -TEST(link_destination_angle_brackets_with_newline_is_not_link) { - const auto result{sourcemeta::core::markdown_to_html("[docs](</a\nb>)")}; - EXPECT_EQ(result, "<p>[docs](&lt;/a\n" - "b&gt;)</p>\n"); -} - -TEST(link_destination_angle_brackets_with_closing_paren) { - const auto result{sourcemeta::core::markdown_to_html("[docs](<a)b>)")}; - EXPECT_EQ(result, "<p><a href=\"a)b\">docs</a></p>\n"); -} - -TEST(link_destination_unmatched_angle_bracket) { - const auto result{sourcemeta::core::markdown_to_html("[docs](<a\\>)")}; - EXPECT_EQ(result, "<p>[docs](&lt;a&gt;)</p>\n"); -} - -TEST(link_destination_invalid_angle_bracket_forms) { - const auto result{sourcemeta::core::markdown_to_html("[x](<y)z\n[x](<y>z)")}; - EXPECT_EQ(result, "<p>[x](&lt;y)z\n" - "[x](<!-- raw HTML omitted -->z)</p>\n"); -} - -TEST(link_destination_escaped_parentheses) { - const auto result{sourcemeta::core::markdown_to_html("[docs](\\(a\\)b)")}; - EXPECT_EQ(result, "<p><a href=\"(a)b\">docs</a></p>\n"); -} - -TEST(link_destination_balanced_parentheses) { - const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)e)")}; - EXPECT_EQ(result, "<p><a href=\"a(b(c)d)e\">docs</a></p>\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, "<p>[docs](a(b(c)d)</p>\n"); -} - -TEST(link_destination_escaped_unbalanced_parentheses) { - const auto result{sourcemeta::core::markdown_to_html("[docs](a\\(b\\(c)")}; - EXPECT_EQ(result, "<p><a href=\"a(b(c\">docs</a></p>\n"); -} - -TEST(link_destination_angle_brackets_unbalanced_parentheses) { - const auto result{sourcemeta::core::markdown_to_html("[docs](<a(b(c>)")}; - EXPECT_EQ(result, "<p><a href=\"a(b(c\">docs</a></p>\n"); -} - -TEST(link_destination_backslash_escapes) { - const auto result{sourcemeta::core::markdown_to_html("[docs](a\\]\\!)")}; - EXPECT_EQ(result, "<p><a href=\"a%5D!\">docs</a></p>\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, - "<p><a href=\"#top\">x</a></p>\n" - "<p><a href=\"https://sourcemeta.com/?q=1#top\">y</a></p>\n"); -} - -TEST(link_destination_backslash_before_letter) { - const auto result{sourcemeta::core::markdown_to_html("[docs](a\\zb)")}; - EXPECT_EQ(result, "<p><a href=\"a%5Czb\">docs</a></p>\n"); -} - -TEST(link_destination_percent_encoding_and_entities) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs](caf%C3%A9&eacute;)")}; - EXPECT_EQ(result, "<p><a href=\"caf%C3%A9%C3%A9\">docs</a></p>\n"); -} - -TEST(link_destination_that_looks_like_title) { - const auto result{sourcemeta::core::markdown_to_html("[docs]('Title')")}; - EXPECT_EQ(result, "<p><a href=\"&#x27;Title&#x27;\">docs</a></p>\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, - "<p><a href=\"/x\" title=\"one\">a</a> <a href=\"/y\" " - "title=\"two\">b</a> <a href=\"/z\" title=\"three\">c</a></p>\n"); -} - -TEST(link_title_escaped_quotes_and_entities) { - const auto result{ - sourcemeta::core::markdown_to_html("[docs](/x 'it\\'s &amp; more')")}; - EXPECT_EQ(result, - "<p><a href=\"/x\" title=\"it's &amp; more\">docs</a></p>\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, "<p><a href=\"/x%C2%A0&#x27;title&#x27;\">docs</a></p>\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, "<p>[docs](/x 'a'b')</p>\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, "<p><a href=\"/x\" title=\"with 'single' and " - "&quot;double&quot;\">docs</a></p>\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, "<p><a href=\"/x\" title=\"title\">docs</a></p>\n"); -} - -TEST(link_spaces_before_parentheses_is_not_link) { - const auto result{sourcemeta::core::markdown_to_html("[docs] (/x)")}; - EXPECT_EQ(result, "<p>[docs] (/x)</p>\n"); -} - -TEST(link_text_with_balanced_brackets) { - const auto result{sourcemeta::core::markdown_to_html("[a [b] [c [d]]](/x)")}; - EXPECT_EQ(result, "<p><a href=\"/x\">a [b] [c [d]]</a></p>\n"); -} - -TEST(link_text_with_unbalanced_closing_bracket) { - const auto result{sourcemeta::core::markdown_to_html("[a] b](/x)")}; - EXPECT_EQ(result, "<p>[a] b](/x)</p>\n"); -} - -TEST(link_text_with_unbalanced_opening_bracket) { - const auto result{sourcemeta::core::markdown_to_html("[a [b](/x)")}; - EXPECT_EQ(result, "<p>[a <a href=\"/x\">b</a></p>\n"); -} - -TEST(link_text_with_escaped_bracket) { - const auto result{sourcemeta::core::markdown_to_html("[a \\] b](/x)")}; - EXPECT_EQ(result, "<p><a href=\"/x\">a ] b</a></p>\n"); -} - -TEST(link_text_with_inline_content) { - const auto result{ - sourcemeta::core::markdown_to_html("[_a_ __b__ `c` ~~d~~](/x)")}; - EXPECT_EQ(result, "<p><a href=\"/x\"><em>a</em> <strong>b</strong> " - "<code>c</code> <del>d</del></a></p>\n"); -} - -TEST(link_text_with_image) { - const auto result{ - sourcemeta::core::markdown_to_html("[![logo](/logo.png 'Logo')](/home)")}; - EXPECT_EQ(result, "<p><a href=\"/home\"><img src=\"/logo.png\" alt=\"logo\" " - "title=\"Logo\" /></a></p>\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, "<p>[a <a href=\"/inner\">b</a> c](/outer)</p>\n"); -} - -TEST(link_inside_emphasis_inside_link_text) { - const auto result{ - sourcemeta::core::markdown_to_html("[a _[b](/inner)_](/outer)")}; - EXPECT_EQ(result, "<p>[a <em><a href=\"/inner\">b</a></em>](/outer)</p>\n"); -} - -TEST(image_description_containing_link) { - const auto result{ - sourcemeta::core::markdown_to_html("![a [b](/inner)](/image.png)")}; - EXPECT_EQ(result, "<p><img src=\"/image.png\" alt=\"a b\" /></p>\n"); -} - -TEST(link_text_brackets_take_precedence_over_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("_[a_](/x)")}; - EXPECT_EQ(result, "<p>_<a href=\"/x\">a_</a></p>\n"); -} - -TEST(link_brackets_take_precedence_over_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("[a _b](/x_)")}; - EXPECT_EQ(result, "<p><a href=\"/x_\">a _b</a></p>\n"); -} - -TEST(emphasis_does_not_span_link_text_brackets) { - const auto result{sourcemeta::core::markdown_to_html("_a [b_ c]")}; - EXPECT_EQ(result, "<p><em>a [b</em> c]</p>\n"); -} - -TEST(link_code_span_takes_precedence) { - const auto result{sourcemeta::core::markdown_to_html("[a`](/x)`")}; - EXPECT_EQ(result, "<p>[a<code>](/x)</code></p>\n"); -} - -TEST(link_autolink_takes_precedence) { - const auto result{sourcemeta::core::markdown_to_html( - "[a<https://sourcemeta.com/?q=](/x)>")}; - EXPECT_EQ(result, "<p>[a<a " - "href=\"https://sourcemeta.com/?q=%5D(/x)\">https://" - "sourcemeta.com/?q=](/x)</a></p>\n"); -} - -TEST(reference_link_full) { - const auto result{sourcemeta::core::markdown_to_html( - "[the docs][manual]\n\n[manual]: /manual 'Manual'")}; - EXPECT_EQ(result, - "<p><a href=\"/manual\" title=\"Manual\">the docs</a></p>\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, "<p><a href=\"/ref\">a [b] c</a></p>\n"); -} - -TEST(reference_link_label_case_folding) { - const auto result{sourcemeta::core::markdown_to_html( - "[text][MaNuAl]\n\n[manual]: /manual")}; - EXPECT_EQ(result, "<p><a href=\"/manual\">text</a></p>\n"); -} - -TEST(reference_link_label_titlecase_digraph_folding) { - const auto result{sourcemeta::core::markdown_to_html("[Dž]\n\n[dž]: /digraph")}; - EXPECT_EQ(result, "<p><a href=\"/digraph\">\xc7\x85</a></p>\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, "<p>[a] <a href=\"/ref\">ref</a></p>\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, "<p>[a]\n" - "<a href=\"/ref\">ref</a></p>\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, "<p><a href=\"/first\">a</a></p>\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, "<p>[a][ref*]</p>\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, "<p>[a][b]c]</p>\n" - "<p>[b]c]: /x</p>\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, "<p><a href=\"/x\">a</a></p>\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, "<p><a href=\"/x\">a\\b</a></p>\n"); -} - -TEST(reference_link_empty_brackets_are_literal) { - const auto result{sourcemeta::core::markdown_to_html("[][]\n\n[]: /x")}; - EXPECT_EQ(result, "<p>[][]</p>\n" - "<p>[]: /x</p>\n"); -} - -TEST(collapsed_reference_link_case_insensitive) { - const auto result{ - sourcemeta::core::markdown_to_html("[MANUAL][]\n\n[manual]: /manual")}; - EXPECT_EQ(result, "<p><a href=\"/manual\">MANUAL</a></p>\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, "<p><a href=\"/manual\">manual</a>\n" - "[]</p>\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, "<p><a href=\"/manual\"><em>the</em> manual</a></p>\n"); -} - -TEST(shortcut_reference_link_escaped) { - const auto result{ - sourcemeta::core::markdown_to_html("\\[manual\\]\n\n[manual]: /manual")}; - EXPECT_EQ(result, "<p>[manual]</p>\n"); -} - -TEST(shortcut_reference_link_label_with_underscore) { - const auto result{ - sourcemeta::core::markdown_to_html("[manual_]: /manual\n\n_[manual_]")}; - EXPECT_EQ(result, "<p>_<a href=\"/manual\">manual_</a></p>\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, "<p><a href=\"/b\">a</a><a href=\"/c\">c</a></p>\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, "<p>[a]<a href=\"/c\">b</a></p>\n"); -} - -TEST(inline_link_takes_precedence_over_reference) { - const auto result{ - sourcemeta::core::markdown_to_html("[manual]()\n\n[manual]: /manual")}; - EXPECT_EQ(result, "<p><a href=\"\">manual</a></p>\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, "<p><a href=\"/manual\">manual</a>(a b)</p>\n"); -} - -TEST(image_alt_text_is_plain_text) { - const auto result{ - sourcemeta::core::markdown_to_html("![_italic_ and `code`](/i.png)")}; - EXPECT_EQ(result, "<p><img src=\"/i.png\" alt=\"italic and code\" /></p>\n"); -} - -TEST(image_alt_text_from_nested_image) { - const auto result{sourcemeta::core::markdown_to_html( - "![outer ![inner](/in.png)](/out.png)")}; - EXPECT_EQ(result, "<p><img src=\"/out.png\" alt=\"outer inner\" /></p>\n"); -} - -TEST(image_alt_text_from_nested_link) { - const auto result{ - sourcemeta::core::markdown_to_html("![outer [inner](/in)](/out.png)")}; - EXPECT_EQ(result, "<p><img src=\"/out.png\" alt=\"outer inner\" /></p>\n"); -} - -TEST(image_collapsed_and_shortcut_references) { - const auto result{sourcemeta::core::markdown_to_html( - "![logo][]\n\n![LOGO]\n\n[logo]: /logo.png 'Logo'")}; - EXPECT_EQ(result, - "<p><img src=\"/logo.png\" alt=\"logo\" title=\"Logo\" /></p>\n" - "<p><img src=\"/logo.png\" alt=\"LOGO\" title=\"Logo\" /></p>\n"); -} - -TEST(image_escaped_bracket_is_literal) { - const auto result{ - sourcemeta::core::markdown_to_html("!\\[logo]\n\n[logo]: /logo.png")}; - EXPECT_EQ(result, "<p>![logo]</p>\n"); -} - -TEST(image_escaped_exclamation_is_link) { - const auto result{ - sourcemeta::core::markdown_to_html("\\![logo]\n\n[logo]: /logo.png")}; - EXPECT_EQ(result, "<p>!<a href=\"/logo.png\">logo</a></p>\n"); -} - -TEST(image_angle_bracket_destination_with_space) { - const auto result{sourcemeta::core::markdown_to_html("![logo](</a b.png>)")}; - EXPECT_EQ(result, "<p><img src=\"/a%20b.png\" alt=\"logo\" /></p>\n"); -} - -TEST(image_title_with_escaped_quote) { - const auto result{ - sourcemeta::core::markdown_to_html("![logo](/l.png 'it\\'s')")}; - EXPECT_EQ(result, - "<p><img src=\"/l.png\" alt=\"logo\" title=\"it's\" /></p>\n"); -} - -TEST(autolink_uri_schemes) { - const auto result{sourcemeta::core::markdown_to_html( - "<irc://chat.example>\n\n<x-custom+scheme.1:thing>\n\n<ab:c>")}; - EXPECT_EQ( - result, - "<p><a href=\"irc://chat.example\">irc://chat.example</a></p>\n" - "<p><a href=\"x-custom+scheme.1:thing\">x-custom+scheme.1:thing</a></p>\n" - "<p><a href=\"ab:c\">ab:c</a></p>\n"); -} - -TEST(autolink_uri_scheme_too_short_is_not_autolink) { - const auto result{sourcemeta::core::markdown_to_html("<z:thing>")}; - EXPECT_EQ(result, "<p>&lt;z:thing&gt;</p>\n"); -} - -TEST(autolink_uri_with_space_is_not_autolink) { - const auto result{ - sourcemeta::core::markdown_to_html("<https://sourcemeta.com/a b>")}; - EXPECT_EQ(result, "<p>&lt;<a " - "href=\"https://sourcemeta.com/a\">https://sourcemeta.com/" - "a</a> b&gt;</p>\n"); -} - -TEST(autolink_uri_backslash_is_literal) { - const auto result{ - sourcemeta::core::markdown_to_html("<https://sourcemeta.com/\\_>")}; - EXPECT_EQ(result, "<p><a " - "href=\"https://sourcemeta.com/%5C_\">https://" - "sourcemeta.com/\\_</a></p>\n"); -} - -TEST(autolink_email) { - const auto result{ - sourcemeta::core::markdown_to_html("<team@sourcemeta.com>")}; - EXPECT_EQ( - result, - "<p><a " - "href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a></p>\n"); -} - -TEST(autolink_email_with_dots_plus_and_hyphen) { - const auto result{sourcemeta::core::markdown_to_html( - "<first.last+tag@sub-domain.sourcemeta.com>")}; - EXPECT_EQ(result, "<p><a " - "href=\"mailto:first.last+tag@sub-domain.sourcemeta.com\">" - "first.last+tag@sub-domain.sourcemeta.com</a></p>\n"); -} - -TEST(autolink_email_with_backslash_is_not_autolink) { - const auto result{ - sourcemeta::core::markdown_to_html("<team\\@sourcemeta.com>")}; - EXPECT_EQ( - result, - "<p>&lt;<a " - "href=\"mailto:team@sourcemeta.com\">team@sourcemeta.com</a>&gt;</p>\n"); -} - -TEST(autolink_blank_or_trailing_space_angle_brackets) { - const auto result{ - sourcemeta::core::markdown_to_html("< >\n\n<https://sourcemeta.com >")}; - EXPECT_EQ( - result, - "<p>&lt; &gt;</p>\n" - "<p>&lt;<a href=\"https://sourcemeta.com\">https://sourcemeta.com</a> " - "&gt;</p>\n"); -} - -TEST(autolink_without_scheme_is_not_autolink) { - const auto result{sourcemeta::core::markdown_to_html("<sourcemeta.com>")}; - EXPECT_EQ(result, "<p>&lt;sourcemeta.com&gt;</p>\n"); -} - -TEST(hard_break_with_many_spaces) { - const auto result{sourcemeta::core::markdown_to_html("one \ntwo")}; - EXPECT_EQ(result, "<p>one<br />\n" - "two</p>\n"); -} - -TEST(hard_break_leading_spaces_on_next_line_ignored) { - const auto result{sourcemeta::core::markdown_to_html("one \n two")}; - EXPECT_EQ(result, "<p>one<br />\n" - "two</p>\n"); -} - -TEST(hard_break_inside_emphasis) { - const auto result{ - sourcemeta::core::markdown_to_html("_one \ntwo_\n\n__one\\\ntwo__")}; - EXPECT_EQ(result, "<p><em>one<br />\n" - "two</em></p>\n" - "<p><strong>one<br />\n" - "two</strong></p>\n"); -} - -TEST(hard_break_not_inside_code_span) { - const auto result{sourcemeta::core::markdown_to_html("`one \ntwo`")}; - EXPECT_EQ(result, "<p><code>one two</code></p>\n"); -} - -TEST(hard_break_not_at_end_of_block) { - const auto result{ - sourcemeta::core::markdown_to_html("text\\\n\ntext \n\n## title\\")}; - EXPECT_EQ(result, "<p>text\\</p>\n" - "<p>text</p>\n" - "<h2>title\\</h2>\n"); -} - -TEST(soft_break_surrounding_spaces_stripped) { - const auto result{sourcemeta::core::markdown_to_html("one \n two")}; - EXPECT_EQ(result, "<p>one\n" - "two</p>\n"); -} - -TEST(text_with_special_characters) { - const auto result{ - sourcemeta::core::markdown_to_html("cost: $5 + 10% ; done?")}; - EXPECT_EQ(result, "<p>cost: $5 + 10% ; done?</p>\n"); -} - -TEST(text_with_greek_characters) { - const auto result{sourcemeta::core::markdown_to_html("λέξη και φράση")}; - EXPECT_EQ(result, - "<p>\xce\xbb\xce\xad\xce\xbe\xce\xb7 \xce\xba\xce\xb1\xce\xb9 " - "\xcf\x86\xcf\x81\xce\xac\xcf\x83\xce\xb7</p>\n"); -} - -TEST(text_internal_spaces_preserved) { - const auto result{sourcemeta::core::markdown_to_html("wide gap")}; - EXPECT_EQ(result, "<p>wide gap</p>\n"); -} diff --git a/test/markdown/markdown_line_breaks_test.cc b/test/markdown/markdown_line_breaks_test.cc new file mode 100644 index 0000000000..fd024fbf67 --- /dev/null +++ b/test/markdown/markdown_line_breaks_test.cc @@ -0,0 +1,58 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(soft_line_break) { + const auto result{sourcemeta::core::markdown_to_html("line one\nline two")}; + EXPECT_EQ(result, "<p>line one\nline two</p>\n"); +} + +TEST(hard_line_break_with_two_spaces) { + const auto result{sourcemeta::core::markdown_to_html("line one \nline two")}; + EXPECT_EQ(result, "<p>line one<br />\nline two</p>\n"); +} + +TEST(hard_line_break_with_backslash) { + const auto result{sourcemeta::core::markdown_to_html("line one\\\nline two")}; + EXPECT_EQ(result, "<p>line one<br />\nline two</p>\n"); +} + +TEST(hard_break_with_many_spaces) { + const auto result{sourcemeta::core::markdown_to_html("one \ntwo")}; + EXPECT_EQ(result, "<p>one<br />\n" + "two</p>\n"); +} + +TEST(hard_break_leading_spaces_on_next_line_ignored) { + const auto result{sourcemeta::core::markdown_to_html("one \n two")}; + EXPECT_EQ(result, "<p>one<br />\n" + "two</p>\n"); +} + +TEST(hard_break_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_one \ntwo_\n\n__one\\\ntwo__")}; + EXPECT_EQ(result, "<p><em>one<br />\n" + "two</em></p>\n" + "<p><strong>one<br />\n" + "two</strong></p>\n"); +} + +TEST(hard_break_not_inside_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`one \ntwo`")}; + EXPECT_EQ(result, "<p><code>one two</code></p>\n"); +} + +TEST(hard_break_not_at_end_of_block) { + const auto result{ + sourcemeta::core::markdown_to_html("text\\\n\ntext \n\n## title\\")}; + EXPECT_EQ(result, "<p>text\\</p>\n" + "<p>text</p>\n" + "<h2>title\\</h2>\n"); +} + +TEST(soft_break_surrounding_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html("one \n two")}; + EXPECT_EQ(result, "<p>one\n" + "two</p>\n"); +} diff --git a/test/markdown/markdown_links_test.cc b/test/markdown/markdown_links_test.cc new file mode 100644 index 0000000000..682a6fa1a7 --- /dev/null +++ b/test/markdown/markdown_links_test.cc @@ -0,0 +1,589 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(inline_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[click here](https://example.com)")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\">click here</a></p>\n"); +} + +TEST(inline_link_with_title) { + const auto result{sourcemeta::core::markdown_to_html( + "[click](https://example.com \"My Title\")")}; + EXPECT_EQ( + result, + "<p><a href=\"https://example.com\" title=\"My Title\">click</a></p>\n"); +} + +TEST(reference_link) { + const auto result{sourcemeta::core::markdown_to_html( + "[click][ref]\n\n[ref]: https://example.com")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\">click</a></p>\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, + "<p><a href=\"https://example.com\" title=\"Title\">click</a></p>\n"); +} + +TEST(collapsed_reference_link) { + const auto result{sourcemeta::core::markdown_to_html( + "[example][]\n\n[example]: https://example.com")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\">example</a></p>\n"); +} + +TEST(shortcut_reference_link) { + const auto result{sourcemeta::core::markdown_to_html( + "[example]\n\n[example]: https://example.com")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\">example</a></p>\n"); +} + +TEST(link_with_emphasis_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "[**bold link**](https://example.com)")}; + EXPECT_EQ(result, "<p><a href=\"https://example.com\"><strong>bold " + "link</strong></a></p>\n"); +} + +TEST(code_inside_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[`code`](https://example.com)")}; + EXPECT_EQ(result, + "<p><a href=\"https://example.com\"><code>code</code></a></p>\n"); +} + +TEST(link_with_special_chars_in_url) { + const auto result{sourcemeta::core::markdown_to_html( + "[search](https://example.com/q?a=1&b=2)")}; + EXPECT_EQ( + result, + "<p><a href=\"https://example.com/q?a=1&amp;b=2\">search</a></p>\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, + "<p><a href=\"/manual\" title=\"The manual\">docs</a></p>\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, "<p><a href=\"/manual\" title=\"Manual\">docs</a></p>\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, "<p><a href=\"/manual\" title=\"\n" + "first\n" + "second\n" + "\">docs</a></p>\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, "<p>[docs]: /manual &quot;first</p>\n" + "<p>second&quot;</p>\n" + "<p>[docs]</p>\n"); +} + +TEST(link_reference_definition_destination_on_next_line) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]:\n /manual\n\n[docs]")}; + EXPECT_EQ(result, "<p><a href=\"/manual\">docs</a></p>\n"); +} + +TEST(link_reference_definition_missing_destination) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]:\n\ntext [docs]")}; + EXPECT_EQ(result, "<p>[docs]:</p>\n" + "<p>text [docs]</p>\n"); +} + +TEST(link_reference_definition_empty_angle_destination) { + const auto result{sourcemeta::core::markdown_to_html("[docs]: <>\n\n[docs]")}; + EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); +} + +TEST(link_reference_definition_title_without_separating_space) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual\"title\"\n\n[docs]")}; + EXPECT_EQ(result, "<p><a href=\"/manual%22title%22\">docs</a></p>\n"); +} + +TEST(link_reference_definition_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]: /a\\b\\#c 'it\\'s'\n\n[docs]")}; + EXPECT_EQ(result, "<p><a href=\"/a%5Cb#c\" title=\"it's\">docs</a></p>\n"); +} + +TEST(link_reference_definition_after_its_use) { + const auto result{ + sourcemeta::core::markdown_to_html("See [docs].\n\n[docs]: /manual")}; + EXPECT_EQ(result, "<p>See <a href=\"/manual\">docs</a>.</p>\n"); +} + +TEST(link_reference_definition_first_one_wins) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /one\n\n[docs]: /two")}; + EXPECT_EQ(result, "<p><a href=\"/one\">docs</a></p>\n"); +} + +TEST(link_reference_definition_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[DoCs]: /manual\n\n[dOcS]")}; + EXPECT_EQ(result, "<p><a href=\"/manual\">dOcS</a></p>\n"); +} + +TEST(link_reference_definition_label_full_case_folding) { + const auto result{ + sourcemeta::core::markdown_to_html("[STRASSE]: /road\n\n[straße]")}; + EXPECT_EQ(result, "<p><a href=\"/road\">stra\xc3\x9f" + "e</a></p>\n"); +} + +TEST(link_reference_definition_alone_renders_nothing) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual'")}; + EXPECT_EQ(result, ""); +} + +TEST(link_reference_definition_with_trailing_text_is_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual 'Manual' trailing")}; + EXPECT_EQ(result, "<p>[docs]: /manual 'Manual' trailing</p>\n"); +} + +TEST(link_reference_definition_title_with_trailing_text_on_next_line) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]: /manual\n'Manual' trailing\n\n[docs]")}; + EXPECT_EQ(result, "<p>'Manual' trailing</p>\n" + "<p><a href=\"/manual\" title=\"Manual\">docs</a></p>\n"); +} + +TEST(link_reference_definition_indented_four_spaces_is_code) { + const auto result{ + sourcemeta::core::markdown_to_html(" [docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "<pre><code>[docs]: /manual\n" + "</code></pre>\n" + "<p>[docs]</p>\n"); +} + +TEST(link_reference_definition_inside_fenced_code_is_content) { + const auto result{sourcemeta::core::markdown_to_html( + "~~~\n[docs]: /manual\n~~~\n\n[docs]")}; + EXPECT_EQ(result, "<pre><code>[docs]: /manual\n" + "</code></pre>\n" + "<p>[docs]</p>\n"); +} + +TEST(link_reference_definition_cannot_interrupt_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("Intro\n[docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "<p>Intro\n" + "[docs]: /manual</p>\n" + "<p>[docs]</p>\n"); +} + +TEST(link_reference_definition_between_heading_and_blockquote) { + const auto result{sourcemeta::core::markdown_to_html( + "## [Docs]\n[docs]: /manual\n> quoted")}; + EXPECT_EQ(result, "<h2><a href=\"/manual\">Docs</a></h2>\n" + "<blockquote>\n" + "<p>quoted</p>\n" + "</blockquote>\n"); +} + +TEST(link_reference_definition_followed_by_dashes) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]: /manual\n---\n[docs]")}; + EXPECT_EQ(result, "<p>---\n" + "<a href=\"/manual\">docs</a></p>\n"); +} + +TEST(link_reference_definitions_consecutive) { + const auto result{sourcemeta::core::markdown_to_html( + "[one]: /1 'One'\n[two]: /2\n 'Two'\n[three]: </3>\n\n[one] [two] " + "[three]")}; + EXPECT_EQ(result, "<p><a href=\"/1\" title=\"One\">one</a> <a href=\"/2\" " + "title=\"Two\">two</a> <a href=\"/3\">three</a></p>\n"); +} + +TEST(link_reference_definition_inside_blockquote_is_global) { + const auto result{ + sourcemeta::core::markdown_to_html("> [docs]: /manual\n\n[docs]")}; + EXPECT_EQ(result, "<blockquote>\n" + "</blockquote>\n" + "<p><a href=\"/manual\">docs</a></p>\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, "<p><a href=\"/manual\">The long label</a></p>\n"); +} + +TEST(link_reference_definition_empty_label_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[]: /manual\n\n[] text")}; + EXPECT_EQ(result, "<p>[]: /manual</p>\n" + "<p>[] text</p>\n"); +} + +TEST(link_reference_definition_whitespace_label_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[ ]: /manual\n\n[ ]")}; + EXPECT_EQ(result, "<p>[ ]: /manual</p>\n" + "<p>[ ]</p>\n"); +} + +TEST(link_destination_single_space) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( )")}; + EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); +} + +TEST(link_destination_empty_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( <> )")}; + EXPECT_EQ(result, "<p><a href=\"\">docs</a></p>\n"); +} + +TEST(link_destination_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs](<a b c>)")}; + EXPECT_EQ(result, "<p><a href=\"a%20b%20c\">docs</a></p>\n"); +} + +TEST(link_destination_with_space_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a b)")}; + EXPECT_EQ(result, "<p>[docs](/a b)</p>\n"); +} + +TEST(link_destination_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a\n/b)")}; + EXPECT_EQ(result, "<p>[docs](/a\n" + "/b)</p>\n"); +} + +TEST(link_destination_angle_brackets_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](</a\nb>)")}; + EXPECT_EQ(result, "<p>[docs](&lt;/a\n" + "b&gt;)</p>\n"); +} + +TEST(link_destination_angle_brackets_with_closing_paren) { + const auto result{sourcemeta::core::markdown_to_html("[docs](<a)b>)")}; + EXPECT_EQ(result, "<p><a href=\"a)b\">docs</a></p>\n"); +} + +TEST(link_destination_unmatched_angle_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[docs](<a\\>)")}; + EXPECT_EQ(result, "<p>[docs](&lt;a&gt;)</p>\n"); +} + +TEST(link_destination_invalid_angle_bracket_forms) { + const auto result{sourcemeta::core::markdown_to_html("[x](<y)z\n[x](<y>z)")}; + EXPECT_EQ(result, "<p>[x](&lt;y)z\n" + "[x](<!-- raw HTML omitted -->z)</p>\n"); +} + +TEST(link_destination_escaped_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](\\(a\\)b)")}; + EXPECT_EQ(result, "<p><a href=\"(a)b\">docs</a></p>\n"); +} + +TEST(link_destination_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)e)")}; + EXPECT_EQ(result, "<p><a href=\"a(b(c)d)e\">docs</a></p>\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, "<p>[docs](a(b(c)d)</p>\n"); +} + +TEST(link_destination_escaped_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\(b\\(c)")}; + EXPECT_EQ(result, "<p><a href=\"a(b(c\">docs</a></p>\n"); +} + +TEST(link_destination_angle_brackets_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](<a(b(c>)")}; + EXPECT_EQ(result, "<p><a href=\"a(b(c\">docs</a></p>\n"); +} + +TEST(link_destination_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\]\\!)")}; + EXPECT_EQ(result, "<p><a href=\"a%5D!\">docs</a></p>\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, + "<p><a href=\"#top\">x</a></p>\n" + "<p><a href=\"https://sourcemeta.com/?q=1#top\">y</a></p>\n"); +} + +TEST(link_destination_backslash_before_letter) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\zb)")}; + EXPECT_EQ(result, "<p><a href=\"a%5Czb\">docs</a></p>\n"); +} + +TEST(link_destination_percent_encoding_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](caf%C3%A9&eacute;)")}; + EXPECT_EQ(result, "<p><a href=\"caf%C3%A9%C3%A9\">docs</a></p>\n"); +} + +TEST(link_destination_that_looks_like_title) { + const auto result{sourcemeta::core::markdown_to_html("[docs]('Title')")}; + EXPECT_EQ(result, "<p><a href=\"&#x27;Title&#x27;\">docs</a></p>\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, + "<p><a href=\"/x\" title=\"one\">a</a> <a href=\"/y\" " + "title=\"two\">b</a> <a href=\"/z\" title=\"three\">c</a></p>\n"); +} + +TEST(link_title_escaped_quotes_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x 'it\\'s &amp; more')")}; + EXPECT_EQ(result, + "<p><a href=\"/x\" title=\"it's &amp; more\">docs</a></p>\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, "<p><a href=\"/x%C2%A0&#x27;title&#x27;\">docs</a></p>\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, "<p>[docs](/x 'a'b')</p>\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, "<p><a href=\"/x\" title=\"with 'single' and " + "&quot;double&quot;\">docs</a></p>\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, "<p><a href=\"/x\" title=\"title\">docs</a></p>\n"); +} + +TEST(link_spaces_before_parentheses_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs] (/x)")}; + EXPECT_EQ(result, "<p>[docs] (/x)</p>\n"); +} + +TEST(link_text_with_balanced_brackets) { + const auto result{sourcemeta::core::markdown_to_html("[a [b] [c [d]]](/x)")}; + EXPECT_EQ(result, "<p><a href=\"/x\">a [b] [c [d]]</a></p>\n"); +} + +TEST(link_text_with_unbalanced_closing_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a] b](/x)")}; + EXPECT_EQ(result, "<p>[a] b](/x)</p>\n"); +} + +TEST(link_text_with_unbalanced_opening_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a [b](/x)")}; + EXPECT_EQ(result, "<p>[a <a href=\"/x\">b</a></p>\n"); +} + +TEST(link_text_with_escaped_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a \\] b](/x)")}; + EXPECT_EQ(result, "<p><a href=\"/x\">a ] b</a></p>\n"); +} + +TEST(link_text_with_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("[_a_ __b__ `c` ~~d~~](/x)")}; + EXPECT_EQ(result, "<p><a href=\"/x\"><em>a</em> <strong>b</strong> " + "<code>c</code> <del>d</del></a></p>\n"); +} + +TEST(link_text_with_image) { + const auto result{ + sourcemeta::core::markdown_to_html("[![logo](/logo.png 'Logo')](/home)")}; + EXPECT_EQ(result, "<p><a href=\"/home\"><img src=\"/logo.png\" alt=\"logo\" " + "title=\"Logo\" /></a></p>\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, "<p>[a <a href=\"/inner\">b</a> c](/outer)</p>\n"); +} + +TEST(link_inside_emphasis_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[a _[b](/inner)_](/outer)")}; + EXPECT_EQ(result, "<p>[a <em><a href=\"/inner\">b</a></em>](/outer)</p>\n"); +} + +TEST(link_text_brackets_take_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_[a_](/x)")}; + EXPECT_EQ(result, "<p>_<a href=\"/x\">a_</a></p>\n"); +} + +TEST(link_brackets_take_precedence_over_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("[a _b](/x_)")}; + EXPECT_EQ(result, "<p><a href=\"/x_\">a _b</a></p>\n"); +} + +TEST(link_code_span_takes_precedence) { + const auto result{sourcemeta::core::markdown_to_html("[a`](/x)`")}; + EXPECT_EQ(result, "<p>[a<code>](/x)</code></p>\n"); +} + +TEST(link_autolink_takes_precedence) { + const auto result{sourcemeta::core::markdown_to_html( + "[a<https://sourcemeta.com/?q=](/x)>")}; + EXPECT_EQ(result, "<p>[a<a " + "href=\"https://sourcemeta.com/?q=%5D(/x)\">https://" + "sourcemeta.com/?q=](/x)</a></p>\n"); +} + +TEST(reference_link_full) { + const auto result{sourcemeta::core::markdown_to_html( + "[the docs][manual]\n\n[manual]: /manual 'Manual'")}; + EXPECT_EQ(result, + "<p><a href=\"/manual\" title=\"Manual\">the docs</a></p>\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, "<p><a href=\"/ref\">a [b] c</a></p>\n"); +} + +TEST(reference_link_label_case_folding) { + const auto result{sourcemeta::core::markdown_to_html( + "[text][MaNuAl]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "<p><a href=\"/manual\">text</a></p>\n"); +} + +TEST(reference_link_label_titlecase_digraph_folding) { + const auto result{sourcemeta::core::markdown_to_html("[Dž]\n\n[dž]: /digraph")}; + EXPECT_EQ(result, "<p><a href=\"/digraph\">\xc7\x85</a></p>\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, "<p>[a] <a href=\"/ref\">ref</a></p>\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, "<p>[a]\n" + "<a href=\"/ref\">ref</a></p>\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, "<p><a href=\"/first\">a</a></p>\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, "<p>[a][ref*]</p>\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, "<p>[a][b]c]</p>\n" + "<p>[b]c]: /x</p>\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, "<p><a href=\"/x\">a</a></p>\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, "<p><a href=\"/x\">a\\b</a></p>\n"); +} + +TEST(reference_link_empty_brackets_are_literal) { + const auto result{sourcemeta::core::markdown_to_html("[][]\n\n[]: /x")}; + EXPECT_EQ(result, "<p>[][]</p>\n" + "<p>[]: /x</p>\n"); +} + +TEST(collapsed_reference_link_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[MANUAL][]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "<p><a href=\"/manual\">MANUAL</a></p>\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, "<p><a href=\"/manual\">manual</a>\n" + "[]</p>\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, "<p><a href=\"/manual\"><em>the</em> manual</a></p>\n"); +} + +TEST(shortcut_reference_link_escaped) { + const auto result{ + sourcemeta::core::markdown_to_html("\\[manual\\]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "<p>[manual]</p>\n"); +} + +TEST(shortcut_reference_link_label_with_underscore) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual_]: /manual\n\n_[manual_]")}; + EXPECT_EQ(result, "<p>_<a href=\"/manual\">manual_</a></p>\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, "<p><a href=\"/b\">a</a><a href=\"/c\">c</a></p>\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, "<p>[a]<a href=\"/c\">b</a></p>\n"); +} + +TEST(inline_link_takes_precedence_over_reference) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual]()\n\n[manual]: /manual")}; + EXPECT_EQ(result, "<p><a href=\"\">manual</a></p>\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, "<p><a href=\"/manual\">manual</a>(a b)</p>\n"); +} diff --git a/test/markdown/markdown_lists_test.cc b/test/markdown/markdown_lists_test.cc new file mode 100644 index 0000000000..c7cd696ec6 --- /dev/null +++ b/test/markdown/markdown_lists_test.cc @@ -0,0 +1,793 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(unordered_list_with_dashes) { + const auto result{ + sourcemeta::core::markdown_to_html("- one\n- two\n- three")}; + EXPECT_EQ(result, + "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); +} + +TEST(unordered_list_with_asterisks) { + const auto result{ + sourcemeta::core::markdown_to_html("* one\n* two\n* three")}; + EXPECT_EQ(result, + "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); +} + +TEST(unordered_list_with_plus) { + const auto result{ + sourcemeta::core::markdown_to_html("+ one\n+ two\n+ three")}; + EXPECT_EQ(result, + "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); +} + +TEST(nested_unordered_list) { + const auto result{sourcemeta::core::markdown_to_html( + "- outer\n - inner\n - inner2\n- outer2")}; + EXPECT_EQ(result, "<ul>\n<li>outer\n<ul>\n<li>inner</li>\n<li>inner2</li>\n" + "</ul>\n</li>\n<li>outer2</li>\n</ul>\n"); +} + +TEST(ordered_list) { + const auto result{ + sourcemeta::core::markdown_to_html("1. one\n2. two\n3. three")}; + EXPECT_EQ(result, + "<ol>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ol>\n"); +} + +TEST(ordered_list_starting_at_nonone) { + const auto result{ + sourcemeta::core::markdown_to_html("3. three\n4. four\n5. five")}; + EXPECT_EQ(result, + "<ol start=\"3\">\n<li>three</li>\n<li>four</li>\n<li>five</li>\n" + "</ol>\n"); +} + +TEST(nested_ordered_in_unordered) { + const auto result{sourcemeta::core::markdown_to_html( + "- item\n 1. sub one\n 2. sub two\n- item2")}; + EXPECT_EQ(result, "<ul>\n<li>item\n<ol>\n<li>sub one</li>\n<li>sub two</li>\n" + "</ol>\n</li>\n<li>item2</li>\n</ul>\n"); +} + +TEST(loose_list) { + const auto result{ + sourcemeta::core::markdown_to_html("- one\n\n- two\n\n- three")}; + EXPECT_EQ(result, "<ul>\n<li>\n<p>one</p>\n</li>\n<li>\n<p>two</p>\n</li>\n" + "<li>\n<p>three</p>\n</li>\n</ul>\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, "<ul>\n<li>\n<p>item:</p>\n<pre><code>code\n</code></pre>\n" + "</li>\n<li>\n<p>next</p>\n</li>\n</ul>\n"); +} + +TEST(deeply_nested_list) { + const auto result{ + sourcemeta::core::markdown_to_html("- a\n - b\n - c\n - d")}; + EXPECT_EQ(result, "<ul>\n<li>a\n<ul>\n<li>b\n<ul>\n<li>c\n<ul>\n<li>d</li>\n" + "</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\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, "<ol>\n<li>\n<p>first</p>\n<p>more text</p>\n</li>\n" + "<li>\n<p>second</p>\n</li>\n</ol>\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, "<ul>\n<li>\n<p>paragraph one</p>\n<p>paragraph two</p>\n" + "<p>paragraph three</p>\n</li>\n</ul>\n"); +} + +TEST(list_marker_takes_precedence_over_code_span) { + const auto result{sourcemeta::core::markdown_to_html("* `alpha\n* beta`")}; + EXPECT_EQ(result, "<ul>\n" + "<li>`alpha</li>\n" + "<li>beta`</li>\n" + "</ul>\n"); +} + +TEST(list_item_content_needs_marker_width_indentation) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "<p>two</p>\n"); +} + +TEST(list_item_content_with_marker_width_indentation) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>one</p>\n" + "<p>two</p>\n" + "</li>\n" + "</ul>\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, "<ol>\n" + "<li>one</li>\n" + "</ol>\n" + "<pre><code>two\n" + "</code></pre>\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, "<ol>\n" + "<li>\n" + "<p>one</p>\n" + "<p>two</p>\n" + "</li>\n" + "</ol>\n"); +} + +TEST(list_item_inside_nested_blockquotes) { + const auto result{ + sourcemeta::core::markdown_to_html("> > - one\n> >\n> > two")}; + EXPECT_EQ(result, "<blockquote>\n" + "<blockquote>\n" + "<ul>\n" + "<li>\n" + "<p>one</p>\n" + "<p>two</p>\n" + "</li>\n" + "</ul>\n" + "</blockquote>\n" + "</blockquote>\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, "<blockquote>\n" + "<blockquote>\n" + "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "<p>two</p>\n" + "</blockquote>\n" + "</blockquote>\n"); +} + +TEST(list_item_marker_needs_following_space) { + const auto result{sourcemeta::core::markdown_to_html("*one\n\n1)two")}; + EXPECT_EQ(result, "<p>*one</p>\n" + "<p>1)two</p>\n"); +} + +TEST(list_item_multiple_blank_lines_between_blocks) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n\n\n two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>one</p>\n" + "<p>two</p>\n" + "</li>\n" + "</ul>\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, "<ol start=\"2\">\n" + "<li>\n" + "<p>one</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "<p>two</p>\n" + "<blockquote>\n" + "<p>quote</p>\n" + "</blockquote>\n" + "</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<p>one</p>\n" + "<pre><code>code\n" + "\n" + "\n" + "more\n" + "</code></pre>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(ordered_list_nine_digit_number) { + const auto result{sourcemeta::core::markdown_to_html("999999999) max")}; + EXPECT_EQ(result, "<ol start=\"999999999\">\n" + "<li>max</li>\n" + "</ol>\n"); +} + +TEST(ordered_list_ten_digit_number_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("9999999999) over")}; + EXPECT_EQ(result, "<p>9999999999) over</p>\n"); +} + +TEST(ordered_list_starting_at_zero) { + const auto result{sourcemeta::core::markdown_to_html("0) zero")}; + EXPECT_EQ(result, "<ol start=\"0\">\n" + "<li>zero</li>\n" + "</ol>\n"); +} + +TEST(ordered_list_number_with_leading_zeros) { + const auto result{sourcemeta::core::markdown_to_html("0007. seven")}; + EXPECT_EQ(result, "<ol start=\"7\">\n" + "<li>seven</li>\n" + "</ol>\n"); +} + +TEST(ordered_list_negative_number_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("-5) negative")}; + EXPECT_EQ(result, "<p>-5) negative</p>\n"); +} + +TEST(list_item_indented_code_after_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("* text\n\n code")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>text</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</li>\n" + "</ul>\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, "<ol start=\"100\">\n" + "<li>\n" + "<p>text</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<pre><code>code\n" + "</code></pre>\n" + "<p>text</p>\n" + "<pre><code>more\n" + "</code></pre>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>\n" + "<pre><code> code\n" + "</code></pre>\n" + "<p>text</p>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>one</li>\n" + "<li>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</li>\n" + "</ul>\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, "<ol>\n" + "<li>one</li>\n" + "</ol>\n"); +} + +TEST(list_item_at_most_one_starting_blank_line) { + const auto result{sourcemeta::core::markdown_to_html("*\n\n one")}; + EXPECT_EQ(result, "<ul>\n" + "<li></li>\n" + "</ul>\n" + "<p>one</p>\n"); +} + +TEST(list_item_empty_between_items) { + const auto result{sourcemeta::core::markdown_to_html("* one\n*\n* two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "<li></li>\n" + "<li>two</li>\n" + "</ul>\n"); +} + +TEST(list_item_empty_with_trailing_spaces) { + const auto result{sourcemeta::core::markdown_to_html("* one\n* \n* two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "<li></li>\n" + "<li>two</li>\n" + "</ul>\n"); +} + +TEST(ordered_list_item_empty_between_items) { + const auto result{sourcemeta::core::markdown_to_html("1) one\n2)\n3) two")}; + EXPECT_EQ(result, "<ol>\n" + "<li>one</li>\n" + "<li></li>\n" + "<li>two</li>\n" + "</ol>\n"); +} + +TEST(list_consisting_of_single_empty_item) { + const auto result{sourcemeta::core::markdown_to_html("-")}; + EXPECT_EQ(result, "<ul>\n" + "<li></li>\n" + "</ul>\n"); +} + +TEST(list_item_empty_cannot_interrupt_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n-\n\ntext\n1)")}; + EXPECT_EQ(result, "<h2>text</h2>\n" + "<p>text\n" + "1)</p>\n"); +} + +TEST(list_item_indented_one_space) { + const auto result{sourcemeta::core::markdown_to_html( + " 1. one\n two\n\n code")}; + EXPECT_EQ(result, "<ol>\n" + "<li>\n" + "<p>one\n" + "two</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</li>\n" + "</ol>\n"); +} + +TEST(list_item_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" - one\n two")}; + EXPECT_EQ(result, "<pre><code>- one\n" + " two\n" + "</code></pre>\n"); +} + +TEST(list_item_lazy_continuation_lines) { + const auto result{ + sourcemeta::core::markdown_to_html(" * one\ntwo\n\n code")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>one\n" + "two</p>\n" + "<pre><code>code\n" + "</code></pre>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(list_item_partially_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html(" * one\n two\nthree")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one\n" + "two\n" + "three</li>\n" + "</ul>\n"); +} + +TEST(list_item_laziness_inside_nested_containers) { + const auto result{ + sourcemeta::core::markdown_to_html("> * > quoted\nlazy line")}; + EXPECT_EQ(result, "<blockquote>\n" + "<ul>\n" + "<li>\n" + "<blockquote>\n" + "<p>quoted\n" + "lazy line</p>\n" + "</blockquote>\n" + "</li>\n" + "</ul>\n" + "</blockquote>\n"); +} + +TEST(list_nested_with_sufficient_indentation) { + const auto result{sourcemeta::core::markdown_to_html("* a\n * b\n * c")}; + EXPECT_EQ(result, "<ul>\n" + "<li>a\n" + "<ul>\n" + "<li>b\n" + "<ul>\n" + "<li>c</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>a</li>\n" + "<li>b</li>\n" + "<li>c</li>\n" + "</ul>\n"); +} + +TEST(ordered_list_nested_needs_marker_width_indentation) { + const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")}; + EXPECT_EQ(result, "<ol start=\"100\">\n" + "<li>a\n" + "<ul>\n" + "<li>b</li>\n" + "</ul>\n" + "</li>\n" + "</ol>\n"); +} + +TEST(ordered_list_not_nested_with_insufficient_indentation) { + const auto result{sourcemeta::core::markdown_to_html("100. a\n * b")}; + EXPECT_EQ(result, "<ol start=\"100\">\n" + "<li>a\n" + "* b</li>\n" + "</ol>\n"); +} + +TEST(list_item_containing_only_list) { + const auto result{sourcemeta::core::markdown_to_html("+ + one")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(list_item_containing_nested_ordered_list) { + const auto result{sourcemeta::core::markdown_to_html("3) * 4) deep")}; + EXPECT_EQ(result, "<ol start=\"3\">\n" + "<li>\n" + "<ul>\n" + "<li>\n" + "<ol start=\"4\">\n" + "<li>deep</li>\n" + "</ol>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "</ol>\n"); +} + +TEST(list_item_containing_headings) { + const auto result{ + sourcemeta::core::markdown_to_html("* ## Title\n* Text\n ===\n more")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<h2>Title</h2>\n" + "</li>\n" + "<li>\n" + "<h1>Text</h1>\n" + "more</li>\n" + "</ul>\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, "<ul>\n" + "<li>one</li>\n" + "<li>two</li>\n" + "</ul>\n" + "<ul>\n" + "<li>three</li>\n" + "</ul>\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, "<ol>\n" + "<li>one</li>\n" + "<li>two</li>\n" + "</ol>\n" + "<ol start=\"3\">\n" + "<li>three</li>\n" + "</ol>\n"); +} + +TEST(list_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Text\n* one\n* two")}; + EXPECT_EQ(result, "<p>Text</p>\n" + "<ul>\n" + "<li>one</li>\n" + "<li>two</li>\n" + "</ul>\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, "<p>We counted\n" + "42. items in total</p>\n"); +} + +TEST(ordered_list_starting_at_one_interrupts_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("We counted\n1. items in total")}; + EXPECT_EQ(result, "<p>We counted</p>\n" + "<ol>\n" + "<li>items in total</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<p>one</p>\n" + "</li>\n" + "<li>\n" + "<p>two</p>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>a\n" + "<ul>\n" + "<li>\n" + "<p>b</p>\n" + "<p>c</p>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(list_separated_by_html_comment) { + const auto result{ + sourcemeta::core::markdown_to_html("* one\n\n<!-- split -->\n\n* two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "<!-- raw HTML omitted -->\n" + "<ul>\n" + "<li>two</li>\n" + "</ul>\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, "<ul>\n" + "<li>a</li>\n" + "<li>b</li>\n" + "<li>c</li>\n" + "<li>d</li>\n" + "<li>e</li>\n" + "</ul>\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, "<ul>\n" + "<li>a</li>\n" + "<li>b</li>\n" + "<li>c\n" + "<ul>\n" + "<li>d</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\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, "<ol>\n" + "<li>\n" + "<p>a</p>\n" + "</li>\n" + "<li>\n" + "<p>b</p>\n" + "<ol start=\"3\">\n" + "<li>c</li>\n" + "</ol>\n" + "</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<p>a</p>\n" + "</li>\n" + "<li>\n" + "<p>b</p>\n" + "</li>\n" + "<li>\n" + "<p>c</p>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(list_loose_with_empty_item) { + const auto result{sourcemeta::core::markdown_to_html("- a\n-\n\n- b")}; + EXPECT_EQ(result, "<ul>\n" + "<li>\n" + "<p>a</p>\n" + "</li>\n" + "<li></li>\n" + "<li>\n" + "<p>b</p>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>\n" + "<p>a</p>\n" + "<p>b</p>\n" + "</li>\n" + "<li>\n" + "<p>c</p>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>\n" + "<p>a</p>\n" + "</li>\n" + "<li>\n" + "<p>b</p>\n" + "</li>\n" + "</ul>\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, "<ul>\n" + "<li>a</li>\n" + "<li>\n" + "<pre><code>x\n" + "\n" + "</code></pre>\n" + "</li>\n" + "<li>b</li>\n" + "</ul>\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, "<ul>\n" + "<li>a\n" + "<ul>\n" + "<li>\n" + "<p>b</p>\n" + "</li>\n" + "<li>\n" + "<p>c</p>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "<li>d</li>\n" + "</ul>\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, "<ul>\n" + "<li>a\n" + "<blockquote>\n" + "<p>b</p>\n" + "<p>c</p>\n" + "</blockquote>\n" + "</li>\n" + "<li>d</li>\n" + "</ul>\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, "<ul>\n" + "<li>a\n" + "<blockquote>\n" + "<p>b</p>\n" + "</blockquote>\n" + "<pre><code>c\n" + "</code></pre>\n" + "</li>\n" + "<li>d</li>\n" + "</ul>\n"); +} + +TEST(list_single_tight_nested_ordered_item) { + const auto result{sourcemeta::core::markdown_to_html("1. a\n 1. b")}; + EXPECT_EQ(result, "<ol>\n" + "<li>a\n" + "<ol>\n" + "<li>b</li>\n" + "</ol>\n" + "</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<pre><code>x\n" + "</code></pre>\n" + "<p>y</p>\n" + "</li>\n" + "</ul>\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, "<ol>\n" + "<li>\n" + "<p>a</p>\n" + "<ul>\n" + "<li>b</li>\n" + "</ul>\n" + "<p>c</p>\n" + "</li>\n" + "</ol>\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, "<ul>\n" + "<li>\n" + "<p>a</p>\n" + "<ol>\n" + "<li>b</li>\n" + "<li>c</li>\n" + "</ol>\n" + "</li>\n" + "<li>\n" + "<p>d</p>\n" + "</li>\n" + "</ul>\n"); +} diff --git a/test/markdown/markdown_paragraphs_test.cc b/test/markdown/markdown_paragraphs_test.cc new file mode 100644 index 0000000000..37ce6a50a6 --- /dev/null +++ b/test/markdown/markdown_paragraphs_test.cc @@ -0,0 +1,76 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(simple_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("Hello world")}; + EXPECT_EQ(result, "<p>Hello world</p>\n"); +} + +TEST(multiple_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html( + "First paragraph\n\nSecond paragraph")}; + EXPECT_EQ(result, "<p>First paragraph</p>\n<p>Second paragraph</p>\n"); +} + +TEST(three_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html("One\n\nTwo\n\nThree")}; + EXPECT_EQ(result, "<p>One</p>\n<p>Two</p>\n<p>Three</p>\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(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, "<p>Use <code>foo()</code> from " + "<a href=\"https://docs.com\">the docs</a>.</p>\n"); +} + +TEST(paragraph_leading_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html(" one\n two")}; + EXPECT_EQ(result, "<p>one\n" + "two</p>\n"); +} + +TEST(paragraph_continuation_leading_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html( + "one\n two\n three")}; + EXPECT_EQ(result, "<p>one\n" + "two\n" + "three</p>\n"); +} + +TEST(paragraph_first_line_indented_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; + EXPECT_EQ(result, "<p>first\n" + "second</p>\n"); +} + +TEST(paragraph_first_line_indented_five_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" first\nsecond")}; + EXPECT_EQ(result, "<pre><code> first\n" + "</code></pre>\n" + "<p>second</p>\n"); +} + +TEST(paragraph_final_trailing_spaces_stripped) { + const auto result{sourcemeta::core::markdown_to_html("one \ntwo ")}; + EXPECT_EQ(result, "<p>one<br />\n" + "two</p>\n"); +} + +TEST(blank_lines_at_start_and_end_ignored) { + const auto result{ + sourcemeta::core::markdown_to_html("\n \none\n\n \n## two\n\n\n")}; + EXPECT_EQ(result, "<p>one</p>\n" + "<h2>two</h2>\n"); +} diff --git a/test/markdown/markdown_pathological_test.cc b/test/markdown/markdown_pathological_test.cc index 95aa906f17..045c3e64e3 100644 --- a/test/markdown/markdown_pathological_test.cc +++ b/test/markdown/markdown_pathological_test.cc @@ -6,8 +6,6 @@ #include <string> // std::string, std::to_string #include <string_view> // std::string_view -using namespace std::literals::string_view_literals; - static auto repeat(const std::string_view pattern, const std::size_t count) -> std::string { std::string result; @@ -218,11 +216,6 @@ TEST(table_with_many_columns) { "</tr>\n</tbody>\n</table>\n"); } -TEST(nul_characters_are_replaced) { - const auto result{sourcemeta::core::markdown_to_html("one\0two\0"sv)}; - EXPECT_EQ(result, "<p>one\xef\xbf\xbdtwo\xef\xbf\xbd</p>\n"); -} - TEST(link_reference_label_of_one_thousand_characters) { const std::string label(1000, 'y'); const auto result{sourcemeta::core::markdown_to_html( diff --git a/test/markdown/markdown_safety_test.cc b/test/markdown/markdown_safety_test.cc index 93e57f94da..0eecb7fd87 100644 --- a/test/markdown/markdown_safety_test.cc +++ b/test/markdown/markdown_safety_test.cc @@ -2,6 +2,73 @@ #include <sourcemeta/core/test.h> +TEST(safe_mode_renders_plain_content) { + const auto result{ + sourcemeta::core::markdown_to_html("Hello **world**", true)}; + EXPECT_EQ(result, "<p>Hello <strong>world</strong></p>\n"); +} + +TEST(safe_mode_omits_raw_html) { + const auto result{ + sourcemeta::core::markdown_to_html("<div onclick=\"x\">hi</div>", true)}; + EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); +} + +TEST(default_omits_raw_html) { + const auto result{ + sourcemeta::core::markdown_to_html("<div onclick=\"x\">hi</div>")}; + EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); +} + +TEST(default_strips_dangerous_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[click](javascript:alert(1))")}; + EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); +} + +TEST(inline_html_is_sanitized) { + const auto result{sourcemeta::core::markdown_to_html("Hello <em>world</em>")}; + EXPECT_EQ( + result, + "<p>Hello <!-- raw HTML omitted -->world<!-- raw HTML omitted --></p>\n"); +} + +TEST(raw_html_block_is_sanitized) { + const auto result{ + sourcemeta::core::markdown_to_html("<div class=\"foo\">bar</div>")}; + EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); +} + +TEST(script_tag_is_sanitized) { + const auto result{ + sourcemeta::core::markdown_to_html("<script>alert('xss')</script>")}; + EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); +} + +TEST(iframe_is_sanitized) { + const auto result{sourcemeta::core::markdown_to_html( + "<iframe src=\"https://evil.com\"></iframe>")}; + EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); +} + +TEST(javascript_link_is_sanitized) { + const auto result{ + sourcemeta::core::markdown_to_html("[click](javascript:alert('xss'))")}; + EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); +} + +TEST(vbscript_link_is_sanitized) { + const auto result{ + sourcemeta::core::markdown_to_html("[click](vbscript:MsgBox)")}; + EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); +} + +TEST(data_uri_link_is_sanitized) { + const auto result{sourcemeta::core::markdown_to_html( + "[click](data:text/html,<h1>hi</h1>)")}; + EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); +} + TEST(safe_mode_strips_mixed_case_javascript_scheme) { const auto result{ sourcemeta::core::markdown_to_html("[click](JaVaScRiPt:void(0))")}; diff --git a/test/markdown/markdown_strikethrough_test.cc b/test/markdown/markdown_strikethrough_test.cc new file mode 100644 index 0000000000..ad176606a5 --- /dev/null +++ b/test/markdown/markdown_strikethrough_test.cc @@ -0,0 +1,86 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(strikethrough) { + const auto result{sourcemeta::core::markdown_to_html("~~deleted~~")}; + EXPECT_EQ(result, "<p><del>deleted</del></p>\n"); +} + +TEST(strikethrough_with_other_formatting) { + const auto result{ + sourcemeta::core::markdown_to_html("~~deleted **bold** text~~")}; + EXPECT_EQ(result, "<p><del>deleted <strong>bold</strong> text</del></p>\n"); +} + +TEST(strikethrough_in_sentence) { + const auto result{ + sourcemeta::core::markdown_to_html("This is ~~wrong~~ correct")}; + EXPECT_EQ(result, "<p>This is <del>wrong</del> correct</p>\n"); +} + +TEST(strikethrough_inside_table_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| h |\n| - |\n| ~~old~~ |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td><del>old</del></td>\n</tr>\n" + "</tbody>\n</table>\n"); +} + +TEST(strikethrough_with_code) { + const auto result{sourcemeta::core::markdown_to_html("~~use `old_func()`~~")}; + EXPECT_EQ(result, "<p><del>use <code>old_func()</code></del></p>\n"); +} + +TEST(strikethrough_with_link) { + const auto result{ + sourcemeta::core::markdown_to_html("~~[removed](https://example.com)~~")}; + EXPECT_EQ(result, + "<p><del><a href=\"https://example.com\">removed</a></del></p>\n"); +} + +TEST(strikethrough_bold_italic_combined) { + const auto result{sourcemeta::core::markdown_to_html("~~***all three***~~")}; + EXPECT_EQ(result, "<p><del><em><strong>all three</strong></em></del></p>\n"); +} + +TEST(single_tilde_is_not_strikethrough) { + const auto result{sourcemeta::core::markdown_to_html("~not deleted~")}; + EXPECT_EQ(result, "<p>~not deleted~</p>\n"); +} + +TEST(strikethrough_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")}; + EXPECT_EQ(result, "<p><del>one\n" + "two</del></p>\n"); +} + +TEST(strikethrough_triple_tildes_inline) { + const auto result{sourcemeta::core::markdown_to_html("x ~~~y~~~ z")}; + EXPECT_EQ(result, "<p>x ~~~y~~~ z</p>\n"); +} + +TEST(strikethrough_unclosed) { + const auto result{sourcemeta::core::markdown_to_html("x ~~y")}; + EXPECT_EQ(result, "<p>x ~~y</p>\n"); +} + +TEST(strikethrough_with_surrounding_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x ~~ y ~~ z")}; + EXPECT_EQ(result, "<p>x ~~ y ~~ z</p>\n"); +} + +TEST(strikethrough_and_emphasis_overlap) { + const auto result{sourcemeta::core::markdown_to_html("~~_x~~_")}; + EXPECT_EQ(result, "<p><del>_x</del>_</p>\n"); +} + +TEST(strikethrough_escaped_closing) { + const auto result{sourcemeta::core::markdown_to_html("~~x\\~~")}; + EXPECT_EQ(result, "<p>~~x~~</p>\n"); +} + +TEST(strikethrough_inside_link_text) { + const auto result{sourcemeta::core::markdown_to_html("[~~old~~ new](/x)")}; + EXPECT_EQ(result, "<p><a href=\"/x\"><del>old</del> new</a></p>\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 <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(simple_table) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b |\n| - | - |\n| 1 | 2 |")}; + EXPECT_EQ( + result, + "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td>1</td>\n<td>2</td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(table_with_alignment) { + const auto result{sourcemeta::core::markdown_to_html( + "| left | center | right |\n| :--- | :---: | ---: |\n| a | b | c |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n" + "<th align=\"left\">left</th>\n" + "<th align=\"center\">center</th>\n" + "<th align=\"right\">right</th>\n" + "</tr>\n</thead>\n<tbody>\n<tr>\n" + "<td align=\"left\">a</td>\n" + "<td align=\"center\">b</td>\n" + "<td align=\"right\">c</td>\n" + "</tr>\n</tbody>\n</table>\n"); +} + +TEST(table_with_formatting_in_cells) { + const auto result{ + sourcemeta::core::markdown_to_html("| h |\n| - |\n| **bold** |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td><strong>bold</strong></td>\n</tr>\n" + "</tbody>\n</table>\n"); +} + +TEST(table_with_multiple_rows) { + const auto result{sourcemeta::core::markdown_to_html( + "| x | y |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |\n| 5 | 6 |")}; + EXPECT_EQ(result, + "<table>\n<thead>\n<tr>\n<th>x</th>\n<th>y</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td>1</td>\n<td>2</td>\n</tr>\n" + "<tr>\n<td>3</td>\n<td>4</td>\n</tr>\n" + "<tr>\n<td>5</td>\n<td>6</td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(table_with_pipe_in_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| h |\n| - |\n| a \\| b |")}; + EXPECT_EQ(result, + "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td>a | b</td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(link_inside_table_cell) { + const auto result{sourcemeta::core::markdown_to_html( + "| h |\n| - |\n| [click](https://example.com) |")}; + EXPECT_EQ(result, + "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td><a href=\"https://example.com\">click</a>" + "</td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(code_inside_table_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| h |\n| - |\n| `code` |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td><code>code</code></td>\n</tr>\n" + "</tbody>\n</table>\n"); +} + +TEST(empty_table_cells) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b |\n| - | - |\n| | |")}; + EXPECT_EQ(result, + "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(table_header_only) { + const auto result{sourcemeta::core::markdown_to_html("| a | b |\n| - | - |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</" + "thead>\n</table>\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, "<p>Here is a table:</p>\n" + "<table>\n<thead>\n<tr>\n<th>a</th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td>1</td>\n</tr>\n</tbody>\n</table>\n"); +} + +TEST(table_with_inline_code_in_header) { + const auto result{sourcemeta::core::markdown_to_html( + "| `key` | `value` |\n| - | - |\n| a | b |")}; + EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th><code>key</code></th>\n" + "<th><code>value</code></th>\n</tr>\n</thead>\n" + "<tbody>\n<tr>\n<td>a</td>\n<td>b</td>\n</tr>\n" + "</tbody>\n</table>\n"); +} + +TEST(table_escaped_pipes_inside_inline_content) { + const auto result{sourcemeta::core::markdown_to_html( + "| code |\n| ---- |\n| `a\\|b` |\n| _c\\|d_ |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>code</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td><code>a|b</code></td>\n" + "</tr>\n" + "<tr>\n" + "<td><em>c|d</em></td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\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, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "<th>b</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>1</td>\n" + "<td>2</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n" + "<p>after</p>\n"); +} + +TEST(table_ends_at_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n|---|\n| 1 |\n> quote")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>1</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n" + "<blockquote>\n" + "<p>quote</p>\n" + "</blockquote>\n"); +} + +TEST(table_line_without_pipes_is_row) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b |\n|---|---|\ntext row")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "<th>b</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>text row</td>\n" + "<td></td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(table_delimiter_count_mismatch_is_not_table) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b | c |\n| - | - |")}; + EXPECT_EQ(result, "<p>| a | b | c |\n" + "| - | - |</p>\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, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a</th>\n" + "<th>b</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>1</td>\n" + "<td></td>\n" + "</tr>\n" + "<tr>\n" + "<td>1</td>\n" + "<td>2</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(table_without_outer_pipes) { + const auto result{ + sourcemeta::core::markdown_to_html("x | y\n:-- | --:\n1 | 2")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th align=\"left\">x</th>\n" + "<th align=\"right\">y</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td align=\"left\">1</td>\n" + "<td align=\"right\">2</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(table_single_column_with_pipes) { + const auto result{ + sourcemeta::core::markdown_to_html("| only |\n| :-: |\n| one |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th align=\"center\">only</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td align=\"center\">one</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\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, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th align=\"left\">l</th>\n" + "<th align=\"right\">r</th>\n" + "<th align=\"center\">c</th>\n" + "<th>n</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td align=\"left\">1</td>\n" + "<td align=\"right\">2</td>\n" + "<td align=\"center\">3</td>\n" + "<td>4</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(table_delimiter_row_with_surrounding_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html("x|y\n --- | :---: ")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>x</th>\n" + "<th align=\"center\">y</th>\n" + "</tr>\n" + "</thead>\n" + "</table>\n"); +} + +TEST(table_header_cell_with_escaped_pipe) { + const auto result{ + sourcemeta::core::markdown_to_html("| a \\| b | c |\n| - | - |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>a | b</th>\n" + "<th>c</th>\n" + "</tr>\n" + "</thead>\n" + "</table>\n"); +} + +TEST(table_cell_with_reference_link) { + const auto result{sourcemeta::core::markdown_to_html( + "| [manual] |\n| --- |\n\n[manual]: /manual")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th><a href=\"/manual\">manual</a></th>\n" + "</tr>\n" + "</thead>\n" + "</table>\n"); +} + +TEST(table_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("intro\na | b\n- | -")}; + EXPECT_EQ(result, "<p>intro\n" + "a | b</p>\n" + "<ul>\n" + "<li>| -</li>\n" + "</ul>\n"); +} + +TEST(table_inside_list_item) { + const auto result{ + sourcemeta::core::markdown_to_html("* a | b\n - | -\n 1 | 2")}; + EXPECT_EQ(result, "<ul>\n" + "<li>a | b\n" + "<ul>\n" + "<li>| -\n" + "1 | 2</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(table_inside_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> a | b\n> - | -\n> 1 | 2")}; + EXPECT_EQ(result, "<blockquote>\n" + "<p>a | b</p>\n" + "<ul>\n" + "<li>| -\n" + "1 | 2</li>\n" + "</ul>\n" + "</blockquote>\n"); +} + +TEST(table_cell_with_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("| &lt;tag&gt; |\n| --- |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>&lt;tag&gt;</th>\n" + "</tr>\n" + "</thead>\n" + "</table>\n"); +} + +TEST(table_empty_header_cells) { + const auto result{ + sourcemeta::core::markdown_to_html("| | |\n|--|--|\n| x | y |")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th></th>\n" + "<th></th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>x</td>\n" + "<td>y</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} + +TEST(table_emphasis_does_not_span_cells) { + const auto result{ + sourcemeta::core::markdown_to_html("| _a | b_ |\n|---|---|")}; + EXPECT_EQ(result, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>_a</th>\n" + "<th>b_</th>\n" + "</tr>\n" + "</thead>\n" + "</table>\n"); +} + +TEST(table_invalid_delimiter_row_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("| a |\n| :-:x |")}; + EXPECT_EQ(result, "<p>| a |\n" + "| :-:x |</p>\n"); +} + +TEST(table_delimiter_count_mismatch_without_outer_pipes) { + const auto result{sourcemeta::core::markdown_to_html("x | y | z\n- | -")}; + EXPECT_EQ(result, "<p>x | y | z</p>\n" + "<ul>\n" + "<li>| -</li>\n" + "</ul>\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, "<table>\n" + "<thead>\n" + "<tr>\n" + "<th>x</th>\n" + "<th>y</th>\n" + "</tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr>\n" + "<td>1</td>\n" + "<td>2</td>\n" + "</tr>\n" + "</tbody>\n" + "</table>\n"); +} diff --git a/test/markdown/markdown_task_lists_test.cc b/test/markdown/markdown_task_lists_test.cc new file mode 100644 index 0000000000..026dc362d1 --- /dev/null +++ b/test/markdown/markdown_task_lists_test.cc @@ -0,0 +1,131 @@ +#include <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(task_list_unchecked) { + const auto result{sourcemeta::core::markdown_to_html("- [ ] todo")}; + EXPECT_EQ(result, + "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> todo</li>\n" + "</ul>\n"); +} + +TEST(task_list_checked) { + const auto result{sourcemeta::core::markdown_to_html("- [x] done")}; + EXPECT_EQ(result, + "<ul>\n<li><input type=\"checkbox\" checked=\"\" disabled=\"\" />" + " done</li>\n</ul>\n"); +} + +TEST(task_list_mixed) { + const auto result{sourcemeta::core::markdown_to_html( + "- [x] done\n- [ ] todo\n- [x] also done")}; + EXPECT_EQ( + result, + "<ul>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> done</li>\n" + "<li><input type=\"checkbox\" disabled=\"\" /> todo</li>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> also " + "done</li>\n" + "</ul>\n"); +} + +TEST(task_list_with_bold) { + const auto result{ + sourcemeta::core::markdown_to_html("- [ ] **important** task")}; + EXPECT_EQ(result, "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> " + "<strong>important</strong> task</li>\n</ul>\n"); +} + +TEST(task_list_with_link) { + const auto result{sourcemeta::core::markdown_to_html( + "- [x] see [docs](https://example.com)")}; + EXPECT_EQ(result, + "<ul>\n<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> " + "see <a href=\"https://example.com\">docs</a></li>\n</ul>\n"); +} + +TEST(task_list_with_code) { + const auto result{sourcemeta::core::markdown_to_html("- [ ] fix `bug`")}; + EXPECT_EQ(result, "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> " + "fix <code>bug</code></li>\n</ul>\n"); +} + +TEST(task_list_uppercase_marker) { + const auto result{sourcemeta::core::markdown_to_html("* [X] finished")}; + EXPECT_EQ(result, "<ul>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" " + "/> finished</li>\n" + "</ul>\n"); +} + +TEST(task_list_marker_needs_following_space) { + const auto result{sourcemeta::core::markdown_to_html("* [ ]pending")}; + EXPECT_EQ(result, "<ul>\n" + "<li>[ ]pending</li>\n" + "</ul>\n"); +} + +TEST(task_list_marker_with_other_character) { + const auto result{ + sourcemeta::core::markdown_to_html("* [-] partial\n* [x] complete")}; + EXPECT_EQ(result, "<ul>\n" + "<li>[-] partial</li>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" " + "/> complete</li>\n" + "</ul>\n"); +} + +TEST(task_list_nested) { + const auto result{sourcemeta::core::markdown_to_html( + "* [ ] parent\n * [x] child\n * [ ] grandchild")}; + EXPECT_EQ(result, + "<ul>\n" + "<li><input type=\"checkbox\" disabled=\"\" /> parent\n" + "<ul>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> child\n" + "<ul>\n" + "<li><input type=\"checkbox\" disabled=\"\" /> grandchild</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n"); +} + +TEST(task_list_in_ordered_list) { + const auto result{ + sourcemeta::core::markdown_to_html("1) [ ] first\n2) [X] second")}; + EXPECT_EQ( + result, + "<ol>\n" + "<li><input type=\"checkbox\" disabled=\"\" /> first</li>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> second</li>\n" + "</ol>\n"); +} + +TEST(task_list_marker_not_at_item_start) { + const auto result{sourcemeta::core::markdown_to_html("* text [ ] later")}; + EXPECT_EQ(result, "<ul>\n" + "<li>text [ ] later</li>\n" + "</ul>\n"); +} + +TEST(task_list_marker_alone) { + const auto result{sourcemeta::core::markdown_to_html("* [x]")}; + EXPECT_EQ(result, "<ul>\n" + "<li>[x]</li>\n" + "</ul>\n"); +} + +TEST(task_list_in_loose_list) { + const auto result{sourcemeta::core::markdown_to_html("* [x] a\n\n* [ ] b")}; + EXPECT_EQ(result, + "<ul>\n" + "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> \n" + "<p>a</p>\n" + "</li>\n" + "<li><input type=\"checkbox\" disabled=\"\" /> \n" + "<p>b</p>\n" + "</li>\n" + "</ul>\n"); +} diff --git a/test/markdown/markdown_test.cc b/test/markdown/markdown_test.cc deleted file mode 100644 index b56d9c8a71..0000000000 --- a/test/markdown/markdown_test.cc +++ /dev/null @@ -1,938 +0,0 @@ -#include <sourcemeta/core/markdown.h> - -#include <sourcemeta/core/test.h> - -TEST(simple_paragraph) { - const auto result{sourcemeta::core::markdown_to_html("Hello world")}; - EXPECT_EQ(result, "<p>Hello world</p>\n"); -} - -TEST(safe_mode_renders_plain_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Hello **world**", true)}; - EXPECT_EQ(result, "<p>Hello <strong>world</strong></p>\n"); -} - -TEST(safe_mode_omits_raw_html) { - const auto result{ - sourcemeta::core::markdown_to_html("<div onclick=\"x\">hi</div>", true)}; - EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); -} - -TEST(default_omits_raw_html) { - const auto result{ - sourcemeta::core::markdown_to_html("<div onclick=\"x\">hi</div>")}; - EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); -} - -TEST(default_strips_dangerous_link) { - const auto result{ - sourcemeta::core::markdown_to_html("[click](javascript:alert(1))")}; - EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); -} - -TEST(multiple_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html( - "First paragraph\n\nSecond paragraph")}; - EXPECT_EQ(result, "<p>First paragraph</p>\n<p>Second paragraph</p>\n"); -} - -TEST(three_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html("One\n\nTwo\n\nThree")}; - EXPECT_EQ(result, "<p>One</p>\n<p>Two</p>\n<p>Three</p>\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, "<p>line one\nline two</p>\n"); -} - -TEST(hard_line_break_with_two_spaces) { - const auto result{sourcemeta::core::markdown_to_html("line one \nline two")}; - EXPECT_EQ(result, "<p>line one<br />\nline two</p>\n"); -} - -TEST(hard_line_break_with_backslash) { - const auto result{sourcemeta::core::markdown_to_html("line one\\\nline two")}; - EXPECT_EQ(result, "<p>line one<br />\nline two</p>\n"); -} - -TEST(atx_heading_level_1) { - const auto result{sourcemeta::core::markdown_to_html("# Heading 1")}; - EXPECT_EQ(result, "<h1>Heading 1</h1>\n"); -} - -TEST(atx_heading_level_2) { - const auto result{sourcemeta::core::markdown_to_html("## Heading 2")}; - EXPECT_EQ(result, "<h2>Heading 2</h2>\n"); -} - -TEST(atx_heading_level_3) { - const auto result{sourcemeta::core::markdown_to_html("### Heading 3")}; - EXPECT_EQ(result, "<h3>Heading 3</h3>\n"); -} - -TEST(atx_heading_level_4) { - const auto result{sourcemeta::core::markdown_to_html("#### Heading 4")}; - EXPECT_EQ(result, "<h4>Heading 4</h4>\n"); -} - -TEST(atx_heading_level_5) { - const auto result{sourcemeta::core::markdown_to_html("##### Heading 5")}; - EXPECT_EQ(result, "<h5>Heading 5</h5>\n"); -} - -TEST(atx_heading_level_6) { - const auto result{sourcemeta::core::markdown_to_html("###### Heading 6")}; - EXPECT_EQ(result, "<h6>Heading 6</h6>\n"); -} - -TEST(atx_heading_with_closing_hashes) { - const auto result{sourcemeta::core::markdown_to_html("## Heading ##")}; - EXPECT_EQ(result, "<h2>Heading</h2>\n"); -} - -TEST(setext_heading_level_1) { - const auto result{sourcemeta::core::markdown_to_html("Heading 1\n=========")}; - EXPECT_EQ(result, "<h1>Heading 1</h1>\n"); -} - -TEST(setext_heading_level_2) { - const auto result{sourcemeta::core::markdown_to_html("Heading 2\n---------")}; - EXPECT_EQ(result, "<h2>Heading 2</h2>\n"); -} - -TEST(italic_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("*italic*")}; - EXPECT_EQ(result, "<p><em>italic</em></p>\n"); -} - -TEST(italic_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("_italic_")}; - EXPECT_EQ(result, "<p><em>italic</em></p>\n"); -} - -TEST(bold_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("**bold**")}; - EXPECT_EQ(result, "<p><strong>bold</strong></p>\n"); -} - -TEST(bold_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("__bold__")}; - EXPECT_EQ(result, "<p><strong>bold</strong></p>\n"); -} - -TEST(bold_and_italic) { - const auto result{sourcemeta::core::markdown_to_html("***bold italic***")}; - EXPECT_EQ(result, "<p><em><strong>bold italic</strong></em></p>\n"); -} - -TEST(bold_inside_italic) { - const auto result{ - sourcemeta::core::markdown_to_html("*this is **bold** inside italic*")}; - EXPECT_EQ(result, - "<p><em>this is <strong>bold</strong> inside italic</em></p>\n"); -} - -TEST(italic_inside_bold) { - const auto result{ - sourcemeta::core::markdown_to_html("**this is *italic* inside bold**")}; - EXPECT_EQ(result, - "<p><strong>this is <em>italic</em> inside bold</strong></p>\n"); -} - -TEST(inline_code) { - const auto result{sourcemeta::core::markdown_to_html("Use `printf()`")}; - EXPECT_EQ(result, "<p>Use <code>printf()</code></p>\n"); -} - -TEST(inline_code_with_backticks_inside) { - const auto result{ - sourcemeta::core::markdown_to_html("`` there is a ` here ``")}; - EXPECT_EQ(result, "<p><code>there is a ` here</code></p>\n"); -} - -TEST(inline_code_preserves_html) { - const auto result{ - sourcemeta::core::markdown_to_html("`<div class=\"foo\">`")}; - EXPECT_EQ(result, "<p><code>&lt;div class=&quot;foo&quot;&gt;</code></p>\n"); -} - -TEST(fenced_code_block_backticks) { - const auto result{sourcemeta::core::markdown_to_html("```\ncode here\n```")}; - EXPECT_EQ(result, "<pre><code>code here\n</code></pre>\n"); -} - -TEST(fenced_code_block_tildes) { - const auto result{sourcemeta::core::markdown_to_html("~~~\ncode here\n~~~")}; - EXPECT_EQ(result, "<pre><code>code here\n</code></pre>\n"); -} - -TEST(fenced_code_block_with_language) { - const auto result{ - sourcemeta::core::markdown_to_html("```cpp\nint x = 0;\n```")}; - EXPECT_EQ(result, "<pre lang=\"cpp\"><code>int x = 0;\n</code></pre>\n"); -} - -TEST(fenced_code_block_html_escaped) { - const auto result{ - sourcemeta::core::markdown_to_html("```\n<div>&amp;</div>\n```")}; - EXPECT_EQ(result, - "<pre><code>&lt;div&gt;&amp;amp;&lt;/div&gt;\n</code></pre>\n"); -} - -TEST(indented_code_block) { - const auto result{ - sourcemeta::core::markdown_to_html(" int x = 0;\n return x;")}; - EXPECT_EQ(result, "<pre><code>int x = 0;\nreturn x;\n</code></pre>\n"); -} - -TEST(inline_link) { - const auto result{ - sourcemeta::core::markdown_to_html("[click here](https://example.com)")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\">click here</a></p>\n"); -} - -TEST(inline_link_with_title) { - const auto result{sourcemeta::core::markdown_to_html( - "[click](https://example.com \"My Title\")")}; - EXPECT_EQ( - result, - "<p><a href=\"https://example.com\" title=\"My Title\">click</a></p>\n"); -} - -TEST(reference_link) { - const auto result{sourcemeta::core::markdown_to_html( - "[click][ref]\n\n[ref]: https://example.com")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\">click</a></p>\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, - "<p><a href=\"https://example.com\" title=\"Title\">click</a></p>\n"); -} - -TEST(collapsed_reference_link) { - const auto result{sourcemeta::core::markdown_to_html( - "[example][]\n\n[example]: https://example.com")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\">example</a></p>\n"); -} - -TEST(shortcut_reference_link) { - const auto result{sourcemeta::core::markdown_to_html( - "[example]\n\n[example]: https://example.com")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\">example</a></p>\n"); -} - -TEST(link_with_emphasis_inside) { - const auto result{sourcemeta::core::markdown_to_html( - "[**bold link**](https://example.com)")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\"><strong>bold " - "link</strong></a></p>\n"); -} - -TEST(image) { - const auto result{ - sourcemeta::core::markdown_to_html("![alt text](image.png)")}; - EXPECT_EQ(result, "<p><img src=\"image.png\" alt=\"alt text\" /></p>\n"); -} - -TEST(image_with_title) { - const auto result{ - sourcemeta::core::markdown_to_html("![alt](image.png \"My Image\")")}; - EXPECT_EQ( - result, - "<p><img src=\"image.png\" alt=\"alt\" title=\"My Image\" /></p>\n"); -} - -TEST(simple_blockquote) { - const auto result{sourcemeta::core::markdown_to_html("> quoted text")}; - EXPECT_EQ(result, "<blockquote>\n<p>quoted text</p>\n</blockquote>\n"); -} - -TEST(multiline_blockquote) { - const auto result{ - sourcemeta::core::markdown_to_html("> line one\n> line two")}; - EXPECT_EQ(result, "<blockquote>\n<p>line one\nline two</p>\n</blockquote>\n"); -} - -TEST(nested_blockquote) { - const auto result{sourcemeta::core::markdown_to_html("> outer\n>> inner")}; - EXPECT_EQ(result, "<blockquote>\n<p>outer</p>\n<blockquote>\n<p>inner</p>\n" - "</blockquote>\n</blockquote>\n"); -} - -TEST(blockquote_with_formatting) { - const auto result{ - sourcemeta::core::markdown_to_html("> **bold** and *italic*")}; - EXPECT_EQ(result, - "<blockquote>\n<p><strong>bold</strong> and <em>italic</em></p>\n" - "</blockquote>\n"); -} - -TEST(unordered_list_with_dashes) { - const auto result{ - sourcemeta::core::markdown_to_html("- one\n- two\n- three")}; - EXPECT_EQ(result, - "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); -} - -TEST(unordered_list_with_asterisks) { - const auto result{ - sourcemeta::core::markdown_to_html("* one\n* two\n* three")}; - EXPECT_EQ(result, - "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); -} - -TEST(unordered_list_with_plus) { - const auto result{ - sourcemeta::core::markdown_to_html("+ one\n+ two\n+ three")}; - EXPECT_EQ(result, - "<ul>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ul>\n"); -} - -TEST(nested_unordered_list) { - const auto result{sourcemeta::core::markdown_to_html( - "- outer\n - inner\n - inner2\n- outer2")}; - EXPECT_EQ(result, "<ul>\n<li>outer\n<ul>\n<li>inner</li>\n<li>inner2</li>\n" - "</ul>\n</li>\n<li>outer2</li>\n</ul>\n"); -} - -TEST(ordered_list) { - const auto result{ - sourcemeta::core::markdown_to_html("1. one\n2. two\n3. three")}; - EXPECT_EQ(result, - "<ol>\n<li>one</li>\n<li>two</li>\n<li>three</li>\n</ol>\n"); -} - -TEST(ordered_list_starting_at_nonone) { - const auto result{ - sourcemeta::core::markdown_to_html("3. three\n4. four\n5. five")}; - EXPECT_EQ(result, - "<ol start=\"3\">\n<li>three</li>\n<li>four</li>\n<li>five</li>\n" - "</ol>\n"); -} - -TEST(nested_ordered_in_unordered) { - const auto result{sourcemeta::core::markdown_to_html( - "- item\n 1. sub one\n 2. sub two\n- item2")}; - EXPECT_EQ(result, "<ul>\n<li>item\n<ol>\n<li>sub one</li>\n<li>sub two</li>\n" - "</ol>\n</li>\n<li>item2</li>\n</ul>\n"); -} - -TEST(loose_list) { - const auto result{ - sourcemeta::core::markdown_to_html("- one\n\n- two\n\n- three")}; - EXPECT_EQ(result, "<ul>\n<li>\n<p>one</p>\n</li>\n<li>\n<p>two</p>\n</li>\n" - "<li>\n<p>three</p>\n</li>\n</ul>\n"); -} - -TEST(thematic_break_with_dashes) { - const auto result{sourcemeta::core::markdown_to_html("---")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(thematic_break_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("***")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(thematic_break_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("___")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(thematic_break_with_spaces) { - const auto result{sourcemeta::core::markdown_to_html("- - -")}; - EXPECT_EQ(result, "<hr />\n"); -} - -TEST(escape_asterisk) { - const auto result{sourcemeta::core::markdown_to_html("\\*not italic\\*")}; - EXPECT_EQ(result, "<p>*not italic*</p>\n"); -} - -TEST(escape_hash) { - const auto result{sourcemeta::core::markdown_to_html("\\# not a heading")}; - EXPECT_EQ(result, "<p># not a heading</p>\n"); -} - -TEST(escape_brackets) { - const auto result{ - sourcemeta::core::markdown_to_html("\\[not a link\\](url)")}; - EXPECT_EQ(result, "<p>[not a link](url)</p>\n"); -} - -TEST(escape_backtick) { - const auto result{sourcemeta::core::markdown_to_html("\\`not code\\`")}; - EXPECT_EQ(result, "<p>`not code`</p>\n"); -} - -TEST(html_entity_named) { - const auto result{sourcemeta::core::markdown_to_html("&copy; 2025")}; - EXPECT_EQ(result, "<p>\xC2\xA9 2025</p>\n"); -} - -TEST(html_entity_numeric) { - const auto result{sourcemeta::core::markdown_to_html("&#169; 2025")}; - EXPECT_EQ(result, "<p>\xC2\xA9 2025</p>\n"); -} - -TEST(ampersand_in_text) { - const auto result{sourcemeta::core::markdown_to_html("AT&T")}; - EXPECT_EQ(result, "<p>AT&amp;T</p>\n"); -} - -TEST(angle_brackets_in_text) { - const auto result{sourcemeta::core::markdown_to_html("1 < 2 and 3 > 2")}; - EXPECT_EQ(result, "<p>1 &lt; 2 and 3 &gt; 2</p>\n"); -} - -TEST(unicode_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Hello \xC3\xA9\xC3\xA0\xC3\xBC")}; - EXPECT_EQ(result, "<p>Hello \xC3\xA9\xC3\xA0\xC3\xBC</p>\n"); -} - -TEST(cjk_characters) { - const auto result{ - sourcemeta::core::markdown_to_html("\xE4\xBD\xA0\xE5\xA5\xBD")}; - EXPECT_EQ(result, "<p>\xE4\xBD\xA0\xE5\xA5\xBD</p>\n"); -} - -TEST(emoji) { - const auto result{sourcemeta::core::markdown_to_html("\xF0\x9F\x98\x80")}; - EXPECT_EQ(result, "<p>\xF0\x9F\x98\x80</p>\n"); -} - -TEST(inline_html_is_sanitized) { - const auto result{sourcemeta::core::markdown_to_html("Hello <em>world</em>")}; - EXPECT_EQ( - result, - "<p>Hello <!-- raw HTML omitted -->world<!-- raw HTML omitted --></p>\n"); -} - -TEST(raw_html_block_is_sanitized) { - const auto result{ - sourcemeta::core::markdown_to_html("<div class=\"foo\">bar</div>")}; - EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); -} - -TEST(script_tag_is_sanitized) { - const auto result{ - sourcemeta::core::markdown_to_html("<script>alert('xss')</script>")}; - EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); -} - -TEST(iframe_is_sanitized) { - const auto result{sourcemeta::core::markdown_to_html( - "<iframe src=\"https://evil.com\"></iframe>")}; - EXPECT_EQ(result, "<!-- raw HTML omitted -->\n"); -} - -TEST(javascript_link_is_sanitized) { - const auto result{ - sourcemeta::core::markdown_to_html("[click](javascript:alert('xss'))")}; - EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); -} - -TEST(simple_table) { - const auto result{ - sourcemeta::core::markdown_to_html("| a | b |\n| - | - |\n| 1 | 2 |")}; - EXPECT_EQ( - result, - "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td>1</td>\n<td>2</td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(table_with_alignment) { - const auto result{sourcemeta::core::markdown_to_html( - "| left | center | right |\n| :--- | :---: | ---: |\n| a | b | c |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n" - "<th align=\"left\">left</th>\n" - "<th align=\"center\">center</th>\n" - "<th align=\"right\">right</th>\n" - "</tr>\n</thead>\n<tbody>\n<tr>\n" - "<td align=\"left\">a</td>\n" - "<td align=\"center\">b</td>\n" - "<td align=\"right\">c</td>\n" - "</tr>\n</tbody>\n</table>\n"); -} - -TEST(table_with_formatting_in_cells) { - const auto result{ - sourcemeta::core::markdown_to_html("| h |\n| - |\n| **bold** |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td><strong>bold</strong></td>\n</tr>\n" - "</tbody>\n</table>\n"); -} - -TEST(table_with_multiple_rows) { - const auto result{sourcemeta::core::markdown_to_html( - "| x | y |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |\n| 5 | 6 |")}; - EXPECT_EQ(result, - "<table>\n<thead>\n<tr>\n<th>x</th>\n<th>y</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td>1</td>\n<td>2</td>\n</tr>\n" - "<tr>\n<td>3</td>\n<td>4</td>\n</tr>\n" - "<tr>\n<td>5</td>\n<td>6</td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(table_with_pipe_in_cell) { - const auto result{ - sourcemeta::core::markdown_to_html("| h |\n| - |\n| a \\| b |")}; - EXPECT_EQ(result, - "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td>a | b</td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(autolink_url) { - const auto result{ - sourcemeta::core::markdown_to_html("Visit https://example.com today")}; - EXPECT_EQ(result, - "<p>Visit <a href=\"https://example.com\">https://example.com</a>" - " today</p>\n"); -} - -TEST(autolink_http) { - const auto result{sourcemeta::core::markdown_to_html("http://example.com")}; - EXPECT_EQ(result, - "<p><a href=\"http://example.com\">http://example.com</a></p>\n"); -} - -TEST(autolink_www) { - const auto result{sourcemeta::core::markdown_to_html("www.example.com")}; - EXPECT_EQ(result, - "<p><a href=\"http://www.example.com\">www.example.com</a></p>\n"); -} - -TEST(autolink_email) { - const auto result{ - sourcemeta::core::markdown_to_html("Contact user@example.com")}; - EXPECT_EQ(result, "<p>Contact <a href=\"mailto:user@example.com\">" - "user@example.com</a></p>\n"); -} - -TEST(angle_bracket_autolink) { - const auto result{ - sourcemeta::core::markdown_to_html("<https://example.com>")}; - EXPECT_EQ(result, - "<p><a href=\"https://example.com\">https://example.com</a></p>\n"); -} - -TEST(strikethrough) { - const auto result{sourcemeta::core::markdown_to_html("~~deleted~~")}; - EXPECT_EQ(result, "<p><del>deleted</del></p>\n"); -} - -TEST(strikethrough_with_other_formatting) { - const auto result{ - sourcemeta::core::markdown_to_html("~~deleted **bold** text~~")}; - EXPECT_EQ(result, "<p><del>deleted <strong>bold</strong> text</del></p>\n"); -} - -TEST(strikethrough_in_sentence) { - const auto result{ - sourcemeta::core::markdown_to_html("This is ~~wrong~~ correct")}; - EXPECT_EQ(result, "<p>This is <del>wrong</del> correct</p>\n"); -} - -TEST(task_list_unchecked) { - const auto result{sourcemeta::core::markdown_to_html("- [ ] todo")}; - EXPECT_EQ(result, - "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> todo</li>\n" - "</ul>\n"); -} - -TEST(task_list_checked) { - const auto result{sourcemeta::core::markdown_to_html("- [x] done")}; - EXPECT_EQ(result, - "<ul>\n<li><input type=\"checkbox\" checked=\"\" disabled=\"\" />" - " done</li>\n</ul>\n"); -} - -TEST(task_list_mixed) { - const auto result{sourcemeta::core::markdown_to_html( - "- [x] done\n- [ ] todo\n- [x] also done")}; - EXPECT_EQ( - result, - "<ul>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> done</li>\n" - "<li><input type=\"checkbox\" disabled=\"\" /> todo</li>\n" - "<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> also " - "done</li>\n" - "</ul>\n"); -} - -TEST(heading_followed_by_paragraph) { - const auto result{ - sourcemeta::core::markdown_to_html("# Title\n\nSome text here.")}; - EXPECT_EQ(result, "<h1>Title</h1>\n<p>Some text here.</p>\n"); -} - -TEST(blockquote_with_list) { - const auto result{ - sourcemeta::core::markdown_to_html("> items:\n> - one\n> - two")}; - EXPECT_EQ(result, - "<blockquote>\n<p>items:</p>\n<ul>\n<li>one</li>\n<li>two</li>\n" - "</ul>\n</blockquote>\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, "<ul>\n<li>\n<p>item:</p>\n<pre><code>code\n</code></pre>\n" - "</li>\n<li>\n<p>next</p>\n</li>\n</ul>\n"); -} - -TEST(link_inside_heading) { - const auto result{ - sourcemeta::core::markdown_to_html("## [Link](https://example.com)")}; - EXPECT_EQ(result, "<h2><a href=\"https://example.com\">Link</a></h2>\n"); -} - -TEST(emphasis_across_words) { - const auto result{ - sourcemeta::core::markdown_to_html("this is **all bold** here")}; - EXPECT_EQ(result, "<p>this is <strong>all bold</strong> here</p>\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, "<p>Use <code>foo()</code> from " - "<a href=\"https://docs.com\">the docs</a>.</p>\n"); -} - -TEST(strikethrough_inside_table_cell) { - const auto result{ - sourcemeta::core::markdown_to_html("| h |\n| - |\n| ~~old~~ |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td><del>old</del></td>\n</tr>\n" - "</tbody>\n</table>\n"); -} - -TEST(link_inside_table_cell) { - const auto result{sourcemeta::core::markdown_to_html( - "| h |\n| - |\n| [click](https://example.com) |")}; - EXPECT_EQ(result, - "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td><a href=\"https://example.com\">click</a>" - "</td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(code_inside_table_cell) { - const auto result{ - sourcemeta::core::markdown_to_html("| h |\n| - |\n| `code` |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>h</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td><code>code</code></td>\n</tr>\n" - "</tbody>\n</table>\n"); -} - -TEST(empty_table_cells) { - const auto result{ - sourcemeta::core::markdown_to_html("| a | b |\n| - | - |\n| | |")}; - EXPECT_EQ(result, - "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(table_header_only) { - const auto result{sourcemeta::core::markdown_to_html("| a | b |\n| - | - |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</" - "thead>\n</table>\n"); -} - -TEST(task_list_with_bold) { - const auto result{ - sourcemeta::core::markdown_to_html("- [ ] **important** task")}; - EXPECT_EQ(result, "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> " - "<strong>important</strong> task</li>\n</ul>\n"); -} - -TEST(task_list_with_link) { - const auto result{sourcemeta::core::markdown_to_html( - "- [x] see [docs](https://example.com)")}; - EXPECT_EQ(result, - "<ul>\n<li><input type=\"checkbox\" checked=\"\" disabled=\"\" /> " - "see <a href=\"https://example.com\">docs</a></li>\n</ul>\n"); -} - -TEST(task_list_with_code) { - const auto result{sourcemeta::core::markdown_to_html("- [ ] fix `bug`")}; - EXPECT_EQ(result, "<ul>\n<li><input type=\"checkbox\" disabled=\"\" /> " - "fix <code>bug</code></li>\n</ul>\n"); -} - -TEST(code_inside_link) { - const auto result{ - sourcemeta::core::markdown_to_html("[`code`](https://example.com)")}; - EXPECT_EQ(result, - "<p><a href=\"https://example.com\"><code>code</code></a></p>\n"); -} - -TEST(inline_code_inside_heading) { - const auto result{ - sourcemeta::core::markdown_to_html("## The `main` function")}; - EXPECT_EQ(result, "<h2>The <code>main</code> function</h2>\n"); -} - -TEST(image_as_link) { - const auto result{sourcemeta::core::markdown_to_html( - "[![logo](logo.png)](https://example.com)")}; - EXPECT_EQ(result, "<p><a href=\"https://example.com\">" - "<img src=\"logo.png\" alt=\"logo\" /></a></p>\n"); -} - -TEST(strikethrough_with_code) { - const auto result{sourcemeta::core::markdown_to_html("~~use `old_func()`~~")}; - EXPECT_EQ(result, "<p><del>use <code>old_func()</code></del></p>\n"); -} - -TEST(strikethrough_with_link) { - const auto result{ - sourcemeta::core::markdown_to_html("~~[removed](https://example.com)~~")}; - EXPECT_EQ(result, - "<p><del><a href=\"https://example.com\">removed</a></del></p>\n"); -} - -TEST(autolink_inside_list) { - const auto result{sourcemeta::core::markdown_to_html( - "- visit https://example.com\n- done")}; - EXPECT_EQ(result, "<ul>\n<li>visit <a href=\"https://example.com\">" - "https://example.com</a></li>\n<li>done</li>\n</ul>\n"); -} - -TEST(blockquote_with_code_block) { - const auto result{ - sourcemeta::core::markdown_to_html("> example:\n>\n> code here")}; - EXPECT_EQ(result, "<blockquote>\n<p>example:</p>\n" - "<pre><code>code here\n</code></pre>\n</blockquote>\n"); -} - -TEST(blockquote_with_heading) { - const auto result{sourcemeta::core::markdown_to_html("> ## Quoted heading")}; - EXPECT_EQ(result, "<blockquote>\n<h2>Quoted heading</h2>\n</blockquote>\n"); -} - -TEST(deeply_nested_list) { - const auto result{ - sourcemeta::core::markdown_to_html("- a\n - b\n - c\n - d")}; - EXPECT_EQ(result, "<ul>\n<li>a\n<ul>\n<li>b\n<ul>\n<li>c\n<ul>\n<li>d</li>\n" - "</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n"); -} - -TEST(deeply_nested_blockquote) { - const auto result{sourcemeta::core::markdown_to_html("> a\n>> b\n>>> c")}; - EXPECT_EQ(result, "<blockquote>\n<p>a</p>\n<blockquote>\n<p>b</p>\n" - "<blockquote>\n<p>c</p>\n</blockquote>\n</blockquote>\n" - "</blockquote>\n"); -} - -TEST(consecutive_headings) { - const auto result{ - sourcemeta::core::markdown_to_html("# One\n\n## Two\n\n### Three")}; - EXPECT_EQ(result, "<h1>One</h1>\n<h2>Two</h2>\n<h3>Three</h3>\n"); -} - -TEST(consecutive_thematic_breaks) { - const auto result{sourcemeta::core::markdown_to_html("---\n\n---")}; - EXPECT_EQ(result, "<hr />\n<hr />\n"); -} - -TEST(link_with_special_chars_in_url) { - const auto result{sourcemeta::core::markdown_to_html( - "[search](https://example.com/q?a=1&b=2)")}; - EXPECT_EQ( - result, - "<p><a href=\"https://example.com/q?a=1&amp;b=2\">search</a></p>\n"); -} - -TEST(image_with_empty_alt) { - const auto result{sourcemeta::core::markdown_to_html("![](image.png)")}; - EXPECT_EQ(result, "<p><img src=\"image.png\" alt=\"\" /></p>\n"); -} - -TEST(bold_and_italic_in_list_item) { - const auto result{ - sourcemeta::core::markdown_to_html("- ***bold italic item***\n- normal")}; - EXPECT_EQ(result, - "<ul>\n<li><em><strong>bold italic item</strong></em></li>\n" - "<li>normal</li>\n</ul>\n"); -} - -TEST(fenced_code_block_with_empty_lines) { - const auto result{ - sourcemeta::core::markdown_to_html("```\nline one\n\nline three\n```")}; - EXPECT_EQ(result, "<pre><code>line one\n\nline three\n</code></pre>\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, "<ol>\n<li>\n<p>first</p>\n<p>more text</p>\n</li>\n" - "<li>\n<p>second</p>\n</li>\n</ol>\n"); -} - -TEST(heading_with_thematic_break) { - const auto result{ - sourcemeta::core::markdown_to_html("# Title\n\n---\n\nContent")}; - EXPECT_EQ(result, "<h1>Title</h1>\n<hr />\n<p>Content</p>\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, "<p>Here is a table:</p>\n" - "<table>\n<thead>\n<tr>\n<th>a</th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td>1</td>\n</tr>\n</tbody>\n</table>\n"); -} - -TEST(blockquote_followed_by_list) { - const auto result{ - sourcemeta::core::markdown_to_html("> quote\n\n- item one\n- item two")}; - EXPECT_EQ(result, "<blockquote>\n<p>quote</p>\n</blockquote>\n" - "<ul>\n<li>item one</li>\n<li>item two</li>\n</ul>\n"); -} - -TEST(multiple_autolinks_in_paragraph) { - const auto result{sourcemeta::core::markdown_to_html( - "See https://one.com and https://two.com")}; - EXPECT_EQ(result, - "<p>See <a href=\"https://one.com\">https://one.com</a> and " - "<a href=\"https://two.com\">https://two.com</a></p>\n"); -} - -TEST(emphasis_does_not_cross_code_span) { - const auto result{sourcemeta::core::markdown_to_html("*start `code* end`")}; - EXPECT_EQ(result, "<p>*start <code>code* end</code></p>\n"); -} - -TEST(vbscript_link_is_sanitized) { - const auto result{ - sourcemeta::core::markdown_to_html("[click](vbscript:MsgBox)")}; - EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); -} - -TEST(data_uri_link_is_sanitized) { - const auto result{sourcemeta::core::markdown_to_html( - "[click](data:text/html,<h1>hi</h1>)")}; - EXPECT_EQ(result, "<p><a href=\"\">click</a></p>\n"); -} - -TEST(single_character_emphasis) { - const auto result{sourcemeta::core::markdown_to_html("*a*")}; - EXPECT_EQ(result, "<p><em>a</em></p>\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, "<ul>\n<li>\n<p>paragraph one</p>\n<p>paragraph two</p>\n" - "<p>paragraph three</p>\n</li>\n</ul>\n"); -} - -TEST(table_with_inline_code_in_header) { - const auto result{sourcemeta::core::markdown_to_html( - "| `key` | `value` |\n| - | - |\n| a | b |")}; - EXPECT_EQ(result, "<table>\n<thead>\n<tr>\n<th><code>key</code></th>\n" - "<th><code>value</code></th>\n</tr>\n</thead>\n" - "<tbody>\n<tr>\n<td>a</td>\n<td>b</td>\n</tr>\n" - "</tbody>\n</table>\n"); -} - -TEST(strikethrough_bold_italic_combined) { - const auto result{sourcemeta::core::markdown_to_html("~~***all three***~~")}; - EXPECT_EQ(result, "<p><del><em><strong>all three</strong></em></del></p>\n"); -} - -TEST(autolink_with_trailing_punctuation) { - const auto result{ - sourcemeta::core::markdown_to_html("See https://example.com.")}; - EXPECT_EQ( - result, - "<p>See <a href=\"https://example.com\">https://example.com</a>.</p>\n"); -} - -TEST(autolink_in_parentheses) { - const auto result{ - sourcemeta::core::markdown_to_html("(see https://example.com)")}; - EXPECT_EQ( - result, - "<p>(see <a href=\"https://example.com\">https://example.com</a>)</p>\n"); -} - -TEST(blockquote_with_strikethrough) { - const auto result{sourcemeta::core::markdown_to_html("> ~~deleted~~")}; - EXPECT_EQ(result, "<blockquote>\n<p><del>deleted</del></p>\n</blockquote>\n"); -} - -TEST(heading_with_bold_and_code) { - const auto result{ - sourcemeta::core::markdown_to_html("## **Bold** and `code`")}; - EXPECT_EQ(result, "<h2><strong>Bold</strong> and <code>code</code></h2>\n"); -} - -TEST(footnote_basic) { - const auto result{ - sourcemeta::core::markdown_to_html("Text[^1]\n\n[^1]: Footnote content")}; - EXPECT_EQ( - result, - "<p>Text<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\"" - " data-footnote-ref>1</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n<ol>\n" - "<li id=\"fn-1\">\n<p>Footnote content " - "<a href=\"#fnref-1\" class=\"footnote-backref\"" - " data-footnote-backref data-footnote-backref-idx=\"1\"" - " aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n</ol>\n</section>\n"); -} - -TEST(footnote_multiple) { - const auto result{sourcemeta::core::markdown_to_html( - "A[^1] and B[^2]\n\n[^1]: First\n\n[^2]: Second")}; - EXPECT_EQ(result, - "<p>A<sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\"" - " data-footnote-ref>1</a></sup> and " - "B<sup class=\"footnote-ref\"><a href=\"#fn-2\" id=\"fnref-2\"" - " data-footnote-ref>2</a></sup></p>\n" - "<section class=\"footnotes\" data-footnotes>\n<ol>\n" - "<li id=\"fn-1\">\n<p>First " - "<a href=\"#fnref-1\" class=\"footnote-backref\"" - " data-footnote-backref data-footnote-backref-idx=\"1\"" - " aria-label=\"Back to reference 1\">\xe2\x86\xa9</a></p>\n" - "</li>\n" - "<li id=\"fn-2\">\n<p>Second " - "<a href=\"#fnref-2\" class=\"footnote-backref\"" - " data-footnote-backref data-footnote-backref-idx=\"2\"" - " aria-label=\"Back to reference 2\">\xe2\x86\xa9</a></p>\n" - "</li>\n</ol>\n</section>\n"); -} - -TEST(single_tilde_is_not_strikethrough) { - const auto result{sourcemeta::core::markdown_to_html("~not deleted~")}; - EXPECT_EQ(result, "<p>~not deleted~</p>\n"); -} - -TEST(github_pre_lang_format) { - const auto result{ - sourcemeta::core::markdown_to_html("```python\nprint()\n```")}; - EXPECT_EQ(result, "<pre lang=\"python\"><code>print()\n</code></pre>\n"); -} - -TEST(invalid_utf8_is_replaced) { - const auto result{sourcemeta::core::markdown_to_html("hello \xFF world")}; - EXPECT_EQ(result, "<p>hello \xEF\xBF\xBD world</p>\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 <sourcemeta/core/markdown.h> + +#include <sourcemeta/core/test.h> + +TEST(thematic_break_with_dashes) { + const auto result{sourcemeta::core::markdown_to_html("---")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(thematic_break_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("***")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(thematic_break_with_underscores) { + const auto result{sourcemeta::core::markdown_to_html("___")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(thematic_break_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("- - -")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(consecutive_thematic_breaks) { + const auto result{sourcemeta::core::markdown_to_html("---\n\n---")}; + EXPECT_EQ(result, "<hr />\n<hr />\n"); +} + +TEST(thematic_break_with_trailing_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("***\t ")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(thematic_break_plus_signs_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("++++")}; + EXPECT_EQ(result, "<p>++++</p>\n"); +} + +TEST(thematic_break_equals_signs_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("= = = =")}; + EXPECT_EQ(result, "<p>= = = =</p>\n"); +} + +TEST(thematic_break_two_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("**\n\n__")}; + EXPECT_EQ(result, "<p>**</p>\n" + "<p>__</p>\n"); +} + +TEST(thematic_break_indented_one_to_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html(" ___\n ---\n ***")}; + EXPECT_EQ(result, "<hr />\n" + "<hr />\n" + "<hr />\n"); +} + +TEST(thematic_break_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" ---")}; + EXPECT_EQ(result, "<pre><code>---\n" + "</code></pre>\n"); +} + +TEST(thematic_break_indented_four_spaces_continues_paragraph) { + const auto result{ + sourcemeta::core::markdown_to_html("Paragraph text\n ___")}; + EXPECT_EQ(result, "<p>Paragraph text\n" + "___</p>\n"); +} + +TEST(thematic_break_many_characters) { + const auto result{sourcemeta::core::markdown_to_html( + "-------------------------------------------------")}; + EXPECT_EQ(result, "<hr />\n"); +} + +TEST(thematic_break_spaces_between_characters) { + const auto result{sourcemeta::core::markdown_to_html("* * * *")}; + EXPECT_EQ(result, "<hr />\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, "<ul>\n" + "<li>\n" + "<ul>\n" + "<li>\n" + "<ul>\n" + "<li>x</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n" + "</li>\n" + "</ul>\n" + "<p>x______</p>\n" + "<p><em><strong>x</strong></em></p>\n"); +} + +TEST(thematic_break_mixed_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("-*-")}; + EXPECT_EQ(result, "<p>-*-</p>\n"); +} + +TEST(thematic_break_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("alpha\n___\nbeta")}; + EXPECT_EQ(result, "<p>alpha</p>\n" + "<hr />\n" + "<p>beta</p>\n"); +} + +TEST(thematic_break_takes_precedence_over_list_item) { + const auto result{sourcemeta::core::markdown_to_html("- one\n- - -\n- two")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "</ul>\n" + "<hr />\n" + "<ul>\n" + "<li>two</li>\n" + "</ul>\n"); +} + +TEST(thematic_break_inside_list_item) { + const auto result{sourcemeta::core::markdown_to_html("* one\n* - - -")}; + EXPECT_EQ(result, "<ul>\n" + "<li>one</li>\n" + "<li>\n" + "<hr />\n" + "</li>\n" + "</ul>\n"); +} diff --git a/test/markdown/markdown_unsafe_test.cc b/test/markdown/markdown_unsafe_test.cc index a280d0b86b..39c94ae604 100644 --- a/test/markdown/markdown_unsafe_test.cc +++ b/test/markdown/markdown_unsafe_test.cc @@ -2,6 +2,10 @@ #include <sourcemeta/core/test.h> +#include <string_view> // std::string_view + +using namespace std::literals::string_view_literals; + TEST(html_block_script_with_blank_lines) { const auto result{sourcemeta::core::markdown_to_html( "<script type=\"module\">\n\nconst x = 1;\n\n</script>\nafter", false)}; @@ -321,3 +325,21 @@ TEST(unsafe_mode_keeps_dangerous_link) { "[click](javascript:alert(1))", false)}; EXPECT_EQ(result, "<p><a href=\"javascript:alert(1)\">click</a></p>\n"); } + +TEST(unsafe_mode_invalid_utf8_inside_html_block) { + const auto result{ + sourcemeta::core::markdown_to_html("<div>\xFF</div>", false)}; + EXPECT_EQ(result, "<div>\xef\xbf\xbd</div>\n"); +} + +TEST(unsafe_mode_invalid_utf8_inside_inline_html_attribute) { + const auto result{ + sourcemeta::core::markdown_to_html("x <span title=\"\xC3\">", false)}; + EXPECT_EQ(result, "<p>x <span title=\"\xef\xbf\xbd\"></p>\n"); +} + +TEST(unsafe_mode_nul_character_inside_inline_html) { + const auto result{ + sourcemeta::core::markdown_to_html("x <span title=\"a\0z\">"sv, false)}; + EXPECT_EQ(result, "<p>x <span title=\"a\xef\xbf\xbdz\"></p>\n"); +}