| '''
|
| 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()
|