Compare commits
275 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e4b93379fb | ||
![]() |
5165cc48e6 | ||
![]() |
7192bd6595 | ||
![]() |
b9e7b6898f | ||
![]() |
429c1f6f52 | ||
![]() |
2c92a9a368 | ||
![]() |
171e38f28a | ||
![]() |
1379c2666e | ||
![]() |
3a95d8889b | ||
![]() |
28ad473e4a | ||
![]() |
e2763a3007 | ||
![]() |
96d1e6aea7 | ||
![]() |
7ec455083f | ||
![]() |
40ec1b92b8 | ||
![]() |
f3df1ccbac | ||
![]() |
fdd83db65a | ||
![]() |
742e512172 | ||
![]() |
3fea3dfbbc | ||
![]() |
301d7cfb1a | ||
![]() |
7aa84d010c | ||
![]() |
59c4e008db | ||
![]() |
fc115e1f66 | ||
![]() |
079fecaf86 | ||
![]() |
76051a6ac2 | ||
![]() |
8dd161d4b2 | ||
![]() |
a5e38d3b8a | ||
![]() |
8dc1374e7a | ||
![]() |
a738185704 | ||
![]() |
a3f0653e5c | ||
![]() |
d4aa6a7253 | ||
![]() |
f1c02226cc | ||
![]() |
38ba9f9187 | ||
![]() |
bd9db332d2 | ||
![]() |
9b565a6375 | ||
![]() |
a0cd343d85 | ||
![]() |
5e4f50dc09 | ||
![]() |
5783e193ed | ||
![]() |
961e9db514 | ||
![]() |
35ff154ead | ||
![]() |
6404cfd269 | ||
![]() |
73cafd16da | ||
![]() |
a3988d9d56 | ||
![]() |
840771c643 | ||
![]() |
549104c5a8 | ||
![]() |
90ba0e341d | ||
![]() |
0c5d9d55cf | ||
![]() |
56f3d1db47 | ||
![]() |
c56de9e323 | ||
![]() |
79d53bd29f | ||
![]() |
4ead46c4ab | ||
![]() |
59b87539b9 | ||
![]() |
896aeac81b | ||
![]() |
53e416e296 | ||
![]() |
677290336a | ||
![]() |
90e25fecc2 | ||
![]() |
8f46eeaf1b | ||
![]() |
225baffef4 | ||
![]() |
4327e85796 | ||
![]() |
4d2e46e7f6 | ||
![]() |
74d55ee0ea | ||
![]() |
1ca7363460 | ||
![]() |
ca1a511cc4 | ||
![]() |
7c83021009 | ||
![]() |
f742fbf9f2 | ||
![]() |
81bc24808b | ||
![]() |
d5cedc55c9 | ||
![]() |
3900936f63 | ||
![]() |
d43f24ea91 | ||
![]() |
fdebbe3a18 | ||
![]() |
831c5b8d99 | ||
![]() |
530ec97e74 | ||
![]() |
95f2f46bf5 | ||
![]() |
e449b2d61a | ||
![]() |
72362ecf88 | ||
![]() |
7433638fda | ||
![]() |
81f9a410a0 | ||
![]() |
e1c5f5db1c | ||
![]() |
576380c11c | ||
![]() |
b88bc7723d | ||
![]() |
4297eb1276 | ||
![]() |
e0407a19a0 | ||
![]() |
f8e0c91de4 | ||
![]() |
6bb62b11b4 | ||
![]() |
982e7be597 | ||
![]() |
dc79b91dbf | ||
![]() |
1a99e7a2e9 | ||
![]() |
c951df4269 | ||
![]() |
9d3cdf694c | ||
![]() |
be96695586 | ||
![]() |
94dad8029d | ||
![]() |
7b5580b1fa | ||
![]() |
22957d40f6 | ||
![]() |
ace67541a0 | ||
![]() |
0ebbc25672 | ||
![]() |
fef5b88057 | ||
![]() |
d5025c61e8 | ||
![]() |
4fb9445a92 | ||
![]() |
d50745d5a9 | ||
![]() |
f1be1a3462 | ||
![]() |
a16ae7a5eb | ||
![]() |
e4d2d933af | ||
![]() |
e6de31d2d6 | ||
![]() |
ec62ec39d8 | ||
![]() |
92bbbffbc8 | ||
![]() |
725524a214 | ||
![]() |
4a9c2acbf7 | ||
![]() |
befe1c1af7 | ||
![]() |
8c55aed39a | ||
![]() |
b49eee4de7 | ||
![]() |
26f06754e7 | ||
![]() |
3668bb084e | ||
![]() |
b120b1c3d8 | ||
![]() |
1cf10c4cd6 | ||
![]() |
9884ac3e8d | ||
![]() |
991f4eb096 | ||
![]() |
b62e1cf314 | ||
![]() |
5991a6078b | ||
![]() |
aa36c459ae | ||
![]() |
0f7a1a06e9 | ||
![]() |
36164b98ea | ||
![]() |
363a6ba311 | ||
![]() |
224efafda8 | ||
![]() |
ccd8f40028 | ||
![]() |
965b8a6164 | ||
![]() |
a33fa4fd64 | ||
![]() |
229ce562ff | ||
![]() |
d2079a6c9c | ||
![]() |
d5359ef5bb | ||
![]() |
83dea37b51 | ||
![]() |
a5207fef94 | ||
![]() |
3083efba66 | ||
![]() |
c79fb58936 | ||
![]() |
157bfb0cf5 | ||
![]() |
9ad50178f1 | ||
![]() |
14a58adb90 | ||
![]() |
1a082e81d9 | ||
![]() |
0a53a775e8 | ||
![]() |
7cdb08bf7e | ||
![]() |
3a15a8402f | ||
![]() |
8201501d77 | ||
![]() |
b9c4237ac2 | ||
![]() |
4debcb34af | ||
![]() |
5f275aa9c1 | ||
![]() |
e7195601e1 | ||
![]() |
4617f191e3 | ||
![]() |
cc17f36edc | ||
![]() |
bfb3827df1 | ||
![]() |
1c50688cba | ||
![]() |
4792dec8e5 | ||
![]() |
92034c17f9 | ||
![]() |
21a26b807c | ||
![]() |
9fb04dbb2e | ||
![]() |
5ecbcf6808 | ||
![]() |
c20b8f8456 | ||
![]() |
dcc3214527 | ||
![]() |
c6800749ab | ||
![]() |
f039068fae | ||
![]() |
bc4693a71b | ||
![]() |
69130d6a19 | ||
![]() |
d87ee4f78f | ||
![]() |
1573e1eed6 | ||
![]() |
ee88cc3c11 | ||
![]() |
9aab9c1efe | ||
![]() |
95e568ed6b | ||
![]() |
cabc08598b | ||
![]() |
e9f4b07611 | ||
![]() |
d423bec089 | ||
![]() |
95e2f1c8d7 | ||
![]() |
30f7a1c5bf | ||
![]() |
a81c4fe78d | ||
![]() |
6a10237017 | ||
![]() |
fdbdb28617 | ||
![]() |
f25829df58 | ||
![]() |
8e54d6e365 | ||
![]() |
b9b8396b6f | ||
![]() |
3203959b96 | ||
![]() |
b98670b8ed | ||
![]() |
961e1caba9 | ||
![]() |
252f6320ef | ||
![]() |
d7d1367558 | ||
![]() |
0fc77e2f40 | ||
![]() |
112e6783b3 | ||
![]() |
2e0f11bdfa | ||
![]() |
89e0d7950c | ||
![]() |
71881e3d36 | ||
![]() |
d809f9c362 | ||
![]() |
6bc7cd574f | ||
![]() |
dae3a14744 | ||
![]() |
2a31a719ec | ||
![]() |
acfdc9f698 | ||
![]() |
414f9dd248 | ||
![]() |
3e055f84e9 | ||
![]() |
d553353204 | ||
![]() |
5539226f4b | ||
![]() |
8accf0f18a | ||
![]() |
d6cec8a861 | ||
![]() |
c428abca1e | ||
![]() |
64e63d7546 | ||
![]() |
07dc11ccb0 | ||
![]() |
9c2a391ff6 | ||
![]() |
f488a7c48d | ||
![]() |
2e002ff37a | ||
![]() |
72027a1a56 | ||
![]() |
3a8adbf595 | ||
![]() |
1b5834d894 | ||
![]() |
d772aab6d5 | ||
![]() |
bc4c270220 | ||
![]() |
2af1ad2ffc | ||
![]() |
2d9a6f323c | ||
![]() |
fd2d58fd80 | ||
![]() |
f32d615813 | ||
![]() |
9942283db0 | ||
![]() |
1846929110 | ||
![]() |
cb9f765e06 | ||
![]() |
1da8c8938b | ||
![]() |
758f4a73d9 | ||
![]() |
236b7925f1 | ||
![]() |
8cd71830c2 | ||
![]() |
62a8b2d4e7 | ||
![]() |
b2d2df9bef | ||
![]() |
76d87e8f90 | ||
![]() |
4a507c40f1 | ||
![]() |
9b0f42ea50 | ||
![]() |
e1c221273c | ||
![]() |
4be3931cb5 | ||
![]() |
5d83bb8cc0 | ||
![]() |
0dc9eed1c4 | ||
![]() |
bd275b85d7 | ||
![]() |
476eebf3c5 | ||
![]() |
2600587f78 | ||
![]() |
ce99abd1f2 | ||
![]() |
fb403fa9eb | ||
![]() |
a3031af307 | ||
![]() |
e0c75df7b7 | ||
![]() |
9752d02748 | ||
![]() |
b7e1581869 | ||
![]() |
a18db36296 | ||
![]() |
6f65f685f3 | ||
![]() |
5d155c75db | ||
![]() |
b1d5311898 | ||
![]() |
11970ee965 | ||
![]() |
67147b18ac | ||
![]() |
549c2bf858 | ||
![]() |
6362e0f8f2 | ||
![]() |
8bff74ca09 | ||
![]() |
f05ff9c5fa | ||
![]() |
6357a34828 | ||
![]() |
b290d4ad76 | ||
![]() |
d26a67f220 | ||
![]() |
86115b8fce | ||
![]() |
1603162267 | ||
![]() |
1f8610cebe | ||
![]() |
6cf8c5aacb | ||
![]() |
7c8ea80638 | ||
![]() |
8009059b7c | ||
![]() |
d00caf6ed7 | ||
![]() |
ec3cbb8656 | ||
![]() |
7dc8b68092 | ||
![]() |
a06744e941 | ||
![]() |
c562813afa | ||
![]() |
7be056c1f4 | ||
![]() |
53b0a9ad7a | ||
![]() |
87b2cc95bf | ||
![]() |
2e9fdee780 | ||
![]() |
1a11eb46b2 | ||
![]() |
39489ab479 | ||
![]() |
1ac6b41990 | ||
![]() |
eaf89dd45d | ||
![]() |
41e3fa6659 | ||
![]() |
758bdba31e | ||
![]() |
7f1b53cbfd | ||
![]() |
8931e5b45c | ||
![]() |
c18d3ad6c4 | ||
![]() |
4ea7d38b5c | ||
![]() |
b11a1b97b7 |
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"project_id": "KaTeX",
|
||||
"conduit_uri": "https://phabricator.khanacademy.org/",
|
||||
"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"
|
||||
"lint.engine": "ArcanistConfigurationDrivenLintEngine"
|
||||
}
|
||||
|
|
9
.arclint
Normal file
9
.arclint
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
6
.babelrc
Normal file
6
.babelrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
]
|
||||
}
|
83
.eslintrc
Normal file
83
.eslintrc
Normal file
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"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
Normal file
9
.gitattributes
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 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,3 +3,12 @@ node_modules
|
|||
npm-debug.log
|
||||
last.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
68
.jshintrc
|
@ -1,68 +0,0 @@
|
|||
{
|
||||
"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,4 +1,13 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
- stable
|
||||
sudo: required
|
||||
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
|
||||
|
|
74
CODE_OF_CONDUCT.md
Normal file
74
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
# 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,16 +2,21 @@
|
|||
|
||||
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
|
||||
help solving a problem, feel free to stop by the [#katex room on
|
||||
freenode](http://webchat.freenode.net/?channels=katex).
|
||||
help solving a problem, feel free to stop by our [gitter channel](https://gitter.im/Khan/KaTeX).
|
||||
|
||||
## Helpful contributions
|
||||
|
||||
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
|
||||
supported
|
||||
functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX). You
|
||||
can check there to see if we don't support a function you like, or try your
|
||||
functions](https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX) as
|
||||
well as a page that describes how to [examine TeX commands and where to find
|
||||
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
|
||||
[http://khan.github.io/KaTeX/](http://khan.github.io/KaTeX/).
|
||||
|
||||
|
@ -83,7 +88,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
|
||||
created it, add a screenshot test. See
|
||||
[ss_data.json](test/screenshotter/ss_data.json).
|
||||
[ss_data.yaml](test/screenshotter/ss_data.yaml).
|
||||
|
||||
#### Testing in other browsers
|
||||
|
||||
|
@ -100,9 +105,18 @@ Code
|
|||
- 80 character line length
|
||||
- commas last
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
In order to contribute to KaTeX, you must first sign the CLA, found at www.khanacademy.org/r/cla
|
||||
|
|
87
Makefile
87
Makefile
|
@ -1,8 +1,23 @@
|
|||
.PHONY: build dist lint setup copy serve clean metrics test zip contrib
|
||||
build: setup lint build/katex.min.js build/katex.min.css contrib zip compress
|
||||
build: lint build/katex.min.js build/katex.min.css contrib zip compress
|
||||
|
||||
ifeq ($(KATEX_DIST),skip)
|
||||
|
||||
dist:
|
||||
|
||||
else
|
||||
|
||||
dist: build
|
||||
cp --recursive build/katex dist
|
||||
rm -rf 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 BUILDDIR = $(realpath build)
|
||||
|
@ -12,22 +27,27 @@ export UGLIFYJS = $(realpath ./node_modules/.bin/uglifyjs) \
|
|||
--beautify \
|
||||
ascii_only=true,beautify=false
|
||||
|
||||
setup:
|
||||
npm install
|
||||
# The prepublish script in package.json will override the following variable,
|
||||
# setting it to the empty string and thereby avoiding an infinite recursion
|
||||
NIS = .npm-install.stamp
|
||||
|
||||
lint: katex.js server.js cli.js $(wildcard src/*.js) $(wildcard test/*.js) $(wildcard contrib/*/*.js)
|
||||
./node_modules/.bin/jshint $^
|
||||
$(NIS) setup: package.json
|
||||
KATEX_DIST=skip npm install # dependencies only, don't build
|
||||
@touch $(NIS)
|
||||
|
||||
build/katex.js: katex.js $(wildcard src/*.js)
|
||||
$(BROWSERIFY) $< --standalone katex > $@
|
||||
lint: $(NIS) katex.js server.js cli.js $(wildcard src/*.js) $(wildcard test/*.js) $(wildcard contrib/*/*.js) $(wildcard dockers/*/*.js)
|
||||
./node_modules/.bin/eslint $(filter-out %.stamp,$^)
|
||||
|
||||
build/katex.js: katex.js $(wildcard src/*.js) $(NIS)
|
||||
$(BROWSERIFY) -t [ babelify ] $< --standalone katex > $@
|
||||
|
||||
build/katex.min.js: build/katex.js
|
||||
$(UGLIFYJS) < $< > $@
|
||||
|
||||
build/katex.less.css: static/katex.less $(wildcard static/*.less)
|
||||
build/katex.css: static/katex.less $(wildcard static/*.less) $(NIS)
|
||||
./node_modules/.bin/lessc $< $@
|
||||
|
||||
build/katex.min.css: build/katex.less.css
|
||||
build/katex.min.css: build/katex.css
|
||||
./node_modules/.bin/cleancss -o $@ $<
|
||||
|
||||
.PHONY: build/fonts
|
||||
|
@ -38,18 +58,24 @@ build/fonts:
|
|||
cp static/fonts/$$font* $@; \
|
||||
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
|
||||
|
||||
.PHONY: build/contrib
|
||||
build/contrib:
|
||||
mkdir -p build/contrib
|
||||
# Since everything in build/contrib is put in the built files, make sure
|
||||
# there's nothing in there we don't want.
|
||||
@# Since everything in build/contrib is put in the built files, make sure
|
||||
@# there's nothing in there we don't want.
|
||||
rm -rf build/contrib/*
|
||||
$(MAKE) -C contrib/auto-render
|
||||
|
||||
.PHONY: build/katex
|
||||
build/katex: build/katex.min.js build/katex.min.css build/fonts README.md build/contrib
|
||||
build/katex: build/katex.js build/katex.min.js build/katex.css build/katex.min.css build/fonts README.md build/contrib
|
||||
mkdir -p build/katex
|
||||
rm -rf build/katex/*
|
||||
cp -r $^ build/katex
|
||||
|
@ -64,25 +90,30 @@ build/katex.zip: build/katex
|
|||
zip: build/katex.tar.gz build/katex.zip
|
||||
|
||||
compress: build/katex.min.js build/katex.min.css
|
||||
@$(eval JSSIZE!=gzip -c build/katex.min.js | wc -c)
|
||||
@$(eval CSSSIZE!=gzip -c build/katex.min.css | wc -c)
|
||||
@$(eval TOTAL!=echo ${JSSIZE}+${CSSSIZE} | bc)
|
||||
@printf "Minified, gzipped js: %6d\n" "${JSSIZE}"
|
||||
@printf "Minified, gzipped css: %6d\n" "${CSSSIZE}"
|
||||
@printf "Total: %6d\n" "${TOTAL}"
|
||||
@JSSIZE=`gzip -c build/katex.min.js | wc -c`; \
|
||||
CSSSIZE=`gzip -c build/katex.min.css | wc -c`; \
|
||||
TOTAL=`echo $${JSSIZE}+$${CSSSIZE} | bc`; \
|
||||
printf "Minified, gzipped js: %6d\n" "$${JSSIZE}"; \
|
||||
printf "Minified, gzipped css: %6d\n" "$${CSSSIZE}"; \
|
||||
printf "Total: %6d\n" "$${TOTAL}"
|
||||
|
||||
serve:
|
||||
node server.js
|
||||
serve: $(NIS)
|
||||
$(NODE) server.js
|
||||
|
||||
test:
|
||||
./node_modules/.bin/jasmine-node test/katex-spec.js
|
||||
./node_modules/.bin/jasmine-node contrib/auto-render/auto-render-spec.js
|
||||
test: $(NIS)
|
||||
JASMINE_CONFIG_PATH=test/jasmine.json node_modules/.bin/jasmine
|
||||
|
||||
PERL=perl
|
||||
PYTHON=$(shell python2 --version >/dev/null 2>&1 && echo python2 || echo python)
|
||||
|
||||
metrics:
|
||||
cd metrics && ./mapping.pl | ./extract_tfms.py | ./extract_ttfs.py | ./replace_line.py
|
||||
cd metrics && $(PERL) ./mapping.pl | $(PYTHON) ./extract_tfms.py | $(PYTHON) ./extract_ttfs.py | $(PYTHON) ./format_json.py > ../src/fontMetricsData.js
|
||||
|
||||
extended_metrics:
|
||||
cd metrics && $(PERL) ./mapping.pl | $(PYTHON) ./extract_tfms.py | $(PYTHON) ./extract_ttfs.py | $(PYTHON) ./format_json.py --width > ../src/fontMetricsData.js
|
||||
|
||||
clean:
|
||||
rm -rf build/*
|
||||
rm -rf build/* $(NIS)
|
||||
|
||||
screenshots:
|
||||
docker run --volume=$(shell pwd):/KaTeX ss
|
||||
screenshots: test/screenshotter/unicode-fonts $(NIS)
|
||||
dockers/Screenshotter/screenshotter.sh
|
||||
|
|
20
README.md
20
README.md
|
@ -1,21 +1,23 @@
|
|||
# [<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.
|
||||
|
||||
* **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/).
|
||||
* **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).
|
||||
* **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.
|
||||
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).
|
||||
|
||||
## 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>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js" integrity="sha384-/y1Nn9+QQAipbNQWU65krzJralCnuOasHncUFXGkdwntGeSvQicrYkiUBwsgUqc1" crossorigin="anonymous"></script>
|
||||
```
|
||||
|
||||
#### In-browser rendering
|
||||
|
@ -44,11 +46,19 @@ 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:
|
||||
|
||||
- `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:
|
||||
|
||||
```js
|
||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, { displayMode: true });
|
||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}\\in\\RR", element, {
|
||||
displayMode: true,
|
||||
macros: {
|
||||
"\\RR": "\\mathbb{R}"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Automatic rendering of math on a page
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"name": "KaTeX",
|
||||
"version": "0.3.0",
|
||||
"name": "katex",
|
||||
"main": [
|
||||
"dist/katex.js",
|
||||
"dist/katex.css"
|
||||
],
|
||||
"homepage": "http://khan.github.io/KaTeX/",
|
||||
"description": "Fast math typesetting for the web.",
|
||||
"moduleType": [
|
||||
|
@ -18,6 +21,7 @@
|
|||
"/*.txt",
|
||||
"/*.js",
|
||||
"/*.md",
|
||||
"/*.sh",
|
||||
"/package.json",
|
||||
"/Makefile",
|
||||
"/build",
|
||||
|
|
16
check-node-version.js
Normal file
16
check-node-version.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
"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,14 +1,15 @@
|
|||
#!/usr/bin/env node
|
||||
// Simple CLI for KaTeX.
|
||||
// Reads TeX from stdin, outputs HTML to stdout.
|
||||
/* eslint no-console:0 */
|
||||
|
||||
var katex = require("./");
|
||||
var input = "";
|
||||
const katex = require("./");
|
||||
let input = "";
|
||||
|
||||
// Skip the first two args, which are just "node" and "cli.js"
|
||||
var args = process.argv.slice(2);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.indexOf("--help") != -1) {
|
||||
if (args.indexOf("--help") !== -1) {
|
||||
console.log(process.argv[0] + " " + process.argv[1] +
|
||||
" [ --help ]" +
|
||||
" [ --display-mode ]");
|
||||
|
@ -25,7 +26,7 @@ process.stdin.on("data", function(chunk) {
|
|||
});
|
||||
|
||||
process.stdin.on("end", function() {
|
||||
var options = { displayMode: args.indexOf("--display-mode") != -1 };
|
||||
var output = katex.renderToString(input, options);
|
||||
const options = { displayMode: args.indexOf("--display-mode") !== -1 };
|
||||
const output = katex.renderToString(input, options);
|
||||
console.log(output);
|
||||
});
|
||||
|
|
|
@ -6,4 +6,4 @@ $(BUILDDIR)/contrib/auto-render.min.js: $(BUILDDIR)/auto-render.js
|
|||
$(UGLIFYJS) < $< > $@
|
||||
|
||||
$(BUILDDIR)/auto-render.js: auto-render.js
|
||||
$(BROWSERIFY) $< --standalone renderMathInElement > $@
|
||||
$(BROWSERIFY) -t [ babelify ] $< --standalone renderMathInElement > $@
|
||||
|
|
|
@ -10,7 +10,7 @@ This extension isn't part of KaTeX proper, so the script should be separately
|
|||
included in the page:
|
||||
|
||||
```html
|
||||
<script src="/path/to/auto-render.min.js"></script>
|
||||
<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>
|
||||
```
|
||||
|
||||
Then, call the exposed `renderMathInElement` function in a script tag
|
||||
|
@ -27,6 +27,22 @@ before the close body tag:
|
|||
|
||||
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
|
||||
|
||||
This extension exposes a single function, `window.renderMathInElement`, with
|
||||
|
@ -41,23 +57,24 @@ nodes inside this element and render the math in them.
|
|||
|
||||
`options` is an optional object argument with the following keys:
|
||||
|
||||
- `delimiters`: This is a list of delimiters to look for math. Each delimiter
|
||||
has three properties:
|
||||
- `delimiters`: This is a list of delimiters to look for math. Each delimiter
|
||||
has three properties:
|
||||
|
||||
- `left`: A string which starts the math expression (i.e. the left delimiter).
|
||||
- `right`: A string which ends the math expression (i.e. the right delimiter).
|
||||
- `display`: A boolean of whether the math in the expression should be
|
||||
rendered in display mode or not.
|
||||
- `left`: A string which starts the math expression (i.e. the left delimiter).
|
||||
- `right`: A string which ends the math expression (i.e. the right delimiter).
|
||||
- `display`: A boolean of whether the math in the expression should be
|
||||
rendered in display mode or not.
|
||||
|
||||
The default value is:
|
||||
```js
|
||||
[
|
||||
{left: "$$", right: "$$", display: true},
|
||||
{left: "\\[", right: "\\]", display: true},
|
||||
{left: "\\(", right: "\\)", display: false}
|
||||
]
|
||||
```
|
||||
The default value is:
|
||||
|
||||
```js
|
||||
[
|
||||
{left: "$$", right: "$$", display: true},
|
||||
{left: "\\[", right: "\\]", display: true},
|
||||
{left: "\\(", right: "\\)", display: false}
|
||||
]
|
||||
```
|
||||
|
||||
- `ignoredTags`: This is a list of DOM node types to ignore when recursing
|
||||
through. The default value is
|
||||
`["script", "noscript", "style", "textarea", "pre", "code"]`.
|
||||
- `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,21 +4,22 @@
|
|||
/* global it: false */
|
||||
/* global describe: false */
|
||||
|
||||
var splitAtDelimiters = require("./splitAtDelimiters");
|
||||
const splitAtDelimiters = require("./splitAtDelimiters");
|
||||
|
||||
beforeEach(function() {
|
||||
jasmine.addMatchers({
|
||||
toSplitInto: function() {
|
||||
return {
|
||||
compare: function(actual, left, right, result) {
|
||||
var message = {
|
||||
const message = {
|
||||
pass: true,
|
||||
message: "'" + actual + "' split correctly"
|
||||
message: "'" + actual + "' split correctly",
|
||||
};
|
||||
|
||||
var startData = [{type: "text", data: actual}];
|
||||
const startData = [{type: "text", data: actual}];
|
||||
|
||||
var split = splitAtDelimiters(startData, left, right, false);
|
||||
const split =
|
||||
splitAtDelimiters(startData, left, right, false);
|
||||
|
||||
if (split.length !== result.length) {
|
||||
message.pass = false;
|
||||
|
@ -29,12 +30,12 @@ beforeEach(function() {
|
|||
return message;
|
||||
}
|
||||
|
||||
for (var i = 0; i < split.length; i++) {
|
||||
var real = split[i];
|
||||
var correct = result[i];
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
const real = split[i];
|
||||
const correct = result[i];
|
||||
|
||||
var good = true;
|
||||
var diff;
|
||||
let good = true;
|
||||
let diff;
|
||||
|
||||
if (real.type !== correct.type) {
|
||||
good = false;
|
||||
|
@ -58,9 +59,9 @@ beforeEach(function() {
|
|||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -69,12 +70,12 @@ describe("A delimiter splitter", function() {
|
|||
expect("hello").toSplitInto("(", ")", [{type: "text", data: "hello"}]);
|
||||
});
|
||||
|
||||
it("doesn't create a math node when there's only a left delimiter", function() {
|
||||
it("doesn't create a math node with only one left delimiter", function() {
|
||||
expect("hello ( world").toSplitInto(
|
||||
"(", ")",
|
||||
[
|
||||
{type: "text", data: "hello "},
|
||||
{type: "text", data: "( world"}
|
||||
{type: "text", data: "( world"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -82,7 +83,7 @@ describe("A delimiter splitter", function() {
|
|||
expect("hello ) world").toSplitInto(
|
||||
"(", ")",
|
||||
[
|
||||
{type: "text", data: "hello ) world"}
|
||||
{type: "text", data: "hello ) world"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -93,7 +94,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "( world )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -104,7 +105,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "[[ world ]]", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -118,7 +119,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: " boo "},
|
||||
{type: "math", data: " more ",
|
||||
rawData: "( more )", display: false},
|
||||
{type: "text", data: " stuff"}
|
||||
{type: "text", data: " stuff"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -130,7 +131,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "math", data: " world ",
|
||||
rawData: "( world )", display: false},
|
||||
{type: "text", data: " boo "},
|
||||
{type: "text", data: "( left"}
|
||||
{type: "text", data: "( left"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -141,7 +142,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world { ) } ",
|
||||
rawData: "( world { ) } )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
|
||||
expect("hello ( world { { } ) } ) boo").toSplitInto(
|
||||
|
@ -150,7 +151,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world { { } ) } ",
|
||||
rawData: "( world { { } ) } )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -161,7 +162,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world \\) ",
|
||||
rawData: "( world \\) )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
|
||||
/* TODO(emily): make this work maybe?
|
||||
|
@ -171,7 +172,7 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello \\( "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "( world )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
*/
|
||||
});
|
||||
|
@ -183,27 +184,27 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "$ world $", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
it("remembers which delimiters are display-mode", function() {
|
||||
var startData = [{type: "text", data: "hello ( world ) boo"}];
|
||||
const startData = [{type: "text", data: "hello ( world ) boo"}];
|
||||
|
||||
expect(splitAtDelimiters(startData, "(", ")", true)).toEqual(
|
||||
[
|
||||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "( world )", display: true},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
it("works with more than one start datum", function() {
|
||||
var startData = [
|
||||
const startData = [
|
||||
{type: "text", data: "hello ( world ) boo"},
|
||||
{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(
|
||||
|
@ -216,15 +217,15 @@ describe("A delimiter splitter", function() {
|
|||
{type: "text", data: "hello "},
|
||||
{type: "math", data: " world ",
|
||||
rawData: "( world )", display: false},
|
||||
{type: "text", data: " boo"}
|
||||
{type: "text", data: " boo"},
|
||||
]);
|
||||
});
|
||||
|
||||
it("doesn't do splitting inside of math nodes", function() {
|
||||
var startData = [
|
||||
const startData = [
|
||||
{type: "text", 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(
|
||||
|
@ -234,7 +235,7 @@ describe("A delimiter splitter", function() {
|
|||
rawData: "( world )", display: false},
|
||||
{type: "text", data: " boo"},
|
||||
{type: "math", data: "hello ( world ) boo",
|
||||
rawData: "(hello ( world ) boo)", display: true}
|
||||
rawData: "(hello ( world ) boo)", display: true},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
/* eslint no-console:0 */
|
||||
/* global katex */
|
||||
|
||||
var splitAtDelimiters = require("./splitAtDelimiters");
|
||||
const splitAtDelimiters = require("./splitAtDelimiters");
|
||||
|
||||
var splitWithDelimiters = function(text, delimiters) {
|
||||
var data = [{type: "text", data: text}];
|
||||
for (var i = 0; i < delimiters.length; i++) {
|
||||
var delimiter = delimiters[i];
|
||||
const splitWithDelimiters = function(text, delimiters) {
|
||||
let data = [{type: "text", data: text}];
|
||||
for (let i = 0; i < delimiters.length; i++) {
|
||||
const delimiter = delimiters[i];
|
||||
data = splitAtDelimiters(
|
||||
data, delimiter.left, delimiter.right,
|
||||
delimiter.display || false);
|
||||
|
@ -13,20 +14,20 @@ var splitWithDelimiters = function(text, delimiters) {
|
|||
return data;
|
||||
};
|
||||
|
||||
var renderMathInText = function(text, delimiters) {
|
||||
var data = splitWithDelimiters(text, delimiters);
|
||||
const renderMathInText = function(text, delimiters) {
|
||||
const data = splitWithDelimiters(text, delimiters);
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "text") {
|
||||
fragment.appendChild(document.createTextNode(data[i].data));
|
||||
} else {
|
||||
var span = document.createElement("span");
|
||||
var math = data[i].data;
|
||||
const span = document.createElement("span");
|
||||
const math = data[i].data;
|
||||
try {
|
||||
katex.render(math, span, {
|
||||
displayMode: data[i].display
|
||||
displayMode: data[i].display,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!(e instanceof katex.ParseError)) {
|
||||
|
@ -47,17 +48,17 @@ var renderMathInText = function(text, delimiters) {
|
|||
return fragment;
|
||||
};
|
||||
|
||||
var renderElem = function(elem, delimiters, ignoredTags) {
|
||||
for (var i = 0; i < elem.childNodes.length; i++) {
|
||||
var childNode = elem.childNodes[i];
|
||||
const renderElem = function(elem, delimiters, ignoredTags) {
|
||||
for (let i = 0; i < elem.childNodes.length; i++) {
|
||||
const childNode = elem.childNodes[i];
|
||||
if (childNode.nodeType === 3) {
|
||||
// Text node
|
||||
var frag = renderMathInText(childNode.textContent, delimiters);
|
||||
const frag = renderMathInText(childNode.textContent, delimiters);
|
||||
i += frag.childNodes.length - 1;
|
||||
elem.replaceChild(frag, childNode);
|
||||
} else if (childNode.nodeType === 1) {
|
||||
// Element node
|
||||
var shouldRender = ignoredTags.indexOf(
|
||||
const shouldRender = ignoredTags.indexOf(
|
||||
childNode.nodeName.toLowerCase()) === -1;
|
||||
|
||||
if (shouldRender) {
|
||||
|
@ -68,24 +69,26 @@ var renderElem = function(elem, delimiters, ignoredTags) {
|
|||
}
|
||||
};
|
||||
|
||||
var defaultOptions = {
|
||||
const defaultOptions = {
|
||||
delimiters: [
|
||||
{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:
|
||||
// {left: "$", right: "$", display: false}
|
||||
// {left: "$", right: "$", display: false},
|
||||
],
|
||||
|
||||
ignoredTags: [
|
||||
"script", "noscript", "style", "textarea", "pre", "code"
|
||||
]
|
||||
"script", "noscript", "style", "textarea", "pre", "code",
|
||||
],
|
||||
};
|
||||
|
||||
var extend = function(obj) {
|
||||
const extend = function(obj) {
|
||||
// Adapted from underscore.js' `_.extend`. See LICENSE.txt for license.
|
||||
var source, prop;
|
||||
for (var i = 1, length = arguments.length; i < length; i++) {
|
||||
let source;
|
||||
let prop;
|
||||
const length = arguments.length;
|
||||
for (let i = 1; i < length; i++) {
|
||||
source = arguments[i];
|
||||
for (prop in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, prop)) {
|
||||
|
@ -96,7 +99,7 @@ var extend = function(obj) {
|
|||
return obj;
|
||||
};
|
||||
|
||||
var renderMathInElement = function(elem, options) {
|
||||
const renderMathInElement = function(elem, options) {
|
||||
if (!elem) {
|
||||
throw new Error("No element provided to render");
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
var findEndOfMath = function(delimiter, text, startIndex) {
|
||||
/* eslint no-constant-condition:0 */
|
||||
const findEndOfMath = function(delimiter, text, startIndex) {
|
||||
// Adapted from
|
||||
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
|
||||
var index = startIndex;
|
||||
var braceLevel = 0;
|
||||
let index = startIndex;
|
||||
let braceLevel = 0;
|
||||
|
||||
var delimLength = delimiter.length;
|
||||
const delimLength = delimiter.length;
|
||||
|
||||
while (index < text.length) {
|
||||
var character = text[index];
|
||||
const character = text[index];
|
||||
|
||||
if (braceLevel <= 0 &&
|
||||
text.slice(index, index + delimLength) === delimiter) {
|
||||
|
@ -26,23 +27,23 @@ var findEndOfMath = function(delimiter, text, startIndex) {
|
|||
return -1;
|
||||
};
|
||||
|
||||
var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
|
||||
var finalData = [];
|
||||
const splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
|
||||
const finalData = [];
|
||||
|
||||
for (var i = 0; i < startData.length; i++) {
|
||||
for (let i = 0; i < startData.length; i++) {
|
||||
if (startData[i].type === "text") {
|
||||
var text = startData[i].data;
|
||||
const text = startData[i].data;
|
||||
|
||||
var lookingForLeft = true;
|
||||
var currIndex = 0;
|
||||
var nextIndex;
|
||||
let lookingForLeft = true;
|
||||
let currIndex = 0;
|
||||
let nextIndex;
|
||||
|
||||
nextIndex = text.indexOf(leftDelim);
|
||||
if (nextIndex !== -1) {
|
||||
currIndex = nextIndex;
|
||||
finalData.push({
|
||||
type: "text",
|
||||
data: text.slice(0, currIndex)
|
||||
data: text.slice(0, currIndex),
|
||||
});
|
||||
lookingForLeft = false;
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
|
|||
|
||||
finalData.push({
|
||||
type: "text",
|
||||
data: text.slice(currIndex, nextIndex)
|
||||
data: text.slice(currIndex, nextIndex),
|
||||
});
|
||||
|
||||
currIndex = nextIndex;
|
||||
|
@ -77,7 +78,7 @@ var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
|
|||
rawData: text.slice(
|
||||
currIndex,
|
||||
nextIndex + rightDelim.length),
|
||||
display: display
|
||||
display: display,
|
||||
});
|
||||
|
||||
currIndex = nextIndex + rightDelim.length;
|
||||
|
@ -88,7 +89,7 @@ var splitAtDelimiters = function(startData, leftDelim, rightDelim, display) {
|
|||
|
||||
finalData.push({
|
||||
type: "text",
|
||||
data: text.slice(currIndex)
|
||||
data: text.slice(currIndex),
|
||||
});
|
||||
} else {
|
||||
finalData.push(startData[i]);
|
||||
|
|
|
@ -2,8 +2,28 @@ FROM ubuntu:14.04
|
|||
MAINTAINER xymostech <xymostech@gmail.com>
|
||||
|
||||
# Install things
|
||||
RUN apt-get -qq update
|
||||
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
|
||||
RUN apt-get -qq update && 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 \
|
||||
pkg-config \
|
||||
libharfbuzz-dev \
|
||||
libfreetype6-dev || true
|
||||
RUN gem install ttfunk --version 1.1.1
|
||||
|
||||
# Download yuicompressor
|
||||
|
@ -11,7 +31,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
|
||||
|
||||
# Download batik-ttf2svg.jar
|
||||
RUN wget "http://supergsego.com/apache/xmlgraphics/batik/batik-1.7.zip"
|
||||
RUN wget "https://archive.apache.org/dist/xmlgraphics/batik/batik-1.7.zip"
|
||||
RUN unzip -qq batik-1.7.zip
|
||||
RUN mv batik-1.7/batik-ttf2svg.jar /usr/share/java/
|
||||
|
||||
|
@ -22,6 +42,13 @@ RUN sed -i "1s/^/#include <cstddef>/" ttf2eot-0.0.2-2/OpenTypeUtilities.h
|
|||
RUN make -C ttf2eot-0.0.2-2/
|
||||
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
|
||||
RUN git clone "https://code.google.com/p/font-compression-reference/" woff2_compress
|
||||
RUN make -C woff2_compress/woff2/
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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,29 +1,63 @@
|
|||
### 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)
|
||||
have them look mostly the same as the current ones! To start, make a docker
|
||||
image from the included Dockerfile using a command like
|
||||
have them look mostly the same as the current ones! Make sure you have docker
|
||||
installed and running.
|
||||
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:
|
||||
|
||||
docker build --tag=ss .
|
||||
dockers/Screenshotter/screenshotter.sh
|
||||
|
||||
from within this directory (note you need to have docker installed and running
|
||||
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.
|
||||
It will fetch all required selenium docker images, and use them to
|
||||
take screenshots.
|
||||
|
||||
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:
|
||||
## Manual generation
|
||||
|
||||
docker run --volume=/your/KaTeX/:/KaTeX ss
|
||||
If you are creating screenshots on a regular basis, you can keep the
|
||||
docker containers with the selenium setups running. Essentially you
|
||||
are encouraged to reproduce the steps from `screenshotter.sh`
|
||||
manually. Example run for Firefox:
|
||||
|
||||
The `--volume=/your/KaTeX:/KaTeX` switch mounts your KaTeX directory into the
|
||||
docker. Note this is a read-write mounting, so the new screenshots will be
|
||||
directly placed into your KaTeX directory.
|
||||
container=$(docker run -d -P selenium/standalone-firefox:2.46.0)
|
||||
node dockers/Screenshotter/screenshotter.js -b firefox -c ${container}
|
||||
# possibly repeat the above command as often as you need, then eventually
|
||||
docker stop ${container}
|
||||
docker rm ${container}
|
||||
|
||||
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.
|
||||
For Chrome, simply replace both occurrences of `firefox` with `chrome`.
|
||||
|
||||
That's it!
|
||||
## Use without docker
|
||||
|
||||
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
|
||||
|
|
465
dockers/Screenshotter/screenshotter.js
Normal file
465
dockers/Screenshotter/screenshotter.js
Normal file
|
@ -0,0 +1,465 @@
|
|||
/* 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;
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
#!/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()
|
49
dockers/Screenshotter/screenshotter.sh
Executable file
49
dockers/Screenshotter/screenshotter.sh
Executable file
|
@ -0,0 +1,49 @@
|
|||
#!/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}
|
11
dockers/texcmp/Dockerfile
Normal file
11
dockers/texcmp/Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
# 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
|
82
dockers/texcmp/README.md
Normal file
82
dockers/texcmp/README.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
# 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
|
10
dockers/texcmp/package.json
Normal file
10
dockers/texcmp/package.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
255
dockers/texcmp/texcmp.js
Normal file
255
dockers/texcmp/texcmp.js
Normal file
|
@ -0,0 +1,255 @@
|
|||
/* 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]);
|
||||
}
|
17
dockers/texcmp/texcmp.sh
Executable file
17
dockers/texcmp/texcmp.sh
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/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,3 +1,4 @@
|
|||
/* eslint no-console:0 */
|
||||
/**
|
||||
* This is the main entry point for KaTeX. Here, we expose functions for
|
||||
* rendering expressions either to DOM nodes or to markup strings.
|
||||
|
@ -6,24 +7,24 @@
|
|||
* errors in the expression, or errors in javascript handling.
|
||||
*/
|
||||
|
||||
var ParseError = require("./src/ParseError");
|
||||
var Settings = require("./src/Settings");
|
||||
const ParseError = require("./src/ParseError");
|
||||
const Settings = require("./src/Settings");
|
||||
|
||||
var buildTree = require("./src/buildTree");
|
||||
var parseTree = require("./src/parseTree");
|
||||
var utils = require("./src/utils");
|
||||
const buildTree = require("./src/buildTree");
|
||||
const parseTree = require("./src/parseTree");
|
||||
const utils = require("./src/utils");
|
||||
|
||||
/**
|
||||
* Parse and build an expression, and place that expression in the DOM node
|
||||
* given.
|
||||
*/
|
||||
var render = function(expression, baseNode, options) {
|
||||
let render = function(expression, baseNode, options) {
|
||||
utils.clearNode(baseNode);
|
||||
|
||||
var settings = new Settings(options);
|
||||
const settings = new Settings(options);
|
||||
|
||||
var tree = parseTree(expression, settings);
|
||||
var node = buildTree(tree, expression, settings).toNode();
|
||||
const tree = parseTree(expression, settings);
|
||||
const node = buildTree(tree, expression, settings).toNode();
|
||||
|
||||
baseNode.appendChild(node);
|
||||
};
|
||||
|
@ -45,18 +46,18 @@ if (typeof document !== "undefined") {
|
|||
/**
|
||||
* Parse and build an expression, and return the markup for that.
|
||||
*/
|
||||
var renderToString = function(expression, options) {
|
||||
var settings = new Settings(options);
|
||||
const renderToString = function(expression, options) {
|
||||
const settings = new Settings(options);
|
||||
|
||||
var tree = parseTree(expression, settings);
|
||||
const tree = parseTree(expression, settings);
|
||||
return buildTree(tree, expression, settings).toMarkup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an expression and return the parse tree.
|
||||
*/
|
||||
var generateParseTree = function(expression, options) {
|
||||
var settings = new Settings(options);
|
||||
const generateParseTree = function(expression, options) {
|
||||
const settings = new Settings(options);
|
||||
return parseTree(expression, settings);
|
||||
};
|
||||
|
||||
|
@ -69,5 +70,5 @@ module.exports = {
|
|||
* to change. Use at your own risk.
|
||||
*/
|
||||
__parse: generateParseTree,
|
||||
ParseError: ParseError
|
||||
ParseError: ParseError,
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
# Autogenerated code
|
||||
build/**
|
||||
node_modules/**
|
||||
dist/**
|
||||
|
||||
# Third party code
|
||||
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
|
||||
> kpathsea version 6.2.0
|
||||
|
||||
- You need the JSON module for perl. You can install this either from CPAN or with
|
||||
your package manager.
|
||||
- You need the JSON module for perl. You can install this either from CPAN
|
||||
(possibly using the `cpan` command line tool) or with your package manager.
|
||||
|
||||
- You need the python fontforge module. This is probably either installed with
|
||||
fontforge or can be installed from your package manager.
|
||||
- You need the python module fonttools. You can install this either from PyPi
|
||||
(using `easy_install` or `pip`) or with your package manager.
|
||||
|
||||
Once you have these things, run
|
||||
|
||||
make metrics
|
||||
|
||||
which should generate new metrics and place them into `fontMetrics.js`. You're
|
||||
done!
|
||||
which should generate new metrics and place them into `fontMetricsData.json`.
|
||||
You're done!
|
||||
|
|
|
@ -31,7 +31,11 @@ def main():
|
|||
'cmsy10.tfm',
|
||||
'cmti10.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
|
||||
|
@ -48,7 +52,11 @@ def main():
|
|||
'cmsy10': 48,
|
||||
'cmti10': None,
|
||||
'msam10': None,
|
||||
'msbm10': None
|
||||
'msbm10': None,
|
||||
'eufm10': None,
|
||||
'cmtt10': None,
|
||||
'rsfs10': None,
|
||||
'cmss10': None,
|
||||
}
|
||||
|
||||
font_name_to_tfm = {}
|
||||
|
@ -68,11 +76,16 @@ def main():
|
|||
tex_char_num = int(char_data['char'])
|
||||
yshift = float(char_data['yshift'])
|
||||
|
||||
tfm_char = font_name_to_tfm[font].get_char_metrics(tex_char_num)
|
||||
if family == "Script-Regular":
|
||||
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)
|
||||
depth = round(tfm_char.depth - yshift / 1000.0, 5)
|
||||
italic = round(tfm_char.italic_correction, 5)
|
||||
width = round(tfm_char.width, 5)
|
||||
|
||||
skewkern = 0.0
|
||||
if (font_skewchar[font] and
|
||||
|
@ -85,6 +98,7 @@ def main():
|
|||
'depth': depth,
|
||||
'italic': italic,
|
||||
'skew': skewkern,
|
||||
'width': width
|
||||
}
|
||||
|
||||
sys.stdout.write(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import fontforge
|
||||
from fontTools.ttLib import TTFont
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
@ -60,36 +60,53 @@ def main():
|
|||
start_json = json.load(sys.stdin)
|
||||
|
||||
for font, chars in metrics_to_extract.iteritems():
|
||||
fontInfo = fontforge.open("../static/fonts/KaTeX_" + font + ".ttf")
|
||||
fontInfo = TTFont("../static/fonts/KaTeX_" + font + ".ttf")
|
||||
glyf = fontInfo["glyf"]
|
||||
unitsPerEm = float(fontInfo["head"].unitsPerEm)
|
||||
|
||||
for glyph in fontInfo.glyphs():
|
||||
try:
|
||||
char = unichr(glyph.unicode)
|
||||
except ValueError:
|
||||
# We keep ALL Unicode cmaps, not just fontInfo["cmap"].getcmap(3, 1).
|
||||
# This is playing it extra safe, since it reports inconsistencies.
|
||||
# Platform 0 is Unicode, platform 3 is Windows. For platform 3,
|
||||
# encoding 1 is UCS-2 and encoding 10 is UCS-4.
|
||||
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
|
||||
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()
|
||||
|
||||
if char in chars:
|
||||
_, depth, _, height = glyph.boundingBox()
|
||||
height = depth = italic = skew = width = 0
|
||||
glyph = glyf[name]
|
||||
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"]
|
||||
|
||||
depth = -depth
|
||||
|
||||
base_char = chars[char]
|
||||
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"]
|
||||
else:
|
||||
italic = 0
|
||||
skew = 0
|
||||
|
||||
start_json[font][ord(char)] = {
|
||||
"height": height / fontInfo.em,
|
||||
"depth": depth / fontInfo.em,
|
||||
"italic": italic,
|
||||
"skew": skew,
|
||||
}
|
||||
start_json[font][str(code)] = {
|
||||
"height": height / unitsPerEm,
|
||||
"depth": depth / unitsPerEm,
|
||||
"italic": italic,
|
||||
"skew": skew,
|
||||
"width": width
|
||||
}
|
||||
|
||||
sys.stdout.write(
|
||||
json.dumps(start_json, separators=(',', ':'), sort_keys=True))
|
||||
|
|
26
metrics/format_json.py
Normal file
26
metrics/format_json.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
#!/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,8 +135,6 @@ $map{cmmi10} = {
|
|||
0x2E => 0x25B9, # \triangleright
|
||||
0x2F => 0x25C3, # \triangleleft
|
||||
|
||||
0x3A => 0x2E, # .
|
||||
0x3B => 0x2C, # ,
|
||||
0x3C => 0x3C, # <
|
||||
0x3D => 0x2215, # /
|
||||
0x3E => 0x3E, # >
|
||||
|
@ -148,16 +146,23 @@ $map{cmmi10} = {
|
|||
0x5F => 0x2322, # \frown
|
||||
0x60 => 0x2113, # \ell
|
||||
|
||||
0x7B => 0x131, # \imath
|
||||
0x7C => 0x237, # \jmath
|
||||
0x7D => 0x2118, # \wp
|
||||
0x7E => [0x20D7,-653,0],# \vec
|
||||
]
|
||||
],
|
||||
|
||||
"Main-Italic" => [
|
||||
0x7B => 0x131, # \imath
|
||||
0x7C => 0x237, # \jmath
|
||||
],
|
||||
|
||||
"Caligraphic-Regular" => [
|
||||
[0x30,0x39] => 0x30, # Oldstyle 0-9
|
||||
],
|
||||
};
|
||||
|
||||
$map{cmsy10} = {
|
||||
"Main-Regular" => [
|
||||
[0,1] => 0x2212, # -
|
||||
0 => 0x2212, # -
|
||||
1 => 0x22C5, # \cdot
|
||||
2 => 0xD7, # \times
|
||||
3 => 0x2217, # \ast
|
||||
|
@ -256,6 +261,10 @@ $map{cmsy10} = {
|
|||
"Math-Italic" => [
|
||||
0x36 => 0x2F # \not
|
||||
],
|
||||
|
||||
"Caligraphic-Regular" => [
|
||||
[0x41,0x5A] => 0x41, # A-Z
|
||||
],
|
||||
};
|
||||
|
||||
$map{cmex10} = {
|
||||
|
@ -425,8 +434,6 @@ $map{cmti10} = {
|
|||
[7,8] => 0x3A5, # \Upsilon, \Phi
|
||||
[9,0xA] => 0x3A8, # \Psi, \Omega
|
||||
|
||||
0x10 => 0x131, # \imath (roman)
|
||||
0x11 => 0x237, # \jmath (roman)
|
||||
0x12 => [0x300,-511,0], # \grave (combining)
|
||||
0x13 => [0x301,-511,0], # \acute (combining)
|
||||
0x14 => [0x30C,-511,0], # \check (combining)
|
||||
|
@ -557,8 +564,6 @@ $map{cmmib10} = {
|
|||
0x2E => 0x25B9, # \triangleright
|
||||
0x2F => 0x25C3, # \triangleleft
|
||||
|
||||
0x3A => 0x2E, # .
|
||||
0x3B => 0x2C, # ,
|
||||
0x3C => 0x3C, # <
|
||||
0x3D => 0x2215, # /
|
||||
0x3E => 0x3E, # >
|
||||
|
@ -571,8 +576,6 @@ $map{cmmib10} = {
|
|||
0x60 => 0x2113, # \ell
|
||||
0x68 => 0x210F, # \hbar (bar added below)
|
||||
|
||||
0x7B => 0x131, # \imath
|
||||
0x7C => 0x237, # \jmath
|
||||
0x7D => 0x2118, # \wp
|
||||
0x7E => [0x20D7,-729,0],# \vec
|
||||
],
|
||||
|
@ -580,7 +583,7 @@ $map{cmmib10} = {
|
|||
|
||||
$map{cmbsy10} = {
|
||||
"Main-Bold" => [
|
||||
[0,1] => 0x2212, # -
|
||||
0 => 0x2212, # -
|
||||
1 => 0x22C5, # \cdot
|
||||
2 => 0xD7, # \times
|
||||
3 => 0x2217, # \ast
|
||||
|
@ -944,6 +947,106 @@ $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 $mjfont (keys %{$map{$cmfont}}) {
|
||||
$style = $mjfont; $style =~ s/.*?(-|$)//; $style = "Regular" unless $style;
|
||||
|
@ -973,6 +1076,12 @@ sub add_to_output {
|
|||
"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;
|
||||
}
|
||||
|
||||
|
|
|
@ -64,11 +64,21 @@ class TfmFile(object):
|
|||
self.ligkern_program = LigKernProgram(ligkern_table)
|
||||
self.kern_table = kern_table
|
||||
|
||||
def get_char_metrics(self, char_num):
|
||||
def get_char_metrics(self, char_num, fix_rsfs=False):
|
||||
"""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:
|
||||
raise RuntimeError("Invalid character number")
|
||||
|
||||
info = self.char_info[char_num + self.start_char]
|
||||
if fix_rsfs:
|
||||
# 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 = {}
|
||||
if info.has_ligkern():
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
#!/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",
|
||||
"version": "0.3.0",
|
||||
"version": "0.8.0-pre",
|
||||
"description": "Fast math typesetting for the web.",
|
||||
"main": "katex.js",
|
||||
"repository": {
|
||||
|
@ -10,21 +10,36 @@
|
|||
"files": [
|
||||
"katex.js",
|
||||
"cli.js",
|
||||
"src/"
|
||||
"src/",
|
||||
"dist/"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"browserify": "~2.29.1",
|
||||
"clean-css": "~2.2.15",
|
||||
"express": "~3.3.3",
|
||||
"jasmine-node": "2.0.0-beta4",
|
||||
"jshint": "^2.5.6",
|
||||
"less": "~1.7.5",
|
||||
"uglify-js": "~2.4.15"
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babelify": "^7.3.0",
|
||||
"browserify": "^13.3.0",
|
||||
"clean-css": "^3.4.23",
|
||||
"eslint": "^3.13.0",
|
||||
"express": "^4.14.0",
|
||||
"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",
|
||||
"scripts": {
|
||||
"test": "make lint test"
|
||||
"test": "make lint test",
|
||||
"prepublish": "make NIS= dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"match-at": "^0.1.0"
|
||||
|
|
165
release.sh
Executable file
165
release.sh
Executable file
|
@ -0,0 +1,165 @@
|
|||
#!/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,27 +1,42 @@
|
|||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
/* eslint no-console:0 */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
var browserify = require("browserify");
|
||||
var express = require("express");
|
||||
var less = require("less");
|
||||
const babelify = require("babelify");
|
||||
const browserify = require("browserify");
|
||||
const express = require("express");
|
||||
const glob = require("glob");
|
||||
const less = require("less");
|
||||
|
||||
var app = express();
|
||||
const app = express();
|
||||
|
||||
app.use(express.logger());
|
||||
if (require.main === module) {
|
||||
app.use(require("morgan")(
|
||||
":date[iso] :method :url HTTP/:http-version - :status"));
|
||||
}
|
||||
|
||||
var serveBrowserified = function(file, standaloneName) {
|
||||
function serveBrowserified(file, standaloneName, doBabelify) {
|
||||
return function(req, res, next) {
|
||||
var b = browserify();
|
||||
b.add(file);
|
||||
let files;
|
||||
if (Array.isArray(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)];
|
||||
}
|
||||
|
||||
var options = {};
|
||||
const options = {};
|
||||
if (doBabelify) {
|
||||
options.transform = [babelify];
|
||||
}
|
||||
if (standaloneName) {
|
||||
options.standalone = standaloneName;
|
||||
}
|
||||
const b = browserify(files, options);
|
||||
const stream = b.bundle();
|
||||
|
||||
var stream = b.bundle(options);
|
||||
|
||||
var body = "";
|
||||
let body = "";
|
||||
stream.on("data", function(s) { body += s; });
|
||||
stream.on("error", function(e) { next(e); });
|
||||
stream.on("end", function() {
|
||||
|
@ -29,42 +44,59 @@ var serveBrowserified = function(file, standaloneName) {
|
|||
res.send(body);
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
app.get("/katex.js", serveBrowserified("./katex", "katex"));
|
||||
app.get("/test/katex-spec.js", serveBrowserified("./test/katex-spec"));
|
||||
app.get("/contrib/auto-render/auto-render.js",
|
||||
serveBrowserified("./contrib/auto-render/auto-render",
|
||||
"renderMathInElement"));
|
||||
function twoBrowserified(url, file, standaloneName) {
|
||||
app.get(url, serveBrowserified(file, standaloneName, false));
|
||||
app.get("/babel" + url, serveBrowserified(file, standaloneName, true));
|
||||
}
|
||||
|
||||
app.get("/katex.css", function(req, res, next) {
|
||||
fs.readFile("static/katex.less", {encoding: "utf8"}, function(err, data) {
|
||||
function twoUse(url, handler) {
|
||||
app.use(url, handler);
|
||||
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) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var parser = new less.Parser({
|
||||
paths: ["./static"],
|
||||
filename: "katex.less"
|
||||
});
|
||||
|
||||
parser.parse(data, function(err, tree) {
|
||||
less.render(data, {
|
||||
paths: [path.join(__dirname, "static")],
|
||||
filename: "katex.less",
|
||||
}, function(err, output) {
|
||||
if (err) {
|
||||
console.error(String(err));
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/css");
|
||||
res.send(tree.toCSS());
|
||||
res.send(output.css);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.use(express["static"](path.join(__dirname, "static")));
|
||||
app.use(express["static"](path.join(__dirname, "build")));
|
||||
app.use("/test", express["static"](path.join(__dirname, "test")));
|
||||
app.use("/contrib", express["static"](path.join(__dirname, "contrib")));
|
||||
twoStatic("", "static");
|
||||
twoStatic("", "build");
|
||||
twoStatic("/test", "test");
|
||||
twoStatic("/contrib", "contrib");
|
||||
|
||||
app.use(function(err, req, res, next) {
|
||||
console.error(err.stack);
|
||||
|
@ -72,5 +104,9 @@ app.use(function(err, req, res, next) {
|
|||
res.send(500, err.stack);
|
||||
});
|
||||
|
||||
app.listen(7936);
|
||||
console.log("Serving on http://0.0.0.0:7936/ ...");
|
||||
if (require.main === module) {
|
||||
app.listen(7936);
|
||||
console.log("Serving on http://0.0.0.0:7936/ ...");
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
|
|
237
src/Lexer.js
237
src/Lexer.js
|
@ -11,184 +11,99 @@
|
|||
* kinds.
|
||||
*/
|
||||
|
||||
var matchAt = require("match-at");
|
||||
const matchAt = require("match-at");
|
||||
|
||||
var ParseError = require("./ParseError");
|
||||
const ParseError = require("./ParseError");
|
||||
|
||||
// The main lexer class
|
||||
function Lexer(input) {
|
||||
this._input = input;
|
||||
this.input = input;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
// The resulting token returned from `lex`.
|
||||
function Token(text, data, position) {
|
||||
/**
|
||||
* The resulting token returned from `lex`.
|
||||
*
|
||||
* 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.data = data;
|
||||
this.position = position;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
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
|
||||
];
|
||||
/**
|
||||
* Given a pair of tokens (this and endToken), compute a “Token” encompassing
|
||||
* the whole input range enclosed by these two.
|
||||
*
|
||||
* @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) {
|
||||
if (endToken.lexer !== this.lexer) {
|
||||
return new Token(text); // sorry, no position information available
|
||||
}
|
||||
return new Token(text, this.start, endToken.end, this.lexer);
|
||||
};
|
||||
|
||||
// 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]+|.)/;
|
||||
/* The following tokenRegex
|
||||
* - matches typical whitespace (but not NBSP etc.) using its first group
|
||||
* - 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 normal token. It takes a position, a list of
|
||||
* "normal" tokens to try, and whether it should completely ignore whitespace or
|
||||
* not.
|
||||
* This function lexes a single token.
|
||||
*/
|
||||
Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
|
||||
var input = this._input;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no more input to parse, return an EOF token
|
||||
Lexer.prototype.lex = function() {
|
||||
const input = this.input;
|
||||
const pos = this.pos;
|
||||
if (pos === input.length) {
|
||||
return new Token("EOF", null, pos);
|
||||
return new Token("EOF", pos, pos, this);
|
||||
}
|
||||
|
||||
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(
|
||||
const match = matchAt(tokenRegex, input, pos);
|
||||
if (match === null) {
|
||||
throw new ParseError(
|
||||
"Unexpected character: '" + input[pos] + "'",
|
||||
this, pos);
|
||||
};
|
||||
|
||||
// A regex to match a CSS color (like #ffffff or BlueViolet)
|
||||
var cssColor = /#[a-z0-9]+|[a-z]+/i;
|
||||
|
||||
/**
|
||||
* This function lexes a CSS color.
|
||||
*/
|
||||
Lexer.prototype._innerLexColor = function(pos) {
|
||||
var input = this._input;
|
||||
|
||||
// Ignore whitespace
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// A regex to match a dimension. Dimensions look like
|
||||
// "1.2em" or ".4pt" or "1 ex"
|
||||
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);
|
||||
new Token(input[pos], pos, pos + 1, this));
|
||||
}
|
||||
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;
|
||||
|
|
146
src/MacroExpander.js
Normal file
146
src/MacroExpander.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* 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, and color of the
|
||||
* current parse level. It also contains the style and size of the parent parse
|
||||
* level, so size changes can be handled efficiently.
|
||||
* This is the main options class. It contains the style, size, color, and font
|
||||
* of the current parse level. It also contains the style and size of the parent
|
||||
* parse level, so size changes can be handled efficiently.
|
||||
*
|
||||
* 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
|
||||
|
@ -19,6 +19,7 @@ function Options(data) {
|
|||
this.color = data.color;
|
||||
this.size = data.size;
|
||||
this.phantom = data.phantom;
|
||||
this.font = data.font;
|
||||
|
||||
if (data.parentStyle === undefined) {
|
||||
this.parentStyle = data.style;
|
||||
|
@ -38,16 +39,17 @@ function Options(data) {
|
|||
* from "extension" will be copied to the new options object.
|
||||
*/
|
||||
Options.prototype.extend = function(extension) {
|
||||
var data = {
|
||||
const data = {
|
||||
style: this.style,
|
||||
size: this.size,
|
||||
color: this.color,
|
||||
parentStyle: this.style,
|
||||
parentSize: this.size,
|
||||
phantom: this.phantom
|
||||
phantom: this.phantom,
|
||||
font: this.font,
|
||||
};
|
||||
|
||||
for (var key in extension) {
|
||||
for (const key in extension) {
|
||||
if (extension.hasOwnProperty(key)) {
|
||||
data[key] = extension[key];
|
||||
}
|
||||
|
@ -61,7 +63,7 @@ Options.prototype.extend = function(extension) {
|
|||
*/
|
||||
Options.prototype.withStyle = function(style) {
|
||||
return this.extend({
|
||||
style: style
|
||||
style: style,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -70,7 +72,7 @@ Options.prototype.withStyle = function(style) {
|
|||
*/
|
||||
Options.prototype.withSize = function(size) {
|
||||
return this.extend({
|
||||
size: size
|
||||
size: size,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -79,7 +81,7 @@ Options.prototype.withSize = function(size) {
|
|||
*/
|
||||
Options.prototype.withColor = function(color) {
|
||||
return this.extend({
|
||||
color: color
|
||||
color: color,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -88,7 +90,16 @@ Options.prototype.withColor = function(color) {
|
|||
*/
|
||||
Options.prototype.withPhantom = function() {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -104,7 +115,7 @@ Options.prototype.reset = function() {
|
|||
* A map of color names to CSS colors.
|
||||
* TODO(emily): Remove this when we have real macros
|
||||
*/
|
||||
var colorMap = {
|
||||
const colorMap = {
|
||||
"katex-blue": "#6495ed",
|
||||
"katex-orange": "#ffa500",
|
||||
"katex-pink": "#ff00af",
|
||||
|
@ -112,55 +123,55 @@ var colorMap = {
|
|||
"katex-green": "#28ae7b",
|
||||
"katex-gray": "gray",
|
||||
"katex-purple": "#9d38bd",
|
||||
"katex-blueA": "#c7e9f1",
|
||||
"katex-blueB": "#9cdceb",
|
||||
"katex-blueC": "#58c4dd",
|
||||
"katex-blueD": "#29abca",
|
||||
"katex-blueE": "#1c758a",
|
||||
"katex-tealA": "#acead7",
|
||||
"katex-tealB": "#76ddc0",
|
||||
"katex-tealC": "#5cd0b3",
|
||||
"katex-tealD": "#55c1a7",
|
||||
"katex-tealE": "#49a88f",
|
||||
"katex-greenA": "#c9e2ae",
|
||||
"katex-greenB": "#a6cf8c",
|
||||
"katex-greenC": "#83c167",
|
||||
"katex-greenD": "#77b05d",
|
||||
"katex-greenE": "#699c52",
|
||||
"katex-goldA": "#f7c797",
|
||||
"katex-goldB": "#f9b775",
|
||||
"katex-goldC": "#f0ac5f",
|
||||
"katex-goldD": "#e1a158",
|
||||
"katex-goldE": "#c78d46",
|
||||
"katex-redA": "#f7a1a3",
|
||||
"katex-redB": "#ff8080",
|
||||
"katex-redC": "#fc6255",
|
||||
"katex-redD": "#e65a4c",
|
||||
"katex-redE": "#cf5044",
|
||||
"katex-maroonA": "#ecabc1",
|
||||
"katex-maroonB": "#ec92ab",
|
||||
"katex-maroonC": "#c55f73",
|
||||
"katex-maroonD": "#a24d61",
|
||||
"katex-maroonE": "#94424f",
|
||||
"katex-purpleA": "#caa3e8",
|
||||
"katex-purpleB": "#b189c6",
|
||||
"katex-purpleC": "#9a72ac",
|
||||
"katex-purpleD": "#715582",
|
||||
"katex-purpleE": "#644172",
|
||||
"katex-blueA": "#ccfaff",
|
||||
"katex-blueB": "#80f6ff",
|
||||
"katex-blueC": "#63d9ea",
|
||||
"katex-blueD": "#11accd",
|
||||
"katex-blueE": "#0c7f99",
|
||||
"katex-tealA": "#94fff5",
|
||||
"katex-tealB": "#26edd5",
|
||||
"katex-tealC": "#01d1c1",
|
||||
"katex-tealD": "#01a995",
|
||||
"katex-tealE": "#208170",
|
||||
"katex-greenA": "#b6ffb0",
|
||||
"katex-greenB": "#8af281",
|
||||
"katex-greenC": "#74cf70",
|
||||
"katex-greenD": "#1fab54",
|
||||
"katex-greenE": "#0d923f",
|
||||
"katex-goldA": "#ffd0a9",
|
||||
"katex-goldB": "#ffbb71",
|
||||
"katex-goldC": "#ff9c39",
|
||||
"katex-goldD": "#e07d10",
|
||||
"katex-goldE": "#a75a05",
|
||||
"katex-redA": "#fca9a9",
|
||||
"katex-redB": "#ff8482",
|
||||
"katex-redC": "#f9685d",
|
||||
"katex-redD": "#e84d39",
|
||||
"katex-redE": "#bc2612",
|
||||
"katex-maroonA": "#ffbde0",
|
||||
"katex-maroonB": "#ff92c6",
|
||||
"katex-maroonC": "#ed5fa6",
|
||||
"katex-maroonD": "#ca337c",
|
||||
"katex-maroonE": "#9e034e",
|
||||
"katex-purpleA": "#ddd7ff",
|
||||
"katex-purpleB": "#c6b9fc",
|
||||
"katex-purpleC": "#aa87ff",
|
||||
"katex-purpleD": "#7854ab",
|
||||
"katex-purpleE": "#543b78",
|
||||
"katex-mintA": "#f5f9e8",
|
||||
"katex-mintB": "#edf2df",
|
||||
"katex-mintC": "#e0e5cc",
|
||||
"katex-grayA": "#fdfdfd",
|
||||
"katex-grayB": "#f7f7f7",
|
||||
"katex-grayC": "#eeeeee",
|
||||
"katex-grayD": "#dddddd",
|
||||
"katex-grayE": "#cccccc",
|
||||
"katex-grayF": "#aaaaaa",
|
||||
"katex-grayG": "#999999",
|
||||
"katex-grayH": "#555555",
|
||||
"katex-grayI": "#333333",
|
||||
"katex-grayA": "#f6f7f7",
|
||||
"katex-grayB": "#f0f1f2",
|
||||
"katex-grayC": "#e3e5e6",
|
||||
"katex-grayD": "#d6d8da",
|
||||
"katex-grayE": "#babec2",
|
||||
"katex-grayF": "#888d93",
|
||||
"katex-grayG": "#626569",
|
||||
"katex-grayH": "#3b3e40",
|
||||
"katex-grayI": "#21242c",
|
||||
"katex-kaBlue": "#314453",
|
||||
"katex-kaGreen": "#639b24"
|
||||
"katex-kaGreen": "#71B307",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,35 +2,59 @@
|
|||
* 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
|
||||
* 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, lexer, position) {
|
||||
var error = "KaTeX parse error: " + message;
|
||||
function ParseError(message, token) {
|
||||
let error = "KaTeX parse error: " + message;
|
||||
let start;
|
||||
let end;
|
||||
|
||||
if (lexer !== undefined && position !== undefined) {
|
||||
if (token && token.lexer && token.start <= token.end) {
|
||||
// If we have the input and a position, make the error a bit fancier
|
||||
|
||||
// Prepend some information
|
||||
error += " at position " + position + ": ";
|
||||
|
||||
// Get the input
|
||||
var input = lexer._input;
|
||||
// Insert a combining underscore at the correct position
|
||||
input = input.slice(0, position) + "\u0332" +
|
||||
input.slice(position);
|
||||
const input = token.lexer.input;
|
||||
|
||||
// Prepend some information
|
||||
start = token.start;
|
||||
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
|
||||
const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332");
|
||||
|
||||
// Extract some context from the input and add it to the error
|
||||
var begin = Math.max(0, position - 15);
|
||||
var end = position + 15;
|
||||
error += input.slice(begin, end);
|
||||
let left;
|
||||
if (start > 15) {
|
||||
left = "…" + input.slice(start - 15, start);
|
||||
} 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
|
||||
// See http://stackoverflow.com/a/8460753
|
||||
var self = new Error(error);
|
||||
const self = new Error(error);
|
||||
self.name = "ParseError";
|
||||
self.__proto__ = ParseError.prototype;
|
||||
|
||||
self.position = position;
|
||||
self.position = start;
|
||||
return self;
|
||||
}
|
||||
|
||||
|
|
860
src/Parser.js
860
src/Parser.js
File diff suppressed because it is too large
Load Diff
|
@ -3,24 +3,25 @@
|
|||
* default settings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function for getting a default value if the value is undefined
|
||||
*/
|
||||
function get(option, defaultValue) {
|
||||
return option === undefined ? defaultValue : option;
|
||||
}
|
||||
const utils = require("./utils");
|
||||
|
||||
/**
|
||||
* The main Settings object
|
||||
*
|
||||
* The current options stored are:
|
||||
* - displayMode: Whether the expression should be typeset by default in
|
||||
* textstyle or displaystyle (default false)
|
||||
* - displayMode: Whether the expression should be typeset as inline math
|
||||
* (false, the default), meaning that the math starts in
|
||||
* \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) {
|
||||
// allow null options
|
||||
options = options || {};
|
||||
this.displayMode = get(options.displayMode, false);
|
||||
this.displayMode = utils.deflt(options.displayMode, false);
|
||||
this.throwOnError = utils.deflt(options.throwOnError, true);
|
||||
this.errorColor = utils.deflt(options.errorColor, "#cc0000");
|
||||
this.macros = options.macros || {};
|
||||
}
|
||||
|
||||
module.exports = Settings;
|
||||
|
|
62
src/Style.js
62
src/Style.js
|
@ -6,6 +6,20 @@
|
|||
* 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 same for cramped and uncramped version of a style), a cramped flag, and a
|
||||
|
@ -17,6 +31,7 @@ function Style(id, size, multiplier, cramped) {
|
|||
this.size = size;
|
||||
this.cramped = cramped;
|
||||
this.sizeMultiplier = multiplier;
|
||||
this.metrics = metrics[size > 0 ? size - 1 : 0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,34 +86,41 @@ Style.prototype.reset = function() {
|
|||
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
|
||||
var D = 0;
|
||||
var Dc = 1;
|
||||
var T = 2;
|
||||
var Tc = 3;
|
||||
var S = 4;
|
||||
var Sc = 5;
|
||||
var SS = 6;
|
||||
var SSc = 7;
|
||||
const D = 0;
|
||||
const Dc = 1;
|
||||
const T = 2;
|
||||
const Tc = 3;
|
||||
const S = 4;
|
||||
const Sc = 5;
|
||||
const SS = 6;
|
||||
const SSc = 7;
|
||||
|
||||
// String names for the different sizes
|
||||
var sizeNames = [
|
||||
const sizeNames = [
|
||||
"displaystyle textstyle",
|
||||
"textstyle",
|
||||
"scriptstyle",
|
||||
"scriptscriptstyle"
|
||||
"scriptscriptstyle",
|
||||
];
|
||||
|
||||
// Reset names for the different sizes
|
||||
var resetNames = [
|
||||
const resetNames = [
|
||||
"reset-textstyle",
|
||||
"reset-textstyle",
|
||||
"reset-scriptstyle",
|
||||
"reset-scriptscriptstyle"
|
||||
"reset-scriptscriptstyle",
|
||||
];
|
||||
|
||||
// Instances of the different styles
|
||||
var styles = [
|
||||
const styles = [
|
||||
new Style(D, 0, 1.0, false),
|
||||
new Style(Dc, 0, 1.0, true),
|
||||
new Style(T, 1, 1.0, false),
|
||||
|
@ -106,15 +128,15 @@ var styles = [
|
|||
new Style(S, 2, 0.7, false),
|
||||
new Style(Sc, 2, 0.7, true),
|
||||
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
|
||||
var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
|
||||
var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
|
||||
var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
|
||||
var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
|
||||
var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
|
||||
const sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
|
||||
const sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
|
||||
const fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
|
||||
const fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
|
||||
const 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
|
||||
// no more styles can be generated.
|
||||
|
@ -122,5 +144,5 @@ module.exports = {
|
|||
DISPLAY: styles[D],
|
||||
TEXT: styles[T],
|
||||
SCRIPT: styles[S],
|
||||
SCRIPTSCRIPT: styles[SS]
|
||||
SCRIPTSCRIPT: styles[SS],
|
||||
};
|
||||
|
|
|
@ -1,64 +1,169 @@
|
|||
/* eslint no-console:0 */
|
||||
/**
|
||||
* This module contains general functions that can be used for building
|
||||
* different kinds of domTree nodes in a consistent manner.
|
||||
*/
|
||||
|
||||
var domTree = require("./domTree");
|
||||
var fontMetrics = require("./fontMetrics");
|
||||
var symbols = require("./symbols");
|
||||
const domTree = require("./domTree");
|
||||
const fontMetrics = require("./fontMetrics");
|
||||
const 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.
|
||||
* Correctly pulls out metrics for the character, and optionally takes a list of
|
||||
* 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`.
|
||||
*/
|
||||
var makeSymbol = function(value, style, mode, color, classes) {
|
||||
// Replace the value with its replaced value from symbol.js
|
||||
if (symbols[mode][value] && symbols[mode][value].replace) {
|
||||
value = symbols[mode][value].replace;
|
||||
}
|
||||
const makeSymbol = function(value, fontFamily, mode, options, classes) {
|
||||
const lookup = lookupSymbol(value, fontFamily, mode);
|
||||
const metrics = lookup.metrics;
|
||||
value = lookup.value;
|
||||
|
||||
var metrics = fontMetrics.getCharacterMetrics(value, style);
|
||||
|
||||
var symbolNode;
|
||||
let symbolNode;
|
||||
if (metrics) {
|
||||
let italic = metrics.italic;
|
||||
if (mode === "text") {
|
||||
italic = 0;
|
||||
}
|
||||
symbolNode = new domTree.symbolNode(
|
||||
value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
|
||||
value, metrics.height, metrics.depth, italic, metrics.skew,
|
||||
classes);
|
||||
} else {
|
||||
// TODO(emily): Figure out a good way to only print this in development
|
||||
typeof console !== "undefined" && console.warn(
|
||||
"No character metrics for '" + value + "' in style '" +
|
||||
style + "'");
|
||||
fontFamily + "'");
|
||||
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
|
||||
}
|
||||
|
||||
if (color) {
|
||||
symbolNode.style.color = color;
|
||||
if (options) {
|
||||
if (options.style.isTight()) {
|
||||
symbolNode.classes.push("mtight");
|
||||
}
|
||||
if (options.getColor()) {
|
||||
symbolNode.style.color = options.getColor();
|
||||
}
|
||||
}
|
||||
|
||||
return symbolNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a symbol in the italic math font.
|
||||
* Makes a symbol in Main-Regular or AMS-Regular.
|
||||
* Used for rel, bin, open, close, inner, and punct.
|
||||
*/
|
||||
var mathit = function(value, mode, color, classes) {
|
||||
return makeSymbol(
|
||||
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
|
||||
const mathsym = function(value, mode, options, classes) {
|
||||
// Decide what font to render the symbol in by its entry in the symbols
|
||||
// table.
|
||||
// Have a special case for when the value = \ because the \ is used as a
|
||||
// textord in unsupported command errors but cannot be parsed as a regular
|
||||
// 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 {
|
||||
return makeSymbol(
|
||||
value, "AMS-Regular", mode, options, classes.concat(["amsrm"]));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a symbol in the upright roman font.
|
||||
* Makes a symbol in the default font for mathords and textords.
|
||||
*/
|
||||
var mathrm = function(value, mode, color, classes) {
|
||||
// Decide what font to render the symbol in by its entry in the symbols
|
||||
// table.
|
||||
if (symbols[mode][value].font === "main") {
|
||||
return makeSymbol(value, "Main-Regular", mode, color, classes);
|
||||
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 {
|
||||
return makeSymbol(
|
||||
value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -66,13 +171,13 @@ var mathrm = function(value, mode, color, classes) {
|
|||
* Calculate the height, depth, and maxFontSize of an element based on its
|
||||
* children.
|
||||
*/
|
||||
var sizeElementFromChildren = function(elem) {
|
||||
var height = 0;
|
||||
var depth = 0;
|
||||
var maxFontSize = 0;
|
||||
const sizeElementFromChildren = function(elem) {
|
||||
let height = 0;
|
||||
let depth = 0;
|
||||
let maxFontSize = 0;
|
||||
|
||||
if (elem.children) {
|
||||
for (var i = 0; i < elem.children.length; i++) {
|
||||
for (let i = 0; i < elem.children.length; i++) {
|
||||
if (elem.children[i].height > height) {
|
||||
height = elem.children[i].height;
|
||||
}
|
||||
|
@ -91,25 +196,36 @@ var sizeElementFromChildren = function(elem) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Makes a span with the given list of classes, list of children, and color.
|
||||
* Makes a span with the given list of classes, list of children, and options.
|
||||
*
|
||||
* 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`.
|
||||
*/
|
||||
var makeSpan = function(classes, children, color) {
|
||||
var span = new domTree.span(classes, children);
|
||||
const makeSpan = function(classes, children, options) {
|
||||
const span = new domTree.span(classes, children, options);
|
||||
|
||||
sizeElementFromChildren(span);
|
||||
|
||||
if (color) {
|
||||
span.style.color = color;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
var makeFragment = function(children) {
|
||||
var fragment = new domTree.documentFragment(children);
|
||||
const makeFragment = function(children) {
|
||||
const fragment = new domTree.documentFragment(children);
|
||||
|
||||
sizeElementFromChildren(fragment);
|
||||
|
||||
|
@ -121,11 +237,12 @@ var makeFragment = function(children) {
|
|||
* element has the same max font size. To do this, we create a zero-width space
|
||||
* with the correct font size.
|
||||
*/
|
||||
var makeFontSizer = function(options, fontSize) {
|
||||
var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
|
||||
fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
|
||||
const makeFontSizer = function(options, fontSize) {
|
||||
const fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
|
||||
fontSizeInner.style.fontSize =
|
||||
(fontSize / options.style.sizeMultiplier) + "em";
|
||||
|
||||
var fontSizer = makeSpan(
|
||||
const fontSizer = makeSpan(
|
||||
["fontsize-ensurer", "reset-" + options.size, "size5"],
|
||||
[fontSizeInner]);
|
||||
|
||||
|
@ -171,12 +288,12 @@ var makeFontSizer = function(options, fontSize) {
|
|||
* - options: An Options object
|
||||
*
|
||||
*/
|
||||
var makeVList = function(children, positionType, positionData, options) {
|
||||
var depth;
|
||||
var currPos;
|
||||
var i;
|
||||
const makeVList = function(children, positionType, positionData, options) {
|
||||
let depth;
|
||||
let currPos;
|
||||
let i;
|
||||
if (positionType === "individualShift") {
|
||||
var oldChildren = children;
|
||||
const oldChildren = children;
|
||||
children = [oldChildren[0]];
|
||||
|
||||
// Add in kerns to the list of children to get each element to be
|
||||
|
@ -184,9 +301,9 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
|
||||
currPos = depth;
|
||||
for (i = 1; i < oldChildren.length; i++) {
|
||||
var diff = -oldChildren[i].shift - currPos -
|
||||
const diff = -oldChildren[i].shift - currPos -
|
||||
oldChildren[i].elem.depth;
|
||||
var size = diff -
|
||||
const size = diff -
|
||||
(oldChildren[i - 1].elem.height +
|
||||
oldChildren[i - 1].elem.depth);
|
||||
|
||||
|
@ -198,7 +315,7 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
} else if (positionType === "top") {
|
||||
// We always start at the bottom, so calculate the bottom by adding up
|
||||
// all the sizes
|
||||
var bottom = positionData;
|
||||
let bottom = positionData;
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].type === "kern") {
|
||||
bottom -= children[i].size;
|
||||
|
@ -218,27 +335,27 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
}
|
||||
|
||||
// Make the fontSizer
|
||||
var maxFontSize = 0;
|
||||
let maxFontSize = 0;
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].type === "elem") {
|
||||
maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
|
||||
}
|
||||
}
|
||||
var fontSizer = makeFontSizer(options, maxFontSize);
|
||||
const fontSizer = makeFontSizer(options, maxFontSize);
|
||||
|
||||
// Create a new list of actual children at the correct offsets
|
||||
var realChildren = [];
|
||||
const realChildren = [];
|
||||
currPos = depth;
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].type === "kern") {
|
||||
currPos += children[i].size;
|
||||
} else {
|
||||
var child = children[i].elem;
|
||||
const child = children[i].elem;
|
||||
|
||||
var shift = -child.depth - currPos;
|
||||
const shift = -child.depth - currPos;
|
||||
currPos += child.height + child.depth;
|
||||
|
||||
var childWrap = makeSpan([], [fontSizer, child]);
|
||||
const childWrap = makeSpan([], [fontSizer, child]);
|
||||
childWrap.height -= shift;
|
||||
childWrap.depth += shift;
|
||||
childWrap.style.top = shift + "em";
|
||||
|
@ -249,11 +366,11 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
|
||||
// Add in an element at the end with no offset to fix the calculation of
|
||||
// baselines in some browsers (namely IE, sometimes safari)
|
||||
var baselineFix = makeSpan(
|
||||
const baselineFix = makeSpan(
|
||||
["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
|
||||
realChildren.push(baselineFix);
|
||||
|
||||
var vlist = makeSpan(["vlist"], realChildren);
|
||||
const vlist = makeSpan(["vlist"], realChildren);
|
||||
// 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.
|
||||
vlist.height = Math.max(currPos, vlist.height);
|
||||
|
@ -262,7 +379,7 @@ var makeVList = function(children, positionType, positionData, options) {
|
|||
};
|
||||
|
||||
// A table of size -> font size for the different sizing functions
|
||||
var sizingMultiplier = {
|
||||
const sizingMultiplier = {
|
||||
size1: 0.5,
|
||||
size2: 0.7,
|
||||
size3: 0.8,
|
||||
|
@ -272,49 +389,103 @@ var sizingMultiplier = {
|
|||
size7: 1.44,
|
||||
size8: 1.73,
|
||||
size9: 2.07,
|
||||
size10: 2.49
|
||||
size10: 2.49,
|
||||
};
|
||||
|
||||
// A map of spacing functions to their attributes, like size and corresponding
|
||||
// CSS class
|
||||
var spacingFunctions = {
|
||||
const spacingFunctions = {
|
||||
"\\qquad": {
|
||||
size: "2em",
|
||||
className: "qquad"
|
||||
className: "qquad",
|
||||
},
|
||||
"\\quad": {
|
||||
size: "1em",
|
||||
className: "quad"
|
||||
className: "quad",
|
||||
},
|
||||
"\\enspace": {
|
||||
size: "0.5em",
|
||||
className: "enspace"
|
||||
className: "enspace",
|
||||
},
|
||||
"\\;": {
|
||||
size: "0.277778em",
|
||||
className: "thickspace"
|
||||
className: "thickspace",
|
||||
},
|
||||
"\\:": {
|
||||
size: "0.22222em",
|
||||
className: "mediumspace"
|
||||
className: "mediumspace",
|
||||
},
|
||||
"\\,": {
|
||||
size: "0.16667em",
|
||||
className: "thinspace"
|
||||
className: "thinspace",
|
||||
},
|
||||
"\\!": {
|
||||
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 = {
|
||||
fontMap: fontMap,
|
||||
makeSymbol: makeSymbol,
|
||||
mathit: mathit,
|
||||
mathrm: mathrm,
|
||||
mathsym: mathsym,
|
||||
makeSpan: makeSpan,
|
||||
makeFragment: makeFragment,
|
||||
makeVList: makeVList,
|
||||
makeOrd: makeOrd,
|
||||
prependChildren: prependChildren,
|
||||
sizingMultiplier: sizingMultiplier,
|
||||
spacingFunctions: spacingFunctions
|
||||
spacingFunctions: spacingFunctions,
|
||||
};
|
||||
|
|
2424
src/buildHTML.js
2424
src/buildHTML.js
File diff suppressed because it is too large
Load Diff
|
@ -4,18 +4,21 @@
|
|||
* parser.
|
||||
*/
|
||||
|
||||
var buildCommon = require("./buildCommon");
|
||||
var mathMLTree = require("./mathMLTree");
|
||||
var ParseError = require("./ParseError");
|
||||
var symbols = require("./symbols");
|
||||
const buildCommon = require("./buildCommon");
|
||||
const fontMetrics = require("./fontMetrics");
|
||||
const mathMLTree = require("./mathMLTree");
|
||||
const ParseError = require("./ParseError");
|
||||
const symbols = require("./symbols");
|
||||
const utils = require("./utils");
|
||||
|
||||
var makeSpan = buildCommon.makeSpan;
|
||||
const makeSpan = buildCommon.makeSpan;
|
||||
const fontMap = buildCommon.fontMap;
|
||||
|
||||
/**
|
||||
* Takes a symbol and converts it into a MathML text node after performing
|
||||
* optional replacement from symbols.js.
|
||||
*/
|
||||
var makeText = function(text, mode) {
|
||||
const makeText = function(text, mode) {
|
||||
if (symbols[mode][text] && symbols[mode][text].replace) {
|
||||
text = symbols[mode][text].replace;
|
||||
}
|
||||
|
@ -23,383 +26,493 @@ var makeText = function(text, mode) {
|
|||
return new mathMLTree.TextNode(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the math variant as a string or null if none is required.
|
||||
*/
|
||||
const getVariant = function(group, options) {
|
||||
const font = options.font;
|
||||
if (!font) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mode = group.mode;
|
||||
if (font === "mathit") {
|
||||
return "italic";
|
||||
}
|
||||
|
||||
let value = group.value;
|
||||
if (utils.contains(["\\imath", "\\jmath"], value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbols[mode][value] && symbols[mode][value].replace) {
|
||||
value = symbols[mode][value].replace;
|
||||
}
|
||||
|
||||
const fontName = fontMap[font].fontName;
|
||||
if (fontMetrics.getCharacterMetrics(value, fontName)) {
|
||||
return fontMap[options.font].variant;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Functions for handling the different types of groups found in the parse
|
||||
* tree. Each function should take a parse group and return a MathML node.
|
||||
*/
|
||||
var groupTypes = {
|
||||
mathord: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mi",
|
||||
[makeText(group.value, group.mode)]);
|
||||
const groupTypes = {};
|
||||
|
||||
return node;
|
||||
},
|
||||
groupTypes.mathord = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mi",
|
||||
[makeText(group.value, group.mode)]);
|
||||
|
||||
textord: function(group) {
|
||||
var text = makeText(group.value, group.mode);
|
||||
const variant = getVariant(group, options);
|
||||
if (variant) {
|
||||
node.setAttribute("mathvariant", variant);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
var node;
|
||||
if (/[0-9]/.test(group.value)) {
|
||||
node = new mathMLTree.MathNode("mn", [text]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mi", [text]);
|
||||
node.setAttribute("mathvariant", "normal");
|
||||
groupTypes.textord = function(group, options) {
|
||||
const text = makeText(group.value, group.mode);
|
||||
|
||||
const variant = getVariant(group, options) || "normal";
|
||||
|
||||
let node;
|
||||
if (/[0-9]/.test(group.value)) {
|
||||
// TODO(kevinb) merge adjacent <mn> nodes
|
||||
// do it as a post processing step
|
||||
node = new mathMLTree.MathNode("mn", [text]);
|
||||
if (options.font) {
|
||||
node.setAttribute("mathvariant", variant);
|
||||
}
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mi", [text]);
|
||||
node.setAttribute("mathvariant", variant);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.bin = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.rel = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.open = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.close = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.inner = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.punct = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
node.setAttribute("separator", "true");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.ordgroup = function(group, options) {
|
||||
const inner = buildExpression(group.value, options);
|
||||
|
||||
const node = new mathMLTree.MathNode("mrow", inner);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.text = function(group, options) {
|
||||
const inner = buildExpression(group.value.body, options);
|
||||
|
||||
const node = new mathMLTree.MathNode("mtext", inner);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.color = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
|
||||
const node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
node.setAttribute("mathcolor", group.value.color);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.supsub = function(group, options) {
|
||||
const children = [buildGroup(group.value.base, options)];
|
||||
|
||||
if (group.value.sub) {
|
||||
children.push(buildGroup(group.value.sub, options));
|
||||
}
|
||||
|
||||
if (group.value.sup) {
|
||||
children.push(buildGroup(group.value.sup, options));
|
||||
}
|
||||
|
||||
let nodeType;
|
||||
if (!group.value.sub) {
|
||||
nodeType = "msup";
|
||||
} else if (!group.value.sup) {
|
||||
nodeType = "msub";
|
||||
} else {
|
||||
nodeType = "msubsup";
|
||||
}
|
||||
|
||||
const node = new mathMLTree.MathNode(nodeType, children);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.genfrac = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mfrac",
|
||||
[
|
||||
buildGroup(group.value.numer, options),
|
||||
buildGroup(group.value.denom, options),
|
||||
]);
|
||||
|
||||
if (!group.value.hasBarLine) {
|
||||
node.setAttribute("linethickness", "0px");
|
||||
}
|
||||
|
||||
if (group.value.leftDelim != null || group.value.rightDelim != null) {
|
||||
const withDelims = [];
|
||||
|
||||
if (group.value.leftDelim != null) {
|
||||
const leftOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
|
||||
|
||||
leftOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(leftOp);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
withDelims.push(node);
|
||||
|
||||
bin: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
if (group.value.rightDelim != null) {
|
||||
const rightOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
rightOp.setAttribute("fence", "true");
|
||||
|
||||
rel: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
open: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
close: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
inner: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
punct: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value, group.mode)]);
|
||||
|
||||
node.setAttribute("separator", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
ordgroup: function(group) {
|
||||
var inner = buildExpression(group.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mrow", inner);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
text: function(group) {
|
||||
var inner = buildExpression(group.value.body);
|
||||
|
||||
var node = new mathMLTree.MathNode("mtext", inner);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
color: function(group) {
|
||||
var inner = buildExpression(group.value.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
node.setAttribute("mathcolor", group.value.color);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
supsub: function(group) {
|
||||
var children = [buildGroup(group.value.base)];
|
||||
|
||||
if (group.value.sub) {
|
||||
children.push(buildGroup(group.value.sub));
|
||||
withDelims.push(rightOp);
|
||||
}
|
||||
|
||||
if (group.value.sup) {
|
||||
children.push(buildGroup(group.value.sup));
|
||||
}
|
||||
|
||||
var nodeType;
|
||||
if (!group.value.sub) {
|
||||
nodeType = "msup";
|
||||
} else if (!group.value.sup) {
|
||||
nodeType = "msub";
|
||||
} else {
|
||||
nodeType = "msubsup";
|
||||
}
|
||||
|
||||
var node = new mathMLTree.MathNode(nodeType, children);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
genfrac: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mfrac",
|
||||
[buildGroup(group.value.numer),
|
||||
buildGroup(group.value.denom)]);
|
||||
|
||||
if (!group.value.hasBarLine) {
|
||||
node.setAttribute("linethickness", "0px");
|
||||
}
|
||||
|
||||
if (group.value.leftDelim != null || group.value.rightDelim != null) {
|
||||
var withDelims = [];
|
||||
|
||||
if (group.value.leftDelim != null) {
|
||||
var leftOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
|
||||
|
||||
leftOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(leftOp);
|
||||
}
|
||||
|
||||
withDelims.push(node);
|
||||
|
||||
if (group.value.rightDelim != null) {
|
||||
var rightOp = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
|
||||
|
||||
rightOp.setAttribute("fence", "true");
|
||||
|
||||
withDelims.push(rightOp);
|
||||
}
|
||||
|
||||
var outerNode = new mathMLTree.MathNode("mrow", withDelims);
|
||||
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
array: function(group) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtable", group.value.body.map(function(row) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtr", row.map(function(cell) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtd", [buildGroup(cell)]);
|
||||
}));
|
||||
}));
|
||||
},
|
||||
|
||||
sqrt: function(group) {
|
||||
var node;
|
||||
if (group.value.index) {
|
||||
node = new mathMLTree.MathNode(
|
||||
"mroot", [
|
||||
buildGroup(group.value.body),
|
||||
buildGroup(group.value.index)
|
||||
]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode(
|
||||
"msqrt", [buildGroup(group.value.body)]);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
leftright: function(group) {
|
||||
var inner = buildExpression(group.value.body);
|
||||
|
||||
if (group.value.left !== ".") {
|
||||
var leftNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.left, group.mode)]);
|
||||
|
||||
leftNode.setAttribute("fence", "true");
|
||||
|
||||
inner.unshift(leftNode);
|
||||
}
|
||||
|
||||
if (group.value.right !== ".") {
|
||||
var rightNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.right, group.mode)]);
|
||||
|
||||
rightNode.setAttribute("fence", "true");
|
||||
|
||||
inner.push(rightNode);
|
||||
}
|
||||
|
||||
var outerNode = new mathMLTree.MathNode("mrow", inner);
|
||||
const outerNode = new mathMLTree.MathNode("mrow", withDelims);
|
||||
|
||||
return outerNode;
|
||||
},
|
||||
|
||||
accent: function(group) {
|
||||
var accentNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.accent, group.mode)]);
|
||||
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.base),
|
||||
accentNode]);
|
||||
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
spacing: function(group) {
|
||||
var node;
|
||||
|
||||
if (group.value === "\\ " || group.value === "\\space" ||
|
||||
group.value === " " || group.value === "~") {
|
||||
node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("\u00a0")]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mspace");
|
||||
|
||||
node.setAttribute(
|
||||
"width", buildCommon.spacingFunctions[group.value].size);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
op: function(group) {
|
||||
var node;
|
||||
|
||||
// TODO(emily): handle big operators using the `largeop` attribute
|
||||
|
||||
if (group.value.symbol) {
|
||||
// This is a symbol. Just add the symbol.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.body, group.mode)]);
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
katex: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("KaTeX")]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
delimsizing: function(group) {
|
||||
var children = [];
|
||||
|
||||
if (group.value.value !== ".") {
|
||||
children.push(makeText(group.value.value, group.mode));
|
||||
}
|
||||
|
||||
var node = new mathMLTree.MathNode("mo", children);
|
||||
|
||||
if (group.value.delimType === "open" ||
|
||||
group.value.delimType === "close") {
|
||||
// Only some of the delimsizing functions act as fences, and they
|
||||
// return "open" or "close" delimTypes.
|
||||
node.setAttribute("fence", "true");
|
||||
} else {
|
||||
// Explicitly disable fencing if it's not a fence, to override the
|
||||
// defaults.
|
||||
node.setAttribute("fence", "false");
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
styling: function(group) {
|
||||
var inner = buildExpression(group.value.value, inner);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
var styleAttributes = {
|
||||
"display": ["0", "true"],
|
||||
"text": ["0", "false"],
|
||||
"script": ["1", "false"],
|
||||
"scriptscript": ["2", "false"]
|
||||
};
|
||||
|
||||
var attr = styleAttributes[group.value.style];
|
||||
|
||||
node.setAttribute("scriptlevel", attr[0]);
|
||||
node.setAttribute("displaystyle", attr[1]);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
sizing: function(group) {
|
||||
var inner = buildExpression(group.value.value);
|
||||
|
||||
var node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
// TODO(emily): This doesn't produce the correct size for nested size
|
||||
// changes, because we don't keep state of what style we're currently
|
||||
// in, so we can't reset the size to normal before changing it.
|
||||
node.setAttribute(
|
||||
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
overline: function(group) {
|
||||
var operator = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode("\u203e")]);
|
||||
operator.setAttribute("stretchy", "true");
|
||||
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.body),
|
||||
operator]);
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
rule: function(group) {
|
||||
// TODO(emily): Figure out if there's an actual way to draw black boxes
|
||||
// in MathML.
|
||||
var node = new mathMLTree.MathNode("mrow");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
llap: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body)]);
|
||||
|
||||
node.setAttribute("lspace", "-1width");
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
rlap: function(group) {
|
||||
var node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body)]);
|
||||
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
phantom: function(group, options, prev) {
|
||||
var inner = buildExpression(group.value.value);
|
||||
return new mathMLTree.MathNode("mphantom", inner);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.array = function(group, options) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtable", group.value.body.map(function(row) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtr", row.map(function(cell) {
|
||||
return new mathMLTree.MathNode(
|
||||
"mtd", [buildGroup(cell, options)]);
|
||||
}));
|
||||
}));
|
||||
};
|
||||
|
||||
groupTypes.sqrt = function(group, options) {
|
||||
let node;
|
||||
if (group.value.index) {
|
||||
node = new mathMLTree.MathNode(
|
||||
"mroot", [
|
||||
buildGroup(group.value.body, options),
|
||||
buildGroup(group.value.index, options),
|
||||
]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode(
|
||||
"msqrt", [buildGroup(group.value.body, options)]);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.leftright = function(group, options) {
|
||||
const inner = buildExpression(group.value.body, options);
|
||||
|
||||
if (group.value.left !== ".") {
|
||||
const leftNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.left, group.mode)]);
|
||||
|
||||
leftNode.setAttribute("fence", "true");
|
||||
|
||||
inner.unshift(leftNode);
|
||||
}
|
||||
|
||||
if (group.value.right !== ".") {
|
||||
const rightNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.right, group.mode)]);
|
||||
|
||||
rightNode.setAttribute("fence", "true");
|
||||
|
||||
inner.push(rightNode);
|
||||
}
|
||||
|
||||
const outerNode = new mathMLTree.MathNode("mrow", inner);
|
||||
|
||||
return outerNode;
|
||||
};
|
||||
|
||||
groupTypes.middle = function(group, options) {
|
||||
const middleNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.middle, group.mode)]);
|
||||
middleNode.setAttribute("fence", "true");
|
||||
return middleNode;
|
||||
};
|
||||
|
||||
groupTypes.accent = function(group, options) {
|
||||
const accentNode = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.accent, group.mode)]);
|
||||
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.base, options), accentNode]);
|
||||
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.spacing = function(group) {
|
||||
let node;
|
||||
|
||||
if (group.value === "\\ " || group.value === "\\space" ||
|
||||
group.value === " " || group.value === "~") {
|
||||
node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("\u00a0")]);
|
||||
} else {
|
||||
node = new mathMLTree.MathNode("mspace");
|
||||
|
||||
node.setAttribute(
|
||||
"width", buildCommon.spacingFunctions[group.value].size);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.op = function(group, options) {
|
||||
let node;
|
||||
|
||||
// TODO(emily): handle big operators using the `largeop` attribute
|
||||
|
||||
if (group.value.symbol) {
|
||||
// This is a symbol. Just add the symbol.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", [makeText(group.value.body, group.mode)]);
|
||||
} else if (group.value.value) {
|
||||
// This is an operator with children. Add them.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mo", buildExpression(group.value.value, options));
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.mod = function(group, options) {
|
||||
let inner = [];
|
||||
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText("(", group.mode)]));
|
||||
}
|
||||
if (group.value.modType !== "pod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText("mod", group.mode)]));
|
||||
}
|
||||
if (group.value.value) {
|
||||
const space = new mathMLTree.MathNode("mspace");
|
||||
space.setAttribute("width", "0.333333em");
|
||||
inner.push(space);
|
||||
inner = inner.concat(buildExpression(group.value.value, options));
|
||||
}
|
||||
if (group.value.modType === "pod" || group.value.modType === "pmod") {
|
||||
inner.push(new mathMLTree.MathNode(
|
||||
"mo", [makeText(")", group.mode)]));
|
||||
}
|
||||
|
||||
return new mathMLTree.MathNode("mo", inner);
|
||||
};
|
||||
|
||||
groupTypes.katex = function(group) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mtext", [new mathMLTree.TextNode("KaTeX")]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.font = function(group, options) {
|
||||
const font = group.value.font;
|
||||
return buildGroup(group.value.body, options.withFont(font));
|
||||
};
|
||||
|
||||
groupTypes.delimsizing = function(group) {
|
||||
const children = [];
|
||||
|
||||
if (group.value.value !== ".") {
|
||||
children.push(makeText(group.value.value, group.mode));
|
||||
}
|
||||
|
||||
const node = new mathMLTree.MathNode("mo", children);
|
||||
|
||||
if (group.value.mclass === "mopen" ||
|
||||
group.value.mclass === "mclose") {
|
||||
// Only some of the delimsizing functions act as fences, and they
|
||||
// return "mopen" or "mclose" mclass.
|
||||
node.setAttribute("fence", "true");
|
||||
} else {
|
||||
// Explicitly disable fencing if it's not a fence, to override the
|
||||
// defaults.
|
||||
node.setAttribute("fence", "false");
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.styling = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
|
||||
const node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
const styleAttributes = {
|
||||
"display": ["0", "true"],
|
||||
"text": ["0", "false"],
|
||||
"script": ["1", "false"],
|
||||
"scriptscript": ["2", "false"],
|
||||
};
|
||||
|
||||
const attr = styleAttributes[group.value.style];
|
||||
|
||||
node.setAttribute("scriptlevel", attr[0]);
|
||||
node.setAttribute("displaystyle", attr[1]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.sizing = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
|
||||
const node = new mathMLTree.MathNode("mstyle", inner);
|
||||
|
||||
// TODO(emily): This doesn't produce the correct size for nested size
|
||||
// changes, because we don't keep state of what style we're currently
|
||||
// in, so we can't reset the size to normal before changing it. Now
|
||||
// that we're passing an options parameter we should be able to fix
|
||||
// this.
|
||||
node.setAttribute(
|
||||
"mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.overline = function(group, options) {
|
||||
const operator = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode("\u203e")]);
|
||||
operator.setAttribute("stretchy", "true");
|
||||
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mover",
|
||||
[buildGroup(group.value.body, options), operator]);
|
||||
node.setAttribute("accent", "true");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.underline = function(group, options) {
|
||||
const operator = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode("\u203e")]);
|
||||
operator.setAttribute("stretchy", "true");
|
||||
|
||||
const node = new mathMLTree.MathNode(
|
||||
"munder",
|
||||
[buildGroup(group.value.body, options), operator]);
|
||||
node.setAttribute("accentunder", "true");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.rule = function(group) {
|
||||
// TODO(emily): Figure out if there's an actual way to draw black boxes
|
||||
// in MathML.
|
||||
const node = new mathMLTree.MathNode("mrow");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.kern = function(group) {
|
||||
// TODO(kevin): Figure out if there's a way to add space in MathML
|
||||
const node = new mathMLTree.MathNode("mrow");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.llap = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body, options)]);
|
||||
|
||||
node.setAttribute("lspace", "-1width");
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.rlap = function(group, options) {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mpadded", [buildGroup(group.value.body, options)]);
|
||||
|
||||
node.setAttribute("width", "0px");
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
groupTypes.phantom = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
return new mathMLTree.MathNode("mphantom", inner);
|
||||
};
|
||||
|
||||
groupTypes.mclass = function(group, options) {
|
||||
const inner = buildExpression(group.value.value, options);
|
||||
return new mathMLTree.MathNode("mstyle", inner);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -407,11 +520,11 @@ var groupTypes = {
|
|||
* MathML nodes. A little simpler than the HTML version because we don't do any
|
||||
* previous-node handling.
|
||||
*/
|
||||
var buildExpression = function(expression) {
|
||||
var groups = [];
|
||||
for (var i = 0; i < expression.length; i++) {
|
||||
var group = expression[i];
|
||||
groups.push(buildGroup(group));
|
||||
const buildExpression = function(expression, options) {
|
||||
const groups = [];
|
||||
for (let i = 0; i < expression.length; i++) {
|
||||
const group = expression[i];
|
||||
groups.push(buildGroup(group, options));
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
@ -420,14 +533,14 @@ var buildExpression = function(expression) {
|
|||
* Takes a group from the parser and calls the appropriate groupTypes function
|
||||
* on it to produce a MathML node.
|
||||
*/
|
||||
var buildGroup = function(group) {
|
||||
const buildGroup = function(group, options) {
|
||||
if (!group) {
|
||||
return new mathMLTree.MathNode("mrow");
|
||||
}
|
||||
|
||||
if (groupTypes[group.type]) {
|
||||
// Call the groupTypes function
|
||||
return groupTypes[group.type](group);
|
||||
return groupTypes[group.type](group, options);
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Got group of unknown type: '" + group.type + "'");
|
||||
|
@ -442,23 +555,23 @@ var buildGroup = function(group) {
|
|||
* Note that we actually return a domTree element with a `<math>` inside it so
|
||||
* we can do appropriate styling.
|
||||
*/
|
||||
var buildMathML = function(tree, texExpression, settings) {
|
||||
var expression = buildExpression(tree);
|
||||
const buildMathML = function(tree, texExpression, options) {
|
||||
const expression = buildExpression(tree, options);
|
||||
|
||||
// Wrap up the expression in an mrow so it is presented in the semantics
|
||||
// tag correctly.
|
||||
var wrapper = new mathMLTree.MathNode("mrow", expression);
|
||||
const wrapper = new mathMLTree.MathNode("mrow", expression);
|
||||
|
||||
// Build a TeX annotation of the source
|
||||
var annotation = new mathMLTree.MathNode(
|
||||
const annotation = new mathMLTree.MathNode(
|
||||
"annotation", [new mathMLTree.TextNode(texExpression)]);
|
||||
|
||||
annotation.setAttribute("encoding", "application/x-tex");
|
||||
|
||||
var semantics = new mathMLTree.MathNode(
|
||||
const semantics = new mathMLTree.MathNode(
|
||||
"semantics", [wrapper, annotation]);
|
||||
|
||||
var math = new mathMLTree.MathNode("math", [semantics]);
|
||||
const math = new mathMLTree.MathNode("math", [semantics]);
|
||||
|
||||
// You can't style <math> nodes, so we wrap the node in a span.
|
||||
return makeSpan(["katex-mathml"], [math]);
|
||||
|
|
|
@ -1,18 +1,33 @@
|
|||
const buildHTML = require("./buildHTML");
|
||||
const buildMathML = require("./buildMathML");
|
||||
const buildCommon = require("./buildCommon");
|
||||
const Options = require("./Options");
|
||||
const Settings = require("./Settings");
|
||||
const Style = require("./Style");
|
||||
|
||||
var buildHTML = require("./buildHTML");
|
||||
var buildMathML = require("./buildMathML");
|
||||
var buildCommon = require("./buildCommon");
|
||||
const makeSpan = buildCommon.makeSpan;
|
||||
|
||||
var makeSpan = buildCommon.makeSpan;
|
||||
const buildTree = function(tree, expression, settings) {
|
||||
settings = settings || new Settings({});
|
||||
|
||||
let startStyle = Style.TEXT;
|
||||
if (settings.displayMode) {
|
||||
startStyle = Style.DISPLAY;
|
||||
}
|
||||
|
||||
// Setup the default options
|
||||
const options = new Options({
|
||||
style: startStyle,
|
||||
size: "size5",
|
||||
});
|
||||
|
||||
var buildTree = function(tree, expression, settings) {
|
||||
// `buildHTML` sometimes messes with the parse tree (like turning bins ->
|
||||
// ords), so we build the MathML version first.
|
||||
var mathMLNode = buildMathML(tree, expression, settings);
|
||||
var htmlNode = buildHTML(tree, settings);
|
||||
const mathMLNode = buildMathML(tree, expression, options);
|
||||
const htmlNode = buildHTML(tree, options);
|
||||
|
||||
var katexNode = makeSpan(["katex"], [
|
||||
mathMLNode, htmlNode
|
||||
const katexNode = makeSpan(["katex"], [
|
||||
mathMLNode, htmlNode,
|
||||
]);
|
||||
|
||||
if (settings.displayMode) {
|
||||
|
|
256
src/delimiter.js
256
src/delimiter.js
|
@ -20,21 +20,21 @@
|
|||
* used in `\left` and `\right`.
|
||||
*/
|
||||
|
||||
var ParseError = require("./ParseError");
|
||||
var Style = require("./Style");
|
||||
const ParseError = require("./ParseError");
|
||||
const Style = require("./Style");
|
||||
|
||||
var buildCommon = require("./buildCommon");
|
||||
var fontMetrics = require("./fontMetrics");
|
||||
var symbols = require("./symbols");
|
||||
var utils = require("./utils");
|
||||
const buildCommon = require("./buildCommon");
|
||||
const fontMetrics = require("./fontMetrics");
|
||||
const symbols = require("./symbols");
|
||||
const utils = require("./utils");
|
||||
|
||||
var makeSpan = buildCommon.makeSpan;
|
||||
const makeSpan = buildCommon.makeSpan;
|
||||
|
||||
/**
|
||||
* Get the metrics for a given symbol and font, after transformation (i.e.
|
||||
* after following replacement from symbols.js)
|
||||
*/
|
||||
var getMetrics = function(symbol, font) {
|
||||
const getMetrics = function(symbol, font) {
|
||||
if (symbols.math[symbol] && symbols.math[symbol].replace) {
|
||||
return fontMetrics.getCharacterMetrics(
|
||||
symbols.math[symbol].replace, font);
|
||||
|
@ -47,19 +47,22 @@ var getMetrics = function(symbol, font) {
|
|||
/**
|
||||
* Builds a symbol in the given font size (note size is an integer)
|
||||
*/
|
||||
var mathrmSize = function(value, size, mode) {
|
||||
return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
|
||||
const mathrmSize = function(value, size, mode, options) {
|
||||
return buildCommon.makeSymbol(value, "Size" + size + "-Regular",
|
||||
mode, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Puts a delimiter span in a given style, and adds appropriate height, depth,
|
||||
* and maxFontSizes.
|
||||
*/
|
||||
var styleWrap = function(delim, toStyle, options) {
|
||||
var span = makeSpan(
|
||||
["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
|
||||
const styleWrap = function(delim, toStyle, options, classes) {
|
||||
classes = classes || [];
|
||||
const span = makeSpan(
|
||||
classes.concat(["style-wrap", options.style.reset(), toStyle.cls()]),
|
||||
[delim], options);
|
||||
|
||||
var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
|
||||
const multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
|
||||
|
||||
span.height *= multiplier;
|
||||
span.depth *= multiplier;
|
||||
|
@ -73,15 +76,15 @@ var styleWrap = function(delim, toStyle, options) {
|
|||
* font, but is restyled to either be in textstyle, scriptstyle, or
|
||||
* scriptscriptstyle.
|
||||
*/
|
||||
var makeSmallDelim = function(delim, style, center, options, mode) {
|
||||
var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
|
||||
const makeSmallDelim = function(delim, style, center, options, mode, classes) {
|
||||
const text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options);
|
||||
|
||||
var span = styleWrap(text, style, options);
|
||||
const span = styleWrap(text, style, options, classes);
|
||||
|
||||
if (center) {
|
||||
var shift =
|
||||
const shift =
|
||||
(1 - options.style.sizeMultiplier / style.sizeMultiplier) *
|
||||
fontMetrics.metrics.axisHeight;
|
||||
options.style.metrics.axisHeight;
|
||||
|
||||
span.style.top = shift + "em";
|
||||
span.height -= shift;
|
||||
|
@ -95,17 +98,16 @@ var makeSmallDelim = function(delim, style, center, options, mode) {
|
|||
* Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
|
||||
* Size3, or Size4 fonts. It is always rendered in textstyle.
|
||||
*/
|
||||
var makeLargeDelim = function(delim, size, center, options, mode) {
|
||||
var inner = mathrmSize(delim, size, mode);
|
||||
const makeLargeDelim = function(delim, size, center, options, mode, classes) {
|
||||
const inner = mathrmSize(delim, size, mode, options);
|
||||
|
||||
var span = styleWrap(
|
||||
makeSpan(["delimsizing", "size" + size],
|
||||
[inner], options.getColor()),
|
||||
Style.TEXT, options);
|
||||
const span = styleWrap(
|
||||
makeSpan(["delimsizing", "size" + size], [inner], options),
|
||||
Style.TEXT, options, classes);
|
||||
|
||||
if (center) {
|
||||
var shift = (1 - options.style.sizeMultiplier) *
|
||||
fontMetrics.metrics.axisHeight;
|
||||
const shift = (1 - options.style.sizeMultiplier) *
|
||||
options.style.metrics.axisHeight;
|
||||
|
||||
span.style.top = shift + "em";
|
||||
span.height -= shift;
|
||||
|
@ -119,8 +121,8 @@ var makeLargeDelim = function(delim, size, center, options, mode) {
|
|||
* Make an inner span with the given offset and in the given font. This is used
|
||||
* in `makeStackedDelim` to make the stacking pieces for the delimiter.
|
||||
*/
|
||||
var makeInner = function(symbol, font, mode) {
|
||||
var sizeClass;
|
||||
const makeInner = function(symbol, font, mode) {
|
||||
let sizeClass;
|
||||
// Apply the correct CSS class to choose the right font.
|
||||
if (font === "Size1-Regular") {
|
||||
sizeClass = "delim-size1";
|
||||
|
@ -128,7 +130,7 @@ var makeInner = function(symbol, font, mode) {
|
|||
sizeClass = "delim-size4";
|
||||
}
|
||||
|
||||
var inner = makeSpan(
|
||||
const inner = makeSpan(
|
||||
["delimsizinginner", sizeClass],
|
||||
[makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
|
||||
|
||||
|
@ -141,14 +143,18 @@ var makeInner = function(symbol, font, mode) {
|
|||
* Make a stacked delimiter out of a given delimiter, with the total height at
|
||||
* least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
|
||||
*/
|
||||
var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
|
||||
const makeStackedDelim = function(delim, heightTotal, center, options, mode,
|
||||
classes) {
|
||||
// There are four parts, the top, an optional middle, a repeated part, and a
|
||||
// bottom.
|
||||
var top, middle, repeat, bottom;
|
||||
let top;
|
||||
let middle;
|
||||
let repeat;
|
||||
let bottom;
|
||||
top = repeat = bottom = delim;
|
||||
middle = null;
|
||||
// Also keep track of what font the delimiters are in
|
||||
var font = "Size1-Regular";
|
||||
let font = "Size1-Regular";
|
||||
|
||||
// We set the parts and font based on the symbol. Note that we use
|
||||
// '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
|
||||
|
@ -217,6 +223,26 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
|
|||
bottom = "\u23ad";
|
||||
repeat = "\u23aa";
|
||||
font = "Size4-Regular";
|
||||
} else if (delim === "\\lgroup") {
|
||||
top = "\u23a7";
|
||||
bottom = "\u23a9";
|
||||
repeat = "\u23aa";
|
||||
font = "Size4-Regular";
|
||||
} else if (delim === "\\rgroup") {
|
||||
top = "\u23ab";
|
||||
bottom = "\u23ad";
|
||||
repeat = "\u23aa";
|
||||
font = "Size4-Regular";
|
||||
} else if (delim === "\\lmoustache") {
|
||||
top = "\u23a7";
|
||||
bottom = "\u23ad";
|
||||
repeat = "\u23aa";
|
||||
font = "Size4-Regular";
|
||||
} else if (delim === "\\rmoustache") {
|
||||
top = "\u23ab";
|
||||
bottom = "\u23a9";
|
||||
repeat = "\u23aa";
|
||||
font = "Size4-Regular";
|
||||
} else if (delim === "\\surd") {
|
||||
top = "\ue001";
|
||||
bottom = "\u23b7";
|
||||
|
@ -225,65 +251,64 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
|
|||
}
|
||||
|
||||
// Get the metrics of the four sections
|
||||
var topMetrics = getMetrics(top, font);
|
||||
var topHeightTotal = topMetrics.height + topMetrics.depth;
|
||||
var repeatMetrics = getMetrics(repeat, font);
|
||||
var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
|
||||
var bottomMetrics = getMetrics(bottom, font);
|
||||
var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
|
||||
var middleHeightTotal = 0;
|
||||
var middleFactor = 1;
|
||||
const topMetrics = getMetrics(top, font);
|
||||
const topHeightTotal = topMetrics.height + topMetrics.depth;
|
||||
const repeatMetrics = getMetrics(repeat, font);
|
||||
const repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
|
||||
const bottomMetrics = getMetrics(bottom, font);
|
||||
const bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
|
||||
let middleHeightTotal = 0;
|
||||
let middleFactor = 1;
|
||||
if (middle !== null) {
|
||||
var middleMetrics = getMetrics(middle, font);
|
||||
const middleMetrics = getMetrics(middle, font);
|
||||
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
|
||||
middleFactor = 2; // repeat symmetrically above and below middle
|
||||
}
|
||||
|
||||
// Calcuate the minimal height that the delimiter can have.
|
||||
// It is at least the size of the top, bottom, and optional middle combined.
|
||||
var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
|
||||
const minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
|
||||
|
||||
// Compute the number of copies of the repeat symbol we will need
|
||||
var repeatCount = Math.ceil(
|
||||
const repeatCount = Math.ceil(
|
||||
(heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
|
||||
|
||||
// Compute the total height of the delimiter including all the symbols
|
||||
var realHeightTotal =
|
||||
const realHeightTotal =
|
||||
minHeight + repeatCount * middleFactor * repeatHeightTotal;
|
||||
|
||||
// The center of the delimiter is placed at the center of the axis. Note
|
||||
// that in this context, "center" means that the delimiter should be
|
||||
// centered around the axis in the current style, while normally it is
|
||||
// centered around the axis in textstyle.
|
||||
var axisHeight = fontMetrics.metrics.axisHeight;
|
||||
let axisHeight = options.style.metrics.axisHeight;
|
||||
if (center) {
|
||||
axisHeight *= options.style.sizeMultiplier;
|
||||
}
|
||||
// Calculate the depth
|
||||
var depth = realHeightTotal / 2 - axisHeight;
|
||||
const depth = realHeightTotal / 2 - axisHeight;
|
||||
|
||||
// Now, we start building the pieces that will go into the vlist
|
||||
|
||||
// Keep a list of the inner pieces
|
||||
var inners = [];
|
||||
const inners = [];
|
||||
|
||||
// Add the bottom symbol
|
||||
inners.push(makeInner(bottom, font, mode));
|
||||
|
||||
var i;
|
||||
if (middle === null) {
|
||||
// Add that many symbols
|
||||
for (i = 0; i < repeatCount; i++) {
|
||||
for (let i = 0; i < repeatCount; i++) {
|
||||
inners.push(makeInner(repeat, font, mode));
|
||||
}
|
||||
} else {
|
||||
// When there is a middle bit, we need the middle part and two repeated
|
||||
// sections
|
||||
for (i = 0; i < repeatCount; i++) {
|
||||
for (let i = 0; i < repeatCount; i++) {
|
||||
inners.push(makeInner(repeat, font, mode));
|
||||
}
|
||||
inners.push(makeInner(middle, font, mode));
|
||||
for (i = 0; i < repeatCount; i++) {
|
||||
for (let i = 0; i < repeatCount; i++) {
|
||||
inners.push(makeInner(repeat, font, mode));
|
||||
}
|
||||
}
|
||||
|
@ -292,57 +317,59 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
|
|||
inners.push(makeInner(top, font, mode));
|
||||
|
||||
// Finally, build the vlist
|
||||
var inner = buildCommon.makeVList(inners, "bottom", depth, options);
|
||||
const inner = buildCommon.makeVList(inners, "bottom", depth, options);
|
||||
|
||||
return styleWrap(
|
||||
makeSpan(["delimsizing", "mult"], [inner], options.getColor()),
|
||||
Style.TEXT, options);
|
||||
makeSpan(["delimsizing", "mult"], [inner], options),
|
||||
Style.TEXT, options, classes);
|
||||
};
|
||||
|
||||
// There are three kinds of delimiters, delimiters that stack when they become
|
||||
// too large
|
||||
var stackLargeDelimiters = [
|
||||
const stackLargeDelimiters = [
|
||||
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
|
||||
"\\{", "\\lbrace", "\\}", "\\rbrace",
|
||||
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
|
||||
"\\surd"
|
||||
"\\surd",
|
||||
];
|
||||
|
||||
// delimiters that always stack
|
||||
var stackAlwaysDelimiters = [
|
||||
const stackAlwaysDelimiters = [
|
||||
"\\uparrow", "\\downarrow", "\\updownarrow",
|
||||
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
|
||||
"|", "\\|", "\\vert", "\\Vert"
|
||||
"|", "\\|", "\\vert", "\\Vert",
|
||||
"\\lvert", "\\rvert", "\\lVert", "\\rVert",
|
||||
"\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
|
||||
];
|
||||
|
||||
// and delimiters that never stack
|
||||
var stackNeverDelimiters = [
|
||||
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
|
||||
const stackNeverDelimiters = [
|
||||
"<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt",
|
||||
];
|
||||
|
||||
// Metrics of the different sizes. Found by looking at TeX's output of
|
||||
// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
|
||||
// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
|
||||
var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
|
||||
const sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
|
||||
|
||||
/**
|
||||
* Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
|
||||
*/
|
||||
var makeSizedDelim = function(delim, size, options, mode) {
|
||||
const makeSizedDelim = function(delim, size, options, mode, classes) {
|
||||
// < and > turn into \langle and \rangle in delimiters
|
||||
if (delim === "<") {
|
||||
if (delim === "<" || delim === "\\lt") {
|
||||
delim = "\\langle";
|
||||
} else if (delim === ">") {
|
||||
} else if (delim === ">" || delim === "\\gt") {
|
||||
delim = "\\rangle";
|
||||
}
|
||||
|
||||
// Sized delimiters are never centered.
|
||||
if (utils.contains(stackLargeDelimiters, delim) ||
|
||||
utils.contains(stackNeverDelimiters, delim)) {
|
||||
return makeLargeDelim(delim, size, false, options, mode);
|
||||
return makeLargeDelim(delim, size, false, options, mode, classes);
|
||||
} else if (utils.contains(stackAlwaysDelimiters, delim)) {
|
||||
return makeStackedDelim(
|
||||
delim, sizeToMaxHeight[size], false, options, mode);
|
||||
delim, sizeToMaxHeight[size], false, options, mode, classes);
|
||||
} else {
|
||||
throw new ParseError("Illegal delimiter: '" + delim + "'");
|
||||
}
|
||||
|
@ -361,27 +388,7 @@ var makeSizedDelim = function(delim, size, options, mode) {
|
|||
*/
|
||||
|
||||
// Delimiters that never stack try small delimiters and large delimiters only
|
||||
var stackNeverDelimiterSequence = [
|
||||
{type: "small", style: Style.SCRIPTSCRIPT},
|
||||
{type: "small", style: Style.SCRIPT},
|
||||
{type: "small", style: Style.TEXT},
|
||||
{type: "large", size: 1},
|
||||
{type: "large", size: 2},
|
||||
{type: "large", size: 3},
|
||||
{type: "large", size: 4}
|
||||
];
|
||||
|
||||
// Delimiters that always stack try the small delimiters first, then stack
|
||||
var stackAlwaysDelimiterSequence = [
|
||||
{type: "small", style: Style.SCRIPTSCRIPT},
|
||||
{type: "small", style: Style.SCRIPT},
|
||||
{type: "small", style: Style.TEXT},
|
||||
{type: "stack"}
|
||||
];
|
||||
|
||||
// Delimiters that stack when large try the small and then large delimiters, and
|
||||
// stack afterwards
|
||||
var stackLargeDelimiterSequence = [
|
||||
const stackNeverDelimiterSequence = [
|
||||
{type: "small", style: Style.SCRIPTSCRIPT},
|
||||
{type: "small", style: Style.SCRIPT},
|
||||
{type: "small", style: Style.TEXT},
|
||||
|
@ -389,13 +396,33 @@ var stackLargeDelimiterSequence = [
|
|||
{type: "large", size: 2},
|
||||
{type: "large", size: 3},
|
||||
{type: "large", size: 4},
|
||||
{type: "stack"}
|
||||
];
|
||||
|
||||
// Delimiters that always stack try the small delimiters first, then stack
|
||||
const stackAlwaysDelimiterSequence = [
|
||||
{type: "small", style: Style.SCRIPTSCRIPT},
|
||||
{type: "small", style: Style.SCRIPT},
|
||||
{type: "small", style: Style.TEXT},
|
||||
{type: "stack"},
|
||||
];
|
||||
|
||||
// Delimiters that stack when large try the small and then large delimiters, and
|
||||
// stack afterwards
|
||||
const stackLargeDelimiterSequence = [
|
||||
{type: "small", style: Style.SCRIPTSCRIPT},
|
||||
{type: "small", style: Style.SCRIPT},
|
||||
{type: "small", style: Style.TEXT},
|
||||
{type: "large", size: 1},
|
||||
{type: "large", size: 2},
|
||||
{type: "large", size: 3},
|
||||
{type: "large", size: 4},
|
||||
{type: "stack"},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the font used in a delimiter based on what kind of delimiter it is.
|
||||
*/
|
||||
var delimTypeToFont = function(type) {
|
||||
const delimTypeToFont = function(type) {
|
||||
if (type.type === "small") {
|
||||
return "Main-Regular";
|
||||
} else if (type.type === "large") {
|
||||
|
@ -409,20 +436,20 @@ var delimTypeToFont = function(type) {
|
|||
* Traverse a sequence of types of delimiters to decide what kind of delimiter
|
||||
* should be used to create a delimiter of the given height+depth.
|
||||
*/
|
||||
var traverseSequence = function(delim, height, sequence, options) {
|
||||
const traverseSequence = function(delim, height, sequence, options) {
|
||||
// Here, we choose the index we should start at in the sequences. In smaller
|
||||
// sizes (which correspond to larger numbers in style.size) we start earlier
|
||||
// in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
|
||||
// at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
|
||||
var start = Math.min(2, 3 - options.style.size);
|
||||
for (var i = start; i < sequence.length; i++) {
|
||||
const start = Math.min(2, 3 - options.style.size);
|
||||
for (let i = start; i < sequence.length; i++) {
|
||||
if (sequence[i].type === "stack") {
|
||||
// This is always the last delimiter, so we just break the loop now.
|
||||
break;
|
||||
}
|
||||
|
||||
var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
|
||||
var heightDepth = metrics.height + metrics.depth;
|
||||
const metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
|
||||
let heightDepth = metrics.height + metrics.depth;
|
||||
|
||||
// Small delimiters are scaled down versions of the same font, so we
|
||||
// account for the style change size.
|
||||
|
@ -445,15 +472,16 @@ var traverseSequence = function(delim, height, sequence, options) {
|
|||
* Make a delimiter of a given height+depth, with optional centering. Here, we
|
||||
* traverse the sequences, and create a delimiter that the sequence tells us to.
|
||||
*/
|
||||
var makeCustomSizedDelim = function(delim, height, center, options, mode) {
|
||||
if (delim === "<") {
|
||||
const makeCustomSizedDelim = function(delim, height, center, options, mode,
|
||||
classes) {
|
||||
if (delim === "<" || delim === "\\lt") {
|
||||
delim = "\\langle";
|
||||
} else if (delim === ">") {
|
||||
} else if (delim === ">" || delim === "\\gt") {
|
||||
delim = "\\rangle";
|
||||
}
|
||||
|
||||
// Decide what sequence to use
|
||||
var sequence;
|
||||
let sequence;
|
||||
if (utils.contains(stackNeverDelimiters, delim)) {
|
||||
sequence = stackNeverDelimiterSequence;
|
||||
} else if (utils.contains(stackLargeDelimiters, delim)) {
|
||||
|
@ -463,16 +491,18 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
|
|||
}
|
||||
|
||||
// Look through the sequence
|
||||
var delimType = traverseSequence(delim, height, sequence, options);
|
||||
const delimType = traverseSequence(delim, height, sequence, options);
|
||||
|
||||
// Depending on the sequence element we decided on, call the appropriate
|
||||
// function.
|
||||
if (delimType.type === "small") {
|
||||
return makeSmallDelim(delim, delimType.style, center, options, mode);
|
||||
return makeSmallDelim(delim, delimType.style, center, options, mode,
|
||||
classes);
|
||||
} else if (delimType.type === "large") {
|
||||
return makeLargeDelim(delim, delimType.size, center, options, mode);
|
||||
return makeLargeDelim(delim, delimType.size, center, options, mode,
|
||||
classes);
|
||||
} else if (delimType.type === "stack") {
|
||||
return makeStackedDelim(delim, height, center, options, mode);
|
||||
return makeStackedDelim(delim, height, center, options, mode, classes);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -480,19 +510,20 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
|
|||
* Make a delimiter for use with `\left` and `\right`, given a height and depth
|
||||
* of an expression that the delimiters surround.
|
||||
*/
|
||||
var makeLeftRightDelim = function(delim, height, depth, options, mode) {
|
||||
const makeLeftRightDelim = function(delim, height, depth, options, mode,
|
||||
classes) {
|
||||
// We always center \left/\right delimiters, so the axis is always shifted
|
||||
var axisHeight =
|
||||
fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
|
||||
const axisHeight =
|
||||
options.style.metrics.axisHeight * options.style.sizeMultiplier;
|
||||
|
||||
// Taken from TeX source, tex.web, function make_left_right
|
||||
var delimiterFactor = 901;
|
||||
var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
|
||||
const delimiterFactor = 901;
|
||||
const delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
|
||||
|
||||
var maxDistFromAxis = Math.max(
|
||||
const maxDistFromAxis = Math.max(
|
||||
height - axisHeight, depth + axisHeight);
|
||||
|
||||
var totalHeight = Math.max(
|
||||
const totalHeight = Math.max(
|
||||
// In real TeX, calculations are done using integral values which are
|
||||
// 65536 per pt, or 655360 per em. So, the division here truncates in
|
||||
// TeX but doesn't here, producing different results. If we wanted to
|
||||
|
@ -507,11 +538,12 @@ var makeLeftRightDelim = function(delim, height, depth, options, mode) {
|
|||
|
||||
// Finally, we defer to `makeCustomSizedDelim` with our calculated total
|
||||
// height
|
||||
return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
|
||||
return makeCustomSizedDelim(delim, totalHeight, true, options, mode,
|
||||
classes);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sizedDelim: makeSizedDelim,
|
||||
customSizedDelim: makeCustomSizedDelim,
|
||||
leftRightDelim: makeLeftRightDelim
|
||||
leftRightDelim: makeLeftRightDelim,
|
||||
};
|
||||
|
|
137
src/domTree.js
137
src/domTree.js
|
@ -7,16 +7,16 @@
|
|||
*
|
||||
* Similar functions for working with MathML nodes exist in mathMLTree.js.
|
||||
*/
|
||||
|
||||
var utils = require("./utils");
|
||||
const unicodeRegexes = require("./unicodeRegexes");
|
||||
const utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Create an HTML className based on a list of classes. In addition to joining
|
||||
* with spaces, we also remove null or empty classes.
|
||||
*/
|
||||
var createClass = function(classes) {
|
||||
const createClass = function(classes) {
|
||||
classes = classes.slice();
|
||||
for (var i = classes.length - 1; i >= 0; i--) {
|
||||
for (let i = classes.length - 1; i >= 0; i--) {
|
||||
if (!classes[i]) {
|
||||
classes.splice(i, 1);
|
||||
}
|
||||
|
@ -30,14 +30,22 @@ var createClass = function(classes) {
|
|||
* an inline style. It also contains information about its height, depth, and
|
||||
* maxFontSize.
|
||||
*/
|
||||
function span(classes, children, height, depth, maxFontSize, style) {
|
||||
function span(classes, children, options) {
|
||||
this.classes = classes || [];
|
||||
this.children = children || [];
|
||||
this.height = height || 0;
|
||||
this.depth = depth || 0;
|
||||
this.maxFontSize = maxFontSize || 0;
|
||||
this.style = style || {};
|
||||
this.height = 0;
|
||||
this.depth = 0;
|
||||
this.maxFontSize = 0;
|
||||
this.style = {};
|
||||
this.attributes = {};
|
||||
if (options) {
|
||||
if (options.style.isTight()) {
|
||||
this.classes.push("mtight");
|
||||
}
|
||||
if (options.getColor()) {
|
||||
this.style.color = options.getColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,31 +57,35 @@ span.prototype.setAttribute = function(attribute, value) {
|
|||
this.attributes[attribute] = value;
|
||||
};
|
||||
|
||||
span.prototype.tryCombine = function(sibling) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the span into an HTML node
|
||||
*/
|
||||
span.prototype.toNode = function() {
|
||||
var span = document.createElement("span");
|
||||
const span = document.createElement("span");
|
||||
|
||||
// Apply the class
|
||||
span.className = createClass(this.classes);
|
||||
|
||||
// Apply inline styles
|
||||
for (var style in this.style) {
|
||||
for (const style in this.style) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
|
||||
span.style[style] = this.style[style];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply attributes
|
||||
for (var attr in this.attributes) {
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
span.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the children, also as HTML nodes
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
span.appendChild(this.children[i].toNode());
|
||||
}
|
||||
|
||||
|
@ -84,7 +96,7 @@ span.prototype.toNode = function() {
|
|||
* Convert the span into an HTML markup string
|
||||
*/
|
||||
span.prototype.toMarkup = function() {
|
||||
var markup = "<span";
|
||||
let markup = "<span";
|
||||
|
||||
// Add the class
|
||||
if (this.classes.length) {
|
||||
|
@ -93,10 +105,10 @@ span.prototype.toMarkup = function() {
|
|||
markup += "\"";
|
||||
}
|
||||
|
||||
var styles = "";
|
||||
let styles = "";
|
||||
|
||||
// Add the styles, after hyphenation
|
||||
for (var style in this.style) {
|
||||
for (const style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)) {
|
||||
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
|
||||
}
|
||||
|
@ -107,7 +119,7 @@ span.prototype.toMarkup = function() {
|
|||
}
|
||||
|
||||
// Add the attributes
|
||||
for (var attr in this.attributes) {
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += " " + attr + "=\"";
|
||||
markup += utils.escape(this.attributes[attr]);
|
||||
|
@ -118,7 +130,7 @@ span.prototype.toMarkup = function() {
|
|||
markup += ">";
|
||||
|
||||
// Add the markup of the children, also as markup
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
markup += this.children[i].toMarkup();
|
||||
}
|
||||
|
||||
|
@ -133,11 +145,11 @@ span.prototype.toMarkup = function() {
|
|||
* contains children and doesn't have any HTML properties. It also keeps track
|
||||
* of a height, depth, and maxFontSize.
|
||||
*/
|
||||
function documentFragment(children, height, depth, maxFontSize) {
|
||||
function documentFragment(children) {
|
||||
this.children = children || [];
|
||||
this.height = height || 0;
|
||||
this.depth = depth || 0;
|
||||
this.maxFontSize = maxFontSize || 0;
|
||||
this.height = 0;
|
||||
this.depth = 0;
|
||||
this.maxFontSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,10 +157,10 @@ function documentFragment(children, height, depth, maxFontSize) {
|
|||
*/
|
||||
documentFragment.prototype.toNode = function() {
|
||||
// Create a fragment
|
||||
var frag = document.createDocumentFragment();
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
// Append the children
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
frag.appendChild(this.children[i].toNode());
|
||||
}
|
||||
|
||||
|
@ -159,16 +171,24 @@ documentFragment.prototype.toNode = function() {
|
|||
* Convert the fragment into HTML markup
|
||||
*/
|
||||
documentFragment.prototype.toMarkup = function() {
|
||||
var markup = "";
|
||||
let markup = "";
|
||||
|
||||
// Simply concatenate the markup for the children together
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
markup += this.children[i].toMarkup();
|
||||
}
|
||||
|
||||
return markup;
|
||||
};
|
||||
|
||||
const iCombinations = {
|
||||
'î': '\u0131\u0302',
|
||||
'ï': '\u0131\u0308',
|
||||
'í': '\u0131\u0301',
|
||||
// 'ī': '\u0131\u0304', // enable when we add Extended Latin
|
||||
'ì': '\u0131\u0300',
|
||||
};
|
||||
|
||||
/**
|
||||
* A symbol node contains information about a single symbol. It either renders
|
||||
* to a single text node, or a span with a single text node in it, depending on
|
||||
|
@ -183,15 +203,62 @@ function symbolNode(value, height, depth, italic, skew, classes, style) {
|
|||
this.classes = classes || [];
|
||||
this.style = style || {};
|
||||
this.maxFontSize = 0;
|
||||
|
||||
// Mark CJK characters with specific classes so that we can specify which
|
||||
// fonts to use. This allows us to render these characters with a serif
|
||||
// font in situations where the browser would either default to a sans serif
|
||||
// or render a placeholder character.
|
||||
if (unicodeRegexes.cjkRegex.test(value)) {
|
||||
// I couldn't find any fonts that contained Hangul as well as all of
|
||||
// the other characters we wanted to test there for it gets its own
|
||||
// CSS class.
|
||||
if (unicodeRegexes.hangulRegex.test(value)) {
|
||||
this.classes.push('hangul_fallback');
|
||||
} else {
|
||||
this.classes.push('cjk_fallback');
|
||||
}
|
||||
}
|
||||
|
||||
if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin
|
||||
this.value = iCombinations[this.value];
|
||||
}
|
||||
}
|
||||
|
||||
symbolNode.prototype.tryCombine = function(sibling) {
|
||||
if (!sibling
|
||||
|| !(sibling instanceof symbolNode)
|
||||
|| this.italic > 0
|
||||
|| createClass(this.classes) !== createClass(sibling.classes)
|
||||
|| this.skew !== sibling.skew
|
||||
|| this.maxFontSize !== sibling.maxFontSize) {
|
||||
return false;
|
||||
}
|
||||
for (const style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)
|
||||
&& this.style[style] !== sibling.style[style]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const style in sibling.style) {
|
||||
if (sibling.style.hasOwnProperty(style)
|
||||
&& this.style[style] !== sibling.style[style]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.value += sibling.value;
|
||||
this.height = Math.max(this.height, sibling.height);
|
||||
this.depth = Math.max(this.depth, sibling.depth);
|
||||
this.italic = sibling.italic;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a text node or span from a symbol node. Note that a span is only
|
||||
* created if it is needed.
|
||||
*/
|
||||
symbolNode.prototype.toNode = function() {
|
||||
var node = document.createTextNode(this.value);
|
||||
var span = null;
|
||||
const node = document.createTextNode(this.value);
|
||||
let span = null;
|
||||
|
||||
if (this.italic > 0) {
|
||||
span = document.createElement("span");
|
||||
|
@ -203,7 +270,7 @@ symbolNode.prototype.toNode = function() {
|
|||
span.className = createClass(this.classes);
|
||||
}
|
||||
|
||||
for (var style in this.style) {
|
||||
for (const style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)) {
|
||||
span = span || document.createElement("span");
|
||||
span.style[style] = this.style[style];
|
||||
|
@ -224,9 +291,9 @@ symbolNode.prototype.toNode = function() {
|
|||
symbolNode.prototype.toMarkup = function() {
|
||||
// TODO(alpert): More duplication than I'd like from
|
||||
// span.prototype.toMarkup and symbolNode.prototype.toNode...
|
||||
var needsSpan = false;
|
||||
let needsSpan = false;
|
||||
|
||||
var markup = "<span";
|
||||
let markup = "<span";
|
||||
|
||||
if (this.classes.length) {
|
||||
needsSpan = true;
|
||||
|
@ -235,12 +302,12 @@ symbolNode.prototype.toMarkup = function() {
|
|||
markup += "\"";
|
||||
}
|
||||
|
||||
var styles = "";
|
||||
let styles = "";
|
||||
|
||||
if (this.italic > 0) {
|
||||
styles += "margin-right:" + this.italic + "em;";
|
||||
}
|
||||
for (var style in this.style) {
|
||||
for (const style in this.style) {
|
||||
if (this.style.hasOwnProperty(style)) {
|
||||
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
|
||||
}
|
||||
|
@ -251,7 +318,7 @@ symbolNode.prototype.toMarkup = function() {
|
|||
markup += " style=\"" + utils.escape(styles) + "\"";
|
||||
}
|
||||
|
||||
var escaped = utils.escape(this.value);
|
||||
const escaped = utils.escape(this.value);
|
||||
if (needsSpan) {
|
||||
markup += ">";
|
||||
markup += escaped;
|
||||
|
@ -265,5 +332,5 @@ symbolNode.prototype.toMarkup = function() {
|
|||
module.exports = {
|
||||
span: span,
|
||||
documentFragment: documentFragment,
|
||||
symbolNode: symbolNode
|
||||
symbolNode: symbolNode,
|
||||
};
|
||||
|
|
|
@ -1,132 +1,247 @@
|
|||
var parseData = require("./parseData");
|
||||
var ParseError = require("./ParseError");
|
||||
/* eslint no-constant-condition:0 */
|
||||
const parseData = require("./parseData");
|
||||
const ParseError = require("./ParseError");
|
||||
const Style = require("./Style");
|
||||
|
||||
var ParseNode = parseData.ParseNode;
|
||||
var ParseResult = parseData.ParseResult;
|
||||
const ParseNode = parseData.ParseNode;
|
||||
|
||||
/**
|
||||
* Parse the body of the environment, with rows delimited by \\ and
|
||||
* columns delimited by &, and create a nested list in row-major order
|
||||
* with one group per cell.
|
||||
* with one group per cell. If given an optional argument style
|
||||
* ("text", "display", etc.), then each cell is cast into that style.
|
||||
*/
|
||||
function parseArray(parser, pos, mode, result) {
|
||||
var row = [], body = [row], rowGaps = [];
|
||||
function parseArray(parser, result, style) {
|
||||
let row = [];
|
||||
const body = [row];
|
||||
const rowGaps = [];
|
||||
while (true) {
|
||||
var cell = parser.parseExpression(pos, mode, false, null);
|
||||
row.push(new ParseNode("ordgroup", cell.result, mode));
|
||||
pos = cell.position;
|
||||
var next = cell.peek.text;
|
||||
let cell = parser.parseExpression(false, null);
|
||||
cell = new ParseNode("ordgroup", cell, parser.mode);
|
||||
if (style) {
|
||||
cell = new ParseNode("styling", {
|
||||
style: style,
|
||||
value: [cell],
|
||||
}, parser.mode);
|
||||
}
|
||||
row.push(cell);
|
||||
const next = parser.nextToken.text;
|
||||
if (next === "&") {
|
||||
pos = cell.peek.position;
|
||||
parser.consume();
|
||||
} else if (next === "\\end") {
|
||||
break;
|
||||
} else if (next === "\\\\" || next === "\\cr") {
|
||||
var cr = parser.parseFunction(pos, mode);
|
||||
rowGaps.push(cr.result.value.size);
|
||||
pos = cr.position;
|
||||
const cr = parser.parseFunction();
|
||||
rowGaps.push(cr.value.size);
|
||||
row = [];
|
||||
body.push(row);
|
||||
} else {
|
||||
throw new ParseError("Expected & or \\\\ or \\end",
|
||||
parser.lexer, cell.peek.position);
|
||||
parser.nextToken);
|
||||
}
|
||||
}
|
||||
result.body = body;
|
||||
result.rowGaps = rowGaps;
|
||||
return new ParseResult(new ParseNode(result.type, result, mode), pos);
|
||||
return new ParseNode(result.type, result, parser.mode);
|
||||
}
|
||||
|
||||
/*
|
||||
* An environment definition is very similar to a function definition.
|
||||
* Each element of the following array may contain
|
||||
* - names: The names associated with a function. This can be used to
|
||||
* share one implementation between several similar environments.
|
||||
* An environment definition is very similar to a function definition:
|
||||
* it is declared with a name or a list of names, a set of properties
|
||||
* and a handler containing the actual implementation.
|
||||
*
|
||||
* The properties include:
|
||||
* - numArgs: The number of arguments after the \begin{name} function.
|
||||
* - argTypes: (optional) Just like for a function
|
||||
* - allowedInText: (optional) Whether or not the environment is allowed inside
|
||||
* text mode (default false) (not enforced yet)
|
||||
* - numOptionalArgs: (optional) Just like for a function
|
||||
* - handler: The function that is called to handle this environment.
|
||||
* It will receive the following arguments:
|
||||
* - pos: the current position of the parser.
|
||||
* - mode: the current parsing mode.
|
||||
* - envName: the name of the environment, one of the listed names.
|
||||
* - [args]: the arguments passed to \begin.
|
||||
* - positions: the positions associated with these arguments.
|
||||
* A bare number instead of that object indicates the numArgs value.
|
||||
*
|
||||
* The handler function will receive two arguments
|
||||
* - context: information and references provided by the parser
|
||||
* - args: an array of arguments passed to \begin{name}
|
||||
* The context contains the following properties:
|
||||
* - envName: the name of the environment, one of the listed names.
|
||||
* - parser: the parser object
|
||||
* - lexer: the lexer object
|
||||
* - positions: the positions associated with these arguments from args.
|
||||
* The handler must return a ParseResult.
|
||||
*/
|
||||
|
||||
var environmentDefinitions = [
|
||||
|
||||
// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
|
||||
// is part of the source2e.pdf file of LaTeX2e source documentation.
|
||||
{
|
||||
names: ["array"],
|
||||
numArgs: 1,
|
||||
handler: function(pos, mode, envName, colalign, positions) {
|
||||
var parser = this;
|
||||
// Currently only supports alignment, no separators like | yet.
|
||||
colalign = colalign.value.map ? colalign.value : [colalign];
|
||||
colalign = colalign.map(function(node) {
|
||||
var ca = node.value;
|
||||
if ("lcr".indexOf(ca) !== -1) {
|
||||
return ca;
|
||||
}
|
||||
throw new ParseError(
|
||||
"Unknown column alignment: " + node.value,
|
||||
parser.lexer, positions[1]);
|
||||
});
|
||||
var res = {
|
||||
type: "array",
|
||||
colalign: colalign,
|
||||
hskipBeforeAndAfter: true // \@preamble in lttab.dtx
|
||||
};
|
||||
res = parseArray(parser, pos, mode, res);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
|
||||
// The matrix environments of amsmath builds on the array environment
|
||||
// of LaTeX, which is discussed above.
|
||||
{
|
||||
names: ["matrix", "pmatrix", "bmatrix", "vmatrix", "Vmatrix"],
|
||||
handler: function(pos, mode, envName) {
|
||||
var delimiters = {
|
||||
"matrix": null,
|
||||
"pmatrix": ["(", ")"],
|
||||
"bmatrix": ["[", "]"],
|
||||
"vmatrix": ["|", "|"],
|
||||
"Vmatrix": ["\\Vert", "\\Vert"]
|
||||
}[envName];
|
||||
var res = {
|
||||
type: "array",
|
||||
hskipBeforeAndAfter: false // \hskip -\arraycolsep in amsmath
|
||||
};
|
||||
res = parseArray(this, pos, mode, res);
|
||||
if (delimiters) {
|
||||
res.result = new ParseNode("leftright", {
|
||||
body: [res.result],
|
||||
left: delimiters[0],
|
||||
right: delimiters[1]
|
||||
}, mode);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function defineEnvironment(names, props, handler) {
|
||||
if (typeof names === "string") {
|
||||
names = [names];
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
module.exports = (function() {
|
||||
// nested function so we don't leak i and j into the module scope
|
||||
var exports = {};
|
||||
for (var i = 0; i < environmentDefinitions.length; ++i) {
|
||||
var def = environmentDefinitions[i];
|
||||
def.greediness = 1;
|
||||
def.allowedInText = !!def.allowedInText;
|
||||
def.numArgs = def.numArgs || 0;
|
||||
def.numOptionalArgs = def.numOptionalArgs || 0;
|
||||
for (var j = 0; j < def.names.length; ++j) {
|
||||
exports[def.names[j]] = def;
|
||||
}
|
||||
if (typeof props === "number") {
|
||||
props = { numArgs: props };
|
||||
}
|
||||
return exports;
|
||||
})();
|
||||
// Set default values of environments
|
||||
const data = {
|
||||
numArgs: props.numArgs || 0,
|
||||
argTypes: props.argTypes,
|
||||
greediness: 1,
|
||||
allowedInText: !!props.allowedInText,
|
||||
numOptionalArgs: props.numOptionalArgs || 0,
|
||||
handler: handler,
|
||||
};
|
||||
for (let i = 0; i < names.length; ++i) {
|
||||
module.exports[names[i]] = data;
|
||||
}
|
||||
}
|
||||
|
||||
// Decides on a style for cells in an array according to whether the given
|
||||
// environment name starts with the letter 'd'.
|
||||
function dCellStyle(envName) {
|
||||
if (envName.substr(0, 1) === "d") {
|
||||
return "display";
|
||||
} else {
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
|
||||
// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
|
||||
// is part of the source2e.pdf file of LaTeX2e source documentation.
|
||||
// {darray} is an {array} environment where cells are set in \displaystyle,
|
||||
// as defined in nccmath.sty.
|
||||
defineEnvironment(["array", "darray"], {
|
||||
numArgs: 1,
|
||||
}, function(context, args) {
|
||||
let colalign = args[0];
|
||||
colalign = colalign.value.map ? colalign.value : [colalign];
|
||||
const cols = colalign.map(function(node) {
|
||||
const ca = node.value;
|
||||
if ("lcr".indexOf(ca) !== -1) {
|
||||
return {
|
||||
type: "align",
|
||||
align: ca,
|
||||
};
|
||||
} else if (ca === "|") {
|
||||
return {
|
||||
type: "separator",
|
||||
separator: "|",
|
||||
};
|
||||
}
|
||||
throw new ParseError(
|
||||
"Unknown column alignment: " + node.value,
|
||||
node);
|
||||
});
|
||||
let res = {
|
||||
type: "array",
|
||||
cols: cols,
|
||||
hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
|
||||
};
|
||||
res = parseArray(context.parser, res, dCellStyle(context.envName));
|
||||
return res;
|
||||
});
|
||||
|
||||
// The matrix environments of amsmath builds on the array environment
|
||||
// of LaTeX, which is discussed above.
|
||||
defineEnvironment([
|
||||
"matrix",
|
||||
"pmatrix",
|
||||
"bmatrix",
|
||||
"Bmatrix",
|
||||
"vmatrix",
|
||||
"Vmatrix",
|
||||
], {
|
||||
}, function(context) {
|
||||
const delimiters = {
|
||||
"matrix": null,
|
||||
"pmatrix": ["(", ")"],
|
||||
"bmatrix": ["[", "]"],
|
||||
"Bmatrix": ["\\{", "\\}"],
|
||||
"vmatrix": ["|", "|"],
|
||||
"Vmatrix": ["\\Vert", "\\Vert"],
|
||||
}[context.envName];
|
||||
let res = {
|
||||
type: "array",
|
||||
hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath
|
||||
};
|
||||
res = parseArray(context.parser, res, dCellStyle(context.envName));
|
||||
if (delimiters) {
|
||||
res = new ParseNode("leftright", {
|
||||
body: [res],
|
||||
left: delimiters[0],
|
||||
right: delimiters[1],
|
||||
}, context.mode);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
// A cases environment (in amsmath.sty) is almost equivalent to
|
||||
// \def\arraystretch{1.2}%
|
||||
// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
|
||||
// {dcases} is a {cases} environment where cells are set in \displaystyle,
|
||||
// as defined in mathtools.sty.
|
||||
defineEnvironment([
|
||||
"cases",
|
||||
"dcases",
|
||||
], {
|
||||
}, function(context) {
|
||||
let res = {
|
||||
type: "array",
|
||||
arraystretch: 1.2,
|
||||
cols: [{
|
||||
type: "align",
|
||||
align: "l",
|
||||
pregap: 0,
|
||||
// TODO(kevinb) get the current style.
|
||||
// For now we use the metrics for TEXT style which is what we were
|
||||
// doing before. Before attempting to get the current style we
|
||||
// should look at TeX's behavior especially for \over and matrices.
|
||||
postgap: Style.TEXT.metrics.quad,
|
||||
}, {
|
||||
type: "align",
|
||||
align: "l",
|
||||
pregap: 0,
|
||||
postgap: 0,
|
||||
}],
|
||||
};
|
||||
res = parseArray(context.parser, res, dCellStyle(context.envName));
|
||||
res = new ParseNode("leftright", {
|
||||
body: [res],
|
||||
left: "\\{",
|
||||
right: ".",
|
||||
}, context.mode);
|
||||
return res;
|
||||
});
|
||||
|
||||
// An aligned environment is like the align* environment
|
||||
// except it operates within math mode.
|
||||
// Note that we assume \nomallineskiplimit to be zero,
|
||||
// so that \strut@ is the same as \strut.
|
||||
defineEnvironment("aligned", {
|
||||
}, function(context) {
|
||||
let res = {
|
||||
type: "array",
|
||||
cols: [],
|
||||
};
|
||||
res = parseArray(context.parser, res);
|
||||
const emptyGroup = new ParseNode("ordgroup", [], context.mode);
|
||||
let numCols = 0;
|
||||
res.value.body.forEach(function(row) {
|
||||
for (let i = 1; i < row.length; i += 2) {
|
||||
row[i].value.unshift(emptyGroup);
|
||||
}
|
||||
if (numCols < row.length) {
|
||||
numCols = row.length;
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < numCols; ++i) {
|
||||
let align = "r";
|
||||
let pregap = 0;
|
||||
if (i % 2 === 1) {
|
||||
align = "l";
|
||||
} else if (i > 0) {
|
||||
pregap = 2; // one \qquad between columns
|
||||
}
|
||||
res.value.cols[i] = {
|
||||
type: "align",
|
||||
align: align,
|
||||
pregap: pregap,
|
||||
postgap: 0,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
|
File diff suppressed because one or more lines are too long
1752
src/fontMetricsData.js
Normal file
1752
src/fontMetricsData.js
Normal file
File diff suppressed because it is too large
Load Diff
1133
src/functions.js
1133
src/functions.js
File diff suppressed because it is too large
Load Diff
23
src/macros.js
Normal file
23
src/macros.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Predefined macros for KaTeX.
|
||||
* This can be used to define some commands in terms of others.
|
||||
*/
|
||||
|
||||
// This function might one day accept additional argument and do more things.
|
||||
function defineMacro(name, body) {
|
||||
module.exports[name] = body;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// basics
|
||||
defineMacro("\\bgroup", "{");
|
||||
defineMacro("\\egroup", "}");
|
||||
defineMacro("\\begingroup", "{");
|
||||
defineMacro("\\endgroup", "}");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// amsmath.sty
|
||||
|
||||
// \def\overset#1#2{\binrel@{#2}\binrel@@{\mathop{\kern\z@#2}\limits^{#1}}}
|
||||
defineMacro("\\overset", "\\mathop{#2}\\limits^{#1}");
|
||||
defineMacro("\\underset", "\\mathop{#2}\\limits_{#1}");
|
|
@ -8,7 +8,7 @@
|
|||
* domTree.js, creating namespaced DOM nodes and HTML text markup respectively.
|
||||
*/
|
||||
|
||||
var utils = require("./utils");
|
||||
const utils = require("./utils");
|
||||
|
||||
/**
|
||||
* This node represents a general purpose MathML node of any type. The
|
||||
|
@ -33,16 +33,16 @@ MathNode.prototype.setAttribute = function(name, value) {
|
|||
* Converts the math node into a MathML-namespaced DOM element.
|
||||
*/
|
||||
MathNode.prototype.toNode = function() {
|
||||
var node = document.createElementNS(
|
||||
const node = document.createElementNS(
|
||||
"http://www.w3.org/1998/Math/MathML", this.type);
|
||||
|
||||
for (var attr in this.attributes) {
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
node.setAttribute(attr, this.attributes[attr]);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
node.appendChild(this.children[i].toNode());
|
||||
}
|
||||
|
||||
|
@ -53,10 +53,10 @@ MathNode.prototype.toNode = function() {
|
|||
* Converts the math node into an HTML markup string.
|
||||
*/
|
||||
MathNode.prototype.toMarkup = function() {
|
||||
var markup = "<" + this.type;
|
||||
let markup = "<" + this.type;
|
||||
|
||||
// Add the attributes
|
||||
for (var attr in this.attributes) {
|
||||
for (const attr in this.attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
||||
markup += " " + attr + "=\"";
|
||||
markup += utils.escape(this.attributes[attr]);
|
||||
|
@ -66,7 +66,7 @@ MathNode.prototype.toMarkup = function() {
|
|||
|
||||
markup += ">";
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
markup += this.children[i].toMarkup();
|
||||
}
|
||||
|
||||
|
@ -98,5 +98,5 @@ TextNode.prototype.toMarkup = function() {
|
|||
|
||||
module.exports = {
|
||||
MathNode: MathNode,
|
||||
TextNode: TextNode
|
||||
TextNode: TextNode,
|
||||
};
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
/**
|
||||
* The resulting parse tree nodes of the parse tree.
|
||||
*
|
||||
* It is possible to provide position information, so that a ParseNode can
|
||||
* fulfil a role similar to a Token in error reporting.
|
||||
* For details on the corresponding properties see Token constructor.
|
||||
* Providing such information can lead to better error reporting.
|
||||
*
|
||||
* @param {string} type type of node, like e.g. "ordgroup"
|
||||
* @param {?object} value type-specific representation of the node
|
||||
* @param {string} mode parse mode in action for this node,
|
||||
* "math" or "text"
|
||||
* @param {Token=} firstToken first token of the input for this node,
|
||||
* will omit position information if unset
|
||||
* @param {Token=} lastToken last token of the input for this node,
|
||||
* will default to firstToken if unset
|
||||
*/
|
||||
function ParseNode(type, value, mode) {
|
||||
function ParseNode(type, value, mode, firstToken, lastToken) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A result and final position returned by the `.parse...` functions.
|
||||
*
|
||||
*/
|
||||
function ParseResult(result, newPosition, peek) {
|
||||
this.result = result;
|
||||
this.position = newPosition;
|
||||
if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) {
|
||||
this.lexer = firstToken.lexer;
|
||||
this.start = firstToken.start;
|
||||
this.end = (lastToken || firstToken).end;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ParseNode: ParseNode,
|
||||
ParseResult: ParseResult
|
||||
};
|
||||
|
||||
|
|
|
@ -3,13 +3,16 @@
|
|||
* TODO(emily): Remove this
|
||||
*/
|
||||
|
||||
var Parser = require("./Parser");
|
||||
const Parser = require("./Parser");
|
||||
|
||||
/**
|
||||
* Parses an expression using a Parser, then returns the parsed result.
|
||||
*/
|
||||
var parseTree = function(toParse, settings) {
|
||||
var parser = new Parser(toParse, settings);
|
||||
const parseTree = function(toParse, settings) {
|
||||
if (!(typeof toParse === 'string' || toParse instanceof String)) {
|
||||
throw new TypeError('KaTeX can only parse string typed expression');
|
||||
}
|
||||
const parser = new Parser(toParse, settings);
|
||||
|
||||
return parser.parse();
|
||||
};
|
||||
|
|
3165
src/symbols.js
3165
src/symbols.js
File diff suppressed because it is too large
Load Diff
15
src/unicodeRegexes.js
Normal file
15
src/unicodeRegexes.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const hangulRegex = /[\uAC00-\uD7AF]/;
|
||||
|
||||
// This regex combines
|
||||
// - Hiragana: [\u3040-\u309F]
|
||||
// - Katakana: [\u30A0-\u30FF]
|
||||
// - CJK ideograms: [\u4E00-\u9FAF]
|
||||
// - Hangul syllables: [\uAC00-\uD7AF]
|
||||
// Notably missing are halfwidth Katakana and Romanji glyphs.
|
||||
const cjkRegex =
|
||||
/[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/;
|
||||
|
||||
module.exports = {
|
||||
cjkRegex: cjkRegex,
|
||||
hangulRegex: hangulRegex,
|
||||
};
|
46
src/utils.js
46
src/utils.js
|
@ -7,16 +7,16 @@
|
|||
* Provide an `indexOf` function which works in IE8, but defers to native if
|
||||
* possible.
|
||||
*/
|
||||
var nativeIndexOf = Array.prototype.indexOf;
|
||||
var indexOf = function(list, elem) {
|
||||
const nativeIndexOf = Array.prototype.indexOf;
|
||||
const indexOf = function(list, elem) {
|
||||
if (list == null) {
|
||||
return -1;
|
||||
}
|
||||
if (nativeIndexOf && list.indexOf === nativeIndexOf) {
|
||||
return list.indexOf(elem);
|
||||
}
|
||||
var i = 0, l = list.length;
|
||||
for (; i < l; i++) {
|
||||
const l = list.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
if (list[i] === elem) {
|
||||
return i;
|
||||
}
|
||||
|
@ -27,29 +27,36 @@ var indexOf = function(list, elem) {
|
|||
/**
|
||||
* Return whether an element is contained in a list
|
||||
*/
|
||||
var contains = function(list, elem) {
|
||||
const contains = function(list, elem) {
|
||||
return indexOf(list, elem) !== -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provide a default value if a setting is undefined
|
||||
*/
|
||||
const deflt = function(setting, defaultIfUndefined) {
|
||||
return setting === undefined ? defaultIfUndefined : setting;
|
||||
};
|
||||
|
||||
// hyphenate and escape adapted from Facebook's React under Apache 2 license
|
||||
|
||||
var uppercase = /([A-Z])/g;
|
||||
var hyphenate = function(str) {
|
||||
const uppercase = /([A-Z])/g;
|
||||
const hyphenate = function(str) {
|
||||
return str.replace(uppercase, "-$1").toLowerCase();
|
||||
};
|
||||
|
||||
var ESCAPE_LOOKUP = {
|
||||
"&": "&",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
"\"": """,
|
||||
"'": "'"
|
||||
const ESCAPE_LOOKUP = {
|
||||
"&": "&",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
"\"": """,
|
||||
"'": "'",
|
||||
};
|
||||
|
||||
var ESCAPE_REGEX = /[&><"']/g;
|
||||
const ESCAPE_REGEX = /[&><"']/g;
|
||||
|
||||
function escaper(match) {
|
||||
return ESCAPE_LOOKUP[match];
|
||||
return ESCAPE_LOOKUP[match];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,16 +66,16 @@ function escaper(match) {
|
|||
* @return {string} An escaped string.
|
||||
*/
|
||||
function escape(text) {
|
||||
return ("" + text).replace(ESCAPE_REGEX, escaper);
|
||||
return ("" + text).replace(ESCAPE_REGEX, escaper);
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to set the text content of a DOM element in all supported
|
||||
* browsers. Note that we don't define this if there is no document.
|
||||
*/
|
||||
var setTextContent;
|
||||
let setTextContent;
|
||||
if (typeof document !== "undefined") {
|
||||
var testNode = document.createElement("span");
|
||||
const testNode = document.createElement("span");
|
||||
if ("textContent" in testNode) {
|
||||
setTextContent = function(node, text) {
|
||||
node.textContent = text;
|
||||
|
@ -89,9 +96,10 @@ function clearNode(node) {
|
|||
|
||||
module.exports = {
|
||||
contains: contains,
|
||||
deflt: deflt,
|
||||
escape: escape,
|
||||
hyphenate: hyphenate,
|
||||
indexOf: indexOf,
|
||||
setTextContent: setTextContent,
|
||||
clearNode: clearNode
|
||||
clearNode: clearNode,
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
|
||||
.use-ttf(@family, @family-suffix) when (@use-ttf = true) {
|
||||
src+: url('@{font-folder}/KaTeX_@{family}-@{family-suffix}.ttf') format('ttf')
|
||||
src+: url('@{font-folder}/KaTeX_@{family}-@{family-suffix}.ttf') format('truetype')
|
||||
}
|
||||
|
||||
.generate-suffix(@weight, @style) when (@weight = normal) and (@style = normal) {
|
||||
|
@ -48,22 +48,22 @@
|
|||
}
|
||||
|
||||
.font-face('AMS', normal, normal);
|
||||
// .font-face('Caligraphic', bold, normal);
|
||||
// .font-face('Caligraphic', normal, normal);
|
||||
// .font-face('Fraktur', bold, normal);
|
||||
// .font-face('Fraktur', normal, normal);
|
||||
.font-face('Caligraphic', bold, normal);
|
||||
.font-face('Caligraphic', normal, normal);
|
||||
.font-face('Fraktur', bold, normal);
|
||||
.font-face('Fraktur', normal, normal);
|
||||
.font-face('Main', bold, normal);
|
||||
.font-face('Main', normal, italic);
|
||||
.font-face('Main', normal, normal);
|
||||
.font-face('Math', bold, italic);
|
||||
// .font-face('Math', bold, italic);
|
||||
.font-face('Math', normal, italic);
|
||||
.font-face('Math', normal, normal);
|
||||
// .font-face('Math', normal, normal);
|
||||
// .font-face('SansSerif', bold, normal);
|
||||
// .font-face('SansSerif', normal, italic);
|
||||
// .font-face('SansSerif', normal, normal);
|
||||
// .font-face('Script', normal, normal);
|
||||
.font-face('SansSerif', normal, normal);
|
||||
.font-face('Script', normal, normal);
|
||||
.font-face('Size1', normal, normal);
|
||||
.font-face('Size2', normal, normal);
|
||||
.font-face('Size3', normal, normal);
|
||||
.font-face('Size4', normal, normal);
|
||||
// .font-face('Typewriter', normal, normal);
|
||||
.font-face('Typewriter', normal, normal);
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user