gdritter repos bricoleur / 309076d
ormolu Getty Ritter 3 years ago
7 changed file(s) with 242 addition(s) and 197 deletion(s). Collapse all Expand all
11 -- «front-matter»
22 module Main where
3
34 -- «end»
45
56 -- «functions»
67 main :: IO ()
78 main = return ()
9
810 -- «end»
11 {-# LANGUAGE OverloadedStrings #-}
22
3 module Bricoleur.Commands.Splice(runSplice) where
3 module Bricoleur.Commands.Splice (runSplice) where
44
5 import Control.Monad (foldM)
5 import Bricoleur.Config
6 import Bricoleur.Opts
7 import Bricoleur.Utils
8 import Control.Monad (foldM)
69 import qualified Data.Map as M
710 import qualified Data.Text as T
811 import qualified Data.Text.Lazy as TL
912 import qualified Data.Text.Lazy.IO as TL
10 import System.FilePath ((</>))
13 import System.FilePath ((</>))
1114 import qualified System.FilePath as Sys
12
13 import Bricoleur.Config
14 import Bricoleur.Opts
15 import Bricoleur.Utils
1615
1716 findFragmentIdentifiers :: TL.Text -> [T.Text]
1817 findFragmentIdentifiers t
19 | (_, rs) <- TL.break (== '«') t
20 , (f, rs') <- TL.break (== '»') (TL.drop 1 rs)
21 , not (TL.null f) = TL.toStrict f : findFragmentIdentifiers rs'
18 | (_, rs) <- TL.break (== '«') t,
19 (f, rs') <- TL.break (== '»') (TL.drop 1 rs),
20 not (TL.null f) =
21 TL.toStrict f : findFragmentIdentifiers rs'
2222 | otherwise = []
2323
2424 newtype Fragment = Fragment [T.Text] deriving (Eq, Show)
2828
2929 findFragments :: FilePath -> T.Text -> [Source] -> Either TL.Text (IO TL.Text)
3030 findFragments root s sources = case (T.splitOn "/" s) of
31 [] -> throw "Invalid empty fragment name!"
32 (x:rs) -> case [ src
33 | src@Source { sourceName = n } <- sources
34 , n == x
31 [] -> throw "Invalid empty fragment name!"
32 (x : rs) -> case [ src
33 | src@Source {sourceName = n} <- sources,
34 n == x
3535 ] of
36 [src] -> go src rs (sourceExpose src)
37 [] -> throw ("Unable to find source named " % stext) x
38 _ -> throw ("Ambiguous source name: " % stext) x
36 [src] -> go src rs (sourceExpose src)
37 [] -> throw ("Unable to find source named " % stext) x
38 _ -> throw ("Ambiguous source name: " % stext) x
3939 where
40
4140 go q [] (ExposeFile path) =
4241 return (cOpenFile "file source" (root </> sourceDir q </> path))
43 go _ [] ExposeSections{} =
44 throw ("Splice identifier «" % stext %
45 "» matches a file with sections") s
46 go _ [] NamedMap{} =
47 throw ("Splice identifier «" % stext %
48 "» matches a map, but does not specify a key!") s
49
42 go _ [] ExposeSections {} =
43 throw
44 ( "Splice identifier «" % stext
45 % "» matches a file with sections"
46 )
47 s
48 go _ [] NamedMap {} =
49 throw
50 ( "Splice identifier «" % stext
51 % "» matches a map, but does not specify a key!"
52 )
53 s
5054 go q [section] (ExposeSections path) =
5155 return (readSection (root </> sourceDir q </> path) section)
52
53 go _ (_:_) (ExposeSections path) =
54 throw ("Splice identifier «" % stext %
55 "» indexes too far into the path " % string % "!")
56 s path
57
58 go _ (_:_) (ExposeFile path) =
59 throw ("Splice identifier «" % stext %
60 "» indexes too far into the file " % string % "!")
61 s path
62
63 go q (k:rs) (NamedMap m)
56 go _ (_ : _) (ExposeSections path) =
57 throw
58 ( "Splice identifier «" % stext
59 % "» indexes too far into the path "
60 % string
61 % "!"
62 )
63 s
64 path
65 go _ (_ : _) (ExposeFile path) =
66 throw
67 ( "Splice identifier «" % stext
68 % "» indexes too far into the file "
69 % string
70 % "!"
71 )
72 s
73 path
74 go q (k : rs) (NamedMap m)
6475 | Just e <- M.lookup k m = go q rs e
65 | otherwise = throw
66 ("Splice identifier «" % stext %
67 "» references a key " % stext %
68 " which cannot be found.") s k
76 | otherwise =
77 throw
78 ( "Splice identifier «" % stext
79 % "» references a key "
80 % stext
81 % " which cannot be found."
82 )
83 s
84 k
6985
7086 doReplacements :: FilePath -> [Source] -> TL.Text -> IO TL.Text
7187 doReplacements root e original =
7490 doReplace file frag = do
7591 new <- case findFragments root frag e of
7692 Left err -> cDie ("Fragment error: " % text) err
77 Right x -> x
93 Right x -> x
7894 return (TL.replace (toIdent frag) new file)
7995
8096 readSection :: FilePath -> T.Text -> IO TL.Text
8197 readSection path f = do
8298 contents <- cOpenFile "section source" path
8399 case TL.breakOn (toIdent f) contents of
84 (_, "") -> cDie ("Unable to find section " % shown %
85 " in file " % string) f path
100 (_, "") ->
101 cDie
102 ( "Unable to find section " % shown
103 % " in file "
104 % string
105 )
106 f
107 path
86108 (_, rs) ->
87109 let (_, skipLine) = TL.breakOn "\n" rs
88110 (section, _) = TL.breakOn "«end»" (TL.drop 1 skipLine)
89111 (skipLine', _) = TL.breakOnEnd "\n" section
90 in return (TL.init skipLine')
112 in return (TL.init skipLine')
91113
92114 runSplice :: Config -> Options -> IO ()
93115 runSplice conf opts = do
22
33 module Bricoleur.Commands.Test (runTest) where
44
5 import Control.Monad (forM_)
5 import Bricoleur.Config
6 import Bricoleur.Opts
7 import Bricoleur.Utils
8 import Control.Monad (forM_)
69 import qualified Data.ByteString.Lazy.Char8 as B
710 import qualified GHC.IO.Exception as Exn
8 import System.FilePath ((</>))
11 import System.FilePath ((</>))
912 import qualified System.FilePath as Sys
1013 import qualified System.Process as Sys
11
12 import Bricoleur.Opts
13 import Bricoleur.Config
14 import Bricoleur.Utils
15
1614
1715 runTest :: Config -> Options -> IO ()
1816 runTest conf opts = do
1917 let root = Sys.takeDirectory (optFile opts)
2018 cDebug ("running tests for " % shown) (confDocument conf)
21 forM_ (confSources conf) $ \ samp -> do
19 forM_ (confSources conf) $ \samp -> do
2220 cDebug ("- running test for " % stext) (sourceName samp)
2321 runCommand root samp
24
2522
2623 runCommand :: FilePath -> Source -> IO ()
2724 runCommand root src = do
2926 let dir = root </> sourceDir src
3027 cDebug (" $ " % string % " (in '" % string % "')") ln dir
3128 (outH, inH) <- Sys.createPipe
32 let process = (Sys.shell ln) { Sys.cwd = Just (root </> sourceDir src)
33 , Sys.std_out = Sys.UseHandle inH
34 , Sys.std_err = Sys.UseHandle inH
35 }
29 let process =
30 (Sys.shell ln)
31 { Sys.cwd = Just (root </> sourceDir src),
32 Sys.std_out = Sys.UseHandle inH,
33 Sys.std_err = Sys.UseHandle inH
34 }
3635 (_, _, _, p) <- Sys.createProcess process
3736 bufOutput <- B.hGetContents outH
3837 code <- Sys.waitForProcess p
4039 Exn.ExitSuccess -> return ()
4140 Exn.ExitFailure n -> do
4241 let idtext = map (B.append " - ") (B.lines bufOutput)
43 cError ("\nCommand '" % string %
44 "' exited with error (" % shown %
45 ")") ln n
42 cError
43 ( "\nCommand '" % string
44 % "' exited with error ("
45 % shown
46 % ")"
47 )
48 ln
49 n
4650 mapM_ bsErrorLn idtext
11 {-# LANGUAGE OverloadedStrings #-}
22
33 module Bricoleur.Config
4 ( Config(..)
5 , Source(..)
6 , Expose(..)
7 , getConfig
8 , example
9 ) where
4 ( Config (..),
5 Source (..),
6 Expose (..),
7 getConfig,
8 example,
9 )
10 where
1011
11 import Control.Applicative ((<|>))
12 import Control.Applicative ((<|>))
1213 import qualified Data.Adnot as A
1314 import qualified Data.ByteString as B
1415 import qualified Data.Map.Strict as M
1617 import qualified Data.Vector as V
1718
1819 data Config = Config
19 { confDocument :: FilePath
20 , confSources :: [Source]
21 } deriving (Eq, Show)
20 { confDocument :: FilePath,
21 confSources :: [Source]
22 }
23 deriving (Eq, Show)
2224
2325 instance A.FromAdnot Config where
2426 parseAdnot = A.withSumNamed "config file" "document" go
2527 where
2628 go payload
27 | Just file <- payload V.!? 0
28 = Config <$> A.parseAdnot file
29 <*> mapM A.parseAdnot (V.toList (V.tail payload))
29 | Just file <- payload V.!? 0 =
30 Config <$> A.parseAdnot file
31 <*> mapM A.parseAdnot (V.toList (V.tail payload))
3032 | otherwise = Left "expected source file in config"
3133
32
3334 data Source = Source
34 { sourceName :: T.Text
35 , sourceDir :: FilePath
36 , sourceCommands :: [String]
37 , sourceExpose :: Expose
38 } deriving (Eq, Show)
35 { sourceName :: T.Text,
36 sourceDir :: FilePath,
37 sourceCommands :: [String],
38 sourceExpose :: Expose
39 }
40 deriving (Eq, Show)
3941
4042 instance A.FromAdnot Source where
4143 parseAdnot = A.withProduct "source" $ \p -> do
42 name <- p A..: "name"
43 dir <- p A..: "dir"
44 cmds <- p A..: "cmd"
44 name <- p A..: "name"
45 dir <- p A..: "dir"
46 cmds <- p A..: "cmd"
4547 expose <- p A..: "expose"
4648 return (Source name dir cmds expose)
47
4849
4950 data Expose
5051 = ExposeFile FilePath
5152 | ExposeSections FilePath
5253 | NamedMap (M.Map T.Text Expose)
53 deriving (Eq, Show)
54 deriving (Eq, Show)
5455
5556 instance A.FromAdnot Expose where
5657 parseAdnot v = file v <|> sections v <|> namedMap v
5758 where
58 file = A.withSumNamed "exposed fragments" "file" $ \ ps ->
59 file = A.withSumNamed "exposed fragments" "file" $ \ps ->
5960 case V.toList ps of
60 [] -> Left "Expected name for file"
61 [] -> Left "Expected name for file"
6162 [f] -> ExposeFile <$> A.parseAdnot f
62 _ -> Left "Too many arguments to file"
63 _ -> Left "Too many arguments to file"
6364
64 sections = A.withSumNamed "exposed fragments" "sections" $ \ ps ->
65 sections = A.withSumNamed "exposed fragments" "sections" $ \ps ->
6566 case V.toList ps of
66 [] -> Left "Expected name for sections"
67 [] -> Left "Expected name for sections"
6768 [f] -> ExposeSections <$> A.parseAdnot f
68 _ -> Left "Too many arguments to sections"
69 _ -> Left "Too many arguments to sections"
6970
70 namedMap = A.withProduct "exposed fragments" $ \ p ->
71 namedMap = A.withProduct "exposed fragments" $ \p ->
7172 NamedMap <$> mapM A.parseAdnot p
7273
7374 parseConfig :: B.ByteString -> Either String Config
7980 return (parseConfig conf)
8081
8182 example :: Config
82 example = Config
83 { confDocument = "main.md"
84 , confSources =
85 [ Source
86 { sourceName = "rust-sample"
87 , sourceDir = "s1"
88 , sourceCommands = ["cargo clean", "cargo build"]
89 , sourceExpose = ExposeFile "src/main.rs"
90 }
91 , Source
92 { sourceName = "haskell-sample"
93 , sourceDir = "s2"
94 , sourceCommands = ["cabal new-build"]
95 , sourceExpose = ExposeSections "Main.hs"
96 }
97 ]
98 }
83 example =
84 Config
85 { confDocument = "main.md",
86 confSources =
87 [ Source
88 { sourceName = "rust-sample",
89 sourceDir = "s1",
90 sourceCommands = ["cargo clean", "cargo build"],
91 sourceExpose = ExposeFile "src/main.rs"
92 },
93 Source
94 { sourceName = "haskell-sample",
95 sourceDir = "s2",
96 sourceCommands = ["cabal new-build"],
97 sourceExpose = ExposeSections "Main.hs"
98 }
99 ]
100 }
11 module Bricoleur.Opts
2 ( Command(..)
3 , Options(..)
4 , getOpts
5 ) where
2 ( Command (..),
3 Options (..),
4 getOpts,
5 )
6 where
67
7 import Control.Applicative ((<|>))
8 import Control.Applicative ((<|>))
89 import qualified Options.Applicative as Opt
910 import qualified System.Directory as Sys
1011 import qualified System.FilePath as Sys
1213 data Command
1314 = Test
1415 | Splice
15 deriving (Eq, Show)
16 deriving (Eq, Show)
1617
1718 data Options = Options
18 { optFile :: FilePath
19 , optVerbose :: Bool
20 , optCommand :: Command
21 } deriving (Eq, Show)
19 { optFile :: FilePath,
20 optVerbose :: Bool,
21 optCommand :: Command
22 }
23 deriving (Eq, Show)
2224
2325 desc :: String
2426 desc = "Bricoleur: a tool for testing and stiching code into documents"
2527
2628 opts :: Opt.ParserInfo Options
27 opts = Opt.info (p Opt.<**> Opt.helper)
28 (Opt.progDesc desc <>
29 Opt.fullDesc <>
30 Opt.header "arglbargl")
29 opts =
30 Opt.info
31 (p Opt.<**> Opt.helper)
32 ( Opt.progDesc desc
33 <> Opt.fullDesc
34 <> Opt.header "arglbargl"
35 )
3136 where
32 p = Options <$> (path <|> pure "")
33 <*> verbose
34 <*> Opt.subparser (test <> splice)
37 p =
38 Options <$> (path <|> pure "")
39 <*> verbose
40 <*> Opt.subparser (test <> splice)
3541
36 path = Opt.strOption
37 (Opt.short 'f' <>
38 Opt.long "file" <>
39 Opt.metavar "PATH" <>
40 Opt.help "The path to the project file")
42 path =
43 Opt.strOption
44 ( Opt.short 'f'
45 <> Opt.long "file"
46 <> Opt.metavar "PATH"
47 <> Opt.help "The path to the project file"
48 )
4149
42 verbose = Opt.switch
43 (Opt.short 'v' <>
44 Opt.long "verbose" <>
45 Opt.help "Show debug messages")
50 verbose =
51 Opt.switch
52 ( Opt.short 'v'
53 <> Opt.long "verbose"
54 <> Opt.help "Show debug messages"
55 )
4656
47 test = Opt.command "test" $ Opt.info
48 (pure Test Opt.<**> Opt.helper)
49 (Opt.progDesc "test the provided sources")
57 test =
58 Opt.command "test" $
59 Opt.info
60 (pure Test Opt.<**> Opt.helper)
61 (Opt.progDesc "test the provided sources")
5062
51 splice = Opt.command "splice" $ Opt.info
52 (pure Splice Opt.<**> Opt.helper)
53 (Opt.progDesc "splice sources into a final draft")
63 splice =
64 Opt.command "splice" $
65 Opt.info
66 (pure Splice Opt.<**> Opt.helper)
67 (Opt.progDesc "splice sources into a final draft")
5468
5569 getOpts :: IO Options
5670 getOpts = do
5771 cwd <- Sys.getCurrentDirectory
5872 options <- Opt.execParser opts
59 return $ if null (optFile options)
60 then options { optFile = cwd Sys.</> "bricoleur" }
61 else options
73 return $
74 if null (optFile options)
75 then options {optFile = cwd Sys.</> "bricoleur"}
76 else options
11 {-# LANGUAGE OverloadedStrings #-}
22
33 module Bricoleur.Utils
4 ( cOutput
5 , cDebug
6 , cWarn
7 , cError
8 , cDie
9 , bsErrorLn
4 ( cOutput,
5 cDebug,
6 cWarn,
7 cError,
8 cDie,
9 bsErrorLn,
10 cOpenFile,
11 throw,
12 F.format,
13 (F.%),
14 F.stext,
15 F.text,
16 F.string,
17 F.shown,
18 )
19 where
1020
11 , cOpenFile
12
13 , throw
14
15 , F.format
16 , (F.%)
17 , F.stext
18 , F.text
19 , F.string
20 , F.shown
21 ) where
22
23 import qualified Formatting as F
2421 import qualified Data.ByteString.Lazy.Char8 as BS
2522 import qualified Data.Text.Lazy as TL
2623 import qualified Data.Text.Lazy.Builder as TL
2724 import qualified Data.Text.Lazy.IO as TL
25 import qualified Formatting as F
2826 import qualified System.Directory as Sys
2927 import qualified System.Exit as Sys
3028 import qualified System.IO as Sys
3129 import qualified System.Posix.IO as Posix
3230 import qualified System.Posix.Terminal as Posix
33
34 import Prelude (FilePath, IO, Either(Left), ($))
31 import Prelude (Either (Left), FilePath, IO, ($))
3532
3633 -- | Produce a 'Left' value from a format string
3734 throw :: F.Format (Either TL.Text r) a -> a
3835 throw f =
39 F.runFormat f (\ b -> Left (TL.toLazyText b))
40
36 F.runFormat f (\b -> Left (TL.toLazyText b))
4137
4238 stderr :: TL.Text -> IO ()
4339 stderr = TL.hPutStr Sys.stderr
44
4540
4641 -- | Write output to stdout
4742 cOutput :: TL.Text -> IO ()
4843 cOutput = TL.putStrLn
4944
50
5145 -- | Write a debug message to stderr.
5246 cDebug :: F.Format (IO ()) a -> a
53 cDebug msg = F.runFormat msg $ \ b ->
47 cDebug msg = F.runFormat msg $ \b ->
5448 TL.hPutStrLn Sys.stderr (TL.toLazyText b)
55
5649
5750 -- | Write a warning message to stderr. If we are connected to a TTY,
5851 -- then this will write in an orange color.
6053 cWarn msg = F.runFormat msg $ \b -> do
6154 isTTY <- Posix.queryTerminal Posix.stdOutput
6255 if isTTY
63 then do stderr "\x1b[93m"
64 stderr (TL.toLazyText b)
65 stderr "\x1b[39m\n"
56 then do
57 stderr "\x1b[93m"
58 stderr (TL.toLazyText b)
59 stderr "\x1b[39m\n"
6660 else TL.hPutStrLn Sys.stderr (TL.toLazyText b)
67
6861
6962 -- | Write an error message to stderr and exit. If we are connected to
7063 -- a TTY, this message will be in red.
7265 cError msg = F.runFormat msg $ \b -> do
7366 isTTY <- Posix.queryTerminal Posix.stdOutput
7467 if isTTY
75 then do stderr "\x1b[91m"
76 stderr (TL.toLazyText b)
77 stderr "\x1b[39m\n"
68 then do
69 stderr "\x1b[91m"
70 stderr (TL.toLazyText b)
71 stderr "\x1b[39m\n"
7872 else TL.hPutStrLn Sys.stderr (TL.toLazyText b)
7973
8074 -- | Write an error message to stderr and exit. If we are connected to
8175 -- a TTY, this message will be in red.
8276 cDie :: F.Format (IO r) a -> a
83 cDie msg = F.runFormat msg $ \ b -> do
77 cDie msg = F.runFormat msg $ \b -> do
8478 isTTY <- Posix.queryTerminal Posix.stdOutput
8579 if isTTY
86 then do stderr "\x1b[91m"
87 stderr (TL.toLazyText b)
88 stderr "\x1b[39m\n"
80 then do
81 stderr "\x1b[91m"
82 stderr (TL.toLazyText b)
83 stderr "\x1b[39m\n"
8984 else TL.hPutStrLn Sys.stderr (TL.toLazyText b)
9085 Sys.exitFailure
91
9286
9387 bsErrorLn :: BS.ByteString -> IO ()
9488 bsErrorLn bs = do
9589 isTTY <- Posix.queryTerminal Posix.stdOutput
9690 if isTTY
97 then do BS.hPutStr Sys.stderr "\x1b[91m"
98 BS.hPutStr Sys.stderr bs
99 BS.hPutStr Sys.stderr "\x1b[39m\n"
91 then do
92 BS.hPutStr Sys.stderr "\x1b[91m"
93 BS.hPutStr Sys.stderr bs
94 BS.hPutStr Sys.stderr "\x1b[39m\n"
10095 else BS.hPutStrLn Sys.stderr bs
10196
10297 cOpenFile :: TL.Text -> FilePath -> IO TL.Text
10499 exists <- Sys.doesFileExist path
105100 if exists
106101 then TL.readFile path
107 else cDie ("Unable to open " F.% F.text F.%
108 " file at " F.% F.string) purpose path
102 else
103 cDie
104 ( "Unable to open " F.% F.text
105 F.% " file at "
106 F.% F.string
107 )
108 purpose
109 path
11 {-# LANGUAGE OverloadedStrings #-}
22
33 module Bricoleur
4 ( main
5 , Opt.getOpts
6 ) where
7
8 import qualified System.Exit as Sys
9
10 import qualified Bricoleur.Config as Conf
11 import qualified Bricoleur.Opts as Opt
4 ( main,
5 Opt.getOpts,
6 )
7 where
128
139 import qualified Bricoleur.Commands.Splice as Cmd
1410 import qualified Bricoleur.Commands.Test as Cmd
11 import qualified Bricoleur.Config as Conf
12 import qualified Bricoleur.Opts as Opt
13 import qualified System.Exit as Sys
1514
1615 -- | Run the main @bricoleur@ function with the provided options.
1716 main :: Opt.Options -> IO ()
2120 Left err -> Sys.die err
2221 Right x -> return x
2322 case Opt.optCommand opts of
24 Opt.Test -> Cmd.runTest config opts
23 Opt.Test -> Cmd.runTest config opts
2524 Opt.Splice -> Cmd.runSplice config opts