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

пятница, 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) изображений и ссылок. Подробности на странице проекта.

воскресенье, 13 апреля 2014 г.

Несколько трюков с пакетом Latex Listings

Здесь я хочу показать три трюка с пакетом Listings, которые мне очень пригодились при определении “языка” для подсветки действий пользователя внутри командных оболочек и REPLов. Этот виртуальный язык, который я назвал shelloutput, может быть использован в плагине publish-helper. Некоторые сведения о реализации подсветки shelloutput в Listings приведены в разделе Highlighting shells and REPLs на главной странице проекта в github, там же приведен пример с картинками. Итак, определение псевдоязыка shelloutput — это и есть первый трюк, о котором я расскажу. Постановка задачи такова: обеспечить декоративное выделение блоков текста, изображающих действия пользователя внутри любых командных оболочек и REPLов, а также разделение текста с вводом пользователя (промпта) и выводом оболочки. Первая часть задачи решается без определения свойств языка и, соответственно, без участия пакета Listings. Простейший способ — определить новое окружение, внутри которого использовать какой-либо рамочный пакет, например пакет Framed.
\usepackage{framed}
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
Это окружение, которое я назвал Leftbar, поскольку оно использует стиль leftbar из пакета Framed, должно находиться в преамбуле документа. Теперь, чтобы выделить блок текста с действиями пользователя в оболочке, нужно обернуть этот текст в тэги \begin{Leftbar} ... \end{Leftbar}. Перейдем к решению второй части задачи. Нам нужно определить новый язык, который мы сможем использовать в окружении lstlisting, которое, в свою очередь, мы будем помещать внутрь окружения Leftbar. Пусть основной стиль (basicstyle) языка shelloutput будет относиться к выводу оболочки, а стиль для ввода оболочки мы определим отдельно. Самое сложное здесь — это понять, как мы (и Listings) будем отличать ввод оболочки от вывода: промпты у всех оболочек разные и могут отсутствовать вообще. Поэтому мы не будем ориентироваться на промпт, а введем собственный виртуальный промпт, который будем вставлять в начало каждой строки, соответствующей вводу оболочки — ведь мы то знаем, где у нее ввод и вывод! В Listings есть опция moredelim, с помощью которой мы определим внешний вид ввода оболочки, передав ей наш виртуальный промпт (пусть это будет три бара и пробел — ‘||| ’) с флагами il, которые устанавливают, что Listings не должен показывать виртуальный промпт (i), и установки будут действовать в пределах всей строки (l). Вот как это может выглядеть в преамбуле документа:
\usepackage{listings}
\definecolor{shellpromptcolor}{HTML}{000000}
\definecolor{shelloutputcolor}{HTML}{666666}
\lstdefinelanguage{shelloutput}
  {basicstyle=\color{shelloutputcolor}
    \scriptsize
    \ttfamily\itshape,
   moredelim=[il][\color{shellpromptcolor}\upshape]{|||\ }}
В данном случае вывод оболочки будет подсвечен серым цветом и иметь наклонное начертание, в то время как ввод будет черным и прямым. Давайте добавим сюда небольшой, но красивый бонус — декоративные переносы строк. Для этого в преамбулу документа нужно добавить
\usepackage{MnSymbol}
\lstset{prebreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rhookswarrow}}}
\lstset{postbreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rcurvearrowse\space}}}
Пример использования подсветки виртуального языка shelloutput можно найти в разделе An example на странице разработки проекта vim-publish-helper. В этом примере документ, представленный на втором изображении, генерируется не напрямую из документа TeX, а опосредованно, через pandoc flavoured markdown, но результат будет совпадать независимо от языка исходного документа. Второй трюк. Listings преобразует символ переноса (-) в знак минус. Это разумно для многих языков, однако для нашего псевдоязыка shelloutput это выглядит некрасиво, во всяком случае в опциях команд оболочки, таких как ls -l --inode. Вернуть исходный символ переноса достаточно просто — определив опцию literate={-}{-}1 в преамбуле документа или внутри окружения lstlisting, однако этот простой подход ломает настройки alsoletter={-} и breakatwhitespace для символов переноса, что приводит к возможности обрыва строк при переносе на этих символах, а это выглядит очень некрасиво. Лучше всего определить макрос lst@CCPutMacro для символа переноса (фактически переопределить его, так как он уже определен — и в этом-то и проблема — в исходном коде listings.sty). Определение следует поместить в нашем окружении Leftbar, чтобы оно не повлияло на другие языки. Теперь Leftbar будет выглядеть так:
\makeatletter
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \lst@CCPutMacro\lst@ProcessLetter {"2D}{\lst@ttfamily{-{}}{-{}}}
  \@empty\z@\@empty
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
\makeatother
Третий трюк. Русские символы (и вообще UTF-8) в выводе (и вводе) языка shelloutput. Оказывается, Listings до сих пор не поддерживает нормальную обработку национальных символов в формате UTF. Если в shelloutput встретятся русские символы, то они будут переставлены всевозможными способами с удалением пробелов и добавлением неожиданных новых строк. Проблема известная и есть вариант ее решения с помощью опции literate (см. здесь и здесь). Однако, этот способ очень verbose — лично мне он не нравится. Более простое и рабочее решение — использовать опцию escapeinside=|| в объявлении lstlisting, а внутри блока с текстом обрамлять участки с проблемными символами соответствующими эскейп-символами, в данном случае барами (|) и, возможно, внутри них тэгом \verb, который не позволит слипнуться множественным пробелам.

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

четверг, 9 января 2014 г.

vim: плагин publish_helper

Работу, начатую здесь и здесь, решил оформить в виде плагина vim (страница на гитхабе и страница на www.vim.org). Полная документация доступна по ссылкам, а также внутри плагина. Плагин состоит из двух частей. Первая часть - это собственно скрипт на viml, в котором определены команды MakeHtmlCodeHighlight (переименованная и улучшенная MakeBlogArticle) и MakeTexCodeHighlight (тоже улучшенная по сравнению с первой реализацией). Вторая часть написана на haskell и представляет собой фильтр для pandoc (фильтры доступны в pandoc начиная с версии 1.12), который позволяет подменять исходные блоки кода (CodeBlock) внутри абстрактного синтаксического дерева, представляющего документ, на неформатированные блоки (RawBlock), содержащие подсвеченный с помощью vim код.

Для того, чтобы показать как это работает, возьмем оригинальный пример из руководства пользователя pandoc, в котором приведена классическая реализация быстрой сортировки на haskell, и добавим туда еще пару образцов кода.
### Original example from [*Pandoc User's Guide*](http://johnmacfarlane.net/pandoc/README.html#fenced-code-blocks)

~~~~ {#mycode .haskell .numberLines hl="vim" startFrom="100"}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Small C++ code sample

~~~~ {.cpp hl="vim"}
G4UIQt *  qtSession( dynamic_cast< G4UIQt * >( session ) );

if ( qtSession )
{
    qtSession->AddMenu( histoMenuHandle, histoMenuLabel );
    BuildMenuTree( qtSession, histoMenuHandle, gDirectory->GetList() );
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Content of my *~/.vimrc.pandoc*

~~~~ {#vimrc_pandoc .vim .numberLines hl="vim"}
syntax on

filetype on
filetype indent on
filetype plugin on
filetype plugin indent on

let g:lucius_style = 'light'
let g:lucius_contrast = 'high'
let g:lucius_contrast_bg = 'high'

set nocp    " for line breaks with backslashes
colorscheme lucius

let g:PhCtrlTrans = 0
let g:PhHtmlPreAttrs = 'style="white-space: pre-wrap;"'

runtime plugin/publish_helper.vim
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Это синтаксическая подсветка цветовой схемы lucius для типа файла pandoc, который будет доступен, если вы установите плагин для pandoc отсюда. Я вставил ее сюда из vim дословно с помощью команды MakeHtmlCodeHighlight.

Идем дальше. Сохраняем этот пример в файле example.md и генерируем HTML файл example.html с помощью pandoc и фильтра vimhl из нашего плагина.
pandoc --standalone -t html -F vimhl -o example.html example.md
Получаем файл example.html с таким содержимым:
 

Original example from Pandoc User's Guide

100  qsort []     = []
101  qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
102                 qsort (filter (>= x) xs)

Small C++ code sample

G4UIQt *  qtSession( dynamic_cast< G4UIQt * >( session ) );

if ( qtSession )
{
    qtSession->AddMenu( histoMenuHandle, histoMenuLabel );
    BuildMenuTree( qtSession, histoMenuHandle, gDirectory->GetList() );
}

Content of my ~/.vimrc.pandoc

 1  syntax on
 2  
 3  filetype on
 4  filetype indent on
 5  filetype plugin on
 6  filetype plugin indent on
 7  
 8  let g:lucius_style = 'light'
 9  let g:lucius_contrast = 'high'
10  let g:lucius_contrast_bg = 'high'
11  
12  set nocp    " for line breaks with backslashes
13  colorscheme lucius
14  
15  let g:PhCtrlTrans = 0
16  let g:PhHtmlPreAttrs = 'style="white-space: pre-wrap;"'
17  
18  runtime plugin/publish_helper.vim

Это цвета схемы lucius с правильной подсветкой синтаксиса всех образцов. А теперь создадим аналогичный PDF документ.
pandoc --standalone -t latex -F vimhl -o example.tex example.md
Открываем файл example.tex и вставляем в преамбулу строки
\usepackage{color}
\usepackage{xcolor}
\usepackage{fancyvrb}
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
\usepackage{framed}
\newenvironment{Shaded}{
  \definecolor{shadecolor}{rgb}{1.0, 1.0, 0.9}
  \setlength\parskip{0cm}
  \setlength\partopsep{-\topsep}
  \addtolength\partopsep{0.2cm}
  \begin{shaded}
    \scriptsize
}{\end{shaded}}
Зачем это нужно, вы можете прочесть в документации к плагину. Завершаем генерацию example.pdf.
latexmk -pdf example
Вот скриншот получившегося документа:

среда, 30 октября 2013 г.

Прямое цитирование подсветки синтаксиса из vim в tex

В предыдущей статье я показал, как с помощью pandoc экспортировать статьи в формате HTML в формат tex. Поскольку статьи в моем блоге содержат множество примеров исходного кода, то необходимо, чтобы pandoc умел их правильно подсвечивать. С этой задачей pandoc справляется неплохо, делегируя ее исполнение библиотеке highlighting-kate; если же какой-то язык не поддерживается (например VimL), то можно воспользоваться пакетом latex minted.

Всё это прекрасно. Однако выяснилось, что некоторые статьи данного блога (например эта) требуют прямого цитирования подсветки vim, поскольку в них, собственно, обсуждается как vim подсвечивает код и приводятся примеры. Я рассказывал, как я вставляю подсвеченный исходный код из vim в статьи блога здесь. Главная идея - волшебная команда vim MakeBlogArticle, которая преобразует выделенный текст (или весь буфер) в формат HTML с сохраненной подсветкой синтаксиса. MakeBlogArticle использует в качестве бэкенда скрипт TOhtml, поэтому реализовать ее не сложно. Теперь же нам нужна подобная команда MakeTexCodeHighlight, которая будет транслировать выделенный текст (или весь буфер) в некоторое представление tex, совместимое с документами, сгенерированными pandoc. Требования к совместимости понятны. Поскольку pandoc для подсветки исходного кода генерирует среду Shaded, реализованную через fancyvrb, то результирующий документ должен находиться внутри тэгов
\begin{Shaded}
\begin{Highlighting}[]

...

\end{Highlighting}
\end{Shaded}
, а внутри своего тела использовать цветовые тэги \textcolor.

Давайте реализуем команду MakeTexCodeHighlight.

Прежде всего нам понадобятся функции, определяющие цвета текста и фона синтаксической группы элемента под курсором. Вот их реализация:
fun! <SID>get_color_under_cursor(bg)
    let synId = synID(line("."), col("."), 1)
    let name = synIDattr(synId, "name")
    if name == ''
        return 'none'
    endif
    let layer = a:bg ? "bg" : "fg"
    return <SID>Xterm2rgb256(synIDattr(synIDtrans(synId), layer))
endfun

fun! GetFgColorUnderCursor()
    return <SID>get_color_under_cursor(0)
endfun

fun! GetBgColorUnderCursor()
    return <SID>get_color_under_cursor(1)
endfun
Функция get_color_under_cursor() - рабочая лошадка, выполняющая задания двух основных функций: GetFgColorUnderCursor() и GetBgColorUnderCursor(). Ее задача очень простая - выдать шестнадцатиричное значение цвета под курсором. Выражение
synIDattr(synIDtrans(synId), layer)
возвращает номер этого цвета для 256-цветного терминала (да, я ограничился текстовой версией vim, в gvim это работать не будет!), а функция Xterm2rgb256() должна вернуть окончательное шестнадцатиричное число в виде строки. Функции Xterm2rgb256() в vim нет, мы должны ее реализовать. К счастью, подобных функций в разных плагинах vim полно. Многие из них используют обычную таблицу соответствия, однако я нашел аналитическую реализацию данной функции в плагине Colorizer. Вот адаптированная версия Xterm2rgb256(), возвращающая шестнадцатиричное число в виде строки:
" next Xterm2rgb... conversion functions are adopted from plugin Colorizer.vim
fun! <SID>Xterm2rgb16(color)
    " 16 basic colors
    let r=0
    let g=0
    let b=0
    let basic16 = [
                \ [ 0x00, 0x00, 0x00 ], 
                \ [ 0xCD, 0x00, 0x00 ], 
                \ [ 0x00, 0xCD, 0x00 ], 
                \ [ 0xCD, 0xCD, 0x00 ], 
                \ [ 0x00, 0x00, 0xEE ], 
                \ [ 0xCD, 0x00, 0xCD ], 
                \ [ 0x00, 0xCD, 0xCD ], 
                \ [ 0xE5, 0xE5, 0xE5 ], 
                \ [ 0x7F, 0x7F, 0x7F ], 
                \ [ 0xFF, 0x00, 0x00 ], 
                \ [ 0x00, 0xFF, 0x00 ], 
                \ [ 0xFF, 0xFF, 0x00 ], 
                \ [ 0x5C, 0x5C, 0xFF ], 
                \ [ 0xFF, 0x00, 0xFF ], 
                \ [ 0x00, 0xFF, 0xFF ], 
                \ [ 0xFF, 0xFF, 0xFF ]
                \ ]
    let r = basic16[a:color][0]
    let g = basic16[a:color][1]
    let b = basic16[a:color][2]
    return printf("%02x%02x%02x", r, g, b)
endfun

fun! <SID>Xterm2rgb256(color)
    let r=0
    let g=0
    let b=0
    " 16 basic colors
    if a:color < 16
        return <SID>Xterm2rgb16(a:color)
    " color cube color
    elseif a:color >= 16 && a:color < 232
        " the 6 value iterations in the xterm color cube
        let valuerange6 = [ 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF ]
        let color=a:color-16
        let r = valuerange6[(color/36)%6]
        let g = valuerange6[(color/6)%6]
        let b = valuerange6[color%6]
    " gray tone
    elseif a:color >= 232 && a:color <= 255
        let r = 8 + (a:color-232) * 0x0a
        let g = r
        let b = r
    endif
    return printf("%02x%02x%02x", r, g, b)
endfun
Теперь мы можем определить команды vim для того, чтобы комфортно получать значения цветов синтаксического элемента под курсором из терминала (для нашей основной цели эти команды не нужны, разве что только для отладки):
command GetFgColorUnderCursor echo GetFgColorUnderCursor()
command GetBgColorUnderCursor echo GetBgColorUnderCursor()
Следующим шагом будет реализация функции split_synids(), которая будет разбивать текст внутри области текста, ограниченной значениями номеров строк, переданных ей в качестве аргументов, на отдельные элементы, включающие в себя имя синтаксического элемента, его содержание (т.е. соответствующую подстроку), номер строки, в которой он находится, а также значения цветов текста и фона в шестнадцатиричном формате.
fun! <SID>split_synids(fst_line, last_line)
    let result = []
    let save_cursor = getpos('.')
    let save_winline = winline()
    call setpos('.', [0, a:fst_line, 1, 0])
    let cursor = getpos('.')
    while cursor[1] <= a:last_line
        let old_synId = '^'
        let old_start = cursor[2]
        let cols = col('$')
        if cols == 1
            let cursor[1] += 1
            let cursor[2] = 1
            call setpos('.', cursor)
            continue
        endif
        while cursor[2] <= cols
            let synId = synIDattr(synID(line('.'), col('.'), 1), 'name')
            let fg = toupper(GetFgColorUnderCursor())
            let bg = toupper(GetBgColorUnderCursor())
            let cursor[2] += 1
            call setpos('.', cursor)
            if synId != old_synId
                if old_synId != '^'
                    call add(result,
                            \ {'name': old_synId,
                            \ 'content': strpart(getline('.'), old_start - 1,
                            \            cursor[2] - old_start - 1),
                            \  'line': line('.'), 'fg': old_fg, 'bg': old_bg})
                endif
                let old_synId = synId
                let old_start = cursor[2] - 1
            endif
            let old_fg = fg
            let old_bg = bg
        endwhile
        call add(result,
                    \ {'name': synId,
                    \ 'content': strpart(getline('.'), old_start - 1,
                    \            cursor[2] - old_start - 1),
                    \  'line': line('.'), 'fg': fg, 'bg': bg})
        let cursor[1] += 1
        let cursor[2] = 1
        call setpos('.', cursor)
    endwhile
    call setpos('.', save_cursor)
    let move = winline() - save_winline
    if move != 0
        let dir = move < 0 ? '^Y' : '^E'
        exe "normal ".abs(move).dir
    endif
    return result
endfun
В переменной result будем сохранять список синтаксических элементов. Манипуляции с save_cursor и save_winline нужны для восстановления положения курсора и окна после завершения работы функции. Внутренний цикл while обходит элементы одной строки слева направо, добавляя информацию о встреченных синтаксических областях в result. Для этой цели вызываются функции GetFgColorUnderCursor() и GetBgColorUnderCursor(), которые идентифицируют синтаксический элемент под курсором. Возвращенное ими значение (строка, представляющая шестнадцатиричное число) переводится в верхний регистр: это будет нужно в итоговом документе, так как latex почему-то не любит нижний регистр в шестнадцатиричных числах (во всяком случае это верно для тэгов \textcolor). Внешний цикл while перебирает строки сверху вниз.

Теперь напишем функцию write_latex_code_highlights(). Она будет выполнять основную работу по созданию документа: открывать окно для нового буфера, вводить открывающие и закрывающие тэги tex и, собственно, тело документа на основании списка, возвращенного функцией split_synids().
fun! <SID>write_latex_code_highlights(fst_line, last_line, ...)
    let colors = g:colors_name
    colorscheme lucius
    let parts = <SID>split_synids(a:fst_line, a:last_line)
    exe "colorscheme ".colors
    let save_paste = &paste
    new +set\ nowrap\ paste
    normal a\begin{Shaded}^M\begin{Highlighting}[]^M
    let old_line = a:fst_line
    let line = old_line
    for hl in parts
        let line = hl['line']
        while line > old_line
            normal o
            let old_line += 1
        endwhile
        let part = escape(hl['content'], '\{}_$%')
        let part = substitute(part, '\\\\', '\\textbackslash{}', 'g')
        let fg = hl['fg']
        " small hack to paint syntax group links that could have been lost
        " after switching colorscheme in black instead white
        if a:0 && a:1 && fg == 'FFFFFF'
            let fg = '000000'
        endif
        if fg != 'NONE' && part !~ '^\s*$'
            let part = '\textcolor[HTML]{'.fg.'}{'.part.'}'
        endif
        exe "normal a".part
    endfor
    while line < a:last_line
        normal o
        let line += 1
    endwhile
    normal a^M0^D\end{Highlighting}^M\end{Shaded}^[gg
    set ft=tex
    if !save_paste
        set nopaste
    endif
endfun
Код, в общем-то, не должен вызывать затруднений. Прокомментирую лишь некоторые моменты. Я уже писал, зачем при создании документов для публикации я заменяю рабочую цветовую схему на схему lucius: просто она светлая и более подходит для создания статей, чем моя стандартная темная схема. Элементы ^M, ^D и ^[ в коде состоят не из двух символов; это одиночные символы, соответствующие вводу <Enter>, Ctrl-D и <Esc>. Чтобы их ввести в редакторе vim, наберите Ctrl-V и, удерживая клавишу Ctrl, соответствующий символ (M, D или <Esc>). Команда new устанавливает в новом буфере значения nowrap (чтобы строки не заворачивались - нет смысла) и paste (чтобы vim не выполнял вредную автоматическую индентацию текста). Поскольку значение paste устанавливается глобально, то оно восстанавливается при выходе из функции. Внутри цикла for собранные функцией split_synids() синтаксические элементы выводятся с помощью normal a, новые строки добавляются с помощью normal o. При выводе в буфер все символы {, }, _, $ и % слэшируются, а символ \ заменяется на \textbackslash{} - если этого не сделать, то возможны ложные интерпретации строк буфера как команд latex! Если цвет fg данного элемента не NONE, то он обрамляется тэгом \textcolor[HTML]{}{}.

Опциональный третий аргумент write_latex_code_highlights() преобразует белый текст в черный. Белый текст может появиться в результате исчезновения синтаксических групп при переходе в новую цветовую схему. Дело в том, что я использую расширенную цветовую схему для отображения тэгов ctags (см. эту статью), и при замене ее на lucius все группы, добавленные ctags, исчезают, при этом функция GetFgColorUnderCursor() возвращает почему-то значение ffffff, соответствующее белому цвету, хотя реальный цвет "потерянных" элементов черный. В общем этот третий аргумент можно рассматривать как хак и в реальности он вряд ли нужен.

Вот определение команды MakeTexCodeHighlight:
command -range=% MakeTexCodeHighlight
        \ silent call <SID>write_latex_code_highlights(<line1>, <line2>, 1)
Теперь можно выделять нужные строки в буфере (без выделения команда обработает весь буфер) и, после ввода команды MakeTexCodeHighlight, копировать текст в новом окне и вставлять его в документ tex, созданный pandoc.

Пример.

Код на C++:
int  main( void )
{
    return 0;
}
Код tex, сгенерированный MakeTexCodeHighlight:
\begin{Shaded}
\begin{Highlighting}[]
\textcolor[HTML]{005F87}{int}  \textcolor[HTML]{008700}{main}\textcolor[HTML]{870087}{(} \textcolor[HTML]{005F87}{void} \textcolor[HTML]{870087}{)}
\textcolor[HTML]{870087}{\{}
    \textcolor[HTML]{005FAF}{return} \textcolor[HTML]{AF5F00}{0}\textcolor[HTML]{870087}{;}
\textcolor[HTML]{870087}{\}}
\end{Highlighting}
\end{Shaded}
(кстати, чтобы вставить сюда этот результат я сначала выполнил MakeTexCodeHighlight, а затем, уже в новом буфере, MakeBlogArticle).
 
Update. Заметил, что MakeTexCodeHighlight неправильно обрабатывает эти дословные символы ^M, ^D и т.п. Команда MakeBlogArticle делает это правильно, видимо в TOhtml они предварительно транслируются.

Update 2. В функции split_synids() тоже есть контрольные символы ^Y и ^E. Во всех случаях их использования можно избежать. Вообще, их появление в скрипте связано с эмуляцией действий пользователя в терминале с помощью комады normal. Если мы найдем соответствующие функции в API VimL, то команда normal будет не нужна. В самом деле, для сохранения и восстановления окна в API VimL предусмотрены функции winsaveview() и winrestview() - использование их вместо эмуляции действий пользователя в split_synids() упростит эту функцию и поможет избежать побочных эффектов, связанных с возможным ремаппингом  ^Y и ^E пользователем. В функции write_latex_code_highlights() команда normal с контрольными символами используется для эмуляции ввода пользователем строк. В VimL есть функция append(), которая может легко заменить эту эмуляцию и, соответственно, убрать контрольные символы из кода write_latex_code_highlights() и упростить ее понимание.