mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: preserve user tags during sync
This commit is contained in:
parent
6039c78ff8
commit
acb0c2bcb5
4 changed files with 88 additions and 11 deletions
|
|
@ -354,8 +354,10 @@ def _normalize_tags(tags_value: object) -> list[str]:
|
|||
def extract_preserved_tags(text: str) -> list[str] | None:
|
||||
"""Extract preserved tags from existing note frontmatter.
|
||||
|
||||
Returns None when frontmatter is malformed/unreadable.
|
||||
Returns a normalized list of tag strings otherwise.
|
||||
Returns None when frontmatter is malformed/unreadable, or when the
|
||||
tags key is absent (caller should supply defaults).
|
||||
Returns a normalized list of tag strings otherwise (empty list means
|
||||
tags were explicitly set to empty).
|
||||
Only trusts YAML-parsed data — does not use regex fallback.
|
||||
"""
|
||||
fm_match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
|
||||
|
|
@ -363,7 +365,7 @@ def extract_preserved_tags(text: str) -> list[str] | None:
|
|||
if not fm_match:
|
||||
if text.strip().startswith("---"):
|
||||
return None
|
||||
return []
|
||||
return None
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
|
@ -371,6 +373,8 @@ def extract_preserved_tags(text: str) -> list[str] | None:
|
|||
data = yaml.safe_load(fm_match.group(1))
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
if "tags" not in data:
|
||||
return None
|
||||
return _normalize_tags(data.get("tags"))
|
||||
except Exception:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import filelock
|
|||
from paperforge import __version__ as _paperforge_version
|
||||
from paperforge.adapters.obsidian_frontmatter import (
|
||||
_legacy_control_flags,
|
||||
extract_preserved_tags,
|
||||
read_frontmatter_dict,
|
||||
)
|
||||
from paperforge.config import paperforge_paths
|
||||
|
|
@ -269,6 +270,7 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir:
|
|||
import shutil
|
||||
|
||||
from paperforge import __version__ as PAPERFORGE_VERSION
|
||||
from paperforge.adapters.obsidian_frontmatter import has_deep_reading_content
|
||||
from paperforge.worker._utils import lookup_impact_factor, read_json, slugify_filename, write_json, yaml_quote
|
||||
from paperforge.worker.asset_state import (
|
||||
compute_health,
|
||||
|
|
@ -278,7 +280,6 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir:
|
|||
)
|
||||
from paperforge.worker.ocr import validate_ocr_meta
|
||||
from paperforge.worker.paper_meta import write_paper_meta
|
||||
from paperforge.adapters.obsidian_frontmatter import has_deep_reading_content
|
||||
from paperforge.worker.sync import (
|
||||
collection_fields,
|
||||
frontmatter_note,
|
||||
|
|
@ -459,7 +460,8 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir:
|
|||
fm_close = text.find("---\n", 4) # closing --- after opening ---
|
||||
if fm_close != -1:
|
||||
body = text[fm_close + 4 :] # everything after frontmatter
|
||||
new_full = frontmatter_note(entry, "")
|
||||
preserved_tags = extract_preserved_tags(text)
|
||||
new_full = frontmatter_note(entry, text, preserved_tags=preserved_tags)
|
||||
new_fm_close = new_full.find("---\n", 4)
|
||||
if new_fm_close != -1:
|
||||
new_fm = new_full[: new_fm_close + 4] # new frontmatter block with closing ---\n
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from paperforge.adapters.obsidian_frontmatter import (
|
|||
canonicalize_decision,
|
||||
compute_final_collection,
|
||||
extract_preserved_deep_reading,
|
||||
extract_preserved_tags,
|
||||
update_frontmatter_field,
|
||||
)
|
||||
from paperforge.adapters.zotero_paths import (
|
||||
|
|
@ -994,8 +995,10 @@ def next_key(domain: str, export_rows: list[dict]) -> str:
|
|||
return f"{prefix}{max_num + 1:03d}"
|
||||
|
||||
|
||||
def frontmatter_note(entry: dict, existing_text: str = "") -> str:
|
||||
def frontmatter_note(entry: dict, existing_text: str = "", preserved_tags: list[str] | None = None) -> str:
|
||||
preserved_deep = extract_preserved_deep_reading(existing_text)
|
||||
if preserved_tags is None and existing_text:
|
||||
preserved_tags = extract_preserved_tags(existing_text)
|
||||
first_author = entry.get("first_author", "")
|
||||
if not first_author:
|
||||
authors = entry.get("authors", [])
|
||||
|
|
@ -1028,9 +1031,22 @@ def frontmatter_note(entry: dict, existing_text: str = "") -> str:
|
|||
f"deep_reading_status: {yaml_quote(entry.get('deep_reading_status', 'pending'))}",
|
||||
f"pdf_path: {yaml_quote(entry.get('pdf_path', ''))}",
|
||||
f"fulltext_md_path: {yaml_quote('[[{}]]'.format(entry['fulltext_path']) if entry.get('ocr_status') == 'done' and entry.get('fulltext_path') else '')}",
|
||||
"tags:",
|
||||
" - 文献阅读",
|
||||
f" - {entry.get('domain', '')}",
|
||||
]
|
||||
)
|
||||
if preserved_tags is not None:
|
||||
lines.append("tags:")
|
||||
for tag in preserved_tags:
|
||||
lines.append(f" - {tag}")
|
||||
else:
|
||||
lines.extend(
|
||||
[
|
||||
"tags:",
|
||||
" - 文献阅读",
|
||||
f" - {entry.get('domain', '')}",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"---",
|
||||
"",
|
||||
f"# {entry.get('title', '')}",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import filelock
|
||||
|
|
@ -17,13 +16,13 @@ import pytest
|
|||
from paperforge.worker.asset_index import (
|
||||
CURRENT_SCHEMA_VERSION,
|
||||
INDEX_FILENAME,
|
||||
LOCK_TIMEOUT,
|
||||
atomic_write_index,
|
||||
build_envelope,
|
||||
build_index,
|
||||
get_index_path,
|
||||
summarize_index,
|
||||
)
|
||||
from paperforge.worker.sync import frontmatter_note
|
||||
|
||||
|
||||
class TestGetIndexPath:
|
||||
|
|
@ -373,6 +372,62 @@ def _minimal_vault(tmp_path: Path) -> Path:
|
|||
return vault
|
||||
|
||||
|
||||
class TestFrontmatterNoteTagPreservation:
|
||||
"""Unit tests for frontmatter_note() tag preservation behavior."""
|
||||
|
||||
def _make_entry(self, zotero_key: str, domain: str = "test") -> dict:
|
||||
return {
|
||||
"title": "Test Paper",
|
||||
"domain": domain,
|
||||
"zotero_key": zotero_key,
|
||||
"first_author": "Author",
|
||||
"authors": ["Author"],
|
||||
"doi": "",
|
||||
"pmid": "",
|
||||
"year": "",
|
||||
"journal": "",
|
||||
"citation_key": "",
|
||||
"collections": [],
|
||||
"collection_tags": [],
|
||||
"collection_path": "",
|
||||
"impact_factor": "",
|
||||
"abstract": "",
|
||||
"has_pdf": False,
|
||||
"do_ocr": False,
|
||||
"ocr_status": "pending",
|
||||
"deep_reading_status": "pending",
|
||||
"pdf_path": "",
|
||||
"fulltext_path": "",
|
||||
}
|
||||
|
||||
def test_frontmatter_note_preserves_custom_tags(self):
|
||||
entry = self._make_entry("ABC1")
|
||||
existing_text = "---\ntags:\n - mine\n - keep\n---\nmy body\n"
|
||||
result = frontmatter_note(entry, existing_text)
|
||||
assert " - mine" in result
|
||||
assert " - keep" in result
|
||||
assert " - 文献阅读" not in result
|
||||
|
||||
def test_frontmatter_note_adds_defaults_when_no_tags(self):
|
||||
entry = self._make_entry("ABC2")
|
||||
existing_text = "---\ntitle: Test Paper\n---\nbody\n"
|
||||
result = frontmatter_note(entry, existing_text)
|
||||
assert " - 文献阅读" in result
|
||||
assert " - test" in result
|
||||
|
||||
def test_frontmatter_note_preserves_empty_tags(self):
|
||||
entry = self._make_entry("ABC3")
|
||||
existing_text = "---\ntags: []\n---\nbody\n"
|
||||
result = frontmatter_note(entry, existing_text)
|
||||
assert " - 文献阅读" not in result
|
||||
|
||||
def test_frontmatter_note_falls_back_when_malformed(self):
|
||||
entry = self._make_entry("ABC4")
|
||||
existing_text = "---\ntags:\n - foo\nbody without closing frontmatter"
|
||||
result = frontmatter_note(entry, existing_text)
|
||||
assert " - 文献阅读" in result
|
||||
|
||||
|
||||
def _ensure_domain_config(vault: Path) -> None:
|
||||
"""Create domain config so load_domain_config returns a valid configuration."""
|
||||
from paperforge.config import paperforge_paths as _pp
|
||||
|
|
|
|||
Loading…
Reference in a new issue