I want to put mathematical equations in a single page app written in elm. I would like the equations to be rendered in the app and not being embedded as prerendered images.
I tried to realize this using Katex for elm (https://package.elm-lang.org/packages/yotamDvir/elm-katex/latest/) but my approach has 3 major problems:
Here is the code that I am using right now:
index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SPA with Math formulas</title>
<style>
body {
padding: 0;
margin: 0;
background-color: #000000;
color: #ffffff;
}
</style>
<script src="main.js"></script>
<style>
/* LaTeX display environment will effect the LaTeX characters but not the layout on the page */
span.katex-display {
display: inherit;
/* You may comment this out if you want the default behavior */
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
</head>
<body>
<div id="elm-main"> </div>
<script>
// Initialize your Elm program
var app = Elm.Main.init({
flags: location.href,
node: document.getElementById('elm-main')
});
// Inform app of browser navigation (the BACK and FORWARD buttons)
window.addEventListener('popstate', function () {
app.ports.onUrlChange.send(location.href);
});
// Change the URL upon request, inform app of the change.
app.ports.pushUrl.subscribe(function (url) {
history.pushState({}, '', url);
app.ports.onUrlChange.send(location.href);
});
// Render math texts in app
app.ports.renderMath.subscribe(function () {
renderMathInElement(document.body, {
delimiters: [
{
left: "$begin-inline$",
right: "$end-inline$",
display: false
},
{
left: "$begin-display$",
right: "$end-display$",
display: true
}]
});
});
</script>
<noscript>
This site needs javascript enabled in order to work.
</noscript>
</body>
</html>
src/Main.elm
port module Main exposing (..)
import Browser exposing (Document, application)
import Element exposing (Attribute, Element)
import Element.Font
import Html
import Html.Attributes
import Html.Events
import Json.Decode as D
import Katex
import Url
import Url.Parser
main =
Browser.document
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type Route
= Home
| Other
init : String -> ( Route, Cmd Msg )
init url =
( locationHrefToRoute url, Cmd.none )
type Msg
= PushUrl Route
| UrlChanged String
update : Msg -> Route -> ( Route, Cmd Msg )
update msg route =
case msg of
PushUrl newRoute ->
( route, pushUrl (stringFromRoute newRoute) )
UrlChanged url ->
( locationHrefToRoute url, renderMath () )
stringFromRoute : Route -> String
stringFromRoute route =
case route of
Home ->
"/"
Other ->
"/other"
locationHrefToRoute : String -> Route
locationHrefToRoute locationHref =
case Url.fromString locationHref of
Nothing ->
Home
Just url ->
Maybe.withDefault Home (Url.Parser.parse routeParser url)
routeParser : Url.Parser.Parser (Route -> a) a
routeParser =
Url.Parser.oneOf
[ Url.Parser.map Home Url.Parser.top
, Url.Parser.map Other (Url.Parser.s "other")
]
view : Route -> Document Msg
view route =
{ title = "SPA and Katex"
, body =
[ Element.layout
[ Element.Font.color (Element.rgb 1 1 1)
]
(Element.el
[ Element.centerX, Element.centerY ]
(viewPage route)
)
]
}
viewPage : Route -> Element Msg
viewPage route =
case route of
Home ->
Element.column [ Element.spacing 20 ]
[ link Other "Link to Other"
, Element.text "some text"
, "\\mathrm{home} = 6.2 \\times 10^{-34}"
|> Katex.inline
|> Katex.print
|> Element.text
]
Other ->
Element.column [ Element.spacing 20 ]
[ link Home "Link to Home"
, "\\mathrm{other} = 1.3 \\times 10^{-6}"
|> Katex.inline
|> Katex.print
|> Element.text
, Element.text "other text"
]
linkBehaviour : Route -> Attribute Msg
linkBehaviour route =
Element.htmlAttribute
(Html.Events.preventDefaultOn "click"
(D.succeed
( PushUrl route, True )
)
)
link : Route -> String -> Element Msg
link route labelText =
Element.link
[ linkBehaviour route
, Element.Font.color (Element.rgb255 119 35 177)
, Element.Font.underline
]
{ url = stringFromRoute route
, label = Element.text labelText
}
subscriptions : Route -> Sub Msg
subscriptions route =
Sub.batch
[ onUrlChange UrlChanged
]
port onUrlChange : (String -> msg) -> Sub msg
port pushUrl : String -> Cmd msg
port renderMath : () -> Cmd msg
I start my app with elm-live src/Main.elm -u --open -- --output=main.js --debug
:
The elm-katex docs point out ports being unnecessary.
No ports are necessary, but the KaTeX library must be loaded in the event loop. See §Loading KaTeX at the bottom for details.
The Loading KaTeX section of the docs suggests listening for DOMContentLoaded, then calling KaTeX's auto-render extension. That said, KaTeX's auto-render extension works by editing the DOM, meaning it won't play nice with Elm.
Rendering Tex math using KaTeX is a good use-case for a Custom Element (Web Components). It would be relatively straightforward to implement if you're familiar with custom elements but there's likely something already out there that will work.
See the Custom Elements section of the Elm Guide for a good intro.
I've not taken much of a look at it, but here's an option: navsgh/katex-expression
Load katex-expression before you initialize your Elm code, then write something like this:
import Html exposing (Html, node)
import Html.Attributes exposing (attribute)
import Json.Encode as Encode
viewLatex : String -> Html msg
viewLatex expr =
node "katex-expression"
[ attribute "expression" expr
, attribute "katex-options" (Encode.encode 0 options)
]
[]
options : Encode.Value
options =
Encode.object
[ ( "displayMode", Encode.bool True )
]
Here's a quick Ellie demo: https://ellie-app.com/m9HQrnXmydLa1