mirror of https://github.com/zulip/zulip.git
markdown: Add TeX typesetting support.
Co-authored-by: Reid Barton <rwbarton@gmail.com> Fixes #2056.
This commit is contained in:
parent
caef5332d5
commit
34a9e1ae11
|
@ -114,7 +114,8 @@
|
||||||
"user_events": false,
|
"user_events": false,
|
||||||
"Plotly": false,
|
"Plotly": false,
|
||||||
"emoji_codes": false,
|
"emoji_codes": false,
|
||||||
"drafts": false
|
"drafts": false,
|
||||||
|
"katex": false
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-restricted-syntax": 0,
|
"no-restricted-syntax": 0,
|
||||||
|
|
|
@ -37,6 +37,7 @@ add_dependencies({
|
||||||
hash_util: 'js/hash_util',
|
hash_util: 'js/hash_util',
|
||||||
hashchange: 'js/hashchange',
|
hashchange: 'js/hashchange',
|
||||||
fenced_code: 'js/fenced_code.js',
|
fenced_code: 'js/fenced_code.js',
|
||||||
|
katex: 'node_modules/katex/dist/katex.min.js',
|
||||||
});
|
});
|
||||||
|
|
||||||
var doc = "";
|
var doc = "";
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"i18next-localstorage-cache": "0.3.0",
|
"i18next-localstorage-cache": "0.3.0",
|
||||||
"jquery": "1.12.1",
|
"jquery": "1.12.1",
|
||||||
"jquery-validation": "1.16.0",
|
"jquery-validation": "1.16.0",
|
||||||
|
"katex": "0.7.1",
|
||||||
"plotly.js": "1.19.2",
|
"plotly.js": "1.19.2",
|
||||||
"string.prototype.codepointat": "0.2.0",
|
"string.prototype.codepointat": "0.2.0",
|
||||||
"underscore": "1.8.3",
|
"underscore": "1.8.3",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -373,6 +373,14 @@ function handleRealmFilter(pattern, matches) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTex(tex, fullmatch) {
|
||||||
|
try {
|
||||||
|
return katex.renderToString(tex);
|
||||||
|
} catch (ex) {
|
||||||
|
return '<span class="tex-error">' + escape(fullmatch) + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function python_to_js_filter(pattern, url) {
|
function python_to_js_filter(pattern, url) {
|
||||||
// Converts a python named-group regex to a javascript-compatible numbered
|
// Converts a python named-group regex to a javascript-compatible numbered
|
||||||
// group regex... with a regex!
|
// group regex... with a regex!
|
||||||
|
@ -514,6 +522,7 @@ $(function () {
|
||||||
unicodeEmojiHandler: handleUnicodeEmoji,
|
unicodeEmojiHandler: handleUnicodeEmoji,
|
||||||
streamHandler: handleStream,
|
streamHandler: handleStream,
|
||||||
realmFilterHandler: handleRealmFilter,
|
realmFilterHandler: handleRealmFilter,
|
||||||
|
texHandler: handleTex,
|
||||||
renderer: r,
|
renderer: r,
|
||||||
preprocessors: [preprocess_code_blocks],
|
preprocessors: [preprocess_code_blocks],
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,6 +52,16 @@ function wrap_quote(text) {
|
||||||
return quoted_paragraphs.join('\n\n');
|
return quoted_paragraphs.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrap_tex(tex) {
|
||||||
|
try {
|
||||||
|
return katex.renderToString(tex, {
|
||||||
|
displayMode: true,
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
return '<span class="tex-error">' + escape_func(tex) + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.set_stash_func = function (stash_handler) {
|
exports.set_stash_func = function (stash_handler) {
|
||||||
stash_func = stash_handler;
|
stash_func = stash_handler;
|
||||||
};
|
};
|
||||||
|
@ -90,6 +100,28 @@ exports.process_fenced_code = function (content) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lang === 'math' || lang === 'tex' || lang === 'latex') {
|
||||||
|
return {
|
||||||
|
handle_line: function (line) {
|
||||||
|
if (line === fence) {
|
||||||
|
this.done();
|
||||||
|
} else {
|
||||||
|
consume_line(lines, line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
done: function () {
|
||||||
|
var text = wrap_tex(lines.join('\n'));
|
||||||
|
var placeholder = stash_func(text, true);
|
||||||
|
output_lines.push('');
|
||||||
|
output_lines.push(placeholder);
|
||||||
|
output_lines.push('');
|
||||||
|
handler_stack.pop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handle_line: function (line) {
|
handle_line: function (line) {
|
||||||
if (line === fence) {
|
if (line === fence) {
|
||||||
|
|
|
@ -1931,6 +1931,15 @@ div.floating_recipient {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.katex-html {
|
||||||
|
line-height: initial;
|
||||||
|
white-space: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tex-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Khan Academy
|
||||||
|
|
||||||
|
This software also uses portions of the underscore.js project, which is
|
||||||
|
MIT licensed with the following copyright:
|
||||||
|
|
||||||
|
Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
|
||||||
|
Reporters & Editors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Simple CLI for KaTeX.
|
||||||
|
// Reads TeX from stdin, outputs HTML to stdout.
|
||||||
|
|
||||||
|
let katex;
|
||||||
|
try {
|
||||||
|
// Attempt to import KaTeX from the production bundle
|
||||||
|
katex = require("../../serve/min/katex.js");
|
||||||
|
} catch (ex) {
|
||||||
|
// Import KaTeX from node_modules (development environment) otherwise
|
||||||
|
katex = require("../../node_modules/katex/katex.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = "";
|
||||||
|
|
||||||
|
// Skip the first two args, which are just "node" and "cli.js"
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.indexOf("--help") !== -1) {
|
||||||
|
console.log(process.argv[0] + " " + process.argv[1] +
|
||||||
|
" [ --help ]" +
|
||||||
|
" [ --display-mode ]");
|
||||||
|
|
||||||
|
console.log("\n" +
|
||||||
|
"Options:");
|
||||||
|
console.log(" --help Display this help message");
|
||||||
|
console.log(" --display-mode Render in display mode (not inline mode)");
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.on("data", function(chunk) {
|
||||||
|
input += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on("end", function() {
|
||||||
|
var options = { displayMode: args.indexOf("--display-mode") !== -1 };
|
||||||
|
var output = katex.renderToString(input, options);
|
||||||
|
console.log(output);
|
||||||
|
});
|
|
@ -479,9 +479,10 @@ var inline = {
|
||||||
usermention: noop,
|
usermention: noop,
|
||||||
stream: noop,
|
stream: noop,
|
||||||
avatar: noop,
|
avatar: noop,
|
||||||
|
tex: noop,
|
||||||
gravatar: noop,
|
gravatar: noop,
|
||||||
realm_filters: [],
|
realm_filters: [],
|
||||||
text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
|
text: /^[\s\S]+?(?=[\\<!\[_*`$]| {2,}\n|$)/
|
||||||
};
|
};
|
||||||
|
|
||||||
inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
|
inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
|
||||||
|
@ -541,6 +542,7 @@ inline.zulip = merge({}, inline.breaks, {
|
||||||
stream: /^#\*\*([^\*]+)\*\*/m,
|
stream: /^#\*\*([^\*]+)\*\*/m,
|
||||||
avatar: /^!avatar\(([^)]+)\)/,
|
avatar: /^!avatar\(([^)]+)\)/,
|
||||||
gravatar: /^!gravatar\(([^)]+)\)/,
|
gravatar: /^!gravatar\(([^)]+)\)/,
|
||||||
|
tex: /^(\$\$([^ _$](\\\$|[^$])*)(?! )\$\$)\B/,
|
||||||
realm_filters: [],
|
realm_filters: [],
|
||||||
text: replace(inline.breaks.text)
|
text: replace(inline.breaks.text)
|
||||||
('|', '|(\ud83c[\udf00-\udfff]|\ud83d[\udc00-\ude4f]|\ud83d[\ude80-\udeff]|[\u2600-\u26FF]|[\u2700-\u27BF])|')
|
('|', '|(\ud83c[\udf00-\udfff]|\ud83d[\udc00-\ude4f]|\ud83d[\ude80-\udeff]|[\u2600-\u26FF]|[\u2700-\u27BF])|')
|
||||||
|
@ -797,6 +799,13 @@ InlineLexer.prototype.output = function(src) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tex
|
||||||
|
if (cap = this.rules.tex.exec(src)) {
|
||||||
|
src = src.substring(cap[0].length);
|
||||||
|
out += this.tex(cap[2], cap[0]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// text
|
// text
|
||||||
if (cap = this.rules.text.exec(src)) {
|
if (cap = this.rules.text.exec(src)) {
|
||||||
src = src.substring(cap[0].length);
|
src = src.substring(cap[0].length);
|
||||||
|
@ -838,6 +847,12 @@ InlineLexer.prototype.unicodeEmoji = function (name) {
|
||||||
return this.options.unicodeEmojiHandler(name);
|
return this.options.unicodeEmojiHandler(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
InlineLexer.prototype.tex = function (tex, fullmatch) {
|
||||||
|
if (typeof this.options.texHandler !== 'function')
|
||||||
|
return fullmatch;
|
||||||
|
return this.options.texHandler(tex, fullmatch);
|
||||||
|
};
|
||||||
|
|
||||||
InlineLexer.prototype.userAvatar = function (email) {
|
InlineLexer.prototype.userAvatar = function (email) {
|
||||||
if (typeof this.options.avatarHandler !== 'function')
|
if (typeof this.options.avatarHandler !== 'function')
|
||||||
return '!avatar(' + email + ')';
|
return '!avatar(' + email + ')';
|
||||||
|
@ -850,7 +865,6 @@ InlineLexer.prototype.userGravatar = function (email) {
|
||||||
return this.options.avatarHandler(email);
|
return this.options.avatarHandler(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
InlineLexer.prototype.realm_filter = function (filter, matches, orig) {
|
InlineLexer.prototype.realm_filter = function (filter, matches, orig) {
|
||||||
if (typeof this.options.realmFilterHandler !== 'function')
|
if (typeof this.options.realmFilterHandler !== 'function')
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -143,3 +143,25 @@ a space before your phrase or submit it as a quote block by following
|
||||||
the code syntax highlighting format.
|
the code syntax highlighting format.
|
||||||
|
|
||||||
![Quotes](/static/images/help/quotes-screenshot.png)
|
![Quotes](/static/images/help/quotes-screenshot.png)
|
||||||
|
|
||||||
|
## TeX math
|
||||||
|
|
||||||
|
You can display mathematical symbols, expressions and equations using Zulip's
|
||||||
|
[TeX](http://www.tug.org/interest.html#doc) typesetting implementation,
|
||||||
|
based on [KaTeX](https://github.com/Khan/KaTeX).
|
||||||
|
|
||||||
|
!!! tip ""
|
||||||
|
Visit the [KaTeX Wiki](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX)
|
||||||
|
to view a complete of compatible commands.
|
||||||
|
|
||||||
|
Surround elements in valid TeX syntax with `$$two dollar signs$$` to display it
|
||||||
|
as inline content.
|
||||||
|
|
||||||
|
![Inline TeX](/static/images/help/inline-tex-screenshot.png)
|
||||||
|
|
||||||
|
Also, you can show expressions, such as expanded integrals, in TeX
|
||||||
|
*display mode* to present them fully-sized in the center of the messages by
|
||||||
|
fencing them with three back-ticks ` ``` ` or tildes `~~~`, with **math**,
|
||||||
|
**tex** or **latex** immediately after the first set of back-ticks.
|
||||||
|
|
||||||
|
![Display mode TeX](/static/images/help/display-mode-tex-screenshot.png)
|
||||||
|
|
|
@ -46,6 +46,22 @@ subprocess.check_call(['./tools/minify-js'] +
|
||||||
(['--prev-deploy', prev_deploy] if prev_deploy else []),
|
(['--prev-deploy', prev_deploy] if prev_deploy else []),
|
||||||
stdout=fp, stderr=fp)
|
stdout=fp, stderr=fp)
|
||||||
|
|
||||||
|
# Copy the KaTeX files outside node_modules
|
||||||
|
subprocess.check_call(['mkdir', '-p',
|
||||||
|
os.path.join(settings.STATIC_ROOT,
|
||||||
|
'node_modules/katex/dist/')],
|
||||||
|
stdout=fp, stderr=fp)
|
||||||
|
|
||||||
|
subprocess.check_call(['cp', 'node_modules/katex/dist/katex.css',
|
||||||
|
os.path.join(settings.STATIC_ROOT,
|
||||||
|
'node_modules/katex/dist/')],
|
||||||
|
stdout=fp, stderr=fp)
|
||||||
|
|
||||||
|
subprocess.check_call(['cp', '-R', 'node_modules/katex/dist/fonts',
|
||||||
|
os.path.join(settings.STATIC_ROOT,
|
||||||
|
'node_modules/katex/dist/fonts')],
|
||||||
|
stdout=fp, stderr=fp)
|
||||||
|
|
||||||
# Collect the files that we're going to serve; this creates prod-static/serve.
|
# Collect the files that we're going to serve; this creates prod-static/serve.
|
||||||
subprocess.check_call(['./manage.py', 'collectstatic', '--no-default-ignore',
|
subprocess.check_call(['./manage.py', 'collectstatic', '--no-default-ignore',
|
||||||
'--noinput', '-i', 'assets', '-i' 'node_modules'],
|
'--noinput', '-i', 'assets', '-i' 'node_modules'],
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
ZULIP_VERSION = "1.5.1+git"
|
ZULIP_VERSION = "1.5.1+git"
|
||||||
PROVISION_VERSION = '4.11'
|
PROVISION_VERSION = '4.12'
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
import subprocess
|
||||||
# Zulip's main markdown implementation. See docs/markdown.md for
|
# Zulip's main markdown implementation. See docs/markdown.md for
|
||||||
# detailed documentation on our markdown syntax.
|
# detailed documentation on our markdown syntax.
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Text, Tuple, TypeVar, Union
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Text, Tuple, TypeVar, Union
|
||||||
|
@ -40,7 +41,8 @@ from zerver.lib.url_preview import preview as link_preview
|
||||||
from zerver.models import Message, Realm, UserProfile, get_user_profile_by_email
|
from zerver.models import Message, Realm, UserProfile, get_user_profile_by_email
|
||||||
import zerver.lib.alert_words as alert_words
|
import zerver.lib.alert_words as alert_words
|
||||||
import zerver.lib.mention as mention
|
import zerver.lib.mention as mention
|
||||||
from zerver.lib.str_utils import force_text, force_str
|
from zerver.lib.str_utils import force_str, force_text
|
||||||
|
from zerver.lib.tex import render_tex
|
||||||
import six
|
import six
|
||||||
from six.moves import range, html_parser
|
from six.moves import range, html_parser
|
||||||
from typing import Text
|
from typing import Text
|
||||||
|
@ -762,6 +764,18 @@ class ModalLink(markdown.inlinepatterns.Pattern):
|
||||||
|
|
||||||
return a_tag
|
return a_tag
|
||||||
|
|
||||||
|
class Tex(markdown.inlinepatterns.Pattern):
|
||||||
|
def handleMatch(self, match):
|
||||||
|
# type: (Match[Text]) -> Element
|
||||||
|
rendered = render_tex(match.group('body'), is_inline=True)
|
||||||
|
if rendered is not None:
|
||||||
|
return etree.fromstring(rendered.encode('utf-8'))
|
||||||
|
else: # Something went wrong while rendering
|
||||||
|
span = markdown.util.etree.Element('span')
|
||||||
|
span.set('class', 'tex-error')
|
||||||
|
span.text = '$$' + match.group('body') + '$$'
|
||||||
|
return span
|
||||||
|
|
||||||
upload_title_re = re.compile(u"^(https?://[^/]*)?(/user_uploads/\\d+)(/[^/]*)?/[^/]*/(?P<filename>[^/]*)$")
|
upload_title_re = re.compile(u"^(https?://[^/]*)?(/user_uploads/\\d+)(/[^/]*)?/[^/]*/(?P<filename>[^/]*)$")
|
||||||
def url_filename(url):
|
def url_filename(url):
|
||||||
# type: (Text) -> Text
|
# type: (Text) -> Text
|
||||||
|
@ -1146,6 +1160,7 @@ class Bugdown(markdown.Extension):
|
||||||
\*\* # ends by double asterisks
|
\*\* # ends by double asterisks
|
||||||
"""
|
"""
|
||||||
md.inlinePatterns.add('stream', StreamPattern(stream_group), '>backtick')
|
md.inlinePatterns.add('stream', StreamPattern(stream_group), '>backtick')
|
||||||
|
md.inlinePatterns.add('tex', Tex(r'\B\$\$(?P<body>[^ _$](\\\$|[^$])*)(?! )\$\$\B'), '>backtick')
|
||||||
md.inlinePatterns.add('emoji', Emoji(r'(?P<syntax>:[\w\-\+]+:)'), '_end')
|
md.inlinePatterns.add('emoji', Emoji(r'(?P<syntax>:[\w\-\+]+:)'), '_end')
|
||||||
md.inlinePatterns.add('unicodeemoji', UnicodeEmoji(
|
md.inlinePatterns.add('unicodeemoji', UnicodeEmoji(
|
||||||
u'(?P<syntax>[\U0001F300-\U0001F64F\U0001F680-\U0001F6FF\u2600-\u26FF\u2700-\u27BF])'),
|
u'(?P<syntax>[\U0001F300-\U0001F64F\U0001F680-\U0001F6FF\u2600-\u26FF\u2700-\u27BF])'),
|
||||||
|
|
|
@ -62,8 +62,13 @@ Dependencies:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import markdown
|
import markdown
|
||||||
|
import six
|
||||||
|
from django.utils.html import escape
|
||||||
from markdown.extensions.codehilite import CodeHilite, CodeHiliteExtension
|
from markdown.extensions.codehilite import CodeHilite, CodeHiliteExtension
|
||||||
|
from zerver.lib.str_utils import force_bytes
|
||||||
|
from zerver.lib.tex import render_tex
|
||||||
from typing import Any, Dict, Iterable, List, MutableSequence, Optional, Tuple, Union, Text
|
from typing import Any, Dict, Iterable, List, MutableSequence, Optional, Tuple, Union, Text
|
||||||
|
|
||||||
# Global vars
|
# Global vars
|
||||||
|
@ -170,9 +175,37 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
|
||||||
# type: (MutableSequence[Text], Text, Text) -> BaseHandler
|
# type: (MutableSequence[Text], Text, Text) -> BaseHandler
|
||||||
if lang in ('quote', 'quoted'):
|
if lang in ('quote', 'quoted'):
|
||||||
return QuoteHandler(output, fence)
|
return QuoteHandler(output, fence)
|
||||||
|
elif lang in ('math', 'tex', 'latex'):
|
||||||
|
return TexHandler(output, fence)
|
||||||
else:
|
else:
|
||||||
return CodeHandler(output, fence, lang)
|
return CodeHandler(output, fence, lang)
|
||||||
|
|
||||||
|
class CodeHandler(BaseHandler):
|
||||||
|
def __init__(self, output, fence, lang):
|
||||||
|
# type: (MutableSequence[Text], Text, Text) -> None
|
||||||
|
self.output = output
|
||||||
|
self.fence = fence
|
||||||
|
self.lang = lang
|
||||||
|
self.lines = [] # type: List[Text]
|
||||||
|
|
||||||
|
def handle_line(self, line):
|
||||||
|
# type: (Text) -> None
|
||||||
|
if line.rstrip() == self.fence:
|
||||||
|
self.done()
|
||||||
|
else:
|
||||||
|
self.lines.append(line)
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
# type: () -> None
|
||||||
|
text = '\n'.join(self.lines)
|
||||||
|
text = processor.format_code(self.lang, text)
|
||||||
|
text = processor.placeholder(text)
|
||||||
|
processed_lines = text.split('\n')
|
||||||
|
self.output.append('')
|
||||||
|
self.output.extend(processed_lines)
|
||||||
|
self.output.append('')
|
||||||
|
pop()
|
||||||
|
|
||||||
class QuoteHandler(BaseHandler):
|
class QuoteHandler(BaseHandler):
|
||||||
def __init__(self, output, fence):
|
def __init__(self, output, fence):
|
||||||
# type: (MutableSequence[Text], Text) -> None
|
# type: (MutableSequence[Text], Text) -> None
|
||||||
|
@ -197,12 +230,11 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
|
||||||
self.output.append('')
|
self.output.append('')
|
||||||
pop()
|
pop()
|
||||||
|
|
||||||
class CodeHandler(BaseHandler):
|
class TexHandler(BaseHandler):
|
||||||
def __init__(self, output, fence, lang):
|
def __init__(self, output, fence):
|
||||||
# type: (MutableSequence[Text], Text, Text) -> None
|
# type: (MutableSequence[Text], Text) -> None
|
||||||
self.output = output
|
self.output = output
|
||||||
self.fence = fence
|
self.fence = fence
|
||||||
self.lang = lang
|
|
||||||
self.lines = [] # type: List[Text]
|
self.lines = [] # type: List[Text]
|
||||||
|
|
||||||
def handle_line(self, line):
|
def handle_line(self, line):
|
||||||
|
@ -210,12 +242,12 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
|
||||||
if line.rstrip() == self.fence:
|
if line.rstrip() == self.fence:
|
||||||
self.done()
|
self.done()
|
||||||
else:
|
else:
|
||||||
self.lines.append(line)
|
check_for_new_fence(self.lines, line)
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
text = '\n'.join(self.lines)
|
text = '\n'.join(self.lines)
|
||||||
text = processor.format_code(self.lang, text)
|
text = processor.format_tex(text)
|
||||||
text = processor.placeholder(text)
|
text = processor.placeholder(text)
|
||||||
processed_lines = text.split('\n')
|
processed_lines = text.split('\n')
|
||||||
self.output.append('')
|
self.output.append('')
|
||||||
|
@ -282,6 +314,19 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
|
||||||
quoted_paragraphs.append("\n".join("> " + line for line in lines if line != ''))
|
quoted_paragraphs.append("\n".join("> " + line for line in lines if line != ''))
|
||||||
return "\n\n".join(quoted_paragraphs)
|
return "\n\n".join(quoted_paragraphs)
|
||||||
|
|
||||||
|
def format_tex(self, text):
|
||||||
|
# type: (Text) -> Text
|
||||||
|
paragraphs = text.split("\n\n")
|
||||||
|
tex_paragraphs = []
|
||||||
|
for paragraph in paragraphs:
|
||||||
|
html = render_tex(paragraph, is_inline=False)
|
||||||
|
if html is not None:
|
||||||
|
tex_paragraphs.append(html)
|
||||||
|
else:
|
||||||
|
tex_paragraphs.append('<span class="tex-error">' +
|
||||||
|
escape(paragraph) + '</span>')
|
||||||
|
return "\n\n".join(tex_paragraphs)
|
||||||
|
|
||||||
def placeholder(self, code):
|
def placeholder(self, code):
|
||||||
# type: (Text) -> Text
|
# type: (Text) -> Text
|
||||||
return self.markdown.htmlStash.store(code, safe=True)
|
return self.markdown.htmlStash.store(code, safe=True)
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from django.conf import settings
|
||||||
|
from typing import Text
|
||||||
|
from zerver.lib.str_utils import force_bytes
|
||||||
|
|
||||||
|
def render_tex(tex, is_inline=True):
|
||||||
|
# type: (Text, bool) -> Text
|
||||||
|
"""Render a TeX string into HTML using KaTeX
|
||||||
|
|
||||||
|
Returns the HTML string, or None if there was some error in the TeX syntax
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
tex -- Text string with the TeX to render
|
||||||
|
Don't include delimiters ('$$', '\[ \]', etc.)
|
||||||
|
is_inline -- Boolean setting that indicates whether the render should be
|
||||||
|
inline (i.e. for embedding it in text) or not. The latter
|
||||||
|
will show the content centered, and in the "expanded" form
|
||||||
|
(default True)
|
||||||
|
"""
|
||||||
|
|
||||||
|
katex_path = os.path.join(settings.STATIC_ROOT, 'third/katex/cli.js')
|
||||||
|
if not os.path.isfile(katex_path):
|
||||||
|
logging.error("Cannot find KaTeX for latex rendering!")
|
||||||
|
return None
|
||||||
|
command = ['node', katex_path]
|
||||||
|
if not is_inline:
|
||||||
|
command.extend(['--', '--display-mode'])
|
||||||
|
katex = subprocess.Popen(command,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
stdout = katex.communicate(input=force_bytes(tex))[0]
|
||||||
|
if katex.returncode == 0:
|
||||||
|
# stdout contains a newline at the end
|
||||||
|
return stdout.decode('utf-8').strip()
|
||||||
|
else:
|
||||||
|
return None
|
|
@ -715,6 +715,7 @@ PIPELINE = {
|
||||||
'third/bootstrap-notify/css/bootstrap-notify.css',
|
'third/bootstrap-notify/css/bootstrap-notify.css',
|
||||||
'third/spectrum/spectrum.css',
|
'third/spectrum/spectrum.css',
|
||||||
'third/jquery-perfect-scrollbar/css/perfect-scrollbar.css',
|
'third/jquery-perfect-scrollbar/css/perfect-scrollbar.css',
|
||||||
|
'node_modules/katex/dist/katex.css',
|
||||||
'styles/components.css',
|
'styles/components.css',
|
||||||
'styles/zulip.css',
|
'styles/zulip.css',
|
||||||
'styles/settings.css',
|
'styles/settings.css',
|
||||||
|
@ -927,6 +928,12 @@ JS_SPECS = {
|
||||||
'source_filenames': ['third/sockjs/sockjs-0.3.4.js'],
|
'source_filenames': ['third/sockjs/sockjs-0.3.4.js'],
|
||||||
'output_filename': 'min/sockjs-0.3.4.min.js'
|
'output_filename': 'min/sockjs-0.3.4.min.js'
|
||||||
},
|
},
|
||||||
|
'katex': {
|
||||||
|
'source_filenames': [
|
||||||
|
'node_modules/katex/dist/katex.js',
|
||||||
|
],
|
||||||
|
'output_filename': 'min/katex.js'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if PIPELINE_ENABLED:
|
if PIPELINE_ENABLED:
|
||||||
|
|
Loading…
Reference in New Issue