{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE Strict #-} module Djot.Html ( inlinesToByteString , renderHtml , RenderOptions(..) ) where import Djot.AST import Data.Tuple (swap) import Djot.Parse (strToUtf8) import Djot.Options (RenderOptions(..)) import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Builder (Builder, byteString, word8, intDec) import qualified Data.Sequence as Seq import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) import Data.List (sort) import Control.Monad.State import qualified Data.Foldable as F renderHtml :: RenderOptions -> Doc -> Builder renderHtml opts doc = evalState ( (<>) <$> toBuilder (docBlocks doc) <*> toNotes ) BState{ noteMap = docFootnotes doc , noteRefs = mempty , renderedNotes = mempty , referenceMap = docReferences doc <> docAutoReferences doc , options = opts } toNotes :: State BState Builder toNotes = do st <- get let noterefs = noteRefs st let numnotes = M.size noterefs let revnoterefs = sort $ map swap $ M.toList noterefs let toNote (num, lab) = let num' = B8.pack (show num) in inTags "li" NoPos (Attr [("id", "fn" <> num')]) ("\n" <> fromMaybe mempty (M.lookup lab (renderedNotes st))) <> "\n" if numnotes < 1 then pure mempty else pure $ inTags "section" NoPos (Attr [("role", "doc-endnotes")]) ("\n" <> singleTag "hr" NoPos mempty <> "\n" <> inTags "ol" NoPos mempty ("\n" <> foldMap toNote revnoterefs) <> "\n") <> "\n" addBackref :: ByteString -> Blocks -> Blocks addBackref num (Many bls) = Many $ case Seq.viewr bls of rest Seq.:> Node pos attr (Para ils) -> rest Seq.|> Node pos attr (Para (ils <> backlink)) _ -> bls Seq.|> Node NoPos mempty (Para backlink) where backlink = Many $ Seq.singleton $ Node NoPos (Attr [("role", "doc-backlink")]) (Link (str (strToUtf8 "\8617\65038")) (Direct ("#fnref" <> num))) {-# INLINE escapeHtml #-} escapeHtml :: ByteString -> Builder escapeHtml bs = if hasEscapable bs then B.foldl' go mempty bs else byteString bs where hasEscapable = B.any (\w -> w == 38 || w == 60 || w == 62) go b 38 = b <> byteString "&" go b 60 = b <> byteString "<" go b 62 = b <> byteString ">" go b c = b <> word8 c {-# INLINE escapeHtmlAttribute #-} escapeHtmlAttribute :: ByteString -> Builder escapeHtmlAttribute bs = if hasEscapable bs then B.foldl' go mempty bs else byteString bs where hasEscapable = B.any (\w -> w == 38 || w == 60 || w == 62 || w == 34) go b 38 = b <> byteString "&" go b 60 = b <> byteString "<" go b 62 = b <> byteString ">" go b 34 = b <> byteString """ go b c = b <> word8 c data BState = BState { noteMap :: NoteMap , noteRefs :: M.Map ByteString Int , renderedNotes :: M.Map ByteString Builder , referenceMap :: ReferenceMap , options :: RenderOptions } {-# SPECIALIZE toBuilder :: Blocks -> State BState Builder #-} {-# SPECIALIZE toBuilder :: Inlines -> State BState Builder #-} class ToBuilder a where toBuilder :: a -> State BState Builder instance ToBuilder Inlines where toBuilder = fmap F.fold . mapM toBuilder . unMany instance ToBuilder Blocks where toBuilder = fmap F.fold . mapM toBuilder . unMany instance ToBuilder (Node Block) where toBuilder (Node pos attr bl) = let addNl = (<> "\n") in case bl of Para ils -> addNl . inTags "p" pos attr <$> toBuilder ils Heading lev ils -> let tag = case lev of 1 -> "h1" 2 -> "h2" 3 -> "h3" 4 -> "h4" 5 -> "h5" 6 -> "h6" _ -> "p" in addNl . inTags tag pos attr <$> toBuilder ils Section bls -> do contents <- toBuilder bls pure $ addNl $ inTags "section" pos attr $ "\n" <> contents ThematicBreak -> pure $ addNl $ singleTag "hr" pos attr BulletList listSpacing items -> addNl . inTags "ul" pos attr . ("\n" <>) . mconcat <$> mapM toLi items where toLi bls = addNl . inTags "li" NoPos mempty . ("\n" <>) <$> toItemContents listSpacing bls OrderedList listAttr listSpacing items -> addNl . inTags "ol" pos (Attr [("start", strToUtf8 (show start)) | start /= 1] <> Attr [("type", typ) | typ /= "1"] <> attr) . ("\n" <>) . mconcat <$> mapM toLi items where typ = case orderedListStyle listAttr of Decimal -> "1" LetterUpper -> "A" LetterLower -> "a" RomanUpper -> "I" RomanLower -> "i" start = orderedListStart listAttr toLi bls = addNl . inTags "li" NoPos mempty . ("\n" <>) <$> toItemContents listSpacing bls DefinitionList listSpacing defs -> addNl . inTags "dl" pos attr . ("\n" <>) . mconcat <$> mapM (toDefinition listSpacing) defs TaskList listSpacing items -> addNl . inTags "ul" pos (Attr [("class", "task-list")] <> attr) . ("\n" <>) . mconcat <$> mapM (toTaskListItem listSpacing) items Div bls -> addNl . inTags "div" pos attr . ("\n" <>) <$> toBuilder bls BlockQuote bls -> addNl . inTags "blockquote" pos attr . ("\n" <>) <$> toBuilder bls CodeBlock lang bs -> pure $ inTags "pre" pos attr (inTags "code" NoPos codeattr (escapeHtml bs)) <> "\n" where codeattr = if B.null lang then mempty else Attr [("class", "language-" <> lang)] Table mbCaption rows -> do rows' <- mapM toRow rows capt <- case mbCaption of Nothing -> pure mempty Just (Caption bs) -> addNl . inTags "caption" NoPos mempty <$> case F.toList (unMany bs) of [Node _pos at (Para ils)] | at == mempty -> toBuilder ils _ -> ("\n" <>) <$> toBuilder bs pure $ addNl . inTags "table" pos attr . ("\n" <>) $ capt <> mconcat rows' RawBlock (Format "html") bs -> pure $ byteString bs RawBlock _ _ -> pure mempty toRow :: [Cell] -> State BState Builder toRow cells = (<> "\n") . inTags "tr" NoPos mempty . ("\n" <>) . mconcat <$> mapM toCell cells toCell :: Cell -> State BState Builder toCell (Cell cellType align ils) = (<> "\n") . inTags (if cellType == HeadCell then "th" else "td") NoPos attr <$> toBuilder ils where attr = Attr $ case align of AlignDefault -> [] AlignLeft -> [("style", "text-align: left;")] AlignRight -> [("style", "text-align: right;")] AlignCenter -> [("style", "text-align: center;")] toItemContents :: ListSpacing -> Blocks -> State BState Builder toItemContents listSpacing = fmap F.fold . mapM go . unMany where go (Node pos attr bl) = case bl of Para ils | listSpacing == Tight -> if attr == mempty then (<> "\n") <$> toBuilder ils else (<> "\n") . inTags "span" pos attr <$> toBuilder ils | otherwise -> toBuilder (Node pos attr bl) _ -> toBuilder (Node pos attr bl) toTaskListItem :: ListSpacing -> (TaskStatus, Blocks) -> State BState Builder toTaskListItem listSpacing (status, bs) = do body <- case Seq.viewl $ unMany bs of Node pos attr (Para ils) Seq.:< rest -> toItemContents listSpacing (Many (Node pos attr (Para (rawInline (Format "html") ("")) Seq.<| rest)) _ -> toBuilder $ rawBlock (Format "html") input <> bs pure $ inTags "li" NoPos (Attr [("class", if status == Complete then "checked" else "unchecked")]) ("\n" <> body) <> "\n" where inputattr = " type=\"checkbox\"" <> if status == Complete then " checked=\"\"" else "" input = " inputattr <> " />" toDefinition :: ListSpacing -> (Inlines, Blocks) -> State BState Builder toDefinition listSpacing (term, defn) = (<>) <$> ((<> "\n") . inTags "dt" NoPos mempty <$> toBuilder term) <*> ((<> "\n") . inTags "dd" NoPos mempty . ("\n" <>) <$> toItemContents listSpacing defn) instance ToBuilder (Node Inline) where toBuilder (Node pos attr il) = case il of Str bs -> case attr of Attr [] | pos == NoPos -> pure $ escapeHtml bs _ -> pure $ inTags "span" pos attr $ escapeHtml bs SoftBreak -> do opts <- gets options pure $ word8 $ if preserveSoftBreaks opts then 10 else 32 HardBreak -> pure $ singleTag "br" NoPos attr <> "\n" NonBreakingSpace -> pure $ byteString " " Emph ils -> inTags "em" pos attr <$> toBuilder ils Strong ils -> inTags "strong" pos attr <$> toBuilder ils Highlight ils -> inTags "mark" pos attr <$> toBuilder ils Insert ils -> inTags "ins" pos attr <$> toBuilder ils Delete ils -> inTags "del" pos attr <$> toBuilder ils Superscript ils -> inTags "sup" pos attr <$> toBuilder ils Subscript ils -> inTags "sub" pos attr <$> toBuilder ils Quoted SingleQuotes ils -> inSingleQuotes <$> toBuilder ils Quoted DoubleQuotes ils -> inDoubleQuotes <$> toBuilder ils Verbatim bs -> pure $ inTags "code" pos attr (escapeHtml bs) Math DisplayMath bs -> pure $ inTags "span" pos (Attr [("class", "math display")] <> attr) ("\\[" <> escapeHtml bs <> "\\]") Math InlineMath bs -> pure $ inTags "span" pos (Attr [("class", "math inline")] <> attr) ("\\(" <> escapeHtml bs <> "\\)") Symbol bs -> pure $ inTags "span" pos (Attr [("class", "symbol")] <> attr) (":" <> escapeHtml bs <> ":") Span ils -> inTags "span" pos attr <$> toBuilder ils Link ils target -> do attr' <- case target of Direct u -> pure $ Attr [("href", u)] Reference label -> do rm <- gets referenceMap case lookupReference label rm of Nothing -> pure $ Attr [("href", "")] Just (u, Attr as) -> pure $ Attr (("href",u):as) inTags "a" pos (attr' <> attr) <$> toBuilder ils Image ils target -> do attr' <- case target of Direct u -> pure $ Attr [("src", u)] Reference label -> do rm <- gets referenceMap case lookupReference label rm of Nothing -> pure $ Attr [("src", "")] Just (u, Attr as) -> pure $ Attr (("src",u):as) pure $ singleTag "img" pos (Attr [("alt", inlinesToByteString ils)] <> attr' <> attr) EmailLink email -> toBuilder (Node pos attr (Link (str email) (Direct ("mailto:" <> email)))) UrlLink url -> toBuilder (Node pos attr (Link (str url) (Direct url))) RawInline (Format "html") bs -> pure $ byteString bs RawInline _ _ -> pure mempty FootnoteReference label -> do noterefs <- gets noteRefs notemap <- gets noteMap num <- case M.lookup label noterefs of Just num -> pure num Nothing -> do let num = M.size noterefs + 1 modify $ \st -> st{ noteRefs = M.insert label num noterefs } renderedNotesMap <- gets renderedNotes case M.lookup label renderedNotesMap of Just _ -> pure () Nothing -> do -- render the note and add to renderedNotes let num' = B8.pack (show num) rendered <- maybe (toBuilder $ addBackref num' (mempty :: Blocks)) (toBuilder . addBackref num') (lookupNote label notemap) modify $ \st -> st{ renderedNotes = M.insert label rendered (renderedNotes st) } pure num let num' = B8.pack $ show num pure $ inTags "a" pos (Attr [("id", "fnref" <> num'), ("href", "#fn" <> num'), ("role", "doc-noteref")] <> attr) $ inTags "sup" pos mempty (escapeHtml num') {-# INLINE inTags #-} inTags :: ByteString -> Pos -> Attr -> Builder -> Builder inTags tag pos attr contents = "<" <> byteString tag <> posToBuilder pos <> attrToBuilder attr <> ">" <> contents <> " byteString tag <> ">" {-# INLINE singleTag #-} singleTag :: ByteString -> Pos -> Attr -> Builder singleTag tag pos attr = "<" <> byteString tag <> posToBuilder pos <> attrToBuilder attr <> ">" {-# INLINE attrToBuilder #-} attrToBuilder :: Attr -> Builder attrToBuilder (Attr pairs) = foldMap go pairs where go (k,v) = " " <> byteString k <> "=\"" <> escapeHtmlAttribute v <> "\"" {-# INLINE posToBuilder #-} posToBuilder :: Pos -> Builder posToBuilder NoPos = mempty posToBuilder (Pos sl sc el ec) = " data-pos=\"" <> intDec sl <> ":" <> intDec sc <> "-" <> intDec el <> ":" <> intDec ec <> "\"" inSingleQuotes :: Builder -> Builder inSingleQuotes x = byteString (strToUtf8 "\x2018") <> x <> byteString (strToUtf8 "\x2019") inDoubleQuotes :: Builder -> Builder inDoubleQuotes x = byteString (strToUtf8 "\x201C") <> x <> byteString (strToUtf8 "\x201D")