aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile8
-rw-r--r--assets/startpage.txt6
-rw-r--r--internal/port/gemini.go176
-rw-r--r--internal/port/gopher.go138
-rw-r--r--internal/port/main.go69
-rw-r--r--internal/port/tpl/_fonts.html16
-rw-r--r--internal/port/tpl/_header.html48
-rw-r--r--internal/port/tpl/_modals.html24
-rw-r--r--internal/port/tpl/gemini.html38
-rw-r--r--internal/port/tpl/gopher.html59
-rw-r--r--internal/port/tpl/startpage.html120
-rw-r--r--pkg/libgemini/libgemini.go85
-rw-r--r--pkg/libgopher/libgopher.go2
13 files changed, 458 insertions, 331 deletions
diff --git a/Makefile b/Makefile
index 8513d69..58ac412 100644
--- a/Makefile
+++ b/Makefile
@@ -8,10 +8,10 @@ dev: build
8build: clean 8build: 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
17profile: 17profile:
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
6GETTING STARTED -- 6GETTING 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 @@
1package port 1package port
2 2
3import ( 3import (
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
24type SectionType byte 22type GeminiTemplateVariables struct {
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 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
45var ( 30type GeminiNavItem struct {
46 TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") 31 Label string
47) 32 URL string
33}
48 34
49func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { 35type GeminiSection struct {
36 Type libgemini.GeminiDocSectionType
37 Text string
38 URL template.URL
39 Items []string
40}
41
42func 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
78func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []Section) { 71func 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
20type Item struct { 20type gopherTemplateVariables struct {
21 Title string
22 URL string
23 Assets AssetList
24 Lines []GopherItem
25 Nav []GopherNavItem
26 IsPlain bool
27}
28
29type GopherNavItem struct {
30 Label string
31 URL string
32 Current bool
33}
34
35type 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
41func 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
52func 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
26func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { 83func 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
29type TemplateVariables struct { 29type 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
39func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { 36func 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.
120func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { 116func 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 {
74type GeminiDocSectionType byte 73type GeminiDocSectionType byte
75 74
76const ( 75const (
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
86type GeminiDocSection struct { 85type 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
174func ParseGeminiDocument(body *bytes.Buffer) (sections []Section) { 173func 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
188func Get(uri string) (*Response, error) { 188func 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 {