Compare commits

...

275 Commits

Author SHA1 Message Date
Kevin Barabash
e4b93379fb Merge pull request #699 from edemaine/middle
Refactor and comment space splicing code
2017-05-13 15:07:05 -04:00
Erik Demaine
5165cc48e6 Refactor and comment space splicing code 2017-05-12 09:33:14 -04:00
Eddie Kohler
7192bd6595 Fix #683.
Shift spaces back into the replacement middle delimiter.
2017-05-12 09:28:49 -04:00
Tab Atkins Jr
b9e7b6898f Rephrase displayMode description to be clearer (#648)
* Clarify meaning of true and false
* Clarify inline vs. block
2017-04-27 19:02:19 -04:00
Kevin Barabash
429c1f6f52 Merge pull request #678 from edemaine/symbols
Fix font typo math -> main
2017-04-15 20:55:53 -04:00
Kevin Barabash
2c92a9a368 Merge pull request #605 from gagern/overset
Builtin macros, macro arguments, \overset and \underset
2017-04-15 20:39:01 -04:00
Erik Demaine
171e38f28a Old font command support: \rm, \sf, \tt, \bf, \it (#675)
Squashed:
* \rm, \sf, \tt, \bf, \it support
* Fix space consumption after macros without arguments
* Add tests for old font commands
2017-04-07 22:06:23 +01:00
Erik Demaine
1379c2666e Fix font typo math -> main 2017-04-07 14:51:57 -04:00
Martin von Gagern
3a95d8889b Added screenshotter tests for overset and underset 2017-04-05 21:43:21 +01:00
Martin von Gagern
28ad473e4a Indicate missing support for delimited macros 2017-04-05 21:43:12 +01:00
Martin von Gagern
e2763a3007 Support \bgroup and \egroup 2017-04-05 21:43:04 +01:00
Martin von Gagern
96d1e6aea7 Introduce defineMacro function
… as suggested by Erik Demaine, to future-proof the code.
2017-04-05 21:42:48 +01:00
Martin von Gagern
7ec455083f Builtin macros, macro arguments, overset and underset
* Ship predefined macros with the library, in macros.js.
* Allow macro arguments #1 and so on, with argument count deduced from string.
* Use these features to implement \overset and \underset, fixes #484.
2017-04-05 21:42:38 +01:00
Noxx
40ec1b92b8 Fixed missing verb in README (#668) 2017-03-14 11:53:20 -04:00
Tab Atkins Jr
f3df1ccbac Use utils.deflt for Settings (#649)
Rather than duplicate the definition of the undefined-defaulter, use the version already present in utils.
2017-01-27 07:54:05 +01:00
Sergey Pashinin
fdd83db65a fix #509: added some international operators (#647)
* fix #509: added some international operators

* trailing comma
2017-01-26 15:04:18 -05:00
Kevin Barabash
742e512172 Merge pull request #642 from gagern/release
Fix release script after experience from 0.7.1 release
2017-01-23 07:53:48 -05:00
Kevin Barabash
3fea3dfbbc Merge pull request #635 from gagern/fontDiff
Set up a diff attribute and textconv instructions for font files
2017-01-22 19:57:03 -05:00
Kevin Barabash
301d7cfb1a Merge pull request #639 from gagern/printSize
Show compressed sizes correctly
2017-01-22 19:27:17 -05:00
Kevin Barabash
7aa84d010c Merge pull request #644 from gagern/readmeMacros
Describe site-provided macros in README
2017-01-22 18:21:51 -05:00
Martin von Gagern
59c4e008db Fix case of string in README 2017-01-22 22:39:57 +01:00
Martin von Gagern
fc115e1f66 Obtain updated README by using a checkout
As the dist directory isn't available on the original branch, we cant use
its files to update the SRI hashes.  Checking out from the release tag is
therefore the better option.  A checkout will automatically stage its files.
2017-01-22 15:28:37 +01:00
Martin von Gagern
079fecaf86 Update README files to 0.7.1 release 2017-01-22 14:48:00 +01:00
Martin von Gagern
76051a6ac2 Fix release script after experience from 0.7.1 release
* Also edit dist/README.md. Otherwise npm publish will overwrite that
  AFTER the dist directory has been added, causing the git checkout to fail.
  And it's the right thing to do anyway, having ALL the READMEs edited.
* Add all the modified READMEs so they get committed correctly.
* Improve two references to master which are no longer accurate.
* Check for uncommitted changes just before creating the tag.
* Encourage always specifying the next version, as discussed in
  https://github.com/Khan/KaTeX/pull/615#discussion_r97208770
2017-01-22 00:51:00 +01:00
Kevin Barabash
8dd161d4b2 Merge pull request #615 from gagern/release
Improve release script and bower support
2017-01-21 17:49:15 -05:00
Erik Demaine
a5e38d3b8a Switch speed test in README (#640)
Permission to reference page granted in
https://github.com/Khan/KaTeX/issues/420#issuecomment-270810784
2017-01-20 22:50:10 +01:00
Martin von Gagern
8dc1374e7a Show compressed sizes correctly
On my macOS 10.12 all the sizes used to show as zero.  Having them in a
single shell invocation should avoid that problem in a portable fashion.
2017-01-20 12:05:03 +01:00
Erik Demaine
a738185704 Fix x'^2 (#636)
* Fix x'^2
* Add screenshot for x'^2
* x^2' should fail.  Add and fix multiple prime/superscript tests.
2017-01-19 19:50:16 +01:00
Martin von Gagern
a3f0653e5c Set up a diff attribute and textconv instructions for font files
This may help us generate human-readable diffs of font files.
2017-01-19 17:47:59 +01:00
Erik Demaine
d4aa6a7253 Fix all AMS mathord symbols (#618)
* Fix all AMS mathord symbols

* Fix \imath, \jmath, \pounds support

* Fix \mathit support

* Fix Greek capitals

* Default font to main (fix Unicode support)

* Now using correct \maltese

* Correct mathit documentation

* var -> const

* Add trailing commas

* Remove greekCapitals (no longer needed)
2017-01-15 19:05:21 -05:00
Erik Demaine
f1c02226cc Change cell style to 'text' in {array}, {matrix}, {cases}. Add {darray} and {dcases}. (#606)
* Change cell style to 'text' in {array}, {matrix}, {cases}.
* Add {darray} and {dcases} which use display style for their cells.
* Add ArrayMode test with \frac's inside {array} in display mode.
2017-01-15 22:17:45 +01:00
Martin von Gagern
38ba9f9187 Serve files with and without babelify step
As babelify is slow, it may be desriable to not run it during development.
This is OK if the browser is recent enough to understand ES6 natively.
(This does not include current Firefox due to it having problems with
for(const … in …), https://bugzilla.mozilla.org/show_bug.cgi?id=1094995.)
For older browsers, or to check issues possibly introduced by babelify,
adding /babel as the first component of the path will switch to a version
which has been processed by babelify.  This is also used for screenshots.
2017-01-13 22:37:17 -05:00
Martin von Gagern
bd9db332d2 Turn var into const or let 2017-01-13 22:37:17 -05:00
Martin von Gagern
9b565a6375 Include babelify step in browserify calls
This allows using ES6 syntax in our code, while maintaining backwards
compatibility for the generated file.
2017-01-13 22:37:17 -05:00
Martin von Gagern
a0cd343d85 Improve description of macros
Separate type, meaning and format to clarify the paragraph.
2017-01-13 19:59:36 +01:00
Martin von Gagern
5e4f50dc09 Drop --insane flag, just formulate the prompt appropriately 2017-01-13 13:48:25 +01:00
Martin von Gagern
5783e193ed Update CDN version numbers and SRI hashes on release 2017-01-13 13:43:41 +01:00
Martin von Gagern
961e9db514 Add --dry-run and --insane options to release script
Now we have some sanity checks, a way to skip them and a way to see which
commands would be executed on the git and npm side without actually
executing them.
2017-01-13 13:38:39 +01:00
Martin von Gagern
35ff154ead Make release tag an annotated tag
It is customary to use annotated tags for releases, to preserve the
information about when the tag itself was created, by whom and for what
purpose.  In our case we always have a commit directly before the tag, but
some workflows may expect annotated tags nonetheless, “git describe” among
them.  We might want to sign them one day, too.
2017-01-13 13:38:39 +01:00
Martin von Gagern
6404cfd269 Drop version field from bower.json
According to bower docs, that field is deprecated and ignored.
Git tag names (or commit ids) are the way to identify a given version.
2017-01-13 13:38:39 +01:00
Martin von Gagern
73cafd16da Avoid some warnings from bower
Bower prefers to have package names in lower case, and to have uncompressed
sources listed as entry points.  Otherwise it will issue warnings.
2017-01-13 13:38:39 +01:00
Martin von Gagern
a3988d9d56 Allow creating releases from branches other than master 2017-01-13 13:38:39 +01:00
Martin von Gagern
840771c643 Un-ignore dist directory on releases for the sake of bower 2017-01-13 13:38:39 +01:00
James Lee
549104c5a8 auto-renderer README.md - added integrity (#591)
Integrity generated from https://www.srihash.org/
Reference: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
2017-01-13 12:15:57 +01:00
James Lee
90ba0e341d README.md - added integrity (#590)
Integrity generated from https://www.srihash.org/
Reference: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
2017-01-13 12:15:28 +01:00
Martin von Gagern
0c5d9d55cf Document macros in options object 2017-01-13 12:13:07 +01:00
Kevin Barabash
56f3d1db47 Don't cascade .eslintrc
eslint will automatically merge .eslintrc files in the parent folder without "root": true
2017-01-12 22:53:10 -05:00
Kevin Barabash
c56de9e323 Merge pull request #625 from gagern/morgan
Use morgan to log requests in the dev server
2017-01-12 10:15:20 -05:00
Martin von Gagern
79d53bd29f Use morgan to log requests in the dev server
This is motivated by the recent switch to Express 4.
Without this commit, “make serve” will print the following message:

Error: Most middleware (like logger) is no longer bundled
with Express and must be installed separately.  Please see
https://github.com/senchalabs/connect#middleware.
2017-01-12 09:52:29 +01:00
Martin von Gagern
4ead46c4ab Describe using auto renderer in html head (#623)
Describe using auto renderer in html head

Motivated by #621.
2017-01-11 21:04:52 +01:00
Martin von Gagern
59b87539b9 Merge pull request #622 from gagern/commas
Revert "Remove trailing commas for IE 9 compatibility"
2017-01-11 20:56:39 +01:00
Martin von Gagern
896aeac81b Fix some indentation and comma inconsistencies
This makes eslint happy again.
2017-01-11 13:30:11 +01:00
Martin von Gagern
53e416e296 Revert "Remove trailing commas for IE 9 compatibility"
This reverts commit 4d2e46e7f6.

Having trailing commans makes diffs easier to read as it avoids modifying a
line just to add a trailing comma if there is another item to add at the end
of a list.  There are plans to switch to ES6 notation and to translate that
to ES5 as part of the build process.  Since that translation would remove
trailing commas, the IE9 problems that originally motivated the commit
should vanish soon.
2017-01-11 13:26:00 +01:00
Kevin Barabash
677290336a Merge pull request #612 from gagern/updates
Update dependencies to more recent version
2017-01-10 11:09:02 -05:00
Martin von Gagern
90e25fecc2 Update dependencies to more recent version
All these version ranges include the latest version at the time of this
commit, except for the selenium webdriver.  There version 3 is incompatible
with version 2, and switching to a version 3 library appears to require
switching to version 3 docker images as well, which would entail using
different browser versions which in turn would lead to differences for a
large number of screenshots.  That doesn't appear warranted at this time.
2017-01-09 15:09:43 +01:00
Emily Eisenberg
8f46eeaf1b Bump master to v0.8.0-pre 2017-01-06 19:50:50 -08:00
Emily Eisenberg
225baffef4 And make the release script actually work again.
Summary: The previous commit made the `sed` lines do the same thing on mac and
linux. Unfortunately they didn't actually work. Whoops.

Test Plan:
 - Run `./release.sh` a couple times and become horribly confused before
   realizing what's going on.

@kevinb
2017-01-06 19:47:47 -08:00
Emily Eisenberg
4327e85796 Make the release script compatible with both mac and linux
Curse you BSD and GNU sed being so different!

Test Plan:
 - Run `sed -i.bak -e 's|boo|blah|' blah` on both a linux and mac computer

@kevinb
2017-01-06 19:32:22 -08:00
Emily Eisenberg
4d2e46e7f6 Remove trailing commas for IE 9 compatibility
Summary: IE 9 doesn't like trailing commas. When we introduced eslint, we added
a bunch of trailing commas, which makes IE 9 sad.

Test Plan:
 - `make lint`
 - Visit http://localhost:7936/ using IE 9 on browserstack.
 - See that the math loads, and there are no errors in the F12 developer tools.

@kevinb
2017-01-06 19:06:01 -08:00
Kevin Barabash
74d55ee0ea Merge pull request #598 from gagern/screenshotter
Load full-window page before adjusting browser window size
2017-01-05 09:17:17 -05:00
Erik Demaine
1ca7363460 \ldots in text mode; add \textellipsis & \mathellipsis (#597) 2017-01-03 16:47:53 +01:00
Martin von Gagern
ca1a511cc4 Load full-window page before adjusting browser window size
Experimenting with selenium-webdriver@3.0.1 and Firefox 50.1.0 I observed
screenshots having a height of merely 8 pixels.  Presumably the margin or
padding of an otherwise empty document.  So in order to get the actual size
of the document area, the screenshotter now loads a document which fills the
entire viewport.
2017-01-03 15:53:25 +01:00
Eddie Kohler
7c83021009 Support \bmod, \pmod, \pod, and \mod. 2016-12-31 20:51:20 -05:00
Kevin Barabash
f742fbf9f2 Merge pull request #580 from kohler/muunits
Support "mu"-denominated sizes and \mkern
2016-12-19 14:31:04 -07:00
Eddie Kohler
81bc24808b Support \mkern as an alias for \kern. 2016-12-19 13:43:33 -05:00
Eddie Kohler
d5cedc55c9 Support "mu" units for sizes.
18mu is 1em.

And use emPerEx when converting ex to em, rather than xHeight.
(Previously some places used emPerEx and others used xHeight.)
2016-12-19 13:42:27 -05:00
Kevin Barabash
3900936f63 Merge pull request #586 from tabatkins/patch-1
Misspelled variable "candiate"
2016-12-16 18:25:32 -05:00
Tab Atkins Jr
d43f24ea91 Misspelled variable "candiate"
All uses have the same misspelling, so this wasn't a functionality bug, but still better to have things spelled correctly in the source. ^_^
2016-12-16 13:12:45 -08:00
Kevin Barabash
fdebbe3a18 Merge pull request #579 from kohler/kernarg
Allow unbraced kerns, such as \kern1em.
2016-12-08 23:38:04 -05:00
Kevin Barabash
831c5b8d99 Merge pull request #578 from kohler/middle
Handle \middle.
2016-12-08 23:35:36 -05:00
Eddie Kohler
530ec97e74 Allow unbraced kerns, such as \kern1em.
This is actually the *only* syntax TeX allows; braced kern units
are invalid.
2016-12-08 23:26:31 -05:00
Kevin Barabash
95f2f46bf5 Merge pull request #581 from kohler/stylespacing
BUG FIX: Correct spacing next to symbols when styles switch.
2016-12-08 22:56:23 -05:00
Eddie Kohler
e449b2d61a Handle \middle. 2016-12-08 16:41:19 -05:00
Eddie Kohler
72362ecf88 Correct spacing next to symbols when styles switch.
Two nodes were sharing a list of classes, which led to the style
change applying multiple times. Fix that.
2016-12-08 16:20:32 -05:00
Kevin Barabash
7433638fda Merge pull request #573 from kohler/kwork
Fix atom-spacing, add text-mode fonts, and miscellany
2016-12-05 16:56:00 -05:00
Eddie Kohler
81f9a410a0 Add text-mode definitions of symbols like \$. 2016-12-05 16:05:39 -05:00
Eddie Kohler
e1c5f5db1c Add support for \text{rm,it,bf,tt,sf,normal}.
And allow \text to nest inside \text.
2016-12-05 16:05:22 -05:00
Eddie Kohler
576380c11c Text mode: Combine adjacent spans when possible for cleaner HTML.
So `\text{Hi}` becomes one <span...>Hi</span>, rather than two
<span...>H</span><span...>i</span>.

This allows the font renderer to apply kerning, which changes some
test output.
2016-12-05 15:59:10 -05:00
Eddie Kohler
b88bc7723d Italic correction should not be applied to characters in text mode.
This requires changes to screenshotter tests that assumed italic
correction was applied in text mode.
2016-12-05 15:59:10 -05:00
Eddie Kohler
4297eb1276 Unicode punctuation works. 2016-12-05 15:59:05 -05:00
Eddie Kohler
e0407a19a0 Support \atop.
Add Jasmine test and update screenshotter test.
2016-12-05 15:58:26 -05:00
Eddie Kohler
f8e0c91de4 Support \stackrel (#468). 2016-12-05 15:48:05 -05:00
Eddie Kohler
6bb62b11b4 Support \mathop, \mathrel, \mathbin, \mathpunct, etc. class commands.
These commands set their arguments in a given TeX math class. Use
the existing "op" type for \mathop (to support \limits); introduce
a new "mclass" type for the other classes.

Fixes #482. Tests borrowed from #485 (cbreeden).
2016-12-05 15:48:05 -05:00
Eddie Kohler
982e7be597 Calculate math classes from outputs, not inputs.
It's important to get spacing right that the domTree classes reflect
math atom types. So use those types exclusively, rather than
repeating the type mapping twice (once when building spans, once in
getTypeOfGroup).

* Remove getTypeOfGroup.
* Add getTypeOfDomTree (simpler).
* Adjust supsub type calculation.
* Adjust delimsizing internals.
2016-12-05 15:48:05 -05:00
Eddie Kohler
dc79b91dbf Mark delimiters with the proper class (mopen or mclose).
This fixes some atom spacing issues. Update the BinCancellation
screenshotter test.
2016-12-05 15:48:04 -05:00
Eddie Kohler
1a99e7a2e9 The \llap and \rlap commands always generate "mord" atoms.
This is how TeX does it.
2016-12-05 15:48:04 -05:00
Eddie Kohler
c951df4269 Fix #4.
Post-process the list of atoms after they are created, changing
binary operators to ordinary atoms according to the TeXbook's
rules. This makes the `prev` argument redundant, so drop it.

This commit assumes that the math class (mop/mbin/mrel/etc.) comes
first in the `classes` list, if present. Add a TODO to change the
signature of `makeSpan/makeSymbol` to enforce this invariant.
2016-12-05 15:47:06 -05:00
Eddie Kohler
9d3cdf694c Fix spacing between groups to match TeX spacing (#567)
* Internal: Pass full `options` objects to makeSpan/makeSymbol.

Not just the current color. This will facilitate applying options
to built nodes in a standardized way, rather than changing all
callsites.

* Add style switching test: text and scriptstyle in the same group.

* Apply style-specific spacing using different CSS coding.

Specifically, infer style from a class on the *current* element,
rather than the parent element. Use "mtight" class to denote elements
with tight spacing (scriptstyle or scriptscriptstyle). Apply that
class automatically based on options.

* Fix #533, #534, #541.

- #534: Implement getTypeOfGroup for font groups.
- #533, #541: Improve the ways spaces are applied to lists. Since
  CSS adjacency implements mathematical spacing, it's incorrect to
  introduce "convenience spans" for spaces and display changes into
  the generated HTML -- those spans break adjacency. Apply display
  changes directly, and shift space spans into adjacent atoms.

Requires updates to two screenshotter tests, LimitControls and
SupSubLeftAlignReset. The new results for these tests are closer
to TeX output than the old results.

Also requires updates to Jasmine tests, since those assumed output
structures that have changed.

* Fix #136: Size commands generate fragments, not spans.

This is so the size commands don't hide the types of their enclosed
atoms. Addresses #136.

This slightly changes the vertical position of the Sizing test. Not
sure the vertical position matters, so change the test.
2016-11-28 12:55:54 -05:00
Martin von Gagern
be96695586 Improved Docker for Mac support (#568)
* Exit cleanly after invalid screenshot test name

This avoids waiting forever.

* Auto-detect host IP in Docker for Mac environment

This checks all available network addresses to find one which the Selenium
container can connect to.  That way we don't have to analyze network
settings or similar to figure out the main public IP address of the machine.

* Make server less sensitive to current working directory

That way it becomes possible to run screenshotter.js from within the
Screenshotter directory, and still server all files as intended.
2016-11-27 18:57:53 -05:00
Martin von Gagern
94dad8029d Check out test fonts in screenshotter.sh (#570)
This ensures that running screenshots as indicated by the screenshotter
readme will correctly render the Unicode examples, even if not run on
Travis.  It also fixes the commit ID of the unicode fonts.

One alternative would have been using a git submodule.  But as many tools
will initialize submodules automatically, that would mean big downloads for
anyone using such a tool to clone KaTeX.

Another alternative would be tweaking the server to fetch the files on
demand if the local copy is unavailable.  But that would cause additional
network overhead, so grabbing the files just once should be simpler.
2016-11-23 09:38:47 -05:00
Eddie Kohler
7b5580b1fa Screenshotter: Obey --katex/selenium-ip options under --container. (#566)
Some Docker configs, such as Docker for Mac, need explicit --katex-ip
and --selenium-ip options. Obey those options.
2016-11-22 11:23:32 +01:00
Kevin Barabash
22957d40f6 Use style specific FONTDIM metrics (#545)
Summary:
FONTDIM metrics include metrics like sup2, sup3, etc. which are used for
position sub/super-scripts, fractions, delimiters, etc.  TeX uses three
different font styles: textfont2 (DISPLAY & TEXT), scriptfont2 (SCRIPT), and
scriptscriptfont2 (SCRIPTSCRIPT) and has different sets of metrics for each.
This diff adds style specific metrics for better TeX compliance.

Notable squashed commits:
- Recreated screenshots (martin)
- fix getEmPerEx to use getXHeight
- regularize how we access options.style, remove unnecessary newlines
- use var style = options.style in more places in buildHTML
2016-11-04 07:45:08 +01:00
Matthew Rothenberg
ace67541a0 Create CODE_OF_CONDUCT.md (#553) 2016-11-02 22:02:47 -04:00
Martin von Gagern
0ebbc25672 Add build artifacts to .gitignore and allow installing dependencies without building KaTeX
* Let git ignore .npm-install.stamp and dist

The former is created for most makefile targets after dependencies have been
retrieved.  The latter is created by typical operations like “make” without
arguments or “npm install”.  Having these around is to be expected.

Adding this to .gitignore should NOT affect npm packaging, since that is
based on a whitelist in package.json which does mention dist.

* Allow installing dependencies without actually building KaTeX

We have been using “npm install” to install dependencies, but since that
also does build KaTeX itself, it may fail if e.g. there are any style guide
violations.  Now we only fetch dependencies but do not build KaTeX itself.

The make conditionals used here are not part of POSIX make but a GNU
extension.  But we already use functionality not mandated by POSIX (namely
many of the functions like “wildcard”), so this should not make portability
any worse than it already is.
2016-11-02 21:24:59 -04:00
Emily Eisenberg
fef5b88057 Merge pull request #548 from deckar01/547-unminified-assets
Keep unminified katex.js and katex.css build files
2016-11-01 12:08:29 -07:00
Martin von Gagern
d5025c61e8 Fix texcmp issues related to bit depth and kerning (#549)
* Ensure bit depth 8
* Print affected file if PNG failed to read (e.g. due to wrong bit depth)
* Disable running Kern test case through TeX as doing so fails
2016-10-22 19:37:18 -04:00
Jared Deckard
4fb9445a92 Keep unminified katex.js and katex.css build files 2016-10-10 18:28:34 -05:00
Martin von Gagern
d50745d5a9 Make screenshotter work with docker-machine (#494)
Since boot2docker has been superseded by docker-machine, we need this to
support developers on OS X.  The changes to the bash script ensure that we
clean up our containers even if taking screenshots gets hung and requires a
keyboard interrupt, as happens if the IP addresses were guessed incorrectly.
2016-10-07 23:15:54 -04:00
Janis Lesinskis
f1be1a3462 Invalid input error message (#540)
* Added check for type of expressions passed to parseTree function
* Added tests for bad input raising exception
* Added test for supported types NOT throwing exception
* Added test case for parser taking String objects
2016-09-23 14:45:33 +02:00
Mathijs Henquet
a16ae7a5eb Ignore `font' groups when determining base elements, fixes #390 (#469)
* Ignore `font' groups when determining base elements, fixes #390

* fix ===

* Added testcase
2016-09-17 17:22:01 -07:00
Viktor Qvarfordt
e4d2d933af Provide link to auto-render.min.js in usage example (#522) 2016-09-17 15:23:26 -07:00
Martin von Gagern
e6de31d2d6 Call “npm install” automatically if package.json changed (#505)
This adds a stamp file which is used to detect whether the `package.json`
file got updated since the last `npm install`.  If so, `npm install` is run
again to update all modules to the version described in `package.json`.
This happens as a dependency of only those modules which actually need some
npm-installed module.

Setting the corresponding make variable to the empty string disables the
feature, which is used by the `make` invocation in the `prepublish` script
inside `package.json` to avoid infinite loops.  It can also be used by
developers working in an environment with reduced connectivity, as long as
they know what they are doing.
2016-08-23 18:05:50 -07:00
Kevin Barabash
ec62ec39d8 Add support for Latin-1, Cyrillic, and CJK characters inside \text{} (#508)
Summary:
This diff provides support for Latin-1, Cyrillic, and CJK characters
inside \text{} groups.  For Latin-1 and Cyrillic characters we use
glyph metrics from a glyph from Basic Latin that has roughly the same
bounding box.  We use the metrics for a capital 'M' to approximate the
full-width CJK characters.  Half-width characters are not supported yet.

Test Plan:
- make test
- make screenshots

Reviewers: emily
2016-08-01 17:51:40 -07:00
Martin von Gagern
92bbbffbc8 Add --wait option to screenshotter (#510)
Sometimes a page might not be ready when the screenshot is taken, due to
fonts still being loaded or some such.  In --verify mode this is taken care
of by repeated attempts.  But when creating a screenshot for the first time,
it might be desirable to wait a given number of seconds for things to settle
down.  So this commit introduces a --wait option to the screenshotter.
2016-07-29 12:26:30 -07:00
Charles Marsh
725524a214 Specify TrueType font format as 'truetype' rather than 'ttf'
Summary:
I've been experimenting with delivering a TrueType-only KaTeX build for our mobile apps. I couldn't
get anything to render properly until I flipped the `format` specifier from `ttf` to `truetype`.
Though I can't find a definitive source on this, all the examples I've seen online use `truetype`
over `ttf`, and flipping from `ttf` to `truetype` fixed my own issues.

It's unlikely that this has been much of a problem in practice, since so many browsers now support
WOFF (I'm considering a TrueType-only build since it's the least common denominator across Android
4.3+ and mobile Safari).

Test Plan:
- To force TrueType rendering, comment out the `.use-eot`, `.use-woff2`, and `.use-woff` lines in `fonts.less`.
- Run `make serve`.
- Open up the demo page.
    - Verify that the demo text is rendered as before (only the parens are noticeably different in the demo?).

Reviewers: kevinb, emily

Reviewed By: emily

Subscribers: benkomalo

Differential Revision: https://phabricator.khanacademy.org/D29274
2016-07-25 10:28:34 -07:00
Martin von Gagern
4a9c2acbf7 Add some more symbols (#502)
This adds support for the following input sequences:

    -- --- ` ' `` '' \degree \pounds \maltese

resulting in – — ‘ ’ “ ” ° £ ✠ symbols already present in our fonts.

As part of this modification, the recognition of multiple dashes was moved
from the lexer to the parser.
This is neccessary since in math mode a sequence of hyphens is just a
sequence of minus signs.  Just like a pair of apostrophes in math mode is a
double prime not a right double quotation mark.
To make this easier, parseGroup and parseOptionalGroup have been merged.
2016-07-24 19:56:31 -07:00
Martin von Gagern
befe1c1af7 Upgrade less to version 2 (#503)
This fixes a problem where the graceful-js dependency of the old less
version is incompatible with node 7, and will cause warnings on node 6.
The resulting katex.min.css is exactly the same as with the previous
version, so the major version upgrade does not seem to affect the outcome.

The less.Parser has been deprecated, and less.render is the supported
approach now, so we have to switch the development server to that.
2016-07-12 12:23:28 -07:00
Martin von Gagern
8c55aed39a Allow macro definitions in settings (#493)
* Introduce MacroExpander

The job of the MacroExpander is turning a stream of possibly expandable
tokens, as obtained from the Lexer, into a stream of non-expandable tokens
(in KaTeX, even though they may well be expandable in TeX) which can be
processed by the Parser.  The challenge here is that we don't have
mode-specific lexer implementations any more, so we need to do everything on
the token level, including reassembly of sizes and colors.

* Make macros available in development server

Now one can specify macro definitions like \foo=bar as part of the query
string and use these macros in the formula being typeset.

* Add tests for macro expansions

* Handle end of input in special groups

This avoids an infinite loop if input ends prematurely.

* Simplify parseSpecialGroup

The parseSpecialGroup methos now returns a single token spanning the whole
special group, and leaves matching that string against a suitable regular
expression to whoever is calling the method.  Suggested by @cbreeden.

* Incorporate review suggestions

Add improvements suggested by Kevin Barabash during review.

* Input range sanity checks

Ensure that both tokens of a token range come from the same lexer,
and that the range has a non-negative length.

* Improved wording of two comments
2016-07-08 12:24:31 -07:00
Shadaj Laddad
b49eee4de7 Updated Khan Academy colors to new colors
Summary: The KaTex renderer used to use old Khan Academy colors when displaying colored text. The configuration is now updated to have the new colors.

Test Plan:
- verified that colored text now uses the new colors in a browser
- running commands such as `\blueA{blueA}` will style the text in the blueA color from the configuration

Reviewers: emily, kevinb

Reviewed By: kevinb

Differential Revision: https://phabricator.khanacademy.org/D27963
2016-06-07 17:24:47 -07:00
Emily Eisenberg
26f06754e7 Merge pull request #490 from cbreeden/Fix#488-supsub-alignment
Fix #488 Subsup alignments
2016-06-01 15:10:50 -07:00
Christopher Breeden
3668bb084e add msupsub tag to scripts, fix #488 2016-06-01 16:57:56 -05:00
Andrey Mikhaylov (lolmaus)
b120b1c3d8 Fixed indentation in auto-render readme (#481) 2016-05-26 10:10:54 -07:00
Johannes Schmitz
1cf10c4cd6 Update CDN links to latest version (#478) 2016-05-23 11:42:27 -07:00
Kevin Barabash
9884ac3e8d Merge pull request #423 from Khan/kern
Add support for \kern
2016-04-19 21:22:24 -07:00
Emily Eisenberg
991f4eb096 Ignore release.sh in bower.json
Summary: We didn't have a rule for ignoring .sh files in the bower.json,
so it got included in the 0.6.0 release. Oops! This adds it to the
ignore file.

Test Plan:
 - ???

Auditors: kevinb
2016-04-15 18:48:24 -07:00
Emily Eisenberg
b62e1cf314 Make the release script correctly link to the new release page.
Test Plan:
 - Cross fingers!

Auditors: kevinb
2016-04-15 18:30:31 -07:00
Kevin Barabash
5991a6078b Bump master to v0.7.0-pre 2016-04-15 18:18:36 -07:00
Kevin Barabash
aa36c459ae don't do any linting/checking of the release commits
Summary:
We prevent any precommit checks so that we don't have to include
a "Test Plan" etc. in the release commit message.

Test Plan:
- ./release.sh 0.6.0 0.7.0

Auditors: emily
2016-04-15 18:14:13 -07:00
Kevin Barabash
0f7a1a06e9 Blacklist dist/** so that ka-lint doesn't barf on those files
Test Plan:
- ./release.sh 0.6.0 0.7.0

Auditors: emily
2016-04-15 18:11:31 -07:00
Kevin Barabash
36164b98ea Make it compatible with Mac
Test Plan:
- ./release 0.6.0 0.7.0

Auditors: emily
2016-04-15 18:09:14 -07:00
Kevin Barabash
363a6ba311 Merge pull request #461 from Khan/add-release
Add a release script.
2016-04-15 17:33:35 -07:00
Emily Eisenberg
224efafda8 Add a release script.
Summary: Make the release script that's been floating around in my gists
for a while into a real script.

Test Plan:
 - Comment out all the scary bits (the two pushes, and `npm
   publish`), and run:
 - `./release.sh 0.6.0`
 - Checkout the `v0.6.0` tag, see that it successfully modified
   package.json and bower.json to version 0.6.0, and built and committed
   the rest of the files.
 - `git tag -d v0.6.0`
 - `./release.sh 0.6.0 0.7.0`
 - Checkout master, see that it successfully made a commit bumping
   package.json and bower.json to 0.7.0-pre.

Reviewers: @kevinbarabash
2016-04-15 12:19:16 -07:00
Emily Eisenberg
ccd8f40028 Make make lint not automatically fix lint
Summary: Looks like there was lint, but `make lint` wasn't failing on it
because it would just automatically fix it! This removes the `--fix`
from `eslint` and fixes the lint.

Test Plan:
 - `make lint`

Auditors: kevinb
2016-04-15 12:08:14 -07:00
Ben Alpert
965b8a6164 Add \underline support (#456)
![image](https://cloud.githubusercontent.com/assets/6820/14412284/e63218b6-ff13-11e5-912c-fc6e30928b93.png)

Fixes #209.
2016-04-10 20:32:24 -07:00
Kevin Barabash
a33fa4fd64 Merge pull request #448 from Khan/fix-centered-subsuperscripts
Fix sub- and super-scripts not being centered inside of display math
2016-03-07 00:07:25 -08:00
Emily Eisenberg
229ce562ff Fix sub- and super-scripts not being centered inside of display math
It looks like the `text-align: center` is affecting the text in sub and
superscripts. Fixes #447

Test Plan:
 - Visit [this
   example](http://localhost:7936/?text=x%5El_%7Bi%5E%7Bl%2B1%7D%2Bi%2C%20j%5E%7Bl%2B1%7D%2Bj%2C%20d%7D)
 - Edit the HTML to add `<span class="katex-display">...</span>` around the
   `<span class="katex">` node.
 - See that the sub and superscripts are left-aligned, not centered

(It looks like we don't have a way to test this in the screenshotter for now)

@kevinb
2016-03-05 09:37:39 -08:00
Ben Alpert
d2079a6c9c Merge pull request #444 from mathiasbynens/https
README: Use HTTPS where possible
2016-03-02 11:35:47 -08:00
Mathias Bynens
d5359ef5bb README: Use HTTPS where possible
There’s no reason not to always use HTTPS. See https://github.com/konklone/cdns-to-https#background for more info.
2016-03-01 15:44:45 +01:00
Ben Alpert
83dea37b51 Merge pull request #436 from tirsen/master
Bundle css and fonts in npm package.
2016-02-19 11:23:35 -08:00
Jon Tirsen
a5207fef94 Bundle css and fonts in npm package. 2016-02-18 09:21:28 +01:00
Kevin Barabash
3083efba66 Add support for \kern
Summary:
This only supports em and ex units and doesn't handle vertical layouts.
Negative kerning works.

Test Plan:
- make test
- make screenshots (verify that d is slightly overlapping c in the screenshots)

Reviewers: emily
2015-12-30 18:14:29 -08:00
Kevin Barabash
c79fb58936 Revert previous change to make \centerdot like \cdot
Summary:
Alpert alerted me to the fact that \centerdot and \cdot are
not the same despite what MathJax thinks.

Test Plan:
- make serve
- load http://localhost:7936/
- see the `a \centerdot b` produces a small, bottom-aligned square

Auditors: alpert emily
2015-12-29 10:21:44 -08:00
Kevin Barabash
157bfb0cf5 \centerdot should produce the same glyph as \cdot
Summary:
Update the symbol definition for \centerdot so that it does
the same thing as \cdot.
Fixes https://github.com/Khan/KaTeX/issues/421.

Test Plan:
- make serve
- open http://localhost:7936/
- verify that `a \centerdot b` looks the same as `a \cdot b`

Auditors: emily
2015-12-29 10:00:17 -08:00
Kevin Barabash
9ad50178f1 Merge pull request #405 from Khan/eslint
Migrate to eslint
2015-12-04 17:42:06 -08:00
Kevin Barabash
14a58adb90 Migrate to eslint
Summary
We'd like contributors to use the same linter and lint rules that we use
internally.  This diff swaps out eslint for jshint and fixes all lint failures
except for the max-len failures in the test suites.

Test Plan:
- ka-lint src
- make lint
- make test

Reviewers: emily
2015-12-01 10:02:08 -08:00
Kevin Barabash
1a082e81d9 Merge pull request #406 from gagern/ffEmptyPage
Force Firefox to start up with an empty page
2015-12-01 08:17:47 -08:00
Martin von Gagern
0a53a775e8 Force Firefox to start up with an empty page
Otherwise it could happen that some Mozilla page gets shown which has a
minimal size larger than the 786px we're requesting.  And the screenshot
will span that entire page even if the window is smaller, resulting in a
failure to adjust screenshot size.

See http://kb.mozillazine.org/Browser.startup.homepage_override.mstone
and http://kb.mozillazine.org/Browser.startup.page for details.

Just in case, we also include the docker image digests in the travis build
log, to increase chances of reproducing what we get there.
2015-12-01 14:15:45 +01:00
Kevin Barabash
7cdb08bf7e Merge pull request #388 from gagern/updateSelenium
Update Selenium Docker images from 2.46.0 to 2.48.2
2015-11-30 21:19:29 -08:00
Kevin Barabash
3a15a8402f Merge pull request #398 from gagern/aligned
Introduce an aligned environment
2015-11-28 11:20:04 -08:00
Martin von Gagern
8201501d77 Introduce an aligned environment
This is almost like the align* environment, but it starts out in math mode,
so we don't have to worry about the fact that we have no real surrounding
text mode in KaTeX.  This is the first step towards align* and align.
2015-11-23 20:22:06 +01:00
Kevin Barabash
b9c4237ac2 Merge pull request #386 from gagern/nextToken
Avoid re-lexing, move position to internal state
2015-11-23 08:55:11 -08:00
Martin von Gagern
4debcb34af Avoid re-lexing, move position to internal state
Instead of passing around the current position as an argument, we now have a
parser property called pos to keep track of that.  Instead of repeatedly
re-lexing at the current position we now have a property called nextToken
which contains the token beginning at the current position.  We may need to
re-lex if we switch mode.  Since the position is kept in the parser state,
we don't need to return it from parsing methods, which obsoletes the
ParseResult class.
2015-11-23 17:40:56 +01:00
Kevin Barabash
5f275aa9c1 Merge pull request #385 from gagern/testErrors
Added unit tests for error messages
2015-11-21 08:54:50 -08:00
Martin von Gagern
e7195601e1 Added unit tests for error messages
This is an attempt to actually exercise all the code paths which can lead to
a ParserError exception (from malformed user input, without tinkering with
any KaTeX internals or exploiting a KaTeX bug).  It documents the current
state of affairs, without changing any error messages.  Comments indicate
future work, particularly with respect to the position often associated with
these error messages.
2015-11-21 15:38:26 +01:00
Kevin Barabash
4617f191e3 Merge pull request #389 from gagern/jasmine
Drop local copy of jasmine, multiple spec files in browser tests
2015-11-20 13:49:37 -08:00
Martin von Gagern
cc17f36edc Drop local copy of jasmine, multiple spec files in browser tests
Instead of having our own copy of jasmine in the repository, we use
jasmine-core as an npm dependency and load it from there.  That reduces the
size of the repository and helps keeping up to date.  We're not using the
transitive dependency on jasmine-core via jasmine, since the jasmine package
might change its dependency any day (although unlikely).

The katex-spec.js shipped from the server now includes all
`test/*[Ss]pec.js` (as matched via glob) so that additional spec files can
be created and will automatically get included in the browser-side test
suite.  The contrib specs are not included at this point.

Visit http://0.0.0.0:7936/test/test.html while running server.js to see this
in action and verify the lack of failures.
2015-11-12 18:34:31 +01:00
Kevin Barabash
bfb3827df1 Merge pull request #387 from gagern/jasmine
Switch from jasmine-node to jasmine itself
2015-11-12 08:56:06 -08:00
Kevin Barabash
1c50688cba Merge pull request #371 from stared/patch-1
Readme - set recent version (0.5.1) for CDN links
2015-11-12 08:16:30 -08:00
Martin von Gagern
4792dec8e5 Update Selenium Docker images from 2.46.0 to 2.48.2 2015-11-12 17:08:16 +01:00
Martin von Gagern
92034c17f9 Switch from jasmine-node to jasmine itself
Jasmine supports node these days, so there is no longer a need to use a
separate (and unmaintained) package to provide such bindings.

Making the switch exposed several misuses of the `toMatch` assertion in the
existing specification.  Most of them were converted to `toEqual`, since
`toMatch` is only for matching against regular expressions.
2015-11-10 12:38:49 +01:00
Kevin Barabash
21a26b807c Merge pull request #381 from gagern/travisScreenshots
Check screenshots on Travis using Docker and Selenium
2015-11-07 11:04:24 -08:00
Martin von Gagern
9fb04dbb2e Add verification mode to screenshotter
Now Travis can run the screenshotter in verification mode.  The files in the
repository will be seen as the expected outcome, and if the actual result
differs from that, it might be attempted four more times before the test
case is actually deemed failed.  A timeout between page load and screenshot
should allow any possible font issues to settle down.
2015-11-07 16:32:02 +01:00
Martin von Gagern
5ecbcf6808 Check screenshots on Travis using Docker and Selenium
Thanks to the docker service provided by Travis CI, we should be able to
download and use the Selenium docker images in order to run our
screenshotter and check whether all the screenshots match the images from
the repository.
2015-11-06 13:02:53 +01:00
Kevin Barabash
c20b8f8456 Merge pull request #369 from utensil/master
Add a link to symbol/function support preview page
2015-10-20 08:21:12 -07:00
utensil
dcc3214527 Add a link to symbol/function support preview page 2015-10-20 16:25:02 +08:00
Kevin Barabash
c6800749ab Merge pull request #372 from Newman101/Issue370
Resolved Issue #370
2015-10-19 10:29:26 -07:00
newman101
f039068fae Updated link label 2015-10-19 18:18:18 +01:00
newman101
bc4693a71b Resolved Issue #370 2015-10-19 16:57:04 +01:00
Piotr Migdał
69130d6a19 Readme - set recent version (0.5.1) for CDN links 2015-10-19 16:53:05 +02:00
Kevin Barabash
d87ee4f78f Merge pull request #366 from Khan/add_width_to_font_metrics
Add a build step to generate extended metrics that additionally contain glyph widths
2015-10-16 14:41:31 -07:00
Kevin Barabash
1573e1eed6 Add a build step to generate extended metrics that additionally contain glyph widths
Summary:
The ability to use pre-determined character widths will benefit alternative
layout engines such as gagern's canvas layout engine.  I would also like to
experiment would using CSS transforms to absolutely position each glyph.  This
diff adds a new make rule, make extended_metrics, which generates metrics that
also containing glyph widths.

Test Plan:
- run `make extended_metrics`
- verify that fontMetricsData.js contains entries with 5 numbers instead of 4

Reviewers: emily alpert
2015-10-16 14:31:08 -07:00
Kevin Barabash
ee88cc3c11 Merge pull request #363 from gagern/modeStateful
Make mode part of the internal state of the parser
2015-10-05 16:36:05 +00:00
Kevin Barabash
9aab9c1efe Merge pull request #361 from crepererum/feature/gt_lt
Add \gt and \lt
2015-10-05 16:32:49 +00:00
Marco Neumann
95e568ed6b Add \gt and \lt 2015-10-05 08:34:03 +02:00
Martin von Gagern
cabc08598b Make mode part of the internal state of the parser
This is the first step towards #266.
2015-10-04 20:35:43 +02:00
Kevin Barabash
e9f4b07611 Merge pull request #364 from gagern/lexerRewrite
Refactor lexer, avoiding some mode-specific distinctions
2015-10-03 13:31:51 -07:00
Martin von Gagern
d423bec089 Rewrote lexer, avoiding some mode-specific distinctions
There are two main motivations for this commit.  One is unicode input, which
requires unicode characters to get past the lexer.  See discussion in #261.
The second is in preparation for #266, where we'd deal with one token of
look-ahead but might be lexing that token in an unknown mode in some cases.
The unit test shipped with this commit addresses the latter concern, since
it checks that a math-mode-only token may immediately follow some text mode
content group.

In this new implementation, all the various things that could get matched
have been collected into a single regular expression.  The hope is that
this will be beneficial for performance and keep the code simpler.
The code was written with Unicode input in mind, including non-BMP codepoints.

The role of the lexer as a gate keeper, keeping out invalid TeX syntax, has
been abandoned.  That role is still fulfilled by the symbols and functions
tables, though, since any input which is neither a symbol nor a command is
still considered invalid input, even though it lexes successfully.
2015-10-02 20:06:03 +02:00
Kevin Barabash
95e2f1c8d7 Merge pull request #356 from gagern/callingConvention
New calling convention for functions and environments
2015-10-01 08:42:52 -07:00
Martin von Gagern
30f7a1c5bf New calling convention for functions and environments
Fixes issue #255.

Mixing the variable number of arguments a function receives from TeX code
with the fixed arguments which the parser provides can cause some confusion.
After this change, a handler will receive exactly two arguments: one is a
context object from which things provided by the parser can be accessed by
name, which allows for simple extensions in the future.  The other is the
list of TeX arguments, passed as an array.

If we ever switch to EcmaScript 2015, we might want to use its destructuring
features to name the elements of the args array in the function head.  Until
then, destructuring that array manually immediately at the beginning of the
function seems like a useful convention to easily find the meaning of these
arguments.
2015-10-01 13:15:44 +02:00
Kevin Barabash
a81c4fe78d Merge pull request #362 from Khan/array_font_metrics
Use an array of values instead of object literals to save space in fontMetricsData.js
2015-09-30 10:00:23 -07:00
Kevin Barabash
6a10237017 Use an array of values instead of object literals to save space in fontMetricsData.js
Test Plan: make test

Reviewers: emily
2015-09-29 22:16:03 -07:00
Kevin Barabash
fdbdb28617 Merge pull request #346 from gagern/splitSymbols
Reformat symbol table
2015-09-25 14:56:18 -07:00
Kevin Barabash
f25829df58 Redirect people to gitter instead of freenode 2015-09-25 14:35:23 -07:00
Kevin Barabash
8e54d6e365 Merge pull request #360 from gitter-badger/gitter-badge-1
Add a Gitter chat badge to README.md
2015-09-25 14:32:23 -07:00
The Gitter Badger
b9b8396b6f Add Gitter badge 2015-09-25 21:28:31 +00:00
Kevin Barabash
3203959b96 Merge pull request #352 from Khan/fix_font_greediness
Set the greedines of font functions to 2 so that e^\mathbf{x} will parse
2015-09-25 14:20:23 -07:00
Kevin Barabash
b98670b8ed Set the greedines of font functions to 2 so that e^\mathbf{x} will parse
Test Plan: make test

Reviewers: emily
2015-09-25 14:17:23 -07:00
Emily Eisenberg
961e1caba9 Merge pull request #359 from xymostech/fix-zero-rule-padding
Remove 3px border around rules.
2015-09-22 13:16:18 -07:00
Emily Eisenberg
252f6320ef Remove 3px border around rules.
Summary: For some reason, adding `border-style: solid` also adds a 3px
border around elements, which means that all of the rules that we
created are 3px too large. This sets the default size to 0 for all the
edges, which makes them correct.

Test plan:
 - See that `a\rule{0em}{0em}b` produces no visible rule.
 - See the new screenshots look reasonable.
2015-09-22 10:21:33 -07:00
Kevin Barabash
d7d1367558 Merge pull request #347 from gagern/splitEnvironments
Split up environments list into calls to defineEnvironment
2015-09-14 22:51:04 -06:00
Kevin Barabash
0fc77e2f40 Merge pull request #345 from gagern/splitFunctions
Split up functions list into calls to defineFunction
2015-09-14 22:44:21 -06:00
Kevin Barabash
112e6783b3 Merge pull request #349 from gagern/texcmpLogo
Make texcmp work again, including KaTeX logo
2015-09-11 11:43:28 -07:00
Martin von Gagern
2e0f11bdfa Reindent some comment code as well 2015-09-11 09:27:47 +02:00
Kevin Barabash
89e0d7950c Merge pull request #348 from gagern/splitBuildHandlers
Split groupType literals in buildHTML and buildMathML
2015-09-10 22:36:13 -07:00
Martin von Gagern
71881e3d36 Make texcmp work again, including KaTeX logo
Since all the math font test cases use the KaTeX logo, we need that.
I started with the definition of the logo from katex.less, but tweaked that
until it gave a good visual match, in particular a very similar logo width,
no matter the actual numbers.

With that logo, most tests can be compiled again, with the exception of the
one containing illegal functions to test visual error reporting.
That one needs to be explicitely disabled.
2015-09-10 16:05:23 +02:00
Martin von Gagern
d809f9c362 Reindent groupType definitions in buildHTML and buildMathML
Since the previous commit deliberately avoided reindenting, this one here
does just that: reindenting the existing code.  There are no other changes.
2015-09-10 11:47:47 +02:00
Martin von Gagern
6bc7cd574f Split up groupType map in buildHTML and buildMathML code
Having long object literals containing the code is problematic.
It makes it difficult to add auxiliary functions or data close to the
function inside the map where it is needed.
Building the map in several steps, repeating the map name at each step,
avoids that problem since it makes the definitions independent from one
another, so anything can go between them.

This commit deliberately avoided reindenting existing code to match the new
surroundings.  That way it is easier to see where actual changes happen,
even when not performing a whitespace-ignoring diff.
2015-09-10 11:34:34 +02:00
Martin von Gagern
dae3a14744 Reindent environments
Since the previous commit deliberately avoided reindenting, this one here
does just that: reindenting the existing code.
2015-09-10 11:18:11 +02:00
Martin von Gagern
2a31a719ec Split up environments list into calls to defineEnvironment
Having one long array literal to contain the code of all environment
implementations is problematic.  It makes it difficult to add auxiliary
functions or data close to the function inside the list where it is needed.

Now the functions are no longer defined using such a literal, but instead
using calls to a "defineEnvironment" function which receives all the
relevant data.  Since each function call is independent from the others,
anything can go in between.

This commit deliberately avoided reindenting existing code to match the new
surroundings.  That way it is easier to see where actual changes happen,
even when not performing a whitespace-ignoring diff.
2015-09-10 11:14:00 +02:00
Martin von Gagern
acfdc9f698 Rename declareFunction to defineFunction
https://github.com/Khan/KaTeX/pull/262#issuecomment-113981142 indicated a
preference for define over declare.
2015-09-10 10:23:58 +02:00
Martin von Gagern
414f9dd248 Reformat symbol table
Using function calls instead of one big object literal for the symbols makes
the notation far more concise and readable.  Having the actual symbol name
in the last position helps aligning the preceding columns, making the list
easier to read.

Another benefit is that all symbol definitions now pass through a single
function, where additional processing (e.g. for Unicode input) might take
place in a future commit.
2015-09-10 10:22:42 +02:00
Martin von Gagern
3e055f84e9 Reindent
Since the previous commit deliberately avoided reindenting, this one here
does just that: reindenting the existing code.  There are no other changes.
Notice how the new indentation leaves more room to function handlers.
2015-09-10 09:52:00 +02:00
Martin von Gagern
d553353204 Split up functions list into calls to declareFunction
Having one long array literal to contain the code of all function
implementations is problematic.  It makes it difficult to add auxiliary
functions or data close to the function inside the list where it is needed.

Now the functions are no longer defined using such a literal, but instead
using calls to a "declareFunction" function which receives all the relevant
data.  Since each function call is independent from the others, anything can
go in between.

This commit deliberately avoided reindenting existing code to match the new
surroundings.  That way it is easier to see where actual changes happen,
even when not performing a whitespace-ignoring diff.
2015-09-10 09:45:02 +02:00
Martin von Gagern
5539226f4b Strip one level of indirection from functions module exports 2015-09-10 09:22:24 +02:00
Kevin Barabash
8accf0f18a Merge pull request #342 from xymostech/rename-breakOnUnsupportedCmds
Rename breakOnUnsupportedCmds to throwOnError.
2015-09-01 18:49:33 -06:00
Emily Eisenberg
d6cec8a861 Rename breakOnUnsupportedCmds to throwOnError.
Also, the MathBb-chrome test changed, to what I believe is the correct
result? Not sure why it looked wrong before.

Test plan:
 - `make test`
 - take screenshots, see nothing changed.
2015-09-01 16:51:03 -07:00
Kevin Barabash
c428abca1e Merge pull request #292 from kevinb7/fonts-p3_mathml
Adds MathML support for math font commands.
2015-09-01 09:00:16 -06:00
Kevin Barabash
64e63d7546 Adds MathML support for math font commands.
This is part 3 or 3.  The first two pull requests added font metrics, HTML rendering, and screenshot tests.
2015-08-30 17:24:04 -06:00
Kevin Barabash
07dc11ccb0 Merge pull request #341 from gagern/pngRepack
(Re-)Create some more screenshots in a reproducible way
2015-08-30 16:32:58 -06:00
Martin von Gagern
9c2a391ff6 (Re-)Create some more screenshots in a reproducible way
When rebasing for 2e002ff37a I forgot to
re-create the screenshots based on the new parent.  As a consequence, the
font testing images from fd2d58fd80 were not
updated for Firefox and not even included for Chrome.

We still have the strange issue that Lap can result in one of two possible
screenshots, and while the previous commit recreated one of them, this one
here recreates the other.
2015-08-30 23:09:42 +02:00
Kevin Barabash
f488a7c48d Merge pull request #340 from gagern/pngRepack
Use jspngopt and pako to create reproducible PNG files for Chrome as well
2015-08-29 18:57:40 -06:00
Martin von Gagern
2e002ff37a Use jspngopt and pako to create reproducible PNG files for Chrome as well
The combination of jspngopt and pako should eliminate possible causes for
different PNG encodings, although the core reason for #325 remains unknown.
Pako has poorer compression rates than native libz, but optimization can
counter that effect, and actually reduce the size of the screenshots.

The screenshots for LimitControls and UnsupportedCmds on Firefox used to
exhibit subpixel rendering before, for reasons unknown.  The regenerated
versions don't exhibit this.  See #324 for a discussion.
2015-08-30 02:12:55 +02:00
Kevin Barabash
72027a1a56 Merge pull request #330 from xymostech/add-vert-separator
Add | column separators to arrays.
2015-08-28 10:29:26 -06:00
Emily Eisenberg
3a8adbf595 Add | column separators to arrays.
This adds the ability to add `|` to a column description and have
vertical separators be added. I added types to the column descriptions
and added some logic to handle the separators when building the vertical
lists of the array.

Test plan:
 - See the Arrays screenshot looks good.
 - `make test`
2015-08-28 08:50:42 -07:00
Kevin Barabash
1b5834d894 Merge pull request #334 from Khan/revert-333-gitter-badge
Revert "Add a Gitter chat badge to README.md"
2015-08-21 21:19:25 -06:00
Kevin Barabash
d772aab6d5 Revert "Add a Gitter chat badge to README.md" 2015-08-21 21:14:55 -06:00
Kevin Barabash
bc4c270220 Merge pull request #333 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2015-08-21 21:06:08 -06:00
The Gitter Badger
2af1ad2ffc Added Gitter badge 2015-08-22 02:55:03 +00:00
Kevin Barabash
2d9a6f323c Merge pull request #291 from kevinb7/fonts-p2_rendering
Adds math commands, HTML rendering, and screenshotter tests.
2015-08-19 22:07:34 -06:00
Kevin Barabash
fd2d58fd80 Adds math commands, HTML rendering, and screenshotter tests.
This is part 2 of 3.  Part 1 added new fonts metrics.  Part 2 will add MathML support and unit tests.
2015-08-19 22:04:34 -06:00
Kevin Barabash
f32d615813 Merge pull request #294 from gagern/symtypes
Fix symbol group types
2015-08-19 17:44:11 -06:00
Martin von Gagern
9942283db0 Fix incorrect symbol types
These symbols should have different types, according to symgroups.js
2015-08-19 22:57:05 +02:00
Martin von Gagern
1846929110 Add tool to check symbol group types against LaTeX
At the moment, the tool isn't suitable for automatic regression tests yet,
since some symbols will require human verification.  That might change in
the future though, with a list of manually verified symbols which can be
skipped in automatic verification.  For this reason the file got placed into
the test directory.
2015-08-19 22:56:41 +02:00
Kevin Barabash
cb9f765e06 Merge pull request #310 from gagern/ssYaml
Switch Screenshotter data from JSON to YAML
2015-08-18 21:00:20 -06:00
Kevin Barabash
1da8c8938b Merge pull request #323 from blaiprat/fontMetricsData_js
Generate fontMetricsData as JavaScript
2015-08-06 11:15:00 -07:00
Martin von Gagern
758f4a73d9 Merge remote-tracking branch 'origin/master' into ssYaml 2015-08-06 10:18:04 +02:00
Blai Pratdesaba
236b7925f1 Add missing semicolon at the end of fontMetricsData 2015-08-06 09:17:42 +01:00
Blai Pratdesaba
8cd71830c2 Generate fontMetricsData as JavaScript 2015-08-05 19:52:42 +01:00
Martin von Gagern
62a8b2d4e7 Whitelist keys to include in ss_data query strings 2015-08-03 22:45:22 +02:00
Kevin Barabash
b2d2df9bef Merge pull request #317 from JeffEverett/unsupported_commands
Added support for visual depiction of unsupported commands
2015-07-29 10:55:04 -07:00
Kevin Barabash
76d87e8f90 Merge pull request #319 from JeffEverett/fix_limit_controls
Fixed limit controls in textstyle
2015-07-29 09:05:39 -07:00
Kevin Barabash
4a507c40f1 Merge pull request #290 from kevinb7/fonts-p1_metrics
Adds font metrics to support font commands.
2015-07-29 09:04:33 -07:00
Jeff Everett
9b0f42ea50 Fixed limit controls in textstyle 2015-07-28 15:22:30 -06:00
Jeff Everett
e1c221273c Added support for visual depiction of unsupported commands 2015-07-28 00:50:08 -06:00
Kevin Barabash
4be3931cb5 Merge pull request #314 from JeffEverett/limits_and_nolimits
Add support for \limits and \nolimits controls
2015-07-24 19:53:55 -06:00
Jeff Everett
5d83bb8cc0 Added support for \limits and \nolimits controls 2015-07-24 19:33:09 -06:00
Kevin Barabash
0dc9eed1c4 Disable chrome screenshotter tests until we can determine why it's producing different results.
Test Plan:
- run `make screenshots` and verify that it doesn't start the chrome docker image

Auditors: alpert, emily
2015-07-22 22:12:39 -06:00
Kevin Barabash
bd275b85d7 Update make screenshots to run the new screenshotter
Test Plan:
- run `make screenshots` and verify that the screenshotter runs

Auditors: alpert
2015-07-22 22:06:16 -06:00
Kevin Barabash
476eebf3c5 Merge pull request #299 from gagern/delimiters
Provide more delimiters
2015-07-19 21:14:13 -06:00
Emily Eisenberg
2600587f78 Update master version numbers
Summary: Update the version numbers on master to <the-next-version>-pre.

Test plan:
 - ???

Auditors: kevinb, alpert
2015-07-16 14:20:29 -07:00
Emily Eisenberg
ce99abd1f2 Update Dockerfile for ttfautohint, update metrics
Summary:
Update the MathJaxFonts `Dockerfile` to enable the use of
`ttfautohint` which hints our fonts better. Also, fix the location that
batik is downloaded from, update some formatting in the `Dockerfile`,
and update the fonts and metrics.

Test Plan:
 - Compare a rendered `= - A z 4 \Longrightarrow \Sigma \Biggl(` in
   Chrome on Windows at font sizes 10px to 20px before and after this
   change.
 - See that characters look about the same, or better.
 - See that the screenshot tests didn't change in firefox (maybe firefox
   runs the same autohinting algorithm that we do?), and don't visually
   change in chrome

Reviewers: kevinb, alpert

Reviewed By: kevinb, alpert

Differential Revision: https://phabricator.khanacademy.org/D18977
2015-07-16 12:01:26 -07:00
Martin von Gagern
fb403fa9eb Switch Screenshotter data from JSON to YAML
Escaping TeX in JSON as query strings is a pain: you have to double all the
\\, you have to escape the & and the #, you can't easily include line breaks
for readability, and so on.  YAML solves most of these problems for most of
the situations.  Now each test case can be structured, while simple test
cases only consist of a line of verbatim TeX code, with no escaping.

The most troublesome items remaining are lines starting in { since in YAML
these would denote inline mapping types. We use block notation for these.
2015-07-16 08:57:17 +02:00
Kevin Barabash
a3031af307 Merge pull request #308 from gagern/selenium-images
Automatically start a development server
2015-07-15 07:50:37 -06:00
Kevin Barabash
e0c75df7b7 Merge pull request #309 from gagern/chromeScreenshotUpdates
Update chrome screenshots
2015-07-15 07:49:45 -06:00
Martin von Gagern
9752d02748 Automatically start a development server
This avoids one of the few requirements we have left: you no longer have to
start a KaTeX development server, the script will do it for you, using a
random port number.

To reproduce the old behaviour, explicitely state --katex-port=7936.
2015-07-15 15:41:29 +02:00
Martin von Gagern
b7e1581869 Update chrome screenshots
I forgot to re-create these after rebasing.
2015-07-15 15:40:31 +02:00
Kevin Barabash
a18db36296 Merge pull request #264 from gagern/selenium-images
Selenium images
2015-07-14 10:13:56 -06:00
Martin von Gagern
6f65f685f3 Add screenshots taken from Chrome 2015-07-14 18:09:58 +02:00
Martin von Gagern
5d155c75db Switch from own docker image to standard selenium images
Since the Selenium images are available for download, and downloading them
is usually faster than building them from scratch, this makes taking
screenshots easier.  Furthermore, since the Selenium image is not specific
to KaTeX, it could as well be used for other purposes, thus saving space
since a single image can be used in multiple projects.

This change also deals with the non-determinism in the Lap screenshot:
We detect the one known (and accepted) alternate rendering and change the
output file name to Lap_alt in this case.  So either Lap or Lap_alt gets
saved to, and if the image is different from both, then one of these files
will show a modification.  On the other hand, if it is equal to either of
these, then the matching one will get overwritten, showing no change.
2015-07-14 18:09:58 +02:00
Kevin Barabash
b1d5311898 Merge pull request #306 from gagern/browserifyUpdate
Update development server for new browserify version
2015-07-14 06:56:50 -06:00
Martin von Gagern
11970ee965 Update development server for new browserify version
Current browserify no longer accepts an options hash to the bundle method.
2015-07-14 09:51:20 +02:00
Kevin Barabash
67147b18ac Adds font metrics to support font commands.
This is part 1 of 3.  Rendering, screenshots, MathML, and unit tests will
follow in susbequent pull requests.
2015-07-10 21:50:04 -06:00
Kevin Barabash
549c2bf858 Merge pull request #293 from gagern/matrices
Matrices should be of type ord
2015-07-10 09:52:49 -06:00
Martin von Gagern
6362e0f8f2 Test case demonstrating the desired spacing around an array 2015-07-10 17:37:53 +02:00
Martin von Gagern
8bff74ca09 Change group type of array from inner to ord
This is a consequence of Rule 8 of Appendix G of The Tex Book.
2015-07-10 17:37:53 +02:00
Martin von Gagern
f05ff9c5fa Offer some help working out the types of math formula atoms 2015-07-10 17:36:52 +02:00
Kevin Barabash
6357a34828 Merge pull request #303 from gagern/metrics
Improve font metrics creation
2015-07-10 09:07:59 -06:00
Martin von Gagern
b290d4ad76 Switch from fontforge to fonttools
The dependencies of fonttools are much lighter than fontforge, and since all
we need are some metrics, fonttools is very much up to that task.

This addresses issue #288.
2015-07-10 14:32:02 +02:00
Martin von Gagern
d26a67f220 Avoid non-determinisms related to font metric generation
We had some duplicate mappings in mappings to start with.  Now we have some
code to complain loudly about these, and all currently existing duplicates
have been dealt with.

We also had a problem where in some Python dict, existing stuff was using
strings as keys while new data would use numeric indices, thus not
overwriting the previous value.  Now we always use strings as keys.

Along the road, the italic dotless i and j symbols were changed
so that they now are taken from cmmi10 instead of cmti10.
2015-07-10 14:30:42 +02:00
Martin von Gagern
86115b8fce Format font metrix data to have one row for each glyph
This will make reviewing modifications easier, since the affected glyphs
will be more readily visible in the diff.
The formatting tool was applied to the existing data, instead of
regenerating the data, so the semantic content should be unmodified.
2015-07-10 14:30:42 +02:00
Martin von Gagern
1603162267 Save auto-generated font metrics data to separate JSON file
This separates auto-generated code from manually created code.
We need a more recent version of browserify to directly require JSON.
Note that the data was copied, not recreated, so it has not been changed.
This addresses issue #301.
2015-07-10 14:30:41 +02:00
Martin von Gagern
1f8610cebe Auto-detect python binary
We now try whether there is an executable called python2, and if so,
use that in preference to python when executing the metric computation.
Furthermore, we allow the user to specify the path of the python binary
using the PYTHON variable of make, i.e. “make PYTHON=/foo/python2 metrics”.
2015-07-09 14:44:01 +02:00
Kevin Barabash
6cf8c5aacb add a link to Examining TeX page on wiki to CONTRIBUTING.md
Auditors: emily
2015-07-08 23:38:40 -06:00
Martin von Gagern
7c8ea80638 Provide more delimiters
This adds \lgroup, \rgroup, \lmoustache and \rmoustache,
provides \lVert and \rVert with the correct type for each,
and makes \lvert, \rvert, \lVert and \rVert available
through \left...\right.
2015-07-09 02:23:33 +02:00
Kevin Barabash
8009059b7c Merge pull request #295 from gagern/fracspace
Improve horizontal spacing of fractions
2015-07-08 18:17:28 -06:00
Emily Eisenberg
d00caf6ed7 Merge pull request #298 from gagern/Bmatrix
Implement Bmatrix environment
2015-07-08 17:01:08 -07:00
Martin von Gagern
ec3cbb8656 Implement Bmatrix environment 2015-07-08 22:47:22 +02:00
Martin von Gagern
7dc8b68092 Improve horizontal spacing of fractions
A fraction is surrounded by a box of width \nulldelimiterspace on either side.
That size is 1.2pt and does not scale with the style or font size.
Furthermore, a \frac creates a brace-enclosed group which results in a
\mathord, not a \mathinner.
2015-07-07 10:03:34 +02:00
Kevin Barabash
a06744e941 Merge pull request #268 from gagern/texcmp
Generate reference images from LaTeX
2015-07-06 21:04:39 -06:00
Martin von Gagern
c562813afa Take snapshots of formulas rendered by LaTeX and compute visual diffs
The same test cases we use for our screenshots from Firefox are now also
being rendered by pdflatex, so the resulting images can be used as reference
for how things are supposed to look (if we concentrate on compatibility with
LaTeX).  To make comparisons even easier, the differences between LaTeX and
Firefox snapshots are rendered in a visual way, using different colors.

Discussed in pull request #268.
2015-07-06 15:54:14 +02:00
Kevin Barabash
7be056c1f4 Merge pull request #283 from kevinb7/kevinb7-patch-4
Update CONTRIBUTING.md
2015-07-03 10:17:09 -06:00
Kevin Barabash
53b0a9ad7a Update CONTRIBUTING.md
added "Pull Requests" section with some guidelines, plus some addition code style guidelines.

fix grammar and be more specific about splitting large pull request

Update CONTRIBUTING.md
2015-07-03 10:14:06 -06:00
Kevin Barabash
87b2cc95bf Merge pull request #284 from kevinb7/kevinb7-patch-5
Updated Makefile so that it "make dist" works on OS X
2015-07-02 14:36:28 -06:00
Kevin Barabash
2e9fdee780 Merge pull request #285 from kevinb7/kevinb7-patch-6
Added a link to the list of commands on the wiki.
2015-07-02 14:34:24 -06:00
Kevin Barabash
1a11eb46b2 Added a link to the list of commands on the wiki. 2015-07-01 20:21:55 -06:00
Kevin Barabash
39489ab479 Update Makefile
```cp --recursive``` isn't supported on Mac OS X so use ```cp -R``` instead.
2015-07-01 20:18:40 -06:00
Kevin Barabash
1ac6b41990 Merge pull request #280 from JeffEverett/master
Add two AMS symbols: checkmark and circledR
2015-07-01 07:24:11 -06:00
Jeff Everett
eaf89dd45d Add two AMS symbols: checkmark and circledR 2015-07-01 01:57:57 -06:00
Kevin Barabash
41e3fa6659 Merge pull request #279 from gagern/cases
Implement cases environment
2015-07-01 00:08:36 -06:00
Martin von Gagern
758bdba31e Implement cases environment
See issue #278.  Although the official definition makes use of @{…}
notation, we use custom spacing instead, since that seems easier for now.
2015-07-01 08:05:08 +02:00
Emily Eisenberg
7f1b53cbfd Fix arc lint
Summary: Move the linting configuration to `.arclint` so that `arc lint`
works again.

Test plan:
 - `arc lint`

Auditors: alpert
2015-06-26 13:57:02 -07:00
Emily Eisenberg
8931e5b45c Cleanup Makefile
Summary: Ensure that `make dist` is idempotent, and make sure comments
don't get printed out.

Test plan:
 - `make dist`
 - `make dist` again
 - See that there's no `katex` directory in `dist/`
 - See that no comments are printed out during `make dist`

Auditors: alpert
2015-06-20 12:22:04 -07:00
Emily Eisenberg
c18d3ad6c4 Remove font files and auto-render from bower's main
Summary: Bower's bower.json spec says to not include font/image files,
and only to include one file per filetype.

Test plan:
 - `make dist`, commit the result
 - Run `bower install /path/to/KaTeX#master` from /tmp
 - See that this succeeds, and `bower list --paths` has katex.min.js and
   katex.min.css for the KaTeX entry

Auditors: alpert, kevinb
2015-06-20 12:06:30 -07:00
Kevin Barabash
4ea7d38b5c Summary: add "main" field instead of "name" to bower.json
Test plan:
 - Run `bower install katex` from /tmp
 - Verify that there are no errors when running the command
 - Verify that `bower_components/katex/dist/` contains all the build files
 - Verify that the version is 0.4.2

Auditors: emily
2015-06-20 11:08:58 -06:00
Kevin Barabash
b11a1b97b7 Summary:
- Add "main" field to bower.json to prevent errors during installation.
- Bump version to 0.4.1 in bower.json and package.json

https://github.com/Khan/KaTeX/issues/181

Test plan:
 - Run `bower install katex` from /tmp
 - Verify that there are no errors when running the command
   and that `bower_components/katex/dist/` contains all the build files

Auditors: emily
2015-06-20 11:00:41 -06:00
318 changed files with 10939 additions and 9830 deletions

View File

@ -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
View 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
View File

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

83
.eslintrc Normal file
View 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
View 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
View File

@ -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

View File

@ -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
}
}

View File

@ -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
View 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/

View File

@ -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

View File

@ -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

View File

@ -1,21 +1,23 @@
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/) [![Build Status](https://travis-ci.org/Khan/KaTeX.svg?branch=master)](https://travis-ci.org/Khan/KaTeX)
[![Join the chat at https://gitter.im/Khan/KaTeX](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Khan/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* **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:** KaTeXs layout is based on Donald Knuths TeX, the gold standard for math typesetting.
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, and IE 8 - IE 11.
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

View File

@ -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
View 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
View File

@ -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);
});

View File

@ -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 > $@

View File

@ -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"]`.

View File

@ -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},
]);
});
});

View File

@ -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");
}

View File

@ -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]);

View File

@ -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/

View File

@ -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

View File

@ -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

View 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;
}

View File

@ -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()

View 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
View 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
View 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

View 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
View 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
View 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 "$@"

View File

@ -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,
};

View File

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

View File

@ -7,15 +7,15 @@ There are several requirements for generating the metrics used by KaTeX.
this by running `tex --version`, and seeing if it has a line that looks like
> 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!

View File

@ -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(

View File

@ -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
View 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")

View File

@ -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;
}

View File

@ -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():

View File

@ -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)

View File

@ -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
View 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
View File

@ -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;

View File

@ -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
View 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;

View File

@ -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",
};
/**

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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],
};

View File

@ -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,
};

File diff suppressed because it is too large Load Diff

View File

@ -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]);

View File

@ -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) {

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

23
src/macros.js Normal file
View 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}");

View File

@ -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,
};

View File

@ -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
};

View File

@ -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();
};

File diff suppressed because it is too large Load Diff

15
src/unicodeRegexes.js Normal file
View 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,
};

View File

@ -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 = {
"&": "&amp;",
">": "&gt;",
"<": "&lt;",
"\"": "&quot;",
"'": "&#x27;"
const ESCAPE_LOOKUP = {
"&": "&amp;",
">": "&gt;",
"<": "&lt;",
"\"": "&quot;",
"'": "&#x27;",
};
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,
};

View File

@ -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.

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