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 @@ - + @@ -18,5 +18,13 @@ svg { + diff --git a/examples/big-grids.svg b/examples/big-grids.svg index c37cd6e..fb32bfd 100644 --- a/examples/big-grids.svg +++ b/examples/big-grids.svg @@ -1,12 +1,12 @@ - + @@ -109,11 +109,19 @@ svg { -A -B -A -B -A -B + +B +A +B +A +B +A diff --git a/examples/big-shapes.svg b/examples/big-shapes.svg index 7d5ea81..4a54725 100644 --- a/examples/big-shapes.svg +++ b/examples/big-shapes.svg @@ -1,12 +1,12 @@ - + @@ -46,5 +46,13 @@ svg { + diff --git a/examples/circle.svg b/examples/circle.svg index 2fca348..54d61df 100644 --- a/examples/circle.svg +++ b/examples/circle.svg @@ -1,12 +1,12 @@ - + @@ -14,5 +14,13 @@ svg { + diff --git a/examples/circuits.svg b/examples/circuits.svg index 62c5ebe..e1ccde2 100644 --- a/examples/circuits.svg +++ b/examples/circuits.svg @@ -1,12 +1,12 @@ - + @@ -17,7 +17,7 @@ svg { - + @@ -26,16 +26,16 @@ svg { - + - + - + @@ -54,7 +54,7 @@ svg { - + @@ -64,18 +64,18 @@ svg { - + - - - - - - - + + + + + + + @@ -96,6 +96,7 @@ svg { + @@ -105,33 +106,42 @@ svg { - - - - - + + - + - + - + -A -B -C -Y + +A +B +. +. +Y +) +) +C diff --git a/examples/complicated.svg b/examples/complicated.svg index cea4428..5ebca64 100644 --- a/examples/complicated.svg +++ b/examples/complicated.svg @@ -1,12 +1,12 @@ - + @@ -64,8 +64,8 @@ svg { - - + + @@ -100,11 +100,11 @@ svg { - + - - + + @@ -141,8 +141,8 @@ svg { - - + + @@ -152,8 +152,8 @@ svg { - - + + @@ -234,8 +234,8 @@ svg { - - + + @@ -244,181 +244,189 @@ svg { -& -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 + +A +B +o +x +R +o +u +n +d +M +i +x +e +d +R +o +u +n +d +e +d +D +i +a +g +o +n +a +l +s +& +S +q +u +a +r +e +C +o +r +n +e +r +s +S +e +a +r +c +h +I +n +t +e +r +i +o +r +D +i +a +g +l +i +n +e +i +f +( +a +> +b +) +C +u +r +v +e +d +l +i +n +e +o +b +j +- +> +f +c +n +( +) +D +o +n +e +? +J +o +i +n +C +u +r +v +e +d +V +e +r +t +i +c +a +l +3 +n +o +t +: +l +i +n +e +' +q +u +o +t +e +s +' +A +B +* +b +o +l +d +* +N +o +t +a +d +o +t +A +d +a +s +h +- +- +i +s +n +o +t +a +l +i +n +e +N +o +r +/ +i +s +t +h +i +s +. diff --git a/examples/dot-grids.svg b/examples/dot-grids.svg index 6b286e2..9648aaf 100644 --- a/examples/dot-grids.svg +++ b/examples/dot-grids.svg @@ -1,40 +1,40 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -61,49 +61,49 @@ svg { - - - + + + - + - - - - + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -123,68 +123,76 @@ svg { - + - - - - - + + + + + - - + + - - - - + + + + - - + + - + - - + + -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· -· + +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· diff --git a/examples/edge-cases.svg b/examples/edge-cases.svg index 6889980..df810d6 100644 --- a/examples/edge-cases.svg +++ b/examples/edge-cases.svg @@ -1,12 +1,12 @@ - + @@ -74,6 +74,14 @@ svg { - + + diff --git a/examples/flow-chart.svg b/examples/flow-chart.svg index 8ad4e05..4a84530 100644 --- a/examples/flow-chart.svg +++ b/examples/flow-chart.svg @@ -1,12 +1,12 @@ - + @@ -89,67 +89,75 @@ svg { -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 + +S +T +A +R +T +A +B +C +O +M +P +L +E +X +E +N +D +C +H +O +I +C +E +P +R +E +P +A +R +A +T +I +O +N +X +P +R +O +C +E +S +S +I +N +P +U +T +P +R +O +C +E +S +S +P +R +O +C +E +S +S +X diff --git a/examples/graphics.svg b/examples/graphics.svg index dcdbf6e..e2ee3ba 100644 --- a/examples/graphics.svg +++ b/examples/graphics.svg @@ -1,12 +1,12 @@ - + @@ -22,7 +22,7 @@ svg { - + @@ -39,12 +39,12 @@ svg { - + - + @@ -70,59 +70,67 @@ svg { - + - - - -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 + + + + +0 +3 +P +E +y +e ++ +y +R +e +f +l +e +c +t +i +o +n +1 +2 +v +0 +v +3 +4 +7 ++ +x +X +R +e +f +r +a +c +t +i +o +n +5 +6 ++ +z +v +1 +v +2 diff --git a/examples/icons.svg b/examples/icons.svg index 372fcdc..dd32e66 100644 --- a/examples/icons.svg +++ b/examples/icons.svg @@ -1,12 +1,12 @@ - + @@ -126,118 +126,126 @@ svg { - -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 + + +S +e +r +v +e +r +C +l +o +u +d +D +a +t +a +b +a +s +e +I +n +t +e +r +n +e +t +W +i +F +i +B +l +u +e +t +o +o +t +h +# +# +# +# +L +A +N +W +i +n +d +o +w +s +O +S +X +i +O +S +U +b +u +n +t +u +U +b +u +n +t +u +L +a +p +t +o +p +1 +L +a +p +t +o +p +2 +T +a +b +l +e +t +1 +D +e +d +i +c +a +t +e +d +S +e +r +v +e +r +R +a +c +k diff --git a/examples/large-nodes.svg b/examples/large-nodes.svg index e2ee79b..7b7c174 100644 --- a/examples/large-nodes.svg +++ b/examples/large-nodes.svg @@ -1,12 +1,12 @@ - + @@ -98,17 +98,25 @@ svg { -A -1 -2 -3 -4 -B -5 -C -6 -8 -7 -D + +A +1 +2 +4 +8 +3 +B +5 +C +6 +7 +D diff --git a/examples/line-decorations.svg b/examples/line-decorations.svg index 9e3a84f..8275285 100644 --- a/examples/line-decorations.svg +++ b/examples/line-decorations.svg @@ -1,12 +1,12 @@ - + @@ -24,8 +24,8 @@ svg { - - + + @@ -38,9 +38,9 @@ svg { - - - + + + @@ -55,8 +55,8 @@ svg { - - + + @@ -96,18 +96,26 @@ svg { - - - + + + - + - + + diff --git a/examples/line-ends.svg b/examples/line-ends.svg index 6492932..5552eb6 100644 --- a/examples/line-ends.svg +++ b/examples/line-ends.svg @@ -1,28 +1,28 @@ - + - - - + + + - + - + - + - + @@ -34,16 +34,16 @@ svg { - - - + + + - + - + - - + + @@ -54,17 +54,17 @@ svg { - - - - + + + + - + - + - + @@ -74,17 +74,17 @@ svg { - + - + - + - - - + + + - + @@ -138,79 +138,79 @@ svg { - - - - + + + + - + - - + + - + - + - - + + - + - - - - - + + + + + - - - + + + - - + + - - - - - + + + + + - + - - + + - - - - + + + + - - - - - - + + + + + + @@ -218,5 +218,13 @@ svg { + diff --git a/examples/overlaps.svg b/examples/overlaps.svg index 3dc6238..c28f1aa 100644 --- a/examples/overlaps.svg +++ b/examples/overlaps.svg @@ -1,12 +1,12 @@ - + @@ -110,5 +110,13 @@ svg { + diff --git a/examples/regression.svg b/examples/regression.svg new file mode 100644 index 0000000..5d26ed7 --- /dev/null +++ b/examples/regression.svg @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +a +a +a +a +a +a +# +1 +1 +# +1 +1 +t +t +t +t +a +a +a +o +o +x +v +v +o +v +# +1 +v +o +# +1 +v +o +w +# +1 +o +v +e +r +# +1 +m +o +v +e +e +v +o +e +o +v +v +o +e +# +1 +X +A +n +y +f +i +x +t +o +# +1 +m +u +s +t +c +o +n +t +i +n +u +e +t +o +t +r +e +a +t +" +o +" +a +s +a +c +i +r +c +l +e +, +n +o +t +a +l +e +t +t +e +r +. +. +. +o +. +v +v +. +o +o +. +v +e +v +. +o +e +a +v +. +a +o +. +A +l +i +c +e +v +. +C +L +S +# +1 +A +l +i +c +e +v +s +. +C +L +S +a +a +a +b +o +b +a +v +a +a +v +a +a +a +o +a +a +o + + diff --git a/examples/regression.txt b/examples/regression.txt new file mode 100644 index 0000000..d1ab783 --- /dev/null +++ b/examples/regression.txt @@ -0,0 +1,165 @@ + +- a + | a + + +- a + | a + ++- a +| a + + +- + | + + -+ + | + + .- + | + + -. + | + + + +-+ + + + + +-+ #11 + + +-+ + | | + +-+ #11 + + + .-. + | | + '-' + +> +-+ -+ +-+ +> +-+ +-+ +- + _ _ +> |_| |_ + + _ +> '---._ + + _ +> _.---' + + + _____ ___ + / t / | t | ___ + /____/ |___| _|_ + + _____ .-----+ + \ \ .---. | t | + \____\ | t | +-----. + '---' + + ____ +--+ +---+ + |__| +--+ | a +-> + +---+ + + +---+ _____ ____ + |___| | a | \ a \ + +---+ +---+ + + oo x + + o + o + + vv + + ^ + v + + ov #1 + + vo #1 + + vow #1 + + over #1 + + move + + evo + + eov + + voe #1 + + \ | / + vvv + -->o<-- X Any fix to #1 must continue to treat "o" as a circle, not a letter. + ^^^ + / | \ + +.. + + . . . + + o.v + + v.o + + o.ve + + v.oe + + av. + + ao. + + Alice v. CLS #1 + + Alice vs. CLS + + o + + o + a + + a + o + + o> + + + + < + + ^ + + v + + va + + av + + a + v + + | + va + + | + av + +--- + + oa + + ao + diff --git a/examples/small-grids.svg b/examples/small-grids.svg index 4e248fc..d652a14 100644 --- a/examples/small-grids.svg +++ b/examples/small-grids.svg @@ -1,12 +1,12 @@ - + @@ -162,13 +162,21 @@ svg { -a -b -a -b -a -b -a -b + +b +b +a +b +a +b +a +a diff --git a/examples/small-nodes.svg b/examples/small-nodes.svg index 706842b..3ffda2d 100644 --- a/examples/small-nodes.svg +++ b/examples/small-nodes.svg @@ -1,38 +1,38 @@ - + - - + + - - + + - + - + - - + + - - - + + + @@ -64,42 +64,50 @@ svg { - - - - - + + + + + - - - - + + + + - + - - - + + + -A -D -E -B -F -C -G -A -1 -2 -3 -4 -B -5 -C -6 -8 -7 -D + +A +1 +2 +4 +8 +A +B +C +D +E +F +G +3 +B +5 +C +6 +7 +D diff --git a/examples/small-shapes.svg b/examples/small-shapes.svg index 45703a2..ef37944 100644 --- a/examples/small-shapes.svg +++ b/examples/small-shapes.svg @@ -1,12 +1,12 @@ - + @@ -41,10 +41,9 @@ svg { - - + - + @@ -84,5 +83,17 @@ svg { + +. +. +. +. diff --git a/examples/tiny-grids.svg b/examples/tiny-grids.svg index 75dc3aa..db3ef85 100644 --- a/examples/tiny-grids.svg +++ b/examples/tiny-grids.svg @@ -1,260 +1,232 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 diff --git a/examples/tiny-grids.txt b/examples/tiny-grids.txt index 95b1bc1..c4700ff 100644 --- a/examples/tiny-grids.txt +++ b/examples/tiny-grids.txt @@ -1,5 +1,24 @@ - ┌─┬─┬─┬─┬─┐ ▉▉ ▉▉ ▉▉ ⬢ ⬡ ⬡ ┌┬┬┬┬┬┬┬┬┐ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ ___________ +-+-+-+-+ - ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ - ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ▉▉ ⬢ ⬢ ⬢ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ - ├─┼─┼─┼─┼─┤ ▉▉ ▉▉ ⬡ ⬡ ⬡ ⬡ ├┼┼┼┼┼┼┼┼┤ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ - └─┴─┴─┴─┴─┘ ▉▉ ▉▉ ▉▉ ⬡ ⬡ ⬡ └┴┴┴┴┴┴┴┴┘ ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ |__|__|__|__| +-+-+-+-+ +0123456789012345 + ┌─┬─┬─┬─┬─┐ | + ├─┼─┼─┼─┼─┤ | + ├─┼─┼─┼─┼─┤ | + ├─┼─┼─┼─┼─┤ | + └─┴─┴─┴─┴─┘ | + | + ┌┬┬┬┬┬┬┬┬┐ | + ├┼┼┼┼┼┼┼┼┤ | + ├┼┼┼┼┼┼┼┼┤ | + ├┼┼┼┼┼┼┼┼┤ | + └┴┴┴┴┴┴┴┴┘ | + ___________ | + |__|__|__|__| | + |__|__|__|__| | + |__|__|__|__| | + |__|__|__|__| | + | + +-+-+-+-+ | + +-+-+-+-+ | + +-+-+-+-+ | + +-+-+-+-+ | + +-+-+-+-+ | +0123456789012345 diff --git a/examples/trees.svg b/examples/trees.svg index fa493ea..f1d8101 100644 --- a/examples/trees.svg +++ b/examples/trees.svg @@ -1,12 +1,12 @@ - + @@ -74,29 +74,37 @@ svg { -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 + +1 +1 +1 +2 +2 +2 +3 +3 +3 +1 +2 +3 +4 +1 +2 +3 +4 +1 +2 +3 +4 +4 +4 +4 diff --git a/examples/unicode.svg b/examples/unicode.svg index ffe4032..360b84f 100644 --- a/examples/unicode.svg +++ b/examples/unicode.svg @@ -1,214 +1,3069 @@ - + - - - - - - - - - - -x - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -t - - -² - - -d - - -t - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +x + + +t +² +d +t + + + + +² +0 +x +x +p +a +g +e +: +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¤ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +¨ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +´ +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +° +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +º +¹ +² +³ +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +« +» +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +¯ +¯ +  +  +¦ +¦ +­ +­ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +× +÷ +ø +Ø +± +± +¡ +¡ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +3 +0 +x +p +a +g +e +: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +U +n +u +s +a +b +l +e +c +h +a +r +a +c +t +e +r +s +, +d +u +e +t +o +n +o +n +- +s +t +a +n +d +a +r +d +d +i +m +e +n +s +i +o +n +s +i +n +f +o +n +t +s +: +D +e +j +a +V +u +S +a +n +s +M +o +n +o +F +r +e +e +M +o +n +o +U +b +u +n +t +u +M +o +n +o +M +o +n +o +S +p +a +c +e +a +s +r +e +n +d +e +r +e +d +b +y +G +N +U +e +m +a +c +s +: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +2 +x +x +p +a +g +e +: +˂ +˃ +˂ +˃ +˂ +˃ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +C +h +a +r +a +c +t +e +r +s +b +e +l +o +w +a +r +e +h +e +r +e +f +o +r +v +e +r +i +f +i +c +a +t +i +o +n +o +f +s +t +a +n +d +a +r +d +w +i +d +t +h +i +n +. +t +x +t +f +i +l +e +; +t +h +e +y +w +i +l +l +n +o +t +p +a +s +s +t +h +r +o +u +g +h +t +o +u +n +i +c +o +d +e +. +s +v +g +o +u +t +p +u +t +. +U +n +u +s +a +b +l +e +f +o +n +t +s +, +d +u +e +t +o +n +o +n +- +s +t +a +n +d +a +r +d +d +i +m +e +n +s +i +o +n +s +o +f +S +U +B +S +C +R +I +P +T +Z +E +R +O +' + +' +t +h +r +o +u +g +h +S +U +B +S +C +R +I +P +T +N +I +N +E +' + +' +c +h +a +r +a +c +t +e +r +s +i +n +f +o +n +t +s +L +i +b +e +r +a +t +i +o +n +M +o +n +o +N +o +t +o +M +o +n +o +R +e +g +u +l +a +r +a +s +r +e +n +d +e +r +e +d +b +y +G +N +U +e +m +a +c +s +. +2 +0 +x +x +p +a +g +e +: +# + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +3 +0 +x +x +p +a +g +e +: +# +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 diff --git a/examples/unicode.txt b/examples/unicode.txt index 276e459..4bf4b8e 100644 --- a/examples/unicode.txt +++ b/examples/unicode.txt @@ -6,3 +6,64 @@ ▼ ╰╌╌╌╌╌╌╌╯ ╚═══════╝ ┗━━━━━━━┛ ⋱ ┗╍╍╍╍╍╍╍┛⋮ ⎝ ⎩ ⎣╱ ╲ ░░▒▒▓▓▉▉ ▚▚ ◯ ◯ ⏣ ⏣ ⊛ ⋱ ⋮ ◢▉▉◣ ⊜ ∑xᵢ ∫t²dt ⋱ ⋮ ◥▉▉◤ + +² + +0xx page: + +········· ········· ········· ········· ········· ········· ········· ········· ········· ········· ········· +¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ ¤¤¤¤¤¤¤¤¤ +¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ ¨¨¨¨¨¨¨¨¨ +´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ ´´´´´´´´´ +°°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° °°°°°°°°° +º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ º¹²³ +«»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» «»«»«»«» +¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ ¯¯  ¦¦­­ +×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ ×÷øØ ±±¡¡ +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + + +30x page: +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω + +Unusable characters, due to non-standard dimensions in fonts: + DejaVu Sans Mono + FreeMono + Ubuntu Mono + MonoSpace +as rendered by GNU emacs: + +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +⬣ ⬣ ⎔ ⎔ +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ ⊘⊘⊘⊘⊘⊘⊘⊘ +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯╰╌╌╌╌╌╌╌╯ +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ ✶✶✶✶✶✶✶✶ +✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ ✹✹✹✹✹✹✹✹ +⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ ⬣⬣⬣⬣⬣⬣⬣⬣ +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + + +02xx page: +˂ ˃ ˂ ˃ ˂ ˃ +0123456789 + +Characters below are here for verification of standard width in .txt file; they +will not pass through to unicode.svg output. + +Unusable fonts, due to non-standard dimensions of SUBSCRIPT ZERO '₀' through +SUBSCRIPT NINE '₉' characters in fonts + Liberation Mono + Noto Mono Regular +as rendered by GNU emacs. + +20xx page: +# ₀₁₂₃₄₅₆₇₈₉ ⁴⁵⁶⁷⁸⁹ +01234567890123456789 + +30xx page: +# αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω αβγδεζηθικλμνξοπρςστυφχψω +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 diff --git a/examples_test.go b/examples_test.go index b7bcbd8..daec3cc 100644 --- a/examples_test.go +++ b/examples_test.go @@ -13,8 +13,15 @@ import ( qt "github.com/frankban/quicktest" ) -var write = flag.Bool("write", false, "write examples to disk") +var ( + write = flag.Bool("write", false, "write examples to disk") + svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000", + `See help for cmd/goat`) + svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF", + `See help for cmd/goat`) +) +// XX TXT source file suite is limited to a single file -- "circuits.txt" func TestExamplesStableOutput(t *testing.T) { c := qt.New(t) @@ -36,15 +43,18 @@ func TestExamplesStableOutput(t *testing.T) { } func TestExamples(t *testing.T) { - c := qt.New(t) - filenames, err := filepath.Glob(filepath.Join(basePath, "*.txt")) - c.Assert(err, qt.IsNil) + if err != nil { + t.Fatal(err) + } var buff *bytes.Buffer for _, name := range filenames { in := getIn(name) + if testing.Verbose() { + t.Logf("\tprocessing %s\n", name) + } var out io.WriteCloser if *write { out = getOut(name) @@ -63,19 +73,27 @@ func TestExamples(t *testing.T) { } } - svgColorLightScheme := "#323232" - svgColorDarkScheme := "#C8C8C8" - BuildAndWriteSVG(in, out, svgColorLightScheme, svgColorDarkScheme) + BuildAndWriteSVG(in, out, *svgColorLightScheme, *svgColorDarkScheme) in.Close() out.Close() if buff != nil { - golden := getOutString(name) + golden, err := getOutString(name) + if err != nil { + t.Log(err) + } if buff.String() != golden { - c.Log(buff.Len(), len(golden)) - c.Fatalf("Content mismatch for %s", name) - + // XX Skip this if the modification timestamp of the .txt file + // source is fresher than the .svg? + t.Log(buff.Len(), len(golden)) + t.Logf("Content mismatch for %s", toSVGFilename(name)) + t.Logf("%s %s:\n\t%s\nConsider:\n\t%s", + "Option -write not set, and Error reading", + name, + err.Error(), + "$ go test -run TestExamples -v -args -write") + t.FailNow() } in.Close() out.Close() @@ -109,13 +127,13 @@ func getOut(filename string) io.WriteCloser { return out } -func getOutString(filename string) string { +func getOutString(filename string) (string, error) { b, err := ioutil.ReadFile(toSVGFilename(filename)) if err != nil { - panic(err) + return "", err } b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n")) - return string(b) + return string(b), nil } func toSVGFilename(filename string) string { diff --git a/goat.go b/goat.go new file mode 100644 index 0000000..df874ac --- /dev/null +++ b/goat.go @@ -0,0 +1,48 @@ +/* +Package goat formats "ASCII-art" drawings into Github-flavored Markdown. + + + porcelain API + BuildAndWriteSVG() + .----------. + ASCII-art | | Markdown + ----------------------->| +-------------------------> + | | + '----------' + · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · + plumbing API + + Canvas{} + NewCanvas() .-------------------. WriteSVGBody() + | | .-------. + ASCII-art .--. | data map[x,y]rune | | SVG{} | Markdown + ---------->| +--->| text map[x,y]rune +-->| +-------> + '--' | | | | + '-------------------' '-------' + +*/ +package goat + +import ( + "bytes" + "io" +) + +// 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, + svgColorLightScheme, svgColorDarkScheme string) { + svg := buildSVG(src) + writeBytes(dst, svg.String(svgColorLightScheme, svgColorDarkScheme)) +} + +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(), + } +} diff --git a/goat.svg b/goat.svg new file mode 100644 index 0000000..ee43859 --- /dev/null +++ b/goat.svg @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +p +o +r +c +e +l +a +i +n +A +P +I +B +u +i +l +d +A +n +d +W +r +i +t +e +S +V +G +( +) +A +S +C +I +I +- +a +r +t +M +a +r +k +d +o +w +n +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +· +p +l +u +m +b +i +n +g +A +P +I +C +a +n +v +a +s +{ +} +N +e +w +C +a +n +v +a +s +( +) +W +r +i +t +e +S +V +G +B +o +d +y +( +) +A +S +C +I +I +- +a +r +t +d +a +t +a +m +a +p +[ +x +, +y +] +r +u +n +e +S +V +G +{ +} +M +a +r +k +d +o +w +n +t +e +x +t +m +a +p +[ +x +, +y +] +r +u +n +e + + diff --git a/index.go b/index.go index 6061857..3e52bfd 100644 --- a/index.go +++ b/index.go @@ -2,50 +2,53 @@ package goat // Index represents a position within an ASCII diagram. type Index struct { - x int - y int + // units of cells + X, Y int } -// Pixel represents the on-screen coordinates for an Index. -type Pixel Index +// pixel represents the CSS-pixel coordinates for an Index. +type pixel Index // XX different units -- create separate base type? -func (i *Index) asPixel() Pixel { - return Pixel{x: i.x * 8, y: i.y * 16} +func (i *Index) asPixel() pixel { + // TODO define constants rather than hard-wire width and height of cell + return pixel{ + X: i.X * 8, + Y: i.Y * 16} } func (i *Index) asPixelXY() (int, int) { p := i.asPixel() - return p.x, p.y + return p.X, p.Y } func (i *Index) east() Index { - return Index{i.x + 1, i.y} + return Index{i.X + 1, i.Y} } func (i *Index) west() Index { - return Index{i.x - 1, i.y} + return Index{i.X - 1, i.Y} } func (i *Index) north() Index { - return Index{i.x, i.y - 1} + return Index{i.X, i.Y - 1} } func (i *Index) south() Index { - return Index{i.x, i.y + 1} + return Index{i.X, i.Y + 1} } func (i *Index) nWest() Index { - return Index{i.x - 1, i.y - 1} + return Index{i.X - 1, i.Y - 1} } func (i *Index) nEast() Index { - return Index{i.x + 1, i.y - 1} + return Index{i.X + 1, i.Y - 1} } func (i *Index) sWest() Index { - return Index{i.x - 1, i.y + 1} + return Index{i.X - 1, i.Y + 1} } func (i *Index) sEast() Index { - return Index{i.x + 1, i.y + 1} + return Index{i.X + 1, i.Y + 1} } diff --git a/iter.go b/iter.go index e67597d..6b1e7f4 100644 --- a/iter.go +++ b/iter.go @@ -23,7 +23,7 @@ func leftRight(width int, height int) chan Index { // Transpose an upDown order. go func() { for i := range upDown(height, width) { - c <- Index{i.y, i.x} + c <- Index{i.Y, i.X} // X transpose x and y } close(c) diff --git a/iter_test.go b/iter_test.go index abac724..00b3e43 100644 --- a/iter_test.go +++ b/iter_test.go @@ -9,7 +9,7 @@ import ( var eq = qt.CmpEquals( cmp.Comparer(func(i1, i2 Index) bool { - return i1.x == i2.x && i1.y == i2.y + return i1.X == i2.X && i1.Y == i2.Y }), ) @@ -33,7 +33,7 @@ func TestIterators(t *testing.T) { }, }, - // LeftRight + // leftRight // 1 2 // 3 4 { diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..c26f84b --- /dev/null +++ b/make.sh @@ -0,0 +1,73 @@ +#! /bin/sh +# +# Run all tests, and all pre-compilation build steps. +# Certain output files should be committed to the SCM archive. +# +# Recall that 'go get ...' performs compilation-proper for go, within +# the user's local environment. + +set -e +set -x +usage () { + printf "%s\n\n" "$*" + printf "usage: %s [-g GitHub_Username] [-w]\n" ${0##*/} + printf "\t%s\t%s\n" "" + printf "\t%s\t%s\n" "$*" + exit 1 +} + +# Define colors for SVG ~foreground~ seen on Github front page. +svg_color_dark_scheme="#EEF" +svg_color_light_scheme="#011" + +GOMOD=$(go env GOMOD) +from_username=${GOMOD##*github.com/} +githubuser=${from_username%%/*} + +TEST_ARGS= + +while getopts hg:iw flag +do + case $flag in + h) usage "";; + g) githubuser=${OPTARG};; # Override guess based on GOMOD + w) TEST_ARGS=${TEST_ARGS}" -write";; + \?) usage "unrecognized option flag";; + esac +done + +tmpl_expand () { + go run ./cmd/tmpl-expand Root="." GithubUser=${githubuser} "$@" +} + +#tmpl_expand go.mod +#tmpl_expand <./cmd/goat/main.tmpl.go >./cmd/goat/main.go + +# SVG examples/ regeneration. +# +# If the command fails due to expected changes in SVG output, rerun +# this script with "TEST_ARGS=-write" first on the command line. +# X Results are used as "golden" standard for GitHub-side regression tests -- +# so arguments here must not conflict with those in "test.yml". +go test -run . -v \ + ${TEST_ARGS} + +# build README.md +tmpl_expand README.md \ + $(bash -c 'echo ./examples/{trees,overlaps,line-decorations,line-ends,dot-grids,large-nodes,small-grids,big-grids,complicated}.{txt,svg}') + +# '-d' writes ./awkvars.out +cat *.go | + awk ' + /[<]goat[>]/ {p = 1; next} + /[<][/]goat[>]/ {p = 0; next} + p > 0 {print}' | + tee goat.txt | + go run ./cmd/goat \ + -svg-color-dark-scheme ${svg_color_dark_scheme} \ + -svg-color-light-scheme ${svg_color_light_scheme} \ + >goat.svg + +# Render to HTML, for local inspection. +./markdown_to_html.sh README.md >README.html +./markdown_to_html.sh CHANGELOG.md >CHANGELOG.html diff --git a/markdown_to_html.sh b/markdown_to_html.sh new file mode 100755 index 0000000..2d93482 --- /dev/null +++ b/markdown_to_html.sh @@ -0,0 +1,48 @@ +#! /bin/sh +# +# For local test of markdown generation, use a standalone Markdown-to-HTML processer. + +set -e +#set -x + +svg_color_light_scheme="#320" +svg_color_dark_scheme="#FEE" + +# XX An alternative to 'marked', for consideration: +# https://github.com/gomarkdown/mdtohtml + +# See https://github.github.com/gfm/#introduction +# +# The @media query from SVG may be verified in Firefox by switching between Themes +# "Light" and "Dark" in Firefox's "Add-ons Manager". +MARKED_PREAMBLE=' + */ + margin-block-end: 0.8em; + } + code { + font-size: 11pt; + } + +' + +echo "${MARKED_PREAMBLE}" +marked --gfm "$@" diff --git a/svg.go b/svg.go index d836cae..7a416b3 100644 --- a/svg.go +++ b/svg.go @@ -1,82 +1,77 @@ +// All output is buffered into the object SVG, then written to the output stream. package goat import ( - "bytes" "fmt" "io" ) -var HollowCircles bool - type SVG struct { Body string Width int Height int } -func (s SVG) String(svgColorLightScheme string, svgColorDarkScheme string) string { +// See: https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme +func (s SVG) String(svgColorLightScheme, svgColorDarkScheme string) string { style := fmt.Sprintf( ``, svgColorLightScheme, svgColorDarkScheme) return fmt.Sprintf( - "\n" + - "%s\n" + - "%s\n", - "diagram", // XX can this have any effect? + "\n" + + "%s\n" + + "%s\n", "http://www.w3.org/2000/svg", "1.1", s.Height, s.Width, style, s.Body) } -// BuildSVG reads in a newline-delimited ASCII diagram from src and returns an 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, - svgColorLightScheme string, svgColorDarkScheme string) { - svg := BuildSVG(src) - writeBytes(dst, svg.String(svgColorLightScheme, svgColorDarkScheme)) -} - func writeBytes(out io.Writer, format string, args ...interface{}) { bytesOut := fmt.Sprintf(format, args...) _, err := out.Write([]byte(bytesOut)) if err != nil { - panic(nil) + panic(err) + } +} + +func writeText(out io.Writer, canvas *Canvas) { + writeBytes(out, + ` +`) + for _, textObj := range canvas.Text() { + // usual, baseline case + textObj.draw(out) } } // Draw a straight line as an SVG path. -func (l Line) Draw(out io.Writer) { +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 - // --- --- + // | | + // | 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 @@ -85,77 +80,118 @@ func (l Line) Draw(out io.Writer) { // 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 + 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 + start.X -= 4 + stop.X -= 4 + start.Y -= 8 + stop.Y -= 8 case S: - start.y -= 8 - stop.y -= 8 + start.Y -= 8 + stop.Y -= 8 } // Half steps switch l.chop { case N: - stop.y -= 8 + stop.Y -= 8 case S: - start.y += 8 + start.Y += 8 } } if l.needsNudgingDown { - stop.y += 8 + stop.Y += 8 if l.horizontal() { - start.y += 8 + start.Y += 8 } } if l.needsNudgingLeft { - start.x -= 8 + start.X -= 8 } if l.needsNudgingRight { - stop.x += 8 + stop.X += 8 } if l.needsTinyNudgingLeft { - start.x -= 4 + start.X -= 4 if l.orientation == NE { - start.y += 8 + start.Y += 8 } else if l.orientation == SE { - start.y -= 8 + start.Y -= 8 } } if l.needsTinyNudgingRight { - stop.x += 4 + stop.X += 4 if l.orientation == NE { - stop.y -= 8 + stop.Y -= 8 } else if l.orientation == SE { - stop.y += 8 + stop.Y += 8 + } + } + + // If either end is a hollow circle, back off drawing to the edge of the circle, + // rather extending as usual to center of the cell. + const ( + ORTHO = 6 + DIAG_X = 3 // XX By eye, '3' is a bit too much'; '2' is not enough. + DIAG_Y = 5 + ) + if (l.startRune == 'o') { + switch l.orientation { + case NE: + start.X += DIAG_X + start.Y -= DIAG_Y + case E: + start.X += ORTHO + case SE: + start.X += DIAG_X + start.Y += DIAG_Y + case S: + start.Y += ORTHO + default: + panic("impossible orientation") + } + } + // X 'stopRune' case differs from 'startRune' only by inversion of the arithmetic signs. + if (l.stopRune == 'o') { + switch l.orientation { + case NE: + stop.X -= DIAG_X + stop.Y += DIAG_Y + case E: + stop.X -= ORTHO + case SE: + stop.X -= DIAG_X + stop.Y -= DIAG_Y + case S: + stop.Y -= ORTHO + default: + panic("impossible orientation") } } writeBytes(out, "\n", - start.x, start.y, - stop.x, stop.y, + start.X, start.Y, + stop.X, stop.Y, ) } // Draw a solid triangle as an SVG polygon element. -func (t Triangle) Draw(out io.Writer) { +func (t Triangle) draw(out io.Writer) { // https://www.w3.org/TR/SVG/shapes.html#PolygonElement /* @@ -163,13 +199,13 @@ func (t Triangle) Draw(out io.Writer) { | /|\ | | / | \ | x +- / -+- \ -+ - | / | \ | - |/ | \| + | / | \ | + |/ | \| +-----+-----+ - y + y */ - x, y := float32(t.start.asPixel().x), float32(t.start.asPixel().y) + x, y := float32(t.start.asPixel().X), float32(t.start.asPixel().Y) r := 0.0 x0 := x + 8 @@ -261,30 +297,28 @@ func (t Triangle) Draw(out io.Writer) { } // Draw a solid circle as an SVG circle element. -func (c *Circle) Draw(out io.Writer) { +func (c *Circle) draw(out io.Writer) { var fill string if c.bold { fill = "currentColor" } else { - fill = "invert(currentColor)" - if HollowCircles { - fill = "none" - } + fill = "none" } pixel := c.start.asPixel() - + const circleRadius = 6 writeBytes(out, - "\n", - pixel.x, - pixel.y, + "\n", + pixel.X, + pixel.Y, + circleRadius, fill, ) } // Draw a single text character as an SVG text element. -func (t Text) Draw(out io.Writer) { +func (t Text) draw(out io.Writer) { p := t.start.asPixel() - c := t.contents + c := t.str opacity := 0 @@ -309,7 +343,7 @@ func (t Text) Draw(out io.Writer) { if opacity != 0 { writeBytes(out, "", - p.x-4, p.y-8, + p.X-4, p.Y-8, fill, ) return @@ -325,14 +359,15 @@ func (t Text) Draw(out io.Writer) { c = "<" } + // usual case writeBytes(out, - "%s\n", - p.x, p.y+4, c, - ) + `%s +`, + p.X, p.Y+4, c) } // Draw a rounded corner as an SVG elliptical arc element. -func (c *RoundedCorner) Draw(out io.Writer) { +func (c *RoundedCorner) draw(out io.Writer) { // https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands x, y := c.start.asPixelXY() @@ -374,7 +409,7 @@ func (c *RoundedCorner) Draw(out io.Writer) { } // Draw a bridge as an SVG elliptical arc element. -func (b Bridge) Draw(out io.Writer) { +func (b Bridge) draw(out io.Writer) { x, y := b.start.asPixelXY() sweepFlag := 1