Transaction

2A9E54FCC8C8CE…718A0FBBB38F

Block 446,903 · index 0 · indexed

Summary

Hash
2A9E54FCC8C8CEC0887FFFE019BC8C221F8FDB61A7DDFD5993D3718A0FBBB38F
Block
446,903
Size
23812 bytes
Gas used
31,091,998 / 72,962,200
Fee
218886ugnot
Status
success

Messages

#1AddPackagegno.land/r/moul/gallery/v015 arguments
Attached funds
10000000ugnot

Arguments · 15

  1. #1gallery
  2. #2README.md
  3. #3# `gno.land/r/moul/gallery/v0` A curated index of art on gno.land, in whichever form the reader can actually consume. **It stores no art.** Every piece lives in somebody's realm; this one renders the ones it can import and points at the ones it cannot. There is no package-level state anywhere in it, so there is nothing for a redeploy to wipe and nothing it pays storage for. ## Rendered, or linked, and it says which | | | |---|---| | **rendered here** | the source realm is public, so the gallery imports it and calls it live: `gnoface`, `millipede`, `sapin` | | **linked** | the source cannot be imported, so the gallery points at it and says so rather than showing an empty box | `asciiart` is the honest case: it declares `private = true`, which means no realm may ever import it. That is a constraint of the chain, not an omission here, and the piece page says exactly that. ## The target is a parameter Every page answers to `?fmt=txt` as well as the default markdown. The text target is **not markdown with the characters deleted**. It is a different document built from the same content: 1. A link prints its text inline and collects the URL into a numbered footnote at the end. Never `[text](url)`, which is the single worst thing markdown does to a terminal reader. 2. A heading is underlined (`=` for H1, `-` for H2), not prefixed with `#`. 3. A table gets columns padded to their widest cell, under a rule. An unpadded pipe table is the other thing no amount of character-stripping fixes. 4. Art is emitted **verbatim**: never wrapped, never escaped, never fenced. A consumer that asked for text asked for the picture as it is. 5. Prose wraps at 72 runes. A word longer than that is left alone, because a realm path is worse broken than wide. ``` /r/moul/gallery:?fmt=txt the index, pipeable /r/moul/gallery:piece/millipede?fmt=txt one piece, art and all ``` `?fmt=ansi` is deliberately **absent**. Colour cannot reach gnoweb: `p/nt/markdown/sanitize` strips control bytes and raw ESC is garbage in HTML, so a realm offering it would hand every web reader a page of escape bytes. Terminal colour is `p/moul/art`'s `Canvas.ANSI`, for a consumer that asked for it over something other than gnoweb. ## Where the text renderer lives, and why it is here `text.gno` holds `doc`, a small plain-text document builder. It is a **prototype**, deliberately in this realm rather than in a `p/` package. Porting one realm is not enough to know what a shared document model should be; porting the fifth will be. What it already pins is the list of five rules above, which is the part that was actually unknown. When a second realm needs it, it graduates to `p/moul/doc` and this file becomes an import. ## Adding a piece `catalog()` in `catalog.gno`, one struct literal. Set `Draw` to a closure over the source realm's function when the realm is public and vendored; leave it nil and the gallery will link the piece and say why it is not rendered. The catalog is built per call rather than held in a package-level var: a func value is not something to persist, and a realm that stores nothing should not be paying storage for its own index. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/gallery/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/gallery/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4catalog.gno
  5. #5package gallery import ( "gno.land/r/moul/demo/gnoface/v0" "gno.land/r/moul/demo/millipede/v0" "gno.land/r/moul/sapin/v0" ) // piece is one entry in the index. // // The gallery stores no art. Every piece lives in somebody's realm and this is // a pointer to it, which is why there is no package-level state anywhere in // this realm and nothing here survives or needs to survive a redeploy. type piece struct { Slug string Title string Author string Origin string // realm path, without the gno.land prefix License string Note string // Draw renders the piece live, by importing its source. It is nil when the // source cannot be imported, which is the honest half of a curated index: // a private realm or a package this repo has not vendored can be pointed // at but not shown. Draw func() string } // Live reports whether this realm can render the piece itself. func (p piece) Live() bool { return p.Draw != nil } // catalog is built per call rather than held in a package-level var: a func // value is not something to persist, and a realm that stores nothing should // not be paying storage for its own index. func catalog() []piece { return []piece{ { Slug: "gnoface", Title: "gnoface", Author: "moul", Origin: "r/moul/demo/gnoface/v0", License: "see the source realm", Note: "A face generated from a seed. Every seed is a different face, and none of them are stored.", Draw: func() string { return gnoface.Draw(42) }, }, { Slug: "millipede", Title: "millipede", Author: "moul", Origin: "r/moul/demo/millipede/v0", License: "see the source realm", Note: "A millipede of any length. The oldest piece here, and still the one that best explains why ASCII is the cheapest picture there is.", Draw: func() string { return millipede.Draw(8) }, }, { Slug: "sapin", Title: "sapin", Author: "moul", Origin: "r/moul/sapin/v0", License: "see the source realm", Note: "A fir tree, sized on request.", Draw: func() string { return sapin.Sapin(6) }, }, { Slug: "settler25", Title: "Settler #25", Author: "the Settlers", Origin: "r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft", License: "CC0 1.0", Note: "moul's, soulbound, minted at block 409242. A 32x32 indexed sprite with 20 colours, which is what made it the test case for p/moul/art: a luminance ramp resolves those 20 to 9, and keying on the palette index keeps all 20.", }, { Slug: "asciiart", Title: "asciiart", Author: "moul", Origin: "r/moul/x/daily/asciiart/v1", License: "see the source realm", Note: "Patterns from short deterministic algorithms, plus everyone's generations. Declared private = true, so it can be linked but never imported: that is the constraint, not an omission.", }, { Slug: "pixelcanvas", Title: "pixelcanvas", Author: "moul", Origin: "r/moul/x/daily/pixelcanvas/v0", License: "see the source realm", Note: "A shared grid anyone can paint one pixel of.", }, } } func find(slug string) (piece, bool) { for _, p := range catalog() { if p.Slug == slug { return p, true } } return piece{}, false }
  6. #6gallery.gno
  7. #7// Package gallery is a curated index of art on gno.land, in whichever form the // reader can actually consume. // // It stores no art. Every piece lives in somebody else's realm; this one // renders the ones it can import and points at the ones it cannot. // // # The target is a parameter // // Every page answers to ?fmt=txt as well as the default markdown. The text // target is not markdown with the characters deleted: links become numbered // footnotes, headings get underlines instead of a leading '#', and art is // emitted verbatim rather than fenced. The result is meant to be piped, or read // by anything that is not a browser. // // ?fmt=ansi is deliberately absent. Colour cannot reach gnoweb, so a realm that // offered it would be handing every web reader a page of escape bytes. Terminal // colour is p/moul/art's Canvas.ANSI, for a consumer that asked for it over // something other than gnoweb. package gallery import ( "strings" "gno.land/p/moul/art/v0" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/md/v0" "gno.land/p/moul/realmpath/v0" ) // realmPath is this realm's own path, version included. // // gnoweb does NOT resolve an unversioned realm path to its latest version: it // serves /r/moul/gallery as a DIRECTORY INDEX and drops the :path and ?query // entirely. Measured 2026-09-30 on gnoland-1, where /r/moul/outfmt:?fmt=json // renders the index page while /r/moul/outfmt/v0:?fmt=json returns the JSON. // So every self-link has to carry the version, and stating it once is what // keeps the next one from being written without it. Only r/moul/home and // r/moul/blog are genuinely unversioned paths. const realmPath = "/r/moul/gallery/v0" func Render(path string) string { req := realmpath.Parse(path) text := req.Query.Get("fmt") == "txt" if slug := strings.TrimPrefix(req.Path, "piece/"); slug != req.Path && slug != "" { return renderPiece(slug, text) } return renderIndex(text) } // banner is drawn with p/moul/art rather than typed as a string literal, // because a gallery that hand-writes its own ASCII would be a poor advert for // the library it exists to show. func banner() string { c := art.NewCanvas(22, 2) c.Write(0, 0, "moul's gallery") c.Write(0, 1, "art on gno.land") return c.Frame("", art.Round).Text() } func renderIndex(text bool) string { live, linked := 0, 0 for _, p := range catalog() { if p.Live() { live++ } else { linked++ } } if text { var d doc d.Art(banner()) d.Para("Art on gno.land, rendered rather than described. Nothing here is stored in this realm: every piece lives where its author put it.") rows := [][]string{} for _, p := range catalog() { shown := "linked" if p.Live() { shown = "rendered here" } rows = append(rows, []string{p.Slug, p.Author, p.Origin, shown}) } d.Table([]string{"Piece", "Author", "Where", "Shown"}, rows) d.Para(plural(live, "piece") + " rendered from source, " + plural(linked, "piece") + " linked.") d.Para("Open one at " + realmPath + ":piece/<slug>, with ?fmt=txt for this form.") return d.String() } out := md.H1("moul's gallery") out += md.CodeBlock(banner()) out += "\n" out += "Art on gno.land, rendered rather than described. Nothing here is stored in this realm: every piece lives where its author put it.\n\n" t := ui.NewTable("Piece", "Author", "Where", "Shown") for _, p := range catalog() { shown := "linked" if p.Live() { shown = "rendered here" } t.Row( md.Link(p.Title, realmPath+":piece/"+p.Slug), ui.Inline(p.Author), md.InlineCode(p.Origin), shown, ) } out += t.OrEmpty("nothing indexed yet") out += "\n" + md.Italic(plural(live, "piece")+" rendered from source, "+plural(linked, "piece")+" linked.") + "\n\n" out += md.Italic("Every page here also answers to `?fmt=txt`.") + "\n" return out } func renderPiece(slug string, text bool) string { p, ok := find(slug) if !ok { if text { return "No piece called " + slug + ".\n" } return md.H1("Not found") + ui.Empty("no piece called "+ui.Inline(slug)) } if text { var d doc d.H1(p.Title) if p.Live() { d.Art(p.Draw()) } else { d.Para("Not rendered here: this realm cannot import the source.") } d.Para(p.Note) d.Field("Author", p.Author) d.Field("License", p.License) d.Field("Source", p.Origin) d.Link("the source realm", "https://gno.land/"+p.Origin) return d.String() } out := md.H1(p.Title) if p.Live() { out += md.CodeBlock(p.Draw()) } else { out += ui.Empty("not rendered here: this realm cannot import the source") + "\n" } out += "\n" + ui.Inline(p.Note) + "\n\n" t := ui.NewTable("Field", "Value") t.Row("Author", ui.Cell(p.Author)) t.Row("License", ui.Cell(p.License)) t.Row("Source", md.Link(p.Origin, "/"+p.Origin)) out += t.String() out += "\n" + md.Link("< the gallery", realmPath) + "\n" return out } func plural(n int, word string) string { s := itoa(n) + " " + word if n != 1 { s += "s" } return s } func itoa(v int) string { if v == 0 { return "0" } var buf [20]byte i := len(buf) for v > 0 { i-- buf[i] = byte('0' + v%10) v /= 10 } return string(buf[i:]) }
  8. #8gallery_test.gno
  9. #9package gallery import ( "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestRenderIndexMarkdown(t *testing.T) { out := Render("") uassert.True(t, strings.Contains(out, "# moul's gallery"), "heading") uassert.True(t, strings.Contains(out, "```"), "the banner is fenced in markdown") uassert.True(t, strings.Contains(out, "| Piece |"), "a real markdown table") // The path is spelled out rather than taken from realmPath: a test that // reads the same constant the code does would pass with the version // dropped, which is the exact regression this pins. for _, p := range catalog() { uassert.True(t, strings.Contains(out, "/r/moul/gallery/v0:piece/"+p.Slug), p.Slug+" is linked") } } // The two targets are different documents, not one with characters deleted. func TestTextTargetStripsMarkdownAndDefersLinks(t *testing.T) { out := Render("piece/millipede?fmt=txt") uassert.False(t, strings.Contains(out, "```"), "no code fences") uassert.False(t, strings.Contains(out, "**"), "no bold markers") uassert.False(t, strings.Contains(out, "]("), "no inline link syntax") uassert.False(t, strings.Contains(out, "\n#"), "no heading hashes") // A heading is underlined instead. uassert.True(t, strings.Contains(out, "millipede\n========="), "underlined heading") // And the URL is a numbered footnote, not inline noise. uassert.True(t, strings.Contains(out, "the source realm [1]"), "inline marker") uassert.True(t, strings.Contains(out, "\nLinks:\n [1] https://gno.land/r/moul/demo/millipede/v0"), "footnote block") } func TestTextTableColumnsLineUp(t *testing.T) { out := Render("?fmt=txt") var header, rule string for _, line := range strings.Split(out, "\n") { if strings.HasPrefix(line, " Piece ") { header = line } if strings.HasPrefix(line, " ---") { rule = line } } uassert.True(t, header != "", "found the header") uassert.True(t, rule != "", "found the rule") // Every column boundary in the rule is a pair of spaces in the header at // the same rune offset. That is the property an unpadded pipe table does // not have, and the reason the text target has its own table at all. uassert.Equal(t, gutters(rule), gutters(header), "columns start at the same offsets") } // gutters returns the rune offsets at which a column starts: a non-space // preceded by at least two spaces. Measuring where the PADDING starts instead // would compare "Piece" against eleven dashes and report a difference that is // not one. func gutters(s string) string { r := []rune(s) out := "" for i := 2; i < len(r); i++ { if r[i] != ' ' && r[i-1] == ' ' && r[i-2] == ' ' { out += itoa(i) + "," } } return out } func TestArtIsNeverEscapedOrWrapped(t *testing.T) { // The millipede is box-drawing and parentheses. A text target that ran it // through an escaper or a wrapper would ruin it, and it would still look // like a picture, which is what makes this worth pinning. out := Render("piece/millipede?fmt=txt") uassert.True(t, strings.Contains(out, "╚═(███)═╝"), "art survives verbatim") uassert.False(t, strings.Contains(out, "\\("), "no escaping") } func TestUnknownPiece(t *testing.T) { uassert.True(t, strings.Contains(Render("piece/nope"), "no piece called nope"), "markdown 404") uassert.True(t, strings.Contains(Render("piece/nope?fmt=txt"), "No piece called nope."), "text 404") // An empty slug is the index, not a 404. uassert.True(t, strings.Contains(Render("piece/"), "# moul's gallery"), "empty slug falls back to the index") } func TestLinkedPiecesSayWhyTheyAreNotRendered(t *testing.T) { p, ok := find("asciiart") uassert.True(t, ok, "found") uassert.False(t, p.Live(), "asciiart is private = true, so it cannot be imported") out := Render("piece/asciiart") uassert.True(t, strings.Contains(out, "cannot import the source"), "says so rather than showing nothing") } func TestEveryPieceRenders(t *testing.T) { for _, p := range catalog() { out := Render("piece/" + p.Slug) uassert.True(t, strings.Contains(out, p.Title), p.Slug+" markdown") uassert.True(t, len(Render("piece/"+p.Slug+"?fmt=txt")) > 0, p.Slug+" text") } } func TestWrapKeepsLongWordsIntact(t *testing.T) { long := "see gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft now" got := wrap(long, 20) // A path is worse broken than wide, so it overflows rather than splitting. uassert.True(t, strings.Contains(got, "gno.land/r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft"), "unbroken") uassert.Equal(t, 3, len(strings.Split(got, "\n")), "three lines") }
  10. #10gnomod.toml
  11. #11module = "gno.land/r/moul/gallery/v0" gno = "0.9" private = true
  12. #12render_example_test.gno
  13. #13package gallery func ExampleRender() { println(Render("?fmt=txt")) // Output: // ╭──────────────────────╮ // │moul's gallery │ // │art on gno.land │ // ╰──────────────────────╯ // // Art on gno.land, rendered rather than described. Nothing here is stored // in this realm: every piece lives where its author put it. // // Piece Author Where Shown // ----------- ------------ ------------------------------------------------------- ------------- // gnoface moul r/moul/demo/gnoface/v0 rendered here // millipede moul r/moul/demo/millipede/v0 rendered here // sapin moul r/moul/sapin/v0 rendered here // settler25 the Settlers r/g17khqpukees4237dtn3astzapmp462vjhsz6st4/settlers/nft linked // asciiart moul r/moul/x/daily/asciiart/v1 linked // pixelcanvas moul r/moul/x/daily/pixelcanvas/v0 linked // // 3 pieces rendered from source, 3 pieces linked. // // Open one at /r/moul/gallery/v0:piece/<slug>, with ?fmt=txt for this // form. } func ExampleRender_piece() { println(Render("piece/millipede?fmt=txt")) // Output: // millipede // ========= // // ╚⊙ ⊙╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // ╚═(███)═╝ // // A millipede of any length. The oldest piece here, and still the one that // best explains why ASCII is the cheapest picture there is. // Author: moul // License: see the source realm // Source: r/moul/demo/millipede/v0 // the source realm [1] // // Links: // [1] https://gno.land/r/moul/demo/millipede/v0 }
  14. #14text.gno
  15. #15package gallery import "strings" // doc builds the plain-text target: a document that reads well as-is, rather // than markdown with the syntax characters deleted. // // This is a prototype, deliberately small and deliberately here rather than in // a p/ package. Porting one realm is not enough to know what the shared // document model should be; porting the fifth will be. What it already pins is // the set of rules that make text good instead of merely stripped: // // 1. A link prints its text inline and collects the URL into a numbered // footnote. Never "[text](url)", which is the single worst thing markdown // does to a terminal reader. // 2. A heading is underlined, not prefixed with '#'. // 3. Art is emitted verbatim: never wrapped, never escaped, never fenced. // 4. Prose is wrapped at a fixed width; art and fields are not. type doc struct { b strings.Builder links []string } // width is where prose wraps. 72 leaves room for a quoting prefix in an email // or a diff without re-wrapping. const width = 72 func (d *doc) H1(s string) { d.blank() d.b.WriteString(s + "\n" + strings.Repeat("=", runeLen(s)) + "\n") } func (d *doc) H2(s string) { d.blank() d.b.WriteString(s + "\n" + strings.Repeat("-", runeLen(s)) + "\n") } func (d *doc) Para(s string) { if s == "" { return } d.blank() d.b.WriteString(wrap(s, width) + "\n") } func (d *doc) Item(s string) { d.b.WriteString(" - " + s + "\n") } // Table writes columns padded to their widest cell, under a rule. An unpadded // pipe table is the other thing markdown does to a terminal reader that no // amount of character-stripping fixes: the columns stop lining up and the table // stops being a table. // // Cells are laid out by rune count, not byte count, so a box-drawing glyph or // an accent does not shift the column. func (d *doc) Table(headers []string, rows [][]string) { if len(headers) == 0 { return } w := make([]int, len(headers)) for i, h := range headers { w[i] = runeLen(h) } for _, r := range rows { for i, c := range r { if i < len(w) && runeLen(c) > w[i] { w[i] = runeLen(c) } } } d.blank() d.b.WriteString(" " + joinPadded(headers, w) + "\n") rule := make([]string, len(headers)) for i := range rule { rule[i] = strings.Repeat("-", w[i]) } d.b.WriteString(" " + joinPadded(rule, w) + "\n") for _, r := range rows { d.b.WriteString(" " + joinPadded(r, w) + "\n") } } // joinPadded pads every cell but the last, which would only add trailing space. func joinPadded(cells []string, w []int) string { var b strings.Builder for i, c := range cells { if i > 0 { b.WriteString(" ") } b.WriteString(c) if i < len(cells)-1 && i < len(w) { b.WriteString(strings.Repeat(" ", w[i]-runeLen(c))) } } return b.String() } func (d *doc) Field(k, v string) { d.b.WriteString(" " + k + ": " + v + "\n") } // Art writes a block verbatim. No fence, no indent, no escaping: a consumer // that asked for text asked for the picture as it is. func (d *doc) Art(s string) { d.blank() d.b.WriteString(s + "\n") } // Link prints the text inline and defers the URL to the footnote block. func (d *doc) Link(text, url string) { d.links = append(d.links, url) d.b.WriteString(" " + text + " [" + itoa(len(d.links)) + "]\n") } func (d *doc) String() string { out := d.b.String() if len(d.links) == 0 { return out } var b strings.Builder b.WriteString(out) b.WriteString("\nLinks:\n") for i, u := range d.links { b.WriteString(" [" + itoa(i+1) + "] " + u + "\n") } return b.String() } // blank writes a separating newline unless the document is empty or already // ends in one, so no block has to know what came before it. func (d *doc) blank() { s := d.b.String() if s == "" || strings.HasSuffix(s, "\n\n") { return } d.b.WriteString("\n") } // wrap breaks on spaces at w runes. A word longer than w is left alone rather // than cut: a URL or a realm path is worse broken than wide. func wrap(s string, w int) string { var out strings.Builder line := 0 for i, word := range strings.Fields(s) { n := runeLen(word) switch { case i == 0: out.WriteString(word) line = n case line+1+n > w: out.WriteString("\n" + word) line = n default: out.WriteString(" " + word) line += 1 + n } } return out.String() } func runeLen(s string) int { n := 0 for range s { n++ } return n }

Result log

msg:0,success:true,log:,events:[]

← Back to block 446,903