diff --git a/README.md b/README.md
index b3bacb2..e02ef99 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# Railroad Diagrams
+# Railroad Diagrams (or Syntax Diagrams)
+
+Railroad diagrams are a way to visualize context-free grammars.
## How to Read
@@ -9,14 +11,22 @@ The following conventions are used:
- The following diagram shows values `A`, `B` and `C`, which must be specified. The required values are defined on the
main line of the diagram. (_ABNF_: `and = A B C`)
- ![explain-and](./testdata/explain-and.svg)
+
+
+
- The following diagram shows the optional value `A`. The value can be bypassed by following the empty path.
(_ABNF_: `opt = [A] B `)
- ![explain-optional](./testdata/explain-optional.svg)
+
+
+
+- In the example below `A`, `B` and `C` are options. The value can be chosen from the options.
+ (_ABNF_: `or = A / B / C`)
+
+
## Example
-![example-svg](./testdata/example1.svg)
+
## References
diff --git a/internal/svg/bypass.go b/internal/svg/bypass.go
index 3676aad..eda74dd 100644
--- a/internal/svg/bypass.go
+++ b/internal/svg/bypass.go
@@ -1,39 +1,29 @@
package svg
-import "fmt"
+import (
+ "fmt"
+)
type Bypass struct {
Element SVGable
}
func (b Bypass) SVG(start Point) (string, Point, Box) {
- wrappedStart := Point{X: start.X + curveRadius*2, Y: start.Y}
- wrappedSVG, wrappedEnd, wrappedBox := b.Element.SVG(wrappedStart)
+ _, _, wrappedBox := b.Element.SVG(start)
+ l := start.Y - wrappedBox.Position.Y + curveRadius
- l := wrappedBox.Size.Y + (wrappedBox.Position.Y - start.Y) - curveRadius
-
- curveStartSVG, curveStart, curveStartBox := CurveInvS{L: l}.SVG(start)
- curveEndSVG, curveEnd, curveEndBox := CurveS{
- L: curveStart.Y - wrappedEnd.Y - 2*curveRadius,
- }.SVG(Point{
- X: curveStart.X + wrappedBox.Size.X,
- Y: curveStart.Y,
- })
- return fmt.Sprintf(
- `
-
-%s
-
+ curveStartSVG, curveStart, curveStartBox := Line{RelativeEnd: Point{X: 2 * curveRadius, Y: l}}.SVG(start)
+ wrappedSVG, wrappedEnd, wrappedBox := b.Element.SVG(curveStart)
+ curveEndSVG, curveEnd, curveEndBox := Line{RelativeEnd: Point{X: 2 * curveRadius, Y: -l}}.SVG(wrappedEnd)
+ return fmt.Sprintf(`
+
+%s
%s
-
%s
`,
- start.X, start.Y, wrappedStart.X, wrappedStart.Y,
- wrappedSVG,
- wrappedEnd.X, wrappedEnd.Y, curveEnd.X, curveEnd.Y,
-
+ start.X, start.Y, curveEnd.X, curveEnd.Y,
curveStartSVG,
- curveStart.X, curveStart.Y, curveStart.X+wrappedBox.Size.X, curveStart.Y,
+ wrappedSVG,
curveEndSVG,
), curveEnd, curveStartBox.Combine(wrappedBox).Combine(curveEndBox)
}
diff --git a/internal/svg/curve.go b/internal/svg/curve.go
deleted file mode 100644
index 538c889..0000000
--- a/internal/svg/curve.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package svg
-
-import "fmt"
-
-type CurveInvS struct {
- L float64
-}
-
-func (c CurveInvS) SVG(start Point) (string, Point, Box) {
- r, ry, x, y, l := curveRadius, curveRadius, start.X, start.Y, c.L
- return fmt.Sprintf(
- ``,
- x, y,
-
- x+r, y,
- x+r, y+ry,
-
- x+r, y+ry+l,
-
- x+r, y+2*ry+l,
- x+2*r, y+2*ry+l,
-
- strokeWidth,
- ), Point{X: x + 2*r, Y: y + 2*ry + l}, Box{
- Position: Point{X: x, Y: y},
- Size: Point{X: 2 * r, Y: 2*ry + l},
- }
-}
-
-type CurveS struct {
- L float64
-}
-
-func (c CurveS) SVG(start Point) (string, Point, Box) {
- r, ry, x, y, l, ly := curveRadius, -curveRadius, start.X, start.Y, -c.L, c.L
- return fmt.Sprintf(
- ``,
- x, y,
-
- x+r, y,
- x+r, y+ry,
-
- x+r, y+ry+l,
-
- x+r, y+2*ry+l,
- x+2*r, y+2*ry+l,
-
- strokeWidth,
- ), Point{X: x + 2*r, Y: y + 2*ry + l}, Box{
- Position: Point{X: x, Y: y + 2*ry + l},
- Size: Point{X: 2 * r, Y: 2*r + ly},
- }
-}
diff --git a/internal/svg/settings.go b/internal/svg/settings.go
index c0c9144..28c484f 100644
--- a/internal/svg/settings.go
+++ b/internal/svg/settings.go
@@ -28,6 +28,11 @@ func SetCurveRadius(r float64) {
curveRadius = r
}
+// GetCurveRadius returns the configured curve radius.
+func GetCurveRadius() float64 {
+ return curveRadius
+}
+
// SetStrokeWidth sets the stroke width of the curve.
func SetStrokeWidth(w float64) {
strokeWidth = w
diff --git a/internal/svg/svg.go b/internal/svg/svg.go
index 2de4d7f..6ad86ed 100644
--- a/internal/svg/svg.go
+++ b/internal/svg/svg.go
@@ -9,9 +9,9 @@ type SVGable interface {
func DebugSVG(element SVGable, start Point) (string, Point, Box) {
svgElement, end, box := element.SVG(start)
return fmt.Sprintf(
- `
-
-%s
+ `
+
+%s
`,
diff --git a/internal/svg/svg_test.go b/internal/svg/svg_test.go
index cb9ec96..85d1b3c 100644
--- a/internal/svg/svg_test.go
+++ b/internal/svg/svg_test.go
@@ -8,22 +8,6 @@ import (
func TestCurve(t *testing.T) {
if os.Getenv("GENERATE_SVG") != "" {
- for idx, e := range []struct {
- element SVGable
- start Point
- }{
- {CurveS{L: 10}, Point{X: 10, Y: 30}},
- {CurveInvS{L: 10}, Point{X: 10, Y: 10}},
- } {
- svg, _, box := DebugSVG(e.element, e.start)
- _ = os.WriteFile(fmt.Sprintf("testdata/curve%d.svg", idx), []byte(
- fmt.Sprintf(
- ``,
- box.Size.X+20, box.Size.Y+20, svg,
- ),
- ), 0644)
- }
-
t.Run("lines", func(t *testing.T) {
for _, size := range []float64{1, 10} {
var combinedSVG string
@@ -55,7 +39,8 @@ func TestCurve(t *testing.T) {
}
_ = os.WriteFile(fmt.Sprintf("testdata/lines%.0f.svg", size), []byte(
fmt.Sprintf(
- ``,
+ ``,
combinedBox.Position.X-10, combinedBox.Position.Y-10,
combinedBox.Size.X+20, combinedBox.Size.Y+20,
combinedSVG,
@@ -70,7 +55,7 @@ func TestCurve(t *testing.T) {
combinedBox := Box{Position: start}
for _, e := range []SVGable{
Start{},
- CurveS{L: 10},
+ Line{RelativeEnd: Point{X: 10, Y: -10}},
Line{RelativeEnd: Point{X: 10, Y: 10}},
Bypass{
Element: TextBox{Text: "Hello,"},
@@ -78,7 +63,7 @@ func TestCurve(t *testing.T) {
Line{RelativeEnd: Point{X: 10, Y: -10}},
TextBox{Text: "World!", RoundedBorders: true},
Line{RelativeEnd: Point{X: 10}},
- CurveInvS{L: 10},
+ Line{RelativeEnd: Point{X: 10, Y: 10}},
End{},
} {
svg, end, box := DebugSVG(e, start)
@@ -89,7 +74,8 @@ func TestCurve(t *testing.T) {
}
_ = os.WriteFile("testdata/total.svg", []byte(
fmt.Sprintf(
- ``,
+ ``,
combinedBox.Position.X-10, combinedBox.Position.Y-10,
combinedBox.Size.X+20, combinedBox.Size.Y+20,
combinedSVG,
diff --git a/internal/svg/testdata/curve0.svg b/internal/svg/testdata/curve0.svg
deleted file mode 100644
index 319f9c5..0000000
--- a/internal/svg/testdata/curve0.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
\ No newline at end of file
diff --git a/internal/svg/testdata/curve1.svg b/internal/svg/testdata/curve1.svg
deleted file mode 100644
index c1f1181..0000000
--- a/internal/svg/testdata/curve1.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
\ No newline at end of file
diff --git a/internal/svg/testdata/lines1.svg b/internal/svg/testdata/lines1.svg
index 2811855..773f291 100644
--- a/internal/svg/testdata/lines1.svg
+++ b/internal/svg/testdata/lines1.svg
@@ -1,81 +1,82 @@
-
\ No newline at end of file
diff --git a/internal/svg/testdata/lines10.svg b/internal/svg/testdata/lines10.svg
index bc8e94b..35f697f 100644
--- a/internal/svg/testdata/lines10.svg
+++ b/internal/svg/testdata/lines10.svg
@@ -1,81 +1,82 @@
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/internal/svg/testdata/total.svg b/internal/svg/testdata/total.svg
index aecf55d..b3d25d2 100644
--- a/internal/svg/testdata/total.svg
+++ b/internal/svg/testdata/total.svg
@@ -1,61 +1,60 @@
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-Hello,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-World!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+Hello,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+World!
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/railroad.go b/railroad.go
index 053cc81..bf0d337 100644
--- a/railroad.go
+++ b/railroad.go
@@ -9,8 +9,7 @@ func GenerateSVG(r Rule) string {
margin := 10.0
ruleSVG, _, box := r.SVG(svg.Point{})
return fmt.Sprintf(
- `
-%s
+ `%s
`,
box.Position.X-margin, box.Position.Y-margin,
box.Size.X+2*margin, box.Size.Y+2*margin,
@@ -40,7 +39,11 @@ func (a AndExpression) SVG(start svg.Point) (string, svg.Point, svg.Box) {
start.X += 10
}
}
- return fmt.Sprintf(`%s`, combinedSVG), start, startBox
+ return fmt.Sprintf(`
+%s
+`,
+ combinedSVG,
+ ), start, startBox
}
type Expression interface {
@@ -49,6 +52,29 @@ type Expression interface {
expression()
}
+type OrExpression struct {
+ Expr []Expression
+}
+
+func (OrExpression) expression() {}
+
+func (o OrExpression) SVG(start svg.Point) (string, svg.Point, svg.Box) {
+ var combinedSVG string
+ var end svg.Point
+ var combinedBox svg.Box
+ var height float64
+ for _, e := range o.Expr {
+ startLineSVG, startLineEnd, startLineBox := svg.Line{RelativeEnd: svg.Point{X: 10, Y: height}}.SVG(start)
+ wrappedSVG, wrappedEnd, wrappedBox := e.SVG(startLineEnd)
+ endLineSVG, endLineEnd, endLineBox := svg.Line{RelativeEnd: svg.Point{X: 10, Y: -height}}.SVG(wrappedEnd)
+ combinedSVG += startLineSVG + wrappedSVG + endLineSVG
+ end = endLineEnd
+ combinedBox = combinedBox.Combine(startLineBox).Combine(wrappedBox).Combine(endLineBox)
+ height += wrappedBox.Size.Y + svg.GetCurveRadius()
+ }
+ return combinedSVG, end, combinedBox
+}
+
type OptionalExpression struct {
Expr Expression
}
@@ -68,7 +94,13 @@ func (r Rule) SVG(start svg.Point) (string, svg.Point, svg.Box) {
startSVG, startEnd, startBox := svg.Start{}.SVG(start)
ruleSVG, ruleEnd, ruleBox := r.Expr.SVG(startEnd)
endSVG, endEnd, endBox := svg.End{}.SVG(ruleEnd)
- return fmt.Sprintf(`%s%s%s`, r.Name, startSVG, ruleSVG, endSVG), endEnd, startBox.Combine(ruleBox).Combine(endBox)
+ return fmt.Sprintf(`
+%s
+%s
+%s
+`,
+ r.Name, startSVG, ruleSVG, endSVG,
+ ), endEnd, startBox.Combine(ruleBox).Combine(endBox)
}
type Term struct {
@@ -83,5 +115,9 @@ func (Term) expression() {}
func (t Term) SVG(start svg.Point) (string, svg.Point, svg.Box) {
termSVG, termEnd, termBox := svg.TextBox{Text: t.Value}.SVG(start)
- return fmt.Sprintf(`%s`, termSVG), termEnd, termBox
+ return fmt.Sprintf(`
+%s
+`,
+ termSVG,
+ ), termEnd, termBox
}
diff --git a/railroad_test.go b/railroad_test.go
index 5c0149f..53995f6 100644
--- a/railroad_test.go
+++ b/railroad_test.go
@@ -29,6 +29,28 @@ func TestRailroad(t *testing.T) {
},
},
},
+ {
+ Name: "explain-or",
+ Expr: railroad.OrExpression{
+ Expr: []railroad.Expression{
+ railroad.T("A"),
+ railroad.T("B"),
+ railroad.T("C"),
+ },
+ },
+ },
+ {
+ Name: "explain-optional-or",
+ Expr: railroad.OptionalExpression{
+ Expr: railroad.OrExpression{
+ Expr: []railroad.Expression{
+ railroad.T("A"),
+ railroad.T("B"),
+ railroad.T("C"),
+ },
+ },
+ },
+ },
{
Name: "example1",
@@ -43,6 +65,16 @@ func TestRailroad(t *testing.T) {
},
},
},
+ {
+ Name: "optional3",
+ Expr: railroad.OptionalExpression{
+ Expr: railroad.OptionalExpression{
+ Expr: railroad.OptionalExpression{
+ Expr: railroad.T("?"),
+ },
+ },
+ },
+ },
} {
t.Run(r.Name, func(t *testing.T) {
f, err := os.OpenFile("testdata/"+r.Name+".svg", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
diff --git a/testdata/example1.svg b/testdata/example1.svg
index 96aa91e..3a5fb1c 100644
--- a/testdata/example1.svg
+++ b/testdata/example1.svg
@@ -1,25 +1,30 @@
-
+
-
-
-
-
-
-
-Hello,
-
-
-
-
-
-
-
-World!
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+Hello,
+
+
+
+
+
+
+World!
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/explain-and.svg b/testdata/explain-and.svg
index 75c3c18..453de76 100755
--- a/testdata/explain-and.svg
+++ b/testdata/explain-and.svg
@@ -1,14 +1,25 @@
-
+
+
+
+
A
-
+
+
+
B
-
+
+
+
C
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/explain-optional-or.svg b/testdata/explain-optional-or.svg
new file mode 100755
index 0000000..ccdd341
--- /dev/null
+++ b/testdata/explain-optional-or.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+A
+
+
+
+
+B
+
+
+
+
+C
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/explain-optional.svg b/testdata/explain-optional.svg
index 165aca1..7fc6152 100755
--- a/testdata/explain-optional.svg
+++ b/testdata/explain-optional.svg
@@ -1,18 +1,25 @@
-
+
-
-
-
-
-A
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+A
+
+
+
+
+
B
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/explain-or.svg b/testdata/explain-or.svg
new file mode 100755
index 0000000..b628aea
--- /dev/null
+++ b/testdata/explain-or.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+A
+
+
+
+
+B
+
+
+
+
+C
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdata/optional3.svg b/testdata/optional3.svg
new file mode 100755
index 0000000..8e68604
--- /dev/null
+++ b/testdata/optional3.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+?
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file