stamd

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

commit68acb3567a5817c6f1545d11b6c7d74497d8a0e4
parent9fa65259932392378f0c05e045f95c009db79b79
authorDimitrije Dobrota <mail@dimitrijedobrota.com>
dateFri, 3 Jan 2025 20: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|+-
MCMakeLists.txt|+-
MREADME.md|+++++-
Msource/article.cpp|+++++++++++-------------
Msource/article.hpp|+++++++++---
Msource/indexer.cpp|+++++++++++++++---------------
Msource/indexer.hpp|+++++++++-----------------------
Msource/main.cpp|++++++++++++++++++++++++++++++++++++++++-----------------------------
Asource/options.hpp|++++++++++++++++

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