@ 87d9e2c8fb91e219b45806d4a348816f6da0a39d | history
/*
Package Cowsay implements the functionality of the classic [cowsay] Perl
script.
All classic cowfiles have been converted from the original Perl syntax into
Go's text/template format in order to help spread ASCII art cow culture ever
further into the computer systems of the world.
[cowsay]: https://en.wikipedia.org/wiki/Cowsay
*/
package cowsay
import (
"embed"
"fmt"
"io"
"os"
"regexp"
"strings"
"text/template"
)
//go:embed cows
var cowdir embed.FS
// Cowfiles is associated with templates of all the builtin cowfiles.
var Cowfiles *template.Template
func init() {
var err error
Cowfiles, err = template.ParseFS(cowdir, "cows/*")
if err != nil {
panic(err.Error())
}
}
const defaultWidth = 40
// Cow can be configured to say things in different ways.
type Cow struct {
// Mood determines the appearance of the cow's eyes and tongue.
Mood Mood
// Width determines the wrap width of the text inside the speech
// bubble. Setting Width < 0 results in no word wrap. A value of 0
// will use the default width (40-ish).
Width int
body *template.Template
}
type bubble struct {
width int
startL rune
startR rune
bigSideL rune
bigSideR rune
endL rune
endR rune
smallSideL rune
smallSideR rune
voice string
}
var (
sayBubble = &bubble{0,
'/', '\\',
'|', '|',
'\\', '/',
'<', '>',
`\`}
thinkBubble = &bubble{0,
'(', ')',
'(', ')',
'(', ')',
'(', ')',
`o`}
)
// Say prints an ASCII art Cow speaking the input string to stdout, followed by
// a newline.
func (c Cow) Say(s string) {
fmt.Fprintln(os.Stdout, c.moo(s, false))
}
// Think prints an ASCII art Cow thinking the input string to stdout, followed
// by a newline.
func (c Cow) Think(s string) {
fmt.Fprintln(os.Stdout, c.moo(s, true))
}
// Write writes an ASCII art Cow speaking or thinking the input string to the
// provided io.Writer. If think == true, then the Cow will appear thinking;
// otherwise speaking.
func (c Cow) Write(w io.Writer, s []byte, think bool) (int, error) {
return w.Write([]byte(c.moo(string(s), think)))
}
// Cowsay is provided for convenience. It is identical to Cow{}.Say. It prints
// the default cow speaking the input string to stdout, followed by a newline.
func Cowsay(s string) {
Cow{}.Say(s)
}
// Mood determines the appearance of the Cow's eyes and tongue.
type Mood struct {
// Eyes contains a string representation of a Cow's eyes. The first two
// runes of Eyes are used as .Eyes in cowfiles, and the rest is
// ignored. If uninitialized, "oo" will be used. If Eyes has a length
// of 1, it will be right-padded with a space.
Eyes string
// Tongue contains a string representation of a Cow's tongue. The first
// two runes of Tongue are used as .Tongue in cowfiles, and the rest is
// ignored. If uninitialized, " " (two spaces) will be used. If
// Tongue has a length of 1, it will be right-padded with a space.
Tongue string
}
var (
Default = Mood{"oo", ""}
Borg = Mood{"==", ""}
Greedy = Mood{"$$", ""}
Paranoid = Mood{"@@", ""}
Stoned = Mood{"**", "U "}
Tired = Mood{"--", ""}
Wired = Mood{"OO", ""}
Youthful = Mood{"..", ""}
Dead = Mood{"xx", "U "}
)
func (c *Cow) moo(speech string, think bool) string {
var body *template.Template
if c.body == nil {
body = Cowfiles.Lookup("default.cow")
} else {
body = c.body
}
var bubble *bubble
if think {
bubble = thinkBubble
} else {
bubble = sayBubble
}
bubble.width = c.Width
var eyes string
if c.Mood.Eyes == "" {
eyes = "oo"
} else {
eyes = fmt.Sprintf("%-2s", c.Mood.Eyes)[:2]
}
tongue := fmt.Sprintf("%-2s", c.Mood.Tongue)[:2]
data := struct {
Mood
Thoughts string
}{
Mood{eyes, tongue},
bubble.voice,
}
var cowtput strings.Builder
_, _ = cowtput.WriteString(bubble.of(speech))
_ = cowtput.WriteByte('\n')
_ = body.Execute(&cowtput, data)
return cowtput.String()
}
func (b *bubble) of(speech string) string {
var (
noWrap bool
longest int
udderance = make([]string, 0)
)
switch {
case b.width < 0:
noWrap = true
case b.width == 0:
b.width = defaultWidth
default:
}
pad := " "
innerTextWidth := b.width - 1 // adjustment to match original cowsay
if !noWrap {
hspace := regexp.MustCompile(`[ \t]+`)
vspace := regexp.MustCompile(`[\r\n]{2,}`)
speech = hspace.ReplaceAllString(speech, " ")
speech = vspace.ReplaceAllString(speech, "\n\n")
}
lines := strings.Split(speech, "\n")
// first wrap long lines and determine the length of the longest
for _, line := range lines {
if noWrap {
if len(line) > longest {
longest = len(line)
}
udderance = append(udderance, line)
} else {
wlns := wrap(line, innerTextWidth)
for _, wln := range wlns {
if len(wln) > longest {
longest = len(wln)
}
udderance = append(udderance, wln)
}
}
}
// then pad each line to the length of the longest and add bubble sides
if len(udderance) == 1 {
wln := udderance[0]
l, r := b.smallSideL, b.smallSideR
var b strings.Builder
b.WriteRune(l)
b.WriteString(pad)
b.WriteString(wln)
b.WriteString(pad)
b.WriteRune(r)
udderance[0] = b.String()
} else {
for i, wln := range udderance {
var l, r rune
switch {
case i == 0:
l, r = b.startL, b.startR
case i == len(udderance)-1:
l, r = b.endL, b.endR
default:
l, r = b.bigSideL, b.bigSideR
}
room := strings.Repeat(pad, longest-len(wln))
var b strings.Builder
b.WriteRune(l)
b.WriteString(pad)
b.WriteString(wln)
b.WriteString(room)
b.WriteString(pad)
b.WriteRune(r)
udderance[i] = b.String()
}
}
// append top & bottom of bubble
var o strings.Builder
w := longest + 2*len(pad)
o.WriteString(pad)
o.WriteString(strings.Repeat("_", w))
o.WriteByte('\n')
o.WriteString(strings.Join(udderance, "\n"))
o.WriteByte('\n')
o.WriteString(pad)
o.WriteString(strings.Repeat("-", w))
return o.String()
}
// This is a great little recursive word wrapping algo, courtesy of Peter
// Mortensen: https://stackoverflow.com/a/857770
func wrap(text string, width int) []string {
text = strings.TrimSpace(text)
if len(text) <= width {
return []string{text}
} else {
isplit := width
for i := width; i > 0; i-- {
if text[i] == ' ' {
isplit = i
break
}
}
before := strings.TrimRight(text[:isplit], " ")
after := strings.TrimLeft(text[isplit:], " ")
return append([]string{before}, wrap(after, width)...)
}
}
// Load loads a cowfile from the provided path. Builtin cowfiles can be
// specified by either the basename, or basename with extension removed; e.g.
// Load("default") and Load("default.cow") both load the included default
// cowfile.
func Load(path string) (Cow, error) {
var tmpl *template.Template
if t, err := template.ParseFiles(path); err == nil {
tmpl = t
} else if t := Cowfiles.Lookup(fmt.Sprintf("%s", path)); t != nil {
tmpl = t
} else if t := Cowfiles.Lookup(fmt.Sprintf("%s.cow", path)); t != nil {
tmpl = t
} else {
return Cow{}, fmt.Errorf("cowsay: could not find %s cowfile", path)
}
return Cow{body: tmpl}, nil
}