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) + +explain-and + - 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) + +explain-optional + +- In the example below `A`, `B` and `C` are options. The value can be chosen from the options. + (_ABNF_: `or = A / B / C`) + +explain-or ## Example -![example-svg](./testdata/example1.svg) +example1 ## 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( - `%s`, - 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( - `%s`, + `%s +`, 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( - `%s`, + `%s +`, 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 + + \ 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