forked from 0xfe/vexflow
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparser.ts
263 lines (231 loc) · 8.21 KB
/
parser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// A generic text parsing class for VexFlow.
import { log, RuntimeError } from './util';
// To enable logging for this class. Set `Vex.Flow.Parser.DEBUG` to `true`.
// eslint-disable-next-line
function L(...args: any[]): void {
if (Parser.DEBUG) log('Vex.Flow.Parser', args);
}
const NO_ERROR_POS = -1;
export type Match = string | Match[] | null;
export type RuleFunction = () => Rule;
export type TriggerFunction = (state?: { matches: Match[] }) => void;
export interface Rule {
// Lexer Rules
token?: string; // The token property is a string that is compiled into a RegExp.
noSpace?: boolean;
// Parser Rules
expect?: RuleFunction[];
zeroOrMore?: boolean;
oneOrMore?: boolean;
maybe?: boolean;
or?: boolean;
run?: TriggerFunction;
}
export interface Result {
success: boolean;
// Lexer Results
pos?: number;
incrementPos?: number;
matchedString?: string;
// Parser Results
matches?: Match[];
numMatches?: number;
results?: GroupedResults;
errorPos?: number; // Set to NO_ERROR if successful. N if there is an error in the string.
}
// Represents a mixed array containing Result and/or Result[].
// The grouping is determined by the structure of the Grammar.
export type GroupedResults = (Result | Result[])[];
// Converts parser results into an easy to reference list that can be
// used in triggers. This function returns:
// - nested array in which the leaf elements are string or null
// - string (including empty strings)
// - null
function flattenMatches(r: Result | Result[]): Match {
if ('matchedString' in r) return r.matchedString as string; // string
if ('results' in r) return flattenMatches(r.results as Result[]);
const results = r as Result[];
if (results.length === 1) return flattenMatches(results[0]);
if (results.length === 0) return null;
return results.map(flattenMatches); // nested array
}
export interface Grammar {
begin(): RuleFunction;
}
// This is the base parser class. Given an arbitrary context-free grammar, it
// can parse any line and execute code when specific rules are met (e.g.,
// when a string is terminated.)
export class Parser {
static DEBUG: boolean = false;
protected grammar: Grammar;
protected line: string; // Use RegExp to extract tokens from this line.
protected pos: number;
protected errorPos: number;
// For an example of a simple grammar, take a look at tests/parser_tests.ts or
// the EasyScore grammar in easyscore.ts.
constructor(grammar: Grammar) {
this.grammar = grammar;
this.line = '';
this.pos = 0;
this.errorPos = NO_ERROR_POS;
}
// Parse `line` using current grammar. Returns `{success: true}` if the
// line parsed correctly, otherwise returns `{success: false, errorPos: N}`
// where `errorPos` is the location of the error in the string.
parse(line: string): Result {
this.line = line;
this.pos = 0;
this.errorPos = NO_ERROR_POS;
const result = this.expect(this.grammar.begin());
result.errorPos = this.errorPos;
return result;
}
matchFail(returnPos: number): void {
if (this.errorPos === NO_ERROR_POS) this.errorPos = this.pos;
this.pos = returnPos;
}
matchSuccess(): void {
this.errorPos = NO_ERROR_POS;
}
// Look for `token` in this.line[this.pos], and return success
// if one is found. `token` is specified as a regular expression.
matchToken(token: string, noSpace: boolean = false): Result {
const regexp = noSpace ? new RegExp('^((' + token + '))') : new RegExp('^((' + token + ')\\s*)');
const workingLine = this.line.slice(this.pos);
const result = workingLine.match(regexp);
if (result !== null) {
return {
success: true,
matchedString: result[2],
incrementPos: result[1].length,
pos: this.pos,
};
} else {
return { success: false, pos: this.pos };
}
}
// Execute rule to match a sequence of tokens (or rules). If `maybe` is
// set, then return success even if the token is not found, but reset
// the position before exiting.
// TODO: expectOne(...) is never called with the `maybe` parameter.
expectOne(rule: Rule, maybe: boolean = false): Result {
const results: GroupedResults = [];
const pos = this.pos;
let allMatches = true;
let oneMatch = false;
maybe = maybe === true || rule.maybe === true;
// Execute all sub rules in sequence.
if (rule.expect) {
for (const next of rule.expect) {
const localPos = this.pos;
const result = this.expect(next);
// If `rule.or` is set, then return success if any one
// of the subrules match, else all subrules must match.
if (result.success) {
results.push(result);
oneMatch = true;
if (rule.or) break;
} else {
allMatches = false;
if (!rule.or) {
this.pos = localPos;
break;
}
}
}
}
const gotOne = (rule.or && oneMatch) || allMatches;
const success = gotOne || maybe === true;
const numMatches = gotOne ? 1 : 0;
if (maybe && !gotOne) this.pos = pos;
if (success) {
this.matchSuccess();
} else {
this.matchFail(pos);
}
return { success, results, numMatches };
}
// Try to match multiple (one or more) instances of the rule. If `maybe` is set,
// then a failed match is also a success (but the position is reset).
expectOneOrMore(rule: Rule, maybe: boolean = false): Result {
const results: GroupedResults = [];
const pos = this.pos;
let numMatches = 0;
let more = true;
do {
const result = this.expectOne(rule);
if (result.success && result.results) {
numMatches++;
results.push(result.results as Result[]);
} else {
more = false;
}
} while (more);
const success = numMatches > 0 || maybe === true;
if (maybe && !(numMatches > 0)) this.pos = pos;
if (success) {
this.matchSuccess();
} else {
this.matchFail(pos);
}
return { success, results, numMatches };
}
// Match zero or more instances of `rule`. Offloads to `expectOneOrMore`.
expectZeroOrMore(rule: Rule): Result {
return this.expectOneOrMore(rule, true);
}
// Execute the rule produced by the provided `rules` function. This
// offloads to one of the above matchers and consolidates the results. It is also
// responsible for executing any code triggered by the rule (in `rule.run`.)
expect(ruleFunc: RuleFunction): Result {
L('Evaluating rule function:', ruleFunc);
if (!ruleFunc) {
throw new RuntimeError('Invalid rule function');
}
let result: Result;
// Get rule from Grammar class.
// expect(...) handles both lexing & parsing:
// - lexer rules produce tokens.
// - parser rules produce expressions which may trigger code via the
// { run: () => ... } trigger functions in easyscore.ts.
// These functions build the VexFlow objects that are displayed on screen.
const rule: Rule = ruleFunc.bind(this.grammar)();
if (rule.token) {
// A lexer rule has a `token` property.
// Base case: parse the regex and throw an error if the
// line doesn't match.
result = this.matchToken(rule.token, rule.noSpace === true);
if (result.success) {
// Token match! Update position and throw away parsed portion
// of string.
this.pos += result.incrementPos as number;
}
} else if (rule.expect) {
// A parser rule has an `expect` property.
if (rule.oneOrMore) {
result = this.expectOneOrMore(rule);
} else if (rule.zeroOrMore) {
result = this.expectZeroOrMore(rule);
} else {
result = this.expectOne(rule);
}
} else {
L(rule);
throw new RuntimeError('Bad grammar! No `token` or `expect` property ' + rule);
}
// If there's a trigger attached to this rule, then run it.
// Make the matches accessible through `state.matches` in the
// `run: (state) => ...` trigger.
const matches: Match[] = [];
result.matches = matches;
if (result.results) {
result.results.forEach((r) => matches.push(flattenMatches(r)));
}
if (rule.run && result.success) {
rule.run({ matches });
}
return result;
}
}