Milestone 2.

This commit is contained in:
2025-08-18 21:40:41 +02:00
parent 1646d7b827
commit e283e9f696
13 changed files with 615 additions and 107 deletions

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import json
import os
import re
import uuid
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
@@ -15,7 +15,6 @@ from app.utils.slugs import slugify
from app.utils.time import now_iso_utc, is_iso_utc
REQUIRED_FIELDS = ("id", "title", "created", "updated")
OPTIONAL_LIST_FIELDS = ("aliases", "tags")
@@ -42,7 +41,6 @@ def _normalize_metadata(meta: dict) -> dict:
if val is None:
val = []
if isinstance(val, str):
# Allow comma-separated string as input
val = [x.strip() for x in val.split(",") if x.strip()]
if not isinstance(val, list):
val = [str(val)]
@@ -112,15 +110,17 @@ def note_path_for_slug(vault: Vault, slug: str) -> Tuple[str, str]:
return rel, abs_path
def load_note_from_file(vault: Vault, abs_path: str) -> Note:
with open(abs_path, "rb") as f:
post = frontmatter.load(f)
def _load_frontmatter_text(abs_path: str):
# Always read as text for python-frontmatter
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
return frontmatter.loads(f.read())
def load_note_from_file(vault: Vault, abs_path: str) -> Note:
post = _load_frontmatter_text(abs_path)
meta = _normalize_metadata(post.metadata)
title = meta["title"]
slug = slugify(title)
rel, _ = note_path_for_slug(vault, slug)
# Use actual rel path of the file (could differ from current slug if renamed later)
rel = vault.relpath(abs_path)
note = Note(
@@ -161,8 +161,6 @@ def create_note(
slug = slugify(meta["title"])
rel, abs_path = note_path_for_slug(vault, slug)
abs_path = ensure_unique_path(abs_path)
# If collision caused a suffix, recompute rel path accordingly
rel = vault.relpath(abs_path)
note = Note(
@@ -193,29 +191,46 @@ def save_note(vault: Vault, note: Note) -> None:
def list_notes() -> list[Note]:
vault = get_vault()
notes: list[Note] = []
for path in vault.iter_markdown_files():
for p in vault.iter_markdown_files():
try:
notes.append(load_note_from_file(vault, path))
abs_path = p if os.path.isabs(p) else vault.abspath(p)
notes.append(load_note_from_file(vault, abs_path))
except Exception:
# Ignore malformed files for now (could log)
continue
# Sort by updated desc
notes.sort(key=lambda n: n.updated, reverse=True)
return notes
_hex_re = re.compile(r"[0-9a-fA-F]")
def _uuid_key(value: str) -> Optional[str]:
if value is None:
return None
s = "".join(_hex_re.findall(str(value)))
if len(s) == 32:
return s.lower()
try:
return uuid.UUID(str(value)).hex
except Exception:
return None
def load_note_by_id(note_id: str) -> Optional[Note]:
vault = get_vault()
for path in vault.iter_markdown_files():
target_key = _uuid_key(note_id)
if not target_key:
return None
for p in vault.iter_markdown_files():
try:
with open(path, "rb") as f:
post = frontmatter.load(f)
meta = _normalize_metadata(post.metadata)
if str(meta.get("id")) == str(note_id):
# Load fully to construct Note with body and rel path
return load_note_from_file(vault, path)
abs_path = p if os.path.isabs(p) else vault.abspath(p)
post = _load_frontmatter_text(abs_path)
raw_id = (post.metadata or {}).get("id")
cand_key = _uuid_key(raw_id)
if cand_key and cand_key == target_key:
return load_note_from_file(vault, abs_path)
except Exception:
continue
return None
@@ -224,7 +239,6 @@ def update_note_body(note_id: str, new_body: str) -> Optional[Note]:
if not note:
return None
note.body = new_body
# Update timestamp
note.updated = now_iso_utc()
vault = get_vault()
save_note(vault, note)