ContiAI / core /content_converter_service.py
ziadsameh32's picture
Add login page
325b94c
Raw
History Blame Contribute Delete
28.4 kB
#!/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('<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)):
# فقرة عادية (ليست قائمة ولا عنوان)
# تقسيم إلى أسطر إذا كان يحتوي على \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'<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** أو __text__
text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'__([^_]+)__', r'<strong>\1</strong>', text)
# النص المائل *text* أو _text_
text = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', text)
text = re.sub(r'_([^_]+)_', r'<em>\1</em>', text)
# الروابط [text](url)
text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'<a href="\2" target="_blank" rel="noopener noreferrer">\1</a>', text)
# الكود `code`
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 و JS
css_content = get_css_content()
toc_data = generate_table_of_contents(data)
js_content = get_js_content(toc_data)
# بداية HTML
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>
""")
# معالجة المحاور الفرعية (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""" <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_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)