Quarkdown: Markdown That Computes — Functions, Variables, Scripting
Part 2 of the Quarkdown series: function calls, chaining with ::, variables, custom functions, conditionals and loops. How Markdown becomes a genuine little programming language.
by Jean Pierre Kolb ·
In part 1 I introduced Quarkdown as a Markdown superset with a single big idea: the function call. It sounded harmless — a dot, a few curly braces. But behind it lies what separates Quarkdown from every other Markdown dialect: real scripting. Variables, custom functions, conditionals, loops, mathematics — all directly in the document, all in Markdown notation. This is exactly what the "Turing-complete" promise means. In this part I get to the bottom of the mechanics.
The anatomy of a function call
A call starts with a dot; arguments go in curly braces. Arguments can be positional (their meaning follows from their order) or named (name:{value}) — the latter makes the call more readable:
.multiply {6} by:{3}Calls can be nested by using one as the argument of another:
.multiply {.pow {3} to:{2}} by:{.pi}That doesn't stay readable for long. Which is why Quarkdown has its most elegant detail — chaining with ::. The value on the left becomes the first argument of the function on the right. The barely readable
.sum {.subtract {.pow {3} {2}} {1}} {2}turns into the line you actually read like mathematics:
.pow {3} {2}::subtract {1}::sum {2}The rule behind it is simple: .a::b becomes .b {.a}, .a::b::c becomes .c {.b {.a}}. You can append further arguments at any time — the chained value always stays the first one.
A second important concept is the block or body argument: the indented content under a call, corresponding to the last parameter. It can span multiple lines and itself contain functions:
.row alignment:{center}
This document was made by .docauthor
.column
Its name is .docnameImportant: the entire body shares the same indentation (at least two spaces or one tab). Indent one line by four spaces and you accidentally create a code block — one of the few pitfalls.
Variables
You define a variable with .var {name} {value} and access it like a parameter-less function:
.var {name} {Quarkdown}
Hello, **.name**!Reassigning works at any point — including based on the old value:
.var {number} {5}
.number {.number::sum {1}}Variables aren't limited to simple values. Because a body argument may be multi-line, a variable can store a whole layout block and reuse it as often as you like:
.var {myrow}
.row gap:{2cm}
A
B
C
.container background:{teal} padding:{1cm}
.myrowCustom functions
The real tool against repetition is .function. It takes a name and a body; the parameters go as param1 param2: in the first body line:
.function {greet}
to from:
Hello, .to from .from!
.greet {world} from:{John}Three properties are worth remembering:
-
Optional parameters. A
?on the parameter name makes it optional; when the argument is missing, its value isNone. With::otherwise {…}you emulate a default:.function {greet} to from?: Hello, .to from .from::otherwise {unnamed}! -
No
return. Quarkdown has no return statement — every reached instruction becomes part of the output. A function can return any Markdown content or, because the language is weakly typed, any value type:.function {area} width height: .multiply {.width} by:{.height} The area is **.area {4} {2}**. -
Overwriting. A function can be redeclared at any time; from then on the new definition applies. To prevent that, compile with
--forbid-function-overwritingand get an error on name collisions.
Conditionals
.if evaluates a boolean condition and outputs its body only then. The function propagates its content upward — so you can place it inside any expression, for instance to conditionally weave content into a layout:
.row gap:{1cm}
A
.if {.iseven {3}}
B
CThere's no else (yet) — but with .ifnot and .let you can rebuild it cleanly:
.let {.iseven {3}}
condition:
.if {.condition}
3 is even!
.ifnot {.condition}
3 is odd!Loops
.foreach iterates over any iterable value — a number range, a Markdown list, a variable. The trick: the function behaves like a map and returns a collection of the same size, so you can use it as an expression. .1 implicitly references the current item:
.row alignment:{spacearound}
.foreach {1..5}
n:
.multiply {.n} by:{.n}.repeat {n} is shorthand for .foreach {1..n}.
Mathematics, made readable
By the time you reach mathematics, chaining pays off. The area of a circle:
.var {radius} {8}
The area is
.pow {.radius} to:{2}::multiply {.pi}::truncate {2}.The same calculation nested would be a tangle of brackets — chained, it reads left to right like a natural formula. The full list of math functions is in the standard library.
FAQ
Isn't that too much programming for a document?
You decide how far you go. For a simple text you need none of it. Scripting only pays off where repetition or dynamics come into play — the same box twenty times, a generated table, a calculation inside running text. Then one function replaces the copy-paste.
Why :: instead of nested braces?
Both are allowed and equivalent — .a::b is exactly .b {.a}. Chaining exists purely for readability: it flips the reading direction from "inside out" to "left to right," the way you think of calculation steps anyway.
Are there data types?
Yes, but weakly typed: text, numbers, booleans, lists, dictionaries, ranges, colors, sizes and more. Values are converted as needed, and the type of iterated elements is preserved. I'll devote a dedicated section of the series to types later.
Further reading
The introduction and positioning are in part 1: When Markdown Learns Typesetting. In the next part I leave the logic layer and move to the visible result: document types, themes and layout — how the same source becomes a web page, paper, book or presentation. The full function reference is always in the official wiki and the standard library.