"""Audit / convert single-line $$...$$ math blocks for SilverBullet compatibility. SilverBullet's Math library defines block math with the line-anchored marker regex ^\\$\\$$ for BOTH start and end, so `$$...$$` written on one line never closes and is rendered as raw text. Usage: python math_fix.py audit python math_fix.py convert [--apply] """ import os import re import sys CMD = sys.argv[1] BASE = sys.argv[2] if len(sys.argv) > 2 else '.' APPLY = '--apply' in sys.argv LINE_RE = re.compile(r'^\$\$(.+)\$\$$') def notes(): for root, dirs, files in os.walk(BASE): if any(x in root for x in ['_scripts', '_text', '_digest', '.git']): continue for fn in sorted(files): if fn.endswith('.md') and fn != 'README.md': yield os.path.join(root, fn) def walk_lines(path): """Yield (index, line, in_fence).""" fence = False lines = open(path, encoding='utf-8').read().split('\n') for i, line in enumerate(lines): if line.lstrip().startswith('```'): yield i, line, fence fence = not fence continue yield i, line, fence def audit(): total = indented = multi = in_fence = trailing = 0 samples = [] for p in notes(): for i, line, fence in walk_lines(p): s = line.strip() if not s.startswith('$$') or s.count('$$') < 2: continue total += 1 if fence: in_fence += 1 samples.append(('FENCE', p, i, s[:90])) if line != line.lstrip(): indented += 1 samples.append(('INDENT', p, i, s[:90])) if s.count('$$') > 2: multi += 1 samples.append(('MULTI', p, i, s[:90])) if not s.endswith('$$'): trailing += 1 samples.append(('TAILTEXT', p, i, s[-80:])) print('单行 $$ 块总数 :', total) print(' 其中在代码块内 :', in_fence) print(' 其中带前导缩进 :', indented) print(' 其中一行含 >2 个 $$ :', multi) print(' 其中不以 $$ 结尾 :', trailing) for kind, p, i, s in samples[:20]: print(' [%s] %s:%d %s' % (kind, os.path.relpath(p, BASE), i + 1, s)) def convert(): files_changed = 0 blocks_changed = 0 skipped = [] for p in notes(): lines = open(p, encoding='utf-8').read().split('\n') out = [] fence = False n = 0 for line in lines: if line.lstrip().startswith('```'): fence = not fence out.append(line) continue m = LINE_RE.match(line) if m and not fence: body = m.group(1).strip() if '$$' in body: skipped.append((os.path.relpath(p, BASE), line[:90])) out.append(line) continue out.append('$$') out.append(body) out.append('$$') n += 1 continue out.append(line) if n: files_changed += 1 blocks_changed += n print('%3d 处 %s' % (n, os.path.relpath(p, BASE))) if APPLY: with open(p, 'w', encoding='utf-8', newline='\n') as f: f.write('\n'.join(out)) print() print('文件数: %d 转换块数: %d' % (files_changed, blocks_changed)) if skipped: print('跳过(正文含 $$,需人工确认):') for s in skipped: print(' ', s) print('模式:', '已写入' if APPLY else '预演(未写入,加 --apply 才落盘)') if __name__ == '__main__': if CMD == 'audit': audit() elif CMD == 'convert': convert() else: raise SystemExit('unknown command: ' + CMD)