blob: 70b7b46910642c8d291947469a1bd8375bead8a7 [file] [log] [blame]
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
}