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

2.81 Kb · 78 lines
 1package memba_notes_v1
 2
 3import (
 4	"strconv"
 5	"strings"
 6)
 7
 8// gnoweb uses bounded pages. A revision in every continuation link prevents
 9// assembling a whitepaper from different edits; the app reads binary chunks.
10func Render(path string) string {
11	if path == "" {
12		return "# Notes\n\nUse ListPublicJSON for the public directory.\n"
13	}
14	if len(path) > 80 {
15		panic("notes: render path")
16	}
17	parts := strings.Split(path, "/")
18	if len(parts) != 1 && len(parts) != 3 {
19		panic("notes: render path")
20	}
21	n := find(readID(parts[0]))
22	if n.Deleted {
23		return "# Deleted note\n\nPast versions remain in chain history.\n"
24	}
25	page := 0
26	if len(parts) == 3 {
27		rev, err := strconv.ParseUint(parts[1], 10, 64)
28		if err != nil || rev != n.Revision {
29			return "# Note changed\n\n[Start again](:" + idText(n.ID) + ").\n"
30		}
31		page, err = strconv.Atoi(parts[2])
32		if err != nil {
33			panic("notes: render page")
34		}
35	}
36	if !public(n.Mode) {
37		return "# Encrypted note\n\n" + seal(n.Body) + "\nOwner: " + string(n.Owner) + "\n\nEncrypted bytes: " + strconv.Itoa(n.BodyBytes) + " · Revision " + strconv.FormatUint(n.Revision, 10) + "\n\nOpen this note in Memba to decrypt. This seal is decorative.\n"
38	}
39	pages := (n.BodyBytes + MaxBodyChunkBytes - 1) / MaxBodyChunkBytes
40	if pages == 0 {
41		pages = 1
42	}
43	if page < 0 || page >= pages {
44		panic("notes: render page")
45	}
46	start := renderBoundary(n.Body, page*MaxBodyChunkBytes)
47	end := renderBoundary(n.Body, (page+1)*MaxBodyChunkBytes)
48	title := strings.NewReplacer("\\", "\\\\", "[", "\\[", "]", "\\]", "*", "\\*", "_", "\\_", "`", "\\`", "#", "\\#", "<", "&lt;", ">", "&gt;").Replace(string(n.Title))
49	body := strings.ReplaceAll(strings.ReplaceAll(string(n.Body[start:end]), "<", "&lt;"), ">", "&gt;")
50	// Entity tokens are text, not Markdown image syntax, even after a backslash.
51	// Keep ordinary links available without automatically fetching remote images.
52	body = strings.ReplaceAll(body, "![", "&#33;[")
53	nav := ""
54	if pages > 1 {
55		nav = "\n\n---\n\nPage " + strconv.Itoa(page+1) + " of " + strconv.Itoa(pages) + " · " + strconv.Itoa(n.BodyBytes) + " bytes · revision " + strconv.FormatUint(n.Revision, 10) + ". Markdown may continue across pages.\n\n"
56		base := ":" + idText(n.ID) + "/" + strconv.FormatUint(n.Revision, 10) + "/"
57		if page > 0 {
58			nav += "[Previous](" + base + strconv.Itoa(page-1) + ") "
59		}
60		if page+1 < pages {
61			nav += "[Next](" + base + strconv.Itoa(page+1) + ") "
62		}
63		nav += "[Read the complete note in Memba](https://memba.club/os/notes/" + idText(n.ID) + ")\n"
64	}
65	return "# " + title + "\n\n" + body + nav
66}
67
68// Advance at most three continuation bytes; adjacent pages use the same
69// boundary, so no UTF8 byte is lost or repeated.
70func renderBoundary(body []byte, pos int) int {
71	if pos >= len(body) {
72		return len(body)
73	}
74	for pos < len(body) && body[pos]&0xc0 == 0x80 {
75		pos++
76	}
77	return pos
78}