OmniFile-Processor / src /study_guide.py
Dr. Abdulmalek
deploy: OmniFile AI Processor v4.3.0
900df0b
"""
HandwrittenOCR - مولّد المرجع الدراسي v5.3 (محسّن)
======================================================
مبنية على اقتراحات Gemini:
- table_to_markdown(): تحويل الجداول اليدوية إلى Markdown
- generate_study_guide(): توليد مرجع دراسي بصيغة Markdown
من البيانات المستخرجة من الخط اليدوي
- export_study_guide_html(): تصدير المرجع مع تلوين المصطلحات البرمجية
- generate_mermaid_diagram(): مخطط Mermaid للعلاقات بين المصطلحات
- generate_flashcards(): بطاقات تعليمية من المفردات والملاحظات
- export_flashcards_anki(): تصدير بطاقات بتنسيق Anki CSV
استخدامه:
from src.study_guide import (
generate_study_guide, table_to_markdown,
generate_mermaid_diagram, generate_flashcards,
export_flashcards_anki, export_study_guide_html,
)
"""
import os
import json
import csv
import logging
import random
from datetime import datetime
from typing import Optional
from collections import defaultdict
import pandas as pd
logger = logging.getLogger("HandwrittenOCR")
# ===================== تحويل الجداول إلى Markdown =====================
def table_to_markdown(cells_data: list[dict], columns: list[str] = None) -> str:
"""
تحويل البيانات المقطوعة من الجداول إلى تنسيق Markdown.
Parameters:
cells_data: قائمة بأزواج القاموس {english, arabic, context}
columns: أسماء الأعمدة (اختياري)
Returns:
نص Markdown يمثل الجدول
"""
if not cells_data:
return ""
if columns is None:
# استخراج المفاتيح المتاحة
available_keys = set()
for row in cells_data:
available_keys.update(row.keys())
columns = [k for k in ["english", "arabic", "context", "page"]
if k in available_keys]
if not columns:
return ""
# بناء رأس الجدول
header = "| " + " | ".join(columns) + " |"
separator = "| " + " | ".join(["---"] * len(columns)) + " |"
# بناء الصفوف
rows = []
for row in cells_data:
cells = [str(row.get(col, "")) for col in columns]
rows.append("| " + " | ".join(cells) + " |")
return "\n".join([header, separator] + rows)
# ===================== تلوين المصطلحات البرمجية =====================
# ألوان ANSI للتلوين في Markdown
SYNTAX_COLORS = {
"keywords": "#2563EB", # أزرق — الكلمات المحجوزة
"types": "#16A34A", # أخضر — أنواع البيانات
"functions": "#9333EA", # بنفسجي — الدوال والمكتبات
"numbers": "#DC2626", # أحمر — الأرقام
}
PYTHON_BUILTINS_COLOR = {
"print", "input", "len", "range", "type", "int", "str", "float",
"list", "dict", "set", "tuple", "bool", "open", "super", "self",
"append", "extend", "pop", "sort", "join", "split", "strip",
"enumerate", "zip", "map", "filter", "sorted", "reversed",
}
DATA_TYPES = {
"integers", "floating", "points", "strings", "boolean", "list",
"dictionary", "tuple", "none", "mutable", "immutable",
}
def highlight_python_terms(text: str) -> str:
"""
تمييز المصطلحات البرمجية بالنص.
يُستخدم لتلوين الكلمات داخل مرجع Markdown.
Parameters:
text: النص الأصلي
Returns:
النص مع HTML spans للتلوين (يعمل في Markdown)
"""
if not text:
return text
words = text.split()
highlighted = []
for word in words:
clean = word.strip(".,;:!?\"'()-*").lower()
if clean in PYTHON_BUILTINS_COLOR:
highlighted.append(
f'<span style="color:{SYNTAX_COLORS["functions"]}">{word}</span>'
)
elif clean in DATA_TYPES:
highlighted.append(
f'<span style="color:{SYNTAX_COLORS["types"]}">{word}</span>'
)
else:
highlighted.append(word)
return " ".join(highlighted)
# ===================== توليد المرجع الدراسي =====================
def generate_study_guide(
db,
output_path: Optional[str] = None,
title: str = "مرجع دراسة — مستخرج من الملاحظات اليدوية",
y_tolerance: int = 25,
highlight_terms: bool = True,
) -> str:
"""
توليد مرجع دراسي بصيغة Markdown من البيانات الموثقة.
يحوّل الكلمات المكتوبة بخط اليد إلى مستند منظم يحتوي:
1. عنوان وعنوان فرعي لكل صفحة
2. جداول المصطلحات (إنجليزي-عربي)
3. جمل مُعاد بناؤها
4. تلوين المصطلحات البرمجية (اختياري)
Parameters:
db: كائن قاعدة البيانات
output_path: مسار حفظ الملف (اختياري)
title: عنوان المرجع
y_tolerance: حد تباعد Y لنفس السطر
highlight_terms: تفعيل تلوين المصطلحات البرمجية
Returns:
محتوى Markdown الكامل
"""
# جلب البيانات
words = db.get_all()
if not words:
logger.warning("لا توجد بيانات لتوليد المرجع الدراسي")
return ""
df = pd.DataFrame(words)
pages = sorted(df["page_num"].dropna().unique())
guide = []
guide.append(f"# {title}\n")
guide.append(f"تاريخ الإنشاء: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
guide.append(f"عدد الصفحات: {len(pages)}\n")
guide.append("---\n")
for pg in pages:
pg_words = df[df["page_num"] == pg].sort_values(["y", "x"])
if pg_words.empty:
continue
guide.append(f"\n## صفحة رقم: {int(pg)}\n")
# 1. استخراج الجداول (أزواج إنجليزي-عربي)
table_data = _extract_table_from_page(pg_words, y_tolerance)
if table_data:
guide.append("### جداول المصطلحات\n")
guide.append(table_to_markdown(table_data))
guide.append("")
# 2. إعادة بناء الجمل
sentences = _reconstruct_page_sentences(pg_words, y_tolerance)
if sentences:
guide.append("### الملاحظات والشروحات\n")
for sent in sentences:
text = sent["text"]
if highlight_terms:
text = highlight_python_terms(text)
lang_indicator = sent.get("lang", "en")
if lang_indicator == "ar":
guide.append(f"- {text}")
else:
guide.append(f"- {text} *(EN)*")
guide.append("")
content = "\n".join(guide)
if output_path:
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)
logger.info(f"تم حفظ المرجع الدراسي في: {output_path}")
return content
def generate_study_guide_full(
db,
output_dir: Optional[str] = None,
title: str = "مرجع دراسة شامل — مستخرج من الملاحظات اليدوية",
y_tolerance: int = 25,
highlight_terms: bool = True,
include_mermaid: bool = True,
mermaid_type: str = "mindmap",
include_flashcards: bool = True,
flashcard_type: str = "bilingual",
max_flashcards: int = 100,
) -> str:
"""
توليد مرجع دراسي شامل بصيغة Markdown يتضمن:
1. المرجع الأساسي (جداول + ملاحظات)
2. مخطط Mermaid للعلاقات بين المصطلحات
3. بطاقات تعليمية (Flashcards)
هذا هو الإصدار المحسّن من generate_study_guide() الذي يجمع
كل الميزات الجديدة في ملف واحد.
Parameters:
db: كائن قاعدة البيانات
output_dir: مجلد الحفظ (اختياري)
title: عنوان المرجع
y_tolerance: حد تباعد Y لنفس السطر
highlight_terms: تفعيل تلوين المصطلحات البرمجية
include_mermaid: تضمين مخطط Mermaid
mermaid_type: نوع المخطط ("mindmap" | "flowchart" | "graph")
include_flashcards: تضمين البطاقات التعليمية
flashcard_type: نوع البطاقات ("bilingual" | "concept" | "fill_blank")
max_flashcards: الحد الأقصى للبطاقات
Returns:
محتوى Markdown الشامل
"""
# المرجع الأساسي
content = generate_study_guide(
db=db,
title=title,
y_tolerance=y_tolerance,
highlight_terms=highlight_terms,
)
if not content:
return ""
# إضافة مخطط Mermaid
if include_mermaid:
mermaid_code = generate_mermaid_diagram(db, diagram_type=mermaid_type)
if mermaid_code:
content += "\n\n---\n\n"
content += "## خريطة المفردات (Mermaid)\n\n"
content += f"```mermaid\n{mermaid_code}\n```\n"
# إضافة البطاقات التعليمية
if include_flashcards:
cards = generate_flashcards(
db=db,
card_type=flashcard_type,
max_cards=max_flashcards,
)
if cards:
content += "\n\n---\n\n"
content += flashcards_to_markdown(cards, title="البطاقات التعليمية")
# حفظ الملفات
if output_dir:
os.makedirs(output_dir, exist_ok=True)
# حفظ Markdown الشامل
md_path = os.path.join(output_dir, "study_guide_full.md")
with open(md_path, "w", encoding="utf-8") as f:
f.write(content)
logger.info(f"تم حفظ المرجع الشامل في: {md_path}")
# حفظ Mermaid منفصل
if include_mermaid:
mermaid_code = generate_mermaid_diagram(db, diagram_type=mermaid_type)
if mermaid_code:
mermaid_path = os.path.join(output_dir, "vocab_diagram.mmd")
with open(mermaid_path, "w", encoding="utf-8") as f:
f.write(mermaid_code)
logger.info(f"تم حفظ مخطط Mermaid في: {mermaid_path}")
# حفظ Flashcards بصيغة Anki
if include_flashcards:
cards = generate_flashcards(
db=db,
card_type=flashcard_type,
max_cards=max_flashcards,
)
if cards:
anki_path = os.path.join(output_dir, "flashcards_anki.csv")
export_flashcards_anki(cards, anki_path)
# حفظ HTML
html_path = os.path.join(output_dir, "study_guide_full.html")
export_study_guide_html(content, html_path, title=title)
return content
def _extract_table_from_page(
df_page: pd.DataFrame,
y_tolerance: int = 25,
) -> list[dict]:
"""
استخراج أزواج المفردات (إنجليزي-عربي) من صفحة واحدة.
يحاول ربط الكلمات الإنجليزية بالعربية على نفس السطر.
"""
table_rows = []
# تقسيم إلى أسطر
lines = []
current = [df_page.iloc[0].to_dict()]
for i in range(1, len(df_page)):
row = df_page.iloc[i].to_dict()
if abs(row["y"] - current[-1]["y"]) <= y_tolerance:
current.append(row)
else:
lines.append(current)
current = [row]
lines.append(current)
for line in lines:
en_words = []
ar_words = []
for w in line:
text = str(w.get("predicted_text", "")).strip()
if not text:
continue
# تمييز: إنجليزي (ASCII) مقابل عربي
if all(ord(c) < 128 for c in text.replace(" ", "")):
en_words.append(text)
elif any("\u0600" <= c <= "\u06FF" for c in text):
ar_words.append(text)
if en_words or ar_words:
table_rows.append({
"english": " | ".join(en_words) if en_words else "",
"arabic": " | ".join(ar_words) if ar_words else "",
})
return table_rows
def _extract_all_vocabulary(
db,
) -> list[dict]:
"""
استخراج جميع أزواج المفردات (إنجليزي-عربي) من قاعدة البيانات.
يُستخدم لتوليد المخططات والبطاقات التعليمية.
Returns:
قائمة بأزواج {"english": ..., "arabic": ..., "page": ..., "context": ...}
"""
words = db.get_all()
if not words:
return []
df = pd.DataFrame(words)
all_vocab = []
pages = sorted(df["page_num"].dropna().unique())
for pg in pages:
pg_words = df[df["page_num"] == pg].sort_values(["y", "x"])
if pg_words.empty:
continue
table_rows = _extract_table_from_page(pg_words)
for row in table_rows:
if row.get("english") or row.get("arabic"):
row["page"] = int(pg)
all_vocab.append(row)
return all_vocab
def _reconstruct_page_sentences(
df_page: pd.DataFrame,
y_tolerance: int = 25,
) -> list[dict]:
"""إعادة بناء جمل من صفحة واحدة"""
sentences = []
lines = []
current = [df_page.iloc[0].to_dict()]
for i in range(1, len(df_page)):
row = df_page.iloc[i].to_dict()
if abs(row["y"] - current[-1]["y"]) <= y_tolerance:
current.append(row)
else:
lines.append(current)
current = [row]
lines.append(current)
for line in lines:
texts = [
str(w.get("predicted_text", "")).strip()
for w in line
if w.get("predicted_text") and str(w["predicted_text"]).strip()
]
if not texts:
continue
text_preview = " ".join(texts)
lang = "en"
try:
from langdetect import detect
lang = detect(text_preview)
except Exception:
pass
# ترتيب حسب اللغة
sorted_line = sorted(line, key=lambda k: k["x"], reverse=(lang == "ar"))
sentence = " ".join(
str(w.get("predicted_text", "")) for w in sorted_line
).strip()
if sentence:
sentences.append({"text": sentence, "lang": lang})
return sentences
# ===================== مخططات Mermaid =====================
def _sanitize_mermaid_id(text: str) -> str:
"""تنظيف النص ليكون معرّفاً صالحاً في Mermaid."""
clean = text.strip()
# استبدال الأحرف غير المسموحة
for ch in ['"', "'", '(', ')', '{', '}', '[', ']', '<', '>', '/', '\\', '&', '#', '|', ';']:
clean = clean.replace(ch, "_")
clean = clean.replace(" ", "_")
# إزالة التشكيل العربي
arabic_diacritics = set("\u0610\u0611\u0612\u0613\u0614\u0615\u0616\u0617\u0618\u0619\u061A"
"\u064B\u064C\u064D\u064E\u064F\u0650\u0651\u0652")
clean = "".join(c for c in clean if c not in arabic_diacritics)
if not clean:
clean = "term"
# التأكد من أن المعرّف لا يبدأ برقم
if clean[0].isdigit():
clean = "t_" + clean
return clean[:40] # حد أقصى للطول
def generate_mermaid_diagram(
db,
diagram_type: str = "mindmap",
max_terms: int = 50,
) -> str:
"""
توليد مخطط Mermaid من المفردات المستخرجة.
يدعم ثلاثة أنواع من المخططات:
- "mindmap": خريطة ذهنية للمصطلحات الإنجليزية مع ترجماتها العربية
- "flowchart": مخطط انسيابي يربط المصطلحات حسب الصفحات
- "graph": رسم بياني يعرض العلاقات بين المفردات
Parameters:
db: كائن قاعدة البيانات
diagram_type: نوع المخطط ("mindmap" | "flowchart" | "graph")
max_terms: الحد الأقصى للمصطلحات المعروضة
Returns:
نص Mermaid جاهز للتضمين في Markdown
"""
vocab = _extract_all_vocabulary(db)
if not vocab:
logger.warning("لا توجد مفردات لتوليد مخطط Mermaid")
return ""
vocab = vocab[:max_terms]
if diagram_type == "mindmap":
return _generate_mindmap(vocab)
elif diagram_type == "flowchart":
return _generate_flowchart(vocab)
elif diagram_type == "graph":
return _generate_graph(vocab)
else:
logger.warning(f"نوع مخطط غير مدعوم: {diagram_type}")
return _generate_mindmap(vocab)
def _generate_mindmap(vocab: list[dict]) -> str:
"""
خريطة ذهنية: المصطلح الإنجليزي في الفرع الرئيسي، الترجمة العربية في الفرع الفرعي.
يُجمّع المصطلحات حسب الصفحة المصدر.
"""
lines = ["mindmap", " root((المصطلحات))"]
# تجميع حسب الصفحة
by_page = defaultdict(list)
for v in vocab:
page_key = f"صفحة {v.get('page', '?')}"
by_page[page_key].append(v)
for page_label, terms in by_page.items():
page_id = _sanitize_mermaid_id(page_label)
lines.append(f" {page_id}[{page_label}]")
for term in terms:
en = term.get("english", "").strip()
ar = term.get("arabic", "").strip()
if en and ar:
en_id = _sanitize_mermaid_id(en)
ar_id = _sanitize_mermaid_id(ar)
lines.append(f" {en_id}[{en}]")
lines.append(f" {ar_id}[{ar}]")
elif en:
en_id = _sanitize_mermaid_id(en)
lines.append(f" {en_id}[{en}]")
elif ar:
ar_id = _sanitize_mermaid_id(ar)
lines.append(f" {ar_id}[{ar}]")
return "\n".join(lines)
def _generate_flowchart(vocab: list[dict]) -> str:
"""
مخطط انسيابي: يربط المصطلحات حسب الصفحة المصدر.
كل صفحة تمثل عقدة رئيسية، والمصطلحات تخرج منها.
"""
lines = ["flowchart LR"]
by_page = defaultdict(list)
for v in vocab:
page_key = f"صفحة {v.get('page', '?')}"
by_page[page_key].append(v)
page_ids = []
for page_label, terms in by_page.items():
page_id = _sanitize_mermaid_id(page_label)
page_ids.append(page_id)
lines.append(f" {page_id}[{page_label}]")
for term in terms:
en = term.get("english", "").strip()
ar = term.get("arabic", "").strip()
label = f"{en} = {ar}" if en and ar else (en or ar)
if label:
term_id = _sanitize_mermaid_id(label)
lines.append(f" {page_id} --> {term_id}[{label}]")
# ربط الصفحات ببعضها
for i in range(len(page_ids) - 1):
lines.append(f" {page_ids[i]} -.-> {page_ids[i + 1]}")
return "\n".join(lines)
def _generate_graph(vocab: list[dict]) -> str:
"""
رسم بياني يعرض المصطلحات كعقد مرتبطة.
يربط المصطلحات الإنجليزية بالعربية بخطوط متجهة.
"""
lines = ["graph LR"]
node_count = 0
for v in vocab:
en = v.get("english", "").strip()
ar = v.get("arabic", "").strip()
if en and ar:
en_id = _sanitize_mermaid_id(en)
ar_id = _sanitize_mermaid_id(ar)
# تجنب تكرار العقد بنفس المعرّف
lines.append(f" {en_id}[{en}]")
lines.append(f" {ar_id}[{ar}]")
lines.append(f" {en_id} -->|ترجمة| {ar_id}")
node_count += 1
elif en:
en_id = _sanitize_mermaid_id(en)
lines.append(f" {en_id}[{en}]")
node_count += 1
elif ar:
ar_id = _sanitize_mermaid_id(ar)
lines.append(f" {ar_id}[{ar}]")
node_count += 1
return "\n".join(lines)
# ===================== البطاقات التعليمية (Flashcards) =====================
def generate_flashcards(
db,
card_type: str = "bilingual",
max_cards: int = 100,
shuffle: bool = True,
) -> list[dict]:
"""
توليد بطاقات تعليمية (Flashcards) من البيانات المستخرجة.
يدعم عدة أنواع من البطاقات:
- "bilingual": بطاقات ثنائية اللغة (وجه إنجليزي / ظهر عربي)
- "concept": بطاقات مفاهيمية (مصطلح / شرح من السياق)
- "fill_blank": بطاقات ملء الفراغ (جملة مع كلمة محجوبة)
Parameters:
db: كائن قاعدة البيانات
card_type: نوع البطاقات ("bilingual" | "concept" | "fill_blank")
max_cards: الحد الأقصى للبطاقات
shuffle: خلط البطاقات عشوائياً
Returns:
قائمة بطاقات، كل بطاقة dict{"front": ..., "back": ..., "tags": ...}
"""
words = db.get_all()
if not words:
logger.warning("لا توجد بيانات لتوليد البطاقات التعليمية")
return []
df = pd.DataFrame(words)
pages = sorted(df["page_num"].dropna().unique())
if card_type == "bilingual":
cards = _generate_bilingual_flashcards(df, pages)
elif card_type == "concept":
cards = _generate_concept_flashcards(df, pages)
elif card_type == "fill_blank":
cards = _generate_fill_blank_flashcards(df, pages)
else:
logger.warning(f"نوع بطاقات غير مدعوم: {card_type}")
cards = _generate_bilingual_flashcards(df, pages)
if shuffle:
random.shuffle(cards)
return cards[:max_cards]
def _generate_bilingual_flashcards(
df: pd.DataFrame,
pages: list,
) -> list[dict]:
"""
بطاقات ثنائية اللغة: الوجه بالإنجليزية والظهر بالعربية (أو العكس).
يُنشئ بطاقتين لكل زوج (EN→AR و AR→EN) لضمان التعلّم في الاتجاهين.
"""
cards = []
for pg in pages:
pg_words = df[df["page_num"] == pg].sort_values(["y", "x"])
if pg_words.empty:
continue
table_rows = _extract_table_from_page(pg_words)
for row in table_rows:
en = row.get("english", "").strip()
ar = row.get("arabic", "").strip()
if en and ar:
# بطاقة EN → AR
cards.append({
"front": en,
"back": ar,
"tags": [f"page_{int(pg)}", "EN-AR"],
})
# بطاقة AR → EN
cards.append({
"front": ar,
"back": en,
"tags": [f"page_{int(pg)}", "AR-EN"],
})
return cards
def _generate_concept_flashcards(
df: pd.DataFrame,
pages: list,
) -> list[dict]:
"""
بطاقات مفاهيمية: الوجه يحتوي مصطلح، والظهر يحتوي السياق
أو الجملة التي ظهر فيها المصطلح في الملاحظات.
"""
cards = []
for pg in pages:
pg_words = df[df["page_num"] == pg].sort_values(["y", "x"])
if pg_words.empty:
continue
# استخراج المصطلحات من الجداول
table_rows = _extract_table_from_page(pg_words)
for row in table_rows:
en = row.get("english", "").strip()
ar = row.get("arabic", "").strip()
if en:
cards.append({
"front": en,
"back": f"الترجمة العربية: {ar}" if ar else "(بدون ترجمة عربية)",
"tags": [f"page_{int(pg)}", "concept"],
})
# استخراج الجمل التي تحتوي مصطلحات مفيدة
sentences = _reconstruct_page_sentences(pg_words)
for sent in sentences:
text = sent.get("text", "").strip()
if not text or len(text.split()) < 3:
continue
# إنشاء بطاقة من الجملة: الوجه = أول كلمتين، الظهر = بقية الجملة
words_list = text.split()
if len(words_list) >= 4:
front = " ".join(words_list[:2]) + " ___"
back = text
cards.append({
"front": front,
"back": back,
"tags": [f"page_{int(pg)}", "sentence", sent.get("lang", "unknown")],
})
return cards
def _generate_fill_blank_flashcards(
df: pd.DataFrame,
pages: list,
) -> list[dict]:
"""
بطاقات ملء الفراغ: تحجب كلمة عشوائية من الجملة.
الوجه = الجملة مع فراغ، الظهر = الكلمة المحجوبة.
"""
cards = []
for pg in pages:
pg_words = df[df["page_num"] == pg].sort_values(["y", "x"])
if pg_words.empty:
continue
sentences = _reconstruct_page_sentences(pg_words)
for sent in sentences:
text = sent.get("text", "").strip()
if not text or len(text.split()) < 3:
continue
words_list = text.split()
# حجب كلمة عشوائية (ليس الأولى أو الأخيرة)
blank_idx = random.randint(1, len(words_list) - 2) if len(words_list) > 2 else 0
blanked_word = words_list[blank_idx]
words_list[blank_idx] = "______"
front = " ".join(words_list)
cards.append({
"front": front,
"back": blanked_word,
"tags": [f"page_{int(pg)}", "fill_blank", sent.get("lang", "unknown")],
})
return cards
def export_flashcards_anki(
cards: list[dict],
output_path: str,
deck_name: str = "HandwrittenOCR::Study",
include_tags: bool = True,
) -> str:
"""
تصدير البطاقات التعليمية بتنسيق CSV متوافق مع Anki.
تنسيق Anki CSV:
front;back;tags (مع فاصل منقوطة إذا لم يُحدد)
يمكن استيراد هذا الملف مباشرة في Anki عبر:
File > Import > اختيار الملف > نوع: "Separated by Semicolon"
Parameters:
cards: قائمة البطاقات من generate_flashcards()
output_path: مسار حفظ ملف CSV
deck_name: اسم الباقة في Anki
include_tags: تضمين الوسوم
Returns:
مسار الملف المحفوظ
"""
if not cards:
logger.warning("لا توجد بطاقات للتصدير")
return ""
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w", encoding="utf-8-sig", newline="") as f:
writer = csv.writer(f, delimiter=";")
# رأس Anki
if include_tags:
writer.writerow(["front", "back", "tags"])
else:
writer.writerow(["front", "back"])
for card in cards:
front = card.get("front", "").replace(";", ",").replace("\n", " ")
back = card.get("back", "").replace(";", ",").replace("\n", " ")
if include_tags:
tags_str = " ".join(card.get("tags", []))
writer.writerow([front, back, tags_str])
else:
writer.writerow([front, back])
logger.info(f"تم حفظ {len(cards)} بطاقة في: {output_path}")
return output_path
def flashcards_to_markdown(cards: list[dict], title: str = "بطاقات تعليمية") -> str:
"""
تحويل البطاقات التعليمية إلى تنسيق Markdown.
يمكن استخدام هذا مع أدوات مثل Obsidian أو Markdeep.
Parameters:
cards: قائمة البطاقات
title: عنوان القسم
Returns:
نص Markdown يحتوي البطاقات
"""
if not cards:
return ""
lines = [f"### {title}\n"]
lines.append(f"العدد الإجمالي: {len(cards)} بطاقة\n")
for i, card in enumerate(cards, 1):
front = card.get("front", "")
back = card.get("back", "")
tags = card.get("tags", [])
tags_str = f" `{'`, `'.join(tags)}`" if tags else ""
lines.append(f"#### بطاقة {i}{tags_str}\n")
lines.append(f"**الوجه:** {front}\n")
lines.append(f"**الظهر:** ||{back}||\n")
return "\n".join(lines)
# ===================== تصدير HTML (نسخة مطبوعة) =====================
def export_study_guide_html(
markdown_content: str,
output_path: str,
title: str = "مرجع دراسة",
) -> str:
"""
تحويل المرجع من Markdown إلى HTML أنيق للطباعة.
يتضمن تنسيق CSS احترافي مع دعم RTL.
"""
html = f"""<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
body {{
font-family: 'Amiri', 'Simplified Arabic', 'Segoe UI', sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 30px;
line-height: 1.8;
color: #1a1a2e;
background: #ffffff;
}}
h1 {{
text-align: center;
color: #16213e;
border-bottom: 3px solid #0f3460;
padding-bottom: 15px;
}}
h2 {{
color: #0f3460;
border-right: 4px solid #e94560;
padding-right: 15px;
}}
h3 {{
color: #533483;
}}
table {{
border-collapse: collapse;
width: 100%;
margin: 15px 0;
font-size: 14px;
}}
th, td {{
border: 1px solid #ddd;
padding: 10px 15px;
text-align: right;
}}
th {{
background: #0f3460;
color: white;
}}
tr:nth-child(even) {{
background: #f8f9fa;
}}
code {{
background: #f4f4f8;
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}}
@media print {{
body {{ padding: 0; }}
h2 {{ page-break-before: auto; }}
table {{ page-break-inside: avoid; }}
}}
</style>
</head>
<body>
"""
# تحويل Markdown بسيط إلى HTML
lines = markdown_content.split("\n")
in_table = False
for line in lines:
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("# "):
html += f"<h1>{stripped[2:]}</h1>\n"
elif stripped.startswith("## "):
html += f"<h2>{stripped[3:]}</h2>\n"
elif stripped.startswith("### "):
html += f"<h3>{stripped[4:]}</h3>\n"
elif stripped.startswith("---"):
html += "<hr>\n"
elif stripped.startswith("|"):
# جدول Markdown
cells = [c.strip() for c in stripped.split("|") if c.strip()]
if all(set(c) <= {"-"} for c in cells):
continue # فاصل الجدول
if not in_table:
html += "<table>\n"
in_table = True
html += "<tr>"
for cell in cells:
html += f"<td>{cell}</td>"
html += "</tr>\n"
else:
if in_table:
html += "</table>\n"
in_table = False
if stripped.startswith("- "):
html += f"<li>{stripped[2:]}</li>\n"
else:
html += f"<p>{stripped}</p>\n"
if in_table:
html += "</table>\n"
html += "</body>\n</html>"
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
logger.info(f"تم حفظ المرجع HTML في: {output_path}")
return output_path