#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using json = nlohmann::json; using namespace std::string_literals; struct MainConfig { std::string title; std::string url; std::string author; int posts_per_page = 5; }; struct FrontmatterConfig { std::string title; std::string date; std::string categories; std::string tags; bool draft; std::string body; }; struct Config { MainConfig main; FrontmatterConfig frontmatter; }; struct Post { std::string slug; std::string title; std::string date; std::string body_html; }; Config parse_config(const std::string& filepath) { toml::table tbl; Config cfg; try { tbl = toml::parse_file(filepath); } catch (const toml::parse_error& err) { std::cerr << "parsing failed: " << err << std::endl; throw; } if (auto* main = tbl["main"].as_table()) { cfg.main.title = (*main)["title"].value_or("untitled's blog"); cfg.main.url = (*main)["url"].value_or("localhost"); cfg.main.author = (*main)["author"].value_or("linus"); cfg.main.posts_per_page = (int)(*main)["posts_per_page"].value_or(5LL); } return cfg; } Config frontmatter(const std::string& filepath) { Config cfg; std::ifstream file(filepath); std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); const size_t first_fence = content.find("+++"); const size_t second_fence = (first_fence == std::string::npos) ? std::string::npos : content.find("+++", first_fence + 3); if (first_fence != std::string::npos && second_fence != std::string::npos) { std::string fm = content.substr(first_fence + 3, second_fence - (first_fence+3)); try { toml::parse_result result = toml::parse(fm); const toml::table& tbl = result; cfg.frontmatter.title = tbl["title"].value_or(""s); cfg.frontmatter.date = tbl["date"].value_or(""s); cfg.frontmatter.categories = tbl["categories"].value_or(""s); cfg.frontmatter.tags = tbl["tags"].value_or(""s); cfg.frontmatter.draft = tbl["draft"].value_or(true); } catch(const toml::parse_error& err) { std::cerr << "parsing failed: " << err << std::endl; throw; } cfg.frontmatter.body = content.substr(second_fence + 3); } return cfg; } std::string markdown_processing(const std::string& md) { char *raw = cmark_markdown_to_html(md.c_str(), md.size(),0); std::string html(raw); free(raw); return html; } std::string strip_html(const std::string& html) { std::string out; bool in_tag = false; for (char c : html) { if (c == '<') in_tag = true; else if (c == '>') in_tag = false; else if (!in_tag) out += c; } return out; } int main() { inja::Environment env; nlohmann::json site; system("rm -rf public"); if (mkdir("public", 0755) != 0) { std::cerr << "cant create public" << std::endl; return 1; } system("cp -r static/. public/"); Config my_config = parse_config("config.toml"); int ppp = my_config.main.posts_per_page; if (ppp < 1) ppp = 5; site["site"]["title"] = my_config.main.title; site["site"]["author"] = my_config.main.author; site["site"]["url"] = my_config.main.url; std::vector posts; std::string intro_html; DIR *dir = opendir("content"); if (!dir) { std::cerr << "cant open content/" << std::endl; return 1; } struct dirent* ent; while ((ent = readdir(dir)) != nullptr) { if (ent->d_name[0] == '.') continue; const char* ext = strrchr(ent->d_name, '.'); if (!ext || strcmp(ext, ".md") != 0) continue; std::string fname(ent->d_name); std::string slug(fname.c_str(), ext - ent->d_name); std::string fpath = "content/" + fname; Config page = frontmatter(fpath); if (slug == "_index") { intro_html = markdown_processing(page.frontmatter.body); continue; } if (page.frontmatter.draft) continue; posts.push_back({ slug, page.frontmatter.title.empty() ? slug : page.frontmatter.title, page.frontmatter.date, markdown_processing(page.frontmatter.body) }); } closedir(dir); std::sort(posts.begin(), posts.end(), [](const Post& a, const Post& b) { return a.date > b.date; }); for (const auto& p : posts) { nlohmann::json data = site; data["title"] = p.title; data["date"] = p.date; data["draft"] = false; data["body"] = p.body_html; std::string html = env.render_file("templates/post.html", data); std::string outdir = "public/" + p.slug; mkdir(outdir.c_str(), 0755); std::ofstream out(outdir + "/index.html"); out << html; } int total = (int)posts.size(); int pages = total > 0 ? (total + ppp - 1) / ppp : 1; for (int page = 1; page <= pages; page++) { nlohmann::json data = site; data["intro"] = intro_html; json arr = json::array(); int start = (page - 1) * ppp; int end = std::min(start + ppp, total); for (int i = start; i < end; i++) { arr.push_back({ {"title", posts[i].title}, {"slug", posts[i].slug}, {"date", posts[i].date}, {"excerpt", posts[i].body_html} }); } data["posts"] = arr; data["page"] = page; data["total_pages"] = pages; data["has_prev"] = (page > 1); data["has_next"] = (page < pages); data["prev_url"] = (page == 2) ? "/" : ("/page/" + std::to_string(page - 1) + "/"); data["next_url"] = "/page/" + std::to_string(page + 1) + "/"; std::string outdir = (page == 1) ? "public" : ("public/page/" + std::to_string(page)); if (page > 1) system(("mkdir -p " + outdir).c_str()); std::string html = env.render_file("templates/index.html", data); std::ofstream out(outdir + "/index.html"); out << html; } json search_idx = json::array(); for (const auto& p : posts) { search_idx.push_back({ {"title", p.title}, {"slug", p.slug}, {"date", p.date}, {"text", strip_html(p.body_html)} }); } std::ofstream sj("public/search.json"); sj << search_idx.dump(); nlohmann::json data = site; data["title"] = "Search"; system("mkdir -p public/search"); std::string html = env.render_file("templates/search.html", data); std::ofstream so("public/search/index.html"); so << html; std::cout << "wrote " << posts.size() << " posts, " << pages << " page(s), search" << std::endl; return 0; }