summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.toml2
-rw-r--r--src/main.cpp186
-rw-r--r--static/style.css219
-rw-r--r--templates/index.html34
-rw-r--r--templates/post.html24
-rw-r--r--templates/search.html36
6 files changed, 476 insertions, 25 deletions
diff --git a/config.toml b/config.toml
index 14ef4c6..cb12ed0 100644
--- a/config.toml
+++ b/config.toml
@@ -1,3 +1,5 @@
[main]
title = "radhitya.org"
url = "/"
+author = "radhitya"
+posts_per_page = 5
diff --git a/src/main.cpp b/src/main.cpp
index 0902b19..88a2f57 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,12 +5,24 @@
#include <fstream>
#include <cassert>
#include <cmark.h>
+#include <cstdlib>
+#include <inja/inja.hpp>
+#include <nlohmann/json.hpp>
+#include <cstring>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <vector>
+#include <algorithm>
+using json = nlohmann::json;
using namespace std::string_literals;
+
struct MainConfig {
- std::string title;
+ std::string title;
std::string url;
std::string author;
+ int posts_per_page = 5;
};
struct FrontmatterConfig {
@@ -27,6 +39,14 @@ struct Config {
FrontmatterConfig frontmatter;
};
+
+struct Post {
+ std::string slug;
+ std::string title;
+ std::string date;
+ std::string body_html;
+};
+
Config
parse_config(const std::string& filepath)
{
@@ -44,6 +64,7 @@ parse_config(const std::string& filepath)
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;
}
@@ -61,18 +82,17 @@ frontmatter(const std::string& filepath)
? std::string::npos
: content.find("+++", first_fence + 3);
- /* TIL: npos == no position */
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);
+ 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;
@@ -90,25 +110,141 @@ markdown_processing(const std::string& md) {
return html;
}
-int main(int argc, char *argv[]) {
- try {
- Config my_config = parse_config("config.toml");
-
- std::cout << "title name: " << my_config.main.title << std::endl;
- std::cout << "url name: " << my_config.main.url << std::endl;
- std::cout << "author: " << my_config.main.author << std::endl;
-
- Config front = frontmatter(argv[1]);
- std::cout << "title page: " << front.frontmatter.title << std::endl;
- std::cout << "title date: " << front.frontmatter.date << std::endl;
- std::cout << "title categories: " << front.frontmatter.categories << std::endl;
- std::cout << "title tags: " << front.frontmatter.tags << std::endl;
- std::cout << "title draft: " << front.frontmatter.draft << std::endl;
- std::cout << "title body: " << markdown_processing(front.frontmatter.body) << std::endl;
-
- } catch (const std::exception& e) {
- std::cerr << e.what() << std::endl;
+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<Post> 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;
}
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..3eb2fad
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,219 @@
+:root {
+ --bg: #fff;
+ --bg-alt: #eee;
+ --text: #000;
+ --link: #00b;
+ --border: #ccc;
+ --max-w: 800px;
+ --pad: 1rem;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #141414;
+ --bg-alt: #1c1c1c;
+ --text: #fff;
+ --link: #8ab4ff;
+ --border: #444;
+ }
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ font-family: Helvetica, Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.6;
+}
+
+header, main, footer {
+ max-width: var(--max-w);
+ margin: auto;
+ padding: 0.75rem var(--pad);
+}
+
+header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 0.75rem;
+}
+
+header h1 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+header h1 a {
+ color: var(--text);
+ text-decoration: none;
+}
+
+header nav a {
+ color: var(--link);
+ text-decoration: none;
+ font-size: 0.875rem;
+}
+
+header nav a:hover {
+ text-decoration: underline;
+}
+
+main {
+ padding-top: 1.5rem;
+ padding-bottom: 1.5rem;
+ min-height: 60vh;
+}
+
+a { color: var(--link); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+/* post list */
+.post-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.post-list li {
+ padding: 0.25rem 0;
+}
+
+.post-list .date {
+ display: block;
+ font-size: 0.8125rem;
+ opacity: 0.7;
+ line-height: 1.4;
+}
+
+.post-list li a {
+ font-size: 0.9375rem;
+}
+
+/* pagination */
+.pagination {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: 1.5rem;
+ font-family: monospace;
+ align-items: center;
+}
+
+.pagination a {
+ border: 1px solid var(--border);
+ padding: 0.2rem 0.55rem;
+ color: var(--link);
+ text-decoration: none;
+}
+
+.pagination a:hover {
+ background: var(--bg-alt);
+}
+
+.pagination .disabled {
+ color: var(--border);
+}
+
+.pagination .info {
+ opacity: 0.7;
+}
+
+/* content pages */
+article h1 {
+ font-size: 1.5rem;
+ margin-bottom: 0.25rem;
+}
+
+article .meta {
+ display: block;
+ font-size: 0.875rem;
+ opacity: 0.8;
+ margin-bottom: 1rem;
+}
+
+article h2 { font-size: 1.2rem; margin: 1.5rem 0 0.5rem; }
+article h3 { font-size: 1.1rem; margin: 1.25rem 0 0.4rem; }
+
+p, ul, ol, blockquote, pre { margin-bottom: 1rem; }
+ul, ol { padding-left: 1.5rem; }
+li { margin-bottom: 0.25rem; }
+
+pre, code {
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 0.9em;
+}
+
+code {
+ background: var(--bg-alt);
+ padding: 0.15em 0.4em;
+ border-radius: 3px;
+}
+
+pre {
+ padding: 0.5rem 1rem;
+ margin: 0 -1rem;
+ overflow-x: auto;
+ background: #272822;
+ border-radius: 0;
+}
+
+pre code {
+ color: #f8f8f2;
+ background: none;
+ padding: 0;
+ font-size: 0.875em;
+}
+
+blockquote {
+ border-left: 3px solid var(--border);
+ padding-left: 1rem;
+ opacity: 0.85;
+ font-style: italic;
+}
+
+hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 1.5rem 0;
+}
+
+img { max-width: 100%; height: auto; }
+
+/* search */
+input[type="search"] {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-size: 1rem;
+ font-family: inherit;
+ outline: none;
+ margin-bottom: 1.5rem;
+}
+
+input[type="search"]:focus {
+ border-color: var(--link);
+}
+
+footer {
+ margin-top: 2rem;
+ padding-top: 0.75rem;
+ border-top: none;
+ font-size: 0.875rem;
+ opacity: 0.8;
+}
+
+footer p { margin: 0; }
+
+@media (max-width: 767px) {
+ header, main, footer {
+ padding: 0.6rem;
+ }
+ header { margin-top: 0.6rem; }
+ article h1 { font-size: 1.3rem; }
+}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..f981b3b
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{ site.title }}</title>
+<link rel="stylesheet" href="{{ site.url }}style.css">
+</head>
+<body>
+<header>
+ <h1><a href="/">{{ site.title }}</a></h1>
+ <nav><a href="/search/">search</a></nav>
+</header>
+<main>
+{% if intro %}<section class="intro">{{ intro }}</section>{% endif %}
+<ul class="post-list">
+{% for post in posts %}
+ <li>
+ <span class="date">{{ post.date }}</span>
+ <a href="/{{ post.slug }}/">{{ post.title }}</a>
+ </li>
+{% endfor %}
+</ul>
+{% if total_pages > 1 %}
+<nav class="pagination">
+ <span>{% if has_prev %}<a href="{{ prev_url }}">← newer</a>{% else %}<span class="disabled">← newer</span>{% endif %}</span>
+ <span class="info">page {{ page }} of {{ total_pages }}</span>
+ <span>{% if has_next %}<a href="{{ next_url }}">older →</a>{% else %}<span class="disabled">older →</span>{% endif %}</span>
+</nav>
+{% endif %}
+</main>
+<footer><p>{{ site.author }}</p></footer>
+</body>
+</html>
diff --git a/templates/post.html b/templates/post.html
new file mode 100644
index 0000000..531ea63
--- /dev/null
+++ b/templates/post.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{ title }} — {{ site.title }}</title>
+<link rel="stylesheet" href="{{ site.url }}style.css">
+</head>
+<body>
+<header>
+ <h1><a href="/">{{ site.title }}</a></h1>
+ <nav><a href="/search/">search</a></nav>
+</header>
+<main>
+ <article>
+ <h1>{{ title }}</h1>
+ <span class="meta">{{ date }}{% if draft %} ◈ draft{% endif %}</span>
+ {{ body }}
+ </article>
+ <p><a href="/">← back</a></p>
+</main>
+<footer><p>{{ site.author }}</p></footer>
+</body>
+</html>
diff --git a/templates/search.html b/templates/search.html
new file mode 100644
index 0000000..b022c8d
--- /dev/null
+++ b/templates/search.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Search — {{ site.title }}</title>
+<link rel="stylesheet" href="{{ site.url }}style.css">
+</head>
+<body>
+<header>
+ <h1><a href="/">{{ site.title }}</a></h1>
+ <nav><a href="/">home</a></nav>
+</header>
+<main>
+ <input type="search" id="q" placeholder="search posts..." autofocus>
+ <ul id="results" class="post-list"></ul>
+</main>
+<footer><p>{{ site.author }}</p></footer>
+<script>
+var idx = [];
+fetch("/search.json").then(function(r){return r.json()}).then(function(d){idx=d});
+document.getElementById("q").addEventListener("input", function(){
+ var q = this.value.toLowerCase().trim();
+ var html = "";
+ if (q.length < 2) { document.getElementById("results").innerHTML = ""; return; }
+ for (var i = 0; i < idx.length; i++) {
+ var p = idx[i];
+ if (p.title.toLowerCase().indexOf(q) >= 0 || p.text.toLowerCase().indexOf(q) >= 0) {
+ html += '<li><h2><a href="/' + p.slug + '/">' + p.title + '</a></h2><span class="meta">' + p.date + '</span></li>';
+ }
+ }
+ document.getElementById("results").innerHTML = html || "<li class='none'>no results</li>";
+});
+</script>
+</body>
+</html>