Skip to content
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ File in the extended format should follow the specification:
<!-- Attachment: <local path> -->
<!-- Label: <label 1> -->
<!-- Label: <label 2> -->
<!-- Image-Align: <left|center|right> -->

<page contents>
```
Expand Down Expand Up @@ -73,6 +74,14 @@ Setting the sidebar creates a column on the right side. You're able to add any

You can set a page emoji icon by specifying the icon in the headers.

```markdown
<!-- Image-Align: center -->
```

You can set the alignment for all images in the page. Common values are `left`, `center`, and `right`. Can also be set globally via the `--image-align` CLI option (per-page header takes precedence).

**Note**: Images with width >= 760px automatically use `center` instead of the configured alignment, as Confluence requires this for wide images.

Mark supports Go templates, which can be included into article by using path
to the template relative to current working dir, e.g.:

Expand Down Expand Up @@ -252,23 +261,23 @@ some long bash code block
| `linenumbers` | false |
| `1` (any number for firstline) | 1 |

Example:
Example:

* `bash collapse`
* `bash collapse`
If you have long code blocks, you can make them collapsible.
* `bash collapse title Some long long bash function`
* `bash collapse title Some long long bash function`
And you can also add a title.
* `bash linenumbers collapse title Some long long bash function`
* `bash linenumbers collapse title Some long long bash function`
And linenumbers.
* `bash 1 collapse title Some long long bash function`
* `bash 1 collapse title Some long long bash function`
Or directly give a number as firstline number.
* `bash 1 collapse midnight title Some long long bash function`
* `bash 1 collapse midnight title Some long long bash function`
And even themes.
* `- 1 collapse midnight title Some long long code`
* `- 1 collapse midnight title Some long long code`
Please note that, if you want to have a code block without a language
use `-` as the first character, if you want to have the other goodies.

More details at Confluence [Code Block Macro](https://confluence.atlassian.com/doc/code-block-macro-139390.html) doc.
More details at Confluence [Code Block Macro](https://confluence.atlassian.com/doc/code-block-macro-139390.html) doc.

### Block Quotes

Expand Down Expand Up @@ -844,6 +853,7 @@ GLOBAL OPTIONS:
--d2-scale float defines the scaling factor for d2 renderings. (default: 1) [$MARK_D2_SCALE]
--features string [ --features string ] Enables optional features. Current features: d2, mermaid, mention, mkdocsadmonitions (default: "mermaid", "mention") [$MARK_FEATURES]
--insecure-skip-tls-verify skip TLS certificate verification (useful for self-signed certificates) [$MARK_INSECURE_SKIP_TLS_VERIFY]
--image-align string set image alignment (left, center, right). Can be overridden per-file via the Image-Align header. [$MARK_IMAGE_ALIGN]
--help, -h show help
--version, -v print the version
```
Expand All @@ -858,6 +868,7 @@ password = "password-or-api-key-for-confluence-cloud"
base-url = "http://confluence.local"
title-from-h1 = true
drop-h1 = true
image-align = "center"
```

**NOTE**: Labels aren't supported when using `minor-edit`!
Expand Down
21 changes: 19 additions & 2 deletions attachment/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import (
"bytes"
"crypto/sha256"
"encoding/hex"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"net/url"
"path"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/kovetskiy/mark/confluence"
Expand Down Expand Up @@ -210,12 +215,24 @@ func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error)
return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath)
}

return Attachment{
attachment := Attachment{
Name: name,
Filename: strings.ReplaceAll(name, "/", "_"),
FileBytes: fileBytes,
Replace: name,
}, nil
}

// Try to detect image dimensions if it's an image attachment
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".jpg", ".jpeg", ".png", ".gif":
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 {
Expand Down
2 changes: 1 addition & 1 deletion markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.MarkConfig.DropFirstH1), 100),
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path, c.MarkConfig.ImageAlign), 100),
util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
))
Expand Down
5 changes: 5 additions & 0 deletions metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
HeaderInclude = `Include`
HeaderSidebar = `Sidebar`
ContentAppearance = `Content-Appearance`
HeaderImageAlign = `Image-Align`
)

type Meta struct {
Expand All @@ -39,6 +40,7 @@ type Meta struct {
Attachments []string
Labels []string
ContentAppearance string
ImageAlign string
}

const (
Expand Down Expand Up @@ -131,6 +133,9 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, titleFromFi
meta.ContentAppearance = FullWidthContentAppearance
}

case HeaderImageAlign:
meta.ImageAlign = strings.ToLower(strings.TrimSpace(value))

default:
log.Errorf(
nil,
Expand Down
50 changes: 38 additions & 12 deletions renderer/fencedcodeblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,19 +139,32 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
return ast.WalkStop, err
}
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 {
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.Name,
"",
attachment.Filename,
Expand All @@ -170,19 +183,32 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
return ast.WalkStop, err
}
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 {
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.Name,
"",
attachment.Filename,
Expand Down
112 changes: 98 additions & 14 deletions renderer/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package renderer
import (
"bytes"
"path/filepath"
"strconv"
"strings"

"github.com/kovetskiy/mark/attachment"
Expand All @@ -15,20 +16,83 @@ import (
"github.com/yuin/goldmark/util"
)

// calculateAlign determines the appropriate ac:align value
// Images >= 760px must use "center" alignment, smaller images can use configured alignment
func calculateAlign(configuredAlign string, width string) string {
if configuredAlign == "" {
return ""
}

if width != "" {
widthInt, err := strconv.Atoi(width)
if err == nil && widthInt >= 760 {
return "center"
}
}

return configuredAlign
}

// calculateLayout determines the appropriate ac:layout value based on width and alignment
// Images >= 1800px use "full-width", images >= 760px use "wide", otherwise based on alignment
// These thresholds are based on Confluence's behavior as of 2026-02, but may need adjustment in the future
// Returns empty string if no alignment is configured
func calculateLayout(align string, width string) string {
if align == "" {
return ""
}

if width != "" {
widthInt, err := strconv.Atoi(width)
if err == nil {
if widthInt >= 1800 {
return "full-width"
}
if widthInt >= 760 {
return "wide"
}
}
}

switch align {
case "left":
return "align-start"
case "center":
return "center"
case "right":
return "align-end"
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
Path string
Attachments attachment.Attacher
ImageAlign string
}

// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceImageRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, path string, opts ...html.Option) renderer.NodeRenderer {
func NewConfluenceImageRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, path string, imageAlign string, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceImageRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
Attachments: attachments,
ImageAlign: imageAlign,
}
}

Expand All @@ -55,13 +119,21 @@ func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []by
writer,
"ac:image",
struct {
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),
Expand All @@ -74,18 +146,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 {
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,
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
Expand Down
Loading
Loading