@herb-tools/rewriter 0.9.3 → 0.9.4

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/loader.cjs CHANGED
@@ -209,7 +209,7 @@ class Token {
209
209
  }
210
210
 
211
211
  // NOTE: This file is generated by the templates/template.rb script and should not
212
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/errors.ts.erb
212
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/errors.ts.erb
213
213
  class HerbError {
214
214
  type;
215
215
  message;
@@ -20388,7 +20388,7 @@ function deserializePrismNode(bytes, source) {
20388
20388
  }
20389
20389
 
20390
20390
  // NOTE: This file is generated by the templates/template.rb script and should not
20391
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/nodes.ts.erb
20391
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/nodes.ts.erb
20392
20392
  class Node {
20393
20393
  type;
20394
20394
  location;
@@ -23657,7 +23657,7 @@ class ParseResult extends Result {
23657
23657
  }
23658
23658
 
23659
23659
  // NOTE: This file is generated by the templates/template.rb script and should not
23660
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/node-type-guards.ts.erb
23660
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/node-type-guards.ts.erb
23661
23661
  /**
23662
23662
  * Type guard functions for AST nodes.
23663
23663
  * These functions provide type checking by combining both instanceof
@@ -24000,6 +24000,31 @@ function isERBInNode(node) {
24000
24000
  return false;
24001
24001
  return node instanceof ERBInNode || node.type === "AST_ERB_IN_NODE" || node.constructor.type === "AST_ERB_IN_NODE";
24002
24002
  }
24003
+ /**
24004
+ * Checks if a node is any ERB node type
24005
+ */
24006
+ function isERBNode(node) {
24007
+ return isERBOpenTagNode(node) ||
24008
+ isERBContentNode(node) ||
24009
+ isERBEndNode(node) ||
24010
+ isERBElseNode(node) ||
24011
+ isERBIfNode(node) ||
24012
+ isERBBlockNode(node) ||
24013
+ isERBWhenNode(node) ||
24014
+ isERBCaseNode(node) ||
24015
+ isERBCaseMatchNode(node) ||
24016
+ isERBWhileNode(node) ||
24017
+ isERBUntilNode(node) ||
24018
+ isERBForNode(node) ||
24019
+ isERBRescueNode(node) ||
24020
+ isERBEnsureNode(node) ||
24021
+ isERBBeginNode(node) ||
24022
+ isERBUnlessNode(node) ||
24023
+ isERBRenderNode(node) ||
24024
+ isERBStrictLocalsNode(node) ||
24025
+ isERBYieldNode(node) ||
24026
+ isERBInNode(node);
24027
+ }
24003
24028
  /**
24004
24029
  * Map of node classes to their corresponding type guard functions
24005
24030
  *
@@ -24162,6 +24187,17 @@ function isToken(object) {
24162
24187
  function isParseResult(object) {
24163
24188
  return (object instanceof ParseResult) || (object?.constructor?.name === "ParseResult" && "value" in object);
24164
24189
  }
24190
+
24191
+ /**
24192
+ * Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
24193
+ */
24194
+ function isERBOutputNode(node) {
24195
+ if (!isERBNode(node))
24196
+ return false;
24197
+ if (!node.tag_opening?.value)
24198
+ return false;
24199
+ return ["<%=", "<%=="].includes(node.tag_opening?.value);
24200
+ }
24165
24201
  /**
24166
24202
  * Extracts a static string from an array of literal nodes
24167
24203
  * Returns null if any node is not a literal node
@@ -24326,12 +24362,88 @@ function getNodesBeforePosition(nodes, position, inclusive = false) {
24326
24362
  function getNodesAfterPosition(nodes, position, inclusive = true) {
24327
24363
  return nodes.filter(node => node.location && isPositionAfter(node.location.start, position, inclusive));
24328
24364
  }
24365
+ // --- AST Mutation Utilities ---
24366
+ const CHILD_ARRAY_PROPS = ["children", "body", "statements", "conditions"];
24367
+ const LINKED_NODE_PROPS = ["subsequent", "else_clause"];
24368
+ /**
24369
+ * Finds the array containing a target node in the AST, along with its index.
24370
+ * Traverses child arrays and linked node properties (e.g., `subsequent`, `else_clause`).
24371
+ *
24372
+ * Useful for autofix operations that need to splice nodes in/out of their parent array.
24373
+ *
24374
+ * @param root - The root node to search from
24375
+ * @param target - The node to find
24376
+ * @returns The containing array and the target's index, or null if not found
24377
+ */
24378
+ function findParentArray(root, target) {
24379
+ const search = (node) => {
24380
+ const record = node;
24381
+ for (const prop of CHILD_ARRAY_PROPS) {
24382
+ const array = record[prop];
24383
+ if (Array.isArray(array)) {
24384
+ const index = array.indexOf(target);
24385
+ if (index !== -1) {
24386
+ return { array, index };
24387
+ }
24388
+ }
24389
+ }
24390
+ for (const prop of CHILD_ARRAY_PROPS) {
24391
+ const array = record[prop];
24392
+ if (Array.isArray(array)) {
24393
+ for (const child of array) {
24394
+ if (child && typeof child === 'object' && 'type' in child) {
24395
+ const result = search(child);
24396
+ if (result) {
24397
+ return result;
24398
+ }
24399
+ }
24400
+ }
24401
+ }
24402
+ }
24403
+ for (const prop of LINKED_NODE_PROPS) {
24404
+ const value = record[prop];
24405
+ if (value && typeof value === 'object' && 'type' in value) {
24406
+ const result = search(value);
24407
+ if (result) {
24408
+ return result;
24409
+ }
24410
+ }
24411
+ }
24412
+ return null;
24413
+ };
24414
+ return search(root);
24415
+ }
24416
+ /**
24417
+ * Creates a synthetic LiteralNode with the given content and zero location.
24418
+ * Useful for inserting whitespace or newlines during AST mutations.
24419
+ */
24420
+ function createLiteral(content) {
24421
+ return new LiteralNode({
24422
+ type: "AST_LITERAL_NODE",
24423
+ content,
24424
+ location: Location.zero,
24425
+ errors: [],
24426
+ });
24427
+ }
24329
24428
  function createSyntheticToken(value, type = "TOKEN_SYNTHETIC") {
24330
24429
  return new Token(value, Range.zero, Location.zero, type);
24331
24430
  }
24431
+ function createERBOutputNode(expression, tagOpening = "<%=", tagClosing = "%>") {
24432
+ return new ERBContentNode({
24433
+ type: "AST_ERB_CONTENT_NODE",
24434
+ tag_opening: createSyntheticToken(tagOpening),
24435
+ content: createSyntheticToken(expression),
24436
+ tag_closing: createSyntheticToken(tagClosing),
24437
+ parsed: false,
24438
+ valid: true,
24439
+ prism_node: null,
24440
+ location: Location.zero,
24441
+ errors: [],
24442
+ });
24443
+ }
24332
24444
 
24333
24445
  // NOTE: This file is generated by the templates/template.rb script and should not
24334
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/visitor.ts.erb
24446
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/visitor.ts.erb
24335
24447
  class Visitor {
24336
24448
  visit(node) {
24337
24449
  if (!node)
@@ -24726,6 +24838,127 @@ class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
24726
24838
  }
24727
24839
  }
24728
24840
 
24841
+ const STRING_NODE_TYPE = "StringNode";
24842
+ const INTERPOLATED_STRING_NODE_TYPE = "InterpolatedStringNode";
24843
+ const EMBEDDED_STATEMENTS_NODE_TYPE = "EmbeddedStatementsNode";
24844
+ class ERBStringToDirectOutputVisitor extends Visitor {
24845
+ root;
24846
+ constructor(root) {
24847
+ super();
24848
+ this.root = root;
24849
+ }
24850
+ visitERBContentNode(node) {
24851
+ if (!isERBOutputNode(node)) {
24852
+ this.visitChildNodes(node);
24853
+ return;
24854
+ }
24855
+ const prismNode = node.prismNode;
24856
+ if (!prismNode) {
24857
+ this.visitChildNodes(node);
24858
+ return;
24859
+ }
24860
+ const source = node.source;
24861
+ if (!source) {
24862
+ this.visitChildNodes(node);
24863
+ return;
24864
+ }
24865
+ if (!ERBStringToDirectOutputRewriter.isStringOutputNode(prismNode)) {
24866
+ this.visitChildNodes(node);
24867
+ return;
24868
+ }
24869
+ const replacementParts = ERBStringToDirectOutputRewriter.extractReplacementParts(prismNode, source);
24870
+ if (!replacementParts) {
24871
+ this.visitChildNodes(node);
24872
+ return;
24873
+ }
24874
+ const tagOpening = node.tag_opening?.value ?? "<%=";
24875
+ const tagClosing = node.tag_closing?.value ?? "%>";
24876
+ const parentInfo = findParentArray(this.root, node);
24877
+ if (!parentInfo) {
24878
+ this.visitChildNodes(node);
24879
+ return;
24880
+ }
24881
+ const { array: parentArray, index: nodeIndex } = parentInfo;
24882
+ const replacementNodes = [];
24883
+ for (const part of replacementParts) {
24884
+ if (part.type === "text") {
24885
+ replacementNodes.push(createLiteral(part.content));
24886
+ }
24887
+ else {
24888
+ replacementNodes.push(createERBOutputNode(` ${part.expression.trim()} `, tagOpening, tagClosing));
24889
+ }
24890
+ }
24891
+ parentArray.splice(nodeIndex, 1, ...replacementNodes);
24892
+ }
24893
+ }
24894
+ class ERBStringToDirectOutputRewriter extends ASTRewriter {
24895
+ get name() {
24896
+ return "erb-string-to-direct-output";
24897
+ }
24898
+ get description() {
24899
+ return "Replaces ERB string output with direct text and expression tags";
24900
+ }
24901
+ rewrite(node, _context) {
24902
+ const visitor = new ERBStringToDirectOutputVisitor(node);
24903
+ visitor.visit(node);
24904
+ return node;
24905
+ }
24906
+ static isStringOutputNode(prismNode) {
24907
+ const nodeType = prismNode.constructor.name;
24908
+ return nodeType === STRING_NODE_TYPE || nodeType === INTERPOLATED_STRING_NODE_TYPE;
24909
+ }
24910
+ static extractStringContent(stringNode, source) {
24911
+ const unescapedValue = stringNode.unescaped?.value;
24912
+ if (typeof unescapedValue === "string") {
24913
+ return unescapedValue;
24914
+ }
24915
+ const location = stringNode.contentLoc;
24916
+ if (location) {
24917
+ return source.substring(location.startOffset, location.startOffset + location.length);
24918
+ }
24919
+ return "";
24920
+ }
24921
+ static extractExpressionSource(embeddedNode, source) {
24922
+ const openingLocation = embeddedNode.openingLoc;
24923
+ const closingLocation = embeddedNode.closingLoc;
24924
+ if (!openingLocation || !closingLocation)
24925
+ return null;
24926
+ const expressionStart = openingLocation.startOffset + openingLocation.length;
24927
+ const expressionEnd = closingLocation.startOffset;
24928
+ return source.substring(expressionStart, expressionEnd);
24929
+ }
24930
+ static extractReplacementParts(prismNode, source) {
24931
+ const nodeType = prismNode.constructor.name;
24932
+ if (nodeType === STRING_NODE_TYPE) {
24933
+ const textContent = this.extractStringContent(prismNode, source);
24934
+ return [{ type: "text", content: textContent }];
24935
+ }
24936
+ if (nodeType === INTERPOLATED_STRING_NODE_TYPE) {
24937
+ const parts = prismNode.parts;
24938
+ if (!parts || parts.length === 0)
24939
+ return null;
24940
+ const replacementParts = [];
24941
+ for (const part of parts) {
24942
+ const partType = part.constructor.name;
24943
+ if (partType === STRING_NODE_TYPE) {
24944
+ const textContent = this.extractStringContent(part, source);
24945
+ if (textContent) {
24946
+ replacementParts.push({ type: "text", content: textContent });
24947
+ }
24948
+ }
24949
+ else if (partType === EMBEDDED_STATEMENTS_NODE_TYPE) {
24950
+ const expression = this.extractExpressionSource(part, source);
24951
+ if (expression) {
24952
+ replacementParts.push({ type: "expression", expression });
24953
+ }
24954
+ }
24955
+ }
24956
+ return replacementParts.length > 0 ? replacementParts : null;
24957
+ }
24958
+ return null;
24959
+ }
24960
+ }
24961
+
24729
24962
  function serializeAttributeValue(value) {
24730
24963
  const hasERB = value.children.some(child => isERBContentNode(child));
24731
24964
  if (hasERB && value.children.length === 1 && isERBContentNode(value.children[0])) {
@@ -26067,6 +26300,7 @@ class TailwindClassSorterRewriter extends ASTRewriter {
26067
26300
  */
26068
26301
  const builtinRewriters = [
26069
26302
  ActionViewTagHelperToHTMLRewriter,
26303
+ ERBStringToDirectOutputRewriter,
26070
26304
  HTMLToActionViewTagHelperRewriter,
26071
26305
  TailwindClassSorterRewriter
26072
26306
  ];
@@ -26118,6 +26352,7 @@ async function tailwindClassSorter(options = {}) {
26118
26352
  exports.ASTRewriter = ASTRewriter;
26119
26353
  exports.ActionViewTagHelperToHTMLRewriter = ActionViewTagHelperToHTMLRewriter;
26120
26354
  exports.CustomRewriterLoader = CustomRewriterLoader;
26355
+ exports.ERBStringToDirectOutputRewriter = ERBStringToDirectOutputRewriter;
26121
26356
  exports.HTMLToActionViewTagHelperRewriter = HTMLToActionViewTagHelperRewriter;
26122
26357
  exports.StringRewriter = StringRewriter;
26123
26358
  exports.TailwindClassSorterRewriter = TailwindClassSorterRewriter;