stamd

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

commit 3390e3be634ba5943cef85e6d83eb81764b4e942
parent a3ec3431bef4146ae13f8a5d5f27d98dd70f7669
author Dimitrije Dobrota < mail@dimitrijedobrota.com >
date Fri, 2 May 2025 15:21:04 +0200

Use Hemplate 4.0

Diffstat:
M .clang-format | ++++++++++ ----------
M .clang-tidy | ++++++++++++++++++++ ---------------------
M CMakeLists.txt | ++ --
M source/article.cpp | ++++++++++++++++++++++++++++++++++++++++++++++++ ----------------------------------
M source/article.hpp | +++++++++++++++ -------------
M source/indexer.cpp | ++++++++++++++++++++++++++++++++++++++++++++++++ ----------------------------------
M source/indexer.hpp | +++++ ----
M source/main.cpp | +++++++++++++++++++++++++++++++++++++++++++++++++ ---------------------------------
M source/options.hpp | ++ -
M source/utility.hpp | +++++++++++++++++++ --------

10 files changed, 448 insertions(+), 321 deletions(-)


diff --git a/ .clang-format b/ .clang-format

@@ -2,12 +2,12 @@

Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignAfterOpenBracket: BlockIndent
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: true
AlignConsecutiveAssignments: false
AlignConsecutiveBitFields: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignEscapedNewlines: DontAlign
AlignOperands: DontAlign
AlignTrailingComments: false
AllowAllArgumentsOnNextLine: true

@@ -17,9 +17,9 @@ AllowShortEnumsOnASingleLine: false

AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
AllowShortLoopsOnASingleLine: true
AllowShortLambdasOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true

@@ -29,16 +29,16 @@ BinPackParameters: false

BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Always
AfterControlStatement: MultiLine
AfterEnum: true
AfterFunction: true
AfterNamespace: false
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: true
AfterExternBlock: true
BeforeCatch: false
BeforeElse: true
BeforeElse: false
BeforeLambdaBody: true
BeforeWhile: false
IndentBraces: false

@@ -54,7 +54,7 @@ BreakConstructorInitializersBeforeComma: true

BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: true
BreakStringLiterals: true
ColumnLimit: 79
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false

diff --git a/ .clang-tidy b/ .clang-tidy

@@ -4,28 +4,23 @@

# modernize-use-nodiscard: too aggressive, attribute is situationally useful
Checks: "*,\
-altera-*,\
-boost*,\
-cppcoreguidelines-avoid-do-while,\
-cppcoreguidelines-pro-bounds-constant-array-index,\
-fuchsia-*,\
-llvmlibc-*,\
-*-braces-around-statements,\
-bugprone-argument-comment,\
-bugprone-easily-swappable-parameters,\
-concurrency-mt-unsafe,\
-cppcoreguidelines-avoid-magic-numbers,\
-fuchsia-multiple-inheritance,\
-hicpp-signed-bitwise,\
-google-readability-todo,\
-llvm-header-guard,\
-llvm-include-order,\
-misc-no-recursion,\
-llvmlibc-*,\
-misc-include-cleaner,\
-misc-non-private-member-variables-in-classes,\
-modernize-use-nodiscard,\
-misc-no-recursion,\
-modernize-use-trailing-return-type,\
-readability-function-cognitive-complexity,\
-readability-magic-numbers
-readability-suspicious-call-argument,\
-*-ranges,\
"
WarningsAsErrors: ''
CheckOptions:
- key: 'misc-include-cleaner.IgnoreHeaders'
value: 'poafloc.*;md4c.*'
- key: 'bugprone-argument-comment.StrictMode'
value: 'true'
# Prefer using enum classes with 2 values for parameters instead of bools

@@ -61,7 +56,7 @@ CheckOptions:

- key: 'readability-identifier-naming.AbstractClassCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassCase'
value: 'CamelCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassMemberCase'

@@ -83,9 +78,9 @@ CheckOptions:

- key: 'readability-identifier-naming.ConstexprVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.EnumCase'
value: 'CamelCase'
value: 'lower_case'
- key: 'readability-identifier-naming.EnumConstantCase'
value: 'CamelCase'
value: 'lower_case'
- key: 'readability-identifier-naming.FunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalConstantCase'

@@ -139,7 +134,7 @@ CheckOptions:

- key: 'readability-identifier-naming.PublicMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ScopedEnumConstantCase'
value: 'CamelCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StaticConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StaticVariableCase'

@@ -152,18 +147,22 @@ CheckOptions:

value: 'CamelCase'
- key: 'readability-identifier-naming.TypeAliasCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypeAliasIgnoredRegexp'
value: 'N'
- key: 'readability-identifier-naming.TypedefCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypeTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TypeTemplateParameterIgnoredRegexp'
value: 'expr-type'
- key: 'readability-identifier-naming.UnionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ValueTemplateParameterCase'
value: 'CamelCase'
value: 'lower_case'
- key: 'readability-identifier-naming.VariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.VirtualMethodCase'
value: 'lower_case'
- key: 'readability-identifier-length.IgnoredVariableNames'
value: "^[abcdxyznm]$"
- key: 'readability-identifier-length.IgnoredParameterNames'
value: "^[abcdxyznm]$"
...

diff --git a/ CMakeLists.txt b/ CMakeLists.txt

@@ -4,7 +4,7 @@ include(cmake/prelude.cmake)


project(
stamd
VERSION 0.3.2
VERSION 0.4.0
DESCRIPTION "Static Markdown Page Generator"
HOMEPAGE_URL "https://git.dimitrijedobrota.com/stamd.git"
LANGUAGES CXX

@@ -17,7 +17,7 @@ include(cmake/variables.cmake)


find_package(md4c CONFIG REQUIRED)
find_package(poafloc 1 CONFIG REQUIRED)
find_package(hemplate 0.2.2 CONFIG REQUIRED)
find_package(hemplate 0.4.0 CONFIG REQUIRED)


# ---- Declare library ----

diff --git a/ source/article.cpp b/ source/article.cpp

@@ -1,5 +1,4 @@

#include <format>
#include <iostream>
#include <iterator>
#include <numeric>
#include <optional>

@@ -12,13 +11,13 @@


#include "utility.hpp"

namespace stamd {
namespace stamd
{

std::optional<std::string> Article::get(const std::string& key) const
{
const auto itr = m_symbols.find(key);
if (itr == end(m_symbols))
{
if (itr == end(m_symbols)) {
// std::cerr << "Warning: getting invalid value for: " << key << std::endl;
return {};
}

@@ -59,128 +58,177 @@ std::string Article::get_keywords() const

{
static const auto concat = [](const categories_t& categories)
{
if (categories.empty()) return std::string();
return std::accumulate(std::next(std::begin(categories)),
std::end(categories),
*categories.begin(),
[](const auto& acc, const auto& str)
{ return acc + ", " + str; });
if (categories.empty()) {
return std::string();
}

return std::accumulate(
std::next(std::begin(categories)),
std::end(categories),
*categories.begin(),
[](const auto& acc, const auto& str)
{
return acc + ", " + str;
}
);
};

return get("keywords").value_or(concat(m_categories));
}

void Article::print_nav(std::ostream& ost, const std::string& base)
{
using namespace hemplate; // NOLINT

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,
const categories_t& categories)
hemplate::element Article::print_nav(const std::string& base)
{
using namespace hemplate; // NOLINT

ost << html::nav().set("class", "categories");
ost << html::h3("Categories: ");
ost << html::p();
for (const auto& category : categories)
{
auto ctgry = category;
normalize(ctgry);
ost << html::a(category, {{"href", std::format("./{}.html", ctgry)}});
}
ost << html::p();
ost << html::nav();
using namespace hemplate::html; // NOLINT

return nav {
a {
{{"class", "back"}},
"&lt;-- back",
},
a {
{{"href", base}},
"index",
},
a {
{{"href", "/"}},
"home --&gt;",
},
};
}

void Article::write_header(std::ostream& ost) const
hemplate::element Article::print_categories(const categories_t& categories)
{
using namespace hemplate; // NOLINT

ost << html::doctype();
ost << html::html().set("lang", get_language());
ost << html::head()
.add(html::title(get_title()))
// Meta tags
.add(html::meta({{"charset", "UTF-8"}}))
.add(html::meta({{"name", "author"}, {"content", get_author()}}))
.add(html::meta(
{{"name", "description"}, {"content", get_desciprtion()}}))
.add(html::meta(
{{"name", "keywords"}, {"content", get_keywords()}}))
.add(html::meta(
{{"content", "width=device-width, initial-scale=1"},
{"name", "viewport"}}))
// Stylesheets
.add(html::link({{"rel", "stylesheet"}, {"type", "text/css"}})
.set("href", "/css/index.css"))
.add(html::link({{"rel", "stylesheet"}, {"type", "text/css"}})
.set("href", "/css/colors.css"))
// Rss feed
.add(html::link({{"rel", "alternate"},
{"type", "application/atom+xml"},
{"title", "RSS feed"},
{"href", "/blog/rss.xml"}}))
// Atom feed
.add(html::link({{"rel", "alternate"},
{"type", "application/atom+xml"},
{"title", "Atom feed"},
{"href", "/blog/atom.xml"}}))
// Icons
.add(html::link({{"rel", "icon"}, {"type", "image/png"}})
.set("sizes", "32x32")
.set("href", "/img/favicon-32x32.png"))
.add(html::link({{"rel", "icon"}, {"type", "image/png"}})
.set("sizes", "16x16")
.set("href", "/img/favicon-16x16.png"));

ost << html::body();
ost << html::input()
.set("type", "checkbox")
.set("id", "theme_switch")
.set("class", "theme_switch");

ost << html::div().set("id", "content");

if (!m_nonav)
{
ost << html::header();
print_nav(ost, m_options.base_url + "blog");
ost << html::hr();
ost << html::header();
}

ost << html::main();
ost << html::label(" ")
.set("for", "theme_switch")
.set("class", "switch_label");

if (!m_categories.empty()) print_categories(ost, m_categories);
using namespace hemplate::html; // NOLINT

return nav {
{{"class", "categories"}},
h3 {"Categories: "},
p {
transform(
categories,
[](const auto& category)
{
auto ctgry = category;
normalize(ctgry);
return a {
{{"href", std::format("./{}.html", ctgry)}},
category,
};
}
),
},
};
}

void Article::write_footer(std::ostream& ost) const
hemplate::element Article::write(const content_t& content) const
{
using namespace hemplate; // NOLINT

ost << html::main();

if (!m_nonav)
{
ost << html::footer();
ost << html::hr();
print_nav(ost, m_options.base_url + "blog");
ost << html::footer();
}

ost << html::div();
ost << html::script(" ").set("src", "/scripts/main.js");
ost << html::body();
ost << html::html();
using namespace hemplate::html; // NOLINT

return element {
doctype {},
html {
{{"lang", get_language()}},
head {
title {get_title()},
},

// Meta tags
meta {
{{"charset", "UTF-8"}},
},
meta {
{{"name", "author"}, {"content", get_author()}},
},
meta {{
{"name", "description"},
{"content", get_desciprtion()},
}},
meta {{
{"name", "keywords"},
{"content", get_keywords()},
}},
meta {
{{"content", "width=device-width, initial-scale=1"},
{"name", "viewport"}}
},

// Stylesheets
link {{
{"rel", "stylesheet"},
{"type", "text/css"},
{"href", "/css/index.css"},
}},

link {{
{"rel", "stylesheet"},
{"type", "text/css"},
{"href", "/css/colors.css"},
}},

// Rss feed
link {{
{"rel", "alternate"},
{"type", "application/atom+xml"},
{"title", "RSS feed"},
{"href", "/blog/rss.xml"},
}},

// Atom feed
link {{
{"rel", "alternate"},
{"type", "application/atom+xml"},
{"title", "Atom feed"},
{"href", "/blog/atom.xml"},
}},

// Icons
link {{
{"rel", "icon"},
{"type", "image/png"},
{"sizes", "32x32"},
{"href", "/img/favicon-32x32.png"},
}},

link {{
{"rel", "icon"},
{"type", "image/png"},
{"sizes", "16x16"},
{"href", "/img/favicon-16x16.png"},
}},
body {
input {{
{"type", "checkbox"},
{"id", "theme_switch"},
{"class", "theme_switch"},
}},
hemplate::html::div {
{{"id", "content"}},
m_nonav ? element{} : [&] {
return header {
print_nav(m_options.base_url + "blog"),
hr{},
};
}(),
main {
label {
{{"for", "theme_switch"}, {"class", "switch_label"},},
},
m_categories.empty() ? element {} : [&]() {
return print_categories(m_categories);
}(),
content(),
},
m_nonav ? element{} : [&] {
return footer {
hr{},
print_nav(m_options.base_url + "blog"),
};
}(),
},
script {{{"set", "/scripts/main.js"}}},
},
},
};
}

} // namespace stamd

diff --git a/ source/article.hpp b/ source/article.hpp

@@ -5,27 +5,30 @@

#include <string>
#include <unordered_map>

#include <hemplate/html.hpp>

#include "options.hpp"

namespace stamd {
namespace stamd
{

class Article
{
public:
using symbols_t = std::unordered_map<std::string, std::string>;
using symbols_t = std::unordered_map<std::string, std::string>;
using categories_t = std::set<std::string>;

explicit Article(std::string filename,
options_t options,
categories_t categories = {})
explicit Article(
std::string filename, options_t options, categories_t categories = {}
)
: m_filename(std::move(filename))
, m_categories(std::move(categories))
, m_options(std::move(options))
{
}

void write_header(std::ostream& ost) const;
void write_footer(std::ostream& ost) const;
using content_t = std::function<hemplate::element()>;
hemplate::element write(const content_t& content) const;

void insert(const std::string& category) { m_categories.emplace(category); }
void insert(const std::string& key, const std::string& value)

@@ -35,8 +38,8 @@ public:


auto get_categories() const { return m_categories; }

void set_hidden(bool state) { m_hidden = state; }
void set_nonav(bool state) { m_nonav = state; }
void set_hidden() { m_hidden = true; }
void set_nonav() { m_nonav = true; }

bool is_hidden() const { return m_hidden; }

@@ -51,12 +54,11 @@ public:

std::string get_keywords() const;

private:
static void print_nav(std::ostream& ost, const std::string& base);
static void print_categories(std::ostream& ost,
const categories_t& categories);
static hemplate::element print_nav(const std::string& base);
static hemplate::element print_categories(const categories_t& categories);

bool m_hidden = false;
bool m_nonav = false;
bool m_nonav = false;

std::string m_filename;
categories_t m_categories;

diff --git a/ source/indexer.cpp b/ source/indexer.cpp

@@ -9,12 +9,28 @@


#include "indexer.hpp"

#include <hemplate/attribute.hpp>
#include <hemplate/classes.hpp>
#include <hemplate/atom.hpp>
#include <hemplate/html.hpp>
#include <hemplate/rss.hpp>
#include <hemplate/sitemap.hpp>

#include "article.hpp"

namespace stamd {
namespace
{

int64_t parse_time(const std::string& date)
{
std::tm tms = {};
std::stringstream stream(date);
stream >> std::get_time(&tms, "%Y-%m-%d");
return std::mktime(&tms);
}

} // namespace

namespace stamd
{

void Indexer::add(const article_s& article)
{

@@ -28,135 +44,167 @@ void Indexer::add(categories_t categories)


void Indexer::sort()
{
std::sort(begin(m_articles),
end(m_articles),
[](const auto& lft, const auto& rht)
{ return lft->get_date() > rht->get_date(); });
std::sort(
begin(m_articles),
end(m_articles),
[](const auto& lft, const auto& rht)
{
return lft->get_date() > rht->get_date();
}
);
}

int64_t parse_time(const std::string& date)
void Indexer::create_index(std::ostream& ost, const std::string& doc_title)
{
std::tm tms = {};
std::stringstream stream(date);
stream >> std::get_time(&tms, "%Y-%m-%d");
return std::mktime(&tms);
using namespace hemplate::html; // NOLINT

const Article index(doc_title, m_options, m_categories);

ost << index.write(
[&]
{
return element {
h1 {doc_title},
ul {
{{"class", "index"}},
transform(
m_articles,
[](const auto& article) -> element
{
if (article->is_hidden()) {
return {};
}

return li {
span {article->get_date(), " -&nbsp"},
a {
{{"href", article->get_filename()}},
article->get_title(),
},
};
}
),
},
};
}
);
}

void Indexer::create_index(std::ostream& ost, const std::string& name)
void Indexer::create_atom(std::ostream& ost, const std::string& doc_title) const
{
using namespace hemplate; // NOLINT

const Article index(name, m_options, m_categories);
using namespace hemplate::atom; // NOLINT
using hemplate::atom::link;

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;

const auto& filename = article->get_filename();
const auto& title = article->get_title();
const auto& date = article->get_date();

ost << html::li()
.add(html::span(date + " -&nbsp"))
.add(html::a(title).set("href", filename));
const std::string& base_url = m_options.base_url;
const std::string& author_name = m_options.author;

ost << element {
xml {},
feed {},
title {doc_title},
id {base_url},
updated {format_time_now()},
author {name {author_name}},
link {{
{"href", base_url + "/atom.xml"},
{"rel", "self"},
}},
link {{
{"href", base_url + "blog/atom.xml"},
{"rel", "alternate"},
{"type", "text/html"},
}},
feed {
transform(
m_articles,
[&](const auto& article)
{
const auto filename = article->get_filename();
const auto art_title = article->get_title();
const auto date = article->get_date();
const auto art_summary =
article->get("summary").value_or(m_options.summary);

return entry {
title {art_title},
id {base_url + filename},
link {{{"href", base_url + filename}}},
updated {format_time(parse_time((date)))},
summary {art_summary},
};
}
),
},
};
ost << html::ul();
index.write_footer(ost);
}

void Indexer::create_atom(std::ostream& ost, const std::string& name) const
void Indexer::create_rss(std::ostream& ost, const std::string& doc_title) const
{
using namespace hemplate; // NOLINT
using namespace hemplate::rss; // NOLINT
using hemplate::rss::link;

const std::string& base_url = m_options.base_url;

ost << xml();
ost << atom::feed();
ost << atom::title(name);
ost << atom::id(base_url);
ost << atom::updated(atom::format_time_now());
ost << atom::author().add(atom::name(name));
ost << atom::link(" ",
{{"rel", "self"}, {"href", base_url + "blog/atom.xml"}});
ost << atom::link(
" ", {{"href", base_url}, {"rel", "alternate"}, {"type", "text/html"}});

for (const auto& article : m_articles)
{
const auto filename = article->get_filename();
const auto title = article->get_title();
const auto date = article->get_date();
const auto summary = article->get("summary").value_or(m_options.summary);

ost << atom::entry()
.add(atom::title(title))
.add(atom::id(base_url + filename))
.add(atom::link(" ").set("href", base_url + filename))
.add(atom::updated(atom::format_time(parse_time((date)))))
.add(atom::summary(summary));
}

ost << atom::feed();
}

void Indexer::create_rss(std::ostream& ost, const std::string& name) const
{
using namespace hemplate; // NOLINT

const std::string& base_url = m_options.base_url;
const std::string& description = m_options.description;

ost << xml();
ost << rss::rss();
ost << rss::channel();

ost << rss::title(name);
ost << rss::link(base_url);
ost << rss::description(description);
ost << rss::generator("stamd");
ost << rss::language("en-us");
ost << rss::atomLink().set("href", base_url + "blog/rss.xml");

for (const auto& article : m_articles)
{
const auto filename = article->get_filename();
const auto date = article->get_date();
const auto author = article->get("author").value_or(m_options.author);
const auto email = article->get("email").value_or(m_options.email);

ost << rss::item()
.add(rss::title(filename))
.add(rss::link(base_url + filename))
.add(rss::guid(base_url + filename))
.add(rss::pubDate(rss::format_time(parse_time(date))))
.add(rss::author(std::format("{} ({})", email, author)));
}

ost << rss::channel();
ost << rss::rss();
const std::string& desc = m_options.description;

ost << element {
xml {},
rss {
channel {
title {doc_title},
link {base_url},
description {desc},
generator {"stamd"},
language {"en-us"},
atomLink {base_url + "blog/rss.xml"},
transform(
m_articles,
[&](const auto& article)
{
const auto filename = article->get_filename();
const auto date = article->get_date();
const auto author_name =
article->get("author").value_or(m_options.author);
const auto email =
article->get("email").value_or(m_options.email);

return item {
title {filename},
link {base_url + filename},
guid {base_url + filename},
pubDate {format_time(parse_time(date))},
author {std::format("{} ({})", email, author_name)},
};
}
),
},
},
};
}

void Indexer::create_sitemap(std::ostream& ost) const
{
using namespace hemplate; // NOLINT
using namespace hemplate::sitemap; // NOLINT

static const std::string& base_url = m_options.base_url;

ost << xml();
ost << sitemap::urlset();
for (const auto& article : m_articles)
{
const auto& filename = article->get_filename();
const auto& date = article->get_date();

ost << sitemap::url()
.add(sitemap::loc(base_url + filename))
.add(sitemap::lastmod(date));
}
ost << sitemap::urlset();
ost << element {
xml {},
urlset {
transform(
m_articles,
[&](const auto& article)
{
const auto& filename = article->get_filename();
const auto& date = article->get_date();

return url {
loc {base_url + filename},
lastmod {date},
};
}
),
}
};
}

void Indexer::create_robots(std::ostream& ost) const

diff --git a/ source/indexer.hpp b/ source/indexer.hpp

@@ -7,7 +7,8 @@

#include "article.hpp"
#include "options.hpp"

namespace stamd {
namespace stamd
{

class Indexer
{

@@ -30,9 +31,9 @@ public:

void create_robots(std::ostream& ost) const;
void create_sitemap(std::ostream& ost) const;

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);
void create_atom(std::ostream& ost, const std::string& doc_title) const;
void create_rss(std::ostream& ost, const std::string& doc_title) const;
void create_index(std::ostream& ost, const std::string& doc_title);

private:
options_t m_options;

diff --git a/ source/main.cpp b/ source/main.cpp

@@ -15,16 +15,19 @@

#include "options.hpp"
#include "utility.hpp"

namespace
{

void preprocess(stamd::Article& article, std::istream& ist)
{
std::string line;
std::string key;
std::string value;

while (std::getline(ist, line))
{
if (line.empty()) break;
if (line[0] != '@') break;
while (std::getline(ist, line)) {
if (line.empty() && line[0] != '@') {
break;
}

{
std::istringstream iss(line.substr(1));

@@ -35,13 +38,17 @@ void preprocess(stamd::Article& article, std::istream& ist)

trim(value);
}

if (key == "hidden") article.set_hidden(true);
else if (key == "nonav") article.set_nonav(true);
else if (key != "categories") article.insert(key, value);
else
{
if (key == "hidden") {
article.set_hidden();
} else if (key == "nonav") {
article.set_nonav();
} else if (key != "categories") {
article.insert(key, value);
} else {
std::istringstream iss(value);
while (std::getline(iss, value, ',')) article.insert(trim(value));
while (std::getline(iss, value, ',')) {
article.insert(trim(value));
}
}
}
}

@@ -58,8 +65,7 @@ struct arguments_t

int parse_opt(int key, const char* arg, poafloc::Parser* parser)
{
auto* args = static_cast<arguments_t*>(parser->input());
switch (key)
{
switch (key) {
case 'o':
args->output_dir = arg;
break;

@@ -122,11 +128,7 @@ static const poafloc::arg_t arg {

};
// NOLINTEND

void process_output(const MD_CHAR* str, MD_SIZE size, void* data)
{
std::ofstream& ofs = *static_cast<std::ofstream*>(data);
ofs << std::string(str, size);
}
} // namespace

int main(int argc, char* argv[])
{

@@ -134,8 +136,7 @@ int main(int argc, char* argv[])


arguments_t args;

if (poafloc::parse(&arg, argc, argv, 0, &args) != 0)
{
if (poafloc::parse(&arg, argc, argv, 0, &args) != 0) {
std::cerr << "There was an error while parsing arguments";
return 1;
}

@@ -145,8 +146,7 @@ int main(int argc, char* argv[])

category_map_t category_map;
Indexer index(args.options);

for (const auto& path : args.files)
{
for (const auto& path : args.files) {
const std::string filename = path.stem().string() + ".html";

const auto article = make_shared<stamd::Article>(filename, args.options);

@@ -161,26 +161,44 @@ int main(int argc, char* argv[])

// 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()),
process_output,
&ofs,
MD_DIALECT_GITHUB,
0);
article->write_footer(ofs);

if (article->is_hidden()) continue;
ofs << article->write(
[&]() -> hemplate::element
{
std::string html;

static auto process_output =
[](const MD_CHAR* str, MD_SIZE size, void* data)
{
std::string& buffer = *static_cast<std::string*>(data);
buffer += std::string(str, size);
};

md_html(
sst.str().c_str(),
static_cast<MD_SIZE>(sst.str().size()),
process_output,
&html,
MD_DIALECT_GITHUB,
0
);
return html;
}
);

if (article->is_hidden()) {
continue;
}

index.add(article->get_categories());
for (const auto& category : article->get_categories())
{
for (const auto& category : article->get_categories()) {
auto [it, _] = category_map.emplace(category, args.options);
it->second.add(article);
}
}

if (!args.index) return 0;
if (!args.index) {
return 0;
}

index.sort();

@@ -193,8 +211,7 @@ int main(int argc, char* argv[])

std::ofstream ofs_index(args.output_dir / "index.html");
index.create_index(ofs_index, "blog");

for (auto& [category_name, category_index] : category_map)
{
for (auto& [category_name, category_index] : category_map) {
auto ctgry = category_name;
std::ofstream ost(args.output_dir / (normalize(ctgry) + ".html"));

diff --git a/ source/options.hpp b/ source/options.hpp

@@ -2,7 +2,8 @@


#include <string>

namespace stamd {
namespace stamd
{

struct options_t
{

diff --git a/ source/utility.hpp b/ source/utility.hpp

@@ -7,20 +7,31 @@ inline std::string& ltrim(std::string& str)

{
str.erase(
str.begin(),
std::find_if(str.begin(),
str.end(),
[](unsigned char chr) { return std::isspace(chr) == 0; }));
std::find_if(
str.begin(),
str.end(),
[](unsigned char chr)
{
return std::isspace(chr) == 0;
}
)
);
return str;
}

inline std::string& rtrim(std::string& str)
{
str.erase(
std::find_if(str.rbegin(),
str.rend(),
[](unsigned char chr) { return std::isspace(chr) == 0; })
.base(),
str.end());
std::find_if(
str.rbegin(),
str.rend(),
[](unsigned char chr)
{
return std::isspace(chr) == 0;
}
).base(),
str.end()
);
return str;
}