Hastache — реализация Mustache для Haskell

Март 28, 2011, 07:00

Довел до ума и выложил в open source свою реализация шаблонизатора Mustache, на которой, в частности, крутится сайт с которого вы это сейчас читаете.

Взять можно либо на GitHub, либо из HackageDB:

cabal update 
cabal install hastache

Про Mustache вообще я уже написал, повторятся не буду, сразу перейду к Хаскелю.

Простейший пример использования

1
2
3
4
5
6
7
8
9
10
11
12
13
module Main where 

import Text.Hastache
import Text.Hastache.Context
import qualified Data.ByteString.Lazy as LZ

main = do
res <- hastacheStr defaultConfig (encodeStr template)
(mkStrContext context)
LZ.putStrLn res
where
template = "Привет, {{name}}!"
context "name" = MuVariable "Сергей"

Результат:

Привет, Сергей! 

О форматах представления строк

В Hastache используются ByteString. Причем и обычные и Lazy. По обычным удобно искать, поэтому ими представлена исходная строка шаблона. Результатом работы является Lazy ByteString.

Внутри hastache для построения результата использует библиотека blaze-builder. Она обеспечивает эффективное построение Lazy ByteString из большого количество коротких фрагментов. Blaze-builder позволяет как быстро строить ByteString, так и быстро передавать получившийся результат по сети (или записывать в файл). Это достигается за счет того, что библиотека следит за минимальным размером каждого фрагмента ByteString и не позволяет им быть слишком короткими. С помощью функций hastacheStrBuilder и hastacheFileBuilder можно непосредственно получить объект Builder библиотеки blaze-builder и использовать его нужным вам образом (например изменить минимальный размер фрагмента ByteString).

Hastache предполагает, что все ByteString закодированы в Utf-8 и предоставляет функции кодирования и декодирования String в ByteString Utf-8 и обратно.

Контекст

Можно условно выделить два пути передачи контекста в шаблонизатор. Первый — «собрать» все данные в некий пакет (например в хеш-таблицу) и отдать его шаблонизатору чтоб он с ним разбирался. Это отлично подходит для динамических языков типа JavaScript или Python, однако в Haskell какого-то общепринятого метода собрать разные данные в кучку нет (методов, конечно, полно, но для разных применений будут удобны разные методы). Поэтому, немного подумав, я решил пойти по второму пути — пусть контекстом будет функция, которая на вход будет получать имя переменной, а на выходе у неё будет значение (строка например) с которой шаблонизатор может что-то сделать (скажем, вывести в результирующий документ). В результате, способ хранения переменных контекста никак не ограничивается и программист может сам выбрать самый удобные для себя способ.

В Hastache контекстом является функция типа:

MonadIO m => ByteString -> MuType m 

У MuType есть следующие конструкторы:

  • MuVar a => MuVariable a — Любая переменная, для которой определен класс MuVar (в библиотеке hastache этот класс определен для типов String, Int, Double и т.д., полный список в документации).
  • MuList [MuContext m] — Список контекстов для отображения секции шаблона для каждого элемента этого списка.
  • MuBool Bool — Булево значение, используется для показа или скрытия секции.
  • MuVar a => MuLambda (ByteString -> a) — Функция, получающая на вход содержимое секции в виде ByteString и возвращающая его после какого-либо преобразования.
  • MuVar a => MuLambdaM (ByteString -> m a) — Как предыдущий конструктор, но функция выполняется в той монаде, из которой вызвали Hastache.
  • MuNothing — Знак того, что переменная с таким именем в контексте не найдена. Встретив такое значение, Hastache попытается повторить поиск в родительском контексте (например из контекста внутри MuList [..] будет произведен поиск в родительском для этого MuList [..] контексте). Если переменная не будет найдена, то вместо соответствующего тега будет подставлена пустая строка.

Немного примеров.


Работа со списком:

main = do 
res <- hastacheStr defaultConfig (encodeStr template)
(mkStrContext context)
LZ.putStrLn res
where
template = concat [
"{{#heroes}}\n",
"* {{name}} \n",
"{{/heroes}}\n"]
context "heroes" = MuList (map (mkStrContext . mkListContext)
["Безымянный","Небо","Сломанный Меч","Летящий Снег","Цинь Шихуанди"])
mkListContext name = \"name" -> MuVariable name

Результат:

* Безымянный  
* Небо
* Сломанный Меч
* Летящий Снег
* Цинь Шихуанди


Вызов функции:

main = do  
res <- hastacheStr defaultConfig (encodeStr template)
(mkStrContext context)
LZ.putStrLn res
where
template = "{{#reverse}}Привет!{{/reverse}}"
context "reverse" = MuLambda (reverse . decodeStr)

Результат:

!тевирП 

В этом примере используется функция decodeStr (ByteString -> String), она нужна для преобразования ByteString в String с корректной обработкой Utf-8 (есть обратная ей функция encodeStr).


Вызов монадической функции:

import Control.Monad.Writer 

writerFunc :: WriterT [String] IO LZ.ByteString
writerFunc = do
res <- hastacheStr defaultConfig (encodeStr template)
(mkStrContext context)
return res
where
template = "{{#p}}Нео{{/p}}, {{#p}}Тринити{{/p}}, {{#p}}Морфеус{{/p}}\n"
context "p" = MuLambdaM $ \s -> do
tell $ [decodeStr s]
return s

main = do
(res, writerRes) <- runWriterT writerFunc
LZ.putStrLn res
putStrLn "Накопленный результат монады Writer:\n"
mapM_ putStrLn writerRes

Результат:

Нео, Тринити, Морфеус 

Накопленный результат монады Writer:

Нео
Тринити
Морфеус

В этом примере Hastache вызывается из монады WriterT и в процессе работы в эту монаду собираются данные из исходного шаблона.

Конструктор MuLambdaM полезен, если необходимо вызвать IO-функцию над содержимым секции шаблона, или как-то накопить данные из шаблона, или когда преобразование зависит от каких-либо внешних условий, и так далее для всех возможных применений монад.


Использование переменных из родительского контекста:

main = do  
res <- hastacheStr defaultConfig (encodeStr template)
(mkStrContext context)
LZ.putStrLn res
where
template = concat $ map (++ "\n") [
"Текущий язык: {{#isRu}}Русский{{/isRu}}{{^isRu}}English{{/isRu}}\n",
"{{#words}}", -- Вложенный блок
"{{#isRu}}", -- isRu - переменная из родительского контекста
"{{ruWord}}",
"{{/isRu}}",
"{{^isRu}}",
"{{enWord}}",
"{{/isRu}}",
"{{/words}}"]
context "isRu" = MuBool True -- поставьте False для английских слов
context "words" = MuList (map (mkStrContext . mkListContext)
[("Hello","Привет"),("Bye","Пока")])
mkListContext (enw, ruw) = \var -> case var of
"enWord" -> MuVariable enw
"ruWord" -> MuVariable ruw
_ -> MuNothing

Результат:

Текущий язык: Русский 

Привет
Пока

Если вы используете вложенные контексты, может возникнуть ситуация когда нужно получить доступ к какому-либо родительскому контексту из вложенного. К примеру, это может быть некий глобальный флаг. Для этого существует конструктор MuNothing, который ваши функции-контексты должны возвращать встретив неизвестную переменную. Увидев MuNothing, hastache попытается найти эту же переменную в контексте уровнем выше.

Автоматическое создание контекста из типа с указанными именами полей

Один из очевидных методов собирания данных вместе в Haskell — это создание нового типа данных. При этом если в конструкторе этого типа используются именованные поля, то можно воспользоваться функцией-хелпером mkGenericContext которая автоматически создаст контекст для этих данных.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{-# LANGUAGE DeriveDataTypeable #-} 
module Main where

import Text.Hastache
import Text.Hastache.Context
import qualified Data.ByteString.Lazy as LZ
import Data.Data
import Data.Generics

data Book = Book {
title :: String,
publicationYear :: Integer
} deriving (Data, Typeable, Show)

data Life = Life {
born :: Integer,
died :: Integer
} deriving (Data, Typeable, Show)

data Writer = Writer {
name :: String,
life :: Life,
books :: [Book],
links :: [String]
} deriving (Data, Typeable, Show)

db = Writer {
name = "Александр Сергеевич Пушкин",
life = Life 1799 1837,
books = [
Book "Руслан и Людмила" 1820,
Book "Евгений Онегин" 1832,
Book "Дубровский" 1833],
links = [
"http://www.pushkinmuseum.ru",
"http://www.aleksandrpushkin.net.ru",
"http://pushkin-art.ru"
]
}

main = do
res <- hastacheStr defaultConfig (encodeStr template)
(mkGenericContext db)
LZ.putStrLn res
where
template = concat [
"Имя: {{name}} ({{life.born}} - {{life.died}})\n",
"Годы жизни:\n",
"{{#life}}\n",
" Родился: {{born}}\n",
" Умер: {{died}}\n",
"{{/life}}\n",
"Список книг:\n",
"{{#books}}\n",
" {{title}} ({{publicationYear}})\n",
"{{/books}}\n",
"Интересные ссылки:\n",
"{{#links}}\n",
" {{.}}\n",
"{{/links}}\n"
]

Результат:

Имя: Александр Сергеевич Пушкин (1799 - 1837) 
Годы жизни:
Родился: 1799
Умер: 1837
Список книг:
Руслан и Людмила (1820)
Евгений Онегин (1832)
Дубровский (1833)
Интересные ссылки:
http://www.pushkinmuseum.ru
http://www.aleksandrpushkin.net.ru
http://pushkin-art.ru

Если одним из полей является другой тип с указанными именами полей, то к элементам этого типа можно обратится двумя путями: разделив поля точкой (строка 47), либо создав отдельную секцию (сроки 49-52).

Для элементов списка из простых типов генерируется контекст, в котором к элементу можно обратится как {{.}} (строка 59).

Для создания контекста можно использовать типы данных с полями-функциями String -> String, ByteString -> ByteString, String -> m String и т.д. (m — монада из которой вызывается hastache, полный список поддерживаемых полей-функций есть в документации к Text.Hastache.Context):

data WithFunc = WithFunc {   
upperFunc :: String -> String,
reverseFunc :: Data.ByteString.ByteString -> Data.ByteString.ByteString,
upperFuncIO :: String -> IO String,
reverseFuncIO :: Data.ByteString.ByteString ->
IO Data.ByteString.ByteString
} deriving (Data, Typeable)

main = do
res <- hastacheStr defaultConfig (encodeStr template)
(mkGenericContext withFunc)
Data.ByteString.Lazy.putStrLn res
where
withFunc = WithFunc {
upperFunc = map Data.Char.toUpper,
reverseFunc = Data.ByteString.reverse,
upperFuncIO = \s -> do
putStrLn $ "вызов upperFuncIO"
return $ map Data.Char.toUpper s,
reverseFuncIO = \s -> do
putStrLn $ "вызов reverseFuncIO"
return $ Data.ByteString.reverse s
}
template = concat [
"{{#upperFunc}}Haskell{{/upperFunc}}\n",
"{{#reverseFunc}}Haskell{{/reverseFunc}}\n",
"{{#upperFuncIO}}Haskell IO{{/upperFuncIO}}\n",
"{{#reverseFuncIO}}Haskell IO{{/reverseFuncIO}}\n"
]

Результат:

вызов upperFuncIO 
вызов reverseFuncIO
HASKELL
lleksaH
HASKELL IO
OI lleksaH

Автоматически созданные контексты также поддерживают поиск в родительских контекстах.

Конфигурация

Hastache можно конфигурировать с помощью типа MuConfig, имеющего следующие поля:

  • muEscapeFunc :: ByteString -> ByteString — escape функция для преобразования управляющих символов. В стандартной конфигурации работает с HTML символами. Можно написать свою для других языков, либо вообще отключить, указав emptyEscape.
  • muTemplateFileDir :: Maybe FilePath — путь для поиска файлов, включенных в шаблон (через {{> имя_файла}}). Если Nothing, то текущая директория. В стандартной конфигурации — Nothing.
  • muTemplateFileExt :: Maybe String — расширение, добавляемое к именам включаемых файлов. В стандартной конфигурации — Nothing.

Ограничения

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

Заключение

Описал практически все возможности Hastache, за дополнительной информацией обращайтесь к документации (да, я знаю что она может быть лучше) и смотрите исходники.

Я обещал выложить части из которых состоит мой блог, и на мой взгляд, сейчас выложил самую важную. Поэтому, если кто-то хочет сделать себе небольшой сайтик на Haskell, может уже приступать, остальное все просто :) .

За поддержкой и с багрепортами обращайтесь по email, в комментарии к этой записи, или на GitHub.

blog comments powered by Disqus
Сергей Лымарь © 2005-2011, Все права защищены. Сайт реализован на языке Haskell