v0 source pure
Package ui is the shared display vocabulary for r/moul realms: the small set of rendering decisions that every realm ...
View source
gno.land/p/moul/kit/ui/v0
The shared display vocabulary for r/moul realms: the small set of rendering
decisions every realm was making on its own, made once.
First package of the p/moul/kit/* layer
(moul/gno-contracts#151).
kit composes the existing packages, it does not replace them.
Why it exists
Measured across 118 realm directories in this repo:
Realm dirs importing zero p/moul/* |
79 of 118 |
Realm lines inside Render / render* |
5,909 of 23,276 (25.4%) |
| Lines in duplicated private helpers | 857 across 126 functions |
Copies of shortAddr |
11, with four different truncation rules |
Copies of escapeInline |
8, all a byte-identical seven-pair replacer |
The shortAddr number is the one that matters: the same account rendered
differently depending on which realm you opened. That is not duplication, it is
four answers to one question.
What is here, and what is deliberately not
This package does not re-export markdown primitives. Headings, bold, lists,
code blocks and links already have an owner in
p/moul/md; import that alongside. What lives here is only
what had no owner and was therefore copy-pasted.
1import (
2 "gno.land/p/moul/kit/ui/v0"
3 "gno.land/p/moul/md/v0"
4)
5
6func Render(path string) string {
7 t := ui.NewTable("#", "Player", "Score")
8 for i, p := range players {
9 t.Row(strconv.Itoa(i+1)+ui.Podium(i), ui.Addr(p.addr), ui.Cell(p.label))
10 }
11 return md.H1("Leaderboard") + t.OrEmpty("Nobody has played yet.")
12}
Addr(a) |
`g1manfre…dlf5`, the one address format |
AddrFull(a) |
the full address, in backticks |
AddrText(a) |
shortened, no backticks, for a link title |
AddrOf(s) |
Addr for an address already in string form (an avl key) |
Short(s), ShortN(s, head, tail) |
the same rule for any string: a URL, a handle, a commitment hash |
Inline(s), Cell(s) |
escape user text, delegated to p/nt/markdown/sanitize |
Excerpt(s, width) |
a preview of user prose: cut to width runes, then escape. That order, because escaping first and cutting second strands a backslash |
Action(title, fn, args...) |
a clickable call, instead of prose telling the reader to type a function name |
ActionIn(pkgPath, title, fn, args...) |
the same, against another realm |
NewTable(headers...), .Row(...), .String(), .OrEmpty(msg) |
GFM tables |
Empty(msg) |
the italic placeholder where a list would be |
Podium(rank) |
🥇🥈🥉, or "" past third |
Join(sep, parts...) |
concatenate, skipping empty sections |
The escaping contract
Table cells and Action titles are markdown, not plain text.
- Anything that came from a user goes through
Cell(inside a table) orInline(anywhere else) before it reaches this package. - Output of
Addr,AddrFull,AddrOfandPodiumis already safe and must not be escaped again.
Table renders the GFM table itself rather than delegating to
p/moul/mdtable, which unconditionally rewrites | to
| in every cell. Stacked on Cell, which already emits the GFM escape
\|, that double-escapes into a stray backslash (a\|b, found while
porting guestbook). One escaping stage is the only way to get this right, and
it has to be the stage that knows whether the text is user input.
Action also escapes its title, which
p/moul/helplink does not (it carries an // XXX: escape title where this would go).
Design rule
The safe, conventional thing must be the shortest thing to type. A realm author reaching for the obvious call has to land on the correct behaviour; that is the only mechanism that stops these helpers from being rewritten a twelfth time.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.
Package ui is the shared display vocabulary for r/moul realms: the small set of rendering decisions that every realm was making on its own, made once.
It deliberately does NOT re-export markdown primitives. Headings, bold, lists, code blocks and links already have an owner in p/moul/md(/p/moul/md/v0); import that alongside this package. What lives here is only what had no owner and was therefore copy-pasted:
- Addr: one address shortening for the whole namespace. Eleven realms had their own shortAddr with four different truncation rules, so the same account rendered differently depending on which realm you opened.
- Inline / Cell / Excerpt: markdown escaping, delegated to p/nt/markdown/sanitize(/p/nt/markdown/sanitize/v0). Eight realms hand-rolled a seven-pair strings.NewReplacer that misses most of what the real escaper handles. Excerpt adds the length cap those previews also need, in the order that is safe: cut, then escape.
- Action: a clickable call link instead of prose telling the reader to go type a function name.
- Table, Empty, Podium: the last recurring scraps.
Escaping contract
Table cells and Action titles are markdown, not plain text. Anything that came from a user must go through Cell (inside a table) or Inline (anywhere else) before it is handed to this package. Output of Addr, AddrFull and Podium is already safe and must NOT be escaped again.
2
15
func Action
Action renders a clickable call to a function of the current realm.
Example
1ui.Action("Play cell 4", "Move", "gameID", "7", "cell", "4")
Arguments are key/value pairs, as in p/moul/txlink. The title is escaped, which is the one thing helplink.Func does not do.
func ActionIn
ActionIn is Action against another realm, given as the full package path that realm declares on the module line of its gnomod.toml.
This doc deliberately contains no example path, not even a placeholder one. gnopm scans doc comments the way it scans imports, so any package path spelled out in a comment becomes a dependency of the package holding it: one that is neither live nor in the workspace BLOCKS publishing this package and everything that imports it, and naming a live one is no better, since it drags an unrelated realm into every publish plan. This comment previously named a path that deversioning had already removed, which blocked the whole ui dependency tree on every chain.
func Addr
Addr renders an address shortened and in backticks: `g1manfre…dlf5`.
This is the default way to show an address. It is monospace (an address is opaque data, not prose), and the backticks make it inert markdown, so it is safe in a table cell or anywhere else without further escaping.
func AddrFull
AddrFull renders an address in full, in backticks. Use it where the reader needs to copy the value; use Addr everywhere else.
func AddrOf
AddrOf is Addr for an address already in its string form, which is how realms hold one when it is an avl key or a stored field.
func AddrText
AddrText renders an address shortened, with no backticks. Use it inside a link title or another construct where backticks would not render.
func Cell
Cell escapes user-supplied text for a markdown table cell: Inline plus tab and pipe handling, so the value cannot open a new column.
func Empty
Empty renders the placeholder shown where a list would be: an italic line, newline-terminated, so it drops into a Render body as-is.
Example
1ui.Empty("No games yet.") // "_No games yet._\n"
The message is realm chrome, not user input, and is not escaped.
func Excerpt
Excerpt is Inline for a string too long to show whole: it keeps the first width runes, appends Ellipsis, and escapes what it kept.
Use it for a preview of user-written prose where only the beginning carries meaning: the first line of a post in an index, a note on a board, a comment in a list. For an identifier whose tail has to stay recognisable, an address, a hash, a URL, use Short instead, which keeps both ends.
The order is the whole point, and it is what a call site gets wrong. Escaping inserts backslashes, so cutting an ALREADY-escaped string can strand a trailing lone backslash that escapes whatever chrome follows it. Excerpt cuts first, on a rune boundary so a multi-byte character is never split, then escapes. Ellipsis stays outside the escaper: it is this package's chrome, not the user's text.
func Inline
Inline escapes user-supplied text for an inline markdown context: a sentence, a list item, a link title.
It delegates to sanitize.InlineText, which strips bidi and zero-width characters, folds newlines to spaces so the text cannot escape its line, and applies the full CommonMark inline escape. Never concatenate user-supplied text into rendered output without this.
func Join
Join concatenates parts, skipping empty ones, with sep between them. It is the small piece of glue every Render needs to assemble optional sections without emitting stray separators.
func Podium
Podium returns the medal for a zero-based rank, or "" past third place.
func Short
Short shortens any string with the house rule: unchanged when shortening would not save a character, otherwise head + ellipsis + tail.
It is the same rule Addr uses, exposed for the non-address strings realms also truncate: URLs, handles, commitment hashes.
func ShortN
ShortN is Short with an explicit head and tail. A negative head or tail is treated as zero. When head+tail cannot save a character against the input, the input is returned unchanged.
func NewTable
NewTable starts a table with the given header cells.
1
type Table
structTable accumulates markdown table rows and renders them.
Cells are markdown, not plain text: pass Addr and friends straight through, and wrap anything user-supplied in Cell first.
Table renders the GFM table itself rather than delegating to p/moul/mdtable(/p/moul/mdtable/v0), which unconditionally rewrites "|" to "|" in every cell. Stacked on Cell, which already emits the GFM escape "\|", that double-escapes into a stray backslash ("a\|b"). One escaping stage is the only way to get this right, and it has to be the one that knows whether the text is user input.
Methods on Table
func Len
method on TableLen reports how many rows have been added.
func OrEmpty
method on TableOrEmpty renders the table, or the placeholder when it has no rows.
func Row
method on TableRow appends a row and returns the table, so calls can be chained.
A row with fewer cells than there are headers is padded with empty cells; a longer row is kept as-is, which renders as a ragged table rather than silently dropping data.