diff --git a/server/public/model/utils.go b/server/public/model/utils.go index 527828e26c..5ddadedd1d 100644 --- a/server/public/model/utils.go +++ b/server/public/model/utils.go @@ -717,8 +717,8 @@ var ( func ParseHashtags(text string) (string, string) { words := strings.Fields(text) - hashtagString := "" - plainString := "" + var hashtagStringSb strings.Builder + var plainString strings.Builder for _, word := range words { // trim off surrounding punctuation word = puncStart.ReplaceAllString(word, "") @@ -728,11 +728,12 @@ func ParseHashtags(text string) (string, string) { word = hashtagStart.ReplaceAllString(word, "#") if validHashtag.MatchString(word) { - hashtagString += " " + word + hashtagStringSb.WriteString(" " + word) } else { - plainString += " " + word + plainString.WriteString(" " + word) } } + hashtagString := hashtagStringSb.String() if len(hashtagString) > 1000 { hashtagString = hashtagString[:999] @@ -744,7 +745,7 @@ func ParseHashtags(text string) (string, string) { } } - return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) + return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString.String()) } func ClearMentionTags(post string) string { diff --git a/server/public/model/utils_test.go b/server/public/model/utils_test.go index 9865ce82c1..67ef4bab5f 100644 --- a/server/public/model/utils_test.go +++ b/server/public/model/utils_test.go @@ -541,10 +541,58 @@ func TestStringArray_Equal(t *testing.T) { } func TestParseHashtags(t *testing.T) { - for input, output := range hashtags { - o, _ := ParseHashtags(input) - require.Equal(t, o, output, "failed to parse hashtags from input="+input+" expected="+output+" actual="+o) - } + t.Run("basic hashtag extraction", func(t *testing.T) { + for input, output := range hashtags { + o, _ := ParseHashtags(input) + require.Equal(t, o, output, "failed to parse hashtags from input="+input+" expected="+output+" actual="+o) + } + }) + + t.Run("long hashtag string truncation", func(t *testing.T) { + // Test case where hashtag string exceeds 1000 characters with a space to truncate at + longHashtags := "#test " + strings.Repeat("#verylonghashtag ", 50) + hashtagString, plainString := ParseHashtags(longHashtags) + require.NotEmpty(t, hashtagString) + require.LessOrEqual(t, len(hashtagString), 1000) + require.Empty(t, plainString) + // Ensure it truncated at a space + require.NotEqual(t, "", hashtagString) + require.True(t, hashtagString[len(hashtagString)-1] != ' ') + }) + + t.Run("long hashtag string truncation without spaces", func(t *testing.T) { + // Test case where hashtag string exceeds 1000 characters with no space after position 999 + // Create a single very long hashtag that will be truncated + veryLongHashtag := "#" + strings.Repeat("a", 1010) + hashtagString, plainString := ParseHashtags(veryLongHashtag) + // Should be empty because no space was found to truncate at + require.Equal(t, "", hashtagString) + require.Empty(t, plainString) + }) + + t.Run("plain text extraction", func(t *testing.T) { + hashtagString, plainString := ParseHashtags("hello #world this is #test plain text") + require.Equal(t, "#world #test", hashtagString) + require.Equal(t, "hello this is plain text", plainString) + }) + + t.Run("only plain text", func(t *testing.T) { + hashtagString, plainString := ParseHashtags("no hashtags here") + require.Empty(t, hashtagString) + require.Equal(t, "no hashtags here", plainString) + }) + + t.Run("only hashtags", func(t *testing.T) { + hashtagString, plainString := ParseHashtags("#one #two #three") + require.Equal(t, "#one #two #three", hashtagString) + require.Empty(t, plainString) + }) + + t.Run("empty string", func(t *testing.T) { + hashtagString, plainString := ParseHashtags("") + require.Empty(t, hashtagString) + require.Empty(t, plainString) + }) } func TestIsValidAlphaNum(t *testing.T) { diff --git a/server/public/shared/markdown/fenced_code.go b/server/public/shared/markdown/fenced_code.go index c8caad5561..22f43bfd63 100644 --- a/server/public/shared/markdown/fenced_code.go +++ b/server/public/shared/markdown/fenced_code.go @@ -23,11 +23,12 @@ type FencedCode struct { RawCode []FencedCodeLine } -func (b *FencedCode) Code() (result string) { +func (b *FencedCode) Code() string { + var resultSb strings.Builder for _, code := range b.RawCode { - result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End] + resultSb.WriteString(strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]) } - return + return resultSb.String() } func (b *FencedCode) Info() string { diff --git a/server/public/shared/markdown/html.go b/server/public/shared/markdown/html.go index f15bfb664f..fb63c4198e 100644 --- a/server/public/shared/markdown/html.go +++ b/server/public/shared/markdown/html.go @@ -27,69 +27,75 @@ func RenderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition) ( return renderBlockHTML(block, referenceDefinitions, false) } -func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) (result string) { +func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) string { + var resultSb strings.Builder + switch v := block.(type) { case *Document: for _, block := range v.Children { - result += RenderBlockHTML(block, referenceDefinitions) + resultSb.WriteString(RenderBlockHTML(block, referenceDefinitions)) } case *Paragraph: if len(v.Text) == 0 { - return + return "" } if !isTightList { - result += "
" + resultSb.WriteString("
") } for _, inline := range v.ParseInlines(referenceDefinitions) { - result += RenderInlineHTML(inline) + resultSb.WriteString(RenderInlineHTML(inline)) } if !isTightList { - result += "
" + resultSb.WriteString("") } case *List: if v.IsOrdered { if v.OrderedStart != 1 { - result += fmt.Sprintf(`" + resultSb.WriteString("") case *FencedCode: if info := v.Info(); info != "" { language := strings.Fields(info)[0] - result += `") for _, block := range v.Children { - result += RenderBlockHTML(block, referenceDefinitions) + resultSb.WriteString(RenderBlockHTML(block, referenceDefinitions)) } - result += "" + resultSb.WriteString("
`
+ resultSb.WriteString(``)
} else {
- result += ""
+ resultSb.WriteString("")
}
- result += htmlEscaper.Replace(v.Code()) + "
"
+ resultSb.WriteString(htmlEscaper.Replace(v.Code()))
+ resultSb.WriteString("
")
case *IndentedCode:
- result += "" + htmlEscaper.Replace(v.Code()) + "
"
+ resultSb.WriteString("")
+ resultSb.WriteString(htmlEscaper.Replace(v.Code()))
+ resultSb.WriteString("
")
default:
panic(fmt.Sprintf("missing case for type %T", v))
}
- return
+
+ return resultSb.String()
}
func escapeURL(url string) (result string) {
@@ -137,31 +143,37 @@ func RenderInlineHTML(inline Inline) (result string) {
}
result += ` />`
case *InlineLink:
- result += ``
+ resultSb.WriteString(`>`)
for _, inline := range v.Children {
- result += RenderInlineHTML(inline)
+ resultSb.WriteString(RenderInlineHTML(inline))
}
- result += ""
+ resultSb.WriteString("")
+ return resultSb.String()
case *ReferenceLink:
- result += ``
+ resultSb.WriteString(`>`)
for _, inline := range v.Children {
- result += RenderInlineHTML(inline)
+ resultSb.WriteString(RenderInlineHTML(inline))
}
- result += ""
+ resultSb.WriteString("")
+ return resultSb.String()
case *Autolink:
- result += ``
+ var resultSb strings.Builder
+ resultSb.WriteString(``)
for _, inline := range v.Children {
- result += RenderInlineHTML(inline)
+ resultSb.WriteString(RenderInlineHTML(inline))
}
- result += ""
+ resultSb.WriteString("")
+ return resultSb.String()
case *Emoji:
escapedName := htmlEscaper.Replace(v.Name)
result += fmt.Sprintf(``, escapedName, escapedName)
@@ -172,25 +184,30 @@ func RenderInlineHTML(inline Inline) (result string) {
return
}
-func renderImageAltText(children []Inline) (result string) {
+func renderImageAltText(children []Inline) string {
+ var resultSb strings.Builder
for _, inline := range children {
- result += renderImageChildAltText(inline)
+ resultSb.WriteString(renderImageChildAltText(inline))
}
- return
+ return resultSb.String()
}
-func renderImageChildAltText(inline Inline) (result string) {
+func renderImageChildAltText(inline Inline) string {
switch v := inline.(type) {
case *Text:
return v.Text
case *InlineImage:
+ var resultSb strings.Builder
for _, inline := range v.Children {
- result += renderImageChildAltText(inline)
+ resultSb.WriteString(renderImageChildAltText(inline))
}
+ return resultSb.String()
case *InlineLink:
+ var resultSb strings.Builder
for _, inline := range v.Children {
- result += renderImageChildAltText(inline)
+ resultSb.WriteString(renderImageChildAltText(inline))
}
+ return resultSb.String()
}
- return
+ return ""
}
diff --git a/server/public/shared/markdown/indented_code.go b/server/public/shared/markdown/indented_code.go
index a89ee6c70c..9a4a6b8212 100644
--- a/server/public/shared/markdown/indented_code.go
+++ b/server/public/shared/markdown/indented_code.go
@@ -19,11 +19,12 @@ type IndentedCode struct {
RawCode []IndentedCodeLine
}
-func (b *IndentedCode) Code() (result string) {
+func (b *IndentedCode) Code() string {
+ var resultSb strings.Builder
for _, code := range b.RawCode {
- result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
+ resultSb.WriteString(strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End])
}
- return
+ return resultSb.String()
}
func (b *IndentedCode) Continuation(indentation int, r Range) *continuation {
diff --git a/server/public/shared/markdown/reference_definition.go b/server/public/shared/markdown/reference_definition.go
index 69e8ed9401..1c40cf83a1 100644
--- a/server/public/shared/markdown/reference_definition.go
+++ b/server/public/shared/markdown/reference_definition.go
@@ -3,6 +3,8 @@
package markdown
+import "strings"
+
type ReferenceDefinition struct {
RawDestination Range
@@ -24,10 +26,11 @@ func (d *ReferenceDefinition) Title() string {
}
func parseReferenceDefinition(markdown string, ranges []Range) (*ReferenceDefinition, []Range) {
- raw := ""
+ var rawSb strings.Builder
for _, r := range ranges {
- raw += markdown[r.Position:r.End]
+ rawSb.WriteString(markdown[r.Position:r.End])
}
+ raw := rawSb.String()
label, next, ok := parseLinkLabel(raw, 0)
if !ok {