From ddbf83f62c8bb6516203c99acd894c404351b5ae Mon Sep 17 00:00:00 2001 From: Albert Krewinkel Date: Sat, 1 May 2021 18:52:24 +0200 Subject: [PATCH] Docx writer: support colspans and rowspans in tables See: #6315 --- src/Text/Pandoc/Writers/Docx.hs | 6 +- src/Text/Pandoc/Writers/Docx/Table.hs | 200 ++++++++++++++------- src/Text/Pandoc/Writers/GridTable.hs | 4 +- test/docx/golden/table_one_row.docx | Bin 9904 -> 9925 bytes test/docx/golden/table_with_list_cell.docx | Bin 10212 -> 10230 bytes test/docx/golden/tables.docx | Bin 10239 -> 10271 bytes 6 files changed, 140 insertions(+), 70 deletions(-) diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index 7064ded09..e11961bfd 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -53,6 +53,7 @@ import Text.Pandoc.Writers.Docx.Table import Text.Pandoc.Writers.Docx.Types import Text.Pandoc.Shared import Text.Pandoc.Walk +import qualified Text.Pandoc.Writers.GridTable as Grid import Text.Pandoc.Writers.Math import Text.Pandoc.Writers.Shared import Text.TeXMath @@ -889,8 +890,9 @@ blockToOpenXML' _ HorizontalRule = do $ mknode "v:rect" [("style","width:0;height:1.5pt"), ("o:hralign","center"), ("o:hrstd","t"),("o:hr","t")] () ] -blockToOpenXML' opts (Table _ blkCapt specs thead tbody tfoot) = - tableToOpenXML (blocksToOpenXML opts) blkCapt specs thead tbody tfoot +blockToOpenXML' opts (Table attr caption colspecs thead tbodies tfoot) = + tableToOpenXML (blocksToOpenXML opts) + (Grid.toTable attr caption colspecs thead tbodies tfoot) blockToOpenXML' opts el | BulletList lst <- el = addOpenXMLList BulletMarker lst | OrderedList (start, numstyle, numdelim) lst <- el diff --git a/src/Text/Pandoc/Writers/Docx/Table.hs b/src/Text/Pandoc/Writers/Docx/Table.hs index 349f3a4ce..bb931bf08 100644 --- a/src/Text/Pandoc/Writers/Docx/Table.hs +++ b/src/Text/Pandoc/Writers/Docx/Table.hs @@ -14,65 +14,39 @@ module Text.Pandoc.Writers.Docx.Table ) where import Control.Monad.State.Strict +import Data.Array import Data.Text (Text) import Text.Pandoc.Definition import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Writers.Docx.Types import Text.Pandoc.Shared -import Text.Pandoc.Writers.Shared import Text.Printf (printf) +import Text.Pandoc.Writers.GridTable hiding (Table) import Text.Pandoc.Writers.OOXML -import Text.Pandoc.XML.Light as XML +import Text.Pandoc.XML.Light as XML hiding (Attr) import qualified Data.Text as T +import qualified Text.Pandoc.Writers.GridTable as Grid tableToOpenXML :: PandocMonad m => ([Block] -> WS m [Content]) - -> Caption - -> [ColSpec] - -> TableHead - -> [TableBody] - -> TableFoot + -> Grid.Table -> WS m [Content] -tableToOpenXML blocksToOpenXML blkCapt specs thead tbody tfoot = do - let (caption, aligns, widths, headers, rows) = - toLegacyTable blkCapt specs thead tbody tfoot +tableToOpenXML blocksToOpenXML gridTable = do setFirstPara modify $ \s -> s { stInTable = True } - let captionStr = stringify caption - caption' <- if null caption - then return [] - else withParaPropM (pStyleM "Table Caption") - $ blocksToOpenXML [Para caption] - let alignmentFor al = mknode "w:jc" [("w:val",alignmentToString al)] () - -- Table cells require a element, even an empty one! - -- Not in the spec but in Word 2007, 2010. See #4953. And - -- apparently the last element must be a , see #6983. - let cellToOpenXML (al, cell) = do - es <- withParaProp (alignmentFor al) $ blocksToOpenXML cell - return $ - case reverse (onlyElems es) of - b:e:_ | qName (elName b) == "bookmarkEnd" - , qName (elName e) == "p" -> es - e:_ | qName (elName e) == "p" -> es - _ -> es ++ [Elem $ mknode "w:p" [] ()] - headers' <- mapM cellToOpenXML $ zip aligns headers - rows' <- mapM (mapM cellToOpenXML . zip aligns) rows - compactStyle <- pStyleM "Compact" - let emptyCell' = [Elem $ mknode "w:p" [] [mknode "w:pPr" [] [compactStyle]]] - let mkcell contents = mknode "w:tc" [] - $ if null contents - then emptyCell' - else contents - let mkrow cells = - mknode "w:tr" [] $ - map mkcell cells - let textwidth = 7920 -- 5.5 in in twips, 1/20 pt - let fullrow = 5000 -- 100% specified in pct - let (rowwidth :: Int) = round $ fullrow * sum widths - let mkgridcol w = mknode "w:gridCol" - [("w:w", tshow (floor (textwidth * w) :: Integer))] () - let hasHeader = not $ all null headers - modify $ \s -> s { stInTable = False } + let (Grid.Table _attr caption colspecs _rowheads thead tbodies tfoot) = + gridTable + let (Caption _maybeShortCaption captionBlocks) = caption + let captionStr = stringify captionBlocks + captionXml <- if null captionBlocks + then return [] + else withParaPropM (pStyleM "Table Caption") + $ blocksToOpenXML captionBlocks + head' <- cellGridToOpenXML blocksToOpenXML thead + bodies <- mapM (cellGridToOpenXML blocksToOpenXML) tbodies + foot' <- cellGridToOpenXML blocksToOpenXML tfoot + + let hasHeader = not . null . indices . partRowAttrs $ thead -- for compatibility with Word <= 2007, we include a val with a bitmask -- 0×0020 Apply first row conditional formatting -- 0×0040 Apply last row conditional formatting @@ -80,18 +54,12 @@ tableToOpenXML blocksToOpenXML blkCapt specs thead tbody tfoot = do -- 0×0100 Apply last column conditional formatting -- 0×0200 Do not apply row banding conditional formatting -- 0×0400 Do not apply column banding conditional formattin - let tblLookVal :: Int - tblLookVal = if hasHeader then 0x20 else 0 - return $ - caption' ++ - [Elem $ - mknode "w:tbl" [] - ( mknode "w:tblPr" [] - ( mknode "w:tblStyle" [("w:val","Table")] () : - mknode "w:tblW" (if all (== 0) widths - then [("w:type", "auto"), ("w:w", "0")] - else [("w:type", "pct"), ("w:w", tshow rowwidth)]) - () : + let tblLookVal = if hasHeader then (0x20 :: Int) else 0 + let (gridCols, tblWattr) = tableLayout (elems colspecs) + let tbl = mknode "w:tbl" [] + ( mknode "w:tblPr" [] + ( mknode "w:tblStyle" [("w:val","Table")] () : + mknode "w:tblW" tblWattr () : mknode "w:tblLook" [("w:firstRow",if hasHeader then "1" else "0") ,("w:lastRow","0") ,("w:firstColumn","0") @@ -100,15 +68,14 @@ tableToOpenXML blocksToOpenXML blkCapt specs thead tbody tfoot = do ,("w:noVBand","0") ,("w:val", T.pack $ printf "%04x" tblLookVal) ] () : - [ mknode "w:tblCaption" [("w:val", captionStr)] () - | not (null caption) ] ) - : mknode "w:tblGrid" [] - (if all (==0) widths - then [] - else map mkgridcol widths) - : [ mkrow headers' | hasHeader ] ++ - map mkrow rows' - )] + [ mknode "w:tblCaption" [("w:val", captionStr)] () + | not (T.null captionStr) ] + ) + : mknode "w:tblGrid" [] gridCols + : head' ++ mconcat bodies ++ foot' + ) + modify $ \s -> s { stInTable = False } + return $ captionXml ++ [Elem tbl] alignmentToString :: Alignment -> Text alignmentToString = \case @@ -116,3 +83,104 @@ alignmentToString = \case AlignRight -> "right" AlignCenter -> "center" AlignDefault -> "left" + +tableLayout :: [ColSpec] -> ([Element], [(Text, Text)]) +tableLayout specs = + let + textwidth = 7920 -- 5.5 in in twips (1 twip == 1/20 pt) + fullrow = 5000 -- 100% specified in pct (1 pct == 1/50th of a percent) + ncols = length specs + getWidth = \case + ColWidth n -> n + _ -> 0 + widths = map (getWidth . snd) specs + rowwidth = round (fullrow * sum widths) :: Int + widthToTwips w = floor (textwidth * w) :: Int + mkGridCol w = mknode "w:gridCol" [("w:w", tshow (widthToTwips w))] () + in if all (== 0) widths + then ( replicate ncols $ mkGridCol (1.0 / fromIntegral ncols) + , [ ("w:type", "auto"), ("w:w", "0")]) + else ( map mkGridCol widths + , [ ("w:type", "pct"), ("w:w", tshow rowwidth) ]) + +cellGridToOpenXML :: PandocMonad m + => ([Block] -> WS m [Content]) + -> Part + -> WS m [Element] +cellGridToOpenXML blocksToOpenXML part@(Part _ _ rowAttrs) = + if null (indices rowAttrs) + then return mempty + else mapM (rowToOpenXML blocksToOpenXML) $ partToRows part + +data OOXMLCell + = OOXMLCell Attr Alignment RowSpan ColSpan [Block] + | OOXMLCellMerge ColSpan + +data OOXMLRow = OOXMLRow Attr [OOXMLCell] + +partToRows :: Part -> [OOXMLRow] +partToRows part = + let + toOOXMLCell :: RowIndex -> ColIndex -> GridCell -> [OOXMLCell] + toOOXMLCell ridx cidx = \case + ContentCell attr align rowspan colspan blocks -> + [OOXMLCell attr align rowspan colspan blocks] + ContinuationCell idx'@(ridx',cidx') | ridx /= ridx', cidx == cidx' -> + case (partCellArray part)!idx' of + (ContentCell _ _ _ colspan _) -> [OOXMLCellMerge colspan] + x -> error $ "Content cell expected, got, " ++ show x ++ + " at index " ++ show idx' + _ -> mempty + mkRow :: (RowIndex, Attr) -> OOXMLRow + mkRow (ridx, attr) = OOXMLRow attr + . concatMap (uncurry $ toOOXMLCell ridx) + . assocs + . rowArray ridx + $ partCellArray part + in map mkRow $ assocs (partRowAttrs part) + +rowToOpenXML :: PandocMonad m + => ([Block] -> WS m [Content]) + -> OOXMLRow + -> WS m Element +rowToOpenXML blocksToOpenXML (OOXMLRow _attr cells) = do + xmlcells <- mapM (ooxmlCellToOpenXML blocksToOpenXML) cells + -- let align' = case align of + -- AlignDefault -> colAlign + -- _ -> align + return $ mknode "w:tr" [] xmlcells + +ooxmlCellToOpenXML :: PandocMonad m + => ([Block] -> WS m [Content]) + -> OOXMLCell + -> WS m Element +ooxmlCellToOpenXML blocksToOpenXML = \case + OOXMLCellMerge (ColSpan colspan) -> do + return $ mknode "w:tc" [] + [ mknode "w:tcPr" [] [ mknode "w:gridSpan" [("w:val", tshow colspan)] () + , mknode "w:vMerge" [("w:val", "continue")] () ] + , mknode "w:p" [] [mknode "w:pPr" [] ()]] + OOXMLCell _attr align rowspan (ColSpan colspan) contents -> do + -- we handle rowspans via 'leftpad', so we can ignore those here + + compactStyle <- pStyleM "Compact" + es <- withParaProp (alignmentFor align) $ blocksToOpenXML contents + -- Table cells require a element, even an empty one! + -- Not in the spec but in Word 2007, 2010. See #4953. And + -- apparently the last element must be a , see #6983. + return . mknode "w:tc" [] $ + Elem + (mknode "w:tcPr" [] ([ mknode "w:gridSpan" [("w:val", tshow colspan)] () + | colspan > 1] ++ + [ mknode "w:vMerge" [("w:val", "restart")] () + | rowspan > RowSpan 1 ])) : + if null contents + then [Elem $ mknode "w:p" [] [mknode "w:pPr" [] [compactStyle]]] + else case reverse (onlyElems es) of + b:e:_ | qName (elName b) == "bookmarkEnd" -- y tho? + , qName (elName e) == "p" -> es + e:_ | qName (elName e) == "p" -> es + _ -> es ++ [Elem $ mknode "w:p" [] ()] + +alignmentFor :: Alignment -> Element +alignmentFor al = mknode "w:jc" [("w:val",alignmentToString al)] () diff --git a/src/Text/Pandoc/Writers/GridTable.hs b/src/Text/Pandoc/Writers/GridTable.hs index c6f4cf456..bc468febc 100644 --- a/src/Text/Pandoc/Writers/GridTable.hs +++ b/src/Text/Pandoc/Writers/GridTable.hs @@ -87,8 +87,8 @@ toTable attr caption colSpecs thead tbodies tfoot = tbGrids = map bodyToGrid tbodies tfGrid = let (TableFoot footAttr rows) = tfoot in rowsToPart footAttr rows - bodyToGrid (TableBody bodyAttr _rowHeadCols _headRows rows) = - rowsToPart bodyAttr rows + bodyToGrid (TableBody bodyAttr _rowHeadCols headRows rows) = + rowsToPart bodyAttr (headRows ++ rows) data BuilderCell = FilledCell GridCell diff --git a/test/docx/golden/table_one_row.docx b/test/docx/golden/table_one_row.docx index cab3fc31c1f02a7d6bb5eab36c73383ce72d01d9..e60bb303f3720a237da77fd680f108755adf5463 100644 GIT binary patch delta 1431 zcmdnsd(@XVz?+#xgn@~JgTbX>;YQxMjLblK^IAq;#(E$l$RX}2=Q2hHhHh2{20;b} zhVuNP6#bO^Rc`t`!7&D)Mk zS#$pVZm}_T7W0ZPn`Sqw8$bU3$Hw^N(Wkq^Hv27@oS;=y6Eb0@flO9rm}q6~Pg8sK z>D}G?)--O9s+X|m{@ttnyKBwf?W#-r*0-j;<5**$c%32ZS#!7cE#_-tjoi@BW88*?(i&HwY)~UJ|T=-$h z9_uzO#c0uQzKfYtzkfY`RPK)RMWd#-#haxf|GQSMeZp?QG9|yTzIyLHxdVo|e|>!> zB~F+Ykk9sdy4Fm0AN})ni;t|)w_c|8>CiN-j0KwC)sOr=*44G)8Z&2{#KA|#_52^q zS)@D9**l*3x-@C`y%~Ghu4ZkXJfDTY{@(l6_cT^>oUhQDo3-CHl7Ic`1m@%` z;)XFRE2nm*^kdeMtFEOyD3s_*E3l<|+}#q<8skBlf0 zy}5?P8XVJ;cd;%7$9NK31tjY4vTx@Ai%b`AhKdNXGID|v15?VQ44KKVh2-kv{SPaM z9Nq7AxSG2pOYBk^|18_gq@>Kdy!V;m)P0Bhx@F%pOmDGC;GO6#S8r*@RBSGMkyG<%Ps#m#@{BNan>{xI$Eia&)}qWpY$QRg4MxyNoQ-JYn?b~MBHr{=$!$~tNHvr{=IN;9rlxg;U* zsN5{B_-UD{HXkznZm|`AX;xk!v1YoWX86_2MJKt9yghGB`Mst_M{fQt(=4x`OLOi# zZ`r2#rElV-iBGS8oU!^JcffM>yDRDi&Npa&{Fp1eR=n(^@DJ<2*@gI_C4Gi9?)E@Ts#ETh5$7SdLcW_&i; zT}4hFWd0ro;Y=~0`QFS73_=LQfvg3STT~RmYUJ~ng>M2y3V{}ZiUtIDHu;E(0=W19 zrx>6)?igy`0gVNgBLPME1;zTw`9-Oq!UWCLL5wMWZYen6AKD3 zb;~XclB!hg?Eosi0>r{7+D}hzP*no^!geax7D+Y+hF`J_45BD%S|%S8HJ=4;lilY!Om;(BN{ks!X}jrJ*uC3xf{uqf8Jja|`*egh%=($|h&`9lyZbK_)T6`xG=Ze_o%=j{2rdViGJ8#@!E!2pW9Y8?b)!DODdylFZ=%Z=sVzCCt>EsKnOTlql!Bzo@{i*yjKXsDXY&pj2(-kjezGkT(pVJ6(H=G@YGu&MsIO@^ zxg(C)Z`#hgr`DHg;lv$V)Hx**mm1w>{TZ?Tl4azz zsRcd}{Q38+KCDns*?5*k?M~Ah0olbS9kLm6n_d_M-icUmdF)dD%Ew9Pw>zA+nIFh| zLbcxA&;P&`UY8;_FGGd9{rOf6A|g^35)3XVX}`GI{h)Nm%?q0U=R`fcc+mH?tIVDD z1#HQ;w?$gNTw_@8xIZrb-tNHQRHh>3hAUG=ctp(t!oTe5(SF#t`0L+q0Y`4%IlOUe zT-%DCRi9nCZ}LyeRo?vYE0GSSDLpnkk=U@cBukgEjD?9nE2#(sytw!zd!}MCrhe9 koMNjc&Db_MQVn8MjhZym0maE56~!j6R}*0CQv%rs00xrgVgLXD diff --git a/test/docx/golden/table_with_list_cell.docx b/test/docx/golden/table_with_list_cell.docx index 9238c7e20dfe42a54db07aaffee8028264bf6975..a4037cf3218208715415fc48b1c67f8371b7557c 100644 GIT binary patch delta 1297 zcmaFj|IME_z?+#xgn@~JgTbX>;YQxMjLblK^IAq;#(E$l$lN!u=QAS%!&eRl20;b} zhVuNP6#bO^iA+1t~b3J)Ed z{@_;LzrPz8%O~zKJNs#Z_=9_=E$3A0i|yM}H9_h1jMA9ZT%w<4Qhfqri?^=4@b_-} z&FGS?d)}Omx?CS-#5(i-`k0&H8Qck8J+gO=JnzIvFRNK|F-+*gvbzuBZr_UVomo83 zk896~&)hnk;To4tIA@exE;yC9sKJHlwo6jK>P8=d50$%h&Gk0EH-&9=!!+g)?VrA1S~=I>f^`G$N%~D@yiMeH^msHIdU?8esFL8!;F$_HlJa!2B(|JTx?6h>1PvL3=^0k z$hn;Z%s4IR3`tso!mNy(prpl=@+d=Qvb=~~eZ2o+1(Bosy$)A%mt=`uD&wDJo0*i< zd(51vTsHEju*2@!>d4)tdN*h1$DG?~e~xw4)wu6B`F|`}vr{t#kPTib%VGk8<_D&_5>t+kIS1ggG44XDxU?@ z7nSY6w2+D!FG`l49I9djmg!QFX1c^O`8JE3PJlNf69WUhKnQBN*`u5b%=a}wEDOX4 zAiy^Hqlz|Izr3n6(+;-D2iZg>2dMIZh2mAE8O0}8tIC0SbAY^ylXs{pf_Znqaz9iR zkdlF-nkb|EWOFqIu!<-(X~wL{Woq*BAO}lTs`hpOExrQ8!UzWdS$`&PQBwlzxTz-1 zv{iiaesS^1yy_4q$f!#*G0IK0REL-uqb|*OW^x6THybF)raW0dS#0t-bpf_3${=$A DOiCID delta 1301 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