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/Makefile b/Makefile
index a2319d23b..46f811553 100644
--- a/Makefile
+++ b/Makefile
@@ -32,17 +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/math.js \
$(SRC_DIR)/commands/text.js \
$(SRC_DIR)/commands/math/*.js
-# FIXME text.js currently depends on math.js (#435), restore these when fixed:
-# $(SRC_DIR)/commands/*.js \
-# $(SRC_DIR)/commands/*/*.js
SOURCES_BASIC = \
$(BASE_SOURCES) \
@@ -98,6 +103,7 @@ BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile
.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)
@@ -111,6 +117,7 @@ $(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)
@@ -118,6 +125,7 @@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED)
$(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)
@@ -125,10 +133,12 @@ $(UGLY_BASIC_JS): $(BASIC_JS) $(NODE_MODULES_INSTALLED)
$(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
diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md
index 60f6bb0b8..f532f5ff3 100644
--- a/docs/Api_Methods.md
+++ b/docs/Api_Methods.md
@@ -221,6 +221,26 @@ Simulates typing text, one character at a time from where the cursor currently i
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.
diff --git a/docs/Config.md b/docs/Config.md
index d47bc1a1c..b8125ef49 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -87,6 +87,7 @@ Just like [`autoCommands`](#autocommands) above, this takes a string formatted a
`maxDepth` specifies the maximum number of nested MathBlocks. When `maxDepth` is set to 1, the user can type simple math symbols directly into the editor but not into nested MathBlocks, e.g. the numerator and denominator of a fraction.
Nested content in latex rendered during initialization or pasted into mathquill is truncated to avoid violating `maxDepth`. When `maxDepth` is not set, no depth limit is applied by default.
+You can also specify a speech-friendly representation of the operator name by supplying the operator name, a `|` and its speech alternative (separate multiple words with a `-`). For example, `'sin|sine cos|cosine tan|tangent sinh|hyperbolic-sine'`.
## substituteTextarea
@@ -158,13 +159,3 @@ For example, to style as white-on-black instead of black-on-white use:
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');
- }
-
-## Color Change Support on IE8
-
-To support a MathQuill editable background color other than white in IE8, set the background color on both the editable mathField and on elements with class `mq-matrixed`. Then set a Chroma filter on elements with class `mq-matrixed-container`.
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 9cb6c4b44..475a7c727 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,9 @@
"quickstart.html"
],
"devDependencies": {
- "pjs": ">=3.1.0 <5.0.0",
+ "less": ">=1.5.1 <3.0.0",
"mocha": ">=2.4.1",
- "uglify-js": "2.x",
- "less": ">=1.5.1"
+ "pjs": ">=3.1.0 <5.0.0",
+ "uglify-js": "2.x"
}
}
diff --git a/script/test_server.js b/script/test_server.js
index 17a7420d9..b653eb424 100644
--- a/script/test_server.js
+++ b/script/test_server.js
@@ -84,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 50dc4a2ec..48bfd0e8d 100644
--- a/src/commands/math.js
+++ b/src/commands/math.js
@@ -12,19 +12,19 @@ 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
@@ -145,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];
@@ -276,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;
@@ -305,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');
});
};
@@ -327,15 +330,24 @@ var MathCommand = P(MathElement, function(_, super_) {
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+' ');
+ });
+ };
});
/**
* 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 ]);
};
@@ -351,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];
@@ -364,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
);
};
});
@@ -400,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
@@ -416,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);
@@ -445,6 +504,8 @@ var MathBlock = P(MathElement, function(_, super_) {
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
@@ -455,6 +516,12 @@ var MathBlock = P(MathElement, function(_, super_) {
if (cursor.selection) cmd.replaces(cursor.replaceSelection());
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 }));
+ }
}
};
@@ -473,7 +540,7 @@ var MathBlock = P(MathElement, function(_, super_) {
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(); });
}
};
@@ -485,9 +552,14 @@ 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;
};
});
@@ -507,17 +579,30 @@ API.StaticMath = function(APIClasses) {
};
_.init = function() {
super_.init.apply(this, arguments);
- this.__controller.root.postOrder(
- 'registerInnerField', this.innerFields = [], APIClasses.InnerMathField);
+ 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.InnerMathField);
+ 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();
+ };
});
};
diff --git a/src/commands/math/LatexCommandInput.js b/src/commands/math/LatexCommandInput.js
index 19dfc82a6..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);
+ 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 1cd72f042..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['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast =
- bind(BinaryOperator,'\\ast ','∗');
-
+ 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,7 +65,7 @@ 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(_) {
@@ -91,256 +88,251 @@ LatexCmds.mathbb = P(MathCommand, function(_) {
});
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.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 ', '⊙');
-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.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.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦');
-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.nless = bind(VanillaSymbol, '\\nless ', '≮');
-LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯');
+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 ', '▱');
-LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜');
+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.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 ','ℑ');
-
-LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂');
+ bind(VanillaSymbol,'\\Im ','ℑ', 'imaginary');
-LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity =
- bind(VanillaSymbol,'\\infty ','∞');
+LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂', 'partial');
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 ', '∄');
+ bind(VanillaSymbol, '\\nexists ', '∄', 'there is no');
LatexCmds.and = LatexCmds.land = LatexCmds.wedge =
- bind(BinaryOperator,'\\wedge ','∧');
+ bind(BinaryOperator,'\\wedge ','∧', 'and');
-LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\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');
// FIXME: the correct LaTeX would be ^\circ but we can't parse that
-LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°');
+LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°', 'degrees');
-LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠');
-LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡');
+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 49198a404..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_) {
@@ -42,6 +208,23 @@ var Variable = P(Symbol, function(_, super_) {
}
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 };
@@ -65,10 +248,28 @@ 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) {
- super_.createLeftOf.apply(this, arguments);
+ _.checkAutoCmds = function (cursor) {
+ //handle autoCommands
var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength;
if (maxLength > 0) {
// want longest possible autocommand, so join together longest
@@ -89,6 +290,43 @@ var Letter = P(Variable, function(_, super_) {
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;
@@ -105,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;
@@ -157,8 +401,19 @@ var Letter = P(Variable, function(_, super_) {
}
};
function shouldOmitPadding(node) {
- // omit padding if no node, or if node already has padding (to avoid double-padding)
- return !node || (node instanceof BinaryOperator) || (node instanceof SummationNotation);
+ // 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 = {}; // the set of operator names like \sin, \cos, etc that
@@ -199,9 +454,13 @@ var TwoWordOpNames = { limsup: 1, liminf: 1, projlim: 1, injlim: 1 };
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) {
@@ -209,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;
@@ -239,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;
+ });
};
});
@@ -254,15 +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['″'] = LatexCmds.dprime = 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_) {
@@ -272,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
@@ -303,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
@@ -358,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 =
@@ -389,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);
@@ -421,54 +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) {
- function determineOpClassType(node) {
- if (node[L]) {
- // If the left sibling is a binary operator or a separator (comma, semicolon, colon)
- // 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 '';
- }
- } 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 determineOpClassType(node.parent.parent);
- } else {
- return '';
- }
-
- return 'mq-binary-operator';
- };
-
if (dir === R) return; // ignore if sibling only changed on the right
- this.jQ[0].className = determineOpClassType(this);
+ 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) {
@@ -476,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;
@@ -484,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);
@@ -520,9 +942,45 @@ var Equality = P(BinaryOperator, function(_, super_) {
});
LatexCmds['='] = Equality;
-LatexCmds['×'] = 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 1889ba6a9..3a5214ac8 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -1,88 +1,127 @@
/***************************
* 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.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"');
-LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"');
-LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-both"');
-LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"');
+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', ''
@@ -105,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() + '}';
@@ -144,6 +185,8 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) {
.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);
})
;
@@ -156,6 +199,28 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) {
};
});
+// 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) {
@@ -211,11 +276,14 @@ var SupSub = P(MathCommand, function(_, super_) {
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);
};
@@ -246,7 +314,7 @@ 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);
};
@@ -261,16 +329,19 @@ var SupSub = P(MathCommand, function(_, super_) {
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) {
@@ -292,28 +363,6 @@ 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) {
@@ -337,6 +386,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;
@@ -354,6 +405,53 @@ LatexCmds['^'] = P(SupSub, function(_, super_) {
+ ''
;
_.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;
@@ -362,7 +460,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'
@@ -381,11 +480,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;
@@ -407,6 +510,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];
@@ -416,19 +521,20 @@ 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 =
''
+ '∫'
@@ -439,12 +545,11 @@ LatexCmds.integral = P(SummationNotation, function(_, super_) {
+ ''
+ ''
;
- Symbol.prototype.init.call(this, '\\int ', htmlTemplate);
+ Symbol.prototype.init.call(this, '\\int ', htmlTemplate, 'integral');
};
// FIXME: refactor rather than overriding
_.createLeftOf = MathCommand.p.createLeftOf;
});
-
var Fraction =
LatexCmds.frac =
LatexCmds.dfrac =
@@ -462,6 +567,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);
};
});
@@ -471,16 +670,18 @@ 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)
@@ -496,17 +697,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) {
@@ -518,10 +744,6 @@ 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 Hat = LatexCmds.hat = P(MathCommand, function(_, super_) {
@@ -538,16 +760,40 @@ var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) {
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_) {
@@ -571,11 +817,6 @@ function DelimsMixin(_, super_) {
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)
@@ -590,23 +831,48 @@ 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;
};
+ _.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
@@ -617,8 +883,9 @@ 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,
@@ -642,7 +909,7 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) {
.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;
@@ -694,8 +961,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];
@@ -709,6 +977,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);
};
@@ -746,20 +1024,28 @@ var OPP_BRACKS = {
'\\rVert ' : '\\lVert ',
};
-function bindCharBracketPair(open, ctrlSeq) {
+var BRACKET_NAMES = {
+ '〈': 'angle-bracket',
+ '〉': 'angle-bracket',
+ '|': 'pipe'
+};
+
+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() {
var regex = Parser.regex;
@@ -769,13 +1055,13 @@ LatexCmds.left = P(MathCommand, function(_) {
return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle(?![a-zA-Z])|\\lVert(?![a-zA-Z]))/))
.then(function(ctrlSeq) {
- var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : 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(/^(?:[\])|]|\\\}|\\rangle(?![a-zA-Z])|\\rVert(?![a-zA-Z]))/)).map(function(end) {
- var close = (end.charAt(0) === '\\' ? end.slice(1) : 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);
@@ -799,20 +1085,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 =
diff --git a/src/commands/text.js b/src/commands/text.js
index f1dcc4771..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)
@@ -35,7 +36,7 @@ var TextBlock = P(Node, function(_, super_) {
if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L);
if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R);
- textBlock.bubble('reflow');
+ textBlock.bubble(function (node) { node.reflow(); });
};
_.parser = function() {
@@ -65,7 +66,7 @@ var TextBlock = P(Node, function(_, super_) {
_.latex = function() {
var contents = this.textContents();
if (contents.length === 0) return '';
- return '\\text{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}';
+ return this.ctrlSeq + '{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}';
};
_.html = function() {
return (
@@ -74,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.
@@ -115,12 +133,14 @@ var TextBlock = P(Node, function(_, super_) {
cursor.insLeftOf(this);
super_.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness
}
- this.bubble('reflow');
+ 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('reflow');
+ this.bubble(function (node) { node.reflow(); });
};
_.seek = function(pageX, cursor) {
@@ -248,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);
}
};
@@ -308,28 +333,33 @@ 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/main.less b/src/css/main.less
index ba94dd2ed..691e742d5 100644
--- a/src/css/main.less
+++ b/src/css/main.less
@@ -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 dfa53e265..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,17 +90,19 @@
// 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 {
display: inline-block;
white-space: pre;
@@ -127,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;
}
@@ -160,7 +215,7 @@
&.mq-sup-only {
vertical-align: .5em;
- .mq-sup {
+ & > .mq-sup {
display: inline-block;
vertical-align: text-bottom;
}
@@ -196,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 {
@@ -285,16 +343,20 @@
// \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;
@@ -311,7 +373,7 @@
display: block;
text-align: center;
}
-
+
.mq-hat-prefix {
display: block;
text-align: center;
@@ -369,16 +431,24 @@
padding-top: 0.2em;
text-align: center;
- &:before {
- display: block;
- position: relative;
- top: -0.34em;
+ &:after {
+ position: absolute;
+ right: -0.1em;
+ top: -0.48em;
font-size: 0.5em;
- line-height: 0em;
content: '\27A4';
- text-align: right;
}
- &.mq-arrow-left:before {
+ //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);
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 b53bf6b93..e1a608aab 100644
--- a/src/css/mixins/css3.less
+++ b/src/css/mixins/css3.less
@@ -1,10 +1,3 @@
-.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;
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/cursor.js b/src/cursor.js
index 3a6b0f2c7..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;
@@ -98,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()
@@ -232,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();
@@ -290,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/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
index 2e39ec7bb..3c1edaa5a 100644
Binary files a/src/fonts/Symbola-basic.eot and b/src/fonts/Symbola-basic.eot differ
diff --git a/src/fonts/Symbola-basic.ttf b/src/fonts/Symbola-basic.ttf
index cf968b9ad..d1eda7b9e 100644
Binary files a/src/fonts/Symbola-basic.ttf and b/src/fonts/Symbola-basic.ttf differ
diff --git a/src/fonts/Symbola-basic.woff b/src/fonts/Symbola-basic.woff
index 104a15090..a262924b9 100644
Binary files a/src/fonts/Symbola-basic.woff and b/src/fonts/Symbola-basic.woff differ
diff --git a/src/fonts/Symbola-basic.woff2 b/src/fonts/Symbola-basic.woff2
index 7c0d6c8cf..44e703bb4 100644
Binary files a/src/fonts/Symbola-basic.woff2 and b/src/fonts/Symbola-basic.woff2 differ
diff --git a/src/intro.js b/src/intro.js
index 2057d8404..1ff07bbb6 100644
--- a/src/intro.js
+++ b/src/intro.js
@@ -12,8 +12,6 @@
var jQuery = window.jQuery,
undefined,
- mqCmdId = 'mathquill-command-id',
- mqBlockId = 'mathquill-block-id',
min = Math.min,
max = Math.max;
@@ -21,82 +19,11 @@ if (!jQuery) throw 'MathQuill requires jQuery 1.5.2+ to be loaded first';
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 ]));
- };
-}
-
-/**
- * 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_);
- });
-}
-
/**
* 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 85afad967..c3a2dab1a 100644
--- a/src/publicapi.js
+++ b/src/publicapi.js
@@ -70,8 +70,8 @@ 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 = {};
@@ -109,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() {
@@ -122,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);
@@ -138,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;
};
});
@@ -152,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);
@@ -162,7 +167,7 @@ function getInterface(v) {
};
_.empty = function() {
var root = this.__controller.root, cursor = this.__controller.cursor;
- root.eachChild('postOrder', 'dispose');
+
root.ends[L] = root.ends[R] = 0;
root.jQ.empty();
delete cursor.selection;
@@ -178,11 +183,12 @@ function getInterface(v) {
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;
};
@@ -204,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;
};
@@ -224,9 +230,22 @@ function getInterface(v) {
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);
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 be876a23c..148082a91 100644
--- a/src/services/focusBlur.js
+++ b/src/services/focusBlur.js
@@ -1,25 +1,63 @@
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).bind('blur', windowBlur);
});
@@ -27,11 +65,16 @@ Controller.open(function(_) {
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).unbind('blur', windowBlur);
+
+ if (ctrlr.options && ctrlr.options.resetCursorOnBlur) {
+ cursor.resetToEnd(ctrlr);
+ }
}
ctrlr.blurred = true;
cursor.hide().parent.blur();
diff --git a/src/services/keystroke.js b/src/services/keystroke.js
index a53cb5a4d..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,7 +313,7 @@ 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;
};
@@ -244,16 +323,20 @@ Controller.open(function(_) {
if (!cursor[dir] || cursor.selection) return this.deleteDir(dir);
this.notify('edit');
+ var fragRemoved;
if (dir === L) {
- Fragment(cursor.parent.ends[L], cursor[L]).remove();
+ fragRemoved = Fragment(cursor.parent.ends[L], cursor[L]);
} else {
- Fragment(cursor[R], cursor.parent.ends[R]).remove();
- };
+ 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;
};
@@ -281,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 a9fd123a1..432c93261 100644
--- a/src/services/latex.js
+++ b/src/services/latex.js
@@ -18,6 +18,7 @@ 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;
@@ -26,6 +27,7 @@ var latexMathParser = (function() {
// 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 =
@@ -49,6 +51,7 @@ var latexMathParser = (function() {
var command =
controlSequence
.or(variable)
+ .or(number)
.or(symbol)
;
@@ -74,8 +77,12 @@ 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) {
@@ -87,39 +94,208 @@ Controller.open(function(_, super_) {
return this;
};
- _.renderLatexMath = function(latex) {
+
+ _.classifyLatexForEfficientUpdate = function (latex) {
+ if (typeof latex !== 'string') return;
+
+ 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 cursor = this.cursor;
- var options = cursor.options;
- var jQ = root.jQ;
+ 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;
+ }
+
+
+ // 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;
+ };
+ _.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 && block.prepareInsertionAt(cursor)) {
+ if (block) {
block.children().adopt(root, 0, 0);
+ }
+
+ var jQ = root.jQ;
+
+ if (block) {
var html = block.join('html');
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 b2b543a51..b6d4b8b96 100644
--- a/src/services/mouse.js
+++ b/src/services/mouse.js
@@ -4,12 +4,29 @@
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;
@@ -24,30 +41,58 @@ Controller.open(function(_) {
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;
}
cursor.blink = noop;
@@ -62,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 5d01abedc..4238a0107 100644
--- a/src/services/saneKeyboardEvents.util.js
+++ b/src/services/saneKeyboardEvents.util.js
@@ -88,12 +88,12 @@ 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 key or clipboard events to
// say "Hey, I think something was just typed" or "pasted" etc,
@@ -116,8 +116,20 @@ var saneKeyboardEvents = (function() {
checker(e);
});
}
- target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(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) {
@@ -129,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;
@@ -147,7 +159,11 @@ 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 -*- //
@@ -158,16 +174,31 @@ var saneKeyboardEvents = (function() {
keypress = null;
if (shouldBeSelected) checkTextareaOnce(function(e) {
- if (!(e && e.type === 'focusout') && textarea[0].select) {
+ 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
- textarea[0].select();
+ guardedTextareaSelect()
}
});
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;
@@ -179,13 +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) checkTextareaFor(typedText);
+ 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
@@ -210,13 +256,23 @@ 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;
@@ -234,7 +290,7 @@ var saneKeyboardEvents = (function() {
//
// And by nifty, we mean dumb (but useful sometimes).
if (document.activeElement !== textarea[0]) {
- textarea.focus();
+ textarea[0].focus();
}
checkTextareaFor(pastedText);
@@ -242,19 +298,32 @@ var saneKeyboardEvents = (function() {
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,
- keyup: onKeyup,
- focusout: onBlur,
- cut: function() { checkTextareaOnce(function() { handlers.cut(); }); },
- copy: function() { checkTextareaOnce(function() { handlers.copy(); }); },
- 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 23b2ecab1..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 = $(''),
@@ -21,11 +21,12 @@ Controller.open(function(_) {
};
_.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();
@@ -36,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 + '$';
@@ -45,15 +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(jQuery('')
+ this.container.prepend(jQuery(''
;
html =
- ''
- + 'Block:0'
- + 'Block:1'
+ ''
;
@@ -44,7 +44,7 @@ suite('HTML', function() {
test('context-free HTML templates', function() {
var htmlTemplate = ' In Safari on iOS, this should be focusable but not bring up the on-screen keyboard; to test, try focusing anything else and confirm this blurs: (confirmed working on iOS 6.1.3) Should be able to prevent cut, typing, and pasting in this field: 1+2+3 Should be able to prevent typing in this field: 1+2+3 Should wrap anything you type in '<>': 1+2+3 Spaces at the beginning and end of text mode blocks should be visible: 1\text{ And }2 Mutiple consecutive spaces in the middle of a text mode block should not collapse into one space: \text{three spaces}
+
';
- var html = '
';
+ var html = '';
assert.equal(html, renderHtml(0, htmlTemplate), 'self-closing tag');
@@ -57,11 +57,11 @@ suite('HTML', function() {
+ ''
;
html =
- ''
- + 'Block:0'
+ ''
- + ''
- + 'Block:1'
+ + ''
;
@@ -81,17 +81,17 @@ suite('HTML', function() {
+ '&0'
;
html =
- ''
- + ''
- + ''
+ ''
+ + ''
+ + ''
- + 'Block:0'
+ + ''
;
assert.equal(html, renderHtml(2, htmlTemplate), 'multiple nested cmd and block spans');
diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js
index 9b59e8861..c2a2ddf80 100644
--- a/test/unit/latex.test.js
+++ b/test/unit/latex.test.js
@@ -2,7 +2,9 @@ suite('latex', function() {
function assertParsesLatex(str, latex) {
if (arguments.length < 2) latex = str;
- var result = latexMathParser.parse(str).postOrder('finalizeTree', Options.p).join('latex');
+ var result = latexMathParser.parse(str).postOrder(function (node) {
+ node.finalizeTree(Options.p)
+ }).join('latex');
assert.equal(result, latex,
'parsing \''+str+'\', got \''+result+'\', expected \''+latex+'\''
);
@@ -35,23 +37,23 @@ suite('latex', function() {
});
test('simple exponent', function() {
- assertParsesLatex('x^n');
+ assertParsesLatex('x^{n}');
});
test('block exponent', function() {
- assertParsesLatex('x^{n}', 'x^n');
+ assertParsesLatex('x^{n}', 'x^{n}');
assertParsesLatex('x^{nm}');
assertParsesLatex('x^{}', 'x^{ }');
});
test('nested exponents', function() {
- assertParsesLatex('x^{n^m}');
+ assertParsesLatex('x^{n^{m}}');
});
test('exponents with spaces', function() {
- assertParsesLatex('x^ 2', 'x^2');
+ assertParsesLatex('x^ 2', 'x^{2}');
- assertParsesLatex('x ^2', 'x^2');
+ assertParsesLatex('x ^2', 'x^{2}');
});
test('inner groups', function() {
@@ -193,13 +195,13 @@ suite('latex', function() {
test('basic rendering', function() {
assertParsesLatex('x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }',
- 'x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}');
+ 'x=\\frac{-b\\pm\\sqrt{b^{2}-4ac}}{2a}');
});
test('re-rendering', function() {
- assertParsesLatex('a x^2 + b x + c = 0', 'ax^2+bx+c=0');
+ assertParsesLatex('a x^2 + b x + c = 0', 'ax^{2}+bx+c=0');
assertParsesLatex('x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }',
- 'x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}');
+ 'x=\\frac{-b\\pm\\sqrt{b^{2}-4ac}}{2a}');
});
test('empty LaTeX', function () {
@@ -266,23 +268,23 @@ suite('latex', function() {
suite('\\sum', function() {
test('basic', function() {
mq.write('\\sum_{n=0}^5');
- assert.equal(mq.latex(), '\\sum_{n=0}^5');
+ assert.equal(mq.latex(), '\\sum_{n=0}^{5}');
mq.write('x^n');
- assert.equal(mq.latex(), '\\sum_{n=0}^5x^n');
+ assert.equal(mq.latex(), '\\sum_{n=0}^{5}x^{n}');
});
test('only lower bound', function() {
mq.write('\\sum_{n=0}');
assert.equal(mq.latex(), '\\sum_{n=0}^{ }');
mq.write('x^n');
- assert.equal(mq.latex(), '\\sum_{n=0}^{ }x^n');
+ assert.equal(mq.latex(), '\\sum_{n=0}^{ }x^{n}');
});
test('only upper bound', function() {
mq.write('\\sum^5');
- assert.equal(mq.latex(), '\\sum_{ }^5');
+ assert.equal(mq.latex(), '\\sum_{ }^{5}');
mq.write('x^n');
- assert.equal(mq.latex(), '\\sum_{ }^5x^n');
+ assert.equal(mq.latex(), '\\sum_{ }^{5}x^{n}');
});
});
});
@@ -300,25 +302,25 @@ suite('latex', function() {
});
test('initial latex', function() {
- assert.equal(inner1.latex(), 'x_0+x_1+x_2');
+ assert.equal(inner1.latex(), 'x_{0}+x_{1}+x_{2}');
assert.equal(inner2.latex(), '3');
- assert.equal(outer.latex(), '\\frac{x_0+x_1+x_2}{3}');
+ assert.equal(outer.latex(), '\\frac{x_{0}+x_{1}+x_{2}}{3}');
});
test('setting latex', function() {
inner1.latex('\\sum_{i=0}^N x_i');
inner2.latex('N');
- assert.equal(inner1.latex(), '\\sum_{i=0}^Nx_i');
+ assert.equal(inner1.latex(), '\\sum_{i=0}^{N}x_{i}');
assert.equal(inner2.latex(), 'N');
- assert.equal(outer.latex(), '\\frac{\\sum_{i=0}^Nx_i}{N}');
+ assert.equal(outer.latex(), '\\frac{\\sum_{i=0}^{N}x_{i}}{N}');
});
test('writing latex', function() {
inner1.write('+ x_3');
inner2.write('+ 1');
- assert.equal(inner1.latex(), 'x_0+x_1+x_2+x_3');
+ assert.equal(inner1.latex(), 'x_{0}+x_{1}+x_{2}+x_{3}');
assert.equal(inner2.latex(), '3+1');
- assert.equal(outer.latex(), '\\frac{x_0+x_1+x_2+x_3}{3+1}');
+ assert.equal(outer.latex(), '\\frac{x_{0}+x_{1}+x_{2}+x_{3}}{3+1}');
});
test('optional inner field name', function() {
@@ -336,7 +338,7 @@ suite('latex', function() {
mantissa.latex('1.2345');
base.latex('10');
exp.latex('8');
- assert.equal(outer.latex(), '1.2345\\cdot10^8');
+ assert.equal(outer.latex(), '1.2345\\cdot10^{8}');
});
test('make inner field static and then editable', function() {
diff --git a/test/unit/paste.test.js b/test/unit/paste.test.js
new file mode 100644
index 000000000..62a37fb98
--- /dev/null
+++ b/test/unit/paste.test.js
@@ -0,0 +1,70 @@
+suite('paste', function() {
+ var mq;
+ setup(function() {
+ mq = MQ.MathField($('').appendTo('#mock')[0]);
+ });
+
+ function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); }
+
+ function assertLatex(latex) {
+ prayWellFormedPoint(mq.__controller.cursor);
+ assert.equal(mq.latex(), latex);
+ }
+
+ suite('√', function() {
+ test('sqrt symbol in empty latex', function() {
+ $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input');
+ assertLatex('\\sqrt{ }');
+ })
+ test('sqrt symbol in non-empty latex', function() {
+ mq.latex('1+');
+ $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input');
+ assertLatex('1+\\sqrt{ }');
+ })
+ test('sqrt symbol at start of non-empty latex', function () {
+ mq.latex('1+');
+ mq.moveToLeftEnd()
+ $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input');
+ assertLatex('\\sqrt{ }1+');
+ })
+ });
+
+ suite('√2', function() {
+ test('sqrt symbol in empty latex', function() {
+ $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input');
+ assertLatex('\\sqrt{ }2');
+ })
+ test('sqrt symbol in non-empty latex', function() {
+ mq.latex('1+');
+ $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input');
+ assertLatex('1+\\sqrt{ }2');
+ })
+ test('sqrt symbol at start of non-empty latex', function () {
+ mq.latex('1+');
+ mq.moveToLeftEnd()
+ $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input');
+ assertLatex('\\sqrt{ }21+');
+ })
+ });
+
+ suite('sqrt text', function() {
+ test('sqrt symbol in empty latex', function() {
+ $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input');
+ assertLatex('sqrt');
+ })
+ test('sqrt symbol in non-empty latex', function() {
+ mq.latex('1+');
+ $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input');
+ assertLatex('1+sqrt');
+ })
+ test('sqrt symbol at start of non-empty latex', function () {
+ mq.latex('1+');
+ mq.moveToLeftEnd()
+ $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input');
+ assertLatex('sqrt1+');
+ })
+ });
+
+});
+
+
diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js
index 0eebba6fe..e3fffd05b 100644
--- a/test/unit/publicapi.test.js
+++ b/test/unit/publicapi.test.js
@@ -107,7 +107,7 @@ suite('Public API', function() {
test('.html() trivial case', function() {
mq.latex('x+y');
- assert.equal(mq.html(), 'x+y');
+ assert.equal(mq.html(), 'xy');
});
test('.text() with incomplete commands', function() {
@@ -164,6 +164,44 @@ suite('Public API', function() {
mq.empty();
assert.equal(mq.latex(), '');
});
+ test('ARIA labels', function() {
+ mq.setAriaLabel('ARIA label');
+ mq.setAriaPostLabel('ARIA post-label');
+ assert.equal(mq.getAriaLabel(), 'ARIA label');
+ assert.equal(mq.getAriaPostLabel(), 'ARIA post-label');
+ mq.setAriaLabel('');
+ mq.setAriaPostLabel('');
+ assert.equal(mq.getAriaLabel(), 'Math Input');
+ assert.equal(mq.getAriaPostLabel(), '');
+ });
+
+ test('.mathspeak()', function() {
+ function assertMathSpeakEqual(a, b) {
+ assert.equal(normalize(a), normalize(b));
+ function normalize(str) {
+ return str.replace(/\d(?!\d)/g, '$& ').split(/[ ,]+/).join(' ').trim();
+ }
+ }
+
+ mq.latex('123.456');
+ assertMathSpeakEqual(mq.mathspeak(), '123.4 5 6');
+
+ mq.latex('\\frac{d}{dx}\\sqrt{x}');
+ assertMathSpeakEqual(mq.mathspeak(), 'StartFraction "d" Over "d" "x" EndFraction StartRoot "x" EndRoot');
+
+ mq.latex('1+2-3\\cdot\\frac{5}{6^7}=\\left(8+9\\right)');
+ assertMathSpeakEqual(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 to the 7th power EndFraction equals left parenthesis 8 plus 9 right parenthesis');
+
+ // Example 13 from http://www.gh-mathspeak.com/examples/quick-tutorial/index.php?verbosity=v&explicitness=2&interp=0
+ mq.latex('d=\\sqrt{ \\left( x_2 - x_1 \\right)^2 - \\left( y_2 - y_1 \\right)^2 }');
+ assertMathSpeakEqual(mq.mathspeak(), '"d" equals StartRoot left parenthesis "x" Subscript 2 Baseline minus "x" Subscript 1 Baseline right parenthesis squared minus left parenthesis "y" Subscript 2 Baseline minus "y" Subscript 1 Baseline right parenthesis squared EndRoot');
+
+ mq.latex('').typedText('\\langle').keystroke('Spacebar').typedText('u,v'); // .latex() doesn't work yet for angle brackets :(
+ assertMathSpeakEqual(mq.mathspeak(), 'left angle-bracket "u" "v" right angle-bracket');
+
+ mq.latex('\\left| x \\right| + \\left( y \\right|');
+ assertMathSpeakEqual(mq.mathspeak(), 'StartAbsoluteValue "x" EndAbsoluteValue plus left parenthesis "y" right pipe');
+ });
});
test('edit handler interface versioning', function() {
@@ -311,7 +349,7 @@ suite('Public API', function() {
});
}
});
-
+
suite('edit handler', function() {
test('fires when closing a bracket expression', function() {
var count = 0;
@@ -343,11 +381,11 @@ suite('Public API', function() {
mq.cmd('^');
assert.equal(mq.latex(), 'xy^{ }');
mq.cmd('2');
- assert.equal(mq.latex(), 'xy^2');
+ assert.equal(mq.latex(), 'xy^{2}');
mq.keystroke('Right Shift-Left Shift-Left Shift-Left').cmd('\\sqrt');
- assert.equal(mq.latex(), '\\sqrt{xy^2}');
+ assert.equal(mq.latex(), '\\sqrt{xy^{2}}');
mq.typedText('*2**');
- assert.equal(mq.latex(), '\\sqrt{xy^2\\cdot2\\cdot\\cdot}');
+ assert.equal(mq.latex(), '\\sqrt{xy^{2}\\cdot2\\cdot\\cdot}');
});
test('backslash commands are passed their name', function() {
@@ -510,7 +548,7 @@ suite('Public API', function() {
+ 'thegraphicalelementsofadocumentorvisualpresentation.');
});
test('actual LaTeX', function() {
- assertPaste('a_nx^n+a_{n+1}x^{n+1}');
+ assertPaste('a_{n}x^{n}+a_{n+1}x^{n+1}');
assertPaste('\\frac{1}{2\\sqrt{x}}');
});
test('\\text{...}', function() {
@@ -521,7 +559,7 @@ suite('Public API', function() {
test('selection', function(done) {
mq.latex('x^2').select();
setTimeout(function() {
- assert.equal(textarea.val(), 'x^2');
+ assert.equal(textarea.val(), 'x^{2}');
done();
});
});
@@ -557,13 +595,13 @@ suite('Public API', function() {
});
// TODO: braces (currently broken)
test('actual math LaTeX wrapped in dollar signs', function() {
- assertPaste('$a_nx^n+a_{n+1}x^{n+1}$', 'a_nx^n+a_{n+1}x^{n+1}');
+ assertPaste('$a_nx^n+a_{n+1}x^{n+1}$', 'a_{n}x^{n}+a_{n+1}x^{n+1}');
assertPaste('$\\frac{1}{2\\sqrt{x}}$', '\\frac{1}{2\\sqrt{x}}');
});
test('selection', function(done) {
mq.latex('x^2').select();
setTimeout(function() {
- assert.equal(textarea.val(), '$x^2$');
+ assert.equal(textarea.val(), '$x^{2}$');
done();
});
});
@@ -628,25 +666,25 @@ suite('Public API', function() {
test('supsub', function() {
mq.latex('x_a+y^b+z_a^b+w');
- assert.equal(mq.latex(), 'x_a+y^b+z_a^b+w');
+ assert.equal(mq.latex(), 'x_{a}+y^{b}+z_{a}^{b}+w');
mq.moveToLeftEnd().typedText('1');
- assert.equal(mq.latex(), '1x_a+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{a}+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('2');
- assert.equal(mq.latex(), '1x_{2a}+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('3');
- assert.equal(mq.latex(), '1x_{2a}3+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right Right').typedText('4');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('5');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{b}+w');
mq.keystroke('Right Right Right').typedText('6');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^{b}+w');
mq.keystroke('Right Right').typedText('7');
assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^{7b}+w');
@@ -708,28 +746,28 @@ suite('Public API', function() {
test('supsub', function() {
mq.latex('x_a+y^b+z_a^b+w');
- assert.equal(mq.latex(), 'x_a+y^b+z_a^b+w');
+ assert.equal(mq.latex(), 'x_{a}+y^{b}+z_{a}^{b}+w');
mq.moveToLeftEnd().typedText('1');
- assert.equal(mq.latex(), '1x_a+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{a}+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('2');
- assert.equal(mq.latex(), '1x_{2a}+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('3');
- assert.equal(mq.latex(), '1x_{2a}3+y^b+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{b}+z_{a}^{b}+w');
mq.keystroke('Right Right Right').typedText('4');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_{a}^{b}+w');
mq.keystroke('Right Right').typedText('5');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^b+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{b}+w');
mq.keystroke('Right Right Right').typedText('6');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^{6b}+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{6b}+w');
mq.keystroke('Right Right').typedText('7');
- assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^{6b}7+w');
+ assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{6b}7+w');
});
test('nthroot', function() {
@@ -760,7 +798,7 @@ suite('Public API', function() {
assert.equal(mq.latex(), '\\sum_{ }^{ }');
mq.cmd('n');
- assert.equal(mq.latex(), '\\sum_n^{ }', 'cursor in lower limit');
+ assert.equal(mq.latex(), '\\sum_{n}^{ }', 'cursor in lower limit');
});
test('sum starts with `n=`', function() {
var mq = MQ.MathField($('').appendTo('#mock')[0], {
@@ -784,7 +822,7 @@ suite('Public API', function() {
assert.equal(mq.latex(), '\\int_{ }^{ }');
mq.cmd('0');
- assert.equal(mq.latex(), '\\int_0^{ }', 'cursor in the from block');
+ assert.equal(mq.latex(), '\\int_{0}^{ }', 'cursor in the from block');
});
});
@@ -802,6 +840,46 @@ suite('Public API', function() {
});
});
+ suite('overrideKeystroke', function() {
+ test('can intercept key events', function() {
+ var mq = MQ.MathField($('').appendTo('#mock')[0], {
+ overrideKeystroke: function (_key, evt) {
+ key = _key;
+ return mq.keystroke.apply(mq, arguments);
+ }
+ });
+ var key;
+
+ $(mq.el()).find('textarea').trigger({ type: 'keydown', which: '37' });
+ assert.equal(key, 'Left');
+ });
+ test('cut is async', function(done) {
+ var mq = MQ.MathField($('').appendTo('#mock')[0], {
+ onCut: function() {
+ count += 1;
+ }
+ });
+ var count = 0;
+
+ mq.latex('a=2');
+ mq.select();
+
+ $(mq.el()).find('textarea').trigger('cut');
+ assert.equal(count, 0);
+
+ $(mq.el()).find('textarea').trigger('input');
+ assert.equal(count, 0);
+
+ $(mq.el()).find('textarea').trigger('keyup');
+ assert.equal(count, 0);
+
+ setTimeout(function () {
+ assert.equal(count, 1);
+ done();
+ }, 100)
+ });
+ });
+
suite('substituteKeyboardEvents', function() {
test('can intercept key events', function() {
var mq = MQ.MathField($('').appendTo('#mock')[0], {
diff --git a/test/unit/resetCursorOnBlur.test.js b/test/unit/resetCursorOnBlur.test.js
new file mode 100644
index 000000000..c1309694b
--- /dev/null
+++ b/test/unit/resetCursorOnBlur.test.js
@@ -0,0 +1,48 @@
+suite('resetCursorOnBlur', function() {
+ var $el;
+ setup(function() {
+ $el = $('');
+ });
+
+ test('remembers cursor position by default', function(done) {
+ var mq = MQ.MathField($el.appendTo('#mock')[0]);
+
+ mq.latex('a=2');
+ mq.focus()
+ mq.keystroke('Left');
+ mq.typedText('1');
+ assert.equal('a=12', mq.latex());
+
+ mq.blur();
+ setTimeout(function () {
+ mq.focus();
+ setTimeout(function () {
+ mq.typedText('3');
+ assert.equal('a=132', mq.latex());
+ done();
+ }, 1);
+ }, 1);
+ });
+
+ test('forgets cursor position with resetCursorOnBlur option', function(done) {
+ var mq = MQ.MathField($el.appendTo('#mock')[0], {
+ resetCursorOnBlur: true
+ });
+
+ mq.latex('a=2');
+ mq.focus()
+ mq.keystroke('Left');
+ mq.typedText('1');
+ assert.equal('a=12', mq.latex());
+
+ mq.blur();
+ setTimeout(function () {
+ mq.focus();
+ setTimeout(function () {
+ mq.typedText('3');
+ assert.equal('a=123', mq.latex());
+ done();
+ }, 1);
+ }, 1);
+ });
+});
diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js
index 5b3b1f3f1..198464f71 100644
--- a/test/unit/saneKeyboardEvents.test.js
+++ b/test/unit/saneKeyboardEvents.test.js
@@ -148,11 +148,11 @@ suite('saneKeyboardEvents', function() {
// IE < 9 doesn't support selection{Start,End}
if (supportsSelectionAPI()) {
- assert.equal(el[0].selectionStart, 0, 'it\'s selected from the start');
- assert.equal(el[0].selectionEnd, 6, 'it\'s selected to the end');
+ assert.equal(el[0].selectionStart, 0, 'it is not selected at the start');
+ assert.equal(el[0].selectionEnd, 0, 'it is not selected at the end');
}
- assert.equal(el.val(), 'foobar', 'it still has content');
+ assert.equal(el.val(), '', 'it has no content');
});
test('blur then empty selection', function() {
diff --git a/test/unit/scrollHoriz.test.js b/test/unit/scrollHoriz.test.js
new file mode 100644
index 000000000..9ee6ed7e9
--- /dev/null
+++ b/test/unit/scrollHoriz.test.js
@@ -0,0 +1,32 @@
+suite('scrollHoriz', function() {
+ var mq;
+ var $el;
+ setup(function() {
+ $el = $('');
+ mq = MQ.MathField($el.appendTo('#mock')[0]);
+ });
+
+ test('classes added as expected', function(done) {
+ mq.latex('beginning ------------ end');
+ var $root = $el.find('.mq-root-block')
+ assert.ok($root.is(':not(.mq-editing-overflow-left)'), 'no left overflow class');
+ assert.ok($root.is(':not(.mq-editing-overflow-right)'), 'no right overflow class');
+ assert.equal($root.scrollLeft(), 0, 'unscrolled');
+ mq.focus()
+ assert.ok($root.is(':not(.mq-editing-overflow-left)'), 'no left overflow class');
+ assert.ok($root.is('.mq-editing-overflow-right'), 'has right overflow class');
+ mq.keystroke('Shift-Right');
+ setTimeout(function() {
+ assert.ok($root.is('.mq-editing-overflow-left'), 'has left overflow class');
+ assert.ok($root.is(':not(.mq-editing-overflow-right)'), 'no right overflow class');
+ assert.ok($root.scrollLeft() > 0, 'now scrolled');
+ mq.blur()
+ setTimeout(function() {
+ assert.ok($root.is(':not(.mq-editing-overflow-left)'), 'left overflow class removed');
+ assert.ok($root.is(':not(.mq-editing-overflow-right)'), 'no right overflow class');
+ assert.equal($root.scrollLeft(), 0, 'scrolled back left');
+ done();
+ }, 200);
+ }, 200);
+ });
+});
\ No newline at end of file
diff --git a/test/unit/text.test.js b/test/unit/text.test.js
index 120aeb579..17fc99b82 100644
--- a/test/unit/text.test.js
+++ b/test/unit/text.test.js
@@ -186,4 +186,31 @@ suite('text', function() {
});
});
+
+
+ test('HTML for subclassed text blocks', function() {
+ var block = fromLatex('\\text{abc}');
+ var _id = block.html().match(/mathquill-command-id=([0-9]+)/)[1];
+ function id () {
+ _id = parseInt(_id) + 3;
+ return _id;
+ }
+
+ block = fromLatex('\\text{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\textit{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\textbf{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\textsf{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\texttt{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\textsc{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\uppercase{abc}');
+ assert.equal(block.html(), 'abc');
+ block = fromLatex('\\lowercase{abc}');
+ assert.equal(block.html(), 'abc');
+ });
});
diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js
index f64724fc3..b5148128e 100644
--- a/test/unit/typing.test.js
+++ b/test/unit/typing.test.js
@@ -18,6 +18,13 @@ suite('typing with auto-replaces', function() {
assert.equal(mq.latex(), latex);
}
+ function assertMathspeak(mathspeak) {
+ assert.equal(normalize(mq.mathspeak()), normalize(mathspeak));
+ function normalize(str) {
+ return str.replace(/\d(?!\d)/g, '$& ').split(/[ ,]+/).join(' ').trim();
+ }
+ }
+
suite('LiveFraction', function() {
test('full MathQuill', function() {
mq.typedText('1/2').keystroke('Tab').typedText('+sinx/');
@@ -35,6 +42,15 @@ suite('typing with auto-replaces', function() {
});
});
+ suite('EquivalentMinus', function() {
+ test('different minus symbols', function() {
+ //these 4 are all different characters (!!)
+ mq.typedText('−—–-');
+ //these 4 are all the same character
+ assertLatex('----');
+ });
+ });
+
suite('LatexCommandInput', function() {
test('basic', function() {
mq.typedText('\\sqrt-x');
@@ -53,7 +69,7 @@ suite('typing with auto-replaces', function() {
test('auto-operator names', function() {
mq.typedText('\\sin^2');
- assertLatex('\\sin^2');
+ assertLatex('\\sin^{2}');
});
test('nonexistent LaTeX command', function() {
@@ -77,6 +93,185 @@ suite('typing with auto-replaces', function() {
});
});
+ suite('MathspeakShorthand', function() {
+ test('fractions', function() {
+ // Testing singular numeric fractions from 1/2 to 1/10
+ mq.latex('\\frac{1}{2}');
+ assertMathspeak('1 half');
+ mq.latex('\\frac{1}{3}');
+ assertMathspeak('1 third');
+ mq.latex('\\frac{1}{4}');
+ assertMathspeak('1 quarter');
+ mq.latex('\\frac{1}{5}');
+ assertMathspeak('1 fifth');
+ mq.latex('\\frac{1}{6}');
+ assertMathspeak('1 sixth');
+ mq.latex('\\frac{1}{7}');
+ assertMathspeak('1 seventh');
+ mq.latex('\\frac{1}{8}');
+ assertMathspeak('1 eighth');
+ mq.latex('\\frac{1}{9}');
+ assertMathspeak('1 ninth');
+ mq.latex('\\frac{1}{10}');
+ assertMathspeak('StartFraction, 1 Over 10, EndFraction');
+
+ // Testing plural numeric fractions from 31/2 to 31/10
+ mq.latex('\\frac{31}{2}');
+ assertMathspeak('31 halves');
+ mq.latex('\\frac{31}{3}');
+ assertMathspeak('31 thirds');
+ mq.latex('\\frac{31}{4}');
+ assertMathspeak('31 quarters');
+ mq.latex('\\frac{31}{5}');
+ assertMathspeak('31 fifths');
+ mq.latex('\\frac{31}{6}');
+ assertMathspeak('31 sixths');
+ mq.latex('\\frac{31}{7}');
+ assertMathspeak('31 sevenths');
+ mq.latex('\\frac{31}{8}');
+ assertMathspeak('31 eighths');
+ mq.latex('\\frac{31}{9}');
+ assertMathspeak('31 ninths');
+ mq.latex('\\frac{31}{10}');
+ assertMathspeak('StartFraction, 31 Over 10, EndFraction');
+
+ // Fractions with negative numerators should be shortened
+ mq.latex('\\frac{-1}{2}');
+ assertMathspeak('negative 1 half');
+ mq.latex('\\frac{-3}{2}');
+ assertMathspeak('negative 3 halves');
+ mq.latex('-\\frac{3}{4}');
+ assertMathspeak('negative 3 quarters');
+
+ // Fractions with negative denominators should not be shortened
+ mq.latex('\\frac{1}{-2}');
+ assertMathspeak('StartFraction, 1 Over negative 2, EndFraction');
+
+ // Traditional fractions should be spoken if either numerator or denominator are not numeric
+ mq.latex('\\frac{x}{2}');
+ assertMathspeak('StartFraction, "x" Over 2, EndFraction');
+ mq.latex('\\frac{2}{x}');
+ assertMathspeak('StartFraction, 2 Over "x", EndFraction');
+
+ // Traditional fractions should be spoken if either numerator or denominator are not whole numbers
+ mq.latex('\\frac{1.2}{2}');
+ assertMathspeak('StartFraction, 1.2 Over 2, EndFraction');
+ mq.latex('\\frac{4}{2.3}');
+ assertMathspeak('StartFraction, 4 Over 2.3, EndFraction');
+
+ // A whole number followed by a shortened fraction should include the word "and", and other combinations should not.
+ mq.latex('3\\frac{3}{8}');
+ assertMathspeak('3 and 3 eighths');
+ mq.latex('3\\ \\frac{3}{8}');
+ assertMathspeak('3 and 3 eighths');
+ mq.latex('3\\ \\ \\ \\ \\ \\frac{3}{8}');
+ assertMathspeak('3 and 3 eighths');
+ mq.latex('3.1\\frac{3}{8}');
+ assertMathspeak('3.1 3 eighths');
+ mq.latex('3.1\\ \\frac{3}{8}');
+ assertMathspeak('3.1 3 eighths');
+ mq.latex('3.1\\ \\ \\ \\ \\frac{3}{8}');
+ assertMathspeak('3.1 3 eighths');
+ mq.latex('\\ \\frac{1}{2}');
+ assertMathspeak('1 half');
+ mq.latex('3\\frac{3}{x}');
+ assertMathspeak('3 StartFraction, 3 Over "x", EndFraction');
+ mq.latex('x\\frac{3}{8}');
+ assertMathspeak('"x" 3 eighths');
+ });
+
+ test('exponents', function() {
+ // Test simple superscripts and suffix rules
+ mq.latex('x^{0}');
+ assertMathspeak('"x" to the 0 power');
+ mq.latex('x^{1}');
+ assertMathspeak('"x" to the 1st power');
+ mq.latex('x^{2}');
+ assertMathspeak('"x" squared');
+ mq.latex('x^{3}');
+ assertMathspeak('"x" cubed');
+ mq.latex('x^{4}');
+ assertMathspeak('"x" to the 4th power');
+ mq.latex('x^{5}');
+ assertMathspeak('"x" to the 5th power');
+ mq.latex('x^{6}');
+ assertMathspeak('"x" to the 6th power');
+ mq.latex('x^{7}');
+ assertMathspeak('"x" to the 7th power');
+ mq.latex('x^{8}');
+ assertMathspeak('"x" to the 8th power');
+ mq.latex('x^{9}');
+ assertMathspeak('"x" to the 9th power');
+ mq.latex('x^{10}');
+ assertMathspeak('"x" to the 10th power');
+ mq.latex('x^{11}');
+ assertMathspeak('"x" to the 11th power');
+ mq.latex('x^{12}');
+ assertMathspeak('"x" to the 12th power');
+ mq.latex('x^{13}');
+ assertMathspeak('"x" to the 13th power');
+ mq.latex('x^{14}');
+ assertMathspeak('"x" to the 14th power');
+ mq.latex('x^{21}');
+ assertMathspeak('"x" to the 21st power');
+ mq.latex('x^{22}');
+ assertMathspeak('"x" to the 22nd power');
+ mq.latex('x^{23}');
+ assertMathspeak('"x" to the 23rd power');
+ mq.latex('x^{999}');
+ assertMathspeak('"x" to the 999th power');
+ // Values greater than 1000 have no suffix
+ mq.latex('x^{1000}');
+ assertMathspeak('"x" to the 1000 power');
+ mq.latex('x^{10000000000}');
+ assertMathspeak('"x" to the 10000000000 power');
+
+ // Ensure negative exponents are shortened
+ mq.latex('10^{-5}');
+ assertMathspeak('10 to the negative 5th power');
+ mq.latex('x^{-5}');
+ assertMathspeak('"x" to the negative 5th power');
+
+ // Superscripts that are not strictly integers should continue to be spoken in longer form
+ mq.latex('x^{5.3}');
+ assertMathspeak('"x" Superscript, 5.3, Baseline');
+ mq.latex('x^{y}');
+ assertMathspeak('"x" Superscript, "y", Baseline');
+ mq.latex('x^{y^{2}}');
+ assertMathspeak('"x" Superscript, "y" squared, Baseline');
+ });
+
+ test('plus and minus differentiation', function() {
+ // Distinguish between positive vs plus and negative vs. minus
+ mq.latex('-25-25');
+ assertMathspeak('negative 25 minus 25');
+ mq.latex('+25+25');
+ assertMathspeak('positive 25 plus 25');
+ });
+
+ test('styled text', function() {
+ // Test that text-related elements include sensible mathspeak.
+ // Letters in a non-wrapped block should be split apart (interpreted as variables):
+ mq.latex('this is a test');
+ assertMathspeak('"t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t"');
+ // Contents of a text block should be returned exactly as entered with no start and end delimiters spoken:
+ mq.latex('\\text{this is a test}');
+ assertMathspeak('this is a test');
+ // Specifically for mathrm, don't split characters and also don't speak delimiters.
+ // note content is still interpreted as LaTeX, so we use \ to separate words:
+ mq.latex('\\mathrm{this\\ is\\ a\\ test}');
+ assertMathspeak('this is a test');
+ // Any other font command should be spoken "normally"--
+ // letters are split and delimiters are announced for remaining commands:
+ mq.latex('\\mathit{this\\ is\\ a\\ test}');
+ assertMathspeak('StartItalic Font "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" EndItalic Font');
+ mq.latex('\\textcolor{red}{this\\ is\\ a\\ test}');
+ assertMathspeak('Start red "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" End red');
+ mq.latex('\\class{abc}{this\\ is\\ a\\ test}');
+ assertMathspeak('Start abc class "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" End abc class');
+ });
+ });
+
suite('auto-expanding parens', function() {
suite('simple', function() {
test('empty parens ()', function() {
@@ -849,11 +1044,98 @@ suite('typing with auto-replaces', function() {
});
});
+ suite('autoParenthesizedFunctions', function() {
+ setup(function() {
+ mq.config({
+ autoParenthesizedFunctions: 'sin cos tan ln',
+ autoOperatorNames: 'sin ln',
+ autoCommands: 'sum int'
+ });
+ });
+
+ test('individual commands', function(){
+ //autoParenthesized and also operatored
+ mq.typedText('sin')
+ assertLatex('\\sin\\left(\\right)');
+ mq.latex('')
+ //not parenthesized
+ mq.typedText('cot')
+ assertLatex('cot');
+ mq.latex('')
+ //we don't autoparenthesize non-autocommands
+ mq.typedText('tan')
+ assertLatex('tan');
+ mq.latex('')
+ //doesn't parenthesize when the middle is completed
+ mq.typedText('tn')
+ mq.keystroke('Left')
+ mq.typedText('a')
+ assertLatex('tan');
+
+ mq.latex('')
+ //doesn't parenthesize when the middle is completed, but does autoFn
+ mq.typedText('sn')
+ mq.keystroke('Left')
+ mq.typedText('i')
+ assertLatex('\\sin');
+ });
+
+ test('does not double parenthesize if parenthesized', function () {
+ //autoParenthesized and also operatored
+ mq.typedText('sin')
+ assertLatex('\\sin\\left(\\right)');
+ mq.keystroke('Left')
+ mq.keystroke('Backspace')
+ mq.typedText('n')
+ assertLatex('\\sin\\left(\\right)');
+ })
+
+ test('works in \\sum', function () {
+ mq.typedText('sum')
+ assertLatex('\\sum_{ }^{ }');
+ mq.typedText('sin')
+ assertLatex('\\sum_{\\sin\\left(\\right)}^{ }');
+ })
+
+ test('works in \\int', function () {
+ mq.typedText('int')
+ assertLatex('\\int_{ }^{ }');
+ mq.typedText('sin')
+ assertLatex('\\int_{\\sin\\left(\\right)}^{ }');
+ })
+
+ test('does not work in simple subscripts', function () {
+ mq.typedText('x_')
+ assertLatex('x_{ }');
+ mq.typedText('sin')
+ assertLatex('x_{sin}');
+ })
+
+ test('does not work in simple subscripts when pasting', function () {
+ $(mq.el()).find('textarea').trigger('paste').val('x_{sin}').trigger('input');
+ assertLatex('x_{sin}');
+ })
+ });
+
+ suite('typingSlashCreatesNewFraction', function() {
+ setup(function() {
+ mq.config({
+ typingSlashCreatesNewFraction: true
+ });
+ });
+
+ test('typing slash creates new fraction', function(){
+ //autoParenthesized and also operatored
+ mq.typedText('1/')
+ assertLatex('1\\frac{ }{ }');
+ });
+ });
+
suite('autoCommands', function() {
setup(function() {
mq.config({
autoOperatorNames: 'sin pp',
- autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot'
+ autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot cbrt percent'
});
});
@@ -876,6 +1158,7 @@ suite('typing with auto-replaces', function() {
mq.typedText('nthroot');
mq.typedText('n').keystroke('Right').typedText('100').keystroke('Right');
assertLatex('\\sqrt[n]{100}');
+ assertMathspeak('Root Index "n" Start Root 100 End Root');
mq.keystroke('Ctrl-Backspace');
mq.typedText('pi');
@@ -897,6 +1180,16 @@ suite('typing with auto-replaces', function() {
mq.typedText('Gamma');
assertLatex('\\Gamma');
mq.keystroke('Backspace');
+
+ mq.typedText('percent');
+ assertLatex('\\%\\operatorname{of}');
+ mq.keystroke('Backspace');
+
+ mq.typedText('cbrt');
+ assertLatex('\\sqrt[3]{}');
+ assertMathspeak('Start Cube Root End Cube Root');
+ mq.typedText('pi');
+ assertLatex('\\sqrt[3]{\\pi}');
});
test('sequences of auto-commands and other assorted characters', function() {
@@ -975,55 +1268,134 @@ suite('typing with auto-replaces', function() {
// but also that when you backspace you get the right state such that
// you can either type = again to get the non-strict inequality again,
// or backspace again and it'll delete correctly.
- function assertFullyFunctioningInequality(nonStrict, strict) {
+ function assertFullyFunctioningInequality(nonStrict, strict, nonStrictMathspeak, strictMathspeak) {
assertLatex(nonStrict);
+ assertMathspeak(nonStrictMathspeak);
mq.keystroke('Backspace');
assertLatex(strict);
+ assertMathspeak(strictMathspeak);
mq.typedText('=');
assertLatex(nonStrict);
+ assertMathspeak(nonStrictMathspeak);
mq.keystroke('Backspace');
assertLatex(strict);
+ assertMathspeak(strictMathspeak);
mq.keystroke('Backspace');
assertLatex('');
+ assertMathspeak('');
}
test('typing and backspacing <= and >=', function() {
mq.typedText('<');
assertLatex('<');
+ assertMathspeak('less than');
mq.typedText('=');
- assertFullyFunctioningInequality('\\le', '<');
+ assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than');
mq.typedText('>');
assertLatex('>');
mq.typedText('=');
- assertFullyFunctioningInequality('\\ge', '>');
+ assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than');
mq.typedText('<<>>==>><<==');
assertLatex('<<>\\ge=>><\\le=');
+ assertMathspeak('less than less than greater than greater than or equal to equals greater than greater than less than less than or equal to equals');
});
test('typing ≤ and ≥ chars directly', function() {
mq.typedText('≤');
- assertFullyFunctioningInequality('\\le', '<');
+ assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than');
mq.typedText('≥');
- assertFullyFunctioningInequality('\\ge', '>');
+ assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than');
+ });
+
+ test('typing and backspacing \\to', function() {
+ mq.typedText('-');
+ assertLatex('-');
+ assertMathspeak('negative');
+ mq.typedText('>');
+ assertLatex('\\to');
+ assertMathspeak('to');
+ mq.typedText('-');
+ assertLatex('\\to-');
+ assertMathspeak('to negative');
+ mq.typedText('>');
+ assertLatex('\\to\\to');
+ assertMathspeak('to to');
+ mq.keystroke('Backspace');
+ assertLatex('\\to-');
+ assertMathspeak('to negative');
+ mq.keystroke('Backspace');
+ assertLatex('\\to');
+ assertMathspeak('to');
+ mq.keystroke('Backspace');
+ assertLatex('-');
+ assertMathspeak('negative');
+ mq.keystroke('Backspace');
+ mq.typedText('a->b');
+ assertLatex('a\\to b');
+ assertMathspeak('"a" to "b"');
+ mq.latex('');
+ mq.typedText('a→b');
+ assertLatex('a\\to b');
+ assertMathspeak('"a" to "b"');
+ });
+
+ test('typing and backspacing ~', function() {
+ mq.typedText('~');
+ assertLatex('\\sim');
+ assertMathspeak('tilde');
+ mq.typedText('~');
+ assertLatex('\\approx');
+ assertMathspeak('approximately equal');
+ mq.typedText('~');
+ assertLatex('\\approx\\sim');
+ assertMathspeak('approximately equal tilde');
+ mq.typedText('~');
+ assertLatex('\\approx\\approx');
+ assertMathspeak('approximately equal approximately equal');
+ mq.keystroke('Backspace');
+ assertLatex('\\approx\\sim');
+ assertMathspeak('approximately equal tilde');
+ mq.keystroke('Backspace');
+ assertLatex('\\approx');
+ assertMathspeak('approximately equal');
+ mq.keystroke('Backspace');
+ assertLatex('\\sim');
+ assertMathspeak('tilde');
+ mq.keystroke('Backspace');
+ mq.typedText('a~b');
+ assertLatex('a\\sim b');
+ assertMathspeak('"a" tilde "b"');
+ mq.keystroke('Backspace');
+ mq.typedText('~b');
+ assertLatex('a\\approx b');
+ assertMathspeak('"a" approximately equal "b"');
+ });
+ test('typing ≈ char directly', function() {
+ mq.typedText('≈');
+ assertLatex('\\approx');
+ assertMathspeak('approximately equal');
+ mq.keystroke('Backspace');
+ assertLatex('\\sim');
+ assertMathspeak('tilde');
});
suite('rendered from LaTeX', function() {
test('control sequences', function() {
mq.latex('\\le');
- assertFullyFunctioningInequality('\\le', '<');
+ assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than');
mq.latex('\\ge');
- assertFullyFunctioningInequality('\\ge', '>');
+ assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than');
});
test('≤ and ≥ chars', function() {
mq.latex('≤');
- assertFullyFunctioningInequality('\\le', '<');
+ assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than');
mq.latex('≥');
- assertFullyFunctioningInequality('\\ge', '>');
+ assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than');
});
});
});
@@ -1060,22 +1432,22 @@ suite('typing with auto-replaces', function() {
});
test('supSubsRequireOperand', function() {
assert.equal(mq.typedText('^').latex(), '^{ }');
- assert.equal(mq.typedText('2').latex(), '^2');
+ assert.equal(mq.typedText('2').latex(), '^{2}');
assert.equal(mq.typedText('n').latex(), '^{2n}');
mq.latex('');
assert.equal(mq.typedText('x').latex(), 'x');
assert.equal(mq.typedText('^').latex(), 'x^{ }');
- assert.equal(mq.typedText('2').latex(), 'x^2');
+ assert.equal(mq.typedText('2').latex(), 'x^{2}');
assert.equal(mq.typedText('n').latex(), 'x^{2n}');
mq.latex('');
assert.equal(mq.typedText('x').latex(), 'x');
assert.equal(mq.typedText('^').latex(), 'x^{ }');
assert.equal(mq.typedText('^').latex(), 'x^{^{ }}');
- assert.equal(mq.typedText('2').latex(), 'x^{^2}');
+ assert.equal(mq.typedText('2').latex(), 'x^{^{2}}');
assert.equal(mq.typedText('n').latex(), 'x^{^{2n}}');
mq.latex('');
assert.equal(mq.typedText('2').latex(), '2');
- assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2');
+ assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^{2}');
mq.latex('');
MQ.config({ supSubsRequireOperand: true });
@@ -1086,17 +1458,17 @@ suite('typing with auto-replaces', function() {
mq.latex('');
assert.equal(mq.typedText('x').latex(), 'x');
assert.equal(mq.typedText('^').latex(), 'x^{ }');
- assert.equal(mq.typedText('2').latex(), 'x^2');
+ assert.equal(mq.typedText('2').latex(), 'x^{2}');
assert.equal(mq.typedText('n').latex(), 'x^{2n}');
mq.latex('');
assert.equal(mq.typedText('x').latex(), 'x');
assert.equal(mq.typedText('^').latex(), 'x^{ }');
assert.equal(mq.typedText('^').latex(), 'x^{ }');
- assert.equal(mq.typedText('2').latex(), 'x^2');
+ assert.equal(mq.typedText('2').latex(), 'x^{2}');
assert.equal(mq.typedText('n').latex(), 'x^{2n}');
mq.latex('');
assert.equal(mq.typedText('2').latex(), '2');
- assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2');
+ assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^{2}');
});
});
@@ -1120,4 +1492,31 @@ suite('typing with auto-replaces', function() {
assertLatex('\\times');
});
});
+
+ suite('typingPercentWritesPercentOf', function () {
+ test('typingSlashWritesDivisionSymbol', function () {
+ mq.typedText('%');
+ assertLatex('\\%');
+ mq.keystroke('Backspace');
+
+ mq.config({ typingPercentWritesPercentOf: true });
+
+ mq.typedText('%');
+ assertLatex('\\%\\operatorname{of}');
+ mq.keystroke('Backspace');
+ assertLatex('');
+ });
+
+ test('percentof round trips correctly through serializing and parsing', function () {
+ mq.latex('\\%\\operatorname{of}');
+ assertLatex('\\%\\operatorname{of}');
+ });
+
+ test('overline renders as expected', function() {
+ mq.latex('0.3\\overline{5}');
+ assertLatex('0.3\\overline{5}');
+ assertMathspeak('0 .3 StartOverline 5 EndOverline');
+ });
+ });
});
+
diff --git a/test/unit/updown.test.js b/test/unit/updown.test.js
index 783036a0b..52fe6ce08 100644
--- a/test/unit/updown.test.js
+++ b/test/unit/updown.test.js
@@ -171,6 +171,27 @@ suite('up/down', function() {
assert.equal(cursor[L], sub, 'cursor up up from subscript fraction denominator that is at right end goes after subscript');
});
+ test('integral in exponent', function () {
+ controller.renderLatexMath('2^{\\int_0^1}');
+ var exp = rootBlock.ends[R],
+ expBlock = exp.ends[L];
+
+ mq.keystroke('Up');
+ mq.keystroke('Up');
+ assert.equal(cursor.parent.latex(), '1', 'cursor up goes to upper limit');
+ var upperRect = cursor.parent.jQ[0].getBoundingClientRect();
+
+ mq.keystroke('Down');
+ assert.equal(cursor.parent.latex(), '0', 'cursor down goes to lower limit');
+ var lowerRect = cursor.parent.jQ[0].getBoundingClientRect();
+
+ mq.keystroke('Up');
+ assert.equal(cursor.parent.latex(), '1', 'cursor up goes to upper limit');
+
+ var upperAboveLower = upperRect.bottom < lowerRect.top;
+ assert.equal(upperAboveLower, true, 'cursor actually moves downward for lower limit');
+ });
+
test('\\MathQuillMathField{} in a fraction', function() {
var outer = MQ.StaticMath(
$('\\frac{\\MathQuillMathField{n}}{2}').appendTo('#mock')[0]
diff --git a/test/visual.html b/test/visual.html
index b297f5a09..6d22aa34b 100644
--- a/test/visual.html
+++ b/test/visual.html
@@ -2,6 +2,7 @@
+
substituteTextarea
substituteKeyboardEvents
+overrideKeystroke and overrideTypedText
-Text mode
+
3
+
Should be able to prevent cut, typing, and pasting in this field: 1+2+3
+ +Should wrap anything you type in '<>': 1+2+3
+ ++