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

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}