Это первая статья о новом ASP.NET парсере – Razor. Над которым мы работали достаточно долго, и я хотел бы рассказать читателям, как же он работает.
Razor-парсер сильно отличается от существующего ASPX-парсера. Фактически ASPX-парсер, почти полностью, построен на регулярных выражениях, потому что синтаксис достаточно простой для разбора. Razor-парсер же разделен на три компонента:
Когда я говорю “базовое представление” я подразумеваю именно основы, мы не говорим о полностью самостоятельном C# и HTML парсере. У себя в команде мы шутим, называя их “Опознователь разметки” и “Осмыслитель кода” :)
Итого на сцене Razor играет три “актера”: Парсер ядра, Парсер разметки и Парсер кода. Все трое работают вместе, чтобы разобрать документ Razor. Теперь давайте возьмем файл Razor и проведем полный обзор процедуры парсинга с использованием данных актеров. Использовать будем следующий пример:
Итак начнем сверху. Фактически парсер Razor находится в одном из состояний в любой момент парсинга: разбор разметки документа, разбор разметки блока или разбор блока кода. Первые два обрабатываются парсером разметки, а последний парсером кода. Когда парсер ядра запускается первый раз, он вызывает парсер разметки и просит его разобрать разметку документа и вернуть результат. Сейчас парсер находится в состоянии разбора разметки документа. При таком состоянии, он просто ищет символ “@”, ему не важно какие теги ему попадаются и все, что касается HTML, главная цель – “@”. Когда же он нашел @, то решает – это переключение на код или email-адрес? Данное решение основывается на символах до и после @, проверяя на валидность email-адрес. Это всего лишь стандартная процедура, присутствует последовательность проверок, чтобы произвести переключение на режим кода.
В данном случае, когда мы видим первый символ “@”, ему предшествует пробел, что не валидно для email-адреса. Так что мы точно знаем, что нужно переключится на режим кода. Парсер разметки вызывает внутри парсер кода и просит разобрать блок кода. Блок, в определении парсера Razor, в основном является единым куском кода или разметки с четким началом и завершением. Так что “foreach” в нашем случае является примером блока кода. Он начинается с символа “f” и заканчивается “}”. Парсер кода знает достаточно о С#, что бы понять это, соответственно он начинает разбор кода. Парсер кода проделывает некоторое простое отслеживание C# операторов, так что когда он доберется до “<li>”, то понимает, что тег находится в начале C#-выражения. “<li>” невозможно разместить в начале C#-выражения, так что парсер кода знает, что с этого места начинается блок разметки. Поэтому он возвращается в вызов парсера разметки, для того, чтобы разобрать блок HTML. Это создает своего рода рекурсивный пинг-понг между парсерами кода и разметки. Мы начали с разметки, далее вызвали внутри код, далее снова разметка и так далее, до тех пор, пока не получили результат всей цепочки вызовов:
(Понятное дело, я исключил из списка множество вспомогательных методов :).
Это проливает свет на фундаментальное отличие между ASPX и Razor. В ASPX файлах, вы можете думать о коде и разметке, как о двух параллельных потоках. Вы пишете разметку, потом перепрыгиваете и пишете код, потом обратно возвращаетесь и пишете разметку и т.д. Razor же файлы, как дерево. Вы пишете разметку, далее вкладываете в нее код, далее помещаете разметку в код и т.д.
Итак мы только что вызвали парсер разметки для разбора блока разметки, блок начинается с “<li>” и заканчивается на “</li>”. До тех пор, пока не найдем “</li>”, мы не решим, что блок разметки окончен. Так что, если у вас будет “}” где-то внутри “<li>” он не завершит “foreach”, так как мы не продвинулись достаточно далеко вверх по стеку.
Во время разбора “<li>”, парсер разметки видит множество символов “@”, из чего следует множество вызовов парсера кода. Таким образом стек вызова увеличивается:
Я углублюсь в детали обработки блоков позже, потому что процесс немного сложен, в итоге мы закончили с данными блоками кодами и вернулись в блок “<li>”. Далее, мы видим “</li>”, так что мы завершаем и этот блок и возвращаемся в блок “foreach”. “}” закрывает блок, так что теперь мы снова на вершине стека – документа разметки. После этого мы читаем, пока не достигнем конца файла, не найдя больше символов “@”. И вуаля! Мы разобрали данный файл!"
Я надеюсь общая структура алгоритма разбора ясна. Главное – это перестать думать, что парсер кода и разметки работают в отдельных потоках, а вместо этого конструкции располагаются одна в другой. Намекну, вдохновние мы черпали из PowerShell ;).
Razor-парсер сильно отличается от существующего ASPX-парсера. Фактически ASPX-парсер, почти полностью, построен на регулярных выражениях, потому что синтаксис достаточно простой для разбора. Razor-парсер же разделен на три компонента:
- Парсер разметки, который имеет базовое представление о HTML-синтаксисе.
- Парсер кода, который имеет базовое представление C# или VB.
- И главный “дирижер”, которые знает, как соединить два парсера вместе.
Когда я говорю “базовое представление” я подразумеваю именно основы, мы не говорим о полностью самостоятельном C# и HTML парсере. У себя в команде мы шутим, называя их “Опознователь разметки” и “Осмыслитель кода” :)
Итого на сцене Razor играет три “актера”: Парсер ядра, Парсер разметки и Парсер кода. Все трое работают вместе, чтобы разобрать документ Razor. Теперь давайте возьмем файл Razor и проведем полный обзор процедуры парсинга с использованием данных актеров. Использовать будем следующий пример:
Итак начнем сверху. Фактически парсер Razor находится в одном из состояний в любой момент парсинга: разбор разметки документа, разбор разметки блока или разбор блока кода. Первые два обрабатываются парсером разметки, а последний парсером кода. Когда парсер ядра запускается первый раз, он вызывает парсер разметки и просит его разобрать разметку документа и вернуть результат. Сейчас парсер находится в состоянии разбора разметки документа. При таком состоянии, он просто ищет символ “@”, ему не важно какие теги ему попадаются и все, что касается HTML, главная цель – “@”. Когда же он нашел @, то решает – это переключение на код или email-адрес? Данное решение основывается на символах до и после @, проверяя на валидность email-адрес. Это всего лишь стандартная процедура, присутствует последовательность проверок, чтобы произвести переключение на режим кода.
В данном случае, когда мы видим первый символ “@”, ему предшествует пробел, что не валидно для email-адреса. Так что мы точно знаем, что нужно переключится на режим кода. Парсер разметки вызывает внутри парсер кода и просит разобрать блок кода. Блок, в определении парсера Razor, в основном является единым куском кода или разметки с четким началом и завершением. Так что “foreach” в нашем случае является примером блока кода. Он начинается с символа “f” и заканчивается “}”. Парсер кода знает достаточно о С#, что бы понять это, соответственно он начинает разбор кода. Парсер кода проделывает некоторое простое отслеживание C# операторов, так что когда он доберется до “<li>”, то понимает, что тег находится в начале C#-выражения. “<li>” невозможно разместить в начале C#-выражения, так что парсер кода знает, что с этого места начинается блок разметки. Поэтому он возвращается в вызов парсера разметки, для того, чтобы разобрать блок HTML. Это создает своего рода рекурсивный пинг-понг между парсерами кода и разметки. Мы начали с разметки, далее вызвали внутри код, далее снова разметка и так далее, до тех пор, пока не получили результат всей цепочки вызовов:
(Понятное дело, я исключил из списка множество вспомогательных методов :).
Это проливает свет на фундаментальное отличие между ASPX и Razor. В ASPX файлах, вы можете думать о коде и разметке, как о двух параллельных потоках. Вы пишете разметку, потом перепрыгиваете и пишете код, потом обратно возвращаетесь и пишете разметку и т.д. Razor же файлы, как дерево. Вы пишете разметку, далее вкладываете в нее код, далее помещаете разметку в код и т.д.
Итак мы только что вызвали парсер разметки для разбора блока разметки, блок начинается с “<li>” и заканчивается на “</li>”. До тех пор, пока не найдем “</li>”, мы не решим, что блок разметки окончен. Так что, если у вас будет “}” где-то внутри “<li>” он не завершит “foreach”, так как мы не продвинулись достаточно далеко вверх по стеку.
Во время разбора “<li>”, парсер разметки видит множество символов “@”, из чего следует множество вызовов парсера кода. Таким образом стек вызова увеличивается:
Я углублюсь в детали обработки блоков позже, потому что процесс немного сложен, в итоге мы закончили с данными блоками кодами и вернулись в блок “<li>”. Далее, мы видим “</li>”, так что мы завершаем и этот блок и возвращаемся в блок “foreach”. “}” закрывает блок, так что теперь мы снова на вершине стека – документа разметки. После этого мы читаем, пока не достигнем конца файла, не найдя больше символов “@”. И вуаля! Мы разобрали данный файл!"
Я надеюсь общая структура алгоритма разбора ясна. Главное – это перестать думать, что парсер кода и разметки работают в отдельных потоках, а вместо этого конструкции располагаются одна в другой. Намекну, вдохновние мы черпали из PowerShell ;).