mirror of
https://github.com/jgm/pandoc.git
synced 2026-08-28 17:20:47 +08:00
RTF reader: support legacy \pn/\pntext paragraph numbering (#11686).
Word-95-style numbered and bulleted lists are encoded with a
`{\pntext ...}` auto-number destination at the start of each list
paragraph plus a `{\*\pn ...}` destination describing the numbering,
rather than the modern `\listtext`/`\listtable` mechanism. Two problems:
1. The `\pntext` marker text ("1.", "·", etc.) was captured as the
paragraph's first text run, which sits before the paragraph's
`\ls`/`\ilvl`, so emitBlocks (which reads list properties from the first
run) misclassified the paragraph as an ordinary paragraph. The first
item of each list therefore came out as a stray paragraph.
2. The numbering style was ignored, so numbered lists defaulted to
bullets.
Treat `\pntext` like `\listtext`: drop its visible marker text and flag the
start of a new list item. Parse the `{\*\pn ...}` destination
(`\pnlvlbody`/`\pnlvlblt`, `\pndec`, `\pnucltr`, `\pnlcltr`,
`\pnucrm`, `\pnlcrm`, `\pnstart`, and the `\ls`/`\ilvl` keys it
carries) into the list override table so numbered lists are
emitted as ordered lists with the right number style.
`\pn` is a paragraph property that remains in effect until reset
by `\pard`, auto-numbering every paragraph in scope. Track
this (`sPnActive`) so each paragraph becomes its own list item,
rather than merging markerless continuation paragraphs as is done
for modern `\listtext` lists.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,10 @@ data RTFState = RTFState { sOptions :: ReaderOptions
|
|||||||
, sListText :: Bool -- True after a \listtext group,
|
, sListText :: Bool -- True after a \listtext group,
|
||||||
-- marking the start of a new
|
-- marking the start of a new
|
||||||
-- list item
|
-- list item
|
||||||
|
, sPnActive :: Bool -- True while legacy \pn
|
||||||
|
-- paragraph numbering is in
|
||||||
|
-- effect; every paragraph in
|
||||||
|
-- scope is its own list item
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance Default RTFState where
|
instance Default RTFState where
|
||||||
@@ -96,6 +100,7 @@ instance Default RTFState where
|
|||||||
, sListOverrideTable = mempty
|
, sListOverrideTable = mempty
|
||||||
, sEatChars = 0
|
, sEatChars = 0
|
||||||
, sListText = False
|
, sListText = False
|
||||||
|
, sPnActive = False
|
||||||
}
|
}
|
||||||
|
|
||||||
type FontTable = IntMap.IntMap FontFamily
|
type FontTable = IntMap.IntMap FontFamily
|
||||||
@@ -456,6 +461,7 @@ processTok bs (Tok pos tok') = do
|
|||||||
case firsttok of
|
case firsttok of
|
||||||
Tok _ (ControlWord "shppict" _) -> inGroup (foldM processTok bs toks)
|
Tok _ (ControlWord "shppict" _) -> inGroup (foldM processTok bs toks)
|
||||||
Tok _ (ControlWord "shpinst" _) -> inGroup (foldM processTok bs toks)
|
Tok _ (ControlWord "shpinst" _) -> inGroup (foldM processTok bs toks)
|
||||||
|
Tok _ (ControlWord "pn" _) -> bs <$ handlePn toks
|
||||||
_ -> bs <$ (do oldTextContent <- sTextContent <$> getState
|
_ -> bs <$ (do oldTextContent <- sTextContent <$> getState
|
||||||
processTok mempty (Tok pos (Grouped toks))
|
processTok mempty (Tok pos (Grouped toks))
|
||||||
updateState $ \st -> st{ sTextContent = oldTextContent })
|
updateState $ \st -> st{ sTextContent = oldTextContent })
|
||||||
@@ -478,13 +484,22 @@ processTok bs (Tok pos tok') = do
|
|||||||
bs <$ inGroup (handlePict toks)
|
bs <$ inGroup (handlePict toks)
|
||||||
Grouped (Tok _ (ControlWord "stylesheet" _) : toks) ->
|
Grouped (Tok _ (ControlWord "stylesheet" _) : toks) ->
|
||||||
bs <$ inGroup (handleStylesheet toks)
|
bs <$ inGroup (handleStylesheet toks)
|
||||||
Grouped (Tok _ (ControlWord "listtext" _) : _) -> do
|
Grouped (Tok _ (ControlWord cw _) : _)
|
||||||
|
-- \listtext (Word 2000+) and \pntext (legacy Word 95-style
|
||||||
|
-- paragraph numbering) are both auto-generated list-marker
|
||||||
|
-- destinations. Their visible text (the bullet or number) is
|
||||||
|
-- dropped; we synthesize markers ourselves. The marker also
|
||||||
|
-- appears before the paragraph's \ls/\ilvl, so processing its
|
||||||
|
-- contents would otherwise capture the marker text as the
|
||||||
|
-- paragraph's first run, hiding the list properties from
|
||||||
|
-- emitBlocks (see #11686).
|
||||||
|
| cw == "listtext" || cw == "pntext" -> do
|
||||||
-- eject any previous list items...sometimes TextEdit
|
-- eject any previous list items...sometimes TextEdit
|
||||||
-- doesn't put in a \par
|
-- doesn't put in a \par
|
||||||
bs' <- emitBlocks bs
|
bs' <- emitBlocks bs
|
||||||
-- A \listtext group marks the beginning of a new list item.
|
-- A \listtext/\pntext group marks the beginning of a new list
|
||||||
-- A list paragraph lacking one is a continuation paragraph of
|
-- item. A list paragraph lacking one is a continuation
|
||||||
-- the current item (see emitBlocks).
|
-- paragraph of the current item (see emitBlocks).
|
||||||
updateState $ \s -> s{ sListText = True }
|
updateState $ \s -> s{ sListText = True }
|
||||||
pure bs'
|
pure bs'
|
||||||
Grouped (Tok _ (ControlWord "pgdsc" _) : _) -> pure bs
|
Grouped (Tok _ (ControlWord "pgdsc" _) : _) -> pure bs
|
||||||
@@ -659,6 +674,9 @@ processTok bs (Tok pos tok') = do
|
|||||||
ControlWord "pard" _ -> do
|
ControlWord "pard" _ -> do
|
||||||
newbs <- emitBlocks bs
|
newbs <- emitBlocks bs
|
||||||
modifyGroup (const def)
|
modifyGroup (const def)
|
||||||
|
-- \pard resets paragraph properties, ending any legacy \pn
|
||||||
|
-- paragraph numbering that was in effect.
|
||||||
|
updateState $ \s -> s{ sPnActive = False }
|
||||||
getStyleFormatting 0 >>= foldM processTok newbs
|
getStyleFormatting 0 >>= foldM processTok newbs
|
||||||
ControlWord "par" _ -> emitBlocks bs
|
ControlWord "par" _ -> emitBlocks bs
|
||||||
_ -> pure bs
|
_ -> pure bs
|
||||||
@@ -744,7 +762,10 @@ trimFinalLineBreak ils =
|
|||||||
emitBlocks :: PandocMonad m => Blocks -> RTFParser m Blocks
|
emitBlocks :: PandocMonad m => Blocks -> RTFParser m Blocks
|
||||||
emitBlocks bs = do
|
emitBlocks bs = do
|
||||||
annotatedToks <- reverse . sTextContent <$> getState
|
annotatedToks <- reverse . sTextContent <$> getState
|
||||||
hadListText <- sListText <$> getState
|
-- A \listtext group marks a new item; legacy \pn paragraph numbering
|
||||||
|
-- (sPnActive) likewise makes every paragraph its own item. Note that
|
||||||
|
-- sPnActive persists across \par (until \pard) and so is not reset here.
|
||||||
|
hadListText <- (||) <$> (sListText <$> getState) <*> (sPnActive <$> getState)
|
||||||
updateState $ \s -> s{ sTextContent = [], sListText = False }
|
updateState $ \s -> s{ sTextContent = [], sListText = False }
|
||||||
let justCode = def{ gFontFamily = Just Modern }
|
let justCode = def{ gFontFamily = Just Modern }
|
||||||
let prop = case annotatedToks of
|
let prop = case annotatedToks of
|
||||||
@@ -920,6 +941,47 @@ handleListLevel levelTable (lvl, toks) = do
|
|||||||
Just numStyle -> Ordered (start,numStyle,Period)
|
Just numStyle -> Ordered (start,numStyle,Period)
|
||||||
return $ IntMap.insert lvl listType levelTable
|
return $ IntMap.insert lvl listType levelTable
|
||||||
|
|
||||||
|
-- Legacy Word-95 style paragraph numbering: {\*\pn ...}. This
|
||||||
|
-- destination configures the auto-number (or bullet) for the current
|
||||||
|
-- list paragraph. Unlike modern lists there is no \listtable, so we
|
||||||
|
-- synthesize an entry in the list override table here. The \ls and
|
||||||
|
-- \ilvl that key the table appear inside the \pn group itself, as does
|
||||||
|
-- the numbering style (\pnlvlblt for a bullet, otherwise \pndec,
|
||||||
|
-- \pnucltr, etc.). See #11686.
|
||||||
|
handlePn :: PandocMonad m => [Tok] -> RTFParser m ()
|
||||||
|
handlePn toks = do
|
||||||
|
let ls = headDef 0 [n | Tok _ (ControlWord "ls" (Just n)) <- toks]
|
||||||
|
let lvl = headDef 0 [n | Tok _ (ControlWord "ilvl" (Just n)) <- toks]
|
||||||
|
let start = headDef 1 [n | Tok _ (ControlWord "pnstart" (Just n)) <- toks]
|
||||||
|
let isBullet = not $ null [() | Tok _ (ControlWord "pnlvlblt" _) <- toks]
|
||||||
|
let pnStyle (Tok _ (ControlWord w _)) =
|
||||||
|
case w of
|
||||||
|
"pndec" -> Just Decimal
|
||||||
|
"pnucltr" -> Just UpperAlpha
|
||||||
|
"pnlcltr" -> Just LowerAlpha
|
||||||
|
"pnucrm" -> Just UpperRoman
|
||||||
|
"pnlcrm" -> Just LowerRoman
|
||||||
|
_ -> Nothing
|
||||||
|
pnStyle _ = Nothing
|
||||||
|
let mbNumberStyle
|
||||||
|
| isBullet = Nothing
|
||||||
|
| otherwise = case mapMaybe pnStyle toks of
|
||||||
|
(s:_) -> Just s
|
||||||
|
[] -> Just Decimal -- \pnlvlbody default
|
||||||
|
let listType = case mbNumberStyle of
|
||||||
|
Nothing -> Bullet
|
||||||
|
Just numStyle -> Ordered (start, numStyle, Period)
|
||||||
|
-- Don't clobber a type already registered via a real \listtable.
|
||||||
|
-- \pn is a paragraph property that stays in effect (auto-numbering or
|
||||||
|
-- bulleting every paragraph) until reset by \pard, so flag it so that
|
||||||
|
-- each following paragraph becomes its own list item.
|
||||||
|
updateState $ \s ->
|
||||||
|
s{ sListOverrideTable =
|
||||||
|
IntMap.insertWith (\new old -> IntMap.union old new) ls
|
||||||
|
(IntMap.singleton lvl listType)
|
||||||
|
(sListOverrideTable s)
|
||||||
|
, sPnActive = True }
|
||||||
|
|
||||||
handleListOverrideTable :: PandocMonad m => [Tok] -> RTFParser m ()
|
handleListOverrideTable :: PandocMonad m => [Tok] -> RTFParser m ()
|
||||||
handleListOverrideTable toks = mapM_ handleListOverride toks
|
handleListOverrideTable toks = mapM_ handleListOverride toks
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ tests = map rtfTest [ "footnote"
|
|||||||
, "list_simple"
|
, "list_simple"
|
||||||
, "list_complex"
|
, "list_complex"
|
||||||
, "list_multiparagraph"
|
, "list_multiparagraph"
|
||||||
|
, "list_legacy"
|
||||||
, "bookmark"
|
, "bookmark"
|
||||||
, "table_simple"
|
, "table_simple"
|
||||||
, "table_error_codes"
|
, "table_error_codes"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Pandoc
|
||||||
|
Meta { unMeta = fromList [] }
|
||||||
|
[ OrderedList
|
||||||
|
( 1 , Decimal , Period )
|
||||||
|
[ [ Para [ Str "A" ] ]
|
||||||
|
, [ Para [ Str "Numbered" ] ]
|
||||||
|
, [ Para [ Str "List" ] ]
|
||||||
|
]
|
||||||
|
, BulletList
|
||||||
|
[ [ Para [ Str "A" ] ]
|
||||||
|
, [ Para [ Str "Bulleted" ] ]
|
||||||
|
, [ Para [ Str "List" ] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{\rtf1\ansi\deff0{\fonttbl{\f0 Helvetica;}{\f2 Symbol;}{\f3 Symbol;}}
|
||||||
|
\plain\plain\f0\fs20\pard\ssparaaux0\s0{\pntext\pard\plain\f2\fs22 1.\tab}{\*\pn\pnlvlbody\pnstart1\pndec\ls3\ilvl0\pnhang{\pntxta \'2e}}\ls3\ilvl0\fi-360\li360\ql\plain\f0\fs20 A\par Numbered\par List\par
|
||||||
|
\pard\ssparaaux0\s0{\pntext\pard\plain\f3\fs22 \'b7\tab}{\*\pn\pnlvlblt\ls5\ilvl0\pnhang\pnf3{\pntxtb \'b7}}\ls5\ilvl0\fi-360\li360\ql\plain\f0\fs20 A\par Bulleted\par List\par
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user