xlsx 0.1.2 → 1.2.0
raw patch · 72 files changed
Files
- CHANGELOG.markdown +245/−0
- benchmarks/Main.hs +48/−0
- changelog +0/−23
- data/absolute_relationships.xlsx binary
- data/floats.xlsx binary
- data/inline-strings.xlsx binary
- src/Codec/Xlsx.hs +2/−2
- src/Codec/Xlsx/Formatted.hs +444/−0
- src/Codec/Xlsx/Lens.hs +64/−28
- src/Codec/Xlsx/LensCompat.hs +40/−0
- src/Codec/Xlsx/Parser.hs +678/−141
- src/Codec/Xlsx/Parser/Internal.hs +150/−14
- src/Codec/Xlsx/Parser/Internal/Fast.hs +360/−0
- src/Codec/Xlsx/Parser/Internal/Memoize.hs +53/−0
- src/Codec/Xlsx/Parser/Internal/PivotTable.hs +109/−0
- src/Codec/Xlsx/Parser/Internal/Util.hs +43/−0
- src/Codec/Xlsx/Parser/Stream.hs +718/−0
- src/Codec/Xlsx/Parser/Stream/HexpatInternal.hs +95/−0
- src/Codec/Xlsx/Parser/Stream/Row.hs +23/−0
- src/Codec/Xlsx/Parser/Stream/SheetIdentifier.hs +20/−0
- src/Codec/Xlsx/Parser/Stream/SheetIndex.hs +20/−0
- src/Codec/Xlsx/Parser/Stream/SheetInfo.hs +27/−0
- src/Codec/Xlsx/Parser/Stream/SheetItem.hs +26/−0
- src/Codec/Xlsx/Types.hs +337/−78
- src/Codec/Xlsx/Types/AutoFilter.hs +742/−0
- src/Codec/Xlsx/Types/Cell.hs +174/−0
- src/Codec/Xlsx/Types/Comment.hs +24/−0
- src/Codec/Xlsx/Types/Common.hs +713/−0
- src/Codec/Xlsx/Types/ConditionalFormatting.hs +973/−0
- src/Codec/Xlsx/Types/DataValidation.hs +353/−0
- src/Codec/Xlsx/Types/Drawing.hs +500/−0
- src/Codec/Xlsx/Types/Drawing/Chart.hs +828/−0
- src/Codec/Xlsx/Types/Drawing/Common.hs +619/−0
- src/Codec/Xlsx/Types/Internal.hs +3/−0
- src/Codec/Xlsx/Types/Internal/CfPair.hs +37/−0
- src/Codec/Xlsx/Types/Internal/CommentTable.hs +128/−0
- src/Codec/Xlsx/Types/Internal/ContentTypes.hs +74/−0
- src/Codec/Xlsx/Types/Internal/CustomProperties.hs +82/−0
- src/Codec/Xlsx/Types/Internal/DvPair.hs +37/−0
- src/Codec/Xlsx/Types/Internal/FormulaData.hs +54/−0
- src/Codec/Xlsx/Types/Internal/Relationships.hs +146/−0
- src/Codec/Xlsx/Types/Internal/SharedStringTable.hs +121/−0
- src/Codec/Xlsx/Types/PageSetup.hs +670/−0
- src/Codec/Xlsx/Types/PivotTable.hs +164/−0
- src/Codec/Xlsx/Types/PivotTable/Internal.hs +102/−0
- src/Codec/Xlsx/Types/Protection.hs +269/−0
- src/Codec/Xlsx/Types/RefId.hs +25/−0
- src/Codec/Xlsx/Types/RichText.hs +375/−0
- src/Codec/Xlsx/Types/SheetViews.hs +576/−0
- src/Codec/Xlsx/Types/StyleSheet.hs +1697/−0
- src/Codec/Xlsx/Types/Table.hs +121/−0
- src/Codec/Xlsx/Types/Variant.hs +81/−0
- src/Codec/Xlsx/Writer.hs +528/−172
- src/Codec/Xlsx/Writer/Internal.hs +227/−0
- src/Codec/Xlsx/Writer/Internal/PivotTable.hs +204/−0
- src/Codec/Xlsx/Writer/Internal/Stream.hs +46/−0
- src/Codec/Xlsx/Writer/Stream.hs +429/−0
- src/Test.hs +0/−104
- test/AutoFilterTests.hs +25/−0
- test/Common.hs +25/−0
- test/CommonTests.hs +66/−0
- test/CommonTests/CellRefTests.hs +182/−0
- test/CondFmtTests.hs +24/−0
- test/DataTest.hs +0/−68
- test/Diff.hs +14/−0
- test/DrawingTests.hs +398/−0
- test/Main.hs +154/−0
- test/PivotTableTests.hs +197/−0
- test/StreamTests.hs +360/−0
- test/Test/SmallCheck/Series/Instances.hs +127/−0
- test/TestXlsx.hs +599/−0
- xlsx.cabal +172/−66
+ CHANGELOG.markdown view
@@ -0,0 +1,245 @@+1.2.0+------------+* expose sheet state in stream parser+ (thanks to Benjamin McRae <benjamin@supercede.com>)+* drop unnecessary `n` namespace from stream writer+ (thanks to Michael Schneider <michael@m1-s.com>)+* add support support for writing multiple sheets with stream writer+ (thanks to Michael Schneider <michael@m1-s.com>)++1.1.4+------------+* fix the problem with leading slashes for comments, drawings and pivot tables+ (thanks to mindston <mindston@eml.cc>)++1.1.3+------------+* fixed compatibility with xml-conduit moving `rsPretty` into internal module+* dropped support for GHC 9.0.* and added support for GHC 9.8.*++1.1.2+------------+* Strip leading slash from target paths in relations as the ECMA-376 spec requires+ (thanks to Luke Clifton <lukec@themk.net>)++1.1.1+------------+* dropped support for GHC 8.8.* and 8.10.* and added support for GHC 9.4.* and 9.6.*++1.1.0+------------+* Fix default cell type in streaming parser+ (thanks to Nikita Razmakhnin <nikita@supercede.com>)+* Implemented cell range data validation+ (thanks to Florian Fouratier <6524406+flhorizon@users.noreply.github.com>)+* Added support for sheet visibility+ (thanks to Florian Fouratier <6524406+flhorizon@users.noreply.github.com>)+* Added parsing of comment visibility+ (thanks to Luke <luke@supercede.com>)+* Added newtypes for column and row indices+ (thanks to Luke <luke@supercede.com>)++1.0.0+------------+* Add support for streaming xlsx files+* dropped support for GHC 8.4.* and 8.6.* and added support for GHC 9.2.*+* Treat 1900 as a leap year like Excel does++0.8.4+------------+* dropped support for GHC 8.0.* and 8.2.* and added support for GHC 8.10.* and 9.0.*++0.8.3+------------+* compatibility with lens-5.0+* don't output lists with no elements in stylesheet as it causes problems in+ Excel+ (thanks to David Hewson <david.hewson@tracsis.com>)++0.8.2+------+* added a flag allowing to use `microlens` instead of `lens`+ (thanks to Samuel Balco <goodlyrottenapple@gmail.com>)++0.8.1+------+* compatibility with smallcheck-1.2.0++0.8.0+------+* GHC 8.8 compatibility added (GHC 8.6 didn't need any updates). Dropped+ compatilibity with GHC 7.10+ (thanks to David Hewson <david.hewson@tracsis.com>)++0.7.2+-----+* GHC 8.4 compatibility++0.7.1+-----+* improved compatibility with Excel in pivot cache serialization+* added support for character references in fast parsing with `xeno`++0.7.0+-----+* fixed serialization of large integer values (thanks Radoslav Dorcik+ <dixiecko@gmail.com>)+* added fast xlsx parsing using `xeno` library+* dropped support for GHC 7.8.4 and added support for GHC 8.2.2+* added numer format support in differential formatting records+ (thanks Emil Axelsson <emax@chalmers.se>)+* added `inlineStr` cell type support+* added shared formulas support+* added error values support+* helper functions for serialization/deserialization of date values+ (thanks José Romildo Malaquias <malaquias@gmail.com>)++0.6.0+-----+* fixed reading files with optional table name (thanks Aleksey Khudyakov+ <alexey.skladnoy@gmail.com> for reporting)+* removed unnecessary 10cm offset from `simpleAnchorXY`+* `customRowHeight` added to row properties (thanks Aleksey Khudyakov+ <alexey.skladnoy@gmail.com>)+* added `Generic` instances for library types (thanks Remy Goldschmidt+ <taktoa@gmail.com>)+* `hidden` property added for rows (thanks Aleksey Khudyakov+ <alexey.skladnoy@gmail.com>)++0.5.0+-----+* renamed `ColumnsWidth` to more intuitive `ColumnsProperties` and+ added some more fields to it+* added pivot table field sorting and hidden values support+* added support for 4 more chart types++0.4.3+-----+* added (legacy) sheet protection support+* switched to use `r` prefix for relationships namespace in+ workbook.xml to improve compatibility with readers expecting that+ prefix (thanks Stéphane Laurent <laurent_step@yahoo.fr> for+ reporting)+* fixed parsing cells with comments but with no content (thanks+ Stéphane Laurent <laurent_step@yahoo.fr> for reporting)+* added some higher-level helpers work with pictures in SpreadsheetML+ Drawing++0.4.2+-----+* added basic tables support+* fixed boolean element parsing for rich text run properties (thanks+ laurent stephane <laurent_step@yahoo.fr> for reporting)+* fixed problem of `cwStyle` not being optional (thanks laurent+ stephane <laurent_step@yahoo.fr> for reporting)+* added basic autofilter support++0.4.1+-----+* fixed serialization problem of empty validations and pivot caches+ (thanks laurent stephane <laurent_step@yahoo.fr> for reporting)++0.4.0+-----+* implemented basic charts support with only line charts currently+* added data validation support (thanks Emil Axelsson <emax@chalmers.se>)+* fixed reading comments with empty author names (thanks Aleksey+ Khudyakov <alexey.skladnoy@gmail.com> for reporting)+* implemented basic pivot table support+* started using `hindent` to format library code++0.3.0+-----+* implemented number formats+* fixed error of parsing "Default"s in content types (thanks Steve Bigham <steve.bigham@gmail.com> for reporting)+* fixed parsing workbooks with no shared strings (thanks Steve Bigham <steve.bigham@gmail.com> for reporting)+* changed the way sheets are stored to allow abitrary sheet order in a workbook+* separated format information from other cell data in `FormattedCell`+* implemented comment visibility (throught legacy vml drawings)++0.2.4+-----+* added basic images support+* added "normal" cell formula support+* added basic xlsx parsing error reporting (thanks to Brad Ediger <brad.ediger@madriska.com>)++0.2.3+-----+* added conditional formatting support+* fixed reading empty <font> subelements with defaults (thanks Steve Bigham <steve.bigham@gmail.com>)++0.2.2.2+-----+* fixed missing from parsing code font family value 0 (Not applicable) (thanks Steve Bigham <steve.bigham@gmail.com>)+* fixed time type used in haddock example (thanks Manoj <manoj.p.gudi@gmail.com>)++0.2.2.1+-----+* fixed comments data type names and modules/imports++0.2.2+-----+* added cell comments support+* added custom file properties++0.2.1.2+-------+* loosened dependency on data-default package++0.2.1.1+-------+* fixed parsing shared string table entries with no content (thanks to Yuji Yamamoto <whosekiteneverfly@gmail.com>)++0.2.1+-------+* added number formats (thanks to Alan Zimmerman <alan.zimm@gmail.com>)+* loosened dependency on zip-archive package++0.2.0+-----+* added style sheet support (thanks to Edsko de Vries <edsko@well-typed.com>)+* added high level interface for styling (thanks to Edsko de Vries <edsko@well-typed.com>)+* added sheet views support (thanks to Edsko de Vries <edsko@well-typed.com>)+* added page setup support (thanks to Edsko de Vries <edsko@well-typed.com>)+* switched from `System.Time` to `Data.Time`+* added rich text support (thanks to Edsko de Vries <edsko@well-typed.com>) including shared strings+* added a bit better internals for rendering (thanks to Edsko de Vries <edsko@well-typed.com>) and parsing++0.1.2+-----+* added lenses to access cells both using RC and XY style coordinates, RC is used by default++0.1.1.1+-------+* fixed use of internal function for parsing shared strings, previous change was unused in practice++0.1.1+-----+* added support for rich text shared strings (thanks to Steve Bigham <steve.bigham@gmail.com>)++0.1.0.5+-------+* loosened dependency on zlib package++0.1.0.4+-------+* fixed generated xml so it gets read by MS Excel with no warnings (thanks Dmitriy Nikitinskiy <nikitinskiy@gmail.com>)+* improved shared strings collection by using Vector (thanks Dmitriy Nikitinskiy <nikitinskiy@gmail.com>)+* empty xml elements don't get emmitted anymore (thanks Philipp Hausmann <nikitinskiy@gmail.com>)+* imporoved and cleaned up core.xml generation++0.1.0.3+-------+* added "str" cells content extraction as text+* added a notice that formulas in <f> are not yet supported++0.1.0+-----+* better tests and documentation+* lenses for worksheets/cells+* removed streaming support as it needs better API and improved implementaion+* removed CellLocalTime as ambiguous, added CellBool++0.0.1+-----+* initial release
+ benchmarks/Main.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}+module Main (main) where++import Codec.Xlsx+import Codec.Xlsx.Parser.Stream+import Codec.Xlsx.Writer.Stream+import Control.DeepSeq+import Control.Lens+import Control.Monad (void)+import Criterion.Main+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LB+import qualified Data.Conduit as C+import qualified Data.Conduit.Combinators as C+import Data.Maybe++main :: IO ()+main = do+ let filename = "data/testInput.xlsx"+ -- "data/6000.rows.x.26.cols.xlsx"+ bs <- BS.readFile filename+ let bs' = LB.fromStrict bs+ parsed :: Xlsx+ parsed = toXlsxFast bs'+ idx <- fmap (fromMaybe (error "ix not found")) $ runXlsxM filename $ makeIndexFromName "Sample list"+ items <- runXlsxM filename $ collectItems idx+ deepseq (parsed, bs', idx, items) (pure ())+ defaultMain+ [ bgroup+ "readFile"+ [ bench "with xlsx" $ nf toXlsx bs'+ , bench "with xlsx fast" $ nf toXlsxFast bs'+ , bench "with stream (counting)" $ nfIO $ runXlsxM filename $ countRowsInSheet idx+ , bench "with stream (reading)" $ nfIO $ runXlsxM filename $ readSheet idx (pure . rwhnf)+ ]+ , bgroup+ "writeFile"+ [ bench "with xlsx" $ nf (fromXlsx 0) parsed+ , bench "with stream (no sst)" $+ nfIO $ C.runConduit $+ void (writeXlsxWithSharedStrings defaultSettings mempty $ C.yieldMany $ view si_row <$> items)+ C..| C.fold+ , bench "with stream (sst)" $+ nfIO $ C.runConduit $+ void (writeXlsx defaultSettings $ C.yieldMany $ view si_row <$> items)+ C..| C.fold+ ]+ ]
− changelog
@@ -1,23 +0,0 @@-0.1.2- * added lenses to access cells both using RC and XY style coordinates, RC is used by default-0.1.1.1- * fixed use of internal function for parsing shared strings, previous change was unused in practice-0.1.1- * added support for rich text shared strings (thanks to Steve Bigham <steve.bigham@gmail.com>)-0.1.0.5- * loosened dependency on zlib package-0.1.0.4- * fixed generated xml so it gets read by MS Excel with no warnings (thanks Dmitriy Nikitinskiy <nick@linux-i294.site>)- * improved shared strings collection by using Vector (thanks Dmitriy Nikitinskiy <nick@linux-i294.site>)- * empty xml elements don't get emmitted anymore (thanks Philipp Hausmann <ph_git@314.ch>)- * imporoved and cleaned up core.xml generation-0.1.0.3- * added "str" cells content extraction as text- * added a notice that formulas in <f> are not yet supported-0.1.0- * better tests and documentation- * lenses for worksheets/cells- * removed streaming support as it needs better API and improved implementaion- * removed CellLocalTime as ambiguous, added CellBool-0.0.1- * initial release
+ data/absolute_relationships.xlsx view
binary file changed (absent → 6869 bytes)
+ data/floats.xlsx view
binary file changed (absent → 8455 bytes)
+ data/inline-strings.xlsx view
binary file changed (absent → 8695 bytes)
src/Codec/Xlsx.hs view
@@ -24,11 +24,11 @@ -- > import Codec.Xlsx -- > import Control.Lens -- > import qualified Data.ByteString.Lazy as L--- > import System.Time+-- > import Data.Time.Clock.POSIX -- > -- > main :: IO () -- > main = do--- > ct <- getClockTime+-- > ct <- getPOSIXTime -- > let -- > sheet = def & cellValueAt (1,2) ?~ CellDouble 42.0 -- > & cellValueAt (3,2) ?~ CellText "foo"
+ src/Codec/Xlsx/Formatted.hs view
@@ -0,0 +1,444 @@+-- | Higher level interface for creating styled worksheets+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Formatted+ ( FormattedCell(..)+ , Formatted(..)+ , Format(..)+ , formatted+ , formatWorkbook+ , toFormattedCells+ , CondFormatted(..)+ , conditionallyFormatted+ -- * Lenses+ -- ** Format+ , formatAlignment+ , formatBorder+ , formatFill+ , formatFont+ , formatNumberFormat+ , formatProtection+ , formatPivotButton+ , formatQuotePrefix+ -- ** FormattedCell+ , formattedCell+ , formattedFormat+ , formattedColSpan+ , formattedRowSpan+ -- ** FormattedCondFmt+ , condfmtCondition+ , condfmtDxf+ , condfmtPriority+ , condfmtStopIfTrue+ ) where++import Codec.Xlsx.LensCompat+import Control.Monad (forM, guard)+import Control.Monad.State hiding (forM_, mapM)+import Data.Default+import Data.Foldable (asum, forM_)+import Data.Function (on)+import Data.List (foldl', groupBy, sortBy, sortBy)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Ord (comparing)+import Data.Text (Text)+import Data.Traversable (mapM)+import Data.Tuple (swap)+import GHC.Generics (Generic)+import Prelude hiding (mapM)+import Safe (headNote, fromJustNote)++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+ , _formattingNumFmts :: Map Text 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+ , _formattingNumFmts = M.fromList . map swap $ M.toList _styleSheetNumFmts+ , _formattingMerges = []+ }++fromValueList :: Ord a => [a] -> Map a Int+fromValueList = M.fromList . (`zip` [0..])++toValueList :: Map a Int -> [a]+toValueList = map snd . sortBy (comparing fst) . map swap . M.toList++updateStyleSheetFromState :: StyleSheet -> FormattingState -> StyleSheet+updateStyleSheetFromState sSheet FormattingState{..} = sSheet+ { _styleSheetBorders = toValueList _formattingBorders+ , _styleSheetCellXfs = toValueList _formattingCellXfs+ , _styleSheetFills = toValueList _formattingFills+ , _styleSheetFonts = toValueList _formattingFonts+ , _styleSheetNumFmts = M.fromList . map swap $ M.toList _formattingNumFmts+ }++getId :: Ord a => Lens' FormattingState (Map a Int) -> a -> State FormattingState Int+getId = getId' 0++getId' :: Ord a+ => Int+ -> Lens' FormattingState (Map a Int)+ -> a+ -> State FormattingState Int+getId' k f v = do+ aMap <- use f+ case M.lookup v aMap of+ Just anId -> return anId+ Nothing -> do let anId = k + M.size aMap+ f %= M.insert v anId+ return anId++{-------------------------------------------------------------------------------+ Unwrapped cell conditional formatting+-------------------------------------------------------------------------------}++data FormattedCondFmt = FormattedCondFmt+ { _condfmtCondition :: Condition+ , _condfmtDxf :: Dxf+ , _condfmtPriority :: Int+ , _condfmtStopIfTrue :: Maybe Bool+ } deriving (Eq, Show, Generic)++makeLenses ''FormattedCondFmt++{-------------------------------------------------------------------------------+ Cell with formatting+-------------------------------------------------------------------------------}++-- | Formatting options used to format cells+--+-- TODOs:+--+-- * Add a number format ('_cellXfApplyNumberFormat', '_cellXfNumFmtId')+-- * Add references to the named style sheets ('_cellXfId')+data Format = Format+ { _formatAlignment :: Maybe Alignment+ , _formatBorder :: Maybe Border+ , _formatFill :: Maybe Fill+ , _formatFont :: Maybe Font+ , _formatNumberFormat :: Maybe NumberFormat+ , _formatProtection :: Maybe Protection+ , _formatPivotButton :: Maybe Bool+ , _formatQuotePrefix :: Maybe Bool+ } deriving (Eq, Show, Generic)++makeLenses ''Format++-- | Cell with formatting. '_cellStyle' property of '_formattedCell' is ignored+--+-- See 'formatted' for more details.+data FormattedCell = FormattedCell+ { _formattedCell :: Cell+ , _formattedFormat :: Format+ , _formattedColSpan :: Int+ , _formattedRowSpan :: Int+ } deriving (Eq, Show, Generic)++makeLenses ''FormattedCell++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default FormattedCell where+ def = FormattedCell+ { _formattedCell = def+ , _formattedFormat = def+ , _formattedColSpan = 1+ , _formattedRowSpan = 1+ }++instance Default Format where+ def = Format+ { _formatAlignment = Nothing+ , _formatBorder = Nothing+ , _formatFill = Nothing+ , _formatFont = Nothing+ , _formatNumberFormat = Nothing+ , _formatProtection = Nothing+ , _formatPivotButton = Nothing+ , _formatQuotePrefix = Nothing+ }++instance Default FormattedCondFmt where+ def = FormattedCondFmt ContainsBlanks def topCfPriority Nothing++{-------------------------------------------------------------------------------+ 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]+ } deriving (Eq, Show, Generic)++-- | 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 (RowIndex, ColumnIndex) FormattedCell -> StyleSheet -> Formatted+formatted cs styleSheet =+ let initSt = stateFromStyleSheet styleSheet+ (cs', finalSt) = runState (mapM (uncurry formatCell) (M.toList cs)) initSt+ styleSheet' = updateStyleSheetFromState styleSheet finalSt+ in Formatted {+ formattedCellMap = M.fromList (concat cs')+ , formattedStyleSheet = styleSheet'+ , formattedMerges = reverse (finalSt ^. formattingMerges)+ }++-- | Build an 'Xlsx', render provided cells as per the 'StyleSheet'.+formatWorkbook ::+ [(Text, Map (RowIndex, ColumnIndex) FormattedCell)] -> StyleSheet -> Xlsx+formatWorkbook nfcss initStyle = extract go+ where+ initSt = stateFromStyleSheet initStyle+ go = flip runState initSt $+ forM nfcss $ \(name, fcs) -> do+ cs' <- forM (M.toList fcs) $ \(rc, fc) -> formatCell rc fc+ merges <- reverse . _formattingMerges <$> get+ return ( name+ , def & wsCells .~ M.fromList (concat cs')+ & wsMerges .~ merges)+ extract (sheets, st) =+ def & xlSheets .~ sheets+ & xlStyles .~ renderStyleSheet (updateStyleSheetFromState initStyle st)++-- | reverse to 'formatted' which allows to get a map of formatted cells+-- from an existing worksheet and its workbook's style sheet+toFormattedCells :: CellMap -> [Range] -> StyleSheet -> Map (RowIndex, ColumnIndex) FormattedCell+toFormattedCells m merges StyleSheet{..} = applyMerges $ M.map toFormattedCell m+ where+ toFormattedCell cell@Cell{..} =+ FormattedCell+ { _formattedCell = cell{ _cellStyle = Nothing } -- just to remove confusion+ , _formattedFormat = maybe def formatFromStyle $ flip M.lookup cellXfs =<< _cellStyle+ , _formattedColSpan = 1+ , _formattedRowSpan = 1 }+ formatFromStyle cellXf =+ Format+ { _formatAlignment = applied _cellXfApplyAlignment _cellXfAlignment cellXf+ , _formatBorder = flip M.lookup borders =<<+ applied _cellXfApplyBorder _cellXfBorderId cellXf+ , _formatFill = flip M.lookup fills =<<+ applied _cellXfApplyFill _cellXfFillId cellXf+ , _formatFont = flip M.lookup fonts =<<+ applied _cellXfApplyFont _cellXfFontId cellXf+ , _formatNumberFormat = lookupNumFmt =<<+ applied _cellXfApplyNumberFormat _cellXfNumFmtId cellXf+ , _formatProtection = _cellXfProtection cellXf+ , _formatPivotButton = _cellXfPivotButton cellXf+ , _formatQuotePrefix = _cellXfQuotePrefix cellXf }+ idMapped :: [a] -> Map Int a+ idMapped = M.fromList . zip [0..]+ cellXfs = idMapped _styleSheetCellXfs+ borders = idMapped _styleSheetBorders+ fills = idMapped _styleSheetFills+ fonts = idMapped _styleSheetFonts+ lookupNumFmt fId = asum+ [ StdNumberFormat <$> idToStdNumberFormat fId+ , UserNumberFormat <$> M.lookup fId _styleSheetNumFmts]+ applied :: (CellXf -> Maybe Bool) -> (CellXf -> Maybe a) -> CellXf -> Maybe a+ applied applyProp prop cXf = do+ apply <- applyProp cXf+ if apply then prop cXf else fail "not applied"+ applyMerges cells = foldl' onlyTopLeft cells merges+ onlyTopLeft cells range = flip execState cells $ do+ let ((r1, c1), (r2, c2)) =+ fromJustNote "fromRange" $ fromRange range+ nonTopLeft = tail [(r, c) | r<-[r1..r2], c<-[c1..c2]]+ forM_ nonTopLeft (modify . M.delete)+ at (r1, c1) . non def . formattedRowSpan .=+ (unRowIndex r2 - unRowIndex r1 + 1)+ at (r1, c1) . non def . formattedColSpan .=+ (unColumnIndex c2 - unColumnIndex c1 + 1)++data CondFormatted = CondFormatted {+ -- | The resulting stylesheet+ condformattedStyleSheet :: StyleSheet+ -- | The final map of conditional formatting rules applied to ranges+ , condformattedFormattings :: Map SqRef ConditionalFormatting+ } deriving (Eq, Show, Generic)++conditionallyFormatted :: Map CellRef [FormattedCondFmt] -> StyleSheet -> CondFormatted+conditionallyFormatted cfs styleSheet = CondFormatted+ { condformattedStyleSheet = styleSheet & styleSheetDxfs .~ finalDxfs+ , condformattedFormattings = fmts+ }+ where+ (cellFmts, dxf2id) = runState (mapM (mapM mapDxf) cfs) dxf2id0+ dxf2id0 = fromValueList (styleSheet ^. styleSheetDxfs)+ fmts = M.fromList . map mergeSqRef . groupBy ((==) `on` snd) .+ sortBy (comparing snd) $ M.toList cellFmts+ mergeSqRef cellRefs2fmt =+ (SqRef (map fst cellRefs2fmt),+ headNote "fmt group should not be empty" (map snd cellRefs2fmt))+ finalDxfs = toValueList dxf2id++{-------------------------------------------------------------------------------+ Implementation details+-------------------------------------------------------------------------------}++-- | Format a cell with (potentially) rowspan or colspan+formatCell :: (RowIndex, ColumnIndex) -> FormattedCell+ -> State FormattingState [((RowIndex, ColumnIndex), Cell)]+formatCell (row, col) cell = do+ let (block, mMerge) = cellBlock (row, col) cell+ forM_ mMerge $ \merge -> formattingMerges %= (:) merge+ mapM go block+ where+ go :: ((RowIndex, ColumnIndex), FormattedCell)+ -> State FormattingState ((RowIndex, ColumnIndex), Cell)+ go (pos, c@FormattedCell{..}) = do+ styleId <- cellStyleId c+ return (pos, _formattedCell{_cellStyle = styleId})++-- | 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 :: (RowIndex, ColumnIndex) -> FormattedCell+ -> ([((RowIndex, ColumnIndex), FormattedCell)], Maybe Range)+cellBlock (row, col) cell@FormattedCell{..} = (block, merge)+ where+ block :: [((RowIndex, ColumnIndex), 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 :: (RowIndex, ColumnIndex) -> FormattedCell+ cellAt (row', col') =+ if row' == row && col == col'+ then cell+ else def & formattedFormat . formatBorder ?~ borderAt (row', col')++ border = _formatBorder _formattedFormat++ borderAt :: (RowIndex, ColumnIndex) -> Border+ borderAt (row', col') = def+ & borderTop .~ do guard (row' == topRow) ; _borderTop =<< border+ & borderBottom .~ do guard (row' == bottomRow) ; _borderBottom =<< border+ & borderLeft .~ do guard (col' == leftCol) ; _borderLeft =<< border+ & borderRight .~ do guard (col' == rightCol) ; _borderRight =<< border++ topRow, bottomRow :: RowIndex+ leftCol, rightCol :: ColumnIndex+ topRow = row+ bottomRow = RowIndex $ unRowIndex row + _formattedRowSpan - 1+ leftCol = col+ rightCol = ColumnIndex $ unColumnIndex col + _formattedColSpan - 1++cellStyleId :: FormattedCell -> State FormattingState (Maybe Int)+cellStyleId c = mapM (getId formattingCellXfs) =<< constructCellXf c++constructCellXf :: FormattedCell -> State FormattingState (Maybe CellXf)+constructCellXf FormattedCell{_formattedFormat=Format{..}} = do+ mBorderId <- getId formattingBorders `mapM` _formatBorder+ mFillId <- getId formattingFills `mapM` _formatFill+ mFontId <- getId formattingFonts `mapM` _formatFont+ let getFmtId :: Lens' FormattingState (Map Text Int) -> NumberFormat -> State FormattingState Int+ getFmtId _ (StdNumberFormat fmt) = return (stdNumberFormatId fmt)+ getFmtId l (UserNumberFormat fmt) = getId' firstUserNumFmtId l fmt+ mNumFmtId <- getFmtId formattingNumFmts `mapM` _formatNumberFormat+ let xf = CellXf {+ _cellXfApplyAlignment = apply _formatAlignment+ , _cellXfApplyBorder = apply mBorderId+ , _cellXfApplyFill = apply mFillId+ , _cellXfApplyFont = apply mFontId+ , _cellXfApplyNumberFormat = apply _formatNumberFormat+ , _cellXfApplyProtection = apply _formatProtection+ , _cellXfBorderId = mBorderId+ , _cellXfFillId = mFillId+ , _cellXfFontId = mFontId+ , _cellXfNumFmtId = mNumFmtId+ , _cellXfPivotButton = _formatPivotButton+ , _cellXfQuotePrefix = _formatQuotePrefix+ , _cellXfId = Nothing -- TODO+ , _cellXfAlignment = _formatAlignment+ , _cellXfProtection = _formatProtection+ }+ 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++mapDxf :: FormattedCondFmt -> State (Map Dxf Int) CfRule+mapDxf FormattedCondFmt{..} = do+ dxf2id <- get+ dxfId <- case M.lookup _condfmtDxf dxf2id of+ Just i ->+ return i+ Nothing -> do+ let newId = M.size dxf2id+ modify $ M.insert _condfmtDxf newId+ return newId+ return CfRule+ { _cfrCondition = _condfmtCondition+ , _cfrDxfId = Just dxfId+ , _cfrPriority = _condfmtPriority+ , _cfrStopIfTrue = _condfmtStopIfTrue+ }
src/Codec/Xlsx/Lens.hs view
@@ -1,73 +1,109 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DeriveGeneric #-}+ -- | lenses to access sheets, cells and values of 'Xlsx'-{-# LANGUAGE RankNTypes #-} module Codec.Xlsx.Lens- ( ixSheet- , atSheet- , ixCell- , ixCellRC- , ixCellXY- , atCell- , atCellRC- , atCellXY- , cellValueAt- , cellValueAtRC- , cellValueAtXY- ) where+ ( ixSheet+ , atSheet+ , ixCell+ , ixCellRC+ , ixCellXY+ , atCell+ , atCellRC+ , atCellXY+ , cellValueAt+ , cellValueAtRC+ , cellValueAtXY+ ) where import Codec.Xlsx.Types-import Control.Lens+import Codec.Xlsx.LensCompat+import Data.Function (on)+import Data.List (deleteBy) import Data.Text import Data.Tuple (swap)+import GHC.Generics (Generic) +newtype SheetList = SheetList{ unSheetList :: [(Text, Worksheet)] }+ deriving (Eq, Show, Generic)++type instance IxValue (SheetList) = Worksheet+type instance Index (SheetList) = Text++instance Ixed SheetList where+ ix k f sl@(SheetList l) = case lookup k l of+ Just v -> f v <&> \v' -> SheetList (upsert k v' l)+ Nothing -> pure sl+ {-# INLINE ix #-}++instance At SheetList where+ at k f (SheetList l) = f mv <&> \r -> case r of+ Nothing -> SheetList $ maybe l (\v -> deleteBy ((==) `on` fst) (k,v) l) mv+ Just v' -> SheetList $ upsert k v' l+ where+ mv = lookup k l+ {-# INLINE at #-}++upsert :: (Eq k) => k -> v -> [(k,v)] -> [(k,v)]+upsert k v [] = [(k,v)]+upsert k v ((k1,v1):r) =+ if k == k1+ then (k,v):r+ else (k1,v1):upsert k v r+ -- | lens giving access to a worksheet from 'Xlsx' object -- by its name ixSheet :: Text -> Traversal' Xlsx Worksheet-ixSheet s = xlSheets . ix s+ixSheet s = xlSheets . \f -> fmap unSheetList . ix s f . SheetList -- | 'Control.Lens.At' variant of 'ixSheet' lens+--+-- /Note:/ if there is no such sheet in this workbook then new sheet will be+-- added as the last one to the sheet list atSheet :: Text -> Lens' Xlsx (Maybe Worksheet)-atSheet s = xlSheets . at s+atSheet s = xlSheets . \f -> fmap unSheetList . at s f . SheetList -- | lens giving access to a cell in some worksheet -- by its position, by default row+column index is used -- so this lens is a synonym of 'ixCellRC'-ixCell :: (Int, Int) -> Traversal' Worksheet Cell+ixCell :: (RowIndex, ColumnIndex) -> Traversal' Worksheet Cell ixCell = ixCellRC -- | lens to access cell in a worksheet-ixCellRC :: (Int, Int) -> Traversal' Worksheet Cell+ixCellRC :: (RowIndex, ColumnIndex) -> Traversal' Worksheet Cell ixCellRC i = wsCells . ix i -- | lens to access cell in a worksheet using more traditional -- x+y coordinates-ixCellXY :: (Int, Int) -> Traversal' Worksheet Cell-ixCellXY = ixCellRC . swap+ixCellXY :: (ColumnIndex, RowIndex) -> Traversal' Worksheet Cell+ixCellXY i = ixCellRC $ swap i -- | accessor that can read, write or delete cell in a worksheet -- synonym of 'atCellRC' so uses row+column index-atCell :: (Int, Int) -> Lens' Worksheet (Maybe Cell)+atCell :: (RowIndex, ColumnIndex) -> Lens' Worksheet (Maybe Cell) atCell = atCellRC -- | lens to read, write or delete cell in a worksheet-atCellRC :: (Int, Int) -> Lens' Worksheet (Maybe Cell)+atCellRC :: (RowIndex, ColumnIndex) -> Lens' Worksheet (Maybe Cell) atCellRC i = wsCells . at i -- | lens to read, write or delete cell in a worksheet -- using more traditional x+y or row+column index-atCellXY :: (Int, Int) -> Lens' Worksheet (Maybe Cell)-atCellXY = atCellRC . swap+atCellXY :: (ColumnIndex, RowIndex) -> Lens' Worksheet (Maybe Cell)+atCellXY i = atCellRC $ swap i -- | lens to read, write or delete cell value in a worksheet -- with row+column coordinates, synonym for 'cellValueRC'-cellValueAt :: (Int, Int) -> Lens' Worksheet (Maybe CellValue)+cellValueAt :: (RowIndex, ColumnIndex) -> Lens' Worksheet (Maybe CellValue) cellValueAt = cellValueAtRC -- | lens to read, write or delete cell value in a worksheet -- using row+column coordinates of that cell-cellValueAtRC :: (Int, Int) -> Lens' Worksheet (Maybe CellValue)+cellValueAtRC :: (RowIndex, ColumnIndex) -> Lens' Worksheet (Maybe CellValue) cellValueAtRC i = atCell i . non def . cellValue -- | lens to read, write or delete cell value in a worksheet -- using traditional x+y coordinates-cellValueAtXY :: (Int, Int) -> Lens' Worksheet (Maybe CellValue)-cellValueAtXY = cellValueAtRC . swap+cellValueAtXY :: (ColumnIndex, RowIndex) -> Lens' Worksheet (Maybe CellValue)+cellValueAtXY i = cellValueAtRC $ swap i
+ src/Codec/Xlsx/LensCompat.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE RankNTypes #-}++module Codec.Xlsx.LensCompat + ( module L+ , Prism+ , Prism'+ , prism+ , (<>=)+ ) where++#ifdef USE_MICROLENS++import Lens.Micro as L+import Lens.Micro.GHC ()+import Lens.Micro.Mtl as L+import Lens.Micro.Platform as L+import Data.Profunctor.Choice+import Data.Profunctor (dimap)+import Data.Traversable.WithIndex as L+import Lens.Micro.Internal as L+import Control.Monad.State.Class (MonadState, modify)++-- Since micro-lens denies the existence of prisms,+-- I copied over the definitions from Control.Lens for the prism+-- function.+type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)+type Prism' s a = Prism s s a a++prism :: (b -> t) -> (s -> Either t a) -> Prism s t a b+prism bt seta = dimap seta (either pure (fmap bt)) . right'++(<>=) :: (MonadState s m, Monoid a) => ASetter' s a -> a -> m ()+l <>= a = modify (l <>~ a)++#else++import Control.Lens as L++#endif
src/Codec/Xlsx/Parser.hs view
@@ -1,178 +1,715 @@+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NoMonomorphismRestriction #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}+ -- | This module provides a function for reading .xlsx files module Codec.Xlsx.Parser- ( toXlsx- ) where+ ( toXlsx+ , toXlsxEither+ , toXlsxFast+ , toXlsxEitherFast+ , ParseError(..)+ , Parser+ ) where -import qualified Codec.Archive.Zip as Zip-import Control.Applicative-import Control.Arrow ((&&&))-import Control.Monad (liftM4)-import Control.Monad.IO.Class()+import qualified "zip-archive" Codec.Archive.Zip as Zip+import Control.Applicative+import Control.Arrow (left)+import Control.Error.Safe (headErr)+import Control.Error.Util (note)+import Control.Exception (Exception)+import Codec.Xlsx.LensCompat hiding ((<.>), element, views)+import Control.Monad (join, void, liftM4)+import Control.Monad.Except (catchError, throwError)+import Data.Bool (bool)+import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as L-import Data.ByteString.Lazy.Char8()-import Data.IntMap (IntMap)-import qualified Data.IntMap as IM-import Data.List+import qualified Data.ByteString.Lazy as LB+import Data.ByteString.Lazy.Char8 ()+import Data.List+import Data.Map (Map) import qualified Data.Map as M-import Data.Maybe-import Data.Ord-import Data.Text (Text)+import Data.Maybe+import Data.Monoid+import Data.Text (Text) import qualified Data.Text as T-import qualified Data.Text.Read as T-import Data.XML.Types-import Prelude hiding (sequence)-import Text.XML as X-import Text.XML.Cursor--import Codec.Xlsx.Parser.Internal-import Codec.Xlsx.Types+import Data.Traversable+import GHC.Generics (Generic)+import Prelude hiding (sequence)+import Safe (headNote)+import System.FilePath.Posix+import Text.XML as X+import Text.XML.Cursor hiding (bool)+import qualified Xeno.DOM as Xeno +import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Parser.Internal.PivotTable+import Codec.Xlsx.Types+import Codec.Xlsx.Types.Cell (formulaDataFromCursor)+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.CfPair+import Codec.Xlsx.Types.Internal.CommentTable as CommentTable+import Codec.Xlsx.Types.Internal.ContentTypes as ContentTypes+import Codec.Xlsx.Types.Internal.CustomProperties+ as CustomProperties+import Codec.Xlsx.Types.Internal.DvPair+import Codec.Xlsx.Types.Internal.FormulaData+import Codec.Xlsx.Types.Internal.Relationships as Relationships+import Codec.Xlsx.Types.Internal.SharedStringTable+import Codec.Xlsx.Types.PivotTable.Internal -- | Reads `Xlsx' from raw data (lazy bytestring) toXlsx :: L.ByteString -> Xlsx-toXlsx bs = Xlsx sheets styles- where- ar = Zip.toArchive bs- ss = getSharedStrings ar- styles = getStyles ar- wfs = getWorksheetFiles ar- sheets = M.fromList $ map (wfName &&& extractSheet ar ss) wfs+toXlsx = either (error . show) id . toXlsxEither +data ParseError = InvalidZipArchive String+ | MissingFile FilePath+ | InvalidFile FilePath Text+ | InvalidRef FilePath RefId+ | InconsistentXlsx Text+ deriving (Eq, Show, Generic)++instance Exception ParseError++type Parser = Either ParseError++-- | Reads `Xlsx' from raw data (lazy bytestring) using @xeno@ library+-- using some "cheating":+--+-- * not doing 100% xml validation+-- * replacing only <https://www.w3.org/TR/REC-xml/#sec-predefined-ent predefined entities>+-- and <https://www.w3.org/TR/REC-xml/#NT-CharRef Unicode character references>+-- (without checking codepoint validity)+-- * almost not using XML namespaces+toXlsxFast :: L.ByteString -> Xlsx+toXlsxFast = either (error . show) id . toXlsxEitherFast++-- | Reads `Xlsx' from raw data (lazy bytestring), failing with 'Left' on parse error+toXlsxEither :: L.ByteString -> Parser Xlsx+toXlsxEither = toXlsxEitherBase extractSheet++-- | Fast parsing with 'Left' on parse error, see 'toXlsxFast'+toXlsxEitherFast :: L.ByteString -> Parser Xlsx+toXlsxEitherFast = toXlsxEitherBase extractSheetFast++toXlsxEitherBase ::+ (Zip.Archive -> SharedStringTable -> ContentTypes -> Caches -> WorksheetFile -> Parser Worksheet)+ -> L.ByteString+ -> Parser Xlsx+toXlsxEitherBase parseSheet bs = do+ ar <- left InvalidZipArchive $ Zip.toArchiveOrFail bs+ sst <- getSharedStrings ar+ contentTypes <- getContentTypes ar+ (wfs, names, cacheSources, dateBase) <- readWorkbook ar+ sheets <- forM wfs $ \wf -> do+ sheet <- parseSheet ar sst contentTypes cacheSources wf+ return . (wfName wf,) . (wsState .~ wfState wf) $ sheet+ CustomProperties customPropMap <- getCustomProperties ar+ return $ Xlsx sheets (getStyles ar) names customPropMap dateBase+ data WorksheetFile = WorksheetFile { wfName :: Text+ , wfSheetId :: Int+ , wfState :: SheetState , wfPath :: FilePath }- deriving Show--decimal :: Monad m => Text -> m Int-decimal t = case T.decimal t of- Right (d, _) -> return d- _ -> fail "invalid decimal"+ deriving (Show, Generic) -rational :: Monad m => Text -> m Double-rational t = case T.rational t of- Right (r, _) -> return r- _ -> fail "invalid rational"+type Caches = [(CacheId, (Text, CellRef, [CacheField]))] -extractSheet :: Zip.Archive- -> IM.IntMap Text- -> WorksheetFile- -> Worksheet-extractSheet ar ss wf = Worksheet cws rowProps cells merges+extractSheetFast :: Zip.Archive+ -> SharedStringTable+ -> ContentTypes+ -> Caches+ -> WorksheetFile+ -> Parser Worksheet+extractSheetFast ar sst contentTypes caches wf = do+ file <-+ note (MissingFile filePath) $+ Zip.fromEntry <$> Zip.findEntryByPath filePath ar+ sheetRels <- getRels ar filePath+ root <-+ left (\ex -> InvalidFile filePath $ T.pack (show ex)) $+ Xeno.parse (LB.toStrict file)+ parseWorksheet root sheetRels where- file = fromJust $ Zip.fromEntry <$> Zip.findEntryByPath (wfPath wf) ar- cur = case parseLBS def file of- Left _ -> error "could not read file"- Right d -> fromDocument d+ filePath = wfPath wf+ parseWorksheet :: Xeno.Node -> Relationships -> Parser Worksheet+ parseWorksheet root sheetRels = do+ let prefixes = nsPrefixes root+ odrNs =+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"+ odrX = addPrefix prefixes odrNs+ skip = void . maybeChild+ (ws, tableIds, drawingRId, legacyDrRId) <-+ liftEither . collectChildren root $ do+ skip "sheetPr"+ skip "dimension"+ _wsSheetViews <- fmap justNonEmpty . maybeParse "sheetViews" $ \n ->+ collectChildren n $ fromChildList "sheetView"+ skip "sheetFormatPr"+ _wsColumnsProperties <-+ fmap (fromMaybe []) . maybeParse "cols" $ \n ->+ collectChildren n (fromChildList "col")+ (_wsRowPropertiesMap, _wsCells, _wsSharedFormulas) <-+ requireAndParse "sheetData" $ \n -> do+ rows <- collectChildren n $ childList "row"+ collectRows <$> forM rows parseRow+ skip "sheetCalcPr"+ _wsProtection <- maybeFromChild "sheetProtection"+ skip "protectedRanges"+ skip "scenarios"+ _wsAutoFilter <- maybeFromChild "autoFilter"+ skip "sortState"+ skip "dataConsolidate"+ skip "customSheetViews"+ _wsMerges <- fmap (fromMaybe []) . maybeParse "mergeCells" $ \n -> do+ mCells <- collectChildren n $ childList "mergeCell"+ forM mCells $ \mCell -> parseAttributes mCell $ fromAttr "ref"+ _wsConditionalFormattings <-+ M.fromList . map unCfPair <$> fromChildList "conditionalFormatting"+ _wsDataValidations <-+ fmap (fromMaybe mempty) . maybeParse "dataValidations" $ \n -> do+ M.fromList . map unDvPair <$>+ collectChildren n (fromChildList "dataValidation")+ skip "hyperlinks"+ skip "printOptions"+ skip "pageMargins"+ _wsPageSetup <- maybeFromChild "pageSetup"+ skip "headerFooter"+ skip "rowBreaks"+ skip "colBreaks"+ skip "customProperties"+ skip "cellWatches"+ skip "ignoredErrors"+ skip "smartTags"+ drawingRId <- maybeParse "drawing" $ \n ->+ parseAttributes n $ fromAttr (odrX "id")+ legacyDrRId <- maybeParse "legacyDrawing" $ \n ->+ parseAttributes n $ fromAttr (odrX "id")+ tableIds <- fmap (fromMaybe []) . maybeParse "tableParts" $ \n -> do+ tParts <- collectChildren n $ childList "tablePart"+ forM tParts $ \part ->+ parseAttributes part $ fromAttr (odrX "id") - cws = cur $/ element (n"cols") &/ element (n"col") >=>- liftM4 ColumnsWidth <$>- (attribute "min" >=> decimal) <*>- (attribute "max" >=> decimal) <*>- (attribute "width" >=> rational) <*>- (attribute "style" >=> decimal)+ -- all explicitly assigned fields filled below+ return (+ Worksheet+ { _wsDrawing = Nothing+ , _wsPivotTables = []+ , _wsTables = []+ , _wsState = wfState wf+ , ..+ }+ , tableIds+ , drawingRId+ , legacyDrRId) - (rowProps, cells) = collect $ cur $/ element (n"sheetData") &/ element (n"row") >=> parseRow- parseRow c = do- r <- c $| attribute "r" >=> decimal- let ht = if attribute "customHeight" c == ["true"]- then listToMaybe $ c $| attribute "ht" >=> rational- else Nothing- let s = if attribute "s" c /= []- then listToMaybe $ c $| attribute "s" >=> decimal- else Nothing- let rp = if isNothing s && isNothing ht- then Nothing- else Just (RowProps ht s)- return (r, rp, c $/ element (n"c") >=> parseCell)- parseCell :: Cursor -> [(Int, Int, Cell)]+ let commentsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"+ commentTarget :: Maybe FilePath+ commentTarget = logicalNameToZipItemName . relTarget <$> findRelByType commentsType sheetRels+ legacyDrPath = fmap (logicalNameToZipItemName . relTarget) . flip Relationships.lookup sheetRels =<< legacyDrRId+ commentsMap <-+ fmap join . forM commentTarget $ getComments ar legacyDrPath+ let commentCells =+ M.fromList+ [ (fromSingleCellRefNoting r, def { _cellComment = Just cmnt})+ | (r, cmnt) <- maybe [] CommentTable.toList commentsMap+ ]+ assignComment withCmnt noCmnt =+ noCmnt & cellComment .~ (withCmnt ^. cellComment)+ mergeComments = M.unionWith assignComment commentCells+ tables <- forM tableIds $ \rId -> do+ fp <- lookupRelPath filePath sheetRels rId+ getTable ar fp+ drawing <- forM drawingRId $ \dId -> do+ fp <- lookupRelPath filePath sheetRels dId+ getDrawing ar contentTypes fp+ let ptType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"+ pivotTables <- forM (allByType ptType sheetRels) $ \rel -> do+ let ptPath = logicalNameToZipItemName $ relTarget rel+ bs <- note (MissingFile ptPath) $ Zip.fromEntry <$> Zip.findEntryByPath ptPath ar+ note (InconsistentXlsx $ "Bad pivot table in " <> T.pack ptPath) $+ parsePivotTable (flip Prelude.lookup caches) bs++ return $ ws & wsTables .~ tables+ & wsCells %~ mergeComments+ & wsDrawing .~ drawing+ & wsPivotTables .~ pivotTables+ liftEither :: Either Text a -> Parser a+ liftEither = left (\t -> InvalidFile filePath t)+ justNonEmpty v@(Just (_:_)) = v+ justNonEmpty _ = Nothing+ collectRows = foldr collectRow (M.empty, M.empty, M.empty)+ collectRow ::+ ( RowIndex+ , Maybe RowProperties+ , [(RowIndex, ColumnIndex, Cell, Maybe (SharedFormulaIndex, SharedFormulaOptions))])+ -> ( Map RowIndex RowProperties+ , CellMap+ , Map SharedFormulaIndex SharedFormulaOptions)+ -> ( Map RowIndex RowProperties+ , CellMap+ , Map SharedFormulaIndex SharedFormulaOptions)+ collectRow (r, mRP, rowCells) (rowMap, cellMap, sharedF) =+ let (newCells0, newSharedF0) =+ unzip [(((rInd, cInd), cd), shared) | (rInd, cInd, cd, shared) <- rowCells]+ newCells = M.fromAscList newCells0+ newSharedF = M.fromAscList $ catMaybes newSharedF0+ newRowMap =+ case mRP of+ Just rp -> M.insert r rp rowMap+ Nothing -> rowMap+ in (newRowMap, cellMap <> newCells, sharedF <> newSharedF)+ parseRow ::+ Xeno.Node+ -> Either Text ( RowIndex+ , Maybe RowProperties+ , [( RowIndex+ , ColumnIndex+ , Cell+ , Maybe (SharedFormulaIndex, SharedFormulaOptions))])+ parseRow row = do+ (r, s, ht, cstHt, hidden) <-+ parseAttributes row $+ ((,,,,) <$> fromAttr "r" <*> maybeAttr "s" <*> maybeAttr "ht" <*>+ fromAttrDef "customHeight" False <*>+ fromAttrDef "hidden" False)+ let props =+ RowProps+ { rowHeight =+ if cstHt+ then CustomHeight <$> ht+ else AutomaticHeight <$> ht+ , rowStyle = s+ , rowHidden = hidden+ }+ cellNodes <- collectChildren row $ childList "c"+ cells <- forM cellNodes parseCell+ return+ ( RowIndex r+ , if props == def+ then Nothing+ else Just props+ , cells)++ -- NB: According to format specification default value for cells without+ -- `t` attribute is a `n` - number.+ --+ -- Schema part from spec (see the `CellValue` spec reference):+ -- <xsd:complexType name="CT_Cell">+ -- ..+ -- <xsd:attribute name="t" type="ST_CellType" use="optional" default="n"/>+ -- </xsd:complexType>+ parseCell ::+ Xeno.Node+ -> Either Text ( RowIndex+ , ColumnIndex+ , Cell+ , Maybe (SharedFormulaIndex, SharedFormulaOptions)) parseCell cell = do- let- s = listToMaybe $ cell $| attribute "s" >=> decimal- t = fromMaybe "n" $ listToMaybe $ cell $| attribute "t"- d = listToMaybe $ cell $/ element (n"v") &/ content >=> extractCellValue ss t- (c, r) <- T.span (>'9') <$> (cell $| attribute "r")- return (int r, col2int c, Cell s d)- collect = foldr collectRow (M.empty, M.empty)- collectRow (_, Nothing, rowCells) (rowMap, cellMap) =- (rowMap, foldr collectCell cellMap rowCells)- collectRow (r, Just h, rowCells) (rowMap, cellMap) =- (M.insert r h rowMap, foldr collectCell cellMap rowCells)- collectCell (x, y, cd) = M.insert (x,y) cd+ (ref, s, t) <-+ parseAttributes cell $+ (,,) <$> fromAttr "r" <*> maybeAttr "s" <*> fromAttrDef "t" "n"+ (fNode, vNode, isNode) <-+ collectChildren cell $+ (,,) <$> maybeChild "f" <*> maybeChild "v" <*> maybeChild "is"+ let vConverted :: (FromAttrBs a) => Either Text (Maybe a)+ vConverted =+ case contentBs <$> vNode of+ Nothing -> return Nothing+ Just c -> Just <$> fromAttrBs c+ mFormulaData <- mapM fromXenoNode fNode+ d <-+ case t of+ ("s" :: ByteString) -> do+ si <- vConverted+ case sstItem sst =<< si of+ Just xlTxt -> return $ Just (xlsxTextToCellValue xlTxt)+ Nothing -> throwError "bad shared string index"+ "inlineStr" -> mapM (fmap xlsxTextToCellValue . fromXenoNode) isNode+ "str" -> fmap CellText <$> vConverted+ "n" -> fmap CellDouble <$> vConverted+ "b" -> fmap CellBool <$> vConverted+ "e" -> fmap CellError <$> vConverted+ unexpected ->+ throwError $ "unexpected cell type " <> T.pack (show unexpected)+ let (r, c) = fromSingleCellRefNoting ref+ f = frmdFormula <$> mFormulaData+ shared = frmdShared =<< mFormulaData+ return (r, c, Cell s d Nothing f, shared) - merges = cur $/ parseMerges- parseMerges :: Cursor -> [Text]- parseMerges = element (n"mergeCells") &/ element (n"mergeCell") >=> parseMerge- parseMerge c = c $| attribute "ref"+extractSheet ::+ Zip.Archive+ -> SharedStringTable+ -> ContentTypes+ -> Caches+ -> WorksheetFile+ -> Parser Worksheet+extractSheet ar sst contentTypes caches wf = do+ let filePath = wfPath wf+ file <- note (MissingFile filePath) $ Zip.fromEntry <$> Zip.findEntryByPath filePath ar+ cur <- fmap fromDocument . left (\ex -> InvalidFile filePath (T.pack $ show ex)) $+ parseLBS def file+ sheetRels <- getRels ar filePath -extractCellValue :: IntMap Text -> Text -> Text -> [CellValue]-extractCellValue ss "s" v =- case T.decimal v of- Right (d, _) -> maybeToList $ fmap CellText $ IM.lookup d ss- _ -> []-extractCellValue _ "str" str = [CellText str]-extractCellValue _ "n" v =- case T.rational v of- Right (d, _) -> [CellDouble d]- _ -> []-extractCellValue _ "b" "1" = [CellBool True]-extractCellValue _ "b" "0" = [CellBool False]-extractCellValue _ _ _ = []+ -- The specification says the file should contain either 0 or 1 @sheetViews@+ -- (4th edition, section 18.3.1.88, p. 1704 and definition CT_Worksheet, p. 3910)+ let sheetViewList = cur $/ element (addSmlNamespace "sheetViews") &/ element (addSmlNamespace "sheetView") >=> fromCursor+ sheetViews = case sheetViewList of+ [] -> Nothing+ views -> Just views --- | Add office document relationship namespace to name-odr :: Text -> Name-odr x = Name- { nameLocalName = x- , nameNamespace = Just "http://schemas.openxmlformats.org/officeDocument/2006/relationships"- , namePrefix = Nothing- }+ let commentsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"+ commentTarget :: Maybe FilePath+ commentTarget = logicalNameToZipItemName . relTarget <$> findRelByType commentsType sheetRels+ legacyDrRId = cur $/ element (addSmlNamespace "legacyDrawing") >=> fromAttribute (odr"id")+ legacyDrPath = fmap (logicalNameToZipItemName . relTarget) . flip Relationships.lookup sheetRels =<< listToMaybe legacyDrRId --- | Add package relationship namespace to name-pr :: Text -> Name-pr x = Name- { nameLocalName = x- , nameNamespace = Just "http://schemas.openxmlformats.org/package/2006/relationships"- , namePrefix = Nothing- }+ commentsMap :: Maybe CommentTable <- maybe (Right Nothing) (getComments ar legacyDrPath) commentTarget + -- Likewise, @pageSetup@ also occurs either 0 or 1 times+ let pageSetup = listToMaybe $ cur $/ element (addSmlNamespace "pageSetup") >=> fromCursor++ cws = cur $/ element (addSmlNamespace "cols") &/ element (addSmlNamespace "col") >=> fromCursor++ (rowProps, cells0, sharedFormulas) =+ collect $ cur $/ element (addSmlNamespace "sheetData") &/ element (addSmlNamespace "row") >=> parseRow+ parseRow ::+ Cursor+ -> [( RowIndex+ , Maybe RowProperties+ , [(RowIndex, ColumnIndex, Cell, Maybe (SharedFormulaIndex, SharedFormulaOptions))])]+ parseRow c = do+ r <- RowIndex <$> fromAttribute "r" c+ let prop = RowProps+ { rowHeight = do h <- listToMaybe $ fromAttribute "ht" c+ case fromAttribute "customHeight" c of+ [True] -> return $ CustomHeight h+ _ -> return $ AutomaticHeight h+ , rowStyle = listToMaybe $ fromAttribute "s" c+ , rowHidden =+ case fromAttribute "hidden" c of+ [] -> False+ f:_ -> f+ }+ return ( r+ , if prop == def then Nothing else Just prop+ , c $/ element (addSmlNamespace "c") >=> parseCell+ )+ parseCell ::+ Cursor+ -> [(RowIndex, ColumnIndex, Cell, Maybe (SharedFormulaIndex, SharedFormulaOptions))]+ parseCell cell = do+ ref <- fromAttribute "r" cell+ let s = listToMaybe $ cell $| attribute "s" >=> decimal+ -- NB: According to format specification default value for cells without+ -- `t` attribute is a `n` - number.+ --+ -- <xsd:complexType name="CT_Cell" from spec (see the `CellValue` spec reference)>+ -- ..+ -- <xsd:attribute name="t" type="ST_CellType" use="optional" default="n"/>+ -- </xsd:complexType>+ t = fromMaybe "n" $ listToMaybe $ cell $| attribute "t"+ d = listToMaybe $ extractCellValue sst t cell+ mFormulaData = listToMaybe $ cell $/ element (addSmlNamespace "f") >=> formulaDataFromCursor+ f = fst <$> mFormulaData+ shared = snd =<< mFormulaData+ (r, c) = fromSingleCellRefNoting ref+ comment = commentsMap >>= lookupComment ref+ return (r, c, Cell s d comment f, shared)+ collect = foldr collectRow (M.empty, M.empty, M.empty)+ collectRow ::+ ( RowIndex+ , Maybe RowProperties+ , [(RowIndex, ColumnIndex, Cell, Maybe (SharedFormulaIndex, SharedFormulaOptions))])+ -> (Map RowIndex RowProperties, CellMap, Map SharedFormulaIndex SharedFormulaOptions)+ -> (Map RowIndex RowProperties, CellMap, Map SharedFormulaIndex SharedFormulaOptions)+ collectRow (r, mRP, rowCells) (rowMap, cellMap, sharedF) =+ let (newCells0, newSharedF0) =+ unzip [(((x,y),cd), shared) | (x, y, cd, shared) <- rowCells]+ newCells = M.fromList newCells0+ newSharedF = M.fromList $ catMaybes newSharedF0+ newRowMap = case mRP of+ Just rp -> M.insert r rp rowMap+ Nothing -> rowMap+ in (newRowMap, cellMap <> newCells, sharedF <> newSharedF)++ commentCells =+ M.fromList+ [ (fromSingleCellRefNoting r, def {_cellComment = Just cmnt})+ | (r, cmnt) <- maybe [] CommentTable.toList commentsMap+ ]+ cells = cells0 `M.union` commentCells++ mProtection = listToMaybe $ cur $/ element (addSmlNamespace "sheetProtection") >=> fromCursor++ mDrawingId = listToMaybe $ cur $/ element (addSmlNamespace "drawing") >=> fromAttribute (odr"id")++ merges = cur $/ parseMerges+ parseMerges :: Cursor -> [Range]+ parseMerges = element (addSmlNamespace "mergeCells") &/ element (addSmlNamespace "mergeCell") >=> fromAttribute "ref"++ condFormtattings = M.fromList . map unCfPair $ cur $/ element (addSmlNamespace "conditionalFormatting") >=> fromCursor++ validations = M.fromList . map unDvPair $+ cur $/ element (addSmlNamespace "dataValidations") &/ element (addSmlNamespace "dataValidation") >=> fromCursor++ tableIds =+ cur $/ element (addSmlNamespace "tableParts") &/ element (addSmlNamespace "tablePart") >=>+ fromAttribute (odr "id")++ let mAutoFilter = listToMaybe $ cur $/ element (addSmlNamespace "autoFilter") >=> fromCursor++ mDrawing <- case mDrawingId of+ Just dId -> do+ fp <- lookupRelPath filePath sheetRels dId+ Just <$> getDrawing ar contentTypes fp+ Nothing ->+ return Nothing++ let ptType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"+ pTables <- forM (allByType ptType sheetRels) $ \rel -> do+ let ptPath = logicalNameToZipItemName $ relTarget rel+ bs <- note (MissingFile ptPath) $ Zip.fromEntry <$> Zip.findEntryByPath ptPath ar+ note (InconsistentXlsx $ "Bad pivot table in " <> T.pack ptPath) $+ parsePivotTable (flip Prelude.lookup caches) bs++ tables <- forM tableIds $ \rId -> do+ fp <- lookupRelPath filePath sheetRels rId+ getTable ar fp++ return $+ Worksheet+ cws+ rowProps+ cells+ mDrawing+ merges+ sheetViews+ pageSetup+ condFormtattings+ validations+ pTables+ mAutoFilter+ tables+ mProtection+ sharedFormulas+ (wfState wf)++extractCellValue :: SharedStringTable -> Text -> Cursor -> [CellValue]+extractCellValue sst t cur+ | t == "s" = do+ si <- vConverted "shared string"+ case sstItem sst si of+ Just xlTxt -> return $ xlsxTextToCellValue xlTxt+ Nothing -> fail "bad shared string index"+ | t == "inlineStr" =+ cur $/ element (addSmlNamespace "is") >=> fmap xlsxTextToCellValue . fromCursor+ | t == "str" = CellText <$> vConverted "string"+ | t == "n" = CellDouble <$> vConverted "double"+ | t == "b" = CellBool <$> vConverted "boolean"+ | t == "e" = CellError <$> vConverted "error"+ | otherwise = fail "bad cell value"+ where+ vConverted typeStr = do+ vContent <- cur $/ element (addSmlNamespace "v") >=> \c ->+ return (T.concat $ c $/ content)+ case fromAttrVal vContent of+ Right (val, _) -> return $ val+ _ -> fail $ "bad " ++ typeStr ++ " cell value"+ -- | Get xml cursor from the specified file inside the zip archive.-xmlCursor :: Zip.Archive -> FilePath -> Maybe Cursor-xmlCursor ar fname = parse <$> Zip.findEntryByPath fname ar+xmlCursorOptional :: Zip.Archive -> FilePath -> Parser (Maybe Cursor)+xmlCursorOptional ar fname =+ (Just <$> xmlCursorRequired ar fname) `catchError` missingToNothing where- parse entry = case parseLBS def (Zip.fromEntry entry) of- Left _ -> error "could not read file"- Right d -> fromDocument d+ missingToNothing :: ParseError -> Either ParseError (Maybe a)+ missingToNothing (MissingFile _) = return Nothing+ missingToNothing other = throwError other --- | Get shared strings (if there are some) into IntMap.-getSharedStrings :: Zip.Archive -> IM.IntMap Text-getSharedStrings x = case xmlCursor x "xl/sharedStrings.xml" of- Nothing -> IM.empty- Just c -> parseSharedStrings c+-- | Get xml cursor from the given file, failing with MissingFile if not found.+xmlCursorRequired :: Zip.Archive -> FilePath -> Parser Cursor+xmlCursorRequired ar fname = do+ entry <- note (MissingFile fname) $ Zip.findEntryByPath fname ar+ cur <- left (\ex -> InvalidFile fname (T.pack $ show ex)) $ parseLBS def (Zip.fromEntry entry)+ return $ fromDocument cur +fromFileCursorDef ::+ FromCursor a => Zip.Archive -> FilePath -> Text -> a -> Parser a+fromFileCursorDef x fp contentsDescr defVal = do+ mCur <- xmlCursorOptional x fp+ case mCur of+ Just cur ->+ headErr (InvalidFile fp $ "Couldn't parse " <> contentsDescr) $ fromCursor cur+ Nothing -> return defVal++fromFileCursor :: FromCursor a => Zip.Archive -> FilePath -> Text -> Parser a+fromFileCursor x fp contentsDescr = do+ cur <- xmlCursorRequired x fp+ headErr (InvalidFile fp $ "Couldn't parse " <> contentsDescr) $ fromCursor cur++-- | Get shared string table+getSharedStrings :: Zip.Archive -> Parser SharedStringTable+getSharedStrings x =+ fromFileCursorDef x "xl/sharedStrings.xml" "shared strings" sstEmpty++getContentTypes :: Zip.Archive -> Parser ContentTypes+getContentTypes x = fromFileCursor x "[Content_Types].xml" "content types"+ getStyles :: Zip.Archive -> Styles getStyles ar = case Zip.fromEntry <$> Zip.findEntryByPath "xl/styles.xml" ar of Nothing -> Styles L.empty Just xml -> Styles xml --- | getWorksheetFiles pulls the names of the sheets-getWorksheetFiles :: Zip.Archive -> [WorksheetFile]-getWorksheetFiles ar = case xmlCursor ar "xl/workbook.xml" of- Nothing ->- error "invalid workbook"- Just c ->- let- sheetData = c $/ element (n"sheets") &/ element (n"sheet") >=>- liftA2 (,) <$> attribute "name" <*> attribute (odr"id")- wbRels = getWbRels ar- in [WorksheetFile name ("xl/" ++ T.unpack (fromJust $ lookup rId wbRels)) | (name, rId) <- sheetData]+getComments :: Zip.Archive -> Maybe FilePath -> FilePath -> Parser (Maybe CommentTable)+getComments ar drp fp = do+ mCurComments <- xmlCursorOptional ar fp+ mCurDr <- maybe (return Nothing) (xmlCursorOptional ar) drp+ return (liftA2 hide (hidden <$> mCurDr) . listToMaybe . fromCursor =<< mCurComments)+ where+ hide refs (CommentTable m) = CommentTable $ foldl' hideComment m refs+ hideComment m r = M.adjust (\c->c{_commentVisible = False}) r m+ v nm = Name nm (Just "urn:schemas-microsoft-com:vml") Nothing+ x nm = Name nm (Just "urn:schemas-microsoft-com:office:excel") Nothing+ hidden :: Cursor -> [CellRef]+ hidden cur = cur $/ checkElement visibleShape &/+ element (x"ClientData") >=> shapeCellRef+ visibleShape Element{..} = elementName == (v"shape") &&+ maybe False (any ("visibility:hidden"==) . T.split (==';')) (M.lookup "style" elementAttributes)+ shapeCellRef :: Cursor -> [CellRef]+ shapeCellRef c = do+ r0 <- c $/ element (x"Row") &/ content >=> decimal+ c0 <- c $/ element (x"Column") &/ content >=> decimal+ return $ singleCellRef (r0 + 1, c0 + 1) -getWbRels :: Zip.Archive -> [(Text, Text)]-getWbRels ar = case xmlCursor ar "xl/_rels/workbook.xml.rels" of- Nothing -> []- Just c -> c $/ element (pr"Relationship") >=>- liftA2 (,) <$> attribute "Id" <*> attribute "Target"+getCustomProperties :: Zip.Archive -> Parser CustomProperties+getCustomProperties ar =+ fromFileCursorDef ar "docProps/custom.xml" "custom properties" CustomProperties.empty -int :: Text -> Int-int = either error fst . T.decimal+getDrawing :: Zip.Archive -> ContentTypes -> FilePath -> Parser Drawing+getDrawing ar contentTypes fp = do+ cur <- xmlCursorRequired ar fp+ drawingRels <- getRels ar fp+ unresolved <- headErr (InvalidFile fp "Couldn't parse drawing") (fromCursor cur)+ anchors <- forM (unresolved ^. xdrAnchors) $ resolveFileInfo drawingRels+ return $ Drawing anchors+ where+ resolveFileInfo :: Relationships -> Anchor RefId RefId -> Parser (Anchor FileInfo ChartSpace)+ resolveFileInfo rels uAnch =+ case uAnch ^. anchObject of+ Picture {..} -> do+ let mRefId = _picBlipFill ^. bfpImageInfo+ mFI <- lookupFI rels mRefId+ let pic' =+ Picture+ { _picMacro = _picMacro+ , _picPublished = _picPublished+ , _picNonVisual = _picNonVisual+ , _picBlipFill = (_picBlipFill & bfpImageInfo .~ mFI)+ , _picShapeProperties = _picShapeProperties+ }+ return uAnch {_anchObject = pic'}+ Graphic nv rId tr -> do+ chartPath <- lookupRelPath fp rels rId+ chart <- readChart ar chartPath+ return uAnch {_anchObject = Graphic nv chart tr}+ lookupFI _ Nothing = return Nothing+ lookupFI rels (Just rId) = do+ path <- lookupRelPath fp rels rId+ -- content types use paths starting with /+ contentType <-+ note (InvalidFile path "Missing content type") $+ ContentTypes.lookup ("/" <> path) contentTypes+ contents <-+ Zip.fromEntry <$> note (MissingFile path) (Zip.findEntryByPath path ar)+ return . Just $ FileInfo (stripMediaPrefix path) contentType contents+ stripMediaPrefix :: FilePath -> FilePath+ stripMediaPrefix p = fromMaybe p $ stripPrefix "xl/media/" p++readChart :: Zip.Archive -> FilePath -> Parser ChartSpace+readChart ar path = fromFileCursor ar path "chart"++-- | readWorkbook pulls the names of the sheets and the defined names+readWorkbook :: Zip.Archive -> Parser ([WorksheetFile], DefinedNames, Caches, DateBase)+readWorkbook ar = do+ let wbPath = "xl/workbook.xml"+ cur <- xmlCursorRequired ar wbPath+ wbRels <- getRels ar wbPath+ -- Specification says the 'name' is required.+ let mkDefinedName :: Cursor -> [(Text, Maybe Text, Text)]+ mkDefinedName c =+ return+ ( headNote "Missing name attribute" $ attribute "name" c+ , listToMaybe $ attribute "localSheetId" c+ , T.concat $ c $/ content)+ names =+ cur $/ element (addSmlNamespace "definedNames") &/ element (addSmlNamespace "definedName") >=>+ mkDefinedName+ sheets <-+ sequence $+ cur $/ element (addSmlNamespace "sheets") &/ element (addSmlNamespace "sheet") >=>+ liftM4 (worksheetFile wbPath wbRels) <$> attribute "name" <*> fromAttribute "sheetId" <*> fromAttributeDef "state" def <*>+ fromAttribute (odr "id")+ let cacheRefs =+ cur $/ element (addSmlNamespace "pivotCaches") &/ element (addSmlNamespace "pivotCache") >=>+ liftA2 (,) <$> fromAttribute "cacheId" <*> fromAttribute (odr "id")+ caches <-+ forM cacheRefs $ \(cacheId, rId) -> do+ path <- lookupRelPath wbPath wbRels rId+ bs <-+ note (MissingFile path) $ Zip.fromEntry <$> Zip.findEntryByPath path ar+ (sheet, ref, fields0, mRecRId) <-+ note (InconsistentXlsx $ "Bad pivot table cache in " <> T.pack path) $+ parseCache bs+ fields <- case mRecRId of+ Just recId -> do+ cacheRels <- getRels ar path+ recsPath <- lookupRelPath path cacheRels recId+ rCur <- xmlCursorRequired ar recsPath+ let recs = rCur $/ element (addSmlNamespace "r") >=> \cur' ->+ return $ cur' $/ anyElement >=> recordValueFromNode . node+ return $ fillCacheFieldsFromRecords fields0 recs+ Nothing ->+ return fields0+ return $ (cacheId, (sheet, ref, fields))+ let dateBase = bool DateBase1900 DateBase1904 . fromMaybe False . listToMaybe $+ cur $/ element (addSmlNamespace "workbookPr") >=> fromAttribute "date1904"+ return (sheets, DefinedNames names, caches, dateBase)++getTable :: Zip.Archive -> FilePath -> Parser Table+getTable ar fp = do+ cur <- xmlCursorRequired ar fp+ headErr (InvalidFile fp "Couldn't parse drawing") (fromCursor cur)++worksheetFile :: FilePath -> Relationships -> Text -> Int -> SheetState -> RefId -> Parser WorksheetFile+worksheetFile parentPath wbRels name sheetId visibility rId =+ WorksheetFile name sheetId visibility <$> lookupRelPath parentPath wbRels rId++getRels :: Zip.Archive -> FilePath -> Parser Relationships+getRels ar fp = do+ let (dir, file) = splitFileName fp+ relsPath = dir </> "_rels" </> file <.> "rels"+ c <- xmlCursorOptional ar relsPath+ return $ maybe Relationships.empty (setTargetsFrom fp . headNote "Missing rels" . fromCursor) c++-- According to part 2, section 7.3.4 of ECMA-376, when mapping logical item+-- names to ZIP item names we need to remove the leading slash.+--+-- Non-ASCII characters should be percent-encoded as well, but this is not+-- currently implemented.+--+-- https://ecma-international.org/publications-and-standards/standards/ecma-376/+logicalNameToZipItemName :: FilePath -> FilePath+logicalNameToZipItemName ('/' : name) = name+logicalNameToZipItemName name = name++lookupRelPath :: FilePath+ -> Relationships+ -> RefId+ -> Either ParseError FilePath+lookupRelPath fp rels rId =+ logicalNameToZipItemName . relTarget <$> note (InvalidRef fp rId) (Relationships.lookup rId rels)
src/Codec/Xlsx/Parser/Internal.hs view
@@ -1,26 +1,162 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module Codec.Xlsx.Parser.Internal- ( n- , parseSharedStrings- ) where+ ( ParseException(..)+ , n_+ , addSmlNamespace+ , nodeElNameIs+ , FromCursor(..)+ , FromAttrVal(..)+ , fromAttribute+ , fromAttributeDef+ , maybeAttribute+ , fromElementValue+ , maybeElementValue+ , maybeElementValueDef+ , maybeBoolElementValue+ , maybeFromElement+ , attrValIs+ , contentOrEmpty+ , readSuccess+ , readFailure+ , invalidText+ , defaultReadFailure+ , module Codec.Xlsx.Parser.Internal.Util+ , module Codec.Xlsx.Parser.Internal.Fast+ ) where -import qualified Data.IntMap as IM+import Control.Exception (Exception)+import Data.Maybe import Data.Text (Text) import qualified Data.Text as T-import Data.XML.Types+import qualified Data.Text.Read as T+import Data.Typeable (Typeable)+import GHC.Generics (Generic)+import Text.XML import Text.XML.Cursor +import Codec.Xlsx.Writer.Internal+import Codec.Xlsx.Parser.Internal.Fast+import Codec.Xlsx.Parser.Internal.Util +data ParseException = ParseException String+ deriving (Show, Typeable, Generic)++instance Exception ParseException++nodeElNameIs :: Node -> Name -> Bool+nodeElNameIs (NodeElement el) name = elementName el == name+nodeElNameIs _ _ = False++class FromCursor a where+ fromCursor :: Cursor -> [a]++class FromAttrVal a where+ fromAttrVal :: T.Reader a++instance FromAttrVal Text where+ fromAttrVal = readSuccess++instance FromAttrVal Int where+ fromAttrVal = T.signed T.decimal++instance FromAttrVal Integer where+ fromAttrVal = T.signed T.decimal++instance FromAttrVal Double where+ fromAttrVal = T.rational++instance FromAttrVal Bool where+ fromAttrVal x | x == "1" || x == "true" = readSuccess True+ | x == "0" || x == "false" = readSuccess False+ | otherwise = defaultReadFailure++-- | required attribute parsing+fromAttribute :: FromAttrVal a => Name -> Cursor -> [a]+fromAttribute name cursor =+ attribute name cursor >>= runReader fromAttrVal++-- | parsing optional attributes with defaults+fromAttributeDef :: FromAttrVal a => Name -> a -> Cursor -> [a]+fromAttributeDef name defVal cursor =+ case attribute name cursor of+ [attr] -> runReader fromAttrVal attr+ _ -> [defVal]++-- | parsing optional attributes+maybeAttribute :: FromAttrVal a => Name -> Cursor -> [Maybe a]+maybeAttribute name cursor =+ case attribute name cursor of+ [attr] -> Just <$> runReader fromAttrVal attr+ _ -> [Nothing]++fromElementValue :: FromAttrVal a => Name -> Cursor -> [a]+fromElementValue name cursor =+ cursor $/ element name >=> fromAttribute "val"++maybeElementValue :: FromAttrVal a => Name -> Cursor -> [Maybe a]+maybeElementValue name cursor =+ case cursor $/ element name of+ [cursor'] -> maybeAttribute "val" cursor'+ _ -> [Nothing]++maybeElementValueDef :: FromAttrVal a => Name -> a -> Cursor -> [Maybe a]+maybeElementValueDef name defVal cursor =+ case cursor $/ element name of+ [cursor'] -> Just . fromMaybe defVal <$> maybeAttribute "val" cursor'+ _ -> [Nothing]++maybeBoolElementValue :: Name -> Cursor -> [Maybe Bool]+maybeBoolElementValue name cursor = maybeElementValueDef name True cursor++maybeFromElement :: FromCursor a => Name -> Cursor -> [Maybe a]+maybeFromElement name cursor = case cursor $/ element name of+ [cursor'] -> Just <$> fromCursor cursor'+ _ -> [Nothing]++attrValIs :: (Eq a, FromAttrVal a) => Name -> a -> Axis+attrValIs n v c =+ case fromAttribute n c of+ [x] | x == v -> [c]+ _ -> []++contentOrEmpty :: Cursor -> [Text]+contentOrEmpty c =+ case c $/ content of+ [t] -> [t]+ [] -> [""]+ _ -> error "invalid item: more than one text node encountered"++readSuccess :: a -> Either String (a, Text)+readSuccess x = Right (x, T.empty)++readFailure :: Text -> Either String (a, Text)+readFailure = Left . T.unpack++invalidText :: Text -> Text -> Either String (a, Text)+invalidText what txt = readFailure $ T.concat ["Invalid ", what, ": '", txt , "'"]++defaultReadFailure :: Either String (a, Text)+defaultReadFailure = Left "invalid text"++runReader :: T.Reader a -> Text -> [a]+runReader reader t = case reader t of+ Right (r, leftover) | T.null leftover -> [r]+ _ -> []+++{-# DEPRECATED n_ "Renamed to addSmlNamespace" #-}+n_ :: Text -> Name+n_ = addSmlNamespace+ -- | Add sml namespace to name-n :: Text -> Name-n x = Name+addSmlNamespace :: Text -> Name+addSmlNamespace x = Name { nameLocalName = x- , nameNamespace = Just "http://schemas.openxmlformats.org/spreadsheetml/2006/main"+ , nameNamespace = Just mainNamespace , namePrefix = Nothing }--parseSharedStrings :: Cursor -> IM.IntMap Text-parseSharedStrings c = IM.fromAscList $ zip [0..] (c $/ element (n"si") >=> parseT)- where- -- it's either <t> or <r>s with <t> inside- parseT c' = [T.concat $ c' $| orSelf (child >=> (element (n"r"))) &/ element (n"t") &/ content]
+ src/Codec/Xlsx/Parser/Internal/Fast.hs view
@@ -0,0 +1,360 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+module Codec.Xlsx.Parser.Internal.Fast+ ( FromXenoNode(..)+ , collectChildren+ , maybeChild+ , requireChild+ , childList+ , maybeFromChild+ , fromChild+ , fromChildList+ , maybeParse+ , requireAndParse+ , childListAny+ , maybeElementVal+ , toAttrParser+ , parseAttributes+ , FromAttrBs(..)+ , unexpectedAttrBs+ , maybeAttrBs+ , maybeAttr+ , fromAttr+ , fromAttrDef+ , contentBs+ , contentX+ , nsPrefixes+ , addPrefix+ ) where++import Control.Applicative+import Control.Arrow (second)+import Control.Exception (Exception, throw)+import Control.Monad (ap, forM, join, liftM)+import Data.Bifunctor (first)+import Data.Bits ((.|.), shiftL)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Unsafe as SU+import Data.Char (chr)+import Data.Maybe+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Word (Word8)+import Xeno.DOM hiding (parse)++import Codec.Xlsx.Parser.Internal.Util++class FromXenoNode a where+ fromXenoNode :: Node -> Either Text a++newtype ChildCollector a = ChildCollector+ { runChildCollector :: [Node] -> Either Text ([Node], a)+ }++instance Functor ChildCollector where+ fmap f a = ChildCollector $ \ns ->+ second f <$> runChildCollector a ns++instance Applicative ChildCollector where+ pure a = ChildCollector $ \ns ->+ return (ns, a)+ cf <*> ca = ChildCollector $ \ns -> do+ (ns', f) <- runChildCollector cf ns+ (ns'', a) <- runChildCollector ca ns'+ return (ns'', f a)++instance Alternative ChildCollector where+ empty = ChildCollector $ \_ -> Left "ChildCollector.empty"+ ChildCollector f <|> ChildCollector g = ChildCollector $ \ns ->+ either (const $ g ns) Right (f ns)++instance Monad ChildCollector where+ return = pure+ ChildCollector f >>= g = ChildCollector $ \ns ->+ either Left (\(!ns', f') -> runChildCollector (g f') ns') (f ns)++toChildCollector :: Either Text a -> ChildCollector a+toChildCollector unlifted =+ case unlifted of+ Right a -> return a+ Left e -> ChildCollector $ \_ -> Left e++collectChildren :: Node -> ChildCollector a -> Either Text a+collectChildren n c = snd <$> runChildCollector c (children n)++maybeChild :: ByteString -> ChildCollector (Maybe Node)+maybeChild nm =+ ChildCollector $ \case+ (n:ns)+ | name n == nm -> pure (ns, Just n)+ ns -> pure (ns, Nothing)++requireChild :: ByteString -> ChildCollector Node+requireChild nm =+ ChildCollector $ \case+ (n:ns)+ | name n == nm -> pure (ns, n)+ _ ->+ Left $ "required element " <> T.pack (show nm) <> " was not found"++childList :: ByteString -> ChildCollector [Node]+childList nm = do+ mNode <- maybeChild nm+ case mNode of+ Just n -> (n:) <$> childList nm+ Nothing -> return []++maybeFromChild :: (FromXenoNode a) => ByteString -> ChildCollector (Maybe a)+maybeFromChild nm = do+ mNode <- maybeChild nm+ mapM (toChildCollector . fromXenoNode) mNode++fromChild :: (FromXenoNode a) => ByteString -> ChildCollector a+fromChild nm = do+ n <- requireChild nm+ case fromXenoNode n of+ Right a -> return a+ Left e -> ChildCollector $ \_ -> Left e++fromChildList :: (FromXenoNode a) => ByteString -> ChildCollector [a]+fromChildList nm = do+ mA <- maybeFromChild nm+ case mA of+ Just a -> (a:) <$> fromChildList nm+ Nothing -> return []++maybeParse :: ByteString -> (Node -> Either Text a) -> ChildCollector (Maybe a)+maybeParse nm parse = maybeChild nm >>= (toChildCollector . mapM parse)++requireAndParse :: ByteString -> (Node -> Either Text a) -> ChildCollector a+requireAndParse nm parse = requireChild nm >>= (toChildCollector . parse)++childListAny :: (FromXenoNode a) => Node -> Either Text [a]+childListAny = mapM fromXenoNode . children++maybeElementVal :: (FromAttrBs a) => ByteString -> ChildCollector (Maybe a)+maybeElementVal nm = do+ mN <- maybeChild nm+ fmap join . forM mN $ \n ->+ toChildCollector . parseAttributes n $ maybeAttr "val"++-- Stolen from XML Conduit+newtype AttrParser a = AttrParser+ { runAttrParser :: [(ByteString, ByteString)] -> Either Text ( [( ByteString+ , ByteString)]+ , a)+ }++instance Monad AttrParser where+ return a = AttrParser $ \as -> Right (as, a)+ (AttrParser f) >>= g =+ AttrParser $ \as ->+ either Left (\(as', f') -> runAttrParser (g f') as') (f as)+instance Applicative AttrParser where+ pure = return+ (<*>) = ap+instance Functor AttrParser where+ fmap = liftM++attrError :: Text -> AttrParser a+attrError err = AttrParser $ \_ -> Left err++toAttrParser :: Either Text a -> AttrParser a+toAttrParser unlifted =+ case unlifted of+ Right a -> return a+ Left e -> AttrParser $ \_ -> Left e++maybeAttrBs :: ByteString -> AttrParser (Maybe ByteString)+maybeAttrBs attrName = AttrParser $ go id+ where+ go front [] = Right (front [], Nothing)+ go front (a@(nm, val):as) =+ if nm == attrName+ then Right (front as, Just val)+ else go (front . (:) a) as++requireAttrBs :: ByteString -> AttrParser ByteString+requireAttrBs nm = do+ mVal <- maybeAttrBs nm+ case mVal of+ Just val -> return val+ Nothing -> attrError $ "attribute " <> T.pack (show nm) <> " is required"++unexpectedAttrBs :: Text -> ByteString -> Either Text a+unexpectedAttrBs typ val =+ Left $ "Unexpected value for " <> typ <> ": " <> T.pack (show val)++fromAttr :: FromAttrBs a => ByteString -> AttrParser a+fromAttr nm = do+ bs <- requireAttrBs nm+ toAttrParser $ fromAttrBs bs++maybeAttr :: FromAttrBs a => ByteString -> AttrParser (Maybe a)+maybeAttr nm = do+ mBs <- maybeAttrBs nm+ forM mBs (toAttrParser . fromAttrBs)++fromAttrDef :: FromAttrBs a => ByteString -> a -> AttrParser a+fromAttrDef nm defVal = fromMaybe defVal <$> maybeAttr nm++parseAttributes :: Node -> AttrParser a -> Either Text a+parseAttributes n attrParser =+ case runAttrParser attrParser (attributes n) of+ Left e -> Left e+ Right (_, a) -> return a++class FromAttrBs a where+ fromAttrBs :: ByteString -> Either Text a++instance FromAttrBs ByteString where+ fromAttrBs = pure++instance FromAttrBs Bool where+ fromAttrBs x | x == "1" || x == "true" = return True+ | x == "0" || x == "false" = return False+ | otherwise = unexpectedAttrBs "boolean" x++instance FromAttrBs Int where+ -- it appears that parser in text is more optimized than the one in+ -- attoparsec at least as of text-1.2.2.2 and attoparsec-0.13.1.0+ fromAttrBs = first T.pack . eitherDecimal . T.decodeLatin1++instance FromAttrBs Double where+ -- as for rationals+ fromAttrBs = first T.pack . eitherRational . T.decodeLatin1++instance FromAttrBs Text where+ fromAttrBs = replaceEntititesBs++replaceEntititesBs :: ByteString -> Either Text Text+replaceEntititesBs str =+ T.decodeUtf8 . BS.concat <$> findAmp 0+ where+ findAmp :: Int -> Either Text [ByteString]+ findAmp index =+ case elemIndexFrom ampersand str index of+ Nothing -> if BS.null text then return [] else return [text]+ where text = BS.drop index str+ Just fromAmp ->+ if BS.null text+ then checkEntity fromAmp+ else (text:) <$> checkEntity fromAmp+ where text = substring str index fromAmp+ checkEntity index =+ case elemIndexFrom semicolon str index of+ Just fromSemi | fromSemi >= index + 3 -> do+ entity <- checkElementVal (index + 1) (fromSemi - index - 1)+ (BS.singleton entity:) <$> findAmp (fromSemi + 1)+ _ -> Left "Unending entity"+ checkElementVal index len =+ if | len == 2+ && s_index this 0 == 108 -- l+ && s_index this 1 == 116 -- t+ -> return 60 -- '<'+ | len == 2+ && s_index this 0 == 103 -- g+ && s_index this 1 == 116 -- t+ -> return 62 -- '>'+ | len == 3+ && s_index this 0 == 97 -- a+ && s_index this 1 == 109 -- m+ && s_index this 2 == 112 -- p+ -> return 38 -- '&'+ | len == 4+ && s_index this 0 == 113 -- q+ && s_index this 1 == 117 -- u+ && s_index this 2 == 111 -- o+ && s_index this 3 == 116 -- t+ -> return 34 -- '"'+ | len == 4+ && s_index this 0 == 97 -- a+ && s_index this 1 == 112 -- p+ && s_index this 2 == 111 -- o+ && s_index this 3 == 115 -- s+ -> return 39 -- '\''+ | s_index this 0 == 35 -- '#'+ ->+ if s_index this 1 == 120 -- 'x'+ then toEnum <$> checkHexadecimal (index + 2) (len - 2)+ else toEnum <$> checkDecimal (index + 1) (len - 1)+ | otherwise -> Left $ "Bad entity " <> T.pack (show $ (substring str (index-1) (index+len+1)))+ where+ this = BS.drop index str+ checkDecimal index len = BS.foldl' go (Right 0) (substring str index (index + len))+ where+ go :: Either Text Int -> Word8 -> Either Text Int+ go prev c = do+ a <- prev+ if c >= 48 && c <= 57+ then return $ a * 10 + fromIntegral (c - 48)+ else Left $ "Expected decimal digit but encountered " <> T.pack (show (chr $ fromIntegral c))+ checkHexadecimal index len = BS.foldl' go (Right 0) (substring str index (index + len))+ where+ go :: Either Text Int -> Word8 -> Either Text Int+ go prev c = do+ a <- prev+ if | c >= 48 && c <= 57+ -> return $ (a `shiftL` 4) .|. fromIntegral (c - 48)+ | c >= 97 && c <= 122+ -> return $ (a `shiftL` 4) .|. fromIntegral (c - 87)+ | c >= 65 && c <= 90+ -> return $ (a `shiftL` 4) .|. fromIntegral (c - 55)+ | otherwise+ ->+ Left $ "Expected hexadecimal digit but encountered " <> T.pack (show (chr $ fromIntegral c))+ ampersand = 38+ semicolon = 59++data EntityReplaceException = EntityReplaceException deriving Show++instance Exception EntityReplaceException++-- | /O(1)/ 'ByteString' index (subscript) operator, starting from 0.+s_index :: ByteString -> Int -> Word8+s_index ps n+ | n < 0 = throw EntityReplaceException+ | n >= BS.length ps = throw EntityReplaceException+ | otherwise = ps `SU.unsafeIndex` n+{-# INLINE s_index #-}++-- | Get index of an element starting from offset.+elemIndexFrom :: Word8 -> ByteString -> Int -> Maybe Int+elemIndexFrom c str offset = fmap (+ offset) (BS.elemIndex c (BS.drop offset str))+-- Without the INLINE below, the whole function is twice as slow and+-- has linear allocation. See git commit with this comment for+-- results.+{-# INLINE elemIndexFrom #-}++-- | Get a substring of a string.+substring :: ByteString -> Int -> Int -> ByteString+substring s start end = BS.take (end - start) (BS.drop start s)+{-# INLINE substring #-}++newtype NsPrefixes = NsPrefixes [(ByteString, ByteString)]++nsPrefixes :: Node -> NsPrefixes+nsPrefixes root =+ NsPrefixes . flip mapMaybe (attributes root) $ \(nm, val) ->+ (val, ) <$> BS.stripPrefix "xmlns:" nm++addPrefix :: NsPrefixes -> ByteString -> (ByteString -> ByteString)+addPrefix (NsPrefixes prefixes) ns =+ maybe id (\prefix nm -> BS.concat [prefix, ":", nm]) $ Prelude.lookup ns prefixes++contentBs :: Node -> ByteString+contentBs n = BS.concat . map toBs $ contents n+ where+ toBs (Element _) = BS.empty+ toBs (Text bs) = bs+ toBs (CData bs) = bs++contentX :: Node -> Either Text Text+contentX = replaceEntititesBs . contentBs
+ src/Codec/Xlsx/Parser/Internal/Memoize.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}++-- | I rewrote: https://hackage.haskell.org/package/unliftio-0.2.20/docs/src/UnliftIO.Memoize.html#Memoized+-- for monad trans basecontrol+-- we don't need a generic `m` anyway. it's good enough in base IO.+module Codec.Xlsx.Parser.Internal.Memoize+ ( Memoized+ , runMemoized+ , memoizeRef+ ) where++import Control.Applicative as A+import Control.Monad (join)+import Control.Monad.IO.Class+import Data.IORef+import Control.Exception++-- | A \"run once\" value, with results saved. Extract the value with+-- 'runMemoized'. For single-threaded usage, you can use 'memoizeRef' to+-- create a value. If you need guarantees that only one thread will run the+-- action at a time, use 'memoizeMVar'.+--+-- Note that this type provides a 'Show' instance for convenience, but not+-- useful information can be provided.+newtype Memoized a = Memoized (IO a)+ deriving (Functor, A.Applicative, Monad)+instance Show (Memoized a) where+ show _ = "<<Memoized>>"++-- | Extract a value from a 'Memoized', running an action if no cached value is+-- available.+runMemoized :: MonadIO m => Memoized a -> m a+runMemoized (Memoized m) = liftIO m+{-# INLINE runMemoized #-}++-- | Create a new 'Memoized' value using an 'IORef' under the surface. Note that+-- the action may be run in multiple threads simultaneously, so this may not be+-- thread safe (depending on the underlying action).+memoizeRef :: IO a -> IO (Memoized a)+memoizeRef action = do+ ref <- newIORef Nothing+ pure $ Memoized $ do+ mres <- readIORef ref+ res <-+ case mres of+ Just res -> pure res+ Nothing -> do+ res <- try @SomeException action+ writeIORef ref $ Just res+ pure res+ either throwIO pure res
+ src/Codec/Xlsx/Parser/Internal/PivotTable.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Parser.Internal.PivotTable+ ( parsePivotTable+ , parseCache+ , fillCacheFieldsFromRecords+ ) where++import Control.Applicative+import Data.ByteString.Lazy (ByteString)+import Data.List (transpose)+import Data.Maybe (listToMaybe, mapMaybe, maybeToList)+import Data.Text (Text)+import Safe (atMay)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.Relationships (odr)+import Codec.Xlsx.Types.PivotTable+import Codec.Xlsx.Types.PivotTable.Internal++parsePivotTable+ :: (CacheId -> Maybe (Text, Range, [CacheField]))+ -> ByteString+ -> Maybe PivotTable+parsePivotTable srcByCacheId bs =+ listToMaybe . parse . fromDocument $ parseLBS_ def bs+ where+ parse cur = do+ cacheId <- fromAttribute "cacheId" cur+ case srcByCacheId cacheId of+ Nothing -> fail "no such cache"+ Just (_pvtSrcSheet, _pvtSrcRef, cacheFields) -> do+ _pvtDataCaption <- attribute "dataCaption" cur+ _pvtName <- attribute "name" cur+ _pvtLocation <- cur $/ element (addSmlNamespace "location") >=> fromAttribute "ref"+ _pvtRowGrandTotals <- fromAttributeDef "rowGrandTotals" True cur+ _pvtColumnGrandTotals <- fromAttributeDef "colGrandTotals" True cur+ _pvtOutline <- fromAttributeDef "outline" False cur+ _pvtOutlineData <- fromAttributeDef "outlineData" False cur+ let pvtFieldsWithHidden =+ cur $/ element (addSmlNamespace "pivotFields") &/ element (addSmlNamespace "pivotField") >=> \c -> do+ -- actually gets overwritten from cache to have consistent field names+ _pfiName <- maybeAttribute "name" c+ _pfiSortType <- fromAttributeDef "sortType" FieldSortManual c+ _pfiOutline <- fromAttributeDef "outline" True c+ let hidden =+ c $/ element (addSmlNamespace "items") &/ element (addSmlNamespace "item") >=>+ attrValIs "h" True >=> fromAttribute "x"+ _pfiHiddenItems = []+ return (PivotFieldInfo {..}, hidden)+ _pvtFields = flip map (zip [0.. ] pvtFieldsWithHidden) $+ \(i, (PivotFieldInfo {..}, hidden)) ->+ let _pfiHiddenItems =+ [item | (n, item) <- zip [(0 :: Int) ..] items, n `elem` hidden]+ (_pfiName, items) = case atMay cacheFields i of+ Just CacheField{..} -> (Just cfName, cfItems)+ Nothing -> (Nothing, [])+ in PivotFieldInfo {..}+ nToFieldName = zip [0 ..] $ map cfName cacheFields+ fieldNameList fld = maybeToList $ lookup fld nToFieldName+ _pvtRowFields =+ cur $/ element (addSmlNamespace "rowFields") &/ element (addSmlNamespace "field") >=>+ fromAttribute "x" >=> fieldPosition+ _pvtColumnFields =+ cur $/ element (addSmlNamespace "colFields") &/ element (addSmlNamespace "field") >=>+ fromAttribute "x" >=> fieldPosition+ _pvtDataFields =+ cur $/ element (addSmlNamespace "dataFields") &/ element (addSmlNamespace "dataField") >=> \c -> do+ fld <- fromAttribute "fld" c+ _dfField <- fieldNameList fld+ -- TOFIX+ _dfName <- fromAttributeDef "name" "" c+ _dfFunction <- fromAttributeDef "subtotal" ConsolidateSum c+ return DataField {..}+ fieldPosition :: Int -> [PositionedField]+ fieldPosition (-2) = return DataPosition+ fieldPosition n =+ FieldPosition <$> fieldNameList n+ return PivotTable {..}++parseCache :: ByteString -> Maybe (Text, CellRef, [CacheField], Maybe RefId)+parseCache bs = listToMaybe . parse . fromDocument $ parseLBS_ def bs+ where+ parse cur = do+ refId <- maybeAttribute (odr "id") cur+ (sheet, ref) <-+ cur $/ element (addSmlNamespace "cacheSource") &/ element (addSmlNamespace "worksheetSource") >=>+ liftA2 (,) <$> attribute "sheet" <*> fromAttribute "ref"+ let fields =+ cur $/ element (addSmlNamespace "cacheFields") &/ element (addSmlNamespace "cacheField") >=>+ fromCursor+ return (sheet, ref, fields, refId)++fillCacheFieldsFromRecords :: [CacheField] -> [CacheRecord] -> [CacheField]+fillCacheFieldsFromRecords fields recs =+ zipWith addValues fields (transpose recs)+ where+ addValues field recVals =+ if null (cfItems field)+ then field {cfItems = mapMaybe recToCellValue recVals}+ else field+ recToCellValue (CacheText t) = Just $ CellText t+ recToCellValue (CacheNumber n) = Just $ CellDouble n+ recToCellValue (CacheIndex _) = Nothing
+ src/Codec/Xlsx/Parser/Internal/Util.hs view
@@ -0,0 +1,43 @@+module Codec.Xlsx.Parser.Internal.Util+ ( boolean+ , eitherBoolean+ , decimal+ , eitherDecimal+ , rational+ , eitherRational+ ) where++import Data.Text (Text)+import Control.Monad.Fail (MonadFail)+import qualified Data.Text as T+import qualified Data.Text.Read as T+import qualified Control.Monad.Fail as F++decimal :: (MonadFail m, Integral a) => Text -> m a+decimal = fromEither . eitherDecimal++eitherDecimal :: (Integral a) => Text -> Either String a+eitherDecimal t = case T.signed T.decimal t of+ Right (d, leftover) | T.null leftover -> Right d+ _ -> Left $ "invalid decimal: " ++ show t++rational :: (MonadFail m) => Text -> m Double+rational = fromEither . eitherRational++eitherRational :: Text -> Either String Double+eitherRational t = case T.signed T.rational t of+ Right (r, leftover) | T.null leftover -> Right r+ _ -> Left $ "invalid rational: " ++ show t++boolean :: (MonadFail m) => Text -> m Bool+boolean = fromEither . eitherBoolean++eitherBoolean :: Text -> Either String Bool+eitherBoolean t = case T.unpack $ T.strip t of+ "true" -> Right True+ "false" -> Right False+ _ -> Left $ "invalid boolean: " ++ show t++fromEither :: (MonadFail m) => Either String b -> m b+fromEither (Left a) = F.fail a+fromEither (Right b) = return b
+ src/Codec/Xlsx/Parser/Stream.hs view
@@ -0,0 +1,718 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE PatternSynonyms #-}++-- |+-- Module : Codex.Xlsx.Parser.Stream+-- Description : Stream parser for xlsx files+-- Copyright :+-- (c) Adam, 2021+-- (c) Supercede, 2021+-- License : MIT+-- Stability : experimental+-- Portability : POSIX+--+-- Parse @.xlsx@ sheets in constant memory.+--+-- All actions on an xlsx file run inside the 'XlsxM' monad, and must+-- be run with 'runXlsxM'. XlsxM is not a monad transformer, a design+-- inherited from the "zip" package's ZipArchive monad.+--+-- Inside the XlsxM monad, you can stream 'SheetItem's (a row) from a+-- particular sheet, using 'readSheetByIndex', which is callback-based and tied to IO.+--+module Codec.Xlsx.Parser.Stream+ ( XlsxM+ , runXlsxM+ , WorkbookInfo(..)+ , SheetInfo(..)+ , wiSheets+ , getOrParseSharedStringss+ , getWorkbookInfo+ , CellRow+ , readSheet+ , readSheetIdentifier+ , countRowsInSheet+ , countRowsInSheetIdentifier+ , collectItems+ , collectItemsIdentifier+ -- ** Index+ , SheetIndex+ , makeIndex+ , makeIndexFromName+ -- ** Identifier+ , SheetIdentifier+ , getSheetIdentifier+ , makeIdentifierFromName+ -- ** SheetItem+ , SheetItem(..)+ , si_sheet_index+ , si_row+ -- ** Row+ , Row(..)+ , ri_row_index+ , ri_cell_row+ -- * Errors+ , SheetErrors(..)+ , AddCellErrors(..)+ , CoordinateErrors(..)+ , TypeError(..)+ , WorkbookError(..)+ ) where++import qualified "zip" Codec.Archive.Zip as Zip+import Codec.Xlsx.Types.Cell+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.Internal (RefId (..))+import Codec.Xlsx.Types.Internal.Relationships (Relationship (..),+ Relationships (..))+import Conduit (PrimMonad, (.|))+import qualified Conduit as C+import qualified Data.Vector as V+import Codec.Xlsx.LensCompat++import Codec.Xlsx.Parser.Internal+import Control.Monad+import Control.Monad.Catch+import Control.Monad.Except+import Control.Monad.Reader+import Control.Monad.State.Strict+import Data.Bifunctor+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Conduit (ConduitT)+import qualified Data.DList as DL+import Data.Foldable+import Data.IORef+import qualified Data.IntMap.Strict as IntMap+import qualified Data.Map.Strict as M+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Builder as TB+import qualified Data.Text.Read as Read+import Data.Traversable (for)+import Data.XML.Types+import GHC.Generics+import Codec.Xlsx.Parser.Internal.Memoize++import qualified Codec.Xlsx.Parser.Stream.HexpatInternal as HexpatInternal+import Control.Monad.Base+import Control.Monad.Trans.Control+import Text.XML.Expat.Internal.IO as Hexpat+import Text.XML.Expat.SAX as Hexpat+import Codec.Xlsx.Types+import Codec.Xlsx.Parser.Stream.SheetInfo+import Codec.Xlsx.Parser.Stream.Row+import Codec.Xlsx.Parser.Stream.SheetItem+import Codec.Xlsx.Parser.Stream.SheetIdentifier+import Codec.Xlsx.Parser.Stream.SheetIndex++type SharedStringsMap = V.Vector Text++-- | Type of the excel value+--+-- Note: Some values are untyped and rules of their type resolution are not known.+-- They may be treated simply as strings as well as they may be context-dependent.+-- By far we do not bother with it.+data ExcelValueType+ = TS -- ^ shared string+ | TStr -- ^ either an inline string ("inlineStr") or a formula string ("str")+ | TN -- ^ number+ | TB -- ^ boolean+ | TE -- ^ excell error, the sheet can contain error values, for example if =1/0, causes division by zero+ | Untyped -- ^ Not all values have types+ deriving stock (Generic, Show)++-- | State for parsing sheets+data ParserState = MkParserState+ { _ps_row :: ~CellRow -- ^ Current row+ , _ps_cell_row_index :: RowIndex -- ^ Current row number+ , _ps_cell_col_index :: ColumnIndex -- ^ Current column number+ , _ps_cell_style :: Maybe Int+ , _ps_is_in_val :: Bool -- ^ Flag for indexing wheter the parser is in value or not+ , _ps_shared_strings :: SharedStringsMap -- ^ Shared string map+ , _ps_type :: ExcelValueType -- ^ The last detected value type++ , _ps_text_buf :: Text+ -- ^ for hexpat only, which can break up char data into multiple events+ , _ps_worksheet_ended :: Bool+ -- ^ For hexpat only, which can throw errors right at the end of the sheet+ -- rather than ending gracefully.+ } deriving stock (Generic, Show)+makeLenses 'MkParserState++-- | State for parsing shared strings+data SharedStringsState = MkSharedStringsState+ { _ss_string :: TB.Builder -- ^ String we are parsing+ -- TODO: At the moment SharedStrings can be used only to create CellText values.+ -- We should add support for CellRich values.+ , _ss_list :: DL.DList Text -- ^ list of shared strings+ } deriving stock (Generic, Show)+makeLenses 'MkSharedStringsState++type HasParserState = MonadState ParserState+type HasSharedStringsState = MonadState SharedStringsState++-- | Information about the workbook contained in xl/workbook.xml+-- (currently a subset)+data WorkbookInfo = WorkbookInfo+ { _wiSheets :: [SheetInfo]+ } deriving Show+makeLenses 'WorkbookInfo++data XlsxMState = MkXlsxMState+ { _xs_shared_strings :: Memoized (V.Vector Text)+ , _xs_workbook_info :: Memoized WorkbookInfo+ , _xs_relationships :: Memoized Relationships+ }++newtype XlsxM a = XlsxM {_unXlsxM :: ReaderT XlsxMState Zip.ZipArchive a}+ deriving newtype+ ( Functor,+ Applicative,+ Monad,+ MonadIO,+ MonadCatch,+ MonadMask,+ MonadThrow,+ MonadReader XlsxMState,+ MonadBase IO,+ MonadBaseControl IO+ )++-- | Initial parsing state+initialParserState :: ParserState+initialParserState = MkParserState+ { _ps_row = mempty+ , _ps_cell_row_index = 0+ , _ps_cell_col_index = 0+ , _ps_is_in_val = False+ , _ps_shared_strings = mempty+ , _ps_type = Untyped+ , _ps_text_buf = mempty+ , _ps_worksheet_ended = False+ , _ps_cell_style = Nothing+ }++-- | Initial parsing state+initialSharedStrings :: SharedStringsState+initialSharedStrings = MkSharedStringsState+ { _ss_string = mempty+ , _ss_list = mempty+ }++-- | Parse shared string entry from xml event and return it once+-- we've reached the end of given element+{-# SCC parseSharedStrings #-}+parseSharedStrings+ :: ( MonadThrow m+ , HasSharedStringsState m+ )+ => HexpatEvent -> m (Maybe Text)+parseSharedStrings = \case+ -- TODO: Add parsing of text styles to further create CellRich values.+ StartElement "si" _ -> Nothing <$ (ss_string .= mempty)+ EndElement "si" -> Just . LT.toStrict . TB.toLazyText <$> gets _ss_string+ CharacterData txt -> Nothing <$ (ss_string <>= TB.fromText txt)+ _ -> pure Nothing++-- | Run a series of actions on an Xlsx file+runXlsxM :: MonadIO m => FilePath -> XlsxM a -> m a+runXlsxM xlsxFile (XlsxM act) = liftIO $ do+ -- TODO: don't run the withArchive multiple times but use liftWith or runInIO instead+ _xs_workbook_info <- memoizeRef (Zip.withArchive xlsxFile readWorkbookInfo)+ _xs_relationships <- memoizeRef (Zip.withArchive xlsxFile readWorkbookRelationships)+ _xs_shared_strings <- memoizeRef (Zip.withArchive xlsxFile parseSharedStringss)+ Zip.withArchive xlsxFile $ runReaderT act $ MkXlsxMState{..}++liftZip :: Zip.ZipArchive a -> XlsxM a+liftZip = XlsxM . ReaderT . const++parseSharedStringss :: Zip.ZipArchive (V.Vector Text)+parseSharedStringss = do+ sharedStrsSel <- Zip.mkEntrySelector "xl/sharedStrings.xml"+ hasSharedStrs <- Zip.doesEntryExist sharedStrsSel+ if not hasSharedStrs+ then pure mempty+ else do+ let state0 = initialSharedStrings+ byteSrc <- Zip.getEntrySource sharedStrsSel+ st <- liftIO $ runExpat state0 byteSrc $ \evs -> forM_ evs $ \ev -> do+ mTxt <- parseSharedStrings ev+ for_ mTxt $ \txt ->+ ss_list %= (`DL.snoc` txt)+ pure $ V.fromList $ DL.toList $ _ss_list st++{-# SCC getOrParseSharedStringss #-}+getOrParseSharedStringss :: XlsxM (V.Vector Text)+getOrParseSharedStringss = runMemoized =<< asks _xs_shared_strings++readWorkbookInfo :: Zip.ZipArchive WorkbookInfo+readWorkbookInfo = do+ sel <- Zip.mkEntrySelector "xl/workbook.xml"+ src <- Zip.getEntrySource sel+ sheetIndexRef <- liftIO $ newIORef 0+ sheets <- liftIO $ runExpat [] src $ \evs -> forM_ evs $ \case+ StartElement ("sheet" :: ByteString) attrs -> do+ nm <- lookupBy "name" attrs+ sheetId <- lookupBy "sheetId" attrs+ rId <- lookupBy "r:id" attrs+ sheetState <- case lookup "state" attrs of+ Nothing -> pure Visible -- default to visible if not found+ Just ss -> case ss of+ "visible" -> pure Visible+ "hidden" -> pure Hidden+ "veryHidden" -> pure VeryHidden+ _ -> throwM $ InvalidSheetState ss+ sheetNum <- either (throwM . ParseDecimalError sheetId) pure $ eitherDecimal sheetId+ sheetIndex <- liftIO $ atomicModifyIORef' sheetIndexRef $ \i -> (succ i, i)+ let info = MkSheetInfo nm (RefId rId) sheetNum sheetIndex sheetState+ modify' (info :)+ _ -> pure ()+ pure $ WorkbookInfo sheets++lookupBy :: MonadThrow m => ByteString -> [(ByteString, Text)] -> m Text+lookupBy fields attrs = maybe (throwM $ LookupError attrs fields) pure $ lookup fields attrs++-- | Returns information about the workbook, found in+-- xl/workbook.xml. The result is cached so the XML will only be+-- decompressed and parsed once inside a larger XlsxM action.+getWorkbookInfo :: XlsxM WorkbookInfo+getWorkbookInfo = runMemoized =<< asks _xs_workbook_info++readWorkbookRelationships :: Zip.ZipArchive Relationships+readWorkbookRelationships = do+ sel <- Zip.mkEntrySelector "xl/_rels/workbook.xml.rels"+ src <- Zip.getEntrySource sel+ liftIO $ fmap Relationships $ runExpat mempty src $ \evs -> forM_ evs $ \case+ StartElement ("Relationship" :: ByteString) attrs -> do+ rId <- lookupBy "Id" attrs+ rTarget <- lookupBy "Target" attrs+ rType <- lookupBy "Type" attrs+ modify' $ M.insert (RefId rId) $+ Relationship { relType = rType,+ relTarget = T.unpack rTarget+ }+ _ -> pure ()++-- | Gets relationships for the workbook (this means the filenames in+-- the relationships map are relative to "xl/" base path within the+-- zip file.+--+-- The relationships xml file will only be parsed once when called+-- multiple times within a larger XlsxM action.+getWorkbookRelationships :: XlsxM Relationships+getWorkbookRelationships = runMemoized =<< asks _xs_relationships++type HexpatEvent = SAXEvent ByteString Text++relIdToEntrySelector :: RefId -> XlsxM (Maybe Zip.EntrySelector)+relIdToEntrySelector rid = do+ Relationships rels <- getWorkbookRelationships+ for (M.lookup rid rels) $ \rel -> do+ Zip.mkEntrySelector $ "xl/" <> relTarget rel++-- If the given sheet number exists, returns Just a conduit source of the stream+-- of XML events in a particular sheet. Returns Nothing when the sheet doesn't+-- exist.+{-# SCC getSheetXmlSource #-}+getSheetXmlSource ::+ (PrimMonad m, MonadThrow m, C.MonadResource m) =>+ SheetIdentifier ->+ XlsxM (Maybe (ConduitT () ByteString m ()))+getSheetXmlSource (SheetIdentifier refId _sheetId) = do+ -- TODO: The Zip library may throw exceptions that aren't exposed from this+ -- module, so downstream library users would need to add the 'zip' package to+ -- handle them. Consider re-wrapping zip library exceptions, or just+ -- re-export them?+ mSheetSel <- relIdToEntrySelector refId+ sheetExists <- maybe (pure False) (liftZip . Zip.doesEntryExist) mSheetSel+ case mSheetSel of+ Just sheetSel+ | sheetExists ->+ Just <$> liftZip (Zip.getEntrySource sheetSel)+ _ -> pure Nothing++{-# SCC runExpat #-}+runExpat :: forall state tag text.+ (GenericXMLString tag, GenericXMLString text) =>+ state ->+ ConduitT () ByteString (C.ResourceT IO) () ->+ ([SAXEvent tag text] -> StateT state IO ()) ->+ IO state+runExpat initialState byteSource handler = do+ -- Set up state+ ref <- newIORef initialState+ -- Set up parser and callbacks+ (parseChunk, _getLoc) <- Hexpat.hexpatNewParser Nothing Nothing False+ let noExtra _ offset = pure ((), offset)+ {-# SCC processChunk #-}+ {-# INLINE processChunk #-}+ processChunk isFinalChunk chunk = do+ (buf, len, mError) <- parseChunk chunk isFinalChunk+ saxen <- HexpatInternal.parseBuf buf len noExtra+ case mError of+ Just err -> error $ "expat error: " <> show err+ Nothing -> do+ state0 <- liftIO $ readIORef ref+ state1 <-+ {-# SCC "runExpat_runStateT_call" #-}+ execStateT (handler $ map fst saxen) state0+ writeIORef ref state1+ C.runConduitRes $+ byteSource .|+ C.awaitForever (liftIO . processChunk False)+ processChunk True BS.empty+ readIORef ref++runExpatForSheet ::+ ParserState ->+ ConduitT () ByteString (C.ResourceT IO) () ->+ (Row -> IO ()) ->+ XlsxM ()+runExpatForSheet initState byteSource inner =+ void $ liftIO $ runExpat initState byteSource handler+ where+ handler evs = forM_ evs $ \ev -> do+ parseRes <- runExceptT $ matchHexpatEvent ev+ case parseRes of+ Left err -> throwM err+ Right (Just cellRow)+ | not (IntMap.null cellRow) -> do+ rowNum <- use ps_cell_row_index+ liftIO $ inner $ MkRow rowNum cellRow+ _ -> pure ()++-- | this will collect the sheetitems in a list.+-- useful for cases were memory is of no concern but a sheetitem+-- type in a list is needed.+{-# DEPRECATED collectItems "prefer to use collectItemsIdentifier, see issue #193" #-}+collectItems ::+ SheetIndex ->+ XlsxM [SheetItem]+collectItems (MkSheetIndex sheetId) = makeIdentifierFromId sheetId >>= \case+ Nothing -> pure []+ Just identifier -> fmap (MkSheetItem sheetId) <$> collectItemsIdentifier identifier++-- | this will collect the rows in a list.+-- useful for cases were memory is of no concern but a row+-- type in a list is needed.+collectItemsIdentifier ::+ SheetIdentifier ->+ XlsxM [Row]+collectItemsIdentifier sheetIdentifier = do+ res <- liftIO $ newIORef []+ void $ readSheetIdentifier sheetIdentifier $ \item ->+ liftIO (modifyIORef' res (item :))+ fmap reverse $ liftIO $ readIORef res++-- | Look up the index of a case insensitive sheet name+{-# DEPRECATED makeIndexFromName "prefer to use makeIdentifierFromName" #-}+makeIndexFromName :: Text -> XlsxM (Maybe SheetIndex)+makeIndexFromName sheetName = do+ fmap (makeIndex . siSheetId) <$> makeIdentifierFromName sheetName++-- | Look up the identifier of a case insensitive sheet name+makeIdentifierFromName :: Text -> XlsxM (Maybe SheetIdentifier)+makeIdentifierFromName sheetName = do+ wi <- getWorkbookInfo+ -- The Excel UI does not allow a user to create two sheets whose+ -- names differ only in alphabetic case (at least for ascii...)+ let sheetNameCI = T.toLower sheetName+ findRes :: Maybe SheetInfo+ findRes = find ((== sheetNameCI) . T.toLower . sheetInfoName) $ _wiSheets wi+ pure $ getSheetIdentifier <$> findRes++-- | Look up the identifier of a sheet id. Not for exporting since it is somewhat+-- nonsensical if you do not have the sheet id.+makeIdentifierFromId :: Int -> XlsxM (Maybe SheetIdentifier)+makeIdentifierFromId sheetId = do+ WorkbookInfo sheets <- getWorkbookInfo+ pure $ getSheetIdentifier <$> find ((== sheetId) . sheetInfoSheetId) sheets++{-# DEPRECATED readSheet "prefer to use readSheetIdentifier" #-}+readSheet ::+ SheetIndex ->+ -- | Function to consume the sheet's rows+ (SheetItem -> IO ()) ->+ -- | Returns False if sheet doesn't exist, or True otherwise+ XlsxM Bool+readSheet (MkSheetIndex sheetId) inner = makeIdentifierFromId sheetId >>= \case+ Nothing -> pure False+ Just identifier -> readSheetIdentifier identifier (inner . MkSheetItem sheetId)++readSheetIdentifier ::+ SheetIdentifier ->+ -- | Function to consume the sheet's rows+ (Row -> IO ()) ->+ -- | Returns False if sheet doesn't exist, or True otherwise+ XlsxM Bool+readSheetIdentifier identifier inner = do+ mSrc :: Maybe (ConduitT () ByteString (C.ResourceT IO) ()) <- getSheetXmlSource identifier+ let+ case mSrc of+ Nothing -> pure False+ Just sourceSheetXml -> do+ sharedStrs <- getOrParseSharedStringss+ let sheetState0 = initialParserState+ & ps_shared_strings .~ sharedStrs+ runExpatForSheet sheetState0 sourceSheetXml inner+ pure True++-- | Returns number of rows in the given sheet (identified by the+-- sheet's ID, AKA the sheetId attribute, AKA 'sheetInfoSheetId'), or Nothing+-- if the sheet does not exist. Does not perform a full parse of the+-- XML into 'SheetItem's, so it should be more efficient than counting+-- via 'readSheetByIndex'.+{-# DEPRECATED countRowsInSheet "prefer to use countRowsInSheetIdentifier" #-}+countRowsInSheet :: SheetIndex -> XlsxM (Maybe Int)+countRowsInSheet (MkSheetIndex sheetId) =+ makeIdentifierFromId sheetId >>= \case+ Nothing -> pure Nothing+ Just identifier -> countRowsInSheetIdentifier identifier++-- | Returns number of rows in the given sheet or Nothing+-- if the sheet does not exist. Does not perform a full parse of the+-- XML into 'SheetItem's, so it should be more efficient than counting+-- via 'readSheetByIndex'.+countRowsInSheetIdentifier :: SheetIdentifier -> XlsxM (Maybe Int)+countRowsInSheetIdentifier identifier = do+ mSrc :: Maybe (ConduitT () ByteString (C.ResourceT IO) ()) <-+ getSheetXmlSource identifier+ for mSrc $ \sourceSheetXml -> do+ liftIO $ runExpat @Int @ByteString @ByteString 0 sourceSheetXml $ \evs ->+ forM_ evs $ \case+ StartElement "row" _ -> modify' (+1)+ _ -> pure ()++-- | Return row from the state and empty it+popRow :: HasParserState m => m CellRow+popRow = do+ row <- use ps_row+ ps_row .= mempty+ pure row++data AddCellErrors+ = ReadError -- ^ Could not read current cell value+ Text -- ^ Original value+ String -- ^ Error message+ | SharedStringsNotFound -- ^ Could not find string by index in shared string table+ Int -- ^ Given index+ (V.Vector Text) -- ^ Given shared strings to lookup in+ deriving Show++-- | Parse the given value+--+-- If it's a string, we try to get it our of a shared string table+{-# SCC parseValue #-}+parseValue :: SharedStringsMap -> Text -> ExcelValueType -> Either AddCellErrors CellValue+parseValue sstrings txt = \case+ TS -> do+ (idx, _) <- ReadError txt `first` Read.decimal @Int txt+ string <- maybe (Left $ SharedStringsNotFound idx sstrings) Right $ {-# SCC "sstrings_lookup_scc" #-} (sstrings ^? ix idx)+ Right $ CellText string+ TStr -> pure $ CellText txt+ TN -> bimap (ReadError txt) (CellDouble . fst) $ Read.double txt+ TE -> bimap (ReadError txt) (CellError . fst) $ fromAttrVal txt+ TB | txt == "1" -> Right $ CellBool True+ | txt == "0" -> Right $ CellBool False+ | otherwise -> Left $ ReadError txt "Could not read Excel boolean value (expected 0 or 1)"+ Untyped -> Right (parseUntypedValue txt)++-- TODO: some of the cells are untyped and we need to test whether+-- they all are strings or something more complicated+parseUntypedValue :: Text -> CellValue+parseUntypedValue = CellText++-- | Adds a cell to row in state monad+{-# SCC addCellToRow #-}+addCellToRow+ :: ( MonadError SheetErrors m+ , HasParserState m+ )+ => Text -> m ()+addCellToRow txt = do+ st <- get+ style <- use ps_cell_style+ when (_ps_is_in_val st) $ do+ val <- liftEither $ first ParseCellError $ parseValue (_ps_shared_strings st) txt (_ps_type st)+ put $ st { _ps_row = IntMap.insert (unColumnIndex $ _ps_cell_col_index st)+ (Cell { _cellStyle = style+ , _cellValue = Just val+ , _cellComment = Nothing+ , _cellFormula = Nothing+ }) $ _ps_row st}++data SheetErrors+ = ParseCoordinateError CoordinateErrors -- ^ Error while parsing coordinates+ | ParseTypeError TypeError -- ^ Error while parsing types+ | ParseCellError AddCellErrors -- ^ Error while parsing cells+ | ParseStyleErrors StyleError+ | HexpatParseError Hexpat.XMLParseError+ deriving stock Show+ deriving anyclass Exception++type SheetValue = (ByteString, Text)+type SheetValues = [SheetValue]++data CoordinateErrors+ = CoordinateNotFound SheetValues -- ^ If the coordinate was not specified in "r" attribute+ | NoListElement SheetValue SheetValues -- ^ If the value is empty for some reason+ | NoTextContent Content SheetValues -- ^ If the value has something besides @ContentText@ inside+ | DecodeFailure Text SheetValues -- ^ If malformed coordinate text was passed+ deriving stock Show+ deriving anyclass Exception++data TypeError+ = TypeNotFound SheetValues+ | TypeNoListElement SheetValue SheetValues+ | UnkownType Text SheetValues+ | TypeNoTextContent Content SheetValues+ deriving Show+ deriving anyclass Exception++data WorkbookError = LookupError { lookup_attrs :: [(ByteString, Text)], lookup_field :: ByteString }+ | ParseDecimalError Text String+ | InvalidSheetState Text+ deriving Show+ deriving anyclass Exception++{-# SCC matchHexpatEvent #-}+matchHexpatEvent ::+ ( MonadError SheetErrors m,+ HasParserState m+ ) =>+ HexpatEvent ->+ m (Maybe CellRow)+matchHexpatEvent ev = case ev of+ CharacterData txt -> {-# SCC "handle_CharData" #-} do+ inVal <- use ps_is_in_val+ when inVal $+ {-# SCC "append_text_buf" #-} (ps_text_buf <>= txt)+ pure Nothing+ StartElement "c" attrs -> Nothing <$ (setCoord attrs *> setType attrs *> setStyle attrs)+ StartElement "is" _ -> Nothing <$ (ps_is_in_val .= True)+ EndElement "is" -> Nothing <$ finaliseCellValue+ StartElement "v" _ -> Nothing <$ (ps_is_in_val .= True)+ EndElement "v" -> Nothing <$ finaliseCellValue+ -- If beginning of row, empty the state and return nothing.+ -- We don't know if there is anything in the state, the user may have+ -- decided to <row> <row> (not closing). In any case it's the beginning of a new row+ -- so we clear the state.+ StartElement "row" _ -> Nothing <$ popRow+ -- If at the end of the row, we have collected the whole row into+ -- the current state. Empty the state and return the row.+ EndElement "row" -> Just <$> popRow+ StartElement "worksheet" _ -> ps_worksheet_ended .= False >> pure Nothing+ EndElement "worksheet" -> ps_worksheet_ended .= True >> pure Nothing+ -- Skip everything else, e.g. the formula elements <f>+ FailDocument err -> do+ -- this event is emitted at the end the xml stream (possibly+ -- because the xml files in xlsx archives don't end in a+ -- newline, but that's a guess), so we use state to determine if+ -- it's expected.+ finished <- use ps_worksheet_ended+ unless finished $+ throwError $ HexpatParseError err+ pure Nothing+ _ -> pure Nothing++{-# INLINE finaliseCellValue #-}+finaliseCellValue ::+ ( MonadError SheetErrors m, HasParserState m ) => m ()+finaliseCellValue = do+ txt <- gets _ps_text_buf+ addCellToRow txt+ modify' $ \st ->+ st { _ps_is_in_val = False+ , _ps_text_buf = mempty+ }++-- | Update state coordinates accordingly to @parseCoordinates@+{-# SCC setCoord #-}+setCoord+ :: ( MonadError SheetErrors m+ , HasParserState m+ )+ => SheetValues -> m ()+setCoord list = do+ coordinates <- liftEither $ first ParseCoordinateError $ parseCoordinates list+ ps_cell_col_index .= (coordinates ^. _2)+ ps_cell_row_index .= (coordinates ^. _1)++-- | Parse type from values and update state accordingly+setType+ :: ( MonadError SheetErrors m+ , HasParserState m+ )+ => SheetValues -> m ()+setType list = do+ type' <- liftEither $ first ParseTypeError $ parseType list+ ps_type .= type'++-- | Find sheet value by its name+findName :: ByteString -> SheetValues -> Maybe SheetValue+findName name = find ((name ==) . fst)+{-# INLINE findName #-}++setStyle :: (MonadError SheetErrors m, HasParserState m) => SheetValues -> m ()+setStyle list = do+ style <- liftEither $ first ParseStyleErrors $ parseStyle list+ ps_cell_style .= style++data StyleError = InvalidStyleRef { seInput:: Text, seErrorMsg :: String}+ deriving Show++parseStyle :: SheetValues -> Either StyleError (Maybe Int)+parseStyle list =+ case findName "s" list of+ Nothing -> pure Nothing+ Just (_nm, valTex) -> case Read.decimal valTex of+ Left err -> Left (InvalidStyleRef valTex err)+ Right (i, _rem) -> pure $ Just i++-- | Parse value type+{-# SCC parseType #-}+parseType :: SheetValues -> Either TypeError ExcelValueType+parseType list =+ case findName "t" list of+ -- NB: According to format specification default value for cells without+ -- `t` attribute is a `n` - number.+ --+ -- <xsd:complexType name="CT_Cell" from spec (see the `CellValue` spec reference)>+ -- ..+ -- <xsd:attribute name="t" type="ST_CellType" use="optional" default="n"/>+ -- </xsd:complexType>+ Nothing -> Right TN+ Just (_nm, valText)->+ case valText of+ "n" -> Right TN+ "s" -> Right TS+ -- "Cell containing a formula string". Probably shouldn't be TStr..+ "str" -> Right TStr+ "inlineStr" -> Right TStr+ "b" -> Right TB+ "e" -> Right TE+ other -> Left $ UnkownType other list++-- | Parse coordinates from a list of xml elements if such were found on "r" key+{-# SCC parseCoordinates #-}+parseCoordinates :: SheetValues -> Either CoordinateErrors (RowIndex, ColumnIndex)+parseCoordinates list = do+ (_nm, valText) <- maybe (Left $ CoordinateNotFound list) Right $ findName "r" list+ maybe (Left $ DecodeFailure valText list) Right $ fromSingleCellRef $ CellRef valText
+ src/Codec/Xlsx/Parser/Stream/HexpatInternal.hs view
@@ -0,0 +1,95 @@+{-+Under BSD 3-Clause license, (c) 2009 Doug Beardsley <mightybyte@gmail.com>, (c) 2009-2012 Stephen Blackheath <http://blacksapphire.com/antispam/>, (c) 2009 Gregory Collins, (c) 2008 Evan Martin <martine@danga.com>, (c) 2009 Matthew Pocock <matthew.pocock@ncl.ac.uk>, (c) 2007-2009 Galois Inc., (c) 2010 Kevin Jardine, (c) 2012 Simon Hengel++From https://hackage.haskell.org/package/hexpat-0.20.13+ https://github.com/the-real-blackh/hexpat/blob/master/Text/XML/Expat/SAX.hs#L227++copied over because the upstream library doesn't expose this function.+-}+module Codec.Xlsx.Parser.Stream.HexpatInternal (parseBuf) where++import Control.Monad+import Text.XML.Expat.SAX+import qualified Data.ByteString.Internal as I+import Data.Bits+import Data.Int+import Data.ByteString.Internal (c_strlen)+import Data.Word+import Foreign.C+import Foreign.ForeignPtr+import Foreign.Ptr+import Foreign.Storable++{-# SCC parseBuf #-}+parseBuf :: (GenericXMLString tag, GenericXMLString text) =>+ ForeignPtr Word8 -> CInt -> (Ptr Word8 -> Int -> IO (a, Int)) -> IO [(SAXEvent tag text, a)]+parseBuf buf _ processExtra = withForeignPtr buf $ \pBuf -> doit [] pBuf 0+ where+ roundUp32 offset = (offset + 3) .&. complement 3+ doit acc pBuf offset0 = offset0 `seq` do+ typ <- peek (pBuf `plusPtr` offset0 :: Ptr Word32)+ (a, offset) <- processExtra pBuf (offset0 + 4)+ case typ of+ 0 -> return (reverse acc)+ 1 -> do+ nAtts <- peek (pBuf `plusPtr` offset :: Ptr Word32)+ let pName = pBuf `plusPtr` (offset + 4)+ lName <- fromIntegral <$> c_strlen pName+ let name = gxFromByteString $ I.fromForeignPtr buf (offset + 4) lName+ (atts, offset') <- foldM (\(atts, offset) _ -> do+ let pAtt = pBuf `plusPtr` offset+ lAtt <- fromIntegral <$> c_strlen pAtt+ let att = gxFromByteString $ I.fromForeignPtr buf offset lAtt+ offset' = offset + lAtt + 1+ pValue = pBuf `plusPtr` offset'+ lValue <- fromIntegral <$> c_strlen pValue+ let value = gxFromByteString $ I.fromForeignPtr buf offset' lValue+ return ((att, value):atts, offset' + lValue + 1)+ ) ([], offset + 4 + lName + 1) [1,3..nAtts]+ doit ((StartElement name (reverse atts), a) : acc) pBuf (roundUp32 offset')+ 2 -> do+ let pName = pBuf `plusPtr` offset+ lName <- fromIntegral <$> c_strlen pName+ let name = gxFromByteString $ I.fromForeignPtr buf offset lName+ offset' = offset + lName + 1+ doit ((EndElement name, a) : acc) pBuf (roundUp32 offset')+ 3 -> do+ len <- fromIntegral <$> peek (pBuf `plusPtr` offset :: Ptr Word32)+ let text = gxFromByteString $ I.fromForeignPtr buf (offset + 4) len+ offset' = offset + 4 + len+ doit ((CharacterData text, a) : acc) pBuf (roundUp32 offset')+ 4 -> do+ let pEnc = pBuf `plusPtr` offset+ lEnc <- fromIntegral <$> c_strlen pEnc+ let enc = gxFromByteString $ I.fromForeignPtr buf offset lEnc+ offset' = offset + lEnc + 1+ pVer = pBuf `plusPtr` offset'+ pVerFirst <- peek (castPtr pVer :: Ptr Word8)+ (mVer, offset'') <- case pVerFirst of+ 0 -> return (Nothing, offset' + 1)+ 1 -> do+ lVer <- fromIntegral <$> c_strlen (pVer `plusPtr` 1)+ return (Just $ gxFromByteString $ I.fromForeignPtr buf (offset' + 1) lVer, offset' + 1 + lVer + 1)+ _ -> error "hexpat: bad data from C land"+ cSta <- peek (pBuf `plusPtr` offset'' :: Ptr Int8)+ let sta = if cSta < 0 then Nothing else+ if cSta == 0 then Just False else+ Just True+ doit ((XMLDeclaration enc mVer sta, a) : acc) pBuf (roundUp32 (offset'' + 1))+ 5 -> doit ((StartCData, a) : acc) pBuf offset+ 6 -> doit ((EndCData, a) : acc) pBuf offset+ 7 -> do+ let pTarget = pBuf `plusPtr` offset+ lTarget <- fromIntegral <$> c_strlen pTarget+ let target = gxFromByteString $ I.fromForeignPtr buf offset lTarget+ offset' = offset + lTarget + 1+ pData = pBuf `plusPtr` offset'+ lData <- fromIntegral <$> c_strlen pData+ let dat = gxFromByteString $ I.fromForeignPtr buf offset' lData+ doit ((ProcessingInstruction target dat, a) : acc) pBuf (roundUp32 (offset' + lData + 1))+ 8 -> do+ let pText = pBuf `plusPtr` offset+ lText <- fromIntegral <$> c_strlen pText+ let text = gxFromByteString $ I.fromForeignPtr buf offset lText+ doit ((Comment text, a) : acc) pBuf (roundUp32 (offset + lText + 1))+ _ -> error "hexpat: bad data from C land"
+ src/Codec/Xlsx/Parser/Stream/Row.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}++module Codec.Xlsx.Parser.Stream.Row where++import Codec.Xlsx.LensCompat+import Codec.Xlsx.Types.Cell (Cell)+import Codec.Xlsx.Types.Common ( RowIndex)+import Data.IntMap.Strict (IntMap)+import GHC.Generics (Generic)+import Control.DeepSeq (NFData)++type CellRow = IntMap Cell++data Row = MkRow+ { _ri_row_index :: !RowIndex -- ^ Row number+ , _ri_cell_row :: CellRow -- ^ Row itself+ } deriving stock (Generic, Show)+ deriving anyclass NFData++makeLenses 'MkRow
+ src/Codec/Xlsx/Parser/Stream/SheetIdentifier.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE PatternSynonyms #-}++module Codec.Xlsx.Parser.Stream.SheetIdentifier+ ( SheetIdentifier (SheetIdentifier, siRefId, siSheetId)+ , unsafeGetSheetIdentifier+ ) where++import Codec.Xlsx.Types.RefId ( RefId )++-- | Opaque identifier for a sheet.+data SheetIdentifier = MkSheetIdentifier RefId Int+ deriving (Eq, Ord)++pattern SheetIdentifier :: RefId -> Int -> SheetIdentifier+pattern SheetIdentifier { siRefId, siSheetId } <- MkSheetIdentifier siRefId siSheetId++{-# COMPLETE SheetIdentifier #-}++unsafeGetSheetIdentifier :: RefId -> Int -> SheetIdentifier+unsafeGetSheetIdentifier = MkSheetIdentifier
+ src/Codec/Xlsx/Parser/Stream/SheetIndex.hs view
@@ -0,0 +1,20 @@++{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralisedNewtypeDeriving #-}++module Codec.Xlsx.Parser.Stream.SheetIndex (SheetIndex (..), makeIndex) where++import Control.DeepSeq++-- | datatype representing a sheet index, looking it up by name+-- can be done with 'makeIndexFromName', which is the preferred approach.+-- although 'makeIndex' is available in case it's already known.+newtype SheetIndex = MkSheetIndex Int+ deriving newtype (NFData, Show)++-- | This does *no* checking if the index exists or not.+-- you could have index out of bounds issues because of this.+makeIndex :: Int -> SheetIndex+makeIndex = MkSheetIndex++{-# DEPRECATED SheetIndex, MkSheetIndex, makeIndex "SheetIndex will not be supported in future, see issue #193. Use SheetIdentifier instead" #-}
+ src/Codec/Xlsx/Parser/Stream/SheetInfo.hs view
@@ -0,0 +1,27 @@++module Codec.Xlsx.Parser.Stream.SheetInfo where++import Data.Text (Text)+import Codec.Xlsx.Types.RefId (RefId)+import Codec.Xlsx.Parser.Stream.SheetIdentifier+ (SheetIdentifier, unsafeGetSheetIdentifier)+import Codec.Xlsx.Types (SheetState)++-- | Represents sheets from the workbook.xml file. E.g.+-- <sheet name="Data" sheetId="1" state="hidden" r:id="rId2" /+data SheetInfo = MkSheetInfo+ { sheetInfoName :: Text,+ -- | The r:id attribute value+ sheetInfoRelId :: RefId,+ -- | The sheetId attribute value+ sheetInfoSheetId :: Int,+ -- | The index into the sheets in the workbook+ sheetInfoSheetIndex :: Int,+ -- | The sheet visibility state+ sheetInfoState :: SheetState+ } deriving (Show, Eq)++{-# DEPRECATED sheetInfoRelId, sheetInfoSheetId "this field will be removed in future, see issue #193" #-} ++getSheetIdentifier :: SheetInfo -> SheetIdentifier+getSheetIdentifier sheetInfo = unsafeGetSheetIdentifier (sheetInfoRelId sheetInfo) (sheetInfoSheetId sheetInfo)
+ src/Codec/Xlsx/Parser/Stream/SheetItem.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}++module Codec.Xlsx.Parser.Stream.SheetItem where++import Codec.Xlsx.Parser.Stream.Row (Row)++import Codec.Xlsx.LensCompat+import Control.DeepSeq (NFData)+import GHC.Generics (Generic)+++-- | Sheet item+--+-- The current sheet at a time, every sheet is constructed of these items.+data SheetItem = MkSheetItem+ { _si_sheet_index :: !Int -- ^ The sheet number+ , _si_row :: Row+ } deriving stock (Generic, Show)+ deriving anyclass NFData++{-# WARNING SheetItem, MkSheetItem "SheetItem will not be supported in future, see issue #193. Use Row directly instead" #-}+makeLenses 'MkSheetItem+{-# DEPRECATED si_sheet_index, si_row "SheetItem will not be supported in future, see issue #193" #-}
src/Codec/Xlsx/Types.hs view
@@ -1,124 +1,368 @@ {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-}-module Codec.Xlsx.Types- ( Xlsx(..), xlSheets, xlStyles- , def+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE RankNTypes #-}++module Codec.Xlsx.Types (+ -- * The main types+ Xlsx(..) , Styles(..)- , emptyStyles- , ColumnsWidth(..)- , Worksheet(..), wsColumns, wsRowPropertiesMap, wsCells, wsMerges+ , DefinedNames(..)+ , ColumnsProperties(..)+ , PageSetup(..)+ , Worksheet(..)+ , SheetState(..) , CellMap , CellValue(..)- , Cell(..), cellValue, cellStyle+ , CellFormula(..)+ , FormulaExpression(..)+ , Cell.SharedFormulaIndex(..)+ , Cell.SharedFormulaOptions(..)+ , Cell(..)+ , RowHeight(..) , RowProperties (..)- , int2col- , col2int+ -- * Lenses+ -- ** Workbook+ , xlSheets+ , xlStyles+ , xlDefinedNames+ , xlCustomProperties+ , xlDateBase+ -- ** Worksheet+ , wsColumnsProperties+ , wsRowPropertiesMap+ , wsCells+ , wsDrawing+ , wsMerges+ , wsSheetViews+ , wsPageSetup+ , wsConditionalFormattings+ , wsDataValidations+ , wsPivotTables+ , wsAutoFilter+ , wsTables+ , wsProtection+ , wsSharedFormulas+ , wsState+ -- ** Cells+ , Cell.cellValue+ , Cell.cellStyle+ , Cell.cellComment+ , Cell.cellFormula+ -- ** Row properties+ , rowHeightLens+ , _CustomHeight+ , _AutomaticHeight+ -- * Style helpers+ , emptyStyles+ , renderStyleSheet+ , parseStyleSheet+ -- * Misc+ , simpleCellFormula+ , sharedFormulaByIndex+ , def , toRows , fromRows+ , module X ) where -import Control.Lens.TH+import Control.Exception (SomeException, toException)+import Codec.Xlsx.LensCompat (lens, Lens', makeLenses, Prism', prism)+import Control.DeepSeq (NFData) import qualified Data.ByteString.Lazy as L-import Data.Char-import Data.Default-import Data.Function (on)-import Data.List (groupBy)-import Data.Map (Map)+import Data.Default+import Data.Function (on)+import Data.List (groupBy)+import Data.Map (Map) import qualified Data.Map as M-import Data.Text (Text)-import qualified Data.Text as T+import Data.Maybe (catMaybes, isJust)+import Data.Text (Text)+import GHC.Generics (Generic)+import Text.XML (parseLBS, renderLBS)+import Text.XML.Cursor +import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.AutoFilter as X+import Codec.Xlsx.Types.Cell as Cell+import Codec.Xlsx.Types.Comment as X+import Codec.Xlsx.Types.Common as X+import Codec.Xlsx.Types.ConditionalFormatting as X+import Codec.Xlsx.Types.DataValidation as X+import Codec.Xlsx.Types.Drawing as X+import Codec.Xlsx.Types.Drawing.Chart as X+import Codec.Xlsx.Types.Drawing.Common as X+import Codec.Xlsx.Types.PageSetup as X+import Codec.Xlsx.Types.PivotTable as X+import Codec.Xlsx.Types.Protection as X+import Codec.Xlsx.Types.RichText as X+import Codec.Xlsx.Types.SheetViews as X+import Codec.Xlsx.Types.StyleSheet as X+import Codec.Xlsx.Types.Table as X+import Codec.Xlsx.Types.Variant as X+import Codec.Xlsx.Writer.Internal --- | Cell values include text, numbers and booleans,--- standard includes date format also but actually dates--- are represented by numbers with a date format assigned--- to a cell containing it-data CellValue = CellText Text- | CellDouble Double- | CellBool Bool- deriving (Eq, Show)+-- | Height of a row in points (1/72in)+data RowHeight+ = CustomHeight !Double+ -- ^ Row height is set by the user+ | AutomaticHeight !Double+ -- ^ Row height is set automatically by the program+ deriving (Eq, Ord, Show, Read, Generic)+instance NFData RowHeight --- | Currently cell details include only cell values and style ids--- (e.g. formulas from @\<f\>@ and inline strings from @\<is\>@--- subelements are ignored)-data Cell = Cell- { _cellStyle :: Maybe Int- , _cellValue :: Maybe CellValue- } deriving (Eq, Show)+_CustomHeight :: Prism' RowHeight Double+_CustomHeight+ = (prism (\ x1_a4xgd -> CustomHeight x1_a4xgd))+ (\ x_a4xge+ -> case x_a4xge of+ CustomHeight y1_a4xgf -> Right y1_a4xgf+ _ -> Left x_a4xge)+{-# INLINE _CustomHeight #-} -makeLenses ''Cell+_AutomaticHeight :: Prism' RowHeight Double+_AutomaticHeight+ = (prism (\ x1_a4xgg -> AutomaticHeight x1_a4xgg))+ (\ x_a4xgh+ -> case x_a4xgh of+ AutomaticHeight y1_a4xgi -> Right y1_a4xgi+ _ -> Left x_a4xgh)+{-# INLINE _AutomaticHeight #-} -instance Default Cell where- def = Cell Nothing Nothing+-- | Properties of a row. See §18.3.1.73 "row (Row)" for more details+data RowProperties = RowProps+ { rowHeight :: Maybe RowHeight+ -- ^ Row height in points+ , rowStyle :: Maybe Int+ -- ^ Style to be applied to row+ , rowHidden :: Bool+ -- ^ Whether row is visible or not+ } deriving (Eq, Ord, Show, Read, Generic)+instance NFData RowProperties --- | Map containing cell values which are indexed by row and column--- if you need to use more traditional (x,y) indexing please you could--- use corresponding accessors from ''Codec.Xlsx.Lens''-type CellMap = Map (Int, Int) Cell+rowHeightLens :: Lens' RowProperties (Maybe RowHeight)+rowHeightLens = lens rowHeight $ \x y -> x{rowHeight=y} -data RowProperties = RowProps { rowHeight :: Maybe Double, rowStyle::Maybe Int}- deriving (Read,Eq,Show,Ord)+instance Default RowProperties where+ def = RowProps { rowHeight = Nothing+ , rowStyle = Nothing+ , rowHidden = False+ } --- | Column range (from cwMin to cwMax) width-data ColumnsWidth = ColumnsWidth- { cwMin :: Int- , cwMax :: Int- , cwWidth :: Double- , cwStyle :: Int- } deriving (Eq, Show)+-- | Column range (from cwMin to cwMax) properties+data ColumnsProperties = ColumnsProperties+ { cpMin :: Int+ -- ^ First column affected by this 'ColumnWidth' record.+ , cpMax :: Int+ -- ^ Last column affected by this 'ColumnWidth' record.+ , cpWidth :: Maybe Double+ -- ^ Column width measured as the number of characters of the+ -- maximum digit width of the numbers 0, 1, 2, ..., 9 as rendered in+ -- the normal style's font.+ --+ -- See longer description in Section 18.3.1.13 "col (Column Width &+ -- Formatting)" (p. 1605)+ , cpStyle :: Maybe Int+ -- ^ Default style for the affected column(s). Affects cells not yet+ -- allocated in the column(s). In other words, this style applies+ -- to new columns.+ , cpHidden :: Bool+ -- ^ Flag indicating if the affected column(s) are hidden on this+ -- worksheet.+ , cpCollapsed :: Bool+ -- ^ Flag indicating if the outlining of the affected column(s) is+ -- in the collapsed state.+ , cpBestFit :: Bool+ -- ^ Flag indicating if the specified column(s) is set to 'best+ -- fit'.+ } deriving (Eq, Show, Generic)+instance NFData ColumnsProperties +instance FromCursor ColumnsProperties where+ fromCursor c = do+ cpMin <- fromAttribute "min" c+ cpMax <- fromAttribute "max" c+ cpWidth <- maybeAttribute "width" c+ cpStyle <- maybeAttribute "style" c+ cpHidden <- fromAttributeDef "hidden" False c+ cpCollapsed <- fromAttributeDef "collapsed" False c+ cpBestFit <- fromAttributeDef "bestFit" False c+ return ColumnsProperties {..}++instance FromXenoNode ColumnsProperties where+ fromXenoNode root = parseAttributes root $ do+ cpMin <- fromAttr "min"+ cpMax <- fromAttr "max"+ cpWidth <- maybeAttr "width"+ cpStyle <- maybeAttr "style"+ cpHidden <- fromAttrDef "hidden" False+ cpCollapsed <- fromAttrDef "collapsed" False+ cpBestFit <- fromAttrDef "bestFit" False+ return ColumnsProperties {..}++-- | Sheet visibility state+-- cf. Ecma Office Open XML Part 1:+-- 18.18.68 ST_SheetState (Sheet Visibility Types)+-- * "visible"+-- Indicates the sheet is visible (default)+-- * "hidden"+-- Indicates the workbook window is hidden, but can be shown by the user via the user interface.+-- * "veryHidden"+-- Indicates the sheet is hidden and cannot be shown in the user interface (UI). This state is only available programmatically.+data SheetState =+ Visible -- ^ state="visible"+ | Hidden -- ^ state="hidden"+ | VeryHidden -- ^ state="veryHidden"+ deriving (Eq, Show, Generic)++instance NFData SheetState++instance Default SheetState where+ def = Visible++instance FromAttrVal SheetState where+ fromAttrVal "visible" = readSuccess Visible+ fromAttrVal "hidden" = readSuccess Hidden+ fromAttrVal "veryHidden" = readSuccess VeryHidden+ fromAttrVal t = invalidText "SheetState" t++instance FromAttrBs SheetState where+ fromAttrBs "visible" = return Visible+ fromAttrBs "hidden" = return Hidden+ fromAttrBs "veryHidden" = return VeryHidden+ fromAttrBs t = unexpectedAttrBs "SheetState" t++instance ToAttrVal SheetState where+ toAttrVal Visible = "visible"+ toAttrVal Hidden = "hidden"+ toAttrVal VeryHidden = "veryHidden"+ -- | Xlsx worksheet data Worksheet = Worksheet- { _wsColumns :: [ColumnsWidth] -- ^ column widths- , _wsRowPropertiesMap :: Map Int RowProperties -- ^ custom row properties (height, style) map- , _wsCells :: CellMap -- ^ data mapped by (row, column) pairs- , _wsMerges :: [Text] -- ^ list of cell merges (entries of the form @D13:H14@)- } deriving (Eq, Show)+ { _wsColumnsProperties :: [ColumnsProperties] -- ^ column widths+ , _wsRowPropertiesMap :: Map RowIndex RowProperties+ -- ^ custom row properties (height, style) map+ , _wsCells :: CellMap -- ^ data mapped by (row, column) pairs+ , _wsDrawing :: Maybe Drawing -- ^ SpreadsheetML Drawing+ , _wsMerges :: [Range] -- ^ list of cell merges+ , _wsSheetViews :: Maybe [SheetView]+ , _wsPageSetup :: Maybe PageSetup+ , _wsConditionalFormattings :: Map SqRef ConditionalFormatting+ , _wsDataValidations :: Map SqRef DataValidation+ , _wsPivotTables :: [PivotTable]+ , _wsAutoFilter :: Maybe AutoFilter+ , _wsTables :: [Table]+ , _wsProtection :: Maybe SheetProtection+ , _wsSharedFormulas :: Map SharedFormulaIndex SharedFormulaOptions+ , _wsState :: SheetState+ } deriving (Eq, Show, Generic)+instance NFData Worksheet makeLenses ''Worksheet instance Default Worksheet where- def = Worksheet [] M.empty M.empty []+ def =+ Worksheet+ { _wsColumnsProperties = []+ , _wsRowPropertiesMap = M.empty+ , _wsCells = M.empty+ , _wsDrawing = Nothing+ , _wsMerges = []+ , _wsSheetViews = Nothing+ , _wsPageSetup = Nothing+ , _wsConditionalFormattings = M.empty+ , _wsDataValidations = M.empty+ , _wsPivotTables = []+ , _wsAutoFilter = Nothing+ , _wsTables = []+ , _wsProtection = Nothing+ , _wsSharedFormulas = M.empty+ , _wsState = def+ } +-- | Raw worksheet styles, for structured implementation see 'StyleSheet'+-- and functions in "Codec.Xlsx.Types.StyleSheet" newtype Styles = Styles {unStyles :: L.ByteString}- deriving (Eq, Show)+ deriving (Eq, Show, Generic)+instance NFData Styles -- | Structured representation of Xlsx file (currently a subset of its contents) data Xlsx = Xlsx- { _xlSheets :: Map Text Worksheet- , _xlStyles :: Styles- } deriving (Eq, Show)+ { _xlSheets :: [(Text, Worksheet)]+ , _xlStyles :: Styles+ , _xlDefinedNames :: DefinedNames+ , _xlCustomProperties :: Map Text Variant+ , _xlDateBase :: DateBase+ -- ^ date base to use when converting serial value (i.e. 'CellDouble d')+ -- into date-time. Default value is 'DateBase1900'+ --+ -- See also 18.17.4.1 "Date Conversion for Serial Date-Times" (p. 2067)+ } deriving (Eq, Show, Generic)+instance NFData Xlsx ++-- | Defined names+--+-- Each defined name consists of a name, an optional local sheet ID, and a value.+--+-- This element defines the collection of defined names for this workbook.+-- Defined names are descriptive names to represent cells, ranges of cells,+-- formulas, or constant values. Defined names can be used to represent a range+-- on any worksheet.+--+-- Excel also defines a number of reserved names with a special interpretation:+--+-- * @_xlnm.Print_Area@ specifies the workbook's print area.+-- Example value: @SheetName!$A:$A,SheetName!$1:$4@+-- * @_xlnm.Print_Titles@ specifies the row(s) or column(s) to repeat+-- at the top of each printed page.+-- * @_xlnm.Sheet_Title@:refers to a sheet title.+--+-- and others. See Section 18.2.6, "definedNames (Defined Names)" (p. 1728) of+-- the spec (second edition).+--+-- NOTE: Right now this is only a minimal implementation of defined names.+newtype DefinedNames = DefinedNames [(Text, Maybe Text, Text)]+ deriving (Eq, Show, Generic)+instance NFData DefinedNames+ makeLenses ''Xlsx instance Default Xlsx where- def = Xlsx M.empty emptyStyles+ def = Xlsx [] emptyStyles def M.empty DateBase1900 +instance Default DefinedNames where+ def = DefinedNames []+ emptyStyles :: Styles-emptyStyles = Styles "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\-\<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"></styleSheet>"+emptyStyles = Styles "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"></styleSheet>" --- | convert column number (starting from 1) to its textual form (e.g. 3 -> \"C\")-int2col :: Int -> Text-int2col = T.pack . reverse . map int2let . base26- where- int2let 0 = 'Z'- int2let x = chr $ (x - 1) + ord 'A'- base26 0 = []- base26 i = let i' = (i `mod` 26)- i'' = if i' == 0 then 26 else i'- in seq i' (i' : base26 ((i - i'') `div` 26))+-- | Render 'StyleSheet'+--+-- This is used to render a structured 'StyleSheet' into a raw XML 'Styles'+-- document. Actually /replacing/ 'Styles' with 'StyleSheet' would mean we+-- would need to write a /parser/ for 'StyleSheet' as well (and would moreover+-- require that we support the full style sheet specification, which is still+-- quite a bit of work).+renderStyleSheet :: StyleSheet -> Styles+renderStyleSheet = Styles . renderLBS def . toDocument --- | reverse to 'int2col'-col2int :: Text -> Int-col2int = T.foldl' (\i c -> i * 26 + let2int c) 0- where- let2int c = 1 + ord c - ord 'A'+-- | Parse 'StyleSheet'+--+-- This is used to parse raw 'Styles' into structured 'StyleSheet'+-- currently not all of the style sheet specification is supported+-- so parser (and the data model) is to be completed+parseStyleSheet :: Styles -> Either SomeException StyleSheet+parseStyleSheet (Styles bs) = parseLBS def bs >>= parseDoc+ where+ parseDoc doc = case fromCursor (fromDocument doc) of+ [stylesheet] -> Right stylesheet+ _ -> Left . toException $ ParseException "Could not parse style sheets" -- | converts cells mapped by (row, column) into rows which contain -- row index and cells as pairs of column indices and cell values-toRows :: CellMap -> [(Int, [(Int, Cell)])]+toRows :: CellMap -> [(RowIndex, [(ColumnIndex, Cell)])] toRows cells = map extractRow $ groupBy ((==) `on` (fst . fst)) $ M.toList cells where@@ -127,7 +371,22 @@ extractRow _ = error "invalid CellMap row" -- | reverse to 'toRows'-fromRows :: [(Int, [(Int, Cell)])] -> CellMap+fromRows :: [(RowIndex, [(ColumnIndex, Cell)])] -> CellMap fromRows rows = M.fromList $ concatMap mapRow rows where mapRow (r, cells) = map (\(c, v) -> ((r, c), v)) cells+++instance ToElement ColumnsProperties where+ toElement nm ColumnsProperties {..} = leafElement nm attrs+ where+ attrs =+ ["min" .= cpMin, "max" .= cpMax] +++ catMaybes+ [ "style" .=? (justNonDef 0 =<< cpStyle)+ , "width" .=? cpWidth+ , "customWidth" .=? justTrue (isJust cpWidth)+ , "hidden" .=? justTrue cpHidden+ , "collapsed" .=? justTrue cpCollapsed+ , "bestFit" .=? justTrue cpBestFit+ ]
+ src/Codec/Xlsx/Types/AutoFilter.hs view
@@ -0,0 +1,742 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.AutoFilter where++import Control.Arrow (first)+import Control.DeepSeq (NFData)+import Codec.Xlsx.LensCompat (makeLenses)+import Data.Bool (bool)+import Data.ByteString (ByteString)+import Data.Default+import Data.Foldable (asum)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe (catMaybes)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor hiding (bool)+import qualified Xeno.DOM as Xeno++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.ConditionalFormatting (IconSetType)+import Codec.Xlsx.Writer.Internal++-- | The filterColumn collection identifies a particular column in the+-- AutoFilter range and specifies filter information that has been+-- applied to this column. If a column in the AutoFilter range has no+-- criteria specified, then there is no corresponding filterColumn+-- collection expressed for that column.+--+-- See 18.3.2.7 "filterColumn (AutoFilter Column)" (p. 1717)+data FilterColumn+ = Filters FilterByBlank [FilterCriterion]+ | ColorFilter ColorFilterOptions+ | ACustomFilter CustomFilter+ | CustomFiltersOr CustomFilter CustomFilter+ | CustomFiltersAnd CustomFilter CustomFilter+ | DynamicFilter DynFilterOptions+ | IconFilter (Maybe Int) IconSetType+ -- ^ Specifies the icon set and particular icon within that set to+ -- filter by. Icon is specified using zero-based index of an icon in+ -- an icon set. 'Nothing' means "no icon"+ | BottomNFilter EdgeFilterOptions+ -- ^ Specifies the bottom N (percent or number of items) to filter by+ | TopNFilter EdgeFilterOptions+ -- ^ Specifies the top N (percent or number of items) to filter by+ deriving (Eq, Show, Generic)+instance NFData FilterColumn++data FilterByBlank+ = FilterByBlank+ | DontFilterByBlank+ deriving (Eq, Show, Generic)+instance NFData FilterByBlank++data FilterCriterion+ = FilterValue Text+ | FilterDateGroup DateGroup+ deriving (Eq, Show, Generic)+instance NFData FilterCriterion++-- | Used to express a group of dates or times which are used in an+-- AutoFilter criteria+--+-- Section 18.3.2.4 "dateGroupItem (Date Grouping)" (p. 1714)+data DateGroup+ = DateGroupByYear Int+ | DateGroupByMonth Int Int+ | DateGroupByDay Int Int Int+ | DateGroupByHour Int Int Int Int+ | DateGroupByMinute Int Int Int Int Int+ | DateGroupBySecond Int Int Int Int Int Int+ deriving (Eq, Show, Generic)+instance NFData DateGroup++data CustomFilter = CustomFilter+ { cfltOperator :: CustomFilterOperator+ , cfltValue :: Text+ } deriving (Eq, Show, Generic)+instance NFData CustomFilter++data CustomFilterOperator+ = FltrEqual+ -- ^ Show results which are equal to criteria.+ | FltrGreaterThan+ -- ^ Show results which are greater than criteria.+ | FltrGreaterThanOrEqual+ -- ^ Show results which are greater than or equal to criteria.+ | FltrLessThan+ -- ^ Show results which are less than criteria.+ | FltrLessThanOrEqual+ -- ^ Show results which are less than or equal to criteria.+ | FltrNotEqual+ -- ^ Show results which are not equal to criteria.+ deriving (Eq, Show, Generic)+instance NFData CustomFilterOperator++data EdgeFilterOptions = EdgeFilterOptions+ { _efoUsePercents :: Bool+ -- ^ Flag indicating whether or not to filter by percent value of+ -- the column. A false value filters by number of items.+ , _efoVal :: Double+ -- ^ Top or bottom value to use as the filter criteria.+ -- Example: "Filter by Top 10 Percent" or "Filter by Top 5 Items"+ , _efoFilterVal :: Maybe Double+ -- ^ The actual cell value in the range which is used to perform the+ -- comparison for this filter.+ } deriving (Eq, Show, Generic)+instance NFData EdgeFilterOptions++-- | Specifies the color to filter by and whether to use the cell's+-- fill or font color in the filter criteria. If the cell's font or+-- fill color does not match the color specified in the criteria, the+-- rows corresponding to those cells are hidden from view.+--+-- See 18.3.2.1 "colorFilter (Color Filter Criteria)" (p. 1712)+data ColorFilterOptions = ColorFilterOptions+ { _cfoCellColor :: Bool+ -- ^ Flag indicating whether or not to filter by the cell's fill+ -- color. 'True' indicates to filter by cell fill. 'False' indicates+ -- to filter by the cell's font color.+ --+ -- For rich text in cells, if the color specified appears in the+ -- cell at all, it shall be included in the filter.+ , _cfoDxfId :: Maybe Int+ -- ^ Id of differential format record (dxf) in the Styles Part (see+ -- '_styleSheetDxfs') which expresses the color value to filter by.+ } deriving (Eq, Show, Generic)+instance NFData ColorFilterOptions++-- | Specifies dynamic filter criteria. These criteria are considered+-- dynamic because they can change, either with the data itself (e.g.,+-- "above average") or with the current system date (e.g., show values+-- for "today"). For any cells whose values do not meet the specified+-- criteria, the corresponding rows shall be hidden from view when the+-- filter is applied.+--+-- '_dfoMaxVal' shall be required for 'DynFilterTday',+-- 'DynFilterYesterday', 'DynFilterTomorrow', 'DynFilterNextWeek',+-- 'DynFilterThisWeek', 'DynFilterLastWeek', 'DynFilterNextMonth',+-- 'DynFilterThisMonth', 'DynFilterLastMonth', 'DynFilterNextQuarter',+-- 'DynFilterThisQuarter', 'DynFilterLastQuarter',+-- 'DynFilterNextYear', 'DynFilterThisYear', 'DynFilterLastYear', and+-- 'DynFilterYearToDate.+--+-- The above criteria are based on a value range; that is, if today's+-- date is September 22nd, then the range for thisWeek is the values+-- greater than or equal to September 17 and less than September+-- 24. In the thisWeek range, the lower value is expressed+-- '_dfoval'. The higher value is expressed using '_dfoMmaxVal'.+--+-- These dynamic filters shall not require '_dfoVal or '_dfoMaxVal':+-- 'DynFilterQ1', 'DynFilterQ2', 'DynFilterQ3', 'DynFilterQ4',+-- 'DynFilterM1', 'DynFilterM2', 'DynFilterM3', 'DynFilterM4',+-- 'DynFilterM5', 'DynFilterM6', 'DynFilterM7', 'DynFilterM8',+-- 'DynFilterM9', 'DynFilterM10', 'DynFilterM11' and 'DynFilterM12'.+--+-- The above criteria shall not specify the range using valIso and+-- maxValIso because Q1 always starts from M1 to M3, and M1 is always+-- January.+--+-- These types of dynamic filters shall use valIso and shall not use+-- '_dfoMaxVal': 'DynFilterAboveAverage' and 'DynFilterBelowAverage'+--+-- /Note:/ Specification lists 'valIso' and 'maxIso' to store datetime+-- values but it appears that Excel doesn't use them and stored them+-- as numeric values (as it does for datetimes in cell values)+--+-- See 18.3.2.5 "dynamicFilter (Dynamic Filter)" (p. 1715)+data DynFilterOptions = DynFilterOptions+ { _dfoType :: DynFilterType+ , _dfoVal :: Maybe Double+ -- ^ A minimum numeric value for dynamic filter.+ , _dfoMaxVal :: Maybe Double+ -- ^ A maximum value for dynamic filter.+ } deriving (Eq, Show, Generic)+instance NFData DynFilterOptions++-- | Specifies concrete type of dynamic filter used+--+-- See 18.18.26 "ST_DynamicFilterType (Dynamic Filter)" (p. 2452)+data DynFilterType+ = DynFilterAboveAverage+ -- ^ Shows values that are above average.+ | DynFilterBelowAverage+ -- ^ Shows values that are below average.+ | DynFilterLastMonth+ -- ^ Shows last month's dates.+ | DynFilterLastQuarter+ -- ^ Shows last calendar quarter's dates.+ | DynFilterLastWeek+ -- ^ Shows last week's dates, using Sunday as the first weekday.+ | DynFilterLastYear+ -- ^ Shows last year's dates.+ | DynFilterM1+ -- ^ Shows the dates that are in January, regardless of year.+ | DynFilterM10+ -- ^ Shows the dates that are in October, regardless of year.+ | DynFilterM11+ -- ^ Shows the dates that are in November, regardless of year.+ | DynFilterM12+ -- ^ Shows the dates that are in December, regardless of year.+ | DynFilterM2+ -- ^ Shows the dates that are in February, regardless of year.+ | DynFilterM3+ -- ^ Shows the dates that are in March, regardless of year.+ | DynFilterM4+ -- ^ Shows the dates that are in April, regardless of year.+ | DynFilterM5+ -- ^ Shows the dates that are in May, regardless of year.+ | DynFilterM6+ -- ^ Shows the dates that are in June, regardless of year.+ | DynFilterM7+ -- ^ Shows the dates that are in July, regardless of year.+ | DynFilterM8+ -- ^ Shows the dates that are in August, regardless of year.+ | DynFilterM9+ -- ^ Shows the dates that are in September, regardless of year.+ | DynFilterNextMonth+ -- ^ Shows next month's dates.+ | DynFilterNextQuarter+ -- ^ Shows next calendar quarter's dates.+ | DynFilterNextWeek+ -- ^ Shows next week's dates, using Sunday as the first weekday.+ | DynFilterNextYear+ -- ^ Shows next year's dates.+ | DynFilterNull+ -- ^ Common filter type not available.+ | DynFilterQ1+ -- ^ Shows the dates that are in the 1st calendar quarter,+ -- regardless of year.+ | DynFilterQ2+ -- ^ Shows the dates that are in the 2nd calendar quarter,+ -- regardless of year.+ | DynFilterQ3+ -- ^ Shows the dates that are in the 3rd calendar quarter,+ -- regardless of year.+ | DynFilterQ4+ -- ^ Shows the dates that are in the 4th calendar quarter,+ -- regardless of year.+ | DynFilterThisMonth+ -- ^ Shows this month's dates.+ | DynFilterThisQuarter+ -- ^ Shows this calendar quarter's dates.+ | DynFilterThisWeek+ -- ^ Shows this week's dates, using Sunday as the first weekday.+ | DynFilterThisYear+ -- ^ Shows this year's dates.+ | DynFilterToday+ -- ^ Shows today's dates.+ | DynFilterTomorrow+ -- ^ Shows tomorrow's dates.+ | DynFilterYearToDate+ -- ^ Shows the dates between the beginning of the year and today, inclusive.+ | DynFilterYesterday+ -- ^ Shows yesterday's dates.+ deriving (Eq, Show, Generic)+instance NFData DynFilterType++-- | AutoFilter temporarily hides rows based on a filter criteria,+-- which is applied column by column to a table of datain the+-- worksheet.+--+-- TODO: sortState, extList+--+-- See 18.3.1.2 "autoFilter (AutoFilter Settings)" (p. 1596)+data AutoFilter = AutoFilter+ { _afRef :: Maybe CellRef+ , _afFilterColumns :: Map Int FilterColumn+ } deriving (Eq, Show, Generic)+instance NFData AutoFilter++makeLenses ''AutoFilter+++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default AutoFilter where+ def = AutoFilter Nothing M.empty++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor AutoFilter where+ fromCursor cur = do+ _afRef <- maybeAttribute "ref" cur+ let _afFilterColumns = M.fromList $ cur $/ element (addSmlNamespace "filterColumn") >=> \c -> do+ colId <- fromAttribute "colId" c+ fcol <- c $/ anyElement >=> fltColFromNode . node+ return (colId, fcol)+ return AutoFilter {..}++instance FromXenoNode AutoFilter where+ fromXenoNode root = do+ _afRef <- parseAttributes root $ maybeAttr "ref"+ _afFilterColumns <-+ fmap M.fromList . collectChildren root $ fromChildList "filterColumn"+ return AutoFilter {..}++instance FromXenoNode (Int, FilterColumn) where+ fromXenoNode root = do+ colId <- parseAttributes root $ fromAttr "colId"+ fCol <-+ collectChildren root $ asum [filters, color, custom, dynamic, icon, top10]+ return (colId, fCol)+ where+ filters =+ requireAndParse "filters" $ \node -> do+ filterBlank <-+ parseAttributes node $ fromAttrDef "blank" DontFilterByBlank+ filterCriteria <- childListAny node+ return $ Filters filterBlank filterCriteria+ color =+ requireAndParse "colorFilter" $ \node ->+ parseAttributes node $ do+ _cfoCellColor <- fromAttrDef "cellColor" True+ _cfoDxfId <- maybeAttr "dxfId"+ return $ ColorFilter ColorFilterOptions {..}+ custom =+ requireAndParse "customFilters" $ \node -> do+ isAnd <- parseAttributes node $ fromAttrDef "and" False+ cfilters <- collectChildren node $ fromChildList "customFilter"+ case cfilters of+ [f] -> return $ ACustomFilter f+ [f1, f2] ->+ if isAnd+ then return $ CustomFiltersAnd f1 f2+ else return $ CustomFiltersOr f1 f2+ _ ->+ Left $+ "expected 1 or 2 custom filters but found " <>+ T.pack (show $ length cfilters)+ dynamic =+ requireAndParse "dynamicFilter" . flip parseAttributes $ do+ _dfoType <- fromAttr "type"+ _dfoVal <- maybeAttr "val"+ _dfoMaxVal <- maybeAttr "maxVal"+ return $ DynamicFilter DynFilterOptions {..}+ icon =+ requireAndParse "iconFilter" . flip parseAttributes $+ IconFilter <$> maybeAttr "iconId" <*> fromAttr "iconSet"+ top10 =+ requireAndParse "top10" . flip parseAttributes $ do+ top <- fromAttrDef "top" True+ percent <- fromAttrDef "percent" False+ val <- fromAttr "val"+ filterVal <- maybeAttr "filterVal"+ let opts = EdgeFilterOptions percent val filterVal+ if top+ then return $ TopNFilter opts+ else return $ BottomNFilter opts++instance FromXenoNode CustomFilter where+ fromXenoNode root =+ parseAttributes root $+ CustomFilter <$> fromAttrDef "operator" FltrEqual <*> fromAttr "val"++fltColFromNode :: Node -> [FilterColumn]+fltColFromNode n | n `nodeElNameIs` (addSmlNamespace "filters") = do+ let filterCriteria = cur $/ anyElement >=> fromCursor+ filterBlank <- fromAttributeDef "blank" DontFilterByBlank cur+ return $ Filters filterBlank filterCriteria+ | n `nodeElNameIs` (addSmlNamespace "colorFilter") = do+ _cfoCellColor <- fromAttributeDef "cellColor" True cur+ _cfoDxfId <- maybeAttribute "dxfId" cur+ return $ ColorFilter ColorFilterOptions {..}+ | n `nodeElNameIs` (addSmlNamespace "customFilters") = do+ isAnd <- fromAttributeDef "and" False cur+ let cFilters = cur $/ element (addSmlNamespace "customFilter") >=> \c -> do+ op <- fromAttributeDef "operator" FltrEqual c+ val <- fromAttribute "val" c+ return $ CustomFilter op val+ case cFilters of+ [f] ->+ return $ ACustomFilter f+ [f1, f2] ->+ if isAnd+ then return $ CustomFiltersAnd f1 f2+ else return $ CustomFiltersOr f1 f2+ _ ->+ fail "bad custom filter"+ | n `nodeElNameIs` (addSmlNamespace "dynamicFilter") = do+ _dfoType <- fromAttribute "type" cur+ _dfoVal <- maybeAttribute "val" cur+ _dfoMaxVal <- maybeAttribute "maxVal" cur+ return $ DynamicFilter DynFilterOptions{..}+ | n `nodeElNameIs` (addSmlNamespace "iconFilter") = do+ iconId <- maybeAttribute "iconId" cur+ iconSet <- fromAttribute "iconSet" cur+ return $ IconFilter iconId iconSet+ | n `nodeElNameIs` (addSmlNamespace "top10") = do+ top <- fromAttributeDef "top" True cur+ let percent = fromAttributeDef "percent" False cur+ val = fromAttribute "val" cur+ filterVal = maybeAttribute "filterVal" cur+ if top+ then fmap TopNFilter $+ EdgeFilterOptions <$> percent <*> val <*> filterVal+ else fmap BottomNFilter $+ EdgeFilterOptions <$> percent <*> val <*> filterVal+ | otherwise = fail "no matching nodes"+ where+ cur = fromNode n++instance FromCursor FilterCriterion where+ fromCursor = filterCriterionFromNode . node++instance FromXenoNode FilterCriterion where+ fromXenoNode root =+ case Xeno.name root of+ "filter" -> parseAttributes root $ do FilterValue <$> fromAttr "val"+ "dateGroupItem" ->+ parseAttributes root $ do+ grouping <- fromAttr "dateTimeGrouping"+ group <- case grouping of+ ("year" :: ByteString) ->+ DateGroupByYear <$> fromAttr "year"+ "month" ->+ DateGroupByMonth <$> fromAttr "year"+ <*> fromAttr "month"+ "day" ->+ DateGroupByDay <$> fromAttr "year"+ <*> fromAttr "month"+ <*> fromAttr "day"+ "hour" ->+ DateGroupByHour <$> fromAttr "year"+ <*> fromAttr "month"+ <*> fromAttr "day"+ <*> fromAttr "hour"+ "minute" ->+ DateGroupByMinute <$> fromAttr "year"+ <*> fromAttr "month"+ <*> fromAttr "day"+ <*> fromAttr "hour"+ <*> fromAttr "minute"+ "second" ->+ DateGroupBySecond <$> fromAttr "year"+ <*> fromAttr "month"+ <*> fromAttr "day"+ <*> fromAttr "hour"+ <*> fromAttr "minute"+ <*> fromAttr "second"+ _ -> toAttrParser . Left $ "Unexpected date grouping"+ return $ FilterDateGroup group+ _ -> Left "Bad FilterCriterion"++-- TODO: follow the spec about the fact that dategroupitem always go after filter+filterCriterionFromNode :: Node -> [FilterCriterion]+filterCriterionFromNode n+ | n `nodeElNameIs` (addSmlNamespace "filter") = do+ v <- fromAttribute "val" cur+ return $ FilterValue v+ | n `nodeElNameIs` (addSmlNamespace "dateGroupItem") = do+ g <- fromAttribute "dateTimeGrouping" cur+ let year = fromAttribute "year" cur+ month = fromAttribute "month" cur+ day = fromAttribute "day" cur+ hour = fromAttribute "hour" cur+ minute = fromAttribute "minute" cur+ second = fromAttribute "second" cur+ FilterDateGroup <$>+ case g of+ "year" -> DateGroupByYear <$> year+ "month" -> DateGroupByMonth <$> year <*> month+ "day" -> DateGroupByDay <$> year <*> month <*> day+ "hour" -> DateGroupByHour <$> year <*> month <*> day <*> hour+ "minute" ->+ DateGroupByMinute <$> year <*> month <*> day <*> hour <*> minute+ "second" ->+ DateGroupBySecond <$> year <*> month <*> day <*> hour <*> minute <*>+ second+ _ -> fail $ "unexpected dateTimeGrouping " ++ show (g :: Text)+ | otherwise = fail "no matching nodes"+ where+ cur = fromNode n++instance FromAttrVal CustomFilterOperator where+ fromAttrVal "equal" = readSuccess FltrEqual+ fromAttrVal "greaterThan" = readSuccess FltrGreaterThan+ fromAttrVal "greaterThanOrEqual" = readSuccess FltrGreaterThanOrEqual+ fromAttrVal "lessThan" = readSuccess FltrLessThan+ fromAttrVal "lessThanOrEqual" = readSuccess FltrLessThanOrEqual+ fromAttrVal "notEqual" = readSuccess FltrNotEqual+ fromAttrVal t = invalidText "CustomFilterOperator" t++instance FromAttrBs CustomFilterOperator where+ fromAttrBs "equal" = return FltrEqual+ fromAttrBs "greaterThan" = return FltrGreaterThan+ fromAttrBs "greaterThanOrEqual" = return FltrGreaterThanOrEqual+ fromAttrBs "lessThan" = return FltrLessThan+ fromAttrBs "lessThanOrEqual" = return FltrLessThanOrEqual+ fromAttrBs "notEqual" = return FltrNotEqual+ fromAttrBs x = unexpectedAttrBs "CustomFilterOperator" x++instance FromAttrVal FilterByBlank where+ fromAttrVal =+ fmap (first $ bool DontFilterByBlank FilterByBlank) . fromAttrVal++instance FromAttrBs FilterByBlank where+ fromAttrBs = fmap (bool DontFilterByBlank FilterByBlank) . fromAttrBs++instance FromAttrVal DynFilterType where+ fromAttrVal "aboveAverage" = readSuccess DynFilterAboveAverage+ fromAttrVal "belowAverage" = readSuccess DynFilterBelowAverage+ fromAttrVal "lastMonth" = readSuccess DynFilterLastMonth+ fromAttrVal "lastQuarter" = readSuccess DynFilterLastQuarter+ fromAttrVal "lastWeek" = readSuccess DynFilterLastWeek+ fromAttrVal "lastYear" = readSuccess DynFilterLastYear+ fromAttrVal "M1" = readSuccess DynFilterM1+ fromAttrVal "M10" = readSuccess DynFilterM10+ fromAttrVal "M11" = readSuccess DynFilterM11+ fromAttrVal "M12" = readSuccess DynFilterM12+ fromAttrVal "M2" = readSuccess DynFilterM2+ fromAttrVal "M3" = readSuccess DynFilterM3+ fromAttrVal "M4" = readSuccess DynFilterM4+ fromAttrVal "M5" = readSuccess DynFilterM5+ fromAttrVal "M6" = readSuccess DynFilterM6+ fromAttrVal "M7" = readSuccess DynFilterM7+ fromAttrVal "M8" = readSuccess DynFilterM8+ fromAttrVal "M9" = readSuccess DynFilterM9+ fromAttrVal "nextMonth" = readSuccess DynFilterNextMonth+ fromAttrVal "nextQuarter" = readSuccess DynFilterNextQuarter+ fromAttrVal "nextWeek" = readSuccess DynFilterNextWeek+ fromAttrVal "nextYear" = readSuccess DynFilterNextYear+ fromAttrVal "null" = readSuccess DynFilterNull+ fromAttrVal "Q1" = readSuccess DynFilterQ1+ fromAttrVal "Q2" = readSuccess DynFilterQ2+ fromAttrVal "Q3" = readSuccess DynFilterQ3+ fromAttrVal "Q4" = readSuccess DynFilterQ4+ fromAttrVal "thisMonth" = readSuccess DynFilterThisMonth+ fromAttrVal "thisQuarter" = readSuccess DynFilterThisQuarter+ fromAttrVal "thisWeek" = readSuccess DynFilterThisWeek+ fromAttrVal "thisYear" = readSuccess DynFilterThisYear+ fromAttrVal "today" = readSuccess DynFilterToday+ fromAttrVal "tomorrow" = readSuccess DynFilterTomorrow+ fromAttrVal "yearToDate" = readSuccess DynFilterYearToDate+ fromAttrVal "yesterday" = readSuccess DynFilterYesterday+ fromAttrVal t = invalidText "DynFilterType" t++instance FromAttrBs DynFilterType where+ fromAttrBs "aboveAverage" = return DynFilterAboveAverage+ fromAttrBs "belowAverage" = return DynFilterBelowAverage+ fromAttrBs "lastMonth" = return DynFilterLastMonth+ fromAttrBs "lastQuarter" = return DynFilterLastQuarter+ fromAttrBs "lastWeek" = return DynFilterLastWeek+ fromAttrBs "lastYear" = return DynFilterLastYear+ fromAttrBs "M1" = return DynFilterM1+ fromAttrBs "M10" = return DynFilterM10+ fromAttrBs "M11" = return DynFilterM11+ fromAttrBs "M12" = return DynFilterM12+ fromAttrBs "M2" = return DynFilterM2+ fromAttrBs "M3" = return DynFilterM3+ fromAttrBs "M4" = return DynFilterM4+ fromAttrBs "M5" = return DynFilterM5+ fromAttrBs "M6" = return DynFilterM6+ fromAttrBs "M7" = return DynFilterM7+ fromAttrBs "M8" = return DynFilterM8+ fromAttrBs "M9" = return DynFilterM9+ fromAttrBs "nextMonth" = return DynFilterNextMonth+ fromAttrBs "nextQuarter" = return DynFilterNextQuarter+ fromAttrBs "nextWeek" = return DynFilterNextWeek+ fromAttrBs "nextYear" = return DynFilterNextYear+ fromAttrBs "null" = return DynFilterNull+ fromAttrBs "Q1" = return DynFilterQ1+ fromAttrBs "Q2" = return DynFilterQ2+ fromAttrBs "Q3" = return DynFilterQ3+ fromAttrBs "Q4" = return DynFilterQ4+ fromAttrBs "thisMonth" = return DynFilterThisMonth+ fromAttrBs "thisQuarter" = return DynFilterThisQuarter+ fromAttrBs "thisWeek" = return DynFilterThisWeek+ fromAttrBs "thisYear" = return DynFilterThisYear+ fromAttrBs "today" = return DynFilterToday+ fromAttrBs "tomorrow" = return DynFilterTomorrow+ fromAttrBs "yearToDate" = return DynFilterYearToDate+ fromAttrBs "yesterday" = return DynFilterYesterday+ fromAttrBs x = unexpectedAttrBs "DynFilterType" x++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement AutoFilter where+ toElement nm AutoFilter {..} =+ elementList+ nm+ (catMaybes ["ref" .=? _afRef])+ [ elementList+ (addSmlNamespace "filterColumn")+ ["colId" .= colId]+ [fltColToElement fCol]+ | (colId, fCol) <- M.toList _afFilterColumns+ ]++fltColToElement :: FilterColumn -> Element+fltColToElement (Filters filterBlank filterCriteria) =+ let attrs = catMaybes ["blank" .=? justNonDef DontFilterByBlank filterBlank]+ in elementList+ (addSmlNamespace "filters") attrs $ map filterCriterionToElement filterCriteria+fltColToElement (ColorFilter opts) = toElement (addSmlNamespace "colorFilter") opts+fltColToElement (ACustomFilter f) =+ elementListSimple (addSmlNamespace "customFilters") [toElement (addSmlNamespace "customFilter") f]+fltColToElement (CustomFiltersOr f1 f2) =+ elementListSimple+ (addSmlNamespace "customFilters")+ [toElement (addSmlNamespace "customFilter") f | f <- [f1, f2]]+fltColToElement (CustomFiltersAnd f1 f2) =+ elementList+ (addSmlNamespace "customFilters")+ ["and" .= True]+ [toElement (addSmlNamespace "customFilter") f | f <- [f1, f2]]+fltColToElement (DynamicFilter opts) = toElement (addSmlNamespace "dynamicFilter") opts+fltColToElement (IconFilter iconId iconSet) =+ leafElement (addSmlNamespace "iconFilter") $+ ["iconSet" .= iconSet] ++ catMaybes ["iconId" .=? iconId]+fltColToElement (BottomNFilter opts) = edgeFilter False opts+fltColToElement (TopNFilter opts) = edgeFilter True opts++edgeFilter :: Bool -> EdgeFilterOptions -> Element+edgeFilter top EdgeFilterOptions {..} =+ leafElement (addSmlNamespace "top10") $+ ["top" .= top, "percent" .= _efoUsePercents, "val" .= _efoVal] +++ catMaybes ["filterVal" .=? _efoFilterVal]++filterCriterionToElement :: FilterCriterion -> Element+filterCriterionToElement (FilterValue v) =+ leafElement (addSmlNamespace "filter") ["val" .= v]+filterCriterionToElement (FilterDateGroup (DateGroupByYear y)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ ["dateTimeGrouping" .= ("year" :: Text), "year" .= y]+filterCriterionToElement (FilterDateGroup (DateGroupByMonth y m)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ ["dateTimeGrouping" .= ("month" :: Text), "year" .= y, "month" .= m]+filterCriterionToElement (FilterDateGroup (DateGroupByDay y m d)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ ["dateTimeGrouping" .= ("day" :: Text), "year" .= y, "month" .= m, "day" .= d]+filterCriterionToElement (FilterDateGroup (DateGroupByHour y m d h)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ [ "dateTimeGrouping" .= ("hour" :: Text)+ , "year" .= y+ , "month" .= m+ , "day" .= d+ , "hour" .= h+ ]+filterCriterionToElement (FilterDateGroup (DateGroupByMinute y m d h mi)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ [ "dateTimeGrouping" .= ("minute" :: Text)+ , "year" .= y+ , "month" .= m+ , "day" .= d+ , "hour" .= h+ , "minute" .= mi+ ]+filterCriterionToElement (FilterDateGroup (DateGroupBySecond y m d h mi s)) =+ leafElement+ (addSmlNamespace "dateGroupItem")+ [ "dateTimeGrouping" .= ("second" :: Text)+ , "year" .= y+ , "month" .= m+ , "day" .= d+ , "hour" .= h+ , "minute" .= mi+ , "second" .= s+ ]++instance ToElement CustomFilter where+ toElement nm CustomFilter {..} =+ leafElement nm ["operator" .= cfltOperator, "val" .= cfltValue]++instance ToAttrVal CustomFilterOperator where+ toAttrVal FltrEqual = "equal"+ toAttrVal FltrGreaterThan = "greaterThan"+ toAttrVal FltrGreaterThanOrEqual = "greaterThanOrEqual"+ toAttrVal FltrLessThan = "lessThan"+ toAttrVal FltrLessThanOrEqual = "lessThanOrEqual"+ toAttrVal FltrNotEqual = "notEqual"++instance ToAttrVal FilterByBlank where+ toAttrVal FilterByBlank = toAttrVal True+ toAttrVal DontFilterByBlank = toAttrVal False++instance ToElement ColorFilterOptions where+ toElement nm ColorFilterOptions {..} =+ leafElement nm $+ catMaybes ["cellColor" .=? justFalse _cfoCellColor, "dxfId" .=? _cfoDxfId]++instance ToElement DynFilterOptions where+ toElement nm DynFilterOptions {..} =+ leafElement nm $+ ["type" .= _dfoType] +++ catMaybes ["val" .=? _dfoVal, "maxVal" .=? _dfoMaxVal]++instance ToAttrVal DynFilterType where+ toAttrVal DynFilterAboveAverage = "aboveAverage"+ toAttrVal DynFilterBelowAverage = "belowAverage"+ toAttrVal DynFilterLastMonth = "lastMonth"+ toAttrVal DynFilterLastQuarter = "lastQuarter"+ toAttrVal DynFilterLastWeek = "lastWeek"+ toAttrVal DynFilterLastYear = "lastYear"+ toAttrVal DynFilterM1 = "M1"+ toAttrVal DynFilterM10 = "M10"+ toAttrVal DynFilterM11 = "M11"+ toAttrVal DynFilterM12 = "M12"+ toAttrVal DynFilterM2 = "M2"+ toAttrVal DynFilterM3 = "M3"+ toAttrVal DynFilterM4 = "M4"+ toAttrVal DynFilterM5 = "M5"+ toAttrVal DynFilterM6 = "M6"+ toAttrVal DynFilterM7 = "M7"+ toAttrVal DynFilterM8 = "M8"+ toAttrVal DynFilterM9 = "M9"+ toAttrVal DynFilterNextMonth = "nextMonth"+ toAttrVal DynFilterNextQuarter = "nextQuarter"+ toAttrVal DynFilterNextWeek = "nextWeek"+ toAttrVal DynFilterNextYear = "nextYear"+ toAttrVal DynFilterNull = "null"+ toAttrVal DynFilterQ1 = "Q1"+ toAttrVal DynFilterQ2 = "Q2"+ toAttrVal DynFilterQ3 = "Q3"+ toAttrVal DynFilterQ4 = "Q4"+ toAttrVal DynFilterThisMonth = "thisMonth"+ toAttrVal DynFilterThisQuarter = "thisQuarter"+ toAttrVal DynFilterThisWeek = "thisWeek"+ toAttrVal DynFilterThisYear = "thisYear"+ toAttrVal DynFilterToday = "today"+ toAttrVal DynFilterTomorrow = "tomorrow"+ toAttrVal DynFilterYearToDate = "yearToDate"+ toAttrVal DynFilterYesterday = "yesterday"
+ src/Codec/Xlsx/Types/Cell.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Cell+ ( CellFormula(..)+ , FormulaExpression(..)+ , simpleCellFormula+ , sharedFormulaByIndex+ , SharedFormulaIndex(..)+ , SharedFormulaOptions(..)+ , formulaDataFromCursor+ , applySharedFormulaOpts+ , Cell(..)+ , cellStyle+ , cellValue+ , cellComment+ , cellFormula+ , CellMap+ ) where++import Control.Arrow (first)+import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Default+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe (catMaybes, listToMaybe)+import Data.Monoid ((<>))+import Data.Text (Text)+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Comment+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Writer.Internal++-- | Formula for the cell.+--+-- TODO: array, dataTable formula types support+--+-- See 18.3.1.40 "f (Formula)" (p. 1636)+data CellFormula = CellFormula+ { _cellfExpression :: FormulaExpression+ , _cellfAssignsToName :: Bool+ -- ^ Specifies that this formula assigns a value to a name.+ , _cellfCalculate :: Bool+ -- ^ Indicates that this formula needs to be recalculated+ -- the next time calculation is performed.+ -- [/Example/: This is always set on volatile functions,+ -- like =RAND(), and circular references. /end example/]+ } deriving (Eq, Show, Generic)+instance NFData CellFormula++-- | formula type with type-specific options+data FormulaExpression+ = NormalFormula Formula+ | SharedFormula SharedFormulaIndex+ deriving (Eq, Show, Generic)+instance NFData FormulaExpression++defaultFormulaType :: Text+defaultFormulaType = "normal"++-- | index of shared formula in worksheet's 'wsSharedFormulas'+-- property+newtype SharedFormulaIndex = SharedFormulaIndex Int+ deriving (Eq, Ord, Show, Generic)+instance NFData SharedFormulaIndex++data SharedFormulaOptions = SharedFormulaOptions+ { _sfoRef :: CellRef+ , _sfoExpression :: Formula+ }+ deriving (Eq, Show, Generic)+instance NFData SharedFormulaOptions++simpleCellFormula :: Text -> CellFormula+simpleCellFormula expr = CellFormula+ { _cellfExpression = NormalFormula $ Formula expr+ , _cellfAssignsToName = False+ , _cellfCalculate = False+ }++sharedFormulaByIndex :: SharedFormulaIndex -> CellFormula+sharedFormulaByIndex si =+ CellFormula+ { _cellfExpression = SharedFormula si+ , _cellfAssignsToName = False+ , _cellfCalculate = False+ }++-- | Currently cell details include cell values, style ids and cell+-- formulas (inline strings from @\<is\>@ subelements are ignored)+data Cell = Cell+ { _cellStyle :: Maybe Int+ , _cellValue :: Maybe CellValue+ , _cellComment :: Maybe Comment+ , _cellFormula :: Maybe CellFormula+ } deriving (Eq, Show, Generic)+instance NFData Cell++instance Default Cell where+ def = Cell Nothing Nothing Nothing Nothing++makeLenses ''Cell++-- | Map containing cell values which are indexed by row and column+-- if you need to use more traditional (x,y) indexing please you could+-- use corresponding accessors from ''Codec.Xlsx.Lens''+type CellMap = Map (RowIndex, ColumnIndex) Cell++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++formulaDataFromCursor ::+ Cursor -> [(CellFormula, Maybe (SharedFormulaIndex, SharedFormulaOptions))]+formulaDataFromCursor cur = do+ _cellfAssignsToName <- fromAttributeDef "bx" False cur+ _cellfCalculate <- fromAttributeDef "ca" False cur+ t <- fromAttributeDef "t" defaultFormulaType cur+ (_cellfExpression, shared) <-+ case t of+ d| d == defaultFormulaType -> do+ formula <- fromCursor cur+ return (NormalFormula formula, Nothing)+ "shared" -> do+ let expr = listToMaybe $ fromCursor cur+ ref <- maybeAttribute "ref" cur+ si <- fromAttribute "si" cur+ return (SharedFormula si, (,) <$> pure si <*>+ (SharedFormulaOptions <$> ref <*> expr))+ _ ->+ fail $ "Unexpected formula type" ++ show t+ return (CellFormula {..}, shared)++instance FromAttrVal SharedFormulaIndex where+ fromAttrVal = fmap (first SharedFormulaIndex) . fromAttrVal++instance FromAttrBs SharedFormulaIndex where+ fromAttrBs = fmap SharedFormulaIndex . fromAttrBs++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement CellFormula where+ toElement nm CellFormula {..} =+ formulaEl {elementAttributes = elementAttributes formulaEl <> commonAttrs}+ where+ commonAttrs =+ M.fromList $+ catMaybes+ [ "bx" .=? justTrue _cellfAssignsToName+ , "ca" .=? justTrue _cellfCalculate+ , "t" .=? justNonDef defaultFormulaType fType+ ]+ (formulaEl, fType) =+ case _cellfExpression of+ NormalFormula f -> (toElement nm f, defaultFormulaType)+ SharedFormula si -> (leafElement nm ["si" .= si], "shared")++instance ToAttrVal SharedFormulaIndex where+ toAttrVal (SharedFormulaIndex si) = toAttrVal si++applySharedFormulaOpts :: SharedFormulaOptions -> Element -> Element+applySharedFormulaOpts SharedFormulaOptions {..} el =+ el+ { elementAttributes = elementAttributes el <> M.fromList ["ref" .= _sfoRef]+ , elementNodes = NodeContent (unFormula _sfoExpression) : elementNodes el+ }
+ src/Codec/Xlsx/Types/Comment.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Comment where++import Control.DeepSeq (NFData)+import Data.Text (Text)+import GHC.Generics (Generic)++import Codec.Xlsx.Types.Common++-- | User comment for a cell+--+-- TODO: the following child elements:+-- guid, shapeId, commentPr+--+-- Section 18.7.3 "comment (Comment)" (p. 1749)+data Comment = Comment+ { _commentText :: XlsxText+ -- ^ cell comment text, maybe formatted+ -- Section 18.7.7 "text (Comment Text)" (p. 1754)+ , _commentAuthor :: Text+ -- ^ comment author+ , _commentVisible :: Bool+ } deriving (Eq, Show, Generic)+instance NFData Comment
+ src/Codec/Xlsx/Types/Common.hs view
@@ -0,0 +1,713 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Codec.Xlsx.Types.Common+ ( CellRef(..)+ , RowCoord(..)+ , ColumnCoord(..)+ , CellCoord+ , RangeCoord+ , mapBoth+ , col2coord+ , coord2col+ , row2coord+ , coord2row+ , singleCellRef+ , singleCellRef'+ , fromSingleCellRef+ , fromSingleCellRef'+ , fromSingleCellRefNoting+ , escapeRefSheetName+ , unEscapeRefSheetName+ , mkForeignSingleCellRef+ , fromForeignSingleCellRef+ , Range+ , mkRange+ , mkRange'+ , mkForeignRange+ , fromRange+ , fromRange'+ , fromForeignRange+ , SqRef(..)+ , XlsxText(..)+ , xlsxTextToCellValue+ , Formula(..)+ , CellValue(..)+ , ErrorType(..)+ , DateBase(..)+ , dateFromNumber+ , dateToNumber+ , int2col+ , col2int+ , columnIndexToText+ , textToColumnIndex+ -- ** prisms+ , _XlsxText+ , _XlsxRichText+ , _CellText+ , _CellDouble+ , _CellBool+ , _CellRich+ , _CellError+ , RowIndex(..)+ , ColumnIndex(..)+ ) where++import GHC.Generics (Generic)++import Control.Applicative (liftA2)+import Control.Arrow+import Control.DeepSeq (NFData)+import Control.Monad (forM, guard)+import Data.Bifunctor (bimap)+import qualified Data.ByteString as BS+import Data.Char+import Data.Maybe (isJust, fromMaybe)+import Data.Function ((&))+import Data.Ix (inRange)+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Time.Calendar (Day, addDays, diffDays, fromGregorian)+import Data.Time.Clock (UTCTime(UTCTime), picosecondsToDiffTime)+import Safe+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.RichText+import Codec.Xlsx.Writer.Internal+import Codec.Xlsx.LensCompat (Prism', prism)++newtype RowIndex = RowIndex {unRowIndex :: Int}+ deriving (Eq, Ord, Show, Read, Generic, Num, Real, Enum, Integral)+newtype ColumnIndex = ColumnIndex {unColumnIndex :: Int}+ deriving (Eq, Ord, Show, Read, Generic, Num, Real, Enum, Integral)+instance NFData RowIndex+instance NFData ColumnIndex++instance ToAttrVal RowIndex where+ toAttrVal = toAttrVal . unRowIndex++{-# DEPRECATED int2col+ "this function will be removed in an upcoming release, use columnIndexToText instead." #-}+int2col :: ColumnIndex -> Text+int2col = columnIndexToText++{-# DEPRECATED col2int+ "this function will be removed in an upcoming release, use textToColumnIndex instead." #-}+col2int :: Text -> ColumnIndex+col2int = textToColumnIndex++-- | convert column number (starting from 1) to its textual form (e.g. 3 -> \"C\")+columnIndexToText :: ColumnIndex -> Text+columnIndexToText = T.pack . reverse . map int2let . base26 . unColumnIndex+ where+ int2let 0 = 'Z'+ int2let x = chr $ (x - 1) + ord 'A'+ base26 0 = []+ base26 i = let i' = (i `mod` 26)+ i'' = if i' == 0 then 26 else i'+ in seq i' (i' : base26 ((i - i'') `div` 26))++rowIndexToText :: RowIndex -> Text+rowIndexToText = T.pack . show . unRowIndex++-- | reverse of 'columnIndexToText'+textToColumnIndex :: Text -> ColumnIndex+textToColumnIndex = ColumnIndex . T.foldl' (\i c -> i * 26 + let2int c) 0+ where+ let2int c = 1 + ord c - ord 'A'++textToRowIndex :: Text -> RowIndex+textToRowIndex = RowIndex . read . T.unpack++-- | Excel cell or cell range reference (e.g. @E3@), possibly absolute.+-- See 18.18.62 @ST_Ref@ (p. 2482)+--+-- Note: The @ST_Ref@ type can point to another sheet (supported)+-- or a sheet in another workbook (separate .xlsx file, not implemented).+newtype CellRef = CellRef+ { unCellRef :: Text+ } deriving (Eq, Ord, Show, Generic)+instance NFData CellRef++-- | A helper type for coordinates to carry the intent of them being relative or absolute (preceded by '$'):+--+-- > singleCellRefRaw' (RowRel 5, ColumnAbs 1) == "$A5"+data RowCoord+ = RowAbs !RowIndex+ | RowRel !RowIndex+ deriving (Eq, Ord, Show, Read, Generic)+instance NFData RowCoord++data ColumnCoord+ = ColumnAbs !ColumnIndex+ | ColumnRel !ColumnIndex+ deriving (Eq, Ord, Show, Read, Generic)+instance NFData ColumnCoord++type CellCoord = (RowCoord, ColumnCoord)++type RangeCoord = (CellCoord, CellCoord)++mkColumnCoord :: Bool -> ColumnIndex -> ColumnCoord+mkColumnCoord isAbs = if isAbs then ColumnAbs else ColumnRel++mkRowCoord :: Bool -> RowIndex -> RowCoord+mkRowCoord isAbs = if isAbs then RowAbs else RowRel++coord2col :: ColumnCoord -> Text+coord2col (ColumnAbs c) = "$" <> coord2col (ColumnRel c)+coord2col (ColumnRel c) = columnIndexToText c++col2coord :: Text -> ColumnCoord+col2coord t =+ let t' = T.stripPrefix "$" t+ in mkColumnCoord (isJust t') (textToColumnIndex (fromMaybe t t'))++coord2row :: RowCoord -> Text+coord2row (RowAbs c) = "$" <> coord2row (RowRel c)+coord2row (RowRel c) = rowIndexToText c++row2coord :: Text -> RowCoord+row2coord t =+ let t' = T.stripPrefix "$" t+ in mkRowCoord (isJust t') (textToRowIndex (fromMaybe t t'))++-- | Unwrap a Coord into an abstract Int coordinate+unRowCoord :: RowCoord -> RowIndex+unRowCoord (RowAbs i) = i+unRowCoord (RowRel i) = i++-- | Unwrap a Coord into an abstract Int coordinate+unColumnCoord :: ColumnCoord -> ColumnIndex+unColumnCoord (ColumnAbs i) = i+unColumnCoord (ColumnRel i) = i++-- | Helper function to apply the same transformation to both members of a tuple+--+-- > mapBoth Abs (1, 2) == (Abs 1, Abs 2s)+mapBoth :: (a -> b) -> (a, a) -> (b, b)+mapBoth f = bimap f f++-- | Render position in @(row, col)@ format to an Excel reference.+--+-- > singleCellRef (RowIndex 2, ColumnIndex 4) == CellRef "D2"+singleCellRef :: (RowIndex, ColumnIndex) -> CellRef+singleCellRef = CellRef . singleCellRefRaw++-- | Allow specifying whether a coordinate parameter is relative or absolute.+--+-- > singleCellRef' (Rel 5, Abs 1) == CellRef "$A5"+singleCellRef' :: CellCoord -> CellRef+singleCellRef' = CellRef . singleCellRefRaw'++singleCellRefRaw :: (RowIndex, ColumnIndex) -> Text+singleCellRefRaw (row, col) = T.concat [columnIndexToText col, rowIndexToText row]++singleCellRefRaw' :: CellCoord -> Text+singleCellRefRaw' (row, col) =+ coord2col col <> coord2row row++-- | Converse function to 'singleCellRef'+-- Ignores a potential foreign sheet prefix.+fromSingleCellRef :: CellRef -> Maybe (RowIndex, ColumnIndex)+fromSingleCellRef = fromSingleCellRefRaw . unCellRef++-- | Converse function to 'singleCellRef\''+-- Ignores a potential foreign sheet prefix.+fromSingleCellRef' :: CellRef -> Maybe CellCoord+fromSingleCellRef' = fromSingleCellRefRaw' . unCellRef++fromSingleCellRefRaw :: Text -> Maybe (RowIndex, ColumnIndex)+fromSingleCellRefRaw =+ fmap (first unRowCoord . second unColumnCoord) . fromSingleCellRefRaw'++fromSingleCellRefRaw' :: Text -> Maybe CellCoord+fromSingleCellRefRaw' t' = ignoreRefSheetName t' >>= \t -> do+ let (isColAbsolute, remT) =+ T.stripPrefix "$" t+ & \remT' -> (isJust remT', fromMaybe t remT')+ let (colT, rowExpr) = T.span (inRange ('A', 'Z')) remT+ let (isRowAbsolute, rowT) =+ T.stripPrefix "$" rowExpr+ & \rowT' -> (isJust rowT', fromMaybe rowExpr rowT')+ guard $ not (T.null colT) && not (T.null rowT) && T.all isDigit rowT+ row <- decimal rowT+ return $+ bimap+ (mkRowCoord isRowAbsolute)+ (mkColumnCoord isColAbsolute)+ (row, textToColumnIndex colT)++-- | Converse function to 'singleCellRef' expecting valid reference and failig with+-- a standard error message like /"Bad cell reference 'XXX'"/+fromSingleCellRefNoting :: CellRef -> (RowIndex, ColumnIndex)+fromSingleCellRefNoting ref = fromJustNote errMsg $ fromSingleCellRefRaw txt+ where+ txt = unCellRef ref+ errMsg = "Bad cell reference '" ++ T.unpack txt ++ "'"++-- | Frame and escape the referenced sheet name in single quotes (apostrophe).+--+-- Sheet name in ST_Ref can be single-quoted when it contains non-alphanum class, non-ASCII range characters.+-- Intermediate squote characters are escaped in a doubled fashion:+-- "My ' Sheet" -> 'My '' Sheet'+escapeRefSheetName :: Text -> Text+escapeRefSheetName sheetName =+ T.concat ["'", escape sheetName, "'"]+ where+ escape sn = T.splitOn "'" sn & T.intercalate "''"++-- | Unframe and unescape the referenced sheet name.+unEscapeRefSheetName :: Text -> Text+unEscapeRefSheetName = unescape . unFrame+ where+ unescape = T.intercalate "'" . T.splitOn "''"+ unFrame sn = fromMaybe sn $ T.stripPrefix "'" sn >>= T.stripSuffix "'"++ignoreRefSheetName :: Text -> Maybe Text+ignoreRefSheetName t =+ case T.split (== '!') t of+ [_, r] -> Just r+ [r] -> Just r+ _ -> Nothing++-- | Render a single cell existing in another worksheet.+-- This function always renders the sheet name single-quoted regardless the presence of spaces.+-- A sheet name shouldn't contain @"[]*:?/\"@ chars and apostrophe @"'"@ should not happen at extremities.+--+-- > mkForeignRange "MyOtherSheet" (Rel 2, Rel 4) (Abs 6, Abs 8) == "'MyOtherSheet'!D2:$H$6"+mkForeignSingleCellRef :: Text -> CellCoord -> CellRef+mkForeignSingleCellRef sheetName coord =+ let cr = singleCellRefRaw' coord+ in CellRef $ T.concat [escapeRefSheetName sheetName, "!", cr]++-- | Converse function to 'mkForeignSingleCellRef'.+-- The provided CellRef must be a foreign range.+fromForeignSingleCellRef :: CellRef -> Maybe (Text, CellCoord)+fromForeignSingleCellRef r =+ case T.split (== '!') (unCellRef r) of+ [sheetName, ref] -> (unEscapeRefSheetName sheetName,) <$> fromSingleCellRefRaw' ref+ _ -> Nothing++-- | Excel range (e.g. @D13:H14@), actually store as as 'CellRef' in+-- xlsx+type Range = CellRef++-- | Render range+--+-- > mkRange (RowIndex 2, ColumnIndex 4) (RowIndex 6, ColumnIndex 8) == CellRef "D2:H6"+mkRange :: (RowIndex, ColumnIndex) -> (RowIndex, ColumnIndex) -> Range+mkRange fr to = CellRef $ T.concat [singleCellRefRaw fr, ":", singleCellRefRaw to]++-- | Render range with possibly absolute coordinates+--+-- > mkRange' (Abs 2, Abs 4) (6, 8) == CellRef "$D$2:H6"+mkRange' :: (RowCoord,ColumnCoord) -> (RowCoord,ColumnCoord) -> Range+mkRange' fr to =+ CellRef $ T.concat [singleCellRefRaw' fr, ":", singleCellRefRaw' to]++-- | Render a cell range existing in another worksheet.+-- This function always renders the sheet name single-quoted regardless the presence of spaces.+-- A sheet name shouldn't contain @"[]*:?/\"@ chars and apostrophe @"'"@ should not happen at extremities.+--+-- > mkForeignRange "MyOtherSheet" (Rel 2, Rel 4) (Abs 6, Abs 8) == "'MyOtherSheet'!D2:$H$6"+mkForeignRange :: Text -> CellCoord -> CellCoord -> Range+mkForeignRange sheetName fr to =+ case mkRange' fr to of+ CellRef cr -> CellRef $ T.concat [escapeRefSheetName sheetName, "!", cr]++-- | Converse function to 'mkRange' ignoring absolute coordinates.+-- Ignores a potential foreign sheet prefix.+fromRange :: Range -> Maybe ((RowIndex, ColumnIndex), (RowIndex, ColumnIndex))+fromRange r =+ mapBoth (first unRowCoord . second unColumnCoord) <$> fromRange' r++-- | Converse function to 'mkRange\'' to handle possibly absolute coordinates.+-- Ignores a potential foreign sheet prefix.+fromRange' :: Range -> Maybe RangeCoord+fromRange' t' = parseRange =<< ignoreRefSheetName (unCellRef t')+ where+ parseRange t =+ case T.split (== ':') t of+ [from, to] -> liftA2 (,) (fromSingleCellRefRaw' from) (fromSingleCellRefRaw' to)+ _ -> Nothing++-- | Converse function to 'mkForeignRange'.+-- The provided Range must be a foreign range.+fromForeignRange :: Range -> Maybe (Text, RangeCoord)+fromForeignRange r =+ case T.split (== '!') (unCellRef r) of+ [sheetName, ref] -> (unEscapeRefSheetName sheetName,) <$> fromRange' (CellRef ref)+ _ -> Nothing++-- | A sequence of cell references+--+-- See 18.18.76 "ST_Sqref (Reference Sequence)" (p.2488)+newtype SqRef = SqRef [CellRef]+ deriving (Eq, Ord, Show, Generic)++instance NFData SqRef++-- | Common type containing either simple string or rich formatted text.+-- Used in @si@, @comment@ and @is@ elements+--+-- E.g. @si@ spec says: "If the string is just a simple string with formatting applied+-- at the cell level, then the String Item (si) should contain a single text+-- element used to express the string. However, if the string in the cell is+-- more complex - i.e., has formatting applied at the character level - then the+-- string item shall consist of multiple rich text runs which collectively are+-- used to express the string.". So we have either a single "Text" field, or+-- else a list of "RichTextRun"s, each of which is some "Text" with layout+-- properties.+--+-- TODO: Currently we do not support @phoneticPr@ (Phonetic Properties, 18.4.3,+-- p. 1723) or @rPh@ (Phonetic Run, 18.4.6, p. 1725).+--+-- Section 18.4.8, "si (String Item)" (p. 1725)+--+-- See @CT_Rst@, p. 3903+data XlsxText = XlsxText Text+ | XlsxRichText [RichTextRun]+ deriving (Eq, Ord, Show, Generic)++instance NFData XlsxText++xlsxTextToCellValue :: XlsxText -> CellValue+xlsxTextToCellValue (XlsxText txt) = CellText txt+xlsxTextToCellValue (XlsxRichText rich) = CellRich rich++-- | A formula+--+-- See 18.18.35 "ST_Formula (Formula)" (p. 2457)+newtype Formula = Formula {unFormula :: Text}+ deriving (Eq, Ord, Show, Generic)++instance NFData Formula++-- | Cell values include text, numbers and booleans,+-- standard includes date format also but actually dates+-- are represented by numbers with a date format assigned+-- to a cell containing it+-- Specification (ECMA-376):+-- - 18.3.1.4 c (Cell)+-- - 18.18.11 ST_CellType (Cell Type)+data CellValue+ = CellText Text+ | CellDouble Double+ | CellBool Bool+ | CellRich [RichTextRun]+ | CellError ErrorType+ deriving (Eq, Ord, Show, Generic)+++instance NFData CellValue++-- | The evaluation of an expression can result in an error having one+-- of a number of error values.+--+-- See Annex L, L.2.16.8 "Error values" (p. 4764)+data ErrorType+ = ErrorDiv0+ -- ^ @#DIV/0!@ - Intended to indicate when any number, including+ -- zero, is divided by zero.+ | ErrorNA+ -- ^ @#N/A@ - Intended to indicate when a designated value is not+ -- available. For example, some functions, such as @SUMX2MY2@,+ -- perform a series of operations on corresponding elements in two+ -- arrays. If those arrays do not have the same number of elements,+ -- then for some elements in the longer array, there are no+ -- corresponding elements in the shorter one; that is, one or more+ -- values in the shorter array are not available. This error value+ -- can be produced by calling the function @NA@.+ | ErrorName+ -- ^ @#NAME?@ - Intended to indicate when what looks like a name is+ -- used, but no such name has been defined. For example, @XYZ/3@,+ -- where @XYZ@ is not a defined name. @Total is & A10@, where+ -- neither @Total@ nor @is@ is a defined name. Presumably, @"Total+ -- is " & A10@ was intended. @SUM(A1C10)@, where the range @A1:C10@+ -- was intended.+ | ErrorNull+ -- ^ @#NULL!@ - Intended to indicate when two areas are required to+ -- intersect, but do not. For example, In the case of @SUM(B1 C1)@,+ -- the space between @B1@ and @C1@ is treated as the binary+ -- intersection operator, when a comma was intended.+ | ErrorNum+ -- ^ @#NUM!@ - Intended to indicate when an argument to a function+ -- has a compatible type, but has a value that is outside the domain+ -- over which that function is defined. (This is known as a domain+ -- error.) For example, Certain calls to @ASIN@, @ATANH@, @FACT@,+ -- and @SQRT@ might result in domain errors. Intended to indicate+ -- that the result of a function cannot be represented in a value of+ -- the specified type, typically due to extreme magnitude. (This is+ -- known as a range error.) For example, @FACT(1000)@ might result+ -- in a range error.+ | ErrorRef+ -- ^ @#REF!@ - Intended to indicate when a cell reference is+ -- invalid. For example, If a formula contains a reference to a+ -- cell, and then the row or column containing that cell is deleted,+ -- a @#REF!@ error results. If a worksheet does not support 20,001+ -- columns, @OFFSET(A1,0,20000)@ results in a @#REF!@ error.+ | ErrorValue+ -- ^ @#VALUE!@ - Intended to indicate when an incompatible type+ -- argument is passed to a function, or an incompatible type operand+ -- is used with an operator. For example, In the case of a function+ -- argument, a number was expected, but text was provided. In the+ -- case of @1+"ABC"@, the binary addition operator is not defined for+ -- text.+ deriving (Eq, Ord, Show, Generic)++instance NFData ErrorType++-- | Specifies date base used for conversion of serial values to and+-- from datetime values+--+-- See Annex L, L.2.16.9.1 "Date Conversion for Serial Values" (p. 4765)+data DateBase+ = DateBase1900+ -- ^ 1900 date base system, the lower limit is January 1, -9999+ -- 00:00:00, which has serial value -4346018. The upper-limit is+ -- December 31, 9999, 23:59:59, which has serial value+ -- 2,958,465.9999884. The base date for this date base system is+ -- December 30, 1899, which has a serial value of 0.+ | DateBase1904+ -- ^ 1904 backward compatibility date-base system, the lower limit+ -- is January 1, 1904, 00:00:00, which has serial value 0. The upper+ -- limit is December 31, 9999, 23:59:59, which has serial value+ -- 2,957,003.9999884. The base date for this date base system is+ -- January 1, 1904, which has a serial value of 0.+ deriving (Eq, Show, Generic)+instance NFData DateBase++baseDate :: DateBase -> Day+baseDate DateBase1900 = fromGregorian 1899 12 30+baseDate DateBase1904 = fromGregorian 1904 1 1++-- | Converts serial value into datetime according to the specified+-- date base. Because Excel treats 1900 as a leap year even though it isn't,+-- this function converts any numbers that represent some time in /1900-02-29/+-- in Excel to `UTCTime` /1900-03-01 00:00/.+-- See https://docs.microsoft.com/en-gb/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year for details.+--+-- > show (dateFromNumber DateBase1900 42929.75) == "2017-07-13 18:00:00 UTC"+-- > show (dateFromNumber DateBase1900 60) == "1900-03-01 00:00:00 UTC"+-- > show (dateFromNumber DateBase1900 61) == "1900-03-01 00:00:00 UTC"+dateFromNumber :: forall t. RealFrac t => DateBase -> t -> UTCTime+dateFromNumber b d+ -- 60 is Excel's 2020-02-29 00:00 and 61 is Excel's 2020-03-01+ | b == DateBase1900 && d < 60 = getUTCTime (d + 1)+ | b == DateBase1900 && d >= 60 && d < 61 = getUTCTime (61 :: t)+ | otherwise = getUTCTime d+ where+ getUTCTime n =+ let+ (numberOfDays, fractionOfOneDay) = properFraction n+ day = addDays numberOfDays $ baseDate b+ diffTime = picosecondsToDiffTime (round (fractionOfOneDay * 24*60*60*1E12))+ in+ UTCTime day diffTime++-- | Converts datetime into serial value.+-- Because Excel treats 1900 as a leap year even though it isn't,+-- the numbers that represent times in /1900-02-29/ in Excel, in the range /[60, 61[/,+-- are never generated by this function for `DateBase1900`. This means that+-- under those conditions this is not an inverse of `dateFromNumber`.+-- See https://docs.microsoft.com/en-gb/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year for details.+dateToNumber :: Fractional a => DateBase -> UTCTime -> a+dateToNumber b (UTCTime day diffTime) = numberOfDays + fractionOfOneDay+ where+ numberOfDays = fromIntegral (diffDays excel1900CorrectedDay $ baseDate b)+ fractionOfOneDay = realToFrac diffTime / (24 * 60 * 60)+ marchFirst1900 = fromGregorian 1900 3 1+ excel1900CorrectedDay = if day < marchFirst1900+ then addDays (-1) day+ else day++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++-- | See @CT_Rst@, p. 3903+instance FromCursor XlsxText where+ fromCursor cur = do+ let+ ts = cur $/ element (addSmlNamespace "t") >=> contentOrEmpty+ rs = cur $/ element (addSmlNamespace "r") >=> fromCursor+ case (ts,rs) of+ ([t], []) ->+ return $ XlsxText t+ ([], _:_) ->+ return $ XlsxRichText rs+ _ ->+ fail "invalid item"++instance FromXenoNode XlsxText where+ fromXenoNode root = do+ (mCh, rs) <-+ collectChildren root $ (,) <$> maybeChild "t" <*> fromChildList "r"+ mT <- mapM contentX mCh+ case mT of+ Just t -> return $ XlsxText t+ Nothing ->+ case rs of+ [] -> Left $ "missing rich text subelements"+ _ -> return $ XlsxRichText rs++instance FromAttrVal CellRef where+ fromAttrVal = fmap (first CellRef) . fromAttrVal++instance FromAttrBs CellRef where+ -- we presume that cell references contain only latin letters,+ -- numbers and colon+ fromAttrBs = pure . CellRef . T.decodeLatin1++instance FromAttrVal SqRef where+ fromAttrVal t = do+ rs <- mapM (fmap fst . fromAttrVal) $ T.split (== ' ') t+ readSuccess $ SqRef rs++instance FromAttrBs SqRef where+ fromAttrBs bs = do+ -- split on space+ rs <- forM (BS.split 32 bs) fromAttrBs+ return $ SqRef rs++-- | See @ST_Formula@, p. 3873+instance FromCursor Formula where+ fromCursor cur = [Formula . T.concat $ cur $/ content]++instance FromXenoNode Formula where+ fromXenoNode = fmap Formula . contentX++instance FromAttrVal Formula where+ fromAttrVal t = readSuccess $ Formula t++instance FromAttrBs Formula where+ fromAttrBs = fmap Formula . fromAttrBs++instance FromAttrVal ErrorType where+ fromAttrVal "#DIV/0!" = readSuccess ErrorDiv0+ fromAttrVal "#N/A" = readSuccess ErrorNA+ fromAttrVal "#NAME?" = readSuccess ErrorName+ fromAttrVal "#NULL!" = readSuccess ErrorNull+ fromAttrVal "#NUM!" = readSuccess ErrorNum+ fromAttrVal "#REF!" = readSuccess ErrorRef+ fromAttrVal "#VALUE!" = readSuccess ErrorValue+ fromAttrVal t = invalidText "ErrorType" t++instance FromAttrBs ErrorType where+ fromAttrBs "#DIV/0!" = return ErrorDiv0+ fromAttrBs "#N/A" = return ErrorNA+ fromAttrBs "#NAME?" = return ErrorName+ fromAttrBs "#NULL!" = return ErrorNull+ fromAttrBs "#NUM!" = return ErrorNum+ fromAttrBs "#REF!" = return ErrorRef+ fromAttrBs "#VALUE!" = return ErrorValue+ fromAttrBs x = unexpectedAttrBs "ErrorType" x++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++-- | See @CT_Rst@, p. 3903+instance ToElement XlsxText where+ toElement nm si = Element {+ elementName = nm+ , elementAttributes = Map.empty+ , elementNodes = map NodeElement $+ case si of+ XlsxText text -> [elementContent "t" text]+ XlsxRichText rich -> map (toElement "r") rich+ }++instance ToAttrVal CellRef where+ toAttrVal = toAttrVal . unCellRef++-- See 18.18.76, "ST_Sqref (Reference Sequence)", p. 2488.+instance ToAttrVal SqRef where+ toAttrVal (SqRef refs) = T.intercalate " " $ map toAttrVal refs++-- | See @ST_Formula@, p. 3873+instance ToElement Formula where+ toElement nm (Formula txt) = elementContent nm txt++instance ToAttrVal ErrorType where+ toAttrVal ErrorDiv0 = "#DIV/0!"+ toAttrVal ErrorNA = "#N/A"+ toAttrVal ErrorName = "#NAME?"+ toAttrVal ErrorNull = "#NULL!"+ toAttrVal ErrorNum = "#NUM!"+ toAttrVal ErrorRef = "#REF!"+ toAttrVal ErrorValue = "#VALUE!"++-- Since micro-lens denies the existence of prisms,+-- I pasted the splice that's generated from makePrisms,+-- then I copied over the definitions from Control.Lens for the prism+-- function as well.+-- Essentially this is doing the template haskell by hand.++_CellText :: Prism' CellValue Text+_CellText+ = (prism (\ x1_a1ZQv -> CellText x1_a1ZQv))+ (\ x_a1ZQw+ -> case x_a1ZQw of+ CellText y1_a1ZQx -> Right y1_a1ZQx+ _ -> Left x_a1ZQw)+{-# INLINE _CellText #-}+_CellDouble :: Prism' CellValue Double+_CellDouble+ = (prism (\ x1_a1ZQy -> CellDouble x1_a1ZQy))+ (\ x_a1ZQz+ -> case x_a1ZQz of+ CellDouble y1_a1ZQA -> Right y1_a1ZQA+ _ -> Left x_a1ZQz)+{-# INLINE _CellDouble #-}+_CellBool :: Prism' CellValue Bool+_CellBool+ = (prism (\ x1_a1ZQB -> CellBool x1_a1ZQB))+ (\ x_a1ZQC+ -> case x_a1ZQC of+ CellBool y1_a1ZQD -> Right y1_a1ZQD+ _ -> Left x_a1ZQC)+{-# INLINE _CellBool #-}+_CellRich :: Prism' CellValue [RichTextRun]+_CellRich+ = (prism (\ x1_a1ZQE -> CellRich x1_a1ZQE))+ (\ x_a1ZQF+ -> case x_a1ZQF of+ CellRich y1_a1ZQG -> Right y1_a1ZQG+ _ -> Left x_a1ZQF)+{-# INLINE _CellRich #-}+_CellError :: Prism' CellValue ErrorType+_CellError+ = (prism (\ x1_a1ZQH -> CellError x1_a1ZQH))+ (\ x_a1ZQI+ -> case x_a1ZQI of+ CellError y1_a1ZQJ -> Right y1_a1ZQJ+ _ -> Left x_a1ZQI)+{-# INLINE _CellError #-}++_XlsxText :: Prism' XlsxText Text+_XlsxText+ = (prism (\ x1_a1ZzU -> XlsxText x1_a1ZzU))+ (\ x_a1ZzV+ -> case x_a1ZzV of+ XlsxText y1_a1ZzW -> Right y1_a1ZzW+ _ -> Left x_a1ZzV)+{-# INLINE _XlsxText #-}+_XlsxRichText :: Prism' XlsxText [RichTextRun]+_XlsxRichText+ = (prism (\ x1_a1ZzX -> XlsxRichText x1_a1ZzX))+ (\ x_a1ZzY+ -> case x_a1ZzY of+ XlsxRichText y1_a1ZzZ -> Right y1_a1ZzZ+ _ -> Left x_a1ZzY)+{-# INLINE _XlsxRichText #-}
+ src/Codec/Xlsx/Types/ConditionalFormatting.hs view
@@ -0,0 +1,973 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module Codec.Xlsx.Types.ConditionalFormatting+ ( ConditionalFormatting+ , CfRule(..)+ , NStdDev(..)+ , Inclusion(..)+ , CfValue(..)+ , MinCfValue(..)+ , MaxCfValue(..)+ , Condition(..)+ , OperatorExpression(..)+ , TimePeriod(..)+ , IconSetOptions(..)+ , IconSetType(..)+ , DataBarOptions(..)+ , dataBarWithColor+ -- * Lenses+ -- ** CfRule+ , cfrCondition+ , cfrDxfId+ , cfrPriority+ , cfrStopIfTrue+ -- ** IconSetOptions+ , isoIconSet+ , isoValues+ , isoReverse+ , isoShowValue+ -- ** DataBarOptions+ , dboMaxLength+ , dboMinLength+ , dboShowValue+ , dboMinimum+ , dboMaximum+ , dboColor+ -- * Misc+ , topCfPriority+ ) where++import Control.Arrow (first, right)+import Control.DeepSeq (NFData)+import Codec.Xlsx.LensCompat (makeLenses)+import Data.Bool (bool)+import Data.ByteString (ByteString)+import Data.Default+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor hiding (bool)+import qualified Xeno.DOM as Xeno++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.StyleSheet (Color)+import Codec.Xlsx.Writer.Internal++-- | Logical operation used in 'CellIs' condition+--+-- See 18.18.15 "ST_ConditionalFormattingOperator+-- (Conditional Format Operators)" (p. 2446)+data OperatorExpression+ = OpBeginsWith Formula -- ^ 'Begins with' operator+ | OpBetween Formula Formula -- ^ 'Between' operator+ | OpContainsText Formula -- ^ 'Contains' operator+ | OpEndsWith Formula -- ^ 'Ends with' operator+ | OpEqual Formula -- ^ 'Equal to' operator+ | OpGreaterThan Formula -- ^ 'Greater than' operator+ | OpGreaterThanOrEqual Formula -- ^ 'Greater than or equal to' operator+ | OpLessThan Formula -- ^ 'Less than' operator+ | OpLessThanOrEqual Formula -- ^ 'Less than or equal to' operator+ | OpNotBetween Formula Formula -- ^ 'Not between' operator+ | OpNotContains Formula -- ^ 'Does not contain' operator+ | OpNotEqual Formula -- ^ 'Not equal to' operator+ deriving (Eq, Ord, Show, Generic)+instance NFData OperatorExpression++-- | Used in a "contains dates" conditional formatting rule.+-- These are dynamic time periods, which change based on+-- the date the conditional formatting is refreshed / applied.+--+-- See 18.18.82 "ST_TimePeriod (Time Period Types)" (p. 2508)+data TimePeriod+ = PerLast7Days -- ^ A date in the last seven days.+ | PerLastMonth -- ^ A date occuring in the last calendar month.+ | PerLastWeek -- ^ A date occuring last week.+ | PerNextMonth -- ^ A date occuring in the next calendar month.+ | PerNextWeek -- ^ A date occuring next week.+ | PerThisMonth -- ^ A date occuring in this calendar month.+ | PerThisWeek -- ^ A date occuring this week.+ | PerToday -- ^ Today's date.+ | PerTomorrow -- ^ Tomorrow's date.+ | PerYesterday -- ^ Yesterday's date.+ deriving (Eq, Ord, Show, Generic)+instance NFData TimePeriod++-- | Flag indicating whether the 'aboveAverage' and 'belowAverage'+-- criteria is inclusive of the average itself, or exclusive of that+-- value.+data Inclusion+ = Inclusive+ | Exclusive+ deriving (Eq, Ord, Show, Generic)+instance NFData Inclusion++-- | The number of standard deviations to include above or below the+-- average in the conditional formatting rule.+newtype NStdDev =+ NStdDev Int+ deriving (Eq, Ord, Show, Generic)+instance NFData NStdDev++-- | Conditions which could be used for conditional formatting+--+-- See 18.18.12 "ST_CfType (Conditional Format Type)" (p. 2443)+data Condition+ -- | This conditional formatting rule highlights cells that are+ -- above (or maybe equal to) the average for all values in the range.+ = AboveAverage Inclusion (Maybe NStdDev)+ -- | This conditional formatting rule highlights cells in the+ -- range that begin with the given text. Equivalent to+ -- using the LEFT() sheet function and comparing values.+ | BeginsWith Text+ -- | This conditional formatting rule highlights cells that are+ -- below the average for all values in the range.+ | BelowAverage Inclusion (Maybe NStdDev)+ -- | This conditional formatting rule highlights cells whose+ -- values fall in the bottom N percent bracket.+ | BottomNPercent Int+ -- | This conditional formatting rule highlights cells whose+ -- values fall in the bottom N bracket.+ | BottomNValues Int+ -- | This conditional formatting rule compares a cell value+ -- to a formula calculated result, using an operator.+ | CellIs OperatorExpression+ -- | This conditional formatting rule creates a gradated color+ -- scale on the cells with specified colors for specified minimum+ -- and maximum.+ | ColorScale2 MinCfValue Color MaxCfValue Color+ -- | This conditional formatting rule creates a gradated color+ -- scale on the cells with specified colors for specified minimum,+ -- midpoint and maximum.+ | ColorScale3 MinCfValue Color CfValue Color MaxCfValue Color+ -- | This conditional formatting rule highlights cells that+ -- are completely blank. Equivalent of using LEN(TRIM()).+ -- This means that if the cell contains only characters+ -- that TRIM() would remove, then it is considered blank.+ -- An empty cell is also considered blank.+ | ContainsBlanks+ -- | This conditional formatting rule highlights cells with+ -- formula errors. Equivalent to using ISERROR() sheet+ -- function to determine if there is a formula error.+ | ContainsErrors+ -- | This conditional formatting rule highlights cells+ -- containing given text. Equivalent to using the SEARCH()+ -- sheet function to determine whether the cell contains+ -- the text.+ | ContainsText Text+ -- | This conditional formatting rule displays a gradated data bar+ -- in the range of cells.+ | DataBar DataBarOptions+ -- | This conditional formatting rule highlights cells+ -- without formula errors. Equivalent to using ISERROR()+ -- sheet function to determine if there is a formula error.+ | DoesNotContainErrors+ -- | This conditional formatting rule highlights cells that+ -- are not blank. Equivalent of using LEN(TRIM()). This+ -- means that if the cell contains only characters that+ -- TRIM() would remove, then it is considered blank. An+ -- empty cell is also considered blank.+ | DoesNotContainBlanks+ -- | This conditional formatting rule highlights cells that do+ -- not contain given text. Equivalent to using the+ -- SEARCH() sheet function.+ | DoesNotContainText Text+ -- | This conditional formatting rule highlights duplicated+ -- values.+ | DuplicateValues+ -- | This conditional formatting rule highlights cells ending+ -- with given text. Equivalent to using the RIGHT() sheet+ -- function and comparing values.+ | EndsWith Text+ -- | This conditional formatting rule contains a formula to+ -- evaluate. When the formula result is true, the cell is+ -- highlighted.+ | Expression Formula+ -- | This conditional formatting rule applies icons to cells+ -- according to their values.+ | IconSet IconSetOptions+ -- | This conditional formatting rule highlights cells+ -- containing dates in the specified time period. The+ -- underlying value of the cell is evaluated, therefore the+ -- cell does not need to be formatted as a date to be+ -- evaluated. For example, with a cell containing the+ -- value 38913 the conditional format shall be applied if+ -- the rule requires a value of 7/14/2006.+ | InTimePeriod TimePeriod+ -- | This conditional formatting rule highlights cells whose+ -- values fall in the top N percent bracket.+ | TopNPercent Int+ -- | This conditional formatting rule highlights cells whose+ -- values fall in the top N bracket.+ | TopNValues Int+ -- | This conditional formatting rule highlights unique values in the range.+ | UniqueValues+ deriving (Eq, Ord, Show, Generic)+instance NFData Condition++-- | Describes the values of the interpolation points in a color+-- scale, data bar or icon set conditional formatting rules.+--+-- See 18.3.1.11 "cfvo (Conditional Format Value Object)" (p. 1604)+data CfValue+ = CfValue Double+ | CfPercent Double+ | CfPercentile Double+ | CfFormula Formula+ deriving (Eq, Ord, Show, Generic)+instance NFData CfValue++data MinCfValue+ = CfvMin+ | MinCfValue CfValue+ deriving (Eq, Ord, Show, Generic)+instance NFData MinCfValue++data MaxCfValue+ = CfvMax+ | MaxCfValue CfValue+ deriving (Eq, Ord, Show, Generic)+instance NFData MaxCfValue++-- | internal type for (de)serialization+--+-- See 18.18.13 "ST_CfvoType (Conditional Format Value Object Type)" (p. 2445)+data CfvType =+ CfvtFormula+ -- ^ The minimum\/ midpoint \/ maximum value for the gradient is+ -- determined by a formula.+ | CfvtMax+ -- ^ Indicates that the maximum value in the range shall be used as+ -- the maximum value for the gradient.+ | CfvtMin+ -- ^ Indicates that the minimum value in the range shall be used as+ -- the minimum value for the gradient.+ | CfvtNum+ -- ^ Indicates that the minimum \/ midpoint \/ maximum value for the+ -- gradient is specified by a constant numeric value.+ | CfvtPercent+ -- ^ Value indicates a percentage between the minimum and maximum+ -- values in the range shall be used as the minimum \/ midpoint \/+ -- maximum value for the gradient.+ | CfvtPercentile+ -- ^ Value indicates a percentile ranking in the range shall be used+ -- as the minimum \/ midpoint \/ maximum value for the gradient.+ deriving (Eq, Ord, Show, Generic)+instance NFData CfvType++-- | Describes an icon set conditional formatting rule.+--+-- See 18.3.1.49 "iconSet (Icon Set)" (p. 1645)+data IconSetOptions = IconSetOptions+ { _isoIconSet :: IconSetType+ -- ^ icon set used, default value is 'IconSet3Trafficlights1'+ , _isoValues :: [CfValue]+ -- ^ values describing per icon ranges+ , _isoReverse :: Bool+ -- ^ reverses the default order of the icons in the specified icon set+ , _isoShowValue :: Bool+ -- ^ indicates whether to show the values of the cells on which this+ -- icon set is applied.+ } deriving (Eq, Ord, Show, Generic)+instance NFData IconSetOptions++-- | Icon set type for conditional formatting. 'CfValue' fields+-- determine lower range bounds. I.e. @IconSet3Signs (CfPercent 0)+-- (CfPercent 33) (CfPercent 67)@ say that 1st icon will be shown for+-- values ranging from 0 to 33 percents, 2nd for 33 to 67 percent and+-- the 3rd one for values from 67 to 100 percent.+--+-- 18.18.42 "ST_IconSetType (Icon Set Type)" (p. 2463)+data IconSetType =+ IconSet3Arrows -- CfValue CfValue CfValue+ | IconSet3ArrowsGray -- CfValue CfValue CfValue+ | IconSet3Flags -- CfValue CfValue CfValue+ | IconSet3Signs -- CfValue CfValue CfValue+ | IconSet3Symbols -- CfValue CfValue CfValue+ | IconSet3Symbols2 -- CfValue CfValue CfValue+ | IconSet3TrafficLights1 -- CfValue CfValue CfValue+ | IconSet3TrafficLights2 -- CfValue CfValue CfValue+ -- ^ 3 traffic lights icon set with thick black border.+ | IconSet4Arrows -- CfValue CfValue CfValue CfValue+ | IconSet4ArrowsGray -- CfValue CfValue CfValue CfValue+ | IconSet4Rating -- CfValue CfValue CfValue CfValue+ | IconSet4RedToBlack -- CfValue CfValue CfValue CfValue+ | IconSet4TrafficLights -- CfValue CfValue CfValue CfValue+ | IconSet5Arrows -- CfValue CfValue CfValue CfValue CfValue+ | IconSet5ArrowsGray -- CfValue CfValue CfValue CfValue CfValue+ | IconSet5Quarters -- CfValue CfValue CfValue CfValue CfValue+ | IconSet5Rating -- CfValue CfValue CfValue CfValue CfValue+ deriving (Eq, Ord, Show, Generic)+instance NFData IconSetType++-- | Describes a data bar conditional formatting rule.+--+-- See 18.3.1.28 "dataBar (Data Bar)" (p. 1621)+data DataBarOptions = DataBarOptions+ { _dboMaxLength :: Int+ -- ^ The maximum length of the data bar, as a percentage of the cell+ -- width.+ , _dboMinLength :: Int+ -- ^ The minimum length of the data bar, as a percentage of the cell+ -- width.+ , _dboShowValue :: Bool+ -- ^ Indicates whether to show the values of the cells on which this+ -- data bar is applied.+ , _dboMinimum :: MinCfValue+ , _dboMaximum :: MaxCfValue+ , _dboColor :: Color+ } deriving (Eq, Ord, Show, Generic)+instance NFData DataBarOptions++defaultDboMaxLength :: Int+defaultDboMaxLength = 90++defaultDboMinLength :: Int+defaultDboMinLength = 10++dataBarWithColor :: Color -> Condition+dataBarWithColor c =+ DataBar+ DataBarOptions+ { _dboMaxLength = defaultDboMaxLength+ , _dboMinLength = defaultDboMinLength+ , _dboShowValue = True+ , _dboMinimum = CfvMin+ , _dboMaximum = CfvMax+ , _dboColor = c+ }++-- | This collection represents a description of a conditional formatting rule.+--+-- See 18.3.1.10 "cfRule (Conditional Formatting Rule)" (p. 1602)+data CfRule = CfRule+ { _cfrCondition :: Condition+ -- | This is an index to a dxf element in the Styles Part+ -- indicating which cell formatting to+ -- apply when the conditional formatting rule criteria is met.+ , _cfrDxfId :: Maybe Int+ -- | The priority of this conditional formatting rule. This value+ -- is used to determine which format should be evaluated and+ -- rendered. Lower numeric values are higher priority than+ -- higher numeric values, where 1 is the highest priority.+ , _cfrPriority :: Int+ -- | If this flag is set, no rules with lower priority shall+ -- be applied over this rule, when this rule+ -- evaluates to true.+ , _cfrStopIfTrue :: Maybe Bool+ } deriving (Eq, Ord, Show, Generic)+instance NFData CfRule++instance Default IconSetOptions where+ def =+ IconSetOptions+ { _isoIconSet = IconSet3TrafficLights1+ , _isoValues = [CfPercent 0, CfPercent 33.33, CfPercent 66.67]+-- IconSet3TrafficLights1 (CfPercent 0) (CfPercent 33.33) (CfPercent 66.67)+ , _isoReverse = False+ , _isoShowValue = True+ }++makeLenses ''CfRule+makeLenses ''IconSetOptions+makeLenses ''DataBarOptions++type ConditionalFormatting = [CfRule]++topCfPriority :: Int+topCfPriority = 1++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor CfRule where+ fromCursor cur = do+ _cfrDxfId <- maybeAttribute "dxfId" cur+ _cfrPriority <- fromAttribute "priority" cur+ _cfrStopIfTrue <- maybeAttribute "stopIfTrue" cur+ -- spec shows this attribute as optional but it's not clear why could+ -- conditional formatting record be needed with no condition type set+ cfType <- fromAttribute "type" cur+ _cfrCondition <- readCondition cfType cur+ return CfRule{..}++readCondition :: Text -> Cursor -> [Condition]+readCondition "aboveAverage" cur = do+ above <- fromAttributeDef "aboveAverage" True cur+ inclusion <- fromAttributeDef "equalAverage" Exclusive cur+ nStdDev <- maybeAttribute "stdDev" cur+ if above+ then return $ AboveAverage inclusion nStdDev+ else return $ BelowAverage inclusion nStdDev+readCondition "beginsWith" cur = do+ txt <- fromAttribute "text" cur+ return $ BeginsWith txt+readCondition "colorScale" cur = do+ let cfvos = cur $/ element (addSmlNamespace "colorScale") &/ element (addSmlNamespace "cfvo") &| node+ colors = cur $/ element (addSmlNamespace "colorScale") &/ element (addSmlNamespace "color") &| node+ case (cfvos, colors) of+ ([n1, n2], [cn1, cn2]) -> do+ mincfv <- fromCursor $ fromNode n1+ minc <- fromCursor $ fromNode cn1+ maxcfv <- fromCursor $ fromNode n2+ maxc <- fromCursor $ fromNode cn2+ return $ ColorScale2 mincfv minc maxcfv maxc+ ([n1, n2, n3], [cn1, cn2, cn3]) -> do+ mincfv <- fromCursor $ fromNode n1+ minc <- fromCursor $ fromNode cn1+ midcfv <- fromCursor $ fromNode n2+ midc <- fromCursor $ fromNode cn2+ maxcfv <- fromCursor $ fromNode n3+ maxc <- fromCursor $ fromNode cn3+ return $ ColorScale3 mincfv minc midcfv midc maxcfv maxc+ _ ->+ error "Malformed colorScale condition"+readCondition "cellIs" cur = do+ operator <- fromAttribute "operator" cur+ let formulas = cur $/ element (addSmlNamespace "formula") >=> fromCursor+ expr <- readOpExpression operator formulas+ return $ CellIs expr+readCondition "containsBlanks" _ = return ContainsBlanks+readCondition "containsErrors" _ = return ContainsErrors+readCondition "containsText" cur = do+ txt <- fromAttribute "text" cur+ return $ ContainsText txt+readCondition "dataBar" cur = fmap DataBar $ cur $/ element (addSmlNamespace "dataBar") >=> fromCursor+readCondition "duplicateValues" _ = return DuplicateValues+readCondition "endsWith" cur = do+ txt <- fromAttribute "text" cur+ return $ EndsWith txt+readCondition "expression" cur = do+ formula <- cur $/ element (addSmlNamespace "formula") >=> fromCursor+ return $ Expression formula+readCondition "iconSet" cur = fmap IconSet $ cur $/ element (addSmlNamespace "iconSet") >=> fromCursor+readCondition "notContainsBlanks" _ = return DoesNotContainBlanks+readCondition "notContainsErrors" _ = return DoesNotContainErrors+readCondition "notContainsText" cur = do+ txt <- fromAttribute "text" cur+ return $ DoesNotContainText txt+readCondition "timePeriod" cur = do+ period <- fromAttribute "timePeriod" cur+ return $ InTimePeriod period+readCondition "top10" cur = do+ bottom <- fromAttributeDef "bottom" False cur+ percent <- fromAttributeDef "percent" False cur+ rank <- fromAttribute "rank" cur+ case (bottom, percent) of+ (True, True) -> return $ BottomNPercent rank+ (True, False) -> return $ BottomNValues rank+ (False, True) -> return $ TopNPercent rank+ (False, False) -> return $ TopNValues rank+readCondition "uniqueValues" _ = return UniqueValues+readCondition t _ = error $ "Unexpected conditional formatting type " ++ show t++readOpExpression :: Text -> [Formula] -> [OperatorExpression]+readOpExpression "beginsWith" [f] = [OpBeginsWith f ]+readOpExpression "between" [f1, f2] = [OpBetween f1 f2]+readOpExpression "containsText" [f] = [OpContainsText f]+readOpExpression "endsWith" [f] = [OpEndsWith f]+readOpExpression "equal" [f] = [OpEqual f]+readOpExpression "greaterThan" [f] = [OpGreaterThan f]+readOpExpression "greaterThanOrEqual" [f] = [OpGreaterThanOrEqual f]+readOpExpression "lessThan" [f] = [OpLessThan f]+readOpExpression "lessThanOrEqual" [f] = [OpLessThanOrEqual f]+readOpExpression "notBetween" [f1, f2] = [OpNotBetween f1 f2]+readOpExpression "notContains" [f] = [OpNotContains f]+readOpExpression "notEqual" [f] = [OpNotEqual f]+readOpExpression _ _ = []++instance FromXenoNode CfRule where+ fromXenoNode root = parseAttributes root $ do+ _cfrDxfId <- maybeAttr "dxfId"+ _cfrPriority <- fromAttr "priority"+ _cfrStopIfTrue <- maybeAttr "stopIfTrue"+ -- spec shows this attribute as optional but it's not clear why could+ -- conditional formatting record be needed with no condition type set+ cfType <- fromAttr "type"+ _cfrCondition <- readConditionX cfType+ return CfRule {..}+ where+ readConditionX ("aboveAverage" :: ByteString) = do+ above <- fromAttrDef "aboveAverage" True+ inclusion <- fromAttrDef "equalAverage" Exclusive+ nStdDev <- maybeAttr "stdDev"+ if above+ then return $ AboveAverage inclusion nStdDev+ else return $ BelowAverage inclusion nStdDev+ readConditionX "beginsWith" = BeginsWith <$> fromAttr "text"+ readConditionX "colorScale" = toAttrParser $ do+ xs <- collectChildren root . maybeParse "colorScale" $ \node ->+ collectChildren node $ (,) <$> childList "cfvo"+ <*> childList "color"+ case xs of+ Just ([n1, n2], [cn1, cn2]) -> do+ mincfv <- fromXenoNode n1+ minc <- fromXenoNode cn1+ maxcfv <- fromXenoNode n2+ maxc <- fromXenoNode cn2+ return $ ColorScale2 mincfv minc maxcfv maxc+ Just ([n1, n2, n3], [cn1, cn2, cn3]) -> do+ mincfv <- fromXenoNode n1+ minc <- fromXenoNode cn1+ midcfv <- fromXenoNode n2+ midc <- fromXenoNode cn2+ maxcfv <- fromXenoNode n3+ maxc <- fromXenoNode cn3+ return $ ColorScale3 mincfv minc midcfv midc maxcfv maxc+ _ ->+ Left "Malformed colorScale condition"+ readConditionX "cellIs" = do+ operator <- fromAttr "operator"+ formulas <- toAttrParser . collectChildren root $ fromChildList "formula"+ case (operator, formulas) of+ ("beginsWith" :: ByteString, [f]) -> return . CellIs $ OpBeginsWith f+ ("between", [f1, f2]) -> return . CellIs $ OpBetween f1 f2+ ("containsText", [f]) -> return . CellIs $ OpContainsText f+ ("endsWith", [f]) -> return . CellIs $ OpEndsWith f+ ("equal", [f]) -> return . CellIs $ OpEqual f+ ("greaterThan", [f]) -> return . CellIs $ OpGreaterThan f+ ("greaterThanOrEqual", [f]) -> return . CellIs $ OpGreaterThanOrEqual f+ ("lessThan", [f]) -> return . CellIs $ OpLessThan f+ ("lessThanOrEqual", [f]) -> return . CellIs $ OpLessThanOrEqual f+ ("notBetween", [f1, f2]) -> return . CellIs $ OpNotBetween f1 f2+ ("notContains", [f]) -> return . CellIs $ OpNotContains f+ ("notEqual", [f]) -> return . CellIs $ OpNotEqual f+ _ -> toAttrParser $ Left "Bad cellIs rule"+ readConditionX "containsBlanks" = return ContainsBlanks+ readConditionX "containsErrors" = return ContainsErrors+ readConditionX "containsText" = ContainsText <$> fromAttr "text"+ readConditionX "dataBar" =+ fmap DataBar . toAttrParser . collectChildren root $ fromChild "dataBar"+ readConditionX "duplicateValues" = return DuplicateValues+ readConditionX "endsWith" = EndsWith <$> fromAttr "text"+ readConditionX "expression" =+ fmap Expression . toAttrParser . collectChildren root $ fromChild "formula"+ readConditionX "iconSet" =+ fmap IconSet . toAttrParser . collectChildren root $ fromChild "iconSet"+ readConditionX "notContainsBlanks" = return DoesNotContainBlanks+ readConditionX "notContainsErrors" = return DoesNotContainErrors+ readConditionX "notContainsText" =+ DoesNotContainText <$> fromAttr "text"+ readConditionX "timePeriod" = InTimePeriod <$> fromAttr "timePeriod"+ readConditionX "top10" = do+ bottom <- fromAttrDef "bottom" False+ percent <- fromAttrDef "percent" False+ rank <- fromAttr "rank"+ case (bottom, percent) of+ (True, True) -> return $ BottomNPercent rank+ (True, False) -> return $ BottomNValues rank+ (False, True) -> return $ TopNPercent rank+ (False, False) -> return $ TopNValues rank+ readConditionX "uniqueValues" = return UniqueValues+ readConditionX x =+ toAttrParser . Left $ "Unexpected conditional formatting type " <> T.pack (show x)++instance FromAttrVal TimePeriod where+ fromAttrVal "last7Days" = readSuccess PerLast7Days+ fromAttrVal "lastMonth" = readSuccess PerLastMonth+ fromAttrVal "lastWeek" = readSuccess PerLastWeek+ fromAttrVal "nextMonth" = readSuccess PerNextMonth+ fromAttrVal "nextWeek" = readSuccess PerNextWeek+ fromAttrVal "thisMonth" = readSuccess PerThisMonth+ fromAttrVal "thisWeek" = readSuccess PerThisWeek+ fromAttrVal "today" = readSuccess PerToday+ fromAttrVal "tomorrow" = readSuccess PerTomorrow+ fromAttrVal "yesterday" = readSuccess PerYesterday+ fromAttrVal t = invalidText "TimePeriod" t++instance FromAttrBs TimePeriod where+ fromAttrBs "last7Days" = return PerLast7Days+ fromAttrBs "lastMonth" = return PerLastMonth+ fromAttrBs "lastWeek" = return PerLastWeek+ fromAttrBs "nextMonth" = return PerNextMonth+ fromAttrBs "nextWeek" = return PerNextWeek+ fromAttrBs "thisMonth" = return PerThisMonth+ fromAttrBs "thisWeek" = return PerThisWeek+ fromAttrBs "today" = return PerToday+ fromAttrBs "tomorrow" = return PerTomorrow+ fromAttrBs "yesterday" = return PerYesterday+ fromAttrBs x = unexpectedAttrBs "TimePeriod" x++instance FromAttrVal CfvType where+ fromAttrVal "num" = readSuccess CfvtNum+ fromAttrVal "percent" = readSuccess CfvtPercent+ fromAttrVal "max" = readSuccess CfvtMax+ fromAttrVal "min" = readSuccess CfvtMin+ fromAttrVal "formula" = readSuccess CfvtFormula+ fromAttrVal "percentile" = readSuccess CfvtPercentile+ fromAttrVal t = invalidText "CfvType" t++instance FromAttrBs CfvType where+ fromAttrBs "num" = return CfvtNum+ fromAttrBs "percent" = return CfvtPercent+ fromAttrBs "max" = return CfvtMax+ fromAttrBs "min" = return CfvtMin+ fromAttrBs "formula" = return CfvtFormula+ fromAttrBs "percentile" = return CfvtPercentile+ fromAttrBs x = unexpectedAttrBs "CfvType" x++readCfValue :: (CfValue -> a) -> [a] -> [a] -> Cursor -> [a]+readCfValue f minVal maxVal c = do+ vType <- fromAttribute "type" c+ case vType of+ CfvtNum -> do+ v <- fromAttribute "val" c+ return . f $ CfValue v+ CfvtFormula -> do+ v <- fromAttribute "val" c+ return . f $ CfFormula v+ CfvtPercent -> do+ v <- fromAttribute "val" c+ return . f $ CfPercent v+ CfvtPercentile -> do+ v <- fromAttribute "val" c+ return . f $ CfPercentile v+ CfvtMin -> minVal+ CfvtMax -> maxVal++readCfValueX ::+ (CfValue -> a)+ -> Either Text a+ -> Either Text a+ -> Xeno.Node+ -> Either Text a+readCfValueX f minVal maxVal root =+ parseAttributes root $ do+ vType <- fromAttr "type"+ case vType of+ CfvtNum -> do+ v <- fromAttr "val"+ return . f $ CfValue v+ CfvtFormula -> do+ v <- fromAttr "val"+ return . f $ CfFormula v+ CfvtPercent -> do+ v <- fromAttr "val"+ return . f $ CfPercent v+ CfvtPercentile -> do+ v <- fromAttr "val"+ return . f $ CfPercentile v+ CfvtMin -> toAttrParser minVal+ CfvtMax -> toAttrParser maxVal++failMinCfvType :: [a]+failMinCfvType = fail "unexpected 'min' type"++failMinCfvTypeX :: Either Text a+failMinCfvTypeX = Left "unexpected 'min' type"++failMaxCfvType :: [a]+failMaxCfvType = fail "unexpected 'max' type"++failMaxCfvTypeX :: Either Text a+failMaxCfvTypeX = Left "unexpected 'max' type"++instance FromCursor CfValue where+ fromCursor = readCfValue id failMinCfvType failMaxCfvType++instance FromXenoNode CfValue where+ fromXenoNode root = readCfValueX id failMinCfvTypeX failMaxCfvTypeX root++instance FromCursor MinCfValue where+ fromCursor = readCfValue MinCfValue (return CfvMin) failMaxCfvType++instance FromXenoNode MinCfValue where+ fromXenoNode root =+ readCfValueX MinCfValue (return CfvMin) failMaxCfvTypeX root++instance FromCursor MaxCfValue where+ fromCursor = readCfValue MaxCfValue failMinCfvType (return CfvMax)++instance FromXenoNode MaxCfValue where+ fromXenoNode root =+ readCfValueX MaxCfValue failMinCfvTypeX (return CfvMax) root++defaultIconSet :: IconSetType+defaultIconSet = IconSet3TrafficLights1++instance FromCursor IconSetOptions where+ fromCursor cur = do+ _isoIconSet <- fromAttributeDef "iconSet" defaultIconSet cur+ let _isoValues = cur $/ element (addSmlNamespace "cfvo") >=> fromCursor+ _isoReverse <- fromAttributeDef "reverse" False cur+ _isoShowValue <- fromAttributeDef "showValue" True cur+ return IconSetOptions {..}++instance FromXenoNode IconSetOptions where+ fromXenoNode root = do+ (_isoIconSet, _isoReverse, _isoShowValue) <-+ parseAttributes root $ (,,) <$> fromAttrDef "iconSet" defaultIconSet+ <*> fromAttrDef "reverse" False+ <*> fromAttrDef "showValue" True+ _isoValues <- collectChildren root $ fromChildList "cfvo"+ return IconSetOptions {..}++instance FromAttrVal IconSetType where+ fromAttrVal "3Arrows" = readSuccess IconSet3Arrows+ fromAttrVal "3ArrowsGray" = readSuccess IconSet3ArrowsGray+ fromAttrVal "3Flags" = readSuccess IconSet3Flags+ fromAttrVal "3Signs" = readSuccess IconSet3Signs+ fromAttrVal "3Symbols" = readSuccess IconSet3Symbols+ fromAttrVal "3Symbols2" = readSuccess IconSet3Symbols2+ fromAttrVal "3TrafficLights1" = readSuccess IconSet3TrafficLights1+ fromAttrVal "3TrafficLights2" = readSuccess IconSet3TrafficLights2+ fromAttrVal "4Arrows" = readSuccess IconSet4Arrows+ fromAttrVal "4ArrowsGray" = readSuccess IconSet4ArrowsGray+ fromAttrVal "4Rating" = readSuccess IconSet4Rating+ fromAttrVal "4RedToBlack" = readSuccess IconSet4RedToBlack+ fromAttrVal "4TrafficLights" = readSuccess IconSet4TrafficLights+ fromAttrVal "5Arrows" = readSuccess IconSet5Arrows+ fromAttrVal "5ArrowsGray" = readSuccess IconSet5ArrowsGray+ fromAttrVal "5Quarters" = readSuccess IconSet5Quarters+ fromAttrVal "5Rating" = readSuccess IconSet5Rating+ fromAttrVal t = invalidText "IconSetType" t++instance FromAttrBs IconSetType where+ fromAttrBs "3Arrows" = return IconSet3Arrows+ fromAttrBs "3ArrowsGray" = return IconSet3ArrowsGray+ fromAttrBs "3Flags" = return IconSet3Flags+ fromAttrBs "3Signs" = return IconSet3Signs+ fromAttrBs "3Symbols" = return IconSet3Symbols+ fromAttrBs "3Symbols2" = return IconSet3Symbols2+ fromAttrBs "3TrafficLights1" = return IconSet3TrafficLights1+ fromAttrBs "3TrafficLights2" = return IconSet3TrafficLights2+ fromAttrBs "4Arrows" = return IconSet4Arrows+ fromAttrBs "4ArrowsGray" = return IconSet4ArrowsGray+ fromAttrBs "4Rating" = return IconSet4Rating+ fromAttrBs "4RedToBlack" = return IconSet4RedToBlack+ fromAttrBs "4TrafficLights" = return IconSet4TrafficLights+ fromAttrBs "5Arrows" = return IconSet5Arrows+ fromAttrBs "5ArrowsGray" = return IconSet5ArrowsGray+ fromAttrBs "5Quarters" = return IconSet5Quarters+ fromAttrBs "5Rating" = return IconSet5Rating+ fromAttrBs x = unexpectedAttrBs "IconSetType" x++instance FromCursor DataBarOptions where+ fromCursor cur = do+ _dboMaxLength <- fromAttributeDef "maxLength" defaultDboMaxLength cur+ _dboMinLength <- fromAttributeDef "minLength" defaultDboMinLength cur+ _dboShowValue <- fromAttributeDef "showValue" True cur+ let cfvos = cur $/ element (addSmlNamespace "cfvo") &| node+ case cfvos of+ [nMin, nMax] -> do+ _dboMinimum <- fromCursor (fromNode nMin)+ _dboMaximum <- fromCursor (fromNode nMax)+ _dboColor <- cur $/ element (addSmlNamespace "color") >=> fromCursor+ return DataBarOptions{..}+ ns -> do+ fail $ "expected minimum and maximum cfvo nodes but see instead " +++ show (length ns) ++ " cfvo nodes"++instance FromXenoNode DataBarOptions where+ fromXenoNode root = do+ (_dboMaxLength, _dboMinLength, _dboShowValue) <-+ parseAttributes root $ (,,) <$> fromAttrDef "maxLength" defaultDboMaxLength+ <*> fromAttrDef "minLength" defaultDboMinLength+ <*> fromAttrDef "showValue" True+ (_dboMinimum, _dboMaximum, _dboColor) <-+ collectChildren root $ (,,) <$> fromChild "cfvo"+ <*> fromChild "cfvo"+ <*> fromChild "color"+ return DataBarOptions{..}++instance FromAttrVal Inclusion where+ fromAttrVal = right (first $ bool Exclusive Inclusive) . fromAttrVal++instance FromAttrBs Inclusion where+ fromAttrBs = fmap (bool Exclusive Inclusive) . fromAttrBs++instance FromAttrVal NStdDev where+ fromAttrVal = right (first NStdDev) . fromAttrVal++instance FromAttrBs NStdDev where+ fromAttrBs = fmap NStdDev . fromAttrBs++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement CfRule where+ toElement nm CfRule{..} =+ let (condType, condAttrs, condNodes) = conditionData _cfrCondition+ baseAttrs = M.fromList . catMaybes $+ [ Just $ "type" .= condType+ , "dxfId" .=? _cfrDxfId+ , Just $ "priority" .= _cfrPriority+ , "stopIfTrue" .=? _cfrStopIfTrue+ ]+ in Element+ { elementName = nm+ , elementAttributes = M.union baseAttrs condAttrs+ , elementNodes = condNodes+ }++conditionData :: Condition -> (Text, Map Name Text, [Node])+conditionData (AboveAverage i sDevs) =+ ("aboveAverage", M.fromList $ ["aboveAverage" .= True] +++ catMaybes [ "equalAverage" .=? justNonDef Exclusive i+ , "stdDev" .=? sDevs], [])+conditionData (BeginsWith t) = ("beginsWith", M.fromList [ "text" .= t], [])+conditionData (BelowAverage i sDevs) =+ ("aboveAverage", M.fromList $ ["aboveAverage" .= False] +++ catMaybes [ "equalAverage" .=? justNonDef Exclusive i+ , "stdDev" .=? sDevs], [])+conditionData (BottomNPercent n) = ("top10", M.fromList [ "bottom" .= True, "rank" .= n, "percent" .= True ], [])+conditionData (BottomNValues n) = ("top10", M.fromList [ "bottom" .= True, "rank" .= n ], [])+conditionData (CellIs opExpr) = ("cellIs", M.fromList [ "operator" .= op], formulas)+ where (op, formulas) = operatorExpressionData opExpr+conditionData (ColorScale2 minv minc maxv maxc) =+ ( "colorScale"+ , M.empty+ , [ NodeElement $+ elementListSimple+ "colorScale"+ [ toElement "cfvo" minv+ , toElement "cfvo" maxv+ , toElement "color" minc+ , toElement "color" maxc+ ]+ ])+conditionData (ColorScale3 minv minc midv midc maxv maxc) =+ ( "colorScale"+ , M.empty+ , [ NodeElement $+ elementListSimple+ "colorScale"+ [ toElement "cfvo" minv+ , toElement "cfvo" midv+ , toElement "cfvo" maxv+ , toElement "color" minc+ , toElement "color" midc+ , toElement "color" maxc+ ]+ ])+conditionData ContainsBlanks = ("containsBlanks", M.empty, [])+conditionData ContainsErrors = ("containsErrors", M.empty, [])+conditionData (ContainsText t) = ("containsText", M.fromList [ "text" .= t], [])+conditionData (DataBar dbOpts) = ("dataBar", M.empty, [toNode "dataBar" dbOpts])+conditionData DoesNotContainBlanks = ("notContainsBlanks", M.empty, [])+conditionData DoesNotContainErrors = ("notContainsErrors", M.empty, [])+conditionData (DoesNotContainText t) = ("notContainsText", M.fromList [ "text" .= t], [])+conditionData DuplicateValues = ("duplicateValues", M.empty, [])+conditionData (EndsWith t) = ("endsWith", M.fromList [ "text" .= t], [])+conditionData (Expression formula) = ("expression", M.empty, [formulaNode formula])+conditionData (InTimePeriod period) = ("timePeriod", M.fromList [ "timePeriod" .= period ], [])+conditionData (IconSet isOptions) = ("iconSet", M.empty, [toNode "iconSet" isOptions])+conditionData (TopNPercent n) = ("top10", M.fromList [ "rank" .= n, "percent" .= True ], [])+conditionData (TopNValues n) = ("top10", M.fromList [ "rank" .= n ], [])+conditionData UniqueValues = ("uniqueValues", M.empty, [])++operatorExpressionData :: OperatorExpression -> (Text, [Node])+operatorExpressionData (OpBeginsWith f) = ("beginsWith", [formulaNode f])+operatorExpressionData (OpBetween f1 f2) = ("between", [formulaNode f1, formulaNode f2])+operatorExpressionData (OpContainsText f) = ("containsText", [formulaNode f])+operatorExpressionData (OpEndsWith f) = ("endsWith", [formulaNode f])+operatorExpressionData (OpEqual f) = ("equal", [formulaNode f])+operatorExpressionData (OpGreaterThan f) = ("greaterThan", [formulaNode f])+operatorExpressionData (OpGreaterThanOrEqual f) = ("greaterThanOrEqual", [formulaNode f])+operatorExpressionData (OpLessThan f) = ("lessThan", [formulaNode f])+operatorExpressionData (OpLessThanOrEqual f) = ("lessThanOrEqual", [formulaNode f])+operatorExpressionData (OpNotBetween f1 f2) = ("notBetween", [formulaNode f1, formulaNode f2])+operatorExpressionData (OpNotContains f) = ("notContains", [formulaNode f])+operatorExpressionData (OpNotEqual f) = ("notEqual", [formulaNode f])++instance ToElement MinCfValue where+ toElement nm CfvMin = leafElement nm ["type" .= CfvtMin]+ toElement nm (MinCfValue cfv) = toElement nm cfv++instance ToElement MaxCfValue where+ toElement nm CfvMax = leafElement nm ["type" .= CfvtMax]+ toElement nm (MaxCfValue cfv) = toElement nm cfv++instance ToElement CfValue where+ toElement nm (CfValue v) = leafElement nm ["type" .= CfvtNum, "val" .= v]+ toElement nm (CfPercent v) =+ leafElement nm ["type" .= CfvtPercent, "val" .= v]+ toElement nm (CfPercentile v) =+ leafElement nm ["type" .= CfvtPercentile, "val" .= v]+ toElement nm (CfFormula f) =+ leafElement nm ["type" .= CfvtFormula, "val" .= unFormula f]++instance ToAttrVal CfvType where+ toAttrVal CfvtNum = "num"+ toAttrVal CfvtPercent = "percent"+ toAttrVal CfvtMax = "max"+ toAttrVal CfvtMin = "min"+ toAttrVal CfvtFormula = "formula"+ toAttrVal CfvtPercentile = "percentile"++instance ToElement IconSetOptions where+ toElement nm IconSetOptions {..} =+ elementList nm attrs $ map (toElement "cfvo") _isoValues+ where+ attrs = catMaybes+ [ "iconSet" .=? justNonDef defaultIconSet _isoIconSet+ , "reverse" .=? justTrue _isoReverse+ , "showValue" .=? justFalse _isoShowValue+ ]++instance ToAttrVal IconSetType where+ toAttrVal IconSet3Arrows = "3Arrows"+ toAttrVal IconSet3ArrowsGray = "3ArrowsGray"+ toAttrVal IconSet3Flags = "3Flags"+ toAttrVal IconSet3Signs = "3Signs"+ toAttrVal IconSet3Symbols = "3Symbols"+ toAttrVal IconSet3Symbols2 = "3Symbols2"+ toAttrVal IconSet3TrafficLights1 = "3TrafficLights1"+ toAttrVal IconSet3TrafficLights2 = "3TrafficLights2"+ toAttrVal IconSet4Arrows = "4Arrows"+ toAttrVal IconSet4ArrowsGray = "4ArrowsGray"+ toAttrVal IconSet4Rating = "4Rating"+ toAttrVal IconSet4RedToBlack = "4RedToBlack"+ toAttrVal IconSet4TrafficLights = "4TrafficLights"+ toAttrVal IconSet5Arrows = "5Arrows"+ toAttrVal IconSet5ArrowsGray = "5ArrowsGray"+ toAttrVal IconSet5Quarters = "5Quarters"+ toAttrVal IconSet5Rating = "5Rating"++instance ToElement DataBarOptions where+ toElement nm DataBarOptions {..} = elementList nm attrs elements+ where+ attrs = catMaybes+ [ "maxLength" .=? justNonDef defaultDboMaxLength _dboMaxLength+ , "minLength" .=? justNonDef defaultDboMinLength _dboMinLength+ , "showValue" .=? justFalse _dboShowValue+ ]+ elements =+ [ toElement "cfvo" _dboMinimum+ , toElement "cfvo" _dboMaximum+ , toElement "color" _dboColor+ ]++toNode :: ToElement a => Name -> a -> Node+toNode nm = NodeElement . toElement nm++formulaNode :: Formula -> Node+formulaNode = toNode "formula"++instance ToAttrVal TimePeriod where+ toAttrVal PerLast7Days = "last7Days"+ toAttrVal PerLastMonth = "lastMonth"+ toAttrVal PerLastWeek = "lastWeek"+ toAttrVal PerNextMonth = "nextMonth"+ toAttrVal PerNextWeek = "nextWeek"+ toAttrVal PerThisMonth = "thisMonth"+ toAttrVal PerThisWeek = "thisWeek"+ toAttrVal PerToday = "today"+ toAttrVal PerTomorrow = "tomorrow"+ toAttrVal PerYesterday = "yesterday"++instance ToAttrVal Inclusion where+ toAttrVal = toAttrVal . (== Inclusive)++instance ToAttrVal NStdDev where+ toAttrVal (NStdDev n) = toAttrVal n
+ src/Codec/Xlsx/Types/DataValidation.hs view
@@ -0,0 +1,353 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+module Codec.Xlsx.Types.DataValidation+ ( ValidationExpression(..)+ , ValidationType(..)+ , dvAllowBlank+ , dvError+ , dvErrorStyle+ , dvErrorTitle+ , dvPrompt+ , dvPromptTitle+ , dvShowDropDown+ , dvShowErrorMessage+ , dvShowInputMessage+ , dvValidationType+ , ErrorStyle(..)+ , DataValidation(..)+ , ListOrRangeExpression(..)+ , ValidationList+ , maybePlainValidationList+ , maybeValidationRange+ , readValidationType+ , readListFormulas+ , readOpExpression2+ , readValidationTypeOpExp+ , readValExpression+ , viewValidationExpression+ ) where++import Control.Applicative ((<|>))+import Control.DeepSeq (NFData)+import Codec.Xlsx.LensCompat (makeLenses)+import Control.Monad ((>=>), guard)+import Data.ByteString (ByteString)+import Data.Char (isSpace)+import Data.Default+import qualified Data.Map as M+import Data.Maybe (catMaybes, maybeToList)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Text.XML (Element(..), Node(..))+import Text.XML.Cursor (Cursor, ($/), element)++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Writer.Internal++-- See 18.18.20 "ST_DataValidationOperator (Data Validation Operator)" (p. 2439/2449)+data ValidationExpression+ = ValBetween Formula Formula -- ^ "Between" operator+ | ValEqual Formula -- ^ "Equal to" operator+ | ValGreaterThan Formula -- ^ "Greater than" operator+ | ValGreaterThanOrEqual Formula -- ^ "Greater than or equal to" operator+ | ValLessThan Formula -- ^ "Less than" operator+ | ValLessThanOrEqual Formula -- ^ "Less than or equal to" operator+ | ValNotBetween Formula Formula -- ^ "Not between" operator+ | ValNotEqual Formula -- ^ "Not equal to" operator+ deriving (Eq, Show, Generic)+instance NFData ValidationExpression++-- See 18.18.21 "ST_DataValidationType (Data Validation Type)" (p. 2440/2450)+data ValidationType+ = ValidationTypeNone+ | ValidationTypeCustom Formula+ | ValidationTypeDate ValidationExpression+ | ValidationTypeDecimal ValidationExpression+ | ValidationTypeList ListOrRangeExpression+ | ValidationTypeTextLength ValidationExpression+ | ValidationTypeTime ValidationExpression+ | ValidationTypeWhole ValidationExpression+ deriving (Eq, Show, Generic)+instance NFData ValidationType++type ValidationList = [Text]++data ListOrRangeExpression+ = ListExpression ValidationList -- ^ a plain list of elements+ | RangeExpression Range -- ^ a cell or range reference+ deriving (Eq, Show, Generic)+instance NFData ListOrRangeExpression++-- See 18.18.18 "ST_DataValidationErrorStyle (Data Validation Error Styles)" (p. 2438/2448)+data ErrorStyle+ = ErrorStyleInformation+ | ErrorStyleStop+ | ErrorStyleWarning+ deriving (Eq, Show, Generic)+instance NFData ErrorStyle++-- See 18.3.1.32 "dataValidation (Data Validation)" (p. 1614/1624)+data DataValidation = DataValidation+ { _dvAllowBlank :: Bool+ , _dvError :: Maybe Text+ , _dvErrorStyle :: ErrorStyle+ , _dvErrorTitle :: Maybe Text+ , _dvPrompt :: Maybe Text+ , _dvPromptTitle :: Maybe Text+ , _dvShowDropDown :: Bool+ -- ^ A boolean value indicating whether to display a dropdown combo box for a list type data validation.+ -- Note that it has an inverted logic, false shows the dropdown list and true will render the dropdown as+ -- plain text instead.+ , _dvShowErrorMessage :: Bool+ , _dvShowInputMessage :: Bool+ , _dvValidationType :: ValidationType+ } deriving (Eq, Show, Generic)+instance NFData DataValidation++makeLenses ''DataValidation++instance Default DataValidation where+ def = DataValidation+ False Nothing ErrorStyleStop Nothing Nothing Nothing False False False ValidationTypeNone++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromAttrVal ErrorStyle where+ fromAttrVal "information" = readSuccess ErrorStyleInformation+ fromAttrVal "stop" = readSuccess ErrorStyleStop+ fromAttrVal "warning" = readSuccess ErrorStyleWarning+ fromAttrVal t = invalidText "ErrorStyle" t++instance FromAttrBs ErrorStyle where+ fromAttrBs "information" = return ErrorStyleInformation+ fromAttrBs "stop" = return ErrorStyleStop+ fromAttrBs "warning" = return ErrorStyleWarning+ fromAttrBs x = unexpectedAttrBs "ErrorStyle" x++instance FromCursor DataValidation where+ fromCursor cur = do+ _dvAllowBlank <- fromAttributeDef "allowBlank" False cur+ _dvError <- maybeAttribute "error" cur+ _dvErrorStyle <- fromAttributeDef "errorStyle" ErrorStyleStop cur+ _dvErrorTitle <- maybeAttribute "errorTitle" cur+ mop <- fromAttributeDef "operator" "between" cur+ _dvPrompt <- maybeAttribute "prompt" cur+ _dvPromptTitle <- maybeAttribute "promptTitle" cur+ _dvShowDropDown <- fromAttributeDef "showDropDown" False cur+ _dvShowErrorMessage <- fromAttributeDef "showErrorMessage" False cur+ _dvShowInputMessage <- fromAttributeDef "showInputMessage" False cur+ mtype <- fromAttributeDef "type" "none" cur+ _dvValidationType <- readValidationType mop mtype cur+ return DataValidation{..}++instance FromXenoNode DataValidation where+ fromXenoNode root = do+ (op, atype, genDV) <- parseAttributes root $ do+ _dvAllowBlank <- fromAttrDef "allowBlank" False+ _dvError <- maybeAttr "error"+ _dvErrorStyle <- fromAttrDef "errorStyle" ErrorStyleStop+ _dvErrorTitle <- maybeAttr "errorTitle"+ _dvPrompt <- maybeAttr "prompt"+ _dvPromptTitle <- maybeAttr "promptTitle"+ _dvShowDropDown <- fromAttrDef "showDropDown" False+ _dvShowErrorMessage <- fromAttrDef "showErrorMessage" False+ _dvShowInputMessage <- fromAttrDef "showInputMessage" False+ op <- fromAttrDef "operator" "between"+ typ <- fromAttrDef "type" "none"+ return (op, typ, \_dvValidationType -> DataValidation {..})+ valType <- parseValidationType op atype+ return $ genDV valType+ where+ parseValidationType :: ByteString -> ByteString -> Either Text ValidationType+ parseValidationType op atype =+ case atype of+ "none" -> return ValidationTypeNone+ "custom" ->+ ValidationTypeCustom <$> formula1+ "list" -> do+ f <- formula1+ case readListFormulas f of+ Nothing -> Left "validation of type \"list\" with empty formula list"+ Just fs -> return $ ValidationTypeList fs+ "date" ->+ ValidationTypeDate <$> readOpExpression op+ "decimal" ->+ ValidationTypeDecimal <$> readOpExpression op+ "textLength" ->+ ValidationTypeTextLength <$> readOpExpression op+ "time" ->+ ValidationTypeTime <$> readOpExpression op+ "whole" ->+ ValidationTypeWhole <$> readOpExpression op+ unexpected ->+ Left $ "unexpected type of data validation " <> T.pack (show unexpected)+ readOpExpression "between" = uncurry ValBetween <$> formulaPair+ readOpExpression "notBetween" = uncurry ValNotBetween <$> formulaPair+ readOpExpression "equal" = ValEqual <$> formula1+ readOpExpression "greaterThan" = ValGreaterThan <$> formula1+ readOpExpression "greaterThanOrEqual" = ValGreaterThanOrEqual <$> formula1+ readOpExpression "lessThan" = ValLessThan <$> formula1+ readOpExpression "lessThanOrEqual" = ValLessThanOrEqual <$> formula1+ readOpExpression "notEqual" = ValNotEqual <$> formula1+ readOpExpression op = Left $ "data validation, unexpected operator " <> T.pack (show op)+ formula1 = collectChildren root $ fromChild "formula1"+ formulaPair =+ collectChildren root $ (,) <$> fromChild "formula1" <*> fromChild "formula2"++readValidationType :: Text -> Text -> Cursor -> [ValidationType]+readValidationType _ "none" _ = return ValidationTypeNone+readValidationType _ "custom" cur = do+ f <- fromCursor cur+ return $ ValidationTypeCustom f+readValidationType _ "list" cur = do+ f <- cur $/ element (addSmlNamespace "formula1") >=> fromCursor+ as <- maybeToList $ readListFormulas f+ return $ ValidationTypeList as+readValidationType op ty cur = do+ opExp <- readOpExpression2 op cur+ readValidationTypeOpExp ty opExp++-- | Attempt to obtain a plain list expression+maybePlainValidationList :: ValidationType -> Maybe ValidationList+maybePlainValidationList (ValidationTypeList (ListExpression le)) = Just le+maybePlainValidationList _ = Nothing++-- | Attempt to obtain a range expression+maybeValidationRange :: ValidationType -> Maybe Range+maybeValidationRange (ValidationTypeList (RangeExpression re)) = Just re+maybeValidationRange _ = Nothing++readListFormulas :: Formula -> Maybe ListOrRangeExpression+readListFormulas (Formula f) = readQuotedList f <|> readUnquotedCellRange f+ where+ readQuotedList t+ | Just t' <- T.stripPrefix "\"" (T.dropAround isSpace t)+ , Just t'' <- T.stripSuffix "\"" t'+ = Just . ListExpression $ map (T.dropAround isSpace) $ T.splitOn "," t''+ | otherwise = Nothing+ readUnquotedCellRange t =+ -- a CellRef expression of a range (this is not validated beyond the absence of quotes)+ -- note that the foreign sheet name can be 'single-quoted'+ let stripped = T.dropAround isSpace t+ in RangeExpression (CellRef stripped) <$ guard (not (T.null stripped))+ -- This parser expects a comma-separated list surrounded by quotation marks.+ -- Spaces around the quotation marks and commas are removed, but inner spaces+ -- are kept.+ --+ -- The parser seems to be consistent with how Excel treats list formulas, but+ -- I wasn't able to find a specification of the format.+ --+ -- Addendum: <dataValidation type="list" ...> undescriminately designates an actual list or a cell range.+ -- For a cell range validation, instead of a quoted list, it's an unquoted CellRef-like contents of the form:+ -- ActualSheetName!$C$2:$C$18++readOpExpression2 :: Text -> Cursor -> [ValidationExpression]+readOpExpression2 op cur+ | op `elem` ["between", "notBetween"] = do+ f1 <- cur $/ element (addSmlNamespace "formula1") >=> fromCursor+ f2 <- cur $/ element (addSmlNamespace "formula2") >=> fromCursor+ readValExpression op [f1,f2]+readOpExpression2 op cur = do+ f <- cur $/ element (addSmlNamespace "formula1") >=> fromCursor+ readValExpression op [f]++readValidationTypeOpExp :: Text -> ValidationExpression -> [ValidationType]+readValidationTypeOpExp "date" oe = [ValidationTypeDate oe]+readValidationTypeOpExp "decimal" oe = [ValidationTypeDecimal oe]+readValidationTypeOpExp "textLength" oe = [ValidationTypeTextLength oe]+readValidationTypeOpExp "time" oe = [ValidationTypeTime oe]+readValidationTypeOpExp "whole" oe = [ValidationTypeWhole oe]+readValidationTypeOpExp _ _ = []++readValExpression :: Text -> [Formula] -> [ValidationExpression]+readValExpression "between" [f1, f2] = [ValBetween f1 f2]+readValExpression "equal" [f] = [ValEqual f]+readValExpression "greaterThan" [f] = [ValGreaterThan f]+readValExpression "greaterThanOrEqual" [f] = [ValGreaterThanOrEqual f]+readValExpression "lessThan" [f] = [ValLessThan f]+readValExpression "lessThanOrEqual" [f] = [ValLessThanOrEqual f]+readValExpression "notBetween" [f1, f2] = [ValNotBetween f1 f2]+readValExpression "notEqual" [f] = [ValNotEqual f]+readValExpression _ _ = []++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToAttrVal ValidationType where+ toAttrVal ValidationTypeNone = "none"+ toAttrVal (ValidationTypeCustom _) = "custom"+ toAttrVal (ValidationTypeDate _) = "date"+ toAttrVal (ValidationTypeDecimal _) = "decimal"+ toAttrVal (ValidationTypeList _) = "list"+ toAttrVal (ValidationTypeTextLength _) = "textLength"+ toAttrVal (ValidationTypeTime _) = "time"+ toAttrVal (ValidationTypeWhole _) = "whole"++instance ToAttrVal ErrorStyle where+ toAttrVal ErrorStyleInformation = "information"+ toAttrVal ErrorStyleStop = "stop"+ toAttrVal ErrorStyleWarning = "warning"++instance ToElement DataValidation where+ toElement nm DataValidation{..} = Element+ { elementName = nm+ , elementAttributes = M.fromList . catMaybes $+ [ Just $ "allowBlank" .= _dvAllowBlank+ , "error" .=? _dvError+ , Just $ "errorStyle" .= _dvErrorStyle+ , "errorTitle" .=? _dvErrorTitle+ , "operator" .=? op+ , "prompt" .=? _dvPrompt+ , "promptTitle" .=? _dvPromptTitle+ , Just $ "showDropDown" .= _dvShowDropDown+ , Just $ "showErrorMessage" .= _dvShowErrorMessage+ , Just $ "showInputMessage" .= _dvShowInputMessage+ , Just $ "type" .= _dvValidationType+ ]+ , elementNodes = catMaybes+ [ fmap (NodeElement . toElement "formula1") f1+ , fmap (NodeElement . toElement "formula2") f2+ ]+ }+ where+ opExp (o,f1',f2') = (Just o, Just f1', f2')++ op :: Maybe Text+ f1,f2 :: Maybe Formula+ (op,f1,f2) = case _dvValidationType of+ ValidationTypeNone -> (Nothing, Nothing, Nothing)+ ValidationTypeCustom f -> (Nothing, Just f, Nothing)+ ValidationTypeDate f -> opExp $ viewValidationExpression f+ ValidationTypeDecimal f -> opExp $ viewValidationExpression f+ ValidationTypeTextLength f -> opExp $ viewValidationExpression f+ ValidationTypeTime f -> opExp $ viewValidationExpression f+ ValidationTypeWhole f -> opExp $ viewValidationExpression f+ ValidationTypeList as ->+ let renderPlainList l =+ let csvFy = T.intercalate ","+ reQuote x = '"' `T.cons` x `T.snoc` '"'+ in reQuote (csvFy l)+ f = Formula $+ case as of+ RangeExpression re -> unCellRef re+ ListExpression le -> renderPlainList le+ in (Nothing, Just f, Nothing)++viewValidationExpression :: ValidationExpression -> (Text, Formula, Maybe Formula)+viewValidationExpression (ValBetween f1 f2) = ("between", f1, Just f2)+viewValidationExpression (ValEqual f) = ("equal", f, Nothing)+viewValidationExpression (ValGreaterThan f) = ("greaterThan", f, Nothing)+viewValidationExpression (ValGreaterThanOrEqual f) = ("greaterThanOrEqual", f, Nothing)+viewValidationExpression (ValLessThan f) = ("lessThan", f, Nothing)+viewValidationExpression (ValLessThanOrEqual f) = ("lessThanOrEqual", f, Nothing)+viewValidationExpression (ValNotBetween f1 f2) = ("notBetween", f1, Just f2)+viewValidationExpression (ValNotEqual f) = ("notEqual", f, Nothing)
+ src/Codec/Xlsx/Types/Drawing.hs view
@@ -0,0 +1,500 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Drawing where++import Control.Arrow (first)+import Control.DeepSeq (NFData)+import Codec.Xlsx.LensCompat (makeLenses)+import Data.ByteString.Lazy (ByteString)+import Data.Default+import qualified Data.Map as M+import Data.Maybe (catMaybes, listToMaybe, mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Drawing.Chart+import Codec.Xlsx.Types.Drawing.Common+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.Relationships+import Codec.Xlsx.Writer.Internal++-- | information about image file as a par of a drawing+data FileInfo = FileInfo+ { _fiFilename :: FilePath+ -- ^ image filename, images are assumed to be stored under path "xl\/media\/"+ , _fiContentType :: Text+ -- ^ image content type, ECMA-376 advises to use "image\/png" or "image\/jpeg"+ -- if interoperability is wanted+ , _fiContents :: ByteString+ -- ^ image file contents+ } deriving (Eq, Show, Generic)+instance NFData FileInfo++data Marker = Marker+ { _mrkCol :: Int+ , _mrkColOff :: Coordinate+ , _mrkRow :: Int+ , _mrkRowOff :: Coordinate+ } deriving (Eq, Show, Generic)+instance NFData Marker++unqMarker :: (Int, Int) -> (Int, Int) -> Marker+unqMarker (col, colOff) (row, rowOff) =+ Marker col (UnqCoordinate colOff) row (UnqCoordinate rowOff)++data EditAs+ = EditAsTwoCell+ | EditAsOneCell+ | EditAsAbsolute+ deriving (Eq, Show, Generic)+instance NFData EditAs++data Anchoring+ = AbsoluteAnchor+ { absaPos :: Point2D+ , absaExt :: PositiveSize2D+ }+ | OneCellAnchor+ { onecaFrom :: Marker+ , onecaExt :: PositiveSize2D+ }+ | TwoCellAnchor+ { tcaFrom :: Marker+ , tcaTo :: Marker+ , tcaEditAs :: EditAs+ }+ deriving (Eq, Show, Generic)+instance NFData Anchoring++data DrawingObject p g+ = Picture { _picMacro :: Maybe Text+ , _picPublished :: Bool+ , _picNonVisual :: PicNonVisual+ , _picBlipFill :: BlipFillProperties p+ , _picShapeProperties :: ShapeProperties+ -- TODO: style+ }+ | Graphic { _grNonVisual :: GraphNonVisual+ , _grChartSpace :: g+ , _grTransform :: Transform2D}+ -- TODO: sp, grpSp, graphicFrame, cxnSp, contentPart+ deriving (Eq, Show, Generic)+instance (NFData p, NFData g) => NFData (DrawingObject p g)++-- | basic function to create picture drawing object+--+-- /Note:/ specification says that drawing element ids need to be+-- unique within 1 document, otherwise /...document shall be+-- considered non-conformant/.+picture :: DrawingElementId -> FileInfo -> DrawingObject FileInfo c+picture dId fi =+ Picture+ { _picMacro = Nothing+ , _picPublished = False+ , _picNonVisual = nonVis+ , _picBlipFill = bfProps+ , _picShapeProperties = shProps+ }+ where+ nonVis =+ PicNonVisual $+ NonVisualDrawingProperties+ { _nvdpId = dId+ , _nvdpName = T.pack $ _fiFilename fi+ , _nvdpDescription = Nothing+ , _nvdpHidden = False+ , _nvdpTitle = Nothing+ }+ bfProps =+ BlipFillProperties+ {_bfpImageInfo = Just fi, _bfpFillMode = Just FillStretch}+ shProps =+ ShapeProperties+ { _spXfrm = Nothing+ , _spGeometry = Nothing+ , _spFill = Just NoFill+ , _spOutline = Just $ def {_lnFill = Just NoFill}+ }++-- | helper to retrive information about all picture files in+-- particular drawing alongside with their anchorings (i.e. sizes and+-- positions)+extractPictures :: Drawing -> [(Anchoring, FileInfo)]+extractPictures dr = mapMaybe maybePictureInfo $ _xdrAnchors dr+ where+ maybePictureInfo Anchor {..} =+ case _anchObject of+ Picture {..} -> (_anchAnchoring,) <$> _bfpImageInfo _picBlipFill+ _ -> Nothing++-- | This element is used to set certain properties related to a drawing+-- element on the client spreadsheet application.+--+-- see 20.5.2.3 "clientData (Client Data)" (p. 3156)+data ClientData = ClientData+ { _cldLcksWithSheet :: Bool+ -- ^ This attribute indicates whether to disable selection on+ -- drawing elements when the sheet is protected.+ , _cldPrintsWithSheet :: Bool+ -- ^ This attribute indicates whether to print drawing elements+ -- when printing the sheet.+ } deriving (Eq, Show, Generic)+instance NFData ClientData++data PicNonVisual = PicNonVisual+ { _pnvDrawingProps :: NonVisualDrawingProperties+ -- TODO: cNvPicPr+ } deriving (Eq, Show, Generic)+instance NFData PicNonVisual++data GraphNonVisual = GraphNonVisual+ { _gnvDrawingProps :: NonVisualDrawingProperties+ -- TODO cNvGraphicFramePr+ } deriving (Eq, Show, Generic)+instance NFData GraphNonVisual++newtype DrawingElementId = DrawingElementId+ { unDrawingElementId :: Int+ } deriving (Eq, Show, Generic)+instance NFData DrawingElementId++-- see 20.1.2.2.8 "cNvPr (Non-Visual Drawing Properties)" (p. 2731)+data NonVisualDrawingProperties = NonVisualDrawingProperties+ { _nvdpId :: DrawingElementId+ -- ^ Specifies a unique identifier for the current+ -- DrawingML object within the current+ --+ -- TODO: make ids internal and consistent by construction+ , _nvdpName :: Text+ -- ^ Specifies the name of the object.+ -- Typically, this is used to store the original file+ -- name of a picture object.+ , _nvdpDescription :: Maybe Text+ -- ^ Alternative Text for Object+ , _nvdpHidden :: Bool+ , _nvdpTitle :: Maybe Text+ } deriving (Eq, Show, Generic)+instance NFData NonVisualDrawingProperties++data BlipFillProperties a = BlipFillProperties+ { _bfpImageInfo :: Maybe a+ , _bfpFillMode :: Maybe FillMode+ -- TODO: dpi, rotWithShape, srcRect+ } deriving (Eq, Show, Generic)+instance NFData a => NFData (BlipFillProperties a)++-- see @a_EG_FillModeProperties@ (p. 4319)+data FillMode+ -- See 20.1.8.58 "tile (Tile)" (p. 2880)+ = FillTile -- TODO: tx, ty, sx, sy, flip, algn+ -- See 20.1.8.56 "stretch (Stretch)" (p. 2879)+ | FillStretch -- TODO: srcRect+ deriving (Eq, Show, Generic)+instance NFData FillMode++-- See @EG_Anchor@ (p. 4052)+data Anchor p g = Anchor+ { _anchAnchoring :: Anchoring+ , _anchObject :: DrawingObject p g+ , _anchClientData :: ClientData+ } deriving (Eq, Show, Generic)+instance (NFData p, NFData g) => NFData (Anchor p g)++data GenericDrawing p g = Drawing+ { _xdrAnchors :: [Anchor p g]+ } deriving (Eq, Show, Generic)+instance (NFData p, NFData g) => NFData (GenericDrawing p g)++-- See 20.5.2.35 "wsDr (Worksheet Drawing)" (p. 3176)+type Drawing = GenericDrawing FileInfo ChartSpace++type UnresolvedDrawing = GenericDrawing RefId RefId++makeLenses ''Anchor+makeLenses ''DrawingObject+makeLenses ''BlipFillProperties+makeLenses ''GenericDrawing++-- | simple drawing object anchoring using one cell as a top lelft+-- corner and dimensions of that object+simpleAnchorXY :: (Int, Int) -- ^ x+y coordinates of a cell used as+ -- top left anchoring corner+ -> PositiveSize2D -- ^ size of drawing object to be+ -- anchored+ -> DrawingObject p g+ -> Anchor p g+simpleAnchorXY (x, y) sz obj =+ Anchor+ { _anchAnchoring =+ OneCellAnchor {onecaFrom = unqMarker (x, 0) (y, 0), onecaExt = sz}+ , _anchObject = obj+ , _anchClientData = def+ }++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default ClientData where+ def = ClientData True True++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor UnresolvedDrawing where+ fromCursor cur = [Drawing $ cur $/ anyElement >=> fromCursor]++instance FromCursor (Anchor RefId RefId) where+ fromCursor cur = do+ _anchAnchoring <- fromCursor cur+ _anchObject <- cur $/ anyElement >=> fromCursor+ _anchClientData <- cur $/ element (xdr"clientData") >=> fromCursor+ return Anchor{..}++instance FromCursor Anchoring where+ fromCursor = anchoringFromNode . node++anchoringFromNode :: Node -> [Anchoring]+anchoringFromNode n | n `nodeElNameIs` xdr "twoCellAnchor" = do+ tcaEditAs <- fromAttributeDef "editAs" EditAsTwoCell cur+ tcaFrom <- cur $/ element (xdr"from") >=> fromCursor+ tcaTo <- cur $/ element (xdr"to") >=> fromCursor+ return TwoCellAnchor{..}+ | n `nodeElNameIs` xdr "oneCellAnchor" = do+ onecaFrom <- cur $/ element (xdr"from") >=> fromCursor+ onecaExt <- cur $/ element (xdr"ext") >=> fromCursor+ return OneCellAnchor{..}+ | n `nodeElNameIs` xdr "absolueAnchor" = do+ absaPos <- cur $/ element (xdr"pos") >=> fromCursor+ absaExt <- cur $/ element (xdr"ext") >=> fromCursor+ return AbsoluteAnchor{..}+ | otherwise = fail "no matching anchoring node"+ where+ cur = fromNode n++instance FromCursor Marker where+ fromCursor cur = do+ _mrkCol <- cur $/ element (xdr"col") &/ content >=> decimal+ _mrkColOff <- cur $/ element (xdr"colOff") &/ content >=> coordinate+ _mrkRow <- cur $/ element (xdr"row") &/ content >=> decimal+ _mrkRowOff <- cur $/ element (xdr"rowOff") &/ content >=> coordinate+ return Marker{..}++instance FromCursor (DrawingObject RefId RefId) where+ fromCursor = drawingObjectFromNode . node++drawingObjectFromNode :: Node -> [DrawingObject RefId RefId]+drawingObjectFromNode n+ | n `nodeElNameIs` xdr "pic" = do+ _picMacro <- maybeAttribute "macro" cur+ _picPublished <- fromAttributeDef "fPublished" False cur+ _picNonVisual <- cur $/ element (xdr "nvPicPr") >=> fromCursor+ _picBlipFill <- cur $/ element (xdr "blipFill") >=> fromCursor+ _picShapeProperties <- cur $/ element (xdr "spPr") >=> fromCursor+ return Picture {..}+ | n `nodeElNameIs` xdr "graphicFrame" = do+ _grNonVisual <-+ cur $/ element (xdr "nvGraphicFramePr") >=> fromCursor+ _grTransform <- cur $/ element (xdr "xfrm") >=> fromCursor+ _grChartSpace <-+ cur $/ element (a_ "graphic") &/ element (a_ "graphicData") &/+ element (c_ "chart") >=> fmap RefId . attribute (odr "id")+ return Graphic {..}+ | otherwise = fail "no matching drawing object node"+ where+ cur = fromNode n++instance FromCursor PicNonVisual where+ fromCursor cur = do+ _pnvDrawingProps <- cur $/ element (xdr"cNvPr") >=> fromCursor+ return PicNonVisual{..}++instance FromCursor GraphNonVisual where+ fromCursor cur = do+ _gnvDrawingProps <- cur $/ element (xdr "cNvPr") >=> fromCursor+ return GraphNonVisual {..}++instance FromCursor NonVisualDrawingProperties where+ fromCursor cur = do+ _nvdpId <- fromAttribute "id" cur+ _nvdpName <- fromAttribute "name" cur+ _nvdpDescription <- maybeAttribute "descr" cur+ _nvdpHidden <- fromAttributeDef "hidden" False cur+ _nvdpTitle <- maybeAttribute "title" cur+ return NonVisualDrawingProperties{..}++instance FromAttrVal DrawingElementId where+ fromAttrVal = fmap (first DrawingElementId) . fromAttrVal++instance FromCursor (BlipFillProperties RefId) where+ fromCursor cur = do+ let _bfpImageInfo = listToMaybe $ cur $/ element (a_ "blip") >=>+ fmap RefId . attribute (odr"embed")+ _bfpFillMode = listToMaybe $ cur $/ anyElement >=> fromCursor+ return BlipFillProperties{..}++instance FromCursor FillMode where+ fromCursor = fillModeFromNode . node++fillModeFromNode :: Node -> [FillMode]+fillModeFromNode n | n `nodeElNameIs` a_ "stretch" = return FillStretch+ | n `nodeElNameIs` a_ "stretch" = return FillTile+ | otherwise = fail "no matching fill mode node"++-- see 20.5.3.2 "ST_EditAs (Resizing Behaviors)" (p. 3177)+instance FromAttrVal EditAs where+ fromAttrVal "absolute" = readSuccess EditAsAbsolute+ fromAttrVal "oneCell" = readSuccess EditAsOneCell+ fromAttrVal "twoCell" = readSuccess EditAsTwoCell+ fromAttrVal t = invalidText "EditAs" t++instance FromCursor ClientData where+ fromCursor cur = do+ _cldLcksWithSheet <- fromAttributeDef "fLocksWithSheet" True cur+ _cldPrintsWithSheet <- fromAttributeDef "fPrintsWithSheet" True cur+ return ClientData{..}++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToDocument UnresolvedDrawing where+ toDocument = documentFromNsElement "Drawing generated by xlsx" xlDrawingNs+ . toElement "wsDr"++instance ToElement UnresolvedDrawing where+ toElement nm (Drawing anchors) = Element+ { elementName = nm+ , elementAttributes = M.empty+ , elementNodes = map NodeElement $+ map anchorToElement anchors+ }++anchorToElement :: Anchor RefId RefId -> Element+anchorToElement Anchor{..} = el+ { elementNodes = elementNodes el +++ map NodeElement [ drawingObjEl, cdEl ] }+ where+ el = anchoringToElement _anchAnchoring+ drawingObjEl = drawingObjToElement _anchObject+ cdEl = toElement "clientData" _anchClientData++anchoringToElement :: Anchoring -> Element+anchoringToElement anchoring = elementList nm attrs elements+ where+ (nm, attrs, elements) = case anchoring of+ AbsoluteAnchor{..} ->+ ("absoluteAnchor", [],+ [ toElement "pos" absaPos, toElement "ext" absaExt ])+ OneCellAnchor{..} ->+ ("oneCellAnchor", [],+ [ toElement "from" onecaFrom, toElement "ext" onecaExt ])+ TwoCellAnchor{..} ->+ ("twoCellAnchor", [ "editAs" .= tcaEditAs ],+ [ toElement "from" tcaFrom, toElement "to" tcaTo ])++instance ToElement Marker where+ toElement nm Marker{..} = elementListSimple nm elements+ where+ elements = [ elementContent "col" (toAttrVal _mrkCol)+ , elementContent "colOff" (toAttrVal _mrkColOff)+ , elementContent "row" (toAttrVal _mrkRow)+ , elementContent "rowOff" (toAttrVal _mrkRowOff)]++drawingObjToElement :: DrawingObject RefId RefId -> Element+drawingObjToElement Picture {..} = elementList "pic" attrs elements+ where+ attrs =+ catMaybes ["macro" .=? _picMacro, "fPublished" .=? justTrue _picPublished]+ elements =+ [ toElement "nvPicPr" _picNonVisual+ , toElement "blipFill" _picBlipFill+ , toElement "spPr" _picShapeProperties+ ]+drawingObjToElement Graphic {..} = elementListSimple "graphicFrame" elements+ where+ elements =+ [ toElement "nvGraphicFramePr" _grNonVisual+ , toElement "xfrm" _grTransform+ , graphicEl+ ]+ graphicEl =+ elementListSimple+ (a_ "graphic")+ [ elementList+ (a_ "graphicData")+ ["uri" .= chartNs]+ [leafElement (c_ "chart") [odr "id" .= _grChartSpace]]+ ]++instance ToElement PicNonVisual where+ toElement nm PicNonVisual {..} =+ elementListSimple+ nm+ [toElement "cNvPr" _pnvDrawingProps, emptyElement "cNvPicPr"]++instance ToElement GraphNonVisual where+ toElement nm GraphNonVisual {..} =+ elementListSimple+ nm+ [toElement "cNvPr" _gnvDrawingProps, emptyElement "cNvGraphicFramePr"]++instance ToElement NonVisualDrawingProperties where+ toElement nm NonVisualDrawingProperties{..} =+ leafElement nm attrs+ where+ attrs = [ "id" .= _nvdpId+ , "name" .= _nvdpName ] +++ catMaybes [ "descr" .=? _nvdpDescription+ , "hidden" .=? justTrue _nvdpHidden+ , "title" .=? _nvdpTitle ]++instance ToAttrVal DrawingElementId where+ toAttrVal = toAttrVal . unDrawingElementId++instance ToElement (BlipFillProperties RefId) where+ toElement nm BlipFillProperties{..} =+ elementListSimple nm elements+ where+ elements = catMaybes [ (\rId -> leafElement (a_ "blip") [ odr "embed" .= rId ]) <$> _bfpImageInfo+ , fillModeToElement <$> _bfpFillMode ]++fillModeToElement :: FillMode -> Element+fillModeToElement FillStretch = emptyElement (a_ "stretch")+fillModeToElement FillTile = emptyElement (a_ "stretch")++instance ToElement ClientData where+ toElement nm ClientData{..} = leafElement nm attrs+ where+ attrs = catMaybes [ "fLocksWithSheet" .=? justFalse _cldLcksWithSheet+ , "fPrintsWithSheet" .=? justFalse _cldPrintsWithSheet+ ]++instance ToAttrVal EditAs where+ toAttrVal EditAsAbsolute = "absolute"+ toAttrVal EditAsOneCell = "oneCell"+ toAttrVal EditAsTwoCell = "twoCell"++-- | Add Spreadsheet DrawingML namespace to name+xdr :: Text -> Name+xdr x = Name+ { nameLocalName = x+ , nameNamespace = Just xlDrawingNs+ , namePrefix = Nothing+ }++xlDrawingNs :: Text+xlDrawingNs = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
+ src/Codec/Xlsx/Types/Drawing/Chart.hs view
@@ -0,0 +1,828 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Drawing.Chart where++import GHC.Generics (Generic)++import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Default+import Data.Maybe (catMaybes, listToMaybe, maybeToList)+import Data.Text (Text)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.Drawing.Common+import Codec.Xlsx.Writer.Internal++-- | Main Chart holder, combines+-- TODO: title, autoTitleDeleted, pivotFmts+-- view3D, floor, sideWall, backWall, showDLblsOverMax, extLst+data ChartSpace = ChartSpace+ { _chspTitle :: Maybe ChartTitle+ , _chspCharts :: [Chart]+ , _chspLegend :: Maybe Legend+ , _chspPlotVisOnly :: Maybe Bool+ , _chspDispBlanksAs :: Maybe DispBlanksAs+ } deriving (Eq, Show, Generic)+instance NFData ChartSpace++-- | Chart title+--+-- TODO: layout, overlay, spPr, txPr, extLst+newtype ChartTitle =+ ChartTitle (Maybe TextBody)+ deriving (Eq, Show, Generic)+instance NFData ChartTitle++-- | This simple type specifies the possible ways to display blanks.+--+-- See 21.2.3.10 "ST_DispBlanksAs (Display Blanks As)" (p. 3444)+data DispBlanksAs+ = DispBlanksAsGap+ -- ^ Specifies that blank values shall be left as a gap.+ | DispBlanksAsSpan+ -- ^ Specifies that blank values shall be spanned with a line.+ | DispBlanksAsZero+ -- ^ Specifies that blank values shall be treated as zero.+ deriving (Eq, Show, Generic)+instance NFData DispBlanksAs++-- TODO: legendEntry, layout, overlay, spPr, txPr, extLst+data Legend = Legend+ { _legendPos :: Maybe LegendPos+ , _legendOverlay :: Maybe Bool+ } deriving (Eq, Show, Generic)+instance NFData Legend++-- See 21.2.3.24 "ST_LegendPos (Legend Position)" (p. 3449)+data LegendPos+ = LegendBottom+ -- ^ b (Bottom) Specifies that the legend shall be drawn at the+ -- bottom of the chart.+ | LegendLeft+ -- ^ l (Left) Specifies that the legend shall be drawn at the left+ -- of the chart.+ | LegendRight+ -- ^ r (Right) Specifies that the legend shall be drawn at the+ -- right of the chart.+ | LegendTop+ -- ^ t (Top) Specifies that the legend shall be drawn at the top+ -- of the chart.+ | LegendTopRight+ -- ^ tr (Top Right) Specifies that the legend shall be drawn at+ -- the top right of the chart.+ deriving (Eq, Show, Generic)+instance NFData LegendPos++-- | Specific Chart+-- TODO:+-- area3DChart, line3DChart, stockChart, radarChart,+-- pie3DChart, doughnutChart, bar3DChart, ofPieChart,+-- surfaceChart, surface3DChart, bubbleChart+data Chart+ = LineChart { _lnchGrouping :: ChartGrouping+ , _lnchSeries :: [LineSeries]+ , _lnchMarker :: Maybe Bool+ -- ^ specifies that the marker shall be shown+ , _lnchSmooth :: Maybe Bool+ -- ^ specifies the line connecting the points on the chart shall be+ -- smoothed using Catmull-Rom splines+ }+ | AreaChart { _archGrouping :: Maybe ChartGrouping+ , _archSeries :: [AreaSeries]+ }+ | BarChart { _brchDirection :: BarDirection+ , _brchGrouping :: Maybe BarChartGrouping+ , _brchSeries :: [BarSeries]+ }+ | PieChart { _pichSeries :: [PieSeries]+ }+ | ScatterChart { _scchStyle :: ScatterStyle+ , _scchSeries :: [ScatterSeries]+ }+ deriving (Eq, Show, Generic)+instance NFData Chart++-- | Possible groupings for a chart+--+-- See 21.2.3.17 "ST_Grouping (Grouping)" (p. 3446)+data ChartGrouping+ = PercentStackedGrouping+ -- ^ (100% Stacked) Specifies that the chart series are drawn next to each+ -- other along the value axis and scaled to total 100%.+ | StackedGrouping+ -- ^ (Stacked) Specifies that the chart series are drawn next to each+ -- other on the value axis.+ | StandardGrouping+ -- ^(Standard) Specifies that the chart series are drawn on the value+ -- axis.+ deriving (Eq, Show, Generic)+instance NFData ChartGrouping++-- | Possible groupings for a bar chart+--+-- See 21.2.3.4 "ST_BarGrouping (Bar Grouping)" (p. 3441)+data BarChartGrouping+ = BarClusteredGrouping+ -- ^ Specifies that the chart series are drawn next to each other+ -- along the category axis.+ | BarPercentStackedGrouping+ -- ^ (100% Stacked) Specifies that the chart series are drawn next to each+ -- other along the value axis and scaled to total 100%.+ | BarStackedGrouping+ -- ^ (Stacked) Specifies that the chart series are drawn next to each+ -- other on the value axis.+ | BarStandardGrouping+ -- ^(Standard) Specifies that the chart series are drawn on the value+ -- axis.+ deriving (Eq, Show, Generic)+instance NFData BarChartGrouping++-- | Possible directions for a bar chart+--+-- See 21.2.3.3 "ST_BarDir (Bar Direction)" (p. 3441)+data BarDirection+ = DirectionBar+ | DirectionColumn+ deriving (Eq, Show, Generic)+instance NFData BarDirection++-- | Possible styles of scatter chart+--+-- /Note:/ It appears that even for 'ScatterMarker' style Exel draws a+-- line between chart points if otline fill for '_scserShared' isn't+-- set to so it's not quite clear how could Excel use this property+--+-- See 21.2.3.40 "ST_ScatterStyle (Scatter Style)" (p. 3455)+data ScatterStyle+ = ScatterNone+ | ScatterLine+ | ScatterLineMarker+ | ScatterMarker+ | ScatterSmooth+ | ScatterSmoothMarker+ deriving (Eq, Show, Generic)+instance NFData ScatterStyle++-- | Single data point options+--+-- TODO: invertIfNegative, bubble3D, explosion, pictureOptions, extLst+--+-- See 21.2.2.52 "dPt (Data Point)" (p. 3384)+data DataPoint = DataPoint+ { _dpMarker :: Maybe DataMarker+ , _dpShapeProperties :: Maybe ShapeProperties+ } deriving (Eq, Show, Generic)+instance NFData DataPoint++-- | Specifies common series options+-- TODO: spPr+--+-- See @EG_SerShared@ (p. 4063)+data Series = Series+ { _serTx :: Maybe Formula+ -- ^ specifies text for a series name, without rich text formatting+ -- currently only reference formula is supported+ , _serShapeProperties :: Maybe ShapeProperties+ } deriving (Eq, Show, Generic)+instance NFData Series++-- | A series on a line chart+--+-- TODO: dPt, trendline, errBars, cat, extLst+--+-- See @CT_LineSer@ (p. 4064)+data LineSeries = LineSeries+ { _lnserShared :: Series+ , _lnserMarker :: Maybe DataMarker+ , _lnserDataLblProps :: Maybe DataLblProps+ , _lnserVal :: Maybe Formula+ -- ^ currently only reference formula is supported+ , _lnserSmooth :: Maybe Bool+ } deriving (Eq, Show, Generic)+instance NFData LineSeries++-- | A series on an area chart+--+-- TODO: pictureOptions, dPt, trendline, errBars, cat, extLst+--+-- See @CT_AreaSer@ (p. 4065)+data AreaSeries = AreaSeries+ { _arserShared :: Series+ , _arserDataLblProps :: Maybe DataLblProps+ , _arserVal :: Maybe Formula+ } deriving (Eq, Show, Generic)+instance NFData AreaSeries++-- | A series on a bar chart+--+-- TODO: invertIfNegative, pictureOptions, dPt, trendline, errBars,+-- cat, shape, extLst+--+-- See @CT_BarSer@ (p. 4064)+data BarSeries = BarSeries+ { _brserShared :: Series+ , _brserDataLblProps :: Maybe DataLblProps+ , _brserVal :: Maybe Formula+ } deriving (Eq, Show, Generic)+instance NFData BarSeries++-- | A series on a pie chart+--+-- TODO: explosion, cat, extLst+--+-- See @CT_PieSer@ (p. 4065)+data PieSeries = PieSeries+ { _piserShared :: Series+ , _piserDataPoints :: [DataPoint]+ -- ^ normally you should set fill for chart datapoints to make them+ -- properly colored+ , _piserDataLblProps :: Maybe DataLblProps+ , _piserVal :: Maybe Formula+ } deriving (Eq, Show, Generic)+instance NFData PieSeries++-- | A series on a scatter chart+--+-- TODO: dPt, trendline, errBars, smooth, extLst+--+-- See @CT_ScatterSer@ (p. 4064)+data ScatterSeries = ScatterSeries+ { _scserShared :: Series+ , _scserMarker :: Maybe DataMarker+ , _scserDataLblProps :: Maybe DataLblProps+ , _scserXVal :: Maybe Formula+ , _scserYVal :: Maybe Formula+ , _scserSmooth :: Maybe Bool+ } deriving (Eq, Show, Generic)+instance NFData ScatterSeries++-- See @CT_Marker@ (p. 4061)+data DataMarker = DataMarker+ { _dmrkSymbol :: Maybe DataMarkerSymbol+ , _dmrkSize :: Maybe Int+ -- ^ integer between 2 and 72, specifying a size in points+ } deriving (Eq, Show, Generic)+instance NFData DataMarker++data DataMarkerSymbol+ = DataMarkerCircle+ | DataMarkerDash+ | DataMarkerDiamond+ | DataMarkerDot+ | DataMarkerNone+ | DataMarkerPicture+ | DataMarkerPlus+ | DataMarkerSquare+ | DataMarkerStar+ | DataMarkerTriangle+ | DataMarkerX+ | DataMarkerAuto+ deriving (Eq, Show, Generic)+instance NFData DataMarkerSymbol++-- | Settings for the data labels for an entire series or the+-- entire chart+--+-- TODO: numFmt, spPr, txPr, dLblPos, showBubbleSize,+-- separator, showLeaderLines, leaderLines+-- See 21.2.2.49 "dLbls (Data Labels)" (p. 3384)+data DataLblProps = DataLblProps+ { _dlblShowLegendKey :: Maybe Bool+ , _dlblShowVal :: Maybe Bool+ , _dlblShowCatName :: Maybe Bool+ , _dlblShowSerName :: Maybe Bool+ , _dlblShowPercent :: Maybe Bool+ } deriving (Eq, Show, Generic)+instance NFData DataLblProps++-- | Specifies the possible positions for tick marks.++-- See 21.2.3.48 "ST_TickMark (Tick Mark)" (p. 3467)+data TickMark+ = TickMarkCross+ -- ^ (Cross) Specifies the tick marks shall cross the axis.+ | TickMarkIn+ -- ^ (Inside) Specifies the tick marks shall be inside the plot area.+ | TickMarkNone+ -- ^ (None) Specifies there shall be no tick marks.+ | TickMarkOut+ -- ^ (Outside) Specifies the tick marks shall be outside the plot area.+ deriving (Eq, Show, Generic)+instance NFData TickMark++makeLenses ''DataPoint++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default DataPoint where+ def = DataPoint Nothing Nothing++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor ChartSpace where+ fromCursor cur = do+ cur' <- cur $/ element (c_ "chart")+ _chspTitle <- maybeFromElement (c_ "title") cur'+ let _chspCharts =+ cur' $/ element (c_ "plotArea") &/ anyElement >=> chartFromNode . node+ _chspLegend <- maybeFromElement (c_ "legend") cur'+ _chspPlotVisOnly <- maybeBoolElementValue (c_ "plotVisOnly") cur'+ _chspDispBlanksAs <- maybeElementValue (c_ "dispBlanksAs") cur'+ return ChartSpace {..}++chartFromNode :: Node -> [Chart]+chartFromNode n+ | n `nodeElNameIs` (c_ "lineChart") = do+ _lnchGrouping <- fromElementValue (c_ "grouping") cur+ let _lnchSeries = cur $/ element (c_ "ser") >=> fromCursor+ _lnchMarker <- maybeBoolElementValue (c_ "marker") cur+ _lnchSmooth <- maybeBoolElementValue (c_ "smooth") cur+ return LineChart {..}+ | n `nodeElNameIs` (c_ "areaChart") = do+ _archGrouping <- maybeElementValue (c_ "grouping") cur+ let _archSeries = cur $/ element (c_ "ser") >=> fromCursor+ return AreaChart {..}+ | n `nodeElNameIs` (c_ "barChart") = do+ _brchDirection <- fromElementValue (c_ "barDir") cur+ _brchGrouping <-+ maybeElementValueDef (c_ "grouping") BarClusteredGrouping cur+ let _brchSeries = cur $/ element (c_ "ser") >=> fromCursor+ return BarChart {..}+ | n `nodeElNameIs` (c_ "pieChart") = do+ let _pichSeries = cur $/ element (c_ "ser") >=> fromCursor+ return PieChart {..}+ | n `nodeElNameIs` (c_ "scatterChart") = do+ _scchStyle <- fromElementValue (c_ "scatterStyle") cur+ let _scchSeries = cur $/ element (c_ "ser") >=> fromCursor+ return ScatterChart {..}+ | otherwise = fail "no matching chart node"+ where+ cur = fromNode n++instance FromCursor LineSeries where+ fromCursor cur = do+ _lnserShared <- fromCursor cur+ _lnserMarker <- maybeFromElement (c_ "marker") cur+ _lnserDataLblProps <- maybeFromElement (c_ "dLbls") cur+ _lnserVal <-+ cur $/ element (c_ "val") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ _lnserSmooth <- maybeElementValueDef (c_ "smooth") True cur+ return LineSeries {..}++instance FromCursor AreaSeries where+ fromCursor cur = do+ _arserShared <- fromCursor cur+ _arserDataLblProps <- maybeFromElement (c_ "dLbls") cur+ _arserVal <-+ cur $/ element (c_ "val") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ return AreaSeries {..}++instance FromCursor BarSeries where+ fromCursor cur = do+ _brserShared <- fromCursor cur+ _brserDataLblProps <- maybeFromElement (c_ "dLbls") cur+ _brserVal <-+ cur $/ element (c_ "val") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ return BarSeries {..}++instance FromCursor PieSeries where+ fromCursor cur = do+ _piserShared <- fromCursor cur+ let _piserDataPoints = cur $/ element (c_ "dPt") >=> fromCursor+ _piserDataLblProps <- maybeFromElement (c_ "dLbls") cur+ _piserVal <-+ cur $/ element (c_ "val") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ return PieSeries {..}++instance FromCursor ScatterSeries where+ fromCursor cur = do+ _scserShared <- fromCursor cur+ _scserMarker <- maybeFromElement (c_ "marker") cur+ _scserDataLblProps <- maybeFromElement (c_ "dLbls") cur+ _scserXVal <-+ cur $/ element (c_ "xVal") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ _scserYVal <-+ cur $/ element (c_ "yVal") &/ element (c_ "numRef") >=>+ maybeFromElement (c_ "f")+ _scserSmooth <- maybeElementValueDef (c_ "smooth") True cur+ return ScatterSeries {..}++-- should we respect idx and order?+instance FromCursor Series where+ fromCursor cur = do+ _serTx <-+ cur $/ element (c_ "tx") &/ element (c_ "strRef") >=>+ maybeFromElement (c_ "f")+ _serShapeProperties <- maybeFromElement (c_ "spPr") cur+ return Series {..}++instance FromCursor DataMarker where+ fromCursor cur = do+ _dmrkSymbol <- maybeElementValue (c_ "symbol") cur+ _dmrkSize <- maybeElementValue (c_ "size") cur+ return DataMarker {..}++instance FromCursor DataPoint where+ fromCursor cur = do+ _dpMarker <- maybeFromElement (c_ "marker") cur+ _dpShapeProperties <- maybeFromElement (c_ "spPr") cur+ return DataPoint {..}++instance FromAttrVal DataMarkerSymbol where+ fromAttrVal "circle" = readSuccess DataMarkerCircle+ fromAttrVal "dash" = readSuccess DataMarkerDash+ fromAttrVal "diamond" = readSuccess DataMarkerDiamond+ fromAttrVal "dot" = readSuccess DataMarkerDot+ fromAttrVal "none" = readSuccess DataMarkerNone+ fromAttrVal "picture" = readSuccess DataMarkerPicture+ fromAttrVal "plus" = readSuccess DataMarkerPlus+ fromAttrVal "square" = readSuccess DataMarkerSquare+ fromAttrVal "star" = readSuccess DataMarkerStar+ fromAttrVal "triangle" = readSuccess DataMarkerTriangle+ fromAttrVal "x" = readSuccess DataMarkerX+ fromAttrVal "auto" = readSuccess DataMarkerAuto+ fromAttrVal t = invalidText "DataMarkerSymbol" t++instance FromAttrVal BarDirection where+ fromAttrVal "bar" = readSuccess DirectionBar+ fromAttrVal "col" = readSuccess DirectionColumn+ fromAttrVal t = invalidText "BarDirection" t++instance FromAttrVal ScatterStyle where+ fromAttrVal "none" = readSuccess ScatterNone+ fromAttrVal "line" = readSuccess ScatterLine+ fromAttrVal "lineMarker" = readSuccess ScatterLineMarker+ fromAttrVal "marker" = readSuccess ScatterMarker+ fromAttrVal "smooth" = readSuccess ScatterSmooth+ fromAttrVal "smoothMarker" = readSuccess ScatterSmoothMarker+ fromAttrVal t = invalidText "ScatterStyle" t++instance FromCursor DataLblProps where+ fromCursor cur = do+ _dlblShowLegendKey <- maybeBoolElementValue (c_ "showLegendKey") cur+ _dlblShowVal <- maybeBoolElementValue (c_ "showVal") cur+ _dlblShowCatName <- maybeBoolElementValue (c_ "showCatName") cur+ _dlblShowSerName <- maybeBoolElementValue (c_ "showSerName") cur+ _dlblShowPercent <- maybeBoolElementValue (c_ "showPercent") cur+ return DataLblProps {..}++instance FromAttrVal ChartGrouping where+ fromAttrVal "percentStacked" = readSuccess PercentStackedGrouping+ fromAttrVal "standard" = readSuccess StandardGrouping+ fromAttrVal "stacked" = readSuccess StackedGrouping+ fromAttrVal t = invalidText "ChartGrouping" t++instance FromAttrVal BarChartGrouping where+ fromAttrVal "clustered" = readSuccess BarClusteredGrouping+ fromAttrVal "percentStacked" = readSuccess BarPercentStackedGrouping+ fromAttrVal "standard" = readSuccess BarStandardGrouping+ fromAttrVal "stacked" = readSuccess BarStackedGrouping+ fromAttrVal t = invalidText "BarChartGrouping" t++instance FromCursor ChartTitle where+ fromCursor cur = do+ let mTitle = listToMaybe $+ cur $/ element (c_ "tx") &/ element (c_ "rich") >=> fromCursor+ return $ ChartTitle mTitle++instance FromCursor Legend where+ fromCursor cur = do+ _legendPos <- maybeElementValue (c_ "legendPos") cur+ _legendOverlay <- maybeElementValueDef (c_ "overlay") True cur+ return Legend {..}++instance FromAttrVal LegendPos where+ fromAttrVal "b" = readSuccess LegendBottom+ fromAttrVal "l" = readSuccess LegendLeft+ fromAttrVal "r" = readSuccess LegendRight+ fromAttrVal "t" = readSuccess LegendTop+ fromAttrVal "tr" = readSuccess LegendTopRight+ fromAttrVal t = invalidText "LegendPos" t++instance FromAttrVal DispBlanksAs where+ fromAttrVal "gap" = readSuccess DispBlanksAsGap+ fromAttrVal "span" = readSuccess DispBlanksAsSpan+ fromAttrVal "zero" = readSuccess DispBlanksAsZero+ fromAttrVal t = invalidText "DispBlanksAs" t++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default Legend where+ def = Legend {_legendPos = Just LegendBottom, _legendOverlay = Just False}++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToDocument ChartSpace where+ toDocument =+ documentFromNsPrefElement "Charts generated by xlsx" chartNs (Just "c") .+ toElement "chartSpace"++instance ToElement ChartSpace where+ toElement nm ChartSpace {..} =+ elementListSimple nm [nonRounded, chartEl, chSpPr]+ where+ -- no such element gives a chart space with rounded corners+ nonRounded = elementValue "roundedCorners" False+ chSpPr = toElement "spPr" $ def {_spFill = Just $ solidRgb "ffffff"}+ chartEl = elementListSimple "chart" elements+ elements =+ catMaybes+ [ toElement "title" <$> _chspTitle+ -- LO?+ , Just $ elementValue "autoTitleDeleted" False+ , Just $ elementListSimple "plotArea" areaEls+ , toElement "legend" <$> _chspLegend+ , elementValue "plotVisOnly" <$> _chspPlotVisOnly+ , elementValue "dispBlanksAs" <$> _chspDispBlanksAs+ ]+ areaEls = charts ++ axes+ (_, charts, axes) = foldr addChart (1, [], []) _chspCharts+ addChart ch (i, cs, as) =+ let (c, as') = chartToElements ch i+ in (i + length as', c : cs, as' ++ as)++chartToElements :: Chart -> Int -> (Element, [Element])+chartToElements chart axId =+ case chart of+ LineChart {..} ->+ chartElement+ "lineChart"+ stdAxes+ (Just _lnchGrouping)+ _lnchSeries+ []+ (catMaybes+ [ elementValue "marker" <$> _lnchMarker+ , elementValue "smooth" <$> _lnchSmooth+ ])+ AreaChart {..} ->+ chartElement "areaChart" stdAxes _archGrouping _archSeries [] []+ BarChart {..} ->+ chartElement+ "barChart"+ stdAxes+ _brchGrouping+ _brchSeries+ [elementValue "barDir" _brchDirection]+ []+ PieChart {..} -> chartElement "pieChart" [] noGrouping _pichSeries [] []+ ScatterChart {..} ->+ chartElement+ "scatterChart"+ xyAxes+ noGrouping+ _scchSeries+ [elementValue "scatterStyle" _scchStyle]+ []+ where+ noGrouping :: Maybe ChartGrouping+ noGrouping = Nothing+ chartElement+ :: (ToElement s, ToAttrVal gr)+ => Name+ -> [Element]+ -> Maybe gr+ -> [s]+ -> [Element]+ -> [Element]+ -> (Element, [Element])+ chartElement nm axes mGrouping series prepended appended =+ ( elementListSimple nm $+ prepended +++ (maybeToList $ elementValue "grouping" <$> mGrouping) +++ (varyColors : seriesEls series) +++ appended ++ zipWith (\n _ -> elementValue "axId" n) [axId ..] axes+ , axes)+ -- no element seems to be equal to varyColors=true in Excel Online+ varyColors = elementValue "varyColors" False+ seriesEls series = [indexedSeriesEl i s | (i, s) <- zip [0 ..] series]+ indexedSeriesEl+ :: ToElement a+ => Int -> a -> Element+ indexedSeriesEl i s = prependI i $ toElement "ser" s+ prependI i e@Element {..} = e {elementNodes = iNodes i ++ elementNodes}+ iNodes i = map NodeElement [elementValue n i | n <- ["idx", "order"]]++ stdAxes = [catAx axId (axId + 1), valAx "l" (axId + 1) axId]+ xyAxes = [valAx "b" axId (axId + 1), valAx "l" (axId + 1) axId]+ catAx :: Int -> Int -> Element+ catAx i cr =+ elementListSimple "catAx" $+ [ elementValue "axId" i+ , emptyElement "scaling"+ , elementValue "delete" False+ , elementValue "axPos" ("b" :: Text)+ , elementValue "majorTickMark" TickMarkNone+ , elementValue "minorTickMark" TickMarkNone+ , toElement "spPr" grayLines+ , elementValue "crossAx" cr+ , elementValue "auto" True+ ]+ valAx :: Text -> Int -> Int -> Element+ valAx pos i cr =+ elementListSimple "valAx" $+ [ elementValue "axId" i+ , emptyElement "scaling"+ , elementValue "delete" False+ , elementValue "axPos" pos+ , gridLinesEl+ , elementValue "majorTickMark" TickMarkNone+ , elementValue "minorTickMark" TickMarkNone+ , toElement "spPr" grayLines+ , elementValue "crossAx" cr+ ]+ grayLines = def {_spOutline = Just def {_lnFill = Just $ solidRgb "b3b3b3"}}+ gridLinesEl =+ elementListSimple "majorGridlines" [toElement "spPr" grayLines]++instance ToAttrVal ChartGrouping where+ toAttrVal PercentStackedGrouping = "percentStacked"+ toAttrVal StandardGrouping = "standard"+ toAttrVal StackedGrouping = "stacked"++instance ToAttrVal BarChartGrouping where+ toAttrVal BarClusteredGrouping = "clustered"+ toAttrVal BarPercentStackedGrouping = "percentStacked"+ toAttrVal BarStandardGrouping = "standard"+ toAttrVal BarStackedGrouping = "stacked"++instance ToAttrVal BarDirection where+ toAttrVal DirectionBar = "bar"+ toAttrVal DirectionColumn = "col"++instance ToAttrVal ScatterStyle where+ toAttrVal ScatterNone = "none"+ toAttrVal ScatterLine = "line"+ toAttrVal ScatterLineMarker = "lineMarker"+ toAttrVal ScatterMarker = "marker"+ toAttrVal ScatterSmooth = "smooth"+ toAttrVal ScatterSmoothMarker = "smoothMarker"++instance ToElement LineSeries where+ toElement nm LineSeries {..} = simpleSeries nm _lnserShared _lnserVal pr ap+ where+ pr =+ catMaybes+ [ toElement "marker" <$> _lnserMarker+ , toElement "dLbls" <$> _lnserDataLblProps+ ]+ ap = maybeToList $ elementValue "smooth" <$> _lnserSmooth++simpleSeries :: Name+ -> Series+ -> Maybe Formula+ -> [Element]+ -> [Element]+ -> Element+simpleSeries nm shared val prepended appended =+ serEl {elementNodes = elementNodes serEl ++ map NodeElement elements}+ where+ serEl = toElement nm shared+ elements = prepended ++ (valEl val : appended)+ valEl v =+ elementListSimple+ "val"+ [elementListSimple "numRef" $ maybeToList (toElement "f" <$> v)]++instance ToElement DataMarker where+ toElement nm DataMarker {..} = elementListSimple nm elements+ where+ elements =+ catMaybes+ [ elementValue "symbol" <$> _dmrkSymbol+ , elementValue "size" <$> _dmrkSize+ ]++instance ToAttrVal DataMarkerSymbol where+ toAttrVal DataMarkerCircle = "circle"+ toAttrVal DataMarkerDash = "dash"+ toAttrVal DataMarkerDiamond = "diamond"+ toAttrVal DataMarkerDot = "dot"+ toAttrVal DataMarkerNone = "none"+ toAttrVal DataMarkerPicture = "picture"+ toAttrVal DataMarkerPlus = "plus"+ toAttrVal DataMarkerSquare = "square"+ toAttrVal DataMarkerStar = "star"+ toAttrVal DataMarkerTriangle = "triangle"+ toAttrVal DataMarkerX = "x"+ toAttrVal DataMarkerAuto = "auto"++instance ToElement DataLblProps where+ toElement nm DataLblProps {..} = elementListSimple nm elements+ where+ elements =+ catMaybes+ [ elementValue "showLegendKey" <$> _dlblShowLegendKey+ , elementValue "showVal" <$> _dlblShowVal+ , elementValue "showCatName" <$> _dlblShowCatName+ , elementValue "showSerName" <$> _dlblShowSerName+ , elementValue "showPercent" <$> _dlblShowPercent+ ]++instance ToElement AreaSeries where+ toElement nm AreaSeries {..} = simpleSeries nm _arserShared _arserVal pr []+ where+ pr = maybeToList $ fmap (toElement "dLbls") _arserDataLblProps++instance ToElement BarSeries where+ toElement nm BarSeries {..} = simpleSeries nm _brserShared _brserVal pr []+ where+ pr = maybeToList $ fmap (toElement "dLbls") _brserDataLblProps++instance ToElement PieSeries where+ toElement nm PieSeries {..} = simpleSeries nm _piserShared _piserVal pr []+ where+ pr = dPts ++ maybeToList (fmap (toElement "dLbls") _piserDataLblProps)+ dPts = zipWith dPtEl [(0 :: Int) ..] _piserDataPoints+ dPtEl i DataPoint {..} =+ elementListSimple+ "dPt"+ (elementValue "idx" i :+ catMaybes+ [ toElement "marker" <$> _dpMarker+ , toElement "spPr" <$> _dpShapeProperties+ ])++instance ToElement ScatterSeries where+ toElement nm ScatterSeries {..} =+ serEl {elementNodes = elementNodes serEl ++ map NodeElement elements}+ where+ serEl = toElement nm _scserShared+ elements =+ catMaybes+ [ toElement "marker" <$> _scserMarker+ , toElement "dLbls" <$> _scserDataLblProps+ ] +++ [valEl "xVal" _scserXVal, valEl "yVal" _scserYVal] +++ (maybeToList $ fmap (elementValue "smooth") _scserSmooth)+ valEl vnm v =+ elementListSimple+ vnm+ [elementListSimple "numRef" $ maybeToList (toElement "f" <$> v)]++-- should we respect idx and order?+instance ToElement Series where+ toElement nm Series {..} =+ elementListSimple nm $+ [ elementListSimple+ "tx"+ [elementListSimple "strRef" $ maybeToList (toElement "f" <$> _serTx)]+ ] +++ maybeToList (toElement "spPr" <$> _serShapeProperties)++instance ToElement ChartTitle where+ toElement nm (ChartTitle body) =+ elementListSimple nm [txEl, elementValue "overlay" False]+ where+ txEl = elementListSimple "tx" $ catMaybes [toElement (c_ "rich") <$> body]++instance ToElement Legend where+ toElement nm Legend{..} = elementListSimple nm elements+ where+ elements = catMaybes [ elementValue "legendPos" <$> _legendPos+ , elementValue "overlay" <$>_legendOverlay]++instance ToAttrVal LegendPos where+ toAttrVal LegendBottom = "b"+ toAttrVal LegendLeft = "l"+ toAttrVal LegendRight = "r"+ toAttrVal LegendTop = "t"+ toAttrVal LegendTopRight = "tr"++instance ToAttrVal DispBlanksAs where+ toAttrVal DispBlanksAsGap = "gap"+ toAttrVal DispBlanksAsSpan = "span"+ toAttrVal DispBlanksAsZero = "zero"++instance ToAttrVal TickMark where+ toAttrVal TickMarkCross = "cross"+ toAttrVal TickMarkIn = "in"+ toAttrVal TickMarkNone = "none"+ toAttrVal TickMarkOut = "out"++-- | Add chart namespace to name+c_ :: Text -> Name+c_ x =+ Name {nameLocalName = x, nameNamespace = Just chartNs, namePrefix = Just "c"}++chartNs :: Text+chartNs = "http://schemas.openxmlformats.org/drawingml/2006/chart"
+ src/Codec/Xlsx/Types/Drawing/Common.hs view
@@ -0,0 +1,619 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Drawing.Common where++import GHC.Generics (Generic)++import Control.Arrow (first)+import Codec.Xlsx.LensCompat (makeLenses)+import Control.Monad (join)+import Control.Monad.Fail (MonadFail)+import Control.DeepSeq (NFData)+import Data.Default+import Data.Maybe (catMaybes, listToMaybe)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Read as T+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++-- | This simple type represents an angle in 60,000ths of a degree.+-- Positive angles are clockwise (i.e., towards the positive y axis); negative+-- angles are counter-clockwise (i.e., towards the negative y axis).+newtype Angle =+ Angle Int+ deriving (Eq, Show, Generic)+instance NFData Angle++-- | A string with rich text formatting+--+-- TODO: horzOverflow, lIns, tIns, rIns, bIns, numCol, spcCol, rtlCol,+-- fromWordArt, forceAA, upright, compatLnSpc, prstTxWarp,+-- a_EG_TextAutofit, scene3d, a_EG_Text3D, extLst+--+-- See @CT_TextBody@ (p. 4034)+data TextBody = TextBody+ { _txbdRotation :: Angle+ -- ^ Specifies the rotation that is being applied to the text within the bounding box.+ , _txbdSpcFirstLastPara :: Bool+ -- ^ Specifies whether the before and after paragraph spacing defined by the user is+ -- to be respected.+ , _txbdVertOverflow :: TextVertOverflow+ -- ^ Determines whether the text can flow out of the bounding box vertically.+ , _txbdVertical :: TextVertical+ -- ^ Determines if the text within the given text body should be displayed vertically.+ , _txbdWrap :: TextWrap+ -- ^ Specifies the wrapping options to be used for this text body.+ , _txbdAnchor :: TextAnchoring+ -- ^ Specifies the anchoring position of the txBody within the shape.+ , _txbdAnchorCenter :: Bool+ -- ^ Specifies the centering of the text box. The way it works fundamentally is+ -- to determine the smallest possible "bounds box" for the text and then to center+ -- that "bounds box" accordingly. This is different than paragraph alignment, which+ -- aligns the text within the "bounds box" for the text.+ , _txbdParagraphs :: [TextParagraph]+ -- ^ Paragraphs of text within the containing text body+ } deriving (Eq, Show, Generic)+instance NFData TextBody+++-- | Text vertical overflow+-- See 20.1.10.83 "ST_TextVertOverflowType (Text Vertical Overflow)" (p. 3083)+data TextVertOverflow+ = TextVertOverflowClip+ -- ^ Pay attention to top and bottom barriers. Provide no indication that there is+ -- text which is not visible.+ | TextVertOverflowEllipsis+ -- ^ Pay attention to top and bottom barriers. Use an ellipsis to denote that+ -- there is text which is not visible.+ | TextVertOverflow+ -- ^ Overflow the text and pay no attention to top and bottom barriers.+ deriving (Eq, Show, Generic)+instance NFData TextVertOverflow++-- | If there is vertical text, determines what kind of vertical text is going to be used.+--+-- See 20.1.10.82 "ST_TextVerticalType (Vertical Text Types)" (p. 3083)+data TextVertical+ = TextVerticalEA+ -- ^ A special version of vertical text, where some fonts are displayed as if rotated+ -- by 90 degrees while some fonts (mostly East Asian) are displayed vertical.+ | TextVerticalHorz+ -- ^ Horizontal text. This should be default.+ | TextVerticalMongolian+ -- ^ A special version of vertical text, where some fonts are displayed as if rotated+ -- by 90 degrees while some fonts (mostly East Asian) are displayed vertical. The+ -- difference between this and the 'TextVerticalEA' is the text flows top down then+ -- LEFT RIGHT, instead of RIGHT LEFT+ | TextVertical+ -- ^ Determines if all of the text is vertical orientation (each line is 90 degrees+ -- rotated clockwise, so it goes from top to bottom; each next line is to the left+ -- from the previous one).+ | TextVertical270+ -- ^ Determines if all of the text is vertical orientation (each line is 270 degrees+ -- rotated clockwise, so it goes from bottom to top; each next line is to the right+ -- from the previous one).+ | TextVerticalWordArt+ -- ^ Determines if all of the text is vertical ("one letter on top of another").+ | TextVerticalWordArtRtl+ -- ^ Specifies that vertical WordArt should be shown from right to left rather than+ -- left to right.+ deriving (Eq, Show, Generic)+instance NFData TextVertical++-- | Text wrapping types+--+-- See 20.1.10.84 "ST_TextWrappingType (Text Wrapping Types)" (p. 3084)+data TextWrap+ = TextWrapNone+ -- ^ No wrapping occurs on this text body. Words spill out without+ -- paying attention to the bounding rectangle boundaries.+ | TextWrapSquare+ -- ^ Determines whether we wrap words within the bounding rectangle.+ deriving (Eq, Show, Generic)+instance NFData TextWrap++-- | This type specifies a list of available anchoring types for text.+--+-- See 20.1.10.59 "ST_TextAnchoringType (Text Anchoring Types)" (p. 3058)+data TextAnchoring+ = TextAnchoringBottom+ -- ^ Anchor the text at the bottom of the bounding rectangle.+ | TextAnchoringCenter+ -- ^ Anchor the text at the middle of the bounding rectangle.+ | TextAnchoringDistributed+ -- ^ Anchor the text so that it is distributed vertically. When text is horizontal,+ -- this spaces out the actual lines of text and is almost always identical in+ -- behavior to 'TextAnchoringJustified' (special case: if only 1 line, then anchored+ -- in middle). When text is vertical, then it distributes the letters vertically.+ -- This is different than 'TextAnchoringJustified', because it always forces distribution+ -- of the words, even if there are only one or two words in a line.+ | TextAnchoringJustified+ -- ^ Anchor the text so that it is justified vertically. When text is horizontal,+ -- this spaces out the actual lines of text and is almost always identical in+ -- behavior to 'TextAnchoringDistributed' (special case: if only 1 line, then anchored at+ -- top). When text is vertical, then it justifies the letters vertically. This is+ -- different than 'TextAnchoringDistributed' because in some cases such as very little+ -- text in a line, it does not justify.+ | TextAnchoringTop+ -- ^ Anchor the text at the top of the bounding rectangle.+ deriving (Eq, Show, Generic)+instance NFData TextAnchoring++-- See 21.1.2.2.6 "p (Text Paragraphs)" (p. 3211)+data TextParagraph = TextParagraph+ { _txpaDefCharProps :: Maybe TextCharacterProperties+ , _txpaRuns :: [TextRun]+ } deriving (Eq, Show, Generic)+instance NFData TextParagraph++-- | Text character properties+--+-- TODO: kumimoji, lang, altLang, sz, strike, kern, cap, spc,+-- normalizeH, baseline, noProof, dirty, err, smtClean, smtId,+-- bmk, ln, a_EG_FillProperties, a_EG_EffectProperties, highlight,+-- a_EG_TextUnderlineLine, a_EG_TextUnderlineFill, latin, ea, cs,+-- sym, hlinkClick, hlinkMouseOver, rtl, extLst+--+-- See @CT_TextCharacterProperties@ (p. 4039)+data TextCharacterProperties = TextCharacterProperties+ { _txchBold :: Bool+ , _txchItalic :: Bool+ , _txchUnderline :: Bool+ } deriving (Eq, Show, Generic)+instance NFData TextCharacterProperties++-- | Text run+--+-- TODO: br, fld+data TextRun = RegularRun+ { _txrCharProps :: Maybe TextCharacterProperties+ , _txrText :: Text+ } deriving (Eq, Show, Generic)+instance NFData TextRun++-- | This simple type represents a one dimensional position or length+--+-- See 20.1.10.16 "ST_Coordinate (Coordinate)" (p. 2921)+data Coordinate+ = UnqCoordinate Int+ -- ^ see 20.1.10.19 "ST_CoordinateUnqualified (Coordinate)" (p. 2922)+ | UniversalMeasure UnitIdentifier+ Double+ -- ^ see 22.9.2.15 "ST_UniversalMeasure (Universal Measurement)" (p. 3793)+ deriving (Eq, Show, Generic)+instance NFData Coordinate++-- | Units used in "Universal measure" coordinates+-- see 22.9.2.15 "ST_UniversalMeasure (Universal Measurement)" (p. 3793)+data UnitIdentifier+ = UnitCm -- "cm" As defined in ISO 31.+ | UnitMm -- "mm" As defined in ISO 31.+ | UnitIn -- "in" 1 in = 2.54 cm (informative)+ | UnitPt -- "pt" 1 pt = 1/72 in (informative)+ | UnitPc -- "pc" 1 pc = 12 pt (informative)+ | UnitPi -- "pi" 1 pi = 12 pt (informative)+ deriving (Eq, Show, Generic)+instance NFData UnitIdentifier++-- See @CT_Point2D@ (p. 3989)+data Point2D = Point2D+ { _pt2dX :: Coordinate+ , _pt2dY :: Coordinate+ } deriving (Eq, Show, Generic)+instance NFData Point2D++unqPoint2D :: Int -> Int -> Point2D+unqPoint2D x y = Point2D (UnqCoordinate x) (UnqCoordinate y)++-- | Positive position or length in EMUs, maximu allowed value is 27273042316900.+-- see 20.1.10.41 "ST_PositiveCoordinate (Positive Coordinate)" (p. 2942)+newtype PositiveCoordinate =+ PositiveCoordinate Integer+ deriving (Eq, Ord, Show, Generic)+instance NFData PositiveCoordinate++data PositiveSize2D = PositiveSize2D+ { _ps2dX :: PositiveCoordinate+ , _ps2dY :: PositiveCoordinate+ } deriving (Eq, Show, Generic)+instance NFData PositiveSize2D++positiveSize2D :: Integer -> Integer -> PositiveSize2D+positiveSize2D x y =+ PositiveSize2D (PositiveCoordinate x) (PositiveCoordinate y)++cmSize2D :: Integer -> Integer -> PositiveSize2D+cmSize2D x y = positiveSize2D (cm2emu x) (cm2emu y)++cm2emu :: Integer -> Integer+cm2emu cm = 360000 * cm++-- See 20.1.7.6 "xfrm (2D Transform for Individual Objects)" (p. 2849)+data Transform2D = Transform2D+ { _trRot :: Angle+ -- ^ Specifies the rotation of the Graphic Frame.+ , _trFlipH :: Bool+ -- ^ Specifies a horizontal flip. When true, this attribute defines+ -- that the shape is flipped horizontally about the center of its bounding box.+ , _trFlipV :: Bool+ -- ^ Specifies a vertical flip. When true, this attribute defines+ -- that the shape is flipped vetically about the center of its bounding box.+ , _trOffset :: Maybe Point2D+ -- ^ See 20.1.7.4 "off (Offset)" (p. 2847)+ , _trExtents :: Maybe PositiveSize2D+ -- ^ See 20.1.7.3 "ext (Extents)" (p. 2846) or+ -- 20.5.2.14 "ext (Shape Extent)" (p. 3165)+ } deriving (Eq, Show, Generic)+instance NFData Transform2D++-- TODO: custGeom+data Geometry =+ PresetGeometry+ -- TODO: prst, avList+ -- currently uses "rect" with empty avList+ deriving (Eq, Show, Generic)+instance NFData Geometry++-- See 20.1.2.2.35 "spPr (Shape Properties)" (p. 2751)+data ShapeProperties = ShapeProperties+ { _spXfrm :: Maybe Transform2D+ , _spGeometry :: Maybe Geometry+ , _spFill :: Maybe FillProperties+ , _spOutline :: Maybe LineProperties+ -- TODO: bwMode, a_EG_EffectProperties, scene3d, sp3d, extLst+ } deriving (Eq, Show, Generic)+instance NFData ShapeProperties++-- | Specifies an outline style that can be applied to a number of+-- different objects such as shapes and text.+--+-- TODO: cap, cmpd, algn, a_EG_LineDashProperties,+-- a_EG_LineJoinProperties, headEnd, tailEnd, extLst+--+-- See 20.1.2.2.24 "ln (Outline)" (p. 2744)+data LineProperties = LineProperties+ { _lnFill :: Maybe FillProperties+ , _lnWidth :: Int+ -- ^ Specifies the width to be used for the underline stroke. The+ -- value is in EMU, is greater of equal to 0 and maximum value is+ -- 20116800.+ } deriving (Eq, Show, Generic)+instance NFData LineProperties++-- | Color choice for some drawing element+--+-- TODO: scrgbClr, hslClr, sysClr, schemeClr, prstClr+--+-- See @EG_ColorChoice@ (p. 3996)+data ColorChoice =+ RgbColor Text+ -- ^ Specifies a color using the red, green, blue RGB color+ -- model. Red, green, and blue is expressed as sequence of hex+ -- digits, RRGGBB. A perceptual gamma of 2.2 is used.+ --+ -- See 20.1.2.3.32 "srgbClr (RGB Color Model - Hex Variant)" (p. 2773)+ deriving (Eq, Show, Generic)+instance NFData ColorChoice++-- TODO: gradFill, pattFill+data FillProperties =+ NoFill+ -- ^ See 20.1.8.44 "noFill (No Fill)" (p. 2872)+ | SolidFill (Maybe ColorChoice)+ -- ^ Solid fill+ -- See 20.1.8.54 "solidFill (Solid Fill)" (p. 2879)+ deriving (Eq, Show, Generic)+instance NFData FillProperties++-- | solid fill with color specified by hexadecimal RGB color+solidRgb :: Text -> FillProperties+solidRgb t = SolidFill . Just $ RgbColor t++makeLenses ''ShapeProperties++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default ShapeProperties where+ def = ShapeProperties Nothing Nothing Nothing Nothing++instance Default LineProperties where+ def = LineProperties Nothing 0++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor TextBody where+ fromCursor cur = do+ cur' <- cur $/ element (a_ "bodyPr")+ _txbdRotation <- fromAttributeDef "rot" (Angle 0) cur'+ _txbdSpcFirstLastPara <- fromAttributeDef "spcFirstLastPara" False cur'+ _txbdVertOverflow <- fromAttributeDef "vertOverflow" TextVertOverflow cur'+ _txbdVertical <- fromAttributeDef "vert" TextVerticalHorz cur'+ _txbdWrap <- fromAttributeDef "wrap" TextWrapSquare cur'+ _txbdAnchor <- fromAttributeDef "anchor" TextAnchoringTop cur'+ _txbdAnchorCenter <- fromAttributeDef "anchorCtr" False cur'+ let _txbdParagraphs = cur $/ element (a_ "p") >=> fromCursor+ return TextBody {..}++instance FromCursor TextParagraph where+ fromCursor cur = do+ let _txpaDefCharProps =+ join . listToMaybe $+ cur $/ element (a_ "pPr") >=> maybeFromElement (a_ "defRPr")+ _txpaRuns = cur $/ element (a_ "r") >=> fromCursor+ return TextParagraph {..}++instance FromCursor TextCharacterProperties where+ fromCursor cur = do+ _txchBold <- fromAttributeDef "b" False cur+ _txchItalic <- fromAttributeDef "i" False cur+ _txchUnderline <- fromAttributeDef "u" False cur+ return TextCharacterProperties {..}++instance FromCursor TextRun where+ fromCursor cur = do+ _txrCharProps <- maybeFromElement (a_ "rPr") cur+ _txrText <- cur $/ element (a_ "t") &/ content+ return RegularRun {..}++-- See 20.1.10.3 "ST_Angle (Angle)" (p. 2912)+instance FromAttrVal Angle where+ fromAttrVal t = first Angle <$> fromAttrVal t++-- See 20.1.10.83 "ST_TextVertOverflowType (Text Vertical Overflow)" (p. 3083)+instance FromAttrVal TextVertOverflow where+ fromAttrVal "overflow" = readSuccess TextVertOverflow+ fromAttrVal "ellipsis" = readSuccess TextVertOverflowEllipsis+ fromAttrVal "clip" = readSuccess TextVertOverflowClip+ fromAttrVal t = invalidText "TextVertOverflow" t++instance FromAttrVal TextVertical where+ fromAttrVal "horz" = readSuccess TextVerticalHorz+ fromAttrVal "vert" = readSuccess TextVertical+ fromAttrVal "vert270" = readSuccess TextVertical270+ fromAttrVal "wordArtVert" = readSuccess TextVerticalWordArt+ fromAttrVal "eaVert" = readSuccess TextVerticalEA+ fromAttrVal "mongolianVert" = readSuccess TextVerticalMongolian+ fromAttrVal "wordArtVertRtl" = readSuccess TextVerticalWordArtRtl+ fromAttrVal t = invalidText "TextVertical" t++instance FromAttrVal TextWrap where+ fromAttrVal "none" = readSuccess TextWrapNone+ fromAttrVal "square" = readSuccess TextWrapSquare+ fromAttrVal t = invalidText "TextWrap" t++-- See 20.1.10.59 "ST_TextAnchoringType (Text Anchoring Types)" (p. 3058)+instance FromAttrVal TextAnchoring where+ fromAttrVal "t" = readSuccess TextAnchoringTop+ fromAttrVal "ctr" = readSuccess TextAnchoringCenter+ fromAttrVal "b" = readSuccess TextAnchoringBottom+ fromAttrVal "just" = readSuccess TextAnchoringJustified+ fromAttrVal "dist" = readSuccess TextAnchoringDistributed+ fromAttrVal t = invalidText "TextAnchoring" t++instance FromCursor ShapeProperties where+ fromCursor cur = do+ _spXfrm <- maybeFromElement (a_ "xfrm") cur+ let _spGeometry = listToMaybe $ cur $/ anyElement >=> fromCursor+ _spFill = listToMaybe $ cur $/ anyElement >=> fillPropsFromNode . node+ _spOutline <- maybeFromElement (a_ "ln") cur+ return ShapeProperties {..}++instance FromCursor Transform2D where+ fromCursor cur = do+ _trRot <- fromAttributeDef "rot" (Angle 0) cur+ _trFlipH <- fromAttributeDef "flipH" False cur+ _trFlipV <- fromAttributeDef "flipV" False cur+ _trOffset <- maybeFromElement (a_ "off") cur+ _trExtents <- maybeFromElement (a_ "ext") cur+ return Transform2D{..}++instance FromCursor Geometry where+ fromCursor = geometryFromNode . node++geometryFromNode :: Node -> [Geometry]+geometryFromNode n | n `nodeElNameIs` a_ "prstGeom" =+ return PresetGeometry+ | otherwise = fail "no matching geometry node"++instance FromCursor LineProperties where+ fromCursor cur = do+ let _lnFill = listToMaybe $ cur $/ anyElement >=> fromCursor+ _lnWidth <- fromAttributeDef "w" 0 cur+ return LineProperties{..}++instance FromCursor Point2D where+ fromCursor cur = do+ x <- coordinate =<< fromAttribute "x" cur+ y <- coordinate =<< fromAttribute "y" cur+ return $ Point2D x y++instance FromCursor PositiveSize2D where+ fromCursor cur = do+ cx <- PositiveCoordinate <$> fromAttribute "cx" cur+ cy <- PositiveCoordinate <$> fromAttribute "cy" cur+ return $ PositiveSize2D cx cy++instance FromCursor FillProperties where+ fromCursor = fillPropsFromNode . node++fillPropsFromNode :: Node -> [FillProperties]+fillPropsFromNode n+ | n `nodeElNameIs` a_ "noFill" = return NoFill+ | n `nodeElNameIs` a_ "solidFill" = do+ let color =+ listToMaybe $ fromNode n $/ anyElement >=> colorChoiceFromNode . node+ return $ SolidFill color+ | otherwise = fail "no matching line fill node"++colorChoiceFromNode :: Node -> [ColorChoice]+colorChoiceFromNode n+ | n `nodeElNameIs` a_ "srgbClr" = do+ val <- fromAttribute "val" $ fromNode n+ return $ RgbColor val+ | otherwise = fail "no matching color choice node"++coordinate :: MonadFail m => Text -> m Coordinate+coordinate t = case T.decimal t of+ Right (d, leftover) | leftover == T.empty ->+ return $ UnqCoordinate d+ _ ->+ case T.rational t of+ Right (r, "cm") -> return $ UniversalMeasure UnitCm r+ Right (r, "mm") -> return $ UniversalMeasure UnitMm r+ Right (r, "in") -> return $ UniversalMeasure UnitIn r+ Right (r, "pt") -> return $ UniversalMeasure UnitPt r+ Right (r, "pc") -> return $ UniversalMeasure UnitPc r+ Right (r, "pi") -> return $ UniversalMeasure UnitPi r+ _ -> fail $ "invalid coordinate: " ++ show t++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement TextBody where+ toElement nm TextBody {..} = elementListSimple nm (bodyPr : paragraphs)+ where+ bodyPr = leafElement (a_ "bodyPr") bodyPrAttrs+ bodyPrAttrs =+ catMaybes+ [ "rot" .=? justNonDef (Angle 0) _txbdRotation+ , "spcFirstLastPara" .=? justTrue _txbdSpcFirstLastPara+ , "vertOverflow" .=? justNonDef TextVertOverflow _txbdVertOverflow+ , "vert" .=? justNonDef TextVerticalHorz _txbdVertical+ , "wrap" .=? justNonDef TextWrapSquare _txbdWrap+ , "anchor" .=? justNonDef TextAnchoringTop _txbdAnchor+ , "anchorCtr" .=? justTrue _txbdAnchorCenter+ ]+ paragraphs = map (toElement (a_ "p")) _txbdParagraphs++instance ToElement TextParagraph where+ toElement nm TextParagraph {..} = elementListSimple nm elements+ where+ elements =+ case _txpaDefCharProps of+ Just props -> (defRPr props) : runs+ Nothing -> runs+ defRPr props =+ elementListSimple (a_ "pPr") [toElement (a_ "defRPr") props]+ runs = map (toElement (a_ "r")) _txpaRuns++instance ToElement TextCharacterProperties where+ toElement nm TextCharacterProperties {..} = leafElement nm attrs+ where+ attrs = ["b" .= _txchBold, "i" .= _txchItalic, "u" .= _txchUnderline]++instance ToElement TextRun where+ toElement nm RegularRun {..} = elementListSimple nm elements+ where+ elements =+ catMaybes+ [ toElement (a_ "rPr") <$> _txrCharProps+ , Just $ elementContent (a_ "t") _txrText+ ]++instance ToAttrVal TextVertOverflow where+ toAttrVal TextVertOverflow = "overflow"+ toAttrVal TextVertOverflowEllipsis = "ellipsis"+ toAttrVal TextVertOverflowClip = "clip"++instance ToAttrVal TextVertical where+ toAttrVal TextVerticalHorz = "horz"+ toAttrVal TextVertical = "vert"+ toAttrVal TextVertical270 = "vert270"+ toAttrVal TextVerticalWordArt = "wordArtVert"+ toAttrVal TextVerticalEA = "eaVert"+ toAttrVal TextVerticalMongolian = "mongolianVert"+ toAttrVal TextVerticalWordArtRtl = "wordArtVertRtl"++instance ToAttrVal TextWrap where+ toAttrVal TextWrapNone = "none"+ toAttrVal TextWrapSquare = "square"++-- See 20.1.10.59 "ST_TextAnchoringType (Text Anchoring Types)" (p. 3058)+instance ToAttrVal TextAnchoring where+ toAttrVal TextAnchoringTop = "t"+ toAttrVal TextAnchoringCenter = "ctr"+ toAttrVal TextAnchoringBottom = "b"+ toAttrVal TextAnchoringJustified = "just"+ toAttrVal TextAnchoringDistributed = "dist"++instance ToAttrVal Angle where+ toAttrVal (Angle x) = toAttrVal x++instance ToElement ShapeProperties where+ toElement nm ShapeProperties{..} = elementListSimple nm elements+ where+ elements = catMaybes [ toElement (a_ "xfrm") <$> _spXfrm+ , geometryToElement <$> _spGeometry+ , fillPropsToElement <$> _spFill+ , toElement (a_ "ln") <$> _spOutline ]++instance ToElement Point2D where+ toElement nm Point2D{..} = leafElement nm [ "x" .= _pt2dX+ , "y" .= _pt2dY+ ]++instance ToElement PositiveSize2D where+ toElement nm PositiveSize2D{..} = leafElement nm [ "cx" .= _ps2dX+ , "cy" .= _ps2dY ]++instance ToAttrVal Coordinate where+ toAttrVal (UnqCoordinate x) = toAttrVal x+ toAttrVal (UniversalMeasure unit x) = toAttrVal x <> unitToText unit+ where+ unitToText UnitCm = "cm"+ unitToText UnitMm = "mm"+ unitToText UnitIn = "in"+ unitToText UnitPt = "pt"+ unitToText UnitPc = "pc"+ unitToText UnitPi = "pi"++instance ToAttrVal PositiveCoordinate where+ toAttrVal (PositiveCoordinate x) = toAttrVal x++instance ToElement Transform2D where+ toElement nm Transform2D{..} = elementList nm attrs elements+ where+ attrs = catMaybes [ "rot" .=? justNonDef (Angle 0) _trRot+ , "flipH" .=? justTrue _trFlipH+ , "flipV" .=? justTrue _trFlipV ]+ elements = catMaybes [ toElement (a_ "off") <$> _trOffset+ , toElement (a_ "ext") <$> _trExtents ]++geometryToElement :: Geometry -> Element+geometryToElement PresetGeometry =+ leafElement (a_ "prstGeom") ["prst" .= ("rect" :: Text)]++instance ToElement LineProperties where+ toElement nm LineProperties {..} = elementList nm attrs elements+ where+ attrs = catMaybes ["w" .=? justNonDef 0 _lnWidth]+ elements = catMaybes [fillPropsToElement <$> _lnFill]++fillPropsToElement :: FillProperties -> Element+fillPropsToElement NoFill = emptyElement (a_ "noFill")+fillPropsToElement (SolidFill color) =+ elementListSimple (a_ "solidFill") $ catMaybes [colorChoiceToElement <$> color]++colorChoiceToElement :: ColorChoice -> Element+colorChoiceToElement (RgbColor color) =+ leafElement (a_ "srgbClr") ["val" .= color]++-- | Add DrawingML namespace to name+a_ :: Text -> Name+a_ x =+ Name {nameLocalName = x, nameNamespace = Just drawingNs, namePrefix = Just "a"}++drawingNs :: Text+drawingNs = "http://schemas.openxmlformats.org/drawingml/2006/main"
+ src/Codec/Xlsx/Types/Internal.hs view
@@ -0,0 +1,3 @@+module Codec.Xlsx.Types.Internal (module Codec.Xlsx.Types.RefId) where++import Codec.Xlsx.Types.RefId
+ src/Codec/Xlsx/Types/Internal/CfPair.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.CfPair where++import GHC.Generics (Generic)+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.ConditionalFormatting+import Codec.Xlsx.Writer.Internal++-- | Internal helper type for parsing "conditionalFormatting recods+-- TODO: pivot, extList+-- Implementing those will need this implementation to be changed+--+-- See 18.3.1.18 "conditionalFormatting (Conditional Formatting)" (p. 1610)+newtype CfPair = CfPair+ { unCfPair :: (SqRef, ConditionalFormatting)+ } deriving (Eq, Show, Generic)++instance FromCursor CfPair where+ fromCursor cur = do+ sqref <- fromAttribute "sqref" cur+ let cfRules = cur $/ element (addSmlNamespace "cfRule") >=> fromCursor+ return $ CfPair (sqref, cfRules)++instance FromXenoNode CfPair where+ fromXenoNode root = do+ sqref <- parseAttributes root $ fromAttr "sqref"+ cfRules <- collectChildren root $ fromChildList "cfRule"+ return $ CfPair (sqref, cfRules)++instance ToElement CfPair where+ toElement nm (CfPair (sqRef, cfRules)) =+ elementList nm [ "sqref" .= toAttrVal sqRef ]+ (map (toElement "cfRule") cfRules)
+ src/Codec/Xlsx/Types/Internal/CommentTable.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.CommentTable where++import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Lazy.Char8 as LBC8+import Data.List.Extra (nubOrd)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Text.Lazy (toStrict)+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Text.Lazy.Builder.Int as B+import GHC.Generics (Generic)+import Safe+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Comment+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Writer.Internal++newtype CommentTable = CommentTable+ { _commentsTable :: Map CellRef Comment }+ deriving (Eq, Show, Generic)++tshow :: Show a => a -> Text+tshow = Text.pack . show++fromList :: [(CellRef, Comment)] -> CommentTable+fromList = CommentTable . M.fromList++toList :: CommentTable -> [(CellRef, Comment)]+toList = M.toList . _commentsTable++lookupComment :: CellRef -> CommentTable -> Maybe Comment+lookupComment ref = M.lookup ref . _commentsTable++instance ToDocument CommentTable where+ toDocument = documentFromElement "Sheet comments generated by xlsx"+ . toElement "comments"++instance ToElement CommentTable where+ toElement nm (CommentTable m) = Element+ { elementName = nm+ , elementAttributes = M.empty+ , elementNodes = [ NodeElement $ elementListSimple "authors" authorNodes+ , NodeElement . elementListSimple "commentList" $ map commentToEl (M.toList m) ]+ }+ where+ commentToEl (ref, Comment{..}) = Element+ { elementName = "comment"+ , elementAttributes = M.fromList [ ("ref" .= ref)+ , ("authorId" .= lookupAuthor _commentAuthor)+ , ("visible" .= tshow _commentVisible)]+ , elementNodes = [NodeElement $ toElement "text" _commentText]+ }+ lookupAuthor a = fromJustNote "author lookup" $ M.lookup a authorIds+ authorNames = nubOrd . map _commentAuthor $ M.elems m+ decimalToText :: Integer -> Text+ decimalToText = toStrict . B.toLazyText . B.decimal+ authorIds = M.fromList $ zip authorNames (map decimalToText [0..])+ authorNodes = map (elementContent "author") authorNames++instance FromCursor CommentTable where+ fromCursor cur = do+ let authorNames = cur $/ element (addSmlNamespace "authors") &/ element (addSmlNamespace "author") >=> contentOrEmpty+ authors = M.fromList $ zip [0..] authorNames+ items = cur $/ element (addSmlNamespace "commentList") &/ element (addSmlNamespace "comment") >=> parseComment authors+ return . CommentTable $ M.fromList items++parseComment :: Map Int Text -> Cursor -> [(CellRef, Comment)]+parseComment authors cur = do+ ref <- fromAttribute "ref" cur+ txt <- cur $/ element (addSmlNamespace "text") >=> fromCursor+ authorId <- cur $| attribute "authorId" >=> decimal+ visible <- (read . Text.unpack :: Text -> Bool)+ <$> (fromAttribute "visible" cur :: [Text])+ let author = fromJustNote "authorId" $ M.lookup authorId authors+ return (ref, Comment txt author visible)++-- | helper to render comment baloons vml file,+-- currently uses fixed shape+renderShapes :: CommentTable -> ByteString+renderShapes (CommentTable m) = LB.concat+ [ "<xml xmlns:v=\"urn:schemas-microsoft-com:vml\" "+ , "xmlns:o=\"urn:schemas-microsoft-com:office:office\" "+ , "xmlns:x=\"urn:schemas-microsoft-com:office:excel\">"+ , commentShapeType+ , LB.concat commentShapes+ , "</xml>"+ ]+ where+ commentShapeType = LB.concat+ [ "<v:shapetype id=\"baloon\" coordsize=\"21600,21600\" o:spt=\"202\" "+ , "path=\"m,l,21600r21600,l21600,xe\">"+ , "<v:stroke joinstyle=\"miter\"></v:stroke>"+ , "<v:path gradientshapeok=\"t\" o:connecttype=\"rect\"></v:path>"+ , "</v:shapetype>"+ ]+ fromRef cr =+ fromJustNote ("Invalid comment ref: " <> show cr) $ fromSingleCellRef cr+ commentShapes = [ commentShape (fromRef ref) (_commentVisible cmnt)+ | (ref, cmnt) <- M.toList m ]+ commentShape (r, c) v = LB.concat+ [ "<v:shape type=\"#baloon\" "+ , "style=\"position:absolute;width:auto" -- ;width:108pt;height:59.25pt"+ , if v then "" else ";visibility:hidden"+ , "\" fillcolor=\"#ffffe1\" o:insetmode=\"auto\">"+ , "<v:fill color2=\"#ffffe1\"></v:fill><v:shadow color=\"black\" obscured=\"t\"></v:shadow>"+ , "<v:path o:connecttype=\"none\"></v:path><v:textbox style=\"mso-direction-alt:auto\">"+ , "<div style=\"text-align:left\"></div></v:textbox>"+ , "<x:ClientData ObjectType=\"Note\">"+ , "<x:MoveWithCells></x:MoveWithCells><x:SizeWithCells></x:SizeWithCells>"+ , "<x:Anchor>4, 15, 0, 7, 6, 31, 5, 1</x:Anchor><x:AutoFill>False</x:AutoFill>"+ , "<x:Row>"+ , LBC8.pack $ show (r - 1)+ , "</x:Row>"+ , "<x:Column>"+ , LBC8.pack $ show (c - 1)+ , "</x:Column>"+ , "</x:ClientData>"+ , "</v:shape>"+ ]
+ src/Codec/Xlsx/Types/Internal/ContentTypes.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.ContentTypes where++import Control.Arrow+import Data.Foldable (asum)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import System.FilePath.Posix (takeExtension)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal++data CtDefault = CtDefault+ { dfltExtension :: FilePath+ , dfltContentType :: Text+ } deriving (Eq, Show, Generic)++data Override = Override+ { ovrPartName :: FilePath+ , ovrContentType :: Text+ } deriving (Eq, Show, Generic)++data ContentTypes = ContentTypes+ { ctDefaults :: Map FilePath Text+ , ctTypes :: Map FilePath Text+ } deriving (Eq, Show, Generic)++lookup :: FilePath -> ContentTypes -> Maybe Text+lookup path ContentTypes{..} =+ asum [ flip M.lookup ctDefaults =<< ext, M.lookup path ctTypes ]+ where+ ext = case takeExtension path of+ '.':e -> Just e+ _ -> Nothing++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}+instance FromCursor ContentTypes where+ fromCursor cur = do+ let ds = M.fromList . map (dfltExtension &&& dfltContentType) $+ cur $/ element (ct"Default") >=> fromCursor+ ts = M.fromList . map (ovrPartName &&& ovrContentType) $+ cur $/ element (ct"Override") >=> fromCursor+ return (ContentTypes ds ts)++instance FromCursor CtDefault where+ fromCursor cur = do+ dfltExtension <- T.unpack <$> attribute "Extension" cur+ dfltContentType <- attribute "ContentType" cur+ return CtDefault{..}++instance FromCursor Override where+ fromCursor cur = do+ ovrPartName <- T.unpack <$> attribute "PartName" cur+ ovrContentType <- attribute "ContentType" cur+ return Override{..}++-- | Add package relationship namespace to name+ct :: Text -> Name+ct x = Name+ { nameLocalName = x+ , nameNamespace = Just contentTypesNs+ , namePrefix = Nothing+ }++contentTypesNs :: Text+contentTypesNs = "http://schemas.openxmlformats.org/package/2006/content-types"
+ src/Codec/Xlsx/Types/Internal/CustomProperties.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.CustomProperties where++import Data.Map (Map)+import qualified Data.Map as M+import Data.Text (Text)+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Variant+import Codec.Xlsx.Writer.Internal++newtype CustomProperties = CustomProperties (Map Text Variant)+ deriving (Eq, Show, Generic)++fromList :: [(Text, Variant)] -> CustomProperties+fromList = CustomProperties . M.fromList++empty :: CustomProperties+empty = CustomProperties M.empty++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor CustomProperties where+ fromCursor cur = do+ let items = cur $/ element (cpr"property") >=> parseCustomPropertyEntry+ return (fromList items)++parseCustomPropertyEntry :: Cursor -> [(Text, Variant)]+parseCustomPropertyEntry cur = do+ name <- attribute "name" cur+ value <- cur $/ anyElement >=> fromCursor+ return (name, value)++-- | Add custom properties namespace to name+cpr :: Text -> Name+cpr x = Name+ { nameLocalName = x+ , nameNamespace = Just custPropNs+ , namePrefix = Nothing+ }++custPropNs :: Text+custPropNs = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToDocument CustomProperties where+ toDocument =+ documentFromNsElement "Custom properties generated by xlsx"+ "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"+ . toElement "Properties"++instance ToElement CustomProperties where+ toElement nm (CustomProperties m) = Element+ { elementName = nm+ , elementAttributes = M.empty+ , elementNodes = map (NodeElement . toElement "property" . CustomProperty)+ . zip [2..] $ M.toList m+ }++newtype CustomProperty = CustomProperty (Int, (Text, Variant))++instance ToElement CustomProperty where+ toElement nm (CustomProperty (i, (key, val))) = Element+ { elementName = nm+ , elementAttributes = M.fromList [ "name" .= key+ , "fmtid" .= userDefinedFmtID+ , "pid" .= txti i ]+ , elementNodes = [ NodeElement $ variantToElement val ]+ }++-- | FMTID_UserDefinedProperties+userDefinedFmtID :: Text+userDefinedFmtID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
+ src/Codec/Xlsx/Types/Internal/DvPair.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.DvPair where++import qualified Data.Map as M+import GHC.Generics (Generic)+import Text.XML (Element(..))++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.DataValidation+import Codec.Xlsx.Writer.Internal++-- | Internal helper type for parsing data validation records+--+-- See 18.3.1.32 "dataValidation (Data Validation)" (p. 1614/1624)+newtype DvPair = DvPair+ { unDvPair :: (SqRef, DataValidation)+ } deriving (Eq, Show, Generic)++instance FromCursor DvPair where+ fromCursor cur = do+ sqref <- fromAttribute "sqref" cur+ dv <- fromCursor cur+ return $ DvPair (sqref, dv)++instance FromXenoNode DvPair where+ fromXenoNode root = do+ sqref <- parseAttributes root $ fromAttr "sqref"+ dv <- fromXenoNode root+ return $ DvPair (sqref, dv)++instance ToElement DvPair where+ toElement nm (DvPair (sqRef,dv)) = e+ {elementAttributes = M.insert "sqref" (toAttrVal sqRef) $ elementAttributes e}+ where+ e = toElement nm dv
+ src/Codec/Xlsx/Types/Internal/FormulaData.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+module Codec.Xlsx.Types.Internal.FormulaData where++import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Cell+import Codec.Xlsx.Types.Common++data FormulaData = FormulaData+ { frmdFormula :: CellFormula+ , frmdShared :: Maybe (SharedFormulaIndex, SharedFormulaOptions)+ } deriving Generic++defaultFormulaType :: Text+defaultFormulaType = "normal"++instance FromXenoNode FormulaData where+ fromXenoNode n = do+ (bx, ca, t, mSi, mRef) <-+ parseAttributes n $+ (,,,,) <$> fromAttrDef "bx" False+ <*> fromAttrDef "ca" False+ <*> fromAttrDef "t" defaultFormulaType+ <*> maybeAttr "si"+ <*> maybeAttr "ref"+ (expr, shared) <-+ case t of+ d | d == defaultFormulaType -> do+ formula <- contentX n+ return (NormalFormula $ Formula formula, Nothing)+ "shared" -> do+ si <-+ maybe+ (Left "missing si attribute for shared formula")+ return+ mSi+ formula <- Formula <$> contentX n+ return+ ( SharedFormula si+ , mRef >>= \ref -> return (si, (SharedFormulaOptions ref formula)))+ unexpected -> Left $ "Unexpected formula type" <> T.pack (show unexpected)+ let f =+ CellFormula+ { _cellfAssignsToName = bx+ , _cellfCalculate = ca+ , _cellfExpression = expr+ }+ return $ FormulaData f shared
+ src/Codec/Xlsx/Types/Internal/Relationships.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.Relationships where++import Data.List (find)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Network.URI hiding (path)+import Prelude hiding (abs, lookup)+import Safe+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Writer.Internal++data Relationship = Relationship+ { relType :: Text+ , relTarget :: FilePath+ } deriving (Eq, Show, Generic)++-- | Describes relationships according to Open Packaging Convention+--+-- See ECMA-376, 4th Edition Office Open XML File Formats — Open Packaging+-- Conventions+newtype Relationships = Relationships+ { relMap :: Map RefId Relationship+ } deriving (Eq, Show, Generic)++fromList :: [(RefId, Relationship)] -> Relationships+fromList = Relationships . Map.fromList++empty :: Relationships+empty = fromList []++size :: Relationships -> Int+size = Map.size . relMap++relEntry :: RefId -> Text -> FilePath -> (RefId, Relationship)+relEntry rId typ trg = (rId, Relationship (stdRelType typ) trg)++lookup :: RefId -> Relationships -> Maybe Relationship+lookup ref = Map.lookup ref . relMap++setTargetsFrom :: FilePath -> Relationships -> Relationships+setTargetsFrom fp (Relationships m) = Relationships (Map.map fixPath m)+ where+ fixPath rel = rel{ relTarget = fp `joinRel` relTarget rel}++-- | joins relative URI (actually a file path as an internal relation target)+joinRel :: FilePath -> FilePath -> FilePath+joinRel abs rel = uriToString id (relPath `nonStrictRelativeTo` base) ""+ where+ base = fromJustNote "joinRel base path" $ parseURIReference abs+ relPath = fromJustNote "joinRel relative path" $ parseURIReference rel++relFrom :: FilePath -> FilePath -> FilePath+relFrom path base = uriToString id (pathURI `relativeFrom` baseURI) ""+ where+ baseURI = fromJustNote "joinRel base path" $ parseURIReference base+ pathURI = fromJustNote "joinRel relative path" $ parseURIReference path++findRelByType :: Text -> Relationships -> Maybe Relationship+findRelByType t (Relationships m) = find ((==t) . relType) (Map.elems m)++allByType :: Text -> Relationships -> [Relationship]+allByType t (Relationships m) = filter ((==t) . relType) (Map.elems m)++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToDocument Relationships where+ toDocument = documentFromNsElement "Relationships generated by xlsx" pkgRelNs+ . toElement "Relationships"++instance ToElement Relationships where+ toElement nm Relationships{..} = Element+ { elementName = nm+ , elementAttributes = Map.empty+ , elementNodes = map (NodeElement . relToEl "Relationship") $+ Map.toList relMap+ }+ where+ relToEl nm' (relId, rel) = setAttr "Id" relId (toElement nm' rel)++instance ToElement Relationship where+ toElement nm Relationship{..} = Element+ { elementName = nm+ , elementAttributes = Map.fromList [ "Target" .= relTarget+ , "Type" .= relType ]+ , elementNodes = []+ }++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}+instance FromCursor Relationships where+ fromCursor cur = do+ let items = cur $/ element (pr"Relationship") >=> parseRelEntry+ return . Relationships $ Map.fromList items++parseRelEntry :: Cursor -> [(RefId, Relationship)]+parseRelEntry cur = do+ rel <- fromCursor cur+ rId <- attribute "Id" cur+ return (RefId rId, rel)++instance FromCursor Relationship where+ fromCursor cur = do+ ty <- attribute "Type" cur+ trg <- T.unpack <$> attribute "Target" cur+ return $ Relationship ty trg++-- | Add package relationship namespace to name+pr :: Text -> Name+pr x = Name+ { nameLocalName = x+ , nameNamespace = Just pkgRelNs+ , namePrefix = Nothing+ }++-- | Add office document relationship namespace to name+odr :: Text -> Name+odr x = Name+ { nameLocalName = x+ , nameNamespace = Just odRelNs+ , namePrefix = Nothing+ }++odRelNs :: Text+odRelNs = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"++pkgRelNs :: Text+pkgRelNs = "http://schemas.openxmlformats.org/package/2006/relationships"++stdRelType :: Text -> Text+stdRelType t = stdPart <> t+ where+ stdPart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/"
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Internal.SharedStringTable (+ -- * Main types+ SharedStringTable(..)+ , sstConstruct+ , sstLookupText+ , sstLookupRich+ , sstItem+ , sstEmpty+ ) where++import Control.Monad+import qualified Data.Map as Map+import Data.Maybe (mapMaybe)+import qualified Data.Set as Set+import Data.Text (Text)+import Data.Vector (Vector)+import qualified Data.Vector as V+import GHC.Generics (Generic)+import Numeric.Search.Range (searchFromTo)+import Safe (fromJustNote)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types+import Codec.Xlsx.Writer.Internal++-- | Shared string table+--+-- A workbook can contain thousands of cells containing string (non-numeric)+-- data. Furthermore this data is very likely to be repeated across many rows or+-- columns. The goal of implementing a single string table that is shared across+-- the workbook is to improve performance in opening and saving the file by only+-- reading and writing the repetitive information once.+--+-- Relevant parts of the EMCA standard (2nd edition, part 1,+-- <https://ecma-international.org/publications-and-standards/standards/ecma-376/>),+-- page numbers refer to the page in the PDF rather than the page number as+-- printed on the page):+--+-- * Section 18.4, "Shared String Table" (p. 1712)+-- in particular subsection 18.4.9, "sst (Shared String Table)" (p. 1726)+--+-- TODO: The @extLst@ child element is currently unsupported.+newtype SharedStringTable = SharedStringTable {+ sstTable :: Vector XlsxText+ }+ deriving (Eq, Ord, Show, Generic)++sstEmpty :: SharedStringTable+sstEmpty = SharedStringTable V.empty++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToDocument SharedStringTable where+ toDocument = documentFromElement "Shared string table generated by xlsx"+ . toElement "sst"++-- | See @CT_Sst@, p. 3902.+--+-- TODO: The @count@ and @uniqCount@ attributes are currently unsupported.+instance ToElement SharedStringTable where+ toElement nm SharedStringTable{..} = Element {+ elementName = nm+ , elementAttributes = Map.empty+ , elementNodes = map (NodeElement . toElement "si")+ $ V.toList sstTable+ }++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++-- | See @CT_Sst@, p. 3902+--+-- The optional attributes @count@ and @uniqCount@ are being ignored at least currently+instance FromCursor SharedStringTable where+ fromCursor cur = do+ let+ items = cur $/ element (addSmlNamespace "si") >=> fromCursor+ return (SharedStringTable (V.fromList items))++{-------------------------------------------------------------------------------+ Extract shared strings+-------------------------------------------------------------------------------}++-- | Construct the 'SharedStringsTable' from an existing document+sstConstruct :: [Worksheet] -> SharedStringTable+sstConstruct =+ SharedStringTable . V.fromList . uniq . concatMap goSheet+ where+ goSheet :: Worksheet -> [XlsxText]+ goSheet = mapMaybe (_cellValue >=> sstEntry) . Map.elems . _wsCells++ sstEntry :: CellValue -> Maybe XlsxText+ sstEntry (CellText text) = Just $ XlsxText text+ sstEntry (CellRich rich) = Just $ XlsxRichText rich+ sstEntry _ = Nothing++ uniq :: Ord a => [a] -> [a]+ uniq = Set.elems . Set.fromList++sstLookupText :: SharedStringTable -> Text -> Int+sstLookupText sst = sstLookup sst . XlsxText++sstLookupRich :: SharedStringTable -> [RichTextRun] -> Int+sstLookupRich sst = sstLookup sst . XlsxRichText++-- | Internal generalization used by 'sstLookupText' and 'sstLookupRich'+sstLookup :: SharedStringTable -> XlsxText -> Int+sstLookup SharedStringTable{sstTable = shared} si =+ fromJustNote ("SST entry for " ++ show si ++ " not found") $+ searchFromTo (\p -> shared V.! p >= si) 0 (V.length shared - 1)++sstItem :: SharedStringTable -> Int -> Maybe XlsxText+sstItem (SharedStringTable shared) = (V.!?) shared
+ src/Codec/Xlsx/Types/PageSetup.hs view
@@ -0,0 +1,670 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.PageSetup (+ -- * Main types+ PageSetup(..)+ -- * Enumerations+ , CellComments(..)+ , PrintErrors(..)+ , Orientation(..)+ , PageOrder(..)+ , PaperSize(..)+ -- * Lenses+ -- ** PageSetup+ , pageSetupBlackAndWhite+ , pageSetupCellComments+ , pageSetupCopies+ , pageSetupDraft+ , pageSetupErrors+ , pageSetupFirstPageNumber+ , pageSetupFitToHeight+ , pageSetupFitToWidth+ , pageSetupHorizontalDpi+ , pageSetupId+ , pageSetupOrientation+ , pageSetupPageOrder+ , pageSetupPaperHeight+ , pageSetupPaperSize+ , pageSetupPaperWidth+ , pageSetupScale+ , pageSetupUseFirstPageNumber+ , pageSetupUsePrinterDefaults+ , pageSetupVerticalDpi+ ) where++import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Default+import qualified Data.Map as Map+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Text.XML++import Codec.Xlsx.Writer.Internal+import Codec.Xlsx.Parser.Internal++{-------------------------------------------------------------------------------+ Main types+-------------------------------------------------------------------------------}++data PageSetup = PageSetup {+ -- | Print black and white.+ _pageSetupBlackAndWhite :: Maybe Bool++ -- | This attribute specifies how to print cell comments.+ , _pageSetupCellComments :: Maybe CellComments++ -- | Number of copies to print.+ , _pageSetupCopies :: Maybe Int++ -- | Print without graphics.+ , _pageSetupDraft :: Maybe Bool++ -- | Specifies how to print cell values for cells with errors.+ , _pageSetupErrors :: Maybe PrintErrors++ -- | Page number for first printed page. If no value is specified, then+ -- 'automatic' is assumed.+ , _pageSetupFirstPageNumber :: Maybe Int++ -- | Number of vertical pages to fit on.+ , _pageSetupFitToHeight :: Maybe Int++ -- | Number of horizontal pages to fit on.+ , _pageSetupFitToWidth :: Maybe Int++ -- | Horizontal print resolution of the device.+ , _pageSetupHorizontalDpi :: Maybe Int++ -- | Relationship Id of the devMode printer settings part.+ --+ -- (Explicit reference to a parent XML element.)+ --+ -- See 22.8.2.1 "ST_RelationshipId (Explicit Relationship ID)" (p. 3784)+ , _pageSetupId :: Maybe Text++ -- | Orientation of the page.+ , _pageSetupOrientation :: Maybe Orientation++ -- | Order of printed pages+ , _pageSetupPageOrder :: Maybe PageOrder++ -- | Height of custom paper as a number followed by a unit identifier.+ --+ -- When paperHeight and paperWidth are specified, paperSize shall be ignored.+ -- Examples: @"297mm"@, @"11in"@.+ --+ -- See 22.9.2.12 "ST_PositiveUniversalMeasure (Positive Universal Measurement)" (p. 3792)+ , _pageSetupPaperHeight :: Maybe Text++ -- | Pager size+ --+ -- When paperHeight, paperWidth, and paperUnits are specified, paperSize+ -- should be ignored.+ , _pageSetupPaperSize :: Maybe PaperSize++ -- | Width of custom paper as a number followed by a unit identifier+ --+ -- Examples: @21cm@, @8.5in@+ --+ -- When paperHeight and paperWidth are specified, paperSize shall be+ -- ignored.+ , _pageSetupPaperWidth :: Maybe Text++ -- | Print scaling.+ --+ -- This attribute is restricted to values ranging from 10 to 400.+ -- This setting is overridden when fitToWidth and/or fitToHeight are in+ -- use.+ , _pageSetupScale :: Maybe Int++ -- | Use '_pageSetupFirstPageNumber' value for first page number, and do+ -- not auto number the pages.+ , _pageSetupUseFirstPageNumber :: Maybe Bool++ -- | Use the printer’s defaults settings for page setup values and don't+ -- use the default values specified in the schema.+ --+ -- Example: If dpi is not present or specified in the XML, the application+ -- must not assume 600dpi as specified in the schema as a default and+ -- instead must let the printer specify the default dpi.+ , _pageSetupUsePrinterDefaults :: Maybe Bool++ -- | Vertical print resolution of the device.+ , _pageSetupVerticalDpi :: Maybe Int+ }+ deriving (Eq, Ord, Show, Generic)+instance NFData PageSetup++{-------------------------------------------------------------------------------+ Enumerations+-------------------------------------------------------------------------------}++-- | Cell comments+--+-- These enumerations specify how cell comments shall be displayed for paper+-- printing purposes.+--+-- See 18.18.5 "ST_CellComments (Cell Comments)" (p. 2441).+data CellComments =+ -- | Print cell comments as displayed+ CellCommentsAsDisplayed++ -- | Print cell comments at end of document+ | CellCommentsAtEnd++ -- | Do not print cell comments+ | CellCommentsNone+ deriving (Eq, Ord, Show, Generic)+instance NFData CellComments++-- | Print errors+--+-- This enumeration specifies how to display cells with errors when printing the+-- worksheet.+data PrintErrors =+ -- | Display cell errors as blank+ PrintErrorsBlank++ -- | Display cell errors as dashes+ | PrintErrorsDash++ -- | Display cell errors as displayed on screen+ | PrintErrorsDisplayed++ -- | Display cell errors as @#N/A@+ | PrintErrorsNA+ deriving (Eq, Ord, Show, Generic)+instance NFData PrintErrors++-- | Print orientation for this sheet+data Orientation =+ OrientationDefault+ | OrientationLandscape+ | OrientationPortrait+ deriving (Eq, Ord, Show, Generic)+instance NFData Orientation++-- | Specifies printed page order+data PageOrder =+ -- | Order pages vertically first, then move horizontally+ PageOrderDownThenOver++ -- | Order pages horizontally first, then move vertically+ | PageOrderOverThenDown+ deriving (Eq, Ord, Show, Generic)+instance NFData PageOrder++-- | Paper size+data PaperSize =+ PaperA2 -- ^ A2 paper (420 mm by 594 mm)+ | PaperA3 -- ^ A3 paper (297 mm by 420 mm)+ | PaperA3Extra -- ^ A3 extra paper (322 mm by 445 mm)+ | PaperA3ExtraTransverse -- ^ A3 extra transverse paper (322 mm by 445 mm)+ | PaperA3Transverse -- ^ A3 transverse paper (297 mm by 420 mm)+ | PaperA4 -- ^ A4 paper (210 mm by 297 mm)+ | PaperA4Extra -- ^ A4 extra paper (236 mm by 322 mm)+ | PaperA4Plus -- ^ A4 plus paper (210 mm by 330 mm)+ | PaperA4Small -- ^ A4 small paper (210 mm by 297 mm)+ | PaperA4Transverse -- ^ A4 transverse paper (210 mm by 297 mm)+ | PaperA5 -- ^ A5 paper (148 mm by 210 mm)+ | PaperA5Extra -- ^ A5 extra paper (174 mm by 235 mm)+ | PaperA5Transverse -- ^ A5 transverse paper (148 mm by 210 mm)+ | PaperB4 -- ^ B4 paper (250 mm by 353 mm)+ | PaperB5 -- ^ B5 paper (176 mm by 250 mm)+ | PaperC -- ^ C paper (17 in. by 22 in.)+ | PaperD -- ^ D paper (22 in. by 34 in.)+ | PaperE -- ^ E paper (34 in. by 44 in.)+ | PaperExecutive -- ^ Executive paper (7.25 in. by 10.5 in.)+ | PaperFanfoldGermanLegal -- ^ German legal fanfold (8.5 in. by 13 in.)+ | PaperFanfoldGermanStandard -- ^ German standard fanfold (8.5 in. by 12 in.)+ | PaperFanfoldUsStandard -- ^ US standard fanfold (14.875 in. by 11 in.)+ | PaperFolio -- ^ Folio paper (8.5 in. by 13 in.)+ | PaperIsoB4 -- ^ ISO B4 (250 mm by 353 mm)+ | PaperIsoB5Extra -- ^ ISO B5 extra paper (201 mm by 276 mm)+ | PaperJapaneseDoublePostcard -- ^ Japanese double postcard (200 mm by 148 mm)+ | PaperJisB5Transverse -- ^ JIS B5 transverse paper (182 mm by 257 mm)+ | PaperLedger -- ^ Ledger paper (17 in. by 11 in.)+ | PaperLegal -- ^ Legal paper (8.5 in. by 14 in.)+ | PaperLegalExtra -- ^ Legal extra paper (9.275 in. by 15 in.)+ | PaperLetter -- ^ Letter paper (8.5 in. by 11 in.)+ | PaperLetterExtra -- ^ Letter extra paper (9.275 in. by 12 in.)+ | PaperLetterExtraTransverse -- ^ Letter extra transverse paper (9.275 in. by 12 in.)+ | PaperLetterPlus -- ^ Letter plus paper (8.5 in. by 12.69 in.)+ | PaperLetterSmall -- ^ Letter small paper (8.5 in. by 11 in.)+ | PaperLetterTransverse -- ^ Letter transverse paper (8.275 in. by 11 in.)+ | PaperNote -- ^ Note paper (8.5 in. by 11 in.)+ | PaperQuarto -- ^ Quarto paper (215 mm by 275 mm)+ | PaperStandard9_11 -- ^ Standard paper (9 in. by 11 in.)+ | PaperStandard10_11 -- ^ Standard paper (10 in. by 11 in.)+ | PaperStandard10_14 -- ^ Standard paper (10 in. by 14 in.)+ | PaperStandard11_17 -- ^ Standard paper (11 in. by 17 in.)+ | PaperStandard15_11 -- ^ Standard paper (15 in. by 11 in.)+ | PaperStatement -- ^ Statement paper (5.5 in. by 8.5 in.)+ | PaperSuperA -- ^ SuperA/SuperA/A4 paper (227 mm by 356 mm)+ | PaperSuperB -- ^ SuperB/SuperB/A3 paper (305 mm by 487 mm)+ | PaperTabloid -- ^ Tabloid paper (11 in. by 17 in.)+ | PaperTabloidExtra -- ^ Tabloid extra paper (11.69 in. by 18 in.)+ | Envelope6_3_4 -- ^ 6 3/4 envelope (3.625 in. by 6.5 in.)+ | Envelope9 -- ^ #9 envelope (3.875 in. by 8.875 in.)+ | Envelope10 -- ^ #10 envelope (4.125 in. by 9.5 in.)+ | Envelope11 -- ^ #11 envelope (4.5 in. by 10.375 in.)+ | Envelope12 -- ^ #12 envelope (4.75 in. by 11 in.)+ | Envelope14 -- ^ #14 envelope (5 in. by 11.5 in.)+ | EnvelopeB4 -- ^ B4 envelope (250 mm by 353 mm)+ | EnvelopeB5 -- ^ B5 envelope (176 mm by 250 mm)+ | EnvelopeB6 -- ^ B6 envelope (176 mm by 125 mm)+ | EnvelopeC3 -- ^ C3 envelope (324 mm by 458 mm)+ | EnvelopeC4 -- ^ C4 envelope (229 mm by 324 mm)+ | EnvelopeC5 -- ^ C5 envelope (162 mm by 229 mm)+ | EnvelopeC6 -- ^ C6 envelope (114 mm by 162 mm)+ | EnvelopeC65 -- ^ C65 envelope (114 mm by 229 mm)+ | EnvelopeDL -- ^ DL envelope (110 mm by 220 mm)+ | EnvelopeInvite -- ^ Invite envelope (220 mm by 220 mm)+ | EnvelopeItaly -- ^ Italy envelope (110 mm by 230 mm)+ | EnvelopeMonarch -- ^ Monarch envelope (3.875 in. by 7.5 in.).+ deriving (Eq, Ord, Show, Generic)+instance NFData PaperSize++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default PageSetup where+ def = PageSetup {+ _pageSetupBlackAndWhite = Nothing+ , _pageSetupCellComments = Nothing+ , _pageSetupCopies = Nothing+ , _pageSetupDraft = Nothing+ , _pageSetupErrors = Nothing+ , _pageSetupFirstPageNumber = Nothing+ , _pageSetupFitToHeight = Nothing+ , _pageSetupFitToWidth = Nothing+ , _pageSetupHorizontalDpi = Nothing+ , _pageSetupId = Nothing+ , _pageSetupOrientation = Nothing+ , _pageSetupPageOrder = Nothing+ , _pageSetupPaperHeight = Nothing+ , _pageSetupPaperSize = Nothing+ , _pageSetupPaperWidth = Nothing+ , _pageSetupScale = Nothing+ , _pageSetupUseFirstPageNumber = Nothing+ , _pageSetupUsePrinterDefaults = Nothing+ , _pageSetupVerticalDpi = Nothing+ }++{-------------------------------------------------------------------------------+ Lenses+-------------------------------------------------------------------------------}++makeLenses ''PageSetup++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++-- | See @CT_PageSetup@, p. 3922+instance ToElement PageSetup where+ toElement nm PageSetup{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = Map.fromList . catMaybes $ [+ "paperSize" .=? _pageSetupPaperSize+ , "paperHeight" .=? _pageSetupPaperHeight+ , "paperWidth" .=? _pageSetupPaperWidth+ , "scale" .=? _pageSetupScale+ , "firstPageNumber" .=? _pageSetupFirstPageNumber+ , "fitToWidth" .=? _pageSetupFitToWidth+ , "fitToHeight" .=? _pageSetupFitToHeight+ , "pageOrder" .=? _pageSetupPageOrder+ , "orientation" .=? _pageSetupOrientation+ , "usePrinterDefaults" .=? _pageSetupUsePrinterDefaults+ , "blackAndWhite" .=? _pageSetupBlackAndWhite+ , "draft" .=? _pageSetupDraft+ , "cellComments" .=? _pageSetupCellComments+ , "useFirstPageNumber" .=? _pageSetupUseFirstPageNumber+ , "errors" .=? _pageSetupErrors+ , "horizontalDpi" .=? _pageSetupHorizontalDpi+ , "verticalDpi" .=? _pageSetupVerticalDpi+ , "copies" .=? _pageSetupCopies+ , "id" .=? _pageSetupId+ ]+ }++-- | See @ST_CellComments@, p. 3923+instance ToAttrVal CellComments where+ toAttrVal CellCommentsNone = "none"+ toAttrVal CellCommentsAsDisplayed = "asDisplayed"+ toAttrVal CellCommentsAtEnd = "atEnd"++-- | See @ST_PrintError@, p. 3923+instance ToAttrVal PrintErrors where+ toAttrVal PrintErrorsDisplayed = "displayed"+ toAttrVal PrintErrorsBlank = "blank"+ toAttrVal PrintErrorsDash = "dash"+ toAttrVal PrintErrorsNA = "NA"++-- | See @ST_Orientation@, p. 3923+instance ToAttrVal Orientation where+ toAttrVal OrientationDefault = "default"+ toAttrVal OrientationPortrait = "portrait"+ toAttrVal OrientationLandscape = "landscape"++-- | See @ST_PageOrder@, p. 3923+instance ToAttrVal PageOrder where+ toAttrVal PageOrderDownThenOver = "downThenOver"+ toAttrVal PageOrderOverThenDown = "overThenDown"++-- | See @paperSize@ (attribute of @pageSetup@), p. 1659+instance ToAttrVal PaperSize where+ toAttrVal PaperLetter = "1"+ toAttrVal PaperLetterSmall = "2"+ toAttrVal PaperTabloid = "3"+ toAttrVal PaperLedger = "4"+ toAttrVal PaperLegal = "5"+ toAttrVal PaperStatement = "6"+ toAttrVal PaperExecutive = "7"+ toAttrVal PaperA3 = "8"+ toAttrVal PaperA4 = "9"+ toAttrVal PaperA4Small = "10"+ toAttrVal PaperA5 = "11"+ toAttrVal PaperB4 = "12"+ toAttrVal PaperB5 = "13"+ toAttrVal PaperFolio = "14"+ toAttrVal PaperQuarto = "15"+ toAttrVal PaperStandard10_14 = "16"+ toAttrVal PaperStandard11_17 = "17"+ toAttrVal PaperNote = "18"+ toAttrVal Envelope9 = "19"+ toAttrVal Envelope10 = "20"+ toAttrVal Envelope11 = "21"+ toAttrVal Envelope12 = "22"+ toAttrVal Envelope14 = "23"+ toAttrVal PaperC = "24"+ toAttrVal PaperD = "25"+ toAttrVal PaperE = "26"+ toAttrVal EnvelopeDL = "27"+ toAttrVal EnvelopeC5 = "28"+ toAttrVal EnvelopeC3 = "29"+ toAttrVal EnvelopeC4 = "30"+ toAttrVal EnvelopeC6 = "31"+ toAttrVal EnvelopeC65 = "32"+ toAttrVal EnvelopeB4 = "33"+ toAttrVal EnvelopeB5 = "34"+ toAttrVal EnvelopeB6 = "35"+ toAttrVal EnvelopeItaly = "36"+ toAttrVal EnvelopeMonarch = "37"+ toAttrVal Envelope6_3_4 = "38"+ toAttrVal PaperFanfoldUsStandard = "39"+ toAttrVal PaperFanfoldGermanStandard = "40"+ toAttrVal PaperFanfoldGermanLegal = "41"+ toAttrVal PaperIsoB4 = "42"+ toAttrVal PaperJapaneseDoublePostcard = "43"+ toAttrVal PaperStandard9_11 = "44"+ toAttrVal PaperStandard10_11 = "45"+ toAttrVal PaperStandard15_11 = "46"+ toAttrVal EnvelopeInvite = "47"+ toAttrVal PaperLetterExtra = "50"+ toAttrVal PaperLegalExtra = "51"+ toAttrVal PaperTabloidExtra = "52"+ toAttrVal PaperA4Extra = "53"+ toAttrVal PaperLetterTransverse = "54"+ toAttrVal PaperA4Transverse = "55"+ toAttrVal PaperLetterExtraTransverse = "56"+ toAttrVal PaperSuperA = "57"+ toAttrVal PaperSuperB = "58"+ toAttrVal PaperLetterPlus = "59"+ toAttrVal PaperA4Plus = "60"+ toAttrVal PaperA5Transverse = "61"+ toAttrVal PaperJisB5Transverse = "62"+ toAttrVal PaperA3Extra = "63"+ toAttrVal PaperA5Extra = "64"+ toAttrVal PaperIsoB5Extra = "65"+ toAttrVal PaperA2 = "66"+ toAttrVal PaperA3Transverse = "67"+ toAttrVal PaperA3ExtraTransverse = "68"++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}+-- | See @CT_PageSetup@, p. 3922+instance FromCursor PageSetup where+ fromCursor cur = do+ _pageSetupPaperSize <- maybeAttribute "paperSize" cur+ _pageSetupPaperHeight <- maybeAttribute "paperHeight" cur+ _pageSetupPaperWidth <- maybeAttribute "paperWidth" cur+ _pageSetupScale <- maybeAttribute "scale" cur+ _pageSetupFirstPageNumber <- maybeAttribute "firstPageNumber" cur+ _pageSetupFitToWidth <- maybeAttribute "fitToWidth" cur+ _pageSetupFitToHeight <- maybeAttribute "fitToHeight" cur+ _pageSetupPageOrder <- maybeAttribute "pageOrder" cur+ _pageSetupOrientation <- maybeAttribute "orientation" cur+ _pageSetupUsePrinterDefaults <- maybeAttribute "usePrinterDefaults" cur+ _pageSetupBlackAndWhite <- maybeAttribute "blackAndWhite" cur+ _pageSetupDraft <- maybeAttribute "draft" cur+ _pageSetupCellComments <- maybeAttribute "cellComments" cur+ _pageSetupUseFirstPageNumber <- maybeAttribute "useFirstPageNumber" cur+ _pageSetupErrors <- maybeAttribute "errors" cur+ _pageSetupHorizontalDpi <- maybeAttribute "horizontalDpi" cur+ _pageSetupVerticalDpi <- maybeAttribute "verticalDpi" cur+ _pageSetupCopies <- maybeAttribute "copies" cur+ _pageSetupId <- maybeAttribute "id" cur+ return PageSetup{..}++instance FromXenoNode PageSetup where+ fromXenoNode root =+ parseAttributes root $ do+ _pageSetupPaperSize <- maybeAttr "paperSize"+ _pageSetupPaperHeight <- maybeAttr "paperHeight"+ _pageSetupPaperWidth <- maybeAttr "paperWidth"+ _pageSetupScale <- maybeAttr "scale"+ _pageSetupFirstPageNumber <- maybeAttr "firstPageNumber"+ _pageSetupFitToWidth <- maybeAttr "fitToWidth"+ _pageSetupFitToHeight <- maybeAttr "fitToHeight"+ _pageSetupPageOrder <- maybeAttr "pageOrder"+ _pageSetupOrientation <- maybeAttr "orientation"+ _pageSetupUsePrinterDefaults <- maybeAttr "usePrinterDefaults"+ _pageSetupBlackAndWhite <- maybeAttr "blackAndWhite"+ _pageSetupDraft <- maybeAttr "draft"+ _pageSetupCellComments <- maybeAttr "cellComments"+ _pageSetupUseFirstPageNumber <- maybeAttr "useFirstPageNumber"+ _pageSetupErrors <- maybeAttr "errors"+ _pageSetupHorizontalDpi <- maybeAttr "horizontalDpi"+ _pageSetupVerticalDpi <- maybeAttr "verticalDpi"+ _pageSetupCopies <- maybeAttr "copies"+ _pageSetupId <- maybeAttr "id"+ return PageSetup {..}++-- | See @paperSize@ (attribute of @pageSetup@), p. 1659+instance FromAttrVal PaperSize where+ fromAttrVal "1" = readSuccess PaperLetter+ fromAttrVal "2" = readSuccess PaperLetterSmall+ fromAttrVal "3" = readSuccess PaperTabloid+ fromAttrVal "4" = readSuccess PaperLedger+ fromAttrVal "5" = readSuccess PaperLegal+ fromAttrVal "6" = readSuccess PaperStatement+ fromAttrVal "7" = readSuccess PaperExecutive+ fromAttrVal "8" = readSuccess PaperA3+ fromAttrVal "9" = readSuccess PaperA4+ fromAttrVal "10" = readSuccess PaperA4Small+ fromAttrVal "11" = readSuccess PaperA5+ fromAttrVal "12" = readSuccess PaperB4+ fromAttrVal "13" = readSuccess PaperB5+ fromAttrVal "14" = readSuccess PaperFolio+ fromAttrVal "15" = readSuccess PaperQuarto+ fromAttrVal "16" = readSuccess PaperStandard10_14+ fromAttrVal "17" = readSuccess PaperStandard11_17+ fromAttrVal "18" = readSuccess PaperNote+ fromAttrVal "19" = readSuccess Envelope9+ fromAttrVal "20" = readSuccess Envelope10+ fromAttrVal "21" = readSuccess Envelope11+ fromAttrVal "22" = readSuccess Envelope12+ fromAttrVal "23" = readSuccess Envelope14+ fromAttrVal "24" = readSuccess PaperC+ fromAttrVal "25" = readSuccess PaperD+ fromAttrVal "26" = readSuccess PaperE+ fromAttrVal "27" = readSuccess EnvelopeDL+ fromAttrVal "28" = readSuccess EnvelopeC5+ fromAttrVal "29" = readSuccess EnvelopeC3+ fromAttrVal "30" = readSuccess EnvelopeC4+ fromAttrVal "31" = readSuccess EnvelopeC6+ fromAttrVal "32" = readSuccess EnvelopeC65+ fromAttrVal "33" = readSuccess EnvelopeB4+ fromAttrVal "34" = readSuccess EnvelopeB5+ fromAttrVal "35" = readSuccess EnvelopeB6+ fromAttrVal "36" = readSuccess EnvelopeItaly+ fromAttrVal "37" = readSuccess EnvelopeMonarch+ fromAttrVal "38" = readSuccess Envelope6_3_4+ fromAttrVal "39" = readSuccess PaperFanfoldUsStandard+ fromAttrVal "40" = readSuccess PaperFanfoldGermanStandard+ fromAttrVal "41" = readSuccess PaperFanfoldGermanLegal+ fromAttrVal "42" = readSuccess PaperIsoB4+ fromAttrVal "43" = readSuccess PaperJapaneseDoublePostcard+ fromAttrVal "44" = readSuccess PaperStandard9_11+ fromAttrVal "45" = readSuccess PaperStandard10_11+ fromAttrVal "46" = readSuccess PaperStandard15_11+ fromAttrVal "47" = readSuccess EnvelopeInvite+ fromAttrVal "50" = readSuccess PaperLetterExtra+ fromAttrVal "51" = readSuccess PaperLegalExtra+ fromAttrVal "52" = readSuccess PaperTabloidExtra+ fromAttrVal "53" = readSuccess PaperA4Extra+ fromAttrVal "54" = readSuccess PaperLetterTransverse+ fromAttrVal "55" = readSuccess PaperA4Transverse+ fromAttrVal "56" = readSuccess PaperLetterExtraTransverse+ fromAttrVal "57" = readSuccess PaperSuperA+ fromAttrVal "58" = readSuccess PaperSuperB+ fromAttrVal "59" = readSuccess PaperLetterPlus+ fromAttrVal "60" = readSuccess PaperA4Plus+ fromAttrVal "61" = readSuccess PaperA5Transverse+ fromAttrVal "62" = readSuccess PaperJisB5Transverse+ fromAttrVal "63" = readSuccess PaperA3Extra+ fromAttrVal "64" = readSuccess PaperA5Extra+ fromAttrVal "65" = readSuccess PaperIsoB5Extra+ fromAttrVal "66" = readSuccess PaperA2+ fromAttrVal "67" = readSuccess PaperA3Transverse+ fromAttrVal "68" = readSuccess PaperA3ExtraTransverse+ fromAttrVal t = invalidText "PaperSize" t++instance FromAttrBs PaperSize where+ fromAttrBs "1" = return PaperLetter+ fromAttrBs "2" = return PaperLetterSmall+ fromAttrBs "3" = return PaperTabloid+ fromAttrBs "4" = return PaperLedger+ fromAttrBs "5" = return PaperLegal+ fromAttrBs "6" = return PaperStatement+ fromAttrBs "7" = return PaperExecutive+ fromAttrBs "8" = return PaperA3+ fromAttrBs "9" = return PaperA4+ fromAttrBs "10" = return PaperA4Small+ fromAttrBs "11" = return PaperA5+ fromAttrBs "12" = return PaperB4+ fromAttrBs "13" = return PaperB5+ fromAttrBs "14" = return PaperFolio+ fromAttrBs "15" = return PaperQuarto+ fromAttrBs "16" = return PaperStandard10_14+ fromAttrBs "17" = return PaperStandard11_17+ fromAttrBs "18" = return PaperNote+ fromAttrBs "19" = return Envelope9+ fromAttrBs "20" = return Envelope10+ fromAttrBs "21" = return Envelope11+ fromAttrBs "22" = return Envelope12+ fromAttrBs "23" = return Envelope14+ fromAttrBs "24" = return PaperC+ fromAttrBs "25" = return PaperD+ fromAttrBs "26" = return PaperE+ fromAttrBs "27" = return EnvelopeDL+ fromAttrBs "28" = return EnvelopeC5+ fromAttrBs "29" = return EnvelopeC3+ fromAttrBs "30" = return EnvelopeC4+ fromAttrBs "31" = return EnvelopeC6+ fromAttrBs "32" = return EnvelopeC65+ fromAttrBs "33" = return EnvelopeB4+ fromAttrBs "34" = return EnvelopeB5+ fromAttrBs "35" = return EnvelopeB6+ fromAttrBs "36" = return EnvelopeItaly+ fromAttrBs "37" = return EnvelopeMonarch+ fromAttrBs "38" = return Envelope6_3_4+ fromAttrBs "39" = return PaperFanfoldUsStandard+ fromAttrBs "40" = return PaperFanfoldGermanStandard+ fromAttrBs "41" = return PaperFanfoldGermanLegal+ fromAttrBs "42" = return PaperIsoB4+ fromAttrBs "43" = return PaperJapaneseDoublePostcard+ fromAttrBs "44" = return PaperStandard9_11+ fromAttrBs "45" = return PaperStandard10_11+ fromAttrBs "46" = return PaperStandard15_11+ fromAttrBs "47" = return EnvelopeInvite+ fromAttrBs "50" = return PaperLetterExtra+ fromAttrBs "51" = return PaperLegalExtra+ fromAttrBs "52" = return PaperTabloidExtra+ fromAttrBs "53" = return PaperA4Extra+ fromAttrBs "54" = return PaperLetterTransverse+ fromAttrBs "55" = return PaperA4Transverse+ fromAttrBs "56" = return PaperLetterExtraTransverse+ fromAttrBs "57" = return PaperSuperA+ fromAttrBs "58" = return PaperSuperB+ fromAttrBs "59" = return PaperLetterPlus+ fromAttrBs "60" = return PaperA4Plus+ fromAttrBs "61" = return PaperA5Transverse+ fromAttrBs "62" = return PaperJisB5Transverse+ fromAttrBs "63" = return PaperA3Extra+ fromAttrBs "64" = return PaperA5Extra+ fromAttrBs "65" = return PaperIsoB5Extra+ fromAttrBs "66" = return PaperA2+ fromAttrBs "67" = return PaperA3Transverse+ fromAttrBs "68" = return PaperA3ExtraTransverse+ fromAttrBs x = unexpectedAttrBs "PaperSize" x++-- | See @ST_PageOrder@, p. 3923+instance FromAttrVal PageOrder where+ fromAttrVal "downThenOver" = readSuccess PageOrderDownThenOver+ fromAttrVal "overThenDown" = readSuccess PageOrderOverThenDown+ fromAttrVal t = invalidText "PageOrder" t++instance FromAttrBs PageOrder where+ fromAttrBs "downThenOver" = return PageOrderDownThenOver+ fromAttrBs "overThenDown" = return PageOrderOverThenDown+ fromAttrBs x = unexpectedAttrBs "PageOrder" x++-- | See @ST_CellComments@, p. 3923+instance FromAttrVal CellComments where+ fromAttrVal "none" = readSuccess CellCommentsNone+ fromAttrVal "asDisplayed" = readSuccess CellCommentsAsDisplayed+ fromAttrVal "atEnd" = readSuccess CellCommentsAtEnd+ fromAttrVal t = invalidText "CellComments" t++instance FromAttrBs CellComments where+ fromAttrBs "none" = return CellCommentsNone+ fromAttrBs "asDisplayed" = return CellCommentsAsDisplayed+ fromAttrBs "atEnd" = return CellCommentsAtEnd+ fromAttrBs x = unexpectedAttrBs "CellComments" x++-- | See @ST_PrintError@, p. 3923+instance FromAttrVal PrintErrors where+ fromAttrVal "displayed" = readSuccess PrintErrorsDisplayed+ fromAttrVal "blank" = readSuccess PrintErrorsBlank+ fromAttrVal "dash" = readSuccess PrintErrorsDash+ fromAttrVal "NA" = readSuccess PrintErrorsNA+ fromAttrVal t = invalidText "PrintErrors" t++instance FromAttrBs PrintErrors where+ fromAttrBs "displayed" = return PrintErrorsDisplayed+ fromAttrBs "blank" = return PrintErrorsBlank+ fromAttrBs "dash" = return PrintErrorsDash+ fromAttrBs "NA" = return PrintErrorsNA+ fromAttrBs x = unexpectedAttrBs "PrintErrors" x++-- | See @ST_Orientation@, p. 3923+instance FromAttrVal Orientation where+ fromAttrVal "default" = readSuccess OrientationDefault+ fromAttrVal "portrait" = readSuccess OrientationPortrait+ fromAttrVal "landscape" = readSuccess OrientationLandscape+ fromAttrVal t = invalidText "Orientation" t++instance FromAttrBs Orientation where+ fromAttrBs "default" = return OrientationDefault+ fromAttrBs "portrait" = return OrientationPortrait+ fromAttrBs "landscape" = return OrientationLandscape+ fromAttrBs x = unexpectedAttrBs "Orientation" x
+ src/Codec/Xlsx/Types/PivotTable.hs view
@@ -0,0 +1,164 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.PivotTable+ ( PivotTable(..)+ , PivotFieldName(..)+ , PivotFieldInfo(..)+ , FieldSortType(..)+ , PositionedField(..)+ , DataField(..)+ , ConsolidateFunction(..)+ ) where++import Control.Arrow (first)+import Control.DeepSeq (NFData)+import Data.Text (Text)+import GHC.Generics (Generic)++import Codec.Xlsx.Types.Common+import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++data PivotTable = PivotTable+ { _pvtName :: Text+ , _pvtDataCaption :: Text+ , _pvtRowFields :: [PositionedField]+ , _pvtColumnFields :: [PositionedField]+ , _pvtDataFields :: [DataField]+ , _pvtFields :: [PivotFieldInfo]+ , _pvtRowGrandTotals :: Bool+ , _pvtColumnGrandTotals :: Bool+ , _pvtOutline :: Bool+ , _pvtOutlineData :: Bool+ , _pvtLocation :: CellRef+ , _pvtSrcSheet :: Text+ , _pvtSrcRef :: Range+ } deriving (Eq, Show, Generic)+instance NFData PivotTable++data PivotFieldInfo = PivotFieldInfo+ { _pfiName :: Maybe PivotFieldName+ , _pfiOutline :: Bool+ , _pfiSortType :: FieldSortType+ , _pfiHiddenItems :: [CellValue]+ } deriving (Eq, Show, Generic)+instance NFData PivotFieldInfo++-- | Sort orders that can be applied to fields in a PivotTable+--+-- See 18.18.28 "ST_FieldSortType (Field Sort Type)" (p. 2454)+data FieldSortType+ = FieldSortAscending+ | FieldSortDescending+ | FieldSortManual+ deriving (Eq, Ord, Show, Generic)+instance NFData FieldSortType++newtype PivotFieldName =+ PivotFieldName Text+ deriving (Eq, Ord, Show, Generic)+instance NFData PivotFieldName++data PositionedField+ = DataPosition+ | FieldPosition PivotFieldName+ deriving (Eq, Ord, Show, Generic)+instance NFData PositionedField++data DataField = DataField+ { _dfField :: PivotFieldName+ , _dfName :: Text+ , _dfFunction :: ConsolidateFunction+ } deriving (Eq, Show, Generic)+instance NFData DataField++-- | Data consolidation functions specified by the user and used to+-- consolidate ranges of data+--+-- See 18.18.17 "ST_DataConsolidateFunction (Data Consolidation+-- Functions)" (p. 2447)+data ConsolidateFunction+ = ConsolidateAverage+ -- ^ The average of the values.+ | ConsolidateCount+ -- ^ The number of data values. The Count consolidation function+ -- works the same as the COUNTA worksheet function.+ | ConsolidateCountNums+ -- ^ The number of data values that are numbers. The Count Nums+ -- consolidation function works the same as the COUNT worksheet+ -- function.+ | ConsolidateMaximum+ -- ^ The largest value.+ | ConsolidateMinimum+ -- ^ The smallest value.+ | ConsolidateProduct+ -- ^ The product of the values.+ | ConsolidateStdDev+ -- ^ An estimate of the standard deviation of a population, where+ -- the sample is a subset of the entire population.+ | ConsolidateStdDevP+ -- ^ The standard deviation of a population, where the population+ -- is all of the data to be summarized.+ | ConsolidateSum+ -- ^ The sum of the values.+ | ConsolidateVariance+ -- ^ An estimate of the variance of a population, where the sample+ -- is a subset of the entire population.+ | ConsolidateVarP+ -- ^ The variance of a population, where the population is all of+ -- the data to be summarized.+ deriving (Eq, Show, Generic)+instance NFData ConsolidateFunction++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToAttrVal ConsolidateFunction where+ toAttrVal ConsolidateAverage = "average"+ toAttrVal ConsolidateCount = "count"+ toAttrVal ConsolidateCountNums = "countNums"+ toAttrVal ConsolidateMaximum = "max"+ toAttrVal ConsolidateMinimum = "min"+ toAttrVal ConsolidateProduct = "product"+ toAttrVal ConsolidateStdDev = "stdDev"+ toAttrVal ConsolidateStdDevP = "stdDevp"+ toAttrVal ConsolidateSum = "sum"+ toAttrVal ConsolidateVariance = "var"+ toAttrVal ConsolidateVarP = "varp"++instance ToAttrVal PivotFieldName where+ toAttrVal (PivotFieldName n) = toAttrVal n++instance ToAttrVal FieldSortType where+ toAttrVal FieldSortManual = "manual"+ toAttrVal FieldSortAscending = "ascending"+ toAttrVal FieldSortDescending = "descending"++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromAttrVal ConsolidateFunction where+ fromAttrVal "average" = readSuccess ConsolidateAverage+ fromAttrVal "count" = readSuccess ConsolidateCount+ fromAttrVal "countNums" = readSuccess ConsolidateCountNums+ fromAttrVal "max" = readSuccess ConsolidateMaximum+ fromAttrVal "min" = readSuccess ConsolidateMinimum+ fromAttrVal "product" = readSuccess ConsolidateProduct+ fromAttrVal "stdDev" = readSuccess ConsolidateStdDev+ fromAttrVal "stdDevp" = readSuccess ConsolidateStdDevP+ fromAttrVal "sum" = readSuccess ConsolidateSum+ fromAttrVal "var" = readSuccess ConsolidateVariance+ fromAttrVal "varp" = readSuccess ConsolidateVarP+ fromAttrVal t = invalidText "ConsolidateFunction" t++instance FromAttrVal PivotFieldName where+ fromAttrVal = fmap (first PivotFieldName) . fromAttrVal++instance FromAttrVal FieldSortType where+ fromAttrVal "manual" = readSuccess FieldSortManual+ fromAttrVal "ascending" = readSuccess FieldSortAscending+ fromAttrVal "descending" = readSuccess FieldSortDescending+ fromAttrVal t = invalidText "FieldSortType" t
+ src/Codec/Xlsx/Types/PivotTable/Internal.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.PivotTable.Internal+ ( CacheId(..)+ , CacheField(..)+ , CacheRecordValue(..)+ , CacheRecord+ , recordValueFromNode+ ) where++import GHC.Generics (Generic)++import Control.Arrow (first)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.PivotTable+import Codec.Xlsx.Writer.Internal++newtype CacheId = CacheId Int deriving (Eq, Generic)++data CacheField = CacheField+ { cfName :: PivotFieldName+ , cfItems :: [CellValue]+ } deriving (Eq, Show, Generic)++data CacheRecordValue+ = CacheText Text+ | CacheNumber Double+ | CacheIndex Int+ deriving (Eq, Show, Generic)++type CacheRecord = [CacheRecordValue]++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromAttrVal CacheId where+ fromAttrVal = fmap (first CacheId) . fromAttrVal++instance FromCursor CacheField where+ fromCursor cur = do+ cfName <- fromAttribute "name" cur+ let cfItems =+ cur $/ element (addSmlNamespace "sharedItems") &/ anyElement >=>+ cellValueFromNode . node+ return CacheField {..}++cellValueFromNode :: Node -> [CellValue]+cellValueFromNode n+ | n `nodeElNameIs` (addSmlNamespace "s") = CellText <$> attributeV+ | n `nodeElNameIs` (addSmlNamespace "n") = CellDouble <$> attributeV+ | otherwise = fail "no matching shared item"+ where+ cur = fromNode n+ attributeV :: FromAttrVal a => [a]+ attributeV = fromAttribute "v" cur++recordValueFromNode :: Node -> [CacheRecordValue]+recordValueFromNode n+ | n `nodeElNameIs` (addSmlNamespace "s") = CacheText <$> attributeV+ | n `nodeElNameIs` (addSmlNamespace "n") = CacheNumber <$> attributeV+ | n `nodeElNameIs` (addSmlNamespace "x") = CacheIndex <$> attributeV+ | otherwise = fail "not valid cache record value"+ where+ cur = fromNode n+ attributeV :: FromAttrVal a => [a]+ attributeV = fromAttribute "v" cur++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement CacheField where+ toElement nm CacheField {..} =+ elementList nm ["name" .= cfName] [sharedItems]+ where+ -- Excel doesn't like embedded integer sharedImes in cache+ sharedItems = elementList "sharedItems" typeAttrs $+ if containsString then map cvToItem cfItems else []+ cvToItem (CellText t) = leafElement "s" ["v" .= t]+ cvToItem (CellDouble n) = leafElement "n" ["v" .= n]+ cvToItem _ = error "Only string and number values are currently supported"+ typeAttrs =+ catMaybes+ [ "containsNumber" .=? justTrue containsNumber+ , "containsString" .=? justFalse containsString+ , "containsSemiMixedTypes" .=? justFalse containsString+ , "containsMixedTypes" .=? justTrue (containsNumber && containsString)+ ]+ containsNumber = any isNumber cfItems+ isNumber (CellDouble _) = True+ isNumber _ = False+ containsString = any isString cfItems+ isString (CellText _) = True+ isString _ = False
+ src/Codec/Xlsx/Types/Protection.hs view
@@ -0,0 +1,269 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Protection+ ( SheetProtection(..)+ , fullSheetProtection+ , noSheetProtection+ , LegacyPassword+ , legacyPassword+ -- * Lenses+ , sprLegacyPassword+ , sprSheet + , sprObjects + , sprScenarios + , sprFormatCells + , sprFormatColumns + , sprFormatRows + , sprInsertColumns + , sprInsertRows + , sprInsertHyperlinks + , sprDeleteColumns + , sprDeleteRows + , sprSelectLockedCells + , sprSort + , sprAutoFilter + , sprPivotTables + , sprSelectUnlockedCells+ ) where++import GHC.Generics (Generic)++import Control.Arrow (first)+import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Bits+import Data.Char+import Data.Maybe (catMaybes)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Lazy (toStrict)+import Data.Text.Lazy.Builder (toLazyText)+import Data.Text.Lazy.Builder.Int (hexadecimal)++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++newtype LegacyPassword =+ LegacyPassword Text+ deriving (Eq, Show, Generic)+instance NFData LegacyPassword++-- | Creates legacy @XOR@ hashed password.+--+-- /Note:/ The implementation is known to work only for ASCII symbols,+-- if you know how to encode properly others - an email or a PR will+-- be highly apperciated+--+-- See Part 4, 14.7.1 "Legacy Password Hash Algorithm" (p. 73) and+-- Part 4, 15.2.3 "Additional attributes for workbookProtection+-- element (Part 1, §18.2.29)" (p. 220) and Par 4, 15.3.1.6+-- "Additional attribute for sheetProtection element (Part 1,+-- §18.3.1.85)" (p. 229)+legacyPassword :: Text -> LegacyPassword+legacyPassword = LegacyPassword . hex . legacyHash . map ord . T.unpack+ where+ hex = toStrict . toLazyText . hexadecimal+ legacyHash bs =+ mutHash (foldr (\b hash -> b `xor` mutHash hash) 0 bs) `xor` (length bs) `xor`+ 0xCE4B+ mutHash ph = ((ph `shiftR` 14) .&. 1) .|. ((ph `shiftL` 1) .&. 0x7fff)++-- | Sheet protection options to enforce and specify that it needs to+-- be protected+--+-- TODO: algorithms specified in the spec with hashes, salts and spin+-- counts+--+-- See 18.3.1.85 "sheetProtection (Sheet Protection Options)" (p. 1694)+data SheetProtection = SheetProtection+ { _sprLegacyPassword :: Maybe LegacyPassword+ -- ^ Specifies the legacy hash of the password required for editing+ -- this worksheet.+ --+ -- See Part 4, 15.3.1.6 "Additional attribute for sheetProtection+ -- element (Part 1, §18.3.1.85)" (p. 229)+ , _sprSheet :: Bool+ -- ^ the value of this attribute dictates whether the other+ -- attributes of 'SheetProtection' should be applied+ , _sprAutoFilter :: Bool+ -- ^ AutoFilters should not be allowed to operate when the sheet+ -- is protected+ , _sprDeleteColumns :: Bool+ -- ^ deleting columns should not be allowed when the sheet is+ -- protected+ , _sprDeleteRows :: Bool+ -- ^ deleting rows should not be allowed when the sheet is+ -- protected+ , _sprFormatCells :: Bool+ -- ^ formatting cells should not be allowed when the sheet is+ -- protected+ , _sprFormatColumns :: Bool+ -- ^ formatting columns should not be allowed when the sheet is+ -- protected+ , _sprFormatRows :: Bool+ -- ^ formatting rows should not be allowed when the sheet is+ -- protected+ , _sprInsertColumns :: Bool+ -- ^ inserting columns should not be allowed when the sheet is+ -- protected+ , _sprInsertHyperlinks :: Bool+ -- ^ inserting hyperlinks should not be allowed when the sheet is+ -- protected+ , _sprInsertRows :: Bool+ -- ^ inserting rows should not be allowed when the sheet is+ -- protected+ , _sprObjects :: Bool+ -- ^ editing of objects should not be allowed when the sheet is+ -- protected+ , _sprPivotTables :: Bool+ -- ^ PivotTables should not be allowed to operate when the sheet+ -- is protected+ , _sprScenarios :: Bool+ -- ^ Scenarios should not be edited when the sheet is protected+ , _sprSelectLockedCells :: Bool+ -- ^ selection of locked cells should not be allowed when the+ -- sheet is protected+ , _sprSelectUnlockedCells :: Bool+ -- ^ selection of unlocked cells should not be allowed when the+ -- sheet is protected+ , _sprSort :: Bool+ -- ^ sorting should not be allowed when the sheet is protected+ } deriving (Eq, Show, Generic)+instance NFData SheetProtection++makeLenses ''SheetProtection++{-------------------------------------------------------------------------------+ Base instances+-------------------------------------------------------------------------------}++-- | no sheet protection at all+noSheetProtection :: SheetProtection+noSheetProtection =+ SheetProtection+ { _sprLegacyPassword = Nothing+ , _sprSheet = False+ , _sprAutoFilter = False+ , _sprDeleteColumns = False+ , _sprDeleteRows = False+ , _sprFormatCells = False+ , _sprFormatColumns = False+ , _sprFormatRows = False+ , _sprInsertColumns = False+ , _sprInsertHyperlinks = False+ , _sprInsertRows = False+ , _sprObjects = False+ , _sprPivotTables = False+ , _sprScenarios = False+ , _sprSelectLockedCells = False+ , _sprSelectUnlockedCells = False+ , _sprSort = False+ }++-- | protection of all sheet features which could be protected+fullSheetProtection :: SheetProtection+fullSheetProtection =+ SheetProtection+ { _sprLegacyPassword = Nothing+ , _sprSheet = True+ , _sprAutoFilter = True+ , _sprDeleteColumns = True+ , _sprDeleteRows = True+ , _sprFormatCells = True+ , _sprFormatColumns = True+ , _sprFormatRows = True+ , _sprInsertColumns = True+ , _sprInsertHyperlinks = True+ , _sprInsertRows = True+ , _sprObjects = True+ , _sprPivotTables = True+ , _sprScenarios = True+ , _sprSelectLockedCells = True+ , _sprSelectUnlockedCells = True+ , _sprSort = True+ }++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor SheetProtection where+ fromCursor cur = do+ _sprLegacyPassword <- maybeAttribute "password" cur+ _sprSheet <- fromAttributeDef "sheet" False cur+ _sprAutoFilter <- fromAttributeDef "autoFilter" True cur+ _sprDeleteColumns <- fromAttributeDef "deleteColumns" True cur+ _sprDeleteRows <- fromAttributeDef "deleteRows" True cur+ _sprFormatCells <- fromAttributeDef "formatCells" True cur+ _sprFormatColumns <- fromAttributeDef "formatColumns" True cur+ _sprFormatRows <- fromAttributeDef "formatRows" True cur+ _sprInsertColumns <- fromAttributeDef "insertColumns" True cur+ _sprInsertHyperlinks <- fromAttributeDef "insertHyperlinks" True cur+ _sprInsertRows <- fromAttributeDef "insertRows" True cur+ _sprObjects <- fromAttributeDef "objects" False cur+ _sprPivotTables <- fromAttributeDef "pivotTables" True cur+ _sprScenarios <- fromAttributeDef "scenarios" False cur+ _sprSelectLockedCells <- fromAttributeDef "selectLockedCells" False cur+ _sprSelectUnlockedCells <- fromAttributeDef "selectUnlockedCells" False cur+ _sprSort <- fromAttributeDef "sort" True cur + return SheetProtection {..}++instance FromXenoNode SheetProtection where+ fromXenoNode root =+ parseAttributes root $ do+ _sprLegacyPassword <- maybeAttr "password"+ _sprSheet <- fromAttrDef "sheet" False+ _sprAutoFilter <- fromAttrDef "autoFilter" True+ _sprDeleteColumns <- fromAttrDef "deleteColumns" True+ _sprDeleteRows <- fromAttrDef "deleteRows" True+ _sprFormatCells <- fromAttrDef "formatCells" True+ _sprFormatColumns <- fromAttrDef "formatColumns" True+ _sprFormatRows <- fromAttrDef "formatRows" True+ _sprInsertColumns <- fromAttrDef "insertColumns" True+ _sprInsertHyperlinks <- fromAttrDef "insertHyperlinks" True+ _sprInsertRows <- fromAttrDef "insertRows" True+ _sprObjects <- fromAttrDef "objects" False+ _sprPivotTables <- fromAttrDef "pivotTables" True+ _sprScenarios <- fromAttrDef "scenarios" False+ _sprSelectLockedCells <- fromAttrDef "selectLockedCells" False+ _sprSelectUnlockedCells <- fromAttrDef "selectUnlockedCells" False+ _sprSort <- fromAttrDef "sort" True+ return SheetProtection {..}++instance FromAttrVal LegacyPassword where+ fromAttrVal = fmap (first LegacyPassword) . fromAttrVal++instance FromAttrBs LegacyPassword where+ fromAttrBs = fmap LegacyPassword . fromAttrBs++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++instance ToElement SheetProtection where+ toElement nm SheetProtection {..} =+ leafElement nm $+ catMaybes+ [ "password" .=? _sprLegacyPassword+ , "sheet" .=? justTrue _sprSheet+ , "autoFilter" .=? justFalse _sprAutoFilter+ , "deleteColumns" .=? justFalse _sprDeleteColumns+ , "deleteRows" .=? justFalse _sprDeleteRows+ , "formatCells" .=? justFalse _sprFormatCells+ , "formatColumns" .=? justFalse _sprFormatColumns+ , "formatRows" .=? justFalse _sprFormatRows+ , "insertColumns" .=? justFalse _sprInsertColumns+ , "insertHyperlinks" .=? justFalse _sprInsertHyperlinks+ , "insertRows" .=? justFalse _sprInsertRows+ , "objects" .=? justTrue _sprObjects+ , "pivotTables" .=? justFalse _sprPivotTables+ , "scenarios" .=? justTrue _sprScenarios+ , "selectLockedCells" .=? justTrue _sprSelectLockedCells+ , "selectUnlockedCells" .=? justTrue _sprSelectUnlockedCells+ , "sort" .=? justFalse _sprSort+ ]++instance ToAttrVal LegacyPassword where+ toAttrVal (LegacyPassword hash) = hash
+ src/Codec/Xlsx/Types/RefId.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+module Codec.Xlsx.Types.RefId where++import Control.Arrow+import Data.Monoid ((<>))+import Data.Text (Text)+import GHC.Generics (Generic)++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++newtype RefId = RefId { unRefId :: Text } deriving (Eq, Ord, Show, Generic)++instance ToAttrVal RefId where+ toAttrVal = toAttrVal . unRefId++instance FromAttrVal RefId where+ fromAttrVal t = first RefId <$> fromAttrVal t++instance FromAttrBs RefId where+ fromAttrBs = fmap RefId . fromAttrBs++unsafeRefId :: Int -> RefId+unsafeRefId num = RefId $ "rId" <> txti num
+ src/Codec/Xlsx/Types/RichText.hs view
@@ -0,0 +1,375 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.RichText (+ -- * Main types+ RichTextRun(..)+ , RunProperties(..)+ , applyRunProperties+ -- * Lenses+ -- ** RichTextRun+ , richTextRunProperties+ , richTextRunText+ -- ** RunProperties+ , runPropertiesBold+ , runPropertiesCharset+ , runPropertiesColor+ , runPropertiesCondense+ , runPropertiesExtend+ , runPropertiesFontFamily+ , runPropertiesItalic+ , runPropertiesOutline+ , runPropertiesFont+ , runPropertiesScheme+ , runPropertiesShadow+ , runPropertiesStrikeThrough+ , runPropertiesSize+ , runPropertiesUnderline+ , runPropertiesVertAlign+ ) where++import GHC.Generics (Generic)++import Codec.Xlsx.LensCompat (makeLenses)+import Control.Monad+import Control.DeepSeq (NFData)+import Data.Default+import Data.Maybe (catMaybes)+import Data.Text (Text)+import Text.XML+import Text.XML.Cursor+import qualified Data.Map as Map++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.StyleSheet+import Codec.Xlsx.Writer.Internal++-- | Rich Text Run+--+-- This element represents a run of rich text. A rich text run is a region of+-- text that share a common set of properties, such as formatting properties.+--+-- Section 18.4.4, "r (Rich Text Run)" (p. 1724)+data RichTextRun = RichTextRun {+ -- | This element represents a set of properties to apply to the contents of+ -- this rich text run.+ _richTextRunProperties :: Maybe RunProperties++ -- | This element represents the text content shown as part of a string.+ --+ -- NOTE: 'RichTextRun' elements with an empty text field will result in+ -- an error when opening the file in Excel.+ --+ -- Section 18.4.12, "t (Text)" (p. 1727)+ , _richTextRunText :: Text+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData RichTextRun++-- | Run properties+--+-- Section 18.4.7, "rPr (Run Properties)" (p. 1725)+data RunProperties = RunProperties {+ -- | Displays characters in bold face font style.+ --+ -- Section 18.8.2, "b (Bold)" (p. 1757)+ _runPropertiesBold :: Maybe Bool++ -- | This element defines the font character set of this font.+ --+ -- Section 18.4.1, "charset (Character Set)" (p. 1721)+ , _runPropertiesCharset :: Maybe Int++ -- | One of the colors associated with the data bar or color scale.+ --+ -- Section 18.3.1.15, "color (Data Bar Color)" (p. 1608)+ , _runPropertiesColor :: Maybe Color++ -- | Macintosh compatibility setting. Represents special word/character+ -- rendering on Macintosh, when this flag is set. The effect is to condense+ -- the text (squeeze it together).+ --+ -- Section 18.8.12, "condense (Condense)" (p. 1764)+ , _runPropertiesCondense :: Maybe Bool++ -- | This element specifies a compatibility setting used for previous+ -- spreadsheet applications, resulting in special word/character rendering+ -- on those legacy applications, when this flag is set. The effect extends+ -- or stretches out the text.+ --+ -- Section 18.8.17, "extend (Extend)" (p. 1766)+ , _runPropertiesExtend :: Maybe Bool++ -- | The font family this font belongs to. A font family is a set of fonts+ -- having common stroke width and serif characteristics. This is system+ -- level font information. The font name overrides when there are+ -- conflicting values.+ --+ -- Section 18.8.18, "family (Font Family)" (p. 1766)+ , _runPropertiesFontFamily :: Maybe FontFamily++ -- | Displays characters in italic font style. The italic style is defined+ -- by the font at a system level and is not specified by ECMA-376.+ --+ -- Section 18.8.26, "i (Italic)" (p. 1773)+ , _runPropertiesItalic :: Maybe Bool++ -- | This element displays only the inner and outer borders of each+ -- character. This is very similar to Bold in behavior.+ --+ -- Section 18.4.2, "outline (Outline)" (p. 1722)+ , _runPropertiesOutline :: Maybe Bool++ -- | This element is a string representing the name of the font assigned to+ -- display this run.+ --+ -- Section 18.4.5, "rFont (Font)" (p. 1724)+ , _runPropertiesFont :: Maybe Text++ -- | Defines the font scheme, if any, to which this font belongs. When a+ -- font definition is part of a theme definition, then the font is+ -- categorized as either a major or minor font scheme component. When a new+ -- theme is chosen, every font that is part of a theme definition is updated+ -- to use the new major or minor font definition for that theme. Usually+ -- major fonts are used for styles like headings, and minor fonts are used+ -- for body and paragraph text.+ --+ -- Section 18.8.35, "scheme (Scheme)" (p. 1794)+ , _runPropertiesScheme :: Maybe FontScheme++ -- | Macintosh compatibility setting. Represents special word/character+ -- rendering on Macintosh, when this flag is set. The effect is to render a+ -- shadow behind, beneath and to the right of the text.+ --+ -- Section 18.8.36, "shadow (Shadow)" (p. 1795)+ , _runPropertiesShadow :: Maybe Bool++ -- | This element draws a strikethrough line through the horizontal middle+ -- of the text.+ --+ -- Section 18.4.10, "strike (Strike Through)" (p. 1726)+ , _runPropertiesStrikeThrough :: Maybe Bool++ -- | This element represents the point size (1/72 of an inch) of the Latin+ -- and East Asian text.+ --+ -- Section 18.4.11, "sz (Font Size)" (p. 1727)+ , _runPropertiesSize :: Maybe Double++ -- | This element represents the underline formatting style.+ --+ -- Section 18.4.13, "u (Underline)" (p. 1728)+ , _runPropertiesUnderline :: Maybe FontUnderline++ -- | This element adjusts the vertical position of the text relative to the+ -- text's default appearance for this run. It is used to get 'superscript'+ -- or 'subscript' texts, and shall reduce the font size (if a smaller size+ -- is available) accordingly.+ --+ -- Section 18.4.14, "vertAlign (Vertical Alignment)" (p. 1728)+ , _runPropertiesVertAlign :: Maybe FontVerticalAlignment+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData RunProperties++{-------------------------------------------------------------------------------+ Lenses+-------------------------------------------------------------------------------}++makeLenses ''RichTextRun+makeLenses ''RunProperties++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default RichTextRun where+ def = RichTextRun {+ _richTextRunProperties = Nothing+ , _richTextRunText = ""+ }++instance Default RunProperties where+ def = RunProperties {+ _runPropertiesBold = Nothing+ , _runPropertiesCharset = Nothing+ , _runPropertiesColor = Nothing+ , _runPropertiesCondense = Nothing+ , _runPropertiesExtend = Nothing+ , _runPropertiesFontFamily = Nothing+ , _runPropertiesItalic = Nothing+ , _runPropertiesOutline = Nothing+ , _runPropertiesFont = Nothing+ , _runPropertiesScheme = Nothing+ , _runPropertiesShadow = Nothing+ , _runPropertiesStrikeThrough = Nothing+ , _runPropertiesSize = Nothing+ , _runPropertiesUnderline = Nothing+ , _runPropertiesVertAlign = Nothing+ }++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++-- | See @CT_RElt@, p. 3903+instance ToElement RichTextRun where+ toElement nm RichTextRun{..} = Element {+ elementName = nm+ , elementAttributes = Map.empty+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "rPr" <$> _richTextRunProperties+ , Just $ elementContentPreserved "t" _richTextRunText+ ]+ }++-- | See @CT_RPrElt@, p. 3903+instance ToElement RunProperties where+ toElement nm RunProperties{..} = Element {+ elementName = nm+ , elementAttributes = Map.empty+ , elementNodes = map NodeElement . catMaybes $ [+ elementValue "rFont" <$> _runPropertiesFont+ , elementValue "charset" <$> _runPropertiesCharset+ , elementValue "family" <$> _runPropertiesFontFamily+ , elementValue "b" <$> _runPropertiesBold+ , elementValue "i" <$> _runPropertiesItalic+ , elementValue "strike" <$> _runPropertiesStrikeThrough+ , elementValue "outline" <$> _runPropertiesOutline+ , elementValue "shadow" <$> _runPropertiesShadow+ , elementValue "condense" <$> _runPropertiesCondense+ , elementValue "extend" <$> _runPropertiesExtend+ , toElement "color" <$> _runPropertiesColor+ , elementValue "sz" <$> _runPropertiesSize+ , elementValueDef "u" FontUnderlineSingle+ <$> _runPropertiesUnderline+ , elementValue "vertAlign" <$> _runPropertiesVertAlign+ , elementValue "scheme" <$> _runPropertiesScheme+ ]+ }++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++-- | See @CT_RElt@, p. 3903+instance FromCursor RichTextRun where+ fromCursor cur = do+ _richTextRunText <- cur $/ element (addSmlNamespace "t") &/ content+ _richTextRunProperties <- maybeFromElement (addSmlNamespace "rPr") cur+ return RichTextRun{..}++instance FromXenoNode RichTextRun where+ fromXenoNode root = do+ (prNode, tNode) <- collectChildren root $ (,) <$> maybeChild "rPr" <*> requireChild "t"+ _richTextRunProperties <- mapM fromXenoNode prNode+ _richTextRunText <- contentX tNode+ return RichTextRun {..}++-- | See @CT_RPrElt@, p. 3903+instance FromCursor RunProperties where+ fromCursor cur = do+ _runPropertiesFont <- maybeElementValue (addSmlNamespace "rFont") cur+ _runPropertiesCharset <- maybeElementValue (addSmlNamespace "charset") cur+ _runPropertiesFontFamily <- maybeElementValue (addSmlNamespace "family") cur+ _runPropertiesBold <- maybeBoolElementValue (addSmlNamespace "b") cur+ _runPropertiesItalic <- maybeBoolElementValue (addSmlNamespace "i") cur+ _runPropertiesStrikeThrough <- maybeBoolElementValue (addSmlNamespace "strike") cur+ _runPropertiesOutline <- maybeBoolElementValue (addSmlNamespace "outline") cur+ _runPropertiesShadow <- maybeBoolElementValue (addSmlNamespace "shadow") cur+ _runPropertiesCondense <- maybeBoolElementValue (addSmlNamespace "condense") cur+ _runPropertiesExtend <- maybeBoolElementValue (addSmlNamespace "extend") cur+ _runPropertiesColor <- maybeFromElement (addSmlNamespace "color") cur+ _runPropertiesSize <- maybeElementValue (addSmlNamespace "sz") cur+ _runPropertiesUnderline <- maybeElementValueDef (addSmlNamespace "u") FontUnderlineSingle cur+ _runPropertiesVertAlign <- maybeElementValue (addSmlNamespace "vertAlign") cur+ _runPropertiesScheme <- maybeElementValue (addSmlNamespace "scheme") cur+ return RunProperties{..}++instance FromXenoNode RunProperties where+ fromXenoNode root = collectChildren root $ do+ _runPropertiesFont <- maybeElementVal "rFont"+ _runPropertiesCharset <- maybeElementVal "charset"+ _runPropertiesFontFamily <- maybeElementVal "family"+ _runPropertiesBold <- maybeElementVal "b"+ _runPropertiesItalic <- maybeElementVal "i"+ _runPropertiesStrikeThrough <- maybeElementVal "strike"+ _runPropertiesOutline <- maybeElementVal "outline"+ _runPropertiesShadow <- maybeElementVal "shadow"+ _runPropertiesCondense <- maybeElementVal "condense"+ _runPropertiesExtend <- maybeElementVal "extend"+ _runPropertiesColor <- maybeFromChild "color"+ _runPropertiesSize <- maybeElementVal "sz"+ _runPropertiesUnderline <- maybeElementVal "u"+ _runPropertiesVertAlign <- maybeElementVal "vertAlign"+ _runPropertiesScheme <- maybeElementVal "scheme"+ return RunProperties{..}++{-------------------------------------------------------------------------------+ Applying formatting+-------------------------------------------------------------------------------}++#if (MIN_VERSION_base(4,11,0))+instance Semigroup RunProperties where+ a <> b = RunProperties {+ _runPropertiesBold = override _runPropertiesBold+ , _runPropertiesCharset = override _runPropertiesCharset+ , _runPropertiesColor = override _runPropertiesColor+ , _runPropertiesCondense = override _runPropertiesCondense+ , _runPropertiesExtend = override _runPropertiesExtend+ , _runPropertiesFontFamily = override _runPropertiesFontFamily+ , _runPropertiesItalic = override _runPropertiesItalic+ , _runPropertiesOutline = override _runPropertiesOutline+ , _runPropertiesFont = override _runPropertiesFont+ , _runPropertiesScheme = override _runPropertiesScheme+ , _runPropertiesShadow = override _runPropertiesShadow+ , _runPropertiesStrikeThrough = override _runPropertiesStrikeThrough+ , _runPropertiesSize = override _runPropertiesSize+ , _runPropertiesUnderline = override _runPropertiesUnderline+ , _runPropertiesVertAlign = override _runPropertiesVertAlign+ }+ where+ override :: (RunProperties -> Maybe x) -> Maybe x+ override f = f b `mplus` f a++#endif++-- | The 'Monoid' instance for 'RunProperties' is biased: later properties+-- override earlier ones.+instance Monoid RunProperties where+ mempty = def+ a `mappend` b = RunProperties {+ _runPropertiesBold = override _runPropertiesBold+ , _runPropertiesCharset = override _runPropertiesCharset+ , _runPropertiesColor = override _runPropertiesColor+ , _runPropertiesCondense = override _runPropertiesCondense+ , _runPropertiesExtend = override _runPropertiesExtend+ , _runPropertiesFontFamily = override _runPropertiesFontFamily+ , _runPropertiesItalic = override _runPropertiesItalic+ , _runPropertiesOutline = override _runPropertiesOutline+ , _runPropertiesFont = override _runPropertiesFont+ , _runPropertiesScheme = override _runPropertiesScheme+ , _runPropertiesShadow = override _runPropertiesShadow+ , _runPropertiesStrikeThrough = override _runPropertiesStrikeThrough+ , _runPropertiesSize = override _runPropertiesSize+ , _runPropertiesUnderline = override _runPropertiesUnderline+ , _runPropertiesVertAlign = override _runPropertiesVertAlign+ }+ where+ override :: (RunProperties -> Maybe x) -> Maybe x+ override f = f b `mplus` f a++-- | Apply properties to a 'RichTextRun'+--+-- If the 'RichTextRun' specifies its own properties, then these overrule the+-- properties specified here. For example, adding @bold@ to a 'RichTextRun'+-- which is already @italic@ will make the 'RichTextRun' both @bold and @italic@+-- but adding it to one that that is explicitly _not_ bold will leave the+-- 'RichTextRun' unchanged.+applyRunProperties :: RunProperties -> RichTextRun -> RichTextRun+applyRunProperties p (RichTextRun Nothing t) = RichTextRun (Just p) t+applyRunProperties p (RichTextRun (Just p') t) = RichTextRun (Just (p `mappend` p')) t
+ src/Codec/Xlsx/Types/SheetViews.hs view
@@ -0,0 +1,576 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.SheetViews (+ -- * Structured type to construct 'SheetViews'+ SheetView(..)+ , Selection(..)+ , Pane(..)+ , SheetViewType(..)+ , PaneType(..)+ , PaneState(..)+ -- * Lenses+ -- ** SheetView+ , sheetViewColorId+ , sheetViewDefaultGridColor+ , sheetViewRightToLeft+ , sheetViewShowFormulas+ , sheetViewShowGridLines+ , sheetViewShowOutlineSymbols+ , sheetViewShowRowColHeaders+ , sheetViewShowRuler+ , sheetViewShowWhiteSpace+ , sheetViewShowZeros+ , sheetViewTabSelected+ , sheetViewTopLeftCell+ , sheetViewType+ , sheetViewWindowProtection+ , sheetViewWorkbookViewId+ , sheetViewZoomScale+ , sheetViewZoomScaleNormal+ , sheetViewZoomScalePageLayoutView+ , sheetViewZoomScaleSheetLayoutView+ , sheetViewPane+ , sheetViewSelection+ -- ** Selection+ , selectionActiveCell+ , selectionActiveCellId+ , selectionPane+ , selectionSqref+ -- ** Pane+ , paneActivePane+ , paneState+ , paneTopLeftCell+ , paneXSplit+ , paneYSplit+ ) where++import GHC.Generics (Generic)++import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Default+import Data.Maybe (catMaybes, maybeToList, listToMaybe)+import Text.XML+import Text.XML.Cursor+import qualified Data.Map as Map++import Codec.Xlsx.Types.Common+import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++{-------------------------------------------------------------------------------+ Main types+-------------------------------------------------------------------------------}++-- | Worksheet view+--+-- A single sheet view definition. When more than one sheet view is defined in+-- the file, it means that when opening the workbook, each sheet view+-- corresponds to a separate window within the spreadsheet application, where+-- each window is showing the particular sheet containing the same+-- workbookViewId value, the last sheetView definition is loaded, and the others+-- are discarded. When multiple windows are viewing the same sheet, multiple+-- sheetView elements (with corresponding workbookView entries) are saved.+--+-- TODO: The @pivotSelection@ and @extLst@ child elements are unsupported.+--+-- See Section 18.3.1.87 "sheetView (Worksheet View)" (p. 1880)+data SheetView = SheetView {+ -- | Index to the color value for row/column text headings and gridlines.+ -- This is an 'index color value' (ICV) rather than rgb value.+ _sheetViewColorId :: Maybe Int++ -- | Flag indicating that the consuming application should use the default+ -- grid lines color (system dependent). Overrides any color specified in+ -- colorId.+ , _sheetViewDefaultGridColor :: Maybe Bool++ -- | Flag indicating whether the sheet is in 'right to left' display mode.+ -- When in this mode, Column A is on the far right, Column B ;is one column+ -- left of Column A, and so on. Also, information in cells is displayed in+ -- the Right to Left format.+ , _sheetViewRightToLeft :: Maybe Bool++ -- | Flag indicating whether this sheet should display formulas.+ , _sheetViewShowFormulas :: Maybe Bool++ -- | Flag indicating whether this sheet should display gridlines.+ , _sheetViewShowGridLines :: Maybe Bool++ -- | Flag indicating whether the sheet has outline symbols visible. This+ -- flag shall always override SheetPr element's outlinePr child element+ -- whose attribute is named showOutlineSymbols when there is a conflict.+ , _sheetViewShowOutlineSymbols :: Maybe Bool++ -- | Flag indicating whether the sheet should display row and column headings.+ , _sheetViewShowRowColHeaders :: Maybe Bool++ -- | Show the ruler in Page Layout View.+ , _sheetViewShowRuler :: Maybe Bool++ -- | Flag indicating whether page layout view shall display margins. False+ -- means do not display left, right, top (header), and bottom (footer)+ -- margins (even when there is data in the header or footer).+ , _sheetViewShowWhiteSpace :: Maybe Bool++ -- | Flag indicating whether the window should show 0 (zero) in cells+ -- containing zero value. When false, cells with zero value appear blank+ -- instead of showing the number zero.+ , _sheetViewShowZeros :: Maybe Bool++ -- | Flag indicating whether this sheet is selected. When only 1 sheet is+ -- selected and active, this value should be in synch with the activeTab+ -- value. In case of a conflict, the Start Part setting wins and sets the+ -- active sheet tab.+ --+ -- Multiple sheets can be selected, but only one sheet shall be active at+ -- one time.+ , _sheetViewTabSelected :: Maybe Bool++ -- | Location of the top left visible cell Location of the top left visible+ -- cell in the bottom right pane (when in Left-to-Right mode).+ , _sheetViewTopLeftCell :: Maybe CellRef++ -- | Indicates the view type.+ , _sheetViewType :: Maybe SheetViewType++ -- | Flag indicating whether the panes in the window are locked due to+ -- workbook protection. This is an option when the workbook structure is+ -- protected.+ , _sheetViewWindowProtection :: Maybe Bool++ -- | Zero-based index of this workbook view, pointing to a workbookView+ -- element in the bookViews collection.+ --+ -- NOTE: This attribute is required.+ , _sheetViewWorkbookViewId :: Int++ -- | Window zoom magnification for current view representing percent values.+ -- This attribute is restricted to values ranging from 10 to 400. Horizontal &+ -- Vertical scale together.+ , _sheetViewZoomScale :: Maybe Int++ -- | Zoom magnification to use when in normal view, representing percent+ -- values. This attribute is restricted to values ranging from 10 to 400.+ -- Horizontal & Vertical scale together.+ , _sheetViewZoomScaleNormal :: Maybe Int++ -- | Zoom magnification to use when in page layout view, representing+ -- percent values. This attribute is restricted to values ranging from 10 to+ -- 400. Horizontal & Vertical scale together.+ , _sheetViewZoomScalePageLayoutView :: Maybe Int++ -- | Zoom magnification to use when in page break preview, representing+ -- percent values. This attribute is restricted to values ranging from 10 to+ -- 400. Horizontal & Vertical scale together.+ , _sheetViewZoomScaleSheetLayoutView :: Maybe Int++ -- | Worksheet view pane+ , _sheetViewPane :: Maybe Pane++ -- | Worksheet view selection+ --+ -- Minimum of 0, maximum of 4 elements+ , _sheetViewSelection :: [Selection]+ }+ deriving (Eq, Ord, Show, Generic)+instance NFData SheetView++-- | Worksheet view selection.+--+-- Section 18.3.1.78 "selection (Selection)" (p. 1864)+data Selection = Selection {+ -- | Location of the active cell+ _selectionActiveCell :: Maybe CellRef++ -- | 0-based index of the range reference (in the array of references listed+ -- in sqref) containing the active cell. Only used when the selection in+ -- sqref is not contiguous. Therefore, this value needs to be aware of the+ -- order in which the range references are written in sqref.+ --+ -- When this value is out of range then activeCell can be used.+ , _selectionActiveCellId :: Maybe Int++ -- | The pane to which this selection belongs.+ , _selectionPane :: Maybe PaneType++ -- | Range of the selection. Can be non-contiguous set of ranges.+ , _selectionSqref :: Maybe SqRef+ }+ deriving (Eq, Ord, Show, Generic)+instance NFData Selection++-- | Worksheet view pane+--+-- Section 18.3.1.66 "pane (View Pane)" (p. 1843)+data Pane = Pane {+ -- | The pane that is active.+ _paneActivePane :: Maybe PaneType++ -- | Indicates whether the pane has horizontal / vertical splits, and+ -- whether those splits are frozen.+ , _paneState :: Maybe PaneState++ -- | Location of the top left visible cell in the bottom right pane (when in+ -- Left-To-Right mode).+ , _paneTopLeftCell :: Maybe CellRef++ -- | Horizontal position of the split, in 1/20th of a point; 0 (zero) if+ -- none. If the pane is frozen, this value indicates the number of columns+ -- visible in the top pane.+ , _paneXSplit :: Maybe Double++ -- | Vertical position of the split, in 1/20th of a point; 0 (zero) if none.+ -- If the pane is frozen, this value indicates the number of rows visible in+ -- the left pane.+ , _paneYSplit :: Maybe Double+ }+ deriving (Eq, Ord, Show, Generic)+instance NFData Pane++{-------------------------------------------------------------------------------+ Enumerations+-------------------------------------------------------------------------------}++-- | View setting of the sheet+--+-- Section 18.18.69 "ST_SheetViewType (Sheet View Type)" (p. 2726)+data SheetViewType =+ -- | Normal view+ SheetViewTypeNormal++ -- | Page break preview+ | SheetViewTypePageBreakPreview++ -- | Page layout view+ | SheetViewTypePageLayout+ deriving (Eq, Ord, Show, Generic)+instance NFData SheetViewType++-- | Pane type+--+-- Section 18.18.52 "ST_Pane (Pane Types)" (p. 2710)+data PaneType =+ -- | Bottom left pane, when both vertical and horizontal splits are applied.+ --+ -- This value is also used when only a horizontal split has been applied,+ -- dividing the pane into upper and lower regions. In that case, this value+ -- specifies the bottom pane.+ PaneTypeBottomLeft++ -- Bottom right pane, when both vertical and horizontal splits are applied.+ | PaneTypeBottomRight++ -- | Top left pane, when both vertical and horizontal splits are applied.+ --+ -- This value is also used when only a horizontal split has been applied,+ -- dividing the pane into upper and lower regions. In that case, this value+ -- specifies the top pane.+ --+ -- This value is also used when only a vertical split has been applied,+ -- dividing the pane into right and left regions. In that case, this value+ -- specifies the left pane+ | PaneTypeTopLeft++ -- | Top right pane, when both vertical and horizontal splits are applied.+ --+ -- This value is also used when only a vertical split has been applied,+ -- dividing the pane into right and left regions. In that case, this value+ -- specifies the right pane.+ | PaneTypeTopRight+ deriving (Eq, Ord, Show, Generic)+instance NFData PaneType++-- | State of the sheet's pane.+--+-- Section 18.18.53 "ST_PaneState (Pane State)" (p. 2711)+data PaneState =+ -- | Panes are frozen, but were not split being frozen. In this state, when+ -- the panes are unfrozen again, a single pane results, with no split. In+ -- this state, the split bars are not adjustable.+ PaneStateFrozen++ -- | Panes are frozen and were split before being frozen. In this state,+ -- when the panes are unfrozen again, the split remains, but is adjustable.+ | PaneStateFrozenSplit++ -- | Panes are split, but not frozen. In this state, the split bars are+ -- adjustable by the user.+ | PaneStateSplit+ deriving (Eq, Ord, Show, Generic)+instance NFData PaneState++{-------------------------------------------------------------------------------+ Lenses+-------------------------------------------------------------------------------}++makeLenses ''SheetView+makeLenses ''Selection+makeLenses ''Pane++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++-- | NOTE: The 'Default' instance for 'SheetView' sets the required attribute+-- '_sheetViewWorkbookViewId' to @0@.+instance Default SheetView where+ def = SheetView {+ _sheetViewColorId = Nothing+ , _sheetViewDefaultGridColor = Nothing+ , _sheetViewRightToLeft = Nothing+ , _sheetViewShowFormulas = Nothing+ , _sheetViewShowGridLines = Nothing+ , _sheetViewShowOutlineSymbols = Nothing+ , _sheetViewShowRowColHeaders = Nothing+ , _sheetViewShowRuler = Nothing+ , _sheetViewShowWhiteSpace = Nothing+ , _sheetViewShowZeros = Nothing+ , _sheetViewTabSelected = Nothing+ , _sheetViewTopLeftCell = Nothing+ , _sheetViewType = Nothing+ , _sheetViewWindowProtection = Nothing+ , _sheetViewWorkbookViewId = 0+ , _sheetViewZoomScale = Nothing+ , _sheetViewZoomScaleNormal = Nothing+ , _sheetViewZoomScalePageLayoutView = Nothing+ , _sheetViewZoomScaleSheetLayoutView = Nothing+ , _sheetViewPane = Nothing+ , _sheetViewSelection = []+ }++instance Default Selection where+ def = Selection {+ _selectionActiveCell = Nothing+ , _selectionActiveCellId = Nothing+ , _selectionPane = Nothing+ , _selectionSqref = Nothing+ }++instance Default Pane where+ def = Pane {+ _paneActivePane = Nothing+ , _paneState = Nothing+ , _paneTopLeftCell = Nothing+ , _paneXSplit = Nothing+ , _paneYSplit = Nothing+ }++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++-- | See @CT_SheetView@, p. 3913+instance ToElement SheetView where+ toElement nm SheetView{..} = Element {+ elementName = nm+ , elementNodes = map NodeElement . concat $ [+ map (toElement "pane") (maybeToList _sheetViewPane)+ , map (toElement "selection") _sheetViewSelection+ -- TODO: pivotSelection+ -- TODO: extLst+ ]+ , elementAttributes = Map.fromList . catMaybes $ [+ "windowProtection" .=? _sheetViewWindowProtection+ , "showFormulas" .=? _sheetViewShowFormulas+ , "showGridLines" .=? _sheetViewShowGridLines+ , "showRowColHeaders" .=? _sheetViewShowRowColHeaders+ , "showZeros" .=? _sheetViewShowZeros+ , "rightToLeft" .=? _sheetViewRightToLeft+ , "tabSelected" .=? _sheetViewTabSelected+ , "showRuler" .=? _sheetViewShowRuler+ , "showOutlineSymbols" .=? _sheetViewShowOutlineSymbols+ , "defaultGridColor" .=? _sheetViewDefaultGridColor+ , "showWhiteSpace" .=? _sheetViewShowWhiteSpace+ , "view" .=? _sheetViewType+ , "topLeftCell" .=? _sheetViewTopLeftCell+ , "colorId" .=? _sheetViewColorId+ , "zoomScale" .=? _sheetViewZoomScale+ , "zoomScaleNormal" .=? _sheetViewZoomScaleNormal+ , "zoomScaleSheetLayoutView" .=? _sheetViewZoomScaleSheetLayoutView+ , "zoomScalePageLayoutView" .=? _sheetViewZoomScalePageLayoutView+ , Just $ "workbookViewId" .= _sheetViewWorkbookViewId+ ]+ }++-- | See @CT_Selection@, p. 3914+instance ToElement Selection where+ toElement nm Selection{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = Map.fromList . catMaybes $ [+ "pane" .=? _selectionPane+ , "activeCell" .=? _selectionActiveCell+ , "activeCellId" .=? _selectionActiveCellId+ , "sqref" .=? _selectionSqref+ ]+ }++-- | See @CT_Pane@, p. 3913+instance ToElement Pane where+ toElement nm Pane{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = Map.fromList . catMaybes $ [+ "xSplit" .=? _paneXSplit+ , "ySplit" .=? _paneYSplit+ , "topLeftCell" .=? _paneTopLeftCell+ , "activePane" .=? _paneActivePane+ , "state" .=? _paneState+ ]+ }++-- | See @ST_SheetViewType@, p. 3913+instance ToAttrVal SheetViewType where+ toAttrVal SheetViewTypeNormal = "normal"+ toAttrVal SheetViewTypePageBreakPreview = "pageBreakPreview"+ toAttrVal SheetViewTypePageLayout = "pageLayout"++-- | See @ST_Pane@, p. 3914+instance ToAttrVal PaneType where+ toAttrVal PaneTypeBottomRight = "bottomRight"+ toAttrVal PaneTypeTopRight = "topRight"+ toAttrVal PaneTypeBottomLeft = "bottomLeft"+ toAttrVal PaneTypeTopLeft = "topLeft"++-- | See @ST_PaneState@, p. 3929+instance ToAttrVal PaneState where+ toAttrVal PaneStateSplit = "split"+ toAttrVal PaneStateFrozen = "frozen"+ toAttrVal PaneStateFrozenSplit = "frozenSplit"++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}+-- | See @CT_SheetView@, p. 3913+instance FromCursor SheetView where+ fromCursor cur = do+ _sheetViewWindowProtection <- maybeAttribute "windowProtection" cur+ _sheetViewShowFormulas <- maybeAttribute "showFormulas" cur+ _sheetViewShowGridLines <- maybeAttribute "showGridLines" cur+ _sheetViewShowRowColHeaders <- maybeAttribute "showRowColHeaders"cur+ _sheetViewShowZeros <- maybeAttribute "showZeros" cur+ _sheetViewRightToLeft <- maybeAttribute "rightToLeft" cur+ _sheetViewTabSelected <- maybeAttribute "tabSelected" cur+ _sheetViewShowRuler <- maybeAttribute "showRuler" cur+ _sheetViewShowOutlineSymbols <- maybeAttribute "showOutlineSymbols" cur+ _sheetViewDefaultGridColor <- maybeAttribute "defaultGridColor" cur+ _sheetViewShowWhiteSpace <- maybeAttribute "showWhiteSpace" cur+ _sheetViewType <- maybeAttribute "view" cur+ _sheetViewTopLeftCell <- maybeAttribute "topLeftCell" cur+ _sheetViewColorId <- maybeAttribute "colorId" cur+ _sheetViewZoomScale <- maybeAttribute "zoomScale" cur+ _sheetViewZoomScaleNormal <- maybeAttribute "zoomScaleNormal" cur+ _sheetViewZoomScaleSheetLayoutView <- maybeAttribute "zoomScaleSheetLayoutView" cur+ _sheetViewZoomScalePageLayoutView <- maybeAttribute "zoomScalePageLayoutView" cur+ _sheetViewWorkbookViewId <- fromAttribute "workbookViewId" cur+ let _sheetViewPane = listToMaybe $ cur $/ element (addSmlNamespace "pane") >=> fromCursor+ _sheetViewSelection = cur $/ element (addSmlNamespace "selection") >=> fromCursor+ return SheetView{..}++instance FromXenoNode SheetView where+ fromXenoNode root = parseAttributes root $ do+ _sheetViewWindowProtection <- maybeAttr "windowProtection"+ _sheetViewShowFormulas <- maybeAttr "showFormulas"+ _sheetViewShowGridLines <- maybeAttr "showGridLines"+ _sheetViewShowRowColHeaders <- maybeAttr "showRowColHeaders"+ _sheetViewShowZeros <- maybeAttr "showZeros"+ _sheetViewRightToLeft <- maybeAttr "rightToLeft"+ _sheetViewTabSelected <- maybeAttr "tabSelected"+ _sheetViewShowRuler <- maybeAttr "showRuler"+ _sheetViewShowOutlineSymbols <- maybeAttr "showOutlineSymbols"+ _sheetViewDefaultGridColor <- maybeAttr "defaultGridColor"+ _sheetViewShowWhiteSpace <- maybeAttr "showWhiteSpace"+ _sheetViewType <- maybeAttr "view"+ _sheetViewTopLeftCell <- maybeAttr "topLeftCell"+ _sheetViewColorId <- maybeAttr "colorId"+ _sheetViewZoomScale <- maybeAttr "zoomScale"+ _sheetViewZoomScaleNormal <- maybeAttr "zoomScaleNormal"+ _sheetViewZoomScaleSheetLayoutView <- maybeAttr "zoomScaleSheetLayoutView"+ _sheetViewZoomScalePageLayoutView <- maybeAttr "zoomScalePageLayoutView"+ _sheetViewWorkbookViewId <- fromAttr "workbookViewId"+ (_sheetViewPane, _sheetViewSelection) <-+ toAttrParser . collectChildren root $+ (,) <$> maybeFromChild "pane" <*> fromChildList "selection"+ return SheetView {..}++-- | See @CT_Pane@, p. 3913+instance FromCursor Pane where+ fromCursor cur = do+ _paneXSplit <- maybeAttribute "xSplit" cur+ _paneYSplit <- maybeAttribute "ySplit" cur+ _paneTopLeftCell <- maybeAttribute "topLeftCell" cur+ _paneActivePane <- maybeAttribute "activePane" cur+ _paneState <- maybeAttribute "state" cur+ return Pane{..}++instance FromXenoNode Pane where+ fromXenoNode root =+ parseAttributes root $ do+ _paneXSplit <- maybeAttr "xSplit"+ _paneYSplit <- maybeAttr "ySplit"+ _paneTopLeftCell <- maybeAttr "topLeftCell"+ _paneActivePane <- maybeAttr "activePane"+ _paneState <- maybeAttr "state"+ return Pane {..}++-- | See @CT_Selection@, p. 3914+instance FromCursor Selection where+ fromCursor cur = do+ _selectionPane <- maybeAttribute "pane" cur+ _selectionActiveCell <- maybeAttribute "activeCell" cur+ _selectionActiveCellId <- maybeAttribute "activeCellId" cur+ _selectionSqref <- maybeAttribute "sqref" cur+ return Selection{..}++instance FromXenoNode Selection where+ fromXenoNode root =+ parseAttributes root $ do+ _selectionPane <- maybeAttr "pane"+ _selectionActiveCell <- maybeAttr "activeCell"+ _selectionActiveCellId <- maybeAttr "activeCellId"+ _selectionSqref <- maybeAttr "sqref"+ return Selection {..}++-- | See @ST_SheetViewType@, p. 3913+instance FromAttrVal SheetViewType where+ fromAttrVal "normal" = readSuccess SheetViewTypeNormal+ fromAttrVal "pageBreakPreview" = readSuccess SheetViewTypePageBreakPreview+ fromAttrVal "pageLayout" = readSuccess SheetViewTypePageLayout+ fromAttrVal t = invalidText "SheetViewType" t++instance FromAttrBs SheetViewType where+ fromAttrBs "normal" = return SheetViewTypeNormal+ fromAttrBs "pageBreakPreview" = return SheetViewTypePageBreakPreview+ fromAttrBs "pageLayout" = return SheetViewTypePageLayout+ fromAttrBs x = unexpectedAttrBs "SheetViewType" x++-- | See @ST_Pane@, p. 3914+instance FromAttrVal PaneType where+ fromAttrVal "bottomRight" = readSuccess PaneTypeBottomRight+ fromAttrVal "topRight" = readSuccess PaneTypeTopRight+ fromAttrVal "bottomLeft" = readSuccess PaneTypeBottomLeft+ fromAttrVal "topLeft" = readSuccess PaneTypeTopLeft+ fromAttrVal t = invalidText "PaneType" t++instance FromAttrBs PaneType where+ fromAttrBs "bottomRight" = return PaneTypeBottomRight+ fromAttrBs "topRight" = return PaneTypeTopRight+ fromAttrBs "bottomLeft" = return PaneTypeBottomLeft+ fromAttrBs "topLeft" = return PaneTypeTopLeft+ fromAttrBs x = unexpectedAttrBs "PaneType" x++-- | See @ST_PaneState@, p. 3929+instance FromAttrVal PaneState where+ fromAttrVal "split" = readSuccess PaneStateSplit+ fromAttrVal "frozen" = readSuccess PaneStateFrozen+ fromAttrVal "frozenSplit" = readSuccess PaneStateFrozenSplit+ fromAttrVal t = invalidText "PaneState" t++instance FromAttrBs PaneState where+ fromAttrBs "split" = return PaneStateSplit+ fromAttrBs "frozen" = return PaneStateFrozen+ fromAttrBs "frozenSplit" = return PaneStateFrozenSplit+ fromAttrBs x = unexpectedAttrBs "PaneState" x
+ src/Codec/Xlsx/Types/StyleSheet.hs view
@@ -0,0 +1,1697 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+-- | Support for writing (but not reading) style sheets+module Codec.Xlsx.Types.StyleSheet (+ -- * The main two types+ StyleSheet(..)+ , CellXf(..)+ , minimalStyleSheet+ -- * Supporting record types+ , Alignment(..)+ , Border(..)+ , BorderStyle(..)+ , Color(..)+ , Dxf(..)+ , Fill(..)+ , FillPattern(..)+ , Font(..)+ , NumberFormat(..)+ , NumFmt(..)+ , ImpliedNumberFormat (..)+ , FormatCode+ , Protection(..)+ -- * Supporting enumerations+ , CellHorizontalAlignment(..)+ , CellVerticalAlignment(..)+ , FontFamily(..)+ , FontScheme(..)+ , FontUnderline(..)+ , FontVerticalAlignment(..)+ , LineStyle(..)+ , PatternType(..)+ , ReadingOrder(..)+ -- * Lenses+ -- ** StyleSheet+ , styleSheetBorders+ , styleSheetFonts+ , styleSheetFills+ , styleSheetCellXfs+ , styleSheetDxfs+ , styleSheetNumFmts+ -- ** CellXf+ , cellXfApplyAlignment+ , cellXfApplyBorder+ , cellXfApplyFill+ , cellXfApplyFont+ , cellXfApplyNumberFormat+ , cellXfApplyProtection+ , cellXfBorderId+ , cellXfFillId+ , cellXfFontId+ , cellXfNumFmtId+ , cellXfPivotButton+ , cellXfQuotePrefix+ , cellXfId+ , cellXfAlignment+ , cellXfProtection+ -- ** Dxf+ , dxfAlignment+ , dxfBorder+ , dxfFill+ , dxfFont+ , dxfNumFmt+ , dxfProtection+ -- ** Alignment+ , alignmentHorizontal+ , alignmentIndent+ , alignmentJustifyLastLine+ , alignmentReadingOrder+ , alignmentRelativeIndent+ , alignmentShrinkToFit+ , alignmentTextRotation+ , alignmentVertical+ , alignmentWrapText+ -- ** Border+ , borderDiagonalDown+ , borderDiagonalUp+ , borderOutline+ , borderBottom+ , borderDiagonal+ , borderEnd+ , borderHorizontal+ , borderStart+ , borderTop+ , borderVertical+ , borderLeft+ , borderRight+ -- ** BorderStyle+ , borderStyleColor+ , borderStyleLine+ -- ** Color+ , colorAutomatic+ , colorARGB+ , colorTheme+ , colorTint+ -- ** Fill+ , fillPattern+ -- ** FillPattern+ , fillPatternBgColor+ , fillPatternFgColor+ , fillPatternType+ -- ** Font+ , fontBold+ , fontCharset+ , fontColor+ , fontCondense+ , fontExtend+ , fontFamily+ , fontItalic+ , fontName+ , fontOutline+ , fontScheme+ , fontShadow+ , fontStrikeThrough+ , fontSize+ , fontUnderline+ , fontVertAlign+ -- ** Protection+ , protectionHidden+ , protectionLocked+ -- * Helpers+ -- ** Number formats+ , fmtDecimals+ , fmtDecimalsZeroes+ , stdNumberFormatId+ , idToStdNumberFormat+ , firstUserNumFmtId+ ) where++import Codec.Xlsx.LensCompat (makeLenses, (&), (.~))+import Control.DeepSeq (NFData)+import Data.Default+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as M+import Data.Maybe (catMaybes, maybeToList)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++{-------------------------------------------------------------------------------+ The main types+-------------------------------------------------------------------------------}++-- | StyleSheet for an XML document+--+-- Relevant parts of the EMCA standard (4th edition, part 1,+-- <https://ecma-international.org/publications-and-standards/standards/ecma-376/>),+-- page numbers refer to the page in the PDF rather than the page number as+-- printed on the page):+--+-- * Chapter 12, \"SpreadsheetML\" (p. 74)+-- In particular Section 12.3.20, "Styles Part" (p. 104)+-- * Chapter 18, \"SpreadsheetML Reference Material\" (p. 1528)+-- In particular Section 18.8, \"Styles\" (p. 1754) and Section 18.8.39+-- \"styleSheet\" (Style Sheet)\" (p. 1796); it is the latter section that+-- specifies the top-level style sheet format.+--+-- TODO: the following child elements:+--+-- * cellStyles+-- * cellStyleXfs+-- * colors+-- * extLst+-- * tableStyles+--+-- NOTE: Because of undocumented Excel requirements you will probably want to base+-- your style sheet on 'minimalStyleSheet' (a proper style sheet should have some+-- contents for details see+-- <https://stackoverflow.com/questions/26050708/minimal-style-sheet-for-excel-open-xml-with-dates SO post>).+-- 'def' for 'StyleSheet' includes no contents at all and this could be a problem+-- for Excel.+--+-- See also:+--+-- * 'Codec.Xlsx.Types.renderStyleSheet' to translate a 'StyleSheet' to 'Styles'+-- * 'Codec.Xlsx.Formatted.formatted' for a higher level interface.+-- * 'Codec.Xlsx.Types.parseStyleSheet' to translate a raw 'StyleSheet' into 'Styles'+data StyleSheet = StyleSheet+ { _styleSheetBorders :: [Border]+ -- ^ This element contains borders formatting information, specifying all+ -- border definitions for all cells in the workbook.+ --+ -- Section 18.8.5, "borders (Borders)" (p. 1760)++ , _styleSheetCellXfs :: [CellXf]+ -- ^ Cell formats+ --+ -- This element contains the master formatting records (xf) which define the+ -- formatting applied to cells in this workbook. These records are the+ -- starting point for determining the formatting for a cell. Cells in the+ -- Sheet Part reference the xf records by zero-based index.+ --+ -- Section 18.8.10, "cellXfs (Cell Formats)" (p. 1764)++ , _styleSheetFills :: [Fill]+ -- ^ This element defines the cell fills portion of the Styles part,+ -- consisting of a sequence of fill records. A cell fill consists of a+ -- background color, foreground color, and pattern to be applied across the+ -- cell.+ --+ -- Section 18.8.21, "fills (Fills)" (p. 1768)++ , _styleSheetFonts :: [Font]+ -- ^ This element contains all font definitions for this workbook.+ --+ -- Section 18.8.23 "fonts (Fonts)" (p. 1769)++ , _styleSheetDxfs :: [Dxf]+ -- ^ Differential formatting+ --+ -- This element contains the master differential formatting records (dxf's)+ -- which define formatting for all non-cell formatting in this workbook.+ -- Whereas xf records fully specify a particular aspect of formatting (e.g.,+ -- cell borders) by referencing those formatting definitions elsewhere in+ -- the Styles part, dxf records specify incremental (or differential) aspects+ -- of formatting directly inline within the dxf element. The dxf formatting+ -- is to be applied on top of or in addition to any formatting already+ -- present on the object using the dxf record.+ --+ -- Section 18.8.15, "dxfs (Formats)" (p. 1765)++ , _styleSheetNumFmts :: Map Int FormatCode+ -- ^ Number formats+ --+ -- This element contains custom number formats defined in this style sheet+ --+ -- Section 18.8.31, "numFmts (Number Formats)" (p. 1784)+ } deriving (Eq, Ord, Show, Generic)++instance NFData StyleSheet++-- | Cell formatting+--+-- TODO: The @extLst@ field is currently unsupported.+--+-- Section 18.8.45 "xf (Format)" (p. 1800)+data CellXf = CellXf {+ -- | A boolean value indicating whether the alignment formatting specified+ -- for this xf should be applied.+ _cellXfApplyAlignment :: Maybe Bool++ -- | A boolean value indicating whether the border formatting specified for+ -- this xf should be applied.+ , _cellXfApplyBorder :: Maybe Bool++ -- | A boolean value indicating whether the fill formatting specified for+ -- this xf should be applied.+ , _cellXfApplyFill :: Maybe Bool++ -- | A boolean value indicating whether the font formatting specified for+ -- this xf should be applied.+ , _cellXfApplyFont :: Maybe Bool++ -- | A boolean value indicating whether the number formatting specified for+ -- this xf should be applied.+ , _cellXfApplyNumberFormat :: Maybe Bool++ -- | A boolean value indicating whether the protection formatting specified+ -- for this xf should be applied.+ , _cellXfApplyProtection :: Maybe Bool++ -- | Zero-based index of the border record used by this cell format.+ --+ -- (18.18.2, p. 2437).+ , _cellXfBorderId :: Maybe Int++ -- | Zero-based index of the fill record used by this cell format.+ --+ -- (18.18.30, p. 2455)+ , _cellXfFillId :: Maybe Int++ -- | Zero-based index of the font record used by this cell format.+ --+ -- An integer that represents a zero based index into the `styleSheetFonts`+ -- collection in the style sheet (18.18.32, p. 2456).+ , _cellXfFontId :: Maybe Int++ -- | Id of the number format (numFmt) record used by this cell format.+ --+ -- This simple type defines the identifier to a style sheet number format+ -- entry in CT_NumFmts. Number formats are written to the styles part+ -- (18.18.47, p. 2468). See also 18.8.31 (p. 1784) for more information on+ -- number formats.+ --+ , _cellXfNumFmtId :: Maybe Int++ -- | A boolean value indicating whether the cell rendering includes a pivot+ -- table dropdown button.+ , _cellXfPivotButton :: Maybe Bool++ -- | A boolean value indicating whether the text string in a cell should be+ -- prefixed by a single quote mark (e.g., 'text). In these cases, the quote+ -- is not stored in the Shared Strings Part.+ , _cellXfQuotePrefix :: Maybe Bool++ -- | For xf records contained in cellXfs this is the zero-based index of an+ -- xf record contained in cellStyleXfs corresponding to the cell style+ -- applied to the cell.+ --+ -- Not present for xf records contained in cellStyleXfs.+ --+ -- Used by xf records and cellStyle records to reference xf records defined+ -- in the cellStyleXfs collection. (18.18.10, p. 2442)+ -- TODO: the cellStyleXfs field of a style sheet not currently implemented.+ , _cellXfId :: Maybe Int++ -- | Formatting information pertaining to text alignment in cells. There are+ -- a variety of choices for how text is aligned both horizontally and+ -- vertically, as well as indentation settings, and so on.+ , _cellXfAlignment :: Maybe Alignment++ -- | Contains protection properties associated with the cell. Each cell has+ -- protection properties that can be set. The cell protection properties do+ -- not take effect unless the sheet has been protected.+ , _cellXfProtection :: Maybe Protection+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData CellXf++{-------------------------------------------------------------------------------+ Supporting record types+-------------------------------------------------------------------------------}++-- | Alignment+--+-- See 18.8.1 "alignment (Alignment)" (p. 1754)+data Alignment = Alignment {+ -- | Specifies the type of horizontal alignment in cells.+ _alignmentHorizontal :: Maybe CellHorizontalAlignment++ -- | An integer value, where an increment of 1 represents 3 spaces.+ -- Indicates the number of spaces (of the normal style font) of indentation+ -- for text in a cell.+ , _alignmentIndent :: Maybe Int++ -- | A boolean value indicating if the cells justified or distributed+ -- alignment should be used on the last line of text. (This is typical for+ -- East Asian alignments but not typical in other contexts.)+ , _alignmentJustifyLastLine :: Maybe Bool++ -- | An integer value indicating whether the reading order+ -- (bidirectionality) of the cell is leftto- right, right-to-left, or+ -- context dependent.+ , _alignmentReadingOrder :: Maybe ReadingOrder++ -- | An integer value (used only in a dxf element) to indicate the+ -- additional number of spaces of indentation to adjust for text in a cell.+ , _alignmentRelativeIndent :: Maybe Int++ -- | A boolean value indicating if the displayed text in the cell should be+ -- shrunk to fit the cell width. Not applicable when a cell contains+ -- multiple lines of text.+ , _alignmentShrinkToFit :: Maybe Bool++ -- | Text rotation in cells. Expressed in degrees. Values range from 0 to+ -- 180. The first letter of the text is considered the center-point of the+ -- arc.+ , _alignmentTextRotation :: Maybe Int++ -- | Vertical alignment in cells.+ , _alignmentVertical :: Maybe CellVerticalAlignment++ -- | A boolean value indicating if the text in a cell should be line-wrapped+ -- within the cell.+ , _alignmentWrapText :: Maybe Bool+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData Alignment++-- | Expresses a single set of cell border formats (left, right, top, bottom,+-- diagonal). Color is optional. When missing, 'automatic' is implied.+--+-- See 18.8.4 "border (Border)" (p. 1759)+data Border = Border {+ -- | A boolean value indicating if the cell's diagonal border includes a+ -- diagonal line, starting at the top left corner of the cell and moving+ -- down to the bottom right corner of the cell.+ _borderDiagonalDown :: Maybe Bool++ -- | A boolean value indicating if the cell's diagonal border includes a+ -- diagonal line, starting at the bottom left corner of the cell and moving+ -- up to the top right corner of the cell.+ , _borderDiagonalUp :: Maybe Bool++ -- | A boolean value indicating if left, right, top, and bottom borders+ -- should be applied only to outside borders of a cell range.+ , _borderOutline :: Maybe Bool++ -- | Bottom border+ , _borderBottom :: Maybe BorderStyle++ -- | Diagonal+ , _borderDiagonal :: Maybe BorderStyle++ -- | Trailing edge border+ --+ -- See also 'borderRight'+ , _borderEnd :: Maybe BorderStyle++ -- | Horizontal inner borders+ , _borderHorizontal :: Maybe BorderStyle++ -- | Left border+ --+ -- NOTE: The spec does not formally list a 'left' border element, but the+ -- examples do mention 'left' and the scheme contains it too. See also 'borderStart'.+ , _borderLeft :: Maybe BorderStyle++ -- | Right border+ --+ -- NOTE: The spec does not formally list a 'right' border element, but the+ -- examples do mention 'right' and the scheme contains it too. See also 'borderEnd'.+ , _borderRight :: Maybe BorderStyle++ -- | Leading edge border+ --+ -- See also 'borderLeft'+ , _borderStart :: Maybe BorderStyle++ -- | Top border+ , _borderTop :: Maybe BorderStyle++ -- | Vertical inner border+ , _borderVertical :: Maybe BorderStyle+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData Border++-- | Border style+-- See @CT_BorderPr@ (p. 3934)+data BorderStyle = BorderStyle {+ _borderStyleColor :: Maybe Color+ , _borderStyleLine :: Maybe LineStyle+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData BorderStyle++-- | One of the colors associated with the data bar or color scale.+--+-- The 'indexed' attribute (used for backwards compatibility only) is not+-- modelled here.+--+-- See 18.3.1.15 "color (Data Bar Color)" (p. 1608)+data Color = Color {+ -- | A boolean value indicating the color is automatic and system color+ -- dependent.+ _colorAutomatic :: Maybe Bool++ -- | Standard Alpha Red Green Blue color value (ARGB).+ --+ -- This simple type's contents have a length of exactly 8 hexadecimal+ -- digit(s); see "18.18.86 ST_UnsignedIntHex (Hex Unsigned Integer)" (p.+ -- 2511).+ , _colorARGB :: Maybe Text++ -- | A zero-based index into the <clrScheme> collection (20.1.6.2),+ -- referencing a particular <sysClr> or <srgbClr> value expressed in the+ -- Theme part.+ , _colorTheme :: Maybe Int++ -- | Specifies the tint value applied to the color.+ --+ -- If tint is supplied, then it is applied to the RGB value of the color to+ -- determine the final color applied.+ --+ -- The tint value is stored as a double from -1.0 .. 1.0, where -1.0 means+ -- 100% darken and 1.0 means 100% lighten. Also, 0.0 means no change.+ , _colorTint :: Maybe Double+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData Color++-- | This element specifies fill formatting.+--+-- TODO: Gradient fills (18.8.4) are currently unsupported. If we add them,+-- then the spec says (@CT_Fill@, p. 3935), _either_ a gradient _or_ a solid+-- fill pattern should be specified.+--+-- Section 18.8.20, "fill (Fill)" (p. 1768)+data Fill = Fill {+ _fillPattern :: Maybe FillPattern+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData Fill++-- | This element is used to specify cell fill information for pattern and solid+-- color cell fills. For solid cell fills (no pattern), fgColor is used. For+-- cell fills with patterns specified, then the cell fill color is specified by+-- the bgColor element.+--+-- Section 18.8.32 "patternFill (Pattern)" (p. 1793)+data FillPattern = FillPattern {+ _fillPatternBgColor :: Maybe Color+ , _fillPatternFgColor :: Maybe Color+ , _fillPatternType :: Maybe PatternType+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData FillPattern++-- | This element defines the properties for one of the fonts used in this+-- workbook.+--+-- Section 18.2.22 "font (Font)" (p. 1769)+data Font = Font {+ -- | Displays characters in bold face font style.+ _fontBold :: Maybe Bool++ -- | This element defines the font character set of this font.+ --+ -- This field is used in font creation and selection if a font of the given+ -- facename is not available on the system. Although it is not required to+ -- have around when resolving font facename, the information can be stored+ -- for when needed to help resolve which font face to use of all available+ -- fonts on a system.+ --+ -- Charset represents the basic set of characters associated with a font+ -- (that it can display), and roughly corresponds to the ANSI codepage+ -- (8-bit or DBCS) of that character set used by a given language. Given+ -- more common use of Unicode where many fonts support more than one of the+ -- traditional charset categories, and the use of font linking, using+ -- charset to resolve font name is less and less common, but still can be+ -- useful.+ --+ -- These are operating-system-dependent values.+ --+ -- Section 18.4.1 "charset (Character Set)" provides some example values.+ , _fontCharset :: Maybe Int++ -- | Color+ , _fontColor :: Maybe Color++ -- | Macintosh compatibility setting. Represents special word/character+ -- rendering on Macintosh, when this flag is set. The effect is to condense+ -- the text (squeeze it together). SpreadsheetML applications are not+ -- required to render according to this flag.+ , _fontCondense :: Maybe Bool++ -- | This element specifies a compatibility setting used for previous+ -- spreadsheet applications, resulting in special word/character rendering+ -- on those legacy applications, when this flag is set. The effect extends+ -- or stretches out the text. SpreadsheetML applications are not required to+ -- render according to this flag.+ , _fontExtend :: Maybe Bool++ -- | The font family this font belongs to. A font family is a set of fonts+ -- having common stroke width and serif characteristics. This is system+ -- level font information. The font name overrides when there are+ -- conflicting values.+ , _fontFamily :: Maybe FontFamily++ -- | Displays characters in italic font style. The italic style is defined+ -- by the font at a system level and is not specified by ECMA-376.+ , _fontItalic :: Maybe Bool++ -- | This element specifies the face name of this font.+ --+ -- A string representing the name of the font. If the font doesn't exist+ -- (because it isn't installed on the system), or the charset not supported+ -- by that font, then another font should be substituted.+ --+ -- The string length for this attribute shall be 0 to 31 characters.+ , _fontName :: Maybe Text++ -- | This element displays only the inner and outer borders of each+ -- character. This is very similar to Bold in behavior.+ , _fontOutline :: Maybe Bool++ -- | Defines the font scheme, if any, to which this font belongs. When a+ -- font definition is part of a theme definition, then the font is+ -- categorized as either a major or minor font scheme component. When a new+ -- theme is chosen, every font that is part of a theme definition is updated+ -- to use the new major or minor font definition for that theme. Usually+ -- major fonts are used for styles like headings, and minor fonts are used+ -- for body and paragraph text.+ , _fontScheme :: Maybe FontScheme++ -- | Macintosh compatibility setting. Represents special word/character+ -- rendering on Macintosh, when this flag is set. The effect is to render a+ -- shadow behind, beneath and to the right of the text. SpreadsheetML+ -- applications are not required to render according to this flag.+ , _fontShadow :: Maybe Bool++ -- | This element draws a strikethrough line through the horizontal middle+ -- of the text.+ , _fontStrikeThrough :: Maybe Bool++ -- | This element represents the point size (1/72 of an inch) of the Latin+ -- and East Asian text.+ , _fontSize :: Maybe Double++ -- | This element represents the underline formatting style.+ , _fontUnderline :: Maybe FontUnderline++ -- | This element adjusts the vertical position of the text relative to the+ -- text's default appearance for this run. It is used to get 'superscript'+ -- or 'subscript' texts, and shall reduce the font size (if a smaller size+ -- is available) accordingly.+ , _fontVertAlign :: Maybe FontVerticalAlignment+ }+ deriving (Eq, Ord, Show, Generic)++instance NFData Font++-- | A single dxf record, expressing incremental formatting to be applied.+--+-- Section 18.8.14, "dxf (Formatting)" (p. 1765)+data Dxf = Dxf+ { _dxfFont :: Maybe Font+ -- | It seems to be required that this number format entry is duplicated+ -- in '_styleSheetNumFmts' of the style sheet, though the spec says+ -- nothing explicitly about it.+ , _dxfNumFmt :: Maybe NumFmt+ , _dxfFill :: Maybe Fill+ , _dxfAlignment :: Maybe Alignment+ , _dxfBorder :: Maybe Border+ , _dxfProtection :: Maybe Protection+ -- TODO: extList+ } deriving (Eq, Ord, Show, Generic)++instance NFData Dxf++-- | A number format code.+--+-- Section 18.8.30, "numFmt (Number Format)" (p. 1777)+type FormatCode = Text++-- | This element specifies number format properties which indicate+-- how to format and render the numeric value of a cell.+--+-- Section 18.8.30 "numFmt (Number Format)" (p. 1777)+data NumFmt = NumFmt+ { _numFmtId :: Int+ , _numFmtCode :: FormatCode+ } deriving (Eq, Ord, Show, Generic)++instance NFData NumFmt++mkNumFmtPair :: NumFmt -> (Int, FormatCode)+mkNumFmtPair NumFmt{..} = (_numFmtId, _numFmtCode)++-- | This type gives a high-level version of representation of number format+-- used in 'Codec.Xlsx.Formatted.Format'.+data NumberFormat+ = StdNumberFormat ImpliedNumberFormat+ | UserNumberFormat FormatCode+ deriving (Eq, Ord, Show, Generic)++instance NFData NumberFormat++-- | Basic number format with predefined number of decimals+-- as format code of number format in xlsx should be less than 255 characters+-- number of decimals shouldn't be more than 253+fmtDecimals :: Int -> NumberFormat+fmtDecimals k = UserNumberFormat $ "0." <> T.replicate k "#"++-- | Basic number format with predefined number of decimals.+-- Works like 'fmtDecimals' with the only difference that extra zeroes are+-- displayed when number of digits after the point is less than the number+-- of digits specified in the format+fmtDecimalsZeroes :: Int -> NumberFormat+fmtDecimalsZeroes k = UserNumberFormat $ "0." <> T.replicate k "0"++-- | Implied number formats+--+-- /Note:/ This only implements the predefined values for 18.2.30 "All Languages",+-- other built-in format ids (with id < 'firstUserNumFmtId') are stored in 'NfOtherBuiltin'+data ImpliedNumberFormat =+ NfGeneral -- ^> 0 General+ | NfZero -- ^> 1 0+ | Nf2Decimal -- ^> 2 0.00+ | NfMax3Decimal -- ^> 3 #,##0+ | NfThousandSeparator2Decimal -- ^> 4 #,##0.00+ | NfPercent -- ^> 9 0%+ | NfPercent2Decimal -- ^> 10 0.00%+ | NfExponent2Decimal -- ^> 11 0.00E+00+ | NfSingleSpacedFraction -- ^> 12 # ?/?+ | NfDoubleSpacedFraction -- ^> 13 # ??/??+ | NfMmDdYy -- ^> 14 mm-dd-yy+ | NfDMmmYy -- ^> 15 d-mmm-yy+ | NfDMmm -- ^> 16 d-mmm+ | NfMmmYy -- ^> 17 mmm-yy+ | NfHMm12Hr -- ^> 18 h:mm AM/PM+ | NfHMmSs12Hr -- ^> 19 h:mm:ss AM/PM+ | NfHMm -- ^> 20 h:mm+ | NfHMmSs -- ^> 21 h:mm:ss+ | NfMdyHMm -- ^> 22 m/d/yy h:mm+ | NfThousandsNegativeParens -- ^> 37 #,##0 ;(#,##0)+ | NfThousandsNegativeRed -- ^> 38 #,##0 ;[Red](#,##0)+ | NfThousands2DecimalNegativeParens -- ^> 39 #,##0.00;(#,##0.00)+ | NfThousands2DecimalNegativeRed -- ^> 40 #,##0.00;[Red](#,##0.00)+ | NfMmSs -- ^> 45 mm:ss+ | NfOptHMmSs -- ^> 46 [h]:mm:ss+ | NfMmSs1Decimal -- ^> 47 mmss.0+ | NfExponent1Decimal -- ^> 48 ##0.0E+0+ | NfTextPlaceHolder -- ^> 49 @+ | NfOtherImplied Int -- ^ other (non local-neutral?) built-in format (id < 164)+ deriving (Eq, Ord, Show, Generic)++instance NFData ImpliedNumberFormat++stdNumberFormatId :: ImpliedNumberFormat -> Int+stdNumberFormatId NfGeneral = 0 -- General+stdNumberFormatId NfZero = 1 -- 0+stdNumberFormatId Nf2Decimal = 2 -- 0.00+stdNumberFormatId NfMax3Decimal = 3 -- #,##0+stdNumberFormatId NfThousandSeparator2Decimal = 4 -- #,##0.00+stdNumberFormatId NfPercent = 9 -- 0%+stdNumberFormatId NfPercent2Decimal = 10 -- 0.00%+stdNumberFormatId NfExponent2Decimal = 11 -- 0.00E+00+stdNumberFormatId NfSingleSpacedFraction = 12 -- # ?/?+stdNumberFormatId NfDoubleSpacedFraction = 13 -- # ??/??+stdNumberFormatId NfMmDdYy = 14 -- mm-dd-yy+stdNumberFormatId NfDMmmYy = 15 -- d-mmm-yy+stdNumberFormatId NfDMmm = 16 -- d-mmm+stdNumberFormatId NfMmmYy = 17 -- mmm-yy+stdNumberFormatId NfHMm12Hr = 18 -- h:mm AM/PM+stdNumberFormatId NfHMmSs12Hr = 19 -- h:mm:ss AM/PM+stdNumberFormatId NfHMm = 20 -- h:mm+stdNumberFormatId NfHMmSs = 21 -- h:mm:ss+stdNumberFormatId NfMdyHMm = 22 -- m/d/yy h:mm+stdNumberFormatId NfThousandsNegativeParens = 37 -- #,##0 ;(#,##0)+stdNumberFormatId NfThousandsNegativeRed = 38 -- #,##0 ;[Red](#,##0)+stdNumberFormatId NfThousands2DecimalNegativeParens = 39 -- #,##0.00;(#,##0.00)+stdNumberFormatId NfThousands2DecimalNegativeRed = 40 -- #,##0.00;[Red](#,##0.00)+stdNumberFormatId NfMmSs = 45 -- mm:ss+stdNumberFormatId NfOptHMmSs = 46 -- [h]:mm:ss+stdNumberFormatId NfMmSs1Decimal = 47 -- mmss.0+stdNumberFormatId NfExponent1Decimal = 48 -- ##0.0E+0+stdNumberFormatId NfTextPlaceHolder = 49 -- @+stdNumberFormatId (NfOtherImplied i) = i++idToStdNumberFormat :: Int -> Maybe ImpliedNumberFormat+idToStdNumberFormat 0 = Just NfGeneral -- General+idToStdNumberFormat 1 = Just NfZero -- 0+idToStdNumberFormat 2 = Just Nf2Decimal -- 0.00+idToStdNumberFormat 3 = Just NfMax3Decimal -- #,##0+idToStdNumberFormat 4 = Just NfThousandSeparator2Decimal -- #,##0.00+idToStdNumberFormat 9 = Just NfPercent -- 0%+idToStdNumberFormat 10 = Just NfPercent2Decimal -- 0.00%+idToStdNumberFormat 11 = Just NfExponent2Decimal -- 0.00E+00+idToStdNumberFormat 12 = Just NfSingleSpacedFraction -- # ?/?+idToStdNumberFormat 13 = Just NfDoubleSpacedFraction -- # ??/??+idToStdNumberFormat 14 = Just NfMmDdYy -- mm-dd-yy+idToStdNumberFormat 15 = Just NfDMmmYy -- d-mmm-yy+idToStdNumberFormat 16 = Just NfDMmm -- d-mmm+idToStdNumberFormat 17 = Just NfMmmYy -- mmm-yy+idToStdNumberFormat 18 = Just NfHMm12Hr -- h:mm AM/PM+idToStdNumberFormat 19 = Just NfHMmSs12Hr -- h:mm:ss AM/PM+idToStdNumberFormat 20 = Just NfHMm -- h:mm+idToStdNumberFormat 21 = Just NfHMmSs -- h:mm:ss+idToStdNumberFormat 22 = Just NfMdyHMm -- m/d/yy h:mm+idToStdNumberFormat 37 = Just NfThousandsNegativeParens -- #,##0 ;(#,##0)+idToStdNumberFormat 38 = Just NfThousandsNegativeRed -- #,##0 ;[Red](#,##0)+idToStdNumberFormat 39 = Just NfThousands2DecimalNegativeParens -- #,##0.00;(#,##0.00)+idToStdNumberFormat 40 = Just NfThousands2DecimalNegativeRed -- #,##0.00;[Red](#,##0.00)+idToStdNumberFormat 45 = Just NfMmSs -- mm:ss+idToStdNumberFormat 46 = Just NfOptHMmSs -- [h]:mm:ss+idToStdNumberFormat 47 = Just NfMmSs1Decimal -- mmss.0+idToStdNumberFormat 48 = Just NfExponent1Decimal -- ##0.0E+0+idToStdNumberFormat 49 = Just NfTextPlaceHolder -- @+idToStdNumberFormat i = if i < firstUserNumFmtId then Just (NfOtherImplied i) else Nothing++firstUserNumFmtId :: Int+firstUserNumFmtId = 164++-- | Protection properties+--+-- Contains protection properties associated with the cell. Each cell has+-- protection properties that can be set. The cell protection properties do not+-- take effect unless the sheet has been protected.+--+-- Section 18.8.33, "protection (Protection Properties)", p. 1793+data Protection = Protection {+ _protectionHidden :: Maybe Bool+ , _protectionLocked :: Maybe Bool+ }+ deriving (Eq, Ord, Show, Generic)+instance NFData Protection++{-------------------------------------------------------------------------------+ Enumerations+-------------------------------------------------------------------------------}++-- | Horizontal alignment in cells+--+-- See 18.18.40 "ST_HorizontalAlignment (Horizontal Alignment Type)" (p. 2459)+data CellHorizontalAlignment =+ CellHorizontalAlignmentCenter+ | CellHorizontalAlignmentCenterContinuous+ | CellHorizontalAlignmentDistributed+ | CellHorizontalAlignmentFill+ | CellHorizontalAlignmentGeneral+ | CellHorizontalAlignmentJustify+ | CellHorizontalAlignmentLeft+ | CellHorizontalAlignmentRight+ deriving (Eq, Ord, Show, Generic)+instance NFData CellHorizontalAlignment++-- | Vertical alignment in cells+--+-- See 18.18.88 "ST_VerticalAlignment (Vertical Alignment Types)" (p. 2512)+data CellVerticalAlignment =+ CellVerticalAlignmentBottom+ | CellVerticalAlignmentCenter+ | CellVerticalAlignmentDistributed+ | CellVerticalAlignmentJustify+ | CellVerticalAlignmentTop+ deriving (Eq, Ord, Show, Generic)+instance NFData CellVerticalAlignment++-- | Font family+--+-- See 18.8.18 "family (Font Family)" (p. 1766)+-- and 17.18.30 "ST_FontFamily (Font Family Value)" (p. 1388)+data FontFamily =+ -- | Family is not applicable+ FontFamilyNotApplicable++ -- | Proportional font with serifs+ | FontFamilyRoman++ -- | Proportional font without serifs+ | FontFamilySwiss++ -- | Monospace font with or without serifs+ | FontFamilyModern++ -- | Script font designed to mimic the appearance of handwriting+ | FontFamilyScript++ -- | Novelty font+ | FontFamilyDecorative+ deriving (Eq, Ord, Show, Generic)+instance NFData FontFamily++-- | Font scheme+--+-- See 18.18.33 "ST_FontScheme (Font scheme Styles)" (p. 2456)+data FontScheme =+ -- | This font is the major font for this theme.+ FontSchemeMajor++ -- | This font is the minor font for this theme.+ | FontSchemeMinor++ -- | This font is not a theme font.+ | FontSchemeNone+ deriving (Eq, Ord, Show, Generic)+instance NFData FontScheme++-- | Font underline property+--+-- See 18.4.13 "u (Underline)", p 1728+data FontUnderline =+ FontUnderlineSingle+ | FontUnderlineDouble+ | FontUnderlineSingleAccounting+ | FontUnderlineDoubleAccounting+ | FontUnderlineNone+ deriving (Eq, Ord, Show, Generic)+instance NFData FontUnderline++-- | Vertical alignment+--+-- See 22.9.2.17 "ST_VerticalAlignRun (Vertical Positioning Location)" (p. 3794)+data FontVerticalAlignment =+ FontVerticalAlignmentBaseline+ | FontVerticalAlignmentSubscript+ | FontVerticalAlignmentSuperscript+ deriving (Eq, Ord, Show, Generic)+instance NFData FontVerticalAlignment++data LineStyle =+ LineStyleDashDot+ | LineStyleDashDotDot+ | LineStyleDashed+ | LineStyleDotted+ | LineStyleDouble+ | LineStyleHair+ | LineStyleMedium+ | LineStyleMediumDashDot+ | LineStyleMediumDashDotDot+ | LineStyleMediumDashed+ | LineStyleNone+ | LineStyleSlantDashDot+ | LineStyleThick+ | LineStyleThin+ deriving (Eq, Ord, Show, Generic)+instance NFData LineStyle++-- | Indicates the style of fill pattern being used for a cell format.+--+-- Section 18.18.55 "ST_PatternType (Pattern Type)" (p. 2472)+data PatternType =+ PatternTypeDarkDown+ | PatternTypeDarkGray+ | PatternTypeDarkGrid+ | PatternTypeDarkHorizontal+ | PatternTypeDarkTrellis+ | PatternTypeDarkUp+ | PatternTypeDarkVertical+ | PatternTypeGray0625+ | PatternTypeGray125+ | PatternTypeLightDown+ | PatternTypeLightGray+ | PatternTypeLightGrid+ | PatternTypeLightHorizontal+ | PatternTypeLightTrellis+ | PatternTypeLightUp+ | PatternTypeLightVertical+ | PatternTypeMediumGray+ | PatternTypeNone+ | PatternTypeSolid+ deriving (Eq, Ord, Show, Generic)+instance NFData PatternType++-- | Reading order+--+-- See 18.8.1 "alignment (Alignment)" (p. 1754, esp. p. 1755)+data ReadingOrder =+ ReadingOrderContextDependent+ | ReadingOrderLeftToRight+ | ReadingOrderRightToLeft+ deriving (Eq, Ord, Show, Generic)+instance NFData ReadingOrder++{-------------------------------------------------------------------------------+ Lenses+-------------------------------------------------------------------------------}++makeLenses ''StyleSheet+makeLenses ''CellXf+makeLenses ''Dxf++makeLenses ''Alignment+makeLenses ''Border+makeLenses ''BorderStyle+makeLenses ''Color+makeLenses ''Fill+makeLenses ''FillPattern+makeLenses ''Font+makeLenses ''Protection++{-------------------------------------------------------------------------------+ Minimal stylesheet+-------------------------------------------------------------------------------}++-- | Minimal style sheet+--+-- Excel expects some minimal definitions in the stylesheet; you probably want+-- to define your own stylesheets based on this one.+--+-- This more-or-less follows the recommendations at+-- <http://stackoverflow.com/questions/26050708/minimal-style-sheet-for-excel-open-xml-with-dates>,+-- but with some additions based on experimental evidence.+minimalStyleSheet :: StyleSheet+minimalStyleSheet = def+ & styleSheetBorders .~ [defaultBorder]+ & styleSheetFonts .~ [defaultFont]+ & styleSheetFills .~ [fillNone, fillGray125]+ & styleSheetCellXfs .~ [defaultCellXf]+ where+ -- The 'Default' instance for 'Border' uses 'left' and 'right' rather than+ -- 'start' and 'end', because this is what Excel does (even though the spec+ -- says different)+ defaultBorder :: Border+ defaultBorder = def+ & borderBottom .~ Just def+ & borderTop .~ Just def+ & borderLeft .~ Just def+ & borderRight .~ Just def++ defaultFont :: Font+ defaultFont = def+ & fontFamily .~ Just FontFamilySwiss+ & fontSize .~ Just 11++ fillNone, fillGray125 :: Fill+ fillNone = def+ & fillPattern .~ Just (def & fillPatternType .~ Just PatternTypeNone)+ fillGray125 = def+ & fillPattern .~ Just (def & fillPatternType .~ Just PatternTypeGray125)++ defaultCellXf :: CellXf+ defaultCellXf = def+ & cellXfBorderId .~ Just 0+ & cellXfFillId .~ Just 0+ & cellXfFontId .~ Just 0++{-------------------------------------------------------------------------------+ Default instances+-------------------------------------------------------------------------------}++instance Default StyleSheet where+ def = StyleSheet {+ _styleSheetBorders = []+ , _styleSheetFonts = []+ , _styleSheetFills = []+ , _styleSheetCellXfs = []+ , _styleSheetDxfs = []+ , _styleSheetNumFmts = M.empty+ }++instance Default CellXf where+ def = CellXf {+ _cellXfApplyAlignment = Nothing+ , _cellXfApplyBorder = Nothing+ , _cellXfApplyFill = Nothing+ , _cellXfApplyFont = Nothing+ , _cellXfApplyNumberFormat = Nothing+ , _cellXfApplyProtection = Nothing+ , _cellXfBorderId = Nothing+ , _cellXfFillId = Nothing+ , _cellXfFontId = Nothing+ , _cellXfNumFmtId = Nothing+ , _cellXfPivotButton = Nothing+ , _cellXfQuotePrefix = Nothing+ , _cellXfId = Nothing+ , _cellXfAlignment = Nothing+ , _cellXfProtection = Nothing+ }++instance Default Dxf where+ def = Dxf+ { _dxfFont = Nothing+ , _dxfNumFmt = Nothing+ , _dxfFill = Nothing+ , _dxfAlignment = Nothing+ , _dxfBorder = Nothing+ , _dxfProtection = Nothing+ }++instance Default Alignment where+ def = Alignment {+ _alignmentHorizontal = Nothing+ , _alignmentIndent = Nothing+ , _alignmentJustifyLastLine = Nothing+ , _alignmentReadingOrder = Nothing+ , _alignmentRelativeIndent = Nothing+ , _alignmentShrinkToFit = Nothing+ , _alignmentTextRotation = Nothing+ , _alignmentVertical = Nothing+ , _alignmentWrapText = Nothing+ }++instance Default Border where+ def = Border {+ _borderDiagonalDown = Nothing+ , _borderDiagonalUp = Nothing+ , _borderOutline = Nothing+ , _borderBottom = Nothing+ , _borderDiagonal = Nothing+ , _borderEnd = Nothing+ , _borderHorizontal = Nothing+ , _borderStart = Nothing+ , _borderTop = Nothing+ , _borderVertical = Nothing+ , _borderLeft = Nothing+ , _borderRight = Nothing+ }++instance Default BorderStyle where+ def = BorderStyle {+ _borderStyleColor = Nothing+ , _borderStyleLine = Nothing+ }++instance Default Color where+ def = Color {+ _colorAutomatic = Nothing+ , _colorARGB = Nothing+ , _colorTheme = Nothing+ , _colorTint = Nothing+ }++instance Default Fill where+ def = Fill {+ _fillPattern = Nothing+ }++instance Default FillPattern where+ def = FillPattern {+ _fillPatternBgColor = Nothing+ , _fillPatternFgColor = Nothing+ , _fillPatternType = Nothing+ }++instance Default Font where+ def = Font {+ _fontBold = Nothing+ , _fontCharset = Nothing+ , _fontColor = Nothing+ , _fontCondense = Nothing+ , _fontExtend = Nothing+ , _fontFamily = Nothing+ , _fontItalic = Nothing+ , _fontName = Nothing+ , _fontOutline = Nothing+ , _fontScheme = Nothing+ , _fontShadow = Nothing+ , _fontStrikeThrough = Nothing+ , _fontSize = Nothing+ , _fontUnderline = Nothing+ , _fontVertAlign = Nothing+ }++instance Default Protection where+ def = Protection {+ _protectionHidden = Nothing+ , _protectionLocked = Nothing+ }++{-------------------------------------------------------------------------------+ Rendering record types++ NOTE: Excel is sensitive to the order of the child nodes, so we are careful+ to follow the XML schema here. We are also careful to follow the ordering+ for attributes, although this is actually pointless, as xml-conduit stores+ these as a Map, so we lose the ordering. But if we change representation,+ at least they are in the right order (hopefully) in the source code.+-------------------------------------------------------------------------------}++instance ToDocument StyleSheet where+ toDocument = documentFromElement "Stylesheet generated by xlsx"+ . toElement "styleSheet"++-- | See @CT_Stylesheet@, p. 4482+instance ToElement StyleSheet where+ toElement nm StyleSheet{..} = elementListSimple nm elements+ where+ countedElementList' nm' xs = maybeToList $ nonEmptyCountedElementList nm' xs+ elements = countedElementList' "numFmts" (map (toElement "numFmt") numFmts) +++ countedElementList' "fonts" (map (toElement "font") _styleSheetFonts) +++ countedElementList' "fills" (map (toElement "fill") _styleSheetFills) +++ countedElementList' "borders" (map (toElement "border") _styleSheetBorders) +++ -- TODO: cellStyleXfs+ countedElementList' "cellXfs" (map (toElement "xf") _styleSheetCellXfs) +++ -- TODO: cellStyles+ countedElementList' "dxfs" (map (toElement "dxf") _styleSheetDxfs)+ -- TODO: tableStyles+ -- TODO: colors+ -- TODO: extLst+ numFmts = map (uncurry NumFmt) $ M.toList _styleSheetNumFmts++-- | See @CT_Xf@, p. 4486+instance ToElement CellXf where+ toElement nm CellXf{..} = Element {+ elementName = nm+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "alignment" <$> _cellXfAlignment+ , toElement "protection" <$> _cellXfProtection+ -- TODO: extLst+ ]+ , elementAttributes = M.fromList . catMaybes $ [+ "numFmtId" .=? _cellXfNumFmtId+ , "fontId" .=? _cellXfFontId+ , "fillId" .=? _cellXfFillId+ , "borderId" .=? _cellXfBorderId+ , "xfId" .=? _cellXfId+ , "quotePrefix" .=? _cellXfQuotePrefix+ , "pivotButton" .=? _cellXfPivotButton+ , "applyNumberFormat" .=? _cellXfApplyNumberFormat+ , "applyFont" .=? _cellXfApplyFont+ , "applyFill" .=? _cellXfApplyFill+ , "applyBorder" .=? _cellXfApplyBorder+ , "applyAlignment" .=? _cellXfApplyAlignment+ , "applyProtection" .=? _cellXfApplyProtection+ ]+ }++-- | See @CT_Dxf@, p. 3937+instance ToElement Dxf where+ toElement nm Dxf{..} = Element+ { elementName = nm+ , elementNodes = map NodeElement $+ catMaybes [ toElement "font" <$> _dxfFont+ , toElement "numFmt" <$> _dxfNumFmt+ , toElement "fill" <$> _dxfFill+ , toElement "alignment" <$> _dxfAlignment+ , toElement "border" <$> _dxfBorder+ , toElement "protection" <$> _dxfProtection+ ]+ , elementAttributes = M.empty+ }++-- | See @CT_CellAlignment@, p. 4482+instance ToElement Alignment where+ toElement nm Alignment{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = M.fromList . catMaybes $ [+ "horizontal" .=? _alignmentHorizontal+ , "vertical" .=? _alignmentVertical+ , "textRotation" .=? _alignmentTextRotation+ , "wrapText" .=? _alignmentWrapText+ , "relativeIndent" .=? _alignmentRelativeIndent+ , "indent" .=? _alignmentIndent+ , "justifyLastLine" .=? _alignmentJustifyLastLine+ , "shrinkToFit" .=? _alignmentShrinkToFit+ , "readingOrder" .=? _alignmentReadingOrder+ ]+ }++-- | See @CT_Border@, p. 4483+instance ToElement Border where+ toElement nm Border{..} = Element {+ elementName = nm+ , elementAttributes = M.fromList . catMaybes $ [+ "diagonalUp" .=? _borderDiagonalUp+ , "diagonalDown" .=? _borderDiagonalDown+ , "outline" .=? _borderOutline+ ]+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "start" <$> _borderStart+ , toElement "end" <$> _borderEnd+ , toElement "left" <$> _borderLeft+ , toElement "right" <$> _borderRight+ , toElement "top" <$> _borderTop+ , toElement "bottom" <$> _borderBottom+ , toElement "diagonal" <$> _borderDiagonal+ , toElement "vertical" <$> _borderVertical+ , toElement "horizontal" <$> _borderHorizontal+ ]+ }++-- | See @CT_BorderPr@, p. 4483+instance ToElement BorderStyle where+ toElement nm BorderStyle{..} = Element {+ elementName = nm+ , elementAttributes = M.fromList . catMaybes $ [+ "style" .=? _borderStyleLine+ ]+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "color" <$> _borderStyleColor+ ]+ }++-- | See @CT_Color@, p. 4484+instance ToElement Color where+ toElement nm Color{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = M.fromList . catMaybes $ [+ "auto" .=? _colorAutomatic+ , "rgb" .=? _colorARGB+ , "theme" .=? _colorTheme+ , "tint" .=? _colorTint+ ]+ }++-- | See @CT_Fill@, p. 4484+instance ToElement Fill where+ toElement nm Fill{..} = Element {+ elementName = nm+ , elementAttributes = M.empty+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "patternFill" <$> _fillPattern+ ]+ }++-- | See @CT_PatternFill@, p. 4484+instance ToElement FillPattern where+ toElement nm FillPattern{..} = Element {+ elementName = nm+ , elementAttributes = M.fromList . catMaybes $ [+ "patternType" .=? _fillPatternType+ ]+ , elementNodes = map NodeElement . catMaybes $ [+ toElement "fgColor" <$> _fillPatternFgColor+ , toElement "bgColor" <$> _fillPatternBgColor+ ]+ }++-- | See @CT_Font@, p. 4489+instance ToElement Font where+ toElement nm Font{..} = Element {+ elementName = nm+ , elementAttributes = M.empty -- all properties specified as child nodes+ , elementNodes = map NodeElement . catMaybes $ [+ elementValue "name" <$> _fontName+ , elementValue "charset" <$> _fontCharset+ , elementValue "family" <$> _fontFamily+ , elementValue "b" <$> _fontBold+ , elementValue "i" <$> _fontItalic+ , elementValue "strike" <$> _fontStrikeThrough+ , elementValue "outline" <$> _fontOutline+ , elementValue "shadow" <$> _fontShadow+ , elementValue "condense" <$> _fontCondense+ , elementValue "extend" <$> _fontExtend+ , toElement "color" <$> _fontColor+ , elementValue "sz" <$> _fontSize+ , elementValue "u" <$> _fontUnderline+ , elementValue "vertAlign" <$> _fontVertAlign+ , elementValue "scheme" <$> _fontScheme+ ]+ }++-- | See @CT_NumFmt@, p. 3936+instance ToElement NumFmt where+ toElement nm (NumFmt {..}) =+ leafElement nm+ [ "numFmtId" .= toAttrVal _numFmtId+ , "formatCode" .= toAttrVal _numFmtCode+ ]++-- | See @CT_CellProtection@, p. 4484+instance ToElement Protection where+ toElement nm Protection{..} = Element {+ elementName = nm+ , elementNodes = []+ , elementAttributes = M.fromList . catMaybes $ [+ "locked" .=? _protectionLocked+ , "hidden" .=? _protectionHidden+ ]+ }++{-------------------------------------------------------------------------------+ Rendering attribute values+-------------------------------------------------------------------------------}++instance ToAttrVal CellHorizontalAlignment where+ toAttrVal CellHorizontalAlignmentCenter = "center"+ toAttrVal CellHorizontalAlignmentCenterContinuous = "centerContinuous"+ toAttrVal CellHorizontalAlignmentDistributed = "distributed"+ toAttrVal CellHorizontalAlignmentFill = "fill"+ toAttrVal CellHorizontalAlignmentGeneral = "general"+ toAttrVal CellHorizontalAlignmentJustify = "justify"+ toAttrVal CellHorizontalAlignmentLeft = "left"+ toAttrVal CellHorizontalAlignmentRight = "right"++instance ToAttrVal CellVerticalAlignment where+ toAttrVal CellVerticalAlignmentBottom = "bottom"+ toAttrVal CellVerticalAlignmentCenter = "center"+ toAttrVal CellVerticalAlignmentDistributed = "distributed"+ toAttrVal CellVerticalAlignmentJustify = "justify"+ toAttrVal CellVerticalAlignmentTop = "top"++instance ToAttrVal FontFamily where+ toAttrVal FontFamilyNotApplicable = "0"+ toAttrVal FontFamilyRoman = "1"+ toAttrVal FontFamilySwiss = "2"+ toAttrVal FontFamilyModern = "3"+ toAttrVal FontFamilyScript = "4"+ toAttrVal FontFamilyDecorative = "5"++instance ToAttrVal FontScheme where+ toAttrVal FontSchemeMajor = "major"+ toAttrVal FontSchemeMinor = "minor"+ toAttrVal FontSchemeNone = "none"++-- See @ST_UnderlineValues@, p. 3940+instance ToAttrVal FontUnderline where+ toAttrVal FontUnderlineSingle = "single"+ toAttrVal FontUnderlineDouble = "double"+ toAttrVal FontUnderlineSingleAccounting = "singleAccounting"+ toAttrVal FontUnderlineDoubleAccounting = "doubleAccounting"+ toAttrVal FontUnderlineNone = "none"++instance ToAttrVal FontVerticalAlignment where+ toAttrVal FontVerticalAlignmentBaseline = "baseline"+ toAttrVal FontVerticalAlignmentSubscript = "subscript"+ toAttrVal FontVerticalAlignmentSuperscript = "superscript"++instance ToAttrVal LineStyle where+ toAttrVal LineStyleDashDot = "dashDot"+ toAttrVal LineStyleDashDotDot = "dashDotDot"+ toAttrVal LineStyleDashed = "dashed"+ toAttrVal LineStyleDotted = "dotted"+ toAttrVal LineStyleDouble = "double"+ toAttrVal LineStyleHair = "hair"+ toAttrVal LineStyleMedium = "medium"+ toAttrVal LineStyleMediumDashDot = "mediumDashDot"+ toAttrVal LineStyleMediumDashDotDot = "mediumDashDotDot"+ toAttrVal LineStyleMediumDashed = "mediumDashed"+ toAttrVal LineStyleNone = "none"+ toAttrVal LineStyleSlantDashDot = "slantDashDot"+ toAttrVal LineStyleThick = "thick"+ toAttrVal LineStyleThin = "thin"++instance ToAttrVal PatternType where+ toAttrVal PatternTypeDarkDown = "darkDown"+ toAttrVal PatternTypeDarkGray = "darkGray"+ toAttrVal PatternTypeDarkGrid = "darkGrid"+ toAttrVal PatternTypeDarkHorizontal = "darkHorizontal"+ toAttrVal PatternTypeDarkTrellis = "darkTrellis"+ toAttrVal PatternTypeDarkUp = "darkUp"+ toAttrVal PatternTypeDarkVertical = "darkVertical"+ toAttrVal PatternTypeGray0625 = "gray0625"+ toAttrVal PatternTypeGray125 = "gray125"+ toAttrVal PatternTypeLightDown = "lightDown"+ toAttrVal PatternTypeLightGray = "lightGray"+ toAttrVal PatternTypeLightGrid = "lightGrid"+ toAttrVal PatternTypeLightHorizontal = "lightHorizontal"+ toAttrVal PatternTypeLightTrellis = "lightTrellis"+ toAttrVal PatternTypeLightUp = "lightUp"+ toAttrVal PatternTypeLightVertical = "lightVertical"+ toAttrVal PatternTypeMediumGray = "mediumGray"+ toAttrVal PatternTypeNone = "none"+ toAttrVal PatternTypeSolid = "solid"++instance ToAttrVal ReadingOrder where+ toAttrVal ReadingOrderContextDependent = "0"+ toAttrVal ReadingOrderLeftToRight = "1"+ toAttrVal ReadingOrderRightToLeft = "2"++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}+-- | See @CT_Stylesheet@, p. 4482+instance FromCursor StyleSheet where+ fromCursor cur = do+ let+ _styleSheetFonts = cur $/ element (addSmlNamespace "fonts") &/ element (addSmlNamespace "font") >=> fromCursor+ _styleSheetFills = cur $/ element (addSmlNamespace "fills") &/ element (addSmlNamespace "fill") >=> fromCursor+ _styleSheetBorders = cur $/ element (addSmlNamespace "borders") &/ element (addSmlNamespace "border") >=> fromCursor+ -- TODO: cellStyleXfs+ _styleSheetCellXfs = cur $/ element (addSmlNamespace "cellXfs") &/ element (addSmlNamespace "xf") >=> fromCursor+ -- TODO: cellStyles+ _styleSheetDxfs = cur $/ element (addSmlNamespace "dxfs") &/ element (addSmlNamespace "dxf") >=> fromCursor+ _styleSheetNumFmts = M.fromList . map mkNumFmtPair $+ cur $/ element (addSmlNamespace "numFmts")&/ element (addSmlNamespace "numFmt") >=> fromCursor+ -- TODO: tableStyles+ -- TODO: colors+ -- TODO: extLst+ return StyleSheet{..}++-- | See @CT_Font@, p. 4489+instance FromCursor Font where+ fromCursor cur = do+ _fontName <- maybeElementValue (addSmlNamespace "name") cur+ _fontCharset <- maybeElementValue (addSmlNamespace "charset") cur+ _fontFamily <- maybeElementValue (addSmlNamespace "family") cur+ _fontBold <- maybeBoolElementValue (addSmlNamespace "b") cur+ _fontItalic <- maybeBoolElementValue (addSmlNamespace "i") cur+ _fontStrikeThrough<- maybeBoolElementValue (addSmlNamespace "strike") cur+ _fontOutline <- maybeBoolElementValue (addSmlNamespace "outline") cur+ _fontShadow <- maybeBoolElementValue (addSmlNamespace "shadow") cur+ _fontCondense <- maybeBoolElementValue (addSmlNamespace "condense") cur+ _fontExtend <- maybeBoolElementValue (addSmlNamespace "extend") cur+ _fontColor <- maybeFromElement (addSmlNamespace "color") cur+ _fontSize <- maybeElementValue (addSmlNamespace "sz") cur+ _fontUnderline <- maybeElementValueDef (addSmlNamespace "u") FontUnderlineSingle cur+ _fontVertAlign <- maybeElementValue (addSmlNamespace "vertAlign") cur+ _fontScheme <- maybeElementValue (addSmlNamespace "scheme") cur+ return Font{..}++-- | See 18.18.94 "ST_FontFamily (Font Family)" (p. 2517)+instance FromAttrVal FontFamily where+ fromAttrVal "0" = readSuccess FontFamilyNotApplicable+ fromAttrVal "1" = readSuccess FontFamilyRoman+ fromAttrVal "2" = readSuccess FontFamilySwiss+ fromAttrVal "3" = readSuccess FontFamilyModern+ fromAttrVal "4" = readSuccess FontFamilyScript+ fromAttrVal "5" = readSuccess FontFamilyDecorative+ fromAttrVal t = invalidText "FontFamily" t++instance FromAttrBs FontFamily where+ fromAttrBs "0" = return FontFamilyNotApplicable+ fromAttrBs "1" = return FontFamilyRoman+ fromAttrBs "2" = return FontFamilySwiss+ fromAttrBs "3" = return FontFamilyModern+ fromAttrBs "4" = return FontFamilyScript+ fromAttrBs "5" = return FontFamilyDecorative+ fromAttrBs x = unexpectedAttrBs "FontFamily" x++-- | See @CT_Color@, p. 4484+instance FromCursor Color where+ fromCursor cur = do+ _colorAutomatic <- maybeAttribute "auto" cur+ _colorARGB <- maybeAttribute "rgb" cur+ _colorTheme <- maybeAttribute "theme" cur+ _colorTint <- maybeAttribute "tint" cur+ return Color{..}++instance FromXenoNode Color where+ fromXenoNode root =+ parseAttributes root $ do+ _colorAutomatic <- maybeAttr "auto"+ _colorARGB <- maybeAttr "rgb"+ _colorTheme <- maybeAttr "theme"+ _colorTint <- maybeAttr "tint"+ return Color {..}++-- See @ST_UnderlineValues@, p. 3940+instance FromAttrVal FontUnderline where+ fromAttrVal "single" = readSuccess FontUnderlineSingle+ fromAttrVal "double" = readSuccess FontUnderlineDouble+ fromAttrVal "singleAccounting" = readSuccess FontUnderlineSingleAccounting+ fromAttrVal "doubleAccounting" = readSuccess FontUnderlineDoubleAccounting+ fromAttrVal "none" = readSuccess FontUnderlineNone+ fromAttrVal t = invalidText "FontUnderline" t++instance FromAttrBs FontUnderline where+ fromAttrBs "single" = return FontUnderlineSingle+ fromAttrBs "double" = return FontUnderlineDouble+ fromAttrBs "singleAccounting" = return FontUnderlineSingleAccounting+ fromAttrBs "doubleAccounting" = return FontUnderlineDoubleAccounting+ fromAttrBs "none" = return FontUnderlineNone+ fromAttrBs x = unexpectedAttrBs "FontUnderline" x++instance FromAttrVal FontVerticalAlignment where+ fromAttrVal "baseline" = readSuccess FontVerticalAlignmentBaseline+ fromAttrVal "subscript" = readSuccess FontVerticalAlignmentSubscript+ fromAttrVal "superscript" = readSuccess FontVerticalAlignmentSuperscript+ fromAttrVal t = invalidText "FontVerticalAlignment" t++instance FromAttrBs FontVerticalAlignment where+ fromAttrBs "baseline" = return FontVerticalAlignmentBaseline+ fromAttrBs "subscript" = return FontVerticalAlignmentSubscript+ fromAttrBs "superscript" = return FontVerticalAlignmentSuperscript+ fromAttrBs x = unexpectedAttrBs "FontVerticalAlignment" x++instance FromAttrVal FontScheme where+ fromAttrVal "major" = readSuccess FontSchemeMajor+ fromAttrVal "minor" = readSuccess FontSchemeMinor+ fromAttrVal "none" = readSuccess FontSchemeNone+ fromAttrVal t = invalidText "FontScheme" t++instance FromAttrBs FontScheme where+ fromAttrBs "major" = return FontSchemeMajor+ fromAttrBs "minor" = return FontSchemeMinor+ fromAttrBs "none" = return FontSchemeNone+ fromAttrBs x = unexpectedAttrBs "FontScheme" x++-- | See @CT_Fill@, p. 4484+instance FromCursor Fill where+ fromCursor cur = do+ _fillPattern <- maybeFromElement (addSmlNamespace "patternFill") cur+ return Fill{..}++-- | See @CT_PatternFill@, p. 4484+instance FromCursor FillPattern where+ fromCursor cur = do+ _fillPatternType <- maybeAttribute "patternType" cur+ _fillPatternFgColor <- maybeFromElement (addSmlNamespace "fgColor") cur+ _fillPatternBgColor <- maybeFromElement (addSmlNamespace "bgColor") cur+ return FillPattern{..}++instance FromAttrVal PatternType where+ fromAttrVal "darkDown" = readSuccess PatternTypeDarkDown+ fromAttrVal "darkGray" = readSuccess PatternTypeDarkGray+ fromAttrVal "darkGrid" = readSuccess PatternTypeDarkGrid+ fromAttrVal "darkHorizontal" = readSuccess PatternTypeDarkHorizontal+ fromAttrVal "darkTrellis" = readSuccess PatternTypeDarkTrellis+ fromAttrVal "darkUp" = readSuccess PatternTypeDarkUp+ fromAttrVal "darkVertical" = readSuccess PatternTypeDarkVertical+ fromAttrVal "gray0625" = readSuccess PatternTypeGray0625+ fromAttrVal "gray125" = readSuccess PatternTypeGray125+ fromAttrVal "lightDown" = readSuccess PatternTypeLightDown+ fromAttrVal "lightGray" = readSuccess PatternTypeLightGray+ fromAttrVal "lightGrid" = readSuccess PatternTypeLightGrid+ fromAttrVal "lightHorizontal" = readSuccess PatternTypeLightHorizontal+ fromAttrVal "lightTrellis" = readSuccess PatternTypeLightTrellis+ fromAttrVal "lightUp" = readSuccess PatternTypeLightUp+ fromAttrVal "lightVertical" = readSuccess PatternTypeLightVertical+ fromAttrVal "mediumGray" = readSuccess PatternTypeMediumGray+ fromAttrVal "none" = readSuccess PatternTypeNone+ fromAttrVal "solid" = readSuccess PatternTypeSolid+ fromAttrVal t = invalidText "PatternType" t++-- | See @CT_Border@, p. 4483+instance FromCursor Border where+ fromCursor cur = do+ _borderDiagonalUp <- maybeAttribute "diagonalUp" cur+ _borderDiagonalDown <- maybeAttribute "diagonalDown" cur+ _borderOutline <- maybeAttribute "outline" cur+ _borderStart <- maybeFromElement (addSmlNamespace "start") cur+ _borderEnd <- maybeFromElement (addSmlNamespace "end") cur+ _borderLeft <- maybeFromElement (addSmlNamespace "left") cur+ _borderRight <- maybeFromElement (addSmlNamespace "right") cur+ _borderTop <- maybeFromElement (addSmlNamespace "top") cur+ _borderBottom <- maybeFromElement (addSmlNamespace "bottom") cur+ _borderDiagonal <- maybeFromElement (addSmlNamespace "diagonal") cur+ _borderVertical <- maybeFromElement (addSmlNamespace "vertical") cur+ _borderHorizontal <- maybeFromElement (addSmlNamespace "horizontal") cur+ return Border{..}++instance FromCursor BorderStyle where+ fromCursor cur = do+ _borderStyleLine <- maybeAttribute "style" cur+ _borderStyleColor <- maybeFromElement (addSmlNamespace "color") cur+ return BorderStyle{..}++instance FromAttrVal LineStyle where+ fromAttrVal "dashDot" = readSuccess LineStyleDashDot+ fromAttrVal "dashDotDot" = readSuccess LineStyleDashDotDot+ fromAttrVal "dashed" = readSuccess LineStyleDashed+ fromAttrVal "dotted" = readSuccess LineStyleDotted+ fromAttrVal "double" = readSuccess LineStyleDouble+ fromAttrVal "hair" = readSuccess LineStyleHair+ fromAttrVal "medium" = readSuccess LineStyleMedium+ fromAttrVal "mediumDashDot" = readSuccess LineStyleMediumDashDot+ fromAttrVal "mediumDashDotDot" = readSuccess LineStyleMediumDashDotDot+ fromAttrVal "mediumDashed" = readSuccess LineStyleMediumDashed+ fromAttrVal "none" = readSuccess LineStyleNone+ fromAttrVal "slantDashDot" = readSuccess LineStyleSlantDashDot+ fromAttrVal "thick" = readSuccess LineStyleThick+ fromAttrVal "thin" = readSuccess LineStyleThin+ fromAttrVal t = invalidText "LineStyle" t++-- | See @CT_Xf@, p. 4486+instance FromCursor CellXf where+ fromCursor cur = do+ _cellXfAlignment <- maybeFromElement (addSmlNamespace "alignment") cur+ _cellXfProtection <- maybeFromElement (addSmlNamespace "protection") cur+ _cellXfNumFmtId <- maybeAttribute "numFmtId" cur+ _cellXfFontId <- maybeAttribute "fontId" cur+ _cellXfFillId <- maybeAttribute "fillId" cur+ _cellXfBorderId <- maybeAttribute "borderId" cur+ _cellXfId <- maybeAttribute "xfId" cur+ _cellXfQuotePrefix <- maybeAttribute "quotePrefix" cur+ _cellXfPivotButton <- maybeAttribute "pivotButton" cur+ _cellXfApplyNumberFormat <- maybeAttribute "applyNumberFormat" cur+ _cellXfApplyFont <- maybeAttribute "applyFont" cur+ _cellXfApplyFill <- maybeAttribute "applyFill" cur+ _cellXfApplyBorder <- maybeAttribute "applyBorder" cur+ _cellXfApplyAlignment <- maybeAttribute "applyAlignment" cur+ _cellXfApplyProtection <- maybeAttribute "applyProtection" cur+ return CellXf{..}++-- | See @CT_Dxf@, p. 3937+instance FromCursor Dxf where+ fromCursor cur = do+ _dxfFont <- maybeFromElement (addSmlNamespace "font") cur+ _dxfNumFmt <- maybeFromElement (addSmlNamespace "numFmt") cur+ _dxfFill <- maybeFromElement (addSmlNamespace "fill") cur+ _dxfAlignment <- maybeFromElement (addSmlNamespace "alignment") cur+ _dxfBorder <- maybeFromElement (addSmlNamespace "border") cur+ _dxfProtection <- maybeFromElement (addSmlNamespace "protection") cur+ return Dxf{..}++-- | See @CT_NumFmt@, p. 3936+instance FromCursor NumFmt where+ fromCursor cur = do+ _numFmtCode <- fromAttribute "formatCode" cur+ _numFmtId <- fromAttribute "numFmtId" cur+ return NumFmt{..}++-- | See @CT_CellAlignment@, p. 4482+instance FromCursor Alignment where+ fromCursor cur = do+ _alignmentHorizontal <- maybeAttribute "horizontal" cur+ _alignmentVertical <- maybeAttribute "vertical" cur+ _alignmentTextRotation <- maybeAttribute "textRotation" cur+ _alignmentWrapText <- maybeAttribute "wrapText" cur+ _alignmentRelativeIndent <- maybeAttribute "relativeIndent" cur+ _alignmentIndent <- maybeAttribute "indent" cur+ _alignmentJustifyLastLine <- maybeAttribute "justifyLastLine" cur+ _alignmentShrinkToFit <- maybeAttribute "shrinkToFit" cur+ _alignmentReadingOrder <- maybeAttribute "readingOrder" cur+ return Alignment{..}++instance FromAttrVal CellHorizontalAlignment where+ fromAttrVal "center" = readSuccess CellHorizontalAlignmentCenter+ fromAttrVal "centerContinuous" = readSuccess CellHorizontalAlignmentCenterContinuous+ fromAttrVal "distributed" = readSuccess CellHorizontalAlignmentDistributed+ fromAttrVal "fill" = readSuccess CellHorizontalAlignmentFill+ fromAttrVal "general" = readSuccess CellHorizontalAlignmentGeneral+ fromAttrVal "justify" = readSuccess CellHorizontalAlignmentJustify+ fromAttrVal "left" = readSuccess CellHorizontalAlignmentLeft+ fromAttrVal "right" = readSuccess CellHorizontalAlignmentRight+ fromAttrVal t = invalidText "CellHorizontalAlignment" t++instance FromAttrVal CellVerticalAlignment where+ fromAttrVal "bottom" = readSuccess CellVerticalAlignmentBottom+ fromAttrVal "center" = readSuccess CellVerticalAlignmentCenter+ fromAttrVal "distributed" = readSuccess CellVerticalAlignmentDistributed+ fromAttrVal "justify" = readSuccess CellVerticalAlignmentJustify+ fromAttrVal "top" = readSuccess CellVerticalAlignmentTop+ fromAttrVal t = invalidText "CellVerticalAlignment" t++instance FromAttrVal ReadingOrder where+ fromAttrVal "0" = readSuccess ReadingOrderContextDependent+ fromAttrVal "1" = readSuccess ReadingOrderLeftToRight+ fromAttrVal "2" = readSuccess ReadingOrderRightToLeft+ fromAttrVal t = invalidText "ReadingOrder" t++-- | See @CT_CellProtection@, p. 4484+instance FromCursor Protection where+ fromCursor cur = do+ _protectionLocked <- maybeAttribute "locked" cur+ _protectionHidden <- maybeAttribute "hidden" cur+ return Protection{..}
+ src/Codec/Xlsx/Types/Table.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Table where++import Codec.Xlsx.LensCompat (makeLenses)+import Control.DeepSeq (NFData)+import Data.Maybe (catMaybes, maybeToList)+import Data.Text (Text)+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types.AutoFilter+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Writer.Internal++-- | Tables are ranges of data in the worksheet that have special+-- behavior applied which allow users to better sort, analyze, format,+-- manage, add, and delete data. Tables and table columns can also be+-- referenced through formulas by the spreadsheet application using+-- friendly names, making formula calculations that use tables much+-- easier to understand and maintain. Tables provide a natural way for+-- working with large sets of tabular data.+--+-- NOTE: as @headerRowCount@ property isn't yet supported it's+-- supposed that it's library user liability to guarantee that the 1st+-- row of 'tblRef' range contains cells with names specified in+-- `tblColumns`+--+-- Section 18.5 \"Tables\" (p. 1728)+-- Section 18.5.1 \"Tables\" (p. 1729)+-- Section 18.5.1.2 "table (Table)" (p. 1730)+data Table = Table+ { tblDisplayName :: Text+ -- ^ A string representing the name of the table. This is the name+ -- that shall be used in formula references, and displayed in the UI+ -- to the spreadsheet user. This name shall not have any spaces in+ -- it, and it shall be unique amongst all other displayNames and+ -- definedNames in the workbook. The character lengths and+ -- restrictions are the same as for definedNames .+ , tblName :: Maybe Text+ -- ^ A string representing the name of the table that is used to+ -- reference the table programmatically through the spreadsheet+ -- applications object model. This string shall be unique per table+ -- per sheet. It has the same length and character restrictions as+ -- for displayName. By default this should be the same as the+ -- table's 'tblDisplayName' . This name should also be kept in synch with+ -- the displayName when the displayName is updated in the UI by the+ -- spreadsheet user.+ , tblRef :: CellRef+ -- ^ The range on the relevant sheet that the table occupies+ -- expressed using A1 style referencing.+ , tblColumns :: [TableColumn]+ -- ^ columns of this table, specification requires any table to+ -- include at least 1 column+ , tblAutoFilter :: Maybe AutoFilter+ } deriving (Eq, Show, Generic)+instance NFData Table++-- | Single table column+--+-- TODO: styling information+--+-- Section 18.5.1.3 "tableColumn (Table Column)" (p. 1735)+data TableColumn = TableColumn+ { tblcName :: Text+ -- ^ A string representing the unique caption of the table+ -- column. This is what shall be displayed in the header row in the+ -- UI, and is referenced through functions. This name shall be+ -- unique per table.+ } deriving (Eq, Show, Generic)+instance NFData TableColumn++makeLenses ''Table++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor Table where+ fromCursor c = do+ tblDisplayName <- fromAttribute "displayName" c+ tblName <- maybeAttribute "name" c+ tblRef <- fromAttribute "ref" c+ tblAutoFilter <- maybeFromElement (addSmlNamespace "autoFilter") c+ let tblColumns =+ c $/ element (addSmlNamespace "tableColumns") &/ element (addSmlNamespace "tableColumn") >=>+ fmap TableColumn . fromAttribute "name"+ return Table {..}++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++tableToDocument :: Table -> Int -> Document+tableToDocument tbl i =+ documentFromElement "Table generated by xlsx" $+ tableToElement "table" tbl i++tableToElement :: Name -> Table -> Int -> Element+tableToElement nm Table {..} i = elementList nm attrs subElements+ where+ attrs =+ [ "id" .= i+ , "displayName" .= tblDisplayName+ , "ref" .= tblRef+ ] +++ catMaybes+ [ "name" .=? tblName+ ]+ subElements =+ maybeToList (toElement "autoFilter" <$> tblAutoFilter) +++ maybeToList (nonEmptyCountedElementList+ "tableColumns"+ [ leafElement "tableColumn" ["id" .= i', "name" .= tblcName c]+ | (i', c) <- zip [(1 :: Int) ..] tblColumns+ ]+ )
+ src/Codec/Xlsx/Types/Variant.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Types.Variant where++import Control.DeepSeq (NFData)+import Control.Monad.Fail (MonadFail)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 as B64+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import GHC.Generics (Generic)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++data Variant+ = VtBlob ByteString+ | VtBool Bool+ | VtDecimal Double+ | VtLpwstr Text+ | VtInt Int+ -- TODO: vt_vector, vt_array, vt_oblob, vt_empty, vt_null, vt_i1, vt_i2,+ -- vt_i4, vt_i8, vt_ui1, vt_ui2, vt_ui4, vt_ui8, vt_uint, vt_r4, vt_r8,+ -- vt_lpstr, vt_bstr, vt_date, vt_filetime, vt_cy, vt_error, vt_stream,+ -- vt_ostream, vt_storage, vt_ostorage, vt_vstream, vt_clsid+ deriving (Eq, Show, Generic)++instance NFData Variant++{-------------------------------------------------------------------------------+ Parsing+-------------------------------------------------------------------------------}++instance FromCursor Variant where+ fromCursor = variantFromNode . node++variantFromNode :: Node -> [Variant]+variantFromNode n@(NodeElement el) | elementName el == vt "lpwstr" =+ fromNode n $/ content &| VtLpwstr+ | elementName el == vt "bool" =+ fromNode n $/ content >=> fmap VtBool . boolean+ | elementName el == vt "int" =+ fromNode n $/ content >=> fmap VtInt . decimal+ | elementName el == vt "decimal" =+ fromNode n $/ content >=> fmap VtDecimal . rational+ | elementName el == vt "blob" =+ fromNode n $/ content >=> fmap VtBlob . decodeBase64 . killWhitespace+variantFromNode _ = fail "no matching nodes"++killWhitespace :: Text -> Text+killWhitespace = T.filter (/=' ')++decodeBase64 :: MonadFail m => Text -> m ByteString+decodeBase64 t = case B64.decode (T.encodeUtf8 t) of+ Right bs -> return bs+ Left err -> fail $ "invalid base64 value: " ++ err++-- | Add doc props variant types namespace to name+vt :: Text -> Name+vt x = Name+ { nameLocalName = x+ , nameNamespace = Just docPropsVtNs+ , namePrefix = Nothing+ }++docPropsVtNs :: Text+docPropsVtNs = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"++{-------------------------------------------------------------------------------+ Rendering+-------------------------------------------------------------------------------}++variantToElement :: Variant -> Element+variantToElement (VtLpwstr t) = elementContent (vt"lpwstr") t+variantToElement (VtBlob bs) = elementContent (vt"blob") (T.decodeLatin1 $ B64.encode bs)+variantToElement (VtBool b) = elementContent (vt"bool") (txtb b)+variantToElement (VtDecimal d) = elementContent (vt"decimal") (txtd d)+variantToElement (VtInt i) = elementContent (vt"int") (txti i)
src/Codec/Xlsx/Writer.hs view
@@ -1,76 +1,401 @@+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE DeriveGeneric #-} -- | This module provides a function for serializing structured `Xlsx` into lazy bytestring module Codec.Xlsx.Writer- ( fromXlsx- ) where+ ( fromXlsx+ ) where -import qualified Codec.Archive.Zip as Zip-import Control.Arrow (second)-import Control.Lens hiding (transform)+import qualified "zip-archive" Codec.Archive.Zip as Zip+import Control.Arrow (second)+import Codec.Xlsx.LensCompat ((^.), mapped, (%~), (&), (?~))+import Control.Monad (forM)+import Control.Monad.ST+import Control.Monad.State (evalState, get, put) import qualified Data.ByteString.Lazy as L-import Data.ByteString.Lazy.Char8()-import Data.Map (Map)+import Data.ByteString.Lazy.Char8 ()+import Data.List (foldl', mapAccumL)+import Data.Map (Map) import qualified Data.Map as M-import Data.Maybe-import Data.Monoid ((<>))-import Data.Text (Text, )+import Data.Maybe+import Data.Monoid ((<>))+import Data.STRef+import Data.Text (Text) import qualified Data.Text as T-import Data.Text.Lazy (toStrict)-import Data.Text.Lazy.Builder (toLazyText)-import Data.Text.Lazy.Builder.Int-import Data.Text.Lazy.Builder.RealFloat-import qualified Data.Set as S-import Data.Vector (Vector)-import qualified Data.Vector as V-import Numeric.Search.Range (searchFromTo)-import System.Locale-import System.Time-import Text.XML--import Codec.Xlsx.Types+import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime)+import Data.Time.Format (formatTime)+import Data.Time.Format (defaultTimeLocale)+import Data.Tuple.Extra (fst3, snd3, thd3)+import GHC.Generics (Generic)+import Safe+import Text.XML +import Codec.Xlsx.Types+import Codec.Xlsx.Types.Cell (applySharedFormulaOpts)+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.CfPair+import qualified Codec.Xlsx.Types.Internal.CommentTable+ as CommentTable+import Codec.Xlsx.Types.Internal.CustomProperties+import Codec.Xlsx.Types.Internal.DvPair+import Codec.Xlsx.Types.Internal.Relationships as Relationships+ hiding (lookup)+import Codec.Xlsx.Types.Internal.SharedStringTable+import Codec.Xlsx.Types.PivotTable.Internal+import Codec.Xlsx.Writer.Internal+import Codec.Xlsx.Writer.Internal.PivotTable -- | Writes `Xlsx' to raw data (lazy bytestring)-fromXlsx :: ClockTime -> Xlsx -> L.ByteString-fromXlsx ct xlsx =+fromXlsx :: POSIXTime -> Xlsx -> L.ByteString+fromXlsx pt xlsx = Zip.fromArchive $ foldr Zip.addEntryToArchive Zip.emptyArchive entries where- TOD t _ = ct+ t = round pt+ utcTime = posixSecondsToUTCTime pt entries = Zip.toEntry "[Content_Types].xml" t (contentTypesXml files) :- map (\fd -> Zip.toEntry (T.unpack $ fdName fd) t (fdContents fd)) files- files = sheetFiles +++ map (\fd -> Zip.toEntry (fdPath fd) t (fdContents fd)) files+ files = workbookFiles ++ customPropFiles ++ [ FileData "docProps/core.xml"- "application/vnd.openxmlformats-package.core-properties+xml" $ coreXml (toUTCTime ct) "xlsxwriter"+ "application/vnd.openxmlformats-package.core-properties+xml"+ "metadata/core-properties" $ coreXml utcTime "xlsxwriter" , FileData "docProps/app.xml"- "application/vnd.openxmlformats-officedocument.extended-properties+xml" $ appXml (xlsx ^. xlSheets)- , FileData "xl/workbook.xml"- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" $ bookXml (xlsx ^. xlSheets)- , FileData "xl/styles.xml"- "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" $ unStyles (xlsx ^. xlStyles)- , FileData "xl/sharedStrings.xml"- "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" $ ssXml $ V.toList shared- , FileData "xl/_rels/workbook.xml.rels"- "application/vnd.openxmlformats-package.relationships+xml" $ bookRelXml sheetCount- , FileData "_rels/.rels" "application/vnd.openxmlformats-package.relationships+xml" rootRelXml+ "application/vnd.openxmlformats-officedocument.extended-properties+xml"+ "xtended-properties" $ appXml sheetNames+ , FileData "_rels/.rels" "application/vnd.openxmlformats-package.relationships+xml"+ "relationships" rootRelXml ]- sheetFiles =- [ FileData ("xl/worksheets/sheet" <> txti n <> ".xml")- "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" $- sheetXml (w ^. wsColumns) (w ^. wsRowPropertiesMap) cells (w ^. wsMerges) |- (n, cells, w) <- zip3 [1..] sheetCells sheets]- sheets = xlsx ^. xlSheets . to M.elems- sheetCount = length sheets- shared = V.fromList $ S.elems $ S.fromList $ concatMap (concatMap celltext . M.elems . _wsCells) sheets- sheetCells = map (transformSheetData shared) sheets- celltext (Cell{_cellValue=v}) = case v of- Just(CellText a) -> [a]- _ -> []+ rootRelXml = renderLBS def . toDocument $ Relationships.fromList rootRels+ rootFiles = customPropFileRels +++ [ ("officeDocument", "xl/workbook.xml")+ , ("metadata/core-properties", "docProps/core.xml")+ , ("extended-properties", "docProps/app.xml") ]+ rootRels = [ relEntry (unsafeRefId i) typ trg+ | (i, (typ, trg)) <- zip [1..] rootFiles ]+ customProps = xlsx ^. xlCustomProperties+ (customPropFiles, customPropFileRels) = case M.null customProps of+ True -> ([], [])+ False -> ([ FileData "docProps/custom.xml"+ "application/vnd.openxmlformats-officedocument.custom-properties+xml"+ "custom-properties"+ (customPropsXml (CustomProperties customProps)) ],+ [ ("custom-properties", "docProps/custom.xml") ])+ workbookFiles = bookFiles xlsx+ sheetNames = xlsx ^. xlSheets & mapped %~ fst -data FileData = FileData { fdName :: Text+singleSheetFiles :: Int+ -> Cells+ -> [FileData]+ -> Worksheet+ -> STRef s Int+ -> ST s (FileData, [FileData])+singleSheetFiles n cells pivFileDatas ws tblIdRef = do+ ref <- newSTRef 1+ mCmntData <- genComments n cells ref+ mDrawingData <- maybe (return Nothing) (fmap Just . genDrawing n ref) (ws ^. wsDrawing)+ pivRefs <- forM pivFileDatas $ \fd -> do+ refId <- nextRefId ref+ return (refId, fd)+ refTables <- forM (_wsTables ws) $ \tbl -> do+ refId <- nextRefId ref+ tblId <- readSTRef tblIdRef+ modifySTRef' tblIdRef (+1)+ return (refId, genTable tbl tblId)+ let sheetFilePath = "xl/worksheets/sheet" <> show n <> ".xml"+ sheetFile = FileData sheetFilePath+ "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"+ "worksheet" $+ sheetXml+ nss = [ ("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships") ]+ sheetXml= renderLBS def{rsNamespaces=nss} $ Document (Prologue [] Nothing []) root []+ root = addNS "http://schemas.openxmlformats.org/spreadsheetml/2006/main" Nothing $+ elementListSimple "worksheet" rootEls+ rootEls = catMaybes $+ [ elementListSimple "sheetViews" . map (toElement "sheetView") <$> ws ^. wsSheetViews+ , nonEmptyElListSimple "cols" . map (toElement "col") $ ws ^. wsColumnsProperties+ , Just . elementListSimple "sheetData" $+ sheetDataXml cells (ws ^. wsRowPropertiesMap) (ws ^. wsSharedFormulas)+ , toElement "sheetProtection" <$> (ws ^. wsProtection)+ , toElement "autoFilter" <$> (ws ^. wsAutoFilter)+ , nonEmptyElListSimple "mergeCells" . map mergeE1 $ ws ^. wsMerges+ ] ++ map (Just . toElement "conditionalFormatting") cfPairs +++ [ nonEmptyElListSimple "dataValidations" $ map (toElement "dataValidation") dvPairs+ , toElement "pageSetup" <$> ws ^. wsPageSetup+ , fst3 <$> mDrawingData+ , fst <$> mCmntData+ , nonEmptyElListSimple "tableParts"+ [leafElement "tablePart" [odr "id" .= rId] | (rId, _) <- refTables]+ ]+ cfPairs = map CfPair . M.toList $ ws ^. wsConditionalFormattings+ dvPairs = map DvPair . M.toList $ ws ^. wsDataValidations+ mergeE1 r = leafElement "mergeCell" [("ref" .= r)]++ sheetRels = if null referencedFiles+ then []+ else [ FileData ("xl/worksheets/_rels/sheet" <> show n <> ".xml.rels")+ "application/vnd.openxmlformats-package.relationships+xml"+ "relationships" sheetRelsXml ]+ sheetRelsXml = renderLBS def . toDocument . Relationships.fromList $+ [ relEntry i fdRelType (fdPath `relFrom` sheetFilePath)+ | (i, FileData{..}) <- referenced ]+ referenced = fromMaybe [] (snd <$> mCmntData) +++ catMaybes [ snd3 <$> mDrawingData ] +++ pivRefs +++ refTables+ referencedFiles = map snd referenced+ extraFiles = maybe [] thd3 mDrawingData+ otherFiles = sheetRels ++ referencedFiles ++ extraFiles++ return (sheetFile, otherFiles)++nextRefId :: STRef s Int -> ST s RefId+nextRefId r = do+ num <- readSTRef r+ modifySTRef' r (+1)+ return (unsafeRefId num)++sheetDataXml ::+ Cells+ -> Map RowIndex RowProperties+ -> Map SharedFormulaIndex SharedFormulaOptions+ -> [Element]+sheetDataXml rows rh sharedFormulas =+ evalState (mapM rowEl rows) sharedFormulas+ where+ rowEl (r, cells) = do+ let mProps = M.lookup r rh+ hasHeight = case rowHeight =<< mProps of+ Just CustomHeight{} -> True+ _ -> False+ ht = do Just height <- [rowHeight =<< mProps]+ let h = case height of CustomHeight x -> x+ AutomaticHeight x -> x+ return ("ht", txtd h)+ s = do Just st <- [rowStyle =<< mProps]+ return ("s", txti st)+ hidden = fromMaybe False $ rowHidden <$> mProps+ attrs = ht +++ s +++ [ ("r", txti r)+ , ("hidden", txtb hidden)+ , ("outlineLevel", "0")+ , ("collapsed", "false")+ , ("customFormat", "true")+ , ("customHeight", txtb hasHeight)+ ]+ cellEls <- mapM (cellEl r) cells+ return $ elementList "row" attrs cellEls+ cellEl r (icol, cell) = do+ let cellAttrs ref c =+ cellStyleAttr c ++ [("r" .= ref), ("t" .= xlsxCellType c)]+ cellStyleAttr XlsxCell{xlsxCellStyle=Nothing} = []+ cellStyleAttr XlsxCell{xlsxCellStyle=Just s} = [("s", txti s)]+ formula = xlsxCellFormula cell+ fEl0 = toElement "f" <$> formula+ fEl <- case formula of+ Just CellFormula{_cellfExpression=SharedFormula si} -> do+ shared <- get+ case M.lookup si shared of+ Just fOpts -> do+ put $ M.delete si shared+ return $ applySharedFormulaOpts fOpts <$> fEl0+ Nothing ->+ return fEl0+ _ ->+ return fEl0+ return $ elementList "c" (cellAttrs (singleCellRef (r, icol)) cell) $+ catMaybes [fEl, elementContent "v" . value <$> xlsxCellValue cell]++genComments :: Int -> Cells -> STRef s Int -> ST s (Maybe (Element, [ReferencedFileData]))+genComments n cells ref =+ if null comments+ then do+ return Nothing+ else do+ rId1 <- nextRefId ref+ rId2 <- nextRefId ref+ let el = refElement "legacyDrawing" rId2+ return $ Just (el, [(rId1, commentsFile), (rId2, vmlDrawingFile)])+ where+ comments = concatMap (\(row, rowCells) -> mapMaybe (maybeCellComment row) rowCells) cells+ maybeCellComment row (col, cell) = do+ comment <- xlsxComment cell+ return (singleCellRef (row, col), comment)+ commentTable = CommentTable.fromList comments+ commentsFile = FileData commentsPath+ "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"+ "comments"+ commentsBS+ commentsPath = "xl/comments" <> show n <> ".xml"+ commentsBS = renderLBS def $ toDocument commentTable+ vmlDrawingFile = FileData vmlPath+ "application/vnd.openxmlformats-officedocument.vmlDrawing"+ "vmlDrawing"+ vmlDrawingBS+ vmlPath = "xl/drawings/vmlDrawing" <> show n <> ".vml"+ vmlDrawingBS = CommentTable.renderShapes commentTable++genDrawing :: Int -> STRef s Int -> Drawing -> ST s (Element, ReferencedFileData, [FileData])+genDrawing n ref dr = do+ rId <- nextRefId ref+ let el = refElement "drawing" rId+ return (el, (rId, drawingFile), referenced)+ where+ drawingFilePath = "xl/drawings/drawing" <> show n <> ".xml"+ drawingCT = "application/vnd.openxmlformats-officedocument.drawing+xml"+ drawingFile = FileData drawingFilePath drawingCT "drawing" drawingXml+ drawingXml = renderLBS def{rsNamespaces=nss} $ toDocument dr'+ nss = [ ("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing")+ , ("a", "http://schemas.openxmlformats.org/drawingml/2006/main")+ , ("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships") ]+ dr' = Drawing{ _xdrAnchors = reverse anchors' }+ (anchors', images, charts, _) = foldl' collectFile ([], [], [], 1) (dr ^. xdrAnchors)+ collectFile :: ([Anchor RefId RefId], [Maybe (Int, FileInfo)], [(Int, ChartSpace)], Int)+ -> Anchor FileInfo ChartSpace+ -> ([Anchor RefId RefId], [Maybe (Int, FileInfo)], [(Int, ChartSpace)], Int)+ collectFile (as, fis, chs, i) anch0 =+ case anch0 ^. anchObject of+ Picture {..} ->+ let fi = (i,) <$> _picBlipFill ^. bfpImageInfo+ pic' =+ Picture+ { _picMacro = _picMacro+ , _picPublished = _picPublished+ , _picNonVisual = _picNonVisual+ , _picBlipFill =+ (_picBlipFill & bfpImageInfo ?~ RefId ("rId" <> txti i))+ , _picShapeProperties = _picShapeProperties+ }+ anch = anch0 {_anchObject = pic'}+ in (anch : as, fi : fis, chs, i + 1)+ Graphic nv ch tr ->+ let gr' = Graphic nv (RefId ("rId" <> txti i)) tr+ anch = anch0 {_anchObject = gr'}+ in (anch : as, fis, (i, ch) : chs, i + 1)+ imageFiles =+ [ ( unsafeRefId i+ , FileData ("xl/media/" <> _fiFilename) _fiContentType "image" _fiContents)+ | (i, FileInfo {..}) <- reverse (catMaybes images)+ ]++ chartFiles =+ [ (unsafeRefId i, genChart n k chart)+ | (k, (i, chart)) <- zip [1 ..] (reverse charts)+ ]++ innerFiles = imageFiles ++ chartFiles++ drawingRels =+ FileData+ ("xl/drawings/_rels/drawing" <> show n <> ".xml.rels")+ relsCT+ "relationships"+ drawingRelsXml++ drawingRelsXml =+ renderLBS def . toDocument . Relationships.fromList $+ map (refFileDataToRel drawingFilePath) innerFiles++ referenced =+ case innerFiles of+ [] -> []+ _ -> drawingRels : (map snd innerFiles)++genChart :: Int -> Int -> ChartSpace -> FileData+genChart n i ch = FileData path contentType relType contents+ where+ path = "xl/charts/chart" <> show n <> "_" <> show i <> ".xml"+ contentType =+ "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"+ relType = "chart"+ contents = renderLBS def {rsNamespaces = nss} $ toDocument ch+ nss =+ [ ("c", "http://schemas.openxmlformats.org/drawingml/2006/chart")+ , ("a", "http://schemas.openxmlformats.org/drawingml/2006/main")+ ]++data PvGenerated = PvGenerated+ { pvgCacheFiles :: [(CacheId, FileData)]+ , pvgSheetTableFiles :: [[FileData]]+ , pvgOthers :: [FileData]+ }++generatePivotFiles :: [(CellMap, [PivotTable])] -> PvGenerated+generatePivotFiles cmTables = PvGenerated cacheFiles shTableFiles others+ where+ cacheFiles = [cacheFile | (cacheFile, _, _) <- flatRendered]+ shTableFiles = map (map (\(_, tableFile, _) -> tableFile)) rendered+ others = concat [other | (_, _, other) <- flatRendered]+ firstCacheId = 1+ flatRendered = concat rendered+ (_, rendered) =+ mapAccumL+ (\c (cm, ts) -> mapAccumL (\c' t -> (c' + 1, render cm c' t)) c ts)+ firstCacheId+ cmTables+ render cm cacheIdRaw tbl =+ let PivotTableFiles {..} = renderPivotTableFiles cm cacheIdRaw tbl+ cacheId = CacheId cacheIdRaw+ cacheIdStr = show cacheIdRaw+ cachePath =+ "xl/pivotCache/pivotCacheDefinition" <> cacheIdStr <> ".xml"+ cacheFile =+ FileData+ cachePath+ (smlCT "pivotCacheDefinition")+ "pivotCacheDefinition"+ pvtfCacheDefinition+ recordsPath =+ "xl/pivotCache/pivotCacheRecords" <> cacheIdStr <> ".xml"+ recordsFile =+ FileData+ recordsPath+ (smlCT "pivotCacheRecords")+ "pivotCacheRecords"+ pvtfCacheRecords+ cacheRelsFile =+ FileData+ ("xl/pivotCache/_rels/pivotCacheDefinition" <> cacheIdStr <> ".xml.rels")+ relsCT+ "relationships" $+ renderRels [refFileDataToRel cachePath (unsafeRefId 1, recordsFile)]+ renderRels = renderLBS def . toDocument . Relationships.fromList+ tablePath = "xl/pivotTables/pivotTable" <> cacheIdStr <> ".xml"+ tableFile =+ FileData tablePath (smlCT "pivotTable") "pivotTable" pvtfTable+ tableRels =+ FileData+ ("xl/pivotTables/_rels/pivotTable" <> cacheIdStr <> ".xml.rels")+ relsCT+ "relationships" $+ renderRels [refFileDataToRel tablePath (unsafeRefId 1, cacheFile)]+ in ((cacheId, cacheFile), tableFile, [tableRels, cacheRelsFile, recordsFile])++genTable :: Table -> Int -> FileData+genTable tbl tblId = FileData{..}+ where+ fdPath = "xl/tables/table" <> show tblId <> ".xml"+ fdContentType = smlCT "table"+ fdRelType = "table"+ fdContents = renderLBS def $ tableToDocument tbl tblId++data FileData = FileData { fdPath :: FilePath , fdContentType :: Text- , fdContents :: L.ByteString}+ , fdRelType :: Text+ , fdContents :: L.ByteString } -coreXml :: CalendarTime -> Text -> L.ByteString+type ReferencedFileData = (RefId, FileData)++refFileDataToRel :: FilePath -> ReferencedFileData -> (RefId, Relationship)+refFileDataToRel basePath (i, FileData {..}) =+ relEntry i fdRelType (fdPath `relFrom` basePath)++type Cells = [(RowIndex, [(ColumnIndex, XlsxCell)])]++coreXml :: UTCTime -> Text -> L.ByteString coreXml created creator = renderLBS def{rsNamespaces=nss} $ Document (Prologue [] Nothing []) root [] where@@ -80,7 +405,7 @@ , ("xsi","http://www.w3.org/2001/XMLSchema-instance") ] namespaced = nsName nss- date = T.pack $ formatCalendarTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" created+ date = T.pack $ formatTime defaultTimeLocale "%FT%T%QZ" created root = Element (namespaced "cp" "coreProperties") M.empty [ nEl (namespaced "dcterms" "created") (M.fromList [(namespaced "xsi" "type", "dcterms:W3CDTF")]) [NodeContent date]@@ -88,134 +413,180 @@ , nEl (namespaced "cp" "lastModifiedBy") M.empty [NodeContent creator] ] -appXml :: Map Text Worksheet -> L.ByteString-appXml s = renderLBS def $ Document (Prologue [] Nothing []) root []+appXml :: [Text] -> L.ByteString+appXml sheetNames =+ renderLBS def $ Document (Prologue [] Nothing []) root [] where- nsAttrs = M.fromList [("xmlns:vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes")]+ sheetCount = length sheetNames root = Element (extPropNm "Properties") nsAttrs [ extPropEl "TotalTime" [NodeContent "0"] , extPropEl "HeadingPairs" [- vTypeEl "vector" (M.fromList [("size", "2"), ("baseType", "variant")])- [ vTypeEl0 "variant"- [vTypeEl0 "lpstr" [NodeContent "Worksheets"]]- , vTypeEl0 "variant"- [vTypeEl0 "i4" [NodeContent $ txti $ M.size s]]- ]- ]+ vTypeEl "vector" (M.fromList [ ("size", "2")+ , ("baseType", "variant")])+ [ vTypeEl0 "variant"+ [vTypeEl0 "lpstr" [NodeContent "Worksheets"]]+ , vTypeEl0 "variant"+ [vTypeEl0 "i4" [NodeContent $ txti sheetCount]]+ ]+ ] , extPropEl "TitlesOfParts" [- vTypeEl "vector" (M.fromList [("size", txti $ M.size s),("baseType","lpstr")]) $- map (vTypeEl0 "lpstr" . return . NodeContent) $ M.keys s- ]+ vTypeEl "vector" (M.fromList [ ("size", txti sheetCount)+ , ("baseType", "lpstr")]) $+ map (vTypeEl0 "lpstr" . return . NodeContent) sheetNames+ ] ]+ nsAttrs = M.fromList [("xmlns:vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes")] extPropNm n = nm "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" n extPropEl n = nEl (extPropNm n) M.empty vTypeEl0 n = vTypeEl n M.empty vTypeEl = nEl . nm "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" -data XlsxCellData = XlsxSS Int- | XlsxDouble Double- | XlsxBool Bool- deriving (Show, Eq)+data XlsxCellData+ = XlsxSS Int+ | XlsxDouble Double+ | XlsxBool Bool+ | XlsxError ErrorType+ deriving (Eq, Show, Generic)+ data XlsxCell = XlsxCell- { xlsxCellStyle :: Maybe Int- , xlsxCellValue :: Maybe XlsxCellData- } deriving (Show, Eq)+ { xlsxCellStyle :: Maybe Int+ , xlsxCellValue :: Maybe XlsxCellData+ , xlsxComment :: Maybe Comment+ , xlsxCellFormula :: Maybe CellFormula+ } deriving (Eq, Show, Generic) xlsxCellType :: XlsxCell -> Text xlsxCellType XlsxCell{xlsxCellValue=Just(XlsxSS _)} = "s" xlsxCellType XlsxCell{xlsxCellValue=Just(XlsxBool _)} = "b"+xlsxCellType XlsxCell{xlsxCellValue=Just(XlsxError _)} = "e" xlsxCellType _ = "n" -- default in SpreadsheetML schema, TODO: add other types -value :: XlsxCell -> Text-value XlsxCell{xlsxCellValue=Just(XlsxSS i)} = txti i-value XlsxCell{xlsxCellValue=Just(XlsxDouble d)} = txtd d-value XlsxCell{xlsxCellValue=Just(XlsxBool True)} = "1"-value XlsxCell{xlsxCellValue=Just(XlsxBool False)} = "0"-value _ = error "value undefined"+value :: XlsxCellData -> Text+value (XlsxSS i) = txti i+value (XlsxDouble d) = txtd d+value (XlsxBool True) = "1"+value (XlsxBool False) = "0"+value (XlsxError eType) = toAttrVal eType -transformSheetData :: Vector Text -> Worksheet -> [(Int, [(Int, XlsxCell)])]+transformSheetData :: SharedStringTable -> Worksheet -> Cells transformSheetData shared ws = map transformRow $ toRows (ws ^. wsCells) where transformRow = second (map transformCell)- transformCell (c, Cell{_cellValue=v, _cellStyle=s}) =- (c, XlsxCell s (fmap transformValue v))- transformValue (CellText t) =- let Just i = searchFromTo (\p -> shared V.! p >= t) 0 (V.length shared - 1)- in XlsxSS i+ transformCell (c, Cell{..}) =+ (c, XlsxCell _cellStyle (fmap transformValue _cellValue) _cellComment _cellFormula)+ transformValue (CellText t) = XlsxSS (sstLookupText shared t) transformValue (CellDouble dbl) = XlsxDouble dbl transformValue (CellBool b) = XlsxBool b+ transformValue (CellRich r) = XlsxSS (sstLookupRich shared r)+ transformValue (CellError e) = XlsxError e -sheetXml :: [ColumnsWidth] -> Map Int RowProperties -> [(Int, [(Int, XlsxCell)])] -> [Text]-> L.ByteString-sheetXml cws rh rows merges = renderLBS def $ Document (Prologue [] Nothing []) root []- where- cType = xlsxCellType- root = addNS "http://schemas.openxmlformats.org/spreadsheetml/2006/main" $- Element "worksheet" M.empty $ catMaybes- [nonEmptyNmEl "cols" M.empty $ map cwEl cws,- justNmEl "sheetData" M.empty $ map rowEl rows,- nonEmptyNmEl "mergeCells" M.empty $ map mergeE1 merges]- cwEl cw = NodeElement $! Element "col" (M.fromList- [("min", txti $ cwMin cw), ("max", txti $ cwMax cw), ("width", txtd $ cwWidth cw), ("style", txti $ cwStyle cw)]) []- rowEl (r, cells) = nEl "row"- (M.fromList (ht ++ s ++ [("r", txti r) ,("hidden", "false"), ("outlineLevel", "0"),- ("collapsed", "false"), ("customFormat", "true"),- ("customHeight", txtb hasHeight)]))- $ map (cellEl r) cells- where- (ht, hasHeight, s) = case M.lookup r rh of- Just (RowProps (Just h) (Just st)) -> ([("ht", txtd h)], True,[("s", txti st)])- Just (RowProps Nothing (Just st)) -> ([], True, [("s", txti st)])- Just (RowProps (Just h) Nothing ) -> ([("ht", txtd h)], True,[])- _ -> ([], False,[])- mergeE1 t = NodeElement $! Element "mergeCell" (M.fromList [("ref",t)]) []- cellEl r (icol, cell) =- nEl "c" (M.fromList (cellAttrs r (int2col icol) cell))- [nEl "v" M.empty [NodeContent $ value cell] | isJust $ xlsxCellValue cell]- cellAttrs r col cell = cellStyleAttr cell ++ [("r", T.concat [col, txti r]), ("t", cType cell)]- cellStyleAttr XlsxCell{xlsxCellStyle=Nothing} = []- cellStyleAttr XlsxCell{xlsxCellStyle=Just s} = [("s", txti s)]+bookFiles :: Xlsx -> [FileData]+bookFiles xlsx = runST $ do+ ref <- newSTRef 1+ ssRId <- nextRefId ref+ let sheets = xlsx ^. xlSheets & mapped %~ snd+ shared = sstConstruct sheets+ sharedStrings =+ (ssRId, FileData "xl/sharedStrings.xml" (smlCT "sharedStrings") "sharedStrings" $+ ssXml shared)+ stRId <- nextRefId ref+ let style =+ (stRId, FileData "xl/styles.xml" (smlCT "styles") "styles" $+ unStyles (xlsx ^. xlStyles))+ let PvGenerated { pvgCacheFiles = cacheIdFiles+ , pvgOthers = pivotOtherFiles+ , pvgSheetTableFiles = sheetPivotTables+ } =+ generatePivotFiles+ [ (_wsCells, _wsPivotTables)+ | (_, Worksheet {..}) <- xlsx ^. xlSheets+ ]+ sheetCells = map (transformSheetData shared) sheets+ sheetInputs = zip3 sheetCells sheetPivotTables sheets+ tblIdRef <- newSTRef 1+ allSheetFiles <- forM (zip [1..] sheetInputs) $ \(i, (cells, pvTables, sheet)) -> do+ rId <- nextRefId ref+ (sheetFile, others) <- singleSheetFiles i cells pvTables sheet tblIdRef+ return ((rId, sheetFile), others)+ let sheetFiles = map fst allSheetFiles+ sheetAttrsByRId =+ zipWith (\(rId, _) (name, sheet) -> (rId, name, sheet ^. wsState))+ sheetFiles+ (xlsx ^. xlSheets)+ sheetOthers = concatMap snd allSheetFiles+ cacheRefFDsById <- forM cacheIdFiles $ \(cacheId, fd) -> do+ refId <- nextRefId ref+ return (cacheId, (refId, fd))+ let cacheRefsById = [ (cId, rId) | (cId, (rId, _)) <- cacheRefFDsById ]+ cacheRefs = map snd cacheRefFDsById+ bookFile = FileData "xl/workbook.xml" (smlCT "sheet.main") "officeDocument" $+ bookXml sheetAttrsByRId (xlsx ^. xlDefinedNames) cacheRefsById (xlsx ^. xlDateBase)+ rels = FileData "xl/_rels/workbook.xml.rels"+ "application/vnd.openxmlformats-package.relationships+xml"+ "relationships" relsXml+ relsXml = renderLBS def . toDocument . Relationships.fromList $+ [ relEntry i fdRelType (fdPath `relFrom` "xl/workbook.xml")+ | (i, FileData{..}) <- referenced ]+ referenced = sharedStrings:style:sheetFiles ++ cacheRefs+ otherFiles = concat [rels:(map snd referenced), pivotOtherFiles, sheetOthers]+ return $ bookFile:otherFiles -bookXml :: Map Text Worksheet -> L.ByteString-bookXml wss = renderLBS def $ Document (Prologue [] Nothing []) root []+bookXml :: [(RefId, Text, SheetState)]+ -> DefinedNames+ -> [(CacheId, RefId)]+ -> DateBase+ -> L.ByteString+bookXml rIdAttrs (DefinedNames names) cacheIdRefs dateBase =+ renderLBS def {rsNamespaces = nss} $ Document (Prologue [] Nothing []) root [] where- numNames = [(txti i, name) | (i, name) <- zip [1..] (M.keys wss)]- root = addNS "http://schemas.openxmlformats.org/spreadsheetml/2006/main" $ Element "workbook" M.empty- [nEl "sheets" M.empty $- map (\(n, name) -> nEl "sheet"- (M.fromList [("name", name), ("sheetId", n), ("state", "visible"),- (rId, T.concat ["rId", n])]) []) numNames]- rId = nm "http://schemas.openxmlformats.org/officeDocument/2006/relationships" "id"+ nss = [ ("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships") ]+ -- The @bookViews@ element is not required according to the schema, but its+ -- absence can cause Excel to crash when opening the print preview+ -- (see <https://phpexcel.codeplex.com/workitem/2935>). It suffices however+ -- to define a bookViews with a single empty @workbookView@ element+ -- (the @bookViews@ must contain at least one @wookbookView@).+ root =+ addNS "http://schemas.openxmlformats.org/spreadsheetml/2006/main" Nothing $+ elementListSimple+ "workbook"+ ( [ leafElement "workbookPr" (catMaybes ["date1904" .=? justTrue (dateBase == DateBase1904) ])+ , elementListSimple "bookViews" [emptyElement "workbookView"]+ , elementListSimple+ "sheets"+ [ leafElement+ "sheet"+ ["name" .= name, "sheetId" .= i, "state" .= state, (odr "id") .= rId]+ | (i, (rId, name, state)) <- zip [(1 :: Int) ..] rIdAttrs+ ]+ , elementListSimple+ "definedNames"+ [ elementContent0 "definedName" (definedName name lsId) val+ | (name, lsId, val) <- names+ ]+ ] +++ maybeToList+ (nonEmptyElListSimple "pivotCaches" $ map pivotCacheEl cacheIdRefs)+ ) -ssXml :: [Text] -> L.ByteString-ssXml ss =- renderLBS def $ Document (Prologue [] Nothing []) root []- where- root = addNS "http://schemas.openxmlformats.org/spreadsheetml/2006/main" $ Element "sst" M.empty $- map (\s -> nEl "si" M.empty [nEl "t" M.empty [NodeContent s]]) ss+ pivotCacheEl (CacheId cId, refId) =+ leafElement "pivotCache" ["cacheId" .= cId, (odr "id") .= refId] -bookRelXml :: Int -> L.ByteString-bookRelXml n = renderLBS def $ Document (Prologue [] Nothing []) root []- where- root = addNS "http://schemas.openxmlformats.org/package/2006/relationships" $- Element "Relationships" M.empty $- map (\sn -> relEl sn (T.concat ["worksheets/sheet", txti sn, ".xml"]) "worksheet") [1..n]- ++- [relEl (n + 1) "styles.xml" "styles", relEl (n + 2) "sharedStrings.xml" "sharedStrings"]- relEl i target typ =- nEl "Relationship"- (M.fromList [("Id", T.concat ["rId", txti i]), ("Target", target),- ("Type", T.concat ["http://schemas.openxmlformats.org/officeDocument/2006/relationships/", typ])]) []+ definedName :: Text -> Maybe Text -> [(Name, Text)]+ definedName name Nothing = ["name" .= name]+ definedName name (Just lsId) = ["name" .= name, "localSheetId" .= lsId] -rootRelXml :: L.ByteString-rootRelXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\-\<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/><Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\" Target=\"docProps/core.xml\"/><Relationship Id=\"rId3\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\" Target=\"docProps/app.xml\"/></Relationships>"+ssXml :: SharedStringTable -> L.ByteString+ssXml = renderLBS def . toDocument +customPropsXml :: CustomProperties -> L.ByteString+customPropsXml = renderLBS def . toDocument+ contentTypesXml :: [FileData] -> L.ByteString contentTypesXml fds = renderLBS def $ Document (Prologue [] Nothing []) root [] where- root = addNS "http://schemas.openxmlformats.org/package/2006/content-types" $+ root = addNS "http://schemas.openxmlformats.org/package/2006/content-types" Nothing $ Element "Types" M.empty $- map (\fd -> nEl "Override" (M.fromList [("PartName", T.concat ["/", fdName fd]),+ map (\fd -> nEl "Override" (M.fromList [("PartName", T.concat ["/", T.pack $ fdPath fd]), ("ContentType", fdContentType fd)]) []) fds -- | fully qualified XML name@@ -231,7 +602,7 @@ nsName :: [(Text, Text)] -> Text -> Text -> Name nsName nss p n = qName n ns p where- ns = fromJust $ lookup p nss+ ns = fromJustNote "ns name lookup" $ lookup p nss nm :: Text -> Text -> Name nm ns n = Name@@ -239,31 +610,16 @@ , nameNamespace = Just ns , namePrefix = Nothing} -addNS :: Text -> Element -> Element-addNS namespace (Element (Name ln _ _) as ns) = Element (Name ln (Just namespace) Nothing) as (map addNS' ns)- where- addNS' (NodeElement e) = NodeElement $ addNS namespace e- addNS' n = n---- | Creates an element with the given name, attributes and children,--- if there is at least one child. Otherwise returns `Nothing`.-nonEmptyNmEl :: Name -> Map Name Text -> [Node] -> Maybe Node-nonEmptyNmEl _ _ [] = Nothing-nonEmptyNmEl name attrs nodes = justNmEl name attrs nodes---- | Creates an element with the given name, attributes and children.--- Always returns a node/`Just`.-justNmEl :: Name -> Map Name Text -> [Node] -> Maybe Node-justNmEl name attrs nodes = Just $ nEl name attrs nodes- nEl :: Name -> Map Name Text -> [Node] -> Node nEl name attrs nodes = NodeElement $ Element name attrs nodes -txti :: Int -> Text-txti = toStrict . toLazyText . decimal+-- | Creates element holding reference to some linked file+refElement :: Name -> RefId -> Element+refElement name rId = leafElement name [ odr "id" .= rId ] -txtd :: Double -> Text-txtd = toStrict . toLazyText . realFloat+smlCT :: Text -> Text+smlCT t =+ "application/vnd.openxmlformats-officedocument.spreadsheetml." <> t <> "+xml" -txtb :: Bool -> Text-txtb = T.toLower . T.pack . show+relsCT :: Text+relsCT = "application/vnd.openxmlformats-package.relationships+xml"
+ src/Codec/Xlsx/Writer/Internal.hs view
@@ -0,0 +1,227 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveGeneric #-}++module Codec.Xlsx.Writer.Internal (+ -- * Rendering documents+ ToDocument(..)+ , documentFromElement+ , documentFromNsElement+ , documentFromNsPrefElement+ -- * Rendering elements+ , ToElement(..)+ , countedElementList+ , nonEmptyCountedElementList+ , elementList+ , elementListSimple+ , nonEmptyElListSimple+ , leafElement+ , emptyElement+ , elementContent0+ , elementContent+ , elementContentPreserved+ , elementValue+ , elementValueDef+ -- * Rendering attributes+ , ToAttrVal(..)+ , (.=)+ , (.=?)+ , setAttr+ -- * Dealing with namespaces+ , addNS+ , mainNamespace+ -- * Misc+ , txti+ , txtb+ , txtd+ , justNonDef+ , justTrue+ , justFalse+ ) where++import qualified Data.Map as Map+import Data.Maybe (catMaybes)+import Data.String (fromString)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Lazy (toStrict)+import Data.Text.Lazy.Builder (toLazyText)+import Data.Text.Lazy.Builder.Int+import Data.Text.Lazy.Builder.RealFloat+import Text.XML++{-------------------------------------------------------------------------------+ Rendering documents+-------------------------------------------------------------------------------}++class ToDocument a where+ toDocument :: a -> Document++documentFromElement :: Text -> Element -> Document+documentFromElement comment e =+ documentFromNsElement comment mainNamespace e++documentFromNsElement :: Text -> Text -> Element -> Document+documentFromNsElement comment ns e =+ documentFromNsPrefElement comment ns Nothing e++documentFromNsPrefElement :: Text -> Text -> Maybe Text -> Element -> Document+documentFromNsPrefElement comment ns prefix e = Document {+ documentRoot = addNS ns prefix e+ , documentEpilogue = []+ , documentPrologue = Prologue {+ prologueBefore = [MiscComment comment]+ , prologueDoctype = Nothing+ , prologueAfter = []+ }+ }++{-------------------------------------------------------------------------------+ Rendering elements+-------------------------------------------------------------------------------}++class ToElement a where+ toElement :: Name -> a -> Element++countedElementList :: Name -> [Element] -> Element+countedElementList nm as = elementList nm [ "count" .= length as ] as++nonEmptyCountedElementList :: Name -> [Element] -> Maybe Element+nonEmptyCountedElementList nm as = case as of+ [] -> Nothing+ _ -> Just $ countedElementList nm as++elementList :: Name -> [(Name, Text)] -> [Element] -> Element+elementList nm attrs els = Element {+ elementName = nm+ , elementNodes = map NodeElement els+ , elementAttributes = Map.fromList attrs+ }++elementListSimple :: Name -> [Element] -> Element+elementListSimple nm els = elementList nm [] els++nonEmptyElListSimple :: Name -> [Element] -> Maybe Element+nonEmptyElListSimple _ [] = Nothing+nonEmptyElListSimple nm els = Just $ elementListSimple nm els++leafElement :: Name -> [(Name, Text)] -> Element+leafElement nm attrs = elementList nm attrs []++emptyElement :: Name -> Element+emptyElement nm = elementList nm [] []++elementContent0 :: Name -> [(Name, Text)] -> Text -> Element+elementContent0 nm attrs txt = Element {+ elementName = nm+ , elementAttributes = Map.fromList attrs+ , elementNodes = [NodeContent txt]+ }++elementContent :: Name -> Text -> Element+elementContent nm txt = elementContent0 nm [] txt++elementContentPreserved :: Name -> Text -> Element+elementContentPreserved nm txt = elementContent0 nm [ preserveSpace ] txt+ where+ preserveSpace = (+ Name { nameLocalName = "space"+ , nameNamespace = Just "http://www.w3.org/XML/1998/namespace"+ , namePrefix = Nothing+ }+ , "preserve"+ )++{-------------------------------------------------------------------------------+ Rendering attributes+-------------------------------------------------------------------------------}++class ToAttrVal a where+ toAttrVal :: a -> Text++instance ToAttrVal Text where toAttrVal = id+instance ToAttrVal String where toAttrVal = fromString+instance ToAttrVal Int where toAttrVal = txti+instance ToAttrVal Integer where toAttrVal = txti+instance ToAttrVal Double where toAttrVal = txtd++instance ToAttrVal Bool where+ toAttrVal True = "1"+ toAttrVal False = "0"++elementValue :: ToAttrVal a => Name -> a -> Element+elementValue nm a = leafElement nm [ "val" .= a ]++elementValueDef :: (Eq a, ToAttrVal a) => Name -> a -> a -> Element+elementValueDef nm defVal a =+ leafElement nm $ catMaybes [ "val" .=? justNonDef defVal a ]++(.=) :: ToAttrVal a => Name -> a -> (Name, Text)+nm .= a = (nm, toAttrVal a)++(.=?) :: ToAttrVal a => Name -> Maybe a -> Maybe (Name, Text)+_ .=? Nothing = Nothing+nm .=? (Just a) = Just (nm .= a)++setAttr :: ToAttrVal a => Name -> a -> Element -> Element+setAttr nm a el@Element{..} = el{ elementAttributes = attrs' }+ where+ attrs' = Map.insert nm (toAttrVal a) elementAttributes++{-------------------------------------------------------------------------------+ Dealing with namespaces+-------------------------------------------------------------------------------}++-- | Set the namespace for the entire document+--+-- This follows the same policy that the rest of the xlsx package uses.+addNS :: Text -> Maybe Text -> Element -> Element+addNS ns prefix Element{..} = Element{+ elementName = goName elementName+ , elementAttributes = elementAttributes+ , elementNodes = map goNode elementNodes+ }+ where+ goName :: Name -> Name+ goName n@Name{..} =+ case nameNamespace of+ Just _ -> n -- If a namespace was explicitly set, leave it+ Nothing -> Name{+ nameLocalName = nameLocalName+ , nameNamespace = Just ns+ , namePrefix = prefix+ }++ goNode :: Node -> Node+ goNode (NodeElement e) = NodeElement $ addNS ns prefix e+ goNode n = n++-- | The main namespace for Excel+mainNamespace :: Text+mainNamespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"+++txtd :: Double -> Text+txtd v+ | v - fromInteger v' == 0 = txti v'+ | otherwise = toStrict . toLazyText $ realFloat v+ where+ v' = floor v++txtb :: Bool -> Text+txtb = T.toLower . T.pack . show++txti :: (Integral a) => a -> Text+txti = toStrict . toLazyText . decimal++justNonDef :: (Eq a) => a -> a -> Maybe a+justNonDef defVal a | a == defVal = Nothing+ | otherwise = Just a++justFalse :: Bool -> Maybe Bool+justFalse = justNonDef True++justTrue :: Bool -> Maybe Bool+justTrue = justNonDef False
+ src/Codec/Xlsx/Writer/Internal/PivotTable.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+module Codec.Xlsx.Writer.Internal.PivotTable+ ( PivotTableFiles(..)+ , renderPivotTableFiles+ ) where++import Data.ByteString.Lazy (ByteString)+import Data.List (elemIndex, transpose)+import Data.List.Extra (nubOrd)+import qualified Data.Map as M+import Data.Maybe (catMaybes, fromMaybe, mapMaybe)+import Data.Text (Text)+import GHC.Generics (Generic)+import Safe (fromJustNote)+import Text.XML++import Codec.Xlsx.Types.Cell+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.Relationships (odr)+import Codec.Xlsx.Types.PivotTable+import Codec.Xlsx.Types.PivotTable.Internal+import Codec.Xlsx.Writer.Internal++data PivotTableFiles = PivotTableFiles+ { pvtfTable :: ByteString+ , pvtfCacheDefinition :: ByteString+ , pvtfCacheRecords :: ByteString+ } deriving (Eq, Show, Generic)++data CacheDefinition = CacheDefinition+ { cdSourceRef :: CellRef+ , cdSourceSheet :: Text+ , cdFields :: [CacheField]+ } deriving (Eq, Show, Generic)++renderPivotTableFiles :: CellMap -> Int -> PivotTable -> PivotTableFiles+renderPivotTableFiles cm cacheId t = PivotTableFiles {..}+ where+ pvtfTable = renderLBS def $ ptDefinitionDocument cacheId cache t+ cache = generateCache cm t+ (cacheDoc, cacheRecordsDoc) = writeCache cache+ pvtfCacheDefinition = renderLBS def cacheDoc+ pvtfCacheRecords = renderLBS def cacheRecordsDoc++ptDefinitionDocument :: Int -> CacheDefinition -> PivotTable -> Document+ptDefinitionDocument cacheId cache t =+ documentFromElement "Pivot table generated by xlsx" $+ ptDefinitionElement "pivotTableDefinition" cacheId cache t++ptDefinitionElement :: Name -> Int -> CacheDefinition -> PivotTable -> Element+ptDefinitionElement nm cacheId cache PivotTable {..} =+ elementList nm attrs elements+ where+ attrs =+ catMaybes+ [ "colGrandTotals" .=? justFalse _pvtColumnGrandTotals+ , "rowGrandTotals" .=? justFalse _pvtRowGrandTotals+ , "outline" .=? justTrue _pvtOutline+ , "outlineData" .=? justTrue _pvtOutlineData+ ] +++ [ "name" .= _pvtName+ , "dataCaption" .= _pvtDataCaption+ , "cacheId" .= cacheId+ , "dataOnRows" .= (DataPosition `elem` _pvtRowFields)+ ]+ elements = [location, pivotFields, rowFields, colFields, dataFields]+ location =+ leafElement+ "location"+ [ "ref" .= _pvtLocation+ -- TODO : set proper+ , "firstHeaderRow" .= (1 :: Int)+ , "firstDataRow" .= (2 :: Int)+ , "firstDataCol" .= (1 :: Int)+ ]+ name2x = M.fromList $ zip (mapMaybe _pfiName _pvtFields) [0 ..]+ mapFieldToX f = fromJustNote "no field" $ M.lookup f name2x+ pivotFields = elementListSimple "pivotFields" $ map pFieldEl _pvtFields+ maybeFieldIn Nothing _ = False+ maybeFieldIn (Just name) positions = FieldPosition name `elem` positions+ pFieldEl PivotFieldInfo { _pfiName = fName+ , _pfiOutline = outline+ , _pfiSortType = sortType+ , _pfiHiddenItems = hidden+ }+ | fName `maybeFieldIn` _pvtRowFields =+ pFieldEl' fName outline ("axisRow" :: Text) hidden sortType+ | fName `maybeFieldIn` _pvtColumnFields =+ pFieldEl' fName outline ("axisCol" :: Text) hidden sortType+ | otherwise =+ leafElement "pivotField" $+ [ "dataField" .= True+ , "showAll" .= False+ , "outline" .= outline] +++ catMaybes ["name" .=? fName]+ pFieldEl' fName outline axis hidden sortType =+ elementList+ "pivotField"+ ([ "axis" .= axis+ , "showAll" .= False+ , "outline" .= outline+ ] +++ catMaybes [ "name" .=? fName+ , "sortType" .=? justNonDef FieldSortManual sortType])+ [ elementListSimple "items" $+ items fName hidden +++ [leafElement "item" ["t" .= ("default" :: Text)]]+ ]+ items Nothing _ = []+ items (Just fName) hidden =+ [ itemEl x item hidden+ | (x, item) <- zip [0 ..] . fromMaybe [] $ M.lookup fName cachedItems+ ]+ itemEl x item hidden =+ leafElement "item" $+ ["x" .= (x :: Int)] ++ catMaybes ["h" .=? justTrue (item `elem` hidden)]+ cachedItems =+ M.fromList $ [(cfName, cfItems) | CacheField {..} <- cdFields cache]+ rowFields =+ elementListSimple "rowFields" . map fieldEl $+ if length _pvtDataFields > 1+ then _pvtRowFields+ else filter (/= DataPosition) _pvtRowFields+ colFields = elementListSimple "colFields" $ map fieldEl _pvtColumnFields+ fieldEl p = leafElement "field" ["x" .= fieldPos p]+ fieldPos DataPosition = (-2) :: Int+ fieldPos (FieldPosition f) = mapFieldToX f+ dataFields = elementListSimple "dataFields" $ map dFieldEl _pvtDataFields+ dFieldEl DataField {..} =+ leafElement "dataField" $+ catMaybes+ [ "name" .=? Just _dfName+ , "fld" .=? Just (mapFieldToX _dfField)+ , "subtotal" .=? justNonDef ConsolidateSum _dfFunction+ ]++generateCache :: CellMap -> PivotTable -> CacheDefinition+generateCache cm PivotTable {..} =+ CacheDefinition+ { cdSourceRef = _pvtSrcRef+ , cdSourceSheet = _pvtSrcSheet+ , cdFields = cachedFields+ }+ where+ cachedFields = mapMaybe (fmap cache . _pfiName) _pvtFields+ cache name =+ CacheField+ { cfName = name+ , cfItems =+ fromJustNote "specified pivot table field does not exist" $+ M.lookup name itemsByName+ }+ ((r1, c1), (r2, c2)) =+ fromJustNote "Invalid src ref of pivot table " $ fromRange _pvtSrcRef+ getCellValue ix = M.lookup ix cm >>= _cellValue+ itemsByName =+ M.fromList $+ flip mapMaybe [c1 .. c2] $ \c -> do+ CellText nm <- getCellValue (r1, c)+ let values = mapMaybe (\r -> getCellValue (r, c)) [(r1 + 1) .. r2]+ return (PivotFieldName nm, nubOrd values)++writeCache :: CacheDefinition -> (Document, Document)+writeCache CacheDefinition {..} = (cacheDefDoc, cacheRecordsDoc)+ where+ cacheDefDoc =+ documentFromElement "Pivot cache definition generated by xlsx" $+ elementList "pivotCacheDefinition" attrs elements+ attrs = ["invalid" .= True, "refreshOnLoad" .= True, odr "id" .= unsafeRefId 1]+ elements = [worksheetSource, cacheFields]+ worksheetSource =+ elementList+ "cacheSource"+ ["type" .= ("worksheet" :: Text)]+ [ leafElement+ "worksheetSource"+ ["ref" .= cdSourceRef, "sheet" .= cdSourceSheet]+ ]+ cacheFields =+ elementListSimple "cacheFields" $ map (toElement "cacheField") cdFields+ cacheRecordsDoc =+ documentFromElement "Pivot cache records generated by xlsx" .+ elementListSimple "pivotCacheRecords" $+ map (elementListSimple "r" . map recordValueToEl) cacheRecords+ recordValueToEl (CacheText t) = leafElement "s" ["v" .= t]+ recordValueToEl (CacheNumber n) = leafElement "n" ["v" .= n]+ recordValueToEl (CacheIndex i) = leafElement "x" ["v" .= i]+ cacheRecords = transpose $ map (itemsToRecordValues . cfItems) cdFields+ itemsToRecordValues vals =+ if all isText vals+ then indexes vals+ else map itemToRecordValue vals+ isText (CellText _) = True+ isText _ = False+ indexes vals =+ [ CacheIndex . fromJustNote "inconsistend definition" $ elemIndex v vals+ | v <- vals+ ]+ itemToRecordValue (CellDouble d) = CacheNumber d+ itemToRecordValue (CellText t) = CacheText t+ itemToRecordValue v = error $ "Unsupported value for pivot tables: " ++ show v
+ src/Codec/Xlsx/Writer/Internal/Stream.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Internal stream related functions.+-- These are exported because they're tested like this.+-- It's not expected a user would need this.+module Codec.Xlsx.Writer.Internal.Stream+ ( upsertSharedString+ , initialSharedString+ , string_map+ , SharedStringState(..)+ ) where+++import Codec.Xlsx.LensCompat+import Control.Monad.State.Strict+import Data.Map.Strict (Map)+import Data.Maybe+import Data.Text (Text)++newtype SharedStringState = MkSharedStringState+ { _string_map :: Map Text Int+ }+makeLenses 'MkSharedStringState++initialSharedString :: SharedStringState+initialSharedString = MkSharedStringState mempty++-- properties:+-- for a list of [text], every unique text gets a unique number.+upsertSharedString :: MonadState SharedStringState m => Text -> m (Text,Int)+upsertSharedString current = do+ strings <- use string_map++ let mIdx :: Maybe Int+ mIdx = strings ^? ix current++ idx :: Int+ idx = fromMaybe (length strings) mIdx++ newMap :: Map Text Int+ newMap = at current ?~ idx $ strings++ string_map .= newMap+ pure (current, idx)+
+ src/Codec/Xlsx/Writer/Stream.hs view
@@ -0,0 +1,429 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Writes Excel files from a stream, which allows creation of+-- large Excel files while remaining in constant memory.+module Codec.Xlsx.Writer.Stream+ ( writeXlsx+ , writeXlsxMultipleSheets+ , writeXlsxWithSharedStrings+ , writeXlsxWithConfig+ , SheetWriteSettings(..)+ , defaultSettings+ , wsSheetView+ , wsZip+ , wsColumnProperties+ , wsRowProperties+ , wsStyles+ , WriteXlsxConfig+ , defWriteXlsxConfig+ , setWriteSettings+ , setSharedStringsMap+ , setNamesAndConduits+ , setNameInfosAndConduits+ -- *** Shared strings+ , sharedStrings+ , sharedStringsStream+ ) where++import Codec.Archive.Zip.Conduit.UnZip+import Codec.Archive.Zip.Conduit.Zip+import Codec.Xlsx.Parser.Internal (addSmlNamespace)+import Codec.Xlsx.Parser.Stream+import Codec.Xlsx.Types (ColumnsProperties (..), RowProperties (..),+ Styles (..), _AutomaticHeight, _CustomHeight,+ emptyStyles, rowHeightLens, SheetState (..))+import Codec.Xlsx.Types.Cell+import Codec.Xlsx.Types.Common+import Codec.Xlsx.Types.Internal.Relationships (odr, pr, odRelNs)+import Codec.Xlsx.Types.SheetViews+import Codec.Xlsx.Writer.Internal (nonEmptyElListSimple, toAttrVal, toElement,+ txtd, txti)+import Codec.Xlsx.Writer.Internal.Stream+import Conduit (PrimMonad, yield, (.|))+import qualified Conduit as C+import Codec.Xlsx.LensCompat+import Control.Monad+import Control.Monad.Catch+import Control.Monad.Reader.Class+import Control.Monad.State.Strict+import Data.ByteString (ByteString)+import Data.ByteString.Builder (Builder)+import Data.Coerce+import Data.Conduit (ConduitT)+import qualified Data.Conduit.List as CL+import Data.Foldable (fold, traverse_)+import Data.List+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time+import Data.Word+import Data.XML.Types+import Text.Printf+import Text.XML (toXMLElement)+import qualified Text.XML as TXML+import Text.XML.Stream.Render+import Text.XML.Unresolved (elementToEvents)+import Data.Bifunctor (Bifunctor(second))+++upsertSharedStrings :: MonadState SharedStringState m => Row -> m [(Text,Int)]+upsertSharedStrings row =+ traverse upsertSharedString items+ where+ items :: [Text]+ items = row ^.. ri_cell_row . traversed . cellValue . _Just . _CellText++-- | Process sheetItems into shared strings structure to be put into+-- 'writeXlsxWithSharedStrings'+sharedStrings :: Monad m => ConduitT Row b m (Map Text Int)+sharedStrings = void sharedStringsStream .| CL.foldMap (uncurry Map.singleton)++-- | creates a unique number for every encountered string in the stream+-- This is used for creating a required structure in the xlsx format+-- called shared strings. Every string get's transformed into a number+--+-- exposed to allow further processing, we also know the map after processing+-- but I don't think conduit provides a way of getting that out.+-- use 'sharedStrings' to just get the map+sharedStringsStream :: Monad m =>+ ConduitT Row (Text, Int) m (Map Text Int)+sharedStringsStream = fmap (view string_map) $ C.execStateC initialSharedString $+ CL.mapFoldableM upsertSharedStrings++-- | Settings for writing a single sheet.+data SheetWriteSettings = MkSheetWriteSettings+ { _wsSheetView :: [SheetView]+ , _wsZip :: ZipOptions -- ^ Enable zipOpt64=True if you intend writing large xlsx files, zip needs 64bit for files over 4gb.+ , _wsColumnProperties :: [ColumnsProperties]+ , _wsRowProperties :: Map Int RowProperties+ , _wsStyles :: Styles+ }+instance Show SheetWriteSettings where+ -- ZipOptions lacks a show instance-}+ show (MkSheetWriteSettings s _ y r _) = printf "MkSheetWriteSettings{ _wsSheetView=%s, _wsColumnProperties=%s, _wsZip=defaultZipOptions, _wsRowProperties=%s }" (show s) (show y) (show r)+makeLenses ''SheetWriteSettings++defaultSettings :: SheetWriteSettings+defaultSettings = MkSheetWriteSettings+ { _wsSheetView = []+ , _wsColumnProperties = []+ , _wsRowProperties = mempty+ , _wsStyles = emptyStyles+ , _wsZip = defaultZipOptions {+ zipOpt64 = False+ -- There is a magick number in the zip archive package,+ -- https://hackage.haskell.org/package/zip-archive-0.4.1/docs/src/Codec.Archive.Zip.html#local-6989586621679055672+ -- if we enable 64bit the number doesn't align causing the test to fail.+ }+ }++++-- | Transform a 'Row' stream into a stream that creates the xlsx file format+-- (to be consumed by sinkfile for example)+-- This first runs 'sharedStrings' and then 'writeXlsxWithSharedStrings'.+-- If you want xlsx files this is the most obvious function to use.+-- the others are exposed in case you can cache the shared strings for example.+-- See also 'writeXlsxMultipleSheets'.+writeXlsx :: MonadThrow m+ => PrimMonad m+ => SheetWriteSettings -- ^ use 'defaultSettings'+ -> ConduitT () Row m () -- ^ the conduit producing sheetitems+ -> ConduitT () ByteString m Word64 -- ^ result conduit producing xlsx files+writeXlsx settings sheetC = do+ writeXlsxWithConfig (defWriteXlsxConfig+ & setWriteSettings settings+ & setNamesAndConduits [("Sheet1", sheetC)])++-- | Same as 'writeXlsx' but write to multiple sheets.+writeXlsxMultipleSheets :: MonadThrow m+ => PrimMonad m+ => SheetWriteSettings+ -- ^ use 'defaultSettings'.+ -- Currently, all sheets will use the same setting.+ -> [(Text, ConduitT () Row m ())]+ -- ^ the conduits producing sheetitems for each sheet+ -> ConduitT () ByteString m Word64 -- ^ result conduit producing xlsx files+writeXlsxMultipleSheets settings sheets = do+ writeXlsxWithConfig (defWriteXlsxConfig+ & setWriteSettings settings+ & setNamesAndConduits sheets)++-- TODO maybe should use bimap instead: https://hackage.haskell.org/package/bimap-0.4.0/docs/Data-Bimap.html+-- it guarantees uniqueness of both text and int+-- | This write Excel file with a shared strings lookup table.+-- It appears that it is optional.+-- Failed lookups will result in valid xlsx.+-- There are several conditions on shared strings,+--+-- 1. Every text to int is unique on both text and int.+-- 2. Every Int should have a gap no greater than 1. [("xx", 3), ("yy", 4)] is okay, whereas [("xx", 3), ("yy", 5)] is not.+-- 3. It's expected this starts from 0.+--+-- Use 'sharedStringsStream' to get a good shared strings table.+-- This is provided because the user may have a more efficient way of+-- constructing this table than the library can provide,+-- for example through database operations.+writeXlsxWithSharedStrings :: MonadThrow m => PrimMonad m+ => SheetWriteSettings+ -> Map Text Int -- ^ shared strings table+ -> [(Text, ConduitT () Row m ())]+ -> ConduitT () ByteString m Word64+writeXlsxWithSharedStrings settings sharedStrings' sheets =+ writeXlsxWithConfig (defWriteXlsxConfig+ & setWriteSettings settings+ & setSharedStringsMap (Just sharedStrings')+ & setNamesAndConduits sheets)++-- | Configure how to write to an xlsx. Provides setters but no getters to+-- hide internals for future compatibility+data WriteXlsxConfig m = MkWriteXlsxConfig+ { _writeSettings :: SheetWriteSettings+ , _sharedStringsMap :: Maybe (Map Text Int)+ , _infosAndConduits :: [((Text, SheetState), ConduitT () Row m ())]+ }++-- | Create a blank config. Uses the default write setting config, derives shared+-- strings from the conduits and has no conduits or sheet informations within.+defWriteXlsxConfig :: WriteXlsxConfig m+defWriteXlsxConfig = MkWriteXlsxConfig defaultSettings mempty []++-- | Overwrite the existing sheet write settings.+setWriteSettings :: SheetWriteSettings -> WriteXlsxConfig m -> WriteXlsxConfig m+setWriteSettings ws (MkWriteXlsxConfig _ ssm iac) = MkWriteXlsxConfig ws ssm iac++-- | Set the shared strings map. If set to Nothing, derives the shared strings+-- from the conduits.+setSharedStringsMap :: Maybe (Map Text Int) -> WriteXlsxConfig m -> WriteXlsxConfig m+setSharedStringsMap ssm (MkWriteXlsxConfig ws _ iac) = MkWriteXlsxConfig ws ssm iac++-- | Set the sheet infos and conduits for each sheet.+setNameInfosAndConduits :: [((Text, SheetState), ConduitT () Row m ())] -> WriteXlsxConfig m -> WriteXlsxConfig m+setNameInfosAndConduits iac (MkWriteXlsxConfig ws ssm _) = MkWriteXlsxConfig ws ssm iac++-- | Set the names and conduits for each sheet. The sheet info is derived from+-- the ordering.+setNamesAndConduits :: [(Text, ConduitT () Row m ())] -> WriteXlsxConfig m -> WriteXlsxConfig m+setNamesAndConduits nacs (MkWriteXlsxConfig ws ssm _) = MkWriteXlsxConfig ws ssm infosAndConduits'+ where+ infosAndConduits' = nacs <&> \(n, c) -> ((n, Visible), c)++-- | Write to an XLSX using the 'WriteXlsxConfig' structure.+writeXlsxWithConfig :: MonadThrow m => PrimMonad m+ => WriteXlsxConfig m+ -> ConduitT () ByteString m Word64+writeXlsxWithConfig (MkWriteXlsxConfig ws ssm iac) = do+ let rowConduits = foldr ((>>) . snd) mempty iac+ sstrings <- maybe (rowConduits .| sharedStrings) pure ssm+ combinedFiles ws sstrings iac .| zipStream (ws ^. wsZip)++combinedFiles :: PrimMonad m+ => SheetWriteSettings+ -> Map Text Int+ -> [((Text, SheetState), ConduitT () Row m ())]+ -> ConduitT () (ZipEntry, ZipData m) m ()+combinedFiles settings sharedStrings' sheets =+ let indicesAndSheets = zip [1..] sheets+ zippedSheets = map (\(sheetId, (_, rowConduit)) ->+ ( zipEntry ("xl/worksheets/sheet" <> Text.pack (show sheetId) <> ".xml")+ , ZipDataSource $ rowConduit .| C.runReaderC settings (writeWorkSheet sharedStrings') .| eventsToBS+ )) indicesAndSheets+ in+ C.yieldMany $+ [ (zipEntry "xl/sharedStrings.xml", ZipDataSource $ writeSst sharedStrings' .| eventsToBS)+ , (zipEntry "[Content_Types].xml", ZipDataSource $ writeContentTypes .| eventsToBS)+ , (zipEntry "xl/workbook.xml", ZipDataSource $ writeWorkbook (map (second fst) indicesAndSheets)+ .| renderBuilder ( def {rsNamespaces=[("r", odRelNs)]})+ .| C.builderToByteString)+ , (zipEntry "xl/styles.xml", ZipDataByteString $ coerce $ settings ^. wsStyles)+ , (zipEntry "xl/_rels/workbook.xml.rels", ZipDataSource $ writeWorkbookRels (length sheets) .| eventsToBS)+ , (zipEntry "_rels/.rels", ZipDataSource $ writeRootRels .| eventsToBS)+ ] <> zippedSheets++el :: Monad m => Name -> Monad m => forall i. ConduitT i Event m () -> ConduitT i Event m ()+el x = tag x mempty++-- Clark notation is used a lot for xml namespaces in this module:+-- <https://hackage.haskell.org/package/xml-types-0.3.8/docs/Data-XML-Types.html#t:Name>+-- Name has an IsString instance which parses it+override :: Monad m => Text -> Text -> forall i. ConduitT i Event m ()+override content' part =+ tag "{http://schemas.openxmlformats.org/package/2006/content-types}Override"+ (attr "ContentType" content'+ <> attr "PartName" part) $ pure ()++-- | required by Excel.+writeContentTypes :: Monad m => forall i. ConduitT i Event m ()+writeContentTypes = doc "{http://schemas.openxmlformats.org/package/2006/content-types}Types" $ do+ override "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" "/xl/workbook.xml"+ override "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" "/xl/sharedStrings.xml"+ override "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" "/xl/styles.xml"+ override "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" "/xl/worksheets/sheet1.xml"+ override "application/vnd.openxmlformats-package.relationships+xml" "/xl/_rels/workbook.xml.rels"+ override "application/vnd.openxmlformats-package.relationships+xml" "/_rels/.rels"++-- | required by Excel.+writeWorkbook :: Monad m => [(Int, (Text, SheetState))] -> forall i. ConduitT i Event m ()+writeWorkbook sheetInfos =+ let addSheet (sheetId, (sheetName, visibility)) = tag (addSmlNamespace "sheet")+ (attr "name" sheetName+ <> attr "sheetId" (Text.pack $ show sheetId)+ <> attr (odr "id") ("rId" <> (Text.pack $ show (sheetId + 2)))+ <> attr "state" (case visibility of+ Visible -> "visible"+ Hidden -> "hidden"+ VeryHidden -> "veryHidden")+ ) $ pure ()+ in doc (addSmlNamespace "workbook") $+ el (addSmlNamespace "sheets") $ mapM_ addSheet sheetInfos++doc :: Monad m => Name -> forall i. ConduitT i Event m () -> ConduitT i Event m ()+doc root docM = do+ yield EventBeginDocument+ el root docM+ yield EventEndDocument++relationship :: Monad m => Text -> Int -> Text -> forall i. ConduitT i Event m ()+relationship target id' type' =+ tag (pr "Relationship")+ (attr "Type" type'+ <> attr "Id" (Text.pack $ "rId" <> show id')+ <> attr "Target" target+ ) $ pure ()++writeWorkbookRels :: Monad m => forall i. Int -> ConduitT i Event m ()+writeWorkbookRels sheetCount = doc (pr "Relationships") $ do+ relationship "sharedStrings.xml" 1 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"+ relationship "styles.xml" 2 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"+ let schema = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"+ mapM_ (\sheetId -> relationship ("worksheets/sheet" <> (Text.pack (show sheetId)) <> ".xml") (sheetId + 2) schema) [1..sheetCount]++writeRootRels :: Monad m => forall i. ConduitT i Event m ()+writeRootRels = doc (pr "Relationships") $+ relationship "xl/workbook.xml" 1 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"+++zipEntry :: Text -> ZipEntry+zipEntry x = ZipEntry+ { zipEntryName = Left x+ , zipEntryTime = LocalTime (toEnum 0) midnight+ , zipEntrySize = Nothing+ , zipEntryExternalAttributes = Nothing+ }++eventsToBS :: PrimMonad m => ConduitT Event ByteString m ()+eventsToBS = writeEvents .| C.builderToByteString++writeSst :: Monad m => Map Text Int -> forall i. ConduitT i Event m ()+writeSst sharedStrings' = doc (addSmlNamespace "sst") $+ void $ traverse (el (addSmlNamespace "si") . el (addSmlNamespace "t") . content . fst+ ) $ sortBy (\(_, i) (_, y :: Int) -> compare i y) $ Map.toList sharedStrings'++writeEvents :: PrimMonad m => ConduitT Event Builder m ()+writeEvents = renderBuilder def++sheetViews :: forall m . MonadReader SheetWriteSettings m => forall i . ConduitT i Event m ()+sheetViews = do+ sheetView <- view wsSheetView++ unless (null sheetView) $ el (addSmlNamespace "sheetViews") $ do+ let+ view' :: [Element]+ view' = setNameSpaceRec spreadSheetNS . toXMLElement . toElement (addSmlNamespace "sheetView") <$> sheetView++ C.yieldMany $ elementToEvents =<< view'++spreadSheetNS :: Text+spreadSheetNS = fold $ nameNamespace $ addSmlNamespace ""++setNameSpaceRec :: Text -> Element -> Element+setNameSpaceRec space xelm =+ xelm {elementName = ((elementName xelm ){nameNamespace =+ Just space })+ , elementNodes = elementNodes xelm <&> \case+ NodeElement x -> NodeElement (setNameSpaceRec space x)+ y -> y+ }++columns :: MonadReader SheetWriteSettings m => ConduitT Row Event m ()+columns = do+ colProps <- view wsColumnProperties+ let cols :: Maybe TXML.Element+ cols = nonEmptyElListSimple (addSmlNamespace "cols") $ map (toElement (addSmlNamespace "col")) colProps+ traverse_ (C.yieldMany . elementToEvents . toXMLElement) cols++writeWorkSheet :: MonadReader SheetWriteSettings m => Map Text Int -> ConduitT Row Event m ()+writeWorkSheet sharedStrings' = doc (addSmlNamespace "worksheet") $ do+ sheetViews+ columns+ el (addSmlNamespace "sheetData") $ C.awaitForever (mapRow sharedStrings')++mapRow :: MonadReader SheetWriteSettings m => Map Text Int -> Row -> ConduitT Row Event m ()+mapRow sharedStrings' sheetItem = do+ mRowProp <- preview $ wsRowProperties . ix (unRowIndex rowIx) . rowHeightLens . _Just . failing _CustomHeight _AutomaticHeight+ let rowAttr :: Attributes+ rowAttr = ixAttr <> fold (attr "ht" . txtd <$> mRowProp)+ tag (addSmlNamespace "row") rowAttr $+ void $ itraverse (mapCell sharedStrings' rowIx) (sheetItem ^. ri_cell_row)+ where+ rowIx = sheetItem ^. ri_row_index+ ixAttr = attr "r" $ toAttrVal rowIx++mapCell ::+ Monad m => Map Text Int -> RowIndex -> Int -> Cell -> ConduitT Row Event m ()+mapCell sharedStrings' rix cix' cell =+ when (has (cellValue . _Just) cell || has (cellStyle . _Just) cell) $+ tag (addSmlNamespace "c") celAttr $+ when (has (cellValue . _Just) cell) $+ el (addSmlNamespace "v") $+ content $ renderCell sharedStrings' cell+ where+ cix = ColumnIndex cix'+ celAttr = attr "r" ref <>+ renderCellType sharedStrings' cell+ <> foldMap (attr "s" . txti) (cell ^. cellStyle)+ ref :: Text+ ref = coerce $ singleCellRef (rix, cix)++renderCellType :: Map Text Int -> Cell -> Attributes+renderCellType sharedStrings' cell =+ maybe mempty+ (attr "t" . renderType sharedStrings')+ $ cell ^? cellValue . _Just++renderCell :: Map Text Int -> Cell -> Text+renderCell sharedStrings' cell = renderValue sharedStrings' val+ where+ val :: CellValue+ val = fromMaybe (CellText mempty) $ cell ^? cellValue . _Just++renderValue :: Map Text Int -> CellValue -> Text+renderValue sharedStrings' = \case+ CellText x ->+ -- if we can't find it in the sst, print the string+ maybe x toAttrVal $ sharedStrings' ^? ix x+ CellDouble x -> toAttrVal x+ CellBool b -> toAttrVal b+ CellRich _ -> error "rich text is not supported yet"+ CellError err -> toAttrVal err+++renderType :: Map Text Int -> CellValue -> Text+renderType sharedStrings' = \case+ CellText x ->+ maybe "str" (const "s") $ sharedStrings' ^? ix x+ CellDouble _ -> "n"+ CellBool _ -> "b"+ CellRich _ -> "r"+ CellError _ -> "e"
− src/Test.hs
@@ -1,104 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--import Codec.Xlsx--import Control.Applicative ((<$>))-import Control.Lens-import qualified Data.ByteString.Lazy as L-import qualified Data.Map as M-import Data.Text (Text,pack)-import System.Time---xEmpty :: Cell-xEmpty = Cell{_cellValue=Nothing, _cellStyle=Just 0}--xText :: Text -> Cell-xText t = Cell{_cellValue=Just $ CellText t, _cellStyle=Just 0}--xDouble :: Double -> Cell-xDouble d = Cell{_cellValue=Just $ CellDouble d, _cellStyle=Just 0}--styles :: Styles-styles = Styles "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\-\<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\" mc:Ignorable=\"x14ac\">\-\ <numFmts count=\"1\">\-\ <numFmt numFmtId=\"164\" formatCode=\"yyyy/mm/dd\"/>\-\ </numFmts>\-\ <fonts count=\"2\" x14ac:knownFonts=\"1\">\-\ <font>\-\ <sz val=\"11\"/>\-\ <color theme=\"1\"/>\-\ <name val=\"Calibri\"/>\-\ <family val=\"2\"/>\-\ <scheme val=\"minor\"/>\-\ </font>\-\ <font>\-\ <b/>\-\ <sz val=\"11\"/>\-\ <color theme=\"1\"/>\-\ <name val=\"Calibri\"/>\-\ <family val=\"2\"/>\-\ <scheme val=\"minor\"/>\-\ </font>\-\ </fonts>\-\ <fills count=\"2\">\-\ <fill>\-\ <patternFill patternType=\"none\"/>\-\ </fill>\-\ <fill>\-\ <patternFill patternType=\"gray125\"/>\-\ </fill>\-\ </fills>\-\ <borders count=\"1\">\-\ <border>\-\ <left/>\-\ <right/>\-\ <top/>\-\ <bottom/>\-\ <diagonal/>\-\ </border>\-\ </borders>\-\ <cellStyleXfs count=\"1\">\-\ <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/>\-\ </cellStyleXfs>\-\ <cellXfs count=\"5\">\-\ <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>\-\ <xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/>\-\ <xf numFmtId=\"164\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\" applyFont=\"1\"/>\-\ <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/>\-\ <xf numFmtId=\"2\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\" applyFont=\"1\"/>\-\ </cellXfs>\-\ <cellStyles count=\"1\">\-\ <cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/>\-\ </cellStyles>\-\ <dxfs count=\"0\"/>\-\ <tableStyles count=\"0\" defaultTableStyle=\"TableStyleMedium2\" defaultPivotStyle=\"PivotStyleLight16\"/>\-\ <extLst>\-\ <ext xmlns:x14=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main\" uri=\"{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}\">\-\ <x14:slicerStyles defaultSlicerStyle=\"SlicerStyleLight1\"/>\-\ </ext>\-\ <ext xmlns:x15=\"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main\" uri=\"{9260A510-F301-46a8-8635-F512D64BE5F5}\">\-\ <x15:timelineStyles defaultTimelineStyle=\"TimeSlicerStyleLight1\"/>\-\ </ext>\-\ </extLst>\-\</styleSheet>"--main :: IO ()-main = do- ct <- getClockTime- L.writeFile "test.xlsx" $ fromXlsx ct $ Xlsx sheets styles- x <- toXlsx <$> L.readFile "test.xlsx"- putStrLn $ "And cell (3,2) value in list 'List' is " ++- show (x ^? xlSheets . ix "List" . wsCells . ix (3,2) . cellValue . _Just)- where- cols = [ColumnsWidth 1 10 15 1]- rowProps = M.fromList [(1, RowProps (Just 50) (Just 3))]- cells = M.fromList [((r, c), v) | r <- [1..10000], (c, v) <- zip [1..] (row r) ]- row r = [ xText $ pack $ "column1-r" ++ show r- , xText $ pack $ "column2-r" ++ show r- , xEmpty- , xText $ pack $ "column4-r" ++ show r- , xDouble 42.12345- , xText "False"]- sheets = M.fromList [("List", Worksheet cols rowProps cells [])] -- wtf merges?
+ test/AutoFilterTests.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module AutoFilterTests+ ( tests+ ) where++import Test.SmallCheck.Series+import Test.Tasty (testGroup, TestTree)+import Test.Tasty.SmallCheck (testProperty)++import Codec.Xlsx+import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++import Common+import Test.SmallCheck.Series.Instances ()++tests :: TestTree+tests =+ testGroup+ "Types.AutFilter tests"+ [ testProperty "fromCursor . toElement == id" $ \(autoFilter :: AutoFilter) ->+ [autoFilter] == fromCursor (cursorFromElement $ toElement (addSmlNamespace "autoFilter") autoFilter)+ ]
+ test/Common.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Common+ ( parseBS+ , cursorFromElement+ ) where+import Data.ByteString.Lazy (ByteString)+import Text.XML+import Text.XML.Cursor++import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Types (SheetState (..))+import Codec.Xlsx.Writer.Internal++import Test.SmallCheck.Series (Serial)++parseBS :: FromCursor a => ByteString -> [a]+parseBS = fromCursor . fromDocument . parseLBS_ def++cursorFromElement :: Element -> Cursor+cursorFromElement = fromNode . NodeElement . addNS mainNamespace Nothing++instance Monad m => Serial m SheetState
+ test/CommonTests.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module CommonTests+ ( tests+ ) where++import Data.Fixed (Pico, Fixed(..), E12)+import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (UTCTime(..))+import Test.Tasty.SmallCheck (testProperty)+import Test.SmallCheck.Series as Series+ ( Positive(..)+ , Serial(..)+ , newtypeCons+ , cons0+ , (\/)+ )+import Test.Tasty (testGroup, TestTree)+import Test.Tasty.HUnit (testCase, (@?=))++import Codec.Xlsx.Types.Common++import qualified CommonTests.CellRefTests as CellRefTests++tests :: TestTree+tests =+ testGroup+ "Types.Common tests"+ [ testCase "date conversions" $ do+ dateFromNumber DateBase1900 (- 2338.0) @?= read "1893-08-06 00:00:00 UTC"+ dateFromNumber DateBase1900 2.0 @?= read "1900-01-02 00:00:00 UTC"+ dateFromNumber DateBase1900 3687.0 @?= read "1910-02-03 00:00:00 UTC"+ dateFromNumber DateBase1900 38749.0 @?= read "2006-02-01 00:00:00 UTC"+ dateFromNumber DateBase1900 2958465.0 @?= read "9999-12-31 00:00:00 UTC"+ dateFromNumber DateBase1900 59.0 @?= read "1900-02-28 00:00:00 UTC"+ dateFromNumber DateBase1900 59.5 @?= read "1900-02-28 12:00:00 UTC"+ dateFromNumber DateBase1900 60.0 @?= read "1900-03-01 00:00:00 UTC"+ dateFromNumber DateBase1900 60.5 @?= read "1900-03-01 00:00:00 UTC"+ dateFromNumber DateBase1900 61 @?= read "1900-03-01 00:00:00 UTC"+ dateFromNumber DateBase1900 61.5 @?= read "1900-03-01 12:00:00 UTC"+ dateFromNumber DateBase1900 62 @?= read "1900-03-02 00:00:00 UTC"+ dateFromNumber DateBase1904 (-3800.0) @?= read "1893-08-05 00:00:00 UTC"+ dateFromNumber DateBase1904 0.0 @?= read "1904-01-01 00:00:00 UTC"+ dateFromNumber DateBase1904 2225.0 @?= read "1910-02-03 00:00:00 UTC"+ dateFromNumber DateBase1904 37287.0 @?= read "2006-02-01 00:00:00 UTC"+ dateFromNumber DateBase1904 2957003.0 @?= read "9999-12-31 00:00:00 UTC"+ , testCase "Converting dates in the vicinity of 1900-03-01 to numbers" $ do+ -- Note how the fact that 1900-02-29 exists for Excel forces us to skip 60+ dateToNumber DateBase1900 (UTCTime (fromGregorian 1900 2 28) 0) @?= (59 :: Double)+ dateToNumber DateBase1900 (UTCTime (fromGregorian 1900 3 1) 0) @?= (61 :: Double)+ , testProperty "dateToNumber . dateFromNumber == id" $+ -- Because excel treats 1900 as a leap year, dateToNumber and dateFromNumber+ -- aren't inverses of each other in the range n E [60, 61[ for DateBase1900+ \b (n :: Pico) -> (n >= 60 && n < 61 && b == DateBase1900) || n == dateToNumber b (dateFromNumber b $ n)+ , CellRefTests.tests+ ]++instance Monad m => Serial m (Fixed E12) where+ series = newtypeCons MkFixed++instance Monad m => Serial m DateBase where+ series = cons0 DateBase1900 \/ cons0 DateBase1904
+ test/CommonTests/CellRefTests.hs view
@@ -0,0 +1,182 @@++{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module CommonTests.CellRefTests+ ( tests+ ) where++import qualified Control.Applicative as Alt+import Control.Monad+import Data.Char (chr, isPrint)+import qualified Data.List as L+import Data.Text (Text)+import qualified Data.Text as T+import Data.Word (Word8)+import Test.SmallCheck.Series as Series (NonEmpty (..), Positive (..),+ Serial (..), cons1, cons2, generate,+ (\/))+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))+import Test.Tasty.SmallCheck (testProperty)++import Codec.Xlsx.Types.Common++tests :: TestTree+tests =+ testGroup+ "Types.Common CellRef tests"+ [ testProperty "textToColumnIndex . columnIndexToText == id" $+ \(Positive i) -> i == textToColumnIndex (columnIndexToText i)++ , testProperty "row2coord . coord2row = id" $+ \(r :: RowCoord) -> r == row2coord (coord2row r)++ , testProperty "col2coord . coord2col = id" $+ \(c :: ColumnCoord) -> c == col2coord (coord2col c)++ , testProperty "fromSingleCellRef' . singleCellRef' = pure" $+ \(cellCoord :: CellCoord) -> pure cellCoord == fromSingleCellRef' (singleCellRef' cellCoord)++ , testProperty "fromRange' . mkRange' = pure" $+ \(range :: RangeCoord) -> pure range == fromRange' (uncurry mkRange' range)++ , testProperty "fromForeignSingleCellRef . mkForeignSingleCellRef = pure" $+ \(viewForeignCellParams -> params) ->+ pure params == fromForeignSingleCellRef (uncurry mkForeignSingleCellRef params)++ , testProperty "fromSingleCellRef' . mkForeignSingleCellRef = pure . snd" $+ \(viewForeignCellParams -> (nStr, cellCoord)) ->+ pure cellCoord == fromSingleCellRef' (mkForeignSingleCellRef nStr cellCoord)++ , testProperty "fromForeignSingleCellRef . singleCellRef' = const empty" $+ \(cellCoord :: CellCoord) ->+ Alt.empty == fromForeignSingleCellRef (singleCellRef' cellCoord)++ , testProperty "fromForeignRange . mkForeignRange = pure" $+ \(viewForeignRangeParams -> params@(nStr, (start, end))) ->+ pure params == fromForeignRange (mkForeignRange nStr start end)++ , testProperty "fromRange' . mkForeignRange = pure . snd" $+ \(viewForeignRangeParams -> (nStr, range@(start, end))) ->+ pure range == fromRange' (mkForeignRange nStr start end)++ , testProperty "fromForeignRange . mkRange' = const empty" $+ \(range :: RangeCoord) ->+ Alt.empty == fromForeignRange (uncurry mkRange' range)++ , testCase "building single CellRefs" $ do+ singleCellRef' (RowRel 5, ColumnRel 25) @?= CellRef "Y5"+ singleCellRef' (RowRel 5, ColumnAbs 25) @?= CellRef "$Y5"+ singleCellRef' (RowAbs 5, ColumnRel 25) @?= CellRef "Y$5"+ singleCellRef' (RowAbs 5, ColumnAbs 25) @?= CellRef "$Y$5"+ singleCellRef (5, 25) @?= CellRef "Y5"+ , testCase "parsing single CellRefs as abstract coordinates" $ do+ fromSingleCellRef (CellRef "Y5") @?= Just (5, 25)+ fromSingleCellRef (CellRef "$Y5") @?= Just (5, 25)+ fromSingleCellRef (CellRef "Y$5") @?= Just (5, 25)+ fromSingleCellRef (CellRef "$Y$5") @?= Just (5, 25)+ , testCase "parsing single CellRefs as potentially absolute coordinates" $ do+ fromSingleCellRef' (CellRef "Y5") @?= Just (RowRel 5, ColumnRel 25)+ fromSingleCellRef' (CellRef "$Y5") @?= Just (RowRel 5, ColumnAbs 25)+ fromSingleCellRef' (CellRef "Y$5") @?= Just (RowAbs 5, ColumnRel 25)+ fromSingleCellRef' (CellRef "$Y$5") @?= Just (RowAbs 5, ColumnAbs 25)+ fromSingleCellRef' (CellRef "$Y$50") @?= Just (RowAbs 50, ColumnAbs 25)+ fromSingleCellRef' (CellRef "$Y$5$0") @?= Nothing+ fromSingleCellRef' (CellRef "Y5:Z10") @?= Nothing+ , testCase "building ranges" $ do+ mkRange (5, 25) (10, 26) @?= CellRef "Y5:Z10"+ mkRange' (RowRel 5, ColumnRel 25) (RowRel 10, ColumnRel 26) @?= CellRef "Y5:Z10"+ mkRange' (RowAbs 5, ColumnAbs 25) (RowAbs 10, ColumnAbs 26) @?= CellRef "$Y$5:$Z$10"+ mkRange' (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "$Y5:Z$10"+ mkForeignRange "myWorksheet" (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "'myWorksheet'!$Y5:Z$10"+ mkForeignRange "my sheet" (RowRel 5, ColumnAbs 25) (RowAbs 10, ColumnRel 26) @?= CellRef "'my sheet'!$Y5:Z$10"+ , testCase "parsing ranges CellRefs as abstract coordinates" $ do+ fromRange (CellRef "Y5:Z10") @?= Just ((5, 25), (10, 26))+ fromRange (CellRef "$Y$5:$Z$10") @?= Just ((5, 25), (10, 26))+ fromRange (CellRef "myWorksheet!$Y5:Z$10") @?= Just ((5, 25), (10, 26))+ , testCase "parsing ranges CellRefs as potentially absolute coordinates" $ do+ fromRange' (CellRef "Y5:Z10") @?= Just ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26))+ fromRange' (CellRef "$Y$5:$Z$10") @?= Just ((RowAbs 5, ColumnAbs 25), (RowAbs 10, ColumnAbs 26))+ fromRange' (CellRef "myWorksheet!$Y5:Z$10") @?= Just ((RowRel 5, ColumnAbs 25), (RowAbs 10, ColumnRel 26))+ fromForeignRange (CellRef "myWorksheet!$Y5:Z$10") @?= Just ("myWorksheet", ((RowRel 5, ColumnAbs 25), (RowAbs 10, ColumnRel 26)))+ fromForeignRange (CellRef "'myWorksheet'!Y5:Z10") @?= Just ("myWorksheet", ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26)))+ fromForeignRange (CellRef "'my sheet'!Y5:Z10") @?= Just ("my sheet", ((RowRel 5, ColumnRel 25), (RowRel 10, ColumnRel 26)))+ fromForeignRange (CellRef "$Y5:Z$10") @?= Nothing+ ]++instance Monad m => Serial m RowIndex where+ series = cons1 (RowIndex . getPositive)++instance Monad m => Serial m ColumnIndex where+ series = cons1 (ColumnIndex . getPositive)++instance Monad m => Serial m RowCoord where+ series = cons1 (RowAbs . getPositive) \/ cons1 (RowRel . getPositive)++instance Monad m => Serial m ColumnCoord where+ series = cons1 (ColumnAbs . getPositive) \/ cons1 (ColumnRel . getPositive)++-- | Allow defining an instance to generate valid foreign range params+data MkForeignRangeRef =+ MkForeignRangeRef NameString RangeCoord+ deriving (Show)++viewForeignRangeParams :: MkForeignRangeRef -> (Text, RangeCoord)+viewForeignRangeParams (MkForeignRangeRef nameStr range) = (nameString nameStr, range)++instance Monad m => Serial m MkForeignRangeRef where+ series = cons2 MkForeignRangeRef++-- | Allow defining an instance to generate valid foreign cellref params+data MkForeignCellRef =+ MkForeignCellRef NameString CellCoord+ deriving (Show)++viewForeignCellParams :: MkForeignCellRef -> (Text, CellCoord)+viewForeignCellParams (MkForeignCellRef nameStr coord) = (nameString nameStr, coord)++instance Monad m => Serial m MkForeignCellRef where+ series = cons2 MkForeignCellRef++-- | Overload an instance for allowed sheet name chars+newtype NameChar =+ NameChar { _unNCh :: Char }+ deriving (Show, Eq)++-- | Instance for Char which broadens the pool to permitted ascii and some wchars+-- as @Serial m Char@ is only @[ 'A' .. 'Z' ]@+instance Monad m => Serial m NameChar where+ series =+ fmap NameChar $+ let wChars = ['é', '€', '愛']+ startSeq = "ab cd'" -- single quote is permitted as long as it's not 1st or last+ authorizedAscii =+ let asciiRange = L.map (chr . fromIntegral) [minBound @Word8 .. maxBound `div` 2]+ forbiddenClass = "[]*:?/\\" :: String+ isAuthorized c = isPrint c && not (c `L.elem` forbiddenClass)+ isPlanned c = isAuthorized c && not (c `L.elem` startSeq)+ in L.filter isPlanned asciiRange+ in generate $ \d -> take (d + 1) $ startSeq ++ wChars ++ authorizedAscii++-- | Allow defining an instance to generate valid sheetnames (non-empty, valid char set, squote rule)+newtype NameString =+ NameString { _unNS :: Series.NonEmpty NameChar }+ deriving (Show)++nameString :: NameString -> Text+nameString = T.pack . L.map _unNCh . getNonEmpty . _unNS++instance Monad m => Serial m NameString where+ series = fmap NameString $+ series @m @(Series.NonEmpty NameChar) >>= \t -> t <$ guard (isOk t)+ where+ -- squote isn't permitted at the beginning and the end of a sheet's name+ isOk (getNonEmpty -> s) =+ head s /= NameChar '\'' &&+ last s /= NameChar '\''
+ test/CondFmtTests.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module CondFmtTests+ ( tests+ ) where++import Test.Tasty (testGroup, TestTree)+import Test.Tasty.SmallCheck (testProperty)++import Codec.Xlsx+import Codec.Xlsx.Parser.Internal+import Codec.Xlsx.Writer.Internal++import Common+import Test.SmallCheck.Series.Instances ()++tests :: TestTree+tests =+ testGroup+ "Types.ConditionalFormatting tests"+ [ testProperty "fromCursor . toElement == id" $ \(cFmt :: CfRule) ->+ [cFmt] == fromCursor (cursorFromElement $ toElement (addSmlNamespace "cfRule") cFmt)+ ]
− test/DataTest.hs
@@ -1,68 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Main (main) where--import qualified Data.IntMap as IM-import Data.Map (Map)-import qualified Data.Map as M-import Data.Time.Calendar-import Data.Time.LocalTime-import System.Time-import Text.XML-import Text.XML.Cursor--import Test.Tasty (defaultMain, testGroup)-import Test.Tasty.SmallCheck (testProperty)-import Test.Tasty.HUnit (testCase)--import Test.HUnit ((@=?))-import Test.SmallCheck.Series (Positive(..))--import Codec.Xlsx-import Codec.Xlsx.Parser.Internal---main = defaultMain $- testGroup "Tests"- [ testProperty "col2int . int2col == id" $- \(Positive i) -> i == col2int (int2col i)- , testCase "write . read == id" $- testXlsx @=? toXlsx (fromXlsx testTime testXlsx)- , testCase "fromRows . toRows == id" $- testCellMap @=? fromRows (toRows testCellMap)- , testCase "correct shared strings parsing" $- testSharedStrings @=? testParseSharedStrings- ]--testXlsx :: Xlsx-testXlsx = Xlsx sheets emptyStyles- where- sheets = M.fromList [( "List1", sheet )]- sheet = Worksheet cols rowProps testCellMap []- rowProps = M.fromList [(1, RowProps (Just 50) (Just 3))]- cols = [ColumnsWidth 1 10 15 1]--testCellMap :: CellMap-testCellMap = M.fromList [ ((1, 2), cd1), ((1, 5), cd2)- , ((3, 1), cd3), ((3, 2), cd4), ((3, 7), cd5)- ]- where- cd v = Cell{_cellValue=Just v, _cellStyle=Nothing}- cd1 = cd (CellText "just a text")- cd2 = cd (CellDouble 42.4567)- cd3 = cd (CellText "another text")- cd4 = Cell{_cellValue=Nothing, _cellStyle=Nothing} -- shouldn't it be skipped?- cd5 = cd $(CellBool True)--testTime :: ClockTime-testTime = TOD 123 567--testSharedStrings = IM.fromAscList $ zip [0..] ["plain text", "Just example"]--testParseSharedStrings = parseSharedStrings $ fromDocument $ parseLBS_ def strings- where- strings = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\- \<sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"2\" uniqueCount=\"2\">\- \<si><t>plain text</t></si>\- \<si><r><t>Just </t></r><r><rPr><b val=\"true\"/><u val=\"single\"/>\- \<sz val=\"10\"/><rFont val=\"Arial\"/><family val=\"2\"/></rPr><t>example</t></r></si>\- \</sst>"
+ test/Diff.hs view
@@ -0,0 +1,14 @@+module Diff where++import Data.Algorithm.Diff (Diff (..), getGroupedDiff)+import Data.Algorithm.DiffOutput (ppDiff)+import Data.Monoid ((<>))+import Test.Tasty.HUnit (Assertion, HasCallStack, assertBool)+import Text.Groom (groom)++-- | Like '@=?' but producing a diff on failure.+(@==?) :: HasCallStack => (Eq a, Show a) => a -> a -> Assertion+x @==? y =+ assertBool ("Expected:\n" <> groom x <> "\nDifference:\n" <> msg) (x == y)+ where+ msg = ppDiff $ getGroupedDiff (lines . groom $ x) (lines . groom $ y)
+ test/DrawingTests.hs view
@@ -0,0 +1,398 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+module DrawingTests+ ( tests+ , testDrawing+ , testLineChartSpace+ ) where++#ifdef USE_MICROLENS+import Lens.Micro+#else+import Control.Lens+#endif+import Data.ByteString.Lazy (ByteString)+import Test.Tasty (testGroup, TestTree)+import Test.Tasty.HUnit (testCase)+import Text.RawString.QQ+import Text.XML++import Codec.Xlsx+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Writer.Internal++import Common+import Diff++tests :: TestTree+tests =+ testGroup+ "Drawing tests"+ [ testCase "correct drawing parsing" $+ [testDrawing] @==? parseBS testDrawingFile+ , testCase "write . read == id for Drawings" $+ [testDrawing] @==? parseBS testWrittenDrawing+ , testCase "correct chart parsing" $+ [testLineChartSpace] @==? parseBS testLineChartFile+ , testCase "parse . render == id for line Charts" $+ [testLineChartSpace] @==? parseBS (renderChartSpace testLineChartSpace)+ , testCase "parse . render == id for area Charts" $+ [testAreaChartSpace] @==? parseBS (renderChartSpace testAreaChartSpace)+ , testCase "parse . render == id for bar Charts" $+ [testBarChartSpace] @==? parseBS (renderChartSpace testBarChartSpace)+ , testCase "parse . render == id for pie Charts" $+ [testPieChartSpace] @==? parseBS (renderChartSpace testPieChartSpace)+ , testCase "parse . render == id for scatter Charts" $+ [testScatterChartSpace] @==? parseBS (renderChartSpace testScatterChartSpace)+ ]++testDrawing :: UnresolvedDrawing+testDrawing = Drawing [anchor1, anchor2]+ where+ anchor1 =+ Anchor+ {_anchAnchoring = anchoring1, _anchObject = pic, _anchClientData = def}+ anchoring1 =+ TwoCellAnchor+ { tcaFrom = unqMarker (0, 0) (0, 0)+ , tcaTo = unqMarker (12, 320760) (33, 38160)+ , tcaEditAs = EditAsAbsolute+ }+ pic =+ Picture+ { _picMacro = Nothing+ , _picPublished = False+ , _picNonVisual = nonVis1+ , _picBlipFill = bfProps+ , _picShapeProperties = shProps+ }+ nonVis1 =+ PicNonVisual $+ NonVisualDrawingProperties+ { _nvdpId = DrawingElementId 0+ , _nvdpName = "Picture 1"+ , _nvdpDescription = Just ""+ , _nvdpHidden = False+ , _nvdpTitle = Nothing+ }+ bfProps =+ BlipFillProperties+ {_bfpImageInfo = Just (RefId "rId1"), _bfpFillMode = Just FillStretch}+ shProps =+ ShapeProperties+ { _spXfrm = Just trnsfrm+ , _spGeometry = Just PresetGeometry+ , _spFill = Nothing+ , _spOutline = Just $ def {_lnFill = Just NoFill}+ }+ trnsfrm =+ Transform2D+ { _trRot = Angle 0+ , _trFlipH = False+ , _trFlipV = False+ , _trOffset = Just (unqPoint2D 0 0)+ , _trExtents =+ Just+ (PositiveSize2D+ (PositiveCoordinate 10074240)+ (PositiveCoordinate 5402520))+ }+ anchor2 =+ Anchor+ { _anchAnchoring = anchoring2+ , _anchObject = graphic+ , _anchClientData = def+ }+ anchoring2 =+ TwoCellAnchor+ { tcaFrom = unqMarker (0, 87840) (21, 131040)+ , tcaTo = unqMarker (7, 580320) (38, 132480)+ , tcaEditAs = EditAsOneCell+ }+ graphic =+ Graphic+ { _grNonVisual = nonVis2+ , _grChartSpace = RefId "rId2"+ , _grTransform = transform+ }+ nonVis2 =+ GraphNonVisual $+ NonVisualDrawingProperties+ { _nvdpId = DrawingElementId 1+ , _nvdpName = ""+ , _nvdpDescription = Nothing+ , _nvdpHidden = False+ , _nvdpTitle = Nothing+ }+ transform =+ Transform2D+ { _trRot = Angle 0+ , _trFlipH = False+ , _trFlipV = False+ , _trOffset = Just (unqPoint2D 0 0)+ , _trExtents =+ Just+ (PositiveSize2D+ (PositiveCoordinate 10074240)+ (PositiveCoordinate 5402520))+ }++testDrawingFile :: ByteString+testDrawingFile = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">+ <xdr:twoCellAnchor editAs="absolute">+ <xdr:from>+ <xdr:col>0</xdr:col><xdr:colOff>0</xdr:colOff>+ <xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff>+ </xdr:from>+ <xdr:to>+ <xdr:col>12</xdr:col><xdr:colOff>320760</xdr:colOff>+ <xdr:row>33</xdr:row><xdr:rowOff>38160</xdr:rowOff>+ </xdr:to>+ <xdr:pic>+ <xdr:nvPicPr><xdr:cNvPr id="0" name="Picture 1" descr=""/><xdr:cNvPicPr/></xdr:nvPicPr>+ <xdr:blipFill><a:blip r:embed="rId1"></a:blip><a:stretch/></xdr:blipFill>+ <xdr:spPr>+ <a:xfrm><a:off x="0" y="0"/><a:ext cx="10074240" cy="5402520"/></a:xfrm>+ <a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:ln><a:noFill/></a:ln>+ </xdr:spPr>+ </xdr:pic>+ <xdr:clientData/>+ </xdr:twoCellAnchor>+ <xdr:twoCellAnchor editAs="oneCell">+ <xdr:from>+ <xdr:col>0</xdr:col><xdr:colOff>87840</xdr:colOff>+ <xdr:row>21</xdr:row><xdr:rowOff>131040</xdr:rowOff>+ </xdr:from>+ <xdr:to>+ <xdr:col>7</xdr:col><xdr:colOff>580320</xdr:colOff>+ <xdr:row>38</xdr:row><xdr:rowOff>132480</xdr:rowOff>+ </xdr:to>+ <xdr:graphicFrame>+ <xdr:nvGraphicFramePr><xdr:cNvPr id="1" name=""/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr>+ <xdr:xfrm><a:off x="0" y="0"/><a:ext cx="10074240" cy="5402520"/></xdr:xfrm>+ <a:graphic>+ <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">+ <c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" r:id="rId2"/>+ </a:graphicData>+ </a:graphic>+ </xdr:graphicFrame>+ <xdr:clientData/>+ </xdr:twoCellAnchor>+</xdr:wsDr>+|]++testWrittenDrawing :: ByteString+testWrittenDrawing = renderLBS def $ toDocument testDrawing++testLineChartFile :: ByteString+testLineChartFile = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">+<c:chart>+<c:title>+ <c:tx><c:rich><a:bodyPr rot="0" anchor="b"/><a:p><a:r><a:t>Line chart title</a:t></a:r></a:p></c:rich></c:tx>+</c:title>+<c:plotArea>+<c:lineChart>+<c:grouping val="standard"/>+<c:ser>+ <c:idx val="0"/><c:order val="0"/>+ <c:tx><c:strRef><c:f>Sheet1!$A$1</c:f></c:strRef></c:tx>+ <c:spPr>+ <a:solidFill><a:srgbClr val="0000FF"/></a:solidFill>+ <a:ln w="28800"><a:solidFill><a:srgbClr val="0000FF"/></a:solidFill></a:ln>+ </c:spPr>+ <c:marker><c:symbol val="none"/></c:marker>+ <c:val><c:numRef><c:f>Sheet1!$B$1:$D$1</c:f></c:numRef></c:val>+ <c:smooth val="0"/>+</c:ser>+<c:ser>+ <c:idx val="1"/><c:order val="1"/>+ <c:tx><c:strRef><c:f>Sheet1!$A$2</c:f></c:strRef></c:tx>+ <c:spPr>+ <a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>+ <a:ln w="28800"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:ln>+ </c:spPr>+ <c:marker><c:symbol val="none"/></c:marker>+ <c:val><c:numRef><c:f>Sheet1!$B$2:$D$2</c:f></c:numRef></c:val>+ <c:smooth val="0"/>+</c:ser>+<c:marker val="0"/>+<c:smooth val="0"/>+</c:lineChart>+</c:plotArea>+<c:plotVisOnly val="1"/>+<c:dispBlanksAs val="gap"/>+</c:chart>+</c:chartSpace>+|]++oneChartChartSpace :: Chart -> ChartSpace+oneChartChartSpace chart =+ ChartSpace+ { _chspTitle = Just $ ChartTitle (Just titleBody)+ , _chspCharts = [chart]+ , _chspLegend = Nothing+ , _chspPlotVisOnly = Just True+ , _chspDispBlanksAs = Just DispBlanksAsGap+ }+ where+ titleBody =+ TextBody+ { _txbdRotation = Angle 0+ , _txbdSpcFirstLastPara = False+ , _txbdVertOverflow = TextVertOverflow+ , _txbdVertical = TextVerticalHorz+ , _txbdWrap = TextWrapSquare+ , _txbdAnchor = TextAnchoringBottom+ , _txbdAnchorCenter = False+ , _txbdParagraphs =+ [TextParagraph Nothing [RegularRun Nothing "Line chart title"]]+ }++renderChartSpace :: ChartSpace -> ByteString+renderChartSpace = renderLBS def {rsNamespaces = nss} . toDocument+ where+ nss =+ [ ("c", "http://schemas.openxmlformats.org/drawingml/2006/chart")+ , ("a", "http://schemas.openxmlformats.org/drawingml/2006/main")+ ]++testLineChartSpace :: ChartSpace+testLineChartSpace = oneChartChartSpace lineChart+ where+ lineChart =+ LineChart+ { _lnchGrouping = StandardGrouping+ , _lnchSeries = series+ , _lnchMarker = Just False+ , _lnchSmooth = Just False+ }+ series =+ [ LineSeries+ { _lnserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$1"+ , _serShapeProperties = Just $ rgbShape "0000FF"+ }+ , _lnserMarker = Just markerNone+ , _lnserDataLblProps = Nothing+ , _lnserVal = Just $ Formula "Sheet1!$B$1:$D$1"+ , _lnserSmooth = Just False+ }+ , LineSeries+ { _lnserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$2"+ , _serShapeProperties = Just $ rgbShape "FF0000"+ }+ , _lnserMarker = Just markerNone+ , _lnserDataLblProps = Nothing+ , _lnserVal = Just $ Formula "Sheet1!$B$2:$D$2"+ , _lnserSmooth = Just False+ }+ ]+ rgbShape color =+ def+ { _spFill = Just $ solidRgb color+ , _spOutline =+ Just $+ LineProperties {_lnFill = Just $ solidRgb color, _lnWidth = 28800}+ }+ markerNone =+ DataMarker {_dmrkSymbol = Just DataMarkerNone, _dmrkSize = Nothing}++testAreaChartSpace :: ChartSpace+testAreaChartSpace = oneChartChartSpace areaChart+ where+ areaChart =+ AreaChart {_archGrouping = Just StandardGrouping, _archSeries = series}+ series =+ [ AreaSeries+ { _arserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$1"+ , _serShapeProperties =+ Just $+ def+ { _spFill = Just $ solidRgb "000088"+ , _spOutline = Just $ def {_lnFill = Just NoFill}+ }+ }+ , _arserDataLblProps = Nothing+ , _arserVal = Just $ Formula "Sheet1!$B$1:$D$1"+ }+ ]++testBarChartSpace :: ChartSpace+testBarChartSpace =+ oneChartChartSpace+ BarChart+ { _brchDirection = DirectionColumn+ , _brchGrouping = Just BarStandardGrouping+ , _brchSeries =+ [ BarSeries+ { _brserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$1"+ , _serShapeProperties =+ Just $+ def+ { _spFill = Just $ solidRgb "000088"+ , _spOutline = Just $ def {_lnFill = Just NoFill}+ }+ }+ , _brserDataLblProps = Nothing+ , _brserVal = Just $ Formula "Sheet1!$B$1:$D$1"+ }+ ]+ }++testPieChartSpace :: ChartSpace+testPieChartSpace =+ oneChartChartSpace+ PieChart+ { _pichSeries =+ [ PieSeries+ { _piserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$1"+ , _serShapeProperties = Nothing+ }+ , _piserDataPoints =+ [ def & dpShapeProperties ?~ solidFill "000088"+ , def & dpShapeProperties ?~ solidFill "008800"+ , def & dpShapeProperties ?~ solidFill "880000"+ ]+ , _piserDataLblProps = Nothing+ , _piserVal = Just $ Formula "Sheet1!$B$1:$D$1"+ }+ ]+ }+ where+ solidFill color = def & spFill ?~ solidRgb color++testScatterChartSpace :: ChartSpace+testScatterChartSpace =+ oneChartChartSpace+ ScatterChart+ { _scchStyle = ScatterMarker+ , _scchSeries =+ [ ScatterSeries+ { _scserShared =+ Series+ { _serTx = Just $ Formula "Sheet1!$A$2"+ , _serShapeProperties =+ Just $ def {_spOutline = Just $ def {_lnFill = Just NoFill}}+ }+ , _scserMarker = Just $ DataMarker (Just DataMarkerSquare) Nothing+ , _scserDataLblProps = Nothing+ , _scserXVal = Just $ Formula "Sheet1!$B$1:$D$1"+ , _scserYVal = Just $ Formula "Sheet1!$B$2:$D$2"+ , _scserSmooth = Nothing+ }+ ]+ }
+ test/Main.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+module Main+ ( main+ ) where++#ifdef USE_MICROLENS+import Lens.Micro+#else+import Control.Lens+#endif+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as LB+import qualified Data.Map as M+import Data.Maybe (listToMaybe)+import Data.Text (Text)+import qualified StreamTests+import Text.XML++import Test.Tasty (defaultMain, testGroup)+import Test.Tasty.HUnit (testCase, (@=?))+import Test.Tasty.SmallCheck (testProperty)++import TestXlsx++import Codec.Xlsx+import Codec.Xlsx.Formatted++import AutoFilterTests+import Common+import CommonTests+import CondFmtTests+import Diff+import DrawingTests+import PivotTableTests++main :: IO ()+main = defaultMain $+ testGroup "Tests"+ [+ testCase "write . read == id" $ do+ let bs = fromXlsx testTime testXlsx+ LB.writeFile "data-test.xlsx" bs+ testXlsx @==? toXlsx (fromXlsx testTime testXlsx)+ , testCase "write . fast-read == id" $ do+ let bs = fromXlsx testTime testXlsx+ LB.writeFile "data-test.xlsx" bs+ testXlsx @==? toXlsxFast (fromXlsx testTime testXlsx)+ , testCase "fromRows . toRows == id" $+ testCellMap1 @=? fromRows (toRows testCellMap1)+ , testCase "fromRight . parseStyleSheet . renderStyleSheet == id" $+ testStyleSheet @==? fromRight (parseStyleSheet (renderStyleSheet testStyleSheet))+ , testCase "correct shared strings parsing" $+ [testSharedStringTable] @=? parseBS testStrings+ , testCase "correct shared strings parsing: single underline" $+ [withSingleUnderline testSharedStringTable] @=? parseBS testStringsWithSingleUnderline+ , testCase "correct shared strings parsing: double underline" $+ [withDoubleUnderline testSharedStringTable] @=? parseBS testStringsWithDoubleUnderline+ , testCase "correct shared strings parsing even when one of the shared strings entry is just <t/>" $+ [testSharedStringTableWithEmpty] @=? parseBS testStringsWithEmpty+ , testCase "correct comments parsing" $+ [testCommentTable] @=? parseBS testComments+ , testCase "correct custom properties parsing" $+ [testCustomProperties] @==? parseBS testCustomPropertiesXml+ , testCase "proper results from `formatted`" $+ testFormattedResult @==? testRunFormatted+ , testCase "proper results from `formatWorkbook`" $+ testFormatWorkbookResult @==? testFormatWorkbook+ , testCase "formatted . toFormattedCells = id" $ do+ let fmtd = formatted testFormattedCells minimalStyleSheet+ testFormattedCells @==? toFormattedCells (formattedCellMap fmtd) (formattedMerges fmtd)+ (formattedStyleSheet fmtd)+ , testCase "proper results from `conditionallyFormatted`" $+ testCondFormattedResult @==? testRunCondFormatted+ , testCase "toXlsxEither: properly formatted" $+ Right testXlsx @==? toXlsxEither (fromXlsx testTime testXlsx)+ , testCase "toXlsxEither: invalid format" $+ Left (InvalidZipArchive "Did not find end of central directory signature") @==? toXlsxEither "this is not a valid XLSX file"+ , testCase "toXlsx: correct floats parsing (typed and untyped cells are floats by default)"+ $ floatsParsingTests toXlsx+ , testCase "toXlsxFast: correct floats parsing (typed and untyped cells are floats by default)"+ $ floatsParsingTests toXlsxFast+ , testCase "toXlsx: correct resolving of absolute relationship targets"+ $ absoluteRelationshipsTest toXlsx+ , testCase "toXlsxFast: correct resolving of absolute relationship targets"+ $ absoluteRelationshipsTest toXlsxFast+ , testGroup "Codec: sheet state visibility"+ [ testGroup "toXlsxEitherFast"+ [ testProperty "pure state == toXlsxEitherFast (fromXlsx (defXlsxWithState state))" $+ \state ->+ (Right (Just state) ==) $+ fmap sheetStateOfDefXlsx $+ toXlsxEitherFast . fromXlsx testTime $+ defXlsxWithState state+ , testCase "should otherwise infer visible state by default" $+ Right (Just Visible) @=? (fmap sheetStateOfDefXlsx . toXlsxEitherFast) (fromXlsx testTime defXlsx)+ ]+ , testGroup "toXlsxEither"+ [ testProperty "pure state == toXlsxEither (fromXlsx (defXlsxWithState state))" $+ \state ->+ (Right (Just state) ==) $+ fmap sheetStateOfDefXlsx $+ toXlsxEither . fromXlsx testTime $+ defXlsxWithState state+ , testCase "should otherwise infer visible state by default" $+ Right (Just Visible) @=? (fmap sheetStateOfDefXlsx . toXlsxEither) (fromXlsx testTime defXlsx)+ ]+ ]+ , CommonTests.tests+ , CondFmtTests.tests+ , PivotTableTests.tests+ , DrawingTests.tests+ , AutoFilterTests.tests+ , StreamTests.tests+ ]++floatsParsingTests :: (ByteString -> Xlsx) -> IO ()+floatsParsingTests parser = do+ bs <- LB.readFile "data/floats.xlsx"+ let xlsx = parser bs+ parsedCells = maybe mempty (_wsCells . snd) $ listToMaybe $ xlsx ^. xlSheets+ expectedCells = M.fromList+ [ ((1,1), def & cellValue ?~ CellDouble 12.0)+ , ((2,1), def & cellValue ?~ CellDouble 13.0)+ , ((3,1), def & cellValue ?~ CellDouble 14.0 & cellStyle ?~ 1)+ , ((4,1), def & cellValue ?~ CellDouble 15.0)+ ]+ expectedCells @==? parsedCells++-- Test whether absolute logical item names are correctly mapped to ZIP item names.+-- absolute_relationships.xlsx contains relationships of worksheet, comment,+-- drawing and pivot table types with absolute paths in their targets.+absoluteRelationshipsTest :: (ByteString -> Xlsx) -> IO ()+absoluteRelationshipsTest parser = do+ bs <- LB.readFile "data/absolute_relationships.xlsx"+ let xlsx = parser bs+ expectedComment = Just $ Comment (XlsxText "test comment") "author" False+ parsedComment = xlsx ^? ixSheet "Test" . ixCell (2, 1) . cellComment . _Just+ expectedComment @==? parsedComment++constSheetName :: Text+constSheetName = "sheet1"++defXlsx :: Xlsx+defXlsx = def & atSheet constSheetName ?~ def++defXlsxWithState :: SheetState -> Xlsx+defXlsxWithState state =+ def & atSheet constSheetName ?~ (wsState .~ state $ def)++sheetStateOfDefXlsx :: Xlsx -> Maybe SheetState+sheetStateOfDefXlsx xlsx =+ xlsx ^. atSheet constSheetName & mapped %~ _wsState
+ test/PivotTableTests.hs view
@@ -0,0 +1,197 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+module PivotTableTests+ ( tests+ , testPivotTable+ , testPivotSrcCells+ ) where++#ifdef USE_MICROLENS+import Lens.Micro+#else+import Control.Lens+#endif+import Data.ByteString.Lazy (ByteString)+import qualified Data.Map as M+import Data.Maybe (mapMaybe)+import qualified Data.Text as T+import Test.Tasty (testGroup, TestTree)+import Test.Tasty.HUnit (testCase)+import Text.RawString.QQ+import Text.XML++import Codec.Xlsx+import Codec.Xlsx.Parser.Internal.PivotTable+import Codec.Xlsx.Types.Internal (unsafeRefId)+import Codec.Xlsx.Types.PivotTable.Internal+import Codec.Xlsx.Writer.Internal.PivotTable++import Diff++tests :: TestTree+tests =+ testGroup+ "Pivot table tests"+ [ testCase "proper pivot table rendering" $ do+ let ptFiles = renderPivotTableFiles testPivotSrcCells 3 testPivotTable+ parseLBS_ def (pvtfTable ptFiles) @==?+ stripContentSpaces (parseLBS_ def testPivotTableDefinition)+ parseLBS_ def (pvtfCacheDefinition ptFiles) @==?+ stripContentSpaces (parseLBS_ def testPivotCacheDefinition)+ , testCase "proper pivot table parsing" $ do+ let sheetName = "Sheet1"+ ref = CellRef "A1:D5"+ forCacheId (CacheId 3) = Just (sheetName, ref, testPivotCacheFields)+ forCacheId _ = Nothing+ -- fields with numeric values go into cache records+ testPivotCacheFields' =+ [ if cfName cf == PivotFieldName "Color"+ then cf+ else cf {cfItems = []}+ | cf <- testPivotCacheFields+ ]+ Just (sheetName, ref, testPivotCacheFields', Just (unsafeRefId 1)) @==?+ parseCache testPivotCacheDefinition+ Just testPivotTable @==?+ parsePivotTable forCacheId testPivotTableDefinition+ ]++testPivotTable :: PivotTable+testPivotTable =+ PivotTable+ { _pvtName = "PivotTable1"+ , _pvtDataCaption = "Values"+ , _pvtLocation = CellRef "A3:D12"+ , _pvtSrcRef = CellRef "A1:D5"+ , _pvtSrcSheet = "Sheet1"+ , _pvtRowFields = [FieldPosition colorField, DataPosition]+ , _pvtColumnFields = [FieldPosition yearField]+ , _pvtDataFields =+ [ DataField+ { _dfName = "Sum of field Price"+ , _dfField = priceField+ , _dfFunction = ConsolidateSum+ }+ , DataField+ { _dfName = "Sum of field Count"+ , _dfField = countField+ , _dfFunction = ConsolidateSum+ }+ ]+ , _pvtFields =+ [ PivotFieldInfo (Just $ colorField) False FieldSortAscending [CellText "green"]+ , PivotFieldInfo (Just $ yearField) True FieldSortManual []+ , PivotFieldInfo (Just $ priceField) False FieldSortManual []+ , PivotFieldInfo (Just $ countField) False FieldSortManual []+ ]+ , _pvtRowGrandTotals = True+ , _pvtColumnGrandTotals = False+ , _pvtOutline = False+ , _pvtOutlineData = False+ }+ where+ colorField = PivotFieldName "Color"+ yearField = PivotFieldName "Year"+ priceField = PivotFieldName "Price"+ countField = PivotFieldName "Count"++testPivotSrcCells :: CellMap+testPivotSrcCells =+ M.fromList $+ concat+ [ [((row, col), def & cellValue ?~ v) | (col, v) <- zip [1 ..] cells]+ | (row, cells) <- zip [1 ..] cellMap+ ]+ where+ cellMap =+ [ [CellText "Color", CellText "Year", CellText "Price", CellText "Count"]+ , [CellText "green", CellDouble 2012, CellDouble 12.23, CellDouble 17]+ , [CellText "white", CellDouble 2011, CellDouble 73.99, CellDouble 21]+ , [CellText "red", CellDouble 2012, CellDouble 10.19, CellDouble 172]+ , [CellText "white", CellDouble 2012, CellDouble 34.99, CellDouble 49]+ ]++testPivotCacheFields :: [CacheField]+testPivotCacheFields =+ [ CacheField+ (PivotFieldName "Color")+ [CellText "green", CellText "white", CellText "red"]+ , CacheField (PivotFieldName "Year") [CellDouble 2012, CellDouble 2011]+ , CacheField+ (PivotFieldName "Price")+ [CellDouble 12.23, CellDouble 73.99, CellDouble 10.19, CellDouble 34.99]+ , CacheField+ (PivotFieldName "Count")+ [CellDouble 17, CellDouble 21, CellDouble 172, CellDouble 49]+ ]++testPivotTableDefinition :: ByteString+testPivotTableDefinition = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?><!--Pivot table generated by xlsx-->+<pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" name="PivotTable1" cacheId="3" dataOnRows="1" colGrandTotals="0" dataCaption="Values">+ <location ref="A3:D12" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>+ <pivotFields>+ <pivotField name="Color" axis="axisRow" showAll="0" outline="0" sortType="ascending">+ <items>+ <item h="1" x="0"/><item x="1"/><item x="2"/><item t="default"/>+ </items>+ </pivotField>+ <pivotField name="Year" axis="axisCol" showAll="0" outline="1">+ <items>+ <item x="0"/><item x="1"/><item t="default"/>+ </items>+ </pivotField>+ <pivotField name="Price" dataField="1" showAll="0" outline="0"/>+ <pivotField name="Count" dataField="1" showAll="0" outline="0"/>+ </pivotFields>+ <rowFields><field x="0"/><field x="-2"/></rowFields>+ <colFields><field x="1"/></colFields>+ <dataFields>+ <dataField name="Sum of field Price" fld="2"/>+ <dataField name="Sum of field Count" fld="3"/>+ </dataFields>+</pivotTableDefinition>+|]++testPivotCacheDefinition :: ByteString+testPivotCacheDefinition = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?><!--Pivot cache definition generated by xlsx-->+<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="rId1"+ invalid="1" refreshOnLoad="1"+ xmlns:ns="http://schemas.openxmlformats.org/officeDocument/2006/relationships">+ <cacheSource type="worksheet">+ <worksheetSource ref="A1:D5" sheet="Sheet1"/>+ </cacheSource>+ <cacheFields>+ <cacheField name="Color">+ <sharedItems>+ <s v="green"/><s v="white"/><s v="red"/>+ </sharedItems>+ </cacheField>+ <cacheField name="Year">+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/>+ </cacheField>+ <cacheField name="Price">+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/>+ </cacheField>+ <cacheField name="Count">+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/>+ </cacheField>+</cacheFields>+</pivotCacheDefinition>+|]++stripContentSpaces :: Document -> Document+stripContentSpaces doc@Document {documentRoot = root} =+ doc {documentRoot = go root}+ where+ go e@Element {elementNodes = nodes} =+ e {elementNodes = mapMaybe goNode nodes}+ goNode (NodeElement el) = Just $ NodeElement (go el)+ goNode t@(NodeContent txt) =+ if T.strip txt == T.empty+ then Nothing+ else Just t+ goNode other = Just $ other
+ test/StreamTests.hs view
@@ -0,0 +1,360 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE CPP #-}++module StreamTests+ ( tests+ ) where++#ifdef USE_MICROLENS++import Test.Tasty (TestName, TestTree, testGroup)+tests :: TestTree+tests = testGroup+ "I stubbed out the tests module for microlens \+ because it doesn't understand setOf. \+ Volunteers are welcome to fix this!"+ []+#else++import Control.Exception+import Codec.Archive.Zip as Zip+import Codec.Xlsx+import Codec.Xlsx.Parser.Stream+import Conduit ((.|))+import qualified Conduit as C+import Control.Lens hiding (indexed)+import Control.Monad (void)+import Data.Set.Lens+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString as BS+import Data.Map (Map)+import qualified Data.Conduit.Combinators as C+import qualified Data.Map as M+import qualified Data.IntMap.Strict as IM+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Vector as V+import Diff+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase)+import TestXlsx+import qualified Codec.Xlsx.Writer.Stream as SW+import qualified Codec.Xlsx.Writer.Internal.Stream as SW+import Control.Monad (void)+import Control.Monad.State.Lazy+import Test.Tasty.SmallCheck+import Test.SmallCheck.Series.Instances ()+import qualified Data.Set as Set+import Data.Set (Set)+import Text.Printf+import Data.Conduit+import Control.Arrow ((&&&))+import Data.Maybe (fromJust)++tshow :: Show a => a -> Text+tshow = Text.pack . show++toBs :: Xlsx -> BS.ByteString+toBs = LB.toStrict . fromXlsx testTime++tests :: TestTree+tests =+ testGroup "Stream tests"+ [+ testGroup "Writer/shared strings"+ [ testProperty "Input same as the output" sharedStringInputSameAsOutput+ , testProperty "Set of input texts is same as map length" sharedStringInputTextsIsSameAsMapLength+ , testProperty "Set of input texts is as value set length" sharedStringInputTextsIsSameAsValueSetLength+ ],++ testGroup "Reader/shared strings"+ [ testCase "Can parse RichText values" richCellTextIsParsed+ ],+++ testGroup "Reader/Writer"+ [ testCase "Write as stream, see if memory based implementation can read it" $ readWrite simpleWorkbook+ , testCase "Write as stream, see if memory based implementation can read it" $ readWrite simpleWorkbookRow+ , testCase "Test a small workbook which has a fullblown square" $ readWrite smallWorkbook+ , testCase "Test a big workbook as a full square which caused issues with zipstream \+ The buffer of zipstream maybe 1kb, this workbook is big enough \+ to be more than that. \+ So if this encodes/decodes we know we can handle those sizes. \+ In some older version the bytestring got cut off resulting in a corrupt xlsx file"+ $ readWrite bigWorkbook+ , testCase "Test a workbook containing multiple sheets"+ $ readWriteMultipleSheets multipleSheetsWorkbook+ -- , testCase "Write as stream, see if memory based implementation can read it" $ readWrite testXlsx+ -- TODO forall SheetItem write that can be read+ ],++ testGroup "Reader/inline strings"+ [ testCase "Can parse row with inline strings" inlineStringsAreParsed+ ],++ testGroup "Reader/floats parsing"+ [ testCase "Can parse untyped values as floats" untypedCellsAreParsedAsFloats+ ]+ ]++readWrite :: Xlsx -> IO ()+readWrite input = do+ BS.writeFile "testinput.xlsx" (toBs input)+ items <- runXlsxM "testinput.xlsx" $ collectItemsIdentifier . fromJust =<< makeIdentifierFromName "Sheet1"+ bs <- runConduitRes $ void (SW.writeXlsx SW.defaultSettings $ C.yieldMany items) .| C.foldC+ case toXlsxEither $ LB.fromStrict bs of+ Right result ->+ input @==? result+ Left x -> do+ throwIO x++readWriteMultipleSheets :: Xlsx -> IO ()+readWriteMultipleSheets input = do+ BS.writeFile "testinput.xlsx" $ toBs input+ sheetsAndConduits <- runXlsxM "testinput.xlsx" $ do+ sheets <- reverse . _wiSheets <$> getWorkbookInfo+ let sheetCount = length sheets+ let sheetNamesAndStates = map (sheetInfoName &&& sheetInfoState) sheets+ sheetConduits <- map C.yieldMany+ <$> mapM (collectItemsIdentifier . getSheetIdentifier) sheets+ pure $ zip sheetNamesAndStates sheetConduits++ bs <- runConduitRes $ void (SW.writeXlsxWithConfig $ SW.defWriteXlsxConfig & SW.setNameInfosAndConduits sheetsAndConduits) .| C.foldC+ case toXlsxEither $ LB.fromStrict bs of+ Right result -> input @==? result+ Left x -> throwIO x++-- test if the input text is also the result (a property we use for convenience)+sharedStringInputSameAsOutput :: Text -> Either String String+sharedStringInputSameAsOutput someText =+ if someText == out then Right msg else Left msg+ where+ out = fst $ evalState (SW.upsertSharedString someText) SW.initialSharedString+ msg = printf "'%s' = '%s'" (Text.unpack out) (Text.unpack someText)++-- test if unique strings actually get set in the map as keys+sharedStringInputTextsIsSameAsMapLength :: [Text] -> Bool+sharedStringInputTextsIsSameAsMapLength someTexts =+ length result == length unqTexts+ where+ result :: Map Text Int+ result = view SW.string_map $ traverse SW.upsertSharedString someTexts `execState` SW.initialSharedString+ unqTexts :: Set Text+ unqTexts = Set.fromList someTexts++-- test for every unique string we get a unique number+sharedStringInputTextsIsSameAsValueSetLength :: [Text] -> Bool+sharedStringInputTextsIsSameAsValueSetLength someTexts =+ length result == length unqTexts+ where+ result :: Set Int+ result = setOf (SW.string_map . traversed) $ traverse SW.upsertSharedString someTexts `execState` SW.initialSharedString+ unqTexts :: Set Text+ unqTexts = Set.fromList someTexts++-- can we do xx+simpleWorkbook :: Xlsx+simpleWorkbook = def & atSheet "Sheet1" ?~ sheet+ where+ sheet = toWs [ ((RowIndex 1, ColumnIndex 1), a1)+ , ((RowIndex 1, ColumnIndex 2), cellValue ?~ CellText "text at B1 Sheet1" $ def) ]++a1 :: Cell+a1 = cellValue ?~ CellText "text at A1 Sheet1" $ cellStyle ?~ 1 $ def++-- can we do x+-- x+simpleWorkbookRow :: Xlsx+simpleWorkbookRow = def & atSheet "Sheet1" ?~ sheet+ where+ sheet = toWs [ ((RowIndex 1, ColumnIndex 1), a1)+ , ((RowIndex 2, ColumnIndex 1), cellValue ?~ CellText "text at A2 Sheet1" $ def) ]++toWs :: [((RowIndex, ColumnIndex), Cell)] -> Worksheet+toWs x = set wsCells (M.fromList x) def++-- can we do xxx+-- xxx+-- .+-- .+smallWorkbook :: Xlsx+smallWorkbook = def & atSheet "Sheet1" ?~ sheet+ where+ sheet = toWs $ [1..2] >>= \row ->+ [((row,1), a1)+ , ((row,2), def & cellValue ?~ CellText ("text at B"<> tshow row <> " Sheet1"))+ , ((row,3), def & cellValue ?~ CellText "text at C1 Sheet1")+ , ((row,4), def & cellValue ?~ CellDouble (0.2 + 0.1))+ , ((row,5), def & cellValue ?~ CellBool False)+ ]+-- sheets = [("Sheet1" , toWs $ [1..2] >>= \row ->+-- [ ((RowIndex row, ColumnIndex 1), a1)+-- , ((RowIndex row, ColumnIndex 2),+-- def & cellValue ?~ CellText ("text at B"<> tshow row <> " Sheet1"))+-- , ((RowIndex row, ColumnIndex 3),+-- def & cellValue ?~ CellText "text at C1 Sheet1")+-- , ((RowIndex row, ColumnIndex 4),+-- def & cellValue ?~ CellDouble (0.2 + 0.1))+-- , ((RowIndex row, ColumnIndex 5),+-- def & cellValue ?~ CellBool False)+-- ]+-- )]++bigWorkbook :: Xlsx+bigWorkbook = def & atSheet "Sheet1" ?~ sheet+ where+ sheet = toWs $ [1..512] >>= \row ->+ [((row,1), a1)+ ,((row,2), def & cellValue ?~ CellText ("text at B"<> tshow row <> " Sheet1"))+ ,((row,3), def & cellValue ?~ CellText "text at C1 Sheet1")+ ]+-- sheets = [("Sheet1" , toWs $ [1..512] >>= \row ->+-- [((RowIndex row, ColumnIndex 1), a1)+-- ,((RowIndex row, ColumnIndex 2),+-- def & cellValue ?~ CellText ("text at B"<> tshow row <> " Sheet1"))+-- ,((RowIndex row, ColumnIndex 3),+-- def & cellValue ?~ CellText "text at C1 Sheet1")+-- ]+-- )]++multipleSheetsWorkbook :: Xlsx+multipleSheetsWorkbook = simpleWorkbook+ & atSheet "my Sheet 2" ?~ sheet2+ & atSheet "my Sheet 3" ?~ sheet3+ where+ sheet2 = toWs [ ((RowIndex 1, ColumnIndex 1), cellValue ?~ CellText "text at A1 Sheet2" $ def)+ , ((RowIndex 1, ColumnIndex 2), cellValue ?~ CellText "text at B1 Sheet2" $ def) ]+ sheet3 = toWs [ ((RowIndex 1, ColumnIndex 1), cellValue ?~ CellText "text at A1 Sheet3" $ def)+ , ((RowIndex 1, ColumnIndex 2), cellValue ?~ CellText "text at B1 Sheet3" $ def) ]+ & wsState .~ Hidden++inlineStringsAreParsed :: IO ()+inlineStringsAreParsed = do+ items <- runXlsxM "data/inline-strings.xlsx" $ collectItemsIdentifier . fromJust =<< makeIdentifierFromName "Sheet1"+ let expected =+ [ IM.fromList+ [ ( 1,+ Cell+ { _cellStyle = Nothing,+ _cellValue = Just (CellText "My Inline String"),+ _cellComment = Nothing,+ _cellFormula = Nothing+ }+ ),+ ( 2,+ Cell+ { _cellStyle = Nothing,+ _cellValue = Just (CellText "two"),+ _cellComment = Nothing,+ _cellFormula = Nothing+ }+ )+ ]+ ]+ expected @==? (items ^.. traversed . ri_cell_row)++untypedCellsAreParsedAsFloats :: IO ()+untypedCellsAreParsedAsFloats = do+ -- values in that file are under `General` cell-type and are not marked+ -- as numbers explicitly in `t` attribute.+ items <- runXlsxM "data/floats.xlsx" $ collectItemsIdentifier . fromJust =<< makeIdentifierFromName "Лист1"+ let expected =+ [ IM.fromList [ (1, def & cellValue ?~ CellDouble 12.0) ]+ , IM.fromList [ (1, def & cellValue ?~ CellDouble 13.0) ]+ -- cell below has explicit `Numeric` type, while others are all `General`,+ -- but sometimes excel does not add a `t="n"` attr even to numeric cells+ -- but it should be default as number in any cases if `t` is missing+ , IM.fromList [ (1, def & cellValue ?~ CellDouble 14.0 & cellStyle ?~ 1 ) ]+ , IM.fromList [ (1, def & cellValue ?~ CellDouble 15.0) ]+ ]+ expected @==? (_ri_cell_row <$> items)+++richCellTextIsParsed :: IO ()+richCellTextIsParsed = do+ BS.writeFile "testinput.xlsx" (toBs richWorkbook)+ runXlsxM "testinput.xlsx" $ do+ sharedStrings <- getOrParseSharedStringss+ let result = Set.fromList $ V.toList sharedStrings+ liftIO $ expected @==? result++ where+ expected :: Set.Set Text+ expected = Set.fromList+ [ textA1+ , firstClauseB1 <> secondClauseB1+ , firstClauseB2 <> secondClauseB2+ ]++ textA1 = "Text at A1"+ firstClauseB1 = "First clause at B1;"+ firstClauseB2 = "First clause at B2;"+ secondClauseB1 = "Second clause at B1"+ secondClauseB2 = "Second clause at B2"++ richWorkbook :: Xlsx+ richWorkbook = def & atSheet "Sheet1" ?~ toWs+ [ ((RowIndex 1, ColumnIndex 1), cellValue ?~ CellText textA1 $ def)+ , ((RowIndex 2, ColumnIndex 1), cellValue ?~ cellRich firstClauseB1 secondClauseB1 $ def)+ , ((RowIndex 2, ColumnIndex 2), cellValue ?~ cellRich firstClauseB2 secondClauseB2 $ def)+ ]++cellRich :: Text -> Text -> CellValue+cellRich firstClause secondClause = CellRich+ [ RichTextRun+ { _richTextRunProperties = Just RunProperties+ { _runPropertiesBold = Nothing+ , _runPropertiesCharset = Just 1+ , _runPropertiesColor = Just Color+ { _colorAutomatic = Nothing+ , _colorARGB = Nothing+ , _colorTheme = Just 1+ , _colorTint = Nothing+ }+ , _runPropertiesCondense = Nothing+ , _runPropertiesExtend = Nothing+ , _runPropertiesFontFamily = Just FontFamilySwiss+ , _runPropertiesItalic = Nothing+ , _runPropertiesOutline = Nothing+ , _runPropertiesFont = Just "Aptos Narrow"+ , _runPropertiesScheme = Nothing+ , _runPropertiesShadow = Nothing+ , _runPropertiesStrikeThrough = Nothing+ , _runPropertiesSize = Just 11.0+ , _runPropertiesUnderline = Nothing+ , _runPropertiesVertAlign = Nothing+ }+ , _richTextRunText = firstClause+ }+ , RichTextRun+ { _richTextRunProperties = Just RunProperties+ { _runPropertiesBold = Just True+ , _runPropertiesCharset = Just 1+ , _runPropertiesColor = Just Color+ { _colorAutomatic = Nothing+ , _colorARGB = Just "FFFF0000"+ , _colorTheme = Nothing+ , _colorTint = Nothing+ }+ , _runPropertiesCondense = Nothing+ , _runPropertiesExtend = Nothing+ , _runPropertiesFontFamily = Just FontFamilySwiss+ , _runPropertiesItalic = Nothing+ , _runPropertiesOutline = Nothing+ , _runPropertiesFont = Just "Arial"+ , _runPropertiesScheme = Nothing+ , _runPropertiesShadow = Nothing+ , _runPropertiesStrikeThrough = Nothing+ , _runPropertiesSize = Just 8.0+ , _runPropertiesUnderline = Nothing+ , _runPropertiesVertAlign = Nothing+ }+ , _richTextRunText = secondClause+ }+ ]++#endif
+ test/Test/SmallCheck/Series/Instances.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Test.SmallCheck.Series.Instances+ (+ ) where++import Data.Map (Map)+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as T+import Test.SmallCheck.Series++import Codec.Xlsx++#if !MIN_VERSION_smallcheck(1,2,0)+cons6 ::+ ( Serial m a6+ , Serial m a5+ , Serial m a4+ , Serial m a3+ , Serial m a2+ , Serial m a1+ )+ => (a6 -> a5 -> a4 -> a3 -> a2 -> a1 -> a)+ -> Test.SmallCheck.Series.Series m a+cons6 f = decDepth $+ f <$> series+ <~> series+ <~> series+ <~> series+ <~> series+ <~> series+#endif++instance Monad m => Serial m Text where+ series = T.pack <$> series++instance (Serial m k, Serial m v) => Serial m (Map k v) where+ series = Map.singleton <$> series <~> series++{-------------------------------------------------------------------------------+ Conditional formatting+-------------------------------------------------------------------------------}++instance Monad m => Serial m CfRule++instance Monad m => Serial m Condition where+ series = localDepth (const 2) $ cons2 AboveAverage+ \/ cons1 BeginsWith+ \/ cons2 BelowAverage+ \/ cons1 BottomNPercent+ \/ cons1 BottomNValues+ \/ cons1 CellIs+ \/ cons4 ColorScale2+ \/ cons6 ColorScale3+ \/ cons0 ContainsBlanks+ \/ cons0 ContainsErrors+ \/ cons1 ContainsText+ \/ cons1 DataBar+ \/ cons0 DoesNotContainErrors+ \/ cons0 DoesNotContainBlanks+ \/ cons1 DoesNotContainText+ \/ cons0 DuplicateValues+ \/ cons1 EndsWith+ \/ cons1 Expression+ \/ cons1 IconSet+ \/ cons1 InTimePeriod+ \/ cons1 TopNPercent+ \/ cons1 TopNValues+ \/ cons0 UniqueValues++instance Monad m => Serial m NStdDev++instance Monad m => Serial m Inclusion++instance Monad m => Serial m OperatorExpression++instance Monad m => Serial m DataBarOptions++instance Monad m => Serial m MaxCfValue++instance Monad m => Serial m MinCfValue++instance Monad m => Serial m Color++-- TODO: proper formula generator (?)+instance Monad m => Serial m Formula++instance Monad m => Serial m IconSetOptions++instance Monad m => Serial m IconSetType++instance Monad m => Serial m CfValue++instance Monad m => Serial m TimePeriod++{-------------------------------------------------------------------------------+ Autofilter+-------------------------------------------------------------------------------}++instance Monad m => Serial m AutoFilter where+ series = localDepth (const 4) $ cons2 AutoFilter++instance Monad m => Serial m CellRef++instance Monad m => Serial m FilterColumn++instance Monad m => Serial m EdgeFilterOptions++instance Monad m => Serial m CustomFilter++instance Monad m => Serial m CustomFilterOperator++instance Monad m => Serial m FilterCriterion++instance Monad m => Serial m DateGroup++instance Monad m => Serial m FilterByBlank++instance Monad m => Serial m ColorFilterOptions++instance Monad m => Serial m DynFilterOptions++instance Monad m => Serial m DynFilterType
+ test/TestXlsx.hs view
@@ -0,0 +1,599 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE CPP #-}++module TestXlsx where++#ifdef USE_MICROLENS+import Lens.Micro.Platform+#else+import Control.Lens+#endif+import Control.Monad.State.Lazy+import Data.ByteString.Lazy (ByteString)+import Data.Map (Map)+import qualified Data.Map as M+import Data.Time.Clock.POSIX (POSIXTime)+import qualified Data.Vector as V+import Text.RawString.QQ+import Text.XML++import Codec.Xlsx+import Codec.Xlsx.Formatted+import Codec.Xlsx.Types.Internal+import Codec.Xlsx.Types.Internal.CommentTable+import Codec.Xlsx.Types.Internal.CustomProperties+ as CustomProperties+import Codec.Xlsx.Types.Internal.SharedStringTable++import PivotTableTests+import DrawingTests++testXlsx :: Xlsx+testXlsx = Xlsx sheets minimalStyles definedNames customProperties DateBase1904+ where+ sheets =+ [ ("List1", sheet1)+ , ("Another sheet", sheet2)+ , ("with pivot table", pvSheet)+ , ("cellrange DV source", foreignDvSourceSheet) -- "foreign" sheet holding validation data+ , ("cellrange DV test", foreignDvTestSheet) -- applies validation using foreign cell ranges+ , ("hidden sheet", def & wsState .~ Hidden & cellValueAt (1,1) ?~ CellText "I'm hidden!")+ , ("VERY hidden sheet", def & wsState .~ VeryHidden & cellValueAt (1,1) ?~ CellText "I'm VERY hidden!!")+ ]+ sheet1 = Worksheet cols rowProps testCellMap1 drawing ranges+ sheetViews pageSetup cFormatting validations [] (Just autoFilter)+ tables (Just protection) sharedFormulas def+ sharedFormulas =+ M.fromList+ [ (SharedFormulaIndex 0, SharedFormulaOptions (CellRef "A5:C5") (Formula "A4"))+ , (SharedFormulaIndex 1, SharedFormulaOptions (CellRef "B6:C6") (Formula "B3+12"))+ ]+ autoFilter = def & afRef ?~ CellRef "A1:E10"+ & afFilterColumns .~ fCols+ fCols = M.fromList [ (1, Filters DontFilterByBlank+ [FilterValue "a", FilterValue "b",FilterValue "ZZZ"])+ , (2, CustomFiltersAnd (CustomFilter FltrGreaterThanOrEqual "0")+ (CustomFilter FltrLessThan "42"))]+ tables =+ [ Table+ { tblName = Just "Table1"+ , tblDisplayName = "Table1"+ , tblRef = CellRef "A3"+ , tblColumns = [TableColumn "another text"]+ , tblAutoFilter = Just (def & afRef ?~ CellRef "A3")+ }+ ]+ protection =+ fullSheetProtection+ { _sprScenarios = False+ , _sprLegacyPassword = Just $ legacyPassword "hard password"+ }+ sheet2 = def & wsCells .~ testCellMap2+ foreignDvSourceSheet = def & wsCells .~ cellRangeDvSourceMap+ foreignDvTestSheet = def & wsDataValidations .~ foreignValidations &+ wsCells . at (1, 1) ?~+ (def & cellValue ?~ CellText ("Hi! try " <> unCellRef testForeignDvRange))+ pvSheet = sheetWithPvCells & wsPivotTables .~ [testPivotTable]+ sheetWithPvCells = def & wsCells .~ testPivotSrcCells+ rowProps = M.fromList [(1, RowProps { rowHeight = Just (CustomHeight 50)+ , rowStyle = Just 3+ , rowHidden = False+ })]+ cols = [ColumnsProperties 1 10 (Just 15) (Just 1) False False False]+ drawing = Just $ testDrawing { _xdrAnchors = map resolve $ _xdrAnchors testDrawing }+ resolve :: Anchor RefId RefId -> Anchor FileInfo ChartSpace+ resolve Anchor {..} =+ let obj =+ case _anchObject of+ Picture {..} ->+ let blipFill = (_picBlipFill & bfpImageInfo ?~ fileInfo)+ in Picture+ { _picMacro = _picMacro+ , _picPublished = _picPublished+ , _picNonVisual = _picNonVisual+ , _picBlipFill = blipFill+ , _picShapeProperties = _picShapeProperties+ }+ Graphic nv _ tr ->+ Graphic nv testLineChartSpace tr+ in Anchor+ { _anchAnchoring = _anchAnchoring+ , _anchObject = obj+ , _anchClientData = _anchClientData+ }+ fileInfo = FileInfo "dummy.png" "image/png" "fake contents"+ ranges = [mkRange (1,1) (1,2), mkRange (2,2) (10, 5)]+ minimalStyles = renderStyleSheet minimalStyleSheet+ definedNames = DefinedNames [("SampleName", Nothing, "A10:A20")]+ sheetViews = Just [sheetView1, sheetView2]+ sheetView1 = def & sheetViewRightToLeft ?~ True+ & sheetViewTopLeftCell ?~ CellRef "B5"+ sheetView2 = def & sheetViewType ?~ SheetViewTypePageBreakPreview+ & sheetViewWorkbookViewId .~ 5+ & sheetViewSelection .~ [ def & selectionActiveCell ?~ CellRef "C2"+ & selectionPane ?~ PaneTypeBottomRight+ , def & selectionActiveCellId ?~ 1+ & selectionSqref ?~ SqRef [ CellRef "A3:A10"+ , CellRef "B1:G3"]+ ]+ pageSetup = Just $ def & pageSetupBlackAndWhite ?~ True+ & pageSetupCopies ?~ 2+ & pageSetupErrors ?~ PrintErrorsDash+ & pageSetupPaperSize ?~ PaperA4+ customProperties = M.fromList [("some_prop", VtInt 42)]+ cFormatting = M.fromList [(SqRef [CellRef "A1:B3"], rules1), (SqRef [CellRef "C1:C10"], rules2)]+ cfRule c d = CfRule { _cfrCondition = c+ , _cfrDxfId = Just d+ , _cfrPriority = topCfPriority+ , _cfrStopIfTrue = Nothing+ }+ rules1 = [ cfRule ContainsBlanks 1+ , cfRule (ContainsText "foo") 2+ , cfRule (CellIs (OpBetween (Formula "A1") (Formula "B10"))) 3+ ]+ rules2 = [ cfRule ContainsErrors 3 ]++testCellMap1 :: CellMap+testCellMap1 = M.fromList [ ((1, 2), cd1_2), ((1, 5), cd1_5), ((1, 10), cd1_10)+ , ((3, 1), cd3_1), ((3, 2), cd3_2), ((3, 3), cd3_3), ((3, 7), cd3_7)+ , ((4, 1), cd4_1), ((4, 2), cd4_2), ((4, 3), cd4_3)+ , ((5, 1), cd5_1), ((5, 2), cd5_2), ((5, 3), cd5_3)+ , ((6, 2), cd6_2), ((6, 3), cd6_3)+ ]+ where+ cd v = def {_cellValue=Just v}+ cd1_2 = cd (CellText "just a text, fließen, русский <> и & \"in quotes\"")+ cd1_5 = cd (CellDouble 42.4567)+ cd1_10 = cd (CellText "")+ cd3_1 = cd (CellText "another text")+ cd3_2 = def -- shouldn't it be skipped?+ cd3_3 = def & cellValue ?~ CellError ErrorDiv0+ & cellFormula ?~ simpleCellFormula "1/0"+ cd3_7 = cd (CellBool True)+ cd4_1 = cd (CellDouble 1)+ cd4_2 = cd (CellDouble 123456789012345)+ cd4_3 = (cd (CellDouble (1+2))) { _cellFormula =+ Just $ simpleCellFormula "A4+B4<>11"+ }+ cd5_1 = def & cellFormula ?~ sharedFormulaByIndex (SharedFormulaIndex 0)+ cd5_2 = def & cellFormula ?~ sharedFormulaByIndex (SharedFormulaIndex 0)+ cd5_3 = def & cellFormula ?~ sharedFormulaByIndex (SharedFormulaIndex 0)+ cd6_2 = def & cellFormula ?~ sharedFormulaByIndex (SharedFormulaIndex 1)+ cd6_3 = def & cellFormula ?~ sharedFormulaByIndex (SharedFormulaIndex 1)++cellRangeDvSourceMap :: CellMap+cellRangeDvSourceMap = M.fromList [ ((1, 1), def & cellValue ?~ CellText "A-A-A")+ , ((2, 1), def & cellValue ?~ CellText "B-B-B")+ , ((1, 2), def & cellValue ?~ CellText "C-C-C")+ , ((2, 2), def & cellValue ?~ CellText "D-D-D")+ , ((1, 3), def & cellValue ?~ CellDouble 6)+ , ((2, 3), def & cellValue ?~ CellDouble 7)+ , ((3, 1), def & cellValue ?~ CellDouble 5)+ , ((3, 2), def & cellValue ?~ CellText "numbers!")+ , ((3, 3), def & cellValue ?~ CellDouble 5)+ ]++testCellMap2 :: CellMap+testCellMap2 = M.fromList [ ((1, 2), def & cellValue ?~ CellText "something here")+ , ((3, 5), def & cellValue ?~ CellDouble 123.456)+ , ((2, 4),+ def & cellValue ?~ CellText "value"+ & cellComment ?~ comment1+ )+ , ((10, 7),+ def & cellValue ?~ CellText "value"+ & cellComment ?~ comment2+ )+ , ((11, 4), def & cellComment ?~ comment3)+ ]+ where+ comment1 = Comment (XlsxText "simple comment") "bob" True+ comment2 = Comment (XlsxRichText [rich1, rich2]) "alice" False+ comment3 = Comment (XlsxText "comment for an empty cell") "bob" True+ rich1 = def & richTextRunText.~ "Look ma!"+ & richTextRunProperties ?~ (+ def & runPropertiesBold ?~ True+ & runPropertiesFont ?~ "Tahoma")+ rich2 = def & richTextRunText .~ "It's blue!"+ & richTextRunProperties ?~ (+ def & runPropertiesItalic ?~ True+ & runPropertiesColor ?~ (def & colorARGB ?~ "FF000080"))++testTime :: POSIXTime+testTime = 123++fromRight :: Show a => Either a b -> b+fromRight (Right b) = b+fromRight (Left x) = error $ "Right _ was expected but Left " ++ show x ++ " found"++testStyleSheet :: StyleSheet+testStyleSheet = minimalStyleSheet & styleSheetDxfs .~ [dxf1, dxf2, dxf3]+ & styleSheetNumFmts .~ M.fromList [(164, "0.000")]+ & styleSheetCellXfs %~ (++ [cellXf1, cellXf2])+ where+ dxf1 = def & dxfFont ?~ (def & fontBold ?~ True+ & fontSize ?~ 12)+ dxf2 = def & dxfFill ?~ (def & fillPattern ?~ (def & fillPatternBgColor ?~ red))+ dxf3 = def & dxfNumFmt ?~ NumFmt 164 "0.000"+ red = def & colorARGB ?~ "FFFF0000"+ cellXf1 = def+ { _cellXfApplyNumberFormat = Just True+ , _cellXfNumFmtId = Just 2 }+ cellXf2 = def+ { _cellXfApplyNumberFormat = Just True+ , _cellXfNumFmtId = Just 164 }+++withSingleUnderline :: SharedStringTable -> SharedStringTable+withSingleUnderline = withUnderline FontUnderlineSingle++withDoubleUnderline :: SharedStringTable -> SharedStringTable+withDoubleUnderline = withUnderline FontUnderlineDouble++withUnderline :: FontUnderline -> SharedStringTable -> SharedStringTable+withUnderline u (SharedStringTable [text, XlsxRichText [rich1, RichTextRun (Just props) val]]) =+ let newprops = props & runPropertiesUnderline .~ Just u + in SharedStringTable [text, XlsxRichText [rich1, RichTextRun (Just newprops) val]] ++testSharedStringTable :: SharedStringTable+testSharedStringTable = SharedStringTable $ V.fromList items+ where+ items = [text, rich]+ text = XlsxText "plain text"+ rich = XlsxRichText [ RichTextRun Nothing "Just "+ , RichTextRun (Just props) "example" ]+ props = def & runPropertiesBold .~ Just True+ & runPropertiesItalic .~ Just True+ & runPropertiesSize .~ Just 10+ & runPropertiesFont .~ Just "Arial"+ & runPropertiesFontFamily .~ Just FontFamilySwiss++testSharedStringTableWithEmpty :: SharedStringTable+testSharedStringTableWithEmpty =+ SharedStringTable $ V.fromList [XlsxText ""]++testCommentTable :: CommentTable+testCommentTable = CommentTable $ M.fromList+ [ (CellRef "D4", Comment (XlsxRichText rich) "Bob" False)+ , (CellRef "A2", Comment (XlsxText "Some comment here") "CBR" True) ]+ where+ rich = [ RichTextRun+ { _richTextRunProperties =+ Just $ def & runPropertiesBold ?~ True+ & runPropertiesCharset ?~ 1+ & runPropertiesColor ?~ def -- TODO: why not Nothing here?+ & runPropertiesFont ?~ "Calibri"+ & runPropertiesScheme ?~ FontSchemeMinor+ & runPropertiesSize ?~ 8.0+ , _richTextRunText = "Bob:"}+ , RichTextRun+ { _richTextRunProperties =+ Just $ def & runPropertiesCharset ?~ 1+ & runPropertiesColor ?~ def+ & runPropertiesFont ?~ "Calibri"+ & runPropertiesScheme ?~ FontSchemeMinor+ & runPropertiesSize ?~ 8.0+ , _richTextRunText = "Why such high expense?"}]++testStrings :: ByteString+testStrings = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+ <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">+ <si><t>plain text</t></si>+ <si><r><t>Just </t></r><r><rPr><b /><i />+ <sz val="10"/><rFont val="Arial"/><family val="2"/></rPr><t>example</t></r></si>+ </sst>+|]++testStringsWithSingleUnderline :: ByteString+testStringsWithSingleUnderline = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+ <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">+ <si><t>plain text</t></si>+ <si><r><t>Just </t></r><r><rPr><b /><i /><u />+ <sz val="10"/><rFont val="Arial"/><family val="2"/></rPr><t>example</t></r></si>+ </sst>+|]++testStringsWithDoubleUnderline :: ByteString+testStringsWithDoubleUnderline = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+ <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">+ <si><t>plain text</t></si>+ <si><r><t>Just </t></r><r><rPr><b /><i /><u val="double"/>+ <sz val="10"/><rFont val="Arial"/><family val="2"/></rPr><t>example</t></r></si>+ </sst>+|]++testStringsWithEmpty :: ByteString+testStringsWithEmpty = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+ <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">+ <si><t/></si>+ </sst>+|]++testComments :: ByteString+testComments = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">+ <authors>+ <author>Bob</author>+ <author>CBR</author>+ </authors>+ <commentList>+ <comment ref="D4" authorId="0" visible="False">+ <text>+ <r>+ <rPr>+ <b/><sz val="8"/><color indexed="81"/><rFont val="Calibri"/>+ <charset val="1"/><scheme val="minor"/>+ </rPr>+ <t>Bob:</t>+ </r>+ <r>+ <rPr>+ <sz val="8"/><color indexed="81"/><rFont val="Calibri"/>+ <charset val="1"/> <scheme val="minor"/>+ </rPr>+ <t xml:space="preserve">Why such high expense?</t>+ </r>+ </text>+ </comment>+ <comment ref="A2" authorId="1" visible="True">+ <text><t>Some comment here</t></text>+ </comment>+ </commentList>+</comments>+|]++testCustomProperties :: CustomProperties+testCustomProperties = CustomProperties.fromList+ [ ("testTextProp", VtLpwstr "test text property value")+ , ("prop2", VtLpwstr "222")+ , ("bool", VtBool False)+ , ("prop333", VtInt 1)+ , ("decimal", VtDecimal 1.234) ]++testCustomPropertiesXml :: ByteString+testCustomPropertiesXml = [r|+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>+<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="prop2">+ <vt:lpwstr>222</vt:lpwstr>+ </property>+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="3" name="prop333">+ <vt:int>1</vt:int>+ </property>+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="4" name="testTextProp">+ <vt:lpwstr>test text property value</vt:lpwstr>+ </property>+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="5" name="decimal">+ <vt:decimal>1.234</vt:decimal>+ </property>+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="6" name="bool">+ <vt:bool>false</vt:bool>+ </property>+ <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="7" name="blob">+ <vt:blob>+ ZXhhbXBs+ ZSBibG9i+ IGNvbnRl+ bnRz+ </vt:blob>+ </property>+</Properties>+|]++testFormattedResult :: Formatted+testFormattedResult = Formatted cm styleSheet merges+ where+ cm = M.fromList [ ((1, 1), cell11)+ , ((1, 2), cell12)+ , ((2, 5), cell25) ]+ cell11 = Cell+ { _cellStyle = Just 1+ , _cellValue = Just (CellText "text at A1")+ , _cellComment = Nothing+ , _cellFormula = Nothing }+ cell12 = Cell+ { _cellStyle = Just 2+ , _cellValue = Just (CellDouble 1.23)+ , _cellComment = Nothing+ , _cellFormula = Nothing }+ cell25 = Cell+ { _cellStyle = Just 3+ , _cellValue = Just (CellDouble 1.23456)+ , _cellComment = Nothing+ , _cellFormula = Nothing }+ merges = []+ styleSheet =+ minimalStyleSheet & styleSheetCellXfs %~ (++ [cellXf1, cellXf2, cellXf3])+ & styleSheetFonts %~ (++ [font1, font2])+ & styleSheetNumFmts .~ numFmts+ nextFontId = length (minimalStyleSheet ^. styleSheetFonts)+ cellXf1 = def+ { _cellXfApplyFont = Just True+ , _cellXfFontId = Just nextFontId }+ font1 = def+ { _fontName = Just "Calibri"+ , _fontBold = Just True }+ cellXf2 = def+ { _cellXfApplyFont = Just True+ , _cellXfFontId = Just (nextFontId + 1)+ , _cellXfApplyNumberFormat = Just True+ , _cellXfNumFmtId = Just 164 }+ font2 = def+ { _fontItalic = Just True }+ cellXf3 = def+ { _cellXfApplyNumberFormat = Just True+ , _cellXfNumFmtId = Just 2 }+ numFmts = M.fromList [(164, "0.0000")]++testRunFormatted :: Formatted+testRunFormatted = formatted formattedCellMap minimalStyleSheet+ where+ formattedCellMap = flip execState def $ do+ let font1 = def & fontBold ?~ True+ & fontName ?~ "Calibri"+ at (1, 1) ?= (def & formattedCell . cellValue ?~ CellText "text at A1"+ & formattedFormat . formatFont ?~ font1)+ at (1, 2) ?= (def & formattedCell . cellValue ?~ CellDouble 1.23+ & formattedFormat . formatFont . non def . fontItalic ?~ True+ & formattedFormat . formatNumberFormat ?~ fmtDecimalsZeroes 4)+ at (2, 5) ?= (def & formattedCell . cellValue ?~ CellDouble 1.23456+ & formattedFormat . formatNumberFormat ?~ StdNumberFormat Nf2Decimal)++testFormatWorkbookResult :: Xlsx+testFormatWorkbookResult = def & xlSheets .~ sheets+ & xlStyles .~ renderStyleSheet style+ where+ cellMap1 = M.fromList [((1, 1), Cell { _cellStyle = Nothing+ , _cellValue = Just (CellText "text at A1 Sheet1")+ , _cellComment = Nothing+ , _cellFormula = Nothing })]+ cellMap2 = M.fromList [((2, 3), Cell { _cellStyle = Just 1+ , _cellValue = Just (CellDouble 1.23456)+ , _cellComment = Nothing+ , _cellFormula = Nothing })]+ sheets = [ ("Sheet1", def & wsCells .~ cellMap1) , ("Sheet2", def & wsCells .~ cellMap2) ]+ style = minimalStyleSheet & styleSheetNumFmts .~ M.fromList [(164, "DD.MM.YYYY")]+ & styleSheetCellXfs .~ [cellXf1, cellXf2]+ cellXf1 = def+ & cellXfBorderId .~ Just 0+ & cellXfFillId .~ Just 0+ & cellXfFontId .~ Just 0+ cellXf2 = def+ { _cellXfApplyNumberFormat = Just True+ , _cellXfNumFmtId = Just 164 }+ +testFormatWorkbook :: Xlsx+testFormatWorkbook = formatWorkbook sheets minimalStyleSheet+ where+ sheetNames = ["Sheet1", "Sheet2"]+ testFormattedCellMap1 = M.fromList [((1,1), (def & formattedCell . cellValue ?~ CellText "text at A1 Sheet1"))]+ + testFormattedCellMap2 = M.fromList [((2,3), (def & formattedCell . cellValue ?~ CellDouble 1.23456+ & formattedFormat . formatNumberFormat ?~ (UserNumberFormat "DD.MM.YYYY")))]+ sheets = zip sheetNames [testFormattedCellMap1, testFormattedCellMap2]++testCondFormattedResult :: CondFormatted+testCondFormattedResult = CondFormatted styleSheet formattings+ where+ styleSheet =+ minimalStyleSheet & styleSheetDxfs .~ dxfs+ dxfs = [ def & dxfFont ?~ (def & fontUnderline ?~ FontUnderlineSingle)+ , def & dxfFont ?~ (def & fontStrikeThrough ?~ True)+ , def & dxfFont ?~ (def & fontBold ?~ True) ]+ formattings = M.fromList [ (SqRef [CellRef "A1:A2", CellRef "B2:B3"], [cfRule1, cfRule2])+ , (SqRef [CellRef "C3:E10"], [cfRule1])+ , (SqRef [CellRef "F1:G10"], [cfRule3]) ]+ cfRule1 = CfRule+ { _cfrCondition = ContainsBlanks+ , _cfrDxfId = Just 0+ , _cfrPriority = 1+ , _cfrStopIfTrue = Nothing }+ cfRule2 = CfRule+ { _cfrCondition = BeginsWith "foo"+ , _cfrDxfId = Just 1+ , _cfrPriority = 1+ , _cfrStopIfTrue = Nothing }+ cfRule3 = CfRule+ { _cfrCondition = CellIs (OpGreaterThan (Formula "A1"))+ , _cfrDxfId = Just 2+ , _cfrPriority = 1+ , _cfrStopIfTrue = Nothing }++testFormattedCells :: Map (RowIndex, ColumnIndex) FormattedCell+testFormattedCells = flip execState def $ do+ at (1, 1) ?=+ (def & formattedRowSpan .~ 5+ & formattedColSpan .~ 5+ & formattedFormat . formatBorder . non def . borderTop .+ non def . borderStyleLine ?~ LineStyleDashed+ & formattedFormat . formatBorder . non def . borderBottom .+ non def . borderStyleLine ?~ LineStyleDashed)+ at (10, 2) ?= (def & formattedFormat . formatFont . non def . fontBold ?~ True)++testRunCondFormatted :: CondFormatted+testRunCondFormatted = conditionallyFormatted condFmts minimalStyleSheet+ where+ condFmts = flip execState def $ do+ let cfRule1 = def & condfmtCondition .~ ContainsBlanks+ & condfmtDxf . dxfFont . non def . fontUnderline ?~ FontUnderlineSingle+ cfRule2 = def & condfmtCondition .~ BeginsWith "foo"+ & condfmtDxf . dxfFont . non def . fontStrikeThrough ?~ True+ cfRule3 = def & condfmtCondition .~ CellIs (OpGreaterThan (Formula "A1"))+ & condfmtDxf . dxfFont . non def . fontBold ?~ True+ at (CellRef "A1:A2") ?= [cfRule1, cfRule2]+ at (CellRef "B2:B3") ?= [cfRule1, cfRule2]+ at (CellRef "C3:E10") ?= [cfRule1]+ at (CellRef "F1:G10") ?= [cfRule3]++validations :: Map SqRef DataValidation+validations = M.fromList+ [ ( SqRef [CellRef "A1"], def+ )+ , ( SqRef [CellRef "A1", CellRef "B2:C3"], def+ { _dvAllowBlank = True+ , _dvError = Just "incorrect data"+ , _dvErrorStyle = ErrorStyleInformation+ , _dvErrorTitle = Just "error title"+ , _dvPrompt = Just "enter data"+ , _dvPromptTitle = Just "prompt title"+ , _dvShowDropDown = True+ , _dvShowErrorMessage = True+ , _dvShowInputMessage = True+ , _dvValidationType = ValidationTypeList $ ListExpression ["aaaa","bbbb","cccc"]+ }+ )+ , ( SqRef [CellRef "A6", CellRef "I2"], def+ { _dvAllowBlank = False+ , _dvError = Just "aaa"+ , _dvErrorStyle = ErrorStyleWarning+ , _dvErrorTitle = Just "bbb"+ , _dvPrompt = Just "ccc"+ , _dvPromptTitle = Just "ddd"+ , _dvShowDropDown = False+ , _dvShowErrorMessage = False+ , _dvShowInputMessage = False+ , _dvValidationType = ValidationTypeDecimal $ ValGreaterThan $ Formula "10"+ }+ )+ , ( SqRef [CellRef "A7"], def+ { _dvAllowBlank = False+ , _dvError = Just "aaa"+ , _dvErrorStyle = ErrorStyleStop+ , _dvErrorTitle = Just "bbb"+ , _dvPrompt = Just "ccc"+ , _dvPromptTitle = Just "ddd"+ , _dvShowDropDown = False+ , _dvShowErrorMessage = False+ , _dvShowInputMessage = False+ , _dvValidationType = ValidationTypeWhole $ ValNotBetween (Formula "10") (Formula "12")+ }+ )+ ]++testForeignDvRange :: Range+testForeignDvRange = CellRef "B2:B7"++foreignValidations :: Map SqRef DataValidation+foreignValidations = M.fromList+ [ ( SqRef [testForeignDvRange], def+ { _dvAllowBlank = True+ , _dvError = Just "incorrect data"+ , _dvErrorStyle = ErrorStyleInformation+ , _dvErrorTitle = Just "error title"+ , _dvPrompt = Just "Input kebab string"+ , _dvPromptTitle = Just "I love kebab-case"+ , _dvShowDropDown = True+ , _dvShowErrorMessage = True+ , _dvShowInputMessage = True+ , _dvValidationType = ValidationTypeList $ RangeExpression $ CellRef "'cellrange DV source'!$A$1:$B$2"+ }+ )+ ]
xlsx.cabal view
@@ -1,6 +1,6 @@ Name: xlsx -Version: 0.1.2+Version: 1.2.0 Synopsis: Simple and incomplete Excel file parser/writer Description:@@ -10,10 +10,14 @@ For examples look into "Codec.Xlsx". . Format is covered by ECMA-376 standard:- <http://www.ecma-international.org/publications/standards/Ecma-376.htm>+ <https://ecma-international.org/publications-and-standards/standards/ecma-376/>+ .+ 4th edition of the standard with the transitional schema is used for this library. Extra-source-files:- changelog-+ CHANGELOG.markdown+ data/inline-strings.xlsx+ data/floats.xlsx+ data/absolute_relationships.xlsx Homepage: https://github.com/qrilka/xlsx Bug-Reports: https://github.com/qrilka/xlsx/issues@@ -24,84 +28,186 @@ Category: Codec Build-type: Simple-+Tested-with: GHC == 9.2.8, GHC == 9.4.8, GHC == 9.6.6, GHC == 9.8.4 Cabal-version: >=1.10 +Flag microlens+ default: False+ description: Use microlens instead of lens + Library Hs-source-dirs: src+ ghc-options: -Wall Exposed-modules: Codec.Xlsx- , Codec.Xlsx.Parser- , Codec.Xlsx.Types- , Codec.Xlsx.Writer- , Codec.Xlsx.Lens- , Codec.Xlsx.Parser.Internal-- Build-depends: base == 4.*- , containers >= 0.5.0.0- , transformers >= 0.3.0.0- , bytestring >= 0.10.0.2- , text >= 0.11.3.1- , lens >= 3.8 && < 5- , conduit >= 1.0.0- , xml-types == 0.3.*- , xml-conduit >= 1.1.0- , zip-archive == 0.2.*- , digest == 0.0.*- , zlib >= 0.5.4.0- , utf8-string >= 0.3.7- , time >= 1.4.0.1- , old-time >= 1.1.0.1- , old-locale >= 1.0.0.5- , data-default == 0.5.*- , vector >= 0.10- , binary-search- Default-Language: Haskell2010-+ , Codec.Xlsx.Types+ , Codec.Xlsx.Formatted+ , Codec.Xlsx.Lens+ , Codec.Xlsx.Parser+ , Codec.Xlsx.Parser.Internal+ , Codec.Xlsx.Parser.Internal.Fast+ , Codec.Xlsx.Parser.Internal.Util+ , Codec.Xlsx.Parser.Internal.PivotTable+ , Codec.Xlsx.Types.AutoFilter+ , Codec.Xlsx.Types.Cell+ , Codec.Xlsx.Types.Comment+ , Codec.Xlsx.Types.Common+ , Codec.Xlsx.Types.ConditionalFormatting+ , Codec.Xlsx.Types.DataValidation+ , Codec.Xlsx.Types.Drawing+ , Codec.Xlsx.Types.Drawing.Chart+ , Codec.Xlsx.Types.Drawing.Common+ , Codec.Xlsx.Types.Internal+ , Codec.Xlsx.Types.Internal.CfPair+ , Codec.Xlsx.Types.Internal.CommentTable+ , Codec.Xlsx.Types.Internal.ContentTypes+ , Codec.Xlsx.Types.Internal.CustomProperties+ , Codec.Xlsx.Types.Internal.DvPair+ , Codec.Xlsx.Types.Internal.FormulaData+ , Codec.Xlsx.Types.Internal.Relationships+ , Codec.Xlsx.Types.Internal.SharedStringTable+ , Codec.Xlsx.Types.PageSetup+ , Codec.Xlsx.Types.PivotTable+ , Codec.Xlsx.Types.PivotTable.Internal+ , Codec.Xlsx.Types.Protection+ , Codec.Xlsx.Types.RefId+ , Codec.Xlsx.Types.RichText+ , Codec.Xlsx.Types.SheetViews+ , Codec.Xlsx.Types.StyleSheet+ , Codec.Xlsx.Types.Table+ , Codec.Xlsx.Types.Variant+ , Codec.Xlsx.Writer+ , Codec.Xlsx.Writer.Internal+ , Codec.Xlsx.Writer.Internal.PivotTable+ , Codec.Xlsx.Parser.Stream+ , Codec.Xlsx.Writer.Stream+ , Codec.Xlsx.Parser.Stream.Row+ , Codec.Xlsx.Parser.Stream.SheetInfo+ , Codec.Xlsx.Parser.Stream.SheetItem+ , Codec.Xlsx.Writer.Internal.Stream -Executable test- Hs-source-dirs: src- Other-modules: Codec.Xlsx- , Codec.Xlsx.Parser- , Codec.Xlsx.Writer- ghc-options: -Wall -threaded+ -- The only function it exports is also hidden by the upstream library: https://github.com/the-real-blackh/hexpat/blob/master/Text/XML/Expat/SAX.hs#L227+ -- We could expose it but then this function is in the xlsx API for a long time.+ -- It be better to expose it in the upstream library instead I think. It was copied here so the parser can use it.+ Other-modules: Codec.Xlsx.Parser.Stream.HexpatInternal+ , Codec.Xlsx.Parser.Internal.Memoize+ , Codec.Xlsx.Parser.Stream.SheetIdentifier+ , Codec.Xlsx.Parser.Stream.SheetIndex+ , Codec.Xlsx.LensCompat - main-is: Test.hs+ Build-depends: base >= 4.9.0.0 && < 5.0+ , attoparsec+ , base64-bytestring+ , binary-search+ , bytestring >= 0.10.8.0+ , conduit >= 1.0.0+ , containers >= 0.5.0.0+ , data-default+ , deepseq >= 1.4+ , dlist+ , errors+ , extra+ , filepath+ , hexpat+ , mtl >= 2.1+ , network-uri+ , old-locale >= 1.0.0.5+ , safe >= 0.3+ , text >= 0.11.3.1+ , time >= 1.4.0.1+ , transformers >= 0.3.0.0+ , vector >= 0.10+ , xeno >= 0.3.2+ , xml-conduit >= 1.1.0+ , zip-archive >= 0.2+ , zlib >= 0.5.4.0+ , zip+ , zip-stream >= 0.2.0.1+ , xml-types+ , exceptions+ , transformers-base+ , monad-control+ if flag(microlens)+ Build-depends: microlens >= 0.4 && < 0.5+ , microlens-mtl+ , microlens-ghc+ , microlens-th+ , profunctors+ , microlens-platform+ , indexed-traversable+ cpp-options: -DUSE_MICROLENS+ else+ Build-depends: lens >= 3.8 && < 5.4 - Build-depends: base, containers, transformers, bytestring, text- , conduit >= 1.0.0- , xml-types == 0.3.*- , xml-conduit >= 1.1.0- , zip-archive == 0.2.*- , lens >= 3.8 && < 5- , digest > 0.0.0.1- , zlib- , utf8-string- , time- , old-time- , old-locale- , data-default == 0.5.*- , vector >= 0.10- , binary-search Default-Language: Haskell2010+ Other-Extensions: DeriveDataTypeable+ FlexibleInstances+ NoMonomorphismRestriction+ OverloadedStrings+ RankNTypes+ RecordWildCards+ TemplateHaskell+ TupleSections test-suite data-test type: exitcode-stdio-1.0- main-is: DataTest.hs+ main-is: Main.hs hs-source-dirs: test/- Build-Depends: base,- containers,- time,- old-time,- xlsx,- xml-conduit >= 1.1.0,- smallcheck,- HUnit,- tasty,- tasty-smallcheck,- tasty-hunit+ other-modules: AutoFilterTests+ , Common+ , CommonTests+ , CommonTests.CellRefTests+ , CondFmtTests+ , Diff+ , DrawingTests+ , PivotTableTests+ , StreamTests+ , Test.SmallCheck.Series.Instances+ , TestXlsx+ Build-Depends: base+ , bytestring+ , containers+ , Diff >= 0.3.0+ , directory+ , groom+ , mtl+ , raw-strings-qq+ , smallcheck+ , tasty+ , tasty-hunit+ , tasty-smallcheck+ , text+ , time+ , vector+ , xlsx+ , xml-conduit >= 1.1.0+ , conduit+ , filepath+ , deepseq+ , zip+ if flag(microlens)+ Build-depends: microlens >= 0.4 && < 0.5+ , microlens-mtl+ , microlens-platform+ , microlens-th+ cpp-options: -DUSE_MICROLENS+ else+ Build-depends: lens >= 3.8 && < 5.4 Default-Language: Haskell2010 source-repository head type: git location: git://github.com/qrilka/xlsx.git++benchmark bench+ type: exitcode-stdio-1.0+ hs-source-dirs: benchmarks+ main-is: Main.hs+ build-depends: base+ , bytestring+ , criterion+ , xlsx+ , deepseq+ , conduit+ , lens+ default-language: Haskell2010