Align = signs in equations separated by non-MathJax html

24 views
Skip to first unread message

Andrew Murdza

unread,
Oct 15, 2025, 12:48:27 PMOct 15
to MathJax Users
I have
$$x^2+1\and{1}=5$$
<p>This is text</p>
<p>Hello $x^2\and{1}=4$</p>

I want to align the two "=" signs. I don't want to need a macro (I can do that part myself with a JavaScript function), but is there a way to do it with JavaScript code? If it makes it easier, $...$ is left-aligned. I thought of two approaches that might work (there might be another approach I am not aware of)

  • Method 1:
    • Step 1:  If there would be a line break between "Hello" and "$x^2$," add <br> in between them.
    • Step 2: Measure the length of "Hello $x^2$" (if there is no <br>) or "$x^2$" (if there is a <br>) and "$x^2+1$" Call the lengths "length1" and "length2"
    • Step 3: Calculate delta=Math.abs(length2-length1)
    • Step 4: There are four cases
      • If there is a <br> and length1<length2: Add `$\\hspace{${delta}}$ right after <br>
      • If there no is <br> and length1<length2, add `$\\hspace{${delta}}$` between "<p>" and "Hello"
      • If length1===length2, don't add anything
      • If length1>length2, add `\\hspace{${delta}}` between "$$" and "x^2+1"
    • My problems are the following:
      • I don't know to determine if there will be a line break and where the line break will occur
      • I don't know how to measure the widths
  • Method 2:
    • Step 1: <table class="noBorders noCellPadding topAlignCells leftAlignCells"><tr><td>$$x^2+1$$</td><td>$${}=5$$</td></tr><tr><td col-span=2><p>This is text</p></td></tr><tr><td>Hello $x^2$</td><td>$${}=5$$</td></tr></table>
    • My problem: I don't think it would work for something where it goes \and{1}, \and{2}, \and{1}, then \and{2}:
      $$x^2\and{1}=1$$
      <p>Some text</p>
      $$y^2+1\and{2}=5$$
      <p>Hello</p>
      $$x\and{1}=\pm1$$
      <p>Some more text</p>
      $$y^2\and{2}=4$$
If you give me some hints for Method 1, Method 2, or another method, I can try to post my full working code here in case other people also want to use \and.

Davide Cervone

unread,
Oct 16, 2025, 12:14:38 PMOct 16
to mathja...@googlegroups.com
I think you are working harder than necessary with trying to analyze line breaks and such. The table approach could be made to work but is awkward and will not be very good for accessibility.

An alternative would be to use \class to mark the equal signs you want to align and then look up the associated nodes after the math is laid out, call getBoundingClientRect() on them to get their x positions, and use the difference in positions to determine the extra space that is needed.

Here is an example of this approach.  I don't know what the \and{x} macro is, so I've left that out, but you should be able to get the idea from this.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Align equals examples</title>
<script>
MathJax = {
  output: {
    displayAlign: 'left',
  },
  tex: {
    inlineMath: {'[+]': [['$', '$']]}
  },
  startup: {
    async pageReady() {
      await MathJax.startup.defaultPageReady();
      const {STATE} = MathJax._.core.MathItem;
      const jax = MathJax.startup.document.outputJax;
      const [math1, math2] = Array.from(MathJax.startup.document.math);
      const x1 = math1.typesetRoot.querySelector('.equal').getBoundingClientRect().x;
      const x2 = math2.typesetRoot.querySelector('.equal').getBoundingClientRect().x;
      if (x1 === x2) return;
      if (x1 < x2) {
        const dx = jax.fixed(x2 - x1);
        math1.state(STATE.FINDMATH);
        math1.math = math1.math.replace(/\\class/, `\\hspace{${dx}px}\\class`);
      } else {
        const dx = jax.fixed(x1 - x2);
        math2.state(STATE.FINDMATH);
        math2.math = math2.math.replace(/\\class/, `\\hspace{${dx}px}\\class`);
      }
      MathJax.typesetPromise();
    }
  }
}
</script>
</head>
<body>

$$x^2+1\class{equal}{=}5$$
<p>This is text</p>
<p>Hello $x^2\class{equal}{=}4$</p>

</body>
</html>

This will work even when there is a line break between the "Hello" and the following expression, provided that the addition of the extra space in the shorter expression doesn't cause additional line breaks to occur (e.g., if "Hello $...$" didn't have a line break, but adding space into the expression causes a line break after "Hello").

See if that does what you are looking for.

Davide


--
You received this message because you are subscribed to the Google Groups "MathJax Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mathjax-user...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/mathjax-users/153c691d-5dc3-42aa-bdeb-7bf295a0f698n%40googlegroups.com.

Andrew Murdza

unread,
Oct 18, 2025, 4:03:31 PMOct 18
to MathJax Users
When I run this, it looks like this:

x^2+1      =5
Hello x^2=4

But I wanted it to be
      x^2+1=5
Hello x^2=4

I changed 
math1.math = math1.math.replace(/\\class/, `\\hspace{${dx}px}\\class`);
to
math1.math = `\\hspace{${dx}px}`+math1.math;
to get the desired result.

Davide Cervone

unread,
Oct 19, 2025, 6:50:40 PMOct 19
to mathja...@googlegroups.com
I'm glad you got it to work to your satisfaction.

Davide


Andrew Murdza

unread,
Nov 1, 2025, 6:12:23 PMNov 1
to MathJax Users
Hi Davide,

This is the \and and \sep macros in case anyone else wants it. ChatGPT got this on the second try (on the first try, you had to refresh the page. This was because the alignment was slightly off due to the font not being loaded yet). It also figured out the issue when I explained the output without me having to find out the cause myself. I agree that it doesn't understand MathJax very well because it originally did sep: ["\class{sep}{}",0] which I shortened to "\class{sep}{}". It also didn't include the $x^3+4x$ line at the bottom, so the test for \sep not being needed at the beginning was incomplete. For those who are interested, \sep works like even &'s in aligned and \and works like the odd ands.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stable \and alignment</title>
<script>
MathJax={
output:{displayAlign:'left'},

tex:{
inlineMath:{'[+]':[ ['$', '$'] ]},
macros:{
and:["\\class{and-#2}{#1}",2],
sep:"\\class{sep}{}"
}
},
startup:{
async pageReady(){
await MathJax.startup.defaultPageReady();
const doc=MathJax.startup.document;
const {STATE}=MathJax._.core.MathItem;
const MAX_ITERS=6;

async function alignOnce(){
const maths=Array.from(doc.math);
const groups=new Map();

for(const m of maths){
const root=m.typesetRoot;
if(!root) continue;
const andEls=root.querySelectorAll('[class*="and-"]');
andEls.forEach(el=>{
const idClass=Array.from(el.classList).find(c=>c.startsWith('and-'));
if(!idClass) return;
const id=idClass.slice(4);
const box=el.getBoundingClientRect();
const center=box.left+box.width/2;
if(!groups.has(id)) groups.set(id,[]);
groups.get(id).push({mathItem:m,element:el,center});
});
}

if(!groups.size) return false;

const targetCenters=new Map();
for(const [id,arr] of groups.entries()){
targetCenters.set(id,Math.max(...arr.map(o=>o.center)));
}

let changed=false;

for(const [id,arr] of groups.entries()){
const byMath=new Map();
for(const info of arr){
if(!byMath.has(info.mathItem)) byMath.set(info.mathItem,[]);
byMath.get(info.mathItem).push(info);
}

for(const [mathItem,infos] of byMath.entries()){
const currentCenter=Math.min(...infos.map(x=>x.center));
const target=targetCenters.get(id);
let dx=target-currentCenter;
if(dx<=0) continue;

// 👇 round to whole px to avoid tiny misalignments
dx=Math.round(dx);

let src=mathItem.math;
const andRE=new RegExp(String.raw`\\and\{[^}]*\}\{`+id+String.raw`\}`);
const mAnd=src.match(andRE);
if(!mAnd) continue;
const andIndex=mAnd.index;
const before=src.slice(0,andIndex);
const sepIndex=before.lastIndexOf('\\sep');
const h=`\\hspace{${dx}px}`;
let newSrc;
if(sepIndex>=0){
newSrc=src.slice(0,sepIndex)+h+src.slice(sepIndex);
}else{
// implicit \sep at start
newSrc=h+src;
}
mathItem.state(STATE.FINDMATH);
mathItem.math=newSrc;
changed=true;
}
}

if(changed){
await MathJax.typesetPromise();
}
return changed;
}

async function runAlignLoop(){
for(let i=0;i<MAX_ITERS;i++){
const didChange=await alignOnce();
if(!didChange) break;
}
}

// 1) wait for fonts if possible
if(document.fonts && document.fonts.ready){
await document.fonts.ready;
}

// 2) run once right away
await runAlignLoop();

// 3) run again shortly after, to catch late layout/font changes
setTimeout(runAlignLoop,150);
setTimeout(runAlignLoop,500);

}
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-chtml.js" defer></script>
</head>
<body>

$$x^2+1\and{=}{a}5$$
<p>Hello $x^2\and{=}{a}4$</p>

<p>$\sep x+3\and{=}{b}7 \sep y\and{\approx}{c}10$</p>
<p>Some text</p>
<p>$\sep 22222x-1\and{=}{b}11 \sep y+2\and{\approx}{c}9$</p>

<!-- no explicit \sep at start -->
<p>$x+4\and{=}{d}9$</p>
<p>Some more text</p>
<p>$x^3+4x\and{=}{d}9$</p>

</body>
</html>

Andrew Murdza

unread,
Nov 2, 2025, 12:25:49 PMNov 2
to MathJax Users
I later discovered some issues. With a combination of edits from me and ChatGPT (ChatGPT couldn't figure out how to fix something but I did it myself) they have been resolved. This is the updated code if anyone wants it.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stable \and alignment</title>
<script>
MathJax={
output:{displayAlign:'left'},
tex:{
inlineMath:{'[+]':[ ['$', '$'] ]},
macros:{
and:["\\class{and-#2}{#1}",2],
sep:"\\class{sep}{}"
}
},
startup:{
async pageReady(){
await MathJax.startup.defaultPageReady();

function findAndWithId(src, id){
const needle = id;
let searchFrom = 0;

while (searchFrom < src.length){
// 1) find next \and{
const start = src.indexOf("\\and{", searchFrom);
if (start === -1) return null;

// 2) parse the first arg (balanced)
let i = start + "\\and{".length;
let depth = 1;
while (i < src.length && depth > 0){
const ch = src[i];
if (ch === "{") depth++;
else if (ch === "}") depth--;
i++;
}
if (depth !== 0){
// malformed, skip this one and keep looking
searchFrom = start + 5;
continue;
}

// 3) skip any whitespace/newlines that might have been inserted
while (i < src.length && /\s/.test(src[i])) i++;

// 4) now we expect {<id>}
if (src[i] === "{"){
const j = i + 1;
const maybeId = src.slice(j, j + needle.length);
const after = src[j + needle.length];
if (maybeId === needle && after === "}"){
// success
return {
start, // index of "\"
arg1End: i, // index of '{' of 2nd arg
end: j + needle.length + 1 // right after closing }
};
}
}

// not this one → move on
searchFrom = i;
}

return null;
}
async function createAndAligner(){

function isHiddenByDisplay(el) {
while (el) {
const cs = getComputedStyle(el);
if (cs.display === 'none') {
return true; // hidden by this element or ancestor
}
el = el.parentElement;
}
return false; // visible
}

const doc = MathJax.startup.document;
const {STATE} = MathJax._.core.MathItem;
const jax = doc.outputJax;
const MAX_ITERS = 6;

async function alignOnce(){
const maths = Array.from(doc.math);
const groups = new Map();

// collect visible \and
for (const m of maths){
const root = m.typesetRoot;
if (!root) continue;
const andEls = root.querySelectorAll('[class*="and-"]');
andEls.forEach(el=>{
if (isHiddenByDisplay(el)) return;

const idClass = Array.from(el.classList).find(c=>c.startsWith('and-'));
if (!idClass) return;
const id = idClass.slice(4);
const box = el.getBoundingClientRect();
if (box.width === 0 && box.height === 0) return;

if (!groups.has(id)) groups.set(id, []);
groups.get(id).push({
mathItem: m,
center: box.left + box.width/2
});
});
}

if (!groups.size) return false;

// target center per id (rightmost)
// groups: Map<id, Array<{ mathItem, center }>>
const targetCenters = new Map();
for (const [id, arr] of groups.entries()) {
targetCenters.set(id, Math.max(...arr.map(o => o.center)));
}

// 1) build per-mathItem list of ANDs (with their ids)
const perMath = new Map(); // mathItem -> Array<{id, center}>
for (const [id, arr] of groups.entries()) {
for (const info of arr) {
const { mathItem, center } = info;
if (!perMath.has(mathItem)) perMath.set(mathItem, []);
perMath.get(mathItem).push({ id, center });
}
}

let changed = false;

// 2) for each mathItem, process ANDs left→right
for (const [mathItem, ands] of perMath.entries()) {
// sort by visual position in this line
ands.sort((a, b) => a.center - b.center);

let lineMaxShift = 0; // how much this line has been shoved already

for (const { id, center } of ands) {
const target = targetCenters.get(id);
let dx = target - center;

if (Math.abs(dx) <= 0.75) continue;

// if we've already moved this line, don't let this AND move it more
if (lineMaxShift > 0) {
dx = 0;
}

if (Math.abs(dx) <= 0.75) continue;

dx = Math.round(dx);

let src = mathItem.math;
const found = findAndWithId(src, id);
if (!found) continue;

// insert hspace after nearest \sep before this \and
const beforeThisAnd = src.slice(0, found.start);
const sepIndex = beforeThisAnd.lastIndexOf('\\sep');
const h = `\\hspace{${jax.fixed(dx)}px}`;
let newSrc;

if (sepIndex >= 0) {
const sepToken = '\\sep';
let insertPos = sepIndex + sepToken.length;
while (insertPos < src.length && /\s/.test(src[insertPos])) insertPos++;
newSrc = src.slice(0, insertPos) + h + src.slice(insertPos);
} else {
newSrc = h + src;
}

mathItem.state(STATE.FINDMATH);
mathItem.math = newSrc;
changed = true;

// remember how far this line has been pushed
if (dx > lineMaxShift) {
lineMaxShift = dx;
}
}
}

if (changed){
await MathJax.typesetPromise();
}
return changed;
}

async function runAll(){
for (let i=0;i<MAX_ITERS;i++){
const did = await alignOnce();
if (!did) break;
}
}

return runAll;
}



const runAndAlign = await createAndAligner();

if(document.fonts && document.fonts.ready){
await document.fonts.ready;
}
await runAndAlign();
setTimeout(runAndAlign,150);
setTimeout(runAndAlign,500);
window.alignMathJaxAnds = runAndAlign;

}
}
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-chtml.js" defer></script>
</head>
<body>

$$x^2+1\and{=}{a}5$$
<p>Hello $x^2\and{=}{a}4$</p>

<p>$\sep x+3\and{===}{b}7 \sep ttttttttttttty\and{\approx}{c}101\sep11AA\and{hii}{d}$</p>
<p>Some text</p>
<p>$\sep 22222x-1\and{=}{b}11 \sep y+2\and{\approx}{c}9\sep AA\and{hii}{d}$</p>

<p>$x+4\and{=}{d}9$</p>
<p>Some more text</p>
<p>$x^3+4x\and{=}{d}9$</p>

</body>
</html>
Reply all
Reply to author
Forward
0 new messages