#!/usr/bin/env python # -*- coding: utf-8 -*- """ FastAPI Service لتحويل بيانات المقررات الدراسية من Dictionary إلى HTML FastAPI Service for Converting Course Data from Dictionary to HTML """ import re import json from html import escape from typing import Dict, Any def escape_html(text): """تحويل النص إلى HTML آمن""" if not text: return "" text = escape(text) return text def markdown_to_html(text): """تحويل Markdown إلى HTML""" if not text: return "" # تقسيم النص إلى فقرات paragraphs = text.split('\n\n') html_parts = [] for para in paragraphs: para = para.strip() if not para: continue # معالجة القوائم المرقمة (1., 2., إلخ) أو **text** # التحقق من أن السطر يبدأ برقم متبوع بنقطة ومسافة if para and len(para) > 3: # محاولة مطابقة نمط "رقم. مسافة" match = re.match(r'^(\d+)\.\s+(.+)$', para) if match: # قائمة مرقمة if not html_parts or not html_parts[-1].startswith('') html_parts.append('
    ') content = match.group(2).strip() content = process_inline_markdown(content) html_parts.append(f'
  1. {content}
  2. ') continue # معالجة القوائم بنقاط (- أو *) if para.startswith('- ') or para.startswith('* '): if not html_parts or not html_parts[-1].startswith('') html_parts.append('
      ') content = para[2:].strip() content = process_inline_markdown(content) html_parts.append(f'
    • {content}
    • ') continue # إغلاق القوائم المفتوحة قبل الفقرات العادية # لكن فقط إذا لم يكن السطر جزءًا من قائمة should_close_lists = True if para.startswith('- ') or para.startswith('* ') or (para and len(para) > 3 and re.match(r'^(\d+)\.\s+', para)): should_close_lists = False if should_close_lists and html_parts: last_item = html_parts[-1] if html_parts else "" if last_item.startswith('') break elif html_parts[i].startswith('') break # معالجة العناوين (###, ##, #) if para.startswith('### '): content = process_inline_markdown(para[4:].strip()) html_parts.append(f'
      {content}
      ') elif para.startswith('## '): content = process_inline_markdown(para[3:].strip()) html_parts.append(f'

      {content}

      ') elif para.startswith('# '): content = process_inline_markdown(para[2:].strip()) html_parts.append(f'

      {content}

      ') elif not para.startswith('- ') and not para.startswith('* ') and not (para and len(para) > 3 and re.match(r'^(\d+)\.\s+', para)): # فقرة عادية (ليست قائمة ولا عنوان) # تقسيم إلى أسطر إذا كان يحتوي على \n if '\n' in para: lines = para.split('\n') processed_lines = [] for line in lines: line = line.strip() if line: # التحقق إذا كانت السطر يبدو كعنوان (قصير ولا ينتهي بنقطة) if len(line) < 80 and not line.endswith('.') and not line.endswith(':') and not line.startswith('-') and not line.startswith('*'): processed_lines.append(f'{process_inline_markdown(line)}') else: processed_lines.append(process_inline_markdown(line)) content = '
      '.join(processed_lines) else: content = process_inline_markdown(para) html_parts.append(f'

      {content}

      ') # إغلاق أي قوائم مفتوحة if html_parts: if html_parts[-1].startswith('') elif html_parts[-1].startswith('') return '\n'.join(html_parts) def process_inline_markdown(text): """معالجة Markdown داخل السطر (bold, italic, links)""" if not text: return "" # النص الغامق **text** أو __text__ text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) text = re.sub(r'__([^_]+)__', r'\1', text) # النص المائل *text* أو _text_ text = re.sub(r'\*([^*]+)\*', r'\1', text) text = re.sub(r'_([^_]+)_', r'\1', text) # الروابط [text](url) text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1', text) # الكود `code` text = re.sub(r'`([^`]+)`', r'\1', text) return text def generate_table_of_contents(data): """توليد جدول المحتويات كـ JavaScript""" toc_items = [] if 'results' in data and isinstance(data['results'], list): for unit_idx, unit in enumerate(data['results'], 1): unit_id = f"unit-{unit_idx}" unit_name = unit.get('unit_name', f'الوحدة {unit_idx}') toc_items.append({ 'id': unit_id, 'text': f'الوحدة {unit_idx}: {unit_name}', 'children': [] }) if 'topics' in unit and isinstance(unit['topics'], list): for topic_idx, topic in enumerate(unit['topics'], 1): topic_id = f"unit-{unit_idx}-topic-{topic_idx}" topic_title = topic.get('topic_title', f'الموضوع {topic_idx}') toc_items[-1]['children'].append({ 'id': topic_id, 'text': f'{unit_idx}.{topic_idx} {topic_title}' }) return json.dumps(toc_items, ensure_ascii=False, indent=2) def get_css_content(): """إرجاع محتوى ملف CSS""" return """/* تصميم المقرر الدراسي - Course Design */ :root { --primary-color: #2c3e50; --secondary-color: #3498db; --accent-color: #e74c3c; --text-color: #333; --bg-color: #ffffff; --light-bg: #f8f9fa; --border-color: #dee2e6; --shadow: 0 2px 8px rgba(0,0,0,0.1); --shadow-hover: 0 4px 12px rgba(0,0,0,0.15); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.8; color: var(--text-color); background-color: var(--light-bg); direction: rtl; } .container { max-width: 1200px; margin: 0 auto; background-color: var(--bg-color); box-shadow: var(--shadow); } /* Header */ .course-header { background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white; padding: 3rem 2rem; text-align: center; } .course-title { font-size: 2.5rem; margin-bottom: 1.5rem; font-weight: 700; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); } .course-audience { background: rgba(255,255,255,0.1); padding: 1.5rem; border-radius: 8px; margin-top: 1.5rem; text-align: right; } .section-title { font-size: 1.3rem; margin-bottom: 0.5rem; font-weight: 600; } .audience-text { font-size: 1.1rem; line-height: 1.8; opacity: 0.95; } /* Table of Contents */ .table-of-contents { background-color: var(--light-bg); padding: 2rem; border-bottom: 3px solid var(--secondary-color); position: relative; top: 0; z-index: 100; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .table-of-contents h2 { color: var(--primary-color); margin-bottom: 1rem; font-size: 1.5rem; } #toc-list { list-style: none; } #toc-list > li { margin-bottom: 0.5rem; } #toc-list > li > a { display: block; padding: 0.75rem 1rem; color: var(--primary-color); text-decoration: none; font-weight: 600; border-radius: 5px; transition: all 0.3s ease; border-right: 3px solid transparent; } #toc-list > li > a:hover { background-color: rgba(52, 152, 219, 0.1); border-right-color: var(--secondary-color); transform: translateX(-5px); } #toc-list ul { list-style: none; margin-top: 0.5rem; margin-right: 1rem; } #toc-list ul li a { display: block; padding: 0.5rem 1rem; color: #555; text-decoration: none; font-size: 0.95rem; border-radius: 5px; transition: all 0.3s ease; } #toc-list ul li a:hover { background-color: rgba(52, 152, 219, 0.1); color: var(--secondary-color); } /* Main Content */ .course-content { padding: 2rem; } .unit-section { margin-bottom: 4rem; padding: 2rem; background-color: var(--bg-color); border-radius: 10px; box-shadow: var(--shadow); scroll-margin-top: 150px; } .unit-header { border-bottom: 3px solid var(--secondary-color); padding-bottom: 1.5rem; margin-bottom: 2rem; } .unit-title { font-size: 2rem; color: var(--primary-color); margin-bottom: 1rem; font-weight: 700; line-height: 1.4; } .unit-outcome { background: linear-gradient(135deg, #e8f4f8 0%, #d1ecf1 100%); padding: 1.5rem; border-radius: 8px; border-right: 4px solid var(--secondary-color); margin-top: 1rem; } .outcome-title { font-size: 1.2rem; color: var(--primary-color); margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem; } .outcome-title .icon { font-size: 1.5rem; } .outcome-text { font-size: 1.05rem; line-height: 1.8; color: #444; } /* Topics */ .topics-container { margin-top: 2rem; } .topic-container { margin-bottom: 3rem; padding: 1.5rem; background-color: var(--light-bg); border-radius: 8px; scroll-margin-top: 150px; } .topic-title { font-size: 1.6rem; color: var(--primary-color); margin-bottom: 1.5rem; padding-bottom: 0.75rem; border-bottom: 2px solid var(--border-color); display: flex; align-items: center; gap: 0.75rem; } .topic-number { background: var(--secondary-color); color: white; padding: 0.4rem 0.8rem; border-radius: 5px; font-size: 1rem; font-weight: 700; min-width: 50px; text-align: center; } /* Subtopics */ .subtopic-container { margin-bottom: 2.5rem; padding: 1.5rem; background-color: white; border-radius: 8px; border-right: 4px solid var(--secondary-color); box-shadow: 0 1px 3px rgba(0,0,0,0.1); scroll-margin-top: 150px; } .subtopic-title { font-size: 1.3rem; color: var(--primary-color); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.75rem; } .subtopic-number { background: var(--accent-color); color: white; padding: 0.3rem 0.7rem; border-radius: 5px; font-size: 0.9rem; font-weight: 700; min-width: 45px; text-align: center; } .subtopic-description { background-color: #fff3cd; padding: 1rem; border-radius: 5px; margin-bottom: 1rem; border-right: 3px solid #ffc107; } .description-label { font-weight: 600; color: #856404; margin-left: 0.5rem; } .description-text { color: #856404; font-style: italic; } .subtopic-content { margin-top: 1.5rem; line-height: 2; color: #444; } .subtopic-content p { margin-bottom: 1rem; text-align: justify; font-size: 1.05rem; } .subtopic-content strong { color: var(--primary-color); font-weight: 600; } .subtopic-sources { margin-top: 2rem; padding: 1.5rem; background-color: var(--light-bg); border-radius: 8px; border-top: 2px solid var(--border-color); } .sources-title { font-size: 1.1rem; color: #666; margin-bottom: 1rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; } .sources-title::before { content: "🔗"; font-size: 1.2rem; } .sources-list { list-style: none; padding-right: 1rem; } .sources-list li { margin-bottom: 0.75rem; padding: 0.5rem; background-color: white; border-radius: 5px; transition: all 0.3s ease; } .sources-list li:hover { background-color: #e9ecef; transform: translateX(-3px); } .sources-list a { color: var(--secondary-color); text-decoration: none; font-size: 0.95rem; word-break: break-all; transition: color 0.3s ease; } .sources-list a:hover { color: var(--accent-color); text-decoration: underline; } /* Footer */ .course-footer { background-color: var(--primary-color); color: white; text-align: center; padding: 2rem; margin-top: 3rem; } /* Scroll to Top Button */ .scroll-top-btn { position: fixed; bottom: 30px; left: 30px; width: 50px; height: 50px; background: var(--secondary-color); color: white; border: none; border-radius: 50%; font-size: 1.5rem; cursor: pointer; box-shadow: var(--shadow); opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 1000; } .scroll-top-btn.visible { opacity: 1; visibility: visible; } .scroll-top-btn:hover { background: var(--primary-color); transform: translateY(-3px); box-shadow: var(--shadow-hover); } /* Responsive Design */ @media (max-width: 768px) { .course-title { font-size: 1.8rem; } .course-header { padding: 2rem 1.5rem; } .course-content { padding: 1rem; } .unit-section { padding: 1.5rem; } .unit-title { font-size: 1.5rem; } .topic-title { font-size: 1.3rem; flex-direction: column; align-items: flex-start; } .subtopic-title { font-size: 1.1rem; flex-direction: column; align-items: flex-start; } .table-of-contents { padding: 1rem; position: relative; } .scroll-top-btn { bottom: 20px; left: 20px; width: 45px; height: 45px; } } /* Print Styles */ @media print { .table-of-contents { position: relative; page-break-after: always; } .scroll-top-btn { display: none; } .unit-section { page-break-inside: avoid; } .subtopic-container { page-break-inside: avoid; } } """ def get_js_content(toc_data): """إرجاع محتوى ملف JavaScript""" return f"""// JavaScript for Course HTML // جدول المحتويات const tocData = {toc_data}; // إنشاء جدول المحتويات function buildTableOfContents() {{ const tocList = document.getElementById('toc-list'); if (!tocList) return; tocData.forEach(unit => {{ const unitLi = document.createElement('li'); const unitLink = document.createElement('a'); unitLink.href = `#${{unit.id}}`; unitLink.textContent = unit.text; unitLi.appendChild(unitLink); if (unit.children && unit.children.length > 0) {{ const topicsUl = document.createElement('ul'); unit.children.forEach(topic => {{ const topicLi = document.createElement('li'); const topicLink = document.createElement('a'); topicLink.href = `#${{topic.id}}`; topicLink.textContent = topic.text; topicLi.appendChild(topicLink); topicsUl.appendChild(topicLi); }}); unitLi.appendChild(topicsUl); }} tocList.appendChild(unitLi); }}); // إضافة تأثير Scroll السلس عند النقر document.querySelectorAll('#toc-list a').forEach(link => {{ link.addEventListener('click', function(e) {{ e.preventDefault(); const targetId = this.getAttribute('href').substring(1); const targetElement = document.getElementById(targetId); if (targetElement) {{ const offsetTop = targetElement.offsetTop - 100; window.scrollTo({{ top: offsetTop, behavior: 'smooth' }}); // تحديث URL بدون إعادة تحميل الصفحة history.pushState(null, null, `#${{targetId}}`); }} }}); }}); }} // زر العودة للأعلى function setupScrollToTop() {{ const scrollBtn = document.getElementById('scroll-top'); if (!scrollBtn) return; window.addEventListener('scroll', function() {{ if (window.pageYOffset > 300) {{ scrollBtn.classList.add('visible'); }} else {{ scrollBtn.classList.remove('visible'); }} }}); scrollBtn.addEventListener('click', function() {{ window.scrollTo({{ top: 0, behavior: 'smooth' }}); }}); }} // تمييز القسم الحالي في جدول المحتويات function highlightCurrentSection() {{ const sections = document.querySelectorAll('.unit-section, .topic-container'); const tocLinks = document.querySelectorAll('#toc-list a'); window.addEventListener('scroll', function() {{ let current = ''; const scrollPosition = window.pageYOffset + 150; sections.forEach(section => {{ const sectionTop = section.offsetTop; const sectionHeight = section.clientHeight; if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {{ current = section.getAttribute('id'); }} }}); tocLinks.forEach(link => {{ link.classList.remove('active'); if (link.getAttribute('href') === `#${{current}}`) {{ link.classList.add('active'); }} }}); }}); }} // تهيئة عند تحميل الصفحة document.addEventListener('DOMContentLoaded', function() {{ buildTableOfContents(); setupScrollToTop(); highlightCurrentSection(); // إذا كان هناك hash في URL، قم بالتمرير إليه if (window.location.hash) {{ setTimeout(() => {{ const targetId = window.location.hash.substring(1); const targetElement = document.getElementById(targetId); if (targetElement) {{ const offsetTop = targetElement.offsetTop - 100; window.scrollTo({{ top: offsetTop, behavior: 'smooth' }}); }} }}, 500); }} }}); // إضافة تأثيرات تفاعلية على الروابط document.addEventListener('DOMContentLoaded', function() {{ const sourcesLinks = document.querySelectorAll('.sources-list a'); sourcesLinks.forEach(link => {{ link.addEventListener('mouseenter', function() {{ this.style.transform = 'translateX(-3px)'; }}); link.addEventListener('mouseleave', function() {{ this.style.transform = 'translateX(0)'; }}); }}); }}); """ def convert_dict_to_html(data: Dict[str, Any]) -> str: """ تحويل Dictionary إلى HTML string Args: data: Dictionary يحتوي على بيانات المقرر الدراسي Returns: str: HTML string كامل """ html_parts = [] # توليد CSS و JS css_content = get_css_content() toc_data = generate_table_of_contents(data) js_content = get_js_content(toc_data) # بداية HTML html_parts.append(""" {}

      {}

      """.format( escape(data.get('course_name', 'مقرر دراسي')), css_content, escape(data.get('course_name', 'مقرر دراسي')) )) # الجمهور المستهدف if 'course_audience' in data and data['course_audience']: html_parts.append("""

      الجمهور المستهدف

      {}

      """.format(escape_html(data['course_audience']))) html_parts.append("""
      """) # معالجة الوحدات if 'results' in data and isinstance(data['results'], list): for unit_idx, unit in enumerate(data['results'], 1): # عنوان الوحدة if 'unit_name' in unit: unit_id = f"unit-{unit_idx}" html_parts.append(f"""

      الوحدة {unit_idx}: {escape(unit['unit_name'])}

      """) # مخرجات الوحدة if 'unit_outcome' in unit and unit['unit_outcome']: html_parts.append(f"""

      🎯 مخرجات التعلم للوحدة

      {escape_html(unit['unit_outcome'])}

      """) html_parts.append("
      ") # معالجة الموضوعات if 'topics' in unit and isinstance(unit['topics'], list): html_parts.append("""
      """) for topic_idx, topic in enumerate(unit['topics'], 1): # عنوان الموضوع if 'topic_title' in topic: topic_id = f"unit-{unit_idx}-topic-{topic_idx}" html_parts.append(f"""

      {unit_idx}.{topic_idx} {escape(topic['topic_title'])}

      """) # معالجة المحاور الفرعية (Subtopics) if 'subtopics' in topic and isinstance(topic['subtopics'], list): for subtopic_idx, subtopic in enumerate(topic['subtopics'], 1): subtopic_id = f"unit-{unit_idx}-topic-{topic_idx}-subtopic-{subtopic_idx}" html_parts.append(f"""

      {unit_idx}.{topic_idx}.{subtopic_idx} {escape(subtopic.get('title', ''))}

      """) # الوصف if 'description' in subtopic and subtopic['description']: html_parts.append(f"""
      الوصف: {escape_html(subtopic['description'])}
      """) # المحتوى المولد if 'generated_content' in subtopic and subtopic['generated_content']: content = subtopic['generated_content'].strip() # تحويل Markdown إلى HTML markdown_html = markdown_to_html(content) html_parts.append('
      ') html_parts.append(markdown_html) html_parts.append("
      ") # المصادر if 'sources' in subtopic and subtopic['sources'] and len(subtopic['sources']) > 0: html_parts.append('
      ') html_parts.append('
      المصادر:
      ') html_parts.append('
        ') for source in subtopic['sources']: if source.strip(): html_parts.append(f"""
      • {escape(source.strip())}
      • """) html_parts.append("
      ") html_parts.append("
      ") html_parts.append("
      ") html_parts.append("
      ") html_parts.append("
      ") html_parts.append("
      ") html_parts.append("""

      تم إنشاء هذا المقرر بواسطة أداة تحويل JSON إلى HTML

      """.format(js_content)) return '\n'.join(html_parts)