diff --git a/ts/output/chtml/FontData.ts b/ts/output/chtml/FontData.ts
index a5180c61b..a9bae47c8 100644
--- a/ts/output/chtml/FontData.ts
+++ b/ts/output/chtml/FontData.ts
@@ -39,6 +39,7 @@ import {
StyleJsonSheet,
} from '../../util/StyleJson.js';
import { em } from '../../util/lengths.js';
+import { VFUZZ, HFUZZ } from '../common/FontData.js';
export * from '../common/FontData.js';
@@ -479,6 +480,7 @@ export class ChtmlFontData extends FontData<
HDW: ChtmlCharData
): number {
if (!n) return 0;
+ let fuzz = 0;
const [h, d, w] = this.getChar(v, n);
const css: StyleJsonData = { width: this.em0(w) };
if (part !== 'ext') {
@@ -494,18 +496,25 @@ export class ChtmlFontData extends FontData<
css.margin = `${this.em(y)} ${dw} ${this.em(-y)}`;
} else {
//
+ // Adjust the height and depth for a little overlap.
// Set the line-height to have the extenders touch,
- // (plus a little extra for Safari, whose line-height is
- // not accurate), and shift the extender stack to overlap
- // the ends.
+ // and shift the extender stack to overlap the ends.
//
- css['line-height'] = this.em0(h + d + 0.005);
- styles[`mjx-stretchy-v${c} > mjx-${part} > mjx-spacer`] = {
- 'margin-top': this.em(-d),
- };
+ fuzz = VFUZZ;
+ const lh = Math.max(VFUZZ, h + d - VFUZZ);
+ css['line-height'] = this.em0(lh);
+ //
+ // Adjust the top margin to make sure we have overlap with the top part
+ //
+ const D = h - lh / 2 - VFUZZ;
+ if (D) {
+ styles[`mjx-stretchy-v${c} > mjx-ext > mjx-spacer`] = {
+ 'margin-top': this.em(D),
+ };
+ }
}
styles[`mjx-stretchy-v${c} > mjx-${part}`] = css;
- return h + d;
+ return Math.max(0, h + d - fuzz);
}
/*******************************************************/
@@ -556,7 +565,7 @@ export class ChtmlFontData extends FontData<
}
if (data.ext) {
styles[`mjx-stretchy-h${c} > mjx-ext > mjx-spacer`]['letter-spacing'] =
- this.em(-data.ext);
+ this.em(-data.ext - HFUZZ);
}
}
diff --git a/ts/output/chtml/Wrappers/mo.ts b/ts/output/chtml/Wrappers/mo.ts
index 404708054..d4f19fedf 100644
--- a/ts/output/chtml/Wrappers/mo.ts
+++ b/ts/output/chtml/Wrappers/mo.ts
@@ -30,6 +30,8 @@ import {
ChtmlDelimiterData,
ChtmlFontData,
ChtmlFontDataClass,
+ VFUZZ,
+ HFUZZ,
} from '../FontData.js';
import { CharDataArray } from '../../common/FontData.js';
import {
@@ -273,7 +275,7 @@ export const ChtmlMo = (function (): ChtmlMoClass {
// The ext parameter should be 0, but line-height in Safari
// is not accurate, so this produces extra extenders to compensate
//
- this.createAssembly(parts, stretch, stretchv, dom, h + d, 0.05, '\n');
+ this.createAssembly(parts, stretch, stretchv, dom, h + d, VFUZZ, '\n');
//
// Vertical needs an extra (empty) element to get vertical position right
// in some browsers (e.g., Safari)
@@ -282,7 +284,8 @@ export const ChtmlMo = (function (): ChtmlMoClass {
styles.height = this.em(h + d);
styles.verticalAlign = this.em(-d);
} else {
- this.createAssembly(parts, stretch, stretchv, dom, w, delim.ext || 0);
+ const ext = (delim.ext || 0) + HFUZZ;
+ this.createAssembly(parts, stretch, stretchv, dom, w, ext);
styles.width = this.em(w);
}
//
@@ -340,10 +343,12 @@ export const ChtmlMo = (function (): ChtmlMoClass {
// Set up the beginning, extension, and end pieces
//
this.createPart('mjx-beg', parts[0], sn[0], sv[0], dom);
- this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl);
+ /* prettier-ignore */
+ this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH1, WHx, nl, WHb, WHm / 2 || WHe);
if (parts[3]) {
this.createPart('mjx-mid', parts[3], sn[3], sv[3], dom);
- this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl);
+ /* prettier-ignore */
+ this.createPart('mjx-ext', parts[1], sn[1], sv[1], dom, WH2, WHx, nl, WHm / 2, WHe);
}
this.createPart('mjx-end', parts[2], sn[2], sv[2], dom);
}
@@ -359,6 +364,8 @@ export const ChtmlMo = (function (): ChtmlMoClass {
* @param {number} W The extension width
* @param {number} Wx The width of the extender character
* @param {string} nl Character to use between extenders
+ * @param {number} Wb The beginning width
+ * @param {number} We The ending width
*/
protected createPart(
part: string,
@@ -368,7 +375,9 @@ export const ChtmlMo = (function (): ChtmlMoClass {
dom: N[],
W: number = 0,
Wx: number = 0,
- nl: string = ''
+ nl: string = '',
+ Wb: number = 0,
+ We: number = 0
) {
if (n) {
const options = data[3];
@@ -379,13 +388,28 @@ export const ChtmlMo = (function (): ChtmlMoClass {
const c = options.c || String.fromCodePoint(n);
let nodes = [] as (N | T)[];
if (part === 'mjx-ext' && (Wx || options.dx)) {
+ //
+ // If the top and bottom must overlap, adjust the border sizes and remove the clipping
+ //
+ if (W < 0 && nl) {
+ dom.push(
+ this.html(part, {
+ ...(font ? { class: font } : {}),
+ style: {
+ 'border-width': `${this.em(Wb + W / 2)} 0 ${this.em(We + W / 2)}`,
+ 'clip-path': 'none',
+ },
+ })
+ );
+ return;
+ }
//
// Some combining characters are listed as width 0,
// so get "real" width from dx and take off some
// for the right bearing.
//
if (!Wx) {
- Wx = Math.max(0.06, 2 * options.dx - 0.06);
+ Wx = Math.max(HFUZZ, 2 * options.dx - HFUZZ);
}
const n = Math.min(Math.ceil(W / Wx) + 1, 500);
if (options.cmb) {
diff --git a/ts/output/common/FontData.ts b/ts/output/common/FontData.ts
index 779196645..cf9609f88 100644
--- a/ts/output/common/FontData.ts
+++ b/ts/output/common/FontData.ts
@@ -30,6 +30,11 @@ import { retryAfter } from '../../util/Retries.js';
import { DIRECTION } from './Direction.js';
export { DIRECTION } from './Direction.js';
+/*****************************************************************/
+
+export const VFUZZ = 0.07; // overlap for vertical stretchy glyphs
+export const HFUZZ = 0.07; // overlap for horizontal stretchy glyphs
+
/****************************************************************************/
/**
diff --git a/ts/output/svg/Wrappers/mo.ts b/ts/output/svg/Wrappers/mo.ts
index 087330bec..cee32ffd5 100644
--- a/ts/output/svg/Wrappers/mo.ts
+++ b/ts/output/svg/Wrappers/mo.ts
@@ -30,6 +30,8 @@ import {
SvgDelimiterData,
SvgFontData,
SvgFontDataClass,
+ VFUZZ,
+ HFUZZ,
} from '../FontData.js';
import {
CommonMo,
@@ -41,11 +43,6 @@ import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js';
import { BBox } from '../../../util/BBox.js';
import { DIRECTION, SvgCharData } from '../FontData.js';
-/*****************************************************************/
-
-const VFUZZ = 0.1; // overlap for vertical stretchy glyphs
-const HFUZZ = 0.1; // overlap for horizontal stretchy glyphs
-
/*****************************************************************/
/**
* The SvgMo interface for the SVG Mo wrapper
@@ -333,27 +330,40 @@ export const SvgMo = (function (): SvgMoClass {
* @param {number} B The height of the bottom glyph in the delimiter
* @param {number} W The width of the stretched delimiter
*/
- /* prettier-ignore */
- protected addExtV(n: number, v: string, H: number, D: number, T: number, B: number, W: number) {
+ protected addExtV(
+ n: number,
+ v: string,
+ H: number,
+ D: number,
+ T: number,
+ B: number,
+ W: number
+ ) {
if (!n) return;
- T = Math.max(0, T - VFUZZ); // A little overlap on top
- B = Math.max(0, B - VFUZZ); // A little overlap on bottom
+ T = Math.max(0, T - VFUZZ); // // A little overlap on top
+ B = Math.max(0, B - VFUZZ); // // A little overlap on bottom
const adaptor = this.adaptor;
const [h, d, w] = this.getChar(n, v);
- const Y = H + D - T - B; // The height of the extender
- const s = 1.5 * Y / (h + d); // Scale height by 1.5 to avoid bad ends
- // (glyphs with rounded or anti-aliased ends don't stretch well,
- // so this makes for sharper ends)
- const y = (s * (h - d) - Y) / 2; // The bottom point to clip the extender
+ const Y = H + D - T - B; // // The height of the extender
+ const s = (1.5 * Y) / (h + d); // // Scale height by 1.5 to avoid bad ends
+ // // (glyphs with rounded or anti-aliased ends don't stretch well,
+ // // so this makes for sharper ends)
+ const y = (s * (h - d) - Y) / 2; // // The bottom point to clip the extender
if (Y <= 0) return;
const svg = this.svg('svg', {
- width: this.fixed(w), height: this.fixed(Y),
- y: this.fixed(B - D), x: this.fixed((W - w) / 2),
- viewBox: [0, y, w, Y].map(x => this.fixed(x)).join(' ')
+ width: this.fixed(w),
+ height: this.fixed(Y),
+ y: this.fixed(B - D),
+ x: this.fixed((W - w) / 2),
+ viewBox: [0, y, w, Y].map((x) => this.fixed(x)).join(' '),
});
this.addGlyph(n, v, 0, 0, svg);
const glyph = adaptor.lastChild(svg);
- adaptor.setAttribute(glyph as N, 'transform', `scale(1,${this.jax.fixed(s)})`);
+ adaptor.setAttribute(
+ glyph as N,
+ 'transform',
+ `scale(1,${this.jax.fixed(s)})`
+ );
if (this.dom[0]) {
adaptor.append(this.dom[0], svg);
}