gallery.gno
5.02 Kb · 176 lines
1// Package gallery is a curated index of art on gno.land, in whichever form the
2// reader can actually consume.
3//
4// It stores no art. Every piece lives in somebody else's realm; this one
5// renders the ones it can import and points at the ones it cannot.
6//
7// # The target is a parameter
8//
9// Every page answers to ?fmt=txt as well as the default markdown. The text
10// target is not markdown with the characters deleted: links become numbered
11// footnotes, headings get underlines instead of a leading '#', and art is
12// emitted verbatim rather than fenced. The result is meant to be piped, or read
13// by anything that is not a browser.
14//
15// ?fmt=ansi is deliberately absent. Colour cannot reach gnoweb, so a realm that
16// offered it would be handing every web reader a page of escape bytes. Terminal
17// colour is p/moul/art's Canvas.ANSI, for a consumer that asked for it over
18// something other than gnoweb.
19package gallery
20
21import (
22 "strings"
23
24 "gno.land/p/moul/art/v0"
25 "gno.land/p/moul/kit/ui/v0"
26 "gno.land/p/moul/md/v0"
27 "gno.land/p/moul/realmpath/v0"
28)
29
30// realmPath is this realm's own path, version included.
31//
32// gnoweb does NOT resolve an unversioned realm path to its latest version: it
33// serves /r/moul/gallery as a DIRECTORY INDEX and drops the :path and ?query
34// entirely. Measured 2026-09-30 on gnoland-1, where /r/moul/outfmt:?fmt=json
35// renders the index page while /r/moul/outfmt/v0:?fmt=json returns the JSON.
36// So every self-link has to carry the version, and stating it once is what
37// keeps the next one from being written without it. Only r/moul/home and
38// r/moul/blog are genuinely unversioned paths.
39const realmPath = "/r/moul/gallery/v0"
40
41func Render(path string) string {
42 req := realmpath.Parse(path)
43 text := req.Query.Get("fmt") == "txt"
44
45 if slug := strings.TrimPrefix(req.Path, "piece/"); slug != req.Path && slug != "" {
46 return renderPiece(slug, text)
47 }
48 return renderIndex(text)
49}
50
51// banner is drawn with p/moul/art rather than typed as a string literal,
52// because a gallery that hand-writes its own ASCII would be a poor advert for
53// the library it exists to show.
54func banner() string {
55 c := art.NewCanvas(22, 2)
56 c.Write(0, 0, "moul's gallery")
57 c.Write(0, 1, "art on gno.land")
58 return c.Frame("", art.Round).Text()
59}
60
61func renderIndex(text bool) string {
62 live, linked := 0, 0
63 for _, p := range catalog() {
64 if p.Live() {
65 live++
66 } else {
67 linked++
68 }
69 }
70
71 if text {
72 var d doc
73 d.Art(banner())
74 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.")
75 rows := [][]string{}
76 for _, p := range catalog() {
77 shown := "linked"
78 if p.Live() {
79 shown = "rendered here"
80 }
81 rows = append(rows, []string{p.Slug, p.Author, p.Origin, shown})
82 }
83 d.Table([]string{"Piece", "Author", "Where", "Shown"}, rows)
84 d.Para(plural(live, "piece") + " rendered from source, " + plural(linked, "piece") + " linked.")
85 d.Para("Open one at " + realmPath + ":piece/<slug>, with ?fmt=txt for this form.")
86 return d.String()
87 }
88
89 out := md.H1("moul's gallery")
90 out += md.CodeBlock(banner())
91 out += "\n"
92 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"
93
94 t := ui.NewTable("Piece", "Author", "Where", "Shown")
95 for _, p := range catalog() {
96 shown := "linked"
97 if p.Live() {
98 shown = "rendered here"
99 }
100 t.Row(
101 md.Link(p.Title, realmPath+":piece/"+p.Slug),
102 ui.Inline(p.Author),
103 md.InlineCode(p.Origin),
104 shown,
105 )
106 }
107 out += t.OrEmpty("nothing indexed yet")
108 out += "\n" + md.Italic(plural(live, "piece")+" rendered from source, "+plural(linked, "piece")+" linked.") + "\n\n"
109 out += md.Italic("Every page here also answers to `?fmt=txt`.") + "\n"
110 return out
111}
112
113func renderPiece(slug string, text bool) string {
114 p, ok := find(slug)
115 if !ok {
116 if text {
117 return "No piece called " + slug + ".\n"
118 }
119 return md.H1("Not found") + ui.Empty("no piece called "+ui.Inline(slug))
120 }
121
122 if text {
123 var d doc
124 d.H1(p.Title)
125 if p.Live() {
126 d.Art(p.Draw())
127 } else {
128 d.Para("Not rendered here: this realm cannot import the source.")
129 }
130 d.Para(p.Note)
131 d.Field("Author", p.Author)
132 d.Field("License", p.License)
133 d.Field("Source", p.Origin)
134 d.Link("the source realm", "https://gno.land/"+p.Origin)
135 return d.String()
136 }
137
138 out := md.H1(p.Title)
139 if p.Live() {
140 out += md.CodeBlock(p.Draw())
141 } else {
142 out += ui.Empty("not rendered here: this realm cannot import the source") + "\n"
143 }
144 out += "\n" + ui.Inline(p.Note) + "\n\n"
145
146 t := ui.NewTable("Field", "Value")
147 t.Row("Author", ui.Cell(p.Author))
148 t.Row("License", ui.Cell(p.License))
149 t.Row("Source", md.Link(p.Origin, "/"+p.Origin))
150 out += t.String()
151
152 out += "\n" + md.Link("< the gallery", realmPath) + "\n"
153 return out
154}
155
156func plural(n int, word string) string {
157 s := itoa(n) + " " + word
158 if n != 1 {
159 s += "s"
160 }
161 return s
162}
163
164func itoa(v int) string {
165 if v == 0 {
166 return "0"
167 }
168 var buf [20]byte
169 i := len(buf)
170 for v > 0 {
171 i--
172 buf[i] = byte('0' + v%10)
173 v /= 10
174 }
175 return string(buf[i:])
176}