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

// ./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;
}
}
};