| Left header | +middle header | +last header | +
|---|---|---|
| cell 1 | +cell 2 | +cell 3 | +
| cell 4 | +cell 5 | +cell 6 | +
...` HTML span, or a code span delimited by a
+ * run of N backtick characters and a later run of exactly N backticks
+ * (per CommonMark, a run with no matching close is ordinary text, and
+ * scanning resumes right after it).
+ *
+ * @method
+ * @param {const std::string&} line
+ * @return {std::vector";
+ static const std::string closeCodeTag = "";
+
+ std::vector.*)_(?!.*`.*|.*<\/code>.*)([^_]*)_(?!.*`.*|.*<\/code>.*))"
+ // 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_])(.*?[^\s])_(_*)\b)");
+ static std::string replacement = "$1$2$3";
+
+ ApplyOutsideProtectedSpans(
+ line,
+ [](std::string& segment)
+ { segment = std::regex_replace(segment, re, replacement); }
);
- static std::string replacement = "$1";
-
- line = std::regex_replace(line, re, replacement);
}
}; // class EmphasizedParser
diff --git a/include/maddy/italicparser.h b/include/maddy/italicparser.h
index 3f7075d..1b21e03 100644
--- a/include/maddy/italicparser.h
+++ b/include/maddy/italicparser.h
@@ -9,6 +9,7 @@
#include
#include
+#include "maddy/codespanutils.h"
#include "maddy/lineparser.h"
// -----------------------------------------------------------------------------
@@ -38,11 +39,14 @@ class ItalicParser : public LineParser
*/
void Parse(std::string& line) override
{
- static std::regex re(
- R"((?!.*`.*|.*.*)\*(?!.*`.*|.*<\/code>.*)([^\*]*)\*(?!.*`.*|.*<\/code>.*))"
- );
+ static std::regex re(R"(\*([^\*]*)\*)");
static std::string replacement = "$1";
- line = std::regex_replace(line, re, replacement);
+
+ ApplyOutsideProtectedSpans(
+ line,
+ [](std::string& segment)
+ { segment = std::regex_replace(segment, re, replacement); }
+ );
}
}; // class ItalicParser
diff --git a/include/maddy/parser.h b/include/maddy/parser.h
index 660752f..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;
}
@@ -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/strikethroughparser.h b/include/maddy/strikethroughparser.h
index effa5d8..df18a9c 100644
--- a/include/maddy/strikethroughparser.h
+++ b/include/maddy/strikethroughparser.h
@@ -9,6 +9,7 @@
#include
#include
+#include "maddy/codespanutils.h"
#include "maddy/lineparser.h"
// -----------------------------------------------------------------------------
@@ -38,12 +39,14 @@ class StrikeThroughParser : public LineParser
*/
void Parse(std::string& line) override
{
- static std::regex re(
- R"((?!.*`.*|.*.*)\~\~(?!.*`.*|.*<\/code>.*)([^\~]*)\~\~(?!.*`.*|.*<\/code>.*))"
- );
+ static std::regex re(R"(\~\~([^\~]*)\~\~)");
static std::string replacement = "$1";
- line = std::regex_replace(line, re, replacement);
+ ApplyOutsideProtectedSpans(
+ line,
+ [](std::string& segment)
+ { segment = std::regex_replace(segment, re, replacement); }
+ );
}
}; // class StrikeThroughParser
diff --git a/include/maddy/strongparser.h b/include/maddy/strongparser.h
index 348f2d4..5e39223 100644
--- a/include/maddy/strongparser.h
+++ b/include/maddy/strongparser.h
@@ -9,6 +9,7 @@
#include
#include
+#include "maddy/codespanutils.h"
#include "maddy/lineparser.h"
// -----------------------------------------------------------------------------
@@ -40,19 +41,26 @@ class StrongParser : public LineParser
*/
void Parse(std::string& line) override
{
- 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])(.*?[^\s])\*\*)"};
+ // 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_])(.*?[^\s])__(_*)\b)"};
+
+ ApplyOutsideProtectedSpans(
+ line,
+ [](std::string& segment)
+ {
+ segment =
+ std::regex_replace(segment, reAsterisk, "$1");
+ segment =
+ std::regex_replace(segment, reUnderscore, "$1$2$3");
}
- };
- static std::string replacement = "$1";
- for (const auto& re : res)
- {
- line = std::regex_replace(line, re, replacement);
- }
+ );
}
}; // class StrongParser
diff --git a/include/maddy/tableparser.h b/include/maddy/tableparser.h
index 9f1051f..7f7e6bc 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,47 @@ 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 +123,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 +241,223 @@ 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 << "" << cell << " ";
+ }
+
+ this->result << " ";
+ }
+
+ void WriteGfmRow(const std::string& line)
+ {
+ this->result << "";
+
+ for (std::string cell : SplitRow(line))
+ {
+ this->parseLine(cell);
+ this->result << "" << cell << " ";
+ }
+
+ 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/CMakeLists.txt b/tests/CMakeLists.txt
index ce5d729..01b0843 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -27,7 +27,7 @@ FetchContent_MakeAvailable(googletest)
# ------------------------------------------------------------------------------
-file(GLOB_RECURSE MADDY_TESTS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/maddy/*.cpp)
+file(GLOB_RECURSE MADDY_TESTS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/maddy/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/maddy/*.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/*.h)
# ------------------------------------------------------------------------------
diff --git a/tests/maddy/test_maddy_emphasizedparser.cpp b/tests/maddy/test_maddy_emphasizedparser.cpp
index 6442779..e4dce08 100644
--- a/tests/maddy/test_maddy_emphasizedparser.cpp
+++ b/tests/maddy/test_maddy_emphasizedparser.cpp
@@ -21,6 +21,121 @@ 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)
+{
+ // 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";
+ 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);
+
+ 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 +147,68 @@ 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);
+}
+
+// The following cases are adapted from the CommonMark spec
+// (https://spec.commonmark.org/), which defines how a code span's
+// backtick delimiters are matched and how it interacts with surrounding
+// markup.
+
+TEST(MADDY_EMPHASIZEDPARSER, ItMatchesBacktickRunsByEqualLength)
+{
+ // CommonMark spec example 349: "`foo``bar``" -> "`foobar".
+ // The lone opening backtick has no closing run of length 1 (the next
+ // runs are length 2), so it is ordinary text; the two length-2 runs
+ // pair up into the code span. Emphasized text on either side of this
+ // is still parsed normally.
+ std::string text = "_pre_ `foo``bar`` _post_";
+ std::string expected = "pre `foo``bar`` post";
+ auto emphasizedParser = std::make_shared();
+
+ emphasizedParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
+
+TEST(MADDY_EMPHASIZEDPARSER, ItLetsALongerBacktickFenceProtectAnInnerBacktick)
+{
+ // CommonMark spec example 329: "`` foo ` bar ``" -> "foo ` bar".
+ // A double-backtick fence spans across a single backtick in its
+ // content; text after the fence is still parsed normally.
+ std::string text = "`` foo ` bar `` and _emph_";
+ std::string expected = "`` foo ` bar `` and emph";
+ auto emphasizedParser = std::make_shared();
+
+ emphasizedParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
diff --git a/tests/maddy/test_maddy_italicparser.cpp b/tests/maddy/test_maddy_italicparser.cpp
index 03c5aac..785757b 100644
--- a/tests/maddy/test_maddy_italicparser.cpp
+++ b/tests/maddy/test_maddy_italicparser.cpp
@@ -20,3 +20,38 @@ TEST(MADDY_ITALICPARSER, ItReplacesMarkdownWithItalicHTML)
ASSERT_EQ(text, expected);
}
+
+// The following cases are adapted from the CommonMark spec
+// (https://spec.commonmark.org/), which defines how a code span's
+// backtick delimiters are matched and how it interacts with surrounding
+// markup.
+
+TEST(MADDY_ITALICPARSER, ItMatchesBacktickRunsByEqualLength)
+{
+ // CommonMark spec example 349: "`foo``bar``" -> "`foobar".
+ // The lone opening backtick has no closing run of length 1 (the next
+ // runs are length 2), so it is ordinary text; the two length-2 runs
+ // pair up into the code span. Italic text on either side of this is
+ // still parsed normally.
+ std::string text = "*pre* `foo``bar`` *post*";
+ std::string expected = "pre `foo``bar`` post";
+ auto italicParser = std::make_shared();
+
+ italicParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
+
+TEST(MADDY_ITALICPARSER, ItLetsALongerBacktickFenceProtectAnInnerBacktick)
+{
+ // CommonMark spec example 329: "`` foo ` bar ``" -> "foo ` bar".
+ // A double-backtick fence spans across a single backtick in its
+ // content; text after the fence is still parsed normally.
+ std::string text = "`` foo ` bar `` and *italic*";
+ std::string expected = "`` foo ` bar `` and italic";
+ auto italicParser = std::make_shared();
+
+ italicParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
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 =
+ "Left header middle header last "
+ "header cell 1 cell 2 cell "
+ "3 cell 4 cell 5 cell "
+ "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_strikethroughparser.cpp b/tests/maddy/test_maddy_strikethroughparser.cpp
index a31108c..57c7736 100644
--- a/tests/maddy/test_maddy_strikethroughparser.cpp
+++ b/tests/maddy/test_maddy_strikethroughparser.cpp
@@ -33,3 +33,40 @@ TEST(MADDY_STRIKETHROUGHPARSER, ItDoesNotParseInsideInlineCode)
ASSERT_EQ(expected, text);
}
+
+// The following cases are adapted from the CommonMark spec
+// (https://spec.commonmark.org/), which defines how a code span's
+// backtick delimiters are matched and how it interacts with surrounding
+// markup.
+
+TEST(MADDY_STRIKETHROUGHPARSER, ItMatchesBacktickRunsByEqualLength)
+{
+ // CommonMark spec example 349: "`foo``bar``" -> "`foobar".
+ // The lone opening backtick has no closing run of length 1 (the next
+ // runs are length 2), so it is ordinary text; the two length-2 runs
+ // pair up into the code span. Struck-through text on either side of
+ // this is still parsed normally.
+ std::string text = "~~pre~~ `foo``bar`` ~~post~~";
+ std::string expected = "pre `foo``bar`` post";
+ auto strikeThroughParser = std::make_shared();
+
+ strikeThroughParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
+
+TEST(
+ MADDY_STRIKETHROUGHPARSER, ItLetsALongerBacktickFenceProtectAnInnerBacktick
+)
+{
+ // CommonMark spec example 329: "`` foo ` bar ``" -> "foo ` bar".
+ // A double-backtick fence spans across a single backtick in its
+ // content; text after the fence is still parsed normally.
+ std::string text = "`` foo ` bar `` and ~~struck~~";
+ std::string expected = "`` foo ` bar `` and struck";
+ auto strikeThroughParser = std::make_shared();
+
+ strikeThroughParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
diff --git a/tests/maddy/test_maddy_strongparser.cpp b/tests/maddy/test_maddy_strongparser.cpp
index f006e26..48675f8 100644
--- a/tests/maddy/test_maddy_strongparser.cpp
+++ b/tests/maddy/test_maddy_strongparser.cpp
@@ -68,8 +68,10 @@ TEST(MADDY_STRONGPARSER, ItDoesNotParseInsideInlineCode)
std::vector tests{
{
+ // Per CommonMark, a code span protects only its own extent: text
+ // before it (here, "**bla**") is still eligible for parsing.
"some text **bla** `/**text**/` testing `**it**` out",
- "some text **bla** `/**text**/` testing `**it**` out",
+ "some text bla `/**text**/` testing `**it**` out",
},
{"some text _bla_ text testing __it__ out",
"some text _bla_ text testing it out"},
@@ -83,3 +85,185 @@ TEST(MADDY_STRONGPARSER, ItDoesNotParseInsideInlineCode)
ASSERT_EQ(test.expected, test.text);
}
}
+
+// The following cases are adapted from the CommonMark spec
+// (https://spec.commonmark.org/), which defines how a code span's
+// backtick delimiters are matched and how it interacts with surrounding
+// markup.
+
+TEST(MADDY_STRONGPARSER, ItMatchesBacktickRunsByEqualLength)
+{
+ // CommonMark spec example 349: "`foo``bar``" -> "`foobar".
+ // The lone opening backtick has no closing run of length 1 (the next
+ // runs are length 2), so it is ordinary text; the two length-2 runs
+ // pair up into the code span. Bold text on either side of this is
+ // still parsed normally.
+ std::string text = "**pre** `foo``bar`` **post**";
+ std::string expected =
+ "pre `foo``bar`` post";
+ auto strongParser = std::make_shared();
+
+ strongParser->Parse(text);
+
+ ASSERT_EQ(expected, text);
+}
+
+TEST(MADDY_STRONGPARSER, ItLetsALongerBacktickFenceProtectAnInnerBacktick)
+{
+ // CommonMark spec example 329: "`` foo ` bar ``" -> "foo ` bar".
+ // A double-backtick fence spans across a single backtick in its
+ // content; text after the fence is still parsed normally.
+ std::string text = "`` foo ` bar `` and **bold**";
+ std::string expected = "`` foo ` bar `` and bold";
+ auto strongParser = std::make_shared();
+
+ strongParser->Parse(text);
+
+ ASSERT_EQ(expected, 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(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(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(MADDY_STRONGPARSER, ItAllowsTripleUnderscores)
+{
+ // 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";
+ 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);
+
+ 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(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);
+}
diff --git a/tests/maddy/test_maddy_tableparser.cpp b/tests/maddy/test_maddy_tableparser.cpp
index 5d50d92..592fd7e 100644
--- a/tests/maddy/test_maddy_tableparser.cpp
+++ b/tests/maddy/test_maddy_tableparser.cpp
@@ -67,3 +67,98 @@ 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 header middle header last "
+ "header cell 1 cell 2 cell "
+ "3 cell 4 cell 5 cell "
+ "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(""));
+}