320 lines
7.5 KiB
Go
320 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"tildegit.org/tjp/sliderule"
|
|
"tildegit.org/tjp/sliderule/gemini"
|
|
"tildegit.org/tjp/sliderule/gemini/gemtext"
|
|
"tildegit.org/tjp/sliderule/gopher"
|
|
"tildegit.org/tjp/sliderule/gopher/gophermap"
|
|
"tildegit.org/tjp/sliderule/spartan"
|
|
)
|
|
|
|
func docType(u *url.URL, response *sliderule.Response) string {
|
|
_, gopherType := gopherURL(u)
|
|
switch gopherType {
|
|
case gopher.MenuType, gopher.SearchType:
|
|
return "text/x-gophermap"
|
|
case gopher.TextFileType, gopher.ErrorType, gopher.InfoMessageType:
|
|
return "text/plain"
|
|
case gopher.MacBinHexType,
|
|
gopher.DosBinType,
|
|
gopher.BinaryFileType,
|
|
gopher.ImageFileType,
|
|
gopher.MovieFileType,
|
|
gopher.SoundFileType,
|
|
gopher.DocumentType:
|
|
return "application/octet-stream"
|
|
case gopher.UuencodedType:
|
|
return "text/x-uuencode"
|
|
case gopher.GifFileType:
|
|
return "image/gif"
|
|
case gopher.BitmapType:
|
|
return "image/bmp"
|
|
case gopher.PngImageFileType:
|
|
return "image/png"
|
|
case gopher.HTMLType:
|
|
return "text/html"
|
|
case gopher.RtfDocumentType:
|
|
return "application/rtf"
|
|
case gopher.WavSoundFileType:
|
|
return "audio/wav"
|
|
case gopher.PdfDocumentType:
|
|
return "application/pdf"
|
|
case gopher.XmlDocumentType:
|
|
return "application/xml"
|
|
}
|
|
|
|
if metaIsMediaType(u, response) {
|
|
mtype, _, err := mime.ParseMediaType(response.Meta.(string))
|
|
if err == nil {
|
|
return mtype
|
|
}
|
|
}
|
|
|
|
if u.Scheme == "http" || u.Scheme == "https" {
|
|
resp := response.Meta.(*http.Response)
|
|
mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
|
if err == nil {
|
|
return mtype
|
|
}
|
|
}
|
|
|
|
return "text/plain"
|
|
}
|
|
|
|
func metaIsMediaType(u *url.URL, response *sliderule.Response) bool {
|
|
if u.Scheme == "gemini" && response.Status == gemini.StatusSuccess {
|
|
return true
|
|
}
|
|
if u.Scheme == "spartan" && response.Status == spartan.StatusSuccess {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseDoc(doctype string, body []byte, conf *Config) (string, []Link, error) {
|
|
switch doctype {
|
|
case "text/x-gophermap":
|
|
return parseGophermapDoc(body, conf.SoftWrap)
|
|
case "text/gemini":
|
|
return parseGemtextDoc(body, conf.SoftWrap)
|
|
}
|
|
|
|
return string(body), nil, nil
|
|
}
|
|
|
|
func parseGophermapDoc(body []byte, softWrap int) (string, []Link, error) {
|
|
var b strings.Builder
|
|
var l []Link
|
|
mapdoc, err := gophermap.Parse(bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
links := 0
|
|
for _, item := range mapdoc {
|
|
if item.Type != gopher.InfoMessageType {
|
|
links += 1
|
|
}
|
|
}
|
|
width := numberWidth(links - 1)
|
|
infopad := string(bytes.Repeat([]byte{' '}, width+3))
|
|
|
|
i := 0
|
|
for _, item := range mapdoc {
|
|
switch item.Type {
|
|
case gopher.InfoMessageType:
|
|
for _, line := range fold(item.Display, softWrap) {
|
|
if _, err := b.WriteString(infopad + line + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
default:
|
|
l = append(l, Link{
|
|
Text: item.Display,
|
|
Target: fmtGopherURL(item.Type, item.Selector, item.Hostname, item.Port),
|
|
})
|
|
if _, err := b.WriteString(fmt.Sprintf("[%d]%s %s\n", i, padding(i, width), linkStyle.Render(item.Display))); err != nil {
|
|
return "", nil, err
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
return b.String(), l, nil
|
|
}
|
|
|
|
var (
|
|
linkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
|
|
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
|
quoteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Italic(true)
|
|
rawStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("249"))
|
|
h1Style = lipgloss.NewStyle().Foreground(lipgloss.Color("154")).Bold(true).Underline(true)
|
|
h2Style = lipgloss.NewStyle().Foreground(lipgloss.Color("50")).Underline(true)
|
|
h3Style = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Underline(true)
|
|
listStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3"))
|
|
)
|
|
|
|
func parseGemtextDoc(body []byte, softWrap int) (string, []Link, error) {
|
|
var b strings.Builder
|
|
var l []Link
|
|
gemdoc, err := gemtext.Parse(bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
links := 0
|
|
for _, item := range gemdoc {
|
|
if item.Type() == gemtext.LineTypeLink || item.Type() == gemtext.LineTypePrompt {
|
|
links += 1
|
|
}
|
|
}
|
|
width := numberWidth(links - 1)
|
|
textpad := string(bytes.Repeat([]byte{' '}, width+3))
|
|
|
|
i := 0
|
|
for _, item := range gemdoc {
|
|
isPrompt := false
|
|
hLevel := 0
|
|
switch item.Type() {
|
|
case gemtext.LineTypePrompt:
|
|
isPrompt = true
|
|
fallthrough
|
|
case gemtext.LineTypeLink:
|
|
ll := item.(gemtext.LinkLine)
|
|
u, err := url.Parse(ll.URL())
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
l = append(l, Link{
|
|
Text: ll.Label(),
|
|
Target: u,
|
|
Prompt: isPrompt,
|
|
})
|
|
label := ll.Label()
|
|
if len(label) == 0 {
|
|
label = ll.URL()
|
|
}
|
|
for j, line := range fold(label, softWrap) {
|
|
var prefix string
|
|
if j == 0 {
|
|
prefix = fmt.Sprintf("[%d]%s ", i, padding(i, width))
|
|
} else {
|
|
prefix = strings.Repeat(" ", width+3)
|
|
}
|
|
if _, err := fmt.Fprintf(&b, "%s%s\n", prefix, linkStyle.Render(line)); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
i += 1
|
|
case gemtext.LineTypeQuote:
|
|
q := item.(gemtext.QuoteLine)
|
|
for _, line := range fold(q.Body(), softWrap-1) {
|
|
line = strings.TrimSpace(line)
|
|
if _, err := b.WriteString(textpad + "> " + quoteStyle.Render(line) + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
case gemtext.LineTypePreformatToggle:
|
|
case gemtext.LineTypePreformattedText:
|
|
for _, line := range fold(item.String(), softWrap) {
|
|
if _, err := b.WriteString(textpad + rawStyle.Render(line) + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
case gemtext.LineTypeHeading3:
|
|
hLevel += 1
|
|
fallthrough
|
|
case gemtext.LineTypeHeading2:
|
|
hLevel += 1
|
|
fallthrough
|
|
case gemtext.LineTypeHeading1:
|
|
hLevel += 1
|
|
var style lipgloss.Style
|
|
switch hLevel {
|
|
case 1:
|
|
style = h1Style
|
|
case 2:
|
|
style = h2Style
|
|
case 3:
|
|
style = h3Style
|
|
}
|
|
|
|
for _, line := range fold(item.String(), softWrap) {
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if _, err := b.WriteString(textpad + style.Render(line) + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
case gemtext.LineTypeListItem:
|
|
li := item.(gemtext.ListItemLine)
|
|
for i, line := range fold(li.Body(), softWrap-2) {
|
|
lpad := " "
|
|
if i == 0 {
|
|
lpad = "* "
|
|
}
|
|
if _, err := b.WriteString(textpad + lpad + listStyle.Render(line) + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
default:
|
|
for _, line := range fold(item.String(), softWrap) {
|
|
if _, err := b.WriteString(textpad + line + "\n"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return b.String(), l, nil
|
|
}
|
|
|
|
func fold(line string, width int) []string {
|
|
rs := []rune(strings.TrimSuffix(line, "\n"))
|
|
if len(rs) == 0 {
|
|
return []string{""}
|
|
}
|
|
|
|
var b []string
|
|
outer:
|
|
for len(rs) > 0 {
|
|
if len(rs) <= width {
|
|
b = append(b, string(rs))
|
|
break
|
|
}
|
|
|
|
w := width
|
|
for rs[w] != ' ' && w > 0 {
|
|
w -= 1
|
|
}
|
|
if w == 0 {
|
|
for i := width + 1; i < len(rs); i += 1 {
|
|
if rs[i] == ' ' {
|
|
b = append(b, string(rs[:i]))
|
|
rs = rs[i+1:]
|
|
continue outer
|
|
}
|
|
}
|
|
b = append(b, string(rs))
|
|
break outer
|
|
}
|
|
|
|
b = append(b, string(rs[:w]))
|
|
rs = rs[w+1:]
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
func fmtGopherURL(itemtype sliderule.Status, selector, hostname, port string) *url.URL {
|
|
if port != "70" {
|
|
hostname += ":" + port
|
|
}
|
|
return &url.URL{
|
|
Scheme: "gopher",
|
|
Host: hostname,
|
|
Path: "/" + string(byte(itemtype)) + selector,
|
|
}
|
|
}
|
|
|
|
func numberWidth(i int) int {
|
|
n := 1
|
|
for i > 9 {
|
|
i /= 10
|
|
n += 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
func padding(num int, width int) string {
|
|
return strings.Repeat(" ", width-numberWidth(num))
|
|
}
|