From 28bf902599205c9e3105fa4cebba9d528533ada2 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Mon, 8 Jul 2024 22:53:13 +0100 Subject: [PATCH] fix: improve how lists are displayed Improve how ordered and unordered lists are displayed within the contents of a status by adding indentation when a list item is wrapped. Changes: - fix: improve how ordered and unordered lists are displayed in status and status list views. - fix: improve how media attachments are displayed in status list views. - refactor: move the line wrapping and HTML converting functions from utilities to the internal printer package. - refactor: the convertHTMLToText now (optionally) applies line wrapping after conversion. --- internal/printer/account.go | 7 +- internal/{utilities => printer}/html.go | 10 ++- internal/printer/instance.go | 5 +- internal/printer/poll.go | 4 +- internal/printer/printer.go | 26 +++---- internal/printer/status.go | 25 ++++--- internal/printer/wrap.go | 96 +++++++++++++++++++++++++ internal/utilities/wrap.go | 59 --------------- 8 files changed, 138 insertions(+), 94 deletions(-) rename internal/{utilities => printer}/html.go (87%) create mode 100644 internal/printer/wrap.go delete mode 100644 internal/utilities/wrap.go diff --git a/internal/printer/account.go b/internal/printer/account.go index cbb6496..1ffef82 100644 --- a/internal/printer/account.go +++ b/internal/printer/account.go @@ -9,7 +9,6 @@ import ( "strings" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) func (p Printer) PrintAccount(account model.Account, relationship *model.AccountRelationship, preferences *model.Preferences) { @@ -28,11 +27,11 @@ func (p Printer) PrintAccount(account model.Account, relationship *model.Account builder.WriteString("\n" + p.fieldFormat("Statuses:")) builder.WriteString(" " + strconv.Itoa(account.StatusCount)) builder.WriteString("\n\n" + p.headerFormat("BIOGRAPHY:")) - builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(account.Note), "\n", p.maxTerminalWidth)) + builder.WriteString(p.convertHTMLToText(account.Note, true)) builder.WriteString("\n\n" + p.headerFormat("METADATA:")) for _, field := range account.Fields { - builder.WriteString("\n" + p.fieldFormat(field.Name) + ": " + utilities.ConvertHTMLToText(field.Value)) + builder.WriteString("\n" + p.fieldFormat(field.Name) + ": " + p.convertHTMLToText(field.Value, false)) } builder.WriteString("\n\n" + p.headerFormat("ACCOUNT URL:")) @@ -80,7 +79,7 @@ func (p Printer) accountRelationship(relationship *model.AccountRelationship) st if relationship.PrivateNote != "" { builder.WriteString("\n\n" + p.headerFormat("YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:")) - builder.WriteString("\n" + utilities.WrapLines(relationship.PrivateNote, "\n", p.maxTerminalWidth)) + builder.WriteString("\n" + p.wrapLines(relationship.PrivateNote, 0)) } return builder.String() diff --git a/internal/utilities/html.go b/internal/printer/html.go similarity index 87% rename from internal/utilities/html.go rename to internal/printer/html.go index 54fc55c..ff6644d 100644 --- a/internal/utilities/html.go +++ b/internal/printer/html.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -package utilities +package printer import ( "io" @@ -23,7 +23,7 @@ type htmlConvertState struct { orderedListIndex int } -func ConvertHTMLToText(text string) string { +func (p Printer) convertHTMLToText(text string, wrapLines bool) string { var builder strings.Builder state := htmlConvertState{ @@ -37,6 +37,10 @@ func ConvertHTMLToText(text string) string { tt := token.Next() switch tt { case html.ErrorToken: + if wrapLines { + return p.wrapLines(builder.String(), 0) + } + return builder.String() case html.TextToken: text := token.Token().Data @@ -66,7 +70,7 @@ func processTagToken(state *htmlConvertState, writer io.StringWriter, tag string case "
  • ": switch state.htmlListType { case htmlUnorderedList: - _, _ = writer.WriteString("• ") + _, _ = writer.WriteString(symbolBullet + " ") case htmlOrderedList: _, _ = writer.WriteString(strconv.Itoa(state.orderedListIndex) + ". ") state.orderedListIndex++ diff --git a/internal/printer/instance.go b/internal/printer/instance.go index ce1b14e..a19f53b 100644 --- a/internal/printer/instance.go +++ b/internal/printer/instance.go @@ -8,7 +8,6 @@ import ( "strings" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) func (p Printer) PrintInstance(instance model.InstanceV2) { @@ -18,13 +17,13 @@ func (p Printer) PrintInstance(instance model.InstanceV2) { builder.WriteString("\n" + instance.Title) builder.WriteString("\n\n" + p.headerFormat("INSTANCE DESCRIPTION:")) - builder.WriteString("\n" + utilities.WrapLines(instance.DescriptionText, "\n", p.maxTerminalWidth)) + builder.WriteString("\n" + p.wrapLines(instance.DescriptionText, 0)) builder.WriteString("\n\n" + p.headerFormat("DOMAIN:")) builder.WriteString("\n" + instance.Domain) builder.WriteString("\n\n" + p.headerFormat("TERMS AND CONDITIONS:")) - builder.WriteString("\n" + utilities.WrapLines(instance.TermsText, "\n ", p.maxTerminalWidth)) + builder.WriteString("\n" + p.wrapLines(instance.TermsText, 2)) builder.WriteString("\n\n" + p.headerFormat("VERSION:")) builder.WriteString("\nRunning GoToSocial " + instance.Version) diff --git a/internal/printer/poll.go b/internal/printer/poll.go index cd24da1..8a8a34d 100644 --- a/internal/printer/poll.go +++ b/internal/printer/poll.go @@ -71,8 +71,8 @@ func (p Printer) pollOptions(poll model.Poll) string { } func (p Printer) pollMeter(votage float64) string { - numVoteBlocks := int(math.Floor(float64(p.maxTerminalWidth) * votage)) - numBackgroundBlocks := p.maxTerminalWidth - numVoteBlocks + numVoteBlocks := int(math.Floor(float64(p.lineWrapCharacterLimit) * votage)) + numBackgroundBlocks := p.lineWrapCharacterLimit - numVoteBlocks voteBlockColor := p.theme.boldgreen backgroundBlockColor := p.theme.grey diff --git a/internal/printer/printer.go b/internal/printer/printer.go index a63fc9f..9b1f641 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -43,17 +43,17 @@ type theme struct { } type Printer struct { - theme theme - noColor bool - maxTerminalWidth int - pager string - statusSeparator string + theme theme + noColor bool + lineWrapCharacterLimit int + pager string + statusSeparator string } func NewPrinter( noColor bool, pager string, - maxTerminalWidth int, + lineWrapCharacterLimit int, ) *Printer { theme := theme{ reset: "\033[0m", @@ -68,16 +68,16 @@ func NewPrinter( boldyellow: "\033[33;1m", } - if maxTerminalWidth < minTerminalWidth { - maxTerminalWidth = minTerminalWidth + if lineWrapCharacterLimit < minTerminalWidth { + lineWrapCharacterLimit = minTerminalWidth } return &Printer{ - theme: theme, - noColor: noColor, - maxTerminalWidth: maxTerminalWidth, - pager: pager, - statusSeparator: strings.Repeat("\u2501", maxTerminalWidth), + theme: theme, + noColor: noColor, + lineWrapCharacterLimit: lineWrapCharacterLimit, + pager: pager, + statusSeparator: strings.Repeat("\u2501", lineWrapCharacterLimit), } } diff --git a/internal/printer/status.go b/internal/printer/status.go index 5bc40db..af37ea9 100644 --- a/internal/printer/status.go +++ b/internal/printer/status.go @@ -9,7 +9,6 @@ import ( "strings" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) func (p Printer) PrintStatus(status model.Status) { @@ -24,7 +23,7 @@ func (p Printer) PrintStatus(status model.Status) { // The content of the status. builder.WriteString("\n\n" + p.headerFormat("CONTENT:")) - builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", p.maxTerminalWidth)) + builder.WriteString(p.convertHTMLToText(status.Content, true)) // Details of media attachments (if any). if len(status.MediaAttachments) > 0 { @@ -93,7 +92,10 @@ func (p Printer) PrintStatusList(list model.StatusList) { if status.Reblog != nil { builder.WriteString( - "\n" + utilities.WrapLines("reposted this status from "+p.fullDisplayNameFormat(status.Reblog.Account.DisplayName, status.Reblog.Account.Acct), "\n", p.maxTerminalWidth), + "\n" + p.wrapLines( + "reposted this status from "+p.fullDisplayNameFormat(status.Reblog.Account.DisplayName, status.Reblog.Account.Acct), + 0, + ), ) statusID = status.Reblog.ID @@ -103,22 +105,25 @@ func (p Printer) PrintStatusList(list model.StatusList) { mediaAttachments = status.Reblog.MediaAttachments } - builder.WriteString("\n" + utilities.WrapLines(utilities.ConvertHTMLToText(content), "\n", p.maxTerminalWidth)) + builder.WriteString("\n" + p.convertHTMLToText(content, true)) if poll != nil { builder.WriteString(p.pollOptions(*poll)) } for _, media := range mediaAttachments { - builder.WriteString("\n\n" + symbolImage + " Media attachment: " + media.ID) - builder.WriteString("\n Media type: " + media.Type + "\n ") + builder.WriteString("\n\n" + symbolImage + " " + p.fieldFormat("Media attachment: ") + media.ID) + builder.WriteString("\n " + p.fieldFormat("Media type: ") + media.Type + "\n") - description := media.Description - if description == "" { - description = noMediaDescription + description := " " + p.fieldFormat("Description: ") + + if media.Description == "" { + description += noMediaDescription + } else { + description += media.Description } - builder.WriteString(utilities.WrapLines(description, "\n ", p.maxTerminalWidth-3)) + builder.WriteString(p.wrapLines(description, 2)) } boosted := symbolBoosted diff --git a/internal/printer/wrap.go b/internal/printer/wrap.go new file mode 100644 index 0000000..bae7a58 --- /dev/null +++ b/internal/printer/wrap.go @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "regexp" + "strings" + "unicode" +) + +type extraIndentConditiion struct { + pattern *regexp.Regexp + indent string +} + +func (p Printer) wrapLines(text string, nIndent int) string { + if nIndent >= p.lineWrapCharacterLimit { + nIndent = 0 + } + + separator := "\n" + strings.Repeat(" ", nIndent) + + lines := strings.Split(text, "\n") + + if len(lines) == 1 { + return wrapLine(lines[0], separator, p.lineWrapCharacterLimit-nIndent) + } + + var builder strings.Builder + + extraIndentConditions := []extraIndentConditiion{ + { + pattern: regexp.MustCompile(`^[-*` + symbolBullet + `]\s.*$`), + indent: " ", + }, + { + pattern: regexp.MustCompile(`^[0-9]{1}\.\s.*$`), + indent: " ", + }, + { + pattern: regexp.MustCompile(`^[0-9]{2}\.\s.*$`), + indent: " ", + }, + } + + for ind, line := range lines { + builder.WriteString(wrapLine(line, separator+extraIndent(line, extraIndentConditions), p.lineWrapCharacterLimit-nIndent)) + + if ind < len(lines)-1 { + builder.WriteString(separator) + } + } + + return builder.String() +} + +func wrapLine(line, separator string, charLimit int) string { + if len(line) <= charLimit { + return line + } + + leftcursor, rightcursor := 0, 0 + + var builder strings.Builder + + for rightcursor < (len(line) - charLimit) { + rightcursor += (charLimit - 1) + + for (rightcursor > leftcursor) && !unicode.IsSpace(rune(line[rightcursor-1])) { + rightcursor-- + } + + if rightcursor == leftcursor { + rightcursor = leftcursor + charLimit + } + + builder.WriteString(line[leftcursor:rightcursor] + separator) + leftcursor = rightcursor + } + + builder.WriteString(line[rightcursor:]) + + return builder.String() +} + +func extraIndent(line string, conditions []extraIndentConditiion) string { + for ind := range conditions { + if conditions[ind].pattern.MatchString(line) { + return conditions[ind].indent + } + } + + return "" +} diff --git a/internal/utilities/wrap.go b/internal/utilities/wrap.go deleted file mode 100644 index 55a4c40..0000000 --- a/internal/utilities/wrap.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dan Anglin -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package utilities - -import ( - "strings" - "unicode" -) - -func WrapLines(text, separator string, charLimit int) string { - lines := strings.Split(text, "\n") - - if len(lines) == 1 { - return wrapLine(lines[0], separator, charLimit) - } - - var builder strings.Builder - - for i, line := range lines { - builder.WriteString(wrapLine(line, separator, charLimit)) - - if i < len(lines)-1 { - builder.WriteString(separator) - } - } - - return builder.String() -} - -func wrapLine(line, separator string, charLimit int) string { - if len(line) <= charLimit { - return line - } - - leftcursor, rightcursor := 0, 0 - - var builder strings.Builder - - for rightcursor < (len(line) - charLimit) { - rightcursor += (charLimit - 1) - - for (rightcursor > leftcursor) && !unicode.IsSpace(rune(line[rightcursor-1])) { - rightcursor-- - } - - if rightcursor == leftcursor { - rightcursor = leftcursor + charLimit - } - - builder.WriteString(line[leftcursor:rightcursor] + separator) - leftcursor = rightcursor - } - - builder.WriteString(line[rightcursor:]) - - return builder.String() -}