@herb-tools/core 0.8.10 → 0.9.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.
@@ -3,10 +3,12 @@ import { Result } from "./result.js"
3
3
  import { DocumentNode } from "./nodes.js"
4
4
  import { HerbError } from "./errors.js"
5
5
  import { HerbWarning } from "./warning.js"
6
+ import { ParserOptions } from "./parser-options.js"
6
7
 
7
8
  import type { SerializedHerbError } from "./errors.js"
8
9
  import type { SerializedHerbWarning } from "./warning.js"
9
10
  import type { SerializedDocumentNode } from "./nodes.js"
11
+ import type { SerializedParserOptions } from "./parser-options.js"
10
12
 
11
13
  import type { Visitor } from "./visitor.js"
12
14
 
@@ -15,6 +17,7 @@ export type SerializedParseResult = {
15
17
  source: string
16
18
  warnings: SerializedHerbWarning[]
17
19
  errors: SerializedHerbError[]
20
+ options: SerializedParserOptions
18
21
  }
19
22
 
20
23
  /**
@@ -25,6 +28,9 @@ export class ParseResult extends Result {
25
28
  /** The document node generated from the source code. */
26
29
  readonly value: DocumentNode
27
30
 
31
+ /** The parser options used during parsing. */
32
+ readonly options: ParserOptions
33
+
28
34
  /**
29
35
  * Creates a `ParseResult` instance from a serialized result.
30
36
  * @param result - The serialized parse result containing the value and source.
@@ -36,6 +42,7 @@ export class ParseResult extends Result {
36
42
  result.source,
37
43
  result.warnings.map((warning) => HerbWarning.from(warning)),
38
44
  result.errors.map((error) => HerbError.from(error)),
45
+ ParserOptions.from(result.options),
39
46
  )
40
47
  }
41
48
 
@@ -45,15 +52,19 @@ export class ParseResult extends Result {
45
52
  * @param source - The source code that was parsed.
46
53
  * @param warnings - An array of warnings encountered during parsing.
47
54
  * @param errors - An array of errors encountered during parsing.
55
+ * @param options - The parser options used during parsing.
48
56
  */
49
57
  constructor(
50
58
  value: DocumentNode,
51
59
  source: string,
52
60
  warnings: HerbWarning[] = [],
53
61
  errors: HerbError[] = [],
62
+ options: ParserOptions = new ParserOptions(),
54
63
  ) {
55
64
  super(source, warnings, errors)
56
65
  this.value = value
66
+ this.options = options
67
+ this.value.setSource(source)
57
68
  }
58
69
 
59
70
  /**
@@ -1,7 +1,61 @@
1
- export interface ParserOptions {
1
+ export interface ParseOptions {
2
2
  track_whitespace?: boolean
3
+ analyze?: boolean
4
+ strict?: boolean
5
+ action_view_helpers?: boolean
6
+ prism_nodes?: boolean
7
+ prism_nodes_deep?: boolean
8
+ prism_program?: boolean
3
9
  }
4
10
 
5
- export const DEFAULT_PARSER_OPTIONS: ParserOptions = {
11
+ export type SerializedParserOptions = Required<ParseOptions>
12
+
13
+ export const DEFAULT_PARSER_OPTIONS: SerializedParserOptions = {
6
14
  track_whitespace: false,
15
+ analyze: true,
16
+ strict: true,
17
+ action_view_helpers: false,
18
+ prism_nodes: false,
19
+ prism_nodes_deep: false,
20
+ prism_program: false,
21
+ }
22
+
23
+ /**
24
+ * Represents the parser options used during parsing.
25
+ */
26
+ export class ParserOptions {
27
+ /** Whether strict mode was enabled during parsing. */
28
+ readonly strict: boolean
29
+
30
+ /** Whether whitespace tracking was enabled during parsing. */
31
+ readonly track_whitespace: boolean
32
+
33
+ /** Whether analysis was performed during parsing. */
34
+ readonly analyze: boolean
35
+
36
+ /** Whether ActionView tag helper transformation was enabled during parsing. */
37
+ readonly action_view_helpers: boolean
38
+
39
+ /** Whether Prism node serialization was enabled during parsing. */
40
+ readonly prism_nodes: boolean
41
+
42
+ /** Whether deep Prism node serialization was enabled during parsing. */
43
+ readonly prism_nodes_deep: boolean
44
+
45
+ /** Whether the full Prism ProgramNode was serialized on the DocumentNode. */
46
+ readonly prism_program: boolean
47
+
48
+ static from(options: SerializedParserOptions): ParserOptions {
49
+ return new ParserOptions(options)
50
+ }
51
+
52
+ constructor(options: ParseOptions = {}) {
53
+ this.strict = options.strict ?? DEFAULT_PARSER_OPTIONS.strict
54
+ this.track_whitespace = options.track_whitespace ?? DEFAULT_PARSER_OPTIONS.track_whitespace
55
+ this.analyze = options.analyze ?? DEFAULT_PARSER_OPTIONS.analyze
56
+ this.action_view_helpers = options.action_view_helpers ?? DEFAULT_PARSER_OPTIONS.action_view_helpers
57
+ this.prism_nodes = options.prism_nodes ?? DEFAULT_PARSER_OPTIONS.prism_nodes
58
+ this.prism_nodes_deep = options.prism_nodes_deep ?? DEFAULT_PARSER_OPTIONS.prism_nodes_deep
59
+ this.prism_program = options.prism_program ?? DEFAULT_PARSER_OPTIONS.prism_program
60
+ }
7
61
  }
@@ -0,0 +1,44 @@
1
+ import { deserialize } from "@ruby/prism/src/deserialize.js"
2
+ import type { ParseResult as PrismParseResult } from "@ruby/prism/src/deserialize.js"
3
+
4
+ export * as PrismNodes from "@ruby/prism/src/nodes.js"
5
+
6
+ export { Visitor as PrismVisitor, BasicVisitor as PrismBasicVisitor } from "@ruby/prism/src/visitor.js"
7
+
8
+ export type PrismNode = any
9
+ export type PrismLocation = { startOffset: number; length: number }
10
+ export type { PrismParseResult }
11
+
12
+ export { inspectPrismNode, inspectPrismSerialized } from "./inspect.js"
13
+
14
+ /**
15
+ * Deserialize a Prism parse result from the raw bytes produced by pm_serialize().
16
+ *
17
+ * @param bytes - The serialized bytes (from prism_serialized field on ERB nodes)
18
+ * @param source - The original source string that was parsed
19
+ * @returns The deserialized Prism ParseResult containing the AST
20
+ */
21
+ export function deserializePrismParseResult(bytes: Uint8Array, source: string): PrismParseResult {
22
+ const sourceBytes = new TextEncoder().encode(source)
23
+
24
+ return deserialize(sourceBytes, bytes)
25
+ }
26
+
27
+ /**
28
+ * Deserialize a Prism node from the raw bytes produced by pm_serialize().
29
+ * pm_serialize() serializes a single node subtree, so the ParseResult's
30
+ * value is the Prism node directly (not wrapped in ProgramNode).
31
+ *
32
+ * @param bytes - The serialized bytes (from prism_serialized field on ERB nodes)
33
+ * @param source - The original source string that was parsed
34
+ * @returns The Prism node, or null if deserialization fails
35
+ */
36
+ export function deserializePrismNode(bytes: Uint8Array, source: string): PrismNode | null {
37
+ try {
38
+ const result = deserializePrismParseResult(bytes, source)
39
+
40
+ return result.value ?? null
41
+ } catch {
42
+ return null
43
+ }
44
+ }
@@ -0,0 +1,118 @@
1
+ import { deserializePrismNode } from "./index.js"
2
+
3
+ import type { PrismLocation } from "./index.js"
4
+ import type { PrismNode } from "./index.js"
5
+
6
+ function offsetToLineColumn(source: string, offset: number): string {
7
+ let line = 1
8
+ let column = 0
9
+
10
+ for (let i = 0; i < offset && i < source.length; i++) {
11
+ if (source[i] === "\n") {
12
+ line++
13
+ column = 0
14
+ } else {
15
+ column++
16
+ }
17
+ }
18
+
19
+ return `${line}:${column}`
20
+ }
21
+
22
+ function formatLocation(location: PrismLocation, source: string): string {
23
+ const start = offsetToLineColumn(source, location.startOffset)
24
+ const end = offsetToLineColumn(source, location.startOffset + location.length)
25
+
26
+ return `(${start})-(${end})`
27
+ }
28
+
29
+ function isPrismNode(value: any): boolean {
30
+ return value && typeof value === "object" && typeof value.toJSON === "function" && value.location
31
+ }
32
+
33
+ export function inspectPrismNode(node: PrismNode, source: string, prefix: string = ""): string {
34
+ if (!node) return "∅\n"
35
+
36
+ const nodeName = typeof node.toJSON === "function" ? node.toJSON().type : node.constructor.name
37
+ let output = ""
38
+
39
+ output += `@ ${nodeName}`
40
+
41
+ if (node.location) {
42
+ output += ` (location: ${formatLocation(node.location, source)})`
43
+ }
44
+
45
+ output += "\n"
46
+
47
+ const fields = getNodeFields(node)
48
+
49
+ fields.forEach((field, index) => {
50
+ const isLastField = index === fields.length - 1
51
+ const symbol = isLastField ? "└── " : "├── "
52
+ const childPrefix = prefix + (isLastField ? " " : "│ ")
53
+ const value = node[field]
54
+
55
+ if (value === null || value === undefined) {
56
+ output += `${prefix}${symbol}${field}: ∅\n`
57
+ } else if (typeof value === "string") {
58
+ output += `${prefix}${symbol}${field}: ${JSON.stringify(value)}\n`
59
+ } else if (typeof value === "number" || typeof value === "boolean") {
60
+ output += `${prefix}${symbol}${field}: ${value}\n`
61
+ } else if (Array.isArray(value)) {
62
+ output += `${prefix}${symbol}${field}: `
63
+
64
+ if (value.length === 0) {
65
+ output += "[]\n"
66
+ } else {
67
+ output += `(${value.length} item${value.length === 1 ? "" : "s"})\n`
68
+
69
+ value.forEach((item: any, i: number) => {
70
+ const isLastItem = i === value.length - 1
71
+ const itemSymbol = isLastItem ? "└── " : "├── "
72
+ const itemPrefix = childPrefix + (isLastItem ? " " : "│ ")
73
+
74
+ if (isPrismNode(item)) {
75
+ output += `${childPrefix}${itemSymbol}${inspectPrismNode(item, source, itemPrefix).trimStart()}`
76
+ } else {
77
+ output += `${childPrefix}${itemSymbol}${item}\n`
78
+ }
79
+ })
80
+ }
81
+ } else if (isPrismNode(value)) {
82
+ output += `${prefix}${symbol}${field}:\n`
83
+ output += `${childPrefix}└── ${inspectPrismNode(value, source, childPrefix + " ").trimStart()}`
84
+ } else if (typeof value === "object" && value.startOffset !== undefined) {
85
+ output += `${prefix}${symbol}${field}: (location: ${formatLocation(value, source)})\n`
86
+ } else if (typeof value === "object" && "value" in value && "encoding" in value) {
87
+ output += `${prefix}${symbol}${field}: ${JSON.stringify(value.value)}\n`
88
+ } else {
89
+ output += `${prefix}${symbol}${field}: ${String(value)}\n`
90
+ }
91
+ })
92
+
93
+ return output
94
+ }
95
+
96
+ function getNodeFields(node: PrismNode): string[] {
97
+ const skip = new Set(["nodeID", "location", "flags"])
98
+ const fields: string[] = []
99
+
100
+ for (const key of Object.keys(node)) {
101
+ if (!skip.has(key)) {
102
+ fields.push(key)
103
+ }
104
+ }
105
+
106
+ return fields
107
+ }
108
+
109
+ export function inspectPrismSerialized(bytes: Uint8Array, source: string, prefix: string = ""): string {
110
+ try {
111
+ const node = deserializePrismNode(bytes, source)
112
+ if (!node) return "∅"
113
+
114
+ return "\n" + prefix + "└── " + inspectPrismNode(node, source, prefix + " ").trimStart().trimEnd()
115
+ } catch {
116
+ return `(${bytes.length} bytes, deserialize error)`
117
+ }
118
+ }
package/src/util.ts CHANGED
@@ -5,15 +5,3 @@ export function ensureString(object: any): string {
5
5
 
6
6
  throw new TypeError("Argument must be a string")
7
7
  }
8
-
9
- export function convertToUTF8(string: string) {
10
- const bytes = []
11
-
12
- for (let i = 0; i < string.length; i++) {
13
- bytes.push(string.charCodeAt(i))
14
- }
15
-
16
- const decoder = new TextDecoder("utf-8")
17
-
18
- return decoder.decode(new Uint8Array(bytes))
19
- }
package/src/visitor.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.8.10/templates/javascript/packages/core/src/visitor.ts.erb
2
+ // be modified manually. See /Users/marcoroth/Development/herb-release-0.9.0/templates/javascript/packages/core/src/visitor.ts.erb
3
3
 
4
4
  import {
5
5
  Node,
@@ -7,11 +7,18 @@ import {
7
7
  DocumentNode,
8
8
  LiteralNode,
9
9
  HTMLOpenTagNode,
10
+ HTMLConditionalOpenTagNode,
10
11
  HTMLCloseTagNode,
12
+ HTMLOmittedCloseTagNode,
13
+ HTMLVirtualCloseTagNode,
11
14
  HTMLElementNode,
15
+ HTMLConditionalElementNode,
12
16
  HTMLAttributeValueNode,
13
17
  HTMLAttributeNameNode,
14
18
  HTMLAttributeNode,
19
+ RubyLiteralNode,
20
+ RubyHTMLAttributesSplatNode,
21
+ ERBOpenTagNode,
15
22
  HTMLTextNode,
16
23
  HTMLCommentNode,
17
24
  HTMLDoctypeNode,
@@ -48,11 +55,18 @@ export interface IVisitor {
48
55
  visitDocumentNode(node: DocumentNode): void
49
56
  visitLiteralNode(node: LiteralNode): void
50
57
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void
58
+ visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode): void
51
59
  visitHTMLCloseTagNode(node: HTMLCloseTagNode): void
60
+ visitHTMLOmittedCloseTagNode(node: HTMLOmittedCloseTagNode): void
61
+ visitHTMLVirtualCloseTagNode(node: HTMLVirtualCloseTagNode): void
52
62
  visitHTMLElementNode(node: HTMLElementNode): void
63
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void
53
64
  visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void
54
65
  visitHTMLAttributeNameNode(node: HTMLAttributeNameNode): void
55
66
  visitHTMLAttributeNode(node: HTMLAttributeNode): void
67
+ visitRubyLiteralNode(node: RubyLiteralNode): void
68
+ visitRubyHTMLAttributesSplatNode(node: RubyHTMLAttributesSplatNode): void
69
+ visitERBOpenTagNode(node: ERBOpenTagNode): void
56
70
  visitHTMLTextNode(node: HTMLTextNode): void
57
71
  visitHTMLCommentNode(node: HTMLCommentNode): void
58
72
  visitHTMLDoctypeNode(node: HTMLDoctypeNode): void
@@ -118,16 +132,36 @@ export class Visitor implements IVisitor {
118
132
  this.visitChildNodes(node)
119
133
  }
120
134
 
135
+ visitHTMLConditionalOpenTagNode(node: HTMLConditionalOpenTagNode): void {
136
+ this.visitNode(node)
137
+ this.visitChildNodes(node)
138
+ }
139
+
121
140
  visitHTMLCloseTagNode(node: HTMLCloseTagNode): void {
122
141
  this.visitNode(node)
123
142
  this.visitChildNodes(node)
124
143
  }
125
144
 
145
+ visitHTMLOmittedCloseTagNode(node: HTMLOmittedCloseTagNode): void {
146
+ this.visitNode(node)
147
+ this.visitChildNodes(node)
148
+ }
149
+
150
+ visitHTMLVirtualCloseTagNode(node: HTMLVirtualCloseTagNode): void {
151
+ this.visitNode(node)
152
+ this.visitChildNodes(node)
153
+ }
154
+
126
155
  visitHTMLElementNode(node: HTMLElementNode): void {
127
156
  this.visitNode(node)
128
157
  this.visitChildNodes(node)
129
158
  }
130
159
 
160
+ visitHTMLConditionalElementNode(node: HTMLConditionalElementNode): void {
161
+ this.visitNode(node)
162
+ this.visitChildNodes(node)
163
+ }
164
+
131
165
  visitHTMLAttributeValueNode(node: HTMLAttributeValueNode): void {
132
166
  this.visitNode(node)
133
167
  this.visitChildNodes(node)
@@ -143,6 +177,22 @@ export class Visitor implements IVisitor {
143
177
  this.visitChildNodes(node)
144
178
  }
145
179
 
180
+ visitRubyLiteralNode(node: RubyLiteralNode): void {
181
+ this.visitNode(node)
182
+ this.visitChildNodes(node)
183
+ }
184
+
185
+ visitRubyHTMLAttributesSplatNode(node: RubyHTMLAttributesSplatNode): void {
186
+ this.visitNode(node)
187
+ this.visitChildNodes(node)
188
+ }
189
+
190
+ visitERBOpenTagNode(node: ERBOpenTagNode): void {
191
+ this.visitNode(node)
192
+ this.visitERBNode(node)
193
+ this.visitChildNodes(node)
194
+ }
195
+
146
196
  visitHTMLTextNode(node: HTMLTextNode): void {
147
197
  this.visitNode(node)
148
198
  this.visitChildNodes(node)