aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 07:54:34 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 07:54:34 -0500
commitf3b583e67ec18345c2b5c988f452388894363d1e (patch)
treed5e95df54cf379b275f422386fcc92036b338734 /pearl.el
parentbd2aa5705d94b7d403096c62637e64c479ea0de5 (diff)
downloadpearl-f3b583e67ec18345c2b5c988f452388894363d1e.tar.gz
pearl-f3b583e67ec18345c2b5c988f452388894363d1e.zip
feat: render inline code spans verbatim in the Org buffer
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.
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el91
1 files changed, 59 insertions, 32 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).