Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

render.gno

20.53 Kb · 582 lines
  1package kourtv3
  2import (
  3	"strconv"
  4	"strings"
  5	sanitize "gno.land/p/nt/markdown/sanitize/v0"
  6)
  7func Render(path string) string {
  8	return withSiteBanner(render(path), path)
  9}
 10func render(path string) string {
 11	path = strings.Trim(path, "/")
 12	if path == "" {
 13		return renderDirectory()
 14	}
 15	parts := strings.SplitN(path, "/", 4)
 16	if len(parts) == 1 {
 17		if parts[0] == helpPath {
 18			return renderHelp()
 19		}
 20		if parts[0] == adminParamsPath {
 21			return renderAdminParams()
 22		}
 23		return renderCourt(parts[0])
 24	}
 25	if parts[1] == "mod" {
 26		return renderModLog(parts[0])
 27	}
 28	if parts[1] == electionPath {
 29		if len(parts) != 2 {
 30			return "## Not found\nThe ballot is /<court>/election."
 31		}
 32		return renderElection(parts[0])
 33	}
 34	if parts[1] == "folder" {
 35		if len(parts) != 3 {
 36			return "## Not found\nA folder is /<court>/folder/<id>."
 37		}
 38		fid, ferr := strconv.ParseUint(parts[2], 10, 64)
 39		if ferr != nil {
 40			return "## Not found\nA folder id must be a number."
 41		}
 42		return renderFolderPage(parts[0], fid)
 43	}
 44	if parts[1] == "me" {
 45		if len(parts) == 3 {
 46			return renderStanding(parts[0], parts[2])
 47		}
 48		return renderStandingIndex(parts[0])
 49	}
 50	id, err := strconv.ParseUint(parts[1], 10, 64)
 51	if err != nil {
 52		return "## Not found\nA claim id must be a number."
 53	}
 54	if len(parts) >= 3 && parts[2] == "board" {
 55		if len(parts) == 4 {
 56			if parts[3] == "top" {
 57				return renderBoardTop(parts[0], id)
 58			}
 59			if parts[3] == "hidden" {
 60				return renderBoardHidden(parts[0], id)
 61			}
 62			return renderBoardRow(parts[0], id, parts[3])
 63		}
 64		return renderBoardPage(parts[0], id)
 65	}
 66	if len(parts) == 4 {
 67		return "## Not found\nThat is not a page."
 68	}
 69	if len(parts) == 3 {
 70		return renderPositions(parts[0], id, parts[2])
 71	}
 72	return renderClaim(parts[0], id)
 73}
 74func renderDirectory() string {
 75	b := strings.Builder{}
 76	b.WriteString("# " + platformName + "\n\nLet Truth be told.\n\n")
 77	b.WriteString("Stake on claims of fact. Your principal always returns 1×.\n\n")
 78	b.WriteString(helpLink + "\n\n")
 79	afterHeader := b.Len()
 80	writeTier(&b, "Featured", tierFeatured)
 81	writeListedByBurn(&b)
 82	if b.Len() == afterHeader {
 83		b.WriteString("_No courts yet._\n")
 84	}
 85	return b.String()
 86}
 87func writeListedByBurn(b *strings.Builder) {
 88	slugs := listedPage("burn", 0, renderPageSize)
 89	if len(slugs) == 0 {
 90		return
 91	}
 92	b.WriteString("## Courts\n\n")
 93	shown := 0
 94	for _, s := range slugs {
 95		c := mustCourt(s)
 96		if c.tier != tierListed {
 97			continue
 98		}
 99		shown++
100		b.WriteString("- [" + courtNameFor(c) + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + s + ") · " +
101			qualifiedSymbol(c) + " — " + strconv.FormatUint(c.nextID, 10) + " claims\n")
102	}
103	if total := len(ListByTier(int(tierListed))); total > shown {
104		b.WriteString("- _…and " + strconv.Itoa(total-shown) +
105			" more; open any court directly at /r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:<slug>_\n")
106	}
107	b.WriteString("\n")
108}
109const renderPageSize = 50
110func writeTier(b *strings.Builder, heading string, tier uint8) {
111	slugs := ListByTier(int(tier))
112	if len(slugs) == 0 {
113		return
114	}
115	b.WriteString("## " + heading + "\n\n")
116	shown := len(slugs)
117	if shown > renderPageSize {
118		shown = renderPageSize
119	}
120	for _, s := range slugs[:shown] {
121		c := mustCourt(s)
122		b.WriteString("- [" + courtNameFor(c) + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + s + ") · " +
123			qualifiedSymbol(c) + " — " + strconv.FormatUint(c.nextID, 10) + " claims\n")
124	}
125	if len(slugs) > shown {
126		b.WriteString("- _…and " + strconv.Itoa(len(slugs)-shown) +
127			" more; open any court directly at /r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:<slug>_\n")
128	}
129	b.WriteString("\n")
130}
131func renderCourt(slug string) string {
132	if !Exists(slug) {
133		return "## Not found\nNo court by that slug."
134	}
135	c := mustCourt(slug)
136	b := strings.Builder{}
137	b.WriteString("# " + courtNameFor(c) + "\n\n")
138	if c.tier == tierHidden {
139		b.WriteString("> **Delisted by the global DAO.** This court appears in no " +
140			"listing and is reachable only by direct link. Nothing else changes: " +
141			"its claims still stake, answer, settle and pay out as usual.\n\n")
142	}
143	if d := courtDescFor(c); d != "" {
144		b.WriteString(d + "\n\n")
145	}
146	writeFolderIndex(&b, c, slug)
147	b.WriteString("## Claims\n\n")
148	if c.nextID == 0 {
149		b.WriteString("_No claims yet._\n\n")
150		writeCourtCoin(&b, c)
151		return b.String()
152	}
153	lo := uint64(1)
154	if c.nextID > renderPageSize {
155		lo = c.nextID - renderPageSize + 1
156	}
157	for id := c.nextID; id >= lo; id-- {
158		cs := mustClaim(c, id)
159		if HiddenFromListing(slug, id) {
160			continue
161		}
162		b.WriteString("- [" + claimTitleFor(c, cs) + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug + "/" +
163			strconv.FormatUint(id, 10) + ") — " + claimStatus(cs) + "\n")
164	}
165	if lo > 1 {
166		b.WriteString("- _…and " + strconv.FormatUint(lo-1, 10) +
167			" older; open any by id at /r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug + "/<id>_\n")
168	}
169	b.WriteString("\n")
170	writeStrip(&b, slug)
171	writePending(&b, slug)
172	writeCourtCoin(&b, c)
173	b.WriteString("[Moderation log](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug + "/mod)\n")
174	return b.String()
175}
176func writeCourtCoin(b *strings.Builder, c *Court) {
177	b.WriteString("## This court's coin\n\n")
178	b.WriteString("- " + qualifiedSymbol(c) +
179		" — this court's own coin, not interchangeable with any other court's\n")
180	nextCoin, ok := c.crv.Cost(c.minted, coinUnit)
181	if ok {
182		b.WriteString("- price: " + strconv.FormatInt(nextCoin, 10) +
183			" ugnot per coin — the next coin's price, and it only rises\n")
184	} else {
185		b.WriteString("- price: the curve is at its cap; no more coin can be minted\n")
186	}
187	if c.oneWay {
188		b.WriteString("- one-way: every payment burns in full; nothing is held and " +
189			"nothing returns GNOT\n")
190	} else {
191		perCoin, _ := redeemQuote(c, coinUnit)
192		b.WriteString("- held for the coin: " + ugnotAmt(c.reserve) + " GNOT · " +
193			"one coin returns " + strconv.FormatInt(perCoin, 10) +
194			" ugnot right now (what is held × coin ÷ supply, rounded down; " +
195			"below the next coin's price unless coin has been destroyed by bonds " +
196			"since the last offering) · " + pct(burnBps, bpsDenom) +
197			" of each payment burns\n")
198	}
199	b.WriteString("- in circulation: " + strconv.FormatInt(c.coin.TotalSupply(), 10) +
200		" · paid out as rewards so far: " + strconv.FormatInt(c.emittedTotal, 10) + "\n")
201	b.WriteString("- reward reservoir: " + strconv.FormatInt(c.reservoirR(), 10) +
202		" waiting to be earned · " + strconv.FormatInt(c.seniorOwed, 10) +
203		" already owed to earlier earners\n\n")
204	b.WriteString(helpLink + "\n\n")
205}
206func renderClaim(slug string, id uint64) string {
207	if !Exists(slug) {
208		return "## Not found\nNo court by that slug."
209	}
210	c := mustCourt(slug)
211	if id == 0 || id > c.nextID {
212		return "## Not found\nNo claim by that id."
213	}
214	cs := mustClaim(c, id)
215	b := strings.Builder{}
216	b.WriteString("# " + claimTitleFor(c, cs) + "\n\n")
217	if body := claimBodyQuoted(c, cs); body != "" {
218		b.WriteString(body)
219	}
220	writeClaimMedia(&b, c, cs)
221	b.WriteString(hideBanner(c, cs))
222	writeRelations(&b, c, id)
223	writeAppeal(&b, c, cs)
224	if cs.seeded {
225		b.WriteString("_Seeded by a moderator to start this court; the author earns nothing from it._\n\n")
226	}
227	b.WriteString("## Signal\n\n")
228	if cs.frozenAt != 0 {
229		fTotal := cs.yesStakeAtFreeze + cs.noStakeAtFreeze
230		b.WriteString("- staked when answered: " + strconv.FormatInt(fTotal, 10) +
231			" " + qualifiedSymbol(c) + " (YES " + strconv.FormatInt(cs.yesStakeAtFreeze, 10) +
232			" / NO " + strconv.FormatInt(cs.noStakeAtFreeze, 10) + ")\n")
233		if fTotal > 0 {
234			b.WriteString("- lean when answered: " + pct(cs.yesStakeAtFreeze, fTotal) + " YES\n")
235		}
236		if live := cs.yesStake + cs.noStake; live > 0 {
237			b.WriteString("- still unwithdrawn: " + strconv.FormatInt(live, 10) +
238				" " + qualifiedSymbol(c) + "\n")
239		}
240	} else {
241		total := cs.yesStake + cs.noStake
242		b.WriteString("- staked now: " + strconv.FormatInt(total, 10) +
243			" " + qualifiedSymbol(c) + " (YES " + strconv.FormatInt(cs.yesStake, 10) +
244			" / NO " + strconv.FormatInt(cs.noStake, 10) + ")\n")
245		if total > 0 {
246			b.WriteString("- right now: " + pct(cs.yesStake, total) + " YES\n")
247		}
248		b.WriteString("- pays: not set until the claim is answered — it is " +
249			"this claim's total stake against a typical claim in this court, " +
250			"so staking now is what sets it\n")
251	}
252	h := heightNow()
253	if yAvg, ym := cs.yes.Average(h, periodBlocks); ym {
254		if tAvg, tm := cs.oi.Average(h, periodBlocks); tm && tAvg > 0 {
255			b.WriteString("- trailing week: " + pct(yAvg, tAvg) + " YES\n")
256		}
257	}
258	yc := convToCC(cs.yesConvHi, cs.yesConvLo)
259	nc := convToCC(cs.noConvHi, cs.noConvLo)
260	if yc+nc > 0 {
261		b.WriteString("- lifetime, weighted by how long each stake was held: " + pct(yc, yc+nc) + " YES\n")
262	}
263	b.WriteString("\n## Status\n\n" + claimStatus(cs) + "\n")
264	if cs.frozenAt != 0 {
265		b.WriteString("\n## Resolution\n\n")
266		b.WriteString("- answer: " + sideName(int(cs.answer)) + " (bond " +
267			strconv.FormatInt(cs.answerBond0, 10) + " " + qualifiedSymbol(c) + ")\n")
268		b.WriteString("- the answerer has been challenged and upheld " +
269			strconv.FormatInt(int64(AnswerRecord(slug, cs.answerer)), 10) +
270			" time(s) in this court — answers nobody contested do not count\n")
271		if PriorityGateActive(slug) {
272			b.WriteString("- answer priority is live here — a record of " +
273				strconv.FormatInt(int64(priorityNetRecord), 10) +
274				"+ earns a 24h head start on new claims\n")
275		}
276		if cs.failedRounds > 0 {
277			b.WriteString("- failed dispute rounds: " + strconv.FormatInt(cs.failedRounds, 10) +
278				" of " + strconv.FormatInt(int64(maxFailedRounds), 10) +
279				" — the next dispute bond doubles\n")
280		}
281		if cs.disputeOpen {
282			b.WriteString("- dispute round " + strconv.FormatInt(cs.round, 10) +
283				" vote open — no running total is shown here until it closes\n")
284		} else if cs.verdictAt == 0 {
285			b.WriteString("- disputing this answer costs a bond of " +
286				strconv.FormatInt(cs.disputeBond0(), 10) + " " + qualifiedSymbol(c) +
287				", doubling for each round that fails to reach a quorum\n")
288			b.WriteString("  - win and the answerer's bond is burned and you are " +
289				"compensated, up to twice your own bond; lose and yours burns " +
290				"instead. Nothing passes between the two of you — a forfeit is " +
291				"burned, a compensation is newly issued\n")
292			b.WriteString("  - if the round draws no quorum, half your bond burns " +
293				"and half returns; three such rounds close the claim undecided, " +
294				"with every stake out at 1× and the deposit and fee refunded\n")
295			b.WriteString("  - **your stake is not at risk either way** — only the " +
296				"bond is\n")
297		}
298		if cs.verdictAt != 0 && !cs.provClose {
299			b.WriteString("- verdict: " + sideName(int(cs.provisional)) + " — " + cs.route + "\n")
300		}
301		if cs.tierRef <= 0 {
302			b.WriteString("- pays " + tierText(tierParBps) +
303				" — this claim was answered before ratings were recorded, so it " +
304				"pays the standard amount\n")
305		} else {
306			b.WriteString("- pays " + tierText(tierBpsFor(cs)) +
307				" — this claim held " + strconv.FormatInt(cs.xBarFrozen, 10) +
308				" " + qualifiedSymbol(c) + " against a typical " +
309				strconv.FormatInt(cs.tierRef, 10) + " " + qualifiedSymbol(c) + " here\n")
310			b.WriteString("- the rating is the money that showed up, not a vote: " +
311				"a claim of typical size pays 1.00×, and it is capped at 0.25× and 2.00×\n")
312			if cs.spamTotalW > 0 && cs.spamW > 0 {
313				b.WriteString("- and " + tierText(spamNetBps(cs)) +
314					" of that, because " + strconv.FormatInt(cs.spamW, 10) +
315					" of " + strconv.FormatInt(cs.spamTotalW, 10) +
316					" of the weight cast flagged it as spam\n")
317			}
318		}
319		switch {
320		}
321		if cs.rewardsOpened {
322			w, a, ans, carrot := cs.drawWinners, cs.drawAuthor, cs.drawAnswerer, cs.carrotPool
323			b.WriteString("- rewards open — pools (" + qualifiedSymbol(c) +
324				"): accuracy " + strconv.FormatInt(w, 10) +
325				", author " + strconv.FormatInt(a, 10) + ", answerer " + strconv.FormatInt(ans, 10) +
326				", participation remaining " + strconv.FormatInt(carrot, 10) + "\n")
327		}
328	}
329	b.WriteString("\n_To see your own stake on this claim, put your address on " +
330		"the end of this page's path:_ `/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + sanitize.InlineText(slug) +
331		"/" + strconv.FormatUint(id, 10) + "/<your address>`\n\n")
332	writeBoardLink(&b, c, cs, id)
333	return b.String()
334}
335func renderPositions(slug string, id uint64, addrStr string) string {
336	if !Exists(slug) {
337		return "## Not found\nNo court by that slug."
338	}
339	c := mustCourt(slug)
340	if id == 0 || id > c.nextID {
341		return "## Not found\nNo claim by that id."
342	}
343	who := address(addrStr)
344	if !who.IsValid() {
345		return "## Not found\nThat is not a valid address."
346	}
347	cs := mustClaim(c, id)
348	b := strings.Builder{}
349	b.WriteString("# Positions on this claim\n\n")
350	b.WriteString(hideBanner(c, cs))
351	b.WriteString("Claim: [" + claimTitleFor(c, cs) + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug +
352		"/" + strconv.FormatUint(id, 10) + ") in [" + courtNameFor(c) +
353		"](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug + ")\n\n")
354	b.WriteString("Address: " + sanitize.InlineText(addrStr) + "\n\n")
355	yStake := StakeOf(slug, id, sideYES, who)
356	nStake := StakeOf(slug, id, sideNO, who)
357	yConv := ConvictionOf(slug, id, sideYES, who)
358	nConv := ConvictionOf(slug, id, sideNO, who)
359	b.WriteString("## Stake (what you have in, and always get back)\n\n")
360	b.WriteString("- YES: " + strconv.FormatInt(yStake, 10) +
361		"   NO: " + strconv.FormatInt(nStake, 10) + " (" + qualifiedSymbol(c) +
362		" base units)\n")
363	b.WriteString("- conviction so far — YES: " + strconv.FormatInt(yConv, 10) +
364		"   NO: " + strconv.FormatInt(nConv, 10) + " (the reward weight)\n")
365	bal := c.coin.BalanceOf(who)
366	locked := lockedOf(c, who)
367	voted := voteLockedOf(c, who)
368	b.WriteString("\n## This address's " + qualifiedSymbol(c) +
369		" in this court (all claims)\n\n")
370	b.WriteString("- held: " + strconv.FormatInt(bal, 10) +
371		" — every unit votes, committed or not\n")
372	b.WriteString("- committed as stake: " + strconv.FormatInt(locked, 10) + "\n")
373	b.WriteString("- committed by voting: " + strconv.FormatInt(voted, 10) +
374		" — until each question resolves\n")
375	b.WriteString("- free to stake: " + strconv.FormatInt(spendable(c, who), 10) + "\n")
376	b.WriteString("- free to bond, deposit or transfer: " +
377		strconv.FormatInt(disposable(c, who), 10) + "\n")
378	roles := ""
379	if who == cs.author {
380		roles += "author "
381	}
382	if cs.frozenAt != 0 && who == cs.answerer {
383		roles += "answerer "
384	}
385	if roles != "" {
386		b.WriteString("- roles: " + roles + "\n")
387	}
388	if rec := AnswerRecord(slug, who); rec > 0 {
389		b.WriteString("- answers challenged and upheld in this court: " + strconv.FormatInt(int64(rec), 10) + "\n")
390	}
391	b.WriteString("\n## What you can do\n\n")
392	switch {
393	case cs.verdictAt == 0 && cs.frozenAt == 0:
394		b.WriteString("- staking is open — stake or unstake freely until an answer posts\n")
395	case cs.verdictAt == 0:
396		b.WriteString("- answered, awaiting the verdict — principal is never withheld; withdraw once it is final\n")
397	default:
398		b.WriteString("- the verdict is final — withdraw your principal on either side (1×)\n")
399		if cs.rewardsOpened {
400			win := int(cs.provisional)
401			if !cs.provClose && ((win == sideYES && yStake+yConv > 0) || (win == sideNO && nStake+nConv > 0)) {
402				b.WriteString("- you backed the winning side — pull your accuracy reward\n")
403			}
404			if who == cs.author && cs.drawAuthor > 0 {
405				b.WriteString("- author reward available — pull it\n")
406			}
407			if who == cs.answerer && cs.drawAnswerer > 0 {
408				b.WriteString("- answerer reward available — pull it\n")
409			}
410		}
411	}
412	return b.String()
413}
414func tierText(bps int64) string {
415	if bps < 0 {
416		bps = 0
417	}
418	frac := (bps % tierParBps) / 100
419	out := strconv.FormatInt(bps/tierParBps, 10) + "."
420	if frac < 10 {
421		out += "0"
422	}
423	return out + strconv.FormatInt(frac, 10) + "×"
424}
425func sideName(side int) string {
426	if side == sideYES {
427		return "YES"
428	}
429	return "NO"
430}
431func ClaimStatus(courtSlug string, claimID uint64) string {
432	return claimStatus(mustClaim(mustCourt(courtSlug), claimID))
433}
434func claimStatus(cs *claimState) string {
435	switch {
436	case cs.closed:
437		return "closed — never answered; every stake exited 1×, the deposit refunded"
438	case cs.spamClosed:
439		return "discarded — more than half the weight cast called this claim spam; " +
440			"every stake withdraws 1×, the answerer's and disputer's bonds return, " +
441			"the filing fee burns"
442	case cs.provClose:
443		return "closed without a decision — three dispute rounds failed quorum; everyone withdraws 1×, deposit and fee refunded"
444	case cs.verdictAt != 0:
445		return "settled " + sideName(int(cs.provisional)) + " — every stake withdraws 1×"
446	case cs.disputeOpen:
447		disputed := int(cs.answer)
448		if cs.provisional >= 0 {
449			disputed = int(cs.provisional)
450		}
451		return "disputed " + sideName(disputed) +
452			" — a sealed vote is deciding; principal is never withheld"
453	case cs.provisional >= 0:
454		return "provisional verdict " + sideName(int(cs.provisional)) +
455			" — reopenable by a new dispute until block " + strconv.FormatInt(cs.escrowUntil, 10) +
456			"; the side it is against may withdraw 1× now"
457	case cs.frozenAt != 0:
458		return "answered " + sideName(int(cs.answer)) +
459			" — staking frozen; disputable until block " +
460			strconv.FormatInt(cs.answerHeight+settleDelay, 10) + ", then it settles undisputed"
461	default:
462		return "open — stake YES or NO; unstake freely until an answer posts"
463	}
464}
465func pct(num, den int64) string {
466	if den <= 0 || num < 0 {
467		return "—"
468	}
469	bps := mulDiv128(num, 10000, den)
470	return strconv.FormatInt(bps/100, 10) + "." + pad2(bps%100) + "%"
471}
472func pad2(v int64) string {
473	if v < 10 {
474		return "0" + strconv.FormatInt(v, 10)
475	}
476	return strconv.FormatInt(v, 10)
477}
478func writeFolderIndex(b *strings.Builder, c *Court, slug string) {
479	if c.mod == nil {
480		return
481	}
482	shown := 0
483	for _, f := range c.mod.folderRows() {
484		if f.parent != 0 || f.retired || f.purged {
485			continue
486		}
487		if shown == 0 {
488			b.WriteString("Filed under: ")
489		} else {
490			b.WriteString(" · ")
491		}
492		b.WriteString("[" + sanitize.InlineText(f.name) + "](/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" +
493			slug + "/folder/" + strconv.FormatUint(f.id, 10) + ")")
494		shown++
495	}
496	if shown > 0 {
497		b.WriteString("\n\n")
498	}
499}
500func renderFolderPage(slug string, fid uint64) string {
501	if !Exists(slug) {
502		return "## Not found\nNo court by that slug."
503	}
504	c := mustCourt(slug)
505	if c.mod == nil {
506		return "## Not found\nThis court has no folders."
507	}
508	v := c.mod.folders.Get(beClaimKey(fid))
509	if v == nil {
510		return "## Not found\nNo folder by that id."
511	}
512	f := v.(*folder)
513	base := "/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/kourtv3:" + slug
514	var b strings.Builder
515	if f.purged {
516		b.WriteString("# [purged:" + sanitize.InlineText(f.code) + "]\n\n")
517		b.WriteString("_This heading was purged by the global DAO. Its name and " +
518			"description were erased, not hidden._\n\n")
519		b.WriteString("[← " + courtNameFor(c) + "](" + base + ")\n")
520		return b.String()
521	}
522	b.WriteString("# " + sanitize.InlineText(f.name) + "\n\n")
523	b.WriteString("[← " + courtNameFor(c) + "](" + base + ")")
524	for up, n := f.parent, 0; up != 0 && n <= maxFolders; n++ {
525		pv := c.mod.folders.Get(beClaimKey(up))
526		if pv == nil {
527			break
528		}
529		pf := pv.(*folder)
530		b.WriteString(" · [" + sanitize.InlineText(pf.name) + "](" + base +
531			"/folder/" + strconv.FormatUint(pf.id, 10) + ")")
532		up = pf.parent
533	}
534	b.WriteString("\n\n")
535	if f.retired {
536		b.WriteString("> This heading was retired by a moderator. It is kept so " +
537			"links to it still resolve.\n\n")
538	}
539	if f.bornOf != 0 {
540		b.WriteString("> The court voted this heading into existence: [claim #" +
541			strconv.FormatUint(f.bornOf, 10) + "](" + base + "/" +
542			strconv.FormatUint(f.bornOf, 10) + ") settled YES, and carrying it was " +
543			"a second act anyone could take.\n\n")
544	}
545	if f.desc != "" {
546		b.WriteString(sanitize.Block(f.desc) + "\n")
547	}
548	kids := 0
549	for _, k := range c.mod.folderRows() {
550		if k.parent != f.id || k.retired || k.purged {
551			continue
552		}
553		if kids == 0 {
554			b.WriteString("## Under this heading\n\n")
555		}
556		kids++
557		b.WriteString("- [" + sanitize.InlineText(k.name) + "](" + base + "/folder/" +
558			strconv.FormatUint(k.id, 10) + ")\n")
559	}
560	if kids > 0 {
561		b.WriteString("\n")
562	}
563	b.WriteString("## Claims filed here\n\n")
564	filed := 0
565	for _, id := range f.items {
566		if id == 0 || id > c.nextID {
567			continue
568		}
569		cs := mustClaim(c, id)
570		filed++
571		b.WriteString("- [" + claimTitleFor(c, cs) + "](" + base + "/" +
572			strconv.FormatUint(id, 10) + ") — " + claimStatus(cs) + "\n")
573	}
574	if filed == 0 {
575		b.WriteString("_Nothing filed here yet._\n")
576	}
577	b.WriteString("\n_A folder curates and can never bury: every claim above is " +
578		"also on the court's own newest-first list, and nothing is reachable only " +
579		"from here._\n\n")
580	b.WriteString(helpLink + "\n")
581	return b.String()
582}