generated from srid/haskell-template
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathRender.hs
291 lines (276 loc) · 11.4 KB
/
Render.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
{-# LANGUAGE RecordWildCards #-}
module Heist.Extra.Splices.Pandoc.Render (
renderPandocWith,
rpBlock,
rpInline,
rpBlock',
rpInline',
) where
import Data.Map.Strict qualified as Map
import Data.Map.Syntax ((##))
import Data.Text qualified as T
import Heist qualified as H
import Heist.Extra (runCustomNode)
import Heist.Extra.Splices.Pandoc.Attr (concatAttr, rpAttr)
import Heist.Extra.Splices.Pandoc.Ctx (
RenderCtx (..),
rewriteClass,
)
import Heist.Extra.Splices.Pandoc.TaskList qualified as TaskList
import Heist.Interpreted qualified as HI
import Text.Pandoc.Builder qualified as B
import Text.Pandoc.Definition (Pandoc (..))
import Text.Pandoc.Walk as W
import Text.XmlHtml qualified as X
renderPandocWith :: RenderCtx -> Pandoc -> HI.Splice Identity
renderPandocWith ctx (Pandoc _meta blocks) =
foldMapM (rpBlock ctx) blocks
rpBlock :: RenderCtx -> B.Block -> HI.Splice Identity
rpBlock ctx@RenderCtx {..} b = do
fromMaybe (rpBlock' ctx b) $ blockSplice b
-- | Render using user override in pandoc.tpl, falling back to default HTML.
withTplTag :: RenderCtx -> Text -> H.Splices (HI.Splice Identity) -> HI.Splice Identity -> HI.Splice Identity
withTplTag RenderCtx {..} name splices default_ =
case X.childElementTag name =<< rootNode of
Nothing -> default_
Just node -> runCustomNode node splices
rpBlock' :: RenderCtx -> B.Block -> HI.Splice Identity
rpBlock' ctx@RenderCtx {..} b = case b of
B.Plain is ->
rpInlineWithTasks ctx is
B.Para is -> do
let innerSplice = rpInlineWithTasks ctx is
withTplTag ctx "Para" ("inlines" ## innerSplice) $
one . X.Element "p" mempty <$> innerSplice
B.LineBlock iss ->
flip foldMapM iss $ \is ->
foldMapM (rpInline ctx) is >> pure [X.TextNode "\n"]
B.CodeBlock (id', mkLangClass -> classes, attrs) s -> do
pure $
one . X.Element "div" (rpAttr $ bAttr b) $
one . X.Element "pre" mempty $
one . X.Element "code" (rpAttr (id', classes, attrs)) $
one $ X.TextNode s
B.RawBlock (B.Format fmt) s -> do
pure $ case fmt of
"html" ->
rawNode "div" s
"video" ->
-- HACK format. TODO: replace with ![[foo.mp4]]
one . X.Element "video" [("autoplay", ""), ("loop", ""), ("muted", "")] $
one . X.Element "source" [("src", T.strip s)] $
one . X.Element "p" mempty $
[ X.TextNode "Your browser doesn't support HTML5 video. Here is a "
, X.Element "a" [("href", T.strip s)] $
one . X.TextNode $ "link to the video"
, X.TextNode " instead."
]
_ ->
one . X.Element "pre" [("class", "pandoc-raw-" <> show fmt)] $ one . X.TextNode $ s
B.BlockQuote bs ->
withTplTag ctx "BlockQuote" ("blocks" ## rpBlock ctx `foldMapM` bs) $
one . X.Element "blockquote" mempty <$> foldMapM (rpBlock ctx) bs
B.OrderedList _ bss ->
withTplTag ctx "OrderedList" (pandocListSplices "OrderedList" bss) $ do
fmap (one . X.Element "ol" (rpAttr $ bAttr b)) $
flip foldMapM bss $
fmap (one . X.Element "li" mempty) . foldMapM (rpBlock ctx)
B.BulletList bss ->
withTplTag ctx "BulletList" (pandocListSplices "BulletList" bss) $ do
fmap (one . X.Element "ul" (rpAttr $ bAttr b)) $
flip foldMapM bss $
fmap (one . X.Element "li" mempty) . foldMapM (rpBlock ctx)
B.DefinitionList defs ->
withTplTag ctx "DefinitionList" (definitionListSplices defs) $
fmap (one . X.Element "dl" mempty) $
flip foldMapM defs $ \(term, descList) -> do
a <- foldMapM (rpInline ctx) term
as <-
flip foldMapM descList $
fmap (one . X.Element "dd" mempty) . foldMapM (rpBlock ctx)
pure $ a <> as
B.Header level attr is ->
one . X.Element (headerTag level) (rpAttr $ concatAttr attr $ bAttr b)
<$> foldMapM (rpInline ctx) is
B.HorizontalRule ->
withTplTag ctx "HorizontalRule" mempty (pure $ one $ X.Element "hr" mempty mempty)
B.Table attr _captions _colSpec (B.TableHead _ hrows) tbodys _tfoot -> do
-- TODO: Move tailwind styles to pandoc.tpl
let borderStyle = "border-gray-300"
rowStyle = [("class", "border-b-2 border-t-2 " <> borderStyle)]
cellStyle = [("class", "py-2 px-2 align-top border-r-2 border-l-2 " <> borderStyle)]
tableAttr = ("", ["mb-3"], mempty)
-- TODO: Apply captions, colSpec, etc.
fmap (one . X.Element "table" (rpAttr $ concatAttr attr tableAttr)) $ do
thead <- fmap (one . X.Element "thead" mempty) $
flip foldMapM hrows $ \(B.Row _ cells) ->
fmap (one . X.Element "tr" rowStyle) $
flip foldMapM cells $ \(B.Cell _ _ _ _ blks) ->
one . X.Element "th" cellStyle <$> foldMapM (rpBlock ctx) blks
tbody <- fmap (one . X.Element "tbody" mempty) $
flip foldMapM tbodys $ \(B.TableBody _ _ _ rows) ->
flip foldMapM rows $ \(B.Row _ cells) ->
fmap (one . X.Element "tr" rowStyle) $
flip foldMapM cells $ \(B.Cell _ _ _ _ blks) ->
one . X.Element "td" cellStyle <$> foldMapM (rpBlock ctx) blks
pure $ thead <> tbody
B.Div attr bs ->
one . X.Element (getTag "div" attr) (rpAttr $ rewriteClass ctx attr)
<$> foldMapM (rpBlock ctx) bs
B.Null ->
pure []
where
getTag defaultTag (_, _, Map.fromList -> attrs) =
Map.lookup "tag" attrs & fromMaybe defaultTag
mkLangClass classes' =
-- Tag code block with "foo language-foo" classes, if the user specified
-- "foo" as the language identifier. This enables external syntax
-- highlighters to detect the language.
--
-- If no language is specified, use "language-none" as the language This
-- works at least on prism.js,[1] in that - syntax highlighting is turned
-- off all the while background styling is applied, to be consistent with
-- code blocks with language set.
--
-- [1] https://github.com/PrismJS/prism/pull/2738
fromMaybe ["language-none"] $ do
classes <- nonEmpty classes'
let lang = head classes
pure $ lang : ("language-" <> lang) : tail classes
definitionListSplices :: [([B.Inline], [[B.Block]])] -> H.Splices (HI.Splice Identity)
definitionListSplices defs = do
"DefinitionList:Items" ## (HI.runChildrenWith . uncurry itemsSplices) `foldMapM` defs
where
itemsSplices :: [B.Inline] -> [[B.Block]] -> H.Splices (HI.Splice Identity)
itemsSplices term descriptions = do
"DefinitionList:Item:Term" ## foldMapM (rpInline ctx) term
"DefinitionList:Item:DescList" ## (HI.runChildrenWith . descListSplices) `foldMapM` descriptions
descListSplices :: [B.Block] -> H.Splices (HI.Splice Identity)
descListSplices bs = "DefinitionList:Item:Desc" ## rpBlock ctx `foldMapM` bs
pandocListSplices :: Text -> [[B.Block]] -> H.Splices (HI.Splice Identity)
pandocListSplices tagPrefix bss =
(tagPrefix <> ":Items") ## (HI.runChildrenWith . itemsSplices) `foldMapM` bss
where
itemsSplices :: [B.Block] -> H.Splices (HI.Splice Identity)
itemsSplices bs = do
(tagPrefix <> ":Item") ## foldMapM (rpBlock ctx) bs
headerTag :: HasCallStack => Int -> Text
headerTag n =
if n >= 1 && n <= 6
then "h" <> show n
else error "Invalid pandoc header level"
rpInline :: RenderCtx -> B.Inline -> HI.Splice Identity
rpInline ctx@RenderCtx {..} i = do
fromMaybe (rpInline' ctx i) $ inlineSplice i
rpInline' :: RenderCtx -> B.Inline -> HI.Splice Identity
rpInline' ctx@RenderCtx {..} i = case i of
B.Str s ->
pure $ one . X.TextNode $ s
B.Emph is ->
one . X.Element "em" mempty <$> foldMapM (rpInline ctx) is
B.Strong is ->
one . X.Element "strong" mempty <$> foldMapM (rpInline ctx) is
B.Underline is ->
one . X.Element "u" mempty <$> foldMapM (rpInline ctx) is
B.Strikeout is ->
one . X.Element "s" mempty <$> foldMapM (rpInline ctx) is
B.Superscript is ->
one . X.Element "sup" mempty <$> foldMapM (rpInline ctx) is
B.Subscript is ->
one . X.Element "sub" mempty <$> foldMapM (rpInline ctx) is
B.Quoted qt is ->
flip inQuotes qt $ foldMapM (rpInline ctx) is
B.Code attr s ->
pure $
one . X.Element "code" (rpAttr $ concatAttr attr $ iAttr i) $
one . X.TextNode $ s
B.Space -> pure $ one . X.TextNode $ " "
B.SoftBreak -> pure $ one . X.TextNode $ " "
B.LineBreak ->
pure $ one $ X.Element "br" mempty mempty
B.RawInline (B.Format fmt) s ->
if fmt == "html"
then pure $ rawNode "span" s
else
pure $
one . X.Element "pre" [("class", "pandoc-raw-" <> show fmt)] $
one . X.TextNode $ s
B.Math mathType s ->
case mathType of
B.InlineMath ->
pure $
one . X.Element "span" [("class", "math inline")] $
one . X.TextNode $ "\\(" <> s <> "\\)"
B.DisplayMath ->
pure $
one . X.Element "span" [("class", "math display")] $
one . X.TextNode $ "$$" <> s <> "$$"
B.Link attr is (url, tit) -> do
let attrs =
catMaybes [Just ("href", url), guard (not $ T.null tit) >> pure ("title", tit)]
<> rpAttr (concatAttr attr $ iAttr i)
one . X.Element "a" attrs <$> foldMapM (rpInline ctx) is
B.Image attr is (url, tit) -> do
let attrs =
catMaybes
[ pure ("src", url)
, guard (not $ T.null tit) >> pure ("title", tit)
, pure ("alt", plainify is)
]
<> rpAttr (rewriteClass ctx attr)
pure $ one . X.Element "img" attrs $ mempty
B.Note _bs -> do
-- Footnotes are to be handled separately; see Footenotes.hs
pure $ one $ X.Element "sup" mempty $ one $ X.TextNode "*"
B.Span attr is -> do
one . X.Element "span" (rpAttr $ rewriteClass ctx attr) <$> foldMapM (rpInline ctx) is
B.SmallCaps is ->
foldMapM (rpInline ctx) is
B.Cite _citations is ->
-- TODO: What to do with _citations here?
withTplTag ctx "Cite" ("inlines" ## rpInline ctx `foldMapM` is) $
one . X.Element "cite" mempty <$> foldMapM (rpInline ctx) is
where
inQuotes :: HI.Splice Identity -> B.QuoteType -> HI.Splice Identity
inQuotes w = \case
B.SingleQuote ->
w <&> \nodes ->
[X.TextNode "‘"] <> nodes <> [X.TextNode "’"]
B.DoubleQuote ->
w <&> \nodes ->
[X.TextNode "“"] <> nodes <> [X.TextNode "”"]
-- | Like rpInline', but supports task checkbox in the given inlines.
rpInlineWithTasks :: RenderCtx -> [B.Inline] -> HI.Splice Identity
rpInlineWithTasks ctx is =
rpTask ctx is $
rpInline ctx `foldMapM` is
rpTask :: RenderCtx -> [B.Inline] -> HI.Splice Identity -> HI.Splice Identity
rpTask ctx is default_ =
maybe default_ render (TaskList.parseTaskFromInlines is)
where
render (checked, taskInlines) = do
let tag = bool "Task:Unchecked" "Task:Checked" checked
withTplTag
ctx
tag
("inlines" ## rpInline ctx `foldMapM` taskInlines)
default_
rawNode :: Text -> Text -> [X.Node]
rawNode wrapperTag s =
one . X.Element wrapperTag (one ("xmlhtmlRaw", "")) $
one . X.TextNode $ s
-- | Convert Pandoc AST inlines to raw text.
plainify :: [B.Inline] -> Text
plainify = W.query $ \case
B.Str x -> x
B.Code _attr x -> x
B.Space -> " "
B.SoftBreak -> " "
B.LineBreak -> " "
-- TODO: if fmt is html, we should strip the html tags
B.RawInline _fmt s -> s
-- Ignore "wrapper" inlines like span.
B.Span _ _ -> ""
-- TODO: How to wrap math stuff here?
B.Math _mathTyp s -> s
_ -> ""