aboutsummaryrefslogtreecommitdiffstats
path: root/skate/wordwrap
diff options
context:
space:
mode:
authorMartin Czygan <martin.czygan@gmail.com>2021-04-21 17:49:12 +0200
committerMartin Czygan <martin.czygan@gmail.com>2021-04-21 17:49:12 +0200
commit3cb5513cb405af78a01750a29a93be28ac5d90e4 (patch)
tree46774566984606a113a17803da81f3b14ada742f /skate/wordwrap
parentae9e380225be648ced23d814cd1d08d1621976bd (diff)
downloadrefcat-3cb5513cb405af78a01750a29a93be28ac5d90e4.tar.gz
refcat-3cb5513cb405af78a01750a29a93be28ac5d90e4.zip
wip: a few dot examples
Diffstat (limited to 'skate/wordwrap')
-rw-r--r--skate/wordwrap/wordwrap.go83
-rw-r--r--skate/wordwrap/wordwrap_test.go99
2 files changed, 182 insertions, 0 deletions
diff --git a/skate/wordwrap/wordwrap.go b/skate/wordwrap/wordwrap.go
new file mode 100644
index 0000000..f7bedda
--- /dev/null
+++ b/skate/wordwrap/wordwrap.go
@@ -0,0 +1,83 @@
+package wordwrap
+
+import (
+ "bytes"
+ "unicode"
+)
+
+const nbsp = 0xA0
+
+// WrapString wraps the given string within lim width in characters.
+//
+// Wrapping is currently naive and only happens at white-space. A future
+// version of the library will implement smarter wrapping. This means that
+// pathological cases can dramatically reach past the limit, such as a very
+// long word.
+func WrapString(s string, lim uint) string {
+ // Initialize a buffer with a slightly larger size to account for breaks
+ init := make([]byte, 0, len(s))
+ buf := bytes.NewBuffer(init)
+
+ var current uint
+ var wordBuf, spaceBuf bytes.Buffer
+ var wordBufLen, spaceBufLen uint
+
+ for _, char := range s {
+ if char == '\n' {
+ if wordBuf.Len() == 0 {
+ if current+spaceBufLen > lim {
+ current = 0
+ } else {
+ current += spaceBufLen
+ spaceBuf.WriteTo(buf)
+ }
+ spaceBuf.Reset()
+ spaceBufLen = 0
+ } else {
+ current += spaceBufLen + wordBufLen
+ spaceBuf.WriteTo(buf)
+ spaceBuf.Reset()
+ spaceBufLen = 0
+ wordBuf.WriteTo(buf)
+ wordBuf.Reset()
+ wordBufLen = 0
+ }
+ buf.WriteRune(char)
+ current = 0
+ } else if unicode.IsSpace(char) && char != nbsp {
+ if spaceBuf.Len() == 0 || wordBuf.Len() > 0 {
+ current += spaceBufLen + wordBufLen
+ spaceBuf.WriteTo(buf)
+ spaceBuf.Reset()
+ spaceBufLen = 0
+ wordBuf.WriteTo(buf)
+ wordBuf.Reset()
+ wordBufLen = 0
+ }
+
+ spaceBuf.WriteRune(char)
+ spaceBufLen++
+ } else {
+ wordBuf.WriteRune(char)
+ wordBufLen++
+
+ if current+wordBufLen+spaceBufLen > lim && wordBufLen < lim {
+ buf.WriteRune('\n')
+ current = 0
+ spaceBuf.Reset()
+ spaceBufLen = 0
+ }
+ }
+ }
+
+ if wordBuf.Len() == 0 {
+ if current+spaceBufLen <= lim {
+ spaceBuf.WriteTo(buf)
+ }
+ } else {
+ spaceBuf.WriteTo(buf)
+ wordBuf.WriteTo(buf)
+ }
+
+ return buf.String()
+}
diff --git a/skate/wordwrap/wordwrap_test.go b/skate/wordwrap/wordwrap_test.go
new file mode 100644
index 0000000..98010eb
--- /dev/null
+++ b/skate/wordwrap/wordwrap_test.go
@@ -0,0 +1,99 @@
+package wordwrap
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestWrapString(t *testing.T) {
+ cases := []struct {
+ Input, Output string
+ Lim uint
+ }{
+ // A simple word passes through.
+ {
+ "foo",
+ "foo",
+ 4,
+ },
+ // A single word that is too long passes through.
+ // We do not break words.
+ {
+ "foobarbaz",
+ "foobarbaz",
+ 4,
+ },
+ // Lines are broken at whitespace.
+ {
+ "foo bar baz",
+ "foo\nbar\nbaz",
+ 4,
+ },
+ // Lines are broken at whitespace, even if words
+ // are too long. We do not break words.
+ {
+ "foo bars bazzes",
+ "foo\nbars\nbazzes",
+ 4,
+ },
+ // A word that would run beyond the width is wrapped.
+ {
+ "fo sop",
+ "fo\nsop",
+ 4,
+ },
+ // Do not break on non-breaking space.
+ {
+ "foo bar\u00A0baz",
+ "foo\nbar\u00A0baz",
+ 10,
+ },
+ // Whitespace that trails a line and fits the width
+ // passes through, as does whitespace prefixing an
+ // explicit line break. A tab counts as one character.
+ {
+ "foo\nb\t r\n baz",
+ "foo\nb\t r\n baz",
+ 4,
+ },
+ // Trailing whitespace is removed if it doesn't fit the width.
+ // Runs of whitespace on which a line is broken are removed.
+ {
+ "foo \nb ar ",
+ "foo\nb\nar",
+ 4,
+ },
+ // An explicit line break at the end of the input is preserved.
+ {
+ "foo bar baz\n",
+ "foo\nbar\nbaz\n",
+ 4,
+ },
+ // Explicit break are always preserved.
+ {
+ "\nfoo bar\n\n\nbaz\n",
+ "\nfoo\nbar\n\n\nbaz\n",
+ 4,
+ },
+ // Complete example:
+ {
+ " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ",
+ " This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* baz\nBAM",
+ 6,
+ },
+ // Multi-byte characters
+ {
+ strings.Repeat("\u2584 ", 4),
+ "\u2584 \u2584" + "\n" +
+ strings.Repeat("\u2584 ", 2),
+ 4,
+ },
+ }
+
+ for i, tc := range cases {
+ actual := WrapString(tc.Input, tc.Lim)
+ if actual != tc.Output {
+ t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Input, tc.Output, actual)
+ }
+ }
+}