You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1015 lines
33 KiB
1015 lines
33 KiB
|
|
// ./stgen3 src dest 7.98s user 0.32s system 102% cpu 8.123 total |
|
#include <map> |
|
#include <ranges> |
|
#include <stack> |
|
#include <unordered_map> |
|
#include <vector> |
|
#include <string> |
|
#include <filesystem> |
|
#include <spdlog/spdlog.h> |
|
#include <tinyxml2.h> |
|
#include <set> |
|
|
|
#include "util.h" |
|
#include "markdown.h" |
|
|
|
namespace fs = std::filesystem; |
|
|
|
const std::string TEMPLATE_CODE_START = "{{"; |
|
const std::string TEMPLATE_CODE_END = "}}"; |
|
|
|
|
|
enum job_type { |
|
COPY_FILE = 1, |
|
MARKDOWN = 1 << 1, |
|
TEMPLATE = 1 << 2, |
|
DELETE_FILE = 1 << 3, |
|
MAKE_DIR = 1 << 4, |
|
WRITE_ARTICLE = 1 << 5, |
|
POSTPROCESS_HTML = 1 << 6 |
|
}; |
|
|
|
|
|
struct blog_item { |
|
job_type type; |
|
fs::path src; |
|
time_t post_date; |
|
std::unordered_map<std::string, std::string> properties; |
|
}; |
|
|
|
|
|
class substitution_plugin { |
|
public: |
|
|
|
std::vector<std::string> get_arguments(const std::string &invocation, int numargs); |
|
|
|
/* Must be globally unique: invocation name in the template */ |
|
virtual std::string hook_name() = 0; |
|
|
|
|
|
/* Must return lengh of replaced text */ |
|
virtual int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) = 0; |
|
|
|
virtual ~substitution_plugin() = default; |
|
}; |
|
|
|
class s2_substitution_plugin : public substitution_plugin { |
|
public: |
|
|
|
int |
|
perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override |
|
{ |
|
return 0; |
|
} |
|
|
|
/* Must return lengh of replaced text */ |
|
virtual int |
|
perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties, |
|
const std::unordered_map<fs::path, blog_item, pathHash> & pages) = 0; |
|
}; |
|
|
|
/* |
|
* Returns vector of arguments from string of the form " cmdname:arg1 :arg2 ". |
|
* Trims whitespace from around arguments so the result is |
|
* {"cmdname", "arg1", "arg2"} |
|
* |
|
* @numargs: the number of argument to get after the command name: in this case |
|
* 2. This is so the last argument can contain ':', since it just reads until |
|
* the end. |
|
* |
|
* if it cannot find numargs arguments, it finds as many arguments as it can |
|
* and retuns them, so it is neccessary to check the size of the returned |
|
* vector before use. |
|
*/ |
|
std::vector<std::string> substitution_plugin::get_arguments(const std::string &invocation, int numargs) { |
|
std::vector<std::string> args; |
|
|
|
int next = invocation.find(":"); |
|
|
|
if (next == std::string::npos) { |
|
return {}; |
|
} |
|
|
|
args.push_back(invocation.substr(0, next)); |
|
int last = next + 1; |
|
|
|
|
|
for (int i = 1; i < numargs; i++) { |
|
next = invocation.find(":", last); |
|
if (next == std::string::npos) { |
|
spdlog::warn("get_arguments: not enough arguments"); |
|
break; |
|
} |
|
|
|
args.push_back(trim_whitespace(invocation.substr(last, next - last))); |
|
last = next + 1; |
|
} |
|
|
|
args.push_back(invocation.substr(last)); |
|
|
|
return args; |
|
|
|
} |
|
|
|
class file_transclude_plugin : public substitution_plugin { |
|
public: |
|
std::string hook_name() override {return "include";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
auto args = get_arguments(invocation, 1); |
|
std::string filename {args.at(1)}; |
|
fs::path path; |
|
|
|
std::string subst_file; |
|
|
|
if (properties.count("transclude base") && |
|
fs::exists(path = fs::path(properties.at("transclude_base")).append(filename))) { |
|
subst_file = read_file(path); |
|
} else if (fs::exists(path = fs::path(properties.at("current_directory")).append(filename))) { |
|
subst_file = read_file(path); |
|
} else if(fs::exists(path = fs::path(properties.at("source_root")).append(filename))) { |
|
subst_file= read_file(path); |
|
} else { |
|
return 0; |
|
} |
|
|
|
file_text.replace(start, end - start, subst_file); |
|
|
|
return subst_file.length(); |
|
} |
|
|
|
}; |
|
|
|
|
|
class mmd_snippet_transclude_plugin : public substitution_plugin { |
|
public: |
|
std::string hook_name() override {return "md";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
auto args = get_arguments(invocation, 1); |
|
fs::path path; |
|
|
|
mmd::markdown_parser p {}; |
|
|
|
if (args.size() == 2) { |
|
std::string subst_text = p.parse_to_html(args.at(1)); |
|
file_text.replace(start, end - start, subst_text); |
|
return subst_text.length(); |
|
} |
|
|
|
return 0; |
|
} |
|
}; |
|
|
|
class variable_transclude_plugin final : public substitution_plugin { |
|
public: |
|
std::string hook_name() override {return "";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
auto args = get_arguments(invocation, 1); |
|
|
|
if (args.size() != 2) { |
|
return 0; |
|
} |
|
|
|
std::string name {args.at(1)}; |
|
|
|
std::string subst_text = TEMPLATE_CODE_START + invocation + TEMPLATE_CODE_END; |
|
int rval = 0; |
|
|
|
if (properties.count(name)) { |
|
subst_text = properties.at(name); |
|
rval = subst_text.length(); |
|
} |
|
|
|
file_text.replace(start, end - start, subst_text); |
|
return rval; |
|
} |
|
}; |
|
|
|
|
|
class ifdef_plugin : public substitution_plugin { |
|
|
|
protected: |
|
struct do_replace { |
|
std::string::size_type first; |
|
std::string::size_type second; |
|
std::string body; |
|
bool name_exists; |
|
bool error; |
|
}; |
|
|
|
do_replace should_substitute(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) { |
|
|
|
do_replace val {}; |
|
val.first = invocation.find(":") + 1; |
|
val.second = invocation.find(":", val.first); |
|
|
|
auto args = get_arguments(invocation, 2); |
|
|
|
if (args.size() != 3) { |
|
val.error = true; |
|
return val; |
|
} |
|
|
|
std::string name {args.at(1)}; |
|
|
|
// std::string name = invocation.substr(val.first, val.second - val.first); |
|
|
|
std::string subst_text = ""; |
|
val.body = args.at(2); // invocation.substr(val.second + 1); |
|
|
|
if (properties.count(name)) { |
|
val.name_exists = true; |
|
} |
|
|
|
return val; |
|
} |
|
|
|
|
|
public: |
|
std::string hook_name() override {return "ifdef";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
do_replace val = should_substitute(start, end, invocation, file_text, properties); |
|
|
|
std::string subst_text = ""; |
|
|
|
if (val.error) { |
|
spdlog::warn("Bad ifdef syntax."); |
|
} else { |
|
if (val.name_exists) { |
|
subst_text = val.body; |
|
} |
|
} |
|
|
|
file_text.replace(start, end - start, subst_text); |
|
|
|
return subst_text.length(); |
|
} |
|
}; |
|
|
|
|
|
class comment_plugin : public substitution_plugin { |
|
public: |
|
std::string hook_name() override {return "#";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
|
|
std::string subst_text = "\n"; |
|
|
|
file_text.replace(start, end - start, subst_text); |
|
|
|
return subst_text.length(); // success substituting 0 |
|
|
|
} |
|
}; |
|
|
|
|
|
class ifndef_plugin : public ifdef_plugin { |
|
public: |
|
std::string hook_name() override {return "ifndef";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
do_replace val = should_substitute(start, end, invocation, file_text, properties); |
|
|
|
std::string subst_text = ""; |
|
|
|
if (val.error) { |
|
spdlog::warn("Bad ifdef syntax."); |
|
} else { |
|
if (!val.name_exists) { |
|
subst_text = val.body; |
|
} |
|
} |
|
|
|
file_text.replace(start, end - start, subst_text); |
|
|
|
return subst_text.length(); |
|
|
|
} |
|
}; |
|
|
|
|
|
class file_index_plugin : public substitution_plugin { |
|
public: |
|
|
|
struct post_entry { |
|
fs::path path; |
|
time_t date; |
|
std::string title; |
|
std::multimap<time_t, post_entry> sub_directories; |
|
}; |
|
|
|
std::set<std::string> exclude_filenames {}; |
|
|
|
mmd::markdown_parser parser {}; |
|
|
|
|
|
file_index_plugin() {} |
|
file_index_plugin(std::set<std::string> exclude) : exclude_filenames(exclude) {} |
|
|
|
/** |
|
* Recursively get a sorted unordered_map of directories. |
|
* |
|
* Directory must exist. Does not handle any errors. |
|
* |
|
*/ |
|
std::multimap<time_t, post_entry> get_directory_list(const fs::path &dir, |
|
const std::unordered_map<std::string, std::string> &properties) { |
|
|
|
// todo support custom depth |
|
|
|
std::multimap<time_t, post_entry> entries; |
|
for (auto &p : fs::directory_iterator(dir)) { |
|
|
|
// skip conditions |
|
if (file_ext(p.path()) == "template") { |
|
// skip templates |
|
continue; |
|
} |
|
if (file_ext(p.path()) == "draft") { |
|
// skip drafts |
|
continue; |
|
} |
|
|
|
if (p.path().filename().string().at(0) == '.') { |
|
// skip hidden files |
|
continue; |
|
} |
|
|
|
if (exclude_filenames.contains(p.path().filename())) { |
|
continue; |
|
} |
|
|
|
if (p.is_directory()) { |
|
auto m = get_directory_list(p, properties); |
|
std::string title = p.path().filename(); |
|
title += "/"; |
|
|
|
if (!m.empty()) { |
|
post_entry smallest_entry = m.begin()->second; |
|
entries.insert({smallest_entry.date, {p, smallest_entry.date, title, m}}); |
|
} else { |
|
time_t t = to_time_t(fs::last_write_time(p)); |
|
entries.insert({t, {p, t, title}}); |
|
} |
|
|
|
} else { |
|
std::string file = read_file(fs::path(p)); |
|
|
|
if (parser.get_property(file, "noindex")) { |
|
continue; |
|
} |
|
|
|
std::optional<std::string> date_time = parser.get_property(file, "date"); |
|
std::optional<std::string> article_title = parser.get_property(file, "title"); |
|
|
|
struct post_entry post; |
|
|
|
// add date |
|
if (date_time) { |
|
date::sys_seconds timepoint; |
|
std::chrono::file_clock f; |
|
std::istringstream in {*date_time}; |
|
in >> date::parse(properties.at("date-in-format"), timepoint); |
|
auto t = std::chrono::system_clock::to_time_t(timepoint); |
|
post = {p, t}; |
|
} else { |
|
fs::file_time_type t = fs::last_write_time(p); |
|
auto syst = to_time_t(t); |
|
post = {p, syst}; |
|
} |
|
|
|
// add title |
|
if (article_title) { |
|
post.title = *article_title; |
|
} else { |
|
post.title = post.path.filename(); |
|
} |
|
|
|
entries.insert({post.date, post}); |
|
} |
|
} |
|
|
|
return entries; |
|
} |
|
|
|
public: |
|
|
|
std::string hook_name() override {return "postlist";}; |
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties) override { |
|
|
|
int rval = 0; |
|
|
|
auto args = get_arguments(invocation, 1); |
|
std::string relpath = args.at(1); |
|
|
|
fs::path path; |
|
if (relpath.at(0) == '/') { |
|
// path relative to site root |
|
path = fs::path(properties.at("source_root")).append(relpath.substr(1)); |
|
} else { |
|
// path relative to current directory |
|
path = fs::path(properties.at("current_directory")).append(relpath); |
|
} |
|
|
|
std::string markdown_index = ""; |
|
|
|
if (fs::exists(path)) { |
|
auto m = get_directory_list(path,properties); |
|
|
|
|
|
for (auto entry = m.rbegin(); entry != m.rend(); entry++) { |
|
std::ostringstream ss; |
|
if (entry->second.date != time_t {0}) { |
|
date::to_stream(ss, properties.at("date-out-format").c_str(), std::chrono::system_clock::from_time_t(entry->second.date)); |
|
} |
|
ss << "\n: "; |
|
std::string url = stgen::compute_url(entry->second.path, properties); |
|
std::string title = entry->second.title; |
|
|
|
if (fs::is_directory(entry->second.path)) { |
|
ss << "[" << title << "](" << url << ")\n\n"; |
|
} else { |
|
ss << "[" << title << "](" << url << ")\n\n"; |
|
} |
|
|
|
markdown_index += ss.str(); |
|
} |
|
} else { |
|
spdlog::warn("build index: path not exist: {}", path.string()); |
|
} |
|
|
|
rval = markdown_index.length(); |
|
if (rval) { |
|
file_text.replace(start, end - start, markdown_index); |
|
} |
|
|
|
return rval; |
|
} |
|
}; |
|
|
|
|
|
class feed_builder { |
|
public: |
|
enum feed_format { |
|
ATOM, |
|
RSS |
|
}; |
|
|
|
private: |
|
feed_format type = ATOM; |
|
tinyxml2::XMLDocument feed; |
|
tinyxml2::XMLElement *e_feed; |
|
|
|
public: |
|
|
|
feed_builder(const std::string &url, const std::string &title, |
|
const std::vector<std::string> &author_names, |
|
feed_format type) : type(type) { |
|
using namespace tinyxml2; |
|
|
|
auto decl = feed.NewDeclaration(); |
|
feed.InsertFirstChild(decl); |
|
|
|
if (ATOM == type) { |
|
e_feed = feed.NewElement("feed"); |
|
e_feed->SetAttribute("xmlns", "http://www.w3.org/2005/Atom"); |
|
feed.InsertEndChild(e_feed); |
|
} else if (RSS == type) { |
|
auto i_feed = feed.NewElement("rss"); |
|
i_feed->SetAttribute("version", 2.0); |
|
feed.InsertEndChild(i_feed); |
|
e_feed = i_feed->InsertNewChildElement("channel"); |
|
} |
|
|
|
auto e_title = e_feed->InsertNewChildElement("title"); |
|
e_title->SetText(title.c_str()); |
|
|
|
auto e_link = e_feed->InsertNewChildElement("link"); |
|
e_link->SetAttribute("rel", "self"); |
|
|
|
std::string uurl = url; |
|
e_link->SetAttribute("href", uurl.c_str()); |
|
|
|
if (ATOM == type) { |
|
auto e_updated = e_feed->InsertNewChildElement("updated"); |
|
/* shouldn't really be generation time: should be last modification time |
|
* - might be nice to check the diff against the old feed, or only |
|
* update when the site has been changed. |
|
*/ |
|
auto timepoint = std::chrono::system_clock::now(); |
|
std::ostringstream ss; |
|
date::to_stream(ss, "%FT%TZ", timepoint); |
|
e_updated->SetText(ss.str().c_str()); |
|
|
|
|
|
auto e_author = e_feed->InsertNewChildElement("author"); |
|
for (auto author: author_names) { |
|
auto e_author_name = e_author->InsertNewChildElement("name"); |
|
e_author_name->SetText(author.c_str()); |
|
} |
|
|
|
auto e_feed_id = e_feed->InsertNewChildElement("id"); |
|
e_feed_id->SetText(uurl.c_str()); |
|
} else if (RSS == type) { |
|
auto e_updated = e_feed->InsertNewChildElement("pubDate"); |
|
/* shouldn't really be generation time: should be last modification time |
|
* - might be nice to check the diff against the old feed, or only |
|
* update when the site has been changed. |
|
*/ |
|
auto timepoint = std::chrono::system_clock::now(); |
|
std::ostringstream ss; |
|
date::to_stream(ss, "%a, %d %b %Y %T %Z", timepoint); |
|
e_updated->SetText(ss.str().c_str()); |
|
|
|
auto e_feed_id = e_feed->InsertNewChildElement("guid"); |
|
e_feed_id->SetText(uurl.c_str()); |
|
|
|
auto e_descr = e_feed->InsertNewChildElement("description"); |
|
e_descr->SetText(title.c_str()); |
|
} |
|
} |
|
|
|
tinyxml2::XMLElement *add_article(std::string const &url, std::string const &title, time_t updated) { |
|
|
|
tinyxml2::XMLElement * article; |
|
|
|
if (ATOM == type) { |
|
article = e_feed->InsertNewChildElement("entry"); |
|
|
|
auto id = article->InsertNewChildElement("id"); |
|
id->SetText(url.c_str()); |
|
|
|
auto a_title = article->InsertNewChildElement("title"); |
|
a_title->SetText(title.c_str()); |
|
|
|
auto a_updated = article->InsertNewChildElement("updated"); |
|
|
|
auto timepoint = std::chrono::system_clock::from_time_t(updated); |
|
std::ostringstream ss; |
|
date::to_stream(ss, "%FT%TZ", timepoint); |
|
|
|
a_updated->SetText(ss.str().c_str()); |
|
|
|
auto link = article->InsertNewChildElement("link"); |
|
link->SetAttribute("rel", "alternate"); |
|
link->SetAttribute("href", url.c_str()); |
|
} else if (RSS == type) { |
|
article = e_feed->InsertNewChildElement("item"); |
|
|
|
auto a_title = article->InsertNewChildElement("title"); |
|
a_title->SetText(title.c_str()); |
|
|
|
|
|
auto link = article->InsertNewChildElement("link"); |
|
link->SetAttribute("rel", "alternate"); |
|
link->SetAttribute("href", url.c_str()); |
|
} |
|
|
|
return article; |
|
} |
|
|
|
|
|
tinyxml2::XMLElement *add_article(std::string const &url, std::string const &title, time_t updated, time_t published) { |
|
auto article = add_article(url, title, updated); |
|
std::string key; |
|
|
|
if (ATOM == type) { |
|
key = "published"; |
|
auto timepoint = std::chrono::system_clock::from_time_t(published); |
|
std::ostringstream ss; |
|
date::to_stream(ss, "%FT%TZ", timepoint); |
|
auto a_published = article->InsertNewChildElement(key.c_str()); |
|
a_published->SetText(ss.str().c_str()); |
|
} else if (RSS == type) { |
|
key = "pubDate"; |
|
auto timepoint = std::chrono::system_clock::from_time_t(published); |
|
std::ostringstream ss; |
|
date::to_stream(ss, "%a, %d %b %Y %T %Z", timepoint); |
|
auto a_published = article->InsertNewChildElement(key.c_str()); |
|
a_published->SetText(ss.str().c_str()); |
|
} |
|
|
|
|
|
return article; |
|
} |
|
|
|
std::string str() { |
|
tinyxml2::XMLPrinter p; |
|
feed.Print(&p); |
|
return std::string {p.CStr()}; |
|
} |
|
|
|
tinyxml2::XMLElement *add_article(std::string const &url, std::string const &title, const std::string &category, time_t updated, time_t published) { |
|
auto article = add_article(url, title, updated, published); |
|
auto e_category = article->InsertNewChildElement("category"); |
|
if (ATOM == type) { |
|
e_category->SetAttribute("term", category.c_str()); |
|
} else if (RSS == type) { |
|
e_category->SetText(category.c_str()); |
|
} |
|
|
|
return article; |
|
} |
|
|
|
tinyxml2::XMLElement *add_article(std::string const &url, std::string const &title, const std::string &category, time_t updated, time_t published, std::string content) { |
|
auto article = add_article(url, title, category, updated, published); |
|
if (ATOM == type) { |
|
auto e_content = article->InsertNewChildElement("content"); |
|
e_content->SetText(content.c_str()); |
|
e_content->SetAttribute("type", "html"); |
|
} else if (RSS == type) { |
|
auto e_content = article->InsertNewChildElement("description"); |
|
e_content->SetText(content.c_str()); |
|
e_content->SetAttribute("type", "html"); |
|
} |
|
|
|
return article; |
|
} |
|
}; |
|
|
|
|
|
std::multimap<time_t, blog_item> get_sorted_post_list(const std::string &cs_directories, |
|
const std::unordered_map<std::string, std::string> &properties, const std::unordered_map<fs::path, blog_item, pathHash> & pages) { |
|
|
|
std::vector<fs::path> paths; |
|
|
|
std::string::size_type first = 0; |
|
auto relpath = cs_directories; |
|
std::string::size_type next = relpath.find(","); |
|
|
|
fs::path path; |
|
do { |
|
if (next == std::string::npos) |
|
next = relpath.length(); |
|
|
|
std::string spath = relpath.substr(first, next - first); |
|
|
|
if (spath.at(0) == '/') { |
|
// path relative to site root |
|
path = fs::canonical(fs::path(properties.at("source_root")).append(spath.substr(1))); |
|
} else { |
|
// path relative to current directory |
|
path = fs::canonical(fs::path(properties.at("current_directory")).append(spath)); |
|
} |
|
|
|
paths.push_back(path); |
|
|
|
first = next + 1; |
|
next = relpath.find(",", first); |
|
} while (first < relpath.length()); |
|
|
|
std::multimap<time_t, blog_item> feed_items; |
|
|
|
|
|
|
|
for (std::string dir: paths) { |
|
for (auto &page: pages) { |
|
if (!(page.second.type & (job_type::TEMPLATE | job_type::MARKDOWN))) { |
|
continue; |
|
} |
|
if (page.first.filename() == "index.html" || file_ext(page.first.filename()) == "xml") { |
|
continue; |
|
} |
|
if (page.second.src.string() == properties.at("current_file")) { |
|
continue; |
|
} |
|
if (page.second.src.string().find(dir) != std::string::npos) { |
|
// this is a page to add |
|
feed_items.insert({page.second.post_date, page.second}); |
|
} |
|
} |
|
} |
|
|
|
return feed_items; |
|
} |
|
|
|
|
|
class rss_feed_plugin : public s2_substitution_plugin { |
|
public: |
|
|
|
std::string hook_name() override {return "feed";}; |
|
|
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties, const std::unordered_map<fs::path, blog_item, pathHash> & pages) override { |
|
|
|
|
|
auto args = get_arguments(invocation, 2); |
|
std::string type = args.at(1); |
|
std::string relpath = args.at(2); |
|
feed_builder::feed_format format; |
|
if (type == "rss") { |
|
format = feed_builder::feed_format::RSS; |
|
} else if (type == "atom") { |
|
format = feed_builder::feed_format::ATOM; |
|
} |
|
|
|
|
|
auto feed_items = get_sorted_post_list(relpath, properties, pages); |
|
|
|
feed_builder f {stgen::compute_url(properties.at("current_file"), properties), properties.at("name"), {properties.at("author")}, format}; |
|
|
|
|
|
for (auto entry = feed_items.rbegin(); entry != feed_items.rend(); entry++) { |
|
// write into rss feed |
|
std::string category = entry->second.src.parent_path().filename().string(); |
|
|
|
std::string title; |
|
if (entry->second.properties.count("title")) |
|
title = entry->second.properties.at("title"); |
|
else |
|
title = entry->second.src.filename().string(); |
|
|
|
std::string content; |
|
if (entry->second.properties.count("original")) { |
|
content = entry->second.properties.at("original"); |
|
} else { |
|
// fall back to full generated page. |
|
content = entry->second.properties.at("body"); |
|
} |
|
|
|
f.add_article(stgen::compute_url(entry->second.src, properties), title, category, entry->second.post_date, entry->second.post_date, content); |
|
|
|
|
|
} |
|
|
|
std::string text = f.str(); |
|
file_text.replace(start, end - start, text); |
|
|
|
return text.length(); |
|
} |
|
}; |
|
|
|
|
|
class microblog_plugin : public s2_substitution_plugin { |
|
public: |
|
|
|
std::string hook_name() override {return "microblog";}; |
|
|
|
|
|
int perform_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties, const std::unordered_map<fs::path, blog_item, pathHash> & pages) override { |
|
|
|
auto args = get_arguments(invocation, 1); |
|
std::string relpath = args.at(1); |
|
|
|
std::multimap<time_t, blog_item> feed_items = get_sorted_post_list(relpath, properties, pages); |
|
|
|
std::string text = ""; |
|
|
|
// feed_builder f {compute_url(properties.at("current_file"), properties), properties.at("name"), {properties.at("author")}}; |
|
|
|
for (auto entry = feed_items.rbegin(); entry != feed_items.rend(); entry++) { |
|
// write into rss feed |
|
std::string category = entry->second.src.parent_path().filename().string(); |
|
|
|
std::string content; |
|
if (entry->second.properties.count("original")) { |
|
content = entry->second.properties.at("original"); |
|
} else { |
|
// fall back to full generated page. |
|
content = entry->second.properties.at("body"); |
|
} |
|
|
|
std::ostringstream ss {}; |
|
auto timepoint = std::chrono::system_clock::from_time_t(entry->second.post_date); |
|
date::to_stream(ss, properties.at("date-out-format").c_str(), timepoint); |
|
|
|
text += "</hr>\n<div class=\"microblog_post\">"; |
|
|
|
text += "<div class=\"microblog_date\"><a href=\"" + stgen::compute_url(entry->second.src, entry->second.properties) + "\">"; |
|
text += ss.str(); |
|
text += "</a></div>"; |
|
text += "<div class=microblog_content>"; |
|
if (entry->second.properties.count("title")) { |
|
text += "\n<h2>" + entry->second.properties.at("title") + " </h2>\n"; |
|
} |
|
text += content; |
|
text += "</div></div>\n"; |
|
|
|
} |
|
|
|
file_text.replace(start, end - start, text); |
|
|
|
return text.length(); |
|
} |
|
}; |
|
|
|
|
|
class templater { |
|
|
|
std::unordered_map<std::string, substitution_plugin *> substitution_commands {}; |
|
std::unordered_map<std::string, s2_substitution_plugin *> s2_substitution_commands {}; |
|
const std::optional<const std::unordered_map<fs::path, blog_item, pathHash>> NO_PAGES {}; |
|
|
|
public: |
|
|
|
templater() { |
|
/* |
|
substitution_commands[variable_transclude_plugin::hook_name] = new variable_transclude_plugin {}; |
|
substitution_commands[file_index_plugin::hook_name] = new file_index_plugin{}; |
|
substitution_commands[ifdef_plugin::hook_name] = new ifdef_plugin{}; |
|
substitution_commands[ifndef_plugin::hook_name] = new ifndef_plugin{}; |
|
substitution_commands[file_transclude_plugin::hook_name] = new file_transclude_plugin{}; |
|
substitution_commands[mmd_snippet_transclude_plugin::hook_name] = new mmd_snippet_transclude_plugin{}; |
|
|
|
s2_substitution_commands[rss_feed_plugin::hook_name] = new rss_feed_plugin{}; |
|
s2_substitution_commands[microblog_plugin::hook_name] = new microblog_plugin{}; |
|
*/ |
|
} |
|
|
|
|
|
templater(std::vector<substitution_plugin *> s1, std::vector<s2_substitution_plugin *> s2) { |
|
for (auto s : s1) { |
|
substitution_commands.insert({s->hook_name(), s}); |
|
} |
|
for (auto s : s2) { |
|
s2_substitution_commands.insert({s->hook_name(), s}); |
|
} |
|
|
|
} |
|
|
|
|
|
struct done_subtitution_options { |
|
int num; |
|
bool recurse = false; |
|
}; |
|
|
|
done_subtitution_options |
|
do_substitution(int start, int end, const std::string &invocation, |
|
std::string &file_text, |
|
const std::unordered_map<std::string, std::string> &properties, |
|
const std::optional<const std::unordered_map<fs::path, blog_item, pathHash>> &pages) |
|
{ |
|
|
|
std::string command_name = invocation.substr(0, invocation.find(":")); |
|
|
|
bool recurse = true; |
|
if (command_name == "feed") |
|
recurse = false; |
|
|
|
if (substitution_commands.count(command_name)) { |
|
int num = substitution_commands.at(command_name)->perform_substitution( |
|
start, end, invocation, file_text, properties); |
|
return {num, recurse}; |
|
} |
|
|
|
if (pages) { |
|
if (s2_substitution_commands.count(command_name)) { |
|
int num = s2_substitution_commands.at(command_name)->perform_substitution( |
|
start, end, invocation, file_text, properties, *pages); |
|
return {num, recurse}; |
|
} |
|
} |
|
spdlog::warn("substplugin error: {}", invocation); |
|
|
|
return {}; |
|
} |
|
|
|
void |
|
run_substitution_plugins(std::string &text, |
|
const std::unordered_map<std::string, std::string> &properties) |
|
{ |
|
run_substitution_plugins(text, properties, NO_PAGES, true); |
|
} |
|
|
|
void |
|
run_substitution_plugins_once(std::string &text, |
|
const std::unordered_map<std::string, std::string> &properties) |
|
{ |
|
run_substitution_plugins(text, properties, NO_PAGES, false); |
|
} |
|
|
|
void |
|
run_substitution_plugins(std::string &text, |
|
const std::unordered_map<std::string, std::string> &properties, const std::optional<const std::unordered_map<fs::path, blog_item, pathHash>> &pages) { |
|
run_substitution_plugins(text, properties,pages,true); |
|
} |
|
|
|
|
|
|
|
void |
|
run_substitution_plugins(std::string &text, |
|
const std::unordered_map<std::string, std::string> &properties, const std::optional<const std::unordered_map<fs::path, blog_item, pathHash>> &pages, bool allow_recursion) |
|
{ |
|
std::string::size_type next = text.find(TEMPLATE_CODE_START, 0); |
|
|
|
for (auto next = text.find(TEMPLATE_CODE_START); |
|
next != std::string::npos; |
|
next = text.find(TEMPLATE_CODE_START, next)) { |
|
|
|
// allow escaping inclusion |
|
if (next > 0 && text.at(next - 1) == '\\') { |
|
next = text.find(TEMPLATE_CODE_END, next); |
|
continue; |
|
} |
|
|
|
std::string::size_type end = text.find(TEMPLATE_CODE_END, next); |
|
std::string::size_type next_start = text.find(TEMPLATE_CODE_START, |
|
next + TEMPLATE_CODE_START.length()); |
|
|
|
if (end == std::string::npos) { |
|
return; |
|
} |
|
|
|
int search_from = next + TEMPLATE_CODE_START.length(); |
|
int loops = 0; |
|
while (next_start < end) { |
|
|
|
// we found a nested tag |
|
// {{ifdef:tag:hello world {{tag}} }} |
|
// |
|
|
|
next_start = text.find(TEMPLATE_CODE_START, search_from); |
|
|
|
end = text.find(TEMPLATE_CODE_END, search_from); |
|
|
|
// spdlog::warn("searching {}", text.substr(search_from, end)); |
|
// spdlog::warn("found start {} end {}", next_start - search_from, end - search_from); |
|
|
|
if (end == std::string::npos) { |
|
spdlog::warn("Reached end of file when looking for closing template tag: {}", properties.at("current_file")); |
|
return; |
|
} |
|
|
|
if (next_start == std::string::npos) { |
|
break; |
|
} |
|
|
|
search_from = end + TEMPLATE_CODE_END.length(); |
|
|
|
} |
|
|
|
int ss = next + TEMPLATE_CODE_START.length(); |
|
std::string invocation = text.substr(ss, end - ss); |
|
|
|
int ff = invocation.find_first_not_of(" \n\t"); |
|
if (ff != std::string::npos) { |
|
int ll = invocation.find_last_not_of(" \n\t"); |
|
invocation = invocation.substr(ff, ll - ff + 1); |
|
} |
|
|
|
end += TEMPLATE_CODE_END.length(); |
|
|
|
auto subst = do_substitution(next, end, invocation, text, properties, pages); |
|
//next += subst.num; |
|
if (!subst.num) { |
|
// unsuccesful |
|
next += TEMPLATE_CODE_START.length(); |
|
|
|
// spdlog::info("Substitution failed, {} in {}", nvocation, properties.at("current_file")); |
|
} else if (!allow_recursion || !subst.recurse) { |
|
// do not recurse into substituted content |
|
next += subst.num; |
|
} |
|
|
|
// at this point next needs to point to the start of the text just |
|
// substituted in |
|
|
|
} |
|
} |
|
|
|
~templater() { |
|
for (auto e : substitution_commands) { |
|
delete e.second; |
|
} |
|
for (auto e: s2_substitution_commands) { |
|
delete e.second; |
|
} |
|
} |
|
};
|
|
|