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 Visit https://example.com"
+ " today Contact "
+ "user@example.com See https://one.com and "
+ "https://two.com See https://example.com. (see https://example.com) https://"
+ "sourcemeta.com/`x` <z:thing> <https://sourcemeta.com/"
+ "a b> https://"
+ "sourcemeta.com/\\_ "
+ "first.last+tag@sub-domain.sourcemeta.com < > <https://sourcemeta.com "
+ "> <sourcemeta.com> see www.sourcemeta.com/docs/intro Go to www.sourcemeta.com/"
+ "x.y! https://"
+ "sourcemeta.com/wiki/Thing_(disambiguation) https://"
+ "sourcemeta.com/a_(b))) (see https://"
+ "sourcemeta.com/a_(b)) https://"
+ "sourcemeta.com/?x=1& https://"
+ "sourcemeta.com/?x=1&y=2\n
\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,
+ "
www.sourcemeta._com
\n" + "www.source_meta.com
\n"); +} + +TEST(extended_autolink_underscore_in_earlier_domain_segment) { + const auto result{ + sourcemeta::core::markdown_to_html("www.docs_site.sourcemeta.com")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(extended_autolink_http_with_query_and_fragment) { + const auto result{sourcemeta::core::markdown_to_html( + "https://sourcemeta.com/search?q=json+schema#results")}; + EXPECT_EQ( + result, + "https://" + "sourcemeta.com/search?q=json+schema#results
\n"); +} + +TEST(extended_autolink_email_local_and_domain_symbols) { + const auto result{sourcemeta::core::markdown_to_html( + "team@sourcemeta.com\n\nteam@source+meta.com\n\nteam+tag@sourcemeta." + "com")}; + EXPECT_EQ( + result, + "\n" + "team@source+meta.com
\n" + "team+tag@sourcemeta.com" + "p>\n"); +} + +TEST(extended_autolink_email_trailing_symbols) { + const auto result{sourcemeta::core::markdown_to_html( + "first.last@sourcemeta.com.\n\nfirst.last@sourcemeta.com-\n\nfirst.last@" + "sourcemeta.com_")}; + EXPECT_EQ(result, "
first.last@" + "sourcemeta.com.
\n" + "first.last@sourcemeta.com-
\n" + "first.last@sourcemeta.com_
\n"); +} + +TEST(extended_autolink_mailto_and_xmpp) { + const auto result{sourcemeta::core::markdown_to_html( + "mailto:team@sourcemeta.com\n\nxmpp:team@sourcemeta.com/resource")}; + EXPECT_EQ( + result, + "\n" + "xmpp:team@sourcemeta.com/" + "resource
\n"); +} + +TEST(extended_autolink_not_after_word_character) { + const auto result{sourcemeta::core::markdown_to_html("xwww.sourcemeta.com")}; + EXPECT_EQ(result, "xwww.sourcemeta.com
\n"); +} + +TEST(extended_autolink_inside_emphasis) { + const auto result{ + sourcemeta::core::markdown_to_html("_https://sourcemeta.com_")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(extended_autolink_ftp_scheme) { + const auto result{ + sourcemeta::core::markdown_to_html("ftp://files.sourcemeta.com/archive")}; + EXPECT_EQ(result, "ftp://" + "files.sourcemeta.com/archive
\n"); +} + +TEST(extended_autolink_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[https://sourcemeta.com](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(extended_autolink_inside_code_span) { + const auto result{ + sourcemeta::core::markdown_to_html("`https://sourcemeta.com`")}; + EXPECT_EQ(result, "https://sourcemeta.com
\n\n"); +} + +TEST(multiline_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("> line one\n> line two")}; + EXPECT_EQ(result, "quoted text
\n
\n\n"); +} + +TEST(nested_blockquote) { + const auto result{sourcemeta::core::markdown_to_html("> outer\n>> inner")}; + EXPECT_EQ(result, "line one\nline two
\n
\n\n"); +} + +TEST(blockquote_with_formatting) { + const auto result{ + sourcemeta::core::markdown_to_html("> **bold** and *italic*")}; + EXPECT_EQ(result, + "outer
\n\n\ninner
\n" + "
\n\n"); +} + +TEST(blockquote_with_list) { + const auto result{ + sourcemeta::core::markdown_to_html("> items:\n> - one\n> - two")}; + EXPECT_EQ(result, + "bold and italic
\n" + "
\n\n"); +} + +TEST(blockquote_with_code_block) { + const auto result{ + sourcemeta::core::markdown_to_html("> example:\n>\n> code here")}; + EXPECT_EQ(result, "items:
\n\n
\n- one
\n- two
\n" + "
\n\n"); +} + +TEST(blockquote_with_heading) { + const auto result{sourcemeta::core::markdown_to_html("> ## Quoted heading")}; + EXPECT_EQ(result, "example:
\n" + "\ncode here\n
\n\n"); +} + +TEST(deeply_nested_blockquote) { + const auto result{sourcemeta::core::markdown_to_html("> a\n>> b\n>>> c")}; + EXPECT_EQ(result, "Quoted heading
\n
\n\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, "a
\n\n\n" + "b
\n" + "\n\nc
\n
\n\n" + "quote
\n
\n\n"); +} + +TEST(blockquote_marker_without_following_space) { + const auto result{ + sourcemeta::core::markdown_to_html(">## Title\n>text\n> more")}; + EXPECT_EQ(result, "\n
deleted
\n" + "\n"); +} + +TEST(blockquote_marker_indented_up_to_three_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html(" > one\n > two\n > three")}; + EXPECT_EQ(result, "Title
\n" + "text\n" + "more
\n" + "
\n" + "\n"); +} + +TEST(blockquote_marker_indented_four_spaces_is_code) { + const auto result{sourcemeta::core::markdown_to_html(" > one\n > two")}; + EXPECT_EQ(result, "one\n" + "two\n" + "three
\n" + "
> one\n"
+ "> two\n"
+ "\n");
+}
+
+TEST(blockquote_lazy_paragraph_continuation) {
+ const auto result{sourcemeta::core::markdown_to_html("> one\ntwo")};
+ EXPECT_EQ(result, "\n" + "\n"); +} + +TEST(blockquote_lazy_continuation_between_marked_lines) { + const auto result{sourcemeta::core::markdown_to_html("> one\ntwo\n> three")}; + EXPECT_EQ(result, "one\n" + "two
\n" + "
\n" + "\n"); +} + +TEST(blockquote_laziness_does_not_apply_to_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("> one\n***")}; + EXPECT_EQ(result, "one\n" + "two\n" + "three
\n" + "
\n" + "\n" + "one
\n" + "
\n" + "\n" + "\n" + "
\n" + "- one
\n" + "
\n" + "\n" + "\n" + "code\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" + "\n"); +} + +TEST(blockquote_empty_indented_marker) { + const auto result{sourcemeta::core::markdown_to_html(" >")}; + EXPECT_EQ(result, "one\n" + "* two
\n" + "
\n" + "\n"); +} + +TEST(blockquote_empty_multiple_lines) { + const auto result{sourcemeta::core::markdown_to_html(">\n>\n> ")}; + EXPECT_EQ(result, "
\n" + "\n"); +} + +TEST(blockquote_blank_first_and_last_lines) { + const auto result{sourcemeta::core::markdown_to_html("> \n> text\n>")}; + EXPECT_EQ(result, "
\n" + "\n"); +} + +TEST(blockquote_blank_line_separates_blockquotes) { + const auto result{sourcemeta::core::markdown_to_html("> one\n\n> two")}; + EXPECT_EQ(result, "text
\n" + "
\n" + "\n" + "one
\n" + "
\n" + "\n"); +} + +TEST(blockquote_empty_marker_line_joins_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html("> one\n> \n> two")}; + EXPECT_EQ(result, "two
\n" + "
\n" + "\n"); +} + +TEST(blockquote_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("text\n>quote")}; + EXPECT_EQ(result, "one
\n" + "two
\n" + "
text
\n" + "\n" + "\n"); +} + +TEST(blockquote_ended_by_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("> one\n- - -\n> two")}; + EXPECT_EQ(result, "quote
\n" + "
\n" + "\n" + "one
\n" + "
\n" + "\n"); +} + +TEST(blockquote_lazy_line_after_blank_marker_line) { + const auto result{sourcemeta::core::markdown_to_html("> one\n>\ntwo")}; + EXPECT_EQ(result, "two
\n" + "
\n" + "\n" + "one
\n" + "
two
\n"); +} + +TEST(blockquote_nested_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html(">> one\ntwo")}; + EXPECT_EQ(result, "\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" + "\n"); +} + +TEST(blockquote_indented_code_needs_five_spaces) { + const auto result{ + sourcemeta::core::markdown_to_html("> code\n\n> text")}; + EXPECT_EQ(result, "\n" + "\n" + "one\n" + "two
\n" + "\n" + "\n" + "three
\n" + "
\n" + "\n" + "\n" + "code\n" + "
\n" + "\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 @@ +#includetext
\n" + "
AT&T
\n"); +} + +TEST(angle_brackets_in_text) { + const auto result{sourcemeta::core::markdown_to_html("1 < 2 and 3 > 2")}; + EXPECT_EQ(result, "1 < 2 and 3 > 2
\n"); +} + +TEST(unicode_content) { + const auto result{ + sourcemeta::core::markdown_to_html("Hello \xC3\xA9\xC3\xA0\xC3\xBC")}; + EXPECT_EQ(result, "Hello \xC3\xA9\xC3\xA0\xC3\xBC
\n"); +} + +TEST(cjk_characters) { + const auto result{ + sourcemeta::core::markdown_to_html("\xE4\xBD\xA0\xE5\xA5\xBD")}; + EXPECT_EQ(result, "\xE4\xBD\xA0\xE5\xA5\xBD
\n"); +} + +TEST(emoji) { + const auto result{sourcemeta::core::markdown_to_html("\xF0\x9F\x98\x80")}; + EXPECT_EQ(result, "\xF0\x9F\x98\x80
\n"); +} + +TEST(invalid_utf8_is_replaced) { + const auto result{sourcemeta::core::markdown_to_html("hello \xFF world")}; + EXPECT_EQ(result, "hello \xEF\xBF\xBD world
\n"); +} + +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, "kiwi
\n" + "mango
\n" + "kiwi
\n" + " mango\n"
+ "\n"
+ "\n" + "\n"); +} + +TEST(tab_after_list_marker) { + const auto result{sourcemeta::core::markdown_to_html("+\t\tcedar")}; + EXPECT_EQ(result, "\n" + "cedar\n" + " oak\n" + "
cedar\n"
+ "\n"
+ "text
\n" + "more
\n"); +} + +TEST(cr_line_endings) { + const auto result{ + sourcemeta::core::markdown_to_html("## Title\rtext\r\rmore")}; + EXPECT_EQ(result, "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, "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"); +} + +TEST(invalid_utf8_truncated_two_byte_sequence_at_end) { + const auto result{sourcemeta::core::markdown_to_html("caf\xC3")}; + EXPECT_EQ(result, "caf\xef\xbf\xbd
\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, "x\xef\xbf\xbd" + "b
\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, "x\xef\xbf\xbd\n" + "y
\n"); +} + +TEST(invalid_utf8_stray_continuation_byte) { + const auto result{sourcemeta::core::markdown_to_html("x\x80y")}; + EXPECT_EQ(result, "x\xef\xbf\xbdy
\n"); +} + +TEST(invalid_utf8_consecutive_continuation_bytes) { + const auto result{sourcemeta::core::markdown_to_html("\x80\x81\xBF")}; + EXPECT_EQ(result, "\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd
\n"); +} + +TEST(invalid_utf8_overlong_encoded_slash) { + const auto result{sourcemeta::core::markdown_to_html("path\xC0\xAF" + "etc")}; + EXPECT_EQ(result, "path\xef\xbf\xbd" + "etc
\n"); +} + +TEST(invalid_utf8_overlong_encoded_nul) { + const auto result{sourcemeta::core::markdown_to_html("x\xE0\x80\x80y")}; + EXPECT_EQ(result, "x\xef\xbf\xbdy
\n"); +} + +TEST(invalid_utf8_encoded_surrogate) { + const auto result{sourcemeta::core::markdown_to_html("x\xED\xA0\x80y")}; + EXPECT_EQ(result, "x\xef\xbf\xbdy
\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, "x\xef\xbf\xbdy
\n"); +} + +TEST(invalid_utf8_five_byte_sequence) { + const auto result{ + sourcemeta::core::markdown_to_html("x\xF8\x88\x80\x80\x80y")}; + EXPECT_EQ(result, + "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, "
\xef\xbf\xbd" + "abc
\n"); +} + +TEST(invalid_utf8_bytes_fe_and_ff) { + const auto result{sourcemeta::core::markdown_to_html("\xFE\xFF text")}; + EXPECT_EQ(result, "\xef\xbf\xbd\xef\xbf\xbd text
\n"); +} + +TEST(invalid_utf8_in_heading) { + const auto result{sourcemeta::core::markdown_to_html("# t\xFFitle")}; + EXPECT_EQ(result, "a\xef\xbf\xbd"
+ "b
\xef\xbf\xbd\n"
+ "\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, "code\n"
+ "\n");
+}
+
+TEST(invalid_utf8_in_indented_code) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" \xE2\x82\n next")};
+ EXPECT_EQ(result, "\xef\xbf\xbd\n"
+ "next\n"
+ "\n");
+}
+
+TEST(invalid_utf8_in_link_destination) {
+ const auto result{sourcemeta::core::markdown_to_html("[x](/a\xFF"
+ "b)")};
+ EXPECT_EQ(result, "\n");
+}
+
+TEST(invalid_utf8_in_link_title) {
+ const auto result{sourcemeta::core::markdown_to_html("[x](/a 't\xC3')")};
+ EXPECT_EQ(result, "\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, "\n");
+}
+
+TEST(invalid_utf8_in_autolink) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("https://" + "sourcemeta.com/\xef\xbf\xbd
\n"); +} + +TEST(invalid_utf8_in_extended_autolink) { + const auto result{ + sourcemeta::core::markdown_to_html("www.sourcemeta.com/\xC3path")}; + EXPECT_EQ(result, + "www.sourcemeta.com/\xef\xbf\xbdpath
\n"); +} + +TEST(invalid_utf8_in_table_cell) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n| - |\n| \xFF |")}; + EXPECT_EQ(result, "| a | \n" + "
|---|
| \xef\xbf\xbd | \n" + "

\xef\xbf\xbd
\n"); +} + +TEST(invalid_utf8_in_footnote_label) { + const auto result{ + sourcemeta::core::markdown_to_html("x[^\xFF]\n\n[^\xFF]: note")}; + EXPECT_EQ(result, + "x1
\n" + "note \xe2\x86\xa9
\n" + "&\xef\xbf\xbd" + "amp;
\n"); +} + +TEST(nul_character_at_start) { + const auto result{sourcemeta::core::markdown_to_html("\0text"sv)}; + EXPECT_EQ(result, "\xef\xbf\xbdtext
\n"); +} + +TEST(nul_character_in_code_span) { + const auto result{sourcemeta::core::markdown_to_html("`x\0y`"sv)}; + EXPECT_EQ(result, "x\xef\xbf\xbdy
x\xef\xbf\xbdy\n"
+ "\n");
+}
+
+TEST(nul_character_in_link_destination) {
+ const auto result{sourcemeta::core::markdown_to_html("[x](/a\0z)"sv)};
+ EXPECT_EQ(result, "\n");
+}
+
+TEST(nul_character_in_heading) {
+ const auto result{sourcemeta::core::markdown_to_html("# a\0"sv)};
+ EXPECT_EQ(result, "| a | \n" + "
|---|
| \xef\xbf\xbd | \n" + "
x\x01y
\n"); +} + +TEST(delete_character_preserved) { + const auto result{sourcemeta::core::markdown_to_html("x\x7Fy")}; + EXPECT_EQ(result, "x\x7fy
\n"); +} + +TEST(form_feed_between_words) { + const auto result{sourcemeta::core::markdown_to_html("a\fz")}; + EXPECT_EQ(result, "a\x0cz
\n"); +} + +TEST(vertical_tab_between_words) { + const auto result{sourcemeta::core::markdown_to_html("a\vz")}; + EXPECT_EQ(result, "a\x0bz
\n"); +} + +TEST(form_feed_between_thematic_break_characters) { + const auto result{sourcemeta::core::markdown_to_html("-\f-\f-")}; + EXPECT_EQ(result, "-\x0c-\x0c-
\n"); +} + +TEST(lone_carriage_return_at_end) { + const auto result{sourcemeta::core::markdown_to_html("text\r")}; + EXPECT_EQ(result, "text
\n"); +} + +TEST(carriage_return_before_list_item) { + const auto result{sourcemeta::core::markdown_to_html("a\r* b")}; + EXPECT_EQ(result, "a
\n" + "a
\n"
+ "b
*\xc2\xa0item
\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, "\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83" + "code
\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, "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbb
\n"); +} + +TEST(combining_accent_preserved) { + const auto result{sourcemeta::core::markdown_to_html("e\xCC\x81")}; + EXPECT_EQ(result, "e\xcc\x81
\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, "\xd7\xa9\xd7\x9c\xd7\x95\xd7\x9d
\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, "a\n" + "\xef\xbb\xbf" + "b
\n"); +} + +TEST(four_byte_character_in_link_destination) { + const auto result{ + sourcemeta::core::markdown_to_html("[x](/\xF0\x9F\x98\x80)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(non_ascii_link_destination_is_percent_encoded) { + const auto result{sourcemeta::core::markdown_to_html("[x](/caf\xC3\xA9)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(quotes_in_text_are_escaped) { + const auto result{sourcemeta::core::markdown_to_html("say \"hi\" and 'bye'")}; + EXPECT_EQ(result, "say "hi" and 'bye'
\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"); +} 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 @@ +#includecode here\n\n");
+}
+
+TEST(fenced_code_block_tildes) {
+ const auto result{sourcemeta::core::markdown_to_html("~~~\ncode here\n~~~")};
+ EXPECT_EQ(result, "code here\n\n");
+}
+
+TEST(fenced_code_block_with_language) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("```cpp\nint x = 0;\n```")};
+ EXPECT_EQ(result, "int x = 0;\n\n");
+}
+
+TEST(fenced_code_block_html_escaped) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("```\n<div>&</div>\n\n");
+}
+
+TEST(indented_code_block) {
+ const auto result{
+ sourcemeta::core::markdown_to_html(" int x = 0;\n return x;")};
+ EXPECT_EQ(result, "int x = 0;\nreturn x;\n\n");
+}
+
+TEST(fenced_code_block_with_empty_lines) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("```\nline one\n\nline three\n```")};
+ EXPECT_EQ(result, "line one\n\nline three\n\n");
+}
+
+TEST(github_pre_lang_format) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("```python\nprint()\n```")};
+ EXPECT_EQ(result, "print()\n\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, "code\n"
+ "\n"
+ "code\n"
+ "\n"
+ " 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" + "\n" + "\n" + "code\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
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");
+}
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 Use printf()
there is a ` here
<div class="foo">
a`b
`x`
x
x
\xc2\xa0x
one two three
a b c
a\\b
``x```
\n"); +} + +TEST(code_span_unmatched_opening_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("``x")}; + EXPECT_EQ(result, "``x
\n"); +} + +TEST(code_span_longer_closing_run_does_not_close) { + const auto result{sourcemeta::core::markdown_to_html("``x```y``")}; + EXPECT_EQ(result, "x```y
_x_
[text code](/url)
<https://sourcemeta.com/x>`
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_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 @@ +#includeitalic
\n"); +} + +TEST(italic_with_underscores) { + const auto result{sourcemeta::core::markdown_to_html("_italic_")}; + EXPECT_EQ(result, "italic
\n"); +} + +TEST(bold_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("**bold**")}; + EXPECT_EQ(result, "bold
\n"); +} + +TEST(bold_with_underscores) { + const auto result{sourcemeta::core::markdown_to_html("__bold__")}; + EXPECT_EQ(result, "bold
\n"); +} + +TEST(bold_and_italic) { + const auto result{sourcemeta::core::markdown_to_html("***bold italic***")}; + EXPECT_EQ(result, "bold italic
\n"); +} + +TEST(bold_inside_italic) { + const auto result{ + sourcemeta::core::markdown_to_html("*this is **bold** inside italic*")}; + EXPECT_EQ(result, + "this is bold inside italic
\n"); +} + +TEST(italic_inside_bold) { + const auto result{ + sourcemeta::core::markdown_to_html("**this is *italic* inside bold**")}; + EXPECT_EQ(result, + "this is italic inside bold
\n"); +} + +TEST(emphasis_across_words) { + const auto result{ + sourcemeta::core::markdown_to_html("this is **all bold** here")}; + EXPECT_EQ(result, "this is all bold here
\n"); +} + +TEST(bold_and_italic_in_list_item) { + const auto result{ + sourcemeta::core::markdown_to_html("- ***bold italic item***\n- normal")}; + EXPECT_EQ(result, + "*start code* end
a
\n"); +} + +TEST(emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x _ y z_")}; + EXPECT_EQ(result, "x _ y z_
\n"); +} + +TEST(emphasis_opening_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x*(y)*")}; + EXPECT_EQ(result, "x*(y)*
\n"); +} + +TEST(emphasis_opening_followed_by_non_breaking_space) { + const auto result{sourcemeta::core::markdown_to_html("_\xC2\xA0" + "x\xC2\xA0_")}; + EXPECT_EQ(result, "_\xc2\xa0x\xc2\xa0_
\n"); +} + +TEST(emphasis_intraword_with_asterisks) { + const auto result{ + sourcemeta::core::markdown_to_html("un*frigging*believable")}; + EXPECT_EQ(result, "unfriggingbelievable
\n"); +} + +TEST(emphasis_intraword_with_underscores_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("snake_case_name")}; + EXPECT_EQ(result, "snake_case_name
\n"); +} + +TEST(emphasis_intraword_with_underscores_in_cyrillic) { + const auto result{sourcemeta::core::markdown_to_html("река_течёт_быстро")}; + EXPECT_EQ(result, "\xd1\x80\xd0\xb5\xd0\xba\xd0\xb0_" + "\xd1\x82\xd0\xb5\xd1\x87\xd1\x91\xd1\x82_" + "\xd0\xb1\xd1\x8b\xd1\x81\xd1\x82\xd1\x80\xd0\xbe
\n"); +} + +TEST(emphasis_between_digits) { + const auto result{sourcemeta::core::markdown_to_html("1*2*3\n\n1_2_3")}; + EXPECT_EQ(result, "123
\n" + "1_2_3
\n"); +} + +TEST(emphasis_underscore_between_letters_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x_(y)_z")}; + EXPECT_EQ(result, "x_(y)_z
\n"); +} + +TEST(emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x._[y]_")}; + EXPECT_EQ(result, "x.[y]
\n"); +} + +TEST(emphasis_mismatched_delimiter_characters) { + const auto result{sourcemeta::core::markdown_to_html("*x_")}; + EXPECT_EQ(result, "*x_
\n"); +} + +TEST(emphasis_closing_preceded_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("_x y _")}; + EXPECT_EQ(result, "_x y _
\n"); +} + +TEST(emphasis_closing_on_new_line) { + const auto result{sourcemeta::core::markdown_to_html("_x y\n_")}; + EXPECT_EQ(result, "_x y\n" + "_
\n"); +} + +TEST(emphasis_closing_not_right_flanking) { + const auto result{sourcemeta::core::markdown_to_html("_[_x]")}; + EXPECT_EQ(result, "_[_x]
\n"); +} + +TEST(emphasis_nested_with_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[_x_]_")}; + EXPECT_EQ(result, "[x]
\n"); +} + +TEST(emphasis_closing_followed_by_letter) { + const auto result{sourcemeta::core::markdown_to_html("*x*y")}; + EXPECT_EQ(result, "xy
\n"); +} + +TEST(emphasis_underscore_closing_followed_by_letter) { + const auto result{sourcemeta::core::markdown_to_html("_x_y\n\n_x_y_z_")}; + EXPECT_EQ(result, "_x_y
\n" + "x_y_z
\n"); +} + +TEST(emphasis_underscore_followed_by_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("_[x]_!")}; + EXPECT_EQ(result, "[x]!
\n"); +} + +TEST(strong_emphasis_opening_followed_by_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("__ x y__")}; + EXPECT_EQ(result, "__ x y__
\n"); +} + +TEST(strong_emphasis_between_letter_and_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x__(y)__")}; + EXPECT_EQ(result, "x__(y)__
\n"); +} + +TEST(strong_emphasis_intraword_with_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("x**y**z")}; + EXPECT_EQ(result, "xyz
\n"); +} + +TEST(strong_emphasis_nested_asterisks) { + const auto result{sourcemeta::core::markdown_to_html("**x, **y**, z**")}; + EXPECT_EQ(result, "x, y, z
\n"); +} + +TEST(strong_emphasis_underscore_after_punctuation) { + const auto result{sourcemeta::core::markdown_to_html("x.__[y]__")}; + EXPECT_EQ(result, "x.[y]
\n"); +} + +TEST(emphasis_rule_of_three_nested_strong) { + const auto result{sourcemeta::core::markdown_to_html("_x__y__z_")}; + EXPECT_EQ(result, "x__y__z
\n"); +} + +TEST(emphasis_rule_of_three_unmatched) { + const auto result{sourcemeta::core::markdown_to_html("_x__y_")}; + EXPECT_EQ(result, "x__y
\n"); +} + +TEST(emphasis_rule_of_three_triple_opening) { + const auto result{sourcemeta::core::markdown_to_html("___x__ y_")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(emphasis_rule_of_three_triple_closing) { + const auto result{sourcemeta::core::markdown_to_html("_x __y___")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(emphasis_rule_of_three_intraword) { + const auto result{sourcemeta::core::markdown_to_html("a***b***c")}; + EXPECT_EQ(result, "abc
\n"); +} + +TEST(emphasis_rule_of_three_long_runs) { + const auto result{sourcemeta::core::markdown_to_html("a****b*******c")}; + EXPECT_EQ(result, "ab***c
\n"); +} + +TEST(emphasis_containing_link) { + const auto result{ + sourcemeta::core::markdown_to_html("_see [docs](/manual)_")}; + EXPECT_EQ(result, "see docs
\n"); +} + +TEST(emphasis_empty_delimiters_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "__ empty strong\n\n____ empty double")}; + EXPECT_EQ(result, "__ empty strong
\n" + "____ empty double
\n"); +} + +TEST(emphasis_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("_one\ntwo_")}; + EXPECT_EQ(result, "one\n" + "two
\n"); +} + +TEST(strong_emphasis_nested_inside_itself) { + const auto result{sourcemeta::core::markdown_to_html("**x **y** z**")}; + EXPECT_EQ(result, "x y z
\n"); +} + +TEST(strong_emphasis_quadruple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("****x****")}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(strong_emphasis_quintuple_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("*****x*****")}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(emphasis_with_escaped_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("x _\\__\n\nx __\\___")}; + EXPECT_EQ(result, "x _
\n" + "x _
\n"); +} + +TEST(emphasis_with_excess_delimiters) { + const auto result{ + sourcemeta::core::markdown_to_html("_x__\n\n__x_\n\n_x____")}; + EXPECT_EQ(result, "x_
\n" + "_x
\n" + "x___
\n"); +} + +TEST(emphasis_overlapping_delimiters) { + const auto result{sourcemeta::core::markdown_to_html("_x *y_ z*")}; + EXPECT_EQ(result, "x *y z*
\n"); +} + +TEST(emphasis_overlapping_strong_and_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x **y _z w** v_")}; + EXPECT_EQ(result, "x y _z w v
\n"); +} + +TEST(emphasis_unclosed_outer_strong) { + const auto result{sourcemeta::core::markdown_to_html("__x __y z__")}; + EXPECT_EQ(result, "__x y z
\n"); +} + +TEST(emphasis_unclosed_outer_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("_x _y z_")}; + EXPECT_EQ(result, "_x y z
\n"); +} + +TEST(emphasis_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("_[x_](/url)")}; + EXPECT_EQ(result, "_x_
\n"); +} + +TEST(emphasis_asterisk_does_not_span_link_brackets) { + const auto result{sourcemeta::core::markdown_to_html("*x [y*](/url)")}; + EXPECT_EQ(result, "*x y*
\n"); +} + +TEST(strong_emphasis_does_not_span_code_span) { + const auto result{sourcemeta::core::markdown_to_html("**x `**` y**")}; + EXPECT_EQ(result, "x ** y
_xhttps://" + "sourcemeta.com/?a=_
\n" + "**xhttps://" + "sourcemeta.com/?b=**
\n"); +} + +TEST(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"); +} 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\xC2\xA9 2025
\n"); +} + +TEST(html_entity_numeric) { + const auto result{sourcemeta::core::markdown_to_html("© 2025")}; + EXPECT_EQ(result, "\xC2\xA9 2025
\n"); +} + +TEST(entity_named_references) { + const auto result{sourcemeta::core::markdown_to_html( + "< > " ' € …\n→ " + "⪡̸ ★")}; + EXPECT_EQ(result, "< > " ' \xe2\x82\xac \xe2\x80\xa6\n" + "\xe2\x86\x92 \xe2\xaa\xa1\xcc\xb8 \xe2\x98\x85
\n"); +} + +TEST(entity_decimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("A € 😀")}; + EXPECT_EQ(result, "A \xe2\x82\xac \xf0\x9f\x98\x80
\n"); +} + +TEST(entity_hexadecimal_references) { + const auto result{ + sourcemeta::core::markdown_to_html("A € 😀")}; + EXPECT_EQ(result, "A \xe2\x82\xac \xf0\x9f\x98\x80
\n"); +} + +TEST(entity_invalid_references_are_literal) { + const auto result{sourcemeta::core::markdown_to_html( + "&euro &y; z; z;\n\n\n&NotARealEntity;")}; + EXPECT_EQ(result, "&euro &y; &#z; &#xz;\n" + "\xef\xbf\xbd\n" + "\xef\xbf\xbd\n" + "&NotARealEntity;
\n"); +} + +TEST(entity_without_semicolon_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("&")}; + EXPECT_EQ(result, "&
\n"); +} + +TEST(entity_unknown_name_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("¬real;")}; + EXPECT_EQ(result, "¬real;
\n"); +} + +TEST(entity_code_point_beyond_unicode_range) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "\xef\xbf\xbd
\n"); +} + +TEST(entity_surrogate_code_point) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_EQ(result, "\xef\xbf\xbd
\n"); +} + +TEST(entity_in_link_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/café 'café')")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(entity_in_reference_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs]\n\n[docs]: /café 'café'")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(entity_inside_code_span_is_literal) { + const auto result{sourcemeta::core::markdown_to_html("`é`")}; + EXPECT_EQ(result, "é
é&\n"
+ "\n");
+}
+
+TEST(entity_cannot_create_emphasis) {
+ const auto result{sourcemeta::core::markdown_to_html("_word_")};
+ EXPECT_EQ(result, "_word_
\n"); +} + +TEST(entity_cannot_create_list) { + const auto result{sourcemeta::core::markdown_to_html("- item")}; + EXPECT_EQ(result, "- item
\n"); +} + +TEST(entity_newline_is_not_line_break) { + const auto result{sourcemeta::core::markdown_to_html("one two")}; + EXPECT_EQ(result, "one\n" + "two
\n"); +} + +TEST(entity_tabs_are_not_indentation) { + const auto result{sourcemeta::core::markdown_to_html(" code")}; + EXPECT_EQ(result, "\t\tcode
\n"); +} + +TEST(entity_quotes_do_not_delimit_link_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/manual "Title")")}; + EXPECT_EQ(result, "[docs](/manual "Title")
\n"); +} 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*not italic*
\n"); +} + +TEST(escape_hash) { + const auto result{sourcemeta::core::markdown_to_html("\\# not a heading")}; + EXPECT_EQ(result, "# not a heading
\n"); +} + +TEST(escape_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("\\[not a link\\](url)")}; + EXPECT_EQ(result, "[not a link](url)
\n"); +} + +TEST(escape_backtick) { + const auto result{sourcemeta::core::markdown_to_html("\\`not code\\`")}; + EXPECT_EQ(result, "`not code`
\n"); +} + +TEST(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\\\n2\\) no " + "list\n\\+ no list\n\\> no quote\n\\& no entity")}; + EXPECT_EQ(result, "_no emphasis_\n" + "<em> no tag\n" + "!no image\n" + "2) no list\n" + "+ no list\n" + "> no quote\n" + "& no entity
\n"); +} + +TEST(backslash_escaped_backslash_before_emphasis) { + const auto result{sourcemeta::core::markdown_to_html("\\\\_emphasis_")}; + EXPECT_EQ(result, "\\emphasis
\n"); +} + +TEST(backslash_at_end_of_line_is_hard_break) { + const auto result{sourcemeta::core::markdown_to_html("one\\\ntwo")}; + EXPECT_EQ(result, "one
\n"
+ "two
\\*\\_
\\*\\_\n"
+ "\n");
+}
+
+TEST(backslash_inside_fenced_code_is_literal) {
+ const auto result{sourcemeta::core::markdown_to_html("```\n\\*\\_\n```")};
+ EXPECT_EQ(result, "\\*\\_\n"
+ "\n");
+}
+
+TEST(backslash_inside_autolink_is_literal) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("https://" + "sourcemeta.com/\\_\\*
\n"); +} + +TEST(backslash_escapes_in_link_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/a\\_b 'c\\_d')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(backslash_escapes_in_reference_definition) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs]\n\n[docs]: /a\\_b 'c\\_d'")}; + EXPECT_EQ(result, "\n"); +} 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 @@ +#includeText1
\n" + "Footnote content " + "\xe2\x86\xa9
\n" + "First " + "\xe2\x86\xa9
\n" + "Second " + "\xe2\x86\xa9
\n" + "body
\n"); +} + +TEST(footnote_reference_without_definition) { + const auto result{sourcemeta::core::markdown_to_html("body[^ghost]")}; + EXPECT_EQ(result, "body[^ghost]
\n"); +} + +TEST(footnote_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("body[^LABEL]\n\n[^label]: text")}; + EXPECT_EQ(result, + "body1
\n" + "text \xe2\x86\xa9
\n" + "body1
\n" + "first
\n" + "\n" + "\n" + "quoted
\n" + "
body1
\n" + "text \xe2\x86\xa9
\n" + "| h | \n" + "
|---|
| c1 | \n" + "
table note \xe2\x86\xa9
\n" + "body1
\n" + "refers2 \xe2\x86\xa9
\n" + "nested \xe2\x86\xa9
\n" + "body1
\n" + "\n" + "\n" + "
inside quote \xe2\x86\xa9
\n" + "body^two words
\n"); +} diff --git a/test/markdown/markdown_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 @@ +#includeSome text here.
\n"); +} + +TEST(link_inside_heading) { + const auto result{ + sourcemeta::core::markdown_to_html("## [Link](https://example.com)")}; + EXPECT_EQ(result, "main functionContent
\n"); +} + +TEST(heading_with_bold_and_code) { + const auto result{ + sourcemeta::core::markdown_to_html("## **Bold** and `code`")}; + EXPECT_EQ(result, "codebeta
\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, "## 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, "one line
\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\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" + "===
\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, "`
\n"); +} + +TEST(setext_heading_equals_underline_is_lazy_continuation) { + const auto result{sourcemeta::core::markdown_to_html("> Title\n===")}; + EXPECT_EQ(result, "\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, "Title\n" + "===
\n" + "
\n" + "\n" + "quote\n" + "continued
\n" + "
==
\n"); +} + +TEST(setext_heading_dashes_after_thematic_break) { + const auto result{sourcemeta::core::markdown_to_html("***\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, "Intro
\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" + "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" + "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"); +} 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








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

line one\nline two
\n"); +} + +TEST(hard_line_break_with_two_spaces) { + const auto result{sourcemeta::core::markdown_to_html("line one \nline two")}; + EXPECT_EQ(result, "line one
\nline two
line one
\nline two
one
\n"
+ "two
one
\n"
+ "two
one
\n"
+ "two
one
\n"
+ "two
one two
text\\
\n" + "text
\n" + "one\n" + "two
\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[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, "\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, "\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, "\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, "\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, "\n"); +} + +TEST(link_reference_definition_label_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[DoCs]: /manual\n\n[dOcS]")}; + EXPECT_EQ(result, "\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, "\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" + "\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, "\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, "quoted
\n" + "
---\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]: 3>\n\n[one] [two] " + "[three]")}; + EXPECT_EQ(result, "\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" + "\n"); +} + +TEST(link_reference_definition_label_whitespace_normalized) { + const auto result{sourcemeta::core::markdown_to_html( + "[the long\n label]: /manual\n\n[The long label]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_reference_definition_empty_label_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[]: /manual\n\n[] text")}; + EXPECT_EQ(result, "
[]: /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(link_destination_single_space) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( )")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_empty_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]( <> )")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_angle_brackets_with_spaces) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_with_space_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a b)")}; + EXPECT_EQ(result, "[docs](/a b)
\n"); +} + +TEST(link_destination_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/a\n/b)")}; + EXPECT_EQ(result, "[docs](/a\n" + "/b)
\n"); +} + +TEST(link_destination_angle_brackets_with_newline_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "[docs](</a\n" + "b>)
\n"); +} + +TEST(link_destination_angle_brackets_with_closing_paren) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_unmatched_angle_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "[docs](<a>)
\n"); +} + +TEST(link_destination_invalid_angle_bracket_forms) { + const auto result{sourcemeta::core::markdown_to_html("[x]([x](<y)z\n" + "[x](z)
\n"); +} + +TEST(link_destination_escaped_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](\\(a\\)b)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_balanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)e)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_unbalanced_parentheses_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a(b(c)d)")}; + EXPECT_EQ(result, "[docs](a(b(c)d)
\n"); +} + +TEST(link_destination_escaped_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\(b\\(c)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_angle_brackets_unbalanced_parentheses) { + const auto result{sourcemeta::core::markdown_to_html("[docs]()")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_backslash_escapes) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\]\\!)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_fragments_and_queries) { + const auto result{sourcemeta::core::markdown_to_html( + "[x](#top)\n\n[y](https://sourcemeta.com/?q=1#top)")}; + EXPECT_EQ(result, + "\n" + "\n"); +} + +TEST(link_destination_backslash_before_letter) { + const auto result{sourcemeta::core::markdown_to_html("[docs](a\\zb)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_percent_encoding_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](caf%C3%A9é)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_destination_that_looks_like_title) { + const auto result{sourcemeta::core::markdown_to_html("[docs]('Title')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_title_quote_styles) { + const auto result{sourcemeta::core::markdown_to_html( + "[a](/x 'one') [b](/y (two)) [c](/z \"three\")")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(link_title_escaped_quotes_and_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x 'it\\'s & more')")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(link_title_separated_by_non_breaking_space) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](/x\xC2\xA0'title')")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_title_with_unescaped_inner_quote_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs](/x 'a'b')")}; + EXPECT_EQ(result, "[docs](/x 'a'b')
\n"); +} + +TEST(link_title_parentheses_with_quotes_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "[docs](/x (with 'single' and \"double\"))")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_whitespace_around_destination_and_title) { + const auto result{ + sourcemeta::core::markdown_to_html("[docs](\n /x\n 'title'\n)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_spaces_before_parentheses_is_not_link) { + const auto result{sourcemeta::core::markdown_to_html("[docs] (/x)")}; + EXPECT_EQ(result, "[docs] (/x)
\n"); +} + +TEST(link_text_with_balanced_brackets) { + const auto result{sourcemeta::core::markdown_to_html("[a [b] [c [d]]](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_unbalanced_closing_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a] b](/x)")}; + EXPECT_EQ(result, "[a] b](/x)
\n"); +} + +TEST(link_text_with_unbalanced_opening_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a [b](/x)")}; + EXPECT_EQ(result, "[a b
\n"); +} + +TEST(link_text_with_escaped_bracket) { + const auto result{sourcemeta::core::markdown_to_html("[a \\] b](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_inline_content) { + const auto result{ + sourcemeta::core::markdown_to_html("[_a_ __b__ `c` ~~d~~](/x)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_text_with_image) { + const auto result{ + sourcemeta::core::markdown_to_html("[](/home)")}; + EXPECT_EQ(result, "\n"); +} + +TEST(link_inside_link_text_is_not_allowed) { + const auto result{ + sourcemeta::core::markdown_to_html("[a [b](/inner) c](/outer)")}; + EXPECT_EQ(result, "[a b c](/outer)
\n"); +} + +TEST(link_inside_emphasis_inside_link_text) { + const auto result{ + sourcemeta::core::markdown_to_html("[a _[b](/inner)_](/outer)")}; + EXPECT_EQ(result, "[a b](/outer)
\n"); +} + +TEST(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, "\n"); +} + +TEST(link_code_span_takes_precedence) { + const auto result{sourcemeta::core::markdown_to_html("[a`](/x)`")}; + EXPECT_EQ(result, "[a](/x)
[ahttps://" + "sourcemeta.com/?q=](/x)
\n"); +} + +TEST(reference_link_full) { + const auto result{sourcemeta::core::markdown_to_html( + "[the docs][manual]\n\n[manual]: /manual 'Manual'")}; + EXPECT_EQ(result, + "\n"); +} + +TEST(reference_link_text_with_nested_brackets) { + const auto result{ + sourcemeta::core::markdown_to_html("[a [b] c][ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_case_folding) { + const auto result{sourcemeta::core::markdown_to_html( + "[text][MaNuAl]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_titlecase_digraph_folding) { + const auto result{sourcemeta::core::markdown_to_html("[Dž]\n\n[dž]: /digraph")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_space_between_labels_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a] [ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "[a] ref
\n"); +} + +TEST(reference_link_newline_between_labels_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a]\n[ref]\n\n[ref]: /ref")}; + EXPECT_EQ(result, "[a]\n" + "ref
\n"); +} + +TEST(reference_link_uses_first_definition) { + const auto result{sourcemeta::core::markdown_to_html( + "[a][ref]\n\n[ref]: /first\n[ref]: /second")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_escapes_are_literal) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][ref\\*]\n\n[ref*]: /ref")}; + EXPECT_EQ(result, "[a][ref*]
\n"); +} + +TEST(reference_link_label_with_unescaped_bracket_is_invalid) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b]c]\n\n[b]c]: /x")}; + EXPECT_EQ(result, "[a][b]c]
\n" + "[b]c]: /x
\n"); +} + +TEST(reference_link_label_with_escaped_bracket) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b\\]c]\n\n[b\\]c]: /x")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_label_with_escaped_backslash) { + const auto result{ + sourcemeta::core::markdown_to_html("[a\\\\b]: /x\n\n[a\\\\b]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_empty_brackets_are_literal) { + const auto result{sourcemeta::core::markdown_to_html("[][]\n\n[]: /x")}; + EXPECT_EQ(result, "[][]
\n" + "[]: /x
\n"); +} + +TEST(collapsed_reference_link_case_insensitive) { + const auto result{ + sourcemeta::core::markdown_to_html("[MANUAL][]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(collapsed_reference_link_across_lines_is_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual]\n[]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "manual\n" + "[]
\n"); +} + +TEST(shortcut_reference_link_with_emphasis) { + const auto result{sourcemeta::core::markdown_to_html( + "[_the_ manual]\n\n[_the_ manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(shortcut_reference_link_escaped) { + const auto result{ + sourcemeta::core::markdown_to_html("\\[manual\\]\n\n[manual]: /manual")}; + EXPECT_EQ(result, "[manual]
\n"); +} + +TEST(shortcut_reference_link_label_with_underscore) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual_]: /manual\n\n_[manual_]")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_full_takes_precedence_over_shortcut) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b][c]\n\n[b]: /b\n[c]: /c")}; + EXPECT_EQ(result, "\n"); +} + +TEST(reference_link_undefined_label_falls_back) { + const auto result{ + sourcemeta::core::markdown_to_html("[a][b][c]\n\n[a]: /a\n[c]: /c")}; + EXPECT_EQ(result, "[a]b
\n"); +} + +TEST(inline_link_takes_precedence_over_reference) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual]()\n\n[manual]: /manual")}; + EXPECT_EQ(result, "\n"); +} + +TEST(inline_link_invalid_falls_back_to_reference) { + const auto result{ + sourcemeta::core::markdown_to_html("[manual](a b)\n\n[manual]: /manual")}; + EXPECT_EQ(result, "manual(a b)
\n"); +} 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 @@ +#includeone
\ntwo
\nthree
\nitem:
\ncode\n\n"
+ "next
\nfirst
\nmore text
\nsecond
\nparagraph one
\nparagraph two
\n" + "paragraph three
\ntwo
\n"); +} + +TEST(list_item_content_with_marker_width_indentation) { + const auto result{sourcemeta::core::markdown_to_html("* one\n\n two")}; + EXPECT_EQ(result, "one
\n" + "two
\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, "one
\n" + "two
\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" + "
\n" + "- \n" + "
\n" + "one
\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, "\n" + "\n" + "\n" + "
\n" + "- one
\n" + "two
\n" + "
*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, "one
\n" + "two
\n" + "one
\n" + "code\n"
+ "\n"
+ "two
\n" + "\n" + "\n" + "quote
\n" + "
one
\n" + "code\n"
+ "\n"
+ "\n"
+ "more\n"
+ "\n"
+ "9999999999) over
\n"); +} + +TEST(ordered_list_starting_at_zero) { + const auto result{sourcemeta::core::markdown_to_html("0) zero")}; + 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, "text
\n" + "code\n"
+ "\n"
+ "text
\n" + "code\n"
+ "\n"
+ "code\n"
+ "\n"
+ "text
\n" + "more\n"
+ "\n"
+ " code\n"
+ "\n"
+ "text
\n" + "code\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, "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, "one\n" + "two
\n" + "code\n"
+ "\n"
+ "- 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, "one\n" + "two
\n" + "code\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" + "
\n" + "- \n" + "
\n" + "\n" + "\n" + "quoted\n" + "lazy line
\n" + "
Text
\n" + "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" + "one
\n" + "two
\n" + "b
\n" + "c
\n" + "a
\n" + "b
\n" + "a
\n" + "b
\n" + "c
\n" + "a
\n" + "b
\n" + "a
\n" + "b
\n" + "c
\n" + "a
\n" + "b
\n" + "x\n"
+ "\n"
+ "\n"
+ "b
\n" + "c
\n" + "\n" + "\n" + "b
\n" + "c
\n" + "
\n" + "\n" + "b
\n" + "
c\n"
+ "\n"
+ "x\n"
+ "\n"
+ "y
\n" + "a
\n" + "c
\n" + "a
\n" + "d
\n" + "Hello world
\n"); +} + +TEST(multiple_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html( + "First paragraph\n\nSecond paragraph")}; + EXPECT_EQ(result, "First paragraph
\nSecond paragraph
\n"); +} + +TEST(three_paragraphs) { + const auto result{sourcemeta::core::markdown_to_html("One\n\nTwo\n\nThree")}; + EXPECT_EQ(result, "One
\nTwo
\nThree
\n"); +} + +TEST(empty_input) { + const auto result{sourcemeta::core::markdown_to_html("")}; + EXPECT_TRUE(result.empty()); +} + +TEST(whitespace_only) { + const auto result{sourcemeta::core::markdown_to_html(" \n\n ")}; + EXPECT_TRUE(result.empty()); +} + +TEST(paragraph_with_inline_code_and_link) { + const auto result{sourcemeta::core::markdown_to_html( + "Use `foo()` from [the docs](https://docs.com).")}; + EXPECT_EQ(result, "Use foo() from "
+ "the docs.
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
one
\n" + "" + repeat("x y ", 5000) + "z" + + repeat(" y x", 5000) + "
\n"); +} + +TEST(underscore_closers_before_punctuation_without_openers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("x_. ", 10000))}; + EXPECT_EQ(result, "" + repeat("x_. ", 9999) + "x_.
\n"); +} + +TEST(double_underscore_openers_without_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("__y ", 10000))}; + EXPECT_EQ(result, "" + repeat("__y ", 9999) + "__y
\n"); +} + +TEST(link_closers_without_openers) { + const auto result{sourcemeta::core::markdown_to_html(repeat("]b", 10000))}; + EXPECT_EQ(result, "" + repeat("]b", 10000) + "
\n"); +} + +TEST(image_openers_without_closers) { + const auto result{sourcemeta::core::markdown_to_html(repeat(")}; + EXPECT_EQ(result, "" + repeat("[ x](", 10000) + "
\n"); +} + +TEST(nested_image_brackets) { + const auto input{repeat("![", 5000) + "x" + repeat("]", 5000)}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "" + input + "
\n"); +} + +TEST(unclosed_image_angle_bracket_destinations) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat(" + "
\n"); +} + +TEST(backtick_runs_of_increasing_length) { + const auto input{increasing_backtick_runs(2000)}; + const auto result{sourcemeta::core::markdown_to_html(input)}; + EXPECT_EQ(result, "" + input + "
\n"); +} + +TEST(code_span_with_eighty_backticks) { + const auto result{sourcemeta::core::markdown_to_html( + std::string(80, '`') + "x" + std::string(80, '`'))}; + EXPECT_EQ(result, "x
" + input + "
\n"); +} + +TEST(alternating_blockquote_and_list_nesting_on_one_line) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat("> - ", 30) + "deep")}; + EXPECT_EQ(result, repeat("\n\n", 29)); +} + +TEST(alternating_blockquote_and_list_nesting_on_one_line_beyond_depth_limit) { + const auto result{ + sourcemeta::core::markdown_to_html(repeat("> - ", 50) + "deep")}; + EXPECT_EQ(result, repeat("\n
\n- \n", 29) + + "
\n\n\n" + + repeat("\n
\n" + "- deep
\n
\n\n", 49)); +} + +TEST(staircase_list_of_one_hundred_fifty_levels) { + const auto result{sourcemeta::core::markdown_to_html(staircase_list(150))}; + EXPECT_EQ(result, repeat("\n
\n- \n", 49) + + "
\n\n\n" + + repeat("- deep
\n
| a | \nb | \nc | \n" + "
|---|---|---|
| x | \ny | \nz | \n
| h | \n", 1000) + + "
|---|
| c | \n", 1000) + + "
[" + 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..0eecb7fd87 --- /dev/null +++ b/test/markdown/markdown_safety_test.cc @@ -0,0 +1,218 @@ +#includeHello world
\n"); +} + +TEST(safe_mode_omits_raw_html) { + const auto result{ + sourcemeta::core::markdown_to_html("Hello world
\n"); +} + +TEST(raw_html_block_is_sanitized) { + const auto result{ + sourcemeta::core::markdown_to_html("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( + "strong
\n" + "\n"); +} + +TEST(safe_mode_omits_html_block_with_blank_line_inside) { + const auto result{sourcemeta::core::markdown_to_html( + "\n__text__\n\n_more_\n
\nmore
\n" + "\n"); +} + +TEST(safe_mode_omits_inline_tag_with_attributes) { + const auto result{ + sourcemeta::core::markdown_to_html("x y")}; + EXPECT_EQ(result, "x y
\n"); +} + +TEST(safe_mode_escapes_markup_in_link_title) { + const auto result{sourcemeta::core::markdown_to_html("[x](/y '&\"')")}; + EXPECT_EQ( + result, + "\n"); +} + +TEST(safe_mode_escapes_quotes_in_image_title) { + const auto result{ + sourcemeta::core::markdown_to_html(R"MD()MD")}; + EXPECT_EQ( + result, + "
x
\n"); +} + +TEST(safe_mode_omits_raw_html_taking_precedence_over_link) { + const auto result{ + sourcemeta::core::markdown_to_html("[x ")}; + EXPECT_EQ(result, "[x
\n"); +} diff --git a/test/markdown/markdown_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 @@ +#includedeleted
deleted bold text
This is wrong correct
| h | \n
|---|
use old_func()
all three
~not deleted~
\n"); +} + +TEST(strikethrough_spanning_lines) { + const auto result{sourcemeta::core::markdown_to_html("~~one\ntwo~~")}; + EXPECT_EQ(result, "one\n"
+ "two
x ~~~y~~~ z
\n"); +} + +TEST(strikethrough_unclosed) { + const auto result{sourcemeta::core::markdown_to_html("x ~~y")}; + EXPECT_EQ(result, "x ~~y
\n"); +} + +TEST(strikethrough_with_surrounding_whitespace) { + const auto result{sourcemeta::core::markdown_to_html("x ~~ y ~~ z")}; + EXPECT_EQ(result, "x ~~ y ~~ z
\n"); +} + +TEST(strikethrough_and_emphasis_overlap) { + const auto result{sourcemeta::core::markdown_to_html("~~_x~~_")}; + EXPECT_EQ(result, "_x_
~~x~~
\n"); +} + +TEST(strikethrough_inside_link_text) { + const auto result{sourcemeta::core::markdown_to_html("[~~old~~ new](/x)")}; + EXPECT_EQ(result, "\n"); +} diff --git a/test/markdown/markdown_tables_test.cc b/test/markdown/markdown_tables_test.cc new file mode 100644 index 0000000000..920ed27a96 --- /dev/null +++ b/test/markdown/markdown_tables_test.cc @@ -0,0 +1,417 @@ +#include| a | \nb | \n
|---|---|
| 1 | \n2 | \n
| left | \n" + "center | \n" + "right | \n" + "
|---|---|---|
| a | \n" + "b | \n" + "c | \n" + "
| h | \n
|---|
| bold | \n
| x | \ny | \n
|---|---|
| 1 | \n2 | \n
| 3 | \n4 | \n
| 5 | \n6 | \n
| h | \n
|---|
| a | b | \n
| h | \n
|---|
| click" + " | \n
| h | \n
|---|
code | \n
| a | \nb | \n
|---|---|
| \n | \n |
| a | \nb | \n
|---|
Here is a table:
\n" + "| a | \n
|---|
| 1 | \n
key | \n"
+ "value | \n
|---|---|
| a | \nb | \n
| code | \n" + "
|---|
a|b | \n"
+ "
| c|d | \n" + "
| a | \n" + "b | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
after
\n"); +} + +TEST(table_ends_at_blockquote) { + const auto result{ + sourcemeta::core::markdown_to_html("| a |\n|---|\n| 1 |\n> quote")}; + EXPECT_EQ(result, "| a | \n" + "
|---|
| 1 | \n" + "
\n" + "\n"); +} + +TEST(table_line_without_pipes_is_row) { + const auto result{ + sourcemeta::core::markdown_to_html("| a | b |\n|---|---|\ntext row")}; + EXPECT_EQ(result, "quote
\n" + "
| a | \n" + "b | \n" + "
|---|---|
| text row | \n" + "\n" + " |
| a | b | c |\n" + "| - | - |
\n"); +} + +TEST(table_rows_with_fewer_and_more_cells) { + const auto result{sourcemeta::core::markdown_to_html( + "| a | b |\n| - | - |\n| 1 |\n| 1 | 2 | 3 |")}; + EXPECT_EQ(result, "| a | \n" + "b | \n" + "
|---|---|
| 1 | \n" + "\n" + " |
| 1 | \n" + "2 | \n" + "
| x | \n" + "y | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
| only | \n" + "
|---|
| one | \n" + "
| l | \n" + "r | \n" + "c | \n" + "n | \n" + "
|---|---|---|---|
| 1 | \n" + "2 | \n" + "3 | \n" + "4 | \n" + "
| x | \n" + "y | \n" + "
|---|
| a | b | \n" + "c | \n" + "
|---|
| manual | \n" + "
|---|
intro\n" + "a | b
\n" + "\n" + "\n"); +} + +TEST(table_cell_with_entities) { + const auto result{ + sourcemeta::core::markdown_to_html("| <tag> |\n| --- |")}; + EXPECT_EQ(result, "a | b
\n" + "\n" + "
\n" + "- | -\n" + "1 | 2
\n" + "
| <tag> | \n" + "
|---|
| \n" + " | \n" + " |
|---|---|
| x | \n" + "y | \n" + "
| _a | \n" + "b_ | \n" + "
|---|
| a |\n" + "| :-:x |
\n"); +} + +TEST(table_delimiter_count_mismatch_without_outer_pipes) { + const auto result{sourcemeta::core::markdown_to_html("x | y | z\n- | -")}; + EXPECT_EQ(result, "x | y | z
\n" + "| x | \n" + "y | \n" + "
|---|---|
| 1 | \n" + "2 | \n" + "
buga
\n" + "b
\n" + "hello world
\n"); - 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"); -} - -TEST(safe_mode_renders_plain_content) { - const auto result{ - sourcemeta::core::markdown_to_html("Hello **world**", true)}; - EXPECT_EQ(result, "Hello world
\n"); -} - -TEST(safe_mode_omits_raw_html) { - const auto result{ - sourcemeta::core::markdown_to_html("First paragraph
\nSecond paragraph
\n"); -} - -TEST(three_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html("One\n\nTwo\n\nThree")}; - EXPECT_EQ(result, "One
\nTwo
\nThree
\n"); -} - -TEST(empty_input) { - const auto result{sourcemeta::core::markdown_to_html("")}; - EXPECT_TRUE(result.empty()); -} - -TEST(whitespace_only) { - const auto result{sourcemeta::core::markdown_to_html(" \n\n ")}; - EXPECT_TRUE(result.empty()); -} - -TEST(soft_line_break) { - const auto result{sourcemeta::core::markdown_to_html("line one\nline two")}; - EXPECT_EQ(result, "line one\nline two
\n"); -} - -TEST(hard_line_break_with_two_spaces) { - const auto result{sourcemeta::core::markdown_to_html("line one \nline two")}; - EXPECT_EQ(result, "line one
\nline two
line one
\nline two
italic
\n"); -} - -TEST(italic_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("_italic_")}; - EXPECT_EQ(result, "italic
\n"); -} - -TEST(bold_with_asterisks) { - const auto result{sourcemeta::core::markdown_to_html("**bold**")}; - EXPECT_EQ(result, "bold
\n"); -} - -TEST(bold_with_underscores) { - const auto result{sourcemeta::core::markdown_to_html("__bold__")}; - EXPECT_EQ(result, "bold
\n"); -} - -TEST(bold_and_italic) { - const auto result{sourcemeta::core::markdown_to_html("***bold italic***")}; - EXPECT_EQ(result, "bold italic
\n"); -} - -TEST(bold_inside_italic) { - const auto result{ - sourcemeta::core::markdown_to_html("*this is **bold** inside italic*")}; - EXPECT_EQ(result, - "this is bold inside italic
\n"); -} - -TEST(italic_inside_bold) { - const auto result{ - sourcemeta::core::markdown_to_html("**this is *italic* inside bold**")}; - EXPECT_EQ(result, - "this is italic inside bold
\n"); -} - -TEST(inline_code) { - const auto result{sourcemeta::core::markdown_to_html("Use `printf()`")}; - EXPECT_EQ(result, "Use printf()
there is a ` here
<div class="foo">
code here\n\n");
-}
-
-TEST(fenced_code_block_tildes) {
- const auto result{sourcemeta::core::markdown_to_html("~~~\ncode here\n~~~")};
- EXPECT_EQ(result, "code here\n\n");
-}
-
-TEST(fenced_code_block_with_language) {
- const auto result{
- sourcemeta::core::markdown_to_html("```cpp\nint x = 0;\n```")};
- EXPECT_EQ(result, "int x = 0;\n\n");
-}
-
-TEST(fenced_code_block_html_escaped) {
- const auto result{
- sourcemeta::core::markdown_to_html("```\n<div>&</div>\n\n");
-}
-
-TEST(indented_code_block) {
- const auto result{
- sourcemeta::core::markdown_to_html(" int x = 0;\n return x;")};
- EXPECT_EQ(result, "int x = 0;\nreturn x;\n\n");
-}
-
-TEST(inline_link) {
- const auto result{
- sourcemeta::core::markdown_to_html("[click here](https://example.com)")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(inline_link_with_title) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click](https://example.com \"My Title\")")};
- EXPECT_EQ(
- result,
- "\n");
-}
-
-TEST(reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click][ref]\n\n[ref]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(reference_link_with_title) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[click][ref]\n\n[ref]: https://example.com \"Title\"")};
- EXPECT_EQ(
- result,
- "\n");
-}
-
-TEST(collapsed_reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[example][]\n\n[example]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(shortcut_reference_link) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[example]\n\n[example]: https://example.com")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(link_with_emphasis_inside) {
- const auto result{sourcemeta::core::markdown_to_html(
- "[**bold link**](https://example.com)")};
- EXPECT_EQ(result, "\n");
-}
-
-TEST(image) {
- const auto result{
- sourcemeta::core::markdown_to_html("")};
- EXPECT_EQ(result, "

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

line one\n\nline three\n\n");
-}
-
-TEST(ordered_list_with_paragraphs) {
- const auto result{sourcemeta::core::markdown_to_html(
- "1. first\n\n more text\n\n2. second")};
- EXPECT_EQ(result, "first
\nmore text
\nsecond
\nContent
\n"); -} - -TEST(table_after_paragraph) { - const auto result{sourcemeta::core::markdown_to_html( - "Here is a table:\n\n| a |\n| - |\n| 1 |")}; - EXPECT_EQ(result, "Here is a table:
\n" - "| a | \n
|---|
| 1 | \n
\n\n" - "quote
\n
See https://one.com and " - "https://two.com
\n"); -} - -TEST(emphasis_does_not_cross_code_span) { - const auto result{sourcemeta::core::markdown_to_html("*start `code* end`")}; - EXPECT_EQ(result, "*start code* end
a
\n"); -} - -TEST(list_item_with_multiple_paragraphs) { - const auto result{sourcemeta::core::markdown_to_html( - "- paragraph one\n\n paragraph two\n\n paragraph three")}; - EXPECT_EQ(result, "paragraph one
\nparagraph two
\n" - "paragraph three
\nkey | \n"
- "value | \n
|---|---|
| a | \nb | \n
all three
See https://example.com.
\n"); -} - -TEST(autolink_in_parentheses) { - const auto result{ - sourcemeta::core::markdown_to_html("(see https://example.com)")}; - EXPECT_EQ( - result, - "(see https://example.com)
\n"); -} - -TEST(blockquote_with_strikethrough) { - const auto result{sourcemeta::core::markdown_to_html("> ~~deleted~~")}; - EXPECT_EQ(result, "\n\n"); -} - -TEST(heading_with_bold_and_code) { - const auto result{ - sourcemeta::core::markdown_to_html("## **Bold** and `code`")}; - EXPECT_EQ(result, "\n
deleted
codeText1
\n" - "Footnote content " - "\xe2\x86\xa9
\n" - "First " - "\xe2\x86\xa9
\n" - "Second " - "\xe2\x86\xa9
\n" - "~not deleted~
\n"); -} - -TEST(github_pre_lang_format) { - const auto result{ - sourcemeta::core::markdown_to_html("```python\nprint()\n```")}; - EXPECT_EQ(result, "print()\n\n");
-}
-
-TEST(invalid_utf8_is_replaced) {
- const auto result{sourcemeta::core::markdown_to_html("hello \xFF world")};
- EXPECT_EQ(result, "hello \xEF\xBF\xBD world
\n"); -} diff --git a/test/markdown/markdown_thematic_breaks_test.cc b/test/markdown/markdown_thematic_breaks_test.cc new file mode 100644 index 0000000000..45c31cc640 --- /dev/null +++ b/test/markdown/markdown_thematic_breaks_test.cc @@ -0,0 +1,131 @@ +#include++++
\n"); +} + +TEST(thematic_break_equals_signs_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("= = = =")}; + EXPECT_EQ(result, "= = = =
\n"); +} + +TEST(thematic_break_two_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("**\n\n__")}; + EXPECT_EQ(result, "**
\n" + "__
\n"); +} + +TEST(thematic_break_indented_one_to_three_spaces) { + const auto result{sourcemeta::core::markdown_to_html(" ___\n ---\n ***")}; + EXPECT_EQ(result, "---\n"
+ "\n");
+}
+
+TEST(thematic_break_indented_four_spaces_continues_paragraph) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("Paragraph text\n ___")};
+ EXPECT_EQ(result, "Paragraph text\n" + "___
\n"); +} + +TEST(thematic_break_many_characters) { + const auto result{sourcemeta::core::markdown_to_html( + "-------------------------------------------------")}; + EXPECT_EQ(result, "x______
\n" + "x
\n"); +} + +TEST(thematic_break_mixed_characters_is_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("-*-")}; + EXPECT_EQ(result, "-*-
\n"); +} + +TEST(thematic_break_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html("alpha\n___\nbeta")}; + EXPECT_EQ(result, "alpha
\n" + "beta
\n"); +} + +TEST(thematic_break_takes_precedence_over_list_item) { + const auto result{sourcemeta::core::markdown_to_html("- one\n- - -\n- two")}; + EXPECT_EQ(result, "after
\n"); +} + +TEST(html_block_pre_keeps_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nline one\n\n*not emphasis*\n\nafter", false)}; + EXPECT_EQ(result, "
\n" + "line one\n" + "\n" + "*not emphasis*\n" + "\n" + "
after
\n"); +} + +TEST(html_block_style_with_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "<style>\n" + "\n" + ".x { color: red; }\n" + "\n" + "</style>\n" + "after
\n"); +} + +TEST(html_block_comment_with_blank_lines) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_processing_instruction) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_declaration) { + const auto result{ + sourcemeta::core::markdown_to_html("", false)}; + EXPECT_EQ(result, "\n"); +} + +TEST(html_block_cdata_section) { + const auto result{sourcemeta::core::markdown_to_html( + "\nafter", false)}; + EXPECT_EQ(result, "\n" + "after
\n"); +} + +TEST(html_block_ends_at_blank_line) { + const auto result{sourcemeta::core::markdown_to_html( + "two
\n" + "Text\n" + "\n" + "more
\n"); +} + +TEST(html_block_section_interrupts_paragraph) { + const auto result{sourcemeta::core::markdown_to_html( + "Text\nText
\n" + "<div>code</div>\n"
+ "\n");
+}
+
+TEST(html_block_inside_list_item) {
+ const auto result{
+ sourcemeta::core::markdown_to_html("* \n"); +} + +TEST(inline_html_invalid_tag_names) { + const auto result{sourcemeta::core::markdown_to_html("<1x> <-x>", false)}; + EXPECT_EQ(result, "
<1x> <-x>
\n"); +} + +TEST(inline_html_invalid_attribute_name) { + const auto result{ + sourcemeta::core::markdown_to_html("", false)}; + EXPECT_EQ(result, "<span 1x="y">
\n"); +} + +TEST(inline_html_invalid_attribute_values) { + const auto result{ + sourcemeta::core::markdown_to_html(" \n"); +} + +TEST(inline_html_invalid_whitespace) { + const auto result{sourcemeta::core::markdown_to_html( + "< span> ", false)}; + EXPECT_EQ(result, "< span> <span/ > <span a=b\n" + "c!d>
\n"); +} + +TEST(inline_html_closing_tags) { + const auto result{ + sourcemeta::core::markdown_to_html("</span class="x">
\n"); +} + +TEST(inline_html_comment_with_double_hyphen) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_comment_edge_forms) { + const auto result{sourcemeta::core::markdown_to_html( + "x y -->\n\nx y -->", false)}; + EXPECT_EQ(result, "x y -->
\n" + "x y -->
\n"); +} + +TEST(inline_html_processing_instruction) { + const auto result{sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_declaration) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_cdata_section) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_entity_in_attribute_is_preserved) { + const auto result{ + sourcemeta::core::markdown_to_html("x ", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(inline_html_backslash_in_attribute_is_preserved) { + const auto result{ + sourcemeta::core::markdown_to_html(R"MD(x )MD", false)}; + EXPECT_EQ(result, "x
\n"); +} + +TEST(tagfilter_every_disallowed_tag) { + const auto result{sourcemeta::core::markdown_to_html( + "