package formatter import ( "fmt" "io" "sort" "strings" "github.com/marte-dev/marte-dev-tools/internal/parser" ) type Insertable struct { Position parser.Position Text string IsDoc bool } type Formatter struct { insertables []Insertable cursor int writer io.Writer } func Format(config *parser.Configuration, w io.Writer) { ins := []Insertable{} for _, c := range config.Comments { ins = append(ins, Insertable{Position: c.Position, Text: fixComment(c.Text), IsDoc: c.Doc}) } for _, p := range config.Pragmas { ins = append(ins, Insertable{Position: p.Position, Text: fixComment(p.Text)}) } // Sort sort.Slice(ins, func(i, j int) bool { if ins[i].Position.Line != ins[j].Position.Line { return ins[i].Position.Line < ins[j].Position.Line } return ins[i].Position.Column < ins[j].Position.Column }) f := &Formatter{ insertables: ins, writer: w, } f.formatConfig(config) } func fixComment(text string) string { if strings.HasPrefix(text, "//!") { if len(text) > 3 && text[3] != ' ' { return "//! " + text[3:] } } else if strings.HasPrefix(text, "//#") { if len(text) > 3 && text[3] != ' ' { return "//# " + text[3:] } } else if strings.HasPrefix(text, "//") { if len(text) > 2 && text[2] != ' ' && text[2] != '#' && text[2] != '!' { return "// " + text[2:] } } return text } func (f *Formatter) formatConfig(config *parser.Configuration) { lastLine := 0 if config.Package != nil { f.flushCommentsBefore(config.Package.Position, 0, false) // Package comments usually detached unless specifically doc fmt.Fprintf(f.writer, "#package %s", config.Package.URI) lastLine = config.Package.Position.Line if f.hasTrailingComment(lastLine) { fmt.Fprintf(f.writer, " %s", f.popComment()) } fmt.Fprintln(f.writer) fmt.Fprintln(f.writer) } for _, def := range config.Definitions { f.flushCommentsBefore(def.Pos(), 0, true) // Stick to definition lastLine = f.formatDefinition(def, 0) if f.hasTrailingComment(lastLine) { fmt.Fprintf(f.writer, " %s", f.popComment()) } fmt.Fprintln(f.writer) } f.flushRemainingComments(0) } func (f *Formatter) formatDefinition(def parser.Definition, indent int) int { indentStr := strings.Repeat(" ", indent) switch d := def.(type) { case *parser.Field: fmt.Fprintf(f.writer, "%s%s = ", indentStr, d.Name) endLine := f.formatValue(d.Value, indent) return endLine case *parser.ObjectNode: fmt.Fprintf(f.writer, "%s%s = {", indentStr, d.Name) if f.hasTrailingComment(d.Position.Line) { fmt.Fprintf(f.writer, " %s", f.popComment()) } fmt.Fprintln(f.writer) f.formatSubnode(d.Subnode, indent+1) fmt.Fprintf(f.writer, "%s}", indentStr) return d.Subnode.EndPosition.Line } return 0 } func (f *Formatter) formatSubnode(sub parser.Subnode, indent int) { for _, def := range sub.Definitions { f.flushCommentsBefore(def.Pos(), indent, true) // Stick to definition lastLine := f.formatDefinition(def, indent) if f.hasTrailingComment(lastLine) { fmt.Fprintf(f.writer, " %s", f.popComment()) } fmt.Fprintln(f.writer) } f.flushCommentsBefore(sub.EndPosition, indent, false) } func (f *Formatter) formatValue(val parser.Value, indent int) int { switch v := val.(type) { case *parser.StringValue: if v.Quoted { fmt.Fprintf(f.writer, "\"%s\"", v.Value) } else { // Should strictly parse unquoted as ReferenceValue or identifiers, but fallback here fmt.Fprint(f.writer, v.Value) } return v.Position.Line case *parser.IntValue: fmt.Fprint(f.writer, v.Raw) return v.Position.Line case *parser.FloatValue: fmt.Fprint(f.writer, v.Raw) return v.Position.Line case *parser.BoolValue: fmt.Fprintf(f.writer, "%v", v.Value) return v.Position.Line case *parser.ReferenceValue: fmt.Fprint(f.writer, v.Value) return v.Position.Line case *parser.ArrayValue: fmt.Fprint(f.writer, "{ ") for i, e := range v.Elements { if i > 0 { fmt.Fprint(f.writer, " ") } f.formatValue(e, indent) } fmt.Fprint(f.writer, " }") if v.EndPosition.Line > 0 { return v.EndPosition.Line } // Fallback if EndPosition not set (shouldn't happen with new parser) if len(v.Elements) > 0 { return v.Elements[len(v.Elements)-1].Pos().Line } return v.Position.Line default: return 0 } } func (f *Formatter) flushCommentsBefore(pos parser.Position, indent int, stick bool) { indentStr := strings.Repeat(" ", indent) for f.cursor < len(f.insertables) { c := f.insertables[f.cursor] if c.Position.Line < pos.Line || (c.Position.Line == pos.Line && c.Position.Column < pos.Column) { fmt.Fprintf(f.writer, "%s%s\n", indentStr, c.Text) f.cursor++ } else { break } } // If stick is true, we don't print extra newline. // The caller will print the definition immediately after this function returns. // If stick is false (e.g. end of block comments), we act normally. // But actually, the previous implementation didn't print extra newlines between comments and code // explicitly, it relied on the loop in formatConfig/formatSubnode to print newline AFTER definition. // So comments naturally sat on top. // The issue is if there WAS a blank line in source, we ignore it and squash. This implements "stick". } func (f *Formatter) flushRemainingComments(indent int) { indentStr := strings.Repeat(" ", indent) for f.cursor < len(f.insertables) { c := f.insertables[f.cursor] fmt.Fprintf(f.writer, "%s%s\n", indentStr, c.Text) f.cursor++ } } func (f *Formatter) hasTrailingComment(line int) bool { if f.cursor >= len(f.insertables) { return false } c := f.insertables[f.cursor] return c.Position.Line == line } func (f *Formatter) popComment() string { if f.cursor >= len(f.insertables) { return "" } c := f.insertables[f.cursor] f.cursor++ return c.Text }