@herb-tools/formatter 0.7.5 → 0.8.1

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.
@@ -3,4 +3,5 @@ export declare class CLI {
3
3
  private parseArguments;
4
4
  run(): Promise<void>;
5
5
  private readStdin;
6
+ private resolvePatternToFiles;
6
7
  }
@@ -0,0 +1,160 @@
1
+ import { Node, HTMLTextNode, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode } from "@herb-tools/core";
2
+ /**
3
+ * Analysis result for HTMLElementNode formatting decisions
4
+ */
5
+ export interface ElementFormattingAnalysis {
6
+ openTagInline: boolean;
7
+ elementContentInline: boolean;
8
+ closeTagInline: boolean;
9
+ }
10
+ /**
11
+ * Content unit represents a piece of content in text flow
12
+ * Can be atomic (inline elements, ERB) or splittable (text)
13
+ */
14
+ export interface ContentUnit {
15
+ content: string;
16
+ type: 'text' | 'inline' | 'erb' | 'block';
17
+ isAtomic: boolean;
18
+ breaksFlow: boolean;
19
+ isHerbDisable?: boolean;
20
+ }
21
+ /**
22
+ * Content unit paired with its source AST node
23
+ */
24
+ export interface ContentUnitWithNode {
25
+ unit: ContentUnit;
26
+ node: Node | null;
27
+ }
28
+ export declare const FORMATTABLE_ATTRIBUTES: Record<string, string[]>;
29
+ export declare const INLINE_ELEMENTS: Set<string>;
30
+ export declare const CONTENT_PRESERVING_ELEMENTS: Set<string>;
31
+ export declare const SPACEABLE_CONTAINERS: Set<string>;
32
+ export declare const TIGHT_GROUP_PARENTS: Set<string>;
33
+ export declare const TIGHT_GROUP_CHILDREN: Set<string>;
34
+ export declare const SPACING_THRESHOLD = 3;
35
+ /**
36
+ * Token list attributes that contain space-separated values and benefit from
37
+ * spacing around ERB content for readability
38
+ */
39
+ export declare const TOKEN_LIST_ATTRIBUTES: Set<string>;
40
+ /**
41
+ * Check if a node is pure whitespace (empty text node with only whitespace)
42
+ */
43
+ export declare function isPureWhitespaceNode(node: Node): boolean;
44
+ /**
45
+ * Check if a node is non-whitespace (has meaningful content)
46
+ */
47
+ export declare function isNonWhitespaceNode(node: Node): boolean;
48
+ /**
49
+ * Find the previous meaningful (non-whitespace) sibling
50
+ * Returns -1 if no meaningful sibling is found
51
+ */
52
+ export declare function findPreviousMeaningfulSibling(siblings: Node[], currentIndex: number): number;
53
+ /**
54
+ * Check if there's whitespace between two indices in children array
55
+ */
56
+ export declare function hasWhitespaceBetween(children: Node[], startIndex: number, endIndex: number): boolean;
57
+ /**
58
+ * Filter children to remove insignificant whitespace
59
+ */
60
+ export declare function filterSignificantChildren(body: Node[]): Node[];
61
+ /**
62
+ * Smart filter that preserves exactly ONE whitespace before herb:disable comments
63
+ */
64
+ export declare function filterEmptyNodesForHerbDisable(nodes: Node[]): Node[];
65
+ /**
66
+ * Check if a word is standalone closing punctuation
67
+ */
68
+ export declare function isClosingPunctuation(word: string): boolean;
69
+ /**
70
+ * Check if a line ends with opening punctuation
71
+ */
72
+ export declare function lineEndsWithOpeningPunctuation(line: string): boolean;
73
+ /**
74
+ * Check if a string is an ERB tag
75
+ */
76
+ export declare function isERBTag(text: string): boolean;
77
+ /**
78
+ * Check if a string ends with an ERB tag
79
+ */
80
+ export declare function endsWithERBTag(text: string): boolean;
81
+ /**
82
+ * Check if a string starts with an ERB tag
83
+ */
84
+ export declare function startsWithERBTag(text: string): boolean;
85
+ /**
86
+ * Determine if space is needed between the current line and the next word
87
+ */
88
+ export declare function needsSpaceBetween(currentLine: string, word: string): boolean;
89
+ /**
90
+ * Build a line by adding a word with appropriate spacing
91
+ */
92
+ export declare function buildLineWithWord(currentLine: string, word: string): string;
93
+ /**
94
+ * Check if a node is an inline element or ERB node
95
+ */
96
+ export declare function isInlineOrERBNode(node: Node): boolean;
97
+ /**
98
+ * Check if an element should be treated as inline based on its tag name
99
+ */
100
+ export declare function isInlineElement(tagName: string): boolean;
101
+ /**
102
+ * Check if the current inline element is adjacent to a previous inline element (no whitespace between)
103
+ */
104
+ export declare function isAdjacentToPreviousInline(siblings: Node[], index: number): boolean;
105
+ /**
106
+ * Check if a node should be appended to the last line (for adjacent inline elements and punctuation)
107
+ */
108
+ export declare function shouldAppendToLastLine(child: Node, siblings: Node[], index: number): boolean;
109
+ /**
110
+ * Check if user-intentional spacing should be preserved (double newlines between elements)
111
+ */
112
+ export declare function shouldPreserveUserSpacing(child: Node, siblings: Node[], index: number): boolean;
113
+ /**
114
+ * Check if children contain any text content with newlines
115
+ */
116
+ export declare function hasMultilineTextContent(children: Node[]): boolean;
117
+ /**
118
+ * Check if all nested elements in the children are inline elements
119
+ */
120
+ export declare function areAllNestedElementsInline(children: Node[]): boolean;
121
+ /**
122
+ * Check if element has complex ERB control flow
123
+ */
124
+ export declare function hasComplexERBControlFlow(inlineNodes: Node[]): boolean;
125
+ /**
126
+ * Check if children contain mixed text and inline elements (like "text<em>inline</em>text")
127
+ * or mixed ERB output and text (like "<%= value %> text")
128
+ * This indicates content that should be formatted inline even with structural newlines
129
+ */
130
+ export declare function hasMixedTextAndInlineContent(children: Node[]): boolean;
131
+ export declare function isContentPreserving(element: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): boolean;
132
+ /**
133
+ * Count consecutive inline elements/ERB at the start of children (with no whitespace between)
134
+ */
135
+ export declare function countAdjacentInlineElements(children: Node[]): number;
136
+ /**
137
+ * Check if a node represents a block-level element
138
+ */
139
+ export declare function isBlockLevelNode(node: Node): boolean;
140
+ /**
141
+ * Check if an element is a line-breaking element (br or hr)
142
+ */
143
+ export declare function isLineBreakingElement(node: Node): boolean;
144
+ /**
145
+ * Normalize text by replacing multiple spaces with single space and trim
146
+ * Then split into words
147
+ */
148
+ export declare function normalizeAndSplitWords(text: string): string[];
149
+ /**
150
+ * Check if text ends with whitespace
151
+ */
152
+ export declare function endsWithWhitespace(text: string): boolean;
153
+ /**
154
+ * Check if an ERB content node is a herb:disable comment
155
+ */
156
+ export declare function isHerbDisableComment(node: Node): boolean;
157
+ /**
158
+ * Check if a text node is YAML frontmatter (starts and ends with ---)
159
+ */
160
+ export declare function isFrontmatter(node: Node): node is HTMLTextNode;
@@ -25,12 +25,6 @@ export declare class FormatPrinter extends Printer {
25
25
  private elementStack;
26
26
  private elementFormattingAnalysis;
27
27
  source: string;
28
- private static readonly INLINE_ELEMENTS;
29
- private static readonly CONTENT_PRESERVING_ELEMENTS;
30
- private static readonly SPACEABLE_CONTAINERS;
31
- private static readonly TIGHT_GROUP_PARENTS;
32
- private static readonly TIGHT_GROUP_CHILDREN;
33
- private static readonly SPACING_THRESHOLD;
34
28
  constructor(source: string, options: Required<FormatOptions>);
35
29
  print(input: Node | ParseResult | Token): string;
36
30
  /**
@@ -94,23 +88,10 @@ export declare class FormatPrinter extends Printer {
94
88
  * @returns true if spacing should be added before the current element
95
89
  */
96
90
  private shouldAddSpacingBetweenSiblings;
97
- /**
98
- * Token list attributes that contain space-separated values and benefit from
99
- * spacing around ERB content for readability
100
- */
101
- private static readonly TOKEN_LIST_ATTRIBUTES;
102
91
  /**
103
92
  * Check if we're currently processing a token list attribute that needs spacing
104
93
  */
105
- private isInTokenListAttribute;
106
- /**
107
- * Find the previous meaningful (non-whitespace) sibling
108
- */
109
- private findPreviousMeaningfulSibling;
110
- /**
111
- * Check if a node represents a block-level element
112
- */
113
- private isBlockLevelNode;
94
+ private get isInTokenListAttribute();
114
95
  /**
115
96
  * Render attributes as a space-separated string
116
97
  */
@@ -119,8 +100,8 @@ export declare class FormatPrinter extends Printer {
119
100
  * Determine if a tag should be rendered inline based on attribute count and other factors
120
101
  */
121
102
  private shouldRenderInline;
122
- private getAttributeName;
123
103
  private wouldClassAttributeBeMultiline;
104
+ private getAttributeName;
124
105
  private getAttributeValue;
125
106
  private hasMultilineAttributes;
126
107
  private formatClassAttribute;
@@ -192,58 +173,114 @@ export declare class FormatPrinter extends Printer {
192
173
  * Determines if the close tag should be rendered inline (usually follows content decision)
193
174
  */
194
175
  private shouldRenderCloseTagInline;
195
- private isNonWhitespaceNode;
176
+ private formatFrontmatter;
196
177
  /**
197
- * Check if an element should be treated as inline based on its tag name
178
+ * Append a child node to the last output line
198
179
  */
199
- private isInlineElement;
180
+ private appendChildToLastLine;
200
181
  /**
201
- * Check if we're in a text flow context (parent contains mixed text and inline elements)
182
+ * Visit children in a text flow context (mixed text and inline elements)
183
+ * Handles word wrapping and keeps adjacent inline elements together
202
184
  */
203
185
  private visitTextFlowChildren;
204
- private isInTextFlowContext;
205
- private renderInlineOpen;
206
- renderAttribute(attribute: HTMLAttributeNode): string;
207
186
  /**
208
- * Try to render a complete element inline including opening tag, children, and closing tag
187
+ * Wrap remaining words that don't fit on the current line
188
+ * Returns the wrapped lines with proper indentation
209
189
  */
210
- private tryRenderInlineFull;
190
+ private wrapRemainingWords;
211
191
  /**
212
- * Try to render just the children inline (without tags)
192
+ * Try to merge text starting with punctuation to inline content
193
+ * Returns object with merged content and whether processing should stop
213
194
  */
214
- private tryRenderChildrenInline;
195
+ private tryMergePunctuationText;
215
196
  /**
216
- * Try to render children inline if they are simple enough.
217
- * Returns the inline string if possible, null otherwise.
197
+ * Render adjacent inline elements together on one line
218
198
  */
219
- private tryRenderInline;
199
+ private renderAdjacentInlineElements;
200
+ /**
201
+ * Render an inline element as a string
202
+ */
203
+ private renderInlineElementAsString;
204
+ /**
205
+ * Render an ERB node as a string
206
+ */
207
+ private renderERBAsString;
208
+ /**
209
+ * Visit remaining children after processing adjacent inline elements
210
+ */
211
+ private visitRemainingChildren;
212
+ /**
213
+ * Build words array from text/inline/ERB and wrap them
214
+ */
215
+ private buildAndWrapTextFlow;
216
+ /**
217
+ * Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
218
+ * Returns true if merge was performed
219
+ */
220
+ private tryMergeTextAfterAtomic;
221
+ /**
222
+ * Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace
223
+ * Returns true if merge was performed
224
+ */
225
+ private tryMergeAtomicAfterText;
226
+ /**
227
+ * Check if there's whitespace between current node and last processed node
228
+ */
229
+ private hasWhitespaceBeforeNode;
230
+ /**
231
+ * Check if last unit in result ends with whitespace
232
+ */
233
+ private lastUnitEndsWithWhitespace;
220
234
  /**
221
- * Check if children contain mixed text and inline elements (like "text<em>inline</em>text")
222
- * or mixed ERB output and text (like "<%= value %> text")
223
- * This indicates content that should be formatted inline even with structural newlines
235
+ * Process a text node and add it to results (with potential merging)
224
236
  */
225
- private hasMixedTextAndInlineContent;
237
+ private processTextNode;
226
238
  /**
227
- * Check if children contain any text content with newlines
239
+ * Process an inline element and add it to results (with potential merging)
228
240
  */
229
- private hasMultilineTextContent;
241
+ private processInlineElement;
230
242
  /**
231
- * Check if all nested elements in the children are inline elements
243
+ * Process an ERB content node and add it to results (with potential merging)
232
244
  */
233
- private areAllNestedElementsInline;
245
+ private processERBContentNode;
234
246
  /**
235
- * Check if element has complex ERB control flow
247
+ * Convert AST nodes to content units with node references
236
248
  */
237
- private hasComplexERBControlFlow;
249
+ private buildContentUnitsWithNodes;
238
250
  /**
239
- * Filter children to remove insignificant whitespace
251
+ * Flush accumulated words to output with wrapping
240
252
  */
241
- private filterSignificantChildren;
253
+ private flushWords;
254
+ /**
255
+ * Wrap words to fit within line length and push to output
256
+ * Handles punctuation spacing intelligently
257
+ * Excludes herb:disable comments from line length calculations
258
+ */
259
+ private wrapAndPushWords;
260
+ private isInTextFlowContext;
261
+ private renderInlineOpen;
262
+ renderAttribute(attribute: HTMLAttributeNode): string;
263
+ /**
264
+ * Try to render a complete element inline including opening tag, children, and closing tag
265
+ */
266
+ private tryRenderInlineFull;
267
+ /**
268
+ * Check if children contain a leading herb:disable comment (after optional whitespace)
269
+ */
270
+ private hasLeadingHerbDisable;
271
+ /**
272
+ * Try to render just the children inline (without tags)
273
+ */
274
+ private tryRenderChildrenInline;
275
+ /**
276
+ * Try to render children inline if they are simple enough.
277
+ * Returns the inline string if possible, null otherwise.
278
+ */
279
+ private tryRenderInline;
242
280
  /**
243
- * Filter out empty text nodes and whitespace nodes
281
+ * Get filtered children, using smart herb:disable filtering if needed
244
282
  */
245
- private filterEmptyNodes;
283
+ private getFilteredChildren;
246
284
  private renderElementInline;
247
285
  private renderChildrenInline;
248
- private isContentPreserving;
249
286
  }
@@ -1,5 +1,6 @@
1
- import type { FormatOptions } from "./options.js";
1
+ import type { Config } from "@herb-tools/config";
2
2
  import type { HerbBackend } from "@herb-tools/core";
3
+ import type { FormatOptions } from "./options.js";
3
4
  /**
4
5
  * Formatter uses a Herb Backend to parse the source and then
5
6
  * formats the resulting AST into a well-indented, wrapped string.
@@ -7,10 +8,25 @@ import type { HerbBackend } from "@herb-tools/core";
7
8
  export declare class Formatter {
8
9
  private herb;
9
10
  private options;
11
+ /**
12
+ * Creates a Formatter instance from a Config object (recommended).
13
+ *
14
+ * @param herb - The Herb backend instance for parsing
15
+ * @param config - Optional Config instance for formatter options
16
+ * @param options - Additional options to override config
17
+ * @returns A configured Formatter instance
18
+ */
19
+ static from(herb: HerbBackend, config?: Config, options?: FormatOptions): Formatter;
20
+ /**
21
+ * Creates a new Formatter instance.
22
+ *
23
+ * @param herb - The Herb backend instance for parsing
24
+ * @param options - Format options (including rewriters)
25
+ */
10
26
  constructor(herb: HerbBackend, options?: FormatOptions);
11
27
  /**
12
28
  * Format a source string, optionally overriding format options per call.
13
29
  */
14
- format(source: string, options?: FormatOptions): string;
30
+ format(source: string, options?: FormatOptions, filePath?: string): string;
15
31
  private parse;
16
32
  }
@@ -1,14 +1,21 @@
1
+ import type { ASTRewriter, StringRewriter } from "@herb-tools/rewriter";
1
2
  /**
2
3
  * Formatting options for the Herb formatter.
3
4
  *
4
5
  * indentWidth: number of spaces per indentation level.
5
6
  * maxLineLength: maximum line length before wrapping text or attributes.
7
+ * preRewriters: AST rewriters to run before formatting.
8
+ * postRewriters: String rewriters to run after formatting.
6
9
  */
7
10
  export interface FormatOptions {
8
11
  /** number of spaces per indentation level; defaults to 2 */
9
12
  indentWidth?: number;
10
13
  /** maximum line length before wrapping; defaults to 80 */
11
14
  maxLineLength?: number;
15
+ /** Pre-format rewriters (transform AST before formatting); defaults to [] */
16
+ preRewriters?: ASTRewriter[];
17
+ /** Post-format rewriters (transform string after formatting); defaults to [] */
18
+ postRewriters?: StringRewriter[];
12
19
  }
13
20
  /**
14
21
  * Default values for formatting options.
@@ -0,0 +1,12 @@
1
+ import { Visitor } from "@herb-tools/core";
2
+ import type { ERBContentNode, ParseResult } from "@herb-tools/core";
3
+ export declare const isScaffoldTemplate: (result: ParseResult) => boolean;
4
+ /**
5
+ * Visitor that detects if the AST represents a Rails scaffold template.
6
+ * Scaffold templates contain escaped ERB tags (<%%= or <%%)
7
+ * and should not be formatted to preserve their exact structure.
8
+ */
9
+ export declare class ScaffoldTemplateDetector extends Visitor {
10
+ hasEscapedERB: boolean;
11
+ visitERBContentNode(node: ERBContentNode): void;
12
+ }
@@ -0,0 +1,23 @@
1
+ import type { CustomRewriterLoaderOptions } from "@herb-tools/rewriter/loader";
2
+ export interface FormatterRewriterOptions extends CustomRewriterLoaderOptions {
3
+ /**
4
+ * Whether to load custom rewriters from the project
5
+ * Defaults to true
6
+ */
7
+ loadCustomRewriters?: boolean;
8
+ /**
9
+ * Names of pre-format rewriters to run (in order)
10
+ */
11
+ pre?: string[];
12
+ /**
13
+ * Names of post-format rewriters to run (in order)
14
+ */
15
+ post?: string[];
16
+ }
17
+ export interface FormatterRewriterInfo {
18
+ preCount: number;
19
+ postCount: number;
20
+ warnings: string[];
21
+ preNames: string[];
22
+ postNames: string[];
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/formatter",
3
- "version": "0.7.5",
3
+ "version": "0.8.1",
4
4
  "description": "Auto-formatter for HTML+ERB templates with intelligent indentation, line wrapping, and ERB-aware pretty-printing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -35,11 +35,10 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@herb-tools/core": "0.7.5",
39
- "@herb-tools/printer": "0.7.5"
40
- },
41
- "devDependencies": {
42
- "glob": "^11.0.3"
38
+ "@herb-tools/config": "0.8.1",
39
+ "@herb-tools/core": "0.8.1",
40
+ "@herb-tools/printer": "0.8.1",
41
+ "@herb-tools/rewriter": "0.8.1"
43
42
  },
44
43
  "files": [
45
44
  "package.json",