]> Git — Sourcephile - doclang.git/blob - Language/DTC/Write/HTML5.hs
Add HTML5 rendering of ToF.
[doclang.git] / Language / DTC / Write / HTML5.hs
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
11
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
21 import Control.Monad
22 import Data.Bool
23 import Data.Char (Char)
24 import Data.Default.Class (Default(..))
25 import Data.Eq (Eq(..))
26 import Data.Foldable (Foldable(..))
27 import Data.Function (($), const, flip, on)
28 import Data.Functor (Functor(..), (<$>))
29 import Data.Functor.Compose (Compose(..))
30 import Data.Int (Int)
31 import Data.Map.Strict (Map)
32 import Data.Maybe (Maybe(..), maybe, mapMaybe, fromJust, listToMaybe)
33 import Data.Monoid (Monoid(..))
34 import Data.Ord (Ord(..))
35 import Data.Semigroup (Semigroup(..))
36 import Data.String (String)
37 import Data.Text (Text)
38 import Data.TreeSeq.Strict (Tree(..), Trees)
39 import Data.Tuple (snd)
40 import System.FilePath (FilePath)
41 import Text.Blaze ((!))
42 import Text.Blaze.Html (Html)
43 import Text.Show (Show(..))
44 import qualified Control.Monad.Trans.State as S
45 import qualified Data.List as List
46 import qualified Data.Map.Strict as Map
47 import qualified Data.Sequence as Seq
48 import qualified Data.Strict.Maybe as Strict
49 import qualified Data.Text as Text
50 import qualified Data.Text.Lazy as TL
51 import qualified Data.TreeMap.Strict as TreeMap
52 import qualified Data.TreeSeq.Strict as Tree
53 import qualified Data.TreeSeq.Strict.Zipper as Tree
54 import qualified Text.Blaze.Html5 as H
55 import qualified Text.Blaze.Html5.Attributes as HA
56 import qualified Text.Blaze.Internal as H
57
58 import Text.Blaze.Utils
59 import Data.Locale hiding (localize, Index)
60 import qualified Data.Locale as Locale
61
62 import Language.DTC.Document (Document)
63 import Language.DTC.Write.XML ()
64 import Language.XML (XmlName(..), XmlPos(..))
65 import qualified Language.DTC.Document as DTC
66 import qualified Language.DTC.Index as Index
67 -- import Debug.Trace (trace)
68
69 (<&>) :: Functor f => f a -> (a -> b) -> f b
70 (<&>) = flip (<$>)
71 infixl 4 <&>
72
73 -- * Type 'Html5'
74 type Html5 = StateMarkup State ()
75
76 -- ** Type 'State'
77 data State
78 = State
79 { state_styles :: Map FilePath CSS
80 , state_scripts :: Map FilePath Script
81 , state_localize :: MsgHtml5 -> Html5
82 , state_indexs :: Map XmlPos (DTC.Terms, Index.Refs)
83 , state_figures :: Map Text (Map XmlPos DTC.Title)
84 }
85 state :: State
86 state = State
87 { state_styles = mempty
88 , state_scripts = mempty
89 , state_localize = html5ify . show
90 , state_indexs = mempty
91 , state_figures = mempty
92 }
93 type CSS = Text
94 type Script = Text
95
96 -- ** Type 'Keys'
97 data Keys
98 = Keys
99 { keys_index :: Map XmlPos DTC.Terms
100 , keys_figure :: Map Text (Map XmlPos DTC.Title)
101 } deriving (Show)
102
103 keys :: Trees DTC.BodyKey DTC.BodyValue -> Keys
104 keys body = foldl' flt (Keys mempty mempty) (Compose body)
105 where
106 flt acc = \case
107 DTC.Index{..} -> acc{keys_index =
108 Map.insert pos terms $ keys_index acc}
109 DTC.Figure{..} -> acc{keys_figure =
110 Map.insertWith (<>)
111 type_ (Map.singleton pos title) $
112 keys_figure acc}
113 _ -> acc
114
115 -- ** Class 'Html5ify'
116 class Html5ify a where
117 html5ify :: a -> Html5
118 instance Html5ify Char where
119 html5ify = html5ify . H.toMarkup
120 instance Html5ify Text where
121 html5ify = html5ify . H.toMarkup
122 instance Html5ify String where
123 html5ify = html5ify . H.toMarkup
124 instance Html5ify H.Markup where
125 html5ify = Compose . return
126 instance Html5ify DTC.Title where
127 html5ify (DTC.Title t) = html5ify t
128 instance Html5ify DTC.Para where
129 html5ify = mapM_ html5ify
130 instance Html5ify DTC.Ident where
131 html5ify (DTC.Ident i) = html5ify i
132
133 html5Document ::
134 Localize ls Html5 MsgHtml5 =>
135 Locales ls =>
136 LocaleIn ls -> Document -> Html
137 html5Document locale DTC.Document{..} = do
138 let Keys{..} = keys body
139 let (body',state_indexs) =
140 case foldMap Index.refsOfTerms keys_index of
141 refs | null refs -> (body, mempty)
142 | otherwise ->
143 (<$> S.runState
144 (Index.indexify body)
145 Index.state
146 { Index.state_refs = refs }) $ \Index.State{state_refs} ->
147 (<$> keys_index) $ \terms ->
148 (terms,) $
149 TreeMap.intersection const state_refs $
150 Index.refsOfTerms terms
151 let (html5Body, State{state_styles,state_scripts}) =
152 runStateMarkup state{state_indexs, state_figures=keys_figure} $ do
153 liftStateMarkup $ S.modify $ \s -> s{state_localize = Locale.localize locale}
154 html5ify body'
155
156 H.docType
157 H.html ! HA.lang (attrValue $ countryCode locale) $ do
158 H.head $ do
159 H.meta ! HA.httpEquiv "Content-Type"
160 ! HA.content "text/html; charset=UTF-8"
161 whenSome (DTC.titles $ DTC.about (head :: DTC.Head)) $ \ts ->
162 H.title $
163 H.toMarkup $ plainify $ List.head ts
164 forM_ (DTC.links $ DTC.about (head :: DTC.Head)) $ \DTC.Link{rel, href} ->
165 H.link ! HA.rel (attrValue rel)
166 ! HA.href (attrValue href)
167 H.meta ! HA.name "generator"
168 ! HA.content "tct"
169 let chapters =
170 (`mapMaybe` toList body) $ \case
171 TreeN k@DTC.Section{} _ -> Just k
172 _ -> Nothing
173 forM_ chapters $ \DTC.Section{..} ->
174 H.link ! HA.rel "Chapter"
175 ! HA.title (attrValue $ plainify title)
176 ! HA.href ("#"<>attrValue pos)
177 H.link ! HA.rel "stylesheet"
178 ! HA.type_ "text/css"
179 ! HA.href "style/dtc-html5.css"
180 forM_ state_styles $ \style ->
181 H.style ! HA.type_ "text/css" $
182 H.toMarkup style
183 forM_ state_scripts $ \script ->
184 H.script ! HA.type_ "application/javascript" $
185 H.toMarkup script
186 H.body
187 html5Body
188
189 -- * Type 'BodyCursor'
190 -- | Cursor to navigate within a 'DTC.Body' according to many axis (like in XSLT).
191 type BodyCursor = Tree.Zipper DTC.BodyKey DTC.BodyValue
192 instance Html5ify DTC.Body where
193 html5ify body =
194 forM_ (Tree.zippers body) $ \z ->
195 forM_ (Tree.axis_repeat Tree.axis_following_sibling_nearest `Tree.runAxis` z) $
196 html5ify
197
198 instance Html5ify BodyCursor where
199 html5ify z =
200 case Tree.current z of
201 TreeN k _ts -> html5BodyKey z k
202 Tree0 v -> html5BodyValue z v
203
204 html5BodyKey :: BodyCursor -> DTC.BodyKey -> Html5
205 html5BodyKey z = \case
206 DTC.Section{..} ->
207 H.section ! HA.class_ "section"
208 ! HA.id (attrValue pos) $$ do
209 html5CommonAttrs attrs $
210 H.table ! HA.class_ "section-header" $$
211 H.tbody $$
212 H.tr $$ do
213 H.td ! HA.class_ "section-number" $$ do
214 html5SectionNumber $ xmlPosAncestors pos
215 H.td ! HA.class_ "section-title" $$ do
216 (case List.length $ xmlPosAncestors pos of
217 0 -> H.h1
218 1 -> H.h2
219 2 -> H.h3
220 3 -> H.h4
221 4 -> H.h5
222 5 -> H.h6
223 _ -> H.h6) $$
224 html5ify title
225 forM_ (Tree.axis_child `Tree.runAxis` z) $
226 html5ify
227 html5BodyValue :: BodyCursor -> DTC.BodyValue -> Html5
228 html5BodyValue z = \case
229 DTC.Block b -> html5ify b
230 DTC.ToC{..} -> do
231 H.nav ! HA.class_ "toc"
232 ! HA.id (attrValue pos) $$ do
233 H.span ! HA.class_ "toc-name" $$
234 H.a ! HA.href (attrValue pos) $$
235 html5ify MsgHTML5_Table_of_Contents
236 H.ul $$
237 forM_ (Tree.axis_following_sibling `Tree.runAxis` z) $
238 html5ifyToC depth
239 DTC.ToF{..} -> do
240 H.nav ! HA.class_ "tof"
241 ! HA.id (attrValue pos) $$
242 H.table ! HA.class_ "tof" $$
243 H.tbody $$
244 html5ifyToF types
245 DTC.Figure{..} ->
246 html5CommonAttrs attrs $
247 H.div ! HA.class_ ("figure " <> attrValue ("figure-"<>type_))
248 ! HA.id (attrValue pos) $$ do
249 H.table ! HA.class_ "figure-caption" $$
250 H.tbody $$
251 H.tr $$ do
252 H.td ! HA.class_ "figure-number" $$ do
253 H.a ! HA.href ("#"<>attrValue pos) $$ do
254 html5ify type_
255 html5PosAncestors $ xmlPosAncestors pos
256 html5ify $ MsgHTML5_Colon
257 " "
258 H.td ! HA.class_ "figure-name" $$
259 html5ify title
260 H.div ! HA.class_ "figure-content" $$ do
261 html5ify blocks
262 DTC.Index{pos} -> do
263 (allTerms,refsByTerm) <- liftStateMarkup $ S.gets $ (Map.! pos) . state_indexs
264 let chars = Index.termsByChar allTerms
265 H.div ! HA.class_ "index"
266 ! HA.id (attrValue pos) $$ do
267 H.nav ! HA.class_ "index-nav" $$ do
268 forM_ (Map.keys chars) $ \char ->
269 H.a ! HA.href ("#"<>(attrValue pos <> "." <> attrValue char)) $$
270 html5ify char
271 H.dl ! HA.class_ "index-chars" $$
272 forM_ (Map.toList chars) $ \(char,terms) -> do
273 H.dt $$
274 let i = attrValue pos <> "." <> attrValue char in
275 H.a ! HA.id i
276 ! HA.href ("#"<>i) $$
277 html5ify char
278 H.dd $$
279 H.dl ! HA.class_ "index-term" $$ do
280 forM_ terms $ \aliases -> do
281 H.dt $$
282 H.ul ! HA.class_ "index-aliases" $$
283 forM_ (listToMaybe aliases) $ \term ->
284 H.li ! HA.id (attrValue Index.Ref{term, count=0, section=def}) $$
285 html5ify term
286 H.dd $$ do
287 let refs =
288 List.sortBy (compare `on` Index.section) $
289 (`foldMap` aliases) $ \words -> fromJust $ do
290 path <- Index.pathFromWords words
291 Strict.maybe Nothing (Just . List.reverse) $
292 TreeMap.lookup path refsByTerm
293 sequence_ $
294 List.intersperse ", " $
295 (<$> refs) $ \ref@Index.Ref{..} ->
296 H.a ! HA.href ("#"<>attrValue ref) $$
297 html5ify $
298 List.intercalate "." $
299 List.reverse $
300 (<$> xmlPosAncestors section) $ \(_n,c) -> show c
301
302 instance Html5ify DTC.Words where
303 html5ify = html5ify . Index.plainifyWords
304
305 cleanPara :: DTC.Para -> DTC.Para
306 cleanPara p =
307 p >>= (`Tree.bindTrees` \case
308 TreeN DTC.Iref{} ls -> ls
309 TreeN DTC.Note{} _ -> mempty
310 h -> pure h)
311
312 html5ifyToC :: Maybe DTC.Nat -> BodyCursor -> Html5
313 html5ifyToC depth z =
314 case Tree.current z of
315 TreeN DTC.Section{..} _ts -> do
316 H.li $$ do
317 H.table ! HA.class_ "toc-entry" $$
318 H.tbody $$
319 H.tr $$ do
320 H.td ! HA.class_ "section-number" $$
321 html5SectionRef $ xmlPosAncestors pos
322 H.td ! HA.class_ "section-title" $$
323 html5ify $ cleanPara $ DTC.unTitle title
324 when (maybe True (> DTC.Nat 1) depth && not (null sections)) $
325 H.ul $$
326 forM_ sections $
327 html5ifyToC (depth >>= DTC.predNat)
328 _ -> pure ()
329 where
330 sections =
331 (`Tree.runAxis` z) $
332 Tree.axis_child
333 `Tree.axis_filter_current` \case
334 TreeN DTC.Section{} _ -> True
335 _ -> False
336
337 html5ifyToF :: [Text] -> Html5
338 html5ifyToF types = do
339 figsByType <- liftStateMarkup $ S.gets state_figures
340 let figs =
341 Map.foldMapWithKey (\ty -> ((ty,) <$>)) $
342 Map.intersection figsByType $
343 Map.fromList [(ty,()) | ty <- types]
344 forM_ (Map.toList figs) $ \(pos, (type_, title)) ->
345 H.tr $$ do
346 H.td ! HA.class_ "figure-number" $$
347 H.a ! HA.href ("#"<>attrValue pos) $$ do
348 html5ify type_
349 html5PosAncestors $ xmlPosAncestors pos
350 H.td ! HA.class_ "figure-name" $$
351 html5ify $ cleanPara $ DTC.unTitle title
352
353 instance Html5ify [DTC.Block] where
354 html5ify = mapM_ html5ify
355 instance Html5ify DTC.Block where
356 html5ify = \case
357 DTC.Para{..} ->
358 html5CommonAttrs attrs $
359 H.p ! HA.class_ "para"
360 ! HA.id (attrValue pos) $$ do
361 html5ify para
362 DTC.OL{..} ->
363 html5CommonAttrs attrs $
364 H.ol ! HA.class_ "ol"
365 ! HA.id (attrValue pos) $$ do
366 forM_ items $ \item ->
367 H.li $$ html5ify item
368 DTC.UL{..} ->
369 html5CommonAttrs attrs $
370 H.ul ! HA.class_ "ul"
371 ! HA.id (attrValue pos) $$ do
372 forM_ items $ \item ->
373 H.li $$ html5ify item
374 DTC.RL{..} ->
375 html5CommonAttrs attrs $
376 H.div ! HA.class_ "rl"
377 ! HA.id (attrValue pos) $$ do
378 H.table $$
379 forM_ refs html5ify
380 DTC.Comment t ->
381 html5ify $ H.Comment (H.Text t) ()
382 instance Html5ify DTC.Lines where
383 html5ify = \case
384 Tree0 v ->
385 case v of
386 DTC.BR -> html5ify H.br
387 DTC.Plain t -> html5ify t
388 TreeN k ls ->
389 case k of
390 DTC.B -> H.strong $$ html5ify ls
391 DTC.Code -> H.code $$ html5ify ls
392 DTC.Del -> H.del $$ html5ify ls
393 DTC.I -> H.i $$ html5ify ls
394 DTC.Sub -> H.sub $$ html5ify ls
395 DTC.Sup -> H.sup $$ html5ify ls
396 DTC.SC -> H.span ! HA.class_ "smallcaps" $$ html5ify ls
397 DTC.U -> H.span ! HA.class_ "underline" $$ html5ify ls
398 DTC.Note -> ""
399 DTC.Q ->
400 H.span ! HA.class_ "q" $$ do
401 html5ify MsgHTML5_QuoteOpen
402 H.i $$ html5ify ls
403 html5ify MsgHTML5_QuoteClose
404 DTC.Eref{..} ->
405 H.a ! HA.class_ "eref"
406 ! HA.href (attrValue href) $$
407 html5ify ls
408 DTC.Iref{..} ->
409 H.span ! HA.class_ "iref"
410 ! HA.id (attrValue Index.Ref{term, count, section=def}) $$
411 html5ify ls
412 DTC.Ref{..} ->
413 H.a ! HA.class_ "ref"
414 ! HA.href ("#"<>attrValue to) $$
415 if null ls
416 then html5ify to
417 else html5ify ls
418 DTC.Rref{..} ->
419 H.a ! HA.class_ "rref"
420 ! HA.href (attrValue to) $$
421 html5ify ls
422 instance AttrValue Index.Ref where
423 attrValue Index.Ref{..} =
424 "iref" <> "." <> attrValue (Index.plainifyWords term) <>
425 if count > 0
426 then "." <> attrValue count
427 else ""
428 instance Html5ify DTC.About where
429 html5ify DTC.About{..} =
430 forM_ titles $ \(DTC.Title title) ->
431 html5ify $ Seq.singleton $ TreeN DTC.Q title
432 instance Html5ify DTC.Reference where
433 html5ify DTC.Reference{id=id_, ..} =
434 H.tr $$ do
435 H.td ! HA.class_ "reference-key" $$
436 html5ify id_
437 H.td ! HA.class_ "reference-content" $$
438 html5ify about
439
440 html5CommonAttrs :: DTC.CommonAttrs -> Html5 -> Html5
441 html5CommonAttrs DTC.CommonAttrs{id=id_, ..} =
442 Compose . (addClass . addId <$>) . getCompose
443 where
444 addClass =
445 case classes of
446 [] -> id
447 _ -> H.AddCustomAttribute "class" (H.Text $ Text.unwords classes)
448 addId = maybe id (\(DTC.Ident i) ->
449 H.AddCustomAttribute "id" (H.Text i)) id_
450
451 html5SectionNumber :: [(XmlName,Int)] -> Html5
452 html5SectionNumber = go [] . List.reverse
453 where
454 go :: [(XmlName,Int)] -> [(XmlName,Int)] -> Html5
455 go _rs [] = pure ()
456 go rs (a@(_n,cnt):as) = do
457 H.a ! HA.href (attrValue $ "#"<>textXmlPosAncestors (a:rs)) $$
458 html5ify $ show cnt
459 when (not (null as) || null rs) $ do
460 html5ify '.'
461 go (a:rs) as
462
463 html5SectionRef :: [(XmlName,Int)] -> Html5
464 html5SectionRef as =
465 H.a ! HA.href (attrValue $ "#"<>textXmlPosAncestors as) $$
466 html5PosAncestors as
467
468 html5PosAncestors :: [(XmlName,Int)] -> Html5
469 html5PosAncestors as =
470 case as of
471 [(_n,c)] -> do
472 html5ify $ show c
473 html5ify '.'
474 _ ->
475 html5ify $
476 Text.intercalate "." $
477 List.reverse $
478 Text.pack . show . snd <$> as
479
480 textXmlPosAncestors :: [(XmlName,Int)] -> Text
481 textXmlPosAncestors =
482 snd . foldr (\(n,c) (nParent,acc) ->
483 (n,
484 (if Text.null acc
485 then acc
486 else acc <> ".") <>
487 Text.pack
488 (if n == nParent
489 then show c
490 else show n<>show c)
491 )
492 )
493 ("","")
494
495 -- * Class 'Plainify'
496 class Plainify a where
497 plainify :: a -> TL.Text
498 instance Plainify TL.Text where
499 plainify = id
500 instance Plainify Text where
501 plainify = TL.fromStrict
502 instance Plainify DTC.Para where
503 plainify = foldMap plainify
504 instance Plainify DTC.Lines where
505 plainify = \case
506 Tree0 v ->
507 case v of
508 DTC.BR -> "\n"
509 DTC.Plain p -> plainify p
510 TreeN k ls ->
511 case k of
512 DTC.B -> "*"<>plainify ls<>"*"
513 DTC.Code -> "`"<>plainify ls<>"`"
514 DTC.Del -> "-"<>plainify ls<>"-"
515 DTC.I -> "/"<>plainify ls<>"/"
516 DTC.Note -> ""
517 DTC.Q -> "« "<>plainify ls<>" »"
518 DTC.SC -> plainify ls
519 DTC.Sub -> plainify ls
520 DTC.Sup -> plainify ls
521 DTC.U -> "_"<>plainify ls<>"_"
522 DTC.Eref{..} -> plainify ls
523 DTC.Iref{..} -> plainify ls
524 DTC.Ref{..} -> plainify ls
525 DTC.Rref{..} -> plainify ls
526 instance Plainify DTC.Title where
527 plainify (DTC.Title t) = plainify t
528
529 instance AttrValue XmlPos where
530 attrValue = attrValue . textXmlPosAncestors . xmlPosAncestors
531
532 -- * Type 'MsgHtml5'
533 data MsgHtml5
534 = MsgHTML5_Table_of_Contents
535 | MsgHTML5_Colon
536 | MsgHTML5_QuoteOpen
537 | MsgHTML5_QuoteClose
538 deriving (Show)
539 instance Html5ify MsgHtml5 where
540 html5ify msg = do
541 loc <- liftStateMarkup $ S.gets state_localize
542 loc msg
543 instance LocalizeIn FR Html5 MsgHtml5 where
544 localizeIn _ = \case
545 MsgHTML5_Table_of_Contents -> "Sommaire"
546 MsgHTML5_Colon -> " :"
547 MsgHTML5_QuoteOpen -> "« "
548 MsgHTML5_QuoteClose -> " »"
549 instance LocalizeIn EN Html5 MsgHtml5 where
550 localizeIn _ = \case
551 MsgHTML5_Table_of_Contents -> "Summary"
552 MsgHTML5_Colon -> ":"
553 MsgHTML5_QuoteOpen -> "“"
554 MsgHTML5_QuoteClose -> "”"