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 ·

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 .docname

Important: 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}
    .myrow

Custom 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 is None. 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-overwriting and 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

    C

There'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.