From de331501d76689522ad5b40f8ef38e3fb3272043 Mon Sep 17 00:00:00 2001 From: Bram Nijenkamp Date: Tue, 31 Mar 2026 12:00:58 +0200 Subject: [PATCH 1/2] feat(parser, gen): added support for GFM tables. --- include/maddy/gfmtableparser.h | 220 +++++++++++++++++++++++++++++++++ include/maddy/parser.h | 9 ++ include/maddy/parserconfig.h | 5 +- 3 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 include/maddy/gfmtableparser.h diff --git a/include/maddy/gfmtableparser.h b/include/maddy/gfmtableparser.h new file mode 100644 index 0000000..689f0d5 --- /dev/null +++ b/include/maddy/gfmtableparser.h @@ -0,0 +1,220 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * GfmTableParser + * + * Parses GitHub Flavored Markdown (GFM) tables. + * + * A GFM table starts with a line like `| Header | Header |`, + * followed by a separator line like `| --- | --- |`, + * then data rows like `| Cell | Cell |`. + * + * @class + */ +class GfmTableParser : public BlockParser +{ +public: + GfmTableParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isFinished_(false) + , lineIndex_(0) + {} + + /** + * IsStartingLine + * + * A GFM table starts with a line that begins and ends with `|` and + * contains at least one inner `|` separator. + */ + static bool IsStartingLine(const std::string& line) + { + // Must start with optional whitespace then `|` + auto trimmed = trimWhitespace(line); + if (trimmed.size() < 5) return false; // minimum: `| a |` + if (trimmed.front() != '|') return false; + if (trimmed.back() != '|') return false; + // Must have at least one inner `|` (i.e. at least 2 cells) + // or just one cell like `| foo |` is also valid + return true; + } + + void AddLine(std::string& line) override + { + auto trimmed = trimWhitespace(line); + + // If we already started and the line doesn't look like a table row, finish. + if (lineIndex_ > 0 && (trimmed.empty() || trimmed.front() != '|')) + { + finish(); + return; + } + + if (lineIndex_ == 0) + { + // Header row + headerCells_ = splitRow(trimmed); + ++lineIndex_; + return; + } + + if (lineIndex_ == 1) + { + // Separator row — parse alignment + auto separators = splitRow(trimmed); + for (const auto& sep : separators) + { + auto s = trimWhitespace(sep); + bool leftColon = !s.empty() && s.front() == ':'; + bool rightColon = !s.empty() && s.back() == ':'; + + if (leftColon && rightColon) + alignments_.push_back("center"); + else if (rightColon) + alignments_.push_back("right"); + else + alignments_.push_back("left"); + } + ++lineIndex_; + return; + } + + // Data row + bodyRows_.push_back(splitRow(trimmed)); + ++lineIndex_; + } + + bool IsFinished() const override { return this->isFinished_; } + +protected: + bool isInlineBlockAllowed() const override { return false; } + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string&) override + { + result << ""; + + // thead + result << ""; + for (size_t i = 0; i < headerCells_.size(); ++i) + { + std::string cell = headerCells_[i]; + this->parseLine(cell); + result << "" << cell << ""; + } + result << ""; + + // tbody + if (!bodyRows_.empty()) + { + result << ""; + for (const auto& row : bodyRows_) + { + result << ""; + for (size_t i = 0; i < row.size(); ++i) + { + std::string cell = row[i]; + this->parseLine(cell); + result << "" << cell << ""; + } + // Pad missing cells + for (size_t i = row.size(); i < headerCells_.size(); ++i) + { + result << ""; + } + result << ""; + } + result << ""; + } + + result << "
"; + } + +private: + bool isFinished_; + uint32_t lineIndex_; + std::vector headerCells_; + std::vector alignments_; + std::vector> bodyRows_; + + void finish() + { + std::string emptyLine = ""; + this->parseBlock(emptyLine); + this->isFinished_ = true; + } + + static std::string trimWhitespace(const std::string& str) + { + auto start = str.find_first_not_of(" \t"); + if (start == std::string::npos) return ""; + auto end = str.find_last_not_of(" \t"); + return str.substr(start, end - start + 1); + } + + static std::vector splitRow(const std::string& row) + { + std::vector cells; + // Strip leading and trailing `|` + std::string inner = row; + if (!inner.empty() && inner.front() == '|') inner.erase(0, 1); + if (!inner.empty() && inner.back() == '|') inner.pop_back(); + + std::string cell; + for (size_t i = 0; i < inner.size(); ++i) + { + if (inner[i] == '\\' && i + 1 < inner.size() && inner[i + 1] == '|') + { + cell += '|'; + ++i; + } + else if (inner[i] == '|') + { + cells.push_back(trimWhitespace(cell)); + cell.clear(); + } + else + { + cell += inner[i]; + } + } + cells.push_back(trimWhitespace(cell)); + return cells; + } +}; // class GfmTableParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/include/maddy/parser.h b/include/maddy/parser.h index 660752f..589d565 100644 --- a/include/maddy/parser.h +++ b/include/maddy/parser.h @@ -22,6 +22,7 @@ #include "maddy/orderedlistparser.h" #include "maddy/paragraphparser.h" #include "maddy/quoteparser.h" +#include "maddy/gfmtableparser.h" #include "maddy/tableparser.h" #include "maddy/unorderedlistparser.h" @@ -276,6 +277,14 @@ class Parser { return this->getBlockParserForLine(line); } ); } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::GFM_TABLE_PARSER) != 0) && + maddy::GfmTableParser::IsStartingLine(line)) + { + parser = std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, nullptr + ); + } else if ((!this->config || (this->config->enabledParsers & maddy::types::TABLE_PARSER) != 0) && maddy::TableParser::IsStartingLine(line)) diff --git a/include/maddy/parserconfig.h b/include/maddy/parserconfig.h index f95ee04..75587c5 100644 --- a/include/maddy/parserconfig.h +++ b/include/maddy/parserconfig.h @@ -43,9 +43,10 @@ enum PARSER_TYPE : uint32_t TABLE_PARSER = 0b10000000000000000, UNORDERED_LIST_PARSER = 0b100000000000000000, LATEX_BLOCK_PARSER = 0b1000000000000000000, + GFM_TABLE_PARSER = 0b10000000000000000000, - DEFAULT = 0b0111111111110111111, - ALL = 0b1111111111111111111, + DEFAULT = 0b10111111111110111111, + ALL = 0b11111111111111111111, }; // clang-format on From 45a96c05ed8448bb819f512cbbfbf91652f7996d Mon Sep 17 00:00:00 2001 From: Bram Nijenkamp Date: Fri, 17 Apr 2026 11:21:26 +0200 Subject: [PATCH 2/2] interupt headings --- include/maddy/parser.h | 12 ++++++++++++ tests/maddy/test_maddy_parser.cpp | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/include/maddy/parser.h b/include/maddy/parser.h index 589d565..595a4b4 100644 --- a/include/maddy/parser.h +++ b/include/maddy/parser.h @@ -136,6 +136,18 @@ class Parser for (std::string line; std::getline(markdown, line);) { + // ATX headings interrupt paragraphs (CommonMark §4.1) + if (currentBlockParser && + dynamic_cast(currentBlockParser.get()) && + (!this->config || (this->config->enabledParsers & maddy::types::HEADLINE_PARSER) != 0) && + maddy::HeadlineParser::IsStartingLine(line)) + { + std::string emptyLine = ""; + currentBlockParser->AddLine(emptyLine); + result += currentBlockParser->GetResult().str(); + currentBlockParser = nullptr; + } + if (!currentBlockParser) { currentBlockParser = getBlockParserForLine(line); diff --git a/tests/maddy/test_maddy_parser.cpp b/tests/maddy/test_maddy_parser.cpp index ed8530a..2a6b942 100644 --- a/tests/maddy/test_maddy_parser.cpp +++ b/tests/maddy/test_maddy_parser.cpp @@ -67,6 +67,18 @@ TEST(MADDY_PARSER, ItShouldParseInlineCodeInHeadlines) ASSERT_EQ(expectedHTML, output); } +TEST(MADDY_PARSER, ItShouldAllowHeadingToInterruptParagraph) +{ + const std::string md = "Some text\n# m h dom mon dow oBusinessProcess\n## Usage\n"; + const std::string expected = + "

Some text

" + "

m h dom mon dow oBusinessProcess

" + "

Usage

"; + std::stringstream markdown(md); + auto parser = std::make_shared(); + ASSERT_EQ(expected, parser->Parse(markdown)); +} + TEST(MADDY_PARSER, ItShouldNotParseInlineCodeInHeadlineIfDisabled) { const std::string headlineTest = R"(