ods2csv (empty) → 0.0
raw patch · 4 files changed
+239/−0 lines, 4 filesdep +basedep +non-emptydep +optparse-applicativesetup-changed
Dependencies added: base, non-empty, optparse-applicative, shell-utility, spreadsheet, tagchup, utility-ht, xml-basic
Files
- LICENSE +30/−0
- Setup.lhs +3/−0
- ods2csv.cabal +51/−0
- src/Main.hs +155/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2023, Henning Thielemann++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Henning Thielemann nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,3 @@+#! /usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ ods2csv.cabal view
@@ -0,0 +1,51 @@+Name: ods2csv+Version: 0.0+Synopsis: Convert Open Document Spreadsheet ODS to CSV+Description:+ Usually you would convert ODS to CSV via @libreoffice --headless@+ but this conflicts with running GUI instances of @libreoffice@+ and requires a full libreoffice installation+ 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+ 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.+ .+ ToDo: Provide FODS parser as library function, maybe in @spreadsheet@.+Homepage: https://hub.darcs.net/thielema/ods2csv+License: BSD3+License-File: LICENSE+Author: Henning Thielemann+Maintainer: haskell@henning-thielemann.de+Category: Text, CSV+Build-Type: Simple+Cabal-Version: >=1.10++Source-Repository this+ Tag: 0.0+ Type: darcs+ Location: https://hub.darcs.net/thielema/ods2csv++Source-Repository head+ Type: darcs+ Location: https://hub.darcs.net/thielema/ods2csv++Executable ods2csv+ Build-Depends:+ 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,+ non-empty >=0.3.4 && <0.4,+ utility-ht >=0.0.10 && <0.1,+ base >=4.5 && <5+ Hs-Source-Dirs: src+ Main-Is: Main.hs+ Default-Language: Haskell2010+ GHC-Options:+ -Wall -fwarn-incomplete-uni-patterns -fwarn-tabs
+ src/Main.hs view
@@ -0,0 +1,155 @@+module Main where++import Shell.Utility.ParseArgument (parseNumber)+import Shell.Utility.Exit (exitFailureMsg)++import qualified Options.Applicative as OP++import qualified Text.HTML.Tagchup.Parser as TagParser+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 as NameC++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 Data.Tuple.HT (mapSnd)+import Data.Maybe (mapMaybe)+import Data.Monoid ((<>))++import Control.Monad (join, guard)+import Control.Applicative (pure, (<*>), (<|>))++import Text.Printf (printf)++++maybeTableName :: Tag.T Name.T String -> Maybe String+maybeTableName tag = do+ (foundName, attrs) <- Tag.maybeOpen tag+ guard $ NameC.match "table:table" foundName+ Attr.lookupLit "table:name" attrs++maybeCell :: Tag.T Name.T String -> Maybe Int+maybeCell tag = do+ (foundName, attrs) <- Tag.maybeOpen tag+ guard $ NameC.match "table:table-cell" foundName+ case fmap reads $ Attr.lookupLit "table:number-columns-repeated" attrs of+ Just [(n,_)] -> return n+ _ -> return 1++{-+<config:config-item-map-entry config:name="BlaBlaBla">+<table:table table:name="BlaBlaBla" table:style-name="ta1">+-}+extractTablesNames :: [Tag.T Name.T String] -> [String]+extractTablesNames = mapMaybe maybeTableName++listTables :: FilePath -> IO ()+listTables input =+ mapM_ putStrLn . extractTablesNames . TagParser.runSoup =<< readFile input+++extractTablesContents :: [Tag.T Name.T String] -> [(String, [[String]])]+extractTablesContents =+ map (mapSnd (+ map+ (concatMap+ (uncurry $ \n ->+ (\texts ->+ replicate n $+ case texts of+ text:_ -> text+ [] -> "")+ .+ mapMaybe Tag.maybeText+ .+ dropWhile (not . TagMatch.openNameLit "text:p"))+ .+ snd+ .+ ListHT.segmentBeforeJust maybeCell+ .+ NonEmpty.tail)+ .+ snd+ .+ NonEmptyM.segmentBefore+ (TagMatch.openNameLit "table:table-row"))) .+ snd . ListHT.segmentBeforeJust maybeTableName++contentFromTables :: Char -> FilePath -> IO ()+contentFromTables separator input =+ mapM_+ (\(tableName, content) -> do+ putStrLn ""+ putStrLn tableName+ putStrLn (Spreadsheet.toString '"' separator content)) .+ extractTablesContents .+ TagParser.runSoup+ =<< readFile input++contentFromTable :: Char -> Either String Int -> FilePath -> IO ()+contentFromTable separator tableSelector input = do+ tables <- extractTablesContents . TagParser.runSoup <$> readFile input+ putStr . Spreadsheet.toString '"' separator =<<+ case filter (\(tableId, (tableName, _content)) ->+ either (tableName==) (tableId==) tableSelector) $+ zip [1..] tables of+ (_,(_,found)):_ -> return found+ _ ->+ exitFailureMsg $+ "table with " +++ either (printf "number %d") (printf "name %s") tableSelector +++ " not found"+++parser :: OP.Parser (IO ())+parser =+ ((OP.flag' listTables $+ OP.long "list-tables" <>+ OP.help "List all tables in an ODS document")+ <|>+ (pure contentFromTable+ <*>+ (OP.option+ (OP.eitherReader (\str ->+ case str of+ "TAB" -> Right '\t'+ [c] -> Right c+ _ -> Left "separator must be one character")) $+ OP.long "separator" <>+ OP.metavar "CHAR" <>+ OP.value ',' <>+ OP.help "CSV separator, TAB for tabulator")+ <*>+ (+ (fmap Left $+ OP.strOption $+ OP.long "sheetname" <>+ OP.metavar "NAME" <>+ OP.help "Select table by name")+ <|>+ (fmap Right $+ OP.option (OP.eitherReader $ parseNumber "page" (0<) "positive") $+ OP.long "sheetnumber" <>+ OP.metavar "ONEBASED" <>+ OP.help "Select table by number")+ )))+ <*>+ OP.strArgument+ (OP.metavar "INPUT" <>+ OP.help "Input Document")++info :: OP.Parser a -> OP.ParserInfo a+info p =+ OP.info+ (OP.helper <*> p)+ (OP.fullDesc <>+ OP.progDesc "Convert Open Document Spreadsheet ODS to CSV.")++main :: IO ()+main = join $ OP.execParser $ info parser