feat: add support for image dimensions

This commit is contained in:
Johan Fagerberg 2026-02-19 09:44:33 +01:00 committed by Manuel Rüger
parent c32cd79dc8
commit 4d887bde74
6 changed files with 65 additions and 7 deletions

View File

@ -78,7 +78,14 @@ You can set a page emoji icon by specifying the icon in the headers.
<!-- Image-Align: center --> <!-- Image-Align: center -->
``` ```
You can set the alignment for all images in the page. Common values are `left`, `center`, and `right`. This adds the `ac:align` attribute to image tags. Can also be set globally via the `--image-align` CLI option (per-page header takes precedence). You can set the alignment for all images in the page. Common values are `left`, `center`, and `right`. This adds the `ac:align` attribute to image tags and also sets the corresponding `ac:layout` attribute:
- `left``ac:align="left" ac:layout="align-start"`
- `center``ac:align="center" ac:layout="center"`
- `right``ac:align="right" ac:layout="align-end"`
**Note**: Images with width >= 760px automatically use `ac:align="wide"` with `ac:layout="center"` instead of the configured alignment, as Confluence requires this for wide images.
Custom values are passed through as-is with only the `ac:align` attribute. Can also be set globally via the `--image-align` CLI option (per-page header takes precedence).
Mark supports Go templates, which can be included into article by using path Mark supports Go templates, which can be included into article by using path
to the template relative to current working dir, e.g.: to the template relative to current working dir, e.g.:

View File

@ -4,11 +4,16 @@ import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io" "io"
"net/url" "net/url"
"path" "path"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/kovetskiy/mark/confluence" "github.com/kovetskiy/mark/confluence"
@ -210,12 +215,20 @@ func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error)
return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath) return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath)
} }
return Attachment{ attachment := Attachment{
Name: name, Name: name,
Filename: strings.ReplaceAll(name, "/", "_"), Filename: strings.ReplaceAll(name, "/", "_"),
FileBytes: fileBytes, FileBytes: fileBytes,
Replace: name, Replace: name,
}, nil }
// Try to detect image dimensions
if config, _, err := image.DecodeConfig(bytes.NewReader(fileBytes)); err == nil {
attachment.Width = strconv.Itoa(config.Width)
attachment.Height = strconv.Itoa(config.Height)
}
return attachment, nil
} }
func CompileAttachmentLinks(markdown []byte, attachments []Attachment) []byte { func CompileAttachmentLinks(markdown []byte, attachments []Attachment) []byte {

View File

@ -139,10 +139,14 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
return ast.WalkStop, err return ast.WalkStop, err
} }
r.Attachments.Attach(attachment) r.Attachments.Attach(attachment)
effectiveAlign := calculateAlign(r.MarkConfig.ImageAlign, attachment.Width)
err = r.Stdlib.Templates.ExecuteTemplate( err = r.Stdlib.Templates.ExecuteTemplate(
writer, writer,
"ac:image", "ac:image",
struct { struct {
Align string
Width string Width string
Height string Height string
Title string Title string
@ -150,6 +154,7 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
Attachment string Attachment string
Url string Url string
}{ }{
effectiveAlign,
attachment.Width, attachment.Width,
attachment.Height, attachment.Height,
attachment.Name, attachment.Name,
@ -170,10 +175,14 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
return ast.WalkStop, err return ast.WalkStop, err
} }
r.Attachments.Attach(attachment) r.Attachments.Attach(attachment)
effectiveAlign := calculateAlign(r.MarkConfig.ImageAlign, attachment.Width)
err = r.Stdlib.Templates.ExecuteTemplate( err = r.Stdlib.Templates.ExecuteTemplate(
writer, writer,
"ac:image", "ac:image",
struct { struct {
Align string
Width string Width string
Height string Height string
Title string Title string
@ -181,6 +190,7 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
Attachment string Attachment string
Url string Url string
}{ }{
effectiveAlign,
attachment.Width, attachment.Width,
attachment.Height, attachment.Height,
attachment.Name, attachment.Name,

View File

@ -3,6 +3,7 @@ package renderer
import ( import (
"bytes" "bytes"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/kovetskiy/mark/attachment" "github.com/kovetskiy/mark/attachment"
@ -15,6 +16,30 @@ import (
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
) )
// calculateAlign determines the appropriate ac:align value based on width
// Images >= 760px wide use "wide", otherwise use the configured alignment
func calculateAlign(configuredAlign string, width string) string {
if configuredAlign == "" {
return ""
}
if width == "" {
return configuredAlign
}
// Parse width and check if >= 760
widthInt, err := strconv.Atoi(width)
if err != nil {
return configuredAlign
}
if widthInt >= 760 {
return "wide"
}
return configuredAlign
}
type ConfluenceImageRenderer struct { type ConfluenceImageRenderer struct {
html.Config html.Config
Stdlib *stdlib.Lib Stdlib *stdlib.Lib
@ -78,6 +103,8 @@ func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []by
r.Attachments.Attach(attachments[0]) r.Attachments.Attach(attachments[0])
effectiveAlign := calculateAlign(r.ImageAlign, attachments[0].Width)
err = r.Stdlib.Templates.ExecuteTemplate( err = r.Stdlib.Templates.ExecuteTemplate(
writer, writer,
"ac:image", "ac:image",
@ -90,9 +117,9 @@ func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []by
Attachment string Attachment string
Url string Url string
}{ }{
r.ImageAlign, effectiveAlign,
"", attachments[0].Width,
"", attachments[0].Height,
string(n.Title), string(n.Title),
string(nodeToHTMLText(n, source)), string(nodeToHTMLText(n, source)),
attachments[0].Filename, attachments[0].Filename,

View File

@ -212,6 +212,7 @@ func templates(api *confluence.API) (*template.Template, error) {
`ac:image`: text( `ac:image`: text(
`<ac:image`, `<ac:image`,
`{{ if .Align }} ac:align="{{ .Align }}"{{ end }}`, `{{ 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 .Width }} ac:width="{{ .Width }}"{{ end }}`, `{{ if .Width }} ac:width="{{ .Width }}"{{ end }}`,
`{{ if .Height }} ac:height="{{ .Height }}"{{ end }}`, `{{ if .Height }} ac:height="{{ .Height }}"{{ end }}`,
`{{ if .Title }} ac:title="{{ .Title }}"{{ 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="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="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>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:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></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: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: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"/><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> <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>