From 80682a84a34c3503193a0f06a9daa65ae88090d8 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 29 Oct 2024 17:47:16 -0500 Subject: [PATCH 01/19] Fix security vulnerabilities in npm dependencies. --- package-lock.json | 287 +++++++++++++++++++++---------------- src/services/exportText.ts | 2 +- 2 files changed, 166 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5561d551..882371c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2860,9 +2860,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", - "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", + "integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==", "cpu": [ "arm" ], @@ -2874,9 +2874,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", - "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz", + "integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==", "cpu": [ "arm64" ], @@ -2888,9 +2888,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", - "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz", + "integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==", "cpu": [ "arm64" ], @@ -2902,9 +2902,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", - "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz", + "integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==", "cpu": [ "x64" ], @@ -2915,10 +2915,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz", + "integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz", + "integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", - "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz", + "integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==", "cpu": [ "arm" ], @@ -2930,9 +2958,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", - "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz", + "integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==", "cpu": [ "arm" ], @@ -2944,9 +2972,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", - "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz", + "integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==", "cpu": [ "arm64" ], @@ -2958,9 +2986,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", - "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz", + "integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==", "cpu": [ "arm64" ], @@ -2972,9 +3000,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", - "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz", + "integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==", "cpu": [ "ppc64" ], @@ -2986,9 +3014,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", - "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz", + "integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==", "cpu": [ "riscv64" ], @@ -3000,9 +3028,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", - "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz", + "integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==", "cpu": [ "s390x" ], @@ -3014,9 +3042,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", - "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", + "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", "cpu": [ "x64" ], @@ -3028,9 +3056,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", - "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz", + "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==", "cpu": [ "x64" ], @@ -3042,9 +3070,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", - "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz", + "integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==", "cpu": [ "arm64" ], @@ -3056,9 +3084,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", - "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz", + "integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==", "cpu": [ "ia32" ], @@ -3070,9 +3098,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", - "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz", + "integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==", "cpu": [ "x64" ], @@ -3188,9 +3216,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -4159,9 +4187,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -4173,7 +4201,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -4730,9 +4758,9 @@ "peer": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -4835,9 +4863,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -5399,9 +5427,9 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -5804,38 +5832,38 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -5981,14 +6009,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -6575,9 +6603,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7441,11 +7469,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -8015,9 +8046,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true, "license": "MIT" }, @@ -8816,13 +8847,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -9156,13 +9187,13 @@ } }, "node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", + "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -9172,22 +9203,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", + "@rollup/rollup-android-arm-eabi": "4.24.3", + "@rollup/rollup-android-arm64": "4.24.3", + "@rollup/rollup-darwin-arm64": "4.24.3", + "@rollup/rollup-darwin-x64": "4.24.3", + "@rollup/rollup-freebsd-arm64": "4.24.3", + "@rollup/rollup-freebsd-x64": "4.24.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.3", + "@rollup/rollup-linux-arm-musleabihf": "4.24.3", + "@rollup/rollup-linux-arm64-gnu": "4.24.3", + "@rollup/rollup-linux-arm64-musl": "4.24.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.3", + "@rollup/rollup-linux-riscv64-gnu": "4.24.3", + "@rollup/rollup-linux-s390x-gnu": "4.24.3", + "@rollup/rollup-linux-x64-gnu": "4.24.3", + "@rollup/rollup-linux-x64-musl": "4.24.3", + "@rollup/rollup-win32-arm64-msvc": "4.24.3", + "@rollup/rollup-win32-ia32-msvc": "4.24.3", + "@rollup/rollup-win32-x64-msvc": "4.24.3", "fsevents": "~2.3.2" } }, @@ -9442,9 +9475,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -9483,6 +9516,16 @@ "dev": true, "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9587,16 +9630,16 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" diff --git a/src/services/exportText.ts b/src/services/exportText.ts index e9df7753..178a6476 100644 --- a/src/services/exportText.ts +++ b/src/services/exportText.ts @@ -1,4 +1,4 @@ -// Extension to the Controller that allows exporting of math in a human-readable text format. +// Extension to the Controller that allows exporting of math in a human-readable text format. import type { Constructor } from 'src/constants'; import type { ControllerBase } from 'src/controller'; From 10cf9b48c1a6e46a6a5d20ada5362e85361c4365 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 29 Oct 2024 21:43:21 -0500 Subject: [PATCH 02/19] Convert the TNode byId property to a Map instead of a record. This is a more proper way of handling this sort of thing. This was observed in initial work on switching to using strict type checking. --- src/publicapi.ts | 2 +- src/publicapiBasic.ts | 2 +- src/services/mouse.ts | 13 ++++++------- src/tree/node.ts | 10 +++++----- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/publicapi.ts b/src/publicapi.ts index f39951a6..c9dfaeb9 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -52,7 +52,7 @@ export default class MathQuill { const MQ = (el: unknown) => { if (!(el instanceof HTMLElement)) return; const blockId = el.querySelector('.mq-root-block')?.getAttribute(mqBlockId) ?? false; - const ctrlr = blockId ? TNode.byId[parseInt(blockId)].controller : undefined; + const ctrlr = blockId ? TNode.byId.get(parseInt(blockId))?.controller : undefined; return ctrlr?.apiClass; }; diff --git a/src/publicapiBasic.ts b/src/publicapiBasic.ts index 5369159f..b4bbafcc 100644 --- a/src/publicapiBasic.ts +++ b/src/publicapiBasic.ts @@ -46,7 +46,7 @@ export default class MathQuill { const MQ = (el: unknown) => { if (!(el instanceof HTMLElement)) return; const blockId = el.querySelector('.mq-root-block')?.getAttribute(mqBlockId) ?? false; - const ctrlr = blockId ? TNode.byId[parseInt(blockId)].controller : undefined; + const ctrlr = blockId ? TNode.byId.get(parseInt(blockId))?.controller : undefined; return ctrlr?.apiClass; }; diff --git a/src/services/mouse.ts b/src/services/mouse.ts index 02c348de..394a324e 100644 --- a/src/services/mouse.ts +++ b/src/services/mouse.ts @@ -20,12 +20,11 @@ export const MouseEventController = & // Drag-to-select event handling this.mouseDownHandler = (e: MouseEvent) => { const rootEl = (e.target as HTMLElement).closest('.mq-root-block')!; - const root = - TNode.byId[ - parseInt((rootEl?.getAttribute(mqBlockId) || ultimateRootEl?.getAttribute(mqBlockId)) ?? '0') - ]; + const root = TNode.byId.get( + parseInt((rootEl?.getAttribute(mqBlockId) || ultimateRootEl?.getAttribute(mqBlockId)) ?? '0') + ); - if (!root.controller) { + if (!root?.controller) { throw 'controller undefined... what?'; } @@ -152,7 +151,7 @@ export const MouseEventController = & ); } } - const node = nodeId ? TNode.byId[nodeId] : this.root; + const node = nodeId ? TNode.byId.get(nodeId) : this.root; pray('nodeId is the id of some TNode that exists', !!node); // Don't clear the selection until after getting node from target, in case @@ -160,7 +159,7 @@ export const MouseEventController = & // seek from root, which is less accurate (e.g. fraction). cursor.clearSelection().show(); - node.seek(pageX, cursor); + node?.seek(pageX, cursor); // Before .selectFrom when mouse-selecting, so // always hits no-selection case in scrollHoriz and scrolls slower diff --git a/src/tree/node.ts b/src/tree/node.ts index dcbfef45..a53046bd 100644 --- a/src/tree/node.ts +++ b/src/tree/node.ts @@ -20,7 +20,7 @@ const prayOverridden = (name: string) => pray(`"${name}" should be overridden or // Only doing tree node manipulation via these adopt/disown methods guarantees well-formedness of the tree. export class TNode { static id = 0; - static byId: Record = {}; + static byId = new Map(); static uniqueNodeId = () => ++TNode.id; elements: VNode = new VNode(); @@ -66,11 +66,11 @@ export class TNode { constructor() { this.id = TNode.uniqueNodeId(); - TNode.byId[this.id] = this; + TNode.byId.set(this.id, this); } dispose() { - delete TNode.byId[this.id]; + TNode.byId.delete(this.id); } toString() { @@ -89,8 +89,8 @@ export class TNode { if (el instanceof HTMLElement) { const cmdId = parseInt(el.getAttribute(mqCmdId) ?? '0'); const blockId = parseInt(el.getAttribute(mqBlockId) ?? '0'); - if (cmdId) TNode.byId[cmdId].addToElements(el); - if (blockId) TNode.byId[blockId].addToElements(el); + if (cmdId) TNode.byId.get(cmdId)?.addToElements(el); + if (blockId) TNode.byId.get(blockId)?.addToElements(el); } for (let child = el.firstChild; child; child = child.nextSibling) { addToElements(child); From 2c30040fddb7f110757a070ed33ba6943c79726a Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 30 Oct 2024 19:55:03 -0500 Subject: [PATCH 03/19] Turn on @typescript-eslint's strict type checking. This requires a few changes, but mostly they are just typings which don't change the actual generated code. There several eslint-ignore statements added for this that should be removed (with some effort). Note that the pray and prayDirection methods has been removed. Checking the conditions directly in place requires about the same amount of code, and has the benifit of revealing proper typings (not undefined mainly) to typescript if the error is not thrown. --- eslint.config.mjs | 2 +- src/abstractFields.ts | 10 +- src/commands/math/LatexCommandInput.ts | 9 +- src/commands/math/basicSymbols.ts | 6 +- src/commands/math/commands.ts | 12 +- src/commands/mathBlock.ts | 28 ++-- src/commands/mathElements.ts | 189 ++++++++++++++----------- src/commands/textBlock.ts | 3 +- src/commands/textElements.ts | 49 ++++--- src/constants.ts | 68 +++------ src/controller.ts | 46 +++--- src/cursor.ts | 79 ++++++----- src/mixins.ts | 11 +- src/options.ts | 4 +- src/publicapi.ts | 23 +-- src/publicapiBasic.ts | 21 +-- src/services/focusBlur.ts | 2 +- src/services/latex.ts | 15 +- src/services/mouse.ts | 22 +-- src/services/parser.util.ts | 16 ++- src/services/textarea.ts | 25 +++- src/tree/fragment.ts | 25 ++-- src/tree/node.ts | 47 +++--- src/tree/vNode.ts | 22 +-- 24 files changed, 399 insertions(+), 335 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 597d022e..1125bff5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,7 +10,7 @@ export default [ }, { files: ['**/*.{js,mjs,cjs,ts,tsx}'] }, pluginJs.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.strictTypeChecked, ...tseslint.configs.stylisticTypeChecked, { languageOptions: { diff --git a/src/abstractFields.ts b/src/abstractFields.ts index 75de0297..0f305d98 100644 --- a/src/abstractFields.ts +++ b/src/abstractFields.ts @@ -53,6 +53,8 @@ export class AbstractMathQuill { el.append(...contents); return el; }; + + return this; } get options() { @@ -106,7 +108,7 @@ export class EditableField extends AbstractMathQuill { focus() { if (document.activeElement === this.__controller.textarea) - this.__controller.textarea?.dispatchEvent(new FocusEvent('focus')); + this.__controller.textarea.dispatchEvent(new FocusEvent('focus')); else this.__controller.textarea?.focus(); return this; } @@ -114,7 +116,7 @@ export class EditableField extends AbstractMathQuill { blur() { if (document.activeElement !== this.__controller.textarea) this.__controller.textarea?.dispatchEvent(new FocusEvent('blur')); - else this.__controller.textarea?.blur(); + else this.__controller.textarea.blur(); return this; } @@ -129,7 +131,9 @@ export class EditableField extends AbstractMathQuill { const root = this.__controller.root, cursor = this.__controller.cursor; root.eachChild('postOrder', 'dispose'); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete root.ends[L]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete root.ends[R]; root.elements.empty(); delete cursor.selection; @@ -142,7 +146,7 @@ export class EditableField extends AbstractMathQuill { cursor = ctrlr.cursor; if (/^\\[a-z]+$/i.test(cmd) && !cursor.isTooDeep()) { cmd = cmd.slice(1); - const klass = LatexCmds[cmd]; + const klass = LatexCmds[cmd] as Constructor | undefined; if (klass) { const newCmd = new klass(cmd); if (cursor.selection) newCmd.replaces(cursor.replaceSelection()); diff --git a/src/commands/math/LatexCommandInput.ts b/src/commands/math/LatexCommandInput.ts index e3e3567b..e7c9a665 100644 --- a/src/commands/math/LatexCommandInput.ts +++ b/src/commands/math/LatexCommandInput.ts @@ -26,7 +26,9 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { createBlocks() { super.createBlocks(); - const leftEnd = this.ends[L]!; + const leftEnd = this.ends[L]; + + if (!leftEnd) return; leftEnd.focus = () => { leftEnd.parent?.elements.addClass('mq-has-cursor'); @@ -58,7 +60,8 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { e.preventDefault(); return; } - return super.keystroke(key, e, ctrlr); + super.keystroke(key, e, ctrlr); + return; }; } @@ -93,7 +96,7 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { this.remove(); if (this[R]) cursor.insLeftOf(this[R]); - else cursor.insAtRightEnd(this.parent!); + else if (this.parent) cursor.insAtRightEnd(this.parent); const latex = this.ends[L]?.latex() || ' '; if (latex in LatexCmds) { diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index fdf63e24..d3feacbc 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -1,6 +1,6 @@ // Symbols for Basic Mathematics -import type { Direction } from 'src/constants'; +import type { Direction, Constructor } from 'src/constants'; import { noop, L, R, bindMixin, LatexCmds, CharCmds } from 'src/constants'; import { Options } from 'src/options'; import type { Cursor } from 'src/cursor'; @@ -80,7 +80,7 @@ LatexCmds["'"] = LatexCmds.prime = bindMixin(VanillaSymbol, "'", '′'); // LatexCmds['\u2033'] = LatexCmds.dprime = bindMixin(VanillaSymbol, '\u2033', '″'); LatexCmds.backslash = bindMixin(VanillaSymbol, '\\backslash ', '\\'); -if (!CharCmds['\\']) CharCmds['\\'] = LatexCmds.backslash; +if (!(CharCmds['\\'] as Constructor | undefined)) CharCmds['\\'] = LatexCmds.backslash; LatexCmds.$ = bindMixin(VanillaSymbol, '\\$', '$'); @@ -208,7 +208,7 @@ class LatexFragment extends MathCommand { createLeftOf(cursor: Cursor) { const block: MathCommand = latexMathParser.parse(this.latex()); - block.children().adopt(cursor.parent!, cursor[L], cursor[R]); + if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); cursor[L] = block.ends[R]; cursor.element.before(...block.domify().contents); block.finalizeInsert(cursor.options, cursor); diff --git a/src/commands/math/commands.ts b/src/commands/math/commands.ts index ac98b7b3..ce9cf7a2 100644 --- a/src/commands/math/commands.ts +++ b/src/commands/math/commands.ts @@ -147,7 +147,7 @@ LatexCmds.subscript = LatexCmds._ = class extends SupSub { finalizeTree() { this.downInto = this.sub = this.ends[L]; - this.sub!.upOutOf = insLeftOfMeUnlessAtEnd; + if (this.sub) this.sub.upOutOf = insLeftOfMeUnlessAtEnd; super.finalizeTree(); } }; @@ -166,7 +166,7 @@ LatexCmds.superscript = finalizeTree() { this.upInto = this.sup = this.ends[R]; - this.sup!.downOutOf = insLeftOfMeUnlessAtEnd; + if (this.sup) this.sup.downOutOf = insLeftOfMeUnlessAtEnd; super.finalizeTree(); } }; @@ -237,7 +237,7 @@ const FractionChooseCreateLeftOfMixin = > if (leftward instanceof UpperLowerLimitCommand && leftward[R] instanceof SupSub) { leftward = leftward[R]; - if (leftward && leftward[R] instanceof SupSub && leftward[R]?.ctrlSeq != leftward.ctrlSeq) + if (leftward[R] instanceof SupSub && leftward[R].ctrlSeq != leftward.ctrlSeq) leftward = leftward[R]; } @@ -503,7 +503,8 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { } finalizeTree(options: Options) { - const ctrlr = new Controller(this.ends[L]!, this.elements.firstElement, options); + if (!this.ends[L]) throw 'Missing left end finalizing editable tree'; + const ctrlr = new Controller(this.ends[L], this.elements.firstElement, options); ctrlr.KIND_OF_MQ = 'MathField'; this.field = new InnerMathField(ctrlr); this.field.name = this.name; @@ -516,7 +517,8 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { } registerInnerField(innerFields: InnerMathFieldStore) { - innerFields.push(this.field!); + if (!this.field) throw 'Unable to register editable without field'; + innerFields.push(this.field); } latex() { diff --git a/src/commands/mathBlock.ts b/src/commands/mathBlock.ts index 45917a32..81549b07 100644 --- a/src/commands/mathBlock.ts +++ b/src/commands/mathBlock.ts @@ -14,8 +14,9 @@ export const writeMethodMixin = >(Base: TBase) class extends Base { writeHandler?: (cursor: Cursor, ch: string) => boolean; - chToCmd(ch: string, options: Options): TNode { - const cons = CharCmds[ch] || LatexCmds[ch]; + chToCmd(ch: string, options?: Options): TNode { + const cons = + (CharCmds[ch] as Constructor | undefined) || (LatexCmds[ch] as Constructor | undefined); // exclude f because it gets a dedicated command with more spacing if (/^[a-eg-zA-Z]$/.exec(ch)) return new Letter(ch); else if (/^\d$/.test(ch)) return new Digit(ch); @@ -43,9 +44,10 @@ export const writeMethodMixin = >(Base: TBase) cursor[L] && !cursor[R] && !cursor.selection && - cursor.options.charsThatBreakOutOfSupSub.includes(ch) + cursor.options.charsThatBreakOutOfSupSub.includes(ch) && + this.parent ) { - cursor.insRightOf(this.parent!); + cursor.insRightOf(this.parent); } } @@ -72,7 +74,7 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { } text() { - return this.ends[L] && this.ends[L] === this.ends[R] ? (this.ends[L]?.text() ?? '') : this.join('text'); + return this.ends[L] && this.ends[L] === this.ends[R] ? this.ends[L].text() : this.join('text'); } keystroke(key: string, e: KeyboardEvent, ctrlr: Controller) { @@ -86,7 +88,7 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { ctrlr.escapeDir(key === 'Shift-Spacebar' ? L : R, key, e); return; } - return super.keystroke(key, e, ctrlr); + super.keystroke(key, e, ctrlr); } // editability methods: called by the cursor for editing, cursor movements, @@ -95,11 +97,11 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { moveOutOf(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { const updownInto = updown && this.parent?.[`${updown}Into`]; if (!updownInto && this[dir]) cursor.insAtDirEnd(dir === L ? R : L, this[dir]); - else cursor.insDirOf(dir, this.parent!); + else if (this.parent) cursor.insDirOf(dir, this.parent); } selectOutOf(dir: Direction, cursor: Cursor) { - cursor.insDirOf(dir, this.parent!); + if (this.parent) cursor.insDirOf(dir, this.parent); } deleteOutOf(_dir: Direction, cursor: Cursor) { @@ -125,10 +127,10 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { const all = Parser.all; const eof = Parser.eof; - const block: MathCommand = latexMathParser.skip(eof).or(all.result(false)).parse(latex); + const block: MathCommand | undefined = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { - block.children().adopt(cursor.parent!, cursor[L], cursor[R]); + if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); const elements = block.domify(); cursor.element.before(...elements.contents); cursor[L] = block.ends[R]; @@ -173,11 +175,11 @@ export class RootMathCommand extends writeMethodMixin(MathCommand) { leftEnd.write = (cursor: Cursor, ch: string) => { if (ch !== '$') this.write(cursor, ch); else if (leftEnd.isEmpty()) { - cursor.insRightOf(leftEnd.parent!); + if (leftEnd.parent) cursor.insRightOf(leftEnd.parent); leftEnd.parent?.deleteTowards(L, cursor); new VanillaSymbol('\\$', '$').createLeftOf(cursor.show()); - } else if (!cursor[R]) cursor.insRightOf(leftEnd.parent!); - else if (!cursor[L]) cursor.insLeftOf(leftEnd.parent!); + } else if (!cursor[R] && leftEnd.parent) cursor.insRightOf(leftEnd.parent); + else if (!cursor[L] && leftEnd.parent) cursor.insLeftOf(leftEnd.parent); else this.write(cursor, ch); }; } diff --git a/src/commands/mathElements.ts b/src/commands/mathElements.ts index 0c479584..4981b796 100644 --- a/src/commands/mathElements.ts +++ b/src/commands/mathElements.ts @@ -1,18 +1,7 @@ // Abstract classes of math blocks and commands. import type { Direction, Constructor } from 'src/constants'; -import { - noop, - L, - R, - mqCmdId, - pray, - mqBlockId, - LatexCmds, - OPP_BRACKS, - BuiltInOpNames, - TwoWordOpNames -} from 'src/constants'; +import { noop, L, R, mqCmdId, mqBlockId, LatexCmds, OPP_BRACKS, BuiltInOpNames, TwoWordOpNames } from 'src/constants'; import { Parser } from 'services/parser.util'; import { Selection } from 'src/selection'; import { deleteSelectTowardsMixin, DelimsMixin } from 'src/mixins'; @@ -69,7 +58,7 @@ export class MathElement extends TNode { // down to cutoff, removing anything deeper. while (queue.length) { const current = queue.shift(); - current?.[0].children()?.each((child: TNode) => { + current?.[0].children().each((child: TNode) => { const i = child instanceof MathBlock ? 1 : 0; depth = current[1] + i; @@ -78,6 +67,7 @@ export class MathElement extends TNode { } else { (i ? child.children() : child).remove(); } + return true; }); } } @@ -150,10 +140,11 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { } placeCursor(cursor: Cursor) { + if (!this.ends[L]) return; // Insert the cursor at the right end of the first empty child, searching from // left to right, or if not empty, then to the right end of the child. cursor.insAtRightEnd( - this.foldChildren(this.ends[L]!, (leftward, child) => (leftward.isEmpty() ? leftward : child)) + this.foldChildren(this.ends[L], (leftward, child) => (leftward.isEmpty() ? leftward : child)) ); } @@ -206,6 +197,7 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { block.seek(pageX, cursor); return false; } + return true; }); } @@ -272,14 +264,14 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { // - All open tags should have matching close tags, which means our inner // loop should always encounter a close tag and drop nesting to 0. If // a close tag is missing, the loop will continue until i >= tokens.length - // and token becomes undefined. This will not infinite loop, even in - // production without pray(), because it will then TypeError on .slice(). + // and token becomes undefined. This will loop infinitely, because it + // will then TypeError on .slice(). const blocks = this.blocks; - const cmdId = ` ${mqCmdId}=${this.id}`; + const cmdId = ` ${mqCmdId}=${this.id.toString()}`; const tokens: string[] = this.htmlTemplate.match(/<[^<>]+>|[^<>]+/g) as string[]; - pray('no unmatched angle brackets', tokens.join('') === this.htmlTemplate); + if (tokens.join('') !== this.htmlTemplate) throw new Error('no unmatched angle brackets'); // add cmdId to all top-level tags for (let i = 0, token = tokens[0]; token; ++i, token = tokens[i]) { @@ -289,7 +281,7 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { } // top-level open tags else if (token.startsWith('<')) { - pray('not an unmatched top-level close tag', token.charAt(1) !== '/'); + if (token.charAt(1) === '/') throw new Error('unmatched top-level close tag'); tokens[i] = `${token.slice(0, -1)}${cmdId}>`; @@ -298,7 +290,7 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { do { i += 1; token = tokens[i]; - pray('no missing close tags', !!token); + if (!token) throw new Error('missing close tags'); // close tags if (token.startsWith('&(\d+)/g, - (_$0, $1: number) => ` ${mqBlockId}=${blocks[$1]?.id ?? ''}>${blocks[$1]?.join('html') ?? ''}` + (_$0, $1: number) => + ` ${mqBlockId}=${blocks[$1]?.id.toString() ?? ''}>${blocks[$1]?.join('html') ?? ''}` ); } @@ -403,7 +396,7 @@ export class BinaryOperator extends Symbol { isUnary = false; constructor(ctrlSeq: string, html?: string, text?: string, useRawHtml = false) { - super(ctrlSeq, useRawHtml === true ? html : `${html ?? ''}`, text); + super(ctrlSeq, useRawHtml ? html : `${html ?? ''}`, text); } } @@ -472,7 +465,7 @@ export class Equality extends BinaryOperator { createLeftOf(cursor: Cursor) { if (cursor[L] instanceof Inequality && cursor[L].strict) { cursor[L].swap(false); - cursor[L]?.bubble('reflow'); + cursor[L].bubble('reflow'); return; } super.createLeftOf(cursor); @@ -484,14 +477,12 @@ export class Digit extends VanillaSymbol { 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)) + ((cursor[L] instanceof Variable && cursor[L].isItalic) || + (cursor[L] instanceof SupSub && cursor[L][L] instanceof Variable && cursor[L][L].isItalic)) ) { new LatexCmds._().createLeftOf(cursor); super.createLeftOf(cursor); - cursor.insRightOf(cursor.parent!.parent!); + if (cursor.parent?.parent) cursor.insRightOf(cursor.parent.parent); } else super.createLeftOf(cursor); } } @@ -523,7 +514,9 @@ export class Letter extends Variable { constructor(ch: string, htmlTemplate?: string) { super(ch, htmlTemplate); this.letter = ch; - this.siblingDeleted = this.siblingCreated = (opts: Options, dir?: Direction) => this.finalizeTree(opts, dir); + this.siblingDeleted = this.siblingCreated = (opts: Options, dir?: Direction) => { + this.finalizeTree(opts, dir); + }; } createLeftOf(cursor: Cursor) { @@ -537,7 +530,7 @@ export class Letter extends Variable { l: TNode | undefined = this, i = 0; // FIXME: l.ctrlSeq === l.letter checks if first or last in an operator name - while (l instanceof Letter && l?.ctrlSeq === l?.letter && i < maxLength) { + while (l instanceof Letter && l.ctrlSeq === l.letter && i < maxLength) { str = l.letter + str; l = l[L]; ++i; @@ -549,7 +542,8 @@ export class Letter extends Variable { for (i = 1, l = this; i < str.length; ++i, l = l?.[L]); new Fragment(l, this).remove(); cursor[L] = l?.[L]; - return new LatexCmds[str](str).createLeftOf(cursor); + new LatexCmds[str](str).createLeftOf(cursor); + return; } str = str.slice(1); } @@ -586,6 +580,7 @@ export class Letter extends Variable { new Fragment(l?.[R] || this.parent?.ends[L], r?.[L] || this.parent?.ends[R]).each((el: TNode) => { (el as Letter).italicize(true).elements.removeClass('mq-first', 'mq-last', 'mq-followed-by-supsub'); el.ctrlSeq = (el as Letter).letter; + return true; }); // check for operator names: at each position from left to right, check @@ -600,7 +595,7 @@ export class Letter extends Variable { last = letter; } - const isBuiltIn = BuiltInOpNames[word]; + const isBuiltIn = BuiltInOpNames[word] as 1 | undefined; (first as Letter).ctrlSeq = (isBuiltIn ? '\\' : '\\operatorname{') + (first?.ctrlSeq ?? ''); (last as Letter).ctrlSeq += isBuiltIn ? ' ' : '}'; if (word in TwoWordOpNames) last?.[L]?.[L]?.[L]?.elements.addClass('mq-last'); @@ -628,7 +623,8 @@ export class Letter extends Variable { export function insLeftOfMeUnlessAtEnd(this: SupSub, cursor: Cursor) { // cursor.insLeftOf(cmd), unless cursor at the end of block, and every // ancestor cmd is at the end of every ancestor block - const cmd = this.parent!; + const cmd = this.parent; + if (!cmd) return; let ancestorCmd: TNode | Point | undefined = cursor; do { if (ancestorCmd?.[R]) return cursor.insLeftOf(cmd); @@ -690,9 +686,9 @@ export class Fraction extends MathCommand { return !needParens; }); - const blankDefault = dir === L ? 0 : 1; + const blankDefault = dir === L ? '0' : '1'; const l = this.ends[dir]?.text() !== ' ' && this.ends[dir]?.text(); - return l ? (needParens ? `(${l})` : l) : blankDefault; + return l ? ((needParens as boolean) ? `(${l})` : l) : blankDefault; }; return (leftward instanceof BinaryOperator && leftward.isUnary) || leftward?.elements.hasClass('mq-operator-name') || @@ -702,8 +698,10 @@ export class Fraction extends MathCommand { } finalizeTree() { - this.upInto = this.ends[R]!.upOutOf = this.ends[L]; - this.downInto = this.ends[L]!.downOutOf = this.ends[R]; + this.upInto = this.ends[L]; + if (this.ends[R]) this.ends[R].upOutOf = this.ends[L]; + this.downInto = this.ends[R]; + if (this.ends[L]) this.ends[L].downOutOf = this.ends[R]; } } @@ -743,7 +741,7 @@ export const supSubText = (prefix: string, block?: TNode) => { }); const l = block?.text() !== ' ' && block?.text(); - return l ? prefix + (needParens ? `(${l})` : l) : ''; + return l ? prefix + ((needParens as boolean) ? `(${l})` : l) : ''; }; export class SupSub extends MathCommand { @@ -754,12 +752,12 @@ export class SupSub extends MathCommand { this.reflow = () => { const block = this.elements; // mq-supsub - const prev = block.first.previousElementSibling as HTMLElement; + const prev = block.first.previousElementSibling; // We can't normalize the superscript without having a previous element (which is the base). if (!prev) return; - const sup = block.children('.mq-sup').firstElement; // mq-supsub -> mq-sup + const sup = block.children('.mq-sup').firstElement as HTMLElement | undefined; // mq-supsub -> mq-sup if (sup) { const supStyle = getComputedStyle(sup); const supRect = sup.getBoundingClientRect(); @@ -771,7 +769,7 @@ export class SupSub extends MathCommand { const needed = sup_bottom - prev.getBoundingClientRect().top - 0.7 * sup_fontsize; const cur_margin = parseInt(supStyle.marginBottom); // Lift the superscript up with margin-bottom. - sup.style.marginBottom = `${cur_margin + needed}px`; + sup.style.marginBottom = `${(cur_margin + needed).toString()}px`; } }; @@ -799,7 +797,7 @@ export class SupSub extends MathCommand { (leftward && leftward.ctrlSeq !== '\\ ' && !(leftward instanceof BinaryOperator) && - !/^[,;:]$/.test(leftward.ctrlSeq ?? '')) || + !/^[,;:]$/.test(leftward.ctrlSeq)) || (!leftward && parent?.parent instanceof MathFunction && parent == parent.parent.blocks[0]) ); } @@ -819,12 +817,12 @@ export class SupSub extends MathCommand { } if (this.replacedFragment) { - this.replacedFragment.adopt(cursor.parent!, cursor[L], cursor[R]); + if (cursor.parent) this.replacedFragment.adopt(cursor.parent, cursor[L], cursor[R]); cursor[L] = this.replacedFragment.ends[R]; } } - contactWeld(cursor: Cursor) { + contactWeld(cursor?: Cursor) { // Look on either side for a SupSub. If one is found compare this .sub, .sup with its .sub, .sup. If this has // one that it doesn't, then call .addBlock() on it with this block. If this has one that it also has, then // insert this block's children into its block, unless this block has none, in which case insert the cursor into @@ -853,7 +851,7 @@ export class SupSub extends MathCommand { if (cursor[L] === this) { if (dir === R && pt) { if (pt[L]) cursor.insRightOf(pt[L]); - else cursor.insAtLeftEnd(pt.parent!); + else if (pt.parent) cursor.insAtLeftEnd(pt.parent); } else cursor.insRightOf(this[dir] as TNode); } else { if (pt?.[R]) cursor.insRightOf(pt[R]); @@ -863,7 +861,7 @@ export class SupSub extends MathCommand { } } - this.maybeFlatten(cursor.options); + if (cursor) this.maybeFlatten(cursor.options); // Only allow deletion of a sup or sub in a function supsub block when it is empty. // This sets that up when the first sup or sub is created. @@ -885,7 +883,7 @@ export class SupSub extends MathCommand { } finalizeTree() { - this.ends[L]!.isSupSubLeft = true; + if (this.ends[L]) this.ends[L].isSupSubLeft = true; } // Check to see if this has an invalid base. If so bring the children out, and remove this SupSub. @@ -896,12 +894,13 @@ export class SupSub extends MathCommand { for (const supsub of ['sub', 'sup'] as (keyof Pick)[]) { const src = this[supsub]; if (!src) continue; - src.children().disown().adopt(this.parent!, this[L], this).elements.insDirOf(L, this.elements); + if (this.parent) + src.children().disown().adopt(this.parent, this[L], this).elements.insDirOf(L, this.elements); } this.remove(); if (leftward === cursor?.[L]) { - if (cursor?.[L]?.[R]) cursor.insLeftOf(cursor[L]?.[R]); - else cursor?.insAtDirEnd(L, cursor.parent!); + if (cursor?.[L]?.[R]) cursor.insLeftOf(cursor[L][R]); + else if (cursor?.parent) cursor.insAtDirEnd(L, cursor.parent); } } } @@ -943,7 +942,8 @@ export class SupSub extends MathCommand { addBlock(block: TNode) { if (this.supsub === 'sub') { - this.sup = this.upInto = this.sub!.upOutOf = block; + this.sup = this.upInto = block; + if (this.sub) this.sub.upOutOf = block; block.adopt(this, this.sub).downOutOf = this.sub; const blockEl = document.createElement('span'); @@ -953,7 +953,8 @@ export class SupSub extends MathCommand { this.elements.firstElement.prepend(blockEl); block.elements = new VNode(blockEl); } else { - this.sub = this.downInto = this.sup!.downOutOf = block; + this.sub = this.downInto = block; + if (this.sup) this.sup.downOutOf = block; block.adopt(this, undefined, this.sup).upOutOf = this.sup; const blockEl = document.createElement('span'); @@ -983,13 +984,15 @@ export class SupSub extends MathCommand { if (thisSupsub.isEmpty()) { cursor[dir === L ? R : L] = thisSupsub.ends[dir]; this.supsub = oppositeSupsub; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this[supsub]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this[`${updown}Into`]; const remainingSupsub = this[oppositeSupsub] as SupSub; remainingSupsub[`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; remainingSupsub.deleteOutOf = (dir: Direction, cursor: Cursor) => { if (remainingSupsub.isEmpty()) this.remove(); - cursor.insAtDirEnd(dir, this.parent!); + if (this.parent) cursor.insAtDirEnd(dir, this.parent); }; if (supsub === 'sub') { this.elements.addClass('mq-sup-only'); @@ -997,24 +1000,30 @@ export class SupSub extends MathCommand { } thisSupsub.remove(); } - cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent!); + if (thisSupsub.parent) + cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent); }; } else { thisSupsub.deleteOutOf = (dir: Direction, cursor: Cursor) => { - cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent!); + if (thisSupsub.parent) + cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent); if (!thisSupsub.isEmpty()) { const end = thisSupsub.ends[dir]; - thisSupsub - .children() - .disown() - .withDirAdopt(dir, cursor.parent!, cursor[dir], cursor[dir === L ? R : L]) - .elements.insDirOf(dir === L ? R : L, cursor.element); + if (cursor.parent) { + thisSupsub + .children() + .disown() + .withDirAdopt(dir, cursor.parent, cursor[dir], cursor[dir === L ? R : L]) + .elements.insDirOf(dir === L ? R : L, cursor.element); + } cursor[dir === L ? R : L] = end; } this.supsub = oppositeSupsub; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this[supsub]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this[`${updown}Into`]; - this[oppositeSupsub]![`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; + if (this[oppositeSupsub]) this[oppositeSupsub][`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; delete (this[oppositeSupsub] as Partial).deleteOutOf; if (supsub === 'sub') { this.elements.addClass('mq-sup-only'); @@ -1060,8 +1069,8 @@ export class UpperLowerLimitCommand extends MathCommand { finalizeTree() { this.downInto = this.ends[L]; this.upInto = this.ends[R]; - this.ends[L]!.upOutOf = this.ends[R]; - this.ends[R]!.downOutOf = this.ends[L]; + if (this.ends[L]) this.ends[L].upOutOf = this.ends[R]; + if (this.ends[R]) this.ends[R].downOutOf = this.ends[L]; } } @@ -1128,10 +1137,10 @@ const BracketMixin = >(Base: TBase) => side = this.side = brack.side === L ? R : L; // Move the stuff between this bracket and the ghost of the other bracket outside. - if (brack === cursor.parent?.parent && cursor[side]) { - new Fragment(cursor[side], cursor.parent?.ends[side], side === L ? R : L) + if (brack === cursor.parent?.parent && cursor[side] && brack.parent) { + new Fragment(cursor[side], cursor.parent.ends[side], side === L ? R : L) .disown() - .withDirAdopt(side === L ? R : L, brack.parent!, brack, brack[side]) + .withDirAdopt(side === L ? R : L, brack.parent, brack, brack[side]) .elements.insDirOf(side, brack.elements); } @@ -1182,30 +1191,33 @@ const BracketMixin = >(Base: TBase) => brack.replaces( new Fragment(cursor[side === L ? R : L], cursor.parent?.ends[side === L ? R : L], side) ); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete cursor[side === L ? R : L]; } super.createLeftOf(cursor); } - if (side === L) cursor.insAtLeftEnd(brack.ends[L]!); + if (side === L && brack.ends[L]) cursor.insAtLeftEnd(brack.ends[L]); else cursor.insRightOf(brack); } unwrap() { - const node = this.blocks[this.contentIndex].children().disown().adopt(this.parent!, this, this[R]); + const node = this.parent + ? this.blocks[this.contentIndex].children().disown().adopt(this.parent, this, this[R]) + : undefined; if (node) this.elements.last.after(...node.elements.contents); this.remove(); } deleteSide(side: Direction, outward: boolean, cursor: Cursor) { - const parent = this.parent!, + const parent = this.parent, sib = this[side], - farEnd = parent.ends[side]; + farEnd = parent?.ends[side]; if (side === this.side) { // If deleting a non-ghost of a one-sided bracket, then unwrap the bracket. this.unwrap(); if (sib) cursor.insDirOf(side === L ? R : L, sib); - else cursor.insAtDirEnd(side, parent); + else if (parent) cursor.insAtDirEnd(side, parent); return; } @@ -1219,7 +1231,7 @@ const BracketMixin = >(Base: TBase) => this.unwrap(); origEnd?.siblingCreated?.(cursor.options, side); if (sib) cursor.insDirOf(side === L ? R : L, sib); - else cursor.insAtDirEnd(side, parent); + else if (parent) cursor.insAtDirEnd(side, parent); } else { // If deleting a like, inner close-brace of ([1+2}+3) where the outer open-paren is a ghost, // then become [1+3+3). @@ -1230,7 +1242,7 @@ const BracketMixin = >(Base: TBase) => // If deleting outward from a solid pair, unwrap. this.unwrap(); if (sib) cursor.insDirOf(side === L ? R : L, sib); - else cursor.insAtDirEnd(side, parent); + else if (parent) cursor.insAtDirEnd(side, parent); return; } else { // If deleting just one of a pair of brackets, become one-sided. @@ -1270,8 +1282,9 @@ const BracketMixin = >(Base: TBase) => finalizeTree() { if (!this.inserted) { - this.blocks[this.contentIndex].deleteOutOf = (dir: Direction, cursor: Cursor) => + this.blocks[this.contentIndex].deleteOutOf = (dir: Direction, cursor: Cursor) => { this.deleteSide(dir, true, cursor); + }; this.inserted = true; return; } @@ -1320,13 +1333,11 @@ export class Bracket extends BracketMixin(MathCommand) { } latex() { - return `\\left${this.sides[L]?.ctrlSeq ?? ''}${this.ends[L]?.latex() ?? ''}\\right${ - this.sides[R]?.ctrlSeq ?? '' - }`; + return `\\left${this.sides[L].ctrlSeq}${this.ends[L]?.latex() ?? ''}\\right${this.sides[R].ctrlSeq}`; } text() { - return `${this.sides[L]?.ch ?? ''}${this.ends[L]?.text() ?? ''}${this.sides[R]?.ch ?? ''}`; + return `${this.sides[L].ch}${this.ends[L]?.text() ?? ''}${this.sides[R].ch}`; } } @@ -1346,7 +1357,9 @@ export class MathFunction extends BracketMixin(MathCommand) { if (dir === R) this.finalizeTree(); else this.updateFirst(); }; - this.siblingDeleted = () => this.updateFirst(); + this.siblingDeleted = () => { + this.updateFirst(); + }; } // Add or remove padding depending on what is before the function name. @@ -1390,9 +1403,13 @@ export class MathFunction extends BracketMixin(MathCommand) { // If at the left end of the supsub block and a character was typed that extends this function name to // another one, then extend the function name. - if (!cursor[L] && LatexCmds[`${this.ctrlSeq.slice(1)}${ch}`]?.prototype instanceof MathFunction) { + if ( + !cursor[L] && + (LatexCmds[`${this.ctrlSeq.slice(1)}${ch}`] as Constructor | undefined)?.prototype instanceof + MathFunction + ) { this.ctrlSeq = `${this.ctrlSeq}${ch}`; - this.elements.children().first.textContent += ch; + this.elements.children().first.textContent = (this.elements.children().first.textContent ?? '') + ch; this.bubble('reflow'); return true; } @@ -1467,7 +1484,11 @@ export class MathFunction extends BracketMixin(MathCommand) { this.blocks[0].deleteOutOf = (dir: Direction, cursor: Cursor) => { // If at the left end of the supsub block and removing the last character of this function name is still // a valid function name, then shorten the function name. - if (!cursor[L] && LatexCmds[this.ctrlSeq.slice(1, -1)]?.prototype instanceof MathFunction) { + if ( + !cursor[L] && + (LatexCmds[this.ctrlSeq.slice(1, -1)] as Constructor | undefined)?.prototype instanceof + MathFunction + ) { this.ctrlSeq = this.ctrlSeq.slice(0, -1); this.elements.children().first.textContent = this.ctrlSeq.slice(1); this.bubble('reflow'); @@ -1481,7 +1502,7 @@ export class MathFunction extends BracketMixin(MathCommand) { this.blocks[1].deleteOutOf = (dir: Direction, cursor: Cursor) => { // If deleting left out of the content block, move the cursor to the supsub block. - if (dir === L) cursor?.insAtRightEnd(this.blocks[0]); + if (dir === L) cursor.insAtRightEnd(this.blocks[0]); else this.deleteSide(dir, true, cursor); }; } @@ -1606,7 +1627,7 @@ export const latexMathParser = (() => { ) ) .then((ctrlSeq: string) => { - const cmdKlass = LatexCmds[ctrlSeq]; + const cmdKlass = LatexCmds[ctrlSeq] as Constructor | undefined; if (cmdKlass) { return (new cmdKlass(ctrlSeq) as MathCommand).parser(); diff --git a/src/commands/textBlock.ts b/src/commands/textBlock.ts index 405a55b5..f591d58a 100644 --- a/src/commands/textBlock.ts +++ b/src/commands/textBlock.ts @@ -9,7 +9,8 @@ import { RootMathCommand } from 'commands/mathBlock'; export class RootTextBlock extends RootMathBlock { keystroke(key: string, e: KeyboardEvent, ctrlr: Controller) { if (key === 'Spacebar' || key === 'Shift-Spacebar') return; - return super.keystroke(key, e, ctrlr); + super.keystroke(key, e, ctrlr); + return; } write(cursor: Cursor, ch: string) { diff --git a/src/commands/textElements.ts b/src/commands/textElements.ts index 7414685c..f85384b5 100644 --- a/src/commands/textElements.ts +++ b/src/commands/textElements.ts @@ -1,7 +1,7 @@ // Elements for abstract classes of text blocks import type { Direction } from 'src/constants'; -import { mqCmdId, L, R, pray, prayDirection, LatexCmds, CharCmds } from 'src/constants'; +import { mqCmdId, L, R, LatexCmds, CharCmds } from 'src/constants'; import { Parser } from 'services/parser.util'; import type { Cursor } from 'src/cursor'; import { Point } from 'tree/point'; @@ -78,7 +78,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { } html() { - return `${this.textContents()}`; + return `${this.textContents()}`; } // editability methods: called by the cursor for editing, cursor movements, @@ -132,9 +132,9 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { else { // split apart const leftBlock = new TextBlock(); - const leftPc = this.ends[L]!; - leftPc.disown().elements.detach(); - leftPc.adopt(leftBlock); + const leftPc = this.ends[L]; + leftPc?.disown().elements.detach(); + leftPc?.adopt(leftBlock); cursor.insLeftOf(this); super.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness @@ -150,7 +150,8 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { seek(pageX: number, cursor: Cursor) { cursor.hide(); - const textPc = this.fuseChildren()!; + const textPc = this.fuseChildren(); + if (!textPc) return; // Insert cursor at approx position in DOMTextNode const cursorStyle = getComputedStyle(this.elements.firstElement); @@ -172,7 +173,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { let prevDispl = dir; // displ * prevDispl > 0 iff displacement direction === previous direction while (cursor[dir] && displ * prevDispl > 0) { - cursor[dir]?.moveTowards(dir, cursor); + cursor[dir].moveTowards(dir, cursor); prevDispl = displ; displ = pageX - cursor.offset().left; } @@ -217,17 +218,18 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { this.fuseChildren(); } - this?.getController()?.handle('textBlockExit'); + this.getController()?.handle('textBlockExit'); } fuseChildren() { this.elements.first.normalize(); - const textPcDom = this.elements.first.firstChild as Text; + const textPcDom = this.elements.first.firstChild as Text | undefined; if (!textPcDom) return; - pray('only node in TextBlock span is Text node', textPcDom.nodeType === 3); + // nodeType === 3 is a text node. - // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + // See https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType. + if (textPcDom.nodeType !== 3) throw new Error('only node in TextBlock span must be a Text node'); const textPc = new TextPiece(textPcDom.data); textPc.addToElements(textPcDom); @@ -279,15 +281,16 @@ class TextPiece extends TNode { this.dom?.insertData(0, text); } - insTextAtDirEnd(text: string, dir: Direction) { - prayDirection(dir); + insTextAtDirEnd(text: string, dir: Direction | undefined) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); if (dir === R) this.appendText(text); else this.prependText(text); } splitRight(i: number) { - const newPc = new TextPiece(this.textStr.slice(i)).adopt(this.parent!, this, this[R]); - newPc.addToElements(this.dom!.splitText(i)); + const newPc = new TextPiece(this.textStr.slice(i)); + if (this.parent) newPc.adopt(this.parent, this, this[R]); + if (this.dom) newPc.addToElements(this.dom.splitText(i)); this.textStr = this.textStr.slice(0, i); return newPc; } @@ -296,16 +299,16 @@ class TextPiece extends TNode { return text.charAt(dir === L ? 0 : -1 + text.length); } - moveTowards(dir: Direction, cursor: Cursor) { - prayDirection(dir); + moveTowards(dir: Direction | undefined, cursor: Cursor) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const ch = this.endChar(dir === L ? R : L, this.textStr); - const from = this[dir === L ? R : L] as TextPiece; + const from = this[dir === L ? R : L] as TextPiece | undefined; if (from) from.insTextAtDirEnd(ch, dir); else new TextPiece(ch).createDir(dir === L ? R : L, cursor); - return this.deleteTowards(dir, cursor); + this.deleteTowards(dir, cursor); } latex() { @@ -330,8 +333,8 @@ class TextPiece extends TNode { } } - selectTowards(dir: Direction, cursor: Cursor) { - prayDirection(dir); + selectTowards(dir: Direction | undefined, cursor: Cursor) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const anticursor = cursor.anticursor; const ch = this.endChar(dir === L ? R : L, this.textStr); @@ -341,7 +344,7 @@ class TextPiece extends TNode { anticursor[dir] = newPc; cursor.insDirOf(dir, newPc); } else { - const from = this[dir === L ? R : L] as TextPiece; + const from = this[dir === L ? R : L] as TextPiece | undefined; if (from) from.insTextAtDirEnd(ch, dir); else { const newPc = new TextPiece(ch).createDir(dir === L ? R : L, cursor); @@ -353,7 +356,7 @@ class TextPiece extends TNode { } } - return this.deleteTowards(dir, cursor); + this.deleteTowards(dir, cursor); } } diff --git a/src/constants.ts b/src/constants.ts index 9d8f1b11..2daa1eaf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,21 +40,18 @@ export const noop = () => { // 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. -type CallableKeyOf = keyof { - [P in keyof S as S[P] extends (arg1?: U, arg2?: V) => T ? P : never]: unknown; +type CallableKeyOf = keyof { + [P in keyof S as S[P] extends (arg1?: unknown, arg2?: unknown) => T ? P : never]: unknown; }; -export const iterator = (generator: (yield_: (obj: R) => S | undefined) => T) => { - return (fn: ((obj: R) => S) | string, arg1?: U, arg2?: V) => { +export const iterator = (generator: (yield_: (obj: R) => S | undefined) => T) => { + return (fn: ((obj: R) => S) | string, ...args: unknown[]) => { const yield_ = typeof fn === 'function' ? fn : (obj: R) => { if (fn in obj) - return (obj[fn as CallableKeyOf] as unknown as (arg1?: U, arg2?: V) => S)( - arg1, - arg2 - ); + return (obj[fn as CallableKeyOf] as unknown as (...args: unknown[]) => S)(...args); }; return generator(yield_); }; @@ -73,45 +70,24 @@ export const bindMixin = >( } }; -// a development-only debug method. This definition and all -// calls to `pray` will be stripped from the minified -// build of mathquill. -// -// This function must be called by name to be removed -// at compile time. Do not define another function -// with the same name, and only call this function by -// name. -export const pray = (message: string, cond = false) => { - if (!cond) throw new Error(`prayer failed: ${message}`); -}; - -export const prayDirection = (dir: Direction) => { - pray('a direction was passed', dir === L || dir === R); -}; - export const prayWellFormed = (parent?: TNode, leftward?: TNode, rightward?: TNode) => { - pray('a parent is always present', !!parent); - pray( - 'leftward is properly set up', - (() => { - // either it's empty and `rightward` is the left end child (possibly empty) - if (!leftward) return parent?.ends[L] === rightward; - - // or it's there and its [R] and .parent are properly set up - return leftward[R] === rightward && leftward.parent === parent; - })() - ); - - pray( - 'rightward is properly set up', - (() => { - // either it's empty and `leftward` is the right end child (possibly empty) - if (!rightward) return parent?.ends[R] === leftward; - - // or it's there and its [L] and .parent are properly set up - return rightward[L] === leftward && rightward.parent === parent; - })() - ); + if (!parent) throw new Error('a parent must be present'); + + // Either leftward is empty and `rightward` is the left end child (possibly empty) + // or leftward is there and leftward's [R] and .parent are properly set up. + if ( + (!leftward && parent.ends[L] !== rightward) || + (leftward && (leftward[R] !== rightward || leftward.parent !== parent)) + ) + throw new Error('leftward is not properly set up'); + + // Either rightward is empty and `leftward` is the right end child (possibly empty) + // or rightward is there and rightward's [L] and .parent are properly set up. + if ( + (!rightward && parent.ends[R] !== leftward) || + (rightward && (rightward[L] !== leftward || rightward.parent !== parent)) + ) + throw new Error('rightward is not properly set up'); }; // Registry of LaTeX commands and commands created when typing a single character. diff --git a/src/controller.ts b/src/controller.ts index 6657df5b..623121fa 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -1,7 +1,7 @@ // Controller for a MathQuill instance, on which services are registered. import type { Direction } from 'src/constants'; -import { L, R, prayDirection } from 'src/constants'; +import { L, R } from 'src/constants'; import type { Handler, DirectionHandler, Handlers, Options } from 'src/options'; import { Cursor } from 'src/cursor'; import type { AbstractMathQuill } from 'src/abstractFields'; @@ -39,9 +39,9 @@ export class ControllerBase { handle(name: keyof Handlers, dir?: Direction) { const handlers = this.options.handlers; - if (handlers?.[name]) { - if (dir === L || dir === R) (handlers[name] as DirectionHandler)?.(dir, this.apiClass!); - else (handlers[name] as Handler)?.(this.apiClass!); + if (handlers?.[name] && this.apiClass) { + if (dir === L || dir === R) (handlers[name] as DirectionHandler)(dir, this.apiClass); + else (handlers[name] as Handler)(this.apiClass); } } @@ -70,8 +70,8 @@ export class Controller extends ExportText( root.controller = this; } - escapeDir(dir: Direction, _key: string, e: KeyboardEvent) { - prayDirection(dir); + escapeDir(dir: Direction | undefined, _key: string, e: KeyboardEvent) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const cursor = this.cursor; // only prevent default of Tab if not in the root editable @@ -85,14 +85,14 @@ export class Controller extends ExportText( return this.notify('move'); } - moveDir(dir: Direction) { - prayDirection(dir); + moveDir(dir: Direction | undefined) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes; if (cursor.selection) { - cursor.insDirOf(dir, cursor.selection.ends[dir]!); - } else if (cursor[dir]) cursor[dir]?.moveTowards(dir, cursor, updown); + if (cursor.selection.ends[dir]) cursor.insDirOf(dir, cursor.selection.ends[dir]); + } else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown); else cursor.parent?.moveOutOf(dir, cursor, updown); return this.notify('move'); @@ -118,8 +118,8 @@ export class Controller extends ExportText( const cursor = this.notify('upDown').cursor; const dirInto: keyof TNode = `${dir}Into`, dirOutOf: keyof TNode = `${dir}OutOf`; - if (cursor[R]?.[dirInto]) cursor.insAtLeftEnd(cursor[R]?.[dirInto]); - else if (cursor[L]?.[dirInto]) cursor.insAtRightEnd(cursor[L]?.[dirInto]); + if (cursor[R]?.[dirInto]) cursor.insAtLeftEnd(cursor[R][dirInto]); + else if (cursor[L]?.[dirInto]) cursor.insAtRightEnd(cursor[L][dirInto]); else { cursor.parent?.bubble((ancestor: TNode) => { if (ancestor[dirOutOf]) { @@ -139,14 +139,14 @@ export class Controller extends ExportText( return this.moveUpDown('down'); } - deleteDir(dir: Direction) { - prayDirection(dir); + deleteDir(dir: Direction | undefined) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const cursor = this.cursor; const hadSelection = cursor.selection; this.notify('edit'); // Shows the cursor and deletes a selection if present. if (!hadSelection) { - if (cursor[dir]) cursor[dir]?.deleteTowards(dir, cursor); + if (cursor[dir]) cursor[dir].deleteTowards(dir, cursor); else cursor.parent?.deleteOutOf(dir, cursor); } @@ -161,8 +161,8 @@ export class Controller extends ExportText( return this; } - ctrlDeleteDir(dir: Direction) { - prayDirection(dir); + ctrlDeleteDir(dir: Direction | undefined) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); const cursor = this.cursor; if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); @@ -172,7 +172,7 @@ export class Controller extends ExportText( } else { new Fragment(cursor[R], cursor.parent?.ends[R]).remove(); } - cursor.insAtDirEnd(dir, cursor.parent!); + if (cursor.parent) cursor.insAtDirEnd(dir, cursor.parent); // Call the contactWeld for a SupSub so that it can deal with having its base deleted. cursor[R]?.postOrder('contactWeld', cursor); @@ -192,10 +192,10 @@ export class Controller extends ExportText( return this.deleteDir(R); } - selectDir(dir: Direction) { + selectDir(dir: Direction | undefined) { const cursor = this.notify('select').cursor, seln = cursor.selection; - prayDirection(dir); + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); if (!cursor.anticursor) cursor.startSelection(); @@ -204,7 +204,7 @@ export class Controller extends ExportText( // "if node we're selecting towards is inside selection (hence retracting) // and is on the *far side* of the selection (hence is only node selected) // and the anticursor is *inside* that node, not just on the other side" - if (seln && seln?.ends[dir] === node && cursor.anticursor?.[dir === L ? R : L] !== node) { + if (seln && seln.ends[dir] === node && cursor.anticursor?.[dir === L ? R : L] !== node) { node.unselectInto(dir, cursor); } else node.selectTowards(dir, cursor); } else cursor.parent?.selectOutOf(dir, cursor); @@ -214,10 +214,10 @@ export class Controller extends ExportText( } selectLeft() { - return this.selectDir(L); + this.selectDir(L); } selectRight() { - return this.selectDir(R); + this.selectDir(R); } } diff --git a/src/cursor.ts b/src/cursor.ts index 00986514..aea7a8b6 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -5,7 +5,7 @@ // A fake cursor in the fake textbox that the math is rendered in. import type { Direction } from 'src/constants'; -import { L, R, pray, prayDirection } from 'src/constants'; +import { L, R } from 'src/constants'; import type { Options } from 'src/options'; import { Point } from 'tree/point'; import type { TNode } from 'tree/node'; @@ -43,7 +43,7 @@ export class Cursor extends Point { if (this.selection && this.selection.ends[L]?.[L] === this[L]) this.selection.elements.first.before(this.element); else this[R].elements.first.before(this.element); - } else this.parent!.elements.firstElement.append(this.element); + } else this.parent?.elements.firstElement.append(this.element); this.parent?.focus(); } this.intervalId = setInterval(this.blink, 500); @@ -59,21 +59,21 @@ export class Cursor extends Point { } withDirInsertAt(dir: Direction, parent: TNode, withDir?: TNode, oppDir?: TNode) { - const oldParent = this.parent!; + const oldParent = this.parent; this.parent = parent; this[dir] = withDir; this[dir === L ? R : L] = oppDir; // By contract blur is called after all has been said and done and the cursor has actually been moved. - if (oldParent !== parent && oldParent.blur) oldParent.blur(this); + if (oldParent !== parent && oldParent?.blur) oldParent.blur(this); } - insDirOf(dir: Direction, el: TNode) { - prayDirection(dir); + insDirOf(dir: Direction | undefined, el: TNode) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); if (dir === L) el.elements.first.before(this.element); else el.elements.last.after(this.element); - this.withDirInsertAt(dir, el.parent!, el[dir], el); + if (el.parent) this.withDirInsertAt(dir, el.parent, el[dir], el); this.parent?.elements.addClass('mq-has-cursor'); return this; } @@ -86,8 +86,8 @@ export class Cursor extends Point { return this.insDirOf(R, el); } - insAtDirEnd(dir: Direction, el: TNode) { - prayDirection(dir); + insAtDirEnd(dir: Direction | undefined, el: TNode) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); if (dir === L) el.elements.firstElement.prepend(this.element); else el.elements.lastElement.append(this.element); @@ -112,10 +112,10 @@ export class Cursor extends Point { // + if not seek a position in the node that is horizontally closest to the cursor's current position jumpUpDown(from: TNode, to: TNode) { this.upDownCache[from.id] = Point.copy(this); - const cached = this.upDownCache[to.id]; + const cached = this.upDownCache[to.id] as Point | undefined; if (cached) { if (cached[R]) this.insLeftOf(cached[R]); - else this.insAtRightEnd(cached.parent!); + else if (cached.parent) this.insAtRightEnd(cached.parent); } else { to.seek(this.offset().left, this); } @@ -126,33 +126,36 @@ export class Cursor extends Point { } unwrapGramp() { - const gramp = this.parent!.parent!; - const greatgramp = gramp.parent!; - const rightward = gramp[R]; - - let leftward = gramp[L]; - gramp.disown().eachChild((uncle: TNode) => { - if (uncle.isEmpty()) return; - - uncle - .children() - .adopt(greatgramp, leftward, rightward) - .each((cousin: TNode) => { - gramp.elements.first.before(...cousin.elements.contents); - }); + const gramp = this.parent?.parent; + const greatgramp = gramp?.parent; + const rightward = gramp?.[R]; + + let leftward = gramp?.[L]; + gramp?.disown().eachChild((uncle: TNode) => { + if (uncle.isEmpty()) return true; + + if (greatgramp) + uncle + .children() + .adopt(greatgramp, leftward, rightward) + .each((cousin: TNode) => { + gramp.elements.first.before(...cousin.elements.contents); + return true; + }); leftward = uncle.ends[R]; + return true; }); if (!this[R]) { // Find something rightward to insert left of. - if (this[L]) this[R] = this[L]?.[R]; + if (this[L]) this[R] = this[L][R]; else { while (!this[R]) { this.parent = this.parent?.[R]; - if (this.parent) this[R] = this.parent?.ends[L]; + if (this.parent) this[R] = this.parent.ends[L]; else { - this[R] = gramp[R]; + this[R] = gramp?.[R]; this.parent = greatgramp; break; } @@ -160,12 +163,12 @@ export class Cursor extends Point { } } if (this[R]) this.insLeftOf(this[R]); - else this.insAtRightEnd(greatgramp); + else if (greatgramp) this.insAtRightEnd(greatgramp); - gramp.elements.remove(); + gramp?.elements.remove(); - if (gramp[L]?.siblingDeleted) gramp[L]?.siblingDeleted?.(this.options, R); - if (gramp[R]?.siblingDeleted) gramp[R]?.siblingDeleted?.(this.options, L); + if (gramp?.[L]?.siblingDeleted) gramp[L].siblingDeleted(this.options, R); + if (gramp?.[R]?.siblingDeleted) gramp[R].siblingDeleted(this.options, L); } startSelection() { @@ -185,8 +188,7 @@ export class Cursor extends Point { select() { if (this[L] === this.anticursor?.[L] && this.parent === this.anticursor?.parent) return false; - pray('selection well formed', !!this.anticursor && !!this.anticursor.ancestors); - if (!this.anticursor?.ancestors) return false; + if (!this.anticursor?.ancestors) throw new Error('selection not well formed'); // Find the lowest common ancestor (`lca`), and the ancestor of the cursor // whose parent is the LCA (which will be an end of the selection fragment). @@ -199,7 +201,7 @@ export class Cursor extends Point { break; } } - pray('cursor and anticursor in the same tree', !!lca); + if (!lca) throw new Error('cursor and anticursor must be in the same tree'); // The cursor and the anticursor should be in the same tree, because the // mousemove handler attached to the document, unlike the one attached to // the root HTML DOM element, doesn't try to get the math tree node of the @@ -208,7 +210,7 @@ export class Cursor extends Point { // The other end of the selection fragment, the ancestor of the anticursor // whose parent is the LCA. - const antiAncestor = this.anticursor.ancestors[lca?.id ?? 0]; + const antiAncestor = this.anticursor.ancestors[lca.id]; // Now we have two either Nodes or Points, guaranteed to have a common // parent and guaranteed that if both are Points, they are not the same, @@ -247,8 +249,9 @@ export class Cursor extends Point { if (leftEnd instanceof Point) leftEnd = leftEnd[R]; if (rightEnd instanceof Point) rightEnd = rightEnd[L]; - this.hide().selection = lca?.selectChildren(leftEnd, rightEnd); - this.insDirOf(dir, this.selection!.ends[dir]!); + this.hide().selection = lca.selectChildren(leftEnd, rightEnd); + const selectionEndDir = this.selection?.ends[dir]; + if (selectionEndDir) this.insDirOf(dir, selectionEndDir); this.selectionChanged?.(); return true; } diff --git a/src/mixins.ts b/src/mixins.ts index 0c68e732..b3463848 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -25,8 +25,8 @@ export const RootBlockMixin = (_: MathElement) => { export const deleteSelectTowardsMixin = >(Base: TBase) => class extends Base { moveTowards(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { - const updownInto = updown && this[`${updown}Into`]; - cursor.insAtDirEnd(dir === L ? R : L, updownInto || this.ends[dir === L ? R : L]!); + const nodeAtEnd = (updown && this[`${updown}Into`]) || this.ends[dir === L ? R : L]; + if (nodeAtEnd) cursor.insAtDirEnd(dir === L ? R : L, nodeAtEnd); } deleteTowards(dir: Direction, cursor: Cursor) { @@ -42,8 +42,11 @@ export const deleteSelectTowardsMixin = >(Base: // Use a CSS transform to scale the HTML elements, // or gracefully degrade to increasing the fontSize to match the vertical Y scaling factor. -export const scale = (elts: HTMLElement[], x: number, y: number) => - elts.forEach((elt) => (elt.style.transform = `scale(${x},${y})`)); +export const scale = (elts: HTMLElement[], x: number, y: number) => { + elts.forEach((elt) => { + elt.style.transform = `scale(${x.toString()},${y.toString()})`; + }); +}; export const DelimsMixin = >(Base: TBase) => class extends Base { diff --git a/src/options.ts b/src/options.ts index c920f14d..0711ae98 100644 --- a/src/options.ts +++ b/src/options.ts @@ -120,6 +120,7 @@ export class Options { if (!this.#_autoCommands) throw 'autoCommands setter not working'; const removeCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of removeCmds) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.#_autoCommands[cmd]; } this.#_autoCommands._maxLength = Object.keys(this.#_autoCommands).reduce( @@ -206,6 +207,7 @@ export class Options { if (!this.#_autoOperatorNames) throw 'autoOperatorNames setter not working'; const removeCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of removeCmds) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.#_autoOperatorNames[cmd]; } this.#_autoOperatorNames._maxLength = Object.keys(this.#_autoOperatorNames).reduce( @@ -254,7 +256,7 @@ export class Options { return this.#_leftRightIntoCmdGoes ?? Options.#leftRightIntoCmdGoes; } set leftRightIntoCmdGoes(updown: 'up' | 'down' | undefined) { - if (updown && updown !== 'up' && updown !== 'down') { + if (updown && updown !== 'up' && (updown as string) !== 'down') { throw `"up" or "down" required for leftRightIntoCmdGoes option, got "${updown as string}"`; } if (this instanceof Options) this.#_leftRightIntoCmdGoes = updown; diff --git a/src/publicapi.ts b/src/publicapi.ts index c9dfaeb9..aec28b2c 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -26,20 +26,21 @@ declare global { const isBrowser = Object.getPrototypeOf(Object.getPrototypeOf(globalThis)) !== Object.prototype; interface MQApi { - (el: unknown): AbstractMathQuill | void; + (el: unknown): AbstractMathQuill | undefined; saneKeyboardEvents: typeof saneKeyboardEvents; config: (opts: InputOptions) => MQApi; registerEmbed: (name: string, options: (data: string) => EmbedOptions) => void; - StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; - MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; - InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; - TextField: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; + StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + TextField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; } // globally exported API object +// eslint-disable-next-line @typescript-eslint/no-extraneous-class export default class MathQuill { - static origMathQuill?: MathQuill = isBrowser ? window.MathQuill : undefined; - static VERSION?: string; + static origMathQuill: MathQuill | undefined = isBrowser ? window.MathQuill : undefined; + static VERSION = '0.0.1'; static getInterface() { const APIClasses: Record = {}; @@ -80,12 +81,12 @@ export default class MathQuill { opts: InputOptions ) => { const mq = MQ(el); - if (mq instanceof APIClasses[kind] || !(el instanceof HTMLElement)) return mq; - const ctrlr = new Controller(new APIClasses[kind].RootBlock(), el, new Options()); + if (mq instanceof APIClass || !(el instanceof HTMLElement)) return mq; + const ctrlr = new Controller(new APIClass.RootBlock(), el, new Options()); ctrlr.KIND_OF_MQ = kind; - return new APIClasses[kind](ctrlr).config(opts).__mathquillify(); + return new APIClass(ctrlr).config(opts).__mathquillify(); }; - Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClasses[kind]); + Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClass); } return MQ as MQApi; diff --git a/src/publicapiBasic.ts b/src/publicapiBasic.ts index b4bbafcc..dee1c4c5 100644 --- a/src/publicapiBasic.ts +++ b/src/publicapiBasic.ts @@ -21,19 +21,20 @@ declare global { } interface MQApi { - (el: unknown): AbstractMathQuill | void; + (el: unknown): AbstractMathQuill | undefined; saneKeyboardEvents: typeof saneKeyboardEvents; config: (opts: InputOptions) => MQApi; registerEmbed: (name: string, options: (data: string) => EmbedOptions) => void; - StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; - MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; - InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | void; + StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; } // globally exported API object +// eslint-disable-next-line @typescript-eslint/no-extraneous-class export default class MathQuill { - static origMathQuill?: MathQuill = window.MathQuill; - static VERSION?: string; + static origMathQuill: MathQuill | undefined = window.MathQuill; + static VERSION = '0.0.1'; static getInterface() { const APIClasses: Record = {}; @@ -74,12 +75,12 @@ export default class MathQuill { opts: InputOptions ) => { const mq = MQ(el); - if (mq instanceof APIClasses[kind] || !(el instanceof HTMLElement)) return mq; - const ctrlr = new Controller(new APIClasses[kind].RootBlock(), el, new Options()); + if (mq instanceof APIClass || !(el instanceof HTMLElement)) return mq; + const ctrlr = new Controller(new APIClass.RootBlock(), el, new Options()); ctrlr.KIND_OF_MQ = kind; - return new APIClasses[kind](ctrlr).config(opts).__mathquillify(); + return new APIClass(ctrlr).config(opts).__mathquillify(); }; - Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClasses[kind]); + Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClass); } return MQ as MQApi; diff --git a/src/services/focusBlur.ts b/src/services/focusBlur.ts index ef9656d2..16e2c302 100644 --- a/src/services/focusBlur.ts +++ b/src/services/focusBlur.ts @@ -16,7 +16,7 @@ export const FocusBlurEvents = >(Base: if (!this.cursor.parent) this.cursor.insAtRightEnd(this.root); if (this.cursor.selection) { this.cursor.selection.elements.removeClass('mq-blur'); - this.selectionChanged?.(); // Re-select textarea contents after tabbing away and back. + this.selectionChanged(); // Re-select textarea contents after tabbing away and back. } else this.cursor.show(); }; diff --git a/src/services/latex.ts b/src/services/latex.ts index 53d38645..43e212ca 100644 --- a/src/services/latex.ts +++ b/src/services/latex.ts @@ -22,10 +22,15 @@ export const LatexControllerExtension = (latex); + const block: MathBlock | undefined = latexMathParser + .skip(Parser.eof) + .or(Parser.all.result(false)) + .parse(latex); this.root.eachChild('postOrder', 'dispose'); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.root.ends[L]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.root.ends[R]; if (block instanceof MathBlock && block.prepareInsertionAt(this.cursor)) { @@ -46,9 +51,13 @@ export const LatexControllerExtension = (el as HTMLElement).remove()); + .forEach((el) => { + (el as HTMLElement).remove(); + }); this.root.eachChild('postOrder', 'dispose'); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.root.ends[L]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.root.ends[R]; delete this.cursor.selection; this.cursor.show().insAtRightEnd(this.root); @@ -74,7 +83,7 @@ export const LatexControllerExtension = & // Drag-to-select event handling this.mouseDownHandler = (e: MouseEvent) => { - const rootEl = (e.target as HTMLElement).closest('.mq-root-block')!; + const rootEl = e.target instanceof HTMLElement ? e.target.closest('.mq-root-block') : null; const root = TNode.byId.get( - parseInt((rootEl?.getAttribute(mqBlockId) || ultimateRootEl?.getAttribute(mqBlockId)) ?? '0') + parseInt((rootEl?.getAttribute(mqBlockId) || ultimateRootEl.getAttribute(mqBlockId)) ?? '0') ); if (!root?.controller) { @@ -50,7 +50,7 @@ export const MouseEventController = & const mousemove = (e: Event) => (target = e.target as HTMLElement); const docmousemove = (e: MouseEvent) => { if (!cursor.anticursor) cursor.startSelection(); - ctrlr.seek(target!, e.pageX ?? 0).cursor.select(); + ctrlr.seek(target, e.pageX).cursor.select(); target = undefined; }; // Outside rootEl, the MathQuill node corresponding to the target (if any) @@ -79,7 +79,7 @@ export const MouseEventController = & // If this is a double click, then select the block that is to the right of the cursor, and return. // Note that the interpretation of what a block is in this situation is not a true MathQuill block. // Rather an attempt is made to select word like blocks. - ctrlr.seek(e.target as HTMLElement, e.pageX ?? 0); + ctrlr.seek(e.target as HTMLElement, e.pageX); if (!cursor[R] && cursor[L]?.parent === root) ctrlr.moveLeft(); if (cursor[R] instanceof Letter) { @@ -95,6 +95,7 @@ export const MouseEventController = & ctrlr.moveLeft(); cursor.startSelection(); while ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition cursor[R] && cursor[R] instanceof Letter && cursor[R].isPartOfOperator === currentNode.isPartOfOperator @@ -104,6 +105,7 @@ export const MouseEventController = & // If a "Digit" is to the right of the cursor, then select all adjacent "Digit"s. while (cursor[L] && cursor[L] instanceof Digit) ctrlr.moveLeft(); cursor.startSelection(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (cursor[R] && cursor[R] instanceof Digit) ctrlr.selectRight(); } else { cursor.startSelection(); @@ -121,12 +123,12 @@ export const MouseEventController = & } if (ctrlr.blurred) { - if (!ctrlr.editable) rootEl?.prepend(textareaSpan!); + if (!ctrlr.editable && textareaSpan) rootEl?.prepend(textareaSpan); textarea?.focus(); } cursor.blink = noop; - ctrlr.seek(e.target as HTMLElement, e.pageX ?? 0).cursor.startSelection(); + ctrlr.seek(e.target as HTMLElement, e.pageX).cursor.startSelection(); rootEl?.addEventListener('mousemove', mousemove); ownerDocument.addEventListener('mousemove', docmousemove); @@ -138,7 +140,7 @@ export const MouseEventController = & this.container.addEventListener('mousedown', this.mouseDownHandler); } - seek(target: HTMLElement, pageX: number) { + seek(target: HTMLElement | undefined, pageX: number) { const cursor = this.notify('select').cursor; let nodeId = 0; @@ -152,14 +154,14 @@ export const MouseEventController = & } } const node = nodeId ? TNode.byId.get(nodeId) : this.root; - pray('nodeId is the id of some TNode that exists', !!node); + if (!node) throw new Error('nodeId is not the id of a TNode that exists'); // Don't clear the selection until after getting node from target, in case // target was selection span. Otherwise target will have no parent and will // seek from root, which is less accurate (e.g. fraction). cursor.clearSelection().show(); - node?.seek(pageX, cursor); + node.seek(pageX, cursor); // Before .selectFrom when mouse-selecting, so // always hits no-selection case in scrollHoriz and scrolls slower diff --git a/src/services/parser.util.ts b/src/services/parser.util.ts index 0895eb06..646a4e07 100644 --- a/src/services/parser.util.ts +++ b/src/services/parser.util.ts @@ -1,5 +1,3 @@ -import { pray } from 'src/constants'; - type ParserBody = ( stream: string, success: (stream: string, ...args: any[]) => SR, @@ -20,6 +18,7 @@ export class Parser { this._ = body; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters parse(stream?: string | number | boolean | object) { return this.skip(Parser.eof)._( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -33,13 +32,14 @@ export class Parser { // Primitive combinators or(alternative: Parser) { - pray('or is passed a parser', alternative instanceof Parser); + if (!(alternative instanceof Parser)) throw new Error('or is passed a parser'); return new Parser((stream, onSuccess, onFailure) => this._(stream, onSuccess, () => alternative._(stream, onSuccess, onFailure)) ); } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters then(next: Parser | ((result: T) => Parser) | (() => Parser)) { return new Parser((stream, onSuccess, onFailure) => this._( @@ -47,8 +47,8 @@ export class Parser { (newStream, result: T) => { const nextParser = next instanceof Parser ? next : typeof next === 'function' ? next(result) : undefined; - pray('a parser is returned', nextParser instanceof Parser); - return nextParser!._(newStream, onSuccess, onFailure); + if (!(nextParser instanceof Parser)) throw new Error('a parser is returned'); + return nextParser._(newStream, onSuccess, onFailure); }, onFailure ) @@ -56,6 +56,7 @@ export class Parser { } // Optimized iterative combinators + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters many() { return new Parser((stream, onSuccess) => { const xs: T[] = []; @@ -76,6 +77,7 @@ export class Parser { }); } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters times(min: number, max: number = min) { return new Parser((stream, onSuccess, onFailure) => { const xs: T[] = []; @@ -104,6 +106,7 @@ export class Parser { } // Higher-level combinators + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters result(res: T) { return this.then(Parser.succeed(res)); } @@ -140,7 +143,7 @@ export class Parser { } static regex(re: RegExp) { - pray('regexp parser is anchored', re.toString().charAt(1) === '^'); + if (re.toString().charAt(1) !== '^') throw new Error('regexp parser is anchored'); return new Parser((stream, onSuccess, onFailure) => { const match = re.exec(stream); @@ -150,6 +153,7 @@ export class Parser { }); } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters static succeed(result?: T) { return new Parser((stream, onSuccess) => onSuccess(stream, result)); } diff --git a/src/services/textarea.ts b/src/services/textarea.ts index e4aad369..7c1527d3 100644 --- a/src/services/textarea.ts +++ b/src/services/textarea.ts @@ -29,7 +29,9 @@ export const TextAreaController = < this.textareaSpan.append(textarea); this.textarea = textarea; - this.cursor.selectionChanged = () => this.selectionChanged(); + this.cursor.selectionChanged = () => { + this.selectionChanged(); + }; } selectionChanged() { @@ -37,7 +39,9 @@ export const TextAreaController = < // and/or calling textarea.select() can have anomalously bad performance: // https://github.com/mathquill/mathquill/issues/43#issuecomment-1399080 if (!this.textareaSelectionTimeout) { - this.textareaSelectionTimeout = setTimeout(() => this.setTextareaSelection()); + this.textareaSelectionTimeout = setTimeout(() => { + this.setTextareaSelection(); + }); } } @@ -69,7 +73,9 @@ export const TextAreaController = < e.stopPropagation(); e.preventDefault(); }); - this.textarea?.addEventListener('copy', () => this.setTextareaSelection()); + this.textarea?.addEventListener('copy', () => { + this.setTextareaSelection(); + }); this.textarea?.addEventListener('focus', () => (this.blurred = false)); this.textarea?.addEventListener('blur', () => { if (this.cursor.selection) this.cursor.selection.clear(); @@ -88,8 +94,12 @@ export const TextAreaController = < } editablesTextareaEvents() { - const { select } = saneKeyboardEvents(this.textarea!, this as unknown as Controller); - this.selectFn = (text) => select(text); + if (this.textarea) { + const { select } = saneKeyboardEvents(this.textarea, this as unknown as Controller); + this.selectFn = (text) => { + select(text); + }; + } this.container.prepend(this.textareaSpan as HTMLElement); this.focusBlurEvents(); } @@ -120,7 +130,10 @@ export const TextAreaController = < } typedText(ch: string) { - if (ch === '\n') return this.handle('enter'); + if (ch === '\n') { + this.handle('enter'); + return; + } const cursor = this.notify().cursor; cursor.parent?.write(cursor, ch); this.scrollHoriz(); diff --git a/src/tree/fragment.ts b/src/tree/fragment.ts index baf080c2..49a53377 100644 --- a/src/tree/fragment.ts +++ b/src/tree/fragment.ts @@ -1,7 +1,7 @@ // Fragment base classes of edit tree-related objects import type { Direction } from 'src/constants'; -import { L, R, iterator, pray, prayWellFormed } from 'src/constants'; +import { L, R, iterator, prayWellFormed } from 'src/constants'; import type { Ends } from 'tree/node'; import { VNode } from 'tree/vNode'; import { TNode } from 'tree/node'; @@ -20,25 +20,25 @@ export class Fragment { elements: VNode = new VNode(); ends: Ends = {}; disowned?: boolean; - each = iterator((yield_: (node: TNode) => TNode | boolean | void) => { + each = iterator((yield_: (node: TNode) => TNode | boolean | undefined) => { let el = this.ends[L]; if (!el) return this; - for (; el !== this.ends[R]?.[R]; el = el?.[R]) { - if (yield_(el!) === false) break; + for (; el !== this.ends[R]?.[R]; el = el[R]) { + if (!el) continue; + if (yield_(el) === false) break; } return this; }); constructor(withDir?: TNode, oppDir?: TNode, dir: Direction = L) { - pray('no half-empty fragments', !withDir === !oppDir); - + if (!withDir !== !oppDir) throw new Error('no half-empty fragments'); if (!withDir) return; - pray('withDir is passed to Fragment', withDir instanceof TNode); - pray('oppDir is passed to Fragment', oppDir instanceof TNode); - pray('withDir and oppDir have the same parent', withDir.parent === oppDir?.parent); + if (!(withDir instanceof TNode)) throw new Error('withDir must be passed to Fragment'); + if (!(oppDir instanceof TNode)) throw new Error('oppDir must be passed to Fragment'); + if (withDir.parent !== oppDir.parent) throw new Error('withDir and oppDir must have the same parent'); this.ends[dir] = withDir; this.ends[dir === L ? R : L] = oppDir; @@ -83,7 +83,7 @@ export class Fragment { parent.ends[R] = rightEnd; } - this.ends[R]![R] = rightward; + if (this.ends[R]) this.ends[R][R] = rightward; this.each((el: TNode) => { el[L] = leftward; @@ -91,6 +91,7 @@ export class Fragment { if (leftward) leftward[R] = el; leftward = el; + return true; }); return this; @@ -105,8 +106,9 @@ export class Fragment { this.disowned = true; const rightEnd = this.ends[R]; - const parent = leftEnd.parent!; + const parent = leftEnd.parent; + if (!parent) throw new Error('a parent must always present'); prayWellFormed(parent, leftEnd[L], leftEnd); prayWellFormed(parent, rightEnd, rightEnd?.[R]); @@ -135,6 +137,7 @@ export class Fragment { let ret = fold; this.each((el: TNode) => { ret = fn(ret, el); + return true; }); return ret; } diff --git a/src/tree/node.ts b/src/tree/node.ts index a53046bd..7c9b34ee 100644 --- a/src/tree/node.ts +++ b/src/tree/node.ts @@ -1,7 +1,7 @@ // TNode base class of edit tree-related objects import type { Direction } from 'src/constants'; -import { L, R, iterator, pray, prayDirection, mqCmdId, mqBlockId } from 'src/constants'; +import { L, R, iterator, mqCmdId, mqBlockId } from 'src/constants'; import type { Options } from 'src/options'; import type { Controller } from 'src/controller'; import type { Cursor } from 'src/cursor'; @@ -14,7 +14,9 @@ export interface Ends { [R]?: TNode; } -const prayOverridden = (name: string) => pray(`"${name}" should be overridden or never called on this node`); +const prayOverridden = (name: string) => { + throw new Error(`"${name}" should be overridden or never called on this node`); +}; // MathQuill virtual-DOM tree-node abstract base class // Only doing tree node manipulation via these adopt/disown methods guarantees well-formedness of the tree. @@ -46,19 +48,22 @@ export class TNode { reflow?: () => void; - bubble = iterator((yield_: (node: TNode) => TNode | boolean | void) => { - // eslint-disable-next-line @typescript-eslint/no-this-alias - for (let ancestor: TNode | undefined = this; ancestor; ancestor = ancestor.parent) { - if (yield_(ancestor) === false) break; - } + bubble = iterator( + (yield_: (node: TNode) => TNode | boolean | undefined) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + for (let ancestor: TNode | undefined = this; ancestor; ancestor = ancestor.parent) { + if (yield_(ancestor) === false) break; + } - return this; - }); + return this; + } + ); - postOrder = iterator((yield_: (node: TNode) => TNode | boolean | void) => { + postOrder = iterator((yield_: (node: TNode) => TNode | boolean | undefined) => { (function recurse(descendant: TNode) { descendant.eachChild(recurse); yield_(descendant); + return true; })(this); return this; @@ -74,7 +79,7 @@ export class TNode { } toString() { - return `{{ MathQuill TNode #${this.id} }}`; + return `{{ MathQuill TNode #${this.id.toString()} }}`; } addToElements(el: VNode | HTMLElement) { @@ -97,15 +102,17 @@ export class TNode { } }; - localVNode.contents.forEach((element) => addToElements(element)); + localVNode.contents.forEach((element) => { + addToElements(element); + }); return localVNode; } - createDir(dir: Direction, cursor: Cursor) { - prayDirection(dir); + createDir(dir: Direction | undefined, cursor: Cursor) { + if (dir !== L && dir !== R) throw new Error('a direction was not passed'); this.domify(); this.elements.insDirOf(dir, cursor.element); - cursor[dir] = this.adopt(cursor.parent!, cursor[L], cursor[R]); + if (cursor.parent) cursor[dir] = this.adopt(cursor.parent, cursor[L], cursor[R]); return this; } @@ -129,7 +136,7 @@ export class TNode { return new Fragment(this.ends[L], this.ends[R]); } - eachChild(method: 'postOrder' | ((node: TNode) => boolean) | ((node: TNode) => void), order?: string) { + eachChild(method: 'postOrder' | ((node: TNode) => boolean), order?: string) { const children = this.children(); children.each(method, order); return this; @@ -190,7 +197,7 @@ export class TNode { // End -> move to the end of the current block. case 'End': - ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent!); + if (cursor.parent) ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); break; // Ctrl-End -> move all the way to the end of the root block. @@ -214,7 +221,7 @@ export class TNode { // Home -> move to the start of the root block or the current block. case 'Home': - ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent!); + if (cursor.parent) ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); break; // Ctrl-Home -> move to the start of the current block. @@ -263,6 +270,7 @@ export class TNode { case 'Shift-Up': if (cursor[L]) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (cursor[L]) ctrlr.selectLeft(); } else { ctrlr.selectLeft(); @@ -271,6 +279,7 @@ export class TNode { case 'Shift-Down': if (cursor[R]) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (cursor[R]) ctrlr.selectRight(); } else { ctrlr.selectRight(); @@ -339,7 +348,7 @@ export class TNode { return this; } chToCmd(_ignore_ch: string, _ignore_options: Options): TNode { - return this; + return this as TNode; } getController() { diff --git a/src/tree/vNode.ts b/src/tree/vNode.ts index 5316c08f..56c39cae 100644 --- a/src/tree/vNode.ts +++ b/src/tree/vNode.ts @@ -54,7 +54,9 @@ export class VNode { } detach() { - this.contents.forEach((child) => (child as Element).remove()); + this.contents.forEach((child) => { + (child as Element).remove(); + }); } remove() { @@ -71,7 +73,7 @@ export class VNode { children(selector?: string) { return new VNode( - this.contents.reduce((ret, el) => { + this.contents.reduce((ret, el) => { ret.push( ...Array.from(el.childNodes).filter((child) => { return ( @@ -80,16 +82,16 @@ export class VNode { }) ); return ret; - }, [] as Node[]) + }, []) ); } find(selector: string) { return new VNode( - this.contents.reduce((ret, el) => { + this.contents.reduce((ret, el) => { ret.push(...(el as Element).querySelectorAll(selector)); return ret; - }, [] as Node[]) + }, []) ); } @@ -118,21 +120,21 @@ export class VNode { return false; } - html(contents: string): VNode; + html(contents: string): this; html(): string; - html(contents?: string): string | VNode { + html(contents?: string): string | this { if (typeof contents === 'string') { this.contents.forEach((elt) => { if (elt instanceof Element) elt.innerHTML = contents; }); return this; } - return this.firstElement?.innerHTML ?? ''; + return this.firstElement.innerHTML; } - text(contents: string): VNode; + text(contents: string): this; text(): string; - text(contents?: string): string | VNode { + text(contents?: string): string | this { if (contents) { this.contents.forEach((elt) => { if (elt instanceof Element) elt.textContent = contents; From 243eb7d438ac67c22a62b2e33c36ea7205c7a154 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 31 Oct 2024 05:15:47 -0500 Subject: [PATCH 04/19] Remove mathquill basic. WeBWorK doesn't use that, and I don't want to maintain it anymore. I don't see the point of a MathQuill instance that doesn't do many of the the things that make MathQuill usefull. --- README.md | 5 +- package.json | 1 - public/basic-test.html | 55 ------------------ public/unit-test.html | 2 - src/commands/math/basicSymbols.ts | 3 +- src/index.ts | 6 +- src/indexBasic.ts | 7 --- src/publicapi.ts | 28 ++++++---- src/publicapiBasic.ts | 93 ------------------------------- test/publicapi.test.js | 26 +-------- test/typing.test.js | 10 +--- webpack.config.js | 3 - 12 files changed, 26 insertions(+), 213 deletions(-) delete mode 100644 public/basic-test.html delete mode 100644 src/indexBasic.ts delete mode 100644 src/publicapiBasic.ts diff --git a/README.md b/README.md index 3d821889..96187ec8 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,8 @@ Additionally you can run `npm run serve` which will automatically re-build and s tests. Then you can enter , , -, , -, or just in your browser to view the various available -pages. +, , or just in +your browser to view the various available pages. ## Open-Source License diff --git a/package.json b/package.json index 5c02c439..ed9317f7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "scripts": { "build:lib": "NODE_OPTIONS='--no-warnings=ExperimentalWarning' rollup -c", "build:dev": "webpack --mode development", - "build:all-js": "BUILD_BASIC=true webpack --mode production", "build:web": "webpack --mode production", "build": "npm run build:web && npm run build:lib", "serve": "webpack serve --mode development", diff --git a/public/basic-test.html b/public/basic-test.html deleted file mode 100644 index 962dcccf..00000000 --- a/public/basic-test.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - MathQuill-basic Demo - - - - - - -
-

MathQuill-basic Demo local test page

- -

- Backslash \ and dollar $ aren't special, / still works, font - works: -

- -

\sqrt{2}

- -

-
- - - - - diff --git a/public/unit-test.html b/public/unit-test.html index 57b127b0..4f980556 100644 --- a/public/unit-test.html +++ b/public/unit-test.html @@ -25,9 +25,7 @@ - diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index d3feacbc..a230bb19 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -1,6 +1,6 @@ // Symbols for Basic Mathematics -import type { Direction, Constructor } from 'src/constants'; +import type { Direction } from 'src/constants'; import { noop, L, R, bindMixin, LatexCmds, CharCmds } from 'src/constants'; import { Options } from 'src/options'; import type { Cursor } from 'src/cursor'; @@ -80,7 +80,6 @@ LatexCmds["'"] = LatexCmds.prime = bindMixin(VanillaSymbol, "'", '′'); // LatexCmds['\u2033'] = LatexCmds.dprime = bindMixin(VanillaSymbol, '\u2033', '″'); LatexCmds.backslash = bindMixin(VanillaSymbol, '\\backslash ', '\\'); -if (!(CharCmds['\\'] as Constructor | undefined)) CharCmds['\\'] = LatexCmds.backslash; LatexCmds.$ = bindMixin(VanillaSymbol, '\\$', '$'); diff --git a/src/index.ts b/src/index.ts index 2966efc7..14c246d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -import MathQuill from 'src/publicapi'; +import mathQuill from 'src/publicapi'; import { MQ_VERSION } from 'src/version'; import 'css/main.less'; -MathQuill.VERSION = MQ_VERSION; +mathQuill.VERSION = MQ_VERSION; -window.MathQuill = MathQuill; +window.MathQuill = mathQuill; diff --git a/src/indexBasic.ts b/src/indexBasic.ts deleted file mode 100644 index 2c658294..00000000 --- a/src/indexBasic.ts +++ /dev/null @@ -1,7 +0,0 @@ -import MathQuill from 'src/publicapiBasic'; -import { MQ_VERSION } from 'src/version'; -import 'css/mainBasic.less'; - -MathQuill.VERSION = MQ_VERSION; - -window.MathQuill = MathQuill; diff --git a/src/publicapi.ts b/src/publicapi.ts index aec28b2c..65d53f1b 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -36,13 +36,19 @@ interface MQApi { TextField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; } +interface MathQuill { + origMathQuill: MathQuill | undefined; + VERSION: string; + getInterface(): MQApi; + noConflict(): MathQuill; +} + // globally exported API object -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export default class MathQuill { - static origMathQuill: MathQuill | undefined = isBrowser ? window.MathQuill : undefined; - static VERSION = '0.0.1'; +const mathQuill: MathQuill = { + origMathQuill: isBrowser ? window.MathQuill : undefined, + VERSION: '0.0.1', - static getInterface() { + getInterface() { const APIClasses: Record = {}; // Function that takes an HTML element and, if it's the root HTML element of a @@ -90,11 +96,13 @@ export default class MathQuill { } return MQ as MQApi; - } + }, - static noConflict() { - if (!isBrowser) return MathQuill; + noConflict() { + if (!isBrowser) return mathQuill; window.MathQuill = this.origMathQuill; - return MathQuill; + return mathQuill; } -} +}; + +export default mathQuill; diff --git a/src/publicapiBasic.ts b/src/publicapiBasic.ts deleted file mode 100644 index dee1c4c5..00000000 --- a/src/publicapiBasic.ts +++ /dev/null @@ -1,93 +0,0 @@ -// The publicly exposed MathQuill API. - -import type { EmbedOptions } from 'src/constants'; -import { mqBlockId, EMBEDS } from 'src/constants'; -import type { InputOptions } from 'src/options'; -import { Options } from 'src/options'; -import { TNode } from 'tree/node'; -import { saneKeyboardEvents } from 'services/saneKeyboardEvents.util'; -import { Controller } from 'src/controller'; -import type { AbstractMathQuillConstructor, AbstractMathQuill } from 'src/abstractFields'; -import { StaticMath, MathField, InnerMathField } from 'commands/math'; - -// These files need to be imported to construct the library of commands. -import 'commands/math/commands'; -import 'commands/math/basicSymbols'; - -declare global { - interface Window { - MathQuill?: MathQuill; - } -} - -interface MQApi { - (el: unknown): AbstractMathQuill | undefined; - saneKeyboardEvents: typeof saneKeyboardEvents; - config: (opts: InputOptions) => MQApi; - registerEmbed: (name: string, options: (data: string) => EmbedOptions) => void; - StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; - MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; - InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; -} - -// globally exported API object -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export default class MathQuill { - static origMathQuill: MathQuill | undefined = window.MathQuill; - static VERSION = '0.0.1'; - - static getInterface() { - const APIClasses: Record = {}; - - // Function that takes an HTML element and, if it's the root HTML element of a - // static math or math or text field, returns an API object for it (else, undefined). - // const mathfield = MQ.MathField(mathFieldSpan); - // assert(MQ(mathFieldSpan).id === mathfield.id); - // assert(MQ(mathFieldSpan).id === MQ(mathFieldSpan).id); - const MQ = (el: unknown) => { - if (!(el instanceof HTMLElement)) return; - const blockId = el.querySelector('.mq-root-block')?.getAttribute(mqBlockId) ?? false; - const ctrlr = blockId ? TNode.byId.get(parseInt(blockId))?.controller : undefined; - return ctrlr?.apiClass; - }; - - MQ.saneKeyboardEvents = saneKeyboardEvents; - - MQ.config = (opts: InputOptions) => { - Options.config(Options.prototype, opts); - return MQ; - }; - - MQ.registerEmbed = (name: string, options: (data: string) => EmbedOptions) => { - if (!/^[a-z][a-z0-9]*$/i.test(name)) { - throw 'Embed name must start with letter and be only letters and digits'; - } - EMBEDS[name] = options; - }; - - // Export the API functions that MathQuill-ify an HTML element into API objects - // of each class. If the element had already been MathQuill-ified but into a - // different kind (or it's not an HTML element), return undefined. - for (const [kind, APIClass] of Object.entries({ StaticMath, MathField, InnerMathField })) { - APIClasses[kind] = APIClass; - (MQ as MQApi)[kind as keyof Pick] = ( - el: unknown, - opts: InputOptions - ) => { - const mq = MQ(el); - if (mq instanceof APIClass || !(el instanceof HTMLElement)) return mq; - const ctrlr = new Controller(new APIClass.RootBlock(), el, new Options()); - ctrlr.KIND_OF_MQ = kind; - return new APIClass(ctrlr).config(opts).__mathquillify(); - }; - Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClass); - } - - return MQ as MQApi; - } - - static noConflict() { - window.MathQuill = this.origMathQuill; - return MathQuill; - } -} diff --git a/test/publicapi.test.js b/test/publicapi.test.js index 78e32295..dd2a0a47 100644 --- a/test/publicapi.test.js +++ b/test/publicapi.test.js @@ -1,4 +1,4 @@ -/* global suite, test, assert, setup, teardown, MQ, MQBasic */ +/* global suite, test, assert, setup, teardown, MQ */ import { L, R } from 'src/constants'; @@ -43,30 +43,6 @@ suite('Public API', () => { }); }); - suite('mathquill-basic', () => { - let mq; - setup(() => { - const el = document.createElement('span'); - document.getElementById('mock')?.append(el); - mq = MQBasic.MathField(el); - }); - - test('typing \\', () => { - mq.typedText('\\'); - assert.equal(mq.latex(), '\\backslash'); - }); - - test('typing $', () => { - mq.typedText('$'); - assert.equal(mq.latex(), '\\$'); - }); - - test('parsing of advanced symbols', () => { - mq.latex('\\oplus'); - assert.equal(mq.latex(), ''); // TODO: better LaTeX parse error behavior - }); - }); - suite('basic API methods', () => { let mq; setup(() => { diff --git a/test/typing.test.js b/test/typing.test.js index 5413f520..672c4a54 100644 --- a/test/typing.test.js +++ b/test/typing.test.js @@ -1,4 +1,4 @@ -/* global suite, test, assert, setup, MQ, MQBasic */ +/* global suite, test, assert, setup, MQ */ import { Bracket } from 'commands/mathElements'; import { L, R, prayWellFormed } from 'src/constants'; @@ -35,14 +35,6 @@ suite('typing with auto-replaces', () => { mq.latex('').typedText('1 2/3'); assertLatex('1\\ \\frac{2}{3}'); }); - - test('mathquill-basic', () => { - const el = document.createElement('span'); - document.getElementById('mock')?.append(el); - const mq_basic = MQBasic.MathField(el); - mq_basic.typedText('1/2'); - assert.equal(mq_basic.latex(), '\\frac{1}{2}'); - }); }); suite('LatexCommandInput', () => { diff --git a/webpack.config.js b/webpack.config.js index dbdf8984..7d87f882 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -101,7 +101,6 @@ module.exports = (_env, argv) => { config.devtool = 'source-map'; config.entry['mathquill.test'] = './test/index.js'; - config.entry['mathquill-basic'] = './src/indexBasic.ts'; config.resolve.alias.test = path.resolve(__dirname, 'test'); config.devServer = { server: { type: 'http' }, @@ -118,8 +117,6 @@ module.exports = (_env, argv) => { } else { // eslint-disable-next-line no-console console.log('Using production mode.'); - - if (process.env.BUILD_BASIC) config.entry['mathquill-basic'] = './src/indexBasic.ts'; } return config; From db2639ab21c58e36a3c4b512a36d6d88e97645e3 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 10 Nov 2024 05:42:55 -0600 Subject: [PATCH 05/19] Fix some typing issues with the api. --- src/abstractFields.ts | 12 +++--- src/commands/math.ts | 4 +- src/commands/math/basicSymbols.ts | 4 +- src/commands/mathBlock.ts | 2 +- src/commands/text.ts | 2 + src/commands/textElements.ts | 2 +- src/options.ts | 4 +- src/publicapi.ts | 67 ++++++++++++++++++++----------- src/services/latex.ts | 6 +-- src/services/mouse.ts | 2 +- src/services/parser.util.ts | 16 +++----- 11 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/abstractFields.ts b/src/abstractFields.ts index 0f305d98..96ab9be2 100644 --- a/src/abstractFields.ts +++ b/src/abstractFields.ts @@ -14,7 +14,7 @@ export class AbstractMathQuill { __controller: Controller; __options: Options; id: number; - revert?: () => void; + revert?: () => HTMLElement; static RootBlock?: Constructor; constructor(ctrlr: Controller) { @@ -73,6 +73,8 @@ export class AbstractMathQuill { return this.__controller.exportText(); } + latex(latex: string): this; + latex(): string; latex(latex?: string) { if (typeof latex !== 'undefined') { this.__controller.renderLatexMath(latex); @@ -203,16 +205,16 @@ export class EditableField extends AbstractMathQuill { dropEmbedded( pageX: number, pageY: number, - options: { text?: () => string; htmlTemplate?: string; latex?: () => string } + options: { text?: () => string; htmlString?: string; latex?: () => string } ) { - const el = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY) as HTMLElement; + const el = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY); this.__controller.seek(el, pageX); const cmd = new LatexCmds.embed().setOptions(options); cmd.createLeftOf(this.__controller.cursor); } - clickAt(clientX: number, clientY: number, target: HTMLElement | undefined) { - target = target || (document.elementFromPoint(clientX, clientY) as HTMLElement); + clickAt(clientX: number, clientY: number, target?: Element | null) { + target = target || document.elementFromPoint(clientX, clientY); const ctrlr = this.__controller, root = ctrlr.root; diff --git a/src/commands/math.ts b/src/commands/math.ts index 0c4cbe10..7c2bf40b 100644 --- a/src/commands/math.ts +++ b/src/commands/math.ts @@ -52,8 +52,10 @@ export class StaticMath extends AbstractMathQuill { return this; } + latex(latex: string): this; + latex(): string; latex(latex?: string) { - const returned = super.latex(latex); + const returned = typeof latex === 'string' ? super.latex(latex) : super.latex(); if (typeof latex !== 'undefined') { this.__controller.root.postOrder( 'registerInnerField', diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index a230bb19..512e9d11 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -206,7 +206,7 @@ class LatexFragment extends MathCommand { } createLeftOf(cursor: Cursor) { - const block: MathCommand = latexMathParser.parse(this.latex()); + const block = latexMathParser.parse(this.latex()); if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); cursor[L] = block.ends[R]; cursor.element.before(...block.domify().contents); @@ -217,7 +217,7 @@ class LatexFragment extends MathCommand { } parser() { - const block: MathCommand = latexMathParser.parse(this.latex()); + const block = latexMathParser.parse(this.latex()); return Parser.succeed(block.children()); } } diff --git a/src/commands/mathBlock.ts b/src/commands/mathBlock.ts index 81549b07..3fcc7fda 100644 --- a/src/commands/mathBlock.ts +++ b/src/commands/mathBlock.ts @@ -127,7 +127,7 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { const all = Parser.all; const eof = Parser.eof; - const block: MathCommand | undefined = latexMathParser.skip(eof).or(all.result(false)).parse(latex); + const block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); diff --git a/src/commands/text.ts b/src/commands/text.ts index 4fd7acf5..8c288987 100644 --- a/src/commands/text.ts +++ b/src/commands/text.ts @@ -10,6 +10,8 @@ export class TextField extends EditableField { return super.__mathquillify('mq-editable-field', 'mq-text-mode'); } + latex(latex: string): this; + latex(): string; latex(latex?: string) { if (typeof latex !== 'undefined') { this.__controller.renderLatexText(latex); diff --git a/src/commands/textElements.ts b/src/commands/textElements.ts index f85384b5..d0271d08 100644 --- a/src/commands/textElements.ts +++ b/src/commands/textElements.ts @@ -248,7 +248,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { // A piece of plain text, with a TextBlock as a parent and no children. This wraps a single DOMTextNode. // For convenience, it has a textStr property that is a string that mirrors the text contents of the DOMTextNode. // Text contents must always be nonempty. -class TextPiece extends TNode { +export class TextPiece extends TNode { textStr: string; dom?: Text; diff --git a/src/options.ts b/src/options.ts index 0711ae98..3b948deb 100644 --- a/src/options.ts +++ b/src/options.ts @@ -38,14 +38,14 @@ export interface InputOptions { autoSubscriptNumerals?: boolean; typingSlashWritesDivisionSymbol?: boolean; typingAsteriskWritesTimesSymbol?: boolean; - substituteTextarea?: () => HTMLTextAreaElement; + substituteTextarea?: () => HTMLElement; handlers?: Handlers; overridePaste?: () => void; overrideCut?: () => void; overrideCopy?: () => void; overrideTypedText?: (text: string) => void; overrideKeystroke?: (key: string, event: KeyboardEvent) => void; - ignoreNextMousedown: (e?: MouseEvent) => boolean; + ignoreNextMousedown?: (e?: MouseEvent) => boolean; } interface NamesWLength { diff --git a/src/publicapi.ts b/src/publicapi.ts index 65d53f1b..1f523a06 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -26,14 +26,26 @@ declare global { const isBrowser = Object.getPrototypeOf(Object.getPrototypeOf(globalThis)) !== Object.prototype; interface MQApi { - (el: unknown): AbstractMathQuill | undefined; + (el?: HTMLElement): AbstractMathQuill | undefined; saneKeyboardEvents: typeof saneKeyboardEvents; - config: (opts: InputOptions) => MQApi; - registerEmbed: (name: string, options: (data: string) => EmbedOptions) => void; - StaticMath: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; - MathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; - InnerMathField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; - TextField: (el: unknown, opts: InputOptions) => AbstractMathQuill | undefined; + config(opts: InputOptions): MQApi; + registerEmbed(name: string, options: (data: string) => EmbedOptions): void; + StaticMath: { + (el?: null): undefined; + (el: HTMLElement, opts?: InputOptions): StaticMath; + }; + MathField: { + (el?: null): undefined; + (el: HTMLElement, opts?: InputOptions): MathField; + }; + InnerMathField: { + (el?: null): undefined; + (el: HTMLElement, opts?: InputOptions): InnerMathField; + }; + TextField: { + (el?: null): undefined; + (el: HTMLElement, opts?: InputOptions): TextField; + }; } interface MathQuill { @@ -49,14 +61,12 @@ const mathQuill: MathQuill = { VERSION: '0.0.1', getInterface() { - const APIClasses: Record = {}; - // Function that takes an HTML element and, if it's the root HTML element of a // static math or math or text field, returns an API object for it (else, undefined). // const mathfield = MQ.MathField(mathFieldSpan); // assert(MQ(mathFieldSpan).id === mathfield.id); // assert(MQ(mathFieldSpan).id === MQ(mathFieldSpan).id); - const MQ = (el: unknown) => { + const MQ = (el?: HTMLElement) => { if (!(el instanceof HTMLElement)) return; const blockId = el.querySelector('.mq-root-block')?.getAttribute(mqBlockId) ?? false; const ctrlr = blockId ? TNode.byId.get(parseInt(blockId))?.controller : undefined; @@ -65,9 +75,9 @@ const mathQuill: MathQuill = { MQ.saneKeyboardEvents = saneKeyboardEvents; - MQ.config = (opts: InputOptions) => { + MQ.config = (opts: InputOptions): MQApi => { Options.config(Options.prototype, opts); - return MQ; + return MQ as MQApi; }; MQ.registerEmbed = (name: string, options: (data: string) => EmbedOptions) => { @@ -80,20 +90,31 @@ const mathQuill: MathQuill = { // Export the API functions that MathQuill-ify an HTML element into API objects // of each class. If the element had already been MathQuill-ified but into a // different kind (or it's not an HTML element), return undefined. - for (const [kind, APIClass] of Object.entries({ StaticMath, MathField, InnerMathField, TextField })) { - APIClasses[kind] = APIClass; - (MQ as MQApi)[kind as keyof Pick] = ( - el: unknown, - opts: InputOptions - ) => { + const createEntrypoint = ( + kind: keyof Pick, + APIClass: MQClass + ) => { + function mqEntrypoint(el?: null): undefined; + function mqEntrypoint(el: HTMLElement, opts?: InputOptions): InstanceType; + function mqEntrypoint(el?: HTMLElement | null, opts?: InputOptions) { + if (!(el instanceof HTMLElement)) return; const mq = MQ(el); - if (mq instanceof APIClass || !(el instanceof HTMLElement)) return mq; + if (!(el instanceof HTMLElement) || mq instanceof APIClass) return mq; const ctrlr = new Controller(new APIClass.RootBlock(), el, new Options()); ctrlr.KIND_OF_MQ = kind; - return new APIClass(ctrlr).config(opts).__mathquillify(); - }; - Object.setPrototypeOf((MQ as MQApi)[kind as keyof MQApi], APIClass); - } + return new APIClass(ctrlr).config(opts ?? {}).__mathquillify(); + } + return mqEntrypoint; + }; + + MQ.StaticMath = createEntrypoint('StaticMath', StaticMath); + MQ.StaticMath.prototype = StaticMath.prototype; + MQ.MathField = createEntrypoint('MathField', MathField); + MQ.MathField.prototype = MathField.prototype; + MQ.InnerMathField = createEntrypoint('InnerMathField', InnerMathField); + MQ.InnerMathField.prototype = InnerMathField.prototype; + MQ.TextField = createEntrypoint('TextField', TextField); + MQ.TextField.prototype = TextField.prototype; return MQ as MQApi; }, diff --git a/src/services/latex.ts b/src/services/latex.ts index 43e212ca..9f250361 100644 --- a/src/services/latex.ts +++ b/src/services/latex.ts @@ -22,10 +22,10 @@ export const LatexControllerExtension = (latex); this.root.eachChild('postOrder', 'dispose'); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete @@ -83,7 +83,7 @@ export const LatexControllerExtension = (latex); if (commands) { for (const command of commands) { diff --git a/src/services/mouse.ts b/src/services/mouse.ts index ef583e9c..cdbe28e6 100644 --- a/src/services/mouse.ts +++ b/src/services/mouse.ts @@ -140,7 +140,7 @@ export const MouseEventController = & this.container.addEventListener('mousedown', this.mouseDownHandler); } - seek(target: HTMLElement | undefined, pageX: number) { + seek(target: Element | null | undefined, pageX: number) { const cursor = this.notify('select').cursor; let nodeId = 0; diff --git a/src/services/parser.util.ts b/src/services/parser.util.ts index 646a4e07..f56d4199 100644 --- a/src/services/parser.util.ts +++ b/src/services/parser.util.ts @@ -133,16 +133,16 @@ export class Parser { } // Primitive parsers - static string(str: string) { + static string = (str: string) => { return new Parser((stream, onSuccess, onFailure) => { const head = stream.slice(0, str.length); if (head === str) return onSuccess(stream.slice(str.length), head); else return onFailure(stream, `expected '${str}'`); }); - } + }; - static regex(re: RegExp) { + static regex = (re: RegExp) => { if (re.toString().charAt(1) !== '^') throw new Error('regexp parser is anchored'); return new Parser((stream, onSuccess, onFailure) => { @@ -151,16 +151,12 @@ export class Parser { if (match) return onSuccess(stream.slice(match[0].length), match[0]); else return onFailure(stream, `expected ${re.toString()}`); }); - } + }; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - static succeed(result?: T) { - return new Parser((stream, onSuccess) => onSuccess(stream, result)); - } + static succeed = (result?: T) => new Parser((stream, onSuccess) => onSuccess(stream, result)); - static fail(msg?: string) { - return new Parser((stream, _, onFailure) => onFailure(stream, msg)); - } + static fail = (msg?: string) => new Parser((stream, _, onFailure) => onFailure(stream, msg)); static letter = Parser.regex(/^[a-z]/i); static letters = Parser.regex(/^[a-z]*/i); From 68cfd7e26ece5499ea4da561acf5bc657534c45d Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 31 Oct 2024 14:57:52 -0500 Subject: [PATCH 06/19] Dispense with the L, R enum. Typescript continually has issues with that usage. So now instead there are "left" and "right" properties of nodes and the cursor. The direction type is just a type union of the strings 'left' and 'right'. The advantages of a numeric value had already been taken away when this project was initially converted to typescript. For instance, you can't multiply an enum value by -1 (to get the opposite direction). --- eslint.config.mjs | 4 +- src/abstractFields.ts | 14 +- src/commands/math/LatexCommandInput.ts | 10 +- src/commands/math/advancedSymbols.ts | 4 +- src/commands/math/basicSymbols.ts | 20 +- src/commands/math/commands.ts | 72 ++--- src/commands/mathBlock.ts | 38 +-- src/commands/mathElements.ts | 383 ++++++++++++------------ src/commands/textElements.ts | 89 +++--- src/constants.ts | 25 +- src/controller.ts | 55 ++-- src/cursor.ts | 79 +++-- src/mixins.ts | 7 +- src/options.ts | 34 +-- src/publicapi.ts | 2 +- src/selection.ts | 3 +- src/services/latex.ts | 17 +- src/services/mouse.ts | 36 +-- src/services/parser.util.ts | 2 +- src/services/saneKeyboardEvents.util.ts | 3 +- src/services/scrollHoriz.ts | 3 +- src/services/textarea.ts | 2 +- src/tree/fragment.ts | 57 ++-- src/tree/node.ts | 92 +++--- src/tree/point.ts | 11 +- src/tree/vNode.ts | 5 +- test/MathFunction.test.js | 7 +- test/SupSub.test.js | 4 +- test/backspace.test.js | 24 +- test/latex.test.js | 21 +- test/publicapi.test.js | 44 ++- test/select.test.js | 40 +-- test/text.test.js | 6 +- test/tree.test.js | 79 +++-- test/typing.test.js | 6 +- test/updown.test.js | 94 +++--- 36 files changed, 691 insertions(+), 701 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1125bff5..450b8b40 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -55,13 +55,11 @@ export default [ 'no-void': 'off', 'one-var': 'off', - 'prefer-promise-reject-errors': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_ignore_' }], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }], '@typescript-eslint/prefer-nullish-coalescing': 'off', - '@typescript-eslint/only-throw-error': 'off', // Allow console and debugger during development only. 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', diff --git a/src/abstractFields.ts b/src/abstractFields.ts index 96ab9be2..78104c5f 100644 --- a/src/abstractFields.ts +++ b/src/abstractFields.ts @@ -1,5 +1,5 @@ import type { Direction, Constructor } from 'src/constants'; -import { L, R, noop, mqBlockId, mqCmdId, LatexCmds } from 'src/constants'; +import { noop, mqBlockId, mqCmdId, LatexCmds } from 'src/constants'; import type { InputOptions } from 'src/options'; import { Options } from 'src/options'; import type { Controller } from 'src/controller'; @@ -133,10 +133,8 @@ export class EditableField extends AbstractMathQuill { const root = this.__controller.root, cursor = this.__controller.cursor; root.eachChild('postOrder', 'dispose'); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete root.ends[L]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete root.ends[R]; + delete root.ends.left; + delete root.ends.right; root.elements.empty(); delete cursor.selection; cursor.insAtRightEnd(root); @@ -165,7 +163,7 @@ export class EditableField extends AbstractMathQuill { select() { const ctrlr = this.__controller; ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (ctrlr.cursor[L]) ctrlr.selectLeft(); + while (ctrlr.cursor.left) ctrlr.selectLeft(); return this; } @@ -179,10 +177,10 @@ export class EditableField extends AbstractMathQuill { return this; } moveToLeftEnd() { - return this.moveToDirEnd(L); + return this.moveToDirEnd('left'); } moveToRightEnd() { - return this.moveToDirEnd(R); + return this.moveToDirEnd('right'); } keystroke(keys: string) { diff --git a/src/commands/math/LatexCommandInput.ts b/src/commands/math/LatexCommandInput.ts index e7c9a665..72864f0e 100644 --- a/src/commands/math/LatexCommandInput.ts +++ b/src/commands/math/LatexCommandInput.ts @@ -1,6 +1,6 @@ // Input box to type backslash commands -import { L, R, LatexCmds, CharCmds } from 'src/constants'; +import { LatexCmds, CharCmds } from 'src/constants'; import type { Controller } from 'src/controller'; import type { Cursor } from 'src/cursor'; import type { Fragment } from 'tree/fragment'; @@ -26,7 +26,7 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { createBlocks() { super.createBlocks(); - const leftEnd = this.ends[L]; + const leftEnd = this.ends.left; if (!leftEnd) return; @@ -88,17 +88,17 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { } latex() { - return '\\' + (this.ends[L]?.latex() ?? '') + ' '; + return '\\' + (this.ends.left?.latex() ?? '') + ' '; } renderCommand(cursor: Cursor) { this.elements = new VNode(this.elements.last); this.remove(); - if (this[R]) cursor.insLeftOf(this[R]); + if (this.right) cursor.insLeftOf(this.right); else if (this.parent) cursor.insAtRightEnd(this.parent); - const latex = this.ends[L]?.latex() || ' '; + const latex = this.ends.left?.latex() || ' '; if (latex in LatexCmds) { const cmd = new LatexCmds[latex](latex); if (this._replacedFragment) cmd.replaces(this._replacedFragment); diff --git a/src/commands/math/advancedSymbols.ts b/src/commands/math/advancedSymbols.ts index 75367733..8a025ebb 100644 --- a/src/commands/math/advancedSymbols.ts +++ b/src/commands/math/advancedSymbols.ts @@ -1,6 +1,6 @@ // Symbols for Advanced Mathematics -import { R, noop, bindMixin, LatexCmds } from 'src/constants'; +import { noop, bindMixin, LatexCmds } from 'src/constants'; import { Parser } from 'services/parser.util'; import { VanillaSymbol, BinaryOperator, MathCommand } from 'commands/mathElements'; @@ -402,7 +402,7 @@ LatexCmds.deg = LatexCmds.degree = class degree extends VanillaSymbol { } text() { - const rightText = this[R]?.text(); + const rightText = this.right?.text(); return `\u00B0${rightText && /^[^FCK]$/.test(rightText) ? ' ' : ''}`; } }; diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index 512e9d11..e487d6d9 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -1,7 +1,7 @@ // Symbols for Basic Mathematics import type { Direction } from 'src/constants'; -import { noop, L, R, bindMixin, LatexCmds, CharCmds } from 'src/constants'; +import { noop, bindMixin, LatexCmds, CharCmds } from 'src/constants'; import { Options } from 'src/options'; import type { Cursor } from 'src/cursor'; import { Parser } from 'services/parser.util'; @@ -35,7 +35,7 @@ class OperatorName extends Symbol { parser() { const block = new MathBlock(); for (const char of this.ctrlSeq) { - new Letter(char).adopt(block, block.ends[R]); + new Letter(char).adopt(block, block.ends.right); } return Parser.succeed(block.children()); } @@ -206,13 +206,13 @@ class LatexFragment extends MathCommand { } createLeftOf(cursor: Cursor) { - const block = latexMathParser.parse(this.latex()); - if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); - cursor[L] = block.ends[R]; + const block: MathCommand = latexMathParser.parse(this.latex()); + if (cursor.parent) block.children().adopt(cursor.parent, cursor.left, cursor.right); + cursor.left = block.ends.right; cursor.element.before(...block.domify().contents); block.finalizeInsert(cursor.options, cursor); - block.ends[R]?.[R]?.siblingCreated?.(cursor.options, L); - block.ends[L]?.[L]?.siblingCreated?.(cursor.options, R); + block.ends.right?.right?.siblingCreated?.(cursor.options, 'left'); + block.ends.left?.left?.siblingCreated?.(cursor.options, 'right'); cursor.parent?.bubble('reflow'); } @@ -262,11 +262,11 @@ class PlusMinus extends BinaryOperator { contactWeld(_opts: Options, dir?: Direction) { const isUnary = (node: TNode): boolean => { - if (node[L]) { + if (node.left) { // 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] as BinaryOperator).ctrlSeq)) { + if (node.left instanceof BinaryOperator || /^[,;:([]$/.test((node.left as BinaryOperator).ctrlSeq)) { return true; } } else if (node.parent?.parent?.isStyleBlock()) { @@ -280,7 +280,7 @@ class PlusMinus extends BinaryOperator { return false; }; - if (dir === R) return; // ignore if sibling only changed on the right + if (dir === 'right') return; // ignore if sibling only changed on the right this.isUnary = isUnary(this); this.elements.firstElement.className = this.isUnary ? '' : 'mq-binary-operator'; return this; diff --git a/src/commands/math/commands.ts b/src/commands/math/commands.ts index ce9cf7a2..fbdd7511 100644 --- a/src/commands/math/commands.ts +++ b/src/commands/math/commands.ts @@ -1,7 +1,7 @@ // Commands and Operators. import type { Constructor } from 'src/constants'; -import { L, R, bindMixin, LatexCmds, CharCmds, OPP_BRACKS, EMBEDS, EmbedOptions } from 'src/constants'; +import { bindMixin, LatexCmds, CharCmds, OPP_BRACKS, EMBEDS, EmbedOptions } from 'src/constants'; import type { Options } from 'src/options'; import { Controller } from 'src/controller'; import { Cursor } from 'src/cursor'; @@ -146,7 +146,7 @@ LatexCmds.subscript = LatexCmds._ = class extends SupSub { } finalizeTree() { - this.downInto = this.sub = this.ends[L]; + this.downInto = this.sub = this.ends.left; if (this.sub) this.sub.upOutOf = insLeftOfMeUnlessAtEnd; super.finalizeTree(); } @@ -165,7 +165,7 @@ LatexCmds.superscript = } finalizeTree() { - this.upInto = this.sup = this.ends[R]; + this.upInto = this.sup = this.ends.right; if (this.sup) this.sup.downOutOf = insLeftOfMeUnlessAtEnd; super.finalizeTree(); } @@ -222,7 +222,7 @@ const FractionChooseCreateLeftOfMixin = > class extends Base { createLeftOf(cursor: Cursor) { if (!this.replacedFragment) { - let leftward: TNode | undefined = cursor[L]; + let leftward: TNode | undefined = cursor.left; while ( leftward && !( @@ -233,21 +233,21 @@ const FractionChooseCreateLeftOfMixin = > /^[,;:]$/.test(leftward.ctrlSeq) ) // lookbehind for operator ) - leftward = leftward[L]; + leftward = leftward.left; - if (leftward instanceof UpperLowerLimitCommand && leftward[R] instanceof SupSub) { - leftward = leftward[R]; - if (leftward[R] instanceof SupSub && leftward[R].ctrlSeq != leftward.ctrlSeq) - leftward = leftward[R]; + if (leftward instanceof UpperLowerLimitCommand && leftward.right instanceof SupSub) { + leftward = leftward.right; + if (leftward.right instanceof SupSub && leftward.right.ctrlSeq != leftward.ctrlSeq) + leftward = leftward.right; } if ( - leftward !== cursor[L] && + leftward !== cursor.left && !cursor.isTooDeep(1) && - !cursor[L]?.elements.hasClass('mq-operator-name') + !cursor.left?.elements.hasClass('mq-operator-name') ) { - this.replaces(new Fragment(leftward?.[R] || cursor.parent?.ends[L], cursor[L])); - cursor[L] = leftward; + this.replaces(new Fragment(leftward?.right || cursor.parent?.ends.left, cursor.left)); + cursor.left = leftward; } } super.createLeftOf(cursor); @@ -281,16 +281,16 @@ LatexCmds.log = class extends MathFunction { } text() { - const base = this.blocks[0].ends[L]?.sub?.text() || ''; + const base = this.blocks[0].ends.left?.sub?.text() || ''; const param = this.blocks[1].text() || ''; - const exponent = supSubText('^', this.blocks[0].ends[L]?.sup); + const exponent = supSubText('^', this.blocks[0].ends.left?.sup); if (!base) return super.text(); else if (base === '10') { return exponent ? `(log10(${param}))${exponent}` : `log10(${param})`; } else if (this.getController()?.options.logsChangeBase) { - let leftward = this[L]; - for (; leftward && leftward.ctrlSeq === '\\ '; leftward = leftward[L]); + let leftward = this.left; + for (; leftward && leftward.ctrlSeq === '\\ '; leftward = leftward.left); return exponent || (leftward && !(leftward instanceof BinaryOperator)) || (leftward instanceof BinaryOperator && leftward.isUnary) @@ -314,7 +314,7 @@ class SquareRoot extends MathCommand { this.textTemplate = ['sqrt(', ')']; this.reflow = () => { - const block = this.ends[R]?.elements.firstElement; + const block = this.ends.right?.elements.firstElement; if (block) { scale( [block.previousElementSibling as HTMLElement], @@ -367,19 +367,19 @@ class NthRoot extends SquareRoot { } latex() { - return `\\sqrt[${this.ends[L]?.latex() ?? ''}]{${this.ends[R]?.latex() ?? ''}}`; + return `\\sqrt[${this.ends.left?.latex() ?? ''}]{${this.ends.right?.latex() ?? ''}}`; } text() { - const index = this.ends[L]?.text() ?? ''; - if (index === '' || index === '2') return `sqrt(${this.ends[R]?.text() ?? ''})`; + const index = this.ends.left?.text() ?? ''; + if (index === '' || index === '2') return `sqrt(${this.ends.right?.text() ?? ''})`; if (this.getController()?.options.rootsAreExponents) { - const isSupR = this[R] instanceof SupSub && this[R].supsub === 'sup'; - return `${isSupR ? '(' : ''}(${this.ends[R]?.text() ?? ''})^(1/${index})${isSupR ? ')' : ''}`; + const isSupR = this.right instanceof SupSub && this.right.supsub === 'sup'; + return `${isSupR ? '(' : ''}(${this.ends.right?.text() ?? ''})^(1/${index})${isSupR ? ')' : ''}`; } - return `root(${index},${this.ends[R]?.text() ?? ''})`; + return `root(${index},${this.ends.right?.text() ?? ''})`; } } LatexCmds.root = LatexCmds.nthroot = NthRoot; @@ -403,17 +403,17 @@ const bindCharBracketPair = (open: string, ctrlSeq?: string) => { const curCtrlSeq = ctrlSeq || open, close = OPP_BRACKS[open], end = OPP_BRACKS[curCtrlSeq]; - CharCmds[open] = bindMixin(Bracket, L, open, close, curCtrlSeq, end); - CharCmds[close] = bindMixin(Bracket, R, open, close, curCtrlSeq, end); + CharCmds[open] = bindMixin(Bracket, 'left', open, close, curCtrlSeq, end); + CharCmds[close] = bindMixin(Bracket, 'right', open, close, curCtrlSeq, end); }; bindCharBracketPair('('); bindCharBracketPair('['); bindCharBracketPair('{', '\\{'); -LatexCmds.langle = bindMixin(Bracket, L, '⟨', '⟩', '\\langle ', '\\rangle '); -LatexCmds.rangle = bindMixin(Bracket, R, '⟨', '⟩', '\\langle ', '\\rangle '); -LatexCmds.abs = CharCmds['|'] = bindMixin(Bracket, L, '|', '|', '|', '|'); -LatexCmds.lVert = bindMixin(Bracket, L, '∥', '∥', '\\lVert ', '\\rVert '); -LatexCmds.rVert = bindMixin(Bracket, R, '∥', '∥', '\\lVert ', '\\rVert '); +LatexCmds.langle = bindMixin(Bracket, 'left', '⟨', '⟩', '\\langle ', '\\rangle '); +LatexCmds.rangle = bindMixin(Bracket, 'right', '⟨', '⟩', '\\langle ', '\\rangle '); +LatexCmds.abs = CharCmds['|'] = bindMixin(Bracket, 'left', '|', '|', '|', '|'); +LatexCmds.lVert = bindMixin(Bracket, 'left', '∥', '∥', '\\lVert ', '\\rVert '); +LatexCmds.rVert = bindMixin(Bracket, 'right', '∥', '∥', '\\lVert ', '\\rVert '); LatexCmds.left = class extends MathCommand { parser() { @@ -503,8 +503,8 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { } finalizeTree(options: Options) { - if (!this.ends[L]) throw 'Missing left end finalizing editable tree'; - const ctrlr = new Controller(this.ends[L], this.elements.firstElement, options); + if (!this.ends.left) throw new Error('Missing left end finalizing editable tree'); + const ctrlr = new Controller(this.ends.left, this.elements.firstElement, options); ctrlr.KIND_OF_MQ = 'MathField'; this.field = new InnerMathField(ctrlr); this.field.name = this.name; @@ -517,16 +517,16 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { } registerInnerField(innerFields: InnerMathFieldStore) { - if (!this.field) throw 'Unable to register editable without field'; + if (!this.field) throw new Error('Unable to register editable without field'); innerFields.push(this.field); } latex() { - return this.ends[L]?.latex() ?? ''; + return this.ends.left?.latex() ?? ''; } text() { - return this.ends[L]?.text() ?? ''; + return this.ends.left?.text() ?? ''; } }; diff --git a/src/commands/mathBlock.ts b/src/commands/mathBlock.ts index 3fcc7fda..155b0da5 100644 --- a/src/commands/mathBlock.ts +++ b/src/commands/mathBlock.ts @@ -1,5 +1,5 @@ import type { Direction, Constructor } from 'src/constants'; -import { L, R, LatexCmds, CharCmds } from 'src/constants'; +import { LatexCmds, CharCmds } from 'src/constants'; import { RootBlockMixin } from 'src/mixins'; import type { Options } from 'src/options'; import type { Controller } from 'src/controller'; @@ -41,8 +41,8 @@ export const writeMethodMixin = >(Base: TBase) return; } if ( - cursor[L] && - !cursor[R] && + cursor.left && + !cursor.right && !cursor.selection && cursor.options.charsThatBreakOutOfSupSub.includes(ch) && this.parent @@ -74,7 +74,7 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { } text() { - return this.ends[L] && this.ends[L] === this.ends[R] ? this.ends[L].text() : this.join('text'); + return this.ends.left && this.ends.left === this.ends.right ? this.ends.left.text() : this.join('text'); } keystroke(key: string, e: KeyboardEvent, ctrlr: Controller) { @@ -82,10 +82,10 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { ctrlr.options.spaceBehavesLikeTab && ctrlr.cursor.depth() > 1 && (key === 'Spacebar' || key === 'Shift-Spacebar') && - ctrlr.cursor[L]?.ctrlSeq !== ',' + ctrlr.cursor.left?.ctrlSeq !== ',' ) { e.preventDefault(); - ctrlr.escapeDir(key === 'Shift-Spacebar' ? L : R, key, e); + ctrlr.escapeDir(key === 'Shift-Spacebar' ? 'left' : 'right', key, e); return; } super.keystroke(key, e, ctrlr); @@ -96,7 +96,7 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { // the cursor moveOutOf(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { const updownInto = updown && this.parent?.[`${updown}Into`]; - if (!updownInto && this[dir]) cursor.insAtDirEnd(dir === L ? R : L, this[dir]); + if (!updownInto && this[dir]) cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this[dir]); else if (this.parent) cursor.insDirOf(dir, this.parent); } @@ -109,17 +109,17 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { } seek(pageX: number, cursor: Cursor) { - let node = this.ends[R]; + let node = this.ends.right; const rect = node?.elements.firstElement.getBoundingClientRect() ?? undefined; if (!node || (rect?.left ?? 0) + (rect?.width ?? 0) < pageX) { cursor.insAtRightEnd(this); return; } - if (pageX < (this.ends[L]?.elements.firstElement.getBoundingClientRect().left ?? 0)) { + if (pageX < (this.ends.left?.elements.firstElement.getBoundingClientRect().left ?? 0)) { cursor.insAtLeftEnd(this); return; } - while (pageX < (node?.elements.firstElement.getBoundingClientRect().left ?? 0)) node = node?.[L]; + while (pageX < (node?.elements.firstElement.getBoundingClientRect().left ?? 0)) node = node?.left; node?.seek(pageX, cursor); } @@ -130,13 +130,13 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { const block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { - if (cursor.parent) block.children().adopt(cursor.parent, cursor[L], cursor[R]); + if (cursor.parent) block.children().adopt(cursor.parent, cursor.left, cursor.right); const elements = block.domify(); cursor.element.before(...elements.contents); - cursor[L] = block.ends[R]; + cursor.left = block.ends.right; block.finalizeInsert(cursor.options, cursor); - block.ends[R]?.[R]?.siblingCreated?.(cursor.options, L); - block.ends[L]?.[L]?.siblingCreated?.(cursor.options, R); + block.ends.right?.right?.siblingCreated?.(cursor.options, 'left'); + block.ends.left?.left?.siblingCreated?.(cursor.options, 'right'); cursor.parent?.bubble('reflow'); } } @@ -171,20 +171,20 @@ export class RootMathCommand extends writeMethodMixin(MathCommand) { createBlocks() { super.createBlocks(); - const leftEnd = this.ends[L] as RootMathCommand; + const leftEnd = this.ends.left as RootMathCommand; leftEnd.write = (cursor: Cursor, ch: string) => { if (ch !== '$') this.write(cursor, ch); else if (leftEnd.isEmpty()) { if (leftEnd.parent) cursor.insRightOf(leftEnd.parent); - leftEnd.parent?.deleteTowards(L, cursor); + leftEnd.parent?.deleteTowards('left', cursor); new VanillaSymbol('\\$', '$').createLeftOf(cursor.show()); - } else if (!cursor[R] && leftEnd.parent) cursor.insRightOf(leftEnd.parent); - else if (!cursor[L] && leftEnd.parent) cursor.insLeftOf(leftEnd.parent); + } else if (!cursor.right && leftEnd.parent) cursor.insRightOf(leftEnd.parent); + else if (!cursor.left && leftEnd.parent) cursor.insLeftOf(leftEnd.parent); else this.write(cursor, ch); }; } latex() { - return `$${this.ends[L]?.latex() ?? ''}$`; + return `$${this.ends.left?.latex() ?? ''}$`; } } diff --git a/src/commands/mathElements.ts b/src/commands/mathElements.ts index 4981b796..5c9532ce 100644 --- a/src/commands/mathElements.ts +++ b/src/commands/mathElements.ts @@ -1,7 +1,7 @@ // Abstract classes of math blocks and commands. import type { Direction, Constructor } from 'src/constants'; -import { noop, L, R, mqCmdId, mqBlockId, LatexCmds, OPP_BRACKS, BuiltInOpNames, TwoWordOpNames } from 'src/constants'; +import { noop, mqCmdId, mqBlockId, LatexCmds, OPP_BRACKS, BuiltInOpNames, TwoWordOpNames } from 'src/constants'; import { Parser } from 'services/parser.util'; import { Selection } from 'src/selection'; import { deleteSelectTowardsMixin, DelimsMixin } from 'src/mixins'; @@ -28,8 +28,8 @@ export class MathElement extends TNode { this.postOrder('blur'); this.postOrder('reflow'); - this[R]?.siblingCreated?.(options, L); - this[L]?.siblingCreated?.(options, R); + this.right?.siblingCreated?.(options, 'left'); + this.left?.siblingCreated?.(options, 'right'); this.bubble('reflow'); } @@ -106,7 +106,7 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { this.blocks = blocks; for (const block of blocks) { - block.adopt(this, this.ends[R]); + block.adopt(this, this.ends.right); } return this; @@ -135,16 +135,16 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { for (let i = 0; i < numBlocks; ++i) { this.blocks[i] = new MathBlock(); - this.blocks[i].adopt(this, this.ends[R]); + this.blocks[i].adopt(this, this.ends.right); } } placeCursor(cursor: Cursor) { - if (!this.ends[L]) return; + if (!this.ends.left) return; // Insert the cursor at the right end of the first empty child, searching from // left to right, or if not empty, then to the right end of the child. cursor.insAtRightEnd( - this.foldChildren(this.ends[L], (leftward, child) => (leftward.isEmpty() ? leftward : child)) + this.foldChildren(this.ends.left, (leftward, child) => (leftward.isEmpty() ? leftward : child)) ); } @@ -153,43 +153,43 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { } unselectInto(dir: Direction, cursor: Cursor) { - cursor.insAtDirEnd(dir === L ? R : L, cursor.anticursor?.ancestors?.[this.id] as TNode); + cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', cursor.anticursor?.ancestors?.[this.id] as TNode); } seek(pageX: number, cursor: Cursor) { const getBounds = (node: TNode) => { const rect = node.elements.firstElement.getBoundingClientRect(); - return { [L]: rect.left, [R]: rect.left + rect.width }; + return { left: rect.left, right: rect.left + rect.width }; }; const cmdBounds = getBounds(this); - if (pageX < cmdBounds[L]) { + if (pageX < cmdBounds.left) { cursor.insLeftOf(this); return; } - if (pageX > cmdBounds[R]) { + if (pageX > cmdBounds.right) { cursor.insRightOf(this); return; } - let leftLeftBound = cmdBounds[L]; + let leftLeftBound = cmdBounds.left; this.eachChild((block: TNode) => { const blockBounds = getBounds(block); - if (pageX < blockBounds[L]) { + if (pageX < blockBounds.left) { // closer to this block's left bound, or the bound left of that? - if (pageX - leftLeftBound < blockBounds[L] - pageX) { - if (block[L]) cursor.insAtRightEnd(block[L]); + if (pageX - leftLeftBound < blockBounds.left - pageX) { + if (block.left) cursor.insAtRightEnd(block.left); else cursor.insLeftOf(this); } else cursor.insAtLeftEnd(block); return false; - } else if (pageX > blockBounds[R]) { - if (block[R]) - leftLeftBound = blockBounds[R]; // continue to next block + } else if (pageX > blockBounds.right) { + if (block.right) + leftLeftBound = blockBounds.right; // continue to next block else { // last (rightmost) block // closer to this block's right bound, or the this's right bound? - if (cmdBounds[R] - pageX < pageX - blockBounds[R]) { + if (cmdBounds.right - pageX < pageX - blockBounds.right) { cursor.insRightOf(this); } else cursor.insAtRightEnd(block); } @@ -351,10 +351,10 @@ export class Symbol extends MathCommand { } moveTowards(dir: Direction, cursor: Cursor) { - if (dir === L) this.elements.first.before(cursor.element); + if (dir === 'left') this.elements.first.before(cursor.element); else this.elements.last.after(cursor.element); - cursor[dir === L ? R : L] = this; + cursor[dir === 'left' ? 'right' : 'left'] = this; cursor[dir] = this[dir]; } @@ -429,7 +429,7 @@ export class Inequality extends BinaryOperator { } deleteTowards(dir: Direction, cursor: Cursor) { - if (dir === L && !this.strict) { + if (dir === 'left' && !this.strict) { this.swap(true); this.bubble('reflow'); return; @@ -463,9 +463,9 @@ export class Equality extends BinaryOperator { } createLeftOf(cursor: Cursor) { - if (cursor[L] instanceof Inequality && cursor[L].strict) { - cursor[L].swap(false); - cursor[L].bubble('reflow'); + if (cursor.left instanceof Inequality && cursor.left.strict) { + cursor.left.swap(false); + cursor.left.bubble('reflow'); return; } super.createLeftOf(cursor); @@ -477,8 +477,8 @@ export class Digit extends VanillaSymbol { if ( cursor.options.autoSubscriptNumerals && cursor.parent !== cursor.parent?.parent?.sub && - ((cursor[L] instanceof Variable && cursor[L].isItalic) || - (cursor[L] instanceof SupSub && cursor[L][L] instanceof Variable && cursor[L][L].isItalic)) + ((cursor.left instanceof Variable && cursor.left.isItalic) || + (cursor.left instanceof SupSub && cursor.left.left instanceof Variable && cursor.left.left.isItalic)) ) { new LatexCmds._().createLeftOf(cursor); super.createLeftOf(cursor); @@ -502,7 +502,8 @@ export class Variable extends Symbol { else text = text.slice(1, text.length); } else if (text.endsWith(' ') || text.endsWith('}')) { text = text.slice(0, -1); - if (!(this[R] instanceof Bracket || this[R] instanceof Fraction || this[R] instanceof SupSub)) text += ' '; + if (!(this.right instanceof Bracket || this.right instanceof Fraction || this.right instanceof SupSub)) + text += ' '; } return text; } @@ -532,16 +533,16 @@ export class Letter extends Variable { // FIXME: l.ctrlSeq === l.letter checks if first or last in an operator name while (l instanceof Letter && l.ctrlSeq === l.letter && i < maxLength) { str = l.letter + str; - l = l[L]; + l = l.left; ++i; } // Check for an autocommand, going through substrings from longest to shortest. while (str.length) { if (autoCmds[str]) { // eslint-disable-next-line @typescript-eslint/no-this-alias - for (i = 1, l = this; i < str.length; ++i, l = l?.[L]); + for (i = 1, l = this; i < str.length; ++i, l = l?.left); new Fragment(l, this).remove(); - cursor[L] = l?.[L]; + cursor.left = l?.left; new LatexCmds[str](str).createLeftOf(cursor); return; } @@ -558,9 +559,9 @@ export class Letter extends Variable { } finalizeTree(opts: Options, dir?: Direction) { - // don't auto-un-italicize if the sibling to my right changed (dir === R or + // don't auto-un-italicize if the sibling to my right changed (dir === 'right' or // undefined) and it's now a Letter, it will un-italicize everyone - if (dir !== L && this[R] instanceof Letter) return; + if (dir !== 'left' && this.right instanceof Letter) return; this.autoUnItalicize(opts); } @@ -570,14 +571,14 @@ export class Letter extends Variable { // want longest possible operator names, so join together entire contiguous // sequence of letters let str = this.letter, - l = this[L], - r = this[R]; - for (; l instanceof Letter; l = l[L]) str = l.letter + str; - for (; r instanceof Letter; r = r[R]) str += r.letter; + l = this.left, + r = this.right; + for (; l instanceof Letter; l = l.left) str = l.letter + str; + for (; r instanceof Letter; r = r.right) str += r.letter; // removeClass and delete flags from all letters before figuring out // which, if any, are part of an operator name - new Fragment(l?.[R] || this.parent?.ends[L], r?.[L] || this.parent?.ends[R]).each((el: TNode) => { + new Fragment(l?.right || this.parent?.ends.left, r?.left || this.parent?.ends.right).each((el: TNode) => { (el as Letter).italicize(true).elements.removeClass('mq-first', 'mq-last', 'mq-followed-by-supsub'); el.ctrlSeq = (el as Letter).letter; return true; @@ -585,12 +586,12 @@ export class Letter extends Variable { // check for operator names: at each position from left to right, check // substrings from longest to shortest - for (let i = 0, first = l?.[R] || this.parent?.ends[L]; i < str.length; ++i, first = first?.[R]) { + for (let i = 0, first = l?.right || this.parent?.ends.left; i < str.length; ++i, first = first?.right) { for (let len = Math.min(autoOps._maxLength, str.length - i); len > 0; --len) { const word = str.slice(i, i + len); if (autoOps[word]) { let last; - for (let j = 0, letter = first; j < len; j += 1, letter = letter?.[R]) { + for (let j = 0, letter = first; j < len; j += 1, letter = letter?.right) { (letter as Letter).italicize(false); last = letter; } @@ -598,10 +599,10 @@ export class Letter extends Variable { const isBuiltIn = BuiltInOpNames[word] as 1 | undefined; (first as Letter).ctrlSeq = (isBuiltIn ? '\\' : '\\operatorname{') + (first?.ctrlSeq ?? ''); (last as Letter).ctrlSeq += isBuiltIn ? ' ' : '}'; - if (word in TwoWordOpNames) last?.[L]?.[L]?.[L]?.elements.addClass('mq-last'); - if (!this.shouldOmitPadding(first?.[L])) first?.elements.addClass('mq-first'); - if (!this.shouldOmitPadding(last?.[R])) { - const rightward = last?.[R]; + if (word in TwoWordOpNames) last?.left?.left?.left?.elements.addClass('mq-last'); + if (!this.shouldOmitPadding(first?.left)) first?.elements.addClass('mq-first'); + if (!this.shouldOmitPadding(last?.right)) { + const rightward = last?.right; if (rightward instanceof SupSub) rightward.siblingCreated?.(opts); else last?.elements.toggleClass('mq-last', !(rightward instanceof Bracket)); } @@ -627,7 +628,7 @@ export function insLeftOfMeUnlessAtEnd(this: SupSub, cursor: Cursor) { if (!cmd) return; let ancestorCmd: TNode | Point | undefined = cursor; do { - if (ancestorCmd?.[R]) return cursor.insLeftOf(cmd); + if (ancestorCmd?.right) return cursor.insLeftOf(cmd); ancestorCmd = ancestorCmd?.parent?.parent; } while (ancestorCmd !== cmd); cursor.insRightOf(cmd); @@ -648,8 +649,8 @@ export class Fraction extends MathCommand { } text() { - let leftward = this[L]; - for (; leftward && leftward.ctrlSeq === '\\ '; leftward = leftward[L]); + let leftward = this.left; + for (; leftward && leftward.ctrlSeq === '\\ '; leftward = leftward.left); const text = (dir: Direction) => { let needParens = false; @@ -686,22 +687,22 @@ export class Fraction extends MathCommand { return !needParens; }); - const blankDefault = dir === L ? '0' : '1'; + const blankDefault = dir === 'left' ? '0' : '1'; const l = this.ends[dir]?.text() !== ' ' && this.ends[dir]?.text(); return l ? ((needParens as boolean) ? `(${l})` : l) : blankDefault; }; return (leftward instanceof BinaryOperator && leftward.isUnary) || leftward?.elements.hasClass('mq-operator-name') || - (leftward instanceof SupSub && leftward[L]?.elements.hasClass('mq-operator-name')) - ? `(${text(L)}/${text(R)})` - : ` ${text(L)}/${text(R)} `; + (leftward instanceof SupSub && leftward.left?.elements.hasClass('mq-operator-name')) + ? `(${text('left')}/${text('right')})` + : ` ${text('left')}/${text('right')} `; } finalizeTree() { - this.upInto = this.ends[L]; - if (this.ends[R]) this.ends[R].upOutOf = this.ends[L]; - this.downInto = this.ends[R]; - if (this.ends[L]) this.ends[L].downOutOf = this.ends[R]; + this.upInto = this.ends.left; + if (this.ends.right) this.ends.right.upOutOf = this.ends.left; + this.downInto = this.ends.right; + if (this.ends.left) this.ends.left.downOutOf = this.ends.right; } } @@ -775,13 +776,13 @@ export class SupSub extends MathCommand { this.siblingCreated = this.siblingDeleted = (options: Options) => { if ( - this[L] instanceof Letter && - this[L].isPartOfOperator && - this[R] && + this.left instanceof Letter && + this.left.isPartOfOperator && + this.right && !( - this[R] instanceof BinaryOperator || - this[R] instanceof UpperLowerLimitCommand || - this[R] instanceof Bracket + this.right instanceof BinaryOperator || + this.right instanceof UpperLowerLimitCommand || + this.right instanceof Bracket ) ) this.elements.addClass('mq-after-operator-name'); @@ -803,11 +804,11 @@ export class SupSub extends MathCommand { } createLeftOf(cursor: Cursor) { - if (this.hasValidBase(cursor.options, cursor[L], cursor.parent)) { + if (this.hasValidBase(cursor.options, cursor.left, cursor.parent)) { // If this SupSub is being placed on a fraction, then add parentheses around the fraction. - if (cursor[L] instanceof Fraction) { - const brack = new Bracket(R, '(', ')', '(', ')'); - cursor.selection = (cursor[L] as TNode).selectChildren(); + if (cursor.left instanceof Fraction) { + const brack = new Bracket('right', '(', ')', '(', ')'); + cursor.selection = (cursor.left as TNode).selectChildren(); brack.replaces(cursor.replaceSelection()); brack.createLeftOf(cursor); } @@ -817,8 +818,8 @@ export class SupSub extends MathCommand { } if (this.replacedFragment) { - if (cursor.parent) this.replacedFragment.adopt(cursor.parent, cursor[L], cursor[R]); - cursor[L] = this.replacedFragment.ends[R]; + if (cursor.parent) this.replacedFragment.adopt(cursor.parent, cursor.left, cursor.right); + cursor.left = this.replacedFragment.ends.right; } } @@ -828,7 +829,7 @@ export class SupSub extends MathCommand { // insert this block's children into its block, unless this block has none, in which case insert the cursor into // its block (and not this one, since this one will be removed). If something to the left has been deleted, // then this SupSub will be merged with one to the left if it is left next to it after the deletion. - for (const dir of [L, R]) { + for (const dir of ['left', 'right'] as Direction[]) { if (this[dir] instanceof SupSub) { let pt; for (const supsub of ['sub', 'sup'] as (keyof Pick)[]) { @@ -838,23 +839,23 @@ export class SupSub extends MathCommand { if (!dest) this[dir].addBlock(src.disown()); else if (!src.isEmpty()) { // Insert src children at -dir end of dest - src.elements.children().insAtDirEnd(dir === L ? R : L, dest.elements); + src.elements.children().insAtDirEnd(dir === 'left' ? 'right' : 'left', dest.elements); const children = src.children().disown(); - pt = new Point(dest, children.ends[R], dest.ends[L]); - if (dir === L) children.adopt(dest, dest.ends[R]); - else children.adopt(dest, undefined, dest.ends[L]); - } else pt = new Point(dest, undefined, dest.ends[L]); - this.placeCursor = (cursor) => cursor.insAtDirEnd(dir === L ? R : L, dest || src); + pt = new Point(dest, children.ends.right, dest.ends.left); + if (dir === 'left') children.adopt(dest, dest.ends.right); + else children.adopt(dest, undefined, dest.ends.left); + } else pt = new Point(dest, undefined, dest.ends.left); + this.placeCursor = (cursor) => cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', dest || src); } this.remove(); if (cursor) { - if (cursor[L] === this) { - if (dir === R && pt) { - if (pt[L]) cursor.insRightOf(pt[L]); + if (cursor.left === this) { + if (dir === 'right' && pt) { + if (pt.left) cursor.insRightOf(pt.left); else if (pt.parent) cursor.insAtLeftEnd(pt.parent); } else cursor.insRightOf(this[dir] as TNode); } else { - if (pt?.[R]) cursor.insRightOf(pt[R]); + if (pt?.right) cursor.insRightOf(pt.right); } } return; @@ -883,24 +884,27 @@ export class SupSub extends MathCommand { } finalizeTree() { - if (this.ends[L]) this.ends[L].isSupSubLeft = true; + if (this.ends.left) this.ends.left.isSupSubLeft = true; } // Check to see if this has an invalid base. If so bring the children out, and remove this SupSub. maybeFlatten(options: Options) { const cursor = this.getController()?.cursor; - const leftward = cursor?.[R] === this ? cursor[L] : this[L]; + const leftward = cursor?.right === this ? cursor.left : this.left; if (!this.hasValidBase(options, leftward, this.parent) || leftward instanceof Fraction) { for (const supsub of ['sub', 'sup'] as (keyof Pick)[]) { const src = this[supsub]; if (!src) continue; if (this.parent) - src.children().disown().adopt(this.parent, this[L], this).elements.insDirOf(L, this.elements); + src.children() + .disown() + .adopt(this.parent, this.left, this) + .elements.insDirOf('left', this.elements); } this.remove(); - if (leftward === cursor?.[L]) { - if (cursor?.[L]?.[R]) cursor.insLeftOf(cursor[L][R]); - else if (cursor?.parent) cursor.insAtDirEnd(L, cursor.parent); + if (leftward === cursor?.left) { + if (cursor?.left?.right) cursor.insLeftOf(cursor.left.right); + else if (cursor?.parent) cursor.insAtDirEnd('left', cursor.parent); } } } @@ -912,14 +916,14 @@ export class SupSub extends MathCommand { deleteTowards(dir: Direction, cursor: Cursor) { if (cursor.options.autoSubscriptNumerals && this.sub) { - const cmd = this.sub.ends[dir === L ? R : L]; + const cmd = this.sub.ends[dir === 'left' ? 'right' : 'left']; if (cmd instanceof Symbol) cmd.remove(); - else if (cmd) cmd.deleteTowards(dir, cursor.insAtDirEnd(dir === L ? R : L, this.sub)); + else if (cmd) cmd.deleteTowards(dir, cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this.sub)); // TODO: factor out a .removeBlock() or something if (this.sub.isEmpty()) { - this.sub.deleteOutOf(L, cursor.insAtLeftEnd(this.sub)); - if (this.sup) cursor.insDirOf(dir === L ? R : L, this); + this.sub.deleteOutOf('left', cursor.insAtLeftEnd(this.sub)); + if (this.sup) cursor.insDirOf(dir === 'left' ? 'right' : 'left', this); // Note `-dir` because in e.g. x_1^2| want backspacing (leftward) // to delete the 1 but to end up rightward of x^2; with non-negated // `dir` (try it), the cursor appears to have gone "through" the ^2. @@ -937,7 +941,7 @@ export class SupSub extends MathCommand { text() { const mainText = supSubText('_', this.sub) + supSubText('^', this.sup); - return mainText + (mainText && this[R] instanceof Digit ? ' ' : ''); + return mainText + (mainText && this.right instanceof Digit ? ' ' : ''); } addBlock(block: TNode) { @@ -982,12 +986,12 @@ export class SupSub extends MathCommand { // This sets that up when the second sup or sub is created (or when parsing and both exist). thisSupsub.deleteOutOf = (dir: Direction, cursor: Cursor) => { if (thisSupsub.isEmpty()) { - cursor[dir === L ? R : L] = thisSupsub.ends[dir]; + cursor[dir === 'left' ? 'right' : 'left'] = thisSupsub.ends[dir]; this.supsub = oppositeSupsub; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this[supsub]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this[`${updown}Into`]; + if (supsub === 'sup') delete this.sup; + else delete this.sub; + if (updown === 'up') delete this.upInto; + else delete this.downInto; const remainingSupsub = this[oppositeSupsub] as SupSub; remainingSupsub[`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; remainingSupsub.deleteOutOf = (dir: Direction, cursor: Cursor) => { @@ -1001,28 +1005,33 @@ export class SupSub extends MathCommand { thisSupsub.remove(); } if (thisSupsub.parent) - cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent); + cursor.insDirOf(thisSupsub[dir] ? (dir === 'left' ? 'right' : 'left') : dir, thisSupsub.parent); }; } else { thisSupsub.deleteOutOf = (dir: Direction, cursor: Cursor) => { if (thisSupsub.parent) - cursor.insDirOf(thisSupsub[dir] ? (dir === L ? R : L) : dir, thisSupsub.parent); + cursor.insDirOf(thisSupsub[dir] ? (dir === 'left' ? 'right' : 'left') : dir, thisSupsub.parent); if (!thisSupsub.isEmpty()) { const end = thisSupsub.ends[dir]; if (cursor.parent) { thisSupsub .children() .disown() - .withDirAdopt(dir, cursor.parent, cursor[dir], cursor[dir === L ? R : L]) - .elements.insDirOf(dir === L ? R : L, cursor.element); + .withDirAdopt( + dir, + cursor.parent, + cursor[dir], + cursor[dir === 'left' ? 'right' : 'left'] + ) + .elements.insDirOf(dir === 'left' ? 'right' : 'left', cursor.element); } - cursor[dir === L ? R : L] = end; + cursor[dir === 'left' ? 'right' : 'left'] = end; } this.supsub = oppositeSupsub; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this[supsub]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this[`${updown}Into`]; + if (supsub === 'sup') delete this.sup; + else delete this.sub; + if (updown === 'up') delete this.upInto; + else delete this.downInto; if (this[oppositeSupsub]) this[oppositeSupsub][`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; delete (this[oppositeSupsub] as Partial).deleteOutOf; if (supsub === 'sub') { @@ -1039,18 +1048,18 @@ export class SupSub extends MathCommand { export class UpperLowerLimitCommand extends MathCommand { latex() { const simplify = (latex: string) => (latex.length === 1 ? latex : `{${latex || ' '}}`); - return `${this.ctrlSeq}_${simplify(this.ends[L]?.latex() ?? '')}^${simplify(this.ends[R]?.latex() ?? '')}`; + return `${this.ctrlSeq}_${simplify(this.ends.left?.latex() ?? '')}^${simplify(this.ends.right?.latex() ?? '')}`; } text() { const operand = this.ctrlSeq.slice(1, this.ctrlSeq.length - 1); - return `${operand}(${this.ends[L]?.text() ?? ''},${this.ends[R]?.text() ?? ''})`; + return `${operand}(${this.ends.left?.text() ?? ''},${this.ends.right?.text() ?? ''})`; } parser() { const blocks = (this.blocks = [new MathBlock(), new MathBlock()]); for (const block of blocks) { - block.adopt(this, this.ends[R]); + block.adopt(this, this.ends.right); } return Parser.optWhitespace @@ -1058,7 +1067,7 @@ export class UpperLowerLimitCommand extends MathCommand { .then((supOrSub) => { const child = blocks[supOrSub === '_' ? 0 : 1]; return latexMathParser.block.then((block: TNode) => { - block.children().adopt(child, child.ends[R]); + block.children().adopt(child, child.ends.right); return Parser.succeed(this); }); }) @@ -1067,10 +1076,10 @@ export class UpperLowerLimitCommand extends MathCommand { } finalizeTree() { - this.downInto = this.ends[L]; - this.upInto = this.ends[R]; - if (this.ends[L]) this.ends[L].upOutOf = this.ends[R]; - if (this.ends[R]) this.ends[R].downOutOf = this.ends[L]; + this.downInto = this.ends.left; + this.upInto = this.ends.right; + if (this.ends.left) this.ends.left.upOutOf = this.ends.right; + if (this.ends.right) this.ends.right.downOutOf = this.ends.left; } } @@ -1078,11 +1087,11 @@ type BracketType = Bracket | MathFunction; const BracketMixin = >(Base: TBase) => class extends DelimsMixin(Base) { - side?: Direction = L; + side?: Direction = 'left'; sides: { - [L]: { ch: string; ctrlSeq: string }; - [R]: { ch: string; ctrlSeq: string }; - } = { [L]: { ch: '(', ctrlSeq: '(' }, [R]: { ch: ')', ctrlSeq: ')' } }; + left: { ch: string; ctrlSeq: string }; + right: { ch: string; ctrlSeq: string }; + } = { left: { ch: '(', ctrlSeq: '(' }, right: { ch: ')', ctrlSeq: ')' } }; inserted = false; matchBrack(opts: Options, expectedSide?: Direction, node?: TNode): false | BracketType { @@ -1093,10 +1102,10 @@ const BracketMixin = >(Base: TBase) => (!(this instanceof MathFunction) || (!!node.side && node.sides[node.side].ch) === ')')) || (node instanceof MathFunction && !!this.side && this.sides[this.side].ch === ')')) && !!node.side && - node.side !== (expectedSide === L ? R : expectedSide === R ? L : 0) && + node.side !== (expectedSide === 'left' ? 'right' : expectedSide === 'right' ? 'left' : 0) && (!opts.restrictMismatchedBrackets || (this.side && OPP_BRACKS[this.sides[this.side].ch] === node.sides[node.side].ch) || - { '(': ']', '[': ')' }[this.sides[L].ch] === node.sides[R].ch) && + { '(': ']', '[': ')' }[this.sides.left.ch] === node.sides.right.ch) && node ); } @@ -1106,7 +1115,7 @@ const BracketMixin = >(Base: TBase) => if (!this.side) return; // Copy this objects info to brack as this may be a different type of bracket (like (a, b]). brack.sides[this.side] = this.sides[this.side]; - const delim = brack.delims?.[this.side === L ? 0 : 1]; + const delim = brack.delims?.[this.side === 'left' ? 0 : 1]; if (delim) { delim.classList.remove('mq-ghost'); delim.innerHTML = this.sides[this.side].ch; @@ -1114,19 +1123,19 @@ const BracketMixin = >(Base: TBase) => } createLeftOf(cursor: Cursor) { - let brack, side; + let brack, side: Direction | undefined; if (!this.replacedFragment) { // If a selection is not being wrapped in this bracket, check to see if this // bracket is next to or inside an opposing one-sided bracket. const opts = cursor.options; - if (this.sides[L].ch === '|') { + if (this.sides.left.ch === '|') { // Check both sides if this is an absolute value bracket. brack = - this.matchBrack(opts, R, cursor[R]) || - this.matchBrack(opts, L, cursor[L]) || + this.matchBrack(opts, 'right', cursor.right) || + this.matchBrack(opts, 'left', cursor.left) || this.matchBrack(opts, undefined, cursor.parent?.parent); } else { - const otherSide = this.side === L ? R : L; + const otherSide = this.side === 'left' ? 'right' : 'left'; brack = this.matchBrack(opts, otherSide, cursor[otherSide]) || this.matchBrack(opts, otherSide, cursor.parent?.parent); @@ -1134,22 +1143,22 @@ const BracketMixin = >(Base: TBase) => } if (brack) { // brack may be an absolute value with .side not yet set - side = this.side = brack.side === L ? R : L; + side = this.side = brack.side === 'left' ? 'right' : 'left'; // Move the stuff between this bracket and the ghost of the other bracket outside. if (brack === cursor.parent?.parent && cursor[side] && brack.parent) { - new Fragment(cursor[side], cursor.parent.ends[side], side === L ? R : L) + new Fragment(cursor[side], cursor.parent.ends[side], side === 'left' ? 'right' : 'left') .disown() - .withDirAdopt(side === L ? R : L, brack.parent, brack, brack[side]) + .withDirAdopt(side === 'left' ? 'right' : 'left', brack.parent, brack, brack[side]) .elements.insDirOf(side, brack.elements); } - if (this instanceof MathFunction && side === L) { + if (this instanceof MathFunction && side === 'left') { // If a math function is typed between a ghosted left parenthesis and a solid right parenthesis, // then adopt its contents and replace the parentheses. const rightward = - brack === cursor.parent?.parent && cursor[R] !== brack.ends[R]?.[R] - ? new Fragment(cursor[R], brack.ends[R]?.ends[R], L).disown() + brack === cursor.parent?.parent && cursor.right !== brack.ends.right?.right + ? new Fragment(cursor.right, brack.ends.right?.ends.right, 'left').disown() : undefined; cursor.insRightOf(brack); delete this.side; @@ -1168,13 +1177,13 @@ const BracketMixin = >(Base: TBase) => if ( this instanceof MathFunction && !this.replacedFragment && - cursor[R] instanceof Bracket && - cursor[R].sides[L].ch === '(' && - cursor[R].sides[R].ch === ')' + cursor.right instanceof Bracket && + cursor.right.sides.left.ch === '(' && + cursor.right.sides.right.ch === ')' ) { - const paren = cursor[R]; + const paren = cursor.right; this.side = paren.side; - this.replaces(new Fragment(paren.ends[L]?.ends[L], paren.ends[R]?.ends[R], L)); + this.replaces(new Fragment(paren.ends.left?.ends.left, paren.ends.right?.ends.right, 'left')); super.createLeftOf(cursor); paren.remove(); this.bubble('reflow'); @@ -1186,23 +1195,27 @@ const BracketMixin = >(Base: TBase) => side = brack.side; // If wrapping a selection, don't be one-sided. if (brack.replacedFragment) delete brack.side; - else if (cursor[side === L ? R : L]) { + else if (cursor[side === 'left' ? 'right' : 'left']) { // Auto-expand so the ghost is at the far end. brack.replaces( - new Fragment(cursor[side === L ? R : L], cursor.parent?.ends[side === L ? R : L], side) + new Fragment( + cursor[side === 'left' ? 'right' : 'left'], + cursor.parent?.ends[side === 'left' ? 'right' : 'left'], + side + ) ); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete cursor[side === L ? R : L]; + if (side === 'left') delete cursor.right; + else delete cursor.left; } super.createLeftOf(cursor); } - if (side === L && brack.ends[L]) cursor.insAtLeftEnd(brack.ends[L]); + if (side === 'left' && brack.ends.left) cursor.insAtLeftEnd(brack.ends.left); else cursor.insRightOf(brack); } unwrap() { const node = this.parent - ? this.blocks[this.contentIndex].children().disown().adopt(this.parent, this, this[R]) + ? this.blocks[this.contentIndex].children().disown().adopt(this.parent, this, this.right) : undefined; if (node) this.elements.last.after(...node.elements.contents); this.remove(); @@ -1216,13 +1229,13 @@ const BracketMixin = >(Base: TBase) => if (side === this.side) { // If deleting a non-ghost of a one-sided bracket, then unwrap the bracket. this.unwrap(); - if (sib) cursor.insDirOf(side === L ? R : L, sib); + if (sib) cursor.insDirOf(side === 'left' ? 'right' : 'left', sib); else if (parent) cursor.insAtDirEnd(side, parent); return; } const wasSolid = !this.side; - this.side = side === L ? R : L; + this.side = side === 'left' ? 'right' : 'left'; // If deleting a like, outer close-brace of [(1+2)+3} where the inner open-paren // is a ghost then become [1+2)+3. if (this.matchBrack(cursor.options, side, this.blocks[this.contentIndex].ends[this.side])) { @@ -1230,7 +1243,7 @@ const BracketMixin = >(Base: TBase) => const origEnd = this.blocks[this.contentIndex].ends[side]; this.unwrap(); origEnd?.siblingCreated?.(cursor.options, side); - if (sib) cursor.insDirOf(side === L ? R : L, sib); + if (sib) cursor.insDirOf(side === 'left' ? 'right' : 'left', sib); else if (parent) cursor.insAtDirEnd(side, parent); } else { // If deleting a like, inner close-brace of ([1+2}+3) where the outer open-paren is a ghost, @@ -1241,7 +1254,7 @@ const BracketMixin = >(Base: TBase) => } else if (outward && wasSolid) { // If deleting outward from a solid pair, unwrap. this.unwrap(); - if (sib) cursor.insDirOf(side === L ? R : L, sib); + if (sib) cursor.insDirOf(side === 'left' ? 'right' : 'left', sib); else if (parent) cursor.insAtDirEnd(side, parent); return; } else { @@ -1252,7 +1265,7 @@ const BracketMixin = >(Base: TBase) => }; this.delims?.forEach((delim, index) => { delim.classList.remove('mq-ghost'); - if (index === (side === L ? 0 : 1)) { + if (index === (side === 'left' ? 0 : 1)) { delim.classList.add('mq-ghost'); delim.innerHTML = this.sides[side].ch; } @@ -1262,12 +1275,12 @@ const BracketMixin = >(Base: TBase) => // Auto-expand so the ghost is at the far end. const origEnd = this.blocks[this.contentIndex].ends[side]; this.blocks[this.contentIndex].elements.removeClass('mq-empty'); - new Fragment(sib, farEnd, side === L ? R : L) + new Fragment(sib, farEnd, side === 'left' ? 'right' : 'left') .disown() - .withDirAdopt(side === L ? R : L, this.blocks[this.contentIndex], origEnd) + .withDirAdopt(side === 'left' ? 'right' : 'left', this.blocks[this.contentIndex], origEnd) .elements.insAtDirEnd(side, this.blocks[this.contentIndex].elements); origEnd?.siblingCreated?.(cursor.options, side); - cursor.insDirOf(side === L ? R : L, sib); + cursor.insDirOf(side === 'left' ? 'right' : 'left', sib); } else { // Otherwise the cursor goes just outside or just inside the parentheses. if (outward) cursor.insDirOf(side, this); @@ -1277,7 +1290,7 @@ const BracketMixin = >(Base: TBase) => } deleteTowards(dir: Direction, cursor: Cursor) { - this.deleteSide(dir === L ? R : L, false, cursor); + this.deleteSide(dir === 'left' ? 'right' : 'left', false, cursor); } finalizeTree() { @@ -1289,7 +1302,7 @@ const BracketMixin = >(Base: TBase) => return; } - this.delims?.[this.side === L ? 1 : 0].classList.remove('mq-ghost'); + this.delims?.[this.side === 'left' ? 1 : 0].classList.remove('mq-ghost'); delete this.side; } }; @@ -1302,14 +1315,14 @@ export class Bracket extends BracketMixin(MathCommand) { super(`\\left${ctrlSeq}`, undefined, [open, close]); this.side = side; this.sides = { - [L]: { ch: open, ctrlSeq: ctrlSeq }, - [R]: { ch: close, ctrlSeq: end } + left: { ch: open, ctrlSeq: ctrlSeq }, + right: { ch: close, ctrlSeq: end } }; this.placeCursor = noop; // If something is typed between the ghost and the far end of its block, then solidify the ghost. this.siblingCreated = (_opts: Options, dir?: Direction) => { - if (dir === (this.side === L ? R : L)) this.finalizeTree(); + if (dir === (this.side === 'left' ? 'right' : 'left')) this.finalizeTree(); }; } @@ -1321,23 +1334,23 @@ export class Bracket extends BracketMixin(MathCommand) { // Wait until now to set the html template so that .side may be set by createLeftOf or the parser. this.htmlTemplate = '' + - `` + - this.sides[L].ch + + `` + + this.sides.left.ch + '' + '&0' + - `` + - this.sides[R].ch + + `` + + this.sides.right.ch + '' + ''; return super.html(); } latex() { - return `\\left${this.sides[L].ctrlSeq}${this.ends[L]?.latex() ?? ''}\\right${this.sides[R].ctrlSeq}`; + return `\\left${this.sides.left.ctrlSeq}${this.ends.left?.latex() ?? ''}\\right${this.sides.right.ctrlSeq}`; } text() { - return `${this.sides[L].ch}${this.ends[L]?.text() ?? ''}${this.sides[R].ch}`; + return `${this.sides.left.ch}${this.ends.left?.text() ?? ''}${this.sides.right.ch}`; } } @@ -1354,7 +1367,7 @@ export class MathFunction extends BracketMixin(MathCommand) { // Also if something is typed before or after the function, // then determine if padding is needed before the function name. this.siblingCreated = (_opts: Options, dir?: Direction) => { - if (dir === R) this.finalizeTree(); + if (dir === 'right') this.finalizeTree(); else this.updateFirst(); }; this.siblingDeleted = () => { @@ -1365,9 +1378,9 @@ export class MathFunction extends BracketMixin(MathCommand) { // Add or remove padding depending on what is before the function name. updateFirst() { if ( - this[L] && - !(this[L] instanceof BinaryOperator) && - (!(this[L] instanceof Variable) || !this[L].isPartOfOperator) + this.left && + !(this.left instanceof BinaryOperator) && + (!(this.left instanceof Variable) || !this.left.isPartOfOperator) ) (this.elements.first as HTMLEmbedElement).classList.add('mq-first'); else (this.elements.first as HTMLEmbedElement).classList.remove('mq-first'); @@ -1392,7 +1405,7 @@ export class MathFunction extends BracketMixin(MathCommand) { '' + '(' + '&1' + - `)` + + `)` + '' + ''; @@ -1404,7 +1417,7 @@ export class MathFunction extends BracketMixin(MathCommand) { // If at the left end of the supsub block and a character was typed that extends this function name to // another one, then extend the function name. if ( - !cursor[L] && + !cursor.left && (LatexCmds[`${this.ctrlSeq.slice(1)}${ch}`] as Constructor | undefined)?.prototype instanceof MathFunction ) { @@ -1414,7 +1427,7 @@ export class MathFunction extends BracketMixin(MathCommand) { return true; } - this.enterContentBlock(L, cursor); + this.enterContentBlock('left', cursor); // If a starting parenthesis is typed, don't actually create it. Just move the cursor to the content // block, and let the hardcoded parenthesis appear. So it appears as if typing the parenthesis does add @@ -1428,35 +1441,35 @@ export class MathFunction extends BracketMixin(MathCommand) { } enterContentBlock(dir: Direction, cursor: Cursor) { - if (dir === L) cursor.insAtLeftEnd(this.blocks[1]); + if (dir === 'left') cursor.insAtLeftEnd(this.blocks[1]); else cursor.insAtRightEnd(this.blocks[1]); } deleteSide(side: Direction, outward: boolean, cursor: Cursor) { - if (side === L && !(this.blocks[0].isEmpty() && this.blocks[1].isEmpty())) { + if (side === 'left' && !(this.blocks[0].isEmpty() && this.blocks[1].isEmpty())) { let brack, supsub; if (!this.blocks[1].isEmpty()) { cursor.insLeftOf(this); - brack = new Bracket(L, '(', ')', '(', ')'); + brack = new Bracket('left', '(', ')', '(', ')'); brack.replaces(this.blocks[1].children()); brack.createLeftOf(cursor); // Make the parentheses one sided if this function was. if (this.side) { - brack.delims?.[R].classList.add('mq-ghost'); - brack.side = L; + brack.delims?.[1].classList.add('mq-ghost'); + brack.side = 'left'; } } // If there is a SupSub in the supsub block, then it must be replaced with a new instance. This is because // the original instance has the special MathFunction deleteOutOf handlers. Those will not work for a usual // SupSub. - if (!this.blocks[0].isEmpty() && this.blocks[0].ends[L] instanceof SupSub) { + if (!this.blocks[0].isEmpty() && this.blocks[0].ends.left instanceof SupSub) { cursor.insLeftOf(brack ?? this); // A temporary base is created first and then removed later, because if supSubsRequireOperand is true // the following causes errors when the contact weld is called later otherwise. const tmpBase = new Variable('x'); tmpBase.createLeftOf(cursor); - const origSupSub = this.blocks[0].ends[L]; + const origSupSub = this.blocks[0].ends.left; for (const supOrSub of ['sub', 'sup'] as (keyof Pick)[]) { const src = origSupSub[supOrSub]; if (src) { @@ -1485,7 +1498,7 @@ export class MathFunction extends BracketMixin(MathCommand) { // If at the left end of the supsub block and removing the last character of this function name is still // a valid function name, then shorten the function name. if ( - !cursor[L] && + !cursor.left && (LatexCmds[this.ctrlSeq.slice(1, -1)] as Constructor | undefined)?.prototype instanceof MathFunction ) { @@ -1495,14 +1508,14 @@ export class MathFunction extends BracketMixin(MathCommand) { return; } - if (dir === L) this.deleteSide(dir, true, cursor); + if (dir === 'left') this.deleteSide(dir, true, cursor); // If deleting right out of the supsub block, move the cursor to the content block. - else this.enterContentBlock(L, cursor); + else this.enterContentBlock('left', cursor); }; this.blocks[1].deleteOutOf = (dir: Direction, cursor: Cursor) => { // If deleting left out of the content block, move the cursor to the supsub block. - if (dir === L) cursor.insAtRightEnd(this.blocks[0]); + if (dir === 'left') cursor.insAtRightEnd(this.blocks[0]); else this.deleteSide(dir, true, cursor); }; } @@ -1515,7 +1528,7 @@ export class MathFunction extends BracketMixin(MathCommand) { } text() { - return `${this[L] instanceof Letter ? ' ' : ''}${this.ctrlSeq.slice(1)}${this.blocks[0]?.text() ?? ''}(${ + return `${this.left instanceof Letter ? ' ' : ''}${this.ctrlSeq.slice(1)}${this.blocks[0]?.text() ?? ''}(${ this.blocks[1]?.text() ?? '' })`; } @@ -1526,14 +1539,14 @@ export class MathFunction extends BracketMixin(MathCommand) { this.blocks = [new MathBlock(), new MathBlock()]; for (const block of this.blocks) { - block.adopt(this, this.ends[R]); + block.adopt(this, this.ends.right); } return Parser.optWhitespace .then(Parser.regex(/^(?=[_^])/)) .then( latexMathParser.block.map((block: TNode) => { - block.children().adopt(this.blocks[0], this.blocks[0].ends[R]); + block.children().adopt(this.blocks[0], this.blocks[0].ends.right); }) ) .many() @@ -1547,7 +1560,7 @@ export class MathFunction extends BracketMixin(MathCommand) { .many() .map((blocks: MathBlock[]) => { for (const block of blocks) { - block.children().adopt(this.blocks[1], this.blocks[1].ends[R]); + block.children().adopt(this.blocks[1], this.blocks[1].ends.right); } }) .then(Parser.optWhitespace) @@ -1561,7 +1574,7 @@ export class MathFunction extends BracketMixin(MathCommand) { .map((blocks: MathBlock[]) => { if (blocks[blocks.length - 1].text() === ')') blocks.splice(-1); for (const block of blocks) { - block.children().adopt(this.blocks[1], this.blocks[1].ends[R]); + block.children().adopt(this.blocks[1], this.blocks[1].ends.right); } }) .then(Parser.optWhitespace) @@ -1572,12 +1585,12 @@ export class MathFunction extends BracketMixin(MathCommand) { .map((number: string) => { for (const d of number) { const digit = d === '.' ? new VanillaSymbol('.') : new Digit(d); - digit.adopt(this.blocks[1], this.blocks[1].ends[R]); + digit.adopt(this.blocks[1], this.blocks[1].ends.right); } }) .or( latexMathParser.block.map((block: MathBlock) => { - block.children().adopt(this.blocks[1], this.blocks[1].ends[R]); + block.children().adopt(this.blocks[1], this.blocks[1].ends.right); }) ) ) @@ -1606,7 +1619,7 @@ export const latexMathParser = (() => { const firstBlock = blocks[0] || new MathBlock(); for (const block of blocks.slice(1)) { - block.children().adopt(firstBlock, firstBlock.ends[R]); + block.children().adopt(firstBlock, firstBlock.ends.right); } return firstBlock; diff --git a/src/commands/textElements.ts b/src/commands/textElements.ts index d0271d08..46bca9b6 100644 --- a/src/commands/textElements.ts +++ b/src/commands/textElements.ts @@ -1,7 +1,7 @@ // Elements for abstract classes of text blocks import type { Direction } from 'src/constants'; -import { mqCmdId, L, R, LatexCmds, CharCmds } from 'src/constants'; +import { mqCmdId, LatexCmds, CharCmds } from 'src/constants'; import { Parser } from 'services/parser.util'; import type { Cursor } from 'src/cursor'; import { Point } from 'tree/point'; @@ -32,7 +32,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { addToElements(el: VNode) { super.addToElements(el); - this.ends[L]?.addToElements(this.elements.first.firstChild as HTMLElement); + this.ends.left?.addToElements(this.elements.first.firstChild as HTMLElement); } createLeftOf(cursor: Cursor) { @@ -44,8 +44,8 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { for (const char of this.replacedText) this.write(cursor, char); } - this[R]?.siblingCreated?.(cursor.options, L); - this[L]?.siblingCreated?.(cursor.options, R); + this.right?.siblingCreated?.(cursor.options, 'left'); + this.left?.siblingCreated?.(cursor.options, 'right'); this.bubble('reflow'); } @@ -85,18 +85,18 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { // and selection of the MathQuill tree, these all take in a direction and // the cursor moveTowards(dir: Direction, cursor: Cursor) { - cursor.insAtDirEnd(dir === L ? R : L, this); + cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this); } moveOutOf(dir: Direction, cursor: Cursor) { cursor.insDirOf(dir, this); } unselectInto(dir: Direction, cursor: Cursor) { - cursor.insAtDirEnd(dir === L ? R : L, this); + cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this); // Split the text at the stored anticursor position, and reconstruct the anticursor. const newTextPc = (cursor[dir] as TextPiece).splitRight(this.anticursorPosition); - if (dir === L) cursor[L] = newTextPc; - cursor.anticursor = new Point(this, newTextPc[L], newTextPc); + if (dir === 'left') cursor.left = newTextPc; + cursor.anticursor = new Point(this, newTextPc.left, newTextPc); cursor.anticursor.ancestors = {}; for (let ancestor = cursor.anticursor; ancestor.parent; ancestor = ancestor.parent) { cursor.anticursor.ancestors[ancestor.parent.id] = ancestor; @@ -105,7 +105,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { selectOutOf(dir: Direction, cursor: Cursor) { this.anticursorPosition = - dir === L + dir === 'left' ? (cursor.selection?.elements.first.textContent?.length ?? 1) - 1 : this.textContents().length - (cursor.selection?.elements.first.textContent?.length ?? 1) + 1; cursor.insDirOf(dir, this); @@ -121,18 +121,18 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { if (ch !== '$') { this.postOrder('reflow'); - if (!cursor[L]) new TextPiece(ch).createLeftOf(cursor); - else (cursor[L] as unknown as TextPiece).appendText(ch); + if (!cursor.left) new TextPiece(ch).createLeftOf(cursor); + else (cursor.left as unknown as TextPiece).appendText(ch); this.bubble('reflow'); } else if (this.isEmpty()) { cursor.insRightOf(this); new VanillaSymbol('\\$', '$').createLeftOf(cursor); - } else if (!cursor[R]) cursor.insRightOf(this); - else if (!cursor[L]) cursor.insLeftOf(this); + } else if (!cursor.right) cursor.insRightOf(this); + else if (!cursor.left) cursor.insLeftOf(this); else { // split apart const leftBlock = new TextBlock(); - const leftPc = this.ends[L]; + const leftPc = this.ends.left; leftPc?.disown().elements.detach(); leftPc?.adopt(leftBlock); @@ -143,8 +143,8 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { } writeLatex(cursor: Cursor, latex: string) { - if (!cursor[L]) new TextPiece(latex).createLeftOf(cursor); - else (cursor[L] as unknown as TextPiece).appendText(latex); + if (!cursor.left) new TextPiece(latex).createLeftOf(cursor); + else (cursor.left as unknown as TextPiece).appendText(latex); this.bubble('reflow'); } @@ -169,33 +169,35 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { // Move towards mousedown (pageX) let displ = pageX - cursor.show().offset().left; // displacement - const dir = displ && displ < 0 ? L : R; - let prevDispl = dir; + const dir = displ && displ < 0 ? 'left' : 'right'; + const numericDir = dir === 'left' ? -1 : 1; + let prevDispl = numericDir; // displ * prevDispl > 0 iff displacement direction === previous direction while (cursor[dir] && displ * prevDispl > 0) { cursor[dir].moveTowards(dir, cursor); prevDispl = displ; displ = pageX - cursor.offset().left; } - if (dir * displ < -dir * prevDispl) cursor[dir === L ? R : L]?.moveTowards(dir === L ? R : L, cursor); + if (numericDir * displ < -numericDir * prevDispl) + cursor[dir === 'left' ? 'right' : 'left']?.moveTowards(dir === 'left' ? 'right' : 'left', cursor); if (!cursor.anticursor) { // About to start mouse-selecting, the anticursor is going to be placed here. - this.anticursorPosition = cursor[L]?.text().length ?? 0; + this.anticursorPosition = cursor.left?.text().length ?? 0; } else if (cursor.anticursor.parent === this) { // Mouse selecting within this TextBlock, re-insert the anticursor. - const cursorPosition = cursor[L]?.text().length ?? 0; + const cursorPosition = cursor.left?.text().length ?? 0; if (this.anticursorPosition === cursorPosition) { cursor.startSelection(); } else { let newTextPc; if (this.anticursorPosition < cursorPosition) { - newTextPc = (cursor[L] as TextPiece).splitRight(this.anticursorPosition); - cursor[L] = newTextPc; + newTextPc = (cursor.left as TextPiece).splitRight(this.anticursorPosition); + cursor.left = newTextPc; } else { - newTextPc = (cursor[R] as TextPiece).splitRight(this.anticursorPosition - cursorPosition); + newTextPc = (cursor.right as TextPiece).splitRight(this.anticursorPosition - cursorPosition); } - cursor.anticursor = new Point(this, newTextPc[L], newTextPc); + cursor.anticursor = new Point(this, newTextPc.left, newTextPc); cursor.anticursor.ancestors = {}; for (let ancestor = cursor.anticursor; ancestor.parent; ancestor = ancestor.parent) { cursor.anticursor.ancestors[ancestor.parent.id] = ancestor; @@ -210,8 +212,8 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { if (!cursor) return; if (this.textContents() === '') { this.remove(); - if (cursor[L] === this) cursor[L] = this[L]; - else if (cursor[R] === this) cursor[R] = this[R]; + if (cursor.left === this) cursor.left = this.left; + else if (cursor.right === this) cursor.right = this.right; } else { // If the text block contains the selection, then that needs to be removed before fuseChildren is called. if (this.elements.find('.mq-selection').contents.length) cursor.clearSelection(); @@ -282,31 +284,31 @@ export class TextPiece extends TNode { } insTextAtDirEnd(text: string, dir: Direction | undefined) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); - if (dir === R) this.appendText(text); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); + if (dir === 'right') this.appendText(text); else this.prependText(text); } splitRight(i: number) { const newPc = new TextPiece(this.textStr.slice(i)); - if (this.parent) newPc.adopt(this.parent, this, this[R]); + if (this.parent) newPc.adopt(this.parent, this, this.right); if (this.dom) newPc.addToElements(this.dom.splitText(i)); this.textStr = this.textStr.slice(0, i); return newPc; } endChar(dir: Direction, text: string) { - return text.charAt(dir === L ? 0 : -1 + text.length); + return text.charAt(dir === 'left' ? 0 : -1 + text.length); } moveTowards(dir: Direction | undefined, cursor: Cursor) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); - const ch = this.endChar(dir === L ? R : L, this.textStr); + const ch = this.endChar(dir === 'left' ? 'right' : 'left', this.textStr); - const from = this[dir === L ? R : L] as TextPiece | undefined; + const from = this[dir === 'left' ? 'right' : 'left'] as TextPiece | undefined; if (from) from.insTextAtDirEnd(ch, dir); - else new TextPiece(ch).createDir(dir === L ? R : L, cursor); + else new TextPiece(ch).createDir(dir === 'left' ? 'right' : 'left', cursor); this.deleteTowards(dir, cursor); } @@ -317,7 +319,7 @@ export class TextPiece extends TNode { deleteTowards(dir: Direction, cursor: Cursor) { if (this.textStr.length > 1) { - if (dir === R) { + if (dir === 'right') { this.dom?.deleteData(0, 1); this.textStr = this.textStr.slice(1); } else { @@ -334,25 +336,26 @@ export class TextPiece extends TNode { } selectTowards(dir: Direction | undefined, cursor: Cursor) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const anticursor = cursor.anticursor; - const ch = this.endChar(dir === L ? R : L, this.textStr); + const ch = this.endChar(dir === 'left' ? 'right' : 'left', this.textStr); if (anticursor?.[dir] === this) { const newPc = new TextPiece(ch).createDir(dir, cursor); anticursor[dir] = newPc; cursor.insDirOf(dir, newPc); } else { - const from = this[dir === L ? R : L] as TextPiece | undefined; + const from = this[dir === 'left' ? 'right' : 'left'] as TextPiece | undefined; if (from) from.insTextAtDirEnd(ch, dir); else { - const newPc = new TextPiece(ch).createDir(dir === L ? R : L, cursor); - if (cursor.selection) newPc.elements.insDirOf(dir === L ? R : L, cursor.selection.elements); + const newPc = new TextPiece(ch).createDir(dir === 'left' ? 'right' : 'left', cursor); + if (cursor.selection) + newPc.elements.insDirOf(dir === 'left' ? 'right' : 'left', cursor.selection.elements); } - if (this.textStr.length === 1 && anticursor?.[dir === L ? R : L] === this) { - anticursor[dir === L ? R : L] = this[dir === L ? R : L]; + if (this.textStr.length === 1 && anticursor?.[dir === 'left' ? 'right' : 'left'] === this) { + anticursor[dir === 'left' ? 'right' : 'left'] = this[dir === 'left' ? 'right' : 'left']; } } diff --git a/src/constants.ts b/src/constants.ts index 2daa1eaf..97d20d2d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,17 +1,10 @@ import type { TNode } from 'tree/node'; import type { MathCommand } from 'commands/mathElements'; -export const enum Direction { - L = -1, - R = 1 -} +export type Direction = 'left' | 'right'; export const mqCmdId = 'data-mathquill-command-id', - mqBlockId = 'data-mathquill-block-id', - // L = 'left', R = 'right' - // The contract is that they can be used as object properties and -L === R, and -R === L. - L = Direction.L, - R = Direction.R; + mqBlockId = 'data-mathquill-block-id'; export const noop = () => { /* do nothing */ @@ -65,7 +58,7 @@ export const bindMixin = >( ...args: (string | boolean | number | object)[] ) => class extends Base { - constructor(..._ignore_args: any[]) { + constructor(..._args: any[]) { super(...args); } }; @@ -74,18 +67,18 @@ export const prayWellFormed = (parent?: TNode, leftward?: TNode, rightward?: TNo if (!parent) throw new Error('a parent must be present'); // Either leftward is empty and `rightward` is the left end child (possibly empty) - // or leftward is there and leftward's [R] and .parent are properly set up. + // or leftward is there and leftward's .right and .parent are properly set up. if ( - (!leftward && parent.ends[L] !== rightward) || - (leftward && (leftward[R] !== rightward || leftward.parent !== parent)) + (!leftward && parent.ends.left !== rightward) || + (leftward && (leftward.right !== rightward || leftward.parent !== parent)) ) throw new Error('leftward is not properly set up'); // Either rightward is empty and `leftward` is the right end child (possibly empty) - // or rightward is there and rightward's [L] and .parent are properly set up. + // or rightward is there and rightward's .left and .parent are properly set up. if ( - (!rightward && parent.ends[R] !== leftward) || - (rightward && (rightward[L] !== leftward || rightward.parent !== parent)) + (!rightward && parent.ends.right !== leftward) || + (rightward && (rightward.left !== leftward || rightward.parent !== parent)) ) throw new Error('rightward is not properly set up'); }; diff --git a/src/controller.ts b/src/controller.ts index 623121fa..e8ea99f5 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -1,7 +1,6 @@ // Controller for a MathQuill instance, on which services are registered. import type { Direction } from 'src/constants'; -import { L, R } from 'src/constants'; import type { Handler, DirectionHandler, Handlers, Options } from 'src/options'; import { Cursor } from 'src/cursor'; import type { AbstractMathQuill } from 'src/abstractFields'; @@ -39,8 +38,8 @@ export class ControllerBase { handle(name: keyof Handlers, dir?: Direction) { const handlers = this.options.handlers; - if (handlers?.[name] && this.apiClass) { - if (dir === L || dir === R) (handlers[name] as DirectionHandler)(dir, this.apiClass); + if (handlers?.[name]) { + if (dir === 'left' || dir === 'right') (handlers[name] as DirectionHandler)(dir, this.apiClass); else (handlers[name] as Handler)(this.apiClass); } } @@ -71,7 +70,7 @@ export class Controller extends ExportText( } escapeDir(dir: Direction | undefined, _key: string, e: KeyboardEvent) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const cursor = this.cursor; // only prevent default of Tab if not in the root editable @@ -86,22 +85,23 @@ export class Controller extends ExportText( } moveDir(dir: Direction | undefined) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes; if (cursor.selection) { - if (cursor.selection.ends[dir]) cursor.insDirOf(dir, cursor.selection.ends[dir]); + const end = cursor.selection.ends[dir]; + if (end) cursor.insDirOf(dir, end); } else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown); else cursor.parent?.moveOutOf(dir, cursor, updown); return this.notify('move'); } moveLeft() { - return this.moveDir(L); + return this.moveDir('left'); } moveRight() { - return this.moveDir(R); + return this.moveDir('right'); } // moveUp and moveDown have almost identical algorithms: @@ -118,8 +118,8 @@ export class Controller extends ExportText( const cursor = this.notify('upDown').cursor; const dirInto: keyof TNode = `${dir}Into`, dirOutOf: keyof TNode = `${dir}OutOf`; - if (cursor[R]?.[dirInto]) cursor.insAtLeftEnd(cursor[R][dirInto]); - else if (cursor[L]?.[dirInto]) cursor.insAtRightEnd(cursor[L][dirInto]); + if (cursor.right?.[dirInto]) cursor.insAtLeftEnd(cursor.right[dirInto]); + else if (cursor.left?.[dirInto]) cursor.insAtRightEnd(cursor.left[dirInto]); else { cursor.parent?.bubble((ancestor: TNode) => { if (ancestor[dirOutOf]) { @@ -128,6 +128,7 @@ export class Controller extends ExportText( if (ancestor[dirOutOf] instanceof TNode) cursor.jumpUpDown(ancestor, ancestor[dirOutOf]); if (ancestor[dirOutOf] !== true) return false; } + return true; }); } return this; @@ -140,7 +141,7 @@ export class Controller extends ExportText( } deleteDir(dir: Direction | undefined) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const cursor = this.cursor; const hadSelection = cursor.selection; @@ -151,10 +152,10 @@ export class Controller extends ExportText( } // Call the contactWeld for a SupSub so that it can deal with having its base deleted. - cursor[R]?.postOrder('contactWeld', cursor); + cursor.right?.postOrder('contactWeld', cursor); - cursor[L]?.siblingDeleted?.(cursor.options, R); - cursor[R]?.siblingDeleted?.(cursor.options, L); + cursor.left?.siblingDeleted?.(cursor.options, 'right'); + cursor.right?.siblingDeleted?.(cursor.options, 'left'); cursor.parent?.bubble('reflow'); @@ -162,40 +163,40 @@ export class Controller extends ExportText( } ctrlDeleteDir(dir: Direction | undefined) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const cursor = this.cursor; if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); this.notify('edit'); - if (dir === L) { - new Fragment(cursor.parent?.ends[L], cursor[L]).remove(); + if (dir === 'left') { + new Fragment(cursor.parent?.ends.left, cursor.left).remove(); } else { - new Fragment(cursor[R], cursor.parent?.ends[R]).remove(); + new Fragment(cursor.right, cursor.parent?.ends.right).remove(); } if (cursor.parent) cursor.insAtDirEnd(dir, cursor.parent); // Call the contactWeld for a SupSub so that it can deal with having its base deleted. - cursor[R]?.postOrder('contactWeld', cursor); + cursor.right?.postOrder('contactWeld', cursor); - cursor[L]?.siblingDeleted?.(cursor.options, R); - cursor[R]?.siblingDeleted?.(cursor.options, L); + cursor.left?.siblingDeleted?.(cursor.options, 'right'); + cursor.right?.siblingDeleted?.(cursor.options, 'left'); cursor.parent?.bubble('reflow'); return this; } backspace() { - return this.deleteDir(L); + return this.deleteDir('left'); } deleteForward() { - return this.deleteDir(R); + return this.deleteDir('right'); } selectDir(dir: Direction | undefined) { const cursor = this.notify('select').cursor, seln = cursor.selection; - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); if (!cursor.anticursor) cursor.startSelection(); @@ -204,7 +205,7 @@ export class Controller extends ExportText( // "if node we're selecting towards is inside selection (hence retracting) // and is on the *far side* of the selection (hence is only node selected) // and the anticursor is *inside* that node, not just on the other side" - if (seln && seln.ends[dir] === node && cursor.anticursor?.[dir === L ? R : L] !== node) { + if (seln && seln.ends[dir] === node && cursor.anticursor?.[dir === 'left' ? 'right' : 'left'] !== node) { node.unselectInto(dir, cursor); } else node.selectTowards(dir, cursor); } else cursor.parent?.selectOutOf(dir, cursor); @@ -214,10 +215,10 @@ export class Controller extends ExportText( } selectLeft() { - this.selectDir(L); + this.selectDir('left'); } selectRight() { - this.selectDir(R); + this.selectDir('right'); } } diff --git a/src/cursor.ts b/src/cursor.ts index aea7a8b6..4fd601a6 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -5,7 +5,6 @@ // A fake cursor in the fake textbox that the math is rendered in. import type { Direction } from 'src/constants'; -import { L, R } from 'src/constants'; import type { Options } from 'src/options'; import { Point } from 'tree/point'; import type { TNode } from 'tree/node'; @@ -39,10 +38,10 @@ export class Cursor extends Point { clearInterval(this.intervalId); } else { // The cursor was hidden and removed, so insert this.element back into the DOM. - if (this[R]) { - if (this.selection && this.selection.ends[L]?.[L] === this[L]) + if (this.right) { + if (this.selection && this.selection.ends.left?.left === this.left) this.selection.elements.first.before(this.element); - else this[R].elements.first.before(this.element); + else this.right.elements.first.before(this.element); } else this.parent?.elements.firstElement.append(this.element); this.parent?.focus(); } @@ -62,15 +61,15 @@ export class Cursor extends Point { const oldParent = this.parent; this.parent = parent; this[dir] = withDir; - this[dir === L ? R : L] = oppDir; + this[dir === 'left' ? 'right' : 'left'] = oppDir; // By contract blur is called after all has been said and done and the cursor has actually been moved. if (oldParent !== parent && oldParent?.blur) oldParent.blur(this); } insDirOf(dir: Direction | undefined, el: TNode) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); - if (dir === L) el.elements.first.before(this.element); + if (dir === 'left') el.elements.first.before(this.element); else el.elements.last.after(this.element); if (el.parent) this.withDirInsertAt(dir, el.parent, el[dir], el); @@ -79,17 +78,17 @@ export class Cursor extends Point { } insLeftOf(el: TNode) { - return this.insDirOf(L, el); + return this.insDirOf('left', el); } insRightOf(el: TNode) { - return this.insDirOf(R, el); + return this.insDirOf('right', el); } insAtDirEnd(dir: Direction | undefined, el: TNode) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); - if (dir === L) el.elements.firstElement.prepend(this.element); + if (dir === 'left') el.elements.firstElement.prepend(this.element); else el.elements.lastElement.append(this.element); this.withDirInsertAt(dir, el, undefined, el.ends[dir]); @@ -98,11 +97,11 @@ export class Cursor extends Point { } insAtLeftEnd(el: TNode) { - return this.insAtDirEnd(L, el); + return this.insAtDirEnd('left', el); } insAtRightEnd(el: TNode) { - return this.insAtDirEnd(R, el); + return this.insAtDirEnd('right', el); } // Jump up or down from one block TNode to another: @@ -114,7 +113,7 @@ export class Cursor extends Point { this.upDownCache[from.id] = Point.copy(this); const cached = this.upDownCache[to.id] as Point | undefined; if (cached) { - if (cached[R]) this.insLeftOf(cached[R]); + if (cached.right) this.insLeftOf(cached.right); else if (cached.parent) this.insAtRightEnd(cached.parent); } else { to.seek(this.offset().left, this); @@ -128,9 +127,9 @@ export class Cursor extends Point { unwrapGramp() { const gramp = this.parent?.parent; const greatgramp = gramp?.parent; - const rightward = gramp?.[R]; + const rightward = gramp?.right; - let leftward = gramp?.[L]; + let leftward = gramp?.left; gramp?.disown().eachChild((uncle: TNode) => { if (uncle.isEmpty()) return true; @@ -143,32 +142,32 @@ export class Cursor extends Point { return true; }); - leftward = uncle.ends[R]; + leftward = uncle.ends.right; return true; }); - if (!this[R]) { + if (!this.right) { // Find something rightward to insert left of. - if (this[L]) this[R] = this[L][R]; + if (this.left) this.right = this.left.right; else { - while (!this[R]) { - this.parent = this.parent?.[R]; - if (this.parent) this[R] = this.parent.ends[L]; + while (!this.right) { + this.parent = this.parent?.right; + if (this.parent) this.right = this.parent.ends.left; else { - this[R] = gramp?.[R]; + this.right = gramp?.right; this.parent = greatgramp; break; } } } } - if (this[R]) this.insLeftOf(this[R]); + if (this.right) this.insLeftOf(this.right); else if (greatgramp) this.insAtRightEnd(greatgramp); gramp?.elements.remove(); - if (gramp?.[L]?.siblingDeleted) gramp[L].siblingDeleted(this.options, R); - if (gramp?.[R]?.siblingDeleted) gramp[R].siblingDeleted(this.options, L); + if (gramp?.left?.siblingDeleted) gramp.left.siblingDeleted(this.options, 'right'); + if (gramp?.right?.siblingDeleted) gramp.right.siblingDeleted(this.options, 'left'); } startSelection() { @@ -186,7 +185,7 @@ export class Cursor extends Point { } select() { - if (this[L] === this.anticursor?.[L] && this.parent === this.anticursor?.parent) return false; + if (this.left === this.anticursor?.left && this.parent === this.anticursor?.parent) return false; if (!this.anticursor?.ancestors) throw new Error('selection not well formed'); @@ -218,7 +217,7 @@ export class Cursor extends Point { // of the selection. let leftEnd, rightEnd, - dir = R; + dir: Direction = 'right'; // This is an extremely subtle algorithm. // As a special case, `ancestor` could be a Point and `antiAncestor` a TNode @@ -227,27 +226,27 @@ export class Cursor extends Point { // - both Nodes // - `ancestor` a Point and `antiAncestor` a TNode // - `ancestor` a TNode and `antiAncestor` a Point - // `antiAncestor[R] === rightward[R]` for some `rightward` that is + // `antiAncestor.right === rightward.right` for some `rightward` that is // `ancestor` or to its right, if and only if `antiAncestor` is to // the right of `ancestor`. - if (ancestor[L] !== antiAncestor) { - for (let rightward: Point | TNode | undefined = ancestor; rightward; rightward = rightward[R]) { - if (rightward[R] === antiAncestor[R]) { - dir = L; + if (ancestor.left !== antiAncestor) { + for (let rightward: Point | TNode | undefined = ancestor; rightward; rightward = rightward.right) { + if (rightward.right === antiAncestor.right) { + dir = 'left'; leftEnd = ancestor; rightEnd = antiAncestor; break; } } } - if (dir === R) { + if (dir === 'right') { leftEnd = antiAncestor; rightEnd = ancestor; } // only want to select Nodes up to Points, can't select Points themselves - if (leftEnd instanceof Point) leftEnd = leftEnd[R]; - if (rightEnd instanceof Point) rightEnd = rightEnd[L]; + if (leftEnd instanceof Point) leftEnd = leftEnd.right; + if (rightEnd instanceof Point) rightEnd = rightEnd.left; this.hide().selection = lca.selectChildren(leftEnd, rightEnd); const selectionEndDir = this.selection?.ends[dir]; @@ -268,8 +267,8 @@ export class Cursor extends Point { deleteSelection() { if (!this.selection) return; - this[L] = this.selection.ends[L]?.[L]; - this[R] = this.selection.ends[R]?.[R]; + this.left = this.selection.ends.left?.left; + this.right = this.selection.ends.right?.right; this.selection.remove(); this.selectionChanged?.(); delete this.selection; @@ -278,8 +277,8 @@ export class Cursor extends Point { replaceSelection() { const seln = this.selection; if (seln) { - this[L] = seln.ends[L]?.[L]; - this[R] = seln.ends[R]?.[R]; + this.left = seln.ends.left?.left; + this.right = seln.ends.right?.right; delete this.selection; } return seln; diff --git a/src/mixins.ts b/src/mixins.ts index b3463848..73af3e82 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -1,5 +1,4 @@ import type { Direction, Constructor } from 'src/constants'; -import { L, R } from 'src/constants'; import type { Cursor } from 'src/cursor'; import type { VNode } from 'tree/vNode'; import type { TNode } from 'tree/node'; @@ -25,8 +24,8 @@ export const RootBlockMixin = (_: MathElement) => { export const deleteSelectTowardsMixin = >(Base: TBase) => class extends Base { moveTowards(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { - const nodeAtEnd = (updown && this[`${updown}Into`]) || this.ends[dir === L ? R : L]; - if (nodeAtEnd) cursor.insAtDirEnd(dir === L ? R : L, nodeAtEnd); + const nodeAtEnd = (updown && this[`${updown}Into`]) || this.ends[dir === 'left' ? 'right' : 'left']; + if (nodeAtEnd) cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', nodeAtEnd); } deleteTowards(dir: Direction, cursor: Cursor) { @@ -35,7 +34,7 @@ export const deleteSelectTowardsMixin = >(Base: } selectTowards(dir: Direction, cursor: Cursor) { - cursor[dir === L ? R : L] = this; + cursor[dir === 'left' ? 'right' : 'left'] = this; cursor[dir] = this[dir]; } }; diff --git a/src/options.ts b/src/options.ts index 3b948deb..33dbf292 100644 --- a/src/options.ts +++ b/src/options.ts @@ -4,8 +4,8 @@ import type { Direction } from 'src/constants'; import { BuiltInOpNames } from 'src/constants'; import type { AbstractMathQuill } from 'src/abstractFields'; -export type Handler = (mq: AbstractMathQuill) => void; -export type DirectionHandler = (dir: Direction, mq: AbstractMathQuill) => void; +export type Handler = (mq?: AbstractMathQuill) => void; +export type DirectionHandler = (dir: Direction, mq?: AbstractMathQuill) => void; export interface Handlers { enter?: Handler; @@ -88,13 +88,13 @@ export class Options { } if (!/^\s*[a-z]+(?:\s+[a-z]+)*\s*$/i.test(cmds)) { - throw `"${cmds}" not a space-delimited list of only letters`; + throw new Error(`"${cmds}" not a space-delimited list of only letters`); } const list = cmds.trim().split(/\s+/), dict: NamesWLength = { _maxLength: 0 }; for (const cmd of list) { - if (cmd.length < 2) throw `autocommand "${cmd}" not minimum length of 2`; - if (cmd in BuiltInOpNames) throw `"${cmd}" is a built-in operator name`; + if (cmd.length < 2) throw new Error(`autocommand "${cmd}" not minimum length of 2`); + if (cmd in BuiltInOpNames) throw new Error(`"${cmd}" is a built-in operator name`); dict[cmd] = 1; dict._maxLength = Math.max(dict._maxLength, cmd.length); } @@ -104,12 +104,12 @@ export class Options { addAutoCommands(cmds: string | string[]) { if (!this.#_autoCommands) this.autoCommands = Options.#autoCommands; - if (!this.#_autoCommands) throw 'autoCommands setter not working'; + if (!this.#_autoCommands) throw new Error('autoCommands setter not working'); const newCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of newCmds) { - if (/\s/.test(cmd) || !/^[a-z]*$/i.test(cmd)) throw `${cmd} is not a valid autocommand name`; - if (cmd.length < 2) throw `autocommand "${cmd}" not minimum length of 2`; - if (cmd in BuiltInOpNames) throw `"${cmd}" is a built-in operator name`; + if (/\s/.test(cmd) || !/^[a-z]*$/i.test(cmd)) throw new Error(`${cmd} is not a valid autocommand name`); + if (cmd.length < 2) throw new Error(`autocommand "${cmd}" not minimum length of 2`); + if (cmd in BuiltInOpNames) throw new Error(`"${cmd}" is a built-in operator name`); this.#_autoCommands[cmd] = 1; this.#_autoCommands._maxLength = Math.max(this.#_autoCommands._maxLength, cmd.length); } @@ -117,7 +117,7 @@ export class Options { removeAutoCommands(cmds: string | string[]) { if (!this.#_autoCommands) this.autoCommands = Options.#autoCommands; - if (!this.#_autoCommands) throw 'autoCommands setter not working'; + if (!this.#_autoCommands) throw new Error('autoCommands setter not working'); const removeCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of removeCmds) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete @@ -177,12 +177,12 @@ export class Options { } if (!/^\s*[a-z]+(?:\s+[a-z]+)*\s*$/i.test(cmds)) { - throw `"${cmds}" not a space-delimited list of only letters`; + throw new Error(`"${cmds}" not a space-delimited list of only letters`); } const list = cmds.trim().split(/\s+/), dict: NamesWLength = { _maxLength: 0 }; for (const cmd of list) { - if (cmd.length < 2) throw `"${cmd}" not minimum length of 2`; + if (cmd.length < 2) throw new Error(`"${cmd}" not minimum length of 2`); dict[cmd] = 1; dict._maxLength = Math.max(dict._maxLength, cmd.length); } @@ -192,11 +192,11 @@ export class Options { addAutoOperatorNames(cmds: string | string[]) { if (!this.#_autoOperatorNames) this.autoOperatorNames = Options.#autoOperatorNames; - if (!this.#_autoOperatorNames) throw 'autoOperatorNames setter not working'; + if (!this.#_autoOperatorNames) throw new Error('autoOperatorNames setter not working'); const newCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of newCmds) { - if (/\s/.test(cmd) || !/^[a-z]*$/i.test(cmd)) throw `${cmd} is not a valid autocommand name`; - if (cmd.length < 2) throw `"${cmd}" not minimum length of 2`; + if (/\s/.test(cmd) || !/^[a-z]*$/i.test(cmd)) throw new Error(`${cmd} is not a valid autocommand name`); + if (cmd.length < 2) throw new Error(`"${cmd}" not minimum length of 2`); this.#_autoOperatorNames[cmd] = 1; this.#_autoOperatorNames._maxLength = Math.max(this.#_autoOperatorNames._maxLength, cmd.length); } @@ -204,7 +204,7 @@ export class Options { removeAutoOperatorNames(cmds: string | string[]) { if (!this.#_autoOperatorNames) this.autoOperatorNames = Options.#autoOperatorNames; - if (!this.#_autoOperatorNames) throw 'autoOperatorNames setter not working'; + if (!this.#_autoOperatorNames) throw new Error('autoOperatorNames setter not working'); const removeCmds = cmds instanceof Array ? cmds.map((c) => c.trim()) : [cmds.trim()]; for (const cmd of removeCmds) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete @@ -257,7 +257,7 @@ export class Options { } set leftRightIntoCmdGoes(updown: 'up' | 'down' | undefined) { if (updown && updown !== 'up' && (updown as string) !== 'down') { - throw `"up" or "down" required for leftRightIntoCmdGoes option, got "${updown as string}"`; + throw new Error(`"up" or "down" required for leftRightIntoCmdGoes option, got "${updown as string}"`); } if (this instanceof Options) this.#_leftRightIntoCmdGoes = updown; else Options.#leftRightIntoCmdGoes = updown; diff --git a/src/publicapi.ts b/src/publicapi.ts index 1f523a06..e20a10b1 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -82,7 +82,7 @@ const mathQuill: MathQuill = { MQ.registerEmbed = (name: string, options: (data: string) => EmbedOptions) => { if (!/^[a-z][a-z0-9]*$/i.test(name)) { - throw 'Embed name must start with letter and be only letters and digits'; + throw new Error('Embed name must start with letter and be only letters and digits'); } EMBEDS[name] = options; }; diff --git a/src/selection.ts b/src/selection.ts index ffb61493..5768c807 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -5,11 +5,10 @@ import type { Direction } from 'src/constants'; import type { TNode } from 'tree/node'; import { VNode } from 'tree/vNode'; -import { L } from 'src/constants'; import { Fragment } from 'tree/fragment'; export class Selection extends Fragment { - constructor(withDir?: TNode, oppDir?: TNode, dir: Direction = L) { + constructor(withDir?: TNode, oppDir?: TNode, dir: Direction = 'left') { super(withDir, oppDir, dir); const wrapper = document.createElement('span'); wrapper.classList.add('mq-selection'); diff --git a/src/services/latex.ts b/src/services/latex.ts index 9f250361..116bef90 100644 --- a/src/services/latex.ts +++ b/src/services/latex.ts @@ -1,7 +1,6 @@ // Latex Controller Extension import type { Constructor } from 'src/constants'; -import { L, R } from 'src/constants'; import type { TNode } from 'tree/node'; import { Parser } from 'services/parser.util'; import { VanillaSymbol, latexMathParser } from 'commands/mathElements'; @@ -28,10 +27,8 @@ export const LatexControllerExtension = (latex); this.root.eachChild('postOrder', 'dispose'); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.root.ends[L]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.root.ends[R]; + delete this.root.ends.left; + delete this.root.ends.right; if (block instanceof MathBlock && block.prepareInsertionAt(this.cursor)) { block.children().adopt(this.root); @@ -55,10 +52,8 @@ export const LatexControllerExtension = & // Drag-to-select event handling this.mouseDownHandler = (e: MouseEvent) => { - const rootEl = e.target instanceof HTMLElement ? e.target.closest('.mq-root-block') : null; + const rootEl = (e.target as HTMLElement | undefined)?.closest('.mq-root-block'); const root = TNode.byId.get( parseInt((rootEl?.getAttribute(mqBlockId) || ultimateRootEl.getAttribute(mqBlockId)) ?? '0') ); if (!root?.controller) { - throw 'controller undefined... what?'; + throw new Error('controller undefined... what?'); } const ctrlr = root.controller, @@ -72,7 +72,7 @@ export const MouseEventController = & if (e.detail === 3) { // If this is a triple click, then select all and return. ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (cursor[L]) ctrlr.selectLeft(); + while (cursor.left) ctrlr.selectLeft(); mouseup(); return; } else if (e.detail === 2) { @@ -80,41 +80,41 @@ export const MouseEventController = & // Note that the interpretation of what a block is in this situation is not a true MathQuill block. // Rather an attempt is made to select word like blocks. ctrlr.seek(e.target as HTMLElement, e.pageX); - if (!cursor[R] && cursor[L]?.parent === root) ctrlr.moveLeft(); + if (!cursor.right && cursor.left?.parent === root) ctrlr.moveLeft(); - if (cursor[R] instanceof Letter) { + if (cursor.right instanceof Letter) { // If a "Letter" is to the right of the cursor, then try to select all adjacent "Letter"s that // are of the same basic ilk. That means all "Letter"s that are part of an operator name, or // all "Letter"s that are not part of an operator name. - const currentNode = cursor[R]; + const currentNode = cursor.right; while ( - cursor[L] && - cursor[L] instanceof Letter && - cursor[L].isPartOfOperator === currentNode.isPartOfOperator + cursor.left && + cursor.left instanceof Letter && + cursor.left.isPartOfOperator === currentNode.isPartOfOperator ) ctrlr.moveLeft(); cursor.startSelection(); while ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - cursor[R] && - cursor[R] instanceof Letter && - cursor[R].isPartOfOperator === currentNode.isPartOfOperator + cursor.right && + cursor.right instanceof Letter && + cursor.right.isPartOfOperator === currentNode.isPartOfOperator ) ctrlr.selectRight(); - } else if (cursor[R] instanceof Digit) { + } else if (cursor.right instanceof Digit) { // If a "Digit" is to the right of the cursor, then select all adjacent "Digit"s. - while (cursor[L] && cursor[L] instanceof Digit) ctrlr.moveLeft(); + while (cursor.left && cursor.left instanceof Digit) ctrlr.moveLeft(); cursor.startSelection(); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor[R] && cursor[R] instanceof Digit) ctrlr.selectRight(); + while (cursor.right && cursor.right instanceof Digit) ctrlr.selectRight(); } else { cursor.startSelection(); ctrlr.selectRight(); } // If the cursor is in a text block, then select the whole text block. - if (cursor[L]?.parent instanceof TextBlock) { - cursor[L].parent.moveOutOf(L, cursor); + if (cursor.left?.parent instanceof TextBlock) { + cursor.left.parent.moveOutOf('left', cursor); ctrlr.selectRight(); } diff --git a/src/services/parser.util.ts b/src/services/parser.util.ts index f56d4199..9be75eae 100644 --- a/src/services/parser.util.ts +++ b/src/services/parser.util.ts @@ -25,7 +25,7 @@ export class Parser { `${stream}`, (_stream, result: T) => result, (stream: string, message: string) => { - throw `Parse Error: ${message} at ${stream || 'EOF'}`; + throw new Error(`Parse Error: ${message} at ${stream || 'EOF'}`); } ); } diff --git a/src/services/saneKeyboardEvents.util.ts b/src/services/saneKeyboardEvents.util.ts index 8df2d2cd..2e85c3ae 100644 --- a/src/services/saneKeyboardEvents.util.ts +++ b/src/services/saneKeyboardEvents.util.ts @@ -1,7 +1,6 @@ // An abstraction layer wrapping the textarea in an object with methods to manipulate and listen to events. // This is an internal abstraction layer intented to handle cross-browser inconsistencies in event handlers. -import { L } from 'src/constants'; import type { Controller } from 'src/controller'; export const saneKeyboardEvents = (() => { @@ -87,7 +86,7 @@ export const saneKeyboardEvents = (() => { sendInputSpaceKeystroke && controller.options.spaceBehavesLikeTab && controller.cursor.depth() > 1 && - controller.cursor[L]?.ctrlSeq !== ',' + controller.cursor.left?.ctrlSeq !== ',' ) { handleKey('Spacebar', e as KeyboardEvent); setTimeout(() => (textarea.value = '')); diff --git a/src/services/scrollHoriz.ts b/src/services/scrollHoriz.ts index c9c61182..30d50816 100644 --- a/src/services/scrollHoriz.ts +++ b/src/services/scrollHoriz.ts @@ -1,7 +1,6 @@ // Horizontal panning for editable fields that overflow their width import type { Constructor } from 'src/constants'; -import { L, R } from 'src/constants'; import type { ControllerBase } from 'src/controller'; export const HorizontalScroll = >(Base: TBase) => @@ -18,7 +17,7 @@ export const HorizontalScroll = >(Base const rect = this.cursor.selection.elements.firstElement.getBoundingClientRect(); const overLeft = rect.left - (rootRect.left + 20); const overRight = rect.right - (rootRect.right - 20); - if (this.cursor.selection.ends[L] === this.cursor[R]) { + if (this.cursor.selection.ends.left === this.cursor.right) { if (overLeft < 0) scrollBy = overLeft; else if (overRight > 0) { if (rect.left - overRight < rootRect.left + 20) scrollBy = overLeft; diff --git a/src/services/textarea.ts b/src/services/textarea.ts index 7c1527d3..ab737a00 100644 --- a/src/services/textarea.ts +++ b/src/services/textarea.ts @@ -24,7 +24,7 @@ export const TextAreaController = < this.textareaSpan.classList.add('mq-textarea'); const textarea = this.options.substituteTextarea(); if (!textarea.nodeType) { - throw 'substituteTextarea() must return a DOM element'; + throw new Error('substituteTextarea() must return a DOM element'); } this.textareaSpan.append(textarea); this.textarea = textarea; diff --git a/src/tree/fragment.ts b/src/tree/fragment.ts index 49a53377..69c73beb 100644 --- a/src/tree/fragment.ts +++ b/src/tree/fragment.ts @@ -1,7 +1,7 @@ // Fragment base classes of edit tree-related objects import type { Direction } from 'src/constants'; -import { L, R, iterator, prayWellFormed } from 'src/constants'; +import { iterator, prayWellFormed } from 'src/constants'; import type { Ends } from 'tree/node'; import { VNode } from 'tree/vNode'; import { TNode } from 'tree/node'; @@ -21,10 +21,10 @@ export class Fragment { ends: Ends = {}; disowned?: boolean; each = iterator((yield_: (node: TNode) => TNode | boolean | undefined) => { - let el = this.ends[L]; + let el = this.ends.left; if (!el) return this; - for (; el !== this.ends[R]?.[R]; el = el[R]) { + for (; el !== this.ends.right?.right; el = el.right) { if (!el) continue; if (yield_(el) === false) break; } @@ -32,7 +32,7 @@ export class Fragment { return this; }); - constructor(withDir?: TNode, oppDir?: TNode, dir: Direction = L) { + constructor(withDir?: TNode, oppDir?: TNode, dir: Direction = 'left') { if (!withDir !== !oppDir) throw new Error('no half-empty fragments'); if (!withDir) return; @@ -40,8 +40,13 @@ export class Fragment { if (!(oppDir instanceof TNode)) throw new Error('oppDir must be passed to Fragment'); if (withDir.parent !== oppDir.parent) throw new Error('withDir and oppDir must have the same parent'); - this.ends[dir] = withDir; - this.ends[dir === L ? R : L] = oppDir; + if (dir === 'left') { + this.ends.left = withDir; + this.ends.right = oppDir; + } else { + this.ends.left = oppDir; + this.ends.right = withDir; + } // To build the html collection for a fragment, accumulate elements into an array and then call elements.add // once on the result. elements.add sorts the collection according to document order each time it is called, so @@ -57,7 +62,7 @@ export class Fragment { // like Cursor::withDirInsertAt(dir, parent, withDir, oppDir) withDirAdopt(dir: Direction, parent: TNode, withDir?: TNode, oppDir?: TNode) { - return dir === L ? this.adopt(parent, withDir, oppDir) : this.adopt(parent, oppDir, withDir); + return dir === 'left' ? this.adopt(parent, withDir, oppDir) : this.adopt(parent, oppDir, withDir); } adopt(parent: TNode, leftward?: TNode, rightward?: TNode) { @@ -65,30 +70,30 @@ export class Fragment { this.disowned = false; - const leftEnd = this.ends[L]; + const leftEnd = this.ends.left; if (!leftEnd) return this; - const rightEnd = this.ends[R]; + const rightEnd = this.ends.right; if (leftward) { // NB: this is handled in the ::each() block - // leftward[R] = leftEnd + // leftward.right = leftEnd } else { - parent.ends[L] = leftEnd; + parent.ends.left = leftEnd; } if (rightward) { - rightward[L] = rightEnd; + rightward.left = rightEnd; } else { - parent.ends[R] = rightEnd; + parent.ends.right = rightEnd; } - if (this.ends[R]) this.ends[R][R] = rightward; + if (this.ends.right) this.ends.right.right = rightward; this.each((el: TNode) => { - el[L] = leftward; + el.left = leftward; el.parent = parent; - if (leftward) leftward[R] = el; + if (leftward) leftward.right = el; leftward = el; return true; @@ -98,30 +103,30 @@ export class Fragment { } disown() { - const leftEnd = this.ends[L]; + const leftEnd = this.ends.left; // guard for empty and already-disowned fragments if (!leftEnd || this.disowned) return this; this.disowned = true; - const rightEnd = this.ends[R]; + const rightEnd = this.ends.right; const parent = leftEnd.parent; if (!parent) throw new Error('a parent must always present'); - prayWellFormed(parent, leftEnd[L], leftEnd); - prayWellFormed(parent, rightEnd, rightEnd?.[R]); + prayWellFormed(parent, leftEnd.left, leftEnd); + prayWellFormed(parent, rightEnd, rightEnd?.right); - if (leftEnd[L]) { - leftEnd[L][R] = rightEnd?.[R]; + if (leftEnd.left) { + leftEnd.left.right = rightEnd?.right; } else { - parent.ends[L] = rightEnd?.[R]; + parent.ends.left = rightEnd?.right; } - if (rightEnd?.[R]) { - rightEnd[R][L] = leftEnd[L]; + if (rightEnd?.right) { + rightEnd.right.left = leftEnd.left; } else { - parent.ends[R] = leftEnd[L]; + parent.ends.right = leftEnd.left; } return this; diff --git a/src/tree/node.ts b/src/tree/node.ts index 7c9b34ee..b704f4a8 100644 --- a/src/tree/node.ts +++ b/src/tree/node.ts @@ -1,7 +1,7 @@ // TNode base class of edit tree-related objects import type { Direction } from 'src/constants'; -import { L, R, iterator, mqCmdId, mqBlockId } from 'src/constants'; +import { iterator, mqCmdId, mqBlockId } from 'src/constants'; import type { Options } from 'src/options'; import type { Controller } from 'src/controller'; import type { Cursor } from 'src/cursor'; @@ -10,8 +10,8 @@ import { VNode } from 'tree/vNode'; import { Fragment } from 'tree/fragment'; export interface Ends { - [L]?: TNode; - [R]?: TNode; + left?: TNode; + right?: TNode; } const prayOverridden = (name: string) => { @@ -29,8 +29,8 @@ export class TNode { id: number; parent?: TNode; ends: Ends = {}; - [L]?: TNode; - [R]?: TNode; + left?: TNode; + right?: TNode; controller?: Controller; ctrlSeq = ''; @@ -48,16 +48,14 @@ export class TNode { reflow?: () => void; - bubble = iterator( - (yield_: (node: TNode) => TNode | boolean | undefined) => { - // eslint-disable-next-line @typescript-eslint/no-this-alias - for (let ancestor: TNode | undefined = this; ancestor; ancestor = ancestor.parent) { - if (yield_(ancestor) === false) break; - } - - return this; + bubble = iterator((yield_: (node: TNode) => TNode | boolean | undefined) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + for (let ancestor: TNode | undefined = this; ancestor; ancestor = ancestor.parent) { + if (yield_(ancestor) === false) break; } - ); + + return this; + }); postOrder = iterator((yield_: (node: TNode) => TNode | boolean | undefined) => { (function recurse(descendant: TNode) { @@ -109,15 +107,15 @@ export class TNode { } createDir(dir: Direction | undefined, cursor: Cursor) { - if (dir !== L && dir !== R) throw new Error('a direction was not passed'); + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); this.domify(); this.elements.insDirOf(dir, cursor.element); - if (cursor.parent) cursor[dir] = this.adopt(cursor.parent, cursor[L], cursor[R]); + if (cursor.parent) cursor[dir] = this.adopt(cursor.parent, cursor.left, cursor.right); return this; } createLeftOf(el: Cursor) { - this.createDir(L, el); + this.createDir('left', el); } selectChildren(leftEnd?: TNode, rightEnd?: TNode) { @@ -125,7 +123,7 @@ export class TNode { } isEmpty() { - return !this.ends[L] && !this.ends[R]; + return !this.ends.left && !this.ends.right; } isStyleBlock() { @@ -133,7 +131,7 @@ export class TNode { } children() { - return new Fragment(this.ends[L], this.ends[R]); + return new Fragment(this.ends.left, this.ends.right); } eachChild(method: 'postOrder' | ((node: TNode) => boolean), order?: string) { @@ -175,7 +173,7 @@ export class TNode { switch (key) { case 'Ctrl-Shift-Backspace': case 'Ctrl-Backspace': - ctrlr.ctrlDeleteDir(L); + ctrlr.ctrlDeleteDir('left'); break; case 'Shift-Backspace': @@ -186,13 +184,13 @@ export class TNode { // Tab or Esc -> go one block right if it exists, else escape right. case 'Escape': case 'Tab': - ctrlr.escapeDir(R, key, e); + ctrlr.escapeDir('right', key, e); return; // Shift-Tab -> go one block left if it exists, else escape left. case 'Shift-Tab': case 'Shift-Escape': - ctrlr.escapeDir(L, key, e); + ctrlr.escapeDir('left', key, e); return; // End -> move to the end of the current block. @@ -207,14 +205,14 @@ export class TNode { // Shift-End -> select to the end of the current block. case 'Shift-End': - while (cursor[R]) { + while (cursor.right) { ctrlr.selectRight(); } break; // Ctrl-Shift-End -> select to the end of the root block. case 'Ctrl-Shift-End': - while (cursor[R] || cursor.parent !== ctrlr.root) { + while (cursor.right || cursor.parent !== ctrlr.root) { ctrlr.selectRight(); } break; @@ -231,14 +229,14 @@ export class TNode { // Shift-Home -> select to the start of the current block. case 'Shift-Home': - while (cursor[L]) { + while (cursor.left) { ctrlr.selectLeft(); } break; // Ctrl-Shift-Home -> move to the start of the root block. case 'Ctrl-Shift-Home': - while (cursor[L] || cursor.parent !== ctrlr.root) { + while (cursor.left || cursor.parent !== ctrlr.root) { ctrlr.selectLeft(); } break; @@ -269,18 +267,18 @@ export class TNode { break; case 'Shift-Up': - if (cursor[L]) { + if (cursor.left) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor[L]) ctrlr.selectLeft(); + while (cursor.left) ctrlr.selectLeft(); } else { ctrlr.selectLeft(); } break; case 'Shift-Down': - if (cursor[R]) { + if (cursor.right) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor[R]) ctrlr.selectRight(); + while (cursor.right) ctrlr.selectRight(); } else { ctrlr.selectRight(); } @@ -293,7 +291,7 @@ export class TNode { case 'Ctrl-Shift-Delete': case 'Ctrl-Delete': - ctrlr.ctrlDeleteDir(R); + ctrlr.ctrlDeleteDir('right'); break; case 'Shift-Delete': @@ -304,7 +302,7 @@ export class TNode { case 'Meta-A': case 'Ctrl-A': ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (cursor[L]) ctrlr.selectLeft(); + while (cursor.left) ctrlr.selectLeft(); break; default: @@ -326,28 +324,28 @@ export class TNode { focus() { /* do nothing */ } - blur(_ignore_cursor?: Cursor) { + blur(_cursor?: Cursor) { /* do nothing */ } - seek(_ignore_left: number, _ignore_cursor: Cursor) { + seek(_left: number, _cursor: Cursor) { /* do nothing */ } - writeLatex(_ignore_cursor: Cursor, _ignore_latex: string) { + writeLatex(_cursor: Cursor, _latex: string) { /* do nothing */ } - finalizeInsert(_ignore_options: Options, _ignore_cursor: Cursor) { + finalizeInsert(_options: Options, _cursor: Cursor) { /* do nothing */ } - write(_ignore_cursor: Cursor, _ignore_ch: string) { + write(_cursor: Cursor, _ch: string) { /* do nothing */ } - replaces(_ignore_fragment?: string | Fragment) { + replaces(_fragment?: string | Fragment) { /* do nothing */ } - setOptions(_ignore_options: { text?: () => string; htmlTemplate?: string; latex?: () => string }) { + setOptions(_options: { text?: () => string; htmlTemplate?: string; latex?: () => string }) { return this; } - chToCmd(_ignore_ch: string, _ignore_options: Options): TNode { + chToCmd(_ch: string, _options: Options): TNode { return this as TNode; } @@ -360,31 +358,31 @@ export class TNode { } // called by Controller::escapeDir, moveDir - moveOutOf(_ignore_dir: Direction, _ignore_cursor?: Cursor, _ignore_updown?: 'up' | 'down') { + moveOutOf(_dir: Direction, _cursor?: Cursor, _updown?: 'up' | 'down') { prayOverridden('moveOutOf'); } // called by Controller::moveDir - moveTowards(_ignore_dir: Direction, _ignore_cursor: Cursor, _ignore_updown?: 'up' | 'down') { + moveTowards(_dir: Direction, _cursor: Cursor, _updown?: 'up' | 'down') { prayOverridden('moveTowards'); } // called by Controller::deleteDir - deleteOutOf(_ignore_dir: Direction, _ignore_cursor?: Cursor) { + deleteOutOf(_dir: Direction, _cursor?: Cursor) { prayOverridden('deleteOutOf'); } // called by Controller::deleteDir - deleteTowards(_ignore_dir: Direction, _ignore_cursor: Cursor) { + deleteTowards(_dir: Direction, _cursor: Cursor) { prayOverridden('deleteTowards'); } // called by Controller::selectDir - unselectInto(_ignore_dir: Direction, _ignore_cursor: Cursor) { + unselectInto(_dir: Direction, _cursor: Cursor) { prayOverridden('unselectInto'); } // called by Controller::selectDir - selectOutOf(_ignore_dir: Direction, _ignore_cursor?: Cursor) { + selectOutOf(_dir: Direction, _cursor?: Cursor) { prayOverridden('selectOutOf'); } // called by Controller::selectDir - selectTowards(_ignore_dir: Direction, _ignore_cursor: Cursor) { + selectTowards(_dir: Direction, _cursor: Cursor) { prayOverridden('selectTowards'); } } diff --git a/src/tree/point.ts b/src/tree/point.ts index a7a3d7ec..79afe5b0 100644 --- a/src/tree/point.ts +++ b/src/tree/point.ts @@ -1,21 +1,20 @@ // Point base class of edit tree-related objects -import { L, R } from 'src/constants'; import type { TNode } from 'tree/node'; export class Point { parent?: TNode; - [L]?: TNode; - [R]?: TNode; + left?: TNode; + right?: TNode; ancestors?: Record; constructor(parent?: TNode, leftward?: TNode, rightward?: TNode) { this.parent = parent; - this[L] = leftward; - this[R] = rightward; + this.left = leftward; + this.right = rightward; } static copy(pt: Point) { - return new Point(pt.parent, pt[L], pt[R]); + return new Point(pt.parent, pt.left, pt.right); } } diff --git a/src/tree/vNode.ts b/src/tree/vNode.ts index 56c39cae..b58c4d1f 100644 --- a/src/tree/vNode.ts +++ b/src/tree/vNode.ts @@ -1,7 +1,6 @@ // Virtual Node class import type { Direction } from 'src/constants'; -import { L } from 'src/constants'; export class VNode { contents: Node[] = []; @@ -148,13 +147,13 @@ export class VNode { insDirOf(dir: Direction, vNode: Element | CharacterData | VNode) { if (vNode instanceof VNode && !vNode.contents.length) return; - if (dir === L) (vNode instanceof VNode ? vNode.first : vNode).before(...this.contents); + if (dir === 'left') (vNode instanceof VNode ? vNode.first : vNode).before(...this.contents); else (vNode instanceof VNode ? vNode.last : vNode).after(...this.contents); } insAtDirEnd(dir: Direction, vNode: Element | VNode) { if (vNode instanceof VNode && !vNode.contents.length) return; - if (dir === L) (vNode instanceof VNode ? vNode.firstElement : vNode).prepend(...this.contents); + if (dir === 'left') (vNode instanceof VNode ? vNode.firstElement : vNode).prepend(...this.contents); else (vNode instanceof VNode ? vNode.lastElement : vNode).append(...this.contents); } } diff --git a/test/MathFunction.test.js b/test/MathFunction.test.js index 4aa75c99..7b86f9e7 100644 --- a/test/MathFunction.test.js +++ b/test/MathFunction.test.js @@ -1,6 +1,5 @@ /* global suite, test, assert, setup, teardown, MQ */ -import { L, R } from 'src/constants'; import { VNode } from 'src/tree/vNode'; import { MathFunction, latexMathParser } from 'commands/mathElements'; @@ -85,10 +84,10 @@ suite('MathFunction', () => { test('basic latex output', () => { const tree = latexMathParser.parse('\\sin_2^3\\left(x^2+3\\right)').postOrder('finalizeTree', mq.options); - assert.ok(tree.ends[L] instanceof MathFunction); + assert.ok(tree.ends.left instanceof MathFunction); - assert.equal(tree.ends[L].ends[L].join('latex'), '_2^3'); - assert.equal(tree.ends[L].ends[R].join('latex'), 'x^2+3'); + assert.equal(tree.ends.left.ends.left.join('latex'), '_2^3'); + assert.equal(tree.ends.left.ends.right.join('latex'), 'x^2+3'); assert.equal(tree.join('latex'), '\\sin_2^3\\left(x^2+3\\right)'); }); diff --git a/test/SupSub.test.js b/test/SupSub.test.js index a68911f7..e99e4c70 100644 --- a/test/SupSub.test.js +++ b/test/SupSub.test.js @@ -1,6 +1,6 @@ /* global suite, test, assert, setup, MQ */ -import { L, R, noop, prayWellFormed } from 'src/constants'; +import { noop, prayWellFormed } from 'src/constants'; suite('SupSub', () => { let mq; @@ -10,7 +10,7 @@ suite('SupSub', () => { mq = MQ.MathField(field); }); - const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt[L], pt[R]); + const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt.left, pt.right); let expecteds = [ 'x_{ab} x_{ba}, x_a^b x_a^b; x_{ab} x_{ba}, x_a^b x_a^b; x_a x_a, x_a^{} x_a^{}', diff --git a/test/backspace.test.js b/test/backspace.test.js index 560de492..3917d363 100644 --- a/test/backspace.test.js +++ b/test/backspace.test.js @@ -1,6 +1,6 @@ /* global suite, test, assert, setup, MQ */ -import { L, R, prayWellFormed } from 'src/constants'; +import { prayWellFormed } from 'src/constants'; suite('backspace', () => { let mq, rootBlock, controller, cursor; @@ -13,7 +13,7 @@ suite('backspace', () => { cursor = controller.cursor; }); - const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt[L], pt[R]); + const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt.left, pt.right); const assertLatex = (latex) => { prayWellFormedPoint(mq.__controller.cursor); assert.equal(mq.latex(), latex); @@ -21,11 +21,11 @@ suite('backspace', () => { test('backspace through exponent', () => { controller.renderLatexMath('x^{nm}'); - const exp = rootBlock.ends[R], - expBlock = exp.ends[L]; + const exp = rootBlock.ends.right, + expBlock = exp.ends.left; assert.equal(exp.latex(), '^{nm}', 'right end el is exponent'); assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); - assert.equal(cursor[L], exp, 'cursor is at the end of root block'); + assert.equal(cursor.left, exp, 'cursor is at the end of root block'); mq.keystroke('Backspace'); assert.equal(cursor.parent, expBlock, 'cursor up goes into exponent on backspace'); @@ -203,21 +203,21 @@ suite('backspace', () => { mq.keystroke('Backspace'); - const textBlock = rootBlock.ends[R]; + const textBlock = rootBlock.ends.right; assert.equal(cursor.parent, textBlock, 'cursor is in text block'); - assert.equal(cursor[R], undefined, 'cursor is at the end of text block'); - assert.equal(cursor[L].text(), 'x', 'cursor is rightward of the x'); + assert.equal(cursor.right, undefined, 'cursor is at the end of text block'); + assert.equal(cursor.left.text(), 'x', 'cursor is rightward of the x'); assert.equal(mq.latex(), '\\text{x}', 'the x has been deleted'); mq.keystroke('Backspace'); assert.equal(cursor.parent, textBlock, 'cursor is still in text block'); - assert.equal(cursor[R], undefined, 'cursor is at the right end of the text block'); - assert.equal(cursor[L], undefined, 'cursor is at the left end of the text block'); + assert.equal(cursor.right, undefined, 'cursor is at the right end of the text block'); + assert.equal(cursor.left, undefined, 'cursor is at the left end of the text block'); assert.equal(mq.latex(), '', 'the x has been deleted'); mq.keystroke('Backspace'); - assert.equal(cursor[R], undefined, 'cursor is at the right end of the root block'); - assert.equal(cursor[L], undefined, 'cursor is at the left end of the root block'); + assert.equal(cursor.right, undefined, 'cursor is at the right end of the root block'); + assert.equal(cursor.left, undefined, 'cursor is at the left end of the root block'); assert.equal(mq.latex(), ''); }); diff --git a/test/latex.test.js b/test/latex.test.js index 4d9f80e8..f695975e 100644 --- a/test/latex.test.js +++ b/test/latex.test.js @@ -1,6 +1,5 @@ /* global suite, test, assert, setup, MQ */ -import { L } from 'src/constants'; import { Options } from 'src/options'; import { Bracket, latexMathParser } from 'commands/mathElements'; @@ -89,8 +88,8 @@ suite('latex', () => { test('parens', () => { const tree = latexMathParser.parse('\\left(123\\right)'); - assert.ok(tree.ends[L] instanceof Bracket); - const contents = tree.ends[L].ends[L].join('latex'); + assert.ok(tree.ends.left instanceof Bracket); + const contents = tree.ends.left.ends.left.join('latex'); assert.equal(contents, '123'); assert.equal(tree.join('latex'), '\\left(123\\right)'); }); @@ -98,8 +97,8 @@ suite('latex', () => { test('\\langle/\\rangle (issue #508)', () => { const tree = latexMathParser.parse('\\left\\langle 123\\right\\rangle)'); - assert.ok(tree.ends[L] instanceof Bracket); - const contents = tree.ends[L].ends[L].join('latex'); + assert.ok(tree.ends.left instanceof Bracket); + const contents = tree.ends.left.ends.left.join('latex'); assert.equal(contents, '123'); assert.equal(tree.join('latex'), '\\left\\langle 123\\right\\rangle )'); }); @@ -107,8 +106,8 @@ suite('latex', () => { test('\\langle/\\rangle (without whitespace)', () => { const tree = latexMathParser.parse('\\left\\langle123\\right\\rangle)'); - assert.ok(tree.ends[L] instanceof Bracket); - const contents = tree.ends[L].ends[L].join('latex'); + assert.ok(tree.ends.left instanceof Bracket); + const contents = tree.ends.left.ends.left.join('latex'); assert.equal(contents, '123'); assert.equal(tree.join('latex'), '\\left\\langle 123\\right\\rangle )'); }); @@ -116,8 +115,8 @@ suite('latex', () => { test('\\lVert/\\rVert', () => { const tree = latexMathParser.parse('\\left\\lVert 123\\right\\rVert)'); - assert.ok(tree.ends[L] instanceof Bracket); - const contents = tree.ends[L].ends[L].join('latex'); + assert.ok(tree.ends.left instanceof Bracket); + const contents = tree.ends.left.ends.left.join('latex'); assert.equal(contents, '123'); assert.equal(tree.join('latex'), '\\left\\lVert 123\\right\\rVert )'); }); @@ -125,8 +124,8 @@ suite('latex', () => { test('\\lVert/\\rVert (without whitespace)', () => { const tree = latexMathParser.parse('\\left\\lVert123\\right\\rVert)'); - assert.ok(tree.ends[L] instanceof Bracket); - const contents = tree.ends[L].ends[L].join('latex'); + assert.ok(tree.ends.left instanceof Bracket); + const contents = tree.ends.left.ends.left.join('latex'); assert.equal(contents, '123'); assert.equal(tree.join('latex'), '\\left\\lVert 123\\right\\rVert )'); }); diff --git a/test/publicapi.test.js b/test/publicapi.test.js index dd2a0a47..8d9014a5 100644 --- a/test/publicapi.test.js +++ b/test/publicapi.test.js @@ -1,7 +1,5 @@ /* global suite, test, assert, setup, teardown, MQ */ -import { L, R } from 'src/constants'; - suite('Public API', () => { suite('global functions', () => { test('undefined', () => { @@ -122,14 +120,14 @@ suite('Public API', () => { test('.moveToDirEnd(dir)', () => { mq.latex('a x^2 + b x + c = 0'); - assert.equal(mq.__controller.cursor[L].ctrlSeq, '0'); - assert.equal(mq.__controller.cursor[R], undefined); + assert.equal(mq.__controller.cursor.left.ctrlSeq, '0'); + assert.equal(mq.__controller.cursor.right, undefined); mq.moveToLeftEnd(); - assert.equal(mq.__controller.cursor[L], undefined); - assert.equal(mq.__controller.cursor[R].ctrlSeq, 'a'); + assert.equal(mq.__controller.cursor.left, undefined); + assert.equal(mq.__controller.cursor.right.ctrlSeq, 'a'); mq.moveToRightEnd(); - assert.equal(mq.__controller.cursor[L].ctrlSeq, '0'); - assert.equal(mq.__controller.cursor[R], undefined); + assert.equal(mq.__controller.cursor.left.ctrlSeq, '0'); + assert.equal(mq.__controller.cursor.right, undefined); }); test('.empty()', () => { @@ -201,11 +199,11 @@ suite('Public API', () => { mq.keystroke('Right'); // stay at right edge assert.equal(moveCounter, 1); - assert.equal(dir, R); + assert.equal(dir, 'right'); mq.keystroke('Right'); // stay at right edge assert.equal(moveCounter, 2); - assert.equal(dir, R); + assert.equal(dir, 'right'); mq.keystroke('Left'); // right edge of denominator assert.equal(moveCounter, 2); @@ -227,20 +225,20 @@ suite('Public API', () => { mq.keystroke('Left'); // stays at left edge assert.equal(moveCounter, 3); - assert.equal(dir, L); + assert.equal(dir, 'left'); assert.equal(deleteCounter, 0); mq.keystroke('Backspace'); // stays at left edge assert.equal(deleteCounter, 1); - assert.equal(dir, L); + assert.equal(dir, 'left'); mq.keystroke('Backspace'); // stays at left edge assert.equal(deleteCounter, 2); - assert.equal(dir, L); + assert.equal(dir, 'left'); mq.keystroke('Left'); // stays at left edge assert.equal(moveCounter, 4); - assert.equal(dir, L); + assert.equal(dir, 'left'); const mock = document.getElementById('mock'); while (mock.firstChild) mock.firstChild.remove(); @@ -405,14 +403,14 @@ suite('Public API', () => { mq.keystroke('Spacebar'); mq.typedText(' '); - assert.equal(cursor[L].ctrlSeq, '\\ ', 'left of the cursor is ' + cursor[L].ctrlSeq); - assert.equal(cursor[R], undefined, 'right of the cursor is ' + cursor[R]); + assert.equal(cursor.left.ctrlSeq, '\\ ', 'left of the cursor is ' + cursor.left.ctrlSeq); + assert.equal(cursor.right, undefined, 'right of the cursor is ' + cursor.right); mq.keystroke('Backspace'); mq.keystroke('Shift-Spacebar'); mq.typedText(' '); - assert.equal(cursor[L].ctrlSeq, '\\ ', 'left of the cursor is ' + cursor[L].ctrlSeq); - assert.equal(cursor[R], undefined, 'right of the cursor is ' + cursor[R]); + assert.equal(cursor.left.ctrlSeq, '\\ ', 'left of the cursor is ' + cursor.left.ctrlSeq); + assert.equal(cursor.right, undefined, 'right of the cursor is ' + cursor.right); }); test('space behaves like tab when spaceBehavesLikeTab is true', () => { const el = document.createElement('span'); @@ -425,13 +423,13 @@ suite('Public API', () => { mq.keystroke('Left'); mq.keystroke('Spacebar'); - assert.equal(cursor[L].parent, rootBlock, 'parent of the cursor is ' + cursor[L].ctrlSeq); - assert.equal(cursor[R], undefined, 'right cursor is ' + cursor[R]); + assert.equal(cursor.left.parent, rootBlock, 'parent of the cursor is ' + cursor.left.ctrlSeq); + assert.equal(cursor.right, undefined, 'right cursor is ' + cursor.right); mq.keystroke('Left'); mq.keystroke('Shift-Spacebar'); - assert.equal(cursor[L], undefined, 'left cursor is ' + cursor[L]); - assert.equal(cursor[R], rootBlock.ends[L], 'parent of rootBlock is ' + cursor[R]); + assert.equal(cursor.left, undefined, 'left cursor is ' + cursor.left); + assert.equal(cursor.right, rootBlock.ends.left, 'parent of rootBlock is ' + cursor.right); }); test('space behaves like tab when globally set to true', () => { MQ.config({ spaceBehavesLikeTab: true }); @@ -447,7 +445,7 @@ suite('Public API', () => { mq.keystroke('Left'); mq.keystroke('Spacebar'); assert.equal(cursor.parent, rootBlock, 'cursor in root block'); - assert.equal(cursor[R], undefined, 'cursor at end of block'); + assert.equal(cursor.right, undefined, 'cursor at end of block'); MQ.config({ spaceBehavesLikeTab: false }); }); diff --git a/test/select.test.js b/test/select.test.js index 47562131..b8def698 100644 --- a/test/select.test.js +++ b/test/select.test.js @@ -1,6 +1,6 @@ /* global suite, test, assert */ -import { L, R, noop } from 'src/constants'; +import { noop } from 'src/constants'; import { Cursor } from 'src/cursor'; import { Point } from 'tree/point'; import { TNode } from 'tree/node'; @@ -18,18 +18,18 @@ suite('Cursor::select()', () => { let count = 0; lca.selectChildren = function (leftEnd, rightEnd) { count += 1; - assert.equal(frag.ends[L], leftEnd); - assert.equal(frag.ends[R], rightEnd); + assert.equal(frag.ends.left, leftEnd); + assert.equal(frag.ends.right, rightEnd); return TNode.prototype.selectChildren.apply(this, arguments); }; cursor.parent = A.parent; - cursor[L] = A[L]; - cursor[R] = A[R]; + cursor.left = A.left; + cursor.right = A.right; cursor.startSelection(); cursor.parent = B.parent; - cursor[L] = B[L]; - cursor[R] = B[R]; + cursor.left = B.left; + cursor.right = B.right; assert.equal(cursor.select(), true); assert.equal(count, 1); @@ -38,9 +38,9 @@ suite('Cursor::select()', () => { }; const parent = new TNode(); - const child1 = new TNode().adopt(parent, parent.ends[R]); - const child2 = new TNode().adopt(parent, parent.ends[R]); - const child3 = new TNode().adopt(parent, parent.ends[R]); + const child1 = new TNode().adopt(parent, parent.ends.right); + const child2 = new TNode().adopt(parent, parent.ends.right); + const child3 = new TNode().adopt(parent, parent.ends.right); const A = new Point(parent, undefined, child1); const B = new Point(parent, child1, child2); const C = new Point(parent, child2, child3); @@ -88,8 +88,8 @@ suite('Cursor::select()', () => { test('same Point', () => { cursor.parent = A.parent; - cursor[L] = A[L]; - cursor[R] = A[R]; + cursor.left = A.left; + cursor.right = A.right; cursor.startSelection(); assert.equal(cursor.select(), false); }); @@ -98,21 +98,21 @@ suite('Cursor::select()', () => { const anotherTree = new TNode(); cursor.parent = A.parent; - cursor[L] = A[L]; - cursor[R] = A[R]; + cursor.left = A.left; + cursor.right = A.right; cursor.startSelection(); cursor.parent = anotherTree; - cursor[L] = 0; - cursor[R] = 0; + cursor.left = 0; + cursor.right = 0; assert.throws(() => cursor.select()); cursor.parent = anotherTree; - cursor[L] = 0; - cursor[R] = 0; + cursor.left = 0; + cursor.right = 0; cursor.startSelection(); cursor.parent = A.parent; - cursor[L] = A[L]; - cursor[R] = A[R]; + cursor.left = A.left; + cursor.right = A.right; assert.throws(() => cursor.select()); }); }); diff --git a/test/text.test.js b/test/text.test.js index 32d40938..ed552336 100644 --- a/test/text.test.js +++ b/test/text.test.js @@ -1,6 +1,6 @@ /* global suite, test, assert, setup, MQ */ -import { L, R, prayWellFormed } from 'src/constants'; +import { prayWellFormed } from 'src/constants'; import { Controller } from 'src/controller'; import { latexMathParser } from 'commands/mathElements'; @@ -17,7 +17,7 @@ suite('text', () => { }); }); - const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt[L], pt[R]); + const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt.left, pt.right); const assertLatex = (latex) => { prayWellFormedPoint(mq.__controller.cursor); assert.equal(mostRecentlyReportedLatex, latex, 'assertLatex failed'); @@ -104,7 +104,7 @@ suite('text', () => { mq.keystroke('Right'); assertSplit(cursor.element); - assert.equal(cursor[L], undefined); + assert.equal(cursor.left, undefined); assertLatex(''); }); diff --git a/test/tree.test.js b/test/tree.test.js index 51dedea2..0db861f0 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -1,6 +1,5 @@ /* global suite, test, assert */ -import { L, R } from 'src/constants'; import { TNode } from 'tree/node'; import { Fragment } from 'tree/fragment'; @@ -10,13 +9,13 @@ suite('tree', () => { assert.equal(one.parent, parent, 'one.parent is set'); assert.equal(two.parent, parent, 'two.parent is set'); - assert.ok(!one[L], 'one has nothing leftward'); - assert.equal(one[R], two, 'one[R] is two'); - assert.equal(two[L], one, 'two[L] is one'); - assert.ok(!two[R], 'two has nothing rightward'); + assert.ok(!one.left, 'one has nothing leftward'); + assert.equal(one.right, two, 'one.right is two'); + assert.equal(two.left, one, 'two.left is one'); + assert.ok(!two.right, 'two has nothing rightward'); - assert.equal(parent.ends[L], one, 'parent.ends[L] is one'); - assert.equal(parent.ends[R], two, 'parent.ends[R] is two'); + assert.equal(parent.ends.left, one, 'parent.ends.left is one'); + assert.equal(parent.ends.right, two, 'parent.ends.right is two'); }; test('the empty case', () => { @@ -26,11 +25,11 @@ suite('tree', () => { child.adopt(parent); assert.equal(child.parent, parent, 'child.parent is set'); - assert.ok(!child[R], 'child has nothing rightward'); - assert.ok(!child[L], 'child has nothing leftward'); + assert.ok(!child.right, 'child has nothing rightward'); + assert.ok(!child.left, 'child has nothing leftward'); - assert.equal(parent.ends[L], child, 'child is parent.ends[L]'); - assert.equal(parent.ends[R], child, 'child is parent.ends[R]'); + assert.equal(parent.ends.left, child, 'child is parent.ends.left'); + assert.equal(parent.ends.right, child, 'child is parent.ends.right'); }); test('with two children from the left', () => { @@ -66,23 +65,23 @@ suite('tree', () => { middle.adopt(parent, leftward, rightward); assert.equal(middle.parent, parent, 'middle.parent is set'); - assert.equal(middle[L], leftward, 'middle[L] is set'); - assert.equal(middle[R], rightward, 'middle[R] is set'); + assert.equal(middle.left, leftward, 'middle.left is set'); + assert.equal(middle.right, rightward, 'middle.right is set'); - assert.equal(leftward[R], middle, 'leftward[R] is middle'); - assert.equal(rightward[L], middle, 'rightward[L] is middle'); + assert.equal(leftward.right, middle, 'leftward.right is middle'); + assert.equal(rightward.left, middle, 'rightward.left is middle'); - assert.equal(parent.ends[L], leftward, 'parent.ends[L] is leftward'); - assert.equal(parent.ends[R], rightward, 'parent.ends[R] is rightward'); + assert.equal(parent.ends.left, leftward, 'parent.ends.left is leftward'); + assert.equal(parent.ends.right, rightward, 'parent.ends.right is rightward'); }); }); suite('disown', () => { const assertSingleChild = (parent, child) => { - assert.equal(parent.ends[L], child, 'parent.ends[L] is child'); - assert.equal(parent.ends[R], child, 'parent.ends[R] is child'); - assert.ok(!child[L], 'child has nothing leftward'); - assert.ok(!child[R], 'child has nothing rightward'); + assert.equal(parent.ends.left, child, 'parent.ends.left is child'); + assert.equal(parent.ends.right, child, 'parent.ends.right is child'); + assert.ok(!child.left, 'child has nothing leftward'); + assert.ok(!child.right, 'child has nothing rightward'); }; test('the empty case', () => { @@ -92,8 +91,8 @@ suite('tree', () => { child.adopt(parent); child.disown(); - assert.ok(!parent.ends[L], 'parent has no left end child'); - assert.ok(!parent.ends[R], 'parent has no right end child'); + assert.ok(!parent.ends.left, 'parent has no left end child'); + assert.ok(!parent.ends.right, 'parent has no right end child'); }); test('disowning the right end child', () => { @@ -109,7 +108,7 @@ suite('tree', () => { assertSingleChild(parent, one); assert.equal(two.parent, parent, 'two retains its parent'); - assert.equal(two[L], one, 'two retains its [L]'); + assert.equal(two.left, one, 'two retains its .left'); assert.throws(() => two.disown(), 'disown fails on a malformed tree'); }); @@ -127,7 +126,7 @@ suite('tree', () => { assertSingleChild(parent, two); assert.equal(one.parent, parent, 'one retains its parent'); - assert.equal(one[R], two, 'one retains its [R]'); + assert.equal(one.right, two, 'one retains its .right'); assert.throws(() => one.disown(), 'disown fails on a malformed tree'); }); @@ -144,14 +143,14 @@ suite('tree', () => { middle.disown(); - assert.equal(leftward[R], rightward, 'leftward[R] is rightward'); - assert.equal(rightward[L], leftward, 'rightward[L] is leftward'); - assert.equal(parent.ends[L], leftward, 'parent.ends[L] is leftward'); - assert.equal(parent.ends[R], rightward, 'parent.ends[R] is rightward'); + assert.equal(leftward.right, rightward, 'leftward.right is rightward'); + assert.equal(rightward.left, leftward, 'rightward.left is leftward'); + assert.equal(parent.ends.left, leftward, 'parent.ends.left is leftward'); + assert.equal(parent.ends.right, rightward, 'parent.ends.right is rightward'); assert.equal(middle.parent, parent, 'middle retains its parent'); - assert.equal(middle[R], rightward, 'middle retains its [R]'); - assert.equal(middle[L], leftward, 'middle retains its [L]'); + assert.equal(middle.right, rightward, 'middle retains its .right'); + assert.equal(middle.left, leftward, 'middle retains its .left'); assert.throws(() => middle.disown(), 'disown fails on a malformed tree'); }); @@ -180,18 +179,18 @@ suite('tree', () => { } }; const parent = new TNode(); - new ChNode('a').adopt(parent, parent.ends[R]); - const b = new ChNode('b').adopt(parent, parent.ends[R]); - new ChNode('c').adopt(parent, parent.ends[R]); - const d = new ChNode('d').adopt(parent, parent.ends[R]); - new ChNode('e').adopt(parent, parent.ends[R]); + new ChNode('a').adopt(parent, parent.ends.right); + const b = new ChNode('b').adopt(parent, parent.ends.right); + new ChNode('c').adopt(parent, parent.ends.right); + const d = new ChNode('d').adopt(parent, parent.ends.right); + new ChNode('e').adopt(parent, parent.ends.right); const cat = (str, node) => str + node.ch; assert.equal('bcd', new Fragment(b, d).fold('', cat)); - assert.equal('bcd', new Fragment(b, d, L).fold('', cat)); - assert.equal('bcd', new Fragment(d, b, R).fold('', cat)); - assert.throws(() => new Fragment(d, b, L)); - assert.throws(() => new Fragment(b, d, R)); + assert.equal('bcd', new Fragment(b, d, 'left').fold('', cat)); + assert.equal('bcd', new Fragment(d, b, 'right').fold('', cat)); + assert.throws(() => new Fragment(d, b, 'left')); + assert.throws(() => new Fragment(b, d, 'right')); }); test('disown is idempotent', () => { diff --git a/test/typing.test.js b/test/typing.test.js index 672c4a54..b81164c1 100644 --- a/test/typing.test.js +++ b/test/typing.test.js @@ -1,7 +1,7 @@ /* global suite, test, assert, setup, MQ */ import { Bracket } from 'commands/mathElements'; -import { L, R, prayWellFormed } from 'src/constants'; +import { prayWellFormed } from 'src/constants'; suite('typing with auto-replaces', () => { let mq, mostRecentlyReportedLatex; @@ -16,7 +16,7 @@ suite('typing with auto-replaces', () => { }); }); - const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt[L], pt[R]); + const prayWellFormedPoint = (pt) => prayWellFormed(pt.parent, pt.left, pt.right); const assertLatex = (latex) => { prayWellFormedPoint(mq.__controller.cursor); assert.equal(mostRecentlyReportedLatex, latex); @@ -813,7 +813,7 @@ suite('typing with auto-replaces', () => { test('selected and replaced by LiveFraction solidifies ghosts (1+2)/( )', () => { mq.typedText('1+2)/'); assertLatex('\\frac{\\left(1+2\\right)}{ }'); - const bracket = mq.__controller.cursor.parent?.parent?.ends[L]?.ends[L]; + const bracket = mq.__controller.cursor.parent?.parent?.ends.left?.ends.left; assert.ok(bracket instanceof Bracket); assert.ok(!bracket.elements.children().first.classList.contains('mq-ghost')); assert.ok(!bracket.elements.children().last.classList.contains('mq-ghost')); diff --git a/test/updown.test.js b/test/updown.test.js index d4e46837..eae5c733 100644 --- a/test/updown.test.js +++ b/test/updown.test.js @@ -1,7 +1,5 @@ /* global suite, test, assert, setup, MQ */ -import { L, R } from 'src/constants'; - suite('up/down', () => { let mq, rootBlock, controller, cursor; setup(() => { @@ -15,116 +13,116 @@ suite('up/down', () => { test('up/down in out of exponent', () => { controller.renderLatexMath('x^{nm}'); - const exp = rootBlock.ends[R], - expBlock = exp.ends[L]; + const exp = rootBlock.ends.right, + expBlock = exp.ends.left; assert.equal(exp.latex(), '^{nm}', 'right end el is exponent'); assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); - assert.equal(cursor[L], exp, 'cursor is at the end of root block'); + assert.equal(cursor.left, exp, 'cursor is at the end of root block'); mq.keystroke('Up'); assert.equal(cursor.parent, expBlock, 'cursor up goes into exponent'); mq.keystroke('Down'); assert.equal(cursor.parent, rootBlock, 'cursor down leaves exponent'); - assert.equal(cursor[L], exp, 'down when cursor at end of exponent puts cursor after exponent'); + assert.equal(cursor.left, exp, 'down when cursor at end of exponent puts cursor after exponent'); mq.keystroke('Up Left Left'); assert.equal(cursor.parent, expBlock, 'cursor up left stays in exponent'); - assert.equal(cursor[L], undefined, 'cursor is at the beginning of exponent'); + assert.equal(cursor.left, undefined, 'cursor is at the beginning of exponent'); mq.keystroke('Down'); assert.equal(cursor.parent, rootBlock, 'cursor down leaves exponent'); - assert.equal(cursor[R], exp, 'cursor down in beginning of exponent puts cursor before exponent'); + assert.equal(cursor.right, exp, 'cursor down in beginning of exponent puts cursor before exponent'); mq.keystroke('Up Right'); assert.equal(cursor.parent, expBlock, 'cursor up left stays in exponent'); - assert.equal(cursor[L].latex(), 'n', 'cursor is in the middle of exponent'); - assert.equal(cursor[R].latex(), 'm', 'cursor is in the middle of exponent'); + assert.equal(cursor.left.latex(), 'n', 'cursor is in the middle of exponent'); + assert.equal(cursor.right.latex(), 'm', 'cursor is in the middle of exponent'); mq.keystroke('Down'); assert.equal(cursor.parent, rootBlock, 'cursor down leaves exponent'); - assert.equal(cursor[R], exp, 'cursor down in middle of exponent puts cursor before exponent'); + assert.equal(cursor.right, exp, 'cursor down in middle of exponent puts cursor before exponent'); }); // literally just swapped up and down, exponent with subscript, nm with 12 test('up/down in out of subscript', () => { controller.renderLatexMath('a_{12}'); - const sub = rootBlock.ends[R], - subBlock = sub.ends[L]; + const sub = rootBlock.ends.right, + subBlock = sub.ends.left; assert.equal(sub.latex(), '_{12}', 'right end el is subscript'); assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); - assert.equal(cursor[L], sub, 'cursor is at the end of root block'); + assert.equal(cursor.left, sub, 'cursor is at the end of root block'); mq.keystroke('Down'); assert.equal(cursor.parent, subBlock, 'cursor down goes into subscript'); mq.keystroke('Up'); assert.equal(cursor.parent, rootBlock, 'cursor up leaves subscript'); - assert.equal(cursor[L], sub, 'up when cursor at end of subscript puts cursor after subscript'); + assert.equal(cursor.left, sub, 'up when cursor at end of subscript puts cursor after subscript'); mq.keystroke('Down Left Left'); assert.equal(cursor.parent, subBlock, 'cursor down left stays in subscript'); - assert.equal(cursor[L], undefined, 'cursor is at the beginning of subscript'); + assert.equal(cursor.left, undefined, 'cursor is at the beginning of subscript'); mq.keystroke('Up'); assert.equal(cursor.parent, rootBlock, 'cursor up leaves subscript'); - assert.equal(cursor[R], sub, 'cursor up in beginning of subscript puts cursor before subscript'); + assert.equal(cursor.right, sub, 'cursor up in beginning of subscript puts cursor before subscript'); mq.keystroke('Down Right'); assert.equal(cursor.parent, subBlock, 'cursor down left stays in subscript'); - assert.equal(cursor[L].latex(), '1', 'cursor is in the middle of subscript'); - assert.equal(cursor[R].latex(), '2', 'cursor is in the middle of subscript'); + assert.equal(cursor.left.latex(), '1', 'cursor is in the middle of subscript'); + assert.equal(cursor.right.latex(), '2', 'cursor is in the middle of subscript'); mq.keystroke('Up'); assert.equal(cursor.parent, rootBlock, 'cursor up leaves subscript'); - assert.equal(cursor[R], sub, 'cursor up in middle of subscript puts cursor before subscript'); + assert.equal(cursor.right, sub, 'cursor up in middle of subscript puts cursor before subscript'); }); test('up/down into and within fraction', () => { controller.renderLatexMath('\\frac{12}{34}'); - const frac = rootBlock.ends[L], - numer = frac.ends[L], - denom = frac.ends[R]; + const frac = rootBlock.ends.left, + numer = frac.ends.left, + denom = frac.ends.right; assert.equal(frac.latex(), '\\frac{12}{34}', 'fraction is in root block'); - assert.equal(frac, rootBlock.ends[R], 'fraction is sole child of root block'); + assert.equal(frac, rootBlock.ends.right, 'fraction is sole child of root block'); assert.equal(numer.latex(), '12', 'numerator is left end child of fraction'); assert.equal(denom.latex(), '34', 'denominator is right end child of fraction'); mq.keystroke('Up'); assert.equal(cursor.parent, numer, 'cursor up goes into numerator'); - assert.equal(cursor[R], undefined, 'cursor up from right of fraction inserts at right end of numerator'); + assert.equal(cursor.right, undefined, 'cursor up from right of fraction inserts at right end of numerator'); mq.keystroke('Down'); assert.equal(cursor.parent, denom, 'cursor down goes into denominator'); - assert.equal(cursor[R], undefined, 'cursor down from numerator inserts at right end of denominator'); + assert.equal(cursor.right, undefined, 'cursor down from numerator inserts at right end of denominator'); mq.keystroke('Up'); assert.equal(cursor.parent, numer, 'cursor up goes into numerator'); - assert.equal(cursor[R], undefined, 'cursor up from denominator inserts at right end of numerator'); + assert.equal(cursor.right, undefined, 'cursor up from denominator inserts at right end of numerator'); mq.keystroke('Left Left Left'); assert.equal(cursor.parent, rootBlock, 'cursor outside fraction'); - assert.equal(cursor[R], frac, 'cursor before fraction'); + assert.equal(cursor.right, frac, 'cursor before fraction'); mq.keystroke('Up'); assert.equal(cursor.parent, numer, 'cursor up goes into numerator'); - assert.equal(cursor[L], undefined, 'cursor up from left of fraction inserts at left end of numerator'); + assert.equal(cursor.left, undefined, 'cursor up from left of fraction inserts at left end of numerator'); mq.keystroke('Left'); assert.equal(cursor.parent, rootBlock, 'cursor outside fraction'); - assert.equal(cursor[R], frac, 'cursor before fraction'); + assert.equal(cursor.right, frac, 'cursor before fraction'); mq.keystroke('Down'); assert.equal(cursor.parent, denom, 'cursor down goes into denominator'); - assert.equal(cursor[L], undefined, 'cursor down from left of fraction inserts at left end of denominator'); + assert.equal(cursor.left, undefined, 'cursor down from left of fraction inserts at left end of denominator'); }); test('nested subscripts and fractions', () => { controller.renderLatexMath('\\frac{d}{dx_{\\frac{24}{36}0}}\\sqrt{x}=x^{\\frac{1}{2}}'); - const exp = rootBlock.ends[R], - expBlock = exp.ends[L], - half = expBlock.ends[L], - halfDenom = half.ends[R]; + const exp = rootBlock.ends.right, + expBlock = exp.ends.left, + half = expBlock.ends.left, + halfDenom = half.ends.right; mq.keystroke('Left'); assert.equal(cursor.parent, expBlock, 'cursor left goes into exponent'); @@ -134,15 +132,15 @@ suite('up/down', () => { mq.keystroke('Down'); assert.equal(cursor.parent, rootBlock, 'down again puts cursor back in root block'); - assert.equal(cursor[L], exp, 'down from end of half puts cursor after exponent'); + assert.equal(cursor.left, exp, 'down from end of half puts cursor after exponent'); - const derivative = rootBlock.ends[L], - dxBlock = derivative.ends[R], - sub = dxBlock.ends[R], - subBlock = sub.ends[L], - subFrac = subBlock.ends[L], - subFracNumer = subFrac.ends[L], - subFracDenom = subFrac.ends[R]; + const derivative = rootBlock.ends.left, + dxBlock = derivative.ends.right, + sub = dxBlock.ends.right, + subBlock = sub.ends.left, + subFrac = subBlock.ends.left, + subFracNumer = subFrac.ends.left, + subFracDenom = subFrac.ends.right; cursor.insAtLeftEnd(rootBlock); mq.keystroke('Down Right Right Down'); @@ -157,7 +155,7 @@ suite('up/down', () => { mq.keystroke('Up'); assert.equal(cursor.parent, dxBlock, 'cursor up from subscript fraction numerator goes out of subscript'); - assert.equal(cursor[R], sub, 'cursor up from subscript fraction numerator goes before subscript'); + assert.equal(cursor.right, sub, 'cursor up from subscript fraction numerator goes before subscript'); mq.keystroke('Down Down'); assert.equal(cursor.parent, subFracDenom, 'cursor in subscript fraction denominator'); @@ -169,15 +167,15 @@ suite('up/down', () => { "cursor up up from subscript fraction denominator that's not at right end goes out of subscript" ); assert.equal( - cursor[R], + cursor.right, sub, "cursor up up from subscript fraction denominator that's not at right end goes before subscript" ); cursor.insAtRightEnd(subBlock); controller.backspace(); - assert.equal(subFrac[R], undefined, 'subscript fraction is at right end'); - assert.equal(cursor[L], subFrac, 'cursor after subscript fraction'); + assert.equal(subFrac.right, undefined, 'subscript fraction is at right end'); + assert.equal(cursor.left, subFrac, 'cursor after subscript fraction'); mq.keystroke('Down'); assert.equal(cursor.parent, subFracDenom, 'cursor in subscript fraction denominator'); @@ -189,7 +187,7 @@ suite('up/down', () => { 'cursor up up from subscript fraction denominator that is at right end goes out of subscript' ); assert.equal( - cursor[L], + cursor.left, sub, 'cursor up up from subscript fraction denominator that is at right end goes after subscript' ); From de7fe53ba00de29db6735e883cdb98645ac1e12e Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 3 Nov 2024 14:22:26 -0600 Subject: [PATCH 07/19] Add the eslint mocha plugin for better linting of the unit tests. --- eslint.config.mjs | 4 + package-lock.json | 84 ++++++++++ package.json | 1 + test/MathFunction.test.js | 80 ++++----- test/SupSub.test.js | 20 +-- test/autoOperatorNames.test.js | 28 ++-- test/autosubscript.test.js | 22 +-- test/backspace.test.js | 30 ++-- test/css.test.js | 18 +-- test/focusBlur.test.js | 16 +- test/html.test.js | 8 +- test/latex.test.js | 124 +++++++------- test/parser.test.js | 58 +++---- test/publicapi.test.js | 190 +++++++++++----------- test/saneKeyboardEvents.test.js | 58 +++---- test/select.test.js | 18 +-- test/text-output.test.js | 12 +- test/text.test.js | 26 +-- test/tree.test.js | 34 ++-- test/typing.test.js | 278 ++++++++++++++++---------------- test/updown.test.js | 16 +- 21 files changed, 623 insertions(+), 502 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 450b8b40..5b9d905e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,6 +3,7 @@ import pluginJs from '@eslint/js'; import tseslint from 'typescript-eslint'; import eslintConfigPrettier from 'eslint-config-prettier'; import stylistic from '@stylistic/eslint-plugin'; +import mochaPlugin from 'eslint-plugin-mocha'; export default [ { @@ -30,6 +31,7 @@ export default [ languageOptions: { parserOptions: { project: false, program: null, projectService: false } }, rules: { ...tseslint.configs.disableTypeChecked.rules, '@typescript-eslint/no-require-imports': 'off' } }, + { files: ['**/test/*.test.js'], ...mochaPlugin.configs.flat.recommended }, eslintConfigPrettier, { plugins: { '@stylistic': stylistic }, @@ -61,6 +63,8 @@ export default [ '@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }], '@typescript-eslint/prefer-nullish-coalescing': 'off', + 'mocha/no-setup-in-describe': 'off', + // Allow console and debugger during development only. 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' diff --git a/package-lock.json b/package-lock.json index 882371c1..ddac4f3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "css-minimizer-webpack-plugin": "^7.0.0", "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-mocha": "^10.5.0", "eslint-webpack-plugin": "^4.2.0", "globals": "^15.9.0", "less": "^4.2.0", @@ -5643,6 +5644,40 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-mocha": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", + "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^3.0.0", + "globals": "^13.24.0", + "rambda": "^7.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", @@ -5660,6 +5695,35 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", @@ -8883,6 +8947,13 @@ ], "license": "MIT" }, + "node_modules/rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", + "dev": true, + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10649,6 +10720,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index ed9317f7..ce69d075 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "css-minimizer-webpack-plugin": "^7.0.0", "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-mocha": "^10.5.0", "eslint-webpack-plugin": "^4.2.0", "globals": "^15.9.0", "less": "^4.2.0", diff --git a/test/MathFunction.test.js b/test/MathFunction.test.js index 7b86f9e7..2fb9ceb0 100644 --- a/test/MathFunction.test.js +++ b/test/MathFunction.test.js @@ -1,17 +1,17 @@ -/* global suite, test, assert, setup, teardown, MQ */ +/* global assert, MQ */ import { VNode } from 'src/tree/vNode'; import { MathFunction, latexMathParser } from 'commands/mathElements'; -suite('MathFunction', () => { +suite('MathFunction', function () { let mq; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); mq.options.addAutoCommands(['sin', 'log']); }); - teardown(() => { + teardown(function () { mq.el().remove(); }); @@ -26,8 +26,8 @@ suite('MathFunction', () => { assert.equal(result, latex, `parsing '${str}', got '${result}', expected '${latex}'`); }; - suite('parsing', () => { - test('general', () => { + suite('parsing', function () { + test('general', function () { assertParsesLatex('\\sin', '\\sin\\left(\\right)'); assertParsesLatex('\\sin x', '\\sin\\left(x\\right)'); assertParsesLatex('(\\sin)', '(\\sin\\left(\\right))'); @@ -56,7 +56,7 @@ suite('MathFunction', () => { assertParsesLatex('\\arcsin\\left(x\\right)', '\\arcsin\\left(x\\right)'); }); - test('with whitespace', () => { + test('with whitespace', function () { assertParsesLatex(' \\sin x ', '\\sin\\left(x\\right)'); assertParsesLatex('\\sin x + 3 ', '\\sin\\left(x\\right)+3'); assertParsesLatex(' \\sin x ^2 + 3', '\\sin\\left(x\\right)^2+3'); @@ -66,8 +66,8 @@ suite('MathFunction', () => { }); }); - suite('latex rendering', () => { - test('render with latex multiple sup subs', () => { + suite('latex rendering', function () { + test('render with latex multiple sup subs', function () { mq.latex('\\sin_2^3_4^5\\left(x\\right)'); assert.equal(mq.latex(), '\\sin_{24}^{35}\\left(x\\right)'); @@ -81,7 +81,7 @@ suite('MathFunction', () => { assert.equal(mq.latex(), '\\sin_4^2\\left(x\\right)'); }); - test('basic latex output', () => { + test('basic latex output', function () { const tree = latexMathParser.parse('\\sin_2^3\\left(x^2+3\\right)').postOrder('finalizeTree', mq.options); assert.ok(tree.ends.left instanceof MathFunction); @@ -93,8 +93,8 @@ suite('MathFunction', () => { }); }); - suite('deleting subscript and superscript', () => { - test('backspacing out of and then re-typing subscript', () => { + suite('deleting subscript and superscript', function () { + test('backspacing out of and then re-typing subscript', function () { mq.latex('\\sin_a^b'); assert.equal(mq.latex(), '\\sin_a^b\\left(\\right)'); @@ -114,7 +114,7 @@ suite('MathFunction', () => { assert.equal(mq.latex(), '\\sin_a^b\\left(c\\right)'); }); - test('backspacing out of and then re-typing superscript', () => { + test('backspacing out of and then re-typing superscript', function () { mq.latex('\\sin_a^b'); assert.equal(mq.latex(), '\\sin_a^b\\left(\\right)'); @@ -135,8 +135,8 @@ suite('MathFunction', () => { }); }); - suite('extending, shortening, and deleting function name', () => { - test('sin to sinh to sin', () => { + suite('extending, shortening, and deleting function name', function () { + test('sin to sinh to sin', function () { mq.typedText('sin'); assert.equal(mq.latex(), '\\sin\\left(\\right)'); @@ -154,7 +154,7 @@ suite('MathFunction', () => { assert.equal(mq.latex(), '\\sinh\\left(x\\right)'); }); - test('deleting function name with empty contents', () => { + test('deleting function name with empty contents', function () { mq.typedText('sin'); assert.equal(mq.latex(), '\\sin\\left(\\right)'); @@ -168,7 +168,7 @@ suite('MathFunction', () => { assert.equal(mq.latex(), ''); }); - test('deleting function name with contents', () => { + test('deleting function name with contents', function () { mq.typedText('sinx'); assert.equal(mq.latex(), '\\sin\\left(x\\right)'); @@ -183,8 +183,8 @@ suite('MathFunction', () => { }); }); - suite('typing in supsub block', () => { - test('typing underscore starts subscript', () => { + suite('typing in supsub block', function () { + test('typing underscore starts subscript', function () { const cursor = mq.__controller.cursor; mq.typedText('sin'); @@ -202,7 +202,7 @@ suite('MathFunction', () => { assert.equal(cursor.parent?.parent.blocks[0], cursor.parent); }); - test('typing caret start superscript', () => { + test('typing caret start superscript', function () { const cursor = mq.__controller.cursor; mq.typedText('sin'); @@ -220,7 +220,7 @@ suite('MathFunction', () => { assert.equal(cursor.parent?.parent.blocks[0], cursor.parent); }); - test('typing space or anything other than ^ or _ inserts into content block', () => { + test('typing space or anything other than ^ or _ inserts into content block', function () { const cursor = mq.__controller.cursor; mq.typedText('sin'); @@ -252,7 +252,7 @@ suite('MathFunction', () => { assert.equal(cursor.parent?.parent.blocks[1], cursor.parent); }); - test('typing start parenthesis moves to content block without adding additional parentheses', () => { + test('typing start parenthesis moves to content block without adding additional parentheses', function () { const cursor = mq.__controller.cursor; mq.typedText('sin'); @@ -265,10 +265,10 @@ suite('MathFunction', () => { }); }); - suite('behavior as a left bracket', () => { - suite('as initial left bracket', () => { + suite('behavior as a left bracket', function () { + suite('as initial left bracket', function () { let endBracket; - setup(() => { + setup(function () { mq.typedText('sin1+2'); assert.equal(mq.latex(), '\\sin\\left(1+2\\right)'); const fcn = mq.__controller.cursor.parent?.parent; @@ -278,7 +278,7 @@ suite('MathFunction', () => { assert.ok(endBracket.classList.contains('mq-ghost'), 'right parenthesis is ghost initially'); }); - test('typing outside ghost paren solidifies ghost', () => { + test('typing outside ghost paren solidifies ghost', function () { mq.keystroke('Right').typedText('+4'); assert.equal(mq.latex(), '\\sin\\left(1+2\\right)+4'); assert.ok( @@ -291,7 +291,7 @@ suite('MathFunction', () => { assert.ok(endBracket.classList.contains('mq-ghost'), 'right parenthesis is ghost again'); }); - test('close math function parentheses by typing end parenthesis', () => { + test('close math function parentheses by typing end parenthesis', function () { mq.typedText(')'); assert.equal(mq.latex(), '\\sin\\left(1+2\\right)'); assert.ok(!endBracket.classList.contains('mq-ghost')); @@ -305,7 +305,7 @@ suite('MathFunction', () => { assert.ok(!endBracket.classList.contains('mq-ghost')); }); - test('typing non-parenthesis bracket at end does not match and creates new "Bracket"', () => { + test('typing non-parenthesis bracket at end does not match and creates new "Bracket"', function () { mq.typedText(']'); assert.equal(mq.latex(), '\\sin\\left(\\left[1+2\\right]\\right)'); assert.ok(endBracket.classList.contains('mq-ghost')); @@ -319,13 +319,13 @@ suite('MathFunction', () => { assert.ok(!endBracket.classList.contains('mq-ghost')); }); - test('typing "^" at end solidifies function parenthesis', () => { + test('typing "^" at end solidifies function parenthesis', function () { mq.keystroke('Right').typedText('^'); assert.equal(mq.latex(), '\\sin\\left(1+2\\right)^{ }'); assert.ok(!endBracket.classList.contains('mq-ghost')); }); - test('existing content to right is adopted', () => { + test('existing content to right is adopted', function () { mq.keystroke('Backspace Backspace Backspace Backspace Backspace'); mq.typedText('x^2').keystroke('Tab').typedText('+3x-4'); assert.equal(mq.latex(), 'x^2+3x-4'); @@ -336,29 +336,29 @@ suite('MathFunction', () => { }); }); - suite('adding to left of parentheses', () => { - test('typing left of right solid parentheses with left ghost', () => { + suite('adding to left of parentheses', function () { + test('typing left of right solid parentheses with left ghost', function () { mq.typedText('x+3)'); assert.equal(mq.latex(), '\\left(x+3\\right)'); mq.keystroke('Left').typedText('sin'); assert.equal(mq.latex(), 'x+3\\sin\\left(\\right)'); }); - test('typing amid content of right solid parentheses with left ghost', () => { + test('typing amid content of right solid parentheses with left ghost', function () { mq.typedText('x+3)'); assert.equal(mq.latex(), '\\left(x+3\\right)'); mq.keystroke('Left Left').typedText('sin'); assert.equal(mq.latex(), 'x+\\sin\\left(3\\right)'); }); - test('typing right of left ghost parenthesis with right solid parentheses', () => { + test('typing right of left ghost parenthesis with right solid parentheses', function () { mq.typedText('x+3)'); assert.equal(mq.latex(), '\\left(x+3\\right)'); mq.keystroke('Left Left Left Left').typedText('sin'); assert.equal(mq.latex(), '\\sin\\left(x+3\\right)'); }); - test('typing left of parentheses', () => { + test('typing left of parentheses', function () { mq.typedText('x+3)'); assert.equal(mq.latex(), '\\left(x+3\\right)'); mq.keystroke('Left Left Left Left Left').typedText('sin'); @@ -367,8 +367,8 @@ suite('MathFunction', () => { }); }); - suite('text output', () => { - test('function without supsubs', () => { + suite('text output', function () { + test('function without supsubs', function () { mq.typedText('sin'); assert.equal(mq.text(), 'sin()'); @@ -379,7 +379,7 @@ suite('MathFunction', () => { assert.equal(mq.text(), 'sin(x+3)'); }); - test('function with supsubs', () => { + test('function with supsubs', function () { mq.typedText('sin'); assert.equal(mq.text(), 'sin()'); @@ -393,7 +393,7 @@ suite('MathFunction', () => { assert.equal(mq.text(), 'sin_5^3(x)'); }); - test('extended function name', () => { + test('extended function name', function () { mq.typedText('sin'); assert.equal(mq.text(), 'sin()'); @@ -410,7 +410,7 @@ suite('MathFunction', () => { assert.equal(mq.text(), 'sin^5(x)'); }); - test('log with base', () => { + test('log with base', function () { mq.typedText('log'); assert.equal(mq.text(), 'log()'); diff --git a/test/SupSub.test.js b/test/SupSub.test.js index e99e4c70..5d0360bb 100644 --- a/test/SupSub.test.js +++ b/test/SupSub.test.js @@ -1,10 +1,10 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { noop, prayWellFormed } from 'src/constants'; -suite('SupSub', () => { +suite('SupSub', function () { let mq; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); @@ -44,7 +44,7 @@ suite('SupSub', () => { const expected = expecteds[i].split('; ')[j].split(', ')[k].split(' ')[l]; const expectedAfterC = expectedsAfterC[i].split('; ')[j].split(', ')[k].split(' ')[l]; - test(`initial ${initSupsub}script then ${did} ${supsub}script ${side}`, () => { + test(`initial ${initSupsub}script then ${did} ${supsub}script ${side}`, function () { mq.latex(initialLatex); assert.equal(mq.latex(), initialLatex); @@ -82,7 +82,7 @@ suite('SupSub', () => { const expected = expecteds.split('; ')[i].split(', ')[j].split(' ')[k]; const expectedAfterC = expectedsAfterC.split('; ')[i].split(', ')[j].split(' ')[k]; - test(`initial ${initSupsub}script then ${did} '³' ${side}`, () => { + test(`initial ${initSupsub}script then ${did} '³' ${side}`, function () { mq.latex(initialLatex); assert.equal(mq.latex(), initialLatex); @@ -100,7 +100,7 @@ suite('SupSub', () => { }); }); - test("render LaTeX with 2 SupSub's in a row", () => { + test("render LaTeX with 2 SupSub's in a row", function () { mq.latex('x_a_b'); assert.equal(mq.latex(), 'x_{ab}'); @@ -120,7 +120,7 @@ suite('SupSub', () => { assert.equal(mq.latex(), 'x^a'); }); - test("render LaTeX with 3 alternating SupSub's in a row", () => { + test("render LaTeX with 3 alternating SupSub's in a row", function () { mq.latex('x_a^b_c'); assert.equal(mq.latex(), 'x_{ac}^b'); @@ -128,8 +128,8 @@ suite('SupSub', () => { assert.equal(mq.latex(), 'x_b^{ac}'); }); - suite('deleting', () => { - test('backspacing out of and then re-typing subscript', () => { + suite('deleting', function () { + test('backspacing out of and then re-typing subscript', function () { mq.latex('x_a^b'); assert.equal(mq.latex(), 'x_a^b'); @@ -148,7 +148,7 @@ suite('SupSub', () => { mq.typedText('c'); assert.equal(mq.latex(), 'xca^b'); }); - test('backspacing out of and then re-typing superscript', () => { + test('backspacing out of and then re-typing superscript', function () { mq.latex('x_a^b'); assert.equal(mq.latex(), 'x_a^b'); diff --git a/test/autoOperatorNames.test.js b/test/autoOperatorNames.test.js index 83f30f78..71d48711 100644 --- a/test/autoOperatorNames.test.js +++ b/test/autoOperatorNames.test.js @@ -1,10 +1,10 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { Letter } from 'commands/mathElements'; -suite('autoOperatorNames', () => { +suite('autoOperatorNames', function () { let mq; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); @@ -20,7 +20,7 @@ suite('autoOperatorNames', () => { assert.equal(result, expected, `${input}, got '${result}', expected '${expected}'`); }; - test('simple LaTeX parsing, typing', () => { + test('simple LaTeX parsing, typing', function () { const assertAutoOperatorNamesWork = (str, latex) => { let count = 0; const _autoUnItalicize = Letter.prototype.autoUnItalicize; @@ -52,7 +52,7 @@ suite('autoOperatorNames', () => { assertAutoOperatorNamesWork('skerskersker', 's\\ker s\\ker s\\ker'); }); - test('text() output', () => { + test('text() output', function () { const assertTranslatedCorrectly = (latexStr, text) => { mq.latex(latexStr); assertText(`outputting ${latexStr}`, text); @@ -62,7 +62,7 @@ suite('autoOperatorNames', () => { assertTranslatedCorrectly('\\ker\\left(xy\\right)', 'ker(xy)'); }); - test('deleting', () => { + test('deleting', function () { let count = 0; const _autoUnItalicize = Letter.prototype.autoUnItalicize; Letter.prototype.autoUnItalicize = function () { @@ -99,31 +99,31 @@ suite('autoOperatorNames', () => { mq.options.removeAutoOperatorNames('cac'); }); - suite('override autoOperatorNames', () => { - test('basic', () => { + suite('override autoOperatorNames', function () { + test('basic', function () { mq.config({ autoOperatorNames: 'ker lol' }); mq.typedText('arckertrololol'); assert.equal(mq.latex(), 'arc\\ker tro\\operatorname{lol}ol'); }); - test('command contains non-letters', () => { + test('command contains non-letters', function () { assert.throws(() => MQ.config({ autoOperatorNames: 'e1' })); }); - test('command length less than 2', () => { + test('command length less than 2', function () { assert.throws(() => MQ.config({ autoOperatorNames: 'e' })); }); - suite('command list not perfectly space-delimited is okay', () => { - test('double space', () => { + suite('command list not perfectly space-delimited is okay', function () { + test('double space', function () { assert.ok(() => MQ.config({ autoOperatorNames: 'pi theta' })); }); - test('leading space', () => { + test('leading space', function () { assert.ok(() => MQ.config({ autoOperatorNames: ' pi' })); }); - test('trailing space', () => { + test('trailing space', function () { assert.ok(() => MQ.config({ autoOperatorNames: 'pi ' })); }); }); diff --git a/test/autosubscript.test.js b/test/autosubscript.test.js index c991f9c9..8549876b 100644 --- a/test/autosubscript.test.js +++ b/test/autosubscript.test.js @@ -1,8 +1,8 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ -suite('autoSubscript', () => { +suite('autoSubscript', function () { let mq, rootBlock, controller, cursor; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field, { autoSubscriptNumerals: true }); @@ -11,7 +11,7 @@ suite('autoSubscript', () => { cursor = controller.cursor; }); - test('auto subscripting variables', () => { + test('auto subscripting variables', function () { mq.latex('x'); mq.typedText('2'); assert.equal(mq.latex(), 'x_2'); @@ -19,7 +19,7 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), 'x_{23}'); }); - test('do not autosubscript operator name', () => { + test('do not autosubscript operator name', function () { mq.latex('ker'); mq.typedText('2'); assert.equal(mq.latex(), '\\ker2'); @@ -27,7 +27,7 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), '\\ker23'); }); - test('autosubscript exponentiated variables', () => { + test('autosubscript exponentiated variables', function () { mq.latex('x^2'); mq.typedText('2'); assert.equal(mq.latex(), 'x_2^2'); @@ -35,7 +35,7 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), 'x_{23}^2'); }); - test('do not autosubscript exponentiated operator name', () => { + test('do not autosubscript exponentiated operator name', function () { mq.latex('ker^{2}'); mq.typedText('2'); assert.equal(mq.latex(), '\\ker^22'); @@ -43,13 +43,13 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), '\\ker^223'); }); - test('do not autosubscript subscripted operator name', () => { + test('do not autosubscript subscripted operator name', function () { mq.latex('ker_{10}'); mq.typedText('2'); assert.equal(mq.latex(), '\\ker_{10}2'); }); - test('backspace through compound subscript', () => { + test('backspace through compound subscript', function () { mq.latex('x_{2_2}'); //first backspace moves to cursor in subscript and peels it off @@ -65,7 +65,7 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), 'x'); }); - test('backspace through simple subscript', () => { + test('backspace through simple subscript', function () { mq.latex('x_{2+3}'); assert.equal(cursor.parent, rootBlock, 'start in the root block'); @@ -83,7 +83,7 @@ suite('autoSubscript', () => { assert.equal(mq.latex(), 'x'); }); - test('backspace through subscript & superscript with autosubscripting on', () => { + test('backspace through subscript & superscript with autosubscripting on', function () { mq.latex('x_2^{32}'); //first backspace peels off the subscript diff --git a/test/backspace.test.js b/test/backspace.test.js index 3917d363..25fa9ef3 100644 --- a/test/backspace.test.js +++ b/test/backspace.test.js @@ -1,10 +1,10 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { prayWellFormed } from 'src/constants'; -suite('backspace', () => { +suite('backspace', function () { let mq, rootBlock, controller, cursor; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); @@ -19,7 +19,7 @@ suite('backspace', () => { assert.equal(mq.latex(), latex); }; - test('backspace through exponent', () => { + test('backspace through exponent', function () { controller.renderLatexMath('x^{nm}'); const exp = rootBlock.ends.right, expBlock = exp.ends.left; @@ -44,7 +44,7 @@ suite('backspace', () => { assertLatex('x'); }); - test('backspace through complex fraction', () => { + test('backspace through complex fraction', function () { controller.renderLatexMath('1+\\frac{1}{\\frac{1}{2}+\\frac{2}{3}}'); //first backspace moves to denominator @@ -81,7 +81,7 @@ suite('backspace', () => { assertLatex('1+1'); }); - test('backspace through compound subscript', () => { + test('backspace through compound subscript', function () { mq.latex('x_{2_2}'); //first backspace goes into the subscript @@ -105,7 +105,7 @@ suite('backspace', () => { assert.equal(mq.latex(), 'x'); }); - test('backspace through simple subscript', () => { + test('backspace through simple subscript', function () { mq.latex('x_{2+3}'); assert.equal(cursor.parent, rootBlock, 'start in the root block'); @@ -123,7 +123,7 @@ suite('backspace', () => { assert.equal(mq.latex(), 'x'); }); - test('backspace through subscript & superscript', () => { + test('backspace through subscript & superscript', function () { mq.latex('x_2^{32}'); //first backspace takes us into the exponent @@ -159,7 +159,7 @@ suite('backspace', () => { assert.equal(mq.latex(), ''); }); - test('backspace through nthroot', () => { + test('backspace through nthroot', function () { mq.latex('\\sqrt[3]{x}'); //first backspace takes us inside the nthroot @@ -178,7 +178,7 @@ suite('backspace', () => { assert.equal(mq.latex(), ''); }); - test('backspace through large operator', () => { + test('backspace through large operator', function () { mq.latex('\\sum_{n=1}^3x'); //first backspace takes out the argument @@ -198,7 +198,7 @@ suite('backspace', () => { assert.equal(mq.latex(), 'n=1'); }); - test('backspace through text block', () => { + test('backspace through text block', function () { mq.latex('\\text{x}'); mq.keystroke('Backspace'); @@ -221,20 +221,20 @@ suite('backspace', () => { assert.equal(mq.latex(), ''); }); - suite('empties', () => { - test('backspace empty exponent', () => { + suite('empties', function () { + test('backspace empty exponent', function () { mq.latex('x^{}'); mq.keystroke('Backspace'); assert.equal(mq.latex(), 'x'); }); - test('backspace empty sqrt', () => { + test('backspace empty sqrt', function () { mq.latex('1+\\sqrt{}'); mq.keystroke('Backspace'); assert.equal(mq.latex(), '1+'); }); - test('backspace empty fraction', () => { + test('backspace empty fraction', function () { mq.latex('1+\\frac{}{}'); mq.keystroke('Backspace'); assert.equal(mq.latex(), '1+'); diff --git a/test/css.test.js b/test/css.test.js index 0d2e4e93..e96542b5 100644 --- a/test/css.test.js +++ b/test/css.test.js @@ -1,7 +1,7 @@ -/* global suite, test, assert, MQ */ +/* global assert, MQ */ -suite('CSS', () => { - test("math field doesn't affect ancestor's .scrollWidth", () => { +suite('CSS', function () { + test("math field doesn't affect ancestor's .scrollWidth", function () { const container = document.createElement('div'); container.style.fontSize = '16px'; container.style.height = '25px'; // must be greater than font-size * 115% + 2 * 2px (padding) + 2 * 1px (border) @@ -29,7 +29,7 @@ suite('CSS', () => { ); }; - test('empty root block does not collapse', () => { + test('empty root block does not collapse', function () { const testEl = document.createElement('span'); document.getElementById('mock')?.append(testEl); @@ -40,7 +40,7 @@ suite('CSS', () => { assert.ok(getHeight(rootEl) > 0, 'Empty root block height should be above 0.'); }); - test('empty block does not collapse', () => { + test('empty block does not collapse', function () { const testEl = document.createElement('span'); testEl.textContent = '\\frac{}{}'; document.getElementById('mock')?.append(testEl); @@ -51,7 +51,7 @@ suite('CSS', () => { assert.ok(getHeight(numeratorEl) > 0, 'Empty numerator height should be above 0.'); }); - test('test florin spacing', () => { + test('test florin spacing', function () { const span = document.createElement('span'); document.getElementById('mock')?.append(span); @@ -66,7 +66,7 @@ suite('CSS', () => { ); }); - test('unary PlusMinus before separator', () => { + test('unary PlusMinus before separator', function () { const span = document.createElement('span'); document.getElementById('mock')?.append(span); const mq = MQ.MathField(span); @@ -93,7 +93,7 @@ suite('CSS', () => { assertBinaryOperator(33, '(-1,-1-1)-1,(+1;+1+1)+1,(\\pm1,\\pm1\\pm1)\\pm'); }); - test('proper unary/binary within style block', () => { + test('proper unary/binary within style block', function () { const span = document.createElement('span'); document.getElementById('mock')?.append(span); const mq = MQ.MathField(span); @@ -125,7 +125,7 @@ suite('CSS', () => { assertBinaryOperator(6, '\\textcolor{red}{\\class{dummy}{-}}2\\textcolor{green}{\\class{dummy}{+}}'); }); - test('operator name spacing, e.g., ker x', () => { + test('operator name spacing, e.g., ker x', function () { const span = document.createElement('span'); document.getElementById('mock')?.append(span); const mq = MQ.MathField(span); diff --git a/test/focusBlur.test.js b/test/focusBlur.test.js index a2938860..ce8beab2 100644 --- a/test/focusBlur.test.js +++ b/test/focusBlur.test.js @@ -1,15 +1,15 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ -suite('focusBlur', () => { +suite('focusBlur', function () { const assertHasFocus = (mq, name, invert) => assert.ok( !!invert ^ (mq.el().querySelector('textarea') === document.activeElement), name + (invert ? ' does not have focus' : ' has focus') ); - suite('handlers can shift focus away', () => { + suite('handlers can shift focus away', function () { let mq, mq2, wasUpOutOfCalled; - setup(() => { + setup(function () { const mock = document.getElementById('mock'); const span = document.createElement('span'); mock?.append(span); @@ -36,7 +36,7 @@ suite('focusBlur', () => { assert.ok(wasUpOutOfCalled); }; - test('normally', () => { + test('normally', function () { mq.focus(); assertHasFocus(mq, 'mq'); @@ -44,7 +44,7 @@ suite('focusBlur', () => { assertHasFocus(mq2, 'mq2'); }); - test("even if there's a selection", (done) => { + test("even if there's a selection", function (done) { mq.focus(); assertHasFocus(mq, 'mq'); @@ -62,7 +62,7 @@ suite('focusBlur', () => { }); }); - test('select behaves normally after blurring and re-focusing', (done) => { + test('select behaves normally after blurring and re-focusing', function (done) { const span = document.createElement('span'); document.getElementById('mock')?.append(span); const mq = MQ.MathField(span); @@ -94,7 +94,7 @@ suite('focusBlur', () => { }); }); - test('blur event fired when math field loses focus', (done) => { + test('blur event fired when math field loses focus', function (done) { const mock = document.getElementById('mock'); const span = document.createElement('span'); mock?.append(span); diff --git a/test/html.test.js b/test/html.test.js index 9c2a191a..2755f155 100644 --- a/test/html.test.js +++ b/test/html.test.js @@ -1,9 +1,9 @@ -/* global suite, test, assert */ +/* global assert */ import { mqCmdId, mqBlockId } from 'src/constants'; import { MathCommand } from 'commands/mathElements'; -suite('HTML', () => { +suite('HTML', function () { const renderHtml = (numBlocks, htmlTemplate) => { const cmd = { id: 1, @@ -22,7 +22,7 @@ suite('HTML', () => { return MathCommand.prototype.html.call(cmd); }; - test('simple HTML templates', () => { + test('simple HTML templates', function () { let htmlTemplate = 'A Symbol'; let html = `A Symbol`; @@ -43,7 +43,7 @@ suite('HTML', () => { assert.equal(html, renderHtml(2, htmlTemplate), 'container span with two block spans'); }); - test('context-free HTML templates', () => { + test('context-free HTML templates', function () { let htmlTemplate = '
'; let html = `
`; diff --git a/test/latex.test.js b/test/latex.test.js index f695975e..8d249fca 100644 --- a/test/latex.test.js +++ b/test/latex.test.js @@ -1,9 +1,9 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { Options } from 'src/options'; import { Bracket, latexMathParser } from 'commands/mathElements'; -suite('latex', () => { +suite('latex', function () { const options = new Options(); const assertParsesLatex = (str, latex) => { @@ -13,18 +13,22 @@ suite('latex', () => { assert.equal(result, latex, `parsing '${str}', got '${result}', expected '${latex}'`); }; - test('empty LaTeX', () => { + test('empty LaTeX', function () { assertParsesLatex(''); assertParsesLatex(' ', ''); assertParsesLatex('{}', ''); assertParsesLatex(' {}{} {{{}} }', ''); }); - test('variables', () => assertParsesLatex('xyz')); + test('variables', function () { + assertParsesLatex('xyz'); + }); - test('variables that can be mathbb', () => assertParsesLatex('PNZQRCH')); + test('variables that can be mathbb', function () { + assertParsesLatex('PNZQRCH'); + }); - test('can parse mathbb symbols', () => { + test('can parse mathbb symbols', function () { assertParsesLatex( '\\P\\N\\Z\\Q\\R\\C\\H', '\\mathbb{P}\\mathbb{N}\\mathbb{Z}\\mathbb{Q}\\mathbb{R}\\mathbb{C}\\mathbb{H}' @@ -32,27 +36,31 @@ suite('latex', () => { assertParsesLatex('\\mathbb{P}\\mathbb{N}\\mathbb{Z}\\mathbb{Q}\\mathbb{R}\\mathbb{C}\\mathbb{H}'); }); - test('can parse mathbb error case', () => { + test('can parse mathbb error case', function () { assert.throws(() => assertParsesLatex('\\mathbb + 2')); assert.throws(() => assertParsesLatex('\\mathbb{A}')); }); - test('simple exponent', () => assertParsesLatex('x^n')); + test('simple exponent', function () { + assertParsesLatex('x^n'); + }); - test('block exponent', () => { + test('block exponent', function () { assertParsesLatex('x^{n}', 'x^n'); assertParsesLatex('x^{nm}'); assertParsesLatex('x^{}', 'x^{ }'); }); - test('nested exponents', () => assertParsesLatex('x^{n^m}')); + test('nested exponents', function () { + assertParsesLatex('x^{n^m}'); + }); - test('exponents with spaces', () => { + test('exponents with spaces', function () { assertParsesLatex('x^ 2', 'x^2'); assertParsesLatex('x ^2', 'x^2'); }); - test('inner groups', () => { + test('inner groups', function () { assertParsesLatex('a{bc}d', 'abcd'); assertParsesLatex('{bc}d', 'bcd'); assertParsesLatex('a{bc}', 'abc'); @@ -66,7 +74,7 @@ suite('latex', () => { assertParsesLatex('{asdf{asdf{asdf}asdf}asdf}', 'asdfasdfasdfasdfasdf'); }); - test('commands without braces', () => { + test('commands without braces', function () { assertParsesLatex('\\frac12', '\\frac{1}{2}'); assertParsesLatex('\\frac1a', '\\frac{1}{a}'); assertParsesLatex('\\frac ab', '\\frac{a}{b}'); @@ -79,13 +87,13 @@ suite('latex', () => { assert.throws(() => latexMathParser.parse('\\frac')); }); - test('whitespace', () => { + test('whitespace', function () { assertParsesLatex(' a + b ', 'a+b'); assertParsesLatex(' ', ''); assertParsesLatex('', ''); }); - test('parens', () => { + test('parens', function () { const tree = latexMathParser.parse('\\left(123\\right)'); assert.ok(tree.ends.left instanceof Bracket); @@ -94,7 +102,7 @@ suite('latex', () => { assert.equal(tree.join('latex'), '\\left(123\\right)'); }); - test('\\langle/\\rangle (issue #508)', () => { + test('\\langle/\\rangle (issue #508)', function () { const tree = latexMathParser.parse('\\left\\langle 123\\right\\rangle)'); assert.ok(tree.ends.left instanceof Bracket); @@ -103,7 +111,7 @@ suite('latex', () => { assert.equal(tree.join('latex'), '\\left\\langle 123\\right\\rangle )'); }); - test('\\langle/\\rangle (without whitespace)', () => { + test('\\langle/\\rangle (without whitespace)', function () { const tree = latexMathParser.parse('\\left\\langle123\\right\\rangle)'); assert.ok(tree.ends.left instanceof Bracket); @@ -112,7 +120,7 @@ suite('latex', () => { assert.equal(tree.join('latex'), '\\left\\langle 123\\right\\rangle )'); }); - test('\\lVert/\\rVert', () => { + test('\\lVert/\\rVert', function () { const tree = latexMathParser.parse('\\left\\lVert 123\\right\\rVert)'); assert.ok(tree.ends.left instanceof Bracket); @@ -121,7 +129,7 @@ suite('latex', () => { assert.equal(tree.join('latex'), '\\left\\lVert 123\\right\\rVert )'); }); - test('\\lVert/\\rVert (without whitespace)', () => { + test('\\lVert/\\rVert (without whitespace)', function () { const tree = latexMathParser.parse('\\left\\lVert123\\right\\rVert)'); assert.ok(tree.ends.left instanceof Bracket); @@ -130,66 +138,70 @@ suite('latex', () => { assert.equal(tree.join('latex'), '\\left\\lVert 123\\right\\rVert )'); }); - test('\\langler should not parse', () => { + test('\\langler should not parse', function () { assert.throws(() => latexMathParser.parse('\\left\\langler123\\right\\rangler')); }); - test('\\lVerte should not parse', () => { + test('\\lVerte should not parse', function () { assert.throws(() => latexMathParser.parse('\\left\\lVerte123\\right\\rVerte')); }); - test('parens with whitespace', () => assertParsesLatex('\\left ( 123 \\right ) ', '\\left(123\\right)')); + test('parens with whitespace', function () { + assertParsesLatex('\\left ( 123 \\right ) ', '\\left(123\\right)'); + }); - test('escaped whitespace', () => { + test('escaped whitespace', function () { assertParsesLatex('\\ ', '\\ '); assertParsesLatex('\\ ', '\\ '); assertParsesLatex(' \\ \\\t\t\t\\ \\\n\n\n', '\\ \\ \\ \\ '); assertParsesLatex('\\space\\ \\ space ', '\\ \\ \\ space'); }); - test('\\text', () => { + test('\\text', function () { assertParsesLatex('\\text { lol! } ', '\\text{ lol! }'); assertParsesLatex('\\text{apples} \\ne \\text{oranges}', '\\text{apples}\\ne \\text{oranges}'); assertParsesLatex('\\text{}', ''); }); - test('\\textcolor', () => assertParsesLatex('\\textcolor{blue}{8}', '\\textcolor{blue}{8}')); + test('\\textcolor', function () { + assertParsesLatex('\\textcolor{blue}{8}', '\\textcolor{blue}{8}'); + }); - test('\\class', () => { + test('\\class', function () { assertParsesLatex('\\class{name}{8}', '\\class{name}{8}'); assertParsesLatex('\\class{name}{8-4}', '\\class{name}{8-4}'); }); - test('not real LaTex commands, but valid symbols', () => { + test('not real LaTex commands, but valid symbols', function () { assertParsesLatex('\\parallelogram '); assertParsesLatex('\\circledot ', '\\odot '); assertParsesLatex('\\degree '); assertParsesLatex('\\square '); }); - suite('public API', () => { + suite('public API', function () { let mq; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); }); - suite('.latex(...)', () => { + suite('.latex(...)', function () { const assertParsesLatex = (str, latex) => { if (typeof latex === 'undefined') latex = str; mq.latex(str); assert.equal(mq.latex(), latex); }; - test('basic rendering', () => { + test('basic rendering', function () { assertParsesLatex( 'x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }', 'x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}' ); }); - test('re-rendering', () => { + test('re-rendering', function () { assertParsesLatex('a x^2 + b x + c = 0', 'ax^2+bx+c=0'); assertParsesLatex( 'x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }', @@ -197,14 +209,14 @@ suite('latex', () => { ); }); - test('empty LaTeX', () => { + test('empty LaTeX', function () { assertParsesLatex(''); assertParsesLatex(' ', ''); assertParsesLatex('{}', ''); assertParsesLatex(' {}{} {{{}} }', ''); }); - test('coerces to a string', () => { + test('coerces to a string', function () { assertParsesLatex(undefined, ''); assertParsesLatex(null, 'null'); assertParsesLatex(0, '0'); @@ -217,8 +229,8 @@ suite('latex', () => { }); }); - suite('.write(...)', () => { - test('empty LaTeX', () => { + suite('.write(...)', function () { + test('empty LaTeX', function () { const assertParsesLatex = (str, latex) => { if (typeof latex === 'undefined') latex = str; mq.write(str); @@ -230,7 +242,7 @@ suite('latex', () => { assertParsesLatex(' {}{} {{{}} }', ''); }); - test('overflow triggers automatic horizontal scroll', (done) => { + test('overflow triggers automatic horizontal scroll', function (done) { const mqEl = mq.el(); const rootEl = mq.__controller.root.elements.first; const cursor = mq.__controller.cursor; @@ -258,22 +270,22 @@ suite('latex', () => { }, 150); }); - suite('\\sum', () => { - test('basic', () => { + suite('\\sum', function () { + test('basic', function () { mq.write('\\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'); }); - test('only lower bound', () => { + 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'); }); - test('only upper bound', () => { + test('only upper bound', function () { mq.write('\\sum^5'); assert.equal(mq.latex(), '\\sum_{ }^5'); mq.write('x^n'); @@ -283,9 +295,9 @@ suite('latex', () => { }); }); - suite('\\MathQuillMathField', () => { + suite('\\MathQuillMathField', function () { let outer, inner1, inner2; - setup(() => { + setup(function () { const field = document.createElement('span'); field.textContent = '\\frac{\\MathQuillMathField{x_0 + x_1 + x_2}}{\\MathQuillMathField{3}}'; document.getElementById('mock')?.append(field); @@ -294,13 +306,13 @@ suite('latex', () => { inner2 = outer.innerFields[1]; }); - test('initial latex', () => { + test('initial latex', function () { 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}'); }); - test('setting latex', () => { + test('setting latex', function () { inner1.latex('\\sum_{i=0}^N x_i'); inner2.latex('N'); assert.equal(inner1.latex(), '\\sum_{i=0}^Nx_i'); @@ -308,7 +320,7 @@ suite('latex', () => { assert.equal(outer.latex(), '\\frac{\\sum_{i=0}^Nx_i}{N}'); }); - test('writing latex', () => { + test('writing latex', function () { inner1.write('+ x_3'); inner2.write('+ 1'); assert.equal(inner1.latex(), 'x_0+x_1+x_2+x_3'); @@ -316,7 +328,7 @@ suite('latex', () => { assert.equal(outer.latex(), '\\frac{x_0+x_1+x_2+x_3}{3+1}'); }); - test('optional inner field name', () => { + test('optional inner field name', function () { outer.latex( '\\MathQuillMathField[mantissa]{}\\cdot\\MathQuillMathField[base]{}^{\\MathQuillMathField[exp]{}}' ); @@ -336,7 +348,7 @@ suite('latex', () => { assert.equal(outer.latex(), '1.2345\\cdot10^8'); }); - test('make inner field static and then editable', () => { + test('make inner field static and then editable', function () { outer.latex('y=\\MathQuillMathField[m]{\\textcolor{blue}{m}}x+\\MathQuillMathField[b]{b}'); assert.equal(outer.innerFields.length, 2); // assert.equal(outer.innerFields.m.__controller.container, false); @@ -376,7 +388,7 @@ suite('latex', () => { assert.equal(outer.innerFields.get('b').__controller.editable, true); }); - test('separate API object', () => { + test('separate API object', function () { const outer2 = MQ(outer.el()); assert.equal(outer2.innerFields.length, 2); assert.equal(outer2.innerFields[0].id, inner1.id); @@ -384,16 +396,16 @@ suite('latex', () => { }); }); - suite('error handling', () => { + suite('error handling', function () { let mq; - setup(() => { + setup(function () { const field = document.createElement('span'); document.getElementById('mock')?.append(field); mq = MQ.MathField(field); }); const testCantParse = (title, ...args) => { - test(title, () => { + test(title, function () { for (const arg of args) { mq.latex(arg); assert.equal(mq.latex(), '', `shouldn't parse '${arg}'`); @@ -411,8 +423,8 @@ suite('latex', () => { ); }); - suite('selectable span', () => { - setup(() => { + suite('selectable span', function () { + setup(function () { const field = document.createElement('span'); field.innerHTML = '2<x'; document.getElementById('mock')?.append(field); @@ -421,6 +433,8 @@ suite('latex', () => { const selectableContent = () => document.querySelector('#mock .mq-selectable').textContent; - test('escapes < in textContent', () => assert.equal(selectableContent(), '$2 { +suite('parser', function () { const string = Parser.string; const regex = Parser.regex; const letter = Parser.letter; @@ -12,13 +12,13 @@ suite('parser', () => { const eof = Parser.eof; const all = Parser.all; - test('Parser.string', () => { + test('Parser.string', function () { const parser = string('x'); assert.equal(parser.parse('x'), 'x'); assert.throws(() => parser.parse('y')); }); - test('Parser.regex', () => { + test('Parser.regex', function () { const parser = regex(/^[0-9]/); assert.equal(parser.parse('1'), '1'); @@ -27,15 +27,15 @@ suite('parser', () => { assert.throws(() => regex(/./), 'must be anchored'); }); - suite('then', () => { - test('with a parser, uses the last return value', () => { + suite('then', function () { + test('with a parser, uses the last return value', function () { const parser = string('x').then(string('y')); assert.equal(parser.parse('xy'), 'y'); assert.throws(() => parser.parse('y')); assert.throws(() => parser.parse('xz')); }); - test('asserts that a parser is returned', () => { + test('asserts that a parser is returned', function () { const parser1 = letter.then(() => 'not a parser'); assert.throws(() => parser1.parse('x')); @@ -43,7 +43,7 @@ suite('parser', () => { assert.throws(() => parser2.parse('xx')); }); - test('with a function that returns a parser, continues with that parser', () => { + test('with a function that returns a parser, continues with that parser', function () { let piped; const parser = string('x').then((x) => { piped = x; @@ -56,8 +56,8 @@ suite('parser', () => { }); }); - suite('map', () => { - test('with a function, pipes the value in and uses that return value', () => { + suite('map', function () { + test('with a function, pipes the value in and uses that return value', function () { let piped; const parser = string('x').map((x) => { @@ -70,8 +70,8 @@ suite('parser', () => { }); }); - suite('result', () => { - test('returns a constant result', () => { + suite('result', function () { + test('returns a constant result', function () { const oneParser = string('x').result(1); assert.equal(oneParser.parse('x'), 1); @@ -85,8 +85,8 @@ suite('parser', () => { }); }); - suite('skip', () => { - test('uses the previous return value', () => { + suite('skip', function () { + test('uses the previous return value', function () { const parser = string('x').skip(string('y')); assert.equal(parser.parse('xy'), 'x'); @@ -94,8 +94,8 @@ suite('parser', () => { }); }); - suite('or', () => { - test('two parsers', () => { + suite('or', function () { + test('two parsers', function () { const parser = string('x').or(string('y')); assert.equal(parser.parse('x'), 'x'); @@ -103,7 +103,7 @@ suite('parser', () => { assert.throws(() => parser.parse('z')); }); - test('with then', () => { + test('with then', function () { const parser = string('\\') .then(() => string('y')) .or(string('z')); @@ -116,8 +116,8 @@ suite('parser', () => { const assertEqualArray = (arr1, arr2) => assert.equal(arr1.join(), arr2.join()); - suite('many', () => { - test('simple case', () => { + suite('many', function () { + test('simple case', function () { const letters = letter.many(); assertEqualArray(letters.parse('x'), ['x']); @@ -127,7 +127,7 @@ suite('parser', () => { assert.throws(() => letters.parse('xyz1')); }); - test('followed by then', () => { + test('followed by then', function () { const parser = string('x').many().then(string('y')); assert.equal(parser.parse('y'), 'y'); @@ -136,15 +136,15 @@ suite('parser', () => { }); }); - suite('times', () => { - test('zero case', () => { + suite('times', function () { + test('zero case', function () { const zeroLetters = letter.times(0); assertEqualArray(zeroLetters.parse(''), []); assert.throws(() => zeroLetters.parse('x')); }); - test('nonzero case', () => { + test('nonzero case', function () { const threeLetters = letter.times(3); assertEqualArray(threeLetters.parse('xyz'), ['x', 'y', 'z']); @@ -158,7 +158,7 @@ suite('parser', () => { assert.throws(() => thenDigit.parse('xyzw')); }); - test('with a min and max', () => { + test('with a min and max', function () { const someLetters = letter.times(2, 4); assertEqualArray(someLetters.parse('xy'), ['x', 'y']); @@ -177,7 +177,7 @@ suite('parser', () => { assert.throws(() => thenDigit.parse('x1')); }); - test('atLeast', () => { + test('atLeast', function () { const atLeastTwo = letter.atLeast(2); assertEqualArray(atLeastTwo.parse('xy'), ['x', 'y']); @@ -186,18 +186,18 @@ suite('parser', () => { }); }); - suite('fail', () => { + suite('fail', function () { const fail = Parser.fail; const succeed = Parser.succeed; - test('use Parser.fail to fail dynamically', () => { + test('use Parser.fail to fail dynamically', function () { const parser = any.then((ch) => fail(`character ${ch} not allowed`)).or(string('x')); assert.throws(() => parser.parse('y')); assert.equal(parser.parse('x'), 'x'); }); - test('use Parser.succeed or Parser.fail to branch conditionally', () => { + test('use Parser.succeed or Parser.fail to branch conditionally', function () { let allowedOperator; const parser = string('x') @@ -217,7 +217,7 @@ suite('parser', () => { }); }); - test('eof', () => { + test('eof', function () { const parser = optWhitespace.skip(eof).or(all.result('default')); assert.equal(parser.parse(' '), ' '); diff --git a/test/publicapi.test.js b/test/publicapi.test.js index 8d9014a5..9eae47a3 100644 --- a/test/publicapi.test.js +++ b/test/publicapi.test.js @@ -1,8 +1,8 @@ -/* global suite, test, assert, setup, teardown, MQ */ +/* global assert, MQ */ -suite('Public API', () => { - suite('global functions', () => { - test('undefined', () => { +suite('Public API', function () { + suite('global functions', function () { + test('undefined', function () { assert.equal(MQ(), undefined); assert.equal(MQ(0), undefined); assert.equal(MQ(''), undefined); @@ -12,14 +12,14 @@ suite('Public API', () => { assert.equal(MQ.MathField(''), undefined); }); - test('MQ.MathField()', () => { + test('MQ.MathField()', function () { const el = document.createElement('span'); el.textContent = 'x^2'; const mathField = MQ.MathField(el); assert.ok(mathField instanceof MQ.MathField); }); - test('identity of API object returned by MQ()', () => { + test('identity of API object returned by MQ()', function () { const mathFieldSpan = document.createElement('span'); const mathField = MQ.MathField(mathFieldSpan); @@ -32,7 +32,7 @@ suite('Public API', () => { assert.equal(MQ(mathFieldSpan).data, MQ(mathFieldSpan).data); }); - test('blurred when created', () => { + test('blurred when created', function () { const el = document.createElement('span'); MQ.MathField(el); const rootBlock = el.querySelector('.mq-root-block'); @@ -41,22 +41,22 @@ suite('Public API', () => { }); }); - suite('basic API methods', () => { + suite('basic API methods', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); }); - test('.revert()', () => { + test('.revert()', function () { const el = document.createElement('span'); el.innerHTML = 'some HTML'; const mq = MQ.MathField(el); assert.equal(mq.revert().innerHTML, 'some HTML'); }); - test('select, clearSelection', () => { + test('select, clearSelection', function () { mq.latex('n+\\frac{n}{2}'); assert.ok(!mq.__controller.cursor.selection); mq.select(); @@ -65,7 +65,7 @@ suite('Public API', () => { assert.ok(!mq.__controller.cursor.selection); }); - test("latex while there's a selection", () => { + test("latex while there's a selection", function () { mq.latex('a'); assert.equal(mq.latex(), 'a'); mq.select(); @@ -76,12 +76,12 @@ suite('Public API', () => { assert.equal(mq.latex(), 'bc'); }); - test('.html() trivial case', () => { + test('.html() trivial case', function () { mq.latex('x+y'); assert.equal(mq.html(), 'x+y'); }); - test('.text() with incomplete commands', () => { + test('.text() with incomplete commands', function () { assert.equal(mq.text(), ''); mq.typedText('\\'); assert.equal(mq.text(), '\\'); @@ -91,7 +91,7 @@ suite('Public API', () => { assert.equal(mq.text(), '\\sqrt'); }); - test('.text() with complete commands', () => { + test('.text() with complete commands', function () { mq.latex('\\sqrt{}'); assert.equal(mq.text(), 'sqrt()'); mq.latex('\\nthroot[]{}'); @@ -118,7 +118,7 @@ suite('Public API', () => { assert.equal(mq.text(), '*2*3***4'); }); - test('.moveToDirEnd(dir)', () => { + test('.moveToDirEnd(dir)', function () { mq.latex('a x^2 + b x + c = 0'); assert.equal(mq.__controller.cursor.left.ctrlSeq, '0'); assert.equal(mq.__controller.cursor.right, undefined); @@ -130,14 +130,14 @@ suite('Public API', () => { assert.equal(mq.__controller.cursor.right, undefined); }); - test('.empty()', () => { + test('.empty()', function () { mq.latex('xyz'); mq.empty(); assert.equal(mq.latex(), ''); }); }); - test('edit handler interface versioning', () => { + test('edit handler interface versioning', function () { let count = 0; const el = document.createElement('span'); @@ -155,9 +155,9 @@ suite('Public API', () => { assert.equal(count, 2); // sigh, once for postOrder and once for bubble }); - suite('*OutOf handlers', () => { + suite('*OutOf handlers', function () { const testHandlers = (title, mathFieldMaker) => { - test(title, () => { + test(title, function () { let enterCounter = 0, upCounter = 0, moveCounter = 0, @@ -281,7 +281,7 @@ suite('Public API', () => { } ); - suite('global MQ.config()', () => { + suite('global MQ.config()', function () { testHandlers('a MQ.MathField', (options) => { const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -297,12 +297,14 @@ suite('Public API', () => { return MQ.StaticMath(el).innerFields[0]; }); - teardown(() => MQ.config({ handlers: undefined })); + teardown(function () { + MQ.config({ handlers: undefined }); + }); }); }); - suite('edit handler', () => { - test('fires when closing a bracket expression', () => { + suite('edit handler', function () { + test('fires when closing a bracket expression', function () { let count = 0; const el = document.createElement('span'); @@ -316,16 +318,16 @@ suite('Public API', () => { }); }); - suite('.cmd(...)', () => { + suite('.cmd(...)', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); }); - test('basic', () => { + test('basic', function () { mq.cmd('x'); assert.equal(mq.latex(), 'x'); mq.cmd('y'); @@ -340,27 +342,27 @@ suite('Public API', () => { assert.equal(mq.latex(), '\\sqrt{xy^2\\cdot2\\cdot\\cdot}'); }); - test('backslash commands are passed their name', () => { + test('backslash commands are passed their name', function () { mq.cmd('\\alpha'); assert.equal(mq.latex(), '\\alpha'); }); - test('replaces selection', () => { + test('replaces selection', function () { mq.typedText('49').select().cmd('\\sqrt'); assert.equal(mq.latex(), '\\sqrt{49}'); }); - test('operator name', () => { + test('operator name', function () { mq.cmd('\\ker'); assert.equal(mq.latex(), '\\ker'); }); - test('nonexistent LaTeX command is noop', () => { + test('nonexistent LaTeX command is noop', function () { mq.typedText('49').select().cmd('\\asdf').cmd('\\sqrt'); assert.equal(mq.latex(), '\\sqrt{49}'); }); - test('overflow triggers automatic horizontal scroll', (done) => { + test('overflow triggers automatic horizontal scroll', function (done) { const mqEl = mq.el(); const rootEl = mq.__controller.root.elements.first; const cursor = mq.__controller.cursor; @@ -389,9 +391,9 @@ suite('Public API', () => { }); }); - suite('spaceBehavesLikeTab', () => { + suite('spaceBehavesLikeTab', function () { let mq, rootBlock, cursor; - test('space behaves like tab with default opts', () => { + test('space behaves like tab with default opts', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); @@ -412,7 +414,7 @@ suite('Public API', () => { assert.equal(cursor.left.ctrlSeq, '\\ ', 'left of the cursor is ' + cursor.left.ctrlSeq); assert.equal(cursor.right, undefined, 'right of the cursor is ' + cursor.right); }); - test('space behaves like tab when spaceBehavesLikeTab is true', () => { + test('space behaves like tab when spaceBehavesLikeTab is true', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el, { spaceBehavesLikeTab: true }); @@ -431,7 +433,7 @@ suite('Public API', () => { assert.equal(cursor.left, undefined, 'left cursor is ' + cursor.left); assert.equal(cursor.right, rootBlock.ends.left, 'parent of rootBlock is ' + cursor.right); }); - test('space behaves like tab when globally set to true', () => { + test('space behaves like tab when globally set to true', function () { MQ.config({ spaceBehavesLikeTab: true }); const el = document.createElement('span'); @@ -451,42 +453,42 @@ suite('Public API', () => { }); }); - suite('maxDepth option', () => { + suite('maxDepth option', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el, { maxDepth: 1 }); }); - teardown(() => { + teardown(function () { mq.el().remove(); }); - test('prevents nested math input via .write() method', () => { + test('prevents nested math input via .write() method', function () { mq.write('1\\frac{\\frac{3}{3}}{2}'); assert.equal(mq.latex(), '1\\frac{ }{ }'); }); - test('prevents nested math input via keyboard input', () => { + test('prevents nested math input via keyboard input', function () { mq.cmd('/').write('x'); assert.equal(mq.latex(), '\\frac{ }{ }'); }); - test('stops new fraction moving content into numerator', () => { + test('stops new fraction moving content into numerator', function () { mq.write('x').cmd('/'); assert.equal(mq.latex(), 'x\\frac{ }{ }'); }); - test('prevents nested math input via replacedFragment', () => { + test('prevents nested math input via replacedFragment', function () { mq.cmd('(').keystroke('Left').cmd('('); assert.equal(mq.latex(), '\\left(\\right)'); }); }); - suite('statelessClipboard option', () => { - suite('default', () => { + suite('statelessClipboard option', function () { + suite('default', function () { let mq, textarea; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); @@ -502,25 +504,28 @@ suite('Public API', () => { assert.equal(mq.latex(), latex); }; - test('numbers and letters', () => assertPaste('123xyz')); - test('a sentence', () => + test('numbers and letters', function () { + assertPaste('123xyz'); + }); + test('a sentence', function () { assertPaste( 'Lorem ipsum is a placeholder text commonly used to ' + 'demonstrate the graphical elements of a document or ' + 'visual presentation.', 'Loremipsumisaplaceholdertextcommonlyusedtodemonstrate' + 'thegraphicalelementsofadocumentorvisualpresentation.' - )); - test('actual LaTeX', () => { + ); + }); + test('actual LaTeX', function () { assertPaste('a_nx^n+a_{n+1}x^{n+1}'); assertPaste('\\frac{1}{2\\sqrt{x}}'); }); - test('\\text{...}', () => { + test('\\text{...}', function () { assertPaste('\\text{lol}'); assertPaste('1+\\text{lol}+2'); assertPaste('\\frac{\\text{apples}}{\\text{oranges}}'); }); - test('selection', (done) => { + test('selection', function (done) { mq.latex('x^2').select(); setTimeout(() => { assert.equal(textarea.value, 'x^2'); @@ -528,9 +533,9 @@ suite('Public API', () => { }); }); }); - suite('statelessClipboard set to true', () => { + suite('statelessClipboard set to true', function () { let mq, textarea; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el, { statelessClipboard: true }); @@ -546,8 +551,10 @@ suite('Public API', () => { assert.equal(mq.latex(), latex); }; - test('numbers and letters', () => assertPaste('123xyz', '\\text{123xyz}')); - test('a sentence', () => + test('numbers and letters', function () { + assertPaste('123xyz', '\\text{123xyz}'); + }); + test('a sentence', function () { assertPaste( 'Lorem ipsum is a placeholder text commonly used to ' + 'demonstrate the graphical elements of a document or ' + @@ -555,15 +562,17 @@ suite('Public API', () => { '\\text{Lorem ipsum is a placeholder text commonly used to ' + 'demonstrate the graphical elements of a document or ' + 'visual presentation.}' - )); - test('backslashes', () => - assertPaste('something pi something asdf', '\\text{something pi something asdf}')); + ); + }); + test('backslashes', function () { + assertPaste('something pi something asdf', '\\text{something pi something asdf}'); + }); // TODO: braces (currently broken) - test('actual math LaTeX wrapped in dollar signs', () => { + 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('$\\frac{1}{2\\sqrt{x}}$', '\\frac{1}{2\\sqrt{x}}'); }); - test('selection', (done) => { + test('selection', function (done) { mq.latex('x^2').select(); setTimeout(() => { assert.equal(textarea.value, '$x^2$'); @@ -573,22 +582,23 @@ suite('Public API', () => { }); }); - suite('leftRightIntoCmdGoes: "up"/"down"', () => { - test('"up" or "down" required', () => + suite('leftRightIntoCmdGoes: "up"/"down"', function () { + test('"up" or "down" required', function () { assert.throws(() => { const el = document.createElement('span'); document.getElementById('mock')?.append(el); MQ.MathField(el, { leftRightIntoCmdGoes: 1 }); - })); - suite('default', () => { + }); + }); + suite('default', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); }); - test('fractions', () => { + test('fractions', function () { mq.latex('\\frac{1}{x}+\\frac{\\frac{1}{2}}{\\frac{3}{4}}'); assert.equal(mq.latex(), '\\frac{1}{x}+\\frac{\\frac{1}{2}}{\\frac{3}{4}}'); @@ -632,7 +642,7 @@ suite('Public API', () => { assert.equal(mq.latex(), 'a\\frac{b1}{cx}d+\\frac{e\\frac{f1}{g2}h}{i\\frac{j3}{k4}l}m'); }); - test('supsub', () => { + 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'); @@ -661,7 +671,7 @@ suite('Public API', () => { assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^{7b}8+w'); }); - test('nthroot', () => { + test('nthroot', function () { mq.latex('\\sqrt[n]{x}'); assert.equal(mq.latex(), '\\sqrt[n]{x}'); @@ -679,15 +689,15 @@ suite('Public API', () => { }); }); - suite('"up"', () => { + suite('"up"', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el, { leftRightIntoCmdGoes: 'up' }); }); - test('fractions', () => { + test('fractions', function () { mq.latex('\\frac{1}{x}+\\frac{\\frac{1}{2}}{\\frac{3}{4}}'); assert.equal(mq.latex(), '\\frac{1}{x}+\\frac{\\frac{1}{2}}{\\frac{3}{4}}'); @@ -713,7 +723,7 @@ suite('Public API', () => { assert.equal(mq.latex(), 'a\\frac{b1}{x}c+\\frac{d\\frac{e1}{2}f}{\\frac{3}{4}}g'); }); - test('supsub', () => { + 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'); @@ -739,7 +749,7 @@ suite('Public API', () => { assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^{6b}7+w'); }); - test('nthroot', () => { + test('nthroot', function () { mq.latex('\\sqrt[n]{x}'); assert.equal(mq.latex(), '\\sqrt[n]{x}'); @@ -758,8 +768,8 @@ suite('Public API', () => { }); }); - suite('sumStartsWithNEquals', () => { - test('sum defaults to empty limits', () => { + suite('sumStartsWithNEquals', function () { + test('sum defaults to empty limits', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); const mq = MQ.MathField(el); @@ -771,7 +781,7 @@ suite('Public API', () => { mq.cmd('n'); assert.equal(mq.latex(), '\\sum_n^{ }', 'cursor in lower limit'); }); - test('sum starts with `n=`', () => { + test('sum starts with `n=`', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); const mq = MQ.MathField(el, { sumStartsWithNEquals: true }); @@ -783,7 +793,7 @@ suite('Public API', () => { mq.cmd('0'); assert.equal(mq.latex(), '\\sum_{n=0}^{ }', 'cursor after the `n=`'); }); - test('integral still has empty limits', () => { + test('integral still has empty limits', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); const mq = MQ.MathField(el, { sumStartsWithNEquals: true }); @@ -797,8 +807,8 @@ suite('Public API', () => { }); }); - suite('substituteTextarea', () => { - test("doesn't blow up on selection", () => { + suite('substituteTextarea', function () { + test("doesn't blow up on selection", function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -816,8 +826,8 @@ suite('Public API', () => { }); }); - suite('keyboard event overrides', () => { - test('can intercept key events', () => { + suite('keyboard event overrides', function () { + test('can intercept key events', function () { let key; const el = document.createElement('span'); @@ -832,7 +842,7 @@ suite('Public API', () => { ); assert.equal(key, 'Left'); }); - test('cut is NOT async (why should it be?)', () => { + test('cut is NOT async (why should it be?)', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -856,8 +866,8 @@ suite('Public API', () => { }); }); - suite('clickAt', () => { - test('inserts at coordinates', () => { + suite('clickAt', function () { + test('inserts at coordinates', function () { // Insert filler to make the page taller than the window so that this test is deterministic. const filler = document.createElement('div'); filler.style.height = `${window.offsetHeight}px`; @@ -883,7 +893,7 @@ suite('Public API', () => { assert.equal(mq.latex(), '\\frac{mmmm}{mmxmm}'); }); - test('target is optional', () => { + test('target is optional', function () { // Insert filler to make the page taller than the window so that this test is deterministic. const filler = document.createElement('div'); filler.style.height = `${window.offsetHeight}px`; @@ -909,8 +919,8 @@ suite('Public API', () => { }); }); - suite('dropEmbedded', () => { - test('inserts into empty', () => { + suite('dropEmbedded', function () { + test('inserts into empty', function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); const mq = MQ.MathField(el); @@ -925,7 +935,7 @@ suite('Public API', () => { assert.equal(mq.latex(), 'embedded latex'); }); - test('inserts at coordinates', () => { + test('inserts at coordinates', function () { // Insert filler to make the page taller than the window so that this test is deterministic. const filler = document.createElement('div'); filler.style.height = `${window.offsetHeight}px`; @@ -956,7 +966,7 @@ suite('Public API', () => { }); }); - test('.registerEmbed()', () => { + test('.registerEmbed()', function () { let calls = 0, data; diff --git a/test/saneKeyboardEvents.test.js b/test/saneKeyboardEvents.test.js index e0b64bed..f18fb0c6 100644 --- a/test/saneKeyboardEvents.test.js +++ b/test/saneKeyboardEvents.test.js @@ -1,4 +1,4 @@ -/* global suite, test, assert, setup */ +/* global assert */ import { noop } from 'src/constants'; import { saneKeyboardEvents } from 'services/saneKeyboardEvents.util'; @@ -9,17 +9,17 @@ import { Controller } from 'src/controller'; // FIXME: Most of this needs to be reworked. The fact is that the fake events that are being sent do not correctly // emulate actual behavior in a browser, and so the tests are a complete sham. -suite('saneKeyboardEvents', () => { +suite('saneKeyboardEvents', function () { let el; const supportsSelectionAPI = () => 'selectionStart' in el; - setup(() => { + setup(function () { el = document.createElement('textarea'); document.getElementById('mock')?.append(el); }); - test('normal keys', (done) => { + test('normal keys', function (done) { let counter = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -44,7 +44,7 @@ suite('saneKeyboardEvents', () => { el.value = 'a'; }); - test('normal keys without keypress', (done) => { + test('normal keys without keypress', function (done) { let counter = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -69,7 +69,7 @@ suite('saneKeyboardEvents', () => { el.value = 'a'; }); - test('one keydown only', (done) => { + test('one keydown only', function (done) { let counter = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -85,7 +85,7 @@ suite('saneKeyboardEvents', () => { el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', which: 8, keyCode: 8, bubbles: true })); }); - test('a series of keydowns only', (done) => { + test('a series of keydowns only', function (done) { let counter = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -105,7 +105,7 @@ suite('saneKeyboardEvents', () => { el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', which: 37, keyCode: 37, bubbles: true })); }); - test('three keydowns and corresponding keypresses', (done) => { + test('three keydowns and corresponding keypresses', function (done) { let counter = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -128,8 +128,8 @@ suite('saneKeyboardEvents', () => { el.dispatchEvent(new KeyboardEvent('keypress', { key: 'Backspace', which: 8, keyCode: 8, bubbles: true })); }); - suite('select', () => { - test("select populates the textarea but doesn't call .typedText()", () => { + suite('select', function () { + test("select populates the textarea but doesn't call .typedText()", function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -152,7 +152,7 @@ suite('saneKeyboardEvents', () => { "select populates the textarea but doesn't call text" + ' on keydown, even when the selection is not properly' + ' detectable', - () => { + function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -167,7 +167,7 @@ suite('saneKeyboardEvents', () => { } ); - test('blurring', () => { + test('blurring', function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -185,7 +185,7 @@ suite('saneKeyboardEvents', () => { assert.equal(el.value, 'foobar', 'it still has content'); }); - test('blur then empty selection', () => { + test('blur then empty selection', function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -235,8 +235,8 @@ suite('saneKeyboardEvents', () => { }); }); - suite("selected text after keypress or paste doesn't get mistaken" + ' for inputted text', () => { - test('select() immediately after paste', () => { + suite("selected text after keypress or paste doesn't get mistaken" + ' for inputted text', function () { + test('select() immediately after paste', function () { let pastedText; let onPaste = (text) => (pastedText = text); @@ -258,7 +258,7 @@ suite('saneKeyboardEvents', () => { assert.equal(el.value, '$2$'); }); - test('select() after paste/input', () => { + test('select() after paste/input', function () { let pastedText; let onPaste = (text) => (pastedText = text); @@ -283,7 +283,7 @@ suite('saneKeyboardEvents', () => { assert.equal(el.value, '$2$'); }); - test('select() immediately after keydown/keypress', () => { + test('select() immediately after keydown/keypress', function () { let typedText; let onText = (text) => (typedText = text); @@ -311,7 +311,7 @@ suite('saneKeyboardEvents', () => { assert.equal(el.value, '$2$'); }); - test('select() after keydown/keypress/input', () => { + test('select() after keydown/keypress/input', function () { let typedText; let onText = (text) => (typedText = text); @@ -340,8 +340,8 @@ suite('saneKeyboardEvents', () => { assert.equal(el.value, '$2$'); }); - suite('keys that should not move cursor or clear selection', () => { - test('without keypress', () => { + suite('keys that should not move cursor or clear selection', function () { + test('without keypress', function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -376,7 +376,7 @@ suite('saneKeyboardEvents', () => { assert.ok(document.activeElement !== el, 'textarea remains blurred'); }); - test('with keypress, many characters selected', () => { + test('with keypress, many characters selected', function () { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; const shim = saneKeyboardEvents(el, ctrlr); @@ -413,7 +413,7 @@ suite('saneKeyboardEvents', () => { assert.ok(document.activeElement !== el, 'textarea remains blurred'); }); - test('with keypress, only 1 character selected', () => { + test('with keypress, only 1 character selected', function () { let count = 0; const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); @@ -469,8 +469,8 @@ suite('saneKeyboardEvents', () => { }); }); - suite('paste', () => { - test('paste event only', (done) => { + suite('paste', function () { + test('paste event only', function (done) { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overridePaste = (text) => { assert.equal(text, '$x^2+1$'); @@ -483,7 +483,7 @@ suite('saneKeyboardEvents', () => { el.dispatchEvent(event); }); - test('paste after keydown/keypress', (done) => { + test('paste after keydown/keypress', function (done) { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; ctrlr.options.overridePaste = (text) => { @@ -504,7 +504,7 @@ suite('saneKeyboardEvents', () => { el.dispatchEvent(event); }); - test('paste after keydown/keypress and before input', (done) => { + test('paste after keydown/keypress and before input', function (done) { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; ctrlr.options.overridePaste = (text) => { @@ -527,7 +527,7 @@ suite('saneKeyboardEvents', () => { ); }); - test('keypress timeout happening before paste timeout', (done) => { + test('keypress timeout happening before paste timeout', function (done) { const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideKeystroke = noop; ctrlr.options.overridePaste = (text) => { @@ -552,8 +552,8 @@ suite('saneKeyboardEvents', () => { }); }); - suite('copy', () => { - test('only runs handler once even if handler synchronously selects', () => { + suite('copy', function () { + test('only runs handler once even if handler synchronously selects', function () { // ...which MathQuill does and resulted in a stack overflow: https://git.io/vosm0 const ctrlr = new Controller(new MathField.RootBlock(), el, new Options()); ctrlr.options.overrideCopy = () => shim.select(); diff --git a/test/select.test.js b/test/select.test.js index b8def698..e1433824 100644 --- a/test/select.test.js +++ b/test/select.test.js @@ -1,4 +1,4 @@ -/* global suite, test, assert */ +/* global assert */ import { noop } from 'src/constants'; import { Cursor } from 'src/cursor'; @@ -6,7 +6,7 @@ import { Point } from 'tree/point'; import { TNode } from 'tree/node'; import { Fragment } from 'tree/fragment'; -suite('Cursor::select()', () => { +suite('Cursor::select()', function () { const cursor = new Cursor(); cursor.selectionChanged = noop; @@ -49,19 +49,19 @@ suite('Cursor::select()', () => { const pt2 = new Point(child2); const pt3 = new Point(child3); - test('same parent, one TNode', () => { + test('same parent, one TNode', function () { assertSelection(A, B, child1); assertSelection(B, C, child2); assertSelection(C, D, child3); }); - test('same Parent, many Nodes', () => { + test('same Parent, many Nodes', function () { assertSelection(A, C, child1, child2); assertSelection(A, D, child1, child3); assertSelection(B, D, child2, child3); }); - test('Point next to parent of other Point', () => { + test('Point next to parent of other Point', function () { assertSelection(A, pt1, child1); assertSelection(B, pt1, child1); @@ -72,13 +72,13 @@ suite('Cursor::select()', () => { assertSelection(D, pt3, child3); }); - test("Points' parents are siblings", () => { + test("Points' parents are siblings", function () { assertSelection(pt1, pt2, child1, child2); assertSelection(pt2, pt3, child2, child3); assertSelection(pt1, pt3, child1, child3); }); - test('Point is sibling of parent of other Point', () => { + test('Point is sibling of parent of other Point', function () { assertSelection(A, pt2, child1, child2); assertSelection(A, pt3, child1, child3); assertSelection(B, pt3, child2, child3); @@ -86,7 +86,7 @@ suite('Cursor::select()', () => { assertSelection(pt1, C, child1, child2); }); - test('same Point', () => { + test('same Point', function () { cursor.parent = A.parent; cursor.left = A.left; cursor.right = A.right; @@ -94,7 +94,7 @@ suite('Cursor::select()', () => { assert.equal(cursor.select(), false); }); - test('different trees', () => { + test('different trees', function () { const anotherTree = new TNode(); cursor.parent = A.parent; diff --git a/test/text-output.test.js b/test/text-output.test.js index f27bf1b5..62fbdba2 100644 --- a/test/text-output.test.js +++ b/test/text-output.test.js @@ -1,8 +1,8 @@ -/* global suite, test, assert, setup, teardown, MQ */ +/* global assert, MQ */ -suite('text() output', () => { +suite('text() output', function () { let mq; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -22,13 +22,13 @@ suite('text() output', () => { maxDepth: 10 }); }); - teardown(() => { + teardown(function () { mq.el().remove(); }); // FIXME: For WeBWorK text output is extremely important. So much more of this is needed. - test('degrees typed with no spaces', () => { + test('degrees typed with no spaces', function () { mq.typedText('0degC'); assert.equal(mq.text(), '0\u00B0C', '0 degrees Celsius'); mq.empty(); @@ -46,7 +46,7 @@ suite('text() output', () => { mq.empty(); }); - test('degrees typed with spaces', () => { + test('degrees typed with spaces', function () { mq.typedText('0 degC'); assert.equal(mq.text(), '0 \u00B0C', '0 degrees Celsius'); mq.empty(); diff --git a/test/text.test.js b/test/text.test.js index ed552336..7b4e21b4 100644 --- a/test/text.test.js +++ b/test/text.test.js @@ -1,12 +1,12 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { prayWellFormed } from 'src/constants'; import { Controller } from 'src/controller'; import { latexMathParser } from 'commands/mathElements'; -suite('text', () => { +suite('text', function () { let mq, mostRecentlyReportedLatex; - setup(() => { + setup(function () { mostRecentlyReportedLatex = NaN; // != to everything const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -47,7 +47,7 @@ suite('text', () => { } }; - test('changes the text nodes as the cursor moves around', () => { + test('changes the text nodes as the cursor moves around', function () { const block = fromLatex('\\text{abc}'); const ctrlr = new Controller(block, 0, 0); const cursor = ctrlr.cursor.insAtRightEnd(block); @@ -74,7 +74,7 @@ suite('text', () => { assertSplit(cursor.element, 'abc', null); }); - test('does not change latex as the cursor moves around', () => { + test('does not change latex as the cursor moves around', function () { const block = fromLatex('\\text{x}'); const ctrlr = new Controller(block, 0, 0); ctrlr.cursor.insAtRightEnd(block); @@ -86,8 +86,8 @@ suite('text', () => { assert.equal(block.latex(), '\\text{x}'); }); - suite('typing', () => { - test('stepping out of an empty block deletes it', () => { + suite('typing', function () { + test('stepping out of an empty block deletes it', function () { const controller = mq.__controller; const cursor = controller.cursor; @@ -108,7 +108,7 @@ suite('text', () => { assertLatex(''); }); - test('typing $ in a textblock splits it', () => { + test('typing $ in a textblock splits it', function () { const controller = mq.__controller; const cursor = controller.cursor; @@ -124,8 +124,8 @@ suite('text', () => { }); }); - suite('pasting', () => { - test('sanity', () => { + suite('pasting', function () { + test('sanity', function () { const controller = mq.__controller; const cursor = controller.cursor; @@ -140,7 +140,7 @@ suite('text', () => { prayWellFormedPoint(cursor); }); - test('pasting a dollar sign', () => { + test('pasting a dollar sign', function () { const controller = mq.__controller; const cursor = controller.cursor; @@ -155,7 +155,7 @@ suite('text', () => { prayWellFormedPoint(cursor); }); - test('pasting a backslash', () => { + test('pasting a backslash', function () { const controller = mq.__controller; const cursor = controller.cursor; @@ -170,7 +170,7 @@ suite('text', () => { prayWellFormedPoint(cursor); }); - test('pasting a curly brace', () => { + test('pasting a curly brace', function () { const controller = mq.__controller; const cursor = controller.cursor; diff --git a/test/tree.test.js b/test/tree.test.js index 0db861f0..28d32bb6 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -1,10 +1,10 @@ -/* global suite, test, assert */ +/* global assert */ import { TNode } from 'tree/node'; import { Fragment } from 'tree/fragment'; -suite('tree', () => { - suite('adopt', () => { +suite('tree', function () { + suite('adopt', function () { const assertTwoChildren = (parent, one, two) => { assert.equal(one.parent, parent, 'one.parent is set'); assert.equal(two.parent, parent, 'two.parent is set'); @@ -18,7 +18,7 @@ suite('tree', () => { assert.equal(parent.ends.right, two, 'parent.ends.right is two'); }; - test('the empty case', () => { + test('the empty case', function () { const parent = new TNode(); const child = new TNode(); @@ -32,7 +32,7 @@ suite('tree', () => { assert.equal(parent.ends.right, child, 'child is parent.ends.right'); }); - test('with two children from the left', () => { + test('with two children from the left', function () { const parent = new TNode(); const one = new TNode(); const two = new TNode(); @@ -43,7 +43,7 @@ suite('tree', () => { assertTwoChildren(parent, one, two); }); - test('with two children from the right', () => { + test('with two children from the right', function () { const parent = new TNode(); const one = new TNode(); const two = new TNode(); @@ -54,7 +54,7 @@ suite('tree', () => { assertTwoChildren(parent, one, two); }); - test('adding one in the middle', () => { + test('adding one in the middle', function () { const parent = new TNode(); const leftward = new TNode(); const rightward = new TNode(); @@ -76,7 +76,7 @@ suite('tree', () => { }); }); - suite('disown', () => { + suite('disown', function () { const assertSingleChild = (parent, child) => { assert.equal(parent.ends.left, child, 'parent.ends.left is child'); assert.equal(parent.ends.right, child, 'parent.ends.right is child'); @@ -84,7 +84,7 @@ suite('tree', () => { assert.ok(!child.right, 'child has nothing rightward'); }; - test('the empty case', () => { + test('the empty case', function () { const parent = new TNode(); const child = new TNode(); @@ -95,7 +95,7 @@ suite('tree', () => { assert.ok(!parent.ends.right, 'parent has no right end child'); }); - test('disowning the right end child', () => { + test('disowning the right end child', function () { const parent = new TNode(); const one = new TNode(); const two = new TNode(); @@ -113,7 +113,7 @@ suite('tree', () => { assert.throws(() => two.disown(), 'disown fails on a malformed tree'); }); - test('disowning the left end child', () => { + test('disowning the left end child', function () { const parent = new TNode(); const one = new TNode(); const two = new TNode(); @@ -131,7 +131,7 @@ suite('tree', () => { assert.throws(() => one.disown(), 'disown fails on a malformed tree'); }); - test('disowning the middle', () => { + test('disowning the middle', function () { const parent = new TNode(); const leftward = new TNode(); const rightward = new TNode(); @@ -156,8 +156,8 @@ suite('tree', () => { }); }); - suite('fragments', () => { - test('an empty fragment', () => { + suite('fragments', function () { + test('an empty fragment', function () { const empty = new Fragment(); let count = 0; @@ -166,12 +166,12 @@ suite('tree', () => { assert.equal(count, 0, 'each is a noop on an empty fragment'); }); - test('half-empty fragments are disallowed', () => { + test('half-empty fragments are disallowed', function () { assert.throws(() => new Fragment(new TNode(), 0), 'half-empty on the right'); assert.throws(() => new Fragment(0, new TNode()), 'half-empty on the left'); }); - test('directionalized constructor call', () => { + test('directionalized constructor call', function () { const ChNode = class extends TNode { constructor(ch) { super(); @@ -193,7 +193,7 @@ suite('tree', () => { assert.throws(() => new Fragment(b, d, 'right')); }); - test('disown is idempotent', () => { + test('disown is idempotent', function () { const parent = new TNode(); const one = new TNode().adopt(parent); const two = new TNode().adopt(parent, one); diff --git a/test/typing.test.js b/test/typing.test.js index b81164c1..36c55f12 100644 --- a/test/typing.test.js +++ b/test/typing.test.js @@ -1,11 +1,11 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ import { Bracket } from 'commands/mathElements'; import { prayWellFormed } from 'src/constants'; -suite('typing with auto-replaces', () => { +suite('typing with auto-replaces', function () { let mq, mostRecentlyReportedLatex; - setup(() => { + setup(function () { mostRecentlyReportedLatex = NaN; // != to everything const el = document.createElement('span'); document.getElementById('mock')?.append(el); @@ -23,8 +23,8 @@ suite('typing with auto-replaces', () => { assert.equal(mq.latex(), latex); }; - suite('LiveFraction', () => { - test('full MathQuill', () => { + suite('LiveFraction', function () { + test('full MathQuill', function () { mq.options.addAutoCommands('sin'); mq.typedText('1/2').keystroke('Tab').typedText('+sinx').keystroke('Tab').typedText('/'); assertLatex('\\frac{1}{2}+\\frac{\\sin\\left(x\\right)}{ }'); @@ -37,63 +37,63 @@ suite('typing with auto-replaces', () => { }); }); - suite('LatexCommandInput', () => { - test('basic', () => { + suite('LatexCommandInput', function () { + test('basic', function () { mq.typedText('\\sqrt-x'); assertLatex('\\sqrt{-x}'); }); - test('advanced (math function)', () => { + test('advanced (math function)', function () { mq.typedText('\\sin^2'); assertLatex('\\sin^2\\left(\\right)'); }); - test("they're passed their name", () => { + test("they're passed their name", function () { mq.cmd('\\alpha'); assert.equal(mq.latex(), '\\alpha'); }); - test('replaces selection', () => { + test('replaces selection', function () { mq.typedText('49').select().typedText('\\sqrt').keystroke('Enter'); assertLatex('\\sqrt{49}'); }); - test('auto-operator names', () => { + test('auto-operator names', function () { mq.typedText('\\ker^2'); assertLatex('\\ker^2'); }); - test('nonexistent LaTeX command', () => { + test('nonexistent LaTeX command', function () { mq.typedText('\\asdf').keystroke('Enter'); assertLatex('\\text{asdf}'); }); - test('nonexistent LaTeX command, then symbol', () => { + test('nonexistent LaTeX command, then symbol', function () { mq.typedText('\\asdf+'); assertLatex('\\text{asdf}+'); }); - test('dollar sign', () => { + test('dollar sign', function () { mq.typedText('$'); assertLatex('\\$'); }); - test('\\text followed by command', () => { + test('\\text followed by command', function () { mq.typedText('\\text{'); assertLatex('\\text{\\{}'); }); }); - suite('auto-expanding parens', () => { - suite('simple', () => { - test('empty parens ()', () => { + suite('auto-expanding parens', function () { + suite('simple', function () { + test('empty parens ()', function () { mq.typedText('('); assertLatex('\\left(\\right)'); mq.typedText(')'); assertLatex('\\left(\\right)'); }); - test('straight typing 1+(2+3)+4', () => { + test('straight typing 1+(2+3)+4', function () { mq.typedText('1+(2+3)+4'); assertLatex('1+\\left(2+3\\right)+4'); }); @@ -103,7 +103,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\sin\\left(\\right)'); }); - test('wrapping things in parens 1+(2+3)+4', () => { + test('wrapping things in parens 1+(2+3)+4', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left').typedText(')'); @@ -112,14 +112,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left(2+3\\right)+4'); }); - test('nested parens 1+(2+(3+4)+5)+6', () => { + test('nested parens 1+(2+(3+4)+5)+6', function () { mq.typedText('1+(2+(3+4)+5)+6'); assertLatex('1+\\left(2+\\left(3+4\\right)+5\\right)+6'); }); }); - suite('mismatched brackets', () => { - test('empty mismatched brackets (] and [}', () => { + suite('mismatched brackets', function () { + test('empty mismatched brackets (] and [}', function () { mq.typedText('('); assertLatex('\\left(\\right)'); mq.typedText(']'); @@ -130,7 +130,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\left(\\right]\\left[\\right\\}'); }); - test('typing mismatched brackets 1+(2+3]+4', () => { + test('typing mismatched brackets 1+(2+3]+4', function () { mq.typedText('1+'); assertLatex('1+'); mq.typedText('('); @@ -141,7 +141,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left(2+3\\right]+4'); }); - test('wrapping things in mismatched brackets 1+(2+3]+4', () => { + test('wrapping things in mismatched brackets 1+(2+3]+4', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left').typedText(']'); @@ -150,46 +150,48 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left(2+3\\right]+4'); }); - test('nested mismatched brackets 1+(2+[3+4)+5]+6', () => { + test('nested mismatched brackets 1+(2+[3+4)+5]+6', function () { mq.typedText('1+(2+[3+4)+5]+6'); assertLatex('1+\\left(2+\\left[3+4\\right)+5\\right]+6'); }); - suite('restrictMismatchedBrackets', () => { - setup(() => mq.config({ restrictMismatchedBrackets: true })); - test('typing (|x|+1) works', () => { + suite('restrictMismatchedBrackets', function () { + setup(function () { + mq.config({ restrictMismatchedBrackets: true }); + }); + test('typing (|x|+1) works', function () { mq.typedText('(|x|+1)'); assertLatex('\\left(\\left|x\\right|+1\\right)'); }); - test('typing [x} becomes [{x}]', () => { + test('typing [x} becomes [{x}]', function () { mq.typedText('[x}'); assertLatex('\\left[\\left\\{x\\right\\}\\right]'); }); - test('normal matching pairs {f(n), [a,b]} work', () => { + test('normal matching pairs {f(n), [a,b]} work', function () { mq.typedText('{f(n), [a,b]}'); assertLatex('\\left\\{f\\left(n\\right),\\ \\left[a,b\\right]\\right\\}'); }); - test('[a,b) and (a,b] still work', () => { + test('[a,b) and (a,b] still work', function () { mq.typedText('[a,b) + (a,b]'); assertLatex('\\left[a,b\\right)\\ +\\ \\left(a,b\\right]'); }); }); }); - suite('pipes', () => { - test('empty pipes ||', () => { + suite('pipes', function () { + test('empty pipes ||', function () { mq.typedText('|'); assertLatex('\\left|\\right|'); mq.typedText('|'); assertLatex('\\left|\\right|'); }); - test('straight typing 1+|2+3|+4', () => { + test('straight typing 1+|2+3|+4', function () { mq.typedText('1+|2+3|+4'); assertLatex('1+\\left|2+3\\right|+4'); }); - test('wrapping things in pipes 1+|2+3|+4', () => { + test('wrapping things in pipes 1+|2+3|+4', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Home Right Right').typedText('|'); @@ -198,28 +200,28 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left|2+3\\right|+4'); }); - suite('can type mismatched paren/pipe group from any side', () => { - suite('straight typing', () => { - test('|)', () => { + suite('can type mismatched paren/pipe group from any side', function () { + suite('straight typing', function () { + test('|)', function () { mq.typedText('|)'); assertLatex('\\left|\\right)'); }); - test('(|', () => { + test('(|', function () { mq.typedText('(|'); assertLatex('\\left(\\right|'); }); }); - suite('the other direction', () => { - test('|)', () => { + suite('the other direction', function () { + test('|)', function () { mq.typedText(')'); assertLatex('\\left(\\right)'); mq.keystroke('Left').typedText('|'); assertLatex('\\left|\\right)'); }); - test('(|', () => { + test('(|', function () { mq.typedText('||'); assertLatex('\\left|\\right|'); mq.keystroke('Left Left Delete'); @@ -233,14 +235,16 @@ suite('typing with auto-replaces', () => { suite('backspacing', backspacingTests); - suite('backspacing with restrictMismatchedBrackets', () => { - setup(() => mq.config({ restrictMismatchedBrackets: true })); + suite('backspacing with restrictMismatchedBrackets', function () { + setup(function () { + mq.config({ restrictMismatchedBrackets: true }); + }); backspacingTests(); }); function backspacingTests() { - test('typing then backspacing a close-paren in the middle of 1+2+3+4', () => { + test('typing then backspacing a close-paren in the middle of 1+2+3+4', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left').typedText(')'); @@ -249,7 +253,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing close-paren then open-paren of 1+(2+3)+4', () => { + test('backspacing close-paren then open-paren of 1+(2+3)+4', function () { mq.typedText('1+(2+3)+4'); assertLatex('1+\\left(2+3\\right)+4'); mq.keystroke('Left Left Backspace'); @@ -258,14 +262,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing open-paren of 1+(2+3)+4', () => { + test('backspacing open-paren of 1+(2+3)+4', function () { mq.typedText('1+(2+3)+4'); assertLatex('1+\\left(2+3\\right)+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('backspacing close-bracket then open-paren of 1+(2+3]+4', () => { + test('backspacing close-bracket then open-paren of 1+(2+3]+4', function () { mq.typedText('1+(2+3]+4'); assertLatex('1+\\left(2+3\\right]+4'); mq.keystroke('Left Left Backspace'); @@ -274,14 +278,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing open-paren of 1+(2+3]+4', () => { + test('backspacing open-paren of 1+(2+3]+4', function () { mq.typedText('1+(2+3]+4'); assertLatex('1+\\left(2+3\\right]+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('backspacing close-bracket then open-paren of 1+(2+3] (nothing after paren group)', () => { + test('backspacing close-bracket then open-paren of 1+(2+3] (nothing after paren group)', function () { mq.typedText('1+(2+3]'); assertLatex('1+\\left(2+3\\right]'); mq.keystroke('Backspace'); @@ -290,14 +294,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3'); }); - test('backspacing open-paren of 1+(2+3] (nothing after paren group)', () => { + test('backspacing open-paren of 1+(2+3] (nothing after paren group)', function () { mq.typedText('1+(2+3]'); assertLatex('1+\\left(2+3\\right]'); mq.keystroke('Left Left Left Left Backspace'); assertLatex('1+2+3'); }); - test('backspacing close-bracket then open-paren of (2+3]+4 (nothing before paren group)', () => { + test('backspacing close-bracket then open-paren of (2+3]+4 (nothing before paren group)', function () { mq.typedText('(2+3]+4'); assertLatex('\\left(2+3\\right]+4'); mq.keystroke('Left Left Backspace'); @@ -306,7 +310,7 @@ suite('typing with auto-replaces', () => { assertLatex('2+3+4'); }); - test('backspacing open-paren of (2+3]+4 (nothing before paren group)', () => { + test('backspacing open-paren of (2+3]+4 (nothing before paren group)', function () { mq.typedText('(2+3]+4'); assertLatex('\\left(2+3\\right]+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); @@ -322,7 +326,7 @@ suite('typing with auto-replaces', () => { ); } - test('backspacing close-bracket then open-paren of 1+(]+4 (empty paren group)', () => { + test('backspacing close-bracket then open-paren of 1+(]+4 (empty paren group)', function () { mq.typedText('1+(]+4'); assertLatex('1+\\left(\\right]+4'); mq.keystroke('Left Left Backspace'); @@ -332,14 +336,14 @@ suite('typing with auto-replaces', () => { assertLatex('1++4'); }); - test('backspacing open-paren of 1+(]+4 (empty paren group)', () => { + test('backspacing open-paren of 1+(]+4 (empty paren group)', function () { mq.typedText('1+(]+4'); assertLatex('1+\\left(\\right]+4'); mq.keystroke('Left Left Left Backspace'); assertLatex('1++4'); }); - test('backspacing close-bracket then open-paren of 1+(] (empty paren group, nothing after)', () => { + test('backspacing close-bracket then open-paren of 1+(] (empty paren group, nothing after)', function () { mq.typedText('1+(]'); assertLatex('1+\\left(\\right]'); mq.keystroke('Backspace'); @@ -348,14 +352,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+'); }); - test('backspacing open-paren of 1+(] (empty paren group, nothing after)', () => { + test('backspacing open-paren of 1+(] (empty paren group, nothing after)', function () { mq.typedText('1+(]'); assertLatex('1+\\left(\\right]'); mq.keystroke('Left Backspace'); assertLatex('1+'); }); - test('backspacing close-bracket then open-paren of (]+4 (empty paren group, nothing before)', () => { + test('backspacing close-bracket then open-paren of (]+4 (empty paren group, nothing before)', function () { mq.typedText('(]+4'); assertLatex('\\left(\\right]+4'); mq.keystroke('Left Left Backspace'); @@ -365,14 +369,14 @@ suite('typing with auto-replaces', () => { assertLatex('+4'); }); - test('backspacing open-paren of (]+4 (empty paren group, nothing before)', () => { + test('backspacing open-paren of (]+4 (empty paren group, nothing before)', function () { mq.typedText('(]+4'); assertLatex('\\left(\\right]+4'); mq.keystroke('Left Left Left Backspace'); assertLatex('+4'); }); - test('rendering mismatched brackets 1+(2+3]+4 from LaTeX then backspacing close-bracket then open-paren', () => { + test('rendering mismatched brackets 1+(2+3]+4 from LaTeX then backspacing close-bracket then open-paren', function () { mq.latex('1+\\left(2+3\\right]+4'); assertLatex('1+\\left(2+3\\right]+4'); mq.keystroke('Left Left Backspace'); @@ -381,14 +385,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('rendering mismatched brackets 1+(2+3]+4 from LaTeX then backspacing open-paren', () => { + test('rendering mismatched brackets 1+(2+3]+4 from LaTeX then backspacing open-paren', function () { mq.latex('1+\\left(2+3\\right]+4'); assertLatex('1+\\left(2+3\\right]+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('rendering paren group 1+(2+3)+4 from LaTeX then backspacing close-paren then open-paren', () => { + test('rendering paren group 1+(2+3)+4 from LaTeX then backspacing close-paren then open-paren', function () { mq.latex('1+\\left(2+3\\right)+4'); assertLatex('1+\\left(2+3\\right)+4'); mq.keystroke('Left Left Backspace'); @@ -397,14 +401,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('rendering paren group 1+(2+3)+4 from LaTeX then backspacing open-paren', () => { + test('rendering paren group 1+(2+3)+4 from LaTeX then backspacing open-paren', function () { mq.latex('1+\\left(2+3\\right)+4'); assertLatex('1+\\left(2+3\\right)+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('wrapping selection in parens 1+(2+3)+4 then backspacing close-paren then open-paren', () => { + test('wrapping selection in parens 1+(2+3)+4 then backspacing close-paren then open-paren', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left Shift-Left Shift-Left Shift-Left').typedText(')'); @@ -415,7 +419,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('wrapping selection in parens 1+(2+3)+4 then backspacing open-paren', () => { + test('wrapping selection in parens 1+(2+3)+4 then backspacing open-paren', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left Shift-Left Shift-Left Shift-Left').typedText('('); @@ -424,7 +428,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing close-bracket of 1+(2+3] (nothing after) then typing', () => { + test('backspacing close-bracket of 1+(2+3] (nothing after) then typing', function () { mq.typedText('1+(2+3]'); assertLatex('1+\\left(2+3\\right]'); mq.keystroke('Backspace'); @@ -433,7 +437,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left(2+3+4\\right)'); }); - test('backspacing open-paren of (2+3]+4 (nothing before) then typing', () => { + test('backspacing open-paren of (2+3]+4 (nothing before) then typing', function () { mq.typedText('(2+3]+4'); assertLatex('\\left(2+3\\right]+4'); mq.keystroke('Home Right Backspace'); @@ -442,7 +446,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing paren containing a one-sided paren 0+[(1+2)+3]+4', () => { + test('backspacing paren containing a one-sided paren 0+[(1+2)+3]+4', function () { mq.typedText('0+[1+2+3]+4'); assertLatex('0+\\left[1+2+3\\right]+4'); mq.keystroke('Left Left Left Left Left').typedText(')'); @@ -451,14 +455,14 @@ suite('typing with auto-replaces', () => { assertLatex('0+\\left[1+2\\right)+3+4'); }); - test('backspacing paren inside a one-sided paren (0+[1+2]+3)+4', () => { + test('backspacing paren inside a one-sided paren (0+[1+2]+3)+4', function () { mq.typedText('0+[1+2]+3)+4'); assertLatex('\\left(0+\\left[1+2\\right]+3\\right)+4'); mq.keystroke('Left Left Left Left Left Backspace'); assertLatex('0+\\left[1+2+3\\right)+4'); }); - test('backspacing paren containing and inside a one-sided paren (([1+2]))', () => { + test('backspacing paren containing and inside a one-sided paren (([1+2]))', function () { mq.typedText('(1+2))'); assertLatex('\\left(\\left(1+2\\right)\\right)'); mq.keystroke('Left Left').typedText(']'); @@ -469,7 +473,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\left(1+2\\right)'); }); - test('auto-expanding calls .siblingCreated() on new siblings 1+((2+3))', () => { + test('auto-expanding calls .siblingCreated() on new siblings 1+((2+3))', function () { mq.typedText('1+((2+3))'); assertLatex('1+\\left(\\left(2+3\\right)\\right)'); mq.keystroke('Left Left Left Left Left Left Delete'); @@ -481,7 +485,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left(2+3\\right)'); }); - test('that unwrapping calls .siblingCreated() on new siblings ((1+2)+(3+4))+5', () => { + test('that unwrapping calls .siblingCreated() on new siblings ((1+2)+(3+4))+5', function () { mq.typedText('(1+2+3+4)+5'); assertLatex('\\left(1+2+3+4\\right)+5'); mq.keystroke('Home Right Right Right Right').typedText(')'); @@ -514,8 +518,8 @@ suite('typing with auto-replaces', () => { assertLatex('123'); }); - suite('pipes', () => { - test('typing then backspacing a pipe in the middle of 1+2+3+4', () => { + suite('pipes (backspacing)', function () { + test('typing then backspacing a pipe in the middle of 1+2+3+4', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left Left').typedText('|'); @@ -524,7 +528,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing close-pipe then open-pipe of 1+|2+3|+4', () => { + test('backspacing close-pipe then open-pipe of 1+|2+3|+4', function () { mq.typedText('1+|2+3|+4'); assertLatex('1+\\left|2+3\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -533,14 +537,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing open-pipe of 1+|2+3|+4', () => { + test('backspacing open-pipe of 1+|2+3|+4', function () { mq.typedText('1+|2+3|+4'); assertLatex('1+\\left|2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('backspacing close-pipe then open-pipe of 1+|2+3| (nothing after pipe pair)', () => { + test('backspacing close-pipe then open-pipe of 1+|2+3| (nothing after pipe pair)', function () { mq.typedText('1+|2+3|'); assertLatex('1+\\left|2+3\\right|'); mq.keystroke('Backspace'); @@ -549,14 +553,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3'); }); - test('backspacing open-pipe of 1+|2+3| (nothing after pipe pair)', () => { + test('backspacing open-pipe of 1+|2+3| (nothing after pipe pair)', function () { mq.typedText('1+|2+3|'); assertLatex('1+\\left|2+3\\right|'); mq.keystroke('Left Left Left Left Backspace'); assertLatex('1+2+3'); }); - test('backspacing close-pipe then open-pipe of |2+3|+4 (nothing before pipe pair)', () => { + test('backspacing close-pipe then open-pipe of |2+3|+4 (nothing before pipe pair)', function () { mq.typedText('|2+3|+4'); assertLatex('\\left|2+3\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -565,7 +569,7 @@ suite('typing with auto-replaces', () => { assertLatex('2+3+4'); }); - test('backspacing open-pipe of |2+3|+4 (nothing before pipe pair)', () => { + test('backspacing open-pipe of |2+3|+4 (nothing before pipe pair)', function () { mq.typedText('|2+3|+4'); assertLatex('\\left|2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); @@ -581,7 +585,7 @@ suite('typing with auto-replaces', () => { ); } - test('backspacing close-pipe then open-pipe of 1+||+4 (empty pipe pair)', () => { + test('backspacing close-pipe then open-pipe of 1+||+4 (empty pipe pair)', function () { mq.typedText('1+||+4'); assertLatex('1+\\left|\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -591,14 +595,14 @@ suite('typing with auto-replaces', () => { assertLatex('1++4'); }); - test('backspacing open-pipe of 1+||+4 (empty pipe pair)', () => { + test('backspacing open-pipe of 1+||+4 (empty pipe pair)', function () { mq.typedText('1+||+4'); assertLatex('1+\\left|\\right|+4'); mq.keystroke('Left Left Left Backspace'); assertLatex('1++4'); }); - test('backspacing close-pipe then open-pipe of 1+|| (empty pipe pair, nothing after)', () => { + test('backspacing close-pipe then open-pipe of 1+|| (empty pipe pair, nothing after)', function () { mq.typedText('1+||'); assertLatex('1+\\left|\\right|'); mq.keystroke('Backspace'); @@ -607,14 +611,14 @@ suite('typing with auto-replaces', () => { assertLatex('1+'); }); - test('backspacing open-pipe of 1+|| (empty pipe pair, nothing after)', () => { + test('backspacing open-pipe of 1+|| (empty pipe pair, nothing after)', function () { mq.typedText('1+||'); assertLatex('1+\\left|\\right|'); mq.keystroke('Left Backspace'); assertLatex('1+'); }); - test('backspacing close-pipe then open-pipe of ||+4 (empty pipe pair, nothing before)', () => { + test('backspacing close-pipe then open-pipe of ||+4 (empty pipe pair, nothing before)', function () { mq.typedText('||+4'); assertLatex('\\left|\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -624,14 +628,14 @@ suite('typing with auto-replaces', () => { assertLatex('+4'); }); - test('backspacing open-pipe of ||+4 (empty pipe pair, nothing before)', () => { + test('backspacing open-pipe of ||+4 (empty pipe pair, nothing before)', function () { mq.typedText('||+4'); assertLatex('\\left|\\right|+4'); mq.keystroke('Left Left Left Backspace'); assertLatex('+4'); }); - test('rendering pipe pair 1+|2+3|+4 from LaTeX then backspacing close-pipe then open-pipe', () => { + test('rendering pipe pair 1+|2+3|+4 from LaTeX then backspacing close-pipe then open-pipe', function () { mq.latex('1+\\left|2+3\\right|+4'); assertLatex('1+\\left|2+3\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -640,7 +644,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('rendering pipe pair 1+|2+3|+4 from LaTeX then backspacing open-pipe', () => { + test('rendering pipe pair 1+|2+3|+4 from LaTeX then backspacing open-pipe', function () { mq.latex('1+\\left|2+3\\right|+4'); assertLatex('1+\\left|2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); @@ -650,7 +654,7 @@ suite('typing with auto-replaces', () => { test( 'rendering mismatched paren/pipe group 1+|2+3)+4 from LaTeX ' + 'then backspacing close-paren then open-pipe', - () => { + function () { mq.latex('1+\\left|2+3\\right)+4'); assertLatex('1+\\left|2+3\\right)+4'); mq.keystroke('Left Left Backspace'); @@ -660,7 +664,7 @@ suite('typing with auto-replaces', () => { } ); - test('rendering mismatched paren/pipe group 1+|2+3)+4 from LaTeX then backspacing open-pipe', () => { + test('rendering mismatched paren/pipe group 1+|2+3)+4 from LaTeX then backspacing open-pipe', function () { mq.latex('1+\\left|2+3\\right)+4'); assertLatex('1+\\left|2+3\\right)+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); @@ -670,7 +674,7 @@ suite('typing with auto-replaces', () => { test( 'rendering mismatched paren/pipe group 1+(2+3|+4 from LaTeX ' + 'then backspacing close-pipe then open-paren', - () => { + function () { mq.latex('1+\\left(2+3\\right|+4'); assertLatex('1+\\left(2+3\\right|+4'); mq.keystroke('Left Left Backspace'); @@ -680,14 +684,14 @@ suite('typing with auto-replaces', () => { } ); - test('rendering mismatched paren/pipe group 1+(2+3|+4 from LaTeX then backspacing open-paren', () => { + test('rendering mismatched paren/pipe group 1+(2+3|+4 from LaTeX then backspacing open-paren', function () { mq.latex('1+\\left(2+3\\right|+4'); assertLatex('1+\\left(2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left Backspace'); assertLatex('1+2+3+4'); }); - test('wrapping selection in pipes 1+|2+3|+4 then backspacing open-pipe', () => { + test('wrapping selection in pipes 1+|2+3|+4 then backspacing open-pipe', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left Shift-Left Shift-Left Shift-Left').typedText('|'); @@ -696,7 +700,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('wrapping selection in pipes 1+|2+3|+4 then backspacing close-pipe then open-pipe', () => { + test('wrapping selection in pipes 1+|2+3|+4 then backspacing close-pipe then open-pipe', function () { mq.typedText('1+2+3+4'); assertLatex('1+2+3+4'); mq.keystroke('Left Left Shift-Left Shift-Left Shift-Left').typedText('|'); @@ -707,7 +711,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing close-pipe of 1+|2+3| (nothing after) then typing', () => { + test('backspacing close-pipe of 1+|2+3| (nothing after) then typing', function () { mq.typedText('1+|2+3|'); assertLatex('1+\\left|2+3\\right|'); mq.keystroke('Backspace'); @@ -716,7 +720,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+\\left|2+3+4\\right|'); }); - test('backspacing open-pipe of |2+3|+4 (nothing before) then typing', () => { + test('backspacing open-pipe of |2+3|+4 (nothing before) then typing', function () { mq.typedText('|2+3|+4'); assertLatex('\\left|2+3\\right|+4'); mq.keystroke('Home Right Backspace'); @@ -725,7 +729,7 @@ suite('typing with auto-replaces', () => { assertLatex('1+2+3+4'); }); - test('backspacing pipe containing a one-sided pipe 0+|1+|2+3||+4', () => { + test('backspacing pipe containing a one-sided pipe 0+|1+|2+3||+4', function () { mq.typedText('0+|1+2+3|+4'); assertLatex('0+\\left|1+2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left').typedText('|'); @@ -734,7 +738,7 @@ suite('typing with auto-replaces', () => { assertLatex('0+1+\\left|2+3\\right|+4'); }); - test('backspacing pipe inside a one-sided pipe 0+|1+|2+3|+4|', () => { + test('backspacing pipe inside a one-sided pipe 0+|1+|2+3|+4|', function () { mq.typedText('0+1+|2+3|+4'); assertLatex('0+1+\\left|2+3\\right|+4'); mq.keystroke('Home Right Right').typedText('|'); @@ -743,7 +747,7 @@ suite('typing with auto-replaces', () => { assertLatex('0+\\left|1+2+3\\right|+4'); }); - test('backspacing pipe containing and inside a one-sided pipe |0+|1+|2+3||+4|', () => { + test('backspacing pipe containing and inside a one-sided pipe |0+|1+|2+3||+4|', function () { mq.typedText('0+|1+2+3|+4'); assertLatex('0+\\left|1+2+3\\right|+4'); mq.keystroke('Home').typedText('|'); @@ -754,7 +758,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\left|0+1+\\left|2+3\\right|+4\\right|'); }); - test('backspacing pipe containing a one-sided pipe facing same way 0+||1+2||+3', () => { + test('backspacing pipe containing a one-sided pipe facing same way 0+||1+2||+3', function () { mq.typedText('0+|1+2|+3'); assertLatex('0+\\left|1+2\\right|+3'); mq.keystroke('Home Right Right Right').typedText('|'); @@ -763,7 +767,7 @@ suite('typing with auto-replaces', () => { assertLatex('0+\\left|\\left|1+2\\right|+3\\right|'); }); - test('backspacing pipe inside a one-sided pipe facing same way 0+|1+|2+3|+4|', () => { + test('backspacing pipe inside a one-sided pipe facing same way 0+|1+|2+3|+4|', function () { mq.typedText('0+1+|2+3|+4'); assertLatex('0+1+\\left|2+3\\right|+4'); mq.keystroke('Home Right Right').typedText('|'); @@ -772,7 +776,7 @@ suite('typing with auto-replaces', () => { assertLatex('0+\\left|1+\\left|2+3+4\\right|\\right|'); }); - test('backspacing open-paren of mismatched paren/pipe group containing a one-sided pipe 0+(1+|2+3||+4', () => { + test('backspacing open-paren of mismatched paren/pipe group containing a one-sided pipe 0+(1+|2+3||+4', function () { mq.latex('0+\\left(1+2+3\\right|+4'); assertLatex('0+\\left(1+2+3\\right|+4'); mq.keystroke('Left Left Left Left Left Left').typedText('|'); @@ -781,7 +785,7 @@ suite('typing with auto-replaces', () => { assertLatex('0+1+\\left|2+3\\right|+4'); }); - test('backspacing open-paren of mismatched paren/pipe group inside a one-sided pipe 0+|1+(2+3|+4|', () => { + test('backspacing open-paren of mismatched paren/pipe group inside a one-sided pipe 0+|1+(2+3|+4|', function () { mq.latex('0+1+\\left(2+3\\right|+4'); assertLatex('0+1+\\left(2+3\\right|+4'); mq.keystroke('Home Right Right').typedText('|'); @@ -792,8 +796,8 @@ suite('typing with auto-replaces', () => { }); } - suite('typing outside ghost paren', () => { - test('typing outside ghost paren solidifies ghost 1+(2+3)', () => { + suite('typing outside ghost paren', function () { + test('typing outside ghost paren solidifies ghost 1+(2+3)', function () { mq.typedText('1+(2+3'); assertLatex('1+\\left(2+3\\right)'); const bracket = mq.__controller.cursor.parent?.parent; @@ -810,7 +814,7 @@ suite('typing with auto-replaces', () => { assert.ok(!bracket.elements.children().last.classList.contains('mq-ghost')); }); - test('selected and replaced by LiveFraction solidifies ghosts (1+2)/( )', () => { + test('selected and replaced by LiveFraction solidifies ghosts (1+2)/( )', function () { mq.typedText('1+2)/'); assertLatex('\\frac{\\left(1+2\\right)}{ }'); const bracket = mq.__controller.cursor.parent?.parent?.ends.left?.ends.left; @@ -824,7 +828,7 @@ suite('typing with auto-replaces', () => { assert.ok(bracket.elements.children().last.classList.contains('mq-ghost')); }); - test('close paren group by typing close-bracket outside ghost paren (1+2]', () => { + test('close paren group by typing close-bracket outside ghost paren (1+2]', function () { mq.typedText('(1+2'); assertLatex('\\left(1+2\\right)'); const bracket = mq.__controller.cursor.parent?.parent; @@ -835,7 +839,7 @@ suite('typing with auto-replaces', () => { assert.ok(!bracket.elements.children().last.classList.contains('mq-ghost')); }); - test('close adjacent paren group before containing paren group (1+(2+3])', () => { + test('close adjacent paren group before containing paren group (1+(2+3])', function () { mq.typedText('(1+(2+3'); assertLatex('\\left(1+\\left(2+3\\right)\\right)'); mq.keystroke('Right').typedText(']'); @@ -844,15 +848,15 @@ suite('typing with auto-replaces', () => { assertLatex('\\left(1+\\left(2+3\\right]\\right]'); }); - test('can type close-bracket on solid side of one-sided paren [](1+2)', () => { + test('can type close-bracket on solid side of one-sided paren [](1+2)', function () { mq.typedText('(1+2'); assertLatex('\\left(1+2\\right)'); mq.moveToLeftEnd().typedText(']'); assertLatex('\\left[\\right]\\left(1+2\\right)'); }); - suite('pipes', () => { - test('close pipe pair from outside to the right |1+2|', () => { + suite('pipes', function () { + test('close pipe pair from outside to the right |1+2|', function () { mq.typedText('|1+2'); assertLatex('\\left|1+2\\right|'); mq.keystroke('Right').typedText('|'); @@ -861,7 +865,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\left|1+2\\right|'); }); - test('close pipe pair from outside to the left |1+2|', () => { + test('close pipe pair from outside to the left |1+2|', function () { mq.typedText('|1+2|'); assertLatex('\\left|1+2\\right|'); mq.keystroke('Home Delete'); @@ -872,7 +876,7 @@ suite('typing with auto-replaces', () => { assertLatex('\\left|1+2\\right|'); }); - test('can type pipe on solid side of one-sided pipe ||||', () => { + test('can type pipe on solid side of one-sided pipe ||||', function () { mq.typedText('|'); assertLatex('\\left|\\right|'); mq.moveToLeftEnd().typedText('|'); @@ -882,15 +886,15 @@ suite('typing with auto-replaces', () => { }); }); - suite('autoCommands', () => { - setup(() => { + suite('autoCommands', function () { + setup(function () { mq.config({ autoOperatorNames: 'ker pp', autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot' }); }); - test('individual commands', () => { + test('individual commands', function () { mq.typedText('sum' + 'n=0'); mq.keystroke('Up').typedText('100').keystroke('Right'); assertLatex('\\sum_{n=0}^{100}'); @@ -932,7 +936,7 @@ suite('typing with auto-replaces', () => { mq.keystroke('Backspace'); }); - test('sequences of auto-commands and other assorted characters', () => { + test('sequences of auto-commands and other assorted characters', function () { mq.typedText('ker' + 'pi'); assertLatex('\\ker\\pi'); mq.keystroke('Left Backspace'); @@ -953,25 +957,29 @@ suite('typing with auto-replaces', () => { assertLatex('\\ker\\pi'); }); - test('has lower "precedence" than operator names', () => { + test('has lower "precedence" than operator names', function () { mq.typedText('ppi'); assertLatex('\\operatorname{pp}i'); mq.keystroke('Left Left').typedText('i'); assertLatex('\\pi pi'); }); - test('command contains non-letters', () => assert.throws(() => MQ.config({ autoCommands: 'e1' }))); + test('command contains non-letters', function () { + assert.throws(() => MQ.config({ autoCommands: 'e1' })); + }); - test('command length less than 2', () => assert.throws(() => MQ.config({ autoCommands: 'e' }))); + test('command length less than 2', function () { + assert.throws(() => MQ.config({ autoCommands: 'e' })); + }); - test('command is a built-in operator name', () => { + test('command is a built-in operator name', function () { const cmds = 'Pr arg det dim gcd hom ker lg lim max min sup limsup liminf injlim projlim Pr'.split(' '); for (const cmd of cmds) { assert.throws(() => MQ.config({ autoCommands: cmd }), `MQ.config({ autoCommands: "${cmd}" })`); } }); - test('built-in operator names even after auto-operator names overridden', () => { + test('built-in operator names even after auto-operator names overridden', function () { MQ.config({ autoOperatorNames: 'dim hom ker hcf hcfe' }); // ^ happen to be the ones required by autoOperatorNames.test.js const cmds = 'Pr arg det gcd lg lim max min sup'.split(' '); @@ -981,7 +989,7 @@ suite('typing with auto-replaces', () => { }); }); - suite('inequalities', () => { + suite('inequalities', function () { // assertFullyFunctioningInequality() checks not only that the inequality // has the right LaTeX and when you backspace it has the right LaTeX, // but also that when you backspace you get the right state such that @@ -998,7 +1006,7 @@ suite('typing with auto-replaces', () => { mq.keystroke('Backspace'); assertLatex(''); } - test('typing and backspacing <=, >=, and !=', () => { + test('typing and backspacing <=, >=, and !=', function () { mq.typedText('<'); assertLatex('<'); mq.typedText('='); @@ -1018,7 +1026,7 @@ suite('typing with auto-replaces', () => { assertLatex('<<>\\ge=>><\\le=!\\ne='); }); - test('typing ≤, ≥, and ≠ chars directly', () => { + test('typing ≤, ≥, and ≠ chars directly', function () { mq.typedText('≤'); assertFullyFunctioningInequality('\\le', '<'); @@ -1029,8 +1037,8 @@ suite('typing with auto-replaces', () => { assertFullyFunctioningInequality('\\ne', '!'); }); - suite('rendered from LaTeX', () => { - test('control sequences', () => { + suite('rendered from LaTeX', function () { + test('control sequences', function () { mq.latex('\\le'); assertFullyFunctioningInequality('\\le', '<'); @@ -1041,7 +1049,7 @@ suite('typing with auto-replaces', () => { assertFullyFunctioningInequality('\\ne', '!'); }); - test('≤, ≥, and ≠ chars', () => { + test('≤, ≥, and ≠ chars', function () { mq.latex('≤'); assertFullyFunctioningInequality('\\le', '<'); @@ -1054,8 +1062,8 @@ suite('typing with auto-replaces', () => { }); }); - suite('SupSub behavior options', () => { - test('charsThatBreakOutOfSupSub', () => { + suite('SupSub behavior options', function () { + test('charsThatBreakOutOfSupSub', function () { assert.equal(mq.typedText('x^2n+y').latex(), 'x^{2n+y}'); mq.latex(''); assert.equal(mq.typedText('x^+2n').latex(), 'x^{+2n}'); @@ -1085,7 +1093,7 @@ suite('typing with auto-replaces', () => { mq.latex(''); }); - test('supSubsRequireOperand', () => { + test('supSubsRequireOperand', function () { assert.equal(mq.typedText('^').latex(), '^{ }'); assert.equal(mq.typedText('2').latex(), '^2'); assert.equal(mq.typedText('n').latex(), '^{2n}'); @@ -1134,8 +1142,8 @@ suite('typing with auto-replaces', () => { }); }); - suite('alternative symbols when typing / and *', () => { - test('typingSlashWritesDivisionSymbol', () => { + suite('alternative symbols when typing / and *', function () { + test('typingSlashWritesDivisionSymbol', function () { mq.typedText('/'); assertLatex('\\frac{ }{ }'); @@ -1144,7 +1152,7 @@ suite('typing with auto-replaces', () => { mq.keystroke('Backspace').typedText('/'); assertLatex('\\div'); }); - test('typingAsteriskWritesTimesSymbol', () => { + test('typingAsteriskWritesTimesSymbol', function () { mq.typedText('*'); assertLatex('\\cdot'); diff --git a/test/updown.test.js b/test/updown.test.js index eae5c733..d963bbe4 100644 --- a/test/updown.test.js +++ b/test/updown.test.js @@ -1,8 +1,8 @@ -/* global suite, test, assert, setup, MQ */ +/* global assert, MQ */ -suite('up/down', () => { +suite('up/down', function () { let mq, rootBlock, controller, cursor; - setup(() => { + setup(function () { const el = document.createElement('span'); document.getElementById('mock')?.append(el); mq = MQ.MathField(el); @@ -11,7 +11,7 @@ suite('up/down', () => { cursor = controller.cursor; }); - test('up/down in out of exponent', () => { + test('up/down in out of exponent', function () { controller.renderLatexMath('x^{nm}'); const exp = rootBlock.ends.right, expBlock = exp.ends.left; @@ -45,7 +45,7 @@ suite('up/down', () => { }); // literally just swapped up and down, exponent with subscript, nm with 12 - test('up/down in out of subscript', () => { + test('up/down in out of subscript', function () { controller.renderLatexMath('a_{12}'); const sub = rootBlock.ends.right, subBlock = sub.ends.left; @@ -78,7 +78,7 @@ suite('up/down', () => { assert.equal(cursor.right, sub, 'cursor up in middle of subscript puts cursor before subscript'); }); - test('up/down into and within fraction', () => { + test('up/down into and within fraction', function () { controller.renderLatexMath('\\frac{12}{34}'); const frac = rootBlock.ends.left, numer = frac.ends.left, @@ -117,7 +117,7 @@ suite('up/down', () => { assert.equal(cursor.left, undefined, 'cursor down from left of fraction inserts at left end of denominator'); }); - test('nested subscripts and fractions', () => { + test('nested subscripts and fractions', function () { controller.renderLatexMath('\\frac{d}{dx_{\\frac{24}{36}0}}\\sqrt{x}=x^{\\frac{1}{2}}'); const exp = rootBlock.ends.right, expBlock = exp.ends.left, @@ -193,7 +193,7 @@ suite('up/down', () => { ); }); - test('\\MathQuillMathField{} in a fraction', () => { + test('\\MathQuillMathField{} in a fraction', function () { const el = document.createElement('span'); el.textContent = '\\frac{\\MathQuillMathField{n}}{2}'; document.getElementById('mock')?.append(el); From fea75a2186b9d10ad0430fbd3b9f0c77ae7a7fcd Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 1 Nov 2024 10:28:09 -0500 Subject: [PATCH 08/19] Add mathspeak (essentially copied from Desmos' MathQuill fork) --- public/unit-test.html | 140 +++++++++++- src/abstractFields.ts | 26 ++- src/commands/math.ts | 3 + src/commands/math/LatexCommandInput.ts | 15 +- src/commands/math/advancedSymbols.ts | 295 +++++++++++++------------ src/commands/math/basicSymbols.ts | 92 ++++++-- src/commands/math/commands.ts | 179 ++++++++++++--- src/commands/mathBlock.ts | 62 +++++- src/commands/mathElements.ts | 220 ++++++++++++++++-- src/commands/textElements.ts | 69 +++++- src/constants.ts | 12 + src/controller.ts | 230 +++++++++++++++++-- src/css/mixins/display.less | 17 ++ src/cursor.ts | 6 +- src/mixins.ts | 2 + src/selection.ts | 4 +- src/services/aria.ts | 83 +++++++ src/services/focusBlur.ts | 2 + src/services/latex.ts | 1 + src/services/mouse.ts | 96 ++++---- src/services/textarea.ts | 49 +++- src/tree/node.ts | 112 +++++++--- test/aria.test.js | 209 ++++++++++++++++++ test/index.js | 1 + 24 files changed, 1604 insertions(+), 321 deletions(-) create mode 100644 src/services/aria.ts create mode 100644 test/aria.test.js diff --git a/public/unit-test.html b/public/unit-test.html index 4f980556..344e6fe3 100644 --- a/public/unit-test.html +++ b/public/unit-test.html @@ -12,12 +12,10 @@ - + @@ -40,9 +38,12 @@

Unit Tests

const mock = document.getElementById('mock'); while (mock.firstChild) mock.firstChild.remove(); }); - const runner = mocha.run(); if (post_xunit_to) { + let xunit = ''; + Mocha.process.stdout.write = (line) => (xunit += line); + const runner = mocha.run(); + // the following is based on // https://github.com/saucelabs-sample-scripts/JavaScript/blob/ // 4946c5cf0ab7325dce5562881dba7c28e30989e5/reporting_mocha.js @@ -67,11 +68,136 @@

Unit Tests

}); runner.on('end', () => { - $.post(post_xunit_to, xunit).complete(() => { + fetch(post_xunit_to, { method: 'post', body: xunit }).then(() => { window.mochaResults = runner.stats; window.mochaResults.reports = failedTests; }); }); + } else { + const json = location.search.indexOf('json') >= 0; + const listTests = location.search.indexOf('listTests') >= 0; + const suiteMap = {}; + const runner = mocha.run(); + + runner.on('suite', (suite) => { + const title = xmlEscape(suite.fullTitle()); + suiteMap[title] = { + assertions: [] + }; + if (listTests) { + suiteMap[title].assertions.push({ + elapsedTime: 0, + timestamp: 0, + result: true, + message: 'okay' + }); + } + }); + + runner.on('pass', (test) => { + if (!listTests) { + const title = getTestSuiteTitle(test); + const elapsedTime = test.duration / 1000; + const timestamp = Date.now(); + suiteMap[title].assertions.push({ + elapsedTime: elapsedTime, + timestamp: timestamp, + result: true, + message: xmlEscape(test.title) + }); + } + }); + + runner.on('fail', (test, err) => { + if (!listTests) { + const title = getTestSuiteTitle(test); + const elapsedTime = test.duration / 1000; + const timestamp = Date.now(); + suiteMap[title].assertions.push({ + elapsedTime: elapsedTime, + timestamp: timestamp, + result: false, + message: xmlEscape(err.message), + stacktrace: xmlEscape(err.stack), + expected: true, + actual: false + }); + } + }); + + runner.on('end', () => { + const moduleResults = []; + for (const suiteTitle in suiteMap) { + if (suiteMap.hasOwnProperty(suiteTitle)) { + const suiteResults = suiteMap[suiteTitle]; + let duration = 0; + for (const assertion of suiteResults.assertions) duration += assertion.elapsedTime; + moduleResults.push({ + name: suiteTitle, + assertions: suiteResults.assertions, + time: duration + }); + } + } + const testResults = { + modules: { mathquill: moduleResults }, + passes: runner.stats.passes, + failures: runner.stats.failures, + skips: 0 + }; + if (json) window.testResultsString = JSON.stringify(testResults, null, 2); + else window.testResultsString = outputXML(testResults); + }); + + const getTestSuiteTitle = (test) => xmlEscape(test.parent.fullTitle()); + + // must escape a few symbols in xml attributes: + // http://stackoverflow.com/questions/866706/ + // which-characters-are-invalid-unless-encoded-in-an-xml-attribute + const xmlEscape = (string) => { + if (typeof string !== 'string') return ''; + string = string || ''; + string = string.replace(/&/g, '&'); + string = string.replace(/"/g, '"'); + string = string.replace(/ { + const xml = []; + xml.push(''); + xml.push(''); + + for (const moduleName in results.modules) { + const module = results.modules[moduleName]; + for (const test of module) { + xml.push(``); + + for (const assertion of test.assertions) { + const assertionMessage = assertion.message || 'no-assertion-message'; + const assertionTime = assertion.elapsedTime; + + xml.push(``); + + if (assertion.result === false) { + xml.push(``); + xml.push(`Expected: ${assertion.expected}\n`); + xml.push(`Actual: ${assertion.actual}\n`); + xml.push(`Stacktrace: ${assertion.stacktrace}`); + xml.push(''); + } else if (assertion.result === undefined) { + xml.push(''); + } + + xml.push(''); + } + + xml.push(''); + } + } + + return xml.join('\n'); + }; } diff --git a/src/abstractFields.ts b/src/abstractFields.ts index 78104c5f..0d6139d3 100644 --- a/src/abstractFields.ts +++ b/src/abstractFields.ts @@ -35,6 +35,7 @@ export class AbstractMathQuill { const rootEl = document.createElement('span'); rootEl.classList.add('mq-root-block'); rootEl.setAttribute(mqBlockId, root.id.toString()); + rootEl.setAttribute('aria-hidden', 'true'); root.elements.add(rootEl); el.append(rootEl); @@ -97,6 +98,19 @@ export class AbstractMathQuill { this.__controller.root.postOrder('reflow'); return this; } + + setAriaLabel(ariaLabel: string) { + this.__controller.setAriaLabel(ariaLabel); + return this; + } + + getAriaLabel() { + return this.__controller.getAriaLabel(); + } + + mathspeak() { + return this.__controller.exportMathSpeak(); + } } export class EditableField extends AbstractMathQuill { @@ -161,9 +175,7 @@ export class EditableField extends AbstractMathQuill { } select() { - const ctrlr = this.__controller; - ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (ctrlr.cursor.left) ctrlr.selectLeft(); + this.__controller.selectAll(); return this; } @@ -226,4 +238,12 @@ export class EditableField extends AbstractMathQuill { this.__controller.cursor.options.ignoreNextMousedown = fn; return this; } + + setAriaPostLabel(ariaPostLabel: string, timeout?: number) { + this.__controller.setAriaPostLabel(ariaPostLabel, timeout); + return this; + } + getAriaPostLabel() { + return this.__controller.getAriaPostLabel(); + } } diff --git a/src/commands/math.ts b/src/commands/math.ts index 7c2bf40b..75f263fe 100644 --- a/src/commands/math.ts +++ b/src/commands/math.ts @@ -45,6 +45,7 @@ export class StaticMath extends AbstractMathQuill { __mathquillify() { super.__mathquillify('mq-math-mode'); + this.__controller.setupStaticField(); if (this.__options.mouseEvents) { this.__controller.delegateMouseEvents(); this.__controller.staticMathTextareaEvents(); @@ -62,6 +63,8 @@ export class StaticMath extends AbstractMathQuill { (this.innerFields = new Store()), InnerMathField ); + // Force an ARIA label update to remain in sync with the new LaTeX value. + this.__controller.updateMathspeak(); } return returned; } diff --git a/src/commands/math/LatexCommandInput.ts b/src/commands/math/LatexCommandInput.ts index 72864f0e..7499a943 100644 --- a/src/commands/math/LatexCommandInput.ts +++ b/src/commands/math/LatexCommandInput.ts @@ -47,16 +47,21 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { leftEnd.write = (cursor: Cursor, ch: string) => { cursor.show().deleteSelection(); - if (/[a-z]/i.exec(ch)) new VanillaSymbol(ch).createLeftOf(cursor); - else { - (leftEnd.parent as LatexCommandInput).renderCommand(cursor); + if (/[a-z]/i.exec(ch)) { + new VanillaSymbol(ch).createLeftOf(cursor); + cursor.controller.aria.alert(ch); + } else { + const cmd = (leftEnd.parent as LatexCommandInput).renderCommand(cursor); + cursor.controller.aria.queue(cmd.mathspeak({ createdLeftOf: cursor })); if (ch !== '\\' || !leftEnd.isEmpty()) cursor.parent?.write(cursor, ch); + else cursor.controller.aria.alert(); } }; leftEnd.keystroke = (key: string, e: KeyboardEvent, ctrlr: Controller) => { if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') { - (leftEnd.parent as LatexCommandInput).renderCommand(ctrlr.cursor); + const cmd = (leftEnd.parent as LatexCommandInput).renderCommand(ctrlr.cursor); + ctrlr.aria.alert(cmd.mathspeak({ createdLeftOf: ctrlr.cursor })); e.preventDefault(); return; } @@ -103,12 +108,14 @@ CharCmds['\\'] = class LatexCommandInput extends MathCommand { const cmd = new LatexCmds[latex](latex); if (this._replacedFragment) cmd.replaces(this._replacedFragment); cmd.createLeftOf(cursor); + return cmd; } else { const cmd = new TextBlock(); cmd.replaces(latex); cmd.createLeftOf(cursor); cursor.insRightOf(cmd); if (this._replacedFragment) this._replacedFragment.remove(); + return cmd; } } }; diff --git a/src/commands/math/advancedSymbols.ts b/src/commands/math/advancedSymbols.ts index 8a025ebb..59b3b713 100644 --- a/src/commands/math/advancedSymbols.ts +++ b/src/commands/math/advancedSymbols.ts @@ -11,7 +11,7 @@ LatexCmds.notin = LatexCmds.otimes = class extends BinaryOperator { constructor(latex: string) { - super(`\\${latex} `, `&${latex};`); + super(`\\${latex} `, `&${latex};`, 'not in'); } }; @@ -20,36 +20,39 @@ LatexCmds['\u2217'] = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = - bindMixin(BinaryOperator, '\\ast ', '∗'); + bindMixin(BinaryOperator, '\\ast ', '∗', 'asterisk'); LatexCmds.therefor = LatexCmds.therefore = bindMixin(BinaryOperator, '\\therefore ', '∴'); LatexCmds.cuz = // l33t LatexCmds.because = bindMixin(BinaryOperator, '\\because ', '∵'); -LatexCmds.prop = LatexCmds.propto = bindMixin(BinaryOperator, '\\propto ', '∝'); +LatexCmds.prop = LatexCmds.propto = bindMixin(BinaryOperator, '\\propto ', '∝', 'proportional to'); -LatexCmds['\u2248'] = LatexCmds.asymp = LatexCmds.approx = bindMixin(BinaryOperator, '\\approx ', '≈'); +LatexCmds['\u2248'] = + LatexCmds.asymp = + LatexCmds.approx = + bindMixin(BinaryOperator, '\\approx ', '≈', 'approximately equal to'); -LatexCmds.isin = LatexCmds.in = bindMixin(BinaryOperator, '\\in ', '∈'); +LatexCmds.isin = LatexCmds.in = bindMixin(BinaryOperator, '\\in ', '∈', 'is in'); -LatexCmds.ni = LatexCmds.contains = bindMixin(BinaryOperator, '\\ni ', '∋'); +LatexCmds.ni = LatexCmds.contains = bindMixin(BinaryOperator, '\\ni ', '∋', 'contains'); LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain = - bindMixin(BinaryOperator, '\\not\\ni ', '∌'); + bindMixin(BinaryOperator, '\\not\\ni ', '∌', 'does not contain'); -LatexCmds.sub = LatexCmds.subset = bindMixin(BinaryOperator, '\\subset ', '⊂'); +LatexCmds.sub = LatexCmds.subset = bindMixin(BinaryOperator, '\\subset ', '⊂', 'subset'); -LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = bindMixin(BinaryOperator, '\\supset ', '⊃'); +LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = bindMixin(BinaryOperator, '\\supset ', '⊃', 'superset'); LatexCmds.nsub = LatexCmds.notsub = LatexCmds.nsubset = LatexCmds.notsubset = - bindMixin(BinaryOperator, '\\not\\subset ', '⊄'); + bindMixin(BinaryOperator, '\\not\\subset ', '⊄', 'not a subset'); LatexCmds.nsup = LatexCmds.notsup = @@ -57,13 +60,13 @@ LatexCmds.nsup = LatexCmds.notsupset = LatexCmds.nsuperset = LatexCmds.notsuperset = - bindMixin(BinaryOperator, '\\not\\supset ', '⊅'); + bindMixin(BinaryOperator, '\\not\\supset ', '⊅', 'not a superset'); LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq = - bindMixin(BinaryOperator, '\\subseteq ', '⊆'); + bindMixin(BinaryOperator, '\\subseteq ', '⊆', 'subset or equal to'); LatexCmds.supe = LatexCmds.supeq = @@ -71,7 +74,7 @@ LatexCmds.supe = LatexCmds.supseteq = LatexCmds.supersete = LatexCmds.superseteq = - bindMixin(BinaryOperator, '\\supseteq ', '⊇'); + bindMixin(BinaryOperator, '\\supseteq ', '⊇', 'superset or equal to'); LatexCmds.nsube = LatexCmds.nsubeq = @@ -81,7 +84,7 @@ LatexCmds.nsube = LatexCmds.nsubseteq = LatexCmds.notsubsete = LatexCmds.notsubseteq = - bindMixin(BinaryOperator, '\\not\\subseteq ', '⊈'); + bindMixin(BinaryOperator, '\\not\\subseteq ', '⊈', 'not subset or equal to'); LatexCmds.nsupe = LatexCmds.nsupeq = @@ -95,7 +98,7 @@ LatexCmds.nsupe = LatexCmds.nsuperseteq = LatexCmds.notsupersete = LatexCmds.notsuperseteq = - bindMixin(BinaryOperator, '\\not\\supseteq ', '⊉'); + bindMixin(BinaryOperator, '\\not\\supseteq ', '⊉', 'not superset or equal to'); // The canonical sets of numbers LatexCmds.mathbb = class extends MathCommand { @@ -122,7 +125,7 @@ LatexCmds.mathbb = class extends MathCommand { } }; -LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = bindMixin(VanillaSymbol, '\\mathbb{N}', 'ℕ'); +LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = bindMixin(VanillaSymbol, '\\mathbb{N}', 'ℕ', 'naturals'); LatexCmds.P = LatexCmds.primes = @@ -131,13 +134,16 @@ LatexCmds.P = LatexCmds.Projective = LatexCmds.probability = LatexCmds.Probability = - bindMixin(VanillaSymbol, '\\mathbb{P}', 'ℙ'); + bindMixin(VanillaSymbol, '\\mathbb{P}', 'ℙ', 'P'); -LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = bindMixin(VanillaSymbol, '\\mathbb{Z}', 'ℤ'); +LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = bindMixin(VanillaSymbol, '\\mathbb{Z}', 'ℤ', 'integers'); -LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals = bindMixin(VanillaSymbol, '\\mathbb{Q}', 'ℚ'); +LatexCmds.Q = + LatexCmds.rationals = + LatexCmds.Rationals = + bindMixin(VanillaSymbol, '\\mathbb{Q}', 'ℚ', 'rationals'); -LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = bindMixin(VanillaSymbol, '\\mathbb{R}', 'ℝ'); +LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = bindMixin(VanillaSymbol, '\\mathbb{R}', 'ℝ', 'reals'); LatexCmds.C = LatexCmds.complex = @@ -147,17 +153,17 @@ LatexCmds.C = LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane = - bindMixin(VanillaSymbol, '\\mathbb{C}', 'ℂ'); + bindMixin(VanillaSymbol, '\\mathbb{C}', 'ℂ', 'comlexes'); LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions = - bindMixin(VanillaSymbol, '\\mathbb{H}', 'ℍ'); + bindMixin(VanillaSymbol, '\\mathbb{H}', 'ℍ', 'quaternions'); // spacing -LatexCmds.quad = LatexCmds.emsp = bindMixin(VanillaSymbol, '\\quad ', ' '); -LatexCmds.qquad = bindMixin(VanillaSymbol, '\\qquad ', ' '); +LatexCmds.quad = LatexCmds.emsp = bindMixin(VanillaSymbol, '\\quad ', ' ', '4 spaces'); +LatexCmds.qquad = bindMixin(VanillaSymbol, '\\qquad ', ' ', '8 spaces'); // spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow // case ',': // return VanillaSymbol('\\, ',' '); @@ -170,74 +176,74 @@ LatexCmds.qquad = bindMixin(VanillaSymbol, '\\qquad ', ' '); // binary operators LatexCmds.diamond = bindMixin(VanillaSymbol, '\\diamond ', '◇'); -LatexCmds.bigtriangleup = bindMixin(VanillaSymbol, '\\bigtriangleup ', '△'); -LatexCmds.ominus = bindMixin(VanillaSymbol, '\\ominus ', '⊖'); -LatexCmds.uplus = bindMixin(VanillaSymbol, '\\uplus ', '⊎'); -LatexCmds.bigtriangledown = bindMixin(VanillaSymbol, '\\bigtriangledown ', '▽'); -LatexCmds.sqcap = bindMixin(VanillaSymbol, '\\sqcap ', '⊓'); -LatexCmds.triangleleft = bindMixin(VanillaSymbol, '\\triangleleft ', '⊲'); -LatexCmds.sqcup = bindMixin(VanillaSymbol, '\\sqcup ', '⊔'); -LatexCmds.triangleright = bindMixin(VanillaSymbol, '\\triangleright ', '⊳'); +LatexCmds.bigtriangleup = bindMixin(VanillaSymbol, '\\bigtriangleup ', '△', 'big triangle up'); +LatexCmds.ominus = bindMixin(VanillaSymbol, '\\ominus ', '⊖', 'o minus'); +LatexCmds.uplus = bindMixin(VanillaSymbol, '\\uplus ', '⊎', 'disjoint union'); +LatexCmds.bigtriangledown = bindMixin(VanillaSymbol, '\\bigtriangledown ', '▽', 'big triangle down'); +LatexCmds.sqcap = bindMixin(VanillaSymbol, '\\sqcap ', '⊓', 'square cap'); +LatexCmds.triangleleft = bindMixin(VanillaSymbol, '\\triangleleft ', '⊲', 'triangle left'); +LatexCmds.sqcup = bindMixin(VanillaSymbol, '\\sqcup ', '⊔', 'square cup'); +LatexCmds.triangleright = bindMixin(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 = bindMixin(VanillaSymbol, '\\odot ', '⊙'); -LatexCmds.bigcirc = bindMixin(VanillaSymbol, '\\bigcirc ', '◯'); -LatexCmds.dagger = bindMixin(VanillaSymbol, '\\dagger ', '†'); -LatexCmds.ddagger = bindMixin(VanillaSymbol, '\\ddagger ', '‡'); -LatexCmds.wr = bindMixin(VanillaSymbol, '\\wr ', '≀'); -LatexCmds.amalg = bindMixin(VanillaSymbol, '\\amalg ', '∐'); +LatexCmds.odot = LatexCmds.circledot = bindMixin(VanillaSymbol, '\\odot ', '⊙', 'circle dot'); +LatexCmds.bigcirc = bindMixin(VanillaSymbol, '\\bigcirc ', '◯', 'big circle'); +LatexCmds.dagger = bindMixin(VanillaSymbol, '\\dagger ', '†', 'dagger'); +LatexCmds.ddagger = bindMixin(VanillaSymbol, '\\ddagger ', '‡', 'big dagger'); +LatexCmds.wr = bindMixin(VanillaSymbol, '\\wr ', '≀', 'wreath'); +LatexCmds.amalg = bindMixin(VanillaSymbol, '\\amalg ', '∐', 'amalgam'); // relationship symbols LatexCmds.models = bindMixin(VanillaSymbol, '\\models ', '⊨'); -LatexCmds.prec = bindMixin(VanillaSymbol, '\\prec ', '≺'); -LatexCmds.succ = bindMixin(VanillaSymbol, '\\succ ', '≻'); -LatexCmds.preceq = bindMixin(VanillaSymbol, '\\preceq ', '≼'); -LatexCmds.succeq = bindMixin(VanillaSymbol, '\\succeq ', '≽'); -LatexCmds.simeq = bindMixin(VanillaSymbol, '\\simeq ', '≃'); -LatexCmds.mid = bindMixin(VanillaSymbol, '\\mid ', '∣'); -LatexCmds.ll = bindMixin(VanillaSymbol, '\\ll ', '≪'); -LatexCmds.gg = bindMixin(VanillaSymbol, '\\gg ', '≫'); -LatexCmds.parallel = bindMixin(VanillaSymbol, '\\parallel ', '∥'); -LatexCmds.nparallel = bindMixin(VanillaSymbol, '\\nparallel ', '∦'); +LatexCmds.prec = bindMixin(VanillaSymbol, '\\prec ', '≺', 'precedes'); +LatexCmds.succ = bindMixin(VanillaSymbol, '\\succ ', '≻', 'succeeds'); +LatexCmds.preceq = bindMixin(VanillaSymbol, '\\preceq ', '≼', 'precedes or equals'); +LatexCmds.succeq = bindMixin(VanillaSymbol, '\\succeq ', '≽', 'succeeds or equals'); +LatexCmds.simeq = bindMixin(VanillaSymbol, '\\simeq ', '≃', 'similar or equal to'); +LatexCmds.mid = bindMixin(VanillaSymbol, '\\mid ', '∣', 'divides'); +LatexCmds.ll = bindMixin(VanillaSymbol, '\\ll ', '≪', 'sufficiently less than'); +LatexCmds.gg = bindMixin(VanillaSymbol, '\\gg ', '≫', 'sufficiently greater than'); +LatexCmds.parallel = bindMixin(VanillaSymbol, '\\parallel ', '∥', 'parallel to'); +LatexCmds.nparallel = bindMixin(VanillaSymbol, '\\nparallel ', '∦', 'not parallel to'); LatexCmds.bowtie = bindMixin(VanillaSymbol, '\\bowtie ', '⋈'); -LatexCmds.sqsubset = bindMixin(VanillaSymbol, '\\sqsubset ', '⊏'); -LatexCmds.sqsupset = bindMixin(VanillaSymbol, '\\sqsupset ', '⊐'); +LatexCmds.sqsubset = bindMixin(VanillaSymbol, '\\sqsubset ', '⊏', 'square subset'); +LatexCmds.sqsupset = bindMixin(VanillaSymbol, '\\sqsupset ', '⊐', 'square superset'); LatexCmds.smile = bindMixin(VanillaSymbol, '\\smile ', '⌣'); -LatexCmds.sqsubseteq = bindMixin(VanillaSymbol, '\\sqsubseteq ', '⊑'); -LatexCmds.sqsupseteq = bindMixin(VanillaSymbol, '\\sqsupseteq ', '⊒'); -LatexCmds.doteq = bindMixin(VanillaSymbol, '\\doteq ', '≐'); +LatexCmds.sqsubseteq = bindMixin(VanillaSymbol, '\\sqsubseteq ', '⊑', 'square subset or equal to'); +LatexCmds.sqsupseteq = bindMixin(VanillaSymbol, '\\sqsupseteq ', '⊒', 'square superset or equal to'); +LatexCmds.doteq = bindMixin(VanillaSymbol, '\\doteq ', '≐', 'dotted equals'); LatexCmds.frown = bindMixin(VanillaSymbol, '\\frown ', '⌢'); -LatexCmds.vdash = bindMixin(VanillaSymbol, '\\vdash ', '⊦'); -LatexCmds.dashv = bindMixin(VanillaSymbol, '\\dashv ', '⊣'); -LatexCmds.nless = bindMixin(VanillaSymbol, '\\nless ', '≮'); -LatexCmds.ngtr = bindMixin(VanillaSymbol, '\\ngtr ', '≯'); +LatexCmds.vdash = bindMixin(VanillaSymbol, '\\vdash ', '⊦', 'v dash'); +LatexCmds.dashv = bindMixin(VanillaSymbol, '\\dashv ', '⊣', 'dash v'); +LatexCmds.nless = bindMixin(VanillaSymbol, '\\nless ', '≮', 'not less than'); +LatexCmds.ngtr = bindMixin(VanillaSymbol, '\\ngtr ', '≯', 'not greater than'); // arrows -LatexCmds.longleftarrow = bindMixin(VanillaSymbol, '\\longleftarrow ', '←'); -LatexCmds.longrightarrow = bindMixin(VanillaSymbol, '\\longrightarrow ', '→'); -LatexCmds.Longleftarrow = bindMixin(VanillaSymbol, '\\Longleftarrow ', '⇐'); -LatexCmds.Longrightarrow = bindMixin(VanillaSymbol, '\\Longrightarrow ', '⇒'); -LatexCmds.longleftrightarrow = bindMixin(VanillaSymbol, '\\longleftrightarrow ', '↔'); -LatexCmds.updownarrow = bindMixin(VanillaSymbol, '\\updownarrow ', '↕'); -LatexCmds.Longleftrightarrow = bindMixin(VanillaSymbol, '\\Longleftrightarrow ', '⇔'); -LatexCmds.Updownarrow = bindMixin(VanillaSymbol, '\\Updownarrow ', '⇕'); -LatexCmds.mapsto = bindMixin(VanillaSymbol, '\\mapsto ', '↦'); -LatexCmds.nearrow = bindMixin(VanillaSymbol, '\\nearrow ', '↗'); -LatexCmds.hookleftarrow = bindMixin(VanillaSymbol, '\\hookleftarrow ', '↩'); -LatexCmds.hookrightarrow = bindMixin(VanillaSymbol, '\\hookrightarrow ', '↪'); -LatexCmds.searrow = bindMixin(VanillaSymbol, '\\searrow ', '↘'); -LatexCmds.leftharpoonup = bindMixin(VanillaSymbol, '\\leftharpoonup ', '↼'); -LatexCmds.rightharpoonup = bindMixin(VanillaSymbol, '\\rightharpoonup ', '⇀'); -LatexCmds.swarrow = bindMixin(VanillaSymbol, '\\swarrow ', '↙'); -LatexCmds.leftharpoondown = bindMixin(VanillaSymbol, '\\leftharpoondown ', '↽'); -LatexCmds.rightharpoondown = bindMixin(VanillaSymbol, '\\rightharpoondown ', '⇁'); -LatexCmds.nwarrow = bindMixin(VanillaSymbol, '\\nwarrow ', '↖'); +LatexCmds.longleftarrow = bindMixin(VanillaSymbol, '\\longleftarrow ', '←', 'left arrow'); +LatexCmds.longrightarrow = bindMixin(VanillaSymbol, '\\longrightarrow ', '→', 'right arrow'); +LatexCmds.Longleftarrow = bindMixin(VanillaSymbol, '\\Longleftarrow ', '⇐', 'left arrow'); +LatexCmds.Longrightarrow = bindMixin(VanillaSymbol, '\\Longrightarrow ', '⇒', 'right arrow'); +LatexCmds.longleftrightarrow = bindMixin(VanillaSymbol, '\\longleftrightarrow ', '↔', 'left and right arrow'); +LatexCmds.updownarrow = bindMixin(VanillaSymbol, '\\updownarrow ', '↕', 'up and down arrow'); +LatexCmds.Longleftrightarrow = bindMixin(VanillaSymbol, '\\Longleftrightarrow ', '⇔', 'left and right arrow'); +LatexCmds.Updownarrow = bindMixin(VanillaSymbol, '\\Updownarrow ', '⇕', 'up and down arrow'); +LatexCmds.mapsto = bindMixin(VanillaSymbol, '\\mapsto ', '↦', 'maps to'); +LatexCmds.nearrow = bindMixin(VanillaSymbol, '\\nearrow ', '↗', 'northeast arrow'); +LatexCmds.hookleftarrow = bindMixin(VanillaSymbol, '\\hookleftarrow ', '↩', 'hook left arrow'); +LatexCmds.hookrightarrow = bindMixin(VanillaSymbol, '\\hookrightarrow ', '↪', 'hook right arrow'); +LatexCmds.searrow = bindMixin(VanillaSymbol, '\\searrow ', '↘', 'southeast arrow'); +LatexCmds.leftharpoonup = bindMixin(VanillaSymbol, '\\leftharpoonup ', '↼', 'left harpoon up'); +LatexCmds.rightharpoonup = bindMixin(VanillaSymbol, '\\rightharpoonup ', '⇀', 'right harpoon up'); +LatexCmds.swarrow = bindMixin(VanillaSymbol, '\\swarrow ', '↙', 'southwest arrow'); +LatexCmds.leftharpoondown = bindMixin(VanillaSymbol, '\\leftharpoondown ', '↽', 'left harpoon down'); +LatexCmds.rightharpoondown = bindMixin(VanillaSymbol, '\\rightharpoondown ', '⇁', 'right harpoon down'); +LatexCmds.nwarrow = bindMixin(VanillaSymbol, '\\nwarrow ', '↖', 'northwest arrow'); // Misc -LatexCmds.ldots = bindMixin(VanillaSymbol, '\\ldots ', '…'); -LatexCmds.cdots = bindMixin(VanillaSymbol, '\\cdots ', '⋯'); -LatexCmds.vdots = bindMixin(VanillaSymbol, '\\vdots ', '⋮'); -LatexCmds.ddots = bindMixin(VanillaSymbol, '\\ddots ', '⋱'); -LatexCmds.surd = bindMixin(VanillaSymbol, '\\surd ', '√'); +LatexCmds.ldots = bindMixin(VanillaSymbol, '\\ldots ', '…', 'ellipsis'); +LatexCmds.cdots = bindMixin(VanillaSymbol, '\\cdots ', '⋯', 'multiplication ellipsis'); +LatexCmds.vdots = bindMixin(VanillaSymbol, '\\vdots ', '⋮', 'vertical ellipsis'); +LatexCmds.ddots = bindMixin(VanillaSymbol, '\\ddots ', '⋱', 'diagonal ellipsis'); +LatexCmds.surd = bindMixin(VanillaSymbol, '\\surd ', '√', 'unresolved root'); LatexCmds.triangle = bindMixin(VanillaSymbol, '\\triangle ', '△'); LatexCmds.ell = bindMixin(VanillaSymbol, '\\ell ', 'ℓ'); LatexCmds.top = bindMixin(VanillaSymbol, '\\top ', '⊤'); @@ -246,55 +252,58 @@ LatexCmds.natural = bindMixin(VanillaSymbol, '\\natural ', '♮'); LatexCmds.sharp = bindMixin(VanillaSymbol, '\\sharp ', '♯'); LatexCmds.wp = bindMixin(VanillaSymbol, '\\wp ', '℘'); LatexCmds.bot = bindMixin(VanillaSymbol, '\\bot ', '⊥'); -LatexCmds.clubsuit = bindMixin(VanillaSymbol, '\\clubsuit ', '♣'); -LatexCmds.diamondsuit = bindMixin(VanillaSymbol, '\\diamondsuit ', '♢'); -LatexCmds.heartsuit = bindMixin(VanillaSymbol, '\\heartsuit ', '♡'); -LatexCmds.spadesuit = bindMixin(VanillaSymbol, '\\spadesuit ', '♠'); +LatexCmds.clubsuit = bindMixin(VanillaSymbol, '\\clubsuit ', '♣', 'club suit'); +LatexCmds.diamondsuit = bindMixin(VanillaSymbol, '\\diamondsuit ', '♢', 'diamond suit'); +LatexCmds.heartsuit = bindMixin(VanillaSymbol, '\\heartsuit ', '♡', 'heart suit'); +LatexCmds.spadesuit = bindMixin(VanillaSymbol, '\\spadesuit ', '♠', 'spade suit'); // not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details LatexCmds.parallelogram = bindMixin(VanillaSymbol, '\\parallelogram ', '▱'); LatexCmds.square = bindMixin(VanillaSymbol, '\\square ', '⬜'); // variable-sized -LatexCmds.oint = bindMixin(VanillaSymbol, '\\oint ', '∮'); -LatexCmds.bigcap = bindMixin(VanillaSymbol, '\\bigcap ', '∩'); -LatexCmds.bigcup = bindMixin(VanillaSymbol, '\\bigcup ', '∪'); -LatexCmds.bigsqcup = bindMixin(VanillaSymbol, '\\bigsqcup ', '⊔'); -LatexCmds.bigvee = bindMixin(VanillaSymbol, '\\bigvee ', '∨'); -LatexCmds.bigwedge = bindMixin(VanillaSymbol, '\\bigwedge ', '∧'); -LatexCmds.bigodot = bindMixin(VanillaSymbol, '\\bigodot ', '⊙'); -LatexCmds.bigotimes = bindMixin(VanillaSymbol, '\\bigotimes ', '⊗'); -LatexCmds.bigoplus = bindMixin(VanillaSymbol, '\\bigoplus ', '⊕'); -LatexCmds.biguplus = bindMixin(VanillaSymbol, '\\biguplus ', '⊎'); +LatexCmds.oint = bindMixin(VanillaSymbol, '\\oint ', '∮', 'o int'); +LatexCmds.bigcap = bindMixin(VanillaSymbol, '\\bigcap ', '∩', 'big cap'); +LatexCmds.bigcup = bindMixin(VanillaSymbol, '\\bigcup ', '∪', 'big cup'); +LatexCmds.bigsqcup = bindMixin(VanillaSymbol, '\\bigsqcup ', '⊔', 'big square cup'); +LatexCmds.bigvee = bindMixin(VanillaSymbol, '\\bigvee ', '∨', 'big vee'); +LatexCmds.bigwedge = bindMixin(VanillaSymbol, '\\bigwedge ', '∧', 'big wedge'); +LatexCmds.bigodot = bindMixin(VanillaSymbol, '\\bigodot ', '⊙', 'big o dot'); +LatexCmds.bigotimes = bindMixin(VanillaSymbol, '\\bigotimes ', '⊗', 'big o times'); +LatexCmds.bigoplus = bindMixin(VanillaSymbol, '\\bigoplus ', '⊕', 'big o plus'); +LatexCmds.biguplus = bindMixin(VanillaSymbol, '\\biguplus ', '⊎', 'big u plus'); // delimiters -LatexCmds.lfloor = bindMixin(VanillaSymbol, '\\lfloor ', '⌊'); -LatexCmds.rfloor = bindMixin(VanillaSymbol, '\\rfloor ', '⌋'); -LatexCmds.lceil = bindMixin(VanillaSymbol, '\\lceil ', '⌈'); -LatexCmds.rceil = bindMixin(VanillaSymbol, '\\rceil ', '⌉'); -LatexCmds.opencurlybrace = LatexCmds.lbrace = bindMixin(VanillaSymbol, '\\lbrace ', '{'); -LatexCmds.closecurlybrace = LatexCmds.rbrace = bindMixin(VanillaSymbol, '\\rbrace ', '}'); -LatexCmds.lbrack = bindMixin(VanillaSymbol, '['); -LatexCmds.rbrack = bindMixin(VanillaSymbol, ']'); +LatexCmds.lfloor = bindMixin(VanillaSymbol, '\\lfloor ', '⌊', 'left floor'); +LatexCmds.rfloor = bindMixin(VanillaSymbol, '\\rfloor ', '⌋', 'right floor'); +LatexCmds.lceil = bindMixin(VanillaSymbol, '\\lceil ', '⌈', 'left ceiling'); +LatexCmds.rceil = bindMixin(VanillaSymbol, '\\rceil ', '⌉', 'right ceiling'); +LatexCmds.opencurlybrace = LatexCmds.lbrace = bindMixin(VanillaSymbol, '\\lbrace ', '{', 'left brace'); +LatexCmds.closecurlybrace = LatexCmds.rbrace = bindMixin(VanillaSymbol, '\\rbrace ', '}', 'right brace'); +LatexCmds.lbrack = bindMixin(VanillaSymbol, '[', 'left bracket'); +LatexCmds.rbrack = bindMixin(VanillaSymbol, ']', 'right bracket'); // various symbols -LatexCmds.slash = bindMixin(VanillaSymbol, '/'); -LatexCmds.vert = bindMixin(VanillaSymbol, '|'); -LatexCmds.perp = LatexCmds.perpendicular = bindMixin(VanillaSymbol, '\\perp ', '⊥'); +LatexCmds.slash = bindMixin(VanillaSymbol, '/', '', '', 'slash'); +LatexCmds.vert = bindMixin(VanillaSymbol, '|', '', '', 'vertical bar'); +LatexCmds.perp = LatexCmds.perpendicular = bindMixin(VanillaSymbol, '\\perp ', '⊥', 'perpendicular'); LatexCmds.nabla = LatexCmds.del = bindMixin(VanillaSymbol, '\\nabla ', '∇'); -LatexCmds.hbar = bindMixin(VanillaSymbol, '\\hbar ', 'ℏ'); +LatexCmds.hbar = bindMixin(VanillaSymbol, '\\hbar ', 'ℏ', 'horizontal bar'); // FIXME: \AA is not valid LaTeX in math mode. Neither is \text\AA (which is what this was before). Furthermore, // \text\AA does not parse correctly. Valid LaTeX in math mode without any packages would be \textup{~\AA}, but that // also does not parse correctly. -LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom = bindMixin(VanillaSymbol, '\\AA ', 'Å', '\u00C5'); +LatexCmds.AA = + LatexCmds.Angstrom = + LatexCmds.angstrom = + bindMixin(VanillaSymbol, '\\AA ', 'Å', '\u00C5', 'angstrom'); -LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = bindMixin(VanillaSymbol, '\\circ ', '∘'); +LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = bindMixin(VanillaSymbol, '\\circ ', '∘', 'circle'); LatexCmds.bull = LatexCmds.bullet = bindMixin(VanillaSymbol, '\\bullet ', '•'); -LatexCmds.setminus = LatexCmds.smallsetminus = bindMixin(VanillaSymbol, '\\setminus ', '∖'); +LatexCmds.setminus = LatexCmds.smallsetminus = bindMixin(VanillaSymbol, '\\setminus ', '∖', 'set minus'); -LatexCmds.not = LatexCmds['\u00ac'] = LatexCmds.neg = bindMixin(VanillaSymbol, '\\neg ', '¬'); +LatexCmds.not = LatexCmds['\u00ac'] = LatexCmds.neg = bindMixin(VanillaSymbol, '\\neg ', '¬', '', 'not'); LatexCmds['\u2026'] = LatexCmds.dots = @@ -302,48 +311,57 @@ LatexCmds['\u2026'] = LatexCmds.hellip = LatexCmds.ellipsis = LatexCmds.hellipsis = - bindMixin(VanillaSymbol, '\\dots ', '…'); + bindMixin(VanillaSymbol, '\\dots ', '…', 'ellipsis'); LatexCmds.converges = LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow = - bindMixin(VanillaSymbol, '\\downarrow ', '↓'); + bindMixin(VanillaSymbol, '\\downarrow ', '↓', 'converges with'); LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow = - bindMixin(VanillaSymbol, '\\Downarrow ', '⇓'); + bindMixin(VanillaSymbol, '\\Downarrow ', '⇓', 'down arrow'); -LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = bindMixin(VanillaSymbol, '\\uparrow ', '↑'); +LatexCmds.diverges = + LatexCmds.uarr = + LatexCmds.uparrow = + bindMixin(VanillaSymbol, '\\uparrow ', '↑', 'diverges from'); -LatexCmds.uArr = LatexCmds.Uparrow = bindMixin(VanillaSymbol, '\\Uparrow ', '⇑'); +LatexCmds.uArr = LatexCmds.Uparrow = bindMixin(VanillaSymbol, '\\Uparrow ', '⇑', 'up arrow'); LatexCmds.to = bindMixin(BinaryOperator, '\\to ', '→'); -LatexCmds.rarr = LatexCmds.rightarrow = bindMixin(VanillaSymbol, '\\rightarrow ', '→'); +LatexCmds.rarr = LatexCmds.rightarrow = bindMixin(VanillaSymbol, '\\rightarrow ', '→', 'right arrow'); -LatexCmds.implies = bindMixin(BinaryOperator, '\\Rightarrow ', '⇒'); +LatexCmds.implies = bindMixin(BinaryOperator, '\\Rightarrow ', '⇒', 'implies'); -LatexCmds.rArr = LatexCmds.Rightarrow = bindMixin(VanillaSymbol, '\\Rightarrow ', '⇒'); +LatexCmds.rArr = LatexCmds.Rightarrow = bindMixin(VanillaSymbol, '\\Rightarrow ', '⇒', 'right arrow'); LatexCmds.gets = bindMixin(BinaryOperator, '\\gets ', '←'); -LatexCmds.larr = LatexCmds.leftarrow = bindMixin(VanillaSymbol, '\\leftarrow ', '←'); +LatexCmds.larr = LatexCmds.leftarrow = bindMixin(VanillaSymbol, '\\leftarrow ', '←', 'left arrow'); -LatexCmds.impliedby = bindMixin(BinaryOperator, '\\Leftarrow ', '⇐'); +LatexCmds.impliedby = bindMixin(BinaryOperator, '\\Leftarrow ', '⇐', 'implied by'); -LatexCmds.lArr = LatexCmds.Leftarrow = bindMixin(VanillaSymbol, '\\Leftarrow ', '⇐'); +LatexCmds.lArr = LatexCmds.Leftarrow = bindMixin(VanillaSymbol, '\\Leftarrow ', '⇐', 'left arrow'); -LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow = bindMixin(VanillaSymbol, '\\leftrightarrow ', '↔'); +LatexCmds.harr = + LatexCmds.lrarr = + LatexCmds.leftrightarrow = + bindMixin(VanillaSymbol, '\\leftrightarrow ', '↔', 'left and right arrow'); -LatexCmds.iff = bindMixin(BinaryOperator, '\\Leftrightarrow ', '⇔'); +LatexCmds.iff = bindMixin(BinaryOperator, '\\Leftrightarrow ', '⇔', 'if and only if'); -LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow = bindMixin(VanillaSymbol, '\\Leftrightarrow ', '⇔'); +LatexCmds.hArr = + LatexCmds.lrArr = + LatexCmds.Leftrightarrow = + bindMixin(VanillaSymbol, '\\Leftrightarrow ', '⇔', 'left and right arrow'); -LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bindMixin(VanillaSymbol, '\\Re ', 'ℜ'); +LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bindMixin(VanillaSymbol, '\\Re ', 'ℜ', 'real'); LatexCmds.Im = LatexCmds.imag = @@ -351,7 +369,7 @@ LatexCmds.Im = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = - bindMixin(VanillaSymbol, '\\Im ', 'ℑ'); + bindMixin(VanillaSymbol, '\\Im ', 'ℑ', 'imaginary'); LatexCmds.part = LatexCmds.partial = bindMixin(VanillaSymbol, '\\partial ', '∂'); @@ -359,7 +377,7 @@ LatexCmds.inf = LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = - bindMixin(VanillaSymbol, '\\infty ', '∞', 'inf'); + bindMixin(VanillaSymbol, '\\infty ', '∞', 'inf', 'infinity'); LatexCmds.pounds = bindMixin(VanillaSymbol, '\\pounds ', '£'); @@ -373,13 +391,13 @@ LatexCmds.xist = LatexCmds.xists = LatexCmds.exist = LatexCmds.exists = - bindMixin(VanillaSymbol, '\\exists ', '∃'); + bindMixin(VanillaSymbol, '\\exists ', '∃', 'there exists'); -LatexCmds.nexists = LatexCmds.nexist = bindMixin(VanillaSymbol, '\\nexists ', '∄'); +LatexCmds.nexists = LatexCmds.nexist = bindMixin(VanillaSymbol, '\\nexists ', '∄', 'there is no'); -LatexCmds.and = LatexCmds.land = LatexCmds.wedge = bindMixin(BinaryOperator, '\\wedge ', '∧'); +LatexCmds.and = LatexCmds.land = LatexCmds.wedge = bindMixin(BinaryOperator, '\\wedge ', '∧', 'and'); -LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bindMixin(BinaryOperator, '\\vee ', '∨'); +LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bindMixin(BinaryOperator, '\\vee ', '∨', 'or'); LatexCmds.o = LatexCmds.O = @@ -389,16 +407,19 @@ LatexCmds.o = LatexCmds.Oslash = LatexCmds.nothing = LatexCmds.varnothing = - bindMixin(BinaryOperator, '\\varnothing ', '∅'); + bindMixin(BinaryOperator, '\\varnothing ', '∅', 'empty set'); -LatexCmds.U = LatexCmds.cup = LatexCmds.union = bindMixin(BinaryOperator, '\\cup ', '∪', 'U'); +LatexCmds.U = LatexCmds.cup = LatexCmds.union = bindMixin(BinaryOperator, '\\cup ', '∪', 'U', 'union'); -LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection = bindMixin(BinaryOperator, '\\cap ', '∩'); +LatexCmds.cap = + LatexCmds.intersect = + LatexCmds.intersection = + bindMixin(BinaryOperator, '\\cap ', '∩', 'intersection'); // FIXME: the correct LaTeX would be ^\circ but we can't parse that LatexCmds.deg = LatexCmds.degree = class degree extends VanillaSymbol { constructor() { - super('\\degree ', '°'); + super('\\degree ', '°', '', 'degrees'); } text() { @@ -408,4 +429,4 @@ LatexCmds.deg = LatexCmds.degree = class degree extends VanillaSymbol { }; LatexCmds.ang = LatexCmds.angle = bindMixin(VanillaSymbol, '\\angle ', '∠'); -LatexCmds.measuredangle = bindMixin(VanillaSymbol, '\\measuredangle ', '∡'); +LatexCmds.measuredangle = bindMixin(VanillaSymbol, '\\measuredangle ', '∡', 'measured angle'); diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index e487d6d9..3817958a 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -74,14 +74,14 @@ LatexCmds.f = class extends Letter { }; // VanillaSymbol's -LatexCmds[' '] = LatexCmds.space = bindMixin(VanillaSymbol, '\\ ', ' '); +LatexCmds[' '] = LatexCmds.space = bindMixin(VanillaSymbol, '\\ ', ' ', '', ' '); -LatexCmds["'"] = LatexCmds.prime = bindMixin(VanillaSymbol, "'", '′'); +LatexCmds["'"] = LatexCmds.prime = bindMixin(VanillaSymbol, "'", '′', '', 'prime'); // LatexCmds['\u2033'] = LatexCmds.dprime = bindMixin(VanillaSymbol, '\u2033', '″'); -LatexCmds.backslash = bindMixin(VanillaSymbol, '\\backslash ', '\\'); +LatexCmds.backslash = bindMixin(VanillaSymbol, '\\backslash ', '\\', '', 'backslash'); -LatexCmds.$ = bindMixin(VanillaSymbol, '\\$', '$'); +LatexCmds.$ = bindMixin(VanillaSymbol, '\\$', '$', '', 'dollar'); // does not use Symbola font class NonSymbolaSymbol extends Symbol { @@ -200,9 +200,12 @@ LatexCmds.Gamma = // symbols that aren't a single MathCommand, but are instead a whole // Fragment. Creates the Fragment from a LaTeX string class LatexFragment extends MathCommand { + latexStr: string; + constructor(latex: string) { super(); this.latex = () => latex; + this.latexStr = latex; } createLeftOf(cursor: Cursor) { @@ -216,6 +219,11 @@ class LatexFragment extends MathCommand { cursor.parent?.bubble('reflow'); } + mathspeak() { + const block = latexMathParser.parse(this.latexStr); + return block.mathspeak(); + } + parser() { const block = latexMathParser.parse(this.latex()); return Parser.succeed(block.children()); @@ -254,8 +262,8 @@ LatexCmds['\u00bd'] = bindMixin(LatexFragment, '\\frac12'); LatexCmds['\u00be'] = bindMixin(LatexFragment, '\\frac34'); class PlusMinus extends BinaryOperator { - constructor(ctrlSeq: string, html?: string, text?: string) { - super(ctrlSeq, `${html || ctrlSeq}`, text, true); + constructor(ctrlSeq: string, html?: string, text?: string, mathspeak?: string) { + super(ctrlSeq, `${html || ctrlSeq}`, text, mathspeak, true); this.siblingCreated = this.siblingDeleted = (opts: Options, dir?: Direction) => this.contactWeld(opts, dir); } @@ -287,18 +295,64 @@ class PlusMinus extends BinaryOperator { } } -LatexCmds['+'] = bindMixin(PlusMinus, '+', '+'); -// These are different dashes, I think one is an en dash and the other is a hyphen. -LatexCmds['\u2013'] = LatexCmds['-'] = bindMixin(PlusMinus, '-', '−'); -LatexCmds['\u00b1'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = bindMixin(PlusMinus, '\\pm ', '±'); -LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = bindMixin(PlusMinus, '\\mp ', '∓'); +LatexCmds['+'] = class extends PlusMinus { + constructor() { + super('+', '+'); + } + mathspeak(): string { + return this.isUnary ? 'positive' : 'plus'; + } +}; -// Semantically should be ⋅, but · looks better -CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = bindMixin(BinaryOperator, '\\cdot ', '·', '*'); +LatexCmds['\u2013'] = LatexCmds['-'] = class extends PlusMinus { + constructor() { + super('-', '−'); + } + mathspeak(): string { + return this.isUnary ? 'negative' : 'minus'; + } +}; + +LatexCmds['\u00b1'] = + LatexCmds.pm = + LatexCmds.plusmn = + LatexCmds.plusminus = + bindMixin(PlusMinus, '\\pm ', '±', '', 'plus-or-minus'); +LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = bindMixin(PlusMinus, '\\mp ', '∓', '', 'minus-or-plus'); -const less = { ctrlSeq: '\\le ', html: '≤', text: '<=', ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<' }; -const greater = { ctrlSeq: '\\ge ', html: '≥', text: '>=', ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>' }; -const neq = { ctrlSeq: '\\ne ', html: '≠', text: '!=', ctrlSeqStrict: '!', htmlStrict: '!', textStrict: '!' }; +// Semantically should be ⋅, but · looks better +CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = bindMixin(BinaryOperator, '\\cdot ', '·', '*', 'times'); + +const less = { + ctrlSeq: '\\le ', + html: '≤', + text: '<=', + mathspeak: 'less than or equal to', + ctrlSeqStrict: '<', + htmlStrict: '<', + textStrict: '<', + mathspeakStrict: 'less than' +}; +const greater = { + ctrlSeq: '\\ge ', + html: '≥', + text: '>=', + mathspeak: 'greater than or equal to', + ctrlSeqStrict: '>', + htmlStrict: '>', + textStrict: '>', + mathspeakStrict: 'greater than' +}; +const neq = { + ctrlSeq: '\\ne ', + html: '≠', + text: '!=', + mathspeak: 'not equal to', + ctrlSeqStrict: '!', + htmlStrict: '!', + textStrict: '!', + mathspeakStrict: 'factorial' +}; LatexCmds['<'] = LatexCmds.lt = bindMixin(Inequality, less, true); LatexCmds['>'] = LatexCmds.gt = bindMixin(Inequality, greater, true); @@ -309,12 +363,12 @@ LatexCmds['\u2260'] = LatexCmds.ne = LatexCmds.neq = bindMixin(FactorialOrNEQ, n LatexCmds['='] = Equality; -LatexCmds['\u00d7'] = LatexCmds.times = bindMixin(BinaryOperator, '\\times ', '×', '*'); +LatexCmds['\u00d7'] = LatexCmds.times = bindMixin(BinaryOperator, '\\times ', '×', '*', 'times'); LatexCmds['\u00f7'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = - bindMixin(BinaryOperator, '\\div ', '÷', '/'); + bindMixin(BinaryOperator, '\\div ', '÷', '/', 'divided by'); -CharCmds['~'] = LatexCmds.sim = bindMixin(BinaryOperator, '\\sim ', '~', '~'); +CharCmds['~'] = LatexCmds.sim = bindMixin(BinaryOperator, '\\sim ', '~', '~', 'tilde'); diff --git a/src/commands/math/commands.ts b/src/commands/math/commands.ts index fbdd7511..7e41b91b 100644 --- a/src/commands/math/commands.ts +++ b/src/commands/math/commands.ts @@ -7,7 +7,7 @@ import { Controller } from 'src/controller'; import { Cursor } from 'src/cursor'; import { Parser } from 'services/parser.util'; import { RootBlockMixin, scale, DelimsMixin } from 'src/mixins'; -import type { TNode } from 'tree/node'; +import type { TNode, MathspeakOptions } from 'tree/node'; import { Fragment } from 'tree/fragment'; import type { InnerMathFieldStore } from 'commands/math'; import { InnerMathField } from 'commands/math'; @@ -26,38 +26,73 @@ import { Bracket, latexMathParser, supSubText, - MathFunction + MathFunction, + intRgx, + getCtrlSeqsFromBlock } from 'commands/mathElements'; class Style extends MathCommand { - constructor(ctrlSeq: string, tagName: string, attrs: string) { + shouldNotSpeakDelimiters: boolean | undefined; + + constructor( + ctrlSeq: string, + tagName: string, + attrs: string, + ariaLabel?: string, + shouldNotSpeakDelimiters?: boolean + ) { super(ctrlSeq, `<${tagName} ${attrs}>&0`); + this.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); + this.mathspeakTemplate = [`Start${this.ariaLabel},`, `End${this.ariaLabel}`]; + + // In most cases, mathspeak should announce the start and end of style blocks. + // There is one exception currently (mathrm). + this.shouldNotSpeakDelimiters = shouldNotSpeakDelimiters; + } + + mathspeak(opts?: MathspeakOptions) { + if (!this.shouldNotSpeakDelimiters || opts?.ignoreShorthand) return super.mathspeak(); + return this.foldChildren('', (speech, block) => `${speech} ${block.mathspeak(opts)}`).trim(); } } // fonts -LatexCmds.mathrm = bindMixin(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"'); -LatexCmds.mathit = bindMixin(Style, '\\mathit', 'i', 'class="mq-font"'); -LatexCmds.mathbf = bindMixin(Style, '\\mathbf', 'b', 'class="mq-font"'); -LatexCmds.mathsf = bindMixin(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"'); -LatexCmds.mathtt = bindMixin(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"'); +LatexCmds.mathrm = bindMixin(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', true); +LatexCmds.mathit = bindMixin(Style, '\\mathit', 'i', 'class="mq-font"', 'Italic Font'); +LatexCmds.mathbf = bindMixin(Style, '\\mathbf', 'b', 'class="mq-font"', 'Bold Font'); +LatexCmds.mathsf = bindMixin(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"', 'Serif Font'); +LatexCmds.mathtt = bindMixin(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"', 'Math Text'); // text-decoration -LatexCmds.underline = bindMixin(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"'); -LatexCmds.overline = LatexCmds.bar = bindMixin(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"'); +LatexCmds.underline = bindMixin(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"', 'Underline'); +LatexCmds.overline = LatexCmds.bar = bindMixin( + Style, + '\\overline', + 'span', + 'class="mq-non-leaf mq-overline"', + 'Overline' +); LatexCmds.overrightarrow = bindMixin( Style, '\\overrightarrow', 'span', - 'class="mq-non-leaf mq-overarrow mq-arrow-right"' + 'class="mq-non-leaf mq-overarrow mq-arrow-right"', + 'Over Right Arrow' +); +LatexCmds.overleftarrow = bindMixin( + Style, + '\\overleftarrow', + 'span', + 'class="mq-non-leaf mq-overarrow mq-arrow-left"', + 'Over Left Arrow' ); -LatexCmds.overleftarrow = bindMixin(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"'); LatexCmds.overleftrightarrow = bindMixin( Style, '\\overleftrightarrow', 'span', - 'class="mq-non-leaf mq-overarrow mq-arrow-both"' + 'class="mq-non-leaf mq-overarrow mq-arrow-both"', + 'Over Left and Right Arrow' ); -LatexCmds.overarc = bindMixin(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"'); +LatexCmds.overarc = bindMixin(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"', 'Over Arc'); LatexCmds.dot = class extends MathCommand { constructor() { super( @@ -70,19 +105,15 @@ LatexCmds.dot = class extends MathCommand { } }; -// `\textcolor{color}{math}` will apply a color to the given math content, where -// `color` is any valid CSS Color Value (see [SitePoint docs][] (recommended), -// [Mozilla docs][], or [W3C spec][]). -// -// [SitePoint docs]: http://reference.sitepoint.com/css/colorvalues -// [Mozilla docs]: https://developer.mozilla.org/en-US/docs/CSS/color_value#Values -// [W3C spec]: http://dev.w3.org/csswg/css3-color/#colorunits +// `\textcolor{color}{math}` will apply a color to the given math content, where `color` is any valid CSS Color Value. LatexCmds.textcolor = class extends MathCommand { color?: string; setColor(color: string) { this.color = color; this.htmlTemplate = `&0`; + this.ariaLabel = color.replace(/^\\/, ''); + this.mathspeakTemplate = [`Start ${this.ariaLabel},`, `End ${this.ariaLabel}`]; } latex() { @@ -120,6 +151,8 @@ LatexCmds.class = class extends MathCommand { .then((cls: string) => { this.cls = cls || ''; this.htmlTemplate = `&0`; + this.ariaLabel = cls + ' class'; + this.mathspeakTemplate = [`Start ${this.ariaLabel},`, `End ${this.ariaLabel}`]; return super.parser(); }); } @@ -143,6 +176,8 @@ LatexCmds.subscript = LatexCmds._ = class extends SupSub { '' + '
'; this.textTemplate = ['_']; + this.mathspeakTemplate = ['Subscript,', ', Baseline']; + this.ariaLabel = 'subscript'; } finalizeTree() { @@ -162,6 +197,40 @@ LatexCmds.superscript = this.htmlTemplate = '' + '&0' + ''; this.textTemplate = ['^(', ')']; + this.ariaLabel = 'superscript'; + this.mathspeakTemplate = ['Superscript,', ', Baseline']; + } + + mathspeak(opts?: MathspeakOptions) { + // Simplify basic exponent speech for common whole numbers. + const child = this.upInto; + if (child) { + // Calculate this item's inner text to determine whether to shorten the returned speech. Do not + // calculate its inner mathspeak until it is known that the speech is to be truncated. Since the + // mathspeak computation is recursive, it should be called only once in this function to avoid + // performance bottlenecks. + const innerText = getCtrlSeqsFromBlock(child); + // If the superscript is a whole number, shorten the speech that is returned. + if (!opts?.ignoreShorthand && intRgx.test(innerText)) { + // Simple cases + if (innerText === '0') return 'to the power of 0'; + if (innerText === '1') return 'to the first power'; + else if (innerText === '2') return 'squared'; + else if (innerText === '3') return 'cubed'; + + // More complex cases. + let 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 (innerText.endsWith('1')) suffix = 'st'; + else if (innerText.endsWith('2')) suffix = 'nd'; + else if (innerText.endsWith('3')) suffix = 'rd'; + } + return `to the ${typeof child === 'object' ? child.mathspeak() : innerText}${suffix} power`; + } + } + return super.mathspeak(); } finalizeTree() { @@ -172,7 +241,7 @@ LatexCmds.superscript = }; class SummationNotation extends UpperLowerLimitCommand { - constructor(ch: string, html: string) { + constructor(ch: string, html: string, ariaLabel?: string) { super( ch, '' + @@ -182,6 +251,7 @@ class SummationNotation extends UpperLowerLimitCommand { '', [ch.length > 1 ? ch.slice(1) : ch] ); + this.ariaLabel = ariaLabel || ch.replace(/^\\/, ''); } createLeftOf(cursor: Cursor) { @@ -194,8 +264,8 @@ class SummationNotation extends UpperLowerLimitCommand { } LatexCmds['\u2211'] = LatexCmds.sum = LatexCmds.summation = bindMixin(SummationNotation, '\\sum ', '∑'); -LatexCmds['\u220f'] = LatexCmds.prod = LatexCmds.product = bindMixin(SummationNotation, '\\prod ', '∏'); -LatexCmds.coprod = LatexCmds.coproduct = bindMixin(SummationNotation, '\\coprod ', '∐'); +LatexCmds['\u220f'] = LatexCmds.prod = LatexCmds.product = bindMixin(SummationNotation, '\\prod ', '∏', 'product'); +LatexCmds.coprod = LatexCmds.coproduct = bindMixin(SummationNotation, '\\coprod ', '∐', 'coproduct'); LatexCmds['\u222b'] = LatexCmds.int = @@ -213,6 +283,7 @@ LatexCmds['\u222b'] = '
' + '
' ); + this.ariaLabel = 'integral'; } }; @@ -312,6 +383,8 @@ class SquareRoot extends MathCommand { '&0' + ''; this.textTemplate = ['sqrt(', ')']; + this.mathspeakTemplate = ['StartSquareRoot,', ', EndSquareRoot']; + this.ariaLabel = 'square root'; this.reflow = () => { const block = this.ends.right?.elements.firstElement; @@ -364,6 +437,7 @@ class NthRoot extends SquareRoot { '&1' + ''; this.textTemplate = ['root(', ',', ')']; + this.ariaLabel = 'root'; } latex() { @@ -381,6 +455,15 @@ class NthRoot extends SquareRoot { return `root(${index},${this.ends.right?.text() ?? ''})`; } + + mathspeak() { + const indexMathspeak = this.ends.left?.mathspeak() ?? ''; + const radicandMathspeak = this.ends.right?.mathspeak() ?? ''; + if (this.ends.left) this.ends.left.ariaLabel = 'Index'; + if (this.ends.right) this.ends.right.ariaLabel = 'Radicand'; + if (indexMathspeak === '3') return `Start Cube Root, ${radicandMathspeak}, End Cube Root`; + else return `Root Index ${indexMathspeak}, Start Root, ${radicandMathspeak}, End Root`; + } } LatexCmds.root = LatexCmds.nthroot = NthRoot; @@ -475,6 +558,21 @@ class Binomial extends DelimsMixin(MathCommand) { '', ['choose(', ',', ')'] ); + this.mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial']; + this.ariaLabel = 'binomial'; + } + + finalizeTree() { + this.upInto = this.ends.left; + this.downInto = this.ends.right; + if (this.ends.right) { + this.ends.right.upOutOf = this.ends.left; + this.ends.right.ariaLabel = 'lower index'; + } + if (this.ends.left) { + this.ends.left.downOutOf = this.ends.right; + this.ends.left.ariaLabel = 'upper index'; + } } } LatexCmds.binom = LatexCmds.binomial = Binomial; @@ -489,7 +587,7 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { constructor() { super( '\\MathQuillMathField', - '' + '&0' + '' + '' + '' + '' ); } @@ -513,6 +611,37 @@ LatexCmds.editable = LatexCmds.MathQuillMathField = class extends MathCommand { ctrlr.editablesTextareaEvents(); ctrlr.cursor.insAtRightEnd(ctrlr.root); RootBlockMixin(ctrlr.root as MathElement); + + // MathQuill applies aria-hidden to .mq-root-block containers because these contain math notation that screen + // readers can't interpret directly. MathQuill uses an aria-live region as a sibling of these block containers + // to provide an alternative representation for screen readers. + // + // MathQuillMathFields have their own focusable text aria and aria live region, so it is incorrect for any + // parent of the editable field to have an aria-hidden property. + // + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden + // + // Handle this by recursively walking the parents of this element until we hit a root block, and if we hit any + // parent with aria-hidden="true", removing the property from the parent and pushing it down to each of the + // parents children. This should result in no parent of this node having aria-hidden="true", but should keep as + // much of what was previously hidden hidden as possible while obeying this constraint. + const pushDownAriaHidden = (node: Node | null) => { + if (node?.parentNode && node instanceof Element && !node.classList.contains('mq-root-block')) { + pushDownAriaHidden(node.parentNode); + } + if (node instanceof Element) { + if (node.getAttribute('aria-hidden') === 'true') { + node.removeAttribute('aria-hidden'); + node.childNodes.forEach((child) => { + if (child instanceof Element) child.setAttribute('aria-hidden', 'true'); + }); + } + } + }; + + pushDownAriaHidden(this.elements.first.parentNode); + this.elements.removeClass('aria-hidden'); + this.field.blur(); } diff --git a/src/commands/mathBlock.ts b/src/commands/mathBlock.ts index 155b0da5..a66a4622 100644 --- a/src/commands/mathBlock.ts +++ b/src/commands/mathBlock.ts @@ -38,6 +38,7 @@ export const writeMethodMixin = >(Base: TBase) if (cmd.isSymbol) cursor.deleteSelection(); else cursor.clearSelection().insRightOf(this.parent); cmd.createLeftOf(cursor.show()); + cursor.controller.aria.queue('Baseline').alert(cmd.mathspeak({ createdLeftOf: cursor })); return; } if ( @@ -48,12 +49,18 @@ export const writeMethodMixin = >(Base: TBase) this.parent ) { cursor.insRightOf(this.parent); + cursor.controller.aria.queue('Baseline'); } } const cmd = this.chToCmd(ch, cursor.options); if (cursor.selection) cmd.replaces(cursor.replaceSelection()); - if (!cursor.isTooDeep()) cmd.createLeftOf(cursor.show()); + if (!cursor.isTooDeep()) { + cmd.createLeftOf(cursor.show()); + // There is a special case for the slash so that fractions are voiced while typing. + if (ch === '/') cursor.controller.aria.alert('over'); + else cursor.controller.aria.alert(cmd.mathspeak({ createdLeftOf: cursor })); + } } }; @@ -61,7 +68,9 @@ export const writeMethodMixin = >(Base: TBase) // symbols and operators that descend (in the Math DOM tree) from // ancestor operators. export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { - join(methodName: keyof Pick) { + ariaLabel = 'block'; + + join(methodName: keyof Pick) { return this.foldChildren('', (fold, child) => fold + child[methodName]()); } @@ -77,6 +86,45 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { return this.ends.left && this.ends.left === this.ends.right ? this.ends.left.text() : this.join('text'); } + mathspeak() { + let tempOp = ''; + const autoOps = this.controller ? this.controller.options.autoOperatorNames : { _maxLength: 0 }; + return ( + this.foldChildren([], (speechArray, cmd) => { + if (cmd instanceof Letter && cmd.isPartOfOperator) { + tempOp += cmd.mathspeak(); + } else { + if (tempOp !== '') { + if (autoOps._maxLength > 0) { + const x = autoOps[tempOp.toLowerCase()]; + if (typeof x === 'string') tempOp = x; + } + speechArray.push(tempOp + ' '); + tempOp = ''; + } + let mathspeakText = cmd.mathspeak(); + const cmdText = cmd.ctrlSeq; + if ( + isNaN(cmdText as unknown as number) && // TODO - revisit this to improve the isNumber() check + cmdText !== '.' && + (!cmd.parent?.parent || !(cmd.parent.parent instanceof LatexCmds.mathrm)) + ) { + 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, (_match, p1: string, p2: string) => p1 + p2.split('').join(' ').trim()) + ); + } + keystroke(key: string, e: KeyboardEvent, ctrlr: Controller) { if ( ctrlr.options.spaceBehavesLikeTab && @@ -96,8 +144,14 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { // the cursor moveOutOf(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { const updownInto = updown && this.parent?.[`${updown}Into`]; - if (!updownInto && this[dir]) cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this[dir]); - else if (this.parent) cursor.insDirOf(dir, this.parent); + if (!updownInto && this[dir]) { + cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this[dir]); + if (cursor.parent) + cursor.controller.aria.queueDirEndOf(dir === 'left' ? 'right' : 'left').queue(cursor.parent, true); + } else if (this.parent) { + cursor.insDirOf(dir, this.parent); + cursor.controller.aria.queueDirOf(dir).queue(this.parent); + } } selectOutOf(dir: Direction, cursor: Cursor) { diff --git a/src/commands/mathElements.ts b/src/commands/mathElements.ts index 5c9532ce..42461ab2 100644 --- a/src/commands/mathElements.ts +++ b/src/commands/mathElements.ts @@ -1,7 +1,16 @@ // Abstract classes of math blocks and commands. import type { Direction, Constructor } from 'src/constants'; -import { noop, mqCmdId, mqBlockId, LatexCmds, OPP_BRACKS, BuiltInOpNames, TwoWordOpNames } from 'src/constants'; +import { + noop, + mqCmdId, + mqBlockId, + LatexCmds, + OPP_BRACKS, + BRACKET_NAMES, + BuiltInOpNames, + TwoWordOpNames +} from 'src/constants'; import { Parser } from 'services/parser.util'; import { Selection } from 'src/selection'; import { deleteSelectTowardsMixin, DelimsMixin } from 'src/mixins'; @@ -9,7 +18,7 @@ import type { Options } from 'src/options'; import type { Cursor } from 'src/cursor'; import { Point } from 'tree/point'; import { VNode } from 'tree/vNode'; -import { TNode } from 'tree/node'; +import { TNode, MathspeakOptions } from 'tree/node'; import { Fragment } from 'tree/fragment'; import { MathBlock } from 'commands/mathBlock'; @@ -80,6 +89,7 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { contentIndex = 0; htmlTemplate: string; textTemplate: string[]; + mathspeakTemplate: string[] = ['']; replacedFragment?: Fragment; constructor(ctrlSeq?: string, htmlTemplate?: string, textTemplate?: string[]) { @@ -326,16 +336,30 @@ export class MathCommand extends deleteSelectTowardsMixin(MathElement) { return text + child_text + (this.textTemplate[i] || ''); }); } + + mathspeak() { + let i = 0; + return this.foldChildren( + `${this.mathspeakTemplate[i] || `Start${this.ctrlSeq.replace(/^\\/, '')}`} `, + (speech, block) => { + ++i; + return `${speech} ${block.mathspeak()} ${ + this.mathspeakTemplate[i] || `End${this.ctrlSeq.replace(/^\\/, '')}` + } `; + } + ); + } } // Lightweight command without blocks or children. export class Symbol extends MathCommand { - constructor(ctrlSeq?: string, html?: string, text?: string) { + constructor(ctrlSeq?: string, html?: string, text?: string, mathspeak?: string) { const textTemplate = text ? text : ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : (ctrlSeq ?? ''); super(ctrlSeq, html, [textTemplate]); this.createBlocks = noop; this.isSymbol = true; + this.mathspeakName = mathspeak || text || ctrlSeq?.replace(/^\\/, ''); } parser() { @@ -356,6 +380,8 @@ export class Symbol extends MathCommand { cursor[dir === 'left' ? 'right' : 'left'] = this; cursor[dir] = this[dir]; + + cursor.controller.aria.queue(this); } deleteTowards(dir: Direction, cursor: Cursor) { @@ -384,19 +410,23 @@ export class Symbol extends MathCommand { isEmpty() { return true; } + + mathspeak() { + return this.mathspeakName || ''; + } } export class VanillaSymbol extends Symbol { - constructor(ch: string, html?: string, text?: string) { - super(ch, `${html || ch}`, text); + constructor(ch: string, html?: string, text?: string, mathspeak?: string) { + super(ch, `${html || ch}`, text, mathspeak); } } export class BinaryOperator extends Symbol { isUnary = false; - constructor(ctrlSeq: string, html?: string, text?: string, useRawHtml = false) { - super(ctrlSeq, useRawHtml ? html : `${html ?? ''}`, text); + constructor(ctrlSeq: string, html?: string, text?: string, mathspeak?: string, useRawHtml = false) { + super(ctrlSeq, useRawHtml ? html : `${html ?? ''}`, text, mathspeak); } } @@ -404,9 +434,11 @@ export interface InequalityData { ctrlSeq: string; html: string; text: string; + mathspeak: string; ctrlSeqStrict: string; htmlStrict: string; textStrict: string; + mathspeakStrict: string; } export class Inequality extends BinaryOperator { @@ -415,7 +447,12 @@ export class Inequality extends BinaryOperator { constructor(data: InequalityData, strict: boolean) { const strictness = strict ? 'Strict' : ''; - super(data[`ctrlSeq${strictness}`], data[`html${strictness}`], data[`text${strictness}`]); + super( + data[`ctrlSeq${strictness}`], + data[`html${strictness}`], + data[`text${strictness}`], + data[`mathspeak${strictness}`] + ); this.data = data; this.strict = strict; } @@ -426,6 +463,7 @@ export class Inequality extends BinaryOperator { this.ctrlSeq = this.data[`ctrlSeq${strictness}`]; this.elements.html(this.data[`html${strictness}`]); this.textTemplate = [this.data[`text${strictness}`]]; + this.mathspeakName = this.data[`mathspeak${strictness}`]; } deleteTowards(dir: Direction, cursor: Cursor) { @@ -459,7 +497,7 @@ export class FactorialOrNEQ extends Inequality { export class Equality extends BinaryOperator { constructor() { - super('=', '='); + super('=', '=', '=', 'equals'); } createLeftOf(cursor: Cursor) { @@ -634,6 +672,24 @@ export function insLeftOfMeUnlessAtEnd(this: SupSub, cursor: Cursor) { cursor.insRightOf(cmd); } +// 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. +export const intRgx = /^[+-]?[\d]+$/; + +// Traverses 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. +export const getCtrlSeqsFromBlock = (block: TNode | undefined): string => { + if (!block) return ''; + + let chars = ''; + block.eachChild((child) => { + chars += child.ctrlSeq; + return true; + }); + + return chars; +}; + export class Fraction extends MathCommand { constructor() { super(); @@ -700,9 +756,77 @@ export class Fraction extends MathCommand { finalizeTree() { this.upInto = this.ends.left; - if (this.ends.right) this.ends.right.upOutOf = this.ends.left; + if (this.ends.right) { + this.ends.right.upOutOf = this.ends.left; + this.ends.right.ariaLabel = 'denominator'; + } this.downInto = this.ends.right; - if (this.ends.left) this.ends.left.downOutOf = this.ends.right; + if (this.ends.left) { + this.ends.left.downOutOf = this.ends.right; + this.ends.left.ariaLabel = 'numerator'; + } + + const fracDepth = this.getFracDepth(); + if (fracDepth > 2) { + this.mathspeakTemplate = [ + `StartDepth${fracDepth.toString()}Fraction,`, + `Depth${fracDepth.toString()}Over`, + `, EndDepth${fracDepth.toString()}Fraction` + ]; + } else if (fracDepth > 1) { + this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction']; + } else { + this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction']; + } + } + + mathspeak(opts?: MathspeakOptions) { + if (opts?.createdLeftOf) { + const cursor = opts.createdLeftOf; + return cursor.parent?.mathspeak() ?? ''; + } + + const numText = getCtrlSeqsFromBlock(this.ends.left); + const denText = getCtrlSeqsFromBlock(this.ends.right); + + // Shorten mathspeak value for whole number fractions whose denominator is less than 10. + if (!opts?.ignoreShorthand && intRgx.test(numText) && intRgx.test(denText)) { + const isSingular = numText === '1' || numText === '-1'; + let newDenSpeech = ''; + if (denText === '2') newDenSpeech = isSingular ? 'half' : 'halves'; + else if (denText === '3') newDenSpeech = isSingular ? 'third' : 'thirds'; + else if (denText === '4') newDenSpeech = isSingular ? 'fourth' : 'fourths'; + 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 !== '') { + // 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." + let precededByInteger = false; + for (let sibling = this.left; sibling; sibling = sibling.left) { + // Ignore whitespace + if (sibling.ctrlSeq === '\\ ') continue; + else if (intRgx.test(sibling.ctrlSeq)) precededByInteger = true; + else break; + } + return `${precededByInteger ? 'and ' : ''}${this.ends.left?.mathspeak() ?? ''} ${newDenSpeech}`; + } + } + + return super.mathspeak(); + } + + getFracDepth() { + const level = 0; + const walkUp = function (item: TNode, level: number): number { + if (item instanceof Fraction) ++level; + if (item.parent) return walkUp(item.parent, level); + else return level; + }; + return walkUp(this, level); } } @@ -1056,6 +1180,14 @@ export class UpperLowerLimitCommand extends MathCommand { return `${operand}(${this.ends.left?.text() ?? ''},${this.ends.right?.text() ?? ''})`; } + mathspeak() { + return `Start ${ + this.ariaLabel ?? this.ctrlSeq.replace(/^\\/, '') + } from ${this.ends.left?.mathspeak() ?? ''} to ${ + this.ends.right?.mathspeak() ?? '' + }, End ${this.ariaLabel ?? this.ctrlSeq.replace(/^\\/, '')}, `; + } + parser() { const blocks = (this.blocks = [new MathBlock(), new MathBlock()]); for (const block of blocks) { @@ -1078,8 +1210,14 @@ export class UpperLowerLimitCommand extends MathCommand { finalizeTree() { this.downInto = this.ends.left; this.upInto = this.ends.right; - if (this.ends.left) this.ends.left.upOutOf = this.ends.right; - if (this.ends.right) this.ends.right.downOutOf = this.ends.left; + if (this.ends.left) { + this.ends.left.upOutOf = this.ends.right; + this.ends.left.ariaLabel = 'lower bound'; + } + if (this.ends.right) { + this.ends.right.downOutOf = this.ends.left; + this.ends.right.ariaLabel = 'upper bound'; + } } } @@ -1352,6 +1490,21 @@ export class Bracket extends BracketMixin(MathCommand) { text() { return `${this.sides.left.ch}${this.ends.left?.text() ?? ''}${this.sides.right.ch}`; } + + mathspeak(opts?: MathspeakOptions) { + const open = this.sides.left.ch, + close = this.sides.right.ch; + if (open === '|' && close === '|') { + this.mathspeakTemplate = ['StartAbsoluteValue,', ', EndAbsoluteValue']; + this.ariaLabel = 'absolute value'; + } else if (opts?.createdLeftOf && this.side) { + return `${this.side} ${BRACKET_NAMES[this.side === 'left' ? this.textTemplate[0] : this.textTemplate[1]]}`; + } else { + this.mathspeakTemplate = ['left ' + BRACKET_NAMES[open] + ',', ', right ' + BRACKET_NAMES[close]]; + this.ariaLabel = BRACKET_NAMES[open] + ' block'; + } + return super.mathspeak(); + } } export class MathFunction extends BracketMixin(MathCommand) { @@ -1373,6 +1526,29 @@ export class MathFunction extends BracketMixin(MathCommand) { this.siblingDeleted = () => { this.updateFirst(); }; + + this.setAriaLabel(); + } + + setAriaLabel() { + const baseName = this.ctrlSeq.slice(1).replace(/^arc/, '').replace(/h$/, ''); + this.ariaLabel = + (this.ctrlSeq.endsWith('h') ? 'hyperbolic ' : '') + + (this.ctrlSeq.startsWith('\\arc') ? 'arc' : '') + + ( + { + sin: 'sine', + cos: 'cosine', + tan: 'tangent', + sec: 'secant', + csc: 'cosecant', + cot: 'cotangent', + exp: 'natural exponential', + ln: 'natural logarithm', + log: 'logarithm' + } as Record + )[baseName]; + if (this.ends.right) this.ends.right.ariaLabel = `${this.ariaLabel ?? ''} parameter`; } // Add or remove padding depending on what is before the function name. @@ -1422,6 +1598,7 @@ export class MathFunction extends BracketMixin(MathCommand) { MathFunction ) { this.ctrlSeq = `${this.ctrlSeq}${ch}`; + this.setAriaLabel(); this.elements.children().first.textContent = (this.elements.children().first.textContent ?? '') + ch; this.bubble('reflow'); return true; @@ -1483,10 +1660,15 @@ export class MathFunction extends BracketMixin(MathCommand) { tmpBase.remove(); } + cursor.controller.aria.queue(this.ariaLabel ?? this.ctrlSeq.slice(1)); this.remove(); if (supsub) cursor.insLeftOf(supsub); else if (brack) cursor.insLeftOf(brack); - } else super.deleteSide(side, outward, cursor); + } else { + if (side === 'left') cursor.controller.aria.queue(this); + else cursor.controller.aria.queue('right parenthesis'); + super.deleteSide(side, outward, cursor); + } } finalizeTree() { @@ -1502,7 +1684,9 @@ export class MathFunction extends BracketMixin(MathCommand) { (LatexCmds[this.ctrlSeq.slice(1, -1)] as Constructor | undefined)?.prototype instanceof MathFunction ) { + cursor.controller.aria.queue(this.ctrlSeq.slice(-1)); this.ctrlSeq = this.ctrlSeq.slice(0, -1); + this.setAriaLabel(); this.elements.children().first.textContent = this.ctrlSeq.slice(1); this.bubble('reflow'); return; @@ -1520,6 +1704,8 @@ export class MathFunction extends BracketMixin(MathCommand) { }; } + if (this.ends.right) this.ends.right.ariaLabel = `${this.ariaLabel ?? ''} parameter`; + this.updateFirst(); } @@ -1533,6 +1719,12 @@ export class MathFunction extends BracketMixin(MathCommand) { })`; } + mathspeak() { + return `${ + this.ariaLabel ?? '' + } ${this.blocks[0].mathspeak()} left parenthesis, ${this.blocks[1].mathspeak()}, right parenthesis`; + } + parser() { // Create the other end solid. delete this.side; diff --git a/src/commands/textElements.ts b/src/commands/textElements.ts index 46bca9b6..839ed2e5 100644 --- a/src/commands/textElements.ts +++ b/src/commands/textElements.ts @@ -6,7 +6,7 @@ import { Parser } from 'services/parser.util'; import type { Cursor } from 'src/cursor'; import { Point } from 'tree/point'; import { VNode } from 'tree/vNode'; -import { TNode } from 'tree/node'; +import { TNode, MathspeakOptions } from 'tree/node'; import { Fragment } from 'tree/fragment'; import { VanillaSymbol } from 'commands/mathElements'; import { deleteSelectTowardsMixin } from 'src/mixins'; @@ -19,10 +19,12 @@ import { BlockFocusBlur } from 'services/focusBlur'; export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { replacedText?: string; anticursorPosition = 0; + mathspeakTemplate = ['StartText', 'EndText']; constructor() { super(); this.ctrlSeq = '\\text'; + this.ariaLabel = 'Text'; } replaces(replacedText?: string | Fragment) { @@ -81,15 +83,28 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { return `${this.textContents()}`; } + mathspeak(opts?: MathspeakOptions) { + if (opts?.ignoreShorthand) { + return `${this.mathspeakTemplate[0]}, ${this.textContents()}, ${this.mathspeakTemplate[1]}`; + } else { + return this.textContents(); + } + } + // 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(dir: Direction, cursor: Cursor) { cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this); + if (cursor.parent) + cursor.controller.aria.queueDirEndOf(dir === 'left' ? 'right' : 'left').queue(cursor.parent, true); } + moveOutOf(dir: Direction, cursor: Cursor) { cursor.insDirOf(dir, this); + cursor.controller.aria.queueDirOf(dir).queue(this); } + unselectInto(dir: Direction, cursor: Cursor) { cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', this); @@ -140,6 +155,7 @@ export class TextBlock extends BlockFocusBlur(deleteSelectTowardsMixin(TNode)) { super.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness } this.bubble('reflow'); + cursor.controller.aria.alert(ch); } writeLatex(cursor: Cursor, latex: string) { @@ -319,19 +335,24 @@ export class TextPiece extends TNode { deleteTowards(dir: Direction, cursor: Cursor) { if (this.textStr.length > 1) { + let deletedChar; if (dir === 'right') { this.dom?.deleteData(0, 1); + deletedChar = this.textStr[0]; this.textStr = this.textStr.slice(1); } else { // Note that the order of these 2 lines is important. // (the second line mutates this.textStr.length) this.dom?.deleteData(-1 + this.textStr.length, 1); + deletedChar = this.textStr[this.textStr.length - 1]; this.textStr = this.textStr.slice(0, -1); } + cursor.controller.aria.queue(deletedChar); } else { this.remove(); this.elements.remove(); cursor[dir] = this[dir]; + cursor.controller.aria.queue(this.textStr); } } @@ -361,6 +382,10 @@ export class TextPiece extends TNode { this.deleteTowards(dir, cursor); } + + mathspeak() { + return this.textStr; + } } LatexCmds.text = @@ -371,7 +396,7 @@ LatexCmds.text = LatexCmds.textmd = TextBlock; -const makeTextBlock = (latex: string, tagName: string, attrs: string) => +const makeTextBlock = (latex: string, tagName: string, attrs: string, ariaLabel: string) => class extends TextBlock { htmlTemplate: string; @@ -379,6 +404,7 @@ const makeTextBlock = (latex: string, tagName: string, attrs: string) => super(); this.ctrlSeq = latex; this.htmlTemplate = `<${tagName} ${attrs}>&0`; + this.ariaLabel = ariaLabel; } }; @@ -388,10 +414,35 @@ LatexCmds.em = LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = - makeTextBlock('\\textit', 'i', 'class="mq-text-mode"'); -LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"'); -LatexCmds.sf = LatexCmds.textsf = makeTextBlock('\\textsf', 'span', 'class="mq-sans-serif mq-text-mode"'); -LatexCmds.tt = LatexCmds.texttt = makeTextBlock('\\texttt', 'span', 'class="mq-monospace mq-text-mode"'); -LatexCmds.textsc = makeTextBlock('\\textsc', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); -LatexCmds.uppercase = makeTextBlock('\\uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); -LatexCmds.lowercase = makeTextBlock('\\lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); + makeTextBlock('\\textit', 'i', 'class="mq-text-mode"', 'Italic'); +LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"', 'Bold'); +LatexCmds.sf = LatexCmds.textsf = makeTextBlock( + '\\textsf', + 'span', + 'class="mq-sans-serif mq-text-mode"', + 'Sans serif font' +); +LatexCmds.tt = LatexCmds.texttt = makeTextBlock( + '\\texttt', + 'span', + 'class="mq-monospace mq-text-mode"', + 'Mono space font' +); +LatexCmds.textsc = makeTextBlock( + '\\textsc', + 'span', + 'style="font-variant:small-caps" class="mq-text-mode"', + 'Variable font' +); +LatexCmds.uppercase = makeTextBlock( + '\\uppercase', + 'span', + 'style="text-transform:uppercase" class="mq-text-mode"', + 'Uppercase' +); +LatexCmds.lowercase = makeTextBlock( + '\\lowercase', + 'span', + 'style="text-transform:lowercase" class="mq-text-mode"', + 'Lowercase' +); diff --git a/src/constants.ts b/src/constants.ts index 97d20d2d..87b6d4cc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -106,6 +106,18 @@ export const OPP_BRACKS: Readonly> = { '\\rVert ': '\\lVert ' }; +export const BRACKET_NAMES: Readonly> = { + '⟨': 'angle-bracket', + '⟩': 'angle-bracket', + '|': 'pipe', + '(': 'parenthesis', + ')': 'parenthesis', + '[': 'bracket', + ']': 'bracket', + '{': 'brace', + '}': 'brace' +}; + export interface EmbedOptions { text?: () => string; htmlString?: string; diff --git a/src/controller.ts b/src/controller.ts index e8ea99f5..96acef1a 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -6,12 +6,14 @@ import { Cursor } from 'src/cursor'; import type { AbstractMathQuill } from 'src/abstractFields'; import { TNode } from 'tree/node'; import { Fragment } from 'tree/fragment'; +import { MathCommand, Bracket } from 'commands/mathElements'; import { HorizontalScroll } from 'services/scrollHoriz'; import { LatexControllerExtension } from 'services/latex'; import { MouseEventController } from 'services/mouse'; import { FocusBlurEvents } from 'services/focusBlur'; import { ExportText } from 'services/exportText'; import { TextAreaController } from 'services/textarea'; +import { Aria } from 'services/aria'; export class ControllerBase { id: number; @@ -25,6 +27,11 @@ export class ControllerBase { blurred?: boolean; textareaSpan?: HTMLSpanElement; textarea?: HTMLTextAreaElement; + mathspeakSpan?: HTMLElement; + aria: Aria; + ariaLabel: string; + ariaPostLabel: string; + _ariaAlertTimeout?: ReturnType; constructor(root: TNode, container: HTMLElement, options: Options) { this.id = root.id; @@ -33,7 +40,11 @@ export class ControllerBase { this.container = container; this.options = options; - this.cursor = new Cursor(root, options); + this.cursor = new Cursor(root, options, this); + + this.aria = new Aria(this); + this.ariaLabel = 'Math Input'; + this.ariaPostLabel = ''; } handle(name: keyof Handlers, dir?: Direction) { @@ -57,6 +68,66 @@ export class ControllerBase { selectionChanged() { /* do nothing */ } + + containerHasFocus() { + return document.activeElement && this.container.contains(document.activeElement); + } + + setAriaLabel(ariaLabel: string) { + const oldAriaLabel = this.getAriaLabel(); + + if (typeof ariaLabel === 'string' && ariaLabel !== '') this.ariaLabel = ariaLabel; + else if (this.editable) this.ariaLabel = 'Math Input'; + else this.ariaLabel = ''; + + // If this field does not have focus, update its computed mathspeak value. Check for focus because updating the + // aria-label attribute of a focused element will cause most screen readers to announce the new value. If the + // field does have focus at the time, it will be updated once a blur event occurs. + if (this.ariaLabel !== oldAriaLabel && !this.containerHasFocus()) this.updateMathspeak(); + return this; + } + + getAriaLabel() { + if (this.ariaLabel !== 'Math Input') return this.ariaLabel; + else if (this.editable) return 'Math Input'; + else return ''; + } + + setAriaPostLabel(ariaPostLabel: string, timeout?: number) { + if (typeof ariaPostLabel === 'string' && ariaPostLabel !== '') { + if (ariaPostLabel !== this.ariaPostLabel && typeof timeout === 'number') { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this._ariaAlertTimeout = setTimeout(() => { + if (this.containerHasFocus()) { + // Voice the new label, but do not update mathspeak content to prevent double-speech. + this.aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); + } else { + // This mathquill does not have focus, so update its mathspeak. + this.updateMathspeak(); + } + }, timeout); + } + this.ariaPostLabel = ariaPostLabel; + } else { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this.ariaPostLabel = ''; + } + return this; + } + + getAriaPostLabel() { + return this.ariaPostLabel || ''; + } + + exportMathSpeak() { + return this.root.mathspeak(); + } + + updateMathspeak(_emptyContent = false) { + // This is defined here so that it can be called above without jumping through a lot of hoops to pacify + // typescript. The method defined in services/textarea.ts will override this, and it is what will actually be + // called above. + } } export class Controller extends ExportText( @@ -81,6 +152,7 @@ export class Controller extends ExportText( if (cursor.parent === this.root) return; cursor.parent?.moveOutOf(dir, cursor); + this.aria.alert(); return this.notify('move'); } @@ -144,6 +216,55 @@ export class Controller extends ExportText( if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); const cursor = this.cursor; + // FIXME: This should be done in the methods of the objects that need this. + if (cursor[dir]) { + if (cursor[dir] instanceof Bracket) { + if (cursor[dir].parent) { + this.aria.queue( + cursor[dir].parent + .chToCmd(cursor[dir].sides[dir === 'left' ? 'right' : 'left'].ch, cursor.options) + .mathspeak({ createdLeftOf: cursor }) + ); + } + // 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 ( + cursor[dir] instanceof MathCommand && + !cursor[dir].blocks.length && + cursor[dir].parent?.ctrlSeq !== '\\text' + ) { + this.aria.queue(cursor[dir]); + } + } else if (cursor.parent?.parent && cursor.parent.parent instanceof TNode) { + if (cursor.parent.parent instanceof Bracket) { + if (cursor.parent.parent.parent) { + this.aria.queue( + cursor.parent.parent.parent + .chToCmd(cursor.parent.parent.sides[dir].ch, cursor.options) + .mathspeak({ createdLeftOf: cursor }) + ); + } + } else if ( + cursor.parent.parent instanceof MathCommand && + cursor.parent.parent.blocks.length && + cursor.parent.parent.mathspeakTemplate.length + ) { + if (cursor.parent.parent.upInto && cursor.parent.parent.downInto) { + // likely a fraction, and we just backspaced over the slash + this.aria.queue(cursor.parent.parent.mathspeakTemplate[1]); + } else { + this.aria.queue( + dir === 'left' + ? cursor.parent.parent.mathspeakTemplate[0] + : (cursor.parent.parent.mathspeakTemplate.at(-1) ?? '') + ); + } + } else { + this.aria.queue(cursor.parent.parent); + } + } + const hadSelection = cursor.selection; this.notify('edit'); // Shows the cursor and deletes a selection if present. if (!hadSelection) { @@ -168,11 +289,12 @@ export class Controller extends ExportText( if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); this.notify('edit'); - if (dir === 'left') { - new Fragment(cursor.parent?.ends.left, cursor.left).remove(); - } else { - new Fragment(cursor.right, cursor.parent?.ends.right).remove(); - } + + let fragmentRemoved: Fragment; + if (dir === 'left') fragmentRemoved = new Fragment(cursor.parent?.ends.left, cursor.left).remove(); + else fragmentRemoved = new Fragment(cursor.right, cursor.parent?.ends.right).remove(); + cursor.controller.aria.queue(fragmentRemoved); + if (cursor.parent) cursor.insAtDirEnd(dir, cursor.parent); // Call the contactWeld for a SupSub so that it can deal with having its base deleted. @@ -193,25 +315,82 @@ export class Controller extends ExportText( return this.deleteDir('right'); } - selectDir(dir: Direction | undefined) { - const cursor = this.notify('select').cursor, - seln = cursor.selection; + private incrementalSelectionOpen = false; + + // startIncrementalSelection, selectDirIncremental, and finishIncrementalSelection should only be called by + // withIncrementalSelection because they must be called in sequence. + + // Start a selection. + private startIncrementalSelection() { + if (this.incrementalSelectionOpen) throw new Error('multiple selections cannot be simultaneously open'); + this.incrementalSelectionOpen = true; + this.notify('select'); + if (!this.cursor.anticursor) this.cursor.startSelection(); + } + + // Update the selection model stored in the cursor without modifying the selection DOM. + private selectDirIncremental(dir: Direction | undefined) { + if (!this.incrementalSelectionOpen) throw new Error('a selection is not open'); if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); - if (!cursor.anticursor) cursor.startSelection(); + const cursor = this.cursor, + seln = cursor.selection; const node = cursor[dir]; if (node) { - // "if node we're selecting towards is inside selection (hence retracting) + // if node we're selecting towards is inside selection (hence retracting) // and is on the *far side* of the selection (hence is only node selected) - // and the anticursor is *inside* that node, not just on the other side" + // and the anticursor is *inside* that node, not just on the other side if (seln && seln.ends[dir] === node && cursor.anticursor?.[dir === 'left' ? 'right' : 'left'] !== node) { node.unselectInto(dir, cursor); } else node.selectTowards(dir, cursor); } else cursor.parent?.selectOutOf(dir, cursor); + } - cursor.clearSelection(); - if (!cursor.select()) cursor.show(); + // Update selection DOM to match cursor model. + private finishIncrementalSelection() { + if (!this.incrementalSelectionOpen) throw new Error('a selection is not open'); + this.cursor.clearSelection(); + if (!this.cursor.select()) this.cursor.show(); + if (this.cursor.selection) { + // Clear first. A selection can fire several times, and if not cleared it would result in repeated speech. + this.aria.clear().queue(this.cursor.selection.join('mathspeak', ' ').trim() + ' selected'); + } + this.incrementalSelectionOpen = false; + } + + // Used to build a selection incrementally in a loop. Calls the passed callback with a selectDir function that may + // be called many times, and defers updating the view until the incremental selection is complete. + // + // Wraps up calling + // + // this.startIncrementalSelection() + // this.selectDirIncremental(dir) // possibly many times + // this.finishIncrementalSelection() + // + // with extra error handling and invariant enforcement. + withIncrementalSelection(cb: (selectDir: (dir: Direction) => void) => void) { + try { + this.startIncrementalSelection(); + try { + cb((dir) => { + this.selectDirIncremental(dir); + }); + } finally { + // Since a selection has been started, attempt to finish it even if the callback throws an error. + this.finishIncrementalSelection(); + } + } finally { + // Mark the selection as closed even if finishIncrementalSelection throws an error. Makes a possible error + // in finishIncrementalSelection more recoverable. + this.incrementalSelectionOpen = false; + } + } + + selectDir(dir: Direction) { + this.withIncrementalSelection((selectDir) => { + selectDir(dir); + }); } selectLeft() { @@ -221,4 +400,27 @@ export class Controller extends ExportText( selectRight() { this.selectDir('right'); } + + selectAll() { + this.notify('move').cursor.insAtRightEnd(this.root); + while (this.cursor.left) this.selectLeft(); + this.withIncrementalSelection((selectDir) => { + while (this.cursor.left) selectDir('left'); + }); + } + + selectToBlockEndInDir(dir: Direction) { + this.withIncrementalSelection((selectDir) => { + while (this.cursor[dir]) selectDir(dir); + }); + } + + selectToRootEndInDir(dir: Direction) { + const cursor = this.cursor; + this.withIncrementalSelection((selectDir) => { + while (cursor[dir] || cursor.parent !== this.root) { + selectDir(dir); + } + }); + } } diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index e4d5dc81..c5f12521 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -1,3 +1,20 @@ .inline-block () { display: inline-block; } + +.mq-aria-alert { + position: absolute; + left: -1000px; + top: -1000px; + width: 0; + height: 0; +} + +.mq-mathspeak { + visibility: hidden; + position: absolute; + left: -1000px; + top: -1000px; + width: 0; + height: 0; +} diff --git a/src/cursor.ts b/src/cursor.ts index 4fd601a6..c0ce0907 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -10,8 +10,10 @@ import { Point } from 'tree/point'; import type { TNode } from 'tree/node'; import type { Selection } from 'src/selection'; import { MathBlock } from 'commands/mathBlock'; +import { ControllerBase } from './controller'; export class Cursor extends Point { + controller: ControllerBase; options: Options; element: HTMLElement = document.createElement('span'); upDownCache: Record = {}; @@ -23,8 +25,9 @@ export class Cursor extends Point { // Closured for setInterval blink: () => void = () => this.element.classList.toggle('mq-blink'); - constructor(initParent: TNode, options: Options) { + constructor(initParent: TNode, options: Options, controller: ControllerBase) { super(initParent); + this.controller = controller; this.options = options; this.element.classList.add('mq-cursor'); this.element.textContent = '\u200B'; @@ -118,6 +121,7 @@ export class Cursor extends Point { } else { to.seek(this.offset().left, this); } + this.controller.aria.queue(to, true); } offset() { diff --git a/src/mixins.ts b/src/mixins.ts index 73af3e82..71a14c1f 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -26,6 +26,8 @@ export const deleteSelectTowardsMixin = >(Base: moveTowards(dir: Direction, cursor: Cursor, updown?: 'up' | 'down') { const nodeAtEnd = (updown && this[`${updown}Into`]) || this.ends[dir === 'left' ? 'right' : 'left']; if (nodeAtEnd) cursor.insAtDirEnd(dir === 'left' ? 'right' : 'left', nodeAtEnd); + if (cursor.parent) + cursor.controller.aria.queueDirEndOf(dir === 'left' ? 'right' : 'left').queue(cursor.parent, true); } deleteTowards(dir: Direction, cursor: Cursor) { diff --git a/src/selection.ts b/src/selection.ts index 5768c807..b40e7ef9 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -29,7 +29,7 @@ export class Selection extends Fragment { return this; } - join(methodName: keyof Pick) { - return this.fold('', (fold, child) => fold + child[methodName]()); + join(methodName: keyof Pick, separator = '') { + return this.fold('', (fold, child) => fold + separator + child[methodName]()); } } diff --git a/src/services/aria.ts b/src/services/aria.ts new file mode 100644 index 00000000..a4b12a1a --- /dev/null +++ b/src/services/aria.ts @@ -0,0 +1,83 @@ +// Add the capability for MathQuill to generate ARIA alerts. + +import type { Direction } from 'src/constants'; +import type { ControllerBase } from 'src/controller'; +import { TNode } from 'tree/node'; +import type { Fragment } from 'tree/fragment'; + +type AriaQueueItem = TNode | Fragment | string; + +export class Aria { + controller: ControllerBase; + span = document.createElement('span'); + msg = ''; + items: AriaQueueItem[] = []; + + constructor(controller: ControllerBase) { + this.controller = controller; + this.span.classList.add('mq-aria-alert'); + this.span.setAttribute('aria-live', 'assertive'); + this.span.setAttribute('aria-atomic', 'true'); + } + + attach() { + const container = this.controller.container; + if (this.span.parentNode !== container) container.prepend(this.span); + } + + queue(item: AriaQueueItem, shouldDescribe = false) { + let output: Fragment | string = ''; + if (item instanceof TNode) { + // 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. + const itemMathspeak = item.mathspeak({ ignoreShorthand: true }); + if (shouldDescribe) { + // Used to ensure item is described when cursor reaches block boundaries. + if (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(dir: Direction | undefined) { + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); + return this.queue(dir === 'left' ? 'before' : 'after'); + } + queueDirEndOf(dir: Direction | undefined) { + if (dir !== 'left' && dir !== 'right') throw new Error('a direction was not passed'); + return this.queue(dir === 'left' ? 'beginning of' : 'end of'); + } + + alert(t?: AriaQueueItem) { + this.attach(); + if (t) this.queue(t); + if (this.items.length) { + // To cut down on potential verbiage from multiple Mathquills firing near-simultaneous ARIA alerts, update + // the text of this instance if its container also has keyboard focus. If it does not, leave the DOM + // unchanged but flush the queue regardless. + // Note: The msg variable is updated regardless of focus for unit tests. + this.msg = this.items + .join(' ') + .replace(/ +(?= )/g, '') + .trim(); + if (this.controller.containerHasFocus()) this.span.textContent = this.msg; + } + return this.clear(); + } + + // Clear out the internal alert message queue. If emptyContent is set, also clear the text content for the alert + // element (typically when the focused field has been blurred) so that stale alert text is not hanging around. + clear(emptyContent = false) { + this.items.length = 0; + if (emptyContent) this.span.textContent = ''; + return this; + } +} diff --git a/src/services/focusBlur.ts b/src/services/focusBlur.ts index 16e2c302..0bf2438a 100644 --- a/src/services/focusBlur.ts +++ b/src/services/focusBlur.ts @@ -11,6 +11,7 @@ export const FocusBlurEvents = >(Base: focusBlurEvents() { this.focusHandler = () => { + this.updateMathspeak(); this.blurred = false; this.container.classList.add('mq-focused'); if (!this.cursor.parent) this.cursor.insAtRightEnd(this.root); @@ -27,6 +28,7 @@ export const FocusBlurEvents = >(Base: this.container.classList.remove('mq-focused'); this.cursor.hide().parent?.blur(); if (this.cursor.selection) this.cursor.selection.elements.addClass('mq-blur'); + this.updateMathspeak(true); }; this.textarea?.addEventListener('blur', this.blurHandler); diff --git a/src/services/latex.ts b/src/services/latex.ts index 116bef90..56bca375 100644 --- a/src/services/latex.ts +++ b/src/services/latex.ts @@ -41,6 +41,7 @@ export const LatexControllerExtension = & const ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink; - const textareaSpan = ctrlr.textareaSpan, - textarea = ctrlr.textarea; + const textarea = ctrlr.textarea; e.preventDefault(); @@ -52,6 +51,11 @@ export const MouseEventController = & if (!cursor.anticursor) cursor.startSelection(); ctrlr.seek(target, e.pageX).cursor.select(); target = undefined; + if (cursor.selection) + ctrlr.aria + .clear() + .queue(cursor.selection.join('mathspeak') + ' selected') + .alert(); }; // Outside rootEl, the MathQuill node corresponding to the target (if any) // won't be inside this root. So don't mislead Controller::seek with it. @@ -59,8 +63,10 @@ export const MouseEventController = & const mouseup = () => { cursor.blink = blink; if (!cursor.selection) { - if (ctrlr.editable) cursor.show(); - else textareaSpan?.remove(); + if (ctrlr.editable) { + cursor.show(); + if (cursor.parent) ctrlr.aria.queue(cursor.parent).alert(); + } } // Delete the mouse handlers now that the drag has ended. @@ -71,8 +77,8 @@ export const MouseEventController = & if (e.detail === 3) { // If this is a triple click, then select all and return. - ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (cursor.left) ctrlr.selectLeft(); + ctrlr.selectAll(); + ctrlr.aria.alert(); mouseup(); return; } else if (e.detail === 2) { @@ -82,50 +88,50 @@ export const MouseEventController = & ctrlr.seek(e.target as HTMLElement, e.pageX); if (!cursor.right && cursor.left?.parent === root) ctrlr.moveLeft(); - if (cursor.right instanceof Letter) { - // If a "Letter" is to the right of the cursor, then try to select all adjacent "Letter"s that - // are of the same basic ilk. That means all "Letter"s that are part of an operator name, or - // all "Letter"s that are not part of an operator name. - const currentNode = cursor.right; - while ( - cursor.left && - cursor.left instanceof Letter && - cursor.left.isPartOfOperator === currentNode.isPartOfOperator - ) - ctrlr.moveLeft(); - cursor.startSelection(); - while ( + ctrlr.withIncrementalSelection((selectDir) => { + if (cursor.right instanceof Letter) { + // If a "Letter" is to the right of the cursor, then try to select all adjacent "Letter"s + // that are of the same basic ilk. That means all "Letter"s that are part of an operator + // name, or all "Letter"s that are not part of an operator name. + const currentNode = cursor.right; + while ( + cursor.left && + cursor.left instanceof Letter && + cursor.left.isPartOfOperator === currentNode.isPartOfOperator + ) + ctrlr.moveLeft(); + cursor.startSelection(); + while ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + cursor.right && + cursor.right instanceof Letter && + cursor.right.isPartOfOperator === currentNode.isPartOfOperator + ) + selectDir('right'); + } else if (cursor.right instanceof Digit) { + // If a "Digit" is to the right of the cursor, then select all adjacent "Digit"s. + while (cursor.left && cursor.left instanceof Digit) ctrlr.moveLeft(); + cursor.startSelection(); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - cursor.right && - cursor.right instanceof Letter && - cursor.right.isPartOfOperator === currentNode.isPartOfOperator - ) - ctrlr.selectRight(); - } else if (cursor.right instanceof Digit) { - // If a "Digit" is to the right of the cursor, then select all adjacent "Digit"s. - while (cursor.left && cursor.left instanceof Digit) ctrlr.moveLeft(); - cursor.startSelection(); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor.right && cursor.right instanceof Digit) ctrlr.selectRight(); - } else { - cursor.startSelection(); - ctrlr.selectRight(); - } - - // If the cursor is in a text block, then select the whole text block. - if (cursor.left?.parent instanceof TextBlock) { - cursor.left.parent.moveOutOf('left', cursor); - ctrlr.selectRight(); - } - + while (cursor.right && cursor.right instanceof Digit) selectDir('right'); + } else { + cursor.startSelection(); + selectDir('right'); + } + + // If the cursor is in a text block, then select the whole text block. + if (cursor.left?.parent instanceof TextBlock) { + cursor.left.parent.moveOutOf('left', cursor); + selectDir('right'); + } + }); + + ctrlr.aria.alert(); mouseup(); return; } - if (ctrlr.blurred) { - if (!ctrlr.editable && textareaSpan) rootEl?.prepend(textareaSpan); - textarea?.focus(); - } + if (ctrlr.blurred) textarea?.focus(); cursor.blink = noop; ctrlr.seek(e.target as HTMLElement, e.pageX).cursor.startSelection(); diff --git a/src/services/textarea.ts b/src/services/textarea.ts index ab737a00..03d69325 100644 --- a/src/services/textarea.ts +++ b/src/services/textarea.ts @@ -79,29 +79,25 @@ export const TextAreaController = < this.textarea?.addEventListener('focus', () => (this.blurred = false)); this.textarea?.addEventListener('blur', () => { if (this.cursor.selection) this.cursor.selection.clear(); - - // Detaching during blur explodes in WebKit - setTimeout(() => { - this.textareaSpan?.remove(); - this.blurred = true; - }); + this.blurred = true; }); this.selectFn = (text) => { if (this.textarea) this.textarea.value = text; if (text) this.textarea?.select(); }; + + this.container.prepend(this.textareaSpan as HTMLElement); } editablesTextareaEvents() { if (this.textarea) { const { select } = saneKeyboardEvents(this.textarea, this as unknown as Controller); - this.selectFn = (text) => { - select(text); - }; + this.selectFn = select; } this.container.prepend(this.textareaSpan as HTMLElement); this.focusBlurEvents(); + this.updateMathspeak(); } unbindEditablesEvents() { @@ -168,4 +164,39 @@ export const TextAreaController = < // FIXME: this always inserts math or a TextBlock, even in a RootTextBlock this.writeLatex(text).cursor.show(); } + + setupStaticField() { + this.mathspeakSpan = document.createElement('span'); + this.mathspeakSpan.classList.add('mq-mathspeak'); + this.container.prepend(this.mathspeakSpan); + this.updateMathspeak(); + this.blurred = true; + this.cursor.hide().parent?.blur(this.cursor); + } + + updateMathspeak(emptyContent = false) { + // If the controller's ARIA label doesn't end with a punctuation mark, add a colon by default to better + // separate it from mathspeak. + const ariaLabel = this.getAriaLabel(); + const labelWithSuffix = /[A-Za-z0-9]$/.test(ariaLabel) ? ariaLabel + ':' : ariaLabel; + const mathspeak = this.root.mathspeak().trim(); + this.aria.clear(emptyContent); + + // For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to + // traverse the content. For editable math, assign the mathspeak to the textarea's ARIA label (AT can use + // text navigation to interrogate the content). Be certain to include the mathspeak for only one of these, + // though, as we don't want to include outdated labels if a field's editable state changes. By design, also + // take careful note that the ariaPostLabel is meant to exist only for editable math (e.g. to serve as an + // evaluation or error message) so it is not included for static math mathspeak calculations. The + // mathspeakSpan should exist only for static math, so we use its presence to decide which approach to take. + if (this.mathspeakSpan) { + this.textarea?.setAttribute('aria-label', ''); + this.mathspeakSpan.textContent = (labelWithSuffix + ' ' + mathspeak).trim(); + } else { + this.textarea?.setAttribute( + 'aria-label', + (labelWithSuffix + ' ' + mathspeak + ' ' + this.ariaPostLabel).trim() + ); + } + } }; diff --git a/src/tree/node.ts b/src/tree/node.ts index b704f4a8..bda8376e 100644 --- a/src/tree/node.ts +++ b/src/tree/node.ts @@ -14,6 +14,11 @@ export interface Ends { right?: TNode; } +export interface MathspeakOptions { + createdLeftOf?: Cursor; + ignoreShorthand?: boolean; +} + const prayOverridden = (name: string) => { throw new Error(`"${name}" should be overridden or never called on this node`); }; @@ -41,6 +46,10 @@ export class TNode { isSymbol?: boolean; isSupSubLeft?: boolean; + ariaLabel?: string; + mathspeakName?: string; + mathspeakTemplate?: string[]; + upInto?: TNode; downInto?: TNode; upOutOf?: ((dir: Direction) => void) | ((cursor: Cursor) => void) | TNode | boolean; @@ -195,50 +204,50 @@ export class TNode { // End -> move to the end of the current block. case 'End': - if (cursor.parent) ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); + if (cursor.parent) { + ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); + ctrlr.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); + ctrlr.aria.queue('end of').queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-End -> select to the end of the current block. case 'Shift-End': - while (cursor.right) { - ctrlr.selectRight(); - } + ctrlr.selectToBlockEndInDir('right'); break; // Ctrl-Shift-End -> select to the end of the root block. case 'Ctrl-Shift-End': - while (cursor.right || cursor.parent !== ctrlr.root) { - ctrlr.selectRight(); - } + ctrlr.selectToRootEndInDir('right'); break; // Home -> move to the start of the root block or the current block. case 'Home': - if (cursor.parent) ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); + if (cursor.parent) { + ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); + ctrlr.aria.queue('beginning of').queue(cursor.parent, true); + } break; // Ctrl-Home -> move to the start of the current block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); + ctrlr.aria.queue('beginning of').queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-Home -> select to the start of the current block. case 'Shift-Home': - while (cursor.left) { - ctrlr.selectLeft(); - } + ctrlr.selectToBlockEndInDir('left'); break; // Ctrl-Shift-Home -> move to the start of the root block. case 'Ctrl-Shift-Home': - while (cursor.left || cursor.parent !== ctrlr.root) { - ctrlr.selectLeft(); - } + ctrlr.selectToRootEndInDir('left'); break; case 'Left': @@ -267,21 +276,25 @@ export class TNode { break; case 'Shift-Up': - if (cursor.left) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor.left) ctrlr.selectLeft(); - } else { - ctrlr.selectLeft(); - } + ctrlr.withIncrementalSelection((selectDir) => { + if (cursor.left) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (cursor.left) selectDir('left'); + } else { + selectDir('left'); + } + }); break; case 'Shift-Down': - if (cursor.right) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (cursor.right) ctrlr.selectRight(); - } else { - ctrlr.selectRight(); - } + ctrlr.withIncrementalSelection((selectDir) => { + if (cursor.right) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (cursor.right) selectDir('right'); + } else { + selectDir('right'); + } + }); break; case 'Ctrl-Up': @@ -301,13 +314,53 @@ export class TNode { case 'Meta-A': case 'Ctrl-A': - ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - while (cursor.left) ctrlr.selectLeft(); + ctrlr.selectAll(); + break; + + // The remaining key strokes are only of benefit to screen reader users. + + // speak parent block that has focus + case 'Ctrl-Alt-Up': + if (cursor.parent?.parent && cursor.parent.parent instanceof TNode) + ctrlr.aria.queue(cursor.parent.parent); + else ctrlr.aria.queue('nothing above'); + break; + + // speak current block that has focus + case 'Ctrl-Alt-Down': + if (cursor.parent && cursor.parent instanceof TNode) ctrlr.aria.queue(cursor.parent); + else ctrlr.aria.queue('block is empty'); + break; + + // speak left-adjacent block + case 'Ctrl-Alt-Left': + if (cursor.parent?.parent?.ends.left) ctrlr.aria.queue(cursor.parent.parent.ends.left); + else ctrlr.aria.queue('nothing to the left'); + break; + + // speak right-adjacent block + case 'Ctrl-Alt-Right': + if (cursor.parent?.parent?.ends.right) ctrlr.aria.queue(cursor.parent.parent.ends.right); + else ctrlr.aria.queue('nothing to the right'); + break; + + // speak selection + case 'Ctrl-Alt-Shift-Down': + if (cursor.selection) ctrlr.aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); + else ctrlr.aria.queue('nothing selected'); + break; + + // speak ARIA post label (evaluation or error) + case 'Ctrl-Alt-=': + case 'Ctrl-Alt-Shift-Right': + if (ctrlr.ariaPostLabel.length) ctrlr.aria.queue(ctrlr.ariaPostLabel); + else ctrlr.aria.queue('no answer'); break; default: return; } + ctrlr.aria.alert(); e.preventDefault(); ctrlr.scrollHoriz(); } @@ -348,6 +401,9 @@ export class TNode { chToCmd(_ch: string, _options: Options): TNode { return this as TNode; } + mathspeak(_options?: MathspeakOptions) { + return ''; + } getController() { // Navigate up the tree to find the controller. diff --git a/test/aria.test.js b/test/aria.test.js new file mode 100644 index 00000000..f8231774 --- /dev/null +++ b/test/aria.test.js @@ -0,0 +1,209 @@ +/* global assert, MQ */ + +suite('aria', function () { + let mathField; + let container; + setup(function () { + container = document.createElement('span'); + document.getElementById('mock')?.append(container); + mathField = MQ.MathField(container); + }); + + const assertAriaEqual = (alertText) => { + assert.equal(alertText, mathField.__controller.aria.msg); + }; + + test('mathfield has aria-hidden on mq-root-block', function () { + mathField.latex('1+\\frac{1}{x}'); + const ariaHiddenChildren = container.querySelectorAll('[aria-hidden="true"]'); + assert.equal(ariaHiddenChildren.length, 1, '1 aria-hidden elements'); + assert.ok(ariaHiddenChildren[0].classList.contains('mq-root-block'), 'aria-hidden is set on mq-root-block'); + }); + + test('static math aria-hidden', function () { + const staticMath = MQ.StaticMath(container); + staticMath.latex('1+\\frac{1}{x}'); + const ariaHiddenChildren = container.querySelectorAll('[aria-hidden="true"]'); + assert.equal(ariaHiddenChildren.length, 1, '1 aria-hidden element'); + assert.ok(ariaHiddenChildren[0].classList.contains('mq-root-block'), 'aria-hidden is set on mq-root-block'); + }); + + test('typing and backspacing over simple expression', function () { + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.typedText('+'); + assertAriaEqual('plus'); + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.typedText('='); + assertAriaEqual('equals'); + mathField.typedText('2'); + assertAriaEqual('2'); + mathField.keystroke('Backspace'); + assertAriaEqual('2'); + mathField.keystroke('Backspace'); + assertAriaEqual('equals'); + mathField.keystroke('Backspace'); + assertAriaEqual('1'); + mathField.keystroke('Backspace'); + assertAriaEqual('plus'); + mathField.keystroke('Backspace'); + assertAriaEqual('1'); + }); + + test('typing and backspacing a fraction', function () { + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.typedText('/'); + assertAriaEqual('over'); + mathField.typedText('2'); + assertAriaEqual('2'); + + // We have logic to shorten the speak we return for common numeric fractions and superscripts. + // While editing, however, the slightly longer form (but unambiguous) form of the item should be spoken. + // In this case, we would shorten the fraction 1/2 to "1 half" when reading, + // but navigating around the equation should result in "StartFraction, 1 Over 2, EndFraction." + mathField.keystroke('Tab'); + assertAriaEqual('after StartFraction, 1 Over 2 , EndFraction'); + + mathField.keystroke('Backspace'); + assertAriaEqual('end of denominator 2'); + mathField.keystroke('Backspace'); + assertAriaEqual('2'); + mathField.keystroke('Backspace'); + assertAriaEqual('Over'); + mathField.keystroke('Backspace'); + assertAriaEqual('1'); + }); + + test('navigating a fraction', function () { + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.typedText('/'); + assertAriaEqual('over'); + mathField.typedText('2'); + assertAriaEqual('2'); + mathField.keystroke('Up'); + assertAriaEqual('numerator 1'); + mathField.keystroke('Down'); + assertAriaEqual('denominator 2'); + mathField.latex(''); + }); + + test('typing and backspacing a binomial', function () { + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.cmd('\\choose'); + // Matching behavior of "over", we don't get "choose" as the ARIA here. + mathField.typedText('2'); + assertAriaEqual('2'); + + mathField.keystroke('Tab'); + assertAriaEqual('after StartBinomial, 1 Choose 2 , EndBinomial'); + + mathField.keystroke('Backspace'); + assertAriaEqual('end of lower index 2'); + mathField.keystroke('Backspace'); + assertAriaEqual('2'); + mathField.keystroke('Backspace'); + assertAriaEqual('Choose'); + mathField.keystroke('Backspace'); + assertAriaEqual('1'); + }); + + test('navigating a binomial', function () { + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.cmd('\\choose'); + // Matching behavior of "over", we don't get "choose" as the ARIA here. + mathField.typedText('2'); + assertAriaEqual('2'); + mathField.keystroke('Up'); + assertAriaEqual('upper index 1'); + mathField.keystroke('Down'); + assertAriaEqual('lower index 2'); + mathField.latex(''); + }); + + test('typing and backspacing through parenthesies', function () { + mathField.typedText('('); + assertAriaEqual('left parenthesis'); + mathField.typedText('1'); + assertAriaEqual('1'); + mathField.typedText('*'); + assertAriaEqual('times'); + mathField.typedText('2'); + assertAriaEqual('2'); + mathField.typedText(')'); + assertAriaEqual('right parenthesis'); + mathField.keystroke('Backspace'); + assertAriaEqual('right parenthesis'); + mathField.keystroke('Backspace'); + assertAriaEqual('2'); + mathField.keystroke('Backspace'); + assertAriaEqual('times'); + mathField.keystroke('Backspace'); + assertAriaEqual('1'); + mathField.keystroke('Backspace'); + assertAriaEqual('left parenthesis'); + }); + + test('testing beginning and end alerts', function () { + mathField.typedText('\\sqrt x'); + mathField.keystroke('Home'); + assertAriaEqual('beginning of square root x'); + mathField.keystroke('End'); + assertAriaEqual('end of square root x'); + mathField.keystroke('Ctrl-Home'); + assertAriaEqual('beginning of Math Input StartSquareRoot, x , EndSquareRoot'); + mathField.keystroke('Ctrl-End'); + assertAriaEqual('end of Math Input StartSquareRoot, x , EndSquareRoot'); + }); + + test('testing aria-label for interactive math', function (done) { + if (document.hasFocus()) { + mathField.focus(); + mathField.typedText('\\sqrt x'); + mathField.blur(); + setTimeout(() => { + assert.equal( + mathField.__controller.textarea.getAttribute('aria-label'), + 'Math Input: StartSquareRoot, x , EndSquareRoot' + ); + done(); + }); + } else { + console.warn( + 'The test "testing aria-label for interactive math" needs the document to have focus.\n' + + 'Normally, the page being open and focused is enough to have focus, ' + + 'but with the Developer Tools open, it depends on whether you last ' + + 'clicked on something in the Developer Tools or on the page itself. ' + + 'Click the page, or close the Developer Tools, and Refresh.' + ); + const mock = document.getElementById('mock'); + while (mock.firstChild) mock.firstChild.remove(); + this.skip(); + } + }); + + test('testing aria-label for static math', function () { + const staticSpan = document.createElement('span'); + staticSpan.classList.add('mathquill-static-math'); + staticSpan.textContent = 'y=\\frac{2x}{3y}'; + document.getElementById('mock')?.append(staticSpan); + const staticMath = MQ.StaticMath(staticSpan); + assert.equal( + staticMath.__controller.mathspeakSpan.textContent, + 'y equals StartFraction, 2 x Over 3 y , EndFraction' + ); + assert.equal('', staticMath.getAriaLabel()); + staticMath.setAriaLabel('Static Label'); + assert.equal( + staticMath.__controller.mathspeakSpan.textContent, + 'Static Label: y equals StartFraction, 2 x Over 3 y , EndFraction' + ); + assert.equal('Static Label', staticMath.getAriaLabel()); + staticMath.latex('2+2'); + assert.equal(staticMath.__controller.mathspeakSpan.textContent, 'Static Label: 2 plus 2'); + }); +}); diff --git a/test/index.js b/test/index.js index c2521ff1..053d75be 100644 --- a/test/index.js +++ b/test/index.js @@ -19,3 +19,4 @@ import 'test/focusBlur.test'; import 'test/SupSub.test'; import 'test/MathFunction.test'; import 'test/text-output.test'; +import 'test/aria.test'; From d5fe9ceec3b3e57b9c5d4c1eee148f76fa85d53f Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 3 Nov 2024 20:25:48 -0600 Subject: [PATCH 09/19] Pull in a few more things from the desmos code. Switch from an aria label on the hidden text area to hidden span that the text area is aria labelledby. This was just merged in their code. Also add the tabbable option. --- docs/Config.md | 7 ++++ public/demo.html | 11 +++++- public/unit-test.html | 2 + src/abstractFields.ts | 28 +++++++------- src/commands/math/commands.ts | 17 ++++---- src/controller.ts | 1 + src/options.ts | 16 +++++++- src/services/focusBlur.ts | 26 +++++++++++-- src/services/textarea.ts | 73 ++++++++++++++++------------------- test/aria.test.js | 71 +++++++++++++++++++++++++++++++--- test/latex.test.js | 15 ------- 11 files changed, 178 insertions(+), 89 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 32095f9b..4cb38896 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -122,6 +122,13 @@ Nested content in latex rendered during initialization or pasted into mathquill Overwriting this may be useful for hacks like suppressing built-in virtual keyboards. It defaults to `