Documentation for diced-ssg (link to script)
diced-ssg is a simple static site generator that takes inspiration from emacs org mode (but less thought out)
i'm writing the docs so i don't forget how this works
directory structure should look like this with generator.py in
the root folder
.
├── cinamorol.jpg
├── generator.py
├── README.txt
├── style.css
└── templates
├── index.t.html
├── links.t.html
├── projects.t.html
├── showerthoughts.t.html
├── ssg-docs.t.html
└── template.html
built files are placed in the root folder
/templates is where the meat goes
.t.html files are templates, any file with this in its name will
be sandwhiched in template.html and placed inside of the
main-content html tag
-
the stylesheet comment is a macro to place
style.css?v=(random number)in the stylesheet for the page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dicedmangoes dumping grounds</title>
<!-- stylesheet -->
</head>
<body>
<div class="header">
dicedmangoes dumping grounds
<a href="index.html" class="header-link">Home</a>
<a href="projects.html" class="header-link">Projects</a>
<a href="links.html" class="header-link">Links</a>
</div>
<main class="main-content">
</main>
<br>
<br>
<br>
<br>
<p>easter egg</p>
<footer id="footer">
<!-- Random shower thought will be displayed here -->
</footer>
<script>
// Array of shower thoughts
const showerThoughts = [
"If you were to open a store selling only international food, it would be a world food store.",
"When you clean out a vacuum cleaner, you become a vacuum cleaner.",
"In a way, every year you live is a year you’re older than you were last year.",
"Money can’t buy happiness, but it can buy a jet ski, which is pretty close.",
"We spend the first year of a baby's life teaching them to walk and talk, and the next 17 years telling them to sit down and shut up.",
"The only time 'success' comes before 'work' is in the dictionary.",
...
file truncated
div class= "block-head" is for macros
-
file macros
file::(path to file)::(max lines to place on page)::- file has 2 args, the path and the max lines
- this is used above to show template.html
-
sorted block macros
- basic usage
- this sorts a region
- see example 1
-
in a block-head div,
sorted::begin::starts a sorted section andsorted::end::ends it - every root level html element in the sorted block must have class="sorted-element" or else the script crashes
- this is useful for sorting dates
-
FOOTGUN: this sorts the entire line, including
<p class=...> - custom sorts
::"sort-command":"_list.(insert command)"::-
this is a block-head argument, just like
::begin::, you can chain them together like::begin::"sort-command"... - add this to the block-head, its json
- the sort command gets
eval(..)ed in python - if you put invalid json it throws a runtime error
- if you put a bad command for sort command it will crash
example 1
advanced usage
custom python commands can be executed
showerthoughts.py is imported and used
custom is a global variable that can be anything
here we set it to the get_posts() function from showerthoughts.py so we can run that function
# 2025-08-14
<h1>new mc server</h1>
<p>
i have a scuffed setup where i use a google cloud server that forwards all
traffic to my pc but now i don't need to open any ports
...
file truncatedimport re
from typing import Iterator
def post(date: str, content: str) -> str:
return "<p class=\"sorted-element\">" + "<b>" + date.strip() + "</b>" + ": " + content + "</p>\n<hr />\n"
def sorter(e: list[str]):
return e[0]
# returns true if it actually got truncated
def truncater(s: str, title: str, summary_length: int) -> tuple[str, bool]:
first_half = s[:summary_length]
second_half = s[summary_length:]
if len(second_half) == 0:
return (first_half, False) # did not truncate
open_tags: Iterator[str] = re.findall(r'<([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>', first_half) # type: ignore
closed_tags: Iterator[str] = re.findall(r'</([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>', first_half) # type: ignore
open_tags = filter(lambda x: not ("br" in x or "hr" in x), open_tags)
at = list(open_tags)
nt = list(closed_tags)
nopen_tags: int = len(at)
nclosed_tags: int = len(nt)
# print(f"{at}, {nt} :: {nopen_tags}, {nclosed_tags}")
output: str = first_half
if nopen_tags > nclosed_tags:
last_open_tag: re.Match[str] | None = re.search(r'<([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>', second_half)
last_closed_tag: re.Match[str] | None = re.search(r'</([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>', second_half)
if (last_closed_tag is None) or (last_open_tag is not None and last_open_tag.end() < last_closed_tag.end()):
print(f"missing closing tag in {title.strip()}")
else:
end = last_closed_tag.end()
output += second_half[:end]
return (output, True)
def write_template(title: str, contents: str) -> str:
id = hash(contents)
with open(f"./gen-templates/post{id}.t.html", 'w') as file:
file.write(f"<h1>{title}:</h1>\n<p>{contents}</p>")
return f"post{str(id)}.html"
def get_posts() -> str:
posts: list[list[str]] = []
with open("./templates/showerthoughts/posts.e.html", 'r') as file:
for line in file:
if line[0] == '#':
posts.append([line[2:], ""])
else:
posts[-1][1] += line + " \n"
output: str = ""
for post_ in posts:
# post_[0] is the date
# post_[1] is the contents
(contents, truncated) = truncater(post_[1], post_[0], 500)
link: str = write_template(post_[0], post_[1])
title = post_[0]
if truncated: contents += f'...<p><a href="{link}">continue reading here</a></p>' # noqa: E701
else: title = f"{post_[0]} <a href='{link}'>permalink</a>" # noqa: E701
output += post(title, contents)
return output
!::import showerthoughts; global custom; custom = showerthoughts.get_posts::
<h1>thoughts feed</h1>
<div class="block-head">sorted::begin::"sort-command":"_list.sort(reverse=True)"::</div>
?::custom()::
<div class="block-head">sorted::end::</div>as you can see i have reinvented markdown by loading the file contents from posts into showerthoughts except its sorted
also showerthoughts.py is massively overcomplicated, 90% of the
code is for creating a summary from the post without cutting out a html
closing tag. it also generates new templates programmatically for each post,
so that each post has a page.
the only way to have a template generate a template is with custom python
commands at the moment, and the generated templates need to be put in
./gen-templates/ and the generated template is not allowed to
generate its own templates.
- ! is for exec-ing python code
- ? sets the current line in the .t.html to the output of the expression from eval()
- ?/ ... /? is a multiline ?
- each of these has one argument encased in ::...:: which is the command to run
- custom python commands get run before anything else, so they can be placed in the sorted block
todos
- make error handling actually good