1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DuplicateRecordFields #-}
3 {-# LANGUAGE ExistentialQuantification #-}
4 {-# LANGUAGE FlexibleInstances #-}
5 {-# LANGUAGE FlexibleContexts #-}
6 {-# LANGUAGE MultiParamTypeClasses #-}
7 {-# LANGUAGE OverloadedStrings #-}
8 {-# LANGUAGE ScopedTypeVariables #-}
9 {-# LANGUAGE TypeApplications #-}
10 {-# LANGUAGE ViewPatterns #-}
11 {-# OPTIONS_GHC -fno-warn-orphans #-}
12 module Hdoc.DTC.Write.HTML5 where
14 import Control.Applicative (Applicative(..))
15 import Control.Category as Cat
18 import Data.Char (Char)
19 import Data.Default.Class (Default(..))
20 import Data.Either (Either(..))
21 import Data.Eq (Eq(..))
22 import Data.Foldable (Foldable(..), concat, any)
23 import Data.Function (($), const, on)
24 import Data.Functor ((<$>))
25 import Data.Functor.Compose (Compose(..))
27 import Data.IntMap.Strict (IntMap)
28 import Data.Map.Strict (Map)
29 import Data.Maybe (Maybe(..), maybe, mapMaybe, fromJust, maybeToList, listToMaybe, fromMaybe, isJust)
30 import Data.Monoid (Monoid(..))
31 import Data.Ord (Ord(..))
32 import Data.Semigroup (Semigroup(..))
33 import Data.String (String, IsString(..))
34 import Data.Text (Text)
35 import Data.TreeSeq.Strict (Tree(..), tree0)
36 import Data.Tuple (snd)
37 import Prelude (mod, (*), Fractional(..), Double, toRational, RealFrac(..), error)
38 import System.FilePath (FilePath)
39 import Text.Blaze ((!))
40 import Text.Blaze.Html (Html)
41 import Text.Show (Show(..))
42 import qualified Control.Monad.Trans.State as S
43 import qualified Data.Char as Char
44 import qualified Data.HashMap.Strict as HM
45 import qualified Data.HashSet as HS
46 import qualified Data.List as List
47 import qualified Data.IntMap.Strict as IntMap
48 import qualified Data.Map.Strict as Map
49 import qualified Data.Sequence as Seq
50 import qualified Data.Set as Set
51 import qualified Data.Strict.Maybe as Strict
52 import qualified Data.Text as Text
53 import qualified Data.Text.Lazy as TL
54 import qualified Data.Tree as Tree
55 import qualified Data.TreeMap.Strict as TreeMap
56 import qualified Data.TreeSeq.Strict as TreeSeq
57 import qualified Hjugement as MJ
58 import qualified Text.Blaze.Html5 as H
59 import qualified Text.Blaze.Html5.Attributes as HA
60 import qualified Text.Blaze.Internal as H
62 import Text.Blaze.Utils
63 import Data.Locale hiding (Index)
66 import Hdoc.DTC.Document as DTC
67 import Hdoc.DTC.Write.Plain (Plainify(..))
68 import Hdoc.DTC.Write.XML ()
69 import Hdoc.DTC.Write.HTML5.Ident
70 import qualified Hdoc.DTC.Collect as Collect
71 import qualified Hdoc.DTC.Index as Index
72 import qualified Hdoc.DTC.Check as Check
73 import qualified Hdoc.DTC.Write.Plain as Plain
76 debug :: Show a => String -> a -> a
77 debug msg a = trace (msg<>": "<>show a) a
78 debugOn :: Show b => String -> (a -> b) -> a -> a
79 debugOn msg get a = trace (msg<>": "<>show (get a)) a
80 debugWith :: String -> (a -> String) -> a -> a
81 debugWith msg get a = trace (msg<>": "<>get a) a
83 showJudgments :: HM.HashMap (Ident,Ident,Maybe Title) [Tree.Tree [Choice]] -> String
87 -- Tree.Node (Left ("","",Nothing)) $
88 (<$> HM.toList js) $ \((j,g,q),ts) ->
90 (Left (unIdent j,unIdent g,Plain.text def <$> q))
94 type Html5 = StateMarkup State ()
95 instance IsString Html5 where
102 , Loqualize locales (L10n Html5)
103 , Loqualize locales (Plain.L10n Plain.Plain)
106 { config_css :: Either FilePath TL.Text
107 , config_locale :: LocaleIn locales
108 , config_generator :: TL.Text
110 instance Default Config where
112 { config_css = Right "style/dtc-html5.css"
113 , config_locale = LocaleIn @'[EN] en_US
114 , config_generator = "https://hackage.haskell.org/package/hdoc"
120 { state_styles :: Map FilePath TL.Text
121 , state_scripts :: Map FilePath TL.Text
122 , state_notes :: Check.NotesBySection
123 , state_judgments :: HS.HashSet Judgment
124 , state_opinions :: HM.HashMap Judgment [MJ.OpinionsByChoice Choice Name (MJ.Ranked Grade)]
126 , state_section :: TreeSeq.Trees BodyNode
127 , state_collect :: Collect.All
128 , state_indexs :: Map Pos (Terms, Index.Irefs) -- TODO: could be a list
129 , state_rrefs :: Check.Rrefs
130 , state_plainify :: Plain.State
131 , state_l10n :: Loqualization (L10n Html5)
133 instance Default State where
136 , state_scripts = def
137 , state_section = def
138 , state_collect = def
142 , state_plainify = def
143 , state_l10n = Loqualization EN_US
144 , state_judgments = HS.empty
145 , state_opinions = def
148 writeHTML5 :: Config -> DTC.Document -> Html
149 writeHTML5 conf@Config{..} doc@DTC.Document{..} = do
150 let state_collect@Collect.All{..} = Collect.collect doc
151 let (checkedBody,Check.State{..}) =
152 Check.check body `S.runState` def
153 { Check.state_irefs = foldMap Index.irefsOfTerms all_index }
154 let (html5Body, endState) =
158 (<$> all_index) $ \terms ->
160 TreeMap.intersection const state_irefs $
161 Index.irefsOfTerms terms
164 , state_section = body
165 , state_l10n = loqualize config_locale
166 , state_plainify = def{Plain.state_l10n = loqualize config_locale}
169 html5DocumentHead head
172 H.html ! HA.lang (attrify $ countryCode config_locale) $ do
173 html5Head conf endState head
176 html5Head :: Config -> State -> Head -> Html
177 html5Head Config{..} State{..} Head{DTC.about=About{..}} = do
179 H.meta ! HA.httpEquiv "Content-Type"
180 ! HA.content "text/html; charset=UTF-8"
181 unless (null titles) $ do
183 H.toMarkup $ Plain.text state_plainify $ List.head titles
184 forM_ links $ \Link{..} ->
186 "stylesheet" | URL "" <- href ->
187 H.style ! HA.type_ "text/css" $
188 H.toMarkup $ Plain.text def plain
190 H.link ! HA.rel (attrify rel)
191 ! HA.href (attrify href)
193 H.link ! HA.rel "self"
194 ! HA.href (attrify href)
195 unless (TL.null config_generator) $ do
196 H.meta ! HA.name "generator"
197 ! HA.content (attrify config_generator)
199 H.meta ! HA.name "keywords"
200 ! HA.content (attrify $ TL.intercalate ", " tags)
202 (`mapMaybe` toList state_section) $ \case
203 Tree k@BodySection{} _ -> Just k
205 forM_ chapters $ \case
207 H.link ! HA.rel "Chapter"
208 ! HA.title (attrify $ plainify title)
209 ! HA.href (refIdent $ identify pos)
211 unless (any (\DTC.Link{..} -> rel == "stylesheet" && href /= URL "") links) $ do
215 H.link ! HA.rel "stylesheet"
216 ! HA.type_ "text/css"
217 ! HA.href (attrify css)
219 H.style ! HA.type_ "text/css" $
220 -- NOTE: as a special case, H.style wraps its content into an External,
221 -- so it does not HTML-escape its content.
223 forM_ state_styles $ \style ->
224 H.style ! HA.type_ "text/css" $
226 unless (any (\DTC.Link{rel} -> rel == "script") links) $ do
227 forM_ state_scripts $ \script ->
228 H.script ! HA.type_ "application/javascript" $
231 html5DocumentHead :: Head -> Html5
232 html5DocumentHead Head{DTC.about=About{..}, judgments} = do
233 unless (null authors) $ do
234 H.div ! HA.class_ "document-head" $$
238 H.td ! HA.class_ "left" $$ docHeaders
239 H.td ! HA.class_ "right" $$ docAuthors
240 unless (null titles) $
241 H.div ! HA.class_ "title" $$ do
242 forM_ titles $ \title ->
243 H.h1 $$ html5ify title
244 st <- liftStateMarkup S.get
246 let sectionJudgments = HS.fromList judgments
247 let opinsBySectionByJudgment = state_opinions st `HM.intersection` HS.toMap sectionJudgments
248 liftStateMarkup $ S.modify' $ \s ->
249 s{ state_judgments = sectionJudgments
251 -- NOTE: drop current opinions of the judgments of this section
252 HM.unionWith (const List.tail)
254 opinsBySectionByJudgment
256 unless (null opinsBySectionByJudgment) $ do
257 let choicesJ = Collect.choicesByJudgment judgments
258 forM_ (HM.toList opinsBySectionByJudgment) $ \(judgment@Judgment{question},opinsBySection) -> do
259 H.div ! HA.class_ "judgment section-judgment document-judgment" $$ do
260 let choices = maybe [] snd $ HM.lookup judgment choicesJ
261 let opins = List.head opinsBySection
262 html5Judgment question choices opins
265 H.table ! HA.class_ "document-headers" $$
267 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
268 forM_ series $ \s@Serie{id=id_, name} ->
272 headerName $ html5ify name
273 headerValue $ html5ify id_
275 headerName $ html5ify name
277 H.a ! HA.href (attrify href) $$
279 forM_ links $ \Link{..} ->
280 unless (TL.null $ unName name) $
282 headerName $ html5ify name
283 headerValue $ html5ify $ Tree PlainEref{href} plain
286 headerName $ l10n_Header_Date loc
287 headerValue $ html5ify d
290 headerName $ l10n_Header_Address loc
291 headerValue $ html5ify $ tree0 $ PlainEref{href}
292 forM_ headers $ \Header{..} ->
294 headerName $ html5ify name
295 headerValue $ html5ify value
297 H.table ! HA.class_ "document-authors" $$
299 forM_ authors $ \a ->
301 H.td ! HA.class_ "author" $$
303 header :: Html5 -> Html5
304 header hdr = H.tr ! HA.class_ "header" $$ hdr
305 headerName :: Html5 -> Html5
307 H.td ! HA.class_ "header-name" $$ do
309 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
311 headerValue :: Html5 -> Html5
313 H.td ! HA.class_ "header-value" $$ do
316 -- * Class 'Html5ify'
317 class Html5ify a where
318 html5ify :: a -> Html5
319 instance Html5ify H.Markup where
320 html5ify = Compose . return
321 instance Html5ify Char where
322 html5ify = html5ify . H.toMarkup
323 instance Html5ify Text where
324 html5ify = html5ify . H.toMarkup
325 instance Html5ify TL.Text where
326 html5ify = html5ify . H.toMarkup
327 instance Html5ify String where
328 html5ify = html5ify . H.toMarkup
329 instance Html5ify Title where
330 html5ify (Title t) = html5ify t
331 instance Html5ify Ident where
332 html5ify (Ident i) = html5ify i
333 instance Html5ify Int where
334 html5ify = html5ify . show
335 instance Html5ify Name where
336 html5ify (Name i) = html5ify i
337 instance Html5ify Nat where
338 html5ify (Nat n) = html5ify n
339 instance Html5ify Nat1 where
340 html5ify (Nat1 n) = html5ify n
341 instance Html5ify a => Html5ify (Maybe a) where
342 html5ify = foldMap html5ify
343 instance Html5ify Body where
345 liftStateMarkup $ S.modify' $ \s -> s{state_section = body}
347 case Seq.viewr body of
348 _ Seq.:> Tree BodyBlock{} _ -> do
349 notes <- liftStateMarkup $ S.gets state_notes
350 maybe mempty html5Notes $
351 Map.lookup mempty notes
353 instance Html5ify (Tree BodyNode) where
354 html5ify (Tree b bs) =
356 BodyBlock blk -> html5ify blk
357 BodySection{..} -> do
358 st <- liftStateMarkup S.get
359 liftStateMarkup $ S.modify' $ \s -> s{state_section = bs}
362 sectionPosPath <- dropSelfPosPath $ pos_Ancestors pos
363 let (sectionNotes, notes) = Map.updateLookupWithKey (\_ _ -> Nothing) sectionPosPath $ state_notes st
364 (,notes) <$> sectionNotes
367 Just (sectionNotes, state_notes) -> do
368 liftStateMarkup $ S.modify' $ \s -> s{state_notes}
369 html5Notes sectionNotes
370 html5CommonAttrs attrs{classes="section":classes attrs} $
371 H.section ! HA.id (attrify $ identify pos) $$ do
372 forM_ aliases html5ify
374 let sectionJudgments = state_judgments st `HS.union` HS.fromList judgments
375 let opinsBySectionByJudgment = state_opinions st `HM.intersection` HS.toMap sectionJudgments
376 let dropChildrenBlocksJudgments =
377 -- NOTE: drop the "phantom" judgments concerning the 'BodyBlock's
378 -- directly children of this 'BodySection'.
380 Tree BodyBlock{} _ -> True
384 liftStateMarkup $ S.modify' $ \s ->
385 s{ state_judgments = sectionJudgments
387 -- NOTE: drop current opinions of the judgments of this section
388 HM.unionWith (const $ List.tail . dropChildrenBlocksJudgments)
390 opinsBySectionByJudgment
392 unless (null opinsBySectionByJudgment) $ do
393 H.aside ! HA.class_ "aside" $$ do
394 let choicesJ = Collect.choicesByJudgment judgments
395 forM_ (HM.toList opinsBySectionByJudgment) $ \(judgment@Judgment{question},opinsBySection) -> do
396 H.div ! HA.class_ "judgment section-judgment" $$ do
397 let choices = maybe [] snd $ HM.lookup judgment choicesJ
398 let opins = List.head opinsBySection
399 html5Judgment question choices opins
401 ! HA.id (attrify $ escapeIdent $ identify title)
402 ! HA.class_ "section-header" $$
405 H.td ! HA.class_ "section-number" $$ do
406 html5SectionNumber $ pos_Ancestors pos
407 H.td ! HA.class_ "section-title" $$ do
408 (case List.length $ pos_Ancestors pos of
419 liftStateMarkup $ S.modify' $ \s ->
420 s{ state_judgments = state_judgments st }
422 notes <- liftStateMarkup $ S.gets state_notes
423 maybe mempty html5Notes $
424 Map.lookup (pos_Ancestors pos) notes
425 liftStateMarkup $ S.modify' $ \s -> s{state_section = state_section st}
426 instance Html5ify Block where
428 BlockPara para -> html5ify para
430 html5CommonAttrs attrs
431 { classes = "page-break":"print-only":classes attrs } $
433 H.p $$ " " -- NOTE: force page break
435 H.nav ! HA.class_ "toc"
436 ! HA.id (attrify $ identify pos) $$ do
437 H.span ! HA.class_ "toc-name" $$
438 H.a ! HA.href (refIdent $ identify pos) $$ do
439 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
440 Plain.l10n_Table_of_Contents loc
442 State{state_section} <- liftStateMarkup S.get
443 forM_ state_section $ html5ifyToC depth
445 H.nav ! HA.class_ "tof"
446 ! HA.id (attrify $ identify pos) $$
447 H.table ! HA.class_ "tof" $$
451 html5CommonAttrs attrs $
452 H.aside ! HA.class_ "aside" $$ do
453 forM_ blocks html5ify
455 html5CommonAttrs attrs
456 { classes = "figure":("figure-"<>type_):classes attrs
457 , DTC.id = Just $ Ident $ Plain.text def $ pos_AncestorsWithFigureNames pos
460 H.table ! HA.class_ "figure-caption" $$
464 then H.a ! HA.href (refIdent $ identify pos) $$ mempty
466 H.td ! HA.class_ "figure-number" $$ do
467 H.a ! HA.href (refIdent $ identify $ pos_AncestorsWithFigureNames pos) $$ do
469 html5ify $ pos_AncestorsWithFigureNames pos
470 forM_ mayTitle $ \title -> do
471 H.td ! HA.class_ "figure-colon" $$ do
472 unless (TL.null type_) $ do
473 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
475 H.td ! HA.class_ "figure-title" $$ do
477 H.div ! HA.class_ "figure-content" $$ do
479 BlockIndex{pos} -> do
480 (allTerms,refsByTerm) <- liftStateMarkup $ S.gets $ (Map.! pos) . state_indexs
481 let chars = Index.termsByChar allTerms
482 H.div ! HA.class_ "index"
483 ! HA.id (attrify $ identify pos) $$ do
484 H.nav ! HA.class_ "index-nav" $$ do
485 forM_ (Map.keys chars) $ \char ->
486 H.a ! HA.href (refIdent (identify pos <> "." <> identify char)) $$
488 H.dl ! HA.class_ "index-chars" $$
489 forM_ (Map.toList chars) $ \(char,terms) -> do
491 let i = identify pos <> "." <> identify char
492 H.a ! HA.id (attrify i)
493 ! HA.href (refIdent i) $$
496 H.dl ! HA.class_ "index-term" $$ do
497 forM_ terms $ \aliases -> do
499 H.ul ! HA.class_ "index-aliases" $$
500 forM_ (List.take 1 aliases) $ \term -> do
501 H.li ! HA.id (attrify $ identifyIref term) $$
505 List.sortBy (compare `on` DTC.section . snd) $
506 (`foldMap` aliases) $ \words ->
508 path <- Index.pathFromWords words
509 Strict.maybe Nothing (Just . ((words,) <$>) . List.reverse) $
510 TreeMap.lookup path refsByTerm in
512 (<$> anchs) $ \(term,Anchor{..}) ->
513 H.a ! HA.class_ "index-iref"
514 ! HA.href (refIdent $ identifyIrefCount term count) $$
515 html5ify $ pos_Ancestors section
516 BlockReferences{..} ->
517 html5CommonAttrs attrs
518 { classes = "references":classes attrs
519 , DTC.id = Just $ Ident $ Plain.text def $ pos_Ancestors pos
525 html5CommonAttrs attrs
526 { classes = "grades":classes attrs
527 , DTC.id = Just $ Ident $ Plain.text def $ pos_Ancestors pos
530 -- let dg = List.head $ List.filter default_ scale
531 -- let sc = MJ.Scale (Set.fromList scale) dg
532 -- o :: Map choice grade
533 -- os :: Opinions (Map judge (Opinion choice grade))
537 html5CommonAttrs attrs
538 { classes = "judges":classes attrs
539 , DTC.id = Just $ Ident $ Plain.text def $ pos_Ancestors pos
543 instance Html5ify Para where
547 { classes="para":cls item
551 html5CommonAttrs attrs
552 { classes = "para":classes attrs
556 forM_ items $ \item ->
557 html5AttrClass (cls item) $
560 id_ = Just . Ident . Plain.text def . pos_Ancestors
563 ParaArtwork{..} -> ["artwork", "artwork-"<>type_]
564 ParaQuote{..} -> ["quote", "quote-"<>type_]
568 ParaJudgment{} -> ["judgment"]
569 instance Html5ify ParaItem where
571 ParaPlain p -> H.p $$ html5ify p
572 ParaArtwork{..} -> H.pre $$ do html5ify text
573 ParaQuote{..} -> H.div $$ do html5ify paras
574 ParaComment t -> html5ify $ H.Comment (H.String $ TL.unpack t) ()
578 forM_ items $ \ListItem{..} -> do
580 H.td ! HA.class_ "name" $$ do
583 H.td ! HA.class_ "value" $$
587 forM_ items $ \item -> do
589 H.dd $$ html5ify item
590 ParaJudgment j -> html5ify j
591 instance Html5ify Judgment where
592 html5ify Judgment{..} = do
593 st <- liftStateMarkup S.get
596 maybe (error $ show grades) MJ.grades $ -- unknown grades
597 HM.lookup grades (Collect.all_grades $ state_collect st)
599 fromMaybe (error $ show judges) $ -- unknown judges
600 HM.lookup judges (Collect.all_judges $ state_collect st)
601 let defaultGradeByJudge =
604 [ g | g <- Set.toList judgmentGrades
605 , isDefault $ MJ.unRank g
608 [ (name, defaultGrade`fromMaybe`judgeDefaultGrade)
609 | DTC.Judge{name,defaultGrades} <- judgmentJudges
610 , let judgeDefaultGrade = do
611 jdg <- listToMaybe [g | (n,g) <- defaultGrades, n == grades]
613 [ g | g <- Set.toList judgmentGrades
614 , let DTC.Grade{name=n} = MJ.unRank g
618 judgmentChoices <- forM choices $ \c@DTC.Choice{opinions} -> do
619 gradeByJudge <- forM opinions $ \DTC.Opinion{judge,grade} -> do
621 fromMaybe (error $ show grade) $ -- unknown grade
623 [ MJ.singleGrade g | g <- Set.toList judgmentGrades
624 , let Grade{name} = MJ.unRank g
628 case MJ.opinions defaultGradeByJudge $ HM.fromList gradeByJudge of
629 (ok,ko) | null ko -> return (c, ok)
630 | otherwise -> error $ show ko -- unknown judge
632 html5Judgment question choices $ HM.fromList judgmentChoices
633 instance Html5ify [Para] where
634 html5ify = mapM_ html5ify
635 instance Html5ify Plain where
641 -- NOTE: gather adjacent PlainNotes
643 | (notes, rest) <- Seq.spanl (\case {Tree PlainNote{} _ -> True; _ -> False}) next -> do
644 H.sup ! HA.class_ "note-numbers" $$ do
646 forM_ notes $ \note -> do
655 instance Html5ify (Tree PlainNode)
656 where html5ify (Tree n ls) =
658 PlainBreak -> html5ify H.br
659 PlainText t -> html5ify t
660 PlainGroup -> html5ify ls
661 PlainB -> H.strong $$ html5ify ls
662 PlainCode -> H.code $$ html5ify ls
663 PlainDel -> H.del $$ html5ify ls
665 i <- liftStateMarkup $ do
666 i <- S.gets $ Plain.state_italic . state_plainify
669 (state_plainify s){Plain.state_italic=
672 H.em ! HA.class_ (if i then "even" else "odd") $$
677 (state_plainify s){Plain.state_italic=i}}
679 html5CommonAttrs attrs $
680 H.span $$ html5ify ls
681 PlainSub -> H.sub $$ html5ify ls
682 PlainSup -> H.sup $$ html5ify ls
683 PlainSC -> H.span ! HA.class_ "smallcaps" $$ html5ify ls
684 PlainU -> H.span ! HA.class_ "underline" $$ html5ify ls
687 Nothing -> error "[BUG] PlainNote has no number."
689 H.a ! HA.class_ "note-ref"
690 ! HA.id ("note-ref."<>attrify num)
691 ! HA.href ("#note."<>attrify num) $$
694 H.span ! HA.class_ "q" $$ do
695 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
696 Plain.l10n_Quote (html5ify $ Tree PlainI ls) loc
698 H.a ! HA.class_ "eref"
699 ! HA.href (attrify href) $$
701 then html5ify $ unURL href
705 Nothing -> html5ify ls
707 H.span ! HA.class_ "iref"
708 ! HA.id (attrify $ identifyIrefCount term count) $$
711 H.a ! HA.class_ "ref"
712 ! HA.href (refIdent $ escapeIdent to) $$
714 then html5ify $ unIdent to
717 refs <- liftStateMarkup $ S.gets $ Collect.all_reference . state_collect
718 case Map.lookup to refs of
721 H.span ! HA.class_ "rref-broken" $$
726 forM_ (List.take 1 titles) $ \(Title title) -> do
727 html5ify $ Tree PlainQ $
730 Just u -> pure $ Tree (PlainEref u) title
733 H.a ! HA.class_ "rref"
734 ! HA.href ("#rref."<>attrify to)
735 ! HA.id ("rref."<>attrify to<>maybe "" (\Anchor{..} -> "."<>attrify count) anchor) $$
738 instance Html5ify [Title] where
740 html5ify . fold . List.intersperse sep . toList
741 where sep = Title $ Seq.singleton $ tree0 $ PlainText " — "
742 instance Html5ify About where
743 html5ify About{..} = do
745 [ html5CommasDot $ concat $
747 , html5ify <$> authors
748 , html5ify <$> maybeToList date
749 , html5ify <$> maybeToList editor
750 , html5ify <$> series
753 H.span ! HA.class_ "print-only" $$ do
759 html5Titles :: [Title] -> [Html5]
760 html5Titles ts | null ts = []
761 html5Titles ts = [html5Title $ joinTitles ts]
763 joinTitles = fold . List.intersperse sep . toList
764 sep = Title $ Seq.singleton $ tree0 $ PlainText " — "
765 html5Title (Title title) =
766 html5ify $ Tree PlainQ $
769 Just u -> pure $ Tree (PlainEref u) title
770 instance Html5ify Serie where
771 html5ify s@Serie{id=id_, name} = do
772 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
776 Plain.l10n_Colon loc :: Html5
780 Tree PlainEref{href} $
782 [ tree0 $ PlainText $ unName name
783 , tree0 $ PlainText $ Plain.l10n_Colon loc
784 , tree0 $ PlainText id_
786 instance Html5ify Entity where
787 html5ify Entity{..} = do
789 _ | not (TL.null email) -> do
790 H.span ! HA.class_ "no-print" $$
792 Tree (PlainEref $ URL $ "mailto:"<>email) $
793 pure $ tree0 $ PlainText name
794 H.span ! HA.class_ "print-only" $$
796 Tree PlainGroup $ Seq.fromList
797 [ tree0 $ PlainText name
798 , tree0 $ PlainText " <"
799 , Tree (PlainEref $ URL $ "mailto:"<>email) $
800 pure $ tree0 $ PlainText email
801 , tree0 $ PlainText ">"
806 pure $ tree0 $ PlainText name
809 tree0 $ PlainText name
814 instance Html5ify Words where
815 html5ify = html5ify . Index.plainifyWords
816 instance Html5ify Alias where
817 html5ify Alias{id=id_, ..} = do
818 H.a ! HA.class_ "alias"
819 ! HA.id (attrify $ identify id_) $$
821 instance Html5ify URL where
823 H.a ! HA.class_ "eref"
824 ! HA.href (attrify url) $$
826 instance Html5ify Date where
828 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
829 Plain.l10n_Date date loc
830 instance Html5ify Reference where
831 html5ify Reference{id=id_, ..} =
833 H.td ! HA.class_ "reference-key" $$
834 html5ify $ Tree PlainRref{anchor=Nothing, to=id_} Seq.empty
835 H.td ! HA.class_ "reference-content" $$ do
837 rrefs <- liftStateMarkup $ S.gets state_rrefs
838 case Map.lookup id_ rrefs of
841 H.span ! HA.class_ "reference-rrefs" $$
843 (<$> List.reverse anchs) $ \Anchor{..} ->
844 H.a ! HA.class_ "reference-rref"
845 ! HA.href ("#rref."<>attrify id_<>"."<>attrify count) $$
846 html5ify $ pos_Ancestors section
847 instance Html5ify PosPath where
855 Text.intercalate "." $
856 Text.pack . show . snd <$> as
857 instance Html5ify Plain.Plain where
859 sp <- liftStateMarkup $ S.gets state_plainify
860 let (t,sp') = Plain.runPlain p sp
862 liftStateMarkup $ S.modify $ \s -> s{state_plainify=sp'}
864 instance Html5ify SVG.Element where
867 B.preEscapedLazyText $
869 instance Semigroup SVG.Element where
873 html5CommasDot :: [Html5] -> Html5
874 html5CommasDot [] = pure ()
875 html5CommasDot hs = do
876 sequence_ $ List.intersperse ", " hs
879 html5Lines :: [Html5] -> Html5
880 html5Lines hs = sequence_ $ List.intersperse (html5ify H.br) hs
882 html5Words :: [Html5] -> Html5
883 html5Words hs = sequence_ $ List.intersperse " " hs
885 html5AttrClass :: [TL.Text] -> Html5 -> Html5
886 html5AttrClass = \case
890 (H.AddCustomAttribute "class"
891 (H.String $ TL.unpack $ TL.unwords cls) <$>) .
894 html5AttrId :: Ident -> Html5 -> Html5
895 html5AttrId (Ident id_) =
897 (H.AddCustomAttribute "id"
898 (H.String $ TL.unpack id_) <$>) .
901 html5CommonAttrs :: CommonAttrs -> Html5 -> Html5
902 html5CommonAttrs CommonAttrs{id=id_, ..} =
903 html5AttrClass classes .
904 maybe Cat.id html5AttrId id_
906 html5SectionNumber :: PosPath -> Html5
907 html5SectionNumber = go mempty
909 go :: PosPath -> PosPath -> Html5
911 case Seq.viewl next of
912 Seq.EmptyL -> pure ()
913 a@(_n,rank) Seq.:< as -> do
914 H.a ! HA.href (refIdent $ identify $ prev Seq.|> a) $$
916 when (not (null as) || null prev) $ do
920 html5SectionRef :: PosPath -> Html5
922 H.a ! HA.href (refIdent $ identify as) $$
925 html5Notes :: IntMap [Para] -> Html5
927 H.aside ! HA.class_ "notes" $$ do
931 forM_ (IntMap.toList notes) $ \(number,content) ->
933 H.td ! HA.class_ "note-ref" $$ do
934 H.a ! HA.class_ "note-number"
935 ! HA.id ("note."<>attrify number)
936 ! HA.href ("#note."<>attrify number) $$ do
939 H.a ! HA.href ("#note-ref."<>attrify number) $$ do
944 html5ifyToC :: Maybe DTC.Nat -> Tree BodyNode -> Html5
945 html5ifyToC depth (Tree b bs) =
947 BodySection{..} -> do
949 H.table ! HA.class_ "toc-entry" $$
952 H.td ! HA.class_ "section-number" $$
953 html5SectionRef $ pos_Ancestors pos
954 H.td ! HA.class_ "section-title" $$
955 html5ify $ cleanPlain $ unTitle title
956 when (maybe True (> Nat 1) depth && not (null sections)) $
959 html5ifyToC (depth >>= predNat)
963 (`Seq.filter` bs) $ \case
964 Tree BodySection{} _ -> True
967 html5ifyToF :: [TL.Text] -> Html5
968 html5ifyToF types = do
969 figuresByType <- liftStateMarkup $ S.gets $ Collect.all_figure . state_collect
971 Map.foldMapWithKey (\ty -> ((ty,) <$>)) $
975 Map.intersection figuresByType $
976 Map.fromList [(ty,()) | ty <- types]
977 forM_ (Map.toList figures) $ \(pos, (type_, title)) ->
979 H.td ! HA.class_ "figure-number" $$
980 H.a ! HA.href (refIdent $ identify pos) $$ do
982 html5ify $ pos_Ancestors pos
984 H.td ! HA.class_ "figure-title" $$
985 html5ify $ cleanPlain $ unTitle ti
987 cleanPlain :: Plain -> Plain
990 Tree PlainIref{} ls -> cleanPlain ls
991 Tree PlainNote{} _ -> mempty
992 Tree n ts -> pure $ Tree n $ cleanPlain ts
997 MJ.OpinionsByChoice Choice Name (MJ.Ranked Grade) ->
999 html5Judgment question choices distByJudgeByChoice = do
1000 let commentJGC = HM.fromList
1001 [ (choice_, HM.fromListWith (<>)
1002 [ (grade, HM.singleton judge comment)
1003 | Opinion{..} <- opinions ])
1004 | choice_@Choice{opinions} <- choices ]
1007 Just title -> H.div ! HA.class_ "question" $$ html5ify title
1008 H.dl ! HA.class_ "choices" $$ do
1009 let meritByChoice@(MJ.MeritByChoice meritC) = MJ.meritByChoice distByJudgeByChoice
1010 let ranking = MJ.majorityRanking meritByChoice
1011 forM_ ranking $ \(choice_@DTC.Choice{title}, majorityValue) -> do
1012 H.dt ! HA.class_ "choice-title" $$ do
1014 H.dd ! HA.class_ "choice-merit" $$ do
1015 let distByJudge = distByJudgeByChoice HM.!choice_
1016 let numJudges = HM.size distByJudge
1017 html5MeritHistogram majorityValue numJudges
1018 let grades = Map.keys $ MJ.unMerit $ meritC HM.!choice_
1019 let commentJG = HM.lookup choice_ commentJGC
1020 html5MeritComments distByJudge grades commentJG
1022 html5MeritComments ::
1023 MJ.Opinions Name (MJ.Ranked Grade) ->
1024 [MJ.Ranked Grade] ->
1025 Maybe (HM.HashMap Name (HM.HashMap Name (Maybe Title))) ->
1027 html5MeritComments distJ grades commentJG = do
1028 Loqualization loc <- liftStateMarkup $ S.gets state_l10n
1029 H.ul ! HA.class_ "merit-comments" $$ do
1030 forM_ grades $ \grade@(MJ.unRank -> DTC.Grade{name=grade_name, color}) -> do
1031 let commentJ = commentJG >>= HM.lookup grade_name
1032 let judgesWithComment =
1033 -- FIXME: sort accents better: « e é f » not « e f é »
1034 List.sortOn (TL.map Char.toLower . unName . (\(j,_,_) -> j))
1035 [ (judge, importance, commentJ >>= HM.lookupDefault Nothing judge)
1036 | (judge, dist) <- HM.toList distJ
1037 , importance <- maybeToList $ Map.lookup grade dist ]
1038 forM_ judgesWithComment $ \(judge, importance, comment) ->
1039 H.li ! HA.class_ ("merit-comment" <> if isJust comment then " judge-comment" else "") $$ do
1041 ! HA.class_ ("judge" <> if judge`HM.member`fromMaybe HM.empty commentJ then "" else " inactive")
1042 ! HA.style ("color:"<>attrify color<>";") $$ do
1043 unless (importance == 1) $ do
1044 H.span ! HA.class_ "section-importance" $$ do
1046 (round::Double -> Int) $
1047 fromRational $ importance * 100
1048 html5ify $ show percent
1054 Plain.l10n_Colon loc :: Html5
1057 html5MeritHistogram :: MJ.MajorityValue (MJ.Ranked Grade) -> Int -> Html5
1058 html5MeritHistogram (MJ.MajorityValue majVal) numJudges = do
1059 H.div ! HA.class_ "merit-histogram" $$ do
1060 forM_ majVal $ \(MJ.unRank -> DTC.Grade{name=grade_name, title=grade_title, color},count) -> do
1061 let percent :: Double =
1062 fromRational $ (toRational $ (ceiling::Double -> Int) $ fromRational $
1063 (count / toRational numJudges) * 100 * 1000) / 1000
1064 let bcolor = "background-color:"<>attrify color<>";"
1065 let width = "width:"<>attrify percent<>"%;"
1066 let display = if percent == 0 then "display:none;" else ""
1068 ! HA.class_ "merit-grade"
1069 ! HA.alt (attrify grade_name) -- FIXME: do not work
1070 ! HA.style (bcolor<>display<>width) $$ do
1072 ! HA.class_ "grade-name" $$ do
1074 Nothing -> html5ify grade_name
1075 Just t -> html5ify t
1077 html5Judgments :: Html5
1079 Collect.All{..} <- liftStateMarkup $ S.gets state_collect
1080 opinionsByChoiceByNodeBySectionByJudgment <-
1081 forM (HM.toList all_judgments) $ \(judgment@Judgment{judges,grades}, choicesBySection) -> do
1082 -- WARNING: only the fields of 'Judgment' used in its 'Hashable' instance
1083 -- can safely be used here: 'judges' and 'grades' are ok
1084 let judgmentGrades =
1085 maybe (error $ show grades) MJ.grades $ -- unknown grades
1086 HM.lookup grades all_grades
1087 let judgmentJudges =
1088 fromMaybe (error $ show judges) $ -- unknown judges
1089 HM.lookup judges all_judges
1090 let defaultGradeByJudge =
1093 [ g | g <- Set.toList judgmentGrades
1094 , isDefault $ MJ.unRank g
1097 [ (name, defaultGrade`fromMaybe`judgeDefaultGrade)
1098 | DTC.Judge{name,defaultGrades} <- judgmentJudges
1099 , let judgeDefaultGrade = do
1100 jdg <- listToMaybe [g | (n,g) <- defaultGrades, n == grades]
1102 [ g | g <- Set.toList judgmentGrades
1103 , let DTC.Grade{name=n} = MJ.unRank g
1107 opinionsByChoiceByNodeBySection <-
1108 forM choicesBySection $ \choicesTree -> do
1109 judgmentTree <- forM choicesTree $ \(section_importance, choices) -> do
1110 judgmentOpinions <- forM choices $ \choice_@DTC.Choice{opinions} -> do
1111 gradeByJudge <- forM opinions $ \DTC.Opinion{judge,grade,importance} -> do
1113 [ g | g <- Set.toList judgmentGrades
1114 , let Grade{name} = MJ.unRank g
1117 Just grd -> return (judge, MJ.Section importance (Just grd))
1118 Nothing -> error $ show grade -- unknown grade
1119 return (choice_, HM.fromList gradeByJudge)
1120 return $ MJ.SectionNode section_importance $ HM.fromList judgmentOpinions
1121 let judgmentChoices = HS.fromList $ snd $ Tree.rootLabel choicesTree
1122 -- NOTE: choices are determined by those at the root Tree.Node.
1123 -- NOTE: core Majority Judgment calculus handled here by MJ
1124 case MJ.opinionsBySection judgmentChoices defaultGradeByJudge judgmentTree of
1125 Right opinionsByChoiceByNode -> return opinionsByChoiceByNode
1126 Left err -> error $ show err -- unknown choice, unknown judge, invalid shares
1127 -- NOTE: 'toList' returns a self-then-descending-then-following traversal of a 'Tree',
1128 -- this will match perfectly withw the 'html5ify' traversal:
1129 -- 'BodySection' by 'BodySection'.
1130 return (judgment, join $ toList <$> opinionsByChoiceByNodeBySection)
1131 liftStateMarkup $ S.modify' $ \st ->
1132 st{state_opinions = HM.fromList opinionsByChoiceByNodeBySectionByJudgment}
1135 instance Attrify Plain.Plain where
1136 attrify p = attrify t
1137 where (t,_) = Plain.runPlain p def
1141 ( Plain.L10n msg lang
1142 , Plain.L10n TL.Text lang
1143 ) => L10n msg lang where
1144 l10n_Header_Address :: FullLocale lang -> msg
1145 l10n_Header_Date :: FullLocale lang -> msg
1146 l10n_Header_Version :: FullLocale lang -> msg
1147 l10n_Header_Origin :: FullLocale lang -> msg
1148 l10n_Header_Source :: FullLocale lang -> msg
1149 instance L10n Html5 EN where
1150 l10n_Header_Address _loc = "Address"
1151 l10n_Header_Date _loc = "Date"
1152 l10n_Header_Origin _loc = "Origin"
1153 l10n_Header_Source _loc = "Source"
1154 l10n_Header_Version _loc = "Version"
1155 instance L10n Html5 FR where
1156 l10n_Header_Address _loc = "Adresse"
1157 l10n_Header_Date _loc = "Date"
1158 l10n_Header_Origin _loc = "Origine"
1159 l10n_Header_Source _loc = "Source"
1160 l10n_Header_Version _loc = "Version"
1162 instance Plain.L10n Html5 EN where
1163 l10n_Colon loc = html5ify (Plain.l10n_Colon loc :: TL.Text)
1164 l10n_Table_of_Contents loc = html5ify (Plain.l10n_Table_of_Contents loc :: TL.Text)
1165 l10n_Date date loc = html5ify (Plain.l10n_Date date loc :: TL.Text)
1166 l10n_Quote msg _loc = do
1167 depth <- liftStateMarkup $ S.gets $ Plain.state_quote . state_plainify
1168 let (o,c) :: (Html5, Html5) =
1169 case unNat depth `mod` 3 of
1174 setDepth $ succNat depth
1180 liftStateMarkup $ S.modify' $ \s ->
1181 s{state_plainify=(state_plainify s){Plain.state_quote=d}}
1182 instance Plain.L10n Html5 FR where
1183 l10n_Colon loc = html5ify (Plain.l10n_Colon loc :: TL.Text)
1184 l10n_Table_of_Contents loc = html5ify (Plain.l10n_Table_of_Contents loc :: TL.Text)
1185 l10n_Date date loc = html5ify (Plain.l10n_Date date loc :: TL.Text)
1186 l10n_Quote msg _loc = do
1187 depth <- liftStateMarkup $ S.gets $ Plain.state_quote . state_plainify
1188 let (o,c) :: (Html5, Html5) =
1189 case unNat depth `mod` 3 of
1194 setDepth $ succNat depth
1200 liftStateMarkup $ S.modify' $ \s ->
1201 s{state_plainify=(state_plainify s){Plain.state_quote=d}}