fix(bib/sync): wrap Zotero notes in zotero-note envelope

Without the <div class="zotero-note znv1"> wrapper, Zotero treats the
note body as plain text on next launch and HTML-escapes every tag,
turning rendered output into a wall of source. The envelope is
Zotero's own sigil for "I already produced this HTML, leave it alone."

Idempotent — wraps only if not already prefixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kert
2026-05-06 14:43:11 -04:00
parent 0800ba8b9e
commit a2f4b1899e
2 changed files with 13 additions and 2 deletions

View File

@@ -539,7 +539,13 @@ def _sync_notes(
title = row["title"] or ""
if title in existing_titles:
continue
db.add_note(zot_parent_id, row["content"], title=title)
# Zotero recognizes notes already in its envelope and leaves the
# HTML alone; without it, on next launch Zotero treats the body
# as plain text and HTML-escapes every tag.
body = row["content"] or ""
if not body.startswith('<div class="zotero-note'):
body = f'<div class="zotero-note znv1">{body}</div>'
db.add_note(zot_parent_id, body, title=title)
existing_titles.add(title)
count += 1
return count

View File

@@ -454,7 +454,12 @@ class TestSyncNotes:
con = sqlite3.connect(zot_db)
rows = con.execute("SELECT title, note FROM itemNotes").fetchall()
con.close()
assert rows == [("Comment text", "<h1>Body</h1><p>text</p>")]
assert rows == [
(
"Comment text",
'<div class="zotero-note znv1"><h1>Body</h1><p>text</p></div>',
)
]
def test_resync_is_idempotent(self, tmp_path):
"""Second sync must not duplicate the note (dedup by parent + title)."""