Skip to content
Snippets Groups Projects
Commit 2d170a20 authored by Munken's avatar Munken
Browse files

Render math in Asciidoc and Markdown with KaTeX using code blocks

parent e3f5c4c5
No related branches found
No related tags found
No related merge requests found
Showing
with 358 additions and 87 deletions
Loading
Loading
@@ -57,7 +57,7 @@
content: this.editor.getValue()
}, function(response) {
currentPane.empty().append(response);
return currentPane.syntaxHighlight();
return currentPane.renderGFM();
});
} else {
this.$toggleButton.show();
Loading
Loading
Loading
Loading
@@ -305,7 +305,7 @@
}
row = form.closest("tr");
note_html = $(note.html);
note_html.syntaxHighlight();
note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
Loading
Loading
@@ -322,7 +322,7 @@
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
$('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
$('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
// append new note to all matching discussions
Loading
Loading
@@ -463,7 +463,7 @@
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $html));
$html.syntaxHighlight();
$html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id);
Loading
Loading
Loading
Loading
@@ -27,7 +27,7 @@
return this.renderMarkdown(mdText, (function(_this) {
return function(response) {
preview.html(response.body);
preview.syntaxHighlight();
preview.renderGFM();
return _this.renderReferencedUsers(response.references.users, form);
};
})(this));
Loading
Loading
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
//
(function() {
$.fn.renderGFM = function() {
this.find('.js-syntax-highlight').syntaxHighlight();
this.find('.js-render-math').renderMath();
};
$(document).on('ready page:load', function() {
return $('body').renderGFM();
});
}).call(this);
/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
// ### Example Markup
//
// <code class="js-render-math"></div>
//
(function() {
// Only load once
var katexLoaded = false;
// Loop over all math elements and render math
var renderWithKaTeX = function (elements) {
elements.each(function () {
var mathNode = $('<span></span>');
var $this = $(this);
var display = $this.attr('data-math-style') === 'display';
try {
katex.render($this.text(), mathNode.get(0), { displayMode: display });
mathNode.insertAfter($this);
$this.remove();
} catch (err) {
// What can we do??
console.log(err.message);
}
});
};
$.fn.renderMath = function() {
var $this = this;
if ($this.length === 0) return;
if (katexLoaded) renderWithKaTeX($this);
else {
// Request CSS file so it is in the cache
$.get(gon.katex_css_url, function() {
var css = $('<link>',
{ rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
});
css.appendTo('head');
// Load KaTeX js
$.getScript(gon.katex_js_url, function() {
katexLoaded = true;
renderWithKaTeX($this); // Run KaTeX
});
});
}
};
}).call(this);
Loading
Loading
@@ -9,57 +9,10 @@
// <div class="js-syntax-highlight"></div>
//
(function() {
// CSS and JS for KaTeX
CSS_PATH = "<%= asset_path('katex.css') %>";
JS_PATH = "<%= asset_path('katex.js') %>";
// Only load once
var katexLoaded = false;
// Loop over all math elements and render math
var renderWithKaTeX = function (elements) {
elements.each(function () {
if (!!$(this).attr('rendered')) return;
$(this).attr('rendered', true);
$(this).hide();
var mathNode = $( "<math>Test</math>" );
mathNode.insertAfter($(this));
var display = $(this).hasClass('highlight');
katex.render($(this).text(), mathNode.get(0), { displayMode: display })
})
};
var handleMath = function () {
var mathElements = $('.code.math');
if (mathElements.length == 0) return;
if (katexLoaded) renderWithKaTeX(mathElements);
else {
// Request CSS file so it is in the cache
$.get(CSS_PATH, function(){
var css = $('<link>',
{rel:'stylesheet',
type:'text/css',
href: CSS_PATH
});
css.appendTo('head');
// Load KaTeX js
$.getScript(JS_PATH, function() {
katexLoaded = true;
renderWithKaTeX(mathElements); // Run KaTeX
})
});
}
};
 
$.fn.syntaxHighlight = function() {
var $children;
 
handleMath();
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
Loading
Loading
@@ -72,8 +25,4 @@
}
};
 
$(document).on('ready page:load', function() {
return $('.js-syntax-highlight').syntaxHighlight();
});
}).call(this);
---
title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
merge_request: 8003
author: Munken
Loading
Loading
@@ -84,6 +84,8 @@ module Gitlab
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
Loading
Loading
# Touch the lexers so it is registered with Rouge
Rouge::Lexers::Math
Loading
Loading
@@ -319,6 +319,40 @@ Here's a sample video:
 
![Sample Video](img/markdown_video.mp4)
 
### Math
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math
It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex].
Math written inside ```$``$``` will be rendered inline with the text.
Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line.
Example:
This math is inline $`a^2+b^2=c^2`$.
This is on a separate line
```math
a^2+b^2=c^2
```
Becomes:
This math is inline $`a^2+b^2=c^2`$.
This is on a separate line
```math
a^2+b^2=c^2
```
_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
## Standard Markdown
 
### Headers
Loading
Loading
@@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
\ No newline at end of file
require 'uri'
module Banzai
module Filter
# HTML filter that adds class="code math" and removes the dolar sign in $`2+2`$.
#
class InlineMathFilter < HTML::Pipeline::Filter
def call
doc.xpath("descendant-or-self::text()[substring(., string-length(.)) = '$']"\
"/following-sibling::*[name() = 'code']"\
"/following-sibling::text()[starts-with(.,'$')]").each do |el|
closing = el
code = el.previous
code[:class] = 'code math'
opening = code.previous
closing.content = closing.content[1..-1]
opening.content = opening.content[0..-2]
closing
end
doc
end
end
end
end
require 'uri'
module Banzai
module Filter
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
#
class MathFilter < HTML::Pipeline::Filter
# This picks out <code>...</code>.
INLINE_MATH = 'descendant-or-self::code'.freeze
# Pick out a code block which is declared math
DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze
# Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style'.freeze
# Class used for tagging elements that should be rendered
TAG_CLASS = 'js-render-math'.freeze
INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
DOLLAR_SIGN = '$'.freeze
def call
doc.xpath(INLINE_MATH).each do |code|
closing = code.next
opening = code.previous
# We need a sibling before and after.
# They should end and start with $ respectively.
if closing && opening &&
closing.content.first == DOLLAR_SIGN &&
opening.content.last == DOLLAR_SIGN
code[:class] = INLINE_CLASSES
code[STYLE_ATTRIBUTE] = 'inline'
closing.content = closing.content[1..-1]
opening.content = opening.content[0..-2]
end
end
doc.xpath(DISPLAY_MATH).each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end
doc
end
end
end
end
Loading
Loading
@@ -48,9 +48,6 @@ module Banzai
end
 
def lexer_for(language)
if language == 'math'
return Rouge::Lexers::Math.new
end
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
 
Loading
Loading
Loading
Loading
@@ -6,7 +6,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter,
 
Filter::InlineMathFilter,
Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLinkFilter,
Loading
Loading
require 'asciidoctor'
require 'asciidoctor/converter/html5'
 
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
Loading
Loading
@@ -23,7 +24,7 @@ module Gitlab
def self.render(input, context, asciidoc_opts = {})
asciidoc_opts.reverse_merge!(
safe: :secure,
backend: :html5,
backend: :gitlab_html5,
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
Loading
Loading
@@ -36,3 +37,31 @@ module Gitlab
end
end
end
module Gitlab
module Asciidoc
class Html5Converter < Asciidoctor::Converter::Html5Converter
extend Asciidoctor::Converter::Config
register_for 'gitlab_html5'
def stem(node)
return super unless node.style.to_sym == :latexmath
%(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>)
end
def inline_quoted(node)
return super unless node.type.to_sym == :latexmath
%(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>)
end
private
def id_attribute(node)
node.id ? %( id="#{node.id}") : nil
end
end
end
end
Loading
Loading
@@ -8,6 +8,8 @@ module Gitlab
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
 
if current_user
gon.current_user_id = current_user.id
Loading
Loading
module Rouge
module Lexers
class Math < Lexer
title "Plain Text"
title "A passthrough lexer used for LaTeX input"
desc "A boring lexer that doesn't highlight anything"
 
tag 'math'
mimetypes 'text/plain'
 
default_options :token => 'Text'
default_options token: 'Text'
 
def token
@token ||= Token[option :token]
Loading
Loading
@@ -18,4 +18,4 @@ module Rouge
end
end
end
end
\ No newline at end of file
end
require 'spec_helper'
describe Banzai::Filter::InlineMathFilter, lib: true do
include FilterSpecHelper
it 'leaves regular inline code unchanged' do
input = "<code>2+2</code>"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'removes surrounding dollar signs and adds class' do
doc = filter("$<code>2+2</code>$")
expect(doc.to_s).to eq '<code class="code math">2+2</code>'
end
it 'only removes surrounding dollar signs' do
doc = filter("test $<code>2+2</code>$ test")
expect(doc.to_s).to eq 'test <code class="code math">2+2</code> test'
end
it 'only removes surrounding single dollar sign' do
doc = filter("test $$<code>2+2</code>$$ test")
expect(doc.to_s).to eq 'test $<code class="code math">2+2</code>$ test'
end
it 'ignores cases with missing dolar sign at the end' do
input = "test $<code>2+2</code> test"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'ignores cases with missing dolar sign at the beginning' do
input = "test <code>2+2</code>$ test"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'ignores dollar signs if it is not adjacent' do
input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>'
doc = filter(input)
expect(doc.to_s).to eq input
end
end
require 'spec_helper'
describe Banzai::Filter::MathFilter, lib: true do
include FilterSpecHelper
it 'leaves regular inline code unchanged' do
input = "<code>2+2</code>"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
doc = filter("$<code>2+2</code>$")
expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>'
end
it 'only removes surrounding dollar signs' do
doc = filter("test $<code>2+2</code>$ test")
before = doc.xpath('descendant-or-self::text()[1]').first
after = doc.xpath('descendant-or-self::text()[3]').first
expect(before.to_s).to eq 'test '
expect(after.to_s).to eq ' test'
end
it 'only removes surrounding single dollar sign' do
doc = filter("test $$<code>2+2</code>$$ test")
before = doc.xpath('descendant-or-self::text()[1]').first
after = doc.xpath('descendant-or-self::text()[3]').first
expect(before.to_s).to eq 'test $'
expect(after.to_s).to eq '$ test'
end
it 'adds data-math-style inline attribute to inline math' do
doc = filter('$<code>2+2</code>$')
code = doc.xpath('descendant-or-self::code').first
expect(code['data-math-style']).to eq 'inline'
end
it 'adds class code and math to inline math' do
doc = filter('$<code>2+2</code>$')
code = doc.xpath('descendant-or-self::code').first
expect(code[:class]).to include("code")
expect(code[:class]).to include("math")
end
it 'adds js-render-math class to inline math' do
doc = filter('$<code>2+2</code>$')
code = doc.xpath('descendant-or-self::code').first
expect(code[:class]).to include("js-render-math")
end
# Cases with faulty syntax. Should be a no-op
it 'ignores cases with missing dolar sign at the end' do
input = "test $<code>2+2</code> test"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'ignores cases with missing dolar sign at the beginning' do
input = "test <code>2+2</code>$ test"
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'ignores dollar signs if it is not adjacent' do
input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>'
doc = filter(input)
expect(doc.to_s).to eq input
end
# Display math
it 'adds data-math-style display attribute to display math' do
doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre['data-math-style']).to eq 'display'
end
it 'adds js-render-math class to display math' do
doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre[:class]).to include("js-render-math")
end
it 'ignores code blocks that are not math' do
input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'requires the pre to contain both code and math' do
input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'dollar signs around to display math' do
doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$')
before = doc.xpath('descendant-or-self::text()[1]').first
after = doc.xpath('descendant-or-self::text()[3]').first
expect(before.to_s).to eq '$'
expect(after.to_s).to eq '$'
end
end
Loading
Loading
@@ -11,7 +11,7 @@ module Gitlab
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :html5,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS
}
 
Loading
Loading
@@ -27,7 +27,7 @@ module Gitlab
it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
backend: :html5,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
}
 
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment