Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}