Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Diagram.fromJSON() to construct a diagram from JSON data #78

Open
wants to merge 1 commit into
base: gh-pages
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ const d = new Diagram("foo", new Choice(0, "bar", "baz"));
// Or use the functions that call the constructors for you
import rr from "./railroad.js";
const d = rr.Diagram("foo", rr.Choice(0, "bar", "baz"));

// Or use the JSON serialization of the diagram
import {Diagram} from "./railroad.js";
const d = Diagram.fromJSON([
{ type: 'Terminal', text: 'foo' }.
{ type: 'Choice', normalIndex: 0, options: [
{ type: 'Terminal', text: 'bar' }, { type: 'Terminal', text: 'baz' }
] } ]);
```

Alternately, you can call ComplexDiagram();
Expand All @@ -55,7 +63,6 @@ The Diagram class also has a few methods:
* `.toSVG()` outputs the diagram as an actual `<svg>` DOM element, ready for appending into a document.
* `.addTo(parent?)` directly appends the diagram, as an `<svg>` element, to the specified parent element. If you omit the parent element, it instead appends to the script element it's being called from, so you can easily insert a diagram into your document by just dropping a tiny inline `<script>` that just calls `new Diagram(...).addTo()` where you want the diagram to show up.


Components
----------

Expand Down Expand Up @@ -137,6 +144,40 @@ you'll have to go adjust the options specifying the text metrics as well.
* `Options.COMMENT_CHAR_WIDTH` - the approximate width, in CSS px, of character in `Comment` text, which by default is smaller than the other textual items. Defaults to `7`.
* `Options.DEBUG` - if `true`, writes some additional "debug information" into the attributes of elements in the output, to help debug sizing issues. Defaults to `false`.

JSON
----

Diagrams can be created from a JSON serialization using `Diagram.fromJSON(input)` or `ComplexDiagram.fromJSON(input)`. (If the JSON input starts with a `Diagram` or `ComplexDigram` node, it will be honoured and the parent class of `fromJSON` will not apply.)

The JSON serialization can be a single object or an array of objects in the format `{ "type": "...", ...parameters }`, where `type` is a class name of a node and `parameters` are constructor arguments following more-or-less closely the naming conventions from the documentation above.

```
{ "type": "Diagram", "items" }
{ "type": "ComplexDiagram", "items" }

{ "type": "Terminal, "text", "href", "title" }
{ "type": "NonTerminal", "text", "href", "title" }
{ "type": "Comment", "text", "href", "title" }
{ "type": "Skip" }
{ "type": "Start", "startType", "label" }
{ "type": "End", "endType" }

{ "type": "Sequence", "items" }
{ "type": "Stack", "items" }
{ "type": "OptionalSequence", "items" }
{ "type": "Sequence", "items" }
{ "type": "Choice", "normalIndex", "options" }
{ "type": "MultipleChoice", "normalIndex", "choiceType", "options" }
{ "type": "HorizontalChoice", "options" }
{ "type": "Optional", "item", "skip" }
{ "type": "OneOrMore", "item", "repeat" }
{ "type": "AlternatingSequence", "option1", "option2" }
{ "type": "ZeroOrMore", "item", "repeat", "skip" }
{ "type": "Group", "item", "label" }
```

If the diagram input should be edited manually, using YAML instead of JSON will make maintenance easier. YAML can be converted to JSON before calling `fromJSON`.

Caveats
-------

Expand Down
89 changes: 89 additions & 0 deletions railroad.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,12 @@ export class Diagram extends DiagramMultiContainer {
delete this.attrs.xmlns;
return result;
}
static fromJSON(input = []) {
return diagramFromJSON(Diagram, input);
}
}
funcs.Diagram = (...args)=>new Diagram(...args);
funcs.Diagram.fromJSON = Diagram.fromJSON;


export class ComplexDiagram extends FakeSVG {
Expand All @@ -319,8 +323,12 @@ export class ComplexDiagram extends FakeSVG {
diagram.items[diagram.items.length-1] = new End({type:"complex"});
return diagram;
}
static fromJSON(input = []) {
return diagramFromJSON(ComplexDiagram, input);
}
}
funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args);
funcs.ComplexDiagram.fromJSON = ComplexDiagram.fromJSON;


export class Sequence extends DiagramMultiContainer {
Expand Down Expand Up @@ -1414,3 +1422,84 @@ function* enumerate(iter) {
count++;
}
}

function diagramFromJSON(Diagram, input) {
if (!input) return new Diagram();
// Wrap an array of nodes in the diagram type decided by the parent
// class of the calling static fromJSON method.
if (Array.isArray(input)) {
return new Diagram(...input.map(nodeFromJSON));
}
// Retain the diagram type specified in the input regardless the parent
// class of the calling static fromJSON method.
switch (input.type) {
case 'Diagram':
case 'ComplexDiagram':
return nodeFromJSON(input);
}
// Wrap the single node in the diagram type decided by the parent
// class of the calling static fromJSON method.
return new Diagram(nodeFromJSON(input));
}

const classes = {
Diagram, ComplexDiagram, Sequence, Stack, OptionalSequence, HorizontalChoice,
AlternatingSequence, Choice, MultipleChoice, Optional, OneOrMore, ZeroOrMore,
Group, Start, End, Terminal, NonTerminal, Comment, Skip
}

function nodeFromJSON(node) {
if (!node) return;
const Node = classes[node.type];
switch (Node) {
case Diagram:
case ComplexDiagram:
case Sequence:
case Stack:
case OptionalSequence:
case HorizontalChoice:
return new Node(...itemsFromJSON(node.items));

case AlternatingSequence:
return new Node(nodeFromJSON(node.option1), nodeFromJSON(node.option2));

case Choice:
return new Node(node.normalIndex || 0, ...itemsFromJSON(node.options));

case MultipleChoice:
return new Node(node.normalIndex || 0, node.choiceType,
...itemsFromJSON(node.options));

case Optional:
return new Node(nodeFromJSON(node.item), node.skip && 'skip');

case OneOrMore:
return new Node(nodeFromJSON(node.item), nodeFromJSON(node.repeat));

case ZeroOrMore:
return new Node(nodeFromJSON(node.item), nodeFromJSON(node.repeat),
node.skip && 'skip');

case Group:
return new Node(nodeFromJSON(node.item), node.label);

case Start:
return new Node(node.startType, node.label);

case End:
return new Node(node.endType);

case Terminal:
case NonTerminal:
case Comment:
return new Node(node.text, { href: node.href, title: node.title });

case Skip:
return new Node();
}
throw new Error(`Unknown node type: "${node.type}".`)
}

function itemsFromJSON(items) {
return items ? items.map(nodeFromJSON) : [];
}