slidewalk/internal/export/export.go
2026-07-15 20:14:07 +02:00

131 lines
3.5 KiB
Go

package export
import (
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"slidewalk/internal/parser"
"slidewalk/internal/render"
"slidewalk/web"
)
// exportedAssets are the static assets (relative to web.Assets' "assets"
// root) copied into every export. The dev-only live-reload client script is
// deliberately not part of this list.
var exportedAssets = []string{
"vendor/style.css",
"vendor/chroma.css",
"vendor/nav.js",
"vendor/mermaid.min.js",
}
// Export renders the slide deck in dir and writes it, along with its
// required assets, into outDir. outName is the filename of the exported
// HTML document, e.g. "index.html".
func Export(dir, outDir, outName string) error {
slides, err := parser.ParseDir(dir)
if err != nil {
return fmt.Errorf("parsing slides in %s: %w", dir, err)
}
if err := os.MkdirAll(outDir, 0o755); err != nil {
return fmt.Errorf("creating output directory %s: %w", outDir, err)
}
if err := writePage(outDir, outName, slides); err != nil {
return err
}
for _, name := range exportedAssets {
dest := filepath.Join(outDir, filepath.FromSlash(name))
if err := copyAsset(name, dest); err != nil {
return err
}
}
if err := copyUserAssets(dir, outDir); err != nil {
return fmt.Errorf("copying assets from %s: %w", dir, err)
}
return nil
}
// copyUserAssets copies every file in dir other than the slide ".md" files
// themselves (e.g. images referenced via Markdown like "![](img/foto.png)")
// into outDir, preserving relative paths and subdirectory structure.
// Dotfiles and dot-directories (".git", ".DS_Store", ...) are skipped.
func copyUserAssets(dir, outDir string) error {
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasPrefix(d.Name(), ".") {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
if d.IsDir() {
return nil
}
if strings.EqualFold(filepath.Ext(d.Name()), ".md") {
return nil
}
rel, err := filepath.Rel(dir, path)
if err != nil {
return fmt.Errorf("computing relative path for %s: %w", path, err)
}
return copyFile(path, filepath.Join(outDir, rel))
})
}
// copyFile copies the file at src to dest, creating any missing parent
// directories.
func copyFile(src, dest string) error {
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("reading %s: %w", src, err)
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("creating directory for %s: %w", dest, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("writing %s: %w", dest, err)
}
return nil
}
func writePage(outDir, outName string, slides []parser.Slide) error {
htmlPath := filepath.Join(outDir, outName)
f, err := os.Create(htmlPath)
if err != nil {
return fmt.Errorf("creating %s: %w", htmlPath, err)
}
defer f.Close()
page := render.Page{Title: filepath.Base(filepath.Clean(outDir)), Slides: slides}
if err := render.Render(f, page); err != nil {
return fmt.Errorf("rendering page to %s: %w", htmlPath, err)
}
return nil
}
// copyAsset copies the embedded asset at "assets/<name>" to dest.
func copyAsset(name, dest string) error {
data, err := fs.ReadFile(web.Assets, path.Join("assets", name))
if err != nil {
return fmt.Errorf("reading embedded asset %s: %w", name, err)
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("creating directory for %s: %w", dest, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("writing %s: %w", dest, err)
}
return nil
}