commit 47c700808a6f622f03694c069c68b70eb67d27c8 Author: jolheiser Date: Thu May 5 16:28:39 2022 -0500 Initial commit Signed-off-by: jolheiser diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea720c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +/chromajson.exe +/chromajson \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec2045e --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 John Olheiser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2380989 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# chromajson + +A JSON schema and loader for [chroma](https://github.com/alecthomas/chroma). + +## Schema + +[schema.json](schema/schema.json) provides a [JSON schema](https://json-schema.org). + +## Styles + +There is a valid [catppuccin](https://github.com/catppuccin) style in [catppuccin.json](schema/testdata/catppuccin.json). + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/chromajson.go b/chromajson.go new file mode 100644 index 0000000..b48bf37 --- /dev/null +++ b/chromajson.go @@ -0,0 +1,79 @@ +package chromajson + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/styles" +) + +// Style is a chroma style +type Style struct { + Name string `json:"name"` + Entries map[string]StyleEntry `json:"style"` +} + +// StyleEntry is a mapping for a chroma style +type StyleEntry struct { + Color string `json:"color"` + Background string `json:"background"` + Border string `json:"border"` + Accents []string `json:"accent"` +} + +// String returns a chroma-representable style entry +func (s StyleEntry) String() string { + normalise := func(c string) string { + return "#" + strings.TrimLeft(c, "#") + } + + parts := make([]string, 0, 4) + if len(s.Accents) > 0 { + parts = append(parts, strings.Join(s.Accents, " ")) + } + if s.Background != "" { + parts = append(parts, fmt.Sprintf("bg:%s", normalise(s.Background))) + } + if s.Border != "" { + parts = append(parts, fmt.Sprintf("border:%s", normalise(s.Border))) + } + if s.Color != "" { + parts = append(parts, normalise(s.Color)) + } + return strings.Join(parts, " ") +} + +// Chroma converts a Style to a chroma.Style +func (s *Style) Chroma() (*chroma.Style, error) { + style := chroma.NewStyleBuilder(s.Name) + for name, entry := range s.Entries { + if token, ok := tokenToType[name]; ok { + style.Add(token, entry.String()) + } + } + return style.Build() +} + +// Register registers the Style directly to chroma +func (s *Style) Register() error { + style, err := s.Chroma() + if err != nil { + return err + } + styles.Register(style) + return nil +} + +// LoadStyle loads a Style from an io.Reader +func LoadStyle(r io.Reader) (*Style, error) { + var s Style + d := json.NewDecoder(r) + d.DisallowUnknownFields() + if err := d.Decode(&s); err != nil { + return nil, err + } + return &s, nil +} diff --git a/chromajson_test.go b/chromajson_test.go new file mode 100644 index 0000000..2519522 --- /dev/null +++ b/chromajson_test.go @@ -0,0 +1,31 @@ +package chromajson + +import ( + "testing" + + "github.com/matryer/is" +) + +func TestStyleEntry(t *testing.T) { + tt := []struct { + Name string + Entry *StyleEntry + Expected string + }{ + {Name: "color1", Entry: &StyleEntry{Color: "#FFF"}, Expected: "#FFF"}, + {Name: "color2", Entry: &StyleEntry{Color: "white"}, Expected: "#white"}, + {Name: "background1", Entry: &StyleEntry{Background: "#FFF"}, Expected: "bg:#FFF"}, + {Name: "background2", Entry: &StyleEntry{Background: "white"}, Expected: "bg:#white"}, + {Name: "border1", Entry: &StyleEntry{Border: "#FFF"}, Expected: "border:#FFF"}, + {Name: "border2", Entry: &StyleEntry{Border: "white"}, Expected: "border:#white"}, + {Name: "accent", Entry: &StyleEntry{Accents: []string{"bold"}}, Expected: "bold"}, + {Name: "mix", Entry: &StyleEntry{Color: "black", Accents: []string{"bold"}, Background: "#ffffff"}, Expected: "bold bg:#ffffff #black"}, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + assert := is.New(t) + assert.Equal(tc.Entry.String(), tc.Expected) + }) + } +} diff --git a/cmd/chromajson/main.go b/cmd/chromajson/main.go new file mode 100644 index 0000000..d3726eb --- /dev/null +++ b/cmd/chromajson/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + + "github.com/alecthomas/chroma/v2/quick" + + "go.jolheiser.com/chromajson" + "go.jolheiser.com/chromajson/schema" +) + +func main() { + fs := flag.NewFlagSet("chromajson", flag.ExitOnError) + styleFlag := fs.String("style", ".style.json", "Chroma JSON style config") + lintFlag := fs.Bool("lint", false, "Lint config and exit") + if err := fs.Parse(os.Args[1:]); err != nil { + fmt.Println(err) + return + } + + if !*lintFlag && fs.NArg() == 0 { + fmt.Println("input argument is required to show style") + return + } + + fi, err := os.Open(*styleFlag) + if err != nil { + fmt.Println(err) + return + } + defer fi.Close() + + if *lintFlag { + lint(fi) + return + } + + style, err := chromajson.LoadStyle(fi) + if err != nil { + fmt.Println(err) + return + } + + if err := style.Register(); err != nil { + fmt.Println(err) + return + } + + source, err := os.ReadFile(fs.Arg(0)) + if err != nil { + fmt.Println(err) + return + } + + if err := quick.Highlight(fs.Output(), string(source), "", "terminal256", style.Name); err != nil { + fmt.Println(err) + return + } +} + +func lint(r io.Reader) { + ok, err := schema.Lint(r) + if ok { + fmt.Println("✅ Config is valid!") + return + } + fmt.Printf("❌ Config is invalid: %v\n", err) + rerrs, ok := err.(schema.ResultErrors) + if !ok { + fmt.Println(err) + return + } + for _, rerr := range rerrs { + fmt.Printf("In %s: %s\n", rerr.Field(), rerr.Description()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..299aadc --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module go.jolheiser.com/chromajson + +go 1.18 + +require ( + github.com/alecthomas/chroma/v2 v2.0.0-alpha4 + github.com/matryer/is v1.4.0 + github.com/xeipuuv/gojsonschema v1.2.0 +) + +require ( + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b5446cc --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/alecthomas/chroma/v2 v2.0.0-alpha4 h1:6s0y/julsg565meUfJd/aDv5nR4srI3Z3RgyId8w3Ro= +github.com/alecthomas/chroma/v2 v2.0.0-alpha4/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY= +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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/mapping.go b/mapping.go new file mode 100644 index 0000000..69966f6 --- /dev/null +++ b/mapping.go @@ -0,0 +1,127 @@ +package chromajson + +import "github.com/alecthomas/chroma/v2" + +var tokenToType = map[string]chroma.TokenType{ + "background": chroma.Background, + "pre_wrapper": chroma.PreWrapper, + "line": chroma.Line, + "line_numbers": chroma.LineNumbers, + "line_numbers_table": chroma.LineNumbersTable, + "line_highlight": chroma.LineHighlight, + "line_table": chroma.LineTable, + "line_table_td": chroma.LineTableTD, + "code_line": chroma.CodeLine, + "error": chroma.Error, + "other": chroma.Other, + "none": chroma.None, + "eof_type": chroma.EOFType, + "keyword": chroma.Keyword, + "keyword_constant": chroma.KeywordConstant, + "keyword_declaration": chroma.KeywordDeclaration, + "keyword_namespace": chroma.KeywordNamespace, + "keyword_pseudo": chroma.KeywordPseudo, + "keyword_reserved": chroma.KeywordReserved, + "keyword_type": chroma.KeywordType, + "name": chroma.Name, + "name_attribute": chroma.NameAttribute, + "name_builtin": chroma.NameBuiltin, + "name_builtin_pseudo": chroma.NameBuiltinPseudo, + "name_class": chroma.NameClass, + "name_constant": chroma.NameConstant, + "name_decorator": chroma.NameDecorator, + "name_entity": chroma.NameEntity, + "name_exception": chroma.NameException, + "name_function": chroma.NameFunction, + "name_function_magic": chroma.NameFunctionMagic, + "name_keyword": chroma.NameKeyword, + "name_label": chroma.NameLabel, + "name_namespace": chroma.NameNamespace, + "name_operator": chroma.NameOperator, + "name_other": chroma.NameOther, + "name_pseudo": chroma.NamePseudo, + "name_property": chroma.NameProperty, + "name_tag": chroma.NameTag, + "name_variable": chroma.NameVariable, + "name_variable_anonymous": chroma.NameVariableAnonymous, + "name_variable_class": chroma.NameVariableClass, + "name_variable_global": chroma.NameVariableGlobal, + "name_variable_instance": chroma.NameVariableInstance, + "name_variable_magic": chroma.NameVariableMagic, + "literal": chroma.Literal, + "literal_date": chroma.LiteralDate, + "literal_other": chroma.LiteralOther, + "literal_string": chroma.LiteralString, + "literal_string_affix": chroma.LiteralStringAffix, + "literal_string_atom": chroma.LiteralStringAtom, + "literal_string_backtick": chroma.LiteralStringBacktick, + "literal_string_boolean": chroma.LiteralStringBoolean, + "literal_string_char": chroma.LiteralStringChar, + "literal_string_delimiter": chroma.LiteralStringDelimiter, + "literal_string_doc": chroma.LiteralStringDoc, + "literal_string_double": chroma.LiteralStringDouble, + "literal_string_escape": chroma.LiteralStringEscape, + "literal_string_heredoc": chroma.LiteralStringHeredoc, + "literal_string_interpol": chroma.LiteralStringInterpol, + "literal_string_name": chroma.LiteralStringName, + "literal_string_other": chroma.LiteralStringOther, + "literal_string_regex": chroma.LiteralStringRegex, + "literal_string_single": chroma.LiteralStringSingle, + "literal_string_symbol": chroma.LiteralStringSymbol, + "literal_number": chroma.LiteralNumber, + "literal_number_bin": chroma.LiteralNumberBin, + "literal_number_float": chroma.LiteralNumberFloat, + "literal_number_hex": chroma.LiteralNumberHex, + "literal_number_integer": chroma.LiteralNumberInteger, + "literal_number_integer_long": chroma.LiteralNumberIntegerLong, + "literal_number_oct": chroma.LiteralNumberOct, + "operator": chroma.Operator, + "operator_word": chroma.OperatorWord, + "punctuation": chroma.Punctuation, + "comment": chroma.Comment, + "comment_hashbang": chroma.CommentHashbang, + "comment_multiline": chroma.CommentMultiline, + "comment_single": chroma.CommentSingle, + "comment_special": chroma.CommentSpecial, + "comment_preproc": chroma.CommentPreproc, + "comment_preproc_file": chroma.CommentPreprocFile, + "generic": chroma.Generic, + "generic_deleted": chroma.GenericDeleted, + "generic_emph": chroma.GenericEmph, + "generic_error": chroma.GenericError, + "generic_heading": chroma.GenericHeading, + "generic_inserted": chroma.GenericInserted, + "generic_output": chroma.GenericOutput, + "generic_prompt": chroma.GenericPrompt, + "generic_strong": chroma.GenericStrong, + "generic_subheading": chroma.GenericSubheading, + "generic_traceback": chroma.GenericTraceback, + "generic_underline": chroma.GenericUnderline, + "text": chroma.Text, + "text_whitespace": chroma.TextWhitespace, + "text_symbol": chroma.TextSymbol, + "text_punctuation": chroma.TextPunctuation, + "whitespace": chroma.Whitespace, + "date": chroma.Date, + "string": chroma.String, + "string_affix": chroma.StringAffix, + "string_backtick": chroma.StringBacktick, + "string_char": chroma.StringChar, + "string_delimiter": chroma.StringDelimiter, + "string_doc": chroma.StringDoc, + "string_double": chroma.StringDouble, + "string_escape": chroma.StringEscape, + "string_heredoc": chroma.StringHeredoc, + "string_interpol": chroma.StringInterpol, + "string_other": chroma.StringOther, + "string_regex": chroma.StringRegex, + "string_single": chroma.StringSingle, + "string_symbol": chroma.StringSymbol, + "number": chroma.Number, + "number_bin": chroma.NumberBin, + "number_float": chroma.NumberFloat, + "number_hex": chroma.NumberHex, + "number_integer": chroma.NumberInteger, + "number_integer_long": chroma.NumberIntegerLong, + "number_oct": chroma.NumberOct, +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 0000000..8079809 --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,44 @@ +package schema + +import ( + _ "embed" + "fmt" + "io" + "strings" + + "github.com/xeipuuv/gojsonschema" +) + +var ( + //go:embed schema.json + schema []byte + schemaLoader = gojsonschema.NewBytesLoader(schema) +) + +// Lint is for linting a style against the schema +func Lint(r io.Reader) (bool, error) { + sourceLoader, t := gojsonschema.NewReaderLoader(r) + if _, err := io.Copy(io.Discard, t); err != nil { + return false, err + } + result, err := gojsonschema.Validate(schemaLoader, sourceLoader) + if err != nil { + return false, err + } + if len(result.Errors()) > 0 { + return false, ResultErrors(result.Errors()) + } + return result.Valid(), nil +} + +// ResultErrors is a slice of gojsonschema.ResultError that implements error +type ResultErrors []gojsonschema.ResultError + +// Error implements error +func (r ResultErrors) Error() string { + errs := make([]string, 0, len(r)) + for _, re := range r { + errs = append(errs, fmt.Sprintf("%s: %s", re.Field(), re.Description())) + } + return strings.Join(errs, " | ") +} diff --git a/schema/schema.json b/schema/schema.json new file mode 100644 index 0000000..ffe7e90 --- /dev/null +++ b/schema/schema.json @@ -0,0 +1,207 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.jojodev.com/jolheiser/chromajson/schema/schema.json", + "title": "Chroma style configuration", + "description": "A chroma style to be registered", + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "description": "Style name", + "type": "string" + }, + "style": { + "description": "Chroma style", + "type": "object", + "properties": { + "background": {"$ref": "#/$defs/style"}, + "pre_wrapper": {"$ref": "#/$defs/style"}, + "line": {"$ref": "#/$defs/style"}, + "line_numbers": {"$ref": "#/$defs/style"}, + "line_numbers_table": {"$ref": "#/$defs/style"}, + "line_highlight": {"$ref": "#/$defs/style"}, + "line_table": {"$ref": "#/$defs/style"}, + "line_table_td": {"$ref": "#/$defs/style"}, + "code_line": {"$ref": "#/$defs/style"}, + "error": {"$ref": "#/$defs/style"}, + "other": {"$ref": "#/$defs/style"}, + "none": {"$ref": "#/$defs/style"}, + "eof_type": {"$ref": "#/$defs/style"}, + + "keyword": {"$ref": "#/$defs/style"}, + "keyword_constant": {"$ref": "#/$defs/style"}, + "keyword_declaration": {"$ref": "#/$defs/style"}, + "keyword_namespace": {"$ref": "#/$defs/style"}, + "keyword_pseudo": {"$ref": "#/$defs/style"}, + "keyword_reserved": {"$ref": "#/$defs/style"}, + "keyword_type": {"$ref": "#/$defs/style"}, + + "name": {"$ref": "#/$defs/style"}, + "name_attribute": {"$ref": "#/$defs/style"}, + "name_builtin": {"$ref": "#/$defs/style"}, + "name_builtin_pseudo": {"$ref": "#/$defs/style"}, + "name_class": {"$ref": "#/$defs/style"}, + "name_constant": {"$ref": "#/$defs/style"}, + "name_decorator": {"$ref": "#/$defs/style"}, + "name_entity": {"$ref": "#/$defs/style"}, + "name_exception": {"$ref": "#/$defs/style"}, + "name_function": {"$ref": "#/$defs/style"}, + "name_function_magic": {"$ref": "#/$defs/style"}, + "name_keyword": {"$ref": "#/$defs/style"}, + "name_label": {"$ref": "#/$defs/style"}, + "name_namespace": {"$ref": "#/$defs/style"}, + "name_operator": {"$ref": "#/$defs/style"}, + "name_other": {"$ref": "#/$defs/style"}, + "name_pseudo": {"$ref": "#/$defs/style"}, + "name_property": {"$ref": "#/$defs/style"}, + "name_tag": {"$ref": "#/$defs/style"}, + "name_variable": {"$ref": "#/$defs/style"}, + "name_variable_anonymous": {"$ref": "#/$defs/style"}, + "name_variable_class": {"$ref": "#/$defs/style"}, + "name_variable_global": {"$ref": "#/$defs/style"}, + "name_variable_instance": {"$ref": "#/$defs/style"}, + "name_variable_magic": {"$ref": "#/$defs/style"}, + + "literal": {"$ref": "#/$defs/style"}, + "literal_date": {"$ref": "#/$defs/style"}, + "literal_other": {"$ref": "#/$defs/style"}, + + "literal_string": {"$ref": "#/$defs/style"}, + "literal_string_affix": {"$ref": "#/$defs/style"}, + "literal_string_atom": {"$ref": "#/$defs/style"}, + "literal_string_backtick": {"$ref": "#/$defs/style"}, + "literal_string_boolean": {"$ref": "#/$defs/style"}, + "literal_string_char": {"$ref": "#/$defs/style"}, + "literal_string_delimiter": {"$ref": "#/$defs/style"}, + "literal_string_doc": {"$ref": "#/$defs/style"}, + "literal_string_double": {"$ref": "#/$defs/style"}, + "literal_string_escape": {"$ref": "#/$defs/style"}, + "literal_string_heredoc": {"$ref": "#/$defs/style"}, + "literal_string_interpol": {"$ref": "#/$defs/style"}, + "literal_string_name": {"$ref": "#/$defs/style"}, + "literal_string_other": {"$ref": "#/$defs/style"}, + "literal_string_regex": {"$ref": "#/$defs/style"}, + "literal_string_single": {"$ref": "#/$defs/style"}, + "literal_string_symbol": {"$ref": "#/$defs/style"}, + + "literal_number": {"$ref": "#/$defs/style"}, + "literal_number_bin": {"$ref": "#/$defs/style"}, + "literal_number_float": {"$ref": "#/$defs/style"}, + "literal_number_hex": {"$ref": "#/$defs/style"}, + "literal_number_integer": {"$ref": "#/$defs/style"}, + "literal_number_integer_long": {"$ref": "#/$defs/style"}, + "literal_number_oct": {"$ref": "#/$defs/style"}, + + "operator": {"$ref": "#/$defs/style"}, + "operator_word": {"$ref": "#/$defs/style"}, + + "punctuation": {"$ref": "#/$defs/style"}, + + "comment": {"$ref": "#/$defs/style"}, + "comment_hashbang": {"$ref": "#/$defs/style"}, + "comment_multiline": {"$ref": "#/$defs/style"}, + "comment_single": {"$ref": "#/$defs/style"}, + "comment_special": {"$ref": "#/$defs/style"}, + + "comment_preproc": {"$ref": "#/$defs/style"}, + "comment_preproc_file": {"$ref": "#/$defs/style"}, + + "generic": {"$ref": "#/$defs/style"}, + "generic_deleted": {"$ref": "#/$defs/style"}, + "generic_emph": {"$ref": "#/$defs/style"}, + "generic_error": {"$ref": "#/$defs/style"}, + "generic_heading": {"$ref": "#/$defs/style"}, + "generic_inserted": {"$ref": "#/$defs/style"}, + "generic_output": {"$ref": "#/$defs/style"}, + "generic_prompt": {"$ref": "#/$defs/style"}, + "generic_strong": {"$ref": "#/$defs/style"}, + "generic_subheading": {"$ref": "#/$defs/style"}, + "generic_traceback": {"$ref": "#/$defs/style"}, + "generic_underline": {"$ref": "#/$defs/style"}, + + "text": {"$ref": "#/$defs/style"}, + "text_whitespace": {"$ref": "#/$defs/style"}, + "text_symbol": {"$ref": "#/$defs/style"}, + "text_punctuation": {"$ref": "#/$defs/style"}, + + "whitespace": {"$ref": "#/properties/style/properties/text_whitespace"}, + + "date": {"$ref": "#/properties/style/properties/literal_date"}, + + "string": {"$ref": "#/properties/style/properties/literal_string"}, + "string_affix": {"$ref": "#/properties/style/properties/literal_string_affix"}, + "string_backtick": {"$ref": "#/properties/style/properties/literal_string_backtick"}, + "string_char": {"$ref": "#/properties/style/properties/literal_string_char"}, + "string_delimiter": {"$ref": "#/properties/style/properties/literal_string_delimiter"}, + "string_doc": {"$ref": "#/properties/style/properties/literal_string_doc"}, + "string_double": {"$ref": "#/properties/style/properties/literal_string_double"}, + "string_escape": {"$ref": "#/properties/style/properties/literal_string_escape"}, + "string_heredoc": {"$ref": "#/properties/style/properties/literal_string_heredoc"}, + "string_interpol": {"$ref": "#/properties/style/properties/literal_string_interpol"}, + "string_other": {"$ref": "#/properties/style/properties/literal_string_other"}, + "string_regex": {"$ref": "#/properties/style/properties/literal_string_regex"}, + "string_single": {"$ref": "#/properties/style/properties/literal_string_single"}, + "string_symbol": {"$ref": "#/properties/style/properties/literal_string_symbol"}, + + "number": {"$ref": "#/properties/style/properties/literal_number"}, + "number_bin": {"$ref": "#/properties/style/properties/literal_number"}, + "number_float": {"$ref": "#/properties/style/properties/literal_number_bin"}, + "number_hex": {"$ref": "#/properties/style/properties/literal_number_hex"}, + "number_integer": {"$ref": "#/properties/style/properties/literal_number_integer"}, + "number_integer_long": {"$ref": "#/properties/style/properties/literal_number_integer_long"}, + "number_oct": {"$ref": "#/properties/style/properties/literal_number_oct"} + }, + "additionalProperties": false + } + }, + "$defs": { + "style": { + "description": "Token style", + "type": "object", + "additionalProperties": false, + "properties": { + "accent": { + "description": "Font accent(s)", + "type": "array", + "items": { + "type": "string", + "enum": ["italic", "noitalic", "bold", "nobold", "underline", "nounderline", "inherit", "noinherit"] + }, + "uniqueItems": true + }, + "color": { + "description": "Font color", + "oneOf": [ + {"$ref": "#/$defs/hex"}, + {"$ref": "#/$defs/ansi"} + ] + }, + "background": { + "description": "Background color", + "oneOf": [ + {"$ref": "#/$defs/hex"}, + {"$ref": "#/$defs/ansi"} + ] + }, + "border": { + "description": "Border color", + "oneOf": [ + {"$ref": "#/$defs/hex"}, + {"$ref": "#/$defs/ansi"} + ] + } + } + }, + "hex": { + "description": "Hex color", + "type": "string", + "pattern": "^#(?:[A-Fa-f\\d]{3}|[A-Fa-f\\d]{6})$" + }, + "ansi": { + "description": "ANSI color", + "type": "string", + "enum": ["black", "darkred", "darkgreen", "brown", "darkblue", "purple", "teal", "lightgray", "darkgray", "red", "green", "yellow", "blue", "fuchsia", "turquoise", "white"] + } + } +} \ No newline at end of file diff --git a/schema/schema_test.go b/schema/schema_test.go new file mode 100644 index 0000000..3870508 --- /dev/null +++ b/schema/schema_test.go @@ -0,0 +1,43 @@ +package schema + +import ( + "embed" + "fmt" + "testing" + + "github.com/matryer/is" +) + +//go:embed testdata +var testdata embed.FS + +func TestLint(t *testing.T) { + tt := []struct { + Name string + NumErr int + }{ + {Name: "catppuccin"}, + {Name: "minimal"}, + {Name: "bad", NumErr: 8}, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + assert := is.New(t) + + fi, err := testdata.Open(fmt.Sprintf("testdata/%s.json", tc.Name)) + assert.NoErr(err) // Should open file + defer fi.Close() + + ok, err := Lint(fi) + if tc.NumErr > 0 { + assert.True(!ok) // Linter should fail + errs, ok := err.(ResultErrors) + assert.True(ok) // err should be of type ResultErrors + assert.Equal(tc.NumErr, len(errs)) // Number of errors should be consistent + } else { + assert.NoErr(err) // Linter should pass + } + }) + } +} diff --git a/schema/testdata/bad.json b/schema/testdata/bad.json new file mode 100644 index 0000000..b614a9d --- /dev/null +++ b/schema/testdata/bad.json @@ -0,0 +1,18 @@ +{ + "name": "bad", + "style": { + "background": { + "color": "rainbow" + }, + "text": { + "background": "#ZZZ" + }, + "foo": {}, + "number_integer": { + "border": "hello, world" + }, + "comment": { + "accent": ["bold", "bold"] + } + } +} \ No newline at end of file diff --git a/schema/testdata/catppuccin.json b/schema/testdata/catppuccin.json new file mode 100644 index 0000000..11e9e74 --- /dev/null +++ b/schema/testdata/catppuccin.json @@ -0,0 +1,146 @@ +{ + "name": "catppuccin", + "style": { + "text_whitespace": { + "color": "#302D41" + }, + "comment": { + "color": "#6E6C7E", + "accent": ["italic"] + }, + "comment_preproc": { + "color": "#96CDFB" + }, + "keyword": { + "color": "#DDB6F2" + }, + "keyword_pseudo": { + "color": "#DDB6F2", + "accent": ["bold"] + }, + "keyword_type": { + "color": "#F2CDCD" + }, + "keyword_constant": { + "color": "#DDB6F2", + "accent": ["italic"] + }, + "operator": { + "color": "#89DCEB" + }, + "operator_word": { + "color": "#89DCEB", + "accent": ["bold"] + }, + "name": { + "color": "#C9CBFF" + }, + "name_builtin": { + "color": "#D9E0EE", + "accent": ["italic"] + }, + "name_function": { + "color": "#89DCEB" + }, + "name_class": { + "color": "#FAE3B0" + }, + "name_namespace": { + "color": "#FAE3B0" + }, + "name_exception": { + "color": "#E8A2AF" + }, + "name_variable": { + "color": "#F8BD96" + }, + "name_constant": { + "color": "#FAE3B0" + }, + "name_label": { + "color": "#FAE3B0" + }, + "name_entity": { + "color": "#F5C2E7" + }, + "name_attribute": { + "color": "#FAE3B0" + }, + "name_tag": { + "color": "#DDB6F2" + }, + "name_decorator": { + "color": "#F5C2E7" + }, + "name_other": { + "color": "#F8BD96" + }, + "punctuation": { + "color": "#D9E0EE" + }, + "string": { + "color": "#ABE9B3" + }, + "string_doc": { + "color": "#ABE9B3" + }, + "string_interpol": { + "color": "#ABE9B3" + }, + "string_escape": { + "color": "#96CDFB" + }, + "string_regex": { + "color": "#96CDFB" + }, + "string_symbol": { + "color": "#ABE9B3" + }, + "string_other": { + "color": "#ABE9B3" + }, + "number": { + "color": "#B5E8E0" + }, + "generic_heading": { + "color": "#89DCEB", + "accent": ["bold"] + }, + "generic_subheading": { + "color": "#89DCEB", + "accent": ["bold"] + }, + "generic_deleted": { + "color": "#E8A2AF" + }, + "generic_inserted": { + "color": "#ABE9B3" + }, + "generic_error": { + "color": "#E8A2AF" + }, + "generic_emph": { + "accent": ["italic"] + }, + "generic_strong": { + "accent": ["bold"] + }, + "generic_prompt": { + "color": "#988BA2", + "accent": ["bold"] + }, + "generic_output": { + "color": "#F8BD96" + }, + "generic_traceback": { + "color": "#E8A2AF" + }, + "error": { + "color": "#F28FAD" + }, + "background": { + "color": "#F8BD96", + "background": "#1E1E2E" + } + } +} \ No newline at end of file diff --git a/schema/testdata/minimal.json b/schema/testdata/minimal.json new file mode 100644 index 0000000..6569d6b --- /dev/null +++ b/schema/testdata/minimal.json @@ -0,0 +1,3 @@ +{ + "name": "minimal" +} \ No newline at end of file