1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
#!/usr/bin/env BQN
# SPDX-FileCopyrightText: Copyright © 2024-2026 sterni
# SPDX-License-Identifier: GPL-3.0-only
#
# blërg is a reimplementation of mblog in BQN. BQN is used as a sort of bespoke
# scripting languages so we can rely on external tools for certain tasks (e.g.
# transforming HTML and parsing MIME messages). A list of dependencies is
# maintained in README.md.
# Utilities
⟨_join, StripRight, DropPrefix, AsciiDown, SplitChar⟩ ← •Import "string.bqn"
⟨R, LR, Execline, GetEnv⟩ ← •Import "run.bqn"
⟨ParseInt⟩ ← •Import "integer.bqn"
⟨ParseCanonicalPath, RenderPath, IsRelative, SplitExt⟩ ← •Import "posix-path.bqn"
date ← •Import "datetime.bqn"
kv ← •Import "kv.bqn"
MkDirP ← ""⊸((•file.CreateDir⍟(¬•file.Exists) •file.At)˜´)∘⌽∘ParseCanonicalPath
RelPath ← •wdpath⊸•file.At
# 3p dependencies
# Update README.md if dependency discovery changes
json ← •Import (GetEnv "BQN_LIBS") •file.At "json.bqn"
# Entries
timeLabels ← "published"‿"modified"
GetTime ← {
6=•Type 𝕩? 𝕨 GetTime 𝕩.published‿𝕩.modified ;
(⍉𝕩≍timeLabels)⊏˜(∧´fin)◶(⊑/)‿(𝕨˙) fin←(∞>|)¨𝕩
}
FmtTime ← {
ts‿label ← 𝕨 GetTime 𝕩
fmtd ← date.ToRfc3339 date.FromUnix ts
label∾" <time datetime="""∾fmtd∾""">"∾fmtd∾"</time>"
}
# (Apple) Mail Notes Backend
# TODO(sterni): avoid argv limit by chunking
Hdrs ← {LR "mhdr"‿"-dh"‿(':' _join 𝕨)∾𝕩}
Dates ← {ParseInt¨ LR "mhdr"‿"-Dh"‿"Date"∾𝕩}
headerNames ← "X-Uniform-Type-Identifier"‿"X-Universally-Unique-Identifier"‿"Subject"
MailNotesBackend ← {𝕊 config:
mailDir ← RelPath config kv.Get "maildir"
outPath ← config kv.Get "base-path"
Entries ⇐ {𝕊:
ms ← LR "mlist"‿mailDir
th ← ⟨≠ms,≠headerNames⟩⥊headerNames Hdrs ms
dh ← Dates ms
ah ← (("com.apple.mail-note"⊸≡⊑)˘/⊢) th∾˘dh≍˘ms
{𝕊 ·‿uuid‿title‿modified‿path:
title ⇐ ⋄ modified ⇐ ⋄ published ⇐ ∞
id ⇐ outPath∾⋈uuid
Render ⇐ {R "execline-cd"‿𝕩‿"mshow"‿"-x"‿path ⋄ R "mn2html"‿path}
}˘ ah
}
}
# Git Backend
converters ← ⍉>⟨
# TODO(sterni): avoid cat
⟨".html", ⋈"cat"⟩,
⟨".md", "lowdown"‿"-T"‿"html"‿"--html-no-skiphtml"‿"--html-no-escapehtml"‿"--html-callout-mdn"⟩,
# TODO(sterni): use emacs
⟨".org", "pandoc"‿"-f"‿"org"‿"-t"‿"html5"⟩,
⟩
# TODO(sterni): pipefail
PipelineCmd ← {Execline "pipeline"‿𝕨∾𝕩}
GitBackend ← {𝕊 config:
repo ← RelPath config kv.Get "repository"
path ← ∾⟜'/' '/' StripRight config "." kv._GetDef "path"
outPath ← config kv.Get "base-path"
# We use zero separated fields when dealing with paths, so quoting is unnecessary
GitCmd ← {"git"‿"-c"‿"core.quotePath=false"‿"-C"‿repo∾𝕩}
rev ← R GitCmd "rev-parse"‿"HEAD"
# Use the author date of the latest commit on the file to establish the date
# of the file. The author date is easier to arbitrarily change and survives
# history rewrites. It could be interesting to ignore commits that touch
# multiple files (especially treewide ones).
gitLogFlags ← ⟨
"--date=unix",
"--pretty=tformat:%ad",
# TODO(sterni): default to --follow on
(config 0 kv._GetDef "follow")⊑⟨"--remove-empty", "--follow"⟩
⟩
PathDates ← {ParseInt¨ (⌽ ⋈○⊑ ⊢) (@+10) SplitChar R GitCmd ⟨"log"⟩∾gitLogFlags∾rev‿"--"‿𝕩}
Entries ⇐ {𝕤⋄
blobs ← ∘‿2⥊@ SplitChar R GitCmd "ls-tree"‿"-zr"‿"--format=%(path)%x00%(objectname)"‿rev‿path
{𝕊 p‿b:
# TODO(sterni): very ugly
extlessp‿ext ← SplitExt p
relp ← path DropPrefix extlessp
("path returned from git did not start with "∾path)!extlessp≢relp
id ⇐ outPath∾ParseCanonicalPath relp
# TODO(sterni): extract from file if possible
title ⇐ •file.Name extlessp
# TODO(sterni): differentiate created and updated
published‿modified ⇐ PathDates p
Render ⇐ {𝕤
conv ← converters kv.Get ext
R (GitCmd "cat-file"‿"blob"‿b) PipelineCmd conv
}
}˘blobs
}
}
backendsAvail ← ⍉>⟨"mail-notes"‿mailNotesBackend, "git"‿gitBackend⟩
# Rendering
RenderPage ← {
∾"<!doctype html>
<html lang=""en"">
<head>
<meta charset=""utf-8"">
<title>"‿𝕨‿"</title>
<body>
<h1>"‿𝕨‿"</h1>"‿𝕩
}
# note: we eliminate dots here which should prevent accidental path traversals
Slugify ← '-'⊸⊣⍟(('A'⊸≤ ∧ ≤⟜'z') ¬∘∨ "-_0123456789"⊸(⊑∊˜)⟜<)¨ AsciiDown
WriteEntry ← {outDir 𝕊 entry:
("entry id must be relative: "∾entryPath)!IsRelative entryPath ← RenderPath Slugify¨ entry.id
entryDir ← MkDirP outDir •file.At entryPath
(entryDir •file.At "index.html") •file.Chars entry.title RenderPage entry.Render entryDir
# TODO(sterni): urlencode
"<li><a href="""∾entryPath∾""">"∾entry.title∾"</a> ("∾config.sortTime FmtTime entry∾")</li>"
}
# Main
configFile‿outDir ← {
# Usage: blërg <config file> <out dir>
! 2=≠•args
# TODO(sterni): expand ~/
RelPath¨ •args
}
CollectBackends ← {
path 𝕊 subdir:
# TODO(sterni): handle this centrally instead of in backends
confs ← (kv.Set⟜"base-path"‿path)¨ subdir kv.Get "backends"
types ← kv.Get⟜"type"¨ confs
here ← confs {𝕏 𝕨}¨ backendsAvail⊸kv.Get¨ types
children ← ⥊CollectBackends´˘ (path⊸∾⋈)⌾⊑˘ ⍉ subdir ⟨⟩ kv._GetDef "subdirs"
here∾children
}
config ← {
parsed ← json.Parse •FChars configFile
backends ⇐ ⟨⟩ CollectBackends parsed kv.Get "output"
title ⇐ parsed kv.Get "title"
# TODO(sterni): should be a per (subdir) index setting
sortTime ⇐ ⊑/timeLabels ≡¨< parsed "published" kv._GetDef "sort_by"
}
entries ← ((⍒ config.sortTime⊸(⊑∘GetTime)¨)⊏⊢) ∾{𝕩.Entries @}¨ config.backends
"All entry IDs must be unique"!(≠=≠∘⍷) •ns.Get⟜"id"¨ entries
# TODO(sterni): prevent clashes between directories and entries
MkDirP outDir
entryIndex ← outDir⊸WriteEntry¨ entries
(outDir •file.At "index.html") •file.Chars config.title RenderPage ∾"<ul>"∾entryIndex∾"</ul>"
|