Skip to content

Commit

Permalink
Railroad: Treat empty string as epsilon, grammar reduction
Browse files Browse the repository at this point in the history
  • Loading branch information
nirname committed Sep 24, 2023
1 parent 681d8c9 commit a2fc6e8
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 76 deletions.
2 changes: 2 additions & 0 deletions cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"bilkent",
"bisheng",
"blrs",
"bnf",
"braintree",
"brkt",
"brolin",
Expand All @@ -42,6 +43,7 @@
"dompurify",
"dont",
"doublecircle",
"ebnf",
"edgechromium",
"elems",
"elkjs",
Expand Down
55 changes: 47 additions & 8 deletions packages/mermaid/src/diagrams/railroad/railroad.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import railroad from './railroadGrammar.jison';
// import { prepareTextForParsing } from '../railroadUtils.js';
import { cleanupComments } from '../../diagram-api/comments.js';
import { db, Rule } from './railroadDB.js';

Check failure on line 5 in packages/mermaid/src/diagrams/railroad/railroad.spec.ts

View workflow job for this annotation

GitHub Actions / lint (18.x)

Import "Rule" is only used as types
// @ts-ignore: yaml
// @ts-ignore: yaml does not export types
import defaultConfigJson from '../../schemas/config.schema.yaml?only-defaults=true';

describe('Railroad diagram', function () {
Expand All @@ -19,28 +19,65 @@ describe('Railroad diagram', function () {

describe('fails to parse', () => {
test.each([
['', 'keyword missing'],
['', 'keyword is missing'],
['rule', 'assign operator is missing'],
['rule==id', 'assign operator is wrong'],
['rule=id', '; missing'],
['rule=id', 'semicolon is missing'],
['rule=(id;', 'parentheses are unbalanced'],
['rule=(id));', 'parentheses are unbalanced'],
["' ::= x;", 'rule is with quote is not wrapped in <>'],
["rule ::= ';", 'quote in rule definition is not wrapped in <>'],
])('%s when %s', (grammar: string) => {
])('`%s` where %s', (grammar: string) => {
grammar = cleanupComments('' + grammar);
expect(() => railroad.parser.parse(grammar)).toThrow();
});
});

describe('parses', () => {
describe('Simple samples', () => {
describe('assignment operators', () => {
// const grammarDefinition = prepareTextForParsing(cleanupComments('railroad-beta\n\n ' + data));
test.each([
['rule ::= id;'],
['rule := id;'],
['rule : id;'],
['rule => id;'],
['rule = id;'],
['rule -> id;'],
])('`%s`', (grammar: string) => {
grammar = cleanupComments('railroad-beta' + grammar);
const grammarWithoutSpaces = grammar.replaceAll(' ', '');
expect(() => railroad.parser.parse(grammar)).not.toThrow();
expect(() => railroad.parser.parse(grammarWithoutSpaces)).not.toThrow();
});
});

describe('rules names', () => {
// const grammarDefinition = prepareTextForParsing(cleanupComments('railroad-beta\n\n ' + data));
test.each([
['rule::=id;'],
['<rule>::=id ;'],
['<rule with spaces>::=id;'],
[`<rule with "double quotes">::=id;`],
[`<rule with 'singe quotes'>::=id;`],
[`<rule with \\<angle quotes\\>>::=id;`],
[`<rule with different escapements \\' \\" \\< \\> \\\\ \\x>::=id;`],
])('`%s` produces', (grammar: string) => {
grammar = cleanupComments('railroad-beta' + grammar);
railroad.parser.parse(grammar);
const x = railroad.yy.getRules() as Rule[];
console.log(x.map((r) => r.toEBNF()));

Check failure on line 68 in packages/mermaid/src/diagrams/railroad/railroad.spec.ts

View workflow job for this annotation

GitHub Actions / lint (18.x)

Unexpected console statement
// expect(() => { railroad.parser.parse(grammar); }).not.toThrow();
// railroad.parser.parse(grammar);
});
});

describe('simple samples', () => {
// const grammarDefinition = prepareTextForParsing(cleanupComments('railroad-beta\n\n ' + data));
test.each([
[''],
['rule=;'],
['rule::=;'],
['rule::=id;'],
['rule::=(id);'],
['rule::=id-id;'],
['rule::=[id];'],
['rule::={id};'],
['rule::=id|id;'],
Expand All @@ -58,7 +95,7 @@ describe('Railroad diagram', function () {
['<"> ::= <"">;'],
["<while> ::= 'while' '(' <condition> ')' <statement>;"],
["<while-loop> ::= 'while' '(' <condition> ')' <statement>;"],
])('%s', (grammar: string) => {
])('`%s` produces', (grammar: string) => {
grammar = cleanupComments('railroad-beta' + grammar);
railroad.parser.parse(grammar);
const x = railroad.yy.getRules() as Rule[];
Expand Down Expand Up @@ -88,7 +125,9 @@ describe('Railroad diagram', function () {
railroad.parser.parse(grammar);
});
});
});

describe('recognizes', function () {
it('Arithmetic Expressions', () => {
const grammar = `
railroad-beta
Expand Down
68 changes: 45 additions & 23 deletions packages/mermaid/src/diagrams/railroad/railroadDB.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// import type { RailroadDB } from './railroadTypes.js';
import { config } from 'process';
import * as configApi from '../../config.js';
import type { DiagramDB } from '../../diagram-api/types.js';

Expand Down Expand Up @@ -46,13 +45,13 @@ const getConsole = () => console;
type Callback<T> = (item: Chunk, index: number, parent: Chunk | undefined, result: T[]) => T;
// type Traverse<T> = (callback: Callback<T>, index: number, parent?: Chunk) => T;

// interface Traversible {
// interface Traversable {
// traverse<T>(callback: Callback<T>, index?: number, parent?: Chunk): T;
// }

// TODO: rewrite toEBNF using traverse
//
// interface Chunk extends Traversible {
// interface Chunk extends Traversable {
// toEBNF(): string;
// }

Expand All @@ -63,17 +62,15 @@ abstract class Chunk {
abstract toEBNF(): string;
}

class Leaf implements Chunk {
abstract class Leaf implements Chunk {
constructor(public label: string) {}

traverse<T>(callback: Callback<T>, index?: number, parent?: Chunk): T {
index ??= 0;
return callback(this, index, parent, []);
}

toEBNF(): string {
return this.label;
}
abstract toEBNF(): string;
}

abstract class Node implements Chunk {
Expand Down Expand Up @@ -120,22 +117,43 @@ class Epsilon extends Leaf {
constructor() {
super('ɛ');
}

toEBNF(): string {
return this.label;
}
}

// remote quote???
class Term extends Leaf {
constructor(public label: string, public quote: string) {
super(label);
toEBNF(): string {
const escaped = this.label.replaceAll(/\\([\\'"])/g, "\\$1");

Check failure on line 128 in packages/mermaid/src/diagrams/railroad/railroadDB.ts

View workflow job for this annotation

GitHub Actions / lint (18.x)

/\\([\\'"])/g can be optimized to /\\(["'\\])/g

return '"' + escaped + '"';
}
}

class NonTerm extends Leaf {
toEBNF(): string {
return this.quote + super.toEBNF() + this.quote;
const escaped = this.label.replaceAll(/\\([\\'"<>])/g, "\\$1");

Check failure on line 136 in packages/mermaid/src/diagrams/railroad/railroadDB.ts

View workflow job for this annotation

GitHub Actions / lint (18.x)

/\\([\\'"<>])/g can be optimized to /\\(["'<>\\])/g

return '<' + escaped + '>';
}
}

class NonTerm extends Leaf {
class Exception implements Chunk {
constructor(public base: Chunk, public except: Chunk) {}

traverse<T>(callback: Callback<T>, index?: number, parent?: Chunk): T {
index ??= 0;
const nested = [
this.base.traverse(callback, 0, this),
this.except.traverse(callback, 1, this),
]

return callback(this, index, parent, nested);
}

toEBNF(): string {
return '<' + super.toEBNF() + '>';
return `(${this.base.toEBNF()}) - ${this.except.toEBNF()}`
}
}

Expand Down Expand Up @@ -172,13 +190,14 @@ class ZeroOrMany extends Node {
}
}

const addTerm = (label: string, quote: string): Chunk => {
return new Term(label, quote);
const addTerm = (label: string): Chunk => {
label.replaceAll(/\\(.)/g, "$1");

return new Term(label);
};
const addNonTerm = (label: string): Chunk => {
return new NonTerm(label);
};

const addZeroOrOne = (chunk: Chunk): Chunk => {
return new ZeroOrOne(chunk);
};
Expand All @@ -188,6 +207,9 @@ const addOneOrMany = (chunk: Chunk): Chunk => {
const addZeroOrMany = (chunk: Chunk): Chunk => {
return new ZeroOrMany(chunk);
};
const addException = (base: Chunk, except: Chunk): Chunk => {
return new Exception(base, except);
}
const addRuleOrChoice = (ID: string, chunk: Chunk): void => {
if (rules[ID]) {
const value = rules[ID];
Expand All @@ -205,13 +227,12 @@ const addSequence = (chunks: Chunk[]): Chunk => {

if (railroadConfig?.compress) {
chunks = chunks
.map((chunk) => {
.flatMap((chunk) => {
if (chunk instanceof Sequence) {
return chunk.children;
}
return chunk;
})
.flat();
});
}

if (chunks.length === 1) {
Expand All @@ -228,13 +249,12 @@ const addChoice = (chunks: Chunk[]): Chunk => {

if (configApi.getConfig().railroad?.compress) {
chunks = chunks
.map((chunk) => {
.flatMap((chunk) => {
if (chunk instanceof Choice) {
return chunk.children;
}
return chunk;
})
.flat();
});
}

if (chunks.length === 1) {
Expand All @@ -259,9 +279,10 @@ export interface RailroadDB extends DiagramDB {
addOneOrMany: (chunk: Chunk) => Chunk;
addRuleOrChoice: (ID: string, chunk: Chunk) => void;
addSequence: (chunks: Chunk[]) => Chunk;
addTerm: (label: string, quote: string) => Chunk;
addTerm: (label: string) => Chunk;
addZeroOrMany: (chunk: Chunk) => Chunk;
addZeroOrOne: (chunk: Chunk) => Chunk;
addException: (base: Chunk, except: Chunk) => Chunk;
clear: () => void;
getConsole: () => Console;
getRules: () => Rule[];
Expand All @@ -277,6 +298,7 @@ export const db: RailroadDB = {
addTerm,
addZeroOrMany,
addZeroOrOne,
addException,
clear,
getConfig: () => configApi.getConfig().railroad,
getConsole,
Expand Down
Loading

0 comments on commit a2fc6e8

Please sign in to comment.