Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b16136ee27 |
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
9
.arclint
9
.arclint
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
83
.eslintrc
83
.eslintrc
|
@ -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
9
.gitattributes
vendored
|
@ -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
9
.gitignore
vendored
|
@ -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
68
.jshintrc
Normal 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
|
||||||
|
}
|
||||||
|
}
|
13
.travis.yml
13
.travis.yml
|
@ -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
|
|
||||||
|
|
|
@ -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/
|
|
|
@ -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
|
||||||
|
|
87
Makefile
87
Makefile
|
@ -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
|
||||||
|
|
20
README.md
20
README.md
|
@ -1,23 +1,21 @@
|
||||||
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [](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/) [](https://travis-ci.org/Khan/KaTeX)
|
||||||
|
|
||||||
[](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:** KaTeX’s layout is based on Donald Knuth’s TeX, the gold standard for math typesetting.
|
* **Print quality:** KaTeX’s layout is based on Donald Knuth’s 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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
13
cli.js
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 > $@
|
||||||
|
|
|
@ -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"]`.
|
|
||||||
|
|
|
@ -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}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
64
dist/README.md
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [](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:** KaTeX’s layout is based on Donald Knuth’s 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
1
dist/contrib/auto-render.min.js
vendored
Normal 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
BIN
dist/fonts/KaTeX_AMS-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_AMS-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_AMS-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_AMS-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_AMS-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_AMS-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_AMS-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Bold.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Bold.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Bold.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Bold.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Bold.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Bold.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Bold.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Bold.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Italic.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Italic.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Italic.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Italic.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Italic.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Italic.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Italic.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Italic.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Main-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Main-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-BoldItalic.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-BoldItalic.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-BoldItalic.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-BoldItalic.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-BoldItalic.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-BoldItalic.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-BoldItalic.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-BoldItalic.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Italic.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Italic.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Italic.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Italic.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Italic.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Italic.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Italic.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Italic.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Math-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Math-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size1-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Size1-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size1-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Size1-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size1-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Size1-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size1-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Size1-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size2-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Size2-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size2-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Size2-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size2-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Size2-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size2-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Size2-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size3-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Size3-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size3-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Size3-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size3-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Size3-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size3-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Size3-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size4-Regular.eot
vendored
Normal file
BIN
dist/fonts/KaTeX_Size4-Regular.eot
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size4-Regular.ttf
vendored
Normal file
BIN
dist/fonts/KaTeX_Size4-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size4-Regular.woff
vendored
Normal file
BIN
dist/fonts/KaTeX_Size4-Regular.woff
vendored
Normal file
Binary file not shown.
BIN
dist/fonts/KaTeX_Size4-Regular.woff2
vendored
Normal file
BIN
dist/fonts/KaTeX_Size4-Regular.woff2
vendored
Normal file
Binary file not shown.
1
dist/katex.min.css
vendored
Normal file
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
4
dist/katex.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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/
|
||||||
|
|
14
dockers/Screenshotter/Dockerfile
Normal file
14
dockers/Screenshotter/Dockerfile
Normal 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
|
|
@ -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
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
107
dockers/Screenshotter/screenshotter.py
Executable file
107
dockers/Screenshotter/screenshotter.py
Executable 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()
|
|
@ -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}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]);
|
|
||||||
}
|
|
|
@ -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 "$@"
|
|
31
katex.js
31
katex.js
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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/**
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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")
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
17
metrics/replace_line.py
Executable 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)
|
35
package.json
35
package.json
|
@ -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"
|
||||||
|
|
165
release.sh
165
release.sh
|
@ -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
106
server.js
|
@ -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;
|
|
||||||
|
|
233
src/Lexer.js
233
src/Lexer.js
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
123
src/Options.js
123
src/Options.js
|
@ -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"
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
834
src/Parser.js
834
src/Parser.js
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||||
|
|
62
src/Style.js
62
src/Style.js
|
@ -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]
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
2396
src/buildHTML.js
2396
src/buildHTML.js
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
Loading…
Reference in New Issue
Block a user