2026-11-01 22:40:20 +03:00 | | | '''Готовит пятую статью для редактора 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',
|
2026-11-02 18:42:23 +03:00 | | | 'Первоначальная установка': 'initial_setup',
|
2026-11-01 22:40:20 +03:00 | | | 'Получаем новую копию терминала': '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')
|
| | |
|
2026-11-02 18:42:23 +03:00 | | | text = re.sub(r'!\[[^\]]*\]\(images/((?:previews/)?[\w.-]+)\)\n\*(Рис\. \d+\..*?)\*', replace_image, text)
|
2026-11-01 22:40:20 +03:00 | | | 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()
|