23 февр. 2012 г.

Mako, Markdown и немного Python...

... или генерируем кучу статического HTML с минимумом копипаста

Программист за два часа может написать программу, которая за две секунды сделает то, что обычный пользователь будет делать полчаса.

Задача

Недавно понадобилось сделать из большого структурированного текста HTML-документы с навигацией.

Решил сделать это с помощью шаблонов и языка разметки Markdown, потому что:

  1. Можно избежать копипаста однотипной HTML-разметки, так как общая структура у документов одинаковая.
  2. При изменении структуры, ссылок на стили и т. п. не нужно изменять много файлов.

Подход

Сходив по ссылке Templating в питоновской вики, я остановился на движке для шаблонов Mako. Из документации видно, что шаблоны Mako поддерживают наследование.

Вынесем общий для всех документов код в базовый шаблон, который затем будем наследовать в конкретных документах и переопределять нужные блоки.

Разметку внутри документов делаем с помощью Markdown для сохранения удобочитаемости.

Также для красоты тексты прогонялись через типограф студии Лебедева.

Будем использовать питоновскую библиотеку для генерации HTML из Markdown.

В качестве бонуса будем автоматически обновлять HTML-документы при изменении с помощью inotify-tools, про которые тут уже была заметка.

На всякий случай скажу, что использую Python 2-ой версии.

В Arch Linux описанные инструменты находятся в пакетах: python2-markdown, python2-markdown и inotify-tools. Библиотеки для Питона можно устанавливать также из PyPI с помощью pip или других подобных утилит.

Код

Главный скрипт updateonsave.sh, который смотрит за изменением файлов в рабочем каталоге и командует другими скриптами:

#!/bin/bash

while true; do
    inotifywait -r -e modify -e move -e create -e delete . \
        && ./site.sh 
; done

Видно, что при изменении файлов вызывается скрипт site.sh:

#!/bin/bash

find . -name '*.mako' \! -name base.mako -print0 | sed 's|[.]mako$||g' | xargs -r -0 python2 render_mako.py

Тут ищутся все файлы, оканчивающиеся на .mako (кроме base.mako, который является базовым шаблоном и из которого делать HTML не надо), из них вырезается это окончание, и оставшееся имя файла передается скрипту на Питоне render_mako.py:

from mako.template import Template
from mako.lookup import TemplateLookup
import sys
import os

lookup = TemplateLookup(directories=['.'], 
        input_encoding='utf-8',
        output_encoding='utf-8')

args = sys.argv[1:]
for tpl_URI in args:
    with open(''.join([tpl_URI, '.html']), 'w') as outfile:
        outfile.write(lookup.get_template(''.join([tpl_URI, '.mako'])).render())

Тут мы создаем вспомогательный объект класса TemplateLookup, который говорит Mako, где искать шаблоны, и задает дополнительные опции для каждого шаблона (в данном случае только кодировки для ввода и вывода). Затем получаем список аргументов, который был передан предыдущим скриптом, и записываем в файл с таким же именем как шаблон, но с расширением .html, результат обработки шаблона.

Ниже приведен пример базового шаблона base.mako, в который вынесены общие части HTML-документов и определены некоторые блоки и функции, которые будут замещены специфическими для каждого документа значениями.

<%!
    from markdown import markdown

    back = ''
    forward = ''
    index = '../index.html'
%>

<%def name="make_figure(filename, title='', width='')">
<div class="figure">
    <img src="${filename}" ${''.join(('alt="', title, '"')) if title else ''} ${''.join(('width="', width, 'px"')) if width else ''}>
    ${''.join(('<p>', title, '.</p>')) if title else ''}
</div>
</%def>

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <title><%block name="title" /></title>
</head>

<body>
<div id="container">

<%block name="nav">
%if self.attr.back or self.attr.index or self.attr.forward:
<div class="nav">
    %if self.attr.back:
    <a href="${self.attr.back}" class="nav_back">Назад</a>
    %endif
    %if self.attr.index:
    <a href="${self.attr.index}" class="nav_index">Содержание</a>
    %endif
    %if self.attr.forward:
    <a href="${self.attr.forward}" class="nav_forward">Вперед</a>
    %endif
</div>
%endif
</%block>

<div id="content">
<h1>${self.title()}</h1>

${markdown(capture(self.body))}
</div>

${self.nav()}
</div>
</body>
</html>

В итоге получится следующая структура документов:

1. Шапка HTML-документа.
2. Навигация - ссылки "вперед", "назад", "содержание", 
если хотя бы одна из этих ссылок присутствует.
3. Тело конкретного документа.
4. Дубликат навигации снизу.
5. Конец HTML-документа.

Теперь приведем пример конкретного шаблона document.mako, наследующего base.mako и поговорим, что к чему с этими шаблонами:

<%!
    back = 'document_before.html'
    forward = 'document_after.html'
%>
<%inherit file="../base.mako" />

<%block name="title">Название страницы</%block>

${parent.make_figure(u'01.jpg', u'Рисунок 1')}

Тут идет некотрый текст, в котором можно применять разметку Markdown:
    **полужирный**
    *курсив*
    [Ссылка](http://google.com)
    Список:
        * Первый пункт
        * Второй пункт
    и так далее

Пойдем с начала шаблона base.mako:

<%!
    from markdown import markdown

    back = ''
    forward = ''
    index = '../index.html'
%>

Эта часть называется в Mako module level block, это просто кусок кода на Python, который распространяется на весь шаблон. Здесь мы импортируем функцию markdown из одноименного модуля и объявляем переменные для использования в блоке навигации, которые можно переопределять в дочерних шаблонах.

<%def name="make_figure(filename, title='', width='')">
<div class="figure">
    <img src="${filename}" ${''.join(('alt="', title, '"')) if title else ''} ${''.join(('width="', width, 'px"')) if width else ''}>
    ${''.join(('<p>', title, '.</p>')) if title else ''}
</div>
</%def>

Здесь объявляется «функция» make_figure, которая служит для удобной вставки изображений. В document.mako видно, как вызывается эта функция:

${parent.make_figure(u'01.jpg', u'Рисунок 1')}

parent— обращение к родительскому шаблону. Аргументы с русскими буквами должны передаваться в виде юникодных строк, потому что для обычных строк декодер по умолчанию ascii, а файл хранится в UTF-8.

Далее по base.mako:

<title><%block name="title" /></title>

В этой части определяется пустой блок с именем title, он же используется как заголовок первого уровня у всех документов (ниже в base.mako):

<h1>${self.title()}</h1>

Дальше определен блок для ссылок с навигацией:

<%block name="nav">
%if self.attr.back or self.attr.index or self.attr.forward:
<div class="nav">
    %if self.attr.back:
    <a href="${self.attr.back}" class="nav_back">Назад</a>
    %endif
    %if self.attr.index:
    <a href="${self.attr.index}" class="nav_index">Содержание</a>
    %endif
    %if self.attr.forward:
    <a href="${self.attr.forward}" class="nav_forward">Вперед</a>
    %endif
</div>
%endif
</%block>

Здесь используются переменные, объявленные выше в module level block и производится проверки, нужно ли отображать навигацию вообще и каждую ссылку в частности.

И наконец, главная часть — тело будущего документа:

${markdown(capture(self.body))}

Здесь функции markdown, подключенной в module level block, передается переменная self.body, которой будет соответствовать тело дочернего шаблона (capture нужна для управления кешированием).

Вернемся опять к document.mako - шаблону, наследующему базовый шаблон:

<%!
    back = 'document_before.html'
    forward = 'document_after.html'
%>

В module level block сразу передопределяем ссылки на предыдущую и следующую страницы (ссылка на содержание останется прежней '../index.html').

Далее указываем, что наследуем шаблон base.mako:

<%inherit file="../base.mako" />

Затем переопределяем блок с названием страницы:

<%block name="title">Название страницы</%block>

Весь текст, который располагается ниже, и попадет в переменную self.body в базовом шаблоне, где произойдет генерация HTML-разметки на основе Markdown, и на выходе получится законченный HTML-документ:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <title>Название страницы</title>
</head>

<body>
<div id="container">

<div class="nav">
    <a href="document_before.html" class="nav_back">Назад</a>
    <a href="../index.html" class="nav_index">Содержание</a>
    <a href="document_after.html" class="nav_forward">Вперед</a>
</div>

<div id="content">
<h1>Название страницы</h1>

<div class="figure">
    <img src="01.jpg" alt="Рисунок 1" >
    <p>Рисунок 1.</p>
</div>

<p>Тут идет некотрый текст, в котором можно применять разметку Markdown:
    <strong>полужирный</strong>
    <em>курсив</em>
    <a href="http://google.com">Ссылка</a>
    Список:
        * Первый пункт
        * Второй пункт
    и так далее</p>
</div>

<div class="nav">
    <a href="document_before.html" class="nav_back">Назад</a>
    <a href="../index.html" class="nav_index">Содержание</a>
    <a href="document_after.html" class="nav_forward">Вперед</a>
</div>

</div>
</body>
</html>

Полезные ссылки:

  1. Настройка подсветки синтаксиса шаблонов Mako для Sublime text 2

Комментариев нет :

Отправить комментарий