150 lines
6.2 KiB
Python
'''
| |||
File: build_article_html.py
| |||
Description: Собирает из docs/article4-draft.md HTML-фрагмент для визуального
| |||
редактора статей на MQL5.community и отдельные файлы с блоками кода.
| |||
| |||
Запуск из корня проекта:
| |||
python docs/publish/build_article_html.py
| |||
'''
| |||
| |||
import html
| |||
import re
| |||
from pathlib import Path
| |||
| |||
import markdown
| |||
| |||
ROOT = Path(__file__).resolve().parents[2]
| |||
SRC = ROOT / 'docs' / 'article4-draft.md'
| |||
OUT_DIR = ROOT / 'docs' / 'publish'
| |||
CODE_DIR = OUT_DIR / 'code'
| |||
| |||
ARTICLE_URL_RE = re.compile(r'href="https://www\.mql5\.com(/ru/[^"]*)"')
| |||
| |||
# Якорные метки: латиница, цифры и «_», строчные, не начинаются с цифры
| |||
ANCHORS = {
| |||
'Введение': 'intro',
| |||
'Чем не устраивает текущий подход': 'current_approach',
| |||
'Намечаем путь': 'plan',
| |||
'Сервис как тип MQL5-программы': 'service_type',
| |||
'Схема базы данных': 'db_schema',
| |||
'Реализация сервиса ServiceStateWriter.mq5': 'implementation',
| |||
'Что показал первый запуск': 'first_run',
| |||
'Обновление Python-стороны': 'python_side',
| |||
'Совместимость с уже написанным интерфейсом': 'compatibility',
| |||
'Развёртывание через config/services.ini': 'deploy',
| |||
'Тестирование': 'testing',
| |||
'Заключение': 'conclusion',
| |||
}
| |||
| |||
FIGURES = {
| |||
'all-terminals-stopped.png': 1,
| |||
'service-not-running.png': 2,
| |||
'all-terminals-running.png': 3,
| |||
}
| |||
| |||
| |||
def split_code(text: str):
| |||
'''Выносит блоки кода в отдельный список, оставляя в тексте метки.'''
| |||
blocks = []
| |||
| |||
def repl(m):
| |||
blocks.append((m.group(1).strip(), m.group(2).rstrip('\n')))
| |||
return f'\n\n@@CODE{len(blocks) - 1}@@\n\n'
| |||
| |||
text = re.sub(r'```(\w*)\n(.*?)\n```', repl, text, flags=re.S)
| |||
return text, blocks
| |||
| |||
| |||
def figures(text: str) -> str:
| |||
'''Рисунок + подпись -> заглушка для картинки и подпись стилем small.'''
| |||
pattern = re.compile(
| |||
r'!\[[^\]]*\]\(images/([\w.-]+)\)\n\*(Рис\. \d+\..*?)\*')
| |||
| |||
def repl(m):
| |||
name, caption = m.group(1), m.group(2)
| |||
return (
| |||
'\n\n<p style="text-align:center;">[[РИС. %d: файл %s]]</p>\n\n'
| |||
'<p style="text-align:center;"><span class="small">%s</span></p>\n\n'
| |||
% (FIGURES[name], name, caption))
| |||
| |||
return pattern.sub(repl, text)
| |||
| |||
| |||
def build() -> None:
| |||
text = SRC.read_text(encoding='utf-8')
| |||
| |||
# Название статьи на сайте задаётся отдельно, оглавление соберём ниже
| |||
text = text[text.index('## Введение'):]
| |||
| |||
# Блок «Содержание архива» оформляется как жирный абзац, а не заголовок
| |||
text = text.replace('## Содержание архива', '**Содержание архива**')
| |||
| |||
text, blocks = split_code(text)
| |||
text = figures(text)
| |||
| |||
body = markdown.markdown(text, extensions=['tables', 'sane_lists'])
| |||
| |||
# Заголовки: название статьи в h2, разделы в h3, подразделы в h4
| |||
body = body.replace('<h3>', '<h4>').replace('</h3>', '</h4>')
| |||
body = body.replace('<h2>', '<h3>').replace('</h2>', '</h3>')
| |||
| |||
# Инлайн-код -> курсив: редактор сайта не умеет моноширинный текст в строке
| |||
body = re.sub(r'<code>(.*?)</code>', r'<i>\1</i>', body, flags=re.S)
| |||
| |||
# В заголовках форматирования не нужно: они попадают в план статьи
| |||
body = re.sub(
| |||
r'<(h[34])>(.*?)</(h[34])>',
| |||
lambda m: '<%s>%s</%s>' % (
| |||
m.group(1), re.sub(r'</?i>', '', m.group(2)), m.group(3)),
| |||
body)
| |||
body = body.replace('<strong>', '<b>').replace('</strong>', '</b>')
| |||
body = body.replace('<em>', '<i>').replace('</em>', '</i>')
| |||
| |||
# Якоря у заголовков разделов и оглавление со ссылками на них
| |||
toc = []
| |||
| |||
def add_anchor(m):
| |||
title = m.group(1)
| |||
anchor = ANCHORS[title]
| |||
toc.append((anchor, title))
| |||
return '<h3 id="%s">%s</h3>' % (anchor, title)
| |||
| |||
body = re.sub(r'<h3>(.*?)</h3>', add_anchor, body)
| |||
| |||
# Якорные ссылки с путём статьи: короткие вида #id сайт вырезает при сохранении
| |||
items = '\n'.join(
| |||
'<li><a href="/ru/articles/20150/#%s">%s</a></li>' % t for t in toc)
| |||
body = '<p><b>Содержание</b></p>\n<ul>\n%s\n</ul>\n' % items + body
| |||
| |||
# Ссылки: на сайте MQL5 относительные, все открываются в новой вкладке
| |||
body = ARTICLE_URL_RE.sub(r'href="\1"', body)
| |||
body = re.sub(r'<a href="((?!/ru/articles/20150/#)[^"]*)">',
| |||
r'<a href="\1" target="_blank">', body)
| |||
| |||
# Предупреждение
| |||
body = re.sub(r'<blockquote>\s*<p><b>Предупреждение\.</b>',
| |||
'<div class="atten">\n<p><b>Предупреждение</b>.', body)
| |||
body = body.replace('</blockquote>', '</div>')
| |||
| |||
# Таблицы в стандартном стиле сайта
| |||
body = body.replace(
| |||
'<table>',
| |||
'<table class="standart" width="100%" cellspacing="0" cellpadding="3">')
| |||
| |||
# Блоки кода
| |||
CODE_DIR.mkdir(parents=True, exist_ok=True)
| |||
for old in CODE_DIR.glob('*.txt'):
| |||
old.unlink()
| |||
for i, (lang, code) in enumerate(blocks):
| |||
block = ('<div class="code-box"><pre class="code">%s</pre></div>'
| |||
% html.escape(code, quote=False))
| |||
body = body.replace(f'<p>@@CODE{i}@@</p>', block)
| |||
(CODE_DIR / f'{i + 1:02d}_{lang or "text"}.txt').write_text(
| |||
code + '\n', encoding='utf-8')
| |||
| |||
(OUT_DIR / 'article4.html').write_text(body + '\n', encoding='utf-8')
| |||
print(f'article4.html: {len(body)} символов, блоков кода: {len(blocks)}')
| |||
| |||
| |||
if __name__ == '__main__':
| |||
build()
|