Browse Source

output for a particular post is now wrapped and looking great

Benton Edmondson 2 years ago
parent
commit
54c7cb7cf5
8 changed files with 233 additions and 109 deletions
  1. 49 8
      ansi/ansi.go
  2. 7 1
      ansi/ansi_test.go
  3. 79 23
      hypertext/hypertext.go
  4. 5 2
      hypertext/hypertext_test.go
  5. 4 6
      kinds/actor.go
  6. 71 42
      kinds/post.go
  7. 1 1
      main.go
  8. 17 26
      style/style.go

+ 49 - 8
ansi/ansi.go

@@ -7,7 +7,7 @@ import (
 )
 
 func expand(text string) [][]string {
-	r := regexp.MustCompile(`(?s)(?:(?:\x1b\[.*?m)*)(.)(?:\x1b\[0m)?`)
+	r := regexp.MustCompile(`(?s)((?:\x1b\[.*?m)*)(.)(?:\x1b\[0m)?`)
 	return r.FindAllStringSubmatch(text, -1)
 }
 
@@ -15,15 +15,15 @@ func Apply(text string, style string) string {
 	expanded := expand(text)
 	result := ""
 	for _, match := range expanded {
-		full := match[0]
-		letter := match[1]
+		prefix := match[1]
+		letter := match[2]
 
 		if letter == "\n" {
 			result += "\n"
 			continue
 		}
 
-		result += "\x1b[" + style + "m" + full + "\x1b[0m"
+		result += "\x1b[" + style + "m" + prefix + letter + "\x1b[0m"
 	}
 	return result
 }
@@ -38,7 +38,7 @@ func Indent(text string, prefix string, includeFirst bool) string {
 
 	for _, match := range expanded {
 		full := match[0]
-		letter := match[1]
+		letter := match[2]
 
 		if letter == "\n" {
 			result += "\n" + prefix
@@ -58,7 +58,7 @@ func Pad(text string, length int) string {
 
 	for _, match := range expanded {
 		full := match[0]
-		letter := match[1]
+		letter := match[2]
 
 		if letter == "\n" {
 			amount := length - lineLength
@@ -97,7 +97,7 @@ func Wrap(text string, length int) string {
 
 	for _, match := range expanded {
 		full := match[0]
-		letter := match[1]
+		letter := match[2]
 
 		/* TODO: I need to find the list of non-breaking whitespace characters
 			to exclude from this conditional */
@@ -133,6 +133,15 @@ func Wrap(text string, length int) string {
 		}
 
 		if letter == "\n" {
+			/*
+				If the spaces can be jammed into the line, add them.
+				This ensures that Wrap(Pad(*)) doesn't eliminate the
+				padding.
+			*/
+			if lineLength + spaceLength <= length {
+				line += space; lineLength += spaceLength
+			}
+
 			/* Add the current line as-is and clear everything */
 			result = append(result, line)
 			line = ""; lineLength = 0
@@ -147,7 +156,10 @@ func Wrap(text string, length int) string {
 	if wordLength > 0 {
 		line += space + word; lineLength += spaceLength + wordLength
 	}
-	finalLetter := expanded[len(expanded)-1][1]
+	finalLetter := ""
+	if len(expanded) > 0 {
+		finalLetter = expanded[len(expanded)-1][2]
+	}
 	if lineLength > 0 || finalLetter == "\n" {
 		result = append(result, line)
 	}
@@ -155,6 +167,32 @@ func Wrap(text string, length int) string {
 	return strings.Join(result, "\n")
 }
 
+func DumbWrap(text string, width int) string {
+	expanded := expand(text)
+	result := ""
+	currentLineLength := 0
+
+	for _, match := range expanded {
+		full := match[0]
+		letter := match[2]
+
+		if letter == "\n" {
+			currentLineLength = 0
+			result += "\n"
+			continue
+		}
+
+		if currentLineLength == width {
+			currentLineLength = 0
+			result += "\n"
+		}
+
+		result += full
+		currentLineLength += 1
+	}
+	return result
+}
+
 /*
 	TODO:
 		add `Scrub` function that removes all ANSI codes from text
@@ -167,4 +205,7 @@ func Wrap(text string, length int) string {
 		add `Squash` function that converts newlines to spaces
 		(this will be used to prevent newlines from appearing
 		in things like names and titles)
+
+		add `StrictWrap` function that wraps not based on whitespace
+		but strictly on length (this will be used for code blocks)
 */

+ 7 - 1
ansi/ansi_test.go

@@ -78,7 +78,7 @@ func TestBasic(t *testing.T) {
 		// Complete example:
 		{
 			" This is a list: \n\n * foo\n * bar\n\n\n * foo  \nbar    ",
-			" This\nis a\nlist:\n\n * foo\n * bar\n\n\n * foo\nbar",
+			" This\nis a\nlist: \n\n * foo\n * bar\n\n\n * foo\nbar",
 			6,
 		},
 		// ANSI sequence codes don't affect length calculation:
@@ -93,6 +93,12 @@ func TestBasic(t *testing.T) {
 			"\x1B[38;2;249;38;114m(\x1B[0m\x1B[38;2;248;248;242mju\nst\nano\nthe\nr\ntes\nt\x1B[38;2;249;38;114m)\x1B[0m",
 			3,
 		},
+		// Many, many newlines shouldn't collapse:
+		{
+			"multi-space   \n\n\n\n\n far down",
+			"multi-sp\nace   \n\n\n\n\n far\ndown",
+			8,
+		},
 	}
 
 	for _, test := range tests {

+ 79 - 23
hypertext/hypertext.go

@@ -7,8 +7,14 @@ import (
 	"regexp"
 	"mimicry/style"
 	"errors"
+	"mimicry/ansi"
 )
 
+// TODO: create a `bulletedList` function for all situations where
+// html-specific wrapping is needed. Put them at the bottom of the file
+// and note that that section is different. (This will include one for
+// headers)
+
 /* Terminal codes and control characters should already be escaped
    by this point */
 func Render(text string, width int) (string, error) {
@@ -20,18 +26,19 @@ func Render(text string, width int) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	serialized, err := serializeList(nodes)
+	rendered, err := renderList(nodes, width)
 	if err != nil {
 		return "", err
 	}
 
-	return strings.TrimSpace(serialized), nil
+	wrapped := ansi.Wrap(rendered, width)
+	return strings.Trim(wrapped, " \n"), nil
 }
 
-func serializeList(nodes []*html.Node) (string, error) {
+func renderList(nodes []*html.Node, width int) (string, error) {
 	output := ""
 	for _, current := range nodes {
-		result, err := renderNode(current, false)
+		result, err := renderNode(current, width, false)
 		if err != nil {
 			return "", err
 		}
@@ -76,7 +83,7 @@ func mergeText(lhs string, rhs string) string {
 	return lhsTrimmed + "\n\n" + rhsTrimmed
 }
 
-func renderNode(node *html.Node, preserveWhitespace bool) (string, error) {
+func renderNode(node *html.Node, width int, preserveWhitespace bool) (string, error) {
 	if node.Type == html.TextNode {
 		if !preserveWhitespace {
 			whitespace := regexp.MustCompile(`[ \t\n\r]+`)
@@ -89,7 +96,7 @@ func renderNode(node *html.Node, preserveWhitespace bool) (string, error) {
 		return "", nil
 	}
 
-	content, err := serializeChildren(node, preserveWhitespace)
+	content, err := renderChildren(node, width, preserveWhitespace)
 	if err != nil {
 		return "", err
 	}
@@ -117,31 +124,70 @@ func renderNode(node *html.Node, preserveWhitespace bool) (string, error) {
 	case "p", "div":
 		return block(content), nil
 	case "pre":
-		content, err := serializeChildren(node, true)
-		return block(style.CodeBlock(content)), err
+		content, err := renderChildren(node, width - 2, true)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width, true)
+		return block(style.CodeBlock(wrapped)), err
 	case "blockquote":
-		return block(style.QuoteBlock(content)), nil
+		content, err := renderChildren(node, width - 1, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width, preserveWhitespace)
+		return block(style.QuoteBlock(wrapped)), nil
 	case "ul":
-		list, err := bulletedList(node, preserveWhitespace)
+		list, err := bulletedList(node, width, preserveWhitespace)
 		return list, err
 	// case "ul":
 	// 	return numberedList(node), nil
 
 	case "h1":
-		return block(style.Header(content, 1)), nil
+		content, err := renderChildren(node, width - 2, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 2, preserveWhitespace)
+		return block(style.Header(wrapped, 1)), nil
 	case "h2":
-		return block(style.Header(content, 2)), nil
+		content, err := renderChildren(node, width - 3, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 3, preserveWhitespace)
+		return block(style.Header(wrapped, 2)), nil
 	case "h3":
-		return block(style.Header(content, 3)), nil
+		content, err := renderChildren(node, width - 4, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 4, preserveWhitespace)
+		return block(style.Header(wrapped, 3)), nil
 	case "h4":
-		return block(style.Header(content, 4)), nil
+		content, err := renderChildren(node, width - 5, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 5, preserveWhitespace)
+		return block(style.Header(wrapped, 4)), nil
 	case "h5":
-		return block(style.Header(content, 5)), nil
+		content, err := renderChildren(node, width - 6, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 6, preserveWhitespace)
+		return block(style.Header(wrapped, 5)), nil
 	case "h6":
-		return block(style.Header(content, 6)), nil
+		content, err := renderChildren(node, width - 7, preserveWhitespace)
+		if err != nil {
+			return "", err
+		}
+		wrapped := situationalWrap(content, width - 7, preserveWhitespace)
+		return block(style.Header(wrapped, 6)), nil
 
 	case "hr":
-		return block("―――"), nil
+		return block(strings.Repeat("―", width)), nil
 	case "img", "video", "audio", "iframe":
 		text := getAttribute("alt", node.Attr)
 		if text == "" {
@@ -153,16 +199,17 @@ func renderNode(node *html.Node, preserveWhitespace bool) (string, error) {
 		if text == "" {
 			return "", errors.New(node.Data + " tag is missing both `alt` and `src` attributes")
 		}
-		return block(style.LinkBlock(text)), nil
+		wrapped := situationalWrap(text, width - 2, preserveWhitespace)
+		return block(style.LinkBlock(wrapped)), nil
 	}
 
 	return "", errors.New("Encountered unrecognized element " + node.Data)
 }
 
-func serializeChildren(node *html.Node, preserveWhitespace bool) (string, error) {
+func renderChildren(node *html.Node, width int, preserveWhitespace bool) (string, error) {
 	output := ""
 	for current := node.FirstChild; current != nil; current = current.NextSibling {
-		result, err := renderNode(current, preserveWhitespace)
+		result, err := renderNode(current, width, preserveWhitespace)
 		if err != nil {
 			return "", err
 		}
@@ -175,7 +222,7 @@ func block(text string) string {
 	return "\n\n" + strings.Trim(text, " \n") + "\n\n"
 }
 
-func bulletedList(node *html.Node, preserveWhitespace bool) (string, error) {
+func bulletedList(node *html.Node, width int, preserveWhitespace bool) (string, error) {
 	output := ""
 	for current := node.FirstChild; current != nil; current = current.NextSibling {
 		if current.Type != html.ElementNode {
@@ -186,11 +233,12 @@ func bulletedList(node *html.Node, preserveWhitespace bool) (string, error) {
 			continue
 		}
 
-		result, err := renderNode(current, preserveWhitespace)
+		result, err := renderNode(current, width - 2, preserveWhitespace)
 		if err != nil {
 			return "", err
 		}
-		output += "\n" + style.Bullet(result)
+		wrapped := situationalWrap(result, width - 2, preserveWhitespace)
+		output += "\n" + style.Bullet(wrapped)
 	}
 
 	if node.Parent == nil {
@@ -209,4 +257,12 @@ func getAttribute(name string, attributes []html.Attribute) string {
 		}
 	}
 	return ""
+}
+
+func situationalWrap(text string, width int, preserveWhitespace bool) string {
+	if preserveWhitespace {
+		return ansi.DumbWrap(text, width)
+	}
+
+	return ansi.Wrap(text, width)
 }

+ 5 - 2
hypertext/hypertext_test.go

@@ -87,13 +87,16 @@ and that they heard`
 	util.AssertEqual(expected, output, t)
 }
 
+// TODO: this is broken for now because my wrap algorithm removes
+// trailing spaces under certain conditions. I need to modify it such that it
+// leaves trailing spaces if possible
 func TestPreservation(t *testing.T) {
-	input := "<pre>tab\tand multi-space   \n\n\n\n\n far down</pre>"
+	input := "<pre>multi-space   \n\n\n\n\n far down</pre>"
 	output, err := Render(input, 50)
 	if err != nil {
 		panic(err)
 	}
-	expected := style.CodeBlock(`tab	and multi-space   
+	expected := style.CodeBlock(`multi-space   
 
 
 

+ 4 - 6
kinds/actor.go

@@ -4,7 +4,6 @@ import (
 	"strings"
 	"net/url"
 	"mimicry/style"
-	"fmt"
 	"mimicry/render"
 )
 
@@ -33,11 +32,10 @@ func (a Actor) InlineName() (string, error) {
 	if err != nil {
 		return "", err
 	}
-	// if kind != "person" {
-	// 	return fmt.Sprintf("%s (%s, %s)", name, id.Hostname(), kind), nil
-	// }
-	// return fmt.Sprintf("%s (%s)", name, id.Hostname()), nil
-	return fmt.Sprintf("%s (%s, %s)", name, id.Hostname(), kind), nil
+	if kind == "person" {
+		return name + " (" + id.Hostname() + ")", nil
+	}
+	return name + " (" + id.Hostname() + ", " + kind + ")", nil
 }
 
 func (a Actor) Category() string {

+ 71 - 42
kinds/post.go

@@ -5,9 +5,9 @@ import (
 	"strings"
 	"time"
 	"mimicry/style"
-	"fmt"
 	"errors"
 	"mimicry/render"
+	"mimicry/ansi"
 )
 
 type Post Dict
@@ -26,24 +26,27 @@ func (p Post) Title() (string, error) {
 	return strings.TrimSpace(title), err
 }
 
-func (p Post) Body() (string, error) {
+func (p Post) Body(width int) (string, error) {
 	body, err := GetNatural(p, "content", "en")
+	if err != nil {
+		return "", err
+	}
 	mediaType, err := Get[string](p, "mediaType")
 	if err != nil {
 		mediaType = "text/html"
 	}
-	return render.Render(body, mediaType, 80)
+	return render.Render(body, mediaType, width)
 }
 
-func (p Post) BodyPreview() (string, error) {
-	body, err := p.Body()
-	// probably should convert to runes and just work with that
-	if len(body) > 280*2 { // this is a bug because len counts bytes whereas later I work based on runes
-		return fmt.Sprintf("%s…", string([]rune(body)[:280])), err
-	} else {
-		return body, err
-	}
-}
+// func (p Post) BodyPreview() (string, error) {
+// 	body, err := p.Body()
+// 	// probably should convert to runes and just work with that
+// 	if len(body) > 280*2 { // this is a bug because len counts bytes whereas later I work based on runes
+// 		return fmt.Sprintf("%s…", string([]rune(body)[:280])), err
+// 	} else {
+// 		return body, err
+// 	}
+// }
 
 func (p Post) Identifier() (*url.URL, error) {
 	return GetURL(p, "id")
@@ -65,14 +68,14 @@ func (p Post) Creators() ([]Actor, error) {
 	return GetContent[Actor](p, "attributedTo")
 }
 
+func (p Post) Recipients() ([]Actor, error) {
+	return GetContent[Actor](p, "to")
+}
+
 func (p Post) Attachments() ([]Link, error) {
 	return GetLinksLenient(p, "attachment")
 }
 
-// func (p Post) bestLink() (Link, error) {
-
-// }
-
 func (p Post) Link() (Link, error) {
 	kind, err := p.Kind()
 	if err != nil {
@@ -94,52 +97,78 @@ func (p Post) Link() (Link, error) {
 	}
 }
 
-// TODO: errors in here should potentially trigger errors!
-func (p Post) String() (string, error) {
+func (p Post) header(width int) (string, error) {
 	output := ""
 
 	if title, err := p.Title(); err == nil {
-		output += style.Bold(title)
-		output += "\n"
-	}
-
-	if body, err := p.Body(); err == nil {
-		output += body
-		output += "\n"
-	} else {
-		return "", err
+		output += style.Bold(title) + "\n"
 	}
 
-	if created, err := p.Created(); err == nil {
-		output += time.Now().Sub(created).String()
+	if kind, err := p.Kind(); err == nil {
+		output += style.Color(kind)
 	}
 
 	if creators, err := p.Creators(); err == nil {
-		output += " "
+		names := []string{}
 		for _, creator := range creators {
 			if name, err := creator.InlineName(); err == nil {
-				output += style.Bold(name) + ", "
+				names = append(names, style.Link(name))
 			}
 		}
+		if len(names) > 0 {
+			output += " by " + strings.Join(names, ", ")
+		}
 	}
 
-	if link, err := p.Link(); err == nil {
-		if linkStr, err := link.String(); err == nil {
-			output += "\n"
-			output += linkStr
+	if recipients, err := p.Recipients(); err == nil {
+		names := []string{}
+		for _, recipient := range recipients {
+			if name, err := recipient.InlineName(); err == nil {
+				names = append(names, style.Link(name))
+			}
 		}
+		if len(names) > 0 {
+			output += " to " + strings.Join(names, ", ")
+		}
+	}
+
+	if created, err := p.Created(); err == nil {
+		output += " at " + style.Color(created.Format("3:04 pm"))
+		output += " on " + style.Color(created.Format("2 Jan 2006"))
+	}
+
+	return ansi.Wrap(output, width), nil
+}
+
+func (p Post) String() (string, error) {
+	output := ""
+	width := 100
+
+	if header, err := p.header(width - 2); err == nil {
+		output += ansi.Indent(header, "  ", true)
+		output += "\n\n"
+	}
+
+	if body, err := p.Body(width - 4); err == nil {
+		output += ansi.Indent(body, "    ", true)
+		output += "\n\n"
 	}
 
 	if attachments, err := p.Attachments(); err == nil {
-		output += "\nAttachments:\n"
-		for _, attachment := range attachments {
-			if attachmentStr, err := attachment.String(); err == nil {
-				output += attachmentStr + "\n"
-			} else {
-				continue
+		if len(attachments) > 0 {
+			section := "Attachments:\n"
+			names := []string{}
+			for _, attachment := range attachments {
+				if name, err := attachment.String(); err == nil {
+					names = append(names, style.Link(name))
+				}
 			}
+			section += ansi.Indent(ansi.Wrap(strings.Join(names, "\n"), width - 4), "  ", true)
+			section = ansi.Indent(ansi.Wrap(section, width - 2), "  ", true)
+			output += section
+			output += "\n"
 		}
 	}
 
-	return strings.TrimSpace(output), nil
+	return output, nil
 }

+ 1 - 1
main.go

@@ -54,6 +54,6 @@ func main() {
 	if str, err := content.String(); err != nil {
 		panic(err)
 	} else {
-		fmt.Println(str)
+		fmt.Print(str)
 	}
 }

+ 17 - 26
style/style.go

@@ -6,10 +6,6 @@ import (
 	"mimicry/ansi"
 )
 
-// TODO: at some point I need to sanitize preexisting escape codes
-// in input, to do so replace the escape character with visual
-// escape character
-
 func background(text string, r uint8, g uint8, b uint8) string {
 	prefix := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
 	return ansi.Apply(text, prefix)
@@ -20,13 +16,6 @@ func foreground(text string, r uint8, g uint8, b uint8) string {
 	return ansi.Apply(text, prefix)
 }
 
-func display(text string, prependCode int, appendCode int) string {
-	return fmt.Sprintf("\x1b[%dm%s\x1b[%dm", prependCode, text, appendCode)
-}
-
-// 21 doesn't work (does double underline)
-// 22 removes bold and faint, faint is never used
-// so it does the job
 func Bold(text string) string {
 	return ansi.Apply(text, "1")
 }
@@ -47,19 +36,6 @@ func Code(text string) string {
 	return background(text, 75, 75, 75)
 }
 
-func CodeBlock(text string) string {
-	return Code(text)
-}
-
-func QuoteBlock(text string) string {
-	withBar := "▌" + strings.ReplaceAll(text, "\n", "\n▌")
-	return Color(withBar)
-}
-
-func LinkBlock(text string) string {
-	return "‣ " + Link(text)
-}
-
 func Highlight(text string) string {
 	return background(text, 13, 125, 0)
 }
@@ -72,11 +48,26 @@ func Link(text string) string {
 	return Underline(Color(text))
 }
 
+func CodeBlock(text string) string {
+	return Code(text)
+}
+
+func QuoteBlock(text string) string {
+	prefixed := ansi.Indent(text, "▌", true)
+	return Color(prefixed)
+}
+
+func LinkBlock(text string) string {
+	indented := ansi.Indent(text, "  ", false)
+	return "‣ " + Link(indented)
+}
+
 func Header(text string, level uint) string {
-	withPrefix := strings.Repeat("⯁", int(level)) + " " + text
+	indented := ansi.Indent(text, strings.Repeat(" ", int(level+1)), false)
+	withPrefix := strings.Repeat("⯁", int(level)) + " " + indented
 	return Color(Bold(withPrefix))
 }
 
 func Bullet(text string) string {
-	return "• " + strings.ReplaceAll(text, "\n", "\n  ")
+	return "• " + ansi.Indent(text, "  ", false)
 }