На этот раз будем решать следующую задачу. Требуется стилизовать отдельные (standalone) изображения (такие обычно помещаются внутрь HTML тэгов <div> … </div>) в документе, представляющем собой отдельный фрагмент HTML (то есть inlined HTML — без поддержки CSS). Я не взял эту задачу с потолка. Именно такие фрагменты я вставляю в свой блог каждый раз, когда пишу очередную статью. Непосредственно HTML код статьи генерируется из простого текстового документа с помощью pandoc и дополнительных фильтров (я писал о фильтрах, которые я использую здесь и здесь).
Фильтры pandoc — замечательная вещь! Но у них есть небольшой недостаток. Если запускать фильтр с помощью опций pandoc -F или --filter, то в них нельзя будет передать их собственные опции, которые, как у каждой отдельной программы, могут быть предусмотрены — это несмотря на обратное утверждение в документации к классу ToJSONFilter. Обойти это можно разными способами: пропускать фильтр через пайпы без применения опций -F или --filter, передавать настройки с помощью переменных среды, в конце концов, создать несколько фильтров в которых захардкодить нужные настройки. Но все это совсем некрасиво.
Наш фильтр должен уметь принимать разные стили изображений. При этом он не должен читать их как опции командной строки, поскольку pandoc их не сможет передать. К счастью, в pandoc есть одна хитрая лазейка на этот случай — метаданные документа. Они не отображаются в сгенерированных документах и при этом в них можно записать все что угодно. Фильтры pandoc имеют доступ к метаданным исходного документа!
Вот исходный код фильтра (я назвал его imgStyleFromMeta), а ниже пояснения.
-- imgStyleFromMeta.hs
{-# OPTIONS_HADDOCK prune, ignore-exports #-}
import Text.Pandoc.JSON
import Text.Pandoc.Walk (walk)
import qualified Data.Map as M
import Data.String.Utils (replace)
-- | Applies image style found in the metadata of the document
--
-- Finds field /img_style/ in the document metadata and applies its value
-- to all standalone images found in the document. Field /img_style/ may be
-- declared inside YAML block on the top of the document like
--
-- > ---
-- > img_style : |
-- > <div class="figure" style="clear: both; text-align: center;">
-- > <a href="$SRC$" style="margin-left: 1em; margin-right: 1em;">
-- > <img border="0" src="$SRC$" /></a></div>
-- > ...
--
-- Additionally placeholders /$SRC$/ and /$TITLE$/ in the /img_style/ will
-- be replaced by actual source and title specified in an image parameters.
--
imgStyleFromMeta :: Maybe Format -> Pandoc -> IO Pandoc
imgStyleFromMeta (Just (Format "html")) p@(Pandoc m bs) =
return $ case (M.lookup "img_style" (unMeta m)) of
Nothing -> p
Just (MetaBlocks [b]) -> Pandoc m (walk (substImgParams b) bs)
Just _ -> p
imgStyleFromMeta _ p = return p
substImgParams :: Block -> Block -> Block
substImgParams b (Para [Image _ (src, title)]) =
walk substImgParams' b
where substImgParams' (RawInline f s) =
RawInline f (foldr (\(a, b) -> replace a b) s
[("$SRC$", src), ("$TITLE$", title)])
substImgParams' b = b
substImgParams _ b = b
main :: IO ()
main = toJSONFilter imgStyleFromMeta
Как установить стиль изображения сказано в комментарии внутри приведенного кода. Функция imgStyleFromMeta пытается найти в метаданных исходного документа поле img_style и, в случае успеха, проходится (walk) по блокам документа bs, подставляя найденный результат. Но не все так просто: функция substImgParams также проходится по объектам, которые нужно вставить, заменяя в них плейсхолдеры $SRC$ и $TITLE$ на результаты совпадения (matches) src и title в Para [Image _ (src, title)]. Кстати, этот паттерн и определяет, к каким типам изображений из AST-представления исходного документа будут применены подстановки стиля. Фильтр paraToSpanBlock из этой статьи преобразует любые блоки, соответствующие паттерну Para contents (ему же соответствуют и блоки с отдельными изображениями) в Plain [Span ... contents] — а это уже не соответствует отдельным изображениям. Соответственно, если сначала пройтись по исходному документу фильтром paraToSpanBlock, а затем фильтром imgStyleFromMeta, то новые стили не будут применены. Исправить это просто: paraToSpanBlock не должен преобразовывать отдельные изображения в спаны — в конце концов они и не должны находиться внутри спанов.
Вот исправленный код paraToSpanBlock.
-- paraToSpanBlock.hs
import Text.Pandoc.JSON
paraToSpanBlock :: Maybe Format -> Block -> IO Block
paraToSpanBlock (Just (Format "html")) b@(Para [Image _ _]) =
return b
paraToSpanBlock (Just (Format "html")) (Para contents) =
return $ Plain [Span ("", [], [("style", style)]) contents]
where style = "display: block; margin-bottom: 16px;\
\ font-family: Arial, Helvetica, sans-serif;"
paraToSpanBlock _ b = return b
main :: IO ()
main = toJSONFilter paraToSpanBlock
Кстати, здесь тоже есть захардкоженный стиль, так что и этот фильтр можно переписать в стиле imgStyleFromMeta.
Update. Запилил новый фильтр styleFromMeta, который умеет делать то же самое, что и imgStyleFromMeta (при этом с возможностью применять разные стили для разных изображений и форматов документа), и paraToSpanBlock. Кроме того, в новом фильтре доступны настройки стилей встроенных (inline) изображений и ссылок. Подробности на странице проекта.