--- name: noteworthy description: Write and edit Noteworthy documents — a Typst framework for textbooks, with blocks (definition/theorem/example/solution), 2D and 3D canvases, geometry, plotting, and live Yjs collaboration. Use when editing .typ files in a Noteworthy project, drawing a figure with shape/graph/canvas, adding a block or cross-reference, changing templates/module, or deploying to a running Noteworthy server. Covers the one rule that is not guessable: a live document is a CRDT room and the file is only its export. --- # Noteworthy A Typst framework for educational documents. `templates/templater.typ` re-exports everything; a content file imports that one path and nothing else. ``` config/ hierarchy.json, metadata.json, constants.json, schemes/ content/ /.typ the document itself templates/ templater.typ, core/, module/ ``` Blocks, covers and layouts come in unqualified; everything else is a namespace: `canvas`, `shape`, `graph`, `data`, `combi`, `dsa`, `timeline`, `trees`. ```typst #import "../../templates/templater.typ": * ``` ## The room is the document; the file is its export A Noteworthy server holds each open file as a Yjs CRDT **room**. While a room is live it is authoritative, and it rewrites the file within a debounce of every edit. **So writing the file behind a live room's back does not edit the document.** The room's next save puts the old text back over what was written, and nobody connected ever sees the change. This is the single most expensive mistake to make here — it looks like it worked, and reverts a few seconds later. Edit through the room instead. When the `noteworthy` MCP server is connected: | tool | for | |---|---| | `list_documents` | what exists, and which files someone has open right now | | `read_document` | the live text when a room holds it, the file otherwise | | `edit_document` | replace old→new; a delta through the room, so an open editor keeps its cursor, scroll and undo | | `append_document` | add at the end, same way | | `render_document` | render the book, a chapter or a page and hand back the pages as images (`format: "pdf"` also leaves the PDF on the server) | | `check_document` | compile the book and return typst's diagnostics | `edit_document` refuses an `old_string` that appears zero times, or more than once without `replace_all` — same rule whether it goes through a room or to a file nobody has open. Without the MCP server, editing files directly is only safe when no server is running against that project. ## Deploying templates and modules `templates/` is not a document, so it is deployed rather than edited through a room. `templates/module` is a git submodule (`noteworthy-modules`). ```bash # commit in the submodule, then bump the pin in the parent repo cd templates/module && git commit && git push cd ../.. && git commit -am "Bump modules: ..." && git push # --delay-updates: every file is staged and renamed at the end, so the project # never sits half-updated. Without it a watching tinymist compiles a tree where # one file has landed and another has not, and reports unresolved imports. rsync -a --delay-updates --exclude='.git' templates/module/ host:project/templates/module/ # reconcile ONLY the paths deployed. An empty body means *every* live room, # which reloads documents nobody asked you to touch. curl -X POST http://host:8010/api/rooms/reload -H 'Content-Type: application/json' \ -d '{"paths": ["templates/module/canvas/space.typ"]}' ``` ## Rendering a page to look at it `render_document` is the short way, and it takes names: `"8/2"`, `"content/8/2.typ"`, `"8"` for a chapter, nothing for the book. Without the MCP server, compile through the parser yourself -- and either way **look at the image** before saying a figure works. ```bash typst compile templates/core/parser.typ out-{n}.png --root . --ppi 110 \ --input 'chapter-folders=["8", "9"]' \ --input 'page-folders={"8": ["1", "2"], "9": ["1"]}' \ --input target=0/1 # index into chapter-folders / page-folders, not names ``` ### A red `?label` in a partial render is correct A compile of one page or one chapter has no cross-page label map, so any `@label` whose target lives on another page renders as a red `?label`. **This is intended and is not something to fix.** Do not rewrite the reference, add the label locally, or report it as broken: it resolves in a full build, where the first pass has collected every label in the book. Render the whole book (`render_document` with no target) if you need to see it resolved. ## Blocks `definition`, `theorem`, `example`, `note`, `notation`, `analysis`, `equation`, `solution`, `proof`. Signature: `#block-kind(title, number, label: "name")[body]`. ```typst #definition("Truncated Cone", label: "tcone")[ ... ] // auto-numbered #note("Why", auto, label: "why")[ ... ] // auto is the number slot @tcone // renders "Definition 8.2.1" ``` Blocks nest, and a `solution` counts within the block containing it. A reference is drawn in that block kind's scheme colour. ## Figures Canvases take objects as positional arguments; arrays of objects are fine. ```typst canvas.cartesian-canvas(size: (6, 4), x-domain: (0, 6), y-domain: (-3, 3), ..objects) canvas.polar-canvas / trig-canvas / graph-canvas / blank-canvas // blank: no axes canvas.space-canvas(x-domain:, y-domain:, z-domain:, size:, elevation:, azimuth:, projection: "orthographic" | "perspective", axis-dir: (y: -1)) ``` | module | what is in it | detail | |---|---|---| | unqualified | blocks, covers, layout, `@ref` | `reference/blocks.md` | | `shape` | points, lines, circles, arcs, polygons, angles, braces, text, constructions, intersections | `reference/geometry.md` | | `graph` | functions, parametric and polar curves, surfaces, vectors, tangents, Riemann sums | `reference/plotting.md` | | `data` | series, CSV, tables, smooth curves | `reference/plotting.md` | | `canvas` | the six canvases and the 3D camera | `reference/plotting.md` | | `combi` `trees` `dsa` `timeline` | permutations, trees, arrays/stacks/graphs/grids, timelines | `reference/structures.md` | Read the reference file for the module you are using: the signatures there are taken from `templates/module`, which is ahead of the feature document in `content/`. ### The 2D/3D rule Every point-built shape takes `(x, y)` **or** `(x, y, z)`. In a space canvas a two-component point lies on the ground plane and a three-component one is in space — so a parametric function is 3D exactly when it returns three numbers. ```typst shape.circle((0, 0, 1.5), radius: 1) // a ring at height 1.5 graph.parametric(t => (calc.cos(t), calc.sin(t), t / 2)) // a helix graph.parametric(x => (x, 0, f(x)), domain: (a, b)) // a profile in the xz-plane ``` A circle is always flat on the ground plane. A ring perpendicular to an axis is a `polyline` or a parametric curve, not a `circle`. ### Styling `style: (stroke: (dash: "dashed"))` keeps the theme's paint and thickness — naming one part of a stroke keeps the rest. A bare colour (`stroke: red`) replaces outright. A `surface` with a translucent colour is not stroked: stroking a facet in its own translucent fill doubles the alpha along every shared edge and the mesh shows up as a grid of lines. Opaque surfaces are stroked, to close antialiasing seams. `brace` takes `angle:` — a direction on the page (0deg right, 90deg up) — and goes to whichever of its two sides points that way, measured after projection. Through a camera that is the only way to say which side you meant. ## Checks worth making - Render and look at it. Overlapping labels and a figure that silently drew nothing both survive a clean compile. - An unknown named argument to a canvas is silently ignored, not an error (`space-canvas` panics on them; the flat ones do not). - `check_document`, or a compile, before leaving an edit behind.