Files
sitegen/content/ru/blog/2025/01-sitegen.md
2025-04-17 12:06:16 +03:00

135 lines
6.5 KiB
Markdown
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.
# Генератор статических сайтов в 90 строк Python кода
Создано: 16 апреля 2025
[In English](/en/blog/2025/01-sitegen)
Давным-давно я сделал небольшой сайт-визитку - он состоял из трех простеньких
HTML-страниц, одного CSS-файла (который я генерировал из SCSS), нескольких
шрифтов и картинок. Этого было более чем достаточно, чтобы ссылка на мой
сайт красовалась в каком-либо резюме или профиле соцсети.
![Изображение старого сайта](/images/01-oldsite.png "Изображение старого сайта")
Недавно я решил продолжить работу <a href="https://github.com/blankhex/bhlib" target="_blank">над своим pet проектом</a>
и хотел бы публиковать на своем сайте всякие заметки и статьи на эту тему.
Мне не хотелось вручную возиться с HTML-файлам, поэтому я решил подыскать
альтернативу в виде какого-нибудь статического генератора сайтов. В идеале, я
хотел бы, чтобы он был:
- Небольшим и достаточно простым
- Мог работать с Markdown
- Мог выполнять подсветку синтаксиса в блоках кода
К сожалению, я не смог найти ни одно подходящее для себя решение, поэтому
я решил собрать свое на коленке используя Python, парсер Markdown'а <a href="https://mistune.lepture.com/en/latest/" target="_blank">mistune</a>,
шаблонизатор <a href="https://jinja.palletsprojects.com/en/stable/" target="_blank">Jinja2</a>
и <a href="https://pygments.org" target="_blank">Pygments</a>. Весь процесс
генерации сводиться к следующему:
1. Для каждого файла во входном каталоге проверяем, является ли он Markdown
- Если да - преобразуем его в HTML (с подсветкой синтаксиса) и записываем
в выходной каталог
- Если нет - копируем файл как есть в выходной каталог
2. Сжимаем содержимое выходного каталога
У данного процесса генерации есть достаточно крупный недостаток - из-за того,
что нет постобработки HTML, любые ссылки на другие страницы Markdown должны
заканчиваться расширением `.html`[^1].
[^1]: Эту проблему можно устранить с помощью специальной настройки веб-сервера,
которая заменяет в расширение `.md` на `.html` или путем автоматического
дописывания расширения `.html` (к примеру: `try_files $uri $uri.html`).
Сам код генератора:
```python
import re, jinja2, mistune, shutil, os, pathlib, tarfile
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
from pygments import highlight
class PygmentsHTMLRenderer(mistune.HTMLRenderer):
def block_code(self, code: str, info = None):
if not info:
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code)
lexer = get_lexer_by_name(info, stripall=True)
formatter = HtmlFormatter(lineseparator='<br>')
return highlight(code, lexer, formatter)
def convert_markdown(page: str):
plugins = ['footnotes', 'table', 'strikethrough', 'url']
renderer = PygmentsHTMLRenderer(escape=False)
return mistune.create_markdown(plugins=plugins, renderer=renderer)(page)
def extract_title(page: str):
matches = re.match('<h1>(.*?)</h1>', page)
if matches:
return matches.group(1)
return 'BlankHex'
def handle_file(path: str, input_dir: str, output_dir: str, template_name: str):
# Calculate input and output paths
relpath = os.path.relpath(path, input_dir)
input_path = path
output_path = os.path.join(output_dir, relpath)
if input_path.endswith('.md'):
output_path = output_path.replace('.md', '.html')
# Don't convert if output path exists
if os.path.exists(output_path):
return
# Run conversion
pathlib.Path(os.path.dirname(output_path)).mkdir(parents=True, exist_ok=True)
if input_path.endswith('.md'):
# Read Markdown document
with open(input_path, 'r') as handle:
markdown_page = handle.read()
# Get Pygments styles for light and dark themes
light_style = HtmlFormatter(style='default').get_style_defs()
dark_style = HtmlFormatter(style='monokai').get_style_defs()
# Convert Markdown document to HTML document
html_page = convert_markdown(markdown_page)
html_header = extract_title(html_page)
environment = jinja2.Environment(loader=jinja2.FileSystemLoader('template/'))
template = environment.get_template(template_name)
output_page = template.render(title=html_header,
body=html_page,
light_style=light_style,
dark_style=dark_style)
# Write HTML document
with open(output_path, 'w') as handle:
handle.write(output_page)
else:
# Copy file as is
shutil.copy(path, output_path)
def convert_dir(input_dir: str, output_dir: str, template_name: str):
# Convert or copy every file from the input directory to the output directory
for subdir, dirs, files in os.walk(input_dir):
for file in files:
handle_file(os.path.join(subdir, file), input_dir, output_dir, template_name)
# Remove output from previous run
if os.path.isdir('public'):
shutil.rmtree('public')
if os.path.isfile('public.tgz'):
os.remove('public.tgz')
# Run conversion
convert_dir('content', 'public', 'template.html')
with tarfile.open('public.tgz', 'w:gz') as tar:
for file in os.listdir('public'):
tar.add(os.path.join('public', file), file)
```