stamd

Static Markdown Page Generator
git clone git://git.dimitrijedobrota.com/stamd.git
Log | Files | Refs | README | LICENSE

commit 68acb3567a5817c6f1545d11b6c7d74497d8a0e4
parent 9fa65259932392378f0c05e045f95c009db79b79
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date:   Fri,  3 Jan 2025 21:35:07 +0100

Version 0.3

* Improve naming convention
* Improve decomposition
* Rework category index creation
* Each article is aware of cmd options

Diffstat:
M.clang-tidy | 2+-
MCMakeLists.txt | 2+-
MREADME.md | 6+++++-
Msource/article.cpp | 24+++++++++++-------------
Msource/article.hpp | 12+++++++++---
Msource/indexer.cpp | 30+++++++++++++++---------------
Msource/indexer.hpp | 32+++++++++-----------------------
Msource/main.cpp | 69++++++++++++++++++++++++++++++++++++++++-----------------------------
Asource/options.hpp | 16++++++++++++++++
9 files changed, 107 insertions(+), 86 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy @@ -61,7 +61,7 @@ CheckOptions: - key: 'readability-identifier-naming.AbstractClassCase' value: 'lower_case' - key: 'readability-identifier-naming.ClassCase' - value: 'camelBack' + value: 'CamelCase' - key: 'readability-identifier-naming.ClassConstantCase' value: 'lower_case' - key: 'readability-identifier-naming.ClassMemberCase' diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -4,7 +4,7 @@ include(cmake/prelude.cmake) project( stamd - VERSION 1.2.10 + VERSION 0.3.0 DESCRIPTION "Static Markdown Page Generator" HOMEPAGE_URL "https://git.dimitrijedobrota.com/stamd.git" LANGUAGES CXX diff --git a/README.md b/README.md @@ -38,11 +38,15 @@ See the [BUILDING](BUILDING.md) document. ## Version History +- 0.3 + * Generate sitemap.xml and robots.txt + * Generate rss.xml and atom.txt feeds + * Configurable links + * Category indexes - 0.2 * C++ rewrite * Improved stability * Improve readability - * Generate sitemap.xml and robots.txt - 0.1 * Quick and dirty proof of concept written in C diff --git a/source/article.cpp b/source/article.cpp @@ -13,7 +13,7 @@ namespace stamd { -std::optional<std::string> article::get(const std::string& key) const +std::optional<std::string> Article::get(const std::string& key) const { const auto itr = m_symbols.find(key); if (itr == end(m_symbols)) @@ -24,39 +24,37 @@ std::optional<std::string> article::get(const std::string& key) const return itr->second; } -std::string article::get_filename() const +std::string Article::get_filename() const { return m_symbols.find("filename")->second; } -std::string article::get_date() const +std::string Article::get_date() const { return get("date").value_or("0000-00-00"); } -std::string article::get_title() const +std::string Article::get_title() const { return get("title").value_or(get_filename()); } -std::string article::get_language() const +std::string Article::get_language() const { return get("language").value_or("en"); } -void article::print_nav(std::ostream& ost) +void Article::print_nav(std::ostream& ost, const std::string& base) { using namespace hemplate; // NOLINT - static const char* base = "https://dimitrijedobrota.com/blog"; - ost << html::nav() .add(html::a("&lt;-- back", {{"class", "back"}})) .add(html::a("index", {{"href", base}})) .add(html::a("home --&gt;", {{"href", "/"}})); } -void article::print_categories(std::ostream& ost, +void Article::print_categories(std::ostream& ost, const categories_t& categories) { using namespace hemplate; // NOLINT @@ -74,7 +72,7 @@ void article::print_categories(std::ostream& ost, ost << html::nav(); } -void article::write_header(std::ostream& ost) const +void Article::write_header(std::ostream& ost) const { using namespace hemplate; // NOLINT @@ -134,7 +132,7 @@ void article::write_header(std::ostream& ost) const if (!m_nonav) { ost << html::header(); - print_nav(ost); + print_nav(ost, m_options.base_url + "blog"); ost << html::hr(); ost << html::header(); } @@ -147,7 +145,7 @@ void article::write_header(std::ostream& ost) const if (!m_categories.empty()) print_categories(ost, m_categories); } -void article::write_footer(std::ostream& ost) const +void Article::write_footer(std::ostream& ost) const { using namespace hemplate; // NOLINT @@ -157,7 +155,7 @@ void article::write_footer(std::ostream& ost) const { ost << html::footer(); ost << html::hr(); - print_nav(ost); + print_nav(ost, m_options.base_url + "blog"); ost << html::footer(); } diff --git a/source/article.hpp b/source/article.hpp @@ -5,16 +5,21 @@ #include <string> #include <unordered_map> +#include "options.hpp" + namespace stamd { -class article +class Article { public: using symbols_t = std::unordered_map<std::string, std::string>; using categories_t = std::set<std::string>; - explicit article(std::string filename, categories_t categories = {}) + explicit Article(std::string filename, + options_t options, + categories_t categories = {}) : m_categories(std::move(categories)) + , m_options(std::move(options)) , m_symbols({{"filename", filename}}) { } @@ -43,7 +48,7 @@ public: std::string get_language() const; private: - static void print_nav(std::ostream& ost); + static void print_nav(std::ostream& ost, const std::string& base); static void print_categories(std::ostream& ost, const categories_t& categories); @@ -51,6 +56,7 @@ private: bool m_nonav = false; categories_t m_categories; + options_t m_options; symbols_t m_symbols; }; diff --git a/source/indexer.cpp b/source/indexer.cpp @@ -3,7 +3,6 @@ #include <ctime> #include <format> #include <iterator> -#include <memory> #include <ostream> #include <string> @@ -16,13 +15,17 @@ namespace stamd { -indexer::article_s& indexer::add(const article_s& article) +void Indexer::add(const article_s& article) { m_articles.emplace_back(article); - return m_articles.back(); } -void indexer::sort() +void Indexer::add(categories_t categories) +{ + m_categories.merge(categories); +} + +void Indexer::sort() { std::sort(begin(m_articles), end(m_articles), @@ -82,21 +85,18 @@ std::string to_rfc3339(const std::string& date) return std::format(rfc3339_f, chrono_time); } -void indexer::create_index(std::ostream& ost, - const std::string& name, - const categories_t& categories) +void Indexer::create_index(std::ostream& ost, const std::string& name) { using namespace hemplate; // NOLINT - auto index = std::make_shared<stamd::article>(name, categories); + const Article index(name, m_options, m_categories); - index->write_header(ost); + index.write_header(ost); ost << html::h1(name); ost << html::ul().set("class", "index"); for (const auto& article : m_articles) { if (article->is_hidden()) continue; - if (name != "blog" && !article->get_categories().contains(name)) continue; const auto& filename = article->get_filename(); const auto& title = article->get_title(); @@ -107,10 +107,10 @@ void indexer::create_index(std::ostream& ost, .add(html::a(title).set("href", filename)); }; ost << html::ul(); - index->write_footer(ost); + index.write_footer(ost); } -void indexer::create_atom(std::ostream& ost, const std::string& name) const +void Indexer::create_atom(std::ostream& ost, const std::string& name) const { using namespace hemplate; // NOLINT @@ -148,7 +148,7 @@ void indexer::create_atom(std::ostream& ost, const std::string& name) const ost << atom::feed(); } -void indexer::create_rss(std::ostream& ost, const std::string& name) const +void Indexer::create_rss(std::ostream& ost, const std::string& name) const { using namespace hemplate; // NOLINT @@ -185,7 +185,7 @@ void indexer::create_rss(std::ostream& ost, const std::string& name) const ost << rss::rss(); } -void indexer::create_sitemap(std::ostream& ost) const +void Indexer::create_sitemap(std::ostream& ost) const { using namespace hemplate; // NOLINT @@ -205,7 +205,7 @@ void indexer::create_sitemap(std::ostream& ost) const ost << sitemap::urlset(); } -void indexer::create_robots(std::ostream& ost) const +void Indexer::create_robots(std::ostream& ost) const { static const std::string& base_url = m_options.base_url; diff --git a/source/indexer.hpp b/source/indexer.hpp @@ -5,36 +5,25 @@ #include <vector> #include "article.hpp" +#include "options.hpp" namespace stamd { -class indexer +class Indexer { public: - using article_s = std::shared_ptr<article>; + using article_s = std::shared_ptr<Article>; using article_list = std::vector<article_s>; - using categories_t = article::categories_t; + using categories_t = Article::categories_t; - struct options_t - { - std::string base_url; - std::string author; - std::string email; - std::string description; - std::string summary; - }; - - explicit indexer(options_t options) + explicit Indexer(options_t options) : m_options(std::move(options)) { - if (m_options.base_url.empty() || m_options.base_url.back() != '/') - { - m_options.base_url += '/'; - } } - article_s& add(const article_s& article); + void add(const article_s& article); + void add(categories_t categories); void sort(); @@ -43,14 +32,11 @@ public: void create_atom(std::ostream& ost, const std::string& name) const; void create_rss(std::ostream& ost, const std::string& name) const; - void create_index(std::ostream& ost, - const std::string& name, - const categories_t& categories); - - void create_categories() const; + void create_index(std::ostream& ost, const std::string& name); private: options_t m_options; + categories_t m_categories; article_list m_articles; }; diff --git a/source/main.cpp b/source/main.cpp @@ -12,9 +12,10 @@ #include "article.hpp" #include "indexer.hpp" +#include "options.hpp" #include "utility.hpp" -void preprocess(stamd::article& article, std::istream& ist) +void preprocess(stamd::Article& article, std::istream& ist) { std::string line; std::string key; @@ -51,7 +52,7 @@ struct arguments_t std::vector<std::filesystem::path> files; bool index = false; - stamd::indexer::options_t options; + stamd::options_t options; }; int parse_opt(int key, const char* arg, poafloc::Parser* parser) @@ -83,6 +84,13 @@ int parse_opt(int key, const char* arg, poafloc::Parser* parser) case poafloc::ARG: args->files.emplace_back(arg); break; + case poafloc::END: + if (args->options.base_url.empty() + || args->options.base_url.back() != '/') + { + args->options.base_url += '/'; + } + break; default: break; } @@ -132,28 +140,27 @@ int main(int argc, char* argv[]) return 1; } - using category_map_t = - std::unordered_map<std::string, indexer::article_list>; + using category_map_t = std::unordered_map<std::string, Indexer>; - stamd::indexer::categories_t categories; category_map_t category_map; - indexer indexer(args.options); + Indexer index(args.options); for (const auto& path : args.files) { const std::string filename = path.stem().string() + ".html"; - std::ifstream ifs(path.string()); - auto& article = indexer.add(make_shared<stamd::article>(filename)); + const auto article = make_shared<stamd::Article>(filename, args.options); + index.add(article); + std::ifstream ifs(path.string()); preprocess(*article, ifs); - // filename can change in preprocessing phase - std::ofstream ofs(args.output_dir / article->get_filename()); std::stringstream sst; - sst << ifs.rdbuf(); + // filename can change in preprocessing phase + std::ofstream ofs(args.output_dir / article->get_filename()); + article->write_header(ofs); md_html(sst.str().c_str(), static_cast<MD_SIZE>(sst.str().size()), @@ -163,39 +170,43 @@ int main(int argc, char* argv[]) 0); article->write_footer(ofs); - if (!article->is_hidden()) + if (article->is_hidden()) continue; + + index.add(article->get_categories()); + for (const auto& category : article->get_categories()) { - categories.merge(article->get_categories()); - for (const auto& category : article->get_categories()) - category_map[category].emplace_back(article); + auto [it, _] = category_map.emplace(category, args.options); + it->second.add(article); } } if (!args.index) return 0; - indexer.sort(); + index.sort(); - std::ofstream rss(args.output_dir / "rss.xml"); - indexer.create_rss(rss, "index"); + std::ofstream ofs_rss(args.output_dir / "rss.xml"); + index.create_rss(ofs_rss, "index"); - std::ofstream atom(args.output_dir / "atom.xml"); - indexer.create_atom(atom, "index"); + std::ofstream ofs_atom(args.output_dir / "atom.xml"); + index.create_atom(ofs_atom, "index"); - std::ofstream index(args.output_dir / "index.html"); - indexer.create_index(index, "blog", categories); + std::ofstream ofs_index(args.output_dir / "index.html"); + index.create_index(ofs_index, "blog"); - for (const auto& [category, articles] : category_map) + for (auto& [category_name, category_index] : category_map) { - auto ctgry = category; + auto ctgry = category_name; std::ofstream ost(args.output_dir / (normalize(ctgry) + ".html")); - indexer.create_index(ost, category, {}); + + category_index.sort(); + category_index.create_index(ost, category_name); } - std::ofstream robots(args.output_dir / "robots.txt"); - indexer.create_robots(robots); + std::ofstream ofs_robots(args.output_dir / "robots.txt"); + index.create_robots(ofs_robots); - std::ofstream sitemap(args.output_dir / "sitemap.xml"); - indexer.create_sitemap(sitemap); + std::ofstream ofs_sitemap(args.output_dir / "sitemap.xml"); + index.create_sitemap(ofs_sitemap); return 0; } diff --git a/source/options.hpp b/source/options.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include <string> + +namespace stamd { + +struct options_t +{ + std::string base_url; // url with trailing '/' + std::string author; + std::string email; + std::string description; + std::string summary; +}; + +} // namespace stamd