Skip to content

Commit c585c71

Browse files
jgmclaude
andcommitted
LaTeX reader: support LaTeX3 (xparse) document commands.
Support the features described in the LaTeX usrguide (https://www.latex-project.org/help/documentation/usrguide.pdf): - `\New/Renew/Provide/DeclareDocumentCommand` and the corresponding `...DocumentEnvironment` commands, with argument specifiers `m o O d D s t r R v e E b` and argument processors (`>{...}`: `\TrimSpaces`, `\ReverseBoolean`, `\SplitArgument`, `\SplitList`). Environment end code can refer to the environment's arguments. - `\ProcessList`, `\UseName`, and `\ExpandArgs` (c, n, N letters). - The conditionals `\IfNoValueTF`, `\IfValueTF`, `\IfBooleanTF`, `IfBlankTF` (and variants with just `T` or `F`). - `\New/Renew/DeclareCommandCopy` and `...EnvironmentCopy`. - Case changing: `\MakeUppercase`, `\MakeLowercase`, and `\MakeTitlecase`, `\NoCaseChange`, `\Declare{Upper,Lower,Title}caseExclusions`; `\CaseSwitch` and `\Declare*caseMapping`. - The expandable evaluators `\inteval` and `\fpeval` (a practical arithmetic subset); `\dimeval` and `\skipeval` substitute their expression unevaluated. - `\expandableinput`. - `\ExplSyntaxOn/Off` regions are tokenized with `:` and `_` as letters, so expl3 code can be skipped cleanly; `\ShowCommand` and the ltkeys commands (`\DeclareKeys`, `\SetKeys`, `\ProcessKeyOptions`, ...) are parsed and ignored. - Some constants, such as `c_ampersand_str`, are supported in expl3 regions. Adds new `ArgSpec` constructors to non-exported module Text.Pandoc.TeX. New tests in `test/command/latex3-commands.md` and `test/command/latex3-usrguide.md`. (The latter correspond to the examples in the user guide and were confirmed against LaTeX output when possible.) Closes #7540. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2d59398 commit c585c71

7 files changed

Lines changed: 1983 additions & 52 deletions

File tree

src/Text/Pandoc/Readers/LaTeX.hs

Lines changed: 138 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -407,22 +407,30 @@ inlineCommands = M.unions
407407
, ("sl", extractSpaces emph <$> inlines)
408408
, ("bf", extractSpaces strong <$> inlines)
409409
, ("tt", formatCode nullAttr <$> inlines)
410+
, ("ttfamily", extractSpaces (formatCode nullAttr) <$> inlines)
410411
, ("rm", inlines)
411412
, ("itshape", extractSpaces emph <$> inlines)
412413
, ("slshape", extractSpaces emph <$> inlines)
413414
, ("scshape", extractSpaces smallcaps <$> inlines)
414415
, ("bfseries", extractSpaces strong <$> inlines)
415-
, ("MakeUppercase", makeUppercase <$> tok)
416-
, ("MakeTextUppercase", makeUppercase <$> tok) -- textcase
417-
, ("uppercase", makeUppercase <$> tok)
418-
, ("MakeLowercase", makeLowercase <$> tok)
419-
, ("MakeTextLowercase", makeLowercase <$> tok)
420-
, ("lowercase", makeLowercase <$> tok)
416+
, ("MakeUppercase", caseTransform "upper" T.toUpper)
417+
, ("MakeTextUppercase", caseTransform "upper" T.toUpper) -- textcase
418+
, ("uppercase", caseTransform "upper" T.toUpper)
419+
, ("MakeLowercase", caseTransform "lower" T.toLower)
420+
, ("MakeTextLowercase", caseTransform "lower" T.toLower)
421+
, ("lowercase", caseTransform "lower" T.toLower)
422+
, ("MakeTitlecase", makeTitlecaseCommand)
423+
, ("NoCaseChange", spanWith ("",["nocasechange"],[]) <$> tok)
424+
, ("CaseSwitch", tok <* tok <* tok <* tok)
421425
, ("thanks", skipopts >> note <$> grouped block)
422426
, ("footnote", skipopts >> footnote)
423427
, ("footnotemark", footnotemark)
424428
, ("footnotetext", footnotetext)
425429
, ("newline", pure B.linebreak)
430+
-- xparse argument markers, in case they leak into the document:
431+
, ("NoValue", pure (B.str "-NoValue-"))
432+
, ("BooleanTrue", pure mempty)
433+
, ("BooleanFalse", pure mempty)
426434
, ("passthrough", fixPassthroughEscapes <$> tok)
427435
-- \passthrough macro used by latex writer
428436
-- for listings
@@ -457,6 +465,7 @@ inlineCommands = M.unions
457465
, ("iftoggle", try $ ifToggle >> inline)
458466
-- include
459467
, ("input", rawInlineOr "input" $ include "input")
468+
, ("expandableinput", rawInlineOr "expandableinput" $ include "input")
460469
-- soul package
461470
, ("st", extractSpaces strikeout <$> tok)
462471
, ("ul", underline <$> tok)
@@ -471,6 +480,19 @@ inlineCommands = M.unions
471480
-- this is used internally by pandoc but the definition is too complicated
472481
-- for pandoc to handle (see #11140):
473482
, ("pandocbounded", tok)
483+
-- LaTeX3 constants
484+
, ("c_ampersand_str", pure (str "&"))
485+
, ("c_atsign_str", pure (str "@"))
486+
, ("c_backslash_str", pure (str "\\"))
487+
, ("c_left_brace_str", pure (str "{"))
488+
, ("c_right_brace_str", pure (str "}"))
489+
, ("c_circumflex_str", pure (str "^"))
490+
, ("c_colon_str", pure (str ":"))
491+
, ("c_dollar_str", pure (str "$"))
492+
, ("c_hash_str", pure (str "#"))
493+
, ("c_percent_str", pure (str "%"))
494+
, ("c_tilde_str", pure (str "~"))
495+
, ("c_underscore_str", pure (str "_"))
474496
]
475497

476498
bracedFilename :: PandocMonad m => LP m Text
@@ -547,15 +569,97 @@ ifdim = do
547569
contents <- manyTill anyTok (controlSeq "fi")
548570
return $ rawInline "latex" $ "\\ifdim" <> untokenize contents <> "\\fi"
549571

550-
makeUppercase :: Inlines -> Inlines
551-
makeUppercase = fromList . walk (alterStr T.toUpper) . toList
552-
553-
makeLowercase :: Inlines -> Inlines
554-
makeLowercase = fromList . walk (alterStr T.toLower) . toList
572+
-- | Parse the argument of a case-changing command (\MakeUppercase,
573+
-- \MakeLowercase) and apply the case transformation, honoring
574+
-- exclusions declared with \Declare*caseExclusions.
575+
caseTransform :: PandocMonad m => Text -> (Text -> Text) -> LP m Inlines
576+
caseTransform kind f = do
577+
void $ option [] keyvals -- locale options, ignored
578+
excl <- M.findWithDefault Set.empty kind . sCaseExclusions <$> getState
579+
caseTransformWith excl f <$> tok
580+
581+
-- | Apply a case transformation to text, leaving math, code and
582+
-- citations untouched, unwrapping (and skipping) \NoCaseChange
583+
-- content, and skipping excluded words.
584+
caseTransformWith :: Set.Set Text -> (Text -> Text) -> Inlines -> Inlines
585+
caseTransformWith excl f = fromList . go . toList
586+
where
587+
go = concatMap goInline
588+
goInline (Span ("",["nocasechange"],[]) ils) = ils
589+
goInline (Str t)
590+
| t `Set.member` excl = [Str t]
591+
| otherwise = [Str (f t)]
592+
goInline (Emph ils) = [Emph (go ils)]
593+
goInline (Strong ils) = [Strong (go ils)]
594+
goInline (Underline ils) = [Underline (go ils)]
595+
goInline (Strikeout ils) = [Strikeout (go ils)]
596+
goInline (Superscript ils) = [Superscript (go ils)]
597+
goInline (Subscript ils) = [Subscript (go ils)]
598+
goInline (SmallCaps ils) = [SmallCaps (go ils)]
599+
goInline (Quoted qt ils) = [Quoted qt (go ils)]
600+
goInline (Span attr ils) = [Span attr (go ils)]
601+
goInline (Link attr ils target) = [Link attr (go ils) target]
602+
goInline x = [x] -- Math, Code, Cite, Space, etc.
603+
604+
-- | Parse the arguments of \MakeTitlecase, supporting the
605+
-- @words=all@ option and title-case exclusions.
606+
makeTitlecaseCommand :: PandocMonad m => LP m Inlines
607+
makeTitlecaseCommand = do
608+
options <- option [] keyvals
609+
excl <- M.findWithDefault Set.empty "title" . sCaseExclusions <$> getState
610+
(if lookup "words" options == Just "all"
611+
then makeTitlecaseAll excl
612+
else makeTitlecase) <$> tok
613+
614+
-- | Titlecase the first letter of each word (\MakeTitlecase with
615+
-- @words=all@), skipping excluded words.
616+
makeTitlecaseAll :: Set.Set Text -> Inlines -> Inlines
617+
makeTitlecaseAll excl = fromList . go . toList
618+
where
619+
go [] = []
620+
go xs =
621+
let (w, rest) = break isSep xs
622+
(seps, rest') = span isSep rest
623+
in tcWord w ++ seps ++ go rest'
624+
isSep Space = True
625+
isSep SoftBreak = True
626+
isSep LineBreak = True
627+
isSep _ = False
628+
tcWord w
629+
| stringify w `Set.member` excl = w
630+
| otherwise = toList (makeTitlecase (fromList w))
631+
632+
-- | Handle \DeclareUppercaseExclusions and friends: store a
633+
-- comma-separated list of words excluded from case changing.
634+
declareCaseExclusions :: PandocMonad m => Text -> LP m Blocks
635+
declareCaseExclusions kind = do
636+
ws <- map T.strip . T.splitOn "," . untokenize <$> braced
637+
updateState $ \st -> st{ sCaseExclusions =
638+
M.insertWith Set.union kind (Set.fromList ws) (sCaseExclusions st) }
639+
return mempty
555640

556-
alterStr :: (Text -> Text) -> Inline -> Inline
557-
alterStr f (Str xs) = Str (f xs)
558-
alterStr _ x = x
641+
-- | Uppercase the first character of the first string (LaTeX3
642+
-- \MakeTitlecase, which title-cases only the first word by default).
643+
makeTitlecase :: Inlines -> Inlines
644+
makeTitlecase = fromList . snd . go . toList
645+
where
646+
go :: [Inline] -> (Bool, [Inline])
647+
go (x : xs) =
648+
case goInline x of
649+
(True, x') -> (True, x' : xs)
650+
(False, x') -> (x' :) <$> go xs
651+
go [] = (False, [])
652+
goInline (Str t) | not (T.null t) =
653+
(True, Str (T.toTitle (T.take 1 t) <> T.drop 1 t))
654+
goInline (Emph ils) = Emph <$> go ils
655+
goInline (Strong ils) = Strong <$> go ils
656+
goInline (Underline ils) = Underline <$> go ils
657+
goInline (SmallCaps ils) = SmallCaps <$> go ils
658+
goInline (Span attr ils) = Span attr <$> go ils
659+
goInline (Link attr ils target) =
660+
(\ils' -> Link attr ils' target) <$> go ils
661+
goInline (Quoted qt ils) = Quoted qt <$> go ils
662+
goInline x = (False, x)
559663

560664
fixPassthroughEscapes :: Inlines -> Inlines
561665
fixPassthroughEscapes = walk go
@@ -1062,10 +1166,30 @@ blockCommands = M.fromList
10621166
-- include
10631167
, ("include", rawBlockOr "include" $ include "include")
10641168
, ("input", rawBlockOr "input" $ include "input")
1169+
, ("expandableinput", rawBlockOr "expandableinput" $ include "input")
10651170
, ("subfile", rawBlockOr "subfile" doSubfile)
10661171
, ("usepackage", rawBlockOr "usepackage" usepackage)
10671172
-- preamble
10681173
, ("PackageError", mempty <$ (braced >> braced >> braced))
1174+
-- LaTeX3 conveniences, parsed and ignored:
1175+
, ("ExplSyntaxOn", pure mempty)
1176+
, ("ExplSyntaxOff", pure mempty)
1177+
, ("ShowCommand", mempty <$ withVerbatimMode (spaces *> anyControlSeq))
1178+
, ("ShowEnvironment", mempty <$ braced)
1179+
, ("DeclareKeys", mempty <$ (skipopts *> braced))
1180+
, ("DeclareUnknownKeyHandler", mempty <$ (skipopts *> braced))
1181+
, ("ProcessKeyOptions", mempty <$ skipopts)
1182+
, ("SetKeys", mempty <$ (skipopts *> braced))
1183+
-- LaTeX3 case changing
1184+
, ("DeclareUppercaseExclusions", declareCaseExclusions "upper")
1185+
, ("DeclareLowercaseExclusions", declareCaseExclusions "lower")
1186+
, ("DeclareTitlecaseExclusions", declareCaseExclusions "title")
1187+
, ("AddToNoCaseChangeList", mempty <$ braced)
1188+
, ("DeclareCaseChangeEquivalent", mempty <$
1189+
(withVerbatimMode (spaces *> anyControlSeq) *> braced))
1190+
, ("DeclareUppercaseMapping", mempty <$ (skipopts *> braced *> braced))
1191+
, ("DeclareLowercaseMapping", mempty <$ (skipopts *> braced *> braced))
1192+
, ("DeclareTitlecaseMapping", mempty <$ (skipopts *> braced *> braced))
10691193
-- epigraph package
10701194
, ("epigraph", epigraph)
10711195
-- alignment

src/Text/Pandoc/Readers/LaTeX/Inline.hs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ import Text.Pandoc.Translations (translateTerm)
3636
import Text.Pandoc.Readers.LaTeX.Parsing
3737
import Text.Pandoc.Extensions (extensionEnabled, Extension(..))
3838
import Text.Pandoc.Parsing (getOption, updateState, getState, notFollowedBy,
39-
manyTill, getInput, setInput, incSourceColumn,
40-
option, many1)
39+
manyTill, option, many1)
4140
import Data.Char (isDigit)
4241
import Text.Pandoc.Highlighting (fromListingsLanguage,)
4342
import Data.Maybe (maybeToList, fromMaybe)
@@ -89,19 +88,6 @@ doverb = do
8988
code . untokenize <$>
9089
manyTill (notFollowedBy newlineTok >> verbTok marker) (symbol marker)
9190

92-
verbTok :: PandocMonad m => Char -> LP m Tok
93-
verbTok stopchar = do
94-
t@(Tok pos toktype txt) <- anyTok
95-
case T.findIndex (== stopchar) txt of
96-
Nothing -> return t
97-
Just i -> do
98-
let (t1, t2) = T.splitAt i txt
99-
TokStream macrosExpanded inp <- getInput
100-
setInput $ TokStream macrosExpanded
101-
$ Tok (incSourceColumn pos i) Symbol (T.singleton stopchar)
102-
: tokenize (incSourceColumn pos (i + 1)) (T.drop 1 t2) ++ inp
103-
return $ Tok pos toktype t1
104-
10591
listingsLanguage :: [(Text, Text)] -> Maybe Text
10692
listingsLanguage opts =
10793
case lookup "language" opts of

0 commit comments

Comments
 (0)