| |
| |
| """ |
| 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 |
| |
| |
| |
| 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('<ol'): |
| |
| if html_parts and html_parts[-1].startswith('<ul'): |
| html_parts.append('</ul>') |
| html_parts.append('<ol class="numbered-list">') |
| |
| content = match.group(2).strip() |
| content = process_inline_markdown(content) |
| html_parts.append(f'<li>{content}</li>') |
| continue |
| |
| |
| if para.startswith('- ') or para.startswith('* '): |
| if not html_parts or not html_parts[-1].startswith('<ul'): |
| if html_parts and html_parts[-1].startswith('<ol'): |
| html_parts.append('</ol>') |
| html_parts.append('<ul class="bullet-list">') |
| |
| content = para[2:].strip() |
| content = process_inline_markdown(content) |
| html_parts.append(f'<li>{content}</li>') |
| 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('<ul') or last_item.startswith('<ol') or last_item.startswith('<li'): |
| |
| for i in range(len(html_parts) - 1, -1, -1): |
| if html_parts[i].startswith('<ol'): |
| html_parts.append('</ol>') |
| break |
| elif html_parts[i].startswith('<ul'): |
| html_parts.append('</ul>') |
| break |
| |
| |
| if para.startswith('### '): |
| content = process_inline_markdown(para[4:].strip()) |
| html_parts.append(f'<h5 class="markdown-h5">{content}</h5>') |
| elif para.startswith('## '): |
| content = process_inline_markdown(para[3:].strip()) |
| html_parts.append(f'<h4 class="markdown-h4">{content}</h4>') |
| elif para.startswith('# '): |
| content = process_inline_markdown(para[2:].strip()) |
| html_parts.append(f'<h3 class="markdown-h3">{content}</h3>') |
| elif not para.startswith('- ') and not para.startswith('* ') and not (para and len(para) > 3 and re.match(r'^(\d+)\.\s+', para)): |
| |
| |
| 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'<strong class="markdown-subtitle">{process_inline_markdown(line)}</strong>') |
| else: |
| processed_lines.append(process_inline_markdown(line)) |
| content = '<br>'.join(processed_lines) |
| else: |
| content = process_inline_markdown(para) |
| |
| html_parts.append(f'<p>{content}</p>') |
| |
| |
| if html_parts: |
| if html_parts[-1].startswith('<ul') or html_parts[-1].startswith('<ol'): |
| if html_parts[-1].startswith('<ul'): |
| html_parts.append('</ul>') |
| elif html_parts[-1].startswith('<ol'): |
| html_parts.append('</ol>') |
| |
| return '\n'.join(html_parts) |
|
|
|
|
| def process_inline_markdown(text): |
| """معالجة Markdown داخل السطر (bold, italic, links)""" |
| if not text: |
| return "" |
| |
| |
| text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', text) |
| text = re.sub(r'__([^_]+)__', r'<strong>\1</strong>', text) |
| |
| |
| text = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', text) |
| text = re.sub(r'_([^_]+)_', r'<em>\1</em>', text) |
| |
| |
| text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'<a href="\2" target="_blank" rel="noopener noreferrer">\1</a>', text) |
| |
| |
| text = re.sub(r'`([^`]+)`', r'<code>\1</code>', 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_content = get_css_content() |
| toc_data = generate_table_of_contents(data) |
| js_content = get_js_content(toc_data) |
| |
| |
| html_parts.append("""<!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>{}</title> |
| <style> |
| {} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header class="course-header"> |
| <h1 class="course-title">{}</h1> |
| """.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(""" <div class="course-audience"> |
| <h2 class="section-title">الجمهور المستهدف</h2> |
| <p class="audience-text">{}</p> |
| </div> |
| """.format(escape_html(data['course_audience']))) |
| |
| html_parts.append(""" </header> |
| |
| <nav class="table-of-contents" id="toc"> |
| <h2>جدول المحتويات</h2> |
| <ul id="toc-list"></ul> |
| </nav> |
| |
| <main class="course-content"> |
| """) |
| |
| |
| 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""" <section class="unit-section" id="{unit_id}"> |
| <div class="unit-header"> |
| <h2 class="unit-title">الوحدة {unit_idx}: {escape(unit['unit_name'])}</h2> |
| """) |
| |
| |
| if 'unit_outcome' in unit and unit['unit_outcome']: |
| html_parts.append(f""" <div class="unit-outcome"> |
| <h3 class="outcome-title"> |
| <span class="icon">🎯</span> |
| مخرجات التعلم للوحدة |
| </h3> |
| <p class="outcome-text">{escape_html(unit['unit_outcome'])}</p> |
| </div> |
| """) |
| |
| html_parts.append(" </div>") |
| |
| |
| if 'topics' in unit and isinstance(unit['topics'], list): |
| html_parts.append(""" <div class="topics-container"> |
| """) |
| |
| 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""" <div class="topic-container" id="{topic_id}"> |
| <h3 class="topic-title"> |
| <span class="topic-number">{unit_idx}.{topic_idx}</span> |
| {escape(topic['topic_title'])} |
| </h3> |
| """) |
| |
| |
| 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""" <div class="subtopic-container" id="{subtopic_id}"> |
| <h4 class="subtopic-title"> |
| <span class="subtopic-number">{unit_idx}.{topic_idx}.{subtopic_idx}</span> |
| {escape(subtopic.get('title', ''))} |
| </h4> |
| """) |
| |
| |
| if 'description' in subtopic and subtopic['description']: |
| html_parts.append(f""" <div class="subtopic-description"> |
| <span class="description-label">الوصف:</span> |
| <span class="description-text">{escape_html(subtopic['description'])}</span> |
| </div> |
| """) |
| |
| |
| if 'generated_content' in subtopic and subtopic['generated_content']: |
| content = subtopic['generated_content'].strip() |
| |
| markdown_html = markdown_to_html(content) |
| |
| html_parts.append(' <div class="subtopic-content">') |
| html_parts.append(markdown_html) |
| html_parts.append(" </div>") |
| |
| |
| if 'sources' in subtopic and subtopic['sources'] and len(subtopic['sources']) > 0: |
| html_parts.append(' <div class="subtopic-sources">') |
| html_parts.append(' <h5 class="sources-title">المصادر:</h5>') |
| html_parts.append(' <ul class="sources-list">') |
| for source in subtopic['sources']: |
| if source.strip(): |
| html_parts.append(f""" <li> |
| <a href="{escape(source.strip())}" target="_blank" rel="noopener noreferrer"> |
| {escape(source.strip())} |
| </a> |
| </li>""") |
| html_parts.append(" </ul>") |
| html_parts.append(" </div>") |
| |
| html_parts.append(" </div>") |
| |
| html_parts.append(" </div>") |
| |
| html_parts.append(" </div>") |
| |
| html_parts.append(" </section>") |
| |
| html_parts.append(""" </main> |
| |
| <footer class="course-footer"> |
| <p>تم إنشاء هذا المقرر بواسطة أداة تحويل JSON إلى HTML</p> |
| </footer> |
| </div> |
| |
| <button id="scroll-top" class="scroll-top-btn" aria-label="العودة للأعلى">↑</button> |
| |
| <script> |
| {} |
| </script> |
| </body> |
| </html>""".format(js_content)) |
| |
| return '\n'.join(html_parts) |
|
|
|
|