mirror of
https://github.com/jgm/pandoc.git
synced 2026-08-28 17:20:47 +08:00
Docx writer: fix section breaks with --top-level-division.
Previously, the fix for #10578 (removing leading section break to avoid blank first page) was implemented inside blocksToOpenXML, which is called recursively for Div contents. Since makeSectionsWithOffsets wraps each section in a Div, this caused ALL section breaks to be stripped, not just the first one (#11482). This commit fixes the issue by tracking whether we've processed the first section header using a new stFirstSectionHeader state flag. Section breaks are now correctly added between chapters/parts while still avoiding a blank first page. Closes #11482. See also #10578. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ Conversion of 'Pandoc' documents to docx.
|
||||
-}
|
||||
module Text.Pandoc.Writers.Docx.OpenXML ( writeOpenXML, maxListLevel ) where
|
||||
|
||||
import Control.Monad ((>=>), when, unless)
|
||||
import Control.Monad (when, unless)
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad.Except (catchError)
|
||||
import Crypto.Hash (hashWith, SHA1(SHA1))
|
||||
@@ -329,14 +329,6 @@ blocksToOpenXML :: (PandocMonad m) => WriterOptions -> [Block] -> WS m [Content]
|
||||
blocksToOpenXML opts =
|
||||
fmap concat . mapM (blockToOpenXML opts)
|
||||
. separateTables . filter (not . isForeignRawBlock)
|
||||
>=>
|
||||
\case
|
||||
a@(x:xs) -> do
|
||||
sep <- sectionSeparator
|
||||
if Just x == sep
|
||||
then pure xs
|
||||
else pure a
|
||||
[] -> pure []
|
||||
|
||||
isForeignRawBlock :: Block -> Bool
|
||||
isForeignRawBlock (RawBlock format _) = format /= "openxml"
|
||||
@@ -414,9 +406,19 @@ blockToOpenXML' opts (Header lev (ident,_,kvs) lst) = do
|
||||
Nothing -> return []
|
||||
else return []
|
||||
contents <- (number ++) <$> inlinesToOpenXML opts lst
|
||||
addSectionBreak <- sectionSeparator >>= \case
|
||||
Just sep | isSection -> pure (sep:)
|
||||
_ -> pure id
|
||||
-- Add section break before section-level headers, but not for the first one
|
||||
-- to avoid a blank first page (#10578, #11482).
|
||||
addSectionBreak <- if isSection
|
||||
then do
|
||||
isFirst <- gets stFirstSectionHeader
|
||||
if isFirst
|
||||
then do
|
||||
modify $ \s -> s { stFirstSectionHeader = False }
|
||||
pure id
|
||||
else sectionSeparator >>= \case
|
||||
Just sep -> pure (sep:)
|
||||
Nothing -> pure id
|
||||
else pure id
|
||||
addSectionBreak <$>
|
||||
if T.null ident
|
||||
then return [Elem $ mknode "w:p" [] (map Elem paraProps ++ contents)]
|
||||
|
||||
@@ -121,6 +121,7 @@ data WriterState = WriterState{
|
||||
, stDelId :: Int
|
||||
, stStyleMaps :: StyleMaps
|
||||
, stFirstPara :: Bool
|
||||
, stFirstSectionHeader :: Bool -- ^ True until first section header is processed
|
||||
, stNumIdUsed :: Bool -- ^ True if the current numId (envListNumId) has been used.
|
||||
-- Should only be used once, for the first paragraph.
|
||||
, stInTable :: Bool
|
||||
@@ -146,6 +147,7 @@ defaultWriterState = WriterState{
|
||||
, stDelId = 1
|
||||
, stStyleMaps = StyleMaps M.empty M.empty
|
||||
, stFirstPara = False
|
||||
, stFirstSectionHeader = True
|
||||
, stNumIdUsed = False
|
||||
, stInTable = False
|
||||
, stInList = False
|
||||
|
||||
@@ -222,6 +222,67 @@ tests = [ testGroup "inlines"
|
||||
"docx/document-properties-short-desc.native"
|
||||
"docx/golden/document-properties-short-desc.docx"
|
||||
]
|
||||
, testGroup "top-level-division"
|
||||
-- Helper to count occurrences of a substring
|
||||
-- Note: counts by splitting on "<w:sectPr" which marks section properties
|
||||
[ testCase "no section break before first chapter (#10578)" $ do
|
||||
-- With --top-level-division=chapter, there should be no section
|
||||
-- break before the first chapter (to avoid blank first page)
|
||||
let opts = def{ writerTopLevelDivision = TopLevelChapter }
|
||||
bs <- runIOorExplode $ do
|
||||
setVerbosity ERROR
|
||||
let doc = Pandoc mempty
|
||||
[ Header 1 ("ch1", [], []) [Str "Chapter", Space, Str "1"]
|
||||
, Para [Str "First", Space, Str "chapter."]
|
||||
]
|
||||
writeDocx opts doc
|
||||
let archive = toArchive bs
|
||||
entry <- case findEntryByPath "word/document.xml" archive of
|
||||
Nothing -> assertFailure "Missing word/document.xml in output docx"
|
||||
Just e -> return e
|
||||
let docXml = show (fromEntry entry)
|
||||
-- Count occurrences of "<w:sectPr" (opening tag for section properties)
|
||||
let countOccurrences needle haystack =
|
||||
length (filter (needle `isPrefixOf`) (tails haystack))
|
||||
where tails [] = []; tails s@(_:xs) = s : tails xs
|
||||
let sectPrCount = countOccurrences "<w:sectPr" docXml
|
||||
-- Should have exactly 1 sectPr (the final document section),
|
||||
-- not 2 (which would mean one before the chapter heading)
|
||||
assertBool ("Expected 1 sectPr (final section only), found " ++ show sectPrCount)
|
||||
(sectPrCount == 1)
|
||||
, testCase "section breaks between chapters (#11482)" $ do
|
||||
-- With --top-level-division=chapter, there should be section
|
||||
-- breaks between chapters (but not before the first one)
|
||||
let opts = def{ writerTopLevelDivision = TopLevelChapter }
|
||||
bs <- runIOorExplode $ do
|
||||
setVerbosity ERROR
|
||||
let doc = Pandoc mempty
|
||||
[ Header 1 ("ch1", [], []) [Str "Chapter", Space, Str "1"]
|
||||
, Para [Str "First", Space, Str "chapter."]
|
||||
, Header 1 ("ch2", [], []) [Str "Chapter", Space, Str "2"]
|
||||
, Para [Str "Second", Space, Str "chapter."]
|
||||
, Header 1 ("ch3", [], []) [Str "Chapter", Space, Str "3"]
|
||||
, Para [Str "Third", Space, Str "chapter."]
|
||||
]
|
||||
writeDocx opts doc
|
||||
let archive = toArchive bs
|
||||
entry <- case findEntryByPath "word/document.xml" archive of
|
||||
Nothing -> assertFailure "Missing word/document.xml in output docx"
|
||||
Just e -> return e
|
||||
let docXml = show (fromEntry entry)
|
||||
-- Count occurrences of "<w:sectPr" (opening tag for section properties)
|
||||
let countOccurrences needle haystack =
|
||||
length (filter (needle `isPrefixOf`) (tails haystack))
|
||||
where tails [] = []; tails s@(_:xs) = s : tails xs
|
||||
let sectPrCount = countOccurrences "<w:sectPr" docXml
|
||||
-- Should have 3 sectPr elements:
|
||||
-- - 1 before chapter 2
|
||||
-- - 1 before chapter 3
|
||||
-- - 1 final document section
|
||||
-- (No section break before chapter 1)
|
||||
assertBool ("Expected 3 sectPr elements, found " ++ show sectPrCount)
|
||||
(sectPrCount == 3)
|
||||
]
|
||||
, testGroup "reference docx"
|
||||
[ testCase "no media directory override in content types" $ do
|
||||
let opts = def{ writerReferenceDoc = Just "docx/inline_images.docx" }
|
||||
|
||||
Reference in New Issue
Block a user