@herb-tools/formatter 0.8.10 → 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,13 +64,15 @@ 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
78
  * Returns empty string if content is empty, otherwise adds a leading space
@@ -81,56 +87,6 @@ export declare class FormatPrinter extends Printer {
81
87
  * Extract inline nodes (non-attribute, non-whitespace) from a list of nodes
82
88
  */
83
89
  private extractInlineNodes;
84
- /**
85
- * Check if a node will render as multiple lines when formatted.
86
- */
87
- private isMultilineElement;
88
- /**
89
- * Get a grouping key for a node (tag name for HTML, ERB type for ERB)
90
- */
91
- private getGroupingKey;
92
- /**
93
- * Detect groups of consecutive same-tag/same-type single-line elements
94
- * Returns a map of index -> group info for efficient lookup
95
- */
96
- private detectTagGroups;
97
- /**
98
- * Determine if spacing should be added between sibling elements
99
- *
100
- * This implements the "rule of three" intelligent spacing system:
101
- * - Adds spacing between 3 or more meaningful siblings
102
- * - Respects semantic groupings (e.g., ul/li, nav/a stay tight)
103
- * - Groups comments with following elements
104
- * - Preserves user-added spacing
105
- *
106
- * @param parentElement - The parent element containing the siblings
107
- * @param siblings - Array of all sibling nodes
108
- * @param currentIndex - Index of the current node being evaluated
109
- * @param hasExistingSpacing - Whether user-added spacing already exists
110
- * @returns true if spacing should be added before the current element
111
- */
112
- private shouldAddSpacingBetweenSiblings;
113
- /**
114
- * Check if we're currently processing a token list attribute that needs spacing
115
- */
116
- private get isInTokenListAttribute();
117
- /**
118
- * Render attributes as a space-separated string
119
- */
120
- private renderAttributesString;
121
- /**
122
- * Determine if a tag should be rendered inline based on attribute count and other factors
123
- */
124
- private shouldRenderInline;
125
- private wouldClassAttributeBeMultiline;
126
- private getAttributeName;
127
- private getAttributeValue;
128
- private hasMultilineAttributes;
129
- private formatClassAttribute;
130
- private isFormattableAttribute;
131
- private formatMultilineAttribute;
132
- private formatMultilineAttributeValue;
133
- private breakTokensIntoLines;
134
90
  /**
135
91
  * Render multiline attributes for a tag
136
92
  */
@@ -139,20 +95,27 @@ export declare class FormatPrinter extends Printer {
139
95
  * Reconstruct the text representation of an ERB node
140
96
  * @param withFormatting - if true, format the content; if false, preserve original
141
97
  */
142
- private reconstructERBNode;
98
+ reconstructERBNode(node: ERBNode, withFormatting?: boolean): string;
143
99
  /**
144
100
  * Print an ERB tag (<% %> or <%= %>) with single spaces around inner content.
145
101
  */
146
102
  printERBNode(node: ERBNode): void;
147
103
  visitDocumentNode(node: DocumentNode): void;
148
104
  visitHTMLElementNode(node: HTMLElementNode): void;
105
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void;
106
+ visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode): void;
149
107
  visitHTMLElementBody(body: Node[], element: HTMLElementNode): void;
108
+ private visitContentPreservingBody;
109
+ private visitInlineElementBody;
110
+ private stripLeadingHerbDisable;
150
111
  /**
151
112
  * Visit element children with intelligent spacing logic
152
113
  *
153
114
  * Tracks line positions and immediately splices blank lines after rendering each child.
154
115
  */
155
116
  private visitElementChildren;
117
+ private visitTextFlowRunInChildren;
118
+ private visitChildWithTrailingHerbDisable;
156
119
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
157
120
  visitHTMLCloseTagNode(node: HTMLCloseTagNode): void;
158
121
  visitHTMLTextNode(node: HTMLTextNode): void;
@@ -165,6 +128,8 @@ export declare class FormatPrinter extends Printer {
165
128
  visitXMLDeclarationNode(node: XMLDeclarationNode): void;
166
129
  visitCDATANode(node: CDATANode): void;
167
130
  visitERBContentNode(node: ERBContentNode): void;
131
+ visitERBOpenTagNode(node: ERBOpenTagNode): void;
132
+ visitHTMLVirtualCloseTagNode(_node: HTMLVirtualCloseTagNode): void;
168
133
  visitERBEndNode(node: ERBEndNode): void;
169
134
  visitERBYieldNode(node: ERBYieldNode): void;
170
135
  visitERBInNode(node: ERBInNode): void;
@@ -197,101 +162,30 @@ export declare class FormatPrinter extends Printer {
197
162
  * Determines if the close tag should be rendered inline (usually follows content decision)
198
163
  */
199
164
  private shouldRenderCloseTagInline;
165
+ private captureHerbDisableInline;
166
+ private fitsOnCurrentLine;
200
167
  private formatFrontmatter;
201
168
  /**
202
169
  * Append a child node to the last output line
203
170
  */
204
171
  private appendChildToLastLine;
205
- /**
206
- * Visit children in a text flow context (mixed text and inline elements)
207
- * Handles word wrapping and keeps adjacent inline elements together
208
- */
209
- private visitTextFlowChildren;
210
- /**
211
- * Wrap remaining words that don't fit on the current line
212
- * Returns the wrapped lines with proper indentation
213
- */
214
- private wrapRemainingWords;
215
- /**
216
- * Try to merge text starting with punctuation to inline content
217
- * Returns object with merged content and whether processing should stop
218
- */
219
- private tryMergePunctuationText;
220
- /**
221
- * Render adjacent inline elements together on one line
222
- */
223
- private renderAdjacentInlineElements;
224
172
  /**
225
173
  * Render an inline element as a string
226
174
  */
227
- private renderInlineElementAsString;
175
+ renderInlineElementAsString(element: HTMLElementNode): string;
228
176
  /**
229
177
  * Render an ERB node as a string
230
178
  */
231
- private renderERBAsString;
179
+ renderERBAsString(node: ERBContentNode): string;
232
180
  /**
233
- * 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.
234
182
  */
235
- private visitRemainingChildren;
236
- /**
237
- * Build words array from text/inline/ERB and wrap them
238
- */
239
- private buildAndWrapTextFlow;
240
- /**
241
- * Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
242
- * Returns true if merge was performed
243
- */
244
- private tryMergeTextAfterAtomic;
245
- /**
246
- * Try to merge an atomic unit (ERB/inline) with preceding text that has no whitespace
247
- * Returns true if merge was performed
248
- */
249
- private tryMergeAtomicAfterText;
250
- /**
251
- * Check if there's whitespace between current node and last processed node
252
- */
253
- private hasWhitespaceBeforeNode;
254
- /**
255
- * Check if last unit in result ends with whitespace
256
- */
257
- private lastUnitEndsWithWhitespace;
258
- /**
259
- * Process a text node and add it to results (with potential merging)
260
- */
261
- private processTextNode;
262
- /**
263
- * Process an inline element and add it to results (with potential merging)
264
- */
265
- private processInlineElement;
266
- /**
267
- * Process an ERB content node and add it to results (with potential merging)
268
- */
269
- private processERBContentNode;
270
- /**
271
- * Convert AST nodes to content units with node references
272
- */
273
- private buildContentUnitsWithNodes;
274
- /**
275
- * Flush accumulated words to output with wrapping
276
- */
277
- private flushWords;
278
- /**
279
- * Wrap words to fit within line length and push to output
280
- * Handles punctuation spacing intelligently
281
- * Excludes herb:disable comments from line length calculations
282
- */
283
- private wrapAndPushWords;
284
- private isInTextFlowContext;
183
+ tryRenderInlineElement(element: HTMLElementNode): string | null;
285
184
  private renderInlineOpen;
286
- renderAttribute(attribute: HTMLAttributeNode): string;
287
185
  /**
288
186
  * Try to render a complete element inline including opening tag, children, and closing tag
289
187
  */
290
188
  private tryRenderInlineFull;
291
- /**
292
- * Check if children contain a leading herb:disable comment (after optional whitespace)
293
- */
294
- private hasLeadingHerbDisable;
295
189
  /**
296
190
  * Try to render just the children inline (without tags)
297
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.10",
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.10",
39
- "@herb-tools/core": "0.8.10",
40
- "@herb-tools/printer": "0.8.10",
41
- "@herb-tools/rewriter": "0.8.10",
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": [