diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4f73f4b57..343adf908 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,8 +1,8 @@ -> Please follow the template below. -> If you don't, your issue may be closed without being addressed. +> Following the template below will ensure that your issue is addressed as quickly as possible. +> If you don't follow the template, it may be closed without being addressed. - -> Include code fragments or CodePen.io links to illustrate the issue. Do not include screenshots of code. +> Include code fragments or CodePen.io links to illustrate the issue. Do not include screenshots of code. +> Simplify the code fragment as much as possible. Remove anything that is not directly related to the issue. > If you get a runtime or TypeScript error, copy the text of the error message, from the JavaScript console, for example. Do not include a screenshot of the error message. @@ -14,7 +14,9 @@ > Do not include screenshots of code, error messages or LaTeX. -> If you use React, Vue or some other framework, try to provide some code that replicates the issue using only plain JavaScript. If you can't, then include a link to a minimal project demonstrating the issue. This will make it **much** easier to reproduce the issue and fix it. +> If you use React, Vue or some other framework, try to provide some code that replicates the issue using only plain JavaScript. +> If you can't, then include a link to a minimal project demonstrating the issue. +> This will make it **much** easier to reproduce the issue and fix it. > If the issue has to do with the Compute Engine or MathJSON, please report it here: https://github.com/cortex-js/compute-engine/issues @@ -35,20 +37,22 @@ here: https://github.com/cortex-js/compute-engine/issues ### Actual Behavior -> (Required) What happened when you followed the steps above? +> (Required) What happens when you follow the steps above? ### Expected Behavior -> (Required) What did you expect to happen instead? It may be obvious to you what should have happened, but if you don't state it explicitly it may not be obvious to others. +> (Required) What did you expect to happen instead? +> It may be obvious to you what should have happened, but if you don't state it explicitly it may not be obvious to others. ### Environment -> Is this a regression: did it use to work in a previous version? - **MathLive version** _If using the cortexjs.io site, the version is displayed -at the bottom of the page. If using the library, the version is available as -`MathfieldElement.version`_ +at the bottom of the page. If using the library, the version is available by +typing `console.log(MathfieldElement.version)` in the console. Include the +output of that command here. + +> Is this a regression: did it use to work in a previous version? **Operating System** _macOS, Windows, iOS. Include the version_ diff --git a/.vscode/launch.json b/.vscode/launch.json index 3153c9a20..70474f252 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,11 +6,15 @@ "configurations": [ { "name": "Launch Chrome", + "type": "chrome", "request": "launch", - "type": "pwa-chrome", "url": "http://localhost:8080/examples/test-cases", - "webRoot": "${workspaceFolder}", - "preLaunchTask": "start" + "webRoot": "${workspaceFolder}/dist", + "preLaunchTask": "npm: start", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///src/*": "${workspaceFolder}/src/*" + } }, { "name": "Debug Jest Tests", @@ -23,8 +27,16 @@ "--runInBand" ], "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "port": 9229 + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Jest Current File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["${relativeFile}", "--no-cache"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d56157a..f62923622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,53 +1,368 @@ -## 0.98.7 _2024-02-26_ +## [Unreleased] + ### Issues Resolved -- **#2280** Handle better very deeply nested expressions +- In some cases, the `placeholder` attribute would not be displayed when the + mathfield was empty. +- When using static math, the font-familly for text content was not correctly + inherited from the parent element. + +## 0.101.0 _2024-07-17_ + +### Breaking Changes + +- The properties `mathVirtualKeyboard.actionKeycap`, + `mathVirtualKeyboard.shiftKeycap`, `mathVirtualKeyboard.backspaceKeycap`, + and `mathVirtualKeyboard.tabKeycap` have been removed. Use the more general + `mathVirtualKeyboard.setKeycap()` method to customize these keycaps, that + is `mathVirtualKeyboard.setKeycap('[action]', {...})` etc... + +### Improvements and New Features + +- Macros can now be specified with `renderMathInElement()` and `renderMathInDocument()` + using the `macros` option. For example: + + ```js + renderMathInElement(element, {macros: {RR: '\\mathbb{R}'}}) + ``` + +- Performance improvements for pages with many mathfields. The initial rendering + can be up to 2x as fast. +- Some keycaps in the virtual keyboard can be customized without having + to define an entire virtual keyboard layout. + + The `mathVirtualKeyboard.getKeycap()` give access to the definition of + special keycaps and `mathVirtualKeyboard.setKeycap()` can be used to + change that definition. + + The keycaps are one of these special shortcuts: + * `[left]`, `[right]`, `[up]`, `[down]`, `[return]`, `[action]`, + * `[space]`, `[tab]`, `[backspace]`, `[shift]`, + * `[undo]`, `[redo]`, `[foreground-color]`, `[background-color]`, + * `[hide-keyboard]`, + * `[.]`, `[,]`, + * `[0]`, `[1]`, `[2]`, `[3]`, `[4]`, + * `[5]`, `[6]`, `[7]`, `[8]`, `[9]`, + * `[+]`, `[-]`, `[*]`, `[/]`, `[^]`, `[_]`, `[=]`, `[.]`, + * `[(]`, `[)]` + + For example, to change the LaTeX inserted when the multiplication key is + pressed use: + + ```js + mathVirtualKeyboard.setKeycap('[*]', {latex: '\\times'}); + ``` + +### Issues Resolved + +- **#2455** Serialization to ASCII Math of brackets and braces is now correct. +- When using Chrome in some locale (such as `es-419`), the context menu would + not be displayed. +- When the `MathfieldElement.isFunction` handler is updated, re-render all + the mathfields on the page to take it into account. +- **#2415** A content change event is now dispatched when the value of the + mathfield is changed as a result of switch from LaTeX mode to math mode + by changing the selection. +- Dispatch a `contextmenu` event any time the context menu is about to be + displayed. This allows the event to be canceled. +- **#2413** When setting the `alphabeticLayout`, the current keyboard would not + be updated in some cases. +- **#2412** The serialization of some expressions to LaTeX could result in some spaces + being omitted. For example, `\lnot p` would serialize as `\lnotp`. +- **#2403** The virtual keyboard Keycap Variants panel was positioned incorrectly + when the page used a RTL layout direction. +- In the virtual keyboard, the background of the variant panel was sometimes + displayed transparently. +- **#2402** Characters inserted after a `\mathbb{}` command were not styled + correctly. +- The `math-virtual-keyboard-command` event was not dispatched when a + mathfield was focused and a keycap was pressed. +- There are now CSS selectors to customize the size of glyphs in the + virtual keyboard (shift, enter, etc...): + - `--keycap-glyph-size` + - `--keycap-glyph-size-lg` + - `--keycap-glyph-size-xl` + +- **#2397** When a `beforeinput` event was canceled, the text would still + be inserted when using the physical keyboard. +- **#2398** When a placeholder was the only element in a group, i.e. + `{\placeholder{}}`, the placeholder was not automatically selected. + +## 0.100.0 _2024-06-12_ + +### Issues Resolved + +- **#2396** Pressing the arrow keys in the virtual keyboard would not move the + selection in the mathfield and display a runtime error in the console. +- **#2395** Added a `dispatchEvent` command which can be attached to a + custom keycap. +- **#2392** Pressing the backspace key after typing several digits would + delete all the digits. + + Its first argument is the name of the dispatched event, and the second + argument is an object with the `detail` property, which is the data + associated with the event. + + ```ts + { + label: "✨", + command: "dispatchEvent('customEvent', {detail: 'some data'})" + } + ``` + + To handle the event, add an event listener to the mathfield element: + + ```js + mf.addEventListener('customEvent', (ev) => { + console.log(ev.detail); + }); + ``` + + +## 0.99.0 _2024-06-10_ + +### Breaking Changes + +- The `mf.offsetFromPoint()` method has been renamed `mf.getOffsetFromPoint()` + +- The `mf.setCaretPoint()` method has been replaced with `mf.position = mf.getOffsetFromPoint()` + +- The `mf.scriptDepth()` and `mf.hitboxFromOffset()` methodds have been + replaced with `mf.getElementInfo()`. + + The `getElementInfo()` method provides more information including any id + that may have been applied with `\htmlId{}`. + + It is useful from within a `click` handler to get more information about the + element that was clicked, e.g. + + ```js + mf.getElementInfo(mf.getOffsetFromPoint(ev.clientX, ev.clientY)) + ``` + + The info returned is an object with the following properties: + + ```ts + export type ElementInfo = { + /** The depth in the expression tree. 0 for top-level elements */ + depth?: number; + + /** The bounding box of the element */ + bounds?: DOMRect; + + /** id associated with this element or its ancestor, set with `\htmlId` or + `\cssId` + */ + id?: string; + + /** HTML attributes associated with element or its ancestores, set with + * `\htmlData` + */ + data?: Record; + + /** The mode (math, text or LaTeX) */ + mode?: ParseMode; + + /** A LaTeX representation of the element */ + latex?: string; + + /** The style (color, weight, variant, etc...) of this element. */ + style?: Style; + }; + ``` + + +### Bold + +The way bold is handled in LaTeX is particularly confusing, reflecting +limitations of the text rendering technology of the time. + +Various attempts have been made over the years to improve the rendering of +bold, but this has resulted in inconsistent behavior. Furthermore, various +implementations of LaTeX and LaTeX-like systems have implemented bold in +different ways. + +This release introduces a more consistent and intuitive handling of bold, +although it may result in different rendering of some formulas compared to +some implementations of LaTeX. + +The original bold command in LaTeX is `\mathbf`. This command renders its +argument using a bold variant of the current font. However, only letters and +numbers can be rendered by this command. It does not affect symbols, operators, +or greek characters. + +For example, `\mathbf{a+b}` will render as `𝐚+𝐛`, with the `a` and `b` in bold, +but the `+` in normal weight. Characters rendered by `\mathbf` are rendered +upright, even if they would have been rendered as italic otherwise. + +The `\boldsymbol` command is an alternative to `\mathbf` that affects more +characters, including Greek letters and symbols. It does not affect +the style of the characters, so they remain italic if they were italic before. +However, the inter-character spacing and italic correction may not be rendered correctly. + +The `\bm` command from the `bm` package is a more modern alternative that +affects even more characters. It also preserves the style of the characters, +so they remain italic if they were italic before. The inter-character spacing +and italic correction are handled correctly. + +The `\bm` command is recommended over `\boldsymbol` and `\mathbf`. However, +it is not part of the standard LaTeX distribution, so it may not always be available. + +When serializing to LaTeX, MathLive will now use `\mathbf` when possible, and +fall back to `\bm` when not. This should result in more consistent rendering +of bold text. + +When parsing, MathLive will interpret both `\mathbf`, `\boldsymbol` and `\bm` as +bold. + +The bold style is now consistently inherited by sub-expressions. + +Similarly, when applying a bold style using `mf.applyStyle({weight: "bold"})`, +the bold attribute is applied to the entire selection, not just the letters +and numbers. + + +### Mode Switching + +- **#2375** The `switch-mode` command has two optionals arguments, a prefix + and suffix. The prefix is inserted before the mode switch, and the suffix + after. The command was behaving incorrectly. It now behaves as expected. +- It is now possible to roundtrip between math and text mode. For example, + selecting a fraction `\frac{a}{b}` and pressing `alt+shift+T` will convert the + selection to `(a)/(b)`. Pressing `alt+shift+T` again will convert it back to + `\frac{a}{b}`. +- When in LaTeX mode, changing the selection would sometimes unexpectedly exit + LaTeX mode, for example after the Select All command. This has been fixed. + +### New Features + +- **`\href`** + + The `\href{url}{content}` command, a MathJax extension that allows a link + to be associated with some content, is now supported. + + Clicking on the content will open the link. By default, the link is opened + in a new window, and only links with a HTTP, HTTPS or FILE protocol are + allowed. This can be controlled by the new `MathfieldElement.openUrl` + property. This property is a function with a single argument, the URL to + be opened, that is called when the content of the `\href` command is clicked on. +- **Tooltip appearance** + + Added CSS variables to control the appearance of the toolip displayed with + `\mathtip` and `\texttip`: + - `--tooltip-border` + - `--tooltip-color` + - `--tooltip-background-color` + - `--tooltip-box-shadow` + - `--tooltip-border-radius`. +- The `maxMatrixCols` property has been added that specifies the maximum number + of columns that a matrix may have. The default value is 10, which follows the + default value from the amsmath package. The property applies to all of + the matrix environments (`matrix`, `pmatrix`, `bmatrix`, etc.). This property is + also accessible via the `max-matrix-cols` attribute. +- The virtual keyboard now supports variants for shifted-keys. This includes + support for Swedish specific characters such as `å`, `ä`, and `ö` and their + uppercase variants. +- Accept `"true"` and `"false"` as values for on/off attributes in the + `` element, for example ``. +- Added a `target` property (a `MathfieldElement`) to the `onMenuSelect` + arguments. +- **#2337** Added an option `MathfieldElement.restoreFocusWhenDocumentFocused` + to control whether a mathfield that was previously focused regains focus + when the tab or window regains focus. This is true by default and matches + the previous behavior, and the behavior of the ` + - x=\frac{\placeholder[numer]{}}{\placeholder[denom][correct]{3} + x=\frac{\placeholder[numer]{}}{\placeholder[denom][correct]{3} - - + + - - - + document.getElementById('mf-5-span').appendChild(mfe5); + }); + + + + \ No newline at end of file diff --git a/test/playwright-tests/max-matrix-cols.spec.ts b/test/playwright-tests/max-matrix-cols.spec.ts new file mode 100644 index 000000000..efda0f53a --- /dev/null +++ b/test/playwright-tests/max-matrix-cols.spec.ts @@ -0,0 +1,32 @@ +import type { MathfieldElement } from '../../src/public/mathfield-element'; + +import { test, expect } from '@playwright/test'; + +test('max-matrix-rows attribute', async ({ page }) => { + await page.goto('/dist/playwright-test-page/'); + + const testLatex = String.raw`\begin{bmatrix} a & b & c & d & e & f & g & h & i & j & k \\ l & m & n & o & p & q & r & s & t & u & v \end{bmatrix}`; + + await page + .locator('#mf-1') + .evaluate( + (e: MathfieldElement, latex: string) => (e.value = latex), + testLatex + ); + + await page + .locator('#mf-6') + .evaluate( + (e: MathfieldElement, latex: string) => (e.value = latex), + testLatex + ); + + // check latex (default #mf-1 should wrap after 10 columns, #mf-6 with max-matrix-cols=11 should not wrap) + expect( + await page.locator('#mf-1').evaluate((e: MathfieldElement) => e.getValue('latex-expanded')) + ).toBe("\\begin{bmatrix}a & b & c & d & e & f & g & h & i & j\\\\ k & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{}\\\\ l & m & n & o & p & q & r & s & t & u\\\\ v & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{} & \\placeholder{}\\end{bmatrix}"); + + expect( + await page.locator('#mf-6').evaluate((e: MathfieldElement) => e.getValue('latex-expanded')) + ).toBe("\\begin{bmatrix}a & b & c & d & e & f & g & h & i & j & k\\\\ l & m & n & o & p & q & r & s & t & u & v\\end{bmatrix}"); +}); \ No newline at end of file diff --git a/test/playwright-tests/mouse.spec.ts b/test/playwright-tests/mouse.spec.ts index 5884523ad..8790f5abe 100644 --- a/test/playwright-tests/mouse.spec.ts +++ b/test/playwright-tests/mouse.spec.ts @@ -35,3 +35,48 @@ test('double/triple click to select', async ({ page }) => { String.raw`\left(x+y\right)-\left(125+s\right)=34` ); }); + +test('context menu select all and cut', async ({ page, browserName }) => { + test.skip( + browserName === 'webkit' && Boolean(process.env.CI), + 'Keyboard paste does not work when headless on Linux (works when run with gui on Linux or headless/gui on MacOs)' + ); + + const modifierKey = /Mac|iPod|iPhone|iPad/.test( + await page.evaluate(() => navigator.platform) + ) + ? 'Meta' + : 'Control'; + + await page.goto('/dist/playwright-test-page/'); + + await page.locator('#mf-1').pressSequentially('x+y=30'); + + // check initial contents + expect( + await page.locator('#mf-1').evaluate((mfe: MathfieldElement) => mfe.value) + ).toBe('x+y=30'); + + // use select all and cut from the context menu + await page.locator('#mf-1').click({button: "right"}); + await page.locator('text=Select All').click(); + await new Promise((resolver) => setTimeout(resolver, 1000)); + await page.locator('#mf-1').click({button: "right"}); + await page.locator('text=Cut').click(); + await new Promise((resolver) => setTimeout(resolver, 1000)); + + // make sure field is empty after + expect( + await page.locator('#mf-1').evaluate((mfe: MathfieldElement) => mfe.value) + ).toBe(''); + + // paste original contents back + // need to use keyboard shortcut since context menu paste requires browser permission + await page.locator('#mf-1').press(`${modifierKey}+v`); + + // initial contents should now be there + expect( + await page.locator('#mf-1').evaluate((mfe: MathfieldElement) => mfe.value.trim()) + ).toBe('x+y=30'); +}); + diff --git a/test/playwright-tests/physical-keyboard.spec.ts b/test/playwright-tests/physical-keyboard.spec.ts index 99f8e51c9..f796d2199 100644 --- a/test/playwright-tests/physical-keyboard.spec.ts +++ b/test/playwright-tests/physical-keyboard.spec.ts @@ -124,7 +124,7 @@ test('escape to enter/exit latex mode', async ({ page }) => { // use latex mode for math field with default settings await page.locator('#mf-1').press('Escape'); - await page.locator('#mf-1').pressSequentially('frac'); + await page.locator('#mf-1').pressSequentially('\\frac'); await page.locator('#mf-1').press('Escape'); // full fraction will be selected, navigate back to numerator await page.locator('#mf-1').press('ArrowLeft'); @@ -442,3 +442,51 @@ test('keyboard shortcuts with placeholders (#2291, #2293, #2294)', async ({ await page.locator('#mf-1').evaluate((e: MathfieldElement) => e.value) ).toBe(String.raw`\frac{f\cdot g}{a}\cdot x`); }); + +test('keyboard cut and paste', async ({ page, browserName }) => { + test.skip( + browserName === 'webkit' && Boolean(process.env.CI), + 'Keyboard paste does not work when headless on Linux (works when run with gui on Linux or headless/gui on MacOs)' + ); + + const modifierKey = /Mac|iPod|iPhone|iPad/.test( + await page.evaluate(() => navigator.platform) + ) + ? 'Meta' + : 'Control'; + + let selectAllCommand = `${modifierKey}+a`; + if (modifierKey === 'Meta' && browserName === 'chromium') { + // Cmd-a not working with Chromium on Mac, need to use Control-A + // Cmd-a works correctly on Chrome and Edge on Mac + selectAllCommand = 'Control+a'; + } + + await page.goto('/dist/playwright-test-page/'); + + await page.locator('#mf-1').pressSequentially('30=r+t'); + + // check initial contents + expect( + await page.locator('#mf-1').evaluate((mfe: MathfieldElement) => mfe.value) + ).toBe('30=r+t'); + + // select all and cut + await page.locator('#mf-1').press(selectAllCommand); + await page.locator('#mf-1').press(`${modifierKey}+x`); + + // should be empty after cut + expect( + await page.locator('#mf-1').evaluate((mfe: MathfieldElement) => mfe.value) + ).toBe(''); + + // paste contents back + await page.locator('#mf-1').press(`${modifierKey}+v`); + + // initial contents should now be there + expect( + await page + .locator('#mf-1') + .evaluate((mfe: MathfieldElement) => mfe.value.trim()) + ).toBe('30=r+t'); +}); diff --git a/test/smoke/index.html b/test/smoke/index.html index c70f0368d..31a376f25 100644 --- a/test/smoke/index.html +++ b/test/smoke/index.html @@ -1,246 +1,820 @@ - - - Smoke Test - - - - - - -
-

Smoke Test

- -
-
- - 1+\sqrt{\frac{2}{3+x}}=x^{2+y} - - - + - - - - -

Selection

-
- -

LaTeX

-
- -

MathJSON

-
-
-
-
-
- -

ASCII Math

-
- -

MathML

-
-
- -

Latex to Speakable Text

-
- -
- - -
-
$$f(x) = \sin(x)$$
- \frac{1}{4} -
-
- - - - + function exprToString(expr) { + return exprToStringRecursive(expr.json, 0); + } + + + hello $ f(x) = \frac{1}{2} $ or $ \foo{hello} $ + + + + \ No newline at end of file diff --git a/test/virtual-keyboard/index.html b/test/virtual-keyboard/index.html index da815be9e..445b3d392 100644 --- a/test/virtual-keyboard/index.html +++ b/test/virtual-keyboard/index.html @@ -1,470 +1,504 @@ - - - Virtual Keyboard Test - - - - - - -
-

Virtual Keyboard Test

- -
-
- f(x)=\frac{1}{x+1}} - 1+\texttip{a}{hello world}+b+\alpha+\mathtip{\alpha+\gamma}{x=\frac34}+ - \frac{a+1} {b+\mathtip{\alpha+\gamma}{x=\frac34} }+ - \frac{a+1}{b+\alpha+\gamma} - - - - - x=\frac{\placeholder[numer]{}}{\placeholder[denom][correct]{3} - - x=\frac{3}{4} - - - -
- - - - - - - -
-
- - + - function updateButtons() { - if (kbd.visible) { - document.getElementById("hide-keyboard").classList.remove("hidden"); - document.getElementById("show-keyboard").classList.add("hidden"); - } else { - document.getElementById("hide-keyboard").classList.add("hidden"); - document.getElementById("show-keyboard").classList.remove("hidden"); - } - } - - - + \ No newline at end of file diff --git a/typedoc.json b/typedoc.json index e56a9d4f3..b4b463dd3 100644 --- a/typedoc.json +++ b/typedoc.json @@ -30,7 +30,7 @@ "./plugins/grok-theme/index.mjs" ], "theme": "grok-theme", - "namedAnchors": true, + "useHTMLAnchors": true, "hidePageHeader": true, "hidePageTitle": true, "hideBreadcrumbs": true,