A Go library for the Learning Content Format (LCF) — an open, plain-text format for authoring learning content of any subject (languages, mathematics, chemistry, history, music, …) as Markdown files that are human-authorable, git-friendly, and machine-scorable. See the format specification at github.com/ortic/lcf.
lcf-go gives you everything needed to build a web viewer (or any tool) on top
of LCF documents:
| Package | Responsibility |
|---|---|
lcf |
Parse a .lcf.md file into typed front-matter + a body AST; extract the assessable units as typed interactions. |
lcf/render |
Render a document body to interactive, framework-agnostic HTML (prose via CommonMark, math preserved for KaTeX/MathJax). |
lcf/score |
Grade learner responses against the answer model, with pluggable domain checkers. |
lcf/catalog |
Index a directory tree of documents into a queryable, faceted catalog for browse/list pages. |
It is validated against the full ortic/knowledge corpus (100 exercises across 10 subjects in English and German).
go get github.com/ortic/lcf-goRequires Go 1.24+. Dependencies: goldmark (CommonMark) and gopkg.in/yaml.v3.
doc, err := lcf.ParseFile("exercises/en/mathematics/fractions.lcf.md")
if err != nil {
log.Fatal(err)
}
fmt.Println(doc.FrontMatter.Title) // "Fractions: Addition, …"
fmt.Println(doc.FrontMatter.Domain.First()) // "math"
for _, item := range doc.Items() {
fmt.Printf("%s is a %s worth %.0f points\n", item.ID, item.Primitive, item.Points)
}Document.Items() returns typed Item values with the answer
model populated per primitive — choice, text, cloze, order, match,
numeric, formula — resolving domain-pack aliases (e.g. the language pack's
[translate] → text, the chemistry pack's [reaction] → formula).
r := render.New(render.Options{Interactive: true})
html, _ := r.Document(doc)
// html now contains <div class="lcf-interaction lcf-choice" data-lcf-id="q1" …>The output is semantic HTML carrying data-lcf-* attributes and stable CSS
classes so your front-end can style it and wire up answering. Inline $…$ and
display $$…$$ math is passed through verbatim for client-side KaTeX/MathJax.
Set ShowAnswers: true for author previews and answer keys.
g := score.NewGrader()
result := g.Grade(doc, map[string]score.Response{
"n1": {Text: "0.875"},
"q1": {Selected: []string{"opt-dfrac-3-4"}},
"c1": {Blanks: map[string]string{"b1": "3", "b2": "4"}},
})
fmt.Printf("%.1f / %.1f\n", result.Awarded, result.Max)Simple items score with sane defaults (declared/exact match ⇒ full points).
Normalisation (case, whitespace, accents, punctuation), match modes
(exact/contains/regex), numeric tolerances (absolute or %), and partial
credit (partial="proportional") are all honoured. Domain grading is delegated
to named Checker hooks — a chem.balanced checker for
balanced chemical equations ships built in; register your own for e.g.
math.symbolic-equiv:
g.RegisterChecker("math.symbolic-equiv", func(item lcf.Item, response string) (correct, handled bool) {
// … call a CAS, return (equivalent, true) …
})cat, _ := catalog.LoadDir("./exercises")
fmt.Println(cat.Len()) // 100
fmt.Println(cat.Domains()) // [{math 10} {chemistry 10} …] — faceted counts
fmt.Println(cat.Locales()) // [{de 50} {en 50}]
for _, e := range cat.ByLocale("en") {
fmt.Println(e.Title, e.Subject, e.ItemCount)
}
doc, _ := cat.OpenID("mathematics.en.fractions") // parse the full body on demandcatalog indexes lightweight metadata per document (no bodies) so a large corpus
loads cheaply, and offers filtering (ByDomain, BySubject, ByLocale,
ByKind, ByTag, Filter) plus faceted navigation (Domains, Subjects,
Locales, Kinds, Tags).
- Graceful degradation (spec §8.4). Parsing never fails on malformed
shortcodes: an unknown or unbalanced block degrades to literal prose, and a
Markdown link like
[text](url)is never mistaken for a[text]interaction. Parsing only errors when the YAML front-matter cannot be decoded. - Stable identity (spec §6). Every interaction carries an
id; the pair(document.id, item.id)is the key you use for attempts, analytics, and spaced-repetition state. - Extensible. The
Registryclassifies block names and maps domain-pack blocks to core primitives; register new blocks without touching the parser. - Round-trippable. Every node retains its original source (
Node.Raw()).
A complete, server-rendered web viewer built on this library — faceted browsing
by language and subject, interactive exercise pages, and answer grading — lives
in the ortic/knowledge repository under
web/.
Targets LCF v0.1. Supports conformance levels L1 (structure/rendering), L2 (assessment/scoring), and L3 (domain packs) via the registry and checker extension points.
MIT © 2026 Ortic Solutions GmbH. See LICENSE.