@herb-tools/formatter 0.8.10 → 0.9.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.
- package/dist/herb-format.js +57800 -16767
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +24404 -3522
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +24404 -3522
- 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 +33 -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 +448 -1459
- 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
package/src/format-helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNode, isERBNode, getTagName, isAnyOf, isERBControlFlowNode, hasERBOutput } from "@herb-tools/core"
|
|
1
|
+
import { isNode, isERBNode, getTagName, isAnyOf, isERBControlFlowNode, hasERBOutput, getStaticAttributeValue, getTokenList, isPureWhitespaceNode } from "@herb-tools/core"
|
|
2
2
|
import { Node, HTMLDoctypeNode, HTMLTextNode, HTMLElementNode, HTMLCommentNode, HTMLOpenTagNode, HTMLCloseTagNode, ERBIfNode, ERBContentNode, WhitespaceNode } from "@herb-tools/core"
|
|
3
3
|
|
|
4
4
|
// --- Types ---
|
|
@@ -34,6 +34,12 @@ export interface ContentUnitWithNode {
|
|
|
34
34
|
|
|
35
35
|
// --- Constants ---
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* ASCII whitespace pattern - use instead of \s to preserve Unicode whitespace
|
|
39
|
+
* characters like NBSP (U+00A0) and full-width space (U+3000)
|
|
40
|
+
*/
|
|
41
|
+
export const ASCII_WHITESPACE = /[ \t\n\r]+/g
|
|
42
|
+
|
|
37
43
|
// TODO: we can probably expand this list with more tags/attributes
|
|
38
44
|
export const FORMATTABLE_ATTRIBUTES: Record<string, string[]> = {
|
|
39
45
|
'*': ['class'],
|
|
@@ -51,37 +57,35 @@ export const CONTENT_PRESERVING_ELEMENTS = new Set([
|
|
|
51
57
|
'script', 'style', 'pre', 'textarea'
|
|
52
58
|
])
|
|
53
59
|
|
|
60
|
+
// https://tailwindcss.com/docs/white-space
|
|
61
|
+
export const WHITESPACE_PRESERVING_CLASSES = [
|
|
62
|
+
'whitespace-pre-line',
|
|
63
|
+
'whitespace-pre-wrap',
|
|
64
|
+
'whitespace-pre',
|
|
65
|
+
'whitespace-break-spaces',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/white-space
|
|
69
|
+
export const WHITESPACE_PRESERVING_STYLE_VALUES = new Set([
|
|
70
|
+
'pre',
|
|
71
|
+
'pre-line',
|
|
72
|
+
'pre-wrap',
|
|
73
|
+
'break-spaces',
|
|
74
|
+
])
|
|
75
|
+
|
|
54
76
|
export const SPACEABLE_CONTAINERS = new Set([
|
|
55
77
|
'div', 'section', 'article', 'main', 'header', 'footer', 'aside',
|
|
56
78
|
'figure', 'details', 'summary', 'dialog', 'fieldset'
|
|
57
79
|
])
|
|
58
80
|
|
|
59
|
-
/**
|
|
60
|
-
* Token list attributes that contain space-separated values and benefit from
|
|
61
|
-
* spacing around ERB content for readability
|
|
62
|
-
*/
|
|
63
|
-
export const TOKEN_LIST_ATTRIBUTES = new Set([
|
|
64
|
-
'class', 'data-controller', 'data-action'
|
|
65
|
-
])
|
|
66
|
-
|
|
67
81
|
|
|
68
82
|
// --- Node Utility Functions ---
|
|
69
83
|
|
|
70
|
-
/**
|
|
71
|
-
* Check if a node is pure whitespace (empty text node with only whitespace)
|
|
72
|
-
*/
|
|
73
|
-
export function isPureWhitespaceNode(node: Node): boolean {
|
|
74
|
-
return isNode(node, HTMLTextNode) && node.content.trim() === ""
|
|
75
|
-
}
|
|
76
|
-
|
|
77
84
|
/**
|
|
78
85
|
* Check if a node is non-whitespace (has meaningful content)
|
|
79
86
|
*/
|
|
80
87
|
export function isNonWhitespaceNode(node: Node): boolean {
|
|
81
|
-
|
|
82
|
-
if (isNode(node, HTMLTextNode)) return node.content.trim() !== ""
|
|
83
|
-
|
|
84
|
-
return true
|
|
88
|
+
return !isPureWhitespaceNode(node)
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
@@ -136,10 +140,9 @@ export function filterEmptyNodesForHerbDisable(nodes: Node[]): Node[] {
|
|
|
136
140
|
let pendingWhitespace: Node | null = null
|
|
137
141
|
|
|
138
142
|
for (const node of nodes) {
|
|
139
|
-
const isWhitespace = isNode(node, WhitespaceNode) || (isNode(node, HTMLTextNode) && node.content.trim() === "")
|
|
140
143
|
const isHerbDisable = isNode(node, ERBContentNode) && isHerbDisableComment(node)
|
|
141
144
|
|
|
142
|
-
if (
|
|
145
|
+
if (isPureWhitespaceNode(node)) {
|
|
143
146
|
if (!pendingWhitespace) {
|
|
144
147
|
pendingWhitespace = node
|
|
145
148
|
}
|
|
@@ -169,7 +172,7 @@ export function isClosingPunctuation(word: string): boolean {
|
|
|
169
172
|
* Check if a line ends with opening punctuation
|
|
170
173
|
*/
|
|
171
174
|
export function lineEndsWithOpeningPunctuation(line: string): boolean {
|
|
172
|
-
return /[(
|
|
175
|
+
return /[([]$/.test(line)
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
/**
|
|
@@ -185,14 +188,14 @@ export function isERBTag(text: string): boolean {
|
|
|
185
188
|
export function endsWithERBTag(text: string): boolean {
|
|
186
189
|
const trimmed = text.trim()
|
|
187
190
|
|
|
188
|
-
return
|
|
191
|
+
return trimmed.endsWith('%>') || /%>\S+$/.test(trimmed)
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
/**
|
|
192
195
|
* Check if a string starts with an ERB tag
|
|
193
196
|
*/
|
|
194
197
|
export function startsWithERBTag(text: string): boolean {
|
|
195
|
-
return
|
|
198
|
+
return text.trim().startsWith('<%')
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
/**
|
|
@@ -391,26 +394,52 @@ export function hasMixedTextAndInlineContent(children: Node[]): boolean {
|
|
|
391
394
|
return (hasText && hasInlineElements) || (hasERBOutput(children) && hasText)
|
|
392
395
|
}
|
|
393
396
|
|
|
397
|
+
export function hasWhitespacePreservingStyle(element: HTMLElementNode): boolean {
|
|
398
|
+
if (getTokenList(element, "class").some(klass => WHITESPACE_PRESERVING_CLASSES.some(whitespace => klass.includes(whitespace)))) return true
|
|
399
|
+
|
|
400
|
+
const styleValue = getStaticAttributeValue(element, "style")
|
|
401
|
+
if (styleValue) {
|
|
402
|
+
const match = styleValue.match(/white-space\s*:\s*([^;!]+)/)
|
|
403
|
+
|
|
404
|
+
if (match) {
|
|
405
|
+
const value = match[1].trim().toLowerCase()
|
|
406
|
+
if (WHITESPACE_PRESERVING_STYLE_VALUES.has(value)) return true
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
394
413
|
export function isContentPreserving(element: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): boolean {
|
|
395
414
|
const tagName = getTagName(element)
|
|
415
|
+
if (CONTENT_PRESERVING_ELEMENTS.has(tagName)) return true
|
|
416
|
+
|
|
417
|
+
if (isNode(element, HTMLElementNode)) {
|
|
418
|
+
return hasWhitespacePreservingStyle(element)
|
|
419
|
+
}
|
|
396
420
|
|
|
397
|
-
return
|
|
421
|
+
return false
|
|
398
422
|
}
|
|
399
423
|
|
|
400
424
|
/**
|
|
401
|
-
* Count consecutive inline elements/ERB
|
|
425
|
+
* Count consecutive inline elements/ERB with no whitespace between them.
|
|
426
|
+
* Starts from startIndex and skips indices in processedIndices.
|
|
402
427
|
*/
|
|
403
|
-
export function countAdjacentInlineElements(children: Node[]): number {
|
|
428
|
+
export function countAdjacentInlineElements(children: Node[], startIndex = 0, processedIndices?: Set<number>): number {
|
|
404
429
|
let count = 0
|
|
405
430
|
let lastSignificantIndex = -1
|
|
406
431
|
|
|
407
|
-
for (let i =
|
|
432
|
+
for (let i = startIndex; i < children.length; i++) {
|
|
408
433
|
const child = children[i]
|
|
409
434
|
|
|
410
435
|
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
|
|
411
436
|
continue
|
|
412
437
|
}
|
|
413
438
|
|
|
439
|
+
if (processedIndices?.has(i)) {
|
|
440
|
+
break
|
|
441
|
+
}
|
|
442
|
+
|
|
414
443
|
const isInlineOrERB = (isNode(child, HTMLElementNode) && isInlineElement(getTagName(child))) || isNode(child, ERBContentNode)
|
|
415
444
|
|
|
416
445
|
if (!isInlineOrERB) {
|
|
@@ -487,6 +516,21 @@ export function isHerbDisableComment(node: Node): boolean {
|
|
|
487
516
|
return trimmed.startsWith("herb:disable")
|
|
488
517
|
}
|
|
489
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Check if children contain a leading herb:disable comment (after optional whitespace)
|
|
521
|
+
*/
|
|
522
|
+
export function hasLeadingHerbDisable(children: Node[]): boolean {
|
|
523
|
+
for (const child of children) {
|
|
524
|
+
if (isNode(child, WhitespaceNode) || (isNode(child, HTMLTextNode) && child.content.trim() === "")) {
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return isNode(child, ERBContentNode) && isHerbDisableComment(child)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
|
|
490
534
|
/**
|
|
491
535
|
* Check if a text node is YAML frontmatter (starts and ends with ---)
|
|
492
536
|
*/
|