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/" 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 }