From 7389919bb491b78373ea2197800988b3a61cf0ce Mon Sep 17 00:00:00 2001 From: Ben Steinberg Date: Sat, 21 Sep 2019 01:13:29 -0400 Subject: [PATCH] Preserve built-in styles in DOCX with custom style (#5670) This commit prevents custom styles on divs and spans from overriding styles on certain elements inside them, like headings, blockquotes, and links. On those elements, the "native" style is required for the element to display correctly. This change also allows nesting of custom styles; in order to do so, it removes the default "Compact" style applied to Plain blocks, except when inside a table. --- MANUAL.txt | 6 +- src/Text/Pandoc/Writers/Docx.hs | 77 ++++++++++++++------ test/Tests/Writers/Docx.hs | 4 + test/docx/custom-style-preserve.native | 15 ++++ test/docx/golden/custom_style_preserve.docx | Bin 0 -> 10595 bytes test/docx/golden/document-properties.docx | Bin 10358 -> 10350 bytes 6 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 test/docx/custom-style-preserve.native create mode 100644 test/docx/golden/custom_style_preserve.docx diff --git a/MANUAL.txt b/MANUAL.txt index 8b9e02da2..3c9f158ef 100644 --- a/MANUAL.txt +++ b/MANUAL.txt @@ -5255,8 +5255,10 @@ styles, pandoc allows you to define custom styles for blocks and text using `div`s and `span`s, respectively. If you define a `div` or `span` with the attribute `custom-style`, -pandoc will apply your specified style to the contained elements. So, -for example using the `bracketed_spans` syntax, +pandoc will apply your specified style to the contained elements (with +the exception of elements whose function depends on a style, like +headings, code blocks, block quotes, or links). So, for example, using +the `bracketed_spans` syntax, [Get out]{custom-style="Emphatically"}, he said. diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs index b41b17ff9..02db23db5 100644 --- a/src/Text/Pandoc/Writers/Docx.hs +++ b/src/Text/Pandoc/Writers/Docx.hs @@ -81,8 +81,23 @@ listMarkerToId (NumberMarker sty delim n) = OneParen -> '2' TwoParens -> '3' -data WriterEnv = WriterEnv{ envTextProperties :: [Element] - , envParaProperties :: [Element] +data EnvProps = EnvProps{ styleElement :: Maybe Element + , otherElements :: [Element] + } + +instance Semigroup EnvProps where + EnvProps Nothing es <> EnvProps s es' = EnvProps s (es ++ es') + EnvProps s es <> EnvProps _ es' = EnvProps s (es ++ es') + +instance Monoid EnvProps where + mempty = EnvProps Nothing [] + +squashProps :: EnvProps -> [Element] +squashProps (EnvProps Nothing es) = es +squashProps (EnvProps (Just e) es) = e : es + +data WriterEnv = WriterEnv{ envTextProperties :: EnvProps + , envParaProperties :: EnvProps , envRTL :: Bool , envListLevel :: Int , envListNumId :: Int @@ -93,8 +108,8 @@ data WriterEnv = WriterEnv{ envTextProperties :: [Element] } defaultWriterEnv :: WriterEnv -defaultWriterEnv = WriterEnv{ envTextProperties = [] - , envParaProperties = [] +defaultWriterEnv = WriterEnv{ envTextProperties = mempty + , envParaProperties = mempty , envRTL = False , envListLevel = -1 , envListNumId = 1 @@ -115,6 +130,7 @@ data WriterState = WriterState{ , stDelId :: Int , stStyleMaps :: StyleMaps , stFirstPara :: Bool + , stInTable :: Bool , stTocTitle :: [Inline] , stDynamicParaProps :: Set.Set String , stDynamicTextProps :: Set.Set String @@ -133,6 +149,7 @@ defaultWriterState = WriterState{ , stDelId = 1 , stStyleMaps = defaultStyleMaps , stFirstPara = False + , stInTable = False , stTocTitle = [Str "Table of Contents"] , stDynamicParaProps = Set.empty , stDynamicTextProps = Set.empty @@ -496,7 +513,7 @@ writeDocx opts doc@(Pandoc meta _) = do case key' of "description" -> intercalate "_x000d_\n" (map stringify $ lookupMetaBlocks "description" meta') _ -> lookupMetaString key' meta' - + let docProps = mknode "cp:coreProperties" [("xmlns:cp","http://schemas.openxmlformats.org/package/2006/metadata/core-properties") ,("xmlns:dc","http://purl.org/dc/elements/1.1/") @@ -901,8 +918,12 @@ blockToOpenXML' opts (Header lev (ident,_,_) lst) = do $ stSectionIds s } bookmarkedContents <- wrapBookmark bookmarkName contents return [mknode "w:p" [] (paraProps ++ bookmarkedContents)] -blockToOpenXML' opts (Plain lst) = withParaProp (pCustomStyle "Compact") - $ blockToOpenXML opts (Para lst) +blockToOpenXML' opts (Plain lst) = do + isInTable <- gets stInTable + let block = blockToOpenXML opts (Para lst) + para <- if isInTable then withParaProp (pCustomStyle "Compact") block else block + return $ para + -- title beginning with fig: indicates that the image is a figure blockToOpenXML' opts (Para [Image attr alt (src,'f':'i':'g':':':tit)]) = do setFirstPara @@ -910,7 +931,7 @@ blockToOpenXML' opts (Para [Image attr alt (src,'f':'i':'g':':':tit)]) = do if null alt then "Figure" else "CaptionedFigure" - paraProps <- local (\env -> env { envParaProperties = prop : envParaProperties env }) (getParaProps False) + paraProps <- local (\env -> env { envParaProperties = EnvProps (Just prop) [] <> envParaProperties env }) (getParaProps False) contents <- inlinesToOpenXML opts [Image attr alt (src,tit)] captionNode <- withParaProp (pCustomStyle "ImageCaption") $ blockToOpenXML opts (Para alt) @@ -939,7 +960,8 @@ blockToOpenXML' _ b@(RawBlock format str) report $ BlockNotRendered b return [] blockToOpenXML' opts (BlockQuote blocks) = do - p <- withParaPropM (pStyleM "Block Text") $ blocksToOpenXML opts blocks + p <- withParaPropM (pStyleM "Block Text") + $ blocksToOpenXML opts blocks setFirstPara return p blockToOpenXML' opts (CodeBlock attrs@(ident, _, _) str) = do @@ -955,6 +977,7 @@ blockToOpenXML' _ HorizontalRule = do ("o:hrstd","t"),("o:hr","t")] () ] blockToOpenXML' opts (Table caption aligns widths headers rows) = do setFirstPara + modify $ \s -> s { stInTable = True } let captionStr = stringify caption caption' <- if null caption then return [] @@ -990,6 +1013,7 @@ blockToOpenXML' opts (Table caption aligns widths headers rows) = do let mkgridcol w = mknode "w:gridCol" [("w:w", show (floor (textwidth * w) :: Integer))] () let hasHeader = not (all null headers) + modify $ \s -> s { stInTable = False } return $ caption' ++ [mknode "w:tbl" [] @@ -1063,16 +1087,22 @@ withNumId numid = local $ \env -> env{ envListNumId = numid } asList :: (PandocMonad m) => WS m a -> WS m a asList = local $ \env -> env{ envListLevel = envListLevel env + 1 } +isStyle :: Element -> Bool +isStyle e = isElem [] "w" "rStyle" e || + isElem [] "w" "pStyle" e + getTextProps :: (PandocMonad m) => WS m [Element] getTextProps = do props <- asks envTextProperties - return $ if null props + let squashed = squashProps props + return $ if null squashed then [] - else [mknode "w:rPr" [] props] + else [mknode "w:rPr" [] squashed] withTextProp :: PandocMonad m => Element -> WS m a -> WS m a withTextProp d p = - local (\env -> env {envTextProperties = d : envTextProperties env}) p + local (\env -> env {envTextProperties = ep <> envTextProperties env}) p + where ep = if isStyle d then EnvProps (Just d) [] else EnvProps Nothing [d] withTextPropM :: PandocMonad m => WS m Element -> WS m a -> WS m a withTextPropM = (. flip withTextProp) . (>>=) @@ -1085,13 +1115,14 @@ getParaProps displayMathPara = do let listPr = [mknode "w:numPr" [] [ mknode "w:ilvl" [("w:val",show listLevel)] () , mknode "w:numId" [("w:val",show numid)] () ] | listLevel >= 0 && not displayMathPara] - return $ case listPr ++ props of + return $ case listPr ++ squashProps props of [] -> [] ps -> [mknode "w:pPr" [] ps] withParaProp :: PandocMonad m => Element -> WS m a -> WS m a withParaProp d p = - local (\env -> env {envParaProperties = d : envParaProperties env}) p + local (\env -> env {envParaProperties = ep <> envParaProperties env}) p + where ep = if isStyle d then EnvProps (Just d) [] else EnvProps Nothing [d] withParaPropM :: PandocMonad m => WS m Element -> WS m a -> WS m a withParaPropM = (. flip withParaProp) . (>>=) @@ -1264,8 +1295,8 @@ inlineToOpenXML' opts (Note bs) = do insertNoteRef xs = Para [notemarkerXml] : xs contents <- local (\env -> env{ envListLevel = -1 - , envParaProperties = [] - , envTextProperties = [] }) + , envParaProperties = mempty + , envTextProperties = mempty }) (withParaPropM (pStyleM "Footnote Text") $ blocksToOpenXML opts $ insertNoteRef bs) let newnote = mknode "w:footnote" [("w:id", notenum)] contents @@ -1417,16 +1448,18 @@ withDirection x = do -- We want to clean all bidirection (bidi) and right-to-left (rtl) -- properties from the props first. This is because we don't want -- them to stack up. - let paraProps' = filter (\e -> (qName . elName) e /= "bidi") paraProps - textProps' = filter (\e -> (qName . elName) e /= "rtl") textProps + let paraProps' = filter (\e -> (qName . elName) e /= "bidi") (otherElements paraProps) + textProps' = filter (\e -> (qName . elName) e /= "rtl") (otherElements textProps) + paraStyle = styleElement paraProps + textStyle = styleElement textProps if isRTL -- if we are going right-to-left, we (re?)add the properties. then flip local x $ - \env -> env { envParaProperties = mknode "w:bidi" [] () : paraProps' - , envTextProperties = mknode "w:rtl" [] () : textProps' + \env -> env { envParaProperties = EnvProps paraStyle $ mknode "w:bidi" [] () : paraProps' + , envTextProperties = EnvProps textStyle $ mknode "w:rtl" [] () : textProps' } - else flip local x $ \env -> env { envParaProperties = paraProps' - , envTextProperties = textProps' + else flip local x $ \env -> env { envParaProperties = EnvProps paraStyle paraProps' + , envTextProperties = EnvProps textStyle textProps' } wrapBookmark :: (PandocMonad m) => String -> [Element] -> WS m [Element] diff --git a/test/Tests/Writers/Docx.hs b/test/Tests/Writers/Docx.hs index c958ddf7d..9e1414c40 100644 --- a/test/Tests/Writers/Docx.hs +++ b/test/Tests/Writers/Docx.hs @@ -155,6 +155,10 @@ tests = [ testGroup "inlines" 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)" diff --git a/test/docx/custom-style-preserve.native b/test/docx/custom-style-preserve.native new file mode 100644 index 000000000..859f71c20 --- /dev/null +++ b/test/docx/custom-style-preserve.native @@ -0,0 +1,15 @@ +[Para [Span ("",[],[("custom-style","MyStyle")]) [Str "This",Space,Str "span",Note [Para [Str "Neither",Space,Str "footnote",Space,Str "nor",Space,Str "footnote",Space,Str "reference",Space,Str "should",Space,Str "get",Space,Str "a",Space,Str "custom",Space,Str "style",Space,Str "from",Space,Str "its",Space,Str "span."]],Space,Str "should",Space,Str "have",Space,Str "a",Space,Str "custom",Space,Str "style",Space,Str "(",Link ("",[],[]) [Str "link"] ("http://example.com/",""),Str "),"],Space,Str "but",Space,Str "the",Space,Str "text",Space,Str "after",Space,Str "the",Space,Str "comma",Space,Str "shouldn\8217t,",Space,Str "nor",Space,Str "should",Space,Str "the",Space,Str "link."] +,Div ("",[],[("custom-style","MyOtherStyle")]) + [Para [Str "The",Space,Str "contents",Space,Str "of",Space,Str "this",Space,Str "div",Space,Str "should",Space,Str "have",Space,Str "a",Space,Str "custom",Space,Str "style,",Space,Str "but",Space,Link ("",[],[]) [Str "this",Space,Str "link",Space,Str "should",Space,Str "not"] ("http://example.com/",""),Str "."] + ,Header 2 ("this-header-should-not-have-the-divs-custom-style",[],[]) [Str "This",Space,Str "header",Space,Str "should",Space,Str "not",Space,Str "have",Space,Str "the",Space,Str "div\8217s",Space,Str "custom",Space,Str "style"] + ,BlockQuote + [Para [Str "This",Space,Str "blockquote",Space,Str "should",Space,Str "not."]] + ,CodeBlock ("",[],[]) "# This code block should not." + ,Para [Str "But",Space,Str "this",Space,Str "paragraph",Space,Str "should.",Note [Para [Str "Neither",Space,Str "footnote",Space,Str "nor",Space,Str "footnote",Space,Str "reference",Space,Str "should",Space,Str "get",Space,Str "a",Space,Str "custom",Space,Str "style",Space,Str "from",Space,Str "its",Space,Str "div."]]]] +,Div ("",[],[("custom-style","MyOuterStyle")]) + [Div ("",[],[("custom-style","MyInnerStyle")]) + [Para [Str "This",Space,Str "should",Space,Str "have",Space,Str "MyInnerStyle."] + ,Header 3 ("this-heading-should-not",[],[]) [Str "This",Space,Str "heading",Space,Str "should",Space,Str "not"]] + ,Para [Str "This",Space,Str "should",Space,Str "have",Space,Str "MyOuterStyle,",Space,Str "but",Space,Str "the",Space,Str "following",Space,Str "elision",Space,Str "should",Space,Str "have",Space,Str "its",SoftBreak,Str "own",Space,Str "style.",Space,Span ("",[],[("custom-style","Elision")]) [Str "..."]] + ,BlockQuote + [Para [Str "This",Space,Str "blockquote",Space,Str "should",Space,Str "include",Space,Strong [Str "bold",Space,Str "text",Space,Str "with",Space,Str "an",Space,Str "elision:",SoftBreak,Span ("",[],[("custom-style","Elision")]) [Str "..."]]]]]] diff --git a/test/docx/golden/custom_style_preserve.docx b/test/docx/golden/custom_style_preserve.docx new file mode 100644 index 0000000000000000000000000000000000000000..06371d51ed39447c660631e5b4dee268f26e8e5f GIT binary patch literal 10595 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 Rz^