@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.
@@ -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.6.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.6.0",
39
- "@herb-tools/printer": "0.6.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}, ${Herb.version}`.split(", ").join("\n "))
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
  }
@@ -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', 'tbody', 'tfoot'
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 (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
494
- const content = attribute.value.children.map(child => {
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 (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
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.push(this.indent + `<${tagName}`)
632
+ this.pushWithIndent(`<${tagName}`)
623
633
 
624
634
  this.withIndent(() => {
625
635
  allChildren.forEach(child => {
626
636
  if (isNode(child, HTMLAttributeNode)) {
627
- this.push(this.indent + this.renderAttribute(child))
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.push(this.indent + "/>")
645
+ this.pushWithIndent("/>")
636
646
  } else {
637
- this.push(this.indent + ">")
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
- const analysis = this.elementFormattingAnalysis.get(element)
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.push(this.indent + closingTag)
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.push(this.indent + this.renderAttribute(node))
939
+ this.pushWithIndent(this.renderAttribute(node))
925
940
  }
926
941
 
927
942
  visitHTMLAttributeNameNode(node: HTMLAttributeNameNode) {
928
- this.push(this.indent + getCombinedAttributeName(node))
943
+ this.pushWithIndent(getCombinedAttributeName(node))
929
944
  }
930
945
 
931
946
  visitHTMLAttributeValueNode(node: HTMLAttributeValueNode) {
932
- this.push(this.indent + IdentityPrinter.print(node))
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) || isNode(child, LiteralNode)) {
959
+ if (isNode(child, HTMLTextNode) || isNode(child, LiteralNode)) {
945
960
  return child.content
946
- } else if (isERBNode(child) || isNode(child, ERBContentNode)) {
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.push(this.indent + open + inner + close)
1011
+ this.pushWithIndent(open + inner + close)
997
1012
  }
998
1013
 
999
1014
  visitERBCommentNode(node: ERBContentNode) {
1000
- const open = node.tag_opening?.value ?? ""
1001
- const close = node.tag_closing?.value ?? ""
1015
+ const open = node.tag_opening?.value || "<%#"
1016
+ const content = node?.content?.value || ""
1017
+ const close = node.tag_closing?.value || "%>"
1002
1018
 
1003
- let inner: string
1019
+ const contentLines = content.split("\n")
1020
+ const contentTrimmedLines = content.trim().split("\n")
1004
1021
 
1005
- if (node.content && node.content.value) {
1006
- const rawInner = node.content.value
1007
- const lines = rawInner.split("\n")
1022
+ if (contentLines.length === 1 && contentTrimmedLines.length === 1) {
1023
+ const startsWithSpace = content[0] === " "
1024
+ const before = startsWithSpace ? "" : " "
1008
1025
 
1009
- if (lines.length > 2) {
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
- inner = "\n" + innerLines.join("\n") + "\n"
1014
- } else {
1015
- inner = ` ${rawInner.trim()} `
1016
- }
1017
- } else {
1018
- inner = ""
1028
+ return
1029
+ }
1030
+
1031
+ if (contentTrimmedLines.length === 1) {
1032
+ this.pushWithIndent(open + ' ' + content.trim() + ' ' + close)
1033
+ return
1019
1034
  }
1020
1035
 
1021
- this.push(this.indent + open + inner + close)
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.push(this.indent + IdentityPrinter.print(node))
1049
+ this.pushWithIndent(IdentityPrinter.print(node))
1026
1050
  }
1027
1051
 
1028
1052
  visitXMLDeclarationNode(node: XMLDeclarationNode) {
1029
- this.push(this.indent + IdentityPrinter.print(node))
1053
+ this.pushWithIndent(IdentityPrinter.print(node))
1030
1054
  }
1031
1055
 
1032
1056
  visitCDATANode(node: CDATANode) {
1033
- this.push(this.indent + IdentityPrinter.print(node))
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
- const isSelfClosing = node.open_tag?.tag_closing?.value === "/>"
1303
-
1304
- if (isSelfClosing || node.is_void) return true
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.push(this.indent + currentLineContent.trim())
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.push(this.indent + currentLineContent.trim())
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.push(this.indent + currentLineContent.trim())
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 (attribute.value && isNode(attribute.value, HTMLAttributeValueNode)) {
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) || isNode(child, LiteralNode)) {
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
  }