diff --git a/internal/router/template.go b/internal/router/template.go
index 0bae96548..1e3b2e715 100644
--- a/internal/router/template.go
+++ b/internal/router/template.go
@@ -25,7 +25,9 @@ import (
"path/filepath"
"reflect"
"regexp"
+ "slices"
"strings"
+ "sync"
"unsafe"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -134,25 +136,25 @@ func LoadTemplates(engine *gin.Engine) error {
}
var funcMap = template.FuncMap{
- "add": add,
- "acctInstance": acctInstance,
- "objectPosition": objectPosition,
- "demojify": demojify,
- "deref": deref,
- "emojify": emojify,
- "escape": escape,
- "increment": increment,
- "indent": indent,
- "indentAttr": indentAttr,
- "isNil": isNil,
- "outdentPre": outdentPre,
- "noescapeAttr": noescapeAttr,
- "noescape": noescape,
- "oddOrEven": oddOrEven,
- "subtract": subtract,
- "timestampPrecise": timestampPrecise,
- "timestampVague": timestampVague,
- "visibilityIcon": visibilityIcon,
+ "add": add,
+ "acctInstance": acctInstance,
+ "objectPosition": objectPosition,
+ "demojify": demojify,
+ "deref": deref,
+ "emojify": emojify,
+ "escape": escape,
+ "increment": increment,
+ "indent": indent,
+ "indentAttr": indentAttr,
+ "isNil": isNil,
+ "outdentPreformatted": outdentPreformatted,
+ "noescapeAttr": noescapeAttr,
+ "noescape": noescape,
+ "oddOrEven": oddOrEven,
+ "subtract": subtract,
+ "timestampPrecise": timestampPrecise,
+ "timestampVague": timestampVague,
+ "visibilityIcon": visibilityIcon,
}
func oddOrEven(n int) string {
@@ -291,11 +293,31 @@ func subtract(n1 int, n2 int) int {
}
var (
- indentRegex = regexp.MustCompile(`(?m)^`)
+ // Find starts of lines to replace with indent.
+ indentRegex = regexp.MustCompile(`(?m)^`)
+
+ // One indent level.
indentStr = " "
indentStrLen = len(indentStr)
- indents = strings.Repeat(indentStr, 12)
- indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)^((?:%s)+)
.*
`, indentStr))
+
+ // Preformatted slice of indents.
+ indents = strings.Repeat(indentStr, 12)
+
+ // Measure indent at the start of a line.
+ indentDepthStr = fmt.Sprintf(`^((?:%s)+)`, indentStr)
+ indentDepth = regexp.MustCompile(`(?m)` + indentDepthStr)
+
+ // Find tags and determine how indented they are.
+ indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)%s.*
`, indentDepthStr))
+ // Find content of alt or title attributes.
+ indentAltOrTitle = regexp.MustCompile(`(?Ums)\b(?:alt|title)="(.*)"(?:\b|>|$)`)
+
+ // Map of lazily-compiled replaceIndent
+ // regexes, keyed by the indent they
+ // replace, to avoid recompilation.
+ //
+ // At *most* 12 entries long.
+ replaceIndents = sync.Map{}
)
// indent appropriately indents the given html
@@ -318,32 +340,104 @@ func indentAttr(n int, html template.HTMLAttr) template.HTMLAttr {
return noescapeAttr(out)
}
-// outdentPre outdents all `` tags in the
-// given HTML so that they render correctly in code
-// blocks, even if they were indented before.
-func outdentPre(html template.HTML) template.HTML {
+// outdentPreformatted outdents all preformatted text in
+// the given HTML, ie., in `alt` and `title` attributes,
+// and between `` tags, so that it renders correctly,
+// even if it was indented before.
+func outdentPreformatted(html template.HTML) template.HTML {
input := string(html)
output := regexes.ReplaceAllStringFunc(indentPre, input,
func(match string, buf *bytes.Buffer) string {
// Reuse the regex to pull out submatches.
matches := indentPre.FindAllStringSubmatch(match, -1)
+
+ // Ensure matches
+ // expected length.
if len(matches) != 1 {
return match
}
+ // Ensure inner matches
+ // expected length.
+ innerMatches := matches[0]
+ if len(innerMatches) != 2 {
+ return match
+ }
+
var (
- indented = matches[0][0]
- indent = matches[0][1]
+ indentedContent = innerMatches[0]
+ indent = innerMatches[1]
)
- // Outdent everything in the inner match, add
- // a newline at the end to make it a bit neater.
- outdented := strings.ReplaceAll(indented, indent, "")
+ // Outdent everything in the inner match.
+ outdented := strings.ReplaceAll(indentedContent, indent, "")
// Replace original match with the outdented version.
- return strings.ReplaceAll(match, indented, outdented)
+ return strings.ReplaceAll(match, indentedContent, outdented)
},
)
+
+ output = regexes.ReplaceAllStringFunc(indentAltOrTitle, output,
+ func(match string, buf *bytes.Buffer) string {
+ // Reuse the regex to pull out submatches.
+ matches := indentAltOrTitle.FindAllStringSubmatch(match, -1)
+
+ // Ensure matches
+ // expected length.
+ if len(matches) != 1 {
+ return match
+ }
+
+ // Ensure inner matches
+ // expected length.
+ innerMatches := matches[0]
+ if len(innerMatches) != 2 {
+ return match
+ }
+
+ // The content of the alt or title
+ // attr inside quotation marks.
+ indentedContent := innerMatches[1]
+
+ // Find all indents in this text.
+ indents := indentDepth.FindAllString(indentedContent, -1)
+ if len(indents) == 0 {
+ // No indents in this text,
+ // it's probably just something
+ // inline like `alt="whatever"`.
+ return match
+ }
+
+ // Find the shortest indent as this
+ // is undoubtedly the one we added.
+ //
+ // By targeting the shortest one we
+ // avoid removing user-inserted
+ // whitespace at the start of lines
+ // of alt text (eg., in poetry etc).
+ slices.Sort(indents)
+ indent := indents[0]
+
+ // Load or create + store the
+ // regex to replace this indent,
+ // avoiding recompilation.
+ var replaceIndent *regexp.Regexp
+ if replaceIndentI, ok := replaceIndents.Load(indent); ok {
+ // Got regex for this indent.
+ replaceIndent = replaceIndentI.(*regexp.Regexp)
+ } else {
+ // No regex stored for
+ // this indent yet, store it.
+ replaceIndent = regexp.MustCompile(`(?m)^` + indent)
+ replaceIndents.Store(indent, replaceIndent)
+ }
+
+ // Remove all occurrences of the indent
+ // at the start of a line in the match.
+ return replaceIndent.ReplaceAllString(match, "")
+ },
+ )
+
return noescape(output)
}
diff --git a/internal/router/template_test.go b/internal/router/template_test.go
index 19bf759e0..1c82d3ba4 100644
--- a/internal/router/template_test.go
+++ b/internal/router/template_test.go
@@ -22,10 +22,19 @@ import (
"testing"
)
-func TestOutdentPre(t *testing.T) {
+func TestOutdentPreformatted(t *testing.T) {
const html = template.HTML(`
-
+
Here's a bunch of HTML, read it and weep, weep then!
<section class="about-user">
<div class="col-header">
@@ -67,7 +76,15 @@ func TestOutdentPre(t *testing.T) {
-
+
Here's a bunch of HTML, read it and weep, weep then!
<section class="about-user">
<div class="col-header">
@@ -112,7 +129,16 @@ func TestOutdentPre(t *testing.T) {
const expected = template.HTML(`
-
+
Here's a bunch of HTML, read it and weep, weep then!
<section class="about-user">
<div class="col-header">
@@ -154,7 +180,15 @@ func TestOutdentPre(t *testing.T) {
-
+
Here's a bunch of HTML, read it and weep, weep then!
<section class="about-user">
<div class="col-header">
@@ -197,7 +231,7 @@ func TestOutdentPre(t *testing.T) {
`)
- out := outdentPre(html)
+ out := outdentPreformatted(html)
if out != expected {
t.Fatalf("unexpected output:\n`%s`\n", out)
}
diff --git a/web/template/page.tmpl b/web/template/page.tmpl
index 0a54e74cb..4ea168300 100644
--- a/web/template/page.tmpl
+++ b/web/template/page.tmpl
@@ -79,7 +79,7 @@ image/webp
{{- include "page_header.tmpl" . | indent 3 }}
- {{- include .pageContent . | indent 3 | outdentPre }}
+ {{- include .pageContent . | indent 3 | outdentPreformatted }}