Commit ece599ff authored by Robert Knight's avatar Robert Knight

Refactor markdown and math rendering

Extract markdown and math rendering out of the editor
component into a separate module and add tests.
parent 8137324b
......@@ -2,10 +2,10 @@
var angular = require('angular');
var debounce = require('lodash.debounce');
var katex = require('katex');
var commands = require('../markdown-commands');
var mediaEmbedder = require('../media-embedder');
var renderMarkdown = require('../render-markdown');
/**
* @ngdoc directive
......@@ -16,7 +16,7 @@ var mediaEmbedder = require('../media-embedder');
* the markdown editor.
*/
// @ngInject
module.exports = function($filter, $sanitize) {
module.exports = function($sanitize) {
return {
controller: function () {},
link: function(scope, elem) {
......@@ -132,94 +132,6 @@ module.exports = function($filter, $sanitize) {
scope.preview = !scope.preview;
};
var renderInlineMath = function(textToCheck) {
var re = /\\?\\\(|\\?\\\)/g;
var startMath = null;
var endMath = null;
var match;
var indexes = [];
while ((match = re.exec(textToCheck))) {
indexes.push(match.index);
}
for (var i = 0, index; i < indexes.length; i++) {
index = indexes[i];
if (startMath === null) {
startMath = index + 2;
} else {
endMath = index;
}
if (startMath !== null && endMath !== null) {
try {
var math = katex.renderToString(textToCheck.substring(startMath, endMath));
textToCheck = (
textToCheck.substring(0, (startMath - 2)) + math +
textToCheck.substring(endMath + 2)
);
startMath = null;
endMath = null;
return renderInlineMath(textToCheck);
} catch (error) {
$sanitize(textToCheck.substring(startMath, endMath));
}
}
}
return textToCheck;
};
var renderMathAndMarkdown = function(textToCheck) {
var convert = $filter('converter');
var re = /\$\$/g;
var startMath = 0;
var endMath = 0;
var indexes = (function () {
var match;
var result = [];
while ((match = re.exec(textToCheck))) {
result.push(match.index);
}
return result;
})();
indexes.push(textToCheck.length);
var parts = (function () {
var result = [];
/* jshint -W083 */
for (var i = 0, index; i < indexes.length; i++) {
index = indexes[i];
result.push((function () {
if (startMath > endMath) {
endMath = index + 2;
try {
// \\displaystyle tells KaTeX to render the math in display style (full sized fonts).
return katex.renderToString($sanitize("\\displaystyle {" + textToCheck.substring(startMath, index) + "}"));
} catch (error) {
return $sanitize(textToCheck.substring(startMath, index));
}
} else {
startMath = index + 2;
return $sanitize(convert(renderInlineMath(textToCheck.substring(endMath, index))));
}
})());
}
/* jshint +W083 */
return result;
})();
var htmlString = parts.join('');
// Transform the HTML string into a DOM element.
var domElement = document.createElement('div');
domElement.innerHTML = htmlString;
mediaEmbedder.replaceLinksWithEmbeds(domElement);
return domElement.innerHTML;
};
// React to the changes to the input
var handleInputChange = debounce(function () {
scope.$apply(function () {
......@@ -230,15 +142,16 @@ module.exports = function($filter, $sanitize) {
// Re-render the markdown when the view needs updating.
scope.$watch('text', function () {
output.innerHTML = renderMathAndMarkdown(scope.text || '');
output.innerHTML = renderMarkdown(scope.text || '', $sanitize);
mediaEmbedder.replaceLinksWithEmbeds(output);
});
scope.showEditor = function () {
return !scope.readOnly && !scope.preview;
};
// Exit preview mode when leaving edit mode
scope.$watch('readOnly', function () {
// Exit preview mode when editor stops
scope.preview = false;
});
......
......@@ -50,18 +50,17 @@ describe('markdown', function () {
fn();
};
},
'../render-markdown': noCallThru(function (markdown, $sanitize) {
return $sanitize('rendered:' + markdown);
}),
'../markdown-commands': {
convertSelectionToLink: mockFormattingCommand,
toggleBlockStyle: mockFormattingCommand,
toggleSpanStyle: mockFormattingCommand,
LinkType: require('../../markdown-commands').LinkType,
},
})))
.filter('converter', function () {
return function (input) {
return 'rendered:' + input;
};
});
}),
}));
});
beforeEach(function () {
......@@ -103,17 +102,6 @@ describe('markdown', function () {
});
});
describe('math rendering', function () {
it('should render LaTeX', function () {
var editor = util.createDirective(document, 'markdown', {
readOnly: true,
text: '$$x*2$$',
});
assert.equal(getRenderedHTML(editor),
'rendered:math:\\displaystyle {x*2}rendered:');
});
});
describe('toolbar buttons', function () {
it('should apply formatting when clicking toolbar buttons', function () {
var editor = util.createDirective(document, 'markdown', {
......
'use strict';
var showdown = require('showdown');
function targetBlank(converter) {
function targetBlank() {
function filter(text) {
return text.replace(/<a href=/g, '<a target="_blank" href=');
}
......
'use strict';
var katex = require('katex');
var showdown = require('showdown');
function targetBlank() {
function filter(text) {
return text.replace(/<a href=/g, '<a target="_blank" href=');
}
return [{type: 'output', filter: filter}];
}
function renderMarkdown(html) {
// see https://github.com/showdownjs/showdown#valid-options
var converter = new showdown.Converter({
extensions: [targetBlank],
simplifiedAutoLink: true,
// Since we're using simplifiedAutoLink we also use
// literalMidWordUnderscores because otherwise _'s in URLs get
// transformed into <em>'s.
// See https://github.com/showdownjs/showdown/issues/211
literalMidWordUnderscores: true,
});
return converter.makeHtml(html);
}
function renderInlineMath(textToCheck, $sanitize) {
var re = /\\?\\\(|\\?\\\)/g;
var startMath = null;
var endMath = null;
var match;
var indexes = [];
while ((match = re.exec(textToCheck))) {
indexes.push(match.index);
}
for (var i = 0, index; i < indexes.length; i++) {
index = indexes[i];
if (startMath === null) {
startMath = index + 2;
} else {
endMath = index;
}
if (startMath !== null && endMath !== null) {
try {
var math = katex.renderToString(textToCheck.substring(startMath, endMath));
textToCheck = (
textToCheck.substring(0, (startMath - 2)) + math +
textToCheck.substring(endMath + 2)
);
startMath = null;
endMath = null;
return renderInlineMath(textToCheck);
} catch (error) {
$sanitize(textToCheck.substring(startMath, endMath));
}
}
}
return textToCheck;
}
function renderMathAndMarkdown(textToCheck, $sanitize) {
var re = /\$\$/g;
var startMath = 0;
var endMath = 0;
var indexes = (function () {
var match;
var result = [];
while ((match = re.exec(textToCheck))) {
result.push(match.index);
}
return result;
})();
indexes.push(textToCheck.length);
var parts = (function () {
var result = [];
/* jshint -W083 */
for (var i = 0, index; i < indexes.length; i++) {
index = indexes[i];
result.push((function () {
if (startMath > endMath) {
endMath = index + 2;
try {
// \\displaystyle tells KaTeX to render the math in display style (full sized fonts).
return katex.renderToString($sanitize("\\displaystyle {" + textToCheck.substring(startMath, index) + "}"));
} catch (error) {
return $sanitize(textToCheck.substring(startMath, index));
}
} else {
startMath = index + 2;
return $sanitize(renderMarkdown(renderInlineMath(textToCheck.substring(endMath, index), $sanitize)));
}
})());
}
/* jshint +W083 */
return result;
})();
return parts.join('');
}
module.exports = renderMathAndMarkdown;
'use strict';
var proxyquire = require('proxyquire');
describe('render-markdown', function () {
var render;
var renderMarkdown;
beforeEach(function () {
renderMarkdown = proxyquire('../render-markdown', {
katex: {
renderToString: function (input) {
return 'math:' + input;
},
},
});
render = function (markdown) {
return renderMarkdown(markdown, function (html) { return html; });
};
});
describe('autolinking', function () {
it('should autolink URLs', function () {
assert.equal(render('See this link - http://arxiv.org/article'),
'<p>See this link - <a target="_blank" href="http://arxiv.org/article">' +
'http://arxiv.org/article</a></p>');
});
it('should autolink URLs with _\'s in them correctly', function () {
assert.equal(
render(
'See this https://hypothes.is/stream?q=tag:group_test_needs_card'),
'<p>See this <a target="_blank" ' +
'href="https://hypothes.is/stream?q=tag:group_test_needs_card">' +
'https://hypothes.is/stream?q=tag:group_test_needs_card</a></p>');
});
});
describe('markdown rendering', function () {
it('should render markdown', function () {
assert.equal(render('one **two** three'),
'<p>one <strong>two</strong> three</p>');
});
it('should sanitize the result', function () {
var sanitize = function (html) {
return '<safe>' + html + '</safe>';
};
assert.equal(renderMarkdown('one **two** three', sanitize),
'<safe><p>one <strong>two</strong> three</p></safe>');
});
});
describe('math rendering', function () {
it('should render LaTeX', function () {
assert.equal(render('$$x*2$$'), 'math:\\displaystyle {x*2}');
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment