render.gno

package memba_appstore_v2

import (
	"chain/runtime"

	"gno.land/p/nt/ufmt/v0"
)

// RenderMaxRows bounds the Render() output so a large catalog can never make the
// realm render-DoS (mirrors the feed's live-only bounded render).
const RenderMaxRows = 100

// Render shows the live catalog (bounded) — a read-only trust surface for gnoweb.
// The frontend reads structured data via the getters, not this markdown.
func Render(path string) string {
	sb := ""
	sb += "# Memba App Store\n\n"
	sb += ufmt.Sprintf("Curated gno.land dApps. **Listing fee:** %d ugnot → treasury.\n\n", registrationFee)
	if paused {
		sb += "> ⏸️ Registration is paused.\n\n"
	}

	rows := 0
	live := 0
	sb += "| App | Package | Category |\n|---|---|---|\n"
	listings.Iterate("", "", func(_ string, v any) bool {
		l := v.(*Listing)
		if !isVisible(l) {
			return false
		}
		live++
		if rows >= RenderMaxRows {
			return false // stop appending, keep counting via the header below
		}
		rows++
		sb += ufmt.Sprintf("| %s | `%s` | %s |\n", esc(l.Name), esc(l.PkgPath), esc(l.Category))
		return false
	})

	if live == 0 {
		sb += "\n*No live apps yet.*\n"
	} else {
		sb += ufmt.Sprintf("\n*%d live app(s)%s. Block %d.*\n", live, moreNote(live), runtime.ChainHeight())
	}
	return sb
}

func moreNote(live int) string {
	if live > RenderMaxRows {
		return ufmt.Sprintf(" (showing first %d)", RenderMaxRows)
	}
	return ""
}

// esc neutralizes the markdown/table metachars that could break the table or inject
// layout from an attacker-supplied listing field.
func esc(s string) string {
	out := ""
	for _, r := range s {
		switch r {
		case '|':
			out += "\\|"
		case '\n', '\r':
			out += " "
		case '`':
			out += "'"
		default:
			out += string(r)
		}
	}
	return out
}