git » css3fmt » master » tree

[master] / css3fmt.go

// css3fmt is an auto-indenter/formatter for CSS.
//
// See https://blitiri.com.ar/git/r/css3fmt for more details.
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"strings"

	"github.com/gorilla/css/scanner"
)

var (
	rewrite = flag.Bool("w", false,
		"Do not print reformatted sources to standard output."+
			" If a file's formatting is different from gofmt's,"+
			" overwrite it with the formatted version")
)

func main() {
	flag.Parse()

	if args := flag.Args(); len(args) > 0 {
		for _, fname := range args {
			f, err := os.Open(fname)
			if err != nil {
				fatalf("%s: %s\n", fname, err)
			}
			defer f.Close()

			s := indent(f)
			if *rewrite {
				err = ioutil.WriteFile(fname, []byte(s), 0660)
				if err != nil {
					fatalf("%s: %s\n", fname, err)
				}
			} else {
				os.Stdout.WriteString(s)
			}
		}
	} else {
		os.Stdout.WriteString(indent(os.Stdin))
	}
}

func fatalf(s string, a ...interface{}) {
	fmt.Fprintf(os.Stderr, s, a...)
	os.Exit(1)
}

func indent(f *os.File) string {
	buf, err := ioutil.ReadAll(f)
	if err != nil {
		fatalf("%s: error reading: %s\n", f.Name(), err)
	}

	out := output{}

	s := scanner.New(string(buf))
scan:
	for {
		t := s.Next()
		switch t.Type {
		case scanner.TokenEOF:
			break scan
		case scanner.TokenError:
			fatalf("%s:%d:%d: error tokenizing: %s\n",
				f.Name(), t.Line, t.Column, t.Value)
		case scanner.TokenChar:
			switch t.Value {
			case "{":
				out.emit("{\n")
				out.indent++
			case "}":
				out.indent--
				out.emit("}\n")
				if out.indent == 0 {
					out.emit("\n")
				}
			case ";":
				if out.inFunc {
					out.inFunc = false
					out.indent--
				}
				out.emit(";\n")
			default:
				out.emit(t.Value)
			}
		case scanner.TokenS:
			if !strings.Contains(t.Value, "\n") {
				out.emit(" ")
			} else if out.inFunc {
				// Respect newline within functions, as users know best how to
				// break up arguments.
				out.emit("\n")
			}
			out.afterEmptyLine = strings.Contains(t.Value, "\n\n")
		case scanner.TokenComment:
			out.emitComment(t.Value)
		case scanner.TokenFunction:
			out.emit(t.Value)
			if !out.inFunc {
				out.inFunc = true
				out.indent++
			}
		default:
			out.emit(t.Value)
		}

		//fmt.Printf("\n«%s»\n", t)
	}

	return strings.Trim(out.buf.String(), "\n") + "\n"
}

type output struct {
	indent int

	afterN         bool
	afterEmptyLine bool
	inFunc         bool

	buf strings.Builder
}

func (o *output) emit(s string) {
	// Indent if we just came from a newline, UNLESS we're only printing a
	// newline, to avoid trailing spaces.
	if o.afterN && s != "\n" {
		for i := 0; i < o.indent; i++ {
			//o.buf.WriteString("‧‧‧‧")
			o.buf.WriteString("    ")
		}
	}

	o.buf.WriteString(s)
	o.afterN = strings.HasSuffix(s, "\n")
	o.afterEmptyLine = false
}

func (o *output) emitComment(s string) {
	// We preserve empty newlines before comments, so they can be used to
	// break long series of entries, or to group sections.
	if o.afterEmptyLine {
		o.emit("\n")
	}

	// Emit the lines adjusting indentation for "* ".
	lines := strings.Split(s, "\n")
	for _, l := range lines {
		if strings.HasPrefix(trimAllSp(l), "* ") {
			l = " " + trimAllSp(l)
		}
		o.emit(l + "\n")
	}
}

func trimAllSp(s string) string {
	return strings.Trim(s, " \t\r\n")
}