Background
I’ve written about bringing user-defined KaTeX macros into Hugo a few years ago. Looking back, I realized that I only knew how to copy code at that time.
Goal
- To replace the current code for my math editor with Showdown-KaTeX.
- To bring mhchem into my LaTeX + Markdown sandbox.
Motivation
To get the benefits of the three free (as in “freedom”) technologies.
- Markdown syntax is (much) simple(r than its LaTeX equivalent, especially for tables, ordered/unordered lists, etc).
- LaTeX syntax for math is, in the long run, worth learning, so that your fingers can stay on the keyboard while editing math expressions.
- mhchem allows writing chemical equations conveniently as in the previous point.
However, Rattle has pointed out the
difficulties of mixing LaTeX and Markdown syntax. In his proposed
solution for WordPress, he first renders KaTeX before moving to Markdown. He
has provided an example use case with a dollar sign $
in a normal Markdown
content (e.g. An apple costs $1.5.)
Another difficulty comes from Showdown’s treatment of KaTeX’s output (already
in HTML). For example, $<$
becomes
<span class="katex">
<span class="katex-mathml">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<semantics>
<mrow><mo><</mo></mrow>
<annotation encoding="application/x-tex"><</annotation>
</semantics>
</math>
</span>
<span class="katex-html" aria-hidden="true">
<span class="base">
<span class="strut" style="height:0.5782em;vertical-align:-0.0391em;"></span>
<span class="mrel"><</span>
</span>
</span>
</span>
The above output is generated by katex.renderToString("<")
. Three years ago,
I worried that the ampersand &
would get further expanded to &
by the
Markdown parser. With a better knowledge about its makeHtml()
function, it
turns out that I was wrong.
converter.makeHtml("&") // returns '<p>&</p>'
converter.makeHtml("<") // returns '<p><</p>'
Standard info
The use of each free JS library/extension is well-documented except Showdown-KaTeX. Adding these script tags in the following order works.
- KaTeX CSS
- KaTeX JS
- KaTeX mhchem extension
- KaTeX auto render extension
- Showdown
- custom Showdown
unquote
extension
Having fear for the possible crash between KaTeX and Showdown, I tried Showdown-KaTeX. On its demo page, there’re only three external libraries.
- KaTeX CSS
- Bootmark (another library by Showdown-KaTeX’s author)
- Showdown-KaTeX
After bumping KaTeX’s version number in both the linked KaTeX’s CSS script
and package.json
, I’ve managed to get newer KaTeX’s core syntax work on
this Showdown extension’s sandbox, such as CD
environment,
which KaTeX has implemented since v0.13.0, which was published one
year after the last commit of this Showdown extension.
Lacking patience to read their source code (which consists of over 10,000 lines of code), I tried the followiing without success.
- KaTeX CSS
- KaTeX JS
- KaTeX mhchem extension
- KaTeX auto render extension
- Showdown
- Showdown-KaTeX
Analysis
Showdown-KaTeX has not been updated for two years. The author of the PR for Showdown update created his own fork on GitHub and on the npm registry a couple of days ago after receiving no response from the package owner. Unfortunately, it seemed that he’s not interested into writing math due to the unchanged KaTeX version number in his commit—that’s normal because upgrading KaTeX dependency might break old things.
After the trial described in the last section, I took a quick look into the
source code of src/showdown-katex.js
, whose second line was easy
enough the understand.
import katex from 'katex';
import renderMathInElement from 'katex/dist/contrib/auto-render';
import showdown from 'showdown';
import asciimathToTex from './asciimath-to-tex';
I was expecting a simple line like import someFunction from 'katex/dist/contrib/mhchem'
. That motivated me to look into
KaTeX mhchem extension’s source code.
In KaTeX’s page for mhchem extension, it’s said that the
extension modifies the katex
object. This can be verified by mhchem’s source
code, which defines three LaTeX macros
\ce
: chemical expression\pu
: physical unit- another macro for triple bond?
The former two commands call chemParse(tokens, stateMachine)
function.
token
and stateMachine
are the code inside {}
and the macro (either "ce"
or "pu"
) respectively. The comments above those KaTeX macros says that
chemParse(...)
is the main function that deals with these two macros.
However importing simply that function doesn’t make sense, as we want to access
those two KaTeX macros.
N.B. The \pu{}
command isn’t implemented in LaTeX’s mhchem package due to the
existing command \unit{}
in siunitx package.
I tried reading the difference between import
and require
. The only thing
that I can understand is the position where these two keywords can be put.
input
has to be put at the top, and it can’t be loaded conditionally (e.g. put
inside an if..else
statement.). You can require
a Node.JS package inside
there.
I spent a while to search for some sample code with the keywords ‘“katex” AND “mhchem” AND “import”’.
Luckily, in KaTeX/KaTeX#1995, someone has given the anwser: import 'katex/dist/contrib/mhchem.js'
. I added that line above the second line in
src/showdown-katex.js
, and I managed to enable \ce{}
and \pu{}
in the
sandbox with the default delimiters for LaTeX code block. The ones for inline
KaTeX expressions didn’t work.
```latex
\ce{CH4 + 2O2 -> CO2 + 2H2O}
```
After three days of work, I really wanted to take a break. Lacking knowledge in Node.JS, I felt that my way of working wasn’t efficient. I would return to a web developement course after this little personal project.
In fact, the nps build
command built some source files into a distribution
folder dist/
containing both the minified and original versions of script with
KaTeX (core, mhchem and auto render extensions) loaded above. The minified
script is aboout 600 KB.
Note: Despite the line import showdown from 'showdown'
, unlike the other
packages, Showdown has to be loaded.
Distribution of my own fork on the NPM registry
In order to upload my own customized version of the Showdown extension somewhere
in a public CDN, so that I can include that like other external <script>
s, I
have to upload it to the NPM registry, and use jsDelivr’s auto-backup
service. The company’s website home page shows the rules of correspondance
between packages hosted on the NPM registry and the auto-backup’s links. Since
that’s the first time that I published a package into the NPM registry, it took
me a hour to figure out how to do so. The
official guide for created scoped public packages should be enough.
- Switch to WSL if you’re on M$ Win*.
- Sign up online for an account on the NPM registry (for free). It’s free unless you want private packages.
- Since the local repo is
git clone
d from my fork on GitHub, the first few steps can be skipped. Thenpmrc
command isn’t needed since I’m not using multiple accounts there. - The command
npm init --scope=@my-username
will start an interactive session asking for some basic information about the author. Since the--scope
has be supplied, inside the interactive session, it defaults to@my-username
. Note thatmy-username
has to match your user name exactly, and the match is cAsE sensitive. For the other fields, just accepts the default. I’ve replaced the fields for the author with my details and added “mhchem” to the tag and package description so that other users might notice the difference. - I used
npm publish
as I wasn’t sure whatnpm deploy
did. - After my scoped public package was uploaded, it’s possible to access jsDelivr’s backup, but I had to wait for a few hours for the generation of the corresponding landing page, on which the link and/or SRI (Subresource Integrity) (or the complete HTML tag) can be copied and pasted with a few mouse clicks.
Difficulties and skills learnt
From my past experience with Staticman, a Node.JS project can usually be
- built with
npm build
- run with
npm run
ornode index.js
Knowing little about Node.JS and nothing on Yarn, it took me one whole night
to find out that the "scripts"
in package.json
is using Yarn. There’s no
need to install nps
at a global level. In fact, due to the presence of
yarn-lock.json
, it’s possible to use yarn
to install each script
defined
in package.json
.
Thanks to M$’s tutorial on Node.JS, I’ve learnt using
npm outdated
to view outdated packages and the available versions, andncu -u
to updatepackage.json
without actually installing the newer packages tonode_modules/
.
I’ve read Netwoven’s article to review these commands.
During the rebuild, I saw some strange errors in Git Bash. On Unix, it’s common
to use ENV_VAR=val cmd -flag args
, but that doesn’t work on M$ Win*. I’ve
reported that in obedm503/showdown-katex#40. Not wishing to
spend time finding a suitable cross-platform syntax, I switched to WSL (Windows
Subsystem for Linux) for compilation.
VSCode ESLint has complained the following lines in package.json
.
"ava": {
"require": "@babel/register"
},
The tools has suggested the use of an array [...]
. That puzzled me as I
recalled seeing "requires"
somewhere else for package dependencies, and that
the dependent objects are inside a pair of curly braces. Here’s an
example from Stack Overflow.
"requires": {
"@angular-devkit/core": "0.8.5",
"rxjs": "6.2.2",
"tree-kill": "1.2.0",
"webpack-sources": "1.3.0"
},
Thanks to Discord, I’ve found out that the two are used in different cases.