xlsx 0.7.0 → 0.7.1
raw patch · 12 files changed
+168/−35 lines, 12 files
Files
- CHANGELOG.markdown +5/−0
- src/Codec/Xlsx/Formatted.hs +1/−0
- src/Codec/Xlsx/Parser.hs +17/−4
- src/Codec/Xlsx/Parser/Internal/Fast.hs +29/−0
- src/Codec/Xlsx/Parser/Internal/PivotTable.hs +20/−3
- src/Codec/Xlsx/Types/Internal.hs +5/−0
- src/Codec/Xlsx/Types/PivotTable/Internal.hs +26/−1
- src/Codec/Xlsx/Writer.hs +15/−4
- src/Codec/Xlsx/Writer/Internal.hs +2/−3
- src/Codec/Xlsx/Writer/Internal/PivotTable.hs +34/−9
- test/PivotTableTests.hs +13/−10
- xlsx.cabal +1/−1
CHANGELOG.markdown view
@@ -1,3 +1,8 @@+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
src/Codec/Xlsx/Formatted.hs view
@@ -6,6 +6,7 @@ module Codec.Xlsx.Formatted ( FormattedCell(..) , Formatted(..)+ , Format(..) , formatted , toFormattedCells , CondFormatted(..)
src/Codec/Xlsx/Parser.hs view
@@ -78,9 +78,12 @@ type Parser = Either ParseError -- | Reads `Xlsx' from raw data (lazy bytestring) using @xeno@ library--- using some "cheating"+-- using some "cheating":+-- -- * not doing 100% xml validation--- * replacing only basic XML enttities+-- * 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@@ -639,10 +642,20 @@ path <- lookupRelPath wbPath wbRels rId bs <- note (MissingFile path) $ Zip.fromEntry <$> Zip.findEntryByPath path ar- sources <-+ (sheet, ref, fields0, mRecRId) <- note (InconsistentXlsx $ "Bad pivot table cache in " <> T.pack path) $ parseCache bs- return (cacheId, sources)+ fields <- case mRecRId of+ Just recId -> do+ cacheRels <- getRels ar path+ recsPath <- lookupRelPath path cacheRels recId+ rCur <- xmlCursorRequired ar recsPath+ let recs = rCur $/ element (n_ "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 (n_ "workbookPr") >=> fromAttribute "date1904" return (sheets, DefinedNames names, caches, dateBase)
src/Codec/Xlsx/Parser/Internal/Fast.hs view
@@ -35,9 +35,11 @@ import Control.Arrow (second) import Control.Exception (Exception, throw) import Control.Monad (ap, forM, join, liftM)+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)@@ -277,9 +279,36 @@ && 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
src/Codec/Xlsx/Parser/Internal/PivotTable.hs view
@@ -4,11 +4,13 @@ module Codec.Xlsx.Parser.Internal.PivotTable ( parsePivotTable , parseCache+ , fillCacheFieldsFromRecords ) where import Control.Applicative import Data.ByteString.Lazy (ByteString)-import Data.Maybe (listToMaybe, maybeToList)+import Data.List (transpose)+import Data.Maybe (listToMaybe, mapMaybe, maybeToList) import Data.Text (Text) import Safe (atMay) import Text.XML@@ -16,6 +18,8 @@ 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 @@ -79,14 +83,27 @@ FieldPosition <$> fieldNameList n return PivotTable {..} -parseCache :: ByteString -> Maybe (Text, CellRef, [CacheField])+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 (n_ "cacheSource") &/ element (n_ "worksheetSource") >=> liftA2 (,) <$> attribute "sheet" <*> fromAttribute "ref" let fields = cur $/ element (n_ "cacheFields") &/ element (n_ "cacheField") >=> fromCursor- return (sheet, ref, fields)+ 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/Types/Internal.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-} module Codec.Xlsx.Types.Internal where import Control.Arrow+import Data.Monoid ((<>)) import Data.Text (Text) import GHC.Generics (Generic) @@ -18,3 +20,6 @@ instance FromAttrBs RefId where fromAttrBs = fmap RefId . fromAttrBs++unsafeRefId :: Int -> RefId+unsafeRefId num = RefId $ "rId" <> txti num
src/Codec/Xlsx/Types/PivotTable/Internal.hs view
@@ -4,12 +4,16 @@ 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 @@ -25,6 +29,14 @@ , cfItems :: [CellValue] } deriving (Eq, Show, Generic) +data CacheRecordValue+ = CacheText Text+ | CacheNumber Double+ | CacheIndex Int+ deriving (Eq, Show, Generic)++type CacheRecord = [CacheRecordValue]+ {------------------------------------------------------------------------------- Parsing -------------------------------------------------------------------------------}@@ -50,6 +62,17 @@ attributeV :: FromAttrVal a => [a] attributeV = fromAttribute "v" cur +recordValueFromNode :: Node -> [CacheRecordValue]+recordValueFromNode n+ | n `nodeElNameIs` (n_ "s") = CacheText <$> attributeV+ | n `nodeElNameIs` (n_ "n") = CacheNumber <$> attributeV+ | n `nodeElNameIs` (n_ "x") = CacheIndex <$> attributeV+ | otherwise = fail "not valid cache record value"+ where+ cur = fromNode n+ attributeV :: FromAttrVal a => [a]+ attributeV = fromAttribute "v" cur+ {------------------------------------------------------------------------------- Rendering -------------------------------------------------------------------------------}@@ -58,7 +81,9 @@ toElement nm CacheField {..} = elementList nm ["name" .= cfName] [sharedItems] where- sharedItems = elementList "sharedItems" typeAttrs $ map cvToItem cfItems+ -- 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"
src/Codec/Xlsx/Writer.hs view
@@ -156,9 +156,6 @@ modifySTRef' r (+1) return (unsafeRefId num) -unsafeRefId :: Int -> RefId-unsafeRefId num = RefId $ "rId" <> txti num- sheetDataXml :: Cells -> Map Int RowProperties@@ -350,6 +347,20 @@ (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 =@@ -360,7 +371,7 @@ relsCT "relationships" $ renderRels [refFileDataToRel tablePath (unsafeRefId 1, cacheFile)]- in ((cacheId, cacheFile), tableFile, [tableRels])+ in ((cacheId, cacheFile), tableFile, [tableRels, cacheRelsFile, recordsFile]) genTable :: Table -> Int -> FileData genTable tbl tblId = FileData{..}
src/Codec/Xlsx/Writer/Internal.hs view
@@ -38,15 +38,14 @@ , justFalse ) where +import qualified Data.Map as Map+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 qualified Data.Map as Map-import Data.String (fromString) import Text.XML {-------------------------------------------------------------------------------
src/Codec/Xlsx/Writer/Internal/PivotTable.hs view
@@ -7,6 +7,7 @@ ) 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)@@ -17,6 +18,8 @@ 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@@ -24,6 +27,7 @@ data PivotTableFiles = PivotTableFiles { pvtfTable :: ByteString , pvtfCacheDefinition :: ByteString+ , pvtfCacheRecords :: ByteString } deriving (Eq, Show, Generic) data CacheDefinition = CacheDefinition@@ -37,7 +41,9 @@ where pvtfTable = renderLBS def $ ptDefinitionDocument cacheId cache t cache = generateCache cm t- pvtfCacheDefinition = renderLBS def $ toDocument cache+ (cacheDoc, cacheRecordsDoc) = writeCache cache+ pvtfCacheDefinition = renderLBS def cacheDoc+ pvtfCacheRecords = renderLBS def cacheRecordsDoc ptDefinitionDocument :: Int -> CacheDefinition -> PivotTable -> Document ptDefinitionDocument cacheId cache t =@@ -157,15 +163,13 @@ let values = mapMaybe (\r -> getCellValue (r, c)) [(r1 + 1) .. r2] return (PivotFieldName nm, nubOrd values) -instance ToDocument CacheDefinition where- toDocument =- documentFromElement "Pivot cache definition generated by xlsx" .- toElement "pivotCacheDefinition"--instance ToElement CacheDefinition where- toElement nm CacheDefinition {..} = elementList nm attrs elements+writeCache :: CacheDefinition -> (Document, Document)+writeCache CacheDefinition {..} = (cacheDefDoc, cacheRecordsDoc) where- attrs = ["invalid" .= True, "refreshOnLoad" .= True]+ 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@@ -177,3 +181,24 @@ ] 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
test/PivotTableTests.hs view
@@ -18,6 +18,7 @@ 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 @@ -38,7 +39,14 @@ ref = CellRef "A1:D5" forCacheId (CacheId 3) = Just (sheetName, ref, testPivotCacheFields) forCacheId _ = Nothing- Just (sheetName, ref, testPivotCacheFields) @==?+ -- 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@@ -145,6 +153,7 @@ 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">@@ -157,19 +166,13 @@ </sharedItems> </cacheField> <cacheField name="Year">- <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0">- <n v="2012"/><n v="2011"/>- </sharedItems>+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/> </cacheField> <cacheField name="Price">- <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0">- <n v="12.23"/><n v="73.99"/><n v="10.19"/><n v="34.99"/>- </sharedItems>+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/> </cacheField> <cacheField name="Count">- <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0">- <n v="17"/><n v="21"/><n v="172"/><n v="49"/>- </sharedItems>+ <sharedItems containsNumber="1" containsString="0" containsSemiMixedTypes="0"/> </cacheField> </cacheFields> </pivotCacheDefinition>
xlsx.cabal view
@@ -1,6 +1,6 @@ Name: xlsx -Version: 0.7.0+Version: 0.7.1 Synopsis: Simple and incomplete Excel file parser/writer Description: