Docx reader: key list numbering off abstractNumId.

Word represents "restart numbering" on a style-based list by pointing
only the first item of the restarted list at a new `numId` that shares the
original list's abstract numbering definition but carries a
`w:startOverride`; the remaining items keep using the original `numId`.

Pandoc keyed list continuation and grouping on the `numId`, so the
restarted items continued the stale count from the earlier list (and
were split into a separate ordered list with the wrong start).

Key continuation and grouping off the `abstractNumId` instead (the real
running counter in Word), and treat `startOverride` as a restart that
resets the count.

Closes #8367.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John MacFarlane
2026-06-09 09:58:26 +00:00
parent c60b7f3f08
commit 1b4a5356b4
5 changed files with 59 additions and 13 deletions
+7 -6
View File
@@ -782,15 +782,16 @@ bodyPartToBlocks (Paragraph pPr parparts) = do
then (<> horizontalRule)
else id)
<$> getMainPar
bodyPartToBlocks (ListItem pPr numId lvl (Just levelInfo) parparts) = do
-- We check whether this current numId has previously been used,
-- since Docx expects us to pick up where we left off.
bodyPartToBlocks (ListItem pPr numId lvl isRestart (Just levelInfo) parparts) = do
-- We check whether this current abstract numbering id has previously been
-- used, since Docx expects us to pick up where we left off -- unless this
-- item explicitly restarts numbering (see #8367).
listState <- gets docxListState
let startFromState = M.lookup (numId, lvl) listState
Level _ fmt txt startFromLevelInfo = levelInfo
start = case startFromState of
Just n -> n + 1
Nothing -> fromMaybe 1 startFromLevelInfo
Just n | not isRestart -> n + 1
_ -> fromMaybe 1 startFromLevelInfo
kvs = [ ("level", lvl)
, ("num-id", numId)
, ("format", fmt)
@@ -804,7 +805,7 @@ bodyPartToBlocks (ListItem pPr numId lvl (Just levelInfo) parparts) = do
in M.insert (numId, lvl) start (M.filterWithKey notExpired listState) }
blks <- bodyPartToBlocks (Paragraph pPr parparts)
return $ divWith ("", ["list-item"], kvs) blks
bodyPartToBlocks (ListItem pPr _ _ _ parparts) =
bodyPartToBlocks (ListItem pPr _ _ _ _ parparts) =
let pPr' = pPr {pStyle = constructBogusParStyleData "list-paragraph": pStyle pPr}
in
bodyPartToBlocks $ Paragraph pPr' parparts
+35 -7
View File
@@ -304,7 +304,7 @@ defaultParagraphStyle = ParagraphStyle { pStyle = []
data BodyPart = Paragraph ParagraphStyle [ParPart]
| Heading Int ParaStyleName ParagraphStyle T.Text T.Text (Maybe Level)
[ParPart]
| ListItem ParagraphStyle T.Text T.Text (Maybe Level) [ParPart]
| ListItem ParagraphStyle T.Text T.Text Bool (Maybe Level) [ParPart]
| Tbl (Maybe T.Text) T.Text TblGrid TblLook [Row]
| Captioned ParagraphStyle [ParPart] BodyPart
| HRule
@@ -619,6 +619,28 @@ lookupLevel numId ilvl (Numbering _ numbs absNumbs) = do
_ ->
lookup ilvl $ map (\l@(Level i _ _ _) -> (i, l)) lvls
-- | Given a @numId@ and level, return the abstract numbering id (the stable
-- identifier shared by all numbering instances based on the same abstract
-- numbering definition) together with a flag indicating whether this
-- particular instance restarts numbering at this level (via a
-- @w:startOverride@). Word represents "restart numbering" by pointing the
-- first item of the restarted list at a new numId that shares the abstract
-- numbering of the original list but carries a startOverride; the remaining
-- items keep using the original numId. Keying list continuation off the
-- abstract numbering id (rather than the numId) lets those items be treated
-- as one continuous, restarted list. See #8367.
lookupNumberingInfo :: T.Text -> T.Text -> Numbering -> (T.Text, Bool)
lookupNumberingInfo numId ilvl (Numbering _ numbs _) =
case lookup numId $
map (\(Numb nid absnumid ovr) -> (nid, (absnumid, ovr))) numbs of
Nothing -> (numId, False)
Just (absNumId, ovrrides) ->
let isRestart = case lookup ilvl $
map (\lo@(LevelOverride i _ _) -> (i, lo)) ovrrides of
Just (LevelOverride _ (Just _) _) -> True
_ -> False
in (absNumId, isRestart)
loElemToLevelOverride :: NameSpaces -> Element -> Maybe LevelOverride
loElemToLevelOverride ns element
| isElem ns "w" "lvlOverride" element = do
@@ -820,8 +842,10 @@ pNumInfo = getParStyleField numInfo . pStyle
mkListItem :: ParagraphStyle -> Text -> Text -> [ParPart] -> D BodyPart
mkListItem parstyle numId lvl parparts = do
lvlInfo <- lookupLevel numId lvl <$> asks envNumbering
return $ ListItem parstyle numId lvl lvlInfo parparts
numbering <- asks envNumbering
let lvlInfo = lookupLevel numId lvl numbering
(absNumId, isRestart) = lookupNumberingInfo numId lvl numbering
return $ ListItem parstyle absNumId lvl isRestart lvlInfo parparts
pStyleIndentation :: ParagraphStyle -> Maybe ParIndentation
pStyleIndentation style = (getParStyleField indent . pStyle) style
@@ -877,7 +901,8 @@ elemToBodyPart ns element
elemToBodyPart ns element
| isElem ns "w" "p" element
, Just (numId, lvl) <- getNumInfo ns element = do
lvlInfo <- lookupLevel numId lvl <$> asks envNumbering
numbering <- asks envNumbering
let lvlInfo = lookupLevel numId lvl numbering
parstyle <- elemToParagraphStyle ns element
<$> asks envParStyles
<*> asks envNumbering
@@ -885,7 +910,8 @@ elemToBodyPart ns element
case pHeading parstyle of
Nothing -> mkListItem parstyle numId lvl parparts
Just (parstylename, lev)
-> return $ Heading lev parstylename parstyle numId lvl lvlInfo parparts
-> let (absNumId, _) = lookupNumberingInfo numId lvl numbering
in return $ Heading lev parstylename parstyle absNumId lvl lvlInfo parparts
elemToBodyPart ns element
| isElem ns "w" "p" element
, [ppr] <- elChildren element
@@ -928,8 +954,10 @@ elemToBodyPart ns element
mkListItem parstyle numId lvl parparts
Just (parstylename, lev) -> do
let (numId, lvl) = fromMaybe ("","") $ pNumInfo parstyle
lvlInfo <- lookupLevel numId lvl <$> asks envNumbering
return $ Heading lev parstylename parstyle numId lvl lvlInfo parparts
numbering <- asks envNumbering
let lvlInfo = lookupLevel numId lvl numbering
(absNumId, _) = lookupNumberingInfo numId lvl numbering
return $ Heading lev parstylename parstyle absNumId lvl lvlInfo parparts
Nothing -> return $ Paragraph parstyle parparts
elemToBodyPart ns element
| isElem ns "w" "tbl" element = do
+4
View File
@@ -281,6 +281,10 @@ tests = [ testGroup "document"
"sublists reset numbering to 1"
"docx/lists_sublist_reset.docx"
"docx/lists_sublist_reset.native"
, testCompare
"style-based list restarted on one item (#8367)"
"docx/lists_restart_8367.docx"
"docx/lists_restart_8367.native"
, testCompare
"definition lists"
"docx/definition_list.docx"
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
[Header 1 ("section-1",[],[]) [Str "Section",Space,Str "1"]
,OrderedList (1,Decimal,Period)
[[Para [Str "Item",Space,Str "1"]]
,[Para [Str "Item",Space,Str "2"]]
,[Para [Str "Item",Space,Str "3"]]]
,Para [Str "Conclusion"]
,Header 1 ("section-2",[],[]) [Str "Section",Space,Str "2"]
,OrderedList (1,Decimal,Period)
[[Para [Str "Item",Space,Str "1"]]
,[Para [Str "Item",Space,Str "2"]]
,[Para [Str "Item",Space,Str "3"]]
,[Para [Str "Item",Space,Str "4"]]]
,Para [Str "Conclusion"]]