diff --git a/.gitmodules b/.gitmodules index f0a1a61..78abf0a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "schemes"] path = schemes - url = https://github.com/tinted-theming/base16-schemes.git + url = https://github.com/tinted-theming/schemes.git diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 72394be..5d2d3e3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,49 +9,48 @@ builds: goarch: - amd64 - arm64 + +archives: +- format: binary +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# Docker is split into 2 sections - the images we're building and the manifests +# we're linking them into. We start off by specifying an image for each platform +# we're building for. dockers: -- - goos: linux - goarch: amd64 +- image_templates: ["ghcr.io/base16-project/base16-builder-go:{{ .Version }}-amd64"] use: buildx - image_templates: - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-amd64" + build_flag_templates: + - --platform=linux/amd64 extra_files: - entrypoint.sh - build_flag_templates: - - "--platform=linux/amd64" -- - goos: linux +- image_templates: ["ghcr.io/base16-project/base16-builder-go:{{ .Version }}-arm64"] goarch: arm64 use: buildx - image_templates: - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-arm64" + build_flag_templates: + - --platform=linux/arm64/v8 extra_files: - entrypoint.sh - build_flag_templates: - - "--platform=linux/arm64/v8" + +# The manifests link together multiple built images as a single tag. This lets +# us bundle both an amd64 and arm64 version of the same image as the same tag. docker_manifests: -- name_template: "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}" +- name_template: "ghcr.io/base16-project/base16-builder-go:{{ .Tag }}" image_templates: - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-amd64" - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-arm64" -- name_template: "ghcr.io/tinted-theming/base16-builder-go:v{{ .Major }}.{{ .Minor }}" + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-amd64" + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-arm64" +- name_template: "ghcr.io/base16-project/base16-builder-go:v{{ .Major }}.{{ .Minor }}" image_templates: - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-amd64" - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-arm64" -- name_template: "ghcr.io/tinted-theming/base16-builder-go:latest" + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-amd64" + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-arm64" +- name_template: "ghcr.io/base16-project/base16-builder-go:latest" image_templates: - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-amd64" - - "ghcr.io/tinted-theming/base16-builder-go:{{ .Tag }}-arm64" -archives: -- format: binary - replacements: - amd64: x86_64 -snapshot: - name_template: "{{ incpatch .Version }}-next" -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-amd64" + - "ghcr.io/base16-project/base16-builder-go:{{ .Version }}-arm64" diff --git a/README.md b/README.md index ece3517..0186efb 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,23 @@ A simple builder for base16 templates and schemes. -This currently implements version 0.10.0 of the -[base16 spec](https://github.com/tinted-theming/home). +This currently implements version 0.11.0 of the [base16 spec](https://github.com/tinted-theming/home). ## Building Currently version 1.16 or higher of the Go compiler is needed. Unfortunately, because the schemes are stored in a separate repo, the schemes -repo needs to be cloned before building. +submodule needs to be cloned before building. The following command will clone the schemes directory ``` -$ git clone https://github.com/tinted-theming/base16-schemes.git schemes +$ git submodule update --init ``` Now that the repo is cloned, you can use `go build` to create a binary. You may -wish to update the schemes dir to get new included schemes. In the future this -will most likely be provided as a submodule, updated on a regular basis. +wish to update the schemes dir to get new included schemes. ## Commands @@ -37,14 +35,3 @@ Usage of base16-builder-go: -verbose Log all debug messages ``` - -## Notes - -I'm open to making a few template-specific tweaks as long as they'll be useful -to other templates. Below is a listing of the additions to the base16 spec which -this builder supports. - -### Additional variables - -* `scheme-slug-underscored` - A version of the scheme slug where dashes have - been replaced with underscores. diff --git a/go.mod b/go.mod index f738eac..475d939 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,21 @@ module github.com/tinted-theming/base16-builder-go go 1.18 require ( - github.com/cbroglie/mustache v1.3.1 - github.com/nlepage/go-tarfs v1.1.0 - github.com/sirupsen/logrus v1.8.1 + github.com/cbroglie/mustache v1.4.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/nlepage/go-tarfs v1.2.1 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.0 + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 + golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.15.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index f9025e4..191ef03 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,42 @@ -github.com/cbroglie/mustache v1.3.1 h1:S6Lrg+YHT9e2DOy6RZi9f+rU69F6NarEYGZGzw+X5LU= -github.com/cbroglie/mustache v1.3.1/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= +github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= +github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs= -github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8= +github.com/nlepage/go-tarfs v1.2.1 h1:o37+JPA+ajllGKSPfy5+YpsNHDjZnAoyfvf5GsUa+Ks= +github.com/nlepage/go-tarfs v1.2.1/go.mod h1:rno18mpMy9aEH1IiJVftFsqPyIpwqSUiAOpJYjlV2NA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 46aa249..cce459f 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -//go:embed schemes/*.yaml +//go:embed schemes/*/*.yaml var schemesFS embed.FS var ( @@ -29,7 +29,7 @@ var ( version = "dev" commit = "unknown" date = "unknown" - specVersion = "0.10.1" + specVersion = "0.11.0" ) func init() { @@ -47,7 +47,7 @@ func init() { func getSchemesFromGithub() (fs.FS, error) { log.Info("Attempting to load schemes from GitHub") - r, err := http.Get("https://github.com/tinted-theming/base16-schemes/archive/refs/heads/main.tar.gz") + r, err := http.Get("https://github.com/tinted-theming/schemes/archive/refs/heads/spec-0.11.tar.gz") if err != nil { return nil, err } @@ -64,7 +64,7 @@ func getSchemesFromGithub() (fs.FS, error) { // The archive has a subfolder containing all the schemes, so we return a // subfs of the folder. - return fs.Sub(targetFS, "base16-schemes-main") + return fs.Sub(targetFS, "schemes-spec-0.11") } func main() { diff --git a/scheme.go b/scheme.go index 10709fd..98d3ac8 100644 --- a/scheme.go +++ b/scheme.go @@ -3,155 +3,115 @@ package main import ( "fmt" "io/fs" - "path/filepath" "strings" - yaml "gopkg.in/yaml.v3" + "github.com/hashicorp/go-multierror" ) -var bases = []string{ - "00", "01", "02", "03", "04", "05", "06", "07", - "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", +type ColorScheme struct { + Name string + System string + Author string + Slug string + Description string + Variant string + Palette map[string]color } -type scheme struct { - Slug string `yaml:"-"` - - Scheme string `yaml:"scheme"` - Author string `yaml:"author"` - Description string `yaml:"description"` - - // Colors will hold all the "base*" variables. - Colors map[string]color `yaml:",inline"` -} - -func schemeFromFile(schemesFS fs.FS, fileName string) (*scheme, bool) { - ret := &scheme{} - - logger := log.WithField("file", fileName) - - if !strings.HasSuffix(fileName, ".yaml") { - logger.Error("Scheme must end in .yaml") - return nil, false - } - - data, err := fs.ReadFile(schemesFS, fileName) - if err != nil { - logger.Error(err) - return nil, false - } - - err = yaml.Unmarshal(data, ret) - if err != nil { - logger.Error(err) - return nil, false - } - - // Now that we have the data, we can sanitize it - ok := true - if ret.Scheme == "" { - logger.Error("Scheme name cannot be empty") - ok = false +func (s *ColorScheme) TemplateVariables() map[string]interface{} { + ret := map[string]interface{}{ + "scheme-name": s.Name, + "scheme-author": s.Author, + "scheme-slug": s.Slug, + "scheme-system": s.System, + "scheme-description": s.Description, + "scheme-variant": s.Variant, + "scheme-slug-underscored": strings.Replace(s.Slug, "-", "_", -1), } - // Author is a warning because there appear to be some themes - // without them. - if ret.Author == "" { - logger.Warn("Scheme author should not be empty") + ret["scheme-is-light-variant"] = s.Variant == "light" + ret["scheme-is-dark-variant"] = s.Variant == "dark" + if s.Variant != "" { + ret[fmt.Sprintf("scheme-is-%s-variant", s.Variant)] = true } - if len(bases) != len(ret.Colors) { - logger.Error("Wrong number of colors in scheme") - ok = false + for colorKey, colorVal := range s.Palette { + // Note that we only lowercase the output of this to match the reference + // repo. + ret[colorKey+"-hex"] = fmt.Sprintf("%02x%02x%02x", colorVal.R, colorVal.G, colorVal.B) + ret[colorKey+"-hex-bgr"] = fmt.Sprintf("%02x%02x%02x", colorVal.B, colorVal.G, colorVal.R) + + ret[colorKey+"-rgb-r"] = colorVal.R + ret[colorKey+"-rgb-g"] = colorVal.G + ret[colorKey+"-rgb-b"] = colorVal.B + ret[colorKey+"-dec-r"] = float32(colorVal.R) / 255 + ret[colorKey+"-dec-g"] = float32(colorVal.G) / 255 + ret[colorKey+"-dec-b"] = float32(colorVal.B) / 255 + ret[colorKey+"-hex-r"] = fmt.Sprintf("%02x", colorVal.R) + ret[colorKey+"-hex-g"] = fmt.Sprintf("%02x", colorVal.G) + ret[colorKey+"-hex-b"] = fmt.Sprintf("%02x", colorVal.B) } - // Sanitize any fields which were added later - if ret.Description == "" { - ret.Description = ret.Scheme - } + return ret +} - // Now that we've got all that out of the way, we can start - // processing stuff. +func loadSchemes(schemesFS fs.FS) ([]*ColorScheme, bool) { + schemes := make(map[string]map[string]*ColorScheme) - // Take the last path component and chop off .yaml - ret.Slug = filepath.Base(strings.TrimSuffix(fileName, ".yaml")) + merr := &multierror.Error{} - for _, base := range bases { - baseKey := "base" + base - if _, innerOk := ret.Colors[baseKey]; !innerOk { - logger.Errorf("Scheme missing %q", baseKey) - ok = false - continue + // Walk the fs.FS we have and load all yaml files as scheme files. + err := fs.WalkDir(schemesFS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err } - } - return ret, ok -} + if d.IsDir() { + return nil + } -func (s *scheme) mustacheContext() map[string]interface{} { - ret := map[string]interface{}{ - "scheme-name": s.Scheme, - "scheme-author": s.Author, - "scheme-slug": s.Slug, - "scheme-description": s.Description, + filename := d.Name() + if !strings.HasSuffix(filename, ".yaml") { + return nil + } - // Any extensions on the spec should go here - "scheme-slug-underscored": strings.Replace(s.Slug, "-", "_", -1), - } + scheme, err := LoadScheme(schemesFS, path) + if err != nil { + merr = AppendError(merr, multierror.Prefix(err, fmt.Sprintf("failed to load scheme %s:", path))) + return nil + } - for _, base := range bases { - baseKey := "base" + base - baseVal := s.Colors[baseKey] + if _, ok := schemes[scheme.System]; !ok { + schemes[scheme.System] = make(map[string]*ColorScheme) + } - // Note that we only lowercase the output of this to match the reference - // repo. - ret[baseKey+"-hex"] = fmt.Sprintf("%02x%02x%02x", baseVal.R, baseVal.G, baseVal.B) - ret[baseKey+"-hex-bgr"] = fmt.Sprintf("%02x%02x%02x", baseVal.B, baseVal.G, baseVal.R) - - ret[baseKey+"-rgb-r"] = baseVal.R - ret[baseKey+"-rgb-g"] = baseVal.G - ret[baseKey+"-rgb-b"] = baseVal.B - ret[baseKey+"-dec-r"] = float32(baseVal.R) / 255 - ret[baseKey+"-dec-g"] = float32(baseVal.G) / 255 - ret[baseKey+"-dec-b"] = float32(baseVal.B) / 255 - ret[baseKey+"-hex-r"] = fmt.Sprintf("%02x", baseVal.R) - ret[baseKey+"-hex-g"] = fmt.Sprintf("%02x", baseVal.G) - ret[baseKey+"-hex-b"] = fmt.Sprintf("%02x", baseVal.B) - } + if _, ok := schemes[scheme.System][scheme.Slug]; ok { + merr = AppendErrorf(merr, "conflicting scheme %s-%s", scheme.System, scheme.Slug) + return nil + } - return ret -} + log.Debugf("Found scheme %q", scheme.Slug) -func loadSchemes(schemesFS fs.FS) ([]*scheme, bool) { - schemes := make(map[string]*scheme) + schemes[scheme.System][scheme.Slug] = scheme - schemePaths, err := fs.Glob(schemesFS, "*.yaml") + return nil + }) if err != nil { log.Error(err) return nil, false } - for _, schemePath := range schemePaths { - scheme, ok := schemeFromFile(schemesFS, schemePath) - if !ok { - log.Errorf("Failed to load scheme") - return nil, false - } - - // XXX: this should never happen because it's now a single schemes dir, - // but we include this check just in case someone messed something up. - if _, ok := schemes[scheme.Slug]; ok { - log.WithField("scheme", scheme.Slug).Warnf("Conflicting scheme") - } - - log.Debugf("Found scheme %q", scheme.Slug) - - schemes[scheme.Slug] = scheme + if err := merr.ErrorOrNil(); err != nil { + log.Error(err) + return nil, false } - ret := []*scheme{} - for _, scheme := range schemes { - ret = append(ret, scheme) + // Flatten all the schemes into a list. + var ret []*ColorScheme + for _, system := range schemes { + for _, scheme := range system { + ret = append(ret, scheme) + } } return ret, true diff --git a/schemes b/schemes index a3dc916..b686498 160000 --- a/schemes +++ b/schemes @@ -1 +1 @@ -Subproject commit a3dc916cf90471a422c0bfe1bb4b1bdd12185ced +Subproject commit b68649875afb03bc30ebbc738390f3ae0dc444ac diff --git a/systems.go b/systems.go new file mode 100644 index 0000000..71226a3 --- /dev/null +++ b/systems.go @@ -0,0 +1,184 @@ +package main + +import ( + "errors" + "fmt" + "io/fs" + "path/filepath" + "strings" + + "github.com/hashicorp/go-multierror" + "gopkg.in/yaml.v3" +) + +type baseScheme struct { + System string `yaml:"system"` +} + +func LoadScheme(schemesFS fs.FS, filename string) (*ColorScheme, error) { + var baseScheme baseScheme + + data, err := fs.ReadFile(schemesFS, filename) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(data, &baseScheme) + if err != nil { + return nil, err + } + + // If no system is specified, it should be loaded as a legacy scheme + // (base16/base24). + if baseScheme.System == "" { + return LoadLegacyScheme(filename, data) + } + + return LoadCommonScheme(data) +} + +var base16Bases = []string{ + "base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07", + "base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F", +} + +var base24Bases = []string{ + "base10", "base11", "base12", "base13", "base14", "base15", "base16", "base17", +} + +type legacyScheme struct { + Name string `yaml:"scheme"` + Author string `yaml:"author"` + Description string `yaml:"description"` + Palette map[string]color `yaml:",inline"` +} + +func LoadLegacyScheme(filename string, data []byte) (*ColorScheme, error) { + var scheme legacyScheme + err := yaml.Unmarshal(data, &scheme) + if err != nil { + return nil, err + } + + var missingColors []string + + for _, baseName := range base16Bases { + if _, ok := scheme.Palette[baseName]; !ok { + missingColors = append(missingColors, baseName) + } + } + + if len(missingColors) != 0 { + return nil, fmt.Errorf("Missing colors from base16 palette: %s", strings.Join(missingColors, ", ")) + } + + for _, baseName := range base24Bases { + if _, ok := scheme.Palette[baseName]; !ok { + missingColors = append(missingColors, baseName) + } + } + + // If there were more than 16 colors and there were missing colors, we know + // they were from the base24 palette. + if len(scheme.Palette) > 16 && len(missingColors) != 0 { + return nil, fmt.Errorf("Missing colors from base24 palette: %s", strings.Join(missingColors, ", ")) + } + + // Now that we've validated the data, we can convert it to the internal + // ColorScheme format. + ret := &ColorScheme{ + Name: scheme.Name, + Author: scheme.Author, + Slug: filepath.Base(strings.TrimSuffix(filename, ".yaml")), + Description: scheme.Description, + Palette: scheme.Palette, + } + + if len(ret.Palette) == 16 { + ret.System = "base16" + } else if len(ret.Palette) == 24 { + ret.System = "base24" + } else { + return nil, fmt.Errorf("Unexpected number of palette colors: %d", len(ret.Palette)) + } + + // Description isn't technically in base16, so we fall back to the name. + if ret.Description == "" { + ret.Description = ret.Name + } + + return ret, nil +} + +type commonScheme struct { + Slug string `yaml:"slug"` + Name string `yaml:"name"` + Author string `yaml:"author"` + System string `yaml:"system"` + Description string `yaml:"description"` + Variant string `yaml:"variant"` + Palette map[string]color `yaml:"palette"` + Mappings map[string]string `yaml:"mappings"` +} + +func LoadCommonScheme(data []byte) (*ColorScheme, error) { + var scheme commonScheme + + err := yaml.Unmarshal(data, &scheme) + if err != nil { + return nil, err + } + + ret := &ColorScheme{ + Slug: scheme.Slug, + Name: scheme.Name, + Author: scheme.Author, + System: scheme.System, + Description: scheme.Description, + Variant: scheme.Variant, + Palette: scheme.Palette, + } + + // Missing the author field is a warning, not an error because there appear + // to be some pre-existing themes without them. + if ret.Author == "" { + log.Warn("Scheme author should not be empty") + } + + // If we have an empty slug, we need to infer it from the scheme name. This + // involves normalizing any unicode, lower-casing it, replacing spaces with + // dashes. + if ret.Slug == "" { + ret.Slug, err = Slugify(ret.Name) + if err != nil { + return nil, err + } + } + + if ret.Description == "" { + ret.Description = ret.Name + } + + if ret.Name == "" { + return nil, errors.New("scheme name cannot be empty") + } + + merr := &multierror.Error{} + + // Copy any mappings into the palette + for key, alias := range scheme.Mappings { + if _, ok := ret.Palette[key]; ok { + merr = AppendErrorf(merr, "duplicate key in palette and mappings: %s", key) + continue + } + + if _, ok := ret.Palette[alias]; !ok { + merr = AppendErrorf(merr, "missing referenced alias: %s", alias) + continue + } + + ret.Palette[key] = ret.Palette[alias] + } + + return ret, merr.ErrorOrNil() +} diff --git a/template.go b/template.go index 71c00ce..c5d7fe2 100644 --- a/template.go +++ b/template.go @@ -1,20 +1,24 @@ package main import ( + "errors" "fmt" "io/ioutil" "os" "path/filepath" "github.com/cbroglie/mustache" + "golang.org/x/exp/slices" yaml "gopkg.in/yaml.v3" ) type template struct { - Name string `yaml:"-"` - Dir string `yaml:"-"` - Extension string `yaml:"extension"` - OutputDir string `yaml:"output"` + Name string `yaml:"-"` + Dir string `yaml:"-"` + Filename string `yaml:"filename"` + Extension string `yaml:"extension"` + OutputDir string `yaml:"output"` + SupportedSystems []string `yaml:"supported-systems"` } func templatesFromFile(templatesDir string) ([]*template, error) { @@ -34,12 +38,24 @@ func templatesFromFile(templatesDir string) ([]*template, error) { t.Name = k t.Dir = templatesDir - if t.OutputDir == "" { - log.Warn("OutputDir missing from theme config block") + if t.Filename == "" { + log.Info("Filename missing from theme config block, inferring from OutputDir and Extension") + + if t.OutputDir == "" { + log.Warn("OutputDir missing from theme config block") + t.OutputDir = "." + } + + if t.Extension == "" { + return nil, errors.New("Extension missing from theme config block") + } + + t.Filename = fmt.Sprintf("%s/{{ scheme-system }}-{{ scheme-slug }}%s", t.OutputDir, t.Extension) } - if t.Extension == "" { - log.Warn("Extension missing from theme config block") + if len(t.SupportedSystems) == 0 { + log.Warn("Systems not set in theme config block, inferring base16") + t.SupportedSystems = []string{"base16"} } log.Debugf("Found template %q in dir %q", t.Name, t.Dir) @@ -50,7 +66,7 @@ func templatesFromFile(templatesDir string) ([]*template, error) { return ret, nil } -func (t *template) Render(schemes []*scheme) error { +func (t *template) Render(schemes []*ColorScheme) error { m, err := mustache.ParseFile(filepath.Join(t.Dir, "templates", t.Name+".mustache")) if err != nil { return err @@ -69,9 +85,28 @@ func (t *template) Render(schemes []*scheme) error { return fmt.Errorf("Output dir %s is not a dir", outputDir) } + var templateRendered bool + for _, scheme := range schemes { - fileName := filepath.Join(outputDir, "base16-"+scheme.Slug+t.Extension) - rendered, err := m.Render(scheme.mustacheContext()) + templateVariables := scheme.TemplateVariables() + + // If the scheme's system wasn't in this template's supported systems + // list, we skip it. + if !slices.Contains(t.SupportedSystems, scheme.System) { + continue + } + + filenameTemplate, err := mustache.ParseString(t.Filename) + if err != nil { + return err + } + + fileName, err := filenameTemplate.Render(templateVariables) + if err != nil { + return err + } + + rendered, err := m.Render(templateVariables) if err != nil { return err } @@ -82,6 +117,14 @@ func (t *template) Render(schemes []*scheme) error { if err != nil { return err } + + templateRendered = true + } + + // We want to ensure at least 1 valid scheme exists for each template being + // rendered. + if !templateRendered { + return errors.New("No valid schemes for template") } return nil diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..9b978de --- /dev/null +++ b/utils.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "strings" + "unicode" + + "github.com/hashicorp/go-multierror" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +// Slugify takes an input string, drops all non-alphanumeric ASCII characters or +// spaces/dashes and lower cases it. +func Slugify(str string) (string, error) { + // This works by normalizing the string to Unicode NFD form (which is the + // decomposed version), and then dropping any combining characters. + result, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn))), str) + if err != nil { + return "", err + } + + // The previous nomalization should have been enough, but for good measure, + // we drop any non-ascii characters. + result = strings.Map(func(r rune) rune { + // Drop all unicode + if r > unicode.MaxASCII { + return -1 + } + + // Replace spaces with dash, keep existing dashes. + if r == ' ' || r == '-' { + return '-' + } + + // Keep alpha-numeric characters + if unicode.IsLetter(r) || unicode.IsNumber(r) { + return r + } + + // Drop everything else + return -1 + }, result) + + return strings.ToLower(result), nil +} + +func AppendErrorf(err error, format string, args ...interface{}) *multierror.Error { + return multierror.Append(err, fmt.Errorf(format, args...)) +} + +func AppendError(err error, errs ...error) *multierror.Error { + return multierror.Append(err, errs...) +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..97309ac --- /dev/null +++ b/utils_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSlugify(t *testing.T) { + var testCases = []struct { + Input string + Output string + }{ + { + Input: "Hello World", + Output: "hello-world", + }, + { + Input: "Rosé Pine", + Output: "rose-pine", + }, + } + + for _, testCase := range testCases { + ret, err := Slugify(testCase.Input) + assert.NoError(t, err) + assert.Equal(t, testCase.Output, ret) + } +}