diff options
Diffstat (limited to 'content/ru/blog/2025/01-sitegen.md')
| -rw-r--r-- | content/ru/blog/2025/01-sitegen.md | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/content/ru/blog/2025/01-sitegen.md b/content/ru/blog/2025/01-sitegen.md new file mode 100644 index 0000000..76b7353 --- /dev/null +++ b/content/ru/blog/2025/01-sitegen.md @@ -0,0 +1,134 @@ +# Генератор статических сайтов в 90 строк Python кода + +Создано: 16 апреля 2025 + +[In English](/en/blog/2025/01-sitegen) + +Давным-давно я сделал небольшой сайт-визитку - он состоял из трех простеньких +HTML-страниц, одного CSS-файла (который я генерировал из SCSS), нескольких +шрифтов и картинок. Этого было более чем достаточно, чтобы ссылка на мой +сайт красовалась в каком-либо резюме или профиле соцсети. + + + +Недавно я решил продолжить работу <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) +``` |
