@herb-tools/formatter 0.6.0 → 0.7.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 +447 -132
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +372 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +372 -121
- package/dist/index.esm.js.map +1 -1
- package/dist/types/format-printer.d.ts +6 -0
- package/package.json +3 -3
- package/src/cli.ts +4 -2
- package/src/format-printer.ts +80 -50
|
@@ -26,6 +26,7 @@ export declare class FormatPrinter extends Printer {
|
|
|
26
26
|
private elementFormattingAnalysis;
|
|
27
27
|
source: string;
|
|
28
28
|
private static readonly INLINE_ELEMENTS;
|
|
29
|
+
private static readonly CONTENT_PRESERVING_ELEMENTS;
|
|
29
30
|
private static readonly SPACEABLE_CONTAINERS;
|
|
30
31
|
private static readonly TIGHT_GROUP_PARENTS;
|
|
31
32
|
private static readonly TIGHT_GROUP_CHILDREN;
|
|
@@ -58,6 +59,10 @@ export declare class FormatPrinter extends Printer {
|
|
|
58
59
|
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
59
60
|
*/
|
|
60
61
|
private push;
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
64
|
+
*/
|
|
65
|
+
private pushWithIndent;
|
|
61
66
|
private withIndent;
|
|
62
67
|
private get indent();
|
|
63
68
|
/**
|
|
@@ -240,4 +245,5 @@ export declare class FormatPrinter extends Printer {
|
|
|
240
245
|
private filterEmptyNodes;
|
|
241
246
|
private renderElementInline;
|
|
242
247
|
private renderChildrenInline;
|
|
248
|
+
private isContentPreserving;
|
|
243
249
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/formatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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,8 +35,8 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@herb-tools/core": "0.
|
|
39
|
-
"@herb-tools/printer": "0.
|
|
38
|
+
"@herb-tools/core": "0.7.0",
|
|
39
|
+
"@herb-tools/printer": "0.7.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"glob": "^11.0.3"
|
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join, resolve } from "path"
|
|
|
6
6
|
import { Herb } from "@herb-tools/node-wasm"
|
|
7
7
|
import { Formatter } from "./formatter.js"
|
|
8
8
|
|
|
9
|
-
import { name, version } from "../package.json"
|
|
9
|
+
import { name, version, dependencies } from "../package.json"
|
|
10
10
|
|
|
11
11
|
const pluralize = (count: number, singular: string, plural: string = singular + 's'): string => {
|
|
12
12
|
return count === 1 ? singular : plural
|
|
@@ -52,7 +52,9 @@ export class CLI {
|
|
|
52
52
|
|
|
53
53
|
if (args.includes("--version") || args.includes("-v")) {
|
|
54
54
|
console.log("Versions:")
|
|
55
|
-
console.log(` ${name}@${version}
|
|
55
|
+
console.log(` ${name}@${version}`)
|
|
56
|
+
console.log(` @herb-tools/printer@${dependencies['@herb-tools/printer']}`)
|
|
57
|
+
console.log(` ${Herb.version}`.split(", ").join("\n "))
|
|
56
58
|
|
|
57
59
|
process.exit(0)
|
|
58
60
|
}
|
package/src/format-printer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dedent from "dedent"
|
|
1
2
|
import {
|
|
2
3
|
getTagName,
|
|
3
4
|
getCombinedAttributeName,
|
|
@@ -105,13 +106,18 @@ export class FormatPrinter extends Printer {
|
|
|
105
106
|
'tt', 'var', 'del', 'ins', 'mark', 's', 'u', 'time', 'wbr'
|
|
106
107
|
])
|
|
107
108
|
|
|
109
|
+
private static readonly CONTENT_PRESERVING_ELEMENTS = new Set([
|
|
110
|
+
'script', 'style', 'pre', 'textarea'
|
|
111
|
+
])
|
|
112
|
+
|
|
108
113
|
private static readonly SPACEABLE_CONTAINERS = new Set([
|
|
109
114
|
'div', 'section', 'article', 'main', 'header', 'footer', 'aside',
|
|
110
115
|
'figure', 'details', 'summary', 'dialog', 'fieldset'
|
|
111
116
|
])
|
|
112
117
|
|
|
113
118
|
private static readonly TIGHT_GROUP_PARENTS = new Set([
|
|
114
|
-
'ul', 'ol', 'nav', 'select', 'datalist', 'optgroup', 'tr', 'thead',
|
|
119
|
+
'ul', 'ol', 'nav', 'select', 'datalist', 'optgroup', 'tr', 'thead',
|
|
120
|
+
'tbody', 'tfoot'
|
|
115
121
|
])
|
|
116
122
|
|
|
117
123
|
private static readonly TIGHT_GROUP_CHILDREN = new Set([
|
|
@@ -229,6 +235,15 @@ export class FormatPrinter extends Printer {
|
|
|
229
235
|
this.lines.push(line)
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @deprecated refactor to use @herb-tools/printer infrastructre (or rework printer use push and this.lines)
|
|
240
|
+
*/
|
|
241
|
+
private pushWithIndent(line: string) {
|
|
242
|
+
const indent = line.trim() === "" ? "" : this.indent
|
|
243
|
+
|
|
244
|
+
this.push(indent + line)
|
|
245
|
+
}
|
|
246
|
+
|
|
232
247
|
private withIndent<T>(callback: () => T): T {
|
|
233
248
|
this.indentLevel++
|
|
234
249
|
const result = callback()
|
|
@@ -490,21 +505,16 @@ export class FormatPrinter extends Printer {
|
|
|
490
505
|
}
|
|
491
506
|
|
|
492
507
|
private getAttributeValue(attribute: HTMLAttributeNode): string {
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
if (isNode(child, HTMLTextNode)) {
|
|
496
|
-
return child.content
|
|
497
|
-
}
|
|
498
|
-
return IdentityPrinter.print(child)
|
|
499
|
-
}).join('')
|
|
500
|
-
return content
|
|
508
|
+
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
509
|
+
return attribute.value.children.map(child => isNode(child, HTMLTextNode) ? child.content : IdentityPrinter.print(child)).join('')
|
|
501
510
|
}
|
|
511
|
+
|
|
502
512
|
return ''
|
|
503
513
|
}
|
|
504
514
|
|
|
505
515
|
private hasMultilineAttributes(attributes: HTMLAttributeNode[]): boolean {
|
|
506
516
|
return attributes.some(attribute => {
|
|
507
|
-
if (
|
|
517
|
+
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
508
518
|
const content = getCombinedStringFromNodes(attribute.value.children)
|
|
509
519
|
|
|
510
520
|
if (/\r?\n/.test(content)) {
|
|
@@ -619,12 +629,12 @@ export class FormatPrinter extends Printer {
|
|
|
619
629
|
* Render multiline attributes for a tag
|
|
620
630
|
*/
|
|
621
631
|
private renderMultilineAttributes(tagName: string, allChildren: Node[] = [], isSelfClosing: boolean = false,) {
|
|
622
|
-
this.
|
|
632
|
+
this.pushWithIndent(`<${tagName}`)
|
|
623
633
|
|
|
624
634
|
this.withIndent(() => {
|
|
625
635
|
allChildren.forEach(child => {
|
|
626
636
|
if (isNode(child, HTMLAttributeNode)) {
|
|
627
|
-
this.
|
|
637
|
+
this.pushWithIndent(this.renderAttribute(child))
|
|
628
638
|
} else if (!isNode(child, WhitespaceNode)) {
|
|
629
639
|
this.visit(child)
|
|
630
640
|
}
|
|
@@ -632,9 +642,9 @@ export class FormatPrinter extends Printer {
|
|
|
632
642
|
})
|
|
633
643
|
|
|
634
644
|
if (isSelfClosing) {
|
|
635
|
-
this.
|
|
645
|
+
this.pushWithIndent("/>")
|
|
636
646
|
} else {
|
|
637
|
-
this.
|
|
647
|
+
this.pushWithIndent(">")
|
|
638
648
|
}
|
|
639
649
|
}
|
|
640
650
|
|
|
@@ -719,8 +729,13 @@ export class FormatPrinter extends Printer {
|
|
|
719
729
|
}
|
|
720
730
|
|
|
721
731
|
visitHTMLElementBody(body: Node[], element: HTMLElementNode) {
|
|
722
|
-
|
|
732
|
+
if (this.isContentPreserving(element)) {
|
|
733
|
+
element.body.map(child => this.pushToLastLine(IdentityPrinter.print(child)))
|
|
723
734
|
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const analysis = this.elementFormattingAnalysis.get(element)
|
|
724
739
|
const hasTextFlow = this.isInTextFlowContext(null, body)
|
|
725
740
|
const children = this.filterSignificantChildren(body, hasTextFlow)
|
|
726
741
|
|
|
@@ -881,7 +896,7 @@ export class FormatPrinter extends Printer {
|
|
|
881
896
|
if (this.currentElement && closeTagInline) {
|
|
882
897
|
this.pushToLastLine(closingTag)
|
|
883
898
|
} else {
|
|
884
|
-
this.
|
|
899
|
+
this.pushWithIndent(closingTag)
|
|
885
900
|
}
|
|
886
901
|
}
|
|
887
902
|
|
|
@@ -921,15 +936,15 @@ export class FormatPrinter extends Printer {
|
|
|
921
936
|
}
|
|
922
937
|
|
|
923
938
|
visitHTMLAttributeNode(node: HTMLAttributeNode) {
|
|
924
|
-
this.
|
|
939
|
+
this.pushWithIndent(this.renderAttribute(node))
|
|
925
940
|
}
|
|
926
941
|
|
|
927
942
|
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode) {
|
|
928
|
-
this.
|
|
943
|
+
this.pushWithIndent(getCombinedAttributeName(node))
|
|
929
944
|
}
|
|
930
945
|
|
|
931
946
|
visitHTMLAttributeValueNode(node: HTMLAttributeValueNode) {
|
|
932
|
-
this.
|
|
947
|
+
this.pushWithIndent(IdentityPrinter.print(node))
|
|
933
948
|
}
|
|
934
949
|
|
|
935
950
|
// TODO: rework
|
|
@@ -941,9 +956,9 @@ export class FormatPrinter extends Printer {
|
|
|
941
956
|
|
|
942
957
|
if (node.children && node.children.length > 0) {
|
|
943
958
|
inner = node.children.map(child => {
|
|
944
|
-
if (isNode(child, HTMLTextNode) ||
|
|
959
|
+
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
945
960
|
return child.content
|
|
946
|
-
} else if (isERBNode(child) ||
|
|
961
|
+
} else if (isERBNode(child) || isNode(child, ERBContentNode)) {
|
|
947
962
|
return this.reconstructERBNode(child, false)
|
|
948
963
|
} else {
|
|
949
964
|
return ""
|
|
@@ -993,44 +1008,53 @@ export class FormatPrinter extends Printer {
|
|
|
993
1008
|
inner = ""
|
|
994
1009
|
}
|
|
995
1010
|
|
|
996
|
-
this.
|
|
1011
|
+
this.pushWithIndent(open + inner + close)
|
|
997
1012
|
}
|
|
998
1013
|
|
|
999
1014
|
visitERBCommentNode(node: ERBContentNode) {
|
|
1000
|
-
const open = node.tag_opening?.value
|
|
1001
|
-
const
|
|
1015
|
+
const open = node.tag_opening?.value || "<%#"
|
|
1016
|
+
const content = node?.content?.value || ""
|
|
1017
|
+
const close = node.tag_closing?.value || "%>"
|
|
1002
1018
|
|
|
1003
|
-
|
|
1019
|
+
const contentLines = content.split("\n")
|
|
1020
|
+
const contentTrimmedLines = content.trim().split("\n")
|
|
1004
1021
|
|
|
1005
|
-
if (
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1022
|
+
if (contentLines.length === 1 && contentTrimmedLines.length === 1) {
|
|
1023
|
+
const startsWithSpace = content[0] === " "
|
|
1024
|
+
const before = startsWithSpace ? "" : " "
|
|
1008
1025
|
|
|
1009
|
-
|
|
1010
|
-
const childIndent = this.indent + " ".repeat(this.indentWidth)
|
|
1011
|
-
const innerLines = lines.slice(1, -1).map(line => childIndent + line.trim())
|
|
1026
|
+
this.pushWithIndent(open + before + content.trimEnd() + ' ' + close)
|
|
1012
1027
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (contentTrimmedLines.length === 1) {
|
|
1032
|
+
this.pushWithIndent(open + ' ' + content.trim() + ' ' + close)
|
|
1033
|
+
return
|
|
1019
1034
|
}
|
|
1020
1035
|
|
|
1021
|
-
|
|
1036
|
+
const firstLineEmpty = contentLines[0].trim() === ""
|
|
1037
|
+
const dedentedContent = dedent(firstLineEmpty ? content : content.trimStart())
|
|
1038
|
+
|
|
1039
|
+
this.pushWithIndent(open)
|
|
1040
|
+
|
|
1041
|
+
this.withIndent(() => {
|
|
1042
|
+
dedentedContent.split("\n").forEach(line => this.pushWithIndent(line))
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
this.pushWithIndent(close)
|
|
1022
1046
|
}
|
|
1023
1047
|
|
|
1024
1048
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode) {
|
|
1025
|
-
this.
|
|
1049
|
+
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1026
1050
|
}
|
|
1027
1051
|
|
|
1028
1052
|
visitXMLDeclarationNode(node: XMLDeclarationNode) {
|
|
1029
|
-
this.
|
|
1053
|
+
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1030
1054
|
}
|
|
1031
1055
|
|
|
1032
1056
|
visitCDATANode(node: CDATANode) {
|
|
1033
|
-
this.
|
|
1057
|
+
this.pushWithIndent(IdentityPrinter.print(node))
|
|
1034
1058
|
}
|
|
1035
1059
|
|
|
1036
1060
|
visitERBContentNode(node: ERBContentNode) {
|
|
@@ -1202,7 +1226,7 @@ export class FormatPrinter extends Printer {
|
|
|
1202
1226
|
* Determines if the open tag should be rendered inline
|
|
1203
1227
|
*/
|
|
1204
1228
|
private shouldRenderOpenTagInline(node: HTMLElementNode): boolean {
|
|
1205
|
-
const children = node.open_tag?.children ||
|
|
1229
|
+
const children = node.open_tag?.children || []
|
|
1206
1230
|
const attributes = filterNodes(children, HTMLAttributeNode)
|
|
1207
1231
|
const inlineNodes = this.extractInlineNodes(children)
|
|
1208
1232
|
const hasERBControlFlow = inlineNodes.some(node => isERBControlFlowNode(node)) || children.some(node => isERBControlFlowNode(node))
|
|
@@ -1299,9 +1323,9 @@ export class FormatPrinter extends Printer {
|
|
|
1299
1323
|
* Determines if the close tag should be rendered inline (usually follows content decision)
|
|
1300
1324
|
*/
|
|
1301
1325
|
private shouldRenderCloseTagInline(node: HTMLElementNode, elementContentInline: boolean): boolean {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
if (
|
|
1326
|
+
if (node.is_void) return true
|
|
1327
|
+
if (node.open_tag?.tag_closing?.value === "/>") return true
|
|
1328
|
+
if (this.isContentPreserving(node)) return true
|
|
1305
1329
|
|
|
1306
1330
|
const children = this.filterSignificantChildren(node.body, this.isInTextFlowContext(null, node.body))
|
|
1307
1331
|
|
|
@@ -1379,7 +1403,7 @@ export class FormatPrinter extends Printer {
|
|
|
1379
1403
|
}
|
|
1380
1404
|
} else {
|
|
1381
1405
|
if (currentLineContent.trim()) {
|
|
1382
|
-
this.
|
|
1406
|
+
this.pushWithIndent(currentLineContent.trim())
|
|
1383
1407
|
currentLineContent = ""
|
|
1384
1408
|
}
|
|
1385
1409
|
|
|
@@ -1387,7 +1411,7 @@ export class FormatPrinter extends Printer {
|
|
|
1387
1411
|
}
|
|
1388
1412
|
} else {
|
|
1389
1413
|
if (currentLineContent.trim()) {
|
|
1390
|
-
this.
|
|
1414
|
+
this.pushWithIndent(currentLineContent.trim())
|
|
1391
1415
|
currentLineContent = ""
|
|
1392
1416
|
}
|
|
1393
1417
|
|
|
@@ -1418,7 +1442,7 @@ export class FormatPrinter extends Printer {
|
|
|
1418
1442
|
}
|
|
1419
1443
|
} else {
|
|
1420
1444
|
if (currentLineContent.trim()) {
|
|
1421
|
-
this.
|
|
1445
|
+
this.pushWithIndent(currentLineContent.trim())
|
|
1422
1446
|
currentLineContent = ""
|
|
1423
1447
|
}
|
|
1424
1448
|
|
|
@@ -1523,7 +1547,7 @@ export class FormatPrinter extends Printer {
|
|
|
1523
1547
|
|
|
1524
1548
|
let value = ""
|
|
1525
1549
|
|
|
1526
|
-
if (
|
|
1550
|
+
if (isNode(attribute.value, HTMLAttributeValueNode)) {
|
|
1527
1551
|
const attributeValue = attribute.value
|
|
1528
1552
|
|
|
1529
1553
|
let open_quote = attributeValue.open_quote?.value ?? ""
|
|
@@ -1531,7 +1555,7 @@ export class FormatPrinter extends Printer {
|
|
|
1531
1555
|
let htmlTextContent = ""
|
|
1532
1556
|
|
|
1533
1557
|
const content = attributeValue.children.map((child: Node) => {
|
|
1534
|
-
if (isNode(child, HTMLTextNode) ||
|
|
1558
|
+
if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
|
|
1535
1559
|
htmlTextContent += child.content
|
|
1536
1560
|
|
|
1537
1561
|
return child.content
|
|
@@ -1819,4 +1843,10 @@ export class FormatPrinter extends Printer {
|
|
|
1819
1843
|
|
|
1820
1844
|
return content.replace(/\s+/g, ' ').trim()
|
|
1821
1845
|
}
|
|
1846
|
+
|
|
1847
|
+
private isContentPreserving(element: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): boolean {
|
|
1848
|
+
const tagName = getTagName(element)
|
|
1849
|
+
|
|
1850
|
+
return FormatPrinter.CONTENT_PRESERVING_ELEMENTS.has(tagName)
|
|
1851
|
+
}
|
|
1822
1852
|
}
|