I often run into two problems when reading a blog post on mathematics: My feed-reader does not evaluate Javascript, and all equations are presented as raw Mathjax or Latex-inline code.
There are of course feed-readers that do, but I don't use them. I could open the article in a browser, but alas I do block Javascript there as well.
Whilst Mathjax is a very comfy solution to displaying nice looking equations on a webpage, most blog-pages these days (with their respective feeds included) are rendered using static website generators such as Jekyll or Hugo. This means all equations can be pre-baked and do not need any Javascript at all.
Math equations can be generated in multiple different ways though:
- SVG: Render the entire equation into an SVG. These will scale nicely and always look sharp.
- MathML: Use the W3 standard to render math equations directly in HTML. They too scale well.
- Image: Render the equation into a regular image. This can look jaggy/aliased.
I decided to investigate options 1 and 2. It turns out however that SVG is an element that gets blocked by many feed-readers or filtered out entirely.
Therefore, option 2 was left. My page is generated using Jekyll and uses plugins to render math with the help of mathjax-node-page, a Node module which receives a piece of Latex-infused text and then spits out MathML.
If you want to replicate this process, read on.
Node and mathjax-node-page
As a first step, install npm and the mathjax-node-page module.
$ PKGMANAGER install npm
$ npm install mathjax-node-page
You'll end up with a directory node_modules that contains the binary mjpage which is used by the script in the next section.
00-render-math.rb
The following piece of code renders MathML in case the page features any Latex markers (and is not an XML file). It calls mjpage to replace said markers with MathML code, then puts the content back. Store this as _plugins/00-render-math.rb.
The script does two more things to make sure each equation is followed by a reference number, and removes some annoying leftover attributes from MathML because I format everything using CSS.
require 'nokogiri'
MATH_TAG_REGEX = /(<script[^>]*type="math\/tex|\\\[.*\\\]|\\\(.*\\\))/im
FIELDS = {
"format" => "--format",
"font" => "--font",
"equation_number" => "--eqno",
"output" => "--output",
"eqno" => "--eqno",
"ex_size" => "--ex",
"width" => "--width",
"extensions" => "--extensions",
"font_url" => "--fontURL"
}
FLAGS = {
"linebreaks" => "--linebreaks",
"single_dollars" => "--dollars",
"semantics" => "--semantics",
"notexthints" => "--notexthints",
"fragment" => "--fragment"
}
def run_mjpage(config, output)
mathified = ""
exit_status = 0
command = "node_modules/mathjax-node-page/bin/mjpage"
FIELDS.each do |name, flag|
unless config[name].nil?
command << " " << flag << " " << config[name].to_s
end
end
FLAGS.each do |name, flag|
unless config[name].nil?
command << " " << flag
end
end
begin
Open3.popen2(command) {|i,o,t|
i.print output
i.close
o.each {|line|
mathified.concat(line)
}
exit_status = t.value
}
return mathified unless exit_status != 0
Jekyll.logger.abort_with "render-math:", "'node_modules/mathjax-node-page/mjpage' not found"
rescue
Jekyll.logger.abort_with "render-math:", "Failed to execute 'node_modules/mathjax-node-page/mjpage'"
end
end
Jekyll::Hooks.register([:pages, :posts], :post_convert) do |page|
if MATH_TAG_REGEX.match?(page.content) && !page.relative_path.end_with?(".xml")
page.content = run_mjpage(page.site.config["mjpage"], page.content)
# exchange for regular mathml rows
page.content = page.content.gsub(/<\/?mlabeledtr\b([^>]*)>/i) { |tag| tag.start_with?('</') ? "</mtr>" : "<mtr#{$1}>" }
doc = Nokogiri::HTML(page.content)
# add column alignment for labels
doc.css('mtable').each do |mtable|
mtable.remove_attribute('width')
mtable.remove_attribute('columnspacing')
mtable.remove_attribute('rowspacing')
mtable['columnalign'] = "left"
# Move first mtd to the end in each mtr
mtable.css('mtr').each do |mtr|
first_mtd = mtr.css('mtd').first
if first_mtd
first_mtd.remove
mtr.add_child(first_mtd)
end
end
end
page.content = doc.to_html
Jekyll.logger.info("Mathified " + page.relative_path)
end
end
You can configure the behavior of this script in your _config.yml. Here's the configuration I currently run.
mjpage:
single_dollars: true
eqno: all
width: 83
output: MML
fragment: true
Importantly, output: MML sets the output to MathML instead of SVG.
01-add-rss-content.rb
Instead of re-rendering the output for RSS, this plugin takes the already mathified content variable of each page and puts the entire output into a custom page variable for later. Store it as _plugins/01-add-rss-content.rb. The number in the plugin file determines the execution order, so this one is executed after 00-render-math.rb.
Jekyll::Hooks.register([:posts], :post_convert) do |p|
doc = Nokogiri::HTML.parse(p.content)
p.data["rsscontent"] = doc.at('body').inner_html.strip
end
rss.xml
In the final RSS XML, we can access the page's generated rsscontent variable from the plugin and just dump it directly into the description. This ensures that the generated MathML code is also present in the RSS item, and the HTML of the webpage's body matches exactly with the RSS item's <description>.
...
<description><![CDATA[ {{ post.rsscontent }} ]]></description>
...
And that's it. Now the webpage and the feed both contain beautiful rendered math equations, even when Javascript is entirely disabled, just as the W3 intended!