Add code for generating HTML

Test Plan: Ran unit tests. Looked at `\blue{\displaystyle \left(\dfrac{a^\sigma}{\sin \theta}\right\Updownarrow \intop_{1/2}^{z^z} \sum_{i=0}^\infty x \,dx}` in Chrome and saw the future in my eyes.

Reviewers: emily

Reviewed By: emily

Subscribers: jessie

Differential Revision: http://phabricator.khanacademy.org/D13154
This commit is contained in:
Ben Alpert 2014-09-12 17:59:26 -07:00
parent 3e78a76a47
commit 7df5b4bba8
4 changed files with 146 additions and 9 deletions

View File

@ -1,7 +1,10 @@
// These objects store the data about the DOM nodes we create, as well as some
// extra data. They can then be transformed into real DOM nodes with the toDOM
// function. They are useful for both storing extra properties on the nodes, as
// well as providing a way to easily work with the DOM.
// extra data. They can then be transformed into real DOM nodes with the toNode
// function or HTML markup using toMarkup. They are useful for both storing
// extra properties on the nodes, as well as providing a way to easily work
// with the DOM.
var utils = require("./utils");
var createClass = function(classes) {
classes = classes.slice();
@ -23,7 +26,7 @@ function span(classes, children, height, depth, maxFontSize, style) {
this.style = style || {};
}
span.prototype.toDOM = function() {
span.prototype.toNode = function() {
var span = document.createElement("span");
span.className = createClass(this.classes);
@ -35,12 +38,44 @@ span.prototype.toDOM = function() {
}
for (var i = 0; i < this.children.length; i++) {
span.appendChild(this.children[i].toDOM());
span.appendChild(this.children[i].toNode());
}
return span;
};
span.prototype.toMarkup = function() {
var markup = "<span";
if (this.classes.length) {
markup += " class=\"";
markup += utils.escape(createClass(this.classes));
markup += "\"";
}
var styles = "";
for (var style in this.style) {
if (this.style.hasOwnProperty(style)) {
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
}
}
if (styles) {
markup += " style=\"" + utils.escape(styles) + "\"";
}
markup += ">";
for (var i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
markup += "</span>";
return markup;
};
function documentFragment(children, height, depth, maxFontSize) {
this.children = children || [];
this.height = height || 0;
@ -48,16 +83,26 @@ function documentFragment(children, height, depth, maxFontSize) {
this.maxFontSize = maxFontSize || 0;
}
documentFragment.prototype.toDOM = function() {
documentFragment.prototype.toNode = function() {
var frag = document.createDocumentFragment();
for (var i = 0; i < this.children.length; i++) {
frag.appendChild(this.children[i].toDOM());
frag.appendChild(this.children[i].toNode());
}
return frag;
};
documentFragment.prototype.toMarkup = function() {
var markup = "";
for (var i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
return markup;
};
function symbolNode(value, height, depth, italic, classes, style) {
this.value = value || "";
this.height = height || 0;
@ -68,7 +113,7 @@ function symbolNode(value, height, depth, italic, classes, style) {
this.maxFontSize = 0;
}
symbolNode.prototype.toDOM = function() {
symbolNode.prototype.toNode = function() {
var node = document.createTextNode(this.value);
var span = null;
@ -97,6 +142,47 @@ symbolNode.prototype.toDOM = function() {
}
};
symbolNode.prototype.toMarkup = function() {
// TODO(alpert): More duplication than I'd like from
// span.prototype.toMarkup and symbolNode.prototype.toNode...
var needsSpan = false;
var markup = "<span";
if (this.classes.length) {
needsSpan = true;
markup += " class=\"";
markup += utils.escape(createClass(this.classes));
markup += "\"";
}
var styles = "";
if (this.italic > 0) {
styles += "margin-right:" + this.italic + "em;";
}
for (var style in this.style) {
if (this.style.hasOwnProperty(style)) {
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
}
}
if (styles) {
needsSpan = true;
markup += " style=\"" + utils.escape(styles) + "\"";
}
var escaped = utils.escape(this.value);
if (needsSpan) {
markup += ">";
markup += escaped;
markup += "</span>";
return markup;
} else {
return escaped;
}
};
module.exports = {
span: span,
documentFragment: documentFragment,

View File

@ -8,12 +8,18 @@ var process = function(toParse, baseNode) {
utils.clearNode(baseNode);
var tree = parseTree(toParse);
var node = buildTree(tree).toDOM();
var node = buildTree(tree).toNode();
baseNode.appendChild(node);
};
var renderToString = function(toParse) {
var tree = parseTree(toParse);
return buildTree(tree).toMarkup();
};
module.exports = {
process: process,
renderToString: renderToString,
ParseError: ParseError
};

View File

@ -1,3 +1,4 @@
var katex = require("../katex");
var buildTree = require("../buildTree");
var parseTree = require("../parseTree");
var ParseError = require("../ParseError");
@ -986,3 +987,14 @@ describe("A bin builder", function() {
expect(getBuilt("\\blue{x+}+y")[1].classes).toContain("mord");
});
});
describe("A markup generator", function() {
it("marks trees up", function() {
// Just a few quick sanity checks here...
var markup = katex.renderToString("\\sigma^2");
expect(markup.indexOf("<span")).toBe(0);
expect(markup).toContain("\u03c3"); // sigma
expect(markup).toContain("margin-right");
expect(markup).not.toContain("marginRight");
});
});

View File

@ -19,6 +19,37 @@ var contains = function(list, elem) {
return indexOf(list, elem) !== -1;
};
// hyphenate and escape adapted from Facebook's React under Apache 2 license
var uppercase = /([A-Z])/g;
var hyphenate = function(str) {
return str.replace(uppercase, "-$1").toLowerCase();
};
var ESCAPE_LOOKUP = {
"&": "&amp;",
">": "&gt;",
"<": "&lt;",
"\"": "&quot;",
"'": "&#x27;"
};
var ESCAPE_REGEX = /[&><"']/g;
function escaper(match) {
return ESCAPE_LOOKUP[match];
}
/**
* Escapes text to prevent scripting attacks.
*
* @param {*} text Text value to escape.
* @return {string} An escaped string.
*/
function escape(text) {
return ('' + text).replace(ESCAPE_REGEX, escaper);
}
var setTextContent;
if (typeof document !== "undefined") {
@ -40,6 +71,8 @@ function clearNode(node) {
module.exports = {
contains: contains,
escape: escape,
hyphenate: hyphenate,
indexOf: indexOf,
setTextContent: setTextContent,
clearNode: clearNode