Различные статьи по Qt и c++

Markdown → Qt Help: пайплайн через pandoc/MkDocs вместо ручного HTML

Введение

Писать справку напрямую в HTML утомительно для авторов контента, не являющихся разработчиками — теги, экранирование символов, ручная синхронизация навигации отвлекают от содержания. Markdown — гораздо более дружелюбный формат для авторов, и существующие инструменты (pandoc, MkDocs) умеют конвертировать его в HTML, который затем можно обернуть в .qhp и скомпилировать в .qch, как мы делали в начале цикла. В статье построим пайплайн от исходников в Markdown до готового .qch.

Концепция

Markdown-файлы хранят содержательную часть документации в простом текстовом формате с минимальной разметкой (заголовки через #, списки через -, ссылки через [текст](url)). Pandoc конвертирует каждый .md-файл в самостоятельный HTML-файл с предсказуемой структурой заголовков, которую затем можно использовать для автоматической генерации TOC — заголовки #/## естественно превращаются в иерархию разделов .qhp. Генерация .qhp-файла на основе структуры Markdown-документов автоматизируется отдельным скриптом, который сканирует директорию с исходниками, конвертирует каждый файл и собирает итоговый .qhp на основе их заголовков.

Пример кода: Markdown → Qt Help

<!-- docs/markdown/getting_started.md -->
# Начало работы

## Системные требования

Приложение работает на Windows 10+, macOS 12+ и большинстве дистрибутивов Linux.

## Установка

Скачайте установщик с официального сайта и запустите его.

## Первый запуск

При первом запуске будет предложено настроить базовые параметры.
#!/usr/bin/env bash
# build_help_from_markdown.sh — конвертация Markdown в HTML и сборка .qch

set -e

SOURCE_DIR="docs/markdown"
OUTPUT_DIR="docs/html"
mkdir -p "$OUTPUT_DIR"

echo "Конвертация Markdown в HTML через pandoc..."
for mdfile in "$SOURCE_DIR"/*.md; do
    basename=$(basename "$mdfile" .md)
    pandoc "$mdfile" \
        --standalone \
        --css="../style.css" \
        --metadata title="$basename" \
        -o "$OUTPUT_DIR/$basename.html"
done

echo "Генерация .qhp на основе сконвертированных файлов..."
python3 generate_qhp.py "$OUTPUT_DIR" > docs/myapp.qhp

echo "Компиляция .qch..."
qhelpgenerator docs/myapp.qhp -o docs/myapp.qch

echo "Готово: docs/myapp.qch"
# generate_qhp.py — упрощённый генератор .qhp на основе HTML-заголовков
import sys
import os
from html.parser import HTMLParser

class HeadingExtractor(HTMLParser):
    def __init__(self):
        super().__init__()
        self.headings = []
        self.current_tag = None

    def handle_starttag(self, tag, attrs):
        if tag in ("h1", "h2", "h3"):
            self.current_tag = tag
            attr_dict = dict(attrs)
            self.headings.append({"tag": tag, "id": attr_dict.get("id", ""), "text": ""})

    def handle_data(self, data):
        if self.current_tag and self.headings:
            self.headings[-1]["text"] += data

    def handle_endtag(self, tag):
        self.current_tag = None

def main(html_dir):
    print('<?xml version="1.0" encoding="UTF-8"?>')
    print('<QtHelpProject version="1.0">')
    print('    <namespace>com.mycompany.myapp.1.0</namespace>')
    print('    <virtualFolder>doc</virtualFolder>')
    print('    <filterSection>')
    print('        <toc>')

    files = []
    for filename in sorted(os.listdir(html_dir)):
        if not filename.endswith(".html"):
            continue
        files.append(filename)
        with open(os.path.join(html_dir, filename), encoding="utf-8") as f:
            parser = HeadingExtractor()
            parser.feed(f.read())

        for heading in parser.headings:
            ref = f"html/{filename}"
            if heading["id"]:
                ref += f"#{heading['id']}"
            indent = "    " * (int(heading["tag"][1]))
            print(f'{indent}<section title="{heading["text"]}" ref="{ref}" />')

    print('        </toc>')
    print('        <files>')
    for filename in files:
        print(f'            <file>html/{filename}</file>')
    print('        </files>')
    print('    </filterSection>')
    print('</QtHelpProject>')

if __name__ == "__main__":
    main(sys.argv[1])

Пояснения к коду

Скрипт build_help_from_markdown.sh сначала конвертирует каждый Markdown-файл в самостоятельный HTML-документ через pandoc --standalone, подключая общий CSS-файл для единообразного оформления всех страниц — флаг --standalone важен, поскольку без него pandoc вывел бы только фрагмент <body> без обёртки <html>/<head>, которая нужна для самостоятельного HTML-файла, готового к включению в .qch.

generate_qhp.py — упрощённый, но рабочий генератор: он разбирает HTML-заголовки (h1/h2/h3) каждого сконвертированного файла через стандартный html.parser и строит TOC, отражающий вложенность заголовков через отступ — реальный продакшен-инструмент обычно сложнее (поддержка нескольких уровней вложенности директорий, явных метаданных порядка страниц), но базовый принцип — автоматическое построение навигации из структуры контента — продемонстрирован полностью.

Подводные камни

  • Pandoc не добавляет id к заголовкам автоматически без явной настройки. Без атрибутов id у <h2>/<h3> ссылки на конкретные разделы внутри страницы (а не только на страницу целиком) не сработают — нужно использовать расширение pandoc --id-prefix или явно прописывать идентификаторы в исходном Markdown через синтаксис ## Заголовок {#my-id} (поддерживается с расширением header_attributes).
  • Порядок страниц в TOC, определяемый алфавитной сортировкой имён файлов, редко совпадает с желаемым логическим порядком изложения материала (например, «Введение» должно идти раньше «Установки», но алфавитно может оказаться после) — реальный пайплайн должен либо использовать явный список порядка страниц в конфигурационном файле, либо префиксы с номерами в именах файлов (01_intro.md, 02_install.md).
  • Markdown-расширения (таблицы, сноски, синтаксис кода с подсветкой), специфичные для конкретного диалекта Markdown, могут конвертироваться pandoc по-разному в зависимости от указанного формата исходного текста (--from gfm для GitHub-стиля, --from markdown для стандартного Pandoc Markdown) — несогласованность между диалектом, в котором фактически пишут авторы, и диалектом, который ожидает команда конвертации, приводит к неожиданным артефактам в итоговом HTML.
  • Отсутствие проверки на ошибки конвертации pandoc для отдельных файлов в цикле bash-скрипта. Если один Markdown-файл содержит синтаксическую проблему, вызывающую сбой pandoc для этого конкретного файла, скрипт (без set -e, реагирующего только на общий код возврата, но не гарантирующего диагностику конкретного файла) может продолжить выполнение с пропущенным файлом, что обнаружится только при последующей проверке итоговой справки на полноту контента.