109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Add a short-name alias (ID prefix stripped) to every ID-prefixed note's
|
|
frontmatter, so Obsidian's alias-autocomplete can produce clean [[ID|Name]]
|
|
links without the user typing the ID. Idempotent: skips notes that already
|
|
have that alias or non-ID-prefixed notes.
|
|
"""
|
|
import os, re, sys
|
|
|
|
VAULT = "/vault"
|
|
# Matches "F.50.12 Jen", "R.31.10 Hindi", "F.22.11 Places We've Slept" etc.
|
|
ID_TITLE_RE = re.compile(r'^([RJFI](?:\.\d+){1,3})\s+(.+)$')
|
|
|
|
FRONTMATTER_RE = re.compile(r'^---\n(.*?)\n---\n', re.DOTALL)
|
|
|
|
def split_frontmatter(text):
|
|
m = FRONTMATTER_RE.match(text)
|
|
if m:
|
|
return m.group(1), text[m.end():]
|
|
return None, text
|
|
|
|
def add_alias(fm_text, alias):
|
|
lines = fm_text.split("\n") if fm_text else []
|
|
# find aliases: line (list style) or aliases: [a, b]
|
|
alias_line_idx = None
|
|
for i, l in enumerate(lines):
|
|
if re.match(r'^aliases\s*:', l):
|
|
alias_line_idx = i
|
|
break
|
|
if alias_line_idx is None:
|
|
lines.append(f"aliases:\n - {alias}")
|
|
return "\n".join(lines)
|
|
line = lines[alias_line_idx]
|
|
# inline list form: aliases: [a, b]
|
|
m = re.match(r'^aliases\s*:\s*\[(.*)\]\s*$', line)
|
|
if m:
|
|
existing = [x.strip().strip('"\'') for x in m.group(1).split(",") if x.strip()]
|
|
if alias in existing:
|
|
return None # already present
|
|
existing.append(alias)
|
|
lines[alias_line_idx] = "aliases: [" + ", ".join(existing) + "]"
|
|
return "\n".join(lines)
|
|
# block list form: aliases:\n - a\n - b
|
|
if line.strip() == "aliases:" or re.match(r'^aliases\s*:\s*$', line):
|
|
j = alias_line_idx + 1
|
|
existing = []
|
|
while j < len(lines) and re.match(r'^\s*-\s*', lines[j]):
|
|
existing.append(lines[j].strip()[2:].strip().strip('"\''))
|
|
j += 1
|
|
if alias in existing:
|
|
return None
|
|
lines.insert(j, f" - {alias}")
|
|
return "\n".join(lines)
|
|
# single scalar alias: aliases: Foo
|
|
scalar = line.split(":", 1)[1].strip().strip('"\'')
|
|
if scalar == alias:
|
|
return None
|
|
lines[alias_line_idx] = "aliases:\n - " + scalar + f"\n - {alias}"
|
|
return "\n".join(lines)
|
|
|
|
def main():
|
|
execute = "--execute" in sys.argv
|
|
changed = 0
|
|
skipped_no_id = 0
|
|
skipped_has_alias = 0
|
|
errors = []
|
|
for dp, _, fns in os.walk(VAULT):
|
|
if ".git" in dp.split(os.sep):
|
|
continue
|
|
for fn in fns:
|
|
if not fn.lower().endswith(".md"):
|
|
continue
|
|
base = fn[:-3]
|
|
m = ID_TITLE_RE.match(base)
|
|
if not m:
|
|
skipped_no_id += 1
|
|
continue
|
|
alias = m.group(2)
|
|
p = os.path.join(dp, fn)
|
|
try:
|
|
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
|
text = f.read()
|
|
except Exception as e:
|
|
errors.append((p, str(e)))
|
|
continue
|
|
fm, rest = split_frontmatter(text)
|
|
new_fm = add_alias(fm or "", alias)
|
|
if new_fm is None:
|
|
skipped_has_alias += 1
|
|
continue
|
|
new_text = f"---\n{new_fm}\n---\n\n{rest.lstrip(chr(10))}" if fm is None else f"---\n{new_fm}\n---\n{rest}"
|
|
changed += 1
|
|
if execute:
|
|
with open(p, "w", encoding="utf-8") as f:
|
|
f.write(new_text)
|
|
|
|
print(f"Notes with ID-prefixed title: {changed + skipped_has_alias}")
|
|
print(f" Would add alias: {changed}")
|
|
print(f" Already had alias: {skipped_has_alias}")
|
|
print(f"Notes without ID prefix (skipped, not applicable): {skipped_no_id}")
|
|
if errors:
|
|
print(f"Errors: {len(errors)}")
|
|
for p, e in errors[:10]:
|
|
print(f" {p}: {e}")
|
|
if not execute:
|
|
print("\n(dry run -- pass --execute to write changes)")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|