/rst2docx

reStructuredText to DOCX filter for Pandoc

Primary LanguageLua

RST2DOCX: пишите текст, генерируйте .docx

Введение

RST2DOCX — фильтр и дополнительные файлы, в сумме позволяющие конвертировать текстовые документы в формате reStructuredText при помощи Pandoc в файлы формата OpenXML, которые читаются MS Word (.docx).

Формат файлов OpenXML указан не случайно: Pandoc создаёт файлы именно этого стандарта, которые отображаются в MS Word как «файлы ограниченной функциональности». MS Word использует расширенную версию XML-схем этого стандарта.

Эти фильтры были написаны мною для того, чтобы было удобно и быстро работать с документацией в моём текстовом редакторе — Neovim, получая на выходе MS Word (в моём случае это были документы с оформлением, соответствующим ГОСТ). По понятным причинам сопоставимо быстрая и комфортная работа с большими документами в MS Word невозможна, хотя определенному улучшению рабочий процесс в MS Word поддаётся (с телеги с квадратными колёсами можно пересесть на колёса круглые, но всё равно это телега).

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

При работе с MS Word меня страшно раздражало, что: текст пунктов меню; текст, вводимый пользователем; названия окон в программном обеспечении — всё это всегда просто выделялось полужирным шрифтом (хорошо если вообще выделялось), как правило без применения каких-либо стилей. reStructuredText позволяет реализовать семантику текста, то есть пометить определенные фрагменты текста ролью, а при помощи этого фильтра на выходе в .docx получить фрагмент текста, помеченный стилем, который можно отредактировать и исправить внешний вид только пунктов меню или только вводимого пользователем текста.

Надеюсь, что этот фильтр поможет перетянуть на свою сторону часть технических писателей, которые сегодня вынуждены работать в MS Word, но хотели бы перейти на другую технологию. При помощи этого фильтра на разных его стадиях было выпущено и сдано заказчикам тысячи полторы страниц документации ГОСТ-овского образца.

Эта система рождалась урывками в ходе работы, и нельзя сказать, чтобы в ней не было недостатков; это стихийно выросшая система, которую сейчас нет ни возможности, ни особого желания исправлять. Текст фильтра на MoonScript местами прокомментирован, а местами — нет. В связи с этим pull requests приветствуются.

При написании текста предполагается начальное знакомство читателя с форматом reStructuredText: читатель должен отличать директиву от роли и от поля (свойства директивы или документа).

Changelog

15.06.2022

Доработана обработка поля :key:: — если раньше нужно было оборачивать каждый элемент сочетания клавиш, то теперь можно написать 🔑`CTRL-ALT-DEL` или 🔑`CTRL+ALT+DEL`, а на выходе будет <CTRL>+<ALT>+<DEL>.

23.01.2023

Добавлено упоминание о том, что фильтр работает только с Pandoc v2.14.

Ограничения и недостатки

К сожалению, из-за несовершенства имеющихся инструментов идеальной конвертации не получится.

MS Word — «плоский» текстовый редактор, не имеющий представления об иерархии и вложенности элементов друг в друга. Все фрагменты текста, включая «вложенные» списки, на деле являются объектами одного уровня, а их «вложенность» — просто особенность примененного к этому фрагменту стиля оформления или настройки просмотра (см. «структурный вид» в MS Word).

Чтобы привести документ reStructuredText (точнее, его представление в Pandoc) к плоскому виду, фильтр обходит абстрактное синтаксическое дерево, получаемое при чтении reStructuredText Pandoc-ом, и устраняет вложенность элементов, приводя их к плоской структуре — вложенные списки становятся плоскими (но со стилями MS Word, симулирующими вложенность), из иллюстраций выдёргиваются подписи (в Pandoc подпись к иллюстрации является её свойством) и из них делаются абзацы, и т. д. и т. п.

Совместимость с версиями Pandoc

Фильтр работает только с Pandoc v.2.14.

В связи с тем, что модели данных Pandoc постоянно меняются, а документация на сайте, насколько я вижу, всегда описывает последнюю версию программы, доработка скрипта под более поздние версии возможна только одним способом — обновление скрипта до актуальной версии Pandoc и её моделей данных. На сегодняшний день это означает заметные усилия по переработке, потому что модели данных изменились сильно.

Таблицы

Часть функциональности reStructuredText Pandoc не поддерживает до сих пор: в первую очередь это касается ячеек таблиц, занимающих более одного ряда или колонки. От таких таблиц придётся отказаться, потому что они не могут быть созданы ни через директиву .. list-table, ни через CSV.

Вариант с ручным редактированием таблиц возможен, но на фоне других проблем, требующих ручного редактирования документа, таблицы я бы оставил в стороне.

Стили инлайн-изображений

Второй недостаток: неспособность Pandoc создавать стили для графических объектов внутри абзаца (то есть не занимающих целый абзац). Такие объекты называются инлайн-изображениями. Это приводит к тому, что если вы хотите видеть иконку в составе текста абзаца, она будет, но вы не сможете её, например, опустить на 2-3 пункта ниже базовой линии шрифта, из-за чего такая иконка будет распирать абзац. Точнее, опустить-то её вы сможете, но только каждую иконку в отдельности, а не через стиль. Понятно, что при сотнях иконок в тексте ручное их редактирование — не вариант.

Этот второй недостаток я решил грязным хаком, переписав несколько строк в исходном коде Pandoc, которые добавляют к инлайн-изображению стиль, который можно определить в фильтре, но до полноценной переработки docx-writer для Pandoc дело не дойдёт ещё долго в связи с состоянием его кодовой базы.

Вот патч этого хака от коммита 5750f60442923780dda91d63dd11b19044462d9. Патч позволяет пробрасывать из Lua в инлайн-изображение стиль, который появится в документе.

diff --git a/src/Text/Pandoc/Writers/Docx.hs b/src/Text/Pandoc/Writers/Docx.hs
index ce7133f33..171ea3c2a 100644
--- a/src/Text/Pandoc/Writers/Docx.hs
+++ b/src/Text/Pandoc/Writers/Docx.hs
@@ -1231,9 +1231,15 @@ inlineToOpenXML' opts (Link _ txt (src,_)) = do
                        M.insert src i extlinks }
               return i
   return [ Elem $ mknode "w:hyperlink" [("r:id",id')] contents ]
-inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do
+inlineToOpenXML' opts (Image attr@(imgident, _, kvs) alt (src, title)) = do
   pageWidth <- asks envPrintWidth
   imgs <- gets stImages
+  inlineImageStyle <- case lookup dynamicStyleKey kvs of
+                           Just (fromString . T.unpack -> styleName) -> do
+                                                cStyleMap <- gets (smCharStyle . stStyleMaps)
+                                                let sty' = getStyleIdFromName styleName cStyleMap
+                                                return sty'
+                           Nothing -> return "none"
   let
   stImage = M.lookup (T.unpack src) imgs
   generateImgElt (ident, _fp, mt, img) = do
@@ -1313,8 +1319,8 @@ inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do
               , spPr
               ]
            ]
-        imgElt = mknode "w:r" [] $
-          mknode "w:drawing" [] $
+        imgElt = mknode "w:r" [] [ mknode "w:rPr" []
+                                   [ mknode "w:rStyle" [("w:val", fromStyleId inlineImageStyle)] ()], mknode "w:drawing" [] $
            mknode "wp:inline" []
               [ mknode "wp:extent" [("cx",tshow xemu),("cy",tshow yemu)] ()
               , mknode "wp:effectExtent"
@@ -1327,6 +1333,7 @@ inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do
               ] ()
               , graphic
               ]
+            ]
      return [Elem imgElt]

   wrapBookmark imgident =<< case stImage of

В коде фильтра это выглядит так:

elseif 'icon' == _exp_0 then
   local roleAttr = pandoc.Attr("", { }, {
      {
      "custom-style",
      "Иконка"
      }
   })

Это означает, что можно объявить алиас в RST:

.. |processes-icon| image:: images\icons\out\processes.png
   :class: icon

При вызове такого алиаса будет вставлено изображение со стилем «Иконка», и этот стиль можно объявить заранее в шаблоне или настроить его непосредственно в документе.

Колонтитулы (рамки ГОСТ)

Третий недостаток: колонтитулы. Pandoc не извлекает колонтитулы из шаблона [1] а значит колонтитул придётся создавать в MS Word самостоятельно. Поскольку колонтитулы оформляются стилем по умолчанию, этот стиль по умолчанию можно переопределить, установив ему выравнивание и размер шрифта, но не более.

[1]Правильнее будет сказать, что сами колонтитулы отсутствуют в модели абстрактного синтаксического дерева Pandoc, и не скоро появятся (если появятся вообще), потому что обновление модели представления документа потребует переработки всех модулей чтения и записи, что крайне затруднительно, учитывая объем кода на Haskell и количество активных участников разработки.

Поскольку содержимое docx-шаблона можно и нужно хранить в репозитории, чтобы обеспечить единообразие оформления, в репозитории можно хранить и содержимое верхних/нижних колонтитулов. Эти файлы (с описанием рамок ГОСТ) можно подгружать в создаваемый .docx, просто распаковывая документ (силами PowerShell и сборщика Invoke-Build это можно исполнить прямо в оперативной памяти) и подменяя уже существующие файлы. Чтобы колонтитул отображался, можно либо обрабатывать содержимое XML-документа силами того же PowerShell, устанавливая в нём ссылки на тот или иной колонтитул для раздела, либо предусмотреть директиву reStructuredText и функцию фильтра, которая бы преобразовывала такую директиву в XML-код разрыва раздела, указывающий на использование нужного колонтитула.

Порядок работы и структура директорий (Windows)

c:/users/user/appdata/roaming/pandoc
├───defaults
│     settings.yaml
├───filters
│  └───rst2docx
│        my_filter.lua
├───includes
│     roles.rst
├───pandoc-patches
├───scripts
└───templates
   └───my_template
      ├───customXml
      │   └───_rels
      ├───docProps
      ├───word
      │   ├───theme
      │   └───_rels
      │     styles.xml
      └───_rels

Я рекомендую следующую последовательность работы с этим фильтром:

  1. Создать шаблон документа MS Word, в котором будут храниться стили, которыми будет оформлен конечный документ.

  2. Извлечь styles.xml из этого шаблона и сохранить его в репозитории, после чего редактировать только его, а docx-шаблон просто собирать, упаковывая туда файлы при помощи архиватора: 7z a -tZIP -r "my_template.docx" "./my_template/*", где в директории my_template хранятся все файлы, подлежащие упаковке в шаблон документа.

  3. Написать настройки в defaults/settings.yaml, в соответствии с которыми будет выполняться конверсия.

  4. Настроить и сохранить фильтр my_filter.lua.

  5. Выполнить сборку командой вида pandoc --verbose --defaults=settings.yaml --metadata-file=document-meta.json --metadata=some:data --no-highlight -o $documentName c:\users\user\appdata\roaming\pandoc\includes\roles.rst uielements.rst introduction.rst document.rst.

    document-meta.json содержит в себе метаданные, которые мы хотим подшить в MS Word. Очень удобно.

    При желании скрипт сборщика можно дополнить препроцессорами и другими обработчиками исходного текста документа.

  6. Записать команду(ы) сборки в makefile или default.build.ps1 (если используется PowerShell и замечательный сборщик Invoke-Build).

settings.yaml

Файл настроек, подгружаемый Pandoc при помощи ключа --defaults=settings.yaml, представляет из себя список параметров, которые обычно передаются в Pandoc в командной строке.

from: rst
to: docx
reference-doc: c:\users\user\appdata\roaming\pandoc\templates\my_template.docx
highlight-style: monochrome
table-of-contents: true

filters:
- my_filter.lua

Сравните с pandoc -f rst -t docx --reference-doc=c:\users\user\appdata\roaming\pandoc\templates\my_template.docx --filters=my_filter.lua.

roles.rst

В этот файл помещаются роли reStructuredText, которые подгружаются во все создаваемые документы (см. п. 5 порядка работы). Подробности ниже.

my_filter.lua

Тут сразу нужно сделать оговорку: я пишу фильтры на MoonScript (я использую расширение файла .mp), который является диалектом Lua и транспилируется в .lua при каждом сохранении .mp.

Фильтр также можно написать на Fennel — Lisp-подобном языке, которые тоже транспилируется в Lua.

Таких языков, транспилируемых в Lua, достаточно много. Эти промежуточные языки используются для того, чтобы упростить текст фильтра, переложив труды на компилятор, и, к примеру, избавить себя от необходимость постоянно писать local, иначе возможны неприятные баги, которые очень сложно вылавливать.

Настройка фильтра заключается в том, что нужно указать на стили MS Word, которые будут использоваться при трансляции.

paraName = "Main" -- стиль абзаца по умолчанию
pictureName = "Picture" -- стиль изображения
pictureCaptionName = "Picture Caption" -- стиль подписи к изображению
tableCaptionName = "Table Caption" -- стиль примечания к таблице
tableRowName = "Table Row" -- стиль текста в таблице
headingName = "Num Heading" -- префикс заголовков, к которому будет добавлен номер уровня заголовка
bulletName = "Unnumbered" -- стиль ненумерованного списка

paraAttr = pandoc.Attr("", { }, {{"custom-style", paraName }})
imageAttr = pandoc.Attr("", { }, {{"custom-style", pictureName }})
imageCaptionAttr = pandoc.Attr("", { }, {{"custom-style", pictureCaptionName }})
tableCaptionAttr = pandoc.Attr( "", { }, {{"custom-style", tableCaptionName }} )
tableRowAttr = pandoc.Attr( "", { }, {{"custom-style", tableRowName }} )

h1Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 1" }})
h2Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 2" }})
h3Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 3" }})
h4Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 4" }})
h5Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 5" }})
h6Attr = pandoc.Attr("", { }, {{"custom-style", headingName .. " 6" }})

Инлайн (внутристрочные) роли пока оформлены непосредственно в тексте фильтра и не вынесены в настройки.

Для ролей button, command и других при обработке возвращается Span со стилем с именем «Кнопка», «Команда» и так далее; иными словами, если в исходном документе будет фрагмент :button:`Открыть`, то в .docx будет текст «Открыть», помеченный стилем «Кнопка». Оформление стиля «Кнопка» зависит от используемого шаблона.

В коде ниже следует обратить внимание, что текст роли возвращается либо «как есть», либо обёрнутый в кавычки или знаки дюйма (иногда таковы требования заказчика). Если написано return idfunc …, значит текст возвращается как есть. Если написано return wrapDblQuote или return wrapAngleBrackets, то возвращаемый текст будет обёрнут в стандартные русские кавычки-ёлочки или угловые скобки. Соответствующие функции объявлены в фильтре.

putRole = (element) ->
role = getRole element
el = pandoc.utils.stringify element
switch role
   when 'ref'
      makeRef element
   when 'prop'
      makeProperty element
   when 'link'
      makeLink element -- поле ссылки на закладку
   when 'linknum'
      makeLinkNum element -- поле ссылки на закладку с вставкой номера абзаца, на который ссылаешься
   when 'linkpage'
      makeLinkPage element -- поле ссылки на закладку в вставкой номера страницы, на которой такая закладка расположена
   when 'linknumpage'
      makeLinkNumPage element -- поле ссылки на закладку в вставкой номера абзаца и номера страницы, на которой такая закладка расположена
   when 'input'
      makeInputField element
   when 'area'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Область" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'button'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Кнопка" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'command'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Команда" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'field'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Поле" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'file'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Файл" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'flag'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Флаг" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'folder'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Папка" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'icon'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Иконка" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'key'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Клавиша" }})
      return wrapAngleBrackets pandoc.Span(el, roleAttr)
   when 'menu'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Меню" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'page'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Страница" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'parameter'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Параметр" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'path'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Путь" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'screen'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Экран" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'section'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Раздел" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'switch'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Переключатель" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'tab'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Вкладка" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'url'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "URL" }})
      link = pandoc.utils.stringify el
      return pandoc.Span(link, roleAttr)
   when 'user'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Пользователь" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'userole'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Роль" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'value'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Значение" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'window'
      roleAttr = pandoc.Attr("",{  },{{ "custom-style", "Окно" }})
      return wrapDblQuote pandoc.Span(el, roleAttr)
   when 'i'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Курсив" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'b'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Полужирный" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'yellow'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Yellow" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'fuchsia'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Fuchsia" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'green'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Green" }})
      return idfunc pandoc.Span(el, roleAttr)
   when 'red'
      roleAttr = pandoc.Attr("",{ },{{ "custom-style", "Red" }})
      return idfunc pandoc.Span(el, roleAttr)
   else
      return pandoc.Span(element)

Правила написания документа reStructuredText

Помимо обычного текста, в итоговом документе могут появиться таблицы и изображения, на которые ставятся ссылки. Выглядит это так.

.. figure:: images\login.png
   :name: Форма входа в систему

   Внешний вид формы ввода имени учётной записи (логина) и пароля

.. list-table:: Кнопки работы с записями таблицы
   :header-rows: 1

   * - Наименование кнопки
     - Описание кнопки

   * - :button:`Создать`
     - Создать новую запись
   * - :button:`Удалить`
     - Удалить выбранную запись
   * - :button:`Редактировать`
     - Открыть вкладку редактирования выбранной записи

В результате обработки такого текста фильтром получится изображение, под которым будет подпись «Рисунок 1 — Внешний вид формы ввода имени учётной записи (логина) и пароля», и таблица с заголовком «Кнопки работы с записями таблицы».

Ссылочные механизмы

В фильтре реализован ссылочный механизм, который позволяет ставить ссылки на заголовки документа, изображения и таблицы. Работает он следующим образом: в reStructuredText вставляется текст с ролями:

  • 🔗`Форма входа в систему` — поле ссылки на закладку (на изображение с :name: Форма входа в систему или на таблицу .. list-table:: Форма входа в систему; на выходе получится ссылочное текстовое поле REF с текстом «Форма входа в систему»). Это полезно, когда хочется написать в тексте документа что-нибудь типа «(см. раздел «Форма входа в систему»)».
  • :linknum:`Форма входа в систему` — поле ссылки на закладку с вставкой номера абзаца, на который ссылаешься. Для таблиц и изображений это номер (автонумератор) таблицы или изображения («1» и «1»).
  • :linkpage:`Форма входа в систему` — поле ссылки на закладку в вставкой номера страницы, на которой такая закладка расположена («см. стр. 1»).
  • :linknumpage:`Форма входа в систему;таблица;таблицу` — поле ссылки на закладку в вставкой номеров объекта и страницы, на которой такая закладка расположена («см. таблицу 1 на стр. 2»).

Правила ГОСТ и русского языка требуют, чтобы на рисунок или таблицу, следующих непосредственно за отсылкой к ним, ставилась ссылка вида «таблица 1» или «рисунок 2». В остальных случаях, когда целевой объект находится выше ссылки или на другой странице, ставится ссылка вида «см. таблицу 1» или «см. рисунок 1».

Для поддержки этой функциональности поля :link*: имеют параметры: :linkpage:`Окончание и отмена нанесения контура;таблица;таблицу` или :linknumpage:`Рисование линии объекта;рисунок`.

Если параметров нет, то будет вставлен просто номер страницы или объекта.

Если в ссылочные роли передан один параметр, он будет использован для обоих случаев (непосредственного следования целевого объекта за отсылкой к нему и для остальных случаев): текст 🔗`Окончание и отмена нанесения контура;таблица` приведёт к тому, что на объект на другой странице ссылка будет выглядеть как «см. таблица 2», что очевидно неправильно.

Если присутствуют оба параметра, то первый параметр будет использован для случаев непосредственного следования (непосредственного следования целевого объекта за отсылкой к нему) — «таблица 1», а второй — для остальных («см. таблицу 2»).

Фильтр при обработке изображений figure и таблиц вставляет к ним подписи с автонумераторами. Автонумераторы MS Word (SEQ) оборачиваются в закладки, каждая закладка имеет уникальный идентификатор, который зависит от:

  • поля :name: для иллюстрации (figure);
  • подписи таблицы.

Отсюда следует, что заголовки документа, внутренние наименования изображений (:name:) и тексты подписей к таблицам никогда не должны совпадать между собой, иначе их идентификатор будет одинаков, и ссылка будет ссылаться только на последний в документе объект-закладку с такой подписью. Для изображений figure проще, потому что :name: нигде не показывается, а значит в этот идентификатор можно писать любой текст.

Указанное выше условие накладывает некоторые ограничения на автора, поскольку в документах ГОСТ часто можно встретить подзаголовки с одинаковым текстом, но в разных разделах; на такие заголовки поставить автоматическую ссылку пока невозможно. Возможно организовать внутреннюю индексацию ссылочных элементов, но это потребует того, чтобы в :linknum: rST-документа писался не только текст заголовка, а и что-то ещё, чтобы обработчик мог понять, что ссылка должна быть поставлена, скажем, на второй заголовок с таким текстом, а не на последний.

Идентификаторы (имена) закладок, вставляемые фильтром, начинаются с буквы Z. Я считаю порочной практику «скрытых» закладок MS Word, начинающихся со знака подчёркивания (_).

Врезки

Под врезками здесь я понимаю особым образом оформленные надписи вида: «ВНИМАНИЕ!», «Примечание:» и т. д.

В reStructuredText такие врезки я создаю при помощи директив .. attention:: (стиль «Внимание»), .. note:: (стиль «Примечание») и .. tip:: («Совет»).

Зачем нужен roles.rst

Кастомные роли — инлайн-разметка текста вида :myrole:`Содержимое` — на сегодняшний момент обрабатываются Pandoc по-разному в зависимости от того, объявлены они заранее или нет.

``how it is``

в абстрактном синтаксическом дереве Pandoc будет выглядеть как

{"t":"Code","c":[["",[],[]],"how it is"]}

Текст с ролью :input:`Text`, если роль input объявлена (.. role:: input), вставляется как

{"t":"Span","c":[["",["input"],[]],[{"t":"Str","c":"Text"}]]}

и если не объявлена, то как

{"t":"Code","c":[["",["interpreted-text"],[["role","input"]]],"Text"]}

Поэтому в roles.rst указываются роли, которые используются в документе, чтобы получать на выходе Span, а не Code.

При этом фильтр умеет правильно обрабатывать как объявленную, так и не объявленную заранее роль, вставляя то, что нам нужно — текст, помеченный соответствующим стилем.