class Jekyll::Mathifier

Run Jekyll documents through MathJax's node engine, transform style attributes into inline style tags and compute their hashes

Constants

FIELDS
MATH_TAG_REGEX

Attributes

csp_hashes[RW]

Public Class Methods

compileStyleElement(parsed_doc, style_attributes) click to toggle source

Compile all style attributes into CSS classes in a single <style> element in the head

# File lib/jekyll-mathjax-csp.rb, line 79
def compileStyleElement(parsed_doc, style_attributes)
  style_content = ""
  style_attributes.each do |digest, style_attribute|
    style_content += ".mathjax-inline-#{digest}{#{style_attribute}}"
  end
  style_tag = parsed_doc.at_css("head").add_child("<style>#{style_content}</style>")[0]
  hashStyleTag(style_tag)
end
extractStyleAttributes(parsed_doc) click to toggle source

Extract all style attributes from SVG and container elements and replace them with a new CSS class with a deterministic name

# File lib/jekyll-mathjax-csp.rb, line 56
def extractStyleAttributes(parsed_doc)
  style_attributes = {}
  styled_tags = parsed_doc.css("svg[style],mjx-container[style]")
  for styled_tag in styled_tags do
    style_attribute = styled_tag["style"]
    digest = Digest::MD5.hexdigest(style_attribute)[0..15]
    style_attributes[digest] = style_attribute

    digest_class = "mathjax-inline-#{digest}"
    styled_tag["class"] = "#{styled_tag["class"] || ""} #{digest_class}"
    styled_tag.remove_attribute("style")
  end
  return style_attributes
end
hashStyleTag(style_tag) click to toggle source

Compute a CSP hash source (using SHA256)

# File lib/jekyll-mathjax-csp.rb, line 72
def hashStyleTag(style_tag)
  csp_digest = "'sha256-#{Digest::SHA256.base64digest(style_tag.content)}'"
  style_tag.add_previous_sibling("<!-- #{csp_digest} -->")
  @csp_hashes.add(csp_digest)
end
mathable?(doc) click to toggle source
# File lib/jekyll-mathjax-csp.rb, line 157
def mathable?(doc)
  (doc.is_a?(Jekyll::Page) || doc.write?) &&
    doc.output_ext == ".html" || (doc.permalink && doc.permalink.end_with?("/"))
end
mathify(docs, config) click to toggle source

Render math in batch for all documents

# File lib/jekyll-mathjax-csp.rb, line 112
def mathify(docs, config)
  docs = docs.select { |d| Jekyll::Mathifier.mathable?(d) } # can be mathified?
             .select { |d| MATH_TAG_REGEX.match?(d.output) } # needs mathifying?
             .reduce({}) { |h, d| h.merge!(d.path => d) } # use #path to identify a doc
  return if docs.empty?

  Jekyll.logger.info "mathjax_csp:", "Found #{docs.length} documents."

  docs.each do |path, doc|
    Jekyll.logger.info "Checking math:", path
    parsed_doc = Nokogiri::HTML::Document.parse(doc.output)
    # Ensure that all styles we pick up weren't present before MathJax ran
    unless parsed_doc.css("svg[style],mjx-container[style]").empty?
      Jekyll.logger.error "mathjax_csp:", "Inline style on <svg> or <mjx-container> element present"
      Jekyll.logger.error "", "before rendering math due to misconfiguration or server-side"
      Jekyll.logger.abort_with "", "style injection."
    end
  end

  mathjaxify_input = docs.reduce({}) { |h, (path, doc)| h.merge!(path => doc.output) }
                         .to_json
  mathified_docs = JSON.parse(run_mathjaxify(config, mathjaxify_input))

  mathified_docs.each do |path, mathified_html|
    parsed_doc = Nokogiri::HTML::Document.parse(mathified_html)
    last_child = parsed_doc.at_css("head").last_element_child()
    if last_child.name == "style"
      # Set strip_css to true in _config.yml if you load the styles MathJax adds to the head
      # from an external *.css file
      if config["strip_css"]
        Jekyll.logger.info "", "Remember to <link> in external stylesheet!"
        last_child.remove
      else
        hashStyleTag(last_child)
      end
    end

    style_attributes = extractStyleAttributes(parsed_doc)
    compileStyleElement(parsed_doc, style_attributes)
    docs[path].output = parsed_doc.to_html
  end

  Jekyll.logger.info "mathjax_csp:", "Mathified #{mathified_docs.length} documents."
end
run_mathjaxify(config, input) click to toggle source

Run the MathJax node backend on a JSON input containing an file: HTML map

# File lib/jekyll-mathjax-csp.rb, line 89
def run_mathjaxify(config, input)
  mathified = ""
  exit_status = 0
  command = "node #{Gem.bin_path("jekyll-mathjax-csp", "mathjaxify")}"

  FIELDS.each do |name, flag|
    unless config[name].nil?
      if [true, false].include? config[name]
        command << " " << flag
      else
        command << " " << flag << " " << config[name].to_s
      end
    end
  end

  node_path = Dir.pwd + "/node_modules"
  Jekyll.logger.info "mathjax_csp:", "starting node #{command}"
  mathified, status = Open3.capture2({"NODE_PATH" => node_path}, command, stdin_data: input)
  return mathified unless status != 0
  Jekyll.logger.abort_with "mathjax_csp:", "Failed to execute 'bin/mathjaxify'"
end