pax_global_header00006660000000000000000000000064142052051670014514gustar00rootroot0000000000000052 comment=cc97a132eb5e894f8c4615d04029e4f4d071561c goat-0.5.0/000077500000000000000000000000001420520516700124505ustar00rootroot00000000000000goat-0.5.0/.github/000077500000000000000000000000001420520516700140105ustar00rootroot00000000000000goat-0.5.0/.github/workflows/000077500000000000000000000000001420520516700160455ustar00rootroot00000000000000goat-0.5.0/.github/workflows/test.yml000066400000000000000000000007201420520516700175460ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000000671420520516700144430ustar00rootroot00000000000000*.swp .DS_Store vendor examples/*.svg goat goat.test goat-0.5.0/.vscode/000077500000000000000000000000001420520516700140115ustar00rootroot00000000000000goat-0.5.0/.vscode/settings.json000066400000000000000000000000461420520516700165440ustar00rootroot00000000000000{ "autoHide.autoHidePanel": false } goat-0.5.0/LICENSE000066400000000000000000000020661420520516700134610ustar00rootroot00000000000000The 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.md000066400000000000000000000211041420520516700137250ustar00rootroot00000000000000# 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 ![Trees Example](https://cdn.rawgit.com/blampe/goat/master/examples/trees.svg) ``` . . . .--- 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 ![Overlaps Example](https://cdn.rawgit.com/blampe/goat/master/examples/overlaps.svg) ``` .-. .-. .-. .-. .-. .-. | | | | | | | | | | | | .---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--. | | | | | | | | | | | | | | | | | | '---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------' | | | | | | | | | | | | '-' '-' '-' '-' '-' '-' ``` ### Line Decorations ![Line Decorations Example](https://cdn.rawgit.com/blampe/goat/master/examples/line-decorations.svg) ``` ________ o * * .--------------. *---+--. | | o o | ^ \ / | .----------. | | | '--* -+- | | v / \ / | | <------. | | | '-----> .---(---' --->*<--- / .+->*<--o----' | | | | | <--' ^ ^ | | | | | ^ \ | '--------' | | \/ *-----' o |<----->| '-----' |__| v '------------' | /\ *---------------' ``` ### Line Ends ![Line Ends Example](https://cdn.rawgit.com/blampe/goat/master/examples/line-ends.svg) ``` 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 ![Dot Grids Example](https://cdn.rawgit.com/blampe/goat/master/examples/dot-grids.svg) ``` 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 ![Large Node Example](https://cdn.rawgit.com/blampe/goat/master/examples/large-nodes.svg) ``` .---. .-. .-. .-. .-. | A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 | '---' '-' '+' '-' | '-' | ^ | ^ v | v | .-. .-+-. .-. .-+-. .-. .+. .---. | 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D | '-' '---' '-' '---' '-' '-' '---' ``` ### Small Grids ![Small Grids Example](https://cdn.rawgit.com/blampe/goat/master/examples/small-grids.svg) ``` ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' ``` ### Big Grids ![Big Grids Example](https://cdn.rawgit.com/blampe/goat/master/examples/big-grids.svg) ``` .----. .----. / \ / \ .-----+-----+-----. + +----+ +----. | | | | .-----+-----+-----+-----+ \ / \ / \ | | | | / / / / / +----+ B +----+ + +-----+-----+-----+ +-----+-----+-----+-----+ / \ / \ / | | | | / / / / / + A +----+ +----+ | | B | | +-----+-----+-----+-----+ \ / \ / \ +-----+-----+-----+ / / A / B / / '----+ +----+ + | | | | +-----+-----+-----+-----+ \ / \ / | A | | | / / / / / '----' '----' '-----+-----+-----' '-----+-----+-----+-----+ ``` ### Complicated ![Complicated Example](https://cdn.rawgit.com/blampe/goat/master/examples/complicated.svg) ``` +-------------------+ ^ .---. | 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.go000066400000000000000000000607371420520516700142670ustar00rootroot00000000000000package 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.go000066400000000000000000000010511420520516700153060ustar00rootroot00000000000000package 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/000077500000000000000000000000001420520516700142665ustar00rootroot00000000000000goat-0.5.0/examples/arrows.svg000066400000000000000000000020301420520516700163170ustar00rootroot00000000000000 goat-0.5.0/examples/arrows.txt000066400000000000000000000000421420520516700163400ustar00rootroot00000000000000 ^ | <---+---> | v goat-0.5.0/examples/big-grids.svg000066400000000000000000000167711420520516700166720ustar00rootroot00000000000000 A B A B A B goat-0.5.0/examples/big-grids.txt000066400000000000000000000015531420520516700167020ustar00rootroot00000000000000 .----. .----. / \ / \ .-----+-----+-----. + +----+ +----. | | | | .-----+-----+-----+-----+ \ / \ / \ | | | | / / / / / +----+ B +----+ + +-----+-----+-----+ +-----+-----+-----+-----+ / \ / \ / | | | | / / / / / + A +----+ +----+ | | B | | +-----+-----+-----+-----+ \ / \ / \ +-----+-----+-----+ / / A / B / / '----+ +----+ + | | | | +-----+-----+-----+-----+ \ / \ / | A | | | / / / / / '----' '----' '-----+-----+-----' '-----+-----+-----+-----+ goat-0.5.0/examples/big-shapes.svg000066400000000000000000000052221420520516700170320ustar00rootroot00000000000000 goat-0.5.0/examples/big-shapes.txt000066400000000000000000000010601420520516700170460ustar00rootroot00000000000000 .---------. . .-------. .-------. .---------. .-----. .----. \ / / \ \ \ | | | | / \ / \ \ / / \ \ \ | | | | / \ | | \ / / \ \ \ | | | | \ / | | \ / / \ \ \ | | | | \ / \ / ' '---------' '-------' '-------' '---------' '-----' '----' goat-0.5.0/examples/circle.svg000066400000000000000000000007121420520516700162500ustar00rootroot00000000000000 goat-0.5.0/examples/circle.txt000066400000000000000000000000301420520516700162610ustar00rootroot00000000000000 .-. | | '-' goat-0.5.0/examples/circuits.svg000066400000000000000000000220611420520516700166350ustar00rootroot00000000000000 A B C Y goat-0.5.0/examples/circuits.txt000066400000000000000000000021301420520516700166500ustar00rootroot00000000000000 ____ * | |_____.---. | o _____| )----------)-------. / \ | '---' | __|__ /___\ | | \ / | '-------------. | \ / A ----------------' | | o .-------------------. o-----)-------' | | |___.---. | |___.---. B ---*---.__.---. ___| )--*--.__..---. ____) )----- Y __| o----*--' '---' ______)) )---' '---' C -------' '---' | | ''---' | o | / \ | /___\ | | '--------------' goat-0.5.0/examples/complicated.svg000066400000000000000000001057131420520516700173020ustar00rootroot00000000000000 & A M S i o i q f b B x u j o e a ( - x d r a > e f R > c o C n u o b ( n r ) ) d n e e J d r o s i n N o R D t o i u a a n g d d l o i t n e D i a g o n a l s C V u e r r v t e i d c a l n o t A N C : o u l d r r i A a / I v n s i n e e h s t d - e - t r l B i h i i s i o n ' s r e q n . u * o o b t t o e l a s d ' * l i n e D o n S e e ? a r c 3 h goat-0.5.0/examples/complicated.txt000066400000000000000000000040321420520516700173120ustar00rootroot00000000000000+-------------------+ ^ .---. | 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.svg000066400000000000000000000342311420520516700167060ustar00rootroot00000000000000 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · goat-0.5.0/examples/dot-grids.txt000066400000000000000000000007451420520516700167310ustar00rootroot00000000000000 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.svg000066400000000000000000000150411420520516700170100ustar00rootroot00000000000000 goat-0.5.0/examples/edge-cases.txt000066400000000000000000000005401420520516700170260ustar00rootroot00000000000000 +-+ \ \ | / / + + \ v v v / +-+ \ .---------. / \ | / v| |v vvv +-+ --->| |<--- -->o<-- | | ^| |^ ^^^ +-+ / '---------' \ / | \ / ^ ^ ^ \ / / | \ \ goat-0.5.0/examples/flow-chart.svg000066400000000000000000000303331420520516700170570ustar00rootroot00000000000000 P S I R T N O A P C R U E T T S S E N D A C P H R O O I C C E E S S B C P O R M O X P C L E E S X S P R E P A R A T I O N X goat-0.5.0/examples/flow-chart.txt000066400000000000000000000014741420520516700171030ustar00rootroot00000000000000 . .---------. / \ | START | / \ .-+-------+-. ___________ '----+----' .-------. A / \ B | |COMPLEX| | / \ .-. | | END |<-----+CHOICE +----->| | | +--->+ PREPARATION +--->| X | v '-------' \ / | |PROCESS| | \___________/ '-' .---------. \ / '-+---+---+-' / INPUT / \ / '-----+---' ' | ^ v | .-----------. .-----+-----. .-. | PROCESS +---------------->| PROCESS |<------+ X | '-----------' '-----------' '-' goat-0.5.0/examples/graphics.svg000066400000000000000000000237761420520516700166260ustar00rootroot00000000000000 1 5 0 4 2 6 3 7 + z + y + x v 1 v P 0 X v 3 E y v e 2 R e f r a c t i o n R e f l e c t i o n goat-0.5.0/examples/graphics.txt000066400000000000000000000015451420520516700166340ustar00rootroot00000000000000 . 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.svg000066400000000000000000000500711420520516700161250ustar00rootroot00000000000000 W L i a n p d t o o w p s 1 I n W t i e F r i n e t L a S O p e S t r o v X p e r 2 C l o B u l d u e t o o t h T a i b O l S e t 1 D a U D t b e a # u d b n i a t c s # u a e t e d L S A e N r v e U r b # u R n a t c # u k goat-0.5.0/examples/icons.txt000066400000000000000000000026611420520516700161470ustar00rootroot00000000000000 .-. .--------. .-+ | | | .--+ '--. |'--------'| | 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.svg000066400000000000000000000222771420520516700172210ustar00rootroot00000000000000 A 1 2 3 4 B 5 C 6 8 7 D goat-0.5.0/examples/large-nodes.txt000066400000000000000000000012641420520516700172320ustar00rootroot00000000000000 .---. .-. .-. .-. .-. | A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 | '---' '-' '+' '-' | '-' | ^ | ^ v | v | .-. .-+-. .-. .-+-. .-. .+. .---. | 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D | '-' '---' '-' '---' '-' '-' '---' goat-0.5.0/examples/line-decorations.svg000066400000000000000000000211261420520516700202500ustar00rootroot00000000000000 goat-0.5.0/examples/line-decorations.txt000066400000000000000000000012201420520516700202610ustar00rootroot00000000000000 ________ o * * .--------------. *---+--. | | o o | ^ \ / | .----------. | | | '--* -+- | | v / \ / | | <------. | | | '-----> .---(---' --->*<--- / .+->*<--o----' | | | | | <--' ^ ^ | | | | | ^ \ | '--------' | | \/ *-----' o |<----->| '-----' |__| v '------------' | /\ *---------------' goat-0.5.0/examples/line-ends.svg000066400000000000000000000470131420520516700166720ustar00rootroot00000000000000 goat-0.5.0/examples/line-ends.txt000066400000000000000000000012711420520516700167060ustar00rootroot00000000000000 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.svg000066400000000000000000000170401420520516700166440ustar00rootroot00000000000000 goat-0.5.0/examples/overlaps.txt000066400000000000000000000011431420520516700166610ustar00rootroot00000000000000 .-. .-. .-. .-. .-. .-. | | | | | | | | | | | | .---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--. | | | | | | | | | | | | | | | | | | '---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------' | | | | | | | | | | | | '-' '-' '-' '-' '-' '-' goat-0.5.0/examples/small-grids.svg000066400000000000000000000262501420520516700172320ustar00rootroot00000000000000 a b a b a b a b goat-0.5.0/examples/small-grids.txt000066400000000000000000000011061420520516700172430ustar00rootroot00000000000000 ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' goat-0.5.0/examples/small-nodes.svg000066400000000000000000000231401420520516700172250ustar00rootroot00000000000000 A D E B F C G A 1 2 3 4 B 5 C 6 8 7 D goat-0.5.0/examples/small-nodes.txt000066400000000000000000000010071420520516700172430ustar00rootroot00000000000000 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.svg000066400000000000000000000124151420520516700174030ustar00rootroot00000000000000 goat-0.5.0/examples/small-shapes.txt000066400000000000000000000010621420520516700174170ustar00rootroot00000000000000 .---. __ .. .--. . .-----. \ / .---. .---. ___ ___ | | | ) / \ / \ \ / .-. . ' . | | .---. .---. | | / \ | | '--' '' \ / / \ \ / | | / \ / \ '---' / / \ \ | | \___/ |___| .. __ '--' '-----' ' '-' '---' /___\ '---' '---' '---' ( | |__| '' goat-0.5.0/examples/tiny-grids.svg000066400000000000000000000555631420520516700171160ustar00rootroot00000000000000 goat-0.5.0/examples/tiny-grids.txt000066400000000000000000000015101420520516700171150ustar00rootroot00000000000000 ┌─┬─┬─┬─┬─┐ ▉▉ ▉▉ ▉▉ ⬢ ⬡ ⬡ ┌┬┬┬┬┬┬┬┬┐ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ ___________ +-+-+-+-+ ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ▉▉ ⬢ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬡ ⬡ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ └─┴─┴─┴─┴─┘ ▉▉ ▉▉ ▉▉ ⬡ ⬡ ⬡ └┴┴┴┴┴┴┴┴┘ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ goat-0.5.0/examples/trees.svg000066400000000000000000000155371420520516700161440ustar00rootroot00000000000000 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 goat-0.5.0/examples/trees.txt000066400000000000000000000011461420520516700161530ustar00rootroot00000000000000 . . . .--- 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.svg000066400000000000000000000530301420520516700164360ustar00rootroot00000000000000 x t ² d t goat-0.5.0/examples/unicode.txt000066400000000000000000000022671420520516700164640ustar00rootroot00000000000000 ↖ ↗ ✶ ✹ ✩ ⓵ ⎲ ░░▒▒▓▓▉▉ ▚▚ ▢ ▢ ⬚ ⬚ ⊕ ▲ ◀━━━━━━━▶ ↙ ↘ ➊ ❶ ➀ ① ➕ ➖ ➗ ❌ ⎳ ╲ ╱ ▚▚ ▢ ▢ ⬚ ⬚ ⊖ ┃ ╭╌╌╌╌╌╌╌╮ ╔═══════╗ ┏━━━━━━━┓ ┏╍╍╍╍╍╍╍┓ ╲ ╱ ░░▒▒▓▓▉▉ ▚▚ ⬣ ⬣ ⎔ ⎔ ⊗ ┃ ╎ ╎ ║ ║ ┃ ┃ ╏ ╏ ⎛ ⎧ ⎡ ╳ ░░▒▒▓▓▉▉ ▚▚ ⬣ ⬣ ⎔ ⎔ ⊘ ┃ ╎ ╎ ║ ║ ┃ ┃ ╏ ╏⋮ ⎜ ⎨ ⎢ ╱ ╲ ░░▒▒▓▓▉▉ ▚▚ ◯ ◯ ⏣ ⏣ ⊙ ▼ ╰╌╌╌╌╌╌╌╯ ╚═══════╝ ┗━━━━━━━┛ ⋱ ┗╍╍╍╍╍╍╍┛⋮ ⎝ ⎩ ⎣╱ ╲ ░░▒▒▓▓▉▉ ▚▚ ◯ ◯ ⏣ ⏣ ⊛ ⋱ ⋮ ◢▉▉◣ ⊜ ∑xᵢ ∫t²dt ⋱ ⋮ ◥▉▉◤ goat-0.5.0/examples_test.go000066400000000000000000000041071420520516700156560ustar00rootroot00000000000000package 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.mod000066400000000000000000000005171420520516700135610ustar00rootroot00000000000000module 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.sum000066400000000000000000000030441420520516700136040ustar00rootroot00000000000000github.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.go000066400000000000000000000015311420520516700141060ustar00rootroot00000000000000package 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.go000066400000000000000000000022731420520516700137460ustar00rootroot00000000000000package 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.go000066400000000000000000000024631420520516700150060ustar00rootroot00000000000000package 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.go000066400000000000000000000146211420520516700136020ustar00rootroot00000000000000package goat import ( "bytes" "fmt" "io" ) type SVG struct { Body string Width int Height int } func (s SVG) String() string { return fmt.Sprintf("\n%s\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", "diagram", "http://www.w3.org/2000/svg", "1.1", canvas.heightScreen(), canvas.widthScreen(), ) canvas.WriteSVGBody(dst) 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, ) }