Последние изменения 24.10.2021
Сегодня разбавим решения задач с Project Euler статьёй о том, как можно приготовить блог с использованием таких популярных языков, как Haskell, Ruby и Javascript. Выясним, почему AsciiDoc круче, чем Markdown, а Hakyll на голову выше Jekyll.
AsciiDoc vs Markdown
Обычно я пишу про что-то связанное с математикой, и для этого мне нужна поддержка формул. Markdown заставляет экранировать латеховские бэкслеши в начале команд, что неудобно. В первую очередь это связано с тем, что markdown предназначен для написания простых в плане оформления текстов со ссылками и картинками. Он просто не расчитан на такое использование.
Ещё одна проблема markdown в том, что его проблематично экспортировать в человекочитаемый LaTeX. Конечно можно через pandoc, но вычищать его потом можно замучиться. А мне хочется ещё и pdf версии статей делать с его помощью.
В то же самое время существует формат AsciiDoc, в базовых конструкциях слабо отличающийся по синтаксису от markdown, но при этом поддерживающий больше возможностей и расширяемый. Из потрясающих возможностей, доступных сразу:
-
математические формулы
-
оглавление
-
директивы препроцессора, позволяющие делать условную компиляцию документа
-
введение и приложения
-
сноски
-
пользовательские типы блоков и макросы
Захотелось мне странного — попробовать AsciiDoc вместо markdown для написания постов в блоге. А в Jekyll это сделать оказалось не так-то просто. Да, есть плагин jekyll-asciidoc, но он не поддерживается Github Pages, поэтому сайт придётся собирать локально или использовать Github Actions. А удобство Jekyll именно в том, что не нужно делать лишних действий — запушил код на Github и сайт сам пересобрался.
Если сайт в любом случае теперь придётся собирать самому, то можно попробовать любой другой генератор статических сайтов. Давно присматривался к hakyll, поэтому решил остановиться на нём.
Hakyll
Hakyll — это скорее библиотека для построения статического сайта, чем утилита. Вместо декларативной конфигурации сайта в YAML, как это происходит в случае Jekyll, здесь ты сам описываешь процедуру построения сайта от начала и до конца при помощи DSL на базе Haskell. Это позволяет очень гибко настраивать различные аспекты процесса и добавлять новую функциональность, дописывая недостающие функции прямо в конфигурационном файле.
Стандартный вариант, генерируемый командой hakyll-init
выглядит так:
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid (mappend)
import Hakyll
--------------------------------------------------------------------------------
main :: IO ()
main = hakyll $ do
match "images/*" $ do
route idRoute
compile copyFileCompiler
match "css/*" $ do
route idRoute
compile compressCssCompiler
match (fromList ["about.rst", "contact.markdown"]) $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/default.html" defaultContext
>>= relativizeUrls
match "posts/*" $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
create ["archive.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
let archiveCtx =
listField "posts" postCtx (return posts) `mappend`
constField "title" "Archives" `mappend`
defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/archive.html" archiveCtx
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
>>= relativizeUrls
match "index.html" $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "posts/*"
let indexCtx =
listField "posts" postCtx (return posts) `mappend`
defaultContext
getResourceBody
>>= applyAsTemplate indexCtx
>>= loadAndApplyTemplate "templates/default.html" indexCtx
>>= relativizeUrls
match "templates/*" $ compile templateBodyCompiler
--------------------------------------------------------------------------------
postCtx :: Context String
postCtx =
dateField "date" "%B %e, %Y" `mappend`
defaultContext
За генерацию html-страниц с постами отвечают вот эти строчки
match "posts/*" $ do
route $ setExtension "html"
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
Как видно, hakyll использует pandoc для преобразования форматов. Но сгенерировать html из asciidoc он не в состоянии, поэтому пришлось написать свою функцию для компиляции. В составе hakyll присутствует функция unixFilter, которая позволяет запустить процесс и передать ему на стандартный поток ввода содержимое файла
htmlCompiler :: Compiler (Item String)
htmlCompiler = do
getResourceString >>= withItemBody (unixFilter "asciidoctor-latex"
["-b"
, "html5"
, "-s"
, "-a"
, "embedded"
, "-a"
, "skip-front-matter"
, "-"])
...
match "posts/*" $ do
route $ setExtension "html"
compile $ htmlCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
Здесь во втором аргументе unixFilter передаётся список параметров командной строки для asciidoctor-latex
, а именно:
-
-b html5
указывает, что нужно конвертировать asciidoc в html5 -
-s
не добавляет собственных asciidoctorовских хедера и футера, только конвертирует содержимое -
-a embedded
не печатает заголовок, это отдано на откуп шаблонам hakyll -
-a skip-front-matter
игнорирует front-matter с метаданными для hakyll в начале файла -
-
заставляет читать со стандартного потока ввода
Далее мне захотелось, чтобы материалы, относящиеся к посту (картинки, код) лежали в каталоге этого поста. В результате получилось следующее
htmlCompiler :: Compiler (Item String)
htmlCompiler = do
path <- getResourceFilePath
let (dir,_) = splitFileName path
getResourceString >>= withItemBody (unixFilter "asciidoctor-latex"
["-b"
, "html5"
, "-B"
, dir
, "-s"
, "-a"
, "embedded"
, "-a"
, "skip-front-matter"
, "-"])
...
match "posts/*/index.adoc" $ do
route $ setExtension "html"
compile $ htmlCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
match "posts/**/*" $ do
route idRoute
compile copyFileCompiler
Ну и чтобы избавиться от index.html
в конце ссылок
removeIndexHtml :: Item String -> Compiler (Item String)
removeIndexHtml item = return $ fmap (withUrls removeIndexStr) item
where
removeIndexStr :: String -> String
removeIndexStr url = case splitFileName url of
(dir, "index.html") | isLocal dir -> dir
_ -> url
isLocal :: String -> Bool
isLocal uri = not (isInfixOf "://" uri)
...
match "posts/*/index.adoc" $ do
route $ setExtension "html"
compile $ htmlCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
>>= removeIndexHtml
LaTeX
Сам asciidoctor не позволяет генерировать latex, но это можно сделать с помощью расширения asciidoctor-latex. Для лучшей поддержки широких возможностей latex, он привносит конструкцию env для окружений:
[env.environment]
--
Тут что-то по-латеховски
--
что при компиляции в latex превратится в
\begin{environment}
Тут что-то по-латеховски
\end{environment}
Так, для формул он предоставляет блок env.equation
:
[env.equation]
--
\lim_{x \to 0} \frac{\sin x}{x} = 1
--
Для рендеринга формул в html я использую KaTeX, пока на стороне браузера с использованием js, но в планах довести до ума asciidoctor-latex
, чтобы делать это во время построения сайта.
Тёмная тема
Для поддержки тёмной темы в вебе пришлось использовать svg для графиков. Для этого их приходится вставлять напрямую в html. В тоже время, этот подход не подойдёт для других форматов, например, для pdf. Там графику нужно вставить картинкой. Asciidoc позволяет решить эту проблему конструкциями ifdef
и ifndef
:
ifdef::backend-html5[]
++++
include::time_improved.svg[]
++++
endif::[]
ifndef::backend-html5[]
image::time_improved.png[]
endif::[]
В данном случае, при компиляции в html вставится содержимое файла time_improved.svg
, а при любом другом формате вывода будет добавлено изображение time_improved.png
.
Заключение
Основным недостатком данного способа является использование заброшенного сообществом asciidoctor-latex.