diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..61e9748e8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +52972ff01abab19a5861ac2364efc7471c336ae7 +d0530dbba74c87bbeb02433a490c49c28c1deb9d +8546f1fd2784ef579ef951c6d3ec4c20b1b156c5 +353fd24b7d01420d405377985dc5a43ea7b69498 \ No newline at end of file diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 000000000..7994bc363 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[blame] + ignoreRevsFile = .git-blame-ignore-revs \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..1df75316f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,8 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# run prettier on staged files +npx lint-staged + +# typecheck +make lint \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..900872f5f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +*.min.js +*.min.css +build +src/intro.js +src/outro.js +test/support/jquery-1.5.2.js \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..dc2fb828f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/Makefile b/Makefile index 38851776c..f178a9d94 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile # -*- Build tasks -*- # -.PHONY: all basic dev js uglify css font clean +.PHONY: all basic dev js uglify css font clean setup-gitconfig prettify-all all: font css uglify basic: $(UGLY_BASIC_JS) $(BASIC_CSS) unminified_basic: $(BASIC_JS) $(BASIC_CSS) @@ -114,6 +114,14 @@ css: $(BUILD_CSS) font: $(FONT_TARGET) clean: rm -rf $(BUILD_DIR) +# This adds an entry to your local .git/config file that looks like this: +# [include] +# path = ../.gitconfig +# that tells git to include the additional configuration specified inside the .gitconfig file that's checked in here. +setup-gitconfig: + @git config --local include.path ../.gitconfig +prettify-all: + npx prettier --write '**/*.{ts,js,css,html}' $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii | ./script/tsc-emit-only > $@ @@ -143,7 +151,7 @@ $(BASIC_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(NODE_MODULES_INSTALLED): package.json test -e $(NODE_MODULES_INSTALLED) || rm -rf ./node_modules/ # robust against previous botched npm install - NODE_ENV=development npm install + NODE_ENV=development npm ci touch $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS): @@ -169,7 +177,7 @@ test: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) @echo "** now open test/{unit,visual}.html in your browser to run the {unit,visual} tests. **" benchmark: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) @echo - @echo "** now open benchmark/select.html in your browser. **" + @echo "** now open benchmark/{render,select}.html in your browser. **" $(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/tsc-emit-only > $@ diff --git a/benchmark/render.html b/benchmark/render.html new file mode 100644 index 000000000..5e313d8d5 --- /dev/null +++ b/benchmark/render.html @@ -0,0 +1,173 @@ + + +
+ + + ++ This page randomly generates one or more latex expressions and times how + long it takes to mathquillify them. Query parameters: +
+Benchmark inserting and then selecting n characters
+ +| nchars | +render (ms) | +select (ms) | +
|---|
Static math span: x = \frac{ -b \pm \sqrt{b^2-4ac} }{ 2a } +
+ Static math span: + x = \frac{ -b \pm \sqrt{b^2-4ac} }{ 2a } +
Editable math field: x^2
LaTeX of what you typed: x^2
MathQuill’s Getting - Started Guide
++ MathQuill’s Getting Started Guide +
@@ -27,13 +33,13 @@ // you may pass in an options object: var mathField = MQ.MathField(mathFieldSpan, { spaceBehavesLikeTab: true, // an example config option, for more see: - // http://docs.mathquill.com/en/latest/Config/ + // http://docs.mathquill.com/en/latest/Config/ handlers: { - edit: function() { + edit: function () { // retrieve, in LaTeX format, the math that was typed: latexSpan.textContent = mathField.latex(); - } - } + }, + }, }); diff --git a/script/screenshots.js b/script/screenshots.js index 547669b5f..ac34f37c0 100644 --- a/script/screenshots.js +++ b/script/screenshots.js @@ -20,185 +20,238 @@ var accessKey = process.env.SAUCE_ACCESS_KEY; var build_name = process.env.MQ_CI_BUILD_NAME; var baseDir = process.env.CIRCLE_ARTIFACTS; if (!baseDir) { - console.error('No $CIRCLE_ARTIFACTS found, for testing do something like `CIRCLE_ARTIFACTS=/tmp script/screenshots.js`'); + console.error( + 'No $CIRCLE_ARTIFACTS found, for testing do something like `CIRCLE_ARTIFACTS=/tmp script/screenshots.js`' + ); process.exit(1); } -fs.mkdirSync(baseDir+'/imgs'); -fs.mkdirSync(baseDir+'/imgs/pieces'); -fs.mkdirSync(baseDir+'/browser_logs'); +fs.mkdirSync(baseDir + '/imgs'); +fs.mkdirSync(baseDir + '/imgs/pieces'); +fs.mkdirSync(baseDir + '/browser_logs'); var browsers = [ { config: { browserName: 'Internet Explorer', - platform: 'Windows XP' + platform: 'Windows XP', }, - pinned: true // assume pinned to IE 8 + pinned: true, // assume pinned to IE 8 }, { config: { browserName: 'Internet Explorer', - platform: 'Windows 7' + platform: 'Windows 7', }, - pinned: true // assume pinned to IE 11 + pinned: true, // assume pinned to IE 11 }, { config: { browserName: 'MicrosoftEdge', - platform: 'Windows 10' - } + platform: 'Windows 10', + }, }, { config: { browserName: 'Firefox', - platform: 'OS X 10.11' - } + platform: 'OS X 10.11', + }, }, { config: { browserName: 'Safari', - platform: 'OS X 10.11' - } + platform: 'OS X 10.11', + }, }, { config: { browserName: 'Chrome', - platform: 'OS X 10.11' - } + platform: 'OS X 10.11', + }, }, { config: { browserName: 'Firefox', - platform: 'Linux' - } - } + platform: 'Linux', + }, + }, ]; - -browsers.forEach(function(browser) { +browsers.forEach(function (browser) { browser.config.build = build_name; - browser.config.name = 'Visual tests, ' + browser.config.browserName + ' on ' + browser.config.platform; - browser.config.customData = {build_url: process.env.CIRCLE_BUILD_URL}; - var browserDriver = wd.promiseChainRemote('ondemand.saucelabs.com', 80, username, accessKey); - return browserDriver.init(browser.config) - .then(function(args) { - var cfg = browser.config, capabilities = args[1]; - var version = capabilities.version || capabilities.browserVersion; - var sessionName = [cfg.browserName, version, cfg.platform].join(' '); - if (capabilities.platformVersion) sessionName += ' ' + capabilities.platformVersion; - console.log(sessionName, 'init', args); + browser.config.name = + 'Visual tests, ' + + browser.config.browserName + + ' on ' + + browser.config.platform; + browser.config.customData = { build_url: process.env.CIRCLE_BUILD_URL }; + var browserDriver = wd.promiseChainRemote( + 'ondemand.saucelabs.com', + 80, + username, + accessKey + ); + return browserDriver + .init(browser.config) + .then(function (args) { + var cfg = browser.config, + capabilities = args[1]; + var version = capabilities.version || capabilities.browserVersion; + var sessionName = [cfg.browserName, version, cfg.platform].join(' '); + if (capabilities.platformVersion) + sessionName += ' ' + capabilities.platformVersion; + console.log(sessionName, 'init', args); - var evergreen = browser.pinned ? '' : '_(evergreen)'; - var fileName = [cfg.browserName, version + evergreen, cfg.platform].join('_'); - if (capabilities.platformVersion) fileName += ' ' + capabilities.platformVersion; - fileName = fileName.replace(/ /g, '_'); + var evergreen = browser.pinned ? '' : '_(evergreen)'; + var fileName = [cfg.browserName, version + evergreen, cfg.platform].join( + '_' + ); + if (capabilities.platformVersion) + fileName += ' ' + capabilities.platformVersion; + fileName = fileName.replace(/ /g, '_'); - return browserDriver.get(url) - .then(willLog(sessionName, 'get')) - .safeExecute('document.body.focus()') // blur anything that's auto-focused - .then(willLog(sessionName, 'document.body.focus()')) - .safeExecute('document.documentElement.style.overflow = "hidden"') // hide scrollbars - .then(willLog(sessionName, 'hide scrollbars')) - .then(function() { - // Microsoft Edge starts out with illegally big window: https://git.io/vD63O - if (cfg.browserName === 'MicrosoftEdge') { - return browserDriver.getWindowSize() - .then(function(size) { - return browserDriver.setWindowSize(size.width, size.height) + return browserDriver + .get(url) + .then(willLog(sessionName, 'get')) + .safeExecute('document.body.focus()') // blur anything that's auto-focused + .then(willLog(sessionName, 'document.body.focus()')) + .safeExecute('document.documentElement.style.overflow = "hidden"') // hide scrollbars + .then(willLog(sessionName, 'hide scrollbars')) + .then(function () { + // Microsoft Edge starts out with illegally big window: https://git.io/vD63O + if (cfg.browserName === 'MicrosoftEdge') { + return browserDriver + .getWindowSize() + .then(function (size) { + return browserDriver.setWindowSize(size.width, size.height); + }) + .then( + willLog(sessionName, 'reset window size (Edge-only workaround)') + ); + } }) - .then(willLog(sessionName, 'reset window size (Edge-only workaround)')) - } - }) - .then(function() { - return [browserDriver.safeExecute('document.documentElement.scrollHeight'), - browserDriver.safeExecute('document.documentElement.clientHeight')]; - }) - .spread(function(scrollHeight, viewportHeight) { - console.log(sessionName, 'get scrollHeight, clientHeight', scrollHeight, viewportHeight); + .then(function () { + return [ + browserDriver.safeExecute('document.documentElement.scrollHeight'), + browserDriver.safeExecute('document.documentElement.clientHeight'), + ]; + }) + .spread(function (scrollHeight, viewportHeight) { + console.log( + sessionName, + 'get scrollHeight, clientHeight', + scrollHeight, + viewportHeight + ); - // the easy case: IE and Firefox on Linux return a screenshot of the entire webpage - if (cfg.browserName === 'Internet Explorer'|| (cfg.browserName === 'Firefox' && cfg.platform === 'Linux')) { - return browserDriver.saveScreenshot(baseDir + '/imgs/' + fileName + '.png') - .then(willLog(sessionName, 'saveScreenshot')) - // the hard case: for Chrome, Safari, and Edge, scroll through the page and - // take screenshots of each piece; circle.yml will stitch them together - } else { - var piecesDir = baseDir + '/imgs/pieces/' + fileName + '/'; - fs.mkdirSync(piecesDir); + // the easy case: IE and Firefox on Linux return a screenshot of the entire webpage + if ( + cfg.browserName === 'Internet Explorer' || + (cfg.browserName === 'Firefox' && cfg.platform === 'Linux') + ) { + return browserDriver + .saveScreenshot(baseDir + '/imgs/' + fileName + '.png') + .then(willLog(sessionName, 'saveScreenshot')); + // the hard case: for Chrome, Safari, and Edge, scroll through the page and + // take screenshots of each piece; circle.yml will stitch them together + } else { + var piecesDir = baseDir + '/imgs/pieces/' + fileName + '/'; + fs.mkdirSync(piecesDir); - var scrollTop = 0; - var index = 1; + var scrollTop = 0; + var index = 1; - return (function loop() { - return browserDriver.safeEval('window.scrollTo(0,'+scrollTop+');') - .then(willLog(sessionName, 'scrollTo()')) - .saveScreenshot(piecesDir + index + '.png') - .then(function() { - console.log(sessionName, 'saveScreenshot'); + return (function loop() { + return browserDriver + .safeEval('window.scrollTo(0,' + scrollTop + ');') + .then(willLog(sessionName, 'scrollTo()')) + .saveScreenshot(piecesDir + index + '.png') + .then(function () { + console.log(sessionName, 'saveScreenshot'); - scrollTop += viewportHeight; - index += 1; + scrollTop += viewportHeight; + index += 1; - // if the viewport hasn't passed the bottom edge of the page yet, - // scroll down and take another screenshot - if (scrollTop + viewportHeight <= scrollHeight) { - // Use `window.scrollTo` because thats what jQuery does: - // https://github.com/jquery/jquery/blob/1.12.3/src/offset.js#L186 - // Use `window.scrollTo` instead of jQuery because jQuery was - // causing a stackoverflow in Safari. - return loop(); - } else { // we are past the bottom edge of the page, reduce window size to - // fit only the part of the page that hasn't been screenshotted. + // if the viewport hasn't passed the bottom edge of the page yet, + // scroll down and take another screenshot + if (scrollTop + viewportHeight <= scrollHeight) { + // Use `window.scrollTo` because thats what jQuery does: + // https://github.com/jquery/jquery/blob/1.12.3/src/offset.js#L186 + // Use `window.scrollTo` instead of jQuery because jQuery was + // causing a stackoverflow in Safari. + return loop(); + } else { + // we are past the bottom edge of the page, reduce window size to + // fit only the part of the page that hasn't been screenshotted. - // If there is no remaining part of the page, we're done, short-circuit - if (scrollTop === scrollHeight) return browserDriver; + // If there is no remaining part of the page, we're done, short-circuit + if (scrollTop === scrollHeight) return browserDriver; - return browserDriver.getWindowSize() - .then(function(windowSize) { - console.log(sessionName, 'getWindowSize'); - // window size is a little bigger than the viewport because of address - // bar and scrollbars and stuff - var windowPadding = windowSize.height - viewportHeight; - var newWindowHeight = scrollHeight - scrollTop + windowPadding; - return browserDriver.setWindowSize(windowSize.width, newWindowHeight) - .then(willLog(sessionName, 'setWindowSize')) - .safeEval('window.scrollTo(0,'+scrollHeight+');') - .then(willLog(sessionName, 'scrollTo() Final')) - .saveScreenshot(piecesDir + index + '.png') - .then(willLog(sessionName, 'saveScreenshot Final')); - }); + return browserDriver + .getWindowSize() + .then(function (windowSize) { + console.log(sessionName, 'getWindowSize'); + // window size is a little bigger than the viewport because of address + // bar and scrollbars and stuff + var windowPadding = windowSize.height - viewportHeight; + var newWindowHeight = + scrollHeight - scrollTop + windowPadding; + return browserDriver + .setWindowSize(windowSize.width, newWindowHeight) + .then(willLog(sessionName, 'setWindowSize')) + .safeEval('window.scrollTo(0,' + scrollHeight + ');') + .then(willLog(sessionName, 'scrollTo() Final')) + .saveScreenshot(piecesDir + index + '.png') + .then(willLog(sessionName, 'saveScreenshot Final')); + }); + } + }); + })(); + } + }) + .then(function () { + return browserDriver.log('browser').then( + function (logs) { + var logfile = + baseDir + + '/browser_logs/' + + sessionName.replace(/ /g, '_') + + '.log'; + return new Promise(function (resolve, reject) { + fs.writeFile( + logfile, + JSON.stringify(logs, null, 2), + function (err) { + err ? reject(err) : resolve(); + } + ); + }).then(willLog(sessionName, 'writeFile')); + }, + function (err) { + // the Edge, IE, and Firefox-on-macOS drivers don't support logs, but the others do + console.log( + sessionName, + 'Error fetching logs:', + JSON.stringify(err, null, 2) + ); } - }); - }()); - } + ); + }); }) - .then(function() { - return browserDriver.log('browser') - .then(function(logs) { - var logfile = baseDir + '/browser_logs/' + sessionName.replace(/ /g, '_') + '.log'; - return new Promise(function(resolve, reject) { - fs.writeFile(logfile, JSON.stringify(logs, null, 2), function(err) { - err ? reject(err) : resolve(); - }); - }) - .then(willLog(sessionName, 'writeFile')); - }, function(err) { - // the Edge, IE, and Firefox-on-macOS drivers don't support logs, but the others do - console.log(sessionName, 'Error fetching logs:', JSON.stringify(err, null, 2)); - }); - }); - }) - .sauceJobStatus(true) - .fail(function(err) { - console.log('ERROR:', browser.config.browserName, browser.config.platform); - console.log(JSON.stringify(err, null, 2)); - return browserDriver.sauceJobStatus(false); - }) - .quit(); + .sauceJobStatus(true) + .fail(function (err) { + console.log( + 'ERROR:', + browser.config.browserName, + browser.config.platform + ); + console.log(JSON.stringify(err, null, 2)); + return browserDriver.sauceJobStatus(false); + }) + .quit(); function willLog() { var msg = [].join.call(arguments, ' '); - return function(value) { + return function (value) { console.log(msg); return value; }; diff --git a/script/test_server.js b/script/test_server.js index b653eb424..c734a1d5e 100644 --- a/script/test_server.js +++ b/script/test_server.js @@ -11,80 +11,91 @@ var HOST = process.env.HOST || '0.0.0.0'; // main http.createServer(serveRequest).listen(PORT, HOST); -console.log('listening on '+HOST+':'+PORT); +console.log('listening on ' + HOST + ':' + PORT); run_make_test(); -'src test Makefile package.json'.split(' ').forEach(function(filename) { +'src test Makefile package.json'.split(' ').forEach(function (filename) { recursivelyWatch(filename, run_make_test); }); // functions function serveRequest(req, res) { - var reqTime = new Date; - enqueueOrDo(function() { + var reqTime = new Date(); + enqueueOrDo(function () { var filepath = path.normalize(url.parse(req.url).pathname).slice(1); - fs.readFile(filepath, function(err, data) { + fs.readFile(filepath, function (err, data) { if (err) { if (err.code === 'ENOENT' || err.code === 'EISDIR') { res.statusCode = 404; res.end('404 Not Found: /' + filepath + '\n'); - } - else { + } else { console.log(err); res.statusCode = 500; res.end('500 Internal Server Error: ' + err.code + '\n'); } - } - else { + } else { var ext = filepath.match(/\.[^.]+$/); if (ext) res.setHeader('Content-Type', 'text/' + ext[0].slice(1)); res.end(data); } - console.log('[%s] %s %s /%s - %s%sms', - reqTime.toISOString(), res.statusCode, req.method, filepath, - (data ? (data.length >> 10) + 'kb, ' : ''), Date.now() - reqTime); + console.log( + '[%s] %s %s /%s - %s%sms', + reqTime.toISOString(), + res.statusCode, + req.method, + filepath, + data ? (data.length >> 10) + 'kb, ' : '', + Date.now() - reqTime + ); }); }); } - function recursivelyWatch(watchee, cb) { - fs.readdir(watchee, function(err, files) { - if (err) { // not a directory, just watch it + fs.readdir(watchee, function (err, files) { + if (err) { + // not a directory, just watch it fs.watch(watchee, cb); - } - else { // a directory, recurse, also watch for files being added or deleted + } else { + // a directory, recurse, also watch for files being added or deleted files.forEach(recurse); - fs.watch(watchee, function() { - fs.readdir(watchee, function(err, filesNew) { + fs.watch(watchee, function () { + fs.readdir(watchee, function (err, filesNew) { if (err) return; // watchee may have been deleted // filesNew - files = new files or dirs to watch - filesNew.filter(function(file) { return files.indexOf(file) < 0; }) - .forEach(recurse); + filesNew + .filter(function (file) { + return files.indexOf(file) < 0; + }) + .forEach(recurse); files = filesNew; }); cb(); }); } - function recurse(file) { recursivelyWatch(path.join(watchee, file), cb); } + function recurse(file) { + recursivelyWatch(path.join(watchee, file), cb); + } }); } - var q; -function enqueueOrDo(cb) { q ? q.push(cb) : cb(); } +function enqueueOrDo(cb) { + q ? q.push(cb) : cb(); +} function run_make_test() { if (q) return; q = []; - console.log('[%s]\nmake test', (new Date).toISOString()); + console.log('[%s]\nmake test', new Date().toISOString()); var make_test = child_process.exec('make test', { env: process.env }); make_test.stdout.pipe(process.stdout, { end: false }); make_test.stderr.pipe(process.stderr, { end: false }); - make_test.on('exit', function(code) { + make_test.on('exit', function (code) { if (code) { console.error('Exit Code ' + code); } else { - var serverAddress = HOST === '0.0.0.0' ? 'localhost:' + PORT : HOST + ':' + PORT; + 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'); } diff --git a/src/commands/math.ts b/src/commands/math.ts index 24dea4b82..abd243582 100644 --- a/src/commands/math.ts +++ b/src/commands/math.ts @@ -8,18 +8,26 @@ * Both MathBlock's and MathCommand's descend from it. */ class MathElement extends MQNode { - finalizeInsert (options:CursorOptions, cursor:Cursor) { + finalizeInsert(options: CursorOptions, cursor: Cursor) { var self = this; - self.postOrder(function (node) { node.finalizeTree(options) }); - self.postOrder(function (node) { node.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(function (node) { node.blur(cursor); }); + self.postOrder(function (node) { + node.blur(cursor); + }); - self.postOrder(function (node) { node.reflow(); }); + self.postOrder(function (node) { + node.reflow(); + }); var selfR = self[R]; var selfL = self[L]; if (selfR) selfR.siblingCreated(options, L); @@ -28,35 +36,35 @@ class MathElement extends MQNode { node.reflow(); return undefined; }); - }; + } // If the maxDepth option is set, make sure // deeply nested content is truncated. Just return // false if the cursor is already too deep. - prepareInsertionAt (cursor:Cursor) { + prepareInsertionAt(cursor: Cursor) { var maxDepth = cursor.options.maxDepth; if (maxDepth !== undefined) { var cursorDepth = cursor.depth(); if (cursorDepth > maxDepth) { return false; } - this.removeNodesDeeperThan(maxDepth-cursorDepth); + this.removeNodesDeeperThan(maxDepth - cursorDepth); } return true; - }; + } // Remove nodes that are more than `cutoff` // blocks deep from this node. - removeNodesDeeperThan (cutoff:number) { + removeNodesDeeperThan(cutoff: number) { var depth = 0; - var queue:[[MQNode, number]] = [[this, depth]]; - var current:[MQNode, number] | undefined; + var queue: [[MQNode, number]] = [[this, depth]]; + var current: [MQNode, number] | undefined; // Do a breadth-first search of this node's descendants // down to cutoff, removing anything deeper. - while (current = queue.shift()) { + while ((current = queue.shift())) { var c = current; c[0].children().each(function (child) { - var i = (child instanceof MathBlock) ? 1 : 0; - depth = c[1]+i; + var i = child instanceof MathBlock ? 1 : 0; + depth = c[1] + i; if (depth <= cutoff) { queue.push([child, depth]); @@ -66,7 +74,7 @@ class MathElement extends MQNode { return undefined; }); } - }; + } } /** @@ -74,31 +82,39 @@ class MathElement extends MQNode { * Descendant commands are organized into blocks. */ class MathCommand extends MathElement { - replacedFragment:Fragment | undefined; + replacedFragment: Fragment | undefined; - constructor (ctrlSeq?:string, htmlTemplate?:string, textTemplate?:string[]) { + constructor( + ctrlSeq?: string, + htmlTemplate?: string, + textTemplate?: string[] + ) { super(); this.setCtrlSeqHtmlAndText(ctrlSeq, htmlTemplate, textTemplate); } - setCtrlSeqHtmlAndText (ctrlSeq?:string, htmlTemplate?:string, textTemplate?:string[]) { + setCtrlSeqHtmlAndText( + ctrlSeq?: string, + htmlTemplate?: string, + textTemplate?: string[] + ) { if (!this.ctrlSeq) this.ctrlSeq = ctrlSeq; if (htmlTemplate) this.htmlTemplate = htmlTemplate; if (textTemplate) this.textTemplate = textTemplate; } // obvious methods - replaces (replacedFragment:Fragment) { + replaces(replacedFragment: Fragment) { replacedFragment.disown(); this.replacedFragment = replacedFragment; - }; - isEmpty () { - return this.foldChildren(true, function(isEmpty, child) { + } + isEmpty() { + return this.foldChildren(true, function (isEmpty, child) { return isEmpty && child.isEmpty(); }); - }; + } - parser ():Parser(alternative: Parser): Parser{ + or (alternative: Parser): Parser{ pray('or is passed a parser', alternative instanceof Parser); var self = this; - return new Parser(function(stream, onSuccess, onFailure) { + return new Parser(function (stream, onSuccess, onFailure) { return self._(stream, onSuccess, failure); function failure(_newStream: string) { return alternative._(stream, onSuccess, onFailure); } }); - }; + } - then (next:Parser|((result: T)=>Parser)):Parser{ + then(next: Parser| ((result: T) => Parser)): Parser{ var self = this; - return new Parser(function(stream: string, onSuccess, onFailure) { + return new Parser(function (stream: string, onSuccess, onFailure) { return self._(stream, success, onFailure) as any as Q; function success(newStream: string, result: T) { - var nextParser = (next instanceof Parser ? next : next(result)); + var nextParser = next instanceof Parser ? next : next(result); pray('a parser is returned', nextParser instanceof Parser); return nextParser._(newStream, onSuccess, onFailure); } }); - }; + } // -*- optimized iterative combinators -*- // - many (): Parser{ + many(): Parser { var self = this; - return new Parser(function(stream, onSuccess, _onFailure) { + return new Parser(function (stream, onSuccess, _onFailure) { var xs: T[] = []; while (self._(stream, success, failure)); return onSuccess(stream, xs); @@ -86,13 +85,13 @@ class Parser { return false; } }); - }; + } - times (min: number, max?: number): Parser { + times(min: number, max?: number): Parser { if (arguments.length < 2) max = min; var self = this; - return new Parser(function(stream, onSuccess, onFailure) { + return new Parser(function (stream, onSuccess, onFailure) { var xs: T[] = []; var result: boolean = true; var failure; @@ -126,74 +125,80 @@ class Parser { return false; } }); - }; + } // -*- higher-level combinators -*- // - result (res: Q): Parser{ return this.then(Parser.succeed(res)); }; - atMost (n: number) { return this.times(0, n); }; - atLeast (n: number) { + result(res: Q): Parser{ + return this.then(Parser.succeed(res)); + } + atMost(n: number) { + return this.times(0, n); + } + atLeast(n: number) { var self = this; - return self.times(n).then(function(start) { - return self.many().map(function(end) { + return self.times(n).then(function (start) { + return self.many().map(function (end) { return start.concat(end); }); }); - }; + } - map(fn: (result: T)=>Q): Parser{ - return this.then(function(result) { return Parser.succeed(fn(result)); }); - }; + map(fn: (result: T) => Q): Parser{ + return this.then(function (result) { + return Parser.succeed(fn(result)); + }); + } - skip(two: Parser): Parser{ - return this.then(function(result) { return two.result(result); }); - }; + skip (two: Parser): Parser{ + return this.then(function (result) { + return two.result(result); + }); + } // -*- primitive parsers -*- // - static string (str: string): Parser { + static string(str: string): Parser { var len = str.length; - var expected = "expected '"+str+"'"; + var expected = "expected '" + str + "'"; - return new Parser(function(stream, onSuccess, onFailure) { + return new Parser(function (stream, onSuccess, onFailure) { var head = stream.slice(0, len); if (head === str) { return onSuccess(stream.slice(len), head); - } - else { + } else { return onFailure(stream, expected); } }); - }; + } - static regex (re: RegExp): Parser { + static regex(re: RegExp): Parser { pray('regexp parser is anchored', re.toString().charAt(1) === '^'); - var expected = 'expected '+re; + var expected = 'expected ' + re; - return new Parser(function(stream, onSuccess, onFailure) { + return new Parser(function (stream, onSuccess, onFailure) { var match = re.exec(stream); if (match) { var result = match[0]; return onSuccess(stream.slice(result.length), result); - } - else { + } else { return onFailure(stream, expected); } }); - }; + } - static succeed (result: Q): Parser{ - return new Parser(function(stream: string, onSuccess) { + static succeed(result: Q): Parser{ + return new Parser(function (stream: string, onSuccess) { return onSuccess(stream, result); }); - }; + } - static fail (msg: string):Parser{ - return new Parser(function(stream, _, onFailure) { + static fail(msg: string): Parser { + return new Parser(function (stream, _, onFailure) { return onFailure(stream, msg) as never; }); - }; + } static letter = Parser.regex(/^[a-z]/i); static letters = Parser.regex(/^[a-z]*/i); @@ -202,19 +207,31 @@ class Parser { static whitespace = Parser.regex(/^\s+/); static optWhitespace = Parser.regex(/^\s*/); - static any: Parser = new Parser(function(stream, onSuccess, onFailure) { + static any: Parser = new Parser(function ( + stream, + onSuccess, + onFailure + ) { if (!stream) return onFailure(stream, 'expected any character'); return onSuccess(stream.slice(1), stream.charAt(0)); }); - static all: Parser = new Parser(function(stream, onSuccess, _onFailure) { + static all: Parser = new Parser(function ( + stream, + onSuccess, + _onFailure + ) { return onSuccess('', stream); }); - static eof: Parser = new Parser(function(stream, onSuccess, onFailure) { + static eof: Parser = new Parser(function ( + stream, + onSuccess, + onFailure + ) { if (stream) return onFailure(stream, 'expected EOF'); return onSuccess(stream, stream); }); -}; +} diff --git a/src/services/saneKeyboardEvents.util.ts b/src/services/saneKeyboardEvents.util.ts index 32ed871c1..96c936b06 100644 --- a/src/services/saneKeyboardEvents.util.ts +++ b/src/services/saneKeyboardEvents.util.ts @@ -20,9 +20,9 @@ * + event handler logic * + attach event handlers and export methods ************************************************/ -type TextareaChecker = ((e?:Event) => void); +type TextareaChecker = (e?: Event) => void; -var saneKeyboardEvents = (function() { +var saneKeyboardEvents = (function () { // The following [key values][1] map was compiled from the // [DOM3 Events appendix section on key codes][2] and // [a widely cited report on cross-browser tests of key codes][3], @@ -32,7 +32,7 @@ var saneKeyboardEvents = (function() { // [1]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#keys-keyvalues // [2]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes // [3]: http://unixpapa.com/js/key.html - const KEY_VALUES:Record = { + const KEY_VALUES: Record = { 8: 'Backspace', 9: 'Tab', @@ -63,12 +63,12 @@ var saneKeyboardEvents = (function() { 46: 'Del', - 144: 'NumLock' + 144: 'NumLock', }; // To the extent possible, create a normalized string representation // of the key combo (i.e., key code and modifier keys). - function stringify(evt:JQ_KeyboardEvent) { + function stringify(evt: JQ_KeyboardEvent) { var which = evt.which || evt.keyCode; var keyVal = KEY_VALUES[which]; var key; @@ -89,9 +89,9 @@ var saneKeyboardEvents = (function() { // create a keyboard events shim that calls callbacks at useful times // and exports useful public methods - return function saneKeyboardEvents(el:$, controller:Controller) { - var keydown:JQ_KeyboardEvent | null = null; - var keypress:KeyboardEvent | null = null; + return function saneKeyboardEvents(el: $, controller: Controller) { + var keydown: JQ_KeyboardEvent | null = null; + var keypress: KeyboardEvent | null = null; var textarea = jQuery(el); var target = jQuery(controller.container || textarea); @@ -104,25 +104,28 @@ var saneKeyboardEvents = (function() { // after selecting something and then typing, the textarea is // incorrectly reported as selected during the input event (but not // subsequently). - var checkTextarea:TextareaChecker = noop; - var timeoutId:number; - function checkTextareaFor(checker:TextareaChecker) { + var checkTextarea: TextareaChecker = noop; + var timeoutId: number; + function checkTextareaFor(checker: TextareaChecker) { checkTextarea = checker; clearTimeout(timeoutId); timeoutId = setTimeout(checker); } - function checkTextareaOnce(checker:TextareaChecker) { - checkTextareaFor(function(e) { + function checkTextareaOnce(checker: TextareaChecker) { + checkTextareaFor(function (e) { checkTextarea = noop; clearTimeout(timeoutId); checker(e); }); } - target.bind('keydown keypress input keyup paste', function(e:KeyboardEvent) { - checkTextarea(e); - }); + target.bind( + 'keydown keypress input keyup paste', + function (e: KeyboardEvent) { + checkTextarea(e); + } + ); - function guardedTextareaSelect () { + function guardedTextareaSelect() { try { // IE can throw an 'Incorrect Function' error if you // try to select a textarea that is hidden. It seems @@ -130,11 +133,11 @@ var saneKeyboardEvents = (function() { // 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) {}; + } catch (e) {} } // -*- public methods -*- // - function select(text:string) { + function select(text: string) { // check textarea at least once/one last time before munging (so // no race condition if selection happens after keypress/paste but // before checkTextarea), then never again ('cos it's been munged) @@ -169,24 +172,25 @@ var saneKeyboardEvents = (function() { } // -*- event handlers -*- // - function onKeydown(e:KeyboardEvent) { + function onKeydown(e: KeyboardEvent) { if (e.target !== textarea[0]) return; keydown = e; keypress = null; - if (shouldBeSelected) checkTextareaOnce(function(e?:Event) { - if (!(e && e.type === 'focusout')) { - // re-select textarea in case it's an unrecognized key that clears - // the selection, then never again, 'cos next thing might be blur - guardedTextareaSelect() - } - }); + if (shouldBeSelected) + checkTextareaOnce(function (e?: Event) { + if (!(e && e.type === 'focusout')) { + // re-select textarea in case it's an unrecognized key that clears + // the selection, then never again, 'cos next thing might be blur + guardedTextareaSelect(); + } + }); handleKey(); } - function isArrowKey (e:JQ_KeyboardEvent) { + function isArrowKey(e: JQ_KeyboardEvent) { if (!e || !e.originalEvent) return false; // The keyPress event in FF reports which=0 for some reason. The new @@ -201,7 +205,7 @@ var saneKeyboardEvents = (function() { return false; } - function onKeypress(e:KeyboardEvent) { + function onKeypress(e: KeyboardEvent) { if (e.target !== textarea[0]) return; // call the key handler for repeated keypresses. @@ -220,12 +224,11 @@ var saneKeyboardEvents = (function() { checkTextareaFor(typedText); } } - function onKeyup(e:KeyboardEvent) { + function onKeyup(e: KeyboardEvent) { if (e.target !== textarea[0]) return; // Handle case of no keypress event being sent if (!!keydown && !keypress) { - // only check for typed text if this key can type text. Otherwise // you can end up with mathquill thinking text was typed if you // use the mq.keystroke('Right') command while a single character @@ -276,7 +279,7 @@ var saneKeyboardEvents = (function() { textarea.val(''); } - function onPaste(e:Event) { + function onPaste(e: Event) { if (e.target !== textarea[0]) return; // browsers are dumb. @@ -311,9 +314,15 @@ var saneKeyboardEvents = (function() { keypress: onKeypress, keyup: onKeyup, focusout: onBlur, - copy: function(e:Event) { e.preventDefault(); }, - cut: function(e:Event) { e.preventDefault(); }, - paste: function(e:Event) { e.preventDefault(); } + copy: function (e: Event) { + e.preventDefault(); + }, + cut: function (e: Event) { + e.preventDefault(); + }, + paste: function (e: Event) { + e.preventDefault(); + }, }); } else { target.bind({ @@ -321,15 +330,23 @@ var saneKeyboardEvents = (function() { keypress: onKeypress, keyup: onKeyup, focusout: onBlur, - cut: function() { checkTextareaOnce(function() { controller.cut(); }); }, - copy: function() { checkTextareaOnce(function() { controller.copy(); }); }, - paste: onPaste + cut: function () { + checkTextareaOnce(function () { + controller.cut(); + }); + }, + copy: function () { + checkTextareaOnce(function () { + controller.copy(); + }); + }, + paste: onPaste, }); } // -*- export public methods -*- // return { - select: select + select: select, }; }; -}()); +})(); diff --git a/src/services/scrollHoriz.ts b/src/services/scrollHoriz.ts index 3075a402b..6967a3ea6 100644 --- a/src/services/scrollHoriz.ts +++ b/src/services/scrollHoriz.ts @@ -4,7 +4,7 @@ **********************************************/ class Controller_scrollHoriz extends Controller_mouse { - setOverflowClasses () { + setOverflowClasses() { var root = this.root.jQ[0]; var shouldHaveOverflowRight = false; var shouldHaveOverflowLeft = false; @@ -12,19 +12,26 @@ class Controller_scrollHoriz extends Controller_mouse { var width = root.getBoundingClientRect().width; var scrollWidth = root.scrollWidth; var scroll = root.scrollLeft; - shouldHaveOverflowRight = (scrollWidth > width + scroll); - shouldHaveOverflowLeft = (scroll > 0); + 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') + 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 () { - var cursor = this.cursor, seln = cursor.selection; + scrollHoriz() { + var cursor = this.cursor, + seln = cursor.selection; var rootRect = this.root.jQ[0].getBoundingClientRect(); if (!cursor.jQ[0] && !seln) { - this.root.jQ.stop().animate({scrollLeft: 0}, 100, () => { + this.root.jQ.stop().animate({ scrollLeft: 0 }, 100, () => { this.setOverflowClasses(); }); return; @@ -40,26 +47,26 @@ class Controller_scrollHoriz extends Controller_mouse { if (seln.ends[L] === cursor[R]) { if (overLeft < 0) var scrollBy = overLeft; else if (overRight > 0) { - if (rect.left - overRight < rootRect.left + 20) var scrollBy = overLeft; + if (rect.left - overRight < rootRect.left + 20) + var scrollBy = overLeft; else var scrollBy = overRight; - } - else return; - } - else { + } else return; + } else { if (overRight > 0) var scrollBy = overRight; else if (overLeft < 0) { - if (rect.right - overLeft > rootRect.right - 20) var scrollBy = overRight; + if (rect.right - overLeft > rootRect.right - 20) + var scrollBy = overRight; else var scrollBy = overLeft; - } - else return; + } else return; } } - 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, () => { + 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, () => { this.setOverflowClasses(); }); - }; -}; + } +} diff --git a/src/services/textarea.ts b/src/services/textarea.ts index aa23be57a..cac0b0201 100644 --- a/src/services/textarea.ts +++ b/src/services/textarea.ts @@ -2,17 +2,21 @@ * Manage the MathQuill instance's textarea * (as owned by the Controller) ********************************************/ -Options.prototype.substituteTextarea = function() { - return $('