[READ-ONLY] Mirror of https://github.com/darccio/zas. Most simple static website generator in Golang.
go static-site-generation static-site-generator
Something went wrong. Try again.
zas init.go
9.7 kB · 280 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281/* * 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 <http://www.gnu.org/licenses/>. */
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 = `<!DOCTYPE html><html><head><title>{{.Title}}</title></head><body>{{.Body}}</body></html>`
// 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)}