From 206d492f6d0b2ef24e4c5ef55cfcca479d44873a Mon Sep 17 00:00:00 2001 From: dmullis Date: Fri, 21 Jun 2024 23:39:31 -0700 Subject: [PATCH 01/16] Address complaints from `shellcheck` --- pre-push.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pre-push.sh b/pre-push.sh index d865ec3..e5f6f64 100755 --- a/pre-push.sh +++ b/pre-push.sh @@ -12,14 +12,14 @@ set -x usage () { set +x printf "%s\n\n" "$*" - printf "usage: %s [-w]\n" ${0##*/} - printf "\t%s\t%s\n" "" - printf "\t%s\t%s\n" "$*" + printf "usage: %s [-w]\n" "${0##*/}" + printf "\t%s\n" "" + printf "\t%s\n" "$*" exit 1 } TEST_ARGS= -while getopts hg:iw flag +while getopts h:w flag do case $flag in h) usage "";; @@ -36,7 +36,7 @@ GITHUB_REPOSITORY_OWNER=$USER CURRENT_BRANCH_NAME=$(git-branch --show-current) # If the current branch name contains the GitHub username of the owner of the upstream repo, # assume the intention is to prepare and push a pull request. -if [ $(expr $CURRENT_BRANCH_NAME : ".*$UPSTREAM_OWNER") != 0 ] +if [ $(expr "$CURRENT_BRANCH_NAME" : ".*$UPSTREAM_OWNER") != 0 ] then GITHUB_REPOSITORY_OWNER=$UPSTREAM_OWNER fi From 047f25c2515b4126862707a7ef831b4b3e618e83 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 17 Jun 2024 11:55:58 -0700 Subject: [PATCH 02/16] Expose issue https://github.com/blampe/goat/issues/25 --- README.md | 14 ++++---- examples/small-grids.svg | 78 +++++++++++++++++++++++++++++++++++++++- examples/small-grids.txt | 14 ++++---- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d7eff71..16bda42 100644 --- a/README.md +++ b/README.md @@ -176,13 +176,13 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character, ### Small Grids ``` - ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. - ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | - / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ - \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | - / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ - \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | - \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' + ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. .---. .---. .-. .-. 0 + ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | | A | | B | | A | | B | 1 + / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ '---' '---' '-' '-' 2 + \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | .---. .---. .-. .-. 3 + / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ | C | | D | | C | | D | 4 + \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | '---' '---' '-' '-' 5 + \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' 0123456789012345678901234 6 ``` diff --git a/examples/small-grids.svg b/examples/small-grids.svg index d652a14..c2850bf 100644 --- a/examples/small-grids.svg +++ b/examples/small-grids.svg @@ -1,4 +1,4 @@ - + diff --git a/examples/small-grids.txt b/examples/small-grids.txt index 1e95734..7961b21 100644 --- a/examples/small-grids.txt +++ b/examples/small-grids.txt @@ -1,8 +1,8 @@ - ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. - ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | - / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ - \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | - / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ - \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | - \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' + ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. .---. .---. .-. .-. 0 + ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | | A | | B | | A | | B | 1 + / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ '---' '---' '-' '-' 2 + \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | .---. .---. .-. .-. 3 + / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ | C | | D | | C | | D | 4 + \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | '---' '---' '-' '-' 5 + \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' 0123456789012345678901234 6 From 121171bf3b7fc1b8a0ab03f8355c4b8a0d6eae71 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 17 Jun 2024 11:57:58 -0700 Subject: [PATCH 03/16] If browser is in dark-mode, ask for a dark background As shown in Firefox by examples/*.svg. --- examples/arrows.svg | 1 + examples/big-grids.svg | 1 + examples/big-shapes.svg | 1 + examples/circle.svg | 1 + examples/circuits.svg | 1 + examples/complicated.svg | 1 + examples/dot-grids.svg | 1 + examples/edge-cases.svg | 1 + examples/flow-chart.svg | 1 + examples/graphics.svg | 1 + examples/icons.svg | 1 + examples/large-nodes.svg | 1 + examples/line-decorations.svg | 1 + examples/line-ends.svg | 1 + examples/overlaps.svg | 1 + examples/regression.svg | 1 + examples/small-grids.svg | 1 + examples/small-nodes.svg | 1 + examples/small-shapes.svg | 1 + examples/tiny-grids.svg | 1 + examples/trees.svg | 1 + examples/unicode.svg | 1 + goat.svg | 1 + svg.go | 1 + trees.mid-blue.svg | 1 + 25 files changed, 25 insertions(+) diff --git a/examples/arrows.svg b/examples/arrows.svg index f0fcafa..69f9ce9 100644 --- a/examples/arrows.svg +++ b/examples/arrows.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/big-grids.svg b/examples/big-grids.svg index fb32bfd..4a07d1e 100644 --- a/examples/big-grids.svg +++ b/examples/big-grids.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/big-shapes.svg b/examples/big-shapes.svg index 4a54725..a7ed0d5 100644 --- a/examples/big-shapes.svg +++ b/examples/big-shapes.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/circle.svg b/examples/circle.svg index 54d61df..09ea07b 100644 --- a/examples/circle.svg +++ b/examples/circle.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/circuits.svg b/examples/circuits.svg index e1ccde2..9c9b0a8 100644 --- a/examples/circuits.svg +++ b/examples/circuits.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/complicated.svg b/examples/complicated.svg index 5ebca64..20b5498 100644 --- a/examples/complicated.svg +++ b/examples/complicated.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/dot-grids.svg b/examples/dot-grids.svg index 9648aaf..6c46cc7 100644 --- a/examples/dot-grids.svg +++ b/examples/dot-grids.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/edge-cases.svg b/examples/edge-cases.svg index df810d6..aa8370c 100644 --- a/examples/edge-cases.svg +++ b/examples/edge-cases.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/flow-chart.svg b/examples/flow-chart.svg index 4a84530..6f575e7 100644 --- a/examples/flow-chart.svg +++ b/examples/flow-chart.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/graphics.svg b/examples/graphics.svg index e2ee3ba..eb36223 100644 --- a/examples/graphics.svg +++ b/examples/graphics.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/icons.svg b/examples/icons.svg index dd32e66..97f002f 100644 --- a/examples/icons.svg +++ b/examples/icons.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/large-nodes.svg b/examples/large-nodes.svg index 7b7c174..592c524 100644 --- a/examples/large-nodes.svg +++ b/examples/large-nodes.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/line-decorations.svg b/examples/line-decorations.svg index 8275285..3172ecb 100644 --- a/examples/line-decorations.svg +++ b/examples/line-decorations.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/line-ends.svg b/examples/line-ends.svg index 5552eb6..85dd638 100644 --- a/examples/line-ends.svg +++ b/examples/line-ends.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/overlaps.svg b/examples/overlaps.svg index c28f1aa..2db48fc 100644 --- a/examples/overlaps.svg +++ b/examples/overlaps.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/regression.svg b/examples/regression.svg index 5d26ed7..f729e42 100644 --- a/examples/regression.svg +++ b/examples/regression.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/small-grids.svg b/examples/small-grids.svg index c2850bf..ceb2e73 100644 --- a/examples/small-grids.svg +++ b/examples/small-grids.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/small-nodes.svg b/examples/small-nodes.svg index 3ffda2d..6d2cadc 100644 --- a/examples/small-nodes.svg +++ b/examples/small-nodes.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/small-shapes.svg b/examples/small-shapes.svg index ef37944..9a6fe29 100644 --- a/examples/small-shapes.svg +++ b/examples/small-shapes.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/tiny-grids.svg b/examples/tiny-grids.svg index db3ef85..78cc486 100644 --- a/examples/tiny-grids.svg +++ b/examples/tiny-grids.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/trees.svg b/examples/trees.svg index f1d8101..c5c3a78 100644 --- a/examples/trees.svg +++ b/examples/trees.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/examples/unicode.svg b/examples/unicode.svg index 360b84f..a9619c4 100644 --- a/examples/unicode.svg +++ b/examples/unicode.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #FFFFFF; } } diff --git a/goat.svg b/goat.svg index ee43859..7fdc72a 100644 --- a/goat.svg +++ b/goat.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #EEF; } } diff --git a/svg.go b/svg.go index 7a416b3..559d377 100644 --- a/svg.go +++ b/svg.go @@ -21,6 +21,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: %s; } } diff --git a/trees.mid-blue.svg b/trees.mid-blue.svg index a98875e..118fef6 100644 --- a/trees.mid-blue.svg +++ b/trees.mid-blue.svg @@ -5,6 +5,7 @@ svg { } @media (prefers-color-scheme: dark) { svg { + color-scheme: dark; /* ask the browser for a dark background */ color: #2F81F7; } } From de13b04d6a5960ce81902c16854fa50b34d35463 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 17 Jun 2024 12:05:21 -0700 Subject: [PATCH 04/16] Clean up regression test --- examples_test.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/examples_test.go b/examples_test.go index daec3cc..1ac709a 100644 --- a/examples_test.go +++ b/examples_test.go @@ -14,7 +14,7 @@ import ( ) var ( - write = flag.Bool("write", false, "write examples to disk") + write = flag.Bool("write", false, "write examples to disk") // XX rename: more descriptive svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000", `See help for cmd/goat`) svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF", @@ -49,12 +49,12 @@ func TestExamples(t *testing.T) { } var buff *bytes.Buffer - + if write == nil { + t.Logf("Verifying output of current build against earlier .svg files in examples/.\n") + } + var failures int 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) @@ -88,17 +88,25 @@ func TestExamples(t *testing.T) { // 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() + failures++ + } else { + if testing.Verbose() { + t.Logf("Verified contents of SVG file %s\n", + toSVGFilename(name)) + } } in.Close() out.Close() } } + if failures > 0 { + t.Logf(`Failed to verify contents of %d .svg files +Consider: + %s`, + failures, + "$ go test -run TestExamples -v -args -write") + t.FailNow() + } } func BenchmarkComplicated(b *testing.B) { @@ -132,6 +140,7 @@ func getOutString(filename string) (string, error) { if err != nil { return "", err } + // XX Why are there RETURN characters in contents of the .SVG files? b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n")) return string(b), nil } From 262647691a80c464a9df84b6cc50e1e1c86ebce7 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 17 Jun 2024 12:58:33 -0700 Subject: [PATCH 05/16] Add more regression test/examples Surface examples/*.svg files in git-status. --- .gitignore | 4 +- examples/block-characters.svg | 55 + examples/block-characters.txt | 3 + examples/debug.svg | 73 ++ examples/debug.txt | 34 + examples/half-step-bugs.svg | 69 + examples/half-step-bugs.txt | 12 + examples/hollow-circle.svg | 150 +++ examples/hollow-circle.txt | 50 + examples/incompatibilities.svg | 355 ++++++ examples/incompatibilities.txt | 31 + examples/radius16circle.svg | 33 + examples/radius16circle.txt | 9 + examples/standard-width.svg | 1987 +++++++++++++++++++++++++++++ examples/standard-width.txt | 106 ++ examples/tiny-grids-irregular.svg | 125 ++ examples/tiny-grids-irregular.txt | 19 + 17 files changed, 3114 insertions(+), 1 deletion(-) create mode 100644 examples/block-characters.svg create mode 100644 examples/block-characters.txt create mode 100644 examples/debug.svg create mode 100644 examples/debug.txt create mode 100644 examples/half-step-bugs.svg create mode 100644 examples/half-step-bugs.txt create mode 100644 examples/hollow-circle.svg create mode 100644 examples/hollow-circle.txt create mode 100644 examples/incompatibilities.svg create mode 100644 examples/incompatibilities.txt create mode 100644 examples/radius16circle.svg create mode 100644 examples/radius16circle.txt create mode 100644 examples/standard-width.svg create mode 100644 examples/standard-width.txt create mode 100644 examples/tiny-grids-irregular.svg create mode 100644 examples/tiny-grids-irregular.txt diff --git a/.gitignore b/.gitignore index d9e18a4..5a81918 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ .DS_Store vendor -examples/*.svg + +# Hiding the SVG output files introduces friction when adding a new TXT,SVG pair to the test suite. +# examples/*.svg cmd/tmpl-expand/tmpl-expand cmd/goat/goat diff --git a/examples/block-characters.svg b/examples/block-characters.svg new file mode 100644 index 0000000..56c67d0 --- /dev/null +++ b/examples/block-characters.svg @@ -0,0 +1,55 @@ + + + + +S +p +e +c +i +a +l +c +a +s +e +s +s +u +p +p +o +r +t +e +d +b +y +M +a +r +k +d +e +e +p +: + + diff --git a/examples/block-characters.txt b/examples/block-characters.txt new file mode 100644 index 0000000..27a2820 --- /dev/null +++ b/examples/block-characters.txt @@ -0,0 +1,3 @@ +Special cases supported by Markdeep: + +▉ ▓ ▒ ░ diff --git a/examples/debug.svg b/examples/debug.svg new file mode 100644 index 0000000..3184e56 --- /dev/null +++ b/examples/debug.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +a +( +) +a +( +) +( +) +( +) + + diff --git a/examples/debug.txt b/examples/debug.txt new file mode 100644 index 0000000..d9bdf2a --- /dev/null +++ b/examples/debug.txt @@ -0,0 +1,34 @@ + + ( + + ( + + ) + + ( + + a() + + a() + + () + + () + + || + --))-- + || + + || + --((-- + || + + | | +--)-)-- + | | + + | | +--(-(-- + | | + +o- diff --git a/examples/half-step-bugs.svg b/examples/half-step-bugs.svg new file mode 100644 index 0000000..1b4a124 --- /dev/null +++ b/examples/half-step-bugs.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + +d +o +u +b +l +e +- +d +r +a +w +g +a +p +a +t +a +b +u +t +t +i +n +g +l +i +n +e +e +n +d +s + + diff --git a/examples/half-step-bugs.txt b/examples/half-step-bugs.txt new file mode 100644 index 0000000..1c9abb9 --- /dev/null +++ b/examples/half-step-bugs.txt @@ -0,0 +1,12 @@ + + _ +|_| + +double-draw + + + _ +| | +|_| + +gap at abutting line ends diff --git a/examples/hollow-circle.svg b/examples/hollow-circle.svg new file mode 100644 index 0000000..8e2f285 --- /dev/null +++ b/examples/hollow-circle.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +x +x +x +x +x +x +o +o +o +o +* +* +* +* + + diff --git a/examples/hollow-circle.txt b/examples/hollow-circle.txt new file mode 100644 index 0000000..f821866 --- /dev/null +++ b/examples/hollow-circle.txt @@ -0,0 +1,50 @@ +-o + * * o o + 0123456 + + + +->o o<- + +-> x x <- + xxxx + + <---> + <---> + + o<--->o + o<--->o + + + <--> + <--> + + o<-->o + o<-->o + + + <-> < > + <-> < > + + o<->o o< >o + o<->o o< >o + + + <> + <> + + o<>o + o<>o + + +oo +^^ +|| +vv +oo + +** -- +^^ +|| +vv +** -- diff --git a/examples/incompatibilities.svg b/examples/incompatibilities.svg new file mode 100644 index 0000000..31ceaa3 --- /dev/null +++ b/examples/incompatibilities.svg @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +I +n +p +u +t +T +X +T +p +a +t +t +e +r +n +s +s +u +p +p +o +r +t +e +d +b +y +M +a +r +k +D +e +e +p +, +b +u +t +n +o +t +b +y +G +o +a +t +. +H +o +l +l +o +w +c +i +r +c +l +e +s +R +e +n +d +e +r +e +d +t +o +S +V +G +a +s +a +" +h +o +l +l +o +w +" +c +i +r +c +l +e +, +t +r +a +n +s +p +a +r +e +n +t +t +o +b +a +c +k +g +r +o +u +n +d +. +G +o +a +t +- +s +p +e +c +i +f +i +c +a +l +t +e +r +n +a +t +i +v +e +r +e +n +d +e +r +i +n +g +o +p +t +i +o +n +s +a +r +e +a +v +a +i +l +a +b +l +e +o +n +t +h +e +c +o +m +m +a +n +d +l +i +n +e +. +A +l +t +e +r +n +a +t +i +v +e +T +X +T +p +a +t +t +e +r +n +s +t +o +i +n +d +i +c +a +t +e +d +o +u +b +l +e +- +w +i +d +t +h +c +i +r +c +l +e +s +: +G +o +a +t +a +n +d +M +a +r +k +D +e +e +p +M +a +r +k +D +e +e +p +o +n +l +y +M +a +r +k +D +e +e +p +o +n +l +y +P +a +r +a +l +l +e +l +a +r +c +s +. +. +) +) +M +a +r +k +D +e +e +p +o +n +l +y + + diff --git a/examples/incompatibilities.txt b/examples/incompatibilities.txt new file mode 100644 index 0000000..a5828bd --- /dev/null +++ b/examples/incompatibilities.txt @@ -0,0 +1,31 @@ +Input TXT patterns supported by MarkDeep, but not by Goat. + + +Hollow circles + + o + + Rendered to SVG as a "hollow" circle, transparent to background. + Goat-specific alternative rendering options are available on the command line. + + +Alternative TXT patterns to indicate double-width circles: + + .-. + | | Goat and MarkDeep + '-' + + +-+ + + + MarkDeep only + +-+ + + +-+ + | | MarkDeep only + +-+ + + +Parallel arcs + + _..---. + __)) )- MarkDeep only + ''---' diff --git a/examples/radius16circle.svg b/examples/radius16circle.svg new file mode 100644 index 0000000..1996512 --- /dev/null +++ b/examples/radius16circle.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/examples/radius16circle.txt b/examples/radius16circle.txt new file mode 100644 index 0000000..99747b9 --- /dev/null +++ b/examples/radius16circle.txt @@ -0,0 +1,9 @@ + -+ + + + + + + -+ + + +-+ + + + + +-+ diff --git a/examples/standard-width.svg b/examples/standard-width.svg new file mode 100644 index 0000000..4fbfc23 --- /dev/null +++ b/examples/standard-width.svg @@ -0,0 +1,1987 @@ + + + + + + + + + + + + + + + +U +N +I +C +O +D +E +c +h +a +r +a +c +t +e +r +s +X +X +X +X +F +o +l +d +a +l +l +t +h +i +s +i +n +t +o +e +x +a +m +p +l +e +s +/ +u +n +i +c +o +d +e +. +t +x +t +j +o +i +n +t +s +: +* +o +r +e +s +e +r +v +e +d +: +v +^ +) +( +o +r +d +i +n +a +r +y +, +A +S +C +I +I +: +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +o +r +d +i +n +a +r +y +, +U +n +i +c +o +d +e +: + + + + +· +¤ +¨ +´ +« +» +¯ +  +¦ +­ +× +÷ +ø +Ø +± +¡ + + + + + + + + + + + +B +O +X +D +R +A +W +I +N +G +S +L +I +G +H +T +. +. +. + + + + + + + + + + + + + + + + + + + + + + +B +O +X +D +R +A +W +I +N +G +S +L +I +G +H +T +D +O +U +B +L +E +. +. +. + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +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 +: +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 + + + + + + + + + + + +¹ +² +³ + + + + + + +α +β +γ +δ +ε +ζ +η +θ +ι +κ +λ +μ +ν +ξ +ο +π +ρ +ς +σ +τ +υ +φ +χ +ψ +ω +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 + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +N +o +n +- +s +t +a +n +d +a +r +d +w +e +i +g +h +t +u +n +u +s +a +b +l +e +? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +A +L +T +E +R +N +A +T +I +V +E +T +O +O +L +S +# +# +# +N +o +n +- +g +r +a +p +h +i +c +a +l +A +S +C +I +I +s +o +u +r +c +e +: +M +e +r +m +a +i +d +, +P +i +k +c +h +r +. +. +. +# +# +# +G +r +a +p +h +i +c +a +l +A +S +C +I +I +s +o +u +r +c +e +: +A +s +c +i +i +f +l +o +w +a +n +d +T +e +x +t +i +k +U +n +l +i +k +e +G +o +a +t +, +A +s +c +i +i +f +l +o +w +a +n +d +T +e +x +t +i +k +o +f +f +e +r +o +n +l +i +n +e +g +r +a +p +h +i +c +a +l +e +d +i +t +o +r +s +. +D +i +a +g +r +a +m +s +a +r +e +e +x +p +o +r +t +e +d +f +r +o +m +t +h +e +b +r +o +w +s +e +r +s +e +s +s +i +o +n +a +s +g +r +a +p +h +i +c +a +l +U +N +I +C +O +D +E +o +r +A +S +C +I +I +. +F +o +l +l +o +w +- +o +n +m +a +i +n +t +e +n +a +n +c +e +o +f +t +h +e +d +i +a +g +r +a +m +s +o +f +c +o +u +r +s +e +r +e +q +u +i +r +e +s +i +m +p +o +r +t +f +r +o +m +a +p +r +o +j +e +c +t +' +s +c +o +d +e +/ +d +o +c +a +r +c +h +i +v +e +. +A +s +c +i +i +f +l +o +w +a +c +c +o +m +p +l +i +s +h +e +s +t +h +i +s +b +y +C +t +l +- +V +" +p +a +s +t +e +" +. +T +e +x +t +i +k +h +o +w +e +v +e +r +h +a +s +n +o +i +m +p +o +r +t +m +e +t +h +o +d +. +( +h +t +t +p +s +: +/ +/ +g +i +t +h +u +b +. +c +o +m +/ +a +s +t +a +s +h +o +v +/ +t +i +x +i +/ +i +s +s +u +e +s +/ +1 +5 +) +G +o +a +t +b +u +t +n +o +t +A +s +c +i +i +f +l +o +w +n +o +r +T +e +x +t +i +k +c +o +n +t +a +i +n +s +u +p +p +o +r +t +f +o +r +: +1 +. +R +e +n +d +e +r +i +n +g +t +o +a +s +m +o +o +t +h +e +d +S +V +G +o +u +t +p +u +t +. +2 +. +D +i +a +g +o +n +a +l +l +i +n +e +s +. +3 +. +R +o +u +n +d +e +d +c +o +r +n +e +r +s +. +A +s +c +i +i +f +l +o +w +. +c +o +m +( +b +u +t +n +o +t +G +o +a +t +) +e +x +p +o +r +t +s +d +r +a +w +n +l +i +n +e +s +a +s +t +h +e +g +r +a +p +h +i +c +a +l +U +n +i +c +o +d +e +c +h +a +r +a +c +t +e +r +s +B +O +X +D +R +A +W +I +N +G +S +L +I +G +H +T +. +. +. +T +h +e +s +e +h +a +v +e +w +i +d +t +h +s +e +q +u +a +l +t +o +t +h +o +s +e +o +f +s +i +m +p +l +e +A +S +C +I +I +c +h +a +r +a +c +t +e +r +s +i +n +t +h +e +s +t +a +n +d +a +r +d +U +n +i +x +s +y +s +t +e +m +f +o +n +t +s +. +- +h +t +t +p +s +: +/ +/ +w +w +w +. +f +r +e +e +d +e +s +k +t +o +p +. +o +r +g +/ +w +i +k +i +/ +S +o +f +t +w +a +r +e +/ +f +o +n +t +c +o +n +f +i +g +/ +- +$ +a +p +t +s +h +o +w +f +o +n +t +c +o +n +f +i +g +U +n +f +o +r +t +u +n +a +t +e +l +y +, +A +s +c +i +i +f +l +o +w +e +x +p +o +r +t +s +c +e +r +t +a +i +n +a +r +r +o +w +h +e +a +d +s +a +s +U +n +i +c +o +d +e +c +h +a +r +a +c +t +e +r +s +e +. +g +. +" +B +L +A +C +K +U +P +- +P +O +I +N +T +I +N +G +T +R +I +A +N +G +L +E +" +h +a +v +i +n +g +n +o +n +- +s +t +a +n +d +a +r +d +w +i +d +t +h +i +n +t +h +e +p +o +p +u +l +a +r +G +N +U +/ +L +i +n +u +x +s +y +s +t +e +m +f +o +n +t +" +U +b +u +n +t +u +M +o +n +o +R +e +g +u +l +a +r +" +. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +s +d +o +k +p +o +a +s +j +k +f +p +o + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +" +B +O +X +D +R +A +W +I +N +G +S +L +I +G +H +T +D +O +U +B +L +E +. +. +. +" +a +l +s +o +h +a +v +e +s +t +a +n +d +a +r +d +w +i +d +t +h +s +( +n +o +t +u +s +e +d +b +y +A +s +c +i +i +f +l +o +w +) +. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +T +e +x +t +i +k +. +c +o +m +h +a +s +m +o +r +e +l +i +m +i +t +e +d +d +r +a +w +i +n +g +c +h +a +r +a +c +t +e +r +s +, +b +u +t +d +o +e +s +m +a +i +n +t +a +i +n +m +u +l +t +i +- +c +e +l +l +g +e +o +m +e +t +r +y +s +t +r +u +c +t +u +r +e +w +i +t +h +i +n +i +t +s +e +d +i +t +o +r +. + + diff --git a/examples/standard-width.txt b/examples/standard-width.txt new file mode 100644 index 0000000..5c982be --- /dev/null +++ b/examples/standard-width.txt @@ -0,0 +1,106 @@ +UNICODE characters XXXX Fold all this into examples/unicode.txt +--- + +joints: + .'+*o + +reserved: + -_ |v^><*+/\)( + +ordinary, ASCII: +abcdefghijklmnopqrstuvwxyz +ABCDEFGHIJKLMNOPQRSTUVWXYZ +0123456789012345 + +ordinary, Unicode: + +┌─┬┐·¤¨´«»¯ ¦­ +×÷øØ ±¡ +┘┘┘┘┘┘┘┘┘┘┘ BOX DRAWINGS LIGHT ... +│││││││││││ +║║║║║║║║║║║ BOX DRAWINGS LIGHT DOUBLE ... +╚╚╚╚╚╚╚╚╚╚╚ +═══════════ +01234567890 + + +Non-standard dimensions in fonts: + Liberation Mono + Noto Mono Regular +₀₁₂₃₄₅₆₇₈₉ +⁰¹²³⁴⁵⁶⁷⁸⁹ +αβγδεζηθικλμνξοπρςστυφχψω + + +Non-standard dimensions in fonts: + DejaVu Sans Mono + FreeMono + Ubuntu Mono + MonoSpace +⎔ +⬣ +✹ +╱ +╲╲╲╲╲╲╲╲╲ +╳╳╳╳╳╳╳╳╳ +0123456789012345 + +Non-standard weight -- unusable? +╴╴╴╴╴╴╴╴╴╴ +╶╶╶╶╶╶╶╶╶╶ +╵╵╵╵╵╵╵╵╵╵ +╱╱╱╱╱╱╱╱╱╱ +01234567890 + + +ALTERNATIVE TOOLS +--- +### Non-graphical ASCII source: Mermaid, Pikchr ... + +### Graphical ASCII source: Asciiflow and Textik +Unlike Goat, Asciiflow and Textik offer online graphical editors. +Diagrams are exported from the browser session as graphical UNICODE or ASCII. + +Follow-on maintenance of the diagrams of course requires import from a project's code/doc archive. +Asciiflow accomplishes this by Ctl-V "paste". +Textik however has no import method. (https://github.com/astashov/tixi/issues/15) + +Goat but not Asciiflow nor Textik contain support for: + 1. Rendering to a smoothed SVG output. + 2. Diagonal lines. + 3. Rounded corners. + +Asciiflow.com (but not Goat) exports drawn lines as the graphical Unicode +characters BOX DRAWINGS LIGHT ... + +These have widths equal to those of simple ASCII characters in the standard Unix system fonts. + - https://www.freedesktop.org/wiki/Software/fontconfig/ + - $ apt show fontconfig + +Unfortunately, Asciiflow exports certain arrowheads as Unicode characters e.g. "BLACK UP-POINTING + TRIANGLE" having non-standard width in the popular GNU/Linux system font "Ubuntu Mono Regular". + + ┌───────────────────────► + │ + │ + ┌─────┼──────────────┐ ▲ + │ │ │ │ + └─────┼──────────────┘ │ + │ │ + │ │ +┌──────────────────┐ │ +│ │ │ +│ sdokpoasjkfpo ├─────────────────────────────────────────┘ +└──────────────────┘ + +▲▲▲▲▲▲▲ +01234567890 + +"BOX DRAWINGS LIGHT DOUBLE ..." also have standard widths (not used by Asciiflow). + +║║║║║║║║║║║ +╚╚╚╚╚╚╚╚╚╚╚ +═══════════ + +Textik.com has more limited drawing characters, but does maintain multi-cell +geometry structure within its editor. diff --git a/examples/tiny-grids-irregular.svg b/examples/tiny-grids-irregular.svg new file mode 100644 index 0000000..cf77a4e --- /dev/null +++ b/examples/tiny-grids-irregular.svg @@ -0,0 +1,125 @@ + + + + + +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-irregular.txt b/examples/tiny-grids-irregular.txt new file mode 100644 index 0000000..15e47da --- /dev/null +++ b/examples/tiny-grids-irregular.txt @@ -0,0 +1,19 @@ +0123456789012345 + ▉▉ ▉▉ ▉▉ | + ▉▉ ▉▉ | + ▉▉ ▉▉ ▉▉ | + ▉▉ ▉▉ | + ▉▉ ▉▉ ▉▉ | + | + ⬢ ⬡ ⬡ | + ⬢ ⬢ ⬡ ⬡ | + ⬢ ⬢ ⬢ ⬡ ⬡ | + ⬡ ⬡ ⬡ ⬡ | + ⬡ ⬡ ⬡ | + | + ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ | + ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ | + ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ | + ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ | + ⁚⁚⁚⁚⁚⁚⁚⁚⁚⁚ | +0123456789012345 From c76b78c3839254ae48f58708dc82523cdb6f0211 Mon Sep 17 00:00:00 2001 From: dmullis Date: Tue, 18 Jun 2024 21:38:15 -0700 Subject: [PATCH 06/16] Regression testing: Clarify source code and CLI text Bump required Go version to 1.21 for access to functions min() and max(). --- .github/workflows/test.yml | 2 +- examples_test.go | 44 ++++++++++++++++++++++++-------------- go.mod | 2 +- pre-push.sh | 2 ++ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d5ecbc..b92e1e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: build: strategy: matrix: - go-version: [1.20.x] + go-version: [1.21.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: diff --git a/examples_test.go b/examples_test.go index 1ac709a..17e56a4 100644 --- a/examples_test.go +++ b/examples_test.go @@ -14,7 +14,8 @@ import ( ) var ( - write = flag.Bool("write", false, "write examples to disk") // XX rename: more descriptive + write = flag.Bool("write", + false, "write reference SVG output files") svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000", `See help for cmd/goat`) svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF", @@ -50,7 +51,7 @@ func TestExamples(t *testing.T) { var buff *bytes.Buffer if write == nil { - t.Logf("Verifying output of current build against earlier .svg files in examples/.\n") + t.Logf("Verifying equality of current SVG with examples/ references.\n") } var failures int for _, name := range filenames { @@ -83,15 +84,26 @@ func TestExamples(t *testing.T) { if err != nil { t.Log(err) } - if buff.String() != golden { - // 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)) + if newStr := buff.String(); newStr != golden { + // Skip complaint if the modification timestamp of the .txt file + // source is fresher than that of the .svg? + // => NO, Any .txt difference might be an editing mistake. + + t.Logf("Content mismatch for %s. Length was %d, expected %d", + toSVGFilename(name), buff.Len(), len(golden)) + for i:=0; iREADME.md $(git-ls-files examples | tac) + +printf "\nTo install in local GOPATH:\n\t%s\n" "go install ./cmd/goat" From 90d2f755e78d1fb63dc03e21977665a381c49d9d Mon Sep 17 00:00:00 2001 From: dmullis Date: Sun, 23 Jun 2024 17:45:30 -0700 Subject: [PATCH 07/16] Add a script to delete the products of `pre-push.sh -w` --- clean.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 clean.sh diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..53cdabf --- /dev/null +++ b/clean.sh @@ -0,0 +1,3 @@ +#! /bin/sh + +rm -f examples/*.svg *.svg README.md From 2e102889f07f50afc4c2f4d5ec0d97fed1b226e3 Mon Sep 17 00:00:00 2001 From: dmullis Date: Sat, 22 Jun 2024 20:56:18 -0700 Subject: [PATCH 08/16] Detect a possible error --- svg.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/svg.go b/svg.go index 559d377..4c89474 100644 --- a/svg.go +++ b/svg.go @@ -103,10 +103,13 @@ func (l Line) draw(out io.Writer) { // Half steps switch l.chop { + case NONE: case N: stop.Y -= 8 case S: start.Y += 8 + default: + panic("impossible 'chop' orientation") } } From 0576ac72977d90b4d36675fb5b335ae5a9493063 Mon Sep 17 00:00:00 2001 From: dmullis Date: Sat, 29 Jun 2024 22:13:27 -0700 Subject: [PATCH 09/16] Enumerate examples/ for which SVG file contents may need to change Refactor for tidiness. --- examples_test.go | 128 ++++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/examples_test.go b/examples_test.go index 17e56a4..504edf8 100644 --- a/examples_test.go +++ b/examples_test.go @@ -2,15 +2,15 @@ package goat import ( "bytes" + "errors" "flag" + "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "testing" - - qt "github.com/frankban/quicktest" ) var ( @@ -23,9 +23,7 @@ var ( ) // XX TXT source file suite is limited to a single file -- "circuits.txt" -func TestExamplesStableOutput(t *testing.T) { - c := qt.New(t) - +func TestExampleStableOutput(t *testing.T) { var previous string for i := 0; i < 3; i++ { in, err := os.Open(filepath.Join(basePath, "circuits.txt")) @@ -36,7 +34,7 @@ func TestExamplesStableOutput(t *testing.T) { BuildAndWriteSVG(in, &out, "black", "white") in.Close() if i > 0 && previous != out.String() { - c.Fail() + t.FailNow() } previous = out.String() @@ -49,84 +47,87 @@ func TestExamples(t *testing.T) { t.Fatal(err) } - var buff *bytes.Buffer - if write == nil { + if *write { + writeExamples(t, filenames) + } else { t.Logf("Verifying equality of current SVG with examples/ references.\n") + verifyExamples(t, filenames) } - var failures int +} + + +func writeExamples(t *testing.T, filenames []string) { for _, name := range filenames { in := getIn(name) - var out io.WriteCloser - if *write { - out = getOut(name) - } else { - if buff == nil { - buff = &bytes.Buffer{} - } else { - buff.Reset() - } - out = struct { - io.Writer - io.Closer - }{ - buff, - io.NopCloser(nil), - } - } - + out := getOut(name) BuildAndWriteSVG(in, out, *svgColorLightScheme, *svgColorDarkScheme) - in.Close() out.Close() + } +} - if buff != nil { - golden, err := getOutString(name) - if err != nil { - t.Log(err) - } - if newStr := buff.String(); newStr != golden { - // Skip complaint if the modification timestamp of the .txt file - // source is fresher than that of the .svg? - // => NO, Any .txt difference might be an editing mistake. - - t.Logf("Content mismatch for %s. Length was %d, expected %d", - toSVGFilename(name), buff.Len(), len(golden)) - for i:=0; i 0 { + if len(failures) > 0 { t.Logf(`Failed to verify contents of %d .svg files -Consider: - %s`, - failures, - "$ go test -run TestExamples -v -args -write") +Failing files:`, + len(failures)) + for _, name := range failures { + svgFile := toSVGFilename(name) + fmt.Printf("\t\t%s\n", svgFile) + } t.FailNow() } } +func compareSVG(t *testing.T, buff *bytes.Buffer, fileName string) error { + golden, err := getOutString(fileName) + if err != nil { + t.Log(err) + } + if newStr := buff.String(); newStr != golden { + // Skip complaint if the modification timestamp of the .txt file + // source is fresher than that of the .svg? + // => NO, Any .txt difference might be an editing mistake. + + t.Logf("Content mismatch for %s. Length was %d, expected %d", + toSVGFilename(fileName), buff.Len(), len(golden)) + for i:=0; i Date: Mon, 1 Jul 2024 10:14:50 -0700 Subject: [PATCH 10/16] Issue 25: Expose additional cases --- examples/small-grids.txt | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/examples/small-grids.txt b/examples/small-grids.txt index 7961b21..e848d4b 100644 --- a/examples/small-grids.txt +++ b/examples/small-grids.txt @@ -1,8 +1,33 @@ - ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. .---. .---. .-. .-. 0 - ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | | A | | B | | A | | B | 1 - / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ '---' '---' '-' '-' 2 - \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | .---. .---. .-. .-. 3 - / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ | C | | D | | C | | D | 4 - \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | '---' '---' '-' '-' 5 - \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' 0123456789012345678901234 6 + ___ ___ .---+---+---+---+---. .---+---+---+---. + ___/ \___/ \ | | | | | | / \ / \ / \ / \ / +/ \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +\___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ +/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +\___/ \___/ \ | | a | | | | / \ / \ / \ / \ / + \___/ \___/ '---+---+---+---+---' '---+---+---+---' +.---. .---. +--+ +-+ +-+ .-. .--. .-. +-+ +--+ .-. .--. +| | | | | | | | | | | | | | | | | | | | | | | | ++---+ +---+ +--+ +-+ | | | | | | | | +-+ +--+ '-' '--' +| +---+ | | | | | | | | | | | | | ++---+ b +---+ +--+ +-+ | | | | | | | | +| a +---+ | | | | | | | | | | | | | +'---' '---' +--+ +-+ +-+ '-' '--' '-' + ++---+ .---. .---. +| B | | E | | F | ++---+ '---' '---' +.---. +---+ .---. +| D | | G | | H | +'---' +---+ '---' +0123456789012345678901232 + +Not Supported: + .---+---+---. ++---+ .-. .-. 0 | | a | | b | +| A | | A | | B | 1 +++ +--++-+-++--+ ++---+ '-' '-' 2 | | | | | | | ++---+ .-. .-. 3 +-+ +--++-+-++--+ +| C | | C | | D | 4 | | | | | | ++---+ '-' '-' 5 +++ '---+---+---' +0123434567890123456 | From 145eae86a0fdbd9608105b161325d24f99573450 Mon Sep 17 00:00:00 2001 From: dmullis Date: Sun, 30 Jun 2024 20:47:09 -0700 Subject: [PATCH 11/16] Upon regression failure, produce HTML for browser to surface differences --- examples-regression_test.go | 205 ++++++++++++++++++++++++++++++++++++ examples_test.go | 129 +---------------------- 2 files changed, 206 insertions(+), 128 deletions(-) create mode 100644 examples-regression_test.go diff --git a/examples-regression_test.go b/examples-regression_test.go new file mode 100644 index 0000000..2eaff49 --- /dev/null +++ b/examples-regression_test.go @@ -0,0 +1,205 @@ +package goat + +import ( + "bytes" + "errors" + "flag" + //"fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "text/template" +) + +const ( + examplesDir = "examples" +) + +var ( + write = flag.Bool("write", + false, "write reference SVG output files") + svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000", + `See help for cmd/goat`) + svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF", + `See help for cmd/goat`) + + // Begin the directory name with '_' to hide from git. + svgDeltaDir = flag.String("svg-delta-dir", "_examples_new", + `Directory to be filled with a delta-image file for each +newly-generated SVG that does not match those in ` + examplesDir) +) + +func TestExamples(t *testing.T) { + // XX This sweeps up ~every~ *.txt file in examples/ + txtPaths, err := filepath.Glob(filepath.Join(examplesDir, "*.txt")) + if err != nil { + t.Fatal(err) + } + + baseNames := make([]string, len(txtPaths)) + for i := range txtPaths { + baseName, found := strings.CutPrefix(txtPaths[i], examplesDir+"/") + if !found { + panic("Could not cut prefix from pathname.") + } + baseNames[i] = baseName + } + + if *write { + writeExamples(examplesDir, examplesDir, baseNames, *svgColorLightScheme, *svgColorDarkScheme) + } else { + t.Logf("Verifying equality of current SVG with examples/ references.\n") + verifyExamples(t, examplesDir, baseNames) + } +} + + +func writeExamples(inDir, outDir string, baseNames []string, lightColor, darkColor string) { + for _, name := range baseNames { + in := getIn(inDir + "/" + name) + out := getOut(outDir + "/" + name) + BuildAndWriteSVG(in, out, lightColor, darkColor) + in.Close() + out.Close() + } +} + +func verifyExamples(t *testing.T, examplesDir string, baseNames []string) { + var failures []string + for _, name := range baseNames { + in := getIn(examplesDir + "/" + name) + buff := &bytes.Buffer{} + BuildAndWriteSVG(in, buff, *svgColorLightScheme, *svgColorDarkScheme) + in.Close() + if nil != compareSVG(t, buff, examplesDir, name) { + failures = append(failures, name) + } + + } + if len(failures) > 0 { + t.Logf(`Failed to verify contents of %d .svg files`, + len(failures)) + err := os.Mkdir(*svgDeltaDir, 0770) + if err != nil { + t.Fatalf(` + Aborting: "%v"`, err) + } + writeExamples(examplesDir, *svgDeltaDir, failures, "#000088", "#88CCFF") + writeDeltaHTML(t, "../" + examplesDir, *svgDeltaDir, failures) + t.FailNow() + } +} + +func compareSVG(t *testing.T, buff *bytes.Buffer, examplesDir string, baseName string) error { + fileName := examplesDir + "/" + baseName + golden, err := getOutString(fileName) + if err != nil { + t.Log(err) + } + if newStr := buff.String(); newStr != golden { + // Skip complaint if the modification timestamp of the .txt file + // source is fresher than that of the .svg? + // => NO, Any .txt difference might be an editing mistake. + + t.Logf("Content mismatch for %s. Length was %d, expected %d", + toSVGFilename(fileName), buff.Len(), len(golden)) + for i:=0; i +.blended-images { + height: 100%; /* XX How to make equal to pixel bounds of the SVGs? */ + background-size: contain, contain; + background-repeat: no-repeat; + background-blend-mode: difference; + background-image: url('{{.ExamplesDir}}/{{.SvgBaseName}}'), url('{{.DeltaDir}}/{{.SvgBaseName}}'); + } + + +
+
+
+`)) + for _, name := range baseNames { + htmlOutName := stripSuffix(name) + ".html" + t.Logf("\t%s", htmlOutName) + htmlOutFile, err := os.Create(deltaDir + "/" + htmlOutName) + err = tmpl.Execute(htmlOutFile, map[string]string{ + "ExamplesDir": examplesDir, + "DeltaDir": ".", + "SvgBaseName": toSVGFilename(name), + }) + htmlOutFile.Close() + if err != nil { + panic(err) + } + } +} + +func getIn(txtFilename string) io.ReadCloser { + in, err := os.Open(txtFilename) + if err != nil { + panic(err) + } + return in +} + +func getOutExport(pathPrefix, txtBaseName string) io.WriteCloser { + svgBaseName := toSVGFilename(txtBaseName) + out, err := os.Create(pathPrefix + svgBaseName) + if err != nil { + panic(err) + } + return out +} + +func getOut(txtFilename string) io.WriteCloser { + out, err := os.Create(toSVGFilename(txtFilename)) + if err != nil { + panic(err) + } + return out +} + +func getOutString(txtFilename string) (string, error) { + b, err := ioutil.ReadFile(toSVGFilename(txtFilename)) + if err != nil { + // XX Simply panic rather than return an error? + return "", err + } + // XX Why are there RETURN characters in contents of the .SVG files? + b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n")) + return string(b), nil +} + +func toSVGFilename(txtFilename string) string { + return strings.TrimSuffix(txtFilename, filepath.Ext(txtFilename)) + ".svg" +} + +func stripSuffix(basename string) string { + return strings.Split(basename,".")[0] +} diff --git a/examples_test.go b/examples_test.go index 504edf8..8131225 100644 --- a/examples_test.go +++ b/examples_test.go @@ -2,31 +2,17 @@ package goat import ( "bytes" - "errors" - "flag" - "fmt" "io" - "io/ioutil" "os" "path/filepath" - "strings" "testing" ) -var ( - write = flag.Bool("write", - false, "write reference SVG output files") - svgColorLightScheme = flag.String("svg-color-light-scheme", "#000000", - `See help for cmd/goat`) - svgColorDarkScheme = flag.String("svg-color-dark-scheme", "#FFFFFF", - `See help for cmd/goat`) -) - // XX TXT source file suite is limited to a single file -- "circuits.txt" func TestExampleStableOutput(t *testing.T) { var previous string for i := 0; i < 3; i++ { - in, err := os.Open(filepath.Join(basePath, "circuits.txt")) + in, err := os.Open(filepath.Join(examplesDir, "circuits.txt")) if err != nil { t.Fatal(err) } @@ -41,86 +27,6 @@ func TestExampleStableOutput(t *testing.T) { } } -func TestExamples(t *testing.T) { - filenames, err := filepath.Glob(filepath.Join(basePath, "*.txt")) - if err != nil { - t.Fatal(err) - } - - if *write { - writeExamples(t, filenames) - } else { - t.Logf("Verifying equality of current SVG with examples/ references.\n") - verifyExamples(t, filenames) - } -} - - -func writeExamples(t *testing.T, filenames []string) { - for _, name := range filenames { - in := getIn(name) - out := getOut(name) - BuildAndWriteSVG(in, out, *svgColorLightScheme, *svgColorDarkScheme) - in.Close() - out.Close() - } -} - -func verifyExamples(t *testing.T, filenames []string) { - var failures []string - for _, name := range filenames { - in := getIn(name) - buff := &bytes.Buffer{} - BuildAndWriteSVG(in, buff, *svgColorLightScheme, *svgColorDarkScheme) - in.Close() - if nil != compareSVG(t, buff, name) { - failures = append(failures, name) - } - - } - if len(failures) > 0 { - t.Logf(`Failed to verify contents of %d .svg files -Failing files:`, - len(failures)) - for _, name := range failures { - svgFile := toSVGFilename(name) - fmt.Printf("\t\t%s\n", svgFile) - } - t.FailNow() - } -} - -func compareSVG(t *testing.T, buff *bytes.Buffer, fileName string) error { - golden, err := getOutString(fileName) - if err != nil { - t.Log(err) - } - if newStr := buff.String(); newStr != golden { - // Skip complaint if the modification timestamp of the .txt file - // source is fresher than that of the .svg? - // => NO, Any .txt difference might be an editing mistake. - - t.Logf("Content mismatch for %s. Length was %d, expected %d", - toSVGFilename(fileName), buff.Len(), len(golden)) - for i:=0; i Date: Mon, 1 Jul 2024 09:13:36 -0700 Subject: [PATCH 12/16] Split 'canvas.go' along major data structure boundaries --- canvas.go | 787 ++-------------------------------------------------- drawable.go | 756 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 776 insertions(+), 767 deletions(-) create mode 100644 drawable.go diff --git a/canvas.go b/canvas.go index 20d5c72..51c35a4 100644 --- a/canvas.go +++ b/canvas.go @@ -10,7 +10,6 @@ type ( runeSet map[rune]exists ) - // Characters where more than one line segment can come together. var jointRunes = []rune{ '.', @@ -94,23 +93,6 @@ func isTriangle(r rune) bool { return r == '^' || r == 'v' || r == '<' || r == '>' } -// Canvas represents a 2D ASCII rectangle. -type Canvas struct { - // units of cells - Width, Height int - - data map[Index]rune - text map[Index]rune -} - -func (c *Canvas) heightScreen() int { - return c.Height*16 + 8 + 1 -} - -func (c *Canvas) widthScreen() int { - return (c.Width + 1) * 8 -} - // Arg 'canvasMap' is typically either Canvas.data or Canvas.text func inSet(set runeSet, canvasMap map[Index]rune, i Index) (inset bool) { r, inMap := canvasMap[i] @@ -131,6 +113,26 @@ func (c *Canvas) runeAt(i Index) rune { return ' ' } +// Canvas represents a 2D ASCII rectangle. +type Canvas struct { + // units of cells + Width, Height int + + data map[Index]rune + text map[Index]rune +} + +func (c *Canvas) heightScreen() int { + // XX Why " + 8 + 1"? + return c.Height*16 + 8 + 1 +} + +func (c *Canvas) widthScreen() int { + // XX Why "c.Width + 1"? + return (c.Width + 1) * 8 +} + + // NewCanvas creates a fully-populated Canvas according to GoAT-formatted text read from // an io.Reader, consuming all bytes available. func NewCanvas(in io.Reader) (c Canvas) { @@ -197,755 +199,6 @@ func (c *Canvas) MoveToText() { } } -// Drawable represents anything that can Draw itself. -type Drawable interface { - draw(out io.Writer) -} - -// Line represents a straight segment between two points 'start' and 'stop', where -// 'start' is either lesser in X (north-east, east, south-east), or -// equal in X and lesser in Y (south). -type Line struct { - start Index - stop Index - - startRune rune - stopRune rune - - // dashed bool - needsNudgingDown bool - needsNudgingLeft bool - needsNudgingRight bool - needsTinyNudgingLeft bool - needsTinyNudgingRight bool - - // This is a line segment all by itself. This centers the segment around - // the midline. - lonely bool - // N or S. Only useful for half steps - chops of this half of the line. - chop Orientation - - // X-major, Y-minor. Therefore, always one of the compass points NE, E, SE, S. - orientation Orientation - - state lineState -} - -type lineState int - -const ( - _Unstarted lineState = iota - _Started -) - -func (l *Line) started() bool { - return l.state == _Started -} - -func (c *Canvas) setStart(l *Line, i Index) { - if l.state == _Unstarted { - l.start = i - l.startRune = c.runeAt(i) - l.stop = i - l.stopRune = c.runeAt(i) - l.state = _Started - } -} - -func (c *Canvas) setStop(l *Line, i Index) { - if l.state == _Started { - l.stop = i - l.stopRune = c.runeAt(i) - } -} - -func (l *Line) goesSomewhere() bool { - return l.start != l.stop -} - -func (l *Line) horizontal() bool { - return l.orientation == E || l.orientation == W -} - -func (l *Line) vertical() bool { - return l.orientation == N || l.orientation == S -} - -func (l *Line) diagonal() bool { - return l.orientation == NE || l.orientation == SE || l.orientation == SW || l.orientation == NW -} - -// XX drop names 'start' below - -// Triangle corresponds to "^", "v", "<" and ">" runes in the absence of -// surrounding alphanumerics. -type Triangle struct { - start Index - orientation Orientation - needsNudging bool -} - -// Circle corresponds to "o" or "*" runes in the absence of surrounding -// alphanumerics. -type Circle struct { - start Index - bold bool -} - -// RoundedCorner corresponds to combinations of "-." or "-'". -type RoundedCorner struct { - start Index - orientation Orientation -} - -// Text corresponds to any runes not reserved for diagrams, or reserved runes -// surrounded by alphanumerics. -type Text struct { - start Index - str string // Possibly multiple bytes, from Unicode source of type 'rune' -} - -// Bridge corresponds to combinations of "-)-" or "-(-" and is displayed as -// the vertical line "hopping over" the horizontal. -type Bridge struct { - start Index - orientation Orientation -} - -// Orientation represents the primary direction that a Drawable is facing. -type Orientation int - -const ( - NONE Orientation = iota // No orientation; no structure present. - N // North - NE // Northeast - NW // Northwest - S // South - SE // Southeast - SW // Southwest - E // East - W // West -) - -// WriteSVGBody writes the entire content of a Canvas out to a stream in SVG format. -func (c *Canvas) WriteSVGBody(dst io.Writer) { - writeBytes(dst, "\n") - - for _, l := range c.Lines() { - l.draw(dst) - } - - for _, tI := range c.Triangles() { - tI.draw(dst) - } - - for _, c := range c.RoundedCorners() { - c.draw(dst) - } - - for _, c := range c.Circles() { - c.draw(dst) - } - - for _, bI := range c.Bridges() { - bI.draw(dst) - } - - writeText(dst, c) - - writeBytes(dst, "\n") -} - -// Lines returns a slice of all Line drawables that we can detect -- in all -// possible orientations. -func (c *Canvas) Lines() (lines []Line) { - horizontalMidlines := c.getLinesForSegment('-') - - diagUpLines := c.getLinesForSegment('/') - for i, l := range diagUpLines { - // /_ - if c.runeAt(l.start.east()) == '_' { - diagUpLines[i].needsTinyNudgingLeft = true - } - - // _ - // / - if c.runeAt(l.stop.north()) == '_' { - diagUpLines[i].needsTinyNudgingRight = true - } - - // _ - // / - if !l.lonely && c.runeAt(l.stop.nEast()) == '_' { - diagUpLines[i].needsTinyNudgingRight = true - } - - // _/ - if !l.lonely && c.runeAt(l.start.west()) == '_' { - diagUpLines[i].needsTinyNudgingLeft = true - } - - // \ - // / - if !l.lonely && c.runeAt(l.stop.north()) == '\\' { - diagUpLines[i].needsTinyNudgingRight = true - } - - // / - // \ - if !l.lonely && c.runeAt(l.start.south()) == '\\' { - diagUpLines[i].needsTinyNudgingLeft = true - } - } - - diagDownLines := c.getLinesForSegment('\\') - for i, l := range diagDownLines { - // _\ - if c.runeAt(l.stop.west()) == '_' { - diagDownLines[i].needsTinyNudgingRight = true - } - - // _ - // \ - if c.runeAt(l.start.north()) == '_' { - diagDownLines[i].needsTinyNudgingLeft = true - } - - // _ - // \ - if !l.lonely && c.runeAt(l.start.nWest()) == '_' { - diagDownLines[i].needsTinyNudgingLeft = true - } - - // \_ - if !l.lonely && c.runeAt(l.stop.east()) == '_' { - diagDownLines[i].needsTinyNudgingRight = true - } - - // \ - // / - if !l.lonely && c.runeAt(l.stop.south()) == '/' { - diagDownLines[i].needsTinyNudgingRight = true - } - - // / - // \ - if !l.lonely && c.runeAt(l.start.north()) == '/' { - diagDownLines[i].needsTinyNudgingLeft = true - } - } - - horizontalBaselines := c.getLinesForSegment('_') - for i, l := range horizontalBaselines { - // TODO: make this nudge an orientation - horizontalBaselines[i].needsNudgingDown = true - - // _ - // _| | - if c.runeAt(l.stop.sEast()) == '|' || c.runeAt(l.stop.nEast()) == '|' { - horizontalBaselines[i].needsNudgingRight = true - } - - // _ - // | _| - if c.runeAt(l.start.sWest()) == '|' || c.runeAt(l.start.nWest()) == '|' { - horizontalBaselines[i].needsNudgingLeft = true - } - - // _ - // _/ \ - if c.runeAt(l.stop.east()) == '/' || c.runeAt(l.stop.sEast()) == '\\' { - horizontalBaselines[i].needsTinyNudgingRight = true - } - - // _ - // \_ / - if c.runeAt(l.start.west()) == '\\' || c.runeAt(l.start.sWest()) == '/' { - horizontalBaselines[i].needsTinyNudgingLeft = true - } - - // _\ - if c.runeAt(l.stop.east()) == '\\' { - horizontalBaselines[i].needsNudgingRight = true - horizontalBaselines[i].needsTinyNudgingRight = true - } - - // - // /_ - if c.runeAt(l.start.west()) == '/' { - horizontalBaselines[i].needsNudgingLeft = true - horizontalBaselines[i].needsTinyNudgingLeft = true - } - // _ - // / - if c.runeAt(l.stop.south()) == '/' { - horizontalBaselines[i].needsTinyNudgingRight = true - } - - // _ - // \ - if c.runeAt(l.start.south()) == '\\' { - horizontalBaselines[i].needsTinyNudgingLeft = true - } - - // _ - // ' - if c.runeAt(l.start.sWest()) == '\'' { - horizontalBaselines[i].needsNudgingLeft = true - } - - // _ - // ' - if c.runeAt(l.stop.sEast()) == '\'' { - horizontalBaselines[i].needsNudgingRight = true - } - } - - verticalLines := c.getLinesForSegment('|') - - lines = append(lines, horizontalMidlines...) - lines = append(lines, horizontalBaselines...) - lines = append(lines, verticalLines...) - lines = append(lines, diagUpLines...) - lines = append(lines, diagDownLines...) - lines = append(lines, c.HalfSteps()...) // vertical, only - - return -} - -func newHalfStep(i Index, chop Orientation) Line { - return Line{ - start: i, - stop: i.south(), - lonely: true, - chop: chop, - orientation: S, - } -} - -func (c *Canvas) HalfSteps() (lines []Line) { - for idx := range upDown(c.Width, c.Height) { - if o := c.partOfHalfStep(idx); o != NONE { - lines = append( - lines, - newHalfStep(idx, o), - ) - } - } - return -} - -func (c *Canvas) getLinesForSegment(segment rune) []Line { - var iter canvasIterator - var orientation Orientation - var passThroughs []rune - - switch segment { - case '-': - iter = leftRight - orientation = E - passThroughs = append(jointRunes, '<', '>', '(', ')') - case '_': - iter = leftRight - orientation = E - passThroughs = append(jointRunes, '|') - case '|': - iter = upDown - orientation = S - passThroughs = append(jointRunes, '^', 'v') - case '/': - iter = diagUp - orientation = NE - passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|') - case '\\': - iter = diagDown - orientation = SE - passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|') - default: - return nil - } - - return c.getLines(iter, segment, passThroughs, orientation) -} - -// ci: the order that we traverse locations on the canvas. -// segment: the primary character we're tracking for this line. -// passThroughs: characters the line segment is allowed to be drawn underneath -// (without terminating the line). -// orientation: the orientation for this line. -func (c *Canvas) getLines( - ci canvasIterator, - segment rune, - passThroughs []rune, - o Orientation, -) (lines []Line) { - // Helper to throw the current line we're tracking on to the slice and - // start a new one. - snip := func(cl Line) Line { - // Only collect lines that actually go somewhere or are isolated - // segments; otherwise, discard what's been collected so far within 'cl'. - if cl.goesSomewhere() { - lines = append(lines, cl) - } - - return Line{orientation: o} - } - - currentLine := Line{orientation: o} - lastSeenRune := ' ' - - for idx := range ci(c.Width+1, c.Height+1) { - r := c.runeAt(idx) - - isSegment := r == segment - isPassThrough := contains(passThroughs, r) - isRoundedCorner := c.isRoundedCorner(idx) - isDot := isDot(r) - isTriangle := isTriangle(r) - - justPassedThrough := contains(passThroughs, lastSeenRune) - - shouldKeep := (isSegment || isPassThrough) && isRoundedCorner == NONE - - // This is an edge case where we have a rounded corner... that's also a - // joint... attached to orthogonal line, e.g.: - // - // '+-- - // | - // - // TODO: This also depends on the orientation of the corner and our - // line. - // NW / NE line can't go with EW/NS lines, vertical is OK though. - if isRoundedCorner != NONE && o != E && (c.partOfVerticalLine(idx) || c.partOfDiagonalLine(idx)) { - shouldKeep = true - } - - // Don't connect | to > for diagonal lines or )) for horizontal lines. - if isPassThrough && justPassedThrough && o != S { - currentLine = snip(currentLine) - } - - // Don't connect o to o, + to o, etc. This character is a new pass-through - // so we still want to respect shouldKeep; we just don't want to draw - // the existing line through this cell. - if justPassedThrough && (isDot || isTriangle) { - currentLine = snip(currentLine) - } - - switch currentLine.state { - case _Unstarted: - if shouldKeep { - c.setStart(¤tLine, idx) - } - case _Started: - if !shouldKeep { - // Snip the existing line, don't add the current cell to it - // *unless* its a line segment all by itself. If it is, keep a - // record that it's an individual segment because we need to - // adjust later in the / and \ cases. - if !currentLine.goesSomewhere() && lastSeenRune == segment { - if !c.partOfRoundedCorner(currentLine.start) { - c.setStop(¤tLine, idx) - currentLine.lonely = true - } - } - currentLine = snip(currentLine) - } else if isPassThrough { - // Snip the existing line but include the current pass-through - // character because we may be continuing the line. - c.setStop(¤tLine, idx) - currentLine = snip(currentLine) - c.setStart(¤tLine, idx) - } else if shouldKeep { - // Keep the line going and extend it by this character. - c.setStop(¤tLine, idx) - } - } - - lastSeenRune = r - } - return -} - -// Triangles detects intended triangles -- typically at the end of an intended line -- -// and returns a representational slice composed of types Triangle and Line. -func (c *Canvas) Triangles() (triangles []Drawable) { - o := NONE - - for idx := range upDown(c.Width, c.Height) { - needsNudging := false - start := idx - - r := c.runeAt(idx) - - if !isTriangle(r) { - continue - } - - // Identify orientation and nudge the triangle to touch any - // adjacent walls. - switch r { - case '^': - o = N - // ^ and ^ - // / \ - if c.runeAt(start.sWest()) == '/' { - o = NE - } else if c.runeAt(start.sEast()) == '\\' { - o = NW - } - case 'v': - if c.runeAt(start.north()) == '|' { - // | - // v - o = S - } else if c.runeAt(start.nEast()) == '/' { - // / - // v - o = SW - } else if c.runeAt(start.nWest()) == '\\' { - // \ - // v - o = SE - } else { - // Conclusion: Meant as a text string 'v', not a triangle - //panic("Not sufficient to fix all 'v' troubles.") - // continue XX Already committed to non-text output for this string? - o = S - } - case '<': - o = W - case '>': - o = E - } - - // Determine if we need to snap the triangle to something and, if so, - // draw a tail if we need to. - switch o { - case N: - r := c.runeAt(start.north()) - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append(triangles, newHalfStep(start, N)) - } - case NW: - r := c.runeAt(start.nWest()) - // Need to draw a tail. - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append( - triangles, - Line{ - start: start.nWest(), - stop: start, - orientation: SE, - }, - ) - } - case NE: - r := c.runeAt(start.nEast()) - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append( - triangles, - Line{ - start: start, - stop: start.nEast(), - orientation: NE, - }, - ) - } - case S: - r := c.runeAt(start.south()) - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append(triangles, newHalfStep(start, S)) - } - case SE: - r := c.runeAt(start.sEast()) - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append( - triangles, - Line{ - start: start, - stop: start.sEast(), - orientation: SE, - }, - ) - } - case SW: - r := c.runeAt(start.sWest()) - if r == '-' || isJoint(r) && !isDot(r) { - needsNudging = true - triangles = append( - triangles, - Line{ - start: start.sWest(), - stop: start, - orientation: NE, - }, - ) - } - case W: - r := c.runeAt(start.west()) - if isDot(r) { - needsNudging = true - } - case E: - r := c.runeAt(start.east()) - if isDot(r) { - needsNudging = true - } - } - - triangles = append( - triangles, - Triangle{ - start: start, - orientation: o, - needsNudging: needsNudging, - }, - ) - } - return -} - -// Circles returns a slice of all 'o' and '*' characters not considered text. -func (c *Canvas) Circles() (circles []Circle) { - for idx := range upDown(c.Width, c.Height) { - // TODO INCOMING - if c.runeAt(idx) == 'o' { - circles = append(circles, Circle{start: idx}) - } else if c.runeAt(idx) == '*' { - circles = append(circles, Circle{start: idx, bold: true}) - } - } - return -} - -// RoundedCorners returns a slice of all curvy corners in the diagram. -func (c *Canvas) RoundedCorners() (corners []RoundedCorner) { - for idx := range leftRight(c.Width, c.Height) { - if o := c.isRoundedCorner(idx); o != NONE { - corners = append( - corners, - RoundedCorner{start: idx, orientation: o}, - ) - } - } - return -} - -// For . and ' characters this will return a non-NONE orientation if the -// character falls on a rounded corner. -func (c *Canvas) isRoundedCorner(i Index) Orientation { - r := c.runeAt(i) - - if !isJoint(r) { - return NONE - } - - left := i.west() - right := i.east() - lowerLeft := i.sWest() - lowerRight := i.sEast() - upperLeft := i.nWest() - upperRight := i.nEast() - - opensUp := r == '\'' || r == '+' - opensDown := r == '.' || r == '+' - - dashRight := c.runeAt(right) == '-' || c.runeAt(right) == '+' || c.runeAt(right) == '_' || c.runeAt(upperRight) == '_' - dashLeft := c.runeAt(left) == '-' || c.runeAt(left) == '+' || c.runeAt(left) == '_' || c.runeAt(upperLeft) == '_' - - isVerticalSegment := func(i Index) bool { - r := c.runeAt(i) - return r == '|' || r == '+' || r == ')' || r == '(' || isDot(r) - } - - // .- or .- - // | + - if opensDown && dashRight && isVerticalSegment(lowerLeft) { - return NW - } - - // -. or -. or -. or _. or -. - // | + ) ) o - if opensDown && dashLeft && isVerticalSegment(lowerRight) { - return NE - } - - // | or + or | or + or + or_ ) - // -' -' +' +' ++ ' - if opensUp && dashLeft && isVerticalSegment(upperRight) { - return SE - } - - // | or + - // '- '- - if opensUp && dashRight && isVerticalSegment(upperLeft) { - return SW - } - - return NONE -} - -// Text returns a slice of all text characters not belonging to part of the diagram. -// Must be stably sorted, to satisfy regression tests. -func (c *Canvas) Text() (text []Text) { - for idx := range leftRight(c.Width, c.Height) { - r, found := c.text[idx] - if !found { - continue - } - text = append(text, Text{ - start: idx, - str: string(r)}) - } - return -} - -// Bridges returns a slice of all bridges, "-)-" or "-(-", composed as a sequence of -// either type Bridge or type Line. -func (c *Canvas) Bridges() (bridges []Drawable) { - for idx := range leftRight(c.Width, c.Height) { - if o := c.isBridge(idx); o != NONE { - bridges = append( - bridges, - newHalfStep(idx.north(), S), - newHalfStep(idx.south(), N), - Bridge{ - start: idx, - orientation: o, - }, - ) - } - } - return -} - -// -)- or -(- or -func (c *Canvas) isBridge(i Index) Orientation { - r := c.runeAt(i) - - left := c.runeAt(i.west()) - right := c.runeAt(i.east()) - - if left != '-' || right != '-' { - return NONE - } - - if r == '(' { - return W - } - - if r == ')' { - return E - } - - return NONE -} func (c *Canvas) shouldMoveToText(i Index) bool { i_r := c.runeAt(i) diff --git a/drawable.go b/drawable.go new file mode 100644 index 0000000..3ad14e4 --- /dev/null +++ b/drawable.go @@ -0,0 +1,756 @@ +package goat + +import ( + "io" +) + +// Drawable represents anything that can Draw itself. +type Drawable interface { + draw(out io.Writer) +} + +// Line represents a straight segment between two points 'start' and 'stop', where +// 'start' is either lesser in X (north-east, east, south-east), or +// equal in X and lesser in Y (south). +type Line struct { + start Index + stop Index + + startRune rune + stopRune rune + + // dashed bool + needsNudgingDown bool + needsNudgingLeft bool + needsNudgingRight bool + needsTinyNudgingLeft bool + needsTinyNudgingRight bool + + // This is a line segment all by itself. This centers the segment around + // the midline. + lonely bool + // N or S. Only useful for half steps - chops of this half of the line. + chop Orientation + + // X-major, Y-minor. Therefore, always one of the compass points NE, E, SE, S. + orientation Orientation + + state lineState +} + +type lineState int + +const ( + _Unstarted lineState = iota + _Started +) + +func (l *Line) started() bool { + return l.state == _Started +} + +func (c *Canvas) setStart(l *Line, i Index) { + if l.state == _Unstarted { + l.start = i + l.startRune = c.runeAt(i) + l.stop = i + l.stopRune = c.runeAt(i) + l.state = _Started + } +} + +func (c *Canvas) setStop(l *Line, i Index) { + if l.state == _Started { + l.stop = i + l.stopRune = c.runeAt(i) + } +} + +func (l *Line) goesSomewhere() bool { + return l.start != l.stop +} + +func (l *Line) horizontal() bool { + return l.orientation == E || l.orientation == W +} + +func (l *Line) vertical() bool { + return l.orientation == N || l.orientation == S +} + +func (l *Line) diagonal() bool { + return l.orientation == NE || l.orientation == SE || l.orientation == SW || l.orientation == NW +} + +// XX drop names 'start' below + +// Triangle corresponds to "^", "v", "<" and ">" runes in the absence of +// surrounding alphanumerics. +type Triangle struct { + start Index + orientation Orientation + needsNudging bool +} + +// Circle corresponds to "o" or "*" runes in the absence of surrounding +// alphanumerics. +type Circle struct { + start Index + bold bool +} + +// RoundedCorner corresponds to combinations of "-." or "-'". +type RoundedCorner struct { + start Index + orientation Orientation +} + +// Text corresponds to any runes not reserved for diagrams, or reserved runes +// surrounded by alphanumerics. +type Text struct { + start Index + str string // Possibly multiple bytes, from Unicode source of type 'rune' +} + +// Bridge corresponds to combinations of "-)-" or "-(-" and is displayed as +// the vertical line "hopping over" the horizontal. +type Bridge struct { + start Index + orientation Orientation +} + +// Orientation represents the primary direction that a Drawable is facing. +type Orientation int + +const ( + NONE Orientation = iota // No orientation; no structure present. + N // North + NE // Northeast + NW // Northwest + S // South + SE // Southeast + SW // Southwest + E // East + W // West +) + +// WriteSVGBody writes the entire content of a Canvas out to a stream in SVG format. +func (c *Canvas) WriteSVGBody(dst io.Writer) { + writeBytes(dst, "\n") + + for _, l := range c.Lines() { + l.draw(dst) + } + + for _, tI := range c.Triangles() { + tI.draw(dst) + } + + for _, c := range c.RoundedCorners() { + c.draw(dst) + } + + for _, c := range c.Circles() { + c.draw(dst) + } + + for _, bI := range c.Bridges() { + bI.draw(dst) + } + + writeText(dst, c) + + writeBytes(dst, "\n") +} + +// Lines returns a slice of all Line drawables that we can detect -- in all +// possible orientations. +func (c *Canvas) Lines() (lines []Line) { + horizontalMidlines := c.getLinesForSegment('-') + + diagUpLines := c.getLinesForSegment('/') + for i, l := range diagUpLines { + // /_ + if c.runeAt(l.start.east()) == '_' { + diagUpLines[i].needsTinyNudgingLeft = true + } + + // _ + // / + if c.runeAt(l.stop.north()) == '_' { + diagUpLines[i].needsTinyNudgingRight = true + } + + // _ + // / + if !l.lonely && c.runeAt(l.stop.nEast()) == '_' { + diagUpLines[i].needsTinyNudgingRight = true + } + + // _/ + if !l.lonely && c.runeAt(l.start.west()) == '_' { + diagUpLines[i].needsTinyNudgingLeft = true + } + + // \ + // / + if !l.lonely && c.runeAt(l.stop.north()) == '\\' { + diagUpLines[i].needsTinyNudgingRight = true + } + + // / + // \ + if !l.lonely && c.runeAt(l.start.south()) == '\\' { + diagUpLines[i].needsTinyNudgingLeft = true + } + } + + diagDownLines := c.getLinesForSegment('\\') + for i, l := range diagDownLines { + // _\ + if c.runeAt(l.stop.west()) == '_' { + diagDownLines[i].needsTinyNudgingRight = true + } + + // _ + // \ + if c.runeAt(l.start.north()) == '_' { + diagDownLines[i].needsTinyNudgingLeft = true + } + + // _ + // \ + if !l.lonely && c.runeAt(l.start.nWest()) == '_' { + diagDownLines[i].needsTinyNudgingLeft = true + } + + // \_ + if !l.lonely && c.runeAt(l.stop.east()) == '_' { + diagDownLines[i].needsTinyNudgingRight = true + } + + // \ + // / + if !l.lonely && c.runeAt(l.stop.south()) == '/' { + diagDownLines[i].needsTinyNudgingRight = true + } + + // / + // \ + if !l.lonely && c.runeAt(l.start.north()) == '/' { + diagDownLines[i].needsTinyNudgingLeft = true + } + } + + horizontalBaselines := c.getLinesForSegment('_') + for i, l := range horizontalBaselines { + // TODO: make this nudge an orientation + horizontalBaselines[i].needsNudgingDown = true + + // _ + // _| | + if c.runeAt(l.stop.sEast()) == '|' || c.runeAt(l.stop.nEast()) == '|' { + horizontalBaselines[i].needsNudgingRight = true + } + + // _ + // | _| + if c.runeAt(l.start.sWest()) == '|' || c.runeAt(l.start.nWest()) == '|' { + horizontalBaselines[i].needsNudgingLeft = true + } + + // _ + // _/ \ + if c.runeAt(l.stop.east()) == '/' || c.runeAt(l.stop.sEast()) == '\\' { + horizontalBaselines[i].needsTinyNudgingRight = true + } + + // _ + // \_ / + if c.runeAt(l.start.west()) == '\\' || c.runeAt(l.start.sWest()) == '/' { + horizontalBaselines[i].needsTinyNudgingLeft = true + } + + // _\ + if c.runeAt(l.stop.east()) == '\\' { + horizontalBaselines[i].needsNudgingRight = true + horizontalBaselines[i].needsTinyNudgingRight = true + } + + // + // /_ + if c.runeAt(l.start.west()) == '/' { + horizontalBaselines[i].needsNudgingLeft = true + horizontalBaselines[i].needsTinyNudgingLeft = true + } + // _ + // / + if c.runeAt(l.stop.south()) == '/' { + horizontalBaselines[i].needsTinyNudgingRight = true + } + + // _ + // \ + if c.runeAt(l.start.south()) == '\\' { + horizontalBaselines[i].needsTinyNudgingLeft = true + } + + // _ + // ' + if c.runeAt(l.start.sWest()) == '\'' { + horizontalBaselines[i].needsNudgingLeft = true + } + + // _ + // ' + if c.runeAt(l.stop.sEast()) == '\'' { + horizontalBaselines[i].needsNudgingRight = true + } + } + + verticalLines := c.getLinesForSegment('|') + + lines = append(lines, horizontalMidlines...) + lines = append(lines, horizontalBaselines...) + lines = append(lines, verticalLines...) + lines = append(lines, diagUpLines...) + lines = append(lines, diagDownLines...) + lines = append(lines, c.HalfSteps()...) // vertical, only + + return +} + +func newHalfStep(i Index, chop Orientation) Line { + return Line{ + start: i, + stop: i.south(), + lonely: true, + chop: chop, + orientation: S, + } +} + +func (c *Canvas) HalfSteps() (lines []Line) { + for idx := range upDown(c.Width, c.Height) { + if o := c.partOfHalfStep(idx); o != NONE { + lines = append( + lines, + newHalfStep(idx, o), + ) + } + } + return +} + +func (c *Canvas) getLinesForSegment(segment rune) []Line { + var iter canvasIterator + var orientation Orientation + var passThroughs []rune + + switch segment { + case '-': + iter = leftRight + orientation = E + passThroughs = append(jointRunes, '<', '>', '(', ')') + case '_': + iter = leftRight + orientation = E + passThroughs = append(jointRunes, '|') + case '|': + iter = upDown + orientation = S + passThroughs = append(jointRunes, '^', 'v') + case '/': + iter = diagUp + orientation = NE + passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|') + case '\\': + iter = diagDown + orientation = SE + passThroughs = append(jointRunes, 'o', '*', '<', '>', '^', 'v', '|') + default: + return nil + } + + return c.getLines(iter, segment, passThroughs, orientation) +} + +// ci: the order that we traverse locations on the canvas. +// segment: the primary character we're tracking for this line. +// passThroughs: characters the line segment is allowed to be drawn underneath +// (without terminating the line). +// orientation: the orientation for this line. +func (c *Canvas) getLines( + ci canvasIterator, + segment rune, + passThroughs []rune, + o Orientation, +) (lines []Line) { + // Helper to throw the current line we're tracking on to the slice and + // start a new one. + snip := func(cl Line) Line { + // Only collect lines that actually go somewhere or are isolated + // segments; otherwise, discard what's been collected so far within 'cl'. + if cl.goesSomewhere() { + lines = append(lines, cl) + } + + return Line{orientation: o} + } + + currentLine := Line{orientation: o} + lastSeenRune := ' ' + + for idx := range ci(c.Width+1, c.Height+1) { + r := c.runeAt(idx) + + isSegment := r == segment + isPassThrough := contains(passThroughs, r) + isRoundedCorner := c.isRoundedCorner(idx) + isDot := isDot(r) + isTriangle := isTriangle(r) + + justPassedThrough := contains(passThroughs, lastSeenRune) + + shouldKeep := (isSegment || isPassThrough) && isRoundedCorner == NONE + + // This is an edge case where we have a rounded corner... that's also a + // joint... attached to orthogonal line, e.g.: + // + // '+-- + // | + // + // TODO: This also depends on the orientation of the corner and our + // line. + // NW / NE line can't go with EW/NS lines, vertical is OK though. + if isRoundedCorner != NONE && o != E && (c.partOfVerticalLine(idx) || c.partOfDiagonalLine(idx)) { + shouldKeep = true + } + + // Don't connect | to > for diagonal lines or )) for horizontal lines. + if isPassThrough && justPassedThrough && o != S { + currentLine = snip(currentLine) + } + + // Don't connect o to o, + to o, etc. This character is a new pass-through + // so we still want to respect shouldKeep; we just don't want to draw + // the existing line through this cell. + if justPassedThrough && (isDot || isTriangle) { + currentLine = snip(currentLine) + } + + switch currentLine.state { + case _Unstarted: + if shouldKeep { + c.setStart(¤tLine, idx) + } + case _Started: + if !shouldKeep { + // Snip the existing line, don't add the current cell to it + // *unless* its a line segment all by itself. If it is, keep a + // record that it's an individual segment because we need to + // adjust later in the / and \ cases. + if !currentLine.goesSomewhere() && lastSeenRune == segment { + if !c.partOfRoundedCorner(currentLine.start) { + c.setStop(¤tLine, idx) + currentLine.lonely = true + } + } + currentLine = snip(currentLine) + } else if isPassThrough { + // Snip the existing line but include the current pass-through + // character because we may be continuing the line. + c.setStop(¤tLine, idx) + currentLine = snip(currentLine) + c.setStart(¤tLine, idx) + } else if shouldKeep { + // Keep the line going and extend it by this character. + c.setStop(¤tLine, idx) + } + } + + lastSeenRune = r + } + return +} + +// Triangles detects intended triangles -- typically at the end of an intended line -- +// and returns a representational slice composed of types Triangle and Line. +func (c *Canvas) Triangles() (triangles []Drawable) { + o := NONE + + for idx := range upDown(c.Width, c.Height) { + needsNudging := false + start := idx + + r := c.runeAt(idx) + + if !isTriangle(r) { + continue + } + + // Identify orientation and nudge the triangle to touch any + // adjacent walls. + switch r { + case '^': + o = N + // ^ and ^ + // / \ + if c.runeAt(start.sWest()) == '/' { + o = NE + } else if c.runeAt(start.sEast()) == '\\' { + o = NW + } + case 'v': + if c.runeAt(start.north()) == '|' { + // | + // v + o = S + } else if c.runeAt(start.nEast()) == '/' { + // / + // v + o = SW + } else if c.runeAt(start.nWest()) == '\\' { + // \ + // v + o = SE + } else { + // Conclusion: Meant as a text string 'v', not a triangle + //panic("Not sufficient to fix all 'v' troubles.") + // continue XX Already committed to non-text output for this string? + o = S + } + case '<': + o = W + case '>': + o = E + } + + // Determine if we need to snap the triangle to something and, if so, + // draw a tail if we need to. + switch o { + case N: + r := c.runeAt(start.north()) + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append(triangles, newHalfStep(start, N)) + } + case NW: + r := c.runeAt(start.nWest()) + // Need to draw a tail. + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append( + triangles, + Line{ + start: start.nWest(), + stop: start, + orientation: SE, + }, + ) + } + case NE: + r := c.runeAt(start.nEast()) + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append( + triangles, + Line{ + start: start, + stop: start.nEast(), + orientation: NE, + }, + ) + } + case S: + r := c.runeAt(start.south()) + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append(triangles, newHalfStep(start, S)) + } + case SE: + r := c.runeAt(start.sEast()) + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append( + triangles, + Line{ + start: start, + stop: start.sEast(), + orientation: SE, + }, + ) + } + case SW: + r := c.runeAt(start.sWest()) + if r == '-' || isJoint(r) && !isDot(r) { + needsNudging = true + triangles = append( + triangles, + Line{ + start: start.sWest(), + stop: start, + orientation: NE, + }, + ) + } + case W: + r := c.runeAt(start.west()) + if isDot(r) { + needsNudging = true + } + case E: + r := c.runeAt(start.east()) + if isDot(r) { + needsNudging = true + } + } + + triangles = append( + triangles, + Triangle{ + start: start, + orientation: o, + needsNudging: needsNudging, + }, + ) + } + return +} + +// Circles returns a slice of all 'o' and '*' characters not considered text. +func (c *Canvas) Circles() (circles []Circle) { + for idx := range upDown(c.Width, c.Height) { + // TODO INCOMING + if c.runeAt(idx) == 'o' { + circles = append(circles, Circle{start: idx}) + } else if c.runeAt(idx) == '*' { + circles = append(circles, Circle{start: idx, bold: true}) + } + } + return +} + +// RoundedCorners returns a slice of all curvy corners in the diagram. +func (c *Canvas) RoundedCorners() (corners []RoundedCorner) { + for idx := range leftRight(c.Width, c.Height) { + if o := c.isRoundedCorner(idx); o != NONE { + corners = append( + corners, + RoundedCorner{start: idx, orientation: o}, + ) + } + } + return +} + +// For . and ' characters this will return a non-NONE orientation if the +// character falls on a rounded corner. +func (c *Canvas) isRoundedCorner(i Index) Orientation { + r := c.runeAt(i) + + if !isJoint(r) { + return NONE + } + + left := i.west() + right := i.east() + lowerLeft := i.sWest() + lowerRight := i.sEast() + upperLeft := i.nWest() + upperRight := i.nEast() + + opensUp := r == '\'' || r == '+' + opensDown := r == '.' || r == '+' + + dashRight := c.runeAt(right) == '-' || c.runeAt(right) == '+' || c.runeAt(right) == '_' || c.runeAt(upperRight) == '_' + dashLeft := c.runeAt(left) == '-' || c.runeAt(left) == '+' || c.runeAt(left) == '_' || c.runeAt(upperLeft) == '_' + + isVerticalSegment := func(i Index) bool { + r := c.runeAt(i) + return r == '|' || r == '+' || r == ')' || r == '(' || isDot(r) + } + + // .- or .- + // | + + if opensDown && dashRight && isVerticalSegment(lowerLeft) { + return NW + } + + // -. or -. or -. or _. or -. + // | + ) ) o + if opensDown && dashLeft && isVerticalSegment(lowerRight) { + return NE + } + + // | or + or | or + or + or_ ) + // -' -' +' +' ++ ' + if opensUp && dashLeft && isVerticalSegment(upperRight) { + return SE + } + + // | or + + // '- '- + if opensUp && dashRight && isVerticalSegment(upperLeft) { + return SW + } + + return NONE +} + +// Text returns a slice of all text characters not belonging to part of the diagram. +// Must be stably sorted, to satisfy regression tests. +func (c *Canvas) Text() (text []Text) { + for idx := range leftRight(c.Width, c.Height) { + r, found := c.text[idx] + if !found { + continue + } + text = append(text, Text{ + start: idx, + str: string(r)}) + } + return +} + +// Bridges returns a slice of all bridges, "-)-" or "-(-", composed as a sequence of +// either type Bridge or type Line. +func (c *Canvas) Bridges() (bridges []Drawable) { + for idx := range leftRight(c.Width, c.Height) { + if o := c.isBridge(idx); o != NONE { + bridges = append( + bridges, + newHalfStep(idx.north(), S), + newHalfStep(idx.south(), N), + Bridge{ + start: idx, + orientation: o, + }, + ) + } + } + return +} + +// -)- or -(- or +func (c *Canvas) isBridge(i Index) Orientation { + r := c.runeAt(i) + + left := c.runeAt(i.west()) + right := c.runeAt(i.east()) + + if left != '-' || right != '-' { + return NONE + } + + if r == '(' { + return W + } + + if r == ')' { + return E + } + + return NONE +} + From bd89bfd86b784e13cc3f811838590abbbec2ad59 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 1 Jul 2024 13:54:18 -0700 Subject: [PATCH 13/16] Refactor for readability; comments --- canvas.go | 6 +++--- drawable.go | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/canvas.go b/canvas.go index 51c35a4..d1edf88 100644 --- a/canvas.go +++ b/canvas.go @@ -12,8 +12,8 @@ type ( // Characters where more than one line segment can come together. var jointRunes = []rune{ - '.', - '\'', + '.', // possible ... top corner of a 90 degree angle, or curve + '\'', // possible ... bottom corner of a 90 degree angle, or curve '+', '*', 'o', @@ -84,7 +84,7 @@ func isJoint(r rune) bool { return contains(jointRunes, r) } -// XX rename 'isCircle()'? +// XX rename 'isSpot()'? func isDot(r rune) bool { return r == 'o' || r == '*' } diff --git a/drawable.go b/drawable.go index 3ad14e4..65bae81 100644 --- a/drawable.go +++ b/drawable.go @@ -372,19 +372,20 @@ func (c *Canvas) getLinesForSegment(segment rune) []Line { return nil } - return c.getLines(iter, segment, passThroughs, orientation) + return c.getLines(segment, iter, orientation, passThroughs) } -// ci: the order that we traverse locations on the canvas. -// segment: the primary character we're tracking for this line. -// passThroughs: characters the line segment is allowed to be drawn underneath -// (without terminating the line). +// segment: the primary character expected along a continuing Line +// ci: the order that the loop below traverse locations on the canvas. // orientation: the orientation for this line. +// passThroughs: characters that will produce a mark that the line segment +// is allowed to be drawn either through or, in the case of 'o', "underneath" -- +// without terminating the line. func (c *Canvas) getLines( - ci canvasIterator, segment rune, - passThroughs []rune, + ci canvasIterator, o Orientation, + passThroughs []rune, ) (lines []Line) { // Helper to throw the current line we're tracking on to the slice and // start a new one. @@ -401,6 +402,7 @@ func (c *Canvas) getLines( currentLine := Line{orientation: o} lastSeenRune := ' ' + // X Purpose of the '+1' overscan is to reset lastSeenRune to ' ' upon wrapping the minor axis. for idx := range ci(c.Width+1, c.Height+1) { r := c.runeAt(idx) From 954bfd0e881f15be1a0d5d1f423e3cf2281b1cf6 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 1 Jul 2024 16:08:45 -0700 Subject: [PATCH 14/16] Better diagnostic upon abort for TAB char in input --- canvas.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/canvas.go b/canvas.go index d1edf88..fba894a 100644 --- a/canvas.go +++ b/canvas.go @@ -2,7 +2,9 @@ package goat import ( "bufio" + "log" "io" + "os" ) type ( @@ -156,14 +158,19 @@ func NewCanvas(in io.Reader) (c Canvas) { // https://go.dev/ref/spec#For_statements // But yet, counterintuitively, type of lineStr[_index_] is 'byte'. // https://go.dev/ref/spec#String_types - // XXXX Refactor to use []rune from above. for _, r := range lineStr { //if r > 255 { // fmt.Printf("linestr=\"%s\"\n", lineStr) // fmt.Printf("r == 0x%x\n", r) //} if r == ' ' { - panic("TAB character found on input") + file, isFile := in.(*os.File) + fileName := "unknown" + if isFile { + fileName = file.Name() + } + log.Panicf("\n\tFound TAB in %s, row %d, column %d\n", + fileName, height+1, w) } i := Index{w, height} c.data[i] = r From 7b1281f99d4edbafe91f0576c6f3928def44d073 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 1 Jul 2024 16:37:58 -0700 Subject: [PATCH 15/16] Issue #25 -- Vertical edges of boxes run together Regression Testing --- Ran ./pre-push: SVG output of three files changed. Log excerpt: === RUN TestExamples ... examples-regression_test.go:83: Failed to verify contents of 3 .svg files examples-regression_test.go:130: Writing new SVG and HTML delta files into _examples_new/ examples-regression_test.go:149: complicated.html examples-regression_test.go:149: regression.html examples-regression_test.go:149: small-grids.html ... Visual inspection of .html files in browser shows fix effective in _examples_new/small-grids.{html,svg}, and no visually apparent change in the other two sets of results. --- drawable.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drawable.go b/drawable.go index 65bae81..c76912b 100644 --- a/drawable.go +++ b/drawable.go @@ -441,6 +441,10 @@ func (c *Canvas) getLines( currentLine = snip(currentLine) } + if o == S && (r == '.' || lastSeenRune == '\'') { + currentLine = snip(currentLine) + } + switch currentLine.state { case _Unstarted: if shouldKeep { From a1befb34667403ac927c5c0b507f0e647d20e602 Mon Sep 17 00:00:00 2001 From: dmullis Date: Mon, 1 Jul 2024 19:56:36 -0700 Subject: [PATCH 16/16] Update generated SVG and Markdown files, for GitHub --- README.md | 53 +++- examples/complicated.svg | 1 - examples/regression.svg | 2 +- examples/small-grids.svg | 620 ++++++++++++++++++++++++--------------- 4 files changed, 423 insertions(+), 253 deletions(-) diff --git a/README.md b/README.md index 16bda42..9997522 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ => CONCLUSION: Don't use badges. Instead, pay attention to Action result status. --> - + ## What **GoAT** Can Do For You @@ -58,7 +58,7 @@ with rows above and below. ## Installation ``` - $ go install github.com/blampe/goat/cmd/goat@latest + $ go install github.com/dmullis/goat/cmd/goat@latest ``` @@ -176,14 +176,39 @@ Note that '·' above is not ASCII, but rather Unicode, the MIDDLE DOT character, ### Small Grids ``` - ___ ___ .---+---+---+---+---. .---+---+---+---. .---. .---. .---. .---. .-. .-. 0 - ___/ \___/ \ | | | | | | / \ / \ / \ / \ / | +---+ | | A | | B | | A | | B | 1 - / \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ +---+ '---' '---' '-' '-' 2 - \___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ | +---+ | .---. .---. .-. .-. 3 - / a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +---+ b +---+ | C | | D | | C | | D | 4 - \___/ \___/ \ | | a | | | | / \ / \ / \ / \ / | a +---+ | '---' '---' '-' '-' 5 - \___/ \___/ '---+---+---+---+---' '---+---+---+---' '---' '---' 0123456789012345678901234 6 - + ___ ___ .---+---+---+---+---. .---+---+---+---. + ___/ \___/ \ | | | | | | / \ / \ / \ / \ / +/ \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +\___/ b \___/ \ | | | b | | | \ / \a/ \b/ \ / \ +/ a \___/ \___/ +---+---+---+---+---+ +---+---+---+---+ +\___/ \___/ \ | | a | | | | / \ / \ / \ / \ / + \___/ \___/ '---+---+---+---+---' '---+---+---+---' + +.---. .---. +--+ +-+ +-+ .-. .--. .-. +-+ +--+ .-. .--. +| | | | | | | | | | | | | | | | | | | | | | | | ++---+ +---+ +--+ +-+ | | | | | | | | +-+ +--+ '-' '--' +| +---+ | | | | | | | | | | | | | ++---+ b +---+ +--+ +-+ | | | | | | | | +| a +---+ | | | | | | | | | | | | | +'---' '---' +--+ +-+ +-+ '-' '--' '-' + ++---+ .---. .---. +| B | | E | | F | ++---+ '---' '---' +.---. +---+ .---. +| D | | G | | H | +'---' +---+ '---' +0123456789012345678901232 + +Not Supported: + .---+---+---. ++---+ .-. .-. 0 | | a | | b | +| A | | A | | B | 1 +++ +--++-+-++--+ ++---+ '-' '-' 2 | | | | | | | ++---+ .-. .-. 3 +-+ +--++-+-++--+ +| C | | C | | D | 4 | | | | | | ++---+ '-' '-' 5 +++ '---+---+---' +0123434567890123456 | ``` ![](//examples/small-grids.svg) @@ -244,12 +269,12 @@ The core engine of ```goat``` is accessible as a Go library package, for inclusi code of your own. The code implements a subset, and some extensions, of the ASCII diagram generation function of the browser-side Javascript in [Markdeep](http://casual-effects.com/markdeep/). -A nicely formatted reference may be found at [pkg.go.dev](https://pkg.go.dev/github.com/blampe/goat). +A nicely formatted reference may be found at [pkg.go.dev](https://pkg.go.dev/github.com/dmullis/goat). ### Installation ``` - $ go get -u github.com/blampe/goat/ + $ go get -u github.com/dmullis/goat/ ``` ### Library Data Flow ![](//goat.svg) @@ -261,13 +286,13 @@ source file [./goat.go](./goat.go). ### Project Tenets diff --git a/examples/complicated.svg b/examples/complicated.svg index 20b5498..0bb438a 100644 --- a/examples/complicated.svg +++ b/examples/complicated.svg @@ -134,7 +134,6 @@ svg { - diff --git a/examples/regression.svg b/examples/regression.svg index f729e42..0d716e3 100644 --- a/examples/regression.svg +++ b/examples/regression.svg @@ -105,7 +105,7 @@ svg { - + diff --git a/examples/small-grids.svg b/examples/small-grids.svg index ceb2e73..529a066 100644 --- a/examples/small-grids.svg +++ b/examples/small-grids.svg @@ -1,4 +1,4 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -0 -A -B -A -B -1 -2 -b -b -a -b -3 -a -b -C -D -C -D -4 -a -a -5 -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 -6 +b +b +a +b +a +a +b +a +B +E +F +D +G +H +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +2 +N +o +t +S +u +p +p +o +r +t +e +d +: +0 +a +b +A +A +B +1 +2 +3 +C +C +D +4 +5 +0 +1 +2 +3 +4 +3 +4 +5 +6 +7 +8 +9 +0 +1 +2 +3 +4 +5 +6