Skip to content

Commit da21770

Browse files
authored
Merge pull request #1441 from mathjax/fix/stretchy-assemblies
Fix several issues with multi-character stretchy assemblies. (mathjax/MathJax#3528, mathjax/MathJax#3531)
2 parents c7f3ba0 + b16c2b4 commit da21770

File tree

4 files changed

+93
-45
lines changed

4 files changed

+93
-45
lines changed

ts/output/chtml/FontData.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
StyleJsonSheet,
4040
} from '../../util/StyleJson.js';
4141
import { em } from '../../util/lengths.js';
42+
import { VFUZZ, HFUZZ } from '../common/FontData.js';
4243

4344
export * from '../common/FontData.js';
4445

@@ -479,6 +480,7 @@ export class ChtmlFontData extends FontData<
479480
HDW: ChtmlCharData
480481
): number {
481482
if (!n) return 0;
483+
let fuzz = 0;
482484
const [h, d, w] = this.getChar(v, n);
483485
const css: StyleJsonData = { width: this.em0(w) };
484486
if (part !== 'ext') {
@@ -494,18 +496,25 @@ export class ChtmlFontData extends FontData<
494496
css.margin = `${this.em(y)} ${dw} ${this.em(-y)}`;
495497
} else {
496498
//
499+
// Adjust the height and depth for a little overlap.
497500
// Set the line-height to have the extenders touch,
498-
// (plus a little extra for Safari, whose line-height is
499-
// not accurate), and shift the extender stack to overlap
500-
// the ends.
501+
// and shift the extender stack to overlap the ends.
501502
//
502-
css['line-height'] = this.em0(h + d + 0.005);
503-
styles[`mjx-stretchy-v${c} > mjx-${part} > mjx-spacer`] = {
504-
'margin-top': this.em(-d),
505-
};
503+
fuzz = VFUZZ;
504+
const lh = Math.max(VFUZZ, h + d - VFUZZ);
505+
css['line-height'] = this.em0(lh);
506+
//
507+
// Adjust the top margin to make sure we have overlap with the top part
508+
//
509+
const D = h - lh / 2 - VFUZZ;
510+
if (D) {
511+
styles[`mjx-stretchy-v${c} > mjx-ext > mjx-spacer`] = {
512+
'margin-top': this.em(D),
513+
};
514+
}
506515
}
507516
styles[`mjx-stretchy-v${c} > mjx-${part}`] = css;
508-
return h + d;
517+
return Math.max(0, h + d - fuzz);
509518
}
510519

511520
/*******************************************************/
@@ -556,7 +565,7 @@ export class ChtmlFontData extends FontData<
556565
}
557566
if (data.ext) {
558567
styles[`mjx-stretchy-h${c} > mjx-ext > mjx-spacer`]['letter-spacing'] =
559-
this.em(-data.ext);
568+
this.em(-data.ext - HFUZZ);
560569
}
561570
}
562571

ts/output/chtml/Wrappers/mo.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
ChtmlDelimiterData,
3131
ChtmlFontData,
3232
ChtmlFontDataClass,
33+
VFUZZ,
34+
HFUZZ,
3335
} from '../FontData.js';
3436
import { CharDataArray } from '../../common/FontData.js';
3537
import {
@@ -273,7 +275,7 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
273275
// The ext parameter should be 0, but line-height in Safari
274276
// is not accurate, so this produces extra extenders to compensate
275277
//
276-
this.createAssembly(parts, stretch, stretchv, dom, h + d, 0.05, '\n');
278+
this.createAssembly(parts, stretch, stretchv, dom, h + d, VFUZZ, '\n');
277279
//
278280
// Vertical needs an extra (empty) element to get vertical position right
279281
// in some browsers (e.g., Safari)
@@ -282,7 +284,8 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
282284
styles.height = this.em(h + d);
283285
styles.verticalAlign = this.em(-d);
284286
} else {
285-
this.createAssembly(parts, stretch, stretchv, dom, w, delim.ext || 0);
287+
const ext = (delim.ext || 0) + HFUZZ;
288+
this.createAssembly(parts, stretch, stretchv, dom, w, ext);
286289
styles.width = this.em(w);
287290
}
288291
//
@@ -340,10 +343,12 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
340343
// Set up the beginning, extension, and end pieces
341344
//
342345
this.createPart('mjx-beg', parts[0], sn[0], sv[0], dom);
343-
this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl);
346+
/* prettier-ignore */
347+
this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl, WHb, WHm / 2 || WHe);
344348
if (parts[3]) {
345349
this.createPart('mjx-mid', parts[3], sn[3], sv[3], dom);
346-
this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl);
350+
/* prettier-ignore */
351+
this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl, WHm / 2, WHe);
347352
}
348353
this.createPart('mjx-end', parts[2], sn[2], sv[2], dom);
349354
}
@@ -359,6 +364,8 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
359364
* @param {number} W The extension width
360365
* @param {number} Wx The width of the extender character
361366
* @param {string} nl Character to use between extenders
367+
* @param {number} Wb The beginning width
368+
* @param {number} We The ending width
362369
*/
363370
protected createPart(
364371
part: string,
@@ -368,7 +375,9 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
368375
dom: N[],
369376
W: number = 0,
370377
Wx: number = 0,
371-
nl: string = ''
378+
nl: string = '',
379+
Wb: number = 0,
380+
We: number = 0
372381
) {
373382
if (n) {
374383
const options = data[3];
@@ -379,13 +388,28 @@ export const ChtmlMo = (function <N, T, D>(): ChtmlMoClass<N, T, D> {
379388
const c = options.c || String.fromCodePoint(n);
380389
let nodes = [] as (N | T)[];
381390
if (part === 'mjx-ext' && (Wx || options.dx)) {
391+
//
392+
// If the top and bottom must overlap, adjust the border sizes and remove the clipping
393+
//
394+
if (W < 0 && nl) {
395+
dom.push(
396+
this.html(part, {
397+
...(font ? { class: font } : {}),
398+
style: {
399+
'border-width': `${this.em(Wb + W / 2)} 0 ${this.em(We + W / 2)}`,
400+
'clip-path': 'none',
401+
},
402+
})
403+
);
404+
return;
405+
}
382406
//
383407
// Some combining characters are listed as width 0,
384408
// so get "real" width from dx and take off some
385409
// for the right bearing.
386410
//
387411
if (!Wx) {
388-
Wx = Math.max(0.06, 2 * options.dx - 0.06);
412+
Wx = Math.max(HFUZZ, 2 * options.dx - HFUZZ);
389413
}
390414
const n = Math.min(Math.ceil(W / Wx) + 1, 500);
391415
if (options.cmb) {

ts/output/common/FontData.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ import { retryAfter } from '../../util/Retries.js';
3030
import { DIRECTION } from './Direction.js';
3131
export { DIRECTION } from './Direction.js';
3232

33+
/*****************************************************************/
34+
35+
export const VFUZZ = 0.07; // overlap for vertical stretchy glyphs
36+
export const HFUZZ = 0.07; // overlap for horizontal stretchy glyphs
37+
3338
/****************************************************************************/
3439

3540
/**

ts/output/svg/Wrappers/mo.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
SvgDelimiterData,
3131
SvgFontData,
3232
SvgFontDataClass,
33+
VFUZZ,
34+
HFUZZ,
3335
} from '../FontData.js';
3436
import {
3537
CommonMo,
@@ -41,11 +43,6 @@ import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js';
4143
import { BBox } from '../../../util/BBox.js';
4244
import { DIRECTION, SvgCharData } from '../FontData.js';
4345

44-
/*****************************************************************/
45-
46-
const VFUZZ = 0.1; // overlap for vertical stretchy glyphs
47-
const HFUZZ = 0.1; // overlap for horizontal stretchy glyphs
48-
4946
/*****************************************************************/
5047
/**
5148
* The SvgMo interface for the SVG Mo wrapper
@@ -269,7 +266,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
269266
/**
270267
* @param {number} n The number of the character to look up
271268
* @param {string} variant The variant for the character to look up
272-
* @returns {SvgCharData} The full CharData object, with CharOptions guaranteed to be defined
269+
* @returns {SvgCharData} The full CharData object, with CharOptions guaranteed to be defined
273270
*/
274271
protected getChar(n: number, variant: string): SvgCharData {
275272
const char = this.font.getChar(variant, n) || [0, 0, 0, null];
@@ -287,7 +284,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
287284
* @param {number} x The x position of the glyph
288285
* @param {number} y The y position of the glyph
289286
* @param {N} parent The container for the glyph
290-
* @returns {number} The width of the character placed
287+
* @returns {number} The width of the character placed
291288
*/
292289
protected addGlyph(
293290
n: number,
@@ -315,7 +312,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
315312
* @param {string} v The variant for the top glyph
316313
* @param {number} H The height of the stretched delimiter
317314
* @param {number} W The width of the stretched delimiter
318-
* @returns {number} The total height of the top glyph
315+
* @returns {number} The total height of the top glyph
319316
*/
320317
protected addTop(n: number, v: string, H: number, W: number): number {
321318
if (!n) return 0;
@@ -333,27 +330,40 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
333330
* @param {number} B The height of the bottom glyph in the delimiter
334331
* @param {number} W The width of the stretched delimiter
335332
*/
336-
/* prettier-ignore */
337-
protected addExtV(n: number, v: string, H: number, D: number, T: number, B: number, W: number) {
333+
protected addExtV(
334+
n: number,
335+
v: string,
336+
H: number,
337+
D: number,
338+
T: number,
339+
B: number,
340+
W: number
341+
) {
338342
if (!n) return;
339-
T = Math.max(0, T - VFUZZ); // A little overlap on top
340-
B = Math.max(0, B - VFUZZ); // A little overlap on bottom
343+
T = Math.max(0, T - VFUZZ); // A little overlap on top
344+
B = Math.max(0, B - VFUZZ); // A little overlap on bottom
341345
const adaptor = this.adaptor;
342346
const [h, d, w] = this.getChar(n, v);
343-
const Y = H + D - T - B; // The height of the extender
344-
const s = 1.5 * Y / (h + d); // Scale height by 1.5 to avoid bad ends
345-
// (glyphs with rounded or anti-aliased ends don't stretch well,
346-
// so this makes for sharper ends)
347-
const y = (s * (h - d) - Y) / 2; // The bottom point to clip the extender
347+
const Y = H + D - T - B; // The height of the extender
348+
const s = (1.5 * Y) / (h + d); // Scale height by 1.5 to avoid bad ends
349+
// (glyphs with rounded or anti-aliased ends don't stretch well,
350+
// so this makes for sharper ends)
351+
const y = (s * (h - d) - Y) / 2; // The bottom point to clip the extender
348352
if (Y <= 0) return;
349353
const svg = this.svg('svg', {
350-
width: this.fixed(w), height: this.fixed(Y),
351-
y: this.fixed(B - D), x: this.fixed((W - w) / 2),
352-
viewBox: [0, y, w, Y].map(x => this.fixed(x)).join(' ')
354+
width: this.fixed(w),
355+
height: this.fixed(Y),
356+
y: this.fixed(B - D),
357+
x: this.fixed((W - w) / 2),
358+
viewBox: [0, y, w, Y].map((x) => this.fixed(x)).join(' '),
353359
});
354360
this.addGlyph(n, v, 0, 0, svg);
355361
const glyph = adaptor.lastChild(svg);
356-
adaptor.setAttribute(glyph as N, 'transform', `scale(1,${this.jax.fixed(s)})`);
362+
adaptor.setAttribute(
363+
glyph as N,
364+
'transform',
365+
`scale(1,${this.jax.fixed(s)})`
366+
);
357367
if (this.dom[0]) {
358368
adaptor.append(this.dom[0], svg);
359369
}
@@ -367,7 +377,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
367377
* @param {string} v The variant for the bottom glyph
368378
* @param {number} D The depth of the stretched delimiter
369379
* @param {number} W The width of the stretched delimiter
370-
* @returns {number} The total height of the bottom glyph
380+
* @returns {number} The total height of the bottom glyph
371381
*/
372382
protected addBot(n: number, v: string, D: number, W: number): number {
373383
if (!n) return 0;
@@ -395,7 +405,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
395405
/**
396406
* @param {number} n The character number for the left glyph of the stretchy character
397407
* @param {string} v The variant for the left glyph
398-
* @returns {number} The width of the left glyph
408+
* @returns {number} The width of the left glyph
399409
*/
400410
protected addLeft(n: number, v: string): number {
401411
return n ? this.addGlyph(n, v, 0, 0) : 0;
@@ -418,14 +428,14 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
418428
x: number = 0
419429
) {
420430
if (!n) return;
421-
R = Math.max(0, R - HFUZZ); // A little less than the width of the right glyph
422-
L = Math.max(0, L - HFUZZ); // A little less than the width of the left glyph
431+
R = Math.max(0, R - HFUZZ); // A little less than the width of the right glyph
432+
L = Math.max(0, L - HFUZZ); // A little less than the width of the left glyph
423433
const adaptor = this.adaptor;
424434
const [h, d, w] = this.getChar(n, v);
425-
const X = W - L - R; // The width of the extender
426-
const Y = h + d + 2 * VFUZZ; // The height (plus some fuzz) of the extender
427-
const s = 1.5 * (X / w); // Scale the width so that left- and right-bearing won't hurt us
428-
const D = -(d + VFUZZ); // The bottom position of the glyph
435+
const X = W - L - R; // The width of the extender
436+
const Y = h + d + 2 * VFUZZ; // The height (plus some fuzz) of the extender
437+
const s = 1.5 * (X / w); // Scale the width so that left- and right-bearing won't hurt us
438+
const D = -(d + VFUZZ); // The bottom position of the glyph
429439
if (X <= 0) return;
430440
const svg = this.svg('svg', {
431441
width: this.fixed(X),
@@ -453,7 +463,7 @@ export const SvgMo = (function <N, T, D>(): SvgMoClass<N, T, D> {
453463
* @param {number} n The character number for the right glyph of the stretchy character
454464
* @param {string} v The variant for the right glyph
455465
* @param {number} W The width of the stretched character
456-
* @returns {number} The width of the right glyph
466+
* @returns {number} The width of the right glyph
457467
*/
458468
protected addRight(n: number, v: string, W: number): number {
459469
if (!n) return 0;

0 commit comments

Comments
 (0)