-
Notifications
You must be signed in to change notification settings - Fork 17
/
SideNoteHTML.hs
161 lines (142 loc) · 5.87 KB
/
SideNoteHTML.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
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- |
Module : Text.Pandoc.SideNoteHTML
Description : Convert pandoc footnotes to sidenotes
Copyright : (c) Tony Zorman 2023
License : MIT
Maintainer : Tony Zorman <[email protected]>
Stability : experimental
Portability : non-portable
-}
module Text.Pandoc.SideNoteHTML (usingSideNotesHTML) where
import Control.Monad (foldM)
import Control.Monad.State (State, get, modify', runState)
import Data.Text (Text)
import Text.Pandoc (runPure, writeHtml5String)
import Text.Pandoc.Definition (Block (..), Inline (..), Pandoc (..))
import Text.Pandoc.Options (WriterOptions)
import Text.Pandoc.Shared (tshow)
import Text.Pandoc.Walk (walkM)
import qualified Data.Text as T
-- type NoteType :: Type
data NoteType = Sidenote | Marginnote
deriving stock (Show, Eq)
-- type SidenoteState :: Type
data SidenoteState = SNS
{ _writer :: !WriterOptions
, counter :: !Int
}
-- type Sidenote :: Type -> Type
type Sidenote = State SidenoteState
-- | Like 'Text.Pandoc.SideNote.usingSideNotes', but immediately
-- pre-render the sidenotes. This has the advantage that sidenotes may
-- be wrapped in a @<div>@ (instead of a 'Span'), which allows arbitrary
-- blocks to be nested in them. The disadvantage is that one now has to
-- specify the 'WriterOptions' for the current document, meaning this is
-- meant to be used as a module and is unlikely to be useful as a
-- standalone application.
--
-- ==== __Example__
--
-- Using this function with <https://jaspervdj.be/hakyll/ hakyll> could
-- look something like the following, defining an equivalent to the
-- default @pandocCompiler@.
--
-- > myPandocCompiler :: Compiler (Item String)
-- > myPandocCompiler =
-- > pandocCompilerWithTransformM
-- > defaultHakyllReaderOptions
-- > defaultHakyllWriterOptions
-- > (usingSideNotesHTML defaultHakyllWriterOptions)
--
usingSideNotesHTML :: WriterOptions -> Pandoc -> Pandoc
usingSideNotesHTML writer (Pandoc meta blocks) =
-- Drop a superfluous paragraph at the start of the document.
Pandoc meta . someStart . walkBlocks (SNS writer 0) $ blocks
where
someStart :: [Block] -> [Block]
someStart = \case
(Para [Str ""] : bs) -> bs
bs -> bs
walkBlocks :: SidenoteState -> [Block] -> [Block]
walkBlocks sns = \case
[] -> []
(b : bs) -> b' <> walkBlocks s' bs
where (b', s') = walkM mkSidenote [b] `runState` sns
-- Sidenotes can probably appear in more places; this should be
-- filled-in at some point.
mkSidenote :: [Block] -> Sidenote [Block]
mkSidenote = foldM (\acc b -> (acc <>) <$> single b) []
where
-- Try to find and render a sidenote in a single block.
single :: Block -> Sidenote [Block]
single = \case
-- Simulate a paragraph by inserting a dummy block; this is needed
-- in case two consecutive paragraphs have sidenotes, or a paragraph
-- doesn't have one at all.
Para inlines -> (Para [Str ""] :) <$> renderSidenote [] inlines
Plain inlines -> renderSidenote [] inlines
OrderedList attrs bs -> (:[]) . OrderedList attrs <$> traverse mkSidenote bs
BulletList bs -> (:[]) . BulletList <$> traverse mkSidenote bs
block -> pure [block]
renderSidenote :: [Inline] -> [Inline] -> Sidenote [Block]
renderSidenote !inlines = \case
[] -> pure [plain inlines]
Note bs : xs -> do block <- go bs
mappend [ -- Start gluing before, see [Note Comment].
plain (RawInline "html" commentStart : inlines)
, block
]
<$> renderSidenote
[RawInline "html" commentEnd] -- End gluing after
xs
b : xs -> renderSidenote (b : inlines) xs
where
go :: [Block] -> Sidenote Block
go blocks = do
SNS w i <- get <* modify' (\sns -> sns{ counter = 1 + counter sns })
let (typ, noteText) = getNoteType (render w blocks)
pure . RawBlock "html" $
mconcat [ commentEnd -- End gluing before
, label typ i <> input i <> note typ noteText
, commentStart -- Start gluing after
]
-- The '{-}' symbol differentiates between margin note and side note.
getNoteType :: Text -> (NoteType, Text)
getNoteType t
| "{-} " `T.isPrefixOf` t = (Marginnote, T.drop 4 t)
| otherwise = (Sidenote , t)
render :: WriterOptions -> [Block] -> Text
render w bs = case runPure (writeHtml5String w (Pandoc mempty bs)) of
Left err -> error $ "Text.Pandoc.SideNoteHTML.writePandocWith: " ++ show err
Right txt -> T.drop 1 (T.dropWhile (/= '\n') txt)
commentEnd :: T.Text
commentEnd = "-->"
commentStart :: T.Text
commentStart = "<!--"
plain :: [Inline] -> Block
plain = Plain . reverse
label :: NoteType -> Int -> Text
label nt i = "<label for=\"sn-" <> tshow i <> "\" class=\"margin-toggle" <> sidenoteNumber <> "\">" <> altSymbol <> "</label>"
where
sidenoteNumber :: Text = case nt of
Sidenote -> " sidenote-number"
Marginnote -> ""
altSymbol :: Text = case nt of
Sidenote -> ""
Marginnote -> "⊕"
input :: Int -> Text
input i = "<input type=\"checkbox\" id=\"sn-" <> tshow i <> "\" class=\"margin-toggle\"/>"
note :: NoteType -> Text -> Text
note nt body = "<div class=\"" <> T.toLower (tshow nt) <> "\">" <> body <> "</div>"
{- [Note Comment]
This is obviously horrible, but we have to do this in order for the
blocks (which are now not inline elements anymore!) immediately before
and after the sidenote to be "glued" to the sidenote itself. In this
way, the number indicating the sidenote does not have an extra space
associated to it on either side, which otherwise would be the case.
-}