gitea/vendor/gopkg.in/editorconfig/editorconfig-core-go.v1/editorconfig.go

289 lines
7.2 KiB
Go

// Package editorconfig can be used to parse and generate editorconfig files.
// For more information about editorconfig, see http://editorconfig.org/
package editorconfig
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"gopkg.in/ini.v1"
)
const (
ConfigNameDefault = ".editorconfig"
)
// 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"
)
// Definition represents a definition inside the .editorconfig file.
// E.g. a section of the file.
// The definition is composed of the selector ("*", "*.go", "*.{js.css}", etc),
// plus the properties of the selected files.
type Definition struct {
Selector string `ini:"-" json:"-"`
Charset string `ini:"charset" json:"charset,omitempty"`
IndentStyle string `ini:"indent_style" json:"indent_style,omitempty"`
IndentSize string `ini:"indent_size" json:"indent_size,omitempty"`
TabWidth int `ini:"tab_width" json:"tab_width,omitempty"`
EndOfLine string `ini:"end_of_line" json:"end_of_line,omitempty"`
TrimTrailingWhitespace bool `ini:"trim_trailing_whitespace" json:"trim_trailing_whitespace,omitempty"`
InsertFinalNewline bool `ini:"insert_final_newline" json:"insert_final_newline,omitempty"`
Raw map[string]string `ini:"-" json:"-"`
}
// 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
}
// ParseBytes parses from a slice of bytes.
func ParseBytes(data []byte) (*Editorconfig, error) {
iniFile, err := ini.Load(data)
if err != nil {
return nil, err
}
editorConfig := &Editorconfig{}
editorConfig.Root = iniFile.Section(ini.DEFAULT_SECTION).Key("root").MustBool(false)
for _, sectionStr := range iniFile.SectionStrings() {
if sectionStr == ini.DEFAULT_SECTION {
continue
}
var (
iniSection = iniFile.Section(sectionStr)
definition = &Definition{}
raw = make(map[string]string)
)
err := iniSection.MapTo(&definition)
if err != nil {
return nil, err
}
// tab_width defaults to indent_size:
// https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#tab_width
if definition.TabWidth <= 0 {
if num, err := strconv.Atoi(definition.IndentSize); err == nil {
definition.TabWidth = num
}
}
// Shallow copy all properties
for k, v := range iniSection.KeysHash() {
raw[k] = v
}
definition.Selector = sectionStr
definition.Raw = raw
editorConfig.Definitions = append(editorConfig.Definitions, definition)
}
return editorConfig, nil
}
// ParseFile parses from a file.
func ParseFile(f string) (*Editorconfig, error) {
data, err := ioutil.ReadFile(f)
if err != nil {
return nil, err
}
return ParseBytes(data)
}
var (
regexpBraces = regexp.MustCompile("{.*}")
)
func filenameMatches(pattern, name string) bool {
// basic match
matched, _ := filepath.Match(pattern, name)
if matched {
return true
}
// foo/bar/main.go should match main.go
matched, _ = filepath.Match(pattern, filepath.Base(name))
if matched {
return true
}
// foo should match foo/main.go
matched, _ = filepath.Match(filepath.Join(pattern, "*"), name)
if matched {
return true
}
// *.{js,go} should match main.go
if str := regexpBraces.FindString(pattern); len(str) > 0 {
// remote initial "{" and final "}"
str = strings.TrimPrefix(str, "{")
str = strings.TrimSuffix(str, "}")
// testing for empty brackets: "{}"
if len(str) == 0 {
patt := regexpBraces.ReplaceAllString(pattern, "*")
matched, _ = filepath.Match(patt, filepath.Base(name))
return matched
}
for _, patt := range strings.Split(str, ",") {
patt = regexpBraces.ReplaceAllString(pattern, patt)
matched, _ = filepath.Match(patt, filepath.Base(name))
if matched {
return true
}
}
}
return false
}
func (d *Definition) merge(md *Definition) {
if len(d.Charset) == 0 {
d.Charset = md.Charset
}
if len(d.IndentStyle) == 0 {
d.IndentStyle = md.IndentStyle
}
if len(d.IndentSize) == 0 {
d.IndentSize = md.IndentSize
}
if d.TabWidth <= 0 {
d.TabWidth = md.TabWidth
}
if len(d.EndOfLine) == 0 {
d.EndOfLine = md.EndOfLine
}
if !d.TrimTrailingWhitespace {
d.TrimTrailingWhitespace = md.TrimTrailingWhitespace
}
if !d.InsertFinalNewline {
d.InsertFinalNewline = md.InsertFinalNewline
}
for k, v := range md.Raw {
if _, ok := d.Raw[k]; !ok {
d.Raw[k] = v
}
}
}
func (d *Definition) InsertToIniFile(iniFile *ini.File) {
iniSec := iniFile.Section(d.Selector)
for k, v := range d.Raw {
iniSec.Key(k).SetValue(v)
}
}
// 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 {
def := &Definition{}
def.Raw = make(map[string]string)
for i := len(e.Definitions) - 1; i >= 0; i-- {
actualDef := e.Definitions[i]
if filenameMatches(actualDef.Selector, name) {
def.merge(actualDef)
}
}
return def
}
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
}
// 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) {
var (
iniFile = ini.Empty()
buffer = bytes.NewBuffer(nil)
)
iniFile.Section(ini.DEFAULT_SECTION).Comment = "http://editorconfig.org"
if e.Root {
iniFile.Section(ini.DEFAULT_SECTION).Key("root").SetValue(boolToString(e.Root))
}
for _, d := range e.Definitions {
d.InsertToIniFile(iniFile)
}
_, err := iniFile.WriteTo(buffer)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// Save saves the Editorconfig to a compatible INI file.
func (e *Editorconfig) Save(filename string) error {
data, err := e.Serialize()
if err != nil {
return err
}
return ioutil.WriteFile(filename, data, 0666)
}
// 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) {
return GetDefinitionForFilenameWithConfigname(filename, ConfigNameDefault)
}
func GetDefinitionForFilenameWithConfigname(filename string, configname string) (*Definition, error) {
abs, err := filepath.Abs(filename)
if err != nil {
return nil, err
}
definition := &Definition{}
definition.Raw = make(map[string]string)
dir := abs
for dir != filepath.Dir(dir) {
dir = filepath.Dir(dir)
ecFile := filepath.Join(dir, configname)
if _, err := os.Stat(ecFile); os.IsNotExist(err) {
continue
}
ec, err := ParseFile(ecFile)
if err != nil {
return nil, err
}
definition.merge(ec.GetDefinitionForFilename(filename))
if ec.Root {
break
}
}
return definition, nil
}