forked from antekov/mt5-manager
'''Готовит пятую статью для редактора 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> </p>\n\1', body)
| |
body = body.replace('<p><b>Содержание архива</b></p>',
| |
'<p> </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()
|