diff --git a/pkg/cataloghtml/whee.go b/pkg/cataloghtml/whee.go
new file mode 100644
index 00000000..c299596a
--- /dev/null
+++ b/pkg/cataloghtml/whee.go
@@ -0,0 +1,180 @@
+package cataloghtml
+
+import (
+ "context"
+ "html/template"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+
+ "github.com/warpfork/warpforge/pkg/workspace"
+ "github.com/warpfork/warpforge/wfapi"
+)
+
+type SiteConfig struct {
+ Ctx context.Context
+
+ // Data Access Broker for getting Catalog info.
+ // Some functions pass around data in memory,
+ // but sometimes those objects just contain CIDs, which we'll need to go load.
+ // This has helper functions that do the loading.
+ // Arguably should be a parameter, but would end up in almost every single function, so, eh.
+ Cat_dab workspace.Catalog
+
+ // A plain string for output path prefix is used because golang still lacks
+ // an interface for filesystem *writing* -- io/fs is only reading. Sigh.
+ OutputPath string
+
+ // Set to "/" if you'll be publishing at the root of a subdomain.
+ URLPrefix string
+}
+
+func (cfg SiteConfig) tfuncs() map[string]interface{} {
+ return map[string]interface{}{
+ "string": func(x interface{}) string { // golang would you please shut the fuck up and let me be productive, honestly
+ // this is for things that are literally typedefs of string but the template package isn't smart enough to be calm about unboxing it.
+ return reflect.ValueOf(x).String()
+ },
+ "url": func(parts ...string) string {
+ return path.Join(append([]string{cfg.URLPrefix}, parts...)...)
+ },
+ }
+}
+
+func (cfg SiteConfig) CatalogAndChildrenToHtml() error {
+ if err := cfg.CatalogToHtml(); err != nil {
+ return err
+ }
+ modNames := cfg.Cat_dab.Modules()
+ for _, modName := range modNames {
+ catMod, err := cfg.Cat_dab.GetModule(wfapi.CatalogRef{modName, "", ""})
+ if err != nil {
+ return err
+ }
+ if err := cfg.CatalogModuleAndChildrenToHtml(*catMod); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// CatalogToHtml generates a root page that links to all the modules.
+//
+// This function has no parameters because it uses the DAB in the SiteConfig entirely.
+func (cfg SiteConfig) CatalogToHtml() error {
+ // Future: It's perhaps a bit odd that this uses the workspace.Catalog object instead of the API object. We probably haven't hammered out appropriate data access helpers yet.
+ if err := os.MkdirAll(cfg.OutputPath, 0775); err != nil {
+ return err
+ }
+ f, err := os.OpenFile(filepath.Join(cfg.OutputPath, "index.html"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // TODO: it's completely bork that we don't have access to the CIDs here. workspace.Catalog is Not Good right now.
+ // TODO: this probably needs sorting to be stable.
+ // Future: we should have a CID of the entire catalog tree root snapshot somewhere, too. (It should probably use prolly trees or something, though, which is not available as a convenient library yet.)
+ t := template.Must(template.New("main").Funcs(cfg.tfuncs()).Parse(`
+
+
+
catalog
+
+ modules
+
+
+ `))
+ return t.Execute(f, cfg.Cat_dab.Modules())
+}
+
+func (cfg SiteConfig) CatalogModuleAndChildrenToHtml(catMod wfapi.CatalogModule) error {
+ if err := cfg.CatalogModuleToHtml(catMod); err != nil {
+ return err
+ }
+ for _, releaseName := range catMod.Releases.Keys {
+ rel, err := cfg.Cat_dab.GetRelease(wfapi.CatalogRef{catMod.Name, releaseName, ""})
+ if err != nil {
+ return err
+ }
+ if err := cfg.ReleaseToHtml(catMod, *rel); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (cfg SiteConfig) CatalogModuleToHtml(catMod wfapi.CatalogModule) error {
+ if err := os.MkdirAll(filepath.Join(cfg.OutputPath, string(catMod.Name)), 0775); err != nil {
+ return err
+ }
+ f, err := os.OpenFile(filepath.Join(cfg.OutputPath, string(catMod.Name), "index.html"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ t := template.Must(template.New("main").Funcs(cfg.tfuncs()).Parse(`
+
+
+ module:
+
{{ .Name }}
+
+ (back to root)
+ releases
+
+ {{- $dot := . -}}
+ {{- range $releaseKey := .Releases.Keys }}
+ - {{ $releaseKey }} (cid: {{ index $dot.Releases.Values $releaseKey }})
+ {{- end }}
+
+ metadata
+ {{- range $metadataKey := .Metadata.Keys }}
+ {{ $metadataKey }}{{ index $dot.Metadata.Values $metadataKey }}
+ {{- end }}
+
+ `))
+ return t.Execute(f, catMod)
+}
+
+func (cfg SiteConfig) ReleaseToHtml(catMod wfapi.CatalogModule, rel wfapi.CatalogRelease) error {
+ if err := os.MkdirAll(filepath.Join(cfg.OutputPath, string(catMod.Name), string(rel.ReleaseName)), 0775); err != nil {
+ return err
+ }
+ f, err := os.OpenFile(filepath.Join(cfg.OutputPath, string(catMod.Name), string(rel.ReleaseName), "index.html"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ t := template.Must(template.New("main").Funcs(cfg.tfuncs()).Parse(`
+
+
+ module:
+
{{ .Module.Name }}
+ release:
+ {{ .Release.ReleaseName }}
+
+ (back to root; back to module index)
+ items
+
+ {{- $dot := .Release -}}
+ {{- range $itemKey := .Release.Items.Keys }}
+ - {{ $itemKey }} : {{ index $dot.Items.Values $itemKey }}
+ {{- end }}
+
+ metadata
+ {{- range $metadataKey := .Release.Metadata.Keys }}
+ {{ $metadataKey }}{{ index $dot.Metadata.Values $metadataKey }}
+ {{- end }}
+
+ `))
+ return t.Execute(f, map[string]interface{}{
+ "Module": catMod,
+ "Release": rel,
+ })
+}
diff --git a/pkg/cataloghtml/whee_test.go b/pkg/cataloghtml/whee_test.go
new file mode 100644
index 00000000..340d14c6
--- /dev/null
+++ b/pkg/cataloghtml/whee_test.go
@@ -0,0 +1,36 @@
+package cataloghtml
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/warpfork/warpforge/pkg/workspace"
+)
+
+func TestWhee(t *testing.T) {
+ // t.Skip("incomplete")
+ homedir, err := os.UserHomeDir()
+ if err != nil {
+ panic(err)
+ }
+ // This is a very sketchy "live" "test" that assumes you've run `warpforge catalog update` before,
+ // and operates (readonly!) on that real data.
+ cat_dab, err := workspace.OpenCatalog(os.DirFS("/"), filepath.Join(homedir, ".warpforge/catalogs/warpsys")[1:])
+ if err != nil {
+ panic(err)
+ }
+ // Output paths are currently hardcoded and can be seen in the config object below.
+ // No actual assertions take place on this; the "test" is manually looking at that output.
+ cfg := SiteConfig{
+ Ctx: context.Background(),
+ Cat_dab: cat_dab,
+ OutputPath: "/tmp/wf-test-cathtml/",
+ URLPrefix: "/tmp/wf-test-cathtml/",
+ }
+ os.RemoveAll(cfg.OutputPath)
+ if err := cfg.CatalogAndChildrenToHtml(); err != nil {
+ panic(err)
+ }
+}