/* * Copyright (c) 2013 Dario Castañé. * This file is part of Zas. * * Zas is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Zas is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Zas. If not, see . */ package zas import ( "fmt" "os" "path/filepath" "dario.cat/mergo" "github.com/darccio/zas/internal/i18n" yaml "go.yaml.in/yaml/v3" ) // ConfigSection is a section of zas configuration: a string-keyed map, // as produced by decoding a YAML mapping and by every ConfigSection{...} // literal in this package. go.yaml.in/yaml/v3 special-cases a named map // type whose keys are strings and whose values are interface{}: once it // decodes the outermost mapping into a ConfigSection, it keeps using that // same named type for every nested mapping found inside an interface{} // value, at any depth (see stringMapType propagation in its decoder) - // so a config.yml with several levels of nested sections decodes into // nested ConfigSection values all the way down, with no custom // UnmarshalYAML method required. (That propagation requires every key in // the mapping to be a string; GetSection's map[string]interface{} // fallback below covers a section that arrives some other way.) type ConfigSection map[string]interface{} // NewConfig loads ConfigFile (as defined in constants.go). // It must be a YAML file. func NewConfig() (ConfigSection, error) { data, err := os.ReadFile(ConfigFile) if err != nil { return nil, err } var config ConfigSection if err = yaml.Unmarshal(data, &config); err != nil { return nil, err } if config == nil { config = ConfigSection{} } // mergo.Merge only recursively merges a nested map's own keys when // dst already has a real (even if empty) map value to merge into. If // a whole top-level section like "mimetypes" is absent from config, // mergo instead falls back to assigning defaults' own map object for // that section directly - config and defaults would then share the // exact same underlying map, so mutating config's defaulted section // (e.g. through GetSection) would corrupt defaults, and any other // holder of it, for as long as either is still referenced. Giving // every missing section a fresh, empty map first means mergo's own // merge does the actual copying, one key at a time, into a map // nothing else references - no separate deep-copy logic needed. defaults := DefaultConfig() for key, value := range defaults { if _, ok := value.(ConfigSection); !ok { continue } if _, exists := config[key]; !exists { config[key] = ConfigSection{} } } if err = mergo.Merge(&config, defaults); err != nil { return nil, err } return config, nil } // NewI18n loads I18nFile (as defined in constants.go). // It must be a YAML file. func NewI18n(mainlang string) (strs i18n.Strings, err error) { data, err := os.ReadFile(I18nFile) if err != nil { if os.IsNotExist(err) { return make(i18n.Strings), nil } return nil, err } strs = make(i18n.Strings) if err = yaml.Unmarshal(data, &strs); err != nil { return nil, err } for k, v := range strs { if v == nil { v = make(map[string]string) strs[k] = v } if _, ok := v[mainlang]; !ok { v[mainlang] = k } } return strs, nil } // GetString returns a string value from current section, or "" if key is // missing or not a string. Callers that need to tell "absent"/"wrong type" // apart from a legitimately empty string should use GetStringOK instead. func (cs ConfigSection) GetString(key string) (value string) { value, _ = cs.GetStringOK(key) return } // GetStringOK returns a string value from current section, and whether key // was present and held a string value. ok is false both when key is absent // and when it holds a non-string value; callers that need to tell those two // cases apart should check key's presence themselves beforehand. func (cs ConfigSection) GetStringOK(key string) (value string, ok bool) { value, ok = cs[key].(string) return } // GetBool returns a bool value from current section, or false if key is // missing or not a bool. Callers that need to tell "absent"/"wrong type" // apart from a legitimately false value should use GetBoolOK instead. func (cs ConfigSection) GetBool(key string) (value bool) { value, _ = cs.GetBoolOK(key) return } // GetBoolOK returns a bool value from current section, and whether key was // present and held a bool value. ok is false both when key is absent and // when it holds a non-bool value. func (cs ConfigSection) GetBoolOK(key string) (value, ok bool) { value, ok = cs[key].(bool) return } // GetSection returns a subsection from current section, or nil if key is // missing or not a section. func (cs ConfigSection) GetSection(key string) (value ConfigSection) { value, _ = cs.GetSectionOK(key) return } // GetSectionOK returns a subsection from current section, and whether key // resolved to a real section. func (cs ConfigSection) GetSectionOK(key string) (value ConfigSection, ok bool) { switch raw := cs[key].(type) { case ConfigSection: value, ok = raw, true case map[string]interface{}: // A section built by some means other than decoding YAML into a // ConfigSection-typed destination (e.g. a caller constructing one // by hand as a plain map[string]interface{}) - see // TestGetSectionRawYAMLMap. value, ok = ConfigSection(raw), true } return } // GetZString returns a string value from default Zas section. func (cs ConfigSection) GetZString(key string) string { s := cs.GetSection(Name) return s.GetString(key) } // GetStringSlice returns a string-slice value from current section, or nil // if key is missing, not a sequence, or contains any non-string element. func (cs ConfigSection) GetStringSlice(key string) (value []string) { value, _ = cs.GetStringSliceOK(key) return } // GetStringSliceOK returns a string-slice value from current section, and // whether key was present and held a sequence of strings. ok is false when // key is absent, holds a non-sequence value, or holds a sequence with any // non-string element - go.yaml.in/yaml/v3 decodes a YAML sequence into // []interface{}, the same way it decodes a mapping into ConfigSection (see // that type's doc comment above), so each element still needs its own type // assertion. func (cs ConfigSection) GetStringSliceOK(key string) (value []string, ok bool) { raw, isSeq := cs[key].([]interface{}) if !isSeq { return nil, false } value = make([]string, 0, len(raw)) for _, item := range raw { s, isString := item.(string) if !isString { return nil, false } value = append(value, s) } return value, true } // GetZStringSlice returns a string-slice value from the default Zas // section. func (cs ConfigSection) GetZStringSlice(key string) []string { s := cs.GetSection(Name) return s.GetStringSlice(key) } // defaultLayout is the minimal html/template document scaffolded as // LayoutFile by Init.Run: just enough for "zas init && zas" to succeed // standalone, matching the README's quickstart. Modeled on the smallest // layouts under testdata (e.g. testdata/walk-error-base/.zas/layout.html), // it renders each page's title and body and nothing else - it's a // starting point, not a real design. const defaultLayout = ` {{.Title}} {{.Body}} ` // Init implements the "init" subcommand, which scaffolds a new Zas // repository in the current directory. type Init struct { // Force, when true, makes Run overwrite an existing ConfigFile or // LayoutFile with its scaffolded default. When false (the default), // Run leaves either file alone if it already exists, so re-running // "zas init" on a site never silently discards hand-edited content. Force bool } // Run scaffolds Dir, and writes a default ConfigFile and LayoutFile. // Without Force set, an existing ConfigFile or LayoutFile is left // untouched (Run says so rather than staying silent about it); with // Force set, both are overwritten unconditionally. func (i *Init) Run() error { path, err := filepath.Abs(Dir) if err != nil { return err } if _, statErr := os.Stat(Dir); os.IsNotExist(statErr) { if err := os.Mkdir(Dir, DefaultDirPerm); err != nil { return err } fmt.Printf("Initialized empty %s repository in %s\n", DisplayName, path) } else { fmt.Printf("Reinitialized existing %s repository in %s\n", DisplayName, path) } config, err := yaml.Marshal(DefaultConfig()) if err != nil { return err } if err := i.writeScaffold(ConfigFile, config); err != nil { return err } return i.writeScaffold(LayoutFile, []byte(defaultLayout)) } // writeScaffold writes data to path, creating or overwriting it, unless // path already exists and Force is false - in which case it leaves the // existing file untouched and prints that it did, instead of silently // discarding whatever is there. func (i *Init) writeScaffold(path string, data []byte) error { if !i.Force { switch _, statErr := os.Stat(path); { case statErr == nil: fmt.Printf("%s already exists, not overwriting (use -force to overwrite)\n", path) return nil case !os.IsNotExist(statErr): return statErr } } return os.WriteFile(path, data, DefaultFilePerm) }