diff options
author | lns <matzeton@googlemail.com> | 2021-10-20 19:58:38 +0200 |
---|---|---|
committer | lns <matzeton@googlemail.com> | 2021-10-20 19:58:38 +0200 |
commit | 384724a00be9dbb8f40d295f99b9c6bcfb48b387 (patch) | |
tree | 6e053365bb995bf675397368281c0a3ff92bd9fc | |
parent | 7b01dd8e4b1779e4c4dd782dbec87acce0f4754b (diff) |
Added MD4C support to render HTML from Markdown text.
* additional blog metadata properties
Signed-off-by: lns <matzeton@googlemail.com>
-rw-r--r-- | CMakeLists.txt | 19 | ||||
-rw-r--r-- | blog/my-second-blog-entry.json | 5 | ||||
-rw-r--r-- | blog/my-second-blog-entry.md | 3 | ||||
-rw-r--r-- | blog/my-third-blog-entry.json | 8 | ||||
-rw-r--r-- | blog/my-third-blog-entry.md | 2 | ||||
-rw-r--r-- | blog/my-very-first-blog-entry.json | 5 | ||||
-rw-r--r-- | src/EventManager.cpp | 6 | ||||
-rw-r--r-- | src/EventManager.hpp | 1 | ||||
-rw-r--r-- | src/Filesystem.cpp | 14 | ||||
-rw-r--r-- | src/Filesystem.hpp | 3 | ||||
-rw-r--r-- | src/TemplateManager.cpp | 63 | ||||
-rw-r--r-- | src/content/blog/Blog.cpp | 54 | ||||
-rw-r--r-- | src/content/blog/Blog.hpp | 4 | ||||
-rw-r--r-- | src/content/markdown/Markdown.cpp | 32 | ||||
-rw-r--r-- | src/main.cpp | 57 | ||||
-rw-r--r-- | wwwroot/index.html | 21 |
16 files changed, 182 insertions, 115 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index b107660..1de9b72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,27 @@ set(CMAKE_CXX_FLAGS_DEBUG "-g3 -fpic -fno-omit-frame-pointer -fno-rtti -ffunctio set(CMAKE_CXX_FLAGS_RELEASE "-Os -fpic -fomit-frame-pointer -ffunction-sections -fdata-sections -flto") set(CMAKE_EXE_LINKER_FLAGS "-Wl,-gc-sections") -set(INJA_SRCDIR "${PROJECT_SOURCE_DIR}/deps/inja" CACHE STRING "Path to the inja source directory.") +set(INJA_SRCDIR "${PROJECT_SOURCE_DIR}/deps/inja" CACHE STRING + "Path to the inja source directory.") if(NOT EXISTS "${INJA_SRCDIR}/single_include/inja/inja.hpp") message(FATAL_ERROR "inja missing") endif() -include_directories(${CPP_HTTPLIB_SRCDIR} ${INJA_SRCDIR}/single_include ${INJA_SRCDIR}/third_party/include) +set(MD4C_SRCDIR "${PROJECT_SOURCE_DIR}/deps/md4c/src" CACHE STRING + "Path to the md4c source directory.") +if(NOT EXISTS "${MD4C_SRCDIR}/md4c-html.h") + message(FATAL_ERROR "md4c missing") +endif() + +include_directories(${CPP_HTTPLIB_SRCDIR} ${INJA_SRCDIR}/single_include + ${INJA_SRCDIR}/third_party/include + ${MD4C_SRCDIR}) +set(MD4C_SOURCES "${MD4C_SRCDIR}/entity.c" + "${MD4C_SRCDIR}/md4c.c" + "${MD4C_SRCDIR}/md4c-html.c") + file(GLOB_RECURSE SOURCES "src/*.cpp") -add_executable(cpp-web ${SOURCES}) +add_executable(cpp-web ${MD4C_SOURCES} ${SOURCES}) #target_compile_definitions(cpp-web PUBLIC CPPHTTPLIB_THREAD_POOL_COUNT=4) #target_compile_options(cpp-web PUBLIC "-std=c++14") if(CMAKE_BUILD_TYPE MATCHES Release) diff --git a/blog/my-second-blog-entry.json b/blog/my-second-blog-entry.json index b8c2ec0..ecb2b8b 100644 --- a/blog/my-second-blog-entry.json +++ b/blog/my-second-blog-entry.json @@ -1,5 +1,8 @@ { + "title": "Second blog entry.", + "tags": [ "bla" ], + "author": "lns", "createDate": "23.02.1989 23:59", "publishDate": "30.12.2020 1:00", - "published": false + "published": true } diff --git a/blog/my-second-blog-entry.md b/blog/my-second-blog-entry.md index 292b1e1..054046a 100644 --- a/blog/my-second-blog-entry.md +++ b/blog/my-second-blog-entry.md @@ -1,2 +1,5 @@ Text ======= + +*lorem* **ipsum** +~~striked~~ diff --git a/blog/my-third-blog-entry.json b/blog/my-third-blog-entry.json new file mode 100644 index 0000000..9682cd2 --- /dev/null +++ b/blog/my-third-blog-entry.json @@ -0,0 +1,8 @@ +{ + "title": "Third.", + "tags": [ "test" ], + "author": "lns", + "createDate": "24.02.1989 23:59", + "publishDate": "31.12.2020 1:00", + "published": false +} diff --git a/blog/my-third-blog-entry.md b/blog/my-third-blog-entry.md new file mode 100644 index 0000000..292b1e1 --- /dev/null +++ b/blog/my-third-blog-entry.md @@ -0,0 +1,2 @@ +Text +======= diff --git a/blog/my-very-first-blog-entry.json b/blog/my-very-first-blog-entry.json index dec2711..52f52f7 100644 --- a/blog/my-very-first-blog-entry.json +++ b/blog/my-very-first-blog-entry.json @@ -1,5 +1,8 @@ { + "title": "My first blog entry!", + "tags": [ "test", "bla" ], + "author": "lns", "createDate": "22.02.1989 23:59", "publishDate": "31.12.2020 1:00", - "published": false + "published": true } diff --git a/src/EventManager.cpp b/src/EventManager.cpp index 6c735a8..d45dcee 100644 --- a/src/EventManager.cpp +++ b/src/EventManager.cpp @@ -189,13 +189,15 @@ bool EventManager::Init(std::string host, uint16_t port) } evhttp_set_gencb(m_EvHttp, EvGenericInterceptor, &m_DefaultCallback); - m_EvSocket = evhttp_bind_socket_with_handle(m_EvHttp, host.c_str(), port); - if (m_EvSocket == nullptr) + if (evhttp_bind_socket(m_EvHttp, host.c_str(), port) != 0) { fprintf(stderr, "couldn't bind to %s:%d. Exiting.\n", host.c_str(), port); return false; } + evhttp_set_allowed_methods(m_EvHttp, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_HEAD); + evhttp_set_default_content_type(m_EvHttp, NULL); + m_EvTermEvent = evsignal_new(m_EvBase, SIGINT, do_term, m_EvBase); if (m_EvTermEvent == nullptr) { diff --git a/src/EventManager.hpp b/src/EventManager.hpp index 5d74602..f994510 100644 --- a/src/EventManager.hpp +++ b/src/EventManager.hpp @@ -42,7 +42,6 @@ private: struct event_config * m_EvConfig = nullptr; struct event_base * m_EvBase = nullptr; struct evhttp * m_EvHttp = nullptr; - struct evhttp_bound_socket * m_EvSocket = nullptr; struct evconnlistener * m_EvListener = nullptr; struct event * m_EvTermEvent = nullptr; }; diff --git a/src/Filesystem.cpp b/src/Filesystem.cpp index 04dbb1b..9fe13bb 100644 --- a/src/Filesystem.cpp +++ b/src/Filesystem.cpp @@ -72,8 +72,18 @@ bool Filesystem::AddSingleFile(std::string path, std::string root) std::cout << "Adding file: " << path << " (" << size << " bytes) as " << relpath << " to " << this << std::endl; } - fd.mime = magic_file(m_Magic, path.c_str()); - m_Files[relpath] = fd; + { + char const * const mp = magic_file(m_Magic, path.c_str()); + if (mp != NULL) + { + fd.mime = magic_file(m_Magic, path.c_str()); + } + else + { + fd.mime = "application/octet-stream"; + } + m_Files[relpath] = fd; + } return true; } diff --git a/src/Filesystem.hpp b/src/Filesystem.hpp index 02150fb..98fbdcd 100644 --- a/src/Filesystem.hpp +++ b/src/Filesystem.hpp @@ -9,11 +9,12 @@ #include <vector> using FilesDict = std::unordered_map<std::string, struct file_data>; +using Data = std::vector<unsigned char>; struct file_data { std::string mime; - std::vector<unsigned char> data; + Data data; }; class Filesystem diff --git a/src/TemplateManager.cpp b/src/TemplateManager.cpp index ddde2c4..1e027f2 100644 --- a/src/TemplateManager.cpp +++ b/src/TemplateManager.cpp @@ -4,36 +4,45 @@ TemplateManager::TemplateManager() { - AddInjaCallback("test_fn", 0, [](inja::Arguments & args) { - (void)args; - return "Just a test fn."; - }); - AddInjaCallback("test_return_true", 0, [](inja::Arguments & args) { - (void)args; - return true; - }); + AddInjaCallback("test_fn", + 0, + [](inja::Arguments & args) + { + (void)args; + return "Just a test fn."; + }); + AddInjaCallback("test_return_true", + 0, + [](inja::Arguments & args) + { + (void)args; + return true; + }); /* * indent(input: str, width: int, first: bool, blank: bool); */ - AddInjaCallback("indent", 4, [](inja::Arguments & args) { - std::stringstream stream(args.at(0)->get<std::string>()); - std::string line, out; - bool is_first_line = true; - while (std::getline(stream, line)) - { - if (is_first_line == false || args.at(2)->get<bool>() == true) - { - if (line != "" || args.at(3)->get<bool>() == false) - { - line.insert(0, args.at(1)->get<std::size_t>(), ' '); - } - } - line += '\n'; - out += line; - is_first_line = false; - } - return out; - }); + AddInjaCallback("indent", + 4, + [](inja::Arguments & args) + { + std::stringstream stream(args.at(0)->get<std::string>()); + std::string line, out; + bool is_first_line = true; + while (std::getline(stream, line)) + { + if (is_first_line == false || args.at(2)->get<bool>() == true) + { + if (line != "" || args.at(3)->get<bool>() == false) + { + line.insert(0, args.at(1)->get<std::size_t>(), ' '); + } + } + line += '\n'; + out += line; + is_first_line = false; + } + return out; + }); } void TemplateManager::ParseTemplates(Filesystem & fs) diff --git a/src/content/blog/Blog.cpp b/src/content/blog/Blog.cpp index d2a66c8..ba8638e 100644 --- a/src/content/blog/Blog.cpp +++ b/src/content/blog/Blog.cpp @@ -41,9 +41,9 @@ bool Blog::Init() m_Redirections.push_back(std::filesystem::path(jfile.first).stem()); } - std::sort(m_BlogEntriesSortedByDate.begin(), m_BlogEntriesSortedByDate.end(), [](auto const & a, auto const & b) { - return a->publishDate > b->publishDate; - }); + std::sort(m_BlogEntriesSortedByDate.begin(), + m_BlogEntriesSortedByDate.end(), + [](auto const & a, auto const & b) { return a->publishDate > b->publishDate; }); m_BlogContents.Init(); @@ -100,17 +100,19 @@ bool Blog::ValidateAndSetMetdadata(BlogMetadata const & blogMetadata, BlogEntry { bool retval = true; std::function<bool(BlogMetadata const &, std::string const)> validateMetadata = - [blogEntry](BlogMetadata const & bm, std::string const tname) { - if (bm.find(tname) == bm.cend()) - { - std::cerr << "Metadata validation: JSON key '" << tname << "' missing in " - << blogEntry->metadata_filename << std::endl; - return false; - } - return true; - }; - std::function<bool(std::string const &, std::time_t &)> parseDateTime = [](std::string const & timeStr, - std::time_t & time) { + [blogEntry](BlogMetadata const & bm, std::string const tname) + { + if (bm.find(tname) == bm.cend()) + { + std::cerr << "Metadata validation: JSON key '" << tname << "' missing in " << blogEntry->metadata_filename + << std::endl; + return false; + } + return true; + }; + std::function<bool(std::string const &, std::time_t &)> parseDateTime = + [](std::string const & timeStr, std::time_t & time) + { std::tm tm = {}; std::stringstream ss(timeStr); ss >> std::get_time(&tm, "%d.%m.%y %H:%M"); @@ -124,6 +126,27 @@ bool Blog::ValidateAndSetMetdadata(BlogMetadata const & blogMetadata, BlogEntry return true; }; + if (validateMetadata(blogMetadata, "title") == false) + { + retval = false; + } + blogEntry->title = blogMetadata["title"]; + + if (validateMetadata(blogMetadata, "tags") == false) + { + retval = false; + } + for (auto const & tag : blogMetadata["tags"]) + { + blogEntry->tags.push_back(tag); + } + + if (validateMetadata(blogMetadata, "author") == false) + { + retval = false; + } + blogEntry->author = blogMetadata["author"]; + if (validateMetadata(blogMetadata, "createDate") == false || parseDateTime(blogMetadata["createDate"], blogEntry->createDate) == false) { @@ -184,6 +207,9 @@ void Blog::GenerateBlogListing(RenderData & rd) RenderData re; re["metadata_filename"] = e->metadata_filename; re["content_filename"] = e->content_filename; + re["title"] = e->title; + re["tags"] = e->tags; + re["author"] = e->author; re["createDate"] = e->createDate; re["publishDate"] = e->publishDate; re["published"] = e->published; diff --git a/src/content/blog/Blog.hpp b/src/content/blog/Blog.hpp index f577d69..d99185a 100644 --- a/src/content/blog/Blog.hpp +++ b/src/content/blog/Blog.hpp @@ -16,6 +16,10 @@ struct blog_entry std::string const metadata_filename; std::string const content_filename; + + std::string title; + std::vector<std::string> tags; + std::string author; std::time_t createDate; std::time_t publishDate; bool published; diff --git a/src/content/markdown/Markdown.cpp b/src/content/markdown/Markdown.cpp index 63358b4..a85f333 100644 --- a/src/content/markdown/Markdown.cpp +++ b/src/content/markdown/Markdown.cpp @@ -1,5 +1,7 @@ #include "Markdown.hpp" +#include <md4c-html.h> + Markdown::Markdown(std::string uriBasePath, std::string markdownFilesPath, std::string mainTemplatePath) : Content(), m_UriBasePath(uriBasePath), @@ -8,6 +10,13 @@ Markdown::Markdown(std::string uriBasePath, std::string markdownFilesPath, std:: { } +extern "C" void markdown_to_html_conversion(const MD_CHAR * const text, MD_SIZE size, void * const userdata) +{ + std::string * html = (std::string *)userdata; + + html->append(text, size); +} + bool Markdown::Init() { std::cout << "Markdown files path: " << m_MarkdownFilesPath << std::endl; @@ -22,8 +31,27 @@ bool Markdown::Init() for (auto const & mfile : fs.GetFiles()) { - m_Markdowns[mfile.first] = - std::make_shared<std::string>(std::string(mfile.second.data.begin(), mfile.second.data.end())); + Data const & data = mfile.second.data; + std::string html; + + html.reserve(data.size() / 8 + 64); + int ret = md_html((MD_CHAR const *)data.data(), + data.size(), + markdown_to_html_conversion, + &html, + MD_DIALECT_GITHUB, + MD_FLAG_COLLAPSEWHITESPACE | MD_FLAG_PERMISSIVEURLAUTOLINKS | MD_FLAG_PERMISSIVEWWWAUTOLINKS | + MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_TABLES | + MD_FLAG_STRIKETHROUGH | MD_FLAG_LATEXMATHSPANS | MD_FLAG_WIKILINKS | MD_FLAG_TASKLISTS | + MD_FLAG_UNDERLINE); + + if (ret != 0) + { + std::cerr << "Markdown HTML rendering failed." << std::endl; + return false; + } + + m_Markdowns[mfile.first] = std::make_shared<std::string>(html); } return true; diff --git a/src/main.cpp b/src/main.cpp index 50f0ec5..4969d70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,61 +11,6 @@ #include <inja/inja.hpp> #include <iostream> -static void example_inja_render(struct evhttp_request * const req, EvUserData ud) -{ - (void)ud; - - inja::Environment env; - nlohmann::json data; - data["name"] = "Peter"; - data["city"] = "Brunswick"; - data["age"] = 29; - data["names"] = {"Jeff", "Seb"}; - data["brother"]["name"] = "Chris"; - data["brother"]["daughters"] = {"Maria", "Helen"}; - data["brother"]["daughter0"] = {{"name", "Maria"}}; - data["is_happy"] = true; - data["is_sad"] = false; - data["relatives"]["mother"] = "Maria"; - data["relatives"]["brother"] = "Chris"; - data["relatives"]["sister"] = "Jenny"; - data["vars"] = {2, 3, 4, 0, -1, -2, -3}; - - auto reply = env.render( - "<html><body>\n" - "Hello {{ name }}! I come from {{ city }}.<br>\n" - "Hello {{ names.1 }}!<br>\n" - "Hello {{ brother.name }}!<br>\n" - "Hello {{ brother.daughter0.name }}!<br>\n" - "{{ \"{{ no_value }}\" }}<br>\n" - "Hello{# This is a comment #}!<br>\n" - "{# --- #Todo --- #}<br>\n" - "{% for name in names %}a{% endfor %}<br>\n" - "Hello {% for name in names %}{{ name }} {% endfor %}!<br>\n" - "Hello {% for name in names %}{{ loop.index }}: {{ name }}, {% " - "endfor %}!<br>\n" - "{% for type, name in relatives %}{{ type }}: {{ name }}, {% endfor " - "%}<br>\n" - "{% for v in vars %}{% if v > 0 %}+{% endif %}{% endfor %}<br>\n" - "{% for name in names %}{{ loop.index }}: {{ name }}{% if not " - "loop.is_last %}, {% endif %}{% endfor %}!<br>\n" - "{% for name in names %}{{ loop.index }}: {{ name }}{% if " - "loop.is_last == false %}, {% endif %}{% endfor %}!<br>\n" - "{% for name in names %}a{% endfor %}<br>\n" - "</body></html>\n", - data); - - evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "text/html"); - - struct evbuffer * const output = evbuffer_new(); - if (output != nullptr) - { - evbuffer_add(output, reply.c_str(), reply.size()); - evhttp_send_reply(req, 200, "OK", output); - evbuffer_free(output); - } -} - int main(int argc, char ** argv) { char const * host = "127.0.0.1"; @@ -130,8 +75,6 @@ int main(int argc, char ** argv) } EventManager evmgr(ctmgr); - // evmgr.SetDefaultCallback(example_inja_render, {}); - evmgr.AddCallback("/bla", example_inja_render, {}); evmgr.Init(host, port); // ctmgr.ShutdownAll(); diff --git a/wwwroot/index.html b/wwwroot/index.html index 8d3bbd4..554a952 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -2,13 +2,26 @@ <p> <b>blabla</b> Test fn....: <b>{{ test_fn }}</b><br> -Test RetFN.: <b>{{ test_return_true }}</b><br> +Test RetFN.: <b>{{ test_return_true }}</b><br><br><br> {% if exists("blog_listing") and length(blog_listing) > 0 %} +<table> ## for entry in blog_listing - {{ loop.index1 }}: {{ entry.content_filename }} -> {{ entry.published }}<br> - <b>{{ entry }}</b><br> - {{ indent(entry.content, 8, false, false) }}<br> + <tr> + <td>{{ loop.index1 }}</td> + <td>{{ entry.content_filename }}</td> + <td>{{ entry.title }}</td> + <td>{{ entry.tags }}</td> + <td>{{ entry.author }}</td> + <td>{{ entry.createDate }}</td> + <td>{{ entry.publishDate }}</td> + <td>{{ entry.published }}</td> + <td> + {{ indent(entry.content, 12, false, false) }} + </td> + <td>{% if loop.is_last == false %}more{% else %}eof{% endif %}<br> + </tr> ## endfor +</table> {% endif %} </p> </body></html> |