forked from antekov/mt5-manager
- реализовать создание portable-экземпляров и настройку счетов - обновить интерфейс и добавить обновление проекта через Git - добавить сборку и распространение сервиса - организовать материалы статей и описать актуальную версию - запрашивать тестовый пароль интерактивно и расширить проверки
'''
| |
File: build_article_html.py
| |
Description: Собирает из docs/articles/part-04/article.md HTML-фрагмент для визуального
| |
редактора статей на MQL5.community и отдельные файлы с блоками кода.
| |
| |
Запуск из корня проекта:
| |
python docs/articles/part-04/publish/build_article_html.py
| |
'''
| |
| |
import html
| |
import re
| |
from pathlib import Path
| |
| |
import markdown
| |
| |
ARTICLE_DIR = Path(__file__).resolve().parents[1]
| |
SRC = ARTICLE_DIR / 'article.md'
| |
OUT_DIR = ARTICLE_DIR / '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()
|