diff --git a/Info.plist b/Info.plist old mode 100755 new mode 100644 diff --git a/WorkPad.pro b/WorkPad.pro old mode 100755 new mode 100644 index 337ead2..69393c6 --- a/WorkPad.pro +++ b/WorkPad.pro @@ -1,13 +1,13 @@ QT += core gui -greaterThan(QT_MAJOR_VERSION, 5): QT += widgets +greaterThan(QT_MAJOR_VERSION, 5): QT += widgets webenginewidgets CONFIG += c++17 -win32:VERSION = 2.1.0.0 # major.minor.patch.build -else:VERSION = 2.1.0 # major.minor.patch +win32:VERSION = 3.0.0.0 # major.minor.patch.build +else:VERSION = 3.0.0 # major.minor.patch -DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\" +DEFINES += APP_VERSION=\"\\\"$${VERSION}-rc1\\\"\" DEFINES += APP_NAME=\"\\\"WorkPad\\\"\" # remove possible other optimization flags @@ -83,6 +83,8 @@ SOURCES += \ src/services/savemanager.cpp HEADERS += \ + maddy/parserconfig.h \ + maddy/parser.h \ src/frames/configdialog.h \ src/services/configmanager.h \ src/frames/exportdialog.h \ diff --git a/icon.icns b/icon.icns old mode 100755 new mode 100644 diff --git a/icon.ico b/icon.ico old mode 100755 new mode 100644 diff --git a/icons.qrc b/icons.qrc old mode 100755 new mode 100644 diff --git a/maddy/blockparser.h b/maddy/blockparser.h new file mode 100644 index 0000000..f93dd2c --- /dev/null +++ b/maddy/blockparser.h @@ -0,0 +1,192 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include +// windows compatibility includes +#include +#include + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * BlockParser + * + * The code expects every child to have the following static function to be + * implemented: + * `static bool IsStartingLine(const std::string& line)` + * + * @class + */ +class BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + BlockParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : result("", std::ios_base::ate | std::ios_base::in | std::ios_base::out) + , childParser(nullptr) + , parseLineCallback(parseLineCallback) + , getBlockParserForLineCallback(getBlockParserForLineCallback) + {} + + /** + * dtor + * + * @method + */ + virtual ~BlockParser() {} + + /** + * AddLine + * + * Adding a line which has to be parsed. + * + * @method + * @param {std::string&} line + * @return {void} + */ + virtual void AddLine(std::string& line) + { + this->parseBlock(line); + + if (this->isInlineBlockAllowed() && !this->childParser) + { + this->childParser = this->getBlockParserForLine(line); + } + + if (this->childParser) + { + this->childParser->AddLine(line); + + if (this->childParser->IsFinished()) + { + this->result << this->childParser->GetResult().str(); + this->childParser = nullptr; + } + + return; + } + + if (this->isLineParserAllowed()) + { + this->parseLine(line); + } + + this->result << line; + } + + /** + * IsFinished + * + * Check if the BlockParser is done + * + * @method + * @return {bool} + */ + virtual bool IsFinished() const = 0; + + /** + * GetResult + * + * Get the parsed HTML output. + * + * @method + * @return {std::stringstream} + */ + std::stringstream& GetResult() { return this->result; } + + /** + * Clear + * + * Clear the result to reuse the parser object. + * + * It is only used by one test for now. + * + * @method + * @return {void} + */ + void Clear() { this->result.str(""); } + +protected: + std::stringstream result; + std::shared_ptr childParser; + + virtual bool isInlineBlockAllowed() const = 0; + virtual bool isLineParserAllowed() const = 0; + virtual void parseBlock(std::string& line) = 0; + + void parseLine(std::string& line) + { + if (parseLineCallback) + { + parseLineCallback(line); + } + } + + uint32_t getIndentationWidth(const std::string& line) const + { + bool hasMetNonSpace = false; + + uint32_t indentation = static_cast(std::count_if( + line.begin(), + line.end(), + [&hasMetNonSpace](unsigned char c) + { + if (hasMetNonSpace) + { + return false; + } + + if (std::isspace(c)) + { + return true; + } + + hasMetNonSpace = true; + return false; + } + )); + + return indentation; + } + + std::shared_ptr getBlockParserForLine(const std::string& line) + { + if (getBlockParserForLineCallback) + { + return getBlockParserForLineCallback(line); + } + + return nullptr; + } + +private: + std::function parseLineCallback; + std::function(const std::string& line)> + getBlockParserForLineCallback; +}; // class BlockParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/breaklineparser.h b/maddy/breaklineparser.h new file mode 100644 index 0000000..e60dda2 --- /dev/null +++ b/maddy/breaklineparser.h @@ -0,0 +1,50 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include + +#include "maddy/lineparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * BreakLineParser + * + * @class + */ +class BreakLineParser : public LineParser +{ +public: + /** + * Parse + * + * From Markdown: `text\r\n text` + * + * To HTML: `text
text` + * + * @method + * @param {std::string&} line The line to interpret + * @return {void} + */ + void Parse(std::string& line) override + { + static std::regex re(R"((\r\n|\r))"); + static std::string replacement = "
"; + + line = std::regex_replace(line, re, replacement); + } +}; // class BreakLineParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/checklistparser.h b/maddy/checklistparser.h new file mode 100644 index 0000000..36a154b --- /dev/null +++ b/maddy/checklistparser.h @@ -0,0 +1,127 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * ChecklistParser + * + * @class + */ +class ChecklistParser : public BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + ChecklistParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isStarted(false) + , isFinished(false) + {} + + /** + * IsStartingLine + * + * An unordered list starts with `* `. + * + * @method + * @param {const std::string&} line + * @return {bool} + */ + static bool IsStartingLine(const std::string& line) + { + static std::regex re(R"(^- \[[x| ]\] .*)"); + return std::regex_match(line, re); + } + + /** + * IsFinished + * + * @method + * @return {bool} + */ + bool IsFinished() const override { return this->isFinished; } + +protected: + bool isInlineBlockAllowed() const override { return true; } + + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string& line) override + { + bool isStartOfNewListItem = IsStartingLine(line); + uint32_t indentation = getIndentationWidth(line); + + static std::regex lineRegex("^(- )"); + line = std::regex_replace(line, lineRegex, ""); + + static std::regex emptyBoxRegex(R"(^\[ \])"); + static std::string emptyBoxReplacement = ""; + line = std::regex_replace(line, emptyBoxRegex, emptyBoxReplacement); + + static std::regex boxRegex(R"(^\[x\])"); + static std::string boxReplacement = + ""; + line = std::regex_replace(line, boxRegex, boxReplacement); + + if (!this->isStarted) + { + line = "
") != std::string::npos) + { + line = "" + line; + this->isFinished = true; + return; + } + + if (isStartOfNewListItem) + { + line = "
  • ") != std::string::npos) + { + line = "" + line; + this->isFinished = true; + return; + } + + if (isStartOfNewListItem) + { + line = "
  • " + line; + } + } + +private: + bool isStarted; + bool isFinished; + + bool isStartOfNewListItem(const std::string& line) const + { + static std::regex re(R"(^(?:[1-9]+[0-9]*\. |\* ).*)"); + return std::regex_match(line, re); + } +}; // class OrderedListParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/paragraphparser.h b/maddy/paragraphparser.h new file mode 100644 index 0000000..4c477b8 --- /dev/null +++ b/maddy/paragraphparser.h @@ -0,0 +1,115 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * ParagraphParser + * + * @class + */ +class ParagraphParser : public BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + ParagraphParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback, + bool isEnabled + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isStarted(false) + , isFinished(false) + , isEnabled(isEnabled) + {} + + /** + * IsStartingLine + * + * If the line is not empty, it will be a paragraph. + * + * This block parser has to always run as the last one! + * + * @method + * @param {const std::string&} line + * @return {bool} + */ + static bool IsStartingLine(const std::string& line) { return !line.empty(); } + + /** + * IsFinished + * + * An empty line will end the paragraph. + * + * @method + * @return {bool} + */ + bool IsFinished() const override { return this->isFinished; } + +protected: + bool isInlineBlockAllowed() const override { return false; } + + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string& line) override + { + if (this->isEnabled && !this->isStarted) + { + line = "

    " + line + " "; + this->isStarted = true; + return; + } + else if (!this->isEnabled && !this->isStarted) + { + line += " "; + this->isStarted = true; + return; + } + + if (this->isEnabled && line.empty()) + { + line += "

    "; + this->isFinished = true; + return; + } + else if (!this->isEnabled && line.empty()) + { + line += "
    "; + this->isFinished = true; + return; + } + + line += " "; + } + +private: + bool isStarted; + bool isFinished; + bool isEnabled; +}; // class ParagraphParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/parser.h b/maddy/parser.h new file mode 100644 index 0000000..9f4a4cb --- /dev/null +++ b/maddy/parser.h @@ -0,0 +1,412 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include + +#include "maddy/parserconfig.h" + +// BlockParser +#include "maddy/checklistparser.h" +#include "maddy/codeblockparser.h" +#include "maddy/headlineparser.h" +#include "maddy/horizontallineparser.h" +#include "maddy/htmlparser.h" +#include "maddy/latexblockparser.h" +#include "maddy/orderedlistparser.h" +#include "maddy/paragraphparser.h" +#include "maddy/quoteparser.h" +#include "maddy/tableparser.h" +#include "maddy/unorderedlistparser.h" + +// LineParser +#include "maddy/breaklineparser.h" +#include "maddy/emphasizedparser.h" +#include "maddy/imageparser.h" +#include "maddy/inlinecodeparser.h" +#include "maddy/italicparser.h" +#include "maddy/linkparser.h" +#include "maddy/strikethroughparser.h" +#include "maddy/strongparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * Parser + * + * Transforms Markdown to HTML + * + * @class + */ +class Parser +{ +public: + /** + * Version info + * + * Check https://github.com/progsource/maddy/blob/master/CHANGELOG.md + * for the changelog. + */ + static const std::string& version() + { + static const std::string v = "1.3.0"; + return v; + } + + /** + * ctor + * + * Initializes all `LineParser` + * + * @method + */ + Parser(std::shared_ptr config = nullptr) : config(config) + { + // deprecated backward compatibility + // will be removed in 1.4.0 latest including the booleans + if (this->config && !this->config->isEmphasizedParserEnabled) + { + this->config->enabledParsers &= ~maddy::types::EMPHASIZED_PARSER; + } + if (this->config && !this->config->isHTMLWrappedInParagraph) + { + this->config->enabledParsers |= maddy::types::HTML_PARSER; + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::BREAKLINE_PARSER) != 0) + { + this->breakLineParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::EMPHASIZED_PARSER) != 0) + { + this->emphasizedParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::IMAGE_PARSER) != 0) + { + this->imageParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::INLINE_CODE_PARSER) != 0) + { + this->inlineCodeParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::ITALIC_PARSER) != 0) + { + this->italicParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::LINK_PARSER) != 0) + { + this->linkParser = std::make_shared(); + } + + if (!this->config || (this->config->enabledParsers & + maddy::types::STRIKETHROUGH_PARSER) != 0) + { + this->strikeThroughParser = std::make_shared(); + } + + if (!this->config || + (this->config->enabledParsers & maddy::types::STRONG_PARSER) != 0) + { + this->strongParser = std::make_shared(); + } + } + + /** + * Parse + * + * @method + * @param {const std::istream&} markdown + * @return {std::string} HTML + */ + std::string Parse(std::istream& markdown) const + { + std::string result = ""; + std::shared_ptr currentBlockParser = nullptr; + + for (std::string line; std::getline(markdown, line);) + { + if (!currentBlockParser) + { + currentBlockParser = getBlockParserForLine(line); + } + + if (currentBlockParser) + { + currentBlockParser->AddLine(line); + + if (currentBlockParser->IsFinished()) + { + result += currentBlockParser->GetResult().str(); + currentBlockParser = nullptr; + } + } + } + + // make sure, that all parsers are finished + if (currentBlockParser) + { + std::string emptyLine = ""; + currentBlockParser->AddLine(emptyLine); + if (currentBlockParser->IsFinished()) + { + result += currentBlockParser->GetResult().str(); + currentBlockParser = nullptr; + } + } + + return result; + } + +private: + std::shared_ptr config; + std::shared_ptr breakLineParser; + std::shared_ptr emphasizedParser; + std::shared_ptr imageParser; + std::shared_ptr inlineCodeParser; + std::shared_ptr italicParser; + std::shared_ptr linkParser; + std::shared_ptr strikeThroughParser; + std::shared_ptr strongParser; + + // block parser have to run before + void runLineParser(std::string& line) const + { + // Attention! ImageParser has to be before LinkParser + if (this->imageParser) + { + this->imageParser->Parse(line); + } + + if (this->linkParser) + { + this->linkParser->Parse(line); + } + + // Attention! StrongParser has to be before EmphasizedParser + if (this->strongParser) + { + this->strongParser->Parse(line); + } + + if (this->emphasizedParser) + { + this->emphasizedParser->Parse(line); + } + + if (this->strikeThroughParser) + { + this->strikeThroughParser->Parse(line); + } + + if (this->inlineCodeParser) + { + this->inlineCodeParser->Parse(line); + } + + if (this->italicParser) + { + this->italicParser->Parse(line); + } + + if (this->breakLineParser) + { + this->breakLineParser->Parse(line); + } + } + + std::shared_ptr getBlockParserForLine(const std::string& line + ) const + { + std::shared_ptr parser; + + if ((!this->config || (this->config->enabledParsers & + maddy::types::CODE_BLOCK_PARSER) != 0) && + maddy::CodeBlockParser::IsStartingLine(line)) + { + parser = std::make_shared(nullptr, nullptr); + } + else if (this->config && + (this->config->enabledParsers & maddy::types::LATEX_BLOCK_PARSER + ) != 0 && + maddy::LatexBlockParser::IsStartingLine(line)) + { + parser = std::make_shared(nullptr, nullptr); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::HEADLINE_PARSER) != 0) && + maddy::HeadlineParser::IsStartingLine(line)) + { + if (!this->config || this->config->isHeadlineInlineParsingEnabled) + { + parser = std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + nullptr, + true + ); + } + else + { + parser = + std::make_shared(nullptr, nullptr, false); + } + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::HORIZONTAL_LINE_PARSER) != 0) && + maddy::HorizontalLineParser::IsStartingLine(line)) + { + parser = std::make_shared(nullptr, nullptr); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::QUOTE_PARSER) != 0) && + maddy::QuoteParser::IsStartingLine(line)) + { + parser = std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + [this](const std::string& line) + { return this->getBlockParserForLine(line); } + ); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::TABLE_PARSER) != 0) && + maddy::TableParser::IsStartingLine(line)) + { + parser = std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, nullptr + ); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::CHECKLIST_PARSER) != 0) && + maddy::ChecklistParser::IsStartingLine(line)) + { + parser = this->createChecklistParser(); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::ORDERED_LIST_PARSER) != 0) && + maddy::OrderedListParser::IsStartingLine(line)) + { + parser = this->createOrderedListParser(); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::UNORDERED_LIST_PARSER) != 0) && + maddy::UnorderedListParser::IsStartingLine(line)) + { + parser = this->createUnorderedListParser(); + } + else if (this->config && + (this->config->enabledParsers & maddy::types::HTML_PARSER) != 0 && + maddy::HtmlParser::IsStartingLine(line)) + { + parser = std::make_shared(nullptr, nullptr); + } + else if (maddy::ParagraphParser::IsStartingLine(line)) + { + parser = std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + nullptr, + (!this->config || + (this->config->enabledParsers & maddy::types::PARAGRAPH_PARSER) != 0) + ); + } + + return parser; + } + + std::shared_ptr createChecklistParser() const + { + return std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + [this](const std::string& line) + { + std::shared_ptr parser; + + if ((!this->config || (this->config->enabledParsers & + maddy::types::CHECKLIST_PARSER) != 0) && + maddy::ChecklistParser::IsStartingLine(line)) + { + parser = this->createChecklistParser(); + } + + return parser; + } + ); + } + + std::shared_ptr createOrderedListParser() const + { + return std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + [this](const std::string& line) + { + std::shared_ptr parser; + + if ((!this->config || (this->config->enabledParsers & + maddy::types::ORDERED_LIST_PARSER) != 0) && + maddy::OrderedListParser::IsStartingLine(line)) + { + parser = this->createOrderedListParser(); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::UNORDERED_LIST_PARSER) != 0 + ) && + maddy::UnorderedListParser::IsStartingLine(line)) + { + parser = this->createUnorderedListParser(); + } + + return parser; + } + ); + } + + std::shared_ptr createUnorderedListParser() const + { + return std::make_shared( + [this](std::string& line) { this->runLineParser(line); }, + [this](const std::string& line) + { + std::shared_ptr parser; + + if ((!this->config || (this->config->enabledParsers & + maddy::types::ORDERED_LIST_PARSER) != 0) && + maddy::OrderedListParser::IsStartingLine(line)) + { + parser = this->createOrderedListParser(); + } + else if ((!this->config || (this->config->enabledParsers & + maddy::types::UNORDERED_LIST_PARSER) != 0 + ) && + maddy::UnorderedListParser::IsStartingLine(line)) + { + parser = this->createUnorderedListParser(); + } + + return parser; + } + ); + } +}; // class Parser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/parserconfig.h b/maddy/parserconfig.h new file mode 100644 index 0000000..04acdc7 --- /dev/null +++ b/maddy/parserconfig.h @@ -0,0 +1,97 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +#include + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +namespace types { + +// clang-format off +/** + * PARSER_TYPE + * + * Bitwise flags to turn on/off each parser +*/ +enum PARSER_TYPE : uint32_t +{ + NONE = 0, + + BREAKLINE_PARSER = 0b1, + CHECKLIST_PARSER = 0b10, + CODE_BLOCK_PARSER = 0b100, + EMPHASIZED_PARSER = 0b1000, + HEADLINE_PARSER = 0b10000, + HORIZONTAL_LINE_PARSER = 0b100000, + HTML_PARSER = 0b1000000, + IMAGE_PARSER = 0b10000000, + INLINE_CODE_PARSER = 0b100000000, + ITALIC_PARSER = 0b1000000000, + LINK_PARSER = 0b10000000000, + ORDERED_LIST_PARSER = 0b100000000000, + PARAGRAPH_PARSER = 0b1000000000000, + QUOTE_PARSER = 0b10000000000000, + STRIKETHROUGH_PARSER = 0b100000000000000, + STRONG_PARSER = 0b1000000000000000, + TABLE_PARSER = 0b10000000000000000, + UNORDERED_LIST_PARSER = 0b100000000000000000, + LATEX_BLOCK_PARSER = 0b1000000000000000000, + + DEFAULT = 0b0111111111110111111, + ALL = 0b1111111111111111111, +}; +// clang-format on + +} // namespace types + +/** + * ParserConfig + * + * @class + */ +struct ParserConfig +{ + /** + * @deprecated will be removed in 1.4.0 latest + * + * this flag = false == `enabledParsers &= ~maddy::types::EMPHASIZED_PARSER` + */ + bool isEmphasizedParserEnabled; + + /** + * @deprecated will be removed in 1.4.0 latest + * + * this flag = false == `enabledParsers |= maddy::types::HTML_PARSER` + */ + bool isHTMLWrappedInParagraph; + + /** + * en-/disable headline inline-parsing + * + * default: enabled + */ + bool isHeadlineInlineParsingEnabled; + + /** + * enabled parsers bitfield + */ + uint32_t enabledParsers; + + ParserConfig() + : isEmphasizedParserEnabled(true) + , isHTMLWrappedInParagraph(true) + , isHeadlineInlineParsingEnabled(true) + , enabledParsers(maddy::types::DEFAULT) + {} +}; // class ParserConfig + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/quoteparser.h b/maddy/quoteparser.h new file mode 100644 index 0000000..13151ef --- /dev/null +++ b/maddy/quoteparser.h @@ -0,0 +1,152 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * QuoteParser + * + * @class + */ +class QuoteParser : public BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + QuoteParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isStarted(false) + , isFinished(false) + {} + + /** + * IsStartingLine + * + * A quote starts with `> `. + * + * @method + * @param {const std::string&} line + * @return {bool} + */ + static bool IsStartingLine(const std::string& line) + { + static std::regex re(R"(^\>.*)"); + return std::regex_match(line, re); + } + + /** + * AddLine + * + * Adding a line which has to be parsed. + * + * @method + * @param {std::string&} line + * @return {void} + */ + void AddLine(std::string& line) override + { + if (!this->isStarted) + { + this->result << "
    "; + this->isStarted = true; + } + + bool finish = false; + if (line.empty()) + { + finish = true; + } + + this->parseBlock(line); + + if (this->isInlineBlockAllowed() && !this->childParser) + { + this->childParser = this->getBlockParserForLine(line); + } + + if (this->childParser) + { + this->childParser->AddLine(line); + + if (this->childParser->IsFinished()) + { + this->result << this->childParser->GetResult().str(); + this->childParser = nullptr; + } + + return; + } + + if (this->isLineParserAllowed()) + { + this->parseLine(line); + } + + if (finish) + { + this->result << "
    "; + this->isFinished = true; + } + + this->result << line; + } + + /** + * IsFinished + * + * @method + * @return {bool} + */ + bool IsFinished() const override { return this->isFinished; } + +protected: + bool isInlineBlockAllowed() const override { return true; } + + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string& line) override + { + static std::regex lineRegexWithSpace(R"(^\> )"); + line = std::regex_replace(line, lineRegexWithSpace, ""); + static std::regex lineRegexWithoutSpace(R"(^\>)"); + line = std::regex_replace(line, lineRegexWithoutSpace, ""); + + if (!line.empty()) + { + line += " "; + } + } + +private: + bool isStarted; + bool isFinished; +}; // class QuoteParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/strikethroughparser.h b/maddy/strikethroughparser.h new file mode 100644 index 0000000..effa5d8 --- /dev/null +++ b/maddy/strikethroughparser.h @@ -0,0 +1,52 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include + +#include "maddy/lineparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * StrikeThroughParser + * + * @class + */ +class StrikeThroughParser : public LineParser +{ +public: + /** + * Parse + * + * From Markdown: `text ~~text~~` + * + * To HTML: `text text` + * + * @method + * @param {std::string&} line The line to interpret + * @return {void} + */ + void Parse(std::string& line) override + { + static std::regex re( + R"((?!.*`.*|.*.*)\~\~(?!.*`.*|.*<\/code>.*)([^\~]*)\~\~(?!.*`.*|.*<\/code>.*))" + ); + static std::string replacement = "$1"; + + line = std::regex_replace(line, re, replacement); + } +}; // class StrikeThroughParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/strongparser.h b/maddy/strongparser.h new file mode 100644 index 0000000..348f2d4 --- /dev/null +++ b/maddy/strongparser.h @@ -0,0 +1,61 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include + +#include "maddy/lineparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * StrongParser + * + * Has to be used before the `EmphasizedParser`. + * + * @class + */ +class StrongParser : public LineParser +{ +public: + /** + * Parse + * + * From Markdown: `text **text** __text__` + * + * To HTML: `text text text` + * + * @method + * @param {std::string&} line The line to interpret + * @return {void} + */ + void Parse(std::string& line) override + { + static std::vector res{ + std::regex{ + R"((?!.*`.*|.*.*)\*\*(?!.*`.*|.*<\/code>.*)([^\*\*]*)\*\*(?!.*`.*|.*<\/code>.*))" + }, + std::regex{ + R"((?!.*`.*|.*.*)__(?!.*`.*|.*<\/code>.*)([^__]*)__(?!.*`.*|.*<\/code>.*))" + } + }; + static std::string replacement = "$1"; + for (const auto& re : res) + { + line = std::regex_replace(line, re, replacement); + } + } +}; // class StrongParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/tableparser.h b/maddy/tableparser.h new file mode 100644 index 0000000..9f1051f --- /dev/null +++ b/maddy/tableparser.h @@ -0,0 +1,233 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * TableParser + * + * For more information, see the docs folder. + * + * @class + */ +class TableParser : public BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + TableParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isStarted(false) + , isFinished(false) + , currentBlock(0) + , currentRow(0) + {} + + /** + * IsStartingLine + * + * If the line has exact `|table>`, then it is starting the table. + * + * @method + * @param {const std::string&} line + * @return {bool} + */ + static bool IsStartingLine(const std::string& line) + { + static std::string matchString("|table>"); + return line == matchString; + } + + /** + * AddLine + * + * Adding a line which has to be parsed. + * + * @method + * @param {std::string&} line + * @return {void} + */ + void AddLine(std::string& line) override + { + 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; + } + } + + /** + * IsFinished + * + * A table ends with `|isFinished; } + +protected: + bool isInlineBlockAllowed() const override { return false; } + + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string&) override + { + result << ""; + + bool hasHeader = false; + bool hasFooter = false; + bool isFirstBlock = true; + uint32_t currentBlockNumber = 0; + + if (this->table.size() > 1) + { + hasHeader = true; + } + + if (this->table.size() >= 3) + { + hasFooter = true; + } + + for (const std::vector>& block : this->table) + { + bool isInHeader = false; + bool isInFooter = false; + ++currentBlockNumber; + + if (hasHeader && isFirstBlock) + { + result << ""; + isInHeader = true; + } + else if (hasFooter && currentBlockNumber == this->table.size()) + { + result << ""; + isInFooter = true; + } + else + { + result << ""; + } + + for (const std::vector& row : block) + { + result << ""; + + for (const std::string& column : row) + { + if (isInHeader) + { + result << ""; + } + } + + result << ""; + } + + if (isInHeader) + { + result << ""; + } + else if (isInFooter) + { + result << ""; + } + else + { + result << ""; + } + + isFirstBlock = false; + } + + result << "
    "; + } + else + { + result << ""; + } + + result << column; + + if (isInHeader) + { + result << ""; + } + else + { + result << "
    "; + } + +private: + bool isStarted; + bool isFinished; + uint32_t currentBlock; + uint32_t currentRow; + std::vector>> table; +}; // class TableParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/maddy/unorderedlistparser.h b/maddy/unorderedlistparser.h new file mode 100644 index 0000000..53859bf --- /dev/null +++ b/maddy/unorderedlistparser.h @@ -0,0 +1,118 @@ +/* + * This project is licensed under the MIT license. For more information see the + * LICENSE file. + */ +#pragma once + +// ----------------------------------------------------------------------------- + +#include +#include +#include + +#include "maddy/blockparser.h" + +// ----------------------------------------------------------------------------- + +namespace maddy { + +// ----------------------------------------------------------------------------- + +/** + * UnorderedListParser + * + * @class + */ +class UnorderedListParser : public BlockParser +{ +public: + /** + * ctor + * + * @method + * @param {std::function} parseLineCallback + * @param {std::function(const std::string& + * line)>} getBlockParserForLineCallback + */ + UnorderedListParser( + std::function parseLineCallback, + std::function(const std::string& line)> + getBlockParserForLineCallback + ) + : BlockParser(parseLineCallback, getBlockParserForLineCallback) + , isStarted(false) + , isFinished(false) + {} + + /** + * IsStartingLine + * + * An unordered list starts with `* `. + * + * @method + * @param {const std::string&} line + * @return {bool} + */ + static bool IsStartingLine(const std::string& line) + { + static std::regex re("^[+*-] .*"); + return std::regex_match(line, re); + } + + /** + * IsFinished + * + * @method + * @return {bool} + */ + bool IsFinished() const override { return this->isFinished; } + +protected: + bool isInlineBlockAllowed() const override { return true; } + + bool isLineParserAllowed() const override { return true; } + + void parseBlock(std::string& line) override + { + bool isStartOfNewListItem = IsStartingLine(line); + uint32_t indentation = getIndentationWidth(line); + + static std::regex lineRegex("^([+*-] )"); + line = std::regex_replace(line, lineRegex, ""); + + if (!this->isStarted) + { + line = "
    • " + line; + this->isStarted = true; + return; + } + + if (indentation >= 2) + { + line = line.substr(2); + return; + } + + if (line.empty() || line.find("
    • ") != std::string::npos || + line.find("
    • ") != std::string::npos || + line.find("
    ") != std::string::npos) + { + line = "
  • " + line; + this->isFinished = true; + return; + } + + if (isStartOfNewListItem) + { + line = "
  • " + line; + } + } + +private: + bool isStarted; + bool isFinished; +}; // class UnorderedListParser + +// ----------------------------------------------------------------------------- + +} // namespace maddy diff --git a/resources/logo.png b/resources/logo.png old mode 100755 new mode 100644 diff --git a/src/frames/configdialog.ui b/src/frames/configdialog.ui index 89e1d30..19c0fce 100644 --- a/src/frames/configdialog.ui +++ b/src/frames/configdialog.ui @@ -95,7 +95,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -121,7 +121,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -161,7 +161,7 @@ Author: Aurélie Delhaie (github.com/mojitaurelie) - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter @@ -177,7 +177,7 @@ Version 1.2.0.0 - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter @@ -198,7 +198,7 @@ WorkPad - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -217,7 +217,7 @@ - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -233,7 +233,7 @@ <html><head/><body><p>Under MIT license</p></body></html> - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter @@ -249,7 +249,7 @@ true - Copyright 2022 Aurélie Delhaie + Copyright 2025 Aurélie Delhaie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -270,7 +270,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES - QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Ok diff --git a/src/frames/mainwindow.cpp b/src/frames/mainwindow.cpp old mode 100755 new mode 100644 index 5e53c48..b130f89 --- a/src/frames/mainwindow.cpp +++ b/src/frames/mainwindow.cpp @@ -13,6 +13,8 @@ MainWindow::MainWindow(QWidget *parent) { ui->setupUi(this); timer = new QTimer(this); + updateViewThread = nullptr; + parser = std::make_shared(); connect(ui->actionAdd_folder, &QAction::triggered, this, &MainWindow::createFolder); connect(ui->actionAdd, &QAction::triggered, this, &MainWindow::createNote); connect(ui->actionSave, &QAction::triggered, this, &MainWindow::save); @@ -21,6 +23,7 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->plainTextEdit, &QPlainTextEdit::textChanged, this, &MainWindow::plainContentChanged); connect(ui->treeWidget, &QTreeWidget::itemSelectionChanged, this, &MainWindow::selectionChanged); connect(ui->treeWidget, &QTreeWidget::customContextMenuRequested, this, &MainWindow::prepareMenu); + connect(this, &MainWindow::updateViewers, this, &MainWindow::viewReady); const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); ui->contentEdit->setFont(fixedFont); ui->plainTextEdit->setFont(fixedFont); @@ -100,8 +103,7 @@ void MainWindow::selectionChanged() ui->contentEdit->setDisabled(false); ui->plainTextEdit->setPlainText(n->getContent()); ui->contentEdit->setPlainText(n->getContent()); - ui->markdownViewer->setMarkdown(ui->contentEdit->toPlainText()); - ui->markdownViewer2->setMarkdown(n->getContent()); + updateHTMLView(); ui->plainTextEdit->blockSignals(false); ui->contentEdit->blockSignals(false); @@ -331,6 +333,12 @@ void MainWindow::exportNote() } } +void MainWindow::viewReady(QString html) +{ + ui->webEngineViewer->setHtml(html); + ui->webEngineEditor->setHtml(html); +} + void MainWindow::markdownContentChanged() { timer->stop(); @@ -341,17 +349,11 @@ void MainWindow::markdownContentChanged() QString uuid = ui->treeWidget->selectedItems()[0]->text(COLUMN_UUID); Note *n = savemng->getNoteByUUID(uuid); if (n != nullptr) { - QScrollBar *scrollbar = ui->markdownViewer->verticalScrollBar(); QString content = ui->contentEdit->toPlainText(); - - int pos = scrollbar->sliderPosition(); - ui->markdownViewer->setMarkdown(content); - scrollbar->setSliderPosition(pos); - - ui->markdownViewer2->setMarkdown(content); ui->plainTextEdit->setPlainText(content); n->setContent(content); + updateHTMLView(); } } ui->plainTextEdit->blockSignals(false); @@ -374,10 +376,9 @@ void MainWindow::plainContentChanged() Note *n = savemng->getNoteByUUID(uuid); if (n != nullptr) { QString content = ui->plainTextEdit->toPlainText(); - ui->markdownViewer->setMarkdown(content); - ui->markdownViewer2->setMarkdown(content); ui->contentEdit->setPlainText(content); n->setContent(content); + updateHTMLView(); } } ui->plainTextEdit->blockSignals(false); @@ -410,6 +411,23 @@ void MainWindow::updateListView() } } +void MainWindow::updateHTMLView() +{ + if (threadLock.tryLock()) { + updateViewThread = new std::thread([this](){ + QString content = ui->plainTextEdit->toPlainText(); + std::stringstream markdownInput(content.toStdString()); + QString htmlOutput = QString::fromStdString(parser->Parse(markdownInput)); + emit updateViewers(htmlOutput); + threadLock.unlock(); + }); + } else { + QTimer::singleShot(200, [this]() { + updateHTMLView(); + }); + } +} + void MainWindow::clearAndDisableFields() { ui->contentEdit->blockSignals(true); @@ -420,8 +438,6 @@ void MainWindow::clearAndDisableFields() ui->plainTextEdit->setDisabled(true); ui->plainTextEdit->clear(); ui->contentEdit->clear(); - ui->markdownViewer->clear(); - ui->markdownViewer2->clear(); ui->titleLabel->setText(""); this->setWindowTitle("WorkPad"); diff --git a/src/frames/mainwindow.h b/src/frames/mainwindow.h old mode 100755 new mode 100644 index 1c329a9..4eb3d90 --- a/src/frames/mainwindow.h +++ b/src/frames/mainwindow.h @@ -10,10 +10,15 @@ #include #include #include +#include +#include #include "../services/savemanager.h" #include "../services/configmanager.h" +#include "../../maddy/parserconfig.h" +#include "../../maddy/parser.h" + #define COLUMN_NAME 0 #define COLUMN_UUID 1 #define COLUMN_TYPE 2 @@ -33,6 +38,9 @@ public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); +signals: + void updateViewers(QString html); + private slots: void createNote(); void createFolder(); @@ -46,15 +54,20 @@ private slots: void moveNote(); void editName(); void exportNote(); + void viewReady(QString html); private: Ui::MainWindow *ui; SaveManager *savemng; ConfigManager *cfgmng; QTimer *timer; + std::thread *updateViewThread; + std::shared_ptr parser; + QMutex threadLock; void updateListView(); void clearAndDisableFields(); void selectTreeItem(QString uuid); + void updateHTMLView(); }; #endif // MAINWINDOW_H diff --git a/src/frames/mainwindow.ui b/src/frames/mainwindow.ui old mode 100755 new mode 100644 index 214f585..f900f2d --- a/src/frames/mainwindow.ui +++ b/src/frames/mainwindow.ui @@ -35,7 +35,7 @@ - QAbstractItemView::NoEditTriggers + QAbstractItemView::EditTrigger::NoEditTriggers true @@ -50,7 +50,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -71,10 +71,10 @@ - QTabWidget::South + QTabWidget::TabPosition::South - QTabWidget::Rounded + QTabWidget::TabShape::Rounded 0 @@ -117,36 +117,18 @@ - QPlainTextEdit::NoWrap + QPlainTextEdit::LineWrapMode::NoWrap - - - true - + 200 - 0 + 200 - - QTextEdit::AutoAll - - - QTextEdit::NoWrap - - - true - - - false - - - Qt::TextSelectableByMouse - @@ -159,14 +141,7 @@ - - - QTextEdit::NoWrap - - - true - - + @@ -189,7 +164,7 @@ - Qt::PreventContextMenu + Qt::ContextMenuPolicy::PreventContextMenu toolBar @@ -198,7 +173,7 @@ false - Qt::ToolButtonTextUnderIcon + Qt::ToolButtonStyle::ToolButtonTextUnderIcon false @@ -277,6 +252,14 @@ + + + QWebEngineView + QWidget +
    qwebengineview.h
    + 1 +
    +
    diff --git a/src/main.cpp b/src/main.cpp old mode 100755 new mode 100644 diff --git a/src/models/note.cpp b/src/models/note.cpp old mode 100755 new mode 100644 diff --git a/src/models/note.h b/src/models/note.h old mode 100755 new mode 100644 diff --git a/src/services/savemanager.cpp b/src/services/savemanager.cpp old mode 100755 new mode 100644 diff --git a/src/services/savemanager.h b/src/services/savemanager.h old mode 100755 new mode 100644