@herb-tools/formatter 0.4.0 → 0.4.2

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/index.esm.js CHANGED
@@ -129,7 +129,7 @@ class Token {
129
129
  }
130
130
 
131
131
  // NOTE: This file is generated by the templates/template.rb script and should not
132
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/errors.ts.erb
132
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/errors.ts.erb
133
133
  class HerbError {
134
134
  type;
135
135
  message;
@@ -579,7 +579,7 @@ function convertToUTF8(string) {
579
579
  }
580
580
 
581
581
  // NOTE: This file is generated by the templates/template.rb script and should not
582
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/nodes.ts.erb
582
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/nodes.ts.erb
583
583
  class Node {
584
584
  type;
585
585
  location;
@@ -2574,7 +2574,7 @@ function fromSerializedNode(node) {
2574
2574
  }
2575
2575
 
2576
2576
  // NOTE: This file is generated by the templates/template.rb script and should not
2577
- // be modified manually. See /Users/marcoroth/Development/herb-release/templates/javascript/packages/core/src/visitor.ts.erb
2577
+ // be modified manually. See /Users/marcoroth/Development/herb-release-6/templates/javascript/packages/core/src/visitor.ts.erb
2578
2578
  class Visitor {
2579
2579
  visit(node) {
2580
2580
  if (!node)
@@ -2689,6 +2689,8 @@ class Printer extends Visitor {
2689
2689
  source;
2690
2690
  lines = [];
2691
2691
  indentLevel = 0;
2692
+ inlineMode = false;
2693
+ isInComplexNesting = false;
2692
2694
  constructor(source, options) {
2693
2695
  super();
2694
2696
  this.source = source;
@@ -2702,6 +2704,7 @@ class Printer extends Visitor {
2702
2704
  const node = object;
2703
2705
  this.lines = [];
2704
2706
  this.indentLevel = indentLevel;
2707
+ this.isInComplexNesting = false; // Reset for each top-level element
2705
2708
  if (typeof node.accept === 'function') {
2706
2709
  node.accept(this);
2707
2710
  }
@@ -2751,6 +2754,8 @@ class Printer extends Visitor {
2751
2754
  const tagName = open.tag_name?.value ?? "";
2752
2755
  const indent = this.indent();
2753
2756
  const attributes = open.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
2757
+ const inlineNodes = open.children.filter(child => !(child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') &&
2758
+ !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE'));
2754
2759
  const children = node.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
2755
2760
  !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
2756
2761
  const hasClosing = open.tag_closing?.value === ">" || open.tag_closing?.value === "/>";
@@ -2759,7 +2764,7 @@ class Printer extends Visitor {
2759
2764
  this.push(indent + `<${tagName}`);
2760
2765
  return;
2761
2766
  }
2762
- if (attributes.length === 0) {
2767
+ if (attributes.length === 0 && inlineNodes.length === 0) {
2763
2768
  if (children.length === 0) {
2764
2769
  if (isSelfClosing) {
2765
2770
  this.push(indent + `<${tagName} />`);
@@ -2772,6 +2777,28 @@ class Printer extends Visitor {
2772
2777
  }
2773
2778
  return;
2774
2779
  }
2780
+ if (children.length >= 1) {
2781
+ if (this.isInComplexNesting) {
2782
+ if (children.length === 1) {
2783
+ const child = children[0];
2784
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
2785
+ const textContent = child.content.trim();
2786
+ const singleLine = `<${tagName}>${textContent}</${tagName}>`;
2787
+ if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
2788
+ this.push(indent + singleLine);
2789
+ return;
2790
+ }
2791
+ }
2792
+ }
2793
+ }
2794
+ else {
2795
+ const inlineResult = this.tryRenderInline(children, tagName);
2796
+ if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
2797
+ this.push(indent + inlineResult);
2798
+ return;
2799
+ }
2800
+ }
2801
+ }
2775
2802
  this.push(indent + `<${tagName}>`);
2776
2803
  this.withIndent(() => {
2777
2804
  children.forEach(child => this.visit(child));
@@ -2781,14 +2808,40 @@ class Printer extends Visitor {
2781
2808
  }
2782
2809
  return;
2783
2810
  }
2784
- const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing);
2811
+ if (attributes.length === 0 && inlineNodes.length > 0) {
2812
+ const inline = this.renderInlineOpen(tagName, [], isSelfClosing, inlineNodes, open.children);
2813
+ if (children.length === 0) {
2814
+ if (isSelfClosing || node.is_void) {
2815
+ this.push(indent + inline);
2816
+ }
2817
+ else {
2818
+ this.push(indent + inline + `</${tagName}>`);
2819
+ }
2820
+ return;
2821
+ }
2822
+ this.push(indent + inline);
2823
+ this.withIndent(() => {
2824
+ children.forEach(child => this.visit(child));
2825
+ });
2826
+ if (!node.is_void && !isSelfClosing) {
2827
+ this.push(indent + `</${tagName}>`);
2828
+ }
2829
+ return;
2830
+ }
2831
+ const inline = this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children);
2785
2832
  const singleAttribute = attributes[0];
2786
- const hasEmptyValue = singleAttribute &&
2833
+ singleAttribute &&
2787
2834
  (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2788
2835
  singleAttribute.value?.children.length === 0;
2789
- const shouldKeepInline = attributes.length <= 3 &&
2790
- !hasEmptyValue &&
2791
- inline.length + indent.length <= this.maxLineLength;
2836
+ const hasERBControlFlow = inlineNodes.some(node => node instanceof ERBIfNode || node.type === 'AST_ERB_IF_NODE' ||
2837
+ node instanceof ERBUnlessNode || node.type === 'AST_ERB_UNLESS_NODE' ||
2838
+ node instanceof ERBBlockNode || node.type === 'AST_ERB_BLOCK_NODE' ||
2839
+ node instanceof ERBCaseNode || node.type === 'AST_ERB_CASE_NODE' ||
2840
+ node instanceof ERBWhileNode || node.type === 'AST_ERB_WHILE_NODE' ||
2841
+ node instanceof ERBForNode || node.type === 'AST_ERB_FOR_NODE');
2842
+ const shouldKeepInline = (attributes.length <= 3 &&
2843
+ inline.length + indent.length <= this.maxLineLength) ||
2844
+ (inlineNodes.length > 0 && !hasERBControlFlow);
2792
2845
  if (shouldKeepInline) {
2793
2846
  if (children.length === 0) {
2794
2847
  if (isSelfClosing) {
@@ -2816,27 +2869,67 @@ class Printer extends Visitor {
2816
2869
  }
2817
2870
  return;
2818
2871
  }
2819
- this.push(indent + `<${tagName}`);
2820
- this.withIndent(() => {
2821
- attributes.forEach(attribute => {
2822
- this.push(this.indent() + this.renderAttribute(attribute));
2872
+ if (inlineNodes.length > 0 && hasERBControlFlow) {
2873
+ this.push(indent + `<${tagName}`);
2874
+ this.withIndent(() => {
2875
+ open.children.forEach(child => {
2876
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
2877
+ this.push(this.indent() + this.renderAttribute(child));
2878
+ }
2879
+ else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
2880
+ this.visit(child);
2881
+ }
2882
+ });
2823
2883
  });
2824
- });
2825
- if (isSelfClosing) {
2826
- this.push(indent + "/>");
2827
- }
2828
- else if (node.is_void) {
2829
- this.push(indent + ">");
2884
+ if (isSelfClosing) {
2885
+ this.push(indent + "/>");
2886
+ }
2887
+ else if (node.is_void) {
2888
+ this.push(indent + ">");
2889
+ }
2890
+ else if (children.length === 0) {
2891
+ this.push(indent + ">" + `</${tagName}>`);
2892
+ }
2893
+ else {
2894
+ this.push(indent + ">");
2895
+ this.withIndent(() => {
2896
+ children.forEach(child => this.visit(child));
2897
+ });
2898
+ this.push(indent + `</${tagName}>`);
2899
+ }
2830
2900
  }
2831
- else if (children.length === 0) {
2832
- this.push(indent + ">" + `</${tagName}>`);
2901
+ else if (inlineNodes.length > 0) {
2902
+ this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children));
2903
+ if (!isSelfClosing && !node.is_void && children.length > 0) {
2904
+ this.withIndent(() => {
2905
+ children.forEach(child => this.visit(child));
2906
+ });
2907
+ this.push(indent + `</${tagName}>`);
2908
+ }
2833
2909
  }
2834
2910
  else {
2835
- this.push(indent + ">");
2911
+ this.push(indent + `<${tagName}`);
2836
2912
  this.withIndent(() => {
2837
- children.forEach(child => this.visit(child));
2913
+ attributes.forEach(attribute => {
2914
+ this.push(this.indent() + this.renderAttribute(attribute));
2915
+ });
2838
2916
  });
2839
- this.push(indent + `</${tagName}>`);
2917
+ if (isSelfClosing) {
2918
+ this.push(indent + "/>");
2919
+ }
2920
+ else if (node.is_void) {
2921
+ this.push(indent + ">");
2922
+ }
2923
+ else if (children.length === 0) {
2924
+ this.push(indent + ">" + `</${tagName}>`);
2925
+ }
2926
+ else {
2927
+ this.push(indent + ">");
2928
+ this.withIndent(() => {
2929
+ children.forEach(child => this.visit(child));
2930
+ });
2931
+ this.push(indent + `</${tagName}>`);
2932
+ }
2840
2933
  }
2841
2934
  }
2842
2935
  visitHTMLOpenTagNode(node) {
@@ -2867,11 +2960,10 @@ class Printer extends Visitor {
2867
2960
  const attributes = node.attributes.filter((attribute) => attribute instanceof HTMLAttributeNode || attribute.type === 'AST_HTML_ATTRIBUTE_NODE');
2868
2961
  const inline = this.renderInlineOpen(tagName, attributes, true);
2869
2962
  const singleAttribute = attributes[0];
2870
- const hasEmptyValue = singleAttribute &&
2963
+ singleAttribute &&
2871
2964
  (singleAttribute.value instanceof HTMLAttributeValueNode || singleAttribute.value?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
2872
2965
  singleAttribute.value?.children.length === 0;
2873
2966
  const shouldKeepInline = attributes.length <= 3 &&
2874
- !hasEmptyValue &&
2875
2967
  inline.length + indent.length <= this.maxLineLength;
2876
2968
  if (shouldKeepInline) {
2877
2969
  this.push(indent + inline);
@@ -3044,15 +3136,50 @@ class Printer extends Visitor {
3044
3136
  }
3045
3137
  }
3046
3138
  visitERBIfNode(node) {
3047
- this.printERBNode(node);
3048
- this.withIndent(() => {
3049
- node.statements.forEach(child => this.visit(child));
3050
- });
3051
- if (node.subsequent) {
3052
- this.visit(node.subsequent);
3139
+ if (this.inlineMode) {
3140
+ const open = node.tag_opening?.value ?? "";
3141
+ const content = node.content?.value ?? "";
3142
+ const close = node.tag_closing?.value ?? "";
3143
+ this.lines.push(open + content + close);
3144
+ if (node.statements.length > 0) {
3145
+ this.lines.push(" ");
3146
+ }
3147
+ node.statements.forEach((child, index) => {
3148
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3149
+ this.lines.push(this.renderAttribute(child));
3150
+ }
3151
+ else {
3152
+ this.visit(child);
3153
+ }
3154
+ if (index < node.statements.length - 1) {
3155
+ this.lines.push(" ");
3156
+ }
3157
+ });
3158
+ if (node.statements.length > 0) {
3159
+ this.lines.push(" ");
3160
+ }
3161
+ if (node.subsequent) {
3162
+ this.visit(node.subsequent);
3163
+ }
3164
+ if (node.end_node) {
3165
+ const endNode = node.end_node;
3166
+ const endOpen = endNode.tag_opening?.value ?? "";
3167
+ const endContent = endNode.content?.value ?? "";
3168
+ const endClose = endNode.tag_closing?.value ?? "";
3169
+ this.lines.push(endOpen + endContent + endClose);
3170
+ }
3053
3171
  }
3054
- if (node.end_node) {
3055
- this.printERBNode(node.end_node);
3172
+ else {
3173
+ this.printERBNode(node);
3174
+ this.withIndent(() => {
3175
+ node.statements.forEach(child => this.visit(child));
3176
+ });
3177
+ if (node.subsequent) {
3178
+ this.visit(node.subsequent);
3179
+ }
3180
+ if (node.end_node) {
3181
+ this.printERBNode(node.end_node);
3182
+ }
3056
3183
  }
3057
3184
  }
3058
3185
  visitERBElseNode(node) {
@@ -3132,8 +3259,54 @@ class Printer extends Visitor {
3132
3259
  this.visit(node.end_node);
3133
3260
  }
3134
3261
  // --- Utility methods ---
3135
- renderInlineOpen(name, attributes, selfClose) {
3262
+ renderInlineOpen(name, attributes, selfClose, inlineNodes = [], allChildren = []) {
3136
3263
  const parts = attributes.map(attribute => this.renderAttribute(attribute));
3264
+ if (inlineNodes.length > 0) {
3265
+ let result = `<${name}`;
3266
+ if (allChildren.length > 0) {
3267
+ const currentIndentLevel = this.indentLevel;
3268
+ this.indentLevel = 0;
3269
+ const tempLines = this.lines;
3270
+ this.lines = [];
3271
+ allChildren.forEach(child => {
3272
+ if (child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE') {
3273
+ this.lines.push(" " + this.renderAttribute(child));
3274
+ }
3275
+ else if (!(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE')) {
3276
+ const wasInlineMode = this.inlineMode;
3277
+ this.inlineMode = true;
3278
+ this.lines.push(" ");
3279
+ this.visit(child);
3280
+ this.inlineMode = wasInlineMode;
3281
+ }
3282
+ });
3283
+ const inlineContent = this.lines.join("");
3284
+ this.lines = tempLines;
3285
+ this.indentLevel = currentIndentLevel;
3286
+ result += inlineContent;
3287
+ }
3288
+ else {
3289
+ if (parts.length > 0) {
3290
+ result += ` ${parts.join(" ")}`;
3291
+ }
3292
+ const currentIndentLevel = this.indentLevel;
3293
+ this.indentLevel = 0;
3294
+ const tempLines = this.lines;
3295
+ this.lines = [];
3296
+ inlineNodes.forEach(node => {
3297
+ const wasInlineMode = this.inlineMode;
3298
+ this.inlineMode = true;
3299
+ this.visit(node);
3300
+ this.inlineMode = wasInlineMode;
3301
+ });
3302
+ const inlineContent = this.lines.join("");
3303
+ this.lines = tempLines;
3304
+ this.indentLevel = currentIndentLevel;
3305
+ result += inlineContent;
3306
+ }
3307
+ result += selfClose ? " />" : ">";
3308
+ return result;
3309
+ }
3137
3310
  return `<${name}${parts.length ? " " + parts.join(" ") : ""}${selfClose ? " /" : ""}>`;
3138
3311
  }
3139
3312
  renderAttribute(attribute) {
@@ -3145,8 +3318,7 @@ class Printer extends Visitor {
3145
3318
  const open_quote = (attrValue.open_quote?.value ?? "");
3146
3319
  const close_quote = (attrValue.close_quote?.value ?? "");
3147
3320
  const attribute_value = attrValue.children.map((attr) => {
3148
- if (attr instanceof HTMLTextNode || attr.type === 'AST_HTML_TEXT_NODE' ||
3149
- attr instanceof LiteralNode || attr.type === 'AST_LITERAL_NODE') {
3321
+ if (attr instanceof HTMLTextNode || attr.type === 'AST_HTML_TEXT_NODE' || attr instanceof LiteralNode || attr.type === 'AST_LITERAL_NODE') {
3150
3322
  return attr.content;
3151
3323
  }
3152
3324
  else if (attr instanceof ERBContentNode || attr.type === 'AST_ERB_CONTENT_NODE') {
@@ -3159,6 +3331,124 @@ class Printer extends Visitor {
3159
3331
  }
3160
3332
  return name + equals + value;
3161
3333
  }
3334
+ /**
3335
+ * Try to render children inline if they are simple enough.
3336
+ * Returns the inline string if possible, null otherwise.
3337
+ */
3338
+ tryRenderInline(children, tagName, depth = 0) {
3339
+ if (children.length > 10) {
3340
+ return null;
3341
+ }
3342
+ const maxNestingDepth = this.getMaxNestingDepth(children, 0);
3343
+ if (maxNestingDepth > 1) {
3344
+ this.isInComplexNesting = true;
3345
+ return null;
3346
+ }
3347
+ for (const child of children) {
3348
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3349
+ const textContent = child.content;
3350
+ if (textContent.includes('\n')) {
3351
+ return null;
3352
+ }
3353
+ }
3354
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3355
+ const element = child;
3356
+ const openTag = element.open_tag;
3357
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3358
+ if (attributes.length > 0) {
3359
+ return null;
3360
+ }
3361
+ }
3362
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') ;
3363
+ else {
3364
+ return null;
3365
+ }
3366
+ }
3367
+ const oldLines = this.lines;
3368
+ const oldInlineMode = this.inlineMode;
3369
+ try {
3370
+ this.lines = [];
3371
+ this.inlineMode = true;
3372
+ let content = '';
3373
+ for (const child of children) {
3374
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3375
+ content += child.content;
3376
+ }
3377
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3378
+ const element = child;
3379
+ const openTag = element.open_tag;
3380
+ const childTagName = openTag?.tag_name?.value || '';
3381
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3382
+ const attributesString = attributes.length > 0
3383
+ ? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
3384
+ : '';
3385
+ const elementContent = this.renderElementInline(element);
3386
+ content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`;
3387
+ }
3388
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3389
+ const erbNode = child;
3390
+ const open = erbNode.tag_opening?.value ?? "";
3391
+ const erbContent = erbNode.content?.value ?? "";
3392
+ const close = erbNode.tag_closing?.value ?? "";
3393
+ content += `${open} ${erbContent.trim()} ${close}`;
3394
+ }
3395
+ }
3396
+ content = content.replace(/\s+/g, ' ').trim();
3397
+ return `<${tagName}>${content}</${tagName}>`;
3398
+ }
3399
+ finally {
3400
+ this.lines = oldLines;
3401
+ this.inlineMode = oldInlineMode;
3402
+ }
3403
+ }
3404
+ /**
3405
+ * Calculate the maximum nesting depth in a subtree of nodes.
3406
+ */
3407
+ getMaxNestingDepth(children, currentDepth) {
3408
+ let maxDepth = currentDepth;
3409
+ for (const child of children) {
3410
+ if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3411
+ const element = child;
3412
+ const elementChildren = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3413
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3414
+ const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1);
3415
+ maxDepth = Math.max(maxDepth, childDepth);
3416
+ }
3417
+ }
3418
+ return maxDepth;
3419
+ }
3420
+ /**
3421
+ * Render an HTML element's content inline (without the wrapping tags).
3422
+ */
3423
+ renderElementInline(element) {
3424
+ const children = element.body.filter(child => !(child instanceof WhitespaceNode || child.type === 'AST_WHITESPACE_NODE') &&
3425
+ !((child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') && child?.content.trim() === ""));
3426
+ let content = '';
3427
+ for (const child of children) {
3428
+ if (child instanceof HTMLTextNode || child.type === 'AST_HTML_TEXT_NODE') {
3429
+ content += child.content;
3430
+ }
3431
+ else if (child instanceof HTMLElementNode || child.type === 'AST_HTML_ELEMENT_NODE') {
3432
+ const childElement = child;
3433
+ const openTag = childElement.open_tag;
3434
+ const childTagName = openTag?.tag_name?.value || '';
3435
+ const attributes = openTag.children.filter((child) => child instanceof HTMLAttributeNode || child.type === 'AST_HTML_ATTRIBUTE_NODE');
3436
+ const attributesString = attributes.length > 0
3437
+ ? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
3438
+ : '';
3439
+ const childContent = this.renderElementInline(childElement);
3440
+ content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`;
3441
+ }
3442
+ else if (child instanceof ERBContentNode || child.type === 'AST_ERB_CONTENT_NODE') {
3443
+ const erbNode = child;
3444
+ const open = erbNode.tag_opening?.value ?? "";
3445
+ const erbContent = erbNode.content?.value ?? "";
3446
+ const close = erbNode.tag_closing?.value ?? "";
3447
+ content += `${open} ${erbContent.trim()} ${close}`;
3448
+ }
3449
+ }
3450
+ return content.replace(/\s+/g, ' ').trim();
3451
+ }
3162
3452
  }
3163
3453
 
3164
3454
  /**