// 在公式块末尾加上{{}}表示跳过编号
// 引用写成$\text{(a-b)}$的形式
// 数字a表示第几个的H1标题(有公式的才会被计入),b表示这个H1标题下的第几个公式块
(async () => {
const INCLUDE_TABLE_FORMULAS = 0; // 0: 表格内公式不参与、完全不动;1: 表格内公式也参与编号
const editor = app.workspace.activeEditor?.editor;
if (!editor) { if (typeof Notice === 'function') new Notice('未找到活动编辑器'); return; }
const text = editor.getValue();
const isEscaped = (s, idx) => { let c=0,i=idx-1; while(i>=0&&s[i]==='\\'){c++;i--;} return (c%2)===1; };
const normalizeNo = (raw) => { const m=String(raw).match(/^\s*(\d+)\s*-\s*(\d+)\s*$/); return m?`${+m[1]}-${+m[2]}`:null; };
const inRanges = (pos, ranges) => { let l=0,r=ranges.length-1; while(l<=r){const m=(l+r)>>1,rg=ranges[m]; if(pos<rg.start) r=m-1; else if(pos>=rg.end) l=m+1; else return true;} return false; };
const sortRanges = (ranges)=>ranges.sort((a,b)=>a.start-b.start);
const fenceRanges=[];{ const re=/^```.*$/gm; let m,starts=[]; while((m=re.exec(text))!==null) starts.push(m.index);
for(let i=0;i+1<starts.length;i+=2){ const endLineEnd=text.indexOf('\n',starts[i+1]); fenceRanges.push({start:starts[i], end:endLineEnd===-1?text.length:(endLineEnd+1)}); }
sortRanges(fenceRanges);
}
const lineOffsets=[0];{let off=0; while(true){const n=text.indexOf('\n',off); if(n===-1) break; off=n+1; lineOffsets.push(off);} }
const lineCount=lineOffsets.length;
const getLineStart=(i)=>lineOffsets[i]??text.length;
const getLineEnd=(i)=>(i+1<lineCount)?lineOffsets[i+1]:text.length;
const getLineByPos=(pos)=>{let l=0,r=lineCount-1,ans=0; while(l<=r){const m=(l+r)>>1,s=getLineStart(m),e=getLineEnd(m); if(pos<s) r=m-1; else if(pos>=e) l=m+1; else {ans=m;break;}} return ans;};
const tableRanges=[];{
const isSepLine=(s)=>{const t=s.trim(); return t.includes('|')&&t.includes('-')&&/^[\s:\-\|]+$/.test(t);};
for(let li=0;li<lineCount-1;li++){
if(inRanges(getLineStart(li), fenceRanges)) continue;
const L=text.slice(getLineStart(li), getLineEnd(li));
if(!L.includes('|')) continue;
let lj=li+1; while(lj<lineCount && text.slice(getLineStart(lj), getLineEnd(lj)).trim()==='') lj++;
if(lj>=lineCount) break;
const N=text.slice(getLineStart(lj), getLineEnd(lj));
if(!isSepLine(N)) continue;
let k=lj+1;
while(k<lineCount){
const t=text.slice(getLineStart(k), getLineEnd(k));
if(t.trim()==='') break;
if(!t.includes('|')) break;
if(inRanges(getLineStart(k), fenceRanges)) break;
k++;
}
tableRanges.push({start:getLineStart(li), end:getLineStart(k)});
li=k-1;
}
sortRanges(tableRanges);
}
const sections=(()=>{const arr=[]; const re=/^#\s.*$/gm; let m,starts=[]; while((m=re.exec(text))!==null) starts.push(m.index);
for(let i=0;i<starts.length;i++){ const start=starts[i]; const end=(i+1<starts.length)?starts[i+1]:text.length; arr.push({ ordinal:i+1, start, end }); } return arr;})();
if(sections.length===0){ if(typeof Notice==='function') new Notice('未检测到一级标题(# ),未执行编号。'); return; }
const findSectionForPos=(p)=>sections.find(s=>p>=s.start&&p<s.end)||null;
const blocks=[];{ let i=0; while(i<text.length){ const st=text.indexOf('$$',i); if(st===-1) break;
if(isEscaped(text,st) || inRanges(st,fenceRanges)) { i=st+2; continue; }
let ed=-1,j=st+2; while(j<text.length){ const k=text.indexOf('$$',j); if(k===-1) break; if(!isEscaped(text,k) && !inRanges(k,fenceRanges)){ed=k;break;} j=k+2; }
if(ed===-1) break;
const li=getLineByPos(st);
const inTable=inRanges(getLineStart(li), tableRanges);
blocks.push({ start:st, end:ed+2, innerStart:st+2, innerEnd:ed, inTable });
i=ed+2;
} }
if(blocks.length===0){ if(typeof Notice==='function') new Notice('未检测到 $$…$$ 公式块。'); return; }
const tagRegexCapt=/\\tag\s*\{\s*([^}]+)\s*\}/i;
const tagRegexRemove=/\s*\\tag\s*\{[^}]*\}\s*/gi;
const hasExempt=(core)=>/\{\{\}\}\s*$/.test(core.trim());
const removeAnyTag=(inner)=>inner.replace(tagRegexRemove,' ').replace(/\s+$/,'');
const isNumberable=(blk)=>{
if(blk.inTable && !INCLUDE_TABLE_FORMULAS) return false;
const inner=text.slice(blk.innerStart, blk.innerEnd);
const trailing=(inner.match(/\s*$/)||[''])[0];
const core=inner.slice(0, inner.length - trailing.length);
return !hasExempt(core);
};
const activeSecIndex=new Map(); let nextIdx=1;
for(const b of blocks){
if(!isNumberable(b)) continue;
const sec=findSectionForPos(b.start); if(!sec) continue;
if(!activeSecIndex.has(sec.ordinal)) activeSecIndex.set(sec.ordinal, nextIdx++);
}
if(activeSecIndex.size===0){ if(typeof Notice==='function') new Notice('没有可编号的公式(全部在表格或被豁免)。'); return; }
const blocksInSections=new Map();
blocks.forEach((b, idx)=>{ const sec=findSectionForPos(b.start); if(!sec) return;
if(!blocksInSections.has(sec.ordinal)) blocksInSections.set(sec.ordinal, []);
blocksInSections.get(sec.ordinal).push(idx);
});
const edits=[]; const mappingOldToNew=new Map(); let eqUpdated=0;
for(const [secOrd, idxs] of blocksInSections.entries()){
const secNo=activeSecIndex.get(secOrd); if(!secNo) continue;
let bCounter=0;
idxs.sort((a,b)=>blocks[a].start-blocks[b].start);
for(const bi of idxs){
const blk=blocks[bi];
const inner=text.slice(blk.innerStart, blk.innerEnd);
const trailing=(inner.match(/\s*$/)||[''])[0];
const core=inner.slice(0, inner.length - trailing.length);
if(blk.inTable && !INCLUDE_TABLE_FORMULAS) continue;
if(hasExempt(core)){
// 豁免:保留 {{}},移除旧 tag,不编号
const coreClean=removeAnyTag(core);
const newInner=`${coreClean}${trailing}`;
edits.push({ start:blk.start, end:blk.end, replacement:`$$${newInner}$$` });
eqUpdated++;
continue;
}
bCounter++;
const newNo=`${secNo}-${bCounter}`;
const m=inner.match(tagRegexCapt);
if(m){ const oldNorm=normalizeNo(m[1]); if(oldNorm) mappingOldToNew.set(oldNorm, newNo); }
const coreNoTag=removeAnyTag(core);
const newInner=`${coreNoTag} \\tag{${newNo}}${trailing}`;
edits.push({ start:blk.start, end:blk.end, replacement:`$$${newInner}$$` });
eqUpdated++;
}
}
edits.sort((a,b)=>a.start-b.start);
let out='', last=0;
for(const e of edits){ out+=text.slice(last,e.start)+e.replacement; last=e.end; }
out+=text.slice(last);
let refUpdated=0;
const refRe=/\$\s*\\text\{\s*\(\s*(\d+)\s*-\s*(\d+)\s*\)\s*\}\s*\$/g;
out=out.replace(refRe,(m,a,b)=>{
const key=`${+a}-${+b}`;
const to=mappingOldToNew.get(key);
if(to && to!==key){ refUpdated++; return `$\\text{(${to})}$`; }
return m;
});
editor.setValue(out);
if(typeof Notice==='function') new Notice(`自动编号完成:更新公式 ${eqUpdated} 个;更新引用 ${refUpdated} 处(样式 \\text{(a-b)})。`);
})();
下面是效果演示
