New math engine SsKaTeX #455
Conversation
On branch de.1tein.ccorn.sskatex.execjs, I have prepared an ExecJS-based version. This might enable SsKaTeX use under JRuby which cannot install the Duktape gem. It might be interesting to test this with the CI's Node.js installation as well as with other engine bindings like In case of interest, I could merge that branch here, or create another pull request, although that sounds like a bad idea to me. The Caveat still applies: The engine flexibility of ExecJS increases the likelihood of malfunctioning configurations and corresponding ticket filings. I'd rather prefer something specific that is known to work. |
By the way, private functions of SsKaTeX are documented too. |
Thank you for you pull request! I'm currently rather busy so I had just a quick look - here are some of the more obvious things I noticed:
As for duktape vs execjs: I don't know either but if duktape is self-contained, with less dependencies, I would go for it. I will have a more in-depth look when I have some time in the coming weeks. |
New commits addressing the following:
I am used to contributing diffs, not entire branches, so I knew I would mess this up:
Not sure how to repair this without dents in the Github workflow. Rebasing seems out of the question because this branch is already public. I could transfer the changes summarily to a new branch, close this pull request and start a new one.
Installing Duktape needs not much more than a C99 compiler and a version of Ruby that can load native object files. Even my old PowerMac G5 can install Duktape (as opposed to V8, Node.js, and all that). However, your CI has demonstrated that JRuby cannot install Duktape. With ExecJS that should not be an issue. It would even use Node.js, so your CI tests should pass without additional gems even for JRuby (not tested). ExecJS is described as being able to use other engines such as There should be a way to preempt ExecJS's automatic JS engine selection on startup and make it user-configurable instead. This way, users could stick to a configuration known to work. Until that is the case with ExecJS, I'd favor Duktape. |
No, that's not necessary. Just work on your branch but rebase, squash, etc. and then force push. After all: This is just a branch in your repo and although public there is no need to be conservative in this case. So please don't open a new pull request - thanks! |
Radically squashed now. The CI failed, but that is |
Due to repeated rebasing and squashing, there are some commits referenced here that are no longer part of a named branch. I have deleted them locally (their SHA1 ids no longer exist), but Github keeps them even with
Mission accomplished: No dents in the workflow (but noise instead). |
I find that the engine selection (and the associated availability tests) by ExecJS can be overridden with an environment variable ENV['EXECJS_RUNTIME'] ||= 'Disabled'
require 'execjs'
[...]
ExecJS.runtime = ExecJS::Runtimes::Duktape This seems to encourage an ExecJS-based approach. |
I have now merged an The results are encouraging. For better control, I have introduced an additional option for Note that there is no explicit dependency on |
I have done a performance comparison of What I expected was that
Edit: After inspection of the Edit: After adding the same optimization to the |
Thanks for your pull request! I have had time today to review your changes and provide some feedback. |
@@ -1,3 +1,4 @@ | |||
htmldoc | |||
katex |
gettalong
Oct 30, 2017
Owner
Why is this line here?
Why is this line here?
ccorn
Oct 31, 2017
Author
Contributor
The intention was to avoid recursive file listings by git status
when katex.tar.gz
has been downloaded und unpacked to get SsKaTeX covered in the tests. Turns out that git status
lists only katex
if that line is missing, no matter how big the tree below that is (in contrast to e.g. monotone's mtn list unknown
), so I'll remove it.
The intention was to avoid recursive file listings by git status
when katex.tar.gz
has been downloaded und unpacked to get SsKaTeX covered in the tests. Turns out that git status
lists only katex
if that line is missing, no matter how big the tree below that is (in contrast to e.g. monotone's mtn list unknown
), so I'll remove it.
@@ -0,0 +1,8 @@ | |||
#! /bin/bash |
gettalong
Oct 30, 2017
Owner
Please remove this file, it is not a binary in the sense that it needs to be available to the end user.
Please remove this file, it is not a binary in the sense that it needs to be available to the end user.
ccorn
Oct 31, 2017
Author
Contributor
It is a developer tool, and therefore it is not installed. It is used when updating KaTeX which tends to vary the generated HTML+MathML with each version. I'd move it to another subdirectory, if you find bin
misleading. tools
perhaps? Or the existing test
? Or test/bin
?
It is a developer tool, and therefore it is not installed. It is used when updating KaTeX which tends to vary the generated HTML+MathML with each version. I'd move it to another subdirectory, if you find bin
misleading. tools
perhaps? Or the existing test
? Or test/bin
?
gettalong
Oct 31, 2017
Owner
The bin/
directory is for application binaries only, so not development tools. Best way would be to incorporate this into the Rakefile under the dev
namespace.
The bin/
directory is for application binaries only, so not development tools. Best way would be to incorporate this into the Rakefile under the dev
namespace.
@@ -0,0 +1,31 @@ | |||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill |
gettalong
Oct 30, 2017
Owner
This file can only be included in kramdown if its license is compatible. Please make sure that it is, include the necessary statements as comment in this file and also place a note in COPYING.
This file can only be included in kramdown if its license is compatible. Please make sure that it is, include the necessary statements as comment in this file and also place a note in COPYING.
gettalong
Oct 30, 2017
Owner
Same procedure for escape_nonascii_html.js
unless this is your code.
Same procedure for escape_nonascii_html.js
unless this is your code.
ccorn
Oct 31, 2017
Author
Contributor
Will do. The polyfill for object_assign.js
is from MDN and seems to be covered by this copyright spec. The history of the MDN entry dates it to 2014 or later. Therefore the following paragraph seems to apply:
Code samples added on or after August 20, 2010 are in the public domain. No licensing notice is necessary, but if you need one, you can use: "Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/".
The code for escape_nonascii_html.js
is by me.
Will do. The polyfill for object_assign.js
is from MDN and seems to be covered by this copyright spec. The history of the MDN entry dates it to 2014 or later. Therefore the following paragraph seems to apply:
Code samples added on or after August 20, 2010 are in the public domain. No licensing notice is necessary, but if you need one, you can use: "Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/".
The code for escape_nonascii_html.js
is by me.
gettalong
Oct 31, 2017
Owner
Thanks for investigating! Then please add the information that this file is in the public domain to the COPYING file.
Thanks for investigating! Then please add the information that this file is in the public domain to the COPYING file.
ccorn
Nov 3, 2017
Author
Contributor
Turns out that we can rewrite tex_to_html.js
such that object_assign.js
is not needed and the resulting Javascript looks slightly better.
Turns out that we can rewrite tex_to_html.js
such that object_assign.js
is not needed and the resulting Javascript looks slightly better.
and [SsKaTeX](../math_engine/sskatex.html). | ||
Each one requires a Javascript engine installed where kramdown runs, | ||
in order to perform the precompilation. | ||
The resulting pages still require CSS and fonts, but no Javascript anymore. | ||
|
gettalong
Oct 30, 2017
Owner
Please wrap the paragraphs with a line length of 100 characters.
Please wrap the paragraphs with a line length of 100 characters.
gettalong
Oct 30, 2017
Owner
Same for doc/math_engine/sskatex.page
.
Same for doc/math_engine/sskatex.page
.
A typical SsKaTeX configuration looks like this: | ||
|
||
~~~ yaml | ||
math_engine: sskatex |
gettalong
Oct 30, 2017
Owner
Why does the line start with two spaces?
Why does the line start with two spaces?
ccorn
Oct 31, 2017
•
Author
Contributor
So that one can paste it below an unindented math_engine_opts:
. As this seems to be non-obvious, I'll unindent the fragments.
Update: Not math_engine_opts:
here, but the kramdown:
in Jekyll's _config.yml
. Yet another reason for unindenting.
So that one can paste it below an unindented math_engine_opts:
. As this seems to be non-obvious, I'll unindent the fragments.
Update: Not math_engine_opts:
here, but the kramdown:
in Jekyll's _config.yml
. Yet another reason for unindenting.
gettalong
Oct 31, 2017
Owner
Ah 😄 Indeed, I hadn't thought of that. I agree, it is better to unindent the fragments.
Ah
# Given a +math_engine_opts+ dictionary, | ||
# return a new JS engine context, initialized with the JS helper files, | ||
# and with a JS object +katexopts+ containing general KaTeX options. | ||
def newcontext(config) |
gettalong
Oct 30, 2017
Owner
Should be called new_context
.
Should be called new_context
.
'Disabled').to_s.to_sym | ||
ExecJS.runtime = JSRUN_FROMSYM[jsrun] | ||
if config[:verbose] | ||
warn "Available JS runtimes: #{jsruns_available.join(', ')}" |
gettalong
Oct 30, 2017
Owner
Don't use warn, use converter.warning
.
Don't use warn, use converter.warning
.
ccorn
Oct 31, 2017
Author
Contributor
Seems better (will try), unless such use implies actual warning semantics. The use by SsKaTeX is rather meant for logging under the verbose
or debug
options.
Seems better (will try), unless such use implies actual warning semantics. The use by SsKaTeX is rather meant for logging under the verbose
or debug
options.
gettalong
Nov 2, 2017
Owner
Yeah, I get that. However, if one consciously enables verbose output it is okay to use the warning facility. This way the output will be correctly shown by whatever tool uses kramdown.
As for debug
: That's different since it should only ever be used for development purposes. So using warn
would be fine by me.
Yeah, I get that. However, if one consciously enables verbose output it is okay to use the warning facility. This way the output will be correctly shown by whatever tool uses kramdown.
As for debug
: That's different since it should only ever be used for development purposes. So using warn
would be fine by me.
ccorn
Nov 3, 2017
Author
Contributor
I have now tried using converter.warning
for the verbose
option. For that, I had to reorganize SsKaTeX's private interfaces a bit (passing converter
instead of just config
). Unfortunately, a jekyll build
now does not output those messages at all. It seems I have to go back to warn
directly.
However, I'd keep the changed interfaces, just in case we need converter.warning
for other things.
I have now tried using converter.warning
for the verbose
option. For that, I had to reorganize SsKaTeX's private interfaces a bit (passing converter
instead of just config
). Unfortunately, a jekyll build
now does not output those messages at all. It seems I have to go back to warn
directly.
However, I'd keep the changed interfaces, just in case we need converter.warning
for other things.
|
||
# Return a properly initialized JS engine context, | ||
# depending on the given _config_. Cache its reference for reuse. | ||
def jscontext(config) |
gettalong
Oct 30, 2017
Owner
Should be called js_context
.
Should be called js_context
.
|
||
# This should really be provided by +ExecJS::Runtimes+: | ||
# A list of available JS engines, as symbols, in the order of preference. | ||
def jsruns_available |
gettalong
Oct 30, 2017
Owner
Shouldn't this be called available_jsruns
since it returns the available JS runtimes?
Shouldn't this be called available_jsruns
since it returns the available JS runtimes?
# Return a properly initialized JS engine context, | ||
# depending on the given _config_. Cache its reference for reuse. | ||
def jscontext(config) | ||
JSCTX[config] ||= newcontext(config) |
gettalong
Oct 30, 2017
Owner
I think that this could potentially be used for a denial of service attack since if kramdown is used by server application and parses client input, a client could modify math_engine_opts
continuously to create new entries here.
How much memory does one entry take? How much time overhead is this compilation step?
I think that this could potentially be used for a denial of service attack since if kramdown is used by server application and parses client input, a client could modify math_engine_opts
continuously to create new entries here.
How much memory does one entry take? How much time overhead is this compilation step?
ccorn
Oct 31, 2017
•
Author
Contributor
Caching the context is necessary because the MathEngine API does not seem to provide a method for just the engine initialization and processing of options. I have no figures yet, but I remember that the speedup compared to re-initialization for each formula was substantial enough that I would not go back.
In a typical Jekyll setup, kramdown: math_engine_opts:
are configured globally (in _config.yaml
), so the context initialization gets done only once per jekyll build
(or jekyll serve
). Therefore it is shared across all kramdown source files, which is particularly beneficial for large sites.
Regarding overall security: If the user controls part of the filesystem (e.g. can upload files) and can tweak math_engine_opts
without restriction, the user could point katexjs
(or any of the libs
) to a file containing a definition of katex.renderToString
(or our entry function tex_to_html
) with an endless loop or more nefarious intent. The Javascript in there could do anything, with the privileges of the kramdown process. So, unless user-tweakable kramdown options can be restricted for such a service, the gates to hell are open. What threat model do we have?
Attack scenarios aside: Indeed a long-running process should have a way of forgetting no-longer used configs, otherwise memory usage would be unbounded.
A minimalistic approach would be to cache only the last used config. This already works for Jekyll.
A better approach would be to clear the context cache when it takes more than a prescribed limit of memory. I'd work in that direction.
Caching the context is necessary because the MathEngine API does not seem to provide a method for just the engine initialization and processing of options. I have no figures yet, but I remember that the speedup compared to re-initialization for each formula was substantial enough that I would not go back.
In a typical Jekyll setup, kramdown: math_engine_opts:
are configured globally (in _config.yaml
), so the context initialization gets done only once per jekyll build
(or jekyll serve
). Therefore it is shared across all kramdown source files, which is particularly beneficial for large sites.
Regarding overall security: If the user controls part of the filesystem (e.g. can upload files) and can tweak math_engine_opts
without restriction, the user could point katexjs
(or any of the libs
) to a file containing a definition of katex.renderToString
(or our entry function tex_to_html
) with an endless loop or more nefarious intent. The Javascript in there could do anything, with the privileges of the kramdown process. So, unless user-tweakable kramdown options can be restricted for such a service, the gates to hell are open. What threat model do we have?
Attack scenarios aside: Indeed a long-running process should have a way of forgetting no-longer used configs, otherwise memory usage would be unbounded.
A minimalistic approach would be to cache only the last used config. This already works for Jekyll.
A better approach would be to clear the context cache when it takes more than a prescribed limit of memory. I'd work in that direction.
ccorn
Oct 31, 2017
•
Author
Contributor
Some benchmarks for a small site with 178 formulae, JS engine Duktape and default options:
- 0.41s for
newcontext
- avg 0.018s per
compile_tex_math
(excluding jscontext
)
- Many inline formulae take less than 3ms.
Therefore, caching at least one context is necessary.
Some benchmarks for a small site with 178 formulae, JS engine Duktape and default options:
- 0.41s for
newcontext
- avg 0.018s per
compile_tex_math
(excludingjscontext
) - Many inline formulae take less than 3ms.
Therefore, caching at least one context is necessary.
ccorn
Nov 1, 2017
Author
Contributor
It occurs to me that recursive computation of context object sizes would be mostly unnecessary overhead. Instead, the configuration cache could be cleared if the number of its key-value pairs is about to exceed some maximum, say 10.
I'd even make that limit configurable via math_engine_opts
because not doing so does not really improve security as long as users can (and need to) specify arbitrary katexjs
.
It occurs to me that recursive computation of context object sizes would be mostly unnecessary overhead. Instead, the configuration cache could be cleared if the number of its key-value pairs is about to exceed some maximum, say 10.
I'd even make that limit configurable via math_engine_opts
because not doing so does not really improve security as long as users can (and need to) specify arbitrary katexjs
.
gettalong
Nov 2, 2017
Owner
Thanks for investigating!
Since caching is necessary, instead of simple hash, it would probably be better than to use an LRU data structure, like this https://github.com/gettalong/hexapdf/blob/master/lib/hexapdf/utils/lru_cache.rb.
As for making the number of cached items configurable: Since the math_engine_opts
can change with every input document, I don't see the use of making this configurable. I'd say that caching the last 10 context objects would just be fine.
Thanks for investigating!
Since caching is necessary, instead of simple hash, it would probably be better than to use an LRU data structure, like this https://github.com/gettalong/hexapdf/blob/master/lib/hexapdf/utils/lru_cache.rb.
As for making the number of cached items configurable: Since the math_engine_opts
can change with every input document, I don't see the use of making this configurable. I'd say that caching the last 10 context objects would just be fine.
ccorn
Nov 2, 2017
•
Author
Contributor
Since caching is necessary, instead of simple hash, it would probably be better than to use an LRU data structure, like this https://github.com/gettalong/hexapdf/blob/master/lib/hexapdf/utils/lru_cache.rb.
Cool. That one is Affero GPL, so I suppose that code needs a separate notice in COPYING, unless you, the author, decide otherwise for kramdown. (I'd rather copy that module instead of adding a dependency on hexapdf.)
Since caching is necessary, instead of simple hash, it would probably be better than to use an LRU data structure, like this https://github.com/gettalong/hexapdf/blob/master/lib/hexapdf/utils/lru_cache.rb.
Cool. That one is Affero GPL, so I suppose that code needs a separate notice in COPYING, unless you, the author, decide otherwise for kramdown. (I'd rather copy that module instead of adding a dependency on hexapdf.)
ccorn
Nov 2, 2017
Author
Contributor
I don't see the use of making this configurable. I'd say that caching the last 10 context objects would just be fine.
Agreed because making that configurable exposes an implementation detail which we may want to change without having to change the user documentation.
I don't see the use of making this configurable. I'd say that caching the last 10 context objects would just be fine.
Agreed because making that configurable exposes an implementation detail which we may want to change without having to change the user documentation.
I have committed changes according to the above review and discussion. These changes have now been squashed to two commits:
|
The CI failed again, not due to SsKaTeX but apparently because of mathjax-node resp. its dependencies becoming ever more demanding. |
With those changes (using |
I so regret having this pile of [use your imagination here, you may use "reprocessed food stuff"] called NodeJS as a test dependency... I have used your instructions to update the mathjax-node-cli integration, lights are green again on the master branch. Thank you very much for your latest work!! I have looked through the files and only have two remaining requests:
I will then merge your changes and release a new version next week. |
I forgot:
|
|
The name SsKaTeX means server-side KaTeX, as opposed to client-side KaTeX. It eliminates the need (and the flexibility, and the overhead) for client-side math-rendering Javascript. Consider this a lightweight alternative to Mathjax-Node. SsKaTeX uses KaTeX's katex.min.js (not included) instead of MathJax and uses a configurable or auto-selected JS engine (via ExecJS), e.g. Duktape, rather than depending strictly on Node.js. Javascript execution context initialization is done only once. With some JS engines, no inter-process communication is needed because the Javascript is handled by library functions. As a result, the performance is reasonable.
Thanks for your changes! I have cherry-picked your commit and modified it a bit:
The changes are already on Github and Travis is green. Thanks again for your contribution! |
The removed task is (still) a dependency for the remaining Rakefile task: task update_katex_tests: [:test_katexjs] I added it to have something that shows developers how to actually get KaTeX. (They should not need to peek into
I omitted that deliberately because the availability tests for
As you like. The intent was to make clear that the constants are not considered part of a stable interface and might disappear in the future, so other modules should better not rely on them. If in doubt, I make implementation details private.
Ouch. Awfully sorry for that. Your fix gives the intended semantics. Thanks. |
I might add that there is a subtlety with ExecJS runtime availability. The |
To summarize, I'd propose the following conservative changes, although I understand the motivation for the current state:
diff --git a/Rakefile b/Rakefile
index 44e22d3..d30fbf3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -267,6 +267,18 @@ EOF
puts "Look through the above mentioned files and correct all problems" if inserted
end
+ desc "Check for KaTeX availability"
+ task :test_katexjs do
+ katexjs = 'katex/katex.min.js'
+ raise (<<TKJ) unless File.exists? katexjs
+Cannot find file '#{katexjs}'.
+You need to download KaTeX e.g. from https://github.com/Khan/KaTeX/releases/
+and extract at least '#{katexjs}'.
+Alternatively, if you have a copy of KaTeX unpacked somewhere else,
+you can create a symbolic link 'katex' pointing to that KaTeX directory.
+TKJ
+ end
+
desc "Update kramdown SsKaTeX test reference outputs"
task update_katex_tests: [:test_katexjs] do
# Not framed in terms of rake file tasks to prevent accidental overwrites.
diff --git a/lib/kramdown/converter/math_engine/sskatex.rb b/lib/kramdown/converter/math_engine/sskatex.rb
index bb30dfd..0e5a98f 100644
--- a/lib/kramdown/converter/math_engine/sskatex.rb
+++ b/lib/kramdown/converter/math_engine/sskatex.rb
@@ -25,7 +25,8 @@ module Kramdown::Converter::MathEngine
require 'json'
ENV['EXECJS_RUNTIME'] = 'Disabled' # Defer automatic JS engine selection
require 'execjs'
- ExecJS::Runtimes.runtimes.select(&:available?).size > 0
+ # No test for any JS engine availability here; specifics are config-dependent anyway
+ true
rescue LoadError
false
ensure |
Thanks for your comments. I agree with the second point concerning the availability of the runtimes. And thanks for pointing out the dangling task dependency. However, the removed task does only check the availability of the katex JS file. Even if that is available but execjs is not, it will provide an invalid output. Since only those who who develop kramdown will make use of this task, they will know how to install the required dependencies. Additionally, there is a remark in the README file that points to the travis configuration file for information on regarding the installation of the dependencies. So I'm inclined to not include that KaTeX file check. |
Thanks for the feedback. Since it is important that KaTeX availability is properly checked when updating the reference outputs, let me improve the test dependency so that it actually shows whether SsKaTeX can be used. I have renamed it to diff --git a/Rakefile b/Rakefile
index 44e22d3..e36ffd1 100644
--- a/Rakefile
+++ b/Rakefile
@@ -267,8 +267,26 @@ EOF
puts "Look through the above mentioned files and correct all problems" if inserted
end
+ desc "Check for SsKaTeX availability"
+ task :test_sskatex_deps do
+ katexjs = 'katex/katex.min.js'
+ raise (<<TKJ) unless File.exists? katexjs
+Cannot find file '#{katexjs}'.
+You need to download KaTeX e.g. from https://github.com/Khan/KaTeX/releases/
+and extract at least '#{katexjs}'.
+Alternatively, if you have a copy of KaTeX unpacked somewhere else,
+you can create a symbolic link 'katex' pointing to that KaTeX directory.
+TKJ
+ html = %x{echo '$$a$$' | #{RbConfig.ruby} -Ilib bin/kramdown --math-engine sskatex}
+ raise (<<XJS) unless / class="katex"/ === html
+Some static dependency of SsKaTeX, probably the 'execjs' gem, is not available.
+If you 'gem install execjs', also make sure that some JS engine is available,
+e.g. by installing one of the gems 'duktape', 'therubyracer', or 'therubyrhino'.
+XJS
+ end
+
desc "Update kramdown SsKaTeX test reference outputs"
- task update_katex_tests: [:test_katexjs] do
+ task update_katex_tests: [:test_sskatex_deps] do
# Not framed in terms of rake file tasks to prevent accidental overwrites.
stems = ['test/testcases/block/15_math/sskatex',
'test/testcases/span/math/sskatex'] NB: Herein, multi-line output strings have been formatted to 80 characters per line because those go to the terminal. |
Proposed diffs now available as commits on this freshly rebased branch |
Introduction
The branch
de.1tein.ccorn.sskatex
contains a new math-to-HTML/CSS/MathML engine: SsKaTeX, meaning server-side KaTeX, as opposed to client-side KaTeX. It eliminates the need (and the flexibility, and the overhead) for client-side math-rendering Javascript.Consider this a lightweight alternative to Mathjax-Node. SsKaTeX uses KaTeX's
katex.min.js
(not included), interpreted by theDuktape
JS engine instead of MathJax and Node.js.In contrast to Mathjax-Node, you do not need a NodeJS installation (whose dependency V8 is unavailable for my
apple-darwin9-powerpc64
platform), and no inter-process communication is needed because the JS is handled by the Duktape library via the Ruby bindings provided by theduktape
gem. Javascript execution context initialization is done only once. As a result, the performance is reasonable.What you need
On the kramdown side:
Duktape
,katex.min.js
from KaTeX.All these requirements need to be met only if SsKaTeX is actually used.
SsKaTeX quickstart configuration:
In the HTML templates:
On the actual server:
All that KaTeX stuff is conveniently packaged in a tarball available at the KaTeX releases page.
There are some more configuration options; for the details I refer you to the documentation.
Things done
rake doc
), both for sources (RDoc) and for Webgen.rake test
likes them..travis.yml
updated, so your CI should install theduktape
gem and downloadkatex.min.js
into akatex
folder and run the tests automatically.Things to consider
Why not use
execjs
instead of hard-codingduktape
? Becauseduktape
is lightweight and known to work (currently);execjs
could accidentally break your kramdown processing once it finds another shiny Javascript interpreter that happens not to work with KaTeX.However, if there are downsides to this approach, replacing
duktape
withexecjs
should be straightforward, as the usage is similar.Encoding:
SsKaTeX::call(converter, el, opts)
theel.value
is encoded in UTF-8. Otherwise, an extra.encode('UTF-8')
might be necessary.jsquote
.Semantics:
SsKaTeX::AVAILABLE
constant being true is not sufficient for availability: It mostly checks for the presence ofduktape
. One would also have to test for the presence ofkatex.min.js
, but its location is configuration-dependent, and the module does not (seem to) have access to such configuration at initialization time.Looking forward to your feedback.