From 9292d3c267375dd464ae981bdeb2227b07134ae6 Mon Sep 17 00:00:00 2001 From: Lucian Smith Date: Wed, 30 Apr 2025 14:56:25 -0700 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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