summaryrefslogtreecommitdiff
path: root/website.go
diff options
context:
space:
mode:
Diffstat (limited to 'website.go')
-rw-r--r--website.go158
1 files changed, 153 insertions, 5 deletions
diff --git a/website.go b/website.go
index e543bbe..e64e7f8 100644
--- a/website.go
+++ b/website.go
@@ -1,11 +1,14 @@
package main
import (
+ "bytes"
"cmp"
"embed"
"encoding/json"
"errors"
"fmt"
+ "html"
+ "html/template"
"io/fs"
"log"
"net/http"
@@ -17,6 +20,15 @@ import (
"time"
"github.com/a-h/templ"
+ chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
+ "github.com/yuin/goldmark"
+ highlighting "github.com/yuin/goldmark-highlighting/v2"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+ gmhtml "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
"gopkg.in/yaml.v3"
)
@@ -159,6 +171,56 @@ type Blog struct {
Desc string
Date time.Time
Body string // raw markdown for rendering
+ HTML string // rendered body
+}
+
+var md = goldmark.New(
+ goldmark.WithExtensions(
+ extension.GFM,
+ extension.Footnote,
+ highlighting.NewHighlighting(
+ highlighting.WithFormatOptions(chromahtml.WithClasses(true)),
+ highlighting.WithWrapperRenderer(func(w util.BufWriter, ctx highlighting.CodeBlockContext, entering bool) {
+ if entering {
+ lang, _ := ctx.Language()
+ fmt.Fprintf(w, `<div class="codeblock" data-lang="%s">`, template.HTMLEscapeString(string(lang)))
+ } else {
+ w.WriteString(`</div>`)
+ }
+ }),
+ ),
+ ),
+ 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 {
@@ -167,6 +229,20 @@ type frontmatter struct {
Date time.Time `yaml:"date"`
}
+func findBlog(blogs []Blog, slug string) (blog, prev, next *Blog) {
+ i := slices.IndexFunc(blogs, func(b Blog) bool { return b.Slug == slug })
+ if i < 0 {
+ return nil, nil, nil
+ }
+ if i+1 < len(blogs) {
+ prev = &blogs[i+1] // older
+ }
+ if i > 0 {
+ next = &blogs[i-1] // newer
+ }
+ return &blogs[i], prev, next
+}
+
var datePrefix = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})-(.+)\.md$`)
func parseBlog(name, raw string) (Blog, error) {
@@ -182,6 +258,12 @@ func parseBlog(name, raw string) (Blog, error) {
}
}
+ var buf bytes.Buffer
+ if err := md.Convert([]byte(body), &buf); err != nil {
+ return Blog{}, err
+ }
+ HTML := ligatures(buf.String())
+
slug := strings.TrimSuffix(name, ".md")
date := frontmatter.Date // frontmatter wins
if match := datePrefix.FindStringSubmatch(name); match != nil {
@@ -204,6 +286,7 @@ func parseBlog(name, raw string) (Blog, error) {
Date: date,
Body: body,
Desc: frontmatter.Desc,
+ HTML: HTML,
}, nil
}
@@ -264,12 +347,66 @@ func latestBlog(blogs []Blog) *Blog {
return &blogs[0]
}
+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"))
+}
+
+func (externalLinks) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
+ src := reader.Source()
+ ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ switch link := n.(type) {
+ case *ast.Link:
+ if bytes.HasPrefix(link.Destination, []byte("http")) {
+ openInNewTab(link)
+ }
+ case *ast.AutoLink:
+ if link.AutoLinkType == ast.AutoLinkURL && bytes.HasPrefix(link.URL(src), []byte("http")) {
+ openInNewTab(link)
+ }
+ }
+ return ast.WalkContinue, nil
+ })
+}
+
var projects = []Project{
{
Slug: "krypton",
Title: "Krypton",
Category: "Programming Language Compiler",
- Desc: "A modern Minecraft creator event. As a developer, I helped with minor improvements to the cosmetics system. ",
+ Desc: "",
// Icon: IconCpu("text-[#F5A670] self-center"),
Icon: IconKrypton(),
Tags: []ProjectTag{
@@ -294,6 +431,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"},
@@ -514,13 +652,14 @@ func main() {
}
jetbrains := FontPack{
- Monospace: "JetBrains Mono",
- Normal: `system-ui, sans-serif`,
+ Monospace: `JetBrains Mono, monospace`,
+ Normal: `Inter, system-ui, sans-serif`,
}
minecraft := FontPack{
- Monospace: `"Monocraft", "JetBrains Mono", monospace`,
- Normal: `"Monocraft", system-ui, sans-serif`,
+ Monospace: `Monocraft, JetBrains Mono, monospace`,
+ // Normal: `Inter, Lower Pixel Regular, Monocraft, system-ui, sans-serif`,
+ Normal: `LowerPixelRegular, Inter, system-ui, sans-serif`,
}
SetTheme("carbonight-cat", carbonightCat)
@@ -564,6 +703,15 @@ func main() {
BlogsPage(currentStyle(r), getBlogs()).Render(r.Context(), w)
})
+ mux.HandleFunc("GET /blogs/{slug}", func(w http.ResponseWriter, r *http.Request) {
+ blog, prev, next := findBlog(getBlogs(), r.PathValue("slug"))
+ if blog == nil {
+ notFoundHandler(w, r)
+ return
+ }
+ BlogPage(currentStyle(r), *blog, prev, next).Render(r.Context(), w)
+ })
+
mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) {
theme, ok := GetTheme(n)
return theme.Vars(), ok