A family of focused F# libraries for building and testing Fable applications. Each library has a single responsibility and a name drawn from the same metaphor: the act of writing.
Scriptorium: a room in a monastery where manuscripts were copied and illuminated by hand.
The scribe sits down and fills the pot of Ink — colour and style, ready to give life to every stroke. Bold or dim, red or green, the Ink asks no questions. It simply waits to be used.
The scribe reaches for a sheet of Parchment and begins to write. The words appear structured, purposeful — a log of what happened, what was warned, what went wrong. Parchment dips into the Ink pot to illuminate its headings: a cyan [INFO], a yellow [WARN], a red [ERROR]. The record is clear. The page holds everything.
Then the scribe takes the Quill — and it is the Nib at its tip that does the real work. That sharp, precise point is where expectation meets reality. The Nib touches the Parchment and either the mark is true, or it is not. Every flaw is recorded without interruption; the hand does not stop mid-stroke. Only when the page is full does the Quill lift — and every deviation is reported at once.
The Quill itself is the instrument that holds all of this together. It carries the Nib, dips into the Ink, and lays the results onto the Parchment — a complete manuscript of your test run, illuminated in green for what passed, red for what failed, and dim grey for what was left unwritten.
Fable.Ink ──────────────────────────────────────────────── leaf
Fable.Parchment → Fable.Ink ──────────────────────────────────── leaf
Fable.Nib ──────────────────────────────────────────────── leaf
Fable.Quill → Fable.Ink, Fable.Parchment, Fable.Nib ─────── leaf
Fable.Ink is the ink itself — pure colour and style for terminal output.
It wraps ANSI escape codes into a small set of composable functions:
red, green, bold, dim, underline, strip, and so on.
No dependencies. Everything downstream reaches for Ink when it needs to colour output.
Fable.Parchment is the page that receives the ink — structured log output.
It exposes a single [<RequireQualifiedAccess>] module, Log, with functions
Log.log, Log.info, Log.warn, Log.error, Log.newLine, and Log.inline_.
Each prefixed variant (info, warn, error) uses Ink to colour its label.
Depends on: Fable.Ink.
Fable.Nib is the nib of the quill — the sharp, precise point that makes the mark.
It is a standalone fluent assertion library: write expectations as composable functions,
chain them with >>, and collect every failure before reporting.
No runner dependency. Can be used with any test harness or inline in application code.
Depends on: nothing.
Fable.Quill is the quill itself — the test runner that holds everything together.
It provides a DSL (testCase, testList, ftestCase, ptestList, …),
an executor that respects focused / pending state,
and a pretty-printer that renders results with colour, indentation, and OSC 8 terminal hyperlinks
pointing back to the exact source location of each failure.
Depends on: Fable.Ink, Fable.Parchment, Fable.Nib.
Fable.Ripple is the reactive UI framework.
A value changes; a ripple propagates outward through the adaptive graph;
the DOM updates — no diff, no virtual tree.
Built on FSharp.Data.Adaptive.
Value drops, UI wakes
No diff, no virtual tree
Just ripples outward
Fable.UrlParser is a composable URL parser. It supports path segments, query parameters (required, optional, flag, multi-value), fragments, and hash-based routing. No dependencies.
Fable.Ink
│
├── Fable.Parchment
│ │
│ └── Fable.Quill ◄── Fable.Nib
│
Fable.Nib
│
└── (used by Fable.Quill, and directly by test suites)
Fable.UrlParser (independent)
Fable.Ripple (independent)
| Test project | Tests | Runner |
|---|---|---|
tests/Fable.Quill.Test |
Runner behaviour (focus, pending, paths, exit codes) | dotnet fable … --runScript |
tests/Fable.Nib.Test |
All assertion combinators (primitives, collections, Option, Result, DU, tags, short-circuit) | dotnet fable … --runScript |
tests/Fable.UrlParser.Tests |
SimpleParser (segments, query, fragment, hash, error depth) | dotnet fable … --runScript |
tests/Fable.Ripple.Tests |
DOM rendering and routing | Vitest (browser) |
Run a suite:
dotnet fable tests/Fable.Quill.Test/ --runScript
dotnet fable tests/Fable.Nib.Test/ --runScript
dotnet fable tests/Fable.UrlParser/ --runScript// Bring assertion functions into scope
open Fable.Nib
// Bring test DSL into scope
open Fable.Quill.Runner
open type Fable.Quill.Runner.Test
[<EntryPoint>]
let main _ =
runTests [
testList "My suite" [
testCase("equality", fun () ->
assertThat 42 (isEqualTo 42)
)
testCase("option chain", fun () ->
assertThat (Some "hello") (Option.value >> isEqualTo "hello")
)
testCase("record fields with tags", fun () ->
assertThat { Name = "alice"; Age = 25 } (
inside _.Age (tag "age" >> isGreaterOrEqual 18)
>> inside _.Name (tag "name" >> isNotEqualTo "")
)
)
]
]Attributes are applied to DOM elements sequentially, in the order they appear in source. This matches browser behaviour but can produce surprises:
// 'valueVal' is set before 'step', so the browser clamps it to 1.
input (
type' = "range",
min = "0",
max = "1",
valueVal = AVal.const "0.5", // ← applied before step
step = "0.1",
onInput = ...
)Similarly for lazy image loading:
// 'src' fires the request before 'loading = "lazy"' is set.
img (
src = "https://example.com/image.png",
loading = "lazy",
)Attribute order is intentionally not reordered by the library. If order matters, place the controlling attribute before the dependent one.