diff --git a/src/Text/Pandoc/Readers/Docx/Parse.hs b/src/Text/Pandoc/Readers/Docx/Parse.hs index 0a9c2ca0e..6e9889db1 100644 --- a/src/Text/Pandoc/Readers/Docx/Parse.hs +++ b/src/Text/Pandoc/Readers/Docx/Parse.hs @@ -160,8 +160,10 @@ unwrapElement ns element | isElem ns "w" "smartTag" element = concatMap (unwrapElement ns) (elChildren element) | isElem ns "w" "p" element - , textboxes@(_:_) <- findChildrenByName ns "w" "r" element >>= findTextboxes - = concatMap (unwrapElement ns) (concatMap elChildren textboxes) -- handle #9214, #11053 + , _:_ <- findChildrenByName ns "w" "r" element >>= findTextboxes + = let result = splitP (elContent element) [] -- handle #9214, #11053 + in [element{ elName = QName "textbox-image" Nothing Nothing + , elContent = map Elem result }] | isElem ns "w" "r" element , Just fallback <- findChildByName ns "mc" "AlternateContent" element >>= findChildByName ns "mc" "Fallback" @@ -169,6 +171,26 @@ unwrapElement ns element | otherwise = [element{ elContent = concatMap (unwrapContent ns) (elContent element) }] where + -- Split a w:p's children around textbox-bearing runs, preserving order. + -- Non-textbox content is grouped into a copy of the original w:p; + -- textbox content is unwrapped into sibling elements in place. + splitP [] acc = [wrapP acc | hasContent acc] + splitP (c:cs) acc + | Elem el <- c + , isElem ns "w" "r" el + , tbs@(_:_) <- findTextboxes el + = [wrapP acc | hasContent acc] + ++ concatMap (unwrapElement ns) (concatMap elChildren tbs) + ++ splitP cs [] + | otherwise + = splitP cs (acc ++ [c]) + wrapP cs = element{ elContent = concatMap (unwrapContent ns) cs } + hasContent = any (\case Elem el -> isElem ns "w" "r" el + && any (isContentElem . elName) + (elChildren el) + _ -> False) + isContentElem n = qName n `elem` + ["t", "drawing", "pict", "br", "tab", "sym", "fldChar", "instrText"] -- Search textbox content in the run's effective children. -- If AlternateContent is present, use only the fallback branch, -- matching the w:r unwrapping logic and avoiding duplicate textbox @@ -475,9 +497,17 @@ archiveToDocument zf = do return $ Document namespaces body elemToBody :: NameSpaces -> Element -> D Body -elemToBody ns element | isElem ns "w" "body" element = - Body . addCaptioned <$> mapD (elemToBodyPart ns) (elChildren element) -elemToBody _ _ = throwError WrongElem +elemToBody ns element + | isElem ns "w" "body" element = + Body . addCaptioned <$> concatMapM (elemToBodyParts ns) (elChildren element) + | otherwise = throwError WrongElem + where + elemToBodyParts ns' el + | qName (elName el) == "textbox-image" + = addSplitGroupCaptioned . splitMixedParagraphs + <$> mapD (elemToBodyPart ns') (elChildren el) + | otherwise + = ((:[]) <$> elemToBodyPart ns' el) `catchError` (\_ -> return []) archiveToStyles :: Archive -> (CharStyleMap, ParStyleMap) archiveToStyles = archiveToStyles' getStyleId getStyleId @@ -803,6 +833,28 @@ addCaptioned (x : Paragraph parstyle parparts : xs) = Captioned parstyle parparts x : addCaptioned xs addCaptioned (x:xs) = x : addCaptioned xs +-- Split paragraphs that contain both a Drawing and text into two body parts, +-- so that the captioning logic can combine them. +splitMixedParagraphs :: [BodyPart] -> [BodyPart] +splitMixedParagraphs [] = [] +splitMixedParagraphs (Paragraph ps parts : rest) + | (drawings@(_:_), texts@(_:_)) <- partition isDrawingPart parts + = Paragraph ps drawings : Paragraph ps texts : splitMixedParagraphs rest + where isDrawingPart Drawing{} = True + isDrawingPart _ = False +splitMixedParagraphs (x:xs) = x : splitMixedParagraphs xs + +-- For textbox-image groups: combine image + any adjacent text into Captioned +addSplitGroupCaptioned :: [BodyPart] -> [BodyPart] +addSplitGroupCaptioned [] = [] +addSplitGroupCaptioned (Paragraph parstyle parparts : x : xs) + | isCaptionable x + = Captioned parstyle parparts x : addSplitGroupCaptioned xs +addSplitGroupCaptioned (x : Paragraph parstyle parparts : xs) + | isCaptionable x + = Captioned parstyle parparts x : addSplitGroupCaptioned xs +addSplitGroupCaptioned (x:xs) = x : addSplitGroupCaptioned xs + isCaptionable :: BodyPart -> Bool isCaptionable (Paragraph _ [Drawing{}]) = True isCaptionable (Tbl{}) = True diff --git a/test/Tests/Readers/Docx.hs b/test/Tests/Readers/Docx.hs index f2c05332a..c4b19b6d7 100644 --- a/test/Tests/Readers/Docx.hs +++ b/test/Tests/Readers/Docx.hs @@ -223,6 +223,10 @@ tests = [ testGroup "document" "image inside textbox content with duplicate encoding" "docx/textbox_image_duplicate_encoding.docx" "docx/textbox_image_duplicate_encoding.native" + , testCompare + "image with textbox caption in same paragraph" + "docx/image_with_textbox_caption.docx" + "docx/image_with_textbox_caption.native" ] , testGroup "blocks" [ testCompare @@ -487,6 +491,9 @@ tests = [ testGroup "document" , testMediaBag "image inside textbox content with duplicate encoding populates media bag" "docx/textbox_image_duplicate_encoding.docx" + , testMediaBag + "image with textbox caption in same paragraph populates media bag" + "docx/image_with_textbox_caption.docx" ] , testGroup "custom styles" [ testCompare diff --git a/test/docx/image_with_textbox_caption.docx b/test/docx/image_with_textbox_caption.docx new file mode 100644 index 000000000..6e7da567f Binary files /dev/null and b/test/docx/image_with_textbox_caption.docx differ diff --git a/test/docx/image_with_textbox_caption.native b/test/docx/image_with_textbox_caption.native new file mode 100644 index 000000000..9880537da --- /dev/null +++ b/test/docx/image_with_textbox_caption.native @@ -0,0 +1,44 @@ +[ Figure + ( "" , [] , [] ) + (Caption + Nothing + [ Para + [ Str "1" + , Space + , Str "Daniel" + , Space + , Str "Schliebner:" + , Space + , Str "Cantor'sches" + , Space + , Str "Diagonalverfahren." + , Space + , Str "Von" + , Space + , Str "Mengen," + , Space + , Str "Unendlichkeiten" + , Space + , Str "und" + , Space + , Str "Wahnsinn," + , Space + , Str "Pdf:" + , Space + , Str + "https://www2.informatik.hu-berlin.de/~kossahl/Uni/Ma1/Cantor.pdf" + ] + ]) + [ Plain + [ Image + ( "" + , [] + , [ ( "width" , "2.3680555555555554in" ) + , ( "height" , "0.9340277777777778in" ) + ] + ) + [] + ( "media/image1.emf" , "" ) + ] + ] +]