Показаны сообщения с ярлыком pandoc filters. Показать все сообщения
Показаны сообщения с ярлыком pandoc filters. Показать все сообщения

пятница, 12 сентября 2014 г.

Охранные выражения с образцом (pattern guards) в haskell на конкретном примере

Я опять переписал фильтр для pandoc, о котором рассказывал здесь и здесь. Давайте посмотрим на оригинальное определение первого клоза функции substStyle.
substStyle (Format fm) m b@(Para [Image ((Style style):alt) (src, title)]) =
    case M.lookup style m of
        Nothing -> b
        Just (MetaMap mm) ->
            let params = (alt, src, title)
            in case M.lookup fm mm of
                   Nothing -> b
                   Just (MetaBlocks [RawBlock f s]) ->
                       RawBlock f $ substParams fm params s
                   Just (MetaBlocks [mb]) ->
                       walk substParams' mb
                       where substParams' (RawInline f s) =
                                   RawInline f $ substParams fm params s
                             substParams' i = i
                   Just _ -> b
        Just _ -> b
Смысл этой функции простой: вернуть новое форматирование в случае, если имя style было найдено в списке объектов метаданных документа (результат Just (MetaMap mm) в первом выражении case) и представление найденного объекта метаданных соответствует MetaBlocks [RawBlock f s] (для встроенного кода latex) или просто MetaBlocks [mb] (для встроенного кода html) как результат проверки второго case. Несмотря на то, что нас фактически интересуют только эти два условия (в остальных случаях мы просто вернем исходный блок b), использование вложенных case привело к формированию неоправданно разросшегося куста с шестью ветвями, четыре из которых в общем-то ничего полезного не делают (просто возвращают b). Как бы нам подрезать ненужные ветви? Оказывается, в haskell есть механизм, который позволит решить эту задачу. Это охранные выражения с образцом (я попытался перевести pattern guards без искажения семантики этих объектов). Документация доступна здесь. Несмотря на то, что охранные выражения с образцом являются синтаксическим расширением, никакие дополнительные флаги компиляции для их использования не требуются, поскольку они были включены в стандарт Haskell 2010. В упомянутой выше документации приводится пример с двумя последовательными поисками в отображении: как раз то, что нам нужно. Вот результат трансляции кейсов из substStyle в охранные выражения.
substStyle (Format fm) m b@(Para [Image ((Style style):alt) (src, title)])
    | Just (MetaMap mm) <- M.lookup style m
    , Just (MetaBlocks [mb]) <- M.lookup fm mm =
        let params = (alt, src, title)
            substStyle' mb
                | RawBlock f s <- mb =
                    RawBlock f $ substParams fm params s
                | otherwise =
                    let substParams' (RawInline f s) =
                                RawInline f $ substParams fm params s
                        substParams' i = i
                    in walk substParams' mb
        in substStyle' mb
    | otherwise = b
Итак, новая функция substStyle реализована с помощью охранных выражений с образцом, в которых последовательно ищутся значение style в метаданных документа и (на основе уже найденного объекта-стиля mm!) представление стиля в метаданных. Если представление стиля соответствует MetaBlocks [mb], то, с помощью функции substStyle’, которая тоже реализована с помощью охранных выражений с образцом, выполняется подстановка шаблона стиля в документ. Хочу также отметить, что в данном случае let-выражения нельзя заменить словом where, поскольку последнее может быть использовано только после перечисления всех охранных выражений функции, таким образом замена внешнего let на where приведет просто к ошибке компиляции, ну а в случае с внутренним let его замена на where слегка изменит семантику: программа в общем-то скомпилируется, поскольку where будет стоять после всех охранных выражений, однако все объявления внутри него (то есть функция substParams’) будут видны во всех ветвях охранных выражений — а зачем нам это надо? Оставшиеся трансляции case в pattern guards. Из
substStyle (Format fm) m b@(Para cnt) =
    let walk' = walk $ substInlineStyle (Format fm) m
    in case M.lookup "para_style" m of
           Nothing -> walk' b
           Just (MetaMap mm) ->
               case M.lookup fm mm of
                   Nothing -> walk' b
                   Just (MetaBlocks [Para [Span attr _]]) ->
                       walk' $ Plain [Span attr cnt]
                   Just _ -> walk' b
           Just _ -> walk' b
в
substStyle (Format fm) m b@(Para cnt)
    | Just (MetaMap mm) <- M.lookup "para_style" m
    , Just (MetaBlocks [Para [Span attr _]]) <- M.lookup fm mm =
        walk' $ Plain [Span attr cnt]
    | otherwise = walk' b
    where walk' = walk $ substInlineStyle (Format fm) m
(здесь мы использовали where, поскольку объявленная в нем функция walk’ должна быть доступна во всех ветвях охранных выражений). Из
substInlineStyle' (Format fm) m style params i =
    case M.lookup style m of
        Nothing -> i
        Just (MetaMap mm) ->
            case M.lookup fm mm of
                Nothing -> i
                Just (MetaBlocks [Para ((RawInline f s):r)]) ->
                    RawInline f $ substParams fm params $
                                    s ++ stringify' fm (map subst r)
                    where subst (Style "ALT") = RawInline f "$ALT$"
                          subst i = i
                Just _ -> i
        Just _ -> i
в
substInlineStyle' (Format fm) m style params i
    | Just (MetaMap mm) <- M.lookup style m
    , Just (MetaBlocks [Para ((RawInline f s):r)]) <- M.lookup fm mm =
        let subst (Style "ALT") = RawInline f "$ALT$"
            subst i = i
        in RawInline f $ substParams fm params $
                                s ++ stringify' fm (map subst r)
    | otherwise = i
Итак, мы заменили все case на охранные выражения, сократили код на 12 строк и сделали его более понятным. По-моему, это вин! Update. Реализовав локальную функцию substStyle’ в первом клозе substStyle с помощью охранных выражений, я, честно говоря, перестарался. В самом деле, какой смысл вытаскивать образец из связанной переменной, если эту переменную можно изначально представить в виде декомпозиции с интересующим нас конструктором? Использование охранных выражений с образцом в этом случае — это просто ненатуральный аналог сопоставления с образцом. Таким образом, первый клоз substStyle следует переписать так:
substStyle (Format fm) m b@(Para [Image ((Style style):alt) (src, title)])
    | Just (MetaMap mm) <- M.lookup style m
    , Just (MetaBlocks [mb]) <- M.lookup fm mm =
        let params = (alt, src, title)
            substStyle' (RawBlock f s) = RawBlock f $ substParams fm params s
            substStyle' b = walk substParams' b
                where substParams' (RawInline f s) =
                            RawInline f $ substParams fm params s
                      substParams' i = i
        in substStyle' mb
    | otherwise = b
И substParams’ теперь можно определить внутри блока where, поскольку два определения substStyle’ стали лексически независимы, в отличие от случая с охранными выражениями.

суббота, 6 сентября 2014 г.

Синонимы образцов (pattern synonyms) в ghc-7.8 на конкретном примере

В предыдущей статье был представлен фильтр для pandoc, который позволяет настраивать стили изображений. В дальнейшем возможности фильтра были расширены и новая версия размещена на github. Основное нововведение — это возможность ссылаться на определение стиля, объявленного в метаданных документа, прямо из ссылки на изображение в исходном документе. Например, ссылка
![$img_style$ *Пример изображения*](../images/gnome-terminal-italic.png)
без использования фильтра styleFromMeta будет оформлена с альтернативным текстом img_style Пример изображения, причем запись img_style будет рассматриваться как встроенная математическая запись (inline math), поскольку она окружена символами доллара. Фильтр styleFromMeta проверяет, не начинается ли содержимое внутри квадратных скобок в определениях ссылок (и не только на изображения) со встроенной математической записи, и если это так, то пытается найти объект с именем этой записи в метаданных документа. В случае удачного поиска найденный объект подставляется в выходной документ с возможной заменой плейсхолдеров $ALT$, $SRC$ и $TITLE$ на значения из определения ссылки. Вместо $ALT$ подставляется остаток содержимого внутри квадратных скобок определения ссылки без начальных пробелов. В нашем примере значению $ALT$ будет соответствовать строка Пример изображения. Алгоритм поиска ссылок с возможным указанием на требуемый стиль реализуется с помощью сопоставления с образцом (pattern matching). Например, так.
substStyle :: Format -> MMap -> Block -> Block
substStyle (Format fm) m
           b@(Para [Image ((Math InlineMath style):alt) (src, title)]) =
Я не привел тело функции, поскольку к обсуждаемой проблеме оно не имеет отношения. Тип MMap объявлен как
type MMap = M.Map String MetaValue
, он соответствует способу хранения метаданных документа в pandoc. Главная проблема определения substStyle состоит в необходимости записывать образец для поиска стиля как Math InlineMath style. Но ведь это же просто деталь реализации! Просто так совпало, что мы выбрали обрамление долларами для отметки ссылок на стиль, и ссылки эти не имеют никакого отношения к встроенным математическим записям. Иными словами, использование оригинальных имен конструкторов в данном случае искажает семантику образца и не отражает наше намерение использовать его для извлечения ссылок на стиль. Определение
substStyle (Format fm) m b@(Para [Image ((Style style):alt) (src, title)]) =
выглядело бы гораздо лучше. Провернуть такой трюк позволит новое синтаксическое расширение Синонимы образцов (pattern synonyms), появившееся в ghc-7.8. Выражение Style в новом определении substStyle можно объявить как синоним образца.
pattern Style x <- Math InlineMath x
В данном случае мы объявили односторонний (unidirectional) синоним образца: для определения таких синонимов используется символ <-. Двухсторонние (bidirectional) синонимы объявляются с помощью символа =. Разница между двумя типами синонимов образцов простая: односторонние синонимы можно использовать только для сопоставления с образцом, в то время как двухсторонние — как обычные выражения. Подробнее о синонимах образцов можно узнать здесь. Все это хорошо, но что делать, если у нас в наличии старый компилятор ghc? Он не поймет, что это за объявление pattern Style x и что за Style объявлен в определении функции styleFromMeta. В нашем случае проблема легко решается введением условной компиляции и применением макросов cpp. В самом начале программы добавим объявления
{-# LANGUAGE CPP #-}
#if __GLASGOW_HASKELL__ >= 708
{-# LANGUAGE PatternSynonyms #-}
#endif
В первой строке заявлено, что мы хотим использовать препроцессор cpp. Далее, если версия ghc не древнее 7.8, мы включаем расширение PatternSynonyms. После объявлений импортов модулей вставляем строки
#if __GLASGOW_HASKELL__ >= 708
pattern Style x <- Math InlineMath x
#else
#define Style Math InlineMath
#endif
Если компилятор ghc древнее версии 7.8, то препроцессор cpp заменит слово Style на два слова Math InlineMath во всем исходном коде программы непосредственно перед ее компиляцией.

четверг, 21 августа 2014 г.

Еще один фильтр для pandoc

На этот раз будем решать следующую задачу. Требуется стилизовать отдельные (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) изображений и ссылок. Подробности на странице проекта.

суббота, 5 апреля 2014 г.

Простой фильтр pandoc для преобразования параграфов HTML в блоки спанов

Вот исходный код фильтра (файл paraToSpanBlock.hs).
-- paraToSpanBlock.hs
import Text.Pandoc.JSON

paraToSpanBlock :: Maybe Format -> Block -> IO Block
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
Фильтр преобразует блоки параграфов <p> ... </p> в блоки спанов <span style="STYLE"> ... </span>, где STYLE — значение, возвращаемое функцией style, объявленной внутри исходного кода фильтра и устанавливающей внешний вид этих блоков — величину отступа снизу и шрифт. Этот фильтр позволяет с легкостью генерировать код HTML для непосредственной вставки в блог на blogspot.com. Да, этот текст написан на pandoc flavoured markdown и преобразован в HTML с помощью команды
pandoc -thtml -FparaToSpanBlock -Fvimhl -S src.md
Здесь src.md — файл с этим исходным текстом, paraToSpanBlock и vimhl — фильтры paraToSpanBlock, о котором идет речь, и vimhl, о котором я писал здесь. Для компиляции фильтра paraToSpanBlock в командной строке нужно ввести
ghc --make paraToSpanBlock
Скомпилированную программу paraToSpanBlock следует поместить в директорию, объявленную в переменной окружения PATH. Плюсы в использовании простого текстового языка разметки для создания контента блога очевидны. Самое главное — это возможность редактирования контента в простом текстовом редакторе. Второе — возможность генерации документов в разных форматах, например в формате PDF, и здесь такой замечательный инструмент как Pandoc проявляет себя во всей красе. Третье (в том числе как следствие использования автоматических инструментов) — последовательное применение стилей к разным элементам текста, не допускающее случайных ошибок автора документа.