diff --git a/.gitattributes b/.gitattributes index 8e0600418..66fbd753f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,4 @@ * text=lf # don't mess with fonts -src/font/* text=binary +src/fonts/* text=binary diff --git a/.gitignore b/.gitignore index 493a3217d..febbb5c96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,2 @@ -/tmp - -# build tools /node_modules - -# dist /build -/mathquill-*.tgz -/mathquill.github.com diff --git a/BUILDING b/BUILDING index 33fc2f205..b60c43354 100644 --- a/BUILDING +++ b/BUILDING @@ -15,5 +15,9 @@ re-make, and also serve the demo, the unit tests, and the visual tests. unit tests -> http://localhost:9292/test/unit.html visual tests -> http://localhost:9292/test/visual.html +If building on Windows: + 1. Install GNU Make from http://gnuwin32.sourceforge.net/packages/make.htm. Do not use make derivitives from mSYS or MinGW. Ensure the location of make.exe is added to your PATH environment variable. + 2. Grab the latest Git for Windows from https://git-scm.com/download/win. When installing, add Git and its optional shell tools to your PATH environment variable (these questions are asked during setup). + If any of this does not work, please let us know! We want to make hacking on mathquill as easy as possible. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d2d54974..e48a51276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,45 @@ -## v0.10.0: 2016-02-20 +## v0.10.1: Fix `font-size: 0` typing problems and more + +_2016-03-21_ + +Important fix: remove `font-size: 0` on textarea (#585), fixing typing +in Chrome Canary (#540) as well as the Enter key not triggering the +`enter` handler in Webkit and Blink (#566). `transform: scale(0)` is +used instead and expected to be much more robust. + +(Note: if you're coming from v0.9.x, there've been major API changes, +see the [v0.9.x → v0.10.0 Migration Guide][].) + +[v0.9.x → v0.10.0 Migration Guide]: https://github.com/mathquill/mathquill/wiki/v0.9.x-%E2%86%92-v0.10.0-Migration-Guide + +**new features:** +- (#544, #552, #558, #581) new symbols `\nparallel`, `\measuredangle`, + `\odot`, `\parallelogram` (nonstandard), `\nless`, `\ngtr`, `\square` +- (#544) new commands `\overleftarrow`, `\overrightarrow` + + +**bugfixes:** +- (#585) fix typing in Chrome Canary, Enter key in Webkit+Blink +- (#582) fix `\degree` symbol to round-trip (rather than exporting + `^\circ` which doesn't parse as one symbol) +- (#578) fix `.text()` to output `\cdot` as `*` +- (#529, #571, #574) fix `.text()` of fractions, spaces, variables followed + by exponents +- (#577) fix `\triangle` symbol to match LaTeX better +- (#568) hotfix #435 order-dependence breaking clean build on Linux +- (#560) fix florin spacing still too close +- (#546) fix parsing or pasting `×` (Unicode times symbol) +- (#519/#487) fix auto-horizontal-scroll/pan on API calls +- (#528) fix #429 can't move cursor out of `TextBlock` +- (#526) fix exponentiation to export `^` not `**` +- (#525) fix Tab while there's a selection + +**build system fixes:** +- (#532) add console output to show URL of local test pages + +## v0.10.0: Total API overhaul, new features galore + +_2016-02-20_ Many major changes including a total overhaul of the API (no more auto-MathQuill-ifying of `.mathquill-editable` etc, and no more jQuery @@ -100,7 +141,9 @@ itself): See the [v0.9.x → v0.10.0 Migration Guide] - (#117, #142, #186, #287) massive refactor of cursor methods to not assume the edit tree is double-layered -## v0.9.4: 2014-1-22 +## v0.9.4: URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40 + +_2014-1-22_ URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40 (#371) @@ -118,7 +161,9 @@ URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40 (#371) **docs:** - (#283) change license from LGPL to Mozilla Public License -## v0.9.3: 2013-11-11 +## v0.9.3: Fix `NZQRC` appearing double-struck/blackboard bold + +_2013-11-11_ **new features:** - (#185) add `\vec` @@ -137,7 +182,9 @@ URGENT HOTFIX for cursor showing up as an ugly box in Chrome 40 (#371) - (#189) replace Connect with tiny handwritten static server - upgrade to uglifyjs2 -## v0.9.2: 2013-04-02 +## v0.9.2: Fix bug in hotfix for typing over selections in Safari 5.1 + +_2013-04-02_ NOTE: The hotfix for typing over selections in Safari 5.1 (#135) from v0.9.1 had a huge bug, fixed as #166. @@ -162,7 +209,9 @@ v0.9.1 had a huge bug, fixed as #166. - New site-building system - no more submodules, `npm` only -## v0.9.1: 2012-12-19 +## v0.9.1: Hotfix for typing over selections in Safari 5.1 + +_2012-12-19_ * Started the changelog * Added a `make publish` script diff --git a/Makefile b/Makefile index 1c005903a..46f811553 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,20 @@ +# +# -*- Prerequisites -*- +# + +# the fact that 'I am Node.js' is unquoted here looks wrong to me but it +# CAN'T be quoted, I tried. Apparently in GNU Makefiles, in the paren+comma +# syntax for conditionals, quotes are literal; and because the $(shell...) +# call has parentheses and single and double quotes, the quoted syntaxes +# don't work (I tried), we HAVE to use the paren+comma syntax +ifneq ($(shell node -e 'console.log("I am Node.js")'), I am Node.js) + ifeq ($(shell nodejs -e 'console.log("I am Node.js")' 2>/dev/null), I am Node.js) + $(error You have /usr/bin/nodejs but no /usr/bin/node, please 'sudo apt-get install nodejs-legacy' (see http://stackoverflow.com/a/21171188/362030 )) + endif + + $(error Please install Node.js: https://nodejs.org/ ) +endif + # # -*- Configuration -*- # @@ -15,13 +32,22 @@ BASE_SOURCES = \ $(SRC_DIR)/cursor.js \ $(SRC_DIR)/controller.js \ $(SRC_DIR)/publicapi.js \ - $(SRC_DIR)/services/*.util.js \ - $(SRC_DIR)/services/*.js + $(SRC_DIR)/services/parser.util.js \ + $(SRC_DIR)/services/saneKeyboardEvents.util.js \ + $(SRC_DIR)/services/aria.js \ + $(SRC_DIR)/services/exportText.js \ + $(SRC_DIR)/services/focusBlur.js \ + $(SRC_DIR)/services/keystroke.js \ + $(SRC_DIR)/services/latex.js \ + $(SRC_DIR)/services/mouse.js \ + $(SRC_DIR)/services/scrollHoriz.js \ + $(SRC_DIR)/services/textarea.js SOURCES_FULL = \ $(BASE_SOURCES) \ - $(SRC_DIR)/commands/*.js \ - $(SRC_DIR)/commands/*/*.js + $(SRC_DIR)/commands/math.js \ + $(SRC_DIR)/commands/text.js \ + $(SRC_DIR)/commands/math/*.js SOURCES_BASIC = \ $(BASE_SOURCES) \ @@ -33,8 +59,8 @@ CSS_DIR = $(SRC_DIR)/css CSS_MAIN = $(CSS_DIR)/main.less CSS_SOURCES = $(shell find $(CSS_DIR) -name '*.less') -FONT_SOURCE = $(SRC_DIR)/font -FONT_TARGET = $(BUILD_DIR)/font +FONT_SOURCE = $(SRC_DIR)/fonts +FONT_TARGET = $(BUILD_DIR)/fonts UNIT_TESTS = ./test/unit/*.test.js @@ -49,11 +75,6 @@ BASIC_CSS = $(BUILD_DIR)/mathquill-basic.css BUILD_TEST = $(BUILD_DIR)/mathquill.test.js UGLY_JS = $(BUILD_DIR)/mathquill.min.js UGLY_BASIC_JS = $(BUILD_DIR)/mathquill-basic.min.js -CLEAN += $(BUILD_DIR)/* - -DISTDIR = ./mathquill-$(VERSION) -DIST = $(DISTDIR).tgz -CLEAN += $(DIST) # programs and flags UGLIFY ?= ./node_modules/.bin/uglifyjs @@ -79,41 +100,50 @@ BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile # -*- Build tasks -*- # -.PHONY: all basic dev js uglify css font dist clean +.PHONY: all basic dev js uglify css font clean all: font css uglify basic: $(UGLY_BASIC_JS) $(BASIC_CSS) +unminified_basic: $(BASIC_JS) $(BASIC_CSS) # dev is like all, but without minification dev: font css js js: $(BUILD_JS) uglify: $(UGLY_JS) css: $(BUILD_CSS) font: $(FONT_TARGET) -dist: $(DIST) clean: - rm -rf $(CLEAN) + rm -rf $(BUILD_DIR) $(PJS_SRC): $(NODE_MODULES_INSTALLED) $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ $(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_BASIC_JS): $(BASIC_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ $(BUILD_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) $(LESS_OPTS) $(CSS_MAIN) > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(BASIC_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) --modify-var="basic=true" $(LESS_OPTS) $(CSS_MAIN) > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(NODE_MODULES_INSTALLED): package.json - npm install + test -e $(NODE_MODULES_INSTALLED) || rm -rf ./node_modules/ # robust against previous botched npm install + NODE_ENV=development npm install touch $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS): @@ -124,12 +154,6 @@ $(FONT_TARGET): $(FONT_SOURCE) $(BUILD_DIR_EXISTS) rm -rf $@ cp -r $< $@ -$(DIST): $(UGLY_JS) $(BUILD_JS) $(BUILD_CSS) $(FONT_TARGET) - rm -rf $(DISTDIR) - cp -r $(BUILD_DIR) $(DISTDIR) - tar -czf $(DIST) --exclude='\.gitkeep' $(DISTDIR) - rm -r $(DISTDIR) - # # -*- Test tasks -*- # @@ -143,83 +167,4 @@ test: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) $(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ > $@ - -# -# -*- site (mathquill.github.com) tasks -# - -.PHONY: site publish site-pull - -SITE = mathquill.github.com -SITE_CLONE_URL = git@github.com:mathquill/mathquill.github.com -SITE_COMMITMSG = 'updating mathquill to $(VERSION)' - -DOWNLOADS_PAGE = $(SITE)/downloads.html -DIST_DOWNLOAD = $(SITE)/downloads/$(DIST) - -site: $(SITE) $(SITE)/mathquill $(SITE)/demo.html $(SITE)/support $(DOWNLOADS_PAGE) - -publish: site-pull site - pwd - cd $(SITE) \ - && git add -- mathquill demo.html support downloads downloads.html \ - && git commit -m $(SITE_COMMITMSG) \ - && git push - -$(SITE)/mathquill: $(DIST) - mkdir -p $@ - tar -xzf $(DIST) \ - --directory $@ \ - --strip-components=2 - -$(DIST_DOWNLOAD): $(DIST) - mkdir -p $(dir $@) - cp $^ $@ - -# freaking bsd, i swear -# adapted from https://developer.apple.com/library/mac/documentation/opensource/Conceptual/ShellScripting/PortingScriptstoMacOSX/PortingScriptstoMacOSX.html#//apple_ref/doc/uid/TP40004268-TP40003517-SW21 -ifeq (x, $(shell echo xy | sed -r 's/(x)y/\1/' 2>/dev/null)) - # gnu - SED = sed -r - SED_I = $(SED) -i -else - # bsd - SED = sed -E - SED_I = $(SED) -i '' -endif - -$(DOWNLOADS_PAGE): $(DIST_DOWNLOAD) - @echo Using $(SED) - @echo -n updating downloads page... - @$(SED_I) \ - -e '/Latest version:/ s/[0-9]+[.][0-9]+[.][0-9]+/$(VERSION)/g' \ - $(DOWNLOADS_PAGE) - @mkdir -p tmp - @ls $(SITE)/downloads/*.tgz \ - | egrep -o '[0-9]+[.][0-9]+[.][0-9]+' \ - | fgrep -v $(VERSION) \ - | sort -rn -t. -k 1,1 -k 2,2 -k 3,3 \ - | sed 's|.*|
](http://mathquill.com)
-[Slack]: http://slackin.mathquill.com
+The MathQuill project is supported by its [partners](http://mathquill.com/partners.html). We hold ourselves to a compassionate [Code of Conduct](http://docs.mathquill.com/en/latest/Code_of_Conduct/).
-## Usage
+MathQuill is resuming active development and we're committed to getting things running smoothly. Find a dusty corner? [Let us know in Slack.](http://slackin.mathquill.com) (Prefer IRC? We're `#mathquill` on Freenode.)
-Just load MathQuill and call our constructors on some HTML element DOM objects,
-for example:
+## Getting Started
-```html
-
-
-
+MathQuill has a simple interface. This brief example creates a MathQuill element and renders, then reads a given input:
+```javascript
+var htmlElement = document.getElementById('some_id');
+var config = {
+ handlers: { edit: function(){ ... } },
+ restrictMismatchedBrackets: true
+};
+var mathField = MQ.MathField(htmlElement, config);
-- Solve ax^2 + bx + c = 0: - x= -
- - -``` - -To load MathQuill, -- [jQuery 1.4.3+](http://jquery.com) has to be loaded before `mathquill.js` - ([Google CDN-hosted copy][] recommended) -- the fonts should be served from the `font/` directory relative to - `mathquill.css` (unless you'd rather change where your copy of `mathquill.css` - includes them from), which is already the case if you just: -- download and serve [the latest release][]. - -[Google CDN-hosted copy]: http://code.google.com/apis/libraries/devguide.html#jquery -[the latest release]: https://github.com/mathquill/mathquill/releases/latest - -To use the MathQuill API, first get the latest version of the interface: - -```js -var MQ = MathQuill.getInterface(2); -``` - -Now you can call `MQ.StaticMath()` or `MQ.MathField()`, which MathQuill-ify -an HTML element and return an API object. If the element had already been -MathQuill-ified into the same kind, return that kind of API object (if -different kind or not an HTML element, `null`). Note that it always returns -either an instance of itself, or `null`. - -```js -var staticMath = MQ.StaticMath(staticMathSpan); -mathField instanceof MQ.StaticMath // => true -mathField instanceof MQ // => true -mathField instanceof MathQuill // => true - -var mathField = MQ.MathField(mathFieldSpan); -mathField instanceof MQ.MathField // => true -mathField instanceof MQ.EditableField // => true -mathField instanceof MQ // => true -mathField instanceof MathQuill // => true -``` - -`MQ` itself is a function that takes an HTML element and, if it's the root -HTML element of a static math or math field, returns an API object for it -(if not, `null`): - -```js -MQ(mathFieldSpan) instanceof MQ.MathField // => true -MQ(otherSpan) // => null -``` - -API objects for the same MathQuill instance have the same `.id`, which will -always be a unique truthy primitive value that can be used as an object key -(like an ad hoc [`Map`][] or [`Set`][]): - -```js -MQ(mathFieldSpan).id === mathField.id // => true - -var setOfMathFields = {}; -setOfMathFields[mathField.id] = mathField; -MQ(mathFieldSpan).id in setOfMathFields // => true -staticMath.id in setOfMathFields // => false +mathField.latex('2^{\\frac{3}{2}}'); // Renders the given LaTeX in the MathQuill field +mathField.latex(); // => '2^{\\frac{3}{2}}' ``` -[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map -[`Set`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set - -Similarly, API objects for the same MathQuill instance share a `.data` object -(which can be used like an ad hoc [`WeakMap`][] or [`WeakSet`][]): - -```js -MQ(mathFieldSpan).data === mathField.data // => true -mathField.data.foo = 'bar'; -MQ(mathFieldSpan).data.foo // => 'bar' -``` - -[`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap -[`WeakSet`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet - -Any element that has been MathQuill-ified can be reverted: - -```html - - someHTML
-
-```
-```js
-MQ($('#revert-me')[0]).revert().html(); // => 'some HTML'
-```
-
-MathQuill uses computed dimensions, so if they change (because an element was
-mathquill-ified before it was in the visible HTML DOM, or the font size
-changed), then you'll need to tell MathQuill to recompute:
-
-```js
-var mathFieldSpan = $('\\sqrt{2}');
-var mathField = MQ.MathField(mathFieldSpan[0]);
-mathFieldSpan.appendTo(document.body);
-mathField.reflow();
-```
-
-MathQuill API objects further expose the following public methods:
-
-* `.el()` returns the root HTML element
-* `.html()` returns the contents as static HTML
-* `.latex()` returns the contents as LaTeX
-* `.latex('a_n x^n')` will render the argument as LaTeX
-
-Additionally, descendants of `MQ.EditableField` (currently only `MQ.MathField`)
-expose:
-
-* `.write(' - 1')` will write some LaTeX at the current cursor position
-* `.cmd('\\sqrt')` will enter a LaTeX command at the current cursor position or
- with the current selection
-* `.select()` selects the contents (just like [on `textarea`s][] and [on
- `input`s][])
-* `.clearSelection()` clears the current selection
-* `.moveTo{Left,Right,Dir}End()` move the cursor to the left/right end of the
- editable field, respectively. (The first two are implemented in terms of
- `.moveToDirEnd(dir)` where `dir` is one of `MQ.L` or `MQ.R`, constants that
- obey the contract that `MQ.L === -MQ.R` and vice versa.)
-* `.keystroke(keys)` simulates keystrokes given a string like `"Ctrl-Home Del"`,
- a whitespace-delimited list of [key values][] with optional prefixes
-* `.typedText(text)` simulates typing text, one character at a time
-* `ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ` `.dropEmbedded(pageX, pageY, options)` insert a custom
- embedded element at the given coordinates, where `options` is an object like:
-
- ```js
- {
- htmlString: '',
- text: function() { return 'custom_embed'; },
- latex: function() { return '\customEmbed'; }
- }
- ```
-
-[on `textarea`s]: http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-48880622
-[on `input`s]: http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-34677168
-[key values]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes
-
-MathQuill overwrites the global `MathQuill` variable when loaded. You can undo
-that with `.noConflict()` (similar to [`jQuery.noConflict()`]
-(http://api.jquery.com/jQuery.noConflict)):
-
-```html
-
-
-
-```
-
-(Warning: This lets different copies of MathQuill each power their own
- math fields, but using different copies on the same DOM element won't
- work. Anyway, .noConflict() is primarily to help you reduce globals.)
-
-#### Configuration Options
-
-`MQ.MathField()` can also take an options object:
-
-```js
-var el = $('x^2').appendTo('body');
-var mathField = MQ.MathField(el[0], {
- spaceBehavesLikeTab: true,
- leftRightIntoCmdGoes: 'up',
- restrictMismatchedBrackets: true,
- sumStartsWithNEquals: true,
- supSubsRequireOperand: true,
- charsThatBreakOutOfSupSub: '+-=<>',
- autoSubscriptNumerals: true,
- autoCommands: 'pi theta sqrt sum',
- autoOperatorNames: 'sin cos etc',
- substituteTextarea: function() {
- return document.createElement('textarea');
- },
- handlers: {
- edit: function(mathField) { ... },
- upOutOf: function(mathField) { ... },
- moveOutOf: function(dir, mathField) { if (dir === MQ.L) ... else ... }
- }
-});
-```
+Check out our [Getting Started Guide](http://docs.mathquill.com/en/latest/Getting_Started/) for setup instructions and basic MathQuill usage.
-To change `mathField`'s options, the `.config({ ... })` method takes an options
-object in the same format.
+## Docs
-Global defaults for a page may be set with `MQ.config({ ... })`.
+Most documentation for MathQuill is located on [ReadTheDocs](http://docs.mathquill.com/en/latest/).
-If `spaceBehavesLikeTab` is true the keystrokes {Shift-,}Spacebar will behave
-like {Shift-,}Tab escaping from the current block (as opposed to the default
-behavior of inserting a Space character).
-
-By default, the Left and Right keys move the cursor through all possible cursor
-positions in a particular order: right into a fraction puts the cursor at the
-left end of the numerator, right out of the numerator puts the cursor at the
-left end of the denominator, right out of the denominator puts the cursor to the
-right of the fraction; symmetrically, left into a fraction puts the cursor at
-the right end of the denominator, etc. Note that right out of the numerator to
-the left end of the denominator is actually leftwards (and downwards, it's
-basically wrapped). If instead you want right to always go right, and left to
-always go left, you can set `leftRightIntoCmdGoes` to `'up'` or `'down'` so that
-left and right go up or down (respectively) into commands, e.g. `'up'` means
-that left into a fraction goes up into the numerator, skipping the denominator;
-symmetrically, right out of the numerator skips the denominator and puts the
-cursor to the right of the fraction, which unlike the default behavior is
-actually rightwards (the drawback is the denominator is always skipped, you
-can't get to it with just Left and Right, you have to press Down); which is
-the same behavior as the Desmos calculator. `'down'` instead means it is the
-numerator that is always skipped, which is the same behavior as the Mac OS X
-built-in app Grapher.
-
-If `restrictMismatchedBrackets` is true then you can type [a,b) and [a,b), but
-if you try typing `[x}` or `\langle x|`, you'll get `[{x}]` or
-`\langle|x|\rangle` instead. This lets you type `(|x|+1)` normally; otherwise,
-you'd get `\left( \right| x \left| + 1 \right)`.
-
-If `sumStartsWithNEquals` is true then when you type `\sum`, `\prod`, or
-`\coprod`, the lower limit starts out with `n=`, e.g. you get the LaTeX
-`\sum_{n=}^{ }`, rather than empty by default.
-
-`supSubsRequireOperand` disables typing of superscripts and subscripts when
-there's nothing to the left of the cursor to be exponentiated or subscripted.
-Averts the especially confusing typo `x^^2`, which looks much like `x^2`.
-
-`charsThatBreakOutOfSupSub` sets the chars that when typed, "break out" of
-superscripts and subscripts: for example, typing `x^2n+y` normally results in
-the LaTeX `x^{2n+y}`, you have to hit Down or Tab (or Space if
-`spaceBehavesLikeTab` is true) to move the cursor out of the exponent and get
-the LaTeX `x^{2n}+y`; this option makes `+` "break out" of the exponent and
-type what you expect. Problem is, now you can't just type `x^n+m` to get the
-LaTeX `x^{n+m}`, you have to type `x^(n+m` and delete the paren or something.
-(Doesn't apply to the first character in a superscript or subscript, so typing
-`x^-6` still results in `x^{-6}`.)
-
-`autoCommands`, a space-delimited list of LaTeX control words (no backslash,
-letters only, min length 2), defines the (default empty) set of "auto-commands",
-commands automatically rendered by just typing the letters without typing a
-backslash first.
-
-`autoOperatorNames`, a list of the same form (space-delimited letters-only each
-length>=2), and overrides the set of operator names that automatically become
-non-italicized when typing the letters without typing a backslash first, like
-`sin`, `log`, etc. (Defaults to [the LaTeX built-in operator names][Wikia], but
-with additional trig operators like `sech`, `arcsec`, `arsinh`, etc.)
-
-[Wikia]: http://latex.wikia.com/wiki/List_of_LaTeX_symbols#Named_operators:_sin.2C_cos.2C_etc.
-
-`substituteTextarea`, a function that creates a focusable DOM element, called
-when setting up a math field. It defaults to ``,
-but for example, Desmos substitutes `` on iOS to
-suppress the built-in virtual keyboard in favor of a custom math keypad that
-calls the MathQuill API. Unfortunately there's no universal [check for a virtual
-keyboard][StackOverflow], you can't even [detect a touchscreen][stucox] (notably
-[Modernizr gave up][Modernizr]) and even if you could, Windows 8 and ChromeOS
-devices have both physical keyboards and touchscreens and you can connect
-physical keyboards to iOS and Android devices with Bluetooth, so touchscreen !=
-virtual keyboard. Desmos currently sniffs the user agent for iOS, so Bluetooth
-keyboards just don't work in Desmos on iOS, the tradeoffs are up to you.
-
-[StackOverflow]: http://stackoverflow.com/q/2593139/362030
-[stucox]: http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
-[Modernizr]: https://github.com/Modernizr/Modernizr/issues/548
-
-Supported handlers:
-- `moveOutOf`, `deleteOutOf`, and `selectOutOf` are called with `dir` and the
- math field API object as arguments
-- `upOutOf`, `downOutOf`, `enter`, and `edit` are called with just the API
- object as the argument
-
-The `*OutOf` handlers are called when Left/Right/Up/Down/Backspace/Del/
-Shift-Left/Shift-Right is pressed but the cursor is at the left/right/top/bottom
-edge and so nothing happens within the math field. For example, when the cursor
-is at the left edge, pressing the Left key causes the `moveOutOf` handler (if
-provided) to be called with `MQ.L` and the math field API object as arguments,
-and Backspace causes `deleteOutOf` (if provided) to be called with `MQ.L` and
-the API object as arguments, etc.
-
-The `enter` handler is called whenever Enter is pressed.
-
-The `edit` handler is called when the contents of the field might have been
-changed by stuff being typed, or deleted, or written with the API, etc.
-(Deprecated aliases: `edited`, `reflow`.)
-
-Handlers are always called directly on the `handlers` object passed in,
-preserving the `this` value, so you can do stuff like:
-```js
-var MathList = P(function(_) {
- _.init = function() {
- this.maths = [];
- this.el = ...
- };
- _.add = function() {
- var math = MQ.MathField($('')[0], { handlers: this });
- $(math.el()).appendTo(this.el);
- math.data.i = this.maths.length;
- this.maths.push(math);
- };
- _.moveOutOf = function(dir, math) {
- var adjacentI = (dir === MQ.L ? math.data.i - 1 : math.data.i + 1);
- var adjacentMath = this.maths[adjacentI];
- if (adjacentMath) adjacentMath.focus().moveToDirEnd(-dir);
- };
- ...
-});
-```
-Of course you can always ignore the last argument, like when the handlers close
-over the math field:
-```js
-var latex = '';
-var mathField = MQ.MathField($('#mathfield')[0], {
- handlers: {
- edit: function() { latex = mathField.latex(); },
- enter: function() { submitLatex(latex); }
- }
-});
-```
-
-**A Note On Changing Colors:**
-
-To change the foreground color, don't just set the `color`, also set
-the `border-color`, because the cursor, fraction bar, and square root
-overline are all borders, not text. (Example below.)
-
-Due to technical limitations of IE8, if you support it, and want to give
-a MathQuill editable a background color other than white, and support
-square roots, parentheses, square brackets, or curly braces, you will
-need to, in addition to of course setting the background color on the
-editable itself, set it on elements with class `mq-matrixed`, and then set
-a Chroma filter on elements with class `mq-matrixed-container`.
-
-For example, to style as white-on-black instead of black-on-white:
-
- #my-math-input {
- color: white;
- border-color: white;
- background: black;
- }
- #my-math-input .mq-matrixed {
- background: black;
- }
- #my-math-input .mq-matrixed-container {
- filter: progid:DXImageTransform.Microsoft.Chroma(color='black');
- }
-
-(This is because almost all math rendered by MathQuill has a transparent
-background, so for them it's sufficient to set the background color on
-the editable itself. The exception is, IE8 doesn't support CSS
-transforms, so MathQuill uses a matrix filter to stretch parens etc,
-which [anti-aliases wrongly without an opaque background][Transforms],
-so MathQuill defaults to white.)
-
-[Transforms]: http://github.com/mathquill/mathquill/wiki/Transforms
-
-## Building and Testing
-
-To hack on MathQuill, you're gonna want to build and test the source files
-you edit. In addition to `make`, MathQuill uses some build tools written on
-[Node](http://nodejs.org/#download), so you will need to install that before
-running `make`. (Once it's installed, `make` automatically does `npm install`,
-installing the necessary build tools.)
-
-- `make` builds `build/mathquill.{css,js,min.js}`
-- `make dev` won't try to minify MathQuill (which can be annoyingly slow)
-- `make test` builds `mathquill.test.js` (used by `test/unit.html`) and also
- doesn't minify
-- `make basic` builds `mathquill-basic.{js,min.js,css}` and
- `font/Symbola-basic.{eot,ttf}`; serve and load them instead for a stripped-
- down version of MathQuill for basic mathematics, without advanced LaTeX
- commands. Specifically, it doesn't let you type LaTeX backslash commands
- with `\` or text blocks with `$`, and also won't render any LaTeX commands
- that can't by typed without `\`. The resulting JS is only somewhat smaller,
- but the font is like 100x smaller. (TODO: reduce full MathQuill's font size.)
-
-## Understanding The Source Code
-
-All the CSS is in `src/css`. Most of it's pretty straightforward, the choice of
-font isn't settled, and fractions are somewhat arcane, see the Wiki pages
-["Fonts"](http://github.com/mathquill/mathquill/wiki/Fonts) and
-["Fractions"](http://github.com/mathquill/mathquill/wiki/Fractions).
-
-All the JavaScript that you actually want to read is in `src/`, `build/` is
-created by `make` to contain the same JS cat'ed and minified.
-
-There's a lot of JavaScript but the big picture isn't too complicated, there's 2
-thin layers sandwiching 2 broad but modularized layers:
-
-- At the highest level, the public API is a thin wrapper around calls to:
-- "services" on the "controller", which sets event listeners that call:
-- methods on "commands" in the "edit tree", which call:
-- tree- and cursor-manipulation methods, at the lowest level, to move the
- cursor or edit the tree or whatever.
-
-More specifically:
-
-(In comments and internal documentation, `::` means `.prototype.`.)
-
-- At the lowest level, the **edit tree** of JS objects represents math and text
- analogously to how [the HTML DOM][] represents a web page.
- + (Old docs variously called this the "math tree", the "fake DOM", or some
- combination thereof, like the "math DOM".)
- + `tree.js` defines base classes of objects relating to the tree.
- + `cursor.js` defines objects representing the cursor and a selection of
- math or text, with associated HTML elements.
-- Interlude: a **feature** is a unit of publicly exposed functionality, either
- by the API or interacted with by typists. Following are the 2 disjoint
- categories of features.
-- A **command** is a thing you can type and edit like a fraction, square root,
- or "for all" symbol, ∀. They are implemented as a class of node objects
- in the edit tree, like `Fraction`, `SquareRoot`, or `VanillaSymbol`.
- + Each command has an associated **control sequence** (as termed by Knuth;
- in the LaTeX community, commonly called a "macro" or "command"), a token
- in TeX and LaTeX syntax consisting of a backslash then any single
- character or string of letters, like `\frac` or \ . Unlike
- loose usage in the LaTeX community, where `\ne` and `\neq` (which print
- the same symbol, ≠) might or might not be considered the same command,
- in the context of MathQuill they are considered different "control
- sequences" for the same "command".
-- A **service** is a feature that applies to all or many commands, like typing,
- moving the cursor around, LaTeX exporting, LaTeX parsing. Note that each of
- these varies by command (the cursor goes in a different place when moving into
- a fraction vs into a square root, they export different LaTeX, etc), cue
- polymorphism: services define methods on the controller that call methods on
- nodes in the edit tree with certain contracts, such as a controller method
- called on initialization to set listeners for keyboard events, that when the
- Left key is pressed, calls `.moveTowards` on the node just left of the cursor,
- dispatching on what kind of command the node is (`Fraction::moveTowards` and
- `SquareRoot::moveTowards` can insert the cursor in different places).
- + `controller.js` defines the base class for the **controller**, which each
- math field or static math instance has one of, and to which each service
- adds methods.
-- `publicapi.js` defines the global `MathQuill.getInterface()` function, the
- `MQ.MathField()` etc. constructors, and the API objects returned by
- them. The constructors, and the API methods on the objects they return, call
- appropriate controller methods to initialize and manipulate math field and
- static math instances.
-
-[the HTML DOM]: http://www.w3.org/TR/html5-author/introduction.html#a-quick-introduction-to-html
-
-Misc.:
-
-`intro.js` defines some simple sugar for the idiomatic JS classes used
-throughout MathQuill, plus some globals and opening boilerplate.
-
-Classes are defined using [Pjs][], and the variable `_` is used by convention as
-the prototype.
-
-[pjs]: https://github.com/jneen/pjs
-
-`services/*.util.js` files are unimportant to the overall architecture, you can
-ignore them until you have to deal with code that is using them.
+Some older documentation still exists on the [Wiki](https://github.com/mathquill/mathquill/wiki).
## Open-Source License
The Source Code Form of MathQuill is subject to the terms of the Mozilla Public
-License, v. 2.0: http://mozilla.org/MPL/2.0/
+License, v. 2.0: [http://mozilla.org/MPL/2.0/](http://mozilla.org/MPL/2.0/)
The quick-and-dirty is you can do whatever if modifications to MathQuill are in
public GitHub forks. (Other ways to publicize modifications are also fine, as
-are private use modifications. See also: [MPL 2.0 FAQ][])
-
-[MPL 2.0 FAQ]: https://www.mozilla.org/en-US/MPL/2.0/FAQ/
+are private use modifications. See also: [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/))
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 000000000..29cdb51e3
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,315 @@
+# Okay so maybe everyone else already knows all this, but it took some time
+# for Michael and I [Han] to really see how everything fits together.
+#
+# Basically, what we're doing here is automated browser testing, so CircleCI
+# handles the automation, and Sauce Labs handles the browser testing.
+# Specifically, Sauce Labs offers a REST API to run tests in browsers in VMs,
+# and CircleCI can be configured to listen for git pushes and run local
+# servers and call out to REST APIs to test against these local servers.
+#
+# The flow goes like this:
+# - CircleCI notices/is notified of a git push
+# - they pull and checkout and magically know to install dependencies and shit
+# + https://circleci.com/docs/manually/
+# - their magic works fine for MathQuill's dependencies but to run the tests,
+# it foolishly runs `make test`, what an inconceivable mistake
+# - that's where we come in: `circle.yml` lets us override the test script.
+# + https://circleci.com/docs/configuration/
+# - our `circle.yml` first installs and runs a tunnel to Sauce Labs
+# - and runs `make server`
+# - then it calls out to Sauce Labs' REST API to open browsers that reach
+# back through the tunnel to access test pages on the local server
+# + > Sauce Connect allows you to run a test server within the CircleCI
+# > build container and expose it it (using a URL like `localhost:8080`)
+# > to Sauce Labs’ browsers.
+#
+# https://circleci.com/docs/browser-testing-with-sauce-labs/
+#
+# - boom testing boom
+
+
+# this file is based on https://github.com/circleci/sauce-connect/blob/a65e41c91e02550ce56c75740a422bebc4acbf6f/circle.yml
+# via https://circleci.com/docs/browser-testing-with-sauce-labs/
+#
+# then translated from 1.0 to 2.0 with: https://circleci.com/docs/2.0/config-translation/
+
+version: 2
+jobs:
+ build:
+ working_directory: ~/mathquill/mathquill
+ parallelism: 1
+ shell: /bin/bash --login
+ # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did.
+ # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables .
+ environment:
+ CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
+ CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
+ # In CircleCI 1.0 we used a pre-configured image with a large number of languages and other packages.
+ # In CircleCI 2.0 you can now specify your own image, or use one of our pre-configured images.
+ # The following configuration line tells CircleCI to use the specified docker image as the runtime environment for you job.
+ # We have selected a pre-built image that mirrors the build environment we use on
+ # the 1.0 platform, but we recommend you choose an image more tailored to the needs
+ # of each job. For more information on choosing an image (or alternatively using a
+ # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/
+ # To see the list of pre-built images that CircleCI provides for most common languages see
+ # https://circleci.com/docs/2.0/circleci-images/
+ docker:
+ - image: circleci/node:lts-browsers
+ steps:
+ # Machine Setup
+ # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each
+ # The following `checkout` command checks out your code to your working directory. In 1.0 we did this implicitly. In 2.0 you can choose where in the course of a job your code should be checked out.
+ - checkout
+ # Prepare for artifact and test results collection equivalent to how it was done on 1.0.
+ # In many cases you can simplify this from what is generated here.
+ # 'See docs on artifact collection here https://circleci.com/docs/2.0/artifacts/'
+ - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
+ # Dependencies
+ # This would typically go in either a build or a build-and-test job when using workflows
+ # Restore the dependency cache
+ - restore_cache:
+ keys:
+ # This branch if available
+ - v1-dep-{{ .Branch }}-
+ # Default branch if not
+ - v1-dep-master-
+ # Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly
+ - v1-dep-
+ # This is based on your 1.0 configuration file or project settings
+ - run:
+ command: |-
+ # SauceConnect: download if not cached, and launch with retry
+ test $SAUCE_USERNAME && test $SAUCE_ACCESS_KEY || {
+ echo 'Sauce Labs credentials required. Sign up here: https://saucelabs.com/opensauce/'
+ exit 1
+ }
+
+ mkdir -p ~/sauce-connect
+ cd ~/sauce-connect
+
+ if [ -x sc-*-linux/bin/sc ]; then
+ echo Using cached sc-*-linux/bin/sc
+ else
+ time wget https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz
+ time tar -xzf sc-4.5.4-linux.tar.gz
+ fi
+
+ time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \
+ --readyfile ~/sauce_is_ready
+ test -e ~/sauce_was_ready && exit
+
+ echo 'Sauce Connect failed, try redownloading (https://git.io/vSxsJ)'
+ rm -rf *
+ time wget https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz
+ time tar -xzf sc-4.5.4-linux.tar.gz
+
+ time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \
+ --readyfile ~/sauce_is_ready
+ test -e ~/sauce_was_ready && exit
+
+ echo 'ERROR: Exited twice without creating readyfile' \
+ | tee /dev/stderr > ~/sauce_is_ready
+ exit 1
+ background: true
+ # The following line was run implicitly in your 1.0 builds based on what CircleCI inferred about the structure of your project. In 2.0 you need to be explicit about which commands should be run. In some cases you can discard inferred commands if they are not relevant to your project.
+ - run: if [ -z "${NODE_ENV:-}" ]; then export NODE_ENV=test; fi
+ - run: export PATH="~/mathquill/mathquill/node_modules/.bin:$PATH"
+ - run: npm install
+ # Save dependency cache
+ - save_cache:
+ key: v1-dep-{{ .Branch }}-{{ epoch }}
+ paths:
+ # This is a broad list of cache paths to include many possible development environments
+ # You can probably delete some of these entries
+ - vendor/bundle
+ - ~/virtualenvs
+ - ~/.m2
+ - ~/.ivy2
+ - ~/.bundle
+ - ~/.go_workspace
+ - ~/.gradle
+ - ~/.cache/bower
+ # These cache paths were specified in the 1.0 config
+ - ~/sauce-connect
+ - ./node_modules
+ # Test
+ # This would typically be a build job when using workflows, possibly combined with build
+ # This is based on your 1.0 configuration file or project settings
+ - run: |-
+ # Generate link to Many-Worlds build and add to GitHub Commit Status
+ curl -i -X POST https://api.github.com/repos/mathquill/mathquill/statuses/$CIRCLE_SHA1 \
+ -u MathQuillBot:$GITHUB_STATUS_API_KEY \
+ -d '{
+ "context": "ci/many-worlds",
+ "state": "success",
+ "description": "Try the tests on the Many-Worlds build of this commit:",
+ "target_url": "http://many-worlds.glitch.me/mathquill/mathquill/commit/'$CIRCLE_SHA1'/test/"
+ }'
+ # Safari on Sauce can only connect to port 3000, 4000, 7000, or 8000. Edge needs port 7000 or 8000.
+ # https://david263a.wordpress.com/2015/04/18/fixing-safari-cant-connect-to-localhost-issue-when-using-sauce-labs-connect-tunnel/
+ # https://support.saucelabs.com/customer/portal/questions/14368823-requests-to-localhost-on-microsoft-edge-are-failing-over-sauce-connect
+ - run:
+ command: PORT=8000 make server
+ background: true
+ # Wait for tunnel to be ready (`make server` is much faster, no need to wait for it)
+ - run: while [ ! -e ~/sauce_is_ready ]; do sleep 1; done; touch ~/sauce_was_ready; test -z "$(<~/sauce_is_ready)"
+ # This is based on your 1.0 configuration file or project settings
+ - run:
+ command: |-
+ # Screenshots: capture in the background while running unit tests
+ mkdir -p $CIRCLE_TEST_REPORTS/mocha
+
+ # CircleCI expects test results to be reported in an JUnit/xUnit-style XML file:
+ # https://circleci.com/docs/test-metadata/#a-namemochajsamocha-for-nodejs
+ # Our unit tests are in a browser, so they can't write to a file, and Sauce
+ # apparently truncates custom data in their test result reports, so instead we
+ # POST to this trivial Node server on localhost:9000 that writes the body of
+ # any POST request to $CIRCLE_TEST_REPORTS/junit/test-results.xml
+ node -e '
+ require("http").createServer(function(req, res) {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ req.pipe(process.stdout);
+ req.on("end", res.end.bind(res));
+ })
+ .listen(9000);
+ console.error("listening on http://0.0.0.0:9000/");
+ ' 2>&1 >$CIRCLE_TEST_REPORTS/junit/test-results.xml | {
+ # ^ note: `2>&1` must precede `>$CIRCLE_TEST_REPORTS/...` because
+ # shell redirect is like assignment; if it came after, then both
+ # stdout and stderr would be written to `xunit.xml` and nothing
+ # would be piped into here
+
+ head -1 # wait for "listening on ..." to be logged
+
+ # https://circleci.com/docs/environment-variables/
+ build_name="CircleCI build #$CIRCLE_BUILD_NUM"
+ if [ $CIRCLE_PR_NUMBER ]; then
+ build_name="$build_name: PR #$CIRCLE_PR_NUMBER"
+ [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)"
+ else
+ build_name="$build_name: $CIRCLE_BRANCH"
+ fi
+ build_name="$build_name @ ${CIRCLE_SHA1:0:7}"
+ export MQ_CI_BUILD_NAME="$build_name"
+
+ time { test -d node_modules/wd || npm install wd; }
+ time node script/screenshots.js http://localhost:8000/test/visual.html \
+ && touch ~/screenshots_are_ready || echo EXIT STATUS $? | tee /dev/stderr > ~/screenshots_are_ready:
+ }
+ background: true
+ - run: |-
+ # Unit tests in the browser
+
+ echo '1. Launch tests'
+ echo
+
+ # https://circleci.com/docs/environment-variables/
+ build_name="CircleCI build #$CIRCLE_BUILD_NUM"
+ if [ $CIRCLE_PR_NUMBER ]; then
+ build_name="$build_name: PR #$CIRCLE_PR_NUMBER"
+ [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)"
+ else
+ build_name="$build_name: $CIRCLE_BRANCH"
+ fi
+ build_name="$build_name @ ${CIRCLE_SHA1:0:7}"
+
+ # "build" and "customData" parameters from:
+ # https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-TestAnnotation
+ set -o pipefail
+ curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests \
+ -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "name": "Unit tests, Mocha",
+ "build": "'"$build_name"'",
+ "customData": {"build_url": "'"$CIRCLE_BUILD_URL"'"},
+ "framework": "mocha",
+ "url": "http://localhost:8000/test/unit.html?post_xunit_to=http://localhost:9000",
+ "platforms": [["", "Chrome", ""]]
+ }' \
+ | tee /dev/stderr | tail -1 > js-tests.json
+
+ echo '2. Wait for tests to finish:'
+ echo
+ # > Make the request multiple times as the tests run until the response
+ # > contains `completed: true` to the get the final results.
+ # https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods
+ while true # Bash has no do...while >:(
+ do
+ sleep 5
+ curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests/status \
+ -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \
+ -H 'Content-Type: application/json' \
+ -d @js-tests.json \
+ | tee /dev/stderr | tail -1 > status.json
+
+ # deliberately do `... != false` rather than `... == true`
+ # because unexpected values should break rather than infinite loop
+ [ "$(jq .completed HTML
+
+```
+```js
+mathfield.revert().html(); // => 'some HTML'
+```
+
+## .reflow()
+
+MathQuill uses computed dimensions, so if they change (because an element was mathquill-ified before it was in the visible HTML DOM, or the font size changed), then you'll need to tell MathQuill to recompute:
+
+```js
+var mathFieldSpan = $('\\sqrt{2}');
+var mathField = MQ.MathField(mathFieldSpan[0]);
+mathFieldSpan.appendTo(document.body);
+mathField.reflow();
+```
+
+## .el()
+
+Returns the root HTML element.
+
+## .latex()
+
+Returns the contents as LaTeX.
+
+## .latex(latex_string)
+
+This will render the argument as LaTeX in the MathQuill instance.
+
+
+
+# Editable MathField methods
+
+Editable math fields have all of the [above](#mathquill-base-methods) methods in addition to the ones listed here.
+
+## .focus()
+
+Puts the focus on the editable field.
+
+## .blur()
+
+Removes focus from the editable field.
+
+## .write(latex_string)
+
+Write the given LaTeX at the current cursor position. If the cursor does not have focus, writes to last position the cursor occupied in the editable field.
+
+```javascript
+mathField.write(' - 1'); // writes ' - 1' to mathField at the cursor position
+```
+
+## .cmd(latex_string)
+
+Enter a LaTeX command at the current cursor position or with the current selection. If the cursor does not have focus, it writes it to last position the cursor occupied in the editable field.
+
+```javascript
+mathField.cmd('\\sqrt'); // writes a square root command at the cursor position
+```
+
+## .select()
+
+Selects the contents (just like [on `textarea`s](http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-48880622) and [on `input`s](http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-34677168)).
+
+## .clearSelection()
+
+Clears the selection.
+
+## .moveToLeftEnd(), .moveToRightEnd()
+
+Move the cursor to the left/right end of the editable field, respectively. These are shorthand for [`.moveToDirEnd(L/R)`](#movetodirenddirection), respectively.
+
+## .moveToDirEnd(direction)
+
+Moves the cursor to the end of the mathfield in the direction specified. The direction can be one of `MQ.L` or `MQ.R`. These are constants, where `MQ.L === -MQ.R` and vice versa. This function may be easier to use than [moveToLeftEnd or moveToRightEnd](#movetoleftend-movetorightend) if used in the [`moveOutOf` handler](Config.md#outof-handlers).
+
+```javascript
+var config = {
+ handlers: {
+ moveOutOf: function(direction) {
+ nextMathFieldOver.movetoDirEnd(-direction);
+ }
+ }
+});
+```
+
+## .keystroke(keys)
+
+Simulates keystrokes given a string like `"Ctrl-Home Del"`, a whitespace-delimited list of [key inputs](http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes) with optional prefixes.
+
+```javascript
+mathField.keystroke('Shift-Left'); // Selects character before the current cursor position
+```
+
+## .typedText(text)
+
+Simulates typing text, one character at a time from where the cursor currently is. This is supposed to be identical to what would happen if a user were typing the text in.
+
+```javascript
+// Types part of the demo from mathquill.com without delays between keystrokes
+mathField.typedText('x=-b\\pm \\sqrt b^2 -4ac');
+```
+
+## .setAriaLabel(ariaLabel)
+
+Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'`
+
+## .getAriaLabel()
+
+Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned.
+
+## .setAriaPostLabel(ariaPostLabel, timeout)
+
+Specify a suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. Default: `''` (empty string)
+
+If a timeout (in ms) is supplied, and the math field has keyboard focus when the time has elapsed, an ARIA alert will fire which will cause a screen reader to read the content of the field along with the ARIA post-label. This is useful if the post-label contains an evaluation, error message, or other text that the user needs to know about.
+
+## .getAriaPostLabel()
+
+Returns the suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. If no ARIA post-label has been specified, `''` (empty string) is returned.
+
+[`aria-label`]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute
+
+## .config(new_config)
+
+Changes the [configuration](Config.md) of just this math field.
+
+## .dropEmbedded(pageX, pageY, options) **[ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ](#note-on-experimental-features)**
+
+Insert a custom embedded element at the given coordinates, where `options` is an object like:
+```js
+{
+ htmlString: '',
+ text: function() { return 'custom_embed'; },
+ latex: function() { return '\\customEmbed'; }
+}
+```
+
+## .registerEmbed('name', function(id){ return options; }) **[ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ](#note-on-experimental-features)**
+
+Allows MathQuill to parse custom embedded objects from latex, where `options` is an object like the one defined above in `.dropEmbedded()`. This will parse the following latex into the embedded object you defined: `\embed{name}[id]}`.
+
+## Note on Experimental Features
+
+Methods marked as experimental may be altered drastically or removed in future versions. They may also receive less maintenance than other non-experimental features.
+
+# Inner MathField methods
+
+Inner math fields have all of the [above](#editable-mathfield-methods) methods in addition to the ones listed here.
+
+## makeStatic()
+
+Converts the editable inner field into a static one.
+
+## makeEditable()
+
+Converts the static inner field into an editable one.
diff --git a/docs/Code_of_Conduct.md b/docs/Code_of_Conduct.md
new file mode 100644
index 000000000..7d01333ce
--- /dev/null
+++ b/docs/Code_of_Conduct.md
@@ -0,0 +1,21 @@
+## Code of Conduct
+
+### Quick Version
+
+The MathQuill project and its supporting communication channels, including GitHub, the MathQuill Slack, and the #mathquill IRC channel, are dedicated to providing a harassment-free experience for everyone, regardless of gender, gender identity and expression, sexual orientation, disability, physical appearance, age, body size, race, or religion. We do not tolerate harassment of participants in any form. Sexual language and imagery is not appropriate without discussion and pre-approval from a moderator. Participants violating these rules may be sanctioned or expelled from the group at the discretion of [@laughinghan], [@stufflebear], or another moderator.
+
+### Additional Details
+Harassment includes offensive verbal comments related to gender, gender identity and expression, sexual orientation, disability, physical appearance, age, body size, race, religion, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, sustained disruption of discussion, inappropriate contact, and unwelcome sexual attention. Disclosure of another person’s contact information including legal name or residence without their permission is also unacceptable behavior.
+
+Participants asked to stop any harassing or inappropriate behavior are expected to comply immediately.
+
+If a participant engages in harassing behavior, [@laughinghan], [@stufflebear], or another moderator may take any action they deem appropriate, including warning the offender or permanent expulsion from the channel.
+
+If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact us at \ . Unlike loose usage in the LaTeX community, where `\ne` and `\neq` might or might not be considered the same command, in the context of MathQuill they are considered different "control sequences" for the same "command".
+
+## Service
+
+A **service** is a feature that applies to all or many actions, such as typing, moving the cursor around, LaTeX exporting, or LaTeX parsing. Each of these actions vary by command. For example, the cursor goes in a different place when moving into a fraction vs into a square root and they each export different LaTeX.
+
+Services define methods on the controller that call methods on nodes in the edit tree with certain contracts, such as a controller method called on initialization to set listeners for keyboard events, that when the Left key is pressed, calls `.moveTowards` on the node just left of the cursor, dispatching on what kind of command the node is (`Fraction::moveTowards` and `SquareRoot::moveTowards` can insert the cursor in different places).
+
+[`controller.js`](https://github.com/mathquill/mathquill/blob/master/src/controller.js) defines the base class for the **controller**, which each math field or static math instance has one of, and to which each service adds methods.
+
+## API
+
+[`publicapi.js`](https://github.com/mathquill/mathquill/blob/master/src/publicapi.js) defines the global `MathQuill.getInterface()` function, the mathField constructors, and the API objects returned by them. The constructors, and the API methods on the objects they return, call appropriate controller methods to initialize and manipulate math field and static math instances.
+
+## Other Components
+
+[`services/*.util.js`](https://github.com/mathquill/mathquill/tree/master/src/services) files are unimportant to the overall architecture. You can largely ignore them until you have to deal with code that is using them.
+
+[`intro.js`](https://github.com/mathquill/mathquill/blob/master/src/intro.js) defines some simple sugar for the idiomatic JS classes used throughout MathQuill, plus some globals and opening boilerplate.
+
+## Conventions
+
+Classes are defined using [Pjs](https://github.com/jneen/pjs), and the variable `_` is used by convention as the prototype.
+
+In comments and internal documentation, `::` means `.prototype.`.
diff --git a/docs/Getting_Started.md b/docs/Getting_Started.md
new file mode 100644
index 000000000..978184132
--- /dev/null
+++ b/docs/Getting_Started.md
@@ -0,0 +1,63 @@
+# Download and Load
+
+Download [the latest release](https://github.com/mathquill/mathquill/releases/latest) or [build from source](Contributing.md#building-and-testing).
+
+MathQuill depends on [jQuery 1.5.2+](http://jquery.com), we recommend the [Google CDN-hosted copy](http://code.google.com/apis/libraries/devguide.html#jquery).
+
+Load MathQuill with something like (order matters):
+```html
+
+
+
+
+```
+
+Now you can call our [API methods](Api_Methods.md) on `MQ`.
+
+# Basic Usage
+
+MathQuill instances are created from HTML elements. For the full list of constructors and API methods, see [API Methods](Api_Methods.md).
+
+## Static Math Rendering
+
+To statically render a formula, call [`MQ.StaticMath()`](Api_Methods.md#mqstaticmathhtml_element) on an HTML element:
+```html
+Solve ax^2 + bx + c = 0.
+ + +``` + +## Editable Math Fields + +To create an editable math field, call [`MQ.MathField()`](Api_Methods.md#mqmathfieldhtml_element-config) on an HTML element and, optionally, a [config options object](Config.md). The following example features a math field with a handler to check the answer every time an edit may have occurred: +```html +x=
+ + +``` + +## Get and Set Math + +To get and set the contents of a math field, use [`mathField.latex()`](Api_Methods.md#latex). + +Math fields are initialized with the text that was in the span, parsed as LaTeX. This can be updated by calling [`mathField.latex(latexString)`](Api_Methods.md#latexlatex_string). To programmatically type text into a math field, use [`.typedText(string)`](Api_Methods.md#typedtexttext), + +# Join the Community + +[ font-size
+ much too small at 75%, override */
+body code {
+ font-size: 90%;
+}
+body pre code {
+ font-size: 100%;
+}
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 000000000..32d46ee88
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..870730bae
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,23 @@
+site_name: MathQuill
+site_url: http://docs.mathquill.com
+repo_url: https://github.com/mathquill/mathquill
+site_description: Easily type math in your webapp
+site_author: Han, Jeanine, Mary
+site_favicon: http://mathquill.com/favicon.ico
+google_analytics: ['UA-73742753-3', 'docs.mathquill.com']
+
+pages:
+ - index.md
+ - Getting_Started.md
+ - 'API Methods': Api_Methods.md
+ - 'Config Options': Config.md
+ - 'Under the Hood': Contributing.md
+ - Code_of_Conduct.md
+
+markdown_extensions:
+ - toc:
+ permalink: True
+
+theme: readthedocs
+extra_css:
+ - extra.css
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..489f69df9
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,906 @@
+{
+ "name": "mathquill",
+ "version": "0.10.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "ajv": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+ "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "co": "^4.6.0",
+ "json-stable-stringify": "^1.0.1"
+ }
+ },
+ "align-text": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2",
+ "longest": "^1.0.1",
+ "repeat-string": "^1.5.2"
+ }
+ },
+ "asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+ "dev": true,
+ "optional": true
+ },
+ "asn1": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
+ "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
+ "dev": true,
+ "optional": true
+ },
+ "assert-plus": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
+ "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
+ "dev": true,
+ "optional": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true,
+ "optional": true
+ },
+ "aws-sign2": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
+ "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
+ "dev": true,
+ "optional": true
+ },
+ "aws4": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
+ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=",
+ "dev": true,
+ "optional": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
+ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "boom": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
+ "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "hoek": "2.x.x"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
+ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+ "dev": true
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+ "dev": true,
+ "optional": true
+ },
+ "center-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+ "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+ "dev": true,
+ "requires": {
+ "align-text": "^0.1.3",
+ "lazy-cache": "^1.0.3"
+ }
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "dev": true,
+ "requires": {
+ "center-align": "^0.1.1",
+ "right-align": "^0.1.1",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+ "dev": true,
+ "optional": true
+ },
+ "combined-stream": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+ "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true,
+ "optional": true
+ },
+ "cryptiles": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
+ "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "boom": "2.x.x"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true,
+ "optional": true
+ },
+ "diff": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
+ "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
+ "dev": true
+ },
+ "ecc-jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0"
+ }
+ },
+ "errno": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "extend": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
+ "dev": true,
+ "optional": true
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+ "dev": true,
+ "optional": true
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+ "dev": true,
+ "optional": true
+ },
+ "form-data": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
+ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.5",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+ "dev": true,
+ "optional": true
+ },
+ "growl": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
+ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
+ "dev": true
+ },
+ "har-schema": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
+ "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=",
+ "dev": true,
+ "optional": true
+ },
+ "har-validator": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
+ "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ajv": "^4.9.1",
+ "har-schema": "^1.0.5"
+ }
+ },
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "dev": true
+ },
+ "hawk": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+ "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "boom": "2.x.x",
+ "cryptiles": "2.x.x",
+ "hoek": "2.x.x",
+ "sntp": "1.x.x"
+ }
+ },
+ "he": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+ "dev": true
+ },
+ "hoek": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
+ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+ "dev": true,
+ "optional": true
+ },
+ "http-signature": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+ "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "assert-plus": "^0.2.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
+ "dev": true,
+ "optional": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+ "dev": true,
+ "optional": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+ "dev": true,
+ "optional": true
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "dev": true,
+ "optional": true
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "dev": true,
+ "optional": true
+ },
+ "json-stable-stringify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+ "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "jsonify": "~0.0.0"
+ }
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+ "dev": true,
+ "optional": true
+ },
+ "jsonify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+ "dev": true,
+ "optional": true
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ },
+ "lazy-cache": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
+ "dev": true
+ },
+ "less": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz",
+ "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==",
+ "dev": true,
+ "requires": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "mime": "^1.2.11",
+ "mkdirp": "^0.5.0",
+ "promise": "^7.1.1",
+ "request": "2.81.0",
+ "source-map": "^0.5.3"
+ }
+ },
+ "longest": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+ "dev": true
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "optional": true
+ },
+ "mime-db": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+ "dev": true,
+ "optional": true
+ },
+ "mime-types": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+ "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "mime-db": "~1.33.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "mocha": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz",
+ "integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==",
+ "dev": true,
+ "requires": {
+ "browser-stdout": "1.3.0",
+ "commander": "2.11.0",
+ "debug": "3.1.0",
+ "diff": "3.3.1",
+ "escape-string-regexp": "1.0.5",
+ "glob": "7.1.2",
+ "growl": "1.10.3",
+ "he": "1.1.1",
+ "mkdirp": "0.5.1",
+ "supports-color": "4.4.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "oauth-sign": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+ "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "performance-now": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
+ "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=",
+ "dev": true,
+ "optional": true
+ },
+ "pjs": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pjs/-/pjs-4.0.0.tgz",
+ "integrity": "sha1-aMp9me0z1KZSuLe0P5lvOR71Efk=",
+ "dev": true
+ },
+ "promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "asap": "~2.0.3"
+ }
+ },
+ "prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+ "dev": true,
+ "optional": true
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+ "dev": true,
+ "optional": true
+ },
+ "qs": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
+ "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
+ "dev": true,
+ "optional": true
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true
+ },
+ "request": {
+ "version": "2.81.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
+ "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aws-sign2": "~0.6.0",
+ "aws4": "^1.2.1",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.5",
+ "extend": "~3.0.0",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.1.1",
+ "har-validator": "~4.2.1",
+ "hawk": "~3.1.3",
+ "http-signature": "~1.1.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.7",
+ "oauth-sign": "~0.8.1",
+ "performance-now": "^0.2.0",
+ "qs": "~6.4.0",
+ "safe-buffer": "^5.0.1",
+ "stringstream": "~0.0.4",
+ "tough-cookie": "~2.3.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.0.0"
+ }
+ },
+ "right-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+ "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+ "dev": true,
+ "requires": {
+ "align-text": "^0.1.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+ "dev": true,
+ "optional": true
+ },
+ "sntp": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
+ "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "hoek": "2.x.x"
+ }
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "sshpk": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
+ "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "tweetnacl": "~0.14.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "stringstream": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
+ "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
+ "dev": true,
+ "optional": true
+ },
+ "supports-color": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
+ "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^2.0.0"
+ }
+ },
+ "tough-cookie": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+ "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "punycode": "^1.4.1"
+ }
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "dev": true,
+ "optional": true
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.5.1",
+ "uglify-to-browserify": "~1.0.0",
+ "yargs": "~3.10.0"
+ }
+ },
+ "uglify-to-browserify": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+ "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+ "dev": true,
+ "optional": true
+ },
+ "uuid": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
+ "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
+ "dev": true,
+ "optional": true
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "window-size": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+ "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+ "dev": true
+ },
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^1.0.2",
+ "cliui": "^2.1.0",
+ "decamelize": "^1.0.0",
+ "window-size": "0.1.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 703e3ecb8..475a7c727 100644
--- a/package.json
+++ b/package.json
@@ -1,18 +1,21 @@
{
"name": "mathquill",
"description": "Easily type math in your webapp",
- "version": "0.10.0",
+ "version": "0.10.1",
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "https://github.com/mathquill/mathquill.git"
},
- "dependencies": {
- "pjs": "3.x"
- },
+ "files": [
+ "build/mathquill.{css,js,min.js}",
+ "build/fonts/Symbola.*",
+ "quickstart.html"
+ ],
"devDependencies": {
- "mocha": "*",
- "uglify-js": "2.x",
- "less": ">=1.5.1"
+ "less": ">=1.5.1 <3.0.0",
+ "mocha": ">=2.4.1",
+ "pjs": ">=3.1.0 <5.0.0",
+ "uglify-js": "2.x"
}
}
diff --git a/quickstart.html b/quickstart.html
new file mode 100644
index 000000000..0bb332769
--- /dev/null
+++ b/quickstart.html
@@ -0,0 +1,40 @@
+
+
+
+ MathQuill Quickstart
+
+
+
+ Static math span: x = \frac{ -b \pm \sqrt{b^2-4ac} }{ 2a }
+
Editable math field: x^2
+ LaTeX of what you typed: x^2
+ MathQuill’s Getting
+ Started Guide
+
+
+
+
+
+
diff --git a/script/prep-release.sh b/script/prep-release.sh
new file mode 100755
index 000000000..1f0d2bffa
--- /dev/null
+++ b/script/prep-release.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+set -e -o pipefail
+die () { printf '\n\tERROR: %s\n\n' "$*"; exit 1; }
+
+#
+# -1. Old versions of npm omit random files due to race condition https://git.io/vooV3
+#
+equalOrNewer () { # inspired by http://stackoverflow.com/a/25731924/362030
+ printf '%s\n%s\n' "$@" | sort -cnrt . -k 1,1 -k 2,2 -k 3,3 2>/dev/null
+}
+npm_v="$(npm -v)"
+if echo "$npm_v" | grep -q '^2\.'; then
+ equalOrNewer "$npm_v" 2.15.8 \
+ || die 'Your npm@2 version must be >=2.15.8, see https://git.io/vooV3'
+else
+ equalOrNewer "$npm_v" 3.10.1 \
+ || die 'Your npm@3 version must be >=3.10.1, see https://git.io/vooV3'
+fi
+
+#
+# 0. Clean tree & repo state except for CHANGELOG
+#
+files="$(git diff --name-only HEAD)"
+test "$files" \
+ || { echo 'First, you must add an entry to CHANGELOG.md'; exit 1; }
+test "$files" = CHANGELOG.md \
+ || die 'You have uncommitted changes other than to CHANGELOG.md'
+test "$(git rev-parse --abbrev-ref HEAD)" = master \
+ || die 'You must be on master'
+test "$(git rev-list --count @{upstream}..)" = 0 \
+ || test "$1" = --allow-unpushed-commits \
+ || die "You have unpushed commits (do $0 --allow-unpushed-commits to continue anyway)"
+
+#
+# 1. Bump package.json version
+#
+change_summary="$(git diff HEAD | grep '^+' | sed -n '2 s/^+## // p')"
+version="$(echo "$change_summary" | sed 's/:.*//')"
+git cat-file -e "$version" 2>/dev/null \
+ && die "$version already exists"
+npm version "$version" --no-git-tag-version >/dev/null
+echo "1. Bumped package.json version to \""$(node -p 'require("./package.json").version')"\""
+
+#
+# 2. Build
+#
+echo '2. make:'
+make 2>&1 | sed 's/^/ /'
+
+#
+# 3. Package as tarball + zipfile
+#
+tarball=$(npm pack) # create tarball
+tar -xzf $tarball # extract tarball as package/
+zipfile=${tarball%.tgz}.zip
+zip -qrX $zipfile package # create zipfile from package/
+echo "3. Collected release files into package/, packed as $tarball and $zipfile"
+
+#
+# 4. Commit
+#
+git add CHANGELOG.md package.json
+git commit -m "$change_summary" | sed '1 s/^/4. Committed: /; 2,$ s/^/ /'
+
+#
+# 5. Record shrinkwrap
+#
+npm shrinkwrap --dev | sed 's/^/5. /'
+shrinkwrap="$(/dev/null \
+ || die 'No tarball or zipfile, first run script/prep-release.sh'
+
+test "$GITHUB_ACCESS_TOKEN" || {
+ echo
+ echo ' ERROR: No $GITHUB_ACCESS_TOKEN defined.'
+ echo
+ echo 'This script needs an access token to create GitHub Releases.'
+ echo 'Follow these instructions to create a token authorized for the "repo" scope:'
+ echo ' https://help.github.com/articles/creating-an-access-token-for-command-line-use/'
+ echo 'Then do:'
+ echo " GITHUB_ACCESS_TOKEN= $0"
+ exit 1
+}
+
+#
+# 1. npm publish
+#
+npm publish $tarball
+
+#
+# 2. git push, with tag
+#
+git push origin master tag $tagname
+
+#
+# 3. Create GitHub Release
+#
+changelog_entry="$(git show CHANGELOG.md | grep '^+' | sed -n '2,$ s/^+// p')"
+json="$(
+ tagname=$tagname \
+ summary="$(echo "$changelog_entry" | sed -n '1 s/^## // p')" \
+ body="$(echo "$changelog_entry" | tail +5)" \
+ node -p 'JSON.stringify({
+ tag_name: process.env.tagname,
+ name: process.env.summary,
+ body: process.env.body
+ })'
+)"
+
+endpoint='https://api.github.com/repos/mathquill/mathquill/releases'
+release_response="$(curl -s "$endpoint" -d "$json" \
+ -H "Authorization: token $GITHUB_ACCESS_TOKEN")"
+upload_url="$(response="$release_response" \
+ node -p 'JSON.parse(process.env.response).upload_url' | sed 's/{.*}$//')"
+
+cat $tarball | curl "$upload_url?name=$tarball" --data-binary @- \
+ -H 'Content-Type: application/x-gzip' -H "Authorization: token $GITHUB_ACCESS_TOKEN"
+cat $zipfile | curl "$upload_url?name=$zipfile" --data-binary @- \
+ -H 'Content-Type: application/zip' -H "Authorization: token $GITHUB_ACCESS_TOKEN"
+
+#
+# 4. Cleanup
+#
+rm -rf package $tarball $zipfile
diff --git a/script/screenshots.js b/script/screenshots.js
new file mode 100644
index 000000000..547669b5f
--- /dev/null
+++ b/script/screenshots.js
@@ -0,0 +1,206 @@
+// This script assumes the following:
+// 1. You've installed wd with `npm install wd'.
+// 2. You've set the environment variables $SAUCE_USERNAME and $SAUCE_ACCESS_KEY.
+// 3. If the environment variable $CIRCLE_ARTIFACTS is not set images will be saved in /tmp
+//
+// This scripts creates following files for each browser in browserVersions:
+// $CIRCLE_ARTIFACTS/imgs/{browser_version_platform}/#.png
+//
+// The intention of this script is that it will be ran from CircleCI
+//
+// Example usage:
+// node screenshots.js http://localhost:9292/test/visual.html
+// node screenshots.js http://google.com
+
+var wd = require('wd');
+var fs = require('fs');
+var url = process.argv[2];
+var username = process.env.SAUCE_USERNAME;
+var accessKey = process.env.SAUCE_ACCESS_KEY;
+var build_name = process.env.MQ_CI_BUILD_NAME;
+var baseDir = process.env.CIRCLE_ARTIFACTS;
+if (!baseDir) {
+ console.error('No $CIRCLE_ARTIFACTS found, for testing do something like `CIRCLE_ARTIFACTS=/tmp script/screenshots.js`');
+ process.exit(1);
+}
+fs.mkdirSync(baseDir+'/imgs');
+fs.mkdirSync(baseDir+'/imgs/pieces');
+fs.mkdirSync(baseDir+'/browser_logs');
+
+var browsers = [
+ {
+ config: {
+ browserName: 'Internet Explorer',
+ platform: 'Windows XP'
+ },
+ pinned: true // assume pinned to IE 8
+ },
+ {
+ config: {
+ browserName: 'Internet Explorer',
+ platform: 'Windows 7'
+ },
+ pinned: true // assume pinned to IE 11
+ },
+ {
+ config: {
+ browserName: 'MicrosoftEdge',
+ platform: 'Windows 10'
+ }
+ },
+ {
+ config: {
+ browserName: 'Firefox',
+ platform: 'OS X 10.11'
+ }
+ },
+ {
+ config: {
+ browserName: 'Safari',
+ platform: 'OS X 10.11'
+ }
+ },
+ {
+ config: {
+ browserName: 'Chrome',
+ platform: 'OS X 10.11'
+ }
+ },
+ {
+ config: {
+ browserName: 'Firefox',
+ platform: 'Linux'
+ }
+ }
+];
+
+
+browsers.forEach(function(browser) {
+ browser.config.build = build_name;
+ browser.config.name = 'Visual tests, ' + browser.config.browserName + ' on ' + browser.config.platform;
+ browser.config.customData = {build_url: process.env.CIRCLE_BUILD_URL};
+ var browserDriver = wd.promiseChainRemote('ondemand.saucelabs.com', 80, username, accessKey);
+ return browserDriver.init(browser.config)
+ .then(function(args) {
+ var cfg = browser.config, capabilities = args[1];
+ var version = capabilities.version || capabilities.browserVersion;
+ var sessionName = [cfg.browserName, version, cfg.platform].join(' ');
+ if (capabilities.platformVersion) sessionName += ' ' + capabilities.platformVersion;
+ console.log(sessionName, 'init', args);
+
+ var evergreen = browser.pinned ? '' : '_(evergreen)';
+ var fileName = [cfg.browserName, version + evergreen, cfg.platform].join('_');
+ if (capabilities.platformVersion) fileName += ' ' + capabilities.platformVersion;
+ fileName = fileName.replace(/ /g, '_');
+
+ return browserDriver.get(url)
+ .then(willLog(sessionName, 'get'))
+ .safeExecute('document.body.focus()') // blur anything that's auto-focused
+ .then(willLog(sessionName, 'document.body.focus()'))
+ .safeExecute('document.documentElement.style.overflow = "hidden"') // hide scrollbars
+ .then(willLog(sessionName, 'hide scrollbars'))
+ .then(function() {
+ // Microsoft Edge starts out with illegally big window: https://git.io/vD63O
+ if (cfg.browserName === 'MicrosoftEdge') {
+ return browserDriver.getWindowSize()
+ .then(function(size) {
+ return browserDriver.setWindowSize(size.width, size.height)
+ })
+ .then(willLog(sessionName, 'reset window size (Edge-only workaround)'))
+ }
+ })
+ .then(function() {
+ return [browserDriver.safeExecute('document.documentElement.scrollHeight'),
+ browserDriver.safeExecute('document.documentElement.clientHeight')];
+ })
+ .spread(function(scrollHeight, viewportHeight) {
+ console.log(sessionName, 'get scrollHeight, clientHeight', scrollHeight, viewportHeight);
+
+ // the easy case: IE and Firefox on Linux return a screenshot of the entire webpage
+ if (cfg.browserName === 'Internet Explorer'|| (cfg.browserName === 'Firefox' && cfg.platform === 'Linux')) {
+ return browserDriver.saveScreenshot(baseDir + '/imgs/' + fileName + '.png')
+ .then(willLog(sessionName, 'saveScreenshot'))
+ // the hard case: for Chrome, Safari, and Edge, scroll through the page and
+ // take screenshots of each piece; circle.yml will stitch them together
+ } else {
+ var piecesDir = baseDir + '/imgs/pieces/' + fileName + '/';
+ fs.mkdirSync(piecesDir);
+
+ var scrollTop = 0;
+ var index = 1;
+
+ return (function loop() {
+ return browserDriver.safeEval('window.scrollTo(0,'+scrollTop+');')
+ .then(willLog(sessionName, 'scrollTo()'))
+ .saveScreenshot(piecesDir + index + '.png')
+ .then(function() {
+ console.log(sessionName, 'saveScreenshot');
+
+ scrollTop += viewportHeight;
+ index += 1;
+
+ // if the viewport hasn't passed the bottom edge of the page yet,
+ // scroll down and take another screenshot
+ if (scrollTop + viewportHeight <= scrollHeight) {
+ // Use `window.scrollTo` because thats what jQuery does:
+ // https://github.com/jquery/jquery/blob/1.12.3/src/offset.js#L186
+ // Use `window.scrollTo` instead of jQuery because jQuery was
+ // causing a stackoverflow in Safari.
+ return loop();
+ } else { // we are past the bottom edge of the page, reduce window size to
+ // fit only the part of the page that hasn't been screenshotted.
+
+ // If there is no remaining part of the page, we're done, short-circuit
+ if (scrollTop === scrollHeight) return browserDriver;
+
+ return browserDriver.getWindowSize()
+ .then(function(windowSize) {
+ console.log(sessionName, 'getWindowSize');
+ // window size is a little bigger than the viewport because of address
+ // bar and scrollbars and stuff
+ var windowPadding = windowSize.height - viewportHeight;
+ var newWindowHeight = scrollHeight - scrollTop + windowPadding;
+ return browserDriver.setWindowSize(windowSize.width, newWindowHeight)
+ .then(willLog(sessionName, 'setWindowSize'))
+ .safeEval('window.scrollTo(0,'+scrollHeight+');')
+ .then(willLog(sessionName, 'scrollTo() Final'))
+ .saveScreenshot(piecesDir + index + '.png')
+ .then(willLog(sessionName, 'saveScreenshot Final'));
+ });
+ }
+ });
+ }());
+ }
+ })
+ .then(function() {
+ return browserDriver.log('browser')
+ .then(function(logs) {
+ var logfile = baseDir + '/browser_logs/' + sessionName.replace(/ /g, '_') + '.log';
+ return new Promise(function(resolve, reject) {
+ fs.writeFile(logfile, JSON.stringify(logs, null, 2), function(err) {
+ err ? reject(err) : resolve();
+ });
+ })
+ .then(willLog(sessionName, 'writeFile'));
+ }, function(err) {
+ // the Edge, IE, and Firefox-on-macOS drivers don't support logs, but the others do
+ console.log(sessionName, 'Error fetching logs:', JSON.stringify(err, null, 2));
+ });
+ });
+ })
+ .sauceJobStatus(true)
+ .fail(function(err) {
+ console.log('ERROR:', browser.config.browserName, browser.config.platform);
+ console.log(JSON.stringify(err, null, 2));
+ return browserDriver.sauceJobStatus(false);
+ })
+ .quit();
+
+ function willLog() {
+ var msg = [].join.call(arguments, ' ');
+ return function(value) {
+ console.log(msg);
+ return value;
+ };
+ }
+});
diff --git a/script/test_server.js b/script/test_server.js
index a551bae27..b653eb424 100644
--- a/script/test_server.js
+++ b/script/test_server.js
@@ -35,6 +35,8 @@ function serveRequest(req, res) {
}
}
else {
+ var ext = filepath.match(/\.[^.]+$/);
+ if (ext) res.setHeader('Content-Type', 'text/' + ext[0].slice(1));
res.end(data);
}
@@ -82,8 +84,9 @@ function run_make_test() {
if (code) {
console.error('Exit Code ' + code);
} else {
- console.log('\nMathQuill is now running on localhost:9292');
- console.log('Open http://localhost:9292/test/demo.html\n');
+ var serverAddress = HOST === '0.0.0.0' ? 'localhost:' + PORT : HOST + ':' + PORT;
+ console.log('\nMathQuill is now running on ' + serverAddress);
+ console.log('Open http://' + serverAddress + '/test/demo.html\n');
}
for (var i = 0; i < q.length; i += 1) q[i]();
q = undefined;
diff --git a/src/commands/math.js b/src/commands/math.js
index 63341e3c0..48bfd0e8d 100644
--- a/src/commands/math.js
+++ b/src/commands/math.js
@@ -12,19 +12,56 @@ var MathElement = P(Node, function(_, super_) {
// SupSub::contactWeld, and is deliberately only passed in by writeLatex,
// see ea7307eb4fac77c149a11ffdf9a831df85247693
var self = this;
- self.postOrder('finalizeTree', options);
- self.postOrder('contactWeld', cursor);
+ self.postOrder(function (node) { node.finalizeTree(options) });
+ self.postOrder(function (node) { node.contactWeld(cursor) });
// note: this order is important.
// empty elements need the empty box provided by blur to
// be present in order for their dimensions to be measured
// correctly by 'reflow' handlers.
- self.postOrder('blur');
+ self.postOrder(function (node) { node.blur(); });
- self.postOrder('reflow');
+ self.postOrder(function (node) { node.reflow(); });
if (self[R].siblingCreated) self[R].siblingCreated(options, L);
if (self[L].siblingCreated) self[L].siblingCreated(options, R);
- self.bubble('reflow');
+ self.bubble(function (node) { node.reflow(); });
+ };
+ // If the maxDepth option is set, make sure
+ // deeply nested content is truncated. Just return
+ // false if the cursor is already too deep.
+ _.prepareInsertionAt = function(cursor) {
+ var maxDepth = cursor.options.maxDepth;
+ if (maxDepth !== undefined) {
+ var cursorDepth = cursor.depth();
+ if (cursorDepth > maxDepth) {
+ return false;
+ }
+ this.removeNodesDeeperThan(maxDepth-cursorDepth);
+ }
+ return true;
+ };
+ // Remove nodes that are more than `cutoff`
+ // blocks deep from this node.
+ _.removeNodesDeeperThan = function (cutoff) {
+ var depth = 0;
+ var queue = [[this, depth]];
+ var current;
+
+ // Do a breadth-first search of this node's descendants
+ // down to cutoff, removing anything deeper.
+ while (queue.length) {
+ current = queue.shift();
+ current[0].children().each(function (child) {
+ var i = (child instanceof MathBlock) ? 1 : 0;
+ depth = current[1]+i;
+
+ if (depth <= cutoff) {
+ queue.push([child, depth]);
+ } else {
+ (i ? child.children() : child).remove();
+ }
+ });
+ }
};
});
@@ -78,6 +115,8 @@ var MathCommand = P(MathElement, function(_, super_) {
if (replacedFragment) {
replacedFragment.adopt(cmd.ends[L], 0, 0);
replacedFragment.jQ.appendTo(cmd.ends[L].jQ);
+ cmd.placeCursor(cursor);
+ cmd.prepareInsertionAt(cursor);
}
cmd.finalizeInsert(cursor.options);
cmd.placeCursor(cursor);
@@ -106,6 +145,7 @@ var MathCommand = P(MathElement, function(_, super_) {
_.moveTowards = function(dir, cursor, updown) {
var updownInto = updown && this[updown+'Into'];
cursor.insAtDirEnd(-dir, updownInto || this.ends[-dir]);
+ aria.queueDirEndOf(-dir).queue(cursor.parent, true);
};
_.deleteTowards = function(dir, cursor) {
if (this.isEmpty()) cursor[dir] = this.remove()[dir];
@@ -237,17 +277,19 @@ var MathCommand = P(MathElement, function(_, super_) {
pray('no unmatched angle brackets', tokens.join('') === this.htmlTemplate);
- // add cmdId to all top-level tags
+ // add cmdId and aria-hidden (for screen reader users) to all top-level tags
+ // Note: with the RegExp search/replace approach, it's possible that an element which is both a command and block may contain redundant aria-hidden attributes.
+ // In practice this doesn't appear to cause problems for screen readers.
for (var i = 0, token = tokens[0]; token; i += 1, token = tokens[i]) {
// top-level self-closing tags
if (token.slice(-2) === '/>') {
- tokens[i] = token.slice(0,-2) + cmdId + '/>';
+ tokens[i] = token.slice(0,-2) + cmdId + ' aria-hidden="true"/>';
}
// top-level open tags
else if (token.charAt(0) === '<') {
pray('not an unmatched top-level close tag', token.charAt(1) !== '/');
- tokens[i] = token.slice(0,-1) + cmdId + '>';
+ tokens[i] = token.slice(0,-1) + cmdId + ' aria-hidden="true">';
// skip matching top-level close tag and all tag pairs in between
var nesting = 1;
@@ -266,7 +308,7 @@ var MathCommand = P(MathElement, function(_, super_) {
}
}
return tokens.join('').replace(/>&(\d+)/g, function($0, $1) {
- return ' mathquill-block-id=' + blocks[$1].id + '>' + blocks[$1].join('html');
+ return ' mathquill-block-id=' + blocks[$1].id + ' aria-hidden="true">' + blocks[$1].join('html');
});
};
@@ -285,7 +327,15 @@ var MathCommand = P(MathElement, function(_, super_) {
if (text && cmd.textTemplate[i] === '('
&& child_text[0] === '(' && child_text.slice(-1) === ')')
return text + child_text.slice(1, -1) + cmd.textTemplate[i];
- return text + child.text() + (cmd.textTemplate[i] || '');
+ return text + child_text + (cmd.textTemplate[i] || '');
+ });
+ };
+ _.mathspeakTemplate = [];
+ _.mathspeak = function() {
+ var cmd = this, i = 0;
+ return cmd.foldChildren(cmd.mathspeakTemplate[i] || 'Start'+cmd.ctrlSeq+' ', function(speech, block) {
+ i += 1;
+ return speech + ' ' + block.mathspeak() + ' ' + (cmd.mathspeakTemplate[i]+' ' || 'End'+cmd.ctrlSeq+' ');
});
};
});
@@ -294,9 +344,10 @@ var MathCommand = P(MathElement, function(_, super_) {
* Lightweight command without blocks or children.
*/
var Symbol = P(MathCommand, function(_, super_) {
- _.init = function(ctrlSeq, html, text) {
- if (!text) text = ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : ctrlSeq;
+ _.init = function(ctrlSeq, html, text, mathspeak) {
+ if (!text && !!ctrlSeq) text = ctrlSeq.replace(/^\\/, '');
+ this.mathspeakName = mathspeak || text;
super_.init.call(this, ctrlSeq, html, [ text ]);
};
@@ -312,6 +363,7 @@ var Symbol = P(MathCommand, function(_, super_) {
cursor.jQ.insDirOf(dir, this.jQ);
cursor[-dir] = this;
cursor[dir] = this[dir];
+ aria.queue(this);
};
_.deleteTowards = function(dir, cursor) {
cursor[dir] = this.remove()[dir];
@@ -325,19 +377,20 @@ var Symbol = P(MathCommand, function(_, super_) {
};
_.latex = function(){ return this.ctrlSeq; };
- _.text = function(){ return this.textTemplate; };
+ _.text = function(){ return this.textTemplate.join(''); };
+ _.mathspeak = function(){ return this.mathspeakName; };
_.placeCursor = noop;
_.isEmpty = function(){ return true; };
});
var VanillaSymbol = P(Symbol, function(_, super_) {
- _.init = function(ch, html) {
- super_.init.call(this, ch, ''+(html || ch)+'');
+ _.init = function(ch, html, mathspeak) {
+ super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak);
};
});
var BinaryOperator = P(Symbol, function(_, super_) {
- _.init = function(ctrlSeq, html, text) {
+ _.init = function(ctrlSeq, html, text, mathspeak) {
super_.init.call(this,
- ctrlSeq, ''+html+'', text
+ ctrlSeq, ''+html+'', text, mathspeak
);
};
});
@@ -361,6 +414,45 @@ var MathBlock = P(MathElement, function(_, super_) {
this.join('text')
;
};
+ _.mathspeak = function() {
+ var tempOp = '';
+ var autoOps = {};
+ if (this.controller) autoOps = this.controller.options.autoOperatorNames;
+ return this.foldChildren([], function(speechArray, cmd) {
+ if (cmd.isPartOfOperator) {
+ tempOp += cmd.mathspeak();
+ } else {
+ if(tempOp!=='') {
+ if(autoOps !== {} && autoOps._maxLength > 0) {
+ var x = autoOps[tempOp.toLowerCase()];
+ if(typeof x === 'string') tempOp = x;
+ }
+ speechArray.push(tempOp+' ');
+ tempOp = '';
+ }
+ var mathspeakText = cmd.mathspeak();
+ var cmdText = cmd.ctrlSeq;
+ if (
+ isNaN(cmdText) &&
+ cmdText !== '.' &&
+ (!cmd.parent || !cmd.parent.parent || !cmd.parent.parent.isTextBlock())
+ ) {
+ mathspeakText = ' ' + mathspeakText + ' ';
+ }
+ speechArray.push(mathspeakText);
+ }
+ return speechArray;
+ })
+ .join('')
+ .replace(/ +(?= )/g,'')
+ // For Apple devices in particular, split out digits after a decimal point so they aren't read aloud as whole words.
+ // Not doing so makes 123.456 potentially spoken as "one hundred twenty-three point four hundred fifty-six."
+ // Instead, add spaces so it is spoken as "one hundred twenty-three point four five six."
+ .replace(/(\.)([0-9]+)/g, function(match, p1, p2) {
+ return p1 + p2.split('').join(' ').trim();
+ });
+ };
+ _.ariaLabel = 'block';
_.keystroke = function(key, e, ctrlr) {
if (ctrlr.options.spaceBehavesLikeTab
@@ -377,8 +469,14 @@ var MathBlock = P(MathElement, function(_, super_) {
// the cursor
_.moveOutOf = function(dir, cursor, updown) {
var updownInto = updown && this.parent[updown+'Into'];
- if (!updownInto && this[dir]) cursor.insAtDirEnd(-dir, this[dir]);
- else cursor.insDirOf(dir, this.parent);
+ if (!updownInto && this[dir]) {
+ cursor.insAtDirEnd(-dir, this[dir]);
+ aria.queueDirEndOf(-dir).queue(cursor.parent, true);
+ }
+ else {
+ cursor.insDirOf(dir, this.parent);
+ aria.queueDirOf(dir).queue(this.parent);
+ }
};
_.selectOutOf = function(dir, cursor) {
cursor.insDirOf(dir, this.parent);
@@ -395,22 +493,55 @@ var MathBlock = P(MathElement, function(_, super_) {
while (pageX < node.jQ.offset().left) node = node[L];
return node.seek(pageX, cursor);
};
- _.chToCmd = function(ch) {
+ _.chToCmd = function(ch, options) {
var cons;
// exclude f because it gets a dedicated command with more spacing
if (ch.match(/^[a-eg-zA-Z]$/))
return Letter(ch);
else if (/^\d$/.test(ch))
return Digit(ch);
+ else if (options && options.typingSlashWritesDivisionSymbol && ch === '/')
+ return LatexCmds['÷'](ch);
+ else if (options && options.typingAsteriskWritesTimesSymbol && ch === '*')
+ return LatexCmds['×'](ch);
+ else if (options && options.typingPercentWritesPercentOf && ch === '%')
+ return LatexCmds.percentof(ch);
else if (cons = CharCmds[ch] || LatexCmds[ch])
return cons(ch);
else
return VanillaSymbol(ch);
};
_.write = function(cursor, ch) {
- var cmd = this.chToCmd(ch);
+ var cmd = this.chToCmd(ch, cursor.options);
if (cursor.selection) cmd.replaces(cursor.replaceSelection());
- cmd.createLeftOf(cursor.show());
+ if (!cursor.isTooDeep()) {
+ cmd.createLeftOf(cursor.show());
+ // special-case the slash so that fractions are voiced while typing
+ if (ch === '/') {
+ aria.alert('over');
+ } else {
+ aria.alert(cmd.mathspeak({ createdLeftOf: cursor }));
+ }
+ }
+ };
+
+ _.writeLatex = function(cursor, latex) {
+
+ var all = Parser.all;
+ var eof = Parser.eof;
+
+ var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex);
+
+ if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) {
+ block.children().adopt(cursor.parent, cursor[L], cursor[R]);
+ var jQ = block.jQize();
+ jQ.insertBefore(cursor.jQ);
+ cursor[L] = block.ends[R];
+ block.finalizeInsert(cursor.options, cursor);
+ if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L);
+ if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R);
+ cursor.parent.bubble(function (node) { node.reflow(); });
+ }
};
_.focus = function() {
@@ -421,35 +552,57 @@ var MathBlock = P(MathElement, function(_, super_) {
};
_.blur = function() {
this.jQ.removeClass('mq-hasCursor');
- if (this.isEmpty())
+ if (this.isEmpty()) {
this.jQ.addClass('mq-empty');
-
+ if (this.isEmptyParens()) {
+ this.jQ.addClass('mq-empty-parens');
+ } else if (this.isEmptySquareBrackets()) {
+ this.jQ.addClass('mq-empty-square-brackets');
+ }
+ }
return this;
};
});
+Options.p.mouseEvents = true;
API.StaticMath = function(APIClasses) {
return P(APIClasses.AbstractMathQuill, function(_, super_) {
this.RootBlock = MathBlock;
- _.__mathquillify = function() {
+ _.__mathquillify = function(opts, interfaceVersion) {
+ this.config(opts);
super_.__mathquillify.call(this, 'mq-math-mode');
- this.__controller.delegateMouseEvents();
- this.__controller.staticMathTextareaEvents();
+ if (this.__options.mouseEvents) {
+ this.__controller.delegateMouseEvents();
+ this.__controller.staticMathTextareaEvents();
+ }
return this;
};
_.init = function() {
super_.init.apply(this, arguments);
- this.__controller.root.postOrder(
- 'registerInnerField', this.innerFields = [], APIClasses.MathField);
+ var innerFields = this.innerFields = [];
+ this.__controller.root.postOrder(function (node) {
+ node.registerInnerField(innerFields, APIClasses.InnerMathField);
+ });
};
_.latex = function() {
var returned = super_.latex.apply(this, arguments);
if (arguments.length > 0) {
- this.__controller.root.postOrder(
- 'registerInnerField', this.innerFields = [], APIClasses.MathField);
+ var innerFields = this.innerFields = [];
+ this.__controller.root.postOrder(function (node) {
+ node.registerInnerField(innerFields, APIClasses.InnerMathField);
+ });
+ // Force an ARIA label update to remain in sync with the new LaTeX value.
+ this.__controller.updateMathspeak();
}
return returned;
};
+ _.setAriaLabel = function(ariaLabel) {
+ this.__controller.setAriaLabel(ariaLabel);
+ return this;
+ };
+ _.getAriaLabel = function () {
+ return this.__controller.getAriaLabel();
+ };
});
};
@@ -466,3 +619,20 @@ API.MathField = function(APIClasses) {
};
});
};
+
+API.InnerMathField = function(APIClasses) {
+ return P(APIClasses.MathField, function(_, super_) {
+ _.makeStatic = function() {
+ this.__controller.editable = false;
+ this.__controller.root.blur();
+ this.__controller.unbindEditablesEvents();
+ this.__controller.container.removeClass('mq-editable-field');
+ };
+ _.makeEditable = function() {
+ this.__controller.editable = true;
+ this.__controller.editablesTextareaEvents();
+ this.__controller.cursor.insAtRightEnd(this.__controller.root);
+ this.__controller.container.addClass('mq-editable-field');
+ };
+ });
+};
diff --git a/src/commands/math/LatexCommandInput.js b/src/commands/math/LatexCommandInput.js
index fb4e18e96..dca839ab7 100644
--- a/src/commands/math/LatexCommandInput.js
+++ b/src/commands/math/LatexCommandInput.js
@@ -30,15 +30,24 @@ CharCmds['\\'] = P(MathCommand, function(_, super_) {
this.ends[L].write = function(cursor, ch) {
cursor.show().deleteSelection();
- if (ch.match(/[a-z]/i)) VanillaSymbol(ch).createLeftOf(cursor);
+ if (ch.match(/[a-z]/i)) {
+ VanillaSymbol(ch).createLeftOf(cursor);
+ // TODO needs tests
+ aria.alert(ch);
+ }
else {
- this.parent.renderCommand(cursor);
- if (ch !== '\\' || !this.isEmpty()) this.parent.parent.write(cursor, ch);
+ var cmd = this.parent.renderCommand(cursor);
+ // TODO needs tests
+ aria.queue(cmd.mathspeak({ createdLeftOf: cursor }));
+ if (ch !== '\\' || !this.isEmpty()) cursor.parent.write(cursor, ch);
+ else aria.alert();
}
};
this.ends[L].keystroke = function(key, e, ctrlr) {
if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') {
- this.parent.renderCommand(ctrlr.cursor);
+ var cmd = this.parent.renderCommand(ctrlr.cursor);
+ // TODO needs tests
+ aria.alert(cmd.mathspeak({ createdLeftOf: ctrlr.cursor }));
e.preventDefault();
return;
}
@@ -88,6 +97,7 @@ CharCmds['\\'] = P(MathCommand, function(_, super_) {
if (this._replacedFragment)
this._replacedFragment.remove();
}
+ return cmd;
};
});
diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js
index adebbddc1..865edfc9c 100644
--- a/src/commands/math/advancedSymbols.js
+++ b/src/commands/math/advancedSymbols.js
@@ -12,55 +12,52 @@ LatexCmds.otimes = P(BinaryOperator, function(_, super_) {
};
});
-LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠');
-
-LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast =
- bind(BinaryOperator,'\\ast ','∗');
- //case 'there4 = // a special exception for this one, perhaps?
+LatexCmds['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast =
+ bind(BinaryOperator,'\\ast ','∗', 'low asterisk');
LatexCmds.therefor = LatexCmds.therefore =
- bind(BinaryOperator,'\\therefore ','∴');
+ bind(BinaryOperator,'\\therefore ','∴', 'therefore');
LatexCmds.cuz = // l33t
-LatexCmds.because = bind(BinaryOperator,'\\because ','∵');
+LatexCmds.because = bind(BinaryOperator,'\\because ','∵', 'because');
-LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝');
+LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝', 'proportional to');
-LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈');
+LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈'), 'approximately equal to';
-LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈');
+LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈', 'is in');
-LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋');
+LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋', 'is not in');
LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain =
- bind(BinaryOperator,'\\not\\ni ','∌');
+ bind(BinaryOperator,'\\not\\ni ','∌', 'does not contain');
-LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂');
+LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂', 'subset');
LatexCmds.sup = LatexCmds.supset = LatexCmds.superset =
- bind(BinaryOperator,'\\supset ','⊃');
+ bind(BinaryOperator,'\\supset ','⊃', 'superset');
LatexCmds.nsub = LatexCmds.notsub =
LatexCmds.nsubset = LatexCmds.notsubset =
- bind(BinaryOperator,'\\not\\subset ','⊄');
+ bind(BinaryOperator,'\\not\\subset ','⊄', 'not a subset');
LatexCmds.nsup = LatexCmds.notsup =
LatexCmds.nsupset = LatexCmds.notsupset =
LatexCmds.nsuperset = LatexCmds.notsuperset =
- bind(BinaryOperator,'\\not\\supset ','⊅');
+ bind(BinaryOperator,'\\not\\supset ','⊅', 'not a superset');
LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq =
- bind(BinaryOperator,'\\subseteq ','⊆');
+ bind(BinaryOperator,'\\subseteq ','⊆', 'subset or equal to');
LatexCmds.supe = LatexCmds.supeq =
LatexCmds.supsete = LatexCmds.supseteq =
LatexCmds.supersete = LatexCmds.superseteq =
- bind(BinaryOperator,'\\supseteq ','⊇');
+ bind(BinaryOperator,'\\supseteq ','⊇', 'superset or equal to');
LatexCmds.nsube = LatexCmds.nsubeq =
LatexCmds.notsube = LatexCmds.notsubeq =
LatexCmds.nsubsete = LatexCmds.nsubseteq =
LatexCmds.notsubsete = LatexCmds.notsubseteq =
- bind(BinaryOperator,'\\not\\subseteq ','⊈');
+ bind(BinaryOperator,'\\not\\subseteq ','⊈', 'not subset or equal to');
LatexCmds.nsupe = LatexCmds.nsupeq =
LatexCmds.notsupe = LatexCmds.notsupeq =
@@ -68,252 +65,274 @@ LatexCmds.nsupsete = LatexCmds.nsupseteq =
LatexCmds.notsupsete = LatexCmds.notsupseteq =
LatexCmds.nsupersete = LatexCmds.nsuperseteq =
LatexCmds.notsupersete = LatexCmds.notsuperseteq =
- bind(BinaryOperator,'\\not\\supseteq ','⊉');
-
+ bind(BinaryOperator,'\\not\\supseteq ','⊉', 'not superset or equal to');
//the canonical sets of numbers
+LatexCmds.mathbb = P(MathCommand, function(_) {
+ _.createLeftOf = noop;
+ _.numBlocks = function() { return 1; };
+ _.parser = function() {
+ var string = Parser.string;
+ var regex = Parser.regex;
+ var optWhitespace = Parser.optWhitespace;
+ return optWhitespace.then(string('{'))
+ .then(optWhitespace)
+ .then(regex(/^[NPZQRCH]/))
+ .skip(optWhitespace)
+ .skip(string('}'))
+ .map(function(c) {
+ // instantiate the class for the matching char
+ return LatexCmds[c]();
+ });
+ };
+});
+
LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals =
- bind(VanillaSymbol,'\\mathbb{N}','ℕ');
+ bind(VanillaSymbol,'\\mathbb{N}','ℕ', 'naturals');
LatexCmds.P =
LatexCmds.primes = LatexCmds.Primes =
LatexCmds.projective = LatexCmds.Projective =
LatexCmds.probability = LatexCmds.Probability =
- bind(VanillaSymbol,'\\mathbb{P}','ℙ');
+ bind(VanillaSymbol,'\\mathbb{P}','ℙ', 'P');
LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers =
- bind(VanillaSymbol,'\\mathbb{Z}','ℤ');
+ bind(VanillaSymbol,'\\mathbb{Z}','ℤ', 'integers');
LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals =
- bind(VanillaSymbol,'\\mathbb{Q}','ℚ');
+ bind(VanillaSymbol,'\\mathbb{Q}','ℚ', 'rationals');
LatexCmds.R = LatexCmds.reals = LatexCmds.Reals =
- bind(VanillaSymbol,'\\mathbb{R}','ℝ');
+ bind(VanillaSymbol,'\\mathbb{R}','ℝ', 'reals');
LatexCmds.C =
LatexCmds.complex = LatexCmds.Complex =
LatexCmds.complexes = LatexCmds.Complexes =
LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane =
- bind(VanillaSymbol,'\\mathbb{C}','ℂ');
+ bind(VanillaSymbol,'\\mathbb{C}','ℂ', 'complexes');
LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions =
- bind(VanillaSymbol,'\\mathbb{H}','ℍ');
+ bind(VanillaSymbol,'\\mathbb{H}','ℍ', 'quaternions');
//spacing
-LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' ');
-LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' ');
+LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' ', '4 spaces');
+LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' ', '8 spaces');
/* spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow
case ',':
- return VanillaSymbol('\\, ',' ');
+ return VanillaSymbol('\\, ',' ', 'comma');
case ':':
- return VanillaSymbol('\\: ',' ');
+ return VanillaSymbol('\\: ',' ', 'colon');
case ';':
- return VanillaSymbol('\\; ',' ');
+ return VanillaSymbol('\\; ',' ', 'semicolon');
case '!':
- return Symbol('\\! ','');
+ return Symbol('\\! ','', 'exclamation point');
*/
//binary operators
-LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇');
-LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△');
-LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖');
-LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎');
-LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽');
-LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓');
-LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲');
-LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔');
-LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳');
-LatexCmds.odot = bind(VanillaSymbol, '\\odot ', '⊙');
-LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯');
-LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '');
-LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '');
-LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀');
-LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐');
+LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇', 'diamond');
+LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△', 'triangle up');
+LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖', 'o minus');
+LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎', 'disjoint union');
+LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽', 'triangle down');
+LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓', 'greatest lower bound');
+LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲', 'triangle left');
+LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔', 'least upper bound');
+LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳', 'triangle right');
+//circledot is not a not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details
+LatexCmds.odot = LatexCmds.circledot = bind(VanillaSymbol, '\\odot ', '⊙', 'circle dot');
+LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯', 'circle');
+LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '', 'dagger');
+LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '', 'big dagger');
+LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀', 'wreath');
+LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐', 'amalgam');
//relationship symbols
-LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨');
-LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺');
-LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻');
-LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼');
-LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽');
-LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃');
-LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣');
-LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪');
-LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫');
-LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥');
-LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈');
-LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏');
-LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐');
-LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣');
-LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑');
-LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒');
-LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐');
-LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢');
-LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦');
-LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣');
+LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨', 'models');
+LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺', 'precedes');
+LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻', 'succeeds');
+LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼', 'precedes or equals');
+LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽', 'succeeds or equals');
+LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃', 'similar or equal to');
+LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣', 'divides');
+LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪', 'll');
+LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫', 'gg');
+LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥', 'parallel with');
+LatexCmds.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel with');
+LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈', 'bowtie');
+LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏', 'square subset');
+LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐', 'square superset');
+LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣', 'smile');
+LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑', 'square subset or equal to');
+LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒', 'square superset or equal to');
+LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐', 'dotted equals');
+LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢', 'frown');
+LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦', 'v dash');
+LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣', 'dash v');
+LatexCmds.nless = bind(VanillaSymbol, '\\nless ', '≮', 'not less than');
+LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯', 'not greater than');
//arrows
-LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←');
-LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→');
-LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐');
-LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒');
-LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔');
-LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕');
-LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔');
-LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕');
-LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦');
-LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗');
-LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩');
-LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪');
-LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘');
-LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼');
-LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀');
-LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙');
-LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽');
-LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁');
-LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖');
+LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←', 'left arrow');
+LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→', 'right arrow');
+LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐', 'left arrow');
+LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒', 'right arrow');
+LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔', 'left and right arrow');
+LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕', 'up and down arrow');
+LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔', 'left and right arrow');
+LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕', 'up and down arrow');
+LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦', 'maps to');
+LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗', 'northeast arrow');
+LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩', 'hook left arrow');
+LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪', 'hook right arrow');
+LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘', 'southeast arrow');
+LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼', 'left harpoon up');
+LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀', 'right harpoon up');
+LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙', 'southwest arrow');
+LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽', 'left harpoon down');
+LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁', 'right harpoon down');
+LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖', 'northwest arrow');
//Misc
-LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…');
-LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯');
-LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮');
-LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱');
-LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√');
-LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '▵');
-LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ');
-LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤');
-LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭');
-LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮');
-LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯');
-LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘');
-LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥');
-LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣');
-LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢');
-LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡');
-LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠');
+LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…', 'l dots');
+LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯', 'c dots');
+LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮', 'v dots');
+LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱', 'd dots');
+LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√', 'unresolved root');
+LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '△', 'triangle');
+LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ', 'ell');
+LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤', 'top');
+LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭', 'flat');
+LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮', 'natural');
+LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯', 'sharp');
+LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘', 'wp');
+LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥', 'bot');
+LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣', 'club suit');
+LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢', 'diamond suit');
+LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡', 'heart suit');
+LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠', 'spade suit');
+//not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details
+LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '▱', 'parallelogram');
+LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜', 'square');
//variable-sized
-LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮');
-LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩');
-LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪');
-LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔');
-LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨');
-LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧');
-LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙');
-LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗');
-LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕');
-LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎');
+LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮', 'o int');
+LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩', 'big cap');
+LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪', 'big cup');
+LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔', 'big square cup');
+LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨', 'big vee');
+LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧', 'big wedge');
+LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙', 'big o dot');
+LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗', 'big o times');
+LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕', 'big o plus');
+LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎', 'big u plus');
//delimiters
-LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊');
-LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋');
-LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈');
-LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉');
-LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{');
-LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}');
-LatexCmds.lbrack = bind(VanillaSymbol, '[');
-LatexCmds.rbrack = bind(VanillaSymbol, ']');
+LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊', 'left floor');
+LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋', 'right floor');
+LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈', 'left ceiling');
+LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉', 'right ceiling');
+LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{', 'left brace');
+LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}', 'right brace');
+LatexCmds.lbrack = bind(VanillaSymbol, '[', 'left bracket');
+LatexCmds.rbrack = bind(VanillaSymbol, ']', 'right bracket');
//various symbols
-
-LatexCmds['∫'] =
-LatexCmds['int'] =
-LatexCmds.integral = bind(Symbol,'\\int ','∫');
-
-LatexCmds.slash = bind(VanillaSymbol, '/');
-LatexCmds.vert = bind(VanillaSymbol,'|');
-LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥');
+LatexCmds.slash = bind(VanillaSymbol, '/', 'slash');
+LatexCmds.vert = bind(VanillaSymbol,'|', 'vertical bar');
+LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥', 'perpendicular');
LatexCmds.nabla = LatexCmds.del = bind(VanillaSymbol,'\\nabla ','∇');
-LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ');
+LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ', 'horizontal bar');
LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom =
- bind(VanillaSymbol,'\\text\\AA ','Å');
+ bind(VanillaSymbol,'\\text\\AA ','Å', 'AA');
LatexCmds.ring = LatexCmds.circ = LatexCmds.circle =
- bind(VanillaSymbol,'\\circ ','∘');
+ bind(VanillaSymbol,'\\circ ','∘', 'circle');
-LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•');
+LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•', 'bullet');
LatexCmds.setminus = LatexCmds.smallsetminus =
- bind(VanillaSymbol,'\\setminus ','∖');
+ bind(VanillaSymbol,'\\setminus ','∖', 'set minus');
-LatexCmds.not = //bind(Symbol,'\\not ','/');
-LatexCmds['¬'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬');
+LatexCmds.not = //bind(Symbol,'\\not ','/', 'not');
+LatexCmds['¬'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬', 'not');
LatexCmds['…'] = LatexCmds.dots = LatexCmds.ellip = LatexCmds.hellip =
LatexCmds.ellipsis = LatexCmds.hellipsis =
- bind(VanillaSymbol,'\\dots ','…');
+ bind(VanillaSymbol,'\\dots ','…', 'ellipsis');
LatexCmds.converges =
LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow =
- bind(VanillaSymbol,'\\downarrow ','↓');
+ bind(VanillaSymbol,'\\downarrow ','↓', 'converges with');
LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow =
- bind(VanillaSymbol,'\\Downarrow ','⇓');
+ bind(VanillaSymbol,'\\Downarrow ','⇓', 'down arrow');
LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow =
- bind(VanillaSymbol,'\\uparrow ','↑');
+ bind(VanillaSymbol,'\\uparrow ','↑', 'diverges from');
-LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑');
+LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑', 'up arrow');
-LatexCmds.to = bind(BinaryOperator,'\\to ','→');
+LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→', 'right arrow');
-LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→');
+LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒', 'implies');
-LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒');
+LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒', 'right arrow');
-LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒');
+LatexCmds.gets = bind(BinaryOperator,'\\gets ','←', 'gets');
-LatexCmds.gets = bind(BinaryOperator,'\\gets ','←');
+LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←', 'left arrow');
-LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←');
+LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐', 'implied by');
-LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐');
-
-LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐');
+LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐', 'left arrow');
LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow =
- bind(VanillaSymbol,'\\leftrightarrow ','↔');
+ bind(VanillaSymbol,'\\leftrightarrow ','↔', 'left and right arrow');
-LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔');
+LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔', 'if and only if');
LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow =
- bind(VanillaSymbol,'\\Leftrightarrow ','⇔');
+ bind(VanillaSymbol,'\\Leftrightarrow ','⇔', 'left and right arrow');
-LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ');
+LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ', 'real');
LatexCmds.Im = LatexCmds.imag =
LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary =
- bind(VanillaSymbol,'\\Im ','ℑ');
+ bind(VanillaSymbol,'\\Im ','ℑ', 'imaginary');
-LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂');
+LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂', 'partial');
-LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity =
- bind(VanillaSymbol,'\\infty ','∞');
+LatexCmds.pounds = bind(VanillaSymbol,'\\pounds ','£');
LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym =
- bind(VanillaSymbol,'\\aleph ','ℵ');
+ bind(VanillaSymbol,'\\aleph ','ℵ', 'alef sym');
LatexCmds.xist = //LOL
LatexCmds.xists = LatexCmds.exist = LatexCmds.exists =
- bind(VanillaSymbol,'\\exists ','∃');
+ bind(VanillaSymbol,'\\exists ','∃', 'there exists at least 1');
+
+LatexCmds.nexists = LatexCmds.nexist =
+ bind(VanillaSymbol, '\\nexists ', '∄', 'there is no');
LatexCmds.and = LatexCmds.land = LatexCmds.wedge =
- bind(VanillaSymbol,'\\wedge ','∧');
+ bind(BinaryOperator,'\\wedge ','∧', 'and');
-LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(VanillaSymbol,'\\vee ','∨');
+LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\vee ','∨', 'or');
LatexCmds.o = LatexCmds.O =
LatexCmds.empty = LatexCmds.emptyset =
LatexCmds.oslash = LatexCmds.Oslash =
LatexCmds.nothing = LatexCmds.varnothing =
- bind(BinaryOperator,'\\varnothing ','∅');
+ bind(BinaryOperator,'\\varnothing ','∅', 'nothing');
-LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪');
+LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪', 'union');
LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection =
- bind(BinaryOperator,'\\cap ','∩');
+ bind(BinaryOperator,'\\cap ','∩', 'intersection');
-LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'^\\circ ','°');
+// FIXME: the correct LaTeX would be ^\circ but we can't parse that
+LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°', 'degrees');
-LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠');
+LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠', 'angle');
+LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡', 'measured angle');
diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js
index 361009fc2..45b16c62d 100644
--- a/src/commands/math/basicSymbols.js
+++ b/src/commands/math/basicSymbols.js
@@ -1,8 +1,160 @@
/*********************************
* Symbols for Basic Mathematics
********************************/
+var DigitGroupingChar = P(Symbol, function(_, super_) {
+ _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) {
+ // don't try to fix digit grouping if the sibling to my right changed (dir === R or
+ // undefined) and it's now a DigitGroupingChar, it will try to fix grouping
+ if (dir !== L && this[R] instanceof DigitGroupingChar) return;
+ this.fixDigitGrouping(opts);
+ };
+
+ _.fixDigitGrouping = function (opts) {
+ if (!opts.enableDigitGrouping) return;
+
+ var left = this;
+ var right = this;
+
+ var spacesFound = 0;
+ var dots = [];
+
+ var SPACE = '\\ ';
+ var DOT = '.';
+
+ // traverse left as far as possible (starting at this char)
+ var node = left;
+ do {
+ if (/^[0-9]$/.test(node.ctrlSeq)) {
+ left = node
+ } else if (node.ctrlSeq === SPACE) {
+ left = node
+ spacesFound += 1;
+ } else if (node.ctrlSeq === DOT) {
+ left = node
+ dots.push(node);
+ } else {
+ break;
+ }
+ } while (node = left[L]);
+
+ // traverse right as far as possible (starting to right of this char)
+ while (node = right[R]) {
+ if (/^[0-9]$/.test(node.ctrlSeq)) {
+ right = node
+ } else if (node.ctrlSeq === SPACE) {
+ right = node
+ spacesFound += 1;
+ } else if (node.ctrlSeq === DOT) {
+ right = node
+ dots.push(node);
+ } else {
+ break;
+ }
+ }
+
+ // trim the leading spaces
+ while (right !== left && left.ctrlSeq === SPACE) {
+ left = left[R];
+ spacesFound -= 1;
+ }
+
+ // trim the trailing spaces
+ while (right !== left && right.ctrlSeq === SPACE) {
+ right = right[L];
+ spacesFound -= 1;
+ }
+
+ // happens when you only have a space
+ if (left === right && left.ctrlSeq === SPACE) return;
+
+ var disableFormatting = spacesFound > 0 || dots.length > 1;
+ if (disableFormatting) {
+ this.removeGroupingBetween(left, right);
+ } else if (dots[0]) {
+ if (dots[0] !== left) {
+ this.addGroupingBetween(dots[0][L], left);
+ }
+ if (dots[0] !== right) {
+ // we do not show grouping to the right of a decimal place #yet
+ this.removeGroupingBetween(dots[0][R], right);
+ }
+ } else {
+ this.addGroupingBetween(right, left);
+ }
+ };
+
+ _.removeGroupingBetween = function (left, right) {
+ var node = left;
+ do {
+ node.setGroupingClass(undefined);
+ if (node === right) break;
+ } while (node = node[R]);
+ };
+
+ _.addGroupingBetween = function (start, end) {
+ var node = start;
+ var count = 0;
+
+ var totalDigits = 0;
+ var node = start;
+ while (node) {
+ totalDigits += 1;
+
+ if (node === end) break;
+ node = node[L];
+ }
+
+ var numDigitsInFirstGroup = totalDigits % 3;
+ if (numDigitsInFirstGroup === 0) numDigitsInFirstGroup = 3;
+
+ var node = start;
+ while (node) {
+ count += 1;
+
+ var cls = undefined;
+
+ // only do grouping if we have at least 4 numbers
+ if (totalDigits >= 4) {
+ if (count === totalDigits) {
+ cls = 'mq-group-leading-' + numDigitsInFirstGroup;
+ } else if (count % 3 === 0) {
+ if (count !== totalDigits) {
+ cls = 'mq-group-start'
+ }
+ }
+
+ if (!cls) {
+ cls = 'mq-group-other'
+ }
+ }
+
+ node.setGroupingClass(cls);
+
+ if (node === end) break;
+ node = node[L];
+ }
+ };
+
+ _.setGroupingClass = function (cls) {
+ // nothing changed (either class is the same or it's still undefined)
+ if (this._groupingClass === cls) return;
+
+ // remove existing class
+ if (this._groupingClass) this.jQ.removeClass(this._groupingClass);
+
+ // add new class
+ if (cls) this.jQ.addClass(cls);
+
+ // cache the groupingClass
+ this._groupingClass = cls;
+ }
+});
+
+var Digit = P(DigitGroupingChar, function(_, super_) {
+ _.init = function(ch, html, mathspeak) {
+ super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak);
+ };
-var Digit = P(VanillaSymbol, function(_, super_) {
_.createLeftOf = function(cursor) {
if (cursor.options.autoSubscriptNumerals
&& cursor.parent !== cursor.parent.parent.sub
@@ -16,6 +168,20 @@ var Digit = P(VanillaSymbol, function(_, super_) {
}
else super_.createLeftOf.call(this, cursor);
};
+ _.mathspeak = function(opts) {
+ if (opts && opts.createdLeftOf) {
+ var cursor = opts.createdLeftOf;
+ if (cursor.options.autoSubscriptNumerals
+ && cursor.parent !== cursor.parent.parent.sub
+ && ((cursor[L] instanceof Variable && cursor[L].isItalic !== false)
+ || (cursor[L] instanceof SupSub
+ && cursor[L][L] instanceof Variable
+ && cursor[L][L].isItalic !== false))) {
+ return 'Subscript ' + super_.mathspeak.call(this) + ' Baseline';
+ }
+ }
+ return super_.mathspeak.apply(this, arguments);
+ };
});
var Variable = P(Symbol, function(_, super_) {
@@ -24,14 +190,41 @@ var Variable = P(Symbol, function(_, super_) {
};
_.text = function() {
var text = this.ctrlSeq;
- if (this[L] && !(this[L] instanceof Variable)
- && !(this[L] instanceof BinaryOperator))
- text = '*' + text;
- if (this[R] && !(this[R] instanceof BinaryOperator)
- && !(this[R].ctrlSeq === '^'))
- text += '*';
+ if (this.isPartOfOperator) {
+ if (text[0] == '\\') {
+ text = text.slice(1, text.length);
+ }
+ else if (text[text.length-1] == ' ') {
+ text = text.slice (0, -1);
+ }
+ } else {
+ if (this[L] && !(this[L] instanceof Variable)
+ && !(this[L] instanceof BinaryOperator)
+ && this[L].ctrlSeq !== '\\ ')
+ text = '*' + text;
+ if (this[R] && !(this[R] instanceof BinaryOperator)
+ && !(this[R] instanceof SupSub))
+ text += '*';
+ }
return text;
};
+ _.mathspeak = function() {
+ var text = this.ctrlSeq;
+ if (
+ this.isPartOfOperator ||
+ text.length > 1 ||
+ (this.parent && this.parent.parent && this.parent.parent.isTextBlock())
+ ) {
+ return super_.mathspeak.call(this);
+ } else {
+ // Apple voices in VoiceOver (such as Alex, Bruce, and Victoria) do
+ // some strange pronunciation given certain expressions,
+ // e.g. "y-2" is spoken as "ee minus 2" (as if the y is short).
+ // Not an ideal solution, but surrounding non-numeric text blocks with quotation marks works.
+ // This bug has been acknowledged by Apple.
+ return '"'+text+'"';
+ }
+ };
});
Options.p.autoCommands = { _maxLength: 0 };
@@ -55,32 +248,89 @@ optionProcessors.autoCommands = function(cmds) {
return dict;
};
+Options.p.autoParenthesizedFunctions = {_maxLength: 0};
+optionProcessors.autoParenthesizedFunctions = function (cmds) {
+ if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) {
+ throw '"'+cmds+'" not a space-delimited list of only letters';
+ }
+ var list = cmds.split(' '), dict = {}, maxLength = 0;
+ for (var i = 0; i < list.length; i += 1) {
+ var cmd = list[i];
+ if (cmd.length < 2) {
+ throw 'autocommand "'+cmd+'" not minimum length of 2';
+ }
+ dict[cmd] = 1;
+ maxLength = max(maxLength, cmd.length);
+ }
+ dict._maxLength = maxLength;
+ return dict;
+}
+
var Letter = P(Variable, function(_, super_) {
_.init = function(ch) { return super_.init.call(this, this.letter = ch); };
- _.createLeftOf = function(cursor) {
+ _.checkAutoCmds = function (cursor) {
+ //handle autoCommands
var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength;
if (maxLength > 0) {
// want longest possible autocommand, so join together longest
// sequence of letters
- var str = this.letter, l = cursor[L], i = 1;
- while (l instanceof Letter && i < maxLength) {
+ var str = '', l = this, i = 0;
+ // FIXME: l.ctrlSeq === l.letter checks if first or last in an operator name
+ while (l instanceof Letter && l.ctrlSeq === l.letter && i < maxLength) {
str = l.letter + str, l = l[L], i += 1;
}
// check for an autocommand, going thru substrings longest to shortest
while (str.length) {
if (autoCmds.hasOwnProperty(str)) {
- for (var i = 2, l = cursor[L]; i < str.length; i += 1, l = l[L]);
- Fragment(l, cursor[L]).remove();
+ for (var i = 1, l = this; i < str.length; i += 1, l = l[L]);
+ Fragment(l, this).remove();
cursor[L] = l[L];
return LatexCmds[str](str).createLeftOf(cursor);
}
str = str.slice(1);
}
}
+ }
+
+ _.autoParenthesize = function (cursor) {
+ //exit early if already parenthesized
+ var right = cursor.parent.ends[R]
+ if (right && right instanceof Bracket && right.ctrlSeq === '\\left(') {
+ return
+ }
+
+ //exit early if in simple subscript
+ if (this.isParentSimpleSubscript()) {
+ return;
+ }
+
+ //handle autoParenthesized functions
+ var str = '', l = this, i = 0;
+
+ var autoParenthesizedFunctions = cursor.options.autoParenthesizedFunctions, maxLength = autoParenthesizedFunctions._maxLength;
+ var autoOperatorNames = cursor.options.autoOperatorNames
+ while (l instanceof Letter && i < maxLength) {
+ str = l.letter + str, l = l[L], i += 1;
+ }
+ // check for an autoParenthesized functions, going thru substrings longest to shortest
+ // only allow autoParenthesized functions that are also autoOperatorNames
+ while (str.length) {
+ if (autoParenthesizedFunctions.hasOwnProperty(str) && autoOperatorNames.hasOwnProperty(str)) {
+ return cursor.parent.write(cursor, '(');
+ }
+ str = str.slice(1);
+ }
+ }
+
+ _.createLeftOf = function(cursor) {
super_.createLeftOf.apply(this, arguments);
+
+ this.checkAutoCmds(cursor);
+ this.autoParenthesize(cursor);
};
_.italicize = function(bool) {
this.isItalic = bool;
+ this.isPartOfOperator = !bool;
this.jQ.toggleClass('mq-operator-name', !bool);
return this;
};
@@ -93,6 +343,12 @@ var Letter = P(Variable, function(_, super_) {
_.autoUnItalicize = function(opts) {
var autoOps = opts.autoOperatorNames;
if (autoOps._maxLength === 0) return;
+
+ //exit early if in simple subscript
+ if (this.isParentSimpleSubscript()) {
+ return;
+ }
+
// want longest possible operator names, so join together entire contiguous
// sequence of letters
var str = this.letter;
@@ -102,7 +358,7 @@ var Letter = P(Variable, function(_, super_) {
// removeClass and delete flags from all letters before figuring out
// which, if any, are part of an operator name
Fragment(l[R] || this.parent.ends[L], r[L] || this.parent.ends[R]).each(function(el) {
- el.italicize(true).jQ.removeClass('mq-first mq-last');
+ el.italicize(true).jQ.removeClass('mq-first mq-last mq-followed-by-supsub');
el.ctrlSeq = el.letter;
});
@@ -121,8 +377,21 @@ var Letter = P(Variable, function(_, super_) {
first.ctrlSeq = (isBuiltIn ? '\\' : '\\operatorname{') + first.ctrlSeq;
last.ctrlSeq += (isBuiltIn ? ' ' : '}');
if (TwoWordOpNames.hasOwnProperty(word)) last[L][L][L].jQ.addClass('mq-last');
- if (nonOperatorSymbol(first[L])) first.jQ.addClass('mq-first');
- if (nonOperatorSymbol(last[R])) last.jQ.addClass('mq-last');
+ if (!shouldOmitPadding(first[L])) first.jQ.addClass('mq-first');
+ if (!shouldOmitPadding(last[R])) {
+ if (last[R] instanceof SupSub) {
+ var supsub = last[R]; // XXX monkey-patching, but what's the right thing here?
+ // Have operatorname-specific code in SupSub? A CSS-like language to style the
+ // math tree, but which ignores cursor and selection (which CSS can't)?
+ var respace = supsub.siblingCreated = supsub.siblingDeleted = function() {
+ supsub.jQ.toggleClass('mq-after-operator-name', !(supsub[R] instanceof Bracket));
+ };
+ respace();
+ }
+ else {
+ last.jQ.toggleClass('mq-last', !(last[R] instanceof Bracket));
+ }
+ }
i += len - 1;
first = last;
@@ -131,14 +400,31 @@ var Letter = P(Variable, function(_, super_) {
}
}
};
- function nonOperatorSymbol(node) {
- return node instanceof Symbol && !(node instanceof BinaryOperator);
+ function shouldOmitPadding(node) {
+ // omit padding if no node
+ if (!node) return true;
+
+ // do not add padding between letter and '.'
+ if (node.ctrlSeq === '.') return true;
+
+ // do not add padding between letter and binary operator. The
+ // binary operator already has padding
+ if (node instanceof BinaryOperator) return true;
+
+ if (node instanceof SummationNotation) return true;
+
+ return false;
}
});
-var BuiltInOpNames = {}; // http://latex.wikia.com/wiki/List_of_LaTeX_symbols#Named_operators:_sin.2C_cos.2C_etc.
- // except for over/under line/arrow \lim variants like \varlimsup
+var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that
+ // are built-into LaTeX, see Section 3.17 of the Short Math Guide: http://tinyurl.com/jm9okjc
+ // MathQuill auto-unitalicizes some operator names not in that set, like 'hcf'
+ // and 'arsinh', which must be exported as \operatorname{hcf} and
+ // \operatorname{arsinh}. Note: over/under line/arrow \lim variants like
+ // \varlimsup are not supported
+var AutoOpNames = Options.p.autoOperatorNames = { _maxLength: 9 }; // the set
+ // of operator names that MathQuill auto-unitalicizes by default; overridable
var TwoWordOpNames = { limsup: 1, liminf: 1, projlim: 1, injlim: 1 };
-var AutoOpNames = Options.p.autoOperatorNames = { _maxLength: 9 };
(function() {
var mostOps = ('arg deg det dim exp gcd hom inf ker lg lim ln log max min sup'
+ ' limsup liminf injlim projlim Pr').split(' ');
@@ -168,9 +454,13 @@ var AutoOpNames = Options.p.autoOperatorNames = { _maxLength: 9 };
AutoOpNames[moreNonstandardOps[i]] = 1;
}
}());
+
optionProcessors.autoOperatorNames = function(cmds) {
- if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) {
- throw '"'+cmds+'" not a space-delimited list of only letters';
+ if(typeof cmds !== 'string') {
+ throw '"'+cmds+'" not a space-delimited list';
+ }
+ if (!/^[a-z\|\-]+(?: [a-z\|\-]+)*$/i.test(cmds)) {
+ throw '"'+cmds+'" not a space-delimited list of letters or "|"';
}
var list = cmds.split(' '), dict = {}, maxLength = 0;
for (var i = 0; i < list.length; i += 1) {
@@ -178,8 +468,21 @@ optionProcessors.autoOperatorNames = function(cmds) {
if (cmd.length < 2) {
throw '"'+cmd+'" not minimum length of 2';
}
- dict[cmd] = 1;
- maxLength = max(maxLength, cmd.length);
+ if(cmd.indexOf('|') < 0) { // normal auto operator
+ dict[cmd] = cmd;
+ maxLength = max(maxLength, cmd.length);
+ }
+ else { // this item has a speech-friendly alternative
+ var cmdArray = cmd.split('|');
+ if(cmdArray.length > 2) {
+ throw '"'+cmd+'" has more than 1 mathspeak delimiter';
+ }
+ if (cmdArray[0].length < 2) {
+ throw '"'+cmd[0]+'" not minimum length of 2';
+ }
+ dict[cmdArray[0]] = cmdArray[1].replace(/-/g, ' '); // convert dashes to spaces for the sake of speech
+ maxLength = max(maxLength, cmdArray[0].length);
+ }
}
dict._maxLength = maxLength;
return dict;
@@ -208,7 +511,23 @@ LatexCmds.operatorname = P(MathCommand, function(_) {
_.createLeftOf = noop;
_.numBlocks = function() { return 1; };
_.parser = function() {
- return latexMathParser.block.map(function(b) { return b.children(); });
+ return latexMathParser.block.map(function(b) {
+ // Check for the special case of \operatorname{ans}, which has
+ // a special html representation
+ var isAllLetters = true;
+ var str = '';
+ var children = b.children();
+ children.each(function(child) {
+ if (child instanceof Letter) {
+ str += child.letter;
+ } else {
+ isAllLetters = false;
+ }
+ });
+ if (isAllLetters && str === 'ans') return LatexCmds[str](str);
+ // In cases other than `ans`, just return the children directly
+ return children;
+ });
};
});
@@ -223,14 +542,28 @@ LatexCmds.f = P(Letter, function(_, super_) {
});
// VanillaSymbol's
-LatexCmds[' '] = LatexCmds.space = bind(VanillaSymbol, '\\ ', ' ');
+LatexCmds[' '] = LatexCmds.space = P(DigitGroupingChar, function(_, super_) {
+ _.init = function () {
+ super_.init.call(this, '\\ ', ' ', ' ');
+ };
+});
-LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′');
+LatexCmds['.'] = P(DigitGroupingChar, function(_, super_) {
+ _.init = function () {
+ super_.init.call(this, '.', '.', '.');
+ };
+});
-LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\');
+LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′', 'prime');
+LatexCmds['″'] = LatexCmds.dprime = bind(VanillaSymbol, '″', '″', 'double prime');
+
+LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\', 'backslash');
if (!CharCmds['\\']) CharCmds['\\'] = LatexCmds.backslash;
-LatexCmds.$ = bind(VanillaSymbol, '\\$', '$');
+LatexCmds.$ = bind(VanillaSymbol, '\\$', '$', 'dollar');
+
+LatexCmds.square = bind(VanillaSymbol, '\\square ', '\u25A1', 'square');
+LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '\u2223', 'mid');
// does not use Symbola font
var NonSymbolaSymbol = P(Symbol, function(_, super_) {
@@ -240,8 +573,36 @@ var NonSymbolaSymbol = P(Symbol, function(_, super_) {
});
LatexCmds['@'] = NonSymbolaSymbol;
-LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&');
-LatexCmds['%'] = bind(NonSymbolaSymbol, '\\%', '%');
+LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&', 'and');
+LatexCmds['%'] = P(NonSymbolaSymbol, function(_, super_) {
+ _.init = function () {
+ super_.init.call(this, '\\%', '%', 'percent');
+ };
+ _.parser = function () {
+ var optWhitespace = Parser.optWhitespace;
+ var string = Parser.string;
+
+ // Parse `\%\operatorname{of}` as special `percentof` node so that
+ // it will be serialized properly and deleted as a unit.
+ return optWhitespace
+ .then(
+ string('\\operatorname{of}')
+ .map(function () {
+ return LatexCmds.percentof();
+ })
+ ).or(super_.parser.call(this))
+ ;
+ }
+});
+
+LatexCmds['∥'] = LatexCmds.parallel =
+ bind(VanillaSymbol, '\\parallel ', '∥', 'parallel');
+
+LatexCmds['∦'] = LatexCmds.nparallel =
+ bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel');
+
+LatexCmds['⟂'] = LatexCmds.perp =
+ bind(VanillaSymbol, '\\perp ', '⟂', 'perpendicular');
//the following are all Greek to me, but this helped a lot: http://www.ams.org/STIX/ion/stixsig03.html
@@ -271,54 +632,54 @@ LatexCmds.omega = P(Variable, function(_, super_) {
//why can't anybody FUCKING agree on these
LatexCmds.phi = //W3C or Unicode?
- bind(Variable,'\\phi ','ϕ');
+ bind(Variable,'\\phi ','ϕ', 'phi');
LatexCmds.phiv = //Elsevier and 9573-13
LatexCmds.varphi = //AMS and LaTeX
- bind(Variable,'\\varphi ','φ');
+ bind(Variable,'\\varphi ','φ', 'phi');
LatexCmds.epsilon = //W3C or Unicode?
- bind(Variable,'\\epsilon ','ϵ');
+ bind(Variable,'\\epsilon ','ϵ', 'epsilon');
LatexCmds.epsiv = //Elsevier and 9573-13
LatexCmds.varepsilon = //AMS and LaTeX
- bind(Variable,'\\varepsilon ','ε');
+ bind(Variable,'\\varepsilon ','ε', 'epsilon');
LatexCmds.piv = //W3C/Unicode and Elsevier and 9573-13
LatexCmds.varpi = //AMS and LaTeX
- bind(Variable,'\\varpi ','ϖ');
+ bind(Variable,'\\varpi ','ϖ', 'piv');
LatexCmds.sigmaf = //W3C/Unicode
LatexCmds.sigmav = //Elsevier
LatexCmds.varsigma = //LaTeX
- bind(Variable,'\\varsigma ','ς');
+ bind(Variable,'\\varsigma ','ς', 'sigma');
LatexCmds.thetav = //Elsevier and 9573-13
LatexCmds.vartheta = //AMS and LaTeX
LatexCmds.thetasym = //W3C/Unicode
- bind(Variable,'\\vartheta ','ϑ');
+ bind(Variable,'\\vartheta ','ϑ', 'theta');
LatexCmds.upsilon = //AMS and LaTeX and W3C/Unicode
LatexCmds.upsi = //Elsevier and 9573-13
- bind(Variable,'\\upsilon ','υ');
+ bind(Variable,'\\upsilon ','υ', 'upsilon');
//these aren't even mentioned in the HTML character entity references
LatexCmds.gammad = //Elsevier
LatexCmds.Gammad = //9573-13 -- WTF, right? I dunno if this was a typo in the reference (see above)
LatexCmds.digamma = //LaTeX
- bind(Variable,'\\digamma ','ϝ');
+ bind(Variable,'\\digamma ','ϝ', 'gamma');
LatexCmds.kappav = //Elsevier
LatexCmds.varkappa = //AMS and LaTeX
- bind(Variable,'\\varkappa ','ϰ');
+ bind(Variable,'\\varkappa ','ϰ', 'kappa');
LatexCmds.rhov = //Elsevier and 9573-13
LatexCmds.varrho = //AMS and LaTeX
- bind(Variable,'\\varrho ','ϱ');
+ bind(Variable,'\\varrho ','ϱ', 'rho');
//Greek constants, look best in non-italicized Times New Roman
-LatexCmds.pi = LatexCmds['π'] = bind(NonSymbolaSymbol,'\\pi ','π');
-LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ');
+LatexCmds.pi = LatexCmds['π'] = bind(NonSymbolaSymbol,'\\pi ','π', 'pi');
+LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ', 'lambda');
//uppercase greek letters
@@ -326,7 +687,7 @@ LatexCmds.Upsilon = //LaTeX
LatexCmds.Upsi = //Elsevier and 9573-13
LatexCmds.upsih = //W3C/Unicode "upsilon with hook"
LatexCmds.Upsih = //'cos it makes sense to me
- bind(Symbol,'\\Upsilon ','ϒ'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :(
+ bind(Symbol,'\\Upsilon ','ϒ', 'capital upsilon'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :(
//other symbols with the same LaTeX command and HTML character entity reference
LatexCmds.Gamma =
@@ -357,8 +718,9 @@ var LatexFragment = P(MathCommand, function(_) {
block.finalizeInsert(cursor.options, cursor);
if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L);
if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R);
- cursor.parent.bubble('reflow');
+ cursor.parent.bubble(function (node) { node.reflow(); });
};
+ _.mathspeak = function() { return latexMathParser.parse(this.latex).mathspeak(); };
_.parser = function() {
var frag = latexMathParser.parse(this.latex).children();
return Parser.succeed(frag);
@@ -374,11 +736,12 @@ var LatexFragment = P(MathCommand, function(_) {
// largely coincides with, so Microsoft Word sometimes inserts them
// and they get copy-pasted into MathQuill.
//
-// (Irrelevant but funny story: Windows-1252 is actually a strict
-// superset of the "closely related but distinct"[3] "ISO 8859-1" --
-// see the lack of a dash after "ISO"? Completely different character
-// set, like elephants vs elephant seals, or "Zombies" vs "Zombie
-// Redneck Torture Family". What kind of idiot would get them confused.
+// (Irrelevant but funny story: though not a superset of Latin-1 aka
+// ISO-8859-1, Windows-1252 **is** a strict superset of the "closely
+// related but distinct"[3] "ISO 8859-1" -- see the lack of a dash
+// after "ISO"? Completely different character set, like elephants vs
+// elephant seals, or "Zombies" vs "Zombie Redneck Torture Family".
+// What kind of idiot would get them confused.
// People in fact got them confused so much, it was so common to
// mislabel Windows-1252 text as ISO-8859-1, that most modern web
// browsers and email clients treat the MIME charset of ISO-8859-1
@@ -388,35 +751,125 @@ var LatexFragment = P(MathCommand, function(_) {
// [2]: http://en.wikipedia.org/wiki/Number_Forms
// [3]: http://en.wikipedia.org/wiki/ISO/IEC_8859-1
// [4]: http://en.wikipedia.org/wiki/Windows-1252
+LatexCmds['⁰'] = bind(LatexFragment, '^0');
LatexCmds['¹'] = bind(LatexFragment, '^1');
LatexCmds['²'] = bind(LatexFragment, '^2');
LatexCmds['³'] = bind(LatexFragment, '^3');
+LatexCmds['⁴'] = bind(LatexFragment, '^4');
+LatexCmds['⁵'] = bind(LatexFragment, '^5');
+LatexCmds['⁶'] = bind(LatexFragment, '^6');
+LatexCmds['⁷'] = bind(LatexFragment, '^7');
+LatexCmds['⁸'] = bind(LatexFragment, '^8');
+LatexCmds['⁹'] = bind(LatexFragment, '^9');
+
LatexCmds['¼'] = bind(LatexFragment, '\\frac14');
LatexCmds['½'] = bind(LatexFragment, '\\frac12');
LatexCmds['¾'] = bind(LatexFragment, '\\frac34');
+// this is a hack to make pasting the √ symbol
+// actually insert a sqrt command. This isn't ideal,
+// but it's way better than what we have now. I think
+// before we invest any more time into this single character
+// we should consider how to make the pipe (|) automatically
+// insert absolute value. We also will want the percent (%)
+// to expand to '% of'. I've always just thought mathquill's
+// ability to handle pasted latex magical until I started actually
+// testing it. It's a lot more buggy that I previously thought.
+//
+// KNOWN ISSUES:
+// 1) pasting √ does not put focus in side the sqrt symbol
+// 2) pasting √2 puts the 2 outside of the sqrt symbol.
+//
+// The first issue seems like we could invest more time into this to
+// fix it, but doesn't feel worth special casing. I think we'd want
+// to address it by addressing ALL pasting issues.
+//
+// The second issue seems like it might go away too if you fix paste to
+// act more like simply typing the characters out. I'd be scared to try
+// to make that change because I'm fairly confident I'd break something
+// around handling valid latex as latex rather than treating it as keystrokes.
+LatexCmds['√'] = bind(LatexFragment, '\\sqrt{}');
+
+// Binary operator determination is used in several contexts for PlusMinus nodes and their descendants.
+// For instance, we set the item's class name based on this factor, and also assign different mathspeak values (plus vs positive, negative vs minus).
+function isBinaryOperator(node) {
+ if (node[L]) {
+ // If the left sibling is a binary operator or a separator (comma, semicolon, colon, space)
+ // or an open bracket (open parenthesis, open square bracket)
+ // consider the operator to be unary
+ if (node[L] instanceof BinaryOperator || /^(\\ )|[,;:\(\[]$/.test(node[L].ctrlSeq)) {
+ return false;
+ }
+ } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) {
+ //if we are in a style block at the leftmost edge, determine unary/binary based on
+ //the style block
+ //this allows style blocks to be transparent for unary/binary purposes
+ return isBinaryOperator(node.parent.parent);
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
var PlusMinus = P(BinaryOperator, function(_) {
- _.init = VanillaSymbol.prototype.init;
+ _.init = VanillaSymbol.prototype.init;
_.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
if (dir === R) return; // ignore if sibling only changed on the right
- this.jQ[0].className =
- (!this[L] || this[L] instanceof BinaryOperator ? '' : 'mq-binary-operator');
+ this.jQ[0].className = isBinaryOperator(this)
+ ? 'mq-binary-operator'
+ : '';
+
return this;
};
});
-LatexCmds['+'] = bind(PlusMinus, '+', '+');
-//yes, these are different dashes, I think one is an en dash and the other is a hyphen
-LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−');
+LatexCmds['+'] = P(PlusMinus, function(_, super_) {
+ _.init = function () {
+ super_.init.call(this, '+', '+');
+ };
+ _.mathspeak = function() {
+ return isBinaryOperator(this) ? 'plus' : 'positive';
+ };
+});
+
+//yes, these are different dashes, en-dash, em-dash, unicode minus, actual dash
+LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(PlusMinus, function(_, super_) {
+ _.init = function () {
+ super_.init.call(this, '-', '−');
+ };
+ _.mathspeak = function() {
+ return isBinaryOperator(this) ? 'minus' : 'negative';
+ };
+});
+
LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus =
- bind(PlusMinus,'\\pm ','±');
+ bind(PlusMinus,'\\pm ','±', 'plus-or-minus');
LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus =
- bind(PlusMinus,'\\mp ','∓');
+ bind(PlusMinus,'\\mp ','∓', 'minus-or-plus');
CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot =
- bind(BinaryOperator, '\\cdot ', '·');
-//semantically should be ⋅, but · looks better
+ bind(BinaryOperator, '\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better
+
+var To = P(BinaryOperator, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, '\\to ','→', 'to');
+ }
+ _.deleteTowards = function(dir, cursor) {
+ if (dir === L) {
+ var l = cursor[L];
+ Fragment(l, this).remove();
+ cursor[L] = l[L];
+ LatexCmds['−']().createLeftOf(cursor);
+ cursor[L].bubble(function (node) { node.reflow(); });
+ return;
+ }
+ super_.deleteTowards.apply(this, arguments);
+ };
+})
+
+LatexCmds['→'] = LatexCmds.to = To;
var Inequality = P(BinaryOperator, function(_, super_) {
_.init = function(data, strict) {
@@ -424,7 +877,7 @@ var Inequality = P(BinaryOperator, function(_, super_) {
this.strict = strict;
var strictness = (strict ? 'Strict' : '');
super_.init.call(this, data['ctrlSeq'+strictness], data['html'+strictness],
- data['text'+strictness]);
+ data['text'+strictness], data['mathspeak'+strictness]);
};
_.swap = function(strict) {
this.strict = strict;
@@ -432,35 +885,56 @@ var Inequality = P(BinaryOperator, function(_, super_) {
this.ctrlSeq = this.data['ctrlSeq'+strictness];
this.jQ.html(this.data['html'+strictness]);
this.textTemplate = [ this.data['text'+strictness] ];
+ this.mathspeakName = this.data['mathspeak'+strictness];
};
_.deleteTowards = function(dir, cursor) {
if (dir === L && !this.strict) {
this.swap(true);
- this.bubble('reflow');
+ this.bubble(function (node) { node.reflow(); });
return;
}
super_.deleteTowards.apply(this, arguments);
};
});
-var less = { ctrlSeq: '\\le ', html: '≤', text: '≤',
- ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<' };
-var greater = { ctrlSeq: '\\ge ', html: '≥', text: '≥',
- ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>' };
+var less = { ctrlSeq: '\\le ', html: '≤', text: '≤', mathspeak: 'less than or equal to',
+ ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<', mathspeakStrict: 'less than'};
+var greater = { ctrlSeq: '\\ge ', html: '≥', text: '≥', mathspeak: 'greater than or equal to',
+ ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>', mathspeakStrict: 'greater than'};
+
+var Greater = P(Inequality, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, greater, true);
+ };
+ _.createLeftOf = function(cursor) {
+ if (cursor[L] instanceof BinaryOperator && cursor[L].ctrlSeq === '-') {
+ var l = cursor[L];
+ cursor[L] = l[L];
+ l.remove();
+ To().createLeftOf(cursor);
+ cursor[L].bubble(function (node) { node.reflow(); });
+ return;
+ }
+ super_.createLeftOf.apply(this, arguments);
+ };
+})
LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true);
-LatexCmds['>'] = LatexCmds.gt = bind(Inequality, greater, true);
+LatexCmds['>'] = LatexCmds.gt = Greater;
LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false);
LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false);
+LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity =
+ bind(VanillaSymbol,'\\infty ','∞', 'infinity');
+LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal');
var Equality = P(BinaryOperator, function(_, super_) {
_.init = function() {
- super_.init.call(this, '=', '=');
+ super_.init.call(this, '=', '=', '=', 'equals');
};
_.createLeftOf = function(cursor) {
if (cursor[L] instanceof Inequality && cursor[L].strict) {
cursor[L].swap(false);
- cursor[L].bubble('reflow');
+ cursor[L].bubble(function (node) { node.reflow(); });
return;
}
super_.createLeftOf.apply(this, arguments);
@@ -468,9 +942,45 @@ var Equality = P(BinaryOperator, function(_, super_) {
});
LatexCmds['='] = Equality;
-LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]');
+LatexCmds['×'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]', 'times');
LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides =
- bind(BinaryOperator,'\\div ','÷', '[/]');
+ bind(BinaryOperator,'\\div ','÷', '[/]', 'over');
+
+
+var Sim = P(BinaryOperator, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, '\\sim ', '~', '~', 'tilde');
+ };
+ _.createLeftOf = function(cursor) {
+ if (cursor[L] instanceof Sim) {
+ var l = cursor[L];
+ cursor[L] = l[L];
+ l.remove();
+ Approx().createLeftOf(cursor);
+ cursor[L].bubble(function (node) { node.reflow(); });
+ return;
+ }
+ super_.createLeftOf.apply(this, arguments);
+ };
+});
+
+var Approx = P(BinaryOperator, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, '\\approx ', '≈', '≈', 'approximately equal');
+ };
+ _.deleteTowards = function(dir, cursor) {
+ if (dir === L) {
+ var l = cursor[L];
+ Fragment(l, this).remove();
+ cursor[L] = l[L];
+ Sim().createLeftOf(cursor);
+ cursor[L].bubble(function (node) { node.reflow(); });
+ return;
+ }
+ super_.deleteTowards.apply(this, arguments);
+ };
+});
-CharCmds['~'] = LatexCmds.sim = bind(BinaryOperator, '\\sim ', '~', '~');
+CharCmds['~'] = LatexCmds.sim = Sim;
+LatexCmds['≈'] = LatexCmds.approx = Approx;
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index bfc2f1a14..445329d17 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -1,84 +1,136 @@
/***************************
* Commands and Operators.
**************************/
-
-var scale, // = function(jQ, x, y) { ... }
-//will use a CSS 2D transform to scale the jQuery-wrapped HTML elements,
-//or the filter matrix transform fallback for IE 5.5-8, or gracefully degrade to
-//increasing the fontSize to match the vertical Y scaling factor.
-
-//ideas from http://github.com/louisremi/jquery.transform.js
-//see also http://msdn.microsoft.com/en-us/library/ms533014(v=vs.85).aspx
-
- forceIERedraw = noop,
- div = document.createElement('div'),
- div_style = div.style,
- transformPropNames = {
- transform:1,
- WebkitTransform:1,
- MozTransform:1,
- OTransform:1,
- msTransform:1
+var SVG_SYMBOLS = {
+ 'sqrt': {
+ html:
+ ''
},
- transformPropName;
-
-for (var prop in transformPropNames) {
- if (prop in div_style) {
- transformPropName = prop;
- break;
+ '|': {
+ width: '.4em',
+ html:
+ ''
+ },
+ '[': {
+ width: '.55em',
+ html:
+ ''
+ },
+ ']': {
+ width: '.55em',
+ html:
+ ''
+ },
+ '(': {
+ width: '.55em',
+ html:
+ ''
+ },
+ ')': {
+ width: '.55em',
+ html:
+ ''
+ },
+ '{': {
+ width: '.7em',
+ html:
+ ''
+ },
+ '}': {
+ width: '.7em',
+ html:
+ ''
+ },
+ '∥': {
+ width: '.7em',
+ html:
+ ''
+ },
+ '〈': {
+ width: '.55em',
+ html:
+ ''
+ },
+ '〉': {
+ width: '.55em',
+ html:
+ ''
}
-}
-
-if (transformPropName) {
- scale = function(jQ, x, y) {
- jQ.css(transformPropName, 'scale('+x+','+y+')');
- };
-}
-else if ('filter' in div_style) { //IE 6, 7, & 8 fallback, see https://github.com/laughinghan/mathquill/wiki/Transforms
- forceIERedraw = function(el){ el.className = el.className; };
- scale = function(jQ, x, y) { //NOTE: assumes y > x
- x /= (1+(y-1)/2);
- jQ.css('fontSize', y + 'em');
- if (!jQ.hasClass('mq-matrixed-container')) {
- jQ.addClass('mq-matrixed-container')
- .wrapInner('');
- }
- var innerjQ = jQ.children()
- .css('filter', 'progid:DXImageTransform.Microsoft'
- + '.Matrix(M11=' + x + ",SizingMethod='auto expand')"
- );
- function calculateMarginRight() {
- jQ.css('marginRight', (innerjQ.width()-1)*(x-1)/x + 'px');
- }
- calculateMarginRight();
- var intervalId = setInterval(calculateMarginRight);
- $(window).load(function() {
- clearTimeout(intervalId);
- calculateMarginRight();
- });
- };
-}
-else {
- scale = function(jQ, x, y) {
- jQ.css('fontSize', y + 'em');
- };
-}
+};
var Style = P(MathCommand, function(_, super_) {
- _.init = function(ctrlSeq, tagName, attrs) {
+ _.init = function(ctrlSeq, tagName, attrs, ariaLabel, opts) {
super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'+tagName+'>');
+ _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, '');
+ _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel];
+ // In most cases, mathspeak should announce the start and end of style blocks.
+ // There is one exception currently (mathrm).
+ _.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters;
+ };
+ _.mathspeak = function(opts) {
+ if (
+ !this.shouldNotSpeakDelimiters ||
+ (opts && opts.ignoreShorthand)
+ ) {
+ return super_.mathspeak.call(this);
+ }
+ return this.foldChildren('', function(speech, block) {
+ return speech + ' ' + block.mathspeak(opts);
+ }).trim();
};
});
//fonts
-LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"');
-LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"');
-LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"');
-LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"');
-LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"');
+LatexCmds.mathrm = P(Style, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', { shouldNotSpeakDelimiters: true });
+ };
+ _.isTextBlock = function() {
+ return true;
+ };
+});
+LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"', 'Italic Font');
+LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"', 'Bold Font');
+LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"', 'Serif Font');
+LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"', 'Math Text');
//text-decoration
-LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"');
-LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"');
+LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"', 'Underline');
+LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"', 'Overline');
+LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"', 'Over Right Arrow');
+LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"', 'Over Left Arrow');
+LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow ', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-leftright"', 'Over Left and Right Arrow');
+LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"', 'Over Arc');
+LatexCmds.dot = P(MathCommand, function(_, super_) {
+ _.init = function() {
+ super_.init.call(this, '\\dot', ''
+ + '˙'
+ + '&0'
+ + ''
+ );
+ };
+});
// `\textcolor{color}{math}` will apply a color to the given math content, where
// `color` is any valid CSS Color Value (see [SitePoint docs][] (recommended),
@@ -92,6 +144,8 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) {
this.color = color;
this.htmlTemplate =
'&0';
+ _.ariaLabel = color.replace(/^\\/, '');
+ _.mathspeakTemplate = ['Start ' + _.ariaLabel + ',', 'End ' + _.ariaLabel];
};
_.latex = function() {
return '\\textcolor{' + this.color + '}{' + this.blocks[0].latex() + '}';
@@ -112,6 +166,9 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) {
})
;
};
+ _.isStyleBlock = function() {
+ return true;
+ };
});
// Very similar to the \textcolor command, but will add the given CSS class.
@@ -126,17 +183,48 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) {
.then(regex(/^[-\w\s\\\xA0-\xFF]*/))
.skip(string('}'))
.then(function(cls) {
+ self.cls = cls || '';
self.htmlTemplate = '&0';
+ self.ariaLabel = cls + ' class';
+ self.mathspeakTemplate = ['Start ' + self.ariaLabel + ',', 'End ' + self.ariaLabel];
return super_.parser.call(self);
})
;
};
+ _.latex = function() {
+ return '\\class{' + this.cls + '}{' + this.blocks[0].latex() + '}';
+ };
+ _.isStyleBlock = function() {
+ return true;
+ };
});
+// This test is used to determine whether an item may be treated as a whole number
+// for shortening the verbalized (mathspeak) forms of some fractions and superscripts.
+var intRgx = /^[\+\-]?[\d]+$/;
+
+// Traverses the top level of the passed block's children and returns the concatenation of their ctrlSeq properties.
+// Used in shortened mathspeak computations as a block's .text() method can be potentially expensive.
+//
+function getCtrlSeqsFromBlock(block) {
+ if (
+ typeof(block) !== 'object' ||
+ typeof(block.children) !== 'function'
+ )
+ return block;
+ var children = block.children();
+ if (!children || !children.ends[L]) return block;
+ var chars = '';
+ for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) {
+ if (sibling.ctrlSeq !== undefined) chars += sibling.ctrlSeq;
+ }
+ return chars;
+}
+
var SupSub = P(MathCommand, function(_, super_) {
_.ctrlSeq = '_{...}^{...}';
_.createLeftOf = function(cursor) {
- if (!cursor[L] && cursor.options.supSubsRequireOperand) return;
+ if (!this.replacedFragment && !cursor[L] && cursor.options.supSubsRequireOperand) return;
return super_.createLeftOf.apply(this, arguments);
};
_.contactWeld = function(cursor) {
@@ -179,21 +267,23 @@ var SupSub = P(MathCommand, function(_, super_) {
break;
}
}
- this.respace();
};
Options.p.charsThatBreakOutOfSupSub = '';
_.finalizeTree = function() {
this.ends[L].write = function(cursor, ch) {
if (cursor.options.autoSubscriptNumerals && this === this.parent.sub) {
if (ch === '_') return;
- var cmd = this.chToCmd(ch);
+ var cmd = this.chToCmd(ch, cursor.options);
if (cmd instanceof Symbol) cursor.deleteSelection();
else cursor.clearSelection().insRightOf(this.parent);
- return cmd.createLeftOf(cursor.show());
+ cmd.createLeftOf(cursor.show());
+ aria.queue('Baseline').alert(cmd.mathspeak({ createdLeftOf: cursor }));
+ return;
}
if (cursor[L] && !cursor[R] && !cursor.selection
&& cursor.options.charsThatBreakOutOfSupSub.indexOf(ch) > -1) {
cursor.insRightOf(this.parent);
+ aria.queue('Baseline');
}
MathBlock.p.write.apply(this, arguments);
};
@@ -224,28 +314,34 @@ var SupSub = P(MathCommand, function(_, super_) {
_.latex = function() {
function latex(prefix, block) {
var l = block && block.latex();
- return block ? prefix + (l.length === 1 ? l : '{' + (l || ' ') + '}') : '';
+ return block ? prefix + '{' + (l || ' ') + '}' : '';
}
return latex('_', this.sub) + latex('^', this.sup);
};
- _.respace = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
- if (dir === R) return; // ignore if sibling only changed on the right
- this.jQ.toggleClass('mq-limit', this[L].ctrlSeq === '\\int ');
+ _.text = function() {
+ function text(prefix, block) {
+ var l = block && block.text();
+ return block ? prefix + (l.length === 1 ? l : '(' + (l || ' ') + ')') : '';
+ }
+ return text('_', this.sub) + text('^', this.sup);
};
_.addBlock = function(block) {
if (this.supsub === 'sub') {
this.sup = this.upInto = this.sub.upOutOf = block;
block.adopt(this, this.sub, 0).downOutOf = this.sub;
- block.jQ = $('').append(block.jQ.children())
- .attr(mqBlockId, block.id).prependTo(this.jQ);
+ block.jQ = $('').append(block.jQ.children()).prependTo(this.jQ);
+ Node.linkElementByBlockNode(block.jQ[0], block);
}
else {
this.sub = this.downInto = this.sup.downOutOf = block;
block.adopt(this, 0, this.sup).upOutOf = this.sup;
block.jQ = $('').append(block.jQ.children())
- .attr(mqBlockId, block.id).appendTo(this.jQ.removeClass('mq-sup-only'));
+ .appendTo(this.jQ.removeClass('mq-sup-only'));
+ Node.linkElementByBlockNode(block.jQ[0], block);
this.jQ.append('');
}
+
+
// like 'sub sup'.split(' ').forEach(function(supsub) { ... });
for (var i = 0; i < 2; i += 1) (function(cmd, supsub, oppositeSupsub, updown) {
cmd[supsub].deleteOutOf = function(dir, cursor) {
@@ -267,6 +363,28 @@ var SupSub = P(MathCommand, function(_, super_) {
};
}(this, 'sub sup'.split(' ')[i], 'sup sub'.split(' ')[i], 'down up'.split(' ')[i]));
};
+ _.reflow = function() {
+ var $block = this.jQ ;//mq-supsub
+ var $prev = $block.prev() ;
+
+ if ( !$prev.length ) {
+ //we cant normalize it without having prev. element (which is base)
+ return ;
+ }
+
+ var $sup = $block.children( '.mq-sup' );//mq-supsub -> mq-sup
+ if ( $sup.length ) {
+ var sup_fontsize = parseInt( $sup.css('font-size') ) ;
+ var sup_bottom = $sup.offset().top + $sup.height() ;
+ //we want that superscript overlaps top of base on 0.7 of its font-size
+ //this way small superscripts like x^2 look ok, but big ones like x^(1/2/3) too
+ var needed = sup_bottom - $prev.offset().top - 0.7*sup_fontsize ;
+ var cur_margin = parseInt( $sup.css('margin-bottom' ) ) ;
+ //we lift it up with margin-bottom
+ $sup.css( 'margin-bottom', cur_margin + needed ) ;
+ }
+ } ;
+
});
function insLeftOfMeUnlessAtEnd(cursor) {
@@ -290,6 +408,8 @@ LatexCmds._ = P(SupSub, function(_, super_) {
+ ''
;
_.textTemplate = [ '_' ];
+ _.mathspeakTemplate = [ 'Subscript,', ', Baseline'];
+ _.ariaLabel = 'subscript';
_.finalizeTree = function() {
this.downInto = this.sub = this.ends[L];
this.sub.upOutOf = insLeftOfMeUnlessAtEnd;
@@ -306,7 +426,54 @@ LatexCmds['^'] = P(SupSub, function(_, super_) {
+ '&0'
+ ''
;
- _.textTemplate = [ '^' ];
+ _.textTemplate = ['^(', ')'];
+ _.mathspeak = function(opts) {
+ // Simplify basic exponent speech for common whole numbers.
+ var child = this.upInto;
+ if (child !== undefined) {
+ // Calculate this item's inner text to determine whether to shorten the returned speech.
+ // Do not calculate its inner mathspeak now until we know that the speech is to be truncated.
+ // Since the mathspeak computation is recursive, we want to call it only once in this function to avoid performance bottlenecks.
+ var innerText = getCtrlSeqsFromBlock(child);
+ // If the superscript is a whole number, shorten the speech that is returned.
+ if (
+ (!opts || !opts.ignoreShorthand) &&
+ intRgx.test(innerText)
+ ) {
+ // Simple cases
+ if (innerText === '0') {
+ return 'to the 0 power';
+ } else if (innerText === '2') {
+ return 'squared';
+ } else if (innerText === '3') {
+ return 'cubed';
+ }
+
+ // More complex cases.
+ var suffix = '';
+ // Limit suffix addition to exponents < 1000.
+ if (/^[+-]?\d{1,3}$/.test(innerText)) {
+ if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) {
+ suffix = 'th';
+ } else if (/1$/.test(innerText)) {
+ suffix = 'st';
+ } else if (/2$/.test(innerText)) {
+ suffix = 'nd';
+ } else if (/3$/.test(innerText)) {
+ suffix = 'rd';
+ }
+ }
+ var innerMathspeak = typeof(child) === 'object'
+ ? child.mathspeak()
+ : innerText;
+ return 'to the ' + innerMathspeak + suffix + ' power';
+ }
+ }
+ return super_.mathspeak.call(this);
+ };
+
+ _.ariaLabel = 'superscript';
+ _.mathspeakTemplate = [ 'Superscript,', ', Baseline'];
_.finalizeTree = function() {
this.upInto = this.sup = this.ends[R];
this.sup.downOutOf = insLeftOfMeUnlessAtEnd;
@@ -315,7 +482,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) {
});
var SummationNotation = P(MathCommand, function(_, super_) {
- _.init = function(ch, html) {
+ _.init = function(ch, html, ariaLabel) {
+ _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, '');
var htmlTemplate =
''
+ '&1'
@@ -334,11 +502,15 @@ var SummationNotation = P(MathCommand, function(_, super_) {
};
_.latex = function() {
function simplify(latex) {
- return latex.length === 1 ? latex : '{' + (latex || ' ') + '}';
+ return '{' + (latex || ' ') + '}';
}
return this.ctrlSeq + '_' + simplify(this.ends[L].latex()) +
'^' + simplify(this.ends[R].latex());
};
+ _.mathspeak = function() {
+ return 'Start ' + this.ariaLabel + ' from ' + this.ends[L].mathspeak() +
+ ' to ' + this.ends[R].mathspeak() + ', end ' + this.ariaLabel + ', ';
+ };
_.parser = function() {
var string = Parser.string;
var optWhitespace = Parser.optWhitespace;
@@ -360,6 +532,8 @@ var SummationNotation = P(MathCommand, function(_, super_) {
}).many().result(self);
};
_.finalizeTree = function() {
+ this.ends[L].ariaLabel = 'lower bound';
+ this.ends[R].ariaLabel = 'upper bound';
this.downInto = this.ends[L];
this.upInto = this.ends[R];
this.ends[L].upOutOf = this.ends[R];
@@ -369,15 +543,35 @@ var SummationNotation = P(MathCommand, function(_, super_) {
LatexCmds['∑'] =
LatexCmds.sum =
-LatexCmds.summation = bind(SummationNotation,'\\sum ','∑');
+LatexCmds.summation = bind(SummationNotation,'\\sum ','∑', 'sum');
LatexCmds['∏'] =
LatexCmds.prod =
-LatexCmds.product = bind(SummationNotation,'\\prod ','∏');
+LatexCmds.product = bind(SummationNotation,'\\prod ','∏', 'product');
LatexCmds.coprod =
-LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐');
+LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐', 'co product');
+LatexCmds['∫'] =
+LatexCmds['int'] =
+LatexCmds.integral = P(SummationNotation, function(_, super_) {
+ _.init = function() {
+ _.ariaLabel = 'integral';
+ var htmlTemplate =
+ ''
+ + '∫'
+ + ''
+ + '&1'
+ + '&0'
+ + ''
+ + ''
+ + ''
+ ;
+ Symbol.prototype.init.call(this, '\\int ', htmlTemplate, 'integral');
+ };
+ // FIXME: refactor rather than overriding
+ _.createLeftOf = MathCommand.p.createLeftOf;
+});
var Fraction =
LatexCmds.frac =
LatexCmds.dfrac =
@@ -395,6 +589,100 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) {
_.finalizeTree = function() {
this.upInto = this.ends[R].upOutOf = this.ends[L];
this.downInto = this.ends[L].downOutOf = this.ends[R];
+ this.ends[L].ariaLabel = 'numerator';
+ this.ends[R].ariaLabel = 'denominator';
+ if(this.getFracDepth() > 1) {
+ this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction'];
+ } else {
+ this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction'];
+ }
+ };
+
+ _.mathspeak = function(opts) {
+ if (opts && opts.createdLeftOf) {
+ var cursor = opts.createdLeftOf;
+ return cursor.parent.mathspeak();
+ }
+
+ var numText = getCtrlSeqsFromBlock(this.ends[L]);
+ var denText = getCtrlSeqsFromBlock(this.ends[R]);
+
+ // Shorten mathspeak value for whole number fractions whose denominator is less than 10.
+ if (
+ (!opts || !opts.ignoreShorthand) &&
+ intRgx.test(numText) && intRgx.test(denText)
+ ) {
+ var isSingular = numText === '1' || numText === '-1';
+ var newDenSpeech = '';
+ if (denText === '2') {
+ newDenSpeech = isSingular
+ ? 'half'
+ : 'halves';
+ } else if (denText === '3') {
+ newDenSpeech = isSingular
+ ? 'third'
+ : 'thirds';
+ } else if (denText === '4') {
+ newDenSpeech = isSingular
+ ? 'quarter'
+ : 'quarters';
+ } else if (denText === '5') {
+ newDenSpeech = isSingular
+ ? 'fifth'
+ : 'fifths';
+ } else if (denText === '6') {
+ newDenSpeech = isSingular
+ ? 'sixth'
+ : 'sixths';
+ } else if (denText === '7') {
+ newDenSpeech = isSingular
+ ? 'seventh'
+ : 'sevenths';
+ } else if (denText === '8') {
+ newDenSpeech = isSingular
+ ? 'eighth'
+ : 'eighths';
+ } else if (denText === '9') {
+ newDenSpeech = isSingular
+ ? 'ninth'
+ : 'ninths';
+ }
+ if (newDenSpeech !== '') {
+ var output = '';
+ // Handle the case of an integer followed by a simplified fraction such as 1\frac{1}{2}.
+ // Such combinations should be spoken aloud as "1 and 1 half."
+ // Start at the left sibling of the fraction and continue leftward until something other than a digit or whitespace is found.
+ var precededByInteger = false;
+ for (var sibling = this[L]; sibling[L] !== undefined; sibling = sibling[L]) {
+ // Ignore whitespace
+ if (sibling.ctrlSeq === '\\ ') {
+ continue;
+ } else if (intRgx.test(sibling.ctrlSeq)) {
+ precededByInteger = true;
+ } else {
+ precededByInteger = false;
+ break;
+ }
+ }
+ if (precededByInteger) {
+ output += 'and ';
+ }
+ output += this.ends[L].mathspeak() + ' ' + newDenSpeech;
+ return output;
+ }
+ }
+
+ return super_.mathspeak.apply(this, arguments);
+ };
+
+ _.getFracDepth = function() {
+ var level = 0;
+ var walkUp = function(item, level) {
+ if(item instanceof Node && item.ctrlSeq && item.ctrlSeq.toLowerCase().search('frac') >= 0) level += 1;
+ if(item.parent) return walkUp(item.parent, level);
+ else return level;
+ };
+ return walkUp(this, level);
};
});
@@ -404,23 +692,25 @@ CharCmds['/'] = P(Fraction, function(_, super_) {
_.createLeftOf = function(cursor) {
if (!this.replacedFragment) {
var leftward = cursor[L];
- while (leftward &&
- !(
- leftward instanceof BinaryOperator ||
- leftward instanceof (LatexCmds.text || noop) ||
- leftward instanceof SummationNotation ||
- leftward.ctrlSeq === '\\ ' ||
- /^[,;:]$/.test(leftward.ctrlSeq)
- ) //lookbehind for operator
- ) leftward = leftward[L];
+ if (!cursor.options.typingSlashCreatesNewFraction) {
+ while (leftward &&
+ !(
+ leftward instanceof BinaryOperator ||
+ leftward instanceof (LatexCmds.text || noop) ||
+ leftward instanceof SummationNotation ||
+ leftward.ctrlSeq === '\\ ' ||
+ /^[,;:]$/.test(leftward.ctrlSeq)
+ ) //lookbehind for operator
+ ) leftward = leftward[L];
+ }
if (leftward instanceof SummationNotation && leftward[R] instanceof SupSub) {
leftward = leftward[R];
if (leftward[R] instanceof SupSub && leftward[R].ctrlSeq != leftward.ctrlSeq)
leftward = leftward[R];
}
- if (leftward !== cursor[L]) {
+ if (leftward !== cursor[L] && !cursor.isTooDeep(1)) {
this.replaces(Fragment(leftward[R] || cursor.parent.ends[L], cursor[L]));
cursor[L] = leftward;
}
@@ -429,17 +719,42 @@ CharCmds['/'] = P(Fraction, function(_, super_) {
};
});
+LatexCmds.ans = P(Symbol, function(_, super_) {
+ _.init = function(ch) {
+ super_.init.call(this,
+ '\\operatorname{ans}',
+ 'ans',
+ 'ans'
+ );
+ };
+});
+
+LatexCmds.percent =
+LatexCmds.percentof = P(Symbol, function (_, super_) {
+ _.init = function () {
+ super_.init.call(
+ this,
+ '\\%\\operatorname{of}',
+ '% of ',
+ 'percent of'
+ )
+ };
+});
+
var SquareRoot =
-LatexCmds.sqrt =
-LatexCmds['√'] = P(MathCommand, function(_, super_) {
+LatexCmds.sqrt = P(MathCommand, function(_, super_) {
_.ctrlSeq = '\\sqrt';
_.htmlTemplate =
- ''
- + '√'
+ ''
+ + ''
+ + SVG_SYMBOLS.sqrt.html
+ + ''
+ '&0'
+ ''
;
_.textTemplate = ['sqrt(', ')'];
+ _.mathspeakTemplate = ['StartRoot,', ', EndRoot'];
+ _.ariaLabel = 'root';
_.parser = function() {
return latexMathParser.optBlock.then(function(optBlock) {
return latexMathParser.block.map(function(block) {
@@ -451,49 +766,79 @@ LatexCmds['√'] = P(MathCommand, function(_, super_) {
});
}).or(super_.parser.call(this));
};
- _.reflow = function() {
- var block = this.ends[R].jQ;
- scale(block.prev(), 1, block.innerHeight()/+block.css('fontSize').slice(0,-2) - .1);
- };
});
-var Vec = LatexCmds.vec = P(MathCommand, function(_, super_) {
- _.ctrlSeq = '\\vec';
+var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) {
+ _.ctrlSeq = '\\hat';
_.htmlTemplate =
''
- + '→'
- + '&0'
+ + '^'
+ + '&0'
+ ''
;
- _.textTemplate = ['vec(', ')'];
+ _.textTemplate = ['hat(', ')'];
});
var NthRoot =
LatexCmds.nthroot = P(SquareRoot, function(_, super_) {
_.htmlTemplate =
- '&0'
- + ''
- + '√'
- + '&1'
+ ''
+ + '&0'
+ + ''
+ + ''
+ + SVG_SYMBOLS.sqrt.html
+ + ''
+ + '&1'
+ + ''
+ ''
;
_.textTemplate = ['sqrt[', '](', ')'];
_.latex = function() {
return '\\sqrt['+this.ends[L].latex()+']{'+this.ends[R].latex()+'}';
};
+ _.mathspeak = function() {
+ var indexMathspeak = this.ends[L].mathspeak();
+ var radicandMathspeak = this.ends[R].mathspeak();
+ this.ends[L].ariaLabel = 'Index';
+ this.ends[R].ariaLabel = 'Radicand';
+ if (indexMathspeak === '3') { // cube root
+ return 'Start Cube Root, '+radicandMathspeak+', End Cube Root';
+ } else {
+ return 'Root Index '+indexMathspeak+', Start Root, '+radicandMathspeak+', End Root';
+ }
+ };
});
+var CubeRoot =
+LatexCmds.cbrt = P(NthRoot, function(_, super_) {
+ _.createLeftOf = function(cursor) {
+ super_.createLeftOf.apply(this, arguments);
+ Digit('3').createLeftOf(cursor);
+ cursor.controller.moveRight();
+ };
+});
+
+var DiacriticAbove = P(MathCommand, function(_, super_) {
+ _.init = function(ctrlSeq, symbol, textTemplate) {
+ var htmlTemplate =
+ ''
+ + ''+symbol+''
+ + '&0'
+ + ''
+ ;
+
+ super_.init.call(this, ctrlSeq, htmlTemplate, textTemplate);
+ };
+});
+LatexCmds.vec = bind(DiacriticAbove, '\\vec', '→', ['vec(', ')']);
+LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']);
+
function DelimsMixin(_, super_) {
_.jQadd = function() {
super_.jQadd.apply(this, arguments);
this.delimjQs = this.jQ.children(':first').add(this.jQ.children(':last'));
this.contentjQ = this.jQ.children(':eq(1)');
};
- _.reflow = function() {
- var height = this.contentjQ.outerHeight()
- / parseFloat(this.contentjQ.css('fontSize'));
- scale(this.delimjQs, min(1 + .2*(height - 1), 1.2), 1.2*height);
- };
}
// Round/Square/Curly/Angle Brackets (aka Parens/Brackets/Braces)
@@ -508,28 +853,51 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
this.sides[R] = { ch: close, ctrlSeq: end };
};
_.numBlocks = function() { return 1; };
- _.html = function() { // wait until now so that .side may
+ _.html = function() {
+ var leftSymbol = this.getSymbol(L);
+ var rightSymbol = this.getSymbol(R);
+
+ // wait until now so that .side may
this.htmlTemplate = // be set by createLeftOf or parser
- ''
- + ''
- + this.sides[L].ch
+ ''
+ + ''
+ + leftSymbol.html
+ ''
- + '&0'
- + ''
- + this.sides[R].ch
+ + '&0'
+ + ''
+ + rightSymbol.html
+ ''
+ ''
;
return super_.html.call(this);
};
+ _.getSymbol = function (side) {
+ return SVG_SYMBOLS[this.sides[side || R].ch] || {width: '0', html: ''};
+ };
_.latex = function() {
return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq;
};
- _.oppBrack = function(opts, node, expectedSide) {
- // return node iff it's a 1-sided bracket of expected side (if any, may be
- // undefined), and of opposite side from me if I'm not a pipe
+ _.mathspeak = function(opts) {
+ var open = this.sides[L].ch, close = this.sides[R].ch;
+ if (open === '|' && close === '|') {
+ this.mathspeakTemplate = ['StartAbsoluteValue,', ', EndAbsoluteValue'];
+ this.ariaLabel = 'absolute value';
+ }
+ else if (opts && opts.createdLeftOf && this.side) {
+ var ch = '';
+ if (this.side === L) ch = this.textTemplate[0];
+ else if (this.side === R) ch = this.textTemplate[1];
+ return (this.side === L ? 'left ' : 'right ') + BRACKET_NAMES[ch];
+ }
+ else {
+ this.mathspeakTemplate = ['left ' + BRACKET_NAMES[open]+',', ', right ' + BRACKET_NAMES[close]];
+ this.ariaLabel = BRACKET_NAMES[open]+' block';
+ }
+ return super_.mathspeak.call(this);
+ };
+ _.matchBrack = function(opts, expectedSide, node) {
+ // return node iff it's a matching 1-sided bracket of expected side (if any)
return node instanceof Bracket && node.side && node.side !== -expectedSide
- && (this.sides[this.side].ch === '|' || node.side === -this.side)
&& (!opts.restrictMismatchedBrackets
|| OPP_BRACKS[this.sides[this.side].ch] === node.sides[node.side].ch
|| { '(': ']', '[': ')' }[this.sides[L].ch] === node.sides[R].ch) && node;
@@ -537,17 +905,23 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
_.closeOpposing = function(brack) {
brack.side = 0;
brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be
- brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b))
- .removeClass('mq-ghost').html(this.sides[this.side].ch);
+ var $brack = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b))
+ .removeClass('mq-ghost');
+ this.replaceBracket($brack, this.side);
};
_.createLeftOf = function(cursor) {
if (!this.replacedFragment) { // unless wrapping seln in brackets,
// check if next to or inside an opposing one-sided bracket
- // (must check both sides 'cos I might be a pipe)
var opts = cursor.options;
- var brack = this.oppBrack(opts, cursor[L], L)
- || this.oppBrack(opts, cursor[R], R)
- || this.oppBrack(opts, cursor.parent.parent);
+ if (this.sides[L].ch === '|') { // check both sides if I'm a pipe
+ var brack = this.matchBrack(opts, R, cursor[R])
+ || this.matchBrack(opts, L, cursor[L])
+ || this.matchBrack(opts, 0, cursor.parent.parent);
+ }
+ else {
+ var brack = this.matchBrack(opts, -this.side, cursor[-this.side])
+ || this.matchBrack(opts, -this.side, cursor.parent.parent);
+ }
}
if (brack) {
var side = this.side = -brack.side; // may be pipe with .side not yet set
@@ -556,8 +930,8 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
Fragment(cursor[side], cursor.parent.ends[side], -side) // me and ghost outside
.disown().withDirAdopt(-side, brack.parent, brack, brack[side])
.jQ.insDirOf(side, brack.jQ);
- brack.bubble('reflow');
}
+ brack.bubble(function (node) { node.reflow(); });
}
else {
brack = this, side = brack.side;
@@ -589,7 +963,7 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
var opts = cursor.options, wasSolid = !this.side;
this.side = -side;
// if deleting like, outer close-brace of [(1+2)+3} where inner open-paren
- if (this.oppBrack(opts, this.ends[L].ends[this.side], side)) { // is ghost,
+ if (this.matchBrack(opts, side, this.ends[L].ends[this.side])) { // is ghost,
this.closeOpposing(this.ends[L].ends[this.side]); // then become [1+2)+3
var origEnd = this.ends[L].ends[side];
this.unwrap();
@@ -597,7 +971,7 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
sib ? cursor.insDirOf(-side, sib) : cursor.insAtDirEnd(side, parent);
}
else { // if deleting like, inner close-brace of ([1+2}+3) where outer
- if (this.oppBrack(opts, this.parent.parent, side)) { // open-paren is
+ if (this.matchBrack(opts, side, this.parent.parent)) { // open-paren is
this.parent.parent.closeOpposing(this); // ghost, then become [1+2+3)
this.parent.parent.unwrap();
} // else if deleting outward from a solid pair, unwrap
@@ -609,8 +983,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
else { // else deleting just one of a pair of brackets, become one-sided
this.sides[side] = { ch: OPP_BRACKS[this.sides[this.side].ch],
ctrlSeq: OPP_BRACKS[this.sides[this.side].ctrlSeq] };
- this.delimjQs.removeClass('mq-ghost')
- .eq(side === L ? 0 : 1).addClass('mq-ghost').html(this.sides[side].ch);
+ var $brack = this.delimjQs.removeClass('mq-ghost')
+ .eq(side === L ? 0 : 1).addClass('mq-ghost');
+ this.replaceBracket($brack, side);
}
if (sib) { // auto-expand so ghost is at far end
var origEnd = this.ends[L].ends[side];
@@ -624,6 +999,16 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
: cursor.insAtDirEnd(side, this.ends[L]));
}
};
+ _.replaceBracket = function ($brack, side) {
+ var symbol = this.getSymbol(side);
+ $brack.html(symbol.html).css('width', symbol.width);
+
+ if (side === L) {
+ $brack.next().css('margin-left', symbol.width);
+ } else {
+ $brack.prev().css('margin-right', symbol.width);
+ }
+ };
_.deleteTowards = function(dir, cursor) {
this.deleteSide(-dir, false, cursor);
};
@@ -656,20 +1041,32 @@ var OPP_BRACKS = {
'〉': '〈',
'\\langle ': '\\rangle ',
'\\rangle ': '\\langle ',
- '|': '|'
+ '|': '|',
+ '\\lVert ' : '\\rVert ',
+ '\\rVert ' : '\\lVert ',
+};
+
+var BRACKET_NAMES = {
+ '〈': 'angle-bracket',
+ '〉': 'angle-bracket',
+ '|': 'pipe'
};
-function bindCharBracketPair(open, ctrlSeq) {
+function bindCharBracketPair(open, ctrlSeq, name) {
var ctrlSeq = ctrlSeq || open, close = OPP_BRACKS[open], end = OPP_BRACKS[ctrlSeq];
CharCmds[open] = bind(Bracket, L, open, close, ctrlSeq, end);
CharCmds[close] = bind(Bracket, R, open, close, ctrlSeq, end);
+ BRACKET_NAMES[open] = BRACKET_NAMES[close] = name;
}
-bindCharBracketPair('(');
-bindCharBracketPair('[');
-bindCharBracketPair('{', '\\{');
+bindCharBracketPair('(', null, 'parenthesis');
+bindCharBracketPair('[', null, 'bracket');
+bindCharBracketPair('{', '\\{', 'brace');
LatexCmds.langle = bind(Bracket, L, '〈', '〉', '\\langle ', '\\rangle ');
LatexCmds.rangle = bind(Bracket, R, '〈', '〉', '\\langle ', '\\rangle ');
CharCmds['|'] = bind(Bracket, L, '|', '|', '|', '|');
+LatexCmds.lVert = bind(Bracket, L, '∥', '∥', '\\lVert ', '\\rVert ');
+LatexCmds.rVert = bind(Bracket, R, '∥', '∥', '\\lVert ', '\\rVert ');
+
LatexCmds.left = P(MathCommand, function(_) {
_.parser = function() {
@@ -678,13 +1075,17 @@ LatexCmds.left = P(MathCommand, function(_) {
var succeed = Parser.succeed;
var optWhitespace = Parser.optWhitespace;
- return optWhitespace.then(regex(/^(?:[([|]|\\\{)/))
- .then(function(ctrlSeq) { // TODO: \langle, \rangle
- var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : ctrlSeq);
+ return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle(?![a-zA-Z])|\\lVert(?![a-zA-Z]))/))
+ .then(function(ctrlSeq) {
+ var open = ctrlSeq.replace(/^\\/, '');
+ if (ctrlSeq=="\\langle") { open = '〈'; ctrlSeq = ctrlSeq + ' '; }
+ if (ctrlSeq=="\\lVert") { open = '∥'; ctrlSeq = ctrlSeq + ' '; }
return latexMathParser.then(function (block) {
return string('\\right').skip(optWhitespace)
- .then(regex(/^(?:[\])|]|\\\})/)).map(function(end) {
- var close = (end.charAt(0) === '\\' ? end.slice(1) : end);
+ .then(regex(/^(?:[\])|]|\\\}|\\rangle(?![a-zA-Z])|\\rVert(?![a-zA-Z]))/)).map(function(end) {
+ var close = end.replace(/^\\/, '');
+ if (end=="\\rangle") { close = '〉'; end = end + ' '; }
+ if (end=="\\rVert") { close = '∥'; end = end + ' '; }
var cmd = Bracket(0, open, close, ctrlSeq, end);
cmd.blocks = [ block ];
block.adopt(cmd, 0, 0);
@@ -706,20 +1107,29 @@ LatexCmds.right = P(MathCommand, function(_) {
var Binomial =
LatexCmds.binom =
LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) {
+ var leftSymbol = SVG_SYMBOLS['('];
+ var rightSymbol = SVG_SYMBOLS[')'];
+
_.ctrlSeq = '\\binom';
_.htmlTemplate =
- ''
- + '('
- + ''
+ ''
+ + ''
+ + leftSymbol.html
+ + ''
+ + ''
+ ''
+ '&0'
+ '&1'
+ ''
+ ''
- + ')'
+ + ''
+ + rightSymbol.html
+ + ''
+ ''
;
_.textTemplate = ['choose(',',',')'];
+ _.mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial'];
+ _.ariaLabel = 'binomial';
});
var Choose =
@@ -742,8 +1152,8 @@ LatexCmds.MathQuillMathField = P(MathCommand, function(_, super_) {
.map(function(name) { self.name = name; }).or(succeed())
.then(super_.parser.call(self));
};
- _.finalizeTree = function() {
- var ctrlr = Controller(this.ends[L], this.jQ, Options());
+ _.finalizeTree = function(options) {
+ var ctrlr = Controller(this.ends[L], this.jQ, options);
ctrlr.KIND_OF_MQ = 'MathField';
ctrlr.editable = true;
ctrlr.createTextarea();
@@ -758,12 +1168,34 @@ LatexCmds.MathQuillMathField = P(MathCommand, function(_, super_) {
_.text = function(){ return this.ends[L].text(); };
});
-var Embed = P(Symbol, function(_, super_) {
- _.init = function(options) {
- super_.init.call(this);
+// Embed arbitrary things
+// Probably the closest DOM analogue would be an iframe?
+// From MathQuill's perspective, it's a Symbol, it can be
+// anywhere and the cursor can go around it but never in it.
+// Create by calling public API method .dropEmbedded(),
+// or by calling the global public API method .registerEmbed()
+// and rendering LaTeX like \embed{registeredName} (see test).
+var Embed = LatexCmds.embed = P(Symbol, function(_, super_) {
+ _.setOptions = function(options) {
function noop () { return ""; }
this.text = options.text || noop;
this.htmlTemplate = options.htmlString || "";
this.latex = options.latex || noop;
- }
+ return this;
+ };
+ _.parser = function() {
+ var self = this,
+ string = Parser.string, regex = Parser.regex, succeed = Parser.succeed;
+ return string('{').then(regex(/^[a-z][a-z0-9]*/i)).skip(string('}'))
+ .then(function(name) {
+ // the chars allowed in the optional data block are arbitrary other than
+ // excluding curly braces and square brackets (which'd be too confusing)
+ return string('[').then(regex(/^[-\w\s]*/)).skip(string(']'))
+ .or(succeed()).map(function(data) {
+ return self.setOptions(EMBEDS[name](data));
+ })
+ ;
+ })
+ ;
+ };
});
diff --git a/src/commands/text.js b/src/commands/text.js
index 42f85d4b4..fdaaf5da0 100644
--- a/src/commands/text.js
+++ b/src/commands/text.js
@@ -10,6 +10,7 @@
*/
var TextBlock = P(Node, function(_, super_) {
_.ctrlSeq = '\\text';
+ _.ariaLabel = 'Text';
_.replaces = function(replacedText) {
if (replacedText instanceof Fragment)
@@ -27,15 +28,15 @@ var TextBlock = P(Node, function(_, super_) {
var textBlock = this;
super_.createLeftOf.call(this, cursor);
- if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L);
- if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R);
- textBlock.bubble('reflow');
-
cursor.insAtRightEnd(textBlock);
if (textBlock.replacedText)
for (var i = 0; i < textBlock.replacedText.length; i += 1)
textBlock.write(cursor, textBlock.replacedText.charAt(i));
+
+ if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L);
+ if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R);
+ textBlock.bubble(function (node) { node.reflow(); });
};
_.parser = function() {
@@ -48,10 +49,8 @@ var TextBlock = P(Node, function(_, super_) {
return optWhitespace
.then(string('{')).then(regex(/^[^}]*/)).skip(string('}'))
.map(function(text) {
- // TODO: is this the correct behavior when parsing
- // the latex \text{} ? This violates the requirement that
- // the text contents are always nonempty. Should we just
- // disown the parent node instead?
+ if (text.length === 0) return Fragment();
+
TextPiece(text).adopt(textBlock, 0, 0);
return textBlock;
})
@@ -64,7 +63,11 @@ var TextBlock = P(Node, function(_, super_) {
});
};
_.text = function() { return '"' + this.textContents() + '"'; };
- _.latex = function() { return '\\text{' + this.textContents() + '}'; };
+ _.latex = function() {
+ var contents = this.textContents();
+ if (contents.length === 0) return '';
+ return this.ctrlSeq + '{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}';
+ };
_.html = function() {
return (
''
@@ -72,12 +75,29 @@ var TextBlock = P(Node, function(_, super_) {
+ ''
);
};
+ _.mathspeakTemplate = ['Start'+_.ariaLabel, 'End'+_.ariaLabel];
+ _.mathspeak = function(opts) {
+ if (opts && opts.ignoreShorthand) {
+ return this.mathspeakTemplate[0]+', '+this.textContents() +', '+this.mathspeakTemplate[1]
+ } else {
+ return this.textContents();
+ }
+ };
+ _.isTextBlock = function() {
+ return true;
+ };
// editability methods: called by the cursor for editing, cursor movements,
// and selection of the MathQuill tree, these all take in a direction and
// the cursor
- _.moveTowards = function(dir, cursor) { cursor.insAtDirEnd(-dir, this); };
- _.moveOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); };
+ _.moveTowards = function(dir, cursor) {
+ cursor.insAtDirEnd(-dir, this);
+ aria.queueDirEndOf(-dir).queue(cursor.parent, true);
+ };
+ _.moveOutOf = function(dir, cursor) {
+ cursor.insDirOf(dir, this);
+ aria.queueDirOf(dir).queue(this);
+ };
_.unselectInto = _.moveTowards;
// TODO: make these methods part of a shared mixin or something.
@@ -107,12 +127,20 @@ var TextBlock = P(Node, function(_, super_) {
else { // split apart
var leftBlock = TextBlock();
var leftPc = this.ends[L];
- leftPc.disown();
+ leftPc.disown().jQ.detach();
leftPc.adopt(leftBlock, 0, 0);
cursor.insLeftOf(this);
- super_.createLeftOf.call(leftBlock, cursor);
+ super_.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness
}
+ this.bubble(function (node) { node.reflow(); });
+ // TODO needs tests
+ aria.alert(ch);
+ };
+ _.writeLatex = function(cursor, latex) {
+ if (!cursor[L]) TextPiece(latex).createLeftOf(cursor);
+ else cursor[L].appendText(latex);
+ this.bubble(function (node) { node.reflow(); });
};
_.seek = function(pageX, cursor) {
@@ -162,15 +190,22 @@ var TextBlock = P(Node, function(_, super_) {
}
};
- _.blur = function() {
+ _.blur = function(cursor) {
MathBlock.prototype.blur.call(this);
- fuseChildren(this);
+ if (!cursor) return;
+ if (this.textContents() === '') {
+ this.remove();
+ if (cursor[L] === this) cursor[L] = this[L];
+ else if (cursor[R] === this) cursor[R] = this[R];
+ }
+ else fuseChildren(this);
};
function fuseChildren(self) {
self.jQ[0].normalize();
var textPcDom = self.jQ[0].firstChild;
+ if (!textPcDom) return;
pray('only node in TextBlock span is Text node', textPcDom.nodeType === 3);
// nodeType === 3 has meant a Text node since ancient times:
// http://reference.sitepoint.com/javascript/Node/nodeType
@@ -233,29 +268,34 @@ var TextPiece = P(Node, function(_, super_) {
var from = this[-dir];
if (from) from.insTextAtDirEnd(ch, dir);
else TextPiece(ch).createDir(-dir, cursor);
-
return this.deleteTowards(dir, cursor);
};
+ _.mathspeak =
_.latex = function() { return this.text; };
_.deleteTowards = function(dir, cursor) {
if (this.text.length > 1) {
+ var deletedChar;
if (dir === R) {
this.dom.deleteData(0, 1);
+ deletedChar = this.text[0];
this.text = this.text.slice(1);
}
else {
// note that the order of these 2 lines is annoyingly important
// (the second line mutates this.text.length)
this.dom.deleteData(-1 + this.text.length, 1);
+ deletedChar = this.text[this.text.length - 1];
this.text = this.text.slice(0, -1);
}
+ aria.queue(deletedChar);
}
else {
this.remove();
this.jQ.remove();
cursor[dir] = this[dir];
+ aria.queue(this.text);
}
};
@@ -287,35 +327,39 @@ var TextPiece = P(Node, function(_, super_) {
};
});
-CharCmds.$ =
LatexCmds.text =
LatexCmds.textnormal =
LatexCmds.textrm =
LatexCmds.textup =
LatexCmds.textmd = TextBlock;
-function makeTextBlock(latex, tagName, attrs) {
+function makeTextBlock(latex, ariaLabel, tagName, attrs) {
return P(TextBlock, {
ctrlSeq: latex,
- htmlTemplate: '<'+tagName+' '+attrs+'>&0'+tagName+'>'
+ ariaLabel: ariaLabel,
+ mathspeakTemplate: ['Start'+ariaLabel, 'End'+ariaLabel],
+ html: function() {
+ var cmdId = 'mathquill-command-id=' + this.id;
+ return '<'+tagName+' '+attrs+' '+cmdId+'>'+this.textContents()+''+tagName+'>';
+ }
});
}
LatexCmds.em = LatexCmds.italic = LatexCmds.italics =
LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl =
- makeTextBlock('\\textit', 'i', 'class="mq-text-mode"');
+ makeTextBlock('\\textit', 'Italic', 'i', 'class="mq-text-mode"');
LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf =
- makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"');
+ makeTextBlock('\\textbf', 'Bold', 'b', 'class="mq-text-mode"');
LatexCmds.sf = LatexCmds.textsf =
- makeTextBlock('\\textsf', 'span', 'class="mq-sans-serif mq-text-mode"');
+ makeTextBlock('\\textsf', 'Sans serif font', 'span', 'class="mq-sans-serif mq-text-mode"');
LatexCmds.tt = LatexCmds.texttt =
- makeTextBlock('\\texttt', 'span', 'class="mq-monospace mq-text-mode"');
+ makeTextBlock('\\texttt', 'Mono space font', 'span', 'class="mq-monospace mq-text-mode"');
LatexCmds.textsc =
- makeTextBlock('\\textsc', 'span', 'style="font-variant:small-caps" class="mq-text-mode"');
+ makeTextBlock('\\textsc', 'Variable font', 'span', 'style="font-variant:small-caps" class="mq-text-mode"');
LatexCmds.uppercase =
- makeTextBlock('\\uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"');
+ makeTextBlock('\\uppercase', 'Uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"');
LatexCmds.lowercase =
- makeTextBlock('\\lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"');
+ makeTextBlock('\\lowercase', 'Lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"');
var RootMathCommand = P(MathCommand, function(_, super_) {
diff --git a/src/controller.js b/src/controller.js
index 73c8a4bba..3d2b999f3 100644
--- a/src/controller.js
+++ b/src/controller.js
@@ -15,9 +15,12 @@ var Controller = P(function(_) {
this.container = container;
this.options = options;
+ this.ariaLabel = 'Math Input';
+ this.ariaPostLabel = '';
+
root.controller = this;
- this.cursor = root.cursor = Cursor(root, options);
+ this.cursor = root.cursor = Cursor(root, options, this);
// TODO: stop depending on root.cursor, and rm it
};
@@ -38,4 +41,66 @@ var Controller = P(function(_) {
}
return this;
};
+ _.setAriaLabel = function(ariaLabel) {
+ var oldAriaLabel = this.getAriaLabel();
+ if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') {
+ this.ariaLabel = ariaLabel;
+ } else if (this.editable) {
+ this.ariaLabel = 'Math Input';
+ } else {
+ this.ariaLabel = '';
+ }
+ // If this field doesn't have focus, update its computed mathspeak value.
+ // We check for focus because updating the aria-label attribute of a focused element will cause most screen readers to announce the new value (in our case, label along with the expression's mathspeak).
+ // If the field does have focus at the time, it will be updated once a blur event occurs.
+ // Unless we stop using fake text inputs and emulating screen reader behavior, this is going to remain a problem.
+ if (this.ariaLabel !== oldAriaLabel && !this.containerHasFocus()) {
+ this.updateMathspeak();
+ }
+ return this;
+ };
+ _.getAriaLabel = function () {
+ if (this.ariaLabel !== 'Math Input') {
+ return this.ariaLabel;
+ } else if (this.editable) {
+ return 'Math Input';
+ } else {
+ return '';
+ }
+ };
+ _.setAriaPostLabel = function(ariaPostLabel, timeout) {
+ if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel !== '') {
+ if (
+ ariaPostLabel !== this.ariaPostLabel &&
+ typeof timeout === 'number'
+ ) {
+ if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout);
+ this._ariaAlertTimeout = setTimeout(function() {
+ if (this.containerHasFocus()) {
+ // Voice the new label, but do not update content mathspeak to prevent double-speech.
+ aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim());
+ } else {
+ // This mathquill does not have focus, so update its mathspeak.
+ this.updateMathspeak();
+ }
+ }.bind(this), timeout);
+ }
+ this.ariaPostLabel = ariaPostLabel;
+ } else {
+ if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout);
+ this.ariaPostLabel = '';
+ }
+ return this;
+ };
+ _.getAriaPostLabel = function () {
+ return this.ariaPostLabel || '';
+ };
+ _.containerHasFocus = function () {
+ return (
+ document.activeElement &&
+ this.container &&
+ this.container[0] &&
+ this.container[0].contains(document.activeElement)
+ );
+ };
});
diff --git a/src/css/editable.less b/src/css/editable.less
index 26bd9671e..0a0fcd6c8 100644
--- a/src/css/editable.less
+++ b/src/css/editable.less
@@ -1,7 +1,7 @@
.mq-editable-field {
.inline-block;
.mq-cursor {
- border-left: 1px solid black;
+ border-left: 1px solid currentColor;
margin-left: -1px;
position: relative;
z-index: 1;
@@ -19,7 +19,7 @@
&.mq-focused {
.box-shadow(~"#8bd 0 0 1px 2px, inset #6ae 0 0 2px 0");
border-color: #709AC0;
- border-radius: 1px;
+ aria-hidden: true;
}
}
// special styles for editables within static math
diff --git a/src/css/font.less b/src/css/font.less
index 06916b77f..234b16201 100644
--- a/src/css/font.less
+++ b/src/css/font.less
@@ -1,4 +1,4 @@
-@omit-font-face:;
+@omit-font-face:~"";
.font-face;
.font-face() when not (@omit-font-face) {
@font-face {
@@ -7,27 +7,26 @@
}
}
-@basic:;
+@basic:~"";
.font-srcs() when not (@basic) {
- src: url(font/Symbola.eot);
+ src: url(fonts/Symbola.eot);
src:
local("Symbola Regular"),
local("Symbola"),
- url(font/Symbola.woff2) format("woff2"),
- url(font/Symbola.woff) format("woff"),
- url(font/Symbola.ttf) format("truetype"),
- url(font/Symbola.otf) format("opentype"),
- url(font/Symbola.svg#Symbola) format("svg")
+ url(fonts/Symbola.woff2) format("woff2"),
+ url(fonts/Symbola.woff) format("woff"),
+ url(fonts/Symbola.ttf) format("truetype"),
+ url(fonts/Symbola.svg#Symbola) format("svg")
;
}
.font-srcs() when (@basic) {
- src: url(font/Symbola-basic.eot);
+ src: url(fonts/Symbola-basic.eot);
src:
local("Symbola Regular"),
local("Symbola"),
- url(font/Symbola-basic.woff2) format("woff2"),
- url(font/Symbola-basic.woff) format("woff"),
- url(font/Symbola-basic.ttf) format("truetype")
+ url(fonts/Symbola-basic.woff2) format("woff2"),
+ url(fonts/Symbola-basic.woff) format("woff"),
+ url(fonts/Symbola-basic.ttf) format("truetype")
;
}
diff --git a/src/css/main.less b/src/css/main.less
index a56becefd..691e742d5 100644
--- a/src/css/main.less
+++ b/src/css/main.less
@@ -1,6 +1,6 @@
/*
- * MathQuill v0.10.0 http://mathquill.com
- * by Han, Jeanine, and Mary maintainers@mathquill.com
+ * MathQuill {VERSION}, by Han, Jeanine, and Mary
+ * http://mathquill.com | maintainers@mathquill.com
*
* This Source Code Form is subject to the terms of the
* Mozilla Public License, v. 2.0. If a copy of the MPL
@@ -18,4 +18,3 @@
@import "selections.less";
@import "textarea.less";
-@import "matrixed.less";
diff --git a/src/css/math.less b/src/css/math.less
index 954d27c79..107190a62 100644
--- a/src/css/math.less
+++ b/src/css/math.less
@@ -1,3 +1,9 @@
+// look here to see the digit layout strategy:
+// https://www.desmos.com/calculator/ctvh9utz0t
+@digit-separator: .11em;
+@expand-margin: .009em;
+@contract-margin: -.01em;
+
.mq-root-block, .mq-math-mode .mq-root-block {
.inline-block;
width: 100%;
@@ -6,7 +12,40 @@
white-space: nowrap;
overflow: hidden;
vertical-align: middle;
+
+ .mq-digit {
+ margin-left: @expand-margin;
+ margin-right: @expand-margin;
+ }
+
+ .mq-group-start {
+ margin-left: @digit-separator;
+ margin-right: @contract-margin;
+ }
+
+ .mq-group-other {
+ margin-left: @contract-margin;
+ margin-right: @contract-margin;
+ }
+
+ .mq-group-leading-1, .mq-group-leading-2 {
+ margin-left: 0;
+ margin-right: @contract-margin;
+ }
+
+ .mq-group-leading-3 {
+ margin-left: 4 * @expand-margin;
+ margin-right: @contract-margin;
+ }
+
+ &.mq-suppress-grouping {
+ .mq-group-start, .mq-group-other, .mq-group-leading-1, .mq-group-leading-2, .mq-group-leading-3 {
+ margin-left: @expand-margin;
+ margin-right: @expand-margin;
+ }
+ }
}
+
.mq-math-mode {
font-variant: normal;
font-weight: normal;
@@ -25,6 +64,20 @@
line-height: .9;
}
+ svg {
+ // svg symbols are sometimes used for autoscaling brackets and
+ // square root symbols. This piece of css magic allows you to copy
+ // over the current value of the font color to the svg symbols.
+ fill: currentColor;
+
+ // the svg symbols fill their container
+ position:absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
* {
font-size: inherit;
line-height: inherit;
@@ -37,19 +90,30 @@
// TODO: what's the difference between these?
.mq-empty {
- background: #ccc;
+ background: rgba(0,0,0,.2);
&.mq-root-block {
background: transparent;
}
+ &.mq-empty-parens, &.mq-empty-square-brackets {
+ background: transparent
+ }
}
&.mq-empty {
background: transparent;
}
-
.mq-text-mode {
- font-size: 87%;
+ display: inline-block;
+ white-space: pre;
+ }
+
+ .mq-text-mode.mq-hasCursor {
+ box-shadow: inset darkgray 0 .1em .2em;
+ padding: 0 .1em;
+ margin: 0 -.1em;
+
+ min-width: 1ex;
}
.mq-font {
@@ -71,7 +135,7 @@
}
var.mq-f {
- margin-right: 0.1em;
+ margin-right: 0.2em;
margin-left: 0.1em;
}
@@ -80,7 +144,29 @@
}
big {
- font-size: 125%;
+ font-size: 200%;
+ }
+
+ .mq-int {
+ > big {
+ display: inline-block;
+ .transform(scaleX(.7));
+ vertical-align: -.16em;
+ }
+
+ > .mq-supsub {
+ font-size: 80%;
+ vertical-align: -1.1em;
+ padding-right: .2em;
+
+ > .mq-sup > .mq-sup-inner {
+ vertical-align: 1.3em;
+ }
+
+ > .mq-sub {
+ margin-left: -.35em;
+ }
+ }
}
.mq-roman {
@@ -96,11 +182,11 @@
}
.mq-overline {
- border-top: 1px solid black;
+ border-top: 1px solid;
margin-top: 1px;
}
.mq-underline {
- border-bottom: 1px solid black;
+ border-bottom: 1px solid;
margin-bottom: 1px;
}
@@ -122,17 +208,14 @@
// its contents, rather than always being as wide as the subscript.
// See also .fraction
.mq-supsub {
+ text-align: left;
font-size: 90%;
vertical-align: -.5em;
- &.mq-limit {
- font-size: 80%;
- vertical-align: -.4em;
- }
&.mq-sup-only {
vertical-align: .5em;
- .mq-sup {
+ & > .mq-sup {
display: inline-block;
vertical-align: text-bottom;
}
@@ -146,9 +229,6 @@
display: block;
float: left;
}
- &.mq-limit .mq-sub {
- margin-left: -.25em;
- }
.mq-binary-operator {
padding: 0 .1em;
@@ -171,21 +251,24 @@
////
// parentheses
- .mq-paren {
- padding: 0 .1em;
- vertical-align: top;
- -webkit-transform-origin: center .06em;
- -moz-transform-origin: center .06em;
- -ms-transform-origin: center .06em;
- -o-transform-origin: center .06em;
- transform-origin: center .06em;
-
- &.mq-ghost { color: silver; }
-
- + span {
- margin-top: .1em;
- margin-bottom: .1em;
- }
+ .mq-ghost svg { opacity: .2 }
+ .mq-bracket-middle {
+ margin-top: .1em;
+ margin-bottom: .1em;
+ }
+ .mq-bracket-l, .mq-bracket-r {
+ position: absolute;
+ top: 0;
+ bottom: 2px;
+ }
+ .mq-bracket-l {
+ left: 0;
+ }
+ .mq-bracket-r {
+ right:0;
+ }
+ .mq-bracket-container {
+ position: relative;
}
.mq-array {
@@ -208,7 +291,7 @@
var.mq-operator-name.mq-first {
padding-left: .2em;
}
- var.mq-operator-name.mq-last {
+ var.mq-operator-name.mq-last, .mq-supsub.mq-after-operator-name {
padding-right: .2em;
}
@@ -236,7 +319,7 @@
display: inline-block;
}
- .mq-numerator, .mq-denominator {
+ .mq-numerator, .mq-denominator, .mq-dot-recurring {
display: block;
}
@@ -251,39 +334,64 @@
padding: 0.1em;
}
+ .mq-dot-recurring {
+ text-align: center;
+ height: 0.3em;
+ }
+
////
// \sqrt
// square roots
.mq-sqrt-prefix {
- padding-top: 0;
+ position: absolute;
+ top: 1px;
+ bottom: 0.15em;
+ width: 0.95em;
+ }
+
+ .mq-sqrt-container {
position: relative;
- top: 0.1em;
- vertical-align: top;
- .transform-origin(top);
}
.mq-sqrt-stem {
border-top: 1px solid;
margin-top: 1px;
+ margin-left: 0.9em;
padding-left: .15em;
padding-right: .2em;
margin-right: .1em;
padding-top: 1px;
}
- .mq-vector-prefix {
+ .mq-diacritic-above {
+ display: block;
+ text-align: center;
+ line-height: .4em;
+ }
+
+ .mq-diacritic-stem {
+ display: block;
+ text-align: center;
+ }
+
+ .mq-hat-prefix {
display: block;
text-align: center;
- line-height: .25em;
- margin-bottom: -.1em;
- font-size: 0.75em;
+ line-height: .95em;
+ margin-bottom: -.7em;
+ transform: scaleX(1.5);
+ -moz-transform: scaleX(1.5);
+ -o-transform: scaleX(1.5);
+ -webkit-transform: scaleX(1.5);
}
- .mq-vector-stem {
+ .mq-hat-stem {
display: block;
}
.mq-large-operator {
+ vertical-align: -.2em;
+ padding: .2em;
text-align: center;
.mq-from, big, .mq-to {
@@ -303,4 +411,79 @@
cursor: text;
font-family: @symbola;
}
+
+ .mq-overarc {
+ border-top: 1px solid black;
+ -webkit-border-top-right-radius: 50% .3em;
+ -moz-border-radius-topright: 50% .3em;
+ border-top-right-radius: 50% .3em;
+ -webkit-border-top-left-radius: 50% .3em;
+ -moz-border-radius-topleft: 50% .3em;
+ border-top-left-radius: 50% .3em;
+ margin-top: 1px;
+ padding-top: 0.15em;
+ }
+
+ .mq-overarrow {
+ min-width: .5em;
+ border-top: 1px solid black;
+ margin-top: 1px;
+ padding-top: 0.2em;
+ text-align: center;
+
+ &:after {
+ position: absolute;
+ right: -0.1em;
+ top: -0.48em;
+ font-size: 0.5em;
+ content: '\27A4';
+ }
+ //really wish I could use :not here, but less doesn't seem to be happy with that
+ &.mq-arrow-left:after {
+ content: '';
+ display: none;
+ }
+ &.mq-arrow-left:before, &.mq-arrow-leftright:before {
+ position: absolute;
+ top: -0.48em;
+ left: -0.1em;
+ font-size: 0.5em;
+ content: '\27A4';
+ -moz-transform: scaleX(-1);
+ -o-transform: scaleX(-1);
+ -webkit-transform: scaleX(-1);
+ transform: scaleX(-1);
+ filter: FlipH;
+ -ms-filter: "FlipH";
+ }
+ &.mq-arrow-both {
+ vertical-align: text-bottom;
+
+ &.mq-empty {
+ min-height: 1.23em;
+
+ &:after {
+ top: -0.34em;
+ }
+ }
+ &:before{
+ -moz-transform: scaleX(-1);
+ -o-transform: scaleX(-1);
+ -webkit-transform: scaleX(-1);
+ transform: scaleX(-1);
+ filter: FlipH;
+ -ms-filter: "FlipH";
+ }
+ &:after {
+ display: block;
+ position: relative;
+ top: -2.3em;
+ font-size: 0.5em;
+ line-height: 0em;
+ content: '\27A4';
+ visibility: visible; //must override .mq-editable-field.mq-empty:after
+ text-align: right;
+ }
+ }
+ }
}
diff --git a/src/css/matrixed.less b/src/css/matrixed.less
deleted file mode 100644
index 4ebf2d211..000000000
--- a/src/css/matrixed.less
+++ /dev/null
@@ -1,20 +0,0 @@
-@import "./mixins/display";
-
-// We have to set an opaque background color for matrix-stretched
-// elements to anti-alias correctly, so we use the Chroma filter
-// on the immediate parent to make the solid background color
-// transparent.
-
-// See http://github.com/laughinghan/mathquill/wiki/Transforms
-// for more details.
-
-.mq-math-mode {
- .mq-matrixed {
- background: white;
- .inline-block;
- }
- .mq-matrixed-container {
- filter: progid:DXImageTransform.Microsoft.Chroma(color='white');
- margin-top: -.1em;
- }
-}
diff --git a/src/css/mixins/css3.less b/src/css/mixins/css3.less
index 2aecf28ab..e1a608aab 100644
--- a/src/css/mixins/css3.less
+++ b/src/css/mixins/css3.less
@@ -1,9 +1,9 @@
-.transform-origin (...) {
- -webkit-transform-origin: @arguments;
- -moz-transform-origin: @arguments;
- -ms-transform-origin: @arguments;
- -o-transform-origin: @arguments;
- transform-origin: @arguments;
+.transform (...) {
+ -webkit-transform: @arguments;
+ -moz-transform: @arguments;
+ -ms-transform: @arguments;
+ -o-transform: @arguments;
+ transform: @arguments;
}
.user-select (...) {
diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less
index 1b08a3afe..0d1a9b9b4 100644
--- a/src/css/mixins/display.less
+++ b/src/css/mixins/display.less
@@ -2,3 +2,24 @@
display: -moz-inline-box;
display: inline-block;
}
+
+// ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted.
+.mq-aria-alert {
+ position: absolute;
+ left: -1000px;
+ top: -1000px;
+ width: 0px;
+ height: 0px;
+ text-align: left;
+ overflow: hidden;
+}
+
+.mq-mathspeak {
+ position: absolute;
+ left: -1000px;
+ top: -1000px;
+ width: 0px;
+ height: 0px;
+ text-align: left;
+ overflow: hidden;
+}
diff --git a/src/css/selections.less b/src/css/selections.less
index 930de6c62..eb8702a4c 100644
--- a/src/css/selections.less
+++ b/src/css/selections.less
@@ -12,45 +12,29 @@
.mq-selection {
&, & .mq-non-leaf, & .mq-scaled {
background: #B4D5FE !important;
- background: Highlight !important;
- color: HighlightText;
- border-color: HighlightText;
- }
-
- .mq-matrixed {
- // The Chroma filter doesn't support the 'Highlight' keyword,
- // but is only used in IE 8 and below anyway, so just use the
- // default Windows highlight color. Even if the highlight color
- // of the system has been customized, it's not a big deal,
- // most of the solid blue area is chroma keyed, there'll just
- // be a blue anti-aliased fringe around the matrix-filter-
- // stretched text.
-
- // If you use IE 8 or below and customized your highlight
- // color, and after the effort I put into making everything
- // else in MathQuill work in IE 8 and below have the *gall*
- // to complain about the blue fringe that appears in selections
- // around the otherwise beautifully stretched square roots and
- // stuff, and you have no ideas for how to solve the problem,
- // just a complaint, then I'd like to politely suggest that you
- // go choke on a dick. Unless you're into that, in which case,
- // go do something that would make you unhappy instead.
-
- background: #39F !important;
- }
- .mq-matrixed-container {
- filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important;
}
&.mq-blur {
- &, & .mq-non-leaf, & .mq-scaled, & .mq-matrixed {
+ &, & .mq-non-leaf, & .mq-scaled {
background: #D4D4D4 !important;
color: black;
border-color: black;
}
+ }
+ }
+}
+
- .mq-matrixed-container {
- filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important;
+html body { // adding 'html body' for specificity
+ .mq-math-mode, .mq-editable-field {
+ .mq-selection {
+ // do not show a background inside any of the
+ // children of nthroot. We draw a background on
+ // the nthroot itself. We don't want the index
+ // to be covered up by the background of the
+ // radical.
+ .mq-nthroot-container * {
+ background: transparent !important;
}
}
}
diff --git a/src/css/textarea.less b/src/css/textarea.less
index 487e1f1b8..4da9bdbeb 100644
--- a/src/css/textarea.less
+++ b/src/css/textarea.less
@@ -14,11 +14,16 @@
position: absolute; // the only way to hide the textarea *and* the
clip: rect(1em 1em 1em 1em); // blinking insertion point in IE
- font-size: 0; // the only way to hide the blinking blue cursor in iOS 8
+ .transform(scale(0)); // the only way to hide the blinking blue cursor in iOS 8 #584
resize: none; // hotfix: https://code.google.com/p/chromium/issues/detail?id=355199#c1
width: 1px; // don't "stick out" invisibly from a math field,
height: 1px; // can affect ancestor's .scroll{Width,Height}
+
+ // Needed to fix a Safari 10 bug where box-sizing: border-box is
+ // preventing text from being copied.
+ // https://github.com/mathquill/mathquill/issues/686
+ box-sizing: content-box;
}
}
diff --git a/src/cursor.js b/src/cursor.js
index b08b2f6ce..3df5ac0b6 100644
--- a/src/cursor.js
+++ b/src/cursor.js
@@ -11,7 +11,8 @@ JS environment could actually contain many instances. */
//A fake cursor in the fake textbox that the math is rendered in.
var Cursor = P(Point, function(_) {
- _.init = function(initParent, options) {
+ _.init = function(initParent, options, controller) {
+ this.controller = controller;
this.parent = initParent;
this.options = options;
@@ -56,7 +57,8 @@ var Cursor = P(Point, function(_) {
this[-dir] = oppDir;
// by contract, .blur() is called after all has been said and done
// and the cursor has actually been moved
- if (oldParent !== parent && oldParent.blur) oldParent.blur();
+ // FIXME pass cursor to .blur() so text can fix cursor pointers when removing itself
+ if (oldParent !== parent && oldParent.blur) oldParent.blur(this);
};
_.insDirOf = function(dir, el) {
prayDirection(dir);
@@ -97,6 +99,7 @@ var Cursor = P(Point, function(_) {
var pageX = self.offset().left;
to.seek(pageX, self);
}
+ aria.queue(to, true);
};
_.offset = function() {
//in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset()
@@ -231,7 +234,13 @@ var Cursor = P(Point, function(_) {
this.selectionChanged();
return true;
};
-
+ _.resetToEnd = function (controller) {
+ this.clearSelection();
+ var root = controller.root;
+ this[R] = 0;
+ this[L] = root.ends[R];
+ this.parent = root;
+ };
_.clearSelection = function() {
if (this.selection) {
this.selection.clear();
@@ -258,6 +267,19 @@ var Cursor = P(Point, function(_) {
}
return seln;
};
+ _.depth = function() {
+ var node = this;
+ var depth = 0;
+ while (node = node.parent) {
+ depth += (node instanceof MathBlock) ? 1 : 0;
+ }
+ return depth;
+ };
+ _.isTooDeep = function(offset) {
+ if (this.options.maxDepth !== undefined) {
+ return this.depth() + (offset || 0) > this.options.maxDepth;
+ }
+ };
});
var Selection = P(Fragment, function(_, super_) {
@@ -276,9 +298,10 @@ var Selection = P(Fragment, function(_, super_) {
this.jQ.replaceWith(this.jQ[0].childNodes);
return this;
};
- _.join = function(methodName) {
+ _.join = function(methodName, separatorToken) {
+ var separator = separatorToken || '';
return this.fold('', function(fold, child) {
- return fold + child[methodName]();
+ return fold + separator + child[methodName]();
});
};
});
diff --git a/src/font/Symbola-basic.eot b/src/font/Symbola-basic.eot
deleted file mode 100644
index 2e39ec7bb..000000000
Binary files a/src/font/Symbola-basic.eot and /dev/null differ
diff --git a/src/font/Symbola-basic.ttf b/src/font/Symbola-basic.ttf
deleted file mode 100644
index cf968b9ad..000000000
Binary files a/src/font/Symbola-basic.ttf and /dev/null differ
diff --git a/src/font/Symbola-basic.woff b/src/font/Symbola-basic.woff
deleted file mode 100644
index 104a15090..000000000
Binary files a/src/font/Symbola-basic.woff and /dev/null differ
diff --git a/src/font/Symbola-basic.woff2 b/src/font/Symbola-basic.woff2
deleted file mode 100755
index 7c0d6c8cf..000000000
Binary files a/src/font/Symbola-basic.woff2 and /dev/null differ
diff --git a/src/font/Symbola.eot b/src/font/Symbola.eot
deleted file mode 100755
index 0d10a95b8..000000000
Binary files a/src/font/Symbola.eot and /dev/null differ
diff --git a/src/font/Symbola.otf b/src/font/Symbola.otf
deleted file mode 100755
index d54956348..000000000
Binary files a/src/font/Symbola.otf and /dev/null differ
diff --git a/src/font/Symbola.svg b/src/font/Symbola.svg
deleted file mode 100755
index eff3111f8..000000000
--- a/src/font/Symbola.svg
+++ /dev/null
@@ -1,5102 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/src/font/Symbola.ttf b/src/font/Symbola.ttf
deleted file mode 100755
index 52337df9b..000000000
Binary files a/src/font/Symbola.ttf and /dev/null differ
diff --git a/src/font/Symbola.woff b/src/font/Symbola.woff
deleted file mode 100755
index b9bba2398..000000000
Binary files a/src/font/Symbola.woff and /dev/null differ
diff --git a/src/font/Symbola.woff2 b/src/font/Symbola.woff2
deleted file mode 100755
index 9d3e8209c..000000000
Binary files a/src/font/Symbola.woff2 and /dev/null differ
diff --git a/src/fonts/Symbola-basic.css b/src/fonts/Symbola-basic.css
new file mode 100644
index 000000000..3a8186b98
--- /dev/null
+++ b/src/fonts/Symbola-basic.css
@@ -0,0 +1,4 @@
+@font-face {
+ font-family: Symbola;
+ src: url(data:application/font-woff;base64,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) format('woff');
+}
\ No newline at end of file
diff --git a/src/fonts/Symbola-basic.eot b/src/fonts/Symbola-basic.eot
new file mode 100644
index 000000000..3c1edaa5a
Binary files /dev/null and b/src/fonts/Symbola-basic.eot differ
diff --git a/src/fonts/Symbola-basic.ttf b/src/fonts/Symbola-basic.ttf
new file mode 100644
index 000000000..d1eda7b9e
Binary files /dev/null and b/src/fonts/Symbola-basic.ttf differ
diff --git a/src/fonts/Symbola-basic.woff b/src/fonts/Symbola-basic.woff
new file mode 100644
index 000000000..a262924b9
Binary files /dev/null and b/src/fonts/Symbola-basic.woff differ
diff --git a/src/fonts/Symbola-basic.woff2 b/src/fonts/Symbola-basic.woff2
new file mode 100644
index 000000000..44e703bb4
Binary files /dev/null and b/src/fonts/Symbola-basic.woff2 differ
diff --git a/src/fonts/Symbola.eot b/src/fonts/Symbola.eot
new file mode 100644
index 000000000..a78eb196e
Binary files /dev/null and b/src/fonts/Symbola.eot differ
diff --git a/src/fonts/Symbola.svg b/src/fonts/Symbola.svg
new file mode 100644
index 000000000..9783ff7df
--- /dev/null
+++ b/src/fonts/Symbola.svg
@@ -0,0 +1,2025 @@
+
+
+
\ No newline at end of file
diff --git a/src/fonts/Symbola.ttf b/src/fonts/Symbola.ttf
new file mode 100644
index 000000000..ea6a8bba9
Binary files /dev/null and b/src/fonts/Symbola.ttf differ
diff --git a/src/fonts/Symbola.woff b/src/fonts/Symbola.woff
new file mode 100644
index 000000000..059298283
Binary files /dev/null and b/src/fonts/Symbola.woff differ
diff --git a/src/fonts/Symbola.woff2 b/src/fonts/Symbola.woff2
new file mode 100644
index 000000000..6bfbba4d4
Binary files /dev/null and b/src/fonts/Symbola.woff2 differ
diff --git a/src/intro.js b/src/intro.js
index e1d937930..1ff07bbb6 100644
--- a/src/intro.js
+++ b/src/intro.js
@@ -1,6 +1,6 @@
/**
- * MathQuill v0.10.0 http://mathquill.com
- * by Han, Jeanine, and Mary maintainers@mathquill.com
+ * MathQuill {VERSION}, by Han, Jeanine, and Mary
+ * http://mathquill.com | maintainers@mathquill.com
*
* This Source Code Form is subject to the terms of the
* Mozilla Public License, v. 2.0. If a copy of the MPL
@@ -12,89 +12,18 @@
var jQuery = window.jQuery,
undefined,
- mqCmdId = 'mathquill-command-id',
- mqBlockId = 'mathquill-block-id',
min = Math.min,
max = Math.max;
-function noop() {}
-
-/**
- * A utility higher-order function that makes defining variadic
- * functions more convenient by letting you essentially define functions
- * with the last argument as a splat, i.e. the last argument "gathers up"
- * remaining arguments to the function:
- * var doStuff = variadic(function(first, rest) { return rest; });
- * doStuff(1, 2, 3); // => [2, 3]
- */
-var __slice = [].slice;
-function variadic(fn) {
- var numFixedArgs = fn.length - 1;
- return function() {
- var args = __slice.call(arguments, 0, numFixedArgs);
- var varArg = __slice.call(arguments, numFixedArgs);
- return fn.apply(this, args.concat([ varArg ]));
- };
-}
+if (!jQuery) throw 'MathQuill requires jQuery 1.5.2+ to be loaded first';
-/**
- * A utility higher-order function that makes combining object-oriented
- * programming and functional programming techniques more convenient:
- * given a method name and any number of arguments to be bound, returns
- * a function that calls it's first argument's method of that name (if
- * it exists) with the bound arguments and any additional arguments that
- * are passed:
- * var sendMethod = send('method', 1, 2);
- * var obj = { method: function() { return Array.apply(this, arguments); } };
- * sendMethod(obj, 3, 4); // => [1, 2, 3, 4]
- * // or more specifically,
- * var obj2 = { method: function(one, two, three) { return one*two + three; } };
- * sendMethod(obj2, 3); // => 5
- * sendMethod(obj2, 4); // => 6
- */
-var send = variadic(function(method, args) {
- return variadic(function(obj, moreArgs) {
- if (method in obj) return obj[method].apply(obj, args.concat(moreArgs));
- });
-});
-
-/**
- * A utility higher-order function that creates "implicit iterators"
- * from "generators": given a function that takes in a sole argument,
- * a "yield_" function, that calls "yield_" repeatedly with an object as
- * a sole argument (presumably objects being iterated over), returns
- * a function that calls it's first argument on each of those objects
- * (if the first argument is a function, it is called repeatedly with
- * each object as the first argument, otherwise it is stringified and
- * the method of that name is called on each object (if such a method
- * exists)), passing along all additional arguments:
- * var a = [
- * { method: function(list) { list.push(1); } },
- * { method: function(list) { list.push(2); } },
- * { method: function(list) { list.push(3); } }
- * ];
- * a.each = iterator(function(yield_) {
- * for (var i in this) yield_(this[i]);
- * });
- * var list = [];
- * a.each('method', list);
- * list; // => [1, 2, 3]
- * // Note that the for-in loop will yield 'each', but 'each' maps to
- * // the function object created by iterator() which does not have a
- * // .method() method, so that just fails silently.
- */
-function iterator(generator) {
- return variadic(function(fn, args) {
- if (typeof fn !== 'function') fn = send(fn);
- var yield_ = function(obj) { return fn.apply(obj, [ obj ].concat(args)); };
- return generator.call(this, yield_);
- });
-}
+function noop() {}
/**
* sugar to make defining lots of commands easier.
* TODO: rethink this.
*/
+var __slice = [].slice;
function bind(cons /*, args... */) {
var args = __slice.call(arguments, 1);
return function() {
diff --git a/src/publicapi.js b/src/publicapi.js
index 0c51677e5..c3a2dab1a 100644
--- a/src/publicapi.js
+++ b/src/publicapi.js
@@ -2,7 +2,7 @@
* The publicly exposed MathQuill API.
********************************************************/
-var API = {}, Options = P(), optionProcessors = {}, Progenote = P();
+var API = {}, Options = P(), optionProcessors = {}, Progenote = P(), EMBEDS = {};
/**
* Interface Versioning (#459, #495) to allow us to virtually guarantee
@@ -15,7 +15,7 @@ var API = {}, Options = P(), optionProcessors = {}, Progenote = P();
function insistOnInterVer() {
if (window.console) console.warn(
'You are using the MathQuill API without specifying an interface version, ' +
- 'which will fail in v1.0.0. You can fix this easily by doing this before ' +
+ 'which will fail in v1.0.0. Easiest fix is to do the following before ' +
'doing anything else:\n' +
'\n' +
' MathQuill = MathQuill.getInterface(1);\n' +
@@ -31,6 +31,7 @@ function MathQuill(el) {
return MQ1(el);
};
MathQuill.prototype = Progenote.p;
+MathQuill.VERSION = "{VERSION}";
MathQuill.interfaceVersion = function(v) {
// shim for #459-era interface versioning (ended with #495)
if (v !== 1) throw 'Only interface version 1 supported. You specified: ' + v;
@@ -69,14 +70,15 @@ function getInterface(v) {
function MQ(el) {
if (!el || !el.nodeType) return null; // check that `el` is a HTML element, using the
// same technique as jQuery: https://github.com/jquery/jquery/blob/679536ee4b7a92ae64a5f58d90e9cc38c001e807/src/core/init.js#L92
- var blockId = $(el).children('.mq-root-block').attr(mqBlockId);
- var ctrlr = blockId && Node.byId[blockId].controller;
+ var blockNode = Node.getNodeOfElement($(el).children('.mq-root-block')[0]);
+ var ctrlr = blockNode && blockNode.controller;
return ctrlr ? APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null;
};
var APIClasses = {};
MQ.L = L;
MQ.R = R;
+ MQ.saneKeyboardEvents = saneKeyboardEvents;
function config(currentOptions, newOptions) {
if (newOptions && newOptions.handlers) {
@@ -88,6 +90,12 @@ function getInterface(v) {
}
}
MQ.config = function(opts) { config(Options.p, opts); return this; };
+ MQ.registerEmbed = function(name, options) {
+ if (!/^[a-z][a-z0-9]*$/i.test(name)) {
+ throw 'Embed name must start with letter and be only letters and digits';
+ }
+ EMBEDS[name] = options;
+ };
var AbstractMathQuill = APIClasses.AbstractMathQuill = P(Progenote, function(_) {
_.init = function(ctrlr) {
@@ -101,8 +109,8 @@ function getInterface(v) {
ctrlr.createTextarea();
var contents = el.addClass(classNames).contents().detach();
- root.jQ =
- $('').attr(mqBlockId, root.id).appendTo(el);
+ root.jQ = $('').appendTo(el);
+ Node.linkElementByBlockId(root.jQ[0], root.id);
this.latex(contents.text());
this.revert = function() {
@@ -114,6 +122,7 @@ function getInterface(v) {
_.config = function(opts) { config(this.__options, opts); return this; };
_.el = function() { return this.__controller.container[0]; };
_.text = function() { return this.__controller.exportText(); };
+ _.mathspeak = function() { return this.__controller.exportMathSpeak(); };
_.latex = function(latex) {
if (arguments.length > 0) {
this.__controller.renderLatexMath(latex);
@@ -130,7 +139,7 @@ function getInterface(v) {
.replace(/ class=(""|(?= |>))/g, '');
};
_.reflow = function() {
- this.__controller.root.postOrder('reflow');
+ this.__controller.root.postOrder(function (node) { node.reflow(); });
return this;
};
});
@@ -144,7 +153,11 @@ function getInterface(v) {
this.__controller.editablesTextareaEvents();
return this;
};
- _.focus = function() { this.__controller.textarea.focus(); return this; };
+ _.focus = function() {
+ this.__controller.textarea[0].focus();
+ this.__controller.scrollHoriz();
+ return this;
+ };
_.blur = function() { this.__controller.textarea.blur(); return this; };
_.write = function(latex) {
this.__controller.writeLatex(latex);
@@ -152,20 +165,30 @@ function getInterface(v) {
if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur();
return this;
};
+ _.empty = function() {
+ var root = this.__controller.root, cursor = this.__controller.cursor;
+
+ root.ends[L] = root.ends[R] = 0;
+ root.jQ.empty();
+ delete cursor.selection;
+ cursor.insAtRightEnd(root);
+ return this;
+ };
_.cmd = function(cmd) {
var ctrlr = this.__controller.notify(), cursor = ctrlr.cursor;
- if (/^\\[a-z]+$/i.test(cmd)) {
+ if (/^\\[a-z]+$/i.test(cmd) && !cursor.isTooDeep()) {
cmd = cmd.slice(1);
var klass = LatexCmds[cmd];
if (klass) {
cmd = klass(cmd);
if (cursor.selection) cmd.replaces(cursor.replaceSelection());
cmd.createLeftOf(cursor.show());
- this.__controller.scrollHoriz();
}
else /* TODO: API needs better error reporting */;
}
else cursor.parent.write(cursor, cmd);
+
+ ctrlr.scrollHoriz();
if (ctrlr.blurred) cursor.hide().parent.blur();
return this;
};
@@ -187,10 +210,10 @@ function getInterface(v) {
_.moveToLeftEnd = function() { return this.moveToDirEnd(L); };
_.moveToRightEnd = function() { return this.moveToDirEnd(R); };
- _.keystroke = function(keys) {
+ _.keystroke = function(keys, evt) {
var keys = keys.replace(/^\s+|\s+$/g, '').split(/\s+/);
for (var i = 0; i < keys.length; i += 1) {
- this.__controller.keystroke(keys[i], { preventDefault: noop });
+ this.__controller.keystroke(keys[i], evt || { preventDefault: noop });
}
return this;
};
@@ -204,9 +227,35 @@ function getInterface(v) {
var el = document.elementFromPoint(clientX, clientY);
this.__controller.seek($(el), pageX, pageY);
- var cmd = Embed(options);
+ var cmd = Embed().setOptions(options);
cmd.createLeftOf(this.__controller.cursor);
- }
+ };
+ _.setAriaLabel = function(ariaLabel) {
+ this.__controller.setAriaLabel(ariaLabel);
+ return this;
+ };
+ _.getAriaLabel = function () {
+ return this.__controller.getAriaLabel();
+ };
+ _.setAriaPostLabel = function(ariaPostLabel, timeout) {
+ this.__controller.setAriaPostLabel(ariaPostLabel, timeout);
+ return this;
+ };
+ _.getAriaPostLabel = function () {
+ return this.__controller.getAriaPostLabel();
+ };
+ _.clickAt = function(clientX, clientY, target) {
+ target = target || document.elementFromPoint(clientX, clientY);
+ var ctrlr = this.__controller, root = ctrlr.root;
+ if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0];
+ ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset);
+ if (ctrlr.blurred) this.focus();
+ return this;
+ };
+ _.ignoreNextMousedown = function(fn) {
+ this.__controller.cursor.options.ignoreNextMousedown = fn;
+ return this;
+ };
});
MQ.EditableField = function() { throw "wtf don't call me, I'm 'abstract'"; };
MQ.EditableField.prototype = APIClasses.EditableField.prototype;
diff --git a/src/services/aria.js b/src/services/aria.js
new file mode 100755
index 000000000..ea7f43096
--- /dev/null
+++ b/src/services/aria.js
@@ -0,0 +1,87 @@
+/*****************************************
+
+ * Add the capability for mathquill to generate ARIA alerts. Necessary so MQ can convey information as a screen reader user navigates the fake MathQuill textareas.
+ * Official ARIA specification: https://www.w3.org/TR/wai-aria/
+ * WAI-ARIA is still catching on, thus only more recent browsers support it, and even then to varying degrees.
+ * The below implementation attempts to be as broad as possible and may not conform precisely to the spec. But, neither do any browsers or adaptive technologies at this point.
+ * At time of writing, IE 11, FF 44, and Safari 8.0.8 work. Older versions of these browsers should speak as well, but haven't tested precisely which earlier editions pass.
+
+ * Tested AT: on Windows, Window-Eyes, ZoomText Fusion, NVDA, and JAWS (all supported).
+ * VoiceOver on Mac platforms also supported (only tested with OSX 10.10.5 and iOS 9.2.1+).
+ * Chrome 54+ on Android works reliably with Talkback.
+ ****************************************/
+
+var Aria = P(function(_) {
+ _.init = function() {
+ this.jQ = jQuery([]); // empty element
+ // Add the alert DOM element only after the page has loaded.
+ jQuery(document).ready(function() {
+ var el = '.mq-aria-alert';
+ // No matter how many Mathquill instances exist, we only need one alert object to say something.
+ if (!jQuery(el).length) jQuery('body').append(""); // make this as noisy as possible in hopes that all modern screen reader/browser combinations will speak when triggered later.
+ this.jQ = jQuery(el);
+ }.bind(this));
+ this.items = [];
+ this.msg = '';
+ };
+
+ _.queue = function(item, shouldDescribe) {
+ var output = '';
+ if (item instanceof Node) {
+ // Some constructs include verbal shorthand (such as simple fractions and exponents).
+ // Since ARIA alerts relate to moving through interactive content, we don't want to use that shorthand if it exists
+ // since doing so may be ambiguous or confusing.
+ var itemMathspeak = item.mathspeak({ignoreShorthand: true});
+ if (shouldDescribe) { // used to ensure item is described when cursor reaches block boundaries
+ if (
+ item.parent &&
+ item.parent.ariaLabel &&
+ item.ariaLabel === 'block'
+ ) {
+ output = item.parent.ariaLabel+' '+itemMathspeak;
+ } else if (item.ariaLabel) {
+ output = item.ariaLabel+' '+itemMathspeak;
+ }
+ }
+ if (output === '') {
+ output = itemMathspeak;
+ }
+ } else {
+ output = item;
+ }
+ this.items.push(output);
+ return this;
+ };
+ _.queueDirOf = function(dir) {
+ prayDirection(dir);
+ return this.queue(dir === L ? 'before' : 'after');
+ };
+ _.queueDirEndOf = function(dir) {
+ prayDirection(dir);
+ return this.queue(dir === L ? 'beginning of' : 'end of');
+ };
+
+ _.alert = function(t) {
+ if (t) this.queue(t);
+ if (this.items.length) {
+ this.msg = this.items.join(' ').replace(/ +(?= )/g,'').trim();
+ this.jQ.empty().text(this.msg);
+ }
+ return this.clear();
+ };
+
+ _.clear = function() {
+ this.items.length = 0;
+ return this;
+ };
+});
+
+// We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules.
+var aria = Aria();
+
+Controller.open(function(_) {
+ _.aria = aria;
+ // based on http://www.gh-mathspeak.com/examples/quick-tutorial/
+ // and http://www.gh-mathspeak.com/examples/grammar-rules/
+ _.exportMathSpeak = function() { return this.root.mathspeak(); };
+});
diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js
index 55981a5d5..148082a91 100644
--- a/src/services/focusBlur.js
+++ b/src/services/focusBlur.js
@@ -1,41 +1,88 @@
Controller.open(function(_) {
+ this.onNotify(function (e) {
+ // these try to cover all ways that mathquill can be modified
+ if (e === 'edit' || e === 'replace' || e === undefined) {
+ var controller = this.controller;
+ if (!controller) return;
+ if (!controller.options.enableDigitGrouping) return;
+
+ // blurred === false means we are focused. blurred === true or
+ // blurred === undefined means we are not focused.
+ if (controller.blurred !== false) return;
+
+ controller.disableGroupingForSeconds(1);
+ }
+ });
+
+ _.disableGroupingForSeconds = function (seconds) {
+ clearTimeout(this.__disableGroupingTimeout);
+ var jQ = this.root.jQ;
+
+ if (seconds === 0) {
+ jQ.removeClass('mq-suppress-grouping');
+ } else {
+ jQ.addClass('mq-suppress-grouping');
+ this.__disableGroupingTimeout = setTimeout(function () {
+ jQ.removeClass('mq-suppress-grouping');
+ }, seconds * 1000);
+ }
+ }
+
_.focusBlurEvents = function() {
var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor;
var blurTimeout;
ctrlr.textarea.focus(function() {
+ ctrlr.updateMathspeak();
ctrlr.blurred = false;
clearTimeout(blurTimeout);
ctrlr.container.addClass('mq-focused');
- if (!cursor.parent)
- cursor.insAtRightEnd(root);
+ if (!cursor.parent) cursor.insAtRightEnd(root);
if (cursor.selection) {
cursor.selection.jQ.removeClass('mq-blur');
ctrlr.selectionChanged(); //re-select textarea contents after tabbing away and back
- }
- else
+ } else {
cursor.show();
+ }
+ ctrlr.setOverflowClasses();
+
}).blur(function() {
+ if (ctrlr.textareaSelectionTimeout) {
+ clearTimeout(ctrlr.textareaSelectionTimeout);
+ ctrlr.textareaSelectionTimeout = undefined;
+ }
+ ctrlr.disableGroupingForSeconds(0);
ctrlr.blurred = true;
blurTimeout = setTimeout(function() { // wait for blur on window; if
- root.postOrder('intentionalBlur'); // none, intentional blur: #264
+ root.postOrder(function (node) { node.intentionalBlur(); }); // none, intentional blur: #264
cursor.clearSelection().endSelection();
blur();
+ ctrlr.updateMathspeak();
+ ctrlr.scrollHoriz();
});
- $(window).on('blur', windowBlur);
+ $(window).bind('blur', windowBlur);
});
function windowBlur() { // blur event also fired on window, just switching
clearTimeout(blurTimeout); // tabs/windows, not intentional blur
if (cursor.selection) cursor.selection.jQ.addClass('mq-blur');
blur();
+ ctrlr.updateMathspeak();
}
function blur() { // not directly in the textarea blur handler so as to be
cursor.hide().parent.blur(); // synchronous with/in the same frame as
ctrlr.container.removeClass('mq-focused'); // clearing/blurring selection
- $(window).off('blur', windowBlur);
+ $(window).unbind('blur', windowBlur);
+
+ if (ctrlr.options && ctrlr.options.resetCursorOnBlur) {
+ cursor.resetToEnd(ctrlr);
+ }
}
ctrlr.blurred = true;
cursor.hide().parent.blur();
};
+ _.unbindFocusBlurEvents = function() {
+ var ctrlr = this;
+ ctrlr.textarea.unbind('focus blur');
+ };
});
/**
diff --git a/src/services/keystroke.js b/src/services/keystroke.js
index f3ec668cc..fa59223b5 100644
--- a/src/services/keystroke.js
+++ b/src/services/keystroke.js
@@ -39,11 +39,13 @@ Node.open(function(_) {
// End -> move to the end of the current block.
case 'End':
ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent);
+ aria.queue("end of").queue(cursor.parent, true);
break;
// Ctrl-End -> move all the way to the end of the root block.
case 'Ctrl-End':
ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root);
+ aria.queue("end of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel);
break;
// Shift-End -> select to the end of the current block.
@@ -53,21 +55,23 @@ Node.open(function(_) {
}
break;
- // Ctrl-Shift-End -> select to the end of the root block.
+ // Ctrl-Shift-End -> select all the way to the end of the root block.
case 'Ctrl-Shift-End':
while (cursor[R] || cursor.parent !== ctrlr.root) {
ctrlr.selectRight();
}
break;
- // Home -> move to the start of the root block or the current block.
+ // Home -> move to the start of the current block.
case 'Home':
ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent);
+ aria.queue("beginning of").queue(cursor.parent, true);
break;
- // Ctrl-Home -> move to the start of the current block.
+ // Ctrl-Home -> move all the way to the start of the root block.
case 'Ctrl-Home':
ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root);
+ aria.queue("beginning of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel);
break;
// Shift-Home -> select to the start of the current block.
@@ -77,7 +81,7 @@ Node.open(function(_) {
}
break;
- // Ctrl-Shift-Home -> move to the start of the root block.
+ // Ctrl-Shift-Home -> select all the way to the start of the root block.
case 'Ctrl-Shift-Home':
while (cursor[L] || cursor.parent !== ctrlr.root) {
ctrlr.selectLeft();
@@ -129,9 +133,58 @@ Node.open(function(_) {
while (cursor[L]) ctrlr.selectLeft();
break;
+ // These remaining hotkeys are only of benefit to people running screen readers.
+ case 'Ctrl-Alt-Up': // speak parent block that has focus
+ if (cursor.parent.parent && cursor.parent.parent instanceof Node) aria.queue(cursor.parent.parent);
+ else aria.queue('nothing above');
+ break;
+
+ case 'Ctrl-Alt-Down': // speak current block that has focus
+ if (cursor.parent && cursor.parent instanceof Node) aria.queue(cursor.parent);
+ else aria.queue('block is empty');
+ break;
+
+ case 'Ctrl-Alt-Left': // speak left-adjacent block
+ if (
+ cursor.parent.parent &&
+ cursor.parent.parent.ends &&
+ cursor.parent.parent.ends[L] &&
+ cursor.parent.parent.ends[L] instanceof Node
+ ) {
+ aria.queue(cursor.parent.parent.ends[L]);
+ } else {
+ aria.queue('nothing to the left');
+ }
+ break;
+
+ case 'Ctrl-Alt-Right': // speak right-adjacent block
+ if (
+ cursor.parent.parent &&
+ cursor.parent.parent.ends &&
+ cursor.parent.parent.ends[R] &&
+ cursor.parent.parent.ends[R] instanceof Node
+ ) {
+ aria.queue(cursor.parent.parent.ends[R]);
+ } else {
+ aria.queue('nothing to the right');
+ }
+ break;
+
+ case 'Ctrl-Alt-Shift-Down': // speak selection
+ if (cursor.selection) aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected');
+ else aria.queue('nothing selected');
+ break;
+
+ case 'Ctrl-Alt-=':
+ case 'Ctrl-Alt-Shift-Right': // speak ARIA post label (evaluation or error)
+ if (ctrlr.ariaPostLabel.length) aria.queue(ctrlr.ariaPostLabel);
+ else aria.queue('no answer');
+ break;
+
default:
return;
}
+ aria.alert();
e.preventDefault();
ctrlr.scrollHoriz();
};
@@ -162,6 +215,7 @@ Controller.open(function(_) {
if (cursor.parent === this.root) return;
cursor.parent.moveOutOf(dir, cursor);
+ aria.alert();
return this.notify('move');
};
@@ -224,6 +278,31 @@ Controller.open(function(_) {
_.deleteDir = function(dir) {
prayDirection(dir);
var cursor = this.cursor;
+ var cursorEl = cursor[dir], cursorElParent = cursor.parent.parent;
+ if(cursorEl && cursorEl instanceof Node) {
+ if(cursorEl.sides) {
+ aria.queue(cursorEl.parent.chToCmd(cursorEl.sides[-dir].ch).mathspeak({createdLeftOf: cursor}));
+ // generally, speak the current element if it has no blocks,
+ // but don't for text block commands as the deleteTowards method
+ // in the TextCommand class is responsible for speaking the new character under the cursor.
+ } else if (!cursorEl.blocks && cursorEl.parent.ctrlSeq !== '\\text') {
+ aria.queue(cursorEl);
+ }
+ } else if(cursorElParent && cursorElParent instanceof Node) {
+ if(cursorElParent.sides) {
+ aria.queue(cursorElParent.parent.chToCmd(cursorElParent.sides[dir].ch).mathspeak({createdLeftOf: cursor}));
+ } else if (cursorElParent.blocks && cursorElParent.mathspeakTemplate) {
+ if (cursorElParent.upInto && cursorElParent.downInto) { // likely a fraction, and we just backspaced over the slash
+ aria.queue(cursorElParent.mathspeakTemplate[1]);
+ } else {
+ var mst = cursorElParent.mathspeakTemplate;
+ var textToQueue = dir === L ? mst[0] : mst[mst.length - 1];
+ aria.queue(textToQueue);
+ }
+ } else {
+ aria.queue(cursorElParent);
+ }
+ }
var hadSelection = cursor.selection;
this.notify('edit'); // deletes selection if present
@@ -234,22 +313,30 @@ Controller.open(function(_) {
if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R);
if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L);
- cursor.parent.bubble('reflow');
+ cursor.parent.bubble(function (node) { node.reflow(); });
return this;
};
_.ctrlDeleteDir = function(dir) {
prayDirection(dir);
var cursor = this.cursor;
- if (!cursor[L] || cursor.selection) return ctrlr.deleteDir();
+ if (!cursor[dir] || cursor.selection) return this.deleteDir(dir);
this.notify('edit');
- Fragment(cursor.parent.ends[L], cursor[L]).remove();
- cursor.insAtDirEnd(L, cursor.parent);
+ var fragRemoved;
+ if (dir === L) {
+ fragRemoved = Fragment(cursor.parent.ends[L], cursor[L]);
+ } else {
+ fragRemoved = Fragment(cursor[R], cursor.parent.ends[R]);
+ }
+ aria.queue(fragRemoved);
+ fragRemoved.remove();
+
+ cursor.insAtDirEnd(dir, cursor.parent);
if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R);
if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L);
- cursor.parent.bubble('reflow');
+ cursor.parent.bubble(function (node) { node.reflow(); });
return this;
};
@@ -277,6 +364,7 @@ Controller.open(function(_) {
cursor.clearSelection();
cursor.select() || cursor.show();
+ if (cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); // clearing first because selection fires several times, and we don't want repeated speech.
};
_.selectLeft = function() { return this.selectDir(L); };
_.selectRight = function() { return this.selectDir(R); };
diff --git a/src/services/latex.js b/src/services/latex.js
index c5489511e..432c93261 100644
--- a/src/services/latex.js
+++ b/src/services/latex.js
@@ -1,6 +1,6 @@
-// Parser MathCommand
+// Parser MathBlock
var latexMathParser = (function() {
- function commandToBlock(cmd) {
+ function commandToBlock(cmd) { // can also take in a Fragment
var block = MathBlock();
cmd.adopt(block, 0, 0);
return block;
@@ -18,13 +18,16 @@ var latexMathParser = (function() {
var string = Parser.string;
var regex = Parser.regex;
var letter = Parser.letter;
+ var digit = Parser.digit;
var any = Parser.any;
var optWhitespace = Parser.optWhitespace;
var succeed = Parser.succeed;
var fail = Parser.fail;
- // Parsers yielding MathCommands
+ // Parsers yielding either MathCommands, or Fragments of MathCommands
+ // (either way, something that can be adopted by a MathBlock)
var variable = letter.map(function(c) { return Letter(c); });
+ var number = digit.map(function (c) { return Digit(c); });
var symbol = regex(/^[^${}\\_^]/).map(function(c) { return VanillaSymbol(c); });
var controlSequence =
@@ -48,6 +51,7 @@ var latexMathParser = (function() {
var command =
controlSequence
.or(variable)
+ .or(number)
.or(symbol)
;
@@ -73,39 +77,195 @@ var latexMathParser = (function() {
})();
Controller.open(function(_, super_) {
+ _.cleanLatex = function (latex) {
+ //prune unnecessary spaces
+ return latex.replace(/(\\[a-z]+) (?![a-z])/ig,'$1')
+ }
_.exportLatex = function() {
- return this.root.latex().replace(/(\\[a-z]+) (?![a-z])/ig,'$1');
+ return this.cleanLatex(this.root.latex());
+ };
+
+ optionProcessors.maxDepth = function(depth) {
+ return (typeof depth === 'number') ? depth : undefined;
};
_.writeLatex = function(latex) {
var cursor = this.notify('edit').cursor;
+ cursor.parent.writeLatex(cursor, latex);
- var all = Parser.all;
- var eof = Parser.eof;
+ return this;
+ };
- var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex);
+ _.classifyLatexForEfficientUpdate = function (latex) {
+ if (typeof latex !== 'string') return;
- if (block && !block.isEmpty()) {
- block.children().adopt(cursor.parent, cursor[L], cursor[R]);
- var jQ = block.jQize();
- jQ.insertBefore(cursor.jQ);
- cursor[L] = block.ends[R];
- block.finalizeInsert(cursor.options, cursor);
- if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L);
- if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R);
- cursor.parent.bubble('reflow');
+ var matches = latex.match(/-?[0-9.]+$/g);
+ if (matches && matches.length === 1) {
+ return {
+ latex: latex,
+ prefix: latex.substr(0, latex.length - matches[0].length),
+ digits: matches[0]
+ };
+ }
+ };
+ _.renderLatexMathEfficiently = function (latex) {
+ var root = this.root;
+ var oldLatex = this.exportLatex();
+ if (root.ends[L] && root.ends[R] && oldLatex === latex) {
+ return true;
+ }
+ var oldClassification;
+ var classification = this.classifyLatexForEfficientUpdate(latex);
+ if (classification) {
+ oldClassification = this.classifyLatexForEfficientUpdate(oldLatex);
+ if (!oldClassification || oldClassification.prefix !== classification.prefix) {
+ return false;
+ }
+ } else {
+ return false;
}
- return this;
+
+ // check if minus sign is changing
+ var oldDigits = oldClassification.digits;
+ var newDigits = classification.digits;
+ var oldMinusSign = false;
+ var newMinusSign = false;
+ if (oldDigits[0] === '-') {
+ oldMinusSign = true;
+ oldDigits = oldDigits.substr(1);
+ }
+ if (newDigits[0] === '-') {
+ newMinusSign = true;
+ newDigits = newDigits.substr(1);
+ }
+
+ // start at the very end
+ var charNode = this.root.ends[R];
+ var oldCharNodes = [];
+ for (var i= oldDigits.length - 1; i >= 0; i--) {
+ // the tree does not match what we expect
+ if (charNode.ctrlSeq !== oldDigits[i]) {
+ return false;
+ }
+
+ // the trailing digits are not just under the root. We require the root
+ // to be the parent so that we can be sure we do not need a reflow to
+ // grow parens.
+ if (charNode.parent !== root) {
+ return false;
+ }
+
+ // push to the start. We're traversing backwards
+ oldCharNodes.unshift(charNode);
+
+ // move left one character
+ charNode = charNode[L];
+ }
+
+ // remove the minus sign
+ if (oldMinusSign && !newMinusSign) {
+ var oldMinusNode = charNode;
+ if (oldMinusNode.ctrlSeq !== '-') return false;
+ if (oldMinusNode[R] !== oldCharNodes[0]) return false;
+ if (oldMinusNode.parent !== root) return false;
+ if (oldMinusNode[L] && oldMinusNode[L].parent !== root) return false;
+
+ oldCharNodes[0][L] = oldMinusNode[L];
+
+ if (root.ends[L] === oldMinusNode) root.ends[L] = oldCharNodes[0];
+ if (oldMinusNode[L]) oldMinusNode[L][R] = oldCharNodes[0];
+
+ oldMinusNode.jQ.remove();
+ }
+
+ // add a minus sign
+ if (!oldMinusSign && newMinusSign) {
+ var newMinusNode = PlusMinus('-');
+ var minusSpan = document.createElement('span');
+ minusSpan.textContent = '-';
+ newMinusNode.jQ = $(minusSpan);
+
+ if (oldCharNodes[0][L]) oldCharNodes[0][L][R] = newMinusNode;
+ if (root.ends[L] === oldCharNodes[0]) root.ends[L] = newMinusNode;
+
+ newMinusNode.parent = root;
+ newMinusNode[L] = oldCharNodes[0][L];
+ newMinusNode[R] = oldCharNodes[0];
+ oldCharNodes[0][L] = newMinusNode;
+
+ newMinusNode.contactWeld(); // decide if binary operator
+ newMinusNode.jQ.insertBefore(oldCharNodes[0].jQ);
+ }
+
+ // update the text of the current nodes
+ var commonLength = Math.min(oldDigits.length, newDigits.length);
+ for (i=0; i < commonLength; i++) {
+ var newText = newDigits[i];
+ charNode = oldCharNodes[i];
+ if (charNode.ctrlSeq !== newText) {
+ charNode.ctrlSeq = newText;
+ charNode.jQ[0].textContent = newText;
+ charNode.mathspeakName = newText;
+ }
+ }
+
+ // remove the extra digits at the end
+ if (oldDigits.length > newDigits.length) {
+ charNode = oldCharNodes[newDigits.length - 1];
+ root.ends[R] = charNode;
+ charNode[R] = 0;
+
+ for (i = oldDigits.length - 1; i >= commonLength; i--) {
+ oldCharNodes[i].jQ.remove();
+ }
+ }
+
+ // add new digits after the existing ones
+ if (newDigits.length > oldDigits.length) {
+ var frag = document.createDocumentFragment();
+
+ for (i = commonLength; i < newDigits.length; i++) {
+ var span = document.createElement('span');
+ span.className = "mq-digit";
+ span.textContent = newDigits[i];
+
+ var newNode = Digit(newDigits[i]);
+ newNode.parent = root;
+ newNode.jQ = $(span);
+ frag.appendChild(span);
+
+ // splice this node in
+ newNode[L] = root.ends[R];
+ newNode[R] = 0;
+ newNode[L][R] = newNode;
+ root.ends[R] = newNode;
+ }
+
+ root.jQ[0].appendChild(frag);
+ }
+
+ var currentLatex = this.exportLatex();
+ if (currentLatex !== latex) {
+ console.warn('tried updating latex efficiently but did not work. Attempted: ' + latex + ' but wrote: ' + currentLatex);
+ return false;
+ }
+
+ this.cursor.resetToEnd(this);
+
+ var rightMost = root.ends[R];
+ if (rightMost.fixDigitGrouping) {
+ rightMost.fixDigitGrouping(this.cursor.options);
+ }
+
+ return true;
};
- _.renderLatexMath = function(latex) {
+ _.renderLatexMathFromScratch = function (latex) {
var root = this.root, cursor = this.cursor;
-
var all = Parser.all;
var eof = Parser.eof;
var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex);
- root.eachChild('postOrder', 'dispose');
root.ends[L] = root.ends[R] = 0;
if (block) {
@@ -119,19 +279,23 @@ Controller.open(function(_, super_) {
jQ.html(html);
root.jQize(jQ.children());
root.finalizeInsert(cursor.options);
- }
- else {
+ } else {
jQ.empty();
}
-
+ this.updateMathspeak();
delete cursor.selection;
cursor.insAtRightEnd(root);
};
+ _.renderLatexMath = function(latex) {
+ this.notify('replace');
+
+ if (this.renderLatexMathEfficiently(latex)) return;
+ this.renderLatexMathFromScratch(latex);
+ };
_.renderLatexText = function(latex) {
var root = this.root, cursor = this.cursor;
root.jQ.children().slice(1).remove();
- root.eachChild('postOrder', 'dispose');
root.ends[L] = root.ends[R] = 0;
delete cursor.selection;
cursor.show().insAtRightEnd(root);
diff --git a/src/services/mouse.js b/src/services/mouse.js
index c060461cc..b6d4b8b96 100644
--- a/src/services/mouse.js
+++ b/src/services/mouse.js
@@ -3,47 +3,97 @@
*******************************************************/
Controller.open(function(_) {
+ Options.p.ignoreNextMousedown = noop;
+
+ // Whenever edits to the tree occur, in-progress selection events
+ // must be invalidated and selection changes must not be applied to
+ // the edited tree. cancelSelectionOnEdit takes care of this.
+ var cancelSelectionOnEdit;
+ this.onNotify(function (e) {
+ if ((e === 'edit' || e === 'replace')) {
+ // this will be called any time ANY mathquill is edited. We only want
+ // to cancel selection if the selection is happening within the mathquill
+ // that dispatched the notify. Otherwise you won't be able to select any
+ // mathquills while a slider is playing.
+ if (cancelSelectionOnEdit && cancelSelectionOnEdit.cursor === this) {
+ cancelSelectionOnEdit.cb();
+ }
+ }
+ });
+
_.delegateMouseEvents = function() {
var ultimateRootjQ = this.root.jQ;
//drag-to-select event handling
this.container.bind('mousedown.mathquill', function(e) {
var rootjQ = $(e.target).closest('.mq-root-block');
- var root = Node.byId[rootjQ.attr(mqBlockId) || ultimateRootjQ.attr(mqBlockId)];
+ var root = Node.getNodeOfElement(rootjQ[0]) || Node.getNodeOfElement(ultimateRootjQ[0]);
var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink;
var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea;
+ e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
+ e.target.unselectable = true; // http://jsbin.com/yagekiji/1
+
+ if (cursor.options.ignoreNextMousedown(e)) return;
+ else cursor.options.ignoreNextMousedown = noop;
+
var target;
function mousemove(e) { target = $(e.target); }
function docmousemove(e) {
if (!cursor.anticursor) cursor.startSelection();
ctrlr.seek(target, e.pageX, e.pageY).cursor.select();
+ if(cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak') + ' selected').alert();
target = undefined;
}
// outside rootjQ, the MathQuill node corresponding to the target (if any)
// won't be inside this root, so don't mislead Controller::seek with it
- function mouseup(e) {
- cursor.blink = blink;
- if (!cursor.selection) {
- if (ctrlr.editable) {
- cursor.show();
- }
- else {
- textareaSpan.detach();
- }
- }
-
+ function unbindListeners (e) {
// delete the mouse handlers now that we're not dragging anymore
rootjQ.unbind('mousemove', mousemove);
$(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup);
+ cancelSelectionOnEdit = undefined;
+ }
+
+ function updateCursor () {
+ if (ctrlr.editable) {
+ cursor.show();
+ aria.queue(cursor.parent).alert();
+ }
+ else {
+ textareaSpan.detach();
+ }
+ }
+
+ function mouseup(e) {
+ cursor.blink = blink;
+ if (!cursor.selection) updateCursor();
+ unbindListeners(e);
+ }
+
+ var wasEdited;
+ cancelSelectionOnEdit = {
+ cursor: cursor,
+ cb: function () {
+ // If an edit happens while the mouse is down, the existing
+ // selection is no longer valid. Clear it and unbind listeners,
+ // similar to what happens on mouseup.
+ wasEdited = true;
+ cursor.blink = blink;
+ cursor.clearSelection();
+ updateCursor();
+ unbindListeners(e);
+ }
}
if (ctrlr.blurred) {
if (!ctrlr.editable) rootjQ.prepend(textareaSpan);
- textarea.focus();
+ textarea[0].focus();
+ // focus call may bubble to clients, who may then write to
+ // mathquill, triggering cancelSelectionOnEdit. If that happens, we
+ // don't want to stop the cursor blink or bind listeners,
+ // so return early.
+ if (wasEdited) return;
}
- e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
- e.target.unselectable = true; // http://jsbin.com/yagekiji/1
cursor.blink = noop;
ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection();
@@ -57,18 +107,27 @@ Controller.open(function(_) {
});
Controller.open(function(_) {
- _.seek = function(target, pageX, pageY) {
+ _.seek = function($target, pageX, pageY) {
var cursor = this.notify('select').cursor;
+ var node;
+ var targetElm = $target && $target[0];
- if (target) {
- var nodeId = target.attr(mqBlockId) || target.attr(mqCmdId);
- if (!nodeId) {
- var targetParent = target.parent();
- nodeId = targetParent.attr(mqBlockId) || targetParent.attr(mqCmdId);
- }
+ // we can click on an element that is deeply nested past the point
+ // that mathquill knows about. We need to traverse up to the first
+ // node that mathquill is aware of
+ while (targetElm) {
+ // try to find the MQ Node associated with the DOM Element
+ node = Node.getNodeOfElement(targetElm);
+ if (node) break;
+
+ // must be too deep, traverse up to the parent DOM Element
+ targetElm = targetElm.parentElement;
+ }
+
+ // Could not find any nodes, just use the root
+ if (!node) {
+ node = this.root;
}
- var node = nodeId ? Node.byId[nodeId] : this.root;
- pray('nodeId is the id of some Node that exists', node);
// don't clear selection until after getting node from target, in case
// target was selection span, otherwise target will have no parent and will
diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js
index cdb968681..4238a0107 100644
--- a/src/services/saneKeyboardEvents.util.js
+++ b/src/services/saneKeyboardEvents.util.js
@@ -88,15 +88,15 @@ var saneKeyboardEvents = (function() {
// create a keyboard events shim that calls callbacks at useful times
// and exports useful public methods
- return function saneKeyboardEvents(el, handlers) {
+ return function saneKeyboardEvents(el, controller) {
var keydown = null;
var keypress = null;
var textarea = jQuery(el);
- var target = jQuery(handlers.container || textarea);
+ var target = jQuery(controller.container || textarea);
- // checkTextareaFor() is called after keypress or paste events to
- // say "Hey, I think something was just typed" or "pasted" (resp.),
+ // checkTextareaFor() is called after key or clipboard events to
+ // say "Hey, I think something was just typed" or "pasted" etc,
// so that at all subsequent opportune times (next event or timeout),
// will check for expected typed or pasted text.
// Need to check repeatedly because #135: in Safari 5.1 (at least),
@@ -109,8 +109,27 @@ var saneKeyboardEvents = (function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(checker);
}
- target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); });
+ function checkTextareaOnce(checker) {
+ checkTextareaFor(function(e) {
+ checkTextarea = noop;
+ clearTimeout(timeoutId);
+ checker(e);
+ });
+ }
+ target.bind('keydown keypress input keyup paste', function(e) {
+ checkTextarea(e);
+ });
+ function guardedTextareaSelect () {
+ try {
+ // IE can throw an 'Incorrect Function' error if you
+ // try to select a textarea that is hidden. It seems
+ // likely that we don't really care if the selection
+ // fails to happen in this case. Why would the textarea
+ // be hidden? And who would even be able to tell?
+ textarea[0].select();
+ } catch (e) {};
+ }
// -*- public methods -*- //
function select(text) {
@@ -122,7 +141,7 @@ var saneKeyboardEvents = (function() {
clearTimeout(timeoutId);
textarea.val(text);
- if (text && textarea[0].select) textarea[0].select();
+ if (text) guardedTextareaSelect();
shouldBeSelected = !!text;
}
var shouldBeSelected = false;
@@ -140,26 +159,49 @@ var saneKeyboardEvents = (function() {
}
function handleKey() {
- handlers.keystroke(stringify(keydown), keydown);
+ if (controller.options && controller.options.overrideKeystroke) {
+ controller.options.overrideKeystroke(stringify(keydown), keydown);
+ } else {
+ controller.keystroke(stringify(keydown), keydown);
+ }
}
// -*- event handlers -*- //
function onKeydown(e) {
+ if (e.target !== textarea[0]) return;
+
keydown = e;
keypress = null;
- if (shouldBeSelected) checkTextareaFor(function(e) {
- if (!(e && e.type === 'focusout') && textarea[0].select) {
- textarea[0].select(); // re-select textarea in case it's an unrecognized
+ if (shouldBeSelected) checkTextareaOnce(function(e) {
+ if (!(e && e.type === 'focusout')) {
+ // re-select textarea in case it's an unrecognized key that clears
+ // the selection, then never again, 'cos next thing might be blur
+ guardedTextareaSelect()
}
- checkTextarea = noop; // key that clears the selection, then never
- clearTimeout(timeoutId); // again, 'cos next thing might be blur
});
handleKey();
}
+ function isArrowKey (e) {
+ if (!e || !e.originalEvent) return false;
+
+ // The keyPress event in FF reports which=0 for some reason. The new
+ // .key property seems to report reasonable results, so we're using that
+ switch (e.originalEvent.key) {
+ case 'ArrowRight':
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ case 'ArrowUp':
+ return true;
+ }
+
+ return false;
+ }
function onKeypress(e) {
+ if (e.target !== textarea[0]) return;
+
// call the key handler for repeated keypresses.
// This excludes keypresses that happen directly
// after keydown. In that case, there will be
@@ -168,7 +210,28 @@ var saneKeyboardEvents = (function() {
keypress = e;
- checkTextareaFor(typedText);
+ // only check for typed text if this key can type text. Otherwise
+ // you can end up with mathquill thinking text was typed if you
+ // use the mq.keystroke('Right') command while a single character
+ // is selected. Only detected in FF.
+ if (!isArrowKey(e)) {
+ checkTextareaFor(typedText);
+ }
+ }
+ function onKeyup(e) {
+ if (e.target !== textarea[0]) return;
+
+ // Handle case of no keypress event being sent
+ if (!!keydown && !keypress) {
+
+ // only check for typed text if this key can type text. Otherwise
+ // you can end up with mathquill thinking text was typed if you
+ // use the mq.keystroke('Right') command while a single character
+ // is selected. Only detected in FF.
+ if (!isArrowKey(e)) {
+ checkTextareaFor(typedText);
+ }
+ }
}
function typedText() {
// If there is a selection, the contents of the textarea couldn't
@@ -193,15 +256,27 @@ var saneKeyboardEvents = (function() {
var text = textarea.val();
if (text.length === 1) {
textarea.val('');
- handlers.typedText(text);
+ if (controller.options && controller.options.overrideTypedText) {
+ controller.options.overrideTypedText(text);
+ } else {
+ controller.typedText(text);
+ }
} // in Firefox, keys that don't type text, just clear seln, fire keypress
// https://github.com/mathquill/mathquill/issues/293#issuecomment-40997668
- else if (text && textarea[0].select) textarea[0].select(); // re-select if that's why we're here
+ else if (text) guardedTextareaSelect(); // re-select if that's why we're here
}
- function onBlur() { keydown = keypress = null; }
+ function onBlur() {
+ keydown = null;
+ keypress = null;
+ checkTextarea = noop;
+ clearTimeout(timeoutId);
+ textarea.val('');
+ }
function onPaste(e) {
+ if (e.target !== textarea[0]) return;
+
// browsers are dumb.
//
// In Linux, middle-click pasting causes onPaste to be called,
@@ -214,23 +289,41 @@ var saneKeyboardEvents = (function() {
// on keydown too, FWIW).
//
// And by nifty, we mean dumb (but useful sometimes).
- textarea.focus();
+ if (document.activeElement !== textarea[0]) {
+ textarea[0].focus();
+ }
checkTextareaFor(pastedText);
}
function pastedText() {
var text = textarea.val();
textarea.val('');
- if (text) handlers.paste(text);
+ if (text) controller.paste(text);
}
// -*- attach event handlers -*- //
- target.bind({
- keydown: onKeydown,
- keypress: onKeypress,
- focusout: onBlur,
- paste: onPaste
- });
+
+ if (controller.options && controller.options.disableCopyPaste) {
+ target.bind({
+ keydown: onKeydown,
+ keypress: onKeypress,
+ keyup: onKeyup,
+ focusout: onBlur,
+ copy: function(e) { e.preventDefault(); },
+ cut: function(e) { e.preventDefault(); },
+ paste: function(e) { e.preventDefault(); }
+ });
+ } else {
+ target.bind({
+ keydown: onKeydown,
+ keypress: onKeypress,
+ keyup: onKeyup,
+ focusout: onBlur,
+ cut: function() { checkTextareaOnce(function() { controller.cut(); }); },
+ copy: function() { checkTextareaOnce(function() { controller.copy(); }); },
+ paste: onPaste
+ });
+ }
// -*- export public methods -*- //
return {
diff --git a/src/services/scrollHoriz.js b/src/services/scrollHoriz.js
index 73c8f0170..bc7afb4cb 100644
--- a/src/services/scrollHoriz.js
+++ b/src/services/scrollHoriz.js
@@ -4,16 +4,36 @@
**********************************************/
Controller.open(function(_) {
+ _.setOverflowClasses = function () {
+ var root = this.root.jQ[0];
+ var shouldHaveOverflowRight = false;
+ var shouldHaveOverflowLeft = false;
+ if (!this.blurred) {
+ var width = root.getBoundingClientRect().width;
+ var scrollWidth = root.scrollWidth;
+ var scroll = root.scrollLeft;
+ shouldHaveOverflowRight = (scrollWidth > width + scroll);
+ shouldHaveOverflowLeft = (scroll > 0);
+ }
+ if (root.classList.contains('mq-editing-overflow-right') !== shouldHaveOverflowRight)
+ root.classList.toggle('mq-editing-overflow-right')
+ if (root.classList.contains('mq-editing-overflow-left') !== shouldHaveOverflowLeft)
+ root.classList.toggle('mq-editing-overflow-left')
+ }
_.scrollHoriz = function() {
var cursor = this.cursor, seln = cursor.selection;
var rootRect = this.root.jQ[0].getBoundingClientRect();
- if (!seln) {
+ if (!cursor.jQ[0] && !seln) {
+ this.root.jQ.stop().animate({scrollLeft: 0}, 100, function () {
+ this.setOverflowClasses();
+ }.bind(this));
+ return;
+ } else if (!seln) {
var x = cursor.jQ[0].getBoundingClientRect().left;
if (x > rootRect.right - 20) var scrollBy = x - (rootRect.right - 20);
else if (x < rootRect.left + 20) var scrollBy = x - (rootRect.left + 20);
else return;
- }
- else {
+ } else {
var rect = seln.jQ[0].getBoundingClientRect();
var overLeft = rect.left - (rootRect.left + 20);
var overRight = rect.right - (rootRect.right - 20);
@@ -34,6 +54,12 @@ Controller.open(function(_) {
else return;
}
}
- this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100);
+
+ var root = this.root.jQ[0]
+ if (scrollBy < 0 && root.scrollLeft === 0) return
+ if (scrollBy > 0 && root.scrollWidth <= root.scrollLeft + rootRect.width) return
+ this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100, function () {
+ this.setOverflowClasses();
+ }.bind(this));
};
});
diff --git a/src/services/textarea.js b/src/services/textarea.js
index dd2d2de65..78f04d102 100644
--- a/src/services/textarea.js
+++ b/src/services/textarea.js
@@ -6,7 +6,7 @@
Controller.open(function(_) {
Options.p.substituteTextarea = function() {
return $('')[0];
+ 'spellcheck=false x-palm-disable-ste-all=true/>')[0];
};
_.createTextarea = function() {
var textareaSpan = this.textareaSpan = $(''),
@@ -18,15 +18,15 @@ Controller.open(function(_) {
var ctrlr = this;
ctrlr.cursor.selectionChanged = function() { ctrlr.selectionChanged(); };
- ctrlr.container.bind('copy', function() { ctrlr.setTextareaSelection(); });
};
_.selectionChanged = function() {
var ctrlr = this;
- forceIERedraw(ctrlr.container[0]);
// throttle calls to setTextareaSelection(), because setting textarea.value
// and/or calling textarea.select() can have anomalously bad performance:
// https://github.com/mathquill/mathquill/issues/43#issuecomment-1399080
+ //
+ // Note, this timeout may be cleared by the blur handler in focusBlur.js
if (ctrlr.textareaSelectionTimeout === undefined) {
ctrlr.textareaSelectionTimeout = setTimeout(function() {
ctrlr.setTextareaSelection();
@@ -37,7 +37,8 @@ Controller.open(function(_) {
this.textareaSelectionTimeout = undefined;
var latex = '';
if (this.cursor.selection) {
- latex = this.cursor.selection.join('latex');
+ //cleanLatex prunes unnecessary spaces. defined in latex.js
+ latex = this.cleanLatex(this.cursor.selection.join('latex'));
if (this.options.statelessClipboard) {
// FIXME: like paste, only this works for math fields; should ask parent
latex = '$' + latex + '$';
@@ -46,13 +47,21 @@ Controller.open(function(_) {
this.selectFn(latex);
};
_.staticMathTextareaEvents = function() {
- var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor,
+ var ctrlr = this, cursor = ctrlr.cursor,
textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan;
- this.container.prepend('$'+ctrlr.exportLatex()+'$');
+ this.container.prepend(jQuery('