diff options
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | assets/startpage.txt | 6 | ||||
-rw-r--r-- | internal/port/gemini.go | 176 | ||||
-rw-r--r-- | internal/port/gopher.go | 138 | ||||
-rw-r--r-- | internal/port/main.go | 69 | ||||
-rw-r--r-- | internal/port/tpl/_fonts.html | 16 | ||||
-rw-r--r-- | internal/port/tpl/_header.html | 48 | ||||
-rw-r--r-- | internal/port/tpl/_modals.html | 24 | ||||
-rw-r--r-- | internal/port/tpl/gemini.html | 38 | ||||
-rw-r--r-- | internal/port/tpl/gopher.html | 59 | ||||
-rw-r--r-- | internal/port/tpl/startpage.html | 120 | ||||
-rw-r--r-- | pkg/libgemini/libgemini.go | 85 | ||||
-rw-r--r-- | pkg/libgopher/libgopher.go | 2 |
13 files changed, 458 insertions, 331 deletions
@@ -8,10 +8,10 @@ dev: build | |||
8 | build: clean | 8 | build: clean |
9 | sassc -t compressed css/main.scss assets/style.css | 9 | sassc -t compressed css/main.scss assets/style.css |
10 | tsc --strict --module none --outFile /dev/stdout js/* | terser --compress --mangle -o assets/main.js -- | 10 | tsc --strict --module none --outFile /dev/stdout js/* | terser --compress --mangle -o assets/main.js -- |
11 | pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-term-ss03-regular.woff' | 11 | #pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-term-ss03-regular.woff' |
12 | pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' | 12 | #pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' |
13 | pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-aile-regular.woff' | 13 | #pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-aile-regular.woff' |
14 | pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-aile-regular.woff2' | 14 | #pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-aile-regular.woff2' |
15 | go build -o ./port.bin ./cmd/port | 15 | go build -o ./port.bin ./cmd/port |
16 | 16 | ||
17 | profile: | 17 | profile: |
diff --git a/assets/startpage.txt b/assets/startpage.txt index 46b22c1..3d1de44 100644 --- a/assets/startpage.txt +++ b/assets/startpage.txt | |||
@@ -1,6 +1,6 @@ | |||
1 | P R O X Y | 1 | P R O X Y |
2 | - - - - - - for - - - - - - | 2 | - - - - - - for - - - - - - |
3 | G O P H E R + G E M I N I | 3 | G O P H E R + G E M I N I |
4 | 4 | ||
5 | 5 | ||
6 | GETTING STARTED -- | 6 | GETTING STARTED -- |
diff --git a/internal/port/gemini.go b/internal/port/gemini.go index b10da7d..0d8292c 100644 --- a/internal/port/gemini.go +++ b/internal/port/gemini.go | |||
@@ -1,7 +1,6 @@ | |||
1 | package port | 1 | package port |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "bufio" | ||
5 | "bytes" | 4 | "bytes" |
6 | "fmt" | 5 | "fmt" |
7 | "html/template" | 6 | "html/template" |
@@ -10,7 +9,6 @@ import ( | |||
10 | "mime" | 9 | "mime" |
11 | "net/http" | 10 | "net/http" |
12 | "net/url" | 11 | "net/url" |
13 | "regexp" | ||
14 | "strings" | 12 | "strings" |
15 | 13 | ||
16 | "golang.org/x/net/html/charset" | 14 | "golang.org/x/net/html/charset" |
@@ -21,129 +19,83 @@ import ( | |||
21 | "github.com/temoto/robotstxt" | 19 | "github.com/temoto/robotstxt" |
22 | ) | 20 | ) |
23 | 21 | ||
24 | type SectionType byte | 22 | type GeminiTemplateVariables struct { |
25 | |||
26 | const ( | ||
27 | RAW_TEXT = SectionType(0) | ||
28 | REFLOW_TEXT = SectionType(1) | ||
29 | LINK = SectionType(2) | ||
30 | ) | ||
31 | |||
32 | type Section struct { | ||
33 | Type SectionType | ||
34 | Text string | ||
35 | URL template.URL | ||
36 | } | ||
37 | |||
38 | type templateVariables struct { | ||
39 | Title string | 23 | Title string |
40 | URI string | 24 | URL string |
41 | Assets AssetList | 25 | Assets AssetList |
42 | Sections []Section | 26 | Sections []GeminiSection |
27 | Nav []GeminiNavItem | ||
43 | } | 28 | } |
44 | 29 | ||
45 | var ( | 30 | type GeminiNavItem struct { |
46 | TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") | 31 | Label string |
47 | ) | 32 | URL string |
33 | } | ||
48 | 34 | ||
49 | func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | 35 | type GeminiSection struct { |
36 | Type libgemini.GeminiDocSectionType | ||
37 | Text string | ||
38 | URL template.URL | ||
39 | Items []string | ||
40 | } | ||
41 | |||
42 | func resolveURL(uri string, baseURL *url.URL) (resolvedURL string) { | ||
50 | if strings.HasPrefix(uri, "//") { | 43 | if strings.HasPrefix(uri, "//") { |
51 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") | 44 | resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "//") |
52 | } else if strings.HasPrefix(uri, "gemini://") { | 45 | } else if strings.HasPrefix(uri, "gemini://") { |
53 | resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") | 46 | resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "gemini://") |
54 | } else if strings.HasPrefix(uri, "gopher://") { | 47 | } else if strings.HasPrefix(uri, "gopher://") { |
55 | resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") | 48 | resolvedURL = "/gopher/" + strings.TrimPrefix(uri, "gopher://") |
56 | } else { | 49 | } else { |
57 | url, err := url.Parse(uri) | 50 | url, err := url.Parse(uri) |
58 | if err != nil { | 51 | if err != nil { |
59 | return "" | 52 | return "" |
60 | } | 53 | } |
61 | adjustedURI := baseURL.ResolveReference(url) | 54 | adjustedURL := baseURL.ResolveReference(url) |
62 | path := adjustedURI.Path | 55 | path := adjustedURL.Path |
63 | if !strings.HasPrefix(path, "/") { | 56 | if !strings.HasPrefix(path, "/") { |
64 | path = "/" + path | 57 | path = "/" + path |
65 | } | 58 | } |
66 | if adjustedURI.Scheme == "gemini" { | 59 | if adjustedURL.Scheme == "gemini" { |
67 | resolvedURI = "/gemini/" + adjustedURI.Host + path | 60 | resolvedURL = "/gemini/" + adjustedURL.Host + path |
68 | } else if adjustedURI.Scheme == "gopher" { | 61 | } else if adjustedURL.Scheme == "gopher" { |
69 | resolvedURI = "/gopher/" + adjustedURI.Host + path | 62 | resolvedURL = "/gopher/" + adjustedURL.Host + path |
70 | } else { | 63 | } else { |
71 | resolvedURI = adjustedURI.String() | 64 | resolvedURL = adjustedURL.String() |
72 | } | 65 | } |
73 | } | 66 | } |
74 | 67 | ||
75 | return | 68 | return |
76 | } | 69 | } |
77 | 70 | ||
78 | func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []Section) { | 71 | func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []GeminiSection) { |
79 | baseURL, err := url.Parse(fmt.Sprintf( | 72 | baseURL, err := url.Parse(fmt.Sprintf( |
80 | "gemini://%s/%s", | 73 | "gemini://%s/%s", |
81 | hostport, | 74 | hostport, |
82 | uri, | 75 | uri, |
83 | )) | 76 | )) |
84 | if err != nil { | 77 | if err != nil { |
85 | return []Section{} | 78 | return |
86 | } | ||
87 | |||
88 | skipSection := true | ||
89 | |||
90 | section := Section{ | ||
91 | Type: RAW_TEXT, | ||
92 | } | 79 | } |
93 | 80 | ||
94 | scanner := bufio.NewScanner(body) | 81 | unpreppedSections := libgemini.ParseGeminiDocument(body) |
95 | 82 | ||
96 | for scanner.Scan() { | 83 | for _, section := range unpreppedSections { |
97 | line := strings.Trim(scanner.Text(), "\r\n") | 84 | if section.Type != libgemini.LINK { |
98 | line = TermEscapeSGRPattern.ReplaceAllString(line, "") | 85 | sections = append(sections, GeminiSection{ |
99 | 86 | Type: section.Type, | |
100 | linkMatch := libgemini.LinkPattern.FindStringSubmatch(line) | 87 | Text: section.Text, |
101 | if len(linkMatch) != 0 && linkMatch[0] != "" { | 88 | URL: template.URL(section.URL), |
102 | curType := section.Type | 89 | Items: section.Items, |
103 | |||
104 | if !skipSection { | ||
105 | sections = append(sections, section) | ||
106 | } | ||
107 | |||
108 | label := linkMatch[2] | ||
109 | if label == "" { | ||
110 | label = linkMatch[1] | ||
111 | } | ||
112 | |||
113 | sections = append(sections, Section{ | ||
114 | Type: LINK, | ||
115 | Text: label, | ||
116 | URL: template.URL(resolveURI(linkMatch[1], baseURL)), | ||
117 | }) | 90 | }) |
118 | |||
119 | skipSection = false | ||
120 | section = Section{ | ||
121 | Type: curType, | ||
122 | } | ||
123 | } else { | ||
124 | reflowModeMatch := libgemini.ReflowModePattern.FindStringSubmatch(line) | ||
125 | if len(reflowModeMatch) != 0 { | ||
126 | newType := RAW_TEXT | ||
127 | if section.Type == RAW_TEXT { | ||
128 | newType = REFLOW_TEXT | ||
129 | } | ||
130 | |||
131 | if !skipSection { | ||
132 | sections = append(sections, section) | ||
133 | } | ||
134 | |||
135 | skipSection = false | ||
136 | section = Section{ | ||
137 | Type: newType, | ||
138 | } | ||
139 | } else { | ||
140 | section.Text = section.Text + "\n" + line | ||
141 | } | ||
142 | } | 91 | } |
143 | } | ||
144 | 92 | ||
145 | if !skipSection { | 93 | sections = append(sections, GeminiSection{ |
146 | sections = append(sections, section) | 94 | Type: section.Type, |
95 | Text: section.Text, | ||
96 | URL: template.URL(resolveURL(section.URL, baseURL)), | ||
97 | Items: section.Items, | ||
98 | }) | ||
147 | } | 99 | } |
148 | 100 | ||
149 | return | 101 | return |
@@ -176,12 +128,12 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
176 | 128 | ||
177 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | 129 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) |
178 | if err != nil { | 130 | if err != nil { |
179 | if e := tpl.Execute(w, templateVariables{ | 131 | if e := tpl.Execute(w, GeminiTemplateVariables{ |
180 | Title: title, | 132 | Title: title, |
181 | URI: hostport, | 133 | URL: hostport, |
182 | Assets: assetList, | 134 | Assets: assetList, |
183 | Sections: []Section{{ | 135 | Sections: []GeminiSection{{ |
184 | Type: RAW_TEXT, | 136 | Type: libgemini.RAW_TEXT, |
185 | Text: fmt.Sprintf("Error: %s", err), | 137 | Text: fmt.Sprintf("Error: %s", err), |
186 | }}, | 138 | }}, |
187 | }); e != nil { | 139 | }); e != nil { |
@@ -205,12 +157,12 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
205 | ) | 157 | ) |
206 | 158 | ||
207 | if err != nil { | 159 | if err != nil { |
208 | if e := tpl.Execute(w, templateVariables{ | 160 | if e := tpl.Execute(w, GeminiTemplateVariables{ |
209 | Title: title, | 161 | Title: title, |
210 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 162 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
211 | Assets: assetList, | 163 | Assets: assetList, |
212 | Sections: []Section{{ | 164 | Sections: []GeminiSection{{ |
213 | Type: RAW_TEXT, | 165 | Type: libgemini.RAW_TEXT, |
214 | Text: fmt.Sprintf("Error: %s", err), | 166 | Text: fmt.Sprintf("Error: %s", err), |
215 | }}, | 167 | }}, |
216 | }); e != nil { | 168 | }); e != nil { |
@@ -227,12 +179,12 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
227 | uri, | 179 | uri, |
228 | )) | 180 | )) |
229 | if err != nil { | 181 | if err != nil { |
230 | if e := tpl.Execute(w, templateVariables{ | 182 | if e := tpl.Execute(w, GeminiTemplateVariables{ |
231 | Title: title, | 183 | Title: title, |
232 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 184 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
233 | Assets: assetList, | 185 | Assets: assetList, |
234 | Sections: []Section{{ | 186 | Sections: []GeminiSection{{ |
235 | Type: RAW_TEXT, | 187 | Type: libgemini.RAW_TEXT, |
236 | Text: fmt.Sprintf("Error: %s", err), | 188 | Text: fmt.Sprintf("Error: %s", err), |
237 | }}, | 189 | }}, |
238 | }); e != nil { | 190 | }); e != nil { |
@@ -242,17 +194,17 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
242 | return | 194 | return |
243 | } | 195 | } |
244 | 196 | ||
245 | http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) | 197 | http.Redirect(w, req, resolveURL(res.Header.Meta, baseURL), http.StatusFound) |
246 | return | 198 | return |
247 | } | 199 | } |
248 | 200 | ||
249 | if int(res.Header.Status/10) != 2 { | 201 | if int(res.Header.Status/10) != 2 { |
250 | if err := tpl.Execute(w, templateVariables{ | 202 | if err := tpl.Execute(w, GeminiTemplateVariables{ |
251 | Title: title, | 203 | Title: title, |
252 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 204 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
253 | Assets: assetList, | 205 | Assets: assetList, |
254 | Sections: []Section{{ | 206 | Sections: []GeminiSection{{ |
255 | Type: RAW_TEXT, | 207 | Type: libgemini.RAW_TEXT, |
256 | Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), | 208 | Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), |
257 | }}, | 209 | }}, |
258 | }); err != nil { | 210 | }); err != nil { |
@@ -277,20 +229,20 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
277 | writer.Close() | 229 | writer.Close() |
278 | } | 230 | } |
279 | 231 | ||
280 | var sections []Section | 232 | var sections []GeminiSection |
281 | 233 | ||
282 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { | 234 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { |
283 | sections = parseGeminiDocument(buf, uri, hostport) | 235 | sections = parseGeminiDocument(buf, uri, hostport) |
284 | } else { | 236 | } else { |
285 | sections = append(sections, Section{ | 237 | sections = append(sections, GeminiSection{ |
286 | Type: RAW_TEXT, | 238 | Type: libgemini.RAW_TEXT, |
287 | Text: buf.String(), | 239 | Text: buf.String(), |
288 | }) | 240 | }) |
289 | } | 241 | } |
290 | 242 | ||
291 | if err := tpl.Execute(w, templateVariables{ | 243 | if err := tpl.Execute(w, GeminiTemplateVariables{ |
292 | Title: title, | 244 | Title: title, |
293 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 245 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
294 | Assets: assetList, | 246 | Assets: assetList, |
295 | Sections: sections, | 247 | Sections: sections, |
296 | }); err != nil { | 248 | }); err != nil { |
diff --git a/internal/port/gopher.go b/internal/port/gopher.go index abbc4d9..d2283c6 100644 --- a/internal/port/gopher.go +++ b/internal/port/gopher.go | |||
@@ -17,16 +17,73 @@ import ( | |||
17 | "github.com/temoto/robotstxt" | 17 | "github.com/temoto/robotstxt" |
18 | ) | 18 | ) |
19 | 19 | ||
20 | type Item struct { | 20 | type gopherTemplateVariables struct { |
21 | Title string | ||
22 | URL string | ||
23 | Assets AssetList | ||
24 | Lines []GopherItem | ||
25 | Nav []GopherNavItem | ||
26 | IsPlain bool | ||
27 | } | ||
28 | |||
29 | type GopherNavItem struct { | ||
30 | Label string | ||
31 | URL string | ||
32 | Current bool | ||
33 | } | ||
34 | |||
35 | type GopherItem struct { | ||
21 | Link template.URL | 36 | Link template.URL |
22 | Type string | 37 | Type string |
23 | Text string | 38 | Text string |
24 | } | 39 | } |
25 | 40 | ||
41 | func trimLeftChars(s string, n int) string { | ||
42 | m := 0 | ||
43 | for i := range s { | ||
44 | if m >= n { | ||
45 | return s[i:] | ||
46 | } | ||
47 | m++ | ||
48 | } | ||
49 | return s[:0] | ||
50 | } | ||
51 | |||
52 | func urlToNav(url string) (items []GopherNavItem) { | ||
53 | partialURL := "/gopher" | ||
54 | parts := strings.Split(url, "/") | ||
55 | |||
56 | for i, part := range parts { | ||
57 | if i == 1 { | ||
58 | partialURL = partialURL + "/1" | ||
59 | part = trimLeftChars(part, 1) | ||
60 | |||
61 | if part == "" { | ||
62 | continue | ||
63 | } | ||
64 | } else { | ||
65 | partialURL = partialURL + "/" + part | ||
66 | } | ||
67 | |||
68 | current := false | ||
69 | if i == len(parts)-1 || (len(parts) == 2 && i == 0) { | ||
70 | current = true | ||
71 | } | ||
72 | |||
73 | items = append(items, GopherNavItem{ | ||
74 | Label: part, | ||
75 | URL: partialURL, | ||
76 | Current: current, | ||
77 | }) | ||
78 | } | ||
79 | |||
80 | return | ||
81 | } | ||
82 | |||
26 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { | 83 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { |
27 | var title string | 84 | var title string |
28 | 85 | ||
29 | out := make([]Item, len(d.Items)) | 86 | out := make([]GopherItem, len(d.Items)) |
30 | 87 | ||
31 | for i, x := range d.Items { | 88 | for i, x := range d.Items { |
32 | if x.Type == libgopher.INFO && x.Selector == "TITLE" { | 89 | if x.Type == libgopher.INFO && x.Selector == "TITLE" { |
@@ -34,7 +91,7 @@ func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetL | |||
34 | continue | 91 | continue |
35 | } | 92 | } |
36 | 93 | ||
37 | tr := Item{ | 94 | tr := GopherItem{ |
38 | Text: x.Description, | 95 | Text: x.Description, |
39 | Type: x.Type.String(), | 96 | Type: x.Type.String(), |
40 | } | 97 | } |
@@ -89,12 +146,12 @@ func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetL | |||
89 | } | 146 | } |
90 | } | 147 | } |
91 | 148 | ||
92 | return tpl.Execute(w, TemplateVariables{ | 149 | return tpl.Execute(w, gopherTemplateVariables{ |
93 | Title: title, | 150 | Title: title, |
94 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 151 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
95 | Assets: assetList, | 152 | Assets: assetList, |
96 | Lines: out, | 153 | Lines: out, |
97 | Protocol: "gopher", | 154 | Nav: urlToNav(fmt.Sprintf("%s/%s", hostport, uri)), |
98 | }) | 155 | }) |
99 | } | 156 | } |
100 | 157 | ||
@@ -130,13 +187,15 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
130 | 187 | ||
131 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | 188 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) |
132 | if err != nil { | 189 | if err != nil { |
133 | if e := tpl.Execute(w, TemplateVariables{ | 190 | if e := tpl.Execute(w, gopherTemplateVariables{ |
134 | Title: title, | 191 | Title: title, |
135 | URI: hostport, | 192 | URL: hostport, |
136 | Assets: assetList, | 193 | Assets: assetList, |
137 | RawText: fmt.Sprintf("Error: %s", err), | 194 | Lines: []GopherItem{{ |
138 | Error: true, | 195 | Text: fmt.Sprintf("Error: %s", err), |
139 | Protocol: "gopher", | 196 | }}, |
197 | Nav: urlToNav(hostport), | ||
198 | IsPlain: true, | ||
140 | }); e != nil { | 199 | }); e != nil { |
141 | log.Println("Template error: " + e.Error()) | 200 | log.Println("Template error: " + e.Error()) |
142 | log.Println(err.Error()) | 201 | log.Println(err.Error()) |
@@ -158,13 +217,15 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
158 | ) | 217 | ) |
159 | 218 | ||
160 | if err != nil { | 219 | if err != nil { |
161 | if e := tpl.Execute(w, TemplateVariables{ | 220 | if e := tpl.Execute(w, gopherTemplateVariables{ |
162 | Title: title, | 221 | Title: title, |
163 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 222 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
164 | Assets: assetList, | 223 | Assets: assetList, |
165 | RawText: fmt.Sprintf("Error: %s", err), | 224 | Lines: []GopherItem{{ |
166 | Error: true, | 225 | Text: fmt.Sprintf("Error: %s", err), |
167 | Protocol: "gopher", | 226 | }}, |
227 | Nav: urlToNav(fmt.Sprintf("%s/%s", hostport, uri)), | ||
228 | IsPlain: true, | ||
168 | }); e != nil { | 229 | }); e != nil { |
169 | log.Println("Template error: " + e.Error()) | 230 | log.Println("Template error: " + e.Error()) |
170 | } | 231 | } |
@@ -178,12 +239,15 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
178 | buf := new(bytes.Buffer) | 239 | buf := new(bytes.Buffer) |
179 | buf.ReadFrom(res.Body) | 240 | buf.ReadFrom(res.Body) |
180 | 241 | ||
181 | if err := tpl.Execute(w, TemplateVariables{ | 242 | if err := tpl.Execute(w, gopherTemplateVariables{ |
182 | Title: title, | 243 | Title: title, |
183 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 244 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
184 | Assets: assetList, | 245 | Assets: assetList, |
185 | RawText: buf.String(), | 246 | Lines: []GopherItem{{ |
186 | Protocol: "gopher", | 247 | Text: buf.String(), |
248 | }}, | ||
249 | Nav: urlToNav(fmt.Sprintf("%s/%s", hostport, uri)), | ||
250 | IsPlain: true, | ||
187 | }); err != nil { | 251 | }); err != nil { |
188 | log.Println("Template error: " + err.Error()) | 252 | log.Println("Template error: " + err.Error()) |
189 | } | 253 | } |
@@ -200,13 +264,15 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
200 | } | 264 | } |
201 | } else { | 265 | } else { |
202 | if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { | 266 | if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { |
203 | if e := tpl.Execute(w, TemplateVariables{ | 267 | if e := tpl.Execute(w, gopherTemplateVariables{ |
204 | Title: title, | 268 | Title: title, |
205 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 269 | URL: fmt.Sprintf("%s/%s", hostport, uri), |
206 | Assets: assetList, | 270 | Assets: assetList, |
207 | RawText: fmt.Sprintf("Error: %s", err), | 271 | Lines: []GopherItem{{ |
208 | Error: true, | 272 | Text: fmt.Sprintf("Error: %s", err), |
209 | Protocol: "gopher", | 273 | }}, |
274 | Nav: urlToNav(fmt.Sprintf("%s/%s", hostport, uri)), | ||
275 | IsPlain: false, | ||
210 | }); e != nil { | 276 | }); e != nil { |
211 | log.Println("Template error: " + e.Error()) | 277 | log.Println("Template error: " + e.Error()) |
212 | log.Println(e.Error()) | 278 | log.Println(e.Error()) |
diff --git a/internal/port/main.go b/internal/port/main.go index 9fa245e..267df44 100644 --- a/internal/port/main.go +++ b/internal/port/main.go | |||
@@ -26,23 +26,19 @@ type AssetList struct { | |||
26 | PropFontW2 string | 26 | PropFontW2 string |
27 | } | 27 | } |
28 | 28 | ||
29 | type TemplateVariables struct { | 29 | type startTemplateVariables struct { |
30 | Title string | 30 | Title string |
31 | URI string | 31 | URL string |
32 | Assets AssetList | 32 | Assets AssetList |
33 | RawText string | 33 | Content string |
34 | Lines []Item | ||
35 | Error bool | ||
36 | Protocol string | ||
37 | } | 34 | } |
38 | 35 | ||
39 | func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { | 36 | func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { |
40 | return func(w http.ResponseWriter, req *http.Request) { | 37 | return func(w http.ResponseWriter, req *http.Request) { |
41 | if err := tpl.Execute(w, TemplateVariables{ | 38 | if err := tpl.Execute(w, startTemplateVariables{ |
42 | Title: "Gopher/Gemini proxy", | 39 | Title: "Gopher/Gemini proxy", |
43 | Assets: assetList, | 40 | Assets: assetList, |
44 | RawText: startpagetext, | 41 | Content: startpagetext, |
45 | Protocol: "startpage", | ||
46 | }); err != nil { | 42 | }); err != nil { |
47 | log.Println("Template error: " + err.Error()) | 43 | log.Println("Template error: " + err.Error()) |
48 | } | 44 | } |
@@ -118,7 +114,7 @@ func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc { | |||
118 | // a robotstxt.RobotsData struct for testing user agents against | 114 | // a robotstxt.RobotsData struct for testing user agents against |
119 | // a configurable robots.txt file. | 115 | // a configurable robots.txt file. |
120 | func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { | 116 | func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { |
121 | box := packr.New("assets", "../assets") | 117 | box := packr.New("assets", "../../assets") |
122 | 118 | ||
123 | // | 119 | // |
124 | // Robots | 120 | // Robots |
@@ -205,24 +201,6 @@ func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug b | |||
205 | // | 201 | // |
206 | // | 202 | // |
207 | 203 | ||
208 | var templates *template.Template | ||
209 | |||
210 | var allFiles []string | ||
211 | files, err := ioutil.ReadDir("./tpl") | ||
212 | if err != nil { | ||
213 | fmt.Println(err) | ||
214 | } | ||
215 | for _, file := range files { | ||
216 | filename := file.Name() | ||
217 | if strings.HasSuffix(filename, ".html") { | ||
218 | allFiles = append(allFiles, "./tpl/"+filename) | ||
219 | } | ||
220 | } | ||
221 | |||
222 | templates, err = template.ParseFiles(allFiles...) | ||
223 | |||
224 | // | ||
225 | |||
226 | funcMap := template.FuncMap{ | 204 | funcMap := template.FuncMap{ |
227 | "safeHtml": func(s string) template.HTML { | 205 | "safeHtml": func(s string) template.HTML { |
228 | return template.HTML(s) | 206 | return template.HTML(s) |
@@ -266,9 +244,30 @@ func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug b | |||
266 | 244 | ||
267 | // | 245 | // |
268 | 246 | ||
269 | startpageTpl := templates.Lookup("startpage.html").Funcs(funcMap) | 247 | var templates *template.Template |
270 | geminiTpl := templates.Lookup("gemini.html").Funcs(funcMap) | 248 | |
271 | gopherTpl := templates.Lookup("gopher.html").Funcs(funcMap) | 249 | var allFiles []string |
250 | files, err := ioutil.ReadDir("./internal/port/tpl") | ||
251 | if err != nil { | ||
252 | return err | ||
253 | } | ||
254 | for _, file := range files { | ||
255 | filename := file.Name() | ||
256 | if strings.HasSuffix(filename, ".html") { | ||
257 | allFiles = append(allFiles, "./internal/port/tpl/"+filename) | ||
258 | } | ||
259 | } | ||
260 | |||
261 | templates, err = template.New("main.html").Funcs(funcMap).ParseFiles(allFiles...) | ||
262 | if err != nil { | ||
263 | return err | ||
264 | } | ||
265 | |||
266 | // | ||
267 | |||
268 | startpageTpl := templates.Lookup("startpage.html") | ||
269 | geminiTpl := templates.Lookup("gemini.html") | ||
270 | gopherTpl := templates.Lookup("gopher.html") | ||
272 | 271 | ||
273 | // | 272 | // |
274 | // | 273 | // |
diff --git a/internal/port/tpl/_fonts.html b/internal/port/tpl/_fonts.html new file mode 100644 index 0000000..b56aa22 --- /dev/null +++ b/internal/port/tpl/_fonts.html | |||
@@ -0,0 +1,16 @@ | |||
1 | <style> | ||
2 | @font-face { | ||
3 | font-family: 'Iosevka Term SS03'; | ||
4 | font-style: normal; | ||
5 | font-weight: normal; | ||
6 | src: url('{{ .Assets.FontW2 }}') format('woff2'), | ||
7 | url('{{ .Assets.FontW }}') format('woff'); | ||
8 | } | ||
9 | @font-face { | ||
10 | font-family: 'Iosevka Aile'; | ||
11 | font-style: normal; | ||
12 | font-weight: normal; | ||
13 | src: url('{{ .Assets.PropFontW2 }}') format('woff2'), | ||
14 | url('{{ .Assets.PropFontW }}') format('woff'); | ||
15 | } | ||
16 | </style> | ||
diff --git a/internal/port/tpl/_header.html b/internal/port/tpl/_header.html new file mode 100644 index 0000000..5bcd254 --- /dev/null +++ b/internal/port/tpl/_header.html | |||
@@ -0,0 +1,48 @@ | |||
1 | <header class="header header-base"> | ||
2 | <div class="location"> | ||
3 | <a class="location__prefix">{{ .Protocol }}://</a><a class="location__prefix location__prefix--mobile">://</a> | ||
4 | |||
5 | {{- if .URL -}} | ||
6 | {{- $page := . -}} | ||
7 | {{- $href := printf "/%s" .Protocol -}} | ||
8 | {{- $uriParts := split .URL "/" -}} | ||
9 | |||
10 | {{- $uriLast := $uriParts | last -}} | ||
11 | {{- $uriParts = $uriParts | pop -}} | ||
12 | {{- if eq $uriLast "" -}} | ||
13 | {{- $uriLast = $uriParts | last -}} | ||
14 | {{- $uriParts = $uriParts | pop -}} | ||
15 | {{- end -}} | ||
16 | |||
17 | {{- range $i, $part := $uriParts -}} | ||
18 | {{- if and (eq $page.Protocol "gopher") (eq $i 1) -}} | ||
19 | {{- $href = printf "%s/1" $href -}} | ||
20 | {{- $part = $part | trimLeftChar -}} | ||
21 | {{- if not (eq $part "") -}} | ||
22 | {{- $href = printf "%s/%s" $href $part -}} | ||
23 | <span class="location__slash">/</span><a href="{{ $href }}/" class="location__uripart">{{ $part }}</a> | ||
24 | {{- end -}} | ||
25 | {{- else -}} | ||
26 | {{- $href = printf "%s/%s" $href . -}} | ||
27 | {{- if ne $i 0 -}} | ||
28 | <span class="location__slash">/</span> | ||
29 | {{- end -}} | ||
30 | <a href="{{ $href }}/" class="location__uripart">{{ . }}</a> | ||
31 | {{- end -}} | ||
32 | {{- end -}} | ||
33 | {{- if ne (len $uriParts) 0 -}} | ||
34 | <span class="location__slash">/</span> | ||
35 | {{- end -}} | ||
36 | {{- if and (eq $page.Protocol "gopher") (eq (len $uriParts) 1) -}} | ||
37 | {{- $uriLast = $uriLast | trimLeftChar -}} | ||
38 | {{- end -}} | ||
39 | <span class="location__uripart">{{ $uriLast }}</span> | ||
40 | {{- end -}} | ||
41 | </div> | ||
42 | <div class="actions"> | ||
43 | {{- if and (not .Lines) (not .Error) (eq .Protocol "gopher") -}} | ||
44 | <div class="action"><a href="/gopher/{{ .URL | replace "^([^/]*)/0" "$1/9" }}">View raw</a></div> | ||
45 | {{- end -}} | ||
46 | <div class="action"><button class="settings-btn">Settings</button></div> | ||
47 | </div> | ||
48 | </header> | ||
diff --git a/internal/port/tpl/_modals.html b/internal/port/tpl/_modals.html new file mode 100644 index 0000000..3c08d9a --- /dev/null +++ b/internal/port/tpl/_modals.html | |||
@@ -0,0 +1,24 @@ | |||
1 | <aside class="modal modal--settings"> | ||
2 | <div class="modal__content"> | ||
3 | <header class="modal__head header-base"> | ||
4 | <h1 class="modal__title">Settings</h1> | ||
5 | <button class="modal__close-btn">Close</button> | ||
6 | </header> | ||
7 | <div class="setting setting--word-wrap"> | ||
8 | <strong class="setting__label">Wrap wide content</strong> | ||
9 | <button class="setting__value">[N/A]</button> | ||
10 | </div> | ||
11 | <div class="setting setting--monospace-font"> | ||
12 | <strong class="setting__label">Monospace font</strong> | ||
13 | <button class="setting__value">[N/A]</button> | ||
14 | </div> | ||
15 | <div class="setting setting--image-previews"> | ||
16 | <strong class="setting__label">Image thumbnails</strong> | ||
17 | <button class="setting__value">[N/A]</button> | ||
18 | </div> | ||
19 | <div class="setting setting--clickable-plain-links"> | ||
20 | <strong class="setting__label">Clickable links in text files</strong> | ||
21 | <button class="setting__value">[N/A]</button> | ||
22 | </div> | ||
23 | </div> | ||
24 | </aside> | ||
diff --git a/internal/port/tpl/gemini.html b/internal/port/tpl/gemini.html index e69de29..08f1b8e 100644 --- a/internal/port/tpl/gemini.html +++ b/internal/port/tpl/gemini.html | |||
@@ -0,0 +1,38 @@ | |||
1 | <!doctype html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
6 | <title>{{ .Title }} - Gemini proxy</title> | ||
7 | <link rel="stylesheet" href="{{ .Assets.Style }}" /> | ||
8 | {{- template "_fonts.html" . -}} | ||
9 | </head> | ||
10 | <body class="{{ if not .Lines }}is-plain{{ end }}"> | ||
11 | {{- template "_header.html" . -}} | ||
12 | |||
13 | <main class="wrap"> | ||
14 | <pre class="content content--has-monospace-font{{ if .Lines }} content--has-type-annotations{{ end }}"> | ||
15 | {{- if .Lines -}} | ||
16 | {{- $content := "" -}} | ||
17 | {{- range .Lines -}} | ||
18 | {{- if ne $content "" -}} | ||
19 | {{- $content = printf "%s\n" $content -}} | ||
20 | {{- end -}} | ||
21 | {{- if .Link -}} | ||
22 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\">%s </span><a class=\"link link--%s\" href=\"%s\">%s</a>" .Type .Type .Link (.Text | HTMLEscape)) -}} | ||
23 | {{- else -}} | ||
24 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\"> </span>%s" (.Text | HTMLEscape)) -}} | ||
25 | {{- end -}} | ||
26 | {{- end -}} | ||
27 | {{- $content | safeHtml -}} | ||
28 | {{- else -}} | ||
29 | {{- .RawText -}} | ||
30 | {{- end -}} | ||
31 | </pre> | ||
32 | </main> | ||
33 | |||
34 | {{- template "_modals.html" . -}} | ||
35 | |||
36 | <script src="{{ .Assets.JS }}"></script> | ||
37 | </body> | ||
38 | </html> | ||
diff --git a/internal/port/tpl/gopher.html b/internal/port/tpl/gopher.html index e69de29..c971847 100644 --- a/internal/port/tpl/gopher.html +++ b/internal/port/tpl/gopher.html | |||
@@ -0,0 +1,59 @@ | |||
1 | <!doctype html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
6 | <title>{{ .Title }} - Gopher proxy</title> | ||
7 | <link rel="stylesheet" href="{{ .Assets.Style }}" /> | ||
8 | {{- template "_fonts.html" . -}} | ||
9 | </head> | ||
10 | <body class="{{ if .IsPlain }}is-plain{{ end }}"> | ||
11 | <header class="header header-base"> | ||
12 | <div class="location"> | ||
13 | <a class="location__prefix">gopher://</a><a class="location__prefix location__prefix--mobile">://</a> | ||
14 | {{- range $i, $item := .Nav -}} | ||
15 | {{- if ne $i 0 -}} | ||
16 | <span class="location__slash">/</span> | ||
17 | {{- end -}} | ||
18 | {{- if .Current -}} | ||
19 | <span class="location__uripart">{{ .Label }}</span> | ||
20 | {{- else -}} | ||
21 | <a href="{{ .URL }}/" class="location__uripart">{{ .Label }}</a> | ||
22 | {{- end -}} | ||
23 | {{- end -}} | ||
24 | </div> | ||
25 | <div class="actions"> | ||
26 | {{- if .IsPlain -}} | ||
27 | <div class="action"><a href="/gopher/{{ .URL | replace "^([^/]*)/0" "$1/9" }}">View raw</a></div> | ||
28 | {{- end -}} | ||
29 | <div class="action"><button class="settings-btn">Settings</button></div> | ||
30 | </div> | ||
31 | </header> | ||
32 | |||
33 | <main class="wrap"> | ||
34 | <pre class="content content--has-monospace-font{{ if not .IsPlain }} content--has-type-annotations{{ end }}"> | ||
35 | {{- $content := "" -}} | ||
36 | {{- $page := . -}} | ||
37 | {{- range .Lines -}} | ||
38 | {{- if ne $content "" -}} | ||
39 | {{- $content = printf "%s\n" $content -}} | ||
40 | {{- end -}} | ||
41 | {{- if $page.IsPlain -}} | ||
42 | {{- $content = printf "%s%s" $content (.Text | HTMLEscape) -}} | ||
43 | {{- else -}} | ||
44 | {{- if .Link -}} | ||
45 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\">%s </span><a class=\"link link--%s\" href=\"%s\">%s</a>" .Type .Type .Link (.Text | HTMLEscape)) -}} | ||
46 | {{- else -}} | ||
47 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\"> </span>%s" (.Text | HTMLEscape)) -}} | ||
48 | {{- end -}} | ||
49 | {{- end -}} | ||
50 | {{- end -}} | ||
51 | {{- $content | safeHtml -}} | ||
52 | </pre> | ||
53 | </main> | ||
54 | |||
55 | {{- template "_modals.html" . -}} | ||
56 | |||
57 | <script src="{{ .Assets.JS }}"></script> | ||
58 | </body> | ||
59 | </html> | ||
diff --git a/internal/port/tpl/startpage.html b/internal/port/tpl/startpage.html index 8482a6f..cfe519d 100644 --- a/internal/port/tpl/startpage.html +++ b/internal/port/tpl/startpage.html | |||
@@ -3,118 +3,28 @@ | |||
3 | <head> | 3 | <head> |
4 | <meta charset="utf-8"> | 4 | <meta charset="utf-8"> |
5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
6 | <title>{{ .Title }}{{ if ne .Protocol "startpage" }} - {{ .Protocol | title }} proxy{{ end }}</title> | 6 | <title>{{ .Title }}</title> |
7 | <link rel="stylesheet" href="{{ .Assets.Style }}" /> | 7 | <link rel="stylesheet" href="{{ .Assets.Style }}" /> |
8 | <style> | 8 | {{- template "_fonts.html" . -}} |
9 | @font-face { | ||
10 | font-family: 'Iosevka Term SS03'; | ||
11 | font-style: normal; | ||
12 | font-weight: normal; | ||
13 | src: url('{{ .Assets.FontW2 }}') format('woff2'), | ||
14 | url('{{ .Assets.FontW }}') format('woff'); | ||
15 | } | ||
16 | @font-face { | ||
17 | font-family: 'Iosevka Aile'; | ||
18 | font-style: normal; | ||
19 | font-weight: normal; | ||
20 | src: url('{{ .Assets.PropFontW2 }}') format('woff2'), | ||
21 | url('{{ .Assets.PropFontW }}') format('woff'); | ||
22 | } | ||
23 | </style> | ||
24 | </head> | 9 | </head> |
25 | <body class="{{ if not .Lines }}is-plain{{ end }}"> | 10 | <body class="is-plain"> |
26 | <header class="header header-base"> | 11 | <header class="header header-base"> |
27 | <div class="location"> | 12 | <div class="location"> |
28 | <a class="location__prefix">{{ .Protocol }}://</a><a class="location__prefix location__prefix--mobile">://</a> | 13 | <a class="location__prefix">start://</a><a class="location__prefix location__prefix--mobile">://</a> |
29 | 14 | </div> | |
30 | {{- if .URI -}} | 15 | <div class="actions"> |
31 | {{- $page := . -}} | 16 | <div class="action"><button class="settings-btn">Settings</button></div> |
32 | {{- $href := printf "/%s" .Protocol -}} | 17 | </div> |
33 | {{- $uriParts := split .URI "/" -}} | 18 | </header> |
34 | |||
35 | {{- $uriLast := $uriParts | last -}} | ||
36 | {{- $uriParts = $uriParts | pop -}} | ||
37 | {{- if eq $uriLast "" -}} | ||
38 | {{- $uriLast = $uriParts | last -}} | ||
39 | {{- $uriParts = $uriParts | pop -}} | ||
40 | {{- end -}} | ||
41 | 19 | ||
42 | {{- range $i, $part := $uriParts -}} | ||
43 | {{- if and (eq $page.Protocol "gopher") (eq $i 1) -}} | ||
44 | {{- $href = printf "%s/1" $href -}} | ||
45 | {{- $part = $part | trimLeftChar -}} | ||
46 | {{- if not (eq $part "") -}} | ||
47 | {{- $href = printf "%s/%s" $href $part -}} | ||
48 | <span class="location__slash">/</span><a href="{{ $href }}/" class="location__uripart">{{ $part }}</a> | ||
49 | {{- end -}} | ||
50 | {{- else -}} | ||
51 | {{- $href = printf "%s/%s" $href . -}} | ||
52 | {{- if ne $i 0 -}} | ||
53 | <span class="location__slash">/</span> | ||
54 | {{- end -}} | ||
55 | <a href="{{ $href }}/" class="location__uripart">{{ . }}</a> | ||
56 | {{- end -}} | ||
57 | {{- end -}} | ||
58 | {{- if ne (len $uriParts) 0 -}} | ||
59 | <span class="location__slash">/</span> | ||
60 | {{- end -}} | ||
61 | {{- if and (eq $page.Protocol "gopher") (eq (len $uriParts) 1) -}} | ||
62 | {{- $uriLast = $uriLast | trimLeftChar -}} | ||
63 | {{- end -}} | ||
64 | <span class="location__uripart">{{ $uriLast }}</span> | ||
65 | {{- end -}} | ||
66 | </div> | ||
67 | <div class="actions"> | ||
68 | {{- if and (not .Lines) (not .Error) (eq .Protocol "gopher") -}} | ||
69 | <div class="action"><a href="/gopher/{{ .URI | replace "^([^/]*)/0" "$1/9" }}">View raw</a></div> | ||
70 | {{- end -}} | ||
71 | <div class="action"><button class="settings-btn">Settings</button></div> | ||
72 | </div> | ||
73 | </header> | ||
74 | <main class="wrap"> | 20 | <main class="wrap"> |
75 | <pre class="content content--has-monospace-font{{ if .Lines }} content--has-type-annotations{{ end }}"> | 21 | <pre class="content content--has-monospace-font"> |
76 | {{- if .Lines -}} | 22 | {{- .Content -}} |
77 | {{- $content := "" -}} | ||
78 | {{- range .Lines -}} | ||
79 | {{- if ne $content "" -}} | ||
80 | {{- $content = printf "%s\n" $content -}} | ||
81 | {{- end -}} | ||
82 | {{- if .Link -}} | ||
83 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\">%s </span><a class=\"link link--%s\" href=\"%s\">%s</a>" .Type .Type .Link (.Text | HTMLEscape)) -}} | ||
84 | {{- else -}} | ||
85 | {{- $content = printf "%s%s" $content (printf "<span class=\"type-annotation\"> </span>%s" (.Text | HTMLEscape)) -}} | ||
86 | {{- end -}} | ||
87 | {{- end -}} | ||
88 | {{- $content | safeHtml -}} | ||
89 | {{- else -}} | ||
90 | {{- .RawText -}} | ||
91 | {{- end -}} | ||
92 | </pre> | 23 | </pre> |
93 | </main> | 24 | </main> |
94 | <aside class="modal modal--settings"> | 25 | |
95 | <div class="modal__content"> | 26 | {{- template "_modals.html" . -}} |
96 | <header class="modal__head header-base"> | 27 | |
97 | <h1 class="modal__title">Settings</h1> | ||
98 | <button class="modal__close-btn">Close</button> | ||
99 | </header> | ||
100 | <div class="setting setting--word-wrap"> | ||
101 | <strong class="setting__label">Wrap wide content</strong> | ||
102 | <button class="setting__value">[N/A]</button> | ||
103 | </div> | ||
104 | <div class="setting setting--monospace-font"> | ||
105 | <strong class="setting__label">Monospace font</strong> | ||
106 | <button class="setting__value">[N/A]</button> | ||
107 | </div> | ||
108 | <div class="setting setting--image-previews"> | ||
109 | <strong class="setting__label">Image thumbnails</strong> | ||
110 | <button class="setting__value">[N/A]</button> | ||
111 | </div> | ||
112 | <div class="setting setting--clickable-plain-links"> | ||
113 | <strong class="setting__label">Clickable links in text files</strong> | ||
114 | <button class="setting__value">[N/A]</button> | ||
115 | </div> | ||
116 | </div> | ||
117 | </aside> | ||
118 | <script src="{{ .Assets.JS }}"></script> | 28 | <script src="{{ .Assets.JS }}"></script> |
119 | </body> | 29 | </body> |
120 | </html> | 30 | </html> |
diff --git a/pkg/libgemini/libgemini.go b/pkg/libgemini/libgemini.go index 71012ef..48a8ed0 100644 --- a/pkg/libgemini/libgemini.go +++ b/pkg/libgemini/libgemini.go | |||
@@ -6,7 +6,6 @@ import ( | |||
6 | "crypto/tls" | 6 | "crypto/tls" |
7 | "errors" | 7 | "errors" |
8 | "fmt" | 8 | "fmt" |
9 | "html/template" | ||
10 | "io" | 9 | "io" |
11 | "mime" | 10 | "mime" |
12 | "net" | 11 | "net" |
@@ -74,19 +73,19 @@ type Response struct { | |||
74 | type GeminiDocSectionType byte | 73 | type GeminiDocSectionType byte |
75 | 74 | ||
76 | const ( | 75 | const ( |
77 | RAW_TEXT = SectionType(0) | 76 | RAW_TEXT = GeminiDocSectionType(0) |
78 | REFLOW_TEXT = SectionType(1) | 77 | REFLOW_TEXT = GeminiDocSectionType(1) |
79 | LINK = SectionType(2) | 78 | LINK = GeminiDocSectionType(2) |
80 | HEADING_1 = SectionType(3) | 79 | HEADING_1 = GeminiDocSectionType(3) |
81 | HEADING_2 = SectionType(4) | 80 | HEADING_2 = GeminiDocSectionType(4) |
82 | HEADING_3 = SectionType(5) | 81 | HEADING_3 = GeminiDocSectionType(5) |
83 | LIST = SectionType(6) | 82 | LIST = GeminiDocSectionType(6) |
84 | ) | 83 | ) |
85 | 84 | ||
86 | type GeminiDocSection struct { | 85 | type GeminiDocSection struct { |
87 | Type SectionType | 86 | Type GeminiDocSectionType |
88 | Text string | 87 | Text string |
89 | URL template.URL | 88 | URL string |
90 | Items []string | 89 | Items []string |
91 | } | 90 | } |
92 | 91 | ||
@@ -171,13 +170,13 @@ func ParseHeader(line string) (header *Header, err error) { | |||
171 | return | 170 | return |
172 | } | 171 | } |
173 | 172 | ||
174 | func ParseGeminiDocument(body *bytes.Buffer) (sections []Section) { | 173 | func ParseGeminiDocument(body *bytes.Buffer) (sections []GeminiDocSection) { |
175 | scanner := bufio.NewScanner(body) | 174 | scanner := bufio.NewScanner(body) |
176 | 175 | ||
177 | reflow := true | 176 | reflow := true |
178 | ignoreSection := true | 177 | ignoreSection := true |
179 | section := Section{ | 178 | section := GeminiDocSection{ |
180 | Type: REFLOW_TEXT | 179 | Type: REFLOW_TEXT, |
181 | } | 180 | } |
182 | 181 | ||
183 | for scanner.Scan() { | 182 | for scanner.Scan() { |
@@ -185,23 +184,23 @@ func ParseGeminiDocument(body *bytes.Buffer) (sections []Section) { | |||
185 | line = TermEscapeSGRPattern.ReplaceAllString(line, "") | 184 | line = TermEscapeSGRPattern.ReplaceAllString(line, "") |
186 | 185 | ||
187 | reflowMatch := ReflowModePattern.FindStringSubmatch(line) | 186 | reflowMatch := ReflowModePattern.FindStringSubmatch(line) |
188 | if len(heading3Match) != 0 { | 187 | if len(reflowMatch) != 0 && reflowMatch[0] != "" { |
189 | reflow = !reflow | 188 | reflow = !reflow |
190 | continue | 189 | continue |
191 | } | 190 | } |
192 | 191 | ||
193 | if !reflow { | 192 | if !reflow { |
194 | if !ignoreSection { | 193 | if !ignoreSection { |
195 | if section.Type != REFLOW_TEXT { | 194 | if section.Type != RAW_TEXT { |
196 | sections = append(sections, section) | 195 | sections = append(sections, section) |
197 | section = Section{ | 196 | section = GeminiDocSection{ |
198 | Type: REFLOW_TEXT | 197 | Type: RAW_TEXT, |
199 | } | 198 | } |
200 | } | 199 | } |
201 | } else { | 200 | } else { |
202 | ignoreSection = false | 201 | ignoreSection = false |
203 | section = Section{ | 202 | section = GeminiDocSection{ |
204 | Type: REFLOW_TEXT | 203 | Type: RAW_TEXT, |
205 | } | 204 | } |
206 | } | 205 | } |
207 | 206 | ||
@@ -222,80 +221,96 @@ func ParseGeminiDocument(body *bytes.Buffer) (sections []Section) { | |||
222 | } | 221 | } |
223 | 222 | ||
224 | ignoreSection = false | 223 | ignoreSection = false |
225 | section = Section{ | 224 | section = GeminiDocSection{ |
226 | Type: LINK, | 225 | Type: LINK, |
227 | Text: label, | 226 | Text: label, |
228 | URL: template.URL(resolveURI(linkMatch[1], baseURL)), | 227 | URL: linkMatch[1], |
229 | } | 228 | } |
230 | 229 | ||
231 | continue | 230 | continue |
232 | } | 231 | } |
233 | 232 | ||
234 | heading3Match := Heading3Pattern.FindStringSubmatch(line) | 233 | heading3Match := Heading3Pattern.FindStringSubmatch(line) |
235 | if len(heading3Match) != 0 { | 234 | if len(heading3Match) != 0 && heading3Match[0] != "" { |
236 | if !ignoreSection { | 235 | if !ignoreSection { |
237 | sections = append(sections, section) | 236 | sections = append(sections, section) |
238 | } | 237 | } |
239 | 238 | ||
240 | ignoreSection = false | 239 | ignoreSection = false |
241 | section = Section{ | 240 | section = GeminiDocSection{ |
242 | Type: HEADING_3, | 241 | Type: HEADING_3, |
243 | Text: heading3Match[1] | 242 | Text: heading3Match[1], |
244 | } | 243 | } |
245 | 244 | ||
246 | continue | 245 | continue |
247 | } | 246 | } |
248 | 247 | ||
249 | heading2Match := Heading2Pattern.FindStringSubmatch(line) | 248 | heading2Match := Heading2Pattern.FindStringSubmatch(line) |
250 | if len(heading2Match) != 0 { | 249 | if len(heading2Match) != 0 && heading2Match[0] != "" { |
251 | if !ignoreSection { | 250 | if !ignoreSection { |
252 | sections = append(sections, section) | 251 | sections = append(sections, section) |
253 | } | 252 | } |
254 | 253 | ||
255 | ignoreSection = false | 254 | ignoreSection = false |
256 | section = Section{ | 255 | section = GeminiDocSection{ |
257 | Type: HEADING_2, | 256 | Type: HEADING_2, |
258 | Text: heading2Match[1] | 257 | Text: heading2Match[1], |
259 | } | 258 | } |
260 | 259 | ||
261 | continue | 260 | continue |
262 | } | 261 | } |
263 | 262 | ||
264 | heading1Match := Heading1Pattern.FindStringSubmatch(line) | 263 | heading1Match := Heading1Pattern.FindStringSubmatch(line) |
265 | if len(heading1Match) != 0 { | 264 | if len(heading1Match) != 0 && heading1Match[0] != "" { |
266 | if !ignoreSection { | 265 | if !ignoreSection { |
267 | sections = append(sections, section) | 266 | sections = append(sections, section) |
268 | } | 267 | } |
269 | 268 | ||
270 | ignoreSection = false | 269 | ignoreSection = false |
271 | section = Section{ | 270 | section = GeminiDocSection{ |
272 | Type: HEADING_1, | 271 | Type: HEADING_1, |
273 | Text: heading1Match[1] | 272 | Text: heading1Match[1], |
274 | } | 273 | } |
275 | 274 | ||
276 | continue | 275 | continue |
277 | } | 276 | } |
278 | 277 | ||
279 | listItemMatch := ListItemPattern.FindStringSubmatch(line) | 278 | listItemMatch := ListItemPattern.FindStringSubmatch(line) |
280 | if len(listItemMatch) != 0 { | 279 | if len(listItemMatch) != 0 && listItemMatch[0] != "" { |
281 | if !ignoreSection { | 280 | if !ignoreSection { |
282 | if section.Type != LIST { | 281 | if section.Type != LIST { |
283 | sections = append(sections, section) | 282 | sections = append(sections, section) |
284 | section = Section{ | 283 | section = GeminiDocSection{ |
285 | Type: LIST | 284 | Type: LIST, |
286 | } | 285 | } |
287 | } | 286 | } |
288 | } else { | 287 | } else { |
289 | ignoreSection = false | 288 | ignoreSection = false |
290 | section = Section{ | 289 | section = GeminiDocSection{ |
291 | Type: LIST, | 290 | Type: LIST, |
292 | } | 291 | } |
293 | } | 292 | } |
294 | 293 | ||
295 | section.Items = append(section.Items, listItemMatch[1]) | 294 | section.Items = append(section.Items, listItemMatch[1]) |
296 | 295 | ||
297 | continue | 296 | continue |
298 | } | 297 | } |
298 | |||
299 | if !ignoreSection { | ||
300 | if section.Type != REFLOW_TEXT { | ||
301 | sections = append(sections, section) | ||
302 | section = GeminiDocSection{ | ||
303 | Type: REFLOW_TEXT, | ||
304 | } | ||
305 | } | ||
306 | } else { | ||
307 | ignoreSection = false | ||
308 | section = GeminiDocSection{ | ||
309 | Type: REFLOW_TEXT, | ||
310 | } | ||
311 | } | ||
312 | |||
313 | section.Text = section.Text + "\n" + line | ||
299 | } | 314 | } |
300 | 315 | ||
301 | if !ignoreSection { | 316 | if !ignoreSection { |
diff --git a/pkg/libgopher/libgopher.go b/pkg/libgopher/libgopher.go index 86d58ff..dcf13ee 100644 --- a/pkg/libgopher/libgopher.go +++ b/pkg/libgopher/libgopher.go | |||
@@ -184,7 +184,7 @@ type Response struct { | |||
184 | Body io.Reader | 184 | Body io.Reader |
185 | } | 185 | } |
186 | 186 | ||
187 | // Get fetches a Gopher resource by URI | 187 | // Get fetches a Gopher resource by URL |
188 | func Get(uri string) (*Response, error) { | 188 | func Get(uri string) (*Response, error) { |
189 | u, err := url.Parse(uri) | 189 | u, err := url.Parse(uri) |
190 | if err != nil { | 190 | if err != nil { |