mirror of
https://github.com/jgm/pandoc.git
synced 2026-09-04 14:45:50 +00:00
When a reference document uses a non-standard namespace prefix for the WordprocessingML namespace (e.g. `ns0` instead of `w`), `sectPr` elements copied from the reference would retain the non-`w` prefix, producing malformed XML in the output document. Similarly, `extractPageLayout` only matched elements with prefix `w`, missing `sectPr` elements with other prefixes. This is fixed by matching on the namespace URI rather than the prefix, and normalizing the prefix to `w` on all elements and attributes copied from reference-doc `sectPr`. Some new tests have been added, and the test suite has been streamlined using helper functions.
356 lines
14 KiB
Haskell
356 lines
14 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
module Tests.Writers.Docx (tests) where
|
|
|
|
import Codec.Archive.Zip (Archive, findEntryByPath, fromEntry, toArchive)
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import Data.List (isInfixOf, isPrefixOf)
|
|
import qualified Data.Map as M
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as Text
|
|
import qualified Data.Text.IO as T
|
|
import Test.Tasty
|
|
import Test.Tasty.HUnit
|
|
import Tests.Writers.OOXML
|
|
import Text.Pandoc
|
|
import Text.XML.Light ( Element, QName(QName), findAttr, findElements
|
|
, parseXMLDoc )
|
|
|
|
wmlName :: String -> QName
|
|
wmlName n = QName n
|
|
(Just "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
|
|
(Just "w")
|
|
|
|
entryBytes :: String -> Archive -> IO BL.ByteString
|
|
entryBytes path archive =
|
|
maybe (assertFailure $ "Missing " ++ path) (return . fromEntry) $
|
|
findEntryByPath path archive
|
|
|
|
entryXml :: String -> Archive -> IO Element
|
|
entryXml path archive =
|
|
maybe (assertFailure $ "Invalid " ++ path) return . parseXMLDoc
|
|
=<< entryBytes path archive
|
|
|
|
documentXml :: WriterOptions -> Pandoc -> IO Element
|
|
documentXml opts doc =
|
|
entryXml "word/document.xml" . toArchive =<<
|
|
runIOorExplode (setVerbosity ERROR >> writeDocx opts doc)
|
|
|
|
documentXmlFromNative :: WriterOptions -> FilePath -> IO Element
|
|
documentXmlFromNative opts fp = do
|
|
txt <- T.readFile fp
|
|
entryXml "word/document.xml" . toArchive =<<
|
|
runIOorExplode (setVerbosity ERROR >> readNative def txt >>= writeDocx opts)
|
|
|
|
-- we add an extra check to make sure that we're not writing in the
|
|
-- toplevel docx directory. We don't want to accidentally overwrite an
|
|
-- Word-generated docx file used to test the reader.
|
|
docxTest :: String -> WriterOptions -> FilePath -> FilePath -> TestTree
|
|
docxTest testName opts nativeFP goldenFP =
|
|
if "docx/golden/" `isPrefixOf` goldenFP
|
|
then ooxmlTest writeDocx testName opts nativeFP goldenFP
|
|
else testCase testName $
|
|
assertFailure $
|
|
goldenFP ++ " is not in `test/docx/golden`"
|
|
|
|
tests :: [TestTree]
|
|
tests = [ testGroup "inlines"
|
|
[ docxTest
|
|
"font formatting"
|
|
def
|
|
"docx/inline_formatting.native"
|
|
"docx/golden/inline_formatting.docx"
|
|
, docxTest
|
|
"hyperlinks"
|
|
def
|
|
"docx/links.native"
|
|
"docx/golden/links.docx"
|
|
, docxTest
|
|
"inline image"
|
|
def{ writerExtensions =
|
|
enableExtension Ext_native_numbering (writerExtensions def) }
|
|
"docx/image_writer_test.native"
|
|
"docx/golden/image.docx"
|
|
, docxTest
|
|
"inline images"
|
|
def
|
|
"docx/inline_images_writer_test.native"
|
|
"docx/golden/inline_images.docx"
|
|
, docxTest
|
|
"handling unicode input"
|
|
def
|
|
"docx/unicode.native"
|
|
"docx/golden/unicode.docx"
|
|
, docxTest
|
|
"inline code"
|
|
def
|
|
"docx/inline_code.native"
|
|
"docx/golden/inline_code.docx"
|
|
, docxTest
|
|
"inline code in subscript and superscript"
|
|
def
|
|
"docx/verbatim_subsuper.native"
|
|
"docx/golden/verbatim_subsuper.docx"
|
|
]
|
|
, testGroup "blocks"
|
|
[ docxTest
|
|
"headers"
|
|
def
|
|
"docx/headers.native"
|
|
"docx/golden/headers.docx"
|
|
, docxTest
|
|
"nested anchor spans in header"
|
|
def
|
|
"docx/nested_anchors_in_header.native"
|
|
"docx/golden/nested_anchors_in_header.docx"
|
|
, docxTest
|
|
"lists"
|
|
def
|
|
"docx/lists.native"
|
|
"docx/golden/lists.docx"
|
|
, docxTest
|
|
"lists continuing after interruption"
|
|
def
|
|
"docx/lists_continuing.native"
|
|
"docx/golden/lists_continuing.docx"
|
|
, docxTest
|
|
"lists restarting after interruption"
|
|
def
|
|
"docx/lists_restarting.native"
|
|
"docx/golden/lists_restarting.docx"
|
|
, docxTest
|
|
"lists with multiple initial list levels"
|
|
def
|
|
"docx/lists_multiple_initial.native"
|
|
"docx/golden/lists_multiple_initial.docx"
|
|
, docxTest
|
|
"lists with div bullets"
|
|
def
|
|
"docx/lists_div_bullets.native"
|
|
"docx/golden/lists_div_bullets.docx"
|
|
, docxTest
|
|
"definition lists"
|
|
def
|
|
"docx/definition_list.native"
|
|
"docx/golden/definition_list.docx"
|
|
, docxTest
|
|
"task lists"
|
|
def
|
|
"docx/task_list.native"
|
|
"docx/golden/task_list.docx"
|
|
, docxTest
|
|
"issue 9994"
|
|
def
|
|
"docx/lists_9994.native"
|
|
"docx/golden/lists_9994.docx"
|
|
, docxTest
|
|
"footnotes and endnotes"
|
|
def
|
|
"docx/notes.native"
|
|
"docx/golden/notes.docx"
|
|
, docxTest
|
|
"links in footnotes and endnotes"
|
|
def
|
|
"docx/link_in_notes.native"
|
|
"docx/golden/link_in_notes.docx"
|
|
, docxTest
|
|
"blockquotes"
|
|
def
|
|
"docx/block_quotes.native"
|
|
"docx/golden/block_quotes.docx"
|
|
, docxTest
|
|
"tables"
|
|
def
|
|
"docx/tables.native"
|
|
"docx/golden/tables.docx"
|
|
, docxTest
|
|
"tables without explicit column widths"
|
|
def
|
|
"docx/tables-default-widths.native"
|
|
"docx/golden/tables-default-widths.docx"
|
|
, docxTest
|
|
"tables with lists in cells"
|
|
def
|
|
"docx/table_with_list_cell.native"
|
|
"docx/golden/table_with_list_cell.docx"
|
|
, docxTest
|
|
"tables with one row"
|
|
def
|
|
"docx/table_one_row.native"
|
|
"docx/golden/table_one_row.docx"
|
|
, docxTest
|
|
"tables separated with RawBlock"
|
|
def
|
|
"docx/tables_separated_with_rawblock.native"
|
|
"docx/golden/tables_separated_with_rawblock.docx"
|
|
, docxTest
|
|
"code block"
|
|
def
|
|
"docx/codeblock.native"
|
|
"docx/golden/codeblock.docx"
|
|
, docxTest
|
|
"raw OOXML blocks"
|
|
def
|
|
"docx/raw-blocks.native"
|
|
"docx/golden/raw-blocks.docx"
|
|
, docxTest
|
|
"raw bookmark markers"
|
|
def
|
|
"docx/raw-bookmarks.native"
|
|
"docx/golden/raw-bookmarks.docx"
|
|
]
|
|
, testGroup "track changes"
|
|
[ docxTest
|
|
"insertion"
|
|
def
|
|
"docx/track_changes_insertion_all.native"
|
|
"docx/golden/track_changes_insertion.docx"
|
|
, docxTest
|
|
"deletion"
|
|
def
|
|
"docx/track_changes_deletion_all.native"
|
|
"docx/golden/track_changes_deletion.docx"
|
|
, docxTest
|
|
"move text"
|
|
def
|
|
"docx/track_changes_move_all.native"
|
|
"docx/golden/track_changes_move.docx"
|
|
, docxTest
|
|
"comments"
|
|
def
|
|
"docx/comments.native"
|
|
"docx/golden/comments.docx"
|
|
, docxTest
|
|
"scrubbed metadata"
|
|
def
|
|
"docx/track_changes_scrubbed_metadata.native"
|
|
"docx/golden/track_changes_scrubbed_metadata.docx"
|
|
]
|
|
, testGroup "custom styles"
|
|
[ docxTest "custom styles without reference.docx"
|
|
def
|
|
"docx/custom_style.native"
|
|
"docx/golden/custom_style_no_reference.docx"
|
|
, docxTest "custom styles with reference.docx"
|
|
def{writerReferenceDoc = Just "docx/custom-style-reference.docx"}
|
|
"docx/custom_style.native"
|
|
"docx/golden/custom_style_reference.docx"
|
|
, docxTest "suppress custom style for headers and blockquotes"
|
|
def
|
|
"docx/custom-style-preserve.native"
|
|
"docx/golden/custom_style_preserve.docx"
|
|
]
|
|
, testGroup "metadata"
|
|
[ docxTest "document properties (core, custom)"
|
|
def
|
|
"docx/document-properties.native"
|
|
"docx/golden/document-properties.docx"
|
|
, docxTest "document properties (short description)"
|
|
def
|
|
"docx/document-properties-short-desc.native"
|
|
"docx/golden/document-properties-short-desc.docx"
|
|
]
|
|
, testGroup "top-level-division"
|
|
[ testCase "no section break before first chapter (#10578)" $ do
|
|
let opts = def{ writerTopLevelDivision = TopLevelChapter }
|
|
doc <- documentXml opts $ Pandoc mempty
|
|
[ Header 1 ("ch1", [], []) [Str "Chapter", Space, Str "1"]
|
|
, Para [Str "First", Space, Str "chapter."]
|
|
]
|
|
let sectPrCount = length $ findElements (wmlName "sectPr") doc
|
|
assertBool ("Expected 1 sectPr, found " ++ show sectPrCount)
|
|
(sectPrCount == 1)
|
|
, testCase "section breaks between chapters (#11482)" $ do
|
|
let opts = def{ writerTopLevelDivision = TopLevelChapter }
|
|
doc <- documentXml opts $ 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."]
|
|
]
|
|
let sectPrCount = length $ findElements (wmlName "sectPr") doc
|
|
assertBool ("Expected 3 sectPrs, 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"}
|
|
txt <- T.readFile "docx/inline_formatting.native"
|
|
bs <- runIOorExplode $ do
|
|
mblang <- toLang (Just (Text.pack "en-US") :: Maybe Text)
|
|
maybe (return ()) setTranslations mblang
|
|
setVerbosity ERROR
|
|
readNative def txt >>= writeDocx opts
|
|
doc <- entryXml "[Content_Types].xml" $ toArchive bs
|
|
let partNameAttr = QName "PartName" Nothing Nothing
|
|
let overrideName = QName "Override" Nothing Nothing
|
|
let overrides = findElements overrideName doc
|
|
let hasBadOverride =
|
|
any (\el -> findAttr partNameAttr el == Just "/word/media/")
|
|
overrides
|
|
assertBool "Found invalid /word/media/ Override in [Content_Types].xml"
|
|
(not hasBadOverride)
|
|
, testCase "language from reference docx is preserved" $ do
|
|
-- First, verify that the german-reference.docx actually has de-DE
|
|
refBs <- BL.readFile "docx/german-reference.docx"
|
|
let refArchive = toArchive refBs
|
|
refStylesXml <- show <$> entryBytes "word/styles.xml" refArchive
|
|
let getLangLines = filter ("w:lang" `isInfixOf`) . lines
|
|
assertBool ("german-reference.docx w:lang line: " ++
|
|
unlines (getLangLines refStylesXml))
|
|
(any ("de-DE" `isInfixOf`) (getLangLines refStylesXml))
|
|
-- Now test that using this reference preserves the language
|
|
let opts = def{writerReferenceDoc=Just "docx/german-reference.docx"}
|
|
txt <- T.readFile "docx/inline_formatting.native"
|
|
bs <- runIOorExplode $ do
|
|
setVerbosity ERROR
|
|
readNative def txt >>= writeDocx opts
|
|
stylesXml <- show <$> entryBytes "word/styles.xml" (toArchive bs)
|
|
-- Find the w:lang line for debugging
|
|
-- Check that the styles.xml contains the German language
|
|
assertBool ("Language from reference docx not preserved. w:lang lines: " ++ unlines (getLangLines stylesXml))
|
|
(any ("de-DE" `isInfixOf`) (getLangLines stylesXml))
|
|
, testCase "section properties from non-w-prefix reference docx" $ do
|
|
let opts = def{writerReferenceDoc=Just "docx/ns0-reference.docx"}
|
|
doc <- documentXmlFromNative opts "docx/inline_formatting.native"
|
|
case findElements (wmlName "sectPr") doc of
|
|
[] -> assertFailure "sectPr not found in output"
|
|
sectPr:_ -> do
|
|
assertBool "pgSz not found in output"
|
|
(not $ null $ findElements (wmlName "pgSz") sectPr)
|
|
findAttr (wmlName "type") sectPr @?= Just "continuous"
|
|
, testCase "language from metadata overrides reference docx" $ do
|
|
-- Use a reference docx with German language, but specify French in metadata
|
|
let opts = def{writerReferenceDoc=Just "docx/german-reference.docx"}
|
|
bs <- runIOorExplode $ do
|
|
setVerbosity ERROR
|
|
-- Create a document with French language metadata
|
|
let doc = Pandoc (Meta $ M.fromList [("lang", MetaString "fr-FR")])
|
|
[Para [Str "Test"]]
|
|
writeDocx opts doc
|
|
stylesXml <- show <$> entryBytes "word/styles.xml" (toArchive bs)
|
|
-- Check that the styles.xml contains the French language (not German)
|
|
let getLangLines = filter ("w:lang" `isInfixOf`) . lines
|
|
assertBool "Language from metadata did not override reference docx (expected fr-FR)"
|
|
(any ("fr-FR" `isInfixOf`) (getLangLines stylesXml))
|
|
]
|
|
, testGroup "paragraph styles"
|
|
[ testCase "FirstParagraph after heading with footnote (#11573)" $ do
|
|
let opts = def
|
|
bs <- runIOorExplode $ do
|
|
setVerbosity ERROR
|
|
let doc = Pandoc mempty
|
|
[ Header 3 ("heading-with-note", [], [])
|
|
[Note [Para [Str "note"]], Str "Heading"]
|
|
, Para [Str "Para", Space, Str "after."]
|
|
]
|
|
writeDocx opts doc
|
|
docXml <- show <$> entryBytes "word/document.xml" (toArchive bs)
|
|
assertBool
|
|
("Expected FirstParagraph style after heading with footnote, got: "
|
|
++ docXml)
|
|
("FirstParagraph" `isInfixOf` docXml)
|
|
]
|
|
]
|