@herb-tools/formatter 0.7.5 → 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/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 { FormatOptions } from "./options.js"
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
- const result = this.parse(source)
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
- return new FormatPrinter(source, resolvedOptions).print(result.value)
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
+ }