this repo has no description

:sparkles: (Haskell) blog generator code

Changed files
+280 -40
Haskell
+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
··· 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
··· 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
··· 12 12 , head_ 13 13 , title_ 14 14 , code_ 15 - , empty_ 15 + , Content 16 + , txt_ 17 + , img_ 18 + , link_ 19 + , b_ 20 + , i_ 16 21 , render 17 22 ) 18 23 where
+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 =