personal-gemini-capsule/internal/codeview/codeview.go
Travis Shears 5c669f241f
remove the md to gemtext stuff
")

-----------------------------

")
%s
```", code)
")
=> %s %s

", remoteURL, path))

")
")
")

-----------------------------

=> / Back to home
")
2025-10-15 19:01:57 +02:00

193 lines
6.6 KiB
Go

package codeview
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/kulak/gemini"
)
/*
* Codeview is a feature that proxies my personal git server (forgejo)
* Allowing me to render code natively in gemini
*
* Features:
* - [x] Individual file view with line numbers?
* - [x] Dir view with links to files
* - [ ] Project root view with markdown to gemtxt conversion and file tree
*/
func HandleRequest(w gemini.ResponseWriter, req *gemini.Request) {
path := req.URL.Path
switch {
case strings.HasPrefix(path, "/codeview/dir"):
serveDir(w, req, path)
case strings.HasPrefix(path, "/codeview/raw/"):
serveFile(w, req, path)
default:
w.WriteStatusMsg(gemini.StatusNotFound, "Page not found")
}
}
func serveDir(w gemini.ResponseWriter, req *gemini.Request, path string) {
repo := req.URL.Query().Get("repo")
filepath := url.QueryEscape(req.URL.Query().Get("filepath"))
branch := req.URL.Query().Get("branch")
commit := req.URL.Query().Get("commit")
slog.Info("Serving directory", "repo", repo, "filepath", filepath, "branch", branch, "commit", commit)
var gitURL string
ref := ""
if branch != "" {
ref = branch
} else if commit != "" {
ref = commit
}
if filepath == "." {
gitURL = "https://git.travisshears.com/api/v1/repos/travisshears/" + repo + "/contents?ref=" + ref
} else {
gitURL = "https://git.travisshears.com/api/v1/repos/travisshears/" + repo + "/contents/" + filepath + "?ref=" + ref
}
slog.Info("Fetching directory contents", "url", gitURL)
httpReq, err := http.NewRequest("GET", gitURL, nil)
if err != nil {
slog.Error("Failed to create request to git server", "error", err)
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server")
return
}
res, err := http.DefaultClient.Do(httpReq)
if err != nil {
slog.Error("Failed to send request to git server", "error", err)
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server")
return
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
slog.Error("Failed to read res body", "error", err)
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server")
return
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
slog.Error("unexpected status code", "statusCode", res.StatusCode, "body", string(body))
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server")
return
}
var dirRes []struct {
Name string `json:"name"`
Path string `json:"path"`
URL string `json:"html_url"`
Type string `json:"type"`
RawURL string `json:"download_url"`
}
if err := json.Unmarshal(body, &dirRes); err != nil {
slog.Error("failed to parse response", "error", err)
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem parsing response from git server")
return
}
slog.Info("Successfully parsed response from git server", "res", dirRes)
var content strings.Builder
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
content.WriteString("# Directory CodeView\n")
httpURL := fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/branch/main/%s", filepath)
if branch != "" {
httpURL = fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/branch/%s/%s", branch, filepath)
} else if commit != "" {
httpURL = fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/commit/%s/%s", commit, filepath)
}
content.WriteString(fmt.Sprintf("This is a code view which proxies my personal git server. On the clearnet the following dir is available:\n=> %s here\n\n", httpURL))
// print readme page
// var readmeURL string
for _, file := range dirRes {
if file.Type == "file" && file.Name == "README.md" {
slog.Info("Found README.md", "file", file)
body, err := getRemoteFile(file.RawURL)
if err != nil {
slog.Error("Failed to fetch README.md", "url", file.RawURL, "error", err)
continue
}
content.WriteString("README.md found, directory listing below.\n")
content.WriteString(renderCodeFile(body))
content.WriteString("\n\n-----------------------------\n\n")
}
}
for _, file := range dirRes {
switch file.Type {
case "dir":
url := fmt.Sprintf("/codeview/dir?repo=%s&filepath=%s&commit=%s&branch=%s", repo, file.Path, commit, branch)
content.WriteString(fmt.Sprintf("=> %s %s/\n", url, file.Path))
case "file":
p := strings.TrimPrefix(file.URL, "https://git.travisshears.com/travisshears/")
url := fmt.Sprintf("/codeview/raw/%s", p)
content.WriteString(fmt.Sprintf("=> %s %s\n", url, file.Path))
}
}
content.WriteString("\n\n-----------------------------\n\n=> / Back to home\n")
w.WriteBody([]byte(content.String()))
}
func renderCodeFile(code string) string {
// Escape any triple backticks in the code to avoid prematurely ending Gemini preformatted blocks
code = strings.ReplaceAll(code, "```", "\\`\\`\\`")
return fmt.Sprintf("```code\n%s\n```", code)
}
func getRemoteFile(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
slog.Error("Failed to fetch remote file", "url", url, "error", err)
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
slog.Error("Failed to fetch remote file", "url", url, "status", resp.Status)
return "", fmt.Errorf("failed to fetch remote file")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
slog.Error("Failed to read remote file", "url", url, "error", err)
return "", err
}
return string(body), nil
}
func serveFile(w gemini.ResponseWriter, req *gemini.Request, path string) {
path = strings.TrimPrefix(path, "/codeview/raw/")
rawURL := strings.Replace(path, "src", "raw", 1)
rawURL = "https://git.travisshears.com/travisshears/" + rawURL
remoteURL := "https://git.travisshears.com/travisshears/" + path
body, err := getRemoteFile(rawURL)
if err != nil {
slog.Error("Failed to create request to git server", "error", err)
w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server")
return
}
var content strings.Builder
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
// filename := strings.TrimPrefix(remoteURL, "https://git.travisshears.com/travisshears/")
// content.WriteString(fmt.Sprintf("# File: %s\n", filename))
content.WriteString("# File CodeView\n")
content.WriteString(fmt.Sprintf("This is a code view which proxies my personal git server. On the clearnet the following file is available at:\n=> %s %s\n\n", remoteURL, path))
content.WriteString("----------------------------\n\n")
content.WriteString(renderCodeFile(body))
content.WriteString("\n\n-----------------------------\n\n=> / Back to home\n")
w.WriteBody([]byte(content.String()))
}