diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/markdownrenderer.cpp | 92 | ||||
| -rw-r--r-- | src/markdownrenderer.h | 38 |
3 files changed, 133 insertions, 1 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b63ff3e..6108696 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(qtmaildir_lib STATIC keymap.cpp config.cpp mimeparser.cpp + markdownrenderer.cpp requestinterceptor.cpp htmlbuilder.cpp cidschemehandler.cpp @@ -36,7 +37,8 @@ target_include_directories(qtmaildir_lib target_link_libraries(qtmaildir_lib PUBLIC Qt6::Widgets Qt6::Svg Qt6::WebEngineWidgets PkgConfig::GMIME - ${NOTMUCH_LIBRARY}) + ${NOTMUCH_LIBRARY} PkgConfig::CMARK_GFM + ${CMARK_GFM_EXTENSIONS_LIBRARY}) # resources.qrc belongs to the executable, not to the static library. A qrc # compiled into a .a registers itself from a global initialiser, and the linker diff --git a/src/markdownrenderer.cpp b/src/markdownrenderer.cpp new file mode 100644 index 0000000..a9b8177 --- /dev/null +++ b/src/markdownrenderer.cpp @@ -0,0 +1,92 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +// cmark-gfm's headers are C and carry no Qt interaction, so the gmime +// include-order rule does not apply here. They still go first, for consistency +// with mimeparser.cpp. +#include <cmark-gfm.h> +#include <cmark-gfm-core-extensions.h> + +#include "markdownrenderer.h" + +#include <QByteArray> + +#include <cstdlib> + +namespace { + +/// The extensions this application enables, by cmark-gfm's own names. +/// +/// `table` is absent deliberately, not by oversight: tables render badly +/// across mail clients regardless of who generates them. `tagfilter` is absent +/// because CMARK_OPT_SAFE already suppresses raw HTML wholesale, which is the +/// stronger measure. +const char *const kExtensions[] = { "autolink", "strikethrough", "tasklist" }; + +} // namespace + +QString MarkdownRenderer::toHtml(const QString &markdown) +{ + if (markdown.isEmpty()) + return {}; + + // Idempotent and required before cmark_find_syntax_extension() can resolve + // any name. Calling it per render rather than once at startup keeps this + // function free of initialisation order concerns; it is a hash lookup + // after the first call. + cmark_gfm_core_extensions_ensure_registered(); + + // SAFE suppresses raw HTML in the INPUT. It does not escape the output, + // which is markup by definition. + const int options = CMARK_OPT_DEFAULT | CMARK_OPT_SAFE; + + cmark_parser *parser = cmark_parser_new(options); + if (!parser) + return {}; + + for (const char *name : kExtensions) { + // A missing extension is a broken installation rather than a + // condition to handle: the library was found by CMake. Skipping it + // degrades to plain CommonMark rather than crashing. + if (cmark_syntax_extension *extension = cmark_find_syntax_extension(name)) + cmark_parser_attach_syntax_extension(parser, extension); + } + + const QByteArray utf8 = markdown.toUtf8(); + cmark_parser_feed(parser, utf8.constData(), static_cast<size_t>(utf8.size())); + + cmark_node *document = cmark_parser_finish(parser); + if (!document) { + cmark_parser_free(parser); + return {}; + } + + // The extension list must be passed to the renderer as well as to the + // parser. Passing nullptr here parses the tasklist correctly and then + // renders it as a plain list item, which looks like the extension never + // worked. + char *html = cmark_render_html(document, options, + cmark_parser_get_syntax_extensions(parser)); + const QString result = html ? QString::fromUtf8(html) : QString(); + + free(html); + cmark_node_free(document); + cmark_parser_free(parser); + + return result; +} diff --git a/src/markdownrenderer.h b/src/markdownrenderer.h new file mode 100644 index 0000000..80c52bf --- /dev/null +++ b/src/markdownrenderer.h @@ -0,0 +1,38 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include <QString> + +/// Renders the composer's markdown body into the HTML part's fragment. +/// +/// A namespace of free functions rather than a class: there is no state, and +/// keeping it painter-free and widget-free is what lets the extension +/// configuration be tested on its own. `MessageBuilder` calls this; nothing +/// else does. +namespace MarkdownRenderer { + +/// The markdown source as an HTML fragment: no <html>, <head> or <body>. +/// +/// Three extensions are enabled (autolink, strikethrough, tasklist) and +/// tables are deliberately not. Raw HTML in the input is suppressed by +/// CMARK_OPT_SAFE. +QString toHtml(const QString &markdown); + +} // namespace MarkdownRenderer |
