x-1/handlers.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))
}