This is based on guoweis-work PR https://github.com/kovetskiy/mark/pull/145 * feat(confluence): add support for fetching page body and inline comments * feat(cmd): add --preserve-comments flag to preserve inline comments * feat(mark): implement context-aware inline comment preservation * test(mark): add tests for context-aware MergeComments logic * fix: remove empty else branch in MergeComments to fix SA9003 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf: compile markerRegex once as package-level variable Avoids recompiling the inline comment marker regex on every call to MergeComments, which matters for pages with many comment markers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: guard against nil comments pointer in MergeComments Prevents a panic when GetInlineComments returns nil (e.g. on pages where the inline comments feature is not enabled). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add edge-case tests for MergeComments; fix overlapping replacement Four new test cases: - SelectionMissing: comment dropped gracefully when text is gone from new body - OverlappingSelections: overlapping comments no longer corrupt the body; the later match (by position) wins and the earlier overlapping one is dropped - NilComments: nil pointer returns new body unchanged - HTMLEntities: <, >, ' selections match correctly Also fixes the overlapping replacement bug: apply back-to-front and skip any replacement whose end exceeds the start of an already-applied one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: escape ref attribute value in inline comment marker XML Use html.EscapeString on r.ref before interpolating it into the ac:ref attribute to prevent malformed XML if the value ever contains quotes or other special characters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use first occurrence when no context is available in MergeComments Without context the old code left distance=0 for every match and updated bestStart on each iteration, so the final result depended on whichever occurrence was visited last (non-deterministic with respect to the search order). Restructure the loop to break immediately on the first match when hasCtx is false, making the behaviour explicit and deterministic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: log warning when overlapping inline comment marker is dropped Previously the overlap was silently skipped. Now a zerolog Warn message is emitted with the ref, the conflicting byte offsets, and the ref of the already-placed marker, so users can see which comment was lost rather than silently getting incomplete output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: warn when inline comments are silently dropped in MergeComments Three cases now emit a zerolog Warn instead of silently discarding: 1. Comment location != "inline": logs ref and actual location. 2. Selected text not found in new body: logs ref and selection text. 3. Overlapping replacement (existing): adds selection text to the already-present overlap warning for easier diagnosis. Also adds a selection field to the replacement struct so the overlap warning can report the dropped text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update markerRegex to match markers with nested tags Replace ([^<]*) with (?s)(.*?) so the pattern: - Matches marker content that contains nested inline tags (e.g. <strong>) - Matches across newlines ((?s) / DOTALL mode) The old character class [^<]* stopped at the first < inside the marker body, causing the context-extraction step to miss any comment whose original selection spanned formatted text. Add TestMergeComments_NestedTags to cover this path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: guard against empty OriginalSelection in MergeComments strings.Index(s, "") always returns 0, so an empty escapedSelection would spin the search loop indefinitely (or panic when currentPos advances past len(newBody)). Skip comments with an empty selection early, emit a Warn log, and add TestMergeComments_EmptySelection to cover the path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: paginate GetInlineComments to avoid silently truncating results The Confluence child/comment endpoint is paginated. The previous single-request implementation silently dropped any comments beyond the server's default page size. Changes: - Add Links (context, next) to InlineComments struct so the _links field from each page response is decoded. - Rewrite GetInlineComments to loop with limit/start parameters (pageSize=100), accumulating all results, following the same pattern used by GetAttachments and label fetching. - Add TestMergeComments_DuplicateMarkerRef to cover the deduplication guard added in the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix UTF-8 safety, API compat, log verbosity - levenshteinDistance: convert to []rune before empty-string checks so rune counts (not byte counts) are returned for strings with multi-byte characters - Add contextBefore/contextAfter helpers that use utf8.RuneStart to avoid slicing in the middle of a multi-byte UTF-8 sequence when extracting 100-char context windows from oldBody and newBody - Add truncateSelection helper (50 runes + ellipsis) and apply it in all Warn log messages that include the selected text, preventing large or sensitive page content from appearing in logs - Downgrade non-inline comment log from Warn to Debug with message 'comment ignored during inline marker merge: not an inline comment'; page-level comments are not inline markers and are not 'lost' - Restore original one-argument GetPageByID (expand='ancestors,version') and add GetPageByIDExpanded for the one caller that needs a custom expand value, preserving backward compatibility for API consumers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address new PR review comments - Remove custom min() function: shadows the Go 1.21+ built-in min for the entire package; the built-in handles the 3-arg call in levenshteinDistance identically - Validate rune boundaries on strings.Index candidates: skip any match where start or end falls in the middle of a multi-byte UTF-8 rune to prevent corrupt UTF-8 output - Defer preserve-comments API calls until after shouldUpdatePage is determined: avoids unnecessary GetPageByIDExpanded + GetInlineComments round-trips on no-op --changes-only runs - Capitalize Usage string for --preserve-comments flag (util/flags.go) and matching README.md entry to match sentence case of surrounding flags - Run gofmt on util/cli.go to fix struct literal field alignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: document --preserve-comments feature in README Add a dedicated 'Preserving Inline Comments' section under Tricks with: - Usage examples (CLI flag and env var) - Step-by-step explanation of the Levenshtein-based relocation algorithm - Limitations (deleted text, overlapping selections, new pages, changes-only interaction) Also add a cross-reference NOTE near the --preserve-comments flag entry in the Usage section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: fix markdownlint errors in README - Change unordered list markers from dashes to asterisks (MD004) - Remove extra blank line before Issues section (MD012) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extract named types for InlineComments; optimize Levenshtein search - Introduce InlineCommentProperties, InlineCommentExtensions, and InlineCommentResult named types in confluence/api.go, replacing the anonymous nested struct in InlineComments.Results. Callers and tests can now construct/inspect comment objects without repeating the JSON shape. - Simplify makeComments helper in mark_test.go to use the new named types directly, eliminating the verbose anonymous struct literal. - Add two Levenshtein candidate-search optimisations in MergeComments: * Exact-context fast path: if both the before and after windows match exactly, take that occurrence immediately without computing distance. * Lower-bound pruning: skip the full O(m*n) Levenshtein computation for a candidate when the absolute difference in window lengths alone already meets or exceeds the current best distance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use stable sort with ref tie-breaker; fix README overlap description - Replace slices.SortFunc with slices.SortStableFunc for the replacements slice, adding ref as a lexicographic tie-breaker when two markers resolve to the same start offset. This makes overlap resolution fully deterministic across runs. - Correct the README limitation note: the *earlier* overlapping match (lower byte offset) is what gets dropped; the later one (higher byte offset, applied first in the back-to-front pass) is kept. The previous wording said 'the second one is dropped' which was ambiguous and inaccurate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix rune-based lower-bound pruning; clarify test comment - Use utf8.RuneCountInString instead of len() for the Levenshtein lower-bound pruning computation. The levenshteinDistance function operates on rune slices, so byte-length differences can exceed the true rune-length difference for multibyte UTF-8 content, causing valid candidates to be incorrectly skipped. - Update TestMergeComments_SelectionMissing comment to say the comment is 'dropped with a warning' rather than 'silently dropped', matching the actual behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add missing unit tests for helpers and MergeComments scenarios Helper function tests: - TestTruncateSelection: short/exact/long strings and multibyte runes - TestLevenshteinDistance: empty strings, identical, insertions, deletions, substitutions, 'kitten/sitting', and a multibyte UTF-8 case to exercise rune-based counting - TestContextBefore / TestContextAfter: basic windowing, window larger than string, and a case where the raw byte offset lands mid-rune (é) to verify the rune-boundary correction logic MergeComments scenario tests: - TestMergeComments_MultipleComments: two non-overlapping comments both correctly applied via back-to-front replacement - TestMergeComments_EmptyResults: non-nil InlineComments with zero results returns body unchanged - TestMergeComments_NonInlineLocation: page-level comments (location != 'inline') are skipped; body unchanged - TestMergeComments_NoContext: when a ref has no marker in oldBody the first occurrence of the selection in newBody is used - TestMergeComments_UTF8: multibyte (Japanese) characters in both body and selection are handled correctly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix three correctness issues in MergeComments - Fix html import shadowing: alias the 'html' import as 'stdhtml' to avoid shadowing by the local 'html' variable used throughout ProcessFile. Both callers updated: stdhtml.EscapeString for the ref attribute, htmlEscapeText for the selection search. - Fix selection search with quotes/apostrophes: replace html.EscapeString for the selection with a new htmlEscapeText helper that only escapes &, <, > — not ' or ". Confluence storage HTML often leaves quotes and apostrophes unescaped in text nodes, so fully-escaped selections would fail to match and inline comments would be silently dropped. Add TestMergeComments_SelectionWithQuotes. - Fix duplicate-ref warnings: move seenRefs[ref]=true to immediately after the duplicate-check, before the search loop. Previously seenRefs was only set on a successful match, so multiple results for the same MarkerRef with no match in the new body would each emit a 'dropped' warning. Add TestMergeComments_DuplicateMarkerRefDropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Optimize levenshteinDistance to use two rolling rows instead of full matrix Reduces memory allocation from O(m×n) to O(n) by keeping only the previous and current rows. Also swaps r1/r2 so the shorter string is used for column width, minimizing row allocation size. This matters in MergeComments where levenshteinDistance is called for every candidate match of every comment's selection in newBody — on pages with many comments or short/common selections the number of calls can be high. Addresses thread [40] from PR review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix test description and README algorithm doc mark_test.go (thread [43]): - TestMergeComments_HTMLEntities: the description incorrectly claimed ' (apostrophe) was tested; the selection '<world>' contains no apostrophe. Updated comment to accurately describe what is covered (</> entity matching) and note the ' limitation. - Add TestMergeComments_ApostropheSelection: verifies a selection with a literal apostrophe is found when the new body also has a literal apostrophe (the common case from mark's renderer). This exercises the htmlEscapeText path which intentionally does not encode ' or ". README.md (thread [42]): - Step 2 of the algorithm description said context was recorded 'immediately before and after the commented selection' which is ambiguous. Clarified that context windows are taken around the <ac:inline-comment-marker> tag boundaries in the old body (not around the raw selection text), so the context is stable even when the marker wraps additional inline markup such as <strong>. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Unexport mergeComments and cap candidate evaluation Thread [44]: MergeComments was exported but is internal-only — only called within the mark package and tested from the same package. Unexport it to mergeComments to avoid expanding the public API surface unnecessarily. Add a Go doc comment describing the function contract, HTML expectations, and the candidate cap. Thread [45]: The candidate-scoring loop had no upper bound. For short or common selections (e.g. 'a', 'the') on large pages the loop could invoke levenshteinDistance thousands of times, each allocating rune and int slices. Add a maxCandidates=100 constant and break once that many on-rune-boundary occurrences have been evaluated. The exact-context fast-path and lower-bound pruning already skip many candidates before Levenshtein is called, so in practice the cap is only reached for very common selections where the 100th candidate is unlikely to be meaningfully better than an earlier one anyway. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: fix HTMLEntities description and add ApostropheEncoded limitation test Thread #43: TestMergeComments_HTMLEntities had a misleading note claiming it covered the ' apostrophe case, but the selection under test ('<world>') did not include an apostrophe. Remove that note and add a dedicated TestMergeComments_ApostropheEncoded test that explicitly documents the known limitation: when a Confluence body stores an apostrophe as the numeric entity ', mergeComments cannot locate the selection (htmlEscapeText does not encode ' to '), so the comment is dropped with a warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CDATA selection fallback and extract contextWindowBytes constant Thread #46: mergeComments only searched for htmlEscapeText(selection) and would fail for selections inside CDATA-backed macro bodies (e.g. ac:code), where < and > are stored as raw characters rather than HTML entities. Restructure the search loop to build a searchForms slice: the escaped form is tried first (covers normal XML text nodes), and the raw unescaped form is appended as a fallback when they differ. A stopSearch flag exits early on an exact context match or when maxCandidates is reached, preserving the same performance guarantees as before. Add TestMergeComments_CDATASelection to cover this path. Thread #47: The context-window size 100 was repeated in four places across mergeComments (two in the context-extraction loop and two in the scoring loop). Extract it to const contextWindowBytes = 100 so it is easy to tune and stays consistent everywhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
55 KiB
Mark
Mark — a tool for syncing your markdown documentation with Atlassian Confluence pages.
This is very useful if you store documentation to your software in a Git repository and don't want to do an extra job of updating Confluence page using a tinymce wysiwyg enterprise core editor which always breaks everything.
Mark does the same but in a different way. Mark reads your markdown file, creates a Confluence page if it's not found by its name, uploads attachments, translates Markdown into HTML and updates the contents of the page via REST API. It's like you don't even need to create sections/pages in your Confluence anymore, just use them in your Markdown documentation.
Mark uses an extended file format, which, still being valid markdown, contains several HTML-ish metadata headers, which can be used to locate page inside Confluence instance and update it accordingly.
File in the extended format should follow the specification:
<!-- Space: <space key> -->
<!-- Parent: <parent 1> -->
<!-- Parent: <parent 2> -->
<!-- Title: <title> -->
<!-- Attachment: <local path> -->
<!-- Label: <label 1> -->
<!-- Label: <label 2> -->
<!-- Image-Align: <left|center|right> -->
<page contents>
There can be any number of Parent headers, if Mark can't find specified
parent by title, Mark creates it.
Also, optional following headers are supported:
<!-- Layout: (article|plain) -->
- (default) article: content will be put in narrow column for ease of reading;
- plain: content will fill all page;
<!-- Type: (page|blogpost) -->
- (default) page: normal Confluence page - defaults to this if omitted
- blogpost: Blog post in
Space. Cannot haveParent(s)
<!-- Content-Appearance: (full-width|fixed) -->
- (default) full-width: content will fill the full page width
- fixed: content will be rendered in a fixed narrow view
<!-- Sidebar: <h2>Test</h2> -->
Setting the sidebar creates a column on the right side. You're able to add any valid HTML content. Adding this property sets the layout to article.
<!-- Emoji: 🚀 -->
You can set a page emoji icon by specifying the icon in the headers.
<!-- 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.:
<!-- Include: <path> -->
If the template cannot be found relative to the current directory, a fallback directory can be defined via --include-path. This way it is possible to have global include files while local ones will still take precedence.
Optionally the delimiters can be defined:
<!-- Include: <path>
Delims: "<<", ">>"
-->
Or they can be switched off to disable processing:
<!-- Include: <path>
Delims: none
-->
Note: Switching delimiters off really simply changes them to ASCII characters "\x00" and "\x01" which, usually should not occure in a template.
Templates can accept configuration data in YAML format which immediately
follows the Include and Delims tag, if present:
<!-- Include: <path>
<yaml-data> -->
Mark also supports attachments. The standard way involves declaring an
Attachment along with the other items in the header, then have any links
with the same path:
<!-- Attachment: <path-to-image> -->
<beginning of page content>
An attached link is [here](<path-to-image>)
NOTE: Be careful with Attachment! If your path string is a subset of
another longer string or referenced in text, you may get undesired behavior.
Mark also supports macro definitions, which are defined as regexps which will be replaced with specified template:
<!-- Macro: <regexp>
Template: <path>
<yaml-data> -->
NOTE: Make sure to define your macros after your metadata (Title/Space), mark will stop processing metadata if it hits a Macro.
Capture groups can be defined in the macro's which can be later
referenced in the <yaml-data> using ${<number>} syntax, where <number> is
number of a capture group in regexp (${0} is used for entire regexp match),
for example:
<!-- Macro: MYJIRA-\d+
Template: ac:jira:ticket
Ticket: ${0} -->
Macros can also use inline templates.
Inline templates are templates where the template content
is described in the <yaml-data>.
The Template value starts with a #, followed by the key
used in the <yaml-data>.
The key's value must be a string which defines the template's content.
<!-- Macro: <tblbox\s+(.*?)\s*>
Template: #inline
title: ${1}
inline: |
<table>
<thead><tr><th>{{ .title }}</th></tr></thead>
<tbody><tr><td>
-->
<!-- Macro: </tblbox>
Template: #also_inline
also_inline: |
</td></tr></tbody></table>
-->
<tblbox with a title>
and some
content
</tblbox>
Automatic Page Title
If you don't want to specify the page title in the metadata of each file, mark provides two ways to set it automatically.
From the first H1 heading
You can use the --title-from-h1 flag to extract the page title from the first H1 heading in the markdown file. If no H1 heading is found, the title must be set in the page metadata.
From the filename
You can use the --title-from-filename flag to use the filename (without the extension) as the page title. mark will automatically convert the filename to a more readable title by:
- Replacing underscores (
_) and dashes (-) with spaces. - Applying title case to the filename.
For example, a file named my_awesome-page.md will have the title "My Awesome Page".
These two options are mutually exclusive. If both flags are provided, mark will produce an error.
Customizing the page layout
If you set the Layout to plain, the page layout can be customized using HTML comments inside the markdown:
<!-- Layout: plain -->
<!-- ac:layout -->
<!-- ac:layout-section type:three_with_sidebars -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
Even More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout-section type:single -->
<!-- ac:layout-cell -->
Still More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout end -->
Please be aware that mark does not validate the layout, so it's your responsibility to create a valid layout.
Placeholders
You can use this to define placeholders:
<!-- ac:placeholder -->
Placeholder
<!-- ac:placeholder end -->
Code Blocks
```bash
...
some long bash code block
...
```
| Parameter | Default |
|---|---|
collapse |
false |
title |
none |
linenumbers |
false |
1 (any number for firstline) |
1 |
Example:
bash collapseIf you have long code blocks, you can make them collapsible.bash collapse title Some long long bash functionAnd you can also add a title.bash linenumbers collapse title Some long long bash functionAnd linenumbers.bash 1 collapse title Some long long bash functionOr directly give a number as firstline number.bash 1 collapse midnight title Some long long bash functionAnd even themes.- 1 collapse midnight title Some long long codePlease 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 doc.
Block Quotes
GitHub Alerts Support
You can now use GitHub-style alert syntax in your markdown, and Mark will automatically convert them to Confluence macros:
> [!NOTE]
> This creates a blue info box - perfect for helpful information!
> [!TIP]
> This creates a green tip box - great for best practices and suggestions!
> [!IMPORTANT]
> This creates a blue info box - ideal for critical information!
> [!WARNING]
> This creates a yellow warning box - use for important warnings!
> [!CAUTION]
> This creates a red warning box - perfect for dangerous situations!
Technical Details
Block Quotes are converted to Confluence Info/Warn/Note box when the following conditions are met:
- The BlockQuote is on the root level of the document (not nested)
- The first line of the BlockQuote contains one of the following patterns
Info/Warn/Noteor GitHub MD Alerts style[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]
| GitHub Alerts | Confluence | Description |
|---|---|---|
[!TIP] (green lightbulb) |
Tip (green checkmark in circle) | Helpful suggestions and best practices |
[!NOTE] (blue I in circle) |
Info (blue I in circle) | General information and notes |
[!IMPORTANT] (purple exclamation mark in speech bubble) |
Info (blue I in circle) | Critical information that needs attention |
[!WARNING] (yellow exclamation mark in triangle) |
Note (yellow exclamation mark in triangle) | Important warnings and cautions |
[!CAUTION] (red exclamation mark in hexagon) |
Warning (red exclamation mark in hexagon) | Dangerous situations requiring immediate attention |
In any other case the default behaviour will be resumed and html <blockquote> tag will be used
Task Lists
Mark supports GitHub Flavored Markdown task lists.
Task lists are automatically converted to Confluence ac:task-list elements.
- [x] Finished task
- [ ] Unfinished task
If a list is "mixed" (contains both tasks and regular list items), it will fall back to a standard HTML list with textual markers like [x] or [ ] to ensure validity in Confluence storage format.
Template & Macros
By default, mark provides several built-in templates and macros:
-
template
ac:statusto include badge-like text, which accepts following parameters:- Title: text to display in the badge
- Color: color to use as background/border for badge
- Grey
- Red
- Yellow
- Green
- Blue
- Subtle: specify to fill badge with background or not
- true
- false
-
template
ac:boxto include info, tip, note, and warning text boxes. Parameters:- Name: select box style
- info
- tip
- note
- warning
- Icon: show information/tip/exclamation mark/warning icon
- true
- false
- Title: title text of the box
- Body: text to display in the box
See: https://confluence.atlassian.com/conf59/info-tip-note-and-warning-macros-792499127.html
- Name: select box style
-
template
ac:jira:ticketto include JIRA ticket link. Parameters:- Ticket: Jira ticket number like BUGS-123.
See: https://confluence.atlassian.com/conf59/status-macro-792499207.html
-
template
ac:jira:filterto include JIRA Filters/Searches. Parameters:- JQL: The "JQL" query of the search
- Server (Optional): The Jira server to fetch the query from if its not the default of "System Jira"
-
template
ac:jiraissuesto include a list of JIRA tickets. Parameters:- URL (Required), The URL of the XML view of your selected issues. (link to the filter)
- Anonymous (Optional) If this parameter is set to 'true', your JIRA application will return only the issues which allow unrestricted viewing. That is, the issues which are visible to anonymous viewers. If this parameter is omitted or set to 'false', then the results depend on how your administrator has configured the communication between the JIRA application and Confluence. By default, Confluence will show only the issues which the user is authorised to view.
- BaseURL (Optional) If you specify a 'baseurl', then the link in the header, pointing to your JIRA application, will use this base URL instead of the value of the 'url' parameter. This is useful when Confluence connects to JIRA with a different URL from the one used by other users.
- Columns (Optional) A list of JIRA column names, separated by semi-colons (;). You can include many columns recognized by your JIRA application, including custom columns.
- Count (Optional) If this parameter is set to 'true', the issue list will show the number of issues in JIRA. The count will be linked to your JIRA site.
- Cache (Optional) The macro maintains a cache of the issues which result from the JIRA query. If the 'cache' parameter is set to 'off', the relevant part of the cache is cleared each time the macro is reloaded. (The value 'false' also works and has the same effect as 'off'.)
- Height (Optional) The height in pixels of the table displaying the issues.
- RenderMode (Optional) If the value is 'dynamic', the JIRA Issues macro offers an interactive display.
- Title (Optional) You can customise the title text at the top of the issues table with this parameter. For instance, setting the title to 'Bugs-to-fix' will replace the default 'JIRA Issues' text. This can help provide more context to the list of issues displayed.
- Width (Optional) The width of the table displaying the issues. Can be entered as a percentage (%) or in pixels (px).
See: https://confluence.atlassian.com/doc/jira-issues-macro-139380.html
-
template:
ac:emoticonto include emoticons. Parameters:- Name: select emoticon
- smile
- sad
- cheeky
- laugh
- wink
- thumbs-up
- thumbs-down
- information
- tick
- cross
- warning
- plus
- minus
- question
- light-on
- light-off
- yellow-star
- red-star
- green-star
- blue-star
See: https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html
- Name: select emoticon
-
template:
ac:youtubeto include YouTube Widget. Parameters:- URL: YouTube video endpoint
- Width: Width in px. Defaults to "640px"
- Height: Height in px. Defaults to "360px"
-
template:
ac:childrento include Children Display macro- Reverse (Reverse Sort): Use with the
Sort Children Byparameter. When set, the sort order changes from ascending to descending.truefalse(Default)
- Sort (Sort Children By):
creation— to sort by content creation datetitle— to sort alphabetically on titlemodified— to sort of last modification date.- If not specified, manual sorting is used if manually ordered, otherwise alphabetical.
- Style (Heading Style): Choose the style used to display descendants.
- from
h1toh6 - If not specified, default style is applied.
- from
- Page (Parent Page):
/— to list the top-level pages of the current space, i.e. those without parents.pagename— to list the children of the specified page.spacekey:pagename— to list the children of the specified page in the specified space.- If not specified, the current page is used.
- Excerpt (Include Excerpts): Allows you to include a short excerpt under each page in the list.
none- no excerpt will be displayed. (Default)simple- displays the first line of text contained in an Excerpt macro any of the returned pages. If there is not an Excerpt macro on the page, nothing will be shown.rich content- displays the contents of an Excerpt macro, or if there is not an Excerpt macro on the page, the first part of the page content, including formatted text, images and some macros.
- First (Number of Children): Restrict the number of child pages that are displayed at the top level.
- If not specified, no limit is applied.
- Depth (Depth of Descendants): Enter a number to specify the depth of descendants to display. For example, if the value is 2, the macro will display 2 levels of child pages. This setting has no effect if
Show Descendantsis enabled.- If not specified, no limit is applied.
- All (Show Descendants): Choose whether to display all the parent page's descendants.
truefalse(Default)
See: https://confluence.atlassian.com/doc/children-display-macro-139501.html
- Reverse (Reverse Sort): Use with the
-
template:
ac:iframeto include iframe macro (cloud only)- URL: URL to the iframe.
- Frameborder: Choose whether to draw a border around content in the iframe.
show(Default)hide
- Width: Width in px. Defaults to "640px"
- Height: Height in px. Defaults to "360px"
- Scrolling: Allow or prevent scrolling in the iframe to see additional content.
yesnoauto(Default)
- Align: Align the iframe to the left or right of the page.
left(Default)right
See: https://support.atlassian.com/confluence-cloud/docs/insert-the-iframe-macro
-
template:
ac:blog-poststo include blog-posts- Content: How much content will be shown
- titles (default)
- excerpts
- entire
- Time: Specify how much back in time Confluence should look for blog posts (default: unlimited)
- Label: Restrict to blog posts with specific labels
- Author: Restrict to blog posts by specific authors
- Spaces: Restrict to blog posts in specific spaces
- Max: Maximum number of blog posts shown (default: 15)
- Sort: Sorting posts by
- title
- creation (default)
- modified
- Reverse: Reverses the Sort parameter from oldest to newest (default: false)
See: https://confluence.atlassian.com/doc/blog-posts-macro-139470.html
- Content: How much content will be shown
-
template:
ac:includeto include a page- Page: the page to be included
- Space: the space the page is in (optional, otherwise same space)
-
template:
ac:excerpt-includeto include the excerpt from another page- Page: the page the excerpt should be included from
- Name: The specific identifier for the excerpt, allowing multiple Excerpt macros on one page to be referenced individually. If not provided, the first excerpt from the page will be used (optional, cloud only)
- NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
-
template:
ac:excerptto create an excerpt and include it in the page- Excerpt: The text you want to include
- Name: Allows you to identify this macro so that you can add multiple Excerpt macros to one page and use a specific one on another page using the Excerpt Include macro (optional, cloud only)
- OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
- Hidden: Hide the excerpt content (optional, default: false)
-
template:
ac:anchorto set an anchor inside a page- Anchor: Text for the anchor
-
template:
ac:expandto display an expandable/collapsible section of text on your page- Title: Defines the text next to the expand/collapse icon.
- Body: The Text that it is expanded to.
-
template:
ac:profileto display a short summary of a given Confluence user's profile.- Name: The username of the Confluence user whose profile summary you wish to show.
-
template:
ac:contentbylabelto display a list of pages, blog posts or attachments that have particular labels- CQL: The CQL query to discover the content
-
template:
ac:detailssummaryto show summary information from one page on a another page- Headings: Column headings to show
- FirstColumn: Name of the Title Column
- CQL: The CQL query to discover the pages
- SortBy: Sort by a specific column heading
-
template:
ac:detailsto create page properties- Body: Must contain a table with two rows, the table headings are used as property key. The table content is the value.
-
template:
ac:panelto display a block of text within a customisable panel- Title: Panel title (optional)
- Body: Body text of the panel
- BGColor: Background Color
- TitleBGColor: Background color of the title bar
- TitleColor: Text color of the title
- BorderStyle: Style of the panel's border
-
template
ac:recently-updatedto display a list of most recently changed content- Spaces: List of Spaces to watch (optional, default is current Space)
- ShowProfilePic: Show profile picture of editor
- Max: Maximum number of changes
- Types: Include these content types only (comments, blogposts, pages)
- Theme: Apperance of the macro (concise, social, sidebar)
- HideHeading: Determines whether the macro hides or displays the text 'Recently Updated' as a title above the list of content
- Labels: Filter the results by label. The macro will display only the pages etc which are tagged with the label(s) you specify here.
-
template:
ac:pagetreesearchto add a search box to your Confluence page.- Root: Name of the root page whose hierarchy of pages will be searched by this macro. If this not specified, the root page is the current page.
-
template:
ac:columnTo be used with the section macro to define the columns in a page.- Width: Width of the column
- Body: The content of the column
-
template:
ac:multimediato embedd an attached video, animation or other multimedia files in a Confluence page- Name: Name of the file
- Width: Width of the video (optional)
- AutoPlay: Start playing the file on page load (default: false)
-
template
ac:view-file- Name: Name of the file
- Height: height of the view
-
macro
@{...}to mention user by name specified in the braces.
Template & Macros Usecases
Insert Disclaimer
This should be in disclaimer.md.
**NOTE**: this document is generated, do not edit manually.
Add this to your article.md.
<!-- Space: TEST -->
<!-- Title: My Article -->
<!-- Include: disclaimer.md -->
This is my article.
Insert Status Badge
<!-- Space: TEST -->
<!-- Title: TODO List -->
<!-- Macro: :done:
Template: ac:status
Title: DONE
Color: Green -->
<!-- Macro: :todo:
Template: ac:status
Title: TODO
Color: Blue -->
* :done: Write Article
* :todo: Publish Article
Insert Colored Text Box
<!-- Space: TEST -->
<!-- Title: Announcement -->
<!-- Macro: :box:([^:]+):([^:]*):(.+):
Template: ac:box
Icon: true
Name: ${1}
Title: ${2}
Body: ${3} -->
:box:info::Foobar:
:box:tip:Tip of day:Foobar:
:box:note::Foobar:
:box:warning:Alert!:Foobar:
Insert Table of Contents
<!-- Include: ac:toc -->
If default TOC looks don't find a way to your heart, try parametrizing it, for example:
<!-- Macro: :toc:
Template: ac:toc
Printable: 'false'
MinLevel: 2 -->
# This is my nice title
:toc:
You can call the Macro as you like but the Template field must have the ac:toc value.
Also, note the single quotes around 'false'.
See Confluence TOC Macro for the list of parameters - keep in mind that here they start with capital letters. Every skipped field will have the default value, so feel free to include only the ones that you require.
Insert PageTree
# My First Heading
<!-- Include: ac:pagetree -->
The pagetree macro works almost the same as the TOC above, but the tree behavior is more desirable for creating placeholder pages above collections of SOPs.
The default pagetree macro behavior is to insert a tree rooted @self.
The following parameters can be used to alter your default configuration with parameters described more in depth here:Confluence Pagetree Macro.
Parameters:
- Title (of tree root page)
- Sort
- Excerpt
- Reverse
- SearchBox
- ExpandCollapseAll
- StartDepth
E.G.
<!-- Macro: :pagetree:
Template: ac:pagetree
Reverse: 'true'
ExpandCollapseAll: 'true'
StartDepth: 2 -->
# My First Heading
:pagetree:
Insert Children Display
To include Children Display (TOC displaying children pages) use following macro:
<!-- Macro: :children:
Template: ac:children
-->
# This is my nicer title
:children:
You can use various parameters to modify Children Display:
<!-- Macro: :children:
Template: ac:children
Sort: title
Style: h3
Excerpt: simple
First: 10
Page: Space:Page title
Depth: 2
Reverse: false
All: false -->
# This is my nicest title
:children:
Insert Jira Ticket
<!-- Space: TEST -->
<!-- Title: TODO List -->
<!-- Macro: MYJIRA-\d+
Template: ac:jira:ticket
Ticket: ${0} -->
See task MYJIRA-123.
Insert link to existing confluence page by title
This is a [link to an existing confluence page](ac:Pagetitle)
And this is how to link when the linktext is the same as the [Pagetitle](ac:)
Link to a [page title containing spaces](<ac:With Multiple Words>)
Upload and included inline images

will automatically upload the inlined image as an attachment and inline the image using the ac:image template.
If the file is not found, it will inline the image using the ac:image template and link to the image.
Add width for an image
Use the following macro:
<!-- Macro: \!\[.*\]\((.+)\)\<\!\-\- width=(.*) \-\-\>
Template: ac:image
Attachment: ${1}
Width: ${2} -->
And attach any image with the following
<!-- width=300 -->
The width will be the commented html after the image (in this case 300px).
Currently this is not compatible with the automated upload of inline images.
Render Mermaid Diagram
Confluence doesn't provide mermaid.js support natively. Mark provides a convenient way to enable the feature like GitHub does. As long as you have a code block marked as "mermaid", mark will automatically render it as a PNG image and attach it to the page as a rendered version of the code block.
graph TD;
A-->B;
Render D2 Diagram
Optionally you can enable D2 rendering via --features="d2".
This will transform the d2 diagram into a png that will be attached to Confluence, similar to how mermaid-go support works.
All you need is a codeblock marked as "d2".
X -> Y
MkDocs' Admonitions
Optionally you can enable mkdocs-style Admonitions via --features="mkdocsadmonitions".
When enabled, this renders note, warning, tip, info admonitions as Confluence alerts.
!!! note
Installation
Homebrew
brew tap kovetskiy/mark
brew install mark
Go Install
go install github.com/kovetskiy/mark/v16/cmd/mark@latest
Releases
Download a release from the Releases page
Docker
docker run --rm -i kovetskiy/mark:latest mark <params>
Compile and install using docker-compose
Mostly useful when you intend to enhance mark.
# Create the binary
$ docker-compose run markbuilder
# "install" the binary
$ cp mark /usr/local/bin
Usage
NAME:
mark - A tool for updating Atlassian Confluence pages from markdown.
USAGE:
mark [global options]
VERSION:
v16.x.x
DESCRIPTION:
Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark
GLOBAL OPTIONS:
--files string, -f string use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES]
--continue-on-error don't exit if an error occurs while processing a file, continue processing remaining files. [$MARK_CONTINUE_ON_ERROR]
--compile-only show resulting HTML and don't update Confluence page content. [$MARK_COMPILE_ONLY]
--dry-run resolve page and ancestry, show resulting HTML and exit. [$MARK_DRY_RUN]
--edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. [$MARK_EDIT_LOCK]
--drop-h1 don't include the first H1 heading in Confluence output. [$MARK_DROP_H1]
--strip-linebreaks, -L remove linebreaks inside of tags, to accommodate non-standard Confluence behavior [$MARK_STRIP_LINEBREAKS]
--title-from-h1 extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. Mutually exclusive with --title-from-filename. [$MARK_TITLE_FROM_H1]
--title-from-filename use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1. [$MARK_TITLE_FROM_FILENAME]
--title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title [$MARK_TITLE_APPEND_GENERATED_HASH]
--minor-edit don't send notifications while updating Confluence page. [$MARK_MINOR_EDIT]
--version-message string add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
--color string display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
--log-level string set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL]
--username string, -u string use specified username for updating Confluence page. [$MARK_USERNAME]
--password string, -p string use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication. [$MARK_PASSWORD]
--target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
--base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
--config string, -c string use the specified configuration file. (default: "${HOME}/.config/mark.toml") [$MARK_CONFIG]
--ci run on CI mode. It won't fail if files are not found. [$MARK_CI]
--space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
--parents string A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself. [$MARK_PARENTS]
--parents-delimiter string The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
--content-appearance string default content appearance for pages without a Content-Appearance header. Possible values: full-width, fixed. [$MARK_CONTENT_APPEARANCE]
--mermaid-scale float defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
--include-path string Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
--changes-only Avoids re-uploading pages that haven't changed since the last run. [$MARK_CHANGES_ONLY]
--preserve-comments Fetch and preserve inline comments on existing Confluence pages. [$MARK_PRESERVE_COMMENTS]
--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
You can store user credentials in the configuration file, which should be
located in a system specific directory (or specified via -c --config <path>) with the following format (TOML):
username = "your-email"
password = "password-or-api-key-for-confluence-cloud"
# If you are using Confluence Cloud add the /wiki suffix to base_url
base-url = "http://confluence.local"
title-from-h1 = true
drop-h1 = true
image-align = "center"
NOTE: Labels aren't supported when using minor-edit!
NOTE: See Preserving Inline Comments for a detailed description of the --preserve-comments flag.
NOTE: The system specific locations are described in here: https://pkg.go.dev/os#UserConfigDir. Currently, these are: On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib.
Tricks
Continuous Integration
It's quite trivial to integrate Mark into a CI/CD system, here is an example with Snake CI in case of self-hosted Bitbucket Server / Data Center.
stages:
- sync
Sync documentation:
stage: sync
only:
branches:
- main
image: kovetskiy/mark
commands:
- for file in $(find -type f -name '*.md'); do
echo "> Sync $file";
mark -u $MARK_USER -p $MARK_PASS -b $MARK_URL -f $file || exit 1;
echo;
done
In this example, I'm using the kovetskiy/mark image for creating a job container where the
repository with documentation will be cloned to. The following command finds all *.md files and runs mark against them one by one:
for file in $(find -type f -name '*.md'); do
echo "> Sync $file";
mark -u $MARK_USER -p $MARK_PASS -b $MARK_URL -f $file || exit 1;
echo;
done
The following directive tells the CI to run this particular job only if the changes are pushed into the
main branch. It means you can safely push your changes into feature branches without being afraid
that they have automatically shown in Confluence, then go through the reviewal process and automatically
deploy them when PR got merged.
only:
branches:
- main
File Globbing
Rather than running mark multiple times, or looping through a list of files from find, you can use file globbing (i.e. wildcard patterns) to match files in subdirectories. For example:
mark -f "helpful_cmds/*.md"
You can also use ** to get all files recursively.
mark -f "**/docs/*.md"
Linting markdown
We recommend to lint your markdown files with markdownlint-cli2 before publishing them to confluence to catch any conversion errors early.
Preserving Inline Comments
When collaborators leave inline comments on a Confluence page, updating the page via mark will normally erase those comments because the stored body is fully replaced. The --preserve-comments flag re-attaches inline comment markers to the new page body before uploading, so existing review threads survive updates.
mark --preserve-comments -f docs/page.md
Or via environment variable:
MARK_PRESERVE_COMMENTS=true mark -f docs/page.md
How it works:
- Before uploading,
markfetches the current page body and all inline comment markers from the Confluence API. - For each existing
<ac:inline-comment-marker>tag it records the content wrapped by that marker plus a short context window immediately before the opening tag and immediately after the closing tag in the old body (not around the raw selection text, so the context is stable even when the marker wraps additional inline markup such as<strong>). - It searches the new body for the same selected text and picks the occurrence whose surrounding context best matches the original (using Levenshtein distance), so the marker lands in the right place even if nearby text has shifted.
- The updated body—with all markers re-embedded—is then uploaded as normal.
Limitations:
- If the commented text was deleted from the document, the inline comment cannot be relocated and will be lost.
marklogs a warning in this case. - Overlapping selections (two comments anchored to the same stretch of text) are detected; the earlier overlapping match is dropped with a warning, and the later one (higher byte offset) is kept, rather than producing malformed markup.
--preserve-commentsis automatically skipped for newly created pages (there are no comments to preserve yet).- When combined with
--changes-only, the comment-preservation API calls are skipped entirely on runs where the page content has not changed, avoiding unnecessary round-trips.
Issues, Bugs & Contributions
I've started the project to solve my own problem and open sourced the solution so anyone who has a problem like me can solve it too. I have no profits/sponsors from these projects which means I don't really prioritize working on this project in my free time. I still check the issues and do code reviews for Pull Requests which means if you encounter a bug in the program, you should not expect me to fix it as soon as possible, but I'll be very glad to merge your own contributions into the project and release the new version.
I try to label all new issues, so it's easy to find a bug or a feature request to fix/implement, if you are willing to help with the project, you can use the following labels to find issues, just make sure to reply in the issue to let everyone know you took the issue:
Contributors ✨
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!