mt5-manager/docs/articles/part-05/publish/build_article_html.py
137 lines
7.1 KiB
Python
'''Готовит пятую статью для редактора MQL5: HTML, листинги и описание.
Код и изображения заменяются маркерами. На сайте их нужно вставлять через
мастера «Код» и «Картинка», а не вручную размеченными блоками HTML.
Запуск: python docs/articles/part-05/publish/build_article_html.py
Дополнительная зависимость только для публикации: Markdown.
'''
import json
import re
from pathlib import Path
import markdown
HERE = Path(__file__).resolve().parent
ARTICLE = HERE.parent / 'draft.md'
ANCHORS = {
'Введение': 'intro',
'Намечаем путь': 'plan',
'Первоначальная установка': 'initial_setup',
'Получаем новую копию терминала': 'terminal_copy',
'Подключаемся к торговому серверу': 'broker_connection',
'Сохраняем параметры счёта': 'account_settings',
'Подготавливаем исходные файлы': 'source_files',
'Сохраняем конфигурацию': 'configuration',
'Создаём экземпляр': 'create_instance',
'Выбираем счёт при запуске': 'account_login',
'Добавляем веб-интерфейс': 'web_interface',
'Обновляем проект и сервис': 'update_project',
'Тестирование': 'testing',
'Заключение': 'conclusion',
}
DESCRIPTION = ('Добавляем создание новых копий MetaTrader 5 через веб-интерфейс: '
'подготавливаем файлы, настраиваем первый вход на торговый счёт и сохраняем '
'параметры для последующих запусков. Реализуем обновление проекта через Git, '
'сборку и распространение сервиса по терминалам.')
def image_size(path):
data = path.read_bytes()
if data.startswith(b'\x89PNG\r\n\x1a\n') and data[12:16] == b'IHDR':
return int.from_bytes(data[16:20], 'big'), int.from_bytes(data[20:24], 'big')
if not data.startswith(b'\xff\xd8'):
raise ValueError(f'Ожидается снимок в PNG или JPEG: {path.name}')
offset = 2
while offset + 8 < len(data):
if data[offset] != 0xff:
offset += 1
continue
marker = data[offset + 1]
offset += 2
if marker in (0xd8, 0xd9):
continue
size = int.from_bytes(data[offset:offset + 2], 'big')
if marker in (0xc0, 0xc1, 0xc2, 0xc3):
height = int.from_bytes(data[offset + 3:offset + 5], 'big')
width = int.from_bytes(data[offset + 5:offset + 7], 'big')
return width, height
if size < 2:
break
offset += size
raise ValueError(f'Не удалось определить размер снимка: {path.name}')
def build():
text = ARTICLE.read_text(encoding='utf-8')
text = text[text.index('## Введение'):]
text = text.replace('## Содержание архива', '**Содержание архива**')
code, images = [], []
code_dir = HERE / 'code'
code_dir.mkdir(parents=True, exist_ok=True)
def replace_code(match):
number = len(code) + 1
language, content = match.group(1), match.group(2)
name = f'{number:02d}_{language or "text"}.txt'
(code_dir / name).write_text(content.rstrip('\n') + '\n', encoding='utf-8', newline='\n')
code.append({'number': number, 'language': language, 'file': 'code/' + name,
'marker': f'ARTICLE5_CODE_{number:02d}'})
return '\n\n' + code[-1]['marker'] + '\n\n'
text = re.sub(r'```(\w*)\n(.*?)\n```', replace_code, text, flags=re.S)
def replace_image(match):
number = len(images) + 1
image, caption = match.group(1), match.group(2)
path = HERE.parent / 'images' / image
if not path.is_file():
raise FileNotFoundError(path)
width, height = image_size(path)
if width != 750:
raise ValueError(f'{image}: ширина {width} вместо 750 пикселей')
marker = f'ARTICLE5_IMAGE_{number:02d}'
images.append({'number': number, 'file': '../images/' + image,
'caption': caption, 'marker': marker, 'width': width, 'height': height})
return ('\n\n<p style="text-align:center;">' + marker + '</p>\n\n'
'<p style="text-align:center;"><span class="small">' + caption + '</span></p>\n\n')
text = re.sub(r'!\[[^\]]*\]\(images/((?:previews/)?[\w.-]+)\)\n\*(Рис\. \d+\..*?)\*', replace_image, text)
body = markdown.markdown(text, extensions=['tables', 'sane_lists'])
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 = body.replace('<strong>', '<b>').replace('</strong>', '</b>')
body = body.replace('<em>', '<i>').replace('</em>', '</i>')
toc = []
def heading(match):
title = re.sub(r'</?i>', '', match.group(1))
anchor = ANCHORS[title]
toc.append((title, anchor))
return f'<h3 id="{anchor}">{title}</h3>'
body = re.sub(r'<h3>(.*?)</h3>', heading, body)
if len(toc) != len(ANCHORS):
raise RuntimeError('Список разделов не соответствует настройкам публикации.')
contents = '<p><b>Содержание</b></p>\n<ul>\n' + '\n'.join(
f'<li><a href="/ru/articles/23709/#{anchor}">{title}</a></li>' for title, anchor in toc) + '\n</ul>\n'
body = contents + body
# В редакторе MQL5 отступ перед разделом задаём отдельным пустым абзацем.
body = re.sub(r'(<h[34]\b)', r'<p>&nbsp;</p>\n\1', body)
body = body.replace('<p><b>Содержание архива</b></p>',
'<p>&nbsp;</p>\n<p><b>Содержание архива</b></p>')
body = body.replace('<table>', '<table class="standart" width="100%" cellspacing="0" cellpadding="3">')
body = re.sub(r'href="https://www\.mql5\.com(/ru/[^\"]*)"', r'href="\1"', body)
body = re.sub(r'<a href="((?!/ru/articles/23709/#)[^\"]*)">', r'<a href="\1" target="_blank">', body)
HERE.mkdir(parents=True, exist_ok=True)
(HERE / 'article5.html').write_text(body + '\n', encoding='utf-8', newline='\n')
(HERE / 'description.txt').write_text(DESCRIPTION + '\n', encoding='utf-8', newline='\n')
(HERE / 'publication-manifest.json').write_text(json.dumps(
{'article_id': 23709, 'code': code, 'images': images, 'toc': toc},
ensure_ascii=False, indent=2) + '\n', encoding='utf-8', newline='\n')
print(f'Article 5: {len(toc)} headings, {len(code)} code blocks, {len(images)} images.')
if __name__ == '__main__':
build()