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

19.86 Kb · 566 lines
  1package kourt
  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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + 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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:<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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + 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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:<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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + 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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + 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/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + 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	b.WriteString("- price: " + strconv.FormatInt(c.crv.Price(c.minted), 10) +
181		" ugnot per coin — buying burns the GNOT, there is no sell side, " +
182		"and the price only rises\n")
183	b.WriteString("- in circulation: " + strconv.FormatInt(c.coin.TotalSupply(), 10) +
184		" · paid out as rewards so far: " + strconv.FormatInt(c.emittedTotal, 10) + "\n")
185	b.WriteString("- reward reservoir: " + strconv.FormatInt(c.reservoirR(), 10) +
186		" waiting to be earned · " + strconv.FormatInt(c.seniorOwed, 10) +
187		" already owed to earlier earners\n\n")
188	b.WriteString(helpLink + "\n\n")
189}
190func renderClaim(slug string, id uint64) string {
191	if !Exists(slug) {
192		return "## Not found\nNo court by that slug."
193	}
194	c := mustCourt(slug)
195	if id == 0 || id > c.nextID {
196		return "## Not found\nNo claim by that id."
197	}
198	cs := mustClaim(c, id)
199	b := strings.Builder{}
200	b.WriteString("# " + claimTitleFor(c, cs) + "\n\n")
201	if body := claimBodyQuoted(c, cs); body != "" {
202		b.WriteString(body)
203	}
204	writeClaimMedia(&b, c, cs)
205	b.WriteString(hideBanner(c, cs))
206	writeRelations(&b, c, id)
207	writeAppeal(&b, c, cs)
208	if cs.seeded {
209		b.WriteString("_Seeded by a moderator to start this court; the author earns nothing from it._\n\n")
210	}
211	b.WriteString("## Signal\n\n")
212	if cs.frozenAt != 0 {
213		fTotal := cs.yesStakeAtFreeze + cs.noStakeAtFreeze
214		b.WriteString("- staked when answered: " + strconv.FormatInt(fTotal, 10) +
215			" " + qualifiedSymbol(c) + " (YES " + strconv.FormatInt(cs.yesStakeAtFreeze, 10) +
216			" / NO " + strconv.FormatInt(cs.noStakeAtFreeze, 10) + ")\n")
217		if fTotal > 0 {
218			b.WriteString("- lean when answered: " + pct(cs.yesStakeAtFreeze, fTotal) + " YES\n")
219		}
220		if live := cs.yesStake + cs.noStake; live > 0 {
221			b.WriteString("- still unwithdrawn: " + strconv.FormatInt(live, 10) +
222				" " + qualifiedSymbol(c) + "\n")
223		}
224	} else {
225		total := cs.yesStake + cs.noStake
226		b.WriteString("- staked now: " + strconv.FormatInt(total, 10) +
227			" " + qualifiedSymbol(c) + " (YES " + strconv.FormatInt(cs.yesStake, 10) +
228			" / NO " + strconv.FormatInt(cs.noStake, 10) + ")\n")
229		if total > 0 {
230			b.WriteString("- right now: " + pct(cs.yesStake, total) + " YES\n")
231		}
232		b.WriteString("- pays: not set until the claim is answered — it is " +
233			"this claim's total stake against a typical claim in this court, " +
234			"so staking now is what sets it\n")
235	}
236	h := heightNow()
237	if yAvg, ym := cs.yes.Average(h, periodBlocks); ym {
238		if tAvg, tm := cs.oi.Average(h, periodBlocks); tm && tAvg > 0 {
239			b.WriteString("- trailing week: " + pct(yAvg, tAvg) + " YES\n")
240		}
241	}
242	yc := convToCC(cs.yesConvHi, cs.yesConvLo)
243	nc := convToCC(cs.noConvHi, cs.noConvLo)
244	if yc+nc > 0 {
245		b.WriteString("- lifetime, weighted by how long each stake was held: " + pct(yc, yc+nc) + " YES\n")
246	}
247	b.WriteString("\n## Status\n\n" + claimStatus(cs) + "\n")
248	if cs.frozenAt != 0 {
249		b.WriteString("\n## Resolution\n\n")
250		b.WriteString("- answer: " + sideName(int(cs.answer)) + " (bond " +
251			strconv.FormatInt(cs.answerBond0, 10) + " " + qualifiedSymbol(c) + ")\n")
252		b.WriteString("- the answerer has been challenged and upheld " +
253			strconv.FormatInt(int64(AnswerRecord(slug, cs.answerer)), 10) +
254			" time(s) in this court — answers nobody contested do not count\n")
255		if PriorityGateActive(slug) {
256			b.WriteString("- answer priority is live here — a record of " +
257				strconv.FormatInt(int64(priorityNetRecord), 10) +
258				"+ earns a 24h head start on new claims\n")
259		}
260		if cs.failedRounds > 0 {
261			b.WriteString("- failed dispute rounds: " + strconv.FormatInt(cs.failedRounds, 10) +
262				" of " + strconv.FormatInt(int64(maxFailedRounds), 10) +
263				" — the next dispute bond doubles\n")
264		}
265		if cs.disputeOpen {
266			b.WriteString("- dispute round " + strconv.FormatInt(cs.round, 10) +
267				" vote open — no running total is shown here until it closes\n")
268		} else if cs.verdictAt == 0 {
269			b.WriteString("- disputing this answer costs a bond of " +
270				strconv.FormatInt(cs.disputeBond0(), 10) + " " + qualifiedSymbol(c) +
271				", doubling for each round that fails to reach a quorum\n")
272			b.WriteString("  - win and the answerer's bond is burned and you are " +
273				"compensated, up to twice your own bond; lose and yours burns " +
274				"instead. Nothing passes between the two of you — a forfeit is " +
275				"burned, a compensation is newly issued\n")
276			b.WriteString("  - if the round draws no quorum, half your bond burns " +
277				"and half returns; three such rounds close the claim undecided, " +
278				"with every stake out at 1× and the deposit and fee refunded\n")
279			b.WriteString("  - **your stake is not at risk either way** — only the " +
280				"bond is\n")
281		}
282		if cs.verdictAt != 0 && !cs.provClose {
283			b.WriteString("- verdict: " + sideName(int(cs.provisional)) + " — " + cs.route + "\n")
284		}
285		if cs.tierRef <= 0 {
286			b.WriteString("- pays " + tierText(tierParBps) +
287				" — this claim was answered before ratings were recorded, so it " +
288				"pays the standard amount\n")
289		} else {
290			b.WriteString("- pays " + tierText(tierBpsFor(cs)) +
291				" — this claim held " + strconv.FormatInt(cs.xBarFrozen, 10) +
292				" " + qualifiedSymbol(c) + " against a typical " +
293				strconv.FormatInt(cs.tierRef, 10) + " " + qualifiedSymbol(c) + " here\n")
294			b.WriteString("- the rating is the money that showed up, not a vote: " +
295				"a claim of typical size pays 1.00×, and it is capped at 0.25× and 2.00×\n")
296			if cs.spamTotalW > 0 && cs.spamW > 0 {
297				b.WriteString("- and " + tierText(spamNetBps(cs)) +
298					" of that, because " + strconv.FormatInt(cs.spamW, 10) +
299					" of " + strconv.FormatInt(cs.spamTotalW, 10) +
300					" of the weight cast flagged it as spam\n")
301			}
302		}
303		switch {
304		}
305		if cs.rewardsOpened {
306			w, a, ans, carrot := cs.drawWinners, cs.drawAuthor, cs.drawAnswerer, cs.carrotPool
307			b.WriteString("- rewards open — pools (" + qualifiedSymbol(c) +
308				"): accuracy " + strconv.FormatInt(w, 10) +
309				", author " + strconv.FormatInt(a, 10) + ", answerer " + strconv.FormatInt(ans, 10) +
310				", participation remaining " + strconv.FormatInt(carrot, 10) + "\n")
311		}
312	}
313	b.WriteString("\n_To see your own stake on this claim, put your address on " +
314		"the end of this page's path:_ `/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + sanitize.InlineText(slug) +
315		"/" + strconv.FormatUint(id, 10) + "/<your address>`\n\n")
316	writeBoardLink(&b, c, cs, id)
317	return b.String()
318}
319func renderPositions(slug string, id uint64, addrStr string) string {
320	if !Exists(slug) {
321		return "## Not found\nNo court by that slug."
322	}
323	c := mustCourt(slug)
324	if id == 0 || id > c.nextID {
325		return "## Not found\nNo claim by that id."
326	}
327	who := address(addrStr)
328	if !who.IsValid() {
329		return "## Not found\nThat is not a valid address."
330	}
331	cs := mustClaim(c, id)
332	b := strings.Builder{}
333	b.WriteString("# Positions on this claim\n\n")
334	b.WriteString(hideBanner(c, cs))
335	b.WriteString("Claim: [" + claimTitleFor(c, cs) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug +
336		"/" + strconv.FormatUint(id, 10) + ") in [" + courtNameFor(c) +
337		"](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + ")\n\n")
338	b.WriteString("Address: " + sanitize.InlineText(addrStr) + "\n\n")
339	yStake := StakeOf(slug, id, sideYES, who)
340	nStake := StakeOf(slug, id, sideNO, who)
341	yConv := ConvictionOf(slug, id, sideYES, who)
342	nConv := ConvictionOf(slug, id, sideNO, who)
343	b.WriteString("## Stake (what you have in, and always get back)\n\n")
344	b.WriteString("- YES: " + strconv.FormatInt(yStake, 10) +
345		"   NO: " + strconv.FormatInt(nStake, 10) + " (" + qualifiedSymbol(c) +
346		" base units)\n")
347	b.WriteString("- conviction so far — YES: " + strconv.FormatInt(yConv, 10) +
348		"   NO: " + strconv.FormatInt(nConv, 10) + " (the reward weight)\n")
349	bal := c.coin.BalanceOf(who)
350	locked := lockedOf(c, who)
351	voted := voteLockedOf(c, who)
352	b.WriteString("\n## This address's " + qualifiedSymbol(c) +
353		" in this court (all claims)\n\n")
354	b.WriteString("- held: " + strconv.FormatInt(bal, 10) +
355		" — every unit votes, committed or not\n")
356	b.WriteString("- committed as stake: " + strconv.FormatInt(locked, 10) + "\n")
357	b.WriteString("- committed by voting: " + strconv.FormatInt(voted, 10) +
358		" — until each question resolves\n")
359	b.WriteString("- free to stake: " + strconv.FormatInt(spendable(c, who), 10) + "\n")
360	b.WriteString("- free to bond, deposit or transfer: " +
361		strconv.FormatInt(disposable(c, who), 10) + "\n")
362	roles := ""
363	if who == cs.author {
364		roles += "author "
365	}
366	if cs.frozenAt != 0 && who == cs.answerer {
367		roles += "answerer "
368	}
369	if roles != "" {
370		b.WriteString("- roles: " + roles + "\n")
371	}
372	if rec := AnswerRecord(slug, who); rec > 0 {
373		b.WriteString("- answers challenged and upheld in this court: " + strconv.FormatInt(int64(rec), 10) + "\n")
374	}
375	b.WriteString("\n## What you can do\n\n")
376	switch {
377	case cs.verdictAt == 0 && cs.frozenAt == 0:
378		b.WriteString("- staking is open — stake or unstake freely until an answer posts\n")
379	case cs.verdictAt == 0:
380		b.WriteString("- answered, awaiting the verdict — principal is never withheld; withdraw once it is final\n")
381	default:
382		b.WriteString("- the verdict is final — withdraw your principal on either side (1×)\n")
383		if cs.rewardsOpened {
384			win := int(cs.provisional)
385			if !cs.provClose && ((win == sideYES && yStake+yConv > 0) || (win == sideNO && nStake+nConv > 0)) {
386				b.WriteString("- you backed the winning side — pull your accuracy reward\n")
387			}
388			if who == cs.author && cs.drawAuthor > 0 {
389				b.WriteString("- author reward available — pull it\n")
390			}
391			if who == cs.answerer && cs.drawAnswerer > 0 {
392				b.WriteString("- answerer reward available — pull it\n")
393			}
394		}
395	}
396	return b.String()
397}
398func tierText(bps int64) string {
399	if bps < 0 {
400		bps = 0
401	}
402	frac := (bps % tierParBps) / 100
403	out := strconv.FormatInt(bps/tierParBps, 10) + "."
404	if frac < 10 {
405		out += "0"
406	}
407	return out + strconv.FormatInt(frac, 10) + "×"
408}
409func sideName(side int) string {
410	if side == sideYES {
411		return "YES"
412	}
413	return "NO"
414}
415func ClaimStatus(courtSlug string, claimID uint64) string {
416	return claimStatus(mustClaim(mustCourt(courtSlug), claimID))
417}
418func claimStatus(cs *claimState) string {
419	switch {
420	case cs.closed:
421		return "closed — never answered; every stake exited 1×, the deposit refunded"
422	case cs.spamClosed:
423		return "discarded — more than half the weight cast called this claim spam; " +
424			"every stake withdraws 1×, the answerer's and disputer's bonds return, " +
425			"the filing fee burns"
426	case cs.provClose:
427		return "closed without a decision — three dispute rounds failed quorum; everyone withdraws 1×, deposit and fee refunded"
428	case cs.verdictAt != 0:
429		return "settled " + sideName(int(cs.provisional)) + " — every stake withdraws 1×"
430	case cs.disputeOpen:
431		disputed := int(cs.answer)
432		if cs.provisional >= 0 {
433			disputed = int(cs.provisional)
434		}
435		return "disputed " + sideName(disputed) +
436			" — a sealed vote is deciding; principal is never withheld"
437	case cs.provisional >= 0:
438		return "provisional verdict " + sideName(int(cs.provisional)) +
439			" — reopenable by a new dispute until block " + strconv.FormatInt(cs.escrowUntil, 10) +
440			"; the side it is against may withdraw 1× now"
441	case cs.frozenAt != 0:
442		return "answered " + sideName(int(cs.answer)) +
443			" — staking frozen; disputable until block " +
444			strconv.FormatInt(cs.answerHeight+settleDelay, 10) + ", then it settles undisputed"
445	default:
446		return "open — stake YES or NO; unstake freely until an answer posts"
447	}
448}
449func pct(num, den int64) string {
450	if den <= 0 || num < 0 {
451		return "—"
452	}
453	bps := mulDiv128(num, 10000, den)
454	return strconv.FormatInt(bps/100, 10) + "." + pad2(bps%100) + "%"
455}
456func pad2(v int64) string {
457	if v < 10 {
458		return "0" + strconv.FormatInt(v, 10)
459	}
460	return strconv.FormatInt(v, 10)
461}
462func writeFolderIndex(b *strings.Builder, c *Court, slug string) {
463	if c.mod == nil {
464		return
465	}
466	shown := 0
467	for _, f := range c.mod.folderRows() {
468		if f.parent != 0 || f.retired || f.purged {
469			continue
470		}
471		if shown == 0 {
472			b.WriteString("Filed under: ")
473		} else {
474			b.WriteString(" · ")
475		}
476		b.WriteString("[" + sanitize.InlineText(f.name) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" +
477			slug + "/folder/" + strconv.FormatUint(f.id, 10) + ")")
478		shown++
479	}
480	if shown > 0 {
481		b.WriteString("\n\n")
482	}
483}
484func renderFolderPage(slug string, fid uint64) string {
485	if !Exists(slug) {
486		return "## Not found\nNo court by that slug."
487	}
488	c := mustCourt(slug)
489	if c.mod == nil {
490		return "## Not found\nThis court has no folders."
491	}
492	v := c.mod.folders.Get(beClaimKey(fid))
493	if v == nil {
494		return "## Not found\nNo folder by that id."
495	}
496	f := v.(*folder)
497	base := "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug
498	var b strings.Builder
499	if f.purged {
500		b.WriteString("# [purged:" + sanitize.InlineText(f.code) + "]\n\n")
501		b.WriteString("_This heading was purged by the global DAO. Its name and " +
502			"description were erased, not hidden._\n\n")
503		b.WriteString("[← " + courtNameFor(c) + "](" + base + ")\n")
504		return b.String()
505	}
506	b.WriteString("# " + sanitize.InlineText(f.name) + "\n\n")
507	b.WriteString("[← " + courtNameFor(c) + "](" + base + ")")
508	for up, n := f.parent, 0; up != 0 && n <= maxFolders; n++ {
509		pv := c.mod.folders.Get(beClaimKey(up))
510		if pv == nil {
511			break
512		}
513		pf := pv.(*folder)
514		b.WriteString(" · [" + sanitize.InlineText(pf.name) + "](" + base +
515			"/folder/" + strconv.FormatUint(pf.id, 10) + ")")
516		up = pf.parent
517	}
518	b.WriteString("\n\n")
519	if f.retired {
520		b.WriteString("> This heading was retired by a moderator. It is kept so " +
521			"links to it still resolve.\n\n")
522	}
523	if f.bornOf != 0 {
524		b.WriteString("> The court voted this heading into existence: [claim #" +
525			strconv.FormatUint(f.bornOf, 10) + "](" + base + "/" +
526			strconv.FormatUint(f.bornOf, 10) + ") settled YES, and carrying it was " +
527			"a second act anyone could take.\n\n")
528	}
529	if f.desc != "" {
530		b.WriteString(sanitize.Block(f.desc) + "\n")
531	}
532	kids := 0
533	for _, k := range c.mod.folderRows() {
534		if k.parent != f.id || k.retired || k.purged {
535			continue
536		}
537		if kids == 0 {
538			b.WriteString("## Under this heading\n\n")
539		}
540		kids++
541		b.WriteString("- [" + sanitize.InlineText(k.name) + "](" + base + "/folder/" +
542			strconv.FormatUint(k.id, 10) + ")\n")
543	}
544	if kids > 0 {
545		b.WriteString("\n")
546	}
547	b.WriteString("## Claims filed here\n\n")
548	filed := 0
549	for _, id := range f.items {
550		if id == 0 || id > c.nextID {
551			continue
552		}
553		cs := mustClaim(c, id)
554		filed++
555		b.WriteString("- [" + claimTitleFor(c, cs) + "](" + base + "/" +
556			strconv.FormatUint(id, 10) + ") — " + claimStatus(cs) + "\n")
557	}
558	if filed == 0 {
559		b.WriteString("_Nothing filed here yet._\n")
560	}
561	b.WriteString("\n_A folder curates and can never bury: every claim above is " +
562		"also on the court's own newest-first list, and nothing is reachable only " +
563		"from here._\n\n")
564	b.WriteString(helpLink + "\n")
565	return b.String()
566}