cowsay.gno
3.46 Kb · 147 lines
1// Package cowsay is an on-chain Gno port of the classic `cowsay` program:
2// it prints an ASCII cow with a speech bubble containing a message.
3//
4// Original: cowsay by Tony Monroe (1999), a Perl program later ported to Go
5// many times. This realm reimplements the core rendering — bubble framing,
6// word-wrapping and the cow art — as pure, deterministic string logic.
7package cowsay
8
9import (
10 "strings"
11)
12
13// defaultMessage is shown when no message is supplied.
14const defaultMessage = "Moo!"
15
16// wrapWidth is the maximum bubble text width (classic cowsay uses 40).
17const wrapWidth = 40
18
19// Say returns the full cowsay art (speech bubble + cow) for msg.
20// An empty msg falls back to the default message.
21func Say(msg string) string {
22 msg = strings.TrimSpace(msg)
23 if msg == "" {
24 msg = defaultMessage
25 }
26 lines := wrap(msg, wrapWidth)
27 return balloon(lines) + cow
28}
29
30// balloon builds the speech bubble around the given (already wrapped) lines.
31func balloon(lines []string) string {
32 width := maxLen(lines)
33
34 var b strings.Builder
35 // top border: " " + "_"*(width+2)
36 b.WriteString(" ")
37 b.WriteString(strings.Repeat("_", width+2))
38 b.WriteString("\n")
39
40 n := len(lines)
41 for i, ln := range lines {
42 left, right := borderChars(i, n)
43 b.WriteString(left)
44 b.WriteString(" ")
45 b.WriteString(ln)
46 b.WriteString(strings.Repeat(" ", width-len(ln)))
47 b.WriteString(" ")
48 b.WriteString(right)
49 b.WriteString("\n")
50 }
51
52 // bottom border: " " + "-"*(width+2)
53 b.WriteString(" ")
54 b.WriteString(strings.Repeat("-", width+2))
55 b.WriteString("\n")
56 return b.String()
57}
58
59// borderChars picks the left/right bubble characters for line i of n.
60// Single line uses < >. Multi-line uses / \ for the first row,
61// \ / for the last row, and | | for the middle rows.
62func borderChars(i, n int) (string, string) {
63 if n == 1 {
64 return "<", ">"
65 }
66 switch i {
67 case 0:
68 return "/", "\\"
69 case n - 1:
70 return "\\", "/"
71 default:
72 return "|", "|"
73 }
74}
75
76// wrap splits text into lines no longer than width, breaking on spaces.
77// Words longer than width are hard-split.
78func wrap(text string, width int) []string {
79 words := strings.Fields(text)
80 if len(words) == 0 {
81 return []string{""}
82 }
83
84 var lines []string
85 cur := ""
86 for _, w := range words {
87 // hard-split a single word that is too long
88 for len(w) > width {
89 if cur != "" {
90 lines = append(lines, cur)
91 cur = ""
92 }
93 lines = append(lines, w[:width])
94 w = w[width:]
95 }
96 if cur == "" {
97 cur = w
98 } else if len(cur)+1+len(w) <= width {
99 cur = cur + " " + w
100 } else {
101 lines = append(lines, cur)
102 cur = w
103 }
104 }
105 if cur != "" {
106 lines = append(lines, cur)
107 }
108 return lines
109}
110
111// maxLen returns the length of the longest line.
112func maxLen(lines []string) int {
113 m := 0
114 for _, ln := range lines {
115 if len(ln) > m {
116 m = len(ln)
117 }
118 }
119 return m
120}
121
122// cow is the classic happy cow, aligned under the bubble.
123const cow = ` \ ^__^
124 \ (oo)\_______
125 (__)\ )\/\
126 ||----w |
127 || ||
128`
129
130// Render displays the cow for gnoweb.
131//
132// Render("") -> default message ("Moo!")
133// Render("/hello there") -> renders "hello there", word-wrapped
134func Render(path string) string {
135 msg := strings.TrimLeft(path, "/")
136 art := Say(msg)
137
138 var b strings.Builder
139 b.WriteString("# cowsay\n\n")
140 if strings.TrimSpace(msg) == "" {
141 b.WriteString("_Tip: pass a message in the path, e.g._ `/Hello, gno.land!`\n\n")
142 }
143 b.WriteString("```\n")
144 b.WriteString(art)
145 b.WriteString("```\n")
146 return b.String()
147}