'''Готовит пятую статью для редактора 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
' + marker + '
\n\n' '' + caption + '
\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('(.*?)', r'\1', body, flags=re.S)
body = body.replace('', '').replace('', '')
body = body.replace('', '').replace('', '')
toc = []
def heading(match):
title = re.sub(r'?i>', '', match.group(1))
anchor = ANCHORS[title]
toc.append((title, anchor))
return f'Содержание
\nСодержание архива
', '\n
Содержание архива
') body = body.replace('