@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.
- package/dist/herb-format.js +57938 -17957
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +23560 -3225
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +23560 -3225
- package/dist/index.esm.js.map +1 -1
- package/dist/types/attribute-renderer.d.ts +44 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/comment-helpers.d.ts +45 -0
- package/dist/types/format-helpers.d.ts +15 -11
- package/dist/types/format-printer.d.ts +31 -137
- package/dist/types/formatter.d.ts +3 -2
- package/dist/types/spacing-analyzer.d.ts +47 -0
- package/dist/types/text-flow-analyzer.d.ts +22 -0
- package/dist/types/text-flow-engine.d.ts +37 -0
- package/dist/types/text-flow-helpers.d.ts +58 -0
- package/package.json +5 -5
- package/src/attribute-renderer.ts +309 -0
- package/src/cli.ts +32 -11
- package/src/comment-helpers.ts +129 -0
- package/src/format-helpers.ts +73 -29
- package/src/format-printer.ts +447 -1468
- package/src/formatter.ts +10 -4
- package/src/spacing-analyzer.ts +244 -0
- package/src/text-flow-analyzer.ts +212 -0
- package/src/text-flow-engine.ts +311 -0
- package/src/text-flow-helpers.ts +319 -0
|
@@ -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
|
+
}
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
26
|
+
private inContentPreservingContext;
|
|
27
|
+
private inConditionalOpenTagContext;
|
|
25
28
|
private elementStack;
|
|
26
29
|
private elementFormattingAnalysis;
|
|
27
30
|
private nodeIsMultiline;
|
|
28
31
|
private stringLineCount;
|
|
29
|
-
private
|
|
30
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
pushWithIndent(line: string): void;
|
|
68
72
|
private withIndent;
|
|
69
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
renderInlineElementAsString(element: HTMLElementNode): string;
|
|
228
176
|
/**
|
|
229
177
|
* Render an ERB node as a string
|
|
230
178
|
*/
|
|
231
|
-
|
|
179
|
+
renderERBAsString(node: ERBContentNode): string;
|
|
232
180
|
/**
|
|
233
|
-
*
|
|
181
|
+
* Try to render an inline element, returning the full inline string or null if it can't be inlined.
|
|
234
182
|
*/
|
|
235
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"@herb-tools/core": "0.
|
|
40
|
-
"@herb-tools/printer": "0.
|
|
41
|
-
"@herb-tools/rewriter": "0.
|
|
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": [
|