mirror of
https://github.com/justinian/lexx.git
synced 2025-12-09 16:14:32 -08:00
Initial commit
This commit is contained in:
76
src/definition.ts
Normal file
76
src/definition.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { filter_desc } from './filter';
|
||||
import type { language_settings } from './language';
|
||||
|
||||
import Filter from './filter';
|
||||
import Language from './language';
|
||||
import { parse, ASTKinds } from './parser';
|
||||
import Phonology from './phonology';
|
||||
|
||||
|
||||
export function build_language(definition: string): Language {
|
||||
const result = parse(definition);
|
||||
if (!result.ast) {
|
||||
const error = new Error(result.errs.toString());
|
||||
throw error;
|
||||
}
|
||||
|
||||
let settings_vars: Map<string, any> = new Map();
|
||||
let using_modules: string[][] = [];
|
||||
|
||||
let word_patterns: string[][] = [];
|
||||
let filter_patterns: filter_desc[] = [];
|
||||
let classes: Map<string, string[]> = new Map();
|
||||
|
||||
let macros: filter_desc[] = [];
|
||||
let spelling: filter_desc[] = [];
|
||||
|
||||
for (const line of result.ast.lines) {
|
||||
switch (line.kind) {
|
||||
case ASTKinds.using:
|
||||
using_modules.push(line.modules);
|
||||
break;
|
||||
|
||||
case ASTKinds.settings:
|
||||
for (const key of line.settings.keys())
|
||||
settings_vars.set(key, line.settings.get(key));
|
||||
break;
|
||||
|
||||
case ASTKinds.pclass:
|
||||
classes.set(line.name, line.phonemes);
|
||||
break;
|
||||
|
||||
case ASTKinds.macro:
|
||||
macros.push(['\\' + line.name, line.value]);
|
||||
break;
|
||||
|
||||
case ASTKinds.words:
|
||||
word_patterns.push(line.patterns);
|
||||
break;
|
||||
|
||||
case ASTKinds.reject:
|
||||
case ASTKinds.filter:
|
||||
for(const pattern of line.patterns)
|
||||
filter_patterns.push(pattern);
|
||||
break;
|
||||
|
||||
case ASTKinds.spelling:
|
||||
for(const pattern of line.patterns)
|
||||
spelling.push(pattern);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let macro_filter: Filter = new Filter(macros);
|
||||
|
||||
let final_patterns = word_patterns
|
||||
.flat()
|
||||
.map(p => macro_filter.transform(p));
|
||||
|
||||
return new Language(
|
||||
{modules: using_modules.flat(), settings: settings_vars},
|
||||
new Phonology(final_patterns, classes, filter_patterns),
|
||||
new Filter(spelling),
|
||||
);
|
||||
}
|
||||
|
||||
export default build_language;
|
||||
33
src/filter.ts
Normal file
33
src/filter.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type filter_desc = [string, string];
|
||||
export const reject_sentinel: string = "REJECT";
|
||||
|
||||
type filter_entry = [RegExp, string];
|
||||
|
||||
export class Filter {
|
||||
private readonly filters: filter_entry[] = [];
|
||||
|
||||
constructor(descs: filter_desc[]) {
|
||||
for (const desc of descs) {
|
||||
this.filters.push([new RegExp(desc[0], 'g'), desc[1]]);
|
||||
}
|
||||
}
|
||||
|
||||
transform(input: string): string {
|
||||
for (const entry of this.filters)
|
||||
input = input.replaceAll(entry[0], entry[1]);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
filter(input: string): string | null {
|
||||
for (const entry of this.filters) {
|
||||
input = input.replaceAll(entry[0], entry[1]);
|
||||
if (input.includes(reject_sentinel))
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
29
src/language.ts
Normal file
29
src/language.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import Filter from './filter';
|
||||
import Phonology from './phonology';
|
||||
|
||||
export interface language_settings {
|
||||
modules: string[];
|
||||
settings: Map<string, any>;
|
||||
};
|
||||
|
||||
export type word = [string, string];
|
||||
|
||||
export class Language {
|
||||
private readonly settings: language_settings;
|
||||
private readonly phones: Phonology;
|
||||
private readonly ortho: Filter;
|
||||
|
||||
constructor(settings: language_settings, phones: Phonology, ortho: Filter) {
|
||||
this.settings = settings;
|
||||
this.phones = phones;
|
||||
this.ortho = ortho;
|
||||
}
|
||||
|
||||
generate(count: number): word[] {
|
||||
const rand_rate = this.settings.settings.get('random-rate') || 50;
|
||||
let phones = this.phones.generate(count, rand_rate);
|
||||
return phones.map((phone) => [this.ortho.transform(phone), phone]);
|
||||
}
|
||||
};
|
||||
|
||||
export default Language;
|
||||
894
src/parser.ts
Normal file
894
src/parser.ts
Normal file
@@ -0,0 +1,894 @@
|
||||
/* AutoGenerated Code, changes may be overwritten
|
||||
* INPUT GRAMMAR:
|
||||
* ---
|
||||
* import type { filter_desc } from './filter';
|
||||
* import { reject_sentinel } from './filter';
|
||||
* ---
|
||||
* start := lines={statement | comment | eol}+ $
|
||||
* statement := using | settings | pclass | macro | words | reject | filter | spelling
|
||||
* using := 'using:' _modules={ws name=name}+ eol
|
||||
* .modules = string[] { return _modules.map(s => s.name); }
|
||||
* settings := 'settings:' _settings={ws name=name ws? '=' ws? value=value}+ eol
|
||||
* .settings = Map<string, any> { return new Map(_settings.map(s => [s.name, s.value.value])); }
|
||||
* pclass := name='[A-Z]' ws? '=' ws? first=phoneme rest={ws phoneme=phoneme}* eol
|
||||
* .phonemes = string[] { return [this.first, ...rest.map(s => s.phoneme)]; }
|
||||
* macro := name='\$[A-Z]' ws? '=' ws? value=phoneme eol
|
||||
* words := 'words:' _patterns={ws pattern='[A-Z?\$]+'}+ eol
|
||||
* .patterns = string[] { return _patterns.map(s => s.pattern); }
|
||||
* reject := 'reject:' _patterns={ws pattern=phoneme}+ eol
|
||||
* .patterns = Array<filter_desc> { return _patterns.map(s => [s.pattern, reject_sentinel]); }
|
||||
* filter := 'filter:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||
* .patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||
* spelling := 'spelling:' ws first=filter_pat rest={ ws? ';' ws? pattern=filter_pat }* ';'? eol
|
||||
* .patterns = Array<filter_desc> { return [this.first.value, ...this.rest.map(s => s.pattern.value)]; }
|
||||
* filter_pat := from=phoneme ws? '>' ws? to=phoneme
|
||||
* .value = filter_desc { return [this.from, this.to]; }
|
||||
* eol := ws? '\n'
|
||||
* ws := '[\t ]+'
|
||||
* comment := {'^#.*'m | ws} eol
|
||||
* phoneme := '[^\s`:;!]+'
|
||||
* value := num | str
|
||||
* name := '[A-Za-z][A-Za-z0-9_-]*'
|
||||
* num := _value='[0-9]+'
|
||||
* .value = number { return parseInt(this._value); }
|
||||
* str := '"' value='[^"]*' '"'
|
||||
*/
|
||||
|
||||
import type { filter_desc } from './filter';
|
||||
import { reject_sentinel } from './filter';
|
||||
|
||||
type Nullable<T> = T | null;
|
||||
type $$RuleType<T> = () => Nullable<T>;
|
||||
export interface ASTNodeIntf {
|
||||
kind: ASTKinds;
|
||||
}
|
||||
export enum ASTKinds {
|
||||
start = "start",
|
||||
start_$0_1 = "start_$0_1",
|
||||
start_$0_2 = "start_$0_2",
|
||||
start_$0_3 = "start_$0_3",
|
||||
statement_1 = "statement_1",
|
||||
statement_2 = "statement_2",
|
||||
statement_3 = "statement_3",
|
||||
statement_4 = "statement_4",
|
||||
statement_5 = "statement_5",
|
||||
statement_6 = "statement_6",
|
||||
statement_7 = "statement_7",
|
||||
statement_8 = "statement_8",
|
||||
using = "using",
|
||||
using_$0 = "using_$0",
|
||||
settings = "settings",
|
||||
settings_$0 = "settings_$0",
|
||||
pclass = "pclass",
|
||||
pclass_$0 = "pclass_$0",
|
||||
macro = "macro",
|
||||
words = "words",
|
||||
words_$0 = "words_$0",
|
||||
reject = "reject",
|
||||
reject_$0 = "reject_$0",
|
||||
filter = "filter",
|
||||
filter_$0 = "filter_$0",
|
||||
spelling = "spelling",
|
||||
spelling_$0 = "spelling_$0",
|
||||
filter_pat = "filter_pat",
|
||||
eol = "eol",
|
||||
ws = "ws",
|
||||
comment = "comment",
|
||||
comment_$0_1 = "comment_$0_1",
|
||||
comment_$0_2 = "comment_$0_2",
|
||||
phoneme = "phoneme",
|
||||
value_1 = "value_1",
|
||||
value_2 = "value_2",
|
||||
name = "name",
|
||||
num = "num",
|
||||
str = "str",
|
||||
$EOF = "$EOF",
|
||||
}
|
||||
export interface start {
|
||||
kind: ASTKinds.start;
|
||||
lines: [start_$0, ...start_$0[]];
|
||||
}
|
||||
export type start_$0 = start_$0_1 | start_$0_2 | start_$0_3;
|
||||
export type start_$0_1 = statement;
|
||||
export type start_$0_2 = comment;
|
||||
export type start_$0_3 = eol;
|
||||
export type statement = statement_1 | statement_2 | statement_3 | statement_4 | statement_5 | statement_6 | statement_7 | statement_8;
|
||||
export type statement_1 = using;
|
||||
export type statement_2 = settings;
|
||||
export type statement_3 = pclass;
|
||||
export type statement_4 = macro;
|
||||
export type statement_5 = words;
|
||||
export type statement_6 = reject;
|
||||
export type statement_7 = filter;
|
||||
export type statement_8 = spelling;
|
||||
export class using {
|
||||
public kind: ASTKinds.using = ASTKinds.using;
|
||||
public _modules: [using_$0, ...using_$0[]];
|
||||
public modules: string[];
|
||||
constructor(_modules: [using_$0, ...using_$0[]]){
|
||||
this._modules = _modules;
|
||||
this.modules = ((): string[] => {
|
||||
return _modules.map(s => s.name);
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface using_$0 {
|
||||
kind: ASTKinds.using_$0;
|
||||
name: name;
|
||||
}
|
||||
export class settings {
|
||||
public kind: ASTKinds.settings = ASTKinds.settings;
|
||||
public _settings: [settings_$0, ...settings_$0[]];
|
||||
public settings: Map<string, any>;
|
||||
constructor(_settings: [settings_$0, ...settings_$0[]]){
|
||||
this._settings = _settings;
|
||||
this.settings = ((): Map<string, any> => {
|
||||
return new Map(_settings.map(s => [s.name, s.value.value]));
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface settings_$0 {
|
||||
kind: ASTKinds.settings_$0;
|
||||
name: name;
|
||||
value: value;
|
||||
}
|
||||
export class pclass {
|
||||
public kind: ASTKinds.pclass = ASTKinds.pclass;
|
||||
public name: string;
|
||||
public first: phoneme;
|
||||
public rest: pclass_$0[];
|
||||
public phonemes: string[];
|
||||
constructor(name: string, first: phoneme, rest: pclass_$0[]){
|
||||
this.name = name;
|
||||
this.first = first;
|
||||
this.rest = rest;
|
||||
this.phonemes = ((): string[] => {
|
||||
return [this.first, ...rest.map(s => s.phoneme)];
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface pclass_$0 {
|
||||
kind: ASTKinds.pclass_$0;
|
||||
phoneme: phoneme;
|
||||
}
|
||||
export interface macro {
|
||||
kind: ASTKinds.macro;
|
||||
name: string;
|
||||
value: phoneme;
|
||||
}
|
||||
export class words {
|
||||
public kind: ASTKinds.words = ASTKinds.words;
|
||||
public _patterns: [words_$0, ...words_$0[]];
|
||||
public patterns: string[];
|
||||
constructor(_patterns: [words_$0, ...words_$0[]]){
|
||||
this._patterns = _patterns;
|
||||
this.patterns = ((): string[] => {
|
||||
return _patterns.map(s => s.pattern);
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface words_$0 {
|
||||
kind: ASTKinds.words_$0;
|
||||
pattern: string;
|
||||
}
|
||||
export class reject {
|
||||
public kind: ASTKinds.reject = ASTKinds.reject;
|
||||
public _patterns: [reject_$0, ...reject_$0[]];
|
||||
public patterns: Array<filter_desc>;
|
||||
constructor(_patterns: [reject_$0, ...reject_$0[]]){
|
||||
this._patterns = _patterns;
|
||||
this.patterns = ((): Array<filter_desc> => {
|
||||
return _patterns.map(s => [s.pattern, reject_sentinel]);
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface reject_$0 {
|
||||
kind: ASTKinds.reject_$0;
|
||||
pattern: phoneme;
|
||||
}
|
||||
export class filter {
|
||||
public kind: ASTKinds.filter = ASTKinds.filter;
|
||||
public first: filter_pat;
|
||||
public rest: filter_$0[];
|
||||
public patterns: Array<filter_desc>;
|
||||
constructor(first: filter_pat, rest: filter_$0[]){
|
||||
this.first = first;
|
||||
this.rest = rest;
|
||||
this.patterns = ((): Array<filter_desc> => {
|
||||
return [this.first.value, ...this.rest.map(s => s.pattern.value)];
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface filter_$0 {
|
||||
kind: ASTKinds.filter_$0;
|
||||
pattern: filter_pat;
|
||||
}
|
||||
export class spelling {
|
||||
public kind: ASTKinds.spelling = ASTKinds.spelling;
|
||||
public first: filter_pat;
|
||||
public rest: spelling_$0[];
|
||||
public patterns: Array<filter_desc>;
|
||||
constructor(first: filter_pat, rest: spelling_$0[]){
|
||||
this.first = first;
|
||||
this.rest = rest;
|
||||
this.patterns = ((): Array<filter_desc> => {
|
||||
return [this.first.value, ...this.rest.map(s => s.pattern.value)];
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface spelling_$0 {
|
||||
kind: ASTKinds.spelling_$0;
|
||||
pattern: filter_pat;
|
||||
}
|
||||
export class filter_pat {
|
||||
public kind: ASTKinds.filter_pat = ASTKinds.filter_pat;
|
||||
public from: phoneme;
|
||||
public to: phoneme;
|
||||
public value: filter_desc;
|
||||
constructor(from: phoneme, to: phoneme){
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.value = ((): filter_desc => {
|
||||
return [this.from, this.to];
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface eol {
|
||||
kind: ASTKinds.eol;
|
||||
}
|
||||
export type ws = string;
|
||||
export interface comment {
|
||||
kind: ASTKinds.comment;
|
||||
}
|
||||
export type comment_$0 = comment_$0_1 | comment_$0_2;
|
||||
export type comment_$0_1 = string;
|
||||
export type comment_$0_2 = ws;
|
||||
export type phoneme = string;
|
||||
export type value = value_1 | value_2;
|
||||
export type value_1 = num;
|
||||
export type value_2 = str;
|
||||
export type name = string;
|
||||
export class num {
|
||||
public kind: ASTKinds.num = ASTKinds.num;
|
||||
public _value: string;
|
||||
public value: number;
|
||||
constructor(_value: string){
|
||||
this._value = _value;
|
||||
this.value = ((): number => {
|
||||
return parseInt(this._value);
|
||||
})();
|
||||
}
|
||||
}
|
||||
export interface str {
|
||||
kind: ASTKinds.str;
|
||||
value: string;
|
||||
}
|
||||
export class Parser {
|
||||
private readonly input: string;
|
||||
private pos: PosInfo;
|
||||
private negating: boolean = false;
|
||||
private memoSafe: boolean = true;
|
||||
constructor(input: string) {
|
||||
this.pos = {overallPos: 0, line: 1, offset: 0};
|
||||
this.input = input;
|
||||
}
|
||||
public reset(pos: PosInfo) {
|
||||
this.pos = pos;
|
||||
}
|
||||
public finished(): boolean {
|
||||
return this.pos.overallPos === this.input.length;
|
||||
}
|
||||
public clearMemos(): void {
|
||||
}
|
||||
public matchstart($$dpth: number, $$cr?: ErrorTracker): Nullable<start> {
|
||||
return this.run<start>($$dpth,
|
||||
() => {
|
||||
let $scope$lines: Nullable<[start_$0, ...start_$0[]]>;
|
||||
let $$res: Nullable<start> = null;
|
||||
if (true
|
||||
&& ($scope$lines = this.loopPlus<start_$0>(() => this.matchstart_$0($$dpth + 1, $$cr))) !== null
|
||||
&& this.match$EOF($$cr) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.start, lines: $scope$lines};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchstart_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0> {
|
||||
return this.choice<start_$0>([
|
||||
() => this.matchstart_$0_1($$dpth + 1, $$cr),
|
||||
() => this.matchstart_$0_2($$dpth + 1, $$cr),
|
||||
() => this.matchstart_$0_3($$dpth + 1, $$cr),
|
||||
]);
|
||||
}
|
||||
public matchstart_$0_1($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_1> {
|
||||
return this.matchstatement($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstart_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_2> {
|
||||
return this.matchcomment($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstart_$0_3($$dpth: number, $$cr?: ErrorTracker): Nullable<start_$0_3> {
|
||||
return this.matcheol($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement($$dpth: number, $$cr?: ErrorTracker): Nullable<statement> {
|
||||
return this.choice<statement>([
|
||||
() => this.matchstatement_1($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_2($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_3($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_4($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_5($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_6($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_7($$dpth + 1, $$cr),
|
||||
() => this.matchstatement_8($$dpth + 1, $$cr),
|
||||
]);
|
||||
}
|
||||
public matchstatement_1($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_1> {
|
||||
return this.matchusing($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_2($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_2> {
|
||||
return this.matchsettings($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_3($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_3> {
|
||||
return this.matchpclass($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_4($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_4> {
|
||||
return this.matchmacro($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_5($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_5> {
|
||||
return this.matchwords($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_6($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_6> {
|
||||
return this.matchreject($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_7($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_7> {
|
||||
return this.matchfilter($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchstatement_8($$dpth: number, $$cr?: ErrorTracker): Nullable<statement_8> {
|
||||
return this.matchspelling($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchusing($$dpth: number, $$cr?: ErrorTracker): Nullable<using> {
|
||||
return this.run<using>($$dpth,
|
||||
() => {
|
||||
let $scope$_modules: Nullable<[using_$0, ...using_$0[]]>;
|
||||
let $$res: Nullable<using> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:using:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ($scope$_modules = this.loopPlus<using_$0>(() => this.matchusing_$0($$dpth + 1, $$cr))) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new using($scope$_modules);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchusing_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<using_$0> {
|
||||
return this.run<using_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$name: Nullable<name>;
|
||||
let $$res: Nullable<using_$0> = null;
|
||||
if (true
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$name = this.matchname($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.using_$0, name: $scope$name};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchsettings($$dpth: number, $$cr?: ErrorTracker): Nullable<settings> {
|
||||
return this.run<settings>($$dpth,
|
||||
() => {
|
||||
let $scope$_settings: Nullable<[settings_$0, ...settings_$0[]]>;
|
||||
let $$res: Nullable<settings> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:settings:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ($scope$_settings = this.loopPlus<settings_$0>(() => this.matchsettings_$0($$dpth + 1, $$cr))) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new settings($scope$_settings);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchsettings_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<settings_$0> {
|
||||
return this.run<settings_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$name: Nullable<name>;
|
||||
let $scope$value: Nullable<value>;
|
||||
let $$res: Nullable<settings_$0> = null;
|
||||
if (true
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$name = this.matchname($$dpth + 1, $$cr)) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$value = this.matchvalue($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.settings_$0, name: $scope$name, value: $scope$value};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchpclass($$dpth: number, $$cr?: ErrorTracker): Nullable<pclass> {
|
||||
return this.run<pclass>($$dpth,
|
||||
() => {
|
||||
let $scope$name: Nullable<string>;
|
||||
let $scope$first: Nullable<phoneme>;
|
||||
let $scope$rest: Nullable<pclass_$0[]>;
|
||||
let $$res: Nullable<pclass> = null;
|
||||
if (true
|
||||
&& ($scope$name = this.regexAccept(String.raw`(?:[A-Z])`, "", $$dpth + 1, $$cr)) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$first = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
&& ($scope$rest = this.loop<pclass_$0>(() => this.matchpclass_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new pclass($scope$name, $scope$first, $scope$rest);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchpclass_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<pclass_$0> {
|
||||
return this.run<pclass_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$phoneme: Nullable<phoneme>;
|
||||
let $$res: Nullable<pclass_$0> = null;
|
||||
if (true
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$phoneme = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.pclass_$0, phoneme: $scope$phoneme};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchmacro($$dpth: number, $$cr?: ErrorTracker): Nullable<macro> {
|
||||
return this.run<macro>($$dpth,
|
||||
() => {
|
||||
let $scope$name: Nullable<string>;
|
||||
let $scope$value: Nullable<phoneme>;
|
||||
let $$res: Nullable<macro> = null;
|
||||
if (true
|
||||
&& ($scope$name = this.regexAccept(String.raw`(?:\$[A-Z])`, "", $$dpth + 1, $$cr)) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:=)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$value = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.macro, name: $scope$name, value: $scope$value};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchwords($$dpth: number, $$cr?: ErrorTracker): Nullable<words> {
|
||||
return this.run<words>($$dpth,
|
||||
() => {
|
||||
let $scope$_patterns: Nullable<[words_$0, ...words_$0[]]>;
|
||||
let $$res: Nullable<words> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:words:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ($scope$_patterns = this.loopPlus<words_$0>(() => this.matchwords_$0($$dpth + 1, $$cr))) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new words($scope$_patterns);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchwords_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<words_$0> {
|
||||
return this.run<words_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$pattern: Nullable<string>;
|
||||
let $$res: Nullable<words_$0> = null;
|
||||
if (true
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$pattern = this.regexAccept(String.raw`(?:[A-Z?\$]+)`, "", $$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.words_$0, pattern: $scope$pattern};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchreject($$dpth: number, $$cr?: ErrorTracker): Nullable<reject> {
|
||||
return this.run<reject>($$dpth,
|
||||
() => {
|
||||
let $scope$_patterns: Nullable<[reject_$0, ...reject_$0[]]>;
|
||||
let $$res: Nullable<reject> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:reject:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ($scope$_patterns = this.loopPlus<reject_$0>(() => this.matchreject_$0($$dpth + 1, $$cr))) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new reject($scope$_patterns);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchreject_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<reject_$0> {
|
||||
return this.run<reject_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$pattern: Nullable<phoneme>;
|
||||
let $$res: Nullable<reject_$0> = null;
|
||||
if (true
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$pattern = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.reject_$0, pattern: $scope$pattern};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchfilter($$dpth: number, $$cr?: ErrorTracker): Nullable<filter> {
|
||||
return this.run<filter>($$dpth,
|
||||
() => {
|
||||
let $scope$first: Nullable<filter_pat>;
|
||||
let $scope$rest: Nullable<filter_$0[]>;
|
||||
let $$res: Nullable<filter> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:filter:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$first = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||
&& ($scope$rest = this.loop<filter_$0>(() => this.matchfilter_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||
&& ((this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr)) || true)
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new filter($scope$first, $scope$rest);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchfilter_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<filter_$0> {
|
||||
return this.run<filter_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$pattern: Nullable<filter_pat>;
|
||||
let $$res: Nullable<filter_$0> = null;
|
||||
if (true
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$pattern = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.filter_$0, pattern: $scope$pattern};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchspelling($$dpth: number, $$cr?: ErrorTracker): Nullable<spelling> {
|
||||
return this.run<spelling>($$dpth,
|
||||
() => {
|
||||
let $scope$first: Nullable<filter_pat>;
|
||||
let $scope$rest: Nullable<spelling_$0[]>;
|
||||
let $$res: Nullable<spelling> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:spelling:)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& this.matchws($$dpth + 1, $$cr) !== null
|
||||
&& ($scope$first = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||
&& ($scope$rest = this.loop<spelling_$0>(() => this.matchspelling_$0($$dpth + 1, $$cr), 0, -1)) !== null
|
||||
&& ((this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr)) || true)
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = new spelling($scope$first, $scope$rest);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchspelling_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<spelling_$0> {
|
||||
return this.run<spelling_$0>($$dpth,
|
||||
() => {
|
||||
let $scope$pattern: Nullable<filter_pat>;
|
||||
let $$res: Nullable<spelling_$0> = null;
|
||||
if (true
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:;)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$pattern = this.matchfilter_pat($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.spelling_$0, pattern: $scope$pattern};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchfilter_pat($$dpth: number, $$cr?: ErrorTracker): Nullable<filter_pat> {
|
||||
return this.run<filter_pat>($$dpth,
|
||||
() => {
|
||||
let $scope$from: Nullable<phoneme>;
|
||||
let $scope$to: Nullable<phoneme>;
|
||||
let $$res: Nullable<filter_pat> = null;
|
||||
if (true
|
||||
&& ($scope$from = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:>)`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& ($scope$to = this.matchphoneme($$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = new filter_pat($scope$from, $scope$to);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matcheol($$dpth: number, $$cr?: ErrorTracker): Nullable<eol> {
|
||||
return this.run<eol>($$dpth,
|
||||
() => {
|
||||
let $$res: Nullable<eol> = null;
|
||||
if (true
|
||||
&& ((this.matchws($$dpth + 1, $$cr)) || true)
|
||||
&& this.regexAccept(String.raw`(?:\n)`, "", $$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.eol, };
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchws($$dpth: number, $$cr?: ErrorTracker): Nullable<ws> {
|
||||
return this.regexAccept(String.raw`(?:[\t ]+)`, "", $$dpth + 1, $$cr);
|
||||
}
|
||||
public matchcomment($$dpth: number, $$cr?: ErrorTracker): Nullable<comment> {
|
||||
return this.run<comment>($$dpth,
|
||||
() => {
|
||||
let $$res: Nullable<comment> = null;
|
||||
if (true
|
||||
&& this.matchcomment_$0($$dpth + 1, $$cr) !== null
|
||||
&& this.matcheol($$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.comment, };
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchcomment_$0($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0> {
|
||||
return this.choice<comment_$0>([
|
||||
() => this.matchcomment_$0_1($$dpth + 1, $$cr),
|
||||
() => this.matchcomment_$0_2($$dpth + 1, $$cr),
|
||||
]);
|
||||
}
|
||||
public matchcomment_$0_1($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0_1> {
|
||||
return this.regexAccept(String.raw`(?:^#.*)`, "m", $$dpth + 1, $$cr);
|
||||
}
|
||||
public matchcomment_$0_2($$dpth: number, $$cr?: ErrorTracker): Nullable<comment_$0_2> {
|
||||
return this.matchws($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchphoneme($$dpth: number, $$cr?: ErrorTracker): Nullable<phoneme> {
|
||||
return this.regexAccept(String.raw`(?:[^\s\`:;!]+)`, "", $$dpth + 1, $$cr);
|
||||
}
|
||||
public matchvalue($$dpth: number, $$cr?: ErrorTracker): Nullable<value> {
|
||||
return this.choice<value>([
|
||||
() => this.matchvalue_1($$dpth + 1, $$cr),
|
||||
() => this.matchvalue_2($$dpth + 1, $$cr),
|
||||
]);
|
||||
}
|
||||
public matchvalue_1($$dpth: number, $$cr?: ErrorTracker): Nullable<value_1> {
|
||||
return this.matchnum($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchvalue_2($$dpth: number, $$cr?: ErrorTracker): Nullable<value_2> {
|
||||
return this.matchstr($$dpth + 1, $$cr);
|
||||
}
|
||||
public matchname($$dpth: number, $$cr?: ErrorTracker): Nullable<name> {
|
||||
return this.regexAccept(String.raw`(?:[A-Za-z][A-Za-z0-9_-]*)`, "", $$dpth + 1, $$cr);
|
||||
}
|
||||
public matchnum($$dpth: number, $$cr?: ErrorTracker): Nullable<num> {
|
||||
return this.run<num>($$dpth,
|
||||
() => {
|
||||
let $scope$_value: Nullable<string>;
|
||||
let $$res: Nullable<num> = null;
|
||||
if (true
|
||||
&& ($scope$_value = this.regexAccept(String.raw`(?:[0-9]+)`, "", $$dpth + 1, $$cr)) !== null
|
||||
) {
|
||||
$$res = new num($scope$_value);
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public matchstr($$dpth: number, $$cr?: ErrorTracker): Nullable<str> {
|
||||
return this.run<str>($$dpth,
|
||||
() => {
|
||||
let $scope$value: Nullable<string>;
|
||||
let $$res: Nullable<str> = null;
|
||||
if (true
|
||||
&& this.regexAccept(String.raw`(?:")`, "", $$dpth + 1, $$cr) !== null
|
||||
&& ($scope$value = this.regexAccept(String.raw`(?:[^"]*)`, "", $$dpth + 1, $$cr)) !== null
|
||||
&& this.regexAccept(String.raw`(?:")`, "", $$dpth + 1, $$cr) !== null
|
||||
) {
|
||||
$$res = {kind: ASTKinds.str, value: $scope$value};
|
||||
}
|
||||
return $$res;
|
||||
});
|
||||
}
|
||||
public test(): boolean {
|
||||
const mrk = this.mark();
|
||||
const res = this.matchstart(0);
|
||||
const ans = res !== null;
|
||||
this.reset(mrk);
|
||||
return ans;
|
||||
}
|
||||
public parse(): ParseResult {
|
||||
const mrk = this.mark();
|
||||
const res = this.matchstart(0);
|
||||
if (res)
|
||||
return {ast: res, errs: []};
|
||||
this.reset(mrk);
|
||||
const rec = new ErrorTracker();
|
||||
this.clearMemos();
|
||||
this.matchstart(0, rec);
|
||||
const err = rec.getErr()
|
||||
return {ast: res, errs: err !== null ? [err] : []}
|
||||
}
|
||||
public mark(): PosInfo {
|
||||
return this.pos;
|
||||
}
|
||||
// @ts-ignore: loopPlus may not be called
|
||||
private loopPlus<T>(func: $$RuleType<T>): Nullable<[T, ...T[]]> {
|
||||
return this.loop(func, 1, -1) as Nullable<[T, ...T[]]>;
|
||||
}
|
||||
private loop<T>(func: $$RuleType<T>, lb: number, ub: number): Nullable<T[]> {
|
||||
const mrk = this.mark();
|
||||
const res: T[] = [];
|
||||
while (ub === -1 || res.length < ub) {
|
||||
const preMrk = this.mark();
|
||||
const t = func();
|
||||
if (t === null || this.pos.overallPos === preMrk.overallPos) {
|
||||
break;
|
||||
}
|
||||
res.push(t);
|
||||
}
|
||||
if (res.length >= lb) {
|
||||
return res;
|
||||
}
|
||||
this.reset(mrk);
|
||||
return null;
|
||||
}
|
||||
private run<T>($$dpth: number, fn: $$RuleType<T>): Nullable<T> {
|
||||
const mrk = this.mark();
|
||||
const res = fn()
|
||||
if (res !== null)
|
||||
return res;
|
||||
this.reset(mrk);
|
||||
return null;
|
||||
}
|
||||
// @ts-ignore: choice may not be called
|
||||
private choice<T>(fns: Array<$$RuleType<T>>): Nullable<T> {
|
||||
for (const f of fns) {
|
||||
const res = f();
|
||||
if (res !== null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private regexAccept(match: string, mods: string, dpth: number, cr?: ErrorTracker): Nullable<string> {
|
||||
return this.run<string>(dpth,
|
||||
() => {
|
||||
const reg = new RegExp(match, "y" + mods);
|
||||
const mrk = this.mark();
|
||||
reg.lastIndex = mrk.overallPos;
|
||||
const res = this.tryConsume(reg);
|
||||
if(cr) {
|
||||
cr.record(mrk, res, {
|
||||
kind: "RegexMatch",
|
||||
// We substring from 3 to len - 1 to strip off the
|
||||
// non-capture group syntax added as a WebKit workaround
|
||||
literal: match.substring(3, match.length - 1),
|
||||
negated: this.negating,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
private tryConsume(reg: RegExp): Nullable<string> {
|
||||
const res = reg.exec(this.input);
|
||||
if (res) {
|
||||
let lineJmp = 0;
|
||||
let lind = -1;
|
||||
for (let i = 0; i < res[0].length; ++i) {
|
||||
if (res[0][i] === "\n") {
|
||||
++lineJmp;
|
||||
lind = i;
|
||||
}
|
||||
}
|
||||
this.pos = {
|
||||
overallPos: reg.lastIndex,
|
||||
line: this.pos.line + lineJmp,
|
||||
offset: lind === -1 ? this.pos.offset + res[0].length : (res[0].length - lind - 1)
|
||||
};
|
||||
return res[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// @ts-ignore: noConsume may not be called
|
||||
private noConsume<T>(fn: $$RuleType<T>): Nullable<T> {
|
||||
const mrk = this.mark();
|
||||
const res = fn();
|
||||
this.reset(mrk);
|
||||
return res;
|
||||
}
|
||||
// @ts-ignore: negate may not be called
|
||||
private negate<T>(fn: $$RuleType<T>): Nullable<boolean> {
|
||||
const mrk = this.mark();
|
||||
const oneg = this.negating;
|
||||
this.negating = !oneg;
|
||||
const res = fn();
|
||||
this.negating = oneg;
|
||||
this.reset(mrk);
|
||||
return res === null ? true : null;
|
||||
}
|
||||
// @ts-ignore: Memoise may not be used
|
||||
private memoise<K>(rule: $$RuleType<K>, memo: Map<number, [Nullable<K>, PosInfo]>): Nullable<K> {
|
||||
const $scope$pos = this.mark();
|
||||
const $scope$memoRes = memo.get($scope$pos.overallPos);
|
||||
if(this.memoSafe && $scope$memoRes !== undefined) {
|
||||
this.reset($scope$memoRes[1]);
|
||||
return $scope$memoRes[0];
|
||||
}
|
||||
const $scope$result = rule();
|
||||
if(this.memoSafe)
|
||||
memo.set($scope$pos.overallPos, [$scope$result, this.mark()]);
|
||||
return $scope$result;
|
||||
}
|
||||
private match$EOF(et?: ErrorTracker): Nullable<{kind: ASTKinds.$EOF}> {
|
||||
const res: {kind: ASTKinds.$EOF} | null = this.finished() ? { kind: ASTKinds.$EOF } : null;
|
||||
if(et)
|
||||
et.record(this.mark(), res, { kind: "EOF", negated: this.negating });
|
||||
return res;
|
||||
}
|
||||
}
|
||||
export function parse(s: string): ParseResult {
|
||||
const p = new Parser(s);
|
||||
return p.parse();
|
||||
}
|
||||
export interface ParseResult {
|
||||
ast: Nullable<start>;
|
||||
errs: SyntaxErr[];
|
||||
}
|
||||
export interface PosInfo {
|
||||
readonly overallPos: number;
|
||||
readonly line: number;
|
||||
readonly offset: number;
|
||||
}
|
||||
export interface RegexMatch {
|
||||
readonly kind: "RegexMatch";
|
||||
readonly negated: boolean;
|
||||
readonly literal: string;
|
||||
}
|
||||
export type EOFMatch = { kind: "EOF"; negated: boolean };
|
||||
export type MatchAttempt = RegexMatch | EOFMatch;
|
||||
export class SyntaxErr {
|
||||
public pos: PosInfo;
|
||||
public expmatches: MatchAttempt[];
|
||||
constructor(pos: PosInfo, expmatches: MatchAttempt[]) {
|
||||
this.pos = pos;
|
||||
this.expmatches = [...expmatches];
|
||||
}
|
||||
public toString(): string {
|
||||
return `Syntax Error at line ${this.pos.line}:${this.pos.offset}. Expected one of ${this.expmatches.map(x => x.kind === "EOF" ? " EOF" : ` ${x.negated ? 'not ': ''}'${x.literal}'`)}`;
|
||||
}
|
||||
}
|
||||
class ErrorTracker {
|
||||
private mxpos: PosInfo = {overallPos: -1, line: -1, offset: -1};
|
||||
private regexset: Set<string> = new Set();
|
||||
private pmatches: MatchAttempt[] = [];
|
||||
public record(pos: PosInfo, result: any, att: MatchAttempt) {
|
||||
if ((result === null) === att.negated)
|
||||
return;
|
||||
if (pos.overallPos > this.mxpos.overallPos) {
|
||||
this.mxpos = pos;
|
||||
this.pmatches = [];
|
||||
this.regexset.clear()
|
||||
}
|
||||
if (this.mxpos.overallPos === pos.overallPos) {
|
||||
if(att.kind === "RegexMatch") {
|
||||
if(!this.regexset.has(att.literal))
|
||||
this.pmatches.push(att);
|
||||
this.regexset.add(att.literal);
|
||||
} else {
|
||||
this.pmatches.push(att);
|
||||
}
|
||||
}
|
||||
}
|
||||
public getErr(): SyntaxErr | null {
|
||||
if (this.mxpos.overallPos !== -1)
|
||||
return new SyntaxErr(this.mxpos, this.pmatches);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
95
src/phonology.ts
Normal file
95
src/phonology.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import Filter from './filter';
|
||||
import type { filter_desc } from './filter';
|
||||
|
||||
class RejectError extends Error {};
|
||||
|
||||
class WeightedRandom {
|
||||
private readonly phonemes: string[];
|
||||
private readonly weights: number[];
|
||||
private readonly total: number;
|
||||
|
||||
constructor(phonemes: string[]) {
|
||||
this.phonemes = phonemes;
|
||||
const base = Math.log(phonemes.length + 1);
|
||||
|
||||
this.weights = [];
|
||||
for (let i = 0; i < phonemes.length; i++) {
|
||||
this.weights.push(base - Math.log(i+1));
|
||||
}
|
||||
|
||||
this.total = this.weights.reduce((x, y) => x + y, 0);
|
||||
}
|
||||
|
||||
choose(): string {
|
||||
const roll = Math.random() * this.total;
|
||||
let accumulator = 0;
|
||||
for (let i = 0; i < this.phonemes.length; i++) {
|
||||
accumulator += this.weights[i];
|
||||
if (accumulator > roll)
|
||||
return this.phonemes[i];
|
||||
}
|
||||
|
||||
// Should never get here, return the most common item.
|
||||
return this.phonemes[0];
|
||||
}
|
||||
};
|
||||
|
||||
class PhoneMap extends Map<string, WeightedRandom> {
|
||||
replace(input: string, rand_rate: number): string {
|
||||
let self = this;
|
||||
|
||||
function replacer(match, className, questionMark, offset, str) {
|
||||
if (questionMark && (Math.random() * 100) > rand_rate)
|
||||
return "";
|
||||
|
||||
let choices = self.get(className);
|
||||
return self.replace(choices.choose());
|
||||
}
|
||||
return input.replaceAll(/([A-Z])(\?)?/g, replacer);
|
||||
}
|
||||
}
|
||||
|
||||
export class Phonology extends Filter {
|
||||
private readonly patterns: WeightedRandom;
|
||||
private readonly classes: PhoneMap;
|
||||
|
||||
constructor(patterns: string[], classes: Map<string, string[]>, filters: filter_desc[]) {
|
||||
super(filters);
|
||||
this.patterns = new WeightedRandom(patterns);
|
||||
|
||||
this.classes = new PhoneMap();
|
||||
for (const [name, phones] of classes)
|
||||
this.classes.set(name, new WeightedRandom(phones));
|
||||
}
|
||||
|
||||
generate(count: number, rand_rate: number): string[] {
|
||||
return Array
|
||||
.from({length: count}, () => {
|
||||
let result = this.classes.replace(this.patterns.choose(), rand_rate);
|
||||
return this.filter(result);
|
||||
|
||||
const form = this.patterns.choose()
|
||||
.split(/([A-Z]\??)/)
|
||||
.filter(s => {
|
||||
if (s.endsWith('?'))
|
||||
return (Math.random() * 100) <= rand_rate;
|
||||
return !!s;
|
||||
})
|
||||
.map(s => {
|
||||
const pclass = s.substring(0,1);
|
||||
const ph = this.classes.get(pclass);
|
||||
if (!ph) {
|
||||
const all = [...this.classes.keys()].map(k => `'${k}'`).join(', ');
|
||||
throw new Error(`Unknown phoneme class '${pclass}' in ${all}`);
|
||||
}
|
||||
return ph.choose();
|
||||
})
|
||||
.join("");
|
||||
|
||||
return form;
|
||||
})
|
||||
.filter((s): s is "string" => (typeof s === "string"));
|
||||
}
|
||||
};
|
||||
|
||||
export default Phonology;
|
||||
Reference in New Issue
Block a user