aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-11 11:12:10 +0000
committerFuwn <[email protected]>2026-02-11 11:12:10 +0000
commitf37a1ee1e2577a0e8fd76d4c965abb93cd99a6cf (patch)
tree4923021c7a049a9b2badb9dab9bd0e71c1551e7a
parentfix(adapter): Suppress blank lines before continuation keywords (else/catch/f... (diff)
downloadiku-f37a1ee1e2577a0e8fd76d4c965abb93cd99a6cf.tar.xz
iku-f37a1ee1e2577a0e8fd76d4c965abb93cd99a6cf.zip
feat: Support JSON configuration file
-rw-r--r--README.md51
-rw-r--r--configuration.go44
-rw-r--r--engine/engine.go11
-rw-r--r--formatter.go8
-rw-r--r--inspect.go5
-rw-r--r--main.go31
6 files changed, 118 insertions, 32 deletions
diff --git a/README.md b/README.md
index fbdfafd..881add4 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
# 🚀 Iku
-> Grammar-Aware Go Formatter: Structure through separation
+> Grammar-Aware Code Formatter: Structure through separation
Let your code breathe!
-Iku is a grammar-based Go formatter that enforces consistent blank-line placement by statement and declaration type.
+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.
## Philosophy
@@ -20,7 +20,9 @@ Code structure should be visually apparent from its formatting. Iku groups state
## How It Works
-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.
+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.
+
+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.
## Installation
@@ -42,11 +44,13 @@ echo 'package main ...' | iku
# Format and print to stdout
iku file.go
+iku component.tsx
# Format in-place
iku -w file.go
+iku -w src/
-# Format entire directory
+# Format entire directory (Go, JS, TS, JSX, TSX)
iku -w .
# List files that need formatting
@@ -63,9 +67,46 @@ iku -d file.go
| `-w` | Write result to file instead of stdout |
| `-l` | List files whose formatting differs |
| `-d` | Display diffs instead of rewriting |
-| `--comments` | Comment attachment mode: `follow`, `precede`, `standalone` |
| `--version` | Print version |
+## Configuration
+
+Iku looks for `.iku.json` or `iku.json` in the current working directory.
+
+```json
+{
+ "comment_mode": "follow",
+ "group_single_line_functions": false
+}
+```
+
+All fields are optional. Omitted fields use their defaults.
+
+### `comment_mode`
+
+Controls how comments interact with blank-line insertion. Default: `"follow"`.
+
+| Mode | Behaviour |
+|------|-----------|
+| `follow` | Comments attach to the **next** statement. The blank line goes **before** the comment. |
+| `precede` | Comments attach to the **previous** statement. The blank line goes **after** the comment. |
+| `standalone` | Comments are independent. Blank lines are placed strictly by statement rules. |
+
+### `group_single_line_functions`
+
+When `true`, consecutive single-line function declarations of the same type are kept together without blank lines. Default: `false`.
+
+```go
+// group_single_line_functions = true
+func Base() string { return baseDirectory }
+func Config() string { return configFile }
+
+// group_single_line_functions = false (default)
+func Base() string { return baseDirectory }
+
+func Config() string { return configFile }
+```
+
## Examples
### Before
diff --git a/configuration.go b/configuration.go
new file mode 100644
index 0000000..b00d778
--- /dev/null
+++ b/configuration.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+)
+
+type Configuration struct {
+ GroupSingleLineFunctions bool `json:"group_single_line_functions"`
+ CommentMode string `json:"comment_mode"`
+}
+
+func (configuration Configuration) commentMode() (CommentMode, error) {
+ switch strings.ToLower(configuration.CommentMode) {
+ case "", "follow":
+ return CommentsFollow, nil
+ case "precede":
+ return CommentsPrecede, nil
+ case "standalone":
+ return CommentsStandalone, nil
+ default:
+ return 0, fmt.Errorf("invalid comment_mode: %q (use follow, precede, or standalone)", configuration.CommentMode)
+ }
+}
+
+func loadConfiguration() Configuration {
+ var configuration Configuration
+
+ for _, fileName := range []string{".iku.json", "iku.json"} {
+ fileData, readError := os.ReadFile(fileName)
+
+ if readError != nil {
+ continue
+ }
+
+ _ = json.Unmarshal(fileData, &configuration)
+
+ break
+ }
+
+ return configuration
+}
diff --git a/engine/engine.go b/engine/engine.go
index 0b37dd4..6f7b7a0 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -11,7 +11,8 @@ const (
)
type Engine struct {
- CommentMode CommentMode
+ CommentMode CommentMode
+ GroupSingleLineScopes bool
}
func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
@@ -21,6 +22,7 @@ func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
previousWasComment := false
previousWasTopLevel := false
previousWasScoped := false
+ previousWasSingleLineScope := false
for eventIndex, event := range events {
if event.InRawString {
@@ -48,6 +50,7 @@ func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
needsBlankLine := false
currentIsTopLevel := event.HasASTInfo && event.IsTopLevel
currentIsScoped := event.HasASTInfo && event.IsScoped
+ currentIsSingleLineScope := currentIsScoped && !event.IsOpeningBrace && !event.IsClosingBrace
if hasWrittenContent && !previousWasOpenBrace && !event.IsClosingBrace && !event.IsCaseLabel && !event.IsContinuation {
if currentIsTopLevel && previousWasTopLevel && currentStatementType != previousStatementType {
@@ -55,7 +58,9 @@ func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
needsBlankLine = true
}
} else if event.HasASTInfo && (currentIsScoped || previousWasScoped) {
- if !(e.CommentMode == CommentsFollow && previousWasComment) {
+ if e.GroupSingleLineScopes && currentIsSingleLineScope && previousWasSingleLineScope && currentStatementType == previousStatementType {
+ needsBlankLine = false
+ } else if !(e.CommentMode == CommentsFollow && previousWasComment) {
needsBlankLine = true
}
} else if currentStatementType != "" && previousStatementType != "" && currentStatementType != previousStatementType {
@@ -104,10 +109,12 @@ func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
previousStatementType = event.StatementType
previousWasTopLevel = event.IsTopLevel
previousWasScoped = event.IsScoped
+ previousWasSingleLineScope = currentIsSingleLineScope
} else if currentStatementType != "" {
previousStatementType = currentStatementType
previousWasTopLevel = false
previousWasScoped = false
+ previousWasSingleLineScope = false
}
}
diff --git a/formatter.go b/formatter.go
index 0256694..a82d619 100644
--- a/formatter.go
+++ b/formatter.go
@@ -14,7 +14,8 @@ const (
)
type Formatter struct {
- CommentMode CommentMode
+ CommentMode CommentMode
+ Configuration Configuration
}
type lineInformation struct {
@@ -31,7 +32,10 @@ func (f *Formatter) Format(source []byte, filename string) ([]byte, error) {
return nil, err
}
- formattingEngine := &engine.Engine{CommentMode: MapCommentMode(f.CommentMode)}
+ formattingEngine := &engine.Engine{
+ CommentMode: MapCommentMode(f.CommentMode),
+ GroupSingleLineScopes: f.Configuration.GroupSingleLineFunctions,
+ }
return formattingEngine.FormatToBytes(events), nil
}
diff --git a/inspect.go b/inspect.go
index 0c05657..8452de5 100644
--- a/inspect.go
+++ b/inspect.go
@@ -45,7 +45,10 @@ func (f *Formatter) buildLineInfo(tokenFileSet *token.FileSet, parsedFile *ast.F
}
lineInformationMap[startLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: true}
- lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: false}
+
+ if endLine != startLine {
+ lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: false}
+ }
}
ast.Inspect(parsedFile, func(astNode ast.Node) bool {
diff --git a/main.go b/main.go
index c41bfe2..781d873 100644
--- a/main.go
+++ b/main.go
@@ -15,11 +15,10 @@ import (
var version = "dev"
var (
- writeFlag = flag.Bool("w", false, "write result to (source) file instead of stdout")
- listFlag = flag.Bool("l", false, "list files whose formatting differs from iku's")
- diffFlag = flag.Bool("d", false, "display diffs instead of rewriting files")
- commentsFlag = flag.String("comments", "follow", "comment attachment mode: follow, precede, standalone")
- versionFlag = flag.Bool("version", false, "print version")
+ writeFlag = flag.Bool("w", false, "write result to (source) file instead of stdout")
+ listFlag = flag.Bool("l", false, "list files whose formatting differs from iku's")
+ diffFlag = flag.Bool("d", false, "display diffs instead of rewriting files")
+ versionFlag = flag.Bool("version", false, "print version")
)
func main() {
@@ -35,14 +34,15 @@ func main() {
os.Exit(0)
}
- commentMode, err := parseCommentMode(*commentsFlag)
+ configuration := loadConfiguration()
+ commentMode, validationError := configuration.commentMode()
- if err != nil {
- fmt.Fprintf(os.Stderr, "iku: %v\n", err)
+ if validationError != nil {
+ fmt.Fprintf(os.Stderr, "iku: %v\n", validationError)
os.Exit(2)
}
- formatter := &Formatter{CommentMode: commentMode}
+ formatter := &Formatter{CommentMode: commentMode, Configuration: configuration}
if flag.NArg() == 0 {
if *writeFlag {
@@ -84,19 +84,6 @@ func main() {
os.Exit(exitCode)
}
-func parseCommentMode(commentModeString string) (CommentMode, error) {
- switch strings.ToLower(commentModeString) {
- case "follow":
- return CommentsFollow, nil
- case "precede":
- return CommentsPrecede, nil
- case "standalone":
- return CommentsStandalone, nil
- default:
- return 0, fmt.Errorf("invalid comment mode: %q (use follow, precede, or standalone)", commentModeString)
- }
-}
-
var supportedFileExtensions = map[string]bool{
".go": true,
".js": true,