pmi-site/build.py

358 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()