Текст книг, учебных пособий, научно-технических статей, документации, дипломных и курсовых работ часто набирается и редактируется в WYSIWYG-редакторе, таком как Microsoft Word, в том числе вследствие того, что издательства и организации требуют от авторов оформленный по ГОСТ или внутренним стандартам docx-документ. Процесс работы в Microsoft Word и аналогичных редакторах не лишён недостатков: docx-файлы трудно версионировать в git, а для объединения нескольких документов в один придётся перенумеровывать источники, рисунки, таблицы, формулы.

Альтернативой docx является LaTeX. Однако работа со стилями в LaTeX простотой и минималистичным синтаксисом не отличается, причём издательства от использования формата docx отказываться не торопятся. А инструменты в духе typst отличаются нестандартным синтаксисом языка для описания документов, причём возможность генерации сайтов в typst имеет пометку «in preview».

Markdown — популярный и удобный язык разметки, но это также и очень ограниченный формат. Поэтому задача написания в Markdown сложной технической документации по ГОСТ, научной статьи с автоматической настройкой оформления для заданного издательства или хорошо оформленного онлайн-учебника может показаться неосуществимой. В этой статье рассмотрим способ работы над научно-техническими статьями и книгами в формате Markdown на основе Docs as Code с учётом строгих ограничений на оформление, используемый @true-grue и мной при подготовке учебных материалов в РТУ МИРЭА.

image
Наш способ организации сборки научно-технических статей и учебных пособий по Docs as Code

Способ заключается в применении утилиты pandoc для построения дерева абстрактного синтаксиса (AST) документа с последующим переписыванием AST набором фильтров на Lua и трансляцией AST в форматы docx и pdf, соответствующие ГОСТ, а также в диалект markdown, совместимый с mdBook, для генерации онлайн-учебника.

Онлайн-версии книг, написанных с использованием описанного подхода, и репозитории с исходным кодом книг опубликованы на GitHub и GitHub Pages: книга по конфигурационному управлению, книга по разработке кроссплатформенных программмных систем.

1. Сборка простой книги

Для начала попробуем написать миниатюрную книгу в формате Markdown и транслировать исходный код книги в формат docx. Создадим новую папку для нашей книги или даже новый git-репозиторий — поскольку мы будем использовать текстовые форматы для вёрстки книги и конфигурации задействованных инструментов, получаемые результаты будет легко версионировать. Настройку проекта начнём с добавления в папку проекта книги нового файла default.yaml с базовыми настройками pandoc.

Значение параметра bibliography в файле default.yaml — это имя файла, содержащего литературные источники. Опция link-citations делает ссылки на литературные источники интерактивными — при нажатии на интерактивную ссылку в docx или pdf читатель автоматически перемещается к цитируемому источнику в списке литературы. Параметр lang позволяет указать язык документа, а значение параметра csl в файле default.yaml — это имя файла с правилами оформления цитируемых источников:

default.yaml

metadata:
  bibliography: bibliography.bib
  link-citations: true
  csl: gost-r-7-0-5-2008-numeric-iaa.csl
  lang: ru

gost-r-7-0-5-2008-numeric-iaa.csl

В нашей книге будет использоваться стандартное оформление списка литературы, соответствующее ГОСТ Р 7.0.5-2008, csl-файл gost-r-7-0-5-2008-numeric-iaa.csl позаимствуем из проекта GOSTdown, сохраним его в папку с нашей книгой.

Добавим также файл bibliography.bib, содержащий литературные источники в формате BibTeX. Такие сервисы, как Google Scholar и Истина, поддерживают не только поиск актуальных научных работ на заданную тему, но и автоматическое формирование ссылок на цитируемые научные статьи, книги и расшифровки конференций в формате BibTeX — поэтому целесообразно использовать именно этот формат для цитирования источников:

bibliography.bib

@inproceedings{levenshtein,
    title={Двоичные коды с исправлением выпадений, вставок и замещений символов},
    author={Левенштейн, Владимир Иосифович},
    booktitle={Доклады Академии наук},
    volume={163},
    number={4},
    pages={845--848},
    year={1965},
    organization={Российская академия наук},
    url="http://mi.mathnet.ru/dan31411"
}

Теперь создадим файл book.md с текстом нашей книги в формате Markdown. Текст книги будет содержать заголовок первого уровня, ссылку на источник в списке литературы @levenshtein, содержащийся в файле bibliography.bib, а также многострочную формулу на языке описания математических формул, используемом в системе компьютерной вёрстки LaTeX:

book.md

# Расстояние Левенштейна

Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:

$$
f(a, b, i, j) = \begin{cases}
  \max(i, j), & i = 0 \lor j = 0, \\
  f(a, b, i-1, j-1), & a_i = b_j, \\
  1 + \min \left( \begin{array}{}
    f(a, b, i, j-1), \\
    f(a, b, i-1, j), \\
    f(a, b, i-1, j-1)
  \end{array} \right), & a_i \neq b_j.
\end{cases}
$$

# Список литературы

Попробуем скомпилировать нашу маленькую книгу, состоящую только из одного раздела, в файл в формате DOCX для редактора Microsoft Word. Воспользуемся pandoc — универсальным преобразователем документов, написанным на Haskell. Портативную версию утилиты pandoc в виде zip-архива с файлом pandoc.exe внутри можно скачать в разделе Releases на GitHub. В папке с исходным кодом нашей книги создадим подпапку tools и поместим в неё файл pandoc.exe. После чего выполним сборку книги, запустив pandoc со следующими параметрами:

tools/pandoc.exe book.md -d default.yaml --citeproc --from=markdown --to=docx --output=book.docx

В результате трансляции книги из формата Markdown в формат docx был создан документ book.docx с автоматически сформированным списком литературы, соответствующим ГОСТ Р 7.0.5-2008, с автоматически пронумерованными интерактивными ссылками на источники в тексте, а также с формулой в формате Office Math Markup Language (OMML), совместимом с редактором уравнений в Microsoft Word:

Результат сборки Markdown-документа в docx-файл с настройками по умолчанию
Результат сборки Markdown-документа в docx-файл с настройками по умолчанию

2. Нумерация формул и стили текста

Однако, текст скомпилированного на предыдущем шаге документа book.docx пока не соответствует принятым стандартам оформления, таким как, например, ГОСТ 7.32 2017. В частности, в book.docx используется шрифт без засечек, цветные заголовки, одинарный межстрочный интервал. Кроме того, формула не имеет номера, из-за чего ссылаться на неё в тексте затруднительно.

Фильтр pandoc-crossref добавляет в pandoc поддержку автоматической нумерации не только источников из списка литературы, но и рисунков, формул, таблиц, разделов. Портативную версию утилиты pandoc-crossref можно скачать в разделе Releases на GitHub. Поместим скачанный файл pandoc-crossref.exe в папку tools внутри папки с исходным кодом нашей книги — так фильтр станет доступен по относительному пути tools/pandoc-crossref.exe. Создадим новый файл crossref-docx.yaml, в этом файле будут находиться настройки инструмента pandoc-crossref, специализированные для Microsoft Word, такие как префиксы и шаблоны полных и сокращённых названий рисунков, таблиц, формул:

crossref-docx.yaml

figureTitle: Рисунок
figPrefix: рис.
titleDelim: .
tableTitle: Таблица
tblPrefix: табл.
numberSections: true
sectionsDepth: 3
secHeaderDelim: . 
tableEqns: true
eqnPrefixTemplate: ($$i$$)
eqnBlockInlineMath: true
eqnBlockTemplate: |
  `
  <w:pPr>
    <w:tabs>
      <w:tab w:val="center" w:leader="none" w:pos="4680" />
      <w:tab w:val="right" w:leader="none" w:pos="9960" />
    </w:tabs>
  </w:pPr>
  <w:r>
    <w:tab />
  </w:r>
  `{=openxml} $$t$$
  `
  <w:r>
    <w:tab />
  </w:r>
  `{=openxml} $$i$$

Параметр eqnBlockTemplate задаёт шаблон отображения формул в документе Microsoft Word в формате OOXML (Office Open XML, стандарт ECMA-376) — в приведённой конфигурации сама формула выравнивается по центру страницы, а номер формулы — по правому краю страницы.

template.docx

На следующем шаге добавим в папку с книгой файл-шаблон template.docx с настройками стилей текста. Файл-шаблон со стилями, настроенными для соответствия ГОСТ и требованиям издательства, с которым мы сотрудничали, можно найти в нашем репозитории на GitHub, скачать template.docx и поместить в папку с Вашей книгой. Стили текста в Microsoft Word настраиваются вручную, при помощи специального графического интерфейса, доступного в меню "Стили текста" на вкладке "Главная", это меню также можно открыть сочетанием клавиш Ctrl+Shift+Alt+S. В template.docx можно настроить и колонтитулы с нумерацией страниц, их унаследует docx-файл при сборке. Настройка стилей — кропотливая ��абота, но эту настройку достаточно провести один раз, после чего можно переиспользовать файл-шаблон в разных проектах без изменений.

Важно отметить, что стили Microsoft Word, определённые в файле template.docx, позволяют настроить только внешний вид блоков текста разных типов, но не изменить сам текст. Однако, в некоторых стандартах и организациях требуется, например, набирать заголовки разделов заглавными буквами. Для преобразования текста заголовков разделов в верхний регистр напишем на языке Lua фильтр upper.lua, используя API pandoc, позволяющее переписывать дерево абстрактного синтаксиса (AST) документа в процессе его трансляции из исходного формата в целевой. Код фильтра upper.lua поместим в папку tools. Этот фильтр выполнит обход AST и преобразует текст Str в заголовках первого уровня Header (# Пример) в верхний регистр при помощи функции upper из модуля pandoc.text:

tools/upper.lua

local text = require('text')

function Header(el)
    if el.level == 1 then
        return pandoc.walk_block(el, {
            Str = function(el)
                return pandoc.Str(text.upper(el.text))
            end
        })
    end
end

Каждый заголовок первого уровня располагается в начале нового раздела, а новый раздел, в свою очередь, должен начинаться с новой страницы. Вследствие этого в процессе обхода AST pandoc в наборах блоков Blocks, из которых состоит текст книги, перед каждым заголовком первого уровня будем помещать разрыв страницы, обозначаемый как \newpage. Поместим следующий фрагмент кода на Lua в созданный ранее файл tools/upper.lua:

function Blocks(blocks)
    local hblocks = {}
    for i, el in pairs(blocks) do
        if el.t == "Header" and el.level == 1 then
            table.insert(hblocks, pandoc.RawBlock("tex", "\\newpage"))
        end
        table.insert(hblocks, el)
    end
    return hblocks
end

При помощи стороннего фильтра pagebreak.lua, который также необходимо поместить в папку tools, команда \newpage будет автоматически заменена на следующую команду в формате OOXML при сборке книги в формат Microsoft Word: <w:p><w:r><w:br w:type="page"/></w:r></w:p>.

Обновим содержимое md-файла с нашей книгой book.md — теперь мы можем добавить необходимые пояснения к формуле, сославшись на неё в тексте. С новыми настройками разделы в книге будут автоматически нумероваться, для отключения нумерации заголовок явно помечается последовательностью символов {-}:

book.md

# Расстояние Левенштейна

Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:

$$
f(a, b, i, j) = \begin{cases}
  \max(i, j), & i = 0 \lor j = 0, \\
  f(a, b, i-1, j-1), & a_i = b_j, \\
  1 + \min \left( \begin{array}{}
    f(a, b, i, j-1), \\
    f(a, b, i-1, j), \\
    f(a, b, i-1, j-1)
  \end{array} \right), & a_i \neq b_j.
\end{cases}
$$ {#eq:leven}

В формуле @eq:leven $a$ и $b$ – это сравниваемые последовательности символов,
$i$ – это номер символа в строке $a$, $j$ – номер символа в строке $b$.

# Список литературы {-}

Скомпилируем нашу обновлённую книгу:

tools/pandoc.exe book.md -d default.yaml `
    --metadata crossrefYaml=crossref-docx.yaml `
    --filter tools/pandoc-crossref.exe `
    --lua-filter tools/upper.lua `
    --lua-filter tools/pagebreak.lua `
    --citeproc `
    --reference-doc=template.docx `
    --from=markdown `
    --to=docx `
    --output=book.docx

При необходимости можно также на основе заголовков первого (# Пример) и второго (## Пример) уровней автоматически сгенерировать оглавление, добавив опцию --toc при сборке docx-документа. Обновлённый файл book.docx теперь выглядит так:

image
Результат сборки Markdown-документа в docx-файл с оформлением по ГОСТ

3. Изображения как код

Традиционный подход к встраиванию изображений в Markdown — сохранить картинку рядом с md-файлом и поместить в md-файл ссылку на изображение, используя синтаксис ![описание_изображения](путь_к_изображению). Однако, с таким подходом для внесения изменений в изображение потребуется графический редактор. Кроме того, с таким подходом к редактированию изображений затруднительно их версионировать в системе контроля версий git, а результат работы утилиты git diff оказывается не информативным.

Вследствие этого многие сервисы, поддерживающие онлайн-редактирование и просмотр Markdown-документов, такие как, например, GitHub, поддерживают подход «изображения как код» (diagrams as code). Для решения той же проблемы в нашем окружении мы сделали фильтр pysvg, позволяющий генерировать схемы и диаграммы в формате SVG при помощи кода на языке Python.

Фильтр pysvg состоит из двух компонентов — непосредственно фильтра pandoc на языке Lua pysvg.lua и библиотеки с вспомогательными функциями pysvg.py. Фильтр pysvg.lua обходит AST документа и заменяет каждый блок кода CodeBlock, помеченный классом .pysvg, на рисунок, содержащий результат генерации SVG-изображения кодом на языке Python, размещённым внутри элемента CodeBlock. Поместим реализацию фильтра pysvg.lua в папку tools:

tools/pysvg.lua

local function diagram_options(cb)
    local attribs = cb.attributes or {}
    local caption = attribs.caption and pandoc.read(attribs.caption).blocks
    local image_attrs = {}
    for attr, value in pairs(attribs) do
        if attr ~= "caption" then image_attrs[attr] = value end
    end
    return {
        alt = caption and pandoc.utils.blocks_to_inlines(caption) or {},
        caption = caption,
        figure_attrs = {id = cb.identifier},
        image_attrs = image_attrs
    }
end

function CodeBlock(el)
    if el.attr.classes[1] == "pysvg" then
        -- Сгенерируем SVG при помощи программы на Python и модуля pysvg.py.
        -- В качестве имени файла используем хэш-значение от его содержимого.
        local header = "from tools.pysvg import *\n"
        local svg = pandoc.pipe("python", {"-"}, header .. el.text)
        local fname = pandoc.sha1(svg) .. ".svg"
        pandoc.mediabag.insert(fname, "image/svg+xml", svg)

        -- Заменим CodeBlock в AST на рисунок Figure при наличии подписи.
        -- При отсутствии подписи разместим рисунок в AST как обычный текст.
        local options = diagram_options(el)
        local image = pandoc.Image(options.alt, fname, "", options.image_attrs)
        local plain = pandoc.Plain {image}
        return options.caption and
            pandoc.Figure(plain, options.caption, options.figure_attrs) or
            plain
    end
end

В функции diagram_options все атрибуты, кроме подписи к рисунку caption, будем считать атрибутами изображения. С таким подходом становится возможным задать блоку кода, помеченному классом .pysvg, такие атрибуты, как width или height, причём значения этих атрибутов унаследует сгенерированное SVG-изображение. В том случае, если описание изображения не задано, CodeBlock в AST будет заменён на картинку без подписи pandoc.Image. При наличии атрибута caption у блока кода в тексте книги будет размещён полноценный рисунок с подписью и заданным пользователем уникальным идентификатором, который pandoc-crossref затем превратит в порядковый номер рисунка.

Библиотека pysvg.py, в свою очередь, представляет из себя модуль с вспомогательными функциями, которые могут использоваться для процедурной генерации изображений в формате SVG кодом на языке Python, размещённым в тексте книги. Так, например, функция dot в приведённом ниже файле pysvg.py в отдельном процессе запускает утилиту командной строки инструмента для визуализации графов graphviz и перенаправляет сгенерированный при помощи graphviz текст в формате SVG в стандартный вывод. Поместим реализацию библиотеки pysvg.py в папку tools:

tools/pysvg.py

import sys
import subprocess

sys.stdout.reconfigure(encoding='utf-8')

def dot(s):
    p = subprocess.run(['dot', '-Tsvg'],
                       input=s.encode('utf8'),
                       capture_output=True)
    print(p.stdout.decode('utf8'))

Аналогичным функции dot образом могут быть реализованы, например, функции mermaid, plantuml, matplot для генерации графов и диаграмм в формате SVG при помощи инструментов Mermaid, PlantUML и matplotlib, однако для задач авторов статьи возможностей визуализатора графов graphviz пока оказалось достаточно.

Обновим исходный код нашей миниатюрной книги, добавим в текст книги пример программы и пример диаграммы на языке описания графов dot, использующемся в graphviz:

# Расстояние Левенштейна

Расстояние Левенштейна @levenshtein может быть определено как минимальное
число операций **замены**, **вставки** и **удаления** символов, необходимых
для преобразования одной последовательности символов в другую:

$$
f(a, b, i, j) = \begin{cases}
  \max(i, j), & i = 0 \lor j = 0, \\
  f(a, b, i-1, j-1), & a_i = b_j, \\
  1 + \min \left( \begin{array}{}
    f(a, b, i, j-1), \\
    f(a, b, i-1, j), \\
    f(a, b, i-1, j-1)
  \end{array} \right), & a_i \neq b_j.
\end{cases}
$$ {#eq:leven}

В формуле @eq:leven $a$ и $b$ – это сравниваемые последовательности символов,
$i$ – это номер символа в строке $a$, $j$ – номер символа в строке $b$.

Реализуем формулу @eq:leven на Python, поместим программу в файл `lev.py`:

```python
def diff(a, b, i, j):
    if i == 0 or j == 0:
        return max(i, j)
    if a[i] == b[j]:
        return diff(a, b, i - 1, j - 1)
    return 1 + min(diff(a, b, i, j - 1),
                   diff(a, b, i - 1, j),
                   diff(a, b, i - 1, j - 1))

a, b = input(), input()
print(diff(a, b, len(a), len(b)))
```

Схема организации ввода-вывода в `lev.py` показана на @fig:levio:

```{#fig:levio .pysvg caption="Ввод-вывод в `lev.py`" width=70%}
dot('''
digraph G {
    edge [arrowhead=none]
    rankdir=LR
    ranksep=0.3
    1 [label="Строки", shape=none]
    2 [label="stdin", shape=rarrow]
    3 [label="lev", shape=circle, fixedsize=shape, style=filled]
    4 [label="stdout", shape=rarrow]
    5 [label="Число", shape=none]
    1 -> 2
    4 -> 5
    subgraph cluster_0 {
        graph [style=dashed]
        2 -> 3
        3 -> 4
    }
}
''')
```

# Список литературы {-}

Скомпилируем книгу, воспользовавшись следующей командой:

tools/pandoc.exe book.md -d default.yaml `
    --metadata crossrefYaml=crossref-docx.yaml `
    --lua-filter tools/pysvg.lua `
    --filter tools/pandoc-crossref.exe `
    --lua-filter tools/upper.lua `
    --lua-filter tools/pagebreak.lua `
    --citeproc `
    --reference-doc=template.docx `
    --from=markdown `
    --to=docx `
    --output=book.docx

Теперь в редакторе Microsoft Word книга выглядит так:

Результат сборки книги в docx-файл с оформлением по ГОСТ и сгенерированными SVG-изображениями
Результат сборки книги в docx-файл с оформлением по ГОСТ и сгенерированными SVG-изображениями

4. Экспорт в формат PDF

Для автоматического преобразования книги в формат pdf из ранее собранного при помощи pandoc файла в формате docx воспользуемся модулем docx2pdf из реестра пакетов Python. Установить этот модуль можно как в виртуальное окружение, так и глобально. Для глобальной установки достаточно воспользоваться следующей командой:

pip install docx2pdf

Создадим утилиту командной строки doc2pdf.py в папке tools, принимающую на вход имя файла в формате docx и расположение файла в формате pdf, который должен быть сформирован нашей утилитой. Для преобразования docx в pdf воспользуемся функцией convert из модуля docx2pdf. Для корректной работы модуля docx2pdf на Windows или macOS должен быть установлен Microsoft Word, docx2pdf преобразует файл в формате docx в формат pdf посредством взаимодействия с API Microsoft Word:

tools/doc2pdf.py

import sys
from docx2pdf import convert

convert(sys.argv[1], sys.argv[2])

Процесс преобразования Markdown-книги в PDF состоит из двух этапов -- сборки docx-документа при помощи pandoc при помощи описанной в предыдущем разделе команды и преобразования docx в pdf при помощи утилиты командной строки tools/doc2pdf.py:

python tools/doc2pdf.py book.docx book.pdf

Получим следующий результат:

Результат сборки Markdown-документа в pdf-файл с оформлением по ГОСТ
Результат сборки Markdown-документа в pdf-файл с оформлением по ГОСТ

5. Сборка статического сайта mdBook

Теперь попробуем превратить нашу книгу в полноценное веб-приложение при помощи генератора статических сайтов с документацией mdBook. Начнём с создания в папке с книгой файла с конфигурацией генератора mdBook:

book.toml

[book]
title = "Книга"
language = "ru"
src = "src"

[build]
build-dir = "build"

[output.html]
mathjax-support = true
no-section-label = true

Опция no-section-label отключает автоматическую нумерацию разделов, в нашей книге разделы пронумерует фильтр pandoc-crossref на этапе сборки книги в формат mdBook. Опция mathjax-support включает поддержку визуализации формул на языке разметки LaTeX в веб-браузерах. Опция src содержит путь к папке с результатом компиляции исходного кода книги при помощи pandoc в формат, совместимый с mdBook, особенности которого мы рассмотрим ниже. А опция build-dir, в свою очередь, содержит путь к папке, в которую mdBook сохранит сгенерированные статические HTML-страницы.

Мы не можем применить генератор mdBook к файлу с исходным кодом нашей книги book.md напрямую, без предварительной обработки, поскольку в mdBook используется другой синтаксис, например, для математических формул, встроенных в текст, кроме того, mdBook ничего не знает о генераторе SVG-изображений pysvg. Поэтому при генерации сайта мы сначала обработаем файл book.md при помощи pandoc, pandoc-crossref и написанных нами фильтров, и только после этого сгенерируем статические HTML-страницы.

Создадим в папке с книгой новый файл с конфигурацией фильтра pandoc-crossref crossref.yaml. Содержимое этого файла идентично содержимому конфигурационного файла crossref-docx.yaml за исключением удалённой строки с определением шаблона формул eqnBlockTemplate в формате OXML для Microsoft Word. Без переопределения параметра eqnBlockTemplate будет использоваться шаблон формул по умолчанию:

crossref.yaml

figureTitle: Рисунок
figPrefix: рис.
titleDelim: .
eqnPrefixTemplate: ($$i$$)
tableTitle: Таблица
tblPrefix: табл.
numberSections: true
sectionsDepth: 3
secHeaderDelim: .&nbsp;
tableEqns: true

Для математических формул, встроенных в текст, в mdBook используется синтаксис \(x\) вместо синтаксиса $x$. Для учёта этой особенности создадим в папке tools новый Lua-фильтр mdbook.lua и добавим правило переписывания вершин AST pandoc, имеющих тип Math:

tools/mdbook.lua

function Math(el)
    if el.mathtype == "InlineMath" then
        return pandoc.Str('\\(' .. el.text .. '\\)')
    end
    return el
end

Каждый источник из списка литературы, помеченный классом csl-entry, представим в виде параграфа с идентификатором <p id="..."></p>, чтобы на параграф можно было сослаться в URL. Кроме того, пустые блоки Div с идентификатором будем удалять из AST, заменяя их на их содержимое. Добавим в файл tools/mdbook.lua правило переписывания вершин AST типа Div:

function Div(el)
    -- Замена источников в списке литературы на параграфы с id.
    -- После этого исправления заработают ссылки на источники
    -- из списка литературы в веб-версии книги.
    if el.classes:includes('csl-entry') then
        local tree = pandoc.Pandoc(el.content[1].content)
        local text = pandoc.write(tree, 'html'):gsub('\n+', ' ')
        local html = '<p id="' .. el.identifier .. '">' .. text .. '</p>'
        return pandoc.RawBlock('html', html)
    end
    -- Раскрытие содержимого блоков Div с идентификатором.
    if el.identifier ~= "" then
        return el.content
    end
    return el
end

Создадим в папке с книгой подпапку src, в неё поместим конфигурационный файл SUMMARY.md с перечнем страниц книги. Поместим в созданный файл src/SUMMARY.md следующее содержимое в формате Markdown:

src/SUMMARY.md

- [Расстояние Левенштейна](page.md)

Скомпилируем книгу в представление в формате Markdown, совместимое с mdBook:

tools/pandoc.exe book.md -d default.yaml `
    --metadata crossrefYaml=crossref.yaml `
    --lua-filter tools/pysvg.lua `
    --filter tools/pandoc-crossref.exe `
    --citeproc `
    --lua-filter=tools/mdbook.lua `
    --extract-media=img `
    --from=markdown `
    --to=markdown-citations-grid_tables-implicit_figures `
    --output=src/page.md

Переместим папку img со сгенерированными и сохранёнными на диск изображениями из book.md в папку src, сгенерированные в процессе трансформации AST pandoc изображения были сохранены в папку img за счёт использования опции утилиты pandoc --extract-media со значением img. После этого поместим в папку tools портативную версию генератора статических сайтов с документацией mdBook, скачать которую можно со страницы Releases на GitHub, и выполним сборку сайта:

mv img src
tools/mdbook.exe serve

Команда serve генератора mdBook выполняет сборку проекта в набор статических HTML-файлов, а также запускает отладочный веб-сервер, доступный по адресу http://localhost:3000. Наша онлайн-книга в браузере теперь выглядит так:

Результат сборки Markdown-документа в виде статического сайта при помощи mdBook
Результат сборки Markdown-документа в виде статического сайта при помощи mdBook

6. Сборочный скрипт

Запоминать команды, использованные нами для сборки книги в виде docx, pdf и статического сайта затруднительно, поэтому полезно воспользоваться всеми преимуществами подхода Docs as Code и написать сценарий сборки для, например, системы сборки GNU make. Создадим в корне папки с исходным кодом книги файл с именем Makefile и следующим содержимым:

Makefile

all: pdf web
docx:
	tools/pandoc.exe book.md -d default.yaml \
		--metadata crossrefYaml=crossref-docx.yaml \
		--lua-filter tools/pysvg.lua \
		--filter tools/pandoc-crossref.exe \
		--lua-filter tools/upper.lua \
		--lua-filter tools/pagebreak.lua \
		--citeproc \
		--reference-doc=template.docx \
		--from=markdown \
		--to=docx \
		--output=book.docx
pdf: docx
	python tools/doc2pdf.py book.docx book.pdf
web:
	tools/pandoc.exe book.md -d default.yaml \
		--metadata crossrefYaml=crossref.yaml \
		--lua-filter tools/pysvg.lua \
		--filter tools/pandoc-crossref.exe \
		--citeproc \
		--lua-filter=tools/mdbook.lua \
		--extract-media=img \
		--from=markdown \
		--to=markdown-citations-grid_tables-implicit_figures \
		--output=src/page.md
	move img src
	tools/mdbook.exe serve

Теперь для сборки книги в требуемый формат достаточно написать в командной строке команду make формат. Например, для сборки docx-файла для сдачи в издательство достаточно воспользоваться командой make docx, а для сборки статического веб-сайта — командой make web. Команда make all, в свою очередь, выполняет сборку книги во всех поддерживаемых форматах, и может использоваться для CI/CD.

Представленный подход, по нашему опыту, упрощает совместную работу над книгами в git-репозиториях и позволяет, прилагая минимум усилий, адаптировать литературу под требования издательства, а также объединять несколько книг в один большой онлайн-учебник для поддержки учебного курса.

Дальнейшее изучение

За рамками статьи остался Python-скрипт mdbook.py, разбивающий большую книгу на разделы-страницы на основе заголовков 2-го уровня (## Пример) и автоматически формирующий файл с оглавлением SUMMARY.md в формате mdBook, а также особенности настройки GitHub Actions для автоматической сборки и публикации онлайн-версии книги. Оставим эти и другие файлы из git-репозитория онлайн-учебника по курсу конфигурационного управления для самостоятельного изучения заинтересованным читателем.

image
Исходный код книги в редакторе Visual Studio Code

Перечислим также альтернативные подходы к подготовке книг и документации:

Спасибо автору идеи представленного в статье способа организации проектов сборки научно-технических статей и учебников по Docs as Code с учётом строгих ограничений на оформление Петру Советову @true-grue за рецензирование и помощь в подготовке заметки.