@herb-tools/formatter 0.8.9 → 0.9.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.
@@ -0,0 +1,44 @@
1
+ import { HTMLAttributeNode } from "@herb-tools/core";
2
+ import type { ERBNode } from "@herb-tools/core";
3
+ /**
4
+ * Interface that the delegate must implement to provide
5
+ * ERB reconstruction capabilities to the AttributeRenderer.
6
+ */
7
+ export interface AttributeRendererDelegate {
8
+ reconstructERBNode(node: ERBNode, withFormatting: boolean): string;
9
+ }
10
+ /**
11
+ * AttributeRenderer converts HTMLAttributeNode AST nodes into formatted strings.
12
+ * It handles class attribute wrapping, multiline attribute formatting,
13
+ * quote normalization, and token list attribute spacing.
14
+ */
15
+ export declare class AttributeRenderer {
16
+ private delegate;
17
+ private maxLineLength;
18
+ private indentWidth;
19
+ currentAttributeName: string | null;
20
+ indentLevel: number;
21
+ constructor(delegate: AttributeRendererDelegate, maxLineLength: number, indentWidth: number);
22
+ /**
23
+ * Check if we're currently processing a token list attribute that needs spacing
24
+ */
25
+ get isInTokenListAttribute(): boolean;
26
+ /**
27
+ * Render attributes as a space-separated string
28
+ */
29
+ renderAttributesString(attributes: HTMLAttributeNode[], tagName: string): string;
30
+ /**
31
+ * Determine if a tag should be rendered inline based on attribute count and other factors
32
+ */
33
+ shouldRenderInline(totalAttributeCount: number, inlineLength: number, indentLength: number, maxLineLength?: number, hasComplexERB?: boolean, hasMultilineAttributes?: boolean, attributes?: HTMLAttributeNode[]): boolean;
34
+ wouldClassAttributeBeMultiline(content: string, indentLength: number): boolean;
35
+ getAttributeName(attribute: HTMLAttributeNode): string;
36
+ getAttributeValue(attribute: HTMLAttributeNode): string;
37
+ hasMultilineAttributes(attributes: HTMLAttributeNode[]): boolean;
38
+ formatClassAttribute(content: string, name: string, equals: string, open_quote: string, close_quote: string): string;
39
+ isFormattableAttribute(attributeName: string, tagName: string): boolean;
40
+ formatMultilineAttribute(content: string, name: string, open_quote: string, close_quote: string): string;
41
+ formatMultilineAttributeValue(lines: string[]): string;
42
+ breakTokensIntoLines(tokens: string[], currentIndent: number, separator?: string): string[];
43
+ renderAttribute(attribute: HTMLAttributeNode, tagName: string): string;
44
+ }
@@ -1,4 +1,6 @@
1
1
  export declare class CLI {
2
+ protected projectPath: string;
3
+ protected determineProjectPath(patterns: string[]): void;
2
4
  private usage;
3
5
  private parseArguments;
4
6
  run(): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { Node } from "@herb-tools/core";
2
+ /**
3
+ * Result of formatting an ERB comment.
4
+ * - `single-line`: the caller emits the text on a single line (using push or pushWithIndent)
5
+ * - `multi-line`: the caller emits header, indented content lines, and footer separately
6
+ */
7
+ export type ERBCommentResult = {
8
+ type: 'single-line';
9
+ text: string;
10
+ } | {
11
+ type: 'multi-line';
12
+ header: string;
13
+ contentLines: string[];
14
+ footer: string;
15
+ };
16
+ /**
17
+ * Extract the raw inner text from HTML comment children.
18
+ * Joins text/literal nodes by content and ERB nodes via IdentityPrinter.
19
+ */
20
+ export declare function extractHTMLCommentContent(children: Node[]): string;
21
+ /**
22
+ * Format the inner content of an HTML comment.
23
+ *
24
+ * Handles three cases:
25
+ * 1. IE conditional comments (`[if ...` / `<![endif]`) — returned as-is
26
+ * 2. Multiline comments — re-indented with relative indent preservation
27
+ * 3. Single-line comments — wrapped with spaces: ` content `
28
+ *
29
+ * Returns null for IE conditional comments to signal the caller
30
+ * should emit the raw content without reformatting.
31
+ *
32
+ * @param rawInner - The joined children content string (may be empty)
33
+ * @param indentWidth - Number of spaces per indent level
34
+ * @returns The formatted inner string, or null if rawInner is empty-ish
35
+ */
36
+ export declare function formatHTMLCommentInner(rawInner: string, indentWidth: number): string;
37
+ /**
38
+ * Format an ERB comment into either a single-line or multi-line result.
39
+ *
40
+ * @param open - The opening tag (e.g. "<%#")
41
+ * @param content - The raw content string between open/close tags
42
+ * @param close - The closing tag (e.g. "%>")
43
+ * @returns A discriminated union describing how to render the comment
44
+ */
45
+ export declare function formatERBCommentLines(open: string, content: string, close: string): ERBCommentResult;
@@ -25,19 +25,17 @@ export interface ContentUnitWithNode {
25
25
  unit: ContentUnit;
26
26
  node: Node | null;
27
27
  }
28
+ /**
29
+ * ASCII whitespace pattern - use instead of \s to preserve Unicode whitespace
30
+ * characters like NBSP (U+00A0) and full-width space (U+3000)
31
+ */
32
+ export declare const ASCII_WHITESPACE: RegExp;
28
33
  export declare const FORMATTABLE_ATTRIBUTES: Record<string, string[]>;
29
34
  export declare const INLINE_ELEMENTS: Set<string>;
30
35
  export declare const CONTENT_PRESERVING_ELEMENTS: Set<string>;
36
+ export declare const WHITESPACE_PRESERVING_CLASSES: string[];
37
+ export declare const WHITESPACE_PRESERVING_STYLE_VALUES: Set<string>;
31
38
  export declare const SPACEABLE_CONTAINERS: Set<string>;
32
- /**
33
- * Token list attributes that contain space-separated values and benefit from
34
- * spacing around ERB content for readability
35
- */
36
- export declare const TOKEN_LIST_ATTRIBUTES: Set<string>;
37
- /**
38
- * Check if a node is pure whitespace (empty text node with only whitespace)
39
- */
40
- export declare function isPureWhitespaceNode(node: Node): boolean;
41
39
  /**
42
40
  * Check if a node is non-whitespace (has meaningful content)
43
41
  */
@@ -125,11 +123,13 @@ export declare function hasComplexERBControlFlow(inlineNodes: Node[]): boolean;
125
123
  * This indicates content that should be formatted inline even with structural newlines
126
124
  */
127
125
  export declare function hasMixedTextAndInlineContent(children: Node[]): boolean;
126
+ export declare function hasWhitespacePreservingStyle(element: HTMLElementNode): boolean;
128
127
  export declare function isContentPreserving(element: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): boolean;
129
128
  /**
130
- * Count consecutive inline elements/ERB at the start of children (with no whitespace between)
129
+ * Count consecutive inline elements/ERB with no whitespace between them.
130
+ * Starts from startIndex and skips indices in processedIndices.
131
131
  */
132
- export declare function countAdjacentInlineElements(children: Node[]): number;
132
+ export declare function countAdjacentInlineElements(children: Node[], startIndex?: number, processedIndices?: Set<number>): number;
133
133
  /**
134
134
  * Check if a node represents a block-level element
135
135
  */
@@ -151,6 +151,10 @@ export declare function endsWithWhitespace(text: string): boolean;
151
151
  * Check if an ERB content node is a herb:disable comment
152
152
  */
153
153
  export declare function isHerbDisableComment(node: Node): boolean;
154
+ /**
155
+ * Check if children contain a leading herb:disable comment (after optional whitespace)
156
+ */
157
+ export declare function hasLeadingHerbDisable(children: Node[]): boolean;
154
158
  /**
155
159
  * Check if a text node is YAML frontmatter (starts and ends with ---)
156
160
  */
@@ -1,12 +1,14 @@
1
1
  import { Printer } from "@herb-tools/printer";
2
- import { ParseResult, Node, DocumentNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLAttributeNode, HTMLAttributeValueNode, HTMLAttributeNameNode, HTMLTextNode, HTMLCommentNode, HTMLDoctypeNode, ERBContentNode, ERBBlockNode, ERBEndNode, ERBElseNode, ERBIfNode, ERBWhenNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBUntilNode, ERBForNode, ERBRescueNode, ERBEnsureNode, ERBBeginNode, ERBUnlessNode, ERBYieldNode, ERBInNode, XMLDeclarationNode, CDATANode, Token } from "@herb-tools/core";
3
2
  import type { ERBNode } from "@herb-tools/core";
4
3
  import type { FormatOptions } from "./options.js";
4
+ import type { TextFlowDelegate } from "./text-flow-engine.js";
5
+ import type { AttributeRendererDelegate } from "./attribute-renderer.js";
6
+ import { ParseResult, Node, DocumentNode, HTMLOpenTagNode, HTMLConditionalOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLConditionalElementNode, HTMLAttributeNode, HTMLAttributeValueNode, HTMLAttributeNameNode, HTMLTextNode, HTMLCommentNode, HTMLDoctypeNode, ERBContentNode, ERBBlockNode, ERBEndNode, ERBElseNode, ERBIfNode, ERBWhenNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBUntilNode, ERBForNode, ERBRescueNode, ERBEnsureNode, ERBBeginNode, ERBUnlessNode, ERBYieldNode, ERBInNode, ERBOpenTagNode, HTMLVirtualCloseTagNode, XMLDeclarationNode, CDATANode, Token } from "@herb-tools/core";
5
7
  /**
6
8
  * Printer traverses the Herb AST using the Visitor pattern
7
9
  * and emits a formatted string with proper indentation, line breaks, and attribute wrapping.
8
10
  */
9
- export declare class FormatPrinter extends Printer {
11
+ export declare class FormatPrinter extends Printer implements TextFlowDelegate, AttributeRendererDelegate {
10
12
  /**
11
13
  * @deprecated integrate indentWidth into this.options and update FormatOptions to extend from @herb-tools/printer options
12
14
  */
@@ -14,20 +16,22 @@ export declare class FormatPrinter extends Printer {
14
16
  /**
15
17
  * @deprecated integrate maxLineLength into this.options and update FormatOptions to extend from @herb-tools/printer options
16
18
  */
17
- private maxLineLength;
19
+ maxLineLength: number;
18
20
  /**
19
21
  * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
20
22
  */
21
23
  private lines;
22
24
  private indentLevel;
23
25
  private inlineMode;
24
- private currentAttributeName;
26
+ private inContentPreservingContext;
27
+ private inConditionalOpenTagContext;
25
28
  private elementStack;
26
29
  private elementFormattingAnalysis;
27
30
  private nodeIsMultiline;
28
31
  private stringLineCount;
29
- private tagGroupsCache;
30
- private allSingleLineCache;
32
+ private textFlow;
33
+ private attributeRenderer;
34
+ private spacingAnalyzer;
31
35
  source: string;
32
36
  constructor(source: string, options: Required<FormatOptions>);
33
37
  print(input: Node | ParseResult | Token): string;
@@ -60,16 +64,19 @@ export declare class FormatPrinter extends Printer {
60
64
  /**
61
65
  * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
62
66
  */
63
- private push;
67
+ push(line: string): void;
64
68
  /**
65
69
  * @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
66
70
  */
67
- private pushWithIndent;
71
+ pushWithIndent(line: string): void;
68
72
  private withIndent;
69
- private get indent();
73
+ private withInlineMode;
74
+ private withContentPreserving;
75
+ get indent(): string;
70
76
  /**
71
77
  * Format ERB content with proper spacing around the inner content.
72
- * Returns empty string if content is empty, otherwise wraps content with single spaces.
78
+ * Returns empty string if content is empty, otherwise adds a leading space
79
+ * and a trailing space (or newline for heredoc content starting with "<<").
73
80
  */
74
81
  private formatERBContent;
75
82
  /**
@@ -80,56 +87,6 @@ export declare class FormatPrinter extends Printer {
80
87
  * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
81
88
  */
82
89
  private extractInlineNodes;
83
- /**
84
- * Check if a node will render as multiple lines when formatted.
85
- */
86
- private isMultilineElement;
87
- /**
88
- * Get a grouping key for a node (tag name for HTML, ERB type for ERB)
89
- */
90
- private getGroupingKey;
91
- /**
92
- * Detect groups of consecutive same-tag/same-type single-line elements
93
- * Returns a map of index -> group info for efficient lookup
94
- */
95
- private detectTagGroups;
96
- /**
97
- * Determine if spacing should be added between sibling elements
98
- *
99
- * This implements the "rule of three" intelligent spacing system:
100
- * - Adds spacing between 3 or more meaningful siblings
101
- * - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
102
- * - Groups comments with following elements
103
- * - Preserves user-added spacing
104
- *
105
- * @param parentElement - The parent element containing the siblings
106
- * @param siblings - Array of all sibling nodes
107
- * @param currentIndex - Index of the current node being evaluated
108
- * @param hasExistingSpacing - Whether user-added spacing already exists
109
- * @returns true if spacing should be added before the current element
110
- */
111
- private shouldAddSpacingBetweenSiblings;
112
- /**
113
- * Check if we're currently processing a token list attribute that needs spacing
114
- */
115
- private get isInTokenListAttribute();
116
- /**
117
- * Render attributes as a space-separated string
118
- */
119
- private renderAttributesString;
120
- /**
121
- * Determine if a tag should be rendered inline based on attribute count and other factors
122
- */
123
- private shouldRenderInline;
124
- private wouldClassAttributeBeMultiline;
125
- private getAttributeName;
126
- private getAttributeValue;
127
- private hasMultilineAttributes;
128
- private formatClassAttribute;
129
- private isFormattableAttribute;
130
- private formatMultilineAttribute;
131
- private formatMultilineAttributeValue;
132
- private breakTokensIntoLines;
133
90
  /**
134
91
  * Render multiline attributes for a tag
135
92
  */
@@ -138,20 +95,27 @@ export declare class FormatPrinter extends Printer {
138
95
  * Reconstruct the text representation of an ERB node
139
96
  * @param withFormatting - if true, format the content; if false, preserve original
140
97
  */
141
- private reconstructERBNode;
98
+ reconstructERBNode(node: ERBNode, withFormatting?: boolean): string;
142
99
  /**
143
100
  * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
144
101
  */
145
102
  printERBNode(node: ERBNode): void;
146
103
  visitDocumentNode(node: DocumentNode): void;
147
104
  visitHTMLElementNode(node: HTMLElementNode): void;
105
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void;
106
+ visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode): void;
148
107
  visitHTMLElementBody(body: Node[], element: HTMLElementNode): void;
108
+ private visitContentPreservingBody;
109
+ private visitInlineElementBody;
110
+ private stripLeadingHerbDisable;
149
111
  /**
150
112
  * Visit element children with intelligent spacing logic
151
113
  *
152
114
  * Tracks line positions and immediately splices blank lines after rendering each child.
153
115
  */
154
116
  private visitElementChildren;
117
+ private visitTextFlowRunInChildren;
118
+ private visitChildWithTrailingHerbDisable;
155
119
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
156
120
  visitHTMLCloseTagNode(node: HTMLCloseTagNode): void;
157
121
  visitHTMLTextNode(node: HTMLTextNode): void;
@@ -164,6 +128,8 @@ export declare class FormatPrinter extends Printer {
164
128
  visitXMLDeclarationNode(node: XMLDeclarationNode): void;
165
129
  visitCDATANode(node: CDATANode): void;
166
130
  visitERBContentNode(node: ERBContentNode): void;
131
+ visitERBOpenTagNode(node: ERBOpenTagNode): void;
132
+ visitHTMLVirtualCloseTagNode(_node: HTMLVirtualCloseTagNode): void;
167
133
  visitERBEndNode(node: ERBEndNode): void;
168
134
  visitERBYieldNode(node: ERBYieldNode): void;
169
135
  visitERBInNode(node: ERBInNode): void;
@@ -196,101 +162,30 @@ export declare class FormatPrinter extends Printer {
196
162
  * Determines if the close tag should be rendered inline (usually follows content decision)
197
163
  */
198
164
  private shouldRenderCloseTagInline;
165
+ private captureHerbDisableInline;
166
+ private fitsOnCurrentLine;
199
167
  private formatFrontmatter;
200
168
  /**
201
169
  * Append a child node to the last output line
202
170
  */
203
171
  private appendChildToLastLine;
204
- /**
205
- * Visit children in a text flow context (mixed text and inline elements)
206
- * Handles word wrapping and keeps adjacent inline elements together
207
- */
208
- private visitTextFlowChildren;
209
- /**
210
- * Wrap remaining words that don't fit on the current line
211
- * Returns the wrapped lines with proper indentation
212
- */
213
- private wrapRemainingWords;
214
- /**
215
- * Try to merge text starting with punctuation to inline content
216
- * Returns object with merged content and whether processing should stop
217
- */
218
- private tryMergePunctuationText;
219
- /**
220
- * Render adjacent inline elements together on one line
221
- */
222
- private renderAdjacentInlineElements;
223
172
  /**
224
173
  * Render an inline element as a string
225
174
  */
226
- private renderInlineElementAsString;
175
+ renderInlineElementAsString(element: HTMLElementNode): string;
227
176
  /**
228
177
  * Render an ERB node as a string
229
178
  */
230
- private renderERBAsString;
179
+ renderERBAsString(node: ERBContentNode): string;
231
180
  /**
232
- * Visit remaining children after processing adjacent inline elements
181
+ * Try to render an inline element, returning the full inline string or null if it can't be inlined.
233
182
  */
234
- private visitRemainingChildren;
235
- /**
236
- * Build words array from text/inline/ERB and wrap them
237
- */
238
- private buildAndWrapTextFlow;
239
- /**
240
- * Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
241
- * Returns true if merge was performed
242
- */
243
- private tryMergeTextAfterAtomic;
244
- /**
245
- * Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace
246
- * Returns true if merge was performed
247
- */
248
- private tryMergeAtomicAfterText;
249
- /**
250
- * Check if there's whitespace between current node and last processed node
251
- */
252
- private hasWhitespaceBeforeNode;
253
- /**
254
- * Check if last unit in result ends with whitespace
255
- */
256
- private lastUnitEndsWithWhitespace;
257
- /**
258
- * Process a text node and add it to results (with potential merging)
259
- */
260
- private processTextNode;
261
- /**
262
- * Process an inline element and add it to results (with potential merging)
263
- */
264
- private processInlineElement;
265
- /**
266
- * Process an ERB content node and add it to results (with potential merging)
267
- */
268
- private processERBContentNode;
269
- /**
270
- * Convert AST nodes to content units with node references
271
- */
272
- private buildContentUnitsWithNodes;
273
- /**
274
- * Flush accumulated words to output with wrapping
275
- */
276
- private flushWords;
277
- /**
278
- * Wrap words to fit within line length and push to output
279
- * Handles punctuation spacing intelligently
280
- * Excludes herb:disable comments from line length calculations
281
- */
282
- private wrapAndPushWords;
283
- private isInTextFlowContext;
183
+ tryRenderInlineElement(element: HTMLElementNode): string | null;
284
184
  private renderInlineOpen;
285
- renderAttribute(attribute: HTMLAttributeNode): string;
286
185
  /**
287
186
  * Try to render a complete element inline including opening tag, children, and closing tag
288
187
  */
289
188
  private tryRenderInlineFull;
290
- /**
291
- * Check if children contain a leading herb:disable comment (after optional whitespace)
292
- */
293
- private hasLeadingHerbDisable;
294
189
  /**
295
190
  * Try to render just the children inline (without tags)
296
191
  */
@@ -1,5 +1,5 @@
1
1
  import type { Config } from "@herb-tools/config";
2
- import type { HerbBackend } from "@herb-tools/core";
2
+ import type { HerbBackend, ParseOptions } from "@herb-tools/core";
3
3
  import type { FormatOptions } from "./options.js";
4
4
  /**
5
5
  * Formatter uses a Herb Backend to parse the source and then
@@ -8,6 +8,7 @@ import type { FormatOptions } from "./options.js";
8
8
  export declare class Formatter {
9
9
  private herb;
10
10
  private options;
11
+ private parseOptions;
11
12
  /**
12
13
  * Creates a Formatter instance from a Config object (recommended).
13
14
  *
@@ -23,7 +24,7 @@ export declare class Formatter {
23
24
  * @param herb - The Herb backend instance for parsing
24
25
  * @param options - Format options (including rewriters)
25
26
  */
26
- constructor(herb: HerbBackend, options?: FormatOptions);
27
+ constructor(herb: HerbBackend, options?: FormatOptions, parseOptions?: ParseOptions);
27
28
  /**
28
29
  * Format a source string, optionally overriding format options per call.
29
30
  */
@@ -0,0 +1,47 @@
1
+ import { Node, HTMLElementNode } from "@herb-tools/core";
2
+ /**
3
+ * SpacingAnalyzer determines when blank lines should be inserted between
4
+ * sibling elements. It implements the "rule of three" intelligent spacing
5
+ * system: adds spacing between 3+ meaningful siblings, respects semantic
6
+ * groupings, groups comments with following elements, and preserves
7
+ * user-added spacing.
8
+ */
9
+ export declare class SpacingAnalyzer {
10
+ private nodeIsMultiline;
11
+ private tagGroupsCache;
12
+ private allSingleLineCache;
13
+ constructor(nodeIsMultiline: Map<Node, boolean>);
14
+ clear(): void;
15
+ /**
16
+ * Determine if spacing should be added between sibling elements
17
+ *
18
+ * This implements the "rule of three" intelligent spacing system:
19
+ * - Adds spacing between 3 or more meaningful siblings
20
+ * - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
21
+ * - Groups comments with following elements
22
+ * - Preserves user-added spacing
23
+ *
24
+ * @param parentElement - The parent element containing the siblings
25
+ * @param siblings - Array of all sibling nodes
26
+ * @param currentIndex - Index of the current node being evaluated
27
+ * @returns true if spacing should be added before the current element
28
+ */
29
+ shouldAddSpacingBetweenSiblings(parentElement: HTMLElementNode | null, siblings: Node[], currentIndex: number): boolean;
30
+ /**
31
+ * Check if there's a blank line (double newline) in the nodes at the given index
32
+ */
33
+ hasBlankLineBetween(body: Node[], index: number): boolean;
34
+ /**
35
+ * Check if a node will render as multiple lines when formatted.
36
+ */
37
+ private isMultilineElement;
38
+ /**
39
+ * Get a grouping key for a node (tag name for HTML, ERB type for ERB)
40
+ */
41
+ private getGroupingKey;
42
+ /**
43
+ * Detect groups of consecutive same-tag/same-type single-line elements
44
+ * Returns a map of index -> group info for efficient lookup
45
+ */
46
+ private detectTagGroups;
47
+ }
@@ -0,0 +1,22 @@
1
+ import { Node, HTMLElementNode, ERBContentNode } from "@herb-tools/core";
2
+ import type { ContentUnitWithNode } from "./format-helpers.js";
3
+ /**
4
+ * Interface that the delegate must implement to provide
5
+ * rendering capabilities to the TextFlowAnalyzer.
6
+ */
7
+ export interface TextFlowAnalyzerDelegate {
8
+ tryRenderInlineElement(element: HTMLElementNode): string | null;
9
+ renderERBAsString(node: ERBContentNode): string;
10
+ }
11
+ /**
12
+ * TextFlowAnalyzer converts AST nodes into the ContentUnitWithNode[]
13
+ * intermediate representation used by the TextFlowEngine for rendering.
14
+ */
15
+ export declare class TextFlowAnalyzer {
16
+ private delegate;
17
+ constructor(delegate: TextFlowAnalyzerDelegate);
18
+ buildContentUnits(children: Node[]): ContentUnitWithNode[];
19
+ private processTextNode;
20
+ private processInlineElement;
21
+ private processERBContentNode;
22
+ }
@@ -0,0 +1,37 @@
1
+ import { Node, HTMLElementNode } from "@herb-tools/core";
2
+ import type { TextFlowAnalyzerDelegate } from "./text-flow-analyzer.js";
3
+ /**
4
+ * Interface that the FormatPrinter implements to provide
5
+ * rendering capabilities to the TextFlowEngine.
6
+ */
7
+ export interface TextFlowDelegate extends TextFlowAnalyzerDelegate {
8
+ readonly indent: string;
9
+ readonly maxLineLength: number;
10
+ push(line: string): void;
11
+ pushWithIndent(line: string): void;
12
+ renderInlineElementAsString(element: HTMLElementNode): string;
13
+ visit(node: Node): void;
14
+ }
15
+ /**
16
+ * TextFlowEngine handles the formatting of mixed text + inline elements + ERB content.
17
+ *
18
+ * It orchestrates analysis (via TextFlowAnalyzer) and rendering phases:
19
+ * groups adjacent inline elements, and wraps words to fit within line length constraints.
20
+ */
21
+ export declare class TextFlowEngine {
22
+ private delegate;
23
+ private analyzer;
24
+ constructor(delegate: TextFlowDelegate);
25
+ visitTextFlowChildren(children: Node[]): void;
26
+ isInTextFlowContext(children: Node[]): boolean;
27
+ collectTextFlowRun(body: Node[], startIndex: number): {
28
+ nodes: Node[];
29
+ endIndex: number;
30
+ } | null;
31
+ isTextFlowNode(node: Node): boolean;
32
+ private renderAdjacentInlineElements;
33
+ private visitRemainingChildrenAsTextFlow;
34
+ private buildAndWrapTextFlow;
35
+ private flushWords;
36
+ private wrapAndPushWords;
37
+ }
@@ -0,0 +1,58 @@
1
+ import { Node, HTMLTextNode } from "@herb-tools/core";
2
+ import type { ContentUnitWithNode } from "./format-helpers.js";
3
+ /**
4
+ * Check if a node participates in text flow
5
+ */
6
+ export declare function isTextFlowNode(node: Node): boolean;
7
+ /**
8
+ * Check if a node is whitespace that can appear within a text flow run
9
+ */
10
+ export declare function isTextFlowWhitespace(node: Node): boolean;
11
+ /**
12
+ * Collect a run of text flow nodes starting at the given index.
13
+ * Returns the nodes in the run and the index after the last node.
14
+ * Returns null if the run doesn't qualify (needs 2+ text flow nodes with both text and atomic content).
15
+ */
16
+ export declare function collectTextFlowRun(body: Node[], startIndex: number): {
17
+ nodes: Node[];
18
+ endIndex: number;
19
+ } | null;
20
+ /**
21
+ * Check if children represent a text flow context
22
+ * (has text content mixed with inline elements or ERB)
23
+ */
24
+ export declare function isInTextFlowContext(children: Node[]): boolean;
25
+ /**
26
+ * Try to merge text that follows an atomic unit (ERB/inline) with no whitespace.
27
+ * Merges the first word of the text into the preceding atomic unit.
28
+ * Returns true if merge was performed.
29
+ */
30
+ export declare function tryMergeTextAfterAtomic(result: ContentUnitWithNode[], textNode: HTMLTextNode): boolean;
31
+ /**
32
+ * Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace.
33
+ * Splits preceding text, merges last word with atomic content.
34
+ * Returns true if merge was performed.
35
+ */
36
+ export declare function tryMergeAtomicAfterText(result: ContentUnitWithNode[], children: Node[], lastProcessedIndex: number, atomicContent: string, atomicType: 'erb' | 'inline', atomicNode: Node): boolean;
37
+ /**
38
+ * Check if there's whitespace between current node and last processed node
39
+ */
40
+ export declare function hasWhitespaceBeforeNode(children: Node[], lastProcessedIndex: number, currentIndex: number, currentNode: Node): boolean;
41
+ /**
42
+ * Check if last unit in result ends with whitespace
43
+ */
44
+ export declare function lastUnitEndsWithWhitespace(result: ContentUnitWithNode[]): boolean;
45
+ /**
46
+ * Wrap remaining words that don't fit on the current line.
47
+ * Returns the wrapped lines with proper indentation.
48
+ */
49
+ export declare function wrapRemainingWords(words: string[], wrapWidth: number, indent: string): string[];
50
+ /**
51
+ * Try to merge text starting with punctuation to inline content.
52
+ * Returns object with merged content and whether processing should stop.
53
+ */
54
+ export declare function tryMergePunctuationText(inlineContent: string, trimmedText: string, wrapWidth: number, indent: string): {
55
+ mergedContent: string;
56
+ shouldStop: boolean;
57
+ wrappedLines: string[];
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/formatter",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
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,10 +35,10 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@herb-tools/config": "0.8.9",
39
- "@herb-tools/core": "0.8.9",
40
- "@herb-tools/printer": "0.8.9",
41
- "@herb-tools/rewriter": "0.8.9",
38
+ "@herb-tools/config": "0.9.0",
39
+ "@herb-tools/core": "0.9.0",
40
+ "@herb-tools/printer": "0.9.0",
41
+ "@herb-tools/rewriter": "0.9.0",
42
42
  "tinyglobby": "^0.2.15"
43
43
  },
44
44
  "files": [