Class: SsKaTeX

Inherits:
Object
  • Object
show all
Defined in:
lib/sskatex.rb

Overview

This is a TeX-to-HTML+MathML+CSS converter class using the Javascript-based KaTeX, interpreted by one of the Javascript engines supported by ExecJS. The intended purpose is to eliminate the need for math-rendering Javascript in the client’s HTML browser. Therefore the name: SsKaTeX means server-side KaTeX.

Javascript execution context initialization can be done once and then reused for formula renderings with the same general configuration. As a result, the performance is reasonable. Consider this a fast and lightweight alternative to mathjax-node-cli.

Requirements for using SsKaTeX:

Although the converter only needs katex.min.js, you may need to serve the rest of the KaTeX package, that is, CSS and fonts, as resources to the targeted web browsers. The upside is that your HTML templates need no longer include Javascripts for Math (neither katex.js nor any search-and-replace script). Your HTML templates should continue referencing the KaTeX CSS. If you host your own copy of the CSS, also keep hosting the fonts.

Minimal usage example:

tex_to_html = SsKaTeX.new(katex_js: 'path-to-katex/katex.min.js')
# Here you could verify contents of tex_to_html.js_source for security...

body_html = '<p>By Pythagoras, %s. Furthermore:</p>' %
  tex_to_html.call('a^2 + b^2 = c^2', false)  # inline math
body_html <<                                  # block display
  tex_to_html.call('\frac{1}{2} + \frac{1}{3} + \frac{1}{6} = 1', true)
# etc, etc.

More configuration options are described in the Rdoc. Most options, with the notable exception of #katex_opts, do not affect usage nor output, but may be needed to make SsKaTeX work with all the external parts (JS engine and KaTeX). Since KaTeX is distributed separately from the SsKaTeX gem, configuration of the latter must support the specification of Javascript file locations. This implies that execution of arbitrary Javascript code is possible. Specifically, options with js in their names should be accepted from trusted sources only. Applications using SsKaTeX need to check this.

Defined Under Namespace

Modules: Utils

Constant Summary collapse

DATADIR =

Root directory for auxiliary files of this gem

File.expand_path(File.join(File.dirname(__FILE__),
'..', 'data', 'sskatex'))
DEFAULT_JS_DIR =

The default for the #js_dir configuration option. Path of a directory with Javascript helper files.

File.join(DATADIR, 'js')
DEFAULT_KATEX_JS =

The default path to katex.js, cf. the #katex_js configuration option. For a relative path, the starting point is the current working directory.

File.join('katex', 'katex.min.js')
DEFAULT_JS_LIBS =

The default for the #js_libs configuration option. A list of UTF-8-encoded Javascript helper files to load. Relative paths are interpreted relative to #js_dir.

['escape_nonascii_html.js', 'tex_to_html.js']

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cfg = {}, &logger) ⇒ SsKaTeX

Create a new instance configured with keyword arguments and optional logger. The arguments are just stored by reference; no further action is taken until parts of the configuration are actually needed in other method calls. The dictionary with the keyword arguments can be accessed as #config. The logger can be accessed as #logger.



252
253
254
255
# File 'lib/sskatex.rb', line 252

def initialize(cfg = {}, &logger)
  @logger = logger
  self.config = cfg
end

Instance Attribute Details

#loggerObject

This can be used for monitoring or debugging. Must be either nil or a

proc {|level, &block| ...}

where the block is used for on-demand construction of the log message. level is one of:

:verbose

For information about the effective engine configuration. Issued on first use of a changed configuration option.

:debug

For the Javascript expressions used when converting TeX. Issued once per TeX snippet.

For example, to trace :verbose but not :debug messages, set #logger to

lambda {|level, &block| warn(block.call) if level == :verbose}

or, equivalently, to the output of ::warn_logger.

If #logger is nil, no logging will be done.



200
201
202
# File 'lib/sskatex.rb', line 200

def logger
  @logger
end

Class Method Details

.warn_logger(level = :verbose) ⇒ Object

Given a desired log level, this returns an object useful for #logger. That logger object simply outputs messages via warn. If level is :debug, all messages are output. If level is :verbose, only verbose-level messages are output. level can be given as a symbol or as its JSON-equivalent string. If level is anything else, nil will be returned, thus disabling logging.



101
102
103
# File 'lib/sskatex.rb', line 101

def self.warn_logger(level = :verbose)
  LOGGERS[level]
end

Instance Method Details

#call(tex, display_mode = false, &logger) ⇒ Object

Given a TeX math fragment tex and a boolean display_mode (true for block, default false for inline), run the JS engine (using #js_context) and let KaTeX compile the math fragment. Return the resulting HTML string. Can raise errors if something in the process fails. If a block is given, it is used instead of the logger set with #logger=.



384
385
386
387
388
389
390
391
392
393
394
# File 'lib/sskatex.rb', line 384

def call(tex, display_mode = false, &logger)
  logger ||= @logger
  ctx = js_context(&logger)
  js = "tex_to_html(#{Utils.js_quote(tex)}, #{display_mode.to_json}, katex_opts)"
  logd(logger) {"JS eval: #{js}"}
  ans = ctx.eval(js)
  unless ans && ans.start_with?('<') && ans.end_with?('>')
    raise "KaTeX conversion failed!\nInput:\n#{tex}\nOutput:\n#{ans}"
  end
  ans
end

#configObject

A dictionary with the used configuration options. The resulting effective option values can be read from the same-named attributes #katex_js, #katex_opts, #js_dir, #js_libs, #js_run. See also #config=.



225
226
227
# File 'lib/sskatex.rb', line 225

def config
  @config
end

#config=(cfg) ⇒ Object

Reconfigure the conversion engine by passing in a dictionary, without affecting the #logger setting. Changes become effective on first use.

Note: The dict will be shared by reference. Its deep object tree should remain unchanged at least until #js_context or #call has been invoked. Thereafter changes do not matter until #config= is assigned again.



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/sskatex.rb', line 235

def config=(cfg)
  @js_context = nil
  @js_source = nil
  @katex_opts = nil
  @katex_js = nil
  @js_libs = nil
  @js_dir = nil
  @js_runtime = nil
  @js_run = nil
  @config = cfg
end

#js_context(&logger) ⇒ Object

The JS engine context obtained by letting the #js_runtime(&logger) compile the #js_source(&logger). Created at first use e.g. by #call.



375
376
377
# File 'lib/sskatex.rb', line 375

def js_context(&logger)
  @js_context ||= js_runtime(&logger).compile(js_source(&logger))
end

#js_dirObject

The path to a directory with Javascript helper files as specified by #config[ :js_dir ], or its default which is the subdirectory js in the data directory of SsKaTeX. There is no need to change that setting unless you want to experiment with Javascript details.



294
295
296
# File 'lib/sskatex.rb', line 294

def js_dir
  @js_dir ||= @config[:js_dir] || DEFAULT_JS_DIR
end

#js_libsObject

A list of UTF-8-encoded Javascript helper files to load. Can be overridden with #config[ :js_libs ]. Relative paths are interpreted relative to #js_dir. The default setting (in YAML notation) is

js_libs:
  - escape_nonascii_html.js
  - tex_to_html.js

And there is no need to change that unless you want to experiment with Javascript details.

Files available in the default #js_dir are:

escape_nonascii_html.js

defines a function escape_nonascii_html that converts non-ASCII characters to HTML numeric character references. Intended as postprocessing filter.

tex_to_html.js

defines a function tex_to_html(tex, display_mode, katex_opts) that takes a LaTeX math string, a boolean display mode (true for block display, false for inline), and a dict with general KaTeX options, and returns a string with corresponding HTML+MathML output. The implementation is allowed to set katex_opts.displayMode. SsKaTeX applies tex_to_html to each math fragment encountered. The implementation given here uses katex.renderToString and postprocesses the output with escape_nonascii_html.



325
326
327
# File 'lib/sskatex.rb', line 325

def js_libs
  @js_libs ||= @config[:js_libs] || DEFAULT_JS_LIBS
end

#js_run(&logger) ⇒ Object

Identifies the Javascript engine to be used. Defined identifiers include: :RubyRacer, :RubyRhino, :Duktape, :MiniRacer, :Node, :JavaScriptCore, :Spidermonkey, :JScript, :V8, and :Disabled; that last one would raise an error on first run (by #js_context). Which engines are actually available depends on your installation.

#js_run is determined on first demand as follows, then logged and cached for reuse. If #config[ :js_run ] is not defined, the contents of the environment variable EXECJS_RUNTIME will be considered instead; and if that is not defined, an automatic choice will be made. For more information, use the logger to show :verbose messages and consult the documentation of ExecJS. If a block is given, it is used instead of the logger set with #logger=.



270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/sskatex.rb', line 270

def js_run(&logger)
  @js_run ||= begin
    logger ||= @logger
    logv(logger) {"Available JS runtimes: #{Utils.js_runtimes.join(', ')}"}
    jsrun = (@config[:js_run] ||
             ENV['EXECJS_RUNTIME'] ||
             Utils::JSRUN_TOSYM[ExecJS::Runtimes.best_available] ||
             'Disabled').to_s.to_sym
    logv(logger) {"Selected JS runtime: #{jsrun}"}
    jsrun
  end
end

#js_runtime(&logger) ⇒ Object

The ExecJS::Runtime subclass corresponding to #js_run(&logger).



284
285
286
287
288
# File 'lib/sskatex.rb', line 284

def js_runtime(&logger)
  @js_runtime ||= Utils::JSRUN_FROMSYM[js_run(&logger)].tap do |runtime|
    runtime.available?        # trigger necessary initializations
  end
end

#js_source(&logger) ⇒ Object

The concatenation of the contents of the files in #js_libs, in #katex_js, and a JS variable definition for #katex_opts, each item followed by a newline. Created at first use, with filenames and #katex_opts logged. Can be used to validate JS contents before a #js_context is created. If a block is given, it is used instead of the logger set with #logger=.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/sskatex.rb', line 354

def js_source(&logger)
  @js_source ||= begin
    logger ||= @logger

    js = ''
    js_libs.each do |libfile|
      absfile = File.expand_path(libfile, js_dir)
      logv(logger) {"Loading JS file: #{absfile}"}
      js << IO.read(absfile, external_encoding: Encoding::UTF_8) << "\n"
    end
    logv(logger) {"Loading KaTeX JS file: #{katex_js}"}
    js << IO.read(katex_js, external_encoding: Encoding::UTF_8) << "\n"

    js_katex_opts = "var katex_opts = #{katex_opts.to_json}"
    logv(logger) {"JS eval: #{js_katex_opts}"}
    js << js_katex_opts << "\n"
  end
end

#katex_jsObject

The path to your copy of katex.min.js as specified by #config[ :katex_js ] or its default 'katex/katex.min.js'. For a relative path, the starting point is the current working directory.



332
333
334
# File 'lib/sskatex.rb', line 332

def katex_js
  @katex_js ||= @config[:katex_js] || DEFAULT_KATEX_JS
end

#katex_optsObject

A dictionary filled with the contents of #config[ :katex_opts ] if given. These are general KaTeX options such as throwOnError, errorColor, colorIsTextColor, and macros. See the KaTeX documentation for details. Use throwOnError: false if you want parse errors highlighted in the HTML output rather than raised as exceptions when compiling. Note that displayMode is computed dynamically and should not be specified here. Keys can be symbols or strings; if a key is given in both forms, the symbol will be ignored.



345
346
347
# File 'lib/sskatex.rb', line 345

def katex_opts
  @katex_opts ||= Utils.dedup_keys(@config[:katex_opts] || {})
end