PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g12yg9nh4ncma44emgm8msxe8aavzywt0p95tanv/home

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
home
Namespace
g12yg9nh4ncma44emgm8msxe8aavzywt0p95tanv
Files
5 (gnomod.toml)
Exported functions
15
Module
gno.land/r/g12yg9nh4ncma44emgm8msxe8aavzywt0p95tanv/home
gno
0.9

Files (5)

  • gnomod.tomltoml
  • content.gnogno
  • home.gnogno
  • widgets.gnogno
  • writes.gnogno
widgets.gnogno
1package home23import (4	"chain/runtime"5	"encoding/base64"6	"html"7	"strconv"8	"strings"910	"gno.land/p/nt/markdown/sanitize/v0"11)1213// Suggested amount on the Tip button (1 GNOT); visitors can change it.14const tipSuggestion = "1000000ugnot"1516// expand replaces every widget token in one pass. strings.Replacer does not17// rescan its output, so visitor text inside a widget can never trigger a18// token. Only the tokens present in md are built, found with a single scan19// of md.20func expand(md, tab string, page int) string {21	words := map[string]bool{}22	for _, w := range strings.Split(md, ":") {23		words[w] = true24	}25	var pairs []string26	add := func(tok string, build func() string) {27		if words[tok[1:len(tok)-1]] {28			pairs = append(pairs, tok, build())29		}30	}31	add(":banner:", bannerMD)32	add(":badges:", badgesMD)33	add(":now:", nowMD)34	add(":waves:", wavesMD)35	add(":tip:", tipMD)36	add(":sign:", signMD)37	add(":guestbook:", func() string {38		if tab != guestbookTab {39			return "" // a form would target /u/<name> on the profile view40		}41		return guestbookMD(page)42	})43	add(":blog:", func() string {44		if tab != blogTab {45			return "" // its page links lead to the Blog tab46		}47		return blogMD(page)48	})49	add(":posts:", postsMD)50	add(":height:", func() string { return strconv.FormatInt(runtime.ChainHeight(), 10) })51	add(":owner:", func() string { return owner.String() })52	if len(pairs) == 0 {53		return md54	}55	return strings.NewReplacer(pairs...).Replace(md)56}5758func itoa(n int) string { return strconv.Itoa(n) }5960// plural renders "1 wave", "2 waves".61func plural(n int, word string) string {62	if n == 1 {63		return "1 " + word64	}65	return itoa(n) + " " + word + "s"66}6768// svgImage embeds an SVG as a base64 data URI, the one inline image form69// gnoweb keeps.70func svgImage(alt, svg string) string {71	return "![" + sanitize.InlineText(alt) + "](data:image/svg+xml;base64," +72		base64.StdEncoding.EncodeToString([]byte(svg)) + ")"73}7475// ----- colors -----7677// gno.land's own greens, from gnoweb's palette. An image can't follow78// gnoweb's theme toggle, so each one reads on both its white and its79// near-black background.80const (81	colBrand  = "#226C57" // gnoweb's links and buttons82	colDeep   = "#144134" // banner background, text on colSoft83	colAccent = "#60AB96" // banner accents, pill outlines84	colSoft   = "#E7EFED" // pale surface85	colGhost  = "#F0F0F0"86	fontUI    = "system-ui, -apple-system, Segoe UI, Helvetica, Arial, sans-serif"87)8889// ----- banner -----9091func bannerMD() string {92	return svgImage(profile.name+" banner", bannerSVG(runtime.ChainHeight()))93}9495// bannerSVG draws the 720x160 banner: the ghost, a coffee cup, the name and96// the tagline. The face depends on height % 4: calm, happy, wink, surprised.97func bannerSVG(height int64) string {98	var sb strings.Builder99	sb.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="720" height="160" viewBox="0 0 720 160">`)100	sb.WriteString(`<rect width="720" height="160" rx="16" fill="` + colDeep + `"/>`)101	sb.WriteString(`<path d="M60 140 V72 A50 50 0 0 1 160 72 V140 Q147.5 124 135 140 Q122.5 124 110 140 Q97.5 124 85 140 Q72.5 124 60 140 Z" fill="` + colGhost + `"/>`)102	switch height % 4 {103	case 0: // calm104		sb.WriteString(`<ellipse cx="92" cy="74" rx="6" ry="8" fill="` + colDeep + `"/>`)105		sb.WriteString(`<ellipse cx="128" cy="74" rx="6" ry="8" fill="` + colDeep + `"/>`)106		sb.WriteString(`<path d="M100 100 Q110 106 120 100" stroke="` + colDeep + `" stroke-width="4" fill="none" stroke-linecap="round"/>`)107	case 1: // happy108		sb.WriteString(`<path d="M84 78 Q92 66 100 78 M120 78 Q128 66 136 78" stroke="` + colDeep + `" stroke-width="4" fill="none" stroke-linecap="round"/>`)109		sb.WriteString(`<path d="M96 96 Q110 116 124 96 Z" fill="` + colDeep + `"/>`)110	case 2: // wink111		sb.WriteString(`<ellipse cx="92" cy="74" rx="6" ry="8" fill="` + colDeep + `"/>`)112		sb.WriteString(`<path d="M120 76 H136" stroke="` + colDeep + `" stroke-width="4" stroke-linecap="round"/>`)113		sb.WriteString(`<path d="M98 98 Q110 110 122 98" stroke="` + colDeep + `" stroke-width="4" fill="none" stroke-linecap="round"/>`)114	default: // surprised115		sb.WriteString(`<circle cx="92" cy="72" r="8" fill="` + colDeep + `"/>`)116		sb.WriteString(`<circle cx="128" cy="72" r="8" fill="` + colDeep + `"/>`)117		sb.WriteString(`<ellipse cx="110" cy="104" rx="7" ry="9" fill="` + colDeep + `"/>`)118	}119	// Coffee cup with steam: "I turn coffee into code".120	sb.WriteString(`<rect x="180" y="108" width="30" height="30" rx="5" fill="` + colAccent + `"/>`)121	sb.WriteString(`<path d="M210 114 h6 a7 7 0 0 1 0 14 h-6" stroke="` + colAccent + `" stroke-width="4" fill="none"/>`)122	sb.WriteString(`<path d="M188 100 q-5 -7 0 -14 M200 100 q-5 -7 0 -14" stroke="` + colAccent + `" stroke-width="3" fill="none" stroke-linecap="round"/>`)123	sb.WriteString(`<text x="260" y="78" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="` + fontSize(profile.name, 44, 26) + `" font-weight="700" fill="` + colGhost + `">` + html.EscapeString(profile.name) + `</text>`)124	sb.WriteString(`<text x="260" y="116" font-family="` + fontUI + `" font-size="` + fontSize(profile.tagline, 22, 12) + `" fill="` + colAccent + `">` + html.EscapeString(profile.tagline) + `</text>`)125	sb.WriteString(`</svg>`)126	return sb.String()127}128129// fontSize shrinks size so text fits the 440 px between x=260 and the130// banner's right edge. perChar is the rough glyph width at that size; runes131// from U+1100 on (CJK, emoji and the like) count double. Scaling the font,132// rather than setting textLength, never stretches text that already fits.133func fontSize(s string, size, perChar int) string {134	w := 0135	for _, r := range s {136		w += perChar137		if r >= 0x1100 {138			w += perChar139		}140	}141	if w > 440 {142		size = size * 440 / w143	}144	return itoa(size)145}146147// ----- pills: tabs, badges, buttons -----148149// Pills come in two heights: tabs, and chips (badges and buttons). gnoweb150// draws a 16 px icon on a linked image, 8 px from its left edge: images in151// a row get an 8 px margin, and the icon's center sits that far above the152// image's bottom edge. It lands on the center line of a chip about 16 px153// high, so chips are 17 px, with iconGap pixels left clear for it.154// External and $help links get the icon; tabs and the guestbook button155// link inside the realm's own namespace and don't.156const (157	tabH    = 26158	chipH   = 17159	iconGap = 30160)161162// seg is one colored part of a pill.163type seg struct{ text, fill, ink string }164165func segOn(s string) seg   { return seg{s, colBrand, "#fff"} }166func segSoft(s string) seg { return seg{s, colSoft, colDeep} }167168// pillSVG draws a pill h pixels high from segments, left to right, with169// gap empty pixels before the first text. SVG can't measure text before170// drawing, so widths come from the rune count, wide runes (U+1100 on)171// counting double. The outline gives the pale parts an edge on a white172// page.173func pillSVG(h, gap int, segs ...seg) string {174	font, char := 11, 7175	if h == tabH {176		font, char = 13, 8177	}178	widths := make([]int, len(segs))179	w := gap180	for i, s := range segs {181		widths[i] = h // half a pill's height of padding on each side182		for _, r := range s.text {183			widths[i] += char184			if r >= 0x1100 {185				widths[i] += char186			}187		}188		w += widths[i]189	}190	mid := half(h)191	var sb strings.Builder192	sb.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="` + itoa(w) + `" height="` + itoa(h) + `" viewBox="0 0 ` + itoa(w) + ` ` + itoa(h) + `">`)193	sb.WriteString(`<clipPath id="c"><rect width="` + itoa(w) + `" height="` + itoa(h) + `" rx="` + mid + `"/></clipPath>`)194	sb.WriteString(`<g clip-path="url(#c)" font-family="` + fontUI + `" font-size="` + itoa(font) + `" font-weight="600" text-anchor="middle" dominant-baseline="central">`)195	x := 0196	for i, s := range segs {197		pad := 0198		if i == 0 {199			pad = gap200		}201		sb.WriteString(`<rect x="` + itoa(x) + `" width="` + itoa(pad+widths[i]) + `" height="` + itoa(h) + `" fill="` + s.fill + `"/>`)202		sb.WriteString(`<text x="` + itoa(x+pad+widths[i]/2) + `" y="` + mid + `" fill="` + s.ink + `">` + html.EscapeString(s.text) + `</text>`)203		x += pad + widths[i]204	}205	sb.WriteString(`</g><rect x=".5" y=".5" width="` + itoa(w-1) + `" height="` + itoa(h-1) + `" rx="` + half(h-1) + `" fill="none" stroke="` + colAccent + `"/></svg>`)206	return sb.String()207}208209// half writes n/2 with its .5, for SVG coordinates.210func half(n int) string {211	if n%2 == 1 {212		return itoa(n/2) + ".5"213	}214	return itoa(n / 2)215}216217// pillLink is a pill image linked to url, with alt text for screen readers.218func pillLink(alt, url string, h, gap int, segs ...seg) string {219	return "[" + svgImage(alt, pillSVG(h, gap, segs...)) + "](" + url + ")"220}221222// badgesMD draws the badges as one paragraph. A new row starts after a223// hard line break (a backslash ending the line), so rows sit as close as224// wrapped ones.225func badgesMD() string {226	var sb strings.Builder227	for i, b := range badges {228		switch {229		case i > 0 && b.row:230			sb.WriteString("\\\n")231		case i > 0:232			sb.WriteString(" ")233		}234		sb.WriteString(pillLink(b.label+": "+b.value, b.url, chipH, iconGap, segSoft(b.label), segOn(b.value)))235	}236	return sb.String()237}238239// ----- now, buttons -----240241func nowMD() string {242	if now.text == "" {243		return ""244	}245	return "> [!SUCCESS] Now\n> " + now.text + " (block " + strconv.FormatInt(now.height, 10) + ")"246}247248// button is a linked chip: the action, then its count. $help links get249// gnoweb's transaction icon, hence the gap.250func button(action, count, url string, gap int) string {251	return pillLink(action+", "+count, url, chipH, gap, segOn(action), segSoft(count))252}253254func wavesMD() string {255	return button("Wave", plural(waves, "wave"), base+"$help&func=Wave", iconGap)256}257258func tipMD() string {259	return button("Tip 1 GNOT", plural(tips.count, "tip"), base+"$help&func=Tip&.send="+tipSuggestion, iconGap)260}261262// signMD counts hidden entries too: their authors did sign.263func signMD() string {264	return button("Sign the guestbook", plural(order.Size(), "signature"), tabURL(guestbookTab), 0)265}266267// shortAddr keeps the ends of an address: g1jg8mtu…sqf5.268func shortAddr(a address) string {269	s := a.String()270	if len(s) < 16 {271		return s272	}273	return s[:8] + "…" + s[len(s)-4:]274}275276// userLink links an address to its profile page under its short form. The277// address comes from the chain, never from visitor text.278func userLink(a address) string {279	return "[" + shortAddr(a) + "](/u/" + a.String() + ")"280}281282// ----- paging -----283284// clampPage returns the page to show, at most the last one, and the last285// page for total items.286func clampPage(page, total int) (int, int) {287	last := (total + pageSize - 1) / pageSize288	if page > last {289		page = last290	}291	return page, last292}293294// pageNav links the pages around page on tab: Newer, then Older.295func pageNav(tab string, page, last int) string {296	var nav []string297	if page > 1 {298		nav = append(nav, "[Newer]("+tabURL(tab)+"?page="+itoa(page-1)+")")299	}300	if page < last {301		nav = append(nav, "[Older]("+tabURL(tab)+"?page="+itoa(page+1)+")")302	}303	if len(nav) == 0 {304		return ""305	}306	return "\n" + strings.Join(nav, " · ") + "\n"307}308309// ----- guestbook -----310311const signForm = `<gno-form exec="Sign">312  <gno-input name="msg" type="text" placeholder="Say hi in 140 characters" required="true" />313</gno-form>`314315// visitorText makes a stored message safe as one markdown list item.316// sanitize.InlineText escapes links, images, HTML and emphasis; the escaped317// @ and the &#103; entity for the g of g1 keep gnoweb from turning "@name"318// or a typed g1 address into a profile link. Neither replacement produces319// the other's pattern, so the order doesn't matter.320func visitorText(msg string) string {321	s := strings.ReplaceAll(sanitize.InlineText(msg), "@", `\@`)322	return strings.ReplaceAll(s, "g1", "&#103;1")323}324325// guestbookMD shows the form and one page of entries, newest first. Hidden326// entries still count toward the offset, so a page can show fewer than 20.327func guestbookMD(page int) string {328	var sb strings.Builder329	sb.WriteString(signForm)330	sb.WriteString("\n\n")331	total := order.Size()332	if total == 0 {333		sb.WriteString("No entries yet.\n")334		return sb.String()335	}336	page, last := clampPage(page, total)337	order.ReverseIterateByOffset((page-1)*pageSize, pageSize, func(_ string, v any) bool {338		addr, _ := v.(address)339		e, ok := entries.Get(addr.String()).(*entry)340		if ok && !e.hidden { // Render never panics, even on a broken invariant341			sb.WriteString("- " + userLink(addr) + ": " + visitorText(e.msg) + "\n")342		}343		return false344	})345	sb.WriteString(pageNav(guestbookTab, page, last))346	return sb.String()347}348349// ----- blog -----350351// postList lists count posts from offset, newest first: one item each with352// the publish date as a code chip and the linked title.353func postList(offset, count int) string {354	var sb strings.Builder355	postOrder.ReverseIterateByOffset(offset, count, func(_ string, v any) bool {356		slug, _ := v.(string)357		if p, ok := posts.Get(slug).(*post); ok { // Render never panics, even on a broken invariant358			sb.WriteString("- `" + p.published + "` [" + sanitize.InlineText(p.title) + "](" + tabURL(blogTab) + "/" + slug + ")\n")359		}360		return false361	})362	return sb.String()363}364365// blogMD lists one page of posts.366func blogMD(page int) string {367	total := postOrder.Size()368	if total == 0 {369		return "No posts yet.\n"370	}371	page, last := clampPage(page, total)372	return postList((page-1)*pageSize, pageSize) + pageNav(blogTab, page, last)373}374375// postsMD is the Latest posts section, or nothing while there are no posts,376// so no empty heading shows.377func postsMD() string {378	if postOrder.Size() == 0 {379		return ""380	}381	return "## Latest posts\n\n" + postList(0, latestPosts) + "\n[All posts](" + tabURL(blogTab) + ")\n"382}383384// postMD is a post's page. The body is shown as written: no tokens.385func postMD(p *post) string {386	date := p.published387	if p.edited != "" && p.edited != p.published {388		date += " · edited " + p.edited389	}390	return "# " + sanitize.InlineText(p.title) + "\n\n*" + date + "*\n\n" + p.body +391		"\n\n[All posts](" + tabURL(blogTab) + ")"392}393

Functions

  • AcceptOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • Hide(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, author string)

  • Owner() string

  • RemovePage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string)

  • RemovePost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)

  • Render(path string) string

  • SetBadges(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spec string)

  • SetNow(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, text string)

  • SetPage(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tab string, md string)

  • SetPost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, title string, body string)

  • SetProfile(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, tagline string)

  • Sign(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, msg string)

  • Tip(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

  • TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newOwner string)

  • Wave(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.