require "execjs" require "nokogiri" require "net/http" require "json" require "cgi" require "optparse" class KatexRenderer def initialize(source) @context = ExecJS.compile(source) @inline_cache = {} @display_cache = {} end def render(display, string) cache = display ? @display_cache : @inline_cache comment = display ? "display" : string string = CGI.unescapeHTML string cache.fetch(string) do puts " Rendering #{comment}" options = { "throwOnError" => false, "displayMode" => display } cache[string] = @context.call("katex.renderToString", string, options) end end def substitute(content) rendered = content.gsub /\\\(((?:[^\\]|\\[^\)])*)\\\)/ do |match| render(false, $~[1]) end rendered = rendered.gsub /\$\$((?:[^\$]|$[^\$])*)\$\$/ do |match| render(true, $~[1]) end return rendered end end # Provided via this project's Gemfile ExecJS.runtime = ExecJS::Runtimes::Duktape katex = nil OptionParser.new do |opts| opts.banner = "Usage: convert.rb [options]" opts.on("--katex-js-file=FILE", "Use the given KaTeX JS file to process LaTeX") do |f| katex = f end end.parse! files = ARGV if katex katex = File.read(katex) else katex = Net::HTTP.get(URI("https://static.danilafe.com/katex/katex.min.js")) end renderer = KatexRenderer.new(katex) files.each do |file| puts "Rendering file: #{file}" document = Nokogiri::HTML.parse(File.open(file)) document.search('//*[not(ancestor-or-self::code or ancestor-or-self::script)]/text()').each do |t| t.replace(renderer.substitute(t.content)) end File.write(file, document.to_html(encoding: 'UTF-8')) end