From f3b583e67ec18345c2b5c988f452388894363d1e Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 25 May 2026 07:54:34 -0500 Subject: feat: render inline code spans verbatim in the Org buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fetched description with markdown inside backticks — `**bold**`, `[text](url)`, `_italic_` — got its span contents converted along with the surrounding prose, so the Org buffer showed `*bold*` and `[[url][text]]` inside what should read as literal code. Markdown treats a code span as opaque; the converter didn't. Both converters now tokenize code spans first and apply link/bold/italic conversion only to the text between them, keeping span contents literal. The md->org and org->md directions each split into an emphasis-only helper (pearl--md-emphasis-to-org, pearl--org-emphasis-to-md) called on the gaps, with the span passed through untouched. Keeping the split symmetric is what preserves the round-trip identity a fetch + no-edit push depends on. A new test asserts the verbatim rendering for bold, link, and italic inside a span, plus emphasis still converting outside one and between two spans. --- pearl.el | 91 +++++++++++++++++++++++++++++---------------- tests/test-pearl-convert.el | 20 ++++++++-- 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/pearl.el b/pearl.el index 844b9f0..4111557 100644 --- a/pearl.el +++ b/pearl.el @@ -1674,23 +1674,37 @@ PRIORITY-NUM is 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low." ((eq priority-num 4) "Low") (t "Medium"))) +(defun pearl--md-emphasis-to-org (s) + "Convert markdown links / bold / italic in S to Org markup. +S must contain no inline code spans -- those are handled by the caller, which +splits them out so their contents stay verbatim." + ;; [text](url) -> [[url][text]] + (setq s (replace-regexp-in-string + "\\[\\([^]]+\\)\\](\\([^) ]+\\))" "[[\\2][\\1]]" s)) + ;; **bold** -> *bold* + (setq s (replace-regexp-in-string "\\*\\*\\([^*\n]+?\\)\\*\\*" "*\\1*" s)) + ;; _italic_ -> /italic/, word-bounded so identifiers like foo_bar are left alone + (setq s (replace-regexp-in-string + "\\(^\\|[^[:alnum:]_]\\)_\\([^_\n]+?\\)_\\([^[:alnum:]_]\\|$\\)" + "\\1/\\2/\\3" s)) + s) + (defun pearl--md-line-to-org (line) "Convert inline markdown in LINE to Org markup. Handles links, inline code, bold, and underscore italics; other inline markup -passes through unchanged." - (let ((s line)) - ;; [text](url) -> [[url][text]] (before code/emphasis touch the brackets) - (setq s (replace-regexp-in-string - "\\[\\([^]]+\\)\\](\\([^) ]+\\))" "[[\\2][\\1]]" s)) - ;; `code` -> ~code~ - (setq s (replace-regexp-in-string "`\\([^`\n]+\\)`" "~\\1~" s)) - ;; **bold** -> *bold* - (setq s (replace-regexp-in-string "\\*\\*\\([^*\n]+?\\)\\*\\*" "*\\1*" s)) - ;; _italic_ -> /italic/, word-bounded so identifiers like foo_bar are left alone - (setq s (replace-regexp-in-string - "\\(^\\|[^[:alnum:]_]\\)_\\([^_\n]+?\\)_\\([^[:alnum:]_]\\|$\\)" - "\\1/\\2/\\3" s)) - s)) +passes through unchanged. Inline code spans are tokenized first and their +contents kept verbatim, so markdown-looking text in backticks (`**b**`, +`[l](u)`) renders as code rather than being converted." + (let ((result "") + (pos 0)) + (while (string-match "`\\([^`\n]+\\)`" line pos) + ;; emphasis/links on the text before this code span, then the span verbatim + (setq result (concat result + (pearl--md-emphasis-to-org + (substring line pos (match-beginning 0))) + "~" (match-string 1 line) "~")) + (setq pos (match-end 0))) + (concat result (pearl--md-emphasis-to-org (substring line pos))))) (defun pearl--md-to-org (md) "Convert markdown MD to Org markup (the pure-elisp conversion tier). @@ -1726,26 +1740,39 @@ MD." (t (push (pearl--md-line-to-org line) out)))) (string-join (nreverse out) "\n")))) +(defun pearl--org-emphasis-to-md (s) + "Convert Org links / bold / italic in S back to markdown. +S must contain no verbatim ~code~ spans -- the caller splits those out so their +contents stay literal. Italics are word-bounded so paths and URLs are left +alone." + ;; [[url][text]] -> [text](url) + (setq s (replace-regexp-in-string + "\\[\\[\\([^]]+\\)\\]\\[\\([^]]+\\)\\]\\]" "[\\2](\\1)" s)) + ;; [[url]] -> url + (setq s (replace-regexp-in-string "\\[\\[\\([^]]+\\)\\]\\]" "\\1" s)) + ;; *bold* -> **bold** + (setq s (replace-regexp-in-string "\\*\\([^*\n]+?\\)\\*" "**\\1**" s)) + ;; /italic/ -> _italic_, word-bounded so /usr/local paths are left alone + (setq s (replace-regexp-in-string + "\\(^\\|[^[:alnum:]/]\\)/\\([^/\n]+?\\)/\\([^[:alnum:]/]\\|$\\)" + "\\1_\\2_\\3" s)) + s) + (defun pearl--org-line-to-md (line) "Convert inline Org markup in LINE back to markdown. -The inverse of `pearl--md-line-to-org': org links, verbatim, bold, and -italics become their markdown forms. Other text passes through unchanged. -Italics are word-bounded so filesystem paths and URLs are left alone." - (let ((s line)) - ;; [[url][text]] -> [text](url) (before verbatim/emphasis touch brackets) - (setq s (replace-regexp-in-string - "\\[\\[\\([^]]+\\)\\]\\[\\([^]]+\\)\\]\\]" "[\\2](\\1)" s)) - ;; [[url]] -> url - (setq s (replace-regexp-in-string "\\[\\[\\([^]]+\\)\\]\\]" "\\1" s)) - ;; ~code~ -> `code` - (setq s (replace-regexp-in-string "~\\([^~\n]+\\)~" "`\\1`" s)) - ;; *bold* -> **bold** - (setq s (replace-regexp-in-string "\\*\\([^*\n]+?\\)\\*" "**\\1**" s)) - ;; /italic/ -> _italic_, word-bounded so /usr/local paths are left alone - (setq s (replace-regexp-in-string - "\\(^\\|[^[:alnum:]/]\\)/\\([^/\n]+?\\)/\\([^[:alnum:]/]\\|$\\)" - "\\1_\\2_\\3" s)) - s)) +The inverse of `pearl--md-line-to-org': org links, verbatim, bold, and italics +become their markdown forms. Verbatim ~code~ spans are tokenized first and +their contents kept literal (symmetric with the md->org direction), so a code +span containing markup round-trips exactly." + (let ((result "") + (pos 0)) + (while (string-match "~\\([^~\n]+\\)~" line pos) + (setq result (concat result + (pearl--org-emphasis-to-md + (substring line pos (match-beginning 0))) + "`" (match-string 1 line) "`")) + (setq pos (match-end 0))) + (concat result (pearl--org-emphasis-to-md (substring line pos))))) (defun pearl--org-to-md (org) "Convert Org markup ORG back to markdown (the push direction). diff --git a/tests/test-pearl-convert.el b/tests/test-pearl-convert.el index 1337439..2e278de 100644 --- a/tests/test-pearl-convert.el +++ b/tests/test-pearl-convert.el @@ -170,10 +170,10 @@ the conversion-tier docstring) and are excluded here." (ert-deftest test-pearl-convert-roundtrip-boundary-cases () "Round-trip identity holds across markdown boundary cases — the property that -keeps a fetch + no-edit push from changing the remote. Two of these round-trip -safely despite an imperfect intermediate Org form: a URL containing parens, and -inline code whose contents look like markdown (rendered converted in the Org -view, but reversed exactly on push)." +keeps a fetch + no-edit push from changing the remote. A URL containing parens +round-trips despite an imperfect intermediate Org form; inline code whose +contents look like markdown round-trips because both converters keep code-span +contents verbatim rather than translating inside them." (dolist (md '("[a](u1) and [b](u2)" ; multiple links on a line "see [docs](http://x.com/(p))" ; parens inside the URL "`**b** and [l](u) and _i_`" ; markdown-looking inline code @@ -182,5 +182,17 @@ view, but reversed exactly on push)." "text\n```python\nx = 1\n```\nafter")) ; fence with trailing text (should (string= md (pearl--org-to-md (pearl--md-to-org md)))))) +(ert-deftest test-pearl-md-line-code-span-verbatim () + "Markdown inside an inline code span renders verbatim, not converted." + ;; bold / link / italic inside backticks stay literal in the Org code span + (should (string= "~**b**~" (pearl--md-line-to-org "`**b**`"))) + (should (string= "~[l](u)~" (pearl--md-line-to-org "`[l](u)`"))) + (should (string= "~_i_~" (pearl--md-line-to-org "`_i_`"))) + ;; emphasis outside the code span still converts + (should (string= "*x* ~**y**~" (pearl--md-line-to-org "**x** `**y**`"))) + ;; convertible text between two code spans is still converted + (should (string= "~**a**~ and [[u][l]]" + (pearl--md-line-to-org "`**a**` and [l](u)")))) + (provide 'test-pearl-convert) ;;; test-pearl-convert.el ends here -- cgit v1.2.3