1043 lines
32 KiB
JavaScript
1043 lines
32 KiB
JavaScript
/*
|
|
Railroad Diagrams
|
|
by Tab Atkins Jr. (and others)
|
|
http://xanthir.com
|
|
http://twitter.com/tabatkins
|
|
http://github.com/tabatkins/railroad-diagrams
|
|
|
|
This document and all associated files in the github project are licensed under CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
|
This means you can reuse, remix, or otherwise appropriate this project for your own use WITHOUT RESTRICTION.
|
|
(The actual legal meaning can be found at the above link.)
|
|
Don't ask me for permission to use any part of this project, JUST USE IT.
|
|
I would appreciate attribution, but that is not required by the license.
|
|
*/
|
|
|
|
/*
|
|
This file uses a module pattern to avoid leaking names into the global scope.
|
|
The only accidental leakage is the name "temp".
|
|
The exported names can be found at the bottom of this file;
|
|
simply change the names in the array of strings to change what they are called in your application.
|
|
|
|
As well, several configuration constants are passed into the module function at the bottom of this file.
|
|
At runtime, these constants can be found on the Diagram class.
|
|
*/
|
|
|
|
;(function(options) {
|
|
function subclassOf(baseClass, superClass) {
|
|
baseClass.prototype = Object.create(superClass.prototype)
|
|
baseClass.prototype.$super = superClass.prototype
|
|
}
|
|
|
|
function unnull(/* children */) {
|
|
return [].slice.call(arguments).reduce(function(sofar, x) {
|
|
return sofar !== undefined ? sofar : x
|
|
})
|
|
}
|
|
|
|
function determineGaps(outer, inner) {
|
|
var diff = outer - inner
|
|
switch (Diagram.INTERNAL_ALIGNMENT) {
|
|
case "left":
|
|
return [0, diff]
|
|
break
|
|
case "right":
|
|
return [diff, 0]
|
|
break
|
|
case "center":
|
|
default:
|
|
return [diff / 2, diff / 2]
|
|
break
|
|
}
|
|
}
|
|
|
|
function wrapString(value) {
|
|
return typeof value == "string" ? new Terminal(value) : value
|
|
}
|
|
|
|
function stackAtIllegalPosition(items) {
|
|
/* The height of the last line of the Stack is determined by the last child and
|
|
therefore any element outside the Stack could overlap with other elements.
|
|
If the Stack is the last element no overlap can occur. */
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i] instanceof Stack && i !== items.length - 1) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
function SVG(name, attrs, text) {
|
|
attrs = attrs || {}
|
|
text = text || ""
|
|
var el = document.createElementNS("http://www.w3.org/2000/svg", name)
|
|
for (var attr in attrs) {
|
|
if (attr === "xlink:href")
|
|
el.setAttributeNS(
|
|
"http://www.w3.org/1999/xlink",
|
|
"href",
|
|
attrs[attr]
|
|
)
|
|
else el.setAttribute(attr, attrs[attr])
|
|
}
|
|
el.textContent = text
|
|
return el
|
|
}
|
|
|
|
function FakeSVG(tagName, attrs, text) {
|
|
if (!(this instanceof FakeSVG)) return new FakeSVG(tagName, attrs, text)
|
|
if (text) this.children = text
|
|
else this.children = []
|
|
this.tagName = tagName
|
|
this.attrs = unnull(attrs, {})
|
|
return this
|
|
}
|
|
FakeSVG.prototype.format = function(x, y, width) {
|
|
// Virtual
|
|
}
|
|
FakeSVG.prototype.addTo = function(parent) {
|
|
if (parent instanceof FakeSVG) {
|
|
parent.children.push(this)
|
|
return this
|
|
} else {
|
|
var svg = this.toSVG()
|
|
parent.appendChild(svg)
|
|
return svg
|
|
}
|
|
}
|
|
FakeSVG.prototype.escapeString = function(string) {
|
|
// Escape markdown and HTML special characters
|
|
return string.replace(/[*_\`\[\]<&]/g, function(charString) {
|
|
return "&#" + charString.charCodeAt(0) + ";"
|
|
})
|
|
}
|
|
FakeSVG.prototype.toSVG = function() {
|
|
var el = SVG(this.tagName, this.attrs)
|
|
if (typeof this.children == "string") {
|
|
el.textContent = this.children
|
|
} else {
|
|
this.children.forEach(function(e) {
|
|
el.appendChild(e.toSVG())
|
|
})
|
|
}
|
|
return el
|
|
}
|
|
FakeSVG.prototype.toString = function() {
|
|
var str = "<" + this.tagName
|
|
var group = this.tagName == "g" || this.tagName == "svg"
|
|
for (var attr in this.attrs) {
|
|
str +=
|
|
" " +
|
|
attr +
|
|
'="' +
|
|
(this.attrs[attr] + "")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """) +
|
|
'"'
|
|
}
|
|
str += ">"
|
|
if (group) str += "\n"
|
|
if (typeof this.children == "string") {
|
|
str += FakeSVG.prototype.escapeString(this.children)
|
|
} else {
|
|
this.children.forEach(function(e) {
|
|
str += e
|
|
})
|
|
}
|
|
str += "</" + this.tagName + ">\n"
|
|
return str
|
|
}
|
|
|
|
function Path(x, y) {
|
|
if (!(this instanceof Path)) return new Path(x, y)
|
|
FakeSVG.call(this, "path")
|
|
this.attrs.d = "M" + x + " " + y
|
|
}
|
|
subclassOf(Path, FakeSVG)
|
|
Path.prototype.m = function(x, y) {
|
|
this.attrs.d += "m" + x + " " + y
|
|
return this
|
|
}
|
|
Path.prototype.h = function(val) {
|
|
this.attrs.d += "h" + val
|
|
return this
|
|
}
|
|
Path.prototype.right = Path.prototype.h
|
|
Path.prototype.left = function(val) {
|
|
return this.h(-val)
|
|
}
|
|
Path.prototype.v = function(val) {
|
|
this.attrs.d += "v" + val
|
|
return this
|
|
}
|
|
Path.prototype.down = Path.prototype.v
|
|
Path.prototype.up = function(val) {
|
|
return this.v(-val)
|
|
}
|
|
Path.prototype.arc = function(sweep) {
|
|
var x = Diagram.ARC_RADIUS
|
|
var y = Diagram.ARC_RADIUS
|
|
if (sweep[0] == "e" || sweep[1] == "w") {
|
|
x *= -1
|
|
}
|
|
if (sweep[0] == "s" || sweep[1] == "n") {
|
|
y *= -1
|
|
}
|
|
if (sweep == "ne" || sweep == "es" || sweep == "sw" || sweep == "wn") {
|
|
var cw = 1
|
|
} else {
|
|
var cw = 0
|
|
}
|
|
this.attrs.d +=
|
|
"a" +
|
|
Diagram.ARC_RADIUS +
|
|
" " +
|
|
Diagram.ARC_RADIUS +
|
|
" 0 0 " +
|
|
cw +
|
|
" " +
|
|
x +
|
|
" " +
|
|
y
|
|
return this
|
|
}
|
|
Path.prototype.format = function() {
|
|
// All paths in this library start/end horizontally.
|
|
// The extra .5 ensures a minor overlap, so there's no seams in bad rasterizers.
|
|
this.attrs.d += "h.5"
|
|
return this
|
|
}
|
|
|
|
function Diagram(items) {
|
|
if (!(this instanceof Diagram))
|
|
return new Diagram([].slice.call(arguments))
|
|
FakeSVG.call(this, "svg", { class: Diagram.DIAGRAM_CLASS })
|
|
if (stackAtIllegalPosition(items)) {
|
|
throw new RangeError(
|
|
"Stack() must only occur at the very last position of Diagram()."
|
|
)
|
|
}
|
|
this.items = items.map(wrapString)
|
|
this.items.unshift(new Start())
|
|
this.items.push(new End())
|
|
this.width =
|
|
this.items.reduce(function(sofar, el) {
|
|
return sofar + el.width + (el.needsSpace ? 20 : 0)
|
|
}, 0) + 1
|
|
this.height = this.items.reduce(function(sofar, el) {
|
|
return sofar + el.height
|
|
}, 0)
|
|
this.up = Math.max.apply(
|
|
null,
|
|
this.items.map(function(x) {
|
|
return x.up
|
|
})
|
|
)
|
|
this.down = Math.max.apply(
|
|
null,
|
|
this.items.map(function(x) {
|
|
return x.down
|
|
})
|
|
)
|
|
this.formatted = false
|
|
}
|
|
subclassOf(Diagram, FakeSVG)
|
|
for (var option in options) {
|
|
Diagram[option] = options[option]
|
|
}
|
|
Diagram.prototype.format = function(
|
|
paddingt,
|
|
paddingr,
|
|
paddingb,
|
|
paddingl
|
|
) {
|
|
paddingt = unnull(paddingt, 20)
|
|
paddingr = unnull(paddingr, paddingt, 20)
|
|
paddingb = unnull(paddingb, paddingt, 20)
|
|
paddingl = unnull(paddingl, paddingr, 20)
|
|
var x = paddingl
|
|
var y = paddingt
|
|
y += this.up
|
|
var g = FakeSVG(
|
|
"g",
|
|
Diagram.STROKE_ODD_PIXEL_LENGTH
|
|
? { transform: "translate(.5 .5)" }
|
|
: {}
|
|
)
|
|
for (var i = 0; i < this.items.length; i++) {
|
|
var item = this.items[i]
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(g)
|
|
x += 10
|
|
}
|
|
item.format(x, y, item.width + item.offsetX).addTo(g)
|
|
x += item.width + item.offsetX
|
|
y += item.height
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(g)
|
|
x += 10
|
|
}
|
|
}
|
|
this.attrs.width = this.width + paddingl + paddingr
|
|
this.attrs.height =
|
|
this.up + this.height + this.down + paddingt + paddingb
|
|
this.attrs.viewBox = "0 0 " + this.attrs.width + " " + this.attrs.height
|
|
g.addTo(this)
|
|
this.formatted = true
|
|
return this
|
|
}
|
|
Diagram.prototype.addTo = function(parent) {
|
|
var scriptTag = document.getElementsByTagName("script")
|
|
scriptTag = scriptTag[scriptTag.length - 1]
|
|
var parentTag = scriptTag.parentNode
|
|
parent = parent || parentTag
|
|
return this.$super.addTo.call(this, parent)
|
|
}
|
|
Diagram.prototype.toSVG = function() {
|
|
if (!this.formatted) {
|
|
this.format()
|
|
}
|
|
return this.$super.toSVG.call(this)
|
|
}
|
|
Diagram.prototype.toString = function() {
|
|
if (!this.formatted) {
|
|
this.format()
|
|
}
|
|
return this.$super.toString.call(this)
|
|
}
|
|
|
|
function ComplexDiagram() {
|
|
var diagram = new Diagram([].slice.call(arguments))
|
|
var items = diagram.items
|
|
items.shift()
|
|
items.pop()
|
|
items.unshift(new Start(false))
|
|
items.push(new End(false))
|
|
diagram.items = items
|
|
return diagram
|
|
}
|
|
|
|
function Sequence(items) {
|
|
if (!(this instanceof Sequence))
|
|
return new Sequence([].slice.call(arguments))
|
|
FakeSVG.call(this, "g")
|
|
if (stackAtIllegalPosition(items)) {
|
|
throw new RangeError(
|
|
"Stack() must only occur at the very last position of Sequence()."
|
|
)
|
|
}
|
|
this.items = items.map(wrapString)
|
|
this.width = this.items.reduce(function(sofar, el) {
|
|
return sofar + el.width + (el.needsSpace ? 20 : 0)
|
|
}, 0)
|
|
this.offsetX = 0
|
|
this.height = this.items.reduce(function(sofar, el) {
|
|
return sofar + el.height
|
|
}, 0)
|
|
this.up = this.items.reduce(function(sofar, el) {
|
|
return Math.max(sofar, el.up)
|
|
}, 0)
|
|
this.down = this.items.reduce(function(sofar, el) {
|
|
return Math.max(sofar, el.down)
|
|
}, 0)
|
|
}
|
|
subclassOf(Sequence, FakeSVG)
|
|
Sequence.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y + this.height)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
for (var i = 0; i < this.items.length; i++) {
|
|
var item = this.items[i]
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(this)
|
|
x += 10
|
|
}
|
|
item.format(x, y, item.width).addTo(this)
|
|
x += item.width
|
|
y += item.height
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(this)
|
|
x += 10
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
function Stack(items) {
|
|
if (!(this instanceof Stack)) return new Stack([].slice.call(arguments))
|
|
FakeSVG.call(this, "g")
|
|
if (stackAtIllegalPosition(items)) {
|
|
throw new RangeError(
|
|
"Stack() must only occur at the very last position of Stack()."
|
|
)
|
|
}
|
|
if (items.length === 0) {
|
|
throw new RangeError("Stack() must have at least one child.")
|
|
}
|
|
this.items = items.map(wrapString)
|
|
this.width = this.items.reduce(function(sofar, el) {
|
|
return Math.max(sofar, el.width + (el.needsSpace ? 20 : 0))
|
|
}, 0)
|
|
if (this.items.length > 1) {
|
|
this.width += Diagram.ARC_RADIUS * 2
|
|
}
|
|
|
|
this.up = this.items[0].up
|
|
this.down = this.items[this.items.length - 1].down
|
|
|
|
this.height = 0
|
|
for (var i = 0; i < this.items.length; i++) {
|
|
this.height += this.items[i].height
|
|
if (i !== this.items.length - 1) {
|
|
this.height +=
|
|
Math.max(this.items[i].down, Diagram.VERTICAL_SEPARATION) +
|
|
Math.max(
|
|
this.items[i + 1].up,
|
|
Diagram.VERTICAL_SEPARATION
|
|
) +
|
|
Diagram.ARC_RADIUS * 4
|
|
}
|
|
}
|
|
|
|
if (this.items.length === 0) {
|
|
this.offsetX = 0
|
|
} else {
|
|
// the value is usually negative because the linebreak resets the x value for the next element
|
|
this.offsetX = -(
|
|
this.width -
|
|
this.items[this.items.length - 1].width -
|
|
this.items[this.items.length - 1].offsetX -
|
|
(this.items[this.items.length - 1].needsSpace ? 20 : 0)
|
|
)
|
|
if (this.items.length > 1) {
|
|
this.offsetX += Diagram.ARC_RADIUS * 2
|
|
}
|
|
}
|
|
}
|
|
subclassOf(Stack, FakeSVG)
|
|
Stack.prototype.format = function(x, y, width) {
|
|
var xIntitial = x
|
|
|
|
for (var i = 0; i < this.items.length; i++) {
|
|
var item = this.items[i]
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(this)
|
|
x += 10
|
|
}
|
|
item.format(
|
|
x,
|
|
y,
|
|
Math.max(item.width + item.offsetX, Diagram.ARC_RADIUS * 2)
|
|
).addTo(this)
|
|
x += Math.max(item.width + item.offsetX, Diagram.ARC_RADIUS * 2)
|
|
y += item.height
|
|
if (item.needsSpace) {
|
|
Path(x, y)
|
|
.h(10)
|
|
.addTo(this)
|
|
x += 10
|
|
}
|
|
|
|
if (i !== this.items.length - 1) {
|
|
Path(x, y)
|
|
.arc("ne")
|
|
.down(Math.max(item.down, Diagram.VERTICAL_SEPARATION))
|
|
.arc("es")
|
|
.left(x - xIntitial - Diagram.ARC_RADIUS * 2)
|
|
.arc("nw")
|
|
.down(
|
|
Math.max(
|
|
this.items[i + 1].up,
|
|
Diagram.VERTICAL_SEPARATION
|
|
)
|
|
)
|
|
.arc("ws")
|
|
.addTo(this)
|
|
|
|
y +=
|
|
Math.max(item.down, Diagram.VERTICAL_SEPARATION) +
|
|
Math.max(
|
|
this.items[i + 1].up,
|
|
Diagram.VERTICAL_SEPARATION
|
|
) +
|
|
Diagram.ARC_RADIUS * 4
|
|
x = xIntitial + Diagram.ARC_RADIUS * 2
|
|
}
|
|
}
|
|
|
|
Path(x, y)
|
|
.h(width - (this.width + this.offsetX))
|
|
.addTo(this)
|
|
|
|
return this
|
|
}
|
|
|
|
function Choice(normal, items) {
|
|
if (!(this instanceof Choice))
|
|
return new Choice(normal, [].slice.call(arguments, 1))
|
|
FakeSVG.call(this, "g")
|
|
if (typeof normal !== "number" || normal !== Math.floor(normal)) {
|
|
throw new TypeError(
|
|
"The first argument of Choice() must be an integer."
|
|
)
|
|
} else if (normal < 0 || normal >= items.length) {
|
|
throw new RangeError(
|
|
"The first argument of Choice() must be an index for one of the items."
|
|
)
|
|
} else {
|
|
this.normal = normal
|
|
}
|
|
this.items = items.map(wrapString)
|
|
this.width =
|
|
this.items.reduce(function(sofar, el) {
|
|
return Math.max(sofar, el.width)
|
|
}, 0) +
|
|
Diagram.ARC_RADIUS * 4
|
|
this.offsetX = 0
|
|
this.height = this.items[normal].height
|
|
this.up = this.down = 0
|
|
for (var i = 0; i < this.items.length; i++) {
|
|
var item = this.items[i]
|
|
if (i < normal) {
|
|
this.up += Math.max(
|
|
Diagram.ARC_RADIUS,
|
|
item.up +
|
|
item.height +
|
|
item.down +
|
|
Diagram.VERTICAL_SEPARATION
|
|
)
|
|
}
|
|
if (i == normal) {
|
|
this.up += Math.max(Diagram.ARC_RADIUS, item.up)
|
|
this.down += Math.max(Diagram.ARC_RADIUS, item.down)
|
|
}
|
|
if (i > normal) {
|
|
this.down += Math.max(
|
|
Diagram.ARC_RADIUS,
|
|
Diagram.VERTICAL_SEPARATION +
|
|
item.up +
|
|
item.down +
|
|
item.height
|
|
)
|
|
}
|
|
}
|
|
}
|
|
subclassOf(Choice, FakeSVG)
|
|
Choice.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y + this.height)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
var last = this.items.length - 1
|
|
var innerWidth = this.width - Diagram.ARC_RADIUS * 4
|
|
|
|
// Do the elements that curve above
|
|
for (var i = this.normal - 1; i >= 0; i--) {
|
|
var item = this.items[i]
|
|
if (i == this.normal - 1) {
|
|
var distanceFromY = Math.max(
|
|
Diagram.ARC_RADIUS * 2,
|
|
this.items[i + 1].up +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
item.height +
|
|
item.down
|
|
)
|
|
}
|
|
Path(x, y)
|
|
.arc("se")
|
|
.up(distanceFromY - Diagram.ARC_RADIUS * 2)
|
|
.arc("wn")
|
|
.addTo(this)
|
|
item.format(
|
|
x + Diagram.ARC_RADIUS * 2,
|
|
y - distanceFromY,
|
|
innerWidth
|
|
).addTo(this)
|
|
Path(
|
|
x + Diagram.ARC_RADIUS * 2 + innerWidth,
|
|
y - distanceFromY + item.height
|
|
)
|
|
.arc("ne")
|
|
.down(
|
|
distanceFromY -
|
|
item.height +
|
|
this.items[this.normal].height -
|
|
Diagram.ARC_RADIUS * 2
|
|
)
|
|
.arc("ws")
|
|
.addTo(this)
|
|
distanceFromY += Math.max(
|
|
Diagram.ARC_RADIUS,
|
|
item.up +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
(i == 0
|
|
? 0
|
|
: this.items[i - 1].down + this.items[i - 1].height)
|
|
)
|
|
}
|
|
|
|
// Do the straight-line path.
|
|
Path(x, y)
|
|
.right(Diagram.ARC_RADIUS * 2)
|
|
.addTo(this)
|
|
this.items[this.normal]
|
|
.format(x + Diagram.ARC_RADIUS * 2, y, innerWidth)
|
|
.addTo(this)
|
|
Path(x + Diagram.ARC_RADIUS * 2 + innerWidth, y + this.height)
|
|
.right(Diagram.ARC_RADIUS * 2)
|
|
.addTo(this)
|
|
|
|
// Do the elements that curve below
|
|
for (var i = this.normal + 1; i <= last; i++) {
|
|
var item = this.items[i]
|
|
if (i == this.normal + 1) {
|
|
var distanceFromY = Math.max(
|
|
Diagram.ARC_RADIUS * 2,
|
|
this.items[i - 1].height +
|
|
this.items[i - 1].down +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
item.up
|
|
)
|
|
}
|
|
Path(x, y)
|
|
.arc("ne")
|
|
.down(distanceFromY - Diagram.ARC_RADIUS * 2)
|
|
.arc("ws")
|
|
.addTo(this)
|
|
item.format(
|
|
x + Diagram.ARC_RADIUS * 2,
|
|
y + distanceFromY,
|
|
innerWidth
|
|
).addTo(this)
|
|
Path(
|
|
x + Diagram.ARC_RADIUS * 2 + innerWidth,
|
|
y + distanceFromY + item.height
|
|
)
|
|
.arc("se")
|
|
.up(
|
|
distanceFromY -
|
|
Diagram.ARC_RADIUS * 2 +
|
|
item.height -
|
|
this.items[this.normal].height
|
|
)
|
|
.arc("wn")
|
|
.addTo(this)
|
|
distanceFromY += Math.max(
|
|
Diagram.ARC_RADIUS,
|
|
item.height +
|
|
item.down +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
(i == last ? 0 : this.items[i + 1].up)
|
|
)
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
function Optional(item, skip) {
|
|
if (skip === undefined) return Choice(1, Skip(), item)
|
|
else if (skip === "skip") return Choice(0, Skip(), item)
|
|
else throw "Unknown value for Optional()'s 'skip' argument."
|
|
}
|
|
|
|
function OneOrMore(item, rep) {
|
|
if (!(this instanceof OneOrMore)) return new OneOrMore(item, rep)
|
|
FakeSVG.call(this, "g")
|
|
rep = rep || new Skip()
|
|
this.item = wrapString(item)
|
|
this.rep = wrapString(rep)
|
|
this.width =
|
|
Math.max(this.item.width, this.rep.width) + Diagram.ARC_RADIUS * 2
|
|
this.offsetX = 0
|
|
this.height = this.item.height
|
|
this.up = this.item.up
|
|
this.down = Math.max(
|
|
Diagram.ARC_RADIUS * 2,
|
|
this.item.down +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
this.rep.up +
|
|
this.rep.height +
|
|
this.rep.down
|
|
)
|
|
}
|
|
subclassOf(OneOrMore, FakeSVG)
|
|
OneOrMore.prototype.needsSpace = true
|
|
OneOrMore.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y + this.height)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
// Draw item
|
|
Path(x, y)
|
|
.right(Diagram.ARC_RADIUS)
|
|
.addTo(this)
|
|
this.item
|
|
.format(
|
|
x + Diagram.ARC_RADIUS,
|
|
y,
|
|
this.width - Diagram.ARC_RADIUS * 2
|
|
)
|
|
.addTo(this)
|
|
Path(x + this.width - Diagram.ARC_RADIUS, y + this.height)
|
|
.right(Diagram.ARC_RADIUS)
|
|
.addTo(this)
|
|
|
|
// Draw repeat arc
|
|
var distanceFromY = Math.max(
|
|
Diagram.ARC_RADIUS * 2,
|
|
this.item.height +
|
|
this.item.down +
|
|
Diagram.VERTICAL_SEPARATION +
|
|
this.rep.up
|
|
)
|
|
Path(x + Diagram.ARC_RADIUS, y)
|
|
.arc("nw")
|
|
.down(distanceFromY - Diagram.ARC_RADIUS * 2)
|
|
.arc("ws")
|
|
.addTo(this)
|
|
this.rep
|
|
.format(
|
|
x + Diagram.ARC_RADIUS,
|
|
y + distanceFromY,
|
|
this.width - Diagram.ARC_RADIUS * 2
|
|
)
|
|
.addTo(this)
|
|
Path(
|
|
x + this.width - Diagram.ARC_RADIUS,
|
|
y + distanceFromY + this.rep.height
|
|
)
|
|
.arc("se")
|
|
.up(
|
|
distanceFromY -
|
|
Diagram.ARC_RADIUS * 2 +
|
|
this.rep.height -
|
|
this.item.height
|
|
)
|
|
.arc("en")
|
|
.addTo(this)
|
|
|
|
return this
|
|
}
|
|
|
|
function ZeroOrMore(item, rep, skip) {
|
|
return Optional(OneOrMore(item, rep), skip)
|
|
}
|
|
|
|
function Start(simpleType) {
|
|
if (!(this instanceof Start)) return new Start()
|
|
FakeSVG.call(this, "path")
|
|
this.width = 20
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 10
|
|
this.down = 10
|
|
this.simpleType = simpleType
|
|
}
|
|
subclassOf(Start, FakeSVG)
|
|
Start.prototype.format = function(x, y) {
|
|
if (this.simpleType === false) {
|
|
this.attrs.d = "M " + x + " " + (y - 10) + " v 20 m 0 -10 h 20.5"
|
|
} else {
|
|
this.attrs.d =
|
|
"M " +
|
|
x +
|
|
" " +
|
|
(y - 10) +
|
|
" v 20 m 10 -20 v 20 m -10 -10 h 20.5"
|
|
}
|
|
return this
|
|
}
|
|
|
|
function End(simpleType) {
|
|
if (!(this instanceof End)) return new End()
|
|
FakeSVG.call(this, "path")
|
|
this.width = 20
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 10
|
|
this.down = 10
|
|
this.simpleType = simpleType
|
|
}
|
|
subclassOf(End, FakeSVG)
|
|
End.prototype.format = function(x, y) {
|
|
if (this.simpleType === false) {
|
|
this.attrs.d = "M " + x + " " + y + " h 20 m 0 -10 v 20"
|
|
} else {
|
|
this.attrs.d =
|
|
"M " + x + " " + y + " h 20 m -10 -10 v 20 m 10 -20 v 20"
|
|
}
|
|
return this
|
|
}
|
|
|
|
function Terminal(
|
|
text,
|
|
href,
|
|
title,
|
|
occurrenceIdx,
|
|
topRuleName,
|
|
dslRuleName,
|
|
tokenName
|
|
) {
|
|
if (!(this instanceof Terminal))
|
|
return new Terminal(
|
|
text,
|
|
href,
|
|
title,
|
|
occurrenceIdx,
|
|
topRuleName,
|
|
dslRuleName,
|
|
tokenName
|
|
)
|
|
FakeSVG.call(this, "g", { class: "terminal" })
|
|
this.text = text
|
|
this.label = text
|
|
this.href = href
|
|
this.title = title
|
|
this.occurrenceIdx = occurrenceIdx
|
|
this.topRuleName = topRuleName
|
|
this.dslRuleName = dslRuleName
|
|
this.tokenName = tokenName
|
|
this.width =
|
|
text.length * 8 +
|
|
20 /* Assume that each char is .5em, and that the em is 16px */
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 11
|
|
this.down = 11
|
|
}
|
|
subclassOf(Terminal, FakeSVG)
|
|
Terminal.prototype.needsSpace = true
|
|
Terminal.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
FakeSVG("rect", {
|
|
x: x,
|
|
y: y - 11,
|
|
width: this.width,
|
|
height: this.up + this.down,
|
|
rx: 10,
|
|
ry: 10
|
|
}).addTo(this)
|
|
|
|
var text = FakeSVG(
|
|
"text",
|
|
{
|
|
x: x + this.width / 2,
|
|
y: y + 4,
|
|
occurrenceIdx: this.occurrenceIdx,
|
|
topRuleName: this.topRuleName,
|
|
dslRuleName: this.dslRuleName,
|
|
tokenName: this.tokenName,
|
|
label: this.label
|
|
},
|
|
this.text
|
|
)
|
|
var title = FakeSVG("title", {}, this.title)
|
|
if (this.href)
|
|
FakeSVG("a", { "xlink:href": this.href }, [text]).addTo(this)
|
|
else {
|
|
text.addTo(this)
|
|
if (this.title !== undefined) {
|
|
title.addTo(this)
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
function NonTerminal(text, href, occurrenceIdx, topRuleName) {
|
|
if (!(this instanceof NonTerminal))
|
|
return new NonTerminal(text, href, occurrenceIdx, topRuleName)
|
|
FakeSVG.call(this, "g", { class: "non-terminal" })
|
|
this.text = text
|
|
this.ruleName = text
|
|
this.href = href
|
|
this.occurrenceIdx = occurrenceIdx
|
|
this.topRuleName = topRuleName
|
|
this.width = text.length * 8 + 20
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 11
|
|
this.down = 11
|
|
}
|
|
subclassOf(NonTerminal, FakeSVG)
|
|
NonTerminal.prototype.needsSpace = true
|
|
NonTerminal.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
FakeSVG("rect", {
|
|
x: x,
|
|
y: y - 11,
|
|
width: this.width,
|
|
height: this.up + this.down
|
|
}).addTo(this)
|
|
var text = FakeSVG(
|
|
"text",
|
|
{
|
|
x: x + this.width / 2,
|
|
y: y + 4,
|
|
occurrenceIdx: this.occurrenceIdx,
|
|
topRuleName: this.topRuleName,
|
|
ruleName: this.ruleName
|
|
},
|
|
this.text
|
|
)
|
|
if (this.href)
|
|
FakeSVG("a", { "xlink:href": this.href }, [text]).addTo(this)
|
|
else text.addTo(this)
|
|
return this
|
|
}
|
|
|
|
function Comment(text) {
|
|
if (!(this instanceof Comment)) return new Comment(text)
|
|
FakeSVG.call(this, "g")
|
|
this.text = text
|
|
this.width = text.length * 7 + 10
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 11
|
|
this.down = 11
|
|
}
|
|
subclassOf(Comment, FakeSVG)
|
|
Comment.prototype.needsSpace = true
|
|
Comment.prototype.format = function(x, y, width) {
|
|
// Hook up the two sides if this is narrower than its stated width.
|
|
var gaps = determineGaps(width, this.width)
|
|
Path(x, y)
|
|
.h(gaps[0])
|
|
.addTo(this)
|
|
Path(x + gaps[0] + this.width, y + this.height)
|
|
.h(gaps[1])
|
|
.addTo(this)
|
|
x += gaps[0]
|
|
|
|
FakeSVG(
|
|
"text",
|
|
{
|
|
x: x + this.width / 2,
|
|
y: y + 5,
|
|
class: "comment"
|
|
},
|
|
this.text
|
|
).addTo(this)
|
|
return this
|
|
}
|
|
|
|
function Skip() {
|
|
if (!(this instanceof Skip)) return new Skip()
|
|
FakeSVG.call(this, "g")
|
|
this.width = 0
|
|
this.height = 0
|
|
this.offsetX = 0
|
|
this.up = 0
|
|
this.down = 0
|
|
}
|
|
subclassOf(Skip, FakeSVG)
|
|
Skip.prototype.format = function(x, y, width) {
|
|
Path(x, y)
|
|
.right(width)
|
|
.addTo(this)
|
|
return this
|
|
}
|
|
|
|
var root
|
|
if (typeof define === "function" && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
root = {}
|
|
define([], function() {
|
|
return root
|
|
})
|
|
} else if (typeof exports === "object") {
|
|
// CommonJS for node
|
|
root = exports
|
|
} else {
|
|
// Browser globals (root is window.railroad)
|
|
this.railroad = {}
|
|
root = this.railroad
|
|
}
|
|
|
|
var temp = [
|
|
Diagram,
|
|
ComplexDiagram,
|
|
Sequence,
|
|
Stack,
|
|
Choice,
|
|
Optional,
|
|
OneOrMore,
|
|
ZeroOrMore,
|
|
Terminal,
|
|
NonTerminal,
|
|
Comment,
|
|
Skip
|
|
]
|
|
/*
|
|
These are the names that the internal classes are exported as.
|
|
If you would like different names, adjust them here.
|
|
*/
|
|
;[
|
|
"Diagram",
|
|
"ComplexDiagram",
|
|
"Sequence",
|
|
"Stack",
|
|
"Choice",
|
|
"Optional",
|
|
"OneOrMore",
|
|
"ZeroOrMore",
|
|
"Terminal",
|
|
"NonTerminal",
|
|
"Comment",
|
|
"Skip"
|
|
].forEach(function(e, i) {
|
|
root[e] = temp[i]
|
|
})
|
|
}.call(this, {
|
|
VERTICAL_SEPARATION: 8,
|
|
ARC_RADIUS: 10,
|
|
DIAGRAM_CLASS: "railroad-diagram",
|
|
STROKE_ODD_PIXEL_LENGTH: true,
|
|
INTERNAL_ALIGNMENT: "center"
|
|
}))
|