diff --git a/.gitignore b/.gitignore
index 5040fb2..d9e18a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,8 @@
vendor
examples/*.svg
-goat
-goat.test
+cmd/tmpl-expand/tmpl-expand
+cmd/goat/goat
+
+*~
+*.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c044acb..c8fca2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,30 @@ No changes yet.
## [0.5.0] - 2022-02-07
+### Update (@blampe)
+
+I hacked together GoAT a number of years ago while trying to embed some
+diagrams in a Hugo project I was playing with. Through an odd twist of fate
+GoAT eventually made its way into the upstream Hugo project, and if you're
+using [v0.93.0] you can embed these diagrams natively. Neat!
+
+My original implementation was certainly buggy and not on par with markdeep.
+I'm grateful for the folks who've helped smooth out the rough edges, and I've
+updated this project to reflect the good changes made in the Hugo fork,
+including a long-overdue `go.mod`.
+
+There's a lot I would like to do with this project that I will never get to, so
+instead I recommend you look at these forks:
+ * [@bep] is the fork currently used by Hugo, which I expect to be more active
+ over time.
+ * [@dmacvicar] has improved SVG/PNG/PDF rendering.
+ * [@sw46] has implemented a really wonderful hand-drawn style worth checking
+ out.
+
+TODO
+ - Dashed lines signaled by `:` or `=`
+ - Bold lines signaled by ???
+
### Changed
* Merges changes made by @bep and @dmacvicar in their forks. This includes
diff --git a/README.md b/README.md
index f058dfd..f12cb3c 100644
--- a/README.md
+++ b/README.md
@@ -1,75 +1,60 @@
# GoAT: Go ASCII Tool
-
+- Tie together all three of:
+ 1. Your code's major data structures or abstract data/control flows.
+ 2. Related ASCII-art diagrams embedded in comments, adjacent to the source code.
+ 3. Polished line diagrams in your user-facing high-level documentation, with inline links
+ to SVG produced by [goat](./cmd/goat).
+ For Markdown or similar formats, links may be expanded either at build-time or run-time,
+ as needed by your doc tool suite.
-This is a Go implementation of [markdeep.mini.js]'s ASCII diagram
-generation.
+ Your ASCII-art source persists as the single-point-of-truth, revision-controlled along with
+ the code that embeds it.
+ This README contains an [example](#library-data-flow).
-## Update (2022-02-07)
+## You Will Also Need
-I hacked together GoAT a number of years ago while trying to embed some
-diagrams in a Hugo project I was playing with. Through an odd twist of fate
-GoAT eventually made its way into the upstream Hugo project, and if you're
-using [v0.93.0] you can embed these diagrams natively. Neat!
+#### Graphical- or Rectangle-oriented text editing capability
+Both **vim** and **emacs** offer useful support.
+In Emacs, see the built-in rectangle-editing commands, and ```picture-mode```.
-My original implementation was certainly buggy and not on par with markdeep.
-I'm grateful for the folks who've helped smooth out the rough edges, and I've
-updated this project to reflect the good changes made in the Hugo fork,
-including a long-overdue `go.mod`.
+#### A fixed-pitch font with 2:1 height:width ratio as presented by your editor and terminal emulator
+Most fixed-pitch or "monospace" Unicode fonts maintain a 2:1 aspect ratio for
+characters in the ASCII range,
+and all GoAT drawing characters are ASCII.
+However, certain Unicode graphical characters e.g. MIDDLE DOT may be useful, and
+conform to the width of the ASCII range.
-There's a lot I would like to do with this project that I will never get to, so
-instead I recommend you look at these forks:
+CJK characters on the other hand are typically wider than 2:1.
+Non-standard width characters are not in general composable on the left-right axis within a plain-text
+drawing, because the remainder of the line of text to their right is pushed out of alignment
+with rows above and below.
-* [@bep] is the fork currently used by Hugo, which I expect to be more active
- over time.
-* [@dmacvicar] has improved SVG/PNG/PDF rendering.
-* [@sw46] has implemented a really wonderful hand-drawn style worth checking
- out.
-
-## Usage
-
-```bash
-$ go get github.com/blampe/goat
-$ cat my-cool-diagram.txt | goat > my-cool-diagram.svg
+## Installation
+```
+$ go install github.com/blampe/goat/cmd/goat@latest
```
-
-By default, the program reads from stdin, unless `-i infile` is given.
-
-By default, the program writes to stdout, unless `-o outfile` is given or a
-binary format with `-f` is selected.
-
-By default, it writes in [SVG] format, unless another format is specified with
-`-f`.
-
-## TODO
-
-- Dashed lines signaled by `:` or `=`.
-- Bold lines signaled by ???.
## Examples
-Here are some SVGs and the UTF-8 input they were generated from:
-
-### Trees
+Here are some snippets of
+GoAT-formatted UTF-8
+and the SVG each can generate.
+The SVG you see below was linked to by
+inline Markdown image references
+([howto](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#images),
+[spec](https://github.github.com/gfm/#images)) from
+GoAT's [README.md](README.md), then finally rendered to HTML `````` elements by Github's Markdown processor
-![Trees Example](https://cdn.rawgit.com/blampe/goat/main/examples/trees.svg)
+### Trees
```
+
. . . .--- 1 .-- 1 / 1
/ \ | | .---+ .-+ +
/ \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2
@@ -77,13 +62,14 @@ Here are some SVGs and the UTF-8 input they were generated from:
/ \ / \ .-+-. .-+-. .+. .+. | .--- 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/main/examples/overlaps.svg)
+```
+![](./examples/trees.svg)
+### Overlaps
```
+
.-. .-. .-. .-. .-. .-.
| | | | | | | | | | | |
.---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--.
@@ -91,12 +77,11 @@ Here are some SVGs and the UTF-8 input they were generated from:
'---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------'
| | | | | | | | | | | |
'-' '-' '-' '-' '-' '-'
+
```
+![](./examples/overlaps.svg)
### Line Decorations
-
-![Line Decorations Example](https://cdn.rawgit.com/blampe/goat/main/examples/line-decorations.svg)
-
```
________ o * * .--------------.
*---+--. | | o o | ^ \ / | .----------. |
@@ -105,12 +90,11 @@ Here are some SVGs and the UTF-8 input they were generated from:
<--' ^ ^ | | | | | ^ \ | '--------' | |
\/ *-----' o |<----->| '-----' |__| v '------------' |
/\ *---------------'
+
```
+![](./examples/line-decorations.svg)
### Line Ends
-
-![Line Ends Example](https://cdn.rawgit.com/blampe/goat/main/examples/line-ends.svg)
-
```
o--o *--o / / * o o o o o * * * * o o o o * * * * o o o o * * * *
o--* *--* v v ^ ^ | | | | | | | | \ \ \ \ \ \ \ \ / / / / / / / /
@@ -122,26 +106,28 @@ Here are some SVGs and the UTF-8 input they were generated from:
* o | | * o \ \
<--o <--* <--> <--- ---o ---* ---> ---- *<-- o<-- -->o -->*
-```
-### Dot Grids
-![Dot Grids Example](https://cdn.rawgit.com/blampe/goat/main/examples/dot-grids.svg)
+```
+![](./examples/line-ends.svg)
+### 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 · · · · · · · *
+
+
```
Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character, encoded with UTF-8.
+![](./examples/dot-grids.svg)
### Large Nodes
-
-![Large Node Example](https://cdn.rawgit.com/blampe/goat/main/examples/large-nodes.svg)
-
```
+
.---. .-. .-. .-. .-.
| A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 |
'---' '-' '+' '-' | '-'
@@ -150,12 +136,12 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character,
.-. .-+-. .-. .-+-. .-. .+. .---.
| 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D |
'-' '---' '-' '---' '-' '-' '---'
+
```
+![](./examples/large-nodes.svg)
### Small Grids
-
-![Small Grids Example](https://cdn.rawgit.com/blampe/goat/main/examples/small-grids.svg)
-
+![](./examples/small-grids.svg)
```
___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---.
___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ |
@@ -164,12 +150,11 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character,
/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+
\___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ |
\___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---'
-```
-### Big Grids
-![Big Grids Example](https://cdn.rawgit.com/blampe/goat/main/examples/big-grids.svg)
+```
+### Big Grids
```
.----. .----.
/ \ / \ .-----+-----+-----.
@@ -182,12 +167,12 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character,
'----+ +----+ + | | | | +-----+-----+-----+-----+
\ / \ / | A | | | / / / / /
'----' '----' '-----+-----+-----' '-----+-----+-----+-----+
-```
-### Complicated
-![Complicated Example](https://cdn.rawgit.com/blampe/goat/main/examples/complicated.svg)
+```
+![](./examples/big-grids.svg)
+### Complicated
```
+-------------------+ ^ .---.
| A Box |__.--.__ __.--> | .-. | |
@@ -213,9 +198,31 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character,
.-. .---+--------. / A || B *bold* | ^
| | | Not a dot | <---+---<-- A dash--is not a line v |
'-' '---------+--' / Nor/is this. ---
+
```
+![](./examples/complicated.svg)
+
+### More examples are [here](examples)
+
+## The GoAT Library
+
+The core engine of ```goat``` is accessible as a Go library package, for inclusion in specialized
+code of your own.
+The code implements a subset, and some extensions, of the ASCII diagram generation function of the browser-side Javascript in [Markdeep](http://casual-effects.com/markdeep/).
+
+### library Data Flow
+![](./goat.svg)
+
+The diagram above was derived by [./make.sh](./make.sh) from ASCII-art in the Go
+source file [./goat.go](./goat.go).
+
+#### Project Tenets
-More examples are available [here](examples).
+1. Utility and ease of integration into existing projects are paramount.
+2. Compatibility with MarkDeep desired, but not required.
+3. TXT and SVG intelligibility are co-equal in priority.
+4. Composability of TXT not to be sacrificed -- only width-8 characters allowed.
+5. Per-platform support limited to a single widely-available fixed-pitch TXT font.
[@bep]: https://github.com/bep/goat/
[@dmacvicar]: https://github.com/dmacvicar/goat
diff --git a/README.md.tmpl b/README.md.tmpl
index 2527a0c..6597987 100644
--- a/README.md.tmpl
+++ b/README.md.tmpl
@@ -1,221 +1,133 @@
# GoAT: Go ASCII Tool
-
+- Tie together all three of:
+ 1. Your code's major data structures or abstract data/control flows.
+ 2. Related ASCII-art diagrams embedded in comments, adjacent to the source code.
+ 3. Polished line diagrams in your user-facing high-level documentation, with inline links
+ to SVG produced by [goat](./cmd/goat).
+ For Markdown or similar formats, links may be expanded either at build-time or run-time,
+ as needed by your doc tool suite.
-This is a Go implementation of [markdeep.mini.js]'s ASCII diagram
-generation.
+ Your ASCII-art source persists as the single-point-of-truth, revision-controlled along with
+ the code that embeds it.
+ This README contains an [example](#library-data-flow).
-## Update (2022-02-07)
+## You Will Also Need
-I hacked together GoAT a number of years ago while trying to embed some
-diagrams in a Hugo project I was playing with. Through an odd twist of fate
-GoAT eventually made its way into the upstream Hugo project, and if you're
-using [v0.93.0] you can embed these diagrams natively. Neat!
+#### Graphical- or Rectangle-oriented text editing capability
+Both **vim** and **emacs** offer useful support.
+In Emacs, see the built-in rectangle-editing commands, and ```picture-mode```.
-My original implementation was certainly buggy and not on par with markdeep.
-I'm grateful for the folks who've helped smooth out the rough edges, and I've
-updated this project to reflect the good changes made in the Hugo fork,
-including a long-overdue `go.mod`.
+#### A fixed-pitch font with 2:1 height:width ratio as presented by your editor and terminal emulator
+Most fixed-pitch or "monospace" Unicode fonts maintain a 2:1 aspect ratio for
+characters in the ASCII range,
+and all GoAT drawing characters are ASCII.
+However, certain Unicode graphical characters e.g. MIDDLE DOT may be useful, and
+conform to the width of the ASCII range.
-There's a lot I would like to do with this project that I will never get to, so
-instead I recommend you look at these forks:
+CJK characters on the other hand are typically wider than 2:1.
+Non-standard width characters are not in general composable on the left-right axis within a plain-text
+drawing, because the remainder of the line of text to their right is pushed out of alignment
+with rows above and below.
-* [@bep] is the fork currently used by Hugo, which I expect to be more active
- over time.
-* [@dmacvicar] has improved SVG/PNG/PDF rendering.
-* [@sw46] has implemented a really wonderful hand-drawn style worth checking
- out.
-
-## Usage
-
-```bash
-$ go get github.com/blampe/goat
-$ cat my-cool-diagram.txt | goat > my-cool-diagram.svg
+## Installation
+```
+$ go install github.com/{{.GithubUser}}/goat/cmd/goat@latest
```
-
-By default, the program reads from stdin, unless `-i infile` is given.
-
-By default, the program writes to stdout, unless `-o outfile` is given or a
-binary format with `-f` is selected.
-
-By default, it writes in [SVG] format, unless another format is specified with
-`-f`.
-
-## TODO
-
-- Dashed lines signaled by `:` or `=`.
-- Bold lines signaled by ???.
## Examples
-Here are some SVGs and the UTF-8 input they were generated from:
+Here are some snippets of
+GoAT-formatted UTF-8
+and the SVG each can generate.
+The SVG you see below was linked to by
+inline Markdown image references
+([howto](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#images),
+[spec](https://github.github.com/gfm/#images)) from
+GoAT's [README.md](README.md), then finally rendered to HTML `````` elements by Github's Markdown processor
-### Trees
-
-![Trees Example]({{.Root}}/examples/trees.svg)
+### 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
+{{.trees_txt}}
```
+![]({{.Root}}/examples/trees.svg)
### Overlaps
-
-![Overlaps Example]({{.Root}}/examples/overlaps.svg)
-
```
- .-. .-. .-. .-. .-. .-.
- | | | | | | | | | | | |
- .---------. .--+---+--. .--+---+--. .--| |--. .--+ +--. .------|--.
- | | | | | | | | | | | | | | | | | |
- '---------' '--+---+--' '--+---+--' '--| |--' '--+ +--' '--|------'
- | | | | | | | | | | | |
- '-' '-' '-' '-' '-' '-'
+{{.overlaps_txt}}
```
+![]({{.Root}}/examples/overlaps.svg)
### Line Decorations
-
-![Line Decorations Example]({{.Root}}/examples/line-decorations.svg)
-
```
- ________ o * * .--------------.
- *---+--. | | o o | ^ \ / | .----------. |
- | | '--* -+- | | v / \ / | | <------. | |
- | '-----> .---(---' --->*<--- / .+->*<--o----' | | | | |
- <--' ^ ^ | | | | | ^ \ | '--------' | |
- \/ *-----' o |<----->| '-----' |__| v '------------' |
- /\ *---------------'
+{{.line_decorations_txt}}
```
+![]({{.Root}}/examples/line-decorations.svg)
### Line Ends
-
-![Line Ends Example]({{.Root}}/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 -->*
+{{.line_ends_txt}}
```
+![]({{.Root}}/examples/line-ends.svg)
### Dot Grids
-
-![Dot Grids Example]({{.Root}}/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 · · · · · · · *
+{{.dot_grids_txt}}
```
Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character, encoded with UTF-8.
+![]({{.Root}}/examples/dot-grids.svg)
### Large Nodes
-
-![Large Node Example]({{.Root}}/examples/large-nodes.svg)
-
```
- .---. .-. .-. .-. .-.
- | A +----->| 1 +<---->| 2 |<----+ 4 +------------------. | 8 |
- '---' '-' '+' '-' | '-'
- | ^ | ^
- v | v |
- .-. .-+-. .-. .-+-. .-. .+. .---.
- | 3 +---->| B |<----->| 5 +---->| C +---->| 6 +---->| 7 |<---->| D |
- '-' '---' '-' '---' '-' '-' '---'
+{{.large_nodes_txt}}
```
+![]({{.Root}}/examples/large-nodes.svg)
### Small Grids
-
-![Small Grids Example]({{.Root}}/examples/small-grids.svg)
-
+![]({{.Root}}/examples/small-grids.svg)
```
- ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---.
- ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ |
- / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+
- \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ |
- / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+
- \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ |
- \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---'
+{{.small_grids_txt}}
```
### Big Grids
-
-![Big Grids Example]({{.Root}}/examples/big-grids.svg)
-
```
- .----. .----.
- / \ / \ .-----+-----+-----.
- + +----+ +----. | | | | .-----+-----+-----+-----+
- \ / \ / \ | | | | / / / / /
- +----+ B +----+ + +-----+-----+-----+ +-----+-----+-----+-----+
- / \ / \ / | | | | / / / / /
- + A +----+ +----+ | | B | | +-----+-----+-----+-----+
- \ / \ / \ +-----+-----+-----+ / / A / B / /
- '----+ +----+ + | | | | +-----+-----+-----+-----+
- \ / \ / | A | | | / / / / /
- '----' '----' '-----+-----+-----' '-----+-----+-----+-----+
+{{.big_grids_txt}}
```
+![]({{.Root}}/examples/big-grids.svg)
### Complicated
+```
+{{.complicated_txt}}
+```
+![]({{.Root}}/examples/complicated.svg)
+
+### More examples are [here](examples)
+
+## The GoAT Library
+
+The core engine of ```goat``` is accessible as a Go library package, for inclusion in specialized
+code of your own.
+The code implements a subset, and some extensions, of the ASCII diagram generation function of the browser-side Javascript in [Markdeep](http://casual-effects.com/markdeep/).
+
+### library Data Flow
+![]({{.Root}}/goat.svg)
+
+The diagram above was derived by [./make.sh](./make.sh) from ASCII-art in the Go
+source file [./goat.go](./goat.go).
+
+#### Project Tenets
-![Complicated Example]({{.Root}}/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).
+1. Utility and ease of integration into existing projects are paramount.
+2. Compatibility with MarkDeep desired, but not required.
+3. TXT and SVG intelligibility are co-equal in priority.
+4. Composability of TXT not to be sacrificed -- only width-8 characters allowed.
+5. Per-platform support limited to a single widely-available fixed-pitch TXT font.
[@bep]: https://github.com/bep/goat/
[@dmacvicar]: https://github.com/dmacvicar/goat
diff --git a/canvas.go b/canvas.go
index 68ff69c..20d5c72 100644
--- a/canvas.go
+++ b/canvas.go
@@ -2,34 +2,76 @@ package goat
import (
"bufio"
- "bytes"
"io"
- "sort"
)
+type (
+ exists struct{}
+ runeSet map[rune]exists
+)
+
+
// 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,
+var jointRunes = []rune{
+ '.',
+ '\'',
+ '+',
+ '*',
+ 'o',
}
+var reserved = append(
+ jointRunes,
+ []rune{
+ '-',
+ '_',
+ '|',
+ 'v',
+ '^',
+ '>',
+ '<',
+ '/',
+ '\\',
+ ')',
+ '(',
+ ' ', // X SPACE is reserved
+ }...,
+)
+var reservedSet runeSet
+
+var doubleWideSVG = []rune{
+ 'o',
+ '*',
+}
+var wideSVG = []rune{
+ 'v', // X Input containing " over " needs to be considered text.
+// '>', // Uncommenting would get 'o<' and '>o' wrong. But o> and >o -- never desired to be text?
+// '<', // ibid.
+ '^',
+ ')',
+ '(',
+ '.', // Dropping this would cause " v. " to be considered graphics.
+}
+var wideSVGSet = makeSet(append(doubleWideSVG, wideSVG...))
+
+func makeSet(runeSlice []rune) (rs runeSet) {
+ rs = make(runeSet)
+ for _, r := range runeSlice {
+ rs[r] = exists{}
+ }
+ return
+}
+
+func init() {
+ // Recall that ranging over a 'string' type extracts values of type 'rune'.
+
+ reservedSet = make(runeSet)
+ for _, r := range reserved {
+ reservedSet[r] = exists{}
+ }
+}
+
+// XX linear search of slice -- alternative to a map test
func contains(in []rune, r rune) bool {
for _, v := range in {
if r == v {
@@ -43,6 +85,7 @@ func isJoint(r rune) bool {
return contains(jointRunes, r)
}
+// XX rename 'isCircle()'?
func isDot(r rune) bool {
return r == 'o' || r == '*'
}
@@ -53,41 +96,13 @@ func isTriangle(r rune) bool {
// Canvas represents a 2D ASCII rectangle.
type Canvas struct {
- Width int
- Height int
+ // units of cells
+ Width, 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
}
@@ -96,33 +111,60 @@ func (c *Canvas) widthScreen() int {
return (c.Width + 1) * 8
}
+// Arg 'canvasMap' is typically either Canvas.data or Canvas.text
+func inSet(set runeSet, canvasMap map[Index]rune, i Index) (inset bool) {
+ r, inMap := canvasMap[i]
+ if !inMap {
+ return false // r == rune(0)
+ }
+ _, inset = set[r]
+ return
+}
+
+// Looks only at c.data[], ignores c.text[].
+// Returns the rune for ASCII Space i.e. ' ', in the event that map lookup fails.
+// XX Name 'dataRuneAt()' would be more descriptive, but maybe too bulky.
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 {
+// NewCanvas creates a fully-populated Canvas according to GoAT-formatted text read from
+// an io.Reader, consuming all bytes available.
+func NewCanvas(in io.Reader) (c Canvas) {
+ // XX Move this function to top of file.
width := 0
height := 0
scanner := bufio.NewScanner(in)
- data := make(map[Index]rune)
+ c = Canvas{
+ data: make(map[Index]rune),
+ text: nil,
+ }
+ // Fill the 'data' map.
for scanner.Scan() {
- line := scanner.Text()
+ lineStr := 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)
+ // X Type of second value assigned from "for ... range" operator over a string is "rune".
+ // https://go.dev/ref/spec#For_statements
+ // But yet, counterintuitively, type of lineStr[_index_] is 'byte'.
+ // https://go.dev/ref/spec#String_types
+ // XXXX Refactor to use []rune from above.
+ for _, r := range lineStr {
+ //if r > 255 {
+ // fmt.Printf("linestr=\"%s\"\n", lineStr)
+ // fmt.Printf("r == 0x%x\n", r)
+ //}
+ if r == ' ' {
+ panic("TAB character found on input")
+ }
+ i := Index{w, height}
+ c.data[i] = r
w++
}
@@ -132,38 +174,45 @@ func NewCanvas(in io.Reader) Canvas {
height++
}
- text := make(map[Index]rune)
-
- c := Canvas{
- Width: width,
- Height: height,
- data: data,
- text: text,
- }
+ c.Width = width
+ c.Height = height
+ c.text = make(map[Index]rune)
+ // Fill the 'text' map, with runes removed from 'data'.
+ c.MoveToText()
+ return
+}
- // 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)
+// Move contents of every cell that appears, according to a tricky set of rules,
+// to be "text", into a separate map: from data[] to text[].
+// So data[] and text[] are an exact partitioning of the
+// incoming grid-aligned runes.
+func (c *Canvas) MoveToText() {
+ for i := range leftRight(c.Width, c.Height) {
+ if c.shouldMoveToText(i) {
+ c.text[i] = c.runeAt(i) // c.runeAt() Reads from c.data[]
}
}
- for idx := range c.text {
- delete(c.data, idx)
+ for i := range c.text {
+ delete(c.data, i)
}
-
- return c
}
// Drawable represents anything that can Draw itself.
type Drawable interface {
- Draw(out io.Writer)
+ draw(out io.Writer)
}
-// Line represents a straight segment between two points.
+// Line represents a straight segment between two points 'start' and 'stop', where
+// 'start' is either lesser in X (north-east, east, south-east), or
+// equal in X and lesser in Y (south).
type Line struct {
start Index
stop Index
- // dashed bool
+
+ startRune rune
+ stopRune rune
+
+ // dashed bool
needsNudgingDown bool
needsNudgingLeft bool
needsNudgingRight bool
@@ -176,6 +225,7 @@ type Line struct {
// N or S. Only useful for half steps - chops of this half of the line.
chop Orientation
+ // X-major, Y-minor. Therefore, always one of the compass points NE, E, SE, S.
orientation Orientation
state lineState
@@ -192,17 +242,20 @@ func (l *Line) started() bool {
return l.state == _Started
}
-func (l *Line) setStart(i Index) {
+func (c *Canvas) setStart(l *Line, i Index) {
if l.state == _Unstarted {
l.start = i
+ l.startRune = c.runeAt(i)
l.stop = i
+ l.stopRune = c.runeAt(i)
l.state = _Started
}
}
-func (l *Line) setStop(i Index) {
+func (c *Canvas) setStop(l *Line, i Index) {
if l.state == _Started {
l.stop = i
+ l.stopRune = c.runeAt(i)
}
}
@@ -222,10 +275,12 @@ func (l *Line) diagonal() bool {
return l.orientation == NE || l.orientation == SE || l.orientation == SW || l.orientation == NW
}
+// XX drop names 'start' below
+
// Triangle corresponds to "^", "v", "<" and ">" runes in the absence of
// surrounding alphanumerics.
type Triangle struct {
- start Index
+ start Index
orientation Orientation
needsNudging bool
}
@@ -239,21 +294,21 @@ type Circle struct {
// RoundedCorner corresponds to combinations of "-." or "-'".
type RoundedCorner struct {
- start Index
+ 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
+ start Index
+ str string // Possibly multiple bytes, from Unicode source of type 'rune'
}
-// Bridge correspondes to combinations of "-)-" or "-(-" and is displayed as
+// Bridge corresponds to combinations of "-)-" or "-(-" and is displayed as
// the vertical line "hopping over" the horizontal.
type Bridge struct {
- start Index
+ start Index
orientation Orientation
}
@@ -262,49 +317,48 @@ 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
+ N // North
+ NE // Northeast
+ NW // Northwest
+ S // South
+ SE // Southeast
+ SW // Southwest
+ E // East
+ W // West
)
+// WriteSVGBody writes the entire content of a Canvas out to a stream in SVG format.
func (c *Canvas) WriteSVGBody(dst io.Writer) {
writeBytes(dst, "\n")
for _, l := range c.Lines() {
- l.Draw(dst)
+ l.draw(dst)
}
- for _, t := range c.Triangles() {
- t.Draw(dst)
+ for _, tI := range c.Triangles() {
+ tI.draw(dst)
}
for _, c := range c.RoundedCorners() {
- c.Draw(dst)
+ c.draw(dst)
}
for _, c := range c.Circles() {
- c.Draw(dst)
+ c.draw(dst)
}
- for _, b := range c.Bridges() {
- b.Draw(dst)
+ for _, bI := range c.Bridges() {
+ bI.draw(dst)
}
- for _, t := range c.Text() {
- t.Draw(dst)
- }
+ writeText(dst, c)
writeBytes(dst, "\n")
}
// Lines returns a slice of all Line drawables that we can detect -- in all
// possible orientations.
-func (c *Canvas) Lines() []Line {
+func (c *Canvas) Lines() (lines []Line) {
horizontalMidlines := c.getLinesForSegment('-')
diagUpLines := c.getLinesForSegment('/')
@@ -399,13 +453,13 @@ func (c *Canvas) Lines() []Line {
}
// _
- // _/ \
+ // _/ \
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
}
@@ -449,31 +503,27 @@ func (c *Canvas) Lines() []Line {
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()...)
+ lines = append(lines, c.HalfSteps()...) // vertical, only
- return lines
+ return
}
func newHalfStep(i Index, chop Orientation) Line {
return Line{
- start: i,
- stop: i.south(),
- lonely: true,
- chop: chop,
+ start: i,
+ stop: i.south(),
+ lonely: true,
+ chop: chop,
orientation: S,
}
}
-func (c *Canvas) HalfSteps() []Line {
- var lines []Line
-
+func (c *Canvas) HalfSteps() (lines []Line) {
for idx := range upDown(c.Width, c.Height) {
if o := c.partOfHalfStep(idx); o != NONE {
lines = append(
@@ -482,8 +532,7 @@ func (c *Canvas) HalfSteps() []Line {
)
}
}
-
- return lines
+ return
}
func (c *Canvas) getLinesForSegment(segment rune) []Line {
@@ -529,17 +578,14 @@ func (c *Canvas) getLines(
segment rune,
passThroughs []rune,
o Orientation,
-) []Line {
-
- var lines []Line
-
+) (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 {
+ snip := func(cl Line) Line {
// Only collect lines that actually go somewhere or are isolated
- // segments.
- if l.goesSomewhere() {
- lines = append(lines, l)
+ // segments; otherwise, discard what's been collected so far within 'cl'.
+ if cl.goesSomewhere() {
+ lines = append(lines, cl)
}
return Line{orientation: o}
@@ -589,7 +635,7 @@ func (c *Canvas) getLines(
switch currentLine.state {
case _Unstarted:
if shouldKeep {
- currentLine.setStart(idx)
+ c.setStart(¤tLine, idx)
}
case _Started:
if !shouldKeep {
@@ -599,7 +645,7 @@ func (c *Canvas) getLines(
// adjust later in the / and \ cases.
if !currentLine.goesSomewhere() && lastSeenRune == segment {
if !c.partOfRoundedCorner(currentLine.start) {
- currentLine.setStop(idx)
+ c.setStop(¤tLine, idx)
currentLine.lonely = true
}
}
@@ -607,25 +653,23 @@ func (c *Canvas) getLines(
} else if isPassThrough {
// Snip the existing line but include the current pass-through
// character because we may be continuing the line.
- currentLine.setStop(idx)
+ c.setStop(¤tLine, idx)
currentLine = snip(currentLine)
- currentLine.setStart(idx)
+ c.setStart(¤tLine, idx)
} else if shouldKeep {
// Keep the line going and extend it by this character.
- currentLine.setStop(idx)
+ c.setStop(¤tLine, idx)
}
}
lastSeenRune = r
}
-
- return lines
+ return
}
-// Triangles returns a slice of all detectable Triangles.
-func (c *Canvas) Triangles() []Drawable {
- var triangles []Drawable
-
+// Triangles detects intended triangles -- typically at the end of an intended line --
+// and returns a representational slice composed of types Triangle and Line.
+func (c *Canvas) Triangles() (triangles []Drawable) {
o := NONE
for idx := range upDown(c.Width, c.Height) {
@@ -638,26 +682,36 @@ func (c *Canvas) Triangles() []Drawable {
continue
}
- // Identify our orientation and nudge the triangle to touch any
+ // Identify orientation and nudge the triangle to touch any
// adjacent walls.
switch r {
case '^':
o = N
// ^ and ^
- // / \
+ // / \
if c.runeAt(start.sWest()) == '/' {
o = NE
} else if c.runeAt(start.sEast()) == '\\' {
o = NW
}
case 'v':
- o = S
- // / and \
- // v v
- if c.runeAt(start.nEast()) == '/' {
+ if c.runeAt(start.north()) == '|' {
+ // |
+ // v
+ o = S
+ } else if c.runeAt(start.nEast()) == '/' {
+ // /
+ // v
o = SW
} else if c.runeAt(start.nWest()) == '\\' {
+ // \
+ // v
o = SE
+ } else {
+ // Conclusion: Meant as a text string 'v', not a triangle
+ //panic("Not sufficient to fix all 'v' troubles.")
+ // continue XX Already committed to non-text output for this string?
+ o = S
}
case '<':
o = W
@@ -682,8 +736,8 @@ func (c *Canvas) Triangles() []Drawable {
triangles = append(
triangles,
Line{
- start: start.nWest(),
- stop: start,
+ start: start.nWest(),
+ stop: start,
orientation: SE,
},
)
@@ -695,8 +749,8 @@ func (c *Canvas) Triangles() []Drawable {
triangles = append(
triangles,
Line{
- start: start,
- stop: start.nEast(),
+ start: start,
+ stop: start.nEast(),
orientation: NE,
},
)
@@ -714,8 +768,8 @@ func (c *Canvas) Triangles() []Drawable {
triangles = append(
triangles,
Line{
- start: start,
- stop: start.sEast(),
+ start: start,
+ stop: start.sEast(),
orientation: SE,
},
)
@@ -727,8 +781,8 @@ func (c *Canvas) Triangles() []Drawable {
triangles = append(
triangles,
Line{
- start: start.sWest(),
- stop: start,
+ start: start.sWest(),
+ stop: start,
orientation: NE,
},
)
@@ -748,20 +802,17 @@ func (c *Canvas) Triangles() []Drawable {
triangles = append(
triangles,
Triangle{
- start: start,
+ start: start,
orientation: o,
needsNudging: needsNudging,
},
)
}
-
- return triangles
+ return
}
// Circles returns a slice of all 'o' and '*' characters not considered text.
-func (c *Canvas) Circles() []Circle {
- var circles []Circle
-
+func (c *Canvas) Circles() (circles []Circle) {
for idx := range upDown(c.Width, c.Height) {
// TODO INCOMING
if c.runeAt(idx) == 'o' {
@@ -770,14 +821,11 @@ func (c *Canvas) Circles() []Circle {
circles = append(circles, Circle{start: idx, bold: true})
}
}
-
- return circles
+ return
}
// RoundedCorners returns a slice of all curvy corners in the diagram.
-func (c *Canvas) RoundedCorners() []RoundedCorner {
- var corners []RoundedCorner
-
+func (c *Canvas) RoundedCorners() (corners []RoundedCorner) {
for idx := range leftRight(c.Width, c.Height) {
if o := c.isRoundedCorner(idx); o != NONE {
corners = append(
@@ -786,8 +834,7 @@ func (c *Canvas) RoundedCorners() []RoundedCorner {
)
}
}
-
- return corners
+ return
}
// For . and ' characters this will return a non-NONE orientation if the
@@ -818,25 +865,25 @@ func (c *Canvas) isRoundedCorner(i Index) Orientation {
}
// .- or .-
- // | +
+ // | +
if opensDown && dashRight && isVerticalSegment(lowerLeft) {
return NW
}
// -. or -. or -. or _. or -.
- // | + ) ) o
+ // | + ) ) o
if opensDown && dashLeft && isVerticalSegment(lowerRight) {
return NE
}
- // | or + or | or + or + or_ )
- // -' -' +' +' ++ '
+ // | or + or | or + or + or_ )
+ // -' -' +' +' ++ '
if opensUp && dashLeft && isVerticalSegment(upperRight) {
return SE
}
// | or +
- // '- '-
+ // '- '-
if opensUp && dashRight && isVerticalSegment(upperLeft) {
return SW
}
@@ -844,76 +891,24 @@ func (c *Canvas) isRoundedCorner(i Index) Orientation {
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}
+// Must be stably sorted, to satisfy regression tests.
+func (c *Canvas) Text() (text []Text) {
+ for idx := range leftRight(c.Width, c.Height) {
+ r, found := c.text[idx]
+ if !found {
+ continue
}
- j++
+ text = append(text, Text{
+ start: idx,
+ str: string(r)})
}
-
- 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
+ return
}
-// Bridges returns a slice of all bridges, "-)-" or "-(-".
-func (c *Canvas) Bridges() []Drawable {
- var bridges []Drawable
-
+// Bridges returns a slice of all bridges, "-)-" or "-(-", composed as a sequence of
+// either type Bridge or type Line.
+func (c *Canvas) Bridges() (bridges []Drawable) {
for idx := range leftRight(c.Width, c.Height) {
if o := c.isBridge(idx); o != NONE {
bridges = append(
@@ -921,14 +916,13 @@ func (c *Canvas) Bridges() []Drawable {
newHalfStep(idx.north(), S),
newHalfStep(idx.south(), N),
Bridge{
- start: idx,
+ start: idx,
orientation: o,
},
)
}
}
-
- return bridges
+ return
}
// -)- or -(- or
@@ -953,75 +947,88 @@ func (c *Canvas) isBridge(i Index) Orientation {
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
+func (c *Canvas) shouldMoveToText(i Index) bool {
+ i_r := c.runeAt(i)
+ if i_r == ' ' {
+ // X Note that c.runeAt(i) returns ' ' if i lies right of all chars on line i.Y
+ return false
}
- if c.runeAt(i) == ' ' {
- return false
+ // Returns true if the character at index 'i' of c.data[] is reserved for diagrams.
+ // Characters like 'o' and 'v' need more context (e.g., are other text characters
+ // nearby) to determine whether they're part of a diagram.
+ isReserved := func(i Index) (found bool) {
+ i_r, inData := c.data[i]
+ if !inData {
+ // lies off left or right end of line, treat as reserved
+ return true
+ }
+ _, found = reservedSet[i_r]
+ return
}
- if !c.isReserved(i) {
+ if !isReserved(i) {
return true
}
- // This is a reserved character with an incoming line (e.g., "|") above it,
+ // This is a reserved character with an incoming line (e.g., "|") above or below it,
// so call it non-text.
if c.hasLineAboveOrBelow(i) {
return false
}
+ w := i.west()
+ e := i.east()
+
// 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()) {
+ // 'i' is reserved but surrounded by text and probably part of an existing word.
+ // Preserve chains of reserved-but-text characters like "foo----bar".
+ if textLeft := !isReserved(w); textLeft {
+ return true
+ }
+ if textRight := !isReserved(e); textRight {
return true
}
- w := i.west()
- e := i.east()
+ crowded := func (l, r Index) bool {
+ return inSet(wideSVGSet, c.data, l) &&
+ inSet(wideSVGSet, c.data, r)
+ }
+ if crowded(w, i) || crowded(i, e) {
+ return true
+ }
+ // If 'i' has anything other than a space to either left or right, treat as non-text.
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) == '*' {
+ if i_r == 'o' || i_r == '*' {
return false
}
- // We're surrounded by whitespace + text on either side.
- if !c.isReserved(w.west()) || !c.isReserved(e.east()) {
+ // 'i' is surrounded by whitespace or text on one side or the other, at two cell's distance.
+ if !isReserved(w.west()) || !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)
+ i_r := c.runeAt(i)
- switch r {
+ switch i_r {
case '*', 'o', '+', 'v', '^':
return c.partOfDiagonalLine(i) || c.partOfVerticalLine(i)
case '|':
@@ -1126,18 +1133,18 @@ func (c *Canvas) partOfHalfStep(i Index) Orientation {
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
diff --git a/canvas_test.go b/canvas_test.go
index 4bce5d6..bf67eb5 100644
--- a/canvas_test.go
+++ b/canvas_test.go
@@ -31,3 +31,31 @@ func TestReadASCII(t *testing.T) {
c.Assert(expected, qt.Equals, canvas.String())
}
+
+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}
+
+ // Search 'text' map; if nothing there try the 'data' map.
+ r, ok := c.text[idx]
+ if !ok {
+ r = c.runeAt(idx)
+ }
+
+ _, err := buffer.WriteRune(r)
+ if err != nil {
+ continue
+ }
+ }
+
+ err := buffer.WriteByte('\n')
+ if err != nil {
+ continue
+ }
+ }
+
+ return buffer.String()
+}
diff --git a/cmd/goat/main.go b/cmd/goat/main.go
index a6dfdfc..2f5d5b7 100644
--- a/cmd/goat/main.go
+++ b/cmd/goat/main.go
@@ -1,28 +1,29 @@
package main
+// Import ...
import (
"flag"
- "fmt"
"log"
"os"
- "path/filepath"
- "strings"
"github.com/blampe/goat"
)
-func main() {
- log.SetFlags(0)
+// Function init ...
+func init() {
+ log.SetFlags(/*log.Ldate |*/ log.Ltime | log.Lshortfile)
+}
- var inputFilename string
- var outputFilename string
- var format string
- var svgColorLightScheme string
- var svgColorDarkScheme string
+func main() {
+ var (
+ inputFilename,
+ outputFilename,
+ svgColorLightScheme,
+ svgColorDarkScheme string
+ )
flag.StringVar(&inputFilename, "i", "", "Input filename (default stdin)")
flag.StringVar(&outputFilename, "o", "", "Output filename (default stdout for SVG)")
- flag.StringVar(&format, "f", "svg", "Output format: svg (default: svg)")
flag.StringVar(&svgColorLightScheme, "sls", "#000000", `short for -svg-color-light-scheme`)
flag.StringVar(&svgColorLightScheme, "svg-color-light-scheme", "#000000",
`See help for -svg-color-dark-scheme`)
@@ -36,17 +37,8 @@ func main() {
See https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
`)
- flag.BoolVar(&goat.HollowCircles, "hollowcircles", false,
- `If set, the letter 'o' draws a hollow circle, with strokes possibly extending
-into it; otherwise, the circle is filled with a computed inverse of the foreground
-drawing color.`)
flag.Parse()
- format = strings.ToLower(format)
- if format != "svg" {
- log.Fatalf("unrecognized format: %s", format)
- }
-
input := os.Stdin
if inputFilename != "" {
if _, err := os.Stat(inputFilename); os.IsNotExist(err) {
@@ -64,28 +56,11 @@ drawing color.`)
if outputFilename != "" {
var err error
output, err = os.Create(outputFilename)
- defer output.Close()
+ defer output.Close() // XX Move outside 'if' -- close os.Stdout as well?
if err != nil {
log.Fatal(err)
}
- // warn the user if he is writing to an extension different to the
- // file format
- ext := filepath.Ext(outputFilename)
- if fmt.Sprintf(".%s", format) != ext {
- log.Printf("Warning: writing to '%s' with extension '%s' and format %s", outputFilename, ext, strings.ToUpper(format))
- }
- } else {
- // check that we are not writing binary data to terminal
- fileInfo, _ := os.Stdout.Stat()
- isTerminal := (fileInfo.Mode() & os.ModeCharDevice) != 0
- if isTerminal && format != "svg" {
- log.Fatalf("refuse to write binary data to terminal: %s", format)
- }
- }
-
- switch format {
- case "svg":
- goat.BuildAndWriteSVG(input, output,
- svgColorLightScheme, svgColorDarkScheme)
}
+ goat.BuildAndWriteSVG(input, output,
+ svgColorLightScheme, svgColorDarkScheme)
}
diff --git a/cmd/tmpl-expand/main.go b/cmd/tmpl-expand/main.go
new file mode 100644
index 0000000..483e56a
--- /dev/null
+++ b/cmd/tmpl-expand/main.go
@@ -0,0 +1,277 @@
+// Copyright 2022 Donald Mullis. All rights reserved.
+
+// See `tmpl-expand -help` for abstract.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "regexp"
+ "sort"
+ "strings"
+ "text/template"
+)
+
+type (
+ KvpArg struct {
+ Key string
+ Value string
+ }
+ TemplateContext struct {
+ // https://golang.org/pkg/text/template/#hdr-Arguments
+ tmpl *template.Template
+ substitutionsMap map[string]string
+ }
+)
+
+// General args
+var (
+ writeMarkdown = flag.Bool("markdown", false,
+ `Reformat -help usage message into Github-flavored Markdown`)
+
+ exitStatus int
+)
+
+func init() {
+ log.SetFlags(log.Lshortfile)
+}
+
+func main() {
+ flag.Usage = func() {
+ UsageDump()
+ os.Exit(1)
+ }
+ flag.Parse()
+ if !flag.Parsed() {
+ log.Fatalln("flag.Parsed() == false")
+ }
+
+ if *writeMarkdown {
+ UsageMarkdown()
+ return
+ }
+
+ kvpArgs, defFileNameArgs := scanForKVArgs(flag.Args())
+ for _, filename := range defFileNameArgs {
+ kvpArg := scanValueFile(filename)
+ kvpArgs[kvpArg.Key] = kvpArg.Value
+
+ }
+ templateText := getTemplate(os.Stdin)
+ ExpandTemplate(kvpArgs, templateText)
+ os.Exit(exitStatus)
+}
+
+var usageAbstract = `
+ Key=Value
+ Sh-style name=value definition string pairs. The Key name must be
+ valid as a Go map Key acceptable to Go's template
+ package https://pkg.go.dev/text/template
+
+ ValueFilePath
+ File named on the command line containing a possibly multi-line
+ definition of a single 'Value', with its 'Key' derived from the base name of the file.
+ All non-alphanumeric characters in the basename are mapped to "_", to ensure their acceptability as
+ Go template keys.
+
+ TemplateFile
+ A stream read from stdin format template containing references to
+ the 'Key' side of the above pairs.
+
+ ExpansionFile
+ Written to stdout, the expansion of the input template read from stdin.
+
+---
+Example:
+
+ echo >/tmp/valueFile.txt '
+ . +-------+
+ . | a box |
+ . +-------+'
+ echo '
+ . A sentence referencing Key 'boxShape' with Value '{{.boxShape}}', read
+ . from the command line.
+ .
+ . An introductory clause followed by a multi-line block of text,
+ . read from a file:
+ . {{.valueFile}}' |
+ tmpl-expand boxShape='RECTANGULAR' /tmp/valueFile.txt
+
+Result:
+ . A sentence referencing Key boxShape with Value RECTANGULAR, read
+ . from the command line.
+ .
+ . An introductory clause followed by a multi-line block of text,
+ . read from a file:
+ .
+ . +-------+
+ . | a box |
+ . +-------+
+`
+
+func writeUsage(out io.Writer, premable string) {
+ fmt.Fprintf(out, "%s%s", premable,
+ `Usage:
+ tmpl-expand [-markdown] [ Key=Value | ValueFilePath ] ... ExpansionFile
+`)
+ flag.PrintDefaults()
+ fmt.Fprintf(out, "%s\n", usageAbstract)
+}
+
+func UsageDump() {
+ writeUsage(os.Stderr, "")
+}
+
+func scanForKVArgs(args []string) (
+ kvpArgs map[string]string, filenameArgs []string) {
+ kvpArgs = make(map[string]string)
+ for _, arg := range args {
+ kvp := strings.Split(arg, "=")
+ if len(kvp) != 2 {
+ filenameArgs = append(filenameArgs, kvp[0])
+ continue
+ }
+ newKvpArg := newKVPair(kvp)
+
+ // Search earlier Keys for duplicates.
+ // XX N^2 in number of Keys -- use a map instead?
+ for k := range kvpArgs {
+ if k == newKvpArg.Key {
+ log.Printf("Duplicate key specified: '%v', '%v'", kvp, newKvpArg)
+ exitStatus = 1
+ }
+ }
+ kvpArgs[newKvpArg.Key] = newKvpArg.Value
+ }
+ return
+}
+
+func newKVPair(newKvp []string) KvpArg {
+ vetKVstring(newKvp)
+ return KvpArg{
+ Key: newKvp[0],
+ Value: newKvp[1],
+ }
+}
+
+func vetKVstring(kv []string) {
+ reportFatal := func(format string) {
+ // X X Caller disappears from stack, apparently due to inlining, despite
+ // disabling Go optimizer
+ //caller := func(howHigh int) string {
+ // pc, file, line, ok := runtime.Caller(howHigh)
+ // _ = pc
+ // if !ok {
+ // return ""
+ // }
+ // baseFileName := file[strings.LastIndex(file, "/")+1:]
+ // return baseFileName + ":" + strconv.Itoa(line)
+ //}
+ log.Printf(format, kv)
+ log.Fatalln("FATAL")
+ }
+ if len(kv[0]) <= 0 {
+ reportFatal("Key side of Key=Value pair empty: %#v\n")
+ }
+ if len(kv[1]) <= 0 {
+ reportFatal("Value side of Key=Value pair empty: %#v\n")
+ }
+}
+
+var alnumOnlyRE = regexp.MustCompile(`[^a-zA-Z0-9]`)
+
+func scanValueFile(keyPath string) KvpArg {
+ valueFile, err := os.Open(keyPath)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ bytes, err := io.ReadAll(valueFile)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ basename := path.Base(keyPath)
+ return KvpArg{
+ Key: alnumOnlyRE.ReplaceAllLiteralString(basename, "_"),
+ Value: string(bytes),
+ }
+}
+
+//func getTemplate(infile *os.File) (int, string) {
+func getTemplate(infile *os.File) string {
+ var err error
+ var stat os.FileInfo
+ stat, err = infile.Stat()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ templateText := make([]byte, stat.Size())
+ var nRead int
+ templateText, err = io.ReadAll(infile)
+ nRead = len(templateText)
+ if nRead <= 0 {
+ log.Fatalf("os.Read returned %d bytes", nRead)
+ }
+ if err = infile.Close(); err != nil {
+ log.Fatalf("Could not close %v, err=%v", infile, err)
+ }
+ return string(templateText)
+}
+
+func ExpandTemplate(kvpArgs map[string]string, templateText string) {
+
+ ctx := TemplateContext{
+ substitutionsMap: kvpArgs,
+ }
+
+ var err error
+ ctx.tmpl, err = template.New("" /*baseFile*/).Option("missingkey=error").
+ Parse(templateText)
+ if err != nil {
+ log.Printf("Failed to parse '%s'", templateText)
+ log.Fatalln(err)
+ }
+ ctx.writeFile()
+}
+
+func (ctx *TemplateContext) writeFile() {
+ if err := ctx.tmpl.Execute(os.Stdout, ctx.substitutionsMap); err != nil {
+ fmt.Fprintf(os.Stderr, "Template.Execute(outfile, map) returned err=\n %v\n",
+ err)
+ fmt.Fprintf(os.Stderr, "Contents of failing map:\n%s", ctx.formatMap())
+ exitStatus = 1
+ }
+ if err := os.Stdout.Close(); err != nil {
+ log.Fatal(err)
+ }
+ return
+}
+
+// Sort the output, for deterministic comparisons of build failures.
+func (ctx *TemplateContext) formatMap() (out string) {
+ alphaSortMap(ctx.substitutionsMap,
+ func(s string) {
+ v := ctx.substitutionsMap[s]
+ const TRIM = 80
+ if len(v) > TRIM {
+ v = v[:TRIM] + "..."
+ }
+ out += fmt.Sprintf(" % 20s '%v'\n\n", s, v)
+ })
+ return
+}
+
+func alphaSortMap(m map[string]string, next func(s string)) {
+ var h sort.StringSlice
+ for k, _ := range m {
+ h = append(h, k)
+ }
+ h.Sort()
+ for _, s := range h {
+ next(s)
+ }
+}
diff --git a/cmd/tmpl-expand/markdown.go b/cmd/tmpl-expand/markdown.go
new file mode 100644
index 0000000..57b1471
--- /dev/null
+++ b/cmd/tmpl-expand/markdown.go
@@ -0,0 +1,46 @@
+// Copyright 2022 Donald Mullis. All rights reserved.
+
+package main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+func UsageMarkdown() {
+ var bytes strings.Builder
+ flag.CommandLine.SetOutput(&bytes)
+
+ writeUsage(&bytes, `
+
+`)
+ indentedTextToMarkdown(bytes)
+}
+
+var column1Regex = regexp.MustCompile(`^[A-Z]`)
+const column1AtxHeading = " ### "
+
+var column3Regex = regexp.MustCompile(`^ [^ ]`)
+const column3AtxHeading = " #### "
+// https://github.github.com/gfm/#atx-headings
+
+// writes to stdout
+func indentedTextToMarkdown(bytes strings.Builder) {
+ scanner := bufio.NewScanner(strings.NewReader(bytes.String()))
+ for scanner.Scan() {
+ line := scanner.Text()
+ if column1Regex.MatchString(line) {
+ line = column1AtxHeading + line
+ } else if column3Regex.MatchString(line) {
+ line = column3AtxHeading + line
+ }
+ fmt.Println(line)
+ }
+}
diff --git a/examples/arrows.svg b/examples/arrows.svg
index 66f01a6..f0fcafa 100644
--- a/examples/arrows.svg
+++ b/examples/arrows.svg
@@ -1,12 +1,12 @@
-