summaryrefslogtreecommitdiff
path: root/website.go
diff options
context:
space:
mode:
Diffstat (limited to 'website.go')
-rw-r--r--website.go196
1 files changed, 185 insertions, 11 deletions
diff --git a/website.go b/website.go
index b54030c..b6c9af3 100644
--- a/website.go
+++ b/website.go
@@ -7,13 +7,16 @@ import (
"encoding/json"
"errors"
"fmt"
+ "html"
"html/template"
"io/fs"
"log"
+ "maps"
"net/http"
"os"
"regexp"
"slices"
+ "sort"
"strings"
"sync"
"time"
@@ -25,7 +28,7 @@ import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer/html"
+ gmhtml "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"gopkg.in/yaml.v3"
@@ -189,12 +192,39 @@ var md = goldmark.New(
}),
),
),
- goldmark.WithRendererOptions(html.WithUnsafe()),
+ goldmark.WithRendererOptions(gmhtml.WithUnsafe()),
goldmark.WithParserOptions(
parser.WithASTTransformers(util.Prioritized(externalLinks{}, 100)),
),
)
+var tagRe = regexp.MustCompile(`<[^>]*>`)
+
+func ligatures(html string) string {
+ var out strings.Builder
+ depth, last := 0, 0
+ apply := func(s string) string {
+ if depth == 0 {
+ return arrows.Replace(s)
+ }
+ return s
+ }
+ for _, loc := range tagRe.FindAllStringIndex(html, -1) {
+ out.WriteString(apply(html[last:loc[0]]))
+ tag := html[loc[0]:loc[1]]
+ switch {
+ case strings.HasPrefix(tag, "<pre"), strings.HasPrefix(tag, "<code"):
+ depth++
+ case strings.HasPrefix(tag, "</pre"), strings.HasPrefix(tag, "</code"):
+ depth--
+ }
+ out.WriteString(tag)
+ last = loc[1]
+ }
+ out.WriteString(apply(html[last:]))
+ return out.String()
+}
+
type frontmatter struct {
Title string `yaml:"title"`
Desc string `yaml:"description"`
@@ -234,7 +264,7 @@ func parseBlog(name, raw string) (Blog, error) {
if err := md.Convert([]byte(body), &buf); err != nil {
return Blog{}, err
}
- HTML := buf.String()
+ HTML := ligatures(buf.String())
slug := strings.TrimSuffix(name, ".md")
date := frontmatter.Date // frontmatter wins
@@ -321,6 +351,33 @@ func latestBlog(blogs []Blog) *Blog {
type externalLinks struct{}
+func newReplacer(pairs map[string]string) *strings.Replacer {
+ keys := make([]string, 0, len(pairs))
+ for k := range pairs {
+ keys = append(keys, k)
+ }
+ slices.SortFunc(keys, func(a, b string) int { return cmp.Compare(len(b), len(a)) })
+
+ args := make([]string, 0, len(keys)*2)
+ for _, k := range keys {
+ args = append(args, html.EscapeString(k), pairs[k]) // rendered text has < and > escaped
+ }
+ return strings.NewReplacer(args...)
+}
+
+var arrows = newReplacer(map[string]string{
+ // arrows
+ "->": "→", "<-": "←", "<->": "↔",
+ "-->": "⟶", "<--": "⟵", "<-->": "⟷",
+ "->>": "↠", "<<-": "↞", "|->": "↦",
+ "~>": "↝", "<~": "↜",
+ // double arrows
+ "=>": "⇒", "<=>": "⇔",
+ "==>": "⟹", "<==": "⟸", "<==>": "⟺",
+ // comparisons
+ ">=": "≥", "<=": "≤", "!=": "≠", "+-": "±",
+})
+
func openInNewTab(n ast.Node) {
n.SetAttributeString("target", []byte("_blank"))
n.SetAttributeString("rel", []byte("noopener noreferrer"))
@@ -346,12 +403,101 @@ func (externalLinks) Transform(doc *ast.Document, reader text.Reader, pc parser.
})
}
+type ShellResult struct {
+ Output templ.Component // nil renders nothing
+ Clear bool
+ Vars map[string]string
+ Cookies []*http.Cookie
+}
+
+type shellCommand func(r *http.Request, args []string) ShellResult
+
+var shellCommands map[string]shellCommand
+
+func init() {
+ shellCommands = map[string]shellCommand{
+ "fastfetch": func(*http.Request, []string) ShellResult { return ShellResult{Output: Fastfetch(fetchRows)} },
+ "clear": func(*http.Request, []string) ShellResult { return ShellResult{Clear: true} },
+ "help": func(*http.Request, []string) ShellResult { return ShellResult{Output: ShellHelp(shellNames())} },
+ "export": func(r *http.Request, args []string) ShellResult {
+ if len(args) == 0 {
+ return ShellResult{Output: ShellText("usage: export KEY=value")}
+ }
+ res := ShellResult{Vars: map[string]string{}}
+ var lines []string
+ for _, arg := range args {
+ key, value, ok := strings.Cut(arg, "=")
+ if !ok {
+ lines = append(lines, "export: expected KEY=value, got "+arg)
+ continue
+ }
+ value = strings.Trim(value, `"'`)
+ vars, cookie, err := setEnvVar(key, value)
+ if err != nil {
+ lines = append(lines, "export: "+err.Error())
+ continue
+ }
+ maps.Copy(res.Vars, vars)
+ res.Cookies = append(res.Cookies, cookie)
+ lines = append(lines, key+"="+value)
+ }
+ res.Output = ShellLines(lines)
+ return res
+ },
+ }
+}
+
+func setEnvVar(key, value string) (map[string]string, *http.Cookie, error) {
+ switch key {
+ case "THEME":
+ theme, ok := GetTheme(value)
+ if !ok {
+ return nil, nil, fmt.Errorf("unknown theme: %s", value)
+ }
+ return theme.Vars(), envCookie("theme", value), nil
+ case "FONT_PACK":
+ pack, ok := GetFontPack(value)
+ if !ok {
+ return nil, nil, fmt.Errorf("unknown font pack: %s", value)
+ }
+ return pack.Vars(), envCookie("font_pack", value), nil
+ default:
+ return nil, nil, fmt.Errorf("unknown variable: %s", key)
+ }
+}
+
+func envCookie(name, value string) *http.Cookie {
+ return &http.Cookie{Name: name, Value: value, Path: "/", MaxAge: 31536000, SameSite: http.SameSiteLaxMode}
+}
+
+func shellNames() []string {
+ names := make([]string, 0, len(shellCommands))
+ for name := range shellCommands {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
+
+func runShell(r *http.Request, line string) ShellResult {
+ fields := strings.Fields(line)
+ if len(fields) == 0 {
+ return ShellResult{}
+ }
+ run, ok := shellCommands[fields[0]]
+ if !ok {
+ return ShellResult{Output: ShellText(fields[0] + ": command not found")}
+ }
+
+ return run(r, fields[1:])
+}
+
var projects = []Project{
{
Slug: "krypton",
Title: "Krypton",
Category: "Programming Language Compiler",
- Desc: "",
+ Desc: "Krypton is a simple, high-performance, general-purpose language with C-like control designed for good programmers.",
// Icon: IconCpu("text-[#F5A670] self-center"),
Icon: IconKrypton(),
Tags: []ProjectTag{
@@ -376,6 +522,7 @@ var projects = []Project{
Icon: IconCpu("text-[#F16265]"),
Tags: []ProjectTag{
{"Zig", Color("text-[#D8C44B]")},
+ {"Raylib", Color("text-[#FFFFFF]")},
},
Links: []ProjectLink{
{IconGit("text-[#F5A670]"), "Git", "https://git.kyren.codes/kyren/cpu-vs-ai.git"},
@@ -500,7 +647,7 @@ func GetMyUptime() string {
defer myUptime.mu.RUnlock()
// If no ping received in 3 minutes, treat as offline
- if time.Since(myUptime.LastUpdate) > 1*time.Minute {
+ if time.Since(myUptime.LastUpdate) > 3*time.Minute {
return "Asleep"
}
@@ -519,7 +666,10 @@ func handleUptimePing(w http.ResponseWriter, r *http.Request) {
})
} else if r.Method == http.MethodPost {
if devMode == false {
- secretToken := os.Getenv("MY_UPTIME_TOKEN")
+ secretFile := os.Getenv("MY_UPTIME_TOKEN_FILE")
+ bytes, err := os.ReadFile(secretFile)
+ Assert(err == nil)
+ secretToken := string(bytes)
if r.Header.Get("Authorization") != "Bearer "+secretToken {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
@@ -602,7 +752,8 @@ func main() {
minecraft := FontPack{
Monospace: `Monocraft, JetBrains Mono, monospace`,
- Normal: `Monocraft, Inter, system-ui, sans-serif`,
+ // Normal: `Inter, Lower Pixel Regular, Monocraft, system-ui, sans-serif`,
+ Normal: `LowerPixelRegular, Inter, system-ui, sans-serif`,
}
SetTheme("carbonight-cat", carbonightCat)
@@ -634,10 +785,6 @@ func main() {
HomePage(currentStyle(r), latestBlog(getBlogs())).Render(r.Context(), w)
})
- mux.HandleFunc("GET /info", func(w http.ResponseWriter, r *http.Request) {
- InfoPage(currentStyle(r)).Render(r.Context(), w)
- })
-
mux.HandleFunc("GET /projects", func(w http.ResponseWriter, r *http.Request) {
ProjectsPage(currentStyle(r), projects, 2).Render(r.Context(), w)
})
@@ -655,6 +802,33 @@ func main() {
BlogPage(currentStyle(r), *blog, prev, next).Render(r.Context(), w)
})
+ mux.HandleFunc("GET /shell", func(w http.ResponseWriter, r *http.Request) {
+ ShellPage(currentStyle(r)).Render(r.Context(), w)
+ })
+
+ mux.HandleFunc("POST /shell/run", func(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, 1<<10)
+ line := strings.TrimSpace(r.FormValue("cmd"))
+ if len(line) > 200 {
+ line = line[:200]
+ }
+ res := runShell(r, line)
+ for _, c := range res.Cookies {
+ http.SetCookie(w, c)
+ }
+
+ var buf bytes.Buffer
+ if res.Output != nil {
+ if err := ShellEntry(line, res.Output).Render(r.Context(), &buf); err != nil {
+ http.Error(w, "render failed", http.StatusInternalServerError)
+ return
+ }
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-store")
+ json.NewEncoder(w).Encode(map[string]any{"clear": res.Clear, "html": buf.String(), "vars": res.Vars})
+ })
+
mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) {
theme, ok := GetTheme(n)
return theme.Vars(), ok