module Zoxuli ( Zoxuli , PassData(..), PassOne(..), PassTwo(..) , slugify , rawHtml , Link(..), link, linkToAnchor , Code(..), code, inlineCode , h1_, h2_, h3_, h4_, h5_, h6_ , toc , breadcrumbs , Footnote(..), footnote , Page(..), makePage , Project, ProjectMeta(..), makeProject , Post, PostMeta(..), makePost , reverseChronological , SrcFile(..) ) where import Lucid hiding (h2_, h3_, h4_, h5_, h6_) import qualified Lucid as L import Lucid.Base (TermRaw, termRaw) import Optics import GHC.Generics (Generic) import Control.Monad (when) import Control.Monad.State.Lazy (State, get, modify, lift) import Data.Text (Text, replace, toLower, pack, unpack, splitOn) import qualified Data.Text as Text import Data.List (intersperse, inits, sortBy) type Zoxuli = HtmlT (State PassData) () data PassData = One PassOne | Two PassTwo deriving (Generic) data PassOne = PassOne { links :: [Link] , headers :: [Header] , footnotes :: [Zoxuli] , codes :: [Code] } deriving (Generic) data PassTwo = PassTwo { backlinks :: [Link] , headers :: [Header] , footnotes :: [Footnote] , footnoteIndex :: Int , codes :: [Text] , srcFiles :: [SrcFile] } deriving (Generic) data Link = Link { body :: Text , destinationTitle :: Text , destinationPath :: Text } link :: Bool -> Page -> Text -> Zoxuli link createBacklink page body = link' createBacklink page Nothing body linkToAnchor :: Bool -> Page -> Text -> Text -> Zoxuli linkToAnchor createBacklink page anchor body = link' createBacklink page (Just anchor) body link' :: Bool -> Page -> Maybe Text -> Text -> Zoxuli link' createBacklink page anchor body = do when createBacklink $ do let newLink = [Link body page.title page.path] lift . modify $ #_One % #links %~ (<> newLink) a_ [ href_ $ "/" <> page.path <> ".html" <> maybe "" ("#" <>) anchor , id_ $ slugify body ] $ toHtml body data Code = Code { lang :: Text , body :: Text } code :: Text -> Text -> Zoxuli code lang body = lift get >>= \case One _ -> lift . modify $ #_One % #codes %~ (<> newCode) Two s -> case s.codes of (c:rest) -> do rawHtml c lift . modify $ #_Two % #codes .~ rest _ -> pure () where newCode = [Code lang body] inlineCode :: Zoxuli -> Zoxuli inlineCode = code_ [class_ "verbatim"] data Header = Header { level :: Int , body :: Text } deriving (Generic) hx_ :: Int -> (Zoxuli -> Zoxuli) -> Text -> Zoxuli hx_ level headerFunction body = do let newHeader = [Header level body] lift . modify $ #_One % #headers %~ (<> newHeader) headerFunction $ do a_ [ href_ $ "#" <> slugify body , id_ $ slugify body ] "❡"; " " toHtml body h2_ :: Text -> Zoxuli h2_ = hx_ 2 L.h2_ h3_ :: Text -> Zoxuli h3_ = hx_ 3 L.h3_ h4_ :: Text -> Zoxuli h4_ = hx_ 4 L.h4_ h5_ :: Text -> Zoxuli h5_ = hx_ 5 L.h5_ h6_ :: Text -> Zoxuli h6_ = hx_ 6 L.h6_ toc :: Zoxuli toc = lift get <&> (^? #_Two % #headers) >>= \case Just headers@(_:_) -> details_ [class_ "toc"] $ do summary_ "table of contents" ul_ $ tocList headers _ -> pure () tocList :: [Header] -> Zoxuli tocList [] = pure () tocList ((Header level body) : rest) = do let (nested, following) = break (\h -> h.level == level) rest li_ . a_ [ href_ $ "#" <> slugify body ] $ toHtml body if null nested then pure () else ul_ $ tocList nested tocList following breadcrumbs :: Page -> Zoxuli breadcrumbs page = p_ $ do "/ "; sequence_ $ intersperse " / " crumbs where titles = splitOn "/" page.path paths = Text.intercalate "/" <$> tail (inits titles) crumbs = zipWith crumb titles paths crumb :: Text -> Text -> Zoxuli crumb title path = a_ [href_ $ "/" <> path <> ".html"] $ toHtml title data Footnote = Footnote Int Zoxuli footnote :: Zoxuli -> Zoxuli footnote content = lift get >>= \case One _ -> lift . modify $ #_One % #footnotes %~ (<> [content]) Two s -> do sup_ . a_ [ href_ $ "#fn" <> pack (show s.footnoteIndex) , id_ $ "r" <> pack (show s.footnoteIndex) ] $ do "["; toHtml $ show s.footnoteIndex; "]" lift . modify $ #_Two % #footnoteIndex %~ (+ 1) slugify :: Text -> Text slugify = replace " " "-" . toLower rawHtml :: TermRaw arg result => arg -> result rawHtml = termRaw "div" data Page = Page { title :: Text , path :: Text , tags :: [Text] , html :: Zoxuli } deriving (Generic) instance Eq Page where a == b = a.path == b.path makePage :: Text -> Zoxuli -> Page makePage title = Page title path [] where path = case title of "home" -> "index" _ -> slugify title type Project = (Page, ProjectMeta) newtype ProjectMeta = ProjectMeta { repo :: Text } deriving (Generic) makeProject :: Text -> Text -> [Text] -> Zoxuli -> Project makeProject title repo tags html = (Page title path tags html, ProjectMeta repo) where path = "projects/" <> slugify title type Post = (Page, PostMeta) data PostMeta = PostMeta { date :: Text , author :: Text } deriving (Generic) makePost :: Text -> Text -> [Text] -> Zoxuli -> Post makePost title date tags html = (Page title path tags html, PostMeta date "eevie") where path = "posts/" <> slugify title reverseChronological :: [Post] -> [Post] reverseChronological posts = sortBy comp posts where comp a b = getInt b `compare` getInt a getInt :: Post -> Int getInt p = read $ unpack $ replace "-" "" $ p ^. _2 % #date data SrcFile = SrcFile { name :: Text , content :: Text }