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 } var ( iconFolder = readIcon("folder") iconDoc = readIcon("doc") iconImage = readIcon("image") iconVideo = readIcon("video") iconAudio = readIcon("audio") iconArchive = readIcon("archive") iconGeneric = readIcon("generic") ) // 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...) } func readIcon(name string) template.HTML { b, _ := assets.ReadFile("assets/icons/" + name + ".svg") return template.HTML(strings.TrimSpace(string(b))) } 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] }