From 9292d3c267375dd464ae981bdeb2227b07134ae6 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 14:56:25 -0700 Subject: [PATCH 01/11] Improve the emphasis regex. I was getting failures where URLs with underscores were getting blocks thrown in them, creating invalid HTML. --- include/maddy/emphasizedparser.h | 4 +- tests/maddy/test_maddy_emphasizedparser.cpp | 109 ++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/include/maddy/emphasizedparser.h b/include/maddy/emphasizedparser.h index 1705e6f..af30928 100644 --- a/include/maddy/emphasizedparser.h +++ b/include/maddy/emphasizedparser.h @@ -40,8 +40,10 @@ class EmphasizedParser : public LineParser */ void Parse(std::string& line) override { + // Modifed from previous version, with help from + // https://stackoverflow.com/questions/61346949/regex-for-markdown-emphasis static std::regex re( - R"((?!.*`.*|.*.*)_(?!.*`.*|.*<\/code>.*)([^_]*)_(?!.*`.*|.*<\/code>.*))" + R"((?!.*`.*|.*.*)\b_(?![\s])(?!.*`.*|.*<\/code>.*)(.*?[^\s])_\b(?!.*`.*|.*<\/code>.*))" ); static std::string replacement = "$1"; diff --git a/tests/maddy/test_maddy_emphasizedparser.cpp b/tests/maddy/test_maddy_emphasizedparser.cpp index 6442779..8da0b4f 100644 --- a/tests/maddy/test_maddy_emphasizedparser.cpp +++ b/tests/maddy/test_maddy_emphasizedparser.cpp @@ -21,6 +21,85 @@ TEST(MADDY_EMPHASIZEDPARSER, ItReplacesMarkdownWithEmphasizedHTML) ASSERT_EQ(expected, text); } +TEST(MADDY_EMPHASIZEDPARSER, ItReplacesUnderscoresAtStringEdges) +{ + std::string text = "_some text_"; + std::string expected = "some text"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItDoesNotReplaceMarkdownWithInlineUnderscores) +{ + std::string text = "some text_bla_text testing _it_ out"; + std::string expected = "some text_bla_text testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItOnlyReplacesUnderscoresAtWordBreaks) +{ + std::string text = "some _text_bla_ testing _it_ out"; + std::string expected = "some text_bla testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItReplacesUnderscoresWithMultipleWords) +{ + std::string text = "some _text testing it_ out"; + std::string expected = "some text testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItAllowsDoubleUnderscores) +{ + // I'm not sure if this is standard or not, but this is how the github markdown + // parser behaves. Other things I've seen want it to *not* match. + std::string text = "some __text testing it_ out"; + std::string expected = "some _text testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItDoesntReplaceUnderscoresInsideCodeBlocks) +{ + std::string text = "Stuff inside blocks _shouldn't be emphasized_ at all"; + std::string expected = "Stuff inside blocks _shouldn't be emphasized_ at all"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItDoesNotReplaceUnderscoresInURLs) +{ + std::string text = "[Link Title](http://example.com/what_you_didn't_know)"; + std::string expected = "[Link Title](http://example.com/what_you_didn't_know)"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + TEST(MADDY_EMPHASIZEDPARSER, ItDoesNotParseInsideInlineCode) { std::string text = "some text `*bla*` `/**text*/` testing _it_ out"; @@ -32,3 +111,33 @@ TEST(MADDY_EMPHASIZEDPARSER, ItDoesNotParseInsideInlineCode) ASSERT_EQ(expected, text); } + +TEST(MADDY_EMPHASIZEDPARSER, ItParsesOutsideCodeBlocks) +{ + std::string text = + "Stuff inside blocks _shouldn't be emphasized_ " + " but outside _should_."; + std::string expected = + "Stuff inside blocks _shouldn't be emphasized_ " + " but outside should."; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItParsesOutsideTickBlocks) +{ + std::string text = + "Stuff inside `blocks _shouldn't be emphasized_ `" + " but outside _should_."; + std::string expected = + "Stuff inside `blocks _shouldn't be emphasized_ `" + " but outside should."; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} From 2df2fe7fa6e6a68e199d1c0d3c8c993d94b218eb Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 16:23:19 -0700 Subject: [PATCH 02/11] Add strong parser tests. Most don't pass (and are disabled), but should. Unfortunately, adding a word boundary (\b) to the strong regex works for these tests, but somehow the full parser then breaks. The regex that I believed should work is added as a comment, for anyone wishing to make things work going forward. --- include/maddy/strongparser.h | 23 ++++- tests/maddy/test_maddy_strongparser.cpp | 113 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h index 348f2d4..56df620 100644 --- a/include/maddy/strongparser.h +++ b/include/maddy/strongparser.h @@ -40,12 +40,31 @@ class StrongParser : public LineParser */ void Parse(std::string& line) override { + // This version of the regex is changed exactly the same way + // that the regex for the emphasized parser was changed, and + // it then passes all the 'disabled' tests in the 'strong parser' + // test, but then it fails general parsing. For some reason, + // "__text__" translates "text" even though there + // are no word boundaries at the correct places. It's weird! + // + //static std::vector res{ + // std::regex{ + // R"((?!.*`.*|.*.*)\b\*\*(?![\s])(?!.*`.*|.*<\/code>.*)" + // "(.*?[^\s])\*\*\b(?!.*`.*|.*<\/code>.*))" + // }, + // std::regex{ + // R"((?!.*`.*|.*.*)\b__(?![\s])(?!.*`.*|.*<\/code>.*)" + // "(.*?[^\s])__\b(?!.*`.*|.*<\/code>.*))" + // } + //}; static std::vector res{ std::regex{ - R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" + R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)" + "([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" }, std::regex{ - R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)([^__]*)__(?!.*`.*|.*<\/code>.*))" + R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)" + "([^__]*)__(?!.*`.*|.*<\/code>.*))" } }; static std::string replacement = "$1"; diff --git a/tests/maddy/test_maddy_strongparser.cpp b/tests/maddy/test_maddy_strongparser.cpp index f006e26..5fd962f 100644 --- a/tests/maddy/test_maddy_strongparser.cpp +++ b/tests/maddy/test_maddy_strongparser.cpp @@ -83,3 +83,116 @@ TEST(MADDY_STRONGPARSER, ItDoesNotParseInsideInlineCode) ASSERT_EQ(test.expected, test.text); } } + +TEST(MADDY_STRONGPARSER, ItReplacesUnderscoresAtStringEdges) +{ + std::string text = "__some text__"; + std::string expected = "some text"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(DISABLED_MADDY_STRONGPARSER, ItDoesNotReplaceMarkdownWithInlineUnderscores) +{ + std::string text = "some text__bla__text testing __it__ out"; + std::string expected = "some text__bla__text testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(DISABLED_MADDY_STRONGPARSER, ItOnlyReplacesUnderscoresAtWordBreaks) +{ + std::string text = "some __text__bla__ testing __it__ out"; + std::string expected = + "some text__bla testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItReplacesUnderscoresWithMultipleWords) +{ + std::string text = "some __text testing it__ out"; + std::string expected = "some text testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(DISABLED_MADDY_STRONGPARSER, ItAllowsTripleUnderscores) +{ + // I'm not sure if this is standard or not, but this is how the github + // markdown parser behaves. Other things I've seen want it to *not* match. + std::string text = "some ___text testing it__ out"; + std::string expected = "some _text testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItDoesntReplaceUnderscoresInsideCodeBlocks) +{ + std::string text = + "Stuff inside blocks __shouldn't be strong__ at all"; + std::string expected = + "Stuff inside blocks __shouldn't be strong__ at all"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(DISABLED_MADDY_STRONGPARSER, ItDoesNotReplaceUnderscoresInURLs) +{ + std::string text = "[Link Title](http://example.com/what__you__didn't__know)"; + std::string expected = + "[Link Title](http://example.com/what__you__didn't__know)"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItParsesOutsideCodeBlocks) +{ + std::string text = + "Stuff inside blocks __shouldn't be strong__ " + " but outside __should__."; + std::string expected = + "Stuff inside blocks __shouldn't be strong__ " + " but outside should."; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItParsesOutsideTickBlocks) +{ + std::string text = + "Stuff inside `blocks __shouldn't be strong__ `" + " but outside __should__."; + std::string expected = + "Stuff inside `blocks __shouldn't be strong__ `" + " but outside should."; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} From c89a98386501946ebabbc62e3a2911c4c101950e Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 16:30:39 -0700 Subject: [PATCH 03/11] Clang fixes; changelog. --- CHANGELOG.md | 1 + include/maddy/strongparser.h | 28 ++++++++++++------------- tests/maddy/test_maddy_strongparser.cpp | 4 +++- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c0fca..25072a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ maddy uses [semver versioning](https://semver.org/). ## Upcoming +* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Do not create emphasis tags not at word boundaries, i.e. `only_internal_underscores`. * ... ## version 1.5.0 2025-04-21 diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h index 56df620..5cf9c38 100644 --- a/include/maddy/strongparser.h +++ b/include/maddy/strongparser.h @@ -46,24 +46,22 @@ class StrongParser : public LineParser // test, but then it fails general parsing. For some reason, // "__text__" translates "text" even though there // are no word boundaries at the correct places. It's weird! - // - //static std::vector res{ - // std::regex{ - // R"((?!.*`.*|.*.*)\b\*\*(?![\s])(?!.*`.*|.*<\/code>.*)" - // "(.*?[^\s])\*\*\b(?!.*`.*|.*<\/code>.*))" - // }, - // std::regex{ - // R"((?!.*`.*|.*.*)\b__(?![\s])(?!.*`.*|.*<\/code>.*)" - // "(.*?[^\s])__\b(?!.*`.*|.*<\/code>.*))" - // } - //}; + + // static std::vector res{ + // std::regex{ + // R"((?!.*`.*|.*.*)\b\*\*(?![\s])(?!.*`.*|.*<\/code>.*)" + // "(.*?[^\s])\*\*\b(?!.*`.*|.*<\/code>.*))" + // }, + // std::regex{ + // R"((?!.*`.*|.*.*)\b__(?![\s])(?!.*`.*|.*<\/code>.*)" + // "(.*?[^\s])__\b(?!.*`.*|.*<\/code>.*))" + // } + // }; static std::vector res{ - std::regex{ - R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)" + std::regex{R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)" "([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" }, - std::regex{ - R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)" + std::regex{R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)" "([^__]*)__(?!.*`.*|.*<\/code>.*))" } }; diff --git a/tests/maddy/test_maddy_strongparser.cpp b/tests/maddy/test_maddy_strongparser.cpp index 5fd962f..466068d 100644 --- a/tests/maddy/test_maddy_strongparser.cpp +++ b/tests/maddy/test_maddy_strongparser.cpp @@ -132,7 +132,9 @@ TEST(MADDY_STRONGPARSER, ItReplacesUnderscoresWithMultipleWords) TEST(DISABLED_MADDY_STRONGPARSER, ItAllowsTripleUnderscores) { // I'm not sure if this is standard or not, but this is how the github - // markdown parser behaves. Other things I've seen want it to *not* match. + // markdown parser behaves. Other things I've seen want it to *not* + // match. + std::string text = "some ___text testing it__ out"; std::string expected = "some _text testing it out"; auto strongParser = std::make_shared(); From 1a12c2c3ea218b3a7d4e4b7e7e86b1e64f0d9576 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 16:34:08 -0700 Subject: [PATCH 04/11] More clang fixes. --- include/maddy/strongparser.h | 6 ++---- tests/maddy/test_maddy_emphasizedparser.cpp | 14 +++++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h index 5cf9c38..8a66d6e 100644 --- a/include/maddy/strongparser.h +++ b/include/maddy/strongparser.h @@ -59,11 +59,9 @@ class StrongParser : public LineParser // }; static std::vector res{ std::regex{R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)" - "([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" - }, + "([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))"}, std::regex{R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)" - "([^__]*)__(?!.*`.*|.*<\/code>.*))" - } + "([^__]*)__(?!.*`.*|.*<\/code>.*))"} }; static std::string replacement = "$1"; for (const auto& re : res) diff --git a/tests/maddy/test_maddy_emphasizedparser.cpp b/tests/maddy/test_maddy_emphasizedparser.cpp index 8da0b4f..a70c248 100644 --- a/tests/maddy/test_maddy_emphasizedparser.cpp +++ b/tests/maddy/test_maddy_emphasizedparser.cpp @@ -67,8 +67,9 @@ TEST(MADDY_EMPHASIZEDPARSER, ItReplacesUnderscoresWithMultipleWords) TEST(MADDY_EMPHASIZEDPARSER, ItAllowsDoubleUnderscores) { - // I'm not sure if this is standard or not, but this is how the github markdown - // parser behaves. Other things I've seen want it to *not* match. + // I'm not sure if this is standard or not, but this is how the github + // markdown parser behaves. Other things I've seen want it to *not* + // match. std::string text = "some __text testing it_ out"; std::string expected = "some _text testing it out"; auto emphasizedParser = std::make_shared(); @@ -80,8 +81,10 @@ TEST(MADDY_EMPHASIZEDPARSER, ItAllowsDoubleUnderscores) TEST(MADDY_EMPHASIZEDPARSER, ItDoesntReplaceUnderscoresInsideCodeBlocks) { - std::string text = "Stuff inside blocks _shouldn't be emphasized_ at all"; - std::string expected = "Stuff inside blocks _shouldn't be emphasized_ at all"; + std::string text = + "Stuff inside blocks _shouldn't be emphasized_ at all"; + std::string expected = + "Stuff inside blocks _shouldn't be emphasized_ at all"; auto emphasizedParser = std::make_shared(); emphasizedParser->Parse(text); @@ -92,7 +95,8 @@ TEST(MADDY_EMPHASIZEDPARSER, ItDoesntReplaceUnderscoresInsideCodeBlocks) TEST(MADDY_EMPHASIZEDPARSER, ItDoesNotReplaceUnderscoresInURLs) { std::string text = "[Link Title](http://example.com/what_you_didn't_know)"; - std::string expected = "[Link Title](http://example.com/what_you_didn't_know)"; + std::string expected = + "[Link Title](http://example.com/what_you_didn't_know)"; auto emphasizedParser = std::make_shared(); emphasizedParser->Parse(text); From 8e2c44c337719b082cb445157d00737522bd7dc0 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 16:36:31 -0700 Subject: [PATCH 05/11] A clang fix turned out to break stuff! --- include/maddy/strongparser.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h index 8a66d6e..a28d18b 100644 --- a/include/maddy/strongparser.h +++ b/include/maddy/strongparser.h @@ -58,10 +58,12 @@ class StrongParser : public LineParser // } // }; static std::vector res{ - std::regex{R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)" - "([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))"}, - std::regex{R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)" - "([^__]*)__(?!.*`.*|.*<\/code>.*))"} + std::regex{ + R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" + }, + std::regex{ + R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)([^__]*)__(?!.*`.*|.*<\/code>.*))" + } }; static std::string replacement = "$1"; for (const auto& re : res) From 30d6cf9d7ab3fd2e62acd1e18073f29dfe7008e4 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 16:38:07 -0700 Subject: [PATCH 06/11] Fixed double negative phrasing. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25072a3..251add8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ maddy uses [semver versioning](https://semver.org/). ## Upcoming -* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Do not create emphasis tags not at word boundaries, i.e. `only_internal_underscores`. +* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create emphasis tags at word boundaries, i.e. `not only_internal_underscores`. * ... ## version 1.5.0 2025-04-21 From bfb0ff565a60002abad3fc318657dad2f05cb163 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Fri, 21 Aug 2026 13:04:36 -0700 Subject: [PATCH 07/11] Fix for emphasis and strong with internal and extra underscores. Addresses comments from ages ago (sorry!) and also does the correct behavior with unbalanced underscores. Made with help from Claude, but checked and updated a lot. --- CHANGELOG.md | 3 +- include/maddy/emphasizedparser.h | 9 ++-- include/maddy/strongparser.h | 43 ++++++------------ tests/maddy/test_maddy_emphasizedparser.cpp | 40 +++++++++++++++-- tests/maddy/test_maddy_strongparser.cpp | 49 +++++++++++++++++---- 5 files changed, 97 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251add8..e2f1fe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ maddy uses [semver versioning](https://semver.org/). ## Upcoming -* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create emphasis tags at word boundaries, i.e. `not only_internal_underscores`. +* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create strong and emphasis tags at word boundaries, i.e. `not only_internal_underscores`. +* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create strong and emphasis tags at word boundaries for underscores, and correctly leave any leftover delimiters outside the tag on either side, however many there are, i.e. `___text__` becomes `_text` and `_text_______` becomes `text______`. * ... ## version 1.5.0 2025-04-21 diff --git a/include/maddy/emphasizedparser.h b/include/maddy/emphasizedparser.h index af30928..377e674 100644 --- a/include/maddy/emphasizedparser.h +++ b/include/maddy/emphasizedparser.h @@ -40,12 +40,13 @@ class EmphasizedParser : public LineParser */ void Parse(std::string& line) override { - // Modifed from previous version, with help from - // https://stackoverflow.com/questions/61346949/regex-for-markdown-emphasis + // The leading and trailing `(_*)` groups absorb any leftover underscores + // from an unbalanced run (e.g. `__foo_` or `_foo____`), re-emitted + // outside the tag instead of into its content. static std::regex re( - R"((?!.*`.*|.*.*)\b_(?![\s])(?!.*`.*|.*<\/code>.*)(.*?[^\s])_\b(?!.*`.*|.*<\/code>.*))" + R"((?!.*`.*|.*.*)\b(_*)_(?![\s_])(?!.*`.*|.*<\/code>.*)(.*?[^\s])_(_*)\b(?!.*`.*|.*<\/code>.*))" ); - static std::string replacement = "$1"; + static std::string replacement = "$1$2$3"; line = std::regex_replace(line, re, replacement); } diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h index a28d18b..b6c0de7 100644 --- a/include/maddy/strongparser.h +++ b/include/maddy/strongparser.h @@ -40,36 +40,21 @@ class StrongParser : public LineParser */ void Parse(std::string& line) override { - // This version of the regex is changed exactly the same way - // that the regex for the emphasized parser was changed, and - // it then passes all the 'disabled' tests in the 'strong parser' - // test, but then it fails general parsing. For some reason, - // "__text__" translates "text" even though there - // are no word boundaries at the correct places. It's weird! - - // static std::vector res{ - // std::regex{ - // R"((?!.*`.*|.*.*)\b\*\*(?![\s])(?!.*`.*|.*<\/code>.*)" - // "(.*?[^\s])\*\*\b(?!.*`.*|.*<\/code>.*))" - // }, - // std::regex{ - // R"((?!.*`.*|.*.*)\b__(?![\s])(?!.*`.*|.*<\/code>.*)" - // "(.*?[^\s])__\b(?!.*`.*|.*<\/code>.*))" - // } - // }; - static std::vector res{ - std::regex{ - R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" - }, - std::regex{ - R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)([^__]*)__(?!.*`.*|.*<\/code>.*))" - } + // `*` is not a word character, so `\b` next to it does not mean + // "edge of a delimiter run" the way it does for `_`; the asterisk + // variant is left without a word-boundary anchor. + static std::regex reAsterisk{ + R"((?!.*`.*|.*.*)\*\*(?![\s])(?!.*`.*|.*<\/code>.*)(.*?[^\s])\*\*(?!.*`.*|.*<\/code>.*))" + }; + // The leading and trailing `(_*)` groups absorb any leftover underscores + // from an unbalanced run on either side (e.g. `___text__` or + // `__text_______`), re-emitted outside the tag by the caller + // instead of being swallowed into its content. + static std::regex reUnderscore{ + R"((?!.*`.*|.*.*)\b(_*)__(?![\s_])(?!.*`.*|.*<\/code>.*)(.*?[^\s])__(_*)\b(?!.*`.*|.*<\/code>.*))" }; - static std::string replacement = "$1"; - for (const auto& re : res) - { - line = std::regex_replace(line, re, replacement); - } + line = std::regex_replace(line, reAsterisk, "$1"); + line = std::regex_replace(line, reUnderscore, "$1$2$3"); } }; // class StrongParser diff --git a/tests/maddy/test_maddy_emphasizedparser.cpp b/tests/maddy/test_maddy_emphasizedparser.cpp index a70c248..9e1cd52 100644 --- a/tests/maddy/test_maddy_emphasizedparser.cpp +++ b/tests/maddy/test_maddy_emphasizedparser.cpp @@ -67,11 +67,43 @@ TEST(MADDY_EMPHASIZEDPARSER, ItReplacesUnderscoresWithMultipleWords) TEST(MADDY_EMPHASIZEDPARSER, ItAllowsDoubleUnderscores) { - // I'm not sure if this is standard or not, but this is how the github - // markdown parser behaves. Other things I've seen want it to *not* - // match. + // Per CommonMark, a leftover delimiter from an unbalanced run renders + // outside the tag it didn't pair into, not inside it. std::string text = "some __text testing it_ out"; - std::string expected = "some _text testing it out"; + std::string expected = "some _text testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItAllowsTrailingDoubleUnderscores) +{ + std::string text = "some _text testing it__ out"; + std::string expected = "some text testing it_ out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItAllowsManyLeadingLeftoverUnderscores) +{ + std::string text = "some ____text testing it_ out"; + std::string expected = "some ___text testing it out"; + auto emphasizedParser = std::make_shared(); + + emphasizedParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_EMPHASIZEDPARSER, ItAllowsManyTrailingLeftoverUnderscores) +{ + std::string text = "some _text testing it____ out"; + std::string expected = "some text testing it___ out"; auto emphasizedParser = std::make_shared(); emphasizedParser->Parse(text); diff --git a/tests/maddy/test_maddy_strongparser.cpp b/tests/maddy/test_maddy_strongparser.cpp index 466068d..1211b0e 100644 --- a/tests/maddy/test_maddy_strongparser.cpp +++ b/tests/maddy/test_maddy_strongparser.cpp @@ -95,7 +95,7 @@ TEST(MADDY_STRONGPARSER, ItReplacesUnderscoresAtStringEdges) ASSERT_EQ(expected, text); } -TEST(DISABLED_MADDY_STRONGPARSER, ItDoesNotReplaceMarkdownWithInlineUnderscores) +TEST(MADDY_STRONGPARSER, ItDoesNotReplaceMarkdownWithInlineUnderscores) { std::string text = "some text__bla__text testing __it__ out"; std::string expected = "some text__bla__text testing it out"; @@ -106,7 +106,7 @@ TEST(DISABLED_MADDY_STRONGPARSER, ItDoesNotReplaceMarkdownWithInlineUnderscores) ASSERT_EQ(expected, text); } -TEST(DISABLED_MADDY_STRONGPARSER, ItOnlyReplacesUnderscoresAtWordBreaks) +TEST(MADDY_STRONGPARSER, ItOnlyReplacesUnderscoresAtWordBreaks) { std::string text = "some __text__bla__ testing __it__ out"; std::string expected = @@ -129,14 +129,45 @@ TEST(MADDY_STRONGPARSER, ItReplacesUnderscoresWithMultipleWords) ASSERT_EQ(expected, text); } -TEST(DISABLED_MADDY_STRONGPARSER, ItAllowsTripleUnderscores) +TEST(MADDY_STRONGPARSER, ItAllowsTripleUnderscores) { - // I'm not sure if this is standard or not, but this is how the github - // markdown parser behaves. Other things I've seen want it to *not* - // match. - + // Per CommonMark, a leftover delimiter from an unbalanced run renders + // outside the tag it didn't pair into, not inside it. std::string text = "some ___text testing it__ out"; - std::string expected = "some _text testing it out"; + std::string expected = "some _text testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItAllowsTrailingTripleUnderscores) +{ + std::string text = "some __text testing it___ out"; + std::string expected = "some text testing it_ out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItAllowsManyLeadingLeftoverUnderscores) +{ + std::string text = "some ________text testing it__ out"; + std::string expected = "some ______text testing it out"; + auto strongParser = std::make_shared(); + + strongParser->Parse(text); + + ASSERT_EQ(expected, text); +} + +TEST(MADDY_STRONGPARSER, ItAllowsManyTrailingLeftoverUnderscores) +{ + std::string text = "some __text testing it_______ out"; + std::string expected = "some text testing it_____ out"; auto strongParser = std::make_shared(); strongParser->Parse(text); @@ -157,7 +188,7 @@ TEST(MADDY_STRONGPARSER, ItDoesntReplaceUnderscoresInsideCodeBlocks) ASSERT_EQ(expected, text); } -TEST(DISABLED_MADDY_STRONGPARSER, ItDoesNotReplaceUnderscoresInURLs) +TEST(MADDY_STRONGPARSER, ItDoesNotReplaceUnderscoresInURLs) { std::string text = "[Link Title](http://example.com/what__you__didn't__know)"; std::string expected = From 57f04f89a7b5d77150bcad1cada831a182a5ed0a Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Fri, 21 Aug 2026 13:11:37 -0700 Subject: [PATCH 08/11] Update CMake version so that github can find VS 2026. (2022 was removed) --- .github/workflows/run-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b118bec..4a415b8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | @@ -30,13 +30,13 @@ jobs: - uses: actions/checkout@v4 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | mkdir tmp cd tmp - cmake -G "Visual Studio 17 2022" -A x64 -DMADDY_BUILD_WITH_TESTS=ON .. + cmake -G "Visual Studio 18 2026" -A x64 -DMADDY_BUILD_WITH_TESTS=ON .. cmake --build . --config Debug - name: run tests run: | @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v4 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | From 96c354438a66e1d1526ac40ebe73d77598c72691 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Fri, 21 Aug 2026 13:15:02 -0700 Subject: [PATCH 09/11] Fix changelog wording. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f1fe8..2614f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ maddy uses [semver versioning](https://semver.org/). ## Upcoming * ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create strong and emphasis tags at word boundaries, i.e. `not only_internal_underscores`. -* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Only create strong and emphasis tags at word boundaries for underscores, and correctly leave any leftover delimiters outside the tag on either side, however many there are, i.e. `___text__` becomes `_text` and `_text_______` becomes `text______`. +* ![**FIXED**](https://img.shields.io/badge/-FIXED-%23090) Correctly leave any leftover strong or emphasis delimiters outside the tag on either side, however many there are, i.e. `___text__` becomes `_text` and `_text_______` becomes `text______`. * ... ## version 1.5.0 2025-04-21 From 4651fc98ceb55bebc89cd4c79c34ef6b092e4ee3 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Fri, 21 Aug 2026 15:43:41 -0700 Subject: [PATCH 10/11] Split tables parsing into Maddy-style or CommonMark-style. Maddy's parsing of tables is a one-off, and works for both the body and the footer of tables. CommonMark doesn't support table footers at all. However, if you're using maddy as part of a system that needs to round-trip HTML through MD and back again, the table format that a CommonMark-style exporter creates won't be parsed by Maddy. So, this update splits table parsing into two forms: the form Maddy invented, and the form CommonMark/GFM expects. This can be set using a new MADDY_SPECIFIC_PARSER flag, and by default, it parses its own invented format. When turned off, the parser instead parses GFM-style tables. All of this code is just Claude, though all the design was me. --- .github/workflows/run-tests.yml | 14 +- CHANGELOG.md | 1 + docs/definitions.md | 47 ++++ include/maddy/parser.h | 11 +- include/maddy/parserconfig.h | 9 +- include/maddy/tableparser.h | 320 +++++++++++++++++++++---- tests/maddy/test_maddy_parser.cpp | 43 ++++ tests/maddy/test_maddy_tableparser.cpp | 106 ++++++++ 8 files changed, 496 insertions(+), 55 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 29cf5d6..144e73c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -12,8 +12,8 @@ jobs: - uses: actions/checkout@v6 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" - ninjaVersion: "^1.11.1" + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version + ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | mkdir tmp @@ -39,13 +39,13 @@ jobs: - uses: actions/checkout@v6 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" - ninjaVersion: "^1.11.1" + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version + ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | mkdir tmp cd tmp - cmake -G "Visual Studio 17 2022" -A x64 -DMADDY_BUILD_WITH_TESTS=ON .. + cmake -G "Visual Studio 18 2026" -A x64 -DMADDY_BUILD_WITH_TESTS=ON .. cmake --build . --config Debug - name: run tests run: | @@ -57,8 +57,8 @@ jobs: - uses: actions/checkout@v6 - uses: lukka/get-cmake@latest with: - cmakeVersion: "~3.25.0" - ninjaVersion: "^1.11.1" + cmakeVersion: "~4.2.0" # <--= optional, use most recent 4.2.x version + ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version - name: build run: | mkdir tmp diff --git a/CHANGELOG.md b/CHANGELOG.md index a16cc40..c6f08e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ maddy uses [semver versioning](https://semver.org/). ## Upcoming +* ![**ADDED**](https://img.shields.io/badge/-ADDED-%23099) New `maddy::types::MADDY_SPECIFIC_PARSER` config flag (on by default, keeping current behavior). Turning it off switches `TableParser` from maddy's own `|table>` sigil syntax to standard GitHub-flavored-Markdown pipe tables (which have no footer concept). * ... ## version 1.6.0 2025-07-26 diff --git a/docs/definitions.md b/docs/definitions.md index 1a3800e..81d0e79 100644 --- a/docs/definitions.md +++ b/docs/definitions.md @@ -385,6 +385,53 @@ becomes ``` table header and footer are optional +This is maddy's own historical table syntax, and it's used by default +(`maddy::types::MADDY_SPECIFIC_PARSER`). If you need standard +GitHub-flavored-Markdown pipe tables instead, disable it in config: + +```cpp +std::shared_ptr config = std::make_shared(); +config->enabledParsers &= ~maddy::types::MADDY_SPECIFIC_PARSER; + +std::shared_ptr parser = std::make_shared(config); +std::string htmlOutput = parser->Parse(markdownInput); +``` + +After this, tables look like: + +``` +| Left header | middle header | last header | +| --- | --- | --- | +| cell 1 | cell 2 | cell 3 | +| cell 4 | cell 5 | cell 6 | +``` +becomes +```html + + + + + + + + + + + + + + + + + + + + +
Left headermiddle headerlast header
cell 1cell 2cell 3
cell 4cell 5cell 6
+``` +GFM pipe tables have no footer concept, so this mode never produces a +``. + ## LaTeX(MathJax) block support To turn on the LaTeX support - which basically is only a diff --git a/include/maddy/parser.h b/include/maddy/parser.h index 660752f..bcdf560 100644 --- a/include/maddy/parser.h +++ b/include/maddy/parser.h @@ -278,10 +278,17 @@ class Parser } else if ((!this->config || (this->config->enabledParsers & maddy::types::TABLE_PARSER) != 0) && - maddy::TableParser::IsStartingLine(line)) + maddy::TableParser::IsStartingLine( + line, + !this->config || (this->config->enabledParsers & + maddy::types::MADDY_SPECIFIC_PARSER) != 0 + )) { parser = std::make_shared( - [this](std::string& line) { this->runLineParser(line); }, nullptr + [this](std::string& line) { this->runLineParser(line); }, + nullptr, + !this->config || (this->config->enabledParsers & + maddy::types::MADDY_SPECIFIC_PARSER) != 0 ); } else if ((!this->config || (this->config->enabledParsers & diff --git a/include/maddy/parserconfig.h b/include/maddy/parserconfig.h index f95ee04..74ab2c9 100644 --- a/include/maddy/parserconfig.h +++ b/include/maddy/parserconfig.h @@ -44,8 +44,13 @@ enum PARSER_TYPE : uint32_t UNORDERED_LIST_PARSER = 0b100000000000000000, LATEX_BLOCK_PARSER = 0b1000000000000000000, - DEFAULT = 0b0111111111110111111, - ALL = 0b1111111111111111111, + // Not a parser of its own: gates maddy's own historical markdown dialect + // wherever a parser supports both that and a more standard alternative + // (currently just TableParser's `|table>` syntax vs. GFM pipe tables). + MADDY_SPECIFIC_PARSER = 0b10000000000000000000, + + DEFAULT = 0b10111111111110111111, + ALL = 0b11111111111111111111, }; // clang-format on diff --git a/include/maddy/tableparser.h b/include/maddy/tableparser.h index 9f1051f..1e6ec84 100644 --- a/include/maddy/tableparser.h +++ b/include/maddy/tableparser.h @@ -7,10 +7,14 @@ // ----------------------------------------------------------------------------- #include +#include #include +#include #include +#include #include "maddy/blockparser.h" +#include "maddy/paragraphparser.h" // ----------------------------------------------------------------------------- @@ -21,7 +25,37 @@ namespace maddy { /** * TableParser * - * For more information, see the docs folder. + * Supports two independent syntaxes, chosen with `useMaddySpecificMarkdown` + * (see `maddy::types::MADDY_SPECIFIC_PARSER`). + * + * When true (the default, and maddy's original behavior), a table uses + * maddy's own sigils: + * + * ``` + * |table> + * Left header|middle header|last header + * - | - | - + * Cell A1|Cell B1|Cell C1 + * - | - | - + * Foot A|Foot B|Foot C + * |`. Since `IsStartingLine` only sees one line at a time in this + * mode, it can't yet tell a table header from an ordinary line that happens + * to contain a `|`; if the following line isn't a valid separator row, both + * lines are handed off to a ParagraphParser instead. * * @class */ @@ -35,32 +69,48 @@ class TableParser : public BlockParser * @param {std::function} parseLineCallback * @param {std::function(const std::string& * line)>} getBlockParserForLineCallback + * @param {bool} useMaddySpecificMarkdown */ TableParser( std::function parseLineCallback, std::function(const std::string& line)> - getBlockParserForLineCallback + getBlockParserForLineCallback, + bool useMaddySpecificMarkdown = true ) : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , useMaddySpecificMarkdown(useMaddySpecificMarkdown) , isStarted(false) , isFinished(false) , currentBlock(0) , currentRow(0) + , gfmState(GfmState::EXPECT_HEADER) {} /** * IsStartingLine * - * If the line has exact `|table>`, then it is starting the table. + * With maddy-specific markdown, a table starts with exact `|table>`. + * With GFM markdown, a table can only start with a row that has at least + * one `|` separating two cells; whether it really is a table is only + * known once the following line (the separator row) has been seen. * * @method * @param {const std::string&} line + * @param {bool} useMaddySpecificMarkdown * @return {bool} */ - static bool IsStartingLine(const std::string& line) + static bool IsStartingLine( + const std::string& line, + bool useMaddySpecificMarkdown = true + ) { - static std::string matchString("|table>"); - return line == matchString; + if (useMaddySpecificMarkdown) + { + static std::string matchString("|table>"); + return line == matchString; + } + + return IsTableRow(line); } /** @@ -74,52 +124,21 @@ class TableParser : public BlockParser */ void AddLine(std::string& line) override { - if (!this->isStarted && line == "|table>") + if (this->useMaddySpecificMarkdown) { - this->isStarted = true; - return; + this->AddLineMaddyStyle(line); } - - if (this->isStarted) + else { - if (line == "- | - | -") - { - ++this->currentBlock; - this->currentRow = 0; - return; - } - - if (line == "|parseBlock(emptyLine); - this->isFinished = true; - return; - } - - if (this->table.size() < this->currentBlock + 1) - { - this->table.push_back(std::vector>()); - } - this->table[this->currentBlock].push_back(std::vector()); - - std::string segment; - std::stringstream streamToSplit(line); - - while (std::getline(streamToSplit, segment, '|')) - { - this->parseLine(segment); - this->table[this->currentBlock][this->currentRow].push_back(segment); - } - - ++this->currentRow; + this->AddLineGfm(line); } } /** * IsFinished * - * A table ends with `|"; @@ -221,11 +242,222 @@ class TableParser : public BlockParser } private: + bool useMaddySpecificMarkdown; + + // --- maddy-specific-markdown mode state --- bool isStarted; bool isFinished; uint32_t currentBlock; uint32_t currentRow; std::vector>> table; + + void AddLineMaddyStyle(std::string& line) + { + if (!this->isStarted && line == "|table>") + { + this->isStarted = true; + return; + } + + if (this->isStarted) + { + if (line == "- | - | -") + { + ++this->currentBlock; + this->currentRow = 0; + return; + } + + if (line == "|parseBlock(emptyLine); + this->isFinished = true; + return; + } + + if (this->table.size() < this->currentBlock + 1) + { + this->table.push_back(std::vector>()); + } + this->table[this->currentBlock].push_back(std::vector()); + + std::string segment; + std::stringstream streamToSplit(line); + + while (std::getline(streamToSplit, segment, '|')) + { + this->parseLine(segment); + this->table[this->currentBlock][this->currentRow].push_back(segment); + } + + ++this->currentRow; + } + } + + // --- GFM-pipe-table mode state --- + enum class GfmState { EXPECT_HEADER, EXPECT_SEPARATOR, IN_BODY }; + + GfmState gfmState; + std::string headerLine; + std::shared_ptr fallbackParser; + + void AddLineGfm(std::string& line) + { + if (this->fallbackParser) + { + this->fallbackParser->AddLine(line); + + if (this->fallbackParser->IsFinished()) + { + this->result << this->fallbackParser->GetResult().str(); + this->isFinished = true; + } + + return; + } + + switch (this->gfmState) + { + case GfmState::EXPECT_HEADER: + this->headerLine = line; + this->gfmState = GfmState::EXPECT_SEPARATOR; + return; + + case GfmState::EXPECT_SEPARATOR: + if ( + IsSeparatorRow(line) && + SplitRow(line).size() == SplitRow(this->headerLine).size() + ) + { + this->WriteGfmHeader(); + this->gfmState = GfmState::IN_BODY; + } + else + { + this->FallBackToParagraph(line); + } + return; + + case GfmState::IN_BODY: + if (line.empty()) + { + this->result << "
"; + this->isFinished = true; + } + else + { + this->WriteGfmRow(line); + } + return; + } + } + + static bool IsTableRow(const std::string& line) + { + return line.find('|') != std::string::npos && + line.find_first_not_of(" \t") != std::string::npos; + } + + static bool IsSeparatorRow(const std::string& line) + { + if (!IsTableRow(line)) + { + return false; + } + + static const std::regex cellRe("^:?-+:?$"); + + for (const std::string& cell : SplitRow(line)) + { + if (!std::regex_match(cell, cellRe)) + { + return false; + } + } + + return true; + } + + static std::vector SplitRow(const std::string& line) + { + std::vector cells; + std::stringstream stream(line); + std::string cell; + + while (std::getline(stream, cell, '|')) + { + Trim(cell); + + if (!cell.empty()) + { + cells.push_back(cell); + } + } + + return cells; + } + + static void Trim(std::string& str) + { + size_t first = str.find_first_not_of(" \t"); + + if (first == std::string::npos) + { + str.clear(); + return; + } + + size_t last = str.find_last_not_of(" \t"); + str = str.substr(first, last - first + 1); + } + + void WriteGfmHeader() + { + this->result << ""; + + for (std::string cell : SplitRow(this->headerLine)) + { + this->parseLine(cell); + this->result << ""; + } + + this->result << ""; + } + + void WriteGfmRow(const std::string& line) + { + this->result << ""; + + for (std::string cell : SplitRow(line)) + { + this->parseLine(cell); + this->result << ""; + } + + this->result << ""; + } + + void FallBackToParagraph(const std::string& secondLine) + { + this->fallbackParser = std::make_shared( + [this](std::string& l) { this->parseLine(l); }, + nullptr, + true + ); + + std::string first = this->headerLine; + this->fallbackParser->AddLine(first); + + std::string second = secondLine; + this->fallbackParser->AddLine(second); + + if (this->fallbackParser->IsFinished()) + { + this->result << this->fallbackParser->GetResult().str(); + this->isFinished = true; + } + } }; // class TableParser // ----------------------------------------------------------------------------- diff --git a/tests/maddy/test_maddy_parser.cpp b/tests/maddy/test_maddy_parser.cpp index ed8530a..83a20cb 100644 --- a/tests/maddy/test_maddy_parser.cpp +++ b/tests/maddy/test_maddy_parser.cpp @@ -82,3 +82,46 @@ TEST(MADDY_PARSER, ItShouldNotParseInlineCodeInHeadlineIfDisabled) ASSERT_EQ(expectedHTML, output); } + +TEST(MADDY_PARSER, ItShouldParseGfmTablesWhenMaddySpecificParserIsDisabled) +{ + const std::string tableTest = + "| Left header | middle header | last header |\n" + "| --- | --- | --- |\n" + "| cell 1 | cell 2 | cell 3 |\n" + "| cell 4 | cell 5 | cell 6 |\n"; + const std::string expectedHTML = + "
" << cell << "
" << cell << "
Left headermiddle headerlast " + "header
cell 1cell 2cell " + "3
cell 4cell 5cell " + "6
"; + std::stringstream markdown(tableTest); + auto config = std::make_shared(); + config->enabledParsers &= ~maddy::types::MADDY_SPECIFIC_PARSER; + auto parser = std::make_shared(config); + + const std::string output = parser->Parse(markdown); + + ASSERT_EQ(expectedHTML, output); +} + +TEST( + MADDY_PARSER, + ItShouldNotParseMaddySpecificTableSyntaxWhenMaddySpecificParserIsDisabled +) +{ + const std::string tableTest = + "|table>\n" + "A|B\n" + "- | - | -\n" + "1|2\n" + "|(); + config->enabledParsers &= ~maddy::types::MADDY_SPECIFIC_PARSER; + auto parser = std::make_shared(config); + + const std::string output = parser->Parse(markdown); + + ASSERT_EQ(std::string::npos, output.find("")); +} diff --git a/tests/maddy/test_maddy_tableparser.cpp b/tests/maddy/test_maddy_tableparser.cpp index 5d50d92..4bfbe66 100644 --- a/tests/maddy/test_maddy_tableparser.cpp +++ b/tests/maddy/test_maddy_tableparser.cpp @@ -67,3 +67,109 @@ TEST_F(MADDY_TABLEPARSER, ItReplacesMarkdownWithAnHtmlTable) ASSERT_EQ(expected, outputString); } + +// ----------------------------------------------------------------------------- +// GFM pipe-table mode (useMaddySpecificMarkdown = false) +// ----------------------------------------------------------------------------- + +class MADDY_TABLEPARSER_GFM : public ::testing::Test +{ +protected: + std::shared_ptr tableParser; + + void SetUp() override + { + this->tableParser = + std::make_shared(nullptr, nullptr, false); + } +}; + +TEST_F( + MADDY_TABLEPARSER_GFM, + IsStartingLineReturnsTrueForAnyLineWithAPipe +) +{ + ASSERT_TRUE(maddy::TableParser::IsStartingLine("| a | b |", false)); +} + +TEST_F( + MADDY_TABLEPARSER_GFM, + IsStartingLineReturnsFalseForABlankLine +) +{ + ASSERT_FALSE(maddy::TableParser::IsStartingLine(" ", false)); +} + +TEST_F(MADDY_TABLEPARSER_GFM, IsFinishedReturnsFalseInTheBeginning) +{ + ASSERT_FALSE(tableParser->IsFinished()); +} + +TEST_F(MADDY_TABLEPARSER_GFM, ItReplacesMarkdownWithAnHtmlTableAndHasNoFooter) +{ + std::vector markdown = { + "| Left header | middle header | last header |", + "| --- | --- | --- |", + "| cell 1 | cell 2 | cell 3 |", + "| cell 4 | cell 5 | cell 6 |", + "" + }; + std::string expected = + "
Left headermiddle headerlast " + "header
cell 1cell 2cell " + "3
cell 4cell 5cell " + "6
"; + + for (std::string md : markdown) + { + tableParser->AddLine(md); + } + + ASSERT_TRUE(tableParser->IsFinished()); + ASSERT_EQ(expected, tableParser->GetResult().str()); +} + +TEST_F( + MADDY_TABLEPARSER_GFM, + ItFallsBackToAParagraphWhenTheSecondLineIsNotASeparatorRow +) +{ + std::string first = "not | a | table"; + std::string second = "just some more text"; + std::string third = ""; + std::string expected = "

not | a | table just some more text

"; + + tableParser->AddLine(first); + tableParser->AddLine(second); + + if (!tableParser->IsFinished()) + { + tableParser->AddLine(third); + } + + ASSERT_TRUE(tableParser->IsFinished()); + ASSERT_EQ(expected, tableParser->GetResult().str()); +} + +TEST_F( + MADDY_TABLEPARSER_GFM, + OldMaddySpecificSyntaxIsNotRecognizedAsATable +) +{ + std::string first = "|table>"; + std::string second = "Left header|middle header|last header"; + std::string third = ""; + + tableParser->AddLine(first); + tableParser->AddLine(second); + + if (!tableParser->IsFinished()) + { + tableParser->AddLine(third); + } + + ASSERT_TRUE(tableParser->IsFinished()); + ASSERT_EQ( + std::string::npos, tableParser->GetResult().str().find("") + ); +} From 5f4b5effec90926ff3af313f4944972c1b4c2fe7 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Fri, 21 Aug 2026 15:52:09 -0700 Subject: [PATCH 11/11] Format for clang. --- include/maddy/parser.h | 6 +++--- include/maddy/tableparser.h | 20 ++++++++++---------- tests/maddy/test_maddy_tableparser.cpp | 19 ++++--------------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/include/maddy/parser.h b/include/maddy/parser.h index bcdf560..a3c8a18 100644 --- a/include/maddy/parser.h +++ b/include/maddy/parser.h @@ -59,7 +59,7 @@ class Parser */ static const std::string& version() { - static const std::string v = "1.6.0"; // MADDY_VERSION_LINE_REPLACEMENT + static const std::string v = "1.5.0"; return v; } @@ -281,14 +281,14 @@ class Parser maddy::TableParser::IsStartingLine( line, !this->config || (this->config->enabledParsers & - maddy::types::MADDY_SPECIFIC_PARSER) != 0 + maddy::types::MADDY_SPECIFIC_PARSER) != 0 )) { parser = std::make_shared( [this](std::string& line) { this->runLineParser(line); }, nullptr, !this->config || (this->config->enabledParsers & - maddy::types::MADDY_SPECIFIC_PARSER) != 0 + maddy::types::MADDY_SPECIFIC_PARSER) != 0 ); } else if ((!this->config || (this->config->enabledParsers & diff --git a/include/maddy/tableparser.h b/include/maddy/tableparser.h index 1e6ec84..7f7e6bc 100644 --- a/include/maddy/tableparser.h +++ b/include/maddy/tableparser.h @@ -100,8 +100,7 @@ class TableParser : public BlockParser * @return {bool} */ static bool IsStartingLine( - const std::string& line, - bool useMaddySpecificMarkdown = true + const std::string& line, bool useMaddySpecificMarkdown = true ) { if (useMaddySpecificMarkdown) @@ -296,7 +295,12 @@ class TableParser : public BlockParser } // --- GFM-pipe-table mode state --- - enum class GfmState { EXPECT_HEADER, EXPECT_SEPARATOR, IN_BODY }; + enum class GfmState + { + EXPECT_HEADER, + EXPECT_SEPARATOR, + IN_BODY + }; GfmState gfmState; std::string headerLine; @@ -325,10 +329,8 @@ class TableParser : public BlockParser return; case GfmState::EXPECT_SEPARATOR: - if ( - IsSeparatorRow(line) && - SplitRow(line).size() == SplitRow(this->headerLine).size() - ) + if (IsSeparatorRow(line) && + SplitRow(line).size() == SplitRow(this->headerLine).size()) { this->WriteGfmHeader(); this->gfmState = GfmState::IN_BODY; @@ -441,9 +443,7 @@ class TableParser : public BlockParser void FallBackToParagraph(const std::string& secondLine) { this->fallbackParser = std::make_shared( - [this](std::string& l) { this->parseLine(l); }, - nullptr, - true + [this](std::string& l) { this->parseLine(l); }, nullptr, true ); std::string first = this->headerLine; diff --git a/tests/maddy/test_maddy_tableparser.cpp b/tests/maddy/test_maddy_tableparser.cpp index 4bfbe66..592fd7e 100644 --- a/tests/maddy/test_maddy_tableparser.cpp +++ b/tests/maddy/test_maddy_tableparser.cpp @@ -84,18 +84,12 @@ class MADDY_TABLEPARSER_GFM : public ::testing::Test } }; -TEST_F( - MADDY_TABLEPARSER_GFM, - IsStartingLineReturnsTrueForAnyLineWithAPipe -) +TEST_F(MADDY_TABLEPARSER_GFM, IsStartingLineReturnsTrueForAnyLineWithAPipe) { ASSERT_TRUE(maddy::TableParser::IsStartingLine("| a | b |", false)); } -TEST_F( - MADDY_TABLEPARSER_GFM, - IsStartingLineReturnsFalseForABlankLine -) +TEST_F(MADDY_TABLEPARSER_GFM, IsStartingLineReturnsFalseForABlankLine) { ASSERT_FALSE(maddy::TableParser::IsStartingLine(" ", false)); } @@ -151,10 +145,7 @@ TEST_F( ASSERT_EQ(expected, tableParser->GetResult().str()); } -TEST_F( - MADDY_TABLEPARSER_GFM, - OldMaddySpecificSyntaxIsNotRecognizedAsATable -) +TEST_F(MADDY_TABLEPARSER_GFM, OldMaddySpecificSyntaxIsNotRecognizedAsATable) { std::string first = "|table>"; std::string second = "Left header|middle header|last header"; @@ -169,7 +160,5 @@ TEST_F( } ASSERT_TRUE(tableParser->IsFinished()); - ASSERT_EQ( - std::string::npos, tableParser->GetResult().str().find("
") - ); + ASSERT_EQ(std::string::npos, tableParser->GetResult().str().find("
")); }