aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/port/gemini.go176
-rw-r--r--internal/port/gopher.go2
-rw-r--r--internal/port/main.go2
-rw-r--r--pkg/libgemini/libgemini.go165
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 @@
1package port 1package port
2 2
3import ( 3import (
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
24type SectionType byte
25
26const (
27 RAW_TEXT = SectionType(0)
28 REFLOW_TEXT = SectionType(1)
29 LINK = SectionType(2)
30)
31
32type Section struct {
33 Type SectionType
34 Text string
35 URL template.URL
36}
37
38type templateVariables struct {
39 Title string
40 URI string
41 Assets AssetList
42 Sections []Section
43}
44
23var ( 45var (
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
78func 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
56func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { 152func 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
3import ( 3import (
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
51var ( 53var (
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
56type Header struct { 64type Header struct {
@@ -63,6 +71,25 @@ type Response struct {
63 Body io.Reader 71 Body io.Reader
64} 72}
65 73
74type GeminiDocSectionType byte
75
76const (
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
86type GeminiDocSection struct {
87 Type SectionType
88 Text string
89 URL template.URL
90 Items []string
91}
92
66func Get(uri string) (*Response, error) { 93func 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
174func 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}