""" Export module for notes.qpid.pro — Stage 2 Supports: CSV (Excel-friendly with BOM), JSON, Anki TSV (Cloze-ready), Markdown. """ import csv import io import json import re from datetime import datetime, timezone from typing import List, Dict, Any def export_csv(cards: List[Dict[str, Any]]) -> bytes: """CSV with UTF-8 BOM for correct Excel auto-detection of encoding.""" output = io.StringIO() fieldnames = [ "term", "translation", "context", "phonetic", "part_of_speech", "definition_en", "collection", "tags", "notes", "created_at", ] writer = csv.DictWriter( output, fieldnames=fieldnames, extrasaction="ignore", quoting=csv.QUOTE_MINIMAL, lineterminator="\r\n", ) writer.writeheader() for c in cards: writer.writerow({ "term": c.get("term") or "", "translation": c.get("translation") or "", "context": c.get("context_sentence") or c.get("context") or "", "phonetic": c.get("phonetic") or "", "part_of_speech": c.get("part_of_speech") or "", "definition_en": c.get("definition_en") or "", "collection": c.get("collection") or "General", "tags": c.get("tags") or "", "notes": c.get("notes") or "", "created_at": c.get("created_at") or "", }) # UTF-8 BOM (\ufeff) so Excel auto-detects encoding return ("\ufeff" + output.getvalue()).encode("utf-8") def export_json(cards: List[Dict[str, Any]]) -> bytes: """Full JSON export with metadata header.""" payload = { "exported_at": datetime.now(timezone.utc).isoformat(), "total": len(cards), "cards": [ { "id": c.get("id"), "term": c.get("term") or "", "translation": c.get("translation") or "", "context_sentence": c.get("context_sentence") or c.get("context") or "", "phonetic": c.get("phonetic") or "", "part_of_speech": c.get("part_of_speech") or "", "definition_en": c.get("definition_en") or "", "collection": c.get("collection") or "General", "tags": c.get("tags") or "", "notes": c.get("notes") or "", "audio_url": c.get("audio_url") or "", "created_at": c.get("created_at") or "", "updated_at": c.get("updated_at") or "", } for c in cards ], } return json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") def export_anki_tsv(cards: List[Dict[str, Any]]) -> bytes: """ Anki-compatible tab-separated export with cloze deletion support. """ lines: List[str] = [ "#separator:tab", "#html:true", "#notetype:Basic (and reversed card)", "#columns:Front\tBack\tTags\tContext", ] for c in cards: term = (c.get("term") or "").replace("\t", " ").replace("\n", " ") translation = (c.get("translation") or "").replace("\t", " ").replace("\n", " ") phonetic = (c.get("phonetic") or "").replace("\t", " ") context = (c.get("context_sentence") or c.get("context") or "").replace("\t", " ").replace("\n", " ") tags_raw = c.get("tags") or "" collection = (c.get("collection") or "General").strip().replace(" ", "_") # Cloze representation if context contains the term cloze_ctx = "" if context and term and term.lower() in context.lower(): cloze_ctx = re.sub(re.escape(term), f"{{{{c1::{term}}}}}", context, flags=re.IGNORECASE) tag_parts = [f"collection_{collection}"] + [ t.strip().replace(" ", "_") for t in tags_raw.split(",") if t.strip() ] tags_str = " ".join(tag_parts) front = term if not phonetic else f"{term} [{phonetic}]" back = translation lines.append(f"{front}\t{back}\t{tags_str}\t{cloze_ctx or context}") return ("\n".join(lines) + "\n").encode("utf-8") def export_markdown(cards: List[Dict[str, Any]]) -> bytes: """Human-readable Markdown export, grouped by collection.""" now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") lines: List[str] = [ f"# 📚 Vocabulary Export — {now_str}", f"**Total cards:** {len(cards)}", "", ] by_collection: Dict[str, List[Dict[str, Any]]] = {} for c in cards: col = (c.get("collection") or "General").strip() by_collection.setdefault(col, []).append(c) for collection, col_cards in sorted(by_collection.items()): lines.append(f"## {collection} ({len(col_cards)} cards)") lines.append("") lines.append("| Term | Translation | Phonetic | Tags |") lines.append("|------|-------------|----------|------|") for c in col_cards: term = (c.get("term") or "").replace("|", "\\|") translation = (c.get("translation") or "").replace("|", "\\|") phonetic = (c.get("phonetic") or "").replace("|", "\\|") tags = (c.get("tags") or "").replace("|", "\\|") lines.append(f"| **{term}** | {translation} | {phonetic} | {tags} |") lines.append("") lines.append("### Details") lines.append("") for c in col_cards: term = c.get("term") or "" phonetic_str = f" `{c['phonetic']}`" if c.get("phonetic") else "" pos_str = f" *({c['part_of_speech']})*" if c.get("part_of_speech") else "" lines.append(f"### {term}{phonetic_str}{pos_str}") lines.append(f"- **Перевод**: {c.get('translation') or '—'}") if c.get("definition_en"): lines.append(f"- **Толкование**: {c['definition_en']}") if c.get("context_sentence") or c.get("context"): lines.append(f"- **Контекст**: *\"{c.get('context_sentence') or c.get('context')}\"*") if c.get("notes"): lines.append(f"- **Заметка**: {c['notes']}") lines.append("") return ("\n".join(lines) + "\n").encode("utf-8")