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