Showdown KaTeX With mhchem

mhchem support for Showdown-KaTeX

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

  1. To replace the current code for my math editor with Showdown-KaTeX.
  2. To bring mhchem into my LaTeX + Markdown sandbox.

Motivation

To get the benefits of the three free (as in “freedom”) technologies.

  1. Markdown syntax is (much) simple(r than its LaTeX equivalent, especially for tables, ordered/unordered lists, etc).
  2. LaTeX syntax for math is, in the long run, worth learning, so that your fingers can stay on the keyboard while editing math expressions.
  3. 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>&lt;</mo></mrow>
        <annotation encoding="application/x-tex">&lt;</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">&lt;</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 &amp; by the Markdown parser. With a better knowledge about its makeHtml() function, it turns out that I was wrong.

converter.makeHtml("&")  // returns '<p>&amp;</p>'
converter.makeHtml("&lt;")  // returns '<p>&lt;</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.

  1. KaTeX CSS
  2. KaTeX JS
  3. KaTeX mhchem extension
  4. KaTeX auto render extension
  5. Showdown
  6. 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.

  1. KaTeX CSS
  2. Bootmark (another library by Showdown-KaTeX’s author)
  3. 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.

  1. KaTeX CSS
  2. KaTeX JS
  3. KaTeX mhchem extension
  4. KaTeX auto render extension
  5. Showdown
  6. 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

  1. \ce: chemical expression
  2. \pu: physical unit
  3. 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.

  1. Switch to WSL if you’re on M$ Win*.
  2. Sign up online for an account on the NPM registry (for free). It’s free unless you want private packages.
  3. Since the local repo is git cloned from my fork on GitHub, the first few steps can be skipped. The npmrc command isn’t needed since I’m not using multiple accounts there.
  4. 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 that my-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.
  5. I used npm publish as I wasn’t sure what npm deploy did.
  6. 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 or node 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

  1. npm outdated to view outdated packages and the available versions, and
  2. ncu -u to update package.json without actually installing the newer packages to node_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.


No comment

Your email address will not be published. Required fields are marked *.