Показаны сообщения с ярлыком restructuredtext. Показать все сообщения
Показаны сообщения с ярлыком restructuredtext. Показать все сообщения

воскресенье, 2 мая 2010 г.

Парсим reStructuredText на Common Lisp. Часть 2..

В предыдущей части я показал, как можно очень легко парсить простые элементы. Но в размеке reStructuredText также имеются сложные элементы, простейшим из которых является Section, вот несколько примеров:
=============
Hello world
=============

Hello world
-----------

Hello world
!!!!!!!!!!!
Секция обозначается с помощью "украшающей" линии, которая должна быть под заголовком секции. Также, "украшающая" линия может быть дополнительно и над заголовков. Если имеется только нижняя декорация, то количество символов в ней должно совпадать с количеством символов в названии секции, а само название не может начинаться с пробельного символа. Если есть и нижняя и верхняя декорация, то в начале названия можно вставлять пробельные символы, но длина заголовка должна быть меньше длины декорации. Верхняя (если есть) декорация должна совпадать с нижней. Декорация должна состоять из одинаковых символов, разрешены следующие символы: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ . В общем, как-то так :)

Ядро wiki-parser - это чуть более 200 строк кода, которые обеспечивают конечный автомат, почти полностью управляемый с помощью регулярных выражений. Код несколько запутан, но концептуально это очень простая схема, позволяющая легко описывать разметку, например, подобную разметке dokuwiki. Однако, statemashin.py (из docutils) это значительно более сложная схема, которая допускает значительно большую гибкость, но и требует значительно больших усилий по программированию. И связано это с необходимостью обрабатывать конструкции, подобные описанным Section, а в разметке reStructuredText есть ещё, например, куда более сложные таблицы. Ключевая проблема - невозможность описания подобной разметки на основе регулярных выражений.

Однако, тут следует сделать оговорку, что нельзя составить необходимое регулярное выражение в Perl или Python, но библиотека cl-ppcre поддерживает фантастическую возможность - фильтры.

cl-ppcre имеет два варианта описания регулярных выражений - традиционный с помощью строки в стиле Perl, и вариант описания на основе s-выражений. Фактически, строковое представление всегда сначала парсится именно в форму s-выражений. Синтаксис символьной формы можно узнать с помощью функции ppcre:parse-string, например для тэга :entry в описании strong-элемента (см. предыдущую часть) получается следующее:
CL-USER> (ppcre:parse-string "\\*\\*(?=.*\\*\\*)")
(:SEQUENCE "**"
(:POSITIVE-LOOKAHEAD (:SEQUENCE (:GREEDY-REPETITION 0 NIL :EVERYTHING) "**")))
Эта возможность используется в ядре wiki-parser для построения "большого" регулярного выражения (оперировать s-выражениями проще, чем строками), также я порой использую эту возможность, когда затрудняюсь описать нужное мне в строковом виде (обычно, строковое представление удобней, но s-форма допускает большую гибкость). А ещё - s-представление даёт возможность указать произвольную функцию, которая будет работать как часть регулярного выражения. Т.е. когда нельзя написать традиционное регулярное выражение для описания синтаксиса, всегда можно указать произвольную функцию, которая будет производить необходимые проверки!. Для обработки секций я написал следующий код:
(defparameter +section-adornment+
'(#\! #\" #\# #\$ #\% #\&
#\' #\( #\) #\* #\+ #\,
#\- #\. #\/ #\: #\; #\<
#\= #\> #\? #\@ #\[ #\\
#\] #\^ #\_ #\` #\{ #\|
#\} #\~))

(defun regex-section-filter (pos)
(ignore-errors
(flet ((check-condition (flag)
(unless flag (return-from regex-section-filter nil))))
(let ((adornment nil)
(title nil)
(overline-p nil))
(check-condition (char= #\Newline
(char ppcre::*string* pos)))
(incf pos)
(check-condition (null (char= #\Newline
(char ppcre::*string* pos))))
(when (member (char ppcre::*string* pos)
+section-adornment+)
(let ((pos2 (position (char ppcre::*string* pos)
ppcre::*string*
:start (1+ pos)
:test-not #'char=)))
(check-condition (char= #\Newline
(char ppcre::*string* pos2)))
(setf adornment (subseq ppcre::*string* pos pos2)
overline-p t
pos (+ pos2 1))))
(unless overline-p
(check-condition (let ((sp (ppcre:scan "\\s" title)))
(or (null sp)
(> sp 0)))))
(let ((pos2 (position #\Newline
ppcre::*string*
:start pos
:test #'char=)))
(setf title
(subseq ppcre::*string* pos pos2))
(setf pos
(+ pos2 1)))
(let ((pos2 (position #\Newline
ppcre::*string*
:start pos
:test #'char=)))
(cond
(adornment (check-condition (string= adornment
(subseq ppcre::*string* pos pos2))))
(t (check-condition (> pos2 pos))
(setf adornment
(subseq ppcre::*string* pos pos2))
(check-condition (member (char adornment 0)
+section-adornment+))
(check-condition (null (find (char adornment 0)
adornment
:test-not #'char=)))))
(setf pos pos2))
(check-condition (if overline-p
(<= (length title)
(length adornment))
(= (length title)
(length adornment))))
pos))))

(define-mode section (30 :sections)
(:special (:filter regex-section-filter))
(:post-handler (item)
(let* ((lines (cdr (ppcre:split "\\n" (second item))))
(decoration (car (last lines)))
(overline-p (= (length lines) 3))
(title (if overline-p
(second lines)
(first lines))))
(list 'section
(trim-whitespaces title)
(char decoration 0)
overline-p))))
Функция regex-section-filter несколько нудновата, но главное это работает:
CL-USER> (wiki-parser:parse :re-structured-text
"**strong**
=============
Hello world
=============
*em*")
(WIKI-PARSER.RE-STRUCTURED-TEXT:TOPLEVEL
(WIKI-PARSER.RE-STRUCTURED-TEXT:STRONG "strong"))
(WIKI-PARSER.RE-STRUCTURED-TEXT:SECTION "Hello world" #\= T)
(WIKI-PARSER.RE-STRUCTURED-TEXT:EMPHASIS "em")))
Здесь распарсенный элемент WIKI-PARSER.RE-STRUCTURED-TEXT:SECTION имеет название, символ декорации и признак того, что имеется верхняя декорация.

Таким образом, при сохранении предельной простоты для описания тривиальных элементов имеется возможность и для обработки весьма сложной разметки (а код для обработки подобных элементов будет достаточно сложен при любом подходе). Эта схема кажется мне наиболее простой, как в понимании, так и в кодировании и тех, что я видел для обработки подобных разметок.

пятница, 30 апреля 2010 г.

Парсим reStructuredText на Common Lisp. Часть 1.

Читая регулярно Russian Lambda Planet можно увидеть описания всевозможных грамматик, парсер-комбинаторов (или как там они называются) и прочих вещей, о которых мне даже думать страшно. Впрочем, наверно не одному мне, ибо в реальных проектах на php и python, которые я просматривал, ничего подобного не используется, а используется весьма простой и эффективный метод, основанных на регулярных выражениях и "машине состояний", позволяющий удобно сочетать описание грамматики с помощью рег. выражений с вставками произвольного кода. Мне, кстати, попадалось утверждения (которое я не могу не подтвердить, не опровергнуть, ибо абсолютно не разбираюсь в этих вопросе), что все эти грамматики бесполезны для разбора wiki-разметок. Так вот, поскольку у меня есть проблемы с математическим образование, то приходиться парсить по "быдлокодерски" ;)

Почему reStructuredText
Я имею большое желание начать писать мануал для RESTAS, но в мире Common Lisp нет инструмента для документирования, который бы меня устроил. С другой стороны, мне очень нравиться как сделана документация на Django, да и вообще, в мире Python с документированием всё нормально. Документация к Django основана на Sphinx, который использует reStructuredText, поддержку которого обеспечивает пакет docutils. А почему бы не реализовать соответствующий функционал для Common Lisp? Правда, я нашёл пакет cl-docutils, но его размер (около 10 000 строк кода) меня сильно смутил - ну зачем может потребоваться столько строк кода для решения подобной задачи??? Так что решил делать сам (у меня всяко столько кода не будет).

Моя система
Я уже пару раз рассказывал, что мой подход (библиотека wiki-parser) к парсингу (который я опробовал в restas-wiki и cl-closure-template) основан на подходе, подсмотренном в исходниках Dokuwiki, который я правда переработал с учётом возможностей Common Lisp, так что мне кажется, что он стал значительно мощнее. Итак, сначала объявим парсер:
(wiki-parser:define-parser #:wiki-parser.re-structured-text
(:use #:cl #:iter)
(:export #:paragraph))

(in-package #:wiki-parser.re-structured-text)

(define-toplevel-mode
(:allowed :formatting :sections :paragraphs))

(defmethod wiki-parser:parse ((markup (eql :re-structured-text)) (obj string))
(wiki-parser:parse #.*package* obj)))
Макрос define-parser создаёт новый пакет и проводит некоторую его инициализацию - данный подход, когда пакет используется в качестве объекта, я использую также и в RESTAS - это позволяет легко разбить описания синтаксиса на множество отдельных форм (вместо этих огромных простыней, которые можно наблюдать, например, в коде на базе cl-yacc).

Макрос define-toplevel-mode совершенно необходим и определяет какие режимы доступны в начальном состоянии, смысл их стане ясен чуть позже.

Метод wiki-parser:parse используется для непосредственного разбора текста.

Теперь, опередить правила для разбора, например, strong-элемента (например, "*Hello world*") можно чрезвычайно просто:
(define-mode strong (80 :formatting)
(:allowed :formatting)
(:not-allowed strong)
(:entry "\\*\\*(?=.*\\*\\*)")
(:exit "\\*\\*"))

CL-USER> (wiki-parser:parse :re-structured-text
"**Hello world**")
(WIKI-PARSER.RE-STRUCTURED-TEXT:TOPLEVEL
(WIKI-PARSER.RE-STRUCTURED-TEXT:STRONG "Hello world"))
Напомню, что парсер обеспечивает машину состояний и здесь макрос define-mode определяет режим для разбора strong-режима. Его параметры означают следующее
  • strong - название режима. Во-первых, оно фигурирует в результате разбора (как можно увидеть в примере), во-вторых - может использоваться при описании других режимов.
  • 80 - приоритет, регулярные выражения для всех определённых режимов собираются в одно регулярное выражение и с помощью приоритета можно (нужно) управлять очередность проверок.
  • :formatting - означает, что данный режим входит в группу :formatting. Например, выше в макросе define-toplevel-mode я указывал разрешённые режимы в начальном состоянии.
  • (:allowed :formatting) - означает, что внутри данного режима разрешены режимы из группы :formatting. Можно указать несколько групп или отдельных режимов. Легко заметить, что группы обозначается с помощью :keyword, а отдельный режим простым символом
  • (:not-allowed strong) - исключает режим strong из списка разрешённых состояний внутрирежима.
  • (:entry "\\*\\*(?=.*\\*\\*)") - регулярное выражение, позволяющее найти начало режима и убедиться, что он имеет правильное завершение
  • (:exit "\\*\\*") - регулярное выражения, определяющее завершение режима
Мне кажется, что всё предельно просто.


В разметках часто встречаются элементы, которые не имеют начального и завершающего "признака", например, надо опознавать простые гиперссылки. Это можно следующим образом:
(define-mode standalone-hyperlink (330 :formatting)
(:special "(?:ht|f)tp(?:s?)://[0-9a-zA-Z](?:[-.\\w]*[0-9a-zA-Z])*(?::(?:0-9)*)*(?:/?)(?:[a-zA-Z0-9\\-\\.\\?\\,/\\+&%\\$#\\*]*)?"))
Здесь тэг :special используется для описания регулярного выражения, которое позволяет "выкусить" кусок текста целиком:
CL-USER> (wiki-parser:parse :re-structured-text
"http://www.lisp.org/")
(WIKI-PARSER.RE-STRUCTURED-TEXT:TOPLEVEL
(WIKI-PARSER.RE-STRUCTURED-TEXT:STANDALONE-HYPERLINK "http://www.lisp.org/"))
В разметке reStructuredText широко используются ссылки, которые формируются следующим образом: Python_ или `Common Lisp`_. Здесь видно, в первом варианте отсутствует признак начала режима. Для разбора подобных элементов (в разметке reStructuredText используется несколько вариантов ссылок) я используют такой код:
(defun trim-backquotes (str)
(let ((last (1- (length str))))
(if (char= #\`
(char str 0)
(char str last))
(subseq str 1 last)
str)))

(define-mode reference (70 :formatting)
(:special "`.*`_"
"[^\\s]+_")
(:post-handler (item)
(let* ((str (second item)))
(list 'reference
(trim-backquotes
(subseq str 0 (1- (length str))))))))
Здесь тэг :post-handler используется для описания функции, которая должна быть вызвана для финальной обработки распарсенного элемента:
CL-USER> (wiki-parser:parse :re-structured-text
"Python_")
(WIKI-PARSER.RE-STRUCTURED-TEXT:TOPLEVEL
(WIKI-PARSER.RE-STRUCTURED-TEXT:REFERENCE "Python"))

CL-USER> (wiki-parser:parse :re-structured-text
"`Common Lisp`_")
(WIKI-PARSER.RE-STRUCTURED-TEXT:TOPLEVEL
(WIKI-PARSER.RE-STRUCTURED-TEXT:REFERENCE "Common Lisp"))
После описания всех режимов в конец файла необходимо вставить вызов (init-parser), который произведёт инициализация парсера. При определении какого-либо режима в Slime с помощью C-M-x (не С-с С-с) - данный вызов будет производиться автоматически.

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