Skip to content

Commit

Permalink
Merge pull request #776 from mathjax/update-sideset
Browse files Browse the repository at this point in the history
Re-implement \sideset using mmultiscripts.  (mathjax/MathJax#1217)
  • Loading branch information
dpvc authored Feb 25, 2022
2 parents 8c0c0cb + 0944797 commit 4ced76d
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 17 deletions.
2 changes: 2 additions & 0 deletions ts/core/MmlTree/SerializedMmlVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export class SerializedMmlVisitor extends MmlVisitor {
node.getProperty('variantForm') && this.setDataAttribute(data, 'alternate', '1');
node.getProperty('pseudoscript') && this.setDataAttribute(data, 'pseudoscript', 'true');
node.getProperty('autoOP') === false && this.setDataAttribute(data, 'auto-op', 'false');
const scriptalign = node.getProperty('scriptalign') as string;
scriptalign && this.setDataAttribute(data, 'script-align', scriptalign);
const texclass = node.getProperty('texClass') as number;
if (texclass !== undefined) {
let setclass = true;
Expand Down
19 changes: 14 additions & 5 deletions ts/input/mathml/MathMLCompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,27 @@ export class MathMLCompile<N, T, D> {
continue;
}
if (name.substr(0, 9) === 'data-mjx-') {
if (name === 'data-mjx-alternate') {
switch (name.substr(9)) {
case 'alternate':
mml.setProperty('variantForm', true);
} else if (name === 'data-mjx-variant') {
break;
case 'variant':
mml.attributes.set('mathvariant', value);
ignoreVariant = true;
} else if (name === 'data-mjx-smallmatrix') {
break;
case 'smallmatrix':
mml.setProperty('scriptlevel', 1);
mml.setProperty('useHeight', false);
} else if (name === 'data-mjx-accent') {
break;
case 'accent':
mml.setProperty('mathaccent', value === 'true');
} else if (name === 'data-mjx-auto-op') {
break;
case 'auto-op':
mml.setProperty('autoOP', value === 'true');
break;
case 'script-align':
mml.setProperty('scriptalign', value);
break;
}
} else if (name !== 'class') {
let val = value.toLowerCase();
Expand Down
3 changes: 1 addition & 2 deletions ts/input/tex/ams/AmsMappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ new sm.CommandMap('AMSmath-macros', {
dddot: ['Accent', '20DB'],
ddddot: ['Accent', '20DC'],

sideset: ['Macro', '\\mathop{\\mathop{\\rlap{\\phantom{#3}}}\\nolimits#1' +
'\\!\\mathop{#3}\\nolimits#2}', 3],
sideset: 'SideSet',

boxed: ['Macro', '\\fbox{$\\displaystyle{#1}$}', 1],

Expand Down
106 changes: 106 additions & 0 deletions ts/input/tex/ams/AmsMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {FlalignItem} from './AmsItems.js';
import BaseMethods from '../base/BaseMethods.js';
import {TEXCLASS} from '../../../core/MmlTree/MmlNode.js';
import {MmlMunderover} from '../../../core/MmlTree/MmlNodes/munderover.js';
import {MmlNode, AbstractMmlTokenNode} from '../../../core/MmlTree/MmlNode.js';


// Namespace
Expand Down Expand Up @@ -233,6 +234,111 @@ AmsMethods.HandleOperatorName = function(parser: TexParser, name: string) {
parser.i = 0;
};

/**
* Handle sideset.
* @param {TexParser} parser The calling parser.
* @param {string} name The macro name.
*/
AmsMethods.SideSet = function (parser: TexParser, name: string) {
//
// Get the pre- and post-scripts, and any extra material from the arguments
//
const [preScripts, preRest] = splitSideSet(parser.ParseArg(name));
const [postScripts, postRest] = splitSideSet(parser.ParseArg(name));
const base = parser.ParseArg(name);
let mml = base;
//
// If there are pre-scripts...
//
if (preScripts) {
//
// If there is other material...
//
if (preRest) {
//
// Replace the empty base of the prescripts with a phantom element of the
// original base, with width 0 (but still of the correct height and depth).
// so the scripts will be at the right heights.
//
preScripts.replaceChild(
parser.create('node', 'mphantom', [
parser.create('node', 'mpadded', [ParseUtil.copyNode(base, parser)], {width: 0})
]),
NodeUtil.getChildAt(preScripts, 0)
);
} else {
//
// If there is no extra meterial, make a mmultiscripts element
//
mml = parser.create('node', 'mmultiscripts', [base]);
//
// Add any postscripts
//
if (postScripts) {
NodeUtil.appendChildren(mml, [
NodeUtil.getChildAt(postScripts, 1) || parser.create('node', 'none'),
NodeUtil.getChildAt(postScripts, 2) || parser.create('node', 'none')
]);
}
//
// Add the prescripts (left aligned)
//
NodeUtil.setProperty(mml, 'scriptalign', 'left');
NodeUtil.appendChildren(mml, [
parser.create('node', 'mprescripts'),
NodeUtil.getChildAt(preScripts, 1) || parser.create('node', 'none'),
NodeUtil.getChildAt(preScripts, 2) || parser.create('node', 'none')
]);
}
}
//
// If there are postscripts and we didn't make a mmultiscript element above...
//
if (postScripts && mml === base) {
//
// Replace the emtpy base with actual base, and use that as the mml
//
postScripts.replaceChild(base, NodeUtil.getChildAt(postScripts, 0));
mml = postScripts;
}
//
// Push the needed pieces onto the stack.
// Note that the postScripts are in the mml element,
// either as part of the mmultiscripts node, or the
// msubsup with the base inserted into it.
//
if (preRest) {
preScripts && parser.Push(preScripts);
parser.Push(preRest);
}
parser.Push(mml);
postRest && parser.Push(postRest);
};

/**
* Utility for breaking the \sideset scripts from any other material.
* @param {MmlNode} mml The node to check.
* @return {[MmlNode, MmlNode]} The msubsup with the scripts together with any extra nodes.
*/
function splitSideSet(mml: MmlNode): [MmlNode, MmlNode] {
if (!mml || (mml.isInferred && mml.childNodes.length === 0)) return [null, null];
if (mml.isKind('msubsup') && checkSideSetBase(mml)) return [mml, null];
const child = NodeUtil.getChildAt(mml, 0);
if (!(mml.isInferred && child && checkSideSetBase(child))) return [null, mml];
mml.childNodes.splice(0, 1); // remove first child
return [child, mml];
}

/**
* Utility for checking if a \sideset argument has scripts with an empty base.
* @param {MmlNode} mml The node to check.
* @return {boolean} True if the base is not and empty mi element.
*/
function checkSideSetBase(mml: MmlNode): boolean {
const base = mml.childNodes[0];
return base && base.isKind('mi') && (base as AbstractMmlTokenNode).getText() === '';
}


/**
* Handle SkipLimits.
Expand Down
26 changes: 22 additions & 4 deletions ts/output/chtml/Wrappers/mmultiscripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {CommonMmultiscriptsMixin} from '../../common/Wrappers/mmultiscripts.js';
import {MmlMmultiscripts} from '../../../core/MmlTree/MmlNodes/mmultiscripts.js';
import {BBox} from '../../../util/BBox.js';
import {StyleList} from '../../../util/StyleList.js';
import {split} from '../../../util/string.js';

/*****************************************************************/
/**
Expand Down Expand Up @@ -59,6 +60,15 @@ CommonMmultiscriptsMixin<CHTMLWrapper<any, any, any>, Constructor<CHTMLmsubsup<a
},
'mjx-prescripts > mjx-row > mjx-cell': {
'text-align': 'right'
},
'[script-align="left"] > mjx-row > mjx-cell': {
'text-align': 'left'
},
'[script-align="center"] > mjx-row > mjx-cell': {
'text-align': 'center'
},
'[script-align="right"] > mjx-row > mjx-cell': {
'text-align': 'right'
}
};

Expand All @@ -71,6 +81,11 @@ CommonMmultiscriptsMixin<CHTMLWrapper<any, any, any>, Constructor<CHTMLmsubsup<a
const chtml = this.standardCHTMLnode(parent);
const data = this.scriptData;
//
// Get the alignment for the scripts
//
const scriptalign = this.node.getProperty('scriptalign') || 'right left';
const [preAlign, postAlign] = split(scriptalign + ' ' + scriptalign);
//
// Combine the bounding boxes of the pre- and post-scripts,
// and get the resulting baseline offsets
//
Expand All @@ -81,11 +96,13 @@ CommonMmultiscriptsMixin<CHTMLWrapper<any, any, any>, Constructor<CHTMLmsubsup<a
// Place the pre-scripts, then the base, then the post-scripts
//
if (data.numPrescripts) {
this.addScripts(u, -v, true, data.psub, data.psup, this.firstPrescript, data.numPrescripts);
const scripts = this.addScripts(u, -v, true, data.psub, data.psup, this.firstPrescript, data.numPrescripts);
preAlign !== 'right' && this.adaptor.setAttribute(scripts, 'script-align', preAlign);
}
this.childNodes[0].toCHTML(chtml);
if (data.numScripts) {
this.addScripts(u, -v, false, data.sub, data.sup, 1, data.numScripts);
const scripts = this.addScripts(u, -v, false, data.sub, data.sup, 1, data.numScripts);
postAlign !== 'left' && this.adaptor.setAttribute(scripts, 'script-align', postAlign);
}
}

Expand All @@ -99,8 +116,9 @@ CommonMmultiscriptsMixin<CHTMLWrapper<any, any, any>, Constructor<CHTMLmsubsup<a
* @param {BBox} sup The superscript bounding box
* @param {number} i The starting index for the scripts
* @param {number} n The number of sub/super-scripts
* @return {N} The script table for these scripts
*/
protected addScripts(u: number, v: number, isPre: boolean, sub: BBox, sup: BBox, i: number, n: number) {
protected addScripts(u: number, v: number, isPre: boolean, sub: BBox, sup: BBox, i: number, n: number): N {
const adaptor = this.adaptor;
const q = (u - sup.d) + (v - sub.h); // separation of scripts
const U = (u < 0 && v === 0 ? sub.h + u : u); // vertical offset of table
Expand All @@ -110,12 +128,12 @@ CommonMmultiscriptsMixin<CHTMLWrapper<any, any, any>, Constructor<CHTMLmsubsup<a
const sepRow = this.html('mjx-row', rowdef);
const subRow = this.html('mjx-row');
const name = 'mjx-' + (isPre ? 'pre' : '') + 'scripts';
adaptor.append(this.chtml, this.html(name, tabledef, [supRow, sepRow, subRow]));
let m = i + 2 * n;
while (i < m) {
this.childNodes[i++].toCHTML(adaptor.append(subRow, this.html('mjx-cell')) as N);
this.childNodes[i++].toCHTML(adaptor.append(supRow, this.html('mjx-cell')) as N);
}
return adaptor.append(this.chtml, this.html(name, tabledef, [supRow, sepRow, subRow]));
}

}
37 changes: 31 additions & 6 deletions ts/output/svg/Wrappers/mmultiscripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ import {SVGWrapper, Constructor} from '../Wrapper.js';
import {SVGmsubsup} from './msubsup.js';
import {CommonMmultiscriptsMixin} from '../../common/Wrappers/mmultiscripts.js';
import {MmlMmultiscripts} from '../../../core/MmlTree/MmlNodes/mmultiscripts.js';
import {split} from '../../../util/string.js';

/*****************************************************************/

/**
* A function taking two widths and returning an offset of the first in the second
*/
export type AlignFunction = (w: number, W: number) => number;

/**
* Get the function for aligning scripts horizontally (left, center, right)
*/
export function AlignX(align: string) {
return ({
left: (_w, _W) => 0,
center: (w, W) => (W - w) / 2,
right: (w, W) => W - w
} as {[name: string]: AlignFunction})[align] || ((_w, _W) => 0) as AlignFunction;
}

/*****************************************************************/
/**
Expand All @@ -50,6 +69,11 @@ CommonMmultiscriptsMixin<SVGWrapper<any, any, any>, Constructor<SVGmsubsup<any,
const svg = this.standardSVGnode(parent);
const data = this.scriptData;
//
// Get the alignment for the scripts
//
const scriptalign = this.node.getProperty('scriptalign') || 'right left';
const [preAlign, postAlign] = split(scriptalign + ' ' + scriptalign);
//
// Combine the bounding boxes of the pre- and post-scripts,
// and get the resulting baseline offsets
//
Expand All @@ -61,14 +85,14 @@ CommonMmultiscriptsMixin<SVGWrapper<any, any, any>, Constructor<SVGmsubsup<any,
//
let x = 0; // scriptspace
if (data.numPrescripts) {
x = this.addScripts(.05, u, v, true, this.firstPrescript, data.numPrescripts);
x = this.addScripts(.05, u, v, this.firstPrescript, data.numPrescripts, preAlign);
}
const base = this.baseChild;
base.toSVG(svg);
base.place(x, 0);
x += base.getBBox().w;
if (data.numScripts) {
this.addScripts(x, u, v, false, 1, data.numScripts);
this.addScripts(x, u, v, 1, data.numScripts, postAlign);
}
}

Expand All @@ -78,13 +102,14 @@ CommonMmultiscriptsMixin<SVGWrapper<any, any, any>, Constructor<SVGmsubsup<any,
* @param {number} x The x offset of the scripts
* @param {number} u The baseline offset for the superscripts
* @param {number} v The baseline offset for the subscripts
* @param {boolean} isPre True for prescripts, false for scripts
* @param {number} i The starting index for the scripts
* @param {number} n The number of sub/super-scripts
* @param {string} align The alignment for the scripts
* @return {number} The right-hand offset of the scripts
*/
protected addScripts(x: number, u: number, v: number, isPre: boolean, i: number, n: number): number {
protected addScripts(x: number, u: number, v: number, i: number, n: number, align: string): number {
const adaptor = this.adaptor;
const alignX = AlignX(align);
const supRow = adaptor.append(this.element, this.svg('g'));
const subRow = adaptor.append(this.element, this.svg('g'));
this.place(x, u, supRow);
Expand All @@ -98,8 +123,8 @@ CommonMmultiscriptsMixin<SVGWrapper<any, any, any>, Constructor<SVGmsubsup<any,
const w = Math.max(subbox.w * subr, supbox.w * supr);
sub.toSVG(subRow);
sup.toSVG(supRow);
sub.place(dx + (isPre ? w - subbox.w * subr : 0), 0);
sup.place(dx + (isPre ? w - supbox.w * supr : 0), 0);
sub.place(dx + alignX(subbox.w * subr, w), 0);
sup.place(dx + alignX(supbox.w * supr, w), 0);
dx += w;
}
return x + dx;
Expand Down

0 comments on commit 4ced76d

Please sign in to comment.