358 lines
16 KiB
Python
358 lines
16 KiB
Python
import os
|
||
import markdown
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
TEMPLATE = """<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{title} | ФМИ</title>
|
||
<link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||
<link rel="stylesheet" href="/static/css/custom.css">
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body);"></script>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>Физико-математический институт</h1>
|
||
<nav>
|
||
<a href="/">Главная</a>
|
||
<a href="/news.html">Новости</a>
|
||
<a href="/staff.html">Сотрудники</a>
|
||
<a href="/publications.html">Публикации</a>
|
||
<a href="/events.html">События</a>
|
||
</nav>
|
||
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="Поиск по сайту..."
|
||
onkeyup="searchSite()" style="padding: 8px; width: 98%; border: 2px solid #ddd; border-radius: 5px;">
|
||
</div>
|
||
<script>
|
||
function searchSite() {{
|
||
const input = document.getElementById('searchInput').value.toLowerCase();
|
||
const elements = document.querySelectorAll('.person, .news-card, h2, h3');
|
||
|
||
elements.forEach(el => {{
|
||
const text = el.textContent.toLowerCase();
|
||
el.style.display = text.includes(input) ? '' : 'none';
|
||
}});
|
||
}}
|
||
</script>
|
||
</header>
|
||
<main>
|
||
{content}
|
||
</main>
|
||
<footer style="margin-top: 50px; border-top: 1px solid #ccc; padding-top: 20px; font-size: 0.8em; color: #666;">
|
||
© 2026 Физико-математический институт Коми НЦ УрО РАН
|
||
</footer>
|
||
<button class="theme-toggle" onclick="toggleTheme()" title="Переключить тему">🌓</button>
|
||
<script>
|
||
function toggleTheme() {{
|
||
document.body.classList.toggle('dark-mode');
|
||
localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
|
||
}}
|
||
// Восстановление темы при загрузке
|
||
if (localStorage.getItem('theme') === 'dark') {{
|
||
document.body.classList.add('dark-mode');
|
||
}}
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
def parse_front_matter(text):
|
||
if text.strip().startswith('---'):
|
||
parts = text.split('---', 2)
|
||
if len(parts) >= 3:
|
||
meta = {}
|
||
for line in parts[1].strip().split('\n'):
|
||
if ':' in line:
|
||
key, val = line.split(':', 1)
|
||
meta[key.strip()] = val.strip().strip('"')
|
||
return meta, parts[2].strip()
|
||
return {}, text.strip()
|
||
|
||
def build_page(md_path, html_path, title):
|
||
with open(md_path, 'r', encoding='utf-8') as f:
|
||
meta, md_text = parse_front_matter(f.read())
|
||
|
||
html_content = markdown.markdown(md_text, extensions=['fenced_code', 'tables'])
|
||
with open(html_path, 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title=title, content=html_content))
|
||
|
||
def build_breadcrumbs(current_path):
|
||
"""Создает навигационную цепочку (хлебные крошки)"""
|
||
crumbs = ['<nav class="breadcrumbs">']
|
||
crumbs.append('<a href="/">🏠 Главная</a>')
|
||
|
||
parts = current_path.strip('/').split('/')
|
||
|
||
for i, part in enumerate(parts):
|
||
if i == len(parts) - 1:
|
||
# Последний элемент — это текущая страница, делаем её текстом
|
||
display_name = part.replace('.html', '').replace('-', ' ').title()
|
||
crumbs.append(f' / <span style="color: #888;">{display_name}</span>')
|
||
else:
|
||
# Промежуточные элементы. В нашей структуре файлы лежат в корне (news.html),
|
||
# поэтому нужно обязательно добавлять .html к ссылке.
|
||
display_name = part.replace('.html', '').replace('-', ' ').title()
|
||
link = f"/{part}.html" if not part.endswith('.html') else f"/{part}"
|
||
crumbs.append(f' / <a href="{link}">{display_name}</a>')
|
||
|
||
crumbs.append('</nav>')
|
||
return ''.join(crumbs)
|
||
# ------------------------------------------------------------------------
|
||
def main():
|
||
Path('public').mkdir(exist_ok=True)
|
||
Path('public/news').mkdir(exist_ok=True)
|
||
Path('public/staff').mkdir(exist_ok=True)
|
||
Path('public/events').mkdir(exist_ok=True)
|
||
|
||
# 1. Главная страница
|
||
if Path('content/index.md').exists():
|
||
build_page(Path('content/index.md'), 'public/index.html', 'Главная')
|
||
|
||
# 2. Страница новостей (с карточками)
|
||
news_files = sorted(Path('content/news').glob('*.md'), reverse=True)
|
||
|
||
news_cards = []
|
||
for md_file in news_files:
|
||
with open(md_file, 'r', encoding='utf-8') as f:
|
||
meta, body = parse_front_matter(f.read())
|
||
|
||
title = meta.get('title', md_file.stem.replace('-', ' ').title())
|
||
date = meta.get('date', '')
|
||
|
||
# Берем первые 200 символов как анонс
|
||
excerpt = body[:200].strip()
|
||
if len(body) > 200:
|
||
excerpt += '...'
|
||
|
||
# Конвертируем в HTML (только первый абзац)
|
||
first_paragraph = body.split('\n\n')[0] if '\n\n' in body else body[:150]
|
||
excerpt_html = markdown.markdown(first_paragraph, extensions=['fenced_code'])
|
||
|
||
# Форматируем дату
|
||
if date:
|
||
try:
|
||
from datetime import datetime
|
||
date_obj = datetime.strptime(date, '%Y-%m-%d')
|
||
date_formatted = date_obj.strftime('%d.%m.%Y')
|
||
except:
|
||
date_formatted = date
|
||
else:
|
||
date_formatted = md_file.stem.replace('-', '.')
|
||
|
||
# ИСПРАВЛЕНО: добавлен / в начало ссылок
|
||
card_html = f'''
|
||
<div class="news-card">
|
||
<h2><a href="/news/{md_file.stem}.html">{title}</a></h2>
|
||
<div class="news-date">{date_formatted}</div>
|
||
<div class="news-excerpt">{excerpt_html}</div>
|
||
<a href="/news/{md_file.stem}.html" class="news-read-more">Читать далее</a>
|
||
</div>'''
|
||
|
||
news_cards.append(card_html)
|
||
|
||
breadcrumbs = build_breadcrumbs('/news.html')
|
||
news_content = breadcrumbs + "<h2>Последние новости</h2>" + "".join(news_cards)
|
||
|
||
with open('public/news.html', 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title="Новости", content=news_content))
|
||
|
||
# добавлен код генерации отдельных страниц новостей
|
||
for md_file in news_files:
|
||
with open(md_file, 'r', encoding='utf-8') as f:
|
||
meta, body = parse_front_matter(f.read())
|
||
|
||
title = meta.get('title', md_file.stem.replace('-', ' ').title())
|
||
breadcrumbs = build_breadcrumbs(f'/news/{md_file.stem}.html')
|
||
|
||
back_link = f'<a href="/news.html" class="back-link">← Назад к списку новостей</a>'
|
||
full_content = breadcrumbs + back_link + '\n' + body
|
||
|
||
html_content = markdown.markdown(full_content, extensions=['fenced_code', 'tables'])
|
||
|
||
output_path = Path(f'public/news/{md_file.stem}.html')
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title=title, content=html_content))
|
||
|
||
print(f"✅ Создана страница новости: {output_path}")
|
||
|
||
# 3. Страница сотрудников
|
||
staff_by_lab = {}
|
||
unassigned = []
|
||
|
||
for md_file in sorted(Path('content/staff').glob('*.md')):
|
||
with open(md_file, 'r', encoding='utf-8') as f:
|
||
meta, _ = parse_front_matter(f.read())
|
||
|
||
name = meta.get('name', md_file.stem.replace('-', ' ').title())
|
||
degree = meta.get('degree', '')
|
||
position = meta.get('position', '')
|
||
lab = meta.get('lab', 'Без подразделения')
|
||
|
||
details = []
|
||
if degree: details.append(degree)
|
||
if position: details.append(position)
|
||
details_str = ", ".join(details)
|
||
|
||
person_html = f'''<div class="person">
|
||
<div class="person-name"><a href="staff/{md_file.stem}.html">{name}</a></div>
|
||
<div class="person-details">{details_str}</div>
|
||
</div>'''
|
||
|
||
if lab == 'Без подразделения' or not lab:
|
||
unassigned.append(person_html)
|
||
else:
|
||
if lab not in staff_by_lab:
|
||
staff_by_lab[lab] = []
|
||
staff_by_lab[lab].append(person_html)
|
||
|
||
staff_html = "<h2>Наши сотрудники</h2>"
|
||
|
||
lab_order = [
|
||
"Лаборатория математики и телекоммуникаций",
|
||
"Лаборатория теоретической и вычислительной физики",
|
||
"Лаборатория экспериментальной физики"
|
||
]
|
||
|
||
for lab in lab_order:
|
||
if lab in staff_by_lab:
|
||
staff_html += f"<h3>{lab}</h3>"
|
||
for p in staff_by_lab[lab]:
|
||
staff_html += p
|
||
|
||
for lab, people in staff_by_lab.items():
|
||
if lab not in lab_order:
|
||
staff_html += f"<h3>{lab}</h3>"
|
||
for p in people:
|
||
staff_html += p
|
||
|
||
if unassigned:
|
||
staff_html += "<h3>Сотрудники (без привязки к лаборатории)</h3>"
|
||
for p in unassigned:
|
||
staff_html += p
|
||
|
||
with open('public/staff.html', 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title="Сотрудники", content=staff_html))
|
||
|
||
# 4. Персональные страницы сотрудников
|
||
for md_file in Path('content/staff').glob('*.md'):
|
||
with open(md_file, 'r', encoding='utf-8') as f:
|
||
meta, body = parse_front_matter(f.read())
|
||
|
||
name = meta.get('name', md_file.stem.replace('-', ' ').title())
|
||
full_content = f'<a href="/staff.html" class="back-link">← Назад к списку сотрудников</a>\n{body}'
|
||
html_content = markdown.markdown(full_content, extensions=['fenced_code', 'tables'])
|
||
|
||
with open(Path(f'public/staff/{md_file.stem}.html'), 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title=name, content=html_content))
|
||
|
||
# 5. Страница с публикациями (PDF-файлы)
|
||
pdf_files = sorted(Path('static/pdf').glob('*.pdf'))
|
||
if pdf_files:
|
||
pdf_list_html = "<h2>Научные публикации и документы</h2><ul class='pdf-list'>"
|
||
for pdf in pdf_files:
|
||
# Получаем название из имени файла или из метаданных (если есть)
|
||
title = pdf.stem.replace('-', ' ').replace('_', ' ').title()
|
||
size_mb = pdf.stat().st_size / (1024 * 1024)
|
||
pdf_list_html += f"""
|
||
<li>
|
||
<a href="/static/pdf/{pdf.name}" class="pdf-link" target="_blank">{title}</a>
|
||
<span style="color: #666; font-size: 0.9em;">({size_mb:.1f} MB)</span>
|
||
</li>"""
|
||
pdf_list_html += "</ul>"
|
||
|
||
with open('public/publications.html', 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title="Публикации", content=pdf_list_html))
|
||
else:
|
||
# Если PDF пока нет, создаем заглушку
|
||
with open('public/publications.html', 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title="Публикации", content="<h2>Публикации</h2><p>В этом разделе скоро появятся научные публикации и документы.</p>"))
|
||
|
||
# 6. Страница событий (календарь)
|
||
events = []
|
||
|
||
for event_file in sorted(Path('content/events').glob('*.md')):
|
||
with open(event_file, 'r', encoding='utf-8') as f:
|
||
meta, body = parse_front_matter(f.read())
|
||
|
||
title = meta.get('title', event_file.stem.replace('-', ' ').title())
|
||
date = meta.get('date', '')
|
||
time = meta.get('time', '')
|
||
location = meta.get('location', '')
|
||
|
||
# Форматируем дату
|
||
try:
|
||
from datetime import datetime
|
||
date_obj = datetime.strptime(date, '%Y-%m-%d')
|
||
date_formatted = date_obj.strftime('%d %B %Y')
|
||
except:
|
||
date_formatted = date
|
||
|
||
events.append({
|
||
'title': title,
|
||
'date': date_formatted,
|
||
'time': time,
|
||
'location': location,
|
||
'url': f"events/{event_file.stem}.html"
|
||
})
|
||
|
||
# Генерируем HTML для календаря
|
||
events_html = "<h2>Предстоящие события</h2>"
|
||
|
||
if events:
|
||
for event in events:
|
||
events_html += f"""
|
||
<div class="event-item">
|
||
<div class="event-date">
|
||
<div class="event-day">{event['date'].split()[0]}</div>
|
||
<div class="event-month">{event['date'].split()[1]}</div>
|
||
</div>
|
||
<div class="event-content">
|
||
<h3>{event['title']}</h3>
|
||
<div class="event-details">
|
||
<div><strong>Дата:</strong> {event['date']}</div>
|
||
<div><strong>Время:</strong> {event['time']}</div>
|
||
<div><strong>Место:</strong> {event['location']}</div>
|
||
</div>
|
||
<a href="{event['url']}" class="pdf-link" style="margin-top: 15px;">Подробнее</a>
|
||
</div>
|
||
</div>"""
|
||
else:
|
||
events_html = "<p>В ближайшее время мероприятий не запланировано.</p>"
|
||
|
||
# Создаем страницу событий
|
||
breadcrumbs = build_breadcrumbs('/events.html')
|
||
with open('public/events.html', 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title="События", content=breadcrumbs + events_html))
|
||
|
||
# Создаем отдельные страницы для каждого события
|
||
for event_file in Path('content/events').glob('*.md'):
|
||
with open(event_file, 'r', encoding='utf-8') as f:
|
||
meta, body = parse_front_matter(f.read())
|
||
|
||
title = meta.get('title', event_file.stem.replace('-', ' ').title())
|
||
breadcrumbs = build_breadcrumbs(f'/events/{event_file.stem}.html')
|
||
|
||
full_content = breadcrumbs + f'<a href="/events.html" class="back-link">← Назад к календарю</a>\n' + body
|
||
html_content = markdown.markdown(full_content, extensions=['fenced_code', 'tables'])
|
||
|
||
with open(Path(f'public/events/{event_file.stem}.html'), 'w', encoding='utf-8') as f:
|
||
f.write(TEMPLATE.format(title=title, content=html_content))
|
||
|
||
# 7. Копирование статических файлов (картинки, PDF, CSS)
|
||
if Path('static').exists():
|
||
shutil.rmtree('public/static', ignore_errors=True)
|
||
shutil.copytree('static', 'public/static')
|
||
print("✅ Статические файлы скопированы (включая PDF)")
|
||
|
||
print("✅ Сайт успешно собран!")
|
||
|
||
if __name__ == '__main__':
|
||
main() |