Converting Gemtext to HTML with Elixir

I spent a few hours this Saturday afternoon writing this little library, gemtext_to_html. I’d like to spend more time browsing and developing for Gemini space, and this is the first step towards making something Elixir-y to that end.

It ships with basic HTML components, but of course you can define your own components using regular old Phoenix Components and HEEx templates.

defmodule MyApp.MyComponents do
  @behaviour GemtextToHTML.Components
  import Phoenix.Component

  def h1(assigns) do
    ~H"""
    <h1 class="text-lg font-bold"><%= @text %></h1>
    """
  end

  # ...and so on...
end

gemtext = """
# Hello, world

* this is a list
* indeed

pretty neat
"""

GemtextToHTML.render_to_string(gemtext, components: MyApp.MyComponents)
# => "<h1 class="text-lg font-bold">Hello, world</h1>" <> ...

Making dbg/2 useful in IEx

The default behavior of dbg/2 when running an application via IEx (e.g. iex -S mix phx.server) is to halt execution and request to pry into the executing context.

However, in most cases I just want dbg/2 to behave a like a fancier IO.inspect/2.

This can be accomplished by adding this configuration to ~/.iex.exs:

# Change behavior of dbg/2 to stop prying when in IEx
Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []})

One year of Morsle

Morsle has been out for a year, so I thought it would be fun to peruse some stats.

Or, you can look at the stats yourself.

Overall

  • 91,120 unique visitors, or an average of 250 every day. That’s 91,119 more people than Samuel Morse.
  • 114,411 total visits. I guess 23,291 people didn’t get the memo that this is a daily challenge.
  • 372,097 total pageviews by the folks who really wanted to visit that Settings screen.
  • 27% bounce rate which I choose to attribute entirely to bots.

Daily challenge

  • 56% of visitors completed the daily challenge, which coincidentally equals my KDR in Halo Infinite.
  • 99.2% win rate which indicates that maybe I did make the game too easy.
  • 17% of players chose hard mode which, as established, was not clearly hard enough.
  • 46% of players won at 40 WPM, suggesting that perhaps the game’s easiness quotient was too large.

Practice mode

  • 22.6% of visitors played practice mode to get better at cheating on exams.
  • 36% of practice mode games were for callsigns, by people who should have been using a real tool like Learn CW Online.
  • 19% of practice mode games were in hard mode by Chad operators who have never touched a microphone in their lives.

Miscellaneous thoughts

I have received a very small number of support emails. Also, this project was my first time dipping my toe into browser-based testing with Cypress. Coincidence?

Boy, it is nice having a fully-static site hosted for free on a manged provider, with no server component to mange or upkeep. You can’t beat that peace of mind. I should do more fun little projects like this.

This completely failed as a viral marketing campaign for Remote Ham Radio.

I think originally I put 500 words in there? It’s probably time to revisit that and add some more for the upcoming year. And I need to find a better word list – I received a lot of complaints about that.

Dit dit!

A pessimist says the glass is 1/2 empty, an optimist says the glass is 1/2 full, Excel says the glass is January 2nd

mattreyu on Reddit

Avoid using show or hide in the names of variables that control visibility, because it’s hard to differentiate the state from the event that sets it.

“Show” is a verb. “Visible” is an adjective.

The show_modal action sets the modal_visible variable to true.

Your daily reminder that naming things is hard.

Rubber-duck-driven development

Today, I sat down to some code I wrote a week ago. I came across the assignment:

  loading?: true

“Huh,” I thought. “This is confusing. This isn’t really an indication that anything is actually loading. In fact, we show a fake loading state for 500 milliseconds, while we sleep and wait for the text input to debounce before actually loading anything. This variable just controls if the indeterminate spinner is visible. I should document that.”

+ # Controls if the spinner is visible
  loading?: true

“Wait a second…”

- # Controls if the spinner is visible
- loading?: true
+ spinner_visible?: true

And magically, through the power of rubberducking, it was made better.

As always, Jeff Atwood already nailed it.

Abusing Elixir: JS equality

Do you find the == operator too strict? Do you often dream of two values being equal even though they are completely different types, or of maps being equal to absolutely nothing?

Thankfully, Elixir lets us un-import Kernel.==/2 and define our own.

defmodule JSEquality do
  def a == b, do: equal?(a, b) or equal?(b, a)
  def a != b, do: not __MODULE__.==(a, b)

  def a === b, do: strictly_equal?(a, b) or strictly_equal?(b, a)
  def a !== b, do: not __MODULE__.===(a, b)

  defp equal?(1, x) when x in [true, 1], do: true
  defp equal?(0, x) when x in [false, 0], do: true
  defp equal?("1", x) when x in [true, 1, "1"], do: true
  defp equal?("0", x) when x in [false, 0, "0"], do: true
  defp equal?("", x) when x in [false, 0, ""], do: true
  defp equal?(:undefined, x) when x in [nil, :undefined], do: true
  defp equal?(:Infinity, :Infinity), do: true
  defp equal?(:negative_Infinity, :negative_Infinity), do: true
  defp equal?([], x) when x in [false, 0, ""], do: true
  defp equal?([[]], x) when x in [false, 0, ""], do: true
  defp equal?([0], x) when x in [false, 0, "0"], do: true
  defp equal?([1], x) when x in [true, 1, "1"], do: true
  defp equal?(:NaN, _), do: false

  defp equal?(int, x) when is_integer(int),
    do: Kernel.==(int, x) or Kernel.==(Integer.to_string(int), x)

  defp equal?(list, _) when is_list(list), do: false
  defp equal?(map, _) when is_map(map), do: false
  defp equal?(x, x), do: true
  defp equal?(_, _), do: false

  defp strictly_equal?(list, _) when is_list(list), do: false
  defp strictly_equal?(map, _) when is_map(map), do: false
  defp strictly_equal?(:NaN, _), do: false
  defp strictly_equal?(x, x), do: true
  defp strictly_equal?(_, _), do: false
end

And, in use:

iex> import Kernel, except: [==: 2, !=: 2, ===: 2, !==: 2]
Kernel
iex> import JSEquality
JSEquality
iex> "hello" == "hello"
true
iex> "hello" == :world
false
iex> :undefined == nil
true
iex> "1" == [1]
true
iex> %{} == %{}
false
iex> false == "0"
true
iex> [] == []
false
iex> [[]] == false
true
iex> "" == 0      
true
iex> -123 == "-123"
true
iex> :NaN == :NaN
false
iex> "foo" === "foo"
true
iex> -1 === "-1"
false
iex> [] === []
false

Source

Abusing Elixir: classes

Is immutability dragging you down? Despite what the docs claim, Elixir isn’t a real functional language. We can mutate state whenever we want!

defmodule Car do
  defstruct [:gear, :rpm, :speed, :shift, :dealloc]

  # Constructor
  def new do
    state = %{
      gear: 1,
      rpm: 1000
    }

    {:ok, agent} = Agent.start(fn -> state end)

    %__MODULE__{
      # Properties
      gear: fn -> Agent.get(agent, & &1.gear) end,
      rpm: fn -> Agent.get(agent, & &1.rpm) end,
      # Read-only property
      speed: fn -> Agent.get(agent, &(&1.gear * &1.rpm)) end,
      # Method
      shift: fn diff ->
        Agent.update(agent, fn state ->
          Map.update!(state, :gear, &(&1 + diff))
        end)
      end,
      # Destructor
      dealloc: fn -> Agent.stop(agent) end
    }
  end
end

And, in “use”:

iex(1)> car = Car.new()
%Car{
  gear: #Function<3.34229322/0 in Car.new/0>,
  rpm: #Function<4.34229322/0 in Car.new/0>,
  shift: #Function<2.34229322/1 in Car.new/0>,
  speed: #Function<5.34229322/0 in Car.new/0>,
  dealloc: #Function<6.34229322/0 in Car.new/0>
}
iex(2)> car.gear.()
1
iex(3)> car.shift.(1)
:ok
iex(4)> car.gear.()
2
iex(5)> car.speed.()
2000
iex(6)> car.dealloc.()
:ok
iex(7)> car.gear.()
** (exit) exited in: GenServer.call(#PID<0.189.0>, {:get, #Function<9.34229322/1 in Car.new/0>}, 5000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
    (elixir 1.14.0) lib/gen_server.ex:1038: GenServer.call/3

The indomitable message type

When I first started building Remote Ham Radio in 2012, WebSockets were less than a year out of RFC proposal, but they were already the clear future for developing realtime experiences in the browser.

WebSockets are bidirectional streams of data, conveniently packetized into individual messages for ease of sending and receiving.

So, what’s the content of those messages? WebSockets leave that as an exercise for the developer.

First contact

On September 21, 2012 at 1:49am (!), the first commit was made to the RHR server repository. It contained the Ruby Message class.

class Message
  attr_accessor :id, :type, :info
end

Serialized to JSON, this simple data structure would go on to endure the lifetime of RHR, largely unchanged. It enabled the application to scale from a tech demo to a distributed pub-sub message broker that pushes millions of message per hour.

This message format is not a groundbreaking concept. In fact, I didn’t even come up with it. The original source is this Gist by Ismael Celis. The FancyWebSocket class survived in the client codebase for many years. And why not? It just worked.

Here’s the spec

Those three fields – id, type, and info – are the real MVP. In both senses.

{"id": "rotor123", "type": "move", "info": {"heading": 45}}

The id determines which server component the message is destined to (or from), the type indicates the shape of the message, and info is an arbitrary map/dict/hash/object payload whose contents are based on the message type.

Early on, each user client had a dedicated WebSocket connection to each remote station. As the service grew, this scheme did not scale, so a central server was established to forward messages between the clients and the servers. This necessitated two additional fields, user_id and site_id, so that messages could be routed through the central hub.

Five fields in a JSON object. That’s it – that’s the whole API.

Pros

Schemaless

There is no schema definition, but that’s manageable for a single-developer team. It enables rapid development of new message types across the two or three repos requried to plumb everything together.

It also makes gradual deployments easier, as clients do not have to agree on a common schema version. As a tradeoff, clients must ignore unexpected message types, and provide default values for new fields that might not be fully deployed yet.

Portable

It’s a portable format, in the sense that an individual message can be easily moved across several layers of the application. It works in Ruby, it works in JS, and it’s survived the transition between five different message brokers – first peer-to-peer WebSockets, then Ruby, then RabbitMQ, then Redis, and now Elixir.

Ingest-able

As a corollary, it’s extremely easy to get messages into the system. Some clients communicate via an HTTP API instead of a WebSocket, and they can take advantage of the same message format by POSTing a message to a single HTTP endpoint.

Room for improvement

A schema

A proper JSON schema would be nice, to prevent malformed messages from propagating too far into the system. Documentation would be a beneficial side-effect, as there is currently no single source of truth for the 100+ message types. This becomes much more valuable when Developer 2 has entered the chat.

Synchronous requests

Messages are fire-and-forget, and there is no concept of a transaction or an acknowledement that a particular message has been handled. That’s fine, as the async model is particularly well-suited for interacting with physical devices.

Hardware will emit unsolicited messages when its state changes, often it’s slow to respond to requests, and sometimes you trip over a cord and suddenly it doesn’t respond at all. Don’t forget, there’s a 4,800 baud serial device on the other end of that Gigabit connection.

In the end, it’s easier for a client to fire off a message and pray that it receives an expected response in the future.

The downside is that clients can resemble a plate of spaghetti, where the sending of a request is completely decoupled from the handling of the response. This is a pain for actions that are truly transactional in nature, so this protocol could be improved to optionally support an ACK/NACK scheme for those cases.

Binary format

JSON is not the lightest format, but it is human-readible! And as a human, I appreciate that.

The application has not reached a scale where bandwidth has become costly or otherwise a limiting factor in its growth, so JSON continues to reign. Phoenix already applies gzip compression to WebSockets, which is Good Enough. A binary message format could be considered in the future, trading off bandwidth for additional complexity and CPU.

URIs

It’s interesting to consider unifying the user_id, site_id, id, and type fields into a single uri field.

rhr://user_id@site_id/id/type

I’m not sure what advantage this would provide, but it looks neat.

Building the “right thing”

Growing codebases eventually meet a point where their initial assumptions are challenged. External pressures like cost, complexity, security, and features creep in over time – that’s a given. So when beginning a project, it’s tempting to try to build the “right thing” from the start. That’s the heart of the second-system effect.

However, the ability to completely reshape a product over time is one of the great affordances we have as developers. By recognizing that we don’t have complete knowledge of current and future requirements, usually the best option is to build the simplest solution.

Sometimes, it might just be fancy enough.

Text adventures with direnv

If you use direnv to configure your local development environment, you might run into a situation where one set of environment variables does not cut it.

For example, a front-end JS application might need to connect to number of different server environments, such as local development, staging, or production.

Here’s a little secret: .envrc is just a shell script. There’s nothing magic about it.

Armed with that knowledge, here’s an iteration of .envrc that prompts the user for the environment when switching into the project directory:

# .envrc
export COMMON_ENV=here

echo -n "Configure [d]evelopment, [s]taging, or [p]roduction? "
read

case $REPLY in 
  p)
    echo 'Configuring production environment'
    export BASE_URL=https://example.com
    ;;
  s)
    echo 'Configuring staging environment'
    export BASE_URL=https://staging.example.com
    ;;
  *)
    echo 'Configuring development environment'
    export BASE_URL=http://localhost:4000
    ;;
esac

Now, when you cd into the project directory:

❯ cd myapp
direnv: loading ~/myapp/.envrc
Configure [d]evelopment, [s]taging, or [p]roduction? d
Configuring development environment
direnv: export +COMMON_ENV +BASE_URL

Just don’t enter the wrong input, or you might be eaten by a grue.

Introducing Keypress

Three weeks ago, I shared a tidbit in a Discord channel, and someone commented that I should write a blog post about it.

But, I didn’t have a blog.

So I made this one.

I’ve done the static-site-generator thing before. It’s fine – rockwellschrock.com and ww1x.com are static sites. But I’m a web app developer, and I like developing web apps.

So, three weeks ago, I opened a new Obsidian document and hashed out a plan. Here’s the entirety of that document, with a few items crossed-out that didn’t make the final cut.

Requirements

  • Minimal dependency upkeep
  • Simple auth system
  • Swappable themes
  • Posts
    • Post types: short, long, link
    • Drafts
    • Slugs
  • Code blocks with syntax highlighting
  • Mobile-first design

Specifications

  • Phoenix 1.7
    • Controllers only, no LiveView
  • Postgres
  • Tailwind
  • esbuild
  • Markdown
  • HTTP basic auth

After working so closely with LiveView over the past two years, it’s just too good to pass over. Writing controllers and JS feels too clunky now, even (especially?) for simple use cases. And it made it even easier to add stretch features that I hadn’t originally planned out, like previewing posts before publishing.

Swappable themes are implemented, but I didn’t design any fun new styles yet. This feature was built into one of my original static home pages when I first started out on the Web, and it’s time to bring it back.

Slugs? We don’t need no stinking slugs! And how do you generate a slug for a Tweet-style post that doesn’t have a title, anyway? Integer IDs are fine, and will continue to be fine.

And yes, the backend is protected by HTTP basic authentication. If it ain’t broke…

Some Future Ideas

It would be great if this could automatically cross-post to Mastodon.

I’ve been really enamored with Gemini lately. Could this server double-up and serve Gemini content? The biggest unknown there is how to handle the graceful degradation from Markdown to Gemtext, whose formatting options are extremely stripped-down.

Share and Enjoy

You can find the source for Keypress on GitHub.