about summary refs log tree commit diff
path: root/users/sterni/blërg/blërg.bqn
blob: 78191ce84d0cf1c1ad7fadf3a2371396ac0541c5 (plain) (blame)
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>"