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

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}