this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+9912 -197
.github
workflows
content
layouts
sass
static
styles
templates
themes
-4
.gitattributes
···
-
*.png filter=lfs diff=lfs merge=lfs -text
-
*.woff filter=lfs diff=lfs merge=lfs -text
-
*.jpg filter=lfs diff=lfs merge=lfs -text
-
*.jpeg filter=lfs diff=lfs merge=lfs -text
···
+16
.github/workflows/rebuild.yml
···
···
+
name: Trigger Netlify rebuild
+
on:
+
schedule:
+
# Run every 8h
+
- cron: '0 */8 * * *'
+
+
jobs:
+
trigger:
+
name: Send trigger
+
runs-on: ubuntu-latest
+
env:
+
BUILD_HOOK_URL: 'https://api.netlify.com/build_hooks/629f4bdea589057c66219efd'
+
steps:
+
- name: Run cURL request
+
run: |
+
curl -X POST -d '{}' "$BUILD_HOOK_URL"
+3
.gitignore
···
node_modules/
/public
···
node_modules/
/public
+
+
# Local Netlify folder
+
.netlify
-3
.gitmodules
···
-
[submodule "themes/zerm"]
-
path = themes/zerm
-
url = https://github.com/ejmg/zerm.git
···
+2
.ignore
···
···
+
/static
+
/styles
+3
.mdlrc
···
···
+
style 'styles/markdown.rb'
+
+
# vi: ft=ruby
+6
.vale.ini
···
···
+
StylesPath = styles
+
+
[*.md]
+
BasedOnStyles = proselint, write-good
+
write-good.Passive = NO
+
write-good.TooWordy = NO
+1
README.md
···
···
+
templates/shortcodes/readme.md
+37 -21
config.toml
···
-
base_url = "/"
default_language = "en"
title = "Hauleth's blog"
description = "Blog about BEAM, Rust, fantasy & stuff"
theme = "zerm"
-
highlight_code = true
-
highlight_theme = "nord"
-
generate_feed = true
taxonomies = [
-
{name = "tags"},
-
{name = "categories"},
]
[extra]
-
author = "Łukasz Niemier"
theme_color = "blue"
logo_text = "~hauleth"
logo_home_link = "/"
···
show_author = false
show_categories = true
show_tags = true
-
-
# How many menu items to show on desktop. if you set this to 0, only submenu
-
# button will be visible.
-
show_menu_items = 10
# set theme to full screen width.
full_width = false
···
# Must be placed in root of static/ directory...
# og_preview_img = ""
-
# Copyright notice if desired. Defaults to
-
# copyright = "copyright notice here"
# Menu items to display. You define a url and the name of the menu item.
-
# NOTE: `$BASE_URL/` must be included in the url name.
main_menu = [
-
{url="/about/", name="about"},
-
{url="https://twitter.com/hauleth", name="twitter"},
-
{url="https://github.com/hauleth", name="github"},
-
{url="https://gitlab.com/hauleth", name="gitlab"},
]
-
# Displayed as title of drop-down menu when size of main_menu > show_menu_items.
-
menu_more = "show more"
-
# Displayed after teaser text for a blog post.
read_more = "read more"
# not currently used from previous theme, but leaving here for now in case I
# feel like adding it.
read_other_posts = "read other posts"
···
+
base_url = "https://hauleth.dev/"
default_language = "en"
title = "Hauleth's blog"
description = "Blog about BEAM, Rust, fantasy & stuff"
+
+
compile_sass = true
theme = "zerm"
+
generate_feeds = true
+
minify_html = true
taxonomies = [
+
{name = "tags", feed = true},
+
]
+
+
[markdown]
+
highlight_code = true
+
highlight_theme = "css"
+
# TODO: Write custom theme that will fit the overall colouring better
+
highlight_themes_css = [
+
{ theme = "nord", filename = "syntax-theme.css" },
]
[extra]
+
author = "Hauleth"
+
for_hire = true
theme_color = "blue"
+
+
source = "https://tangled.sh/hauleth.dev/blog"
logo_text = "~hauleth"
logo_home_link = "/"
···
show_author = false
show_categories = true
show_tags = true
# set theme to full screen width.
full_width = false
···
# Must be placed in root of static/ directory...
# og_preview_img = ""
+
copyright = "copyright by <a href=\"https://hauleth.dev\" rel=me>hauleth</a>"
# Menu items to display. You define a url and the name of the menu item.
main_menu = [
+
{url="https://plan.cat/~hauleth", name=".plan", rel="me"},
+
{url="https://fosstodon.org/@hauleth", name="toots", rel="me"},
+
{url="https://sr.ht/~hauleth", name="sourcehut", rel="me"},
+
{url="https://github.com/hauleth", name="github", rel="me"},
+
{url="https://gitlab.com/hauleth", name="gitlab", rel="me"},
]
# Displayed after teaser text for a blog post.
read_more = "read more"
# not currently used from previous theme, but leaving here for now in case I
# feel like adding it.
read_other_posts = "read other posts"
+
+
favicon = "favicon-32x32.png"
+
og_preview_img = "banner.png"
+
+
webmention = "https://webmention.io/hauleth.dev/webmention"
+
+
[extra.twitter]
+
site = "@hauleth"
+
creator = "@hauleth"
+
+
[[extra.webrings]]
+
name = "Beambloggers"
+
url = "https://beambloggers.com/"
+6 -2
content/_index.md
···
+++
sort_by = "date"
-
transparent = false
-
paginate_by = 3
insert_anchor_links = "right"
+++
···
+++
+
title = "Hauleth"
+
template = "landing.html"
+
sort_by = "date"
+
paginate_by = 1000
insert_anchor_links = "right"
+++
+
+
{{ readme() }}
-7
content/about.md
···
-
+++
-
title = "foo"
-
transparent = false
-
paginate_by = 0
-
+++
-
-
Foo
···
+9
content/cv/_index.md
···
···
+
+++
+
title = "CV"
+
+
[extra]
+
no_comments = true
+
sitemap = false
+
+++
+
+
{{ cv() }}
+4 -2
content/post/_index.md
···
+++
-
transparent = true
-
render = false
insert_anchor_links = "right"
+++
···
+++
+
sort_by = "date"
+
paginate_by = 1000
insert_anchor_links = "right"
+
transparent = true
+
redirect_to = "/"
+++
+472
content/post/beam-process-memory-usage.md
···
···
+
+++
+
date = 2023-06-10
+
title = "How much memory is needed to run 1M Erlang processes?"
+
description = "How to not write benchmarks"
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"performance"
+
]
+
+++
+
+
Recently [benchmark for concurrency implementation in different
+
languages][benchmark]. In this article [Piotr Kołaczkowski][] used Chat GPT to
+
generate the examples in the different languages and benchmarked them. This was
+
poor choice as I have found this article and read the Elixir example:
+
+
[benchmark]: https://pkolaczk.github.io/memory-consumption-of-async/ "How Much Memory Do You Need to Run 1 Million Concurrent Tasks?"
+
[Piotr Kołaczkowski]: https://github.com/pkolaczk
+
+
```elixir
+
tasks =
+
for _ <- 1..num_tasks do
+
Task.async(fn ->
+
:timer.sleep(10000)
+
end)
+
end
+
+
Task.await_many(tasks, :infinity)
+
```
+
+
And, well, it's pretty poor example of BEAM's process memory usage, and I am
+
not talking about the fact that it uses 4 spaces for indentation.
+
+
For 1 million processes this code reported 3.94 GiB of memory used by the process
+
in Piotr's benchmark, but with little work I managed to reduce it about 4 times
+
to around 0.93 GiB of RAM usage. In this article I will describe:
+
+
- how I did that
+
- why the original code was consuming so much memory
+
- why in the real world you probably should not optimise like I did here
+
- why using ChatGPT to write benchmarking code sucks (TL;DR because that will
+
nerd snipe people like me)
+
+
## What are Erlang processes?
+
+
Erlang is ~~well~~ known of being language which support for concurrency is
+
superb, and Erlang processes are the main reason for that. But what are these?
+
+
In Erlang *process* is the common name for what other languages call *virtual
+
threads* or *green threads*, but in Erlang these have small neat twist - each of
+
the process is isolated from the rest and these processes can communicate only
+
via message passing. That gives Erlang processes 2 features that are rarely
+
spotted in other implementations:
+
+
- Failure isolation - bug, unhandled case, or other issue in single process will
+
not directly affect any other process in the system. VM can send some messages
+
due to process shutdown, and other processes may be killed because of that,
+
but by itself shutting down single process will not cause problems in any
+
process not related to that.
+
- Location transparency - process can be spawned locally or on different
+
machine, but from the viewpoint of the programmer, there is no difference.
+
+
The above features and requirements results in some design choices, but for our
+
purpose only one is truly needed today - each process have separate and (almost)
+
independent memory stack from any other process.
+
+
### Process dictionary
+
+
Each process in Erlang VM has dedicated *mutable* memory space for their
+
internal uses. Most people do not use it for anything because in general it
+
should not be used unless you know exactly what you are doing (in my case, a bad
+
carpenter could count cases when I needed it, on single hand). In general it's
+
*here be dragons* area.
+
+
How it's relevant to us?
+
+
Well, OTP internally uses process dictionary (`pdict` for short) to store
+
metadata about given process that can be later used for debugging purposes. Some
+
data that it store are:
+
+
- Initial function that was run by the given process
+
- PIDs to all ancestors of the given process
+
+
Different processes abstractions (like `gen_server`/`GenServer`, Elixir's
+
`Task`, etc.) can store even more metadata there, `logger` store process
+
metadata in process dictionary, `rand` store state of the PRNGs in the process
+
dictionary. it's used quite extensively by some OTP features.
+
+
### "Well behaved" OTP process
+
+
In addition to the above metadata if the process is meant to be "well behaved"
+
process in OTP system, i.e. process that can be observed and debugged using OTP
+
facilities, it must respond to some additional messages defined by [`sys`][]
+
module. Without that the features like [`observer`][] would not be able to "see"
+
the content of the process state.
+
+
[`sys`]: https://erlang.org/doc/man/sys.html
+
[`observer`]: https://erlang.org/doc/man/observer.html
+
+
## Process memory usage
+
+
As we have seen above, the `Task.async/1` function form Elixir **must** do
+
much more than just simple "start process and live with it". That was one of the
+
most important problems with the original process, it was using system, that was
+
allocating quite substantial memory alongside of the process itself, just to
+
operate this process. In general, that would be desirable approach (as you
+
**really, really, want the debugging facilities**), but in synthetic benchmarks,
+
it reduce the feasibility of such benchmark.
+
+
If we want to avoid that additional memory overhead in our spawned processes we
+
need to go back to more primitive functions in Erlang, namely `erlang:spawn/1`
+
(`Kernel.spawn/1` in Elixir). But that mean that we cannot use
+
`Task.await_many/2` anymore, so we need to workaround it by using custom
+
function:
+
+
```elixir
+
defmodule Bench do
+
def await(pid) when is_pid(pid) do
+
# Monitor is internal feature of Erlang that will inform you (by sending
+
# message) when process you monitor die. The returned value is type called
+
# "reference" which is just simply unique value returned by the VM.
+
# If the process is already dead, then message will be delivered
+
# immediately.
+
ref = Process.monitor(pid)
+
+
receive do
+
{:DOWN, ^ref, :process, _, _} -> :ok
+
end
+
end
+
+
def await_many(pids) do
+
Enum.each(pids, &await/1)
+
end
+
end
+
+
tasks =
+
for _ <- 1..num_tasks do
+
# `Kernel` module is imported by default, so no need for `Kernel.` prefix
+
spawn(fn ->
+
:timer.sleep(10000)
+
end)
+
end
+
+
Bench.await_many(tasks)
+
```
+
+
We already removed one problem (well, two in fact, but we will go into
+
details in next section).
+
+
## All your lists are belongs to us now
+
+
Erlang, like most of the functional programming languages, have 2 built-in
+
sequence types:
+
+
- Tuples - which are non-growable product type of the values, so you can access
+
any field quite fast, but adding more values is performance no-no
+
- (Singly) linked lists - growable type (in most case it will have single type
+
values in it, but in Erlang that is not always the case), which is fast to
+
prepend or pop data from the beginning, but do not try to do anything else if
+
you care about performance.
+
+
In this case we will focus on the 2nd one, as there tuples aren't important at
+
all.
+
+
Singly linked list is simple data structure. It's either special value `[]`
+
(an empty list) or it's something called "cons-cell". Cons-cells are also
+
simple structures - it's 2ary tuple (tuple with 2 elements) where first value
+
is head - the value in the list cell, and another one is the "tail" of the list (aka
+
rest of the list). In Elixir the cons-cell is denoted like that `[head | tail]`.
+
Super simple structure as you can see, and perfect for the functional
+
programming as you can add new values to the list without modifying existing
+
values, so you can be immutable and fast. However if you need to construct the
+
sequence of a lot of values (like our list of all tasks) then we have problem.
+
Because Elixir promises that list returned from the `for` will be **in-order**
+
of the values passed to it. That mean that we either need to process our data
+
like that:
+
+
```elixir
+
def map([], _), do: []
+
+
def map([head | tail], func) do
+
[func.(head) | map(tail, func)]
+
end
+
```
+
+
Where we build call stack (as we cannot have tail call optimisation there, of
+
course sans compiler optimisations). Or we need to build our list in reverse
+
order, and then reverse it before returning (so we can have TCO):
+
+
```elixir
+
def map(list, func), do: do_map(list, func, [])
+
+
def map([], _func, agg), do: :lists.reverse(agg)
+
+
def map([head | tail], func, agg) do
+
map(tail, func, [func.(head) | agg])
+
end
+
```
+
+
Which one of these approaches is more performant is irrelevant[^erlang-perf],
+
what is relevant is that we need either build call stack or construct our list
+
*twice* to be able to conform to the Elixir promises (even if in this case we do
+
not care about order of the list returned by the `for`).
+
+
[^erlang-perf]: Sometimes body recursion will be faster, sometimes TCO will be
+
faster. it's impossible to tell without more benchmarking. For more info check
+
out [superb article by Ferd Herbert](https://ferd.ca/erlang-s-tail-recursion-is-not-a-silver-bullet.html).
+
+
Of course we could mitigate our problem by using `Enum.reduce/3` function (or
+
writing it on our own) and end with code like:
+
+
```elixir
+
defmodule Bench do
+
def await(pid) when is_pid(pid) do
+
ref = Process.monitor(pid)
+
+
receive do
+
{:DOWN, ^ref, :process, _, _} -> :ok
+
end
+
end
+
+
def await_many(pids) do
+
Enum.each(pids, &await/1)
+
end
+
end
+
+
tasks =
+
Enum.reduce(1..num_tasks, [], fn _, agg ->
+
# `Kernel` module is imported by default, so no need for `Kernel.` prefix
+
pid =
+
spawn(fn -> :timer.sleep(10000) end)
+
+
[pid | agg]
+
end)
+
+
Bench.await_many(tasks)
+
```
+
+
Even then we build list of all PIDs.
+
+
Here I can also go back to the "second problem* I have mentioned above.
+
`Task.await_many/1` *also construct a list*. it's list of return value from all
+
the processes in the list, so not only we constructed list for the tasks' PIDs,
+
we also constructed list of return values (which will be `:ok` for all processes
+
as it's what `:timer.sleep/1` returns), and immediately discarded all of that.
+
+
How we can better? See that **all** we care is that all `num_task` processes
+
have gone down. We do not care about any of the return values, all what we want
+
is to know that all processes that we started went down. For that we can just
+
send messages from the spawned processes and count the received messages count:
+
+
```elixir
+
defmodule Bench do
+
def worker(parent) do
+
:timer.sleep(10000)
+
send(parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
this = self()
+
spawn(fn -> worker(this) end)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
Now we do not have any lists involved and we still do what the original task
+
meant to do - spawn `num_tasks` processes and wait till all go down.
+
+
## Arguments copying
+
+
One another thing that we can account there - lambda context and data passing
+
between processes.
+
+
You see, we need to pass `this` (which is PID of the parent) to our newly
+
spawned process. That is suboptimal, as we are looking for the way to reduce
+
amount of the memory (and ignore all other metrics at the same time). As Erlang
+
processes are meant to be "share nothing" type of processes there is problem -
+
we need to copy that PID to all processes. it's just 1 word (which mean 8 bytes
+
on 64-bit architectures, 4 bytes on 32-bit), but hey, we are microbenchmarking,
+
so we cut whatever we can (with 1M processes, this adds up to 8 MiBs).
+
+
Hey, we can avoid that by using yet another feature of Erlang, called
+
*registry*. This is yet another simple feature that allows us to assign PID of
+
the process to the atom, which allows us then to send messages to that process
+
using just name, we have given. While atoms are also 1 word that wouldn't make
+
sense to send it as well, but instead we can do what any reasonable
+
microbenchmarker would do - *hardcode stuff*:
+
+
```elixir
+
defmodule Bench do
+
def worker do
+
:timer.sleep(10000)
+
send(:parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
spawn(fn -> worker() end)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Process.register(self(), :parent)
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
Now we do not pass any arguments, and instead rely on the registry to dispatch
+
our messages to respective processes.
+
+
## One more thing
+
+
As you may have already noticed we are passing lambda to the `spawn/1`. That is
+
also quite suboptimal, because of [difference between remote and local call][remote-vs-local].
+
This mean that we are paying slight memory cost for these processes to keep the
+
old module in memory. Instead we can use either fully qualified function capture
+
or `spawn/3` function that accepts MFA (module, function name, arguments list)
+
argument. We end with:
+
+
[remote-vs-local]: https://www.erlang.org/doc/reference_manual/code_loading.html#code-replacement
+
+
```elixir
+
defmodule Bench do
+
def worker do
+
:timer.sleep(10000)
+
send(:parent, :done)
+
end
+
+
def start(0), do: :ok
+
def start(n) when n > 0 do
+
spawn(&__MODULE__.worker/0)
+
+
start(n - 1)
+
end
+
+
def await(0), do: :ok
+
def await(n) when n > 0 do
+
receive do
+
:done -> await(n - 1)
+
end
+
end
+
end
+
+
Process.register(self(), :parent)
+
+
Bench.start(num_tasks)
+
Bench.await(num_tasks)
+
```
+
+
## Results
+
+
With given Erlang compilation:
+
+
```txt
+
Erlang/OTP 25 [erts-13.2.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
+
+
Elixir 1.14.5 (compiled with Erlang/OTP 25)
+
```
+
+
> Note no JIT as Nix on macOS currently[^currently] disable it and I didn't bother to enable
+
> it in the derivation (it was disabled because there were some issues, but IIRC
+
> these are resolved now).
+
+
[^currently]: Nixpkgs rev `bc3ec5ea`
+
+
The results are as follow (in bytes of peak memory footprint returned by
+
`/usr/bin/time` on macOS):
+
+
| Implementation | 1k | 100k | 1M |
+
| -------------- | -------: | --------: | ---------: |
+
| Original | 45047808 | 452837376 | 4227715072 |
+
| Spawn | 43728896 | 318230528 | 2869723136 |
+
| Reduce | 43552768 | 314798080 | 2849304576 |
+
| Count | 43732992 | 313507840 | 2780540928 |
+
| Registry | 44453888 | 311988224 | 2787237888 |
+
| RemoteCall | 43597824 | 310595584 | 2771525632 |
+
+
As we can see we have reduced the memory use by about 30% by just changing
+
from `Task.async/1` to `spawn/1`. Further optimisations reduced memory usage
+
slightly, but with no such drastic changes.
+
+
Can we do better?
+
+
Well, with some VM flags tinkering - of course.
+
+
You see, by default Erlang VM will not only create some data required for
+
handling process itself[^word]:
+
+
[^word]: Again, word here mean 8 bytes on 64-bit and 4 bytes on 32-bit architectures.
+
+
> | Data Type | Memory Size |
+
> | - | - |
+
> | … | … |
+
> | Erlang process | 338 words when spawned, including a heap of 233 words. |
+
>
+
> -- [Erlang Efficiency Guide: 11. Advanced](https://erlang.org/doc/efficiency_guide/advanced.html#Advanced)
+
+
As we can see, there are 105 words that are required and 233 words which are
+
used for preallocated heap. But this is microbenchmarking, so as we do not need
+
that much of memory (because our processes basically does nothing), we can
+
reduce it. We do not care about time performance anyway. For that we can use
+
`+hms` flag and set it to some small value, for example `1`.
+
+
In addition to heap size Erlang by default load some additional data from the
+
BEAM files. That data is used for debugging and error reporting, but again, we
+
are microbenchmarking, and who need debugging support anyway (answer: everyone,
+
so **do not** do it in production). Luckily for us, the VM has yet another flag
+
for that purpose `+L`.
+
+
Erlang also uses some [ETS][] (Erlang Term Storage) tables by default (for
+
example to support process registry we have mentioned above). ETS tables can be
+
compressed, but by default it's not done, as it can slow down some kinds of
+
operations on such tables. Fortunately there is, another, flag `+ec` that has
+
description:
+
+
> Forces option compressed on all ETS tables. Only intended for test and
+
> evaluation.
+
+
[ETS]: https://erlang.org/doc/man/ets.html
+
+
Sounds good enough for me.
+
+
With all these flags enabled we get peak memory footprint at 996257792 bytes.
+
+
Compare it in more human readable units.
+
+
| | Peak Memory Footprint for 1M processes |
+
| ------------------------ | -------------------------------------- |
+
| Original code | 3.94 GiB |
+
| Improved code | 2.58 GiB |
+
| Improved code with flags | 0.93 GiB |
+
+
Result - about 76% of the peak memory usage reduction. Not bad.
+
+
## Summary
+
+
First of all:
+
+
> Please, do not use ChatGPT for writing code for microbenchmarks.
+
+
The thing about *micro*benchmarking is that we write code that does as little as
+
possible to show (mostly) meaningless features of the given technology in
+
abstract environment. ChatGPT cannot do that, not out of malice or incompetence,
+
but because it used (mostly) *good* and idiomatic code to teach itself,
+
microbenchmarks rarely are something that people will consider to have these
+
qualities. It also cannot consider other features that [wetware][] can take into
+
account (like our "we do not need lists there" thing).
+
+
[wetware]: https://en.wikipedia.org/wiki/Wetware_(brain)
+10 -15
content/post/common-test-for-elixir.md
···
[taxonomies]
tags = [
-
"erlang",
"beam",
-
"elixir",
-
"testing",
-
"programming",
-
"common_test",
-
"commoner"
]
+++
In my new job I have opportunity to work a little bit more with Erlang code and
···
Just check [this out](/common-test-example/simple/index.html). This is example report
generated by the Common Test. As you can see it contains a lot of information in
-
quite readable format. Not only it contains informations about current run, but
all previous runs as well, which is really handy when tracking regressions.
-
But can we store even more informations there? Yes, as CT includes simple
-
logging facility it is completely possible to log your own informations during
-
tests, for example, lets modify our test to log some informations:
```erlang
test_function_name(_Config) ->
···
2 = 1 + 1.
```
-
Now when we run tests again, then we will see more informations (even coloured)
in [our test log](/common-test-example/log/ct_run.ct@NiunioBook.2019-07-16_11.03.21/common-test-example.log.logs/run.2019-07-16_11.03.22/example_suite.test_function_name.html):
![Common Test log "Example message" on green background](/img/common-test/log.png)
···
- On [Elixir forum thread about Commoner][forum]
- On Twitter via `@hauleth`
-
-
### Special thanks
-
-
- José Valim - for reading it through and reviewing before publishing
[ct]: http://erlang.org/doc/apps/common_test/basics_chapter.html
[ctex]: https://github.com/Comcast/ctex
···
[taxonomies]
tags = [
"beam",
+
"testing"
]
+
+
[[extra.thanks]]
+
name = "José Valim"
+
why = "for reading it through and reviewing before publishing"
+++
In my new job I have opportunity to work a little bit more with Erlang code and
···
Just check [this out](/common-test-example/simple/index.html). This is example report
generated by the Common Test. As you can see it contains a lot of information in
+
quite readable format. Not only it contains information about current run, but
all previous runs as well, which is really handy when tracking regressions.
+
But can we store even more information there? Yes, as CT includes simple
+
logging facility it is completely possible to log your own information during
+
tests, for example, lets modify our test to log some information:
```erlang
test_function_name(_Config) ->
···
2 = 1 + 1.
```
+
Now when we run tests again, then we will see more information (even coloured)
in [our test log](/common-test-example/log/ct_run.ct@NiunioBook.2019-07-16_11.03.21/common-test-example.log.logs/run.2019-07-16_11.03.22/example_suite.test_function_name.html):
![Common Test log "Example message" on green background](/img/common-test/log.png)
···
- On [Elixir forum thread about Commoner][forum]
- On Twitter via `@hauleth`
[ct]: http://erlang.org/doc/apps/common_test/basics_chapter.html
[ctex]: https://github.com/Comcast/ctex
+1 -5
content/post/eli5-ownership.md
···
[taxonomies]
tags = [
-
"rust",
-
"programming",
-
"ownership",
-
"eli5",
-
"borrowing"
]
+++
···
[taxonomies]
tags = [
+
"rust"
]
+++
+1 -4
content/post/elixir-application.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"beam",
-
"programming"
]
+++
···
[taxonomies]
tags = [
+
"beam"
]
+++
-59
content/post/erlang-in-mix.md
···
-
+++
-
title = "Erlang in Mix"
-
date = 2019-07-24T12:32:07+02:00
-
draft = true
-
+++
-
-
Currently there are 3 main build tools in BEAM world:
-
-
- Rebar3 - de facto standard in Erlang and other languages (with exception to
-
Elixir). Uses declarative `rebar.config` file (which is in `file:consult/1`
-
format) that can be then formatted via `rebar.config.script` Erlang script.
-
- Mix - standard build tool in Elixir world. Uses imperative `mix.exs` file.
-
- erlang.mk - GNU Make based tool. Uses it's own registry and is mostly known as
-
tool used by Cowboy.
-
-
In this article I will cover only first two and their comparison when it comes
-
to support building mostly Erlang projects (AFAIK `rebar3` do not have yet
-
support for building Elixir projects, mostly because Elixir cannot be used as
-
Erlang library).
-
-
## Declarative vs imperative
-
-
Accordingly to [Wikipedia][declarative programming]:
-
-
> In computer science, declarative programming is a programming paradigm—a style
-
> of building the structure and elements of computer programs—that expresses the
-
> logic of a computation without describing its control flow.
-
-
In other words, we only describe **what** without focusing on **how**. This mean
-
that we have less direct control over our configuration while requiring less
-
knowledge to configure properly.
-
-
At the same time it is (in theory) more secure, as imagine that you would have
-
dependency with such `mix.exs`:
-
-
```elixir
-
defmodule TotallySafeLibrary.Mixfile do
-
use Mix.Project
-
-
# HAHAHA I lied!!! Pwnd MF
-
File.rm_rf!(System.user_home())
-
-
# …
-
end
-
```
-
-
I mean, this is still possible in Rebar via `rebar.config.script`, but it is
-
much harder due to 2 reasons:
-
-
- There is no such function like `File.rm_rf!/1` in Erlang, so the end user
-
would need to write their own.
-
- It is much easier to spot additional file in the repo than review whole one
-
file.
-
-
The same goes for `.app.src` file, which while having more "abstract" format
-
than Mix's `application/0` function ends much simpler without all imperativeness
-
brought by making configuration file executable script.
-
-
## Tasks
···
+8
content/post/indie-web.md
···
···
+
+++
+
title = "IndieWeb"
+
date = 2022-06-08T11:25:55+00:00
+
draft = true
+
+
[taxonomies]
+
tags = []
+
+++
+71
content/post/jeopardy-world.md
···
···
+
+++
+
title = "Jeopardy! world"
+
date = 2025-07-28
+
+
[taxonomies]
+
tags = ["ai", "culture"]
+
+++
+
+
Some time ago, there was an anime available on Netflix — *Godzilla Singular
+
Point*. It wasn't a spectacular success, but it featured a plot device that I
+
think reflects something increasingly common today: you need to know the answer
+
to your question before you can ask it.
+
+
This is something I see all the time in the current wave of AI hype. You need to
+
know what the answer *should* be before you can write a useful prompt.
+
+
<!-- more -->
+
+
The issue I have with many AI use cases is this: unless you have specialized
+
knowledge about the topic you're asking about, you can't reliably tell the
+
difference between a solid AI answer and complete nonsense.
+
+
I've had a few discussions about this on various Discord servers. The example I
+
often use is this simple question posed to an AI:
+
+
> Does 6 character long identification number, that contains digits and upper
+
> case letters (with exception to 0, O, 1, I, and L) is enough to randomly
+
> assign unique identification numbers for 10 million records?
+
+
You can see for your self answer from ChatGPT [there][chatgpt].
+
+
At first glance, the answer looks valid and sensible. The math checks out. It
+
calculates the number of available combinations correctly. Everything seems
+
*fine*.
+
+
**BUT…**
+
+
There is huge issue there, and probably most of the people who have been working
+
with basic statistic or cryptography will notice it. ChatGPT (and any other AI
+
that I have tested out) fail to notice very important word there
+
+
> \[…] randomly \[…]
+
+
This single word invalidates the entire reasoning, despite the correct
+
calculations. Because of the [birthday problem][], the answer isn't feasible.
+
While it's technically possible to assign a unique ID to every record, doing so
+
randomly introduces a high probability of collisions.
+
+
- At around 35,000 generated IDs, there's already a 50% chance of a collision
+
- At around 90,000, the chance of at least one duplicate reaches 99%
+
+
So even though the math is correct, the logic fails under the randomness constraint.
+
+
## *Jeopardy!* world
+
+
This is my main issue with AI tools: if you already have knowledge about the
+
subject, you don’t really need to ask the AI. But if you don’t have that
+
knowledge, you have no reliable way of knowing whether the answer makes sense or
+
not. It’s like playing *Jeopardy!* — you need to know the answer before you can
+
phrase the right question.
+
+
In my view, AI is most useful in areas where the results can be quickly reviewed
+
and discarded if needed. That’s why the whole “vibe coding” (aka slop
+
generation) approach falls short. If you don’t have a good sense of what the
+
output should look like, you probably don’t have the expertise to verify it.
+
+
[And gods forbid you from allowing AI to do anything on production][replit-fuckup].
+
+
[chatgpt]: https://chatgpt.com/share/68879fe7-d4e0-8007-9a30-3a9e2ace791d
+
[birthday problem]: https://en.wikipedia.org/wiki/Birthday_problem
+
[replit-fuckup]: https://www.businessinsider.com/replit-ceo-apologizes-ai-coding-tool-delete-company-database-2025-7?op=1
+482
content/post/log-all-the-things.md
···
···
+
+++
+
title = "Log all the things"
+
date = 2022-01-02
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"observability"
+
]
+
+
[[extra.thanks]]
+
name = "Kai Wern Choong"
+
+++
+
+
In Elixir 1.11 landed set of new features that allows for more powerful logging
+
by utilising Erlang's [`logger`][erl-log] features. Here I will try to describe
+
new possibilities and how You can use them to improve your logs.
+
+
<!-- more -->
+
+
## New log levels {#levels}
+
+
Elixir gained 4 new log levels to total 8 (from most verbose to least verbose):
+
+
- debug
+
- info
+
- **notice** *
+
- warning (renamed from warn)
+
- error
+
- **critical** *
+
- **alert** *
+
- **emergency** *
+
+
<small>* new levels</small>
+
+
This allow to provide finer graded verbosity control, due to compatibility
+
reasons, in Elixir backends we need to translate these levels back to "old" set
+
of 4. The current table looks like:
+
+
| Call level | What Elixir backend will see |
+
| -- | -- |
+
| `debug` | `debug` |
+
| `info` | `info` |
+
| `notice` | **`info`** * |
+
| `warning` (or `warn`) | `warn` |
+
| `error` | `error` |
+
| `critical` | **`error`** * |
+
| `alert` | **`error`** * |
+
| `emergency` | **`error`** * |
+
+
<small>* "translated" messages</small>
+
+
We can set verbosity to all levels. This may be confusing during the transition
+
period, but we cannot change the behaviour until Elixir 2 (which is not
+
happening any time soon).
+
+
Usage of the new levels is "obvious":
+
+
```elixir
+
Logger.notice("Hello")
+
```
+
+
Will produce message with `notice` level of verbosity.
+
+
Additionally the `logger.level` option in configuration supports 2 additional
+
verbosity levels that you can use in your config:
+
+
- `:all` - all messages will be logged, logically exactly the same as `:debug`
+
- `:none` - no messages will be logged
+
+
## Per module log level {#per-module-level}
+
+
This is change that can be quite handy during debugging sessions. With this
+
change we have 4 new functions in `Logger` module:
+
+
- [`get_module_level/1`](https://hexdocs.pm/logger/Logger.html#get_module_level/1)
+
- [`put_module_level/2`](https://hexdocs.pm/logger/Logger.html#put_module_level/2)
+
- [`delete_module_level/1`](https://hexdocs.pm/logger/Logger.html#delete_module_level/1)
+
- [`delete_all_module_level/0`](https://hexdocs.pm/logger/Logger.html#delete_all_module_level/0)
+
+
These allow us to manipulate verbosity level on per-module basis. What is
+
non-obvious and is super handy is that it allows both lowering **and raising**
+
verbosity for given module. This mean that:
+
+
```elixir
+
require Logger
+
+
Logger.configure(level: :error)
+
+
defmodule Foo do
+
def run do
+
Logger.debug("I am still there")
+
end
+
end
+
+
Foo.run() # Does not log anything
+
+
# Set `debug` level for `Foo` module only
+
Logger.put_module_level(Foo, :debug)
+
Foo.run()
+
# `I am still there` is logged
+
Logger.debug("I will not be printed")
+
# Nothing got logged as top-level verbositi is still set to `:error`
+
```
+
+
Of course it will not work if you decide to use [compile time purging][logger-purge]
+
+
## Logger handlers {#handlers}
+
+
---
+
+
**Warning!** This is not fully implemented in both - Erlang and Elixir. Writing
+
your own handlers without additional knowledge can cause overload problems.
+
+
---
+
+
Erlang together with their logging implementation needed to provide a way to
+
ingest these logs somehow. This is done via Erlang logger handlers (in this
+
article called *handlers* in contrast to Elixir backends called *backends*
+
there).
+
+
Handlers are modules that export at least 1 function `log/2` that takes 2
+
arguments:
+
+
- `log_event` which is a map with 3 fields:
+
- `:level` - verbosity level
+
- `:msg` - tuple describing message:
+
- `{:io.format(), [term()]}` - format string and list of terms that should
+
be passed to `:io_lib.format/2` function
+
- `{:report, map() | keyword()}` - report that can be formatted into string
+
by `report_cb/{1,2}` set in metadata map (see below)
+
- `{:string, :unicode.chardata()}` - raw string that should be printed as
+
a message
+
- `:meta` - map containing all metadata for given event. All keys should be
+
atoms and values can be anything. Some keys have special meaning, and some
+
of them will be populated automatically by the `Logger` macros and functions.
+
These are:
+
- `:pid` - PID of the process that fired log event
+
- `:gl` - group leader of the process that fired log event
+
- `:mfa` - tuple in form of `{module(), name :: atom(), arity :: non_neg_integer()}`
+
that describe function that fired log event
+
- `:file` - filename of file that defines the code that fired log event
+
- `:line` - line in the given file where the log event was fired
+
- `:domain` - list of atoms that can be used to describe log events
+
hierarchy which then can be used for filtering. All events fired using
+
`Logger` macros and functions will have `:elixir` prepended to their
+
domain list.
+
- `:report_cb` - function that will be used to format `{:report, map() |
+
keyword()}` messages. This can be either 1-ary function, that takes report
+
and returns `{:io.format(), [term()]}` leaving truncation and further
+
formatting up to the main formatter, or 2-ary function that takes report
+
and configuration map `%{depth: pos_integer() | :unlimited, chars_limit:
+
pos_integer() | :unlimited, single_line: boolean()}` and returns already
+
formatted `:unicode.chardata()`. More about it can be found in [separate
+
section](#structured-logging).
+
+
Return value of this function is ignored. If there will be any exception raised
+
when calling this function, then it will be captured and failing handler will be
+
removed. This is important, as if such handler is the only one, then you can be
+
left without any logging handler and miss logs.
+
+
The important thing about Erlang handlers and Elixir backends is that Erlang
+
handlers functions are called **within caller process** while Elixir backends
+
are called in separate process. This mean that wrongly written Erlang handler
+
can cause quite substantial load on application.
+
+
To read on other, optional, callbacks that can be defined by Erlang handler, that
+
will not be covered there, I suggest looking into [Erlang documentation][formatter_cb].
+
+
## Structured logging {#structured-logging}
+
+
One of the biggest new features in the Elixir 1.11 is support for structured
+
logging. This mean that the log message do not need to be free-form string, but
+
instead we can pass structure, that can provide more machine-readable data for
+
processing in log aggregators. In Elixir 1.11 is simple as passing map as a
+
first argument to the `Logger` macros:
+
+
```elixir
+
Logger.info(%{
+
status: :completed,
+
response: :ok
+
})
+
```
+
+
This will produce message that looks like:
+
+
```log
+
14:08:46.849 [info] [response: :ok, status: :completed]
+
```
+
+
As we can see, the map (called *report*) is formatted as a keyword list. This is
+
default way to present the report data. Unfortunately we cannot access the
+
metadata from the Elixir backends, but we have 2 ways to make these messages
+
more readable for the human operator:
+
+
1. Utilise [`Logger`'s translators](https://hexdocs.pm/logger/Logger.Translator.html)
+
1. Using `:report_cb` field in metadata
+
+
1st option is described quite good in Elixir documentation and is available
+
since Elixir 1.0 as it was used to translate `error_logger` messages in old
+
Erlang versions. Here I will describe the 2nd option which provide way for
+
**caller** to define how report should be formatted into human-readable string.
+
+
`:report_cb` accepts 2 kind of functions as an argument:
+
+
- 1-ary function, that takes report as an argument and should return tuple
+
in form of `{:io.format(), [term()]}` that will be later formatted
+
respectively by the formatters.
+
- 2-ary function that takes report and configuration map as an arguments and
+
should return formatted string.
+
+
1st option is much easier for most use cases, as it do not force you to worry
+
about handling width, depth, and multiline logs, as it will all be handled for
+
you.
+
+
For example, instead of doing:
+
+
```elixir
+
Logger.info("Started HTTP server on http://localhost:8080")
+
```
+
+
We can do:
+
+
```elixir
+
Logger.info(
+
%{
+
protocol: :http,
+
port: 8080,
+
address: "localhost",
+
endpoint: MyEndpoint,
+
handler: Plug.Cowboy
+
},
+
report_cb: &__MODULE__.report_cb/1
+
)
+
+
# …
+
+
def report_cb(%{protocol: protocol, port: port, address: address}) do
+
{"Started ~s server on ~s://~s:~B", [protocol, protocol, address, port]}
+
end
+
```
+
+
While the second entry seems much more verbose, with proper handler, it can
+
provide much more detailed output. Just imagine that we would have handler that
+
output JSON data and what information we could contain in such message:
+
+
```json
+
{
+
"msg": "Started HTTP server on http://localhost:8080",
+
"metadata": {
+
"mfa": "MyMod.start/2",
+
"file": "foo.ex",
+
"line": 42
+
}
+
}
+
```
+
+
Now our log aggregation service need to parse `msg` field to extract all
+
information that is contained there, like port, address, and protocol. With
+
structured logging we can have that message available already there while
+
presenting the "human readable" form as well:
+
+
```json
+
{
+
"text": "Started HTTP server on http://localhost:8080",
+
"msg": {
+
"address": "localhost",
+
"port": 8080,
+
"protocol": "http",
+
"endpoint": "MyEndpoint",
+
"handler": "Plug.Cowboy"
+
},
+
"metadata": {
+
"mfa": "MyMod.start/2",
+
"file": "foo.ex",
+
"line": 42
+
}
+
}
+
```
+
+
You can see there that we can have more information available in the structured
+
log that would otherwise needed to be crammed somewhere into the text message,
+
even if it is not important in "regular" Ops observability.
+
+
This can raise a question - why not use metadata for such functionality, like it
+
is available in [`LoggerJSON`][] or [`Ink`][]? The reason is that their reason
+
existence is different. Metadata meant for "meta" stuff like location, tracing
+
ID, but not for the information about the message itself. It is best shown on
+
example. For this use Elixir's implementation of `GenServer` wrapper that
+
produces error log entry on unknown message handled by default `handle_info/2`:
+
+
```elixir
+
Logger.error(
+
# Report
+
%{
+
label: {GenServer, :no_handle_info},
+
report: %{
+
module: __MODULE__,
+
message: msg,
+
name: proc
+
}
+
},
+
# Metadata
+
%{
+
error_logger: %{tag: :error_msg},
+
report_cb: &GenServer.format_report/1
+
}
+
)
+
```
+
+
As we can see there, the report contains information like:
+
+
- `:label` - that describes type of the event
+
- `:report` - content of the "main" event
+
- `:module` - module that created the event, it is important to notice, that
+
it is also present in metadata (as part of `:mfa` key), but their meaning is
+
different. Module name here is meant for the operator to know the name of
+
the implementor that failed to handle message, while `:mfa` is meant to
+
describe the location of the code that fired the event.
+
- `:message` - the message itself that hasn't been handled. Notice, that it is
+
not stringified in any way there, it is simply passed "as is" to the
+
report. It is meant to be stringified later by the `:report_cb` function.
+
- `:name` - name of the process. Remember, similarly to `:module`, the PID of
+
the current process is part of the metadata, so in theory we could use value
+
from there, but their meaning is different (additionally this one may be an
+
atom in case if the process is locally registered with name).
+
+
Metadata on the other hand contains information that will be useful for
+
filtering or formatting of the event.
+
+
The rule of thumb you can follow is:
+
+
> If it is thing that you will want to filter on, then it probably should be
+
> part of the metadata. If you want to aggregate information or just display
+
> them, it should be part of the message report.
+
+
## Log filtering
+
+
Finally we come to first feature that is not directly accessible from the Elixir
+
`Logger` API (yet). Erlang's `logger` have powerful functionality for filtering
+
log messages which allows us to dynamically decide which message should, or
+
should not be logged. These even can alter messages on the fly.
+
+
Currently that functionality is available only via `:logger` module. It can be
+
used like:
+
+
```elixir
+
defmodule MyFilter do
+
def filter(log_event, opts) do
+
# …
+
end
+
end
+
+
:logger.add_primary_filter(:my_filter, {&MyFilter.filter/2, opts})
+
# Or
+
:logger.add_handler_filter(handler_id, :my_filter, {&MyFilter.filter/2, opts})
+
```
+
+
Few important things that need to be remembered when writing such filters:
+
+
- It is best practice to make such functions public and define filters using
+
remote function capture, like `&__MODULE__.process_disabled/2` (so not
+
anonymous functions either). It will make such filter much easier for VM to
+
handle (it is bigger topic why it is that, I may to cover it in another post).
+
- Filters are ran **within the same process that fired log event**, so it is
+
important to make such filters as fast as possible, and do not do any heavy
+
work there.
+
+
Filters can be used for 2 different things:
+
+
- preventing some messages from being logged
+
- modifying a message
+
+
While the former is much more common, I will try to describe both use cases
+
there, as the latter is also quite useful.
+
+
Filters are defined as 2-ary functions where 1st argument is log event, and
+
second argument is any term that can be used as a configuration for filter.
+
Filter should return one of these 3 values:
+
+
- `:stop` - which will immediately discard message and do not run any additional
+
filters.
+
- `:ignore` - which mean that given filter didn't recognise the given message
+
and leaves it up to other filters to decide on the action. If all filters
+
return `:ignore` then `:filter_default` option for the handler will be taken.
+
By default it is `:log`, which mean that message will be logged, but default
+
handler has it set to `:stop` by default, which mean, that non-matching
+
messages will be discarded.
+
- Just log event (possibly modified) that will cause next filter to be called
+
with altered message. The message returned by the last filter (or in case of
+
`:ignore` return, previous filters) will be the message passed to handler.
+
+
### Preventing some messages from being logged
+
+
Most common use-case for filters will probably be rejecting messages that aren't
+
important for us. [Erlang even prepared some useful filters][logger_filters]:
+
+
- `domain` - allow filtering by metadata `:domain` field (remember as I said
+
that metadata is for filtering?). It supports multiple possible relations
+
between the log domain and defined domain.
+
- `level` - allow filtering (in or out) messages depending on their level, in
+
both directions. It will allow you to filter messages with higher level for
+
some handlers. Just remember, that it will not receive messages that will not
+
pass primary/module level.
+
- `progress` - filters all reports from `supervisor` and
+
`application_controller`. Simply, reduces startup/process shutdown chatter
+
that often is meaningless for most time.
+
- `remote_gl` - filters messages coming from group leader on another node.
+
Useful when you want to discard/log messages coming from other nodes in
+
cluster.
+
+
### Modifying a message
+
+
Sometimes there is need to alter messages in the system. For example we may need
+
to prevent sensitive information from being logged. When using "old" Elixir
+
approach you could abuse translators, but that was error prone, as first
+
successful translator was breaking pipeline, so you couldn't just smash one on
+
top and then keep rest working as is. With "new" approach and structured logging
+
you can just traverse the report and replace all occurrences of the unsafe data
+
with anonymised data. For example:
+
+
```elixir
+
def filter_out_password(%{msg: {:report, report}} = event, _opts) do
+
%{event | msg: {:report, replace(report)}}
+
end
+
+
@filtered "[FILTERED]"
+
+
defp replace(%{password: _} = map) do
+
for {k, v} <- %{map | password: @filtered}, into: %{} do
+
{k, replace(v)}
+
end
+
end
+
+
defp replace(%{"password" => _} = map) do
+
for {k, v} <- %{map | "password" => @filtered}, into: %{} do
+
{k, replace(v)}
+
end
+
end
+
+
defp replace(list) when is_list(list) do
+
for elem <- list do
+
case elem do
+
{:password, _} -> {:password, @filtered}
+
{"password", _} -> {"password", @filtered}
+
{k, v} -> {k, replace(v)}
+
other -> replace(other)
+
end
+
end
+
end
+
+
defp replace(other), do: other
+
```
+
+
This snippet will replace all occurrences of `:password` or `"password"` with
+
filtered out value.
+
+
The disadvantage of such approach - it will make all messages with such fields
+
allowed in case if your filter has `:filter_default` set to `:stop`. That mean,
+
that if you want to make some of them rejected anyway, then you will need to
+
manually add additional step to reject messages that do not fit into your
+
patterns. Alternatively you can use `filter_default: :log` and then use opt-out
+
logging. There currently is no way to alter the message and make other filters
+
decide whether log it or not (as of OTP 24).
+
+
## Summary
+
+
New features and possibilities with relation to logging in Elixir 1.11 can be
+
overwhelming. Fortunately all of the new features are optional and provided in
+
addition to "good 'ol `Logger.info("logging")`". But for the people who works on
+
the observability in BEAM (EEF Observability WG, Sentry, Logflare, etc.) it
+
brings a lot of new powerful capabilities.
+
+
I am thrilled to see what will people create using all that power.
+
+
[erl-log]: https://erlang.org/doc/man/logger.html
+
[syslog]: https://en.wikipedia.org/wiki/Syslog#Severity_level
+
[`LoggerJSON`]: https://github.com/Nebo15/logger_json
+
[`Ink`]: https://hex.pm/packages/ink
+
[logger_filters]: https://erlang.org/doc/man/logger_filters.html
+
[logger-purge]: https://hexdocs.pm/logger/Logger.html#module-application-configuration
+
[formatter_cb]: https://erlang.org/doc/man/logger.html#formatter-callback-functions
-1
content/post/stop-spreading-crap-at-my-home.md
···
[taxonomies]
tags = [
-
"programming",
"culture"
]
+++
···
[taxonomies]
tags = [
"culture"
]
+++
-2
content/post/treachery-of-representation.md
···
[taxonomies]
tags = [
-
"programming",
-
"linguistic",
"culture"
]
+++
···
[taxonomies]
tags = [
"culture"
]
+++
+2 -5
content/post/vim-for-elixir.md
···
[taxonomies]
tags = [
-
"elixir",
-
"erlang",
-
"vim",
-
"neovim",
-
"programming"
]
+++
···
[taxonomies]
tags = [
+
"beam",
+
"vim"
]
+++
+511
content/post/who-watches-watchmen-i.md
···
···
+
+++
+
title = "Who Watches Watchmen? - Part 1"
+
date = 2022-01-17T21:22:18+01:00
+
+
description = """
+
A lot of application use systems like Kubernetes for their deployment. In my
+
humble opinion it is often overkill as system, that offers most of the stuff such
+
thing provide, is already present in your OS. In this article I will try to
+
present how to utilise the most popular system supervisor from Elixir
+
applications.
+
"""
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"systemd"
+
]
+
+++
+
+
I gave talk about this topic on CODE Beam V Americas, but I wasn't really
+
satisfied with it. In this post I will try to describe what my presentation was
+
meant to be about.
+
+
If you are wondering about the presentation, [the slides are on SpeakerDeck][slides].
+
+
[slides]: https://speakerdeck.com/hauleth/who-supervises-supervisors
+
+
## Abstract
+
+
Most of the operating systems are multi-process and multi-user operating
+
systems. This has a lot of positive aspects, like to be able to do more than one
+
thing at the time at our devices, but it introduces a lot of complexities that
+
in most cases are hidden from the users and developers. These things still need
+
to be handled in one or another way. The most basic problems are:
+
+
- some processes need to be started before user can interact with the OS
+
in meaningful (for them) way (for example mounting filesystems, logging,
+
etc.)
+
- some processes require strict startup ordering, for example you may need
+
logging to be started before starting HTTP server
+
- system operator somehow need to know when the process is ready to do their
+
work, which is often some time after process start
+
- system operator should be able to check process state in case when debugging
+
is needed, most commonly via logs
+
- shutdown of the processes should be handled in a way, that will allow other
+
processes to be shut down cleanly (for example application that uses DB should
+
be down before DB itself)
+
+
## Why we need system supervisor?
+
+
System supervisor is a process started early in the OS boot, that should handle
+
starting and managing all other processes that will be run on our system. It is
+
often the init process (first process started by the OS that is running with PID
+
1\) or it is first (and sometimes only) process started by the init process.
+
Popular examples of such supervisors (often integrated with init systems):
+
+
- SysV which is "traditional" implementation that originates at UNIX System
+
V (hence the name)
+
- BSD init that with some variations is used in BSD-based OSes (NetBSD,
+
FreeBSD), it shares some similarities to SysV init and services description is
+
provided by shell scripts
+
- OpenRC that also uses shell-based scripts for service description, used by
+
Linux distributions like Gentoo or Alpine
+
- `launchd` that is used on Darwin (macOS, iPadOS, iOS, watchOS) systems that uses
+
XML-based `plists` for services description
+
- `runit` which is small init and supervisor, but quite capable, for example
+
used by Void Linux
+
- Upstart created by Canonical Ltd. as a replacement for SysV-like init system
+
in Ubuntu (no longer in use in Ubuntu), still used in some distributions like
+
ChromeOS or Synology NAS
+
- `systemd` (this is the name, not "SystemD") that was created by Red Hat
+
employee, (in)famous Lennart Poettering, and later was adopted by almost all
+
major Linux distributions which spawned some heated discussion about it
+
+
In this article I will focus on systemd, and its approach to "new-style system
+
daemons".
+
+
---
+
+
**DISCLAIMER**
+
+
Each of the solutions mentioned above has its strong and weak points. I do not
+
want to start another flame war whether it is good or not. It has some good in
+
it, and it has some bad in it, but we can say that it "won" over the most used
+
distributions, and despite our love or hate towards it, we need to learn how to
+
live with that.
+
+
---
+
+
## Why `systemd`?
+
+
`systemd` became a thing because SysV approach to ordering services' startup was
+
mildly irritating and non-parallelizable. In short, SysV is starting processes
+
exactly in lexicographical order of files in given directory. This meant, that
+
even if your service didn't need the DB at all, but it somehow ended further in
+
the directory listing, you ended in waiting for the DB startup. Additionally,
+
SysV wasn't really monitoring services, it just assumed that when process forked
+
itself to the background, then it is "done" with the startup, and we can
+
continue. This is obviously not true in many cases, for example, if your
+
previous shutdown wasn't clean because of power shortage or other issue, then
+
your DB probably need a bit of time to rebuild state from journal. This causes
+
even more slowdown for the processes further in the list. This is highly
+
undesired in modern, cloud-based, environment, where you can often start the
+
machines on-demand during autoscaling actions. When there is a spike in the
+
traffic that need autoscaling, then the sooner new machine is in usable state
+
the sooner it can take load from other machines.
+
+
Different tools take different approach to solve that issue there. `systemd`
+
take approach that is derived from `launchd` - do not do stuff, that is not
+
needed. We can trigger on action of other services (obviously), but also on
+
stuff like socket activity, path creation/modification, mounts, connection or
+
disconnection of device, time events, etc.
+
+
---
+
+
**DIGRESSION**
+
+
This is exactly the reason why `systemd` has its infamous "feature creep", it
+
doesn't "digest" all services like Cron or `udev`. It is not that these are
+
"tightly" intertwined into `systemd`. You can still replace them with their
+
older counterparts, you will just lose all the features these bring with them.
+
+
---
+
+
Such lazy approach sometimes require changes into the service itself. For
+
example to let supervisor know, that you are ready (not just started), you need
+
some way to communicate with supervisor. In `systemd` you can do so via UNIX
+
socket pointed by `NOTIFY_SOCKET` environment variable passed to your
+
application. With the same socket you can implement another useful feature
+
\- watchdog/heartbeat process. This mean that if for any reason your process
+
became non-responsive (but it will refuse to die), then supervisor will
+
forcefully bring process down and restart it, assuming that the error was
+
accidental.
+
+
About restarting, we can define behaviour of service after main process die. It
+
can be restarted regardless of the exit code, it can be restarted on abnormal
+
exit, it can remain shut, etc. Does this ring a bell? This works similarly to
+
OTP supervisors, but "one level above". If your service utilize system
+
supervisor right, you can make your application almost ultimately self-healing
+
(by restarts).
+
+
## Basic setup
+
+
Now, when we know a little about how and why `systemd` works as it works, we
+
now can go to details on how to utilize that with services in Elixir.
+
+
As a base we will implement super simple Plug application:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
```elixir
+
# hello/router.ex
+
defmodule Hello.Router do
+
use Plug.Router
+
+
plug :match
+
plug :dispatch
+
+
get "/" do
+
send_resp(conn, 200, "Hello World!")
+
end
+
end
+
```
+
+
I will also assume that we are using [Mix release][mix-release] named `hello`
+
that we later copy to `/opt/hello`.
+
+
[mix-release]: https://hexdocs.pm/mix/Mix.Tasks.Release.html
+
+
### systemd unit file
+
+
We have only one thing left, we need to define our [`hello.service`][systemd.service]:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
```
+
+
Now you can create file with that content in
+
`/usr/local/lib/systemd/system/hello.service` and then start it with:
+
+
```
+
# systemctl start hello.service
+
```
+
+
This is the simplest service imaginable, however from the start we have few
+
issues there:
+
+
- It will run service as user running supervisor, so if it is run using global
+
supervisor, then it will run as `root`. You do not want to run anything as
+
`root`.
+
- On error it will produce (BEAM) core dump, which may contain sensitive data.
+
- It can read (and, due to being run as `root`, write) everything in the system,
+
like private data of other processes.
+
+
[systemd.service]: https://www.freedesktop.org/software/systemd/man/systemd.service.html#
+
+
## Service readiness
+
+
Erlang VM isn't really the best tool out there wrt the startup times. In
+
addition to that our application may need some preparation steps before it can
+
be marked as "ready". This is problem that I sometimes encounter in Docker,
+
where some containers do not really have any health check, and then I need to
+
have loop with check in some of the containers that depend on another one. This
+
"workaround" is frustrating, error prone, and can cause nasty Heisenbugs when
+
the timing will be wrong.
+
+
Two possible solutions for this problem are:
+
+
- Readiness probe - another program that is ran after the main process is
+
started, that checks whether our application is ready to work.
+
- Notification system where our application uses some common protocol to inform
+
the supervisor that it finished setup and is ready for work.
+
+
systemd supports the second approach via [`sd_notify`][sd_notify]. The approach
+
there is simple - we have `NOTIFY_SOCKET` environment variable that contain path
+
to the Unix datagram socket, that we can use to send information about state of
+
our application. This socket accept set of different messages, but right now,
+
for our purposes, we will focus only on few of them:
+
+
- `READY=1` - marks our service as ready, aka it is ready to do its work (for
+
example accept incoming HTTP connections in our example). It need to be sent
+
within given timespan after start of the VM, otherwise the process will be
+
killed and possibly restarted
+
- `STATUS=name` - sets status of our application that can be checked via
+
`systemctl status hello.service`, this allows us to have better insight into
+
what is the high level state without manually traversing through logs
+
- `RELOADING=1` - marks, that our application is reloading, which in general may
+
mean a lot of things, but there it will be used to mark `:init.restart/0`-like
+
behaviour (due to [erlang/otp#4698][] there is wrapper for that function in
+
`systemd` library). The process need then to send `READY=1` within given
+
timespan, or the process will be marked as a malfunctioning, and will be
+
forcefully killed and possibly restarted
+
- `STOPPING=1` - marks, that our application began shutting down process, and
+
will be closing soon. If the process will not close within given timespan, it
+
will be forcefully killed
+
+
These messages provide us enough power to not only mark the service as ready,
+
but also provides additional information about system state, so even operator,
+
who knows a little about Erlang or our application runtime, will be able to
+
understand what is going on.
+
+
The main thing is that systemd will wait with activation of the dependants of
+
our system as well as the `systemctl start` and `systemctl restart` commands
+
will wait until our service declare that it is ready.
+
+
Usage of such feature is quite simple:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
# Define `Type=` to `notify`
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
```
+
+
And then in our supervisor tree we need add `:systemd.ready()` **after** last
+
process needed for proper functioning of our application, in our simple example
+
it is after `Plug.Cowboy`:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
:systemd.ready(), # <-- it is function call, as it returns proper
+
# `child_spec/0`
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
Now restarting our service will not finish immediately, but will wait until our
+
service will declare that it is ready.
+
+
```
+
# systemctl restart hello.service
+
```
+
+
About `STOPPING=1` - the magic thing is that the `systemd` library takes care of
+
it for you. As soon as the system will be scheduled to shutdown this message
+
will be automatically sent, and the operator will be notified about this fact.
+
+
We can also provide more information about state of our application. As you may
+
have already noticed, we have [`Plug.Cowboy.Drainer`][] there. It is process that
+
will delay shutdown of our application while there are still open connections.
+
This can take some time, so it would be handy if the operator would see that the
+
draining is in progress. We can easily achieve that by again changing our
+
supervision tree to:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts()},
+
:systemd.ready(),
+
:systemd.set_status(down: [status: "drained"]),
+
{Plug.Cowboy.Drainer, refs: :all, shutdown: 10_000},
+
:systemd.set_status(down: [status: "draining"])
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts do
+
[
+
port: String.to_integer(System.get_env("PORT", "4000"))
+
]
+
end
+
end
+
```
+
+
Now when we will shutdown our application by:
+
+
```
+
# systemctl stop hello.service
+
```
+
+
And we have some connections open to our service (you can simulate that with
+
`wrk`) then when we ran `systemctl status hello.service` in separate terminal
+
(previous will be blocked until our service shuts down) then you will be able to
+
see something like:
+
+
```
+
● hello.service - Example Plug application
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: deactivating (stop-sigterm) since Sat 2022-01-15 17:46:30 CET;
+
1s ago
+
Main PID: 1327 (beam.smp)
+
Status: "draining"
+
Tasks: 19 (limit: 1136)
+
Memory: 106.5M
+
```
+
+
You can notice that the `Status` is set to `"draining"`. As soon as all
+
connections will be drained it will change to `"drained"` and then the
+
application will shut down and service will be marked as `inactive`.
+
+
[sd_notify]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
+
[erlang/otp#4698]: https://github.com/erlang/otp/issues/4698
+
[`Plug.Cowboy.Drainer`]: https://hexdocs.pm/plug_cowboy/2.5.2/Plug.Cowboy.Drainer.html
+
+
## Watchdog
+
+
Watchdog allows us to monitor our application for responsiveness (as mentioned
+
above). It is simple feature that requires our application to ping systemd
+
within specified interval, otherwise the application will be forcibly shut down
+
as malfunctioning. Fortunately for us, the `systemd` library that provides our
+
integration, have that feature out of the box, so all we need to do to achieve
+
expected result is set `WatchdogSec=` option in our `systemd.service` file:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
+
[Service]
+
Environment=PORT=80
+
Type=notify
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
```
+
+
This configuration says that if the VM will not send healthy message each 1
+
minute interval, then the service will be marked as malfunctioning. From the
+
application side we can manage state of the watchdog in several ways:
+
+
- By setting `systemd.watchdog_check` configuration option we can configure the
+
function that will be called on each check, if that function return `true`
+
then it mean that application is healthy and the systemd should be notified
+
with ping, if it returns `false` or fail, then the check will be omitted.
+
- Manually sending trigger message in case of detected problems via
+
`:systemd.watchdog(trigger)`, it will immediately mark service as
+
malfunctioning and will trigger action defined in service unit file (by
+
default it will restart application)
+
- Disabling built in watchdog process via `:systemd.watchdog(:disable)` and then
+
manually sending `:systemd.watchdog(:ping)` within expected intervals
+
(discouraged)
+
+
## Security
+
+
We should start with changing default user and group which is assigned to our
+
process. We can do so in 2 different ways:
+
+
1. Use some existing user and group by defining `User=` and `Group=` directives
+
in our service definition; or
+
2. Create ephemeral user on-demand before our service starts, by using directive
+
`DynamicUser=true` in service definition.
+
+
I prefer second option, as it additionally provides a lot of other security
+
related options, like creating private `/tmp` directory, making system
+
read-only, etc. This has also some disadvantages, like removing all of given
+
data on service shutdown, however there are options to keep some data between
+
launches.
+
+
In addition to that we can add `PrivateDevices=true` that will hide all
+
physical devices from `/dev` leaving only pseudo devices like `/dev/null` or
+
`/dev/urandom` (so you will be able to use `:crypto` and `:ssl` modules without
+
problems).
+
+
Next thing is that we can do, is to [disable crash dumps generated by BEAM][crash].
+
While not strictly needed in this case, it is worth remembering, that it isn't
+
hard to achieve, just use `Environment=ERL_CRASH_DUMP_SECONDS=0`.
+
+
Our new, more secure, `hello.service` will look like:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# We need to add capability to be able to bind on port 80
+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
The problem with that configuration is that our service is now capable on
+
binding **any** port under 1024, so for example, if there is some security
+
issue, then the malicious party can open any of the restricted ports and then
+
serve whatever data they want there. This can be quite problematic, and the
+
solution for that problem will be covered in Part 2, where we will cover socket
+
passing and socket activation for our service.
+
+
With that we achieved quite basic level of isolation to what Docker (or other
+
container runtime) is providing, but it do not require `overlayfs` or anything
+
more, than what you already have on your machine. That means, updates done by
+
your system package manager will be applied to all running services. With that
+
you do not need to rebuild all your containers when there is security patch
+
issued for any of your dependencies.
+
+
Of course it only scratches the surface of what is possible with systemd wrt
+
the hardening of the services. More information can be found in [RedHat
+
article][rh-systemd-hardening] and in [`systemd-analyze security` command
+
output][systemd-analyze-security]. Possible features are:
+
+
- creation of the private networks for your services
+
- disallowing creation of socket connections that are outside of the specified
+
set of families
+
- make only some paths readable
+
- hide some paths from the process
+
- etc.
+
+
Coverage of just that topic is a little bit out of scope for this blog post, so
+
I encourage you to read the documentation of [`systemd.exec`][systemd.exec] and
+
articles mentioned above for more details.
+
+
[crash]: https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/crash_dumps
+
[rh-systemd-hardening]: https://www.redhat.com/sysadmin/mastering-systemd
+
[systemd-analyze-security]: https://www.freedesktop.org/software/systemd/man/systemd-analyze.html#systemd-analyze%20security%20%5BUNIT...%5D
+
[systemd.exec]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
+
+
## Summary
+
+
This blog post is already quite lengthy, so I will split it into separate parts.
+
There probably will be 3 of them:
+
+
- [Part 1 - Basics, security, and FD passing (this one)](./#top)
+
- [Part 2 - Socket activation](@/post/who-watches-watchmen-ii.md)
+
- Part 3 - Logging
+412
content/post/who-watches-watchmen-ii.md
···
···
+
+++
+
title = "Who Watches Watchmen? - Part 2"
+
date = 2023-11-14
+
+
description = """
+
Continuation of travel into making systemd to work for us, not against us. This
+
time we will talk about socket activation and how to make our application run
+
only when we need it to run.
+
"""
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"systemd"
+
]
+
+
[[extra.thanks]]
+
name = "Nicodemus"
+
why = "helping me with my poor English"
+
+++
+
+
This is continuation of [Part I][part-i] where I described the basics of the
+
supervising BEAM applications with systemd and how to create basic, secure
+
service for your Elixir application with it. In this article I will assume that
+
you have read [the previous one][part-i].
+
+
______________________________________________________________________
+
+
We already have our super simple service description. Just to refresh your
+
memory, it is the `hello.service` file once again:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# We need to add capability to be able to bind on port 80
+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
However there is one small problem. It allows our service to listen on **any**
+
restricted port, not just `80` that we want to listen on. This can be
+
troublesome as an attacker that gains RCE on our server can then capture any
+
traffic on any port that we do not want to open (for example exposing port 22
+
using the [`ssh`] module).
+
+
It would be nice if we could somehow inject sockets for only the ports we want
+
to listen to into our application.
+
+
## Socket passing
+
+
Thanks to the [`systemd.socket`][systemd.socket] feature we can achieve that
+
with a little work on our side.
+
+
First we need to create new unit named `hello.socket` next to our
+
`hello.service`:
+
+
```ini
+
[Unit]
+
Description=Listening socket
+
Requires=sockets.target
+
+
[Socket]
+
ListenStream=80
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
It will create a socket connected to TCP 80 (because we used `ListenStream=`,
+
and TCP is the stream protocol). By default it will bind that socket to a
+
service named the same as our socket, so now we need to edit our `hello.service`
+
a little bit:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
Requires=network.target
+
+
[Service]
+
Type=notify
+
Environment=PORT=80
+
ExecStart=/opt/hello/bin/hello start
+
WatchdogSec=1min
+
+
# See, we no longer need to insecurely allow binding to any port
+
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMP_SECONDS=0
+
```
+
+
And we need to modify our `Hello.Application.cowboy_opts/0` to handle the socket
+
which is passed to us a file descriptor:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
fds = :systemd.listen_fds()
+
+
children = [
+
{Plug.Cowboy, [scheme: :http, plug: Hello.Router] ++ cowboy_opts(fds)},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
# If there are no sockets passed to the application, then start listening on
+
# the port specified by the `PORT` environment variable
+
defp cowboy_opts([]) do
+
[port: String.to_integer(System.get_env("PORT", "5000"))]
+
end
+
+
# If there are any socket passed, then use first one
+
defp cowboy_opts([socket | _]) do
+
fd =
+
case socket do
+
# Sockets can be named, which will be passed as the second element in
+
# a tuple
+
{fd, _name} -> fd
+
# Or unnamed, and then it will be just the file descriptor
+
fd -> fd
+
end
+
+
[
+
net: :inet6, # (1)
+
port: 0, # (2)
+
fd: fd # (3)
+
]
+
end
+
end
+
```
+
+
1. Systemd sockets are IPv6 enabled (we explicitly said that we want to listen
+
on both). That means, that we need to mark our connection as an INET6
+
connection. This will not affect IPv4 (INET) connections.
+
1. We are required to pass `:port` key, but its value will be ignored, so we
+
just pass `0`.
+
1. We pass the file descriptor that will be then passed to the Cowboy listener.
+
+
Now when we will start our service:
+
+
```txt
+
# systemctl start hello.service
+
```
+
+
It will be available at `https://localhost/` while still running as an
+
unprivileged user.
+
+
### Multiple ports
+
+
The question may arise - how to allow our service to listen on more than one
+
port, for example you want to have your website available as HTTPS alongside
+
"regular" HTTP. This means that our application needs to listen on two
+
restricted ports:
+
+
- 80 - for HTTP
+
- 443 - for HTTPS
+
+
Now we need to slightly modify a little our socket service and add another one.
+
First rename our `hello.socket` to `hello-http.socket` and add a line
+
`Service=hello.service` and `FileDescriptorName=http` to `[Socket]` section, so
+
we end with:
+
+
```ini
+
[Unit]
+
Description=HTTP Socket
+
Requires=sockets.target
+
+
[Socket]
+
# We declare the name of the file descriptor here to simplify extraction
+
# in the application afterwards. By default it will be the socket name
+
# (so `hello-http` in our case), but `http` is much cleaner.
+
FileDescriptorName=http
+
ListenStream=80
+
Service=hello.service
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
Next we create a similar file, but for HTTPS named `hello-https.socket`
+
+
```ini
+
[Unit]
+
Description=HTTPS Socket
+
Requires=sockets.target
+
+
[Socket]
+
FileDescriptorName=https
+
ListenStream=443
+
Service=hello.service
+
BindIPv6Only=both
+
ReusePort=true
+
NoDelay=true
+
```
+
+
And we add the dependency on both of our sockets to the `hello.service`:
+
+
```ini
+
[Unit]
+
Description=Hello World service
+
After=hello-http.socket hello-https.socket
+
BindTo=hello-http.socket hello-https.socket
+
+
[Service]
+
ExecStart=/opt/hello/bin/hello start
+
+
# Hardening
+
DynamicUser=true
+
PrivateDevices=true
+
Environment=ERL_CRASH_DUMB_SECONDS=0
+
```
+
+
Now we need to somehow differentiate between our sockets in the
+
`Hello.Application`, so we will be able to pass the proper FD to each of the
+
listeners. The `:systemd.listen_fds/0` will return a list of file descriptors,
+
and if they are named, the format will be a 2-tuple where the first element is
+
the file descriptor and the second is the name as a string:
+
+
```elixir
+
# hello/application.ex
+
defmodule Hello.Application do
+
use Application
+
+
def start(_type, _opts) do
+
fds = :systemd.listen_fds()
+
+
router = Hello.Router
+
+
children = [
+
{Plug.Cowboy, [
+
scheme: :http,
+
plug: router
+
] ++ cowboy_opts(fds, "http")},
+
{Plug.Cowboy, [
+
scheme: :https,
+
plug: router,
+
keyfile: "path/to/keyfile.pem",
+
certfile: "path/to/certfile.pem",
+
dhfile: "path/to/dhfile.pem"
+
] ++ cowboy_opts(fds, "https")},
+
{Plug.Cowboy.Drainer, refs: :all}
+
]
+
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
+
defp cowboy_opts(fds, protocol) do
+
case List.keyfind(fds, protocol, 1) do
+
# If there is socket passed for given protocol, then use that one
+
{fd, ^protocol} ->
+
[
+
net: :inet6,
+
port: 0,
+
fd: fd
+
]
+
+
# If there are no sockets passed to the application that match
+
# the protocol, then start listening on the port specified by
+
# `PORT_{protocol}` environment variable
+
_ ->
+
[
+
port: String.to_integer(System.get_env("PORT_#{protocol}", "5000"))
+
]
+
end
+
end
+
```
+
+
Now our application will listen on both - HTTP and HTTPS, despite running as
+
unprivileged user.
+
+
## Socket activation
+
+
Now, that we can inject sockets to our application with ease we can achieve even
+
more fascinating feature - socket activation.
+
+
Some of you may used `inetd` in the past, that allows you to dynamically start
+
processes on network requests. It is quite an interesting tool that detects
+
traffic on certain ports, then spawns a new process to handle it, passing data
+
to and from that process via `STDIN` and `STDOUT`. There was a quirk though, it
+
required the spawned process to shutdown after it handled the request and it was
+
starting a new instance for each request. That works poorly with VMs like BEAM
+
that have substantial startup time and are expected to be long-running systems.
+
BEAM is capable of handling network requests on it's own.
+
+
Fortunately for us, the way that we have implemented our systemd service is all
+
that we need to have our application dynamically activated. To observe that we
+
just need to shutdown everything:
+
+
```txt
+
# systemctl stop hello-http.socket hello-https.socket hello.service
+
```
+
+
And now relaunch **only the sockets**:
+
+
```txt
+
# systemctl start hello-http.socket hello-https.socket
+
```
+
+
We can check, that our service is not running:
+
+
```txt
+
$ systemctl status hello.service
+
● hello.service - Hello World service
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: inactive (dead)
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
```
+
+
We can see the `TriggeredBy` section that tells us, that this service will be
+
started by one of the sockets listed there. Let see what will happen when we
+
will try to request anything from our application:
+
+
```txt
+
$ curl http://localhost/
+
Hello World!
+
```
+
+
You can see that we got a response from our application. This mean that our
+
application must have started, and indeed when we check:
+
+
```txt
+
$ systemctl status hello.service
+
● hello.service - Hello
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: active (running) since Thu 2022-02-03 13:20:27 CET; 4s ago
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
Main PID: 1106 (beam.smp)
+
Tasks: 19 (limit: 1136)
+
Memory: 116.7M
+
CGroup: /system.slice/hello.service
+
├─1106 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
+
└─1138 erl_child_setup 1024
+
```
+
+
It seems to be running, and if we stop it, then we will get information that it
+
still can be activated by our sockets:
+
+
```txt
+
# systemctl stop hello.service
+
Warning: Stopping hello.service, but it can still be activated by:
+
hello-http.socket hello-https.socket
+
```
+
+
That means, that systemd is still listening on the sockets that we defined, even
+
when our application is down, and will start our application again as soon as
+
there are any incoming requests.
+
+
Let test that out again:
+
+
```txt
+
$ curl http://localhost/
+
Hello World!
+
$ systemctl status hello.service
+
● hello.service - Hello
+
Loaded: loaded (/usr/local/lib/systemd/system/hello.service; static; vendor preset: enabled)
+
Active: active (running) since Thu 2022-02-03 13:22:27 CET; 4s ago
+
TriggeredBy: ● hello-http.socket ● hello-https.socket
+
Main PID: 3452 (beam.smp)
+
Tasks: 19 (limit: 1136)
+
Memory: 116.7M
+
CGroup: /system.slice/hello.service
+
├─3452 /opt/hello/erts-12.2/bin/beam.smp -- -root /opt/hello -progname erl -- -home /run/hello -- -noshell -s elixir start_cli -mode embedded -setcookie CR63SVI6L5JAMJSDL3H4XPNMOPHEWSV2FPHCHCAN65CY6ASHMXBA==== -sname hello -c>
+
└─3453 erl_child_setup 1024
+
```
+
+
Our application got launched again, automatically, just by the fact that
+
there was incoming TCP connection.
+
+
Does it work for HTTPS connection as well?
+
+
```txt
+
# systemctl stop hello.service
+
$ curl -k https://localhost/
+
Hello World!
+
```
+
+
It seems so. Independently of which port we try to reach our application on, it
+
will be automatically launched for us and the connection will be properly
+
handled. Do note that systemd will not shut down our process after serving the
+
request. It will continue to run from that point forward.
+
+
## Summary
+
+
I know that it took quite while since the last post (ca. 1.5 years), but I hope
+
that I will be able to write the final part much sooner than this.
+
+
- [Part 1 - Basics, security, and FD passing][part-i]
+
- [Part 2 - Socket activation (this one)](./#top)
+
- Part 3 - Logging
+
+
[part-i]: @/post/who-watches-watchmen-i.md
+
[systemd.socket]: https://www.freedesktop.org/software/systemd/man/systemd.socket.html
+
[`ssh`]: https://erlang.org/doc/man/ssh.html
-27
content/post/who-watches-watchmen.md
···
-
+++
-
title = "Who Watches Watchmen"
-
date = 2020-02-01T23:13:44+01:00
-
draft = true
-
+++
-
-
## OTP supervisors
-
-
If you are reading this post then you probably found it via Erlang/Elixir
-
community so you should be at least a little bit familiar with the OTP
-
supervisors concept. If you aren't yet because you have found it by other matter
-
then I highly encourage you to learn a little about it, however I will try to
-
introduce that concept a little bit there as well.
-
-
### Erlang process tree
-
-
In Erlang the basic "building block" of the whole application is something
-
called process. It is very much like system process (we will expand that
-
later), but much less costly to spawn. The
-
-
## Why we need system supervisor
-
-
## How system supervisor works
-
-
## How to use OTP with systemd
-
-
## What will be possible in future
···
+291
content/post/writing-tests.md
···
···
+
+++
+
date = 2023-11-20
+
title = "How do I write Elixir tests?"
+
+
draft = true
+
+
[taxonomies]
+
tags = [
+
"beam",
+
"testing"
+
]
+
+++
+
+
This post was created for myself to codify some basic guides that I use while
+
writing tests. If you, my dear reader, read this then there is one important
+
thing for you to remember:
+
+
These are **guides** not *rules*. Each code base is different deviations and are
+
expected and *will* happen. Just use the thing between your ears.
+
+
## `@subject` module attribute for module under test
+
+
While writing test in Elixir it is not always obvious what we are testing. Just
+
imagine test like:
+
+
```elixir
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(MyImplementation, bar)
+
end
+
```
+
+
It is not obvious at the first sight what we are testing. And it is pretty
+
simplified example, in real world it can became even harder to notice what is
+
module under test (MUT).
+
+
To resolve that I came up with a simple solution. I create module attribute
+
named `@subject` that points to the MUT:
+
+
```elixir
+
@subject MyImplementation
+
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(@subject, bar)
+
end
+
```
+
+
Now it is more obvious what is MUT and what is just wrapper code around it.
+
+
In the past I have been using `alias` with `:as` option, like:
+
+
```elixir
+
alias MyImplementation, as: Subject
+
```
+
+
However I find module attribute to be more visually outstanding and make it
+
easier for me to notice `@subject` than `Subject`. But your mileage may vary.
+
+
## `describe` with function name
+
+
That one is pretty basic, and I have seen that it is pretty standard for people:
+
when you are writing tests for module functions, then group them in `describe`
+
blocks that will contain name (and arity) of the function in the name. Example:
+
+
```elixir
+
# Module under test
+
defmodule Foo do
+
def a(x, y, z) do
+
# some code
+
end
+
end
+
+
# Tests
+
defmodule FooTest do
+
use ExUnit.Case, async: true
+
+
@subject Foo
+
+
describe "a/3" do
+
# Some tests here
+
end
+
end
+
```
+
+
This allows me to see what functionality I am testing.
+
+
Of course that doesn't apply to the Phoenix controllers, as there we do not test
+
functions, but tuples in form `{method, path}` which I then write as `METHOD
+
path`, for example `POST /users`.
+
+
## Avoid module mocking
+
+
In Elixir we have bunch of the mocking libraries out there, but most of them
+
have quite substantial issue for me - these prevent me from using `async: true`
+
for my tests. This often causes substantial performance hit, as it prevents
+
different modules to run in parallel (not single tests, *modules*, but that is
+
probably material for another post).
+
+
Instead of mocks I prefer to utilise dependency injection. Some people may argue
+
that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further
+
from truth. DI isn't related to OOP, it just have different form, called
+
function arguments. For example, if we want to have function that do something
+
with time, in particular - current time. Then instead of writing:
+
+
```elixir
+
def my_function(a, b) do
+
do_foo(a, b, DateTime.utc_now())
+
end
+
```
+
+
Which would require me to use mocks for `DateTime` or other workarounds to make
+
tests time-independent. I would do:
+
+
```elixir
+
def my_function(a, b, now \\ DateTime.utc_now()) do
+
do_foo(a, b, now)
+
end
+
```
+
+
Which still provide me the ergonomics of `my_function/2` as above, but is way
+
easier to test, as I can pass the date to the function itself. This allows me to
+
run this test in parallel as it will not cause other tests to do weird stuff
+
because of altered `DateTime` behaviour.
+
+
## Avoid `ex_machina` factories
+
+
I have poor experience with tools like `ex_machina` or similar. These often
+
bring whole [Banana Gorilla Jungle problem][bgj] back, just changed a little, as
+
now instead of just passing data around, we create all needless structures for
+
sole purpose of test, even when they aren't needed for anything.
+
+
[bgj]: https://softwareengineering.stackexchange.com/q/368797
+
+
Start with example from [ExMachina README](https://github.com/beam-community/ex_machina#overview):
+
+
```elixir
+
defmodule MyApp.Factory do
+
# with Ecto
+
use ExMachina.Ecto, repo: MyApp.Repo
+
+
# without Ecto
+
use ExMachina
+
+
def user_factory do
+
%MyApp.User{
+
name: "Jane Smith",
+
email: sequence(:email, &"email-#{&1}@example.com"),
+
role: sequence(:role, ["admin", "user", "other"]),
+
}
+
end
+
+
def article_factory do
+
title = sequence(:title, &"Use ExMachina! (Part #{&1})")
+
# derived attribute
+
slug = MyApp.Article.title_to_slug(title)
+
%MyApp.Article{
+
title: title,
+
slug: slug,
+
# associations are inserted when you call `insert`
+
author: build(:user),
+
}
+
end
+
+
# derived factory
+
def featured_article_factory do
+
struct!(
+
article_factory(),
+
%{
+
featured: true,
+
}
+
)
+
end
+
+
def comment_factory do
+
%MyApp.Comment{
+
text: "It's great!",
+
article: build(:article),
+
author: build(:user) # That line is added by me
+
}
+
end
+
end
+
```
+
+
For start we can see a single problem there - we do not validate our factories
+
against our schema changesets. Without additional tests like:
+
+
```elixir
+
@subject MyApp.Article
+
+
test "factory conforms to changeset" do
+
changeset = @subject.changeset(%@subject{}, params_for(:article))
+
+
assert changeset.valid?
+
end
+
```
+
+
We cannot be sure that our tests test what we want them to test. And if we pass
+
custom attribute values in some tests it gets even worse, because we cannot be
+
sure if these are conforming either.
+
+
That mean that our tests may be moot, because we aren't testing against real
+
situations, but against some predefined state.
+
+
Another problem is that if we need to alter the behaviour of the factory it can
+
became quite convoluted. Imagine situation when we want to test if comments by
+
author of the post have some special behaviour (for example it has some
+
additional CSS class to be able to mark them in CSS). That require from us to do
+
some dancing around passing custom attributes:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
And this is simplified example. In the past I needed to deal with situations
+
where I was creating a lot of data to pass through custom attributes to make
+
test sensible.
+
+
Instead I prefer to do stuff directly in code. Instead of relying on some
+
"magical" functions provided by some "magical" macros from external library I
+
can use what I already have - functions in my application.
+
+
Instead of:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
Write:
+
+
```elixir
+
test "comments by author are special" do
+
author = MyApp.Users.create(%{
+
name: "John Doe",
+
email: "john@example.com"
+
})
+
post = MyApp.Blog.create_article(%{
+
author: author,
+
content: "Foo bar",
+
title: "Foo bar"
+
})
+
comment = MyApp.Blog.create_comment_for(article, %{
+
author: author,
+
content: "Foo bar"
+
})
+
+
# rest of the test
+
end
+
```
+
+
It may be a little bit more verbose, but it makes tests way more readable in my
+
opinion. You have all details just in place and you know what to expect. And if
+
you need some piece of data in all (or almost all) tests within
+
module/`describe` block, then you can always can use `setup/1` blocks. Or you
+
can create function per module that will generate data for you. As long as your
+
test module is self-contained and do not receive "magical" data out of thin air,
+
it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from
+
Rails world, that make little to no sense in Elixir.
+
+
If you really need such factories, then just write your own functions that will
+
use your contexts instead of relying on another library. For example:
+
+
```elixir
+
import ExUnit.Assertions
+
+
def create_user(name, email \\ nil, attrs \\ %{}) do
+
email = email || "#{String.replace(name, " ", ".")}@example.com"
+
attrs = Map.merge(attrs, %{name: name, email: email})
+
+
assert {:ok, user} = MyApp.Users.create(attrs)
+
+
user
+
end
+
+
# And so on…
+
```
+
+
This way you do not need to check if all tests use correct validations any
+
longer, as your system will do that for you.
+2 -3
content/post/writing-vim-plugin.md
···
date = 2019-11-04T18:21:18+01:00
description = """
Article about writing Vim plugins, but not about writing Vim plugins. It is
-
how to concieve plugin, how to go from an idea to the full fledged plugin."""
[taxonomies]
tags = [
-
"vim",
-
"viml"
]
+++
···
date = 2019-11-04T18:21:18+01:00
description = """
Article about writing Vim plugins, but not about writing Vim plugins. It is
+
how to conceive plugin, how to go from an idea to the full fledged plugin."""
[taxonomies]
tags = [
+
"vim"
]
+++
-22
default.nix
···
-
{ pkgs ? import <nixpkgs> {}, ... }:
-
-
with pkgs;
-
-
stdenv.mkDerivation {
-
name = "hauleth-blog";
-
src = ./.;
-
-
nativeBuildInputs = [ git ];
-
buildInputs = [ zola ];
-
-
buildPhase = ''
-
git submodule update --init --recursive --depth=1
-
zola build -o $out
-
'';
-
-
dontInstall = true;
-
-
passthru = {
-
inherit zola;
-
};
-
}
···
+57
flake.lock
···
···
+
{
+
"nodes": {
+
"flake-utils": {
+
"inputs": {
+
"systems": "systems"
+
},
+
"locked": {
+
"lastModified": 1731533236,
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
+
}
+
},
+
"nixpkgs": {
+
"locked": {
+
"lastModified": 0,
+
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
+
"path": "/nix/store/h15y13p2w17dhpiyh8pk42v1k4c38a0h-source",
+
"type": "path"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"type": "indirect"
+
}
+
},
+
"root": {
+
"inputs": {
+
"flake-utils": "flake-utils",
+
"nixpkgs": "nixpkgs"
+
}
+
},
+
"systems": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
}
+
},
+
"root": "root",
+
"version": 7
+
}
+51
flake.nix
···
···
+
{
+
description = "Flake utils demo";
+
+
inputs.flake-utils.url = "github:numtide/flake-utils";
+
+
outputs = { self, nixpkgs, flake-utils }:
+
flake-utils.lib.eachDefaultSystem (system:
+
let
+
pkgs = import nixpkgs { inherit system; };
+
blog = pkgs.stdenvNoCC.mkDerivation {
+
name = "hauleth-blog";
+
src = ./.;
+
+
nativeBuildInputs = [
+
pkgs.zola
+
];
+
+
buildPhase = ''
+
zola --version
+
zola build --output-dir $out
+
'';
+
+
dontInstall = true;
+
};
+
in
+
{
+
apps.publish = let
+
program = pkgs.writeShellScript "publish" ''
+
cp -r ${self.packages.${system}.blog} public
+
'';
+
in {
+
type = "app";
+
program = "${program}";
+
};
+
packages = {
+
inherit blog;
+
};
+
defaultPackage = blog;
+
+
devShells.default = pkgs.mkShell {
+
inputsFrom = [ blog ];
+
+
packages = [
+
# pkgs.netlify-cli
+
pkgs.vale
+
pkgs.mdl
+
];
+
};
+
}
+
);
+
}
-2
layouts/partials/extended_head.html
···
-
<script async defer data-domain="hauleth.dev" src="https://plausible.io/js/plausible.js"></script>
-
···
+54
netlify.toml
···
···
+
[build]
+
command = "zola build"
+
publish = "public/"
+
+
[build.environment]
+
ZOLA_VERSION = "0.20.0"
+
+
[context.deploy-preview]
+
command = "zola build --drafts --base-url $DEPLOY_PRIME_URL"
+
+
[[headers]]
+
for = "/*"
+
[headers.values]
+
# Disable Google cohort tracking
+
Permission-Policy = "interest-cohort=()"
+
# Disallow showing the website in frames
+
X-Frame-Options = "DENY"
+
X-XSS-Protection = "1; mode=block"
+
X-Content-Type-Options = "nosniff"
+
X-Clacks-Overhead = "GNU Terry Pratchett"
+
Content-Security-Policy = "default-src 'self'; script-src 'self'; connect-src 'self'; img-src https:"
+
Permissions-Policy = "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()"
+
Referrer-Policy = "no-referrer-when-downgrade"
+
+
[[headers]]
+
for = "/.well-known/webfinger"
+
[headers.values]
+
Content-Type = "application/jrd+json; charset=utf-8"
+
+
[[redirects]]
+
from = "/post"
+
to = "/"
+
force = true
+
+
[[redirects]]
+
from = "/.well-known/webfinger"
+
to = "https://fosstodon.org/.well-known/webfinger?resource=acct:hauleth@fosstodon.org"
+
status = 200
+
+
[[redirects]]
+
from = "/js/script.js"
+
to = "https://plausible.io/js/script.js"
+
status = 200
+
+
[[redirects]]
+
from = "/api/event"
+
to = "https://plausible.io/api/event"
+
status = 200
+
+
[[plugins]]
+
package = "netlify-plugin-webmentions"
+
+
[plugins.inputs]
+
feedPath = "atom.xml"
+65
sass/_buttons.scss
···
···
+
button,
+
.button,
+
a.button {
+
position: relative;
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
padding: 8px 18px;
+
margin-bottom: 5px;
+
text-decoration: none;
+
text-align: center;
+
border-radius: 8px;
+
border: 1px solid transparent;
+
appearance: none;
+
cursor: pointer;
+
outline: none;
+
+
/* variants */
+
+
&.outline {
+
background: transparent;
+
box-shadow: none;
+
padding: 8px 18px;
+
+
:hover {
+
transform: none;
+
box-shadow: none;
+
}
+
}
+
+
&.primary {
+
box-shadow: 0 4px 6px rgba(50, 50, 93, .11), 0 1px 3px rgba(0, 0, 0, .08);
+
+
&:hover {
+
box-shadow: 0 2px 6px rgba(50, 50, 93, .21), 0 1px 3px rgba(0, 0, 0, .08);
+
}
+
}
+
+
&.link {
+
background: none;
+
font-size: 1rem;
+
}
+
+
/* sizes */
+
+
&.small {
+
font-size: .8rem;
+
}
+
+
&.wide {
+
min-width: 200px;
+
padding: 14px 24px;
+
}
+
}
+
+
a.read-more,
+
a.read-more:hover,
+
a.read-more:active {
+
display: inline-flex;
+
background: none;
+
box-shadow: none;
+
padding: 0;
+
margin: 20px 0;
+
max-width: 100%;
+
}
+53
sass/_footer.scss
···
···
+
@import "variables";
+
+
.footer {
+
@media print {
+
display: none;
+
}
+
+
padding: 40px 0;
+
flex-grow: 0;
+
opacity: .5;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
margin: 0;
+
width: 760px;
+
max-width: 100%;
+
+
@media (max-width: $tablet-max-width) {
+
flex-direction: column;
+
}
+
}
+
+
a {
+
color: inherit;
+
}
+
+
.copyright {
+
width: 100%;
+
display: flex;
+
flex-direction: column;
+
align-items: center;
+
justify-content: center;
+
// so `--light-color-secondary` color exists no where else in the stylings
+
// color: var(--light-color-secondary);
+
// As a substitute, I'm going to use the alpha-70 version of accent.
+
color: var(--accent-alpha-70);
+
+
& > *:first-child:not(:only-child) {
+
margin-right: 10px;
+
+
@media (max-width: $tablet-max-width) {
+
margin: 0;
+
}
+
}
+
+
@media (max-width: $tablet-max-width) {
+
// flex-direction: column;
+
margin-top: 10px;
+
}
+
}
+
}
+70
sass/_header.scss
···
···
+
@import "variables";
+
+
.header {
+
@media print {
+
display: none;
+
}
+
+
display: flex;
+
flex-direction: column;
+
position: relative;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
}
+
+
&__logo {
+
display: flex;
+
flex: 1;
+
+
&:after {
+
content: '';
+
background: repeating-linear-gradient(90deg, var(--accent), var(--accent) 2px, transparent 0, transparent 10px);
+
display: block;
+
width: 100%;
+
right: 10px;
+
}
+
+
a {
+
flex: 0 0 auto;
+
max-width: 100%;
+
text-decoration: none;
+
}
+
}
+
+
.menu {
+
margin: 20px 0;
+
--shadow-color: var(--accent-alpha-70);
+
--shadow: 0 10px var(--shadow-color), -10px 10px var(--shadow-color), 10px 10px var(--shadow-color);
+
+
@media (max-width: $tablet-max-width) {
+
margin-bottom: 0;
+
}
+
+
&__inner {
+
display: flex;
+
flex-wrap: wrap;
+
list-style: none;
+
margin: 0;
+
padding: 0;
+
+
li:not(:last-of-type) {
+
margin-right: 20px;
+
margin-bottom: 10px;
+
flex: 0 0 auto;
+
}
+
+
@media (max-width: $phone-max-width) {
+
align-items: flex-start;
+
padding: 0;
+
+
li {
+
margin: 0;
+
padding: 5px;
+
}
+
}
+
}
+
}
+
}
+8
sass/_logo.scss
···
···
+
.logo {
+
display: flex;
+
align-items: center;
+
text-decoration: none;
+
background: var(--accent);
+
color: black;
+
padding: 5px 10px;
+
}
+265
sass/_main.scss
···
···
+
@import "variables";
+
+
:root {
+
font-size: calc(1rem + 0.05vw);
+
line-height: 1.54;
+
color: var(--color);
+
+
@media print {
+
color: #000;
+
line-height: 1.2;
+
font-size: 10pt;
+
}
+
}
+
+
html {
+
box-sizing: border-box;
+
}
+
+
*,
+
*:before,
+
*:after {
+
box-sizing: inherit;
+
}
+
+
body {
+
margin: 0;
+
padding: 0;
+
font-family: ui-monospace, monospace;
+
background-color: var(--background);
+
// text-shadow: 0 0 3px currentcolor;
+
text-rendering: optimizeLegibility;
+
-webkit-font-smoothing: antialiased;
+
-webkit-overflow-scrolling: touch;
+
-webkit-text-size-adjust: 100%;
+
}
+
+
h1, h2, h3, h4, h5, h6 {
+
line-height: 1.3;
+
+
&:not(:first-child) {
+
margin-top: 40px;
+
}
+
+
.zola-anchor {
+
font-size: .75em;
+
visibility: hidden;
+
margin-left: 0.5rem;
+
vertical-align: 1%;
+
text-decoration: none;
+
border-bottom-color: transparent;
+
cursor: pointer;
+
color: var(--accent);
+
+
@media (hover: none){
+
visibility: visible;
+
}
+
}
+
+
&:hover {
+
.zola-anchor {
+
visibility: visible;
+
}
+
}
+
}
+
+
h1 {
+
font-size: 1.4rem;
+
}
+
+
h2 {
+
font-size: 1.3rem;
+
}
+
+
h3 {
+
font-size: 1.2rem;
+
}
+
+
h4, h5, h6 {
+
font-size: 1.15rem;
+
}
+
+
+
a {
+
color: inherit;
+
+
&:hover {
+
color: var(--accent);
+
text-shadow: 0 0 .25em currentcolor;
+
};
+
}
+
+
img {
+
display: block;
+
max-width: 100%;
+
}
+
+
p {
+
margin-bottom: 20px;
+
}
+
+
code {
+
font-family: ui-monospace, monospace;
+
background: var(--accent-alpha-20);
+
color: var(--accent);
+
padding: 1px 6px;
+
margin: 0 2px;
+
font-size: .95rem;
+
hyphens: none;
+
}
+
+
pre {
+
font-family: ui-monospace, monospace;
+
padding: 20px 10px;
+
font-size: .95rem;
+
overflow: auto;
+
border-top: 1px solid rgba(255, 255, 255, .1);
+
border-bottom: 1px solid rgba(255, 255, 255, .1);
+
position: relative;
+
+
+ pre {
+
border-top: 0;
+
margin-top: -40px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
white-space: pre-wrap;
+
word-wrap: break-word;
+
}
+
+
&[data-lang]::before {
+
content: attr(data-lang);
+
display: block;
+
position: absolute;
+
top: 0;
+
right: 0;
+
padding: .2em .5em;
+
font-weight: bold;
+
font-size: .95rem;
+
border-radius: 0 0 0 6px;
+
background-color: var(--accent-alpha-20);
+
}
+
+
code {
+
background: none !important;
+
margin: 0;
+
padding: 0;
+
font-size: inherit;
+
border: none;
+
}
+
}
+
+
blockquote {
+
border-top: 1px solid var(--accent);
+
border-bottom: 1px solid var(--accent);
+
margin: 40px 0;
+
padding: 25px;
+
+
@media (max-width: $phone-max-width) {
+
padding-right: 0;
+
}
+
+
> :first-child {
+
margin-top: 0;
+
position: relative;
+
+
&:before {
+
content: '>';
+
display: block;
+
position: absolute;
+
left: -25px;
+
top: .1em;
+
color: var(--accent);
+
}
+
}
+
+
> :last-child {
+
margin-bottom: 0;
+
}
+
}
+
+
table {
+
table-layout: fixed;
+
border-collapse: collapse;
+
width: 100%;
+
margin: 40px 0;
+
}
+
+
table, th, td {
+
border: 1px dashed var(--accent);
+
padding: 10px;
+
}
+
+
th {
+
color: var(--accent);
+
}
+
+
ul, ol {
+
margin-left: 30px;
+
padding: 0;
+
+
li {
+
padding-left: 1em;
+
margin-top: 5px;
+
margin-bottom: 5px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
margin-left: 20px;
+
}
+
+
ul, ol {
+
margin-top: 10px;
+
margin-bottom: 10px;
+
}
+
}
+
+
.container {
+
display: flex;
+
flex-direction: column;
+
padding: 40px;
+
max-width: 864px;
+
min-height: 100vh;
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
+
+
&.full,
+
&.center {
+
border: none;
+
margin: 0 auto;
+
}
+
+
&.full {
+
max-width: 100%;
+
}
+
+
@media (max-width: $phone-max-width) {
+
padding: 20px;
+
}
+
}
+
+
.content {
+
display: flex;
+
flex-direction: column;
+
}
+
+
hr {
+
width: 100%;
+
border: none;
+
background: var(--border-color);
+
height: 1px;
+
}
+
+
ol {
+
counter-reset: li;
+
list-style: none;
+
+
li {
+
counter-increment: li;
+
+
// Todo change it to ::marker when Safari will support it
+
&::before {
+
margin-left: -2rem;
+
content: counters(li, ".") ". ";
+
}
+
}
+
}
+81
sass/_pagination.scss
···
···
+
@import 'variables';
+
+
.pagination {
+
margin-top: 50px;
+
+
&__title {
+
display: flex;
+
text-align: center;
+
position: relative;
+
margin: 100px 0 20px;
+
+
&-h {
+
text-align: center;
+
margin: 0 auto;
+
padding: 5px 10px;
+
background: var(--background);
+
font-size: .8rem;
+
text-transform: uppercase;
+
letter-spacing: .1em;
+
z-index: 1;
+
}
+
+
hr {
+
position: absolute;
+
left: 0;
+
right: 0;
+
width: 100%;
+
margin-top: 15px;
+
z-index: 0;
+
}
+
}
+
+
&__buttons {
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
+
a {
+
text-decoration: none;
+
}
+
}
+
}
+
+
.button {
+
position: relative;
+
display: inline-flex;
+
align-items: center;
+
justify-content: center;
+
border-radius: 8px;
+
max-width: 40%;
+
padding: 0;
+
cursor: pointer;
+
appearance: none;
+
+
+ .button {
+
margin-left: 10px;
+
}
+
+
a {
+
display: flex;
+
padding: 8px 16px;
+
text-decoration: none;
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&__text {
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&.next .button__icon {
+
margin-left: 8px;
+
}
+
+
&.previous .button__icon {
+
margin-right: 8px;
+
}
+
}
+198
sass/_post.scss
···
···
+
@import "variables";
+
+
.post {
+
width: 100%;
+
text-align: justify;
+
text-wrap: pretty;
+
hyphens: auto;
+
hyphenate-limit-chars: 10;
+
margin: 20px auto;
+
padding: 20px 0 0 0;
+
+
+
@media (max-width: $tablet-max-width) {
+
margin: 0 auto;
+
max-width: 660px;
+
}
+
+
&:first-of-type {
+
padding-top: 0;
+
}
+
+
&:not(:last-of-type) {
+
border-bottom: 1px solid var(--border-color);
+
}
+
+
&-meta {
+
margin-bottom: 10px;
+
color: var(--accent-alpha-70);
+
a { text-decoration: none; }
+
a:hover { text-decoration: underline; }
+
}
+
+
&-title {
+
--border: 3px dotted var(--accent);
+
position: relative;
+
color: var(--accent);
+
margin: 0 0 15px;
+
padding-bottom: 15px;
+
border-bottom: var(--border);
+
+
&:after {
+
content: '';
+
position: absolute;
+
bottom: 2px;
+
display: block;
+
width: 100%;
+
border-bottom: var(--border);
+
}
+
+
a {
+
text-decoration: none;
+
}
+
}
+
+
&.on-list &-title { --border: none; }
+
+
&-tags {
+
display: block;
+
margin-bottom: 20px;
+
opacity: .5;
+
+
a {
+
text-decoration: none;
+
}
+
}
+
+
&-content {
+
margin-top: 30px;
+
}
+
+
&-cover {
+
border: 20px solid var(--accent);
+
background: transparent;
+
margin: 40px 0;
+
padding: 20px;
+
+
@media (max-width: $phone-max-width) {
+
padding: 10px;
+
border-width: 10px;
+
}
+
}
+
+
ul {
+
list-style: "\29bf";
+
+
ul {
+
list-style: "\25a0";
+
+
ul { list-style: "\25ba"; }
+
}
+
}
+
}
+
+
// TODO: try adapting this using a properly nested selector in the block above
+
// for ul items.
+
.tag-list {
+
@media(max-width: $phone-max-width) {
+
margin-left: 5%;
+
}
+
}
+
+
.post--regulation {
+
h1 {
+
justify-content: center;
+
}
+
+
h2 {
+
justify-content: center;
+
margin-bottom: 10px;
+
+
&+ h2 {
+
margin-top: -10px;
+
margin-bottom: 20px;
+
}
+
}
+
}
+
+
.post-list {
+
.post-date {
+
color: var(--accent-alpha-70);
+
text-decoration: none;
+
}
+
+
a {
+
text-decoration: none;
+
}
+
}
+
+
.post-toc {
+
font-size: .8rem;
+
+
.toggleable { display: none; }
+
+
label {
+
color: var(--accent-alpha-70);
+
-webkit-user-select: none; /* Safari */
+
-ms-user-select: none; /* IE 10 and IE 11 */
+
user-select: none; /* Standard syntax */
+
}
+
label::before { content: '>'; margin-right: 1rem; }
+
+
input[type="checkbox"]:checked {
+
& ~ .toggleable { display: block; }
+
& ~ label::before { content: 'v'; }
+
}
+
+
a { text-decoration: none; }
+
+
a:hover { text-decoration: underline; }
+
}
+
+
.for-hire {
+
@media print {
+
display: none;
+
}
+
+
width: 100%;
+
text-align: center;
+
font-weight: bolder;
+
+
border: 1px white solid;
+
padding: 1em;
+
margin: 2em 0;
+
}
+
+
.webmentions .url-only {
+
line-break: anywhere;
+
}
+
+
.halmos {
+
text-align: right;
+
font-size: 1.5em;
+
}
+
+
.footnote-definition {
+
@media (min-width: #{$tablet-max-width + 1px}) {
+
position: absolute;
+
left: 105%;
+
+
width: 10vw;
+
+
margin-top: -7rem;
+
}
+
+
margin-top: 1rem;
+
+
font-size: .8em;
+
+
p {
+
padding-left: .5rem;
+
display: inline;
+
}
+
+
// For some reason `:last-of-type` doesn't work
+
&:has(+ .halmos) {
+
margin-bottom: -.5rem;
+
}
+
}
+40
sass/_rings.scss
···
···
+
.rings {
+
margin-top: 1rem;
+
text-align: center;
+
+
details > summary {
+
list-style: none;
+
cursor: pointer;
+
+
&::before, &::after {
+
margin: 0 .5rem;
+
}
+
+
&::before { content: '▶'; }
+
&::after { content: '◀'; }
+
+
&::-webkit-details-marker {
+
display: none;
+
}
+
}
+
+
details[open] {
+
summary {
+
&::before, &::after {
+
content: '▼';
+
}
+
+
margin-bottom: 1rem;
+
}
+
}
+
+
ul {
+
list-style: none;
+
margin: 0;
+
}
+
+
li {
+
margin: 0;
+
padding: 0;
+
}
+
}
+2
sass/_variables.scss
···
···
+
$phone-max-width: 683px;
+
$tablet-max-width: 1199px;
+22
sass/feed.scss
···
···
+
body {
+
color: #222;
+
font-family: apple-system, system-ui, sans-serif;
+
}
+
.container {
+
align-item: center;
+
display: flex;
+
justify-content: center;
+
}
+
.item {
+
max-width: 768px;
+
}
+
a {
+
color: #4166f5;
+
text-decoration: none;
+
}
+
a:visited {
+
color: #3f00ff;
+
}
+
a:hover {
+
text-decoration: underline;
+
}
+14
sass/style.scss
···
···
+
@import 'variables';
+
@import 'buttons';
+
@import 'header';
+
@import 'logo';
+
@import 'main';
+
@import 'post';
+
@import 'pagination';
+
@import 'footer';
+
@import 'rings';
+
+
:root {
+
--phoneWidth: (max-width: #{$phone-max-width + 1px});
+
--tabletWidth: (max-width: #{$tablet-max-width + 1px});
+
}
-10
shell.nix
···
-
{ pkgs ? import <nixpkgs> {}, ... }:
-
-
let
-
blog = import ./. {};
-
in
-
pkgs.mkShell {
-
buildInputs = [
-
blog.zola
-
];
-
}
···
+9
static/_headers
···
···
+
*
+
Permission-Policy: interest-cohort=()
+
X-Frame-Options: DENY
+
X-XSS-Protection: 1; mode=block
+
X-Clacks-Overhead: GNU Terry Pratchet
+
X-Clacks-Overhead: GNU Joe Armstrong
+
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self';
+
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()
+
Referrer-Policy: strict-origin-when-cross-origin
+3
static/_redirects
···
···
+
/js/script.js https://plausible.io/js/script.js 200
+
/api/event https://plausible.io/api/event 200
+
/.well-known/webfinger https://fosstodon.org/.well-known/webfinger?resource=acct:hauleth@fosstodon.org 200
static/android-chrome-192x192.png

This is a binary file and will not be displayed.

static/android-chrome-512x512.png

This is a binary file and will not be displayed.

static/apple-touch-icon.png

This is a binary file and will not be displayed.

+58
static/atom-style.xml
···
···
+
<?xml version="1.0" encoding="utf-8"?>
+
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
+
<xsl:output method="html" encoding="UTF-8" />
+
<xsl:template match="/atom:feed">
+
<html lang="en" dir="ltr">
+
<head>
+
<title><xsl:value-of select="atom:title"/> - RSS Feed</title>
+
<meta charset="UTF-8" />
+
<meta http-equiv="x-ua-compatible" content="IE=edge,chrome=1" />
+
<meta http-equiv="content-language" content="en_US" />
+
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no" />
+
<meta name="referrer" content="none" />
+
+
<!-- FAVICONS CAN GO HERE -->
+
<link href="/feed.css" rel="stylesheet" />
+
</head>
+
<body>
+
<div class="container">
+
<div class="item">
+
<header>
+
<h1>RSS Feed</h1>
+
<h2>
+
<xsl:value-of select="atom:title"/>
+
</h2>
+
<p>
+
<xsl:value-of select="atom:subtitle"/>
+
</p>
+
<a hreflang="en" target="_blank">
+
<xsl:attribute name="href">
+
<xsl:value-of select="atom:link[@rel='related']/@href"/>
+
</xsl:attribute>
+
Visit Website &#x2192;
+
</a>
+
</header>
+
<main>
+
<h2>Recent Posts</h2>
+
<xsl:for-each select="atom:entry">
+
<article>
+
<h3>
+
<a hreflang="en" target="_blank">
+
<xsl:attribute name="href">
+
<xsl:value-of select="atom:link/@href"/>
+
</xsl:attribute>
+
<xsl:value-of select="atom:title"/>
+
</a>
+
</h3>
+
<p>
+
<xsl:value-of select="atom:summary"/>
+
</p>
+
</article>
+
</xsl:for-each>
+
</main>
+
</div>
+
</div>
+
</body>
+
</html>
+
</xsl:template>
+
</xsl:stylesheet>
static/banner.png

This is a binary file and will not be displayed.

static/favicon-16x16.png

This is a binary file and will not be displayed.

static/favicon-32x32.png

This is a binary file and will not be displayed.

static/favicon-48x48.png

This is a binary file and will not be displayed.

static/favicon.ico

This is a binary file and will not be displayed.

+17
static/favicon.svg
···
···
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:2;">
+
<g transform="matrix(1,0,0,1,-16,0)">
+
<g transform="matrix(2.62238,0,0,1.40473,7.10543e-15,1.77636e-15)">
+
<rect x="0" y="0" width="24.405" height="22.78" style="fill:rgb(35,176,255);"/>
+
</g>
+
<g id="Lambda" transform="matrix(1,0,0,1,16,0)">
+
<g transform="matrix(1,0,0,1,4.43135,0)">
+
<path d="M2.644,26.033L5.977,26.033L15.556,5.994L18.545,5.994" style="fill:none;stroke:black;stroke-width:3px;"/>
+
</g>
+
<g transform="matrix(0.990948,-0.019459,-0.019459,0.958168,4.84334,0.885678)">
+
<path d="M19.831,26.648L16.814,26.587L10.763,16.007" style="fill:none;stroke:black;stroke-width:3.14px;"/>
+
</g>
+
</g>
+
</g>
+
</svg>
static/img/common-test/log.png

This is a binary file and will not be displayed.

static/img/treachery-of-images.jpg

This is a binary file and will not be displayed.

static/img/vim-session.png

This is a binary file and will not be displayed.

+19
static/site.webmanifest
···
···
+
{
+
"name": "Hauleth's blog",
+
"short_name": "Hauleth's blog",
+
"icons": [
+
{
+
"src": "/android-chrome-192x192.png",
+
"sizes": "192x192",
+
"type": "image/png"
+
},
+
{
+
"src": "/android-chrome-512x512.png",
+
"sizes": "512x512",
+
"type": "image/png"
+
}
+
],
+
"theme_color": "#ffffff",
+
"background_color": "#ffffff",
+
"display": "standalone"
+
}
+441
static/syntax-theme.css
···
···
+
/*
+
* theme "Nord" generated by syntect
+
*/
+
+
.z-code {
+
color: #d8dee9;
+
background-color: #2e3440;
+
}
+
+
.z-comment, .z-punctuation.z-definition.z-comment {
+
color: #616e88;
+
}
+
.z-constant.z-numeric {
+
color: #b48ead;
+
}
+
.z-constant.z-language {
+
color: #81a1c1;
+
}
+
.z-constant.z-character.z-escape {
+
color: #ebcb8b;
+
}
+
.z-constant.z-other.z-placeholder {
+
color: #ebcb8b;
+
}
+
.z-constant.z-other {
+
color: #d8dee9;
+
}
+
.z-entity.z-name.z-class {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-struct {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-enum {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-union {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-trait {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-interface {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-entity.z-name.z-type {
+
color: #81a1c1;
+
}
+
.z-entity.z-other.z-inherited-class {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-function {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-function.z-constructor {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-function.z-destructor {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-namespace {
+
color: #8fbcbb;
+
}
+
.z-entity.z-name.z-constant {
+
color: #81a1c1;
+
}
+
.z-entity.z-name.z-label {
+
color: #5e81ac;
+
}
+
.z-entity.z-name.z-section {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-tag {
+
color: #81a1c1;
+
}
+
.z-entity.z-other.z-attribute-name {
+
color: #8fbcbb;
+
}
+
.z-invalid.z-illegal {
+
color: #d8dee9;
+
background-color: #bf616a;
+
}
+
.z-invalid.z-deprecated {
+
color: #d8dee9;
+
background-color: #ebcb8b;
+
}
+
.z-keyword.z-control {
+
color: #81a1c1;
+
}
+
.z-keyword.z-control.z-conditional {
+
color: #81a1c1;
+
}
+
.z-keyword.z-control.z-import {
+
color: #81a1c1;
+
}
+
.z-punctuation.z-definition.z-keyword {
+
color: #81a1c1;
+
}
+
.z-keyword.z-other {
+
color: #81a1c1;
+
}
+
.z-keyword.z-operator, .z-keyword.z-operator.z-assignment, .z-keyword.z-operator.z-arithmetic, .z-keyword.z-operator.z-bitwise, .z-keyword.z-operator.z-logical, .z-keyword.z-operator.z-word {
+
color: #81a1c1;
+
}
+
.z-markup.z-heading {
+
color: #88c0d0;
+
}
+
.z-markup.z-list.z-unnumbered, .z-markup.z-list.z-numbered {
+
color: #d8dee9;
+
}
+
.z-markup.z-bold {
+
font-weight: bold;
+
}
+
.z-markup.z-italic {
+
font-style: italic;
+
}
+
.z-markup.z-inserted {
+
color: #a3be8c;
+
}
+
.z-markup.z-deleted {
+
color: #bf616a;
+
}
+
.z-markup.z-changed {
+
color: #ebcb8b;
+
}
+
.z-markup.z-quote {
+
color: #616e88;
+
}
+
.z-markup.z-raw.z-inline, .z-markup.z-raw.z-block {
+
color: #8fbcbb;
+
}
+
.z-markup.z-other {
+
background-color: #eceff4;
+
}
+
.z-punctuation.z-separator, .z-punctuation.z-terminator {
+
color: #eceff4;
+
}
+
.z-punctuation.z-separator.z-continuation, .z-punctuation.z-accessor {
+
color: #81a1c1;
+
}
+
.z-punctuation.z-definition.z-tag {
+
color: #81a1c1;
+
}
+
.z-storage.z-type, .z-storage.z-modifier {
+
color: #81a1c1;
+
}
+
.z-string.z-quoted.z-single, .z-string.z-quoted.z-double, .z-string.z-quoted.z-triple, .z-string.z-quoted.z-other, .z-string.z-unquoted {
+
color: #a3be8c;
+
}
+
.z-punctuation.z-definition.z-string.z-begin, .z-punctuation.z-definition.z-string.z-end {
+
color: #a3be8c;
+
}
+
.z-string.z-regexp {
+
color: #ebcb8b;
+
}
+
.z-support.z-constant {
+
color: #8fbcbb;
+
}
+
.z-support.z-function {
+
color: #88c0d0;
+
}
+
.z-support.z-module {
+
color: #8fbcbb;
+
}
+
.z-support.z-type {
+
color: #8fbcbb;
+
}
+
.z-support.z-class {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-basic, .z-text.z-xml {
+
color: #eceff4;
+
}
+
.z-variable.z-other, .z-variable.z-other.z-readwrite {
+
color: #d8dee9;
+
}
+
.z-punctuation.z-definition.z-variable {
+
color: #81a1c1;
+
}
+
.z-variable.z-other.z-constant {
+
color: #81a1c1;
+
}
+
.z-variable.z-language {
+
color: #81a1c1;
+
}
+
.z-variable.z-parameter {
+
color: #d8dee9;
+
}
+
.z-variable.z-other.z-member {
+
color: #d8dee9;
+
}
+
.z-variable.z-function {
+
color: #88c0d0;
+
}
+
.z-constant.z-numeric.z-line-number.z-find-in-files {
+
color: #d8dee9;
+
}
+
.z-constant.z-numeric.z-line-number.z-match.z-find-in-files {
+
color: #88c0d0;
+
}
+
.z-entity.z-name.z-filename.z-find-in-files {
+
color: #8fbcbb;
+
}
+
.z-text.z-find-in-files {
+
color: #d8dee9;
+
}
+
.z-source.z-c .z-keyword.z-control.z-import.z-include.z-c, .z-source.z-c .z-meta.z-preprocessor.z-c .z-keyword.z-control.z-import.z-c, .z-source.z-c++ .z-keyword.z-control.z-import.z-include.z-c++, .z-source.z-c++ .z-meta.z-preprocessor.z-c++ .z-keyword.z-control.z-import.z-c++ {
+
color: #5e81ac;
+
}
+
.z-source.z-c .z-keyword.z-operator.z-word.z-c, .z-source.z-c++ .z-keyword.z-operator.z-word.z-c++ {
+
color: #88c0d0;
+
font-style: italic;
+
}
+
.z-source.z-css .z-support.z-type.z-property-name.z-css, .z-source.z-sass .z-support.z-type.z-property-name.z-sass, .z-source.z-css .z-support.z-type.z-vendor-prefix.z-css {
+
color: #d8dee9;
+
}
+
.z-source.z-css .z-constant.z-other.z-color.z-rgb-value.z-css, .z-source.z-sass .z-constant.z-other.z-rgb-value.z-sass {
+
color: #b48ead;
+
}
+
.z-source.z-css .z-support.z-constant.z-color.z-w3c-special-color-keyword.z-css, .z-source.z-css .z-support.z-constant.z-property-value.z-css, .z-source.z-sass .z-support.z-constant.z-property-value.z-sass {
+
color: #81a1c1;
+
}
+
.z-source.z-css .z-entity.z-other.z-pseudo-class.z-css {
+
color: #8fbcbb;
+
}
+
.z-source.z-css .z-keyword.z-control.z-at-rule.z-media.z-css, .z-source.z-css .z-punctuation.z-definition.z-keyword.z-css, .z-source.z-sass .z-keyword.z-control.z-at-rule.z-sass {
+
color: #d08770;
+
}
+
.z-source.z-css .z-support.z-constant.z-color.z-w3c-standard-color-name.z-css {
+
color: #81a1c1;
+
font-style: italic;
+
}
+
.z-source.z-css .z-entity.z-other.z-attribute-name.z-id.z-css, .z-source.z-sass .z-entity.z-other.z-attribute-name.z-id.z-sass {
+
font-weight: bold;
+
}
+
.z-source.z-diff .z-meta.z-diff.z-range.z-context {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-meta.z-diff.z-header.z-from-file {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-punctuation.z-definition.z-from-file {
+
color: #8fbcbb;
+
}
+
.z-source.z-diff .z-punctuation.z-definition.z-separator {
+
color: #81a1c1;
+
}
+
.z-source.z-go .z-support.z-function.z-builtin.z-go {
+
font-style: italic;
+
}
+
.z-text.z-html.z-basic .z-constant.z-character.z-entity.z-html {
+
color: #ebcb8b;
+
}
+
.z-variable.z-annotation.z-java, .z-punctuation.z-definition.z-annotation.z-java {
+
color: #d08770;
+
}
+
.z-punctuation.z-accessor.z-dot.z-java {
+
color: #eceff4;
+
}
+
.z-support.z-other.z-package.z-java {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-comment.z-block.z-documentation.z-javadoc .z-punctuation.z-definition.z-entity.z-html {
+
color: #81a1c1;
+
}
+
.z-source.z-java .z-entity.z-name.z-constant {
+
color: #d8dee9;
+
}
+
.z-source.z-java .z-keyword.z-other.z-documentation, .z-source.z-java .z-comment.z-block.z-documentation.z-javadoc .z-punctuation.z-definition.z-keyword.z-javadoc {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-variable.z-parameter.z-type.z-java {
+
color: #8fbcbb;
+
}
+
.z-source.z-java .z-entity.z-other.z-inherited-class.z-java {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-source.z-java .z-keyword.z-declaration.z-implements.z-java {
+
color: #81a1c1;
+
}
+
.z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-key.z-json .z-string.z-quoted.z-double.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-meta.z-structure.z-dictionary.z-key.z-json .z-string.z-quoted.z-double.z-json {
+
color: #8fbcbb;
+
}
+
.z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-begin.z-json, .z-source.z-json .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-end.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-begin.z-json, .z-source.z-json.z-sublime .z-meta.z-structure.z-dictionary.z-json .z-punctuation.z-definition.z-string.z-end.z-json {
+
color: #eceff4;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-raw.z-code-fence.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-raw.z-code-fence.z-end.z-markdown, .z-text.z-html.z-markdown .z-markup.z-raw.z-code-fence.z-markdown .z-constant.z-other.z-language-name.z-markdown {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-list_item.z-markdown, .z-text.z-html.z-markdown .z-markup.z-list.z-numbered.z-bullet.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-markup.z-quote.z-markdown .z-punctuation.z-definition.z-blockquote.z-markdown {
+
color: #8fbcbb;
+
}
+
.z-text.z-html.z-markdown .z-meta.z-link.z-inline.z-description.z-markdown, .z-text.z-html.z-markdown .z-meta.z-image.z-inline.z-description.z-markdown, .z-text.z-html.z-markdown .z-meta.z-link.z-reference.z-description.z-markdown, .z-text.z-html.z-markdown .z-constant.z-other.z-reference.z-link.z-markdown, .z-text.z-html.z-markdown .z-entity.z-name.z-reference.z-link.z-markdown {
+
color: #88c0d0;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-link.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-link.z-end.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-image.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-image.z-end.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-constant.z-begin.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-definition.z-constant.z-end.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-separator.z-table-cell.z-markdown, .z-text.z-html.z-markdown .z-punctuation.z-section.z-table-header.z-markdown {
+
color: #81a1c1;
+
}
+
.z-text.z-html.z-markdown .z-meta.z-table.z-header.z-markdown {
+
color: #88c0d0;
+
}
+
.z-text.z-html.z-markdown .z-punctuation.z-definition.z-thematic-break.z-markdown {
+
color: #81a1c1;
+
font-weight: bold;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-punctuation.z-section.z-embedded.z-begin.z-php, .z-embedding.z-php .z-text.z-html.z-basic .z-punctuation.z-section.z-embedded.z-end.z-php {
+
color: #5e81ac;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-comment.z-block.z-documentation.z-phpdoc.z-php .z-keyword.z-other.z-phpdoc.z-php {
+
color: #8fbcbb;
+
}
+
.z-embedding.z-php .z-text.z-html.z-basic .z-support.z-other.z-namespace.z-php {
+
color: #8fbcbb;
+
}
+
.z-source.z-python .z-meta.z-annotation.z-python .z-meta.z-qualified-name.z-python .z-support.z-function.z-builtin.z-python, .z-source.z-python .z-punctuation.z-definition.z-annotation.z-python {
+
color: #d08770;
+
}
+
.z-support.z-function.z-builtin.z-python {
+
font-style: italic;
+
}
+
.z-source.z-python .z-support.z-type.z-exception.z-python {
+
color: #8fbcbb;
+
}
+
.z-source.z-python .z-support.z-type.z-python {
+
color: #81a1c1;
+
}
+
.z-source.z-sass .z-variable.z-parameter.z-sass {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-ruby .z-support.z-function.z-builtin.z-ruby {
+
font-style: italic;
+
}
+
.z-source.z-shell .z-support.z-function.z-builtin.z-shell {
+
font-style: italic;
+
}
+
.z-text.z-xml .z-entity.z-name.z-tag.z-namespace {
+
color: #8fbcbb;
+
}
+
.z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml, .z-text.z-xml .z-meta.z-tag.z-preprocessor .z-entity.z-name.z-tag, .z-text.z-xml .z-meta.z-tag.z-preprocessor.z-xml .z-punctuation.z-definition.z-tag.z-begin.z-xml, .z-text.z-xml .z-meta.z-tag.z-preprocessor.z-xml .z-punctuation.z-definition.z-tag.z-end.z-xml, .z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml .z-punctuation.z-definition.z-tag.z-begin.z-xml, .z-text.z-xml .z-meta.z-tag.z-sgml.z-doctype.z-xml .z-punctuation.z-definition.z-tag.z-end.z-xml {
+
color: #5e81ac;
+
}
+
.z-text.z-xml .z-string.z-unquoted.z-cdata.z-xml .z-punctuation.z-definition.z-string.z-begin.z-xml, .z-text.z-xml .z-string.z-unquoted.z-cdata.z-xml .z-punctuation.z-definition.z-string.z-end.z-xml {
+
color: #d08770;
+
font-style: italic;
+
}
+
.z-source.z-yaml .z-entity.z-name.z-tag {
+
color: #8fbcbb;
+
}
+
.z-sublimelinter.z-mark.z-warning {
+
color: #ebcb8b;
+
}
+
.z-sublimelinter.z-mark.z-error {
+
color: #bf616a;
+
}
+
.z-markup.z-ignored.z-git_gutter {
+
color: #616e88;
+
}
+
.z-markup.z-untracked.z-git_gutter {
+
color: #88c0d0;
+
}
+
.z-markup.z-inserted.z-git_gutter {
+
color: #a3be8c;
+
}
+
.z-markup.z-changed.z-git_gutter {
+
color: #ebcb8b;
+
}
+
.z-markup.z-deleted.z-git_gutter {
+
color: #bf616a;
+
}
+
.z-source.z-js .z-tag.z-decorator.z-js .z-entity.z-name.z-tag.z-js, .z-source.z-js .z-tag.z-decorator.z-js .z-punctuation.z-definition.z-tag.z-js {
+
color: #d08770;
+
}
+
.z-source.z-js .z-string.z-quoted.z-js, .z-source.z-js .z-string.z-quoted.z-jsx, .z-source.z-js .z-meta.z-class.z-js .z-meta.z-class.z-property.z-js .z-string.z-interpolated.z-js, .z-source.z-js .z-string.z-interpolated.z-js, .z-source.z-js .z-string.z-template.z-js {
+
color: #a3be8c;
+
}
+
.z-source.z-js .z-variable.z-other.z-constant.z-js {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-js .z-constant.z-other.z-object.z-key.z-js .z-string.z-unquoted.z-label.z-js, .z-source.z-js .z-variable.z-other.z-readwrite.z-js {
+
color: #d8dee9;
+
}
+
.z-source.z-js .z-meta.z-class.z-js .z-meta.z-class.z-property.z-js .z-variable.z-other.z-property.z-js {
+
color: #88c0d0;
+
}
+
.z-source.z-js .z-variable.z-other.z-property.z-static.z-js {
+
font-style: italic;
+
}
+
.z-source.z-js .z-variable.z-other.z-class.z-js {
+
color: #8fbcbb;
+
}
+
.z-source.z-js .z-string.z-interpolated.z-js .z-keyword.z-other.z-substitution.z-begin.z-js, .z-source.z-js .z-string.z-interpolated.z-js .z-keyword.z-other.z-substitution.z-end.z-js, .z-source.z-js .z-punctuation.z-definition.z-template-expression.z-begin.z-js, .z-source.z-js .z-punctuation.z-definition.z-template-expression.z-end.z-js {
+
color: #5e81ac;
+
}
+
.z-source.z-js .z-keyword.z-operator.z-spread.z-js {
+
color: #81a1c1;
+
}
+
.z-source.z-js .z-meta.z-export.z-js .z-variable.z-other.z-readwrite.z-js {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-meta.z-class.z-ts .z-entity.z-other.z-inherited-class.z-ts {
+
color: #8fbcbb;
+
font-weight: bold;
+
}
+
.z-source.z-ts .z-entity.z-name.z-type.z-class.z-ts, .z-source.z-ts .z-entity.z-name.z-type.z-enum.z-ts {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-entity.z-name.z-type.z-ts {
+
color: #8fbcbb;
+
}
+
.z-source.z-ts .z-support.z-type.z-primitive.z-ts {
+
color: #81a1c1;
+
}
+
.z-source.z-ts .z-meta.z-class.z-ts .z-meta.z-method.z-declaration.z-ts .z-meta.z-parameters.z-ts .z-meta.z-decorator.z-ts .z-variable.z-other.z-readwrite.z-ts, .z-source.z-ts .z-meta.z-class.z-ts .z-meta.z-method.z-declaration.z-ts .z-meta.z-parameters.z-ts .z-meta.z-decorator.z-ts .z-punctuation.z-decorator.z-ts {
+
color: #d08770;
+
}
+
.z-source.z-ts .z-punctuation.z-definition.z-template-expression.z-begin.z-ts, .z-source.z-ts .z-punctuation.z-definition.z-template-expression.z-end.z-ts {
+
color: #5e81ac;
+
}
+
.z-source.z-ts .z-string.z-template.z-ts {
+
color: #a3be8c;
+
}
+
.z-source.z-ts .z-support.z-function.z-math.z-ts {
+
font-style: italic;
+
}
+
.z-source.z-ts .z-variable.z-other.z-constant.z-property.z-ts, .z-source.z-ts .z-variable.z-other.z-enummember.z-ts, .z-source.z-ts .z-variable.z-other.z-constant.z-ts {
+
color: #d8dee9;
+
font-weight: bold;
+
}
+
.z-source.z-ts .z-comment.z-block.z-documentation.z-ts .z-storage.z-type.z-class.z-jsdoc {
+
color: #8fbcbb;
+
}
+9
statichost.yml
···
···
+
image: ghcr.io/getzola/zola:v0.21.0
+
command: build
+
public: public
+
image_entrypoint: true
+
+
# TODO: Usa nix there
+
# image: ghcr.io/lix-project/lix:latest
+
# command: nix --extra-experimental-features nix-command --extra-experimental-features flakes run .#publish
+
# public: public
+5
styles/markdown.rb
···
···
+
all
+
exclude_rule 'MD002'
+
exclude_rule 'MD041'
+
+
rule 'MD013', ignore_code_blocks: true
+8
styles/proselint/Airlinese.yml
···
···
+
extends: existence
+
message: "'%s' is airlinese."
+
ignorecase: true
+
level: error
+
tokens:
+
- enplan(?:e|ed|ing|ement)
+
- deplan(?:e|ed|ing|ement)
+
- taking off momentarily
+48
styles/proselint/AnimalLabels.yml
···
···
+
extends: substitution
+
message: "Consider using '%s' instead of '%s'."
+
level: error
+
action:
+
name: replace
+
swap:
+
(?:bull|ox)-like: taurine
+
(?:calf|veal)-like: vituline
+
(?:crow|raven)-like: corvine
+
(?:leopard|panther)-like: pardine
+
bird-like: avine
+
centipede-like: scolopendrine
+
crab-like: cancrine
+
crocodile-like: crocodiline
+
deer-like: damine
+
eagle-like: aquiline
+
earthworm-like: lumbricine
+
falcon-like: falconine
+
ferine: wild animal-like
+
fish-like: piscine
+
fox-like: vulpine
+
frog-like: ranine
+
goat-like: hircine
+
goose-like: anserine
+
gull-like: laridine
+
hare-like: leporine
+
hawk-like: accipitrine
+
hippopotamus-like: hippopotamine
+
lizard-like: lacertine
+
mongoose-like: viverrine
+
mouse-like: murine
+
ostrich-like: struthionine
+
peacock-like: pavonine
+
porcupine-like: hystricine
+
rattlesnake-like: crotaline
+
sable-like: zibeline
+
sheep-like: ovine
+
shrew-like: soricine
+
sparrow-like: passerine
+
swallow-like: hirundine
+
swine-like: suilline
+
tiger-like: tigrine
+
viper-like: viperine
+
vulture-like: vulturine
+
wasp-like: vespine
+
wolf-like: lupine
+
woodpecker-like: picine
+
zebra-like: zebrine
+9
styles/proselint/Annotations.yml
···
···
+
extends: existence
+
message: "'%s' left in text."
+
ignorecase: false
+
level: error
+
tokens:
+
- XXX
+
- FIXME
+
- TODO
+
- NOTE
+8
styles/proselint/Apologizing.yml
···
···
+
extends: existence
+
message: "Excessive apologizing: '%s'"
+
ignorecase: true
+
level: error
+
action:
+
name: remove
+
tokens:
+
- More research is needed
+52
styles/proselint/Archaisms.yml
···
···
+
extends: existence
+
message: "'%s' is archaic."
+
ignorecase: true
+
level: error
+
tokens:
+
- alack
+
- anent
+
- begat
+
- belike
+
- betimes
+
- boughten
+
- brocage
+
- brokage
+
- camarade
+
- chiefer
+
- chiefest
+
- Christiana
+
- completely obsolescent
+
- cozen
+
- divers
+
- deflexion
+
- fain
+
- forsooth
+
- foreclose from
+
- haply
+
- howbeit
+
- illumine
+
- in sooth
+
- maugre
+
- meseems
+
- methinks
+
- nigh
+
- peradventure
+
- perchance
+
- saith
+
- shew
+
- sistren
+
- spake
+
- to wit
+
- verily
+
- whilom
+
- withal
+
- wot
+
- enclosed please find
+
- please find enclosed
+
- enclosed herewith
+
- enclosed herein
+
- inforce
+
- ex postfacto
+
- foreclose from
+
- forewent
+
- for ever
+8
styles/proselint/But.yml
···
···
+
extends: existence
+
message: "Do not start a paragraph with a 'but'."
+
level: error
+
scope: paragraph
+
action:
+
name: remove
+
tokens:
+
- ^But
+782
styles/proselint/Cliches.yml
···
···
+
extends: existence
+
message: "'%s' is a cliche."
+
level: error
+
ignorecase: true
+
tokens:
+
- a chip off the old block
+
- a clean slate
+
- a dark and stormy night
+
- a far cry
+
- a fate worse than death
+
- a fine kettle of fish
+
- a loose cannon
+
- a penny saved is a penny earned
+
- a tough row to hoe
+
- a word to the wise
+
- ace in the hole
+
- acid test
+
- add insult to injury
+
- against all odds
+
- air your dirty laundry
+
- alas and alack
+
- all fun and games
+
- all hell broke loose
+
- all in a day's work
+
- all talk, no action
+
- all thumbs
+
- all your eggs in one basket
+
- all's fair in love and war
+
- all's well that ends well
+
- almighty dollar
+
- American as apple pie
+
- an axe to grind
+
- another day, another dollar
+
- armed to the teeth
+
- as luck would have it
+
- as old as time
+
- as the crow flies
+
- at loose ends
+
- at my wits end
+
- at the end of the day
+
- avoid like the plague
+
- babe in the woods
+
- back against the wall
+
- back in the saddle
+
- back to square one
+
- back to the drawing board
+
- bad to the bone
+
- badge of honor
+
- bald faced liar
+
- bald-faced lie
+
- ballpark figure
+
- banging your head against a brick wall
+
- baptism by fire
+
- barking up the wrong tree
+
- bat out of hell
+
- be all and end all
+
- beat a dead horse
+
- beat around the bush
+
- been there, done that
+
- beggars can't be choosers
+
- behind the eight ball
+
- bend over backwards
+
- benefit of the doubt
+
- bent out of shape
+
- best thing since sliced bread
+
- bet your bottom dollar
+
- better half
+
- better late than never
+
- better mousetrap
+
- better safe than sorry
+
- between a rock and a hard place
+
- between a rock and a hard place
+
- between Scylla and Charybdis
+
- between the devil and the deep blue see
+
- betwixt and between
+
- beyond the pale
+
- bide your time
+
- big as life
+
- big cheese
+
- big fish in a small pond
+
- big man on campus
+
- bigger they are the harder they fall
+
- bird in the hand
+
- bird's eye view
+
- birds and the bees
+
- birds of a feather flock together
+
- bit the hand that feeds you
+
- bite the bullet
+
- bite the dust
+
- bitten off more than he can chew
+
- black as coal
+
- black as pitch
+
- black as the ace of spades
+
- blast from the past
+
- bleeding heart
+
- blessing in disguise
+
- blind ambition
+
- blind as a bat
+
- blind leading the blind
+
- blissful ignorance
+
- blood is thicker than water
+
- blood sweat and tears
+
- blow a fuse
+
- blow off steam
+
- blow your own horn
+
- blushing bride
+
- boils down to
+
- bolt from the blue
+
- bone to pick
+
- bored stiff
+
- bored to tears
+
- bottomless pit
+
- boys will be boys
+
- bright and early
+
- brings home the bacon
+
- broad across the beam
+
- broken record
+
- brought back to reality
+
- bulk large
+
- bull by the horns
+
- bull in a china shop
+
- burn the midnight oil
+
- burning question
+
- burning the candle at both ends
+
- burst your bubble
+
- bury the hatchet
+
- busy as a bee
+
- but that's another story
+
- by hook or by crook
+
- call a spade a spade
+
- called onto the carpet
+
- calm before the storm
+
- can of worms
+
- can't cut the mustard
+
- can't hold a candle to
+
- case of mistaken identity
+
- cast aspersions
+
- cat got your tongue
+
- cat's meow
+
- caught in the crossfire
+
- caught red-handed
+
- chase a red herring
+
- checkered past
+
- chomping at the bit
+
- cleanliness is next to godliness
+
- clear as a bell
+
- clear as mud
+
- close to the vest
+
- cock and bull story
+
- cold shoulder
+
- come hell or high water
+
- comparing apples and oranges
+
- compleat
+
- conspicuous by its absence
+
- cool as a cucumber
+
- cool, calm, and collected
+
- cost a king's ransom
+
- count your blessings
+
- crack of dawn
+
- crash course
+
- creature comforts
+
- cross that bridge when you come to it
+
- crushing blow
+
- cry like a baby
+
- cry me a river
+
- cry over spilt milk
+
- crystal clear
+
- crystal clear
+
- curiosity killed the cat
+
- cut and dried
+
- cut through the red tape
+
- cut to the chase
+
- cute as a bugs ear
+
- cute as a button
+
- cute as a puppy
+
- cuts to the quick
+
- cutting edge
+
- dark before the dawn
+
- day in, day out
+
- dead as a doornail
+
- decision-making process
+
- devil is in the details
+
- dime a dozen
+
- divide and conquer
+
- dog and pony show
+
- dog days
+
- dog eat dog
+
- dog tired
+
- don't burn your bridges
+
- don't count your chickens
+
- don't look a gift horse in the mouth
+
- don't rock the boat
+
- don't step on anyone's toes
+
- don't take any wooden nickels
+
- down and out
+
- down at the heels
+
- down in the dumps
+
- down the hatch
+
- down to earth
+
- draw the line
+
- dressed to kill
+
- dressed to the nines
+
- drives me up the wall
+
- dubious distinction
+
- dull as dishwater
+
- duly authorized
+
- dyed in the wool
+
- eagle eye
+
- ear to the ground
+
- early bird catches the worm
+
- easier said than done
+
- easy as pie
+
- eat your heart out
+
- eat your words
+
- eleventh hour
+
- even the playing field
+
- every dog has its day
+
- every fiber of my being
+
- everything but the kitchen sink
+
- eye for an eye
+
- eyes peeled
+
- face the music
+
- facts of life
+
- fair weather friend
+
- fall by the wayside
+
- fan the flames
+
- far be it from me
+
- fast and loose
+
- feast or famine
+
- feather your nest
+
- feathered friends
+
- few and far between
+
- fifteen minutes of fame
+
- fills the bill
+
- filthy vermin
+
- fine kettle of fish
+
- first and foremost
+
- fish out of water
+
- fishing for a compliment
+
- fit as a fiddle
+
- fit the bill
+
- fit to be tied
+
- flash in the pan
+
- flat as a pancake
+
- flip your lid
+
- flog a dead horse
+
- fly by night
+
- fly the coop
+
- follow your heart
+
- for all intents and purposes
+
- for free
+
- for the birds
+
- for what it's worth
+
- force of nature
+
- force to be reckoned with
+
- forgive and forget
+
- fox in the henhouse
+
- free and easy
+
- free as a bird
+
- fresh as a daisy
+
- full steam ahead
+
- fun in the sun
+
- garbage in, garbage out
+
- gentle as a lamb
+
- get a kick out of
+
- get a leg up
+
- get down and dirty
+
- get the lead out
+
- get to the bottom of
+
- get with the program
+
- get your feet wet
+
- gets my goat
+
- gilding the lily
+
- gilding the lily
+
- give and take
+
- go against the grain
+
- go at it tooth and nail
+
- go for broke
+
- go him one better
+
- go the extra mile
+
- go with the flow
+
- goes without saying
+
- good as gold
+
- good deed for the day
+
- good things come to those who wait
+
- good time was had by all
+
- good times were had by all
+
- greased lightning
+
- greek to me
+
- green thumb
+
- green-eyed monster
+
- grist for the mill
+
- growing like a weed
+
- hair of the dog
+
- hand to mouth
+
- happy as a clam
+
- happy as a lark
+
- hasn't a clue
+
- have a nice day
+
- have a short fuse
+
- have high hopes
+
- have the last laugh
+
- haven't got a row to hoe
+
- he's got his hands full
+
- head honcho
+
- head over heels
+
- hear a pin drop
+
- heard it through the grapevine
+
- heart's content
+
- heavy as lead
+
- hem and haw
+
- high and dry
+
- high and mighty
+
- high as a kite
+
- his own worst enemy
+
- his work cut out for him
+
- hit paydirt
+
- hither and yon
+
- Hobson's choice
+
- hold your head up high
+
- hold your horses
+
- hold your own
+
- hold your tongue
+
- honest as the day is long
+
- horns of a dilemma
+
- horns of a dilemma
+
- horse of a different color
+
- hot under the collar
+
- hour of need
+
- I beg to differ
+
- icing on the cake
+
- if the shoe fits
+
- if the shoe were on the other foot
+
- if you catch my drift
+
- in a jam
+
- in a jiffy
+
- in a nutshell
+
- in a pig's eye
+
- in a pinch
+
- in a word
+
- in hot water
+
- in light of
+
- in the final analysis
+
- in the gutter
+
- in the last analysis
+
- in the nick of time
+
- in the thick of it
+
- in your dreams
+
- innocent bystander
+
- it ain't over till the fat lady sings
+
- it goes without saying
+
- it takes all kinds
+
- it takes one to know one
+
- it's a small world
+
- it's not what you know, it's who you know
+
- it's only a matter of time
+
- ivory tower
+
- Jack of all trades
+
- jockey for position
+
- jog your memory
+
- joined at the hip
+
- judge a book by its cover
+
- jump down your throat
+
- jump in with both feet
+
- jump on the bandwagon
+
- jump the gun
+
- jump to conclusions
+
- just a hop, skip, and a jump
+
- just the ticket
+
- justice is blind
+
- keep a stiff upper lip
+
- keep an eye on
+
- keep it simple, stupid
+
- keep the home fires burning
+
- keep up with the Joneses
+
- keep your chin up
+
- keep your fingers crossed
+
- kick the bucket
+
- kick up your heels
+
- kick your feet up
+
- kid in a candy store
+
- kill two birds with one stone
+
- kiss of death
+
- knock it out of the park
+
- knock on wood
+
- knock your socks off
+
- know him from Adam
+
- know the ropes
+
- know the score
+
- knuckle down
+
- knuckle sandwich
+
- knuckle under
+
- labor of love
+
- ladder of success
+
- land on your feet
+
- lap of luxury
+
- last but not least
+
- last but not least
+
- last hurrah
+
- last-ditch effort
+
- law of the jungle
+
- law of the land
+
- lay down the law
+
- leaps and bounds
+
- let sleeping dogs lie
+
- let the cat out of the bag
+
- let the good times roll
+
- let your hair down
+
- let's talk turkey
+
- letter perfect
+
- lick your wounds
+
- lies like a rug
+
- life's a bitch
+
- life's a grind
+
- light at the end of the tunnel
+
- lighter than a feather
+
- lighter than air
+
- like clockwork
+
- like father like son
+
- like taking candy from a baby
+
- like there's no tomorrow
+
- lion's share
+
- live and learn
+
- live and let live
+
- long and short of it
+
- long lost love
+
- look before you leap
+
- look down your nose
+
- look what the cat dragged in
+
- looking a gift horse in the mouth
+
- looks like death warmed over
+
- loose cannon
+
- lose your head
+
- lose your temper
+
- loud as a horn
+
- lounge lizard
+
- loved and lost
+
- low man on the totem pole
+
- luck of the draw
+
- luck of the Irish
+
- make a mockery of
+
- make hay while the sun shines
+
- make money hand over fist
+
- make my day
+
- make the best of a bad situation
+
- make the best of it
+
- make your blood boil
+
- male chauvinism
+
- man of few words
+
- man's best friend
+
- mark my words
+
- meaningful dialogue
+
- missed the boat on that one
+
- moment in the sun
+
- moment of glory
+
- moment of truth
+
- moment of truth
+
- money to burn
+
- more in sorrow than in anger
+
- more power to you
+
- more sinned against than sinning
+
- more than one way to skin a cat
+
- movers and shakers
+
- moving experience
+
- my better half
+
- naked as a jaybird
+
- naked truth
+
- neat as a pin
+
- needle in a haystack
+
- needless to say
+
- neither here nor there
+
- never look back
+
- never say never
+
- nip and tuck
+
- nip in the bud
+
- nip it in the bud
+
- no guts, no glory
+
- no love lost
+
- no pain, no gain
+
- no skin off my back
+
- no stone unturned
+
- no time like the present
+
- no use crying over spilled milk
+
- nose to the grindstone
+
- not a hope in hell
+
- not a minute's peace
+
- not in my backyard
+
- not playing with a full deck
+
- not the end of the world
+
- not written in stone
+
- nothing to sneeze at
+
- nothing ventured nothing gained
+
- now we're cooking
+
- off the top of my head
+
- off the wagon
+
- off the wall
+
- old hat
+
- olden days
+
- older and wiser
+
- older than dirt
+
- older than Methuselah
+
- on a roll
+
- on cloud nine
+
- on pins and needles
+
- on the bandwagon
+
- on the money
+
- on the nose
+
- on the rocks
+
- on the same page
+
- on the spot
+
- on the tip of my tongue
+
- on the wagon
+
- on thin ice
+
- once bitten, twice shy
+
- one bad apple doesn't spoil the bushel
+
- one born every minute
+
- one brick short
+
- one foot in the grave
+
- one in a million
+
- one red cent
+
- only game in town
+
- open a can of worms
+
- open and shut case
+
- open the flood gates
+
- opportunity doesn't knock twice
+
- out of pocket
+
- out of sight, out of mind
+
- out of the frying pan into the fire
+
- out of the woods
+
- out on a limb
+
- over a barrel
+
- over the hump
+
- pain and suffering
+
- pain in the
+
- panic button
+
- par for the course
+
- part and parcel
+
- party pooper
+
- pass the buck
+
- patience is a virtue
+
- pay through the nose
+
- penny pincher
+
- perfect storm
+
- pig in a poke
+
- pile it on
+
- pillar of the community
+
- pin your hopes on
+
- pitter patter of little feet
+
- plain as day
+
- plain as the nose on your face
+
- play by the rules
+
- play your cards right
+
- playing the field
+
- playing with fire
+
- pleased as punch
+
- plenty of fish in the sea
+
- point with pride
+
- poor as a church mouse
+
- pot calling the kettle black
+
- presidential timber
+
- pretty as a picture
+
- pull a fast one
+
- pull your punches
+
- pulled no punches
+
- pulling your leg
+
- pure as the driven snow
+
- put it in a nutshell
+
- put one over on you
+
- put the cart before the horse
+
- put the pedal to the metal
+
- put your best foot forward
+
- put your foot down
+
- quantum jump
+
- quantum leap
+
- quick as a bunny
+
- quick as a lick
+
- quick as a wink
+
- quick as lightning
+
- quiet as a dormouse
+
- rags to riches
+
- raining buckets
+
- raining cats and dogs
+
- rank and file
+
- rat race
+
- reap what you sow
+
- red as a beet
+
- red herring
+
- redound to one's credit
+
- redound to the benefit of
+
- reinvent the wheel
+
- rich and famous
+
- rings a bell
+
- ripe old age
+
- ripped me off
+
- rise and shine
+
- road to hell is paved with good intentions
+
- rob Peter to pay Paul
+
- roll over in the grave
+
- rub the wrong way
+
- ruled the roost
+
- running in circles
+
- sad but true
+
- sadder but wiser
+
- salt of the earth
+
- scared stiff
+
- scared to death
+
- sea change
+
- sealed with a kiss
+
- second to none
+
- see eye to eye
+
- seen the light
+
- seize the day
+
- set the record straight
+
- set the world on fire
+
- set your teeth on edge
+
- sharp as a tack
+
- shirked his duties
+
- shoot for the moon
+
- shoot the breeze
+
- shot in the dark
+
- shoulder to the wheel
+
- sick as a dog
+
- sigh of relief
+
- signed, sealed, and delivered
+
- sink or swim
+
- six of one, half a dozen of another
+
- six of one, half a dozen of the other
+
- skating on thin ice
+
- slept like a log
+
- slinging mud
+
- slippery as an eel
+
- slow as molasses
+
- smart as a whip
+
- smooth as a baby's bottom
+
- sneaking suspicion
+
- snug as a bug in a rug
+
- sow wild oats
+
- spare the rod, spoil the child
+
- speak of the devil
+
- spilled the beans
+
- spinning your wheels
+
- spitting image of
+
- spoke with relish
+
- spread like wildfire
+
- spring to life
+
- squeaky wheel gets the grease
+
- stands out like a sore thumb
+
- start from scratch
+
- stick in the mud
+
- still waters run deep
+
- stitch in time
+
- stop and smell the roses
+
- straight as an arrow
+
- straw that broke the camel's back
+
- stretched to the breaking point
+
- strong as an ox
+
- stubborn as a mule
+
- stuff that dreams are made of
+
- stuffed shirt
+
- sweating blood
+
- sweating bullets
+
- take a load off
+
- take one for the team
+
- take the bait
+
- take the bull by the horns
+
- take the plunge
+
- takes one to know one
+
- takes two to tango
+
- than you can shake a stick at
+
- the cream of the crop
+
- the cream rises to the top
+
- the more the merrier
+
- the real deal
+
- the real McCoy
+
- the red carpet treatment
+
- the same old story
+
- the straw that broke the camel's back
+
- there is no accounting for taste
+
- thick as a brick
+
- thick as thieves
+
- thick as thieves
+
- thin as a rail
+
- think outside of the box
+
- thinking outside the box
+
- third time's the charm
+
- this day and age
+
- this hurts me worse than it hurts you
+
- this point in time
+
- thought leaders?
+
- three sheets to the wind
+
- through thick and thin
+
- throw in the towel
+
- throw the baby out with the bathwater
+
- tie one on
+
- tighter than a drum
+
- time and time again
+
- time is of the essence
+
- tip of the iceberg
+
- tired but happy
+
- to coin a phrase
+
- to each his own
+
- to make a long story short
+
- to the best of my knowledge
+
- toe the line
+
- tongue in cheek
+
- too good to be true
+
- too hot to handle
+
- too numerous to mention
+
- touch with a ten foot pole
+
- tough as nails
+
- trial and error
+
- trials and tribulations
+
- tried and true
+
- trip down memory lane
+
- twist of fate
+
- two cents worth
+
- two peas in a pod
+
- ugly as sin
+
- under the counter
+
- under the gun
+
- under the same roof
+
- under the weather
+
- until the cows come home
+
- unvarnished truth
+
- up the creek
+
- uphill battle
+
- upper crust
+
- upset the applecart
+
- vain attempt
+
- vain effort
+
- vanquish the enemy
+
- various and sundry
+
- vested interest
+
- viable alternative
+
- waiting for the other shoe to drop
+
- wakeup call
+
- warm welcome
+
- watch your p's and q's
+
- watch your tongue
+
- watching the clock
+
- water under the bridge
+
- wax eloquent
+
- wax poetic
+
- we've got a situation here
+
- weather the storm
+
- weed them out
+
- week of Sundays
+
- went belly up
+
- wet behind the ears
+
- what goes around comes around
+
- what you see is what you get
+
- when it rains, it pours
+
- when push comes to shove
+
- when the cat's away
+
- when the going gets tough, the tough get going
+
- whet (?:the|your) appetite
+
- white as a sheet
+
- whole ball of wax
+
- whole hog
+
- whole nine yards
+
- wild goose chase
+
- will wonders never cease?
+
- wisdom of the ages
+
- wise as an owl
+
- wolf at the door
+
- wool pulled over our eyes
+
- words fail me
+
- work like a dog
+
- world weary
+
- worst nightmare
+
- worth its weight in gold
+
- writ large
+
- wrong side of the bed
+
- yanking your chain
+
- yappy as a dog
+
- years young
+
- you are what you eat
+
- you can run but you can't hide
+
- you only live once
+
- you're the boss
+
- young and foolish
+
- young and vibrant
+30
styles/proselint/CorporateSpeak.yml
···
···
+
extends: existence
+
message: "'%s' is corporate speak."
+
ignorecase: true
+
level: error
+
tokens:
+
- at the end of the day
+
- back to the drawing board
+
- hit the ground running
+
- get the ball rolling
+
- low-hanging fruit
+
- thrown under the bus
+
- think outside the box
+
- let's touch base
+
- get my manager's blessing
+
- it's on my radar
+
- ping me
+
- i don't have the bandwidth
+
- no brainer
+
- par for the course
+
- bang for your buck
+
- synergy
+
- move the goal post
+
- apples to apples
+
- win-win
+
- circle back around
+
- all hands on deck
+
- take this offline
+
- drill-down
+
- elephant in the room
+
- on my plate
+5
styles/proselint/Currency.yml
···
···
+
extends: existence
+
message: "Incorrect use of symbols in '%s'."
+
ignorecase: true
+
raw:
+
- \$[\d]* ?(?:dollars|usd|us dollars)
+15
styles/proselint/Cursing.yml
···
···
+
extends: existence
+
message: "Consider replacing '%s'."
+
level: error
+
ignorecase: true
+
tokens:
+
- shit
+
- piss
+
- fuck
+
- cunt
+
- cocksucker
+
- motherfucker
+
- tits
+
- fart
+
- turd
+
- twat
+7
styles/proselint/DateCase.yml
···
···
+
extends: existence
+
message: With lowercase letters, the periods are standard.
+
ignorecase: true
+
level: error
+
nonword: true
+
tokens:
+
- '\d{1,2} ?[ap]m\b'
+7
styles/proselint/DateMidnight.yml
···
···
+
extends: existence
+
message: "Use 'midnight' or 'noon'."
+
ignorecase: true
+
level: error
+
nonword: true
+
tokens:
+
- '12 ?[ap]\.?m\.?'
+10
styles/proselint/DateRedundancy.yml
···
···
+
extends: existence
+
message: "'a.m.' is always morning; 'p.m.' is always night."
+
ignorecase: true
+
level: error
+
nonword: true
+
tokens:
+
- '\d{1,2} ?a\.?m\.? in the morning'
+
- '\d{1,2} ?p\.?m\.? in the evening'
+
- '\d{1,2} ?p\.?m\.? at night'
+
- '\d{1,2} ?p\.?m\.? in the afternoon'
+7
styles/proselint/DateSpacing.yml
···
···
+
extends: existence
+
message: "It's standard to put a space before '%s'"
+
ignorecase: true
+
level: error
+
nonword: true
+
tokens:
+
- '\d{1,2}[ap]\.?m\.?'
+52
styles/proselint/DenizenLabels.yml
···
···
+
extends: substitution
+
message: Did you mean '%s'?
+
ignorecase: false
+
action:
+
name: replace
+
swap:
+
(?:Afrikaaner|Afrikander): Afrikaner
+
(?:Hong Kongite|Hong Kongian): Hong Konger
+
(?:Indianan|Indianian): Hoosier
+
(?:Michiganite|Michiganian): Michigander
+
(?:New Hampshireite|New Hampshireman): New Hampshirite
+
(?:Newcastlite|Newcastleite): Novocastrian
+
(?:Providencian|Providencer): Providentian
+
(?:Trentian|Trentonian): Tridentine
+
(?:Warsawer|Warsawian): Varsovian
+
(?:Wolverhamptonite|Wolverhamptonian): Wulfrunian
+
Alabaman: Alabamian
+
Albuquerquian: Albuquerquean
+
Anchoragite: Anchorageite
+
Arizonian: Arizonan
+
Arkansawyer: Arkansan
+
Belarusan: Belarusian
+
Cayman Islander: Caymanian
+
Coloradoan: Coloradan
+
Connecticuter: Nutmegger
+
Fairbanksian: Fairbanksan
+
Fort Worther: Fort Worthian
+
Grenadian: Grenadan
+
Halifaxer: Haligonian
+
Hartlepoolian: Hartlepudlian
+
Illinoisian: Illinoisan
+
Iowegian: Iowan
+
Leedsian: Leodenisian
+
Liverpoolian: Liverpudlian
+
Los Angelean: Angeleno
+
Manchesterian: Mancunian
+
Minneapolisian: Minneapolitan
+
Missouran: Missourian
+
Monacan: Monegasque
+
Neopolitan: Neapolitan
+
New Jerseyite: New Jerseyan
+
New Orleansian: New Orleanian
+
Oklahoma Citian: Oklahoma Cityan
+
Oklahomian: Oklahoman
+
Saudi Arabian: Saudi
+
Seattlite: Seattleite
+
Surinamer: Surinamese
+
Tallahassean: Tallahasseean
+
Tennesseean: Tennessean
+
Trois-Rivièrester: Trifluvian
+
Utahan: Utahn
+
Valladolidian: Vallisoletano
+95
styles/proselint/Diacritical.yml
···
···
+
extends: substitution
+
message: Consider using '%s' instead of '%s'.
+
ignorecase: true
+
level: error
+
action:
+
name: replace
+
swap:
+
beau ideal: beau idéal
+
boutonniere: boutonnière
+
bric-a-brac: bric-à-brac
+
cafe: café
+
cause celebre: cause célèbre
+
chevre: chèvre
+
cliche: cliché
+
consomme: consommé
+
coup de grace: coup de grâce
+
crudites: crudités
+
creme brulee: crème brûlée
+
creme de menthe: crème de menthe
+
creme fraice: crème fraîche
+
creme fresh: crème fraîche
+
crepe: crêpe
+
debutante: débutante
+
decor: décor
+
deja vu: déjà vu
+
denouement: dénouement
+
facade: façade
+
fiance: fiancé
+
fiancee: fiancée
+
flambe: flambé
+
garcon: garçon
+
lycee: lycée
+
maitre d: maître d
+
menage a trois: ménage à trois
+
negligee: négligée
+
protege: protégé
+
protegee: protégée
+
puree: purée
+
my resume: my résumé
+
your resume: your résumé
+
his resume: his résumé
+
her resume: her résumé
+
a resume: a résumé
+
the resume: the résumé
+
risque: risqué
+
roue: roué
+
soiree: soirée
+
souffle: soufflé
+
soupcon: soupçon
+
touche: touché
+
tete-a-tete: tête-à-tête
+
voila: voilà
+
a la carte: à la carte
+
a la mode: à la mode
+
emigre: émigré
+
+
# Spanish loanwords
+
El Nino: El Niño
+
jalapeno: jalapeño
+
La Nina: La Niña
+
pina colada: piña colada
+
senor: señor
+
senora: señora
+
senorita: señorita
+
+
# Portuguese loanwords
+
acai: açaí
+
+
# German loanwords
+
doppelganger: doppelgänger
+
Fuhrer: Führer
+
Gewurztraminer: Gewürztraminer
+
vis-a-vis: vis-à-vis
+
Ubermensch: Übermensch
+
+
# Swedish loanwords
+
filmjolk: filmjölk
+
smorgasbord: smörgåsbord
+
+
# Names, places, and companies
+
Beyonce: Beyoncé
+
Bronte: Brontë
+
Champs-Elysees: Champs-Élysées
+
Citroen: Citroën
+
Curacao: Curaçao
+
Lowenbrau: Löwenbräu
+
Monegasque: Monégasque
+
Motley Crue: Mötley Crüe
+
Nescafe: Nescafé
+
Queensryche: Queensrÿche
+
Quebec: Québec
+
Quebecois: Québécois
+
Angstrom: Ångström
+
angstrom: ångström
+
Skoda: Škoda
+45
styles/proselint/GenderBias.yml
···
···
+
extends: substitution
+
message: Consider using '%s' instead of '%s'.
+
ignorecase: true
+
level: error
+
action:
+
name: replace
+
swap:
+
(?:alumnae|alumni): graduates
+
(?:alumna|alumnus): graduate
+
air(?:m[ae]n|wom[ae]n): pilot(s)
+
anchor(?:m[ae]n|wom[ae]n): anchor(s)
+
authoress: author
+
camera(?:m[ae]n|wom[ae]n): camera operator(s)
+
chair(?:m[ae]n|wom[ae]n): chair(s)
+
congress(?:m[ae]n|wom[ae]n): member(s) of congress
+
door(?:m[ae]|wom[ae]n): concierge(s)
+
draft(?:m[ae]n|wom[ae]n): drafter(s)
+
fire(?:m[ae]n|wom[ae]n): firefighter(s)
+
fisher(?:m[ae]n|wom[ae]n): fisher(s)
+
fresh(?:m[ae]n|wom[ae]n): first-year student(s)
+
garbage(?:m[ae]n|wom[ae]n): waste collector(s)
+
lady lawyer: lawyer
+
ladylike: courteous
+
landlord: building manager
+
mail(?:m[ae]n|wom[ae]n): mail carriers
+
man and wife: husband and wife
+
man enough: strong enough
+
mankind: human kind
+
manmade: manufactured
+
men and girls: men and women
+
middle(?:m[ae]n|wom[ae]n): intermediary
+
news(?:m[ae]n|wom[ae]n): journalist(s)
+
ombuds(?:man|woman): ombuds
+
oneupmanship: upstaging
+
poetess: poet
+
police(?:m[ae]n|wom[ae]n): police officer(s)
+
repair(?:m[ae]n|wom[ae]n): technician(s)
+
sales(?:m[ae]n|wom[ae]n): salesperson or sales people
+
service(?:m[ae]n|wom[ae]n): soldier(s)
+
steward(?:ess)?: flight attendant
+
tribes(?:m[ae]n|wom[ae]n): tribe member(s)
+
waitress: waiter
+
woman doctor: doctor
+
woman scientist[s]?: scientist(s)
+
work(?:m[ae]n|wom[ae]n): worker(s)
+39
styles/proselint/GroupTerms.yml
···
···
+
extends: substitution
+
message: Consider using '%s' instead of '%s'.
+
ignorecase: true
+
action:
+
name: replace
+
swap:
+
(?:bunch|group|pack|flock) of chickens: brood of chickens
+
(?:bunch|group|pack|flock) of crows: murder of crows
+
(?:bunch|group|pack|flock) of hawks: cast of hawks
+
(?:bunch|group|pack|flock) of parrots: pandemonium of parrots
+
(?:bunch|group|pack|flock) of peacocks: muster of peacocks
+
(?:bunch|group|pack|flock) of penguins: muster of penguins
+
(?:bunch|group|pack|flock) of sparrows: host of sparrows
+
(?:bunch|group|pack|flock) of turkeys: rafter of turkeys
+
(?:bunch|group|pack|flock) of woodpeckers: descent of woodpeckers
+
(?:bunch|group|pack|herd) of apes: shrewdness of apes
+
(?:bunch|group|pack|herd) of baboons: troop of baboons
+
(?:bunch|group|pack|herd) of badgers: cete of badgers
+
(?:bunch|group|pack|herd) of bears: sloth of bears
+
(?:bunch|group|pack|herd) of bullfinches: bellowing of bullfinches
+
(?:bunch|group|pack|herd) of bullocks: drove of bullocks
+
(?:bunch|group|pack|herd) of caterpillars: army of caterpillars
+
(?:bunch|group|pack|herd) of cats: clowder of cats
+
(?:bunch|group|pack|herd) of colts: rag of colts
+
(?:bunch|group|pack|herd) of crocodiles: bask of crocodiles
+
(?:bunch|group|pack|herd) of dolphins: school of dolphins
+
(?:bunch|group|pack|herd) of foxes: skulk of foxes
+
(?:bunch|group|pack|herd) of gorillas: band of gorillas
+
(?:bunch|group|pack|herd) of hippopotami: bloat of hippopotami
+
(?:bunch|group|pack|herd) of horses: drove of horses
+
(?:bunch|group|pack|herd) of jellyfish: fluther of jellyfish
+
(?:bunch|group|pack|herd) of kangeroos: mob of kangeroos
+
(?:bunch|group|pack|herd) of monkeys: troop of monkeys
+
(?:bunch|group|pack|herd) of oxen: yoke of oxen
+
(?:bunch|group|pack|herd) of rhinoceros: crash of rhinoceros
+
(?:bunch|group|pack|herd) of wild boar: sounder of wild boar
+
(?:bunch|group|pack|herd) of wild pigs: drift of wild pigs
+
(?:bunch|group|pack|herd) of zebras: zeal of wild pigs
+
(?:bunch|group|pack|school) of trout: hover of trout
+8
styles/proselint/Hedging.yml
···
···
+
extends: existence
+
message: "'%s' is hedging."
+
ignorecase: true
+
level: error
+
tokens:
+
- I would argue that
+
- ', so to speak'
+
- to a certain degree
+6
styles/proselint/Hyperbole.yml
···
···
+
extends: existence
+
message: "'%s' is hyperbolic."
+
level: error
+
nonword: true
+
tokens:
+
- '[a-z]+[!?]{2,}'
+11
styles/proselint/Jargon.yml
···
···
+
extends: existence
+
message: "'%s' is jargon."
+
ignorecase: true
+
level: error
+
tokens:
+
- in the affirmative
+
- in the negative
+
- agendize
+
- per your order
+
- per your request
+
- disincentivize
+13
styles/proselint/LGBTOffensive.yml
···
···
+
extends: existence
+
message: "'%s' is offensive. Remove it or consider the context."
+
ignorecase: true
+
tokens:
+
- fag
+
- faggot
+
- dyke
+
- sodomite
+
- homosexual agenda
+
- gay agenda
+
- transvestite
+
- homosexual lifestyle
+
- gay lifestyle
+15
styles/proselint/LGBTTerms.yml
···
···
+
extends: substitution
+
message: "Consider using '%s' instead of '%s'."
+
ignorecase: true
+
action:
+
name: replace
+
swap:
+
homosexual man: gay man
+
homosexual men: gay men
+
homosexual woman: lesbian
+
homosexual women: lesbians
+
homosexual people: gay people
+
homosexual couple: gay couple
+
sexual preference: sexual orientation
+
(?:admitted homosexual|avowed homosexual): openly gay
+
special rights: equal rights
+8
styles/proselint/Malapropisms.yml
···
···
+
extends: existence
+
message: "'%s' is a malapropism."
+
ignorecase: true
+
level: error
+
tokens:
+
- the infinitesimal universe
+
- a serial experience
+
- attack my voracity
+358
styles/proselint/Needless.yml
···
···
+
extends: substitution
+
message: Prefer '%s' over '%s'
+
ignorecase: true
+
action:
+
name: replace
+
swap:
+
'(?:cell phone|cell-phone)': cellphone
+
'(?:cliquey|cliquy)': cliquish
+
'(?:pygmean|pygmaen)': pygmy
+
'(?:retributional|retributionary)': retributive
+
'(?:revokable|revokeable)': revocable
+
abolishment: abolition
+
accessary: accessory
+
accreditate: accredit
+
accruement: accrual
+
accusee: accused
+
acquaintanceship: acquaintance
+
acquitment: acquittal
+
administrate: administer
+
administrated: administered
+
administrating: administering
+
adulterate: adulterous
+
advisatory: advisory
+
advocator: advocate
+
aggrievance: grievance
+
allegator: alleger
+
allusory: allusive
+
amative: amorous
+
amortizement: amortization
+
amphiboly: amphibology
+
anecdotalist: anecdotist
+
anilinctus: anilingus
+
anticipative: anticipatory
+
antithetic: antithetical
+
applicative: applicable
+
applicatory: applicable
+
applier: applicator
+
approbative: approbatory
+
arbitrager: arbitrageur
+
arsenous: arsenious
+
ascendance: ascendancy
+
ascendence: ascendancy
+
ascendency: ascendancy
+
auctorial: authorial
+
averral: averment
+
barbwire: barbed wire
+
benefic: beneficent
+
benignant: benign
+
bestowment: bestowal
+
betrothment: betrothal
+
blamableness: blameworthiness
+
butt naked: buck naked
+
camarade: comrade
+
carta blanca: carte blanche
+
casualities: casualties
+
casuality: casualty
+
catch on fire: catch fire
+
catholicly: catholically
+
cease fire: ceasefire
+
channelize: channel
+
chaplainship: chaplaincy
+
chrysalid: chrysalis
+
chrysalids: chrysalises
+
cigaret: cigarette
+
coemployee: coworker
+
cognitional: cognitive
+
cohabitate: cohabit
+
cohabitor: cohabitant
+
collodium: collodion
+
collusory: collusive
+
commemoratory: commemorative
+
commonty: commonage
+
communicatory: communicative
+
compensative: compensatory
+
complacence: complacency
+
complicitous: complicit
+
computate: compute
+
conciliative: conciliatory
+
concomitancy: concomitance
+
condonance: condonation
+
confirmative: confirmatory
+
congruency: congruence
+
connotate: connote
+
consanguineal: consanguine
+
conspicuity: conspicuousness
+
conspiratorialist: conspirator
+
constitutionist: constitutionalist
+
contingence: contigency
+
contributary: contributory
+
contumacity: contumacy
+
conversible: convertible
+
conveyal: conveyance
+
copartner: partner
+
copartnership: partnership
+
corroboratory: corroborative
+
cotemporaneous: contemporaneous
+
cotemporary: contemporary
+
criminate: incriminate
+
culpatory: inculpatory
+
cumbrance: encumbrance
+
cumulate: accumulate
+
curatory: curative
+
daredeviltry: daredevilry
+
deceptious: deceptive
+
defamative: defamatory
+
defraudulent: fraudulent
+
degeneratory: degenerative
+
delimitate: delimit
+
delusory: delusive
+
denouncement: denunciation
+
depositee: depositary
+
depreciative: depreciatory
+
deprival: deprivation
+
derogative: derogatory
+
destroyable: destructible
+
detoxicate: detoxify
+
detractory: detractive
+
deviancy: deviance
+
deviationist: deviant
+
digamy: deuterogamy
+
digitalize: digitize
+
diminishment: diminution
+
diplomatist: diplomat
+
disassociate: dissociate
+
disciplinatory: disciplinary
+
discriminant: discriminating
+
disenthrone: dethrone
+
disintegratory: disintegrative
+
dismission: dismissal
+
disorientate: disorient
+
disorientated: disoriented
+
disquieten: disquiet
+
distraite: distrait
+
divergency: divergence
+
dividable: divisible
+
doctrinary: doctrinaire
+
documental: documentary
+
domesticize: domesticate
+
duplicatory: duplicative
+
duteous: dutiful
+
educationalist: educationist
+
educatory: educative
+
enigmatas: enigmas
+
enlargen: enlarge
+
enswathe: swathe
+
epical: epic
+
erotism: eroticism
+
ethician: ethicist
+
ex officiis: ex officio
+
exculpative: exculpatory
+
exigeant: exigent
+
exigence: exigency
+
exotism: exoticism
+
expedience: expediency
+
expediential: expedient
+
extensible: extendable
+
eying: eyeing
+
fiefdom: fief
+
flagrance: flagrancy
+
flatulency: flatulence
+
fraudful: fraudulent
+
funebrial: funereal
+
geographical: geographic
+
geometrical: geometric
+
gerry-rigged: jury-rigged
+
goatherder: goatherd
+
gustatorial: gustatory
+
habitude: habit
+
henceforward: henceforth
+
hesitance: hesitancy
+
heterogenous: heterogeneous
+
hierarchic: hierarchical
+
hindermost: hindmost
+
honorand: honoree
+
hypostasize: hypostatize
+
hysteric: hysterical
+
idolatrize: idolize
+
impanel: empanel
+
imperviable: impervious
+
importunacy: importunity
+
impotency: impotence
+
imprimatura: imprimatur
+
improprietous: improper
+
inalterable: unalterable
+
incitation: incitement
+
incommunicative: uncommunicative
+
inconsistence: inconsistency
+
incontrollable: uncontrollable
+
incurment: incurrence
+
indow: endow
+
indue: endue
+
inhibitive: inhibitory
+
innavigable: unnavigable
+
innovational: innovative
+
inquisitional: inquisitorial
+
insistment: insistence
+
insolvable: unsolvable
+
instillment: instillation
+
instinctual: instinctive
+
insuror: insurer
+
insurrectional: insurrectionary
+
interpretate: interpret
+
intervenience: intervention
+
ironical: ironic
+
jerry-rigged: jury-rigged
+
judgmatic: judgmental
+
labyrinthian: labyrinthine
+
laudative: laudatory
+
legitimatization: legitimation
+
legitimatize: legitimize
+
legitimization: legitimation
+
lengthways: lengthwise
+
life-sized: life-size
+
liquorice: licorice
+
lithesome: lithe
+
lollipop: lollypop
+
loth: loath
+
lubricous: lubricious
+
maihem: mayhem
+
medicinal marijuana: medical marijuana
+
meliorate: ameliorate
+
minimalize: minimize
+
mirk: murk
+
mirky: murky
+
misdoubt: doubt
+
monetarize: monetize
+
moveable: movable
+
narcism: narcissism
+
neglective: neglectful
+
negligency: negligence
+
neologizer: neologist
+
neurologic: neurological
+
nicknack: knickknack
+
nictate: nictitate
+
nonenforceable: unenforceable
+
normalcy: normality
+
numbedness: numbness
+
omittable: omissible
+
onomatopoetic: onomatopoeic
+
opinioned: opined
+
optimum advantage: optimal advantage
+
orientate: orient
+
outsized: outsize
+
oversized: oversize
+
overthrowal: overthrow
+
pacificist: pacifist
+
paederast: pederast
+
parachronism: anachronism
+
parti-color: parti-colored
+
participative: participatory
+
party-colored: parti-colored
+
pediatrist: pediatrician
+
penumbrous: penumbral
+
perjorative: pejorative
+
permissory: permissive
+
permutate: permute
+
personation: impersonation
+
pharmaceutic: pharmaceutical
+
pleuritis: pleurisy
+
policy holder: policyholder
+
policyowner: policyholder
+
politicalize: politicize
+
precedency: precedence
+
preceptoral: preceptorial
+
precipitance: precipitancy
+
precipitant: precipitate
+
preclusory: preclusive
+
precolumbian: pre-Columbian
+
prefectoral: prefectorial
+
preponderately: preponderantly
+
preserval: preservation
+
preventative: preventive
+
proconsulship: proconsulate
+
procreational: procreative
+
procurance: procurement
+
propelment: propulsion
+
propulsory: propulsive
+
prosecutive: prosecutory
+
protectory: protective
+
provocatory: provocative
+
pruriency: prurience
+
psychal: psychical
+
punitory: punitive
+
quantitate: quantify
+
questionary: questionnaire
+
quiescency: quiescence
+
rabbin: rabbi
+
reasonability: reasonableness
+
recidivistic: recidivous
+
recriminative: recriminatory
+
recruital: recruitment
+
recurrency: recurrence
+
recusance: recusancy
+
recusation: recusal
+
recusement: recusal
+
redemptory: redemptive
+
referrable: referable
+
referrible: referable
+
refutatory: refutative
+
remitment: remittance
+
remittal: remission
+
renouncement: renunciation
+
renunciable: renounceable
+
reparatory: reparative
+
repudiative: repudiatory
+
requitement: requital
+
rescindment: rescission
+
restoral: restoration
+
reticency: reticence
+
reviewal: review
+
revisal: revision
+
revisional: revisionary
+
revolute: revolt
+
saliency: salience
+
salutiferous: salutary
+
sensatory: sensory
+
sessionary: sessional
+
shareowner: shareholder
+
sicklily: sickly
+
signator: signatory
+
slanderize: slander
+
societary: societal
+
sodomist: sodomite
+
solicitate: solicit
+
speculatory: speculative
+
spiritous: spirituous
+
statutorial: statutory
+
submergeable: submersible
+
submittal: submission
+
subtile: subtle
+
succuba: succubus
+
sufficience: sufficiency
+
suppliant: supplicant
+
surmisal: surmise
+
suspendible: suspendable
+
synthetize: synthesize
+
systemize: systematize
+
tactual: tactile
+
tangental: tangential
+
tautologous: tautological
+
tee-shirt: T-shirt
+
thenceforward: thenceforth
+
transiency: transience
+
transposal: transposition
+
unfrequent: infrequent
+
unreasonability: unreasonableness
+
unrevokable: irrevocable
+
unsubstantial: insubstantial
+
usurpature: usurpation
+
variative: variational
+
vegetive: vegetative
+
vindicative: vindictive
+
vituperous: vituperative
+
vociferant: vociferous
+
volitive: volitional
+
wolverene: wolverine
+
wolvish: wolfish
+
Zoroastrism: Zoroastrianism
+38
styles/proselint/Nonwords.yml
···
···
+
extends: substitution
+
message: "Consider using '%s' instead of '%s'."
+
ignorecase: true
+
level: error
+
action:
+
name: replace
+
swap:
+
affrontery: effrontery
+
analyzation: analysis
+
annoyment: annoyance
+
confirmant: confirmand
+
confirmants: confirmands
+
conversate: converse
+
crained: cranded
+
discomforture: discomfort|discomfiture
+
dispersement: disbursement|dispersal
+
doubtlessly: doubtless|undoubtedly
+
forebearance: forbearance
+
improprietous: improper
+
inclimate: inclement
+
inimicable: inimical
+
irregardless: regardless
+
minimalize: minimize
+
minimalized: minimized
+
minimalizes: minimizes
+
minimalizing: minimizing
+
optimalize: optimize
+
paralyzation: paralysis
+
pettifogger: pettifog
+
proprietous: proper
+
relative inexpense: relatively low price|affordability
+
seldomly: seldom
+
thusly: thus
+
uncategorically: categorically
+
undoubtably: undoubtedly|indubitably
+
unequivocable: unequivocal
+
unmercilessly: mercilessly
+
unrelentlessly: unrelentingly|relentlessly
+22
styles/proselint/Oxymorons.yml
···
···
+
extends: existence
+
message: "'%s' is an oxymoron."
+
ignorecase: true
+
level: error
+
tokens:
+
- amateur expert
+
- increasingly less
+
- advancing backwards
+
- alludes explicitly to
+
- explicitly alludes to
+
- totally obsolescent
+
- completely obsolescent
+
- generally always
+
- usually always
+
- increasingly less
+
- build down
+
- conspicuous absence
+
- exact estimate
+
- found missing
+
- intense apathy
+
- mandatory choice
+
- organized mess
+6
styles/proselint/P-Value.yml
···
···
+
extends: existence
+
message: "You should use more decimal places, unless '%s' is really true."
+
ignorecase: true
+
level: suggestion
+
tokens:
+
- 'p = 0\.0{2,4}'
+30
styles/proselint/RASSyndrome.yml
···
···
+
extends: existence
+
message: "'%s' is redundant."
+
level: error
+
action:
+
name: edit
+
params:
+
- split
+
- ' '
+
- '0'
+
tokens:
+
- ABM missile
+
- ACT test
+
- ABM missiles
+
- ABS braking system
+
- ATM machine
+
- CD disc
+
- CPI Index
+
- GPS system
+
- GUI interface
+
- HIV virus
+
- ISBN number
+
- LCD display
+
- PDF format
+
- PIN number
+
- RAS syndrome
+
- RIP in peace
+
- please RSVP
+
- SALT talks
+
- SAT test
+
- UPC codes
+12
styles/proselint/README.md
···
···
+
Copyright © 2014–2015, Jordan Suchow, Michael Pacer, and Lara A. Ross
+
All rights reserved.
+
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+13
styles/proselint/Skunked.yml
···
···
+
extends: existence
+
message: "'%s' is a bit of a skunked term — impossible to use without issue."
+
ignorecase: true
+
level: error
+
tokens:
+
- bona fides
+
- deceptively
+
- decimate
+
- effete
+
- fulsome
+
- hopefully
+
- impassionate
+
- Thankfully
+17
styles/proselint/Spelling.yml
···
···
+
extends: consistency
+
message: "Inconsistent spelling of '%s'."
+
level: error
+
ignorecase: true
+
either:
+
advisor: adviser
+
centre: center
+
colour: color
+
emphasise: emphasize
+
finalise: finalize
+
focussed: focused
+
labour: labor
+
learnt: learned
+
organise: organize
+
organised: organized
+
organising: organizing
+
recognise: recognize
+11
styles/proselint/Typography.yml
···
···
+
extends: substitution
+
message: Consider using the '%s' symbol instead of '%s'.
+
level: error
+
nonword: true
+
swap:
+
'\.\.\.': …
+
'\([cC]\)': ©
+
'\(TM\)': ™
+
'\(tm\)': ™
+
'\([rR]\)': ®
+
'[0-9]+ ?x ?[0-9]+': ×
+50
styles/proselint/Uncomparables.yml
···
···
+
extends: existence
+
message: "'%s' is not comparable"
+
ignorecase: true
+
level: error
+
action:
+
name: edit
+
params:
+
- split
+
- ' '
+
- '1'
+
raw:
+
- \b(?:absolutely|most|more|less|least|very|quite|largely|extremely|increasingly|kind of|mildy|hardly|greatly|sort of)\b\s*
+
tokens:
+
- absolute
+
- adequate
+
- complete
+
- correct
+
- certain
+
- devoid
+
- entire
+
- 'false'
+
- fatal
+
- favorite
+
- final
+
- ideal
+
- impossible
+
- inevitable
+
- infinite
+
- irrevocable
+
- main
+
- manifest
+
- only
+
- paramount
+
- perfect
+
- perpetual
+
- possible
+
- preferable
+
- principal
+
- singular
+
- stationary
+
- sufficient
+
- 'true'
+
- unanimous
+
- unavoidable
+
- unbroken
+
- uniform
+
- unique
+
- universal
+
- void
+
- whole
+6
styles/proselint/Very.yml
···
···
+
extends: existence
+
message: "Remove '%s'."
+
ignorecase: true
+
level: error
+
tokens:
+
- very
+17
styles/proselint/meta.json
···
···
+
{
+
"author": "jdkato",
+
"description": "A Vale-compatible implementation of the proselint linter.",
+
"email": "support@errata.ai",
+
"lang": "en",
+
"url": "https://github.com/errata-ai/proselint/releases/latest/download/proselint.zip",
+
"feed": "https://github.com/errata-ai/proselint/releases.atom",
+
"issues": "https://github.com/errata-ai/proselint/issues/new",
+
"license": "BSD-3-Clause",
+
"name": "proselint",
+
"sources": [
+
"https://github.com/amperser/proselint"
+
],
+
"vale_version": ">=1.0.0",
+
"coverage": 0.0,
+
"version": "0.1.0"
+
}
+702
styles/write-good/Cliches.yml
···
···
+
extends: existence
+
message: "Try to avoid using clichés like '%s'."
+
ignorecase: true
+
level: warning
+
tokens:
+
- a chip off the old block
+
- a clean slate
+
- a dark and stormy night
+
- a far cry
+
- a fine kettle of fish
+
- a loose cannon
+
- a penny saved is a penny earned
+
- a tough row to hoe
+
- a word to the wise
+
- ace in the hole
+
- acid test
+
- add insult to injury
+
- against all odds
+
- air your dirty laundry
+
- all fun and games
+
- all in a day's work
+
- all talk, no action
+
- all thumbs
+
- all your eggs in one basket
+
- all's fair in love and war
+
- all's well that ends well
+
- almighty dollar
+
- American as apple pie
+
- an axe to grind
+
- another day, another dollar
+
- armed to the teeth
+
- as luck would have it
+
- as old as time
+
- as the crow flies
+
- at loose ends
+
- at my wits end
+
- avoid like the plague
+
- babe in the woods
+
- back against the wall
+
- back in the saddle
+
- back to square one
+
- back to the drawing board
+
- bad to the bone
+
- badge of honor
+
- bald faced liar
+
- ballpark figure
+
- banging your head against a brick wall
+
- baptism by fire
+
- barking up the wrong tree
+
- bat out of hell
+
- be all and end all
+
- beat a dead horse
+
- beat around the bush
+
- been there, done that
+
- beggars can't be choosers
+
- behind the eight ball
+
- bend over backwards
+
- benefit of the doubt
+
- bent out of shape
+
- best thing since sliced bread
+
- bet your bottom dollar
+
- better half
+
- better late than never
+
- better mousetrap
+
- better safe than sorry
+
- between a rock and a hard place
+
- beyond the pale
+
- bide your time
+
- big as life
+
- big cheese
+
- big fish in a small pond
+
- big man on campus
+
- bigger they are the harder they fall
+
- bird in the hand
+
- bird's eye view
+
- birds and the bees
+
- birds of a feather flock together
+
- bit the hand that feeds you
+
- bite the bullet
+
- bite the dust
+
- bitten off more than he can chew
+
- black as coal
+
- black as pitch
+
- black as the ace of spades
+
- blast from the past
+
- bleeding heart
+
- blessing in disguise
+
- blind ambition
+
- blind as a bat
+
- blind leading the blind
+
- blood is thicker than water
+
- blood sweat and tears
+
- blow off steam
+
- blow your own horn
+
- blushing bride
+
- boils down to
+
- bolt from the blue
+
- bone to pick
+
- bored stiff
+
- bored to tears
+
- bottomless pit
+
- boys will be boys
+
- bright and early
+
- brings home the bacon
+
- broad across the beam
+
- broken record
+
- brought back to reality
+
- bull by the horns
+
- bull in a china shop
+
- burn the midnight oil
+
- burning question
+
- burning the candle at both ends
+
- burst your bubble
+
- bury the hatchet
+
- busy as a bee
+
- by hook or by crook
+
- call a spade a spade
+
- called onto the carpet
+
- calm before the storm
+
- can of worms
+
- can't cut the mustard
+
- can't hold a candle to
+
- case of mistaken identity
+
- cat got your tongue
+
- cat's meow
+
- caught in the crossfire
+
- caught red-handed
+
- checkered past
+
- chomping at the bit
+
- cleanliness is next to godliness
+
- clear as a bell
+
- clear as mud
+
- close to the vest
+
- cock and bull story
+
- cold shoulder
+
- come hell or high water
+
- cool as a cucumber
+
- cool, calm, and collected
+
- cost a king's ransom
+
- count your blessings
+
- crack of dawn
+
- crash course
+
- creature comforts
+
- cross that bridge when you come to it
+
- crushing blow
+
- cry like a baby
+
- cry me a river
+
- cry over spilt milk
+
- crystal clear
+
- curiosity killed the cat
+
- cut and dried
+
- cut through the red tape
+
- cut to the chase
+
- cute as a bugs ear
+
- cute as a button
+
- cute as a puppy
+
- cuts to the quick
+
- dark before the dawn
+
- day in, day out
+
- dead as a doornail
+
- devil is in the details
+
- dime a dozen
+
- divide and conquer
+
- dog and pony show
+
- dog days
+
- dog eat dog
+
- dog tired
+
- don't burn your bridges
+
- don't count your chickens
+
- don't look a gift horse in the mouth
+
- don't rock the boat
+
- don't step on anyone's toes
+
- don't take any wooden nickels
+
- down and out
+
- down at the heels
+
- down in the dumps
+
- down the hatch
+
- down to earth
+
- draw the line
+
- dressed to kill
+
- dressed to the nines
+
- drives me up the wall
+
- dull as dishwater
+
- dyed in the wool
+
- eagle eye
+
- ear to the ground
+
- early bird catches the worm
+
- easier said than done
+
- easy as pie
+
- eat your heart out
+
- eat your words
+
- eleventh hour
+
- even the playing field
+
- every dog has its day
+
- every fiber of my being
+
- everything but the kitchen sink
+
- eye for an eye
+
- face the music
+
- facts of life
+
- fair weather friend
+
- fall by the wayside
+
- fan the flames
+
- feast or famine
+
- feather your nest
+
- feathered friends
+
- few and far between
+
- fifteen minutes of fame
+
- filthy vermin
+
- fine kettle of fish
+
- fish out of water
+
- fishing for a compliment
+
- fit as a fiddle
+
- fit the bill
+
- fit to be tied
+
- flash in the pan
+
- flat as a pancake
+
- flip your lid
+
- flog a dead horse
+
- fly by night
+
- fly the coop
+
- follow your heart
+
- for all intents and purposes
+
- for the birds
+
- for what it's worth
+
- force of nature
+
- force to be reckoned with
+
- forgive and forget
+
- fox in the henhouse
+
- free and easy
+
- free as a bird
+
- fresh as a daisy
+
- full steam ahead
+
- fun in the sun
+
- garbage in, garbage out
+
- gentle as a lamb
+
- get a kick out of
+
- get a leg up
+
- get down and dirty
+
- get the lead out
+
- get to the bottom of
+
- get your feet wet
+
- gets my goat
+
- gilding the lily
+
- give and take
+
- go against the grain
+
- go at it tooth and nail
+
- go for broke
+
- go him one better
+
- go the extra mile
+
- go with the flow
+
- goes without saying
+
- good as gold
+
- good deed for the day
+
- good things come to those who wait
+
- good time was had by all
+
- good times were had by all
+
- greased lightning
+
- greek to me
+
- green thumb
+
- green-eyed monster
+
- grist for the mill
+
- growing like a weed
+
- hair of the dog
+
- hand to mouth
+
- happy as a clam
+
- happy as a lark
+
- hasn't a clue
+
- have a nice day
+
- have high hopes
+
- have the last laugh
+
- haven't got a row to hoe
+
- head honcho
+
- head over heels
+
- hear a pin drop
+
- heard it through the grapevine
+
- heart's content
+
- heavy as lead
+
- hem and haw
+
- high and dry
+
- high and mighty
+
- high as a kite
+
- hit paydirt
+
- hold your head up high
+
- hold your horses
+
- hold your own
+
- hold your tongue
+
- honest as the day is long
+
- horns of a dilemma
+
- horse of a different color
+
- hot under the collar
+
- hour of need
+
- I beg to differ
+
- icing on the cake
+
- if the shoe fits
+
- if the shoe were on the other foot
+
- in a jam
+
- in a jiffy
+
- in a nutshell
+
- in a pig's eye
+
- in a pinch
+
- in a word
+
- in hot water
+
- in the gutter
+
- in the nick of time
+
- in the thick of it
+
- in your dreams
+
- it ain't over till the fat lady sings
+
- it goes without saying
+
- it takes all kinds
+
- it takes one to know one
+
- it's a small world
+
- it's only a matter of time
+
- ivory tower
+
- Jack of all trades
+
- jockey for position
+
- jog your memory
+
- joined at the hip
+
- judge a book by its cover
+
- jump down your throat
+
- jump in with both feet
+
- jump on the bandwagon
+
- jump the gun
+
- jump to conclusions
+
- just a hop, skip, and a jump
+
- just the ticket
+
- justice is blind
+
- keep a stiff upper lip
+
- keep an eye on
+
- keep it simple, stupid
+
- keep the home fires burning
+
- keep up with the Joneses
+
- keep your chin up
+
- keep your fingers crossed
+
- kick the bucket
+
- kick up your heels
+
- kick your feet up
+
- kid in a candy store
+
- kill two birds with one stone
+
- kiss of death
+
- knock it out of the park
+
- knock on wood
+
- knock your socks off
+
- know him from Adam
+
- know the ropes
+
- know the score
+
- knuckle down
+
- knuckle sandwich
+
- knuckle under
+
- labor of love
+
- ladder of success
+
- land on your feet
+
- lap of luxury
+
- last but not least
+
- last hurrah
+
- last-ditch effort
+
- law of the jungle
+
- law of the land
+
- lay down the law
+
- leaps and bounds
+
- let sleeping dogs lie
+
- let the cat out of the bag
+
- let the good times roll
+
- let your hair down
+
- let's talk turkey
+
- letter perfect
+
- lick your wounds
+
- lies like a rug
+
- life's a bitch
+
- life's a grind
+
- light at the end of the tunnel
+
- lighter than a feather
+
- lighter than air
+
- like clockwork
+
- like father like son
+
- like taking candy from a baby
+
- like there's no tomorrow
+
- lion's share
+
- live and learn
+
- live and let live
+
- long and short of it
+
- long lost love
+
- look before you leap
+
- look down your nose
+
- look what the cat dragged in
+
- looking a gift horse in the mouth
+
- looks like death warmed over
+
- loose cannon
+
- lose your head
+
- lose your temper
+
- loud as a horn
+
- lounge lizard
+
- loved and lost
+
- low man on the totem pole
+
- luck of the draw
+
- luck of the Irish
+
- make hay while the sun shines
+
- make money hand over fist
+
- make my day
+
- make the best of a bad situation
+
- make the best of it
+
- make your blood boil
+
- man of few words
+
- man's best friend
+
- mark my words
+
- meaningful dialogue
+
- missed the boat on that one
+
- moment in the sun
+
- moment of glory
+
- moment of truth
+
- money to burn
+
- more power to you
+
- more than one way to skin a cat
+
- movers and shakers
+
- moving experience
+
- naked as a jaybird
+
- naked truth
+
- neat as a pin
+
- needle in a haystack
+
- needless to say
+
- neither here nor there
+
- never look back
+
- never say never
+
- nip and tuck
+
- nip it in the bud
+
- no guts, no glory
+
- no love lost
+
- no pain, no gain
+
- no skin off my back
+
- no stone unturned
+
- no time like the present
+
- no use crying over spilled milk
+
- nose to the grindstone
+
- not a hope in hell
+
- not a minute's peace
+
- not in my backyard
+
- not playing with a full deck
+
- not the end of the world
+
- not written in stone
+
- nothing to sneeze at
+
- nothing ventured nothing gained
+
- now we're cooking
+
- off the top of my head
+
- off the wagon
+
- off the wall
+
- old hat
+
- older and wiser
+
- older than dirt
+
- older than Methuselah
+
- on a roll
+
- on cloud nine
+
- on pins and needles
+
- on the bandwagon
+
- on the money
+
- on the nose
+
- on the rocks
+
- on the spot
+
- on the tip of my tongue
+
- on the wagon
+
- on thin ice
+
- once bitten, twice shy
+
- one bad apple doesn't spoil the bushel
+
- one born every minute
+
- one brick short
+
- one foot in the grave
+
- one in a million
+
- one red cent
+
- only game in town
+
- open a can of worms
+
- open and shut case
+
- open the flood gates
+
- opportunity doesn't knock twice
+
- out of pocket
+
- out of sight, out of mind
+
- out of the frying pan into the fire
+
- out of the woods
+
- out on a limb
+
- over a barrel
+
- over the hump
+
- pain and suffering
+
- pain in the
+
- panic button
+
- par for the course
+
- part and parcel
+
- party pooper
+
- pass the buck
+
- patience is a virtue
+
- pay through the nose
+
- penny pincher
+
- perfect storm
+
- pig in a poke
+
- pile it on
+
- pillar of the community
+
- pin your hopes on
+
- pitter patter of little feet
+
- plain as day
+
- plain as the nose on your face
+
- play by the rules
+
- play your cards right
+
- playing the field
+
- playing with fire
+
- pleased as punch
+
- plenty of fish in the sea
+
- point with pride
+
- poor as a church mouse
+
- pot calling the kettle black
+
- pretty as a picture
+
- pull a fast one
+
- pull your punches
+
- pulling your leg
+
- pure as the driven snow
+
- put it in a nutshell
+
- put one over on you
+
- put the cart before the horse
+
- put the pedal to the metal
+
- put your best foot forward
+
- put your foot down
+
- quick as a bunny
+
- quick as a lick
+
- quick as a wink
+
- quick as lightning
+
- quiet as a dormouse
+
- rags to riches
+
- raining buckets
+
- raining cats and dogs
+
- rank and file
+
- rat race
+
- reap what you sow
+
- red as a beet
+
- red herring
+
- reinvent the wheel
+
- rich and famous
+
- rings a bell
+
- ripe old age
+
- ripped me off
+
- rise and shine
+
- road to hell is paved with good intentions
+
- rob Peter to pay Paul
+
- roll over in the grave
+
- rub the wrong way
+
- ruled the roost
+
- running in circles
+
- sad but true
+
- sadder but wiser
+
- salt of the earth
+
- scared stiff
+
- scared to death
+
- sealed with a kiss
+
- second to none
+
- see eye to eye
+
- seen the light
+
- seize the day
+
- set the record straight
+
- set the world on fire
+
- set your teeth on edge
+
- sharp as a tack
+
- shoot for the moon
+
- shoot the breeze
+
- shot in the dark
+
- shoulder to the wheel
+
- sick as a dog
+
- sigh of relief
+
- signed, sealed, and delivered
+
- sink or swim
+
- six of one, half a dozen of another
+
- skating on thin ice
+
- slept like a log
+
- slinging mud
+
- slippery as an eel
+
- slow as molasses
+
- smart as a whip
+
- smooth as a baby's bottom
+
- sneaking suspicion
+
- snug as a bug in a rug
+
- sow wild oats
+
- spare the rod, spoil the child
+
- speak of the devil
+
- spilled the beans
+
- spinning your wheels
+
- spitting image of
+
- spoke with relish
+
- spread like wildfire
+
- spring to life
+
- squeaky wheel gets the grease
+
- stands out like a sore thumb
+
- start from scratch
+
- stick in the mud
+
- still waters run deep
+
- stitch in time
+
- stop and smell the roses
+
- straight as an arrow
+
- straw that broke the camel's back
+
- strong as an ox
+
- stubborn as a mule
+
- stuff that dreams are made of
+
- stuffed shirt
+
- sweating blood
+
- sweating bullets
+
- take a load off
+
- take one for the team
+
- take the bait
+
- take the bull by the horns
+
- take the plunge
+
- takes one to know one
+
- takes two to tango
+
- the more the merrier
+
- the real deal
+
- the real McCoy
+
- the red carpet treatment
+
- the same old story
+
- there is no accounting for taste
+
- thick as a brick
+
- thick as thieves
+
- thin as a rail
+
- think outside of the box
+
- third time's the charm
+
- this day and age
+
- this hurts me worse than it hurts you
+
- this point in time
+
- three sheets to the wind
+
- through thick and thin
+
- throw in the towel
+
- tie one on
+
- tighter than a drum
+
- time and time again
+
- time is of the essence
+
- tip of the iceberg
+
- tired but happy
+
- to coin a phrase
+
- to each his own
+
- to make a long story short
+
- to the best of my knowledge
+
- toe the line
+
- tongue in cheek
+
- too good to be true
+
- too hot to handle
+
- too numerous to mention
+
- touch with a ten foot pole
+
- tough as nails
+
- trial and error
+
- trials and tribulations
+
- tried and true
+
- trip down memory lane
+
- twist of fate
+
- two cents worth
+
- two peas in a pod
+
- ugly as sin
+
- under the counter
+
- under the gun
+
- under the same roof
+
- under the weather
+
- until the cows come home
+
- unvarnished truth
+
- up the creek
+
- uphill battle
+
- upper crust
+
- upset the applecart
+
- vain attempt
+
- vain effort
+
- vanquish the enemy
+
- vested interest
+
- waiting for the other shoe to drop
+
- wakeup call
+
- warm welcome
+
- watch your p's and q's
+
- watch your tongue
+
- watching the clock
+
- water under the bridge
+
- weather the storm
+
- weed them out
+
- week of Sundays
+
- went belly up
+
- wet behind the ears
+
- what goes around comes around
+
- what you see is what you get
+
- when it rains, it pours
+
- when push comes to shove
+
- when the cat's away
+
- when the going gets tough, the tough get going
+
- white as a sheet
+
- whole ball of wax
+
- whole hog
+
- whole nine yards
+
- wild goose chase
+
- will wonders never cease?
+
- wisdom of the ages
+
- wise as an owl
+
- wolf at the door
+
- words fail me
+
- work like a dog
+
- world weary
+
- worst nightmare
+
- worth its weight in gold
+
- wrong side of the bed
+
- yanking your chain
+
- yappy as a dog
+
- years young
+
- you are what you eat
+
- you can run but you can't hide
+
- you only live once
+
- you're the boss
+
- young and foolish
+
- young and vibrant
+32
styles/write-good/E-Prime.yml
···
···
+
extends: existence
+
message: "Try to avoid using '%s'."
+
ignorecase: true
+
level: suggestion
+
tokens:
+
- am
+
- are
+
- aren't
+
- be
+
- been
+
- being
+
- he's
+
- here's
+
- here's
+
- how's
+
- i'm
+
- is
+
- isn't
+
- it's
+
- she's
+
- that's
+
- there's
+
- they're
+
- was
+
- wasn't
+
- we're
+
- were
+
- weren't
+
- what's
+
- where's
+
- who's
+
- you're
+11
styles/write-good/Illusions.yml
···
···
+
extends: repetition
+
message: "'%s' is repeated!"
+
level: error
+
alpha: true
+
action:
+
name: edit
+
params:
+
- truncate
+
- " "
+
tokens:
+
- '[^\s]+'
+183
styles/write-good/Passive.yml
···
···
+
extends: existence
+
message: "'%s' may be passive voice. Use active voice if you can."
+
ignorecase: true
+
level: warning
+
raw:
+
- \b(am|are|were|being|is|been|was|be)\b\s*
+
tokens:
+
- '[\w]+ed'
+
- awoken
+
- beat
+
- become
+
- been
+
- begun
+
- bent
+
- beset
+
- bet
+
- bid
+
- bidden
+
- bitten
+
- bled
+
- blown
+
- born
+
- bought
+
- bound
+
- bred
+
- broadcast
+
- broken
+
- brought
+
- built
+
- burnt
+
- burst
+
- cast
+
- caught
+
- chosen
+
- clung
+
- come
+
- cost
+
- crept
+
- cut
+
- dealt
+
- dived
+
- done
+
- drawn
+
- dreamt
+
- driven
+
- drunk
+
- dug
+
- eaten
+
- fallen
+
- fed
+
- felt
+
- fit
+
- fled
+
- flown
+
- flung
+
- forbidden
+
- foregone
+
- forgiven
+
- forgotten
+
- forsaken
+
- fought
+
- found
+
- frozen
+
- given
+
- gone
+
- gotten
+
- ground
+
- grown
+
- heard
+
- held
+
- hidden
+
- hit
+
- hung
+
- hurt
+
- kept
+
- knelt
+
- knit
+
- known
+
- laid
+
- lain
+
- leapt
+
- learnt
+
- led
+
- left
+
- lent
+
- let
+
- lighted
+
- lost
+
- made
+
- meant
+
- met
+
- misspelt
+
- mistaken
+
- mown
+
- overcome
+
- overdone
+
- overtaken
+
- overthrown
+
- paid
+
- pled
+
- proven
+
- put
+
- quit
+
- read
+
- rid
+
- ridden
+
- risen
+
- run
+
- rung
+
- said
+
- sat
+
- sawn
+
- seen
+
- sent
+
- set
+
- sewn
+
- shaken
+
- shaven
+
- shed
+
- shod
+
- shone
+
- shorn
+
- shot
+
- shown
+
- shrunk
+
- shut
+
- slain
+
- slept
+
- slid
+
- slit
+
- slung
+
- smitten
+
- sold
+
- sought
+
- sown
+
- sped
+
- spent
+
- spilt
+
- spit
+
- split
+
- spoken
+
- spread
+
- sprung
+
- spun
+
- stolen
+
- stood
+
- stridden
+
- striven
+
- struck
+
- strung
+
- stuck
+
- stung
+
- stunk
+
- sung
+
- sunk
+
- swept
+
- swollen
+
- sworn
+
- swum
+
- swung
+
- taken
+
- taught
+
- thought
+
- thrived
+
- thrown
+
- thrust
+
- told
+
- torn
+
- trodden
+
- understood
+
- upheld
+
- upset
+
- wed
+
- wept
+
- withheld
+
- withstood
+
- woken
+
- won
+
- worn
+
- wound
+
- woven
+
- written
+
- wrung
+27
styles/write-good/README.md
···
···
+
Based on [write-good](https://github.com/btford/write-good).
+
+
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
+
+
```
+
The MIT License (MIT)
+
+
Copyright (c) 2014 Brian Ford
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS 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.
+
```
+5
styles/write-good/So.yml
···
···
+
extends: existence
+
message: "Don't start a sentence with '%s'."
+
level: error
+
raw:
+
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'
+6
styles/write-good/ThereIs.yml
···
···
+
extends: existence
+
message: "Don't start a sentence with '%s'."
+
ignorecase: false
+
level: error
+
raw:
+
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'
+221
styles/write-good/TooWordy.yml
···
···
+
extends: existence
+
message: "'%s' is too wordy."
+
ignorecase: true
+
level: warning
+
tokens:
+
- a number of
+
- abundance
+
- accede to
+
- accelerate
+
- accentuate
+
- accompany
+
- accomplish
+
- accorded
+
- accrue
+
- acquiesce
+
- acquire
+
- additional
+
- adjacent to
+
- adjustment
+
- admissible
+
- advantageous
+
- adversely impact
+
- advise
+
- aforementioned
+
- aggregate
+
- aircraft
+
- all of
+
- all things considered
+
- alleviate
+
- allocate
+
- along the lines of
+
- already existing
+
- alternatively
+
- amazing
+
- ameliorate
+
- anticipate
+
- apparent
+
- appreciable
+
- as a matter of fact
+
- as a means of
+
- as far as I'm concerned
+
- as of yet
+
- as to
+
- as yet
+
- ascertain
+
- assistance
+
- at the present time
+
- at this time
+
- attain
+
- attributable to
+
- authorize
+
- because of the fact that
+
- belated
+
- benefit from
+
- bestow
+
- by means of
+
- by virtue of
+
- by virtue of the fact that
+
- cease
+
- close proximity
+
- commence
+
- comply with
+
- concerning
+
- consequently
+
- consolidate
+
- constitutes
+
- demonstrate
+
- depart
+
- designate
+
- discontinue
+
- due to the fact that
+
- each and every
+
- economical
+
- eliminate
+
- elucidate
+
- employ
+
- endeavor
+
- enumerate
+
- equitable
+
- equivalent
+
- evaluate
+
- evidenced
+
- exclusively
+
- expedite
+
- expend
+
- expiration
+
- facilitate
+
- factual evidence
+
- feasible
+
- finalize
+
- first and foremost
+
- for all intents and purposes
+
- for the most part
+
- for the purpose of
+
- forfeit
+
- formulate
+
- have a tendency to
+
- honest truth
+
- however
+
- if and when
+
- impacted
+
- implement
+
- in a manner of speaking
+
- in a timely manner
+
- in a very real sense
+
- in accordance with
+
- in addition
+
- in all likelihood
+
- in an effort to
+
- in between
+
- in excess of
+
- in lieu of
+
- in light of the fact that
+
- in many cases
+
- in my opinion
+
- in order to
+
- in regard to
+
- in some instances
+
- in terms of
+
- in the case of
+
- in the event that
+
- in the final analysis
+
- in the nature of
+
- in the near future
+
- in the process of
+
- inception
+
- incumbent upon
+
- indicate
+
- indication
+
- initiate
+
- irregardless
+
- is applicable to
+
- is authorized to
+
- is responsible for
+
- it is
+
- it is essential
+
- it seems that
+
- it was
+
- magnitude
+
- maximum
+
- methodology
+
- minimize
+
- minimum
+
- modify
+
- monitor
+
- multiple
+
- necessitate
+
- nevertheless
+
- not certain
+
- not many
+
- not often
+
- not unless
+
- not unlike
+
- notwithstanding
+
- null and void
+
- numerous
+
- objective
+
- obligate
+
- obtain
+
- on the contrary
+
- on the other hand
+
- one particular
+
- optimum
+
- overall
+
- owing to the fact that
+
- participate
+
- particulars
+
- pass away
+
- pertaining to
+
- point in time
+
- portion
+
- possess
+
- preclude
+
- previously
+
- prior to
+
- prioritize
+
- procure
+
- proficiency
+
- provided that
+
- purchase
+
- put simply
+
- readily apparent
+
- refer back
+
- regarding
+
- relocate
+
- remainder
+
- remuneration
+
- requirement
+
- reside
+
- residence
+
- retain
+
- satisfy
+
- shall
+
- should you wish
+
- similar to
+
- solicit
+
- span across
+
- strategize
+
- subsequent
+
- substantial
+
- successfully complete
+
- sufficient
+
- terminate
+
- the month of
+
- the point I am trying to make
+
- therefore
+
- time period
+
- took advantage of
+
- transmit
+
- transpire
+
- type of
+
- until such time as
+
- utilization
+
- utilize
+
- validate
+
- various different
+
- what I mean to say is
+
- whether or not
+
- with respect to
+
- with the exception of
+
- witnessed
+207
styles/write-good/Weasel.yml
···
···
+
extends: existence
+
message: "'%s' is a weasel word!"
+
ignorecase: true
+
level: warning
+
tokens:
+
- absolutely
+
- accidentally
+
- additionally
+
- allegedly
+
- alternatively
+
- angrily
+
- anxiously
+
- approximately
+
- awkwardly
+
- badly
+
- barely
+
- beautifully
+
- blindly
+
- boldly
+
- bravely
+
- brightly
+
- briskly
+
- bristly
+
- bubbly
+
- busily
+
- calmly
+
- carefully
+
- carelessly
+
- cautiously
+
- cheerfully
+
- clearly
+
- closely
+
- coldly
+
- completely
+
- consequently
+
- correctly
+
- courageously
+
- crinkly
+
- cruelly
+
- crumbly
+
- cuddly
+
- currently
+
- daily
+
- daringly
+
- deadly
+
- definitely
+
- deliberately
+
- doubtfully
+
- dumbly
+
- eagerly
+
- early
+
- easily
+
- elegantly
+
- enormously
+
- enthusiastically
+
- equally
+
- especially
+
- eventually
+
- exactly
+
- exceedingly
+
- exclusively
+
- extremely
+
- fairly
+
- faithfully
+
- fatally
+
- fiercely
+
- finally
+
- fondly
+
- few
+
- foolishly
+
- fortunately
+
- frankly
+
- frantically
+
- generously
+
- gently
+
- giggly
+
- gladly
+
- gracefully
+
- greedily
+
- happily
+
- hardly
+
- hastily
+
- healthily
+
- heartily
+
- helpfully
+
- honestly
+
- hourly
+
- hungrily
+
- hurriedly
+
- immediately
+
- impatiently
+
- inadequately
+
- ingeniously
+
- innocently
+
- inquisitively
+
- interestingly
+
- irritably
+
- jiggly
+
- joyously
+
- justly
+
- kindly
+
- largely
+
- lately
+
- lazily
+
- likely
+
- literally
+
- lonely
+
- loosely
+
- loudly
+
- loudly
+
- luckily
+
- madly
+
- many
+
- mentally
+
- mildly
+
- monthly
+
- mortally
+
- mostly
+
- mysteriously
+
- neatly
+
- nervously
+
- nightly
+
- noisily
+
- normally
+
- obediently
+
- occasionally
+
- only
+
- openly
+
- painfully
+
- particularly
+
- patiently
+
- perfectly
+
- politely
+
- poorly
+
- powerfully
+
- presumably
+
- previously
+
- promptly
+
- punctually
+
- quarterly
+
- quickly
+
- quietly
+
- rapidly
+
- rarely
+
- really
+
- recently
+
- recklessly
+
- regularly
+
- remarkably
+
- relatively
+
- reluctantly
+
- repeatedly
+
- rightfully
+
- roughly
+
- rudely
+
- sadly
+
- safely
+
- selfishly
+
- sensibly
+
- seriously
+
- sharply
+
- shortly
+
- shyly
+
- significantly
+
- silently
+
- simply
+
- sleepily
+
- slowly
+
- smartly
+
- smelly
+
- smoothly
+
- softly
+
- solemnly
+
- sparkly
+
- speedily
+
- stealthily
+
- sternly
+
- stupidly
+
- substantially
+
- successfully
+
- suddenly
+
- surprisingly
+
- suspiciously
+
- swiftly
+
- tenderly
+
- tensely
+
- thoughtfully
+
- tightly
+
- timely
+
- truthfully
+
- unexpectedly
+
- unfortunately
+
- usually
+
- very
+
- victoriously
+
- violently
+
- vivaciously
+
- warmly
+
- waverly
+
- weakly
+
- wearily
+
- weekly
+
- wildly
+
- wisely
+
- worldly
+
- wrinkly
+
- yearly
+4
styles/write-good/meta.json
···
···
+
{
+
"feed": "https://github.com/errata-ai/write-good/releases.atom",
+
"vale_version": ">=1.0.0"
+
}
+10
templates/404.html
···
···
+
{%- extends "index.html" -%}
+
+
{% block og_preview %}
+
{% endblock og_preview %}
+
+
{% block main %}
+
<h1>404</h1>
+
+
<p>Page not found</p>
+
{% endblock main %}
+1
templates/anchor-link.html
···
···
+
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">#</a>
+40
templates/atom.xml
···
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<?xml-stylesheet type="text/xsl" href="/atom-style.xml"?>
+
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
+
<title>{{ config.title }}
+
{%- if term %} - {{ term.name }}
+
{%- elif section.title %} - {{ section.title }}
+
{%- endif -%}
+
</title>
+
{%- if config.description %}
+
<subtitle>{{ config.description }}</subtitle>
+
{%- endif %}
+
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
+
<link href="
+
{%- if section -%}
+
{{ section.permalink | escape_xml | safe }}
+
{%- else -%}
+
{{ config.base_url | escape_xml | safe }}
+
{%- endif -%}
+
" rel="related"/>
+
<generator uri="https://www.getzola.org/">Zola</generator>
+
<updated>{{ last_updated | date(format="%+") }}</updated>
+
<id>{{ feed_url | safe }}</id>
+
{%- for page in pages %}
+
<entry xml:lang="{{ page.lang }}">
+
<title>{{ page.title }}</title>
+
<published>{{ page.date | date(format="%+") }}</published>
+
<updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
+
<link href="{{ page.permalink | safe }}" type="text/html"/>
+
<id>{{ page.permalink | safe }}</id>
+
<summary>
+
{%- if page.description -%}
+
{{ page.description }}
+
{%- elif page.summary -%}
+
{{ page.summary | safe }}
+
{%- endif -%}
+
</summary>
+
<content type="html">{{ page.content }}</content>
+
</entry>
+
{%- endfor %}
+
</feed>
+103 -3
templates/index.html
···
{% extends "zerm/templates/index.html" %}
{% block general_meta %}
<meta http-equiv="content-type" content="text/html; charset=utf-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
-
<meta name="description" content="{{ config.description }}"/>
-
<script async defer data-domain="hauleth.dev" src="https://plausible.io/js/plausible.js"></script>
{% endblock general_meta %}
···
{% extends "zerm/templates/index.html" %}
+
{% block fonts %}
+
{% endblock fonts %}
+
+
{% block rss %}
+
{%- if config.generate_feed -%}
+
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} Feed" href="{{ get_url(path="atom.xml", trailing_slash=false) }}">
+
{%- endif -%}
+
{% endblock rss %}
+
+
{% block og_preview %}
+
{%- if section -%}
+
<link rel="canonical" href="{{ section.permalink }}" />
+
{%- elif page -%}
+
<link rel="canonical" href="{{ page.permalink }}" />
+
{%- else -%}
+
<link rel="canonical" href="{{ current_url }}" />
+
{%- endif -%}
+
{{ social::og_preview() }}
+
+
{%- if config.extra.twitter.site -%}
+
<meta name="twitter:site" content="{{ config.extra.twitter.site }}" />
+
{%- endif -%}
+
{%- if config.extra.twitter.creator -%}
+
<meta name="twitter:creator" content="{{ config.extra.twitter.creator }}" />
+
{%- endif -%}
+
+
{%- if config.extra.webmention -%}
+
<link rel="webmention" href="{{ config.extra.webmention }}" >
+
{%- endif -%}
+
{% endblock og_preview %}
+
+
{% block copyright %}
+
<div class="copyright">
+
<div class="copyright--user">{{ config.extra.copyright | safe }}</div>
+
<div class="copyright--tracking">
+
public tracking available at <a href="https://plausible.io/hauleth.dev">Plausible.io</a>
+
</div>
+
<div class="copyright--source">
+
<a href="{{ config.extra.source }}">source code</a>
+
</div>
+
</div>
+
{% endblock copyright %}
+
+
{% block script %}
+
<script defer data-domain="hauleth.dev" src="/js/script.js"></script>
+
{% endblock script %}
+
+
{% block css %}
+
<link rel="stylesheet" type="text/css" href="/style.css" />
+
<link rel="stylesheet" type="text/css" href="/syntax-theme.css" />
+
{% if config.extra.theme_color != "orange" -%}
+
{% set color = "/color/" ~ config.extra.theme_color ~ ".css" -%}
+
<link rel="stylesheet" type="text/css" href="{{ color }}" />
+
{%- else -%}
+
<link rel="stylesheet" type="text/css" href="/color/orange.css" />
+
{% endif %}
+
<meta name="theme-color" content="#1d1e28" />
+
{% endblock css %}
+
+
{% block header %}
+
<header class="header">
+
<div class="header__inner">
+
<div class="header__logo">
+
{{ logo::logo() }}
+
</div>
+
</div>
+
<nav class="menu">
+
<ul class="menu__inner">
+
{%- for menu_item in config.extra.main_menu -%}
+
<li>
+
<a href="{% if menu_item.url is matching("^https?://") %}{{ menu_item.url }}{% else %}{{ get_url(path=menu_item.url) }}{% endif %}"
+
{% if menu_item.rel %}rel="{{ menu_item.rel }}"{% endif %}>{{ menu_item.name }}</a>
+
</li>
+
{%- endfor-%}
+
</ul>
+
</nav>
+
</header>
+
{% endblock header %}
+
{% block general_meta %}
<meta http-equiv="content-type" content="text/html; charset=utf-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1">
+
{%- if page.title -%}
+
<meta name="description" content="{{ config.description }} {{ page.title }} {{ page.description }}"/>
+
{%- else -%}
+
<meta name="description" content="{{ config.description }}"/>
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags or page.taxonomies.categories -%}
+
<meta name="keywords" content="
+
{%- if page.taxonomies.categories -%}
+
{%- for cat in page.taxonomies.categories -%}
+
{{ cat }}, {% endfor -%}
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags -%}
+
{%- for tag in page.taxonomies.tags -%}
+
{%- if loop.last -%}
+
{{ tag }}
+
{%- else -%}
+
{{ tag }}, {% endif -%}
+
{%- endfor -%}
+
{%- endif -%}
+
" />
+
{%- endif -%}
{% endblock general_meta %}
+14
templates/landing.html
···
···
+
{% extends "index.html" -%}
+
+
{%- block main -%}
+
<section>
+
<article>
+
{{ section.content | safe }}
+
</article>
+
</section>
+
+
<div>
+
<h2>blog</h2>
+
{{ lists::list_pages() }}
+
</div>
+
{%- endblock main -%}
+16
templates/macros/extended_footer.html
···
···
+
{% macro extended_footer() %}
+
<div class="rings">
+
<details>
+
<summary>Webrings</summary>
+
<ul>
+
{%- for ring in config.extra.webrings -%}
+
<li>
+
<a href="{{ ring.url }}/prev?referrer={{ get_url(path = "/") | safe }}">&laquo;</a>
+
<a href="{{ ring.url }}">{{ ring.name }}</a>
+
<a href="{{ ring.url }}/next?referrer={{ get_url(path = "/") | safe }}">&raquo;</a>
+
</li>
+
{%- endfor -%}
+
</ul>
+
</details>
+
</div>
+
{% endmacro extended_footer %}
+32
templates/macros/lists.html
···
···
+
{% macro list_pages() %}
+
<section class="posts">
+
+
{%- for page in paginator.pages -%}
+
+
<div class="post on-list">
+
<h3 class="post-title">
+
<a href="{{ page.permalink }}">{{ page.title }}</a>
+
</h3>
+
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
+
<div class="post-content">
+
{% if page.description -%}
+
{{ page.description }}
+
{#- end if-check for description -#}
+
{% elif page.summary -%}
+
{{ page.summary | safe }}
+
{% endif -%}
+
</div>
+
{% if page.description or page.summary -%}
+
<div>
+
<a class="read-more button" href="{{ page.permalink }}">{{ config.extra.read_more }} →</a>
+
</div>
+
{% endif -%}
+
</div>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</section>
+
{% endmacro list_pages %}
+69
templates/macros/posts.html
···
···
+
{% macro section_meta(section, author) %}
+
<div class="post-meta">
+
{%- if section.extra["date"] -%}
+
<time class="post-date" datetime="{{ section.extra["date"] }}">
+
{{ section.extra["date"] | date(format="%Y.%m.%d") }}
+
</time>
+
{%- endif -%}
+
</div>
+
{% endmacro section_meta %}
+
+
{% macro meta(page, author) %}
+
<div class="post-meta">
+
<span class="post-date">
+
{%- if page.date -%}
+
<time class="dt-published" datetime="{{ page.date }}">{{ page.date | date(format="%Y.%m.%d") }}</time>
+
{%- endif -%}
+
+
{%- if page.updated -%}
+
[Updated: <time class="dt-updated" datetime="{{ page.updated }}">{{ page.updated | date(format="%Y.%m.%d") }}</time>]
+
{%- endif -%}
+
</span>
+
::
+
<time datetime="P{{ page.reading_time }}M">{{ page.reading_time }} min</time>
+
+
{{ posts::taxonomies(taxonomy=page.taxonomies,
+
disp_cat=config.extra.show_categories,
+
disp_tag=config.extra.show_tags) }}
+
</div>
+
{% endmacro meta %}
+
+
{% macro taxonomies(taxonomy, disp_cat, disp_tag) %}
+
{%- if taxonomy.categories -%}
+
{{ posts::categories(categories=taxonomy.categories) }}
+
{%- endif -%}
+
{%- if taxonomy.tags -%}
+
{{ posts::tags(tags=taxonomy.tags) }}
+
{%- endif -%}
+
{% endmacro taxonomies %}
+
+
{% macro categories(categories) %}
+
:: {
+
{%- for cat in categories -%}
+
{%- if loop.last -%}
+
<a class="p-category" href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>
+
{%- else -%}
+
<a class="p-category" href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>,
+
{%- endif -%}
+
{%- endfor -%}}{# <--- NOTE: OPEN CURLY BRACE #}
+
{% endmacro categories %}
+
+
{% macro tags(tags) %}
+
::
+
{% for tag in tags -%}
+
#<a class="p-category" href="{{get_taxonomy_url(kind="tags", name=tag )}}">{{ tag }}</a>
+
{% endfor -%}
+
{% endmacro tags %}
+
+
{% macro thanks(who) %}
+
{%- if who is object -%}
+
{%- if who.url -%}
+
<a class="u-url p-name" href="{{ who.url }}">{{ who.name }}</a>
+
{%- else -%}
+
<span class="p-name">{{ who.name }}</span>
+
{%- endif -%}
+
{%- if who.why %} for {{ who.why }}{%- endif -%}
+
{%- else -%}
+
<span class="p-name">{{ who }}</span>
+
{%- endif -%}
+
{% endmacro %}
+11
templates/macros/toc.html
···
···
+
{% macro toc(headers) %}
+
<ol start="0">
+
{%- for header in headers -%}
+
<li><a href="{{ header.permalink }}">{{ header.title }}</a>
+
{%- if header.children | length > 0 -%}
+
{{ self::toc(headers=header.children) }}
+
{%- endif -%}
+
</li>
+
{%- endfor -%}
+
</ol>
+
{% endmacro toc %}
+78
templates/page.html
···
···
+
{%- extends "index.html" -%}
+
+
{%- import "macros/toc.html" as toc -%}
+
+
{%- block title -%}
+
<title>{{ page.title }} - {{ config.extra.author }}</title>
+
{%- endblock title -%}
+
+
{%- block main -%}
+
<article class="post h-entry">
+
<h1 class="post-title p-name">
+
<a id="top" href="{{ page.permalink }}">{{ page.title }}</a>
+
</h1>
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
{%- if page.toc | length >= 5 -%}
+
<div class="post-toc">
+
<label for="toc-toggle">Table of content</label>
+
<input type="checkbox" id="toc-toggle" hidden />
+
<div class="toggleable">
+
{{ toc::toc(headers=page.toc) }}
+
</div>
+
</div>
+
{%- endif -%}
+
<div class="post-content e-content">
+
{{ page.content | safe }}
+
<div class="halmos">&#x220e;</div>
+
</div>
+
{%- if config.extra.for_hire -%}
+
<div class="for-hire">
+
<p>Author of this post is currently open for hire (<a href="/cv">CV</a>).<p>
+
<p>You can contact me at <a href="lukasz@niemier.pl">lukasz@niemier.pl</a></p>
+
</div>
+
{%- endif -%}
+
{%- if page.extra.thanks -%}
+
<hr />
+
<p>
+
<b>Special thanks to</b>:
+
<ul>
+
{%- for person in page.extra.thanks -%}
+
<li class="h-card">{{ posts::thanks(who=person) }}</li>
+
{%- endfor -%}
+
</ul>
+
</p>
+
{%- endif -%}
+
{%- if not page.extra.no_comments -%}
+
<hr />
+
<div>
+
<p>You can provide feedback via mailing list
+
<a href="mailto:~hauleth/blog@lists.sr.ht?subject=[Comment] {{
+
page.title }}">~hauleth/blog@lists.sr.ht</a>
+
(<a href="https://lists.sr.ht/~hauleth/blog">archive</a>).</p>
+
</div>
+
+
<div class="webmentions">
+
{%- set webmentions = load_data(url="https://webmention.io/api/mentions.jf2?target=" ~ page.permalink, format="json") -%}
+
{%- if webmentions.children | length > 0 -%}
+
<p>Webmentions:</p>
+
<ul>
+
{%- for mention in webmentions.children -%}
+
<li>
+
{%- if mention.name -%}
+
<a href="{{ mention.url }}">{{ mention.name }}</a>
+
{%- else -%}
+
<a href="{{ mention.url }}" class="url-only">{{ mention.url }}</a>
+
{%- endif -%}
+
{% if mention.author.name != "" %}
+
by&nbsp;<a href="{{ mention.author.url }}">{{ mention.author.name | truncate(length=15) }}</a>
+
{%- endif -%}
+
</li>
+
{%- endfor -%}
+
</ul>
+
{%- else -%}
+
No webmentions yet, you can be first.
+
{%- endif -%}
+
</div>
+
{%- endif -%}
+
</article>
+
{%- endblock main -%}
+4
templates/robots.txt
···
···
+
User-agent: *
+
Disallow: /cv/ /404/ /common-test-example/
+
Allow: /
+
Sitemap: {{ get_url(path="sitemap.xml") }}
+186
templates/shortcodes/cv.md
···
···
+
# Łukasz Jan Niemier
+
+
## Personal information
+
+
Email:
+
<~@hauleth.dev>
+
+
Website:
+
<https://hauleth.dev>
+
+
Twitter:
+
[@hauleth](https://twitter.com/hauleth)
+
+
## Education
+
+
- Poznań University of Technology: Computer Science - no degree - 2012-2015
+
+ Secretary of AKAI - Students' Association of Web Developers
+
+
## Experience
+
+
- Hauleth.dev - Consultant - 2021+
+
+ DockYard/Karambit.ai - 2025
+
* Architectural analysis of Karambit product
+
* Prepared security analysis with detailed report with fixes
+
+ Eiger - 2022-2023
+
* Forte.io
+
- Implemented Interledger protocol for cross-chain transactions
+
* Aleo Blockchain
+
- Implemented GraphQL API for the on-chain data
+
- Created syntax colouring library for Aleo assembly-like language
+
for smart contracts
+
+ Erlang Solutions/Kloeckner GmbH - 2021 - Consultant for Elixir, Ruby,
+
and SQL (PostgreSQL)
+
* Optimised DB query performance by providing PostgreSQL structure
+
analysis and improving indices usage
+
+ Remote Inc. - Senior Backend Engineer - 2020-2021
+
* Architectural analysis of existing codebase
+
+ Kobil GmbH - Erlang/Elixir Developer - 2019-2020
+
* Maintained MongoDB driver for Elixir
+
* Implemented transactions for MongoDB driver in Elixir
+
- Supabase - 2023-2025
+
+ Logflare - logs aggregation service:
+
* Implemented on-the-fly decompression of incoming data that improved
+
ingestion possibilities and reduced transfer usage (created library
+
[`plug_caisson`][] for that purpose)
+
* Implemented DataDog-compatible ingestion endpoint for seamless
+
transition from DataDog provider to Logflare
+
* Improved BigQuery pipeline workflow to reduce congestion on database
+
connections
+
* Added support for AWS Cloud Events metadata extraction
+
* Improved CI usage by splitting different actions to separate steps ran
+
in parallel
+
* Replaced dynamic generation of connection modules for PostgreSQL
+
storage system with Ecto's dynamic repositories to avoid atom exhaustion
+
+ Supavisor - a cloud-native, multi-tenant Postgres connection pooler
+
* Deployment management
+
* Optimised metrics gathering system that resulted in an order of
+
magnitude performance boost
+
* Updated used OTP and Elixir versions from OTP 24 to OTP 27 and Elixir
+
from 1.14 to 1.18
+
* Reduced usage of mocking in tests to improve tests performance and
+
volatility, resulting in reduced CI usage and improved developer
+
experience
+
* Implemented e2e tests against existing Node.js PostgreSQL clients to
+
improve production issues
+
* Implemented multi-region deployment system to provide blue/green
+
deployments
+
* Improved system observability features by making it more resilient and
+
performant
+
* Replaced usage of `ct_slave` with newer `peer` module in OTP
+
- AppUnite - Full-stack Developer/DevOps - 2016-2019:
+
+ JaFolders/AlleFolders
+
* 2x performance improvement by optimising PostgreSQL usage
+
* Reduced geo-queries using PostGIS thanks to better indices and
+
materialised views usage
+
* Implemented UI and brochure viewer in Vue and SVG
+
+ OneMedical/Helium Health
+
* Architectural redesign of application from Rails/MongoDB to
+
Phoenix/PostgreSQL
+
* Prepared hybrid deployment with on-premise/in-cloud system
+
* Migrated of the existing deployments from MongoDB to PostgreSQL
+
- Nukomeet - Full-stack Developer - 2015-2016
+
- Prograils - Junior Developer - 2013
+
+
### Organisations
+
+
- Erlang Ecosystem Foundation - member of the Observability WG
+
- OpenTelemetry Project - member of the Erlang WG
+
+
### Other fields
+
+
- Volunteer:
+
+ Pyrkon Fan Convention
+
* Helper - 2011, 2012, 2013, 2014, 2016
+
* Organizer - 2015
+
+ UEFA Championship 2012 - Poland-Ukraine
+
* ICT Accreditation support
+
- Times Person of the Year - 2006
+
+
### Languages
+
+
- Polish - mother tongue
+
- English - fluent
+
+
### Showcase
+
+
- GitHub: <https://github.com/hauleth>
+
- GitLab: <https://gitlab.com/hauleth>
+
- SourceHut: <https://sr.ht/~hauleth>
+
- StackOverflow: <https://stackoverflow.com/u/1017941>
+
+
### Notable contributions
+
+
- Elixir language:
+
+ Logger reimplementation on top of Erlang's `logger` module
+
+ `mix test --cover` CLI output
+
+ Support for `NO_COLOR` environment variable
+
+ `is_struct/1`
+
+ Fixing module inspection on case-insensitive file systems
+
+ Support for parsing extra arguments via `mix eval` and `eval` command in
+
release
+
- Erlang OTP:
+
+ Support for custom devices in `logger_std_h`
+
+ Fixing `socket` module to support broader set of protocols (for example
+
ICMP)
+
+ Support for global metadata in `logger`
+
+ Support for reconfiguration of `logger` (needed for better Mix and Rebar3
+
integration)
+
+ Several fixes to `logger` and `socket` modules
+
+ Add support for τ constant in `math`
+
- Git:
+
+ Add support for Elixir in diff
+
- Ecto:
+
+ Support aggregations over `*`
+
+ Better error on duplicated `schema` block
+
- Elixir MongoDB driver
+
+ Support for transactions
+
+
### Notable projects
+
+
- <https://github.com/hauleth/erlang-systemd> - systemd integration for Erlang
+
projects
+
- <https://github.com/hauleth/mix_unused> - Mix compiler for detecting unused
+
code
+
- <https://github.com/open-telemetry/opentelemetry-erlang> - maintainer of
+
the Erlang's OpenTelemetry implementation
+
- Vim plugins:
+
+ <https://github.com/hauleth/asyncdo.vim> - simple asynchronous task runner
+
+ <https://github.com/hauleth/sad.vim> - search and replace text - faster
+
+ <https://gitlab.com/hauleth/qfx.vim> - display signs next to QF matches
+
+
### Languages and Frameworks
+
+
- Elixir
+
+ Phoenix
+
+ Ecto
+
- Erlang
+
+ OpenTelemetry collaborator
+
+ EEF Member
+
+ OTP contributor
+
- Nix/NixOS
+
- Rust
+
- PostgreSQL
+
- sh/Bash
+
- Ruby
+
+ Ruby on Rails
+
+
### Technologies
+
+
- Git
+
- Vim
+
- HashiStack
+
+ Terraform
+
+ Consul
+
+ Nomad
+
- GNU/Linux and other UNIX-like systems
+
- TDD/BDD methodologies
+
- Property testing
+
+
## Other
+
+
- Viking reenactor
+
- Keyboard fan
+
- Sci-fi/Fantasy fan and Poznań's Sci-fi/Fantasy club member
+
+
[`plug_caisson`]: https://github.com/supabase/plug_caisson
+17
templates/shortcodes/readme.md
···
···
+
# hello
+
+
<div class="h-card">
+
+
My name is <span class="p-name">Łukasz Niemier</span>, but I am mostly using
+
<span class="p-nickname">Hauleth</span> over the internet.
+
+
I am part of the <span class="h-card"><a class="p-name p-org u-url"
+
href="https://erlef.org/wg/observability">Erlang Ecosystem Foundation
+
Observability WG</a></span> where we are trying to improve observability
+
features in Erlang ecosystem.
+
+
I am open for hire. If you want to inquiry me about my services then
+
contact me at <a class="u-email"
+
href="mailto:lukasz@niemier.pl">lukasz@niemier.pl</a>.
+
+
</div>
+13
templates/sitemap.xml
···
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+
{%- for sitemap_entry in entries %}
+
{%- if sitemap_entry.extra | get(key="sitemap", default=true) %}
+
<url>
+
<loc>{{ sitemap_entry.permalink | escape_xml | safe }}</loc>
+
{%- if sitemap_entry.updated %}
+
<lastmod>{{ sitemap_entry.updated }}</lastmod>
+
{%- endif %}
+
</url>
+
{%- endif %}
+
{%- endfor %}
+
</urlset>
+1
themes/zerm/.gitignore
···
···
+
public
+22
themes/zerm/LICENSE.md
···
···
+
The MIT License (MIT)
+
+
Copyright (c) 2019 elias julian marko garcia
+
Copyright (c) 2019 Paweł Romanowski
+
Copyright (c) 2019 panr
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
+
this software and associated documentation files (the "Software"), to deal in
+
the Software without restriction, including without limitation the rights to
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+
the Software, and to permit persons to whom the Software is furnished to do so,
+
subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+
COPYRIGHT HOLDERS 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.
+61
themes/zerm/README.md
···
···
+
# zerm
+
+
a minimalist and dark theme for [Zola](https://getzola.org).
+
+
![Screenshot](../master/zerm-preview.png?raw=true)
+
+
[**Live Preview!**](https://zerm.ejmg.now.sh/)
+
+
Largely a port of Radek Kozieł's [Terminal
+
Theme](https://github.com/panr/hugo-theme-terminal) for Hugo. 4/5ths of my way
+
through porting this theme, I discovered Paweł Romanowski own independent fork
+
for Zola, [Terminimal](https://github.com/pawroman/zola-theme-terminimal),
+
which helped me get the PostCSS to Sass styling conversion done more
+
quickly. My sincerest thanks to both of you!
+
+
## differences
+
+
This theme is largely true to the original by Radek, but there are some mild
+
differences. They are almost all stylistic in nature and are intended to
+
emphasize minimalism even more. Some of them are as follows:
+
- tags are now included in a post's meta data.
+
- no post image previews.
+
- categories are included in the taxonomy.
+
- bullet points have slightly more margin and different symbols for nesting.
+
- no social media or comment support.
+
+
Some of these might be added later and [PR's are always
+
welcomed](https://github.com/ejmg/zerm/pulls).
+
+
## configuration
+
+
Please follow the Zola documentation for [how to use a
+
theme](https://www.getzola.org/documentation/themes/installing-and-using-themes/#installing-a-theme).
+
+
In `config.toml`, you will find all values for customization that are supported
+
thus far have documentation explaining how they are used. If there is any confusion or something is not working as intended, [please open an issue](https://github.com/ejmg/zerm/issues)!
+
+
## math
+
You can use KaTeX for mathematical typesetting.
+
Assets are only available if you opt-in on a per-page level through
+
a single line (`math=true`) on the extra section of the page frontmatter.
+
+
``` md
+
# index.md
+
+++
+
title="this page title"
+
...
+
+
[extra]
+
math=true
+
+++
+
+
Content
+
```
+
+
Pages wich doesn't opt-in are not affected in any way, so you doesn't have
+
to worry about any performance hit.
+
+
## license
+
+
MIT. See `LICENSE.md` for more details.
+131
themes/zerm/config.toml
···
···
+
# The URL the site will be built for
+
base_url = "/"
+
+
# Used in RSS by default
+
title = "My blog!"
+
description = "placeholder description text for your blog!"
+
+
# The default language, used in RSS
+
# TODO: I would love to support more languages and make this easy to handle
+
# with other facets of the theme.
+
default_language = "en"
+
+
# Whether to generate a RSS feed automatically
+
generate_feed = true
+
# 'atom.xml' (default if unspecified) and 'rss.xml' are officially supported
+
# values for feed_filename in this theme. All other filenames will assume a
+
# link type of 'application/rss+xml'.
+
# feed_filename = "atom.xml"
+
+
# Theme name to use.
+
# NOTE: should not need to mess with this if you are using zerm directly, i.e. cloning the
+
# repository at root and not using as directed by the Zola docs via themes/ directory.
+
# theme = ""
+
+
# Whether to automatically compile all Sass files in the sass directory
+
compile_sass = true
+
+
# Whether to do syntax highlighting
+
# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
+
highlight_code = true
+
+
# Syntax highlighting theme. See:
+
# https://www.getzola.org/documentation/getting-started/configuration/#syntax-highlighting
+
# for more information and themes built into Zola.
+
highlight_theme = "axar" # Other dark themes that work: "1337", "agola-dark",
+
# "visual-studio-dark"
+
+
# Whether to build a search index to be used later on by a JavaScript library
+
build_search_index = false
+
+
# Built in taxonomies of zerm.
+
taxonomies = [
+
{name = "tags"},
+
{name = "categories"},
+
]
+
+
[extra]
+
# Put all your custom variables here
+
#
+
# Many configurations are taken directly from Terminal's config.toml
+
# ---------------------------------------------------------
+
+
# Author name to be added to posts, if enabled.
+
author = "you!"
+
+
# Show author's name in blog post meta data.
+
show_author = false
+
+
# Show categories a blog post is marked with in its meta data.
+
show_categories = true
+
+
# Show tags a blog post is marked with in its meta data.
+
show_tags = true
+
+
# Theme color. You can have any color you want, so long as it's...
+
# ["orange", "blue", "red", "green", "pink"]
+
theme_color = "orange"
+
+
# Custom css to style over the defaults. This is useful when you only have a
+
# few small tweaks to make rather than a major rehaul to the theme.
+
# It would be best to make this a proper .sass or .scss file in sass/ rather
+
# than placing in static/
+
# custom_css = "custom.css"
+
+
# How many menu items to show on desktop. if you set this to 0, only submenu
+
# button will be visible.
+
show_menu_items = 2
+
+
# set theme to full screen width.
+
full_width = false
+
+
# center theme with default width.
+
center = false
+
+
# set a custom favicon. Must be placed in root of static/ directory...
+
# favicon = ""
+
+
+
# Set a custom preview image for your website when posted as a link.
+
# Must be placed in root of static/ directory...
+
# og_preview_img = ""
+
+
# Copyright notice if desired. Defaults to
+
# copyright = "copyright notice here"
+
+
# What is displayed in the top left corner of the website. Default is zerm.
+
logo_text = "zerm"
+
+
# Link in logo. Default returns you to $BASE_URL.
+
logo_home_link = "/"
+
+
# Menu items to display. You define a url and the name of the menu item.
+
# NOTE: `$BASE_URL/` must be included in the url name.
+
main_menu = [
+
{url="/about/", name="about"},
+
{url="/contact/", name="contact"},
+
{url="https://google.com", name="Google", external=true},
+
]
+
+
# Displayed as title of drop-down menu when size of main_menu > show_menu_items.
+
menu_more = "show more"
+
+
# Displayed after teaser text for a blog post.
+
read_more = "read more"
+
+
# not currently used from previous theme, but leaving here for now in case I
+
# feel like adding it.
+
read_other_posts = "read other posts"
+
+
+
# Enable math typesetting with KaTeX
+
# Show math in pages with `math=true` in the TOML frontmatter
+
enable_katex = true
+
+
# Options for disqus
+
disqus = { enabled=false, short_name="" }
+
+
# generate Table of Contents for all articles
+
# Table of Contents can be generated for individual articles
+
# by adding `ToC = true` in [extra] section in frontmatter
+
# ToC = true
+6
themes/zerm/content/_index.md
···
···
+
+++
+
sort_by = "date"
+
transparent = true
+
paginate_by = 3
+
insert_anchor_links = "right"
+
+++
+10
themes/zerm/content/about/_index.md
···
···
+
+++
+
title = "about"
+
path = "about"
+
+
[extra]
+
date = 2019-03-21
+
+++
+
+
Yet another theme for yet another static site generator; that said, I hope you
+
like it.
+14
themes/zerm/content/contact/_index.md
···
···
+
+++
+
title="contact"
+
description="a basic demo of zola. Does it work?"
+
+
[extra]
+
date=2019-03-26
+
+++
+
+
# some links
+
+
- [zola, the static site generator written in rust](https://getzola.org)
+
- [source code for zerm](https://github.com/ejmg/zerm)
+
- [Terminal, the theme zerm was derived from](https://github.com/panr/hugo-theme-terminal)
+
- [Terminimal, another theme for zola based on Terminal](https://github.com/pawroman/zola-theme-terminimal)
+65
themes/zerm/content/demo/index.md
···
···
+
+++
+
title="demo"
+
description="a basic demo of zola."
+
date=2019-08-06
+
+
[taxonomies]
+
tags = ["demo", "zola", "highlighting"]
+
categories = ["programming", "wu tang",]
+
+
[extra]
+
+++
+
+
Here's a general demo of Zola and how things look with zerm.
+
+
# Header I
+
+
Inline code: `println!("Wu Tang!");`
+
+
Zola has built in syntax highlighting. If there's not a theme you like, you can
+
easily add more.
+
+
zerm uses Fira Code fonts, which means we get ligatures in addition to
+
Zola's powerful syntax highlighting ✨.
+
+
```rs
+
fn foo(arg: String) -> Result<u32, Io::Error> {
+
println!("Nice!"); // TODO: the thingy
+
if 1 != 0 {
+
println!("How many ligatures can I contrive??");
+
println!("Turns out a lot! ==> -/-> <!-- <$> >>=");
+
}
+
Ok(42)
+
}
+
```
+
+
## Header II
+
+
Want block quotes? We got block quotes.
+
+
Remember the wise words of Ras Kass:
+
+
> In Hotel Rwanda, reminder to honor these street scholars who ask why
+
U.S. Defense is twenty percent of the tax dollar. Bush gave 6.46 billion to
+
Halliburton for troops support efforts in Iraq; meanwhile, the hood is hurting,
+
please believe that.
+
>
+
> -- "Verses", _Wu-Tang Meets The Indie Culture_
+
+
### Header III
+
+
| members | age | notable album | to be messed with? |
+
|------------------|-----|----------------------------------------------|-------------------------------------------------------------------------|
+
| GZA | 52 | Liquid Swords | no |
+
| ODB | 35 | Return to the 36 Chambers: The Dirty Version | absolutely not |
+
| Raekwon Da Chef | 49 | Only Built 4 Cuban Linx... | `"no"` that's spanish for "no" |
+
| Ghostface Killah | 49 | Fishscale | i swear you keep asking that question and the answer ain't gonna change |
+
| Inspectah Deck | 49 | CZARFACE | `protect ya neck, boy` |
+
+
+
#### Header IV
+
+
Here's a video of my rabbit, Smalls, loaf'n to lofi beats:
+
+
{{ youtube(id="UUpuz8IObcs") }}
+
+13
themes/zerm/content/fiz/index.md
···
···
+
+++
+
title="fiz"
+
description="a basic demo of zola. Does it work?"
+
date=2019-03-25
+
author="elias"
+
+
[taxonomies]
+
tags = ["rust", "test"]
+
# categories = ["misc."]
+
+++
+
+
+
Foo Bar Buzz Fizz Qux Fum
+21
themes/zerm/content/technology_is_hell/index.md
···
···
+
+++
+
title="technology is hell!"
+
description="Yet another blog post ranting about XYZ technology for ABC reasons"
+
date=2019-03-26
+
+
[taxonomies]
+
tags = ["rust", "test"]
+
categories = ["misc."]
+
+++
+
+
+
Nullam eu ante vel est convallis dignissim. Fusce suscipit, wisi nec facilisis
+
facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc
+
porta vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam
+
posuere. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada
+
massa, quis varius mi purus non odio. Pellentesque condimentum, magna ut
+
suscipit hendrerit, ipsum augue ornare nulla, non luctus diam neque sit amet
+
urna. Curabitur vulputate vestibulum lorem. Fusce sagittis, libero non
+
molestie mollis, magna orci ultrices dolor, at vulputate neque nulla lacinia
+
eros. Sed id ligula quis est convallis tempor. Curabitur lacinia pulvinar
+
nibh. Nam a sapien.
+57
themes/zerm/content/using_katex/index.md
···
···
+
+++
+
title="Using KaTeX for mathematical typesetting"
+
date=2021-06-16
+
+
[taxonomies]
+
categories=["test"]
+
tags=["math", "zola"]
+
+
[extra]
+
math=true
+
+++
+
+
The usual way to include LaTeX is to use `$$`, as shown in the examples below.
+
+
These examples are taken from <http://khan.github.io/KaTeX/>
+
+
### Example 1
+
If the text between `$$` contains newlines it will rendered in display mode:
+
```
+
$$
+
f(x) = \int_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,d\xi
+
$$
+
```
+
+
$$
+
f(x) = \int_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,d\xi
+
$$
+
+
### Example 2
+
+
```
+
$$
+
\frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
$$
+
+
1(ϕ5−ϕ)e25π=1+e−2π1+e−4π1+e−6π1+e−8π1+⋯ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
```
+
+
$$
+
\frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
$$
+
+
1(ϕ5−ϕ)e25π=1+e−2π1+e−4π1+e−6π1+e−8π1+⋯ \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
+
+
### Example 3
+
+
```
+
$$
+
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, \quad\quad \text{for }\lvert q\rvert<1.
+
$$
+
```
+
+
$$
+
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, \quad\quad \text{for }\lvert q\rvert<1.
+
$$
+
+
+42
themes/zerm/content/waz/index.md
···
···
+
+++
+
title="waz"
+
description="a basic demo of zola. Does it work? This old man, he played one. He played knick knack on my drum."
+
date=2019-03-27
+
+
[taxonomies]
+
tags = ["rust", "test", "zola"]
+
categories = ["programming", "misc.", "programming languages"]
+
+
[extra]
+
+++
+
+
# Hello Hello
+
+
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec hendrerit
+
tempor tellus. Donec pretium posuere tellus. Proin quam nisl, tincidunt et,
+
mattis eget, convallis nec, purus. Cum sociis natoque penatibus et magnis dis
+
parturient montes, nascetur ridiculus mus. Nulla posuere. Donec vitae dolor.
+
Nullam tristique diam non turpis. Cras placerat accumsan nulla. Nullam
+
rutrum. Nam vestibulum accumsan nisl.
+
+
+
```python
+
def foo(bar, **kwargs):
+
print("yo, this is nice!")
+
```
+
+
## a list
+
+
* Donec hendrerit tempor tellus.
+
* Nam a sapien.
+
* Phasellus at dui in ligula mollis ultricies.
+
* Mauris mollis tincidunt felis.
+
* Nullam rutrum.
+
+
### Yet another list
+
1. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa,
+
quis varius mi purus non odio.
+
2. Donec hendrerit tempor tellus.
+
3. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa,
+
quis varius mi purus non odio.
+
+69
themes/zerm/content/zerm/index.md
···
···
+
+++
+
title="what is zerm?"
+
description="a summary of what zerm is and why it is different."
+
date=2019-08-07
+
updated=2021-02-03
+
+
[taxonomies]
+
tags = ["rust", "test", "zola"]
+
categories = ["programming", "misc.",]
+
+
[extra]
+
ToC = true
+
+++
+
+
# hello
+
+
This is zerm, a minimalist theme for Zola based[^1] off of [panr's](https://twitter.com/panr)
+
theme for Hugo.
+
+
While it's largely faithful to the original, there are some changes:
+
- no prism.js integration, instead we use Zola's syntax highlighting to reduce overhead.
+
- removal of PostCSS and leveraging Zola's use of Sass for simple styling.
+
- much thanks to [Paweł
+
Romanowski's](https://github.com/pawroman/zola-theme-terminimal/)
+
independent fork of Terminal. Their Sass stylings saved me the overhead of
+
figuring it out myself.
+
- no preview images. I want a theme that is focused on content.
+
- support for anchor links.
+
- Other small, opinionated changes that I think lend to the minimalism and
+
aesthetic of zerm.
+
+
+
Things this theme does not have but either Terminal or Terminimal might:
+
- better short-codes for things like embedding videos or images, though I will
+
work on this over time.
+
- better support for things like comments and social media. As of now, I have
+
no plans to add this but [PR's are always
+
**welcomed**](https://github.com/ejmg/zerm/pulls).
+
+
## A quick demo
+
+
`println!("inline code");`
+
+
```rs
+
fn main(n: String) {
+
println!("hello, zola!");
+
}
+
```
+
+
### Header III
+
+
> a somewhat kinda maybe large quote that maybe spans
+
> more than one line but I mean really who even cares
+
> okay maybe I do but point being is yes nice.
+
+
#### Header IV
+
+
| hello | tables | nice |
+
|:-----:|:---------:|------|
+
| wow | much love | yes |
+
+
+
Like zerm? Then [install
+
Zola](https://www.getzola.org/documentation/getting-started/installation/) and
+
[get started](https://www.getzola.org/documentation/themes/installing-and-using-themes/#installing-a-theme)!
+
+
---
+
+
[^1]: fork? port? a little bit of the former, more of the latter?
+7
themes/zerm/package.json
···
···
+
{
+
"scripts":
+
{
+
"install": "curl -L -O https://github.com/getzola/zola/releases/download/v0.13.0/zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz && tar -xzf zola-v0.13.0-x86_64-unknown-linux-gnu.tar.gz",
+
"build": "./zola build"
+
}
+
}
+29
themes/zerm/sass/_buttons.scss
···
···
+
button,
+
.button,
+
a.button {
+
position: relative;
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
padding: 8px 18px;
+
margin-bottom: 5px;
+
text-decoration: none;
+
text-align: center;
+
border-radius: 8px;
+
border: 1px solid transparent;
+
appearance: none;
+
cursor: pointer;
+
outline: none;
+
}
+
+
a.read-more,
+
a.read-more:hover,
+
a.read-more:active {
+
display: inline-flex;
+
background: none;
+
box-shadow: none;
+
padding: 0;
+
margin: 20px 0;
+
max-width: 100%;
+
}
+
+15
themes/zerm/sass/_font.scss
···
···
+
@font-face {
+
font-family: 'Fira Code';
+
font-style: normal;
+
font-weight: 400;
+
font-display: swap;
+
src: url("assets/fonts/FiraCode-Regular.woff2") format("woff2"), url("assets/fonts/FiraCode-Regular.woff") format("woff");
+
}
+
+
@font-face {
+
font-family: 'Fira Code';
+
font-style: normal;
+
font-weight: 800;
+
font-display: swap;
+
src: url("assets/fonts/FiraCode-Bold.woff2") format("woff2"), url("assets/fonts/FiraCode-Bold.woff") format("woff");
+
}
+67
themes/zerm/sass/_footer.scss
···
···
+
@import "variables";
+
+
.footer {
+
padding: 40px 0;
+
flex-grow: 0;
+
opacity: .5;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
margin: 0;
+
width: 760px;
+
max-width: 100%;
+
+
@media (max-width: $tablet-max-width) {
+
flex-direction: column;
+
}
+
}
+
+
a {
+
color: inherit;
+
}
+
+
.copyright {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
font-size: 1rem;
+
// so `--light-color-secondary` color exists no where else in the stylings
+
// color: var(--light-color-secondary);
+
// As a substitute, I'm going to use the alpha-70 version of accent.
+
color: var(--accent-alpha-70);
+
+
&--user {
+
margin: auto;
+
text-align: center;
+
}
+
+
& > *:first-child:not(:only-child) {
+
margin-right: 10px;
+
+
@media (max-width: $tablet-max-width) {
+
border: none;
+
padding: 0;
+
margin: 0;
+
}
+
}
+
+
@media (max-width: $tablet-max-width) {
+
flex-direction: column;
+
margin-top: 10px;
+
}
+
}
+
+
// .copyright-theme-sep {
+
// @media (max-width: $tablet-max-width) {
+
// display: none;
+
// }
+
// }
+
+
// .copyright-theme {
+
// @media (max-width: $tablet-max-width) {
+
// font-size: 0.75rem;
+
// }
+
// }
+
}
+139
themes/zerm/sass/_header.scss
···
···
+
@import "variables";
+
+
@mixin menu {
+
background: var(--background);
+
box-shadow: var(--shadow);
+
color: var(--color);
+
border: 2px solid;
+
margin: 0;
+
padding: 10px;
+
list-style: none;
+
z-index: 99;
+
}
+
+
.header {
+
display: flex;
+
flex-direction: column;
+
position: relative;
+
+
&__inner {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
}
+
+
&__logo {
+
display: flex;
+
flex: 1;
+
+
&:after {
+
content: '';
+
background: repeating-linear-gradient(90deg, var(--accent), var(--accent) 2px, transparent 0, transparent 10px);
+
display: block;
+
width: 100%;
+
right: 10px;
+
}
+
+
a {
+
flex: 0 0 auto;
+
max-width: 100%;
+
text-decoration: none;
+
}
+
}
+
+
.menu {
+
margin: 20px 0;
+
--shadow-color: var(--accent-alpha-70);
+
--shadow: 0 10px var(--shadow-color), -10px 10px var(--shadow-color), 10px 10px var(--shadow-color);
+
+
@media (max-width: $phone-max-width) {
+
@include menu;
+
position: absolute;
+
top: 50px;
+
right: 0;
+
}
+
+
&__inner {
+
// @include menu;
+
display: flex;
+
flex-wrap: wrap;
+
list-style: none;
+
margin: 0;
+
padding: 0;
+
+
&--desktop {
+
@media (max-width: $phone-max-width) {
+
display: none;
+
}
+
}
+
+
&--mobile {
+
display: none;
+
+
@media (max-width: $phone-max-width) {
+
display: block;
+
}
+
}
+
+
li {
+
// &.active {
+
// color: var(--accent-alpha-70);
+
// }
+
&:not(:last-of-type) {
+
margin-right: 20px;
+
margin-bottom: 10px;
+
flex: 0 0 auto;
+
}
+
}
+
+
@media (max-width: $phone-max-width) {
+
flex-direction: column;
+
align-items: flex-start;
+
padding: 0;
+
+
li {
+
margin: 0;
+
padding: 5px;
+
}
+
}
+
}
+
+
&__sub-inner {
+
position: relative;
+
list-style: none;
+
padding: 0;
+
margin: 0;
+
+
&:not(:only-child) {
+
margin-left: 20px;
+
}
+
+
&-more {
+
@include menu;
+
top: 35px;
+
left: 0;
+
+
&-trigger {
+
color: var(--accent);
+
user-select: none;
+
cursor: pointer;
+
}
+
+
li {
+
margin: 0;
+
padding: 5px;
+
white-space: nowrap;
+
}
+
}
+
}
+
+
&-trigger {
+
color: var(--accent);
+
border: 2px solid;
+
margin-left: 10px;
+
height: 100%;
+
padding: 3px 8px;
+
position: relative;
+
}
+
}
+
}
+8
themes/zerm/sass/_logo.scss
···
···
+
.logo {
+
display: flex;
+
align-items: center;
+
text-decoration: none;
+
background: var(--accent);
+
color: black;
+
padding: 5px 10px;
+
}
+335
themes/zerm/sass/_main.scss
···
···
+
@import "variables";
+
+
html {
+
box-sizing: border-box;
+
}
+
+
*,
+
*:before,
+
*:after {
+
box-sizing: inherit;
+
}
+
+
body {
+
margin: 0;
+
padding: 0;
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-size: 1rem;
+
line-height: 1.54;
+
background-color: var(--background);
+
color: var(--color);
+
text-rendering: optimizeLegibility;
+
-webkit-font-smoothing: antialiased;
+
-webkit-overflow-scrolling: touch;
+
-webkit-text-size-adjust: 100%;
+
font-feature-settings: "liga";
+
+
@media (max-width: $phone-max-width) {
+
font-size: 1rem;
+
}
+
}
+
+
h1, h2, h3, h4, h5, h6 {
+
line-height: 1.3;
+
+
&:not(first-child) {
+
margin-top: 40px;
+
}
+
+
.zola-anchor {
+
font-size: 1.5rem;
+
visibility: hidden;
+
margin-left: 0.5rem;
+
vertical-align: 1%;
+
text-decoration: none;
+
border-bottom-color: transparent;
+
cursor: pointer;
+
+
@media(max-width: $phone-max-width){
+
visibility: visible;
+
}
+
}
+
+
&:hover {
+
.zola-anchor {
+
visibility: visible;
+
}
+
}
+
}
+
+
// Actually keeping Pawroman's stylings here for font-size over h1-h6.
+
// I prefer differentiated header height.
+
+
// OLD
+
// ---------------------
+
// h1, h2, h3 {
+
// font-size: 1.4rem;
+
// }
+
+
// h4, h5, h6 {
+
// font-size: 1.2rem;
+
// }
+
+
// Pawroman's
+
// ---------------------
+
h1 {
+
font-size: 1.4rem;
+
}
+
+
h2 {
+
font-size: 1.3rem;
+
}
+
+
h3 {
+
font-size: 1.2rem;
+
}
+
+
h4, h5, h6 {
+
font-size: 1.15rem;
+
}
+
+
+
a {
+
color: inherit;
+
}
+
+
img {
+
display: block;
+
max-width: 100%;
+
+
&.left {
+
margin-right: auto;
+
}
+
+
&.center {
+
margin-left: auto;
+
margin-right: auto;
+
}
+
+
&.right {
+
margin-left: auto;
+
}
+
}
+
+
p {
+
margin-bottom: 20px;
+
}
+
+
figure {
+
display: table;
+
max-width: 100%;
+
margin: 25px 0;
+
+
&.left {
+
// img {
+
margin-right: auto;
+
// }
+
}
+
+
&.center {
+
// img {
+
margin-left: auto;
+
margin-right: auto;
+
// }
+
}
+
+
&.right {
+
// img {
+
margin-left: auto;
+
// }
+
}
+
+
figcaption {
+
font-size: 14px;
+
padding: 5px 10px;
+
margin-top: 5px;
+
background: var(--accent);
+
color: var(--background);
+
+
&.left {
+
text-align: left;
+
}
+
+
&.center {
+
text-align: center;
+
}
+
+
&.right {
+
text-align: right;
+
}
+
}
+
}
+
+
code {
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-feature-settings: normal;
+
background: var(--accent-alpha-20);
+
color: var(--accent);
+
padding: 1px 6px;
+
margin: 0 2px;
+
font-size: .95rem;
+
}
+
+
pre {
+
font-family: 'Fira Code', Menlo, DejaVu Sans Mono, Monaco, Consolas, Ubuntu Mono, monospace;
+
font-feature-settings: "liga";
+
padding: 20px 10px;
+
font-size: .95rem;
+
overflow: auto;
+
border-top: 1px solid rgba(255, 255, 255, .1);
+
border-bottom: 1px solid rgba(255, 255, 255, .1);
+
+
+ pre {
+
border-top: 0;
+
margin-top: -40px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
white-space: pre-wrap;
+
word-wrap: break-word;
+
}
+
+
code {
+
background: none !important;
+
margin: 0;
+
padding: 0;
+
font-size: inherit;
+
border: none;
+
+
table {
+
table-layout: auto;
+
border-collapse: collapse;
+
box-sizing: border-box;
+
width: 100%;
+
margin: 00px 0;
+
}
+
+
table, th, td {
+
border: none;
+
padding: 0px;
+
}
+
+
table tr td:first-child {
+
padding-right: 10px;
+
}
+
+
}
+
}
+
+
blockquote {
+
border-top: 1px solid var(--accent);
+
border-bottom: 1px solid var(--accent);
+
margin: 40px 0;
+
padding: 25px;
+
+
@media (max-width: $phone-max-width) {
+
padding-right: 0;
+
}
+
+
&:before {
+
content: '”';
+
font-family: Georgia, serif;
+
font-size: 3.875rem;
+
position: absolute;
+
left: -40px;
+
top: -20px;
+
}
+
+
p:first-of-type {
+
margin-top: 0;
+
}
+
+
p:last-of-type {
+
margin-bottom: 0;
+
}
+
+
p {
+
position: relative;
+
}
+
+
p:before {
+
content: '>';
+
display: block;
+
position: absolute;
+
left: -25px;
+
color: var(--accent);
+
}
+
}
+
+
table {
+
table-layout: fixed;
+
border-collapse: collapse;
+
width: 100%;
+
margin: 40px 0;
+
}
+
+
table, th, td {
+
border: 1px dashed var(--accent);
+
padding: 10px;
+
}
+
+
th {
+
color: var(--accent);
+
}
+
+
ul, ol {
+
margin-left: 30px;
+
padding: 0;
+
+
li {
+
position: relative;
+
margin-top: 5px;
+
margin-bottom: 5px;
+
}
+
+
@media (max-width: $phone-max-width) {
+
margin-left: 20px;
+
}
+
+
ul, ol {
+
margin-top: 10px;
+
margin-bottom: 10px;
+
}
+
}
+
+
ol ol {
+
list-style-type: lower-alpha;
+
}
+
+
.container {
+
display: flex;
+
flex-direction: column;
+
padding: 40px;
+
max-width: 864px;
+
min-height: 100vh;
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
+
+
&.full,
+
&.center {
+
border: none;
+
margin: 0 auto;
+
}
+
+
&.full {
+
max-width: 100%;
+
}
+
+
@media (max-width: $phone-max-width) {
+
padding: 20px;
+
}
+
}
+
+
.content {
+
display: flex;
+
}
+
+
hr {
+
width: 100%;
+
border: none;
+
background: var(--border-color);
+
height: 1px;
+
}
+
+
.hidden {
+
display: none;
+
}
+63
themes/zerm/sass/_pagination.scss
···
···
+
@import 'variables';
+
+
.pagination {
+
margin-top: 50px;
+
+
&__buttons {
+
display: flex;
+
align-items: center;
+
justify-content: center;
+
+
// @media (max-width: $phone-max-width) {
+
// flex-direction: column;
+
// }
+
+
a {
+
text-decoration: none;
+
}
+
}
+
}
+
+
.button {
+
position: relative;
+
display: inline-flex;
+
align-items: center;
+
justify-content: center;
+
font-size: 1rem;
+
border-radius: 8px;
+
max-width: 40%;
+
padding: 0;
+
cursor: pointer;
+
appearance: none;
+
+
+ .button {
+
margin-left: 10px;
+
}
+
+
// @media (max-width: $phone-max-width) {
+
// max-width: 80%;
+
// }
+
+
a {
+
display: flex;
+
padding: 8px 16px;
+
text-decoration: none;
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&__text {
+
text-overflow: ellipsis;
+
white-space: nowrap;
+
overflow: hidden;
+
}
+
+
&.next .button__icon {
+
margin-left: 8px;
+
}
+
+
&.previous .button__icon {
+
margin-right: 8px;
+
}
+
}
+114
themes/zerm/sass/_post.scss
···
···
+
@import "variables";
+
+
// .posts {
+
// margin: 0 auto;
+
// }
+
+
.post {
+
width: 100%;
+
text-align: left;
+
margin: 20px auto;
+
padding: 20px 0;
+
+
@media (max-width: $tablet-max-width) {
+
max-width: 660px;
+
}
+
+
&:not(:last-of-type) {
+
border-bottom: 1px solid var(--border-color);
+
}
+
+
// %meta {
+
// font-size: 1rem;
+
// margin-bottom: 10px;
+
// color: var(--accent-alpha-70);
+
// }
+
+
// &-meta {
+
// @extend %meta;
+
// }
+
// &-meta-inline {
+
// @extend %meta;
+
+
// display: inline;
+
// }
+
+
&-meta {
+
font-size: 1rem;
+
margin-bottom: 10px;
+
color: var(--accent-alpha-70);
+
}
+
+
&-title {
+
--border: 3px dotted var(--accent);
+
position: relative;
+
color: var(--accent);
+
margin: 0 0 15px;
+
padding-bottom: 15px;
+
border-bottom: var(--border);
+
+
&:after {
+
content: '';
+
position: absolute;
+
bottom: 2px;
+
display: block;
+
width: 100%;
+
border-bottom: var(--border);
+
}
+
+
a {
+
text-decoration: none;
+
}
+
}
+
+
&-content {
+
margin-top: 30px;
+
}
+
+
ul {
+
list-style: none;
+
+
li:before {
+
content: '⦿';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
ul {
+
+
li:before {
+
content: '■';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
+
ul {
+
+
li:before {
+
content: '►';
+
position: absolute;
+
left: -20px;
+
color: var(--accent);
+
}
+
}
+
}
+
}
+
}
+
+
// TODO: try adapting this using a properly nested selector in the block above
+
// for ul items.
+
.tag-list {
+
@media(max-width: $phone-max-width) {
+
margin-left: 5%;
+
}
+
}
+
+
.footnote-definition {
+
color: var(--accent);
+
+
p {
+
display: inline;
+
color: var(--footnote-color);
+
}
+
}
+3
themes/zerm/sass/_semantic.scss
···
···
+
section, article, aside, footer, header, nav {
+
display: block;
+
}
+6
themes/zerm/sass/_toc.scss
···
···
+
div.toc {
+
--shadow-color: var(--accent-alpha-70);
+
--shadow: 10px 10px var(--shadow-color);
+
@include menu;
+
margin: 20px 0;
+
}
+10
themes/zerm/sass/_variables.scss
···
···
+
:root {
+
// *NOTE*:
+
// ------------------------------------------------
+
//Keep the same as the values in variables.scss!!!!!
+
--phoneWidth: (max-width: 684px);
+
--tabletWidth: (max-width: 900px);
+
}
+
+
$phone-max-width: 683px;
+
$tablet-max-width: 899px;
+9
themes/zerm/sass/color/blue.scss
···
···
+
:root {
+
--accent: rgb(35,176,255);
+
--accent-alpha-20: rgba(35,176,255,.2);
+
--accent-alpha-70: rgba(35,176,255,.7);
+
--background: #1D1E28;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+12
themes/zerm/sass/color/green.scss
···
···
+
:root {
+
// --accent: rgb(120,226,160);
+
// --accent-alpha-20: rgba(120,226,160,.2);
+
// --accent-alpha-70: rgba(120,226,160,.7);
+
--accent: rgb(72, 251, 53);
+
--accent-alpha-20: rgba(72, 251, 53,.2);
+
--accent-alpha-70: rgba(72, 251, 53,.7);
+
--background: #1F222A;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/orange.scss
···
···
+
:root {
+
--accent: rgb(255,168,106);
+
--accent-alpha-20: rgba(255, 168, 106, .2);
+
--accent-alpha-70: rgba(255, 168, 106,.7);
+
--background: #211f1a;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/pink.scss
···
···
+
:root {
+
--accent: rgb(238,114,241);
+
--accent-alpha-20: rgba(238,114,241,.2);
+
--accent-alpha-70: rgba(238,114,241,.7);
+
--background: #21202C;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+9
themes/zerm/sass/color/red.scss
···
···
+
:root {
+
--accent: rgb(255,98,102);
+
--accent-alpha-20: rgba(255,98,102,.2);
+
--accent-alpha-70: rgba(255,98,102,.7);
+
--background: #221F29;
+
--color: whitesmoke;
+
--border-color: rgba(255, 255, 255, .1);
+
--footnote-color: rgba(255, 255, 255, .5);
+
}
+10
themes/zerm/sass/style.scss
···
···
+
@import 'buttons';
+
@import 'font';
+
@import 'header';
+
@import 'logo';
+
@import 'main';
+
@import 'post';
+
@import 'pagination';
+
@import 'footer';
+
@import 'semantic';
+
@import 'toc';
themes/zerm/screenshot.png

This is a binary file and will not be displayed.

+1
themes/zerm/static/assets/js/main.js
···
···
+
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){n(1),e.exports=n(2)},function(e,t){},function(e,t){var n=document.querySelector(".container"),o=document.querySelector(".menu"),r=document.querySelector(".menu-trigger"),i=(document.querySelector(".menu__inner--desktop"),document.querySelector(".menu__sub-inner-more-trigger")),u=document.querySelector(".menu__sub-inner-more"),c=getComputedStyle(document.body).getPropertyValue("--phoneWidth"),d=function(){return window.matchMedia(c).matches},s=function(){r&&r.classList.toggle("hidden",!d()),o&&o.classList.toggle("hidden",d()),u&&u.classList.toggle("hidden",!d())};o&&o.addEventListener("click",function(e){return e.stopPropagation()}),u&&u.addEventListener("click",function(e){return e.stopPropagation()}),s(),document.body.addEventListener("click",function(){d()||!u||u.classList.contains("hidden")?d()&&!o.classList.contains("hidden")&&o.classList.add("hidden"):u.classList.add("hidden")}),window.addEventListener("resize",s),r&&r.addEventListener("click",function(e){e.stopPropagation(),o&&o.classList.toggle("hidden")}),i&&i.addEventListener("click",function(e){e.stopPropagation(),u&&u.classList.toggle("hidden"),u.getBoundingClientRect().right>n.getBoundingClientRect().right&&(u.style.left="auto",u.style.right=0)})}]);
+1
themes/zerm/templates/.gitignore
···
···
+
!tags
+12
themes/zerm/templates/404.html
···
···
+
{% extends "index.html" -%}
+
+
{%- block title %}
+
<title>Page not found</title>
+
{%- endblock title -%}
+
+
{%- block main -%}
+
+
Sorry, this page doesn't exist.
+
+
Go back&nbsp<a href="{%- if config.extra.logo_home_link -%}{{ config.extra.logo_home_link }}{%- else -%}{{ config.base_url }}{%- endif -%}">home?</a>
+
{%- endblock main -%}
+1
themes/zerm/templates/anchor-link.html
···
···
+
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">§</a>
+18
themes/zerm/templates/categories/list.html
···
···
+
{% extends "index.html" %}
+
{%- block main -%}
+
<div class="post">
+
+
<h1 class="post-title">{categories}</h1>
+
<ul>
+
{%- for cat in terms -%}
+
<li class="tag-list">
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat.name) }}">{{ "{" }}{{ cat.name }}{{ "}" }}</a>
+
({{ cat.pages | length }} post{{ cat.pages | length | pluralize }})
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main -%}
+30
themes/zerm/templates/categories/single.html
···
···
+
{%- extends "index.html"-%}
+
{%- block main-%}
+
<div class="post">
+
<h1 class="post-title">
+
categories ∋ {{ "{" }}{{ term.name }}{{ "}" }}
+
({{ term.pages | length }} post{{ term.pages | length | pluralize }})
+
</h1>
+
+
<ul>
+
{%- for post in term.pages -%}
+
<li class="tag-list">
+
{{ post.date | date(format="%Y.%m.%d") }}
+
:: <a href="{{ post.permalink }}">{{ post.title }}</a>
+
:: {{ "{" }}
+
{%- for cat in post.taxonomies["categories"] -%}
+
{%- set _cat = get_taxonomy_url(kind="categories", name=cat) -%}
+
{%- if loop.last -%}
+
<a href="{{ _cat }}">{{ cat }}</a>
+
{%- else -%}
+
<a href="{{ _cat }}">{{ cat }}</a>,&nbsp;
+
{%- endif -%}
+
{% endfor %}{{ "}" }}
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main-%}
+97
themes/zerm/templates/index.html
···
···
+
{% import "macros/head.html" as head -%}
+
{% import "macros/logo.html" as logo -%}
+
{% import "macros/header.html" as header -%}
+
{% import "macros/extended_header.html" as extended_header -%}
+
{% import "macros/lists.html" as lists -%}
+
{% import "macros/posts.html" as posts -%}
+
{% import "macros/social.html" as social -%}
+
{% import "macros/utils.html" as utils -%}
+
{% import "macros/menu.html" as menu -%}
+
{% import "macros/pagination.html" as pagination -%}
+
{% import "macros/footer.html" as footer -%}
+
{% import "macros/extended_footer.html" as extended_footer -%}
+
{% import "macros/comments.html" as comments -%}
+
+
<!DOCTYPE html>
+
<html lang="{{ lang }}">
+
<head>
+
{%- block title -%}
+
<title>{{ config.title }}</title>
+
{%- endblock title -%}
+
+
{%- block general_meta -%}
+
{{ head::general_meta() }}
+
{%- endblock general_meta -%}
+
+
{%- block og_preview -%}
+
{{ social::og_preview() }}
+
{%- endblock og_preview -%}
+
+
{%- block fonts -%}
+
{{ head::fonts() }}
+
{%- endblock fonts -%}
+
+
{%- block css -%}
+
{{ head::styling() }}
+
{%- endblock css -%}
+
+
{%- block favicon -%}
+
{{ head::favicon() }}
+
{%- endblock favicon -%}
+
+
{%- block rss -%}
+
{{ head::rss() }}
+
{%- endblock rss -%}
+
+
{%- block math -%}
+
{%- endblock math -%}
+
</head>
+
<body>
+
{#
+
"container full" when width == True, regardless of center
+
"container center" when only center == True
+
"container" when both false.
+
#}
+
{%- if config.extra.full_width -%}
+
{%- set container = "container full" -%}
+
{%- elif config.extra.center -%}
+
{%- set container = "container center" -%}
+
{%- else -%}
+
{%- set container = "container" -%}
+
{%- endif -%}
+
+
<div class="{{ container }}">
+
{%- block header -%}
+
{{ header::header() }}
+
{%- endblock header -%}
+
<div class="content">
+
{%- block main -%}
+
{{ lists::list_pages() }}
+
{%- endblock main -%}
+
</div>
+
{#-
+
I keep pagination out of list, unlike Terminal, because I don't
+
like how the pagination buttons move with the width of the content
+
div.
+
-#}
+
{%- block pagination -%}
+
{{ pagination::paginate() }}
+
{%- endblock pagination -%}
+
+
{%- block footer -%}
+
<footer class="footer">
+
<div class="footer__inner">
+
{%- block copyright -%}
+
{{ footer::copyright() }}
+
{%- endblock copyright -%}
+
+
{%- block script -%}
+
{{ footer::script() }}
+
{%- endblock script -%}
+
</div>
+
{{ extended_footer::extended_footer() }}
+
</footer>
+
{%- endblock footer -%}
+
</div>
+
</body>
+
</html>
+30
themes/zerm/templates/macros/comments.html
···
···
+
{% macro comments() %}
+
{% if config.extra.disqus.enabled is defined and config.extra.disqus.enabled == true %}
+
{{ comments::disqus() }}
+
{% endif %}
+
{% endmacro comments %}
+
+
+
{% macro disqus() %}
+
+
<div id="disqus_thread"></div>
+
<script>
+
/**
+
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
+
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables */
+
+
var disqus_config = function () {
+
this.page.url = '{{ page.permalink | safe }}'; // Replace PAGE_URL with your page's canonical URL variable
+
this.page.identifier = '{{ page.permalink | safe }}'; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
+
};
+
+
(function() { // DON'T EDIT BELOW THIS LINE
+
var d = document, s = d.createElement('script');
+
s.src = 'https://{{config.extra.disqus.short_name}}.disqus.com/embed.js';
+
s.setAttribute('data-timestamp', +new Date());
+
(d.head || d.body).appendChild(s);
+
})();
+
</script>
+
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+
+
{% endmacro disqus %}
+2
themes/zerm/templates/macros/extended_footer.html
···
···
+
{% macro extended_footer() %}
+
{% endmacro extended_footer %}
+2
themes/zerm/templates/macros/extended_header.html
···
···
+
{% macro extended_header() %}
+
{% endmacro extended_header %}
+14
themes/zerm/templates/macros/footer.html
···
···
+
{% macro copyright() %}
+
{%- if config.extra.copyright -%}
+
<div class="copyright copyright--user">{{ config.extra.copyright | safe }}</div>
+
{%- else -%}
+
<div class="copyright">
+
<span>© {{ now() | date(format="%Y") }} <a href="https://github.com/ejmg/zerm">zerm</a> :: Powered by <a href="https://www.getzola.org/">Zola</a></span>
+
<span>:: Theme made by <a href="https://github.com/ejmg">ejmg</a></span>
+
</div>
+
{%- endif -%}
+
{% endmacro copyright %}
+
+
{% macro script() %}
+
<script type="text/javascript" src="{{ get_url(path="assets/js/main.js") }}"></script>
+
{% endmacro script %}
+84
themes/zerm/templates/macros/head.html
···
···
+
{#- TODO:
+
- [x] add rss
+
- [x] favicons
+
- [x] media preview
+
- [ ] twitter
+
- [ ] maybe google adsense
+
-#}
+
+
{% macro fonts() %}
+
<link rel="preload" href="{{ get_url(path="/assets/fonts/FiraCode-Regular.woff2") }}" as="font" type="font/woff2" crossorigin="anonymous">
+
<link rel="preload" href="{{ get_url(path="/assets/fonts/FiraCode-Bold.woff2") }}" as="font" type="font/woff2" crossorigin="anonymous">
+
{% endmacro fonts %}
+
+
{% macro styling() %}
+
<link rel="stylesheet" href="{{ get_url(path="style.css", cachebust=true) }}">
+
{% if config.extra.theme_color != "orange" -%}
+
{% set color = "color/" ~ config.extra.theme_color ~ ".css" -%}
+
<link rel="stylesheet" href="{{ get_url(path=color, cachebust=true) }}">
+
{%- else -%}
+
<link rel="stylesheet" href=" {{ get_url(path="color/orange.css", cachebust=true) }}">
+
{% endif %}
+
{%- if config.extra.custom_css is defined -%}
+
<link rel="stylesheet" href="{{ get_url(path="custom.css", cachebust=true) }}">
+
{% endif %}
+
{% endmacro styling %}
+
+
{% macro favicon() %}
+
{%- if config.extra.favicon is defined -%}
+
<link rel="shortcut icon" href="{{ get_url(path=config.extra.favicon) }}" type="image/x-icon" />
+
{%- endif -%}
+
{% endmacro favicon %}
+
+
{% macro rss() %}
+
{%- if config.generate_feed -%}
+
<link rel="alternate" type={% if config.feed_filename == "atom.xml" %}"application/atom+xml"{% else %}"application/rss+xml"{% endif %} title="{{ config.title }} RSS" href="{{ get_url(path=config.feed_filename) }}">
+
{%- endif -%}
+
{% endmacro rss %}
+
+
{% macro general_meta() %}
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
+
{%- if page.title -%}
+
<meta name="description" content="{{ config.description }} {{ page.title }} {{ page.description }}"/>
+
{%- else -%}
+
<meta name="description" content="{{ config.description }}"/>
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags or page.taxonomies.categories -%}
+
<meta name="keywords" content="
+
{%- if page.taxonomies.categories -%}
+
{%- for cat in page.taxonomies.categories -%}
+
{{ cat }}, {% endfor -%}
+
{%- endif -%}
+
+
{%- if page.taxonomies.tags -%}
+
{%- for tag in page.taxonomies.tags -%}
+
{%- if loop.last -%}
+
{{ tag }}
+
{%- else -%}
+
{{ tag }}, {% endif -%}
+
{%- endfor -%}
+
{%- endif -%}
+
" />
+
{%- endif -%}
+
{% endmacro general_meta %}
+
+
{% macro katex() %}
+
{% if config.extra.enable_katex %}
+
<link rel="stylesheet" href="{{ get_url(path="assets/katex/katex.min.css") }}">
+
+
<script defer type="text/javascript" src="{{ get_url(path="assets/katex/katex.min.js") | safe }}"></script>
+
<script defer type="text/javascript" src="{{ get_url(path="assets/katex/mathtex-script-type.min.js") | safe }}"></script>
+
+
<script defer src="{{ get_url(path="assets/katex/auto-render.min.js") | safe }}"
+
onload="renderMathInElement(document.body, {
+
delimiters: [
+
{left: '$$', right: '$$', display: true},
+
{left: '$', right: '$', display: false},
+
{left: '\\(', right: '\\)', display: false},
+
{left: '\\[', right: '\\]', display: true}
+
]
+
});"></script>
+
{% endif %}
+
{% endmacro katek %}
+16
themes/zerm/templates/macros/header.html
···
···
+
{% macro header() %}
+
<header class="header">
+
<div class="header__inner">
+
<div class="header__logo">
+
{{ logo::logo() }}
+
</div>
+
<div class="menu-trigger">menu</div>
+
</div>
+
{# Check if there are menu items to render, yes if > 0 #}
+
{%- set num = config.extra.main_menu | length -%}
+
{% if num > 0 -%}
+
{{ menu::menu() }}
+
{% endif -%}
+
</header>
+
{% endmacro header %}
+
+39
themes/zerm/templates/macros/lists.html
···
···
+
{% macro list_pages() %}
+
<section class="posts">
+
+
{%- for page in paginator.pages -%}
+
+
<div class="post on-list">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ page.permalink }}">{{ page.title }}</a>
+
</h1>
+
</header>
+
+
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
+
{#- NOTE -#}
+
{#- -------------------------------- -#}
+
{#- Skipping the Cover page implementation. Not included/covered for now. -#}
+
+
<div class="post-content">
+
{% if page.description -%}
+
{{ page.description }}
+
{#- end if-check for description -#}
+
{% elif page.summary -%}
+
{{ page.summary | safe }}
+
{% endif -%}
+
</div>
+
{% if page.description or page.summary -%}
+
<div>
+
<a class="read-more button" href="{{ page.permalink }}">{{ config.extra.read_more }} →</a>
+
</div>
+
{% endif -%}
+
</div>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</section>
+
{% endmacro list_pages %}
+7
themes/zerm/templates/macros/logo.html
···
···
+
{%- macro logo() -%}
+
<a href="{%- if config.extra.logo_home_link -%}{{ config.extra.logo_home_link }}{%- else -%}{{ config.base_url }}{%- endif -%}">
+
<div class="logo">
+
{{config.extra.logo_text | default(value="Terminal") }}
+
</div>
+
</a>
+
{%- endmacro logo -%}
+58
themes/zerm/templates/macros/menu.html
···
···
+
{% macro menu() %}
+
<nav class="menu">
+
<ul class="menu__inner menu__inner--desktop">
+
{#
+
I am almost certain this check isn't necessary, that it will always
+
return true, but the only alternative I can think of is the case where
+
this config value simply doesn't exist (null)? IDK, just copying from
+
original implementation for now.
+
#}
+
{% if config.extra.show_menu_items is defined -%}
+
{% for menu_item in config.extra.main_menu | slice(end=config.extra.show_menu_items) -%}
+
{#
+
Original theme has sub-children checks on main-menu, not worrying about that here.
+
#}
+
<li>
+
<a href="{{ menu::get_link(item=menu_item) }}">{{ menu_item.name }}</a>
+
</li>
+
{% endfor -%}
+
+
{%- set main_len = config.extra.main_menu | length -%}
+
{%- set show_len = config.extra.show_menu_items -%}
+
+
{%- if main_len > show_len -%}
+
<ul class="menu__sub-inner">
+
<li class="menu__sub-inner-more-trigger">{{ config.extra.menu_more }} ▾</li>
+
<ul class="menu__sub-inner-more hidden">
+
{{ menu::items(menu=config.extra.main_menu | slice(start=config.extra.show_menu_items)) }}
+
</ul>
+
</ul>
+
{%- endif -%}
+
{# Continues the original if-check at top of file for show_menu_items #}
+
{% else -%}
+
{{ menu::items(menu=config.extra.main_menu) }}
+
{%- endif -%}
+
</ul>
+
+
<ul class="menu__inner menu__inner--mobile">
+
{{ menu::items(menu=config.extra.main_menu) }}
+
</ul>
+
</nav>
+
{% endmacro menu %}
+
+
{% macro items(menu) %}
+
{%- for menu_item in menu -%}
+
{# skipping sub-child check #}
+
<li>
+
<a href="{{ menu::get_link(item=menu_item) }}">{{ menu_item.name }}</a>
+
</li>
+
{%- endfor-%}
+
{% endmacro items %}
+
+
{% macro get_link(item) %}
+
{% if item.external is defined and item.external == true %}
+
{{ item.url }}
+
{% else %}
+
{{ get_url(path=item.url) }}
+
{% endif %}
+
{% endmacro get_link %}
+24
themes/zerm/templates/macros/pagination.html
···
···
+
{% macro paginate() %}
+
<div class="pagination">
+
<div class="pagination__buttons">
+
{% if paginator.previous -%}
+
<span class="button previous">
+
<a href="{{ paginator.previous }}">
+
<span class="button__icon">←</span>
+
<span class="button__text">newer</span>
+
</a>
+
</span>
+
{# end of if-check for page.previous #}
+
{% endif -%}
+
{% if paginator.next -%}
+
{# end of if-check for page.next #}
+
<span class="button next">
+
<a href="{{ paginator.next }}">
+
<span class="button__text">older</span>
+
<span class="button__icon">→</span>
+
</a>
+
</span>
+
{% endif-%}
+
</div>
+
</div>
+
{% endmacro paginate %}
+100
themes/zerm/templates/macros/posts.html
···
···
+
{% macro section_meta(section, author) %}
+
<div class="post-meta">
+
<span class="post-date">
+
+
{%- if section.extra["date"] -%}
+
{{ section.extra["date"] | date(format="%Y.%m.%d") }}
+
{# end of section.date if-check #}
+
{%- endif -%}
+
</span>
+
+
<span class="post-author">
+
{%- if author -%}
+
{{ utils::author(page=section) }}
+
{%- endif -%}
+
</span>
+
</div>
+
{% endmacro section_meta %}
+
+
{% macro langs(page) %}
+
{% if page.translations | length > 1 %}
+
<div class="post-langs" style="opacity: .5;">
+
<span>Translations: </span>{# TODO translate the span content too #}
+
{% for translated in page.translations %}
+
<a href="{{ translated.permalink }}">{{ translated.lang }}</a>
+
{% endfor %}
+
</div>
+
{% endif %}
+
{% endmacro langs %}
+
+
{% macro meta(page, author) %}
+
<div class="post-meta">
+
<span class="post-date">
+
{%- if page.date -%}
+
{{ page.date | date(format="%Y.%m.%d") }}
+
{# end of page.date if-check #}
+
{%- endif -%}
+
+
{%- if page.updated -%}
+
[Updated: {{ page.updated | date(format="%Y.%m.%d") }}]
+
{# end of page.updated if-check #}
+
{%- endif -%}
+
</span>
+
+
<span class="post-author">
+
{%- if author -%}
+
{{ utils::author(page=page) }}
+
{%- endif -%}
+
</span>
+
+
{{ posts::taxonomies(taxonomy=page.taxonomies,
+
disp_cat=config.extra.show_categories,
+
disp_tag=config.extra.show_tags) }}
+
</div>
+
{% endmacro meta %}
+
+
{% macro taxonomies(taxonomy, disp_cat, disp_tag) %}
+
+
{% if disp_cat and disp_tag -%}
+
{% if taxonomy.categories -%}
+
{{ posts::categories(categories=taxonomy.categories) }}
+
{# end if-check for categories #}
+
{%- endif -%}
+
+
{% if taxonomy.tags -%}
+
{{ posts::tags(tags=taxonomy.tags) }}
+
{# end if-check for tags #}
+
{% endif -%}
+
{% elif disp_cat -%}
+
{% if taxonomy.categories-%}
+
{{ posts::categories(categories=taxonomy.categories) }}
+
{# end if-check for categories #}
+
{% endif -%}
+
{% elif disp_tag -%}
+
{% if taxonomy.tags -%}
+
{{ posts::tags(tags=taxonomy.tags) }}
+
{# end if-check for tags #}
+
{% endif -%}
+
{# end if-check for BOTH disp bools #}
+
{% endif -%}
+
{% endmacro taxonomies %}
+
+
{% macro categories(categories) %}
+
:: {
+
{%- for cat in categories -%}
+
{%- if loop.last -%}
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>
+
{%- else -%}
+
<a href="{{ get_taxonomy_url(kind="categories", name=cat ) }}">{{ cat }}</a>,
+
{# end of if-check for whether last item or not #}
+
{%- endif -%}
+
{%- endfor -%}} {# <--- NOTE: OPEN CURLY BRACE #}
+
{% endmacro categories %}
+
+
{% macro tags(tags) %}
+
::
+
{% for tag in tags -%}
+
#<a href="{{get_taxonomy_url(kind="tags", name=tag )}}">{{ tag }}</a>
+
{# end of tags for-loop #}
+
{% endfor -%}
+
{% endmacro tags %}
+46
themes/zerm/templates/macros/social.html
···
···
+
{% macro og_preview() %}
+
<meta property="og:title" content="{{ social::og_title() }}" />
+
<meta property="og:type" content="website"/>
+
{%- if current_url -%}
+
<meta property="og:url" content="{{ current_url }}"/>
+
{%- endif -%}
+
<meta property="og:description" content="{{ social::og_description() }}"/>
+
{%- if config.extra.og_preview_img -%}
+
<meta property="og:image" content="{{ get_url(path=config.extra.og_preview_img) }}"/>
+
{%- endif -%}
+
{% endmacro og_preview %}
+
+
{% macro og_description() %}
+
{%- if section -%}
+
{%- if section.description -%}
+
{{ section.description }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- elif page -%}
+
{%- if page.summary | string -%}
+
{{ page.summary | striptags | truncate(length=200) }}
+
{%- elif page.description -%}
+
{{ page.description }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- endif -%}
+
{% endmacro og_description %}
+
+
{% macro og_title() -%}
+
{{ config.title }} -&nbsp;
+
{%- if section -%}
+
{%- if section.title -%}
+
{{ section.title | striptags }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- elif page -%}
+
{%- if page.title -%}
+
{{ page.title | striptags }}
+
{%- else -%}
+
{{ config.description }}
+
{%- endif -%}
+
{%- endif -%}
+
{% endmacro og_title %}
+61
themes/zerm/templates/macros/toc.html
···
···
+
{% macro toc (t) %}
+
{% if t %}
+
<div class="toc" id="nav-container">
+
<p class="toc-head">Table of Contents</p>
+
<div id="nav-content" >
+
<ul>
+
{% for h1 in page.toc %}
+
<li>
+
<a href="{{ h1.permalink | safe }}">{{ h1.title }}</a>
+
{% if h1.children %}
+
<ul>
+
{% for h2 in h1.children %}
+
<li>
+
<a href="{{ h2.permalink | safe }}">{{ h2.title }}</a>
+
</li>
+
{% if h2.children %}
+
<ul>
+
{% for h3 in h2.children %}
+
<li>
+
<a href="{{ h3.permalink | safe }}">{{ h3.title }}</a>
+
{% if h3.children %}
+
<ul>
+
{% for h4 in h3.children %}
+
<li>
+
<a href="{{ h4.permalink | safe }}">{{ h4.title }}</a>
+
</li>
+
{% if h4.children %}
+
<ul>
+
{% for h5 in h4.children %}
+
<li>
+
<a href="{{ h5.permalink | safe }}">{{ h5.title }}</a>
+
{% if h5.children %}
+
<ul>
+
{% for h6 in h5.children %}
+
<li>
+
<a href="{{ h5.permalink | safe }}">{{ h6.title }}</a>
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
{% endif %}
+
{% endfor %}
+
</ul>
+
{% endif %}
+
</li>
+
{% endfor %}
+
</ul>
+
</div>
+
</div>
+
{% endif %}
+
{% endmacro %}
+8
themes/zerm/templates/macros/utils.html
···
···
+
{% macro author(page) %}
+
::
+
{% if page.extra.author -%}
+
{{ page.extra.author }}
+
{%- else -%}
+
{{ config.extra.author }}
+
{%- endif -%}
+
{%- endmacro author-%}
+41
themes/zerm/templates/page.html
···
···
+
{% import "macros/head.html" as head -%}
+
{% import "macros/toc.html" as toc -%}
+
{% extends "index.html" -%}
+
+
{%- block math -%}
+
{% if page.extra.math %}
+
{{ head::katex() }}
+
{% endif %}
+
{%- endblock math -%}
+
+
{%- block title %}
+
<title>{{ page.title }} - {{ config.extra.author }}</title>
+
{# TODO: make some kind of social media linking, i guess? #}
+
{#%- include "snippets/social.html" %#}
+
{%- endblock title -%}
+
+
{%- block main -%}
+
<article class="post">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ page.permalink }}">{{ page.title }}</a>
+
</h1>
+
{{ posts::meta(page=page, author=config.extra.show_author) }}
+
{{ posts::langs(page=page) }}
+
+
{%- block ToC -%}
+
{%- if page.extra.ToC or config.extra.ToC -%}
+
{{ toc::toc(t=page.toc) }}
+
{%- endif -%}
+
{%- endblock ToC -%}
+
</header>
+
+
{#- Skipping logic for cover as was in original Terminal theme -#}
+
+
{{ page.content | safe }}
+
+
{{ comments::comments() }}
+
{# TODO: Decide if any sort of commenting functionality is desired? #}
+
{#%- include "snippets/comments.html" -%#}
+
</article>
+
{%- endblock main -%}
+24
themes/zerm/templates/section.html
···
···
+
{% extends "index.html" -%}
+
+
{%- block title %}
+
<title>{{ section.title }} - {{ config.extra.author }}</title>
+
{# TODO: make some kind of social media linking, i guess? #}
+
{#%- include "snippets/social.html" %#}
+
{%- endblock title -%}
+
+
{%- block main -%}
+
<article class="post">
+
<header>
+
<h1 class="post-title">
+
<a href="{{ section.permalink }}">{{ section.title }}</a>
+
</h1>
+
{{ posts::section_meta(section=section, author=config.extra.show_author) }}
+
</header>
+
+
{#- Skipping logic for cover as was in original Terminal theme -#}
+
+
{{ section.content | safe }}
+
{# TODO: Decide if any sort of commenting functionality is desired? #}
+
{#%- include "snippets/comments.html" -%#}
+
</article>
+
{%- endblock main -%}
+12
themes/zerm/templates/static.html
···
···
+
{% extends "index.html" %}
+
+
{% block header %}
+
<title>{{ page.title }} | elias </title>
+
{% endblock header %}
+
+
{% block content %}
+
<article>
+
<h1>{{ page.title }}</h1>
+
{{ page.content | safe }}
+
</article>
+
{% endblock content %}
+18
themes/zerm/templates/tags/list.html
···
···
+
{% extends "index.html" %}
+
{%- block main -%}
+
<div class="post">
+
+
<h1 class="post-title">#tags</h1>
+
<ul>
+
{%- for term in terms -%}
+
<li class="tag-list">
+
<a href="{{ get_taxonomy_url(kind="tags", name=term.name) }}">#{{ term.name }}</a>
+
({{ term.pages | length }} post{{ term.pages | length | pluralize}})
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
</div>
+
{%- endblock main -%}
+23
themes/zerm/templates/tags/single.html
···
···
+
{%- extends "index.html"-%}
+
{%- block main-%}
+
<div class="post">
+
<h1 class="post-title">
+
tags ∋ #{{ term.name }}
+
({{ term.pages | length }} post{{ term.pages | length | pluralize}})
+
</h1>
+
+
<ul>
+
{%- for post in term.pages -%}
+
<li class="tag-list">
+
{{ post.date | date(format="%Y.%m.%d") }}
+
:: <a href="{{ post.permalink }}">{{ post.title }}</a>
+
:: {% for tag in post.taxonomies["tags"] -%} <a href="{%- set _tag = get_taxonomy_url(kind="tags", name=tag) -%}{{ _tag }}">#{{ tag }}</a> {% endfor %}
+
</li>
+
{# End of pagination for-loop #}
+
{%- endfor -%}
+
{#- I don't put pagination here like Terminal does. I don't like how
+
the buttons move with the size of post entries in the content div. -#}
+
</ul>
+
+
</div>
+
{%- endblock main-%}
+26
themes/zerm/theme.toml
···
···
+
name = "zerm"
+
description = "A minimalistic and dark theme based on Radek Kozieł's theme for Hugo"
+
license = "MIT"
+
homepage = "https://github.com/ejmg/zerm"
+
# The minimum version of Zola required
+
min_version = "0.8.0"
+
# An optional live demo URL
+
demo = "https://zerm.ejmg.now.sh/"
+
+
# Any variable there can be overriden in the end user `config.toml`
+
# You don't need to prefix variables by the theme name but as this will
+
# be merged with user data, some kind of prefix or nesting is preferable
+
# Use snake_casing to be consistent with the rest of Zola
+
[extra]
+
+
# The theme author info: you!
+
[author]
+
name = "elias julian marko garcia"
+
homepage = "https://github.com/ejmg"
+
+
# If this is porting a theme from another static site engine, provide
+
# the info of the original author here
+
[original]
+
author = "Radek Kozieł"
+
homepage = "https://radoslawkoziel.pl/"
+
repo = "https://github.com/panr/hugo-theme-terminal"
themes/zerm/zerm-preview.png

This is a binary file and will not be displayed.