lib/kramdown/converter/math_engine/sskatex.rb



# -*- coding: utf-8 -*-
#
#--
# Copyright (C) 2017 Christian Cornelssen <ccorn@1tein.de>
#
# This file is part of kramdown which is licensed under the MIT.
#++

module Kramdown::Converter::MathEngine

  # Consider this a lightweight alternative to MathjaxNode. Uses KaTeX and ExecJS (via ::SsKaTeX)
  # instead of MathJax and Node.js. Javascript execution context initialization is done only once.
  # As a result, the performance is reasonable.
  module SsKaTeX

    # Indicate whether SsKaTeX may be available.
    #
    # This test is incomplete; it cannot test the existence of _katex_js_ nor the availability of a
    # specific _js_run_ because those depend on configuration not given here. This test mainly
    # indicates whether static dependencies such as the +sskatex+ and +execjs+ gems are available.
    AVAILABLE = begin
      require 'sskatex'
      # No test for any JS engine availability here; specifics are config-dependent anyway
      true
    rescue LoadError
      false
    end

    if AVAILABLE

      # Class-level cache for ::SsKaTeX converter state, queried by configuration. Note: KTXC
      # contents may become stale if the contents of used JS files change while the configuration
      # remains unchanged.
      KTXC = ::Kramdown::Utils::LRUCache.new(10)

      # A logger that routes messages to the debug channel only. No need to create this dynamically.
      DEBUG_LOGGER = lambda { |level, &expr| warn(expr.call) }

      class << self
        private

        # Given a Kramdown::Converter::Base object _converter_, retrieves the logging options and
        # builds an object usable for ::SsKaTeX#logger. The result is either +nil+ (no logging) or a
        # +Proc+ object which, when given a _level_ (either +:verbose+ or +:debug+) and a block,
        # decides whether logging is enabled, and if so, evaluates the given block for the message
        # and routes that message to the appropriate channels. With <tt>level == :verbose+</tt>,
        # messages are passed to _converter_.warning if the _converter_'s +:verbose+ option is set.
        # All messages are passed to +warn+ if the _converter_'s +:debug+ option is set.
        #
        # Note that the returned logger may contain references to the given _converter_ and is not
        # affected by subsequent changes in the _converter_'s logging options.
        def logger(converter)
          config = converter.options[:math_engine_opts]
          debug = config[:debug]
          if config[:verbose]
            # Need a closure
            lambda do |level, &expr|
              verbose = (level == :verbose)
              msg = expr.call if debug || verbose
              warn(msg) if debug
              converter.warning(msg) if verbose
            end
          elsif debug
            DEBUG_LOGGER
          end
        end

        # Given a Kramdown::Converter::Base object _converter_, return a ::SsKaTeX converter _sktx_
        # that has been configured with _converter_'s +math_engine_opts+, but not for logging. Cache
        # _sktx_ for reuse, without references to _converter_.
        def katex_conv(converter)
          config = converter.options[:math_engine_opts]
            # Could .reject { |key, _| [:verbose, :debug].include?(key.to_sym) }
            # because the JS engine setup can be reused for different logging settings.
            # But then the +math_engine_opts+ dict would be essentially dup'ed every time,
            # and late activation of logging would miss the initialization if the engine is reused.
          KTXC[config] ||= ::SsKaTeX.new(config)
        end

        public

        # The function used by kramdown for rendering TeX math to HTML
        def call(converter, el, opts)
          display_mode = el.options[:category]
          ans = katex_conv(converter).call(el.value, display_mode == :block, &logger(converter))
          attr = el.attr.dup
          attr.delete('xmlns')
          attr.delete('display')
          ans.insert(ans =~ /[[:space:]>]/, converter.html_attributes(attr))
          ans = ' ' * opts[:indent] << ans << "\n" if display_mode == :block
          ans
        end

      end
    end
  end
end