Refactor projcet structure
This commit is contained in:
197
render.go
Normal file
197
render.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
var md = goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM, extension.Table),
|
||||
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||
)
|
||||
|
||||
type crumb struct{ Name, URL string }
|
||||
type entry struct {
|
||||
Icon template.HTML
|
||||
Name, URL, Meta string
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Title string
|
||||
Crumbs []crumb
|
||||
CanEdit bool
|
||||
EditMode bool
|
||||
PostURL string
|
||||
RawContent string
|
||||
Content template.HTML
|
||||
Entries []entry
|
||||
SpecialContent template.HTML
|
||||
}
|
||||
|
||||
// pageSettings holds the parsed contents of a .page-settings file.
|
||||
type pageSettings struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
// renderMarkdown converts raw markdown to trusted HTML.
|
||||
func renderMarkdown(raw []byte) template.HTML {
|
||||
var buf bytes.Buffer
|
||||
if err := md.Convert(raw, &buf); err != nil {
|
||||
return ""
|
||||
}
|
||||
return template.HTML(buf.String())
|
||||
}
|
||||
|
||||
// extractFirstHeading returns the text of the first ATX heading in raw markdown,
|
||||
// or an empty string if none is found.
|
||||
func extractFirstHeading(raw []byte) string {
|
||||
for _, line := range strings.SplitN(string(raw), "\n", 50) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(strings.TrimLeft(trimmed, "#"))
|
||||
if text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// stripFirstHeading removes the first ATX heading line from raw markdown.
|
||||
func stripFirstHeading(raw []byte) []byte {
|
||||
lines := strings.Split(string(raw), "\n")
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
result := append(lines[:i:i], lines[i+1:]...)
|
||||
return []byte(strings.TrimLeft(strings.Join(result, "\n"), "\n"))
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// parentURL returns the parent URL of a slash-terminated URL path.
|
||||
func parentURL(urlPath string) string {
|
||||
parent := path.Dir(strings.TrimSuffix(urlPath, "/"))
|
||||
if parent == "." || parent == "/" {
|
||||
return "/"
|
||||
}
|
||||
return parent + "/"
|
||||
}
|
||||
|
||||
func listEntries(fsPath, urlPath string) []entry {
|
||||
entries, err := os.ReadDir(fsPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var folders, files []entry
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entryURL := path.Join(urlPath, name)
|
||||
if e.IsDir() {
|
||||
folders = append(folders, entry{
|
||||
Icon: iconFolder,
|
||||
Name: name,
|
||||
URL: entryURL + "/",
|
||||
Meta: info.ModTime().Format("2006-01-02"),
|
||||
})
|
||||
} else {
|
||||
if name == "index.md" {
|
||||
continue // rendered above, don't list it
|
||||
}
|
||||
files = append(files, entry{
|
||||
Icon: fileIcon(name),
|
||||
Name: name,
|
||||
URL: entryURL,
|
||||
Meta: formatSize(info.Size()) + " · " + info.ModTime().Format("2006-01-02"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(folders, func(i, j int) bool { return folders[i].Name < folders[j].Name })
|
||||
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
|
||||
|
||||
return append(folders, files...)
|
||||
}
|
||||
|
||||
// Pixel-art SVG icons — outlined, crispEdges, uses currentColor.
|
||||
const (
|
||||
iconFolder template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><path d="M1 6h14v8H1zm0 0V4h5l1 2"/></svg>`
|
||||
iconDoc template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><path d="M3 1h7l4 4v10H3zM10 1v4h4M5 8h6M5 11h4"/></svg>`
|
||||
iconImage template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><rect x="1" y="2" width="14" height="12"/><path d="M1 11l4-4 3 3 2-2 5 5"/><rect x="10" y="4" width="2" height="2" fill="currentColor" stroke="none"/></svg>`
|
||||
iconVideo template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><rect x="1" y="3" width="14" height="10"/><path d="M6 6v4l5-2z" fill="currentColor" stroke="none"/></svg>`
|
||||
iconAudio template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" stroke="none" shape-rendering="crispEdges"><path d="M2 6h3l4-3v10l-4-3H2z"/><rect x="11" y="5" width="2" height="1"/><rect x="11" y="7" width="3" height="1"/><rect x="11" y="9" width="2" height="1"/></svg>`
|
||||
iconArchive template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><rect x="1" y="1" width="14" height="4"/><path d="M1 5v10h14V5M7 3h2"/><rect x="7" y="6" width="2" height="2" fill="currentColor" stroke="none"/><rect x="7" y="9" width="2" height="1" fill="currentColor" stroke="none"/></svg>`
|
||||
iconGeneric template.HTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" shape-rendering="crispEdges"><path d="M3 1h7l4 4v10H3zM10 1v4h4"/></svg>`
|
||||
)
|
||||
|
||||
func fileIcon(name string) template.HTML {
|
||||
ext := strings.ToLower(path.Ext(name))
|
||||
switch ext {
|
||||
case ".md", ".pdf":
|
||||
return iconDoc
|
||||
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg":
|
||||
return iconImage
|
||||
case ".mp4", ".mkv", ".avi", ".mov":
|
||||
return iconVideo
|
||||
case ".mp3", ".flac", ".ogg", ".wav":
|
||||
return iconAudio
|
||||
case ".zip", ".tar", ".gz", ".7z":
|
||||
return iconArchive
|
||||
default:
|
||||
return iconGeneric
|
||||
}
|
||||
}
|
||||
|
||||
func formatSize(b int64) string {
|
||||
switch {
|
||||
case b < 1024:
|
||||
return fmt.Sprintf("%d B", b)
|
||||
case b < 1024*1024:
|
||||
return fmt.Sprintf("%.1f KB", float64(b)/1024)
|
||||
default:
|
||||
return fmt.Sprintf("%.1f MB", float64(b)/1024/1024)
|
||||
}
|
||||
}
|
||||
|
||||
func buildCrumbs(urlPath string) []crumb {
|
||||
if urlPath == "/" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(strings.Trim(urlPath, "/"), "/")
|
||||
crumbs := make([]crumb, len(parts))
|
||||
for i, p := range parts {
|
||||
crumbs[i] = crumb{
|
||||
Name: p,
|
||||
URL: "/" + strings.Join(parts[:i+1], "/") + "/",
|
||||
}
|
||||
}
|
||||
return crumbs
|
||||
}
|
||||
|
||||
func pageTitle(urlPath string) string {
|
||||
if urlPath == "/" {
|
||||
return "Datascape"
|
||||
}
|
||||
parts := strings.Split(strings.Trim(urlPath, "/"), "/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
Reference in New Issue
Block a user