diff options
-rw-r--r-- | internal/port/gemini.go | 176 | ||||
-rw-r--r-- | internal/port/gopher.go | 2 | ||||
-rw-r--r-- | internal/port/main.go | 2 | ||||
-rw-r--r-- | pkg/libgemini/libgemini.go | 165 |
4 files changed, 303 insertions, 42 deletions
diff --git a/internal/port/gemini.go b/internal/port/gemini.go index f9b0b97..b10da7d 100644 --- a/internal/port/gemini.go +++ b/internal/port/gemini.go | |||
@@ -1,6 +1,7 @@ | |||
1 | package port | 1 | package port |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "bufio" | ||
4 | "bytes" | 5 | "bytes" |
5 | "fmt" | 6 | "fmt" |
6 | "html/template" | 7 | "html/template" |
@@ -15,11 +16,32 @@ import ( | |||
15 | "golang.org/x/net/html/charset" | 16 | "golang.org/x/net/html/charset" |
16 | "golang.org/x/text/transform" | 17 | "golang.org/x/text/transform" |
17 | 18 | ||
18 | "git.vulpes.one/Feuerfuchs/port/port/libgemini" | 19 | "git.vulpes.one/Feuerfuchs/port/pkg/libgemini" |
19 | 20 | ||
20 | "github.com/temoto/robotstxt" | 21 | "github.com/temoto/robotstxt" |
21 | ) | 22 | ) |
22 | 23 | ||
24 | type SectionType byte | ||
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 | ||
40 | URI string | ||
41 | Assets AssetList | ||
42 | Sections []Section | ||
43 | } | ||
44 | |||
23 | var ( | 45 | var ( |
24 | TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") | 46 | TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") |
25 | ) | 47 | ) |
@@ -53,6 +75,80 @@ func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | |||
53 | return | 75 | return |
54 | } | 76 | } |
55 | 77 | ||
78 | func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []Section) { | ||
79 | baseURL, err := url.Parse(fmt.Sprintf( | ||
80 | "gemini://%s/%s", | ||
81 | hostport, | ||
82 | uri, | ||
83 | )) | ||
84 | if err != nil { | ||
85 | return []Section{} | ||
86 | } | ||
87 | |||
88 | skipSection := true | ||
89 | |||
90 | section := Section{ | ||
91 | Type: RAW_TEXT, | ||
92 | } | ||
93 | |||
94 | scanner := bufio.NewScanner(body) | ||
95 | |||
96 | for scanner.Scan() { | ||
97 | line := strings.Trim(scanner.Text(), "\r\n") | ||
98 | line = TermEscapeSGRPattern.ReplaceAllString(line, "") | ||
99 | |||
100 | linkMatch := libgemini.LinkPattern.FindStringSubmatch(line) | ||
101 | if len(linkMatch) != 0 && linkMatch[0] != "" { | ||
102 | curType := section.Type | ||
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 | }) | ||
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 | } | ||
143 | } | ||
144 | |||
145 | if !skipSection { | ||
146 | sections = append(sections, section) | ||
147 | } | ||
148 | |||
149 | return | ||
150 | } | ||
151 | |||
56 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { | 152 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { |
57 | return func(w http.ResponseWriter, req *http.Request) { | 153 | return func(w http.ResponseWriter, req *http.Request) { |
58 | agent := req.UserAgent() | 154 | agent := req.UserAgent() |
@@ -80,13 +176,14 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
80 | 176 | ||
81 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | 177 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) |
82 | if err != nil { | 178 | if err != nil { |
83 | if e := tpl.Execute(w, TemplateVariables{ | 179 | if e := tpl.Execute(w, templateVariables{ |
84 | Title: title, | 180 | Title: title, |
85 | URI: hostport, | 181 | URI: hostport, |
86 | Assets: assetList, | 182 | Assets: assetList, |
87 | RawText: fmt.Sprintf("Error: %s", err), | 183 | Sections: []Section{{ |
88 | Error: true, | 184 | Type: RAW_TEXT, |
89 | Protocol: "gemini", | 185 | Text: fmt.Sprintf("Error: %s", err), |
186 | }}, | ||
90 | }); e != nil { | 187 | }); e != nil { |
91 | log.Println("Template error: " + e.Error()) | 188 | log.Println("Template error: " + e.Error()) |
92 | log.Println(err.Error()) | 189 | log.Println(err.Error()) |
@@ -108,13 +205,14 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
108 | ) | 205 | ) |
109 | 206 | ||
110 | if err != nil { | 207 | if err != nil { |
111 | if e := tpl.Execute(w, TemplateVariables{ | 208 | if e := tpl.Execute(w, templateVariables{ |
112 | Title: title, | 209 | Title: title, |
113 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 210 | URI: fmt.Sprintf("%s/%s", hostport, uri), |
114 | Assets: assetList, | 211 | Assets: assetList, |
115 | RawText: fmt.Sprintf("Error: %s", err), | 212 | Sections: []Section{{ |
116 | Error: true, | 213 | Type: RAW_TEXT, |
117 | Protocol: "gemini", | 214 | Text: fmt.Sprintf("Error: %s", err), |
215 | }}, | ||
118 | }); e != nil { | 216 | }); e != nil { |
119 | log.Println("Template error: " + e.Error()) | 217 | log.Println("Template error: " + e.Error()) |
120 | log.Println(err.Error()) | 218 | log.Println(err.Error()) |
@@ -129,13 +227,14 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
129 | uri, | 227 | uri, |
130 | )) | 228 | )) |
131 | if err != nil { | 229 | if err != nil { |
132 | if e := tpl.Execute(w, TemplateVariables{ | 230 | if e := tpl.Execute(w, templateVariables{ |
133 | Title: title, | 231 | Title: title, |
134 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 232 | URI: fmt.Sprintf("%s/%s", hostport, uri), |
135 | Assets: assetList, | 233 | Assets: assetList, |
136 | RawText: fmt.Sprintf("Error: %s", err), | 234 | Sections: []Section{{ |
137 | Error: true, | 235 | Type: RAW_TEXT, |
138 | Protocol: "gemini", | 236 | Text: fmt.Sprintf("Error: %s", err), |
237 | }}, | ||
139 | }); e != nil { | 238 | }); e != nil { |
140 | log.Println("Template error: " + e.Error()) | 239 | log.Println("Template error: " + e.Error()) |
141 | log.Println(err.Error()) | 240 | log.Println(err.Error()) |
@@ -148,13 +247,14 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
148 | } | 247 | } |
149 | 248 | ||
150 | if int(res.Header.Status/10) != 2 { | 249 | if int(res.Header.Status/10) != 2 { |
151 | if err := tpl.Execute(w, TemplateVariables{ | 250 | if err := tpl.Execute(w, templateVariables{ |
152 | Title: title, | 251 | Title: title, |
153 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 252 | URI: fmt.Sprintf("%s/%s", hostport, uri), |
154 | Assets: assetList, | 253 | Assets: assetList, |
155 | RawText: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), | 254 | Sections: []Section{{ |
156 | Error: true, | 255 | Type: RAW_TEXT, |
157 | Protocol: "gemini", | 256 | Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), |
257 | }}, | ||
158 | }); err != nil { | 258 | }); err != nil { |
159 | log.Println("Template error: " + err.Error()) | 259 | log.Println("Template error: " + err.Error()) |
160 | } | 260 | } |
@@ -177,24 +277,22 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
177 | writer.Close() | 277 | writer.Close() |
178 | } | 278 | } |
179 | 279 | ||
180 | var ( | 280 | var sections []Section |
181 | rawText string | ||
182 | items []Item | ||
183 | ) | ||
184 | 281 | ||
185 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { | 282 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { |
186 | items = parseGeminiDocument(buf, uri, hostport) | 283 | sections = parseGeminiDocument(buf, uri, hostport) |
187 | } else { | 284 | } else { |
188 | rawText = buf.String() | 285 | sections = append(sections, Section{ |
286 | Type: RAW_TEXT, | ||
287 | Text: buf.String(), | ||
288 | }) | ||
189 | } | 289 | } |
190 | 290 | ||
191 | if err := tpl.Execute(w, TemplateVariables{ | 291 | if err := tpl.Execute(w, templateVariables{ |
192 | Title: title, | 292 | Title: title, |
193 | URI: fmt.Sprintf("%s/%s", hostport, uri), | 293 | URI: fmt.Sprintf("%s/%s", hostport, uri), |
194 | Assets: assetList, | 294 | Assets: assetList, |
195 | Lines: items, | 295 | Sections: sections, |
196 | RawText: rawText, | ||
197 | Protocol: "gemini", | ||
198 | }); err != nil { | 296 | }); err != nil { |
199 | log.Println("Template error: " + err.Error()) | 297 | log.Println("Template error: " + err.Error()) |
200 | } | 298 | } |
diff --git a/internal/port/gopher.go b/internal/port/gopher.go index ebeb213..abbc4d9 100644 --- a/internal/port/gopher.go +++ b/internal/port/gopher.go | |||
@@ -11,7 +11,7 @@ import ( | |||
11 | "net/url" | 11 | "net/url" |
12 | "strings" | 12 | "strings" |
13 | 13 | ||
14 | "git.vulpes.one/Feuerfuchs/port/port/libgopher" | 14 | "git.vulpes.one/Feuerfuchs/port/pkg/libgopher" |
15 | 15 | ||
16 | "github.com/davidbyttow/govips/pkg/vips" | 16 | "github.com/davidbyttow/govips/pkg/vips" |
17 | "github.com/temoto/robotstxt" | 17 | "github.com/temoto/robotstxt" |
diff --git a/internal/port/main.go b/internal/port/main.go index 5cdd794..9fa245e 100644 --- a/internal/port/main.go +++ b/internal/port/main.go | |||
@@ -205,6 +205,8 @@ func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug b | |||
205 | // | 205 | // |
206 | // | 206 | // |
207 | 207 | ||
208 | var templates *template.Template | ||
209 | |||
208 | var allFiles []string | 210 | var allFiles []string |
209 | files, err := ioutil.ReadDir("./tpl") | 211 | files, err := ioutil.ReadDir("./tpl") |
210 | if err != nil { | 212 | if err != nil { |
diff --git a/pkg/libgemini/libgemini.go b/pkg/libgemini/libgemini.go index 303490c..71012ef 100644 --- a/pkg/libgemini/libgemini.go +++ b/pkg/libgemini/libgemini.go | |||
@@ -2,9 +2,11 @@ package libgemini | |||
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "bufio" | 4 | "bufio" |
5 | "bytes" | ||
5 | "crypto/tls" | 6 | "crypto/tls" |
6 | "errors" | 7 | "errors" |
7 | "fmt" | 8 | "fmt" |
9 | "html/template" | ||
8 | "io" | 10 | "io" |
9 | "mime" | 11 | "mime" |
10 | "net" | 12 | "net" |
@@ -49,8 +51,14 @@ const ( | |||
49 | ) | 51 | ) |
50 | 52 | ||
51 | var ( | 53 | var ( |
52 | HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") | 54 | HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") |
53 | LinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") | 55 | LinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") |
56 | ReflowModePattern = regexp.MustCompile("^```(.*)$") | ||
57 | Heading1Pattern = regexp.MustCompile("^#(.*)$") | ||
58 | Heading2Pattern = regexp.MustCompile("^##(.*)$") | ||
59 | Heading3Pattern = regexp.MustCompile("^###(.*)$") | ||
60 | ListItemPattern = regexp.MustCompile("^\\*(.*)$") | ||
61 | TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") | ||
54 | ) | 62 | ) |
55 | 63 | ||
56 | type Header struct { | 64 | type Header struct { |
@@ -63,6 +71,25 @@ type Response struct { | |||
63 | Body io.Reader | 71 | Body io.Reader |
64 | } | 72 | } |
65 | 73 | ||
74 | type GeminiDocSectionType byte | ||
75 | |||
76 | const ( | ||
77 | RAW_TEXT = SectionType(0) | ||
78 | REFLOW_TEXT = SectionType(1) | ||
79 | LINK = SectionType(2) | ||
80 | HEADING_1 = SectionType(3) | ||
81 | HEADING_2 = SectionType(4) | ||
82 | HEADING_3 = SectionType(5) | ||
83 | LIST = SectionType(6) | ||
84 | ) | ||
85 | |||
86 | type GeminiDocSection struct { | ||
87 | Type SectionType | ||
88 | Text string | ||
89 | URL template.URL | ||
90 | Items []string | ||
91 | } | ||
92 | |||
66 | func Get(uri string) (*Response, error) { | 93 | func Get(uri string) (*Response, error) { |
67 | u, err := url.Parse(uri) | 94 | u, err := url.Parse(uri) |
68 | if err != nil { | 95 | if err != nil { |
@@ -143,3 +170,137 @@ func ParseHeader(line string) (header *Header, err error) { | |||
143 | 170 | ||
144 | return | 171 | return |
145 | } | 172 | } |
173 | |||
174 | func ParseGeminiDocument(body *bytes.Buffer) (sections []Section) { | ||
175 | scanner := bufio.NewScanner(body) | ||
176 | |||
177 | reflow := true | ||
178 | ignoreSection := true | ||
179 | section := Section{ | ||
180 | Type: REFLOW_TEXT | ||
181 | } | ||
182 | |||
183 | for scanner.Scan() { | ||
184 | line := strings.Trim(scanner.Text(), CRLF) | ||
185 | line = TermEscapeSGRPattern.ReplaceAllString(line, "") | ||
186 | |||
187 | reflowMatch := ReflowModePattern.FindStringSubmatch(line) | ||
188 | if len(heading3Match) != 0 { | ||
189 | reflow = !reflow | ||
190 | continue | ||
191 | } | ||
192 | |||
193 | if !reflow { | ||
194 | if !ignoreSection { | ||
195 | if section.Type != REFLOW_TEXT { | ||
196 | sections = append(sections, section) | ||
197 | section = Section{ | ||
198 | Type: REFLOW_TEXT | ||
199 | } | ||
200 | } | ||
201 | } else { | ||
202 | ignoreSection = false | ||
203 | section = Section{ | ||
204 | Type: REFLOW_TEXT | ||
205 | } | ||
206 | } | ||
207 | |||
208 | section.Text = section.Text + "\n" + line | ||
209 | |||
210 | continue | ||
211 | } | ||
212 | |||
213 | linkMatch := LinkPattern.FindStringSubmatch(line) | ||
214 | if len(linkMatch) != 0 && linkMatch[0] != "" { | ||
215 | if !ignoreSection { | ||
216 | sections = append(sections, section) | ||
217 | } | ||
218 | |||
219 | label := linkMatch[2] | ||
220 | if label == "" { | ||
221 | label = linkMatch[1] | ||
222 | } | ||
223 | |||
224 | ignoreSection = false | ||
225 | section = Section{ | ||
226 | Type: LINK, | ||
227 | Text: label, | ||
228 | URL: template.URL(resolveURI(linkMatch[1], baseURL)), | ||
229 | } | ||
230 | |||
231 | continue | ||
232 | } | ||
233 | |||
234 | heading3Match := Heading3Pattern.FindStringSubmatch(line) | ||
235 | if len(heading3Match) != 0 { | ||
236 | if !ignoreSection { | ||
237 | sections = append(sections, section) | ||
238 | } | ||
239 | |||
240 | ignoreSection = false | ||
241 | section = Section{ | ||
242 | Type: HEADING_3, | ||
243 | Text: heading3Match[1] | ||
244 | } | ||
245 | |||
246 | continue | ||
247 | } | ||
248 | |||
249 | heading2Match := Heading2Pattern.FindStringSubmatch(line) | ||
250 | if len(heading2Match) != 0 { | ||
251 | if !ignoreSection { | ||
252 | sections = append(sections, section) | ||
253 | } | ||
254 | |||
255 | ignoreSection = false | ||
256 | section = Section{ | ||
257 | Type: HEADING_2, | ||
258 | Text: heading2Match[1] | ||
259 | } | ||
260 | |||
261 | continue | ||
262 | } | ||
263 | |||
264 | heading1Match := Heading1Pattern.FindStringSubmatch(line) | ||
265 | if len(heading1Match) != 0 { | ||
266 | if !ignoreSection { | ||
267 | sections = append(sections, section) | ||
268 | } | ||
269 | |||
270 | ignoreSection = false | ||
271 | section = Section{ | ||
272 | Type: HEADING_1, | ||
273 | Text: heading1Match[1] | ||
274 | } | ||
275 | |||
276 | continue | ||
277 | } | ||
278 | |||
279 | listItemMatch := ListItemPattern.FindStringSubmatch(line) | ||
280 | if len(listItemMatch) != 0 { | ||
281 | if !ignoreSection { | ||
282 | if section.Type != LIST { | ||
283 | sections = append(sections, section) | ||
284 | section = Section{ | ||
285 | Type: LIST | ||
286 | } | ||
287 | } | ||
288 | } else { | ||
289 | ignoreSection = false | ||
290 | section = Section{ | ||
291 | Type: LIST, | ||
292 | } | ||
293 | } | ||
294 | |||
295 | section.Items = append(section.Items, listItemMatch[1]) | ||
296 | |||
297 | continue | ||
298 | } | ||
299 | } | ||
300 | |||
301 | if !ignoreSection { | ||
302 | sections = append(sections, section) | ||
303 | } | ||
304 | |||
305 | return | ||
306 | } | ||