Browse Source

Initial commit

master
Dylan Baker 2 years ago
commit
dd208b8ed4
16 changed files with 571 additions and 0 deletions
  1. 3
    0
      .gitignore
  2. 19
    0
      LICENSE
  3. 5
    0
      README.md
  4. 2
    0
      Setup.hs
  5. 116
    0
      app/Main.hs
  6. 58
    0
      package.yaml
  7. 61
    0
      src/Build.hs
  8. 23
    0
      src/CLI.hs
  9. 39
    0
      src/Post.hs
  10. 28
    0
      src/Read.hs
  11. 46
    0
      src/Templates.hs
  12. 59
    0
      src/Utils.hs
  13. 25
    0
      src/Write.hs
  14. 66
    0
      stack.yaml
  15. 19
    0
      stack.yaml.lock
  16. 2
    0
      test/Spec.hs

+ 3
- 0
.gitignore View File

@@ -0,0 +1,3 @@
1
+.stack-work/
2
+clover.cabal
3
+*~

+ 19
- 0
LICENSE View File

@@ -0,0 +1,19 @@
1
+Copyright 2019 Dylan Baker
2
+
3
+Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+this software and associated documentation files (the "Software"), to deal in
5
+the Software without restriction, including without limitation the rights to
6
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+of the Software, and to permit persons to whom the Software is furnished to do
8
+so, subject to the following conditions:
9
+
10
+The above copyright notice and this permission notice shall be included in all
11
+copies or substantial portions of the Software.
12
+
13
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+SOFTWARE.

+ 5
- 0
README.md View File

@@ -0,0 +1,5 @@
1
+# clover
2
+
3
+A static site generator. I use this on my own blog but I don't recommend anyone
4
+else use it as it does only exactly what I need it to do and I don't plan on
5
+adding many features.

+ 2
- 0
Setup.hs View File

@@ -0,0 +1,2 @@
1
+import Distribution.Simple
2
+main = defaultMain

+ 116
- 0
app/Main.hs View File

@@ -0,0 +1,116 @@
1
+module Main where
2
+
3
+import           Control.Monad
4
+import           Data.Aeson
5
+import qualified Data.Map                      as M
6
+import qualified Data.Text                     as T
7
+import           Path                           ( parseAbsDir
8
+                                                , toFilePath
9
+                                                )
10
+import           Path.IO                        ( copyDirRecur )
11
+import           System.Console.ArgParser
12
+import           System.Directory
13
+import           System.FilePath
14
+import           Text.Pandoc
15
+
16
+import           Build                          ( buildRawPost
17
+                                                , buildIndexPage
18
+                                                , buildRSSFeed
19
+                                                , buildPostListing
20
+                                                , buildPostListingItem
21
+                                                )
22
+import           CLI                            ( argParser
23
+                                                , Config
24
+                                                , Config(..)
25
+                                                )
26
+import           Post                           ( parsePost
27
+                                                , parsePosts
28
+                                                , renderPost
29
+                                                , renderPosts
30
+                                                , RawPost
31
+                                                , RichPost
32
+                                                )
33
+import           Read                           ( readIndexTemplate
34
+                                                , readPostTemplate
35
+                                                , readRSSTemplate
36
+                                                , readPostContents
37
+                                                )
38
+import           Templates                      ( defaultIndexTemplate
39
+                                                , defaultPostTemplate
40
+                                                )
41
+import           Utils                          ( errorsAndResults
42
+                                                , lookupMeta'
43
+                                                , makeFullFilePath
44
+                                                , sortByDate
45
+                                                )
46
+import           Write                          ( writeIndexPage
47
+                                                , writePost
48
+                                                , writeRSSFeed
49
+                                                )
50
+
51
+setUpBuildDirectory :: FilePath -> IO ()
52
+setUpBuildDirectory path = do
53
+  buildPathExists <- doesDirectoryExist path
54
+  when buildPathExists $ removeDirectoryRecursive path
55
+  createDirectory path
56
+  createDirectory $ joinPath [path, "posts"]
57
+
58
+copyAssetsDir :: FilePath -> FilePath -> IO ()
59
+copyAssetsDir root build = do
60
+  sourcePath       <- parseAbsDir $ joinPath [root, "assets"]
61
+  destPath         <- parseAbsDir $ joinPath [build, "assets"]
62
+  sourcePathExists <- doesDirectoryExist $ toFilePath sourcePath
63
+  when sourcePathExists $ copyDirRecur sourcePath destPath
64
+
65
+build :: FilePath -> Bool -> IO ()
66
+build dir drafts = do
67
+  cwd           <- getCurrentDirectory
68
+  root          <- if dir == "." then makeAbsolute cwd else makeAbsolute dir
69
+  build         <- makeAbsolute $ joinPath [root, "build"]
70
+  indexTemplate <- readIndexTemplate root
71
+  postTemplate  <- readPostTemplate root
72
+  rssTemplate   <- readRSSTemplate root
73
+  postContents  <- readPostContents root drafts
74
+  let (parseErrors, parsedPosts) = errorsAndResults $ parsePosts postContents
75
+  case parseErrors of
76
+    [] ->
77
+      let (renderErrors, renderedPosts) =
78
+              errorsAndResults $ renderPosts postTemplate parsedPosts
79
+      in  case renderErrors of
80
+            [] -> do
81
+              setUpBuildDirectory build
82
+              mapM_ (writePost build) renderedPosts
83
+              case buildIndexPage indexTemplate parsedPosts of
84
+                Left  err       -> putStrLn err
85
+                Right indexPage -> writeIndexPage build indexPage
86
+              case buildRSSFeed rssTemplate parsedPosts of
87
+                Left  err     -> putStrLn err
88
+                Right rssFeed -> writeRSSFeed build rssFeed
89
+              copyAssetsDir root build
90
+            _ -> putStrLn $ unlines $ map show renderErrors
91
+    _ -> putStrLn $ unlines $ map show parseErrors
92
+
93
+new :: String -> IO ()
94
+new name = do
95
+  path            <- makeAbsolute name
96
+  directoryExists <- doesDirectoryExist path
97
+  if directoryExists
98
+    then putStrLn $ "Error: Directory " ++ name ++ " already exists"
99
+    else do
100
+      createDirectory path
101
+      createDirectory $ joinPath [path, "posts"]
102
+      createDirectory $ joinPath [path, "drafts"]
103
+      createDirectory $ joinPath [path, "templates"]
104
+      writeFile (joinPath [path, "templates", "_index.html"])
105
+                defaultIndexTemplate
106
+      writeFile (joinPath [path, "templates", "_post.html"]) defaultPostTemplate
107
+
108
+run :: Config -> IO ()
109
+run config = case config of
110
+  (Build dir drafts) -> build dir drafts
111
+  (New name        ) -> new name
112
+
113
+main :: IO ()
114
+main = do
115
+  config <- argParser
116
+  runApp config run

+ 58
- 0
package.yaml View File

@@ -0,0 +1,58 @@
1
+name: clover
2
+version: 0.1.0.0
3
+github: "githubuser/clover"
4
+license: BSD3
5
+author: "Author name here"
6
+maintainer: "example@example.com"
7
+copyright: "2019 Author name here"
8
+
9
+extra-source-files:
10
+  - README.md
11
+
12
+# Metadata used when publishing your package
13
+# synopsis:            Short description of your package
14
+# category:            Web
15
+
16
+# To avoid duplicated efforts in documentation and dealing with the
17
+# complications of embedding Haddock markup inside cabal files, it is
18
+# common to point users to the README.md file.
19
+description: Please see the README on GitHub at <https://github.com/githubuser/clover#readme>
20
+
21
+dependencies:
22
+  - aeson
23
+  - argparser
24
+  - base >= 4.7 && < 5
25
+  - containers
26
+  - directory
27
+  - doctemplates
28
+  - filepath
29
+  - pandoc
30
+  - path
31
+  - path-io
32
+  - text
33
+  - time
34
+
35
+library:
36
+  source-dirs: src
37
+
38
+executables:
39
+  clover:
40
+    main: Main.hs
41
+    source-dirs: app
42
+    ghc-options:
43
+      - -threaded
44
+      - -rtsopts
45
+      - -with-rtsopts=-N
46
+    dependencies:
47
+      - clover
48
+
49
+tests:
50
+  clover-test:
51
+    main: Spec.hs
52
+    source-dirs: test
53
+    ghc-options:
54
+      - -threaded
55
+      - -rtsopts
56
+      - -with-rtsopts=-N
57
+    dependencies:
58
+      - clover

+ 61
- 0
src/Build.hs View File

@@ -0,0 +1,61 @@
1
+module Build where
2
+
3
+import           Data.Aeson
4
+import qualified Data.Text                     as T
5
+import           Data.Time
6
+import           GHC.Exts                       ( fromList )
7
+import           System.FilePath
8
+import           Text.Pandoc
9
+
10
+import           Post                           ( renderPost
11
+                                                , RawPost
12
+                                                , RichPost
13
+                                                )
14
+import           Utils                          ( escapeHTML
15
+                                                , lookupMeta'
16
+                                                , sortByDate
17
+                                                )
18
+
19
+parseDay :: String -> Day
20
+parseDay = parseTimeOrError True defaultTimeLocale "%Y-%m-%d"
21
+
22
+buildRawPost :: FilePath -> IO RawPost
23
+buildRawPost path = do
24
+  contents <- readFile path
25
+  return (path, T.pack contents)
26
+
27
+buildIndexPage :: String -> [RichPost] -> Either String T.Text
28
+buildIndexPage layout rawPosts = case compileTemplate (T.pack layout) of
29
+  Left  err      -> Left err
30
+  Right template -> Right $ renderTemplate template (buildPostListing rawPosts)
31
+
32
+buildPostListing :: [RichPost] -> Value
33
+buildPostListing posts =
34
+  let postListing = map buildPostListingItem (sortByDate posts)
35
+  in  object [(T.pack "posts", Array $ fromList postListing)]
36
+
37
+buildPostListingItem :: RichPost -> Value
38
+buildPostListingItem (path, Pandoc m b) =
39
+  let keys = ["title", "date", "slug"]
40
+      bindings = map (\key -> T.pack key .= lookupMeta' key m) keys
41
+      body = case renderPost "<article>$body$</article>" (path, Pandoc m b) of
42
+        Left  _error   -> ""
43
+        Right contents -> T.unpack contents
44
+  in  object
45
+        $  bindings
46
+        ++ [ ( T.pack "link"
47
+             , toJSON
48
+               $ T.concat [T.pack "/posts/", lookupMeta' "slug" m, T.pack "/"]
49
+             )
50
+           , (T.pack "body", toJSON $ escapeHTML body)
51
+           , ( T.pack "iso_date"
52
+             , toJSON $ formatTime defaultTimeLocale
53
+                                   "%a, %d %b %Y 00:00:00 +0000"
54
+                                   (parseDay (T.unpack $ lookupMeta' "date" m))
55
+             )
56
+           ]
57
+
58
+buildRSSFeed :: String -> [RichPost] -> Either String T.Text
59
+buildRSSFeed layout posts = case compileTemplate (T.pack layout) of
60
+  Left  err      -> Left err
61
+  Right template -> Right $ renderTemplate template (buildPostListing posts)

+ 23
- 0
src/CLI.hs View File

@@ -0,0 +1,23 @@
1
+module CLI
2
+  ( argParser
3
+  , Config(..)
4
+  )
5
+where
6
+
7
+import           System.Console.ArgParser
8
+
9
+data Config =
10
+  Build FilePath Bool
11
+  | New String
12
+  deriving (Show)
13
+
14
+argParser :: IO (CmdLnInterface Config)
15
+argParser = mkSubParser
16
+  [ ( "build"
17
+    , mkDefaultApp
18
+      (Build `parsedBy` optPos "." "directory" `andBy` boolFlag "drafts")
19
+      "build"
20
+    )
21
+  , ("new", mkDefaultApp (New `parsedBy` reqPos "name") "new")
22
+  ]
23
+

+ 39
- 0
src/Post.hs View File

@@ -0,0 +1,39 @@
1
+module Post where
2
+
3
+import qualified Data.Text                     as T
4
+import           System.FilePath
5
+import           Text.Pandoc
6
+
7
+type RawPost = (FilePath, T.Text)
8
+
9
+type RichPost = (FilePath, Pandoc)
10
+
11
+parsePosts :: [RawPost] -> [(FilePath, Either PandocError Pandoc)]
12
+parsePosts = map (\(path, contents) -> (path, parsePost contents))
13
+
14
+parsePost :: T.Text -> Either PandocError Pandoc
15
+parsePost contents = runPure $ readMarkdown readerOptions contents
16
+
17
+renderPosts :: String -> [RichPost] -> [(FilePath, Either PandocError T.Text)]
18
+renderPosts layout =
19
+  map (\(path, post) -> (path, renderPost layout (path, post)))
20
+
21
+renderPost :: String -> RichPost -> Either PandocError T.Text
22
+renderPost layout (path, post) =
23
+  runPure $ writeHtml5String (writerOptions $ Just layout) post
24
+
25
+readerOptions :: ReaderOptions
26
+readerOptions =
27
+  let extensions = extensionsFromList
28
+        [ Ext_fenced_code_attributes
29
+        , Ext_fenced_code_blocks
30
+        , Ext_footnotes
31
+        , Ext_link_attributes
32
+        , Ext_yaml_metadata_block
33
+        ]
34
+  in  def { readerExtensions = extensions }
35
+
36
+writerOptions :: Maybe String -> WriterOptions
37
+writerOptions layout = case layout of
38
+  Just t  -> def { writerTemplate = Just t }
39
+  Nothing -> def

+ 28
- 0
src/Read.hs View File

@@ -0,0 +1,28 @@
1
+module Read where
2
+
3
+import           System.Directory               ( listDirectory )
4
+import           System.FilePath                ( FilePath )
5
+
6
+import           Build                          ( buildRawPost )
7
+import           Post                           ( RawPost )
8
+import           Utils                          ( makeFullFilePath )
9
+
10
+readPostTemplate :: FilePath -> IO String
11
+readPostTemplate root = readFile $ root ++ "/templates/_post.html"
12
+
13
+readIndexTemplate :: FilePath -> IO String
14
+readIndexTemplate root = readFile $ root ++ "/templates/_index.html"
15
+
16
+readRSSTemplate :: FilePath -> IO String
17
+readRSSTemplate root = readFile $ root ++ "/templates/_rss.xml"
18
+
19
+readPostContents :: String -> Bool -> IO [RawPost]
20
+readPostContents root drafts = do
21
+  postsPath          <- makeFullFilePath root "posts"
22
+  draftsPath         <- makeFullFilePath root "drafts"
23
+  postPaths          <- listDirectory postsPath
24
+  draftPaths         <- listDirectory draftsPath
25
+  absoluteDraftPaths <- mapM (makeFullFilePath draftsPath) draftPaths
26
+  absolutePostPaths  <- mapM (makeFullFilePath postsPath) postPaths
27
+  mapM buildRawPost
28
+       (absolutePostPaths ++ if drafts then absoluteDraftPaths else [])

+ 46
- 0
src/Templates.hs View File

@@ -0,0 +1,46 @@
1
+module Templates
2
+  ( defaultIndexTemplate
3
+  , defaultPostTemplate
4
+  )
5
+where
6
+
7
+defaultIndexTemplate :: String
8
+defaultIndexTemplate = unlines
9
+  [ "<!DOCTYPE html>"
10
+  , "<html lang=\"en\">"
11
+  , "  <head>"
12
+  , "    <meta charset=\"UTF-8\">"
13
+  , "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
14
+  , "    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">"
15
+  , "    <title></title>"
16
+  , "  </head>"
17
+  , "  <body>"
18
+  , "    <ul>"
19
+  , "      $for(posts)$"
20
+  , "        <li>"
21
+  , "          <a href=\"$posts.link$\">$posts.title$</a>"
22
+  , "        </li>"
23
+  , "      $endfor$"
24
+  , "    </ul>"
25
+  , "  </body>"
26
+  , "</html>"
27
+  ]
28
+
29
+defaultPostTemplate :: String
30
+defaultPostTemplate = unlines
31
+  [ "<!DOCTYPE html>"
32
+  , "<html lang=\"en\">"
33
+  , "  <head>"
34
+  , "    <meta charset=\"UTF-8\">"
35
+  , "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
36
+  , "    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">"
37
+  , "    <title>$title$</title>"
38
+  , "  </head>"
39
+  , "  <body>"
40
+  , "    <h1>$title$</h1>"
41
+  , "    <article>"
42
+  , "      $body$"
43
+  , "    </article>"
44
+  , "  </body>"
45
+  , "</html>"
46
+  ]

+ 59
- 0
src/Utils.hs View File

@@ -0,0 +1,59 @@
1
+module Utils
2
+  ( basename
3
+  , errorsAndResults
4
+  , escapeHTML
5
+  , lookupMeta'
6
+  , makeFullFilePath
7
+  , sortByDate
8
+  )
9
+where
10
+
11
+import           Data.Aeson                     ( toJSON
12
+                                                , Value
13
+                                                )
14
+import           Data.List
15
+import           Data.Ord
16
+import qualified Data.Text                     as T
17
+import           System.Directory
18
+import           System.FilePath
19
+import           Text.Pandoc
20
+
21
+basename :: FilePath -> String
22
+basename = dropExtension . takeFileName
23
+
24
+errorsAndResults
25
+  :: [(FilePath, Either PandocError a)] -> ([PandocError], [(FilePath, a)])
26
+errorsAndResults xs =
27
+  let errors  = [ error | result@(_, Left error) <- xs ]
28
+      results = [ (path, a) | result@(path, Right a) <- xs ]
29
+  in  (errors, results)
30
+
31
+lookupMeta' :: String -> Meta -> T.Text
32
+lookupMeta' key meta = case lookupMeta key meta of
33
+  Just value -> renderMeta value
34
+  Nothing    -> T.empty
35
+
36
+makeFullFilePath :: FilePath -> FilePath -> IO FilePath
37
+makeFullFilePath root path = makeAbsolute $ joinPath [root, path]
38
+
39
+renderMeta :: MetaValue -> T.Text
40
+renderMeta (MetaInlines meta) =
41
+  case runPure $ writePlain (def Nothing) (Pandoc nullMeta [Plain meta]) of
42
+    Left  err  -> T.empty
43
+    Right text -> text
44
+
45
+getDate :: Pandoc -> T.Text
46
+getDate (Pandoc m _) = lookupMeta' "date" m
47
+
48
+sortByDate :: [(FilePath, Pandoc)] -> [(FilePath, Pandoc)]
49
+sortByDate = sortBy (flip (comparing $ getDate . snd))
50
+
51
+escapeHTML :: String -> String
52
+escapeHTML = concatMap f
53
+ where
54
+  f '>'  = "&gt;"
55
+  f '<'  = "&lt;"
56
+  f '&'  = "&amp;"
57
+  f '\"' = "&quot;"
58
+  f '\'' = "&#39;"
59
+  f x    = [x]

+ 25
- 0
src/Write.hs View File

@@ -0,0 +1,25 @@
1
+module Write where
2
+
3
+import qualified Data.Text                     as T
4
+import           System.Directory               ( createDirectoryIfMissing
5
+                                                , makeAbsolute
6
+                                                )
7
+import           System.FilePath                ( joinPath )
8
+
9
+import           Post                           ( RawPost )
10
+import           Utils                          ( basename )
11
+
12
+writePost :: FilePath -> RawPost -> IO ()
13
+writePost buildPath (path, contents) = do
14
+  outputDir <- makeAbsolute $ joinPath [buildPath, "posts", basename path]
15
+  let outputFile = joinPath [outputDir, "index.html"]
16
+  createDirectoryIfMissing False outputDir
17
+  writeFile outputFile $ T.unpack contents
18
+
19
+writeIndexPage :: FilePath -> T.Text -> IO ()
20
+writeIndexPage buildPath page =
21
+  writeFile (joinPath [buildPath, "index.html"]) (T.unpack page)
22
+
23
+writeRSSFeed :: FilePath -> T.Text -> IO ()
24
+writeRSSFeed buildPath feed =
25
+  writeFile (joinPath [buildPath, "rss.xml"]) (T.unpack feed)

+ 66
- 0
stack.yaml View File

@@ -0,0 +1,66 @@
1
+# This file was automatically generated by 'stack init'
2
+#
3
+# Some commonly used options have been documented as comments in this file.
4
+# For advanced use and comprehensive documentation of the format, please see:
5
+# https://docs.haskellstack.org/en/stable/yaml_configuration/
6
+
7
+# Resolver to choose a 'specific' stackage snapshot or a compiler version.
8
+# A snapshot resolver dictates the compiler version and the set of packages
9
+# to be used for project dependencies. For example:
10
+#
11
+# resolver: lts-3.5
12
+# resolver: nightly-2015-09-21
13
+# resolver: ghc-7.10.2
14
+#
15
+# The location of a snapshot can be provided as a file or url. Stack assumes
16
+# a snapshot provided as a file might change, whereas a url resource does not.
17
+#
18
+# resolver: ./custom-snapshot.yaml
19
+# resolver: https://example.com/snapshots/2018-01-01.yaml
20
+resolver: lts-13.28
21
+
22
+# User packages to be built.
23
+# Various formats can be used as shown in the example below.
24
+#
25
+# packages:
26
+# - some-directory
27
+# - https://example.com/foo/bar/baz-0.0.2.tar.gz
28
+#   subdirs:
29
+#   - auto-update
30
+#   - wai
31
+packages:
32
+  - .
33
+# Dependency packages to be pulled from upstream that are not in the resolver.
34
+# These entries can reference officially published versions as well as
35
+# forks / in-progress versions pinned to a git hash. For example:
36
+#
37
+# extra-deps:
38
+# - acme-missiles-0.3
39
+# - git: https://github.com/commercialhaskell/stack.git
40
+#   commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
41
+#
42
+extra-deps:
43
+  - argparser-0.3.4
44
+# Override default flag values for local packages and extra-deps
45
+# flags: {}
46
+
47
+# Extra package databases containing global packages
48
+# extra-package-dbs: []
49
+
50
+# Control whether we use the GHC we find on the path
51
+# system-ghc: true
52
+#
53
+# Require a specific version of stack, using version ranges
54
+# require-stack-version: -any # Default
55
+# require-stack-version: ">=2.1"
56
+#
57
+# Override the architecture used by stack, especially useful on Windows
58
+# arch: i386
59
+# arch: x86_64
60
+#
61
+# Extra directories used by stack for building
62
+# extra-include-dirs: [/path/to/dir]
63
+# extra-lib-dirs: [/path/to/dir]
64
+#
65
+# Allow a newer minor version of GHC than the snapshot specifies
66
+# compiler-check: newer-minor

+ 19
- 0
stack.yaml.lock View File

@@ -0,0 +1,19 @@
1
+# This file was autogenerated by Stack.
2
+# You should not edit this file by hand.
3
+# For more information, please see the documentation at:
4
+#   https://docs.haskellstack.org/en/stable/lock_files
5
+
6
+packages:
7
+- completed:
8
+    hackage: argparser-0.3.4@sha256:f277e6d568e88f0698a0ce1f2dc719d90717ffe99c8622302f65389f2a8ed01c,1952
9
+    pantry-tree:
10
+      size: 1449
11
+      sha256: 8c4dd60221760f9fdc9298baa72ff6cd4ed6ee32353141b04cf243d9286b314e
12
+  original:
13
+    hackage: argparser-0.3.4
14
+snapshots:
15
+- completed:
16
+    size: 500539
17
+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/28.yaml
18
+    sha256: cdde1bfb38fdee21c6acb73d506e78f7e12e0a73892adbbbe56374ebef4d3adf
19
+  original: lts-13.28

+ 2
- 0
test/Spec.hs View File

@@ -0,0 +1,2 @@
1
+main :: IO ()
2
+main = putStrLn "Test suite not yet implemented"

Loading…
Cancel
Save