@herb-tools/core 0.7.5 → 0.8.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/README.md +1 -1
- package/dist/herb-core.browser.js +297 -15
- package/dist/herb-core.browser.js.map +1 -1
- package/dist/herb-core.cjs +301 -14
- package/dist/herb-core.cjs.map +1 -1
- package/dist/herb-core.esm.js +297 -15
- package/dist/herb-core.esm.js.map +1 -1
- package/dist/herb-core.umd.js +301 -14
- package/dist/herb-core.umd.js.map +1 -1
- package/dist/types/ast-utils.d.ts +7 -2
- package/dist/types/didyoumean.d.ts +46 -0
- package/dist/types/errors.d.ts +41 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/levenshtein.d.ts +1 -0
- package/dist/types/location.d.ts +2 -0
- package/dist/types/position.d.ts +2 -0
- package/dist/types/range.d.ts +2 -0
- package/package.json +1 -2
- package/src/ast-utils.ts +9 -2
- package/src/didyoumean.ts +88 -0
- package/src/errors.ts +129 -1
- package/src/index.ts +3 -1
- package/src/levenshtein.ts +119 -0
- package/src/location.ts +17 -4
- package/src/node-type-guards.ts +1 -1
- package/src/nodes.ts +1 -1
- package/src/position.ts +12 -2
- package/src/range.ts +12 -2
- package/src/visitor.ts +1 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { Node, ERBContentNode, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLAttributeNameNode } from "./nodes.js";
|
|
1
|
+
import { Node, ERBNode, ERBContentNode, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLAttributeNameNode } from "./nodes.js";
|
|
2
2
|
import type { Location } from "./location.js";
|
|
3
3
|
import type { Position } from "./position.js";
|
|
4
|
+
export type ERBOutputNode = ERBNode & {
|
|
5
|
+
tag_opening: {
|
|
6
|
+
value: "<%=" | "<%==";
|
|
7
|
+
};
|
|
8
|
+
};
|
|
4
9
|
/**
|
|
5
10
|
* Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
|
|
6
11
|
*/
|
|
7
|
-
export declare function isERBOutputNode(node: Node): node is
|
|
12
|
+
export declare function isERBOutputNode(node: Node): node is ERBOutputNode;
|
|
8
13
|
/**
|
|
9
14
|
* Checks if a node is a non-output ERB node (control flow: <% %>)
|
|
10
15
|
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ranked result with item and distance score.
|
|
3
|
+
*/
|
|
4
|
+
export interface RankedResult {
|
|
5
|
+
item: string;
|
|
6
|
+
score: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Finds the closest matching string from a list using Levenshtein distance.
|
|
10
|
+
* Performs case-insensitive comparison.
|
|
11
|
+
*
|
|
12
|
+
* @param input - The string to match against
|
|
13
|
+
* @param list - The list of candidate strings to search
|
|
14
|
+
* @param threshold - Maximum Levenshtein distance to consider a match. If undefined, returns the closest match regardless of distance.
|
|
15
|
+
* @returns The closest matching string from the list, or null if the list is empty or no match is within the threshold
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* didyoumean('speling', ['spelling', 'writing', 'reading']) // Returns 'spelling'
|
|
20
|
+
* didyoumean('test', []) // Returns null
|
|
21
|
+
* didyoumean('speling', ['spelling', 'writing', 'reading'], 2) // Returns 'spelling' (distance: 1)
|
|
22
|
+
* didyoumean('xyz', ['spelling', 'writing', 'reading'], 2) // Returns null (all distances > 2)
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function didyoumean(input: string, list: string[], threshold?: number): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Returns all strings from a list ranked by their Levenshtein distance from the input string.
|
|
28
|
+
* Performs case-insensitive comparison. Results are sorted with closest matches first.
|
|
29
|
+
*
|
|
30
|
+
* @param input - The string to match against
|
|
31
|
+
* @param list - The list of candidate strings to rank
|
|
32
|
+
* @param threshold - Maximum Levenshtein distance to include in results. If undefined, returns all ranked results.
|
|
33
|
+
* @returns An array of ranked results with items and scores, or an empty array if the list is empty or no matches are within the threshold
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* didyoumeanRanked('speling', ['spelling', 'writing', 'reading'])
|
|
38
|
+
* // Returns [{ item: 'spelling', score: 1 }, { item: 'reading', score: 5 }, { item: 'writing', score: 6 }]
|
|
39
|
+
*
|
|
40
|
+
* didyoumeanRanked('speling', ['spelling', 'writing', 'reading'], 2)
|
|
41
|
+
* // Returns [{ item: 'spelling', score: 1 }]
|
|
42
|
+
*
|
|
43
|
+
* didyoumeanRanked('test', []) // Returns []
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function didyoumeanRanked(input: string, list: string[], threshold?: number): RankedResult[];
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Location, SerializedLocation } from "./location.js";
|
|
2
2
|
import { Token, SerializedToken } from "./token.js";
|
|
3
3
|
import { Diagnostic, MonacoDiagnostic } from "./diagnostic.js";
|
|
4
|
-
export type HerbErrorType = "UNEXPECTED_ERROR" | "UNEXPECTED_TOKEN_ERROR" | "MISSING_OPENING_TAG_ERROR" | "MISSING_CLOSING_TAG_ERROR" | "TAG_NAMES_MISMATCH_ERROR" | "QUOTES_MISMATCH_ERROR" | "VOID_ELEMENT_CLOSING_TAG_ERROR" | "UNCLOSED_ELEMENT_ERROR" | "RUBY_PARSE_ERROR";
|
|
4
|
+
export type HerbErrorType = "UNEXPECTED_ERROR" | "UNEXPECTED_TOKEN_ERROR" | "MISSING_OPENING_TAG_ERROR" | "MISSING_CLOSING_TAG_ERROR" | "TAG_NAMES_MISMATCH_ERROR" | "QUOTES_MISMATCH_ERROR" | "VOID_ELEMENT_CLOSING_TAG_ERROR" | "UNCLOSED_ELEMENT_ERROR" | "RUBY_PARSE_ERROR" | "ERB_CONTROL_FLOW_SCOPE_ERROR" | "MISSINGERB_END_TAG_ERROR";
|
|
5
5
|
export type SerializedErrorType = string;
|
|
6
6
|
export interface SerializedHerbError {
|
|
7
7
|
type: string;
|
|
@@ -228,4 +228,44 @@ export declare class RubyParseError extends HerbError {
|
|
|
228
228
|
toMonacoDiagnostic(): MonacoDiagnostic;
|
|
229
229
|
treeInspect(): string;
|
|
230
230
|
}
|
|
231
|
+
export interface SerializedERBControlFlowScopeError {
|
|
232
|
+
type: "ERB_CONTROL_FLOW_SCOPE_ERROR";
|
|
233
|
+
message: string;
|
|
234
|
+
location: SerializedLocation;
|
|
235
|
+
keyword: string;
|
|
236
|
+
}
|
|
237
|
+
export interface ERBControlFlowScopeErrorProps {
|
|
238
|
+
type: string;
|
|
239
|
+
message: string;
|
|
240
|
+
location: Location;
|
|
241
|
+
keyword: string;
|
|
242
|
+
}
|
|
243
|
+
export declare class ERBControlFlowScopeError extends HerbError {
|
|
244
|
+
readonly keyword: string;
|
|
245
|
+
static from(data: SerializedERBControlFlowScopeError): ERBControlFlowScopeError;
|
|
246
|
+
constructor(props: ERBControlFlowScopeErrorProps);
|
|
247
|
+
toJSON(): SerializedERBControlFlowScopeError;
|
|
248
|
+
toMonacoDiagnostic(): MonacoDiagnostic;
|
|
249
|
+
treeInspect(): string;
|
|
250
|
+
}
|
|
251
|
+
export interface SerializedMissingERBEndTagError {
|
|
252
|
+
type: "MISSINGERB_END_TAG_ERROR";
|
|
253
|
+
message: string;
|
|
254
|
+
location: SerializedLocation;
|
|
255
|
+
keyword: string;
|
|
256
|
+
}
|
|
257
|
+
export interface MissingERBEndTagErrorProps {
|
|
258
|
+
type: string;
|
|
259
|
+
message: string;
|
|
260
|
+
location: Location;
|
|
261
|
+
keyword: string;
|
|
262
|
+
}
|
|
263
|
+
export declare class MissingERBEndTagError extends HerbError {
|
|
264
|
+
readonly keyword: string;
|
|
265
|
+
static from(data: SerializedMissingERBEndTagError): MissingERBEndTagError;
|
|
266
|
+
constructor(props: MissingERBEndTagErrorProps);
|
|
267
|
+
toJSON(): SerializedMissingERBEndTagError;
|
|
268
|
+
toMonacoDiagnostic(): MonacoDiagnostic;
|
|
269
|
+
treeInspect(): string;
|
|
270
|
+
}
|
|
231
271
|
export declare function fromSerializedError(error: SerializedHerbError): HerbError;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export * from "./ast-utils.js";
|
|
2
2
|
export * from "./backend.js";
|
|
3
3
|
export * from "./diagnostic.js";
|
|
4
|
+
export * from "./didyoumean.js";
|
|
4
5
|
export * from "./errors.js";
|
|
5
6
|
export * from "./herb-backend.js";
|
|
7
|
+
export * from "./levenshtein.js";
|
|
6
8
|
export * from "./lex-result.js";
|
|
7
9
|
export * from "./location.js";
|
|
8
|
-
export * from "./nodes.js";
|
|
9
10
|
export * from "./node-type-guards.js";
|
|
11
|
+
export * from "./nodes.js";
|
|
10
12
|
export * from "./parse-result.js";
|
|
11
13
|
export * from "./parser-options.js";
|
|
12
14
|
export * from "./position.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function levenshtein(a: string, b: string): number;
|
package/dist/types/location.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare class Location {
|
|
|
8
8
|
readonly start: Position;
|
|
9
9
|
readonly end: Position;
|
|
10
10
|
static from(location: SerializedLocation): Location;
|
|
11
|
+
static from(line: number, column: number, endLine: number, endColumn: number): Location;
|
|
12
|
+
static get zero(): Location;
|
|
11
13
|
constructor(start: Position, end: Position);
|
|
12
14
|
toHash(): SerializedLocation;
|
|
13
15
|
toJSON(): SerializedLocation;
|
package/dist/types/position.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare class Position {
|
|
|
6
6
|
readonly line: number;
|
|
7
7
|
readonly column: number;
|
|
8
8
|
static from(position: SerializedPosition): Position;
|
|
9
|
+
static from(line: number, column: number): Position;
|
|
10
|
+
static get zero(): Position;
|
|
9
11
|
constructor(line: number, column: number);
|
|
10
12
|
toHash(): SerializedPosition;
|
|
11
13
|
toJSON(): SerializedPosition;
|
package/dist/types/range.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare class Range {
|
|
|
3
3
|
readonly start: number;
|
|
4
4
|
readonly end: number;
|
|
5
5
|
static from(range: SerializedRange): Range;
|
|
6
|
+
static from(start: number, end: number): Range;
|
|
7
|
+
static get zero(): Range;
|
|
6
8
|
constructor(start: number, end: number);
|
|
7
9
|
toArray(): SerializedRange;
|
|
8
10
|
toJSON(): SerializedRange;
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Core module exporting shared interfaces, AST node definitions, and common utilities for Herb",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"license": "MIT",
|
|
7
6
|
"homepage": "https://herb-tools.dev",
|
|
8
7
|
"bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/core%60:%20",
|
package/src/ast-utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Node,
|
|
3
3
|
LiteralNode,
|
|
4
|
+
ERBNode,
|
|
4
5
|
ERBContentNode,
|
|
5
6
|
ERBIfNode,
|
|
6
7
|
ERBUnlessNode,
|
|
@@ -30,11 +31,17 @@ import {
|
|
|
30
31
|
import type { Location } from "./location.js"
|
|
31
32
|
import type { Position } from "./position.js"
|
|
32
33
|
|
|
34
|
+
export type ERBOutputNode = ERBNode & {
|
|
35
|
+
tag_opening: {
|
|
36
|
+
value: "<%=" | "<%=="
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
/**
|
|
34
41
|
* Checks if a node is an ERB output node (generates content: <%= %> or <%== %>)
|
|
35
42
|
*/
|
|
36
|
-
export function isERBOutputNode(node: Node): node is
|
|
37
|
-
if (!
|
|
43
|
+
export function isERBOutputNode(node: Node): node is ERBOutputNode {
|
|
44
|
+
if (!isERBNode(node)) return false
|
|
38
45
|
if (!node.tag_opening?.value) return false
|
|
39
46
|
|
|
40
47
|
return ["<%=", "<%=="].includes(node.tag_opening?.value)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { levenshtein } from "./levenshtein"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ranked result with item and distance score.
|
|
5
|
+
*/
|
|
6
|
+
export interface RankedResult {
|
|
7
|
+
item: string
|
|
8
|
+
score: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ranks a list of strings by their Levenshtein distance from the input string.
|
|
13
|
+
* Items are sorted in ascending order by distance, with closer matches first.
|
|
14
|
+
*
|
|
15
|
+
* @param input - The string to compare against
|
|
16
|
+
* @param list - The list of strings to rank
|
|
17
|
+
* @returns An array of objects containing the item and its distance score, sorted by score
|
|
18
|
+
*/
|
|
19
|
+
function rank(input: string, list: string[]): RankedResult[] {
|
|
20
|
+
return list.map(item => {
|
|
21
|
+
const score = levenshtein(input.toLowerCase(), item.toLowerCase())
|
|
22
|
+
|
|
23
|
+
return { item, score }
|
|
24
|
+
}).sort((a, b) => a.score - b.score)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds the closest matching string from a list using Levenshtein distance.
|
|
29
|
+
* Performs case-insensitive comparison.
|
|
30
|
+
*
|
|
31
|
+
* @param input - The string to match against
|
|
32
|
+
* @param list - The list of candidate strings to search
|
|
33
|
+
* @param threshold - Maximum Levenshtein distance to consider a match. If undefined, returns the closest match regardless of distance.
|
|
34
|
+
* @returns The closest matching string from the list, or null if the list is empty or no match is within the threshold
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* didyoumean('speling', ['spelling', 'writing', 'reading']) // Returns 'spelling'
|
|
39
|
+
* didyoumean('test', []) // Returns null
|
|
40
|
+
* didyoumean('speling', ['spelling', 'writing', 'reading'], 2) // Returns 'spelling' (distance: 1)
|
|
41
|
+
* didyoumean('xyz', ['spelling', 'writing', 'reading'], 2) // Returns null (all distances > 2)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function didyoumean(input: string, list: string[], threshold?: number): string | null {
|
|
45
|
+
if (list.length === 0) return null
|
|
46
|
+
|
|
47
|
+
const scores = rank(input, list)
|
|
48
|
+
|
|
49
|
+
if (scores.length === 0) return null
|
|
50
|
+
|
|
51
|
+
const closest = scores[0]
|
|
52
|
+
|
|
53
|
+
if (threshold !== undefined && closest.score > threshold) return null
|
|
54
|
+
|
|
55
|
+
return closest.item
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns all strings from a list ranked by their Levenshtein distance from the input string.
|
|
60
|
+
* Performs case-insensitive comparison. Results are sorted with closest matches first.
|
|
61
|
+
*
|
|
62
|
+
* @param input - The string to match against
|
|
63
|
+
* @param list - The list of candidate strings to rank
|
|
64
|
+
* @param threshold - Maximum Levenshtein distance to include in results. If undefined, returns all ranked results.
|
|
65
|
+
* @returns An array of ranked results with items and scores, or an empty array if the list is empty or no matches are within the threshold
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* didyoumeanRanked('speling', ['spelling', 'writing', 'reading'])
|
|
70
|
+
* // Returns [{ item: 'spelling', score: 1 }, { item: 'reading', score: 5 }, { item: 'writing', score: 6 }]
|
|
71
|
+
*
|
|
72
|
+
* didyoumeanRanked('speling', ['spelling', 'writing', 'reading'], 2)
|
|
73
|
+
* // Returns [{ item: 'spelling', score: 1 }]
|
|
74
|
+
*
|
|
75
|
+
* didyoumeanRanked('test', []) // Returns []
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function didyoumeanRanked(input: string, list: string[], threshold?: number): RankedResult[] {
|
|
79
|
+
if (list.length === 0) return []
|
|
80
|
+
|
|
81
|
+
const scores = rank(input, list)
|
|
82
|
+
|
|
83
|
+
if (threshold !== undefined) {
|
|
84
|
+
return scores.filter(result => result.score <= threshold)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return scores
|
|
88
|
+
}
|
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.8.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"
|
|
@@ -15,6 +15,8 @@ export type HerbErrorType =
|
|
|
15
15
|
| "VOID_ELEMENT_CLOSING_TAG_ERROR"
|
|
16
16
|
| "UNCLOSED_ELEMENT_ERROR"
|
|
17
17
|
| "RUBY_PARSE_ERROR"
|
|
18
|
+
| "ERB_CONTROL_FLOW_SCOPE_ERROR"
|
|
19
|
+
| "MISSINGERB_END_TAG_ERROR"
|
|
18
20
|
|
|
19
21
|
export type SerializedErrorType = string
|
|
20
22
|
|
|
@@ -681,6 +683,130 @@ export class RubyParseError extends HerbError {
|
|
|
681
683
|
}
|
|
682
684
|
}
|
|
683
685
|
|
|
686
|
+
export interface SerializedERBControlFlowScopeError {
|
|
687
|
+
type: "ERB_CONTROL_FLOW_SCOPE_ERROR";
|
|
688
|
+
message: string;
|
|
689
|
+
location: SerializedLocation;
|
|
690
|
+
keyword: string;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface ERBControlFlowScopeErrorProps {
|
|
694
|
+
type: string;
|
|
695
|
+
message: string;
|
|
696
|
+
location: Location;
|
|
697
|
+
keyword: string;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export class ERBControlFlowScopeError extends HerbError {
|
|
701
|
+
readonly keyword: string;
|
|
702
|
+
|
|
703
|
+
static from(data: SerializedERBControlFlowScopeError): ERBControlFlowScopeError {
|
|
704
|
+
return new ERBControlFlowScopeError({
|
|
705
|
+
type: data.type,
|
|
706
|
+
message: data.message,
|
|
707
|
+
location: Location.from(data.location),
|
|
708
|
+
keyword: data.keyword,
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
constructor(props: ERBControlFlowScopeErrorProps) {
|
|
713
|
+
super(props.type, props.message, props.location);
|
|
714
|
+
|
|
715
|
+
this.keyword = props.keyword;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
toJSON(): SerializedERBControlFlowScopeError {
|
|
719
|
+
return {
|
|
720
|
+
...super.toJSON(),
|
|
721
|
+
type: "ERB_CONTROL_FLOW_SCOPE_ERROR",
|
|
722
|
+
keyword: this.keyword,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
toMonacoDiagnostic(): MonacoDiagnostic {
|
|
727
|
+
return {
|
|
728
|
+
line: this.location.start.line,
|
|
729
|
+
column: this.location.start.column,
|
|
730
|
+
endLine: this.location.end.line,
|
|
731
|
+
endColumn: this.location.end.column,
|
|
732
|
+
message: this.message,
|
|
733
|
+
severity: 'error'
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
treeInspect(): string {
|
|
738
|
+
let output = "";
|
|
739
|
+
|
|
740
|
+
output += `@ ERBControlFlowScopeError ${this.location.treeInspectWithLabel()}\n`;
|
|
741
|
+
output += `├── message: "${this.message}"\n`;
|
|
742
|
+
output += `└── keyword: ${JSON.stringify(this.keyword)}\n`;
|
|
743
|
+
|
|
744
|
+
return output;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export interface SerializedMissingERBEndTagError {
|
|
749
|
+
type: "MISSINGERB_END_TAG_ERROR";
|
|
750
|
+
message: string;
|
|
751
|
+
location: SerializedLocation;
|
|
752
|
+
keyword: string;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export interface MissingERBEndTagErrorProps {
|
|
756
|
+
type: string;
|
|
757
|
+
message: string;
|
|
758
|
+
location: Location;
|
|
759
|
+
keyword: string;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export class MissingERBEndTagError extends HerbError {
|
|
763
|
+
readonly keyword: string;
|
|
764
|
+
|
|
765
|
+
static from(data: SerializedMissingERBEndTagError): MissingERBEndTagError {
|
|
766
|
+
return new MissingERBEndTagError({
|
|
767
|
+
type: data.type,
|
|
768
|
+
message: data.message,
|
|
769
|
+
location: Location.from(data.location),
|
|
770
|
+
keyword: data.keyword,
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
constructor(props: MissingERBEndTagErrorProps) {
|
|
775
|
+
super(props.type, props.message, props.location);
|
|
776
|
+
|
|
777
|
+
this.keyword = props.keyword;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
toJSON(): SerializedMissingERBEndTagError {
|
|
781
|
+
return {
|
|
782
|
+
...super.toJSON(),
|
|
783
|
+
type: "MISSINGERB_END_TAG_ERROR",
|
|
784
|
+
keyword: this.keyword,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
toMonacoDiagnostic(): MonacoDiagnostic {
|
|
789
|
+
return {
|
|
790
|
+
line: this.location.start.line,
|
|
791
|
+
column: this.location.start.column,
|
|
792
|
+
endLine: this.location.end.line,
|
|
793
|
+
endColumn: this.location.end.column,
|
|
794
|
+
message: this.message,
|
|
795
|
+
severity: 'error'
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
treeInspect(): string {
|
|
800
|
+
let output = "";
|
|
801
|
+
|
|
802
|
+
output += `@ MissingERBEndTagError ${this.location.treeInspectWithLabel()}\n`;
|
|
803
|
+
output += `├── message: "${this.message}"\n`;
|
|
804
|
+
output += `└── keyword: ${JSON.stringify(this.keyword)}\n`;
|
|
805
|
+
|
|
806
|
+
return output;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
684
810
|
|
|
685
811
|
export function fromSerializedError(error: SerializedHerbError): HerbError {
|
|
686
812
|
switch (error.type) {
|
|
@@ -693,6 +819,8 @@ export function fromSerializedError(error: SerializedHerbError): HerbError {
|
|
|
693
819
|
case "VOID_ELEMENT_CLOSING_TAG_ERROR": return VoidElementClosingTagError.from(error as SerializedVoidElementClosingTagError);
|
|
694
820
|
case "UNCLOSED_ELEMENT_ERROR": return UnclosedElementError.from(error as SerializedUnclosedElementError);
|
|
695
821
|
case "RUBY_PARSE_ERROR": return RubyParseError.from(error as SerializedRubyParseError);
|
|
822
|
+
case "ERB_CONTROL_FLOW_SCOPE_ERROR": return ERBControlFlowScopeError.from(error as SerializedERBControlFlowScopeError);
|
|
823
|
+
case "MISSINGERB_END_TAG_ERROR": return MissingERBEndTagError.from(error as SerializedMissingERBEndTagError);
|
|
696
824
|
|
|
697
825
|
default:
|
|
698
826
|
throw new Error(`Unknown node type: ${error.type}`);
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export * from "./ast-utils.js"
|
|
2
2
|
export * from "./backend.js"
|
|
3
3
|
export * from "./diagnostic.js"
|
|
4
|
+
export * from "./didyoumean.js"
|
|
4
5
|
export * from "./errors.js"
|
|
5
6
|
export * from "./herb-backend.js"
|
|
7
|
+
export * from "./levenshtein.js"
|
|
6
8
|
export * from "./lex-result.js"
|
|
7
9
|
export * from "./location.js"
|
|
8
|
-
export * from "./nodes.js"
|
|
9
10
|
export * from "./node-type-guards.js"
|
|
11
|
+
export * from "./nodes.js"
|
|
10
12
|
export * from "./parse-result.js"
|
|
11
13
|
export * from "./parser-options.js"
|
|
12
14
|
export * from "./position.js"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The following code is derived from the "js-levenshtein" repository,
|
|
3
|
+
* Copyright (c) 2017 Gustaf Andersson (https://github.com/gustf/js-levenshtein)
|
|
4
|
+
* Licensed under the MIT License (https://github.com/gustf/js-levenshtein/blob/master/LICENSE).
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*
|
|
24
|
+
* https://github.com/marcoroth/stimulus-lsp/blob/52268d4a4d06504dde6cb81f505a23b5db5d5759/server/src/levenshtein.ts
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export function levenshtein(a: string, b: string): number {
|
|
29
|
+
function _min(d0: any, d1: any, d2: any, bx: any, ay: any) {
|
|
30
|
+
return d0 < d1 || d2 < d1 ? (d0 > d2 ? d2 + 1 : d0 + 1) : bx === ay ? d1 : d1 + 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (a === b) {
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (a.length > b.length) {
|
|
38
|
+
const tmp = a
|
|
39
|
+
a = b
|
|
40
|
+
b = tmp
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let la = a.length
|
|
44
|
+
let lb = b.length
|
|
45
|
+
|
|
46
|
+
while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) {
|
|
47
|
+
la--
|
|
48
|
+
lb--
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let offset = 0
|
|
52
|
+
|
|
53
|
+
while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) {
|
|
54
|
+
offset++
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
la -= offset
|
|
58
|
+
lb -= offset
|
|
59
|
+
|
|
60
|
+
if (la === 0 || lb < 3) {
|
|
61
|
+
return lb
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let x = 0
|
|
65
|
+
let y
|
|
66
|
+
let d0
|
|
67
|
+
let d1
|
|
68
|
+
let d2
|
|
69
|
+
let d3
|
|
70
|
+
let dd
|
|
71
|
+
let dy
|
|
72
|
+
let ay
|
|
73
|
+
let bx0
|
|
74
|
+
let bx1
|
|
75
|
+
let bx2
|
|
76
|
+
let bx3
|
|
77
|
+
|
|
78
|
+
const vector = []
|
|
79
|
+
|
|
80
|
+
for (y = 0; y < la; y++) {
|
|
81
|
+
vector.push(y + 1)
|
|
82
|
+
vector.push(a.charCodeAt(offset + y))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const len = vector.length - 1
|
|
86
|
+
|
|
87
|
+
for (; x < lb - 3; ) {
|
|
88
|
+
bx0 = b.charCodeAt(offset + (d0 = x))
|
|
89
|
+
bx1 = b.charCodeAt(offset + (d1 = x + 1))
|
|
90
|
+
bx2 = b.charCodeAt(offset + (d2 = x + 2))
|
|
91
|
+
bx3 = b.charCodeAt(offset + (d3 = x + 3))
|
|
92
|
+
dd = x += 4
|
|
93
|
+
for (y = 0; y < len; y += 2) {
|
|
94
|
+
dy = vector[y]
|
|
95
|
+
ay = vector[y + 1]
|
|
96
|
+
d0 = _min(dy, d0, d1, bx0, ay)
|
|
97
|
+
d1 = _min(d0, d1, d2, bx1, ay)
|
|
98
|
+
d2 = _min(d1, d2, d3, bx2, ay)
|
|
99
|
+
dd = _min(d2, d3, dd, bx3, ay)
|
|
100
|
+
vector[y] = dd
|
|
101
|
+
d3 = d2
|
|
102
|
+
d2 = d1
|
|
103
|
+
d1 = d0
|
|
104
|
+
d0 = dy
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (; x < lb; ) {
|
|
109
|
+
bx0 = b.charCodeAt(offset + (d0 = x))
|
|
110
|
+
dd = ++x
|
|
111
|
+
for (y = 0; y < len; y += 2) {
|
|
112
|
+
dy = vector[y]
|
|
113
|
+
vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1])
|
|
114
|
+
d0 = dy
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return dd
|
|
119
|
+
}
|
package/src/location.ts
CHANGED
|
@@ -10,11 +10,24 @@ export class Location {
|
|
|
10
10
|
readonly start: Position
|
|
11
11
|
readonly end: Position
|
|
12
12
|
|
|
13
|
-
static from(location: SerializedLocation)
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
static from(location: SerializedLocation): Location
|
|
14
|
+
static from(line: number, column: number, endLine: number, endColumn: number): Location
|
|
15
|
+
static from(locationOrLine: SerializedLocation | number, column?: number, endLine?: number, endColumn?: number): Location {
|
|
16
|
+
if (typeof locationOrLine === "number") {
|
|
17
|
+
const start = Position.from(locationOrLine, column!)
|
|
18
|
+
const end = Position.from(endLine!, endColumn!)
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
return new Location(start, end)
|
|
21
|
+
} else {
|
|
22
|
+
const start = Position.from(locationOrLine.start)
|
|
23
|
+
const end = Position.from(locationOrLine.end)
|
|
24
|
+
|
|
25
|
+
return new Location(start, end)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static get zero() {
|
|
30
|
+
return new Location(Position.zero, Position.zero)
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
constructor(start: Position, end: Position) {
|
package/src/node-type-guards.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.8.1/templates/javascript/packages/core/src/node-type-guards.ts.erb
|
|
3
3
|
|
|
4
4
|
import type { Node, NodeType, ERBNode } from "./nodes.js"
|
|
5
5
|
|
package/src/nodes.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.8.1/templates/javascript/packages/core/src/nodes.ts.erb
|
|
3
3
|
|
|
4
4
|
import { Location } from "./location.js"
|
|
5
5
|
import { Token, SerializedToken } from "./token.js"
|
package/src/position.ts
CHANGED
|
@@ -7,8 +7,18 @@ export class Position {
|
|
|
7
7
|
readonly line: number
|
|
8
8
|
readonly column: number
|
|
9
9
|
|
|
10
|
-
static from(position: SerializedPosition)
|
|
11
|
-
|
|
10
|
+
static from(position: SerializedPosition): Position
|
|
11
|
+
static from(line: number, column: number): Position
|
|
12
|
+
static from(positionOrLine: SerializedPosition | number, column?: number): Position {
|
|
13
|
+
if (typeof positionOrLine === "number") {
|
|
14
|
+
return new Position(positionOrLine, column!)
|
|
15
|
+
} else {
|
|
16
|
+
return new Position(positionOrLine.line, positionOrLine.column)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static get zero() {
|
|
21
|
+
return new Position(0, 0)
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
constructor(line: number, column: number) {
|
package/src/range.ts
CHANGED
|
@@ -4,8 +4,18 @@ export class Range {
|
|
|
4
4
|
readonly start: number
|
|
5
5
|
readonly end: number
|
|
6
6
|
|
|
7
|
-
static from(range: SerializedRange)
|
|
8
|
-
|
|
7
|
+
static from(range: SerializedRange): Range
|
|
8
|
+
static from(start: number, end: number): Range
|
|
9
|
+
static from(rangeOrStart: SerializedRange | number, end?: number): Range {
|
|
10
|
+
if (typeof rangeOrStart === "number") {
|
|
11
|
+
return new Range(rangeOrStart, end!)
|
|
12
|
+
} else {
|
|
13
|
+
return new Range(rangeOrStart[0], rangeOrStart[1])
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static get zero() {
|
|
18
|
+
return new Range(0, 0)
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
constructor(start: number, end: number) {
|