From c113ca6717d00870ec10716897d76a6fa62b1d41 Mon Sep 17 00:00:00 2001 From: Nikolay Yakimov <root@livid.pp.ru> Date: Sun, 15 Sep 2019 01:40:23 +0300 Subject: [PATCH] [Docx Reader] Use style names, not ids, for assigning semantic meaning Motivating issues: #5523, #5052, #5074 Style name comparisons are case-insensitive, since those are case-insensitive in Word. w:styleId will be used as style name if w:name is missing (this should only happen for malformed docx and is kept as a fallback to avoid failing altogether on malformed documents) Block quote detection code moved from Docx.Parser to Readers.Docx Code styles, i.e. "Source Code" and "Verbatim Char" now honor style inheritance Docx Reader now honours "Compact" style (used in Pandoc-generated docx). The side-effect is that "Compact" style no longer shows up in docx+styles output. Styles inherited from "Compact" will still show up. Removed obsolete list-item style from divsToKeep. That didn't really do anything for a while now. Add newtypes to differentiate between style names, ids, and different style types (that is, paragraph and character styles) Since docx style names can have spaces in them, and pandoc-markdown classes can't, anywhere when style name is used as a class name, spaces are replaced with ASCII dashes `-`. Get rid of extraneous intermediate types, carrying styleId information. Instead, styleId is saved with other style data. Use RunStyle for inline style definitions only (lacking styleId and styleName); for Character Styles use CharStyle type (which is basicaly RunStyle with styleId and StyleName bolted onto it). --- src/Text/Pandoc/Readers/Docx.hs | 162 +++++++------- src/Text/Pandoc/Readers/Docx/Lists.hs | 25 ++- src/Text/Pandoc/Readers/Docx/Parse.hs | 283 ++++++++++++++++--------- test/Tests/Readers/Docx.hs | 9 + test/docx/compact-style-removal.docx | Bin 0 -> 9951 bytes test/docx/compact-style-removal.native | 5 + test/docx/lists-compact.docx | Bin 0 -> 9952 bytes test/docx/lists-compact.native | 5 + 8 files changed, 306 insertions(+), 183 deletions(-) create mode 100644 test/docx/compact-style-removal.docx create mode 100644 test/docx/compact-style-removal.native create mode 100644 test/docx/lists-compact.docx create mode 100644 test/docx/lists-compact.native diff --git a/src/Text/Pandoc/Readers/Docx.hs b/src/Text/Pandoc/Readers/Docx.hs index a26986af2..9d17ab118 100644 --- a/src/Text/Pandoc/Readers/Docx.hs +++ b/src/Text/Pandoc/Readers/Docx.hs @@ -3,6 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE FlexibleContexts #-} {- | Module : Text.Pandoc.Readers.Docx Copyright : Copyright (C) 2014-2019 Jesse Rosenthal @@ -65,6 +66,7 @@ import Control.Monad.State.Strict import qualified Data.ByteString.Lazy as B import Data.Default (Default) import Data.List (delete, intersect) +import Data.Char (isSpace) import qualified Data.Map as M import Data.Maybe (isJust, fromMaybe) import Data.Sequence (ViewL (..), viewl) @@ -133,13 +135,13 @@ evalDocxContext :: PandocMonad m => DocxContext m a -> DEnv -> DState -> m a evalDocxContext ctx env st = flip evalStateT st $ runReaderT ctx env -- This is empty, but we put it in for future-proofing. -spansToKeep :: [String] +spansToKeep :: [CharStyleName] spansToKeep = [] -divsToKeep :: [String] -divsToKeep = ["list-item", "Definition", "DefinitionTerm"] +divsToKeep :: [ParaStyleName] +divsToKeep = ["Definition", "Definition Term"] -metaStyles :: M.Map String String +metaStyles :: M.Map ParaStyleName String metaStyles = M.fromList [ ("Title", "title") , ("Subtitle", "subtitle") , ("Author", "author") @@ -151,7 +153,7 @@ sepBodyParts = span (\bp -> isMetaPar bp || isEmptyPar bp) isMetaPar :: BodyPart -> Bool isMetaPar (Paragraph pPr _) = - not $ null $ intersect (pStyle pPr) (M.keys metaStyles) + not $ null $ intersect (getStyleNames $ pStyle pPr) (M.keys metaStyles) isMetaPar _ = False isEmptyPar :: BodyPart -> Bool @@ -168,7 +170,7 @@ bodyPartsToMeta' :: PandocMonad m => [BodyPart] -> DocxContext m (M.Map String M bodyPartsToMeta' [] = return M.empty bodyPartsToMeta' (bp : bps) | (Paragraph pPr parParts) <- bp - , (c : _)<- (pStyle pPr) `intersect` (M.keys metaStyles) + , (c : _)<- getStyleNames (pStyle pPr) `intersect` M.keys metaStyles , (Just metaField) <- M.lookup c metaStyles = do inlines <- smushInlines <$> mapM parPartToInlines parParts remaining <- bodyPartsToMeta' bps @@ -198,11 +200,29 @@ fixAuthors (MetaBlocks blks) = g _ = MetaInlines [] fixAuthors mv = mv -codeStyles :: [String] -codeStyles = ["VerbatimChar"] +isInheritedFromStyles :: (Eq (StyleName s), HasStyleName s, HasParentStyle s) => [StyleName s] -> s -> Bool +isInheritedFromStyles names sty + | getStyleName sty `elem` names = True + | Just psty <- getParentStyle sty = isInheritedFromStyles names psty + | otherwise = False -codeDivs :: [String] -codeDivs = ["SourceCode"] +hasStylesInheritedFrom :: [ParaStyleName] -> ParagraphStyle -> Bool +hasStylesInheritedFrom ns s = any (isInheritedFromStyles ns) $ pStyle s + +removeStyleNamed :: ParaStyleName -> ParagraphStyle -> ParagraphStyle +removeStyleNamed sn ps = ps{pStyle = filter (\psd -> getStyleName psd /= sn) $ pStyle ps} + +isCodeCharStyle :: CharStyle -> Bool +isCodeCharStyle = isInheritedFromStyles ["Verbatim Char"] + +isCodeDiv :: ParagraphStyle -> Bool +isCodeDiv = hasStylesInheritedFrom ["Source Code"] + +isBlockQuote :: ParStyle -> Bool +isBlockQuote = + isInheritedFromStyles [ + "Quote", "Block Text", "Block Quote", "Block Quotation" + ] runElemToInlines :: RunElem -> Inlines runElemToInlines (TextRun s) = text s @@ -228,57 +248,31 @@ parPartToString (InternalHyperLink _ runs) = concatMap runToString runs parPartToString (ExternalHyperLink _ runs) = concatMap runToString runs parPartToString _ = "" -blacklistedCharStyles :: [String] +blacklistedCharStyles :: [CharStyleName] blacklistedCharStyles = ["Hyperlink"] resolveDependentRunStyle :: PandocMonad m => RunStyle -> DocxContext m RunStyle resolveDependentRunStyle rPr - | Just (s, _) <- rStyle rPr, s `elem` blacklistedCharStyles = + | Just s <- rParentStyle rPr + , getStyleName s `elem` blacklistedCharStyles = return rPr - | Just (_, cs) <- rStyle rPr = do + | Just s <- rParentStyle rPr = do opts <- asks docxOptions if isEnabled Ext_styles opts then return rPr - else do rPr' <- resolveDependentRunStyle cs - return $ - RunStyle { isBold = case isBold rPr of - Just bool -> Just bool - Nothing -> isBold rPr' - , isItalic = case isItalic rPr of - Just bool -> Just bool - Nothing -> isItalic rPr' - , isSmallCaps = case isSmallCaps rPr of - Just bool -> Just bool - Nothing -> isSmallCaps rPr' - , isStrike = case isStrike rPr of - Just bool -> Just bool - Nothing -> isStrike rPr' - , isRTL = case isRTL rPr of - Just bool -> Just bool - Nothing -> isRTL rPr' - , rVertAlign = case rVertAlign rPr of - Just valign -> Just valign - Nothing -> rVertAlign rPr' - , rUnderline = case rUnderline rPr of - Just ulstyle -> Just ulstyle - Nothing -> rUnderline rPr' - , rStyle = rStyle rPr - } + else leftBiasedMergeRunStyle rPr <$> resolveDependentRunStyle (cStyleData s) | otherwise = return rPr runStyleToTransform :: PandocMonad m => RunStyle -> DocxContext m (Inlines -> Inlines) runStyleToTransform rPr - | Just (s, _) <- rStyle rPr - , s `elem` spansToKeep = do - transform <- runStyleToTransform rPr{rStyle = Nothing} - return $ spanWith ("", [s], []) . transform - | Just (s, _) <- rStyle rPr = do - opts <- asks docxOptions - let extraInfo = if isEnabled Ext_styles opts - then spanWith ("", [], [("custom-style", s)]) - else id - transform <- runStyleToTransform rPr{rStyle = Nothing} - return $ extraInfo . transform + | Just sn <- getStyleName <$> rParentStyle rPr + , sn `elem` spansToKeep = do + transform <- runStyleToTransform rPr{rParentStyle = Nothing} + return $ spanWith ("", [normalizeToClassName sn], []) . transform + | Just s <- rParentStyle rPr = do + ei <- extraInfo spanWith s + transform <- runStyleToTransform rPr{rParentStyle = Nothing} + return $ ei . transform | Just True <- isItalic rPr = do transform <- runStyleToTransform rPr{isItalic = Nothing} return $ emph . transform @@ -310,8 +304,7 @@ runStyleToTransform rPr runToInlines :: PandocMonad m => Run -> DocxContext m Inlines runToInlines (Run rs runElems) - | Just (s, _) <- rStyle rs - , s `elem` codeStyles = do + | maybe False isCodeCharStyle $ rParentStyle rs = do rPr <- resolveDependentRunStyle rs let codeString = code $ concatMap runElemToString runElems return $ case rVertAlign rPr of @@ -526,39 +519,49 @@ trimSps (Many ils) = Many $ Seq.dropWhileL isSp $Seq.dropWhileR isSp ils isSp LineBreak = True isSp _ = False +extraInfo :: (Eq (StyleName a), PandocMonad m, HasStyleName a) + => (Attr -> i -> i) -> a -> DocxContext m (i -> i) +extraInfo f s = do + opts <- asks docxOptions + return $ if | isEnabled Ext_styles opts + -> f ("", [], [("custom-style", fromStyleName $ getStyleName s)]) + | otherwise -> id + parStyleToTransform :: PandocMonad m => ParagraphStyle -> DocxContext m (Blocks -> Blocks) parStyleToTransform pPr | (c:cs) <- pStyle pPr - , c `elem` divsToKeep = do + , getStyleName c `elem` divsToKeep = do let pPr' = pPr { pStyle = cs } transform <- parStyleToTransform pPr' - return $ divWith ("", [c], []) . transform + return $ divWith ("", [normalizeToClassName $ getStyleName c], []) . transform | (c:cs) <- pStyle pPr, - c `elem` listParagraphDivs = do + getStyleName c `elem` listParagraphStyles = do let pPr' = pPr { pStyle = cs, indentation = Nothing} transform <- parStyleToTransform pPr' - return $ divWith ("", [c], []) . transform + return $ divWith ("", [normalizeToClassName $ getStyleName c], []) . transform | (c:cs) <- pStyle pPr = do - opts <- asks docxOptions - let pPr' = pPr { pStyle = cs} + let pPr' = pPr { pStyle = cs } transform <- parStyleToTransform pPr' - let extraInfo = if isEnabled Ext_styles opts - then divWith ("", [], [("custom-style", c)]) - else id - return $ extraInfo . (if fromMaybe False (pBlockQuote pPr) then blockQuote else id) . transform + ei <- extraInfo divWith c + return $ ei . (if isBlockQuote c then blockQuote else id) . transform | null (pStyle pPr) , Just left <- indentation pPr >>= leftParIndent = do let pPr' = pPr { indentation = Nothing } hang = fromMaybe 0 $ indentation pPr >>= hangingParIndent transform <- parStyleToTransform pPr' - return $ if (left - hang) > 0 + return $ if (left - hang) > 0 then blockQuote . transform else transform parStyleToTransform _ = return id +normalizeToClassName :: (FromStyleName a) => a -> String +normalizeToClassName = map go . fromStyleName + where go c | isSpace c = '-' + | otherwise = c + bodyPartToBlocks :: PandocMonad m => BodyPart -> DocxContext m Blocks bodyPartToBlocks (Paragraph pPr parparts) - | not $ null $ codeDivs `intersect` (pStyle pPr) = do + | isCodeDiv pPr = do transform <- parStyleToTransform pPr return $ transform $ @@ -568,13 +571,16 @@ bodyPartToBlocks (Paragraph pPr parparts) ils <-local (\s-> s{docxInHeaderBlock=True}) (smushInlines <$> mapM parPartToInlines parparts) makeHeaderAnchor $ - headerWith ("", delete style (pStyle pPr), []) n ils + headerWith ("", map normalizeToClassName . delete style $ getStyleNames (pStyle pPr), []) n ils | otherwise = do ils <- trimSps . smushInlines <$> mapM parPartToInlines parparts prevParaIls <- gets docxPrevPara dropIls <- gets docxDropCap let ils' = dropIls <> ils - if dropCap pPr + let (paraOrPlain, pPr') + | hasStylesInheritedFrom ["Compact"] pPr = (plain, removeStyleNamed "Compact" pPr) + | otherwise = (para, pPr) + if dropCap pPr' then do modify $ \s -> s { docxDropCap = ils' } return mempty else do modify $ \s -> s { docxDropCap = mempty } @@ -583,41 +589,41 @@ bodyPartToBlocks (Paragraph pPr parparts) ils' handleInsertion = do modify $ \s -> s {docxPrevPara = mempty} - transform <- parStyleToTransform pPr - return $ transform $ para ils'' + transform <- parStyleToTransform pPr' + return $ transform $ paraOrPlain ils'' opts <- asks docxOptions if | isNull ils'' && not (isEnabled Ext_empty_paragraphs opts) -> return mempty - | Just (TrackedChange Insertion _) <- pChange pPr + | Just (TrackedChange Insertion _) <- pChange pPr' , AcceptChanges <- readerTrackChanges opts -> handleInsertion - | Just (TrackedChange Insertion _) <- pChange pPr + | Just (TrackedChange Insertion _) <- pChange pPr' , RejectChanges <- readerTrackChanges opts -> do modify $ \s -> s {docxPrevPara = ils''} return mempty - | Just (TrackedChange Insertion cInfo) <- pChange pPr + | Just (TrackedChange Insertion cInfo) <- pChange pPr' , AllChanges <- readerTrackChanges opts , ChangeInfo _ cAuthor cDate <- cInfo -> do let attr = ("", ["paragraph-insertion"], [("author", cAuthor), ("date", cDate)]) insertMark = spanWith attr mempty - transform <- parStyleToTransform pPr + transform <- parStyleToTransform pPr' return $ transform $ - para $ ils'' <> insertMark - | Just (TrackedChange Deletion _) <- pChange pPr + paraOrPlain $ ils'' <> insertMark + | Just (TrackedChange Deletion _) <- pChange pPr' , AcceptChanges <- readerTrackChanges opts -> do modify $ \s -> s {docxPrevPara = ils''} return mempty - | Just (TrackedChange Deletion _) <- pChange pPr + | Just (TrackedChange Deletion _) <- pChange pPr' , RejectChanges <- readerTrackChanges opts -> handleInsertion - | Just (TrackedChange Deletion cInfo) <- pChange pPr + | Just (TrackedChange Deletion cInfo) <- pChange pPr' , AllChanges <- readerTrackChanges opts , ChangeInfo _ cAuthor cDate <- cInfo -> do let attr = ("", ["paragraph-deletion"], [("author", cAuthor), ("date", cDate)]) insertMark = spanWith attr mempty - transform <- parStyleToTransform pPr + transform <- parStyleToTransform pPr' return $ transform $ - para $ ils'' <> insertMark + paraOrPlain $ ils'' <> insertMark | otherwise -> handleInsertion bodyPartToBlocks (ListItem pPr numId lvl (Just levelInfo) parparts) = do -- We check whether this current numId has previously been used, @@ -638,7 +644,7 @@ bodyPartToBlocks (ListItem pPr numId lvl (Just levelInfo) parparts) = do blks <- bodyPartToBlocks (Paragraph pPr parparts) return $ divWith ("", ["list-item"], kvs) blks bodyPartToBlocks (ListItem pPr _ _ _ parparts) = - let pPr' = pPr {pStyle = "ListParagraph": pStyle pPr} + let pPr' = pPr {pStyle = constructBogusParStyleData "list-paragraph": pStyle pPr} in bodyPartToBlocks $ Paragraph pPr' parparts bodyPartToBlocks (Tbl _ _ _ []) = diff --git a/src/Text/Pandoc/Readers/Docx/Lists.hs b/src/Text/Pandoc/Readers/Docx/Lists.hs index cc390f122..eb24640c5 100644 --- a/src/Text/Pandoc/Readers/Docx/Lists.hs +++ b/src/Text/Pandoc/Readers/Docx/Lists.hs @@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Readers.Docx.Lists Copyright : Copyright (C) 2014-2019 Jesse Rosenthal @@ -14,13 +15,16 @@ Functions for converting flat docx paragraphs into nested lists. module Text.Pandoc.Readers.Docx.Lists ( blocksToBullets , blocksToDefinitions , listParagraphDivs + , listParagraphStyles ) where import Prelude import Data.List import Data.Maybe +import Data.String (fromString) import Text.Pandoc.Generic (bottomUp) import Text.Pandoc.JSON +import Text.Pandoc.Readers.Docx.Parse (ParaStyleName) import Text.Pandoc.Shared (trim, safeRead) isListItem :: Block -> Bool @@ -79,7 +83,10 @@ getListType b@(Div (_, _, kvs) _) | isListItem b = getListType _ = Nothing listParagraphDivs :: [String] -listParagraphDivs = ["ListParagraph"] +listParagraphDivs = ["list-paragraph"] + +listParagraphStyles :: [ParaStyleName] +listParagraphStyles = map fromString listParagraphDivs -- This is a first stab at going through and attaching meaning to list -- paragraphs, without an item marker, following a list item. We @@ -160,7 +167,7 @@ blocksToDefinitions' defAcc acc [] = reverse $ DefinitionList (reverse defAcc) : acc blocksToDefinitions' defAcc acc (Div (_, classes1, _) blks1 : Div (ident2, classes2, kvs2) blks2 : blks) - | "DefinitionTerm" `elem` classes1 && "Definition" `elem` classes2 = + | "Definition-Term" `elem` classes1 && "Definition" `elem` classes2 = let remainingAttr2 = (ident2, delete "Definition" classes2, kvs2) pair = if remainingAttr2 == ("", [], []) then (concatMap plainParaInlines blks1, [blks2]) else (concatMap plainParaInlines blks1, [[Div remainingAttr2 blks2]]) in @@ -169,12 +176,12 @@ blocksToDefinitions' ((defTerm, defItems):defs) acc (Div (ident2, classes2, kvs2) blks2 : blks) | "Definition" `elem` classes2 = let remainingAttr2 = (ident2, delete "Definition" classes2, kvs2) - defItems2 = case remainingAttr2 == ("", [], []) of - True -> blks2 - False -> [Div remainingAttr2 blks2] - defAcc' = case null defItems of - True -> (defTerm, [defItems2]) : defs - False -> (defTerm, init defItems ++ [last defItems ++ defItems2]) : defs + defItems2 = if remainingAttr2 == ("", [], []) + then blks2 + else [Div remainingAttr2 blks2] + defAcc' = if null defItems + then (defTerm, [defItems2]) : defs + else (defTerm, init defItems ++ [last defItems ++ defItems2]) : defs in blocksToDefinitions' defAcc' acc blks blocksToDefinitions' [] acc (b:blks) = @@ -198,7 +205,5 @@ removeListDivs' blk = [blk] removeListDivs :: [Block] -> [Block] removeListDivs = concatMap removeListDivs' - - blocksToDefinitions :: [Block] -> [Block] blocksToDefinitions = blocksToDefinitions' [] [] diff --git a/src/Text/Pandoc/Readers/Docx/Parse.hs b/src/Text/Pandoc/Readers/Docx/Parse.hs index 330c9208f..00c5fb0be 100644 --- a/src/Text/Pandoc/Readers/Docx/Parse.hs +++ b/src/Text/Pandoc/Readers/Docx/Parse.hs @@ -1,7 +1,11 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {- | Module : Text.Pandoc.Readers.Docx.Parse Copyright : Copyright (C) 2014-2019 Jesse Rosenthal @@ -31,6 +35,8 @@ module Text.Pandoc.Readers.Docx.Parse ( Docx(..) , VertAlign(..) , ParIndentation(..) , ParagraphStyle(..) + , ParStyle + , CharStyle(cStyleData) , Row(..) , Cell(..) , TrackedChange(..) @@ -38,8 +44,17 @@ module Text.Pandoc.Readers.Docx.Parse ( Docx(..) , ChangeInfo(..) , FieldInfo(..) , Level(..) + , ParaStyleName + , CharStyleName + , FromStyleName(..) + , HasStyleName(..) + , HasParentStyle(..) , archiveToDocx , archiveToDocxWithWarnings + , getStyleNames + , pHeading + , constructBogusParStyleData + , leftBiasedMergeRunStyle ) where import Prelude import Codec.Archive.Zip @@ -49,10 +64,13 @@ import Control.Monad.Reader import Control.Monad.State.Strict import Data.Bits ((.|.)) import qualified Data.ByteString.Lazy as B -import Data.Char (chr, ord, readLitChar) +import Data.Char (chr, ord, readLitChar, toLower) import Data.List +import Data.Function (on) +import Data.String (IsString(..)) import qualified Data.Map as M import Data.Maybe +import Data.Coerce import System.FilePath import Text.Pandoc.Readers.Docx.Util import Text.Pandoc.Readers.Docx.Fields @@ -160,13 +178,9 @@ newtype Body = Body [BodyPart] type Media = [(FilePath, B.ByteString)] -type CharStyle = (String, RunStyle) +type CharStyleMap = M.Map CharStyleId CharStyle -type ParStyle = (String, ParStyleData) - -type CharStyleMap = M.Map String RunStyle - -type ParStyleMap = M.Map String ParStyleData +type ParStyleMap = M.Map ParaStyleId ParStyle data Numbering = Numbering NameSpaces [Numb] [AbstractNumb] deriving Show @@ -213,12 +227,9 @@ data ChangeInfo = ChangeInfo ChangeId Author ChangeDate data TrackedChange = TrackedChange ChangeType ChangeInfo deriving Show -data ParagraphStyle = ParagraphStyle { pStyle :: [String] +data ParagraphStyle = ParagraphStyle { pStyle :: [ParStyle] , indentation :: Maybe ParIndentation , dropCap :: Bool - , pHeading :: Maybe (String, Int) - , pNumInfo :: Maybe (String, String) - , pBlockQuote :: Maybe Bool , pChange :: Maybe TrackedChange } deriving Show @@ -227,9 +238,6 @@ defaultParagraphStyle :: ParagraphStyle defaultParagraphStyle = ParagraphStyle { pStyle = [] , indentation = Nothing , dropCap = False - , pHeading = Nothing - , pNumInfo = Nothing - , pBlockQuote = Nothing , pChange = Nothing } @@ -254,6 +262,49 @@ newtype Row = Row [Cell] newtype Cell = Cell [BodyPart] deriving Show +newtype CharStyleId = CharStyleId { fromCharStyleId :: String } + deriving (Show, Eq, Ord, FromStyleId) +newtype ParaStyleId = ParaStyleId { fromParaStyleId :: String } + deriving (Show, Eq, Ord, FromStyleId) + +newtype CharStyleName = CharStyleName { fromCharStyleName :: CIString } + deriving (Show, Eq, Ord, IsString, FromStyleName) +newtype ParaStyleName = ParaStyleName { fromParaStyleName :: CIString } + deriving (Show, Eq, Ord, IsString, FromStyleName) + +-- Case-insensitive comparisons +newtype CIString = CIString String deriving (Show, IsString, FromStyleName) + +class FromStyleName a where + fromStyleName :: a -> String + +instance FromStyleName String where + fromStyleName = id + +class FromStyleId a where + fromStyleId :: a -> String + +instance FromStyleId String where + fromStyleId = id + +instance Eq CIString where + (==) = (==) `on` map toLower . coerce + +instance Ord CIString where + compare = compare `on` map toLower . coerce + +leftBiasedMergeRunStyle :: RunStyle -> RunStyle -> RunStyle +leftBiasedMergeRunStyle a b = RunStyle + { isBold = isBold a <|> isBold b + , isItalic = isItalic a <|> isItalic b + , isSmallCaps = isSmallCaps a <|> isSmallCaps b + , isStrike = isStrike a <|> isStrike b + , isRTL = isRTL a <|> isRTL b + , rVertAlign = rVertAlign a <|> rVertAlign b + , rUnderline = rUnderline a <|> rUnderline b + , rParentStyle = rParentStyle a + } + -- (width, height) in EMUs type Extent = Maybe (Double, Double) @@ -285,21 +336,28 @@ data RunElem = TextRun String | LnBrk | Tab | SoftHyphen | NoBreakHyphen data VertAlign = BaseLn | SupScrpt | SubScrpt deriving Show -data RunStyle = RunStyle { isBold :: Maybe Bool - , isItalic :: Maybe Bool - , isSmallCaps :: Maybe Bool - , isStrike :: Maybe Bool - , isRTL :: Maybe Bool - , rVertAlign :: Maybe VertAlign - , rUnderline :: Maybe String - , rStyle :: Maybe CharStyle +data CharStyle = CharStyle { cStyleId :: CharStyleId + , cStyleName :: CharStyleName + , cStyleData :: RunStyle + } deriving (Show) + +data RunStyle = RunStyle { isBold :: Maybe Bool + , isItalic :: Maybe Bool + , isSmallCaps :: Maybe Bool + , isStrike :: Maybe Bool + , isRTL :: Maybe Bool + , rVertAlign :: Maybe VertAlign + , rUnderline :: Maybe String + , rParentStyle :: Maybe CharStyle } deriving Show -data ParStyleData = ParStyleData { headingLev :: Maybe (String, Int) - , isBlockQuote :: Maybe Bool - , numInfo :: Maybe (String, String) - , psStyle :: Maybe ParStyle} +data ParStyle = ParStyle { headingLev :: Maybe (ParaStyleName, Int) + , numInfo :: Maybe (String, String) + , psParentStyle :: Maybe ParStyle + , pStyleName :: ParaStyleName + , pStyleId :: ParaStyleId + } deriving Show defaultRunStyle :: RunStyle @@ -310,7 +368,7 @@ defaultRunStyle = RunStyle { isBold = Nothing , isRTL = Nothing , rVertAlign = Nothing , rUnderline = Nothing - , rStyle = Nothing + , rParentStyle = Nothing } type Target = String @@ -390,7 +448,10 @@ elemToBody ns element | isElem ns "w" "body" element = elemToBody _ _ = throwError WrongElem archiveToStyles :: Archive -> (CharStyleMap, ParStyleMap) -archiveToStyles zf = +archiveToStyles = archiveToStyles' getStyleId getStyleId +archiveToStyles' :: (Ord k1, Ord k2, ElemToStyle a1, ElemToStyle a2) => + (a1 -> k1) -> (a2 -> k2) -> Archive -> (M.Map k1 a1, M.Map k2 a2) +archiveToStyles' conv1 conv2 zf = let stylesElem = findEntryByPath "word/styles.xml" zf >>= (parseXMLDoc . UTF8.toStringLazy . fromEntry) in @@ -399,19 +460,17 @@ archiveToStyles zf = Just styElem -> let namespaces = elemToNameSpaces styElem in - ( M.fromList $ buildBasedOnList namespaces styElem - (Nothing :: Maybe CharStyle), - M.fromList $ buildBasedOnList namespaces styElem - (Nothing :: Maybe ParStyle) ) + ( M.fromList $ map (\r -> (conv1 r, r)) $ buildBasedOnList namespaces styElem Nothing, + M.fromList $ map (\p -> (conv2 p, p)) $ buildBasedOnList namespaces styElem Nothing) -isBasedOnStyle :: (ElemToStyle a) => NameSpaces -> Element -> Maybe a -> Bool +isBasedOnStyle :: (ElemToStyle a, FromStyleId (StyleId a)) => NameSpaces -> Element -> Maybe a -> Bool isBasedOnStyle ns element parentStyle | isElem ns "w" "style" element , Just styleType <- findAttrByName ns "w" "type" element , styleType == cStyleType parentStyle , Just basedOnVal <- findChildByName ns "w" "basedOn" element >>= findAttrByName ns "w" "val" - , Just ps <- parentStyle = basedOnVal == getStyleId ps + , Just ps <- parentStyle = basedOnVal == fromStyleId (getStyleId ps) | isElem ns "w" "style" element , Just styleType <- findAttrByName ns "w" "type" element , styleType == cStyleType parentStyle @@ -419,30 +478,70 @@ isBasedOnStyle ns element parentStyle , Nothing <- parentStyle = True | otherwise = False -class ElemToStyle a where +class HasStyleId a => ElemToStyle a where cStyleType :: Maybe a -> String elemToStyle :: NameSpaces -> Element -> Maybe a -> Maybe a - getStyleId :: a -> String + +class FromStyleId (StyleId a) => HasStyleId a where + type StyleId a + getStyleId :: a -> StyleId a + +class FromStyleName (StyleName a) => HasStyleName a where + type StyleName a + getStyleName :: a -> StyleName a + +class HasParentStyle a where + getParentStyle :: a -> Maybe a + +instance HasParentStyle CharStyle where + getParentStyle = rParentStyle . cStyleData + +instance HasParentStyle ParStyle where + getParentStyle = psParentStyle + +getStyleNames :: (Functor t, HasStyleName a) => t a -> t (StyleName a) +getStyleNames = fmap getStyleName + +constructBogusParStyleData :: ParaStyleName -> ParStyle +constructBogusParStyleData stName = ParStyle + { headingLev = Nothing + , numInfo = Nothing + , psParentStyle = Nothing + , pStyleName = stName + , pStyleId = ParaStyleId . filter (/=' ') . fromStyleName $ stName + } instance ElemToStyle CharStyle where cStyleType _ = "character" elemToStyle ns element parentStyle | isElem ns "w" "style" element - , Just "character" <- findAttrByName ns "w" "type" element - , Just styleId <- findAttrByName ns "w" "styleId" element = - Just (styleId, elemToRunStyle ns element parentStyle) + , Just "character" <- findAttrByName ns "w" "type" element = + elemToCharStyle ns element parentStyle | otherwise = Nothing - getStyleId s = fst s + +instance HasStyleId CharStyle where + type StyleId CharStyle = CharStyleId + getStyleId = cStyleId + +instance HasStyleName CharStyle where + type StyleName CharStyle = CharStyleName + getStyleName = cStyleName instance ElemToStyle ParStyle where cStyleType _ = "paragraph" elemToStyle ns element parentStyle | isElem ns "w" "style" element , Just "paragraph" <- findAttrByName ns "w" "type" element - , Just styleId <- findAttrByName ns "w" "styleId" element = - Just (styleId, elemToParStyleData ns element parentStyle) + = elemToParStyleData ns element parentStyle | otherwise = Nothing - getStyleId s = fst s + +instance HasStyleId ParStyle where + type StyleId ParStyle = ParaStyleId + getStyleId = pStyleId + +instance HasStyleName ParStyle where + type StyleName ParStyle = ParaStyleName + getStyleName = pStyleName getStyleChildren :: (ElemToStyle a) => NameSpaces -> Element -> Maybe a -> [a] getStyleChildren ns element parentStyle @@ -693,6 +792,12 @@ testBitMask bitMaskS n = stringToInteger :: String -> Maybe Integer stringToInteger s = listToMaybe $ map fst (reads s :: [(Integer, String)]) +pHeading :: ParagraphStyle -> Maybe (ParaStyleName, Int) +pHeading = getParStyleField headingLev . pStyle + +pNumInfo :: ParagraphStyle -> Maybe (String, String) +pNumInfo = getParStyleField numInfo . pStyle + elemToBodyPart :: NameSpaces -> Element -> D BodyPart elemToBodyPart ns element | isElem ns "w" "p" element @@ -1003,20 +1108,18 @@ elemToRun ns element return $ Run runStyle runElems elemToRun _ _ = throwError WrongElem -getParentStyleValue :: (ParStyleData -> Maybe a) -> ParStyleData -> Maybe a +getParentStyleValue :: (ParStyle -> Maybe a) -> ParStyle -> Maybe a getParentStyleValue field style | Just value <- field style = Just value - | Just parentStyle <- psStyle style - = getParentStyleValue field (snd parentStyle) + | Just parentStyle <- psParentStyle style + = getParentStyleValue field parentStyle getParentStyleValue _ _ = Nothing -getParStyleField :: (ParStyleData -> Maybe a) -> ParStyleMap -> [String] -> - Maybe a -getParStyleField field stylemap styles - | x <- mapMaybe (\x -> M.lookup x stylemap) styles - , (y:_) <- mapMaybe (getParentStyleValue field) x +getParStyleField :: (ParStyle -> Maybe a) -> [ParStyle] -> Maybe a +getParStyleField field styles + | (y:_) <- mapMaybe (getParentStyleValue field) styles = Just y -getParStyleField _ _ _ = Nothing +getParStyleField _ _ = Nothing getTrackedChange :: NameSpaces -> Element -> Maybe TrackedChange getTrackedChange ns element @@ -1038,10 +1141,10 @@ elemToParagraphStyle ns element sty | Just pPr <- findChildByName ns "w" "pPr" element = let style = mapMaybe - (findAttrByName ns "w" "val") + (fmap ParaStyleId . findAttrByName ns "w" "val") (findChildrenByName ns "w" "pStyle" pPr) in ParagraphStyle - {pStyle = style + {pStyle = mapMaybe (`M.lookup` sty) style , indentation = findChildByName ns "w" "ind" pPr >>= elemToParIndentation ns @@ -1053,9 +1156,6 @@ elemToParagraphStyle ns element sty Just "none" -> False Just _ -> True Nothing -> False - , pHeading = getParStyleField headingLev sty style - , pNumInfo = getParStyleField numInfo sty style - , pBlockQuote = getParStyleField isBlockQuote sty style , pChange = findChildByName ns "w" "rPr" pPr >>= filterChild (\e -> isElem ns "w" "ins" e || isElem ns "w" "moveTo" e || @@ -1085,16 +1185,20 @@ elemToRunStyleD :: NameSpaces -> Element -> D RunStyle elemToRunStyleD ns element | Just rPr <- findChildByName ns "w" "rPr" element = do charStyles <- asks envCharStyles - let parentSty = case + let parentSty = findChildByName ns "w" "rStyle" rPr >>= - findAttrByName ns "w" "val" - of - Just styName | Just style <- M.lookup styName charStyles -> - Just (styName, style) - _ -> Nothing + findAttrByName ns "w" "val" >>= + flip M.lookup charStyles . CharStyleId return $ elemToRunStyle ns element parentSty elemToRunStyleD _ _ = return defaultRunStyle +elemToCharStyle :: NameSpaces + -> Element -> Maybe CharStyle -> Maybe CharStyle +elemToCharStyle ns element parentStyle + = CharStyle <$> (CharStyleId <$> findAttrByName ns "w" "styleId" element) + <*> getElementStyleName ns element + <*> (Just $ elemToRunStyle ns element parentStyle) + elemToRunStyle :: NameSpaces -> Element -> Maybe CharStyle -> RunStyle elemToRunStyle ns element parentStyle | Just rPr <- findChildByName ns "w" "rPr" element = @@ -1117,38 +1221,23 @@ elemToRunStyle ns element parentStyle , rUnderline = findChildByName ns "w" "u" rPr >>= findAttrByName ns "w" "val" - , rStyle = parentStyle + , rParentStyle = parentStyle } elemToRunStyle _ _ _ = defaultRunStyle -getHeaderLevel :: NameSpaces -> Element -> Maybe (String,Int) +getHeaderLevel :: NameSpaces -> Element -> Maybe (ParaStyleName, Int) getHeaderLevel ns element - | Just styleId <- findAttrByName ns "w" "styleId" element - , Just index <- stripPrefix "Heading" styleId - , Just n <- stringToInteger index - , n > 0 = Just (styleId, fromInteger n) - | Just styleId <- findAttrByName ns "w" "styleId" element - , Just index <- findChildByName ns "w" "name" element >>= - findAttrByName ns "w" "val" >>= - stripPrefix "heading " - , Just n <- stringToInteger index - , n > 0 = Just (styleId, fromInteger n) + | Just styleName <- getElementStyleName ns element + , Just n <- stringToInteger =<< + (stripPrefix "heading " . map toLower $ + fromStyleName styleName) + , n > 0 = Just (styleName, fromInteger n) getHeaderLevel _ _ = Nothing -blockQuoteStyleIds :: [String] -blockQuoteStyleIds = ["Quote", "BlockQuote", "BlockQuotation"] - -blockQuoteStyleNames :: [String] -blockQuoteStyleNames = ["Quote", "Block Text"] - -getBlockQuote :: NameSpaces -> Element -> Maybe Bool -getBlockQuote ns element - | Just styleId <- findAttrByName ns "w" "styleId" element - , styleId `elem` blockQuoteStyleIds = Just True - | Just styleName <- findChildByName ns "w" "name" element >>= - findAttrByName ns "w" "val" - , styleName `elem` blockQuoteStyleNames = Just True -getBlockQuote _ _ = Nothing +getElementStyleName :: Coercible String a => NameSpaces -> Element -> Maybe a +getElementStyleName ns el = coerce <$> + ((findChildByName ns "w" "name" el >>= findAttrByName ns "w" "val") + <|> findAttrByName ns "w" "styleId" el) getNumInfo :: NameSpaces -> Element -> Maybe (String, String) getNumInfo ns element = do @@ -1163,15 +1252,19 @@ getNumInfo ns element = do return (numId, lvl) -elemToParStyleData :: NameSpaces -> Element -> Maybe ParStyle -> ParStyleData -elemToParStyleData ns element parentStyle = - ParStyleData +elemToParStyleData :: NameSpaces -> Element -> Maybe ParStyle -> Maybe ParStyle +elemToParStyleData ns element parentStyle + | Just styleId <- findAttrByName ns "w" "styleId" element + , Just styleName <- getElementStyleName ns element + = Just $ ParStyle { headingLev = getHeaderLevel ns element - , isBlockQuote = getBlockQuote ns element , numInfo = getNumInfo ns element - , psStyle = parentStyle - } + , psParentStyle = parentStyle + , pStyleName = styleName + , pStyleId = ParaStyleId styleId + } +elemToParStyleData _ _ _ = Nothing elemToRunElem :: NameSpaces -> Element -> D RunElem elemToRunElem ns element diff --git a/test/Tests/Readers/Docx.hs b/test/Tests/Readers/Docx.hs index 9d0913e55..583a6ec18 100644 --- a/test/Tests/Readers/Docx.hs +++ b/test/Tests/Readers/Docx.hs @@ -255,6 +255,10 @@ tests = [ testGroup "document" "lists" "docx/lists.docx" "docx/lists.native" + , testCompare + "compact lists" + "docx/lists-compact.docx" + "docx/lists-compact.native" , testCompare "lists with level overrides" "docx/lists_level_override.docx" @@ -425,6 +429,11 @@ tests = [ testGroup "document" "custom styles (`+styles`) enabled" "docx/custom-style-reference.docx" "docx/custom-style-with-styles.native" + , testCompareWithOpts + def{readerExtensions=extensionsFromList [Ext_styles]} + "custom styles (`+styles`): Compact style is removed from output" + "docx/compact-style-removal.docx" + "docx/compact-style-removal.native" ] , testGroup "metadata" [ testCompareWithOpts def{readerStandalone=True} diff --git a/test/docx/compact-style-removal.docx b/test/docx/compact-style-removal.docx new file mode 100644 index 0000000000000000000000000000000000000000..fde0064db444556ada76e08f52ef035ffcdbc206 GIT binary patch literal 9951 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 WIXFmG{xX>`0jQ8g(@FK~+y4Mtd<LEX literal 0 HcmV?d00001 diff --git a/test/docx/compact-style-removal.native b/test/docx/compact-style-removal.native new file mode 100644 index 000000000..340878ba0 --- /dev/null +++ b/test/docx/compact-style-removal.native @@ -0,0 +1,5 @@ +[OrderedList (1,Decimal,Period) + [[Plain [Str "One"]] + ,[Plain [Str "Two"]] + ,[Plain [Str "Three"]] + ,[Plain [Str "Four"]]]] diff --git a/test/docx/lists-compact.docx b/test/docx/lists-compact.docx new file mode 100644 index 0000000000000000000000000000000000000000..d7f9e4a062590a4ee7d32cded74f4d13832e60c3 GIT binary patch literal 9952 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 TW#uoE2^D|{S~Q(xzrOtsH_<7l literal 0 HcmV?d00001 diff --git a/test/docx/lists-compact.native b/test/docx/lists-compact.native new file mode 100644 index 000000000..340878ba0 --- /dev/null +++ b/test/docx/lists-compact.native @@ -0,0 +1,5 @@ +[OrderedList (1,Decimal,Period) + [[Plain [Str "One"]] + ,[Plain [Str "Two"]] + ,[Plain [Str "Three"]] + ,[Plain [Str "Four"]]]]