Bugs identified by exporting flow_ip_release.org and comparing output
      against expected Confluence wiki markup, then verifying rendering via
      the Confluence REST API.

      Verbatim/code spans ({{...}}):
      - Replace brace-escaping with new org-confluence--monospace helper that
        correctly escapes: leading "--" → \-\- (prevents strikethrough 
mis-parse),
        * → \* (prevents bold mis-parse; even a lone trailing * closes the span
        early without escaping), [ → \[ and ] → \] (prevents wiki-link 
mis-parse)

      Anchor targets and internal links:
      - Add org-confluence-target: emits {anchor:NAME} macro for <<target>>
      - Add org-confluence-radio-target: emits {anchor:NAME}contents
      - Add org-confluence--sanitize-anchor: replaces ":" with " -" to prevent
        the wiki link parser treating "prefix:rest" as "spaceKey:pageTitle"
      - Add fuzzy link branch in org-confluence-link emitting [desc|#anchor]
      - Register target, radio-target, verbatim in translate-alist

      Inline markup:
      - bold, italic, strike-through, underline: collapse newlines to spaces
        (Confluence inline markup cannot span lines)

      List items:
      - org-confluence-item: fix nil contents crash; collapse soft-wrapped
        continuation lines to a single line (Confluence list items must be
        one line); use Confluence emoticons for checkboxes: (/) checked,
        (x) unchecked, (!) partial

      Links:
      - org-confluence-link: collapse newlines in multi-line descriptions

      Headings:
      - org-confluence-headline: insert blank line between heading and body

      Template:
      - Emit {toc:maxLevel=N} when #+OPTIONS: toc:N specifies a depth
      - Collapse runs of 3+ blank lines to a single blank line
      - Do not emit #+TITLE or #+DATE (redundant when page title is set
        on the Confluence page object via the API)
---
 lisp/ox-confluence.el | 116 ++++++++++++++++++++++++++++++++++--------
 1 file changed, 94 insertions(+), 22 deletions(-)

diff --git a/lisp/ox-confluence.el b/lisp/ox-confluence.el
index cf712eb..d5d6637 100644
--- a/lisp/ox-confluence.el
+++ b/lisp/ox-confluence.el
@@ -60,6 +60,8 @@
                     (template . org-confluence-template)
                     (timestamp . org-confluence-timestamp)
                     (underline . org-confluence-underline)
+                    (radio-target . org-confluence-radio-target)
+                    (target . org-confluence-target)
                     (verbatim . org-confluence-verbatim))
   :menu-entry
   '(?f "Export to Confluence"
@@ -78,7 +80,8 @@
 
 ;; All the functions we use
 (defun org-confluence-bold (_ contents _)
-  (format "*%s*" contents))
+  ;; Collapse newlines — Confluence bold cannot span lines.
+  (format "*%s*" (replace-regexp-in-string "\n" " " (org-trim contents))))
 
 (defun org-confluence-empty (_ _ _)
   "")
@@ -89,23 +92,28 @@
     (org-confluence--block "none" "Confluence" content)))
 
 (defun org-confluence-italic (_ contents _)
-  (format "_%s_" contents))
+  (format "_%s_" (replace-regexp-in-string "\n" " " (org-trim contents))))
 
 (defun org-confluence-item (item contents info)
-  (let ((list-type (org-element-property :type (org-export-get-parent item))))
+  (let* ((list-type (org-element-property :type (org-export-get-parent item)))
+         (checkbox (org-element-property :checkbox item))
+         (checkbox-str (pcase checkbox
+                         (`on    "(/) ")
+                         (`off   "(x) ")
+                         (`trans "(!) "))))
     (concat
      (make-string (1+ (org-confluence--li-depth item))
                   (if (eq list-type 'ordered) ?\# ?\-))
      " "
-     (pcase (org-element-property :checkbox item)
-       (`on "*{{(X)}}* ")
-       (`off "*{{( )}}* ")
-       (`trans "*{{(\\-)}}* "))
+     checkbox-str
      (when (eq list-type 'descriptive)
        (concat "*"
               (org-export-data (org-element-property :tag item) info)
               "* - "))
-     (org-trim contents))))
+     ;; Confluence list items must be a single line; collapse any
+     ;; wrapped continuation text and guard against nil contents.
+     (replace-regexp-in-string "[ \t]*\n[ \t]*" " "
+                                (org-trim (or contents ""))))))
 
 (defun org-confluence-fixed-width (fixed-width _ _)
   (org-confluence--block
@@ -113,13 +121,33 @@
    "Confluence"
    (org-trim (org-element-property :value fixed-width))))
 
+(defun org-confluence--monospace (content)
+  "Wrap CONTENT in Confluence {{...}} monospace markup.
+Escapes characters that the Confluence wiki parser mis-interprets
+even inside {{...}} spans:
+  leading \"--\"  → \\-\\-  (prevents strikethrough-start mis-parse)
+  *               → \\*     (prevents bold mis-parse; even a lone trailing *
+                             causes Confluence to close monospace early)
+  [               → \\[     (prevents wiki-link-start mis-parse)
+  ]               → \\]     (prevents wiki-link-end mis-parse)"
+  (let* ((s (if (string-prefix-p "--" content)
+                (concat "\\-\\-" (substring content 2))
+              content))
+         ;; Escape * unconditionally: even a lone trailing * (e.g. 
--allow-new-*)
+         ;; causes the Confluence parser to close the monospace span early and
+         ;; start a bold region, producing broken output like
+         ;;   <code>--allow-new-</code><strong> flags are *off</strong>
+         ;; The \* keeps the * inside the monospace where it belongs.
+         (s (replace-regexp-in-string "\\*" "\\\\*" s))
+         (s (replace-regexp-in-string "\\[" "\\\\[" s))
+         (s (replace-regexp-in-string "\\]" "\\\\]" s)))
+    (format "\{\{%s\}\}" s)))
+
 (defun org-confluence-verbatim (verbatim _ _)
-  (let ((content (org-element-property :value verbatim)))
-    (format "\{\{%s\}\}" (string-replace "{" "\\{" content))))
+  (org-confluence--monospace (org-element-property :value verbatim)))
 
 (defun org-confluence-code (code _ _)
-  (let ((content (org-element-property :value code)))
-    (format "\{\{%s\}\}" (string-replace "{" "\\{" content))))
+  (org-confluence--monospace (org-element-property :value code)))
 
 (defun org-confluence-headline (headline contents info)
   (let* (;; (low-level-rank (org-export-low-level-p headline info))
@@ -132,22 +160,58 @@
                            (string= todo ""))
                        ""
                      (format "*{{%s}}* " todo))))
-    (format "h%s. %s%s\n%s" level todo-text text
-            (if (org-string-nw-p contents) contents ""))))
+    (format "h%s. %s%s\n%s"
+            level
+            todo-text
+            text
+            ;; Blank line between heading and body improves readability.
+            (if (org-string-nw-p contents) (concat "\n" contents) ""))))
 
 (defun org-confluence-link (link desc _)
   (if (string= "radio" (org-element-property :type link))
       desc
-    (let ((raw-link (org-element-property :raw-link link)))
+    (let* ((type (org-element-property :type link))
+           (raw-link (org-element-property :raw-link link))
+           ;; Normalise multi-line descriptions to a single line.
+           (desc (when desc
+                   (replace-regexp-in-string "\n" " " (org-trim desc)))))
       (concat "["
-              (when (org-string-nw-p desc) (format "%s|" desc))
               (cond
+               ;; Fuzzy links are internal org targets; emit [text|#anchor].
+               ;; Sanitize the anchor name to match org-confluence-target.
+               ((string= type "fuzzy")
+                (let ((anchor (org-confluence--sanitize-anchor raw-link)))
+                  (if (org-string-nw-p desc)
+                      (format "%s|#%s" desc anchor)
+                    (format "%s|#%s" raw-link anchor))))
+               ;; Explicit confluence: scheme — strip the prefix.
                ((string-match "^confluence:" raw-link)
-               (replace-regexp-in-string "^confluence:" "" raw-link))
+                (concat (when (org-string-nw-p desc) (format "%s|" desc))
+                        (replace-regexp-in-string "^confluence:" "" raw-link)))
+               ;; External URL or any other link type.
                (t
-               raw-link))
+                (concat (when (org-string-nw-p desc) (format "%s|" desc))
+                        raw-link)))
               "]"))))
 
+(defun org-confluence--sanitize-anchor (name)
+  "Sanitize NAME for use in Confluence anchor macros and wiki links.
+Colons are replaced with \" -\" because the wiki link parser treats
+\"prefix:rest\" as \"spaceKey:pageTitle\", breaking in-page anchor links."
+  (replace-regexp-in-string ":" " -" name))
+
+(defun org-confluence-target (target _ _)
+  "Transcode a TARGET element to a Confluence {anchor:} macro."
+  (let ((name (org-confluence--sanitize-anchor
+               (org-element-property :value target))))
+    (format "\{anchor:%s\}" name)))
+
+(defun org-confluence-radio-target (radio-target contents _)
+  "Transcode a RADIO-TARGET element to a Confluence {anchor:} macro."
+  (let ((name (org-confluence--sanitize-anchor
+               (org-element-property :value radio-target))))
+    (format "\{anchor:%s\}%s" name contents)))
+
 (defun org-confluence-paragraph (_ contents _)
   "Transcode PARAGRAPH element for Confluence.
 CONTENTS is the paragraph contents.  INFO is a plist used as
@@ -172,7 +236,7 @@ a communication channel."
     (org-confluence--block language "Emacs" content)))
 
 (defun org-confluence-strike-through (_ contents _)
-  (format "-%s-" contents))
+  (format "-%s-" (replace-regexp-in-string "\n" " " (org-trim contents))))
 
 (defun org-confluence-table (_ contents _)
   contents)
@@ -191,8 +255,16 @@ a communication channel."
            "|")))
 
 (defun org-confluence-template (contents info)
-  (let ((depth (plist-get info :with-toc)))
-    (concat (when depth "\{toc\}\n\n") contents)))
+  (let* ((depth (plist-get info :with-toc))
+         ;; Collapse runs of 3+ newlines from ascii-backend inter-section
+         ;; spacing down to a single blank line.
+         (body  (replace-regexp-in-string "\n\n\n+" "\n\n" contents)))
+    (concat
+     (when depth
+       (if (wholenump depth)
+           (format "\{toc:maxLevel=%d\}\n\n" depth)
+         "\{toc\}\n\n"))
+     body)))
 
 (defun org-confluence-timestamp (timestamp _contents _info)
   "Transcode a TIMESTAMP object from Org to Confluence.
@@ -203,7 +275,7 @@ CONTENTS and INFO are ignored."
       translated)))
 
 (defun org-confluence-underline (_ contents _)
-  (format "+%s+" contents))
+  (format "+%s+" (replace-regexp-in-string "\n" " " (org-trim contents))))
 
 (defun org-confluence--block (language theme contents)
   (concat "\{code:theme=" theme
-- 
2.30.1


Reply via email to