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;
<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>