@herb-tools/core 0.5.0 → 0.6.1
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/herb-core.browser.js +1344 -368
- package/dist/herb-core.browser.js.map +1 -1
- package/dist/herb-core.cjs +1443 -368
- package/dist/herb-core.cjs.map +1 -1
- package/dist/herb-core.esm.js +1344 -368
- package/dist/herb-core.esm.js.map +1 -1
- package/dist/herb-core.umd.js +1443 -368
- package/dist/herb-core.umd.js.map +1 -1
- package/dist/types/ast-utils.d.ts +124 -0
- package/dist/types/backend.d.ts +2 -1
- package/dist/types/herb-backend.d.ts +3 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/node-type-guards.d.ts +434 -0
- package/dist/types/nodes.d.ts +187 -97
- package/dist/types/parser-options.d.ts +4 -0
- package/dist/types/visitor.d.ts +3 -2
- package/package.json +1 -1
- package/src/ast-utils.ts +291 -0
- package/src/backend.ts +2 -1
- package/src/errors.ts +1 -1
- package/src/herb-backend.ts +7 -2
- package/src/index.ts +3 -0
- package/src/node-type-guards.ts +878 -0
- package/src/nodes.ts +436 -251
- package/src/parser-options.ts +7 -0
- package/src/visitor.ts +11 -6
package/dist/types/visitor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Node, DocumentNode, LiteralNode, HTMLOpenTagNode, HTMLCloseTagNode,
|
|
1
|
+
import { Node, DocumentNode, LiteralNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLAttributeValueNode, HTMLAttributeNameNode, HTMLAttributeNode, HTMLTextNode, HTMLCommentNode, HTMLDoctypeNode, XMLDeclarationNode, CDATANode, WhitespaceNode, ERBContentNode, ERBEndNode, ERBElseNode, ERBIfNode, ERBBlockNode, ERBWhenNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBUntilNode, ERBForNode, ERBRescueNode, ERBEnsureNode, ERBBeginNode, ERBUnlessNode, ERBYieldNode, ERBInNode } from "./nodes.js";
|
|
2
2
|
export declare class Visitor {
|
|
3
3
|
visit(node: Node | null | undefined): void;
|
|
4
4
|
visitAll(nodes: (Node | null | undefined)[]): void;
|
|
@@ -7,7 +7,6 @@ export declare class Visitor {
|
|
|
7
7
|
visitLiteralNode(node: LiteralNode): void;
|
|
8
8
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void;
|
|
9
9
|
visitHTMLCloseTagNode(node: HTMLCloseTagNode): void;
|
|
10
|
-
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void;
|
|
11
10
|
visitHTMLElementNode(node: HTMLElementNode): void;
|
|
12
11
|
visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void;
|
|
13
12
|
visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void;
|
|
@@ -15,6 +14,8 @@ export declare class Visitor {
|
|
|
15
14
|
visitHTMLTextNode(node: HTMLTextNode): void;
|
|
16
15
|
visitHTMLCommentNode(node: HTMLCommentNode): void;
|
|
17
16
|
visitHTMLDoctypeNode(node: HTMLDoctypeNode): void;
|
|
17
|
+
visitXMLDeclarationNode(node: XMLDeclarationNode): void;
|
|
18
|
+
visitCDATANode(node: CDATANode): void;
|
|
18
19
|
visitWhitespaceNode(node: WhitespaceNode): void;
|
|
19
20
|
visitERBContentNode(node: ERBContentNode): void;
|
|
20
21
|
visitERBEndNode(node: ERBEndNode): void;
|
package/package.json
CHANGED
package/src/ast-utils.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Node,
|
|
3
|
+
LiteralNode,
|
|
4
|
+
ERBContentNode,
|
|
5
|
+
ERBIfNode,
|
|
6
|
+
ERBUnlessNode,
|
|
7
|
+
ERBBlockNode,
|
|
8
|
+
ERBCaseNode,
|
|
9
|
+
ERBCaseMatchNode,
|
|
10
|
+
ERBWhileNode,
|
|
11
|
+
ERBForNode,
|
|
12
|
+
ERBBeginNode,
|
|
13
|
+
HTMLElementNode,
|
|
14
|
+
HTMLOpenTagNode,
|
|
15
|
+
HTMLCloseTagNode,
|
|
16
|
+
HTMLAttributeNameNode,
|
|
17
|
+
HTMLCommentNode
|
|
18
|
+
} from "./nodes.js"
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
isNode,
|
|
22
|
+
isAnyOf,
|
|
23
|
+
isLiteralNode,
|
|
24
|
+
isERBNode,
|
|
25
|
+
isERBContentNode,
|
|
26
|
+
areAllOfType,
|
|
27
|
+
filterLiteralNodes
|
|
28
|
+
} from "./node-type-guards.js"
|
|
29
|
+
|
|
30
|
+
import type { Location } from "./location.js"
|
|
31
|
+
import type { Position } from "./position.js"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
|
|
35
|
+
*/
|
|
36
|
+
export function isERBOutputNode(node: Node): node is ERBContentNode {
|
|
37
|
+
return isNode(node, ERBContentNode) && ["<%=", "<%=="].includes((node as ERBContentNode).tag_opening?.value!)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a node is a non-output ERB node (control flow: <% %>)
|
|
42
|
+
*/
|
|
43
|
+
export function isERBControlFlowNode(node: Node): node is ERBContentNode {
|
|
44
|
+
return isAnyOf(node, ERBIfNode, ERBUnlessNode, ERBBlockNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBForNode, ERBBeginNode)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if an array of nodes contains any ERB content nodes
|
|
49
|
+
*/
|
|
50
|
+
export function hasERBContent(nodes: Node[]): boolean {
|
|
51
|
+
return nodes.some(isERBContentNode)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if an array of nodes contains any ERB output nodes (dynamic content)
|
|
56
|
+
*/
|
|
57
|
+
export function hasERBOutput(nodes: Node[]): boolean {
|
|
58
|
+
return nodes.some(isERBOutputNode)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts a static string from an array of literal nodes
|
|
64
|
+
* Returns null if any node is not a literal node
|
|
65
|
+
*/
|
|
66
|
+
export function getStaticStringFromNodes(nodes: Node[]): string | null {
|
|
67
|
+
if (!areAllOfType(nodes, LiteralNode)) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return nodes.map(node => node.content).join("")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extracts static content from nodes, including mixed literal/ERB content
|
|
76
|
+
* Returns the concatenated literal content, or null if no literal nodes exist
|
|
77
|
+
*/
|
|
78
|
+
export function getStaticContentFromNodes(nodes: Node[]): string | null {
|
|
79
|
+
const literalNodes = filterLiteralNodes(nodes)
|
|
80
|
+
|
|
81
|
+
if (literalNodes.length === 0) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return literalNodes.map(node => node.content).join("")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Checks if nodes contain any literal content (for static validation)
|
|
90
|
+
*/
|
|
91
|
+
export function hasStaticContent(nodes: Node[]): boolean {
|
|
92
|
+
return nodes.some(isLiteralNode)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if nodes are effectively static (only literals and non-output ERB)
|
|
97
|
+
* Non-output ERB like <% if %> doesn't affect static validation
|
|
98
|
+
*/
|
|
99
|
+
export function isEffectivelyStatic(nodes: Node[]): boolean {
|
|
100
|
+
return !hasERBOutput(nodes)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gets static-validatable content from nodes (ignores control ERB, includes literals)
|
|
105
|
+
* Returns concatenated literal content for validation, or null if contains output ERB
|
|
106
|
+
*/
|
|
107
|
+
export function getValidatableStaticContent(nodes: Node[]): string | null {
|
|
108
|
+
if (hasERBOutput(nodes)) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return filterLiteralNodes(nodes).map(node => node.content).join("")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extracts a combined string from nodes, including ERB content
|
|
117
|
+
* For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>")
|
|
118
|
+
* This is useful for debugging or displaying the full attribute name
|
|
119
|
+
*/
|
|
120
|
+
export function getCombinedStringFromNodes(nodes: Node[]): string {
|
|
121
|
+
return nodes.map(node => {
|
|
122
|
+
if (isLiteralNode(node)) {
|
|
123
|
+
return node.content
|
|
124
|
+
} else if (isERBContentNode(node)) {
|
|
125
|
+
const opening = node.tag_opening?.value || ""
|
|
126
|
+
const content = node.content?.value || ""
|
|
127
|
+
const closing = node.tag_closing?.value || ""
|
|
128
|
+
|
|
129
|
+
return `${opening}${content}${closing}`
|
|
130
|
+
} else {
|
|
131
|
+
// For other node types, return a placeholder or empty string
|
|
132
|
+
return `[${node.type}]`
|
|
133
|
+
}
|
|
134
|
+
}).join("")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Checks if an HTML attribute name node has a static (literal-only) name
|
|
139
|
+
*/
|
|
140
|
+
export function hasStaticAttributeName(attributeNameNode: HTMLAttributeNameNode): boolean {
|
|
141
|
+
if (!attributeNameNode.children) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return areAllOfType(attributeNameNode.children, LiteralNode)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks if an HTML attribute name node has dynamic content (contains ERB)
|
|
150
|
+
*/
|
|
151
|
+
export function hasDynamicAttributeName(attributeNameNode: HTMLAttributeNameNode): boolean {
|
|
152
|
+
if (!attributeNameNode.children) {
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return hasERBContent(attributeNameNode.children)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Gets the static string value of an HTML attribute name node
|
|
161
|
+
* Returns null if the attribute name contains dynamic content (ERB)
|
|
162
|
+
*/
|
|
163
|
+
export function getStaticAttributeName(attributeNameNode: HTMLAttributeNameNode): string | null {
|
|
164
|
+
if (!attributeNameNode.children) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return getStaticStringFromNodes(attributeNameNode.children)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gets the combined string representation of an HTML attribute name node
|
|
173
|
+
* This includes both static and dynamic content, useful for debugging
|
|
174
|
+
*/
|
|
175
|
+
export function getCombinedAttributeName(attributeNameNode: HTMLAttributeNameNode): string {
|
|
176
|
+
if (!attributeNameNode.children) {
|
|
177
|
+
return ""
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return getCombinedStringFromNodes(attributeNameNode.children)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the tag name of an HTML element node
|
|
185
|
+
*/
|
|
186
|
+
export function getTagName(node: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): string {
|
|
187
|
+
return node.tag_name?.value ?? ""
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a node is a comment (HTML comment or ERB comment)
|
|
192
|
+
*/
|
|
193
|
+
export function isCommentNode(node: Node): boolean {
|
|
194
|
+
return isNode(node, HTMLCommentNode) || (isERBNode(node) && !isERBControlFlowNode(node))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Compares two positions to determine if the first comes before the second
|
|
199
|
+
* Returns true if pos1 comes before pos2 in source order
|
|
200
|
+
* @param inclusive - If true, returns true when positions are equal
|
|
201
|
+
*/
|
|
202
|
+
function isPositionBefore(position1: Position, position2: Position, inclusive = false): boolean {
|
|
203
|
+
if (position1.line < position2.line) return true
|
|
204
|
+
if (position1.line > position2.line) return false
|
|
205
|
+
|
|
206
|
+
return inclusive ? position1.column <= position2.column : position1.column < position2.column
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Compares two positions to determine if they are equal
|
|
211
|
+
* Returns true if pos1 and pos2 are at the same location
|
|
212
|
+
*/
|
|
213
|
+
export function isPositionEqual(position1: Position, position2: Position): boolean {
|
|
214
|
+
return position1.line === position2.line && position1.column === position2.column
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compares two positions to determine if the first comes after the second
|
|
219
|
+
* Returns true if pos1 comes after pos2 in source order
|
|
220
|
+
* @param inclusive - If true, returns true when positions are equal
|
|
221
|
+
*/
|
|
222
|
+
export function isPositionAfter(position1: Position, position2: Position, inclusive = false): boolean {
|
|
223
|
+
if (position1.line > position2.line) return true
|
|
224
|
+
if (position1.line < position2.line) return false
|
|
225
|
+
|
|
226
|
+
return inclusive ? position1.column >= position2.column : position1.column > position2.column
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Gets nodes that appear before the specified location in source order
|
|
231
|
+
* Uses line and column positions to determine ordering
|
|
232
|
+
*/
|
|
233
|
+
export function getNodesBeforeLocation<T extends Node>(nodes: T[], location: Location): T[] {
|
|
234
|
+
return nodes.filter(node =>
|
|
235
|
+
node.location && isPositionBefore(node.location.end, location.start)
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Gets nodes that appear after the specified location in source order
|
|
241
|
+
* Uses line and column positions to determine ordering
|
|
242
|
+
*/
|
|
243
|
+
export function getNodesAfterLocation<T extends Node>(nodes: T[], location: Location): T[] {
|
|
244
|
+
return nodes.filter(node =>
|
|
245
|
+
node.location && isPositionAfter(node.location.start, location.end)
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Splits nodes into before and after the specified location
|
|
251
|
+
* Returns an object with `before` and `after` arrays
|
|
252
|
+
*/
|
|
253
|
+
export function splitNodesAroundLocation<T extends Node>(nodes: T[], location: Location): { before: T[], after: T[] } {
|
|
254
|
+
return {
|
|
255
|
+
before: getNodesBeforeLocation(nodes, location),
|
|
256
|
+
after: getNodesAfterLocation(nodes, location)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Splits nodes at a specific position
|
|
262
|
+
* Returns nodes that end before the position and nodes that start after the position
|
|
263
|
+
* More precise than splitNodesAroundLocation as it uses a single position point
|
|
264
|
+
* Uses the same defaults as the individual functions: before=exclusive, after=inclusive
|
|
265
|
+
*/
|
|
266
|
+
export function splitNodesAroundPosition<T extends Node>(nodes: T[], position: Position): { before: T[], after: T[] } {
|
|
267
|
+
return {
|
|
268
|
+
before: getNodesBeforePosition(nodes, position), // uses default: inclusive = false
|
|
269
|
+
after: getNodesAfterPosition(nodes, position) // uses default: inclusive = true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Gets nodes that end before the specified position
|
|
275
|
+
* @param inclusive - If true, includes nodes that end exactly at the position (default: false, matching half-open interval semantics)
|
|
276
|
+
*/
|
|
277
|
+
export function getNodesBeforePosition<T extends Node>(nodes: T[], position: Position, inclusive = false): T[] {
|
|
278
|
+
return nodes.filter(node =>
|
|
279
|
+
node.location && isPositionBefore(node.location.end, position, inclusive)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Gets nodes that start after the specified position
|
|
285
|
+
* @param inclusive - If true, includes nodes that start exactly at the position (default: true, matching typical boundary behavior)
|
|
286
|
+
*/
|
|
287
|
+
export function getNodesAfterPosition<T extends Node>(nodes: T[], position: Position, inclusive = true): T[] {
|
|
288
|
+
return nodes.filter(node =>
|
|
289
|
+
node.location && isPositionAfter(node.location.start, position, inclusive)
|
|
290
|
+
)
|
|
291
|
+
}
|
package/src/backend.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { SerializedParseResult } from "./parse-result.js"
|
|
2
2
|
import type { SerializedLexResult } from "./lex-result.js"
|
|
3
|
+
import type { ParserOptions } from "./parser-options.js"
|
|
3
4
|
|
|
4
5
|
interface LibHerbBackendFunctions {
|
|
5
6
|
lex: (source: string) => SerializedLexResult
|
|
6
7
|
lexFile: (path: string) => SerializedLexResult
|
|
7
8
|
|
|
8
|
-
parse: (source: string) => SerializedParseResult
|
|
9
|
+
parse: (source: string, options?: ParserOptions) => SerializedParseResult
|
|
9
10
|
parseFile: (path: string) => SerializedParseResult
|
|
10
11
|
|
|
11
12
|
extractRuby: (source: string) => string
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// NOTE: This file is generated by the templates/template.rb script and should not
|
|
2
|
-
// be modified manually. See /Users/marcoroth/Development/herb-release-0.
|
|
2
|
+
// be modified manually. See /Users/marcoroth/Development/herb-release-0.6.1/templates/javascript/packages/core/src/errors.ts.erb
|
|
3
3
|
|
|
4
4
|
import { Location, SerializedLocation } from "./location.js"
|
|
5
5
|
import { Token, SerializedToken } from "./token.js"
|
package/src/herb-backend.ts
CHANGED
|
@@ -3,8 +3,10 @@ import packageJSON from "../package.json" with { type: "json" }
|
|
|
3
3
|
import { ensureString } from "./util.js"
|
|
4
4
|
import { LexResult } from "./lex-result.js"
|
|
5
5
|
import { ParseResult } from "./parse-result.js"
|
|
6
|
+
import { DEFAULT_PARSER_OPTIONS } from "./parser-options.js"
|
|
6
7
|
|
|
7
8
|
import type { LibHerbBackend, BackendPromise } from "./backend.js"
|
|
9
|
+
import type { ParserOptions } from "./parser-options.js"
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* The main Herb parser interface, providing methods to lex and parse input.
|
|
@@ -64,13 +66,16 @@ export abstract class HerbBackend {
|
|
|
64
66
|
/**
|
|
65
67
|
* Parses the given source string into a `ParseResult`.
|
|
66
68
|
* @param source - The source code to parse.
|
|
69
|
+
* @param options - Optional parsing options.
|
|
67
70
|
* @returns A `ParseResult` instance.
|
|
68
71
|
* @throws Error if the backend is not loaded.
|
|
69
72
|
*/
|
|
70
|
-
parse(source: string): ParseResult {
|
|
73
|
+
parse(source: string, options?: ParserOptions): ParseResult {
|
|
71
74
|
this.ensureBackend()
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
const mergedOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
|
|
77
|
+
|
|
78
|
+
return ParseResult.from(this.backend.parse(ensureString(source), mergedOptions))
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./ast-utils.js"
|
|
1
2
|
export * from "./backend.js"
|
|
2
3
|
export * from "./diagnostic.js"
|
|
3
4
|
export * from "./errors.js"
|
|
@@ -5,7 +6,9 @@ export * from "./herb-backend.js"
|
|
|
5
6
|
export * from "./lex-result.js"
|
|
6
7
|
export * from "./location.js"
|
|
7
8
|
export * from "./nodes.js"
|
|
9
|
+
export * from "./node-type-guards.js"
|
|
8
10
|
export * from "./parse-result.js"
|
|
11
|
+
export * from "./parser-options.js"
|
|
9
12
|
export * from "./position.js"
|
|
10
13
|
export * from "./range.js"
|
|
11
14
|
export * from "./result.js"
|