diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9d5ecbc..b92e1e2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
- go-version: [1.20.x]
+ go-version: [1.21.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
diff --git a/.gitignore b/.gitignore
index d9e18a4..5a81918 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,9 @@
.DS_Store
vendor
-examples/*.svg
+
+# Hiding the SVG output files introduces friction when adding a new TXT,SVG pair to the test suite.
+# examples/*.svg
cmd/tmpl-expand/tmpl-expand
cmd/goat/goat
diff --git a/README.md b/README.md
index d7eff71..9997522 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
=> CONCLUSION: Don't use badges. Instead, pay attention to Action result status.
-->
-
+
## What **GoAT** Can Do For You
@@ -58,7 +58,7 @@ with rows above and below.
## Installation
```
- $ go install github.com/blampe/goat/cmd/goat@latest
+ $ go install github.com/dmullis/goat/cmd/goat@latest
```
@@ -176,14 +176,39 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character,
### Small Grids
```
- ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---.
- ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ |
- / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+
- \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ |
- / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+
- \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ |
- \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---'
-
+ ___ ___ .---+---+---+---+---. .---+---+---+---.
+ ___/ \___/ \ | | | | | | / \ / \ / \ / \ /
+/ \___/ \___/ +---+---+---+---+---+ +---+---+---+---+
+\___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \
+/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+
+\___/ \___/ \ | | a | | | | / \ / \ / \ / \ /
+ \___/ \___/ '---+---+---+---+---' '---+---+---+---'
+
+.---. .---. +--+ +-+ +-+ .-. .--. .-. +-+ +--+ .-. .--.
+| | | | | | | | | | | | | | | | | | | | | | | |
++---+ +---+ +--+ +-+ | | | | | | | | +-+ +--+ '-' '--'
+| +---+ | | | | | | | | | | | | |
++---+ b +---+ +--+ +-+ | | | | | | | |
+| a +---+ | | | | | | | | | | | | |
+'---' '---' +--+ +-+ +-+ '-' '--' '-'
+
++---+ .---. .---.
+| B | | E | | F |
++---+ '---' '---'
+.---. +---+ .---.
+| D | | G | | H |
+'---' +---+ '---'
+0123456789012345678901232
+
+Not Supported:
+ .---+---+---.
++---+ .-. .-. 0 | | a | | b |
+| A | | A | | B | 1 +++ +--++-+-++--+
++---+ '-' '-' 2 | | | | | | |
++---+ .-. .-. 3 +-+ +--++-+-++--+
+| C | | C | | D | 4 | | | | | |
++---+ '-' '-' 5 +++ '---+---+---'
+0123434567890123456 |
```
![](//examples/small-grids.svg)
@@ -244,12 +269,12 @@ The core engine of ```goat``` is accessible as a Go library package, for inclusi
code of your own.
The code implements a subset, and some extensions, of the ASCII diagram generation function of the browser-side Javascript in [Markdeep](http://casual-effects.com/markdeep/).
-A nicely formatted reference may be found at [pkg.go.dev](https://pkg.go.dev/github.com/blampe/goat).
+A nicely formatted reference may be found at [pkg.go.dev](https://pkg.go.dev/github.com/dmullis/goat).
### Installation
```
- $ go get -u github.com/blampe/goat/
+ $ go get -u github.com/dmullis/goat/
```
### Library Data Flow
![](//goat.svg)
@@ -261,13 +286,13 @@ source file [./goat.go](./goat.go).
### Project Tenets
diff --git a/canvas.go b/canvas.go
index 20d5c72..fba894a 100644
--- a/canvas.go
+++ b/canvas.go
@@ -2,7 +2,9 @@ package goat
import (
"bufio"
+ "log"
"io"
+ "os"
)
type (
@@ -10,11 +12,10 @@ type (
runeSet map[rune]exists
)
-
// Characters where more than one line segment can come together.
var jointRunes = []rune{
- '.',
- '\'',
+ '.', // possible ... top corner of a 90 degree angle, or curve
+ '\'', // possible ... bottom corner of a 90 degree angle, or curve
'+',
'*',
'o',
@@ -85,7 +86,7 @@ func isJoint(r rune) bool {
return contains(jointRunes, r)
}
-// XX rename 'isCircle()'?
+// XX rename 'isSpot()'?
func isDot(r rune) bool {
return r == 'o' || r == '*'
}
@@ -94,23 +95,6 @@ func isTriangle(r rune) bool {
return r == '^' || r == 'v' || r == '<' || r == '>'
}
-// Canvas represents a 2D ASCII rectangle.
-type Canvas struct {
- // units of cells
- Width, Height int
-
- data map[Index]rune
- text map[Index]rune
-}
-
-func (c *Canvas) heightScreen() int {
- return c.Height*16 + 8 + 1
-}
-
-func (c *Canvas) widthScreen() int {
- return (c.Width + 1) * 8
-}
-
// Arg 'canvasMap' is typically either Canvas.data or Canvas.text
func inSet(set runeSet, canvasMap map[Index]rune, i Index) (inset bool) {
r, inMap := canvasMap[i]
@@ -131,6 +115,26 @@ func (c *Canvas) runeAt(i Index) rune {
return ' '
}
+// Canvas represents a 2D ASCII rectangle.
+type Canvas struct {
+ // units of cells
+ Width, Height int
+
+ data map[Index]rune
+ text map[Index]rune
+}
+
+func (c *Canvas) heightScreen() int {
+ // XX Why " + 8 + 1"?
+ return c.Height*16 + 8 + 1
+}
+
+func (c *Canvas) widthScreen() int {
+ // XX Why "c.Width + 1"?
+ return (c.Width + 1) * 8
+}
+
+
// NewCanvas creates a fully-populated Canvas according to GoAT-formatted text read from
// an io.Reader, consuming all bytes available.
func NewCanvas(in io.Reader) (c Canvas) {
@@ -154,14 +158,19 @@ func NewCanvas(in io.Reader) (c Canvas) {
// https://go.dev/ref/spec#For_statements
// But yet, counterintuitively, type of lineStr[_index_] is 'byte'.
// https://go.dev/ref/spec#String_types
- // XXXX Refactor to use []rune from above.
for _, r := range lineStr {
//if r > 255 {
// fmt.Printf("linestr=\"%s\"\n", lineStr)
// fmt.Printf("r == 0x%x\n", r)
//}
if r == ' ' {
- panic("TAB character found on input")
+ file, isFile := in.(*os.File)
+ fileName := "unknown"
+ if isFile {
+ fileName = file.Name()
+ }
+ log.Panicf("\n\tFound TAB in %s, row %d, column %d\n",
+ fileName, height+1, w)
}
i := Index{w, height}
c.data[i] = r
@@ -197,755 +206,6 @@ func (c *Canvas) MoveToText() {
}
}
-// Drawable represents anything that can Draw itself.
-type Drawable interface {
- draw(out io.Writer)
-}
-
-// Line represents a straight segment between two points 'start' and 'stop', where
-// 'start' is either lesser in X (north-east, east, south-east), or
-// equal in X and lesser in Y (south).
-type Line struct {
- start Index
- stop Index
-
- startRune rune
- stopRune rune
-
- // dashed bool
- needsNudgingDown bool
- needsNudgingLeft bool
- needsNudgingRight bool
- needsTinyNudgingLeft bool
- needsTinyNudgingRight bool
-
- // This is a line segment all by itself. This centers the segment around
- // the midline.
- lonely bool
- // N or S. Only useful for half steps - chops of this half of the line.
- chop Orientation
-
- // X-major, Y-minor. Therefore, always one of the compass points NE, E, SE, S.
- orientation Orientation
-
- state lineState
-}
-
-type lineState int
-
-const (
- _Unstarted lineState = iota
- _Started
-)
-
-func (l *Line) started() bool {
- return l.state == _Started
-}
-
-func (c *Canvas) setStart(l *Line, i Index) {
- if l.state == _Unstarted {
- l.start = i
- l.startRune = c.runeAt(i)
- l.stop = i
- l.stopRune = c.runeAt(i)
- l.state = _Started
- }
-}
-
-func (c *Canvas) setStop(l *Line, i Index) {
- if l.state == _Started {
- l.stop = i
- l.stopRune = c.runeAt(i)
- }
-}
-
-func (l *Line) goesSomewhere() bool {
- return l.start != l.stop
-}
-
-func (l *Line) horizontal() bool {
- return l.orientation == E || l.orientation == W
-}
-
-func (l *Line) vertical() bool {
- return l.orientation == N || l.orientation == S
-}
-
-func (l *Line) diagonal() bool {
- return l.orientation == NE || l.orientation == SE || l.orientation == SW || l.orientation == NW
-}
-
-// XX drop names 'start' below
-
-// Triangle corresponds to "^", "v", "<" and ">" runes in the absence of
-// surrounding alphanumerics.
-type Triangle struct {
- start Index
- orientation Orientation
- needsNudging bool
-}
-
-// Circle corresponds to "o" or "*" runes in the absence of surrounding
-// alphanumerics.
-type Circle struct {
- start Index
- bold bool
-}
-
-// RoundedCorner corresponds to combinations of "-." or "-'".
-type RoundedCorner struct {
- start Index
- orientation Orientation
-}
-
-// Text corresponds to any runes not reserved for diagrams, or reserved runes
-// surrounded by alphanumerics.
-type Text struct {
- start Index
- str string // Possibly multiple bytes, from Unicode source of type 'rune'
-}
-
-// Bridge corresponds to combinations of "-)-" or "-(-" and is displayed as
-// the vertical line "hopping over" the horizontal.
-type Bridge struct {
- start Index
- orientation Orientation
-}
-
-// Orientation represents the primary direction that a Drawable is facing.
-type Orientation int
-
-const (
- NONE Orientation = iota // No orientation; no structure present.
- N // North
- NE // Northeast
- NW // Northwest
- S // South
- SE // Southeast
- SW // Southwest
- E // East
- W // West
-)
-
-// WriteSVGBody writes the entire content of a Canvas out to a stream in SVG format.
-func (c *Canvas) WriteSVGBody(dst io.Writer) {
- writeBytes(dst, "\n")
-
- for _, l := range c.Lines() {
- l.draw(dst)
- }
-
- for _, tI := range c.Triangles() {
- tI.draw(dst)
- }
-
- for _, c := range c.RoundedCorners() {
- c.draw(dst)
- }
-
- for _, c := range c.Circles() {
- c.draw(dst)
- }
-
- for _, bI := range c.Bridges() {
- bI.draw(dst)
- }
-
- writeText(dst, c)
-
- writeBytes(dst, "\n")
-}
-
-// Lines returns a slice of all Line drawables that we can detect -- in all
-// possible orientations.
-func (c *Canvas) Lines() (lines []Line) {
- horizontalMidlines := c.getLinesForSegment('-')
-
- diagUpLines := c.getLinesForSegment('/')
- for i, l := range diagUpLines {
- // /_
- if c.runeAt(l.start.east()) == '_' {
- diagUpLines[i].needsTinyNudgingLeft = true
- }
-
- // _
- // /
- if c.runeAt(l.stop.north()) == '_' {
- diagUpLines[i].needsTinyNudgingRight = true
- }
-
- // _
- // /
- if !l.lonely && c.runeAt(l.stop.nEast()) == '_' {
- diagUpLines[i].needsTinyNudgingRight = true
- }
-
- // _/
- if !l.lonely && c.runeAt(l.start.west()) == '_' {
- diagUpLines[i].needsTinyNudgingLeft = true
- }
-
- // \
- // /
- if !l.lonely && c.runeAt(l.stop.north()) == '\\' {
- diagUpLines[i].needsTinyNudgingRight = true
- }
-
- // /
- // \
- if !l.lonely && c.runeAt(l.start.south()) == '\\' {
- diagUpLines[i].needsTinyNudgingLeft = true
- }
- }
-
- diagDownLines := c.getLinesForSegment('\\')
- for i, l := range diagDownLines {
- // _\
- if c.runeAt(l.stop.west()) == '_' {
- diagDownLines[i].needsTinyNudgingRight = true
- }
-
- // _
- // \
- if c.runeAt(l.start.north()) == '_' {
- diagDownLines[i].needsTinyNudgingLeft = true
- }
-
- // _
- // \
- if !l.lonely && c.runeAt(l.start.nWest()) == '_' {
- diagDownLines[i].needsTinyNudgingLeft = true
- }
-
- // \_
- if !l.lonely && c.runeAt(l.stop.east()) == '_' {
- diagDownLines[i].needsTinyNudgingRight = true
- }
-
- // \
- // /
- if !l.lonely && c.runeAt(l.stop.south()) == '/' {
- diagDownLines[i].needsTinyNudgingRight = true
- }
-
- // /
- // \
- if !l.lonely && c.runeAt(l.start.north()) == '/' {
- diagDownLines[i].needsTinyNudgingLeft = true
- }
- }
-
- horizontalBaselines := c.getLinesForSegment('_')
- for i, l := range horizontalBaselines {
- // TODO: make this nudge an orientation
- horizontalBaselines[i].needsNudgingDown = true
-
- // _
- // _| |
- if c.runeAt(l.stop.sEast()) == '|' || c.runeAt(l.stop.nEast()) == '|' {
- horizontalBaselines[i].needsNudgingRight = true
- }
-
- // _
- // | _|
- if c.runeAt(l.start.sWest()) == '|' || c.runeAt(l.start.nWest()) == '|' {
- horizontalBaselines[i].needsNudgingLeft = true
- }
-
- // _
- // _/ \
- if c.runeAt(l.stop.east()) == '/' || c.runeAt(l.stop.sEast()) == '\\' {
- horizontalBaselines[i].needsTinyNudgingRight = true
- }
-
- // _
- // \_ /
- if c.runeAt(l.start.west()) == '\\' || c.runeAt(l.start.sWest()) == '/' {
- horizontalBaselines[i].needsTinyNudgingLeft = true
- }
-
- // _\
- if c.runeAt(l.stop.east()) == '\\' {
- horizontalBaselines[i].needsNudgingRight = true
- horizontalBaselines[i].needsTinyNudgingRight = true
- }
-
- //
- // /_
- if c.runeAt(l.start.west()) == '/' {
- horizontalBaselines[i].needsNudgingLeft = true
- horizontalBaselines[i].needsTinyNudgingLeft = true
- }
- // _
- // /
- if c.runeAt(l.stop.south()) == '/' {
- horizontalBaselines[i].needsTinyNudgingRight = true
- }
-
- // _
- // \
- if c.runeAt(l.start.south()) == '\\' {
- horizontalBaselines[i].needsTinyNudgingLeft = true
- }
-
- // _
- // '
- if c.runeAt(l.start.sWest()) == '\'' {
- horizontalBaselines[i].needsNudgingLeft = true
- }
-
- // _
- // '
- if c.runeAt(l.stop.sEast()) == '\'' {
- horizontalBaselines[i].needsNudgingRight = true
- }
- }
-
- verticalLines := c.getLinesForSegment('|')
-
- lines = append(lines, horizontalMidlines...)
- lines = append(lines, horizontalBaselines...)
- lines = append(lines, verticalLines...)
- lines = append(lines, diagUpLines...)
- lines = append(lines, diagDownLines...)
- lines = append(lines, c.HalfSteps()...) // vertical, only
-
- return
-}
-
-func newHalfStep(i Index, chop Orientation) Line {
- return Line{
- start: i,
- stop: i.south(),
- lonely: true,
- chop: chop,
- orientation: S,
- }
-}
-
-func (c *Canvas) HalfSteps() (lines []Line) {
- for idx := range upDown(c.Width, c.Height) {
- if o := c.partOfHalfStep(idx); o != NONE {
- lines = append(
- lines,
- newHalfStep(idx, o),
- )
- }
- }
- return
-}
-
-func (c *Canvas) getLinesForSegment(segment rune) []Line {
- var iter canvasIterator
- var orientation Orientation
- var passThroughs []rune
-
- switch segment {
- case '-':
- iter = leftRight
- orientation = E
- passThroughs = append(jointRunes, '<', '>', '(', ')')
- case '_':
- iter = leftRight
- orientation = E
- passThroughs = append(jointRunes, '|')
- case '|':
- iter = upDown
- orientation = S
- passThroughs = append(jointRunes, '^', 'v')
- case '/':
- iter = diagUp
- orientation = NE
- passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|')
- case '\\':
- iter = diagDown
- orientation = SE
- passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|')
- default:
- return nil
- }
-
- return c.getLines(iter, segment, passThroughs, orientation)
-}
-
-// ci: the order that we traverse locations on the canvas.
-// segment: the primary character we're tracking for this line.
-// passThroughs: characters the line segment is allowed to be drawn underneath
-// (without terminating the line).
-// orientation: the orientation for this line.
-func (c *Canvas) getLines(
- ci canvasIterator,
- segment rune,
- passThroughs []rune,
- o Orientation,
-) (lines []Line) {
- // Helper to throw the current line we're tracking on to the slice and
- // start a new one.
- snip := func(cl Line) Line {
- // Only collect lines that actually go somewhere or are isolated
- // segments; otherwise, discard what's been collected so far within 'cl'.
- if cl.goesSomewhere() {
- lines = append(lines, cl)
- }
-
- return Line{orientation: o}
- }
-
- currentLine := Line{orientation: o}
- lastSeenRune := ' '
-
- for idx := range ci(c.Width+1, c.Height+1) {
- r := c.runeAt(idx)
-
- isSegment := r == segment
- isPassThrough := contains(passThroughs, r)
- isRoundedCorner := c.isRoundedCorner(idx)
- isDot := isDot(r)
- isTriangle := isTriangle(r)
-
- justPassedThrough := contains(passThroughs, lastSeenRune)
-
- shouldKeep := (isSegment || isPassThrough) && isRoundedCorner == NONE
-
- // This is an edge case where we have a rounded corner... that's also a
- // joint... attached to orthogonal line, e.g.:
- //
- // '+--
- // |
- //
- // TODO: This also depends on the orientation of the corner and our
- // line.
- // NW / NE line can't go with EW/NS lines, vertical is OK though.
- if isRoundedCorner != NONE && o != E && (c.partOfVerticalLine(idx) || c.partOfDiagonalLine(idx)) {
- shouldKeep = true
- }
-
- // Don't connect | to > for diagonal lines or )) for horizontal lines.
- if isPassThrough && justPassedThrough && o != S {
- currentLine = snip(currentLine)
- }
-
- // Don't connect o to o, + to o, etc. This character is a new pass-through
- // so we still want to respect shouldKeep; we just don't want to draw
- // the existing line through this cell.
- if justPassedThrough && (isDot || isTriangle) {
- currentLine = snip(currentLine)
- }
-
- switch currentLine.state {
- case _Unstarted:
- if shouldKeep {
- c.setStart(¤tLine, idx)
- }
- case _Started:
- if !shouldKeep {
- // Snip the existing line, don't add the current cell to it
- // *unless* its a line segment all by itself. If it is, keep a
- // record that it's an individual segment because we need to
- // adjust later in the / and \ cases.
- if !currentLine.goesSomewhere() && lastSeenRune == segment {
- if !c.partOfRoundedCorner(currentLine.start) {
- c.setStop(¤tLine, idx)
- currentLine.lonely = true
- }
- }
- currentLine = snip(currentLine)
- } else if isPassThrough {
- // Snip the existing line but include the current pass-through
- // character because we may be continuing the line.
- c.setStop(¤tLine, idx)
- currentLine = snip(currentLine)
- c.setStart(¤tLine, idx)
- } else if shouldKeep {
- // Keep the line going and extend it by this character.
- c.setStop(¤tLine, idx)
- }
- }
-
- lastSeenRune = r
- }
- return
-}
-
-// Triangles detects intended triangles -- typically at the end of an intended line --
-// and returns a representational slice composed of types Triangle and Line.
-func (c *Canvas) Triangles() (triangles []Drawable) {
- o := NONE
-
- for idx := range upDown(c.Width, c.Height) {
- needsNudging := false
- start := idx
-
- r := c.runeAt(idx)
-
- if !isTriangle(r) {
- continue
- }
-
- // Identify orientation and nudge the triangle to touch any
- // adjacent walls.
- switch r {
- case '^':
- o = N
- // ^ and ^
- // / \
- if c.runeAt(start.sWest()) == '/' {
- o = NE
- } else if c.runeAt(start.sEast()) == '\\' {
- o = NW
- }
- case 'v':
- if c.runeAt(start.north()) == '|' {
- // |
- // v
- o = S
- } else if c.runeAt(start.nEast()) == '/' {
- // /
- // v
- o = SW
- } else if c.runeAt(start.nWest()) == '\\' {
- // \
- // v
- o = SE
- } else {
- // Conclusion: Meant as a text string 'v', not a triangle
- //panic("Not sufficient to fix all 'v' troubles.")
- // continue XX Already committed to non-text output for this string?
- o = S
- }
- case '<':
- o = W
- case '>':
- o = E
- }
-
- // Determine if we need to snap the triangle to something and, if so,
- // draw a tail if we need to.
- switch o {
- case N:
- r := c.runeAt(start.north())
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(triangles, newHalfStep(start, N))
- }
- case NW:
- r := c.runeAt(start.nWest())
- // Need to draw a tail.
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(
- triangles,
- Line{
- start: start.nWest(),
- stop: start,
- orientation: SE,
- },
- )
- }
- case NE:
- r := c.runeAt(start.nEast())
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(
- triangles,
- Line{
- start: start,
- stop: start.nEast(),
- orientation: NE,
- },
- )
- }
- case S:
- r := c.runeAt(start.south())
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(triangles, newHalfStep(start, S))
- }
- case SE:
- r := c.runeAt(start.sEast())
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(
- triangles,
- Line{
- start: start,
- stop: start.sEast(),
- orientation: SE,
- },
- )
- }
- case SW:
- r := c.runeAt(start.sWest())
- if r == '-' || isJoint(r) && !isDot(r) {
- needsNudging = true
- triangles = append(
- triangles,
- Line{
- start: start.sWest(),
- stop: start,
- orientation: NE,
- },
- )
- }
- case W:
- r := c.runeAt(start.west())
- if isDot(r) {
- needsNudging = true
- }
- case E:
- r := c.runeAt(start.east())
- if isDot(r) {
- needsNudging = true
- }
- }
-
- triangles = append(
- triangles,
- Triangle{
- start: start,
- orientation: o,
- needsNudging: needsNudging,
- },
- )
- }
- return
-}
-
-// Circles returns a slice of all 'o' and '*' characters not considered text.
-func (c *Canvas) Circles() (circles []Circle) {
- for idx := range upDown(c.Width, c.Height) {
- // TODO INCOMING
- if c.runeAt(idx) == 'o' {
- circles = append(circles, Circle{start: idx})
- } else if c.runeAt(idx) == '*' {
- circles = append(circles, Circle{start: idx, bold: true})
- }
- }
- return
-}
-
-// RoundedCorners returns a slice of all curvy corners in the diagram.
-func (c *Canvas) RoundedCorners() (corners []RoundedCorner) {
- for idx := range leftRight(c.Width, c.Height) {
- if o := c.isRoundedCorner(idx); o != NONE {
- corners = append(
- corners,
- RoundedCorner{start: idx, orientation: o},
- )
- }
- }
- return
-}
-
-// For . and ' characters this will return a non-NONE orientation if the
-// character falls on a rounded corner.
-func (c *Canvas) isRoundedCorner(i Index) Orientation {
- r := c.runeAt(i)
-
- if !isJoint(r) {
- return NONE
- }
-
- left := i.west()
- right := i.east()
- lowerLeft := i.sWest()
- lowerRight := i.sEast()
- upperLeft := i.nWest()
- upperRight := i.nEast()
-
- opensUp := r == '\'' || r == '+'
- opensDown := r == '.' || r == '+'
-
- dashRight := c.runeAt(right) == '-' || c.runeAt(right) == '+' || c.runeAt(right) == '_' || c.runeAt(upperRight) == '_'
- dashLeft := c.runeAt(left) == '-' || c.runeAt(left) == '+' || c.runeAt(left) == '_' || c.runeAt(upperLeft) == '_'
-
- isVerticalSegment := func(i Index) bool {
- r := c.runeAt(i)
- return r == '|' || r == '+' || r == ')' || r == '(' || isDot(r)
- }
-
- // .- or .-
- // | +
- if opensDown && dashRight && isVerticalSegment(lowerLeft) {
- return NW
- }
-
- // -. or -. or -. or _. or -.
- // | + ) ) o
- if opensDown && dashLeft && isVerticalSegment(lowerRight) {
- return NE
- }
-
- // | or + or | or + or + or_ )
- // -' -' +' +' ++ '
- if opensUp && dashLeft && isVerticalSegment(upperRight) {
- return SE
- }
-
- // | or +
- // '- '-
- if opensUp && dashRight && isVerticalSegment(upperLeft) {
- return SW
- }
-
- return NONE
-}
-
-// Text returns a slice of all text characters not belonging to part of the diagram.
-// Must be stably sorted, to satisfy regression tests.
-func (c *Canvas) Text() (text []Text) {
- for idx := range leftRight(c.Width, c.Height) {
- r, found := c.text[idx]
- if !found {
- continue
- }
- text = append(text, Text{
- start: idx,
- str: string(r)})
- }
- return
-}
-
-// Bridges returns a slice of all bridges, "-)-" or "-(-", composed as a sequence of
-// either type Bridge or type Line.
-func (c *Canvas) Bridges() (bridges []Drawable) {
- for idx := range leftRight(c.Width, c.Height) {
- if o := c.isBridge(idx); o != NONE {
- bridges = append(
- bridges,
- newHalfStep(idx.north(), S),
- newHalfStep(idx.south(), N),
- Bridge{
- start: idx,
- orientation: o,
- },
- )
- }
- }
- return
-}
-
-// -)- or -(- or
-func (c *Canvas) isBridge(i Index) Orientation {
- r := c.runeAt(i)
-
- left := c.runeAt(i.west())
- right := c.runeAt(i.east())
-
- if left != '-' || right != '-' {
- return NONE
- }
-
- if r == '(' {
- return W
- }
-
- if r == ')' {
- return E
- }
-
- return NONE
-}
func (c *Canvas) shouldMoveToText(i Index) bool {
i_r := c.runeAt(i)
diff --git a/clean.sh b/clean.sh
new file mode 100755
index 0000000..53cdabf
--- /dev/null
+++ b/clean.sh
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+rm -f examples/*.svg *.svg README.md
diff --git a/drawable.go b/drawable.go
new file mode 100644
index 0000000..c76912b
--- /dev/null
+++ b/drawable.go
@@ -0,0 +1,762 @@
+package goat
+
+import (
+ "io"
+)
+
+// Drawable represents anything that can Draw itself.
+type Drawable interface {
+ draw(out io.Writer)
+}
+
+// Line represents a straight segment between two points 'start' and 'stop', where
+// 'start' is either lesser in X (north-east, east, south-east), or
+// equal in X and lesser in Y (south).
+type Line struct {
+ start Index
+ stop Index
+
+ startRune rune
+ stopRune rune
+
+ // dashed bool
+ needsNudgingDown bool
+ needsNudgingLeft bool
+ needsNudgingRight bool
+ needsTinyNudgingLeft bool
+ needsTinyNudgingRight bool
+
+ // This is a line segment all by itself. This centers the segment around
+ // the midline.
+ lonely bool
+ // N or S. Only useful for half steps - chops of this half of the line.
+ chop Orientation
+
+ // X-major, Y-minor. Therefore, always one of the compass points NE, E, SE, S.
+ orientation Orientation
+
+ state lineState
+}
+
+type lineState int
+
+const (
+ _Unstarted lineState = iota
+ _Started
+)
+
+func (l *Line) started() bool {
+ return l.state == _Started
+}
+
+func (c *Canvas) setStart(l *Line, i Index) {
+ if l.state == _Unstarted {
+ l.start = i
+ l.startRune = c.runeAt(i)
+ l.stop = i
+ l.stopRune = c.runeAt(i)
+ l.state = _Started
+ }
+}
+
+func (c *Canvas) setStop(l *Line, i Index) {
+ if l.state == _Started {
+ l.stop = i
+ l.stopRune = c.runeAt(i)
+ }
+}
+
+func (l *Line) goesSomewhere() bool {
+ return l.start != l.stop
+}
+
+func (l *Line) horizontal() bool {
+ return l.orientation == E || l.orientation == W
+}
+
+func (l *Line) vertical() bool {
+ return l.orientation == N || l.orientation == S
+}
+
+func (l *Line) diagonal() bool {
+ return l.orientation == NE || l.orientation == SE || l.orientation == SW || l.orientation == NW
+}
+
+// XX drop names 'start' below
+
+// Triangle corresponds to "^", "v", "<" and ">" runes in the absence of
+// surrounding alphanumerics.
+type Triangle struct {
+ start Index
+ orientation Orientation
+ needsNudging bool
+}
+
+// Circle corresponds to "o" or "*" runes in the absence of surrounding
+// alphanumerics.
+type Circle struct {
+ start Index
+ bold bool
+}
+
+// RoundedCorner corresponds to combinations of "-." or "-'".
+type RoundedCorner struct {
+ start Index
+ orientation Orientation
+}
+
+// Text corresponds to any runes not reserved for diagrams, or reserved runes
+// surrounded by alphanumerics.
+type Text struct {
+ start Index
+ str string // Possibly multiple bytes, from Unicode source of type 'rune'
+}
+
+// Bridge corresponds to combinations of "-)-" or "-(-" and is displayed as
+// the vertical line "hopping over" the horizontal.
+type Bridge struct {
+ start Index
+ orientation Orientation
+}
+
+// Orientation represents the primary direction that a Drawable is facing.
+type Orientation int
+
+const (
+ NONE Orientation = iota // No orientation; no structure present.
+ N // North
+ NE // Northeast
+ NW // Northwest
+ S // South
+ SE // Southeast
+ SW // Southwest
+ E // East
+ W // West
+)
+
+// WriteSVGBody writes the entire content of a Canvas out to a stream in SVG format.
+func (c *Canvas) WriteSVGBody(dst io.Writer) {
+ writeBytes(dst, "\n")
+
+ for _, l := range c.Lines() {
+ l.draw(dst)
+ }
+
+ for _, tI := range c.Triangles() {
+ tI.draw(dst)
+ }
+
+ for _, c := range c.RoundedCorners() {
+ c.draw(dst)
+ }
+
+ for _, c := range c.Circles() {
+ c.draw(dst)
+ }
+
+ for _, bI := range c.Bridges() {
+ bI.draw(dst)
+ }
+
+ writeText(dst, c)
+
+ writeBytes(dst, "\n")
+}
+
+// Lines returns a slice of all Line drawables that we can detect -- in all
+// possible orientations.
+func (c *Canvas) Lines() (lines []Line) {
+ horizontalMidlines := c.getLinesForSegment('-')
+
+ diagUpLines := c.getLinesForSegment('/')
+ for i, l := range diagUpLines {
+ // /_
+ if c.runeAt(l.start.east()) == '_' {
+ diagUpLines[i].needsTinyNudgingLeft = true
+ }
+
+ // _
+ // /
+ if c.runeAt(l.stop.north()) == '_' {
+ diagUpLines[i].needsTinyNudgingRight = true
+ }
+
+ // _
+ // /
+ if !l.lonely && c.runeAt(l.stop.nEast()) == '_' {
+ diagUpLines[i].needsTinyNudgingRight = true
+ }
+
+ // _/
+ if !l.lonely && c.runeAt(l.start.west()) == '_' {
+ diagUpLines[i].needsTinyNudgingLeft = true
+ }
+
+ // \
+ // /
+ if !l.lonely && c.runeAt(l.stop.north()) == '\\' {
+ diagUpLines[i].needsTinyNudgingRight = true
+ }
+
+ // /
+ // \
+ if !l.lonely && c.runeAt(l.start.south()) == '\\' {
+ diagUpLines[i].needsTinyNudgingLeft = true
+ }
+ }
+
+ diagDownLines := c.getLinesForSegment('\\')
+ for i, l := range diagDownLines {
+ // _\
+ if c.runeAt(l.stop.west()) == '_' {
+ diagDownLines[i].needsTinyNudgingRight = true
+ }
+
+ // _
+ // \
+ if c.runeAt(l.start.north()) == '_' {
+ diagDownLines[i].needsTinyNudgingLeft = true
+ }
+
+ // _
+ // \
+ if !l.lonely && c.runeAt(l.start.nWest()) == '_' {
+ diagDownLines[i].needsTinyNudgingLeft = true
+ }
+
+ // \_
+ if !l.lonely && c.runeAt(l.stop.east()) == '_' {
+ diagDownLines[i].needsTinyNudgingRight = true
+ }
+
+ // \
+ // /
+ if !l.lonely && c.runeAt(l.stop.south()) == '/' {
+ diagDownLines[i].needsTinyNudgingRight = true
+ }
+
+ // /
+ // \
+ if !l.lonely && c.runeAt(l.start.north()) == '/' {
+ diagDownLines[i].needsTinyNudgingLeft = true
+ }
+ }
+
+ horizontalBaselines := c.getLinesForSegment('_')
+ for i, l := range horizontalBaselines {
+ // TODO: make this nudge an orientation
+ horizontalBaselines[i].needsNudgingDown = true
+
+ // _
+ // _| |
+ if c.runeAt(l.stop.sEast()) == '|' || c.runeAt(l.stop.nEast()) == '|' {
+ horizontalBaselines[i].needsNudgingRight = true
+ }
+
+ // _
+ // | _|
+ if c.runeAt(l.start.sWest()) == '|' || c.runeAt(l.start.nWest()) == '|' {
+ horizontalBaselines[i].needsNudgingLeft = true
+ }
+
+ // _
+ // _/ \
+ if c.runeAt(l.stop.east()) == '/' || c.runeAt(l.stop.sEast()) == '\\' {
+ horizontalBaselines[i].needsTinyNudgingRight = true
+ }
+
+ // _
+ // \_ /
+ if c.runeAt(l.start.west()) == '\\' || c.runeAt(l.start.sWest()) == '/' {
+ horizontalBaselines[i].needsTinyNudgingLeft = true
+ }
+
+ // _\
+ if c.runeAt(l.stop.east()) == '\\' {
+ horizontalBaselines[i].needsNudgingRight = true
+ horizontalBaselines[i].needsTinyNudgingRight = true
+ }
+
+ //
+ // /_
+ if c.runeAt(l.start.west()) == '/' {
+ horizontalBaselines[i].needsNudgingLeft = true
+ horizontalBaselines[i].needsTinyNudgingLeft = true
+ }
+ // _
+ // /
+ if c.runeAt(l.stop.south()) == '/' {
+ horizontalBaselines[i].needsTinyNudgingRight = true
+ }
+
+ // _
+ // \
+ if c.runeAt(l.start.south()) == '\\' {
+ horizontalBaselines[i].needsTinyNudgingLeft = true
+ }
+
+ // _
+ // '
+ if c.runeAt(l.start.sWest()) == '\'' {
+ horizontalBaselines[i].needsNudgingLeft = true
+ }
+
+ // _
+ // '
+ if c.runeAt(l.stop.sEast()) == '\'' {
+ horizontalBaselines[i].needsNudgingRight = true
+ }
+ }
+
+ verticalLines := c.getLinesForSegment('|')
+
+ lines = append(lines, horizontalMidlines...)
+ lines = append(lines, horizontalBaselines...)
+ lines = append(lines, verticalLines...)
+ lines = append(lines, diagUpLines...)
+ lines = append(lines, diagDownLines...)
+ lines = append(lines, c.HalfSteps()...) // vertical, only
+
+ return
+}
+
+func newHalfStep(i Index, chop Orientation) Line {
+ return Line{
+ start: i,
+ stop: i.south(),
+ lonely: true,
+ chop: chop,
+ orientation: S,
+ }
+}
+
+func (c *Canvas) HalfSteps() (lines []Line) {
+ for idx := range upDown(c.Width, c.Height) {
+ if o := c.partOfHalfStep(idx); o != NONE {
+ lines = append(
+ lines,
+ newHalfStep(idx, o),
+ )
+ }
+ }
+ return
+}
+
+func (c *Canvas) getLinesForSegment(segment rune) []Line {
+ var iter canvasIterator
+ var orientation Orientation
+ var passThroughs []rune
+
+ switch segment {
+ case '-':
+ iter = leftRight
+ orientation = E
+ passThroughs = append(jointRunes, '<', '>', '(', ')')
+ case '_':
+ iter = leftRight
+ orientation = E
+ passThroughs = append(jointRunes, '|')
+ case '|':
+ iter = upDown
+ orientation = S
+ passThroughs = append(jointRunes, '^', 'v')
+ case '/':
+ iter = diagUp
+ orientation = NE
+ passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|')
+ case '\\':
+ iter = diagDown
+ orientation = SE
+ passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|')
+ default:
+ return nil
+ }
+
+ return c.getLines(segment, iter, orientation, passThroughs)
+}
+
+// segment: the primary character expected along a continuing Line
+// ci: the order that the loop below traverse locations on the canvas.
+// orientation: the orientation for this line.
+// passThroughs: characters that will produce a mark that the line segment
+// is allowed to be drawn either through or, in the case of 'o', "underneath" --
+// without terminating the line.
+func (c *Canvas) getLines(
+ segment rune,
+ ci canvasIterator,
+ o Orientation,
+ passThroughs []rune,
+) (lines []Line) {
+ // Helper to throw the current line we're tracking on to the slice and
+ // start a new one.
+ snip := func(cl Line) Line {
+ // Only collect lines that actually go somewhere or are isolated
+ // segments; otherwise, discard what's been collected so far within 'cl'.
+ if cl.goesSomewhere() {
+ lines = append(lines, cl)
+ }
+
+ return Line{orientation: o}
+ }
+
+ currentLine := Line{orientation: o}
+ lastSeenRune := ' '
+
+ // X Purpose of the '+1' overscan is to reset lastSeenRune to ' ' upon wrapping the minor axis.
+ for idx := range ci(c.Width+1, c.Height+1) {
+ r := c.runeAt(idx)
+
+ isSegment := r == segment
+ isPassThrough := contains(passThroughs, r)
+ isRoundedCorner := c.isRoundedCorner(idx)
+ isDot := isDot(r)
+ isTriangle := isTriangle(r)
+
+ justPassedThrough := contains(passThroughs, lastSeenRune)
+
+ shouldKeep := (isSegment || isPassThrough) && isRoundedCorner == NONE
+
+ // This is an edge case where we have a rounded corner... that's also a
+ // joint... attached to orthogonal line, e.g.:
+ //
+ // '+--
+ // |
+ //
+ // TODO: This also depends on the orientation of the corner and our
+ // line.
+ // NW / NE line can't go with EW/NS lines, vertical is OK though.
+ if isRoundedCorner != NONE && o != E && (c.partOfVerticalLine(idx) || c.partOfDiagonalLine(idx)) {
+ shouldKeep = true
+ }
+
+ // Don't connect | to > for diagonal lines or )) for horizontal lines.
+ if isPassThrough && justPassedThrough && o != S {
+ currentLine = snip(currentLine)
+ }
+
+ // Don't connect o to o, + to o, etc. This character is a new pass-through
+ // so we still want to respect shouldKeep; we just don't want to draw
+ // the existing line through this cell.
+ if justPassedThrough && (isDot || isTriangle) {
+ currentLine = snip(currentLine)
+ }
+
+ if o == S && (r == '.' || lastSeenRune == '\'') {
+ currentLine = snip(currentLine)
+ }
+
+ switch currentLine.state {
+ case _Unstarted:
+ if shouldKeep {
+ c.setStart(¤tLine, idx)
+ }
+ case _Started:
+ if !shouldKeep {
+ // Snip the existing line, don't add the current cell to it
+ // *unless* its a line segment all by itself. If it is, keep a
+ // record that it's an individual segment because we need to
+ // adjust later in the / and \ cases.
+ if !currentLine.goesSomewhere() && lastSeenRune == segment {
+ if !c.partOfRoundedCorner(currentLine.start) {
+ c.setStop(¤tLine, idx)
+ currentLine.lonely = true
+ }
+ }
+ currentLine = snip(currentLine)
+ } else if isPassThrough {
+ // Snip the existing line but include the current pass-through
+ // character because we may be continuing the line.
+ c.setStop(¤tLine, idx)
+ currentLine = snip(currentLine)
+ c.setStart(¤tLine, idx)
+ } else if shouldKeep {
+ // Keep the line going and extend it by this character.
+ c.setStop(¤tLine, idx)
+ }
+ }
+
+ lastSeenRune = r
+ }
+ return
+}
+
+// Triangles detects intended triangles -- typically at the end of an intended line --
+// and returns a representational slice composed of types Triangle and Line.
+func (c *Canvas) Triangles() (triangles []Drawable) {
+ o := NONE
+
+ for idx := range upDown(c.Width, c.Height) {
+ needsNudging := false
+ start := idx
+
+ r := c.runeAt(idx)
+
+ if !isTriangle(r) {
+ continue
+ }
+
+ // Identify orientation and nudge the triangle to touch any
+ // adjacent walls.
+ switch r {
+ case '^':
+ o = N
+ // ^ and ^
+ // / \
+ if c.runeAt(start.sWest()) == '/' {
+ o = NE
+ } else if c.runeAt(start.sEast()) == '\\' {
+ o = NW
+ }
+ case 'v':
+ if c.runeAt(start.north()) == '|' {
+ // |
+ // v
+ o = S
+ } else if c.runeAt(start.nEast()) == '/' {
+ // /
+ // v
+ o = SW
+ } else if c.runeAt(start.nWest()) == '\\' {
+ // \
+ // v
+ o = SE
+ } else {
+ // Conclusion: Meant as a text string 'v', not a triangle
+ //panic("Not sufficient to fix all 'v' troubles.")
+ // continue XX Already committed to non-text output for this string?
+ o = S
+ }
+ case '<':
+ o = W
+ case '>':
+ o = E
+ }
+
+ // Determine if we need to snap the triangle to something and, if so,
+ // draw a tail if we need to.
+ switch o {
+ case N:
+ r := c.runeAt(start.north())
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(triangles, newHalfStep(start, N))
+ }
+ case NW:
+ r := c.runeAt(start.nWest())
+ // Need to draw a tail.
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(
+ triangles,
+ Line{
+ start: start.nWest(),
+ stop: start,
+ orientation: SE,
+ },
+ )
+ }
+ case NE:
+ r := c.runeAt(start.nEast())
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(
+ triangles,
+ Line{
+ start: start,
+ stop: start.nEast(),
+ orientation: NE,
+ },
+ )
+ }
+ case S:
+ r := c.runeAt(start.south())
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(triangles, newHalfStep(start, S))
+ }
+ case SE:
+ r := c.runeAt(start.sEast())
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(
+ triangles,
+ Line{
+ start: start,
+ stop: start.sEast(),
+ orientation: SE,
+ },
+ )
+ }
+ case SW:
+ r := c.runeAt(start.sWest())
+ if r == '-' || isJoint(r) && !isDot(r) {
+ needsNudging = true
+ triangles = append(
+ triangles,
+ Line{
+ start: start.sWest(),
+ stop: start,
+ orientation: NE,
+ },
+ )
+ }
+ case W:
+ r := c.runeAt(start.west())
+ if isDot(r) {
+ needsNudging = true
+ }
+ case E:
+ r := c.runeAt(start.east())
+ if isDot(r) {
+ needsNudging = true
+ }
+ }
+
+ triangles = append(
+ triangles,
+ Triangle{
+ start: start,
+ orientation: o,
+ needsNudging: needsNudging,
+ },
+ )
+ }
+ return
+}
+
+// Circles returns a slice of all 'o' and '*' characters not considered text.
+func (c *Canvas) Circles() (circles []Circle) {
+ for idx := range upDown(c.Width, c.Height) {
+ // TODO INCOMING
+ if c.runeAt(idx) == 'o' {
+ circles = append(circles, Circle{start: idx})
+ } else if c.runeAt(idx) == '*' {
+ circles = append(circles, Circle{start: idx, bold: true})
+ }
+ }
+ return
+}
+
+// RoundedCorners returns a slice of all curvy corners in the diagram.
+func (c *Canvas) RoundedCorners() (corners []RoundedCorner) {
+ for idx := range leftRight(c.Width, c.Height) {
+ if o := c.isRoundedCorner(idx); o != NONE {
+ corners = append(
+ corners,
+ RoundedCorner{start: idx, orientation: o},
+ )
+ }
+ }
+ return
+}
+
+// For . and ' characters this will return a non-NONE orientation if the
+// character falls on a rounded corner.
+func (c *Canvas) isRoundedCorner(i Index) Orientation {
+ r := c.runeAt(i)
+
+ if !isJoint(r) {
+ return NONE
+ }
+
+ left := i.west()
+ right := i.east()
+ lowerLeft := i.sWest()
+ lowerRight := i.sEast()
+ upperLeft := i.nWest()
+ upperRight := i.nEast()
+
+ opensUp := r == '\'' || r == '+'
+ opensDown := r == '.' || r == '+'
+
+ dashRight := c.runeAt(right) == '-' || c.runeAt(right) == '+' || c.runeAt(right) == '_' || c.runeAt(upperRight) == '_'
+ dashLeft := c.runeAt(left) == '-' || c.runeAt(left) == '+' || c.runeAt(left) == '_' || c.runeAt(upperLeft) == '_'
+
+ isVerticalSegment := func(i Index) bool {
+ r := c.runeAt(i)
+ return r == '|' || r == '+' || r == ')' || r == '(' || isDot(r)
+ }
+
+ // .- or .-
+ // | +
+ if opensDown && dashRight && isVerticalSegment(lowerLeft) {
+ return NW
+ }
+
+ // -. or -. or -. or _. or -.
+ // | + ) ) o
+ if opensDown && dashLeft && isVerticalSegment(lowerRight) {
+ return NE
+ }
+
+ // | or + or | or + or + or_ )
+ // -' -' +' +' ++ '
+ if opensUp && dashLeft && isVerticalSegment(upperRight) {
+ return SE
+ }
+
+ // | or +
+ // '- '-
+ if opensUp && dashRight && isVerticalSegment(upperLeft) {
+ return SW
+ }
+
+ return NONE
+}
+
+// Text returns a slice of all text characters not belonging to part of the diagram.
+// Must be stably sorted, to satisfy regression tests.
+func (c *Canvas) Text() (text []Text) {
+ for idx := range leftRight(c.Width, c.Height) {
+ r, found := c.text[idx]
+ if !found {
+ continue
+ }
+ text = append(text, Text{
+ start: idx,
+ str: string(r)})
+ }
+ return
+}
+
+// Bridges returns a slice of all bridges, "-)-" or "-(-", composed as a sequence of
+// either type Bridge or type Line.
+func (c *Canvas) Bridges() (bridges []Drawable) {
+ for idx := range leftRight(c.Width, c.Height) {
+ if o := c.isBridge(idx); o != NONE {
+ bridges = append(
+ bridges,
+ newHalfStep(idx.north(), S),
+ newHalfStep(idx.south(), N),
+ Bridge{
+ start: idx,
+ orientation: o,
+ },
+ )
+ }
+ }
+ return
+}
+
+// -)- or -(- or
+func (c *Canvas) isBridge(i Index) Orientation {
+ r := c.runeAt(i)
+
+ left := c.runeAt(i.west())
+ right := c.runeAt(i.east())
+
+ if left != '-' || right != '-' {
+ return NONE
+ }
+
+ if r == '(' {
+ return W
+ }
+
+ if r == ')' {
+ return E
+ }
+
+ return NONE
+}
+
diff --git a/examples-regression_test.go b/examples-regression_test.go
new file mode 100644
index 0000000..2eaff49
--- /dev/null
+++ b/examples-regression_test.go
@@ -0,0 +1,205 @@
+package goat
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ //"fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "text/template"
+)
+
+const (
+ examplesDir = "examples"
+)
+
+var (
+ write = flag.Bool("write",
+ false, "write reference SVG output files")
+ svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000",
+ `See help for cmd/goat`)
+ svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF",
+ `See help for cmd/goat`)
+
+ // Begin the directory name with '_' to hide from git.
+ svgDeltaDir = flag.String("svg-delta-dir", "_examples_new",
+ `Directory to be filled with a delta-image file for each
+newly-generated SVG that does not match those in ` + examplesDir)
+)
+
+func TestExamples(t *testing.T) {
+ // XX This sweeps up ~every~ *.txt file in examples/
+ txtPaths, err := filepath.Glob(filepath.Join(examplesDir, "*.txt"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ baseNames := make([]string, len(txtPaths))
+ for i := range txtPaths {
+ baseName, found := strings.CutPrefix(txtPaths[i], examplesDir+"/")
+ if !found {
+ panic("Could not cut prefix from pathname.")
+ }
+ baseNames[i] = baseName
+ }
+
+ if *write {
+ writeExamples(examplesDir, examplesDir, baseNames, *svgColorLightScheme, *svgColorDarkScheme)
+ } else {
+ t.Logf("Verifying equality of current SVG with examples/ references.\n")
+ verifyExamples(t, examplesDir, baseNames)
+ }
+}
+
+
+func writeExamples(inDir, outDir string, baseNames []string, lightColor, darkColor string) {
+ for _, name := range baseNames {
+ in := getIn(inDir + "/" + name)
+ out := getOut(outDir + "/" + name)
+ BuildAndWriteSVG(in, out, lightColor, darkColor)
+ in.Close()
+ out.Close()
+ }
+}
+
+func verifyExamples(t *testing.T, examplesDir string, baseNames []string) {
+ var failures []string
+ for _, name := range baseNames {
+ in := getIn(examplesDir + "/" + name)
+ buff := &bytes.Buffer{}
+ BuildAndWriteSVG(in, buff, *svgColorLightScheme, *svgColorDarkScheme)
+ in.Close()
+ if nil != compareSVG(t, buff, examplesDir, name) {
+ failures = append(failures, name)
+ }
+
+ }
+ if len(failures) > 0 {
+ t.Logf(`Failed to verify contents of %d .svg files`,
+ len(failures))
+ err := os.Mkdir(*svgDeltaDir, 0770)
+ if err != nil {
+ t.Fatalf(`
+ Aborting: "%v"`, err)
+ }
+ writeExamples(examplesDir, *svgDeltaDir, failures, "#000088", "#88CCFF")
+ writeDeltaHTML(t, "../" + examplesDir, *svgDeltaDir, failures)
+ t.FailNow()
+ }
+}
+
+func compareSVG(t *testing.T, buff *bytes.Buffer, examplesDir string, baseName string) error {
+ fileName := examplesDir + "/" + baseName
+ golden, err := getOutString(fileName)
+ if err != nil {
+ t.Log(err)
+ }
+ if newStr := buff.String(); newStr != golden {
+ // Skip complaint if the modification timestamp of the .txt file
+ // source is fresher than that of the .svg?
+ // => NO, Any .txt difference might be an editing mistake.
+
+ t.Logf("Content mismatch for %s. Length was %d, expected %d",
+ toSVGFilename(fileName), buff.Len(), len(golden))
+ for i:=0; i
+.blended-images {
+ height: 100%; /* XX How to make equal to pixel bounds of the SVGs? */
+ background-size: contain, contain;
+ background-repeat: no-repeat;
+ background-blend-mode: difference;
+ background-image: url('{{.ExamplesDir}}/{{.SvgBaseName}}'), url('{{.DeltaDir}}/{{.SvgBaseName}}');
+ }
+
+
+
+`))
+ for _, name := range baseNames {
+ htmlOutName := stripSuffix(name) + ".html"
+ t.Logf("\t%s", htmlOutName)
+ htmlOutFile, err := os.Create(deltaDir + "/" + htmlOutName)
+ err = tmpl.Execute(htmlOutFile, map[string]string{
+ "ExamplesDir": examplesDir,
+ "DeltaDir": ".",
+ "SvgBaseName": toSVGFilename(name),
+ })
+ htmlOutFile.Close()
+ if err != nil {
+ panic(err)
+ }
+ }
+}
+
+func getIn(txtFilename string) io.ReadCloser {
+ in, err := os.Open(txtFilename)
+ if err != nil {
+ panic(err)
+ }
+ return in
+}
+
+func getOutExport(pathPrefix, txtBaseName string) io.WriteCloser {
+ svgBaseName := toSVGFilename(txtBaseName)
+ out, err := os.Create(pathPrefix + svgBaseName)
+ if err != nil {
+ panic(err)
+ }
+ return out
+}
+
+func getOut(txtFilename string) io.WriteCloser {
+ out, err := os.Create(toSVGFilename(txtFilename))
+ if err != nil {
+ panic(err)
+ }
+ return out
+}
+
+func getOutString(txtFilename string) (string, error) {
+ b, err := ioutil.ReadFile(toSVGFilename(txtFilename))
+ if err != nil {
+ // XX Simply panic rather than return an error?
+ return "", err
+ }
+ // XX Why are there RETURN characters in contents of the .SVG files?
+ b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
+ return string(b), nil
+}
+
+func toSVGFilename(txtFilename string) string {
+ return strings.TrimSuffix(txtFilename, filepath.Ext(txtFilename)) + ".svg"
+}
+
+func stripSuffix(basename string) string {
+ return strings.Split(basename,".")[0]
+}
diff --git a/examples/arrows.svg b/examples/arrows.svg
index f0fcafa..69f9ce9 100644
--- a/examples/arrows.svg
+++ b/examples/arrows.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/big-grids.svg b/examples/big-grids.svg
index fb32bfd..4a07d1e 100644
--- a/examples/big-grids.svg
+++ b/examples/big-grids.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/big-shapes.svg b/examples/big-shapes.svg
index 4a54725..a7ed0d5 100644
--- a/examples/big-shapes.svg
+++ b/examples/big-shapes.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/block-characters.svg b/examples/block-characters.svg
new file mode 100644
index 0000000..56c67d0
--- /dev/null
+++ b/examples/block-characters.svg
@@ -0,0 +1,55 @@
+
diff --git a/examples/block-characters.txt b/examples/block-characters.txt
new file mode 100644
index 0000000..27a2820
--- /dev/null
+++ b/examples/block-characters.txt
@@ -0,0 +1,3 @@
+Special cases supported by Markdeep:
+
+▉ ▓ ▒ ░
diff --git a/examples/circle.svg b/examples/circle.svg
index 54d61df..09ea07b 100644
--- a/examples/circle.svg
+++ b/examples/circle.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/circuits.svg b/examples/circuits.svg
index e1ccde2..9c9b0a8 100644
--- a/examples/circuits.svg
+++ b/examples/circuits.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/complicated.svg b/examples/complicated.svg
index 5ebca64..0bb438a 100644
--- a/examples/complicated.svg
+++ b/examples/complicated.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
@@ -133,7 +134,6 @@ svg {
-
diff --git a/examples/debug.svg b/examples/debug.svg
new file mode 100644
index 0000000..3184e56
--- /dev/null
+++ b/examples/debug.svg
@@ -0,0 +1,73 @@
+
diff --git a/examples/debug.txt b/examples/debug.txt
new file mode 100644
index 0000000..d9bdf2a
--- /dev/null
+++ b/examples/debug.txt
@@ -0,0 +1,34 @@
+
+ (
+
+ (
+
+ )
+
+ (
+
+ a()
+
+ a()
+
+ ()
+
+ ()
+
+ ||
+ --))--
+ ||
+
+ ||
+ --((--
+ ||
+
+ | |
+--)-)--
+ | |
+
+ | |
+--(-(--
+ | |
+
+o-
diff --git a/examples/dot-grids.svg b/examples/dot-grids.svg
index 9648aaf..6c46cc7 100644
--- a/examples/dot-grids.svg
+++ b/examples/dot-grids.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/edge-cases.svg b/examples/edge-cases.svg
index df810d6..aa8370c 100644
--- a/examples/edge-cases.svg
+++ b/examples/edge-cases.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/flow-chart.svg b/examples/flow-chart.svg
index 4a84530..6f575e7 100644
--- a/examples/flow-chart.svg
+++ b/examples/flow-chart.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/graphics.svg b/examples/graphics.svg
index e2ee3ba..eb36223 100644
--- a/examples/graphics.svg
+++ b/examples/graphics.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/half-step-bugs.svg b/examples/half-step-bugs.svg
new file mode 100644
index 0000000..1b4a124
--- /dev/null
+++ b/examples/half-step-bugs.svg
@@ -0,0 +1,69 @@
+
diff --git a/examples/half-step-bugs.txt b/examples/half-step-bugs.txt
new file mode 100644
index 0000000..1c9abb9
--- /dev/null
+++ b/examples/half-step-bugs.txt
@@ -0,0 +1,12 @@
+
+ _
+|_|
+
+double-draw
+
+
+ _
+| |
+|_|
+
+gap at abutting line ends
diff --git a/examples/hollow-circle.svg b/examples/hollow-circle.svg
new file mode 100644
index 0000000..8e2f285
--- /dev/null
+++ b/examples/hollow-circle.svg
@@ -0,0 +1,150 @@
+
diff --git a/examples/hollow-circle.txt b/examples/hollow-circle.txt
new file mode 100644
index 0000000..f821866
--- /dev/null
+++ b/examples/hollow-circle.txt
@@ -0,0 +1,50 @@
+-o
+ * * o o
+ 0123456
+
+
+
+->o o<-
+
+-> x x <-
+ xxxx
+
+ <--->
+ <--->
+
+ o<--->o
+ o<--->o
+
+
+ <-->
+ <-->
+
+ o<-->o
+ o<-->o
+
+
+ <-> < >
+ <-> < >
+
+ o<->o o< >o
+ o<->o o< >o
+
+
+ <>
+ <>
+
+ o<>o
+ o<>o
+
+
+oo
+^^
+||
+vv
+oo
+
+** --
+^^
+||
+vv
+** --
diff --git a/examples/icons.svg b/examples/icons.svg
index dd32e66..97f002f 100644
--- a/examples/icons.svg
+++ b/examples/icons.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/incompatibilities.svg b/examples/incompatibilities.svg
new file mode 100644
index 0000000..31ceaa3
--- /dev/null
+++ b/examples/incompatibilities.svg
@@ -0,0 +1,355 @@
+
diff --git a/examples/incompatibilities.txt b/examples/incompatibilities.txt
new file mode 100644
index 0000000..a5828bd
--- /dev/null
+++ b/examples/incompatibilities.txt
@@ -0,0 +1,31 @@
+Input TXT patterns supported by MarkDeep, but not by Goat.
+
+
+Hollow circles
+
+ o
+
+ Rendered to SVG as a "hollow" circle, transparent to background.
+ Goat-specific alternative rendering options are available on the command line.
+
+
+Alternative TXT patterns to indicate double-width circles:
+
+ .-.
+ | | Goat and MarkDeep
+ '-'
+
+ +-+
+ + + MarkDeep only
+ +-+
+
+ +-+
+ | | MarkDeep only
+ +-+
+
+
+Parallel arcs
+
+ _..---.
+ __)) )- MarkDeep only
+ ''---'
diff --git a/examples/large-nodes.svg b/examples/large-nodes.svg
index 7b7c174..592c524 100644
--- a/examples/large-nodes.svg
+++ b/examples/large-nodes.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/line-decorations.svg b/examples/line-decorations.svg
index 8275285..3172ecb 100644
--- a/examples/line-decorations.svg
+++ b/examples/line-decorations.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/line-ends.svg b/examples/line-ends.svg
index 5552eb6..85dd638 100644
--- a/examples/line-ends.svg
+++ b/examples/line-ends.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/overlaps.svg b/examples/overlaps.svg
index c28f1aa..2db48fc 100644
--- a/examples/overlaps.svg
+++ b/examples/overlaps.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
diff --git a/examples/radius16circle.svg b/examples/radius16circle.svg
new file mode 100644
index 0000000..1996512
--- /dev/null
+++ b/examples/radius16circle.svg
@@ -0,0 +1,33 @@
+
diff --git a/examples/radius16circle.txt b/examples/radius16circle.txt
new file mode 100644
index 0000000..99747b9
--- /dev/null
+++ b/examples/radius16circle.txt
@@ -0,0 +1,9 @@
+ -+
+ +
+
+ +
+ -+
+
+ +-+
+ + +
+ +-+
diff --git a/examples/regression.svg b/examples/regression.svg
index 5d26ed7..0d716e3 100644
--- a/examples/regression.svg
+++ b/examples/regression.svg
@@ -5,6 +5,7 @@ svg {
}
@media (prefers-color-scheme: dark) {
svg {
+ color-scheme: dark; /* ask the browser for a dark background */
color: #FFFFFF;
}
}
@@ -104,7 +105,7 @@ svg {
-
+
diff --git a/examples/small-grids.svg b/examples/small-grids.svg
index d652a14..529a066 100644
--- a/examples/small-grids.svg
+++ b/examples/small-grids.svg
@@ -1,167 +1,313 @@
-