From fa92e72c35730c700e322e390ff705cdf33c429d Mon Sep 17 00:00:00 2001 From: John MacFarlane Date: Sun, 20 Sep 2026 05:31:30 +0000 Subject: [PATCH] Org: make code line comma-escaping match Emacs' behavior. Org's escaping rule for code in src/example blocks adds a comma to lines matching `^[ \t]*,*(\*|#\+)`, i.e. lines already starting with commas before `*` or `#+` get an additional comma; unescaping removes one comma from such lines. The writer previously left a literal `,#+foo` line unescaped, and the reader then stripped its comma when reading the result back, corrupting the code on round trips. Writer and reader now both handle runs of commas, matching org-escape-code-in-region and org-unescape-code-in-region. Co-Authored-By: Claude --- src/Text/Pandoc/Readers/Org/Blocks.hs | 10 +++++++--- src/Text/Pandoc/Writers/Org.hs | 15 ++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Text/Pandoc/Readers/Org/Blocks.hs b/src/Text/Pandoc/Readers/Org/Blocks.hs index 8609f8226..c2ff33b05 100644 --- a/src/Text/Pandoc/Readers/Org/Blocks.hs +++ b/src/Text/Pandoc/Readers/Org/Blocks.hs @@ -274,11 +274,15 @@ rawBlockContent' blockEnder = try $ do rawLine :: Monad m => OrgParser m Text rawLine = try $ ("" <$ blankline) <|> anyLine + -- Remove one comma from lines consisting of any number of commas + -- followed by "*" or "#+", like Emacs' org-unescape-code-in-region. commaEscaped suff = case T.uncons suff of Just (',', cs) - | "*" <- T.take 1 cs -> cs - | "#+" <- T.take 2 cs -> cs - _ -> suff + | isEscaped cs -> cs + _ -> suff + where + isEscaped cs = let cs' = T.dropWhile (== ',') cs + in "*" `T.isPrefixOf` cs' || "#+" `T.isPrefixOf` cs' -- | Read but ignore all remaining block headers. ignHeaders :: Monad m => OrgParser m () diff --git a/src/Text/Pandoc/Writers/Org.hs b/src/Text/Pandoc/Writers/Org.hs index 2676fadc8..a26d7be16 100644 --- a/src/Text/Pandoc/Writers/Org.hs +++ b/src/Text/Pandoc/Writers/Org.hs @@ -213,13 +213,18 @@ blockToOrg (CodeBlock (ident,classes,kvs) str) = do let (beg, end) = case lang of Nothing -> ("#+begin_example" <> numberlines, "#+end_example") Just x -> ("#+begin_src " <> x <> numberlines <> args, "#+end_src") - -- escape special lines + -- Escape special lines by prepending a comma. Like Emacs' + -- org-escape-code-in-region, escape all lines consisting of + -- indentation, then any number of commas, then "*" or "#+"; this + -- keeps lines that already start with commas intact when the block + -- is unescaped again. + let needsEscape t = let t' = T.dropWhile (== ',') t + in T.isPrefixOf "#+" t' || T.isPrefixOf "*" t' let escape_line line = let (spaces, code) = T.span (\c -> c == ' ' || c == '\t') line - in spaces <> - (if T.isPrefixOf "#+" code || T.isPrefixOf "*" code - then T.cons ',' code - else code) + in if needsEscape code + then spaces <> T.cons ',' code + else line let escaped = T.unlines . map escape_line . T.lines $ str return $ name $$ literal beg $$ literal escaped $$ literal end $$ blankline blockToOrg (BlockQuote blocks) = do