1{-# LANGUAGE TupleSections #-}
5import Control.Applicative
6import Control.Monad.Trans.Except (runExceptT)
7import Control.Arrow (first, second)
8import Control.Category ((>>>))
9import Control.Monad.Writer
10import Data.Text (Text)
11import qualified Data.Text as T
12import qualified Data.Text.IO as T
13import Data.Function (on)
15import Data.Maybe (fromMaybe)
16import Data.Tuple (swap)
17import Data.Version (showVersion)
19import Options.Applicative
20import qualified Text.PrettyPrint.ANSI.Leijen as PP
22import qualified Language.PureScript as P
23import qualified Paths_purescript as Paths
24import System.Exit (exitFailure)
25import System.IO (hPutStrLn, hPrint, hSetEncoding, stderr, stdout, utf8)
26import System.IO.UTF8 (readUTF8FileT, writeUTF8FileT)
27import System.Directory (createDirectoryIfMissing)
28import System.FilePath (takeDirectory)
29import System.FilePath.Glob (glob)
33import qualified Language.PureScript.Docs as D
34import qualified Language.PureScript.Docs.AsMarkdown as D
36-- Available output formats
37data Format = Markdown -- Output documentation in Markdown format
38 | Ctags -- Output ctags symbol index suitable for use with vi
39 | Etags -- Output etags symbol index suitable for use with emacs
40 deriving (Show, Eq, Ord)
42-- | Available methods of outputting Markdown documentation
45 | ToStdOut [P.ModuleName]
46 | ToFiles [(P.ModuleName, FilePath)]
49data PSCDocsOptions = PSCDocsOptions
50 { pscdFormat :: Format
51 , pscdInputFiles :: [FilePath]
52 , pscdDocgen :: DocgenOutput
56docgen :: PSCDocsOptions -> IO ()
57docgen (PSCDocsOptions fmt inputGlob output) = do
58 input <- concat <$> mapM glob inputGlob
60 Etags -> dumpTags input dumpEtags
61 Ctags -> dumpTags input dumpCtags
63 ms <- runExceptT (D.parseFilesInPackages input []
64 >>= uncurry D.convertModulesInPackage)
69 T.putStrLn (D.runDocs (D.modulesAsMarkdown ms))
71 let (ms', missing) = takeByName ms names
73 T.putStrLn (D.runDocs (D.modulesAsMarkdown ms'))
75 let (ms', missing) = takeByName' ms names
78 let ms'' = groupBy ((==) `on` fst) . sortBy (compare `on` fst) $ map swap ms'
79 forM_ ms'' $ \grp -> do
80 let fp = fst (head grp)
81 createDirectoryIfMissing True (takeDirectory fp)
82 writeUTF8FileT fp (D.runDocs (D.modulesAsMarkdown (map snd grp)))
85 guardMissing [] = return ()
86 guardMissing [mn] = do
87 hPutStrLn stderr ("psc-docs: error: unknown module \"" ++ T.unpack (P.runModuleName mn) ++ "\"")
90 hPutStrLn stderr "psc-docs: error: unknown modules:"
92 hPutStrLn stderr (" * " ++ T.unpack (P.runModuleName mn))
95 successOrExit :: Either P.MultipleErrors a -> IO a
101 hPutStrLn stderr $ P.prettyPrintMultipleErrors P.defaultPPEOptions err
104 takeByName = takeModulesByName D.modName
105 takeByName' = takeModulesByName' D.modName
108-- Given a list of module names and a list of modules, return a list of modules
109-- whose names appeared in the given name list, together with a list of names
110-- for which no module could be found in the module list.
112takeModulesByName :: (Eq n) => (m -> n) -> [m] -> [n] -> ([m], [n])
113takeModulesByName getModuleName modules names =
114 first (map fst) (takeModulesByName' getModuleName modules (map (,()) names))
117-- Like takeModulesByName, but also keeps some extra information with each
120takeModulesByName' :: (Eq n) => (m -> n) -> [m] -> [(n, a)] -> ([(m, a)], [n])
121takeModulesByName' getModuleName modules = foldl go ([], [])
123 go (ms, missing) (name, x) =
124 case find ((== name) . getModuleName) modules of
125 Just m -> ((m, x) : ms, missing)
126 Nothing -> (ms, name : missing)
128dumpTags :: [FilePath] -> ([(String, P.Module)] -> [String]) -> IO ()
129dumpTags input renderTags = do
130 e <- P.parseModulesFromFiles (fromMaybe "") <$> mapM (fmap (first Just) . parseFile) (nub input)
136 ldump (renderTags (pairs ms))
139 pairs :: [(Maybe String, m)] -> [(String, m)]
140 pairs = map (first (fromMaybe ""))
142 ldump :: [String] -> IO ()
143 ldump = mapM_ putStrLn
145parseFile :: FilePath -> IO (FilePath, Text)
146parseFile input = (,) input <$> readUTF8FileT input
148inputFile :: Parser FilePath
149inputFile = strArgument $
151 <> help "The input .purs file(s)"
153instance Read Format where
154 readsPrec _ "etags" = [(Etags, "")]
155 readsPrec _ "ctags" = [(Ctags, "")]
156 readsPrec _ "markdown" = [(Markdown, "")]
159format :: Parser Format
160format = option auto $ value Markdown
163 <> help "Set output FORMAT (markdown | etags | ctags)"
165docgenModule :: Parser String
166docgenModule = strOption $
168 <> help "A list of module names which should appear in the output. This can optionally include file paths to write individual modules to, by separating with a colon ':'. For example, Prelude:docs/Prelude.md. This option may be specified multiple times."
170pscDocsOptions :: Parser (Format, [FilePath], [String])
171pscDocsOptions = (,,) <$> format <*> many inputFile <*> many docgenModule
173parseDocgen :: [String] -> Either String DocgenOutput
174parseDocgen [] = Right EverythingToStdOut
175parseDocgen xs = go xs
184 = IToStdOut P.ModuleName
185 | IToFile (P.ModuleName, FilePath)
187parseItem :: String -> DocgenOutputItem
188parseItem s = case elemIndex ':' s of
191 >>> first (P.moduleNameFromString . T.pack)
195 IToStdOut (P.moduleNameFromString (T.pack s))
201combine :: [DocgenOutputItem] -> Either String DocgenOutput
202combine [] = Right EverythingToStdOut
203combine (x:xs) = foldM go (initial x) xs
205 initial (IToStdOut m) = ToStdOut [m]
206 initial (IToFile m) = ToFiles [m]
208 go (ToStdOut ms) (IToStdOut m) = Right (ToStdOut (m:ms))
209 go (ToFiles ms) (IToFile m) = Right (ToFiles (m:ms))
210 go _ _ = Left "Can't mix module names and module name/file path pairs in the same invocation."
212buildOptions :: (Format, [FilePath], [String]) -> IO PSCDocsOptions
213buildOptions (fmt, input, mapping) =
214 case parseDocgen mapping of
215 Right mapping' -> return (PSCDocsOptions fmt input mapping')
217 hPutStrLn stderr "psc-docs: error in --docgen option:"
218 hPutStrLn stderr (" " ++ err)
223 hSetEncoding stdout utf8
224 hSetEncoding stderr utf8
225 execParser opts >>= buildOptions >>= docgen
227 opts = info (version <*> helper <*> pscDocsOptions) infoModList
228 infoModList = fullDesc <> headerInfo <> footerInfo
229 headerInfo = header "psc-docs - Generate Markdown documentation from PureScript source files"
230 footerInfo = footerDoc $ Just $ PP.vcat
231 [ examples, PP.empty, PP.text ("psc-docs " ++ showVersion Paths.version) ]
233 version :: Parser (a -> a)
234 version = abortOption (InfoMsg (showVersion Paths.version)) $ long "version" <> help "Show the version number" <> hidden
238 PP.vcat $ map PP.text
240 , " print documentation for Data.List to stdout:"
241 , " psc-docs \"src/**/*.purs\" \".psc-package/*/*/*/src/**/*.purs\" \\"
242 , " --docgen Data.List"
244 , " write documentation for Data.List to docs/Data.List.md:"
245 , " psc-docs \"src/**/*.purs\" \".psc-package/*/*/*/src/**/*.purs\" \\"
246 , " --docgen Data.List:docs/Data.List.md"
248 , " write documentation for Data.List to docs/Data.List.md, and"
249 , " documentation for Data.List.Lazy to docs/Data.List.Lazy.md:"
250 , " psc-docs \"src/**/*.purs\" \".psc-package/*/*/*/src/**/*.purs\" \\"
251 , " --docgen Data.List:docs/Data.List.md \\"
252 , " --docgen Data.List.Lazy:docs/Data.List.Lazy.md"
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list