1 {-# LANGUAGE DuplicateRecordFields #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE FlexibleContexts #-}
4 {-# LANGUAGE MultiParamTypeClasses #-}
5 {-# LANGUAGE OverloadedStrings #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE TypeApplications #-}
8 {-# LANGUAGE ViewPatterns #-}
9 {-# OPTIONS_GHC -fno-warn-orphans #-}
10 module Language.DTC.Write.HTML5 where
12 -- import Control.Monad.Trans.Class (MonadTrans(..))
13 -- import Data.Functor.Identity (Identity(..))
14 -- import Data.Sequence (Seq)
15 -- import Data.Set (Set)
16 -- import Data.Traversable (Traversable(..))
17 -- import qualified Data.Sequence as Seq
18 -- import qualified Data.TreeSeq.Strict as Tree
19 import Control.Applicative (Applicative(..))
20 import Control.Category
23 import Data.Char (Char)
24 import Data.Default.Class (Default(..))
25 import Data.Foldable (Foldable(..), concat)
26 import Data.Function (($), const, flip, on)
27 import Data.Functor (Functor(..), (<$>))
28 import Data.Functor.Compose (Compose(..))
30 import Data.Map.Strict (Map)
31 import Data.Maybe (Maybe(..), maybe, mapMaybe, fromJust, maybeToList)
32 import Data.Monoid (Monoid(..))
33 import Data.Ord (Ord(..))
34 import Data.Semigroup (Semigroup(..))
35 import Data.String (String)
36 import Data.Text (Text)
37 import Data.TreeSeq.Strict (Tree(..), Trees)
38 import Data.Tuple (snd)
39 import System.FilePath (FilePath)
40 import Text.Blaze ((!))
41 import Text.Blaze.Html (Html)
42 import Text.Show (Show(..))
43 import qualified Control.Monad.Trans.State as S
44 import qualified Data.List as List
45 import qualified Data.Map.Strict as Map
46 import qualified Data.Sequence as Seq
47 import qualified Data.Strict.Maybe as Strict
48 import qualified Data.Text as Text
49 import qualified Data.Text.Lazy as TL
50 import qualified Data.TreeMap.Strict as TreeMap
51 import qualified Data.TreeSeq.Strict as Tree
52 import qualified Data.TreeSeq.Strict.Zipper as Tree
53 import qualified Text.Blaze.Html5 as H
54 import qualified Text.Blaze.Html5.Attributes as HA
55 import qualified Text.Blaze.Internal as H
57 import Text.Blaze.Utils
58 import Data.Locale hiding (localize, Index)
59 import qualified Data.Locale as Locale
61 import Language.DTC.Write.XML ()
62 import Language.DTC.Write.Plain (Plain, Plainify(..))
63 import qualified Language.DTC.Write.Plain as Plain
64 import qualified Language.DTC.Document as DTC
65 import qualified Language.DTC.Anchor as Anchor
67 (<&>) :: Functor f => f a -> (a -> b) -> f b
72 type Html5 = StateMarkup State ()
77 { state_styles :: Map FilePath CSS
78 , state_scripts :: Map FilePath Script
79 , state_indexs :: Map DTC.Pos (DTC.Terms, Anchor.Irefs)
80 , state_rrefs :: Anchor.Rrefs
81 , state_figures :: Map Text (Map DTC.Pos DTC.Title)
82 , state_references :: Map DTC.Ident DTC.About
83 , state_plainify :: Plain.State
85 instance Default State where
87 { state_styles = mempty
88 , state_scripts = mempty
89 , state_indexs = mempty
90 , state_rrefs = mempty
91 , state_figures = mempty
92 , state_references = mempty
93 , state_plainify = def
101 { keys_index :: Map DTC.Pos DTC.Terms
102 , keys_figure :: Map Text (Map DTC.Pos DTC.Title)
103 , keys_reference :: Map DTC.Ident DTC.About
106 keys :: Trees DTC.BodyKey DTC.BodyValue -> Keys
107 keys body = foldl' flt (Keys mempty mempty mempty) (Compose body)
110 DTC.Index{..} -> acc{keys_index =
111 Map.insert pos terms $ keys_index acc}
112 DTC.Figure{..} -> acc{keys_figure =
114 type_ (Map.singleton pos title) $
116 DTC.References{..} -> acc{keys_reference =
119 (DTC.id (r::DTC.Reference))
120 (DTC.about (r::DTC.Reference)))
125 -- ** Class 'Html5ify'
126 class Html5ify a where
127 html5ify :: a -> Html5
128 instance Html5ify Char where
129 html5ify = html5ify . H.toMarkup
130 instance Html5ify Text where
131 html5ify = html5ify . H.toMarkup
132 instance Html5ify TL.Text where
133 html5ify = html5ify . H.toMarkup
134 instance Html5ify String where
135 html5ify = html5ify . H.toMarkup
136 instance Html5ify H.Markup where
137 html5ify = Compose . return
138 instance Html5ify DTC.Title where
139 html5ify (DTC.Title t) = html5ify t
140 instance Html5ify DTC.Para where
141 html5ify = mapM_ html5ify
142 instance Html5ify DTC.Ident where
143 html5ify (DTC.Ident i) = html5ify i
144 instance Html5ify Int where
145 html5ify = html5ify . show
146 instance Html5ify DTC.Nat where
147 html5ify (DTC.Nat n) = html5ify n
148 instance Html5ify DTC.Nat1 where
149 html5ify (DTC.Nat1 n) = html5ify n
152 Localize ls Plain Plain.L10n =>
154 LocaleIn ls -> DTC.Document -> Html
155 html5Document locale DTC.Document{..} = do
156 let Keys{..} = keys body
157 let (body',state_rrefs,state_indexs) =
158 let irefs = foldMap Anchor.irefsOfTerms keys_index in
159 let (body0, Anchor.State{state_irefs, state_rrefs=rrefs}) =
160 Anchor.anchorify body `S.runState`
161 Anchor.state{Anchor.state_irefs=irefs} in
163 (<$> keys_index) $ \terms ->
165 TreeMap.intersection const state_irefs $
166 Anchor.irefsOfTerms terms
167 let state_plainify = def
168 { Plain.state_localize = Locale.localize locale }
169 let (html5Body, State{state_styles,state_scripts}) =
173 , state_figures = keys_figure
174 , state_references = keys_reference
179 H.html ! HA.lang (attrify $ countryCode locale) $ do
181 H.meta ! HA.httpEquiv "Content-Type"
182 ! HA.content "text/html; charset=UTF-8"
183 whenSome (DTC.titles $ DTC.about (head :: DTC.Head)) $ \ts ->
185 H.toMarkup $ Plain.text state_plainify $ List.head ts
186 forM_ (DTC.links $ DTC.about (head :: DTC.Head)) $ \DTC.Link{rel, href} ->
187 H.link ! HA.rel (attrify rel)
188 ! HA.href (attrify href)
189 H.meta ! HA.name "generator"
192 (`mapMaybe` toList body) $ \case
193 TreeN k@DTC.Section{} _ -> Just k
195 forM_ chapters $ \DTC.Section{..} ->
196 H.link ! HA.rel "Chapter"
197 ! HA.title (attrify $ plainify title)
198 ! HA.href ("#"<>attrify pos)
199 H.link ! HA.rel "stylesheet"
200 ! HA.type_ "text/css"
201 ! HA.href "style/dtc-html5.css"
202 forM_ state_styles $ \style ->
203 H.style ! HA.type_ "text/css" $
205 forM_ state_scripts $ \script ->
206 H.script ! HA.type_ "application/javascript" $
211 -- * Type 'BodyCursor'
212 -- | Cursor to navigate within a 'DTC.Body' according to many axis (like in XSLT).
213 type BodyCursor = Tree.Zipper DTC.BodyKey DTC.BodyValue
214 instance Html5ify DTC.Body where
216 forM_ (Tree.zippers body) $ \z ->
217 forM_ (Tree.axis_repeat Tree.axis_following_sibling_nearest `Tree.runAxis` z) $
220 instance Html5ify BodyCursor where
222 case Tree.current z of
223 TreeN k _ts -> html5BodyKey z k
224 Tree0 v -> html5BodyValue z v
226 html5BodyKey :: BodyCursor -> DTC.BodyKey -> Html5
227 html5BodyKey z = \case
229 H.section ! HA.class_ "section"
230 ! HA.id (attrify pos) $$ do
231 html5CommonAttrs attrs $
232 H.table ! HA.class_ "section-header" $$
235 H.td ! HA.class_ "section-number" $$ do
236 html5SectionNumber $ DTC.posAncestors pos
237 H.td ! HA.class_ "section-title" $$ do
238 (case List.length $ DTC.posAncestors pos of
247 forM_ (Tree.axis_child `Tree.runAxis` z) $
249 html5BodyValue :: BodyCursor -> DTC.BodyValue -> Html5
250 html5BodyValue z = \case
251 DTC.Block b -> html5ify b
253 H.nav ! HA.class_ "toc"
254 ! HA.id (attrify pos) $$ do
255 H.span ! HA.class_ "toc-name" $$
256 H.a ! HA.href (attrify pos) $$
257 html5ify Plain.L10n_Table_of_Contents
259 forM_ (Tree.axis_following_sibling `Tree.runAxis` z) $
262 H.nav ! HA.class_ "tof"
263 ! HA.id (attrify pos) $$
264 H.table ! HA.class_ "tof" $$
268 html5CommonAttrs attrs $
269 H.div ! HA.class_ ("figure " <> attrify ("figure-"<>type_))
270 ! HA.id (attrify pos) $$ do
271 H.table ! HA.class_ "figure-caption" $$
274 H.td ! HA.class_ "figure-number" $$ do
275 H.a ! HA.href ("#"<>attrify pos) $$ do
277 html5ify $ DTC.posAncestors pos
278 html5ify $ Plain.L10n_Colon
280 H.td ! HA.class_ "figure-name" $$
282 H.div ! HA.class_ "figure-content" $$ do
285 (allTerms,refsByTerm) <- liftStateMarkup $ S.gets $ (Map.! pos) . state_indexs
286 let chars = Anchor.termsByChar allTerms
287 H.div ! HA.class_ "index"
288 ! HA.id (attrify pos) $$ do
289 H.nav ! HA.class_ "index-nav" $$ do
290 forM_ (Map.keys chars) $ \char ->
291 H.a ! HA.href ("#"<>(attrify pos <> "." <> attrify char)) $$
293 H.dl ! HA.class_ "index-chars" $$
294 forM_ (Map.toList chars) $ \(char,terms) -> do
296 let i = attrify pos <> "." <> attrify char in
298 ! HA.href ("#"<>i) $$
301 H.dl ! HA.class_ "index-term" $$ do
302 forM_ terms $ \aliases -> do
304 H.ul ! HA.class_ "index-aliases" $$
305 forM_ (List.take 1 aliases) $ \term ->
306 H.li ! HA.id (attrify term) $$
310 List.sortBy (compare `on` DTC.section . snd) $
311 (`foldMap` aliases) $ \words ->
313 path <- Anchor.pathFromWords words
314 Strict.maybe Nothing (Just . ((words,) <$>) . List.reverse) $
315 TreeMap.lookup path refsByTerm in
317 (<$> anchs) $ \(term,DTC.Anchor{..}) ->
318 H.a ! HA.class_ "index-iref"
319 ! HA.href ("#"<>attrify (term,count)) $$
320 html5ify $ DTC.posAncestors section
321 DTC.References{..} ->
322 html5CommonAttrs attrs $
323 H.div ! HA.class_ "references"
324 ! HA.id (attrify pos) $$ do
328 instance Html5ify DTC.Words where
329 html5ify = html5ify . Anchor.plainifyWords
331 cleanPara :: DTC.Para -> DTC.Para
333 p >>= (`Tree.bindTrees` \case
334 TreeN DTC.Iref{} ls -> ls
335 TreeN DTC.Note{} _ -> mempty
338 html5ifyToC :: Maybe DTC.Nat -> BodyCursor -> Html5
339 html5ifyToC depth z =
340 case Tree.current z of
341 TreeN DTC.Section{..} _ts -> do
343 H.table ! HA.class_ "toc-entry" $$
346 H.td ! HA.class_ "section-number" $$
347 html5SectionRef $ DTC.posAncestors pos
348 H.td ! HA.class_ "section-title" $$
349 html5ify $ cleanPara $ DTC.unTitle title
350 when (maybe True (> DTC.Nat 1) depth && not (null sections)) $
353 html5ifyToC (depth >>= DTC.predNat)
359 `Tree.axis_filter_current` \case
360 TreeN DTC.Section{} _ -> True
363 html5ifyToF :: [Text] -> Html5
364 html5ifyToF types = do
365 figsByType <- liftStateMarkup $ S.gets state_figures
367 Map.foldMapWithKey (\ty -> ((ty,) <$>)) $
371 Map.intersection figsByType $
372 Map.fromList [(ty,()) | ty <- types]
373 forM_ (Map.toList figs) $ \(pos, (type_, title)) ->
375 H.td ! HA.class_ "figure-number" $$
376 H.a ! HA.href ("#"<>attrify pos) $$ do
378 html5ify $ DTC.posAncestors pos
379 H.td ! HA.class_ "figure-name" $$
380 html5ify $ cleanPara $ DTC.unTitle title
382 instance Html5ify [DTC.Block] where
383 html5ify = mapM_ html5ify
384 instance Html5ify DTC.Block where
387 html5CommonAttrs attrs $
388 H.p ! HA.class_ "para"
389 ! HA.id (attrify pos) $$ do
392 html5CommonAttrs attrs $
393 H.ol ! HA.class_ "ol"
394 ! HA.id (attrify pos) $$ do
395 forM_ items $ \item ->
396 H.li $$ html5ify item
398 html5CommonAttrs attrs $
399 H.ul ! HA.class_ "ul"
400 ! HA.id (attrify pos) $$ do
401 forM_ items $ \item ->
402 H.li $$ html5ify item
404 html5ify $ H.Comment (H.Text t) ()
405 instance Html5ify DTC.Lines where
409 DTC.BR -> html5ify H.br
410 DTC.Plain t -> html5ify t
413 DTC.B -> H.strong $$ html5ify ls
414 DTC.Code -> H.code $$ html5ify ls
415 DTC.Del -> H.del $$ html5ify ls
417 i <- liftStateMarkup $ do
418 i <- S.gets $ Plain.state_italic . state_plainify
421 (state_plainify s){Plain.state_italic=
424 H.em ! HA.class_ (if i then "even" else "odd") $$
429 (state_plainify s){Plain.state_italic=i}}
430 DTC.Sub -> H.sub $$ html5ify ls
431 DTC.Sup -> H.sup $$ html5ify ls
432 DTC.SC -> H.span ! HA.class_ "smallcaps" $$ html5ify ls
433 DTC.U -> H.span ! HA.class_ "underline" $$ html5ify ls
436 d <- liftStateMarkup $ do
437 d <- S.gets $ Plain.state_quote . state_plainify
438 S.modify $ \s -> s{state_plainify=
439 (state_plainify s){Plain.state_quote=
442 H.span ! HA.class_ "q" $$ do
443 html5ify $ Plain.L10n_QuoteOpen d
444 html5ify $ TreeN DTC.I ls
445 html5ify $ Plain.L10n_QuoteClose d
449 (state_plainify s){Plain.state_quote = d}}
451 H.a ! HA.class_ "eref"
452 ! HA.href (attrify href) $$
454 then html5ify $ DTC.unURL href
458 Nothing -> html5ify ls
459 Just DTC.Anchor{..} ->
460 H.span ! HA.class_ "iref"
461 ! HA.id (attrify (term,count)) $$
464 H.a ! HA.class_ "ref"
465 ! HA.href ("#"<>attrify to) $$
470 refs <- liftStateMarkup $ S.gets state_references
471 case Map.lookup to refs of
474 H.span ! HA.class_ "rref-broken" $$
477 Just DTC.About{..} -> do
478 when (not $ null ls) $
479 forM_ (List.take 1 titles) $ \(DTC.Title title) -> do
480 html5ify $ TreeN DTC.Q $
483 Just u -> pure $ TreeN (DTC.Eref u) title
486 H.a ! HA.class_ "rref"
487 ! HA.href ("#rref."<>attrify to)
488 ! HA.id ("rref."<>attrify to<>maybe "" (\DTC.Anchor{..} -> "."<>attrify count) anchor) $$
491 instance Html5ify DTC.URL where
492 html5ify (DTC.URL url) =
493 H.a ! HA.class_ "eref"
494 ! HA.href (attrify url) $$
497 instance Attrify DTC.Words where
499 "iref" <> "." <> attrify (Anchor.plainifyWords term)
500 instance Attrify (DTC.Words,DTC.Nat1) where
501 attrify (term,count) =
503 <> "." <> attrify (Anchor.plainifyWords term)
504 <> "." <> attrify count
505 instance Html5ify DTC.Date where
506 html5ify = html5ify . Plain.L10n_Date
507 instance Html5ify DTC.About where
508 html5ify DTC.About{..} =
509 html5CommasDot $ concat $
510 [ (<$> List.take 1 titles) $ \(DTC.Title title) ->
511 html5ify $ TreeN DTC.Q $
514 Just u -> pure $ TreeN (DTC.Eref u) title
515 , html5Entity <$> authors
516 , html5ify <$> maybeToList date
517 , html5Entity <$> maybeToList editor
518 , html5Serie <$> series
521 html5Serie DTC.Serie{..} = do
523 html5ify Plain.L10n_Colon
525 html5Entity DTC.Entity{url=mu, ..} =
526 html5ify @DTC.Lines $
528 _ | not (Text.null email) ->
529 TreeN (DTC.Eref $ DTC.URL $ "mailto:"<>email) $
530 pure $ Tree0 $ DTC.Plain name
533 pure $ Tree0 $ DTC.Plain name
534 _ -> Tree0 $ DTC.Plain name
535 instance Html5ify DTC.Reference where
536 html5ify DTC.Reference{id=id_, ..} =
538 H.td ! HA.class_ "reference-key" $$
539 html5ify @DTC.Lines $ TreeN DTC.Rref{anchor=Nothing, to=id_} Seq.empty
540 H.td ! HA.class_ "reference-content" $$ do
542 rrefs <- liftStateMarkup $ S.gets state_rrefs
543 case Map.lookup id_ rrefs of
546 H.span ! HA.class_ "reference-rrefs" $$
548 (<$> List.reverse anchs) $ \DTC.Anchor{..} ->
549 H.a ! HA.class_ "reference-rref"
550 ! HA.href ("#rref."<>attrify id_<>"."<>attrify count) $$
551 html5ify $ DTC.posAncestors section
553 html5CommasDot :: [Html5] -> Html5
554 html5CommasDot [] = pure ()
555 html5CommasDot hs = do
556 sequence_ $ List.intersperse ", " hs
559 html5CommonAttrs :: DTC.CommonAttrs -> Html5 -> Html5
560 html5CommonAttrs DTC.CommonAttrs{id=id_, ..} =
561 Compose . (addClass . addId <$>) . getCompose
566 _ -> H.AddCustomAttribute "class" (H.Text $ Text.unwords classes)
567 addId = maybe id (\(DTC.Ident i) ->
568 H.AddCustomAttribute "id" (H.Text i)) id_
570 html5SectionNumber :: DTC.PosPath -> Html5
571 html5SectionNumber = go mempty
573 go :: DTC.PosPath -> DTC.PosPath -> Html5
575 case Seq.viewl next of
576 Seq.EmptyL -> pure ()
577 a@(_n,rank) Seq.:< as -> do
578 H.a ! HA.href ("#"<>attrify (prev Seq.|>a)) $$
580 when (not (null as) || null prev) $ do
584 html5SectionRef :: DTC.PosPath -> Html5
586 H.a ! HA.href ("#"<>attrify as) $$
589 instance Html5ify DTC.PosPath where
597 Text.intercalate "." $
598 Text.pack . show . snd <$> as
599 instance Html5ify Plain where
601 sp <- liftStateMarkup $ S.gets state_plainify
602 let (t,sp') = Plain.runPlain p sp
604 liftStateMarkup $ S.modify $ \s -> s{state_plainify=sp'}
605 instance Attrify Plain where
607 let (t,_) = Plain.runPlain p def in
610 instance Attrify DTC.PosPath where
611 attrify = attrify . plainify
612 instance Attrify DTC.Pos where
613 attrify = attrify . DTC.posAncestors
616 instance Html5ify Plain.L10n where
617 html5ify = html5ify . plainify
618 instance Localize ls Plain Plain.L10n => Localize ls Html5 Plain.L10n where
619 localize loc a = html5ify (Locale.localize loc a::Plain)
620 instance LocalizeIn FR Html5 Plain.L10n where
621 localizeIn loc = html5ify @Plain . localizeIn loc
622 instance LocalizeIn EN Html5 Plain.L10n where
623 localizeIn loc = html5ify @Plain . localizeIn loc