๐Ÿš€ Grammar-Aware Code Formatter: Structure through separation (supports Go, JavaScript, TypeScript, JSX, and TSX)
go formatter code-formatter javascript typescript jsx tsx
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Support JSON configuration file

Fuwn f37a1ee1 b15244a3

+118 -32
+46 -5
README.md
··· 1 1 # ๐Ÿš€ Iku 2 2 3 - > Grammar-Aware Go Formatter: Structure through separation 3 + > Grammar-Aware Code Formatter: Structure through separation 4 4 5 5 6 6 Let your code breathe! 7 7 8 - Iku is a grammar-based Go formatter that enforces consistent blank-line placement by statement and declaration type. 8 + Iku is a grammar-based formatter that enforces consistent blank-line placement by statement and declaration type. It supports Go, JavaScript, TypeScript, JSX, and TSX. 9 9 10 10 ## Philosophy 11 11 ··· 20 20 21 21 ## How It Works 22 22 23 - Iku applies standard Go formatting (via [go/format](https://pkg.go.dev/go/format)) first ([formatter.go#29](https://github.com/Fuwn/iku/blob/main/formatter.go#L29)), then adds its grammar-based blank-line rules on top. Your code gets `go fmt` output plus structural separation. 23 + For Go files, Iku applies standard Go formatting (via [go/format](https://pkg.go.dev/go/format)) first, then adds its grammar-based blank-line rules on top. Your code gets `go fmt` output plus structural separation. 24 + 25 + For JavaScript and TypeScript files (`.js`, `.ts`, `.jsx`, `.tsx`), Iku uses a heuristic line-based analyser that classifies statements by keyword (`function`, `class`, `if`, `for`, `try`, etc.) and applies the same blank-line rules. 24 26 25 27 ## Installation 26 28 ··· 42 44 43 45 # Format and print to stdout 44 46 iku file.go 47 + iku component.tsx 45 48 46 49 # Format in-place 47 50 iku -w file.go 51 + iku -w src/ 48 52 49 - # Format entire directory 53 + # Format entire directory (Go, JS, TS, JSX, TSX) 50 54 iku -w . 51 55 52 56 # List files that need formatting ··· 63 67 | `-w` | Write result to file instead of stdout | 64 68 | `-l` | List files whose formatting differs | 65 69 | `-d` | Display diffs instead of rewriting | 66 - | `--comments` | Comment attachment mode: `follow`, `precede`, `standalone` | 67 70 | `--version` | Print version | 71 + 72 + ## Configuration 73 + 74 + Iku looks for `.iku.json` or `iku.json` in the current working directory. 75 + 76 + ```json 77 + { 78 + "comment_mode": "follow", 79 + "group_single_line_functions": false 80 + } 81 + ``` 82 + 83 + All fields are optional. Omitted fields use their defaults. 84 + 85 + ### `comment_mode` 86 + 87 + Controls how comments interact with blank-line insertion. Default: `"follow"`. 88 + 89 + | Mode | Behaviour | 90 + |------|-----------| 91 + | `follow` | Comments attach to the **next** statement. The blank line goes **before** the comment. | 92 + | `precede` | Comments attach to the **previous** statement. The blank line goes **after** the comment. | 93 + | `standalone` | Comments are independent. Blank lines are placed strictly by statement rules. | 94 + 95 + ### `group_single_line_functions` 96 + 97 + When `true`, consecutive single-line function declarations of the same type are kept together without blank lines. Default: `false`. 98 + 99 + ```go 100 + // group_single_line_functions = true 101 + func Base() string { return baseDirectory } 102 + func Config() string { return configFile } 103 + 104 + // group_single_line_functions = false (default) 105 + func Base() string { return baseDirectory } 106 + 107 + func Config() string { return configFile } 108 + ``` 68 109 69 110 ## Examples 70 111
+44
configuration.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "os" 7 + "strings" 8 + ) 9 + 10 + type Configuration struct { 11 + GroupSingleLineFunctions bool `json:"group_single_line_functions"` 12 + CommentMode string `json:"comment_mode"` 13 + } 14 + 15 + func (configuration Configuration) commentMode() (CommentMode, error) { 16 + switch strings.ToLower(configuration.CommentMode) { 17 + case "", "follow": 18 + return CommentsFollow, nil 19 + case "precede": 20 + return CommentsPrecede, nil 21 + case "standalone": 22 + return CommentsStandalone, nil 23 + default: 24 + return 0, fmt.Errorf("invalid comment_mode: %q (use follow, precede, or standalone)", configuration.CommentMode) 25 + } 26 + } 27 + 28 + func loadConfiguration() Configuration { 29 + var configuration Configuration 30 + 31 + for _, fileName := range []string{".iku.json", "iku.json"} { 32 + fileData, readError := os.ReadFile(fileName) 33 + 34 + if readError != nil { 35 + continue 36 + } 37 + 38 + _ = json.Unmarshal(fileData, &configuration) 39 + 40 + break 41 + } 42 + 43 + return configuration 44 + }
+9 -2
engine/engine.go
··· 11 11 ) 12 12 13 13 type Engine struct { 14 - CommentMode CommentMode 14 + CommentMode CommentMode 15 + GroupSingleLineScopes bool 15 16 } 16 17 17 18 func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) { ··· 21 22 previousWasComment := false 22 23 previousWasTopLevel := false 23 24 previousWasScoped := false 25 + previousWasSingleLineScope := false 24 26 25 27 for eventIndex, event := range events { 26 28 if event.InRawString { ··· 48 50 needsBlankLine := false 49 51 currentIsTopLevel := event.HasASTInfo && event.IsTopLevel 50 52 currentIsScoped := event.HasASTInfo && event.IsScoped 53 + currentIsSingleLineScope := currentIsScoped && !event.IsOpeningBrace && !event.IsClosingBrace 51 54 52 55 if hasWrittenContent && !previousWasOpenBrace && !event.IsClosingBrace && !event.IsCaseLabel && !event.IsContinuation { 53 56 if currentIsTopLevel && previousWasTopLevel && currentStatementType != previousStatementType { ··· 55 58 needsBlankLine = true 56 59 } 57 60 } else if event.HasASTInfo && (currentIsScoped || previousWasScoped) { 58 - if !(e.CommentMode == CommentsFollow && previousWasComment) { 61 + if e.GroupSingleLineScopes && currentIsSingleLineScope && previousWasSingleLineScope && currentStatementType == previousStatementType { 62 + needsBlankLine = false 63 + } else if !(e.CommentMode == CommentsFollow && previousWasComment) { 59 64 needsBlankLine = true 60 65 } 61 66 } else if currentStatementType != "" && previousStatementType != "" && currentStatementType != previousStatementType { ··· 104 109 previousStatementType = event.StatementType 105 110 previousWasTopLevel = event.IsTopLevel 106 111 previousWasScoped = event.IsScoped 112 + previousWasSingleLineScope = currentIsSingleLineScope 107 113 } else if currentStatementType != "" { 108 114 previousStatementType = currentStatementType 109 115 previousWasTopLevel = false 110 116 previousWasScoped = false 117 + previousWasSingleLineScope = false 111 118 } 112 119 } 113 120
+6 -2
formatter.go
··· 14 14 ) 15 15 16 16 type Formatter struct { 17 - CommentMode CommentMode 17 + CommentMode CommentMode 18 + Configuration Configuration 18 19 } 19 20 20 21 type lineInformation struct { ··· 31 32 return nil, err 32 33 } 33 34 34 - formattingEngine := &engine.Engine{CommentMode: MapCommentMode(f.CommentMode)} 35 + formattingEngine := &engine.Engine{ 36 + CommentMode: MapCommentMode(f.CommentMode), 37 + GroupSingleLineScopes: f.Configuration.GroupSingleLineFunctions, 38 + } 35 39 36 40 return formattingEngine.FormatToBytes(events), nil 37 41 }
+4 -1
inspect.go
··· 45 45 } 46 46 47 47 lineInformationMap[startLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: true} 48 - lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: false} 48 + 49 + if endLine != startLine { 50 + lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: false} 51 + } 49 52 } 50 53 51 54 ast.Inspect(parsedFile, func(astNode ast.Node) bool {
+9 -22
main.go
··· 15 15 16 16 var version = "dev" 17 17 var ( 18 - writeFlag = flag.Bool("w", false, "write result to (source) file instead of stdout") 19 - listFlag = flag.Bool("l", false, "list files whose formatting differs from iku's") 20 - diffFlag = flag.Bool("d", false, "display diffs instead of rewriting files") 21 - commentsFlag = flag.String("comments", "follow", "comment attachment mode: follow, precede, standalone") 22 - versionFlag = flag.Bool("version", false, "print version") 18 + writeFlag = flag.Bool("w", false, "write result to (source) file instead of stdout") 19 + listFlag = flag.Bool("l", false, "list files whose formatting differs from iku's") 20 + diffFlag = flag.Bool("d", false, "display diffs instead of rewriting files") 21 + versionFlag = flag.Bool("version", false, "print version") 23 22 ) 24 23 25 24 func main() { ··· 35 34 os.Exit(0) 36 35 } 37 36 38 - commentMode, err := parseCommentMode(*commentsFlag) 37 + configuration := loadConfiguration() 38 + commentMode, validationError := configuration.commentMode() 39 39 40 - if err != nil { 41 - fmt.Fprintf(os.Stderr, "iku: %v\n", err) 40 + if validationError != nil { 41 + fmt.Fprintf(os.Stderr, "iku: %v\n", validationError) 42 42 os.Exit(2) 43 43 } 44 44 45 - formatter := &Formatter{CommentMode: commentMode} 45 + formatter := &Formatter{CommentMode: commentMode, Configuration: configuration} 46 46 47 47 if flag.NArg() == 0 { 48 48 if *writeFlag { ··· 82 82 } 83 83 84 84 os.Exit(exitCode) 85 - } 86 - 87 - func parseCommentMode(commentModeString string) (CommentMode, error) { 88 - switch strings.ToLower(commentModeString) { 89 - case "follow": 90 - return CommentsFollow, nil 91 - case "precede": 92 - return CommentsPrecede, nil 93 - case "standalone": 94 - return CommentsStandalone, nil 95 - default: 96 - return 0, fmt.Errorf("invalid comment mode: %q (use follow, precede, or standalone)", commentModeString) 97 - } 98 85 } 99 86 100 87 var supportedFileExtensions = map[string]bool{