diff options
Diffstat (limited to 'internal/port/gemini.go')
-rw-r--r-- | internal/port/gemini.go | 176 |
1 files changed, 64 insertions, 112 deletions
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 { |