Использование Markdown
В проекте Grammarly Handbook, про который я писал вчера, грамматические карточки из формата MS Word нужно было конвертировать в какой-то внутренний формат, чтобы в этом формате было легко добавлять новые карточки и редактировать существующие. Кроме того, нужно было ограничить функционал редактора карточек, чтобы не было чрезмерного разнообразия форматирования и, как говорит наш дизайнер, "верстка была семантической".
Markdown - разметка не сложнее чем plain text e-mail
Я уже довольно давно для подобных задач использую разметку Markdown. Его автор John Gruber описывает его так:
The overriding design goal for Markdown’s formatting syntax is to make it as readable as possible. The idea is that a Markdown-formatted document should be publishable as-is, as plain text, without looking like it’s been marked up with tags or formatting instructions. While Markdown’s syntax has been influenced by several existing text-to-HTML filters, the single biggest source of inspiration for Markdown’s syntax is the format of plain text email.
Даже эту статью я пишу в разметке markdown (proofpic ниже).
Рабочий пример
Разберем для примера карточку Comma splice.
MS Word
Вот скриншот части MS Word-документа с этой карточкой:
Markdown
Вот так я бы хотел записать такую карточку в markdown:
If two independent clauses are to be joined into one sentence, they should be separated by a conjunction or a semi-colon, or possibly even a conjunction *and* a comma. They can also be separated into two sentences by a period. Using a comma causes a comma splice.
> Koala bears are not actually bears, they are marsupials.
> I am not angry with you, I am not happy with you, either.
> I’m thinking of skipping English class, it’s really boring.
**Exceptions:**
Comma splices *can* be used for artistic or poetic effect, as when one is connecting several short independent clauses. Don’t do this in a formal composition, though; it’s only for creative writing. (If you’re going to pull this off in formal writing, try using a semi-colon.)
> She was beautiful, she was gorgeous, she was ravishing.
Comma splices may also be used if the two independent clauses are somehow contrasting, as when following a statement with a question.
> You are coming to the party, aren’t you?
Html
А вот так эта карточка должна отображаться в html:
If two independent clauses are to be joined into one sentence, they should be separated by a conjunction or a semi-colon, or possibly even a conjunction and a comma. They can also be separated into two sentences by a period. Using a comma causes a comma splice.
Koala bears are not actually bears, they are marsupials.
I am not angry with you, I am not happy with you, either.
I’m thinking of skipping English class, it’s really boring.
Exceptions:
Comma splices can be used for artistic or poetic effect, as when one is connecting several short independent clauses. Don’t do this in a formal composition, though; it’s only for creative writing. (If you’re going to pull this off in formal writing, try using a semi-colon.)
She was beautiful, she was gorgeous, she was ravishing.
Comma splices may also be used if the two independent clauses are somehow contrasting, as when following a statement with a question.
You are coming to the party, aren’t you?
Отличия в синтаксисе
По моему мнению markdown-версия более удобна для редактирования человеком, чем сырой html. Для рендеринга mardown-версии в html есть две питоновские библиотеки: python-markdown и python-markdown2. У первой лучше документация по написанию расширений, а вторая быстрее.
>>> import markdown
>>> import markdown2
>>> markdown.markdown('*Hello*')
u'Hello
'
>>> markdown2.markdown('*Hello*')
u'Hello
\n'
Я выбрал python-markdown из-за легкости написания расширений, с помощью которых можно вносить изменения в рендеринг. Синтаксис нашей карточки отличается от исходного синтаксиса markdown:
- Если цитаты (>) идут одна за одной, то они должны превращаться в отдельные теги
а не одну большую цитату с переносами строк между ними.
- Псевдо-html теги
должны превращаться в, и
соответственно. Их закрывающие теги - в. А содержащие их
должны получать классы state_ok или state_error.
Создаем расширение для markdown
Расширения для python-markdown могут содержать preprocessors (на вход подается текст в разметке markdown), inline patterns (содержат регулярные выражения, определяющие их синтаксис, используются при разборе в дерево), treeprocessors (оперируют деревом, получившимся после парсинга) и postprocessors (подправляют полученный html). Также можно написать свой парсер вместо встроенного BlockParser, в котором уже можно делать вообще все что угодно.
Начинаем писать наше маленькое расширение. По соглашению имя файла должно начинаться с mdx_, а файл должен содержать функцию makeExtension, которая создает инстанс расширения.
def makeExtension(configs=None):
return CardsExtension(configs=configs)
class CardsExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
md.preprocessors.add('split_blockquotes', SplitBlockquotes(md), '_begin')
md.treeprocessors.add('mark_blockquotes', MarkBlockquotes(md), '_begin')
md.postprocessors.add('replace_marker_tags', ReplaceMarkerTags(md), '_end')
SplitBlockquotes вставляет разделительный текст между последовательными цитатами. Этот текст мы уберем на стадии постпроцессинга. Цитаты, заканчивающиеся на два и более символа пробела пропускаем, markdown вставит там перенос строки.
BLOCKQUOTE_SPLITTER = 'blockquote_splitter_paragraph_text'
class SplitBlockquotes(markdown.preprocessors.Preprocessor):
def run(self, lines):
new_lines = []
for line in lines:
if line.startswith('>') and not line.endswith(' '):
new_line = line + '\n\n' + BLOCKQUOTE_SPLITTER + '\n\n'
else:
new_line = line
new_lines.append(new_line)
return new_lines
MarkBlockquotes добавляет в css-классы.
class MarkBlockquotes(markdown.treeprocessors.Treeprocessor):
def run(self, root):
for bq in root.findall('blockquote'):
for elem in bq.iter():
if elem.text and elem.text.find('') != -1:
bq.set('class', 'state_ok')
break
if elem.text and elem.text.find('') != -1:
bq.set('class', 'state_error')
break
ReplaceMarkerTags преобразует теги
и убирает разделительный текст между цитатами.
class ReplaceMarkerTags(markdown.postprocessors.Postprocessor):
def run(self, text):
text = re.sub('', u'', text)
text = re.sub('', u'', text)
text = re.sub('', u'', text)
text = re.sub(' ||', u'', text)
text = re.sub('' + BLOCKQUOTE_SPLITTER + '
', u'', text)
return text
Расширение готово, если мы укажем его имя при рендеринге, то получим желаемый html.
markdown.markdown(txt, extensions=['cards'])
Интеграция в Django-приложение
Мне нужно:
- Удобно рендерить карточки в html, причем желательно кешировать где-то отрендеренную версию, чтобы не увеличивать время загрузки страницы
- Редактировать карточки в настраиваемом редакторе с предварительным просмотром.
Решить эти задачи помогает django-markitup.
Модель
В django-markitup есть специальное поле MarkupField, которое добавляет в базу данных два поля - одно для markdown-версии, а второе - для html. Html-версия обновляется автоматически и как раз решает задачу кеширования.
class Card(models.Model):
...
slug = models.SlugField()
text = MarkupField()
Чтобы при рендеринге использовалось наше расширение 'cards', нужно добавить настройку MARKITUP_FILTER:
MARKITUP_FILTER = ('markdown.markdown', {'safe_mode': False, 'extensions': ['cards']})
Админка
MarkupField заменяет в админке обычную textarea на редактор markitup, который по умолчанию выглядит вот так:
Настройка внешнего вида производится через markitup sets. Я скопировал идущий в поставке set 'markdown', сделал в фотошопе красивые кнопки для цитат-примеров и тегов ok/error. Какие показывать кнопки указываем в set.js:
mySettings = {
previewParserPath: '/markitup/preview/',
onShiftEnter: {keepDefault:false, openWith:'\n\n'},
markupSet: [
{name:'Example block', key:'Q', openWith:'> '},
{name:'Inline example', key:'E', openWith:'', closeWith:' '},
{name:'Ok', key:'1', openWith:'', closeWith:' '},
{name:'Error', key:'2', openWith:'', closeWith:' '},
{separator:'---------------' },
{name:'Bold', key:'B', openWith:'**', closeWith:'**'},
{name:'Italic', key:'I', openWith:'*', closeWith:'*'},
{separator:'---------------' },
{name:'Bulleted List', openWith:'- ' },
{name:'Numeric List', openWith:function(markItUp) {
return markItUp.line+'. ';
}},
{separator:'---------------'},
{name:'Preview', call:'preview', className:"preview"}
]
}
В style.css прописываем стили для кнопок. Кнопки получают классы с индексом, начинающимся с 1.
.markItUp .markItUpButton1 a {
background-image:url(images/example.png);
width: 60px; margin-right: 10px;
}
.markItUp .markItUpButton2 a {
background-image:url(images/inline_example.png);
width: 92px; margin-right: 10px;
}
...
В settings.py добавляем путь к нашему сэту:
MARKITUP_SET = '/media/markitup_hb/set'
А для того чтобы в превью отрендеренная карточка показывалась точно такой же как на сайте, я создал шаблон markitup/preview.html (если у вас Django>=1.2.5, то для работы превью нужно разобраться с CSRF-защитой ajax-запросов, см CSRF exception for AJAX requests.):
markItUp! preview
{{ preview|safe }}
Вот так выглядит доработанный редактор в админке:
Мои личные выводы
В этом проекте markdown, python-markdown и django-markitup сослужили мне хорошую службу. Расширить синтаксис было несложно, а интеграция в джанго-приложение оказалась достойна всяческих похвал.