summaryrefslogtreecommitdiff
path: root/provider/posts/blog/tex-support.md
diff options
context:
space:
mode:
Diffstat (limited to 'provider/posts/blog/tex-support.md')
-rw-r--r--provider/posts/blog/tex-support.md243
1 files changed, 243 insertions, 0 deletions
diff --git a/provider/posts/blog/tex-support.md b/provider/posts/blog/tex-support.md
new file mode 100644
index 0000000..7f43eb6
--- /dev/null
+++ b/provider/posts/blog/tex-support.md
@@ -0,0 +1,243 @@
1---
2title: Cursory Math-Support
3published: 2015-11-05
4tags: Blog Software
5---
6
7## Demonstration
8
9I added some cursory support for math as shown below:
10
11<div class="theorem">
12
13Formulae are rendered with $\text{\LaTeX}$ and included as [SVG](https://en.wikipedia.org/wiki/SVG).
14
15<div class="proof">
16$$e^{ix} =\text{cos}(x) + i \text{sin} (x)$$
17</div>
18<div class="lemma">
19
20Inline formulae get correctly aligned to match the baseline of the surrounding text.
21
22<div class="proof">
23$\text{tan}(\phi) = \frac{\text{sin}(\phi)}{\text{cos}(\phi)}$
24</div>
25</div>
26</div>
27
28## Implementation
29
30Theorem environments are written using [pandoc](http://pandoc.org)s support for block environments:
31
32~~~ {.markdown}
33<div class="theorem">
34
35Formulae are rendered with $\text{\LaTeX}$ and included as [SVG](https://en.wikipedia.org/wiki/SVG).
36
37<div class="proof">
38$$e^{ix} =\text{cos}(x) + i \text{sin} (x)$$
39</div>
40<div class="lemma">
41
42Inline formulae get correctly aligned to match the baseline of the surrounding text.
43
44<div class="proof">
45$\text{tan}(\phi) = \frac{\text{sin}(\phi)}{\text{cos}(\phi)}$
46</div>
47</div>
48</div>
49~~~
50
51Combined with a smattering of CSS this works nicely.
52$\text{\LaTeX}$ support is, however, lacking as I opted not to patch pandoc ([math.kleen.org](https://math.kleen.org) did).
53
54### `Math.hs`
55
56The actual compilation happens in a new module I named `Math.hs`. We´ll start there.
57For your reading pleasure I added some comments to the reproduction below.
58
59~~~ {.haskell}
60module Math
61 ( compileMath
62 ) where
63
64import System.IO (stdout, stderr, hPutStrLn, writeFile, readFile)
65import System.IO.Temp (withSystemTempDirectory)
66import System.Process (callProcess, readProcessWithExitCode)
67import System.Directory (copyFile, getCurrentDirectory, setCurrentDirectory)
68import System.FilePath (takeFileName, FilePath(..), (</>))
69import System.Exit (ExitCode(..))
70
71import Control.Monad (when)
72import Control.Exception (bracket, throwIO)
73import Data.Maybe (fromMaybe, listToMaybe)
74
75import Control.Monad.Writer.Strict (WriterT(..), execWriterT, tell)
76import Control.Monad.Trans (liftIO)
77
78import Control.DeepSeq (($!!))
79
80import Text.Regex.TDFA ((=~))
81
82-- We built a monoid instance for `ExitCode` so we can easily collect failure using a `MonadWriter`
83instance Monoid ExitCode where
84 mempty = ExitSuccess
85 (ExitFailure a) `mappend` _ = ExitFailure a
86 ExitSuccess `mappend` x@(ExitFailure _) = x
87 ExitSuccess `mappend` ExitSuccess = ExitSuccess
88
89
90compileMath :: String -> IO (String, String)
91compileMath = withSystemTempDirectory "math" . compileMath' -- Create a temporary directory, run `compileMath'`, and make sure the directory get's deleted
92
93compileMath' :: String -> FilePath -> IO (String, String)
94compileMath' input tmpDir = do
95 mapM_ (copyToTmp . ("tex" </>)) [ "preamble.tex"
96 , "preview.dtx"
97 , "preview.ins"
98 ]
99 (exitCode, out, err) <- withCurrentDirectory tmpDir $ execWriterT $ do -- Collect stdout, stderr, and exitCode of all subprocesses (stdout and stderr simply get appended to one another)
100 run "latex" [ "-interaction=batchmode"
101 , "preview.ins"
102 ] ""
103 liftIO $ writeFile (tmpDir </> "image.tex") input
104 run "latex" [ "-interaction=batchmode"
105 , "image.tex"
106 ] ""
107 run "dvisvgm" [ "--exact"
108 , "--no-fonts"
109 , tmpDir </> "image.dvi"
110 ] ""
111 when (exitCode /= ExitSuccess) $ do -- Fail with maximum noise if any of the latex passes fail -- otherwise be silent
112 hPutStrLn stdout out
113 hPutStrLn stderr err
114 throwIO exitCode
115 (\x -> return $!! (x, extractAlignment err)) =<< (readFile $ tmpDir </> "image.svg") -- Note the call to `($!!)` -- since we'll be deleting `tmpDir` we need to make sure the entire generated output resides in memory before we leave this block
116 where
117 copyToTmp fp = copyFile fp (tmpDir </> takeFileName fp)
118 run :: String -> [String] -> String -> WriterT (ExitCode, String, String) IO ()
119 run bin args stdin = tell =<< liftIO (readProcessWithExitCode bin args stdin)
120
121withCurrentDirectory :: FilePath -- ^ Directory to execute in
122 -> IO a -- ^ Action to be executed
123 -> IO a
124-- ^ This is provided in newer versions of temporary
125withCurrentDirectory dir action =
126 bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do
127 setCurrentDirectory dir
128 action
129
130extractAlignment :: String -> String
131extractAlignment = fromMaybe "0pt" . extract . (=~ "depth=([^\\s]+)") -- One of the few places where regular expressions really prove usefull
132 where
133 extract :: (String, String, String, [String]) -> Maybe String
134 extract (_, _, _, xs) = listToMaybe xs
135~~~
136
137### `Site.hs`
138
139The more trick part proved to be integration into the framework as provided by [Hakyll](http://jaspervdj.be/hakyll/).
140
141~~~ {.haskell}
142
143
144import qualified Crypto.Hash.SHA256 as SHA256 (hash)
145import qualified Data.ByteString.Char8 as CBS
146import Data.Hex (hex)
147import Data.Char (toLower)
148
149import Math (compileMath)
150import Text.Printf (printf)
151
152main :: IO ()
153main = hakyllWith config $ do
154
155
156 math <- getMath "posts/*" mathTranslation'
157 forM_ math $ \(_, mathStr) -> create [mathTranslation' mathStr] $ do
158 route idRoute
159 compile $ do
160 item <- makeItem mathStr
161 >>= loadAndApplyTemplate "templates/math.tex" defaultContext
162 >>= withItemBody (unsafeCompiler . compileMath) -- unsafeCompiler :: IO a -> Compiler a
163 saveSnapshot "alignment" $ fmap snd item
164 return $ fmap fst item
165
166 match "posts/*" $ do
167 route $ setExtension ".html"
168 compile $ do
169 getResourceBody >>= saveSnapshot "content"
170 pandocCompilerWithTransformM defaultHakyllReaderOptions defaultHakyllWriterOptions mathTransform -- pandocCompilerWithTransformM :: ReaderOptions -> WriterOptions -> (Pandoc -> Compiler Pandoc) -> Item String
171 >>= loadAndApplyTemplate "templates/default.html" defaultContext
172 >>= relativizeUrls
173
174
175
176
177mathTranslation' :: String -> Identifier
178-- ^ This generates the filename for a svg file given the TeX-source
179mathTranslation' = fromCapture "math/*.svg" . map toLower . CBS.unpack . hex . SHA256.hash . CBS.pack
180
181getMath :: Pattern -> (String -> Identifier) -> Rules [([Identifier], String)]
182-- ^ We scrape all posts for math, calls `readPandoc'`
183getMath pattern makeId = do
184 ids <- getMatches pattern
185 mathStrs <- concat `liftM` mapM (\id -> map ((,) [id]) `liftM` getMath' (toFilePath' id)) ids
186 return $ mergeGroups $ groupBy ((==) `on` snd) $ mathStrs
187 where
188 getMath' :: FilePath -> Rules [String]
189 getMath' path = preprocess (query extractMath `liftM` readPandoc' path)
190 extractMath :: Inline -> [String]
191 extractMath (Math _ str) = [str]
192 extractMath _ = []
193 mergeGroups :: [[([Identifier], String)]] -> [([Identifier], String)]
194 mergeGroups = map mergeGroups' . filter (not . null)
195 mergeGroups' :: [([Identifier], String)] -> ([Identifier], String)
196 mergeGroups' xs@((_, str):_) = (concatMap fst xs, str)
197
198readPandoc' :: FilePath -> IO Pandoc
199-- ^ This is copied, almost verbatim, from Hakyll source -- Does what it says on the tin
200readPandoc' path = readFile path >>= either fail return . result'
201 where
202 result' str = case result str of
203 Left (ParseFailure err) -> Left $
204 "parse failed: " ++ err
205 Left (ParsecError _ err) -> Left $
206 "parse failed: " ++ show err
207 Right item' -> Right item'
208 result str = reader defaultHakyllReaderOptions (fileType path) str
209 reader ro t = case t of
210 DocBook -> readDocBook ro
211 Html -> readHtml ro
212 LaTeX -> readLaTeX ro
213 LiterateHaskell t' -> reader (addExt ro Ext_literate_haskell) t'
214 Markdown -> readMarkdown ro
215 MediaWiki -> readMediaWiki ro
216 OrgMode -> readOrg ro
217 Rst -> readRST ro
218 Textile -> readTextile ro
219 _ -> error $
220 "I don't know how to read a file of " ++
221 "the type " ++ show t ++ " for: " ++ path
222
223 addExt ro e = ro {readerExtensions = Set.insert e $ readerExtensions ro}
224
225mathTransform :: Pandoc -> Compiler Pandoc
226-- ^ We replace math by raw html includes of the respective svg files here
227mathTransform = walkM mathTransform'
228 where
229 mathTransform' :: Inline -> Compiler Inline
230 mathTransform' (Math mathType tex) = do
231 alignment <- loadSnapshotBody texId "alignment"
232 let
233 html = printf "<object data=\"/%s\" type=\"image/svg+xml\" style=\"vertical-align:-%s\">%s</object>"
234 (toFilePath texId) (alignment :: String) tex
235 return $ Span ("", [classOf mathType], []) [RawInline (Format "html") html]
236 where
237 texId = mathTranslation' tex
238 classOf DisplayMath = "display-math"
239 classOf InlineMath = "inline-math"
240 mathTransform' x = return x
241
242
243~~~