Browse Source

microblog feed

master
alistair 3 years ago
parent
commit
32948c1504
  1. 172
      default-templates/documentation.md
  2. 1
      default-templates/documentation.md
  3. 3
      default-templates/stgen.json
  4. 105
      default-templates/style.css
  5. 244
      default-templates/style2.css
  6. 200
      main.cpp

172
default-templates/documentation.md

@ -1,172 +0,0 @@ @@ -1,172 +0,0 @@
---
title: stgen3
author: Alistair Michael
date-in-format: %Y
date: 2021
notemplating: yes
---
`stgen3` is a static site generator written in C++17, which uses the MultiMarkdown
markdown parser to generate html. It aims to be fast, portable, and simple to use
without configuration.
### Features
- minimalistic templating system
- single static binary with no dependencies
- no config required
- templates and configuration exist alongside the markdown source
- configuration and templates are inherited down the directory structure
- [Multimarkdown 6 syntax](https://fletcher.github.io/MultiMarkdown-6/)
Note: not all of the following functionality is supported yet.
### Dependencies
- nlohmann json
- MultiMarkdown (clone into the directory `MultiMarkdown` and compile)
- enable the token pool
- date.h (hopefully can be replaced with C++20 `std::chrono` one day)
- spdlog
- xxd
### Building
1. Clone project
2. Clone MultiMarkdown 6 into the folder `MultiMarkdown`, enable the token pool
and compile it.
2. Ensure xxd is installed and run the script `defaults.sh`
3. `mkdir build && cd build && cmake .. && make`
### Basic Behaviour
Provided with a source directory, and an output directory, it recurses through
the source directory compiling all markdown files to html and writing them to
the destination directory, while copying all other files that are not on the
ignored list.
Ignored files list:
- `stgen.json`
- anything ending in `.template`
### Example
Running the following command where, the `src` and `dest` directories do not
exist will create a new template site and build it into the dets directory.
```sh
$ stgen3 src dest
```
### Configuration
Configuration files must be named `stgen.json`, and contain a json string-string
map of attributes. These files can be placed anywhere in the site and their
values are inherited down the file tree from that point. Repeatedly specifying
the same attribute overwrites it.
The main attributes that should be set are
- `url` : the root url of the site
- `name` : the name of the site
For sub-directories, `date` can be set to determine its position in directory
listings sorted by date.
### Per-Article Configuration
Behaviour can be modified by configuration in the article's heading block.
Heading blocks must start at the first line of the file. They may have `---` on
the first and last line.
- `notemplating`: disable templating for the file
- `template`: the html template to substitute content into
## Templating
### Syntax
Templates are surrounded by `{{ }}` braces, they consist of a command name
followed by a colon separated list of arguments. Any starting or trailing
whitespace on commands or arguments is ignored.
Templates can be escaped using a preceding backslash `\{{ }}`. By default,
templating is run against all markdown documents only, and there is no way to
disable this yet.
### Variable substitution
Templates that are replaced with variables are simply a colon followed by the
variable name.
```
{{ :date }}
```
Variables are set in both article headers and `stgen.json`.
Some variables are guaranteed to exist for any article in the system, this
includes:
- `url` : The website's url, it defaults to the output directory absolute path
- `page_url` : The absolute url of the page.
- `name` : The name of the site, it defaults to the input directory name
- `last_updated` : Set to the last write time of the file
- `current_file` : The absolute path of the file in the compiling machine
- `current_directory` : the directory containing the file
- `current file` : the file being processed
Any metavalues set in page headers are accessible as variables. Generally `title`
and `date` should be set.
- `title`: the article's title
- `date`: the publish date of the article
- `author`: the article's author
### Directory listings
```
{{ postlist:relative/path/to/directory }}
```
Directory listings are sorted by the date of the post. Subfolders are ordered
first by their date set in `stgen.json`, then the date of the most recent
article in them, or placed last.
Dates are expected to be in the format `yyyy-mm-dd`.
### Flow Control
```
{{ ifdef:attribute:
Content to include
}}
```
The content of the last argument is included in the document if the specified
attribute is set to something.
The `ifndef` command includes content if the attribute is _not_ set. Template tags can be
nested in this case.
```
{{ ifdef:title
{{ :title }}
}}
{{ifndef:title
Title not set :(
}}
```
### Template Files
The default template markdown files are written to can be overridden by writing
a file called `html.template`. This needs to contain the variable substitution
`{{ :body }}` in order to substitute the compiled markdown content into it.
Templates are also inherited down the directory hierarchy.
The default templates are compiled into the binary.

1
default-templates/documentation.md

@ -0,0 +1 @@ @@ -0,0 +1 @@
../README.md

3
default-templates/stgen.json

@ -2,5 +2,6 @@ @@ -2,5 +2,6 @@
"name": "example",
"url": "http://127.0.0.1:8080",
"date-in-format": "%d-%m-%y",
"date-out-format": "%a, %b %d, %Y"
"date-out-format": "%a, %b %d, %Y",
"author": "name"
}

105
default-templates/style.css

@ -1,7 +1,19 @@ @@ -1,7 +1,19 @@
:root {
--primary-fg: black;
--primary-bg: white;
--primary-link: #0066cc;
--secondary-bg: #eee;
--borders: #bbb;
}
code {
white-space: pre-wrap;
background: #eee;
border-color: #eee;
background: var(--secondary-bg);
border-color: var(--secondary-bg);
border-radius: 6px;
border-style: solid;
border-left-width: 4px;
@ -10,8 +22,15 @@ code { @@ -10,8 +22,15 @@ code {
border-bottom-width:1px;
}
a {
}
.title {
margin-top:18px;
}
div.sourceCode {
background: #eee;
background: var(--secondary-bg);
border-radius: 7px;
}
@ -26,7 +45,7 @@ code { @@ -26,7 +45,7 @@ code {
pre {
font-family: 'Bitstream Vera Sans Mono', 'Monospace', monospace;
background: #eee;
background: var(--secondary-bg);
border-radius:6px;
padding: 0.7em;
}
@ -48,7 +67,7 @@ dl { @@ -48,7 +67,7 @@ dl {
dl dt {
font-size:0.6em;
font-weight:200;
font-weight:400;
}
dl dd {
@ -59,12 +78,14 @@ dl dd { @@ -59,12 +78,14 @@ dl dd {
}
dl dd a {
color:black;
color:var(--primary-fg);
}
body {
font-family: 'Bitrstream Vera Sans', Helvetica Neue, Helvetica, sans-serif, sans;
font-size: 18px;
color:var(--primary-fg);
background:var(--primary-bg);
}
@ -97,8 +118,7 @@ h1, h2, h3, h4, h5, h6 { @@ -97,8 +118,7 @@ h1, h2, h3, h4, h5, h6 {
a {
text-decoration:none;
color: #0066cc;
color: var(--primary-link);
}
@ -108,7 +128,7 @@ div.main_page { @@ -108,7 +128,7 @@ div.main_page {
hr {
border-style: solid;
color: #bbb;
color: var(--borders);
border-width:0.1em;
}
pre.numberSource {
@ -166,19 +186,26 @@ q { @@ -166,19 +186,26 @@ q {
.menu_item {
display:inline-block;
}
.menu_group {
position:inherit;
position:inherit;
}
.menu_group ul {
padding:0;
}
.title {
margin-top:10px;
}
}
blockquote {
border-left: #bbb;
border-left: var(--borders);
border-left-style: solid;
border-left-width: 0.2em;
padding:0.6em;
@ -191,14 +218,12 @@ blockquote p { @@ -191,14 +218,12 @@ blockquote p {
tr:nth-child(even) {background-color: #eee;}
tr:nth-child(even) {background-color: var(--secondary-bg);}
table {
border-collapse: collapse;
display:block;
overflow:auto;
width:100%;
margin: 0, auto;
}
.header th {
@ -212,15 +237,47 @@ table { @@ -212,15 +237,47 @@ table {
}
td {
padding-top:0.3em;
padding-bottom:0.3em;
padding-left: 0.7em;
padding-right:0.7em;
padding:5px;
}
.date {
font-size:0.85em;
}
@media (prefers-color-scheme: dark) {
:root {
--primary-fg: white;
--primary-link: #90e0ff;
--primary-bg: black;
--secondary-bg: #222;
--borders: #bbb;
}
dl dt {
font-size:0.6em;
font-weight:400;
}
}
.microblog_date {
font-size: 0.7em;
text-align: right;
}
.microblog_post .microblog_content p {
margin-top: 0.1em;
}
thead {
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: black;
line-height: 2em;
.microblog_post {
background: var(--secondary-bg);
border-radius: 0.4em;
padding: 0.5em;
margin-top:0.6em;
margin-bottom:0.6em;
}

244
default-templates/style2.css

@ -0,0 +1,244 @@ @@ -0,0 +1,244 @@
code {
white-space: pre-wrap;
background: #eee;
border-color: #eee;
border-radius: 6px;
border-style: solid;
border-left-width: 4px;
border-right-width: 4px;
border-top-width:1px;
border-bottom-width:1px;
}
div.sourceCode {
background: #eee;
border-radius: 7px;
}
pre code {
border-style: none;
background: none;
}
code {
font-family: 'Bitstream Vera Sans Mono', 'Monospace', monospace;
}
pre {
font-family: 'Bitstream Vera Sans Mono', 'Monospace', monospace;
background: #eee;
border-radius:6px;
padding: 0.7em;
}
span.smallcaps {
font-variant: small-caps;
} span.underline {
text-decoration: underline;
}
div.column {
display: inline-block;
vertical-align: top;
width: 50%;
}
dl {
padding-left:1.5em;
}
dl dt {
font-size:0.6em;
font-weight:200;
}
dl dd {
margin-bottom:0.7em;
margin-left:0px;
}
dl dd a {
color:black;
}
body {
font-family: 'Bitrstream Vera Sans', Helvetica Neue, Helvetica, sans-serif, sans;
font-size: 18px;
}
body .main_page p {
line-height:1.3em;
}
.main_page li {
margin-top:0.3em;
margin-bottom:0.3em;
}
img {
margin: auto;
max-width: 90%;
display: block;
}
dd {
margin-top: 5px;
margin-bottom: 7px;
}
h1 {
margin-bottom: 10px;
margin-top: 10px;
}
h1, h2, h3, h4, h5, h6 {
font-weight:400;
}
a {
text-decoration:none;
color: #0066cc;
}
div.main_page {
padding-top: 0;
}
hr {
border-style: solid;
color: #bbb;
border-width:0.1em;
}
pre.numberSource {
margin-left: 4;
}
q {
quotes: "“" "”" "‘" "’";
}
.grid-container {
display: grid;
grid-template-columns: 2fr 3fr 3fr 4fr;
grid-template-rows: 0.5fr repeat(0.5, 1fr);
gap: 1px 1px;
grid-template-areas: "header_group header_group header_group header_group" "menu_group main_page main_page table_of_contents" "menu_group main_page main_page table_of_contents";
}
.header_group { grid-area: header_group; }
.menu_group {
grid-area: menu_group;
position:fixed;
}
.table_of_contents {
grid-area: table_of_contents;
max-width: 100%;
}
.main_page {
grid-area: main_page;
max-width:100%;
}
.menu_item {
display:block;
padding:5px;
display: flex;
justify-content: space-between;
}
@media (max-width: 1200px) {
.grid-container {
display: grid;
gap: 1px 1px;
grid-template-areas: "header_group header_group header_group header_group" "menu_group menu_group menu_group menu_group " "table_of_contents table_of_contents table_of_contents table_of_contents" "main_page main_page main_page main_page";
padding-left:10px;
padding-right:10px;
max-width: 45em;
margin:auto;
}
.menu_item {
display:inline-block;
}
.menu_group {
position:inherit;
}
.menu_group ul {
padding:0;
}
}
blockquote {
border-left: #bbb;
border-left-style: solid;
border-left-width: 0.2em;
padding:0.6em;
padding-left:1em;
}
blockquote p {
margin:0px;
}
tr:nth-child(even) {background-color: #eee;}
table {
border-collapse: collapse;
display:block;
overflow:auto;
width:100%;
margin: 0, auto;
}
.header th {
text-align: left;
border-bottom-style:solid;
border-left:none;
border-right:none;
border-bottom-width:1px;
}
td {
padding-top:0.3em;
padding-bottom:0.3em;
padding-left: 0.7em;
padding-right:0.7em;
}
thead {
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: black;
line-height: 2em;
}
.microblog_date {
font-size: 0.7em;
text-align: right;
}
.microblog_post .microblog_content p {
margin-top: 0.1em;
}
.microblog_post {
background: var(--secondary-bg);
border-radius: 0.4em;
padding: 0.5em;
margin-top:0.1em;
margin-bottom:0.1em;
}

200
main.cpp

@ -876,92 +876,131 @@ class feed_builder { @@ -876,92 +876,131 @@ class feed_builder {
}
};
class rss_feed_plugin : public s2_substitution_plugin {
public:
std::multimap<time_t, blog_item> get_sorted_post_list(const std::string &cs_directories,
const std::map<std::string, std::string> &properties, const std::map<fs::path, blog_item> & pages) {
std::vector<fs::path> paths;
inline static const std::string hook_name = "feed";
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);
spdlog::info("path {}", path.string());
first = next + 1;
next = relpath.find(",", first);
} while (first < relpath.length());
std::multimap<time_t, blog_item> feed_items;
spdlog::info("numpaths {}", paths.size());
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:
inline static const std::string hook_name = "feed";
int perform_substitution(int start, int end, const std::string &invocation,
std::string &file_text,
const std::map<std::string, std::string> &properties, const std::map<fs::path, blog_item> & pages) override {
auto args = get_arguments(invocation, 1);
std::string relpath = args.at(1);
std::vector<fs::path> paths;
auto feed_items = get_sorted_post_list(relpath, properties, pages);
feed_builder f {compute_url(properties.at("current_file"), properties), properties.at("name"), {properties.at("author")}};
std::string::size_type first = 0;
std::string::size_type next = relpath.find(",");
fs::path path;
do {
if (next == std::string::npos)
next = relpath.length();
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 spath = relpath.substr(first, next - first);
std::string title;
if (entry->second.properties.count("title"))
title = entry->second.properties.at("title");
else
title = entry->second.src.filename().string();
if (spath.at(0) == '/') {
// path relative to site root
path = fs::path(properties.at("source_root")).append(spath.substr(1));
std::string content;
if (entry->second.properties.count("original")) {
content = entry->second.properties.at("original");
} else {
// path relative to current directory
path = fs::path(properties.at("current_directory")).append(spath);
// fall back to full generated page.
content = entry->second.properties.at("body");
}
paths.push_back(path);
first = next + 1;
next = relpath.find(",", first);
} while (first < relpath.length());
f.add_article(compute_url(entry->second.src, properties), title, category, entry->second.post_date, entry->second.post_date, content);
std::multimap<time_t, blog_item> feed_items;
for (std::string dir: paths) {
spdlog::info("path: {}", dir);
for (auto &page: pages) {
if (!(page.second.type & (job_type::TEMPLATE | job_type::MARKDOWN))) {
continue;
}
if (page.first.filename() == "index.html") {
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});
}
}
}
std::string text = f.str();
file_text.replace(start, end - start, text);
return text.length();
}
};
/*
file_index_plugin p {};
std::multimap<time_t, file_index_plugin::post_entry> entries;
for (auto path : paths) {
spdlog::info("path {}", path.string());
auto m = p.get_directory_list(path, properties);
class microblog_plugin : public s2_substitution_plugin {
public:
std::stack<file_index_plugin::post_entry> s;
inline static const std::string hook_name = "microblog";
for (auto e : m) {
s.push(e.second);
}
while (!s.empty()) {
auto entry = s.top();
s.pop();
entries.insert({entry.date, entry});
int perform_substitution(int start, int end, const std::string &invocation,
std::string &file_text,
const std::map<std::string, std::string> &properties, const std::map<fs::path, blog_item> & 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);
for (auto sub_entry : entry.sub_directories) {
s.push(sub_entry.second);
}
}
}
*/
std::string text = "";
feed_builder f {compute_url(properties.at("current_file"), properties), properties.at("name"), {properties.at("author")}};
@ -969,12 +1008,6 @@ class rss_feed_plugin : public s2_substitution_plugin { @@ -969,12 +1008,6 @@ class rss_feed_plugin : public s2_substitution_plugin {
// 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");
@ -983,12 +1016,20 @@ class rss_feed_plugin : public s2_substitution_plugin { @@ -983,12 +1016,20 @@ class rss_feed_plugin : public s2_substitution_plugin {
content = entry->second.properties.at("body");
}
f.add_article(compute_url(entry->second.src, properties), title, category, entry->second.post_date, entry->second.post_date, content);
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 += "<div class=\"microblog_post\">";
text += "<div class=\"microblog_date\"><a href=\"" + compute_url(entry->second.src, entry->second.properties) + "\">";
text += ss.str();
text += "</a></div>";
text += "<div class=microblog_content>";
text += content;
text += "</div></div>\n";
}
std::string text = f.str();
file_text.replace(start, end - start, text);
return text.length();
@ -996,6 +1037,7 @@ class rss_feed_plugin : public s2_substitution_plugin { @@ -996,6 +1037,7 @@ class rss_feed_plugin : public s2_substitution_plugin {
};
class builder {
@ -1020,31 +1062,44 @@ class builder { @@ -1020,31 +1062,44 @@ class builder {
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{};
}
int
struct done_subtitution_options {
int num;
bool recurse = true;
};
done_subtitution_options
do_substitution(int start, int end, const std::string &invocation,
std::string &file_text,
const std::map<std::string, std::string> &properties,
std::optional<const std::map<fs::path, blog_item>> pages)
{
spdlog::info("invoc: {}", invocation);
assert(substitution_commands.size() > 0);
std::string command_name = invocation.substr(0, invocation.find(":"));
bool recurse = true;
if (command_name == "feed")
recurse = false;
if (substitution_commands.count(command_name)) {
return substitution_commands.at(command_name)->perform_substitution(
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)) {
return s2_substitution_commands.at(command_name)->perform_substitution(
int num = s2_substitution_commands.at(command_name)->perform_substitution(
start, end, invocation, file_text, properties, *pages);
return {num, recurse};
}
}
return 0;
return {};
}
void
@ -1117,14 +1172,14 @@ class builder { @@ -1117,14 +1172,14 @@ class builder {
end += TEMPLATE_CODE_END.length();
int subst = do_substitution(next, end, invocation, text, properties, pages);
if (!subst) {
auto subst = do_substitution(next, end, invocation, text, properties, pages);
if (!subst.num) {
// unsuccesful
next += TEMPLATE_CODE_START.length();
// spdlog::info("Substitution failed, {} in {}", invocation, properties.at("current_file"));
} else if (properties.count("notemplating")) {
} else if (properties.count("notemplating") || !subst.recurse) {
// do not recurse into substituted content
next += subst;
next += subst.num;
}
// at this point next needs to point to the start of the text just
@ -1769,6 +1824,7 @@ int main(int argc, char **argv) { @@ -1769,6 +1824,7 @@ int main(int argc, char **argv) {
// TODO: Custom template substitution: make recursive through the template
// header declaration: keep running substitution until you run out of
// templates to apply.
// TODO: microblog plugin
auto cmd_options = parse_options(argc, argv);
builder b (cmd_options);

Loading…
Cancel
Save