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

Март 28, 2011, 07:00

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

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

cabal update
cabal install hastache

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

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

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 "Сергей"

Ре­зуль­тат:

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

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

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

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

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

Кон­текст

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

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

MonadIO m => ByteString -> MuType m

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

  • Mu­Var a => Mu­Vari­able a — Лю­бая пе­ре­мен­ная, для ко­то­рой опре­де­лен класс Mu­Var (в биб­лио­те­ке has­tache этот класс опре­де­лен для ти­пов String, Int, Dou­ble и т.д., пол­ный спи­сок в до­ку­мен­та­ции).
  • MuList [Mu­Con­text m] — Спи­сок кон­тек­стов для отоб­ра­же­ния сек­ции шаб­ло­на для каж­до­го эле­мен­та это­го спис­ка.
  • Mu­Bool Bool — Бу­ле­во зна­че­ние, ис­поль­зу­ет­ся для по­ка­за или скры­тия сек­ции.
  • Mu­Var a => Mu­Lamb­da (ByteString -> a) — Функ­ция, по­лу­ча­ю­щая на вход со­дер­жи­мое сек­ции в виде ByteString и воз­вра­ща­ю­щая его по­сле ка­ко­го-либо пре­об­ра­зо­ва­ния.
  • Mu­Var a => Mu­Lamb­daM (ByteString -> m a) — Как преды­ду­щий кон­струк­тор, но функ­ция вы­пол­ня­ет­ся в той мо­на­де, из ко­то­рой вы­зва­ли Has­tache.
  • MuNoth­ing — Знак того, что пе­ре­мен­ная с та­ким име­нем в кон­тек­сте не най­де­на. Встре­тив та­кое зна­че­ние, Has­tache по­пы­та­ет­ся по­вто­рить по­иск в ро­ди­тель­ском кон­тек­сте (на­при­мер из кон­тек­ста внут­ри 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

Ре­зуль­тат:

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

Та­к­же к эле­мен­там спис­ка мож­но об­ра­тит­ся по их но­ме­ру (на­при­мер heroes.0.name). Смот­ри­те при­мер как это де­ла­ет­ся.


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

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

Ре­зуль­тат:

!тевирП

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


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

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:

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

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

Кон­струк­тор Mu­Lamb­daM по­ле­зен, если необ­хо­ди­мо вы­звать 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

Ре­зуль­тат:

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

Привет
Пока

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

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

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

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 — мо­на­да из ко­то­рой вы­зы­ва­ет­ся has­tache, пол­ный спи­сок под­дер­жи­ва­е­мых по­лей-функ­ций есть в до­ку­мен­та­ции к Text.Has­tache.Con­text):

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

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

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

Has­tache мож­но кон­фи­гу­ри­ро­вать с по­мо­щью типа Mu­Con­fig, име­ю­ще­го сле­ду­ю­щие поля:

  • muEscape­Func :: ByteString -> ByteString — es­cape функ­ция для пре­об­ра­зо­ва­ния управ­ля­ю­щих сим­во­лов. В стан­дарт­ной кон­фи­гу­ра­ции ра­бо­та­ет с HTML сим­во­ла­ми. Мож­но на­пи­сать свою для дру­гих язы­ков, либо во­об­ще от­клю­чить, ука­зав emp­tyEscape.
  • muTem­plate­FileDir :: Maybe FilePath — путь для по­ис­ка фай­лов, вклю­чен­ных в шаб­лон (че­рез {{> имя_фай­ла}}). Если Noth­ing, то те­ку­щая ди­рек­то­рия. В стан­дарт­ной кон­фи­гу­ра­ции — Noth­ing.
  • muTem­plate­File­Ext :: Maybe String — рас­ши­ре­ние, до­бав­ля­е­мое к име­нам вклю­ча­е­мых фай­лов. В стан­дарт­ной кон­фи­гу­ра­ции — Noth­ing.

Огра­ни­че­ния

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

За­клю­че­ние

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

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

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

blog comments powered by Disqus
Сергей Лымарь © 2005-2014, Все права защищены.