@herb-tools/core 0.5.0 → 0.6.0
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 +1223 -432
- package/dist/herb-core.browser.js.map +1 -1
- package/dist/herb-core.cjs +1314 -432
- package/dist/herb-core.cjs.map +1 -1
- package/dist/herb-core.esm.js +1223 -432
- package/dist/herb-core.esm.js.map +1 -1
- package/dist/herb-core.umd.js +1314 -432
- package/dist/herb-core.umd.js.map +1 -1
- package/dist/types/ast-utils.d.ts +74 -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 +149 -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 +191 -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 +876 -0
- package/src/nodes.ts +300 -251
- package/src/parser-options.ts +7 -0
- package/src/visitor.ts +11 -6
package/src/ast-utils.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LiteralNode,
|
|
3
|
+
ERBIfNode,
|
|
4
|
+
ERBUnlessNode,
|
|
5
|
+
ERBBlockNode,
|
|
6
|
+
ERBCaseNode,
|
|
7
|
+
ERBCaseMatchNode,
|
|
8
|
+
ERBWhileNode,
|
|
9
|
+
ERBForNode,
|
|
10
|
+
ERBBeginNode,
|
|
11
|
+
HTMLElementNode,
|
|
12
|
+
HTMLOpenTagNode,
|
|
13
|
+
HTMLCloseTagNode,
|
|
14
|
+
HTMLCommentNode
|
|
15
|
+
} from "./nodes.js"
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
isNode,
|
|
19
|
+
isAnyOf,
|
|
20
|
+
isLiteralNode,
|
|
21
|
+
isERBNode,
|
|
22
|
+
isERBContentNode,
|
|
23
|
+
areAllOfType,
|
|
24
|
+
filterLiteralNodes
|
|
25
|
+
} from "./node-type-guards.js"
|
|
26
|
+
|
|
27
|
+
import { Node, ERBContentNode, HTMLAttributeNameNode } from "./nodes.js"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
|
|
31
|
+
*/
|
|
32
|
+
export function isERBOutputNode(node: Node): node is ERBContentNode {
|
|
33
|
+
return isNode(node, ERBContentNode) && ["<%=", "<%=="].includes((node as ERBContentNode).tag_opening?.value!)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a node is a non-output ERB node (control flow: <% %>)
|
|
38
|
+
*/
|
|
39
|
+
export function isERBControlFlowNode(node: Node): node is ERBContentNode {
|
|
40
|
+
return isAnyOf(node, ERBIfNode, ERBUnlessNode, ERBBlockNode, ERBCaseNode, ERBCaseMatchNode, ERBWhileNode, ERBForNode, ERBBeginNode)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if an array of nodes contains any ERB content nodes
|
|
45
|
+
*/
|
|
46
|
+
export function hasERBContent(nodes: Node[]): boolean {
|
|
47
|
+
return nodes.some(isERBContentNode)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks if an array of nodes contains any ERB output nodes (dynamic content)
|
|
52
|
+
*/
|
|
53
|
+
export function hasERBOutput(nodes: Node[]): boolean {
|
|
54
|
+
return nodes.some(isERBOutputNode)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extracts a static string from an array of literal nodes
|
|
60
|
+
* Returns null if any node is not a literal node
|
|
61
|
+
*/
|
|
62
|
+
export function getStaticStringFromNodes(nodes: Node[]): string | null {
|
|
63
|
+
if (!areAllOfType(nodes, LiteralNode)) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nodes.map(node => node.content).join("")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extracts static content from nodes, including mixed literal/ERB content
|
|
72
|
+
* Returns the concatenated literal content, or null if no literal nodes exist
|
|
73
|
+
*/
|
|
74
|
+
export function getStaticContentFromNodes(nodes: Node[]): string | null {
|
|
75
|
+
const literalNodes = filterLiteralNodes(nodes)
|
|
76
|
+
|
|
77
|
+
if (literalNodes.length === 0) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return literalNodes.map(node => node.content).join("")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks if nodes contain any literal content (for static validation)
|
|
86
|
+
*/
|
|
87
|
+
export function hasStaticContent(nodes: Node[]): boolean {
|
|
88
|
+
return nodes.some(isLiteralNode)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks if nodes are effectively static (only literals and non-output ERB)
|
|
93
|
+
* Non-output ERB like <% if %> doesn't affect static validation
|
|
94
|
+
*/
|
|
95
|
+
export function isEffectivelyStatic(nodes: Node[]): boolean {
|
|
96
|
+
return !hasERBOutput(nodes)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets static-validatable content from nodes (ignores control ERB, includes literals)
|
|
101
|
+
* Returns concatenated literal content for validation, or null if contains output ERB
|
|
102
|
+
*/
|
|
103
|
+
export function getValidatableStaticContent(nodes: Node[]): string | null {
|
|
104
|
+
if (hasERBOutput(nodes)) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return filterLiteralNodes(nodes).map(node => node.content).join("")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extracts a combined string from nodes, including ERB content
|
|
113
|
+
* For ERB nodes, includes the full tag syntax (e.g., "<%= foo %>")
|
|
114
|
+
* This is useful for debugging or displaying the full attribute name
|
|
115
|
+
*/
|
|
116
|
+
export function getCombinedStringFromNodes(nodes: Node[]): string {
|
|
117
|
+
return nodes.map(node => {
|
|
118
|
+
if (isLiteralNode(node)) {
|
|
119
|
+
return node.content
|
|
120
|
+
} else if (isERBContentNode(node)) {
|
|
121
|
+
const opening = node.tag_opening?.value || ""
|
|
122
|
+
const content = node.content?.value || ""
|
|
123
|
+
const closing = node.tag_closing?.value || ""
|
|
124
|
+
|
|
125
|
+
return `${opening}${content}${closing}`
|
|
126
|
+
} else {
|
|
127
|
+
// For other node types, return a placeholder or empty string
|
|
128
|
+
return `[${node.type}]`
|
|
129
|
+
}
|
|
130
|
+
}).join("")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks if an HTML attribute name node has a static (literal-only) name
|
|
135
|
+
*/
|
|
136
|
+
export function hasStaticAttributeName(attributeNameNode: HTMLAttributeNameNode): boolean {
|
|
137
|
+
if (!attributeNameNode.children) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return areAllOfType(attributeNameNode.children, LiteralNode)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Checks if an HTML attribute name node has dynamic content (contains ERB)
|
|
146
|
+
*/
|
|
147
|
+
export function hasDynamicAttributeName(attributeNameNode: HTMLAttributeNameNode): boolean {
|
|
148
|
+
if (!attributeNameNode.children) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return hasERBContent(attributeNameNode.children)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets the static string value of an HTML attribute name node
|
|
157
|
+
* Returns null if the attribute name contains dynamic content (ERB)
|
|
158
|
+
*/
|
|
159
|
+
export function getStaticAttributeName(attributeNameNode: HTMLAttributeNameNode): string | null {
|
|
160
|
+
if (!attributeNameNode.children) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return getStaticStringFromNodes(attributeNameNode.children)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Gets the combined string representation of an HTML attribute name node
|
|
169
|
+
* This includes both static and dynamic content, useful for debugging
|
|
170
|
+
*/
|
|
171
|
+
export function getCombinedAttributeName(attributeNameNode: HTMLAttributeNameNode): string {
|
|
172
|
+
if (!attributeNameNode.children) {
|
|
173
|
+
return ""
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return getCombinedStringFromNodes(attributeNameNode.children)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Gets the tag name of an HTML element node
|
|
181
|
+
*/
|
|
182
|
+
export function getTagName(node: HTMLElementNode | HTMLOpenTagNode | HTMLCloseTagNode): string {
|
|
183
|
+
return node.tag_name?.value ?? ""
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if a node is a comment (HTML comment or ERB comment)
|
|
188
|
+
*/
|
|
189
|
+
export function isCommentNode(node: Node): boolean {
|
|
190
|
+
return isNode(node, HTMLCommentNode) || (isERBNode(node) && !isERBControlFlowNode(node))
|
|
191
|
+
}
|
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.0/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"
|