www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 7df5b4bba846f20bd7e506adad8c82f8821a9474
parent 3e78a76a4750563a00d9ed4e2d872f7c2466a61d
Author: Ben Alpert <spicyjalapeno@gmail.com>
Date:   Fri, 12 Sep 2014 17:59:26 -0700

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

Diffstat:
MdomTree.js | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mkatex.js | 8+++++++-
Mtest/katex-spec.js | 12++++++++++++
Mutils.js | 33+++++++++++++++++++++++++++++++++
4 files changed, 146 insertions(+), 9 deletions(-)

diff --git a/domTree.js b/domTree.js @@ -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, diff --git a/katex.js b/katex.js @@ -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 }; diff --git a/test/katex-spec.js b/test/katex-spec.js @@ -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"); + }); +}); diff --git a/utils.js b/utils.js @@ -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