@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/index.cjs CHANGED
@@ -205,7 +205,7 @@ class Token {
205
205
  }
206
206
 
207
207
  // NOTE: This file is generated by the templates/template.rb script and should not
208
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/errors.ts.erb
208
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/errors.ts.erb
209
209
  class HerbError {
210
210
  type;
211
211
  message;
@@ -20384,7 +20384,7 @@ function deserializePrismNode(bytes, source) {
20384
20384
  }
20385
20385
 
20386
20386
  // NOTE: This file is generated by the templates/template.rb script and should not
20387
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/nodes.ts.erb
20387
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/nodes.ts.erb
20388
20388
  class Node {
20389
20389
  type;
20390
20390
  location;
@@ -23653,7 +23653,7 @@ class ParseResult extends Result {
23653
23653
  }
23654
23654
 
23655
23655
  // NOTE: This file is generated by the templates/template.rb script and should not
23656
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/node-type-guards.ts.erb
23656
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/node-type-guards.ts.erb
23657
23657
  /**
23658
23658
  * Type guard functions for AST nodes.
23659
23659
  * These functions provide type checking by combining both instanceof
@@ -23996,6 +23996,31 @@ function isERBInNode(node) {
23996
23996
  return false;
23997
23997
  return node instanceof ERBInNode || node.type === "AST_ERB_IN_NODE" || node.constructor.type === "AST_ERB_IN_NODE";
23998
23998
  }
23999
+ /**
24000
+ * Checks if a node is any ERB node type
24001
+ */
24002
+ function isERBNode(node) {
24003
+ return isERBOpenTagNode(node) ||
24004
+ isERBContentNode(node) ||
24005
+ isERBEndNode(node) ||
24006
+ isERBElseNode(node) ||
24007
+ isERBIfNode(node) ||
24008
+ isERBBlockNode(node) ||
24009
+ isERBWhenNode(node) ||
24010
+ isERBCaseNode(node) ||
24011
+ isERBCaseMatchNode(node) ||
24012
+ isERBWhileNode(node) ||
24013
+ isERBUntilNode(node) ||
24014
+ isERBForNode(node) ||
24015
+ isERBRescueNode(node) ||
24016
+ isERBEnsureNode(node) ||
24017
+ isERBBeginNode(node) ||
24018
+ isERBUnlessNode(node) ||
24019
+ isERBRenderNode(node) ||
24020
+ isERBStrictLocalsNode(node) ||
24021
+ isERBYieldNode(node) ||
24022
+ isERBInNode(node);
24023
+ }
23999
24024
  /**
24000
24025
  * Map of node classes to their corresponding type guard functions
24001
24026
  *
@@ -24158,6 +24183,17 @@ function isToken(object) {
24158
24183
  function isParseResult(object) {
24159
24184
  return (object instanceof ParseResult) || (object?.constructor?.name === "ParseResult" && "value" in object);
24160
24185
  }
24186
+
24187
+ /**
24188
+ * Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
24189
+ */
24190
+ function isERBOutputNode(node) {
24191
+ if (!isERBNode(node))
24192
+ return false;
24193
+ if (!node.tag_opening?.value)
24194
+ return false;
24195
+ return ["<%=", "<%=="].includes(node.tag_opening?.value);
24196
+ }
24161
24197
  /**
24162
24198
  * Extracts a static string from an array of literal nodes
24163
24199
  * Returns null if any node is not a literal node
@@ -24217,12 +24253,88 @@ function getNodesBeforePosition(nodes, position, inclusive = false) {
24217
24253
  function getNodesAfterPosition(nodes, position, inclusive = true) {
24218
24254
  return nodes.filter(node => node.location && isPositionAfter(node.location.start, position, inclusive));
24219
24255
  }
24256
+ // --- AST Mutation Utilities ---
24257
+ const CHILD_ARRAY_PROPS = ["children", "body", "statements", "conditions"];
24258
+ const LINKED_NODE_PROPS = ["subsequent", "else_clause"];
24259
+ /**
24260
+ * Finds the array containing a target node in the AST, along with its index.
24261
+ * Traverses child arrays and linked node properties (e.g., `subsequent`, `else_clause`).
24262
+ *
24263
+ * Useful for autofix operations that need to splice nodes in/out of their parent array.
24264
+ *
24265
+ * @param root - The root node to search from
24266
+ * @param target - The node to find
24267
+ * @returns The containing array and the target's index, or null if not found
24268
+ */
24269
+ function findParentArray(root, target) {
24270
+ const search = (node) => {
24271
+ const record = node;
24272
+ for (const prop of CHILD_ARRAY_PROPS) {
24273
+ const array = record[prop];
24274
+ if (Array.isArray(array)) {
24275
+ const index = array.indexOf(target);
24276
+ if (index !== -1) {
24277
+ return { array, index };
24278
+ }
24279
+ }
24280
+ }
24281
+ for (const prop of CHILD_ARRAY_PROPS) {
24282
+ const array = record[prop];
24283
+ if (Array.isArray(array)) {
24284
+ for (const child of array) {
24285
+ if (child && typeof child === 'object' && 'type' in child) {
24286
+ const result = search(child);
24287
+ if (result) {
24288
+ return result;
24289
+ }
24290
+ }
24291
+ }
24292
+ }
24293
+ }
24294
+ for (const prop of LINKED_NODE_PROPS) {
24295
+ const value = record[prop];
24296
+ if (value && typeof value === 'object' && 'type' in value) {
24297
+ const result = search(value);
24298
+ if (result) {
24299
+ return result;
24300
+ }
24301
+ }
24302
+ }
24303
+ return null;
24304
+ };
24305
+ return search(root);
24306
+ }
24307
+ /**
24308
+ * Creates a synthetic LiteralNode with the given content and zero location.
24309
+ * Useful for inserting whitespace or newlines during AST mutations.
24310
+ */
24311
+ function createLiteral(content) {
24312
+ return new LiteralNode({
24313
+ type: "AST_LITERAL_NODE",
24314
+ content,
24315
+ location: Location.zero,
24316
+ errors: [],
24317
+ });
24318
+ }
24220
24319
  function createSyntheticToken(value, type = "TOKEN_SYNTHETIC") {
24221
24320
  return new Token(value, Range.zero, Location.zero, type);
24222
24321
  }
24322
+ function createERBOutputNode(expression, tagOpening = "<%=", tagClosing = "%>") {
24323
+ return new ERBContentNode({
24324
+ type: "AST_ERB_CONTENT_NODE",
24325
+ tag_opening: createSyntheticToken(tagOpening),
24326
+ content: createSyntheticToken(expression),
24327
+ tag_closing: createSyntheticToken(tagClosing),
24328
+ parsed: false,
24329
+ valid: true,
24330
+ prism_node: null,
24331
+ location: Location.zero,
24332
+ errors: [],
24333
+ });
24334
+ }
24223
24335
 
24224
24336
  // NOTE: This file is generated by the templates/template.rb script and should not
24225
- // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.3/templates/javascript/packages/core/src/visitor.ts.erb
24337
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.4/templates/javascript/packages/core/src/visitor.ts.erb
24226
24338
  class Visitor {
24227
24339
  visit(node) {
24228
24340
  if (!node)
@@ -24617,6 +24729,127 @@ class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
24617
24729
  }
24618
24730
  }
24619
24731
 
24732
+ const STRING_NODE_TYPE = "StringNode";
24733
+ const INTERPOLATED_STRING_NODE_TYPE = "InterpolatedStringNode";
24734
+ const EMBEDDED_STATEMENTS_NODE_TYPE = "EmbeddedStatementsNode";
24735
+ class ERBStringToDirectOutputVisitor extends Visitor {
24736
+ root;
24737
+ constructor(root) {
24738
+ super();
24739
+ this.root = root;
24740
+ }
24741
+ visitERBContentNode(node) {
24742
+ if (!isERBOutputNode(node)) {
24743
+ this.visitChildNodes(node);
24744
+ return;
24745
+ }
24746
+ const prismNode = node.prismNode;
24747
+ if (!prismNode) {
24748
+ this.visitChildNodes(node);
24749
+ return;
24750
+ }
24751
+ const source = node.source;
24752
+ if (!source) {
24753
+ this.visitChildNodes(node);
24754
+ return;
24755
+ }
24756
+ if (!ERBStringToDirectOutputRewriter.isStringOutputNode(prismNode)) {
24757
+ this.visitChildNodes(node);
24758
+ return;
24759
+ }
24760
+ const replacementParts = ERBStringToDirectOutputRewriter.extractReplacementParts(prismNode, source);
24761
+ if (!replacementParts) {
24762
+ this.visitChildNodes(node);
24763
+ return;
24764
+ }
24765
+ const tagOpening = node.tag_opening?.value ?? "<%=";
24766
+ const tagClosing = node.tag_closing?.value ?? "%>";
24767
+ const parentInfo = findParentArray(this.root, node);
24768
+ if (!parentInfo) {
24769
+ this.visitChildNodes(node);
24770
+ return;
24771
+ }
24772
+ const { array: parentArray, index: nodeIndex } = parentInfo;
24773
+ const replacementNodes = [];
24774
+ for (const part of replacementParts) {
24775
+ if (part.type === "text") {
24776
+ replacementNodes.push(createLiteral(part.content));
24777
+ }
24778
+ else {
24779
+ replacementNodes.push(createERBOutputNode(` ${part.expression.trim()} `, tagOpening, tagClosing));
24780
+ }
24781
+ }
24782
+ parentArray.splice(nodeIndex, 1, ...replacementNodes);
24783
+ }
24784
+ }
24785
+ class ERBStringToDirectOutputRewriter extends ASTRewriter {
24786
+ get name() {
24787
+ return "erb-string-to-direct-output";
24788
+ }
24789
+ get description() {
24790
+ return "Replaces ERB string output with direct text and expression tags";
24791
+ }
24792
+ rewrite(node, _context) {
24793
+ const visitor = new ERBStringToDirectOutputVisitor(node);
24794
+ visitor.visit(node);
24795
+ return node;
24796
+ }
24797
+ static isStringOutputNode(prismNode) {
24798
+ const nodeType = prismNode.constructor.name;
24799
+ return nodeType === STRING_NODE_TYPE || nodeType === INTERPOLATED_STRING_NODE_TYPE;
24800
+ }
24801
+ static extractStringContent(stringNode, source) {
24802
+ const unescapedValue = stringNode.unescaped?.value;
24803
+ if (typeof unescapedValue === "string") {
24804
+ return unescapedValue;
24805
+ }
24806
+ const location = stringNode.contentLoc;
24807
+ if (location) {
24808
+ return source.substring(location.startOffset, location.startOffset + location.length);
24809
+ }
24810
+ return "";
24811
+ }
24812
+ static extractExpressionSource(embeddedNode, source) {
24813
+ const openingLocation = embeddedNode.openingLoc;
24814
+ const closingLocation = embeddedNode.closingLoc;
24815
+ if (!openingLocation || !closingLocation)
24816
+ return null;
24817
+ const expressionStart = openingLocation.startOffset + openingLocation.length;
24818
+ const expressionEnd = closingLocation.startOffset;
24819
+ return source.substring(expressionStart, expressionEnd);
24820
+ }
24821
+ static extractReplacementParts(prismNode, source) {
24822
+ const nodeType = prismNode.constructor.name;
24823
+ if (nodeType === STRING_NODE_TYPE) {
24824
+ const textContent = this.extractStringContent(prismNode, source);
24825
+ return [{ type: "text", content: textContent }];
24826
+ }
24827
+ if (nodeType === INTERPOLATED_STRING_NODE_TYPE) {
24828
+ const parts = prismNode.parts;
24829
+ if (!parts || parts.length === 0)
24830
+ return null;
24831
+ const replacementParts = [];
24832
+ for (const part of parts) {
24833
+ const partType = part.constructor.name;
24834
+ if (partType === STRING_NODE_TYPE) {
24835
+ const textContent = this.extractStringContent(part, source);
24836
+ if (textContent) {
24837
+ replacementParts.push({ type: "text", content: textContent });
24838
+ }
24839
+ }
24840
+ else if (partType === EMBEDDED_STATEMENTS_NODE_TYPE) {
24841
+ const expression = this.extractExpressionSource(part, source);
24842
+ if (expression) {
24843
+ replacementParts.push({ type: "expression", expression });
24844
+ }
24845
+ }
24846
+ }
24847
+ return replacementParts.length > 0 ? replacementParts : null;
24848
+ }
24849
+ return null;
24850
+ }
24851
+ }
24852
+
24620
24853
  function serializeAttributeValue(value) {
24621
24854
  const hasERB = value.children.some(child => isERBContentNode(child));
24622
24855
  if (hasERB && value.children.length === 1 && isERBContentNode(value.children[0])) {
@@ -25540,6 +25773,7 @@ function rewriteString(herb, template, rewriters, options = {}) {
25540
25773
 
25541
25774
  exports.ASTRewriter = ASTRewriter;
25542
25775
  exports.ActionViewTagHelperToHTMLRewriter = ActionViewTagHelperToHTMLRewriter;
25776
+ exports.ERBStringToDirectOutputRewriter = ERBStringToDirectOutputRewriter;
25543
25777
  exports.HTMLToActionViewTagHelperRewriter = HTMLToActionViewTagHelperRewriter;
25544
25778
  exports.StringRewriter = StringRewriter;
25545
25779
  exports.asMutable = asMutable;