Architecting a Conversion Engine in Swift
Minimal now supports importing and exporting across Markdown, Rich Text, HTML, PDF, plain text, and our proprietary format MNML. The following essay describes how we accomplished this system in the Swift programming language. To read about the human-centered design and how we fit this new technology into our iOS and macOS app, read the design-centric essay right here.
It’s not easy to coerce one file format into another, and adding support for more and more file formats gets ever more complex. To manage this complexity, we built a cohesive system that relies on an Intermediate Representation to serve as a middleman across file formats. The Intermediate Representation (or “IR”) allows us to simply convert a given file format to and from the IR, instead of across every possible file format pair.<br>Intermediate Representation<br>If we were to convert notes directly from one format to another, we’d be caught in a complex web of conversions that only gets more complex as we add more file formats. To avoid this mess, we first built a system called Intermediate Representation.<br>Mess vs elegance. Without the IR, six file formats would produce 30 relationships (N^2 - N). With the IR, six file formats produces 12 distinct relationships (N * 2).The IR sits in the middle of all file formats. With this architecture, adding support for a new format simply requires building that format’s own bespoke converter, without any concern for other file formats. Complexity grows linearly as we expand support to new types of data.<br>Nature does this.<br>Biologists call this the bow-tie or hourglass architecture. In gist, a simplified intermediary stage allows the two sides of the interaction to be independently complex.<br>For example, cells consume an incredible array of molecules that they then digest (catabolize) into a smaller set of shared intermediaries. On the other side, biosynthesis (anabolism) then fans these intermediaries back into the complex array of molecules that the cell puts to use. If the cell had to map all of the input molecules to the required output molecules, it would need multiple times more internal processes to fully metabolize. The Intermediate Representation makes it simpler.<br>Interestingly, systems often get frozen when everything depends on the same intermediary. While the IR makes it easier for any side of the equation to evolve on its own, it makes it harder for the broader system to evolve: changing the definition of the IR requires everything that interacts with it to update to the new shape.<br>As an example of a "frozen" IR, consider the genetic code, often described as a "frozen accident." Like cellular metabolism, a triplet codon acts as an IR between DNA (genes) and amino acids (proteins). DNA (4 letters) compile down to a codon (the IR), and the codon is then compiled up into amino acids (20 structures). DNA and amino acids don't have direct, dependent interaction; the codon serves as the convenient intermediary that allows diverse life forms to interface. The triplet codon is universal across life, transmitting biological information across species. Now that it exists and every living thing relies on it, the emergence of a different IR is increasingly improbable.<br>(Some commentators describe the Internet Protocol as a frozen IR, resistant to change despite its flaws.)<br>Code<br>Below is our IR's document structure, written in Swift. We placed it in its own "IR" namespace to prevent naming collisions without creating a dedicated package (this code lives alongside the rest of our code).<br>/// Namespace for the Intermediate-Representation.<br>enum IR {<br>// Intentionally has no cases. Exists purely to scope the types below.
// MARK: - Document
extension IR {
/// A parsed note in dialect-neutral form.<br>struct Document: Equatable {
/// A sequence of `Block` (stacks vertically), each containing a sequence of `Inline` (stacks horizontally).<br>var blocks: [Block]
var resources: [String: Resource]
init(blocks: [Block] = [],<br>resources: [String: Resource] = [:]) {<br>...
// MARK: - Blocks
extension IR {
/// A unit of textual content that stacks vertically.<br>indirect enum Block: Equatable {<br>case blankLine<br>case paragraph([Inline])<br>case codeBlock(language: String?, content: String)<br>case heading(level: Int, inlines: [Inline])<br>case bulletList([ListItem])<br>case orderedList(items: [ListItem], start: Int)<br>case todoList([TodoItem])<br>case blockquote([Block])<br>case pullquote([Inline])<br>case horizontalRule<br>case embed(resourceId: String)
struct ListItem: Equatable {<br>var blocks: [Block]<br>init(blocks: [Block]) { self.blocks = blocks }
struct TodoItem: Equatable {<br>var checked: Bool<br>var blocks: [Block]<br>init(checked: Bool, blocks: [Block]) {<br>...
// MARK: - Inlines
extension IR {
/// Content that flows horizontally inside a block.<br>indirect enum Inline: Equatable {<br>case text(String)<br>case strong([Inline])<br>case emphasis([Inline])<br>case underline([Inline])<br>case link(url: String, inlines: [Inline])<br>case...