diff --git a/pkg/cataloghtml/catalogIndex.tmpl.html b/pkg/cataloghtml/catalogIndex.tmpl.html
new file mode 100644
index 00000000..e294bb3f
--- /dev/null
+++ b/pkg/cataloghtml/catalogIndex.tmpl.html
@@ -0,0 +1,27 @@
+{{- /*
+ This template is for the index page made at the root of the catalog html view.
+ It just lists and links to all the modules in the catalog.
+
+ A couple of todos for this document:
+ - We should print CIDs of the modules!
+ ... But we don't right now because the our accessor functions don't make it easy to get. (Work needed on workspace.Catalog.)
+ - Farther future: we should have a CID of the entire catalog tree root snapshot somewhere, too.
+ ... But we don't right now, because that should probably use prolly trees (or some other scalable hash tree thing), which is not available and rigged up as a convenient library yet.
+
+*/ -}}
+
+
{{ $itemKey }} : {{ index $dot.Items.Values $itemKey }}
+ {{- end }}
+
+
metadata
+ {{- range $metadataKey := .Release.Metadata.Keys }}
+
{{ $metadataKey }}
{{ index $dot.Metadata.Values $metadataKey }}
+ {{- end }}
+
+
diff --git a/pkg/cataloghtml/cataloghtml.go b/pkg/cataloghtml/cataloghtml.go
new file mode 100644
index 00000000..ef2a7e62
--- /dev/null
+++ b/pkg/cataloghtml/cataloghtml.go
@@ -0,0 +1,202 @@
+package cataloghtml
+
+import (
+ "context"
+ _ "embed"
+ "html/template"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+
+ "github.com/warpfork/warpforge/pkg/workspace"
+ "github.com/warpfork/warpforge/wfapi"
+)
+
+var (
+ //go:embed catalogIndex.tmpl.html
+ catalogIndexTemplate string
+
+ //go:embed catalogModule.tmpl.html
+ catalogModuleTemplate string
+
+ //go:embed catalogRelease.tmpl.html
+ catalogReleaseTemplate string
+
+ //go:embed css.css
+ cssBody []byte
+
+ // FUTURE: consider the use of `embed.FS` and `template.ParseFS()`, if there grow to be many files here.
+ // It has slightly less compile-time safety checks on filenames, though.
+)
+
+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 {
+ // Very small helper function to stringify things.
+ // This is useful for things that are literally typedefs of string but the template package isn't smart enough to be calm about unboxing it.
+ // (It also does return something for values of non-string types, but not something very useful.)
+ return reflect.ValueOf(x).String()
+ },
+ "url": func(parts ...string) string {
+ return path.Join(append([]string{cfg.URLPrefix}, parts...)...)
+ },
+ }
+}
+
+// CatalogAndChildrenToHtml performs CatalogToHtml, and also
+// procedes to invoke the html'ing of all modules within.
+// Additionally, it does all the other "once" things
+// (namely, outputs a copy of the css).
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+// - warpforge-error-catalog-invalid -- in case the catalog data is invalid.
+// - warpforge-error-catalog-parse -- in case the catalog data failed to parse entirely.
+func (cfg SiteConfig) CatalogAndChildrenToHtml() error {
+ // Emit catalog index.
+ if err := cfg.CatalogToHtml(); err != nil {
+ return err
+ }
+ // Emit the "once" stuff.
+ if err := os.WriteFile(filepath.Join(cfg.OutputPath, "css.css"), cssBody, 0644); err != nil {
+ return wfapi.ErrorIo("couldn't open file for css as part of cataloghtml emission", nil, err)
+ }
+ // Emit all modules within.
+ 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
+}
+
+// doTemplate does the common bits of making files, processing the template,
+// and getting the output where it needs to go.
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+func (cfg SiteConfig) doTemplate(outputPath string, tmpl string, data interface{}) error {
+ if err := os.MkdirAll(filepath.Dir(outputPath), 0775); err != nil {
+ return wfapi.ErrorIo("couldn't mkdir during cataloghtml emission", nil, err)
+ }
+ f, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
+ if err != nil {
+ return wfapi.ErrorIo("couldn't open file for writing during cataloghtml emission", nil, err)
+ }
+ defer f.Close()
+
+ t := template.Must(template.New("main").Funcs(cfg.tfuncs()).Parse(tmpl))
+ if err := t.Execute(f, data); err != nil {
+ return wfapi.ErrorInternal("templating failed", 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.
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+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.
+ return cfg.doTemplate(
+ filepath.Join(cfg.OutputPath, "index.html"),
+ catalogIndexTemplate,
+ cfg.Cat_dab.Modules(),
+ )
+}
+
+// CatalogModuleAndChildrenToHtml performs CatalogModuleToHtml, and also
+// procedes to invoke the html'ing of all releases within.
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+// - warpforge-error-catalog-invalid -- in case the catalog data is invalid.
+// - warpforge-error-catalog-parse -- in case the catalog data failed to parse entirely.
+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
+}
+
+// CatalogModuleToHtml generates a page for a module which enumerates
+// and links to all the releases within it,
+// as well as enumerates all the metadata attached to the catalog module.
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+func (cfg SiteConfig) CatalogModuleToHtml(catMod wfapi.CatalogModule) error {
+ return cfg.doTemplate(
+ filepath.Join(cfg.OutputPath, string(catMod.Name), "_module.html"),
+ catalogModuleTemplate,
+ catMod,
+ )
+}
+
+// CatalogModuleToHtml generates a page for a release within a catalog module
+// which enumerates all the items within it,
+// as well as enumerates all the metadata attached to the release.
+//
+// Possible but not-yet-implemented future features of this output might include:
+// linking better to metadata that references other documents (such as Replays);
+// links to neighboring (e.g. forward and previous) releases; etc.
+//
+// Errors:
+//
+// - warpforge-error-io -- in case of errors writing out the new html content.
+// - warpforge-error-internal -- in case of templating errors.
+func (cfg SiteConfig) ReleaseToHtml(catMod wfapi.CatalogModule, rel wfapi.CatalogRelease) error {
+ return cfg.doTemplate(
+ filepath.Join(cfg.OutputPath, string(catMod.Name), "_releases", string(rel.ReleaseName)+".html"),
+ catalogReleaseTemplate,
+ map[string]interface{}{
+ "Module": catMod,
+ "Release": rel,
+ },
+ )
+}
diff --git a/pkg/cataloghtml/css.css b/pkg/cataloghtml/css.css
new file mode 100644
index 00000000..d6d5d222
--- /dev/null
+++ b/pkg/cataloghtml/css.css
@@ -0,0 +1,5 @@
+/*
+body {
+ color: #F00;
+}
+*/
diff --git a/pkg/cataloghtml/demo_test.go b/pkg/cataloghtml/demo_test.go
new file mode 100644
index 00000000..191a5ff9
--- /dev/null
+++ b/pkg/cataloghtml/demo_test.go
@@ -0,0 +1,33 @@
+package cataloghtml
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/warpfork/warpforge/pkg/workspace"
+)
+
+func TestWhee(t *testing.T) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ panic(err)
+ }
+ cat_dab, err := workspace.OpenCatalog(os.DirFS("/"), filepath.Join(cwd, "../../.warpforge/catalog")[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)
+ }
+}