@herb-tools/formatter 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -11
- package/dist/herb-format.js +23209 -2215
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +1457 -330
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1457 -330
- package/dist/index.esm.js.map +1 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/format-helpers.d.ts +160 -0
- package/dist/types/format-printer.d.ts +87 -50
- package/dist/types/formatter.d.ts +18 -2
- package/dist/types/options.d.ts +7 -0
- package/dist/types/scaffold-template-detector.d.ts +12 -0
- package/dist/types/types.d.ts +23 -0
- package/package.json +5 -6
- package/src/cli.ts +357 -111
- package/src/format-helpers.ts +508 -0
- package/src/format-printer.ts +1010 -390
- package/src/formatter.ts +76 -4
- package/src/options.ts +12 -0
- package/src/scaffold-template-detector.ts +33 -0
- package/src/types.ts +27 -0
package/src/formatter.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { FormatPrinter } from "./format-printer.js"
|
|
2
|
+
|
|
3
|
+
import { isScaffoldTemplate } from "./scaffold-template-detector.js"
|
|
2
4
|
import { resolveFormatOptions } from "./options.js"
|
|
3
5
|
|
|
4
|
-
import type {
|
|
6
|
+
import type { Config } from "@herb-tools/config"
|
|
7
|
+
import type { RewriteContext } from "@herb-tools/rewriter"
|
|
5
8
|
import type { HerbBackend, ParseResult } from "@herb-tools/core"
|
|
9
|
+
import type { FormatOptions } from "./options.js"
|
|
10
|
+
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Formatter uses a Herb Backend to parse the source and then
|
|
@@ -12,6 +17,37 @@ export class Formatter {
|
|
|
12
17
|
private herb: HerbBackend
|
|
13
18
|
private options: Required<FormatOptions>
|
|
14
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Creates a Formatter instance from a Config object (recommended).
|
|
22
|
+
*
|
|
23
|
+
* @param herb - The Herb backend instance for parsing
|
|
24
|
+
* @param config - Optional Config instance for formatter options
|
|
25
|
+
* @param options - Additional options to override config
|
|
26
|
+
* @returns A configured Formatter instance
|
|
27
|
+
*/
|
|
28
|
+
static from(
|
|
29
|
+
herb: HerbBackend,
|
|
30
|
+
config?: Config,
|
|
31
|
+
options: FormatOptions = {}
|
|
32
|
+
): Formatter {
|
|
33
|
+
const formatterConfig = config?.formatter || {}
|
|
34
|
+
|
|
35
|
+
const mergedOptions: FormatOptions = {
|
|
36
|
+
indentWidth: options.indentWidth ?? formatterConfig.indentWidth,
|
|
37
|
+
maxLineLength: options.maxLineLength ?? formatterConfig.maxLineLength,
|
|
38
|
+
preRewriters: options.preRewriters,
|
|
39
|
+
postRewriters: options.postRewriters,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Formatter(herb, mergedOptions)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new Formatter instance.
|
|
47
|
+
*
|
|
48
|
+
* @param herb - The Herb backend instance for parsing
|
|
49
|
+
* @param options - Format options (including rewriters)
|
|
50
|
+
*/
|
|
15
51
|
constructor(herb: HerbBackend, options: FormatOptions = {}) {
|
|
16
52
|
this.herb = herb
|
|
17
53
|
this.options = resolveFormatOptions(options)
|
|
@@ -20,13 +56,49 @@ export class Formatter {
|
|
|
20
56
|
/**
|
|
21
57
|
* Format a source string, optionally overriding format options per call.
|
|
22
58
|
*/
|
|
23
|
-
format(source: string, options: FormatOptions = {}): string {
|
|
24
|
-
|
|
59
|
+
format(source: string, options: FormatOptions = {}, filePath?: string): string {
|
|
60
|
+
let result = this.parse(source)
|
|
61
|
+
|
|
25
62
|
if (result.failed) return source
|
|
63
|
+
if (isScaffoldTemplate(result)) return source
|
|
26
64
|
|
|
27
65
|
const resolvedOptions = resolveFormatOptions({ ...this.options, ...options })
|
|
28
66
|
|
|
29
|
-
|
|
67
|
+
let node = result.value
|
|
68
|
+
|
|
69
|
+
if (resolvedOptions.preRewriters.length > 0) {
|
|
70
|
+
const context: RewriteContext = {
|
|
71
|
+
filePath,
|
|
72
|
+
baseDir: process.cwd() // TODO: format() shouldn't depend on node internals
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const rewriter of resolvedOptions.preRewriters) {
|
|
76
|
+
try {
|
|
77
|
+
node = rewriter.rewrite(node, context)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`Pre-format rewriter "${rewriter.name}" failed:`, error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let formatted = new FormatPrinter(source, resolvedOptions).print(node)
|
|
85
|
+
|
|
86
|
+
if (resolvedOptions.postRewriters.length > 0) {
|
|
87
|
+
const context: RewriteContext = {
|
|
88
|
+
filePath,
|
|
89
|
+
baseDir: process.cwd() // TODO: format() shouldn't depend on node internals
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const rewriter of resolvedOptions.postRewriters) {
|
|
93
|
+
try {
|
|
94
|
+
formatted = rewriter.rewrite(formatted, context)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Post-format rewriter "${rewriter.name}" failed:`, error)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return formatted
|
|
30
102
|
}
|
|
31
103
|
|
|
32
104
|
private parse(source: string): ParseResult {
|
package/src/options.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import type { ASTRewriter, StringRewriter } from "@herb-tools/rewriter"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Formatting options for the Herb formatter.
|
|
3
5
|
*
|
|
4
6
|
* indentWidth: number of spaces per indentation level.
|
|
5
7
|
* maxLineLength: maximum line length before wrapping text or attributes.
|
|
8
|
+
* preRewriters: AST rewriters to run before formatting.
|
|
9
|
+
* postRewriters: String rewriters to run after formatting.
|
|
6
10
|
*/
|
|
7
11
|
export interface FormatOptions {
|
|
8
12
|
/** number of spaces per indentation level; defaults to 2 */
|
|
9
13
|
indentWidth?: number
|
|
10
14
|
/** maximum line length before wrapping; defaults to 80 */
|
|
11
15
|
maxLineLength?: number
|
|
16
|
+
/** Pre-format rewriters (transform AST before formatting); defaults to [] */
|
|
17
|
+
preRewriters?: ASTRewriter[]
|
|
18
|
+
/** Post-format rewriters (transform string after formatting); defaults to [] */
|
|
19
|
+
postRewriters?: StringRewriter[]
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
/**
|
|
@@ -17,6 +25,8 @@ export interface FormatOptions {
|
|
|
17
25
|
export const defaultFormatOptions: Required<FormatOptions> = {
|
|
18
26
|
indentWidth: 2,
|
|
19
27
|
maxLineLength: 80,
|
|
28
|
+
preRewriters: [],
|
|
29
|
+
postRewriters: [],
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
/**
|
|
@@ -30,5 +40,7 @@ export function resolveFormatOptions(
|
|
|
30
40
|
return {
|
|
31
41
|
indentWidth: options.indentWidth ?? defaultFormatOptions.indentWidth,
|
|
32
42
|
maxLineLength: options.maxLineLength ?? defaultFormatOptions.maxLineLength,
|
|
43
|
+
preRewriters: options.preRewriters ?? defaultFormatOptions.preRewriters,
|
|
44
|
+
postRewriters: options.postRewriters ?? defaultFormatOptions.postRewriters,
|
|
33
45
|
}
|
|
34
46
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Visitor } from "@herb-tools/core"
|
|
2
|
+
import type { ERBContentNode, ParseResult } from "@herb-tools/core"
|
|
3
|
+
|
|
4
|
+
export const isScaffoldTemplate = (result: ParseResult): boolean => {
|
|
5
|
+
const detector = new ScaffoldTemplateDetector()
|
|
6
|
+
|
|
7
|
+
detector.visit(result.value)
|
|
8
|
+
|
|
9
|
+
return detector.hasEscapedERB
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Visitor that detects if the AST represents a Rails scaffold template.
|
|
14
|
+
* Scaffold templates contain escaped ERB tags (<%%= or <%%)
|
|
15
|
+
* and should not be formatted to preserve their exact structure.
|
|
16
|
+
*/
|
|
17
|
+
export class ScaffoldTemplateDetector extends Visitor {
|
|
18
|
+
public hasEscapedERB = false
|
|
19
|
+
|
|
20
|
+
visitERBContentNode(node: ERBContentNode): void {
|
|
21
|
+
const opening = node.tag_opening?.value
|
|
22
|
+
|
|
23
|
+
if (opening && opening.startsWith("<%%")) {
|
|
24
|
+
this.hasEscapedERB = true
|
|
25
|
+
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this.hasEscapedERB) return
|
|
30
|
+
|
|
31
|
+
this.visitChildNodes(node)
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CustomRewriterLoaderOptions } from "@herb-tools/rewriter/loader"
|
|
2
|
+
|
|
3
|
+
export interface FormatterRewriterOptions extends CustomRewriterLoaderOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to load custom rewriters from the project
|
|
6
|
+
* Defaults to true
|
|
7
|
+
*/
|
|
8
|
+
loadCustomRewriters?: boolean
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Names of pre-format rewriters to run (in order)
|
|
12
|
+
*/
|
|
13
|
+
pre?: string[]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Names of post-format rewriters to run (in order)
|
|
17
|
+
*/
|
|
18
|
+
post?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FormatterRewriterInfo {
|
|
22
|
+
preCount: number
|
|
23
|
+
postCount: number
|
|
24
|
+
warnings: string[]
|
|
25
|
+
preNames: string[]
|
|
26
|
+
postNames: string[]
|
|
27
|
+
}
|