render.gno
3.71 Kb · 129 lines
1package definition
2
3import (
4 "html"
5 "net/url"
6 "strconv"
7 "strings"
8 "unicode"
9)
10
11func Render(path string) string {
12 u, err := url.Parse(path)
13 if err != nil {
14 return renderError("Invalid URL")
15 }
16 query, err := url.ParseQuery(u.RawQuery)
17 if err != nil {
18 return renderError("Invalid query")
19 }
20 page := 1
21 if raw := query.Get("page"); raw != "" {
22 page, err = strconv.Atoi(raw)
23 if err != nil || page < 1 {
24 return renderError("Invalid page")
25 }
26 }
27 key := query.Get("key")
28 output := "# AKKADIA Definition\n\n[All items](./definition:)\n\n" + renderSearchForm(key)
29 switch u.Path {
30 case "", "list":
31 total := GetKeyCount()
32 output += "## Items (" + strconv.Itoa(total) + ")\n\n"
33 keys := ListKeys(page, 20)
34 if len(keys) == 0 {
35 output += "No entries on this page.\n"
36 }
37 for _, key := range keys {
38 label := key
39 if itemKey, found := GetField(key, "key")["value"]; found && itemKey != "" {
40 label += " — " + itemKey
41 }
42 output += "* [" + renderLabel(label) + "](./definition:search?key=" + url.QueryEscape(key) + "&page=" + strconv.Itoa(page) + ")\n"
43 }
44 pages := 1
45 if total > 0 {
46 pages = (total-1)/20 + 1
47 }
48 output += "\nPage " + strconv.Itoa(page) + " / " + strconv.Itoa(pages) + "\n"
49 return output + renderPages("./definition:list?", page, total)
50 case "search":
51 if strings.TrimSpace(key) == "" {
52 return output + "Key is required\n"
53 }
54 fields, found := getFields(key)
55 if !found {
56 return output + "Entry not found\n"
57 }
58 output += "[Back to list](./definition:list?page=" + strconv.Itoa(page) + ")\n\n"
59 output += "## " + renderLabel(key) + "\n\n"
60 for _, field := range fields {
61 output += "### " + renderLabel(field.Key) + "\n\n"
62 link, err := url.Parse(field.Value)
63 isLink := err == nil && (link.Scheme == "http" || link.Scheme == "https") && link.Hostname() != "" && link.Opaque == ""
64 for _, r := range field.Value {
65 if unicode.IsSpace(r) || unicode.IsControl(r) {
66 isLink = false
67 }
68 }
69 if isLink {
70 // Angle-delimited Markdown destinations retain parentheses and ampersands.
71 target := strings.ReplaceAll(strings.ReplaceAll(link.String(), "<", "%3C"), ">", "%3E")
72 output += "[" + renderLabel(field.Value) + "](<" + target + ">)\n\n"
73 } else {
74 // A fence longer than any user fence keeps HTML and Markdown literal.
75 longest, run := 2, 0
76 for _, r := range field.Value {
77 if r == '`' {
78 run++
79 if run > longest {
80 longest = run
81 }
82 } else {
83 run = 0
84 }
85 }
86 fence := strings.Repeat("`", longest+1)
87 output += fence + "\n" + field.Value + "\n" + fence + "\n\n"
88 }
89 }
90 return output
91 default:
92 return renderError("Page not found")
93 }
94}
95
96func renderLabel(value string) string {
97 // Gnoweb's TOC unescapes Markdown punctuation but leaves HTML entities literal.
98 var output strings.Builder
99 for _, r := range value {
100 if unicode.IsControl(r) {
101 output.WriteString(" ")
102 continue
103 }
104 if (r >= '!' && r <= '/') || (r >= ':' && r <= '@') || (r >= '[' && r <= '`') || (r >= '{' && r <= '~') {
105 output.WriteString("\\")
106 }
107 output.WriteRune(r)
108 }
109 return output.String()
110}
111
112func renderPages(base string, page, total int) string {
113 output := "\n"
114 if page > 1 {
115 output += "[Previous](" + base + "page=" + strconv.Itoa(page-1) + ") "
116 }
117 if total > 0 && page <= (total-1)/20 {
118 output += "[Next](" + base + "page=" + strconv.Itoa(page+1) + ")"
119 }
120 return output + "\n"
121}
122
123func renderError(message string) string {
124 return "# Definition\n\n" + message + "\n\n[All items](./definition:)\n\n" + renderSearchForm("")
125}
126
127func renderSearchForm(key string) string {
128 return "<gno-form path=\"search\">\n <gno-input name=\"key\" value=\"" + html.EscapeString(key) + "\" placeholder=\"Item ID (e.g. 1101000)\" />\n</gno-form>\n\n"
129}