class Mato::HtmlFilters::SyntaxHighlight

Public Class Methods

new(on_rouge_error: ->(ex) { warn ex } click to toggle source
# File lib/mato/html_filters/syntax_highlight.rb, line 11
def initialize(on_rouge_error: ->(ex) { warn ex })
  @on_rouge_error = on_rouge_error
end

Public Instance Methods

call(doc) click to toggle source

@param [Nokogiri::HTML4::DocumentFragment] doc

# File lib/mato/html_filters/syntax_highlight.rb, line 16
def call(doc)
  doc.search("pre").each do |pre|
    if pre.at('code')
      pre.replace(highlight(pre))
    end
  end
end
guess_lexer(language, filename, source) click to toggle source

@param [String] language @param [String] filename @param [String] source @return [Rouge::Lexer]

# File lib/mato/html_filters/syntax_highlight.rb, line 28
def guess_lexer(language, filename, source)
  Rouge::Lexer.find(language)&.tap do |lexer|
    return lexer.new
  end

  Rouge::Lexer.guess(filename: filename, source: source, &:first).new
end
parse_label(class_name) click to toggle source

@param [String,nil] CSS class names, e.g. “foo.js” “ruby:foo.rb”

# File lib/mato/html_filters/syntax_highlight.rb, line 37
def parse_label(class_name)
  a = class_name&.split(/:/) || []
  if a.empty?
    {}
  elsif a.size == 1
    token = a[0].sub(/^language-/, '')
    if Rouge::Lexer.find(token)
      { language: token }
    else
      { filename: token }
    end
  else
    { language: a[0], filename: a[1] }
  end
end

Private Instance Methods

format(lexer, source) click to toggle source
# File lib/mato/html_filters/syntax_highlight.rb, line 95
def format(lexer, source)
  tokens = lexer.lex(source)
  formatter.format(tokens)
rescue => ex
  @on_rouge_error.call(ex)
  raise RougeError.new
end
formatter() click to toggle source
# File lib/mato/html_filters/syntax_highlight.rb, line 91
def formatter
  @formatter ||= Rouge::Formatters::HTML.new
end
highlight(pre) click to toggle source

@param [Nokogiri::XML::Element] pre a <pre/> element @return [Nokogiri::XML::Element] a new <div/> wrapping the given code block

# File lib/mato/html_filters/syntax_highlight.rb, line 57
def highlight(pre)
  code = pre.at('code')
  metadata = parse_label(code['class'])
  language = metadata[:language]&.sub(/^language-/, '')
  filename = metadata[:filename]
  source = code.inner_text

  lexer = guess_lexer(language, filename, source)

  begin
    document = Nokogiri::HTML4.fragment(%{<div class="code-frame"/>})
    div = document.at('div')
    div.add_child(label_fragment(filename || language || lexer.tag)) if filename || !lexer.is_a?(Rouge::Lexers::PlainText)
    div.add_child(%{<pre class="highlight"><code data-lang="#{lexer.tag}">#{format(lexer, source)}</code></pre>})
  rescue => ex
    if ex.is_a?(RougeError) && !lexer.is_a?(Rouge::Lexers::PlainText)
      # Retry highlighting with PlainText lexer if Rouge raises an error.
      # It avoids to affect the error to whole of converting.
      lexer = Rouge::Lexers::PlainText.new
      retry
    else
      raise ex
    end
  end

  document
end
label_fragment(label) click to toggle source
# File lib/mato/html_filters/syntax_highlight.rb, line 85
def label_fragment(label)
  Nokogiri::HTML4.fragment(%{<div class="code-label"/>}).tap do |fragment|
    fragment.at('div').add_child(Nokogiri::XML::Text.new(label, fragment))
  end
end