package editorconfig import ( "bytes" "io" "os" "runtime" "strings" "gopkg.in/ini.v1" ) const ( // ConfigNameDefault represents the name of the configuration file ConfigNameDefault = ".editorconfig" // UnsetValue is the value that unsets a preexisting variable UnsetValue = "unset" ) // IndentStyle possible values const ( IndentStyleTab = "tab" IndentStyleSpaces = "space" ) // EndOfLine possible values const ( EndOfLineLf = "lf" EndOfLineCr = "cr" EndOfLineCrLf = "crlf" ) // Charset possible values const ( CharsetLatin1 = "latin1" CharsetUTF8 = "utf-8" CharsetUTF16BE = "utf-16be" CharsetUTF16LE = "utf-16le" CharsetUTF8BOM = "utf-8 bom" ) // Limits for section name, properties, and values. const ( MaxPropertyLength = 50 MaxSectionLength = 4096 MaxValueLength = 255 ) // Editorconfig represents a .editorconfig file. // // It is composed by a "root" property, plus the definitions defined in the // file. type Editorconfig struct { Root bool Definitions []*Definition config *Config } // newEditorconfig builds the configuration from an INI file. func newEditorconfig(iniFile *ini.File) (*Editorconfig, error) { editorConfig := &Editorconfig{} // Consider mixed-case values for true and false. rootKey := iniFile.Section(ini.DefaultSection).Key("root") rootKey.SetValue(strings.ToLower(rootKey.Value())) editorConfig.Root = rootKey.MustBool(false) for _, sectionStr := range iniFile.SectionStrings() { if sectionStr == ini.DefaultSection || len(sectionStr) > MaxSectionLength { continue } iniSection := iniFile.Section(sectionStr) definition := &Definition{} raw := make(map[string]string) if err := iniSection.MapTo(&definition); err != nil { return nil, err } // Shallow copy all the properties for k, v := range iniSection.KeysHash() { if len(k) > MaxPropertyLength || len(v) > MaxValueLength { continue } raw[strings.ToLower(k)] = v } definition.Raw = raw definition.Selector = sectionStr if err := definition.normalize(); err != nil { return nil, err } editorConfig.Definitions = append(editorConfig.Definitions, definition) } return editorConfig, nil } // GetDefinitionForFilename returns a definition for the given filename. // // The result is a merge of the selectors that matched the file. // The last section has preference over the priors. func (e *Editorconfig) GetDefinitionForFilename(name string) (*Definition, error) { def := &Definition{ Raw: make(map[string]string), } // The last section has preference over the priors. for i := len(e.Definitions) - 1; i >= 0; i-- { actualDef := e.Definitions[i] selector := actualDef.Selector if !strings.HasPrefix(selector, "/") { if strings.ContainsRune(selector, '/') { selector = "/" + selector } else { selector = "/**/" + selector } } if !strings.HasPrefix(name, "/") { if runtime.GOOS != "windows" { name = "/" + name } else { name = "\\" + name } } ok, err := e.FnmatchCase(selector, name) if err != nil { return nil, err } if ok { def.merge(actualDef) } } return def, nil } // FnmatchCase calls the matcher from the config's parser or the vanilla's. func (e *Editorconfig) FnmatchCase(selector string, filename string) (bool, error) { if e.config != nil && e.config.Parser != nil { return e.config.Parser.FnmatchCase(selector, filename) } return FnmatchCase(selector, filename) } // Serialize converts the Editorconfig to a slice of bytes, containing the // content of the file in the INI format. func (e *Editorconfig) Serialize() ([]byte, error) { buffer := bytes.NewBuffer(nil) err := e.Write(buffer) if err != nil { return nil, err } return buffer.Bytes(), nil } // Write writes the Editorconfig to the Writer in a compatible INI file. func (e *Editorconfig) Write(w io.Writer) error { var ( iniFile = ini.Empty() ) iniFile.Section(ini.DefaultSection).Comment = "https://editorconfig.org" if e.Root { iniFile.Section(ini.DefaultSection).Key("root").SetValue(boolToString(e.Root)) } for _, d := range e.Definitions { d.InsertToIniFile(iniFile) } _, err := iniFile.WriteTo(w) return err } // Save saves the Editorconfig to a compatible INI file. func (e *Editorconfig) Save(filename string) error { f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return err } return e.Write(f) } func boolToString(b bool) string { if b { return "true" } return "false" } // Parse parses from a reader. func Parse(r io.Reader) (*Editorconfig, error) { iniFile, err := ini.Load(r) if err != nil { return nil, err } return newEditorconfig(iniFile) } // ParseBytes parses from a slice of bytes. // // Deprecated: use Parse instead. func ParseBytes(data []byte) (*Editorconfig, error) { iniFile, err := ini.Load(data) if err != nil { return nil, err } return newEditorconfig(iniFile) } // ParseFile parses from a file. // // Deprecated: use Parse instead. func ParseFile(path string) (*Editorconfig, error) { iniFile, err := ini.Load(path) if err != nil { return nil, err } return newEditorconfig(iniFile) } // GetDefinitionForFilename given a filename, searches // for .editorconfig files, starting from the file folder, // walking through the previous folders, until it reaches a // folder with `root = true`, and returns the right editorconfig // definition for the given file. func GetDefinitionForFilename(filename string) (*Definition, error) { config := new(Config) return config.Load(filename) } // GetDefinitionForFilenameWithConfigname given a filename and a configname, // searches for configname files, starting from the file folder, // walking through the previous folders, until it reaches a // folder with `root = true`, and returns the right editorconfig // definition for the given file. func GetDefinitionForFilenameWithConfigname(filename string, configname string) (*Definition, error) { config := &Config{ Name: configname, } return config.Load(filename) }