pax_global_header 0000666 0000000 0000000 00000000064 14205205167 0014514 g ustar 00root root 0000000 0000000 52 comment=cc97a132eb5e894f8c4615d04029e4f4d071561c
goat-0.5.0/ 0000775 0000000 0000000 00000000000 14205205167 0012450 5 ustar 00root root 0000000 0000000 goat-0.5.0/.github/ 0000775 0000000 0000000 00000000000 14205205167 0014010 5 ustar 00root root 0000000 0000000 goat-0.5.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14205205167 0016045 5 ustar 00root root 0000000 0000000 goat-0.5.0/.github/workflows/test.yml 0000664 0000000 0000000 00000000720 14205205167 0017546 0 ustar 00root root 0000000 0000000 name: Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
matrix:
go-version: [1.17.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test -race -v .
goat-0.5.0/.gitignore 0000664 0000000 0000000 00000000067 14205205167 0014443 0 ustar 00root root 0000000 0000000 *.swp
.DS_Store
vendor
examples/*.svg
goat
goat.test
goat-0.5.0/.vscode/ 0000775 0000000 0000000 00000000000 14205205167 0014011 5 ustar 00root root 0000000 0000000 goat-0.5.0/.vscode/settings.json 0000664 0000000 0000000 00000000046 14205205167 0016544 0 ustar 00root root 0000000 0000000 {
"autoHide.autoHidePanel": false
}
goat-0.5.0/LICENSE 0000664 0000000 0000000 00000002066 14205205167 0013461 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2016 Bryce Lampe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
goat-0.5.0/README.md 0000664 0000000 0000000 00000021104 14205205167 0013725 0 ustar 00root root 0000000 0000000 # GoAT: Go ASCII Tool
This is a Go implementation of [markdeep.mini.js]'s ASCII diagram
generation.
## Usage
```bash
$ go get github.com/bep/goat
$ goat my-cool-diagram.txt > my-cool-diagram.svg
```
## TODO
- Dashed lines signaled by `:` or `=`.
- Bold lines signaled by ???.
## Examples
Here are some SVGs and the ASCII input they were generated from:
### Trees

```
. . . .--- 1 .-- 1 / 1
/ \ | | .---+ .-+ +
/ \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2
+ + | | | | ---+ ---+ +
/ \ / \ .-+-. .-+-. .+. .+. | .--- 3 | .-- 3 \ / 3
/ \ / \ | | | | | | | | '---+ '-+ +
1 2 3 4 1 2 3 4 1 2 3 4 '--- 4 '-- 4 \ 4
```
### Overlaps

```
.-. .-. .-. .-. .-. .-.
| | | | | | | | | | | |
.---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--.
| | | | | | | | | | | | | | | | | |
'---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------'
| | | | | | | | | | | |
'-' '-' '-' '-' '-' '-'
```
### Line Decorations

```
________ o * * .--------------.
*---+--. | | o o | ^ \ / | .----------. |
| | '--* -+- | | v / \ / | | <------. | |
| '-----> .---(---' --->*<--- / .+->*<--o----' | | | | |
<--' ^ ^ | | | | | ^ \ | '--------' | |
\/ *-----' o |<----->| '-----' |__| v '------------' |
/\ *---------------'
```
### Line Ends

```
o--o *--o / / * o o o o o * * * * o o o o * * * * o o o o * * * *
o--* *--* v v ^ ^ | | | | | | | | \ \ \ \ \ \ \ \ / / / / / / / /
o--> *--> * o / / o * v ' o * v ' o * v \ o * v \ o * v / o * v /
o--- *---
^ ^ ^ ^ . . . . ^ ^ ^ ^ \ \ \ \ ^ ^ ^ ^ / / / /
| | * o \ \ * o | | | | | | | | \ \ \ \ \ \ \ \ / / / / / / / /
v v ^ ^ v v ^ ^ o * v ' o * v ' o * v \ o * v \ o * v / o * v /
* o | | * o \ \
<--o <--* <--> <--- ---o ---* ---> ---- *<-- o<-- -->o -->*
```
### Dot Grids

```
o o o o o * * * * * * * o o * o o o * * * o o o · * · · · · · ·
o o o o o * * * * * o o o o * o o o o * * * * * o * * · * * · · · · · ·
o o o o o * * * * * o * o o o o o o o o * * * * * o o o o o · o · · o · · * * ·
o o o o o * * * * * o * o o o o o o o * * * * o * o o · · · · o · · * ·
o o o o o * * * * * * * * * o o o o * * * o * o · · · · · · · *
```
### Large Nodes

```
.---. .-. .-. .-. .-.
| A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 |
'---' '-' '+' '-' | '-'
| ^ | ^
v | v |
.-. .-+-. .-. .-+-. .-. .+. .---.
| 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D |
'-' '---' '-' '---' '-' '-' '---'
```
### Small Grids

```
___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---.
___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ |
/ \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+
\___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ |
/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+
\___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ |
\___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---'
```
### Big Grids

```
.----. .----.
/ \ / \ .-----+-----+-----.
+ +----+ +----. | | | | .-----+-----+-----+-----+
\ / \ / \ | | | | / / / / /
+----+ B +----+ + +-----+-----+-----+ +-----+-----+-----+-----+
/ \ / \ / | | | | / / / / /
+ A +----+ +----+ | | B | | +-----+-----+-----+-----+
\ / \ / \ +-----+-----+-----+ / / A / B / /
'----+ +----+ + | | | | +-----+-----+-----+-----+
\ / \ / | A | | | / / / / /
'----' '----' '-----+-----+-----' '-----+-----+-----+-----+
```
### Complicated

```
+-------------------+ ^ .---.
| A Box |__.--.__ __.--> | .-. | |
| | '--' v | * |<--- | |
+-------------------+ '-' | |
Round *---(-. |
.-----------------. .-------. .----------. .-------. | | |
| Mixed Rounded | | | / Diagonals \ | | | | | |
| & Square Corners | '--. .--' / \ |---+---| '-)-' .--------.
'--+------------+-' .--. | '-------+--------' | | | | / Search /
| | | | '---. | '-------' | '-+------'
|<---------->| | | | v Interior | ^
' <---' '----' .-----------. ---. .--- v |
.------------------. Diag line | .-------. +---. \ / . |
| if (a > b) +---. .--->| | | | | Curved line \ / / \ |
| obj->fcn() | \ / | '-------' |<--' + / \ |
'------------------' '--' '--+--------' .--. .--. | .-. +Done?+-'
.---+-----. | ^ |\ | | /| .--+ | | \ /
| | | Join \|/ | | Curved | \| |/ | | \ | \ /
| | +----> o --o-- '-' Vertical '--' '--' '-- '--' + .---.
<--+---+-----' | /|\ | | 3 |
v not:line 'quotes' .-' '---'
.-. .---+--------. / A || B *bold* | ^
| | | Not a dot | <---+---<-- A dash--is not a line v |
'-' '---------+--' / Nor/is this. ---
```
More examples are available [here](examples).
[markdeep.mini.js]: http://casual-effects.com/markdeep/
goat-0.5.0/canvas.go 0000664 0000000 0000000 00000060737 14205205167 0014267 0 ustar 00root root 0000000 0000000 package goat
import (
"bufio"
"bytes"
"io"
"sort"
)
// Characters where more than one line segment can come together.
var jointRunes = []rune{'.', '\'', '+', '*', 'o'}
var reservedRunes = map[rune]bool{
'-': true,
'_': true,
'|': true,
'v': true,
'^': true,
'>': true,
'<': true,
'o': true,
'*': true,
'+': true,
'.': true,
'\'': true,
'/': true,
'\\': true,
')': true,
'(': true,
' ': true,
}
func contains(in []rune, r rune) bool {
for _, v := range in {
if r == v {
return true
}
}
return false
}
func isJoint(r rune) bool {
return contains(jointRunes, r)
}
func isDot(r rune) bool {
return r == 'o' || r == '*'
}
func isTriangle(r rune) bool {
return r == '^' || r == 'v' || r == '<' || r == '>'
}
// Canvas represents a 2D ASCII rectangle.
type Canvas struct {
Width int
Height int
data map[Index]rune
text map[Index]rune
}
func (c *Canvas) String() string {
var buffer bytes.Buffer
for h := 0; h < c.Height; h++ {
for w := 0; w < c.Width; w++ {
idx := Index{w, h}
// Grab from our text buffer and if nothing's there try the data
// buffer.
r := c.text[idx]
if r == 0 {
r = c.runeAt(idx)
}
_, err := buffer.WriteRune(r)
if err != nil {
continue
}
}
err := buffer.WriteByte('\n')
if err != nil {
continue
}
}
return buffer.String()
}
func (c *Canvas) heightScreen() int {
return c.Height*16 + 8 + 1
}
func (c *Canvas) widthScreen() int {
return (c.Width + 1) * 8
}
func (c *Canvas) runeAt(i Index) rune {
if val, ok := c.data[i]; ok {
return val
}
return ' '
}
// NewCanvas creates a new canvas with contents read from the given io.Reader.
// Content should be newline delimited.
func NewCanvas(in io.Reader) Canvas {
width := 0
height := 0
scanner := bufio.NewScanner(in)
data := make(map[Index]rune)
for scanner.Scan() {
line := scanner.Text()
w := 0
// Can't use index here because it corresponds to unicode offsets
// instead of logical characters.
for _, c := range line {
idx := Index{x: w, y: height}
data[idx] = rune(c)
w++
}
if w > width {
width = w
}
height++
}
text := make(map[Index]rune)
c := Canvas{
Width: width,
Height: height,
data: data,
text: text,
}
// Extract everything we detect as text to make diagram parsing easier.
for idx := range leftRight(width, height) {
if c.isText(idx) {
c.text[idx] = c.runeAt(idx)
}
}
for idx := range c.text {
delete(c.data, idx)
}
return c
}
// Drawable represents anything that can Draw itself.
type Drawable interface {
Draw(out io.Writer)
}
// Line represents a straight segment between two points.
type Line struct {
start Index
stop Index
// 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
orientation Orientation
state lineState
}
type lineState int
const (
_Unstarted lineState = iota
_Started
)
func (l *Line) started() bool {
return l.state == _Started
}
func (l *Line) setStart(i Index) {
if l.state == _Unstarted {
l.start = i
l.stop = i
l.state = _Started
}
}
func (l *Line) setStop(i Index) {
if l.state == _Started {
l.stop = 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
}
// 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
contents string
}
// Bridge correspondes 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
)
func (c *Canvas) WriteSVGBody(dst io.Writer) {
writeBytes(dst, "\n")
for _, l := range c.Lines() {
l.Draw(dst)
}
for _, t := range c.Triangles() {
t.Draw(dst)
}
for _, c := range c.RoundedCorners() {
c.Draw(dst)
}
for _, c := range c.Circles() {
c.Draw(dst)
}
for _, b := range c.Bridges() {
b.Draw(dst)
}
for _, t := range c.Text() {
t.Draw(dst)
}
writeBytes(dst, "\n")
}
// Lines returns a slice of all Line drawables that we can detect -- in all
// possible orientations.
func (c *Canvas) 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('|')
var lines []Line
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()...)
return lines
}
func newHalfStep(i Index, chop Orientation) Line {
return Line{
start: i,
stop: i.south(),
lonely: true,
chop: chop,
orientation: S,
}
}
func (c *Canvas) HalfSteps() []Line {
var lines []Line
for idx := range upDown(c.Width, c.Height) {
if o := c.partOfHalfStep(idx); o != NONE {
lines = append(
lines,
newHalfStep(idx, o),
)
}
}
return lines
}
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,
) []Line {
var lines []Line
// Helper to throw the current line we're tracking on to the slice and
// start a new one.
snip := func(l Line) Line {
// Only collect lines that actually go somewhere or are isolated
// segments.
if l.goesSomewhere() {
lines = append(lines, l)
}
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 {
currentLine.setStart(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) {
currentLine.setStop(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.
currentLine.setStop(idx)
currentLine = snip(currentLine)
currentLine.setStart(idx)
} else if shouldKeep {
// Keep the line going and extend it by this character.
currentLine.setStop(idx)
}
}
lastSeenRune = r
}
return lines
}
// Triangles returns a slice of all detectable Triangles.
func (c *Canvas) Triangles() []Drawable {
var 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 our 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':
o = S
// / and \
// v v
if c.runeAt(start.nEast()) == '/' {
o = SW
} else if c.runeAt(start.nWest()) == '\\' {
o = SE
}
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 triangles
}
// Circles returns a slice of all 'o' and '*' characters not considered text.
func (c *Canvas) Circles() []Circle {
var 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 circles
}
// RoundedCorners returns a slice of all curvy corners in the diagram.
func (c *Canvas) RoundedCorners() []RoundedCorner {
var 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 corners
}
// 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
}
// A wrapper to enable sorting.
type indexRuneDrawable struct {
i Index
r rune
Drawable
}
// Text returns a slice of all text characters not belonging to part of the diagram.
// How these characters are identified is rather complicated.
func (c *Canvas) Text() []Drawable {
newLine := func(i Index, r rune, o Orientation) Drawable {
stop := i
switch o {
case NE:
stop = i.nEast()
case SE:
stop = i.sEast()
}
l := Line{
start: i,
stop: stop,
lonely: true,
orientation: o,
}
return indexRuneDrawable{
Drawable: l,
i: i,
r: r,
}
}
text := make([]Drawable, len(c.text))
var j int
for i, r := range c.text {
switch r {
// Weird unicode edge cases that markdeep handles. These get
// substituted with lines.
case '╱':
text[j] = newLine(i, r, NE)
case '╲':
text[j] = newLine(i, r, SE)
case '╳':
text[j] = newLine(i, r, NE)
default:
text[j] = indexRuneDrawable{Drawable: Text{start: i, contents: string(r)}, i: i, r: r}
}
j++
}
sort.Slice(text, func(i, j int) bool {
ti, tj := text[i].(indexRuneDrawable), text[j].(indexRuneDrawable)
if ti.i.x == tj.i.x {
return ti.i.y < tj.i.y || (ti.i.y == tj.i.y && ti.r < tj.r)
}
return ti.i.x < tj.i.x
})
return text
}
// Bridges returns a slice of all bridges, "-)-" or "-(-".
func (c *Canvas) Bridges() []Drawable {
var 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 bridges
}
// -)- 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) isText(i Index) bool {
// Short circuit, we already saw this index and called it text.
if _, isText := c.text[i]; isText {
return true
}
if c.runeAt(i) == ' ' {
return false
}
if !c.isReserved(i) {
return true
}
// This is a reserved character with an incoming line (e.g., "|") above it,
// so call it non-text.
if c.hasLineAboveOrBelow(i) {
return false
}
// Reserved characters like "o" or "*" with letters sitting next to them
// are probably text.
// TODO: Fix this to count contiguous blocks of text. If we had a bunch of
// reserved characters previously that were counted as text then this
// should be as well, e.g., "A----B".
// We're reserved but surrounded by text and probably part of an existing
// word. Use a hash lookup on the left to preserve chains of
// reserved-but-text characters like "foo----bar".
if _, textLeft := c.text[i.west()]; textLeft || !c.isReserved(i.east()) {
return true
}
w := i.west()
e := i.east()
if !(c.runeAt(w) == ' ' && c.runeAt(e) == ' ') {
return false
}
// Circles surrounded by whitespace shouldn't be shown as text.
if c.runeAt(i) == 'o' || c.runeAt(i) == '*' {
return false
}
// We're surrounded by whitespace + text on either side.
if !c.isReserved(w.west()) || !c.isReserved(e.east()) {
return true
}
return false
}
// Returns true if the character at this index is not reserved for diagrams.
// Characters like "o" need more context (e.g., are other text characters
// nearby) to determine whether they're part of a diagram.
func (c *Canvas) isReserved(i Index) bool {
r := c.runeAt(i)
_, isReserved := reservedRunes[r]
return isReserved
}
// Returns true if it looks like this character belongs to anything besides a
// horizontal line. This is the context we use to determine if a reserved
// character is text or not.
func (c *Canvas) hasLineAboveOrBelow(i Index) bool {
r := c.runeAt(i)
switch r {
case '*', 'o', '+', 'v', '^':
return c.partOfDiagonalLine(i) || c.partOfVerticalLine(i)
case '|':
return c.partOfVerticalLine(i) || c.partOfRoundedCorner(i)
case '/', '\\':
return c.partOfDiagonalLine(i)
case '-':
return c.partOfRoundedCorner(i)
case '(', ')':
return c.partOfVerticalLine(i)
}
return false
}
// Returns true if a "|" segment passes through this index.
func (c *Canvas) partOfVerticalLine(i Index) bool {
this := c.runeAt(i)
north := c.runeAt(i.north())
south := c.runeAt(i.south())
jointAboveMe := this == '|' && isJoint(north)
if north == '|' || jointAboveMe {
return true
}
jointBelowMe := this == '|' && isJoint(south)
if south == '|' || jointBelowMe {
return true
}
return false
}
// Return true if a "--" segment passes through this index.
func (c *Canvas) partOfHorizontalLine(i Index) bool {
return c.runeAt(i.east()) == '-' || c.runeAt(i.west()) == '-'
}
func (c *Canvas) partOfDiagonalLine(i Index) bool {
r := c.runeAt(i)
n := c.runeAt(i.north())
s := c.runeAt(i.south())
nw := c.runeAt(i.nWest())
se := c.runeAt(i.sEast())
ne := c.runeAt(i.nEast())
sw := c.runeAt(i.sWest())
switch r {
// Diagonal segments can be connected to joint or other segments.
case '/':
return ne == r || sw == r || isJoint(ne) || isJoint(sw) || n == '\\' || s == '\\'
case '\\':
return nw == r || se == r || isJoint(nw) || isJoint(se) || n == '/' || s == '/'
// For everything else just check if we have segments next to us.
default:
return nw == '\\' || ne == '/' || sw == '/' || se == '\\'
}
}
// For "-" and "|" characters returns true if they could be part of a rounded
// corner.
func (c *Canvas) partOfRoundedCorner(i Index) bool {
r := c.runeAt(i)
switch r {
case '-':
dotNext := c.runeAt(i.west()) == '.' || c.runeAt(i.east()) == '.'
hyphenNext := c.runeAt(i.west()) == '\'' || c.runeAt(i.east()) == '\''
return dotNext || hyphenNext
case '|':
dotAbove := c.runeAt(i.nWest()) == '.' || c.runeAt(i.nEast()) == '.'
hyphenBelow := c.runeAt(i.sWest()) == '\'' || c.runeAt(i.sEast()) == '\''
return dotAbove || hyphenBelow
}
return false
}
// TODO: Have this take care of all the vertical line nudging.
func (c *Canvas) partOfHalfStep(i Index) Orientation {
r := c.runeAt(i)
if r != '\'' && r != '.' && r != '|' {
return NONE
}
if c.isRoundedCorner(i) != NONE {
return NONE
}
w := c.runeAt(i.west())
e := c.runeAt(i.east())
n := c.runeAt(i.north())
s := c.runeAt(i.south())
nw := c.runeAt(i.nWest())
ne := c.runeAt(i.nEast())
switch r {
case '\'':
// _ _
// '- -'
if (nw == '_' && e == '-') || (w == '-' && ne == '_') {
return N
}
case '.':
// _.- -._
if (w == '-' && e == '_') || (w == '_' && e == '-') {
return S
}
case '|':
//// _ _
//// | |
if n != '|' && (ne == '_' || nw == '_') {
return N
}
if n == '-' {
return N
}
//// _| |_
if s != '|' && (w == '_' || e == '_') {
return S
}
if s == '-' {
return S
}
}
return NONE
}
goat-0.5.0/canvas_test.go 0000664 0000000 0000000 00000001051 14205205167 0015306 0 ustar 00root root 0000000 0000000 package goat
import (
"bytes"
"testing"
qt "github.com/frankban/quicktest"
)
func TestReadASCII(t *testing.T) {
c := qt.New(t)
var buf bytes.Buffer
// TODO: UNICODE
buf.WriteString(" +-->\n")
buf.WriteString(" | å\n")
buf.WriteString(" +----->")
canvas := NewCanvas(&buf)
c.Assert(canvas.Width, qt.Equals, 8)
c.Assert(canvas.Height, qt.Equals, 3)
buf.Reset()
buf.WriteString(" +--> \n")
buf.WriteString(" | å \n")
buf.WriteString(" +----->\n")
expected := buf.String()
c.Assert(expected, qt.Equals, canvas.String())
}
goat-0.5.0/examples/ 0000775 0000000 0000000 00000000000 14205205167 0014266 5 ustar 00root root 0000000 0000000 goat-0.5.0/examples/arrows.svg 0000664 0000000 0000000 00000002030 14205205167 0016317 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/arrows.txt 0000664 0000000 0000000 00000000042 14205205167 0016340 0 ustar 00root root 0000000 0000000 ^
|
<---+--->
|
v
goat-0.5.0/examples/big-grids.svg 0000664 0000000 0000000 00000016771 14205205167 0016672 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/big-grids.txt 0000664 0000000 0000000 00000001553 14205205167 0016702 0 ustar 00root root 0000000 0000000 .----. .----.
/ \ / \ .-----+-----+-----.
+ +----+ +----. | | | | .-----+-----+-----+-----+
\ / \ / \ | | | | / / / / /
+----+ B +----+ + +-----+-----+-----+ +-----+-----+-----+-----+
/ \ / \ / | | | | / / / / /
+ A +----+ +----+ | | B | | +-----+-----+-----+-----+
\ / \ / \ +-----+-----+-----+ / / A / B / /
'----+ +----+ + | | | | +-----+-----+-----+-----+
\ / \ / | A | | | / / / / /
'----' '----' '-----+-----+-----' '-----+-----+-----+-----+
goat-0.5.0/examples/big-shapes.svg 0000664 0000000 0000000 00000005222 14205205167 0017032 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/big-shapes.txt 0000664 0000000 0000000 00000001060 14205205167 0017046 0 ustar 00root root 0000000 0000000
.---------. . .-------. .-------. .---------. .-----. .----.
\ / / \ \ \ | | | | / \ / \
\ / / \ \ \ | | | | / \ | |
\ / / \ \ \ | | | | \ / | |
\ / / \ \ \ | | | | \ / \ /
' '---------' '-------' '-------' '---------' '-----' '----'
goat-0.5.0/examples/circle.svg 0000664 0000000 0000000 00000000712 14205205167 0016250 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/circle.txt 0000664 0000000 0000000 00000000030 14205205167 0016261 0 ustar 00root root 0000000 0000000
.-.
| |
'-'
goat-0.5.0/examples/circuits.svg 0000664 0000000 0000000 00000022061 14205205167 0016635 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/circuits.txt 0000664 0000000 0000000 00000002130 14205205167 0016650 0 ustar 00root root 0000000 0000000 ____ *
| |_____.---. |
o _____| )----------)-------.
/ \ | '---' | __|__
/___\ | | \ /
| '-------------. | \ /
A ----------------' | | o
.-------------------. o-----)-------' |
| |___.---. | |___.---.
B ---*---.__.---. ___| )--*--.__..---. ____) )----- Y
__| o----*--' '---' ______)) )---' '---'
C -------' '---' | | ''---'
| o
| / \
| /___\
| |
'--------------'
goat-0.5.0/examples/complicated.svg 0000664 0000000 0000000 00000105713 14205205167 0017302 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/complicated.txt 0000664 0000000 0000000 00000004032 14205205167 0017312 0 ustar 00root root 0000000 0000000 +-------------------+ ^ .---.
| A Box |__.--.__ __.--> | .-. | |
| | '--' v | * |<--- | |
+-------------------+ '-' | |
Round *---(-. |
.-----------------. .-------. .----------. .-------. | | |
| Mixed Rounded | | | / Diagonals \ | | | | | |
| & Square Corners | '--. .--' / \ |---+---| '-)-' .--------.
'--+------------+-' .--. | '-------+--------' | | | | / Search /
| | | | '---. | '-------' | '-+------'
|<---------->| | | | v Interior | ^
' <---' '----' .-----------. ---. .--- v |
.------------------. Diag line | .-------. +---. \ / . |
| if (a > b) +---. .--->| | | | | Curved line \ / / \ |
| obj->fcn() | \ / | '-------' |<--' + / \ |
'------------------' '--' '--+--------' .--. .--. | .-. +Done?+-'
.---+-----. | ^ |\ | | /| .--+ | | \ /
| | | Join \|/ | | Curved | \| |/ | | \ | \ /
| | +----> o --o-- '-' Vertical '--' '--' '-- '--' + .---.
<--+---+-----' | /|\ | | 3 |
v not:line 'quotes' .-' '---'
.-. .---+--------. / A || B *bold* | ^
| | | Not a dot | <---+---<-- A dash--is not a line v |
'-' '---------+--' / Nor/is this. ---
goat-0.5.0/examples/dot-grids.svg 0000664 0000000 0000000 00000034231 14205205167 0016706 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/dot-grids.txt 0000664 0000000 0000000 00000000745 14205205167 0016731 0 ustar 00root root 0000000 0000000
o o o o o * * * * * * * o o * o o o * * * o o o · * · · · · · ·
o o o o o * * * * * o o o o * o o o o * * * * * o * * · * * · · · · · ·
o o o o o * * * * * o * o o o o o o o o * * * * * o o o o o · o · · o · · * * ·
o o o o o * * * * * o * o o o o o o o * * * * o * o o · · · · o · · * ·
o o o o o * * * * * * * * * o o o o * * * o * o · · · · · · · *
goat-0.5.0/examples/edge-cases.svg 0000664 0000000 0000000 00000015041 14205205167 0017010 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/edge-cases.txt 0000664 0000000 0000000 00000000540 14205205167 0017026 0 ustar 00root root 0000000 0000000 +-+ \ \ | / /
+ + \ v v v /
+-+ \ .---------. / \ | /
v| |v vvv
+-+ --->| |<--- -->o<--
| | ^| |^ ^^^
+-+ / '---------' \ / | \
/ ^ ^ ^ \
/ / | \ \
goat-0.5.0/examples/flow-chart.svg 0000664 0000000 0000000 00000030333 14205205167 0017057 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/flow-chart.txt 0000664 0000000 0000000 00000001474 14205205167 0017103 0 ustar 00root root 0000000 0000000 .
.---------. / \
| START | / \ .-+-------+-. ___________
'----+----' .-------. A / \ B | |COMPLEX| | / \ .-.
| | END |<-----+CHOICE +----->| | | +--->+ PREPARATION +--->| X |
v '-------' \ / | |PROCESS| | \___________/ '-'
.---------. \ / '-+---+---+-'
/ INPUT / \ /
'-----+---' '
| ^
v |
.-----------. .-----+-----. .-.
| PROCESS +---------------->| PROCESS |<------+ X |
'-----------' '-----------' '-'
goat-0.5.0/examples/graphics.svg 0000664 0000000 0000000 00000023776 14205205167 0016626 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/graphics.txt 0000664 0000000 0000000 00000001545 14205205167 0016634 0 ustar 00root root 0000000 0000000 .
0 3 P * Eye / ^ /
*-------* +y \ +) \ / Reflection
1 /| 2 /| ^ \ \ \ v
*-------* | | v0 \ v3 --------*--------
| |4 | |7 | *----\-----*
| *-----|-* +-----> +x / v X \ .-.<-------- o
|/ |/ / / o \ | / | Refraction / \
*-------* v / \ +-' / \
5 6 +z v1 *------------------* v2 | o-----o
v
goat-0.5.0/examples/icons.svg 0000664 0000000 0000000 00000050071 14205205167 0016125 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/icons.txt 0000664 0000000 0000000 00000002661 14205205167 0016147 0 ustar 00root root 0000000 0000000 .-. .--------.
.-+ | | |
.--+ '--. |'--------'|
| Server Cloud |<------------------>| Database |
'-------------' | |
^ ^ '--------'
Internet | | ^
.------------------------' '-------------. |
| | v
v v .------. .------.
.--------. WiFi .--------. Bluetooth .-----. / # # /| / # # /|
| |<------------->| |<---------->| | +------+/| LAN +------+/|
|Windows | | OS X | | iOS | | +/|<--->| +/|
+--------+ +--------+ | | |Ubuntu+/| |Ubuntu+/|
/// ____ \\\ /// ____ \\\ | o | | +/ | +/
'------------' '------------' '-----' '------' '------'
Laptop 1 Laptop 2 Tablet 1 Dedicated Server Rack
goat-0.5.0/examples/large-nodes.svg 0000664 0000000 0000000 00000022277 14205205167 0017221 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/large-nodes.txt 0000664 0000000 0000000 00000001264 14205205167 0017232 0 ustar 00root root 0000000 0000000
.---. .-. .-. .-. .-.
| A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 |
'---' '-' '+' '-' | '-'
| ^ | ^
v | v |
.-. .-+-. .-. .-+-. .-. .+. .---.
| 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D |
'-' '---' '-' '---' '-' '-' '---'
goat-0.5.0/examples/line-decorations.svg 0000664 0000000 0000000 00000021126 14205205167 0020250 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/line-decorations.txt 0000664 0000000 0000000 00000001220 14205205167 0020261 0 ustar 00root root 0000000 0000000 ________ o * * .--------------.
*---+--. | | o o | ^ \ / | .----------. |
| | '--* -+- | | v / \ / | | <------. | |
| '-----> .---(---' --->*<--- / .+->*<--o----' | | | | |
<--' ^ ^ | | | | | ^ \ | '--------' | |
\/ *-----' o |<----->| '-----' |__| v '------------' |
/\ *---------------'
goat-0.5.0/examples/line-ends.svg 0000664 0000000 0000000 00000047013 14205205167 0016672 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/line-ends.txt 0000664 0000000 0000000 00000001271 14205205167 0016706 0 ustar 00root root 0000000 0000000 o--o *--o / / * o o o o o * * * * o o o o * * * * o o o o * * * *
o--* *--* v v ^ ^ | | | | | | | | \ \ \ \ \ \ \ \ / / / / / / / /
o--> *--> * o / / o * v ' o * v ' o * v \ o * v \ o * v / o * v /
o--- *---
^ ^ ^ ^ . . . . ^ ^ ^ ^ \ \ \ \ ^ ^ ^ ^ / / / /
| | * o \ \ * o | | | | | | | | \ \ \ \ \ \ \ \ / / / / / / / /
v v ^ ^ v v ^ ^ o * v ' o * v ' o * v \ o * v \ o * v / o * v /
* o | | * o \ \
<--o <--* <--> <--- ---o ---* ---> ---- *<-- o<-- -->o -->*
goat-0.5.0/examples/overlaps.svg 0000664 0000000 0000000 00000017040 14205205167 0016644 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/overlaps.txt 0000664 0000000 0000000 00000001143 14205205167 0016661 0 ustar 00root root 0000000 0000000
.-. .-. .-. .-. .-. .-.
| | | | | | | | | | | |
.---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--.
| | | | | | | | | | | | | | | | | |
'---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------'
| | | | | | | | | | | |
'-' '-' '-' '-' '-' '-'
goat-0.5.0/examples/small-grids.svg 0000664 0000000 0000000 00000026250 14205205167 0017232 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/small-grids.txt 0000664 0000000 0000000 00000001106 14205205167 0017243 0 ustar 00root root 0000000 0000000 ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---.
___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ |
/ \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+
\___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ |
/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+
\___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ |
\___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---'
goat-0.5.0/examples/small-nodes.svg 0000664 0000000 0000000 00000023140 14205205167 0017225 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/small-nodes.txt 0000664 0000000 0000000 00000001007 14205205167 0017243 0 ustar 00root root 0000000 0000000
A 1 2 4 8
A B C *----->o<---->o<----o-----------. o
*-------->o<------->o ^ ^ | ^
^ / ^ | | | | |
| v \ v v | v |
o----->o---->o<---->* o<--->*<---->o---->*---->o---->o<---->*
D E F G 3 B 5 C 6 7 D
goat-0.5.0/examples/small-shapes.svg 0000664 0000000 0000000 00000012415 14205205167 0017403 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/small-shapes.txt 0000664 0000000 0000000 00000001062 14205205167 0017417 0 ustar 00root root 0000000 0000000 .---. __ ..
.--. . .-----. \ / .---. .---. ___ ___ | | | )
/ \ / \ \ / .-. . ' . | | .---. .---. | | / \ | | '--' ''
\ / / \ \ / | | / \ / \ '---' / / \ \ | | \___/ |___| .. __
'--' '-----' ' '-' '---' /___\ '---' '---' '---' ( | |__|
''
goat-0.5.0/examples/tiny-grids.svg 0000664 0000000 0000000 00000055563 14205205167 0017116 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/tiny-grids.txt 0000664 0000000 0000000 00000001510 14205205167 0017115 0 ustar 00root root 0000000 0000000 ┌─┬─┬─┬─┬─┐ ▉▉ ▉▉ ▉▉ ⬢ ⬡ ⬡ ┌┬┬┬┬┬┬┬┬┐ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ ___________ +-+-+-+-+
├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+
├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ▉▉ ⬢ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+
├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬡ ⬡ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+
└─┴─┴─┴─┴─┘ ▉▉ ▉▉ ▉▉ ⬡ ⬡ ⬡ └┴┴┴┴┴┴┴┴┘ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+
goat-0.5.0/examples/trees.svg 0000664 0000000 0000000 00000015537 14205205167 0016144 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/trees.txt 0000664 0000000 0000000 00000001146 14205205167 0016153 0 ustar 00root root 0000000 0000000
. . . .--- 1 .-- 1 / 1
/ \ | | .---+ .-+ +
/ \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2
+ + | | | | ---+ ---+ +
/ \ / \ .-+-. .-+-. .+. .+. | .--- 3 | .-- 3 \ / 3
/ \ / \ | | | | | | | | '---+ '-+ +
1 2 3 4 1 2 3 4 1 2 3 4 '--- 4 '-- 4 \ 4
goat-0.5.0/examples/unicode.svg 0000664 0000000 0000000 00000053030 14205205167 0016436 0 ustar 00root root 0000000 0000000
goat-0.5.0/examples/unicode.txt 0000664 0000000 0000000 00000002267 14205205167 0016464 0 ustar 00root root 0000000 0000000 ↖ ↗ ✶ ✹ ✩ ⓵ ⎲ ░░▒▒▓▓▉▉ ▚▚ ▢ ▢ ⬚ ⬚ ⊕
▲ ◀━━━━━━━▶ ↙ ↘ ➊ ❶ ➀ ① ➕ ➖ ➗ ❌ ⎳ ╲ ╱ ▚▚ ▢ ▢ ⬚ ⬚ ⊖
┃ ╭╌╌╌╌╌╌╌╮ ╔═══════╗ ┏━━━━━━━┓ ┏╍╍╍╍╍╍╍┓ ╲ ╱ ░░▒▒▓▓▉▉ ▚▚ ⬣ ⬣ ⎔ ⎔ ⊗
┃ ╎ ╎ ║ ║ ┃ ┃ ╏ ╏ ⎛ ⎧ ⎡ ╳ ░░▒▒▓▓▉▉ ▚▚ ⬣ ⬣ ⎔ ⎔ ⊘
┃ ╎ ╎ ║ ║ ┃ ┃ ╏ ╏⋮ ⎜ ⎨ ⎢ ╱ ╲ ░░▒▒▓▓▉▉ ▚▚ ◯ ◯ ⏣ ⏣ ⊙
▼ ╰╌╌╌╌╌╌╌╯ ╚═══════╝ ┗━━━━━━━┛ ⋱ ┗╍╍╍╍╍╍╍┛⋮ ⎝ ⎩ ⎣╱ ╲ ░░▒▒▓▓▉▉ ▚▚ ◯ ◯ ⏣ ⏣ ⊛
⋱ ⋮ ◢▉▉◣ ⊜
∑xᵢ ∫t²dt ⋱ ⋮ ◥▉▉◤
goat-0.5.0/examples_test.go 0000664 0000000 0000000 00000004107 14205205167 0015656 0 ustar 00root root 0000000 0000000 package goat
import (
"bytes"
"flag"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
var write = flag.Bool("write", false, "write examples to disk")
func TestExamplesStableOutput(t *testing.T) {
c := qt.New(t)
var previous string
for i := 0; i < 3; i++ {
in, err := os.Open(filepath.Join(basePath, "circuits.txt"))
if err != nil {
t.Fatal(err)
}
var out bytes.Buffer
BuildAndWriteSVG(in, &out)
in.Close()
if i > 0 && previous != out.String() {
c.Fail()
}
previous = out.String()
}
}
func TestExamples(t *testing.T) {
c := qt.New(t)
filenames, err := filepath.Glob(filepath.Join(basePath, "*.txt"))
c.Assert(err, qt.IsNil)
var buff *bytes.Buffer
for _, name := range filenames {
in := getIn(name)
var out io.WriteCloser
if *write {
out = getOut(name)
} else {
if buff == nil {
buff = &bytes.Buffer{}
} else {
buff.Reset()
}
out = struct {
io.Writer
io.Closer
}{
buff,
io.NopCloser(nil),
}
}
BuildAndWriteSVG(in, out)
in.Close()
out.Close()
if buff != nil {
golden := getOutString(name)
if buff.String() != golden {
c.Log(buff.Len(), len(golden))
c.Fatalf("Content mismatch for %s", name)
}
in.Close()
out.Close()
}
}
}
func BenchmarkComplicated(b *testing.B) {
in := getIn(filepath.FromSlash("examples/complicated.txt"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
BuildAndWriteSVG(in, io.Discard)
}
}
const basePath string = "examples"
func getIn(filename string) io.ReadCloser {
in, err := os.Open(filename)
if err != nil {
panic(err)
}
return in
}
func getOut(filename string) io.WriteCloser {
out, err := os.Create(toSVGFilename(filename))
if err != nil {
panic(err)
}
return out
}
func getOutString(filename string) string {
b, err := ioutil.ReadFile(toSVGFilename(filename))
if err != nil {
panic(err)
}
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
return string(b)
}
func toSVGFilename(filename string) string {
return strings.TrimSuffix(filename, filepath.Ext(filename)) + ".svg"
}
goat-0.5.0/go.mod 0000664 0000000 0000000 00000000517 14205205167 0013561 0 ustar 00root root 0000000 0000000 module github.com/bep/goat
go 1.17
require (
github.com/frankban/quicktest v1.14.2
github.com/google/go-cmp v0.5.7
)
require (
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
)
goat-0.5.0/go.sum 0000664 0000000 0000000 00000003044 14205205167 0013604 0 ustar 00root root 0000000 0000000 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns=
github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
goat-0.5.0/index.go 0000664 0000000 0000000 00000001531 14205205167 0014106 0 ustar 00root root 0000000 0000000 package goat
// Index represents a position within an ASCII diagram.
type Index struct {
x int
y int
}
// Pixel represents the on-screen coordinates for an Index.
type Pixel Index
func (i *Index) asPixel() Pixel {
return Pixel{x: i.x * 8, y: i.y * 16}
}
func (i *Index) asPixelXY() (int, int) {
p := i.asPixel()
return p.x, p.y
}
func (i *Index) east() Index {
return Index{i.x + 1, i.y}
}
func (i *Index) west() Index {
return Index{i.x - 1, i.y}
}
func (i *Index) north() Index {
return Index{i.x, i.y - 1}
}
func (i *Index) south() Index {
return Index{i.x, i.y + 1}
}
func (i *Index) nWest() Index {
return Index{i.x - 1, i.y - 1}
}
func (i *Index) nEast() Index {
return Index{i.x + 1, i.y - 1}
}
func (i *Index) sWest() Index {
return Index{i.x - 1, i.y + 1}
}
func (i *Index) sEast() Index {
return Index{i.x + 1, i.y + 1}
}
goat-0.5.0/iter.go 0000664 0000000 0000000 00000002273 14205205167 0013746 0 ustar 00root root 0000000 0000000 package goat
type canvasIterator func(width int, height int) chan Index
func upDown(width int, height int) chan Index {
c := make(chan Index, width*height)
go func() {
for w := 0; w < width; w++ {
for h := 0; h < height; h++ {
c <- Index{w, h}
}
}
close(c)
}()
return c
}
func leftRight(width int, height int) chan Index {
c := make(chan Index, width*height)
// Transpose an upDown order.
go func() {
for i := range upDown(height, width) {
c <- Index{i.y, i.x}
}
close(c)
}()
return c
}
func diagDown(width int, height int) chan Index {
c := make(chan Index, width*height)
go func() {
minSum := -height + 1
maxSum := width
for sum := minSum; sum <= maxSum; sum++ {
for w := 0; w < width; w++ {
for h := 0; h < height; h++ {
if w-h == sum {
c <- Index{w, h}
}
}
}
}
close(c)
}()
return c
}
func diagUp(width int, height int) chan Index {
c := make(chan Index, width*height)
go func() {
maxSum := width + height - 2
for sum := 0; sum <= maxSum; sum++ {
for w := 0; w < width; w++ {
for h := 0; h < height; h++ {
if h+w == sum {
c <- Index{w, h}
}
}
}
}
close(c)
}()
return c
}
goat-0.5.0/iter_test.go 0000664 0000000 0000000 00000002463 14205205167 0015006 0 ustar 00root root 0000000 0000000 package goat
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
)
var eq = qt.CmpEquals(
cmp.Comparer(func(i1, i2 Index) bool {
return i1.x == i2.x && i1.y == i2.y
}),
)
func TestIterators(t *testing.T) {
c := qt.New(t)
tests := []struct {
iterator chan Index
expected []Index
}{
// UpDown
// 1 3
// 2 4
{
iterator: upDown(2, 2),
expected: []Index{
{0, 0},
{0, 1},
{1, 0},
{1, 1},
},
},
// LeftRight
// 1 2
// 3 4
{
iterator: leftRight(2, 2),
expected: []Index{
{0, 0},
{1, 0},
{0, 1},
{1, 1},
},
},
// DiagUp
// 1 3
// 2 5
// 4 6
{
iterator: diagUp(2, 3),
expected: []Index{
{0, 0}, // x + y == 0
{0, 1}, // x + y == 1
{1, 0}, // x + y == 1
{0, 2}, // x + y == 2
{1, 1}, // x + y == 2
{1, 2}, // x + y == 3
},
},
// DiagDown
// 2 4 6
// 1 3 5
{
iterator: diagDown(3, 2),
expected: []Index{
{0, 1}, // x - y == -1
{0, 0}, // x - y == 0
{1, 1}, // x - y == 0
{1, 0}, // x - y == 1
{2, 1}, // x - y == 1
{2, 0}, // x - y == 2
},
},
}
for _, tt := range tests {
result := make([]Index, 0, len(tt.expected))
for i := range tt.iterator {
result = append(result, i)
}
c.Assert(result, eq, tt.expected)
}
}
goat-0.5.0/svg.go 0000664 0000000 0000000 00000014621 14205205167 0013602 0 ustar 00root root 0000000 0000000 package goat
import (
"bytes"
"fmt"
"io"
)
type SVG struct {
Body string
Width int
Height int
}
func (s SVG) String() string {
return fmt.Sprintf("\n",
"diagram",
"http://www.w3.org/2000/svg",
"1.1", s.Height, s.Width, s.Body)
}
// BuildSVG reads in a newline-delimited ASCII diagram from src and returns a SVG.
func BuildSVG(src io.Reader) SVG {
var buff bytes.Buffer
canvas := NewCanvas(src)
canvas.WriteSVGBody(&buff)
return SVG{
Body: buff.String(),
Width: canvas.widthScreen(),
Height: canvas.heightScreen(),
}
}
// BuildAndWriteSVG reads in a newline-delimited ASCII diagram from src and writes a
// corresponding SVG diagram to dst.
func BuildAndWriteSVG(src io.Reader, dst io.Writer) {
canvas := NewCanvas(src)
// Preamble
writeBytes(dst,
"\n")
}
func writeBytes(out io.Writer, format string, args ...interface{}) {
bytesOut := fmt.Sprintf(format, args...)
_, err := out.Write([]byte(bytesOut))
if err != nil {
panic(nil)
}
}
// Draw a straight line as an SVG path.
func (l Line) Draw(out io.Writer) {
start := l.start.asPixel()
stop := l.stop.asPixel()
// For cases when a vertical line hits a perpendicular like this:
//
// | |
// | or v
// --- ---
//
// We need to nudge the vertical line half a vertical cell in the
// appropriate direction in order to meet up cleanly with the midline of
// the cell next to it.
// A diagonal segment all by itself needs to be shifted slightly to line
// up with _ baselines:
// _
// \_
//
// TODO make this a method on Line to return accurate pixel
if l.lonely {
switch l.orientation {
case NE:
start.x -= 4
stop.x -= 4
start.y += 8
stop.y += 8
case SE:
start.x -= 4
stop.x -= 4
start.y -= 8
stop.y -= 8
case S:
start.y -= 8
stop.y -= 8
}
// Half steps
switch l.chop {
case N:
stop.y -= 8
case S:
start.y += 8
}
}
if l.needsNudgingDown {
stop.y += 8
if l.horizontal() {
start.y += 8
}
}
if l.needsNudgingLeft {
start.x -= 8
}
if l.needsNudgingRight {
stop.x += 8
}
if l.needsTinyNudgingLeft {
start.x -= 4
if l.orientation == NE {
start.y += 8
} else if l.orientation == SE {
start.y -= 8
}
}
if l.needsTinyNudgingRight {
stop.x += 4
if l.orientation == NE {
stop.y -= 8
} else if l.orientation == SE {
stop.y += 8
}
}
writeBytes(out,
"\n",
start.x, start.y,
stop.x, stop.y,
)
}
// Draw a solid triable as an SVG polygon element.
func (t Triangle) Draw(out io.Writer) {
// https://www.w3.org/TR/SVG/shapes.html#PolygonElement
/*
+-----+-----+
| /|\ |
| / | \ |
x +- / -+- \ -+
| / | \ |
|/ | \|
+-----+-----+
y
*/
x, y := float32(t.start.asPixel().x), float32(t.start.asPixel().y)
r := 0.0
x0 := x + 8
y0 := y
x1 := x - 4
y1 := y - 0.35*16
x2 := x - 4
y2 := y + 0.35*16
switch t.orientation {
case N:
r = 270
if t.needsNudging {
x0 += 8
x1 += 8
x2 += 8
}
case NE:
r = 300
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case NW:
r = 240
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case W:
r = 180
if t.needsNudging {
x0 -= 8
x1 -= 8
x2 -= 8
}
case E:
r = 0
if t.needsNudging {
x0 -= 8
x1 -= 8
x2 -= 8
}
case S:
r = 90
if t.needsNudging {
x0 += 8
x1 += 8
x2 += 8
}
case SW:
r = 120
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
case SE:
r = 60
x0 += 4
x1 += 4
x2 += 4
if t.needsNudging {
x0 += 6
x1 += 6
x2 += 6
}
}
writeBytes(out,
"\n",
x0, y0,
x1, y1,
x2, y2,
r,
x, y,
)
}
// Draw a solid circle as an SVG circle element.
func (c *Circle) Draw(out io.Writer) {
fill := "#fff"
if c.bold {
fill = "currentColor"
}
pixel := c.start.asPixel()
writeBytes(out,
"\n",
pixel.x,
pixel.y,
fill,
)
}
// Draw a single text character as an SVG text element.
func (t Text) Draw(out io.Writer) {
p := t.start.asPixel()
c := t.contents
opacity := 0
// Markdeep special-cases these character and treats them like a
// checkerboard.
switch c {
case "▉":
opacity = -64
case "▓":
opacity = 64
case "▒":
opacity = 128
case "░":
opacity = 191
}
fill := "currentColor"
if opacity > 0 {
fill = fmt.Sprintf("rgb(%d,%d,%d)", opacity, opacity, opacity)
}
if opacity != 0 {
writeBytes(out,
"",
p.x-4, p.y-8,
fill,
)
return
}
// Escape for XML
switch c {
case "&":
c = "&"
case ">":
c = ">"
case "<":
c = "<"
}
writeBytes(out,
"%s\n",
p.x, p.y+4, c,
)
}
// Draw a rounded corner as an SVG elliptical arc element.
func (c *RoundedCorner) Draw(out io.Writer) {
// https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
x, y := c.start.asPixelXY()
startX, startY, endX, endY, sweepFlag := 0, 0, 0, 0, 0
switch c.orientation {
case NW:
startX = x + 8
startY = y
endX = x - 8
endY = y + 16
case NE:
sweepFlag = 1
startX = x - 8
startY = y
endX = x + 8
endY = y + 16
case SE:
sweepFlag = 1
startX = x + 8
startY = y - 16
endX = x - 8
endY = y
case SW:
startX = x - 8
startY = y - 16
endX = x + 8
endY = y
}
writeBytes(out,
"\n",
startX,
startY,
sweepFlag,
endX,
endY,
)
}
// Draw a bridge as an SVG elliptical arc element.
func (b Bridge) Draw(out io.Writer) {
x, y := b.start.asPixelXY()
sweepFlag := 1
if b.orientation == W {
sweepFlag = 0
}
writeBytes(out,
"\n",
x, y-8,
sweepFlag,
x, y+8,
)
}