+31
-21
Haskell/blog/app/Main.hs
+31
-21
Haskell/blog/app/Main.hs
···
3
3
import qualified HsBlog
4
4
import OptParse
5
5
6
+
import Control.Exception (bracket)
6
7
import System.Exit (exitFailure)
7
8
import System.Directory (doesFileExist)
8
9
import System.IO
···
14
15
ConvertDir input output ->
15
16
HsBlog.convertDirectory input output
16
17
17
-
ConvertSingle input output -> do
18
-
(title, inputHandle) <-
19
-
case input of
20
-
Stdin -> pure ("", stdin)
21
-
InputFile file -> (,) file <$> openFile file ReadMode
22
-
23
-
outputHandle <-
24
-
case output of
25
-
Stdout -> pure stdout
26
-
OutputFile file -> do
27
-
exists <- doesFileExist file
28
-
shouldOpenFile <-
29
-
if exists
30
-
then confirm
31
-
else pure True
32
-
if shouldOpenFile
33
-
then openFile file WriteMode
34
-
else exitFailure
18
+
ConvertSingle input output ->
19
+
let
20
+
withInputHandle :: (String -> Handle -> IO a) -> IO a
21
+
withInputHandle action =
22
+
case input of
23
+
Stdin -> action "" stdin
24
+
InputFile file ->
25
+
bracket
26
+
(openFile file ReadMode)
27
+
hClose
28
+
(action file)
35
29
36
-
HsBlog.convertSingle title inputHandle outputHandle
37
-
hClose inputHandle
38
-
hClose outputHandle
30
+
withOutputHandle :: (Handle -> IO a) -> IO a
31
+
withOutputHandle action =
32
+
case output of
33
+
Stdout -> action stdout
34
+
OutputFile file -> do
35
+
exists <- doesFileExist file
36
+
shouldOpenFile <-
37
+
if exists
38
+
then confirm
39
+
else pure True
40
+
if shouldOpenFile
41
+
then
42
+
bracked
43
+
(openFile file WriteMode)
44
+
hClose
45
+
(action file)
46
+
else exitFailure
47
+
in
48
+
withInputHandle (\title -> withOutputHandle . HsBlog.convertSingle title)
39
49
40
50
41
51
confirm :: IO Bool
+184
Haskell/blog/src/Directory.hs
+184
Haskell/blog/src/Directory.hs
···
1
+
module HsBlog.Directory
2
+
( convertDirectory
3
+
, buildIndex
4
+
)
5
+
where
6
+
7
+
import qualified HsBlog.Markup as Markup
8
+
import qualified HsBlog.Html as Html
9
+
import HsBlog.Convert (convert, convertStructure)
10
+
11
+
import Data.List (partition)
12
+
import Data.Traversable (for)
13
+
import Control.Monad (void, when)
14
+
15
+
import System.IO (hPutStrLn, stderr)
16
+
import Control.Exception (catch, displayException, SomeException(..))
17
+
import System.Exit (exitFailure)
18
+
import System.FilePath
19
+
( takeExtension
20
+
, takeBaseName
21
+
, (<.>)
22
+
, (</>)
23
+
, takeFileName
24
+
)
25
+
import System.Directory
26
+
( createDirectory
27
+
, removeDirectoryRecursive
28
+
, listDirectory
29
+
, doesDirectoryExist
30
+
, copyFile
31
+
)
32
+
33
+
convertDirectory :: FilePath -> FilePath -> IO ()
34
+
convertDirectory inputDir outputDir = do
35
+
DirContents filesToProcess filesToCopy <- getDirFilesAndContent inputDir
36
+
createOutputDirectoryOrExit outputDir
37
+
let
38
+
outputHtmls = txtsToRenderedHtml filesToProcess
39
+
copyFiles outputDir filesToCopy
40
+
writeFiles outputDir outputHtmls
41
+
putStrLn "Done."
42
+
43
+
data DirContents
44
+
= DirContents
45
+
{ dcFilesToProcess :: [(FilePath, String)]
46
+
, dcFilesToCopy :: [FilePath]
47
+
}
48
+
49
+
50
+
----------------------------
51
+
-- * Build index page
52
+
53
+
buildIndex :: [(FilePath, Markup.Document)] -> Html.Html
54
+
buildIndex files =
55
+
let
56
+
previews =
57
+
map
58
+
( \(file, doc) ->
59
+
case doc of
60
+
Markup.Heading 1 heading : article ->
61
+
Html.h_ 3 (Html.link_ file (Html.txt_ heading))
62
+
++ foldMap convertStructure (take 3 article)
63
+
++ Html.p_ (Html.link_ file (Html.txt_ "..."))
64
+
_ -> Html.h_3 (Html.link_ file (Html.txt_ file))
65
+
)
66
+
files
67
+
in
68
+
Html.html_
69
+
"Blog"
70
+
(Html.h_ 1 (Html.link_ "index.html" (Html.txt_ "Blog"))
71
+
++ Html.h_ 2 (Html.txt_ "Posts")
72
+
++ mconcat previews
73
+
)
74
+
75
+
---------------------------
76
+
-- * Read directory content
77
+
78
+
getDirFilesAndContent :: FilePath -> IO DirContents
79
+
getDirFilesAndContent inputDir = do
80
+
files <- map (inputDir </>) <$> listDirectory inputDir
81
+
let
82
+
(txtFiles, otherFiles) =
83
+
partition ((== ".txt") . takeExtension) files
84
+
txtFilesAndContents <- applyIoOnList readFile txtFiles >>= filterAndReportFailures
85
+
pure $ DirContents
86
+
{ dcFilesToProcess = txtFilesAndContents
87
+
, dcFilesToCopy = otherFiles
88
+
}
89
+
90
+
--------------------------------
91
+
-- * IO work and handling errors
92
+
93
+
applyIoOnList :: (a -> IO b) -> [a] -> IO [(a, Either String b)]
94
+
applyIoOnList fn inputs = do
95
+
for inputs $ \input -> do
96
+
maybeResult <-
97
+
catch
98
+
(Right <$> action input)
99
+
( \(SomeException e) -> do
100
+
pure $ Left (displayException e)
101
+
)
102
+
pure (input, maybeResult)
103
+
104
+
filterAndReportFailures :: [(a, Either String b)] -> IO [(a, b)]
105
+
filterAndReportFailures =
106
+
foldMap $ \(file, contentOrErr) ->
107
+
case contentOrErr of
108
+
Left err -> do
109
+
hPutStrLn stderr err
110
+
pure []
111
+
Right content ->
112
+
pure [(file, content)]
113
+
114
+
--------------------------------
115
+
-- * Conversion
116
+
117
+
txtsToRenderedHtml :: [(FilePath, String)] -> [(FilePath, String)]
118
+
txtsToRenderedHtml txtFiles =
119
+
let
120
+
txtOutputFiles = map toOutputMarkupFile txtFiles
121
+
index = ("index.html", buildIndex txtOutputFiles)
122
+
in
123
+
map (fmap Html.render) (index : map convertFile txtOutputFiles)
124
+
125
+
toOutputMarkupFile :: (FilePath, String) -> (FilePath, Markup.Document)
126
+
toOutputMarkupFile (file, content) = (takeBaseName file <.> "html", Markup.parse content)
127
+
128
+
convertFile :: (FilePath, Markup.Document) -> (FilePath, Html.Html)
129
+
convertFile (file, doc) = (file, convert file doc)
130
+
131
+
-------------------------
132
+
-- * Output to directory
133
+
134
+
createOutputDirectoryOrExit :: FilePath -> IO ()
135
+
createOutputDirectoryOrExit outputDir =
136
+
whenIO
137
+
(not <$> createOutputDirectory outputDir)
138
+
(hPutStrLn stderr "Cancelled." *> exitFailure)
139
+
140
+
createOutputDirectory :: FilePath -> IO Bool
141
+
createOutputDirectory dir = do
142
+
dirExists <- doesDirectoryExist dir
143
+
create <- if dirExists
144
+
then do
145
+
override <- confirm "Output directory exists. Override?"
146
+
when override (removeDirectoryRecursive dir)
147
+
pure override
148
+
else
149
+
pure True
150
+
when create (createDirectory dir)
151
+
pure create
152
+
153
+
copyFile :: FilePath -> [FilePath] -> IO ()
154
+
copyFile outputDir files = do
155
+
let
156
+
copyFromTo file = copyFile file (outputDir </> takeFileName file)
157
+
void $ applyIoOnList copyFromTo files >>= filterAndReportFailures
158
+
159
+
writeFiles :: FilesPath -> [(FilePath, String)] -> IO ()
160
+
writeFiles outputDir files = do
161
+
let
162
+
writeFileContent (file, content) = writeFile (outputDir </> file) content
163
+
void $ applyIoOnList writeFileContent files >>= filterAndReportFailures
164
+
165
+
------------------------------------
166
+
-- * Utilities
167
+
168
+
confirm :: String -> IO Bool
169
+
confirm question = do
170
+
putStrLn (question <> " (y/n)")
171
+
answer <- getLine
172
+
case answer of
173
+
"y" -> pure True
174
+
"n" -> pure False
175
+
_ -> do
176
+
putStrLn "Invalid response. Use y or n."
177
+
confirm question
178
+
179
+
whenIO :: IO Bool -> IO () -> IO ()
180
+
whenIO cond action = do
181
+
result <- cond
182
+
if result
183
+
then action
184
+
else pure ()
+11
-5
Haskell/blog/src/HsBlog/Convert.hs
+11
-5
Haskell/blog/src/HsBlog/Convert.hs
···
9
9
convertStructure :: Markup.Structure -> Html.Structure
10
10
convertStructure structure =
11
11
case structure of
12
-
Markup.Heading n txt -> Html.h_ n txt
12
+
Markup.Heading n txt ->
13
+
Html.h_ n $ Html.txt_ txt
13
14
14
-
Markup.Paragraph p -> Html.p_ p
15
+
Markup.Paragraph p ->
16
+
Html.p_ $ Html.txt_ p
15
17
16
-
Markup.UnorderedList list -> Html.ul_ $ map Html.p_ list
18
+
Markup.UnorderedList list ->
19
+
Html.ul_ $ map (Html.p_ . Html.txt_) list
17
20
18
-
Markup.OrderedList list -> Html.ol_ $ map Html.p_ list
21
+
Markup.OrderedList list ->
22
+
Html.ol_ $ map (Html.p_ . Html.txt_) list
19
23
20
-
Markup.CodeBlock list -> Html.code_ $ unlines list
24
+
Markup.CodeBlock list ->
25
+
Html.code_ $ unlines list
26
+
+6
-1
Haskell/blog/src/HsBlog/Html.hs
+6
-1
Haskell/blog/src/HsBlog/Html.hs
+48
-13
Haskell/blog/src/HsBlog/Html/Internal.hs
+48
-13
Haskell/blog/src/HsBlog/Html/Internal.hs
···
8
8
9
9
newtype Structure = Structure String
10
10
11
-
type Title = String
12
-
13
-
instance Semigroup Structure where
14
-
(<>) c1 c2 = Structure (getStructuredString c1 <> getStructuredString c2)
11
+
newtype Content = Content String
15
12
16
-
instance Monoid Structure where
17
-
mempty = empty_
18
-
19
-
mconcat list =
20
-
case list of
21
-
[] -> mempty
22
-
x : xs -> x <> mconcat xs
13
+
type Title = String
23
14
24
15
-- * EDSL
25
16
···
28
19
$ el "html"
29
20
$ el "head"
30
21
$ (el "title" $ escape title) ++ el "body" (getStructuredString content)
22
+
23
+
-- * Structure
31
24
32
25
body_ :: String -> Structure
33
26
body_ = Structure . el "body"
···
56
49
code_ :: String -> Structure
57
50
code_ = Structure . el "pre" . escape
58
51
59
-
empty_ :: Structure
60
-
empty_ = Structure ""
52
+
instance Semigroup Structure where
53
+
(<>) c1 c2 = Structure (getStructuredString c1 <> getStructuredString c2)
54
+
55
+
instance Monoid Structure where
56
+
mempty = Structure ""_
57
+
mconcat list =
58
+
case list of
59
+
[] -> mempty
60
+
x : xs -> x <> mconcat xs
61
+
62
+
-- * Content
63
+
64
+
txt_ :: String -> Content
65
+
txt_ = Content . escape
66
+
67
+
link_ :: FilePath -> Content -> Content
68
+
link_ path content = Content
69
+
$ elAttr "a"
70
+
("href=\"" ++ escape path ++ "\"")
71
+
(getContentString content)
72
+
73
+
img_ :: FilePath -> Content
74
+
img_ path = Content $ "<img src=\"" ++ escape path ++ "\">"
75
+
76
+
b_ :: Content -> Content
77
+
b_ = Content . el "b" . getContentString
78
+
79
+
i_ :: Content -> Content
80
+
i_ = Content . el "i" . getContentString
81
+
82
+
instance Semigroup Content where
83
+
(<>) c1 c2 = Content (getContentString c1 <> getContentString c2)
84
+
85
+
instance Monoid Content where
86
+
mempty = Content ""
61
87
62
88
-- * Render
63
89
···
72
98
el tag content =
73
99
"<" ++ tag ++ ">" ++ content ++ "</" ++ tag ++ ">"
74
100
101
+
elAttr :: String -> String -> String -> String
102
+
elAttr tag attrs content =
103
+
"<" ++ tag ++ " " ++ attrs ++ ">" ++ content ++ "</" ++ tag ++ ">"
104
+
75
105
getStructuredString :: Structure -> String
76
106
getStructuredString content =
77
107
case content of
78
108
Structure str -> str
109
+
110
+
getContentString :: Content -> String
111
+
getContentString content =
112
+
case content of
113
+
Content str -> str
79
114
80
115
escape :: String -> String
81
116
escape =