diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 737d132ad..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,16 +0,0 @@ -engines: - eslint: - enabled: true - duplication: - enabled: true - config: - languages: - - javascript: -exclude_paths: - - 'docs/' - - 'dist/' - - 'build/' - - 'test/' -ratings: - paths: - - 'src/**/*' diff --git a/.eslintrc.json b/.eslintrc.json index f8693994e..6a2fdf43f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,39 +2,271 @@ "env": { "browser": true, // "commonjs": true, - "es6": true, - "node": true, + // "es6": true, + "es2020": true, + "jest": true, + "node": true, // For jest, config files, etc... "amd": true, "mocha": true }, + "parser": "@typescript-eslint/parser", + "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module", - "ecmaFeatures": { - "jsx": false - } + // Configure the parser with the tsconfig file in the root project + // (or rather an extended version that add peripheral files such as rollup.config.js + // so that eslint doesn't complain that they are not in the project) + "project": "./tsconfig.eslint.json", + + // "ecmaFeatures": { "jsx": false }, // default + "ecmaVersion": 2020, + // "sourceType": "module" // Allow the use of import + "extraFileExtensions": [".less"] }, "extends": [ // Uses the recommended rules for Typescript "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier // Disable rules that conflict with prettier // See https://prettier.io/docs/en/integrating-with-linters.html "plugin:prettier/recommended" + + // "plugin:no-unsanitized/DOM" ], - // See http://eslint.org/docs/rules/ + // Note: + // "off" or 0 - turn the rule off + // "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code) + // "error" or 2 - turn the rule on as an error (exit code is 1 when triggered) + // See http://eslint.org/docs/rules/ "rules": { - // Note: - // "off" or 0 - turn the rule off - // "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code) - // "error" or 2 - turn the rule on as an error (exit code is 1 when triggered) + // "no-unsanitized/method": [ + // "warn", + // { + // // "disableDefault": true, + // "escape": { + // "methods": [ + // "createHTML", + // "mathfield.config.createHTML", + // "MathfieldPrivate.config.createHTML" + // ] + // } + // } + // ], + "no-restricted-globals": [ + "error", + "postMessage", + "blur", + "focus", + "close", + "frames", + "self", + "parent", + "opener", + "top", + "length", + "closed", + "location", + "origin", + "name", + "locationbar", + "menubar", + "personalbar", + "scrollbars", + "statusbar", + "toolbar", + "status", + "frameElement", + // "navigator", + "customElements", + "external", + "screen", + "innerWidth", + "innerHeight", + "scrollX", + "pageXOffset", + "scrollY", + "pageYOffset", + "screenX", + "screenY", + "outerWidth", + "outerHeight", + "devicePixelRatio", + "clientInformation", + "screenLeft", + "screenTop", + "defaultStatus", + "defaultstatus", + "styleMedia", + "onanimationend", + "onanimationiteration", + "onanimationstart", + "onsearch", + "ontransitionend", + "onwebkitanimationend", + "onwebkitanimationiteration", + "onwebkitanimationstart", + "onwebkittransitionend", + "isSecureContext", + "onabort", + "onblur", + "oncancel", + "oncanplay", + "oncanplaythrough", + "onchange", + "onclick", + "onclose", + "oncontextmenu", + "oncuechange", + "ondblclick", + "ondrag", + "ondragend", + "ondragenter", + "ondragleave", + "ondragover", + "ondragstart", + "ondrop", + "ondurationchange", + "onemptied", + "onended", + "onerror", + "onfocus", + "oninput", + "oninvalid", + "onkeydown", + "onkeypress", + "onkeyup", + "onload", + "onloadeddata", + "onloadedmetadata", + "onloadstart", + "onmousedown", + "onmouseenter", + "onmouseleave", + "onmousemove", + "onmouseout", + "onmouseover", + "onmouseup", + "onmousewheel", + "onpause", + "onplay", + "onplaying", + "onprogress", + "onratechange", + "onreset", + "onresize", + "onscroll", + "onseeked", + "onseeking", + "onselect", + "onstalled", + "onsubmit", + "onsuspend", + "ontimeupdate", + "ontoggle", + "onvolumechange", + "onwaiting", + "onwheel", + "onauxclick", + "ongotpointercapture", + "onlostpointercapture", + "onpointerdown", + "onpointermove", + "onpointerup", + "onpointercancel", + "onpointerover", + "onpointerout", + "onpointerenter", + "onpointerleave", + "onafterprint", + "onbeforeprint", + "onbeforeunload", + "onhashchange", + "onlanguagechange", + "onmessage", + "onmessageerror", + "onoffline", + "ononline", + "onpagehide", + "onpageshow", + "onpopstate", + "onrejectionhandled", + "onstorage", + "onunhandledrejection", + "onunload", + "performance", + "stop", + "open", + "print", + "captureEvents", + "releaseEvents", + // "getComputedStyle", + "matchMedia", + "moveTo", + "moveBy", + "resizeTo", + "resizeBy", + "getSelection", + "find", + "createImageBitmap", + "scroll", + "scrollTo", + "scrollBy", + "onappinstalled", + "onbeforeinstallprompt", + "crypto", + "ondevicemotion", + "ondeviceorientation", + "ondeviceorientationabsolute", + "indexedDB", + "webkitStorageInfo", + "chrome", + "visualViewport", + "speechSynthesis", + "webkitRequestFileSystem", + "webkitResolveLocalFileSystemURL", + "openDatabase" + ], + + // Turn off rules that are Typescript specific + // (they are turned on later in the "overrides" section) + "@typescript-eslint/explicit-module-boundary-types": "off", + + // We use @ts-ignore to generate the .d.ts files using tsc, even though + // they import .less file which tsc doesn't know to handle. + "@typescript-eslint/ban-ts-comment": "off", + + // When used with optional chaining, eslint can give an incorrect warning + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "warn", + + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + + // note you must disable the base rule as it can report incorrect errors + "indent": "off", + "no-const-assign": "warn", "no-this-before-super": "warn", // "no-undef": "warn", "no-unreachable": "warn", "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_" + // "varsIgnorePattern": "^_", + // "vars": "all", + // "args": "after-used", + // "ignoreRestSiblings": false + } + ], + "constructor-super": "warn", "block-scoped-var": "error", @@ -53,16 +285,17 @@ "new-cap": "warn", "no-bitwise": "off", "no-console": "off", + "no-dupe-class-members": "off", // Support TypeScritp polymorphism "no-else-return": "warn", "no-eval": "error", "no-fallthrough": "error", "no-invalid-this": "warn", "no-lone-blocks": "warn", "no-return-assign": ["warn", "always"], + "no-redeclare": "off", "no-self-compare": "error", "no-sequences": "warn", "no-unneeded-ternary": "warn", - "no-unused-expressions": "warn", "no-useless-call": "warn", "no-var": "error", "no-with": "error", @@ -73,5 +306,59 @@ "space-infix-ops": ["error", { "int32Hint": false }], "no-trailing-spaces": [1, { "skipBlankLines": true }] - } + }, + "overrides": [ + { + // Typescript-specific rules + "files": ["*.ts"], + "rules": { + // "@typescript-eslint/prefer-optional-chain": "warn", + "@typescript-eslint/prefer-nullish-coalescing": "warn", + + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "typeParameter", + "format": ["PascalCase"] + } + ], + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-use-before-define": [ + "error", + { + "functions": false, + "classes": false, + "variables": false, + "typedefs": false + } + ], + "@typescript-eslint/no-this-alias": [ + "error", + { + "allowedNames": ["self", "that"] // Allow `const self = this`; `[]` by default + } + ], + "@typescript-eslint/no-for-in-array": "error", + + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "warn", + "@typescript-eslint/no-unnecessary-type-arguments": "warn", + // "@typescript-eslint/typedef": [ + // "warn", + // { + // "parameter": true, + // "arrowParameter": false, + // "variableDeclaration": false + // } + // ], + + "@typescript-eslint/array-type": [ + "warn", + { "default": "array" } + ], + + "@typescript-eslint/no-explicit-any": "off" + } + } + ] } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 952d61279..a24e5bb46 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,42 +1,33 @@ -## Prerequisites +## Description -- [ ] New issue - [Did you check the [issue tracker](https://github.com/arnog/mathlive/issues) to see if this issue has already been reported? If it has, you can +1 it to indicate your interest and be notified when it gets resolved.] +[Description of the bug or feature.] -[For more information, see the `CONTRIBUTING` guide.] +[Include screenshots, screen recordings, code fragments or CodePen if applicable] -## Description +## Steps to Reproduce -[Description of the bug or feature.] +[Provide steps that are specific and repeatable, if possible] -[Include screenshots or code fragments if applicable] +1. [First Step] +2. [Second Step] +3. [and so on...] -### Actual behavior +### Actual Behavior -[What actually happened] +[What happens when you follow the steps above] [If there are any error messages, include the exact text shown, -or upload a screenshot. Some error messages may displayed in the Javascript console.] +or upload a screenshot. Some error messages may be displayed in the Javascript console.] ### Expected Behavior -[What you expected to happen] +[What did you expect to happen insteead] [Is this a regression: did it use to work in a previous version?] -## Steps to Reproduce - -[Provide steps that are specific and repeatable, if possible] - -1. [First Step] -2. [Second Step] -3. [and so on...] - ### Environment **Operating System** [macOS, Windows, iOS. Include the version if possible] **Browser** [Safari, Chrome, IE, Firefox, etc...] - -**URL** [URL where the problem can be reproduced, if available] diff --git a/.gitignore b/.gitignore index 6c947f6d5..d1e8bc34e 100755 --- a/.gitignore +++ b/.gitignore @@ -3,15 +3,18 @@ /build /out /node_modules -/.nyc_output -dist +/coverage + babel.babel TODO.md examples/basic/index.debug.html - +dist/* +# To store secret credentials +*.secret.js # OS generated files # ###################### +**/.DS_Store .DS_Store .DS_Store? ._* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..2c6c30125 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "submodules/math-json"] + path = submodules/math-json + url = https://github.com/cortex-js/math-json.git + branch = master diff --git a/.npmignore b/.npmignore deleted file mode 100644 index a006af1df..000000000 --- a/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -# An empty .npmignore file is necessary for the .gitignore file to be ignored -# Without it, the /dist directory (which is in .gitignore) would not be -# taken into account by npm diff --git a/.travis.yml b/.travis.yml index f41a53dc2..d69fe80cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: node_js node_js: [lts/*] before_install: [npm install -g npm@latest] install: [npm ci] +script: + - npm run lint + - npm run dist cache: directories: - node_modules @@ -11,21 +14,6 @@ branches: - master - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ # tagged with version # - release -before_deploy: - - npm run dist - - npm run deploy-ci -deploy: - provider: npm - on: - tags: true - email: '$NPM_EMAIL' - api_token: '$NPM_API_TOKEN' - access: public - # dry_run: true - edge: true -script: - - npm run lint - - npm run test notifications: slack: secure: dctz771vJCv6UdjBz65n8XkKx5q/LF5UIY+qSRAxLpqwFp9Y3i9trUNoSaDya4PH5sfAHbUHxsBvG8njIKDJY1pckcrCt2Ml2+9fRQ01cCMggNnSK5OlcuoL6lrnbMbaLz3XEUKJY8mfJnnHggUFNqT0IhCR4xgcxiyg90AtHhIGQcIpkFrpmwbW+ZXm7PMTP/svGkL3OMs4lDO6V8XzEGKXd+4rkcvINZwOtKDXbIf7Wm5MXQ2lf4o1DviAJeFqlIUPRRonAs2KPUAl86t+M22EWDExy6NOtmbdA68m+pDaBJ0mMN0OwxsoEJh5e7ml1/UAs82hCu+Kl3xW4emDrrF7k6C065NmWeRqLBVJxcfy+rdNsiBfJhPmBBGc0VTYLqnWR5PLxDBHgLyO7zAjG4n4G8WpCEY0j891Xaw3Cktz8tWWo336BxCmsd2zOUdoWr+aQsml25mlpLYuX2t4HR66jJdz8A7X+e5m4V3/BE+afMKhRYgeuwPVq03TBAtHZp5jLr0TCw9t3qyNrUEXGtqoAYdCThCP1zcpxTTYnxoYHgoEfiE8OVMkMf6R3JpCUQMk1+AbpyVIHeCxfaq7jCZjUG4exMGFNFhTSjl3ePoCLdwfP75uEIiQU/LOJGb2JrJWVjTEBs5P/atk71ijomygAU3D6BtYwoZm0Hqy+l8= diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d6e0e9f8..07a5dbede 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,18 +5,18 @@ "version": "0.2.0", "configurations": [ { - "type": "chrome", - "request": "attach", - "name": "Attach to Chrome", - "port": 9222, - "webRoot": "${workspaceFolder}" - }, - { - "type": "chrome", + "name": "Debug Jest Tests", + "type": "node", "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:8890", - "webRoot": "${workspaceFolder}" + "runtimeArgs": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/.bin/jest", + "test/latex.test.ts", + "--runInBand" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index bf106296d..286cd3374 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,10 @@ { - "eslint.provideLintTask": true, "eslint.options": { "configFile": ".eslintrc.json" }, "cSpell.allowCompoundWords": true, "cSpell.words": ["assistive", "grapheme", "latexify", "mathrm"], - "search.usePCRE2": true, - "cSpell.enabled": false + "cSpell.enabled": false, + "editor.formatOnSave": true, + "liveServer.settings.port": 5501 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6f4fcce..1fa9ee125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,1328 @@ -## +## [Unreleased] + +### Breaking Change + +- Renamed `getCaretPosition()` and `setCaretPosition()` to `get/set caretPoint`. + + "Position" refers to where the caret/insertion point is, as an + offset inside the expression. These methods return client screen + coordinates and the new name better reflect the correct terminology. + +- Removed deprecated (April 2019) method `enterCommandMode()` + +### New Features + +- **#555** Support for IME (Input Method Engines) for Japanese, Chinese, + Korean and other complex scripts. +- `applyStyle()` has now more options. Previously it always toggled the + style of the selection. Now it can either toggle or set the style, and modify the selection or a specific range. +- **#387** `find()` method to search the fragments of an expression + that match a Latex string or regular expression. + + For example the following code snippet will add a yellow background to + the fractions in the expression: + + ```javascript + mf.find(/^\\frac{[^}]*}{[^}]*}\$/).forEach((x) => { + mf.applyStyle({ backgroundColor: 'yellow' }, x, { + suppressChangeNotifications: true, + }); + }); + ``` + +- **#387** `replace()` method to replace fragments of an expression. + + This method is similar to the `replace()` method of the `String` class. + The search pattern can be specified using a string or regular expression, and + the replacement pattern can be a string or a function. + If using a regular expression, it can contain capture groups, and those + can be references in the replacement pattern. + + The following snippet will invert fractions in a formula: + + ```javascript + mf.replace(/^\\frac{([^}]*)}{([^}]*)}$/, '\\frac{$2}{$1}'); + ``` + +- New **Latex Mode** + + This mode replaces the previous **Command Mode**. While the **Command Mode** + (triggered by pressing the **\\** or **ESC** key) was only intended to insert + a single Latex command (e.g. "\aleph"), the **Latex Mode** is a more + comprehensive Latex editing mode. + + To enter the **Latex Mode**, press the **ESC** key or the **\\** key. While + in this mode, a complex Latex expression can be edited. Press + the **ESC** or **Return** key to return to regular editing mode. + + To quickly peek at the Latex code of an expression, select it, then press + **ESC**. Press **ESC** again to return to the regular editing mode. + + To insert a command, press the **\\** key, followed by the command name. + Press the **TAB** key to accept a suggestion, then the **RETURN** key to + return to regular editing mode (previously pressing the **TAB** key + would have exited the command mode). + +- Added `soundsDirectory` option to customize the location of the sound files, + similarly to `fontsDirectory`. +- Enabled audio feedback by default. + +### Bug Fixes + +- The selection in an expression could render incorrectly if it was displayed + before the fonts were fully loaded. This situation is now handled correctly + and the selection is redrawn when fonts finish loading. +- The `typedText` selector dropped its options argument. As a result, the + sound feedback from the virtual keyboard only played for some keys. +- **#697** When using the `` element the command popover did not + display correctly. +- Fixed issues with copy/paste. Copying from a text zone will copy the text + (and not a latex representation of it). Copy from a Latex zone now works. + +### Improvements + +- Improved handling of paste commands: if a JSON item is on the clipboard it + is used in priority, before a `plain/text` item. +- It is now possible to type dead keys such as `alt+e`, and they are properly + displayed as a composition (side effect of the fix for **#555**). + +### Architecture + +- **Complete rewrite of selection handling.** + + This is mostly an internal change, but it will offer some benefits for new + capabilities in the public API as well. + + **Warning**: _This is a very disruptive change, and there might be some edge + cases that will need to be cleaned up._ + + The _position_ of the insertion point is no longer represented by a _path_. + It is now an _offset_ from the start of the expression, with each possible + insertion point position being assigned a sequential value. + + The _selection_ is no longer represented with a _path_ and a + sibling-relative _offset_. It is now a _range_, i.e. a start and end + _offset_. More precisely, the selection is an array of ranges (to represent + discontinuous selections, for example a column in a matrix) and a direction. + + These changes have these benefits: + + - The selection related code is more expressive and much simpler to read + and debug + - The code is also simpler to change so that changes in UI behavior are + much easier to implement. There are several open issues that will be + much easier to fix now. In particular, the `onDelete` function now + regroups all the special handling when pressing the **Backspace** and + **Delete** keys. + - Instead of the esoteric paths, the concept of position as an offset is + much easier to explain and understand, and can now be exposed in the + public API. Consequently, new functionality can be exposed, such as the + `find()` method which returns its results as an array of ranges. It is + also possible now to query and change the current selection, and to + apply styling to a portion of the expression other than the selection. + - The selection is represented as an _array_ of ranges to support + discontinous selections, which are useful to select for example all the + cells in the column of a matrix column. This kind of selection was not + previously possible. + - Incidentally this fixes a circular dependency, which was a smell test + that there was a problem with the previous architecture. + + On a historical note, the reason for the original implementation with paths + was based on the TeX implementation: when rendering a tree of atoms (which + TeX calls _nodes_), the TeX layout algorithm never needs to find the parent + of an atom. The MathLive rendering engine was implemented in the same way. + However, for interactive editing, being able to locate the parent of an atom + is frequetly necessary. The paths were a mechanism to maintain a separate + data structure from the one needed by the rendering engine. However, they + were a complex and clumsy mechanism. Now, a `parent` property has been + introduced in instance of `Atom`, even though it is not necessary for the + rendering phase. It does make the handling of the interactive manipulation + of the formula much easier, though. + +- **Changes to the handling of sentinel atoms (type `'first'`)** + + This is an internal change that does not affect the public API. + + Sentinel atoms are atoms of type `'first'` that are inserted as the first + element in atom branches. Their purpose is to simplify the handling of + "empty" lists, for example an empty numerator or superscript. + + Previously, these atoms where added when an editable atom tree was created, + i.e. in the `editor` code branch, since they are not needed for pure + rendering. However, this created situations where the tree had to be + 'corrected' by inserting missing `'first'`. This code was complex and + resulted in some unexpected operations having the side effect of modifying + the tree. + + The `'first'` atoms are now created during parsing and are present in + editable and non-editable atom trees. + +- **Refactoring of Atom classes** + + This is an internal change that does not affect the public API. + + Each 'kind' of atom (fraction, extensible symbol, boxed expression, etc...) + is now represented by a separate class extending the `Atom` base class (for + example `GenfracAtom`). Each of those classes have a `render()` method that + generates a set of DOM virtual nodes representing the Atom and a `toLatex()` + method which generates a Latex string representing the atom. + + Previously the handling of the different kind of atoms was done procedurally + and all over the code base. The core code is now much smaller and easier to + read, while the specialized code specific to each kind of atom is grouped in + their respective classes. + +- **Unit testing using Jest snapshot** + + Rewrote the unit tests to use Jest snapshots for more comprehensive + validation. + +## 0.59.0 (2020-11-04) + +### Bug Fixes + +- **#685** Virtual keyboard event listeners were not properly released when + the mathfield was removed from the DOM + +## 0.58.0 (2020-10-11) + +### New Features + +- **#225** Added `onCommit` listener to `mf.options`. This listener is invoked + when the user presses **Enter** or **Return** key, or when the field loses + focus and its value has changed since it acquired it. In addition, a + `change` event is triggered when using a `MathfieldElement`. The event + previously named `change` has been renamed to `input`. This mimics the + behavior of `` and ` -
f(x)=
-
1 + \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots = - \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})}, - \quad\quad \text{for $|q|<1$}
-
\frac{1}{4\pi t}e^{-\frac{x^2+y^2}{4t}}
- -
-
+
- + function escapeHtml(string) { + return String(string).replace(/[&<>"'`=/\u200b]/g, function ( + s + ) { + return ( + { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=', + '\u200b': '&#zws;', + }[s] || s + ); + }); + } + + - + \ No newline at end of file diff --git a/examples/index.html b/examples/index.html index 02e7efbc1..5ed182b11 100644 --- a/examples/index.html +++ b/examples/index.html @@ -34,6 +34,18 @@

+
+

+ Latex

+ +

You can get Latex from a mathfield, but you can also set Latex + into a mathfield, and you can even keep both in sync

+

With the help of a third-party library, you can also output to an image

+ +

@@ -44,7 +56,7 @@

- You can also intercept keystrokes to implement keyboard shortcuts. + You can also intercept keystrokes to implement key bindings.