ods2csv 0.0 → 0.1
raw patch · 2 files changed
+78/−16 lines, 2 filesdep +bytestringdep +utf8-stringdep +zip-archive
Dependencies added: bytestring, utf8-string, zip-archive
Files
- ods2csv.cabal +25/−3
- src/Main.hs +53/−13
ods2csv.cabal view
@@ -1,5 +1,5 @@ Name: ods2csv-Version: 0.0+Version: 0.1 Synopsis: Convert Open Document Spreadsheet ODS to CSV Description: Usually you would convert ODS to CSV via @libreoffice --headless@@@ -8,13 +8,32 @@ and has incomprehensible CSV export command-line options and selection of individual tables is inferior. .- This program quickly scans through a FODS document using a XML parser+ This program quickly scans through a FODS or ODS document using a XML parser and watches only the necessary data. It does not interpret or evaluate the formula data, instead it assumes that the contained evaluated values are correct. This is true, if the file was saved from LibreOffice but might not be true if generated or manipulated by other tools. .+ You can feed generated CSV files to LaTeX @datatool.sty@, @csvmidi@,+ or @csvreplace@ from Haskell package @spreadsheet@.+ .+ Example runs of the program:+ .+ > ods2csv --list-tables input.ods+ .+ > ods2csv --sheetnumber=1 input.ods >output.csv+ .+ > ods2csv --sheetname="Table 2" --separator=TAB input.ods >output.tsv+ .+ > ods2csv --sheetnumber=2 input.fods >output.csv+ .+ > ods2csv --sheetnumber=2 <input.fods >output.csv+ .+ The program supports UTF-8 encoding for FODS/XML input,+ and applies Latin1 decoding otherwise.+ For writing CSV it uses the system's default encoding.+ . ToDo: Provide FODS parser as library function, maybe in @spreadsheet@. Homepage: https://hub.darcs.net/thielema/ods2csv License: BSD3@@ -26,7 +45,7 @@ Cabal-Version: >=1.10 Source-Repository this- Tag: 0.0+ Tag: 0.1 Type: darcs Location: https://hub.darcs.net/thielema/ods2csv @@ -36,11 +55,14 @@ Executable ods2csv Build-Depends:+ zip-archive >=0.4.3 && <0.5, spreadsheet >=0.1.3 && <0.2, tagchup >=0.4 && <0.5, xml-basic >=0.1.1 && <0.2, shell-utility >=0.1 && <0.2, optparse-applicative >=0.11 && <0.19,+ utf8-string >=1.0.2 && <1.1,+ bytestring >=0.10 && <0.13, non-empty >=0.3.4 && <0.4, utility-ht >=0.0.10 && <0.1, base >=4.5 && <5
src/Main.hs view
@@ -6,16 +6,24 @@ import qualified Options.Applicative as OP import qualified Text.HTML.Tagchup.Parser as TagParser+import qualified Text.HTML.Tagchup.Process as TagProc import qualified Text.HTML.Tagchup.Tag.Match as TagMatch import qualified Text.HTML.Tagchup.Tag as Tag import qualified Text.XML.Basic.Attribute as Attr-import qualified Text.XML.Basic.Name.MixedCase as Name+import qualified Text.XML.Basic.Name.MixedCase as Name -- Qualified would also be appropriate import qualified Text.XML.Basic.Name as NameC +import qualified Codec.Archive.Zip as Zip+import qualified Data.ByteString.Lazy.UTF8 as B_UTF8+import qualified Data.ByteString.Lazy.Char8 as BC+ import qualified Data.Spreadsheet as Spreadsheet import qualified Data.NonEmpty.Mixed as NonEmptyM import qualified Data.NonEmpty as NonEmpty import qualified Data.List.HT as ListHT+import qualified Data.List as List+import qualified Data.Char as Char+import Data.Function.HT (Id) import Data.Tuple.HT (mapSnd) import Data.Maybe (mapMaybe) import Data.Monoid ((<>))@@ -27,6 +35,35 @@ +readByteContent :: FilePath -> IO BC.ByteString+readByteContent path =+ if List.isSuffixOf ".ods" path+ then do+ archive <- Zip.toArchive <$> BC.readFile path+ case filter (("content.xml"==) . Zip.eRelativePath) $+ Zip.zEntries archive of+ [] -> exitFailureMsg "content.xml missing in ODS file"+ -- FixMe: Zip.fromEntry is a partial function+ contentEntry : _ -> return $ Zip.fromEntry contentEntry+ else BC.readFile path+++fixNameType :: Id [Tag.T Name.T String]+fixNameType = id++{-+In principle it would be fine to ignore encoding, that is,+read and write bytestrings and thus transfer encoding from XML to CSV.+However, we need to match table names.+-}+decode :: BC.ByteString -> String+decode bytes =+ case fmap (map Char.toLower) $ TagProc.getXMLEncoding $+ fixNameType $ TagParser.runSoup $ BC.take 10000 bytes of+ Just "utf-8" -> B_UTF8.toString bytes+ _ -> BC.unpack bytes++ maybeTableName :: Tag.T Name.T String -> Maybe String maybeTableName tag = do (foundName, attrs) <- Tag.maybeOpen tag@@ -48,9 +85,10 @@ extractTablesNames :: [Tag.T Name.T String] -> [String] extractTablesNames = mapMaybe maybeTableName -listTables :: FilePath -> IO ()-listTables input =- mapM_ putStrLn . extractTablesNames . TagParser.runSoup =<< readFile input+listTables :: IO BC.ByteString -> IO ()+listTables readInput =+ mapM_ putStrLn . extractTablesNames . TagParser.runSoup . decode+ =<< readInput extractTablesContents :: [Tag.T Name.T String] -> [(String, [[String]])]@@ -81,20 +119,20 @@ (TagMatch.openNameLit "table:table-row"))) . snd . ListHT.segmentBeforeJust maybeTableName -contentFromTables :: Char -> FilePath -> IO ()-contentFromTables separator input =+contentFromTables :: Char -> IO BC.ByteString -> IO ()+contentFromTables separator readInput = mapM_ (\(tableName, content) -> do putStrLn "" putStrLn tableName putStrLn (Spreadsheet.toString '"' separator content)) . extractTablesContents .- TagParser.runSoup- =<< readFile input+ TagParser.runSoup . decode+ =<< readInput -contentFromTable :: Char -> Either String Int -> FilePath -> IO ()-contentFromTable separator tableSelector input = do- tables <- extractTablesContents . TagParser.runSoup <$> readFile input+contentFromTable :: Char -> Either String Int -> IO BC.ByteString -> IO ()+contentFromTable separator tableSelector readInput = do+ tables <- extractTablesContents . TagParser.runSoup . decode <$> readInput putStr . Spreadsheet.toString '"' separator =<< case filter (\(tableId, (tableName, _content)) -> either (tableName==) (tableId==) tableSelector) $@@ -103,7 +141,7 @@ _ -> exitFailureMsg $ "table with " ++- either (printf "number %d") (printf "name %s") tableSelector +++ either (printf "name %s") (printf "number %d") tableSelector ++ " not found" @@ -140,8 +178,10 @@ OP.help "Select table by number") ))) <*>- OP.strArgument+ OP.argument+ (OP.maybeReader $ Just . readByteContent) (OP.metavar "INPUT" <>+ OP.value BC.getContents <> OP.help "Input Document") info :: OP.Parser a -> OP.ParserInfo a