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

file::./templates/template.html::40:: html link to file
<!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

example 1

file::./templates/examples/ssg-docs-example1.e.html::

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

file::./templates/showerthoughts/posts.e.html::5:: link to file
# 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 truncated
file::./templates/showerthoughts.py:: link to file
import 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
file::./templates/blog-posts.t.html::10:: link to file
!::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.

todos