Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}