diff --git a/src/commands/math.ts b/src/commands/math.ts index 953b56f2f..fbf719d87 100644 --- a/src/commands/math.ts +++ b/src/commands/math.ts @@ -617,8 +617,23 @@ class MathBlock extends MathElement { } chToCmd(ch: string, options: CursorOptions) { var cons; - // exclude f because it gets a dedicated command with more spacing - if (ch.match(/^[a-eg-zA-Z]$/)) return new Letter(ch); + // extract customCharacters early so we can easily default it to 'f' + var customCharacters = options?.customCharacters ?? 'f'; + if ( + customCharacters.indexOf(ch) >= 0 && + (cons = (CharCmds as CharCmdsAny)[ch] || (LatexCmds as LatexCmdsAny)[ch]) + ) { + if (cons.constructor) { + return new cons(ch); + } else { + return cons(ch); + } + } else if ( + // the patch to exclude 'f' from this regex is no longer needed since we + // usecustomCharacters + ch.match(/^[a-zA-Z]$/) + ) + return new Letter(ch); else if (/^\d$/.test(ch)) return new Digit(ch); else if (options && options.typingSlashWritesDivisionSymbol && ch === '/') return (LatexCmds as LatexCmdsSingleCharBuilder)['รท'](ch); diff --git a/src/commands/math/advancedSymbols.ts b/src/commands/math/advancedSymbols.ts index d99465b66..76b802f29 100644 --- a/src/commands/math/advancedSymbols.ts +++ b/src/commands/math/advancedSymbols.ts @@ -644,12 +644,12 @@ LatexCmds.closecurlybrace = LatexCmds.rbrace = bindVanillaSymbol( '}', 'right brace' ); -LatexCmds.lbrack = bindVanillaSymbol('[', 'left bracket'); -LatexCmds.rbrack = bindVanillaSymbol(']', 'right bracket'); +LatexCmds.lbrack = bindVanillaSymbol('[', '[', 'left bracket'); +LatexCmds.rbrack = bindVanillaSymbol(']', ']', 'right bracket'); //various symbols -LatexCmds.slash = bindVanillaSymbol('/', 'slash'); -LatexCmds.vert = bindVanillaSymbol('|', 'vertical bar'); +LatexCmds.slash = bindVanillaSymbol('/', '/', 'slash'); +LatexCmds.vert = bindVanillaSymbol('|', '|', 'vertical bar'); LatexCmds.perp = LatexCmds.perpendicular = bindVanillaSymbol( '\\perp ', '⊥', diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index a41afbc05..d59f68aaf 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -647,7 +647,7 @@ function defaultAutoOpNames() { _maxLength: 9, }; var mostOps = ( - 'arg deg det dim exp gcd hom inf ker lg lim ln log max min sup' + + 'arg deg det dim exp gcd hom inf ker lg ln log max min sup' + ' limsup liminf injlim projlim Pr' ).split(' '); for (var i = 0; i < mostOps.length; i += 1) { diff --git a/src/commands/math/commands.ts b/src/commands/math/commands.ts index b8a672a64..ba147259f 100644 --- a/src/commands/math/commands.ts +++ b/src/commands/math/commands.ts @@ -953,7 +953,9 @@ var LiveFraction = leftward._groupingClass === 'mq-ellipsis-end') || leftward instanceof (LatexCmds.text || noop) || leftward instanceof SummationNotation || + leftward instanceof Limit || leftward.ctrlSeq === '\\ ' || + leftward.ctrlSeq === '&' || /^[,;:]$/.test(leftward.ctrlSeq as string) ) //lookbehind for operator ) @@ -1616,7 +1618,9 @@ LatexCmds.left = class extends MathCommand { var optWhitespace = Parser.optWhitespace; return optWhitespace - .then(regex(/^(?:[([|]|\\\{|\\langle(?![a-zA-Z])|\\lVert(?![a-zA-Z]))/)) + .then( + regex(/^(?:[([|]|\\\{|\\(?:lfloor|lceil|langle|lVert)(?![a-zA-z]))/) + ) .then(function (ctrlSeq) { var open = ctrlSeq.replace(/^\\/, ''); if (ctrlSeq == '\\langle') { @@ -1630,9 +1634,7 @@ LatexCmds.left = class extends MathCommand { return latexMathParser.then(function (block) { return string('\\right') .skip(optWhitespace) - .then( - regex(/^(?:[\])|]|\\\}|\\rangle(?![a-zA-Z])|\\rVert(?![a-zA-Z]))/) - ) + .then(regex(/^(?:[\])|]|\\\}|\\[a-zA-z]+)/)) .map(function (end) { var close = end.replace(/^\\/, ''); if (end == '\\rangle') { @@ -1869,3 +1871,40 @@ class EmbedNode extends MQSymbol { } } LatexCmds.embed = EmbedNode; +class Limit extends MathCommand { + constructor(ctrlSeq?: string, domView?: DOMView, textTemplate?: string[]) { + super(ctrlSeq, domView, textTemplate); + this.ctrlSeq = '\\lim'; + this.domView = new DOMView(1, function (blocks) { + return h('span', { class: 'mq-limit mq-non-leaf' }, [ + h('span', { class: 'mq-limit-label' }, [h.text('lim')]), + h.block('span', { class: 'mq-limit-sub mq-non-leaf' }, blocks[0]), + ]); + }); + this.textTemplate = ['lim(', ')']; + this.mathspeakTemplate = ['Limit', 'EndLimit']; + } + + override latexRecursive(ctx: LatexContext) { + this.checkCursorContextOpen(ctx); + ctx.latex += '\\lim_{'; + this.getEnd(L).latexRecursive(ctx); + ctx.latex += '}'; + this.checkCursorContextClose(ctx); + } + + override parser() { + return Parser.string('_') + .then(function () { + return latexMathParser; + }) + .map(function (block) { + const limit = new Limit('\\lim', undefined!, undefined!); + limit.blocks = [block]; + block.adopt(limit, 0, 0); + return limit; + }) + .or(super.parser()); + } +} +LatexCmds.limit = LatexCmds.lim = Limit; diff --git a/src/css/math.less b/src/css/math.less index 42845dac4..1f6b7ae56 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -1,12 +1,13 @@ // look here to see the digit layout strategy: // https://www.desmos.com/calculator/ctvh9utz0t -@digit-separator: .11em; -@expand-margin: .009em; -@contract-margin: -.01em; -@ellipsis-separator: .14em; +@digit-separator: 0.11em; +@expand-margin: 0.009em; +@contract-margin: -0.01em; +@ellipsis-separator: 0.14em; @ellipsis-internal-sep: 0.009em; -.mq-root-block, .mq-math-mode .mq-root-block { +.mq-root-block, +.mq-math-mode .mq-root-block { .inline-block; width: 100%; padding: 2px; @@ -30,7 +31,8 @@ margin-right: @contract-margin; } - .mq-group-leading-1, .mq-group-leading-2 { + .mq-group-leading-1, + .mq-group-leading-2 { margin-left: 0; margin-right: @contract-margin; } @@ -41,7 +43,11 @@ } &.mq-suppress-grouping { - .mq-group-start, .mq-group-other, .mq-group-leading-1, .mq-group-leading-2, .mq-group-leading-3 { + .mq-group-start, + .mq-group-other, + .mq-group-leading-1, + .mq-group-leading-2, + .mq-group-leading-3 { margin-left: @expand-margin; margin-right: @expand-margin; } @@ -71,14 +77,17 @@ line-height: 1; .inline-block; - .mq-non-leaf, .mq-scaled { + .mq-non-leaf, + .mq-scaled { .inline-block; } // TODO: dasherize non-symbola - var, .mq-text-mode, .mq-nonSymbola { + var, + .mq-text-mode, + .mq-nonSymbola { font-family: @times; - line-height: .9; + line-height: 0.9; } svg { @@ -88,7 +97,7 @@ fill: currentColor; // the svg symbols fill their container - position:absolute; + position: absolute; top: 0; left: 0; width: 100%; @@ -107,12 +116,12 @@ // TODO: what's the difference between these? .mq-empty { - background: rgba(0,0,0,.2); + background: rgba(0, 0, 0, 0.2); &.mq-root-block { background: transparent; } &.mq-quiet-delimiter { - background: transparent + background: transparent; } } @@ -126,9 +135,9 @@ } .mq-text-mode.mq-hasCursor { - box-shadow: inset darkgray 0 .1em .2em; - padding: 0 .1em; - margin: 0 -.1em; + box-shadow: inset darkgray 0 0.1em 0.2em; + padding: 0 0.1em; + margin: 0 -0.1em; min-width: 1ex; } @@ -143,11 +152,14 @@ } // TODO [Han]: Why do we have to special-case .font? - b, b.mq-font { + b, + b.mq-font { font-weight: bolder; } - var, i, i.mq-font { + var, + i, + i.mq-font { font-style: italic; } @@ -167,21 +179,21 @@ .mq-int { > big { display: inline-block; - .transform(scaleX(.7)); - vertical-align: -.16em; + .transform(scaleX(0.7)); + vertical-align: -0.16em; } > .mq-supsub { font-size: 80%; vertical-align: -1.1em; - padding-right: .2em; + padding-right: 0.2em; > .mq-sup > .mq-sup-inner { vertical-align: 1.3em; } > .mq-sub { - margin-left: -.35em; + margin-left: -0.35em; } } } @@ -227,10 +239,10 @@ .mq-supsub { text-align: left; font-size: 90%; - vertical-align: -.5em; + vertical-align: -0.5em; &.mq-sup-only { - vertical-align: .5em; + vertical-align: 0.5em; & > .mq-sup { display: inline-block; @@ -248,7 +260,7 @@ } .mq-binary-operator { - padding: 0 .1em; + padding: 0 0.1em; } // special styles for fractions @@ -261,19 +273,22 @@ sup.mq-nthroot { font-size: 80%; vertical-align: 0.8em; - margin-right: -.6em; - margin-left: .2em; - min-width: .5em; + margin-right: -0.6em; + margin-left: 0.2em; + min-width: 0.5em; } //// // parentheses - .mq-ghost svg { opacity: .2 } + .mq-ghost svg { + opacity: 0.2; + } .mq-bracket-middle { - margin-top: .1em; - margin-bottom: .1em; + margin-top: 0.1em; + margin-bottom: 0.1em; } - .mq-bracket-l, .mq-bracket-r { + .mq-bracket-l, + .mq-bracket-r { position: absolute; top: 0; bottom: 2px; @@ -282,7 +297,7 @@ left: 0; } .mq-bracket-r { - right:0; + right: 0; } .mq-bracket-container { position: relative; @@ -301,15 +316,16 @@ // non-italicized operator names // like \sin, \cos, \ln, etc. .mq-operator-name { - font-family: Symbola, "Times New Roman", serif; - line-height: .9; + font-family: Symbola, 'Times New Roman', serif; + line-height: 0.9; font-style: normal; } var.mq-operator-name.mq-first { - padding-left: .2em; + padding-left: 0.2em; } - var.mq-operator-name.mq-last, .mq-supsub.mq-after-operator-name { - padding-right: .2em; + var.mq-operator-name.mq-last, + .mq-supsub.mq-after-operator-name { + padding-right: 0.2em; } //// @@ -321,22 +337,29 @@ .mq-fraction { font-size: 90%; text-align: center; - vertical-align: -.4em; - padding: 0 .2em; + vertical-align: -0.4em; + padding: 0 0.2em; } // Firefox 2 (and older?) only // because display:inline-block is FUBAR in Gecko < 1.9.0 - .mq-fraction, .mq-large-operator, x:-moz-any-link { + .mq-fraction, + .mq-large-operator, + x:-moz-any-link { display: -moz-groupbox; } // Firefox 3+ (Gecko 1.9.0+) - .mq-fraction, .mq-large-operator, x:-moz-any-link, x:default { + .mq-fraction, + .mq-large-operator, + x:-moz-any-link, + x:default { display: inline-block; } - .mq-numerator, .mq-denominator, .mq-dot-recurring { + .mq-numerator, + .mq-denominator, + .mq-dot-recurring { display: block; } @@ -374,16 +397,16 @@ border-top: 1px solid; margin-top: 1px; margin-left: 0.9em; - padding-left: .15em; - padding-right: .2em; - margin-right: .1em; + padding-left: 0.15em; + padding-right: 0.2em; + margin-right: 0.1em; padding-top: 1px; } .mq-diacritic-above { display: block; text-align: center; - line-height: .4em; + line-height: 0.4em; } .mq-diacritic-stem { @@ -394,8 +417,8 @@ .mq-hat-prefix { display: block; text-align: center; - line-height: .95em; - margin-bottom: -.7em; + line-height: 0.95em; + margin-bottom: -0.7em; transform: scaleX(1.5); -moz-transform: scaleX(1.5); -o-transform: scaleX(1.5); @@ -407,14 +430,17 @@ } .mq-large-operator { - vertical-align: -.2em; - padding: .2em; + vertical-align: -0.2em; + padding: 0.2em; text-align: center; - .mq-from, big, .mq-to { + .mq-from, + big, + .mq-to { display: block; } - .mq-from, .mq-to { + .mq-from, + .mq-to { font-size: 80%; } .mq-from { @@ -423,26 +449,26 @@ } } - - &, .mq-editable-field { + &, + .mq-editable-field { cursor: text; font-family: @symbola; } .mq-overarc { border-top: 1px solid black; - -webkit-border-top-right-radius: 50% .3em; - -moz-border-radius-topright: 50% .3em; - border-top-right-radius: 50% .3em; - -webkit-border-top-left-radius: 50% .3em; - -moz-border-radius-topleft: 50% .3em; - border-top-left-radius: 50% .3em; + -webkit-border-top-right-radius: 50% 0.3em; + -moz-border-radius-topright: 50% 0.3em; + border-top-right-radius: 50% 0.3em; + -webkit-border-top-left-radius: 50% 0.3em; + -moz-border-radius-topleft: 50% 0.3em; + border-top-left-radius: 50% 0.3em; margin-top: 1px; padding-top: 0.15em; } .mq-overarrow { - min-width: .5em; + min-width: 0.5em; border-top: 1px solid black; margin-top: 1px; padding-top: 0.2em; @@ -461,7 +487,8 @@ content: ''; display: none; } - &.mq-arrow-left:before, &.mq-arrow-leftright:before { + &.mq-arrow-left:before, + &.mq-arrow-leftright:before { position: absolute; top: -0.48em; left: -0.1em; @@ -472,7 +499,25 @@ -webkit-transform: scaleX(-1); transform: scaleX(-1); filter: FlipH; - -ms-filter: "FlipH"; + -ms-filter: 'FlipH'; } } + + .mq-limit { + padding: 0 0.2em; + text-align: center; + display: inline-block; + } + + .mq-limit-label { + display: block; + width: 100%; + } + + .mq-limit-sub { + float: right; + display: block; + width: 100%; + font-size: 80%; + } } diff --git a/src/publicapi.ts b/src/publicapi.ts index e982e0466..6e6e78555 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -102,6 +102,7 @@ class Options { typingAsteriskWritesTimesSymbol?: boolean; typingSlashWritesDivisionSymbol: boolean; typingPercentWritesPercentOf?: boolean; + customCharacters?: string | readonly string[]; resetCursorOnBlur?: boolean | undefined; leftRightIntoCmdGoes?: 'up' | 'down'; enableDigitGrouping?: boolean; diff --git a/src/services/latex.ts b/src/services/latex.ts index c19c0fb49..fe54ab105 100644 --- a/src/services/latex.ts +++ b/src/services/latex.ts @@ -67,7 +67,26 @@ var latexMathParser = (function () { return fail('unknown command: \\' + ctrlSeq); } }); - var command = controlSequence.or(variable).or(number).or(symbol); + var loneAmpersand = regex(/^&/).then(() => { + // TODO - is Parser correct? + var cmdKlass = CharCmds['&'] || LatexCmds['&']; + if (cmdKlass) { + if (cmdKlass.constructor) { + var actualClass = cmdKlass as typeof TempSingleCharNode; // TODO - figure out how to know the difference + return new actualClass('&').parser(); + } else { + var builder = cmdKlass as (c: string) => TempSingleCharNode; // TODO - figure out how to know the difference + return builder('&').parser(); + } + } else { + return fail('unknown command: \\&'); + } + }); + var command = loneAmpersand + .or(controlSequence) + .or(variable) + .or(number) + .or(symbol); // Parsers yielding MathBlocks var mathGroup: Parser = string('{') .then(function () { diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 31942c44b..55be8667a 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1387,7 +1387,7 @@ suite('typing with auto-replaces', function () { test('command is a built-in operator name', function () { var cmds = ( - 'Pr arg deg det dim exp gcd hom inf ker lg lim ln log max min sup' + + 'Pr arg deg det dim exp gcd hom inf ker lg ln log max min sup' + ' limsup liminf injlim projlim Pr' ).split(' '); for (var i = 0; i < cmds.length; i += 1) { @@ -1400,9 +1400,7 @@ suite('typing with auto-replaces', function () { test('built-in operator names even after auto-operator names overridden', function () { MQ.config({ autoOperatorNames: 'sin inf arcosh cosh cos cosec csc' }); // ^ happen to be the ones required by autoOperatorNames.test.js - var cmds = 'Pr arg deg det exp gcd inf lg lim ln log max min sup'.split( - ' ' - ); + var cmds = 'Pr arg deg det exp gcd inf lg ln log max min sup'.split(' '); for (var i = 0; i < cmds.length; i += 1) { assert.throws(function () { MQ.config({ autoCommands: cmds[i] });