1// Package urlshort is a simple on-chain URL alias registry.2//3// Callers register an `alias -> url` mapping they own. An alias can only be4// claimed once; the owner may later update the target URL, but nobody else can5// overwrite an alias they do not own. A per-alias click counter is bumped every6// time the alias is resolved through Render("/<alias>").7package urlshort89import (10 "strings"1112 "chain"13 "chain/runtime/unsafe"1415 "gno.land/p/nt/avl/v0"16)1718// entry is the persisted record for a single alias.19type entry struct {20 url string21 owner address22 note string23 clicks int24}2526// aliases maps alias (string) -> *entry, kept in an avl.Tree for deterministic27// iteration order in Render.28var aliases avl.Tree2930// isAlphanumeric reports whether s is non-empty and made only of [0-9A-Za-z].31func isAlphanumeric(s string) bool {32 if s == "" {33 return false34 }35 for _, c := range s {36 switch {37 case c >= '0' && c <= '9':38 case c >= 'a' && c <= 'z':39 case c >= 'A' && c <= 'Z':40 default:41 return false42 }43 }44 return true45}4647// Shorten registers `alias -> url` owned by the caller.48//49// Rules:50// - alias must be non-empty and alphanumeric;51// - url must be non-empty;52// - if the alias is unclaimed, it is created owned by the caller;53// - if it is already claimed by the caller, the url/note are updated;54// - if it is claimed by someone else, the call aborts.55func Shorten(cur realm, alias string, url string, note string) {56 if !isAlphanumeric(alias) {57 panic("alias must be non-empty and alphanumeric")58 }59 if url == "" {60 panic("url must be non-empty")61 }6263 caller := unsafe.PreviousRealm().Address()6465 if v := aliases.Get(alias); v != nil {66 e := v.(*entry)67 if e.owner != caller {68 panic("alias already taken by another owner")69 }70 e.url = url71 e.note = note72 chain.Emit("AliasUpdated", "alias", alias, "owner", caller.String())73 return74 }7576 e := &entry{url: url, owner: caller, note: note, clicks: 0}77 aliases.Set(alias, e)78 chain.Emit("AliasCreated", "alias", alias, "owner", caller.String())79}8081// Remove deletes an alias owned by the caller. Aborts if the alias does not82// exist or the caller is not the owner.83func Remove(cur realm, alias string) {84 v := aliases.Get(alias)85 if v == nil {86 panic("alias not found")87 }88 e := v.(*entry)89 caller := unsafe.PreviousRealm().Address()90 if e.owner != caller {91 panic("only the owner can remove an alias")92 }93 aliases.Remove(alias)94 chain.Emit("AliasRemoved", "alias", alias, "owner", caller.String())95}9697// Lookup returns the target url for an alias and whether it exists. It is a98// read-only helper and does NOT increment the click counter.99func Lookup(alias string) (string, bool) {100 v := aliases.Get(alias)101 if v == nil {102 return "", false103 }104 return v.(*entry).url, true105}106107// Render renders Markdown. The root path lists every alias; "/<alias>" shows a108// single alias detail and bumps its click counter.109func Render(path string) string {110 if path == "" || path == "/" {111 return renderIndex()112 }113114 alias := strings.TrimPrefix(path, "/")115 v := aliases.Get(alias)116 if v == nil {117 return "# URL Shortener\n\nNo alias named `" + alias + "`.\n\n[← all aliases](/)\n"118 }119120 e := v.(*entry)121 e.clicks++122123 var b strings.Builder124 b.WriteString("# 🔗 " + alias + "\n\n")125 b.WriteString("**Target:** <" + e.url + ">\n\n")126 b.WriteString("**Owner:** " + e.owner.String() + "\n\n")127 if e.note != "" {128 b.WriteString("**Note:** " + e.note + "\n\n")129 }130 b.WriteString("**Clicks:** " + itoa(e.clicks) + "\n\n")131 b.WriteString("[← all aliases](/)\n")132 return b.String()133}134135func renderIndex() string {136 var b strings.Builder137 b.WriteString("# 🔗 URL Shortener\n\n")138139 if aliases.Size() == 0 {140 b.WriteString("_No aliases registered yet._\n\n")141 b.WriteString("Call `Shorten(alias, url, note)` to register one.\n")142 return b.String()143 }144145 b.WriteString("| Alias | Target | Owner | Clicks |\n")146 b.WriteString("|-------|--------|-------|--------|\n")147 aliases.Iterate("", "", func(key string, value interface{}) bool {148 e := value.(*entry)149 b.WriteString("| [" + key + "](/" + key + ") | <" + e.url + "> | " +150 short(e.owner.String()) + " | " + itoa(e.clicks) + " |\n")151 return false152 })153 b.WriteString("\nTotal aliases: " + itoa(aliases.Size()) + "\n")154 return b.String()155}156157// short truncates a long address for the index table.158func short(s string) string {159 if len(s) <= 12 {160 return s161 }162 return s[:6] + "…" + s[len(s)-4:]163}164165// itoa converts a non-negative int to its decimal string without importing166// strconv (kept minimal & deterministic).167func itoa(n int) string {168 if n == 0 {169 return "0"170 }171 neg := n < 0172 if neg {173 n = -n174 }175 var buf [20]byte176 i := len(buf)177 for n > 0 {178 i--179 buf[i] = byte('0' + n%10)180 n /= 10181 }182 if neg {183 i--184 buf[i] = '-'185 }186 return string(buf[i:])187}188Lookup(alias string) (string, bool)
Remove(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}, alias string)
Render(path string) string
Shorten(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}, alias string, url string, note string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
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.