基于templater实现的LaTeX公式自动编号和自动修改引用

// 在公式块末尾加上{{}}表示跳过编号
// 引用写成$\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)})。`);
})();

下面是效果演示
序列 01_1

1 个赞