28 files changed,
1257 insertions(+),
0 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2025-11-30 23:06:47 +0200
Change ID:
okuuuswpmurqqnqzurtxyknpntlrkwrq
jump to
A
.github/workflows/ci.yml
@@ -0,0 +1,46 @@
+name: ci + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + name: Build and Push + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go/go.mod + cache-dependency-path: go/go.mod + + - name: Install nvim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: nightly + + - name: build markdown parser + run: make build-parser + + - name: tests + run: | + nvim --version + make test + + - name: build + run: make build + + - name: publish + uses: s0/git-publish-subdir-action@develop + env: + REPO: self + BRANCH: gh-pages + FOLDER: build + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MESSAGE: "{msg}"
A
LICENSE
@@ -0,0 +1,26 @@
+ GLWTS(Good Luck With That Shit) Public License + Copyright (c) Every-fucking-one, except the Author + +Everyone is permitted to copy, distribute, modify, merge, sell, publish, +sublicense or whatever the fuck they want with this software but at their +OWN RISK. + + Preamble + +The author has absolutely no fucking clue what the code in this project +does. It might just fucking work or not, there is no third option. + + + GOOD LUCK WITH THAT SHIT PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION + + 0. You just DO WHATEVER THE FUCK YOU WANT TO as long as you NEVER LEAVE +A FUCKING TRACE TO TRACK THE AUTHOR of the original product to blame for +or hold responsible. + +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Good luck and Godspeed.
A
assets/favicon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> + <text y=".99em" font-size="205">🌀</text> +</svg>
A
go/go.mod
@@ -0,0 +1,13 @@
+module olexsmir.xyz + +go 1.25.3 + +require ( + github.com/alecthomas/chroma/v2 v2.20.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/wyatt915/goldmark-treeblood v0.0.1 // indirect + github.com/wyatt915/treeblood v0.1.16 // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect +)
A
go/go.sum
@@ -0,0 +1,28 @@
+github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs= +github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208= +github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y= +github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A
go/main.go
@@ -0,0 +1,76 @@
+package main + +// #include <stdlib.h> +import "C" + +import ( + "bytes" + "unsafe" + + "github.com/alecthomas/chroma/v2/formatters" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/styles" + treeblood "github.com/wyatt915/goldmark-treeblood" + "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting/v2" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/renderer/html" + callout "gitlab.com/staticnoise/goldmark-callout" +) + +func main() {} + +//export md_to_html +func md_to_html(input *C.char) *C.char { + if input == nil { + return C.CString("") + } + + inp := C.GoString(input) + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, + highlighting.NewHighlighting( + highlighting.WithFormatOptions( + chromahtml.Standalone(false), + chromahtml.WithClasses(true), + ), + ), + extension.NewFootnote( + extension.WithFootnoteIDPrefix([]byte("footnote")), + ), + treeblood.MathML(), + callout.CalloutExtention, + ), + goldmark.WithRendererOptions(html.WithUnsafe()), + ) + + var buf bytes.Buffer + if err := md.Convert([]byte(inp), &buf); err != nil { + return C.CString("") + } + + return C.CString(buf.String()) +} + +//export chroma_css +func chroma_css(theme *C.char) *C.char { + if theme == nil { + return C.CString("") + } + thm := C.GoString(theme) + + var buf bytes.Buffer + formatter := formatters.Get("html").(*chromahtml.Formatter) + if err := formatter.WriteCSS(&buf, styles.Get(thm)); err != nil { + return C.CString("") + } + return C.CString(buf.String()) +} + +//export free_cstring +func free_cstring(s *C.char) { + if s != nil { + C.free(unsafe.Pointer(s)) + } +}
A
lua/lego/css.lua
@@ -0,0 +1,87 @@
+local css = {} + +---@param str string +---@return string +local function to_kebab_case(str) + str = str:gsub("_", "-") + str = str:gsub("([a-z])([A-Z])", "%1-%2"):lower() -- Convert camelCase to kebab-case + return str +end + +---@param value string|number +---@return string +local function value_to_css(value) + if type(value) == "number" then + return tostring(value) + end + return string.format("%s", value) +end + +local function render_properties(properties) + local parts = {} + local keys = {} + for key in pairs(properties) do + table.insert(keys, key) + end + table.sort(keys) + + for _, key in ipairs(keys) do + local value = properties[key] + table.insert(parts, string.format("%s:%s", to_kebab_case(key), value_to_css(value))) + end + + return table.concat(parts, ";") +end + +local function flatten_css_rules(rules) + local all_rules = {} + + local function process_rule(sel, props, prefix) + local full_sel + if sel:find "^&" then + full_sel = prefix .. sel:gsub("^&", "") + else + full_sel = prefix and (prefix .. " " .. sel) or sel + end + local flat_props = {} + for k, v in pairs(props) do + if type(v) == "table" then + process_rule(k, v, full_sel) + else + flat_props[k] = v + end + end + if next(flat_props) then + all_rules[full_sel] = flat_props + end + end + + for sel, props in pairs(rules) do + process_rule(sel, props, nil) + end + + return all_rules +end + +---@param rules table +---@return string +function css.style(rules) + local all_rules = flatten_css_rules(rules) + + local selectors = {} + for s in pairs(all_rules) do + table.insert(selectors, s) + end + table.sort(selectors) + + local rule_parts = {} + for _, sel in ipairs(selectors) do + local props = all_rules[sel] + local props_str = render_properties(props) .. ";" + table.insert(rule_parts, string.format("%s{%s}", sel, props_str)) + end + + return table.concat(rule_parts, "") +end + +return css
A
lua/lego/date.lua
@@ -0,0 +1,9 @@
+local date = {} + +---@param d string +---@return string +function date.date(d) + return d .. "T00:00:00+02:00" +end + +return date
A
lua/lego/file.lua
@@ -0,0 +1,86 @@
+local file = {} +local _writes = {} + +function file.report_duplicates() + local freq = {} + local duplicates = {} + + for _, v in ipairs(_writes) do + freq[v] = (freq[v] or 0) + 1 + end + + for value, count in pairs(freq) do + if count > 1 then + duplicates[#duplicates + 1] = value + end + end + + if #duplicates > 0 then + vim.print("duplicates " .. vim.inspect(duplicates)) + end +end + +---@alias lego.FilePath string|string[] + +---@param p lego.FilePath +---@return string +function file.to_path(p) + if type(p) == "table" then + return vim.fs.joinpath(unpack(p)) + end + return p +end + +---@param path lego.FilePath +---@return string[] +function file.list_dir(path) + return vim.fn.readdir(file.to_path(path)) +end + +---@param path lego.FilePath +function file.read(path) + return vim.fn.readfile(file.to_path(path)) +end + +---@param path lego.FilePath +---@param content string +function file.write(path, content) + path = file.to_path(path) + table.insert(_writes, path) + + vim.print("writing " .. path) + vim.fn.writefile(vim.split(content, "\n", { plain = true }), path) +end + +---@param path lego.FilePath +function file.rm(path) + path = file.to_path(path) + vim.print("deleting " .. path) + vim.fs.rm(path, { force = true, recursive = true }) +end + +---@param path string +function file.mkdir(path) + vim.fn.mkdir(file.to_path(path), "p") +end + +---@param path string +function file.is_dir(path) + local build_dir_stats = vim.uv.fs_stat(file.to_path(path)) + return not build_dir_stats or build_dir_stats.type == "directory" +end + +---@param from string +---@param to string +function file.copy_dir(from, to) + from = file.to_path(from) + to = file.to_path(to) + vim.print("copying " .. to) + + file.mkdir(to) + for _, f in ipairs(vim.fn.readdir(from)) do + vim.uv.fs_copyfile(vim.fs.joinpath(from, f), vim.fs.joinpath(to, f)) + end +end + +return file
A
lua/lego/frontmatter.lua
@@ -0,0 +1,46 @@
+local frontmatter = {} + +---@param lines string[] +---@return table +function frontmatter.extract(lines) + if lines[1] ~= "---" then + return {} + end + + for i = 2, #lines do + if lines[i] == "---" then + local frontmatter_lines = { unpack(lines, 2, i - 1) } + + local result = {} + for _, line in ipairs(frontmatter_lines) do + local key, value = line:match "^%s*(.-)%s*=%s*(.-)%s*$" + if key and value then + result[key] = value + end + end + + return result + end + end + + return {} +end + +---@param lines string[] +---@return string[]|nil +function frontmatter.content(lines) + if lines[1] ~= "---" then + return lines + end + + return vim + .iter(lines) + :skip(1) + :skip(function(el) + return el ~= "---" + end) + :skip(1) + :totable() +end + +return frontmatter
A
lua/lego/html/attribute.lua
@@ -0,0 +1,27 @@
+local hattribute = {} + +---@class lego.HtmlAttribute +---@field [string] string + +---@param attribute string +---@param value string +---@return lego.HtmlAttribute +function hattribute.attr(attribute, value) + return { [attribute] = value } +end + +-- COMMON ATTRIBUTES +-- stylua: ignore start + +---@param class string +function hattribute.class(class) return hattribute.attr("class", class) end + +---@param link string +function hattribute.href(link) return hattribute.attr("href", link) end + +---@param id string +function hattribute.id(id) return hattribute.attr("id", id) end + +-- stylua: ignore end + +return hattribute
A
lua/lego/html/init.lua
@@ -0,0 +1,175 @@
+local a = require "lego.html.attribute" +local html = {} + +---@alias lego.HtmlNode lego._HtmlNote|string + +---@class lego._HtmlNote +---@field tag string +---@field attributes lego.HtmlAttribute[] +---@field children lego._HtmlNote[] + +---@param tag string +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +---@return lego.HtmlNode +function html.el(tag, attributes, children) + local attrs = {} + for _, attr_table in ipairs(attributes or {}) do + for k, v in pairs(attr_table) do + attrs[k] = v + end + end + + return { + tag = tag, + attributes = attrs, + children = children or {}, + } +end + +---@param text string +---@return lego.HtmlNode +function html.text(text) + return text +end + +---@param html_str string +---@return lego.HtmlNode +function html.raw(html_str) + return html_str +end + +---@param txts string[] +---@return string[] +function html.tt(txts) + local tt = vim + .iter(txts) + :map(function(txt) + return { txt, " " } + end) + :flatten() + :totable() + + if tt[#tt] == " " then + table.remove(tt, #tt) + end + + return tt +end + +local _self_closing_tags = { + area = {}, + base = {}, + br = {}, + col = {}, + embed = {}, + hr = {}, + img = {}, + input = {}, + -- link = {}, -- ignored because it needed for rss + meta = {}, + source = {}, + track = {}, + wbr = {}, +} + +---@param node lego.HtmlNode +---@return string +function html.render(node) + if type(node) == "string" then + return node + elseif type(node) == "table" and node.tag then + local attr_keys = {} + for k in pairs(node.attributes or {}) do + table.insert(attr_keys, k) + end + table.sort(attr_keys) + + local attrs_str = "" + for _, v in pairs(attr_keys) do + attrs_str = attrs_str .. string.format(' %s="%s"', v, node.attributes[v]) + end + + if _self_closing_tags[node.tag] then + return string.format("<%s%s>", node.tag, attrs_str) + end + + local children_str = "" + for _, child in ipairs(node.children or {}) do + children_str = children_str .. html.render(child) + end + + return string.format("<%s%s>%s</%s>", node.tag, attrs_str, children_str, node.tag) + end + return "" +end + +function html.render_page(node) + return "<!DOCTYPE html>" .. html.render(node) +end + +-- --- COMMON ELEMENTS +-- stylua: ignore start + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.div(attributes, children) return html.el("div", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.main(attributes, children) return html.el("main", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +function html.meta(attributes) return html.el("meta", attributes, {}) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.span(attributes, children) return html.el("span", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.p(attributes, children) return html.el("p", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.a(attributes, children) return html.el("a", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.ul(attributes, children) return html.el("ul", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.li(attributes, children) return html.el("li", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.title(attributes, children) return html.el("title", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +function html.link(attributes) return html.el("link", attributes, {}) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.h1(attributes, children) return html.el("h1", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.h2(attributes, children) return html.el("h2", attributes, children) end + +---@param attributes lego.HtmlAttribute[] +---@param children lego.HtmlNode[] +function html.nav(attributes, children) return html.el("nav", attributes, children) end + +function html.br() return html.el("br", {}, {}) end + +---@param datetime string +function html.time(datetime) + return html.el("time", + { a.attr("datetime", datetime) }, + { html.text(datetime)}) +end + +-- stylua: ignore end + +return html
A
lua/lego/post.lua
@@ -0,0 +1,43 @@
+local file = require "lego.file" +local frontmatter = require "lego.frontmatter" +local liblego = require "liblego" +local post = {} + +---@class lego.Post +---@field content string +---@field hidden boolean +---@field meta lego.PostMeta + +---@class lego.PostMeta +---@field title string +---@field date string +---@field slug string +---@field desc string + +---@param fpath lego.FilePath +---@return lego.Post +function post.read_file(fpath) + local p = file.read(fpath) + local content = table.concat(frontmatter.content(p) or {}, "\n") + local meta = frontmatter.extract(p) + local hidden = meta["hidden"] == "true" + assert(meta["title"] ~= nil, (file.to_path(fpath) .. " doesn't have title")) + assert(meta["date"] ~= nil, (file.to_path(fpath) .. " doesn't have date")) + assert(meta["slug"] ~= nil, (file.to_path(fpath) .. " doesn't have slug")) + + return { + meta = meta, + hidden = hidden, + content = liblego.md_to_html(content), + } +end + +---MUTATES THE TABLE +---@param posts lego.Post[] +function post.sort_by_date(posts) + table.sort(posts, function(a, b) + return a.meta.date > b.meta.date + end) +end + +return post
A
lua/lego/rss.lua
@@ -0,0 +1,59 @@
+local a = require "lego.html.attribute" +local formatDate = require("lego.date").date +local h = require "lego.html" +local rss = {} + +function rss.escape_html(html) + local map = { + ["&"] = "&", + ["<"] = "<", + [">"] = ">", + ['"'] = """, + ["'"] = "'", + } + + html = html:gsub("[%z\1-\8\11-\12\14-\31]", "") -- remove control chars + return (html:gsub("[&<>\"']", function(c) + return map[c] + end)) +end + +---@param config {feed_url:string, home_url:string, title:string, name:string, email:string, subtitle:string} +---@param posts lego.Post[] +---@return string +function rss.rss(posts, config) + local entries = vim + .iter(posts) + ---@param post lego.Post + :map(function(post) + return h.el("entry", {}, { + h.title({}, { h.text(post.meta.title) }), + h.link { a.href(config.home_url .. "/" .. post.meta.slug) }, + h.el("id", {}, { h.text(config.home_url .. "/" .. post.meta.slug) }), + h.el("updated", {}, { h.text(formatDate(post.meta.date)) }), + h.el("content", { a.attr("type", "html") }, { h.raw(rss.escape_html(post.content)) }), + }) + end) + :totable() + + return [[<?xml version="1.0" encoding="utf-8"?>]] + .. h.render(h.el("feed", { a.attr("xmlns", "http://www.w3.org/2005/Atom") }, { + h.title({}, { h.text(config.title) }), + h.el("subtitle", {}, { h.text(config.subtitle) }), + h.el("id", {}, { h.text(config.home_url .. "/") }), + h.link { a.href(config.home_url), a.attr("rel", "alternate") }, + h.link { + a.href(config.feed_url), + a.attr("rel", "self"), + a.attr("type", "application/atom+xml"), + }, + h.el("updated", {}, { h.text(formatDate(posts[1].meta.date)) }), + h.el("author", {}, { + h.el("name", {}, { h.text(config.name) }), + h.el("email", {}, { h.text(config.email) }), + }), + unpack(entries), + })) +end + +return rss
A
lua/lego/sitemap.lua
@@ -0,0 +1,44 @@
+local a = require "lego.html.attribute" +local formatDate = require("lego.date").date +local h = require "lego.html" +local sitemap = {} + +---@param opts {url:string, date:string, priority: string} +local function url(opts) + return h.el("url", {}, { + h.el("loc", {}, { h.text(opts.url) }), + h.el("lastmod", {}, { h.text(formatDate(opts.date)) }), + h.el("priority", {}, { h.text(opts.priority) }), + }) +end + +---@param posts lego.Post[] +---@param config {site_url:string} +---@return string +function sitemap.sitemap(posts, config) + local urls = vim + .iter(posts) + ---@param post lego.Post + :map(function(post) + return url { + url = config.site_url .. "/" .. post.meta.slug, + date = post.meta.date, + priority = "0.80", + } + end) + :totable() + + return h.render(h.el("urlset", { + a.attr("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9"), + a.attr("xmlns:xhtml", "http://www.w3.org/1999/xhtml"), + }, { + url { + url = config.site_url, + date = posts[1].meta.date, + priority = "1.0", + }, + unpack(urls), + })) +end + +return sitemap
A
lua/liblego.lua
@@ -0,0 +1,39 @@
+local ffi = require "ffi" + +ffi.cdef [[ + void free_cstring(char* s); + char* md_to_html(const char* input); + char* chroma_css(const char* theme); +]] + +local lib = ffi.load "./go/liblego.so" + +local M = {} + +---@param markdown string +---@return string +function M.md_to_html(markdown) + local result = lib.md_to_html(markdown) + local html = ffi.string(result) + lib.free_cstring(result) + if html == "" then + error "failed, good luck" + end + return html +end + +---@param theme string +---@return string +function M.get_css(theme) + local result = lib.chroma_css(theme) + local css = ffi.string(result) + if css == "" then + error "failed, good luck" + end + + css = css:gsub("/%*.-%*/ ", "") + css = css:gsub("\n$", "") + return css +end + +return M
A
lua/minit.lua
@@ -0,0 +1,49 @@
+local function if_test(fn) + if vim.env.TEST == "true" then + fn() + end +end + +local function root(p) + local f = debug.getinfo(1, "S").source:sub(2) + return vim.fn.fnamemodify(f, ":p:h:h") .. (p or "") +end + +local function install_plug(plugin) + local name = plugin:match ".*/(.*)" + local package_root = root ".tests/site/pack/deps/start/" + if not vim.uv.fs_stat(package_root .. name) then + print("Installing " .. plugin) + vim + .system({ + "git", + "clone", + "--depth=1", + "https://github.com/" .. plugin .. ".git", + package_root .. "/" .. name, + }) + :wait() + end +end + +if_test(function() + install_plug "echasnovski/mini.test" +end) + +vim.env.XDG_CONFIG_HOME = root ".tests/config" +vim.env.XDG_DATA_HOME = root ".tests/data" +vim.env.XDG_STATE_HOME = root ".tests/state" +vim.env.XDG_CACHE_HOME = root ".tests/cache" + +vim.opt.runtimepath:append(root()) +vim.opt.packpath:append(root ".tests/site") + +if_test(function() + require("mini.test").setup { + collect = { + find_files = function() + return vim.fn.globpath("lua/tests", "**/*_test.lua", true, true) + end, + }, + } +end)
A
lua/tests/css_test.lua
@@ -0,0 +1,92 @@
+local t = require "tests.testutils" +local _, T, css = t.setup "css" + +local c = require "lego.css" + +css["simple css"] = function() + local rules = { + body = { + margin = 0, + ["font-family"] = "sans-serif", + }, + } + + t.eq(c.style(rules), [[body{font-family:sans-serif;margin:0;}]]) +end + +css["nested-styles"] = function() + local rules = { + body = { + margin = 0, + h1 = { + color = "red", + }, + }, + } + + t.eq(c.style(rules), [[body{margin:0;}body h1{color:red;}]]) +end + +css["camelCase properties"] = function() + local rules = { + [".button"] = { + backgroundColor = "blue", + fontSize = "14px", + }, + } + + t.eq(c.style(rules), [[.button{background-color:blue;font-size:14px;}]]) +end + +css["multiple selectors"] = function() + local rules = { + body = { margin = 0 }, + h1 = { color = "black" }, + } + + t.eq(c.style(rules), [[body{margin:0;}h1{color:black;}]]) +end + +css["deep nesting"] = function() + local rules = { + [".container"] = { + padding = "10px", + [".inner"] = { + margin = 0, + span = { fontWeight = "bold" }, + ["&:hover"] = { color = "blue" }, + }, + }, + } + + t.eq( + c.style(rules), + [[.container{padding:10px;}.container .inner{margin:0;}.container .inner span{font-weight:bold;}.container .inner:hover{color:blue;}]] + ) +end + +css["@media"] = function() + local rules = { + ["@media screen and (min-width: 1200px)"] = { + margin = 0, + }, + } + t.eq(c.style(rules), [[@media screen and (min-width: 1200px){margin:0;}]]) +end + +css[":root"] = function() + local rules = { + [":root"] = { + ["--h1-size"] = "3rem", + }, + + ["@media (true)"] = { + ["--h1-size"] = "2rem", + }, + + h1 = { font_size = "0px" }, + } + t.eq(c.style(rules), [[:root{--h1-size:3rem;}@media (true){--h1-size:2rem;}h1{font-size:0px;}]]) +end + +return T
A
lua/tests/file_test.lua
@@ -0,0 +1,11 @@
+local t = require "tests.testutils" +local _, T, file = t.setup "filee" + +local f = require "lego.file" + +file["to_path"] = function() + t.eq(f.to_path "spec/fixture.md", "spec/fixture.md") + t.eq(f.to_path { "spec", "fixture.md" }, "spec/fixture.md") +end + +return T
A
lua/tests/fixture.md
@@ -0,0 +1,9 @@
+--- +title = This is fixture +slug = testing +date = 2025-09-30 +desc = testing testers test +--- + +# Content +Here's the content.
A
lua/tests/frontmatter_test.lua
@@ -0,0 +1,88 @@
+local t = require "tests.testutils" +local _, T, frontmatter = t.setup "frontmatter" + +local f = require "lego.frontmatter" + +frontmatter["should extract from frontmatter"] = function() + local input = { + "---", + "title=The title", + "link=test", + "---", + "the content is here", + } + + t.eq(f.extract(input), { + title = "The title", + link = "test", + }) +end + +frontmatter["support options with spaces"] = function() + local input = { + "---", + "title = The title", + "link one = some long thing here", + "---", + "the content is here", + } + + t.eq(f.extract(input), { + title = "The title", + ["link one"] = "some long thing here", + }) +end + +frontmatter["should return {} if there's no frontmatter"] = function() + local input = { + "there's no frontmatter", + "just text", + } + + t.eq(f.extract(input), {}) +end + +frontmatter["should return empty list if frontmatter is empty"] = function() + local input = { + "---", + "---", + "there's no frontmatter", + "just text", + } + + t.eq(f.extract(input), {}) +end + +frontmatter["should extract content"] = function() + local input = { + "---", + "title = The title", + "link one = some long thing here", + "---", + "the content is here", + "", + "something", + } + + t.eq(f.content(input), { + "the content is here", + "", + "something", + }) +end + +frontmatter["should extract content with no frontmatter"] = function() + local input = { + "the content is here", + "", + "something", + } + + t.eq(f.content(input), { + "the content is here", + "", + "something", + }) +end + +return T
A
lua/tests/html_test.lua
@@ -0,0 +1,82 @@
+local t = require "tests.testutils" +local _, T, html = t.setup "html" + +local a = require "lego.html.attribute" +local h = require "lego.html" + +html["simple html"] = function() + local node = h.el("div", {}, { h.text "hello" }) + + t.eq(h.render(node), "<div>hello</div>") +end + +html["simple html with attrs"] = function() + local node = h.div({ a.attr("class", "some classes") }, { h.text "string" }) + t.eq(h.render(node), [[<div class="some classes">string</div>]]) +end + +html["self-closing tag"] = function() + local node = h.el("img", { a.attr("src", "image.png"), a.attr("alt", "Alt text") }, {}) + t.eq(h.render(node), [[<img alt="Alt text" src="image.png">]]) +end + +html["nested html"] = function() + local node = h.div({ a.class "container" }, { + h.el("h1", {}, { h.text "Title" }), + h.p({}, { h.text "Paragraph" }), + }) + + t.eq(h.render(node), [[<div class="container"><h1>Title</h1><p>Paragraph</p></div>]]) +end + +html["even more nested html"] = function() + local node = h.div({ a.class "container" }, { + h.el("h1", {}, { h.text "Title" }), + h.div({}, { + h.p({}, { + h.text "text", + h.a({ a.href "google.com" }, { h.text "google" }), + }), + }), + }) + + t.eq( + h.render(node), + [[<div class="container"><h1>Title</h1><div><p>text<a href="google.com">google</a></p></div></div>]] + ) +end + +html["simple page"] = function() + local node = h.el("html", { a.attr("lang", "en") }, { + h.el("head", {}, { + h.el("title", {}, { h.text "My Page" }), + }), + h.el("body", {}, { + h.el("h1", {}, { h.text "Welcome" }), + h.p({}, { h.text "This is a basic HTML page." }), + }), + }) + + t.eq( + h.render_page(node), + [[<!DOCTYPE html><html lang="en"><head><title>My Page</title></head><body><h1>Welcome</h1><p>This is a basic HTML page.</p></body></html>]] + ) +end + +html["row html can be 'embedded'"] = function() + local node = h.el("html", {}, { + h.el("head", {}, { h.el("title", {}, { + h.text "My Page", + }) }), + h.el("body", {}, { + h.raw "<row-element some-kind-of-tag>", + }), + }) + + t.eq( + h.render(node), + "<html><head><title>My Page</title></head><body><row-element some-kind-of-tag></body></html>" + ) +end + +return T
A
lua/tests/post_test.lua
@@ -0,0 +1,45 @@
+local t = require "tests.testutils" +local _, T, post = t.setup "post" + +local p = require "lego.post" + +post["read fixture"] = function() + local inp = p.read_file { "lua", "tests", "fixture.md" } + + t.eq(inp.meta.date, "2025-09-30") + t.eq(inp.meta.slug, "testing") + t.eq(inp.meta.title, "This is fixture") + t.eq(inp.meta.desc, "testing testers test") + + t.eq(inp.content, "<h1>Content</h1>\n<p>Here's the content.</p>\n") +end + +post["sort_by_date"] = function() + local input = { + { meta = { date = "2025-09-30" } }, + { meta = { date = "2024-09-30" } }, + { meta = { date = "2025-08-30" } }, + { meta = { date = "2025-09-28" } }, + { meta = { date = "2025-06-30" } }, + { meta = { date = "2025-07-04" } }, + { meta = { date = "2025-06-21" } }, + { meta = { date = "2025-06-13" } }, + { meta = { date = "2025-06-21" } }, + } + + p.sort_by_date(input) + + t.eq(input, { + { meta = { date = "2025-09-30" } }, + { meta = { date = "2025-09-28" } }, + { meta = { date = "2025-08-30" } }, + { meta = { date = "2025-07-04" } }, + { meta = { date = "2025-06-30" } }, + { meta = { date = "2025-06-21" } }, + { meta = { date = "2025-06-21" } }, + { meta = { date = "2025-06-13" } }, + { meta = { date = "2024-09-30" } }, + }) +end + +return T
A
lua/tests/rss_test.lua
@@ -0,0 +1,12 @@
+local t = require "tests.testutils" +local _, T, rss = t.setup "rss" + +local r = require "lego.rss" + +rss["should escape html"] = function() + local input = "<p>Hello <a>world</a></p>" + + t.eq(r.escape_html(input), "<p>Hello <a>world</a></p>") +end + +return T
A
lua/tests/testutils.lua
@@ -0,0 +1,37 @@
+---@class testutils +local testutils = {} + +local minit_path = vim.fn.expand "%:p:h" .. "minit.lua" + +---@param mod string Module name for which to create a nested test set. +---@return MiniTest.child child +---@return table T +---@return table mod_name +function testutils.setup(mod) + local child = MiniTest.new_child_neovim() + local T = MiniTest.new_set { + hooks = { + post_once = child.stop, + pre_case = function() + child.restart { "-u", minit_path } + end, + }, + } + + T[mod] = MiniTest.new_set {} + return child, T, T[mod] +end + +---@generic T +---@param a T +---@param b T +function testutils.eq(a, b) + return MiniTest.expect.equality(a, b) +end + +---@param msg? string +function testutils.skip(msg) + MiniTest.skip(msg) +end + +return testutils
A
makefile
@@ -0,0 +1,16 @@
+.PHONY: all build build-parser test + +CMD=nvim --clean -u ./lua/minit.lua + +test: + @TEST=true $(CMD) --headless -c "lua MiniTest.run()" + +build-parser: + @cd go; go build -buildmode=c-shared -o liblego.so + +build: + @$(CMD) --headless +"lua require'blog'.build()" +q + +dev: + @watchexec --watch posts --watch lua --exts lua,md -- "make build" & + @bunx http-server ./build -p 8080
A
stylua.toml
@@ -0,0 +1,4 @@
+column_width = 100 +indent_type = "Spaces" +indent_width = 2 +no_call_parentheses = true