Таблицы Markdown — это ад кромешный:
- В ячейках нельзя написать текст длиннее пары слов, а тем более список.
- Если диалект и позволяет пункт 1, это неудобно форматировать.
- Если ячейки не выровнены, таблицу невозможно читать.
- Нет поддержки однотипных таблиц и автоматики, вроде нумерации строк.
Пришло время написать фильтр для Pandoc, рисующий таблицы из структурированного YAML, с нумерацией строк, горизонтальной ориентацией, шаблонами граф, и заодно разобраться, как писать Lua-фильтры.
Тексты я обычно пишу в Markdown и конвертирую в целевой формат с помощью Pandoc. Это программа, которая преобразует документы между форматами, например, из Markdown можно получить и HTML, и другой диалект MD, и DOCX, и PDF (всего более 30 входных и более 50 выходных форматов). Pandoc Markdown имеет много удобных расширений для ссылок, сносок, подписей, формул.
Pandoc работает как композиция функций (еще бы, он же написан на Haskell): конкретный входной формат > абстрактное представление документа > конкретный выходной формат. Абстрактное представление можно изменять при помощи фильтров, написанных на языке Lua. Фильтрам не требуется знать о выходном формате, но они могут учитывать его.
Наш фильтр будет искать в абстрактном представлении блоки кода на условном языке table
, читать YAML внутри них и генерировать абстрактные представления таблиц, которые Pandoc сам выдаст в целевом формате.
pandoc --lua-filter table.lua input.md -o output.html
Какие есть альтернативы и чем они хуже?
- HTML-таблицы работают только в Markdown и конвертируются только в HTML; решается только проблема богатого форматирования в ячейках.
- Генераторы таблиц требуют переключаться из текстового редактора, в них неудобно редактировать собственно содержимое ячеек (пример).
- Плагины редакторов (Emacs Org-Mode, плагины VIM) не универсальны и не всегда доступны.
Напротив, с фильтром для итоговых таблиц работает pandoc-crossref
и все плюшки Pandoc. Фильтр можно использовать и для генерации стандартных таблиц Markdown, указав соответствующий выходной формат. Из недостатков:
- Нельзя объединять ячейки, Pandoc не поддерживает этого (пока).
- Для горизонтальных таблиц стилизацию приходится делать средствами выходного формата, например, через CSS.
Описание таблицы включает три части:
Структура таблицы
Упорядоченный список граф (столбцов):
- Как минимум, у столбца должен быть заголовок (
title
). - Чтобы можно было переставлять столбцы, не трогая данные, должен быть указан атрибут записи, отображаемый в столбце (
id
). - Специальные столбцы не имеют id, а имеют описание, как их заполнять. Для начала нужен порядковый номер (
special: number
). - Выравнивание столбца (
align
).
Также таблица может быть вертикальной или горизонтальной (
orientation
). В последнем случае графы будут строками.
- Как минимум, у столбца должен быть заголовок (
Свойства таблицы: ID для ссылок (
id
) и подпись (caption
). Pandoc позволяет делать подписи к таблицам, но не к блокам кода.
Данные в виде массива словарей YAML.
Структура может быть общей для нескольких таблиц, поэтому можно описать её как непосредственно с таблицей, так и один раз в метаданных (front-matter), после чего сослаться на именованный шаблон (template
).
План реализации:
Из метаданных документа формируем словарь шаблонов.
Для каждого блока кода с классом
table
:
- Разбираем YAML таблицы.
- Если указан шаблон, берем его из словаря, иначе заполняем шаблон из YAML.
- Заполняем индивидуальные свойства таблицы из YAML.
- Формируем записи таблицы из YAML (запись — это строка в обычной таблице или столбец в горизонтальной).
- «Рисуем» таблицу по шаблону, свойствам и записям.
Верхний уровень реализуется как по писаному (весь код доступен по ссылке в конце статьи):
function Pandoc(doc)
local meta_templates = doc.meta['table-templates']
if meta_templates then
for name, value in pairs(meta_templates) do
templates[name] = parse_template(value)
end
end
local blocks = pandoc.walk_block(pandoc.Div(doc.blocks), {
CodeBlock = create_table
})
return pandoc.Pandoc(blocks, doc.meta)
end
Функция parse_template()
немного преобразует формат метаданных. Pandoc представляет их значения как объекты MetaBlock
и MetaInline
. Из них делаются либо простые строки функцией pandoc.utils.stringify()
(например, ориентация), либо визуальные элементы (например, блок текста в заголовке столбца).
Насчет отладки. В документации Pandoc много примеров, но не очень подробно описаны типы. Для отладки фильтров удобно иметь функцию дампа переменных. Серьезные библиотеки печатают слишком много подробностей, я предпочитаю один из простых вариантов.
local function to_inlines(content)
if content == nil then
return {}
elseif type(content) == 'string' then
return {pandoc.Str(content)}
elseif type(content) == 'number' then
return to_inlines(tostring(content))
elseif content.t == 'MetaInlines' then
inlines = {}
for i, item in ipairs(content) do
inlines[i] = item
end
return inlines
end
end
local function to_blocks(content)
if (type(content) == 'table') and content.t == 'MetaBlocks' then
return content
else
return {pandoc.Plain(to_inlines(content))}
end
end
Функция create_table()
вызывается для каждого блока кода в тройных бэктиках.
Нас интересуют только блоки кода «на языке» table
:
if not contains('table', block.classes) then
return block
end
Чтобы разобрать YAML внутри блока кода, формируем документ, состоящий только из YAML-метаданных, разбираем его Pandoc и оставляем только метаданные:
local meta = pandoc.read('---\n' .. block.text .. '\n---').meta
Далее из meta
читается ссылка на шаблон или структура таблицы и свойства конкретной таблицы.
Функция fill_table()
читает из meta
данные по атрибутам, указанным в описании граф. На этом же этапе, если графа отмечена как специальная, генерируется ее содержимое:
local data = {}
for i, serie in ipairs(template.series) do
if serie.special == 'number' then
data[i] = to_blocks(#datum + 1)
else
data[i] = to_blocks(item[serie.id])
end
end
Функция format_table()
формирует итоговый массив ячеек в зависимости от ориентации таблицы и создает абстрактный объект таблицы. Нужно отметить, что если ширины или заголовки должны быть заданы для всех столбцов либо ни для какого, иначе Pandoc просто не создаст таблицу.
Готовый скрипт можно положить в ~/.local/share/pandoc
(data-директорию Pandoc), чтобы обращаться к нему по имени из любого места.
P. S.
Насчет учета выходного формата фильтрами. Например, я пишу спойлеры в Pandoc так:
::: {.spoiler title="Заголовок"}
Содержимое спойлера.
:::
Спойлеров нет в модели документа Pandoc, поэтому фильтр должен выдавать «сырые» блоки примерно следующим образом. Разумеется, реальный код (spoiler.lua
) должен учитывать выходной формат через переменную FORMAT
, причем не механически: фрагмент ниже выдает raw-блоки в HTML, хотя выходной формат — markdown.
function Div(el)
if not el.attr or not contains('spoiler', el.attr.classes) then
return el
end
local title = el.attr.attributes['title'] or 'Спойлер'
table.insert(el.content, 1,
pandoc.RawBlock('html', '<' .. 'spoiler title="' .. title .. '">', 'RawBlock'))
table.insert(el.content,
pandoc.RawBlock('html', '<' .. '/spoiler>', 'RawBlock'))
return el.content
end
Ссылки
Комментарии (6)
vintage
07.11.2019 11:24Насчёт альтернатив, есть куда лучше вариант текстового описания таблиц:
-- ! **Роль** -- ! **Обязанности** -- ! **Разработчик** -- ! 1. Пишед **качественный** код ! 2. Укладывается в сроки -- ! **Менеджер** -- ! 1. Координирует разработчиков ! 2. Делает **красивые** отчёты
kozlyuk Автор
09.11.2019 03:10А что это за язык/диалект? Pandoc распознает его, но как несколько таблиц одна под другой. Также это не позволит сделать объемный текст в одной ячейке. Самые богатые возможности, мне кажется, у AsciiDoc, но синтаксис не самый приятный. Если речь о том, чтобы изменить структуру таблицы, это не всегда подходит и плохо переживает добавление граф.
Единственный пример кода таблицы тёмно малиновым цветом на чёрном фоне.(
Это у меня не сложилось с плагином, который подсвечивал бы синтаксис в блоках кода.
vintage
09.11.2019 09:02А что это за язык/диалект?
Велосипед.
Также это не позволит сделать объемный текст в одной ячейке.
Позволит.
vintage
Единственный пример кода таблицы тёмно малиновым цветом на чёрном фоне.(