Compare commits

..

1 Commits

Author SHA1 Message Date
Emily Eisenberg
b16136ee27 v0.4.0 2015-06-18 15:38:51 -07:00
366 changed files with 9824 additions and 10863 deletions

View File

@ -1,5 +1,8 @@
{ {
"project_id": "KaTeX", "project_id": "KaTeX",
"conduit_uri": "https://phabricator.khanacademy.org/", "conduit_uri": "https://phabricator.khanacademy.org/",
"lint.engine": "ArcanistConfigurationDrivenLintEngine" "lint.engine": "ArcanistSingleLintEngine",
"lint.engine.single.linter": "ArcanistScriptAndRegexLinter",
"linter.scriptandregex.regex": "/^(?P<file>\\S+): line (?P<line>\\d+), col \\d+, (?P<message>.*)$/m",
"linter.scriptandregex.script": "make lint || true"
} }

View File

@ -1,9 +0,0 @@
{
"linters": {
"katex-linter": {
"type": "script-and-regex",
"script-and-regex.script": "make lint || true",
"script-and-regex.regex": "/^(?P<file>\\S+): line (?P<line>\\d+), col \\d+, (?P<message>.*)$/m"
}
}
}

View File

@ -1,6 +0,0 @@
{
"presets": ["es2015"],
"plugins": [
"transform-runtime"
]
}

View File

@ -1,83 +0,0 @@
{
"rules": {
"arrow-spacing": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
// We'd possibly like to remove the 'properties': 'never' one day.
"camelcase": [2, { "properties": "never" }],
"comma-dangle": [2, "always-multiline"],
"comma-spacing": [2, { "before": false, "after": true }],
"constructor-super": 2,
"curly": 2,
"eol-last": 2,
"eqeqeq": [2, "allow-null"],
"guard-for-in": 2,
"indent": [2, 4, {"SwitchCase": 1}],
"keyword-spacing": 2,
"linebreak-style": [2, "unix"],
"max-len": [2, 80, 4, { "ignoreUrls": true, "ignorePattern": "\\brequire\\([\"']|eslint-disable" }],
"no-alert": 2,
"no-array-constructor": 2,
"no-console": 2,
"no-const-assign": 2,
"no-debugger": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-extra-bind": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-this-before-super": 2,
"no-throw-literal": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
"no-unused-vars": [2, {"args": "none", "varsIgnorePattern": "^_*$"}],
"no-useless-call": 2,
"no-var": 2,
"no-with": 2,
"one-var": [2, "never"],
"prefer-const": 2,
"prefer-spread": 0, // re-enable once we use es6
"semi": [2, "always"],
"space-before-blocks": 2,
"space-before-function-paren": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": 2,
// ---------------------------------------
// Stuff we explicitly disable.
// We turned this off because it complains when you have a
// multi-line string, which I think is going too far.
"prefer-template": 0,
// We've decided explicitly not to care about this.
"arrow-parens": 0,
// ---------------------------------------
// TODO(csilvers): enable these if/when community agrees on it.
"prefer-arrow-callback": 0,
"object-curly-spacing": [0, "always"],
// Might be nice to turn this on one day, but since we don't
// use jsdoc anywhere it seems silly to require it yet.
"valid-jsdoc": 0,
"require-jsdoc": 0
},
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"destructuring": true,
"experimentalObjectRestSpread": true,
"forOf": true,
"jsx": true,
"restParams": true,
"spread": true,
"templateStrings": true
},
"env": {
"es6": true,
"node": true,
"browser": true
},
"extends": "eslint:recommended",
"root": true
}

9
.gitattributes vendored
View File

@ -1,9 +0,0 @@
# Install ttx from https://github.com/fonttools/fonttools,
# then add this to your ~/.gitconfig to diff fonts more easily:
#[diff "font"]
# binary = true
# textconv = ttx -q -i -o -
*.ttf diff=font
*.woff diff=font

9
.gitignore vendored
View File

@ -3,12 +3,3 @@ node_modules
npm-debug.log npm-debug.log
last.png last.png
diff.png diff.png
/.npm-install.stamp
/dist/
/test/screenshotter/tex/
/test/screenshotter/diff/
/test/symgroups.tex
/test/symgroups.aux
/test/symgroups.log
/test/symgroups.pdf
/test/screenshotter/unicode-fonts

68
.jshintrc Normal file
View File

@ -0,0 +1,68 @@
{
"bitwise" : true,
"camelcase" : true,
"curly" : true,
"eqeqeq" : false,
"es3" : true,
"forin" : false,
"immed" : true,
"indent" : 4,
"latedef" : false,
"newcap" : true,
"noarg" : true,
"noempty" : true,
"nonbsp" : true,
"nonew" : true,
"plusplus" : false,
"quotmark" : "double",
"undef" : true,
"unused" : "vars",
"strict" : false,
"trailing" : true,
"maxparams" : 7,
"maxdepth" : 6,
"asi" : false,
"boss" : false,
"debug" : false,
"eqnull" : true,
"esnext" : false,
"evil" : false,
"expr" : true,
"funcscope" : false,
"globalstrict" : false,
"iterator" : false,
"lastsemic" : false,
"laxbreak" : true,
"laxcomma" : false,
"loopfunc" : false,
"maxerr" : 50,
"multistr" : false,
"notypeof" : false,
"proto" : true,
"scripturl" : false,
"smarttabs" : false,
"shadow" : false,
"sub" : false,
"supernew" : false,
"validthis" : false,
"noyield" : false,
"browser" : true,
"couch" : false,
"devel" : false,
"dojo" : false,
"jquery" : false,
"mootools" : false,
"node" : true,
"nonstandard" : false,
"prototypejs" : false,
"rhino" : false,
"worker" : false,
"wsh" : false,
"yui" : false,
"globals": {
"JSON": false,
"console": true
}
}

View File

@ -1,13 +1,4 @@
language: node_js language: node_js
node_js: node_js:
- stable - "0.11"
sudo: required - "0.10"
services:
- docker
before_script:
- docker pull selenium/standalone-firefox:2.48.2
- docker pull selenium/standalone-chrome:2.48.2
- docker images --no-trunc
script:
- npm test
- dockers/Screenshotter/screenshotter.sh --verify

View File

@ -1,74 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at opensource@khanacademy.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -2,21 +2,16 @@
We welcome pull requests to KaTeX. If you'd like to add a new symbol, or try to We welcome pull requests to KaTeX. If you'd like to add a new symbol, or try to
tackle adding a larger feature, keep reading. If you have any questions, or want tackle adding a larger feature, keep reading. If you have any questions, or want
help solving a problem, feel free to stop by our [gitter channel](https://gitter.im/Khan/KaTeX). help solving a problem, feel free to stop by the [#katex room on
freenode](http://webchat.freenode.net/?channels=katex).
## Helpful contributions ## Helpful contributions
If you'd like to contribute, try contributing new symbols or functions that If you'd like to contribute, try contributing new symbols or functions that
KaTeX doesn't currently support. The wiki has a page which lists [all of the KaTeX doesn't currently support. The wiki has a page which lists [all of the
supported supported
functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX) as functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX). You
well as a page that describes how to [examine TeX commands and where to find can check there to see if we don't support a function you like, or try your
rules](https://github.com/Khan/KaTeX/wiki/Examining-TeX) which can be quite
useful when adding new commands. There's also a user-contributed [preview page]
(http://utensil-site.github.io/available-in-katex/)
showing how KaTeX would render a series of symbols/functions (including the ones
MathJax listed in their documentation and the extra ones supported by KaTeX). You
can check them to see if we don't support a function you like, or try your
function in the interactive demo at function in the interactive demo at
[http://khan.github.io/KaTeX/](http://khan.github.io/KaTeX/). [http://khan.github.io/KaTeX/](http://khan.github.io/KaTeX/).
@ -88,7 +83,7 @@ changed and why. Otherwise, figure out what is causing the changes and fix it!
If you add a feature that is dependent on the final output looking the way you If you add a feature that is dependent on the final output looking the way you
created it, add a screenshot test. See created it, add a screenshot test. See
[ss_data.yaml](test/screenshotter/ss_data.yaml). [ss_data.json](test/screenshotter/ss_data.json).
#### Testing in other browsers #### Testing in other browsers
@ -105,18 +100,9 @@ Code
- 80 character line length - 80 character line length
- commas last - commas last
- declare variables in the outermost scope that they are used - declare variables in the outermost scope that they are used
- camelCase for variables in JavaScript
- snake_case for variables in Python
In general, try to make your code blend in with the surrounding code. In general, try to make your code blend in with the surrounding code.
## Pull Requests
- link back to the original issue(s) whenever possible
- new commands should be added to the [wiki](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX)
- commits should be squashed before merging
- large pull requests should be broken into separate pull requests (or multiple logically cohesive commits), if possible
## CLA ## CLA
In order to contribute to KaTeX, you must first sign the CLA, found at www.khanacademy.org/r/cla In order to contribute to KaTeX, you must first sign the CLA, found at www.khanacademy.org/r/cla

View File

@ -1,23 +1,8 @@
.PHONY: build dist lint setup copy serve clean metrics test zip contrib .PHONY: build dist lint setup copy serve clean metrics test zip contrib
build: lint build/katex.min.js build/katex.min.css contrib zip compress build: setup lint build/katex.min.js build/katex.min.css contrib zip compress
ifeq ($(KATEX_DIST),skip)
dist:
else
dist: build dist: build
rm -rf dist/ cp --recursive build/katex dist
cp -R build/katex/ dist/
endif
NODE := node # pass NODE=nodejs on Debian without package nodejs-legacy
NODECHK := $(shell $(NODE) ./check-node-version.js)
ifneq ($(NODECHK),OK)
$(error "Node not found or wrong version")
endif
# Export these variables for use in contrib Makefiles # Export these variables for use in contrib Makefiles
export BUILDDIR = $(realpath build) export BUILDDIR = $(realpath build)
@ -27,27 +12,22 @@ export UGLIFYJS = $(realpath ./node_modules/.bin/uglifyjs) \
--beautify \ --beautify \
ascii_only=true,beautify=false ascii_only=true,beautify=false
# The prepublish script in package.json will override the following variable, setup:
# setting it to the empty string and thereby avoiding an infinite recursion npm install
NIS = .npm-install.stamp
$(NIS) setup: package.json lint: katex.js server.js cli.js $(wildcard src/*.js) $(wildcard test/*.js) $(wildcard contrib/*/*.js)
KATEX_DIST=skip npm install # dependencies only, don't build ./node_modules/.bin/jshint $^
@touch $(NIS)
lint: $(NIS) katex.js server.js cli.js $(wildcard src/*.js) $(wildcard test/*.js) $(wildcard contrib/*/*.js) $(wildcard dockers/*/*.js) build/katex.js: katex.js $(wildcard src/*.js)
./node_modules/.bin/eslint $(filter-out %.stamp,$^) $(BROWSERIFY) $< --standalone katex > $@
build/katex.js: katex.js $(wildcard src/*.js) $(NIS)
$(BROWSERIFY) -t [ babelify ] $< --standalone katex > $@
build/katex.min.js: build/katex.js build/katex.min.js: build/katex.js
$(UGLIFYJS) < $< > $@ $(UGLIFYJS) < $< > $@
build/katex.css: static/katex.less $(wildcard static/*.less) $(NIS) build/katex.less.css: static/katex.less $(wildcard static/*.less)
./node_modules/.bin/lessc $< $@ ./node_modules/.bin/lessc $< $@
build/katex.min.css: build/katex.css build/katex.min.css: build/katex.less.css
./node_modules/.bin/cleancss -o $@ $< ./node_modules/.bin/cleancss -o $@ $<
.PHONY: build/fonts .PHONY: build/fonts
@ -58,24 +38,18 @@ build/fonts:
cp static/fonts/$$font* $@; \ cp static/fonts/$$font* $@; \
done done
test/screenshotter/unicode-fonts:
git clone https://github.com/Khan/KaTeX-test-fonts test/screenshotter/unicode-fonts
cd test/screenshotter/unicode-fonts && \
git checkout 99fa66a2da643218754c8236b9f9151cac71ba7c && \
cd ../../../
contrib: build/contrib contrib: build/contrib
.PHONY: build/contrib .PHONY: build/contrib
build/contrib: build/contrib:
mkdir -p build/contrib mkdir -p build/contrib
@# Since everything in build/contrib is put in the built files, make sure # Since everything in build/contrib is put in the built files, make sure
@# there's nothing in there we don't want. # there's nothing in there we don't want.
rm -rf build/contrib/* rm -rf build/contrib/*
$(MAKE) -C contrib/auto-render $(MAKE) -C contrib/auto-render
.PHONY: build/katex .PHONY: build/katex
build/katex: build/katex.js build/katex.min.js build/katex.css build/katex.min.css build/fonts README.md build/contrib build/katex: build/katex.min.js build/katex.min.css build/fonts README.md build/contrib
mkdir -p build/katex mkdir -p build/katex
rm -rf build/katex/* rm -rf build/katex/*
cp -r $^ build/katex cp -r $^ build/katex
@ -90,30 +64,25 @@ build/katex.zip: build/katex
zip: build/katex.tar.gz build/katex.zip zip: build/katex.tar.gz build/katex.zip
compress: build/katex.min.js build/katex.min.css compress: build/katex.min.js build/katex.min.css
@JSSIZE=`gzip -c build/katex.min.js | wc -c`; \ @$(eval JSSIZE!=gzip -c build/katex.min.js | wc -c)
CSSSIZE=`gzip -c build/katex.min.css | wc -c`; \ @$(eval CSSSIZE!=gzip -c build/katex.min.css | wc -c)
TOTAL=`echo $${JSSIZE}+$${CSSSIZE} | bc`; \ @$(eval TOTAL!=echo ${JSSIZE}+${CSSSIZE} | bc)
printf "Minified, gzipped js: %6d\n" "$${JSSIZE}"; \ @printf "Minified, gzipped js: %6d\n" "${JSSIZE}"
printf "Minified, gzipped css: %6d\n" "$${CSSSIZE}"; \ @printf "Minified, gzipped css: %6d\n" "${CSSSIZE}"
printf "Total: %6d\n" "$${TOTAL}" @printf "Total: %6d\n" "${TOTAL}"
serve: $(NIS) serve:
$(NODE) server.js node server.js
test: $(NIS) test:
JASMINE_CONFIG_PATH=test/jasmine.json node_modules/.bin/jasmine ./node_modules/.bin/jasmine-node test/katex-spec.js
./node_modules/.bin/jasmine-node contrib/auto-render/auto-render-spec.js
PERL=perl
PYTHON=$(shell python2 --version >/dev/null 2>&1 && echo python2 || echo python)
metrics: metrics:
cd metrics && $(PERL) ./mapping.pl | $(PYTHON) ./extract_tfms.py | $(PYTHON) ./extract_ttfs.py | $(PYTHON) ./format_json.py > ../src/fontMetricsData.js cd metrics && ./mapping.pl | ./extract_tfms.py | ./extract_ttfs.py | ./replace_line.py
extended_metrics:
cd metrics && $(PERL) ./mapping.pl | $(PYTHON) ./extract_tfms.py | $(PYTHON) ./extract_ttfs.py | $(PYTHON) ./format_json.py --width > ../src/fontMetricsData.js
clean: clean:
rm -rf build/* $(NIS) rm -rf build/*
screenshots: test/screenshotter/unicode-fonts $(NIS) screenshots:
dockers/Screenshotter/screenshotter.sh docker run --volume=$(shell pwd):/KaTeX ss

View File

@ -1,23 +1,21 @@
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [![Build Status](https://travis-ci.org/Khan/KaTeX.svg?branch=master)](https://travis-ci.org/Khan/KaTeX) # [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [![Build Status](https://travis-ci.org/Khan/KaTeX.svg?branch=master)](https://travis-ci.org/Khan/KaTeX)
[![Join the chat at https://gitter.im/Khan/KaTeX](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Khan/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php). * **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://jsperf.com/katex-vs-mathjax/).
* **Print quality:** KaTeXs layout is based on Donald Knuths TeX, the gold standard for math typesetting. * **Print quality:** KaTeXs layout is based on Donald Knuths TeX, the gold standard for math typesetting.
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources. * **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML. * **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, and IE 8 - IE 11. A list of supported commands can be found on the [wiki](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX). KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, and IE 8 - IE 11.
## Usage ## Usage
You can [download KaTeX](https://github.com/khan/katex/releases) and host it on your server or include the `katex.min.js` and `katex.min.css` files on your page directly from a CDN: You can [download KaTeX](https://github.com/khan/katex/releases) and host it on your server or include the `katex.min.js` and `katex.min.css` files on your page directly from a CDN:
```html ```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js" integrity="sha384-/y1Nn9+QQAipbNQWU65krzJralCnuOasHncUFXGkdwntGeSvQicrYkiUBwsgUqc1" crossorigin="anonymous"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.js"></script>
``` ```
#### In-browser rendering #### In-browser rendering
@ -46,19 +44,11 @@ Make sure to include the CSS and font files, but there is no need to include the
You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are: You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are:
- `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`) - `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
- `throwOnError`: `boolean`. If `true`, KaTeX will throw a `ParseError` when it encounters an unsupported command. If `false`, KaTeX will render the unsupported command as text in the color given by `errorColor`. (default: `true`)
- `errorColor`: `string`. A color string given in the format `"#XXX"` or `"#XXXXXX"`. This option determines the color which unsupported commands are rendered in. (default: `#cc0000`)
- `macros`: `object`. A collection of custom macros. Each macro is a property with a name like `\name` (written `"\\name"` in JavaScript) which maps to a string that describes the expansion of the macro.
For example: For example:
```js ```js
katex.render("c = \\pm\\sqrt{a^2 + b^2}\\in\\RR", element, { katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, { displayMode: true });
displayMode: true,
macros: {
"\\RR": "\\mathbb{R}"
}
});
``` ```
#### Automatic rendering of math on a page #### Automatic rendering of math on a page

View File

@ -1,9 +1,6 @@
{ {
"name": "katex", "name": "KaTeX",
"main": [ "version": "0.4.0",
"dist/katex.js",
"dist/katex.css"
],
"homepage": "http://khan.github.io/KaTeX/", "homepage": "http://khan.github.io/KaTeX/",
"description": "Fast math typesetting for the web.", "description": "Fast math typesetting for the web.",
"moduleType": [ "moduleType": [
@ -21,7 +18,6 @@
"/*.txt", "/*.txt",
"/*.js", "/*.js",
"/*.md", "/*.md",
"/*.sh",
"/package.json", "/package.json",
"/Makefile", "/Makefile",
"/build", "/build",

View File

@ -1,16 +0,0 @@
"use strict";
var v = process.version;
v = v.replace(/^v/,"");
v = v.split(".");
v = v.map(function(s){
return parseInt(s);
});
var a = v[0], b = v[1], c = v[2];
if (a < 6 || (a == 6 && b < 5)) {
console.error("Node 6.5 or later required for development. " +
"Version " + process.version + " found");
process.exit(1);
} else {
console.log("OK");
}

13
cli.js
View File

@ -1,15 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
// Simple CLI for KaTeX. // Simple CLI for KaTeX.
// Reads TeX from stdin, outputs HTML to stdout. // Reads TeX from stdin, outputs HTML to stdout.
/* eslint no-console:0 */
const katex = require("./"); var katex = require("./");
let input = ""; var input = "";
// Skip the first two args, which are just "node" and "cli.js" // Skip the first two args, which are just "node" and "cli.js"
const args = process.argv.slice(2); var args = process.argv.slice(2);
if (args.indexOf("--help") !== -1) { if (args.indexOf("--help") != -1) {
console.log(process.argv[0] + " " + process.argv[1] + console.log(process.argv[0] + " " + process.argv[1] +
" [ --help ]" + " [ --help ]" +
" [ --display-mode ]"); " [ --display-mode ]");
@ -26,7 +25,7 @@ process.stdin.on("data", function(chunk) {
}); });
process.stdin.on("end", function() { process.stdin.on("end", function() {
const options = { displayMode: args.indexOf("--display-mode") !== -1 }; var options = { displayMode: args.indexOf("--display-mode") != -1 };
const output = katex.renderToString(input, options); var output = katex.renderToString(input, options);
console.log(output); console.log(output);
}); });

View File

@ -6,4 +6,4 @@ $(BUILDDIR)/contrib/auto-render.min.js: $(BUILDDIR)/auto-render.js
$(UGLIFYJS) < $< > $@ $(UGLIFYJS) < $< > $@
$(BUILDDIR)/auto-render.js: auto-render.js $(BUILDDIR)/auto-render.js: auto-render.js
$(BROWSERIFY) -t [ babelify ] $< --standalone renderMathInElement > $@ $(BROWSERIFY) $< --standalone renderMathInElement > $@

View File

@ -10,7 +10,7 @@ This extension isn't part of KaTeX proper, so the script should be separately
included in the page: included in the page:
```html ```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/contrib/auto-render.min.js" integrity="sha384-dq1/gEHSxPZQ7DdrM82ID4YVol9BYyU7GbWlIwnwyPzotpoc57wDw/guX8EaYGPx" crossorigin="anonymous"></script> <script src="/path/to/auto-render.min.js"></script>
``` ```
Then, call the exposed `renderMathInElement` function in a script tag Then, call the exposed `renderMathInElement` function in a script tag
@ -27,22 +27,6 @@ before the close body tag:
See [index.html](index.html) for an example. See [index.html](index.html) for an example.
If you prefer to have all your setup inside the html `<head>`,
you can use the following script there
(instead of the one above at the end of the `<body>`):
```html
<head>
...
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body);
});
</script>
...
</head>
```
### API ### API
This extension exposes a single function, `window.renderMathInElement`, with This extension exposes a single function, `window.renderMathInElement`, with
@ -57,24 +41,23 @@ nodes inside this element and render the math in them.
`options` is an optional object argument with the following keys: `options` is an optional object argument with the following keys:
- `delimiters`: This is a list of delimiters to look for math. Each delimiter - `delimiters`: This is a list of delimiters to look for math. Each delimiter
has three properties: has three properties:
- `left`: A string which starts the math expression (i.e. the left delimiter). - `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). - `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 - `display`: A boolean of whether the math in the expression should be
rendered in display mode or not. rendered in display mode or not.
The default value is: The default value is:
```js
[
{left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}
]
```
```js - `ignoredTags`: This is a list of DOM node types to ignore when recursing
[ through. The default value is
{left: "$$", right: "$$", display: true}, `["script", "noscript", "style", "textarea", "pre", "code"]`.
{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

@ -4,22 +4,21 @@
/* global it: false */ /* global it: false */
/* global describe: false */ /* global describe: false */
const splitAtDelimiters = require("./splitAtDelimiters"); var splitAtDelimiters = require("./splitAtDelimiters");
beforeEach(function() { beforeEach(function() {
jasmine.addMatchers({ jasmine.addMatchers({
toSplitInto: function() { toSplitInto: function() {
return { return {
compare: function(actual, left, right, result) { compare: function(actual, left, right, result) {
const message = { var message = {
pass: true, pass: true,
message: "'" + actual + "' split correctly", message: "'" + actual + "' split correctly"
}; };
const startData = [{type: "text", data: actual}]; var startData = [{type: "text", data: actual}];
const split = var split = splitAtDelimiters(startData, left, right, false);
splitAtDelimiters(startData, left, right, false);
if (split.length !== result.length) { if (split.length !== result.length) {
message.pass = false; message.pass = false;
@ -30,12 +29,12 @@ beforeEach(function() {
return message; return message;
} }
for (let i = 0; i < split.length; i++) { for (var i = 0; i < split.length; i++) {
const real = split[i]; var real = split[i];
const correct = result[i]; var correct = result[i];
let good = true; var good = true;
let diff; var diff;
if (real.type !== correct.type) { if (real.type !== correct.type) {
good = false; good = false;
@ -59,9 +58,9 @@ beforeEach(function() {
} }
return message; return message;
}, }
}; };
}, }
}); });
}); });
@ -70,12 +69,12 @@ describe("A delimiter splitter", function() {
expect("hello").toSplitInto("(", ")", [{type: "text", data: "hello"}]); expect("hello").toSplitInto("(", ")", [{type: "text", data: "hello"}]);
}); });
it("doesn't create a math node with only one left delimiter", function() { it("doesn't create a math node when there's only a left delimiter", function() {
expect("hello ( world").toSplitInto( expect("hello ( world").toSplitInto(
"(", ")", "(", ")",
[ [
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "text", data: "( world"}, {type: "text", data: "( world"}
]); ]);
}); });
@ -83,7 +82,7 @@ describe("A delimiter splitter", function() {
expect("hello ) world").toSplitInto( expect("hello ) world").toSplitInto(
"(", ")", "(", ")",
[ [
{type: "text", data: "hello ) world"}, {type: "text", data: "hello ) world"}
]); ]);
}); });
@ -94,7 +93,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "( world )", display: false}, rawData: "( world )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
@ -105,7 +104,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "[[ world ]]", display: false}, rawData: "[[ world ]]", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
@ -119,7 +118,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: " boo "}, {type: "text", data: " boo "},
{type: "math", data: " more ", {type: "math", data: " more ",
rawData: "( more )", display: false}, rawData: "( more )", display: false},
{type: "text", data: " stuff"}, {type: "text", data: " stuff"}
]); ]);
}); });
@ -131,7 +130,7 @@ describe("A delimiter splitter", function() {
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "( world )", display: false}, rawData: "( world )", display: false},
{type: "text", data: " boo "}, {type: "text", data: " boo "},
{type: "text", data: "( left"}, {type: "text", data: "( left"}
]); ]);
}); });
@ -142,7 +141,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world { ) } ", {type: "math", data: " world { ) } ",
rawData: "( world { ) } )", display: false}, rawData: "( world { ) } )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
expect("hello ( world { { } ) } ) boo").toSplitInto( expect("hello ( world { { } ) } ) boo").toSplitInto(
@ -151,7 +150,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world { { } ) } ", {type: "math", data: " world { { } ) } ",
rawData: "( world { { } ) } )", display: false}, rawData: "( world { { } ) } )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
@ -162,7 +161,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world \\) ", {type: "math", data: " world \\) ",
rawData: "( world \\) )", display: false}, rawData: "( world \\) )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
/* TODO(emily): make this work maybe? /* TODO(emily): make this work maybe?
@ -172,7 +171,7 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello \\( "}, {type: "text", data: "hello \\( "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "( world )", display: false}, rawData: "( world )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
*/ */
}); });
@ -184,27 +183,27 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "$ world $", display: false}, rawData: "$ world $", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
it("remembers which delimiters are display-mode", function() { it("remembers which delimiters are display-mode", function() {
const startData = [{type: "text", data: "hello ( world ) boo"}]; var startData = [{type: "text", data: "hello ( world ) boo"}];
expect(splitAtDelimiters(startData, "(", ")", true)).toEqual( expect(splitAtDelimiters(startData, "(", ")", true)).toEqual(
[ [
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "( world )", display: true}, rawData: "( world )", display: true},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
it("works with more than one start datum", function() { it("works with more than one start datum", function() {
const startData = [ var startData = [
{type: "text", data: "hello ( world ) boo"}, {type: "text", data: "hello ( world ) boo"},
{type: "math", data: "math", rawData: "(math)", display: true}, {type: "math", data: "math", rawData: "(math)", display: true},
{type: "text", data: "hello ( world ) boo"}, {type: "text", data: "hello ( world ) boo"}
]; ];
expect(splitAtDelimiters(startData, "(", ")", false)).toEqual( expect(splitAtDelimiters(startData, "(", ")", false)).toEqual(
@ -217,15 +216,15 @@ describe("A delimiter splitter", function() {
{type: "text", data: "hello "}, {type: "text", data: "hello "},
{type: "math", data: " world ", {type: "math", data: " world ",
rawData: "( world )", display: false}, rawData: "( world )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"}
]); ]);
}); });
it("doesn't do splitting inside of math nodes", function() { it("doesn't do splitting inside of math nodes", function() {
const startData = [ var startData = [
{type: "text", data: "hello ( world ) boo"}, {type: "text", data: "hello ( world ) boo"},
{type: "math", data: "hello ( world ) boo", {type: "math", data: "hello ( world ) boo",
rawData: "(hello ( world ) boo)", display: true}, rawData: "(hello ( world ) boo)", display: true}
]; ];
expect(splitAtDelimiters(startData, "(", ")", false)).toEqual( expect(splitAtDelimiters(startData, "(", ")", false)).toEqual(
@ -235,7 +234,7 @@ describe("A delimiter splitter", function() {
rawData: "( world )", display: false}, rawData: "( world )", display: false},
{type: "text", data: " boo"}, {type: "text", data: " boo"},
{type: "math", data: "hello ( world ) boo", {type: "math", data: "hello ( world ) boo",
rawData: "(hello ( world ) boo)", display: true}, rawData: "(hello ( world ) boo)", display: true}
]); ]);
}); });
}); });

View File

@ -1,12 +1,11 @@
/* eslint no-console:0 */
/* global katex */ /* global katex */
const splitAtDelimiters = require("./splitAtDelimiters"); var splitAtDelimiters = require("./splitAtDelimiters");
const splitWithDelimiters = function(text, delimiters) { var splitWithDelimiters = function(text, delimiters) {
let data = [{type: "text", data: text}]; var data = [{type: "text", data: text}];
for (let i = 0; i < delimiters.length; i++) { for (var i = 0; i < delimiters.length; i++) {
const delimiter = delimiters[i]; var delimiter = delimiters[i];
data = splitAtDelimiters( data = splitAtDelimiters(
data, delimiter.left, delimiter.right, data, delimiter.left, delimiter.right,
delimiter.display || false); delimiter.display || false);
@ -14,20 +13,20 @@ const splitWithDelimiters = function(text, delimiters) {
return data; return data;
}; };
const renderMathInText = function(text, delimiters) { var renderMathInText = function(text, delimiters) {
const data = splitWithDelimiters(text, delimiters); var data = splitWithDelimiters(text, delimiters);
const fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
for (let i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
if (data[i].type === "text") { if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data)); fragment.appendChild(document.createTextNode(data[i].data));
} else { } else {
const span = document.createElement("span"); var span = document.createElement("span");
const math = data[i].data; var math = data[i].data;
try { try {
katex.render(math, span, { katex.render(math, span, {
displayMode: data[i].display, displayMode: data[i].display
}); });
} catch (e) { } catch (e) {
if (!(e instanceof katex.ParseError)) { if (!(e instanceof katex.ParseError)) {
@ -48,17 +47,17 @@ const renderMathInText = function(text, delimiters) {
return fragment; return fragment;
}; };
const renderElem = function(elem, delimiters, ignoredTags) { var renderElem = function(elem, delimiters, ignoredTags) {
for (let i = 0; i < elem.childNodes.length; i++) { for (var i = 0; i < elem.childNodes.length; i++) {
const childNode = elem.childNodes[i]; var childNode = elem.childNodes[i];
if (childNode.nodeType === 3) { if (childNode.nodeType === 3) {
// Text node // Text node
const frag = renderMathInText(childNode.textContent, delimiters); var frag = renderMathInText(childNode.textContent, delimiters);
i += frag.childNodes.length - 1; i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode); elem.replaceChild(frag, childNode);
} else if (childNode.nodeType === 1) { } else if (childNode.nodeType === 1) {
// Element node // Element node
const shouldRender = ignoredTags.indexOf( var shouldRender = ignoredTags.indexOf(
childNode.nodeName.toLowerCase()) === -1; childNode.nodeName.toLowerCase()) === -1;
if (shouldRender) { if (shouldRender) {
@ -69,26 +68,24 @@ const renderElem = function(elem, delimiters, ignoredTags) {
} }
}; };
const defaultOptions = { var defaultOptions = {
delimiters: [ delimiters: [
{left: "$$", right: "$$", display: true}, {left: "$$", right: "$$", display: true},
{left: "\\[", right: "\\]", display: true}, {left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}, {left: "\\(", right: "\\)", display: false}
// LaTeX uses this, but it ruins the display of normal `$` in text: // LaTeX uses this, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false}, // {left: "$", right: "$", display: false}
], ],
ignoredTags: [ ignoredTags: [
"script", "noscript", "style", "textarea", "pre", "code", "script", "noscript", "style", "textarea", "pre", "code"
], ]
}; };
const extend = function(obj) { var extend = function(obj) {
// Adapted from underscore.js' `_.extend`. See LICENSE.txt for license. // Adapted from underscore.js' `_.extend`. See LICENSE.txt for license.
let source; var source, prop;
let prop; for (var i = 1, length = arguments.length; i < length; i++) {
const length = arguments.length;
for (let i = 1; i < length; i++) {
source = arguments[i]; source = arguments[i];
for (prop in source) { for (prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) { if (Object.prototype.hasOwnProperty.call(source, prop)) {
@ -99,7 +96,7 @@ const extend = function(obj) {
return obj; return obj;
}; };
const renderMathInElement = function(elem, options) { var renderMathInElement = function(elem, options) {
if (!elem) { if (!elem) {
throw new Error("No element provided to render"); throw new Error("No element provided to render");
} }

View File

@ -1,14 +1,13 @@
/* eslint no-constant-condition:0 */ var findEndOfMath = function(delimiter, text, startIndex) {
const findEndOfMath = function(delimiter, text, startIndex) {
// Adapted from // Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
let index = startIndex; var index = startIndex;
let braceLevel = 0; var braceLevel = 0;
const delimLength = delimiter.length; var delimLength = delimiter.length;
while (index < text.length) { while (index < text.length) {
const character = text[index]; var character = text[index];
if (braceLevel <= 0 && if (braceLevel <= 0 &&
text.slice(index, index + delimLength) === delimiter) { text.slice(index, index + delimLength) === delimiter) {
@ -27,23 +26,23 @@ const findEndOfMath = function(delimiter, text, startIndex) {
return -1; return -1;
}; };
const splitAtDelimiters = function(startData, leftDelim, rightDelim, display) { var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
const finalData = []; var finalData = [];
for (let i = 0; i < startData.length; i++) { for (var i = 0; i < startData.length; i++) {
if (startData[i].type === "text") { if (startData[i].type === "text") {
const text = startData[i].data; var text = startData[i].data;
let lookingForLeft = true; var lookingForLeft = true;
let currIndex = 0; var currIndex = 0;
let nextIndex; var nextIndex;
nextIndex = text.indexOf(leftDelim); nextIndex = text.indexOf(leftDelim);
if (nextIndex !== -1) { if (nextIndex !== -1) {
currIndex = nextIndex; currIndex = nextIndex;
finalData.push({ finalData.push({
type: "text", type: "text",
data: text.slice(0, currIndex), data: text.slice(0, currIndex)
}); });
lookingForLeft = false; lookingForLeft = false;
} }
@ -57,7 +56,7 @@ const splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
finalData.push({ finalData.push({
type: "text", type: "text",
data: text.slice(currIndex, nextIndex), data: text.slice(currIndex, nextIndex)
}); });
currIndex = nextIndex; currIndex = nextIndex;
@ -78,7 +77,7 @@ const splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
rawData: text.slice( rawData: text.slice(
currIndex, currIndex,
nextIndex + rightDelim.length), nextIndex + rightDelim.length),
display: display, display: display
}); });
currIndex = nextIndex + rightDelim.length; currIndex = nextIndex + rightDelim.length;
@ -89,7 +88,7 @@ const splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
finalData.push({ finalData.push({
type: "text", type: "text",
data: text.slice(currIndex), data: text.slice(currIndex)
}); });
} else { } else {
finalData.push(startData[i]); finalData.push(startData[i]);

64
dist/README.md vendored Normal file
View File

@ -0,0 +1,64 @@
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [![Build Status](https://travis-ci.org/Khan/KaTeX.svg?branch=master)](https://travis-ci.org/Khan/KaTeX)
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://jsperf.com/katex-vs-mathjax/).
* **Print quality:** KaTeXs layout is based on Donald Knuths TeX, the gold standard for math typesetting.
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, and IE 8 - IE 11.
## Usage
You can [download KaTeX](https://github.com/khan/katex/releases) and host it on your server or include the `katex.min.js` and `katex.min.css` files on your page directly from a CDN:
```html
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.js"></script>
```
#### In-browser rendering
Call `katex.render` with a TeX expression and a DOM element to render into:
```js
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element);
```
If KaTeX can't parse the expression, it throws a `katex.ParseError` error.
#### Server side rendering or rendering to a string
To generate HTML on the server or to generate an HTML string of the rendered math, you can use `katex.renderToString`:
```js
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}");
// '<span class="katex">...</span>'
```
Make sure to include the CSS and font files, but there is no need to include the JavaScript. Like `render`, `renderToString` throws if it can't parse the expression.
#### Rendering options
You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are:
- `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
For example:
```js
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)
## License
KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT).

1
dist/contrib/auto-render.min.js vendored Normal file
View File

@ -0,0 +1 @@
(function(e){if("function"==typeof bootstrap)bootstrap("rendermathinelement",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeRenderMathInElement=e}else"undefined"!=typeof window?window.renderMathInElement=e():global.renderMathInElement=e()})(function(){var e,t,r,n,a;return function i(e,t,r){function n(o,l){if(!t[o]){if(!e[o]){var f=typeof require=="function"&&require;if(!l&&f)return f(o,!0);if(a)return a(o,!0);throw new Error("Cannot find module '"+o+"'")}var s=t[o]={exports:{}};e[o][0].call(s.exports,function(t){var r=e[o][1][t];return n(r?r:t)},s,s.exports,i,e,t,r)}return t[o].exports}var a=typeof require=="function"&&require;for(var o=0;o<r.length;o++)n(r[o]);return n}({1:[function(e,t,r){var n=e("./splitAtDelimiters");var a=function(e,t){var r=[{type:"text",data:e}];for(var a=0;a<t.length;a++){var i=t[a];r=n(r,i.left,i.right,i.display||false)}return r};var i=function(e,t){var r=a(e,t);var n=document.createDocumentFragment();for(var i=0;i<r.length;i++){if(r[i].type==="text"){n.appendChild(document.createTextNode(r[i].data))}else{var o=document.createElement("span");var l=r[i].data;try{katex.render(l,o,{displayMode:r[i].display})}catch(f){if(!(f instanceof katex.ParseError)){throw f}console.error("KaTeX auto-render: Failed to parse `"+r[i].data+"` with ",f);n.appendChild(document.createTextNode(r[i].rawData));continue}n.appendChild(o)}}return n};var o=function(e,t,r){for(var n=0;n<e.childNodes.length;n++){var a=e.childNodes[n];if(a.nodeType===3){var l=i(a.textContent,t);n+=l.childNodes.length-1;e.replaceChild(l,a)}else if(a.nodeType===1){var f=r.indexOf(a.nodeName.toLowerCase())===-1;if(f){o(a,t,r)}}}};var l={delimiters:[{left:"$$",right:"$$",display:true},{left:"\\[",right:"\\]",display:true},{left:"\\(",right:"\\)",display:false}],ignoredTags:["script","noscript","style","textarea","pre","code"]};var f=function(e){var t,r;for(var n=1,a=arguments.length;n<a;n++){t=arguments[n];for(r in t){if(Object.prototype.hasOwnProperty.call(t,r)){e[r]=t[r]}}}return e};var s=function(e,t){if(!e){throw new Error("No element provided to render")}t=f({},l,t);o(e,t.delimiters,t.ignoredTags)};t.exports=s},{"./splitAtDelimiters":2}],2:[function(e,t,r){var n=function(e,t,r){var n=r;var a=0;var i=e.length;while(n<t.length){var o=t[n];if(a<=0&&t.slice(n,n+i)===e){return n}else if(o==="\\"){n++}else if(o==="{"){a++}else if(o==="}"){a--}n++}return-1};var a=function(e,t,r,a){var i=[];for(var o=0;o<e.length;o++){if(e[o].type==="text"){var l=e[o].data;var f=true;var s=0;var d;d=l.indexOf(t);if(d!==-1){s=d;i.push({type:"text",data:l.slice(0,s)});f=false}while(true){if(f){d=l.indexOf(t,s);if(d===-1){break}i.push({type:"text",data:l.slice(s,d)});s=d}else{d=n(r,l,s+t.length);if(d===-1){break}i.push({type:"math",data:l.slice(s+t.length,d),rawData:l.slice(s,d+r.length),display:a});s=d+r.length}f=!f}i.push({type:"text",data:l.slice(s)})}else{i.push(e[o])}}return i};t.exports=a},{}]},{},[1])(1)});

BIN
dist/fonts/KaTeX_AMS-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_AMS-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_AMS-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_AMS-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Bold.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Bold.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Bold.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Bold.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Italic.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Italic.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Italic.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Italic.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Main-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-BoldItalic.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-BoldItalic.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-BoldItalic.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-BoldItalic.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Italic.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Italic.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Italic.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Italic.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Math-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size1-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size1-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size1-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size1-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size2-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size2-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size2-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size2-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size3-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size3-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size3-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size3-Regular.woff2 vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size4-Regular.eot vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size4-Regular.ttf vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size4-Regular.woff vendored Normal file

Binary file not shown.

BIN
dist/fonts/KaTeX_Size4-Regular.woff2 vendored Normal file

Binary file not shown.

1
dist/katex.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/katex.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,28 +2,8 @@ FROM ubuntu:14.04
MAINTAINER xymostech <xymostech@gmail.com> MAINTAINER xymostech <xymostech@gmail.com>
# Install things # Install things
RUN apt-get -qq update && apt-get -qqy install \ RUN apt-get -qq update
git \ RUN apt-get -qqy install git dvipng default-jre default-jdk texlive wget fontforge mftrace fonttools optipng advancecomp man-db build-essential unzip zlib1g-dev python-fontforge ruby woff-tools || true
dvipng \
default-jre \
default-jdk \
texlive \
wget \
fontforge \
mftrace \
fonttools \
optipng \
advancecomp \
man-db \
build-essential \
unzip \
zlib1g-dev \
python-fontforge \
ruby \
woff-tools \
pkg-config \
libharfbuzz-dev \
libfreetype6-dev || true
RUN gem install ttfunk --version 1.1.1 RUN gem install ttfunk --version 1.1.1
# Download yuicompressor # Download yuicompressor
@ -31,7 +11,7 @@ RUN mkdir /usr/share/yui-compressor/
RUN wget "https://github.com/yui/yuicompressor/releases/download/v2.4.8/yuicompressor-2.4.8.jar" -O /usr/share/yui-compressor/yui-compressor.jar RUN wget "https://github.com/yui/yuicompressor/releases/download/v2.4.8/yuicompressor-2.4.8.jar" -O /usr/share/yui-compressor/yui-compressor.jar
# Download batik-ttf2svg.jar # Download batik-ttf2svg.jar
RUN wget "https://archive.apache.org/dist/xmlgraphics/batik/batik-1.7.zip" RUN wget "http://supergsego.com/apache/xmlgraphics/batik/batik-1.7.zip"
RUN unzip -qq batik-1.7.zip RUN unzip -qq batik-1.7.zip
RUN mv batik-1.7/batik-ttf2svg.jar /usr/share/java/ RUN mv batik-1.7/batik-ttf2svg.jar /usr/share/java/
@ -42,13 +22,6 @@ RUN sed -i "1s/^/#include <cstddef>/" ttf2eot-0.0.2-2/OpenTypeUtilities.h
RUN make -C ttf2eot-0.0.2-2/ RUN make -C ttf2eot-0.0.2-2/
RUN mv ttf2eot-0.0.2-2/ttf2eot /usr/bin/ RUN mv ttf2eot-0.0.2-2/ttf2eot /usr/bin/
# Download and compile ttfautohint
RUN wget "http://download.savannah.gnu.org/releases/freetype/ttfautohint-1.3.tar.gz"
RUN tar -xzf ttfautohint-1.3.tar.gz
RUN cd ttfautohint-1.3/ && ./configure --without-qt
RUN make -C ttfautohint-1.3/
RUN mv ttfautohint-1.3/frontend/ttfautohint /usr/bin
# Download and compile woff2_compress # Download and compile woff2_compress
RUN git clone "https://code.google.com/p/font-compression-reference/" woff2_compress RUN git clone "https://code.google.com/p/font-compression-reference/" woff2_compress
RUN make -C woff2_compress/woff2/ RUN make -C woff2_compress/woff2/

View File

@ -0,0 +1,14 @@
FROM ubuntu:14.04
MAINTAINER xymostech <xymostech@gmail.com>
RUN apt-get -qq update
RUN apt-get -qqy install default-jre=2:1.7-51 firefox=28.0+build2-0ubuntu2 xvfb=2:1.15.1-0ubuntu2 wget=1.15-1ubuntu1 python=2.7.5-5ubuntu3 python-pip=1.5.4-1 nodejs=0.10.25~dfsg2-2ubuntu1 || true
RUN wget http://selenium-release.storage.googleapis.com/2.43/selenium-server-standalone-2.43.0.jar
RUN ln -s /usr/bin/nodejs /usr/bin/node
RUN pip install selenium pypng
ENV DISPLAY :1
CMD /bin/bash ~/run.sh
RUN echo "java -jar /selenium-server-standalone-2.43.0.jar > /dev/null &" >> ~/run.sh
RUN echo "Xvfb :1 2> /dev/null &" >> ~/run.sh
RUN echo "make -C /KaTeX serve > /dev/null &" >> ~/run.sh
RUN echo "sleep 2" >> ~/run.sh
RUN echo "/KaTeX/dockers/Screenshotter/screenshotter.py /KaTeX/test/screenshotter/ss_data.json" >> ~/run.sh

View File

@ -1,63 +1,29 @@
# How to generate screenshotter images ### How to generate screenshotter images
----------------------------------------
## Automatic generation of screen shots
Now you too can generate screenshots from your own computer, and (hopefully) Now you too can generate screenshots from your own computer, and (hopefully)
have them look mostly the same as the current ones! Make sure you have docker have them look mostly the same as the current ones! To start, make a docker
installed and running. image from the included Dockerfile using a command like
If all you want is (re)create
all the snapshots for all the browsers, then you can do so by running the
`screenshotter.sh` script:
dockers/Screenshotter/screenshotter.sh docker build --tag=ss .
It will fetch all required selenium docker images, and use them to from within this directory (note you need to have docker installed and running
take screenshots. for this to work). This will build a docker image with the `ss` tag, which you
can then use to run dockers based on it.
## Manual generation This Dockerfile is set up such that it will run everything and generate all the
screenshots when the docker is run, so no interactive input is required. All
that you need to do is mount the KaTeX directory you want to test into the
`/KaTeX` directory in the docker, and run the `ss` docker, like so:
If you are creating screenshots on a regular basis, you can keep the docker run --volume=/your/KaTeX/:/KaTeX ss
docker containers with the selenium setups running. Essentially you
are encouraged to reproduce the steps from `screenshotter.sh`
manually. Example run for Firefox:
container=$(docker run -d -P selenium/standalone-firefox:2.46.0) The `--volume=/your/KaTeX:/KaTeX` switch mounts your KaTeX directory into the
node dockers/Screenshotter/screenshotter.js -b firefox -c ${container} docker. Note this is a read-write mounting, so the new screenshots will be
# possibly repeat the above command as often as you need, then eventually directly placed into your KaTeX directory.
docker stop ${container}
docker rm ${container}
For Chrome, simply replace both occurrences of `firefox` with `chrome`. Since this docker is very self-contained, there should be no need to do
interactive management of the docker, but if you feel the need, you can read the
General Docker Help section of the MathJaxFonts docker readme.
## Use without docker That's it!
It is possible to run `screenshotter.js` without the use of Docker:
npm install selenium-webdriver
node dockers/Screenshotter/screenshotter.js
This will generate screenshots using the Firefox installed on your system.
Browsers other than Firefox can be targeted using the `--browser` option.
For a complete list of options pass `--help` as an argument to
`screenshotter.js`. Using these it should be possible to have the script
connect to almost any Selenium web driver you might have access to.
Note that screenshots taken without Docker are very likely to disagree
from the ones stored in the repository, due to different versions of
various software components being used. The screenshots taken in this
fashion are well suited for visual inspection, but for exact binary
comparisons it would be neccessary to carefully set up the environment
to match the one used by the Docker approach.
## Choosing the list of test cases
Both `screenshotter.js` and `screenshotter.sh` will accept
an `--include` option (short `-i`) which can be used to specify
a list of test cases to be processed, as a comma separated list.
Conversely, the `--exclude` option (short `-x`) can be used
to specify a list of cases which are not being processed.
Examples:
node dockers/Screenshotter/screenshotter.js -i Sqrt,SqrtRoot
dockers/Screenshotter/screenshotter.sh --exclude=GreekLetters

View File

@ -1,465 +0,0 @@
/* eslint no-console:0, prefer-spread:0 */
"use strict";
const childProcess = require("child_process");
const fs = require("fs");
const http = require("http");
const jspngopt = require("jspngopt");
const net = require("net");
const os = require("os");
const pako = require("pako");
const path = require("path");
const selenium = require("selenium-webdriver");
const firefox = require("selenium-webdriver/firefox");
const app = require("../../server");
const data = require("../../test/screenshotter/ss_data");
const dstDir = path.normalize(
path.join(__dirname, "..", "..", "test", "screenshotter", "images"));
//////////////////////////////////////////////////////////////////////
// Process command line arguments
const opts = require("nomnom")
.option("browser", {
abbr: "b",
"default": "firefox",
help: "Name of the browser to use",
})
.option("container", {
abbr: "c",
type: "string",
help: "Name or ID of a running docker container to contact",
})
.option("seleniumURL", {
full: "selenium-url",
help: "Full URL of the Selenium web driver",
})
.option("seleniumIP", {
full: "selenium-ip",
help: "IP address of the Selenium web driver",
})
.option("seleniumPort", {
full: "selenium-port",
"default": 4444,
help: "Port number of the Selenium web driver",
})
.option("katexURL", {
full: "katex-url",
help: "Full URL of the KaTeX development server",
})
.option("katexIP", {
full: "katex-ip",
help: "Full URL of the KaTeX development server",
})
.option("katexPort", {
full: "katex-port",
help: "Port number of the KaTeX development server",
})
.option("include", {
abbr: "i",
help: "Comma-separated list of test cases to process",
})
.option("exclude", {
abbr: "x",
help: "Comma-separated list of test cases to exclude",
})
.option("verify", {
flag: true,
help: "Check whether screenshot matches current file content",
})
.option("wait", {
help: "Wait this many seconds between page load and screenshot",
})
.parse();
let listOfCases;
if (opts.include) {
listOfCases = opts.include.split(",");
} else {
listOfCases = Object.keys(data);
}
if (opts.exclude) {
const exclude = opts.exclude.split(",");
listOfCases = listOfCases.filter(function(key) {
return exclude.indexOf(key) === -1;
});
}
let seleniumURL = opts.seleniumURL;
let seleniumIP = opts.seleniumIP;
let seleniumPort = opts.seleniumPort;
let katexURL = opts.katexURL;
let katexIP = opts.katexIP;
let katexPort = opts.katexPort;
//////////////////////////////////////////////////////////////////////
// Work out connection to selenium docker container
function check(err) {
if (!err) {
return;
}
console.error(err);
console.error(err.stack);
process.exit(1);
}
function cmd() {
const args = Array.prototype.slice.call(arguments);
const cmd = args.shift();
return childProcess.execFileSync(
cmd, args, { encoding: "utf-8" }).replace(/\n$/, "");
}
function guessDockerIPs() {
if (process.env.DOCKER_MACHINE_NAME) {
const machine = process.env.DOCKER_MACHINE_NAME;
seleniumIP = seleniumIP || cmd("docker-machine", "ip", machine);
katexIP = katexIP || cmd("docker-machine", "ssh", machine,
"echo ${SSH_CONNECTION%% *}");
return;
}
try {
// When using boot2docker, seleniumIP and katexIP are distinct.
seleniumIP = seleniumIP || cmd("boot2docker", "ip");
let config = cmd("boot2docker", "config");
config = (/^HostIP = "(.*)"$/m).exec(config);
if (!config) {
console.error("Failed to find HostIP");
process.exit(2);
}
katexIP = katexIP || config[1];
return;
} catch (e) {
// Apparently no boot2docker, continue
}
if (!process.env.DOCKER_HOST && os.type() === "Darwin") {
// Docker for Mac
seleniumIP = seleniumIP || "localhost";
katexIP = katexIP || "*any*"; // see findHostIP
return;
}
// Native Docker on Linux or remote Docker daemon or similar
const gatewayIP = cmd("docker", "inspect",
"-f", "{{.NetworkSettings.Gateway}}", opts.container);
seleniumIP = seleniumIP || gatewayIP;
katexIP = katexIP || gatewayIP;
}
if (!seleniumURL && opts.container) {
if (!seleniumIP || !katexIP) {
guessDockerIPs();
}
seleniumPort = cmd("docker", "port", opts.container, seleniumPort);
seleniumPort = seleniumPort.replace(/^.*:/, "");
}
if (!seleniumURL && seleniumIP) {
seleniumURL = "http://" + seleniumIP + ":" + seleniumPort + "/wd/hub";
}
if (seleniumURL) {
console.log("Selenium driver at " + seleniumURL);
} else {
console.log("Selenium driver in local session");
}
process.nextTick(startServer);
let attempts = 0;
//////////////////////////////////////////////////////////////////////
// Start up development server
let devServer = null;
const minPort = 32768;
const maxPort = 61000;
function startServer() {
if (katexURL || katexPort) {
process.nextTick(tryConnect);
return;
}
const port = Math.floor(Math.random() * (maxPort - minPort)) + minPort;
const server = http.createServer(app).listen(port);
server.once("listening", function() {
devServer = server;
katexPort = port;
attempts = 0;
process.nextTick(tryConnect);
});
server.on("error", function(err) {
if (devServer !== null) { // error after we started listening
throw err;
} else if (++attempts > 50) {
throw new Error("Failed to start up dev server");
} else {
process.nextTick(startServer);
}
});
}
//////////////////////////////////////////////////////////////////////
// Wait for container to become ready
function tryConnect() {
if (!seleniumIP) {
process.nextTick(buildDriver);
return;
}
const sock = net.connect({
host: seleniumIP,
port: +seleniumPort,
});
sock.on("connect", function() {
sock.end();
attempts = 0;
process.nextTick(buildDriver);
}).on("error", function() {
if (++attempts > 50) {
throw new Error("Failed to connect selenium server.");
}
setTimeout(tryConnect, 200);
});
}
//////////////////////////////////////////////////////////////////////
// Build the web driver
let driver;
function buildDriver() {
const builder = new selenium.Builder().forBrowser(opts.browser);
const ffProfile = new firefox.Profile();
ffProfile.setPreference(
"browser.startup.homepage_override.mstone", "ignore");
ffProfile.setPreference("browser.startup.page", 0);
const ffOptions = new firefox.Options().setProfile(ffProfile);
builder.setFirefoxOptions(ffOptions);
if (seleniumURL) {
builder.usingServer(seleniumURL);
}
driver = builder.build();
driver.manage().timeouts().setScriptTimeout(3000).then(function() {
let html = '<!DOCTYPE html>' +
'<html><head><style type="text/css">html,body{' +
'width:100%;height:100%;margin:0;padding:0;overflow:hidden;' +
'}</style></head><body><p>Test</p></body></html>';
html = "data:text/html," + encodeURIComponent(html);
return driver.get(html);
}).then(function() {
setSize(targetW, targetH);
});
}
//////////////////////////////////////////////////////////////////////
// Set the screen size
const targetW = 1024;
const targetH = 768;
function setSize(reqW, reqH) {
return driver.manage().window().setSize(reqW, reqH).then(function() {
return driver.takeScreenshot();
}).then(function(img) {
img = imageDimensions(img);
const actualW = img.width;
const actualH = img.height;
if (actualW === targetW && actualH === targetH) {
findHostIP();
return;
}
if (++attempts > 5) {
throw new Error("Failed to set window size correctly.");
}
return setSize(targetW + reqW - actualW, targetH + reqH - actualH);
}, check);
}
function imageDimensions(img) {
const buf = new Buffer(img, "base64");
return {
buf: buf,
width: buf.readUInt32BE(16),
height: buf.readUInt32BE(20),
};
}
//////////////////////////////////////////////////////////////////////
// Work out how to connect to host KaTeX server
function findHostIP() {
if (!katexIP) {
katexIP = "localhost";
}
if (katexIP !== "*any*" || katexURL) {
if (!katexURL) {
katexURL = "http://" + katexIP + ":" + katexPort + "/babel/";
console.log("KaTeX URL is " + katexURL);
}
process.nextTick(takeScreenshots);
return;
}
// Now we need to find an IP the container can connect to.
// First, install a server component to get notified of successful connects
app.get("/ss-connect.js", function(req, res, next) {
if (!katexURL) {
katexIP = req.query.ip;
katexURL = "http://" + katexIP + ":" + katexPort + "/babel/";
console.log("KaTeX URL is " + katexURL);
process.nextTick(takeScreenshots);
}
res.setHeader("Content-Type", "text/javascript");
res.send("//OK");
});
// Next, enumerate all network addresses
const ips = [];
const devs = os.networkInterfaces();
for (const dev in devs) {
if (devs.hasOwnProperty(dev)) {
const addrs = devs[dev];
for (let i = 0; i < addrs.length; ++i) {
let addr = addrs[i].address;
if (/:/.test(addr)) {
addr = "[" + addr + "]";
}
ips.push(addr);
}
}
}
console.log("Looking for host IP among " + ips.join(", "));
// Load a data: URI document which attempts to contact each of these IPs
let html = "<!doctype html>\n<html><body>\n";
html += ips.map(function(ip) {
return '<script src="http://' + ip + ':' + katexPort +
'/ss-connect.js?ip=' + encodeURIComponent(ip) +
'" defer></script>';
}).join("\n");
html += "\n</body></html>";
html = "data:text/html," + encodeURIComponent(html);
driver.get(html);
}
//////////////////////////////////////////////////////////////////////
// Take the screenshots
let countdown = listOfCases.length;
let exitStatus = 0;
const listOfFailed = [];
function takeScreenshots() {
listOfCases.forEach(takeScreenshot);
}
function takeScreenshot(key) {
const itm = data[key];
if (!itm) {
console.error("Test case " + key + " not known!");
listOfFailed.push(key);
if (exitStatus === 0) {
exitStatus = 1;
}
oneDone();
return;
}
let file = path.join(dstDir, key + "-" + opts.browser + ".png");
let retry = 0;
let loadExpected = null;
if (opts.verify) {
loadExpected = promisify(fs.readFile, file);
}
const url = katexURL + "test/screenshotter/test.html?" + itm.query;
driver.get(url);
if (opts.wait) {
browserSideWait(1000 * opts.wait);
}
driver.takeScreenshot().then(haveScreenshot).then(oneDone, check);
function haveScreenshot(img) {
img = imageDimensions(img);
if (img.width !== targetW || img.height !== targetH) {
throw new Error("Excpected " + targetW + " x " + targetH +
", got " + img.width + "x" + img.height);
}
if (key === "Lap" && opts.browser === "firefox" &&
img.buf[0x32] === 0xf8) {
/* There is some strange non-determinism with this case,
* causing slight vertical shifts. The first difference
* is at offset 0x32, where one file has byte 0xf8 and
* the other has something else. By using a different
* output file name for one of these cases, we accept both.
*/
key += "_alt";
file = path.join(dstDir, key + "-" + opts.browser + ".png");
if (loadExpected) {
loadExpected = promisify(fs.readFile, file);
}
}
const opt = new jspngopt.Optimizer({
pako: pako,
});
const buf = opt.bufferSync(img.buf);
if (loadExpected) {
return loadExpected.then(function(expected) {
if (!buf.equals(expected)) {
if (++retry === 5) {
console.error("FAIL! " + key);
listOfFailed.push(key);
exitStatus = 3;
} else {
console.log("error " + key);
driver.get(url);
browserSideWait(500 * retry);
return driver.takeScreenshot().then(haveScreenshot);
}
} else {
console.log("* ok " + key);
}
});
} else {
return promisify(fs.writeFile, file, buf).then(function() {
console.log(key);
});
}
}
function oneDone() {
if (--countdown === 0) {
if (listOfFailed.length) {
console.error("Failed: " + listOfFailed.join(" "));
}
// devServer.close(cb) will take too long.
process.exit(exitStatus);
}
}
}
// Wait using a timeout call in the browser, to ensure that the wait
// time doesn't start before the page has reportedly been loaded.
function browserSideWait(milliseconds) {
// The last argument (arguments[1] here) is the callback to selenium
return driver.executeAsyncScript(
"window.setTimeout(arguments[1], arguments[0]);",
milliseconds);
}
// Turn node callback style into a call returning a promise,
// like Q.nfcall but using Selenium promises instead of Q ones.
// Second and later arguments are passed to the function named in the
// first argument, and a callback is added as last argument.
function promisify(f) {
const args = Array.prototype.slice.call(arguments, 1);
const deferred = new selenium.promise.Deferred();
args.push(function(err, val) {
if (err) {
deferred.reject(err);
} else {
deferred.fulfill(val);
}
});
f.apply(null, args);
return deferred.promise;
}

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python2
import argparse
import json
import os
import png
import StringIO
import sys
from selenium import webdriver
def get_png_size(png_data):
w, h, _, _ = png.Reader(file=StringIO.StringIO(png_data)).read()
return (w, h)
def set_driver_size(driver, width, height):
"""Correctly sets the size of the driver window so screenshots end up the
provided size"""
driver.set_window_size(width, height)
screenshot_size = get_png_size(driver.get_screenshot_as_png())
attempts = 0
while (width, height) != screenshot_size:
attempts += 1
if attempts > 5:
print "Tried 5 times to size screen correctly, bailing out"
exit(1)
ss_width, ss_height = screenshot_size
driver.set_window_size(
width + (width - ss_width),
height + (height - ss_height))
screenshot_size = get_png_size(driver.get_screenshot_as_png())
def main():
parser = argparse.ArgumentParser(
description='Take screenshots of webpages', add_help=False)
parser.add_argument('file', metavar='file.json')
parser.add_argument('-t', '--tests', metavar='test', nargs='*')
parser.add_argument('-w', '--width', metavar='width', default=1024,
type=int)
parser.add_argument('-h', '--height', metavar='height', default=768,
type=int)
parser.add_argument('-b', '--browser', metavar='browser',
choices=['firefox'], default='firefox')
args = parser.parse_args()
data = None
with open(args.file) as f:
try:
data = json.load(f)
except ValueError:
print "Invalid json in input file:", args.file
exit(1)
tests = []
if args.tests is None:
tests = data.keys()
else:
data_tests = data.keys()
for test in args.tests:
if test not in data_tests:
print "Unknown test:", test
exit(1)
tests = args.tests
print "Starting up"
sys.stdout.flush()
driver = None
if args.browser == 'firefox':
driver = webdriver.Firefox()
else:
print "Unknown browser:", args.browser
exit(1)
set_driver_size(driver, args.width, args.height)
data_dir = os.path.join(
os.path.dirname(os.path.realpath(args.file)), "images")
try:
os.mkdir(data_dir)
except OSError:
pass
for test, url in data.iteritems():
if test in tests:
filename = os.path.join(
data_dir, '%s-%s.png' % (test, args.browser))
print "Running:", test
sys.stdout.flush()
driver.get(url)
driver.get_screenshot_as_file(filename)
print "Done"
if __name__ == '__main__':
main()

View File

@ -1,49 +0,0 @@
#!/bin/bash
# This script does a one-shot creation of screenshots, creating needed
# docker containers and removing them afterwards. During development,
# it might be desirable to avoid the overhead for starting and
# stopping the containers. Developers are encouraged to manage
# suitable containers themselves, calling the screenshotter.js script
# directly.
cleanup() {
[[ "${container}" ]] \
&& docker stop "${container}" >/dev/null \
&& docker rm "${container}" >/dev/null
container=
}
SS_DIR=$(dirname "$0")
TOP_DIR=${SS_DIR}/../..
FONTS_DIR=${TOP_DIR}/test/screenshotter/unicode-fonts
if [[ ! -d "${FONTS_DIR}" ]]; then
echo "Cloning test fonts repository"
git clone https://github.com/Khan/KaTeX-test-fonts "${FONTS_DIR}" \
|| exit 2
fi
pushd "${FONTS_DIR}" || exit 2
git checkout --detach 99fa66a2da643218754c8236b9f9151cac71ba7c || exit 2
popd || exit 2
container=
trap cleanup EXIT
status=0
for browserTag in firefox:2.48.2 chrome:2.48.2; do
browser=${browserTag%:*}
image=selenium/standalone-${browserTag}
echo "Starting container for ${image}"
container=$(docker run -d -P ${image})
[[ ${container} ]] || continue
echo "Container ${container:0:12} started, creating screenshots..."
if node "$(dirname "$0")"/screenshotter.js \
--browser="${browser}" --container="${container}" "$@"; then
res=Done
else
res=Failed
status=1
fi
echo "${res} taking screenshots, stopping and removing ${container:0:12}"
cleanup
done
exit ${status}

View File

@ -1,11 +0,0 @@
# convert from PDF to PNG with -flatten looks really bad on 14.04 LTS
FROM ubuntu:15.04
MAINTAINER Martin von Gagern <gagern@ma.tum.de>
# Disable regular updates, but keep security updates
RUN sed -i 's/^\(deb.*updates\)/#\1/' /etc/apt/sources.list && apt-get update
# Install all required packages, but try not to pull in TOO much
RUN apt-get -qy --no-install-recommends install \
texlive-latex-base etoolbox imagemagick ghostscript nodejs

View File

@ -1,82 +0,0 @@
# How to compare against LaTeX
The tools in this directory can be used to create reference images
using LaTeX, and to compare them against the screenshots taken from a
browser.
## Execution environment
### Docker environment
If you don't want to ensure the presence of all required tools, or
want to make sure that you create reproducible results, simply run
dockers/texcmp/texcmp.sh
from the root of your KaTeX directory tree.
This will build a suitable docker image unless such an image already
exists. It will then use a container based on that image to generate
all the images described below.
Note that the files and directories created in the source tree from
within the docker will be owned by root, so you might have trouble
deleting them later on. Be sure you can obtain superuser permissions
on your computer or know someone who can, just to be safe.
### Native environment
If you want to avoid the overhead of creating a docker container, or
the even larger overhead of setting up docker and creating the initial
image, then you may instead execute the commands
cd dockers/texcmp
npm install
node texcmp.js
from the root of your KaTeX directory tree. Required tools include the
`pdflatex` tool of a standard TeX distribution as well as the
`convert` tool from ImageMagick.
Note that this approach will use `/tmp/texcmp` as a temporary directory.
The use of a single directory name here can lead to conflicts if
multiple developers on the same machine try to use that directory.
Also note that different software configurations can lead to different results,
so if reproducibility is desired, the Docker approach should be chosen.
## Generated files
After running either of the above commands, you will find two
(possibly new) subdirectories inside `test/screenshotter`,
called `tex` and `diff`.
### Rasterized documents
`test/screenshotter/tex` will contain images created by `pdflatex` by
plugging the test case formula in question into the template
`test/screenshotter/test.tex`. This is essentially our reference of
how LaTeX renders a given input.
### Difference images
`test/screenshotter/diff` will contain images depicting the difference
between the LaTeX rendering and the Firefox screenshot. Black areas
indicate overlapping print. Green areas are black in LaTeX but white
in Firefox, while it's the other way round for red areas. Colored
input is first converted to grayscale, before being subject to the
coloring just described. The pictures will be aligned in such a way
as to maximize the overlap between the two versions (i.e. the amount
of black output). The result will then be trimmed so it can easily be
pasted into bug reports.
## Command line arguments
Both `texcmp.sh` and `texcmp.js` will accept the names of test cases
on the command line. This can be useful if one particular test case
is affected by current development, so that the effects on it can be
seen more quickly.
Examples:
dockers/texcmp/texcmp.sh Sqrt SqrtRoot
node dockers/texcmp/texcmp.js Baseline

View File

@ -1,10 +0,0 @@
{
"name": "texcmp",
"description": "KaTeX helper to compare LaTeX output against screenshots",
"license": "MIT",
"dependencies": {
"ndarray-fft": "1.0.0",
"pngparse": "2.0.1",
"q": "1.4.1"
}
}

View File

@ -1,255 +0,0 @@
/* eslint-env node, es6 */
/* eslint-disable no-console */
"use strict";
const childProcess = require("child_process");
const fs = require("fs");
const path = require("path");
const Q = require("q"); // To debug, pass Q_DEBUG=1 in the environment
const pngparse = require("pngparse");
const fft = require("ndarray-fft");
const ndarray = require("ndarray-fft/node_modules/ndarray");
const data = require("../../test/screenshotter/ss_data");
// Adapt node functions to Q promises
const readFile = Q.denodeify(fs.readFile);
const writeFile = Q.denodeify(fs.writeFile);
const mkdir = Q.denodeify(fs.mkdir);
let todo;
if (process.argv.length > 2) {
todo = process.argv.slice(2);
} else {
todo = Object.keys(data).filter(function(key) {
return !data[key].nolatex;
});
}
// Dimensions used when we do the FFT-based alignment computation
const alignWidth = 2048; // should be at least twice the width resp. height
const alignHeight = 2048; // of the screenshots, and a power of two.
// Compute required resolution to match test.html. 16px default font,
// scaled to 4em in test.html, and to 1.21em in katex.css. Corresponding
// LaTeX font size is 10pt. There are 72.27pt per inch.
const pxPerEm = 16 * 4 * 1.21;
const pxPerPt = pxPerEm / 10;
const dpi = pxPerPt * 72.27;
const tmpDir = "/tmp/texcmp";
const ssDir = path.normalize(
path.join(__dirname, "..", "..", "test", "screenshotter"));
const imagesDir = path.join(ssDir, "images");
const teximgDir = path.join(ssDir, "tex");
const diffDir = path.join(ssDir, "diff");
let template;
Q.all([
readFile(path.join(ssDir, "test.tex"), "utf-8"),
ensureDir(tmpDir),
ensureDir(teximgDir),
ensureDir(diffDir),
]).spread(function(data) {
template = data;
// dirs have been created, template has been read, now rasterize.
return Q.all(todo.map(processTestCase));
}).done();
// Process a single test case: rasterize, then create diff
function processTestCase(key) {
const itm = data[key];
let tex = "$" + itm.tex + "$";
if (itm.display) {
tex = "\\[" + itm.tex + "\\]";
}
if (itm.pre) {
tex = itm.pre.replace("<br>", "\\\\") + tex;
}
if (itm.post) {
tex = tex + itm.post.replace("<br>", "\\\\");
}
tex = template.replace(/\$.*\$/, tex.replace(/\$/g, "$$$$"));
const texFile = path.join(tmpDir, key + ".tex");
const pdfFile = path.join(tmpDir, key + ".pdf");
const pngFile = path.join(teximgDir, key + "-pdflatex.png");
const browserFile = path.join(imagesDir, key + "-firefox.png");
const diffFile = path.join(diffDir, key + ".png");
// Step 1: write key.tex file
const fftLatex = writeFile(texFile, tex).then(function() {
// Step 2: call "pdflatex key" to create key.pdf
return execFile("pdflatex", [
"-interaction", "nonstopmode", key,
], {cwd: tmpDir});
}).then(function() {
console.log("Typeset " + key);
// Step 3: call "convert ... key.pdf key.png" to create key.png
return execFile("convert", [
"-density", dpi, "-units", "PixelsPerInch", "-flatten",
"-depth", "8", pdfFile, pngFile,
]);
}).then(function() {
console.log("Rasterized " + key);
// Step 4: apply FFT to that
return readPNG(pngFile).then(fftImage);
});
// Step 5: apply FFT to reference image as well
const fftBrowser = readPNG(browserFile).then(fftImage);
return Q.all([fftBrowser, fftLatex]).spread(function(browser, latex) {
// Now we have the FFT result from both
// Step 6: find alignment which maximizes overlap.
// This uses a FFT-based correlation computation.
let x;
let y;
const real = createMatrix();
const imag = createMatrix();
// Step 6a: (real + i*imag) = latex * conjugate(browser)
for (y = 0; y < alignHeight; ++y) {
for (x = 0; x < alignWidth; ++x) {
const br = browser.real.get(y, x);
const bi = browser.imag.get(y, x);
const lr = latex.real.get(y, x);
const li = latex.imag.get(y, x);
real.set(y, x, br * lr + bi * li);
imag.set(y, x, br * li - bi * lr);
}
}
// Step 6b: (real + i*imag) = inverseFFT(real + i*imag)
fft(-1, real, imag);
// Step 6c: find position where the (squared) absolute value is maximal
let offsetX = 0;
let offsetY = 0;
let maxSquaredNorm = -1; // any result is greater than initial value
for (y = 0; y < alignHeight; ++y) {
for (x = 0; x < alignWidth; ++x) {
const or = real.get(y, x);
const oi = imag.get(y, x);
const squaredNorm = or * or + oi * oi;
if (maxSquaredNorm < squaredNorm) {
maxSquaredNorm = squaredNorm;
offsetX = x;
offsetY = y;
}
}
}
// Step 6d: Treat negative offsets in a non-cyclic way
if (offsetY > (alignHeight / 2)) {
offsetY -= alignHeight;
}
if (offsetX > (alignWidth / 2)) {
offsetX -= alignWidth;
}
console.log("Positioned " + key + ": " + offsetX + ", " + offsetY);
// Step 7: use these offsets to compute difference illustration
const bx = Math.max(offsetX, 0); // browser left padding
const by = Math.max(offsetY, 0); // browser top padding
const lx = Math.max(-offsetX, 0); // latex left padding
const ly = Math.max(-offsetY, 0); // latex top padding
const uw = Math.max(browser.width + bx, latex.width + lx); // union w.
const uh = Math.max(browser.height + by, latex.height + ly); // u. h.
return execFile("convert", [
// First image: latex rendering, converted to grayscale and padded
"(", pngFile, "-grayscale", "Rec709Luminance",
"-extent", uw + "x" + uh + "-" + lx + "-" + ly,
")",
// Second image: browser screenshot, to grayscale and padded
"(", browserFile, "-grayscale", "Rec709Luminance",
"-extent", uw + "x" + uh + "-" + bx + "-" + by,
")",
// Third image: the per-pixel minimum of the first two images
"(", "-clone", "0-1", "-compose", "darken", "-composite", ")",
// First image is red, second green, third blue channel of result
"-channel", "RGB", "-combine",
"-trim", // remove everything with the same color as the corners
diffFile, // output file name
]);
}).then(function() {
console.log("Compared " + key);
});
}
// Create a directory, but ignore error if the directory already exists.
function ensureDir(dir) {
return mkdir(dir).fail(function(err) {
if (err.code !== "EEXIST") {
throw err;
}
});
}
// Execute a given command, and return a promise to its output.
// Don't denodeify here, since fail branch needs access to stderr.
function execFile(cmd, args, opts) {
const deferred = Q.defer();
childProcess.execFile(cmd, args, opts, function(err, stdout, stderr) {
if (err) {
console.error("Error executing " + cmd + " " + args.join(" "));
console.error(stdout + stderr);
err.stdout = stdout;
err.stderr = stderr;
deferred.reject(err);
} else {
deferred.resolve(stdout);
}
});
return deferred.promise;
}
// Read given file and parse it as a PNG file.
function readPNG(file) {
const deferred = Q.defer();
const onerror = deferred.reject.bind(deferred);
const stream = fs.createReadStream(file);
stream.on("error", onerror);
pngparse.parseStream(stream, function(err, image) {
if (err) {
console.log("Failed to load " + file);
onerror(err);
return;
}
deferred.resolve(image);
});
return deferred.promise;
}
// Take a parsed image data structure and apply FFT transformation to it
function fftImage(image) {
const real = createMatrix();
const imag = createMatrix();
let idx = 0;
const nchan = image.channels;
const alphachan = 1 - (nchan % 2);
const colorchan = nchan - alphachan;
for (let y = 0; y < image.height; ++y) {
for (let x = 0; x < image.width; ++x) {
let v = 0;
for (let c = 0; c < colorchan; ++c) {
v += 255 - image.data[idx++];
}
for (let c = 0; c < alphachan; ++c) {
v += image.data[idx++];
}
real.set(y, x, v);
}
}
fft(1, real, imag);
return {
real: real,
imag: imag,
width: image.width,
height: image.height,
};
}
// Create a new matrix of preconfigured dimensions, initialized to zero
function createMatrix() {
const array = new Float64Array(alignWidth * alignHeight);
return new ndarray(array, [alignWidth, alignHeight]);
}

View File

@ -1,17 +0,0 @@
#!/bin/bash
set -x
imgname=katex/texcmp
tag=1.1
imgid=$(docker images | awk "/${imgname//\//\\/} *${tag//./\\.}/{print \$3}")
cd "$(dirname "$0")" || exit $?
npm install || exit $?
if [[ -z ${imgid} ]]; then
docker build -t "${imgname}:${tag}" . || exit $?
fi
base=$(cd ../..; pwd)
docker run --rm \
-v "${base}":/KaTeX \
-w /KaTeX/dockers/texcmp \
"${imgname}:${tag}" \
nodejs texcmp.js "$@"

View File

@ -1,4 +1,3 @@
/* eslint no-console:0 */
/** /**
* This is the main entry point for KaTeX. Here, we expose functions for * This is the main entry point for KaTeX. Here, we expose functions for
* rendering expressions either to DOM nodes or to markup strings. * rendering expressions either to DOM nodes or to markup strings.
@ -7,24 +6,24 @@
* errors in the expression, or errors in javascript handling. * errors in the expression, or errors in javascript handling.
*/ */
const ParseError = require("./src/ParseError"); var ParseError = require("./src/ParseError");
const Settings = require("./src/Settings"); var Settings = require("./src/Settings");
const buildTree = require("./src/buildTree"); var buildTree = require("./src/buildTree");
const parseTree = require("./src/parseTree"); var parseTree = require("./src/parseTree");
const utils = require("./src/utils"); var utils = require("./src/utils");
/** /**
* Parse and build an expression, and place that expression in the DOM node * Parse and build an expression, and place that expression in the DOM node
* given. * given.
*/ */
let render = function(expression, baseNode, options) { var render = function(expression, baseNode, options) {
utils.clearNode(baseNode); utils.clearNode(baseNode);
const settings = new Settings(options); var settings = new Settings(options);
const tree = parseTree(expression, settings); var tree = parseTree(expression, settings);
const node = buildTree(tree, expression, settings).toNode(); var node = buildTree(tree, expression, settings).toNode();
baseNode.appendChild(node); baseNode.appendChild(node);
}; };
@ -46,18 +45,18 @@ if (typeof document !== "undefined") {
/** /**
* Parse and build an expression, and return the markup for that. * Parse and build an expression, and return the markup for that.
*/ */
const renderToString = function(expression, options) { var renderToString = function(expression, options) {
const settings = new Settings(options); var settings = new Settings(options);
const tree = parseTree(expression, settings); var tree = parseTree(expression, settings);
return buildTree(tree, expression, settings).toMarkup(); return buildTree(tree, expression, settings).toMarkup();
}; };
/** /**
* Parse an expression and return the parse tree. * Parse an expression and return the parse tree.
*/ */
const generateParseTree = function(expression, options) { var generateParseTree = function(expression, options) {
const settings = new Settings(options); var settings = new Settings(options);
return parseTree(expression, settings); return parseTree(expression, settings);
}; };
@ -70,5 +69,5 @@ module.exports = {
* to change. Use at your own risk. * to change. Use at your own risk.
*/ */
__parse: generateParseTree, __parse: generateParseTree,
ParseError: ParseError, ParseError: ParseError
}; };

View File

@ -3,7 +3,6 @@
# Autogenerated code # Autogenerated code
build/** build/**
node_modules/** node_modules/**
dist/**
# Third party code # Third party code
test/jasmine/** test/jasmine/**

View File

@ -7,15 +7,15 @@ There are several requirements for generating the metrics used by KaTeX.
this by running `tex --version`, and seeing if it has a line that looks like this by running `tex --version`, and seeing if it has a line that looks like
> kpathsea version 6.2.0 > kpathsea version 6.2.0
- You need the JSON module for perl. You can install this either from CPAN - You need the JSON module for perl. You can install this either from CPAN or with
(possibly using the `cpan` command line tool) or with your package manager. your package manager.
- You need the python module fonttools. You can install this either from PyPi - You need the python fontforge module. This is probably either installed with
(using `easy_install` or `pip`) or with your package manager. fontforge or can be installed from your package manager.
Once you have these things, run Once you have these things, run
make metrics make metrics
which should generate new metrics and place them into `fontMetricsData.json`. which should generate new metrics and place them into `fontMetrics.js`. You're
You're done! done!

View File

@ -31,11 +31,7 @@ def main():
'cmsy10.tfm', 'cmsy10.tfm',
'cmti10.tfm', 'cmti10.tfm',
'msam10.tfm', 'msam10.tfm',
'msbm10.tfm', 'msbm10.tfm'
'eufm10.tfm',
'cmtt10.tfm',
'rsfs10.tfm',
'cmss10.tfm',
] ]
# Extracted by running `\font\a=<font>` and then `\showthe\skewchar\a` in # Extracted by running `\font\a=<font>` and then `\showthe\skewchar\a` in
@ -52,11 +48,7 @@ def main():
'cmsy10': 48, 'cmsy10': 48,
'cmti10': None, 'cmti10': None,
'msam10': None, 'msam10': None,
'msbm10': None, 'msbm10': None
'eufm10': None,
'cmtt10': None,
'rsfs10': None,
'cmss10': None,
} }
font_name_to_tfm = {} font_name_to_tfm = {}
@ -76,16 +68,11 @@ def main():
tex_char_num = int(char_data['char']) tex_char_num = int(char_data['char'])
yshift = float(char_data['yshift']) yshift = float(char_data['yshift'])
if family == "Script-Regular": tfm_char = font_name_to_tfm[font].get_char_metrics(tex_char_num)
tfm_char = font_name_to_tfm[font].get_char_metrics(tex_char_num,
fix_rsfs=True)
else:
tfm_char = font_name_to_tfm[font].get_char_metrics(tex_char_num)
height = round(tfm_char.height + yshift / 1000.0, 5) height = round(tfm_char.height + yshift / 1000.0, 5)
depth = round(tfm_char.depth - yshift / 1000.0, 5) depth = round(tfm_char.depth - yshift / 1000.0, 5)
italic = round(tfm_char.italic_correction, 5) italic = round(tfm_char.italic_correction, 5)
width = round(tfm_char.width, 5)
skewkern = 0.0 skewkern = 0.0
if (font_skewchar[font] and if (font_skewchar[font] and
@ -98,7 +85,6 @@ def main():
'depth': depth, 'depth': depth,
'italic': italic, 'italic': italic,
'skew': skewkern, 'skew': skewkern,
'width': width
} }
sys.stdout.write( sys.stdout.write(

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from fontTools.ttLib import TTFont import fontforge
import sys import sys
import json import json
@ -60,53 +60,36 @@ def main():
start_json = json.load(sys.stdin) start_json = json.load(sys.stdin)
for font, chars in metrics_to_extract.iteritems(): for font, chars in metrics_to_extract.iteritems():
fontInfo = TTFont("../static/fonts/KaTeX_" + font + ".ttf") fontInfo = fontforge.open("../static/fonts/KaTeX_" + font + ".ttf")
glyf = fontInfo["glyf"]
unitsPerEm = float(fontInfo["head"].unitsPerEm)
# We keep ALL Unicode cmaps, not just fontInfo["cmap"].getcmap(3, 1). for glyph in fontInfo.glyphs():
# This is playing it extra safe, since it reports inconsistencies. try:
# Platform 0 is Unicode, platform 3 is Windows. For platform 3, char = unichr(glyph.unicode)
# encoding 1 is UCS-2 and encoding 10 is UCS-4. except ValueError:
cmap = [t.cmap for t in fontInfo["cmap"].tables
if (t.platformID == 0)
or (t.platformID == 3 and t.platEncID in (1, 10))]
for char, base_char in chars.iteritems():
code = ord(char)
names = set(t.get(code) for t in cmap)
if not names:
sys.stderr.write(
"Codepoint {} of font {} maps to no name\n"
.format(code, font))
continue continue
if len(names) != 1:
sys.stderr.write(
"Codepoint {} of font {} maps to multiple names: {}\n"
.format(code, font, ", ".join(sorted(names))))
continue
name = names.pop()
height = depth = italic = skew = width = 0 if char in chars:
glyph = glyf[name] _, depth, _, height = glyph.boundingBox()
if glyph.numberOfContours:
height = glyph.yMax
depth = -glyph.yMin
width = glyph.xMax - glyph.xMin
if base_char:
base_char_str = str(ord(base_char))
base_metrics = start_json[font][base_char_str]
italic = base_metrics["italic"]
skew = base_metrics["skew"]
width = base_metrics["width"]
start_json[font][str(code)] = { depth = -depth
"height": height / unitsPerEm,
"depth": depth / unitsPerEm, base_char = chars[char]
"italic": italic, if base_char:
"skew": skew, base_char_str = str(ord(base_char))
"width": width base_metrics = start_json[font][base_char_str]
}
italic = base_metrics["italic"]
skew = base_metrics["skew"]
else:
italic = 0
skew = 0
start_json[font][ord(char)] = {
"height": height / fontInfo.em,
"depth": depth / fontInfo.em,
"italic": italic,
"skew": skew,
}
sys.stdout.write( sys.stdout.write(
json.dumps(start_json, separators=(',', ':'), sort_keys=True)) json.dumps(start_json, separators=(',', ':'), sort_keys=True))

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python
import sys
import json
props = ['depth', 'height', 'italic', 'skew']
if len(sys.argv) > 1:
if sys.argv[1] == '--width':
props.append('width')
data = json.load(sys.stdin)
sep = "module.exports = {\n "
for font in sorted(data):
sys.stdout.write(sep + json.dumps(font))
sep = ": {\n "
for glyph in sorted(data[font], key=int):
sys.stdout.write(sep + json.dumps(glyph) + ": ")
values = [value if value != 0.0 else 0 for value in
[data[font][glyph][key] for key in props]]
sys.stdout.write(json.dumps(values))
sep = ",\n "
sep = ",\n },\n "
sys.stdout.write(",\n },\n};\n")

View File

@ -135,6 +135,8 @@ $map{cmmi10} = {
0x2E => 0x25B9, # \triangleright 0x2E => 0x25B9, # \triangleright
0x2F => 0x25C3, # \triangleleft 0x2F => 0x25C3, # \triangleleft
0x3A => 0x2E, # .
0x3B => 0x2C, # ,
0x3C => 0x3C, # < 0x3C => 0x3C, # <
0x3D => 0x2215, # / 0x3D => 0x2215, # /
0x3E => 0x3E, # > 0x3E => 0x3E, # >
@ -146,23 +148,16 @@ $map{cmmi10} = {
0x5F => 0x2322, # \frown 0x5F => 0x2322, # \frown
0x60 => 0x2113, # \ell 0x60 => 0x2113, # \ell
0x7D => 0x2118, # \wp
0x7E => [0x20D7,-653,0],# \vec
],
"Main-Italic" => [
0x7B => 0x131, # \imath 0x7B => 0x131, # \imath
0x7C => 0x237, # \jmath 0x7C => 0x237, # \jmath
], 0x7D => 0x2118, # \wp
0x7E => [0x20D7,-653,0],# \vec
"Caligraphic-Regular" => [ ]
[0x30,0x39] => 0x30, # Oldstyle 0-9
],
}; };
$map{cmsy10} = { $map{cmsy10} = {
"Main-Regular" => [ "Main-Regular" => [
0 => 0x2212, # - [0,1] => 0x2212, # -
1 => 0x22C5, # \cdot 1 => 0x22C5, # \cdot
2 => 0xD7, # \times 2 => 0xD7, # \times
3 => 0x2217, # \ast 3 => 0x2217, # \ast
@ -261,10 +256,6 @@ $map{cmsy10} = {
"Math-Italic" => [ "Math-Italic" => [
0x36 => 0x2F # \not 0x36 => 0x2F # \not
], ],
"Caligraphic-Regular" => [
[0x41,0x5A] => 0x41, # A-Z
],
}; };
$map{cmex10} = { $map{cmex10} = {
@ -434,6 +425,8 @@ $map{cmti10} = {
[7,8] => 0x3A5, # \Upsilon, \Phi [7,8] => 0x3A5, # \Upsilon, \Phi
[9,0xA] => 0x3A8, # \Psi, \Omega [9,0xA] => 0x3A8, # \Psi, \Omega
0x10 => 0x131, # \imath (roman)
0x11 => 0x237, # \jmath (roman)
0x12 => [0x300,-511,0], # \grave (combining) 0x12 => [0x300,-511,0], # \grave (combining)
0x13 => [0x301,-511,0], # \acute (combining) 0x13 => [0x301,-511,0], # \acute (combining)
0x14 => [0x30C,-511,0], # \check (combining) 0x14 => [0x30C,-511,0], # \check (combining)
@ -564,6 +557,8 @@ $map{cmmib10} = {
0x2E => 0x25B9, # \triangleright 0x2E => 0x25B9, # \triangleright
0x2F => 0x25C3, # \triangleleft 0x2F => 0x25C3, # \triangleleft
0x3A => 0x2E, # .
0x3B => 0x2C, # ,
0x3C => 0x3C, # < 0x3C => 0x3C, # <
0x3D => 0x2215, # / 0x3D => 0x2215, # /
0x3E => 0x3E, # > 0x3E => 0x3E, # >
@ -576,6 +571,8 @@ $map{cmmib10} = {
0x60 => 0x2113, # \ell 0x60 => 0x2113, # \ell
0x68 => 0x210F, # \hbar (bar added below) 0x68 => 0x210F, # \hbar (bar added below)
0x7B => 0x131, # \imath
0x7C => 0x237, # \jmath
0x7D => 0x2118, # \wp 0x7D => 0x2118, # \wp
0x7E => [0x20D7,-729,0],# \vec 0x7E => [0x20D7,-729,0],# \vec
], ],
@ -583,7 +580,7 @@ $map{cmmib10} = {
$map{cmbsy10} = { $map{cmbsy10} = {
"Main-Bold" => [ "Main-Bold" => [
0 => 0x2212, # - [0,1] => 0x2212, # -
1 => 0x22C5, # \cdot 1 => 0x22C5, # \cdot
2 => 0xD7, # \times 2 => 0xD7, # \times
3 => 0x2217, # \ast 3 => 0x2217, # \ast
@ -947,106 +944,6 @@ $map{msbm10} = {
], ],
}; };
$map{eufm10} = {
"Fraktur-Regular" => [
[0,7] => 0xE300, # variants
0x12 => 0x2018, # left quote
0x13 => 0x2019, # right quote
0x21 => 0x21, # !
[0x26,0x2F] => 0x26, # &, ', (, ), *, +, comma, -, ., /
[0x30,0x39] => 0x30, # 0-9
[0x3A,0x3B] => 0x3A, # :, ;
0x3D => 0x3D, # =
0x3F => 0x3F, # ?
[0x41,0x5A] => 0x41, # A-Z
0x5B => 0x5B, # [
[0x5D,0x5E] => 0x5D, # ], ^
[0x61,0x7A] => 0x61, # a-z
0x7D => 0x22, # "
],
};
$map{cmtt10} = {
"Typewriter-Regular" => [
[0,1] => 0x393, # \Gamma, \Delta
2 => 0x398, # \Theta
3 => 0x39B, # \Lambda
4 => 0x39E, # \Xi
5 => 0x3A0, # \Pi
6 => 0x3A3, # \Sigma
[7,8] => 0x3A5, # \Upsilon, \Phi
[9,0xA] => 0x3A8, # \Psi, \Omega
0xD => 0x2032, # '
0x10 => 0x131, # \imath (roman)
0x11 => 0x237, # \jmath (roman)
0x12 => [0x300,-525,0], # \grave (combining)
0x13 => [0x301,-525,0], # \acute (combining)
0x14 => [0x30C,-525,0], # \check (combining)
0x15 => [0x306,-525,0], # \breve (combining)
0x16 => [0x304,-525,0], # \bar (combining)
0x17 => [0x30A,-525,0], # ring above (combining)
[0x21,0x7F] => 0x21,
0x27 => 2018, # left quote
0x60 => 2019, # right quote
0x5E => [0x302,-525,0], # \hat (combining)
0x7E => [0x303,-525,0], # \tilde (combining)
0x7F => [0x308,-525,0], # \ddot (combining)
],
};
$map{rsfs10} = {
"Script-Regular" => [
[0x41,0x5A] => 0x41, # A-Z
],
};
$map{cmss10} = {
"SansSerif-Regular" => [
[0,1] => 0x393, # \Gamma, \Delta
2 => 0x398, # \Theta
3 => 0x39B, # \Lambda
4 => 0x39E, # \Xi
5 => 0x3A0, # \Pi
6 => 0x3A3, # \Sigma
[7,8] => 0x3A5, # \Upsilon, \Phi
[9,0xA] => 0x3A8, # \Psi, \Omega
0x10 => 0x131, # \imath (roman)
0x11 => 0x237, # \jmath (roman)
0x12 => [0x300,-500,0], # \grave (combining)
0x13 => [0x301,-500,0], # \acute (combining)
0x14 => [0x30C,-500,0], # \check (combining)
0x15 => [0x306,-500,0], # \breve (combining)
0x16 => [0x304,-500,0], # \bar (combining)
0x17 => [0x30A,-542,0], # ring above (combining)
[0x21,0x2F] => 0x21, # !, ", #, $, %, &, ', (, ), *, +, comma, -, ., /
0x22 => 0x201D, # "
0x27 => 0x2019, # '
[0x30,0x39] => 0x30, # 0-9
[0x3A,0x3B] => 0x3A, # :, ;
0x3D => 0x3D, # =
[0x3F,0x40] => 0x3F, # ?, @
[0x41,0x5A] => 0x41, # A-Z
0x5B => 0x5B, # [
0x5C => 0x201C, # ``
[0x5D,0x5E] => 0x5D, # ], ^
0x5E => [0x302,-500,0], # \hat (combining)
0x5F => [0x307,-389,0], # \dot (combining)
0x60 => 0x2018, # `
[0x61,0x7A] => 0x61, # a-z
[0x7B,0x7C] => 0x2013, # \endash, \emdash
0x7B => [0x5F,0,-350], # underline
0x7D => [0x30B,-500,0], # double acute (combining)
0x7E => [0x7E,0,-350], # ~
0x7E => [0x303,-500,0], # \tilde (combining)
0x7F => [0x308,-500,0], # \ddot (combining)
],
};
foreach $cmfont (keys %map) { foreach $cmfont (keys %map) {
foreach $mjfont (keys %{$map{$cmfont}}) { foreach $mjfont (keys %{$map{$cmfont}}) {
$style = $mjfont; $style =~ s/.*?(-|$)//; $style = "Regular" unless $style; $style = $mjfont; $style =~ s/.*?(-|$)//; $style = "Regular" unless $style;
@ -1076,12 +973,6 @@ sub add_to_output {
"yshift" => $yshift "yshift" => $yshift
}; };
if (defined($output{$mjfont}{$to})) {
print STDERR "Duplicate mapping $to for $mjfont: " .
$output{$mjfont}{$to}{font} . ":" .
$output{$mjfont}{$to}{char} . " vs. $cmfont:$from\n";
die "Duplicate mapping!"; # disable this line to see all of them
}
$output{$mjfont}{$to} = $data; $output{$mjfont}{$to} = $data;
} }

View File

@ -64,21 +64,11 @@ class TfmFile(object):
self.ligkern_program = LigKernProgram(ligkern_table) self.ligkern_program = LigKernProgram(ligkern_table)
self.kern_table = kern_table self.kern_table = kern_table
def get_char_metrics(self, char_num, fix_rsfs=False): def get_char_metrics(self, char_num):
"""Return glyph metrics for a unicode code point.
Arguments:
char_num: a unicode code point
fix_rsfs: adjust for rsfs10.tfm's different indexing system
"""
if char_num < self.start_char or char_num > self.end_char: if char_num < self.start_char or char_num > self.end_char:
raise RuntimeError("Invalid character number") raise RuntimeError("Invalid character number")
if fix_rsfs: info = self.char_info[char_num + self.start_char]
# all of the char_nums contained start from zero in rsfs10.tfm
info = self.char_info[char_num - self.start_char]
else:
info = self.char_info[char_num + self.start_char]
char_kern_table = {} char_kern_table = {}
if info.has_ligkern(): if info.has_ligkern():

17
metrics/replace_line.py Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env python2
import sys
with open("../src/fontMetrics.js", "r") as metrics:
old_lines = file.readlines(metrics)
replace = sys.stdin.read()
with open("../src/fontMetrics.js", "w") as output:
for line in old_lines:
if line.startswith("var metricMap"):
output.write("var metricMap = ")
output.write(replace)
output.write(";\n")
else:
output.write(line)

View File

@ -1,6 +1,6 @@
{ {
"name": "katex", "name": "katex",
"version": "0.8.0-pre", "version": "0.4.0",
"description": "Fast math typesetting for the web.", "description": "Fast math typesetting for the web.",
"main": "katex.js", "main": "katex.js",
"repository": { "repository": {
@ -10,36 +10,21 @@
"files": [ "files": [
"katex.js", "katex.js",
"cli.js", "cli.js",
"src/", "src/"
"dist/"
], ],
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"babel-plugin-transform-runtime": "^6.15.0", "browserify": "~2.29.1",
"babel-preset-es2015": "^6.18.0", "clean-css": "~2.2.15",
"babelify": "^7.3.0", "express": "~3.3.3",
"browserify": "^13.3.0", "jasmine-node": "2.0.0-beta4",
"clean-css": "^3.4.23", "jshint": "^2.5.6",
"eslint": "^3.13.0", "less": "~1.7.5",
"express": "^4.14.0", "uglify-js": "~2.4.15"
"glob": "^7.1.1",
"jasmine": "^2.3.2",
"jasmine-core": "^2.3.4",
"js-yaml": "^3.3.1",
"jspngopt": "^0.2.0",
"less": "~2.7.1",
"morgan": "^1.7.0",
"nomnom": "^1.8.1",
"object-assign": "^4.1.0",
"pako": "1.0.4",
"selenium-webdriver": "^2.48.2",
"sri-toolbox": "^0.2.0",
"uglify-js": "~2.7.5"
}, },
"bin": "cli.js", "bin": "cli.js",
"scripts": { "scripts": {
"test": "make lint test", "test": "make lint test"
"prepublish": "make NIS= dist"
}, },
"dependencies": { "dependencies": {
"match-at": "^0.1.0" "match-at": "^0.1.0"

View File

@ -1,165 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
shopt -s extglob
VERSION=
NEXT_VERSION=
BRANCH=$(git rev-parse --abbrev-ref HEAD)
NARGS=0
DRY_RUN=
INSANE=0
# usage [ERROR-MESSAGES...] EXIT-STATUS
usage() {
while [[ $# -gt 1 ]]; do
echo "$1" >&2
shift
done
echo "Usage:"
echo "./release.sh [OPTIONS] <VERSION_TO_RELEASE> [NEXT_VERSION]"
echo ""
echo "Options:"
echo " --dry-run|-n: only print commands, do not execute them."
echo ""
echo "Examples:"
echo " When releasing a new point release:"
echo " ./release.sh 0.6.3 0.6.4"
echo " When releasing a new major version:"
echo " ./release.sh 0.7.0 0.8.0"
echo ""
echo "You may omit NEXT_VERSION in order to avoid creating a commit on"
echo "the branch from which the release was created. Not recommended."
exit $1
}
while [ $# -gt 0 ]; do
case "$1" in
--dry-run|-n|--just-print)
DRY_RUN=true
git() { echo "git $*"; }
npm() { echo "npm $*"; }
;;
-h|-\?|--help)
usage 0
;;
-*)
usage "Unknown option: $1" "" 1
;;
*)
case "$NARGS" in
0)
VERSION="$1"
NARGS=1
;;
1)
NEXT_VERSION="$1"
NARGS=2
;;
*)
usage "Too many arguments: $1" "" 1
;;
esac
;;
esac
shift
done
if [[ $NARGS = 0 ]]; then
usage "Missing argument: version number" "" 1
fi
# Some sanity checks up front
if ! command git diff --stat --exit-code HEAD; then
echo "Please make sure you have no uncommitted changes" >&2
: $((++INSANE))
fi
if ! command npm owner ls katex | grep -q "^$(command npm whoami) <"; then
echo "You don't seem do be logged into npm, use \`npm login\`" >&2
: $((++INSANE))
fi
if [[ $BRANCH != @(v*|master) ]]; then
echo "'$BRANCH' does not like a release branch to me" >&2
: $((++INSANE))
fi
if [[ -z "$NEXT_VERSION" ]]; then
echo "About to release $VERSION from $BRANCH. "
else
echo "About to release $VERSION from $BRANCH and bump to $NEXT_VERSION-pre."
fi
if [[ $INSANE != 0 ]]; then
read -r -p "$INSANE sanity check(s) failed, really proceed? [y/n] " CONFIRM
else
read -r -p "Look good? [y/n] " CONFIRM
fi
if [[ "$CONFIRM" != "y" ]]; then
exit 1
fi
# Make a new detached HEAD
git checkout "$BRANCH"
git pull
git checkout --detach
# Build generated files and add them to the repository (for bower)
git clean -fdx build dist
make setup dist
sed -i.bak -E '/^\/dist\/$/d' .gitignore
rm -f .gitignore.bak
git add .gitignore dist/
# Edit package.json and bower.json to the right version (see
# http://stackoverflow.com/a/22084103 for why we need the .bak file to make
# this mac & linux compatible)
sed -i.bak -E 's|"version": "[^"]+",|"version": "'$VERSION'",|' package.json
rm -f package.json.bak
# Update the version number in CDN URLs included in the README files,
# and regenerate the Subresource Integrity hash for these files.
node update-sri.js "${VERSION}" README.md contrib/*/README.md dist/README.md
# Make the commit and tag, and push them.
git add package.json bower.json README.md contrib/*/README.md dist/README.md
git commit -n -m "v$VERSION"
git diff --stat --exit-status # check for uncommitted changes
git tag -a "v$VERSION" -m "v$VERSION"
git push origin "v$VERSION"
# Update npm (bower and cdnjs update automatically)
npm publish
if [ ! -z "$NEXT_VERSION" ]; then
# Go back to original branch to bump
git checkout "$BRANCH"
# Edit package.json and bower.json to the right version
sed -i.bak -E 's|"version": "[^"]+",|"version": "'$NEXT_VERSION'-pre",|' package.json
rm -f package.json.bak
# Refer to the just-released version in the documentation of the
# development branch, too. Most people will read docs on master.
git checkout "v${VERSION}" -- README.md contrib/*/README.md
git add package.json bower.json
git commit -n -m "Bump $BRANCH to v$NEXT_VERSION-pre"
git push origin "$BRANCH"
# Go back to the tag which has build/katex.tar.gz and build/katex.zip
git checkout "v$VERSION"
fi
echo ""
echo "The automatic parts are done!"
echo "Now all that's left is to create the release on github."
echo "Visit https://github.com/Khan/KaTeX/releases/new?tag=v$VERSION to edit the release notes"
echo "Don't forget to upload build/katex.tar.gz and build/katex.zip to the release!"
if [[ ${DRY_RUN} ]]; then
echo ""
echo "This was a dry run."
echo "Operations using git or npm were printed not executed."
echo "Some files got modified, though, so you might want to undo "
echo "these changes now, e.g. using \`git checkout -- .\` or similar."
echo ""
fi

106
server.js
View File

@ -1,42 +1,27 @@
/* eslint no-console:0 */ var fs = require("fs");
const fs = require("fs"); var path = require("path");
const path = require("path");
const babelify = require("babelify"); var browserify = require("browserify");
const browserify = require("browserify"); var express = require("express");
const express = require("express"); var less = require("less");
const glob = require("glob");
const less = require("less");
const app = express(); var app = express();
if (require.main === module) { app.use(express.logger());
app.use(require("morgan")(
":date[iso] :method :url HTTP/:http-version - :status"));
}
function serveBrowserified(file, standaloneName, doBabelify) { var serveBrowserified = function(file, standaloneName) {
return function(req, res, next) { return function(req, res, next) {
let files; var b = browserify();
if (Array.isArray(file)) { b.add(file);
files = file.map(function(f) { return path.join(__dirname, f); });
} else if (file.indexOf("*") !== -1) {
files = glob.sync(file, {cwd: __dirname});
} else {
files = [path.join(__dirname, file)];
}
const options = {}; var options = {};
if (doBabelify) {
options.transform = [babelify];
}
if (standaloneName) { if (standaloneName) {
options.standalone = standaloneName; options.standalone = standaloneName;
} }
const b = browserify(files, options);
const stream = b.bundle();
let body = ""; var stream = b.bundle(options);
var body = "";
stream.on("data", function(s) { body += s; }); stream.on("data", function(s) { body += s; });
stream.on("error", function(e) { next(e); }); stream.on("error", function(e) { next(e); });
stream.on("end", function() { stream.on("end", function() {
@ -44,59 +29,42 @@ function serveBrowserified(file, standaloneName, doBabelify) {
res.send(body); res.send(body);
}); });
}; };
} };
function twoBrowserified(url, file, standaloneName) { app.get("/katex.js", serveBrowserified("./katex", "katex"));
app.get(url, serveBrowserified(file, standaloneName, false)); app.get("/test/katex-spec.js", serveBrowserified("./test/katex-spec"));
app.get("/babel" + url, serveBrowserified(file, standaloneName, true)); app.get("/contrib/auto-render/auto-render.js",
} serveBrowserified("./contrib/auto-render/auto-render",
"renderMathInElement"));
function twoUse(url, handler) { app.get("/katex.css", function(req, res, next) {
app.use(url, handler); fs.readFile("static/katex.less", {encoding: "utf8"}, function(err, data) {
app.use("/babel" + url, handler);
}
function twoStatic(url, file) {
twoUse(url, express.static(path.join(__dirname, file)));
}
twoBrowserified("/katex.js", "katex", "katex");
twoUse("/test/jasmine", express.static(path.dirname(
require.resolve("jasmine-core/lib/jasmine-core/jasmine.js"))));
twoBrowserified("/test/katex-spec.js", "test/*[Ss]pec.js");
twoBrowserified(
"/contrib/auto-render/auto-render.js",
"contrib/auto-render/auto-render",
"renderMathInElement");
twoUse("/katex.css", function(req, res, next) {
const lessfile = path.join(__dirname, "static", "katex.less");
fs.readFile(lessfile, {encoding: "utf8"}, function(err, data) {
if (err) { if (err) {
next(err); next(err);
return; return;
} }
less.render(data, { var parser = new less.Parser({
paths: [path.join(__dirname, "static")], paths: ["./static"],
filename: "katex.less", filename: "katex.less"
}, function(err, output) { });
parser.parse(data, function(err, tree) {
if (err) { if (err) {
console.error(String(err));
next(err); next(err);
return; return;
} }
res.setHeader("Content-Type", "text/css"); res.setHeader("Content-Type", "text/css");
res.send(output.css); res.send(tree.toCSS());
}); });
}); });
}); });
twoStatic("", "static"); app.use(express["static"](path.join(__dirname, "static")));
twoStatic("", "build"); app.use(express["static"](path.join(__dirname, "build")));
twoStatic("/test", "test"); app.use("/test", express["static"](path.join(__dirname, "test")));
twoStatic("/contrib", "contrib"); app.use("/contrib", express["static"](path.join(__dirname, "contrib")));
app.use(function(err, req, res, next) { app.use(function(err, req, res, next) {
console.error(err.stack); console.error(err.stack);
@ -104,9 +72,5 @@ app.use(function(err, req, res, next) {
res.send(500, err.stack); res.send(500, err.stack);
}); });
if (require.main === module) { app.listen(7936);
app.listen(7936); console.log("Serving on http://0.0.0.0:7936/ ...");
console.log("Serving on http://0.0.0.0:7936/ ...");
}
module.exports = app;

View File

@ -11,99 +11,184 @@
* kinds. * kinds.
*/ */
const matchAt = require("match-at"); var matchAt = require("match-at");
const ParseError = require("./ParseError"); var ParseError = require("./ParseError");
// The main lexer class // The main lexer class
function Lexer(input) { function Lexer(input) {
this.input = input; this._input = input;
this.pos = 0;
} }
/** // The resulting token returned from `lex`.
* The resulting token returned from `lex`. function Token(text, data, position) {
*
* It consists of the token text plus some position information.
* The position information is essentially a range in an input string,
* but instead of referencing the bare input string, we refer to the lexer.
* That way it is possible to attach extra metadata to the input string,
* like for example a file name or similar.
*
* The position information (all three parameters) is optional,
* so it is OK to construct synthetic tokens if appropriate.
* Not providing available position information may lead to
* degraded error reporting, though.
*
* @param {string} text the text of this token
* @param {number=} start the start offset, zero-based inclusive
* @param {number=} end the end offset, zero-based exclusive
* @param {Lexer=} lexer the lexer which in turn holds the input string
*/
function Token(text, start, end, lexer) {
this.text = text; this.text = text;
this.start = start; this.data = data;
this.end = end; this.position = position;
this.lexer = lexer;
} }
// "normal" types of tokens. These are tokens which can be matched by a simple
// regex
var mathNormals = [
/[/|@.""`0-9a-zA-Z]/, // ords
/[*+-]/, // bins
/[=<>:]/, // rels
/[,;]/, // punctuation
/['\^_{}]/, // misc
/[(\[]/, // opens
/[)\]?!]/, // closes
/~/, // spacing
/&/, // horizontal alignment
/\\\\/ // line break
];
// These are "normal" tokens like above, but should instead be parsed in text
// mode.
var textNormals = [
/[a-zA-Z0-9`!@*()-=+\[\]'";:?\/.,]/, // ords
/[{}]/, // grouping
/~/, // spacing
/&/, // horizontal alignment
/\\\\/ // line break
];
// Regexes for matching whitespace
var whitespaceRegex = /\s*/;
var whitespaceConcatRegex = / +|\\ +/;
// This regex matches any other TeX function, which is a backslash followed by a
// word or a single symbol
var anyFunc = /\\(?:[a-zA-Z]+|.)/;
/** /**
* Given a pair of tokens (this and endToken), compute a Token encompassing * This function lexes a single normal token. It takes a position, a list of
* the whole input range enclosed by these two. * "normal" tokens to try, and whether it should completely ignore whitespace or
* * not.
* @param {Token} endToken last token of the range, inclusive
* @param {string} text the text of the newly constructed token
*/ */
Token.prototype.range = function(endToken, text) { Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
if (endToken.lexer !== this.lexer) { var input = this._input;
return new Token(text); // sorry, no position information available var whitespace;
if (ignoreWhitespace) {
// Get rid of whitespace.
whitespace = matchAt(whitespaceRegex, input, pos)[0];
pos += whitespace.length;
} else {
// Do the funky concatenation of whitespace that happens in text mode.
whitespace = matchAt(whitespaceConcatRegex, input, pos);
if (whitespace !== null) {
return new Token(" ", null, pos + whitespace[0].length);
}
} }
return new Token(text, this.start, endToken.end, this.lexer);
// If there's no more input to parse, return an EOF token
if (pos === input.length) {
return new Token("EOF", null, pos);
}
var match;
if ((match = matchAt(anyFunc, input, pos))) {
// If we match a function token, return it
return new Token(match[0], null, pos + match[0].length);
} else {
// Otherwise, we look through the normal token regexes and see if it's
// one of them.
for (var i = 0; i < normals.length; i++) {
var normal = normals[i];
if ((match = matchAt(normal, input, pos))) {
// If it is, return it
return new Token(
match[0], null, pos + match[0].length);
}
}
}
throw new ParseError(
"Unexpected character: '" + input[pos] + "'",
this, pos);
}; };
/* The following tokenRegex // A regex to match a CSS color (like #ffffff or BlueViolet)
* - matches typical whitespace (but not NBSP etc.) using its first group var cssColor = /#[a-z0-9]+|[a-z]+/i;
* - does not match any control character \x00-\x1f except whitespace
* - does not match a bare backslash
* - matches any ASCII character except those just mentioned
* - does not match the BMP private use area \uE000-\uF8FF
* - does not match bare surrogate code units
* - matches any BMP character except for those just described
* - matches any valid Unicode surrogate pair
* - matches a backslash followed by one or more letters
* - matches a backslash followed by any BMP character, including newline
* Just because the Lexer matches something doesn't mean it's valid input:
* If there is no matching function or symbol definition, the Parser will
* still reject the input.
*/
const tokenRegex = new RegExp(
"([ \r\n\t]+)|" + // whitespace
"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint
"|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair
"|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" + // function name
")"
);
/** /**
* This function lexes a single token. * This function lexes a CSS color.
*/ */
Lexer.prototype.lex = function() { Lexer.prototype._innerLexColor = function(pos) {
const input = this.input; var input = this._input;
const pos = this.pos;
if (pos === input.length) { // Ignore whitespace
return new Token("EOF", pos, pos, this); var whitespace = matchAt(whitespaceRegex, input, pos)[0];
pos += whitespace.length;
var match;
if ((match = matchAt(cssColor, input, pos))) {
// If we look like a color, return a color
return new Token(match[0], null, pos + match[0].length);
} else {
throw new ParseError("Invalid color", this, pos);
} }
const match = matchAt(tokenRegex, input, pos); };
if (match === null) {
throw new ParseError( // A regex to match a dimension. Dimensions look like
"Unexpected character: '" + input[pos] + "'", // "1.2em" or ".4pt" or "1 ex"
new Token(input[pos], pos, pos + 1, this)); var sizeRegex = /(-?)\s*(\d+(?:\.\d*)?|\.\d+)\s*([a-z]{2})/;
/**
* This function lexes a dimension.
*/
Lexer.prototype._innerLexSize = function(pos) {
var input = this._input;
// Ignore whitespace
var whitespace = matchAt(whitespaceRegex, input, pos)[0];
pos += whitespace.length;
var match;
if ((match = matchAt(sizeRegex, input, pos))) {
var unit = match[3];
// We only currently handle "em" and "ex" units
if (unit !== "em" && unit !== "ex") {
throw new ParseError("Invalid unit: '" + unit + "'", this, pos);
}
return new Token(match[0], {
number: +(match[1] + match[2]),
unit: unit
}, pos + match[0].length);
}
throw new ParseError("Invalid size", this, pos);
};
/**
* This function lexes a string of whitespace.
*/
Lexer.prototype._innerLexWhitespace = function(pos) {
var input = this._input;
var whitespace = matchAt(whitespaceRegex, input, pos)[0];
pos += whitespace.length;
return new Token(whitespace[0], null, pos);
};
/**
* This function lexes a single token starting at `pos` and of the given mode.
* Based on the mode, we defer to one of the `_innerLex` functions.
*/
Lexer.prototype.lex = function(pos, mode) {
if (mode === "math") {
return this._innerLex(pos, mathNormals, true);
} else if (mode === "text") {
return this._innerLex(pos, textNormals, false);
} else if (mode === "color") {
return this._innerLexColor(pos);
} else if (mode === "size") {
return this._innerLexSize(pos);
} else if (mode === "whitespace") {
return this._innerLexWhitespace(pos);
} }
const text = match[2] || " ";
const start = this.pos;
this.pos += match[0].length;
const end = this.pos;
return new Token(text, start, end, this);
}; };
module.exports = Lexer; module.exports = Lexer;

View File

@ -1,146 +0,0 @@
/**
* This file contains the gullet where macros are expanded
* until only non-macro tokens remain.
*/
const Lexer = require("./Lexer");
const builtinMacros = require("./macros");
const ParseError = require("./ParseError");
const objectAssign = require("object-assign");
function MacroExpander(input, macros) {
this.lexer = new Lexer(input);
this.macros = objectAssign({}, builtinMacros, macros);
this.stack = []; // contains tokens in REVERSE order
this.discardedWhiteSpace = [];
}
/**
* Recursively expand first token, then return first non-expandable token.
*
* At the moment, macro expansion doesn't handle delimited macros,
* i.e. things like those defined by \def\foo#1\end{}.
* See the TeX book page 202ff. for details on how those should behave.
*/
MacroExpander.prototype.nextToken = function() {
for (;;) {
if (this.stack.length === 0) {
this.stack.push(this.lexer.lex());
}
const topToken = this.stack.pop();
const name = topToken.text;
if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
return topToken;
}
let tok;
let expansion = this.macros[name];
if (typeof expansion === "string") {
let numArgs = 0;
if (expansion.indexOf("#") !== -1) {
const stripped = expansion.replace(/##/g, "");
while (stripped.indexOf("#" + (numArgs + 1)) !== -1) {
++numArgs;
}
}
const bodyLexer = new Lexer(expansion);
expansion = [];
tok = bodyLexer.lex();
while (tok.text !== "EOF") {
expansion.push(tok);
tok = bodyLexer.lex();
}
expansion.reverse(); // to fit in with stack using push and pop
expansion.numArgs = numArgs;
this.macros[name] = expansion;
}
if (expansion.numArgs) {
const args = [];
let i;
// obtain arguments, either single token or balanced {…} group
for (i = 0; i < expansion.numArgs; ++i) {
const startOfArg = this.get(true);
if (startOfArg.text === "{") {
const arg = [];
let depth = 1;
while (depth !== 0) {
tok = this.get(false);
arg.push(tok);
if (tok.text === "{") {
++depth;
} else if (tok.text === "}") {
--depth;
} else if (tok.text === "EOF") {
throw new ParseError(
"End of input in macro argument",
startOfArg);
}
}
arg.pop(); // remove last }
arg.reverse(); // like above, to fit in with stack order
args[i] = arg;
} else if (startOfArg.text === "EOF") {
throw new ParseError(
"End of input expecting macro argument", topToken);
} else {
args[i] = [startOfArg];
}
}
// paste arguments in place of the placeholders
expansion = expansion.slice(); // make a shallow copy
for (i = expansion.length - 1; i >= 0; --i) {
tok = expansion[i];
if (tok.text === "#") {
if (i === 0) {
throw new ParseError(
"Incomplete placeholder at end of macro body",
tok);
}
tok = expansion[--i]; // next token on stack
if (tok.text === "#") { // ## → #
expansion.splice(i + 1, 1); // drop first #
} else if (/^[1-9]$/.test(tok.text)) {
// expansion.splice(i, 2, arg[0], arg[1], …)
// to replace placeholder with the indicated argument.
// TODO: use spread once we move to ES2015
expansion.splice.apply(
expansion,
[i, 2].concat(args[tok.text - 1]));
} else {
throw new ParseError(
"Not a valid argument number",
tok);
}
}
}
}
this.stack = this.stack.concat(expansion);
}
};
MacroExpander.prototype.get = function(ignoreSpace) {
this.discardedWhiteSpace = [];
let token = this.nextToken();
if (ignoreSpace) {
while (token.text === " ") {
this.discardedWhiteSpace.push(token);
token = this.nextToken();
}
}
return token;
};
/**
* Undo the effect of the preceding call to the get method.
* A call to this method MUST be immediately preceded and immediately followed
* by a call to get. Only used during mode switching, i.e. after one token
* was got in the old mode but should get got again in a new mode
* with possibly different whitespace handling.
*/
MacroExpander.prototype.unget = function(token) {
this.stack.push(token);
while (this.discardedWhiteSpace.length !== 0) {
this.stack.push(this.discardedWhiteSpace.pop());
}
};
module.exports = MacroExpander;

View File

@ -6,9 +6,9 @@
*/ */
/** /**
* This is the main options class. It contains the style, size, color, and font * This is the main options class. It contains the style, size, and color of the
* of the current parse level. It also contains the style and size of the parent * current parse level. It also contains the style and size of the parent parse
* parse level, so size changes can be handled efficiently. * level, so size changes can be handled efficiently.
* *
* Each of the `.with*` and `.reset` functions passes its current style and size * Each of the `.with*` and `.reset` functions passes its current style and size
* as the parentStyle and parentSize of the new options class, so parent * as the parentStyle and parentSize of the new options class, so parent
@ -19,7 +19,6 @@ function Options(data) {
this.color = data.color; this.color = data.color;
this.size = data.size; this.size = data.size;
this.phantom = data.phantom; this.phantom = data.phantom;
this.font = data.font;
if (data.parentStyle === undefined) { if (data.parentStyle === undefined) {
this.parentStyle = data.style; this.parentStyle = data.style;
@ -39,17 +38,16 @@ function Options(data) {
* from "extension" will be copied to the new options object. * from "extension" will be copied to the new options object.
*/ */
Options.prototype.extend = function(extension) { Options.prototype.extend = function(extension) {
const data = { var data = {
style: this.style, style: this.style,
size: this.size, size: this.size,
color: this.color, color: this.color,
parentStyle: this.style, parentStyle: this.style,
parentSize: this.size, parentSize: this.size,
phantom: this.phantom, phantom: this.phantom
font: this.font,
}; };
for (const key in extension) { for (var key in extension) {
if (extension.hasOwnProperty(key)) { if (extension.hasOwnProperty(key)) {
data[key] = extension[key]; data[key] = extension[key];
} }
@ -63,7 +61,7 @@ Options.prototype.extend = function(extension) {
*/ */
Options.prototype.withStyle = function(style) { Options.prototype.withStyle = function(style) {
return this.extend({ return this.extend({
style: style, style: style
}); });
}; };
@ -72,7 +70,7 @@ Options.prototype.withStyle = function(style) {
*/ */
Options.prototype.withSize = function(size) { Options.prototype.withSize = function(size) {
return this.extend({ return this.extend({
size: size, size: size
}); });
}; };
@ -81,7 +79,7 @@ Options.prototype.withSize = function(size) {
*/ */
Options.prototype.withColor = function(color) { Options.prototype.withColor = function(color) {
return this.extend({ return this.extend({
color: color, color: color
}); });
}; };
@ -90,16 +88,7 @@ Options.prototype.withColor = function(color) {
*/ */
Options.prototype.withPhantom = function() { Options.prototype.withPhantom = function() {
return this.extend({ return this.extend({
phantom: true, phantom: true
});
};
/**
* Create a new options objects with the give font.
*/
Options.prototype.withFont = function(font) {
return this.extend({
font: font || this.font,
}); });
}; };
@ -115,7 +104,7 @@ Options.prototype.reset = function() {
* A map of color names to CSS colors. * A map of color names to CSS colors.
* TODO(emily): Remove this when we have real macros * TODO(emily): Remove this when we have real macros
*/ */
const colorMap = { var colorMap = {
"katex-blue": "#6495ed", "katex-blue": "#6495ed",
"katex-orange": "#ffa500", "katex-orange": "#ffa500",
"katex-pink": "#ff00af", "katex-pink": "#ff00af",
@ -123,55 +112,55 @@ const colorMap = {
"katex-green": "#28ae7b", "katex-green": "#28ae7b",
"katex-gray": "gray", "katex-gray": "gray",
"katex-purple": "#9d38bd", "katex-purple": "#9d38bd",
"katex-blueA": "#ccfaff", "katex-blueA": "#c7e9f1",
"katex-blueB": "#80f6ff", "katex-blueB": "#9cdceb",
"katex-blueC": "#63d9ea", "katex-blueC": "#58c4dd",
"katex-blueD": "#11accd", "katex-blueD": "#29abca",
"katex-blueE": "#0c7f99", "katex-blueE": "#1c758a",
"katex-tealA": "#94fff5", "katex-tealA": "#acead7",
"katex-tealB": "#26edd5", "katex-tealB": "#76ddc0",
"katex-tealC": "#01d1c1", "katex-tealC": "#5cd0b3",
"katex-tealD": "#01a995", "katex-tealD": "#55c1a7",
"katex-tealE": "#208170", "katex-tealE": "#49a88f",
"katex-greenA": "#b6ffb0", "katex-greenA": "#c9e2ae",
"katex-greenB": "#8af281", "katex-greenB": "#a6cf8c",
"katex-greenC": "#74cf70", "katex-greenC": "#83c167",
"katex-greenD": "#1fab54", "katex-greenD": "#77b05d",
"katex-greenE": "#0d923f", "katex-greenE": "#699c52",
"katex-goldA": "#ffd0a9", "katex-goldA": "#f7c797",
"katex-goldB": "#ffbb71", "katex-goldB": "#f9b775",
"katex-goldC": "#ff9c39", "katex-goldC": "#f0ac5f",
"katex-goldD": "#e07d10", "katex-goldD": "#e1a158",
"katex-goldE": "#a75a05", "katex-goldE": "#c78d46",
"katex-redA": "#fca9a9", "katex-redA": "#f7a1a3",
"katex-redB": "#ff8482", "katex-redB": "#ff8080",
"katex-redC": "#f9685d", "katex-redC": "#fc6255",
"katex-redD": "#e84d39", "katex-redD": "#e65a4c",
"katex-redE": "#bc2612", "katex-redE": "#cf5044",
"katex-maroonA": "#ffbde0", "katex-maroonA": "#ecabc1",
"katex-maroonB": "#ff92c6", "katex-maroonB": "#ec92ab",
"katex-maroonC": "#ed5fa6", "katex-maroonC": "#c55f73",
"katex-maroonD": "#ca337c", "katex-maroonD": "#a24d61",
"katex-maroonE": "#9e034e", "katex-maroonE": "#94424f",
"katex-purpleA": "#ddd7ff", "katex-purpleA": "#caa3e8",
"katex-purpleB": "#c6b9fc", "katex-purpleB": "#b189c6",
"katex-purpleC": "#aa87ff", "katex-purpleC": "#9a72ac",
"katex-purpleD": "#7854ab", "katex-purpleD": "#715582",
"katex-purpleE": "#543b78", "katex-purpleE": "#644172",
"katex-mintA": "#f5f9e8", "katex-mintA": "#f5f9e8",
"katex-mintB": "#edf2df", "katex-mintB": "#edf2df",
"katex-mintC": "#e0e5cc", "katex-mintC": "#e0e5cc",
"katex-grayA": "#f6f7f7", "katex-grayA": "#fdfdfd",
"katex-grayB": "#f0f1f2", "katex-grayB": "#f7f7f7",
"katex-grayC": "#e3e5e6", "katex-grayC": "#eeeeee",
"katex-grayD": "#d6d8da", "katex-grayD": "#dddddd",
"katex-grayE": "#babec2", "katex-grayE": "#cccccc",
"katex-grayF": "#888d93", "katex-grayF": "#aaaaaa",
"katex-grayG": "#626569", "katex-grayG": "#999999",
"katex-grayH": "#3b3e40", "katex-grayH": "#555555",
"katex-grayI": "#21242c", "katex-grayI": "#333333",
"katex-kaBlue": "#314453", "katex-kaBlue": "#314453",
"katex-kaGreen": "#71B307", "katex-kaGreen": "#639b24"
}; };
/** /**

View File

@ -2,59 +2,35 @@
* This is the ParseError class, which is the main error thrown by KaTeX * This is the ParseError class, which is the main error thrown by KaTeX
* functions when something has gone wrong. This is used to distinguish internal * functions when something has gone wrong. This is used to distinguish internal
* errors from errors in the expression that the user provided. * errors from errors in the expression that the user provided.
*
* If possible, a caller should provide a Token or ParseNode with information
* about where in the source string the problem occurred.
*
* @param {string} message The error message
* @param {(Token|ParseNode)=} token An object providing position information
*/ */
function ParseError(message, token) { function ParseError(message, lexer, position) {
let error = "KaTeX parse error: " + message; var error = "KaTeX parse error: " + message;
let start;
let end;
if (token && token.lexer && token.start <= token.end) { if (lexer !== undefined && position !== undefined) {
// If we have the input and a position, make the error a bit fancier // If we have the input and a position, make the error a bit fancier
// Get the input
const input = token.lexer.input;
// Prepend some information // Prepend some information
start = token.start; error += " at position " + position + ": ";
end = token.end;
if (start === input.length) {
error += " at end of input: ";
} else {
error += " at position " + (start + 1) + ": ";
}
// Underline token in question using combining underscores // Get the input
const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); var input = lexer._input;
// Insert a combining underscore at the correct position
input = input.slice(0, position) + "\u0332" +
input.slice(position);
// Extract some context from the input and add it to the error // Extract some context from the input and add it to the error
let left; var begin = Math.max(0, position - 15);
if (start > 15) { var end = position + 15;
left = "…" + input.slice(start - 15, start); error += input.slice(begin, end);
} else {
left = input.slice(0, start);
}
let right;
if (end + 15 < input.length) {
right = input.slice(end, end + 15) + "…";
} else {
right = input.slice(end);
}
error += left + underlined + right;
} }
// Some hackery to make ParseError a prototype of Error // Some hackery to make ParseError a prototype of Error
// See http://stackoverflow.com/a/8460753 // See http://stackoverflow.com/a/8460753
const self = new Error(error); var self = new Error(error);
self.name = "ParseError"; self.name = "ParseError";
self.__proto__ = ParseError.prototype; self.__proto__ = ParseError.prototype;
self.position = start; self.position = position;
return self; return self;
} }

File diff suppressed because it is too large Load Diff

View File

@ -3,25 +3,24 @@
* default settings. * default settings.
*/ */
const utils = require("./utils"); /**
* Helper function for getting a default value if the value is undefined
*/
function get(option, defaultValue) {
return option === undefined ? defaultValue : option;
}
/** /**
* The main Settings object * The main Settings object
* *
* The current options stored are: * The current options stored are:
* - displayMode: Whether the expression should be typeset as inline math * - displayMode: Whether the expression should be typeset by default in
* (false, the default), meaning that the math starts in * textstyle or displaystyle (default false)
* \textstyle and is placed in an inline-block); or as display
* math (true), meaning that the math starts in \displaystyle
* and is placed in a block with vertical margin.
*/ */
function Settings(options) { function Settings(options) {
// allow null options // allow null options
options = options || {}; options = options || {};
this.displayMode = utils.deflt(options.displayMode, false); this.displayMode = get(options.displayMode, false);
this.throwOnError = utils.deflt(options.throwOnError, true);
this.errorColor = utils.deflt(options.errorColor, "#cc0000");
this.macros = options.macros || {};
} }
module.exports = Settings; module.exports = Settings;

View File

@ -6,20 +6,6 @@
* information about them. * information about them.
*/ */
const sigmas = require("./fontMetrics.js").sigmas;
const metrics = [{}, {}, {}];
for (const key in sigmas) {
if (sigmas.hasOwnProperty(key)) {
for (let i = 0; i < 3; i++) {
metrics[i][key] = sigmas[key][i];
}
}
}
for (let i = 0; i < 3; i++) {
metrics[i].emPerEx = sigmas.xHeight[i] / sigmas.quad[i];
}
/** /**
* The main style class. Contains a unique id for the style, a size (which is * The main style class. Contains a unique id for the style, a size (which is
* the same for cramped and uncramped version of a style), a cramped flag, and a * the same for cramped and uncramped version of a style), a cramped flag, and a
@ -31,7 +17,6 @@ function Style(id, size, multiplier, cramped) {
this.size = size; this.size = size;
this.cramped = cramped; this.cramped = cramped;
this.sizeMultiplier = multiplier; this.sizeMultiplier = multiplier;
this.metrics = metrics[size > 0 ? size - 1 : 0];
} }
/** /**
@ -86,41 +71,34 @@ Style.prototype.reset = function() {
return resetNames[this.size]; return resetNames[this.size];
}; };
/**
* Return if this style is tightly spaced (scriptstyle/scriptscriptstyle)
*/
Style.prototype.isTight = function() {
return this.size >= 2;
};
// IDs of the different styles // IDs of the different styles
const D = 0; var D = 0;
const Dc = 1; var Dc = 1;
const T = 2; var T = 2;
const Tc = 3; var Tc = 3;
const S = 4; var S = 4;
const Sc = 5; var Sc = 5;
const SS = 6; var SS = 6;
const SSc = 7; var SSc = 7;
// String names for the different sizes // String names for the different sizes
const sizeNames = [ var sizeNames = [
"displaystyle textstyle", "displaystyle textstyle",
"textstyle", "textstyle",
"scriptstyle", "scriptstyle",
"scriptscriptstyle", "scriptscriptstyle"
]; ];
// Reset names for the different sizes // Reset names for the different sizes
const resetNames = [ var resetNames = [
"reset-textstyle", "reset-textstyle",
"reset-textstyle", "reset-textstyle",
"reset-scriptstyle", "reset-scriptstyle",
"reset-scriptscriptstyle", "reset-scriptscriptstyle"
]; ];
// Instances of the different styles // Instances of the different styles
const styles = [ var styles = [
new Style(D, 0, 1.0, false), new Style(D, 0, 1.0, false),
new Style(Dc, 0, 1.0, true), new Style(Dc, 0, 1.0, true),
new Style(T, 1, 1.0, false), new Style(T, 1, 1.0, false),
@ -128,15 +106,15 @@ const styles = [
new Style(S, 2, 0.7, false), new Style(S, 2, 0.7, false),
new Style(Sc, 2, 0.7, true), new Style(Sc, 2, 0.7, true),
new Style(SS, 3, 0.5, false), new Style(SS, 3, 0.5, false),
new Style(SSc, 3, 0.5, true), new Style(SSc, 3, 0.5, true)
]; ];
// Lookup tables for switching from one style to another // Lookup tables for switching from one style to another
const sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
const sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
const fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
const fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
const cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
// We only export some of the styles. Also, we don't export the `Style` class so // We only export some of the styles. Also, we don't export the `Style` class so
// no more styles can be generated. // no more styles can be generated.
@ -144,5 +122,5 @@ module.exports = {
DISPLAY: styles[D], DISPLAY: styles[D],
TEXT: styles[T], TEXT: styles[T],
SCRIPT: styles[S], SCRIPT: styles[S],
SCRIPTSCRIPT: styles[SS], SCRIPTSCRIPT: styles[SS]
}; };

View File

@ -1,169 +1,64 @@
/* eslint no-console:0 */
/** /**
* This module contains general functions that can be used for building * This module contains general functions that can be used for building
* different kinds of domTree nodes in a consistent manner. * different kinds of domTree nodes in a consistent manner.
*/ */
const domTree = require("./domTree"); var domTree = require("./domTree");
const fontMetrics = require("./fontMetrics"); var fontMetrics = require("./fontMetrics");
const symbols = require("./symbols"); var symbols = require("./symbols");
const utils = require("./utils");
// The following have to be loaded from Main-Italic font, using class mainit
const mainitLetters = [
"\\imath", // dotless i
"\\jmath", // dotless j
"\\pounds", // pounds symbol
];
/**
* Looks up the given symbol in fontMetrics, after applying any symbol
* replacements defined in symbol.js
*/
const lookupSymbol = function(value, fontFamily, mode) {
// Replace the value with its replaced value from symbol.js
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
return {
value: value,
metrics: fontMetrics.getCharacterMetrics(value, fontFamily),
};
};
/** /**
* Makes a symbolNode after translation via the list of symbols in symbols.js. * Makes a symbolNode after translation via the list of symbols in symbols.js.
* Correctly pulls out metrics for the character, and optionally takes a list of * Correctly pulls out metrics for the character, and optionally takes a list of
* classes to be attached to the node. * classes to be attached to the node.
*
* TODO: make argument order closer to makeSpan
* TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
* should if present come first in `classes`.
*/ */
const makeSymbol = function(value, fontFamily, mode, options, classes) { var makeSymbol = function(value, style, mode, color, classes) {
const lookup = lookupSymbol(value, fontFamily, mode); // Replace the value with its replaced value from symbol.js
const metrics = lookup.metrics; if (symbols[mode][value] && symbols[mode][value].replace) {
value = lookup.value; value = symbols[mode][value].replace;
}
let symbolNode; var metrics = fontMetrics.getCharacterMetrics(value, style);
var symbolNode;
if (metrics) { if (metrics) {
let italic = metrics.italic;
if (mode === "text") {
italic = 0;
}
symbolNode = new domTree.symbolNode( symbolNode = new domTree.symbolNode(
value, metrics.height, metrics.depth, italic, metrics.skew, value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
classes); classes);
} else { } else {
// TODO(emily): Figure out a good way to only print this in development // TODO(emily): Figure out a good way to only print this in development
typeof console !== "undefined" && console.warn( typeof console !== "undefined" && console.warn(
"No character metrics for '" + value + "' in style '" + "No character metrics for '" + value + "' in style '" +
fontFamily + "'"); style + "'");
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes); symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
} }
if (options) { if (color) {
if (options.style.isTight()) { symbolNode.style.color = color;
symbolNode.classes.push("mtight");
}
if (options.getColor()) {
symbolNode.style.color = options.getColor();
}
} }
return symbolNode; return symbolNode;
}; };
/** /**
* Makes a symbol in Main-Regular or AMS-Regular. * Makes a symbol in the italic math font.
* Used for rel, bin, open, close, inner, and punct.
*/ */
const mathsym = function(value, mode, options, classes) { var mathit = function(value, mode, color, classes) {
return makeSymbol(
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
};
/**
* Makes a symbol in the upright roman font.
*/
var mathrm = function(value, mode, color, classes) {
// Decide what font to render the symbol in by its entry in the symbols // Decide what font to render the symbol in by its entry in the symbols
// table. // table.
// Have a special case for when the value = \ because the \ is used as a if (symbols[mode][value].font === "main") {
// textord in unsupported command errors but cannot be parsed as a regular return makeSymbol(value, "Main-Regular", mode, color, classes);
// text ordinal and is therefore not present as a symbol in the symbols
// table for text
if (value === "\\" || symbols[mode][value].font === "main") {
return makeSymbol(value, "Main-Regular", mode, options, classes);
} else { } else {
return makeSymbol( return makeSymbol(
value, "AMS-Regular", mode, options, classes.concat(["amsrm"])); value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
}
};
/**
* Makes a symbol in the default font for mathords and textords.
*/
const mathDefault = function(value, mode, options, classes, type) {
if (type === "mathord") {
const fontLookup = mathit(value, mode, options, classes);
return makeSymbol(value, fontLookup.fontName, mode, options,
classes.concat([fontLookup.fontClass]));
} else if (type === "textord") {
const font = symbols[mode][value] && symbols[mode][value].font;
if (font === "ams") {
return makeSymbol(
value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
} else { // if (font === "main") {
return makeSymbol(
value, "Main-Regular", mode, options,
classes.concat(["mathrm"]));
}
} else {
throw new Error("unexpected type: " + type + " in mathDefault");
}
};
/**
* Determines which of the two font names (Main-Italic and Math-Italic) and
* corresponding style tags (mainit or mathit) to use for font "mathit",
* depending on the symbol. Use this function instead of fontMap for font
* "mathit".
*/
const mathit = function(value, mode, options, classes) {
if (/[0-9]/.test(value.charAt(0)) ||
// glyphs for \imath and \jmath do not exist in Math-Italic so we
// need to use Main-Italic instead
utils.contains(mainitLetters, value)) {
return {
fontName: "Main-Italic",
fontClass: "mainit",
};
} else {
return {
fontName: "Math-Italic",
fontClass: "mathit",
};
}
};
/**
* Makes either a mathord or textord in the correct font and color.
*/
const makeOrd = function(group, options, type) {
const mode = group.mode;
const value = group.value;
const classes = ["mord"];
const font = options.font;
if (font) {
let fontLookup;
if (font === "mathit" || utils.contains(mainitLetters, value)) {
fontLookup = mathit(value, mode, options, classes);
} else {
fontLookup = fontMap[font];
}
if (lookupSymbol(value, fontLookup.fontName, mode).metrics) {
return makeSymbol(value, fontLookup.fontName, mode, options,
classes.concat([fontLookup.fontClass || font]));
} else {
return mathDefault(value, mode, options, classes, type);
}
} else {
return mathDefault(value, mode, options, classes, type);
} }
}; };
@ -171,13 +66,13 @@ const makeOrd = function(group, options, type) {
* Calculate the height, depth, and maxFontSize of an element based on its * Calculate the height, depth, and maxFontSize of an element based on its
* children. * children.
*/ */
const sizeElementFromChildren = function(elem) { var sizeElementFromChildren = function(elem) {
let height = 0; var height = 0;
let depth = 0; var depth = 0;
let maxFontSize = 0; var maxFontSize = 0;
if (elem.children) { if (elem.children) {
for (let i = 0; i < elem.children.length; i++) { for (var i = 0; i < elem.children.length; i++) {
if (elem.children[i].height > height) { if (elem.children[i].height > height) {
height = elem.children[i].height; height = elem.children[i].height;
} }
@ -196,36 +91,25 @@ const sizeElementFromChildren = function(elem) {
}; };
/** /**
* Makes a span with the given list of classes, list of children, and options. * Makes a span with the given list of classes, list of children, and color.
*
* TODO: Ensure that `options` is always provided (currently some call sites
* don't pass it).
* TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which
* should if present come first in `classes`.
*/ */
const makeSpan = function(classes, children, options) { var makeSpan = function(classes, children, color) {
const span = new domTree.span(classes, children, options); var span = new domTree.span(classes, children);
sizeElementFromChildren(span); sizeElementFromChildren(span);
if (color) {
span.style.color = color;
}
return span; return span;
}; };
/**
* Prepends the given children to the given span, updating height, depth, and
* maxFontSize.
*/
const prependChildren = function(span, children) {
span.children = children.concat(span.children);
sizeElementFromChildren(span);
};
/** /**
* Makes a document fragment with the given list of children. * Makes a document fragment with the given list of children.
*/ */
const makeFragment = function(children) { var makeFragment = function(children) {
const fragment = new domTree.documentFragment(children); var fragment = new domTree.documentFragment(children);
sizeElementFromChildren(fragment); sizeElementFromChildren(fragment);
@ -237,12 +121,11 @@ const makeFragment = function(children) {
* element has the same max font size. To do this, we create a zero-width space * element has the same max font size. To do this, we create a zero-width space
* with the correct font size. * with the correct font size.
*/ */
const makeFontSizer = function(options, fontSize) { var makeFontSizer = function(options, fontSize) {
const fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]); var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
fontSizeInner.style.fontSize = fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
(fontSize / options.style.sizeMultiplier) + "em";
const fontSizer = makeSpan( var fontSizer = makeSpan(
["fontsize-ensurer", "reset-" + options.size, "size5"], ["fontsize-ensurer", "reset-" + options.size, "size5"],
[fontSizeInner]); [fontSizeInner]);
@ -288,12 +171,12 @@ const makeFontSizer = function(options, fontSize) {
* - options: An Options object * - options: An Options object
* *
*/ */
const makeVList = function(children, positionType, positionData, options) { var makeVList = function(children, positionType, positionData, options) {
let depth; var depth;
let currPos; var currPos;
let i; var i;
if (positionType === "individualShift") { if (positionType === "individualShift") {
const oldChildren = children; var oldChildren = children;
children = [oldChildren[0]]; children = [oldChildren[0]];
// Add in kerns to the list of children to get each element to be // Add in kerns to the list of children to get each element to be
@ -301,9 +184,9 @@ const makeVList = function(children, positionType, positionData, options) {
depth = -oldChildren[0].shift - oldChildren[0].elem.depth; depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
currPos = depth; currPos = depth;
for (i = 1; i < oldChildren.length; i++) { for (i = 1; i < oldChildren.length; i++) {
const diff = -oldChildren[i].shift - currPos - var diff = -oldChildren[i].shift - currPos -
oldChildren[i].elem.depth; oldChildren[i].elem.depth;
const size = diff - var size = diff -
(oldChildren[i - 1].elem.height + (oldChildren[i - 1].elem.height +
oldChildren[i - 1].elem.depth); oldChildren[i - 1].elem.depth);
@ -315,7 +198,7 @@ const makeVList = function(children, positionType, positionData, options) {
} else if (positionType === "top") { } else if (positionType === "top") {
// We always start at the bottom, so calculate the bottom by adding up // We always start at the bottom, so calculate the bottom by adding up
// all the sizes // all the sizes
let bottom = positionData; var bottom = positionData;
for (i = 0; i < children.length; i++) { for (i = 0; i < children.length; i++) {
if (children[i].type === "kern") { if (children[i].type === "kern") {
bottom -= children[i].size; bottom -= children[i].size;
@ -335,27 +218,27 @@ const makeVList = function(children, positionType, positionData, options) {
} }
// Make the fontSizer // Make the fontSizer
let maxFontSize = 0; var maxFontSize = 0;
for (i = 0; i < children.length; i++) { for (i = 0; i < children.length; i++) {
if (children[i].type === "elem") { if (children[i].type === "elem") {
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize); maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
} }
} }
const fontSizer = makeFontSizer(options, maxFontSize); var fontSizer = makeFontSizer(options, maxFontSize);
// Create a new list of actual children at the correct offsets // Create a new list of actual children at the correct offsets
const realChildren = []; var realChildren = [];
currPos = depth; currPos = depth;
for (i = 0; i < children.length; i++) { for (i = 0; i < children.length; i++) {
if (children[i].type === "kern") { if (children[i].type === "kern") {
currPos += children[i].size; currPos += children[i].size;
} else { } else {
const child = children[i].elem; var child = children[i].elem;
const shift = -child.depth - currPos; var shift = -child.depth - currPos;
currPos += child.height + child.depth; currPos += child.height + child.depth;
const childWrap = makeSpan([], [fontSizer, child]); var childWrap = makeSpan([], [fontSizer, child]);
childWrap.height -= shift; childWrap.height -= shift;
childWrap.depth += shift; childWrap.depth += shift;
childWrap.style.top = shift + "em"; childWrap.style.top = shift + "em";
@ -366,11 +249,11 @@ const makeVList = function(children, positionType, positionData, options) {
// Add in an element at the end with no offset to fix the calculation of // Add in an element at the end with no offset to fix the calculation of
// baselines in some browsers (namely IE, sometimes safari) // baselines in some browsers (namely IE, sometimes safari)
const baselineFix = makeSpan( var baselineFix = makeSpan(
["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]); ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
realChildren.push(baselineFix); realChildren.push(baselineFix);
const vlist = makeSpan(["vlist"], realChildren); var vlist = makeSpan(["vlist"], realChildren);
// Fix the final height and depth, in case there were kerns at the ends // Fix the final height and depth, in case there were kerns at the ends
// since the makeSpan calculation won't take that in to account. // since the makeSpan calculation won't take that in to account.
vlist.height = Math.max(currPos, vlist.height); vlist.height = Math.max(currPos, vlist.height);
@ -379,7 +262,7 @@ const makeVList = function(children, positionType, positionData, options) {
}; };
// A table of size -> font size for the different sizing functions // A table of size -> font size for the different sizing functions
const sizingMultiplier = { var sizingMultiplier = {
size1: 0.5, size1: 0.5,
size2: 0.7, size2: 0.7,
size3: 0.8, size3: 0.8,
@ -389,103 +272,49 @@ const sizingMultiplier = {
size7: 1.44, size7: 1.44,
size8: 1.73, size8: 1.73,
size9: 2.07, size9: 2.07,
size10: 2.49, size10: 2.49
}; };
// A map of spacing functions to their attributes, like size and corresponding // A map of spacing functions to their attributes, like size and corresponding
// CSS class // CSS class
const spacingFunctions = { var spacingFunctions = {
"\\qquad": { "\\qquad": {
size: "2em", size: "2em",
className: "qquad", className: "qquad"
}, },
"\\quad": { "\\quad": {
size: "1em", size: "1em",
className: "quad", className: "quad"
}, },
"\\enspace": { "\\enspace": {
size: "0.5em", size: "0.5em",
className: "enspace", className: "enspace"
}, },
"\\;": { "\\;": {
size: "0.277778em", size: "0.277778em",
className: "thickspace", className: "thickspace"
}, },
"\\:": { "\\:": {
size: "0.22222em", size: "0.22222em",
className: "mediumspace", className: "mediumspace"
}, },
"\\,": { "\\,": {
size: "0.16667em", size: "0.16667em",
className: "thinspace", className: "thinspace"
}, },
"\\!": { "\\!": {
size: "-0.16667em", size: "-0.16667em",
className: "negativethinspace", className: "negativethinspace"
}, }
};
/**
* Maps TeX font commands to objects containing:
* - variant: string used for "mathvariant" attribute in buildMathML.js
* - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
*/
// A map between tex font commands an MathML mathvariant attribute values
const fontMap = {
// styles
"mathbf": {
variant: "bold",
fontName: "Main-Bold",
},
"mathrm": {
variant: "normal",
fontName: "Main-Regular",
},
"textit": {
variant: "italic",
fontName: "Main-Italic",
},
// "mathit" is missing because it requires the use of two fonts: Main-Italic
// and Math-Italic. This is handled by a special case in makeOrd which ends
// up calling mathit.
// families
"mathbb": {
variant: "double-struck",
fontName: "AMS-Regular",
},
"mathcal": {
variant: "script",
fontName: "Caligraphic-Regular",
},
"mathfrak": {
variant: "fraktur",
fontName: "Fraktur-Regular",
},
"mathscr": {
variant: "script",
fontName: "Script-Regular",
},
"mathsf": {
variant: "sans-serif",
fontName: "SansSerif-Regular",
},
"mathtt": {
variant: "monospace",
fontName: "Typewriter-Regular",
},
}; };
module.exports = { module.exports = {
fontMap: fontMap,
makeSymbol: makeSymbol, makeSymbol: makeSymbol,
mathsym: mathsym, mathit: mathit,
mathrm: mathrm,
makeSpan: makeSpan, makeSpan: makeSpan,
makeFragment: makeFragment, makeFragment: makeFragment,
makeVList: makeVList, makeVList: makeVList,
makeOrd: makeOrd,
prependChildren: prependChildren,
sizingMultiplier: sizingMultiplier, sizingMultiplier: sizingMultiplier,
spacingFunctions: spacingFunctions, spacingFunctions: spacingFunctions
}; };

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More