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 <noreply@anthropic.com>
This commit is contained in:
John MacFarlane
2026-09-20 10:28:30 -07:00
co-authored by Claude
parent fa0ec573e1
commit fa92e72c35
2 changed files with 17 additions and 8 deletions
+7 -3
View File
@@ -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 ()
+10 -5
View File
@@ -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