-----------------------------------------------------------------------------
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE CPP                   #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Miso.Html.Render
-- Copyright   :  (C) 2016-2026 David M. Johnson
-- License     :  BSD3-style (see the file LICENSE)
-- Maintainer  :  David M. Johnson <code@dmj.io>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- = Overview
--
-- "Miso.Html.Render" provides the 'ToHtml' typeclass for serialising a
-- 'Miso.Types.View' tree to a lazy 'Data.ByteString.Lazy.ByteString' of
-- UTF-8 HTML. This is the foundation of miso's
-- <https://en.wikipedia.org/wiki/Server-side_scripting server-side rendering (SSR)>
-- support.
--
-- Instances are provided for both @'Miso.Types.View' m a@ (a single node)
-- and @['Miso.Types.View' m a]@ (a sequence of nodes).
--
-- = Quick start
--
-- @
-- import           "Miso.Html.Render" ('ToHtml', 'toHtml')
-- import qualified Data.ByteString.Lazy as L
--
-- renderPage :: Model -> L.ByteString
-- renderPage m = 'toHtml' (view m)
-- @
--
-- With @servant@, use @'toHtml'@ inside a @'Data.ByteString.Lazy.ByteString'@
-- or @OctetStream@ response, or wire it into a 'Miso.Html.Render.ToHtml' servant
-- MIME type.
--
-- = Rendering rules
--
-- * __'Miso.Types.VNode'__ — rendered as @\<tag attrs\>children\<\/tag\>@.
--   Self-closing elements (@\<br\/\>@, @\<img\/\>@, @\<input\/\>@, …) are
--   rendered without a closing tag.
-- * __'Miso.Types.VText'__ — rendered as a raw text string (no escaping
--   beyond what is already in the 'Miso.String.MisoString').
-- * __'Miso.Types.VComp'__ — recursively renders the sub-component's view
--   using its initial (or hydrated) model.
-- * __'Miso.Types.VFrag'__ — renders all children inline, no wrapper tag.
-- * __Event handlers__ (@'Miso.Types.On'@) — silently dropped; they have
--   no meaning in a static HTML string.
-- * __Boolean properties__ (@disabled@, @checked@, @required@, …) — rendered
--   as bare attribute names when @True@, omitted entirely when @False@.
-- * __Adjacent text nodes__ — collapsed into a single text node to match
--   browser parsing behaviour during hydration.
--
-- = SSR flag
--
-- When compiled with @-fssr@ the renderer calls the component's optional
-- @hydrateModel@ action to derive the initial model (e.g. by fetching from
-- a database), falling back to the static @model@ if the action throws.
--
-- = See also
--
-- * "Miso.Hydrate" — client-side hydration from server-rendered HTML
-- * "Miso.Html.Element" — element smart constructors
-- * "Miso.Html" — top-level HTML DSL re-export hub
-----------------------------------------------------------------------------
module Miso.Html.Render
  ( -- *** Classes
    ToHtml (..)
  ) where
----------------------------------------------------------------------------
import qualified Data.Set as S
import           Data.Set (Set)
import           Data.ByteString.Builder
import qualified Data.ByteString.Lazy as L
import qualified Data.Map.Strict as M
import           Unsafe.Coerce (unsafeCoerce)
#ifdef SSR
import           Control.Exception (SomeException, catch)
import           System.IO.Unsafe (unsafePerformIO)
#endif
----------------------------------------------------------------------------
import           Miso.JSON
import           Miso.String hiding (intercalate)
import qualified Miso.String as MS
import           Miso.Types
----------------------------------------------------------------------------
-- | Class for rendering HTML
class ToHtml a where
  toHtml :: a -> L.ByteString
----------------------------------------------------------------------------
-- | Render a @Miso.Types.View@ to a @L.ByteString@
instance ToHtml (Miso.Types.View m a) where
  toHtml :: View m a -> ByteString
toHtml = View m a -> ByteString
forall m a. View m a -> ByteString
renderView
----------------------------------------------------------------------------
-- | Render a @[Miso.Types.View]@ to a @L.ByteString@
instance ToHtml [Miso.Types.View m a] where
  toHtml :: [View m a] -> ByteString
toHtml = (View m a -> ByteString) -> [View m a] -> ByteString
forall m a. Monoid m => (a -> m) -> [a] -> m
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap View m a -> ByteString
forall m a. View m a -> ByteString
renderView
----------------------------------------------------------------------------
renderView :: View m a -> L.ByteString
renderView :: forall m a. View m a -> ByteString
renderView = Builder -> ByteString
toLazyByteString (Builder -> ByteString)
-> (View m a -> Builder) -> View m a -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. View m a -> Builder
forall m a. View m a -> Builder
renderBuilder
----------------------------------------------------------------------------
intercalate :: Builder -> [Builder] -> Builder
intercalate :: Builder -> [Builder] -> Builder
intercalate Builder
_ [] = Builder
""
intercalate Builder
_ [Builder
x] = Builder
x
intercalate Builder
sep (Builder
x:[Builder]
xs) =
  [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ Builder
x
  , Builder
sep
  , Builder -> [Builder] -> Builder
intercalate Builder
sep [Builder]
xs
  ]
----------------------------------------------------------------------------
booleanProperties :: Set MisoString
booleanProperties :: Set MisoString
booleanProperties = [MisoString] -> Set MisoString
forall a. Ord a => [a] -> Set a
S.fromList
  [ MisoString
"allowfullscreen"
  , MisoString
"allowpaymentrequest"
  , MisoString
"allowusermedia"
  , MisoString
"async"
  , MisoString
"autofocus"
  , MisoString
"autoplay"
  , MisoString
"checked"
  , MisoString
"controls"
  , MisoString
"default"
  , MisoString
"defer"
  , MisoString
"disabled"
  , MisoString
"download"
  , MisoString
"formnovalidate"
  , MisoString
"hidden"
  , MisoString
"inert"
  , MisoString
"ismap"
  , MisoString
"itemscope"
  , MisoString
"loop"
  , MisoString
"multiple"
  , MisoString
"muted"
  , MisoString
"nomodule"
  , MisoString
"novalidate"
  , MisoString
"open"
  , MisoString
"playsinline"
  , MisoString
"readonly"
  , MisoString
"required"
  , MisoString
"reversed"
  , MisoString
"selected"
  , MisoString
"truespeed"
  ]
----------------------------------------------------------------------------
renderBuilder :: forall m a . Miso.Types.View m a -> Builder
renderBuilder :: forall m a. View m a -> Builder
renderBuilder (VText Maybe Key
_ MisoString
"")    = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
" "
renderBuilder (VText Maybe Key
_ MisoString
s)     = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
s
renderBuilder (VNode Namespace
_ MisoString
"doctype" [] []) = Builder
"<!doctype html>"
renderBuilder (VNode Namespace
ns MisoString
tag [Attribute a]
attrs [View m a]
children) = [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ Builder
"<"
  , MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
tag
  , [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat [ Builder
" " Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder -> [Builder] -> Builder
intercalate Builder
" " (Attribute a -> Builder
forall action. Attribute action -> Builder
renderAttrs (Attribute a -> Builder) -> [Attribute a] -> [Builder]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Attribute a]
attrs)
            | Bool -> Bool
not ([Attribute a] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
Prelude.null [Attribute a]
attrs)
            ]
  , if MisoString
tag MisoString -> [MisoString] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [MisoString]
selfClosing then Builder
"/>" else Builder
">"
  , [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
    [ [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
      [ (View m a -> Builder) -> [View m a] -> Builder
forall m a. Monoid m => (a -> m) -> [a] -> m
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap View m a -> Builder
forall m a. View m a -> Builder
renderBuilder ([View m a] -> [View m a]
forall m a. [View m a] -> [View m a]
collapseSiblingTextNodes [View m a]
children)
      , Builder
"</" Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
tag Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
">"
      ]
    | MisoString
tag MisoString -> [MisoString] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [MisoString]
selfClosing
    ]
  ] where
      selfClosing :: [MisoString]
selfClosing = [MisoString]
htmls [MisoString] -> [MisoString] -> [MisoString]
forall a. Semigroup a => a -> a -> a
<> [MisoString]
svgs [MisoString] -> [MisoString] -> [MisoString]
forall a. Semigroup a => a -> a -> a
<> [MisoString]
mathmls
      htmls :: [MisoString]
htmls = [ MisoString
x
              | Namespace
ns Namespace -> Namespace -> Bool
forall a. Eq a => a -> a -> Bool
== Namespace
HTML
              , MisoString
x <- [ MisoString
"area", MisoString
"base", MisoString
"col", MisoString
"embed", MisoString
"img", MisoString
"input", MisoString
"br", MisoString
"hr", MisoString
"meta", MisoString
"link", MisoString
"param", MisoString
"source", MisoString
"track", MisoString
"wbr" ]
              ]
      svgs :: [MisoString]
svgs  = [ MisoString
x
              | Namespace
ns Namespace -> Namespace -> Bool
forall a. Eq a => a -> a -> Bool
== Namespace
SVG
              , MisoString
x <- [ MisoString
"circle", MisoString
"line", MisoString
"rect", MisoString
"path", MisoString
"ellipse", MisoString
"polygon", MisoString
"polyline", MisoString
"use", MisoString
"image"]
              ]
      mathmls :: [MisoString]
mathmls =
              [ MisoString
x
              | Namespace
ns Namespace -> Namespace -> Bool
forall a. Eq a => a -> a -> Bool
== Namespace
MATHML
              , MisoString
x <- [MisoString
"mglyph", MisoString
"mprescripts", MisoString
"none", MisoString
"maligngroup", MisoString
"malignmark" ]
              ]

renderBuilder (VComp Maybe Key
_ (SomeComponent props
props Component m props model action
vcomp_)) =
  (View (ZonkAny 0) (ZonkAny 1) -> Builder)
-> [View (ZonkAny 0) (ZonkAny 1)] -> Builder
forall m a. Monoid m => (a -> m) -> [a] -> m
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap View (ZonkAny 0) (ZonkAny 1) -> Builder
forall m a. View m a -> Builder
renderBuilder [View (ZonkAny 0) (ZonkAny 1)]
forall {a}. [a]
vkids
    where
#ifdef SSR
      vkids = [ unsafeCoerce $ view vcomp_ props (getInitialComponentModel vcomp_) ]
#else
      vkids :: [a]
vkids = [ View model action -> a
forall a b. a -> b
unsafeCoerce (View model action -> a) -> View model action -> a
forall a b. (a -> b) -> a -> b
$ Component m props model action
-> props -> model -> View model action
forall parent props model action.
Component parent props model action
-> props -> model -> View model action
view Component m props model action
vcomp_ props
props (Component m props model action -> model
forall parent props model action.
Component parent props model action -> model
model Component m props model action
vcomp_) ]
#endif
renderBuilder (VFrag Maybe Key
_ [View m a]
kids) = (View m a -> Builder) -> [View m a] -> Builder
forall m a. Monoid m => (a -> m) -> [a] -> m
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap View m a -> Builder
forall m a. View m a -> Builder
renderBuilder [View m a]
kids
----------------------------------------------------------------------------
renderAttrs :: Attribute action -> Builder
renderAttrs :: forall action. Attribute action -> Builder
renderAttrs (ClassList [MisoString]
classes) =
  [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ Builder
"class"
  , String -> Builder
stringUtf8 String
"=\""
  , MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString ([MisoString] -> MisoString
MS.unwords [MisoString]
classes)
  , String -> Builder
stringUtf8 String
"\""
  ]
renderAttrs (Property MisoString
key (Bool Bool
enabled)) -- dmj: account for boolean properties
  | MisoString -> Set MisoString -> Bool
forall a. Ord a => a -> Set a -> Bool
S.member MisoString
key Set MisoString
booleanProperties, Bool
enabled = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
key
  | MisoString -> Set MisoString -> Bool
forall a. Ord a => a -> Set a -> Bool
S.member MisoString
key Set MisoString
booleanProperties, Bool -> Bool
not Bool
enabled = Builder
forall a. Monoid a => a
mempty
  | Bool
otherwise = [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
      [ MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
key
      , String -> Builder
stringUtf8 String
"=\""
      , Value -> Builder
toHtmlFromJSON (Bool -> Value
Bool Bool
enabled)
      , String -> Builder
stringUtf8 String
"\""
      ]
renderAttrs (Property MisoString
"key" Value
_) = Builder
forall a. Monoid a => a
mempty
renderAttrs (Property MisoString
key Value
value) =
  [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
key
  , String -> Builder
stringUtf8 String
"=\""
  , Value -> Builder
toHtmlFromJSON Value
value
  , String -> Builder
stringUtf8 String
"\""
  ]
renderAttrs (On Sink action -> VTree -> LogLevel -> Events -> IO ()
_) = Builder
forall a. Monoid a => a
mempty
renderAttrs (Styles Map MisoString MisoString
styles_) =
  [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ Builder
"style"
  , String -> Builder
stringUtf8 String
"=\""
  , [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
    [ [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
      [ MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
k
      , Char -> Builder
charUtf8 Char
':'
      , MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString MisoString
v
      , Char -> Builder
charUtf8 Char
';'
      ]
    | (MisoString
k,MisoString
v) <- Map MisoString MisoString -> [(MisoString, MisoString)]
forall k a. Map k a -> [(k, a)]
M.toList Map MisoString MisoString
styles_
    ]
  , String -> Builder
stringUtf8 String
"\""
  ]
----------------------------------------------------------------------------
-- | The browser can't distinguish between multiple text nodes
-- and a single text node. So it will always parse a single text node
-- this means we must collapse adjacent text nodes during hydration.
collapseSiblingTextNodes :: [View m a] -> [View m a]
collapseSiblingTextNodes :: forall m a. [View m a] -> [View m a]
collapseSiblingTextNodes [] = []
collapseSiblingTextNodes (VText Maybe Key
_ MisoString
x : VText Maybe Key
k MisoString
y : [View m a]
xs) =
  [View m a] -> [View m a]
forall m a. [View m a] -> [View m a]
collapseSiblingTextNodes (Maybe Key -> MisoString -> View m a
forall model action. Maybe Key -> MisoString -> View model action
VText Maybe Key
k (MisoString
x MisoString -> MisoString -> MisoString
forall a. Semigroup a => a -> a -> a
<> MisoString
y) View m a -> [View m a] -> [View m a]
forall a. a -> [a] -> [a]
: [View m a]
xs)
collapseSiblingTextNodes (View m a
x:[View m a]
xs) =
  View m a
x View m a -> [View m a] -> [View m a]
forall a. a -> [a] -> [a]
: [View m a] -> [View m a]
forall m a. [View m a] -> [View m a]
collapseSiblingTextNodes [View m a]
xs
----------------------------------------------------------------------------
-- | Helper for turning JSON into Text
-- Object, Array and Null are kind of non-sensical here
toHtmlFromJSON :: Value -> Builder
toHtmlFromJSON :: Value -> Builder
toHtmlFromJSON (String MisoString
t)   = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString (MisoString -> MisoString
forall str. ToMisoString str => str -> MisoString
ms MisoString
t)
toHtmlFromJSON (Number Double
t)   = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString (MisoString -> Builder) -> MisoString -> Builder
forall a b. (a -> b) -> a -> b
$ String -> MisoString
forall str. ToMisoString str => str -> MisoString
ms (Double -> String
forall a. Show a => a -> String
show Double
t)
toHtmlFromJSON (Bool Bool
True)  = Builder
"true"
toHtmlFromJSON (Bool Bool
False) = Builder
"false"
toHtmlFromJSON Value
Null         = Builder
"null"
toHtmlFromJSON (Object Map MisoString Value
o)   = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString (MisoString -> Builder) -> MisoString -> Builder
forall a b. (a -> b) -> a -> b
$ String -> MisoString
forall str. ToMisoString str => str -> MisoString
ms (Map MisoString Value -> String
forall a. Show a => a -> String
show Map MisoString Value
o)
toHtmlFromJSON (Array [Value]
a)    = MisoString -> Builder
forall a. FromMisoString a => MisoString -> a
fromMisoString (MisoString -> Builder) -> MisoString -> Builder
forall a b. (a -> b) -> a -> b
$ String -> MisoString
forall str. ToMisoString str => str -> MisoString
ms ([Value] -> String
forall a. Show a => a -> String
show [Value]
a)
-----------------------------------------------------------------------------
#ifdef SSR
-- | Used for server-side model hydration, internally only in 'renderView'.
--
-- We use 'unsafePerformIO' here because @servant@'s 'MimeRender' is a pure function
-- yet we need to allow the users to hydrate in 'IO'.
--
getInitialComponentModel :: Component parent model action -> model
getInitialComponentModel Component {..} =
  case hydrateModel of
    Nothing -> model
    Just action -> unsafePerformIO $
      action `catch` (\(e :: SomeException) -> do
        putStrLn "Encountered exception during model hydration, falling back to default model"
        print e
        pure model)
----------------------------------------------------------------------------
#endif