Add basic auto-render extension

Summary:
Add an auto-render extension to render math on a page. It
exposes a global function (maybe we should attach it to `katex`?) to
render math in an element. It comes with a README on how to use it.
Also, make `make build` build the minified file.

Fixes #26

Test Plan:
 - Visit http://localhost:7936/contrib/auto-render/
 - See that all of the math renders correctly
 - `make test`

Reviewers: alpert, kevinb

Reviewed By: kevinb

Differential Revision: https://phabricator.khanacademy.org/D16620
This commit is contained in:
Emily Eisenberg
2015-04-01 15:57:10 -07:00
parent 99a81aca50
commit cd9bca4a89
12 changed files with 595 additions and 35 deletions

View File

@@ -0,0 +1,9 @@
.PHONY: build
build: $(BUILDDIR)/contrib/auto-render.min.js
$(BUILDDIR)/contrib/auto-render.min.js: $(BUILDDIR)/auto-render.js
$(UGLIFYJS) < $< > $@
$(BUILDDIR)/auto-render.js: auto-render.js
$(BROWSERIFY) $< --standalone renderMathInElement > $@

View File

@@ -0,0 +1,63 @@
# Auto-render extension
This is an extension to automatically render all of the math inside of text. It
searches all of the text nodes in a given element for the given delimiters, and
renders the math in place.
### Usage
This extension isn't part of KaTeX proper, so the script should be separately
included in the page:
```html
<script src="/path/to/auto-render.min.js"></script>
```
Then, call the exposed `renderMathInElement` function in a script tag
before the close body tag:
```html
<body>
...
<script>
renderMathInElement(document.body);
</script>
</body>
```
See [index.html](index.html) for an example.
### API
This extension exposes a single function, `window.renderMathInElement`, with
the following API:
```js
function renderMathInElement(elem, options)
```
`elem` is an HTML DOM element. The function will recursively search for text
nodes inside this element and render the math in them.
`options` is an optional object argument with the following keys:
- `delimiters`: This is a list of delimiters to look for math. Each delimiter
has three properties:
- `left`: A string which starts the math expression (i.e. the left delimiter).
- `right`: A string which ends the math expression (i.e. the right delimiter).
- `display`: A boolean of whether the math in the expression should be
rendered in display mode or not.
The default value is:
```js
[
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}
]
```
- `ignoredTags`: This is a list of DOM node types to ignore when recursing
through. The default value is
`["script", "noscript", "style", "textarea", "pre", "code"]`.

View File

@@ -0,0 +1,217 @@
var splitAtDelimiters = require("./splitAtDelimiters");
beforeEach(function() {
jasmine.addMatchers({
toSplitInto: function() {
return {
compare: function(actual, left, right, result) {
var message = {
pass: true,
message: "'" + actual + "' split correctly"
};
var startData = [{type: "text", data: actual}];
var split = splitAtDelimiters(startData, left, right, false);
if (split.length !== result.length) {
message.pass = false;
message.message = "Different number of splits: " +
split.length + " vs. " + result.length + " (" +
JSON.stringify(split) + " vs. " +
JSON.stringify(result) + ")";
return message;
}
for (var i = 0; i < split.length; i++) {
var real = split[i];
var correct = result[i];
var good = true;
if (real.type !== correct.type) {
good = false;
diff = "type";
} else if (real.data !== correct.data) {
good = false;
diff = "data";
} else if (real.display !== correct.display) {
good = false;
diff = "display";
}
if (!good) {
message.pass = false;
message.message = "Difference at split " +
(i + 1) + ": " + JSON.stringify(real) +
" vs. " + JSON.stringify(correct) +
" (" + diff + " differs)";
break;
}
}
return message;
}
};
}
});
});
describe("A delimiter splitter", function() {
it("doesn't split when there are no delimiters", function() {
expect("hello").toSplitInto("(", ")", [{type: "text", data: "hello"}]);
});
it("doesn't create a math node when there's only a left delimiter", function() {
expect("hello ( world").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "text", data: "( world"}
]);
});
it("doesn't split when there's only a right delimiter", function() {
expect("hello ) world").toSplitInto(
"(", ")",
[
{type: "text", data: "hello ) world"}
]);
});
it("splits when there are both delimiters", function() {
expect("hello ( world ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"}
]);
});
it("splits on multi-character delimiters", function() {
expect("hello [[ world ]] boo").toSplitInto(
"[[", "]]",
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"}
]);
});
it("splits mutliple times", function() {
expect("hello ( world ) boo ( more ) stuff").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo "},
{type: "math", data: " more ", display: false},
{type: "text", data: " stuff"}
]);
});
it("leaves the ending when there's only a left delimiter", function() {
expect("hello ( world ) boo ( left").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo "},
{type: "text", data: "( left"}
]);
});
it("doesn't split when close delimiters are in {}s", function() {
expect("hello ( world { ) } ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world { ) } ", display: false},
{type: "text", data: " boo"}
]);
expect("hello ( world { { } ) } ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world { { } ) } ", display: false},
{type: "text", data: " boo"}
]);
});
it("doesn't split at escaped delimiters", function() {
expect("hello ( world \\) ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello "},
{type: "math", data: " world \\) ", display: false},
{type: "text", data: " boo"}
]);
/* TODO(emily): make this work maybe?
expect("hello \\( ( world ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello \\( "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"}
]);
*/
});
it("splits when the right and left delimiters are the same", function() {
expect("hello $ world $ boo").toSplitInto(
"$", "$",
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"}
]);
});
it("remembers which delimiters are display-mode", function() {
var startData = [{type: "text", data: "hello ( world ) boo"}];
expect(splitAtDelimiters(startData, "(", ")", true)).toEqual(
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: true},
{type: "text", data: " boo"}
]);
});
it("works with more than one start datum", function() {
var startData = [
{type: "text", data: "hello ( world ) boo"},
{type: "math", data: "math", display: true},
{type: "text", data: "hello ( world ) boo"}
];
expect(splitAtDelimiters(startData, "(", ")", false)).toEqual(
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"},
{type: "math", data: "math", display: true},
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"}
]);
});
it("doesn't do splitting inside of math nodes", function() {
var startData = [
{type: "text", data: "hello ( world ) boo"},
{type: "math", data: "hello ( world ) boo", display: true}
];
expect(splitAtDelimiters(startData, "(", ")", false)).toEqual(
[
{type: "text", data: "hello "},
{type: "math", data: " world ", display: false},
{type: "text", data: " boo"},
{type: "math", data: "hello ( world ) boo", display: true}
]);
});
});

View File

@@ -0,0 +1,95 @@
var splitAtDelimiters = require("./splitAtDelimiters");
var splitWithDelimiters = function(text, delimiters) {
var data = [{type: "text", data: text}];
for (var i = 0; i < delimiters.length; i++) {
var delimiter = delimiters[i];
data = splitAtDelimiters(
data, delimiter.left, delimiter.right,
delimiter.display || false);
}
return data;
};
var renderMathInText = function(text, delimiters) {
var data = splitWithDelimiters(text, delimiters);
var fragment = document.createDocumentFragment();
for (var i = 0; i < data.length; i++) {
if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data));
} else {
var span = document.createElement("span");
var math = data[i].data;
katex.render(math, span, {
displayMode: data[i].display
});
fragment.appendChild(span);
}
}
return fragment;
};
var renderElem = function(elem, delimiters, ignoredTags) {
for (var i = 0; i < elem.childNodes.length; i++) {
var childNode = elem.childNodes[i];
if (childNode.nodeType === 3) {
// Text node
var frag = renderMathInText(childNode.textContent, delimiters);
i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode);
} else if (childNode.nodeType === 1) {
// Element node
var shouldRender = ignoredTags.indexOf(
childNode.nodeName.toLowerCase()) === -1;
if (shouldRender) {
renderElem(childNode, delimiters, ignoredTags);
}
} else {
// Something else, ignore
}
}
};
var defaultOptions = {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}
// LaTeX uses this, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false}
],
ignoredTags: [
"script", "noscript", "style", "textarea", "pre", "code"
]
};
var extend = function(obj) {
// Adapted from underscore.js' `_.extend`. See LICENSE.txt for license.
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
obj[prop] = source[prop];
}
}
}
return obj;
};
var renderMathInElement = function(elem, options) {
if (!elem) {
throw new Error("No element provided to render");
}
options = extend({}, defaultOptions, options);
renderElem(elem, options.delimiters, options.ignoredTags);
};
module.exports = renderMathInElement;

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Auto-render test</title>
<script src="/katex.js" type="text/javascript"></script>
<link href="/katex.css" rel="stylesheet" type="text/css">
<script src="./auto-render.js" type="text/javascript"></script>
<style type="text/css">
body {
margin: 0px;
padding: 0px;
font-size: 72px;
}
#test > .blue {
color: blue;
}
</style>
</head>
<body>
<div id="test">
This is some text $math \frac12$ other text
<span class="blue">
Other node \[ displaymath \frac{1}{2} \] blah $$ \int_2^3 $$
</span>
and some <!-- comment --> more text \(and math\) blah. And $math with a
\$ sign$.
<pre>
Stuff in a $pre tag$
</pre>
</div>
<script>
renderMathInElement(
document.getElementById("test"),
{
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false}
]
}
);
</script>
</body>
</html>

View File

@@ -0,0 +1,98 @@
var findEndOfMath = function(delimiter, text, startIndex) {
// Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
var index = startIndex;
var braceLevel = 0;
var delimLength = delimiter.length;
while (index < text.length) {
var character = text[index];
if (braceLevel <= 0 &&
text.slice(index, index + delimLength) === delimiter) {
return index;
} else if (character === "\\") {
index++;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
}
index++;
}
return -1;
};
var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
var finalData = [];
for (var i = 0; i < startData.length; i++) {
if (startData[i].type === "text") {
var text = startData[i].data;
var lookingForLeft = true;
var currIndex = 0;
var nextIndex;
nextIndex = text.indexOf(leftDelim);
if (nextIndex !== -1) {
currIndex = nextIndex;
finalData.push({
type: "text",
data: text.slice(0, currIndex)
});
lookingForLeft = false;
}
while (true) {
if (lookingForLeft) {
nextIndex = text.indexOf(leftDelim, currIndex);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "text",
data: text.slice(currIndex, nextIndex)
});
currIndex = nextIndex;
} else {
nextIndex = findEndOfMath(
rightDelim,
text,
currIndex + leftDelim.length);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "math",
data: text.slice(
currIndex + leftDelim.length,
nextIndex),
display: display
});
currIndex = nextIndex + rightDelim.length;
}
lookingForLeft = !lookingForLeft;
}
finalData.push({
type: "text",
data: text.slice(currIndex)
});
} else {
finalData.push(startData[i]);
}
}
return finalData;
};
module.exports = splitAtDelimiters;