Feature note: Tufte-style margin notes [note-0000]
@note{...} renders a margin note beside the line it comments. Numbering comes from CSS counters, so an author no need to maintain that, numbers are computed automatically via CSShttps://www.w3schools.com/Css/.
A @mention written with no anchor text (i.e. @mention["..."]) inside a note expands into the mentioned card's calling card. With anchor text it stays a plain link (i.e. @mention["..."]{...})
Citing a card therefore costs what writing a link costs, while the reader learns what is over there without leaving the page. Either way the mention reaches the backlink graph.
Where there is no margin to write in the note falls back to an indented block, and on a phone it folds away with the superscript as its tap target.
Feature tr/depends: hash-only file dependencies [tr-depends]
tr's content-addressed build already tracks @include{path} as an extra input to a card's build signature, so a generated HTML file (e.g. Agda's HTML backend) changes invalidate the card even when the .scrbl itself is untouched. But @include also splices the file's bytes into the rendered output, which is only right when you actually want the file inlined.
Sometimes a card links to a hand-authored file it does not want inlined -- e.g. an animation .js loaded from a raw script element instead of being spliced in. Until now the only way to make editing that file invalidate the card was to fake an @include, which would incorrectly splice the JS source into the page.
@tr/depends{path} provides a solution for this: it renders nothing, and only tells the build signature "this card also depends on this file". path is resolved relative to the project root, not _tmp/ like @include is, since a @tr/depends file is never spliced in and so has no render-time resolution rule of its own.
@title{Some Animation}
@tr/depends{assets/anim.js}
@p{... some card content that loads the file via a script element ...}
Editing assets/anim.js now rebuilds the card even though card file xxx.scrbl never changed.
Feature Config: new option for cards filtering [config-option-remove-content]
tr's configuration now has a new option remove-content-if, you can use it to remove cards from build, e.g. you don't want to publish drafts
(define (remove-content-p source-path)
(define segs (map path->string (explode-path source-path)))
(and (equal? "content" (first segs))
(equal? "drafts" (second segs))))
(define site
(hash ...
'remove-content-if remove-content-p))Tool VSCode extension [tr-vscode]
VSCode extension for tr-notes, it provides
- Insert mention: command
tr: Insert Mention (card search) - Create new card with prefix: command
tr: New Card - Live preview: command
tr: Open Preview
Read more in repository.
Break Config as code: site.json becomes site.rkt [config-as-code]
tr's configuration is no longer a JSON file. It is now a module
site.rkt that (provide site) a hash. See Configuration for the current format.
Why [local-0]
JSON could not describe what goes into a page's <head> without an ever-growing list of special-case keys, for example, the old "fedi" option. With the config as code you write scribble HTML elements directly, and you can compute and share values: for example a release config that reuses the dev one and overrides only mode and output-path.
What changed [local-1]
- Configuration is
site.rkt, a module exporting asitehash. - The
"fedi"option is removed. Inject identity and metadata links through the genericheadlist instead (rel="me",fediverse:creator,webmention, …). raco tr initnow scaffoldssite.rkt.
How to migrate [local-2]
Run a build with new tr. When tr sees a leftover site.json it generates the equivalent site.rkt for you, it will upgrade option "fedi" into the matching head elements, then asks you to delete the JSON. Then you:
This conversion is a temporary convenience and may be removed in a future release, so migrate rather than relying on it.
Feature tr/card's new option to start collapsed [tr-card-collapsed]
By default the tr/card is expanded. Pass #:open #f to render it collapsed:
@tr/card[#:open #f]{
@title{Xxx}
@taxon{Feature}
@p{Some words......}
}
Demonstration A collapsed demonstration [local-0]
This card starts collapsed because of #:open #f.
Example Syntax highlighting via tree-sitter [highlight]
@tr/code lets you drop arbitrary Racket into a card, which means you can teach tr new tricks without touching tr itself. Here is a real one: build-time syntax highlighting backed by https://tree-sitter.github.io/tree-sitter/, with no runtime JavaScript at all.
The whole thing is a single module, common/highlight.rkt, that you pull in with @tr/code:
@tr/code{#lang racket
(require "../common/highlight.rkt")
(provide (all-from-out "../common/highlight.rkt"))
}
After that, a card can write @codeblock["go"]{...} and get colored output:
@codeblock["go"]{
m := ir.NewModule()
globalG := m.NewGlobalDef("g", constant.NewInt(types.I32, 2))
println(m.String()) // print the assembled IR
}
One detail worth copying: highlight-available?. If a grammar is missing, fails to load, or its query will not compile, highlight-code does not blow up the build — it falls back to a plain HTML-escaped block, exactly like an un-highlighted @pre. Worst case you lose the colors, not the page.
Everything needed to reproduce this is below — drop highlight.rkt into a common/ directory next to your content, paste the CSS into your stylesheet, and add the grammar build to your Makefile (or any build system you are using).
Source common/highlight.rkt [source-BMV5]
(require ffi/unsafe
racket/file
racket/list
racket/promise
racket/string
scribble/html/html
scribble/html/xml
(only-in scribble/text disable-prefix))
(provide codeblock
highlight-code
code-grammars-dir
tree-sitter-lib-path
highlight-available?)
(define code-grammars-dir (make-parameter "ts-grammars"))
(define tree-sitter-lib-path
(make-parameter
(or (getenv "TREE_SITTER_LIB")
; for MacOS homebrew
"/opt/homebrew/opt/tree-sitter/lib/libtree-sitter")))
(define ts-lib
(delay/sync (ffi-lib (tree-sitter-lib-path) '("0" #f))))
(define-syntax-rule (ts name type)
(get-ffi-obj 'name (force ts-lib) type))
(define-cstruct _TSNode
([context (_array _uint32 4)]
[id _pointer]
[tree _pointer]))
(define-cstruct _TSQueryCapture
([node _TSNode]
[index _uint32]))
(define-cstruct _TSQueryMatch
([id _uint32]
[pattern-index _uint16]
[capture-count _uint16]
[captures _pointer])) ; const TSQueryCapture*
(define (ts-parser-new) ((ts ts_parser_new (_fun -> _pointer))))
(define (ts-parser-delete p) ((ts ts_parser_delete (_fun _pointer -> _void)) p))
(define (ts-parser-set-language p l)
((ts ts_parser_set_language (_fun _pointer _pointer -> _bool)) p l))
(define (ts-parser-parse-string p bs len)
((ts ts_parser_parse_string (_fun _pointer _pointer _bytes _uint32 -> _pointer))
p #f bs len))
(define (ts-tree-delete t) ((ts ts_tree_delete (_fun _pointer -> _void)) t))
(define (ts-tree-root-node t) ((ts ts_tree_root_node (_fun _pointer -> _TSNode)) t))
(define (ts-node-start-byte n) ((ts ts_node_start_byte (_fun _TSNode -> _uint32)) n))
(define (ts-node-end-byte n) ((ts ts_node_end_byte (_fun _TSNode -> _uint32)) n))
(define (ts-query-new lang src)
(define-values (q err-off err-type)
((ts ts_query_new
(_fun _pointer _bytes _uint32 (eo : (_ptr o _uint32)) (et : (_ptr o _int))
-> (q : _pointer) -> (values q eo et)))
lang src (bytes-length src)))
(unless q
(error 'highlight "query compile failed (offset ~a, type ~a)" err-off err-type))
q)
(define (ts-query-delete q) ((ts ts_query_delete (_fun _pointer -> _void)) q))
(define (ts-query-capture-name q idx)
(define-values (p len)
((ts ts_query_capture_name_for_id
(_fun _pointer _uint32 (l : (_ptr o _uint32)) -> (p : _pointer) -> (values p l)))
q idx))
(define bs (make-bytes len))
(memcpy bs p len)
(bytes->string/utf-8 bs))
(define (ts-query-cursor-new) ((ts ts_query_cursor_new (_fun -> _pointer))))
(define (ts-query-cursor-delete c) ((ts ts_query_cursor_delete (_fun _pointer -> _void)) c))
(define (ts-query-cursor-exec c q node)
((ts ts_query_cursor_exec (_fun _pointer _pointer _TSNode -> _void)) c q node))
(define (ts-query-cursor-next-match c)
(define-values (ok m)
((ts ts_query_cursor_next_match
(_fun _pointer (m : (_ptr o _TSQueryMatch)) -> (ok : _bool) -> (values ok m)))
c))
(and ok m))
(define lang-cache (make-hash))
(define (load-lang lang)
(hash-ref!
lang-cache lang
(lambda ()
(define dir (build-path (code-grammars-dir) lang))
(define gram (ffi-lib (path->string (build-path dir lang)) '(#f)))
(define lang-ptr ((get-ffi-obj (format "tree_sitter_~a" lang) gram (_fun -> _pointer))))
(define query-src (file->bytes (build-path dir "highlights.scm")))
(cons lang-ptr (ts-query-new lang-ptr query-src)))))
(define (highlight-available? lang)
(with-handlers ([exn:fail? (lambda (_) #f)])
(load-lang lang)
#t))
(define (capture->class name)
(cond
[(string=? name "keyword") "kw"]
[(member name '("function" "function.method" "function.builtin")) "fn"]
[(string=? name "type") "type"]
[(string=? name "string") "str"]
[(string=? name "escape") "escape"]
[(string=? name "number") "num"]
[(string=? name "comment") "comment"]
[(string=? name "operator") "op"]
[(string=? name "constant.builtin") "const"]
[(string=? name "property") "prop"]
[(string=? name "variable") "var"]
[else (string-replace name "." "-")]))
(define (html-escape s)
(string-replace
(string-replace
(string-replace s "&" "&")
"<" "<")
">" ">"))
(define (highlight-code lang code)
(cond
[(not (highlight-available? lang)) (html-escape code)]
[else
(define code-bytes (string->bytes/utf-8 code))
(define n (bytes-length code-bytes))
(define entry (load-lang lang))
(define lang-ptr (car entry))
(define query (cdr entry))
(define parser (ts-parser-new))
(ts-parser-set-language parser lang-ptr)
(define tree (ts-parser-parse-string parser code-bytes n))
(define root (ts-tree-root-node tree))
(define cursor (ts-query-cursor-new))
(ts-query-cursor-exec cursor query root)
(define name-cache (make-hash))
(define (idx->class idx)
(hash-ref! name-cache idx (lambda () (capture->class (ts-query-capture-name query idx)))))
(define caps
(let loop ([acc '()])
(define m (ts-query-cursor-next-match cursor))
(cond
[(not m) acc]
[else
(define cnt (TSQueryMatch-capture-count m))
(define base (TSQueryMatch-captures m))
(loop
(for/fold ([acc acc]) ([i (in-range cnt)])
(define cap (ptr-ref base _TSQueryCapture i))
(define node (TSQueryCapture-node cap))
(cons (list (ts-node-start-byte node)
(ts-node-end-byte node)
(idx->class (TSQueryCapture-index cap)))
acc)))])))
(define owner (make-vector n #f))
(define (varprio cls) (if (string=? cls "var") 1 0))
(for ([c (in-list (sort caps (lambda (a b)
(define wa (- (cadr a) (car a)))
(define wb (- (cadr b) (car b)))
(cond
[(not (= wa wb)) (< wa wb)]
[(not (= (car a) (car b))) (< (car a) (car b))]
[else (< (varprio (caddr a)) (varprio (caddr b)))]))))])
(for ([b (in-range (car c) (cadr c))])
(unless (vector-ref owner b) (vector-set! owner b (caddr c)))))
(ts-query-cursor-delete cursor)
(ts-tree-delete tree)
(ts-parser-delete parser)
(define out (open-output-string))
(let loop ([i 0])
(when (< i n)
(define cls (vector-ref owner i))
(define j (let scan ([j i])
(if (and (< j n) (equal? (vector-ref owner j) cls)) (scan (add1 j)) j)))
(define text (html-escape (bytes->string/utf-8 (subbytes code-bytes i j))))
(if cls
(begin (display "<span class=\"tok-" out) (display cls out) (display "\">" out)
(display text out) (display "</span>" out))
(display text out))
(loop j)))
(get-output-string out)]))
(define (codeblock lang . content)
(define code
(apply string-append (for/list ([x (in-list (flatten content))]) (format "~a" x))))
(disable-prefix (pre (literal (highlight-code lang code)))))Source CSS: tok-* classes [source-CMSS]
pre .tok-kw { color: #7c4dff; font-weight: 600; }
pre .tok-fn { color: #1565c0; }
pre .tok-type { color: #0277bd; }
pre .tok-str { color: #c62828; }
pre .tok-escape { color: #ad1457; font-weight: 600; }
pre .tok-num { color: #6a1b9a; }
pre .tok-comment { color: #6d7b86; font-style: italic; }
pre .tok-op { color: #00897b; }
pre .tok-const { color: #6a1b9a; }
pre .tok-prop { color: #ad1457; }
pre .tok-var { color: inherit; }Source Makefile [source-AMSK]
Hook grammars into your default target, then add languages by listing them in TS_LANGS.
# tree-sitter grammars
TS_LANGS := go
TS_BUILD := _tmp/ts-grammars-src
.PHONY: grammars
grammars:
@for lang in $(TS_LANGS); do \
out=ts-grammars/$$lang; \
if [ -f $$out/$$lang.dylib ]; then echo "✓ grammar $$lang"; continue; fi; \
case $$lang in \
*) repo=https://github.com/tree-sitter/tree-sitter-$$lang ;; \
esac; \
echo "building grammar $$lang from $$repo"; \
mkdir -p $$out $(TS_BUILD); \
test -d $(TS_BUILD)/tree-sitter-$$lang || git clone --depth 1 $$repo $(TS_BUILD)/tree-sitter-$$lang; \
src=$(TS_BUILD)/tree-sitter-$$lang/src; \
if [ -f $$src/scanner.cc ]; then \
cc -c -fPIC -O2 -I $$src $$src/parser.c -o $(TS_BUILD)/$$lang-parser.o; \
c++ -c -fPIC -O2 -I $$src $$src/scanner.cc -o $(TS_BUILD)/$$lang-scanner.o; \
c++ -shared $(TS_BUILD)/$$lang-parser.o $(TS_BUILD)/$$lang-scanner.o -o $$out/$$lang.dylib; \
else \
files="$$src/parser.c"; \
[ -f $$src/scanner.c ] && files="$$files $$src/scanner.c"; \
cc -shared -fPIC -O2 -I $$src $$files -o $$out/$$lang.dylib; \
fi; \
cp $(TS_BUILD)/tree-sitter-$$lang/queries/highlights.scm $$out/highlights.scm; \
echo "✓ grammar $$lang"; \
done Tool Use tr-agda to create literate agda card [tr-agda]
To create literate agda integration for tr-notes, I publish a new tool https://repo.dannypsnl.me/tr-notes/tr-agda, usage is
uv run tr-agda $(raco tr next ag)
You can provide title and taxon
uv run tr-agda $(raco tr next ag) --title [TITLE] --taxon [TAXON]
Feature tr/card [tr-card]
This is the final feature in the plan, which allows you write
@tr/card{
@title{Xxx}
@taxon{Feature}
@p{Some words......}
}
Which creates an immediate card in current card, let me demonstrate an instance:
Demonstration A demonstration [local-0]
Demonstrate tr/card
To be simple, this feature only allows metadata title and taxon, otherwise you must create a new file.
Use python livereload to create livereload server [tip-0000]
I also use the following approach
from livereload import Server, shell
server = Server()
server.watch('content/**/*.scrbl', shell('raco tr build'))
server.serve(root='_build', port=8000)
Code is very simple (because the work of livereload module), we simply watch scribble files in content/ directory, and serve at port 8000.
Break Update syntax of mention form [mention-0000]
The usual way @mention{addr} still work, but link text part is different, the old one is
@mention[#:title "link text"]{addr}
Now must be
@mention["addr"]{link text}
This is because the following syntax only be parsed properly in this way, in scribble at-expr
@mention["addr"]{@m{S}}
Feature tr/code [trcode]
@tr/code{...} is a new feature to help users create their own extension for card, for example, you can add math macro
@tr/code{#lang racket
(provide (all-defined-out))
(define RR "\\mathbb{R}")
}
Or use this to fetch cards you want to transclude:
@tr/code{
(define scrbl-list (find-files (lambda (x) (string-contains? (path->string (file-name-from-path x)) "guide")) "content"))
(define addr-list
(for/list ([path scrbl-list])
(compute-addr path)))
}
Then I can use @ol[@(for/list ([addr addr-list]) (li addr))] to list these addresses:
- guide-0000
- guide-0001
- guide-0002
- guide-0003
- guide-0004
- guide-0005
- guide-0006
- guide-0007
- guide-0008
- guide-0009
- guide-000A
- guide-000B
The form can be placed as any top-level form, so you can have
@tr/code{...}
@p{...}
@tr/code{...}
But you must have #lang [language] line in the first @tr/code{...} to decide your language.
Feature Typst backend [typst]
We support as backend now, below are example:
@typst{
#set page(width: 10cm, height: 4.5cm, margin: 0.5cm)
$
nabla · bold(E) &= frac(rho, epsilon_0) \
nabla · bold(B) &= 0 \
nabla × bold(E) &= -frac(partial bold(B), partial t) \
nabla × bold(B) &= mu_0(bold(J) + epsilon_0 frac(partial bold(E), partial t))
$
}
@typst|{
#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
#set page(width: 8cm, height: 4cm, margin: 0.3cm)
#diagram(cell-size: 15mm, $
G edge(f, ->) edge("d", pi, ->>) & im(f) \
G slash ker(f) edge("ur", tilde(f), "hook-->")
$)
}|
Tool bib2tr [bib2tr]
A tool converts bib file to scribble cards, https://repo.dannypsnl.me/tr-notes/bib2tr.
Usage [local-0]
Convert a BibTeX file to tr-notes format:
bib2tr -b bibliography.bib content/refs
Convert a DOI to tr-notes format:
bib2tr -D 10.1000/182 content/refs
Tool html2scrbl [html2scrbl]
A web tool to convert input HTML to scribble/html, https://dannypsnl.github.io/html2scrbl/.
Break texfig now must manually assigns LaTeX header part [texfig-0000]
Form @texfig{...} at before implicitly do
\usepackage{quiver}
\usepackage{tikz}
\usetikzlibrary{spath3, intersections, backgrounds}
for users, now they are removed. Hence you must write it out explicit
@texfig[#:header @"
\\usepackage{tikz}
\\usetikzlibrary{spath3, intersections, backgrounds}
"]{
...
}
The output Tex file you will get is
\documentclass[crop,dvisvgm]{standalone}
\usepackage{tikz}
\usetikzlibrary{spath3, intersections, backgrounds}
\begin{document}
...
\end{document} Policy of breaking changes [policy-0000]
Since TR is a finished software (https://josem.co/the-beauty-of-finished-software/), every non-customize behaviour and already worked functions will not be removed, if you found your old cards stop working, reports to https://github.com/dannypsnl/tr/issues. I can't say there won't have any exceptions, but most problems should be counted as TR's bugs, and hence should be fixed.
Due to early experimental code, tikzcd and texfig is unstable.
The styling isn't belongs to above promise, I can only say I would avoid to touch them as possible. For example, recently I want to minimize table of content on mobile to improve UX, but except that, I will not change code of assets.
If a necessary breaking change is made, I will create a new post here that explain how to fix it.