~cricket/jazz

84ec0643a7c9702cc5f19c668cccf9be472aa64b — c piapiac 3 months ago 39e6dbf
update style again

* zig -> 0.8.0
* custom prism theme
* modifies theme to fit my other sites
* removes lib/
* updates LICENSE
14 files changed, 86 insertions(+), 1006 deletions(-)

M .gitignore
M LICENSE
M build.zig
M header.html
M include/ckt
M jazz.zig
D lib/zckt/.gitignore
D lib/zckt/LICENSE
D lib/zckt/README
D lib/zckt/zckt.zig
D static/prism-ghcolors.css
D static/prism-gruvbox-dark.css
M static/prism.css
M static/stylesheet.css
M .gitignore => .gitignore +1 -0
@@ 1,2 1,3 @@
zig-cache/
zig-out/
out/

M LICENSE => LICENSE +9 -7
@@ 1,9 1,11 @@
copyright (c) 2021 <_c@piapiac.org>
dear user,

permission is hereby granted to modify, distribute, and/or use this software 
in any way you damn well please as long as this entire copyright notice, 
along with the disclaimer below, is kept intact
you are free to modify, distribute, and/or 
use this software for any purpose, as long 
as you keep this entire notice intact.

as far as the law allows, this software comes as is, without any warranty 
or condition, and no contributor will be liable to anyone for any damages 
related to this software or license, under any kind of legal claim
this software is provided with no warranty;
i shall not be held liable for any damages 
its use might incur.

-- cricket <_c@piapiac.org>

M build.zig => build.zig +1 -1
@@ 6,7 6,7 @@ pub fn build(b: *std.build.Builder) void {

    const exe = b.addExecutable("jazz", "jazz.zig");

    exe.addPackage(.{ .name="zckt", .path="lib/zckt/zckt.zig" });
    exe.addPackage(.{ .name = "zckt", .path = "include/ckt/zckt.zig" });
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.install();

M header.html => header.html +1 -2
@@ 5,8 5,7 @@
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>jazz/{0s}</title>
        <link rel="stylesheet" href="stylesheet.css">
        <link rel="stylesheet" href="prism-ghcolors.css">
        <link rel="stylesheet" href="prism-gruvbox-dark.css">
        <link rel="stylesheet" href="prism.css">

        <meta name="title" content="{0s}">
        <meta name="description" content="{1s}">

M include/ckt => include/ckt +1 -1
@@ 1,1 1,1 @@
Subproject commit 5d2331a83180b6fb88ee1ef4640ea45e9e489042
Subproject commit 0c3963cc8155daa573a46ed311276fbeba419eab

M jazz.zig => jazz.zig +22 -23
@@ 12,8 12,7 @@ const heap = std.heap;
const log = std.log;
const mem = std.mem;

const ckt = @import("zckt");
const Ckt = ckt.Parser;
const ckt = @import("zckt").parser.table_parser;

const index_file = .{
    @embedFile("index/head.html"),


@@ 55,7 54,7 @@ pub fn main() !void {
    const out = try cwd.openDir("out", .{});

    log.info("open static/\n", .{});
    const static = try cwd.openDir("static", .{.iterate=true});
    const static = try cwd.openDir("static", .{ .iterate = true });
    var static_iter = static.iterate();

    while (try static_iter.next()) |entry| {


@@ 65,27 64,27 @@ pub fn main() !void {
    }

    // GET POSTS
    debug.print("\n",.{});
    debug.print("\n", .{});
    var posts: [post_files.len]ckt.Table = undefined;
    inline for (post_files) |post, i| {
        posts[i] = try Ckt.parse(post, a);
        posts[i] = try ckt.parse(post, a);
        const name = posts[i].getString("filename") orelse unreachable;
        log.info("open & parse posts/{s}",.{name});
        log.info("open & parse posts/{s}", .{name});
    }

    // CREATE INDEX FILE
    debug.print("\n",.{});
    debug.print("\n", .{});
    log.info("create out/index.html", .{});
    var index = try out.createFile("index.html", .{.truncate=true});
    var index = try out.createFile("index.html", .{ .truncate = true });
    log.info("write header", .{});
    try index.writer().print(header_file, .{"index.html", "WELCOME TO CRICKET'S BLOG!"});
    log.info("write index head\n",.{});
    try index.writer().print(header_file, .{ "index.html", "WELCOME TO CRICKET'S BLOG!" });
    log.info("write index head\n", .{});
    _ = try index.write(index_file[0]);

    // CREATE RSS FILE
    log.info("create out/rss.xml", .{});
    var rss = try out.createFile("rss.xml", .{.truncate=true});
    log.info("write rss head\n",.{});
    var rss = try out.createFile("rss.xml", .{ .truncate = true });
    log.info("write rss head\n", .{});
    _ = try rss.write(rss_file[0]);

    // LOOP THROUGH POST FILES


@@ 99,10 98,10 @@ pub fn main() !void {
        const content = post.getString("content") orelse unreachable;

        log.info("post {s}", .{title});
        log.info("write index segment",.{});
        try index.writer().print(index_file[1],.{filename,title,description,pub_date});
        log.info("write index segment", .{});
        try index.writer().print(index_file[1], .{ filename, title, description, pub_date });

        log.info("write rss segment",.{});
        log.info("write rss segment", .{});
        try rss.writer().print(rss_file[1], .{
            title,
            filename,


@@ 112,18 111,18 @@ pub fn main() !void {
            content,
        });

        log.info("create out/{s}",.{filename});
        var file = try out.createFile(filename, .{.truncate=true});
        log.info("write header",.{});
        try file.writer().print(header_file, .{title,description});
        log.info("write out/{s}\n",.{filename});
        try file.writer().print(post_file,.{filename,title,description,pub_date,content});
        log.info("create out/{s}", .{filename});
        var file = try out.createFile(filename, .{ .truncate = true });
        log.info("write header", .{});
        try file.writer().print(header_file, .{ title, description });
        log.info("write out/{s}\n", .{filename});
        try file.writer().print(post_file, .{ filename, title, description, pub_date, content });
    }

    // WRITE INDEX & RSS TAIL
    log.info("write index tail",.{});
    log.info("write index tail", .{});
    _ = try index.write(index_file[2]);

    log.info("write rss tail",.{});
    log.info("write rss tail", .{});
    _ = try rss.write(rss_file[2]);
}

D lib/zckt/.gitignore => lib/zckt/.gitignore +0 -1
@@ 1,1 0,0 @@
zig-cache/

D lib/zckt/LICENSE => lib/zckt/LICENSE +0 -9
@@ 1,9 0,0 @@
copyright (c) 2021 cricket <_c@piapiac.org>

permission is hereby granted to modify, distribute, and/or use this software 
in any way you damn well please as long as this entire copyright notice, 
along with the disclaimer below, is kept intact

as far as the law allows, this software comes as is, without any warranty 
or condition, and no contributor will be liable to anyone for any damages 
related to this software or license, under any kind of legal claim

D lib/zckt/README => lib/zckt/README +0 -44
@@ 1,44 0,0 @@
       _   _
 _____| |_| |
|_ / _| / / _|
/__\__|_\_\__|
    zig ckt parser

ABOUT
    this parser will parse a ckt file into to a stringhashmap
    ckt is a stupid simple file format for notating tables
    you can view more about it on https://sr.ht/~cricket/ckt

USAGE
    dont really use this yet; it's sort of just bodged together to work,
    and tests are lacking / incomplete.

    that being said,
    ---
    const zckt = @import("zckt").Parser;

    // ...
    zkct.parse(data, allocator);
    // returns a table of key/value pairs you can work with,
    // with values being either strings or tables.

DEVELOPMENT
    contributions are welcome to zckt and ckt itself!
    
    don't worry too much about coding style! worry more about the code itself, 
    though, please run `zig fmt` on any changed files before contributing
    and try to follow the zig style guide.

    then, send an email with your patch to ~cricket/zckt-devel@lists.sr.ht
    (unsure how? check out git-send-email.io)

LICENSE
    zckt is free software - it is licensed under the CP/PL license

    this means that you're free to use the software in any way you damn well please,
    as long as the copyright notice and warranty disclaimer are kept intact

    read the LICENSE file for more information

AUTHORS
    do `git shortlog -sne` to show a list of all contributors

D lib/zckt/zckt.zig => lib/zckt/zckt.zig +0 -592
@@ 1,592 0,0 @@
// small, bodged together ckt parser
//
//
const std = @import("std");
const debug = std.debug;
const fmt = std.fmt;
const mem = std.mem;

pub const Key = []const u8;

pub const Value = union(enum) {
    string: []const u8,
    table: Table,

    pub fn deinit(self: *Value, a: *mem.Allocator) void {
        switch (self.*) {
            .string => |str| a.free(str),
            .table => |*table| table.deinit(),
        }
    }

    pub fn asString(self: Value) error{NotAString}![]const u8 {
        switch (self) {
            .string => |str| return str,
            .table => return error.NotAString,
        }
    }

    pub fn asTable(self: Value) error{NotATable}!Table {
        switch (self) {
            .string => return error.NotATable,
            .table => |table| return table,
        }
    }

    pub fn format(self: Value, comptime _: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
        switch (self) {
            .string => |str| try writer.print("{s}", .{str}),
            .table => |table| try writer.print("[ ... ]", .{}), //try writer.print("{s}", .{table}),
        }
    }
};

pub const Table = struct {
    pub const Map = std.StringArrayHashMap(Value);

    map: Map,
    a: *mem.Allocator,

    pub fn init(a: *mem.Allocator) Table {
        return .{
            .map = Map.init(a),
            .a = a,
        };
    }

    pub fn format(self: Table, comptime _: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
        try writer.print("[ ", .{});
        var iter = self.map.iterator();
        while (iter.next()) |entry| try writer.print("{s}={s};", .{ entry.key, entry.value });
        try writer.print(" ]", .{});
    }

    pub fn get(self: *Table, key: []const u8) ?Value {
        return self.map.get(key);
    }

    pub fn getString(self: *Table, key: []const u8) ?[]const u8 {
        const value = self.map.get(key) orelse return null;
        return value.asString() catch return null;
    }

    pub fn getTable(self: *Table, key: []const u8) ?Table {
        const value = self.map.get(key) orelse return null;
        return value.asTable() catch return null;
    }

    pub fn put(self: *Table, key: []const u8, value: Value) !void {
        var result = try self.map.fetchPut(key, value);

        // free memory if needed
        if (result) |*entry| {
            self.a.free(key);
            entry.value.deinit(self.a);
        }
    }

    pub fn deinit(self: *Table) void {
        var iter = self.map.iterator();

        while (iter.next()) |entry| {
            self.a.free(entry.key);
            entry.value.deinit(self.a);
        }

        self.map.deinit();
    }
};

pub const Token = union(enum) {
    pub const String = union(enum) {
        unquoted: []const u8,
        quoted: []const u8,
        multiline: []const u8,

        pub fn format(self: String, comptime _: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
            switch (self) {
                .unquoted => |str| try writer.print("unquoted: {s}", .{str}),
                .quoted => |str| try writer.print("quoted: {s}", .{str}),
                .multiline => |str| try writer.print("multiline: {s}", .{str}),
            }
        }
    };

    keyeq: String, // key =
    value: String, // value
    table_start, // table start
    table_end,

    // just for pretty printing when debugging lol
    pub fn format(self: Token, comptime _: []const u8, options: fmt.FormatOptions, writer: anytype) !void {
        switch (self) {
            .keyeq => |str| try writer.print("key {{{s}}} =", .{str}),
            .value => |str| try writer.print("value {{{s}}};", .{str}),
            .table_start => |str| try writer.print("[", .{}),
            .table_end => |str| try writer.print("]", .{}),
        }
    }
};

pub const Tokenizer = struct {
    const Error = error{
        EmptyKey,
        InvalidChar,
        UnexpectedNewline,
        UnexpectedEof,
    };

    content: []const u8,

    index: usize,
    last: Token,
    finished: bool,

    pub fn init(data: []const u8) Tokenizer {
        return Tokenizer{
            .content = data,

            .index = 0,
            .last = undefined,
            .finished = false,
        };
    }

    fn eof(self: *Tokenizer) Error!void {
        switch (self.last) {
            .table_start => return Error.UnexpectedEof,
            else => self.finished = true,
        }
    }

    fn skipToNewline(self: *Tokenizer) Error!void {
        while (self.content[self.index] != '\n') {
            self.index += 1;
            if (self.index >= self.content.len) {
                return self.eof();
            }
        }
    }

    fn skipPastNewline(self: *Tokenizer) Error!void {
        try self.skipToNewline();
        self.index += 1;
    }

    fn skipWhitespace(self: *Tokenizer) Error!void {
        while (self.index < self.content.len) : (self.index += 1) {
            switch (self.content[self.index]) {
                // actual whitespace
                ' ', '\t', 0x0B, 0x0C, '\r' => continue,
                '#' => return try self.skipToNewline(),
                else => return,
            }
        }
    }

    fn skipWhitespaceAndNewlines(self: *Tokenizer) Error!void {
        while (self.index < self.content.len) : (self.index += 1) {
            switch (self.content[self.index]) {
                // actual whitespace
                ' ', '\t', 0x0B, 0x0C, '\r' => continue,
                '#' => try self.skipPastNewline(),
                '\n' => continue,
                else => return,
            }
        }
        try self.eof();
    }

    fn skipWhitespaceAndNewlinesAndBreaks(self: *Tokenizer) Error!void {
        while (self.index < self.content.len) : (self.index += 1) {
            switch (self.content[self.index]) {
                // actual whitespace
                ' ', '\t', 0x0B, 0x0C, '\r' => continue,
                ';', ',' => continue,
                '#' => try self.skipPastNewline(),
                '\n' => continue,
                else => return,
            }
        }
        try self.eof();
    }

    fn readString(self: *Tokenizer) ![]const u8 {
        const start_index = self.index;
        while (self.index < self.content.len) : (self.index += 1) {
            const char = self.content[self.index];
            switch (char) {
                '\n', '=', ';', ',', ']' => {
                    // remove trailing whitespace
                    // (eg key = value)
                    var end_index = self.index - 1;
                    while (end_index > start_index) {
                        switch (self.content[end_index]) {
                            ' ', '\t', 0x0B, 0x0C, '\r' => end_index -= 1,
                            else => return self.content[start_index .. end_index + 1],
                        }
                    }
                    return error.EmptyKey;
                },
                else => continue,
            }
        }

        // EOF
        try self.eof();
        return self.content[start_index..self.index];
    }

    fn readQuotedString(self: *Tokenizer, comptime quote: u8) Error![]const u8 {
        const start_index = self.index;

        self.index += 1;

        while (self.index < self.content.len) : (self.index += 1) {
            const char = self.content[self.index];
            switch (char) {
                quote => {
                    self.index += 1;
                    return self.content[start_index..self.index];
                },
                '\n' => return error.UnexpectedNewline,
                '\\' => self.index += 1,
                else => continue,
            }
        }

        // EOF before closing bracket
        return error.UnexpectedEof;
    }

    fn readMultilineString(self: *Tokenizer) Error![]const u8 {
        const start_index = self.index;

        const State = enum { root, line };
        var state = State.root;

        while (self.index < self.content.len) : (self.index += 1) {
            switch (state) {
                .root => {
                    const end_index = self.index;
                    try self.skipWhitespace();
                    switch (self.content[self.index]) {
                        '|' => state = State.line,
                        else => return self.content[start_index..end_index],
                    }
                },
                .line => {
                    switch (self.content[self.index]) {
                        '\n' => state = State.root,
                        else => continue,
                    }
                },
            }
        }

        // empty line or EOF
        return self.content[start_index..self.index];
    }

    pub fn next(self: *Tokenizer) Error!?Token {
        if (!self.finished) {
            self.last = ret: {
                // treat ; and , as whitespace
                try self.skipWhitespaceAndNewlinesAndBreaks();
                if (self.finished) return null;
                const char = self.content[self.index];
                switch (char) {
                    '[' => break :ret Token.table_start,
                    ']' => break :ret Token.table_end,
                    '=' => return error.InvalidChar,
                    else => {
                        var value: Token.String = switch (char) {
                            '"' => Token.String{ .quoted = try self.readQuotedString('"') },
                            '\'' => Token.String{ .quoted = try self.readQuotedString('\'') },
                            '|' => Token.String{ .multiline = try self.readMultilineString() },
                            else => Token.String{ .unquoted = try self.readString() },
                        };
                        try self.skipWhitespaceAndNewlines();
                        if (!self.finished) {
                            switch (self.content[self.index]) {
                                ';', ',' => break :ret Token{ .value = value },
                                '=' => break :ret Token{ .keyeq = value },
                                ']' => {
                                    self.index -= 1;
                                    break :ret Token{ .value = value };
                                },
                                else => {
                                    self.index -= 1;
                                    break :ret Token{ .value = value };
                                },
                            }
                        } else break :ret Token{ .value = value };
                    },
                }
            };
            self.index += 1;
            if (self.index >= self.content.len) try self.eof();
            return self.last;
        } else {
            return null;
        }
    }
};

// just a namespace to put parsing-related functions in,
// rather than having them scattered about in the top-level.
pub const Parser = struct {
    const MemError = error{
        OutOfMemory,
    };

    const StringError = error{
        UnexpectedNewline,
        InvalidEscapeChar,
        InvalidHexEscape,
        InvalidUnicodeEscape,
    };

    fn skipWhitespace(str: []const u8, i: *usize) void {
        while (i.* < str.len) : (i.* += 1) {
            switch (str[i.*]) {
                ' ', '\t', 0x0B, 0x0C, '\r' => continue,
                else => return,
            }
        }
    }

    fn parseMultilineString(str: []const u8, a: *mem.Allocator) MemError![]const u8 {
        const State = enum { root, line };

        var state = State.line;
        var buf = std.ArrayList(u8).init(a);
        defer buf.deinit();

        var i: usize = 0;
        debug.assert(str[i] == '|');
        i += 1; // skip initial pipe

        while (i < str.len) : (i += 1) {
            switch (state) {
                .root => {
                    skipWhitespace(str, &i);
                    switch (str[i]) {
                        '|' => state = State.line,
                        else => return buf.toOwnedSlice(),
                    }
                },
                .line => {
                    const char = str[i];
                    switch (char) {
                        '\\' => {
                            const next_is_newline_or_eof = if (i + 1 < str.len) str[i + 1] == '\n' else true;
                            if (next_is_newline_or_eof) {
                                if (str[i - 1] == '\\') continue;
                                i += 1; // skip over newline
                                state = State.root;
                            } else {
                                try buf.append('\\');
                            }
                        },
                        '\n' => {
                            try buf.append('\n');
                            state = State.root;
                        },
                        else => try buf.append(char),
                    }
                },
            }
        }

        // this shouldn't happen; the tokenizer should've caught and errored on this
        return buf.toOwnedSlice();
    }

    // ~~stolen~~ inspired by std/zig/string_literal.zig
    fn parseQuotedString(str: []const u8, a: *mem.Allocator, comptime quote: u8) (MemError || StringError)![]const u8 {
        const State = enum { root, backslash };

        var state = State.root;
        var buf = std.ArrayList(u8).init(a);
        defer buf.deinit();

        var i: usize = 0;
        i += 1;

        while (i < str.len) : (i += 1) {
            const char = str[i];
            switch (state) {
                .root => switch (char) {
                    '\n' => return error.UnexpectedNewline,
                    '\\' => state = State.backslash,
                    quote => {
                        i += 1;
                        return buf.toOwnedSlice();
                    },
                    else => {
                        try buf.append(char);
                    },
                },
                .backslash => switch (char) {
                    'f' => {
                        try buf.append(0x0C);
                        state = State.root;
                    },
                    'n' => {
                        try buf.append('\n');
                        state = State.root;
                    },
                    'r' => {
                        try buf.append('\r');
                        state = State.root;
                    },
                    't' => {
                        try buf.append('\t');
                        state = State.root;
                    },
                    'v' => {
                        try buf.append(0x0B);
                        state = State.root;
                    },
                    '\\', '"', '\'' => {
                        try buf.append(char);
                        state = State.root;
                    },
                    'x' => {
                        if (str.len < i + 3) return error.InvalidHexEscape;
                        if (fmt.parseUnsigned(u8, str[i + 1 .. i + 3], 16)) |byte| {
                            try buf.append(byte);
                            state = State.root;
                            i += 2;
                        } else |err| {
                            return error.InvalidHexEscape;
                        }
                    },
                    'u' => {
                        if (str.len < i + 5) return error.InvalidUnicodeEscape;
                        if (fmt.parseUnsigned(u16, str[i + 1 .. i + 5], 16)) |unic| {
                            try buf.appendSlice(mem.toBytes(unic)[0..]);
                            state = State.root;
                            i += 4;
                        } else |err| {
                            return error.InvalidUnicodeEscape;
                        }
                    },
                    'U' => {
                        if (str.len < i + 9) return error.InvalidUnicodeEscape;
                        if (fmt.parseUnsigned(u32, str[i + 1 .. i + 9], 16)) |unic| {
                            try buf.appendSlice(mem.toBytes(unic)[0..]);
                            state = State.root;
                            i += 8;
                        } else |err| {
                            return error.InvalidUnicodeEscape;
                        }
                    },
                    else => return error.InvalidEscapeChar,
                },
            }
        }

        // unclosed string
        // the tokenizer should've caught this
        return buf.toOwnedSlice();
    }

    fn parseString(tok: Token.String, a: *mem.Allocator) (MemError || StringError)![]const u8 {
        switch (tok) {
            .unquoted => |str| return a.dupe(u8, str),
            .quoted => |str| return switch (str[0]) {
                '"' => parseQuotedString(str, a, '"'),
                '\'' => parseQuotedString(str, a, '\''),
                else => @panic("unknown quote?"),
            },
            .multiline => |str| return parseMultilineString(str, a),
        }
    }

    pub fn parseTable(tokens: *Tokenizer, a: *mem.Allocator) (MemError || StringError || Tokenizer.Error)!Table {
        var table = Table.init(a);
        var key: ?Key = null;
        var value: ?Value = null;
        var i: usize = 0;

        while (try tokens.next()) |token| {
            switch (token) {
                .table_end => return table,
                .keyeq => |str| key = try parseString(str, a),
                .table_start => value = .{ .table = try parseTable(tokens, a) },
                .value => |str| value = .{ .string = try parseString(str, a) },
            }

            if (value) |val| {
                const keystr = key orelse key: {
                    defer i += 1;
                    break :key try fmt.allocPrint(a, "{d}", .{i});
                };
                try table.put(keystr, val);
                value = null;
                key = null;
            }
        }

        // if no table end, return the table anyway;
        // any unclosed tables should've been caught by the tokenizer
        return table;
    }

    // aka 'put that shit in a hashmap please'
    pub fn parse(data: []const u8, a: *mem.Allocator) !Table {
        var tokenizer = Tokenizer.init(data);
        return parseTable(&tokenizer, a);
    }
};


const testing = std.testing;
const alloc = testing.allocator;

// TODO: more tests
test "key = [ table ]" {
    const zckt =
        \\table = [ this is a table :), this is the second value ]
    ;

    var table = try Parser.parse(zckt, alloc);
    defer table.deinit();

    testing.expectEqualStrings(table.getTable("table").?.getString("1").?, "this is the second value");
}

test "duplicate keys overwrite" {
    const zckt =
        \\key = this is a value
        \\key = haha! overwritten
    ;

    var table = try Parser.parse(zckt, alloc);
    defer table.deinit();

    testing.expectEqualStrings(table.getString("key").?, "haha! overwritten");
}

test "multiline string" {
    const zckt =
        \\multiline string =
        \\  |this is a multiline string
        \\  |that means it can span multiple lines
        \\  |however, i can also make \
        \\  |part of it span one line!
    ;

    const multiline_string =
        \\this is a multiline string
        \\that means it can span multiple lines
        \\however, i can also make part of it span one line!
    ;

    var table = try Parser.parse(zckt, alloc);
    defer table.deinit();

    testing.expectEqualStrings(table.getString("multiline string").?, multiline_string);
}

D static/prism-ghcolors.css => static/prism-ghcolors.css +0 -122
@@ 1,122 0,0 @@
/**
 * GHColors theme by Avi Aryan (http://aviaryan.in)
 * Inspired by Github syntax coloring
 */

code[class*="language-"],
pre[class*="language-"] {
	color: #393A34;
	font-family: "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
	direction: ltr;
	text-align: left;
	white-space: pre;
	word-spacing: normal;
	word-break: normal;
	font-size: .9em;
	line-height: 1.2em;

	-moz-tab-size: 4;
	-o-tab-size: 4;
	tab-size: 4;

	-webkit-hyphens: none;
	-moz-hyphens: none;
	-ms-hyphens: none;
	hyphens: none;
}

pre > code[class*="language-"] {
	font-size: 1em;
}

pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
	background: #b3d4fc;
}

pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
	background: #b3d4fc;
}

/* Code blocks */
pre[class*="language-"] {
	padding: 1em;
	margin: .5em 0;
	overflow: auto;
	border: 1px solid #dddddd;
	background-color: white;
}

/* Inline code */
:not(pre) > code[class*="language-"] {
	padding: .2em;
	padding-top: 1px;
	padding-bottom: 1px;
	background: #f8f8f8;
	border: 1px solid #dddddd;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
	color: #999988;
	font-style: italic;
}

.token.namespace {
	opacity: .7;
}

.token.string,
.token.attr-value {
	color: #e3116c;
}

.token.punctuation,
.token.operator {
	color: #393A34; /* no highlight */
}

.token.entity,
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.property,
.token.regex,
.token.inserted {
	color: #36acaa;
}

.token.atrule,
.token.keyword,
.token.attr-name,
.language-autohotkey .token.selector {
	color: #00a4db;
}

.token.function,
.token.deleted,
.language-autohotkey .token.tag {
	color: #9a050f;
}

.token.tag,
.token.selector,
.language-autohotkey .token.keyword {
	color: #00009f;
}

.token.important,
.token.function,
.token.bold {
	font-weight: bold;
}

.token.italic {
	font-style: italic;
}

D static/prism-gruvbox-dark.css => static/prism-gruvbox-dark.css +0 -144
@@ 1,144 0,0 @@
/**
 * Gruvbox dark theme
 *
 * Adapted from a theme based on:
 * Vim Gruvbox dark Theme (https://github.com/morhetz/gruvbox)
 *
 * @author Azat S. <to@azat.io>
 * @version 1.0
 */
@media (prefers-color-scheme: dark) {
    code[class*="language-"],
    pre[class*="language-"] {
    	color: #ebdbb2;
    	font-family: Consolas, Monaco, "Andale Mono", monospace;
    	direction: ltr;
    	text-align: left;
    	white-space: pre;
    	word-spacing: normal;
    	word-break: normal;
    	line-height: 1.5;

    	-moz-tab-size: 4;
    	-o-tab-size: 4;
    	tab-size: 4;

    	-webkit-hyphens: none;
    	-moz-hyphens: none;
    	-ms-hyphens: none;
    	hyphens: none;
    }

    pre[class*="language-"]::-moz-selection,
    pre[class*="language-"] ::-moz-selection,
    code[class*="language-"]::-moz-selection,
    code[class*="language-"] ::-moz-selection {
    	color: #fff;
    	background: #f05e23;
    }

    pre[class*="language-"]::selection,
    pre[class*="language-"] ::selection,
    code[class*="language-"]::selection,
    code[class*="language-"] ::selection {
    	color: #fff;
    	background: #f05e23;
    }

    /* Code blocks */
    pre[class*="language-"] {
    	padding: 1em;
    	margin: 0.5em 0;
    	overflow: auto;
    }

    :not(pre) > code[class*="language-"],
    pre[class*="language-"] {
    	background: #1d2021;
    }

    /* Inline code */
    :not(pre) > code[class*="language-"] {
    	padding: 0.1em;
    	border-radius: 0.3em;
    }

    .token.comment,
    .token.prolog,
    .token.cdata {
    	color: #a89984;
    }

    .token.delimiter,
    .token.boolean,
    .token.keyword,
    .token.selector,
    .token.important,
    .token.atrule {
    	color: #fb4934;
    }

    .token.operator,
    .token.punctuation,
    .token.attr-name {
    	color: #a89984;
    }

    .token.tag,
    .token.tag .punctuation,
    .token.doctype,
    .token.builtin {
    	color: #fabd2f;
    }

    .token.entity,
    .token.number,
    .token.symbol {
    	color: #d3869b;
    }

    .token.property,
    .token.constant,
    .token.variable {
    	color: #fb4934;
    }

    .token.string,
    .token.char {
    	color: #b8bb26;
    }

    .token.attr-value,
    .token.attr-value .punctuation {
    	color: #a89984;
    }

    .token.url {
    	color: #b8bb26;
    	text-decoration: underline;
    }

    .token.function {
    	color: #fabd2f;
    }

    .token.regex {
    	background: #b8bb26;
    }

    .token.bold {
    	font-weight: bold;
    }

    .token.italic {
    	font-style: italic;
    }

    .token.inserted {
    	background: #a89984;
    }

    .token.deleted {
    	background: #fb4934;
    }
}

M static/prism.css => static/prism.css +33 -46
@@ 1,39 1,28 @@
/* PrismJS 1.23.0
https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript+json+json5+toml+xml-doc+yaml+zig */
/*
 Solarized Color Schemes originally by Ethan Schoonover
 http://ethanschoonover.com/solarized

 Ported for PrismJS by Hector Matos
 Website: https://krakendev.io
 Twitter Handle: https://twitter.com/allonsykraken)
*/
https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript+json+json5+toml+xml-doc+yaml+zig

/*
SOLARIZED HEX
--------- -------
base03    #002b36
base02    #073642
base01    #586e75
base00    #657b83
base0     #839496
base1     #93a1a1
base2     #eee8d5
base3     #fdf6e3
yellow    #b58900
orange    #cb4b16
red       #dc322f
magenta   #d33682
violet    #6c71c4
blue      #268bd2
cyan      #2aa198
green     #859900
pink_panther
modified from the above
*/

:root {
    --prism-bg: #FCEADE;
    --prism-fg: #32292F;

    --black:    #FCEADE;
    --pink:     #F374AE;
    --green:    #465C69;
    --yellow:   #DC851F;
    --blue:     #386FA4;
    --magenta:  #7E1946;
    --cyan:     #297373;
    --white:    #32292F;
}

code[class*="language-"],
pre[class*="language-"] {
	color: #657b83; /* base00 */
	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
	color: var(--prism-fg);
	font-family: 'Go Mono', 'Ubuntu Mono', monospace;
	font-size: 1em;
	text-align: left;
	white-space: pre;


@@ 55,12 44,14 @@ pre[class*="language-"] {

pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
	background: #073642; /* base02 */
	background: var(--prism-bg);
}

pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
	background: #073642; /* base02 */
	font-weight: bold;
	background: var(--pink);
	color: var(--prism-fg);
}

/* Code blocks */


@@ 68,12 59,11 @@ pre[class*="language-"] {
	padding: 1em;
	margin: .5em 0;
	overflow: auto;
	border-radius: 0.3em;
}

:not(pre) > code[class*="language-"],
pre[class*="language-"] {
	background-color: #fdf6e3; /* base3 */
	background-color: var(--black);
}

/* Inline code */


@@ 86,11 76,8 @@ pre[class*="language-"] {
.token.prolog,
.token.doctype,
.token.cdata {
	color: #93a1a1; /* base1 */
}

.token.punctuation {
	color: #586e75; /* base01 */
	color: var(--green);
	font-style: italic;
}

.token.namespace {


@@ 104,7 91,7 @@ pre[class*="language-"] {
.token.constant,
.token.symbol,
.token.deleted {
	color: #268bd2; /* blue */
	color: var(--pink);
}

.token.selector,


@@ 114,29 101,29 @@ pre[class*="language-"] {
.token.builtin,
.token.url,
.token.inserted {
	color: #2aa198; /* cyan */
	color: var(--cyan);
}

.token.operator,
.token.entity {
	color: #657b83; /* base00 */
	background: #eee8d5; /* base2 */
	color: var(--yellow);
}

.token.atrule,
.token.attr-value,
.token.keyword {
	color: #859900; /* green */
	color: var(--magenta);
}

.token.function,
.token.class-name {
	color: #b58900; /* yellow */
	color: var(--magenta);
}

.token.regex,
.token.important,
.token.variable {
	color: #cb4b16; /* orange */
	color: var(--cyan);
}

.token.important,

M static/stylesheet.css => static/stylesheet.css +18 -14
@@ 1,43 1,47 @@
/* pink_panther */
:root {
    --bg: white;
    --fg: black;
    --link: lightcoral;
    --bg: #FCEADE;
    --fg: #32292F;
    --link: #F374AE;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: black;
        --fg: white;
        --bg: #32292F;
        --fg: #FCEADE;
    }
}

body {
    /* fonts and colours */
    font-family: "Bitstream Charter", "Palatino", "Georgia", serif;
    font-family: "Go Mono", monospace;
    background-color: var(--bg);
    color: var(--fg);

    /* positioning */
    line-height: 1.5;
    max-width: 400px;
    max-width: 500px;
    padding: 2em;
    margin: 0 auto;
}

a {
    text-decoration: none;
    background-color: var(--bg);
    color: var(--link);
}

a:hover {
    font-weight: bold;
    color: var(--fg);
    background-color: var(--link);
    color: var(--bg);
}

h1, p {
    margin: 5px;
    color: var(--fg);
h1,h2 {
    font-weight: bold;
    margin-bottom: 0px;
}

p,ul {
    margin-top: 5px;
    margin-bottom: 5px;
}

ul {