diff --git a/src/Text/Pandoc/Writers/Powerpoint/Output.hs b/src/Text/Pandoc/Writers/Powerpoint/Output.hs index 21d08f1ab..8540527b0 100644 --- a/src/Text/Pandoc/Writers/Powerpoint/Output.hs +++ b/src/Text/Pandoc/Writers/Powerpoint/Output.hs @@ -1365,6 +1365,132 @@ shapesToElements :: PandocMonad m => Element -> [Shape] -> P m [(Maybe ShapeId, shapesToElements layout shps = concat <$> mapM (shapeToElements layout) shps +-- | Create a GraphicFrame element with explicit positioning +graphicFrameToElementsWithPosition :: + PandocMonad m => + Integer -> -- x position + Integer -> -- y position + Integer -> -- width (cx) + Integer -> -- height (cy) + [Graphic] -> + [ParaElem] -> + P m [(ShapeId, Element)] +graphicFrameToElementsWithPosition x y cx cy tbls caption = do + let cy' = if not $ null caption then cy - captionHeight else cy + + elements <- mapM (graphicToElement cx) tbls + let graphicFrameElts = + ( 6 + , mknode "p:graphicFrame" [] $ + [ mknode "p:nvGraphicFramePr" [] + [ mknode "p:cNvPr" [("id", "6"), ("name", "Content Placeholder 5")] () + , mknode "p:cNvGraphicFramePr" [] + [mknode "a:graphicFrameLocks" [("noGrp", "1")] ()] + , mknode "p:nvPr" [] + [mknode "p:ph" [("idx", "1")] ()] + ] + , mknode "p:xfrm" [] + [ mknode "a:off" [("x", tshow $ 12700 * x), + ("y", tshow $ 12700 * y)] () + , mknode "a:ext" [("cx", tshow $ 12700 * cx), + ("cy", tshow $ 12700 * cy')] () + ] + ] <> elements + ) + + if not $ null caption + then do capElt <- createCaption ((x, y), (cx, cy)) caption + return [graphicFrameElts, capElt] + else return [graphicFrameElts] + +-- | Create a TextBox element with explicit positioning +textBoxToElementWithPosition :: + PandocMonad m => + Element -> + Integer -> -- x position + Integer -> -- y position + Integer -> -- width (cx) + Integer -> -- height (cy) + [Paragraph] -> + P m (Maybe ShapeId, Element) +textBoxToElementWithPosition layout x y cx cy paras + | ns <- elemToNameSpaces layout + , Just cSld <- findChild (elemName ns "p" "cSld") layout + , Just spTree <- findChild (elemName ns "p" "spTree") cSld = do + (shapeId, sp) <- getContentShape ns spTree + elements <- mapM paragraphToElement paras + let txBody = mknode "p:txBody" [] $ + [mknode "a:bodyPr" [] (), mknode "a:lstStyle" [] ()] <> elements + -- Create spPr with explicit positioning + xfrm = mknode "a:xfrm" [] + [ mknode "a:off" [("x", tshow $ 12700 * x), + ("y", tshow $ 12700 * y)] () + , mknode "a:ext" [("cx", tshow $ 12700 * cx), + ("cy", tshow $ 12700 * cy)] () + ] + spPr = mknode "p:spPr" [] [xfrm] + return + . (shapeId,) + . surroundWithMathAlternate + . replaceNamedChildren ns "p" "txBody" [txBody] + . replaceNamedChildren ns "p" "spPr" [spPr] + $ sp +textBoxToElementWithPosition _ _ _ _ _ _ = return (Nothing, mknode "p:sp" [] ()) + +-- | Convert shapes to elements with vertical stacking +-- This positions multiple shapes vertically within a content area, +-- preserving the original order of shapes from the source document +shapesToElementsStacked :: + PandocMonad m => + Element -> -- layout + Integer -> -- x position of content area + Integer -> -- y position of content area + Integer -> -- width (cx) of content area + Integer -> -- total height (cy) of content area + [Shape] -> + P m [(Maybe ShapeId, Content)] +shapesToElementsStacked layout x y cx totalCy shapes = do + -- Calculate heights for each shape based on content size + let gap = 10 -- Small gap between elements + + -- Count "units" for each shape based on content + -- Text paragraphs need more space than table rows (bullets have more padding) + shapeUnits :: Shape -> Int + shapeUnits (TextBox paras) = max 2 (length paras * 2) -- 2 units per paragraph + shapeUnits (GraphicFrame tbls _) = + max 2 $ sum [1 + length rows | Tbl _ _ _ rows <- tbls] -- header + data rows + shapeUnits (Pic {}) = 4 -- images get moderate space + shapeUnits (RawOOXMLShape _) = 2 + + -- Calculate total units and height per unit + totalUnits = sum $ map shapeUnits shapes + heightPerUnit :: Double + heightPerUnit = if totalUnits > 0 + then fromIntegral totalCy / fromIntegral totalUnits + else fromIntegral totalCy + + -- Process shapes in order, tracking Y position + let go :: PandocMonad m => Integer -> [Shape] -> P m [(Maybe ShapeId, Content)] + go _ [] = return [] + go currentY (shape:rest) = do + let units = shapeUnits shape + shapeHeight = round $ fromIntegral units * heightPerUnit + heightWithGap = shapeHeight - gap + + elts <- case shape of + GraphicFrame tbls caption -> + map (bimap Just Elem) <$> + graphicFrameToElementsWithPosition x currentY cx heightWithGap tbls caption + TextBox paras -> do + elt <- textBoxToElementWithPosition layout x currentY cx heightWithGap paras + return [(fst elt, Elem (snd elt))] + _ -> return [] -- Skip other shapes for now + + restElts <- go (currentY + shapeHeight) rest + return $ elts ++ restElts + + go y shapes + graphicFrameToElements :: PandocMonad m => Element -> @@ -1566,9 +1692,33 @@ contentToElement layout hdrShape shapes (shapeId, element) <- nonBodyTextToElement layout [PHType "title"] hdrShape let hdrShapeElements = [Elem element | not (null hdrShape)] contentHeaderId = if null hdrShape then Nothing else shapeId - content' <- local - (\env -> env {envPlaceholder = Placeholder ObjType 0}) - (shapesToElements layout shapes) + + -- Check if we have multiple content shapes that need stacking + let hasMultipleShapes = length shapes > 1 + hasGraphicAndText = any isGraphicFrame shapes && any isTextBox shapes + where + isGraphicFrame (GraphicFrame _ _) = True + isGraphicFrame _ = False + isTextBox (TextBox _) = True + isTextBox _ = False + + content' <- if hasMultipleShapes && hasGraphicAndText + then do + -- Get content area dimensions for stacking + master <- getMaster + (pageWidth, pageHeight) <- asks envPresentationSize + ((x, y), (cx, cy)) <- local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (getContentShapeSize ns layout master) + `catchError` + (\_ -> return ((0, 0), (pageWidth, pageHeight))) + -- Use stacked layout for multiple shapes + local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (shapesToElementsStacked layout x y cx cy shapes) + else + -- Use regular layout for single shapes + local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (shapesToElements layout shapes) + let contentContentIds = mapMaybe fst content' contentElements = snd <$> content' footer <- footerElements content @@ -1688,8 +1838,33 @@ contentWithCaptionToElement layout hdrShape textShapes contentShapes (shapesToElements layout textShapes) let contentWithCaptionCaptionIds = mapMaybe fst text textElements = snd <$> text - content <- local (\env -> env {envPlaceholder = Placeholder ObjType 0}) - (shapesToElements layout contentShapes) + + -- Check if we have multiple content shapes that need stacking + let hasMultipleShapes = length contentShapes > 1 + hasGraphicAndText = any isGraphicFrame contentShapes && any isTextBox contentShapes + where + isGraphicFrame (GraphicFrame _ _) = True + isGraphicFrame _ = False + isTextBox (TextBox _) = True + isTextBox _ = False + + content <- if hasMultipleShapes && hasGraphicAndText + then do + -- Get content area dimensions for stacking + master <- getMaster + (pageWidth, pageHeight) <- asks envPresentationSize + ((x, y), (cx, cy)) <- local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (getContentShapeSize ns layout master) + `catchError` + (\_ -> return ((0, 0), (pageWidth, pageHeight))) + -- Use stacked layout for multiple shapes + local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (shapesToElementsStacked layout x y cx cy contentShapes) + else + -- Use regular layout for single shapes + local (\env -> env {envPlaceholder = Placeholder ObjType 0}) + (shapesToElements layout contentShapes) + let contentWithCaptionContentIds = mapMaybe fst content contentElements = snd <$> content footer <- footerElements contentWithCaption diff --git a/src/Text/Pandoc/Writers/Powerpoint/Presentation.hs b/src/Text/Pandoc/Writers/Powerpoint/Presentation.hs index b969c645b..5b3fe7e2e 100644 --- a/src/Text/Pandoc/Writers/Powerpoint/Presentation.hs +++ b/src/Text/Pandoc/Writers/Powerpoint/Presentation.hs @@ -697,12 +697,14 @@ splitBlocks' cur acc (tbl@Table{} : blks) = do let (nts, blks') = span isNotesDiv blks case cur of [Header n _ _] | n == slideLevel || slideLevel == 0 -> - splitBlocks' [] (acc ++ [cur ++ [tbl] ++ nts]) blks' - _ -> splitBlocks' [] - (if any notText cur - then acc ++ ([cur | not (null cur)]) ++ [tbl : nts] - else acc ++ ([cur ++ [tbl] ++ nts])) - blks' + -- Header + table: add to current and continue accumulating + splitBlocks' (cur ++ [tbl] ++ nts) acc blks' + _ -> let (newCur, newAcc) = if any notText cur + -- Current has notText: save current, table starts new + then ([tbl] ++ nts, acc ++ ([cur | not (null cur)])) + -- Current is text-only: add table to current and continue + else (cur ++ [tbl] ++ nts, acc) + in splitBlocks' newCur newAcc blks' splitBlocks' cur acc (d@(Div (_, classes, _) _): blks) | "columns" `elem` classes = do slideLevel <- asks envSlideLevel let (nts, blks') = span isNotesDiv blks @@ -775,13 +777,22 @@ bodyBlocksToSlide _ (blk : blks) spkNotes = do if makesBlankSlide (blk : blks) then pure (mkSlide BlankSlide) else mkSlide . ContentSlide [] <$> blocksToShapes (blk : blks) + -- Check if there's a table in the content + hasTable = any isTable (blk : blks) + where + isTable Table{} = True + isTable _ = False in case break notText (blk : blks) of ([], _) -> contentOrBlankSlide (_, []) -> contentOrBlankSlide - (textBlocks, contentBlocks) -> do - textShapes <- blocksToShapes textBlocks - contentShapes <- blocksToShapes contentBlocks - return (mkSlide (ContentWithCaptionSlide [] textShapes contentShapes)) + (textBlocks, contentBlocks) + -- When there's a table with surrounding text, use ContentSlide + -- to keep everything in one column with proper vertical stacking + | hasTable -> mkSlide . ContentSlide [] <$> blocksToShapes (blk : blks) + | otherwise -> do + textShapes <- blocksToShapes textBlocks + contentShapes <- blocksToShapes contentBlocks + return (mkSlide (ContentWithCaptionSlide [] textShapes contentShapes)) bodyBlocksToSlide _ [] spkNotes = do sldId <- asks envCurSlideId return $ diff --git a/test/Tests/Writers/Powerpoint.hs b/test/Tests/Writers/Powerpoint.hs index c8dc00e6e..575a52f39 100644 --- a/test/Tests/Writers/Powerpoint.hs +++ b/test/Tests/Writers/Powerpoint.hs @@ -78,6 +78,10 @@ tests = let def "pptx/tables/input.native" "pptx/tables/output.pptx" + , pptxTests "table with surrounding content stays on same slide" + def + "pptx/table-with-surrounding-content/input.native" + "pptx/table-with-surrounding-content/output.pptx" , pptxTests "table of contents" def{ writerTableOfContents = True } "pptx/slide-breaks/input.native" diff --git a/test/command/pptx-table-content-same-slide.md b/test/command/pptx-table-content-same-slide.md new file mode 100644 index 000000000..0d9a971f2 --- /dev/null +++ b/test/command/pptx-table-content-same-slide.md @@ -0,0 +1,121 @@ +Test that content after a table stays on the same slide. + +``` +% pandoc -t native +## Slide with Bullets and Table + +- First bullet before table +- Second bullet before table + +| A | B | +|---|---| +| 1 | 2 | + +- Third bullet after table +- Fourth bullet after table +^D +[ Header + 2 + ( "slide-with-bullets-and-table" , [] , [] ) + [ Str "Slide" + , Space + , Str "with" + , Space + , Str "Bullets" + , Space + , Str "and" + , Space + , Str "Table" + ] +, BulletList + [ [ Plain + [ Str "First" + , Space + , Str "bullet" + , Space + , Str "before" + , Space + , Str "table" + ] + ] + , [ Plain + [ Str "Second" + , Space + , Str "bullet" + , Space + , Str "before" + , Space + , Str "table" + ] + ] + ] +, Table + ( "" , [] , [] ) + (Caption Nothing []) + [ ( AlignDefault , ColWidthDefault ) + , ( AlignDefault , ColWidthDefault ) + ] + (TableHead + ( "" , [] , [] ) + [ Row + ( "" , [] , [] ) + [ Cell + ( "" , [] , [] ) + AlignDefault + (RowSpan 1) + (ColSpan 1) + [ Plain [ Str "A" ] ] + , Cell + ( "" , [] , [] ) + AlignDefault + (RowSpan 1) + (ColSpan 1) + [ Plain [ Str "B" ] ] + ] + ]) + [ TableBody + ( "" , [] , [] ) + (RowHeadColumns 0) + [] + [ Row + ( "" , [] , [] ) + [ Cell + ( "" , [] , [] ) + AlignDefault + (RowSpan 1) + (ColSpan 1) + [ Plain [ Str "1" ] ] + , Cell + ( "" , [] , [] ) + AlignDefault + (RowSpan 1) + (ColSpan 1) + [ Plain [ Str "2" ] ] + ] + ] + ] + (TableFoot ( "" , [] , [] ) []) +, BulletList + [ [ Plain + [ Str "Third" + , Space + , Str "bullet" + , Space + , Str "after" + , Space + , Str "table" + ] + ] + , [ Plain + [ Str "Fourth" + , Space + , Str "bullet" + , Space + , Str "after" + , Space + , Str "table" + ] + ] + ] +] +``` diff --git a/test/pptx/comparison/non-text-first/output.pptx b/test/pptx/comparison/non-text-first/output.pptx index 6ac91ca40..d1a65488a 100644 Binary files a/test/pptx/comparison/non-text-first/output.pptx and b/test/pptx/comparison/non-text-first/output.pptx differ diff --git a/test/pptx/comparison/non-text-first/templated.pptx b/test/pptx/comparison/non-text-first/templated.pptx index 2542b0b89..00a7dedec 100644 Binary files a/test/pptx/comparison/non-text-first/templated.pptx and b/test/pptx/comparison/non-text-first/templated.pptx differ diff --git a/test/pptx/footer/basic/output.pptx b/test/pptx/footer/basic/output.pptx index 2704c1d2f..770c5cfc8 100644 Binary files a/test/pptx/footer/basic/output.pptx and b/test/pptx/footer/basic/output.pptx differ diff --git a/test/pptx/footer/fixed-date/output.pptx b/test/pptx/footer/fixed-date/output.pptx index 02a516997..67ce75332 100644 Binary files a/test/pptx/footer/fixed-date/output.pptx and b/test/pptx/footer/fixed-date/output.pptx differ diff --git a/test/pptx/footer/higher-slide-number/output.pptx b/test/pptx/footer/higher-slide-number/output.pptx index 4794808ff..65139cdaf 100644 Binary files a/test/pptx/footer/higher-slide-number/output.pptx and b/test/pptx/footer/higher-slide-number/output.pptx differ diff --git a/test/pptx/footer/no-title-slide/output.pptx b/test/pptx/footer/no-title-slide/output.pptx index 3206fe9de..41f60fc4a 100644 Binary files a/test/pptx/footer/no-title-slide/output.pptx and b/test/pptx/footer/no-title-slide/output.pptx differ diff --git a/test/pptx/slide-level-0/h1-h2-with-table/output.pptx b/test/pptx/slide-level-0/h1-h2-with-table/output.pptx index 254c01fa7..41fa1660a 100644 Binary files a/test/pptx/slide-level-0/h1-h2-with-table/output.pptx and b/test/pptx/slide-level-0/h1-h2-with-table/output.pptx differ diff --git a/test/pptx/slide-level-0/h1-h2-with-table/templated.pptx b/test/pptx/slide-level-0/h1-h2-with-table/templated.pptx index b270d2d4f..056769c6d 100644 Binary files a/test/pptx/slide-level-0/h1-h2-with-table/templated.pptx and b/test/pptx/slide-level-0/h1-h2-with-table/templated.pptx differ diff --git a/test/pptx/table-with-surrounding-content/input.native b/test/pptx/table-with-surrounding-content/input.native new file mode 100644 index 000000000..176cc4d3e --- /dev/null +++ b/test/pptx/table-with-surrounding-content/input.native @@ -0,0 +1,34 @@ +[Header 2 ("slide-with-bullets-and-table",[],[]) [Str "Slide",Space,Str "with",Space,Str "Bullets",Space,Str "and",Space,Str "Table"] +,BulletList + [[Plain [Str "First",Space,Str "bullet",Space,Str "point",Space,Str "before",Space,Str "the",Space,Str "table"]] + ,[Plain [Str "Second",Space,Str "bullet",Space,Str "point",Space,Str "before",Space,Str "the",Space,Str "table"]]] +,Table ("",[],[]) (Caption Nothing []) + [(AlignDefault,ColWidthDefault) + ,(AlignDefault,ColWidthDefault)] + (TableHead ("",[],[]) + [Row ("",[],[]) + [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Column",Space,Str "A"]] + ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Column",Space,Str "B"]]]]) + [(TableBody ("",[],[]) (RowHeadColumns 0) + [] + [Row ("",[],[]) + [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Value",Space,Str "1"]] + ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Value",Space,Str "2"]]] + ,Row ("",[],[]) + [Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Value",Space,Str "3"]] + ,Cell ("",[],[]) AlignDefault (RowSpan 1) (ColSpan 1) + [Plain [Str "Value",Space,Str "4"]]]])] + (TableFoot ("",[],[]) []) +,BulletList + [[Plain [Str "Third",Space,Str "bullet",Space,Str "point",Space,Str "after",Space,Str "the",Space,Str "table"]] + ,[Plain [Str "Fourth",Space,Str "bullet",Space,Str "point",Space,Str "after",Space,Str "the",Space,Str "table"]]] +,Header 2 ("second-slide-for-reference",[],[]) [Str "Second",Space,Str "Slide",Space,Str "for",Space,Str "Reference"] +,BulletList + [[Plain [Str "This",Space,Str "slide",Space,Str "has",Space,Str "just",Space,Str "bullets"]] + ,[Plain [Str "No",Space,Str "table",Space,Str "here"]] + ,[Plain [Str "Everything",Space,Str "stays",Space,Str "together"]]]] diff --git a/test/pptx/table-with-surrounding-content/output.pptx b/test/pptx/table-with-surrounding-content/output.pptx new file mode 100644 index 000000000..fa39dd5be Binary files /dev/null and b/test/pptx/table-with-surrounding-content/output.pptx differ diff --git a/test/pptx/table-with-surrounding-content/templated.pptx b/test/pptx/table-with-surrounding-content/templated.pptx new file mode 100644 index 000000000..f2cd7036e Binary files /dev/null and b/test/pptx/table-with-surrounding-content/templated.pptx differ