Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions include/maddy/gfmtableparser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* This project is licensed under the MIT license. For more information see the
* LICENSE file.
*/
#pragma once

// -----------------------------------------------------------------------------

#include <functional>
#include <regex>
#include <string>
#include <vector>

#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<void(std::string&)> parseLineCallback,
std::function<std::shared_ptr<BlockParser>(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 << "<table>";

// thead
result << "<thead><tr>";
for (size_t i = 0; i < headerCells_.size(); ++i)
{
std::string cell = headerCells_[i];
this->parseLine(cell);
result << "<th";
if (i < alignments_.size() && alignments_[i] != "left")
{
result << " align=\"" << alignments_[i] << "\"";
}
result << ">" << cell << "</th>";
}
result << "</tr></thead>";

// tbody
if (!bodyRows_.empty())
{
result << "<tbody>";
for (const auto& row : bodyRows_)
{
result << "<tr>";
for (size_t i = 0; i < row.size(); ++i)
{
std::string cell = row[i];
this->parseLine(cell);
result << "<td";
if (i < alignments_.size() && alignments_[i] != "left")
{
result << " align=\"" << alignments_[i] << "\"";
}
result << ">" << cell << "</td>";
}
// Pad missing cells
for (size_t i = row.size(); i < headerCells_.size(); ++i)
{
result << "<td></td>";
}
result << "</tr>";
}
result << "</tbody>";
}

result << "</table>";
}

private:
bool isFinished_;
uint32_t lineIndex_;
std::vector<std::string> headerCells_;
std::vector<std::string> alignments_;
std::vector<std::vector<std::string>> 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<std::string> splitRow(const std::string& row)
{
std::vector<std::string> 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
21 changes: 21 additions & 0 deletions include/maddy/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -135,6 +136,18 @@ class Parser

for (std::string line; std::getline(markdown, line);)
{
// ATX headings interrupt paragraphs (CommonMark §4.1)
if (currentBlockParser &&
dynamic_cast<maddy::ParagraphParser*>(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);
Expand Down Expand Up @@ -276,6 +289,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<maddy::GfmTableParser>(
[this](std::string& line) { this->runLineParser(line); }, nullptr
);
}
else if ((!this->config || (this->config->enabledParsers &
maddy::types::TABLE_PARSER) != 0) &&
maddy::TableParser::IsStartingLine(line))
Expand Down
5 changes: 3 additions & 2 deletions include/maddy/parserconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions tests/maddy/test_maddy_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
"<p>Some text </p>"
"<h1>m h dom mon dow oBusinessProcess</h1>"
"<h2>Usage</h2>";
std::stringstream markdown(md);
auto parser = std::make_shared<maddy::Parser>();
ASSERT_EQ(expected, parser->Parse(markdown));
}

TEST(MADDY_PARSER, ItShouldNotParseInlineCodeInHeadlineIfDisabled)
{
const std::string headlineTest = R"(
Expand Down
Loading