'''Готовит пятую статью для редактора 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('

', '

').replace('

', '') body = body.replace('

', '

').replace('

', '') body = re.sub(r'(.*?)', r'\1', body, flags=re.S) body = body.replace('', '').replace('', '') body = body.replace('', '').replace('', '') toc = [] def heading(match): title = re.sub(r'', '', match.group(1)) anchor = ANCHORS[title] toc.append((title, anchor)) return f'

{title}

' body = re.sub(r'

(.*?)

', heading, body) if len(toc) != len(ANCHORS): raise RuntimeError('Список разделов не соответствует настройкам публикации.') contents = '

Содержание

\n\n' body = contents + body # В редакторе MQL5 отступ перед разделом задаём отдельным пустым абзацем. body = re.sub(r'( 

\n\1', body) body = body.replace('

Содержание архива

', '

 

\n

Содержание архива

') body = body.replace('', '
') body = re.sub(r'href="https://www\.mql5\.com(/ru/[^\"]*)"', r'href="\1"', body) body = re.sub(r'', r'', 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()