devtools/hackdoc: init

This is hackdoc, a documentation rendering tool for monorepos.

This is the first code iteration, that can only serve from a local git
checkout.

The code is incomplete, and is WIP.

Change-Id: I68ef7a991191c1bb1b0fdd2a8d8353aba642e28f
diff --git a/devtools/hackdoc/config/config.go b/devtools/hackdoc/config/config.go
new file mode 100644
index 0000000..70b7b46
--- /dev/null
+++ b/devtools/hackdoc/config/config.go
@@ -0,0 +1,152 @@
+package config
+
+import (
+	"fmt"
+	"html/template"
+	"strings"
+
+	"github.com/BurntSushi/toml"
+	"github.com/golang/glog"
+
+	"code.hackerspace.pl/hscloud/devtools/hackdoc/source"
+)
+
+// Config is a configuration concerning a given path of the source. It is built
+// from files present in the source, and from global configuration.
+type Config struct {
+	// DefaultIndex is the filenames that should attempt to be rendered if no exact path is given
+	DefaultIndex []string
+	// Templates are the templates available to render markdown files, keyed by template name.
+	Templates map[string]*template.Template
+
+	// Errors that occured while building this config (due to config file errors, etc).
+	Errors map[string]error
+}
+
+type configToml struct {
+	DefaultIndex []string                       `toml:"default_index"`
+	Templates    map[string]*configTomlTemplate `toml:"template"`
+}
+
+type configTomlTemplate struct {
+	Sources []string `toml:"sources"`
+}
+
+func parseToml(data []byte) (*configToml, error) {
+	var c configToml
+	err := toml.Unmarshal(data, &c)
+	if err != nil {
+		return nil, err
+	}
+	if c.Templates == nil {
+		c.Templates = make(map[string]*configTomlTemplate)
+	}
+	return &c, nil
+}
+
+func configFileLocations(path string) []string {
+	// Support for unix-style filesystem prefix (/foo/bar/baz) and
+	// perforce-depot-style prefix (//foo/bar/baz).
+	// Also support relative paths.
+	pathTrimmed := strings.TrimLeft(path, "/")
+	prefixLen := len(path) - len(pathTrimmed)
+	prefix := path[:prefixLen]
+	path = pathTrimmed
+	if len(prefix) > 2 {
+		return nil
+	}
+
+	// Turn path into possible directory names, including root.
+	path = strings.Trim(path, "/")
+	parts := strings.Split(path, "/")
+	if parts[0] != "" {
+		parts = append([]string{""}, parts...)
+	}
+
+	locations := []string{}
+	for i, _ := range parts {
+		p := strings.Join(parts[:i+1], "/")
+		p += "/hackdoc.toml"
+		p = prefix + strings.Trim(p, "/")
+		locations = append(locations, p)
+	}
+	return locations
+}
+
+func ForPath(s source.Source, path string) (*Config, error) {
+	if path != "//" {
+		path = strings.TrimRight(path, "/")
+	}
+
+	// Try cache.
+	cacheKey := fmt.Sprintf("config:%s", path)
+	if v := s.CacheGet(cacheKey); v != nil {
+		cfg, ok := v.(*Config)
+		if !ok {
+			glog.Errorf("Cache key %q corrupted, deleting", cacheKey)
+			s.CacheSet([]string{}, cacheKey, nil)
+		} else {
+			return cfg, nil
+		}
+	}
+
+	// Feed cache.
+	cfg := &Config{
+		Templates: make(map[string]*template.Template),
+		Errors:    make(map[string]error),
+	}
+
+	tomlPaths := configFileLocations(path)
+	for _, p := range tomlPaths {
+		file, err := s.IsFile(p)
+		if err != nil {
+			return nil, fmt.Errorf("IsFile(%q): %w", path, err)
+		}
+		if !file {
+			continue
+		}
+		data, err := s.ReadFile(p)
+		if err != nil {
+			return nil, fmt.Errorf("ReadFile(%q): %w", path, err)
+		}
+
+		c, err := parseToml(data)
+		if err != nil {
+			cfg.Errors[p] = err
+			continue
+		}
+
+		err = cfg.updateFromToml(p, s, c)
+		if err != nil {
+			return nil, fmt.Errorf("updating from %q: %w", p, err)
+		}
+	}
+
+	return cfg, nil
+}
+
+func (c *Config) updateFromToml(p string, s source.Source, t *configToml) error {
+	if t.DefaultIndex != nil {
+		c.DefaultIndex = t.DefaultIndex
+	}
+
+	for k, v := range t.Templates {
+		tmpl := template.New(k)
+
+		for _, source := range v.Sources {
+			data, err := s.ReadFile(source)
+			if err != nil {
+				c.Errors[p] = fmt.Errorf("reading template file %q: %w", source, err)
+				return nil
+			}
+			tmpl, err = tmpl.Parse(string(data))
+			if err != nil {
+				c.Errors[p] = fmt.Errorf("parsing template file %q: %w", source, err)
+				return nil
+			}
+		}
+		c.Templates[k] = tmpl
+	}
+
+	return nil
+}