Search code examples
jekyllkatexrouge

Jekyll with Rouge and KaTeX cannot have $ in code


I am creating a new Jekyll blog setup where I want to self-host and generate server-side everything I can (so basically everything besides Disqus and Google Analytics). I want my site to work well even on browsers with JS turned off, which is why I turned to Kramdown+Rouge for syntax highlighting and KaTex for rendeding TeX formula during site generation.

I found a problem though. When I have example like:

```bash
$ echo test
test
```

It will break inside KaTeX command:

{% katexmm %}
{{ content }}
{% endkatexmm %}

I figured out the cause:

  • rouge turns snippet into something like <span class="gp">$</span>
  • katexmm extects that each $ is paired with another $ or escaped as \$

What I want to know is how to fix this, e.g. by escaping all $ inside pre, but leaving other $ (which are intended to use as actual LaTeX snippets) intact. Or maybe somehow configuring jekyll-katex to ignore unpaired $? (throw_error: false option works after text was already matched using dollar signs, so it doesn't help).

I want to fix this in config or in place of applying katexmm, so that I wouldn't have to modify the content of any post.


Solution

  • I managed to get rid of errors by modifying tag from jekyll-katex to work around code (inline, use single tilde) and pre code (block made using indention or 3 tildes):

    # frozen_string_literal: true
    
    require 'jekyll'
    require 'jekyll-katex/configuration'
    require 'jekyll-katex/katex_js'
    require 'nokogiri'
    
    module Jekyll
      module Tags
        # Defines the custom Liquid tag for compile-time rendering of KaTeX math.
        # This differs from the katex tag in that it allows use of `$` and `$$` fencing to mark math mode blocks similar to
        # standard latex.
        #   {% katexmm %}
        #   This is a mixed environment where you can write text as normal but fence off latex math using `$`. Escape
        #   using `\$`. For example.
        #   $latex math with \$$
        #   $$display mode latex$$
        #   {% endkatexmm %}
        class KatexMathModeFixed < Liquid::Block
          LOG_TOPIC = 'KatexMathModeFixed:'
          KATEX ||= Jekyll::Katex::KATEX_JS
          LATEX_TOKEN_PATTERN = /(?<!\\)([$]{2}|[$]{1})(.+?)(?<!\\)\1/m
    
          def initialize(tag_name, markup, tokens)
            super
            @markup = markup
            @tokens = tokens
            @display_mode_rendering = Jekyll::Katex::Configuration.global_rendering_options.merge(displayMode: true)
            @inline_mode_rendering = Jekyll::Katex::Configuration.global_rendering_options.merge(displayMode: false)
          end
    
          def render(context)
            enclosed_block = super
            fixed_block = fix_code(enclosed_block)
            rendered_str = fixed_block.to_s.gsub(LATEX_TOKEN_PATTERN) do |match|
              display_mode = match.to_s.start_with? '$$'
              rendering_options = display_mode ? @display_mode_rendering : @inline_mode_rendering
              Jekyll.logger.debug LOG_TOPIC, "Rendering matched block - #{match}"
              KATEX.call('katex.renderToString', Regexp.last_match(2), rendering_options)
            end
            # KaTeX should fix escaped `$` within fenced blocks, this addresses instances outside of math mode
            rendered_str.to_s.gsub(/\\[$]/, '$').to_s
          end
    
          def fix_code(input)
            updated = false
            html = Nokogiri::HTML.fragment(input)
            Jekyll.logger.debug LOG_TOPIC, "Fixing - #{input}"
            html.css("code, code span").each do |c|
              if c.css('*').empty? && c.content['$']
                updated = true
                Jekyll.logger.debug LOG_TOPIC, "current tag - #{c}"
                content = c.content
                content['$'] = '\$'
                c.content = content
                Jekyll.logger.debug LOG_TOPIC, "current tag now - #{c}/#{content}"
              end
            end
            output = html.to_s
            Jekyll.logger.debug LOG_TOPIC, "Fixed - #{output}"
            if updated then html.to_s else input end
          end
        end
      end
    end
    
    Liquid::Template.register_tag('katexmmx', Jekyll::Tags::KatexMathModeFixed)
    

    It's installable in _plugins directory.

    Thing is, this is still buggy - by default kramdown still tries to use mathjax engine and generates <script type="math/tex">, so that had to be changed. When I was investigating how, I found out that kramdown supports math_engine: katex as well - with that I only had to add fonts and CSS, and jekyll-katex become completely obsolete (as well as my workaround, which I'll leave here if anyone is curious).