diff --git a/src/api.md b/src/api.md index fbfef0d26..86f144755 100644 --- a/src/api.md +++ b/src/api.md @@ -3007,7 +3007,7 @@ The depth in the expression tree. 0 for top-level elements optional id: string; ``` -id associated with this element or its ancestor, set with `\htmlId` or +id associated with this element or its ancestor, set with `\htmlId` or `\cssId` @@ -5990,29 +5990,6 @@ set onScrollIntoView(value): void #### Localization - - - - -##### MathfieldElement.fractionNavigationOrder - -```ts -static fractionNavigationOrder: "numerator-denominator" | "denominator-numerator" = 'numerator-denominator'; -``` - -When using the keyboard to navigate a fraction, the order in which the -numerator and navigator are traversed: -- "numerator-denominator": first the elements in the numerator, then - the elements in the denominator. -- "denominator-numerator": first the elements in the denominator, then - the elements in the numerator. In some East-Asian cultures, fractions - are read and written denominator first ("fēnzhī"). With this option - the keyboard navigation follows this convention. - -**Default**: `"numerator-denominator"` - - - @@ -6047,6 +6024,37 @@ set static decimalSeparator(value): void + + + + +##### MathfieldElement.fractionNavigationOrder + +```ts +get static fractionNavigationOrder(): "denominator-numerator" | "numerator-denominator" +``` + +When using the keyboard to navigate a fraction, the order in which the +numerator and navigator are traversed: +- "numerator-denominator": first the elements in the numerator, then + the elements in the denominator. +- "denominator-numerator": first the elements in the denominator, then + the elements in the numerator. In some East-Asian cultures, fractions + are read and written denominator first ("fēnzhī"). With this option + the keyboard navigation follows this convention. + +**Default**: `"numerator-denominator"` + +```ts +set static fractionNavigationOrder(s): void +``` + +• **s**: `"denominator-numerator"` \| `"numerator-denominator"` + +`"denominator-numerator"` \| `"numerator-denominator"` + + + @@ -6407,9 +6415,9 @@ readonly [`Keybinding`](#keybinding)[] ```ts get letterShapeStyle(): | "auto" - | "french" | "tex" | "iso" + | "french" | "upright" ``` @@ -6421,15 +6429,15 @@ set letterShapeStyle(value): void • **value**: \| `"auto"` - \| `"french"` \| `"tex"` \| `"iso"` + \| `"french"` \| `"upright"` \| `"auto"` - \| `"french"` \| `"tex"` \| `"iso"` + \| `"french"` \| `"upright"` diff --git a/src/atoms/composition.ts b/src/atoms/composition.ts index 559afb81b..78d26eb69 100644 --- a/src/atoms/composition.ts +++ b/src/atoms/composition.ts @@ -1,4 +1,4 @@ -import type { ParseMode, Style } from '../public/core-types'; +import type { ParseMode } from '../public/core-types'; import { Atom } from '../core/atom-class'; import { Box } from '../core/box'; diff --git a/src/atoms/genfrac.ts b/src/atoms/genfrac.ts index 39fabc548..bc5e70dc9 100644 --- a/src/atoms/genfrac.ts +++ b/src/atoms/genfrac.ts @@ -6,8 +6,8 @@ import { VBox } from '../core/v-box'; import { makeCustomSizedDelim, makeNullDelimiter } from '../core/delimiters'; import { Context } from '../core/context'; import { AXIS_HEIGHT } from '../core/font-metrics'; -import type { AtomJson } from 'core/types'; -import { _MathEnvironment } from 'core/math-environment'; +import type { AtomJson } from '../core/types'; +import { _MathEnvironment } from '../core/math-environment'; export type GenfracOptions = { continuousFraction?: boolean; diff --git a/src/atoms/group.ts b/src/atoms/group.ts index d066b03e4..940832bfc 100644 --- a/src/atoms/group.ts +++ b/src/atoms/group.ts @@ -1,4 +1,4 @@ -import type { ParseMode, Style } from '../public/core-types'; +import type { ParseMode } from '../public/core-types'; import { Atom } from '../core/atom-class'; import type { Context } from '../core/context'; diff --git a/src/atoms/latex.ts b/src/atoms/latex.ts index b650ffb6c..7576cc3e5 100644 --- a/src/atoms/latex.ts +++ b/src/atoms/latex.ts @@ -1,5 +1,3 @@ -import type { Style } from '../public/core-types'; - import { Atom } from '../core/atom-class'; import { Box } from '../core/box'; import { Context } from '../core/context'; diff --git a/src/atoms/subsup.ts b/src/atoms/subsup.ts index cbb6ec4ae..0d9af0e19 100644 --- a/src/atoms/subsup.ts +++ b/src/atoms/subsup.ts @@ -11,6 +11,27 @@ export class SubsupAtom extends Atom { this.subsupPlacement = 'auto'; } + get children(): Readonly { + if (!this._children) { + const result: Atom[] = []; + + const sub = this.branch('subscript'); + if (sub) + for (const x of sub) { + result.push(...x.children); + result.push(x); + } + const sup = this.branch('superscript'); + if (sup) + for (const x of sup) { + result.push(...x.children); + result.push(x); + } + this._children = result; + } + return this._children; + } + static fromJson(json: { [key: string]: any }): SubsupAtom { const result = new SubsupAtom(json as any); for (const branch of NAMED_BRANCHES) diff --git a/src/core/atom-class.ts b/src/core/atom-class.ts index d5c3df27f..33bd1c644 100644 --- a/src/core/atom-class.ts +++ b/src/core/atom-class.ts @@ -360,7 +360,7 @@ export class Atom { } bodyToLatex(options: ToLatexOptions): string { - let defaultMode = + const defaultMode = options.defaultMode ?? (this.mode === 'math' ? 'math' : 'text'); return Mode.serialize(this.body, { ...options, defaultMode }); @@ -549,9 +549,7 @@ export class Atom { this.style.variant = style.variant; if (style.variantStyle && !this.style.variantStyle) this.style.variantStyle = style.variantStyle; - } else { - this.style = { ...this.style, ...style }; - } + } else this.style = { ...this.style, ...style }; if (this.style.fontFamily === 'none') delete this.style.fontFamily; diff --git a/src/core/modes-math.ts b/src/core/modes-math.ts index 21fa6e3ac..fad53346d 100644 --- a/src/core/modes-math.ts +++ b/src/core/modes-math.ts @@ -273,9 +273,8 @@ function emitBoldRun(run: Atom[], options: ToLatexOptions): string[] { // Get the content of the run const value = joinLatex(x.map((x) => x.value ?? '')); - if (/^[a-zA-Z0-9]+$/.test(value)) { + if (/^[a-zA-Z0-9]+$/.test(value)) return latexCommand('\\mathbf', joinLatex(emitVariantRun(x, options))); - } // If the run contains a mix of characters, use `\bm` return latexCommand('\\bm', joinLatex(emitVariantRun(x, options))); diff --git a/src/editor-mathfield/autocomplete.ts b/src/editor-mathfield/autocomplete.ts index 22f426b62..449eafcd9 100644 --- a/src/editor-mathfield/autocomplete.ts +++ b/src/editor-mathfield/autocomplete.ts @@ -160,9 +160,9 @@ export function complete( if (completion === 'reject') return true; - let style = { ...computeInsertStyle(mathfield) }; + const style = { ...computeInsertStyle(mathfield) }; // If we're inserting a non-alphanumeric character, reset the variant - if (!/^[a-zA-Z0-9]$/.test(latex) && this.styleBias !== 'none') { + if (!/^[a-zA-Z0-9]$/.test(latex) && mathfield.styleBias !== 'none') { style.variant = 'normal'; style.variantStyle = undefined; } diff --git a/src/editor-mathfield/keyboard-input.ts b/src/editor-mathfield/keyboard-input.ts index 3247a303b..7e48f2cf6 100644 --- a/src/editor-mathfield/keyboard-input.ts +++ b/src/editor-mathfield/keyboard-input.ts @@ -616,7 +616,7 @@ function insertMathModeChar(mathfield: _Mathfield, c: string): void { return; } - let style = { ...computeInsertStyle(mathfield) }; + const style = { ...computeInsertStyle(mathfield) }; // If we're inserting a non-alphanumeric character, reset the variant if (!/[a-zA-Z0-9]/.test(c) && mathfield.styleBias !== 'none') { diff --git a/src/editor-mathfield/mathfield-private.ts b/src/editor-mathfield/mathfield-private.ts index 77b55b18e..4883a0c4d 100644 --- a/src/editor-mathfield/mathfield-private.ts +++ b/src/editor-mathfield/mathfield-private.ts @@ -5,15 +5,15 @@ import type { Keybinding, KeyboardLayoutName, } from '../public/options'; +import type { Mathfield } from '../public/mathfield'; import type { - Mathfield, InsertOptions, OutputFormat, Offset, Range, Selection, ApplyStyleOptions, -} from '../public/mathfield'; +} from '../public/core-types'; import { canVibrate } from '../ui/utils/capabilities'; @@ -452,7 +452,7 @@ If you are using Vue, this may be because you are using the runtime-only build o // to adjust the UI (popover, etc...) window.addEventListener('resize', this, { signal }); document.addEventListener('scroll', this, { signal }); - this.resizeObserver = new ResizeObserver((entries) => { + this.resizeObserver = new ResizeObserver((_entries) => { if (this.resizeObserverStarted) { this.resizeObserverStarted = false; return; @@ -1125,7 +1125,7 @@ If you are using Vue, this may be because you are using the runtime-only build o } else if (s === '&') addColumnAfter(this.model); else { if (this.model.selectionIsCollapsed) { - let style = { ...computeInsertStyle(this) }; + const style = { ...computeInsertStyle(this) }; // If we're inserting a non-alphanumeric character, reset the variant if (!/^[a-zA-Z0-9]$/.test(s) && this.styleBias !== 'none') { style.variant = 'normal'; @@ -1425,8 +1425,7 @@ If you are using Vue, this may be because you are using the runtime-only build o locked?: boolean; correctness?: 'correct' | 'incorrect' | 'undefined'; }): string[] { - return this.model - .getAllAtoms() + return this.model.atoms .filter((a: PromptAtom) => { if (a.type !== 'prompt') return false; if (!filter) return true; @@ -1600,11 +1599,11 @@ If you are using Vue, this may be because you are using the runtime-only build o // If we're at the start or the end of a LaTeX group, // move inside the group and don't switch mode. const sibling = model.at(pos + 1); - if (sibling?.type === 'first' && sibling.mode === 'latex') { + if (sibling?.type === 'first' && sibling.mode === 'latex') model.position = pos + 1; - } else if (latexGroup && sibling?.mode !== 'latex') { + else if (latexGroup && sibling?.mode !== 'latex') model.position = pos - 1; - } else { + else { // We may have moved from math to text, or text to math. this.switchMode(mode); } diff --git a/src/editor-mathfield/mode-editor-latex.ts b/src/editor-mathfield/mode-editor-latex.ts index bd82d0918..59e1660cb 100644 --- a/src/editor-mathfield/mode-editor-latex.ts +++ b/src/editor-mathfield/mode-editor-latex.ts @@ -1,5 +1,5 @@ /* eslint-disable no-new */ -import { Offset, Range, InsertOptions } from '../public/mathfield'; +import { Offset, Range, InsertOptions } from '../public/core-types'; import { LatexAtom, LatexGroupAtom } from '../atoms/latex'; import { range } from '../editor-model/selection-utils'; import { Atom } from '../core/atom-class'; diff --git a/src/editor-mathfield/mode-editor-math.ts b/src/editor-mathfield/mode-editor-math.ts index 829d2bcb2..3c0e1bed4 100644 --- a/src/editor-mathfield/mode-editor-math.ts +++ b/src/editor-mathfield/mode-editor-math.ts @@ -2,7 +2,7 @@ import type { Expression } from '@cortex-js/compute-engine/dist/types/math-json/math-json-format'; -import { InsertOptions, Offset, OutputFormat } from '../public/mathfield'; +import type { InsertOptions, Offset, OutputFormat } from '../public/core-types'; import { requestUpdate } from './render'; @@ -169,7 +169,7 @@ export class MathModeEditor extends ModeEditor { } insert(model: _Model, input: string, options: InsertOptions): boolean { - let data = + const data = typeof input === 'string' ? input : globalThis.MathfieldElement.computeEngine?.box(input).latex ?? ''; diff --git a/src/editor-mathfield/mode-editor-text.ts b/src/editor-mathfield/mode-editor-text.ts index 55dc8d973..6bf224acf 100644 --- a/src/editor-mathfield/mode-editor-text.ts +++ b/src/editor-mathfield/mode-editor-text.ts @@ -1,7 +1,8 @@ /* eslint-disable no-new */ -import type { InsertOptions } from '../public/mathfield'; +import type { InsertOptions } from '../public/core-types'; import { parseLatex } from '../core/core'; +import type { ContextInterface } from '../core/types'; import { Atom } from '../core/atom-class'; import { _Model } from '../editor-model/model-private'; import { range } from '../editor-model/selection-utils'; @@ -10,7 +11,6 @@ import { applyStyleToUnstyledAtoms } from '../editor-model/styling'; import { _Mathfield } from './mathfield-private'; import { ModeEditor } from './mode-editor'; import { requestUpdate } from './render'; -import type { ContextInterface } from '../core/types'; export class TextModeEditor extends ModeEditor { constructor() { diff --git a/src/editor-mathfield/mode-editor.ts b/src/editor-mathfield/mode-editor.ts index 51460c129..f50d475f1 100644 --- a/src/editor-mathfield/mode-editor.ts +++ b/src/editor-mathfield/mode-editor.ts @@ -3,7 +3,7 @@ import { TextAtom } from '../atoms/text'; import { _Model } from '../editor-model/model-private'; import { range } from '../editor-model/selection-utils'; import { MODE_SHIFT_COMMANDS } from '../formats/parse-math-string'; -import { InsertOptions, OutputFormat, Range } from '../public/mathfield'; +import type { InsertOptions, OutputFormat, Range } from '../public/core-types'; import { _Mathfield } from './mathfield-private'; const CLIPBOARD_LATEX_BEGIN = '$$'; diff --git a/src/editor-mathfield/pointer-input.ts b/src/editor-mathfield/pointer-input.ts index 907bd394d..097743bdb 100644 --- a/src/editor-mathfield/pointer-input.ts +++ b/src/editor-mathfield/pointer-input.ts @@ -4,7 +4,7 @@ import { requestUpdate } from './render'; import { Atom } from '../core/atom-class'; import { acceptCommandSuggestion } from './autocomplete'; import { selectGroup } from '../editor-model/commands-select'; -import type { Offset } from 'public/mathfield'; +import type { Offset } from 'public/core-types'; let gLastTap: { x: number; y: number; time: number } | null = null; let gTapCount = 0; diff --git a/src/editor-mathfield/render.ts b/src/editor-mathfield/render.ts index 1baa533be..f6c4a2b84 100644 --- a/src/editor-mathfield/render.ts +++ b/src/editor-mathfield/render.ts @@ -197,9 +197,7 @@ export function render( ) hideMenu = true; // If the width of the element is less than 50px, hide the menu - if (!hideMenu && field.offsetWidth < 50) { - hideMenu = true; - } + if (!hideMenu && field.offsetWidth < 50) hideMenu = true; menuToggle.style.display = hideMenu ? 'none' : ''; } @@ -426,7 +424,6 @@ export function reparse(mathfield: _Mathfield | null): void { } export function reparseAllMathfields(): void { - for (const mathfield of document.querySelectorAll('.ML__mathfield')) { + for (const mathfield of document.querySelectorAll('.ML__mathfield')) if ('_mathfield' in mathfield) reparse(mathfield._mathfield as _Mathfield); - } } diff --git a/src/editor-mathfield/styling.ts b/src/editor-mathfield/styling.ts index 6e913aa69..fcf7fc692 100644 --- a/src/editor-mathfield/styling.ts +++ b/src/editor-mathfield/styling.ts @@ -192,11 +192,12 @@ export function defaultInsertStyleHook( if (bias === 'none') return mathfield.defaultStyle; // In text mode, we inherit the style of the sibling atom - if (model.mode === 'text') + if (model.mode === 'text') { return ( model.at(bias === 'right' ? info.after : info.before)?.style ?? mathfield.defaultStyle ); + } if (model.mode === 'math') { const atom = model.at(bias === 'right' ? info.after : info.before); diff --git a/src/editor-mathfield/utils.ts b/src/editor-mathfield/utils.ts index 43d4f6f11..8b95e45d9 100644 --- a/src/editor-mathfield/utils.ts +++ b/src/editor-mathfield/utils.ts @@ -1,5 +1,5 @@ import { Atom } from '../core/atom-class'; -import type { ElementInfo, Offset, Range } from '../public/mathfield'; +import type { ElementInfo, Offset, Range } from '../public/core-types'; import { OriginValidator } from '../public/options'; import { _Mathfield } from './mathfield-private'; @@ -223,16 +223,17 @@ export function getElementInfo( const atom = mf.model.at(offset); if (!atom) return undefined; - let result: ElementInfo = {}; + const result: ElementInfo = {}; const bounds = getAtomBounds(mf, atom); - if (bounds) + if (bounds) { result.bounds = new DOMRect( bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top ); + } result.depth = atom.treeDepth - 2; @@ -262,9 +263,8 @@ export function getElementInfo( } if (a.command === '\\htmlId' || a.command === '\\cssId') { - if (!result.id && a.args && typeof a.args[0] === 'string') { + if (!result.id && a.args && typeof a.args[0] === 'string') result.id = a.args[0]; - } } a = a.parent; } diff --git a/src/editor-model/commands.ts b/src/editor-model/commands.ts index 92cc23fe6..1427d5eb2 100644 --- a/src/editor-model/commands.ts +++ b/src/editor-model/commands.ts @@ -4,7 +4,7 @@ import { ArrayAtom } from '../atoms/array'; import { LatexAtom } from '../atoms/latex'; import { TextAtom } from '../atoms/text'; import { LETTER_AND_DIGITS } from '../latex-commands/definitions-utils'; -import type { Offset, Selection } from '../public/mathfield'; +import type { Offset, Selection } from '../public/core-types'; import { getCommandSuggestionRange } from '../editor-mathfield/mode-editor-latex'; import { PromptAtom } from '../atoms/prompt'; import { getLocalDOMRect } from 'editor-mathfield/utils'; diff --git a/src/editor-model/delete.ts b/src/editor-model/delete.ts index 73b71145e..8ed6b2f09 100644 --- a/src/editor-model/delete.ts +++ b/src/editor-model/delete.ts @@ -1,5 +1,4 @@ import type { ContentChangeType } from '../public/options'; -import type { Range } from '../public/mathfield'; import { LeftRightAtom } from '../atoms/leftright'; import { Atom } from '../core/atom'; @@ -7,6 +6,8 @@ import { _Model } from './model-private'; import { range } from './selection-utils'; import { MathfieldElement } from 'public/mathfield-element'; import type { Branch } from 'core/types'; +import type { Range } from 'public/core-types'; + // import { // arrayFirstCellByRow, // arrayColRow, diff --git a/src/editor-model/model-private.ts b/src/editor-model/model-private.ts index ebc61b620..91250d65f 100644 --- a/src/editor-model/model-private.ts +++ b/src/editor-model/model-private.ts @@ -1,11 +1,9 @@ import type { - Model, - Mathfield, Offset, Range, Selection, OutputFormat, -} from '../public/mathfield'; +} from '../public/core-types'; import type { ContentChangeOptions, ContentChangeType, @@ -39,6 +37,7 @@ import type { ModelState, GetAtomOptions, AnnounceVerb } from './types'; import type { BranchName, ToLatexOptions } from 'core/types'; import { isValidMathfield } from '../editor-mathfield/utils'; +import { Mathfield, Model } from 'public/mathfield'; /** @internal */ export class _Model implements Model { @@ -378,21 +377,6 @@ export class _Model implements Model { return result; } - /** - * Unlike `getAtoms()`, the argument here is an index - * Return all the atoms, in order, starting at startingIndex - * then looping back at the beginning - */ - getAllAtoms(startingIndex = 0): Readonly { - const result: Atom[] = []; - const last = this.lastOffset; - for (let i = startingIndex; i <= last; i++) result.push(this.atoms[i]); - - for (let i = 0; i < startingIndex; i++) result.push(this.atoms[i]); - - return result; - } - findAtom( filter: (x: Atom) => boolean, startingIndex = 0, @@ -569,6 +553,7 @@ export class _Model implements Model { skipPlaceholders: format === 'latex-without-placeholders', defaultMode: this.mathfield.options.defaultMode, }; + return joinLatex( ranges.map((range) => Atom.serialize(this.getAtoms(range), options)) ); @@ -585,13 +570,13 @@ export class _Model implements Model { /** * Unlike `setSelection`, this method is intended to be used in response - * to a user action, and it performs various adjustments to result - * in a more intuitive selection. + * to a user action, and it performs various adjustments to result in a more + * intuitive selection. + * * For example: - * - when all the children of an atom are selected, the atom - * become selected. - * - this method will *not* change the anchor, but may result - * in a selection whose boundary is outside the anchor + * - when all the children of an atom are selected, the atom become selected. + * - this method will *not* change the anchor, but may result in a selection + * whose boundary is outside the anchor */ extendSelectionTo(anchor: Offset, position: Offset): boolean { if (!this.mathfield.contentEditable && this.mathfield.userSelect === 'none') @@ -675,8 +660,8 @@ export class _Model implements Model { defaultAnnounceHook(this.mathfield, command, previousPosition, atoms); } - // Suppress notification while scope is executed, - // then notify of content change, and selection change (if actual change) + // Suppress notification while scope is executed, then notify of content + // change, and selection change (if actual change) deferNotifications( options: { content?: boolean; diff --git a/src/editor-model/selection-utils.ts b/src/editor-model/selection-utils.ts index 2ff7344fd..34bbfbc4a 100644 --- a/src/editor-model/selection-utils.ts +++ b/src/editor-model/selection-utils.ts @@ -1,5 +1,4 @@ -import { ParseMode } from 'public/core-types'; -import { Offset, Range, Selection } from '../public/mathfield'; +import type { ParseMode, Offset, Range, Selection } from '../public/core-types'; import { _Model } from './model-private'; export function compareSelection( diff --git a/src/editor-model/styling.ts b/src/editor-model/styling.ts index 6cd3e2afc..989b2ae1c 100644 --- a/src/editor-model/styling.ts +++ b/src/editor-model/styling.ts @@ -1,22 +1,20 @@ import { Atom } from '../core/atom'; import type { _Model } from './model-private'; -import { Range } from '../public/mathfield'; import { isArray } from '../common/types'; import { DEFAULT_FONT_SIZE } from '../core/font-metrics'; -import type { Style, VariantStyle } from '../public/core-types'; +import type { Style, VariantStyle, Range } from '../public/core-types'; import { PrivateStyle } from '../core/types'; export function applyStyleToUnstyledAtoms( - atom: Atom | ReadonlyArray | undefined, + atom: Atom | readonly Atom[] | undefined, style?: Style ): void { if (!atom || !style) return; if (isArray(atom)) { // Apply styling options to each atom atom.forEach((x) => applyStyleToUnstyledAtoms(x, style)); - } else if (typeof atom === 'object') { + } else if (typeof atom === 'object') atom.applyStyle(style, { unstyledOnly: true }); - } } /** diff --git a/src/editor-model/types.ts b/src/editor-model/types.ts index 6fdde8a42..cd224b5f3 100644 --- a/src/editor-model/types.ts +++ b/src/editor-model/types.ts @@ -1,6 +1,5 @@ import type { AtomJson } from 'core/types'; -import type { ParseMode } from 'public/core-types'; -import type { Selection } from 'public/mathfield'; +import type { ParseMode, Selection } from 'public/core-types'; export type AnnounceVerb = | 'plonk' diff --git a/src/editor/commands.ts b/src/editor/commands.ts index 274e941ea..03b2c678b 100644 --- a/src/editor/commands.ts +++ b/src/editor/commands.ts @@ -241,12 +241,12 @@ export function parseCommand( ): [SelectorPrivate, ...any[]] | SelectorPrivate | undefined { if (!command) return undefined; if (isArray(command) && command.length > 0) { - let selector = command[0]; + const selector = command[0]; // Convert kebab case (like-this) to camel case (likeThis). selector.replace(/-\w/g, (m) => m[1].toUpperCase()); - if (selector === 'performWithFeedback' && command.length === 2) { + if (selector === 'performWithFeedback' && command.length === 2) return [selector, parseCommand(command[1])]; - } + return [selector as SelectorPrivate, ...command.slice(1)]; } @@ -257,7 +257,7 @@ export function parseCommand( if (match) { const selector = match[1]; selector.replace(/-\w/g, (m) => m[1].toUpperCase()); - let args = match[2].split(',').map((x) => x.trim()); + const args = match[2].split(',').map((x) => x.trim()); return [ selector as SelectorPrivate, ...args.map((arg) => { @@ -280,7 +280,7 @@ export function parseCommand( ]; } - let selector = command; + const selector = command; selector.replace(/-\w/g, (m) => m[1].toUpperCase()); return selector as SelectorPrivate; diff --git a/src/editor/default-menu.ts b/src/editor/default-menu.ts index f637dbc6e..c649dab95 100644 --- a/src/editor/default-menu.ts +++ b/src/editor/default-menu.ts @@ -727,10 +727,11 @@ function variantMenuItem( id: `variant-${variant}`, label: () => { const textSelection = getSelectionPlainString(mf); - if (textSelection.length < 12) + if (textSelection.length < 12) { return convertLatexToMarkup( `\\${command}{${getSelectionPlainString(mf)}}` ); + } return localize(tooltip) ?? tooltip; }, class: 'ML__xl', @@ -753,10 +754,11 @@ function variantStyleMenuItem( label: () => { const textSelection = getSelectionPlainString(mf); - if (textSelection.length > 0 && textSelection.length < 12) + if (textSelection.length > 0 && textSelection.length < 12) { return convertLatexToMarkup( `\\${command}{${getSelectionPlainString(mf)}}` ); + } return localize(tooltip) ?? tooltip; }, diff --git a/src/editor/undo.ts b/src/editor/undo.ts index ec7669fbe..bc99962f3 100644 --- a/src/editor/undo.ts +++ b/src/editor/undo.ts @@ -1,7 +1,7 @@ import type { ModelState } from 'editor-model/types'; import type { _Model } from '../editor-model/model-private'; -import type { Selection } from '../public/mathfield'; +import type { Selection } from '../public/core-types'; export class UndoManager { // Maximum number of undo/redo states diff --git a/src/formats/atom-to-math-json.ts b/src/formats/atom-to-math-json.ts new file mode 100644 index 000000000..7ec74f08f --- /dev/null +++ b/src/formats/atom-to-math-json.ts @@ -0,0 +1,10 @@ +import { Atom } from 'core/atom'; +import { Expression, Range } from 'public/core-types'; + +export function atomToMathJson( + atom: Atom | Readonly | undefined, + range: Range | undefined +): Expression { + // @todo + return ''; +} diff --git a/src/formats/parse-math-string.ts b/src/formats/parse-math-string.ts index 0539076e1..310af6a94 100644 --- a/src/formats/parse-math-string.ts +++ b/src/formats/parse-math-string.ts @@ -1,4 +1,4 @@ -import { OutputFormat } from '../public/mathfield'; +import { OutputFormat } from '../public/core-types'; import { InlineShortcutDefinitions, getInlineShortcut, @@ -216,8 +216,8 @@ function parseMathArgument( let match = ''; s = s.trim(); let rest = s; - let lFence = s.charAt(0); - let rFence = { '(': ')', '{': '}', '[': ']' }[lFence]; + const lFence = s.charAt(0); + const rFence = { '(': ')', '{': '}', '[': ']' }[lFence]; if (rFence) { // It's a fence let level = 1; diff --git a/src/mathlive.ts b/src/mathlive.ts index 969e52b71..314c3a75c 100644 --- a/src/mathlive.ts +++ b/src/mathlive.ts @@ -62,11 +62,11 @@ export function globalMathLive(): MathLiveGlobal { */ export function renderMathInDocument(options?: StaticRenderOptions): void { - if (document.readyState === 'loading') + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => renderMathInElement(document.body, options) ); - else renderMathInElement(document.body, options); + } else renderMathInElement(document.body, options); } function getElement(element: string | HTMLElement): HTMLElement | null { diff --git a/src/public/commands.ts b/src/public/commands.ts index 25ed7f5e2..9764fa242 100644 --- a/src/public/commands.ts +++ b/src/public/commands.ts @@ -1,7 +1,12 @@ import type { Keys } from './types-utils'; -import type { ParseMode, Style, TabularEnvironment } from './core-types'; -import type { InsertOptions, Mathfield, Model } from './mathfield'; +import type { + InsertOptions, + ParseMode, + Style, + TabularEnvironment, +} from './core-types'; +import type { Mathfield, Model } from './mathfield'; /** * How much of the formula should be spoken: diff --git a/src/public/core-types.ts b/src/public/core-types.ts index 6779c4348..ac0267af9 100644 --- a/src/public/core-types.ts +++ b/src/public/core-types.ts @@ -456,3 +456,207 @@ export type Environment = | 'displaymath' | 'center' | TabularEnvironment; + +/** @category MathJSON */ +export declare type Expression = + | number + | string + | { [key: string]: any } + | [Expression, ...Expression[]]; + +/** + * +| Format | Description | +| :-------------------- | :---------------------- | +| `"ascii-math"` | A string of [ASCIIMath](http://asciimath.org/). | +| `"latex"` | LaTeX rendering of the content, with LaTeX macros not expanded. | +| `"latex-expanded"` | All macros are recursively expanded to their definition. | +| `"latex-unstyled"` | Styling (background color, color) is ignored | +| `"latex-without-placeholders"` | Replace `\placeholder` commands with their body | +| `"math-json"` | A MathJSON abstract syntax tree, as an object literal formated as a JSON string. Note: you must import the CortexJS Compute Engine to obtain a result. | +| `"math-ml"` | A string of MathML markup. | +' `"plain-text"` | A plain text rendering of the content. | +| `"spoken"` | Spoken text rendering, using the default format defined in config, which could be either text or SSML markup. | +| `"spoken-text"` | A plain spoken text rendering of the content. | +| `"spoken-ssml"` | A SSML (Speech Synthesis Markup Language) version of the content, which can be used with some text-to-speech engines such as AWS. | +| `"spoken-ssml-with-highlighting"`| Like `"spoken-ssml"` but with additional annotations necessary for synchronized highlighting (read aloud). | + + * To use the`"math-json"` format the Compute Engine library must be loaded. Use for example: + * +```js +import "https://unpkg.com/@cortex-js/compute-engine?module"; +``` + * + */ +export type OutputFormat = + | 'ascii-math' + | 'latex' + | 'latex-expanded' + | 'latex-unstyled' + | 'latex-without-placeholders' + | 'math-json' + | 'math-ml' + | 'plain-text' + | 'spoken' + | 'spoken-text' + | 'spoken-ssml' + | 'spoken-ssml-with-highlighting'; + +export type InsertOptions = { + /** If `"auto"` or omitted, the current mode is used */ + mode?: ParseMode | 'auto'; + /** + * The format of the input string: + * + +| | | +|:------------|:------------| +|`"auto"`| The string is LaTeX fragment or command) (default)| +|`"latex"`| The string is a LaTeX fragment| + * + */ + format?: OutputFormat | 'auto'; + insertionMode?: + | 'replaceSelection' + | 'replaceAll' + | 'insertBefore' + | 'insertAfter'; + /** + * Describes where the selection + * will be after the insertion: + +| | | +| :---------- | :---------- | +|`"placeholder"`| The selection will be the first available placeholder in the text that has been inserted (default)| +|`"after"`| The selection will be an insertion point after the inserted text| +|`"before"`| The selection will be an insertion point before the inserted text| +|`"item"`| The inserted text will be selected| + */ + selectionMode?: 'placeholder' | 'after' | 'before' | 'item'; + + silenceNotifications?: boolean; + /** If `true`, the mathfield will be focused after + * the insertion + */ + focus?: boolean; + /** If `true`, provide audio and haptic feedback + */ + feedback?: boolean; + /** If `true`, scroll the mathfield into view after insertion such that the + * insertion point is visible + */ + scrollIntoView?: boolean; + + style?: Style; +}; + +export type ApplyStyleOptions = { + range?: Range; + operation?: 'set' | 'toggle'; + silenceNotifications?: boolean; +}; + +/** + * Some additional information about an element in the formula + */ + +export type ElementInfo = { + // start?: Offset; + // end?: Offset; + // parent?: ElementInfo; + // kind?: + // | 'accent' + // | 'cell' // Inside a matrix or environment + // | 'box' // Inside a rectangular box + // | 'composition' // Inside an input-method composition + // | 'enclosure' // A more complex kind of box/annotation displayed around a subexpression + // | 'error' + // | 'numerator' + // | 'denominator' + // | 'group' // Delimited with braces + // | 'latex' // Raw LaTeX composition + // | 'overlap' + // | 'above' + // | 'below' + // | 'phantom' + // | 'placeholder' + // | 'superscript' + // | 'subscript' + // | 'radicand' + // | 'index' + // | 'body' + // | 'text'; + + /** 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; +}; + +/** + * A position of the caret/insertion point from the beginning of the formula. + */ +export type Offset = number; + +/** + * A pair of offsets (boundary points) that can be used to denote a fragment + * of an expression. + * + * A range is said to be collapsed when start and end are equal. + * + * When specifying a range, a negative offset can be used to indicate an + * offset from the last valid offset, i.e. -1 is the last valid offset, -2 + * is one offset before that, etc... + * + * A normalized range will always be such that start <= end, start >= 0, + * end >= 0, start < lastOffset, end < lastOffset. All the methods return + * a normalized range. + * + * **See Also** + * * {@linkcode Selection} + */ + +export type Range = [start: Offset, end: Offset]; + +/** + * A selection is a set of ranges (to support discontinuous selection, for + * example when selecting a column in a matrix). + * + * If there is a single range and that range is collapsed, the selection is + * collapsed. + * + * A selection can also have a direction. While many operations are insensitive + * to the direction, a few are. For example, when selecting a fragment of an + * expression from left to right, the direction of this range will be "forward". + * Pressing the left arrow key will sets the insertion at the start of the range. + * Conversely, if the selection is made from right to left, the direction is + * "backward" and pressing the left arrow key will set the insertion point at + * the end of the range. + * + * **See Also** + * * {@linkcode Range} + */ +export type Selection = { + ranges: Range[]; + direction?: 'forward' | 'backward' | 'none'; +}; diff --git a/src/public/mathfield-element.ts b/src/public/mathfield-element.ts index 14e8a00b3..c182a424c 100644 --- a/src/public/mathfield-element.ts +++ b/src/public/mathfield-element.ts @@ -1,21 +1,20 @@ import type { Selector } from './commands'; import type { + Expression, LatexSyntaxError, LatexValue, MacroDictionary, + Offset, ParseMode, Registers, Style, -} from './core-types'; -import type { - InsertOptions, - OutputFormat, - Offset, - Range, Selection, - Mathfield, + Range, + OutputFormat, ElementInfo, -} from './mathfield'; + InsertOptions, +} from './core-types'; +import type { InsertStyleHook, Mathfield } from './mathfield'; import type { InlineShortcutDefinitions, Keybinding, @@ -51,15 +50,8 @@ import { getStylesheet, getStylesheetContent } from '../common/stylesheet'; import { Scrim } from '../ui/utils/scrim'; import { isOffset, isRange, isSelection } from 'editor-model/selection-utils'; import { KeyboardModifiers } from './ui-events-types'; -import { defaultInsertStyleHook } from 'editor-mathfield/styling'; -import { _MathEnvironment } from 'core/math-environment'; - -/** @category MathJSON */ -export declare type Expression = - | number - | string - | { [key: string]: any } - | [Expression, ...Expression[]]; +import { defaultInsertStyleHook } from '../editor-mathfield/styling'; +import { _MathEnvironment } from '../core/math-environment'; if (!isBrowser()) { console.error( @@ -343,12 +335,6 @@ const DEPRECATED_OPTIONS = { fractionNavigationOrder: 'MathfieldElement.fractionNavigationOrder = ...', }; -export type InsertStyleHook = ( - sender: Mathfield, - at: Offset, - info: { before: Offset; after: Offset } -) => Readonly @@ -51,7 +41,8 @@

Smoke Test

- x=\frac{1}{2} + x_{a+1}^{b+2} + @@ -89,9 +80,6 @@

MathML

-

Latex to Speakable Text

-
-
@@ -104,8 +92,7 @@

Latex to Speakable Text

renderMathInDocument, renderMathInElement, MathfieldElement, - convertLatexToMarkup, - convertLatexToSpeakableText, + convertLatexToMarkup } from "/dist/mathlive.mjs"; // } from "https://unpkg.com/mathlive?module"; @@ -605,6 +592,7 @@

Latex to Speakable Text

mf.addEventListener('mount', () => { setupMathfield(mf); + console.log(mf.getValue({ ranges: [[0, 5]] })); mf.addEventListener("input", (ev) => updateContent(mf)); @@ -674,7 +662,6 @@

Latex to Speakable Text

document.getElementById("mathml-render").innerHTML = `${mf.getValue("math-ml")}`; // setHtml('math-json', ce.parse(mf.value)); - setHtml("latex2speech", convertLatexToSpeakableText(mf.getValue())); const expr = mf.expression; if (expr) {