-- | Higher level interface for creating styled worksheets {-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -Wall #-} module Codec.Xlsx.Formatted ( FormattedCell(..) , Formatted(..) , formatted -- * Lenses -- ** FormattedCell , formattedAlignment , formattedBorder , formattedFill , formattedFont , formattedNumberFormat , formattedProtection , formattedPivotButton , formattedQuotePrefix , formattedValue , formattedColSpan , formattedRowSpan ) where import Prelude hiding (mapM) import Control.Lens import Control.Monad.State hiding (mapM, forM_) import Data.Default import Data.Foldable (forM_) import Data.List (sortBy) import Data.Map (Map) import Data.Ord (comparing) import Data.Traversable (mapM) import Data.Tuple (swap) import qualified Data.Map as Map import Codec.Xlsx.Types {------------------------------------------------------------------------------- Internal: formatting state -------------------------------------------------------------------------------} data FormattingState = FormattingState { _formattingBorders :: Map Border Int , _formattingCellXfs :: Map CellXf Int , _formattingFills :: Map Fill Int , _formattingFonts :: Map Font Int , _formattingMerges :: [Range] -- ^ In reverse order } makeLenses ''FormattingState stateFromStyleSheet :: StyleSheet -> FormattingState stateFromStyleSheet StyleSheet{..} = FormattingState{ _formattingBorders = fromValueList _styleSheetBorders , _formattingCellXfs = fromValueList _styleSheetCellXfs , _formattingFills = fromValueList _styleSheetFills , _formattingFonts = fromValueList _styleSheetFonts , _formattingMerges = [] } where fromValueList :: Ord a => [a] -> Map a Int fromValueList = Map.fromList . (`zip` [0..]) stateToStyleSheet :: FormattingState -> StyleSheet stateToStyleSheet FormattingState{..} = StyleSheet{ _styleSheetBorders = toList _formattingBorders , _styleSheetCellXfs = toList _formattingCellXfs , _styleSheetFills = toList _formattingFills , _styleSheetFonts = toList _formattingFonts } where toList :: Map a Int -> [a] toList = map snd . sortBy (comparing fst) . map swap . Map.toList getId :: Ord a => Lens' FormattingState (Map a Int) -> a -> State FormattingState Int getId f a = do aMap <- use f case Map.lookup a aMap of Just aId -> return aId Nothing -> do let aId = Map.size aMap f %= Map.insert a aId return aId {------------------------------------------------------------------------------- Cell with formatting -------------------------------------------------------------------------------} -- | Cell with formatting -- -- See 'formatted' for more details. -- -- TODOs: -- -- * Add a number format ('_cellXfApplyNumberFormat', '_cellXfNumFmtId') -- * Add references to the named style sheets ('_cellXfId') data FormattedCell = FormattedCell { _formattedAlignment :: Maybe Alignment , _formattedBorder :: Maybe Border , _formattedFill :: Maybe Fill , _formattedFont :: Maybe Font , _formattedNumberFormat :: Maybe NumberFormat , _formattedProtection :: Maybe Protection , _formattedPivotButton :: Maybe Bool , _formattedQuotePrefix :: Maybe Bool , _formattedValue :: Maybe CellValue , _formattedColSpan :: Int , _formattedRowSpan :: Int } deriving (Show, Eq) makeLenses ''FormattedCell instance Default FormattedCell where def = FormattedCell { _formattedAlignment = Nothing , _formattedBorder = Nothing , _formattedFill = Nothing , _formattedFont = Nothing , _formattedNumberFormat = Nothing , _formattedProtection = Nothing , _formattedPivotButton = Nothing , _formattedQuotePrefix = Nothing , _formattedValue = Nothing , _formattedColSpan = 1 , _formattedRowSpan = 1 } {------------------------------------------------------------------------------- Client-facing API -------------------------------------------------------------------------------} -- | Result of formatting -- -- See 'formatted' data Formatted = Formatted { -- | The final 'CellMap'; see '_wsCells' formattedCellMap :: CellMap -- | The final stylesheet; see '_xlStyles' (and 'renderStyleSheet') , formattedStyleSheet :: StyleSheet -- | The final list of cell merges; see '_wsMerges' , formattedMerges :: [Range] } -- | Higher level API for creating formatted documents -- -- Creating formatted Excel spreadsheets using the 'Cell' datatype directly, -- even with the support for the 'StyleSheet' datatype, is fairly painful. -- This has a number of causes: -- -- * The 'Cell' datatype wants an 'Int' for the style, which is supposed to -- point into the '_styleSheetCellXfs' part of a stylesheet. However, this can -- be difficult to work with, as it requires manual tracking of cell style -- IDs, which in turns requires manual tracking of font IDs, border IDs, etc. -- * Row-span and column-span properties are set on the worksheet as a whole -- ('wsMerges') rather than on individual cells. -- * Excel does not correctly deal with borders on cells that span multiple -- columns or rows. Instead, these rows must be set on all the edge cells -- in the block. Again, this means that this becomes a global property of -- the spreadsheet rather than properties of individual cells. -- -- This function deals with all these problems. Given a map of 'FormattedCell's, -- which refer directly to 'Font's, 'Border's, etc. (rather than font IDs, -- border IDs, etc.), and an initial stylesheet, it recovers all possible -- sharing, constructs IDs, and then constructs the final 'CellMap', as well as -- the final stylesheet and list of merges. -- -- If you don't already have a 'StyleSheet' you want to use as starting point -- then 'minimalStyleSheet' is a good choice. formatted :: Map (Int, Int) FormattedCell -> StyleSheet -> Formatted formatted cs styleSheet = let initSt = stateFromStyleSheet styleSheet (cs', finalSt) = runState (mapM (uncurry formatCell) (Map.toList cs)) initSt styleSheet' = stateToStyleSheet finalSt in Formatted { formattedCellMap = Map.fromList (concat cs') , formattedStyleSheet = styleSheet' , formattedMerges = reverse (finalSt ^. formattingMerges) } -- | Format a cell with (potentially) rowspan or colspan formatCell :: (Int, Int) -> FormattedCell -> State FormattingState [((Int, Int), Cell)] formatCell (row, col) cell = do let (block, mMerge) = cellBlock (row, col) cell forM_ mMerge $ \merge -> formattingMerges %= (:) merge mapM go block where go :: ((Int, Int), FormattedCell) -> State FormattingState ((Int, Int), Cell) go (pos, c) = do styleId <- cellStyleId c return (pos, Cell styleId (_formattedValue c) Nothing) -- | Cell block corresponding to a single 'FormattedCell' -- -- A single 'FormattedCell' might have a colspan or rowspan greater than 1. -- Although Excel obviously supports cell merges, it does not correctly apply -- borders to the cells covered by the rowspan or colspan. Therefore we create -- a block of cells in this function; the top-left is the cell proper, and the -- remaining cells are the cells covered by the rowspan/colspan. -- -- Also returns the cell merge instruction, if any. cellBlock :: (Int, Int) -> FormattedCell -> ([((Int, Int), FormattedCell)], Maybe Range) cellBlock (row, col) cell@FormattedCell{..} = (block, merge) where block :: [((Int, Int), FormattedCell)] block = [ ((row', col'), cellAt (row', col')) | row' <- [topRow .. bottomRow] , col' <- [leftCol .. rightCol] ] merge :: Maybe Range merge = do guard (topRow /= bottomRow || leftCol /= rightCol) return $ mkRange (topRow, leftCol) (bottomRow, rightCol) cellAt :: (Int, Int) -> FormattedCell cellAt (row', col') = if row' == row && col == col' then cell else def & formattedBorder .~ Just (borderAt (row', col')) borderAt :: (Int, Int) -> Border borderAt (row', col') = def & borderTop .~ do guard (row' == topRow) ; _borderTop =<< _formattedBorder & borderBottom .~ do guard (row' == bottomRow) ; _borderBottom =<< _formattedBorder & borderLeft .~ do guard (col' == leftCol) ; _borderLeft =<< _formattedBorder & borderRight .~ do guard (col' == rightCol) ; _borderRight =<< _formattedBorder topRow, bottomRow, leftCol, rightCol :: Int topRow = row bottomRow = row + _formattedRowSpan - 1 leftCol = col rightCol = col + _formattedColSpan - 1 cellStyleId :: FormattedCell -> State FormattingState (Maybe Int) cellStyleId c = mapM (getId formattingCellXfs) =<< cellXf c cellXf :: FormattedCell -> State FormattingState (Maybe CellXf) cellXf FormattedCell{..} = do mBorderId <- getId formattingBorders `mapM` _formattedBorder mFillId <- getId formattingFills `mapM` _formattedFill mFontId <- getId formattingFonts `mapM` _formattedFont let mNumFmtId = fmap numberFormatId _formattedNumberFormat let xf = CellXf { _cellXfApplyAlignment = apply _formattedAlignment , _cellXfApplyBorder = apply mBorderId , _cellXfApplyFill = apply mFillId , _cellXfApplyFont = apply mFontId , _cellXfApplyNumberFormat = apply _formattedNumberFormat , _cellXfApplyProtection = apply _formattedProtection , _cellXfBorderId = mBorderId , _cellXfFillId = mFillId , _cellXfFontId = mFontId , _cellXfNumFmtId = mNumFmtId , _cellXfPivotButton = _formattedPivotButton , _cellXfQuotePrefix = _formattedQuotePrefix , _cellXfId = Nothing -- TODO , _cellXfAlignment = _formattedAlignment , _cellXfProtection = _formattedProtection } return $ if xf == def then Nothing else Just xf where -- If we have formatting instructions, we want to set the corresponding -- applyXXX properties apply :: Maybe a -> Maybe Bool apply Nothing = Nothing apply (Just _) = Just True