lib/asciidoctor/syntax_highlighter/pygments.rb
# frozen_string_literal: true
module Asciidoctor
class SyntaxHighlighter::PygmentsAdapter < SyntaxHighlighter::Base
register_for 'pygments'
def initialize *args
super
@requires_stylesheet = @style = nil
end
def highlight?
library_available?
end
def highlight node, source, lang, opts
lexer = (::Pygments::Lexer.find_by_alias lang) || (::Pygments::Lexer.find_by_mimetype 'text/plain')
@requires_stylesheet = true unless (noclasses = opts[:css_mode] != :class)
highlight_opts = {
classprefix: TOKEN_CLASS_PREFIX,
cssclass: WRAPPER_CLASS,
nobackground: true,
noclasses: noclasses,
startinline: lexer.name == 'PHP' && !(node.option? 'mixed'),
stripnl: false,
style: (@style ||= (style = opts[:style]) && (style_available? style) || DEFAULT_STYLE),
}
if (highlight_lines = opts[:highlight_lines])
highlight_opts[:hl_lines] = highlight_lines.join ' '
end
if (linenos = opts[:number_lines]) && (highlight_opts[:linenostart] = opts[:start_line_number]) && (highlight_opts[:linenos] = linenos) == :table
if (highlighted = lexer.highlight source, options: highlight_opts)
highlighted = highlighted.sub StyledLinenoColumnStartTagsRx, LinenoColumnStartTagsCs if noclasses
highlighted = highlighted.sub WrapperTagRx, PreTagCs
opts[:callouts] ? [highlighted, (idx = highlighted.index CodeCellStartTagCs) ? idx + CodeCellStartTagCs.length : nil] : highlighted
else
node.sub_specialchars source # handles nil response from ::Pygments::Lexer#highlight
end
elsif (highlighted = lexer.highlight source, options: highlight_opts)
highlighted = highlighted.gsub StyledLinenoSpanTagRx, LinenoSpanTagCs if linenos && noclasses
highlighted.sub WrapperTagRx, '\1'
else
node.sub_specialchars source # handles nil response from ::Pygments::Lexer#highlight
end
end
def format node, lang, opts
if opts[:css_mode] != :class && (@style = (style = opts[:style]) && (style_available? style) || DEFAULT_STYLE) &&
(pre_style_attr_val = base_style @style)
opts[:transform] = proc {|pre| pre['style'] = pre_style_attr_val }
end
super
end
def docinfo? location
@requires_stylesheet && location == :head
end
def docinfo location, doc, opts
if opts[:linkcss]
%(<link rel="stylesheet" href="#{doc.normalize_web_path (stylesheet_basename @style), (doc.attr 'stylesdir', ''), false}"#{opts[:self_closing_tag_slash]}>)
else
%(<style>
#{read_stylesheet @style}
</style>)
end
end
def write_stylesheet? doc
@requires_stylesheet
end
def write_stylesheet doc, to_dir
::File.write (::File.join to_dir, (stylesheet_basename @style)), (read_stylesheet @style), mode: FILE_WRITE_MODE
end
module Loader
private
def library_available?
(@@library_status ||= load_library) == :loaded ? true : nil
end
def load_library
(defined? ::Pygments::Lexer) ? :loaded : (Helpers.require_library 'pygments', 'pygments.rb', :warn).nil? ? :unavailable : :loaded
end
end
module Styles
include Loader
def read_stylesheet style
library_available? ? @@stylesheet_cache[style || DEFAULT_STYLE] || '/* Failed to load Pygments CSS. */' : '/* Pygments CSS disabled because Pygments is not available. */'
end
def stylesheet_basename style
%(pygments-#{style || DEFAULT_STYLE}.css)
end
private
def base_style style
library_available? ? @@base_style_cache[style || DEFAULT_STYLE] : nil
end
def style_available? style
(((@@available_styles ||= ::Pygments.styles.to_set).include? style) rescue nil) && style
end
@@base_style_cache = ::Hash.new do |cache, key|
if BaseStyleRx =~ @@stylesheet_cache[key]
@@base_style_cache = cache.merge key => (style = $1.strip)
style
end
end
@@stylesheet_cache = ::Hash.new do |cache, key|
if (stylesheet = ::Pygments.css BASE_SELECTOR, classprefix: TOKEN_CLASS_PREFIX, style: key)
@@stylesheet_cache = cache.merge key => stylesheet
stylesheet
end
end
DEFAULT_STYLE = 'default'
BASE_SELECTOR = 'pre.pygments'
TOKEN_CLASS_PREFIX = 'tok-'
BaseStyleRx = /^#{BASE_SELECTOR.gsub '.', '\\.'} +\{([^}]+?)\}/
private_constant :BASE_SELECTOR, :TOKEN_CLASS_PREFIX, :BaseStyleRx
end
extend Styles # exports static methods
include Loader, Styles # adds methods to instance
CodeCellStartTagCs = '<td class="code">'
LinenoColumnStartTagsCs = '<td class="linenos"><div class="linenodiv"><pre>'
LinenoSpanTagCs = '<span class="lineno">\1</span>'
PreTagCs = '<pre>\1</pre>'
StyledLinenoColumnStartTagsRx = /<td><div class="linenodiv" style="[^"]+?"><pre style="[^"]+?">/
StyledLinenoSpanTagRx = %r(<span style="background-color: #f0f0f0; padding: 0 5px 0 5px">( *\d+ )</span>)
WRAPPER_CLASS = 'lineno' # doesn't appear in output; Pygments appends "table" to this value to make nested table class
# NOTE <pre> has style attribute when pygments-css=style
# NOTE <div> has trailing newline when pygments-linenums-mode=table
# NOTE initial <span></span> preserves leading blank lines
WrapperTagRx = %r(<div class="#{WRAPPER_CLASS}"><pre\b[^>]*?>(.*)</pre></div>\n*)m
private_constant :CodeCellStartTagCs, :LinenoColumnStartTagsCs, :LinenoSpanTagCs, :PreTagCs, :StyledLinenoColumnStartTagsRx, :StyledLinenoSpanTagRx, :WrapperTagRx, :WRAPPER_CLASS
end
end