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/p/moul/x/wiki/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / wiki
Files
18 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/x/wiki/v0
gno
0.9

Files (18)

  • README.mdmarkdown
  • gnomod.tomltoml
  • diff.gnogno
  • doc.gnogno
  • links.gnogno
  • page.gnogno
  • render.gnogno
  • revision.gnogno
  • talk.gnogno
  • title.gnogno
  • wiki.gnogno
  • diff_test.gnogno
  • limits_test.gnogno
  • links_test.gnogno
  • render_test.gnogno
  • talk_test.gnogno
  • title_test.gnogno
  • wiki_test.gnogno
  • render.gnogno
    1package wiki23import (4	"strings"56	"gno.land/p/moul/md/v0"7	"gno.land/p/nt/markdown/sanitize/v0"8

    Functions

    not supported for pure packages by the node (vm/qfuncs)

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

    "gno.land/p/nt/ufmt/v0"
    9)
    10
    11// DepositPerByte is the storage deposit a realm write locks per byte, in
    12// ugnot (gnolang/gno#6171). It is used only to show a reader what a page
    13// costs; the chain, not this package, does the accounting.
    14const DepositPerByte = 100
    15
    16// Action builds a transaction link for a realm function. The realm supplies
    17// it (txlink.Realm("…").Call is the usual value); a nil Action renders a page
    18// with no edit controls, which is what an archived or read-only mirror wants.
    19type Action func(fn string, args ...string) string
    20
    21// RenderArticle renders a page for reading: the sanitized body with its
    22// wikilinks resolved, a header identifying the current revision, and a footer
    23// with categories and cost.
    24func RenderArticle(c Ctx, w *Wiki, p *Page, act Action) string {
    25 var out strings.Builder
    26 out.WriteString(md.H1(p.Title.String()))
    27
    28 if p.head == nil {
    29 out.WriteString(md.Paragraph("_This page has no revisions._"))
    30 return out.String()
    31 }
    32 out.WriteString(articleMeta(c, w, p, act))
    33
    34 if p.Blanked {
    35 out.WriteString(md.Blockquote("This page was blanked. Its history is still on chain: " +
    36 md.Link("view history", c.Sub(p.Title, "history"))))
    37 return out.String()
    38 }
    39
    40 body, kept := p.Body()
    41 switch {
    42 case !kept:
    43 out.WriteString(md.Blockquote("The current revision's body is no longer held on chain. " +
    44 "Recover it from the transaction that wrote it and check it against `" + p.head.ShortHash() + "`."))
    45 case p.redirect != "":
    46 out.WriteString(md.Paragraph("Redirects to " + md.Link(p.redirect, escapeURL(c.Base+":"+strings.ReplaceAll(p.redirect, " ", "_")))))
    47 default:
    48 // sanitize.BlockRich wraps its output in blank lines on both sides,
    49 // and that padding is load-bearing: a CommonMark HTML block of type
    50 // 6 or 7 is not escaped in any mode, and without the blank line it
    51 // would swallow the footer this function appends next. Do not trim
    52 // it to tidy the output.
    53 out.WriteString(RewriteLinks(c, sanitize.BlockRich(body)))
    54 }
    55
    56 out.WriteString(articleFooter(c, w, p))
    57 return out.String()
    58}
    59
    60func articleMeta(c Ctx, w *Wiki, p *Page, act Action) string {
    61 h := p.head
    62 parts := []string{
    63 ufmt.Sprintf("rev %d", h.ID),
    64 h.Time.Format("2006-01-02 15:04"),
    65 "by " + shortAddr(h.Author),
    66 }
    67 if p.Protection != Open {
    68 parts = append(parts, p.Protection.String())
    69 }
    70 line := strings.Join(parts, " · ")
    71
    72 nav := []string{
    73 md.Link("history", c.Sub(p.Title, "history")),
    74 md.Link("source", c.Sub(p.Title, "raw")),
    75 md.Link("what links here", c.SpecialURL("Backlinks", "page="+p.Title.Slug())),
    76 }
    77 if act != nil {
    78 nav = append(nav, md.Link("edit", act("Edit", "title", p.Title.String())))
    79 }
    80 // The count sits outside the link text on purpose: md.Link sanitizes its
    81 // label, so parentheses inside it would render as "\(1\)".
    82 nav = append(nav, md.Link("discussion", c.Sub(p.Title, "talk"))+
    83 ufmt.Sprintf(" (%d)", w.NumComments(p.Title)))
    84 return md.Paragraph(line + "\n\n" + strings.Join(nav, " · "))
    85}
    86
    87func articleFooter(c Ctx, w *Wiki, p *Page) string {
    88 var out strings.Builder
    89 out.WriteString(md.HorizontalRule())
    90
    91 if len(p.cats) > 0 {
    92 cats := []string{}
    93 for _, key := range p.cats {
    94 t := Title{NS: NSCategory, Name: key[len(NSCategory.Prefix()):]}
    95 cats = append(cats, md.Link(t.Name, c.URL(t)))
    96 }
    97 out.WriteString(md.Paragraph("**Categories:** " + strings.Join(cats, " · ")))
    98 }
    99
    100 held := 0
    101 p.revs.Iterator(0, p.revs.Size()-1, func(_ int, v any) bool {
    102 r := v.(*Revision)
    103 if r.kept {
    104 held += r.Size
    105 }
    106 return false
    107 })
    108 out.WriteString(md.Paragraph(ufmt.Sprintf(
    109 "%s · %d bytes of text held on chain · %s of storage deposit · %s link here",
    110 plural(p.NumRevisions(), "revision", "revisions"),
    111 held, formatGNOT(held*DepositPerByte),
    112 plural(len(w.Backlinks(p.Title)), "page", "pages"))))
    113 return out.String()
    114}
    115
    116// RenderMissing renders the stub shown for a title with no page: the red-link
    117// destination, listing whoever already points at it.
    118func RenderMissing(c Ctx, w *Wiki, t Title, act Action) string {
    119 var out strings.Builder
    120 out.WriteString(md.H1(t.String()))
    121 out.WriteString(md.Paragraph("_This page does not exist yet._"))
    122 if act != nil {
    123 out.WriteString(md.Paragraph(md.Link("Create it", act("Edit", "title", t.String()))))
    124 }
    125 if in := w.Backlinks(t); len(in) > 0 {
    126 out.WriteString(md.H2("Pages that already link here"))
    127 out.WriteString(titleList(c, in))
    128 }
    129 return out.String()
    130}
    131
    132// RenderHistory renders a page's revision list, newest first.
    133func RenderHistory(c Ctx, p *Page, offset, count int, act Action) string {
    134 var out strings.Builder
    135 out.WriteString(md.H1("History of " + p.Title.String()))
    136 out.WriteString(md.Paragraph(md.Link("← back to the article", c.URL(p.Title))))
    137
    138 revs := p.History(offset, count)
    139 if len(revs) == 0 {
    140 out.WriteString(md.Paragraph("_No revisions in this range._"))
    141 return out.String()
    142 }
    143 items := []string{}
    144 for _, r := range revs {
    145 line := ufmt.Sprintf("**rev %d** · %s · %s · %s · %d bytes · `%s`",
    146 r.ID, r.Time.Format("2006-01-02 15:04"), shortAddr(r.Author), string(r.Kind), r.Size, r.ShortHash())
    147 if r.Summary != "" {
    148 line += "\n\n" + sanitize.InlineText(r.Summary)
    149 }
    150 extra := []string{}
    151 if r.Prev != 0 {
    152 extra = append(extra, md.Link("diff", c.Sub(p.Title, "diff")+"?from="+ufmt.Sprintf("%d", r.Prev)+"&to="+ufmt.Sprintf("%d", r.ID)))
    153 }
    154 if r.kept {
    155 extra = append(extra, md.Link("view", c.Sub(p.Title, "rev")+"/"+ufmt.Sprintf("%d", r.ID)))
    156 if act != nil {
    157 extra = append(extra, md.Link("revert to this", act("Revert",
    158 "title", p.Title.String(), "rev", ufmt.Sprintf("%d", r.ID))))
    159 }
    160 } else {
    161 extra = append(extra, "_body evicted_")
    162 }
    163 items = append(items, line+"\n\n"+strings.Join(extra, " · "))
    164 }
    165 out.WriteString(md.BulletList(items))
    166
    167 if offset+len(revs) < p.NumRevisions() {
    168 out.WriteString(md.Paragraph(md.Link("older →",
    169 c.Sub(p.Title, "history")+"?offset="+ufmt.Sprintf("%d", offset+count))))
    170 }
    171 return out.String()
    172}
    173
    174// RenderRevision renders one stored revision verbatim.
    175func RenderRevision(c Ctx, p *Page, r *Revision) string {
    176 var out strings.Builder
    177 out.WriteString(md.H1(ufmt.Sprintf("%s: revision %d", p.Title.String(), r.ID)))
    178 out.WriteString(md.Paragraph(ufmt.Sprintf("%s · %s · `%s`",
    179 r.Time.Format("2006-01-02 15:04"), shortAddr(r.Author), r.Hash)))
    180 body, kept := r.Body()
    181 if !kept {
    182 out.WriteString(md.Blockquote("This revision's body is no longer held on chain."))
    183 return out.String()
    184 }
    185 out.WriteString(RewriteLinks(c, sanitize.BlockRich(body)))
    186 return out.String()
    187}
    188
    189// RenderRaw renders a revision's source inside a code block, which is what a
    190// reader needs before editing and what a verifier needs to re-hash.
    191func RenderRaw(c Ctx, p *Page, r *Revision) string {
    192 var out strings.Builder
    193 out.WriteString(md.H1(ufmt.Sprintf("Source of %s (rev %d)", p.Title.String(), r.ID)))
    194 out.WriteString(md.Paragraph("sha256 `" + r.Hash + "`"))
    195 body, kept := r.Body()
    196 if !kept {
    197 out.WriteString(md.Blockquote("This revision's body is no longer held on chain."))
    198 return out.String()
    199 }
    200 out.WriteString(sanitize.CodeBlock(body))
    201 out.WriteString(md.Paragraph(md.Link("← back to the article", c.URL(p.Title))))
    202 return out.String()
    203}
    204
    205// RenderDiff renders the line diff between two revisions of a page.
    206func RenderDiff(c Ctx, p *Page, from, to *Revision) string {
    207 var out strings.Builder
    208 out.WriteString(md.H1(ufmt.Sprintf("%s: rev %d → rev %d", p.Title.String(), from.ID, to.ID)))
    209
    210 a, okA := from.Body()
    211 b, okB := to.Body()
    212 if !okA || !okB {
    213 out.WriteString(md.Blockquote("One of these revisions' bodies is no longer held on chain, so the diff cannot be computed. " +
    214 "Their hashes are `" + from.ShortHash() + "` and `" + to.ShortHash() + "`."))
    215 return out.String()
    216 }
    217
    218 lines, exact := DiffLines(a, b)
    219 added, removed := DiffStat(lines)
    220 note := ufmt.Sprintf("+%d −%d lines", added, removed)
    221 if !exact {
    222 note += " · changed region larger than " + ufmt.Sprintf("%d", DiffMaxLines) + " lines, shown as a block replacement"
    223 }
    224 out.WriteString(md.Paragraph(note))
    225
    226 var d strings.Builder
    227 for _, l := range lines {
    228 switch l.Op {
    229 case OpInsert:
    230 d.WriteString("+" + l.Text + "\n")
    231 case OpDelete:
    232 d.WriteString("-" + l.Text + "\n")
    233 default:
    234 d.WriteString(" " + l.Text + "\n")
    235 }
    236 }
    237 out.WriteString(sanitize.LanguageCodeBlock("diff", d.String()))
    238 out.WriteString(md.Paragraph(md.Link("← back to the article", c.URL(p.Title))))
    239 return out.String()
    240}
    241
    242// RenderIndex renders the wiki's front page: recent changes and a page count.
    243func RenderIndex(c Ctx, w *Wiki, recent int) string {
    244 var out strings.Builder
    245 s := w.Stats()
    246 out.WriteString(md.H1("Wiki"))
    247 out.WriteString(md.Paragraph(strings.Join([]string{
    248 md.Link("all pages", c.SpecialURL("AllPages", "")),
    249 md.Link("categories", c.SpecialURL("Categories", "")),
    250 md.Link("recent changes", c.SpecialURL("RecentChanges", "")),
    251 md.Link("stats", c.SpecialURL("Stats", "")),
    252 }, " · ")))
    253 out.WriteString(md.Paragraph(ufmt.Sprintf("%d pages · %d revisions · %d bytes on chain",
    254 s.Pages, s.Revisions, s.BytesHeld)))
    255 out.WriteString(md.H2("Recent changes"))
    256 out.WriteString(changeList(c, w.Recent(recent)))
    257 return out.String()
    258}
    259
    260// RenderRecent renders the recent-changes feed.
    261func RenderRecent(c Ctx, w *Wiki, n int) string {
    262 return md.H1("Recent changes") + changeList(c, w.Recent(n))
    263}
    264
    265// RenderAllPages renders the page index for a namespace prefix.
    266func RenderAllPages(c Ctx, w *Wiki, ns Namespace, offset, count int) string {
    267 var out strings.Builder
    268 out.WriteString(md.H1("All pages"))
    269
    270 tabs := []string{}
    271 for i := range namespaces {
    272 n := Namespace(i)
    273 if n == NSSpecial {
    274 continue
    275 }
    276 label := n.String()
    277 if label == "" {
    278 label = "Articles"
    279 }
    280 if n == ns {
    281 label = "**" + label + "**"
    282 }
    283 tabs = append(tabs, md.Link(label, c.SpecialURL("AllPages", "ns="+ufmt.Sprintf("%d", i))))
    284 }
    285 out.WriteString(md.Paragraph(strings.Join(tabs, " · ")))
    286
    287 titles := w.Titles(ns.Prefix(), offset, count)
    288 if len(titles) == 0 {
    289 out.WriteString(md.Paragraph("_No pages in this namespace._"))
    290 return out.String()
    291 }
    292 out.WriteString(titleList(c, titles))
    293 if len(titles) == count {
    294 out.WriteString(md.Paragraph(md.Link("next →",
    295 c.SpecialURL("AllPages", ufmt.Sprintf("ns=%d&offset=%d", uint8(ns), offset+count)))))
    296 }
    297 return out.String()
    298}
    299
    300// RenderCategory renders a category page: its own text, then its members.
    301func RenderCategory(c Ctx, w *Wiki, t Title, p *Page, act Action) string {
    302 var out strings.Builder
    303 if p != nil {
    304 out.WriteString(RenderArticle(c, w, p, act))
    305 } else {
    306 out.WriteString(md.H1(t.String()))
    307 out.WriteString(md.Paragraph("_This category has no description page._"))
    308 }
    309 members := w.CategoryMembers(t)
    310 out.WriteString(md.H2(ufmt.Sprintf("Pages in this category (%d)", len(members))))
    311 if len(members) == 0 {
    312 out.WriteString(md.Paragraph("_None._"))
    313 return out.String()
    314 }
    315 out.WriteString(titleList(c, members))
    316 return out.String()
    317}
    318
    319// RenderCategories lists every category that has at least one member.
    320func RenderCategories(c Ctx, w *Wiki) string {
    321 cats := w.Categories()
    322 var out strings.Builder
    323 out.WriteString(md.H1("Categories"))
    324 if len(cats) == 0 {
    325 out.WriteString(md.Paragraph("_No categories yet._"))
    326 return out.String()
    327 }
    328 items := []string{}
    329 for _, t := range cats {
    330 items = append(items, ufmt.Sprintf("%s (%d)", md.Link(t.Name, c.URL(t)), len(w.CategoryMembers(t))))
    331 }
    332 out.WriteString(md.BulletList(items))
    333 return out.String()
    334}
    335
    336// RenderBacklinks renders "what links here" for a title.
    337func RenderBacklinks(c Ctx, w *Wiki, t Title) string {
    338 var out strings.Builder
    339 out.WriteString(md.H1("Pages that link to " + t.String()))
    340 out.WriteString(md.Paragraph(md.Link("← back to the article", c.URL(t))))
    341 in := w.Backlinks(t)
    342 if len(in) == 0 {
    343 out.WriteString(md.Paragraph("_Nothing links here._"))
    344 return out.String()
    345 }
    346 out.WriteString(titleList(c, in))
    347 return out.String()
    348}
    349
    350// RenderStats renders the wiki's size and what it is paying the chain.
    351func RenderStats(c Ctx, w *Wiki) string {
    352 s := w.Stats()
    353 rows := []string{
    354 ufmt.Sprintf("pages: %d", s.Pages),
    355 ufmt.Sprintf("revisions: %d", s.Revisions),
    356 ufmt.Sprintf("comments: %d", s.Comments),
    357 ufmt.Sprintf("body bytes on chain: %d", s.BytesHeld),
    358 ufmt.Sprintf("storage deposit locked by bodies: %s", formatGNOT(s.BytesHeld*DepositPerByte)),
    359 ufmt.Sprintf("bodies kept per page: %d", s.Retention),
    360 }
    361 return md.H1("Wiki stats") + md.BulletList(rows)
    362}
    363
    364// RenderTalk renders a page's discussion: top-level messages oldest first,
    365// each with its replies.
    366func RenderTalk(c Ctx, w *Wiki, p *Page, offset, count int, act Action) string {
    367 var out strings.Builder
    368 out.WriteString(md.H1("Discussion: " + p.Title.String()))
    369
    370 nav := []string{md.Link("← back to the article", c.URL(p.Title))}
    371 if act != nil {
    372 nav = append(nav, md.Link("add a message", act("Comment", "title", p.Title.String(), "replyTo", "0")))
    373 }
    374 out.WriteString(md.Paragraph(strings.Join(nav, " · ")))
    375
    376 threads := w.Comments(p.Title, offset, count)
    377 if len(threads) == 0 {
    378 out.WriteString(md.Paragraph("_No messages yet._"))
    379 return out.String()
    380 }
    381
    382 items := []string{}
    383 for _, th := range threads {
    384 item := commentLine(c, th.Root, act, p.Title)
    385 for _, r := range th.Replies {
    386 item += "\n" + md.Nested(commentLine(c, r, nil, p.Title), " - ")
    387 }
    388 items = append(items, item)
    389 }
    390 out.WriteString(md.BulletList(items))
    391
    392 total := w.NumComments(p.Title)
    393 if offset+len(threads) < total {
    394 out.WriteString(md.Paragraph(md.Link("older →",
    395 c.Sub(p.Title, "talk")+"?offset="+ufmt.Sprintf("%d", offset+count))))
    396 }
    397 return out.String()
    398}
    399
    400func commentLine(c Ctx, cm *Comment, act Action, t Title) string {
    401 head := ufmt.Sprintf("**#%d** · %s · %s",
    402 cm.ID, cm.Time.Format("2006-01-02 15:04"), shortAddr(cm.Author))
    403 if cm.Hidden {
    404 return head + "\n\n_This message was removed by a steward._"
    405 }
    406 body := sanitize.Block(cm.Body)
    407 if act != nil {
    408 body += "\n\n" + md.Link("reply", act("Comment",
    409 "title", t.String(), "replyTo", ufmt.Sprintf("%d", cm.ID)))
    410 }
    411 return head + body
    412}
    413
    414func titleList(c Ctx, titles []Title) string {
    415 items := []string{}
    416 for _, t := range titles {
    417 items = append(items, md.Link(t.String(), c.URL(t)))
    418 }
    419 return md.BulletList(items)
    420}
    421
    422func changeList(c Ctx, changes []*Change) string {
    423 if len(changes) == 0 {
    424 return md.Paragraph("_Nothing has happened yet._")
    425 }
    426 items := []string{}
    427 for _, ch := range changes {
    428 line := ufmt.Sprintf("%s · %s · %s · %s · rev %d",
    429 md.Link(ch.Title.String(), c.URL(ch.Title)),
    430 string(ch.Rev.Kind),
    431 ch.Rev.Time.Format("2006-01-02 15:04"),
    432 shortAddr(ch.Rev.Author),
    433 ch.Rev.ID)
    434 if ch.Rev.Summary != "" {
    435 line += " · " + sanitize.InlineText(ch.Rev.Summary)
    436 }
    437 items = append(items, line)
    438 }
    439 return md.BulletList(items)
    440}
    441
    442// shortAddr abbreviates an address for display without losing its prefix.
    443func shortAddr(a address) string {
    444 s := a.String()
    445 if len(s) <= 12 {
    446 return "`" + s + "`"
    447 }
    448 return "`" + s[:10] + "…" + s[len(s)-4:] + "`"
    449}
    450
    451// formatGNOT renders a ugnot amount as GNOT.
    452//
    453// The zero padding is written out by hand: gno's ufmt supports no width or
    454// padding flags, so ufmt.Sprintf("%06d", 42) returns "42" silently and the
    455// fractional part of every amount would be wrong by three orders of magnitude.
    456func formatGNOT(ugnot int) string {
    457 whole := ugnot / 1000000
    458 frac := ugnot % 1000000
    459 s := ufmt.Sprintf("%d", frac)
    460 for len(s) < 6 {
    461 s = "0" + s
    462 }
    463 s = strings.TrimRight(s, "0")
    464 if s == "" {
    465 return ufmt.Sprintf("%d GNOT", whole)
    466 }
    467 return ufmt.Sprintf("%d.%s GNOT", whole, s)
    468}
    469
    470// plural renders "1 revision" and "2 revisions".
    471func plural(n int, one, many string) string {
    472 if n == 1 {
    473 return ufmt.Sprintf("%d %s", n, one)
    474 }
    475 return ufmt.Sprintf("%d %s", n, many)
    476}
    477