feat: align image rendering with Confluence default

This commit is contained in:
Johan Fagerberg 2026-02-19 10:24:26 +01:00 committed by Manuel Rüger
parent 4d887bde74
commit cbc7400f92
5 changed files with 184 additions and 30 deletions

View File

@ -141,22 +141,30 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
r.Attachments.Attach(attachment)
effectiveAlign := calculateAlign(r.MarkConfig.ImageAlign, attachment.Width)
effectiveLayout := calculateLayout(effectiveAlign, attachment.Width)
displayWidth := calculateDisplayWidth(attachment.Width, effectiveLayout)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Align string
Width string
Height string
Title string
Alt string
Attachment string
Url string
Align string
Layout string
OriginalWidth string
OriginalHeight string
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
effectiveAlign,
effectiveLayout,
attachment.Width,
attachment.Height,
displayWidth,
attachment.Height,
attachment.Name,
"",
attachment.Filename,
@ -177,22 +185,30 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
r.Attachments.Attach(attachment)
effectiveAlign := calculateAlign(r.MarkConfig.ImageAlign, attachment.Width)
effectiveLayout := calculateLayout(effectiveAlign, attachment.Width)
displayWidth := calculateDisplayWidth(attachment.Width, effectiveLayout)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Align string
Width string
Height string
Title string
Alt string
Attachment string
Url string
Align string
Layout string
OriginalWidth string
OriginalHeight string
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
effectiveAlign,
effectiveLayout,
attachment.Width,
attachment.Height,
displayWidth,
attachment.Height,
attachment.Name,
"",
attachment.Filename,

View File

@ -40,6 +40,43 @@ func calculateAlign(configuredAlign string, width string) string {
return configuredAlign
}
// calculateLayout determines the appropriate ac:layout value based on alignment and width
// Images >= 1800px use "full-width", otherwise based on alignment
func calculateLayout(align string, width string) string {
// Check if full-width should be used
if width != "" {
widthInt, err := strconv.Atoi(width)
if err == nil && widthInt >= 1800 {
return "full-width"
}
}
// Otherwise use layout based on alignment
switch align {
case "left":
return "align-start"
case "center":
return "center"
case "right":
return "align-end"
case "wide":
return "center"
default:
return ""
}
}
// calculateDisplayWidth determines the display width
// Full-width layout uses 1800px, otherwise uses original width
func calculateDisplayWidth(originalWidth string, layout string) string {
if layout == "full-width" {
return "1800"
}
return originalWidth
}
type ConfluenceImageRenderer struct {
html.Config
Stdlib *stdlib.Lib
@ -82,15 +119,21 @@ func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []by
writer,
"ac:image",
struct {
Align string
Width string
Height string
Title string
Alt string
Attachment string
Url string
Align string
Layout string
OriginalWidth string
OriginalHeight string
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
r.ImageAlign,
calculateLayout(r.ImageAlign, ""),
"",
"",
"",
"",
string(n.Title),
@ -104,22 +147,30 @@ func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []by
r.Attachments.Attach(attachments[0])
effectiveAlign := calculateAlign(r.ImageAlign, attachments[0].Width)
effectiveLayout := calculateLayout(effectiveAlign, attachments[0].Width)
displayWidth := calculateDisplayWidth(attachments[0].Width, effectiveLayout)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Align string
Width string
Height string
Title string
Alt string
Attachment string
Url string
Align string
Layout string
OriginalWidth string
OriginalHeight string
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
effectiveAlign,
effectiveLayout,
attachments[0].Width,
attachments[0].Height,
displayWidth,
attachments[0].Height,
string(n.Title),
string(nodeToHTMLText(n, source)),
attachments[0].Filename,

84
renderer/image_test.go Normal file
View File

@ -0,0 +1,84 @@
package renderer
import "testing"
func TestCalculateAlign(t *testing.T) {
tests := []struct {
name string
configuredAlign string
width string
expectedAlign string
}{
{"No alignment configured", "", "1000", ""},
{"No width available", "center", "", "center"},
{"Below threshold", "center", "500", "center"},
{"At threshold", "center", "760", "wide"},
{"Above threshold", "center", "1000", "wide"},
{"Left below threshold", "left", "700", "left"},
{"Left at threshold", "left", "760", "wide"},
{"Invalid width", "center", "abc", "center"},
{"Large image", "center", "2000", "wide"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateAlign(tt.configuredAlign, tt.width)
if result != tt.expectedAlign {
t.Errorf("calculateAlign(%q, %q) = %q, want %q", tt.configuredAlign, tt.width, result, tt.expectedAlign)
}
})
}
}
func TestCalculateLayout(t *testing.T) {
tests := []struct {
name string
align string
width string
expectedLayout string
}{
{"Left alignment", "left", "500", "align-start"},
{"Center alignment", "center", "500", "center"},
{"Right alignment", "right", "500", "align-end"},
{"Wide alignment", "wide", "1000", "center"},
{"Full-width threshold", "center", "1800", "full-width"},
{"Above full-width", "left", "2000", "full-width"},
{"Below full-width", "center", "1799", "center"},
{"No alignment", "", "1000", ""},
{"Unknown alignment", "justify", "500", ""},
{"Invalid width", "center", "abc", "center"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateLayout(tt.align, tt.width)
if result != tt.expectedLayout {
t.Errorf("calculateLayout(%q, %q) = %q, want %q", tt.align, tt.width, result, tt.expectedLayout)
}
})
}
}
func TestCalculateDisplayWidth(t *testing.T) {
tests := []struct {
name string
originalWidth string
layout string
expectedWidth string
}{
{"Full-width layout", "2000", "full-width", "1800"},
{"Center layout keeps original", "1000", "center", "1000"},
{"Align-start keeps original", "800", "align-start", "800"},
{"Empty original", "", "center", ""},
{"Empty layout", "1000", "", "1000"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculateDisplayWidth(tt.originalWidth, tt.layout)
if result != tt.expectedWidth {
t.Errorf("calculateDisplayWidth(%q, %q) = %q, want %q", tt.originalWidth, tt.layout, result, tt.expectedWidth)
}
})
}
}

View File

@ -212,7 +212,10 @@ func templates(api *confluence.API) (*template.Template, error) {
`ac:image`: text(
`<ac:image`,
`{{ if .Align }} ac:align="{{ .Align }}"{{ end }}`,
`{{ if eq .Align "left" }} ac:layout="align-start"{{ else if eq .Align "center" }} ac:layout="center"{{ else if eq .Align "right" }} ac:layout="align-end"{{ else if eq .Align "wide" }} ac:layout="center"{{ end }}`,
`{{ if .Layout }} ac:layout="{{ .Layout }}"{{ end }}`,
`{{ if .OriginalWidth }} ac:original-width="{{ .OriginalWidth }}"{{ end }}`,
`{{ if .OriginalHeight }} ac:original-height="{{ .OriginalHeight }}"{{ end }}`,
`{{ if .Width }} ac:custom-width="true"{{ end }}`,
`{{ if .Width }} ac:width="{{ .Width }}"{{ end }}`,
`{{ if .Height }} ac:height="{{ .Height }}"{{ end }}`,
`{{ if .Title }} ac:title="{{ .Title }}"{{ end }}`,

2
testdata/links.html vendored
View File

@ -5,7 +5,7 @@
<p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
<p><ac:image ac:width="1000" ac:height="631" ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
<p><ac:image ac:original-width="1000" ac:original-height="631" ac:custom-width="true" ac:width="1000" ac:height="631" ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&amp;key2=value2"/></ac:image></p>
<p><ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[My test_link]]></ac:plain-text-link-body></ac:link></p>
<p><ac:link><ri:page ri:content-title="test_link_link"/><ac:plain-text-link-body><![CDATA[Another [Link]]]></ac:plain-text-link-body></ac:link></p>