diff options
28 files changed, 767 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfd9b9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,256 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Artifacts +*.tgz +public
\ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/SiteGen.iml b/.idea/SiteGen.iml new file mode 100644 index 0000000..f286330 --- /dev/null +++ b/.idea/SiteGen.iml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/.venv" /> + </content> + <orderEntry type="jdk" jdkName="Python 3.9 (SiteGen)" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component>
\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..06ec480 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Black"> + <option name="sdkName" value="Python 3.9 (SiteGen)" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d4faa8a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/SiteGen.iml" filepath="$PROJECT_DIR$/.idea/SiteGen.iml" /> + </modules> + </component> +</project>
\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/content/common/404.md b/content/common/404.md new file mode 100644 index 0000000..05ed756 --- /dev/null +++ b/content/common/404.md @@ -0,0 +1,9 @@ +Sorry, there is no page you're looking for :( + +Check [main page](/en/about) + +--- + +Извините, здесь нет страницы, которую вы ищете :( + +Загляните на [главную страницу](/ru/about) diff --git a/content/common/favicon.ico b/content/common/favicon.ico Binary files differnew file mode 100644 index 0000000..b0bfab5 --- /dev/null +++ b/content/common/favicon.ico diff --git a/content/common/favicon.png b/content/common/favicon.png Binary files differnew file mode 100644 index 0000000..8813264 --- /dev/null +++ b/content/common/favicon.png diff --git a/content/common/images/01-oldsite.png b/content/common/images/01-oldsite.png Binary files differnew file mode 100644 index 0000000..959de40 --- /dev/null +++ b/content/common/images/01-oldsite.png diff --git a/content/common/images/hex.png b/content/common/images/hex.png Binary files differnew file mode 100644 index 0000000..8813264 --- /dev/null +++ b/content/common/images/hex.png diff --git a/content/common/index.md b/content/common/index.md new file mode 100644 index 0000000..41b56c5 --- /dev/null +++ b/content/common/index.md @@ -0,0 +1,3 @@ +[English](en/about) + +[Русский](ru/about) diff --git a/content/common/robots.txt b/content/common/robots.txt new file mode 100644 index 0000000..335b71b --- /dev/null +++ b/content/common/robots.txt @@ -0,0 +1,14 @@ +User-agent: *
+Allow: /
+
+User-agent: GPTBot
+Disallow: /
+
+User-agent: ChatGPT-User
+Disallow: /
+
+User-agent: anthropic-ai
+Disallow: /
+
+User-agent: Google-Extended
+Disallow: /
diff --git a/content/en/about.md b/content/en/about.md new file mode 100644 index 0000000..6291e85 --- /dev/null +++ b/content/en/about.md @@ -0,0 +1,19 @@ +# About + +Graduate of <a rel="noreferrer" href="https://itmo.ru" target="_blank">ITMO University</a> +and <a rel="noreferrer" href="https://www.spbstu.ru" target="_blank">Saint Petersburg State Polytechnic University</a>. + +I like: +- programing in C and C++ +- studying various algorithms +- developing games (usually small prototypes) +- foxes + +## Contacts + +Social Media: +- <a rel="noreferrer" target="_blank" href="https://twitter.com/_blankhex_">X/Twitter</a> (`@_blankhex_`) +- <a rel="noreferrer" target="_blank" href="https://www.reddit.com/user/_blankhex_">Reddit</a> (`u/_blankhex_`) +- <a rel="noreferrer" target="_blank" href="https://github.com/blankhex">GitHub</a> (`blankhex`) + +You can also contact me via email: [me@blankhex.com](mailto:me@blankhex.com)
\ No newline at end of file diff --git a/content/en/blog.md b/content/en/blog.md new file mode 100644 index 0000000..b5363e1 --- /dev/null +++ b/content/en/blog.md @@ -0,0 +1,4 @@ +# Blog + +## 2025 +Apr 16 - [Static site generator in 90 lines of Python code](blog/2025/01-sitegen) diff --git a/content/en/blog/2025/01-sitegen.md b/content/en/blog/2025/01-sitegen.md new file mode 100644 index 0000000..99fb97f --- /dev/null +++ b/content/en/blog/2025/01-sitegen.md @@ -0,0 +1,135 @@ +# Static site generator in 90 lines of Python code + +Created: Apr 16, 2025 + +[На русском](/ru/blog/2025/01-sitegen) + +A long time ago, I made a small business card website. It consisted of three +simple HTML pages, one CSS file (which I generated from SCSS), several fonts +and images. That was more than enough to get a link to my website featured +in a resume or social media profile. + + + + +I recently decided to continue working <a href="https://github.com/blankhex/bhlib" target="_blank">on my pet-project</a> +and would like to publish all sorts of notes and articles on this topic on my +website. I didn't want to manually mess with HTML files, so I decided to look +for an alternative in the form of some kind of static website generator. +Ideally, I would like it to be: + +- Small and simple +- Able to work with Markdown +- Able syntax-highlight blocks of code + +Unfortunately, I couldn't find any suitable solutions for myself, so I decided +to build my own using Python, <a href="https://mistune.lepture.com/en/latest/" target="_blank">mistune</a> +Markdown parser, <a href="https://jinja.palletsprojects.com/en/stable/" target="_blank">Jinja2</a> +template engine, and <a href="https://pygments.org" target="_blank">Pygments</a>. +The whole generation process boils down to the following: + +1. For every file in the input directory check whether it is Markdown + - If yes - convert it to HTML (with highlighting) and write to output directory + - If no - copy as is to output directory +2. Compress content of the output directory + +This generation process has a rather major drawback - due to the fact that +there is no post-processing of HTML, any links to other Markdown pages must +end with a `.html` extension[^1]. + +[^1]: This can be mitigated by special web-server configuration, that replaces + `.md` extension with `.html` or by omitting `.md` extension entirely and + using something like `try_files $uri $uri.html` + +Here is the code: + +```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) +``` + diff --git a/content/en/projects.md b/content/en/projects.md new file mode 100644 index 0000000..3440a80 --- /dev/null +++ b/content/en/projects.md @@ -0,0 +1,3 @@ +# Projects + +As of now this page is empty :(
\ No newline at end of file diff --git a/content/ru/about.md b/content/ru/about.md new file mode 100644 index 0000000..d7e0007 --- /dev/null +++ b/content/ru/about.md @@ -0,0 +1,19 @@ +# Обо мне + +Выпускник университетов <a rel="noreferrer" href="https://itmo.ru" target="_blank">ИТМО</a> +и <a rel="noreferrer" href="https://www.spbstu.ru" target="_blank">СПбПУ</a>. + +Я люблю: +- программирование на C и C++ +- изучение различных алгоритмов +- разработку игр (обычно небольшие прототипы) +- лис + +## Контакты + +Я в соцсетях: +- <a rel="noreferrer" target="_blank" href="https://twitter.com/_blankhex_">X/Twitter</a> (`@_blankhex_`) +- <a rel="noreferrer" target="_blank" href="https://www.reddit.com/user/_blankhex_">Reddit</a> (`u/_blankhex_`) +- <a rel="noreferrer" target="_blank" href="https://github.com/blankhex">GitHub</a> (`blankhex`) + +Также можно воспользоваться электронной почтой [me@blankhex.com](mailto:me@blankhex.com) diff --git a/content/ru/blog.md b/content/ru/blog.md new file mode 100644 index 0000000..b12aece --- /dev/null +++ b/content/ru/blog.md @@ -0,0 +1,4 @@ +# Блог + +## 2025 +16 апреля - [Генератор статических сайтов в 90 строк Python кода](blog/2025/01-sitegen)
\ No newline at end of file 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) +``` diff --git a/content/ru/projects.md b/content/ru/projects.md new file mode 100644 index 0000000..16a61b1 --- /dev/null +++ b/content/ru/projects.md @@ -0,0 +1,3 @@ +# Проекты + +Пока здесь ничего нет :(
\ No newline at end of file @@ -0,0 +1,89 @@ +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 for english, russian and 'common' folder +convert_dir('content/en', 'public/en', 'en.html') +convert_dir('content/ru', 'public/ru', 'ru.html') +convert_dir('content/common', 'public', 'none.html') +with tarfile.open('public.tgz', 'w:gz') as tar: + for file in os.listdir('public'): + tar.add(os.path.join('public', file), file) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7694f3b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Jinja2==3.1.6 +MarkupSafe==3.0.2 +mistune==3.1.3 +Pygments==2.19.1 +typing_extensions==4.13.2 diff --git a/template/en.html b/template/en.html new file mode 100644 index 0000000..0ffc06e --- /dev/null +++ b/template/en.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html lang="en"> +<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"/>{% include 'style.html' %}<link rel="icon" type="image/png" href="/favicon.png"/><title>{{ title }}</title></head> +<body><header><div id="logo">blankhex.com</div><nav><a href="/en/about">About</a><a href="/en/blog">Blog</a><a href="/en/projects">Projects</a></nav></header><article>{{ body }}</article><hr><footer><img style="margin:auto; display:block; max-width: 25%; height: auto;" src="/images/hex.png" alt="outline of an orange hexagon"/></footer></body> +</html> diff --git a/template/none.html b/template/none.html new file mode 100644 index 0000000..fdb646c --- /dev/null +++ b/template/none.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"/>{% include 'style.html' %}<link rel="icon" type="image/png" href="/favicon.png"/></head> +<body><header><div id="logo">blankhex.com</div></header><article>{{ body }}</article><hr></body> +</html> diff --git a/template/ru.html b/template/ru.html new file mode 100644 index 0000000..cc64c9c --- /dev/null +++ b/template/ru.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html lang="ru"> +<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"/>{% include 'style.html' %}<link rel="icon" type="image/png" href="/favicon.png"/><title>{{ title }}</title></head> +<body><header><div id="logo">blankhex.com</div><nav><a href="/ru/about">Обо мне</a><a href="/ru/blog">Блог</a><a href="/ru/projects">Проекты</a></nav></header><article>{{ body }}</article><hr><footer><img style="margin:auto; display:block; max-width: 25%; height: auto;" src="/images/hex.png" alt="очертание оранжевого шестиугольника"/></footer></body> +</html> diff --git a/template/style.html b/template/style.html new file mode 100644 index 0000000..01d4a37 --- /dev/null +++ b/template/style.html @@ -0,0 +1,16 @@ +<style> +body { max-width: 45em; margin: 40px auto; padding: 0 10px; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; color: #444 } +h1, h2, h3 { line-height:1.2 } +header { display: flex; justify-content: space-between; } +header > nav > a { margin-left: 1em; } +pre { font-size: 80%; overflow-x: scroll; } +img { display: block; margin: auto; max-width: 100%; } +#logo { font-family: Georgia, serif; font-weight: bold; } +{{ light_style }} +@media (prefers-color-scheme: dark) { +body { color: #c9d1d9; background: #0d1117 } +a:link { color:#58a6ff } +a:visited { color: #8e96f0 } +{{ dark_style }} +} +</style>
\ No newline at end of file |
