diff --git a/LICENSE.txt b/LICENSE.txt index 3e25c643a..f7b2d38cd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,12 @@ The MIT License (MIT) -Copyright (c) 2014 Khan Academy +Copyright (c) 2015 Khan Academy + +This software also uses portions of the underscore.js project, which is +MIT licensed with the following copyright: + +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +24,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index 16eadc0d9..d5d517d5f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,13 @@ -.PHONY: build lint setup copy serve clean metrics test zip -build: setup lint build/katex.min.js build/katex.min.css zip compress +.PHONY: build lint setup copy serve clean metrics test zip contrib +build: setup lint build/katex.min.js build/katex.min.css contrib zip compress + +# Export these variables for use in contrib Makefiles +export BUILDDIR = $(realpath build) +export BROWSERIFY = $(realpath ./node_modules/.bin/browserify) +export UGLIFYJS = $(realpath ./node_modules/.bin/uglifyjs) \ + --mangle \ + --beautify \ + ascii_only=true,beautify=false setup: npm install @@ -8,10 +16,10 @@ lint: katex.js $(wildcard src/*.js) ./node_modules/.bin/jshint $^ build/katex.js: katex.js $(wildcard src/*.js) - ./node_modules/.bin/browserify $< --standalone katex > $@ + $(BROWSERIFY) $< --standalone katex > $@ build/katex.min.js: build/katex.js - ./node_modules/.bin/uglifyjs --mangle --beautify ascii_only=true,beautify=false < $< > $@ + $(UGLIFYJS) < $< > $@ build/katex.less.css: static/katex.less $(wildcard static/*.less) ./node_modules/.bin/lessc $< $@ @@ -27,15 +35,27 @@ build/fonts: cp static/fonts/$$font* $@; \ done +contrib: build/contrib + +.PHONY: build/contrib +build/contrib: + mkdir -p build/contrib + # Since everything in build/contrib is put in the built files, make sure + # there's nothing in there we don't want. + rm -rf build/contrib/* + $(MAKE) -C contrib/auto-render + .PHONY: build/katex -build/katex: build/katex.min.js build/katex.min.css build/fonts README.md +build/katex: build/katex.min.js build/katex.min.css build/fonts README.md build/contrib mkdir -p build/katex + rm -rf build/katex/* cp -r $^ build/katex build/katex.tar.gz: build/katex cd build && tar czf katex.tar.gz katex/ build/katex.zip: build/katex + rm -f $@ cd build && zip -rq katex.zip katex/ zip: build/katex.tar.gz build/katex.zip @@ -53,6 +73,7 @@ serve: test: ./node_modules/.bin/jasmine-node test/katex-spec.js + ./node_modules/.bin/jasmine-node contrib/auto-render/auto-render-spec.js metrics: cd metrics && ./mapping.pl | ./extract_tfms.py | ./extract_ttfs.py | ./replace_line.py diff --git a/README.md b/README.md index 8cc7b263b..605651fb7 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ For example: katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, { displayMode: true }); ``` +#### Automatic rendering of math on a page + +Math on the page can be automatically rendered using the auto-render extension. See [the Auto-render README](contrib/auto-render/README.md) for more information. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/contrib/auto-render/Makefile b/contrib/auto-render/Makefile new file mode 100644 index 000000000..5ae175322 --- /dev/null +++ b/contrib/auto-render/Makefile @@ -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 > $@ diff --git a/contrib/auto-render/README.md b/contrib/auto-render/README.md new file mode 100644 index 000000000..49d9b77e9 --- /dev/null +++ b/contrib/auto-render/README.md @@ -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 + +``` + +Then, call the exposed `renderMathInElement` function in a script tag +before the close body tag: + +```html +
+ ... + + +``` + +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"]`. diff --git a/contrib/auto-render/auto-render-spec.js b/contrib/auto-render/auto-render-spec.js new file mode 100644 index 000000000..c64b15e20 --- /dev/null +++ b/contrib/auto-render/auto-render-spec.js @@ -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} + ]); + }); +}); diff --git a/contrib/auto-render/auto-render.js b/contrib/auto-render/auto-render.js new file mode 100644 index 000000000..b3d3de636 --- /dev/null +++ b/contrib/auto-render/auto-render.js @@ -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; diff --git a/contrib/auto-render/index.html b/contrib/auto-render/index.html new file mode 100644 index 000000000..6eecc020a --- /dev/null +++ b/contrib/auto-render/index.html @@ -0,0 +1,47 @@ + + + + ++ Stuff in a $pre tag$ ++