@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 +238 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +238 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/loader.cjs +239 -4
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.esm.js +239 -5
- package/dist/loader.esm.js.map +1 -1
- package/dist/types/built-ins/erb-string-to-direct-output.d.ts +21 -0
- package/dist/types/built-ins/index.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/rewriter-factories.d.ts +2 -0
- package/package.json +4 -4
- package/src/built-ins/erb-string-to-direct-output.ts +176 -0
- package/src/built-ins/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/rewriter-factories.ts +5 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|