Последние несколько лет ознаменовали становление Markdown в качестве общепринятого языка разметки HTML-текста. Он становится доступным во все большем количестве мест и фактически уже стал стандартом для документации, которая публикуется и редактируется в интернете. Если вы работаете с Git и GitHub, то вы уже используете Markdown для форматирования README.md и, вероятно, всей остальной документации, которую вы пишете для своих проектов, связанных с разработкой программного обеспечения. Большая часть документации для разработчиков, которую вы сегодня можете найти в интернете, будь то коммерческая документация от таких компаний, как Microsoft, Google и т. д., или типовые решения для документации наподобие ReadTheDocs или KavaDocs, — создается и поддерживается с помощью Markdown.

Полный Markdown

Поскольку Markdown является языком с текстовыми управляющими конструкциями, с ним очень легко работать и нужно совсем немного времени, чтобы его освоить. Его легко сравнивать, мержить и отслеживать историю его изменений с помощью встроенных функций в системах контроля версий, таких как Git, и для работы с ним не требуется никакого специального редактора — это обычный текст, с которым можно работать в любом текстовом редакторе.

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

Хоть Markdown ограничивается поддержкой относительного небольшого набора фич HTML, он предоставляет наиболее часто используемые из них, позволяющие реализовать большинство текстовых возможностей. В отличие от WYSIWYG HTML-редакторов, в которых не всегда возможна эффективная работа с текстом из-за нагроможденного форматирования и запаздывающего отображения результатов, Markdown — самый обычный текст, и его можно вводить в простом текстовом поле. Markdown не поддерживает фичи связанные с версткой, он практически полностью фокусируется на встроенной разметке текста. Заголовки, выделение текста жирным шрифтом, курсивом, подчеркивание, ссылки и изображения, а также несколько более сложных структур, таких как списки и таблицы, могут быть выражены с помощью лаконичных и легко запоминающихся символов разметки. Причина, по которой Markdown стал настолько популярным, заключается в том, что он очень прост, и его основы можно легко освоить всего за пару минут. Вы имеете дело с вполне логичным текстовым потоком с небольшим количеством разметки.

Markdown вызывает привыкание. Как только вы начнете писать с применением Markdown, вы захотите использовать его везде. Настолько, что я часто по привычке начинаю набирать текст с Markdown в тех местах, где я хотел бы, чтобы он работал, но где он, увы, не поддерживается — в электронных письмах, сообщениях в Skype или окнах ввода на различных сайтах.

Markdown становится все более популярным — на сегодняшний день мы наблюдаем как Markdown появляется во всех видах решений для документов, и если вы создаете документацию любого рода, вполне вероятно, что Markdown уже является частью этого процесса. Но Markdown простирается гораздо дальше, чем разметка документации: вы также можете использовать его в своих собственных приложениях для реализации расширенного текстового контента для таких вещей, как описание продуктов, интерактивные обсуждения, заметки и любого другого типа встроенного текста, которому не помешало бы дополнительное форматирование для удобства чтения.

Добавляем Markdown в наше приложение

В этой статье я расскажу о ряде фич Markdown, которые вы можете добавить в свои ASP.NET Core приложения. Я начну с того, что вам понадобится для реализации парсинга текста Markdown в HTML с помощью простой библиотеки, которая позволяет легко использовать Markdown в коде и в ваших ASP.NET Core MVC Razor Pages, а также что вам потребуется для рендеринга вашего собственного Markdown-текста из данных в ваше приложение. Еще я покажу пару полезных хелперов для содержимого, которые позволяют встраивать статические Markdown Islands в Razor Pages с помощью Markdown TagHelper, и удобный middleware-компонент Markdown, который позволяет легко обслуживать страницы Markdown как HTML-контент в контексте пользовательского интерфейса вашего веб-приложения.

Если вам не терпится начать работу, вы можете сразу перейти к пакету NuGet или GitHub-репозиторию, но я рекомендую вам продолжить чтение, чтобы лучше разобраться что к чему.

  • Install-Package Westwind.AspnetCore.Markdown

Парсинг Markdown в .NET

Первое, с чем нам необходимо разобраться, — это то, каким образом мы будем парсить Markdown в HTML в .NET. Это на удивление просто, поскольку для .NET доступно сразу несколько парсеров Markdown. Тот, который мне нравится больше всего, называется MarkDig. Он относительно новый, очень быстрый и имеет очень хороший задел в плане расширяемости, что позволяет нам создавать собственные расширения для Markdown.

MarkDig имеет открытый исходный код и доступен для добавления в ваши проекты .NET Core или полного фреймворка в виде NuGet-пакета:

Install-Package Markdig

 Используя MarkDig в его простейшей форме, вы можете сделать следующее:

public static class Markdown
{
    public static string Parse(string markdown)
    {
        var pipeline = new MarkdownPipelineBuilder()
            .UseAdvancedExtensions()
            .Build();
        return Markdown.ToHtml(markdown, pipeline);
    }
}

MarkDig использует конфигурационный конвейер вспомогательных функций, которые можно добавлять поверх базового парсера. Метод .UseAdvancedExtensions() добавляет ряд распространенных расширений (таких как GitHub-Flavored Markdown, List Extensions и т. д.), но вы также можете сами добавить каждый из нужных вам компонентов и тем самым более тонко настроить то, как вы хотите парсить Markdown.

Код, приведенный выше, будет работать, но он не очень эффективен, так как для каждой операции синтаксического анализа конвейер создается заново. В целях повышения производительности гораздо лучше будет создать небольшой уровень абстракции для парсера Markdown, чтобы его можно было кэшировать. Вы можете посмотреть код MarkdownParserFactory и настроенной реализации MarkdownParser на GitHub, где также можно найти интерфейс IMarkdownParser, содержащий метод .Parse(markdown), на основе которого в дальнейшем будет реализован рендеринг.

Чтобы сделать этот функционал легкодоступным из любого места, мы обернем фабрику и функции парсинга статическим классом Markdown, как показано ниже:

public static class Markdown
{
    public static string Parse(string markdown, bool usePragmaLines = false, bool forceReload = false)
    {
        if (string.IsNullOrEmpty(markdown))
            return "";

        var parser = MarkdownParserFactory.GetParser(usePragmaLines, forceReload);
        return parser.Parse(markdown);
    }

    public static HtmlString ParseHtmlString(string markdown, bool usePragmaLines = false, bool forceReload = false)
    {
        return new HtmlString(Parse(markdown, usePragmaLines, forceReload));
    }
}

Затем этот класс можно будет использовать для доступа к функциям Markdown в приложении и компонентах, которые я опишу позже. Также конфигурация middleware Markdown позволит использовать внедрение зависимостей для доступа к этим компонентам, которые я опишу позже.

Теперь, когда у нас есть этот класс, мы можем легко преобразовать Markdown в HTML. Чтобы распарсить Markdown в string прямо в коде нам достаточно использовать:

string html = Markdown.Parse(markdownText)

Чтобы распарсить Markdown во встраиваемую в Razor HTML-строку, вы можете использовать метод .ParseHtmlString():

<div>
    @Markdown.ParseHtmlString(Model.ProductInfoMarkdown)
</div>

Эта пара простых хелперов значительно упрощает преобразование хранимого Markdown в HTML в ваших приложениях.

Markdown вызывает привыкание: как только вы начнете писать с применением Markdown, вы захотите использовать его везде.

Markdown в качестве статического контента для динамического сайта

При создании динамических веб-приложений мы не очень часто задумываемся о статическом контенте. Поскольку статического контента обычно не так много, мы продолжаем реализовывать такие страницы, как “О нас”, “Свяжитесь с нами”, “Политика конфиденциальности” и т. д., используя старый добрый HTML. Большинство этих страниц полностью статичны и зачастую не содержат ничего, кроме текста с несколькими заголовками и списками или каким-либо другим простым форматированием абзацев.

А что если мы заменим эти куски текста на таких страницах блоками статического Markdown-текста, встроенного в содержимое? Или, что еще лучше, реализуем простой способ доставлять Markdown-файлы прямо на наш сайт, как и другую статику?

Чтобы воплотить эту затею, я создал два дополнительных компонента:

  • Markdown-секции: TagHelper для встраивания Markdown-блоков в представления Razor. TagHelper поддерживает встраивание статики или привязанные к модели Markdown-секций в любое представление или страницу Razor. Использование TagHelper позволяет естественным образом заменять громоздкие блоки HTML-текста более простыми в редактировании Markdown-блоками.

  • Middleware для доставки Markdown-файлов в качестве полноценных страниц содержимого. Middleware позволяет нам создать папку или даже целый сайт для доставки .md-файлов в качестве автономных страниц содержимого. Middleware работает путем преобразования содержимого .md-файла на диске и его объединения с шаблоном страницы Markdown, который мы создадим заранее. Markdown рендерится в этот шаблон, в результате чего мы получаем страницу, которая соответствует пользовательскому интерфейсу остальной части нашего сайта.

Давайте же посмотрим, как создать оба этих компонента, разбираясь в процессе, как работают две важные концепции ASP.NET Core (TagHelpers и Middleware) и как мы можем создавать свои собственные реализации.

Markdown-секции — это блоки Markdown-текста, встроенные в более крупный HTML-документ.

Встраиваем Markdown в представление с помощью TagHelper

Основная цель TagHelper’а Markdown — взять фрагмент Markdown-текста либо из его встроенного содержимого, либо из модели, преобразовать его в HTML и внедрить в вывод Razor. TagHelper Markdown позволяет встраивать статический Markdown-текст в представление или страницу Razor с помощью тега <markdown>:

<h3>Markdown TagHelper Block</h3>

<markdown normalize-whitespace="true">
    ## Markdown Text
    * Item 1
    * Item 2

    The current Time is: **@DateTime.Now.ToString("HH:mm:ss")**
</markdown>

Текст между тегами оценивается и преобразуется из Markdown в HTML, что выглядит примерно как на рисунке 1. Обратите внимание, что фрагмент кода Markdown содержит встроенное выражение Razor, которое оценивается и внедряется до того, как происходит парсинг Markdown. Это довольно мощная фича, так как мы можем использовать логику Razor внутри Markdown-контента, что по сути дает нам возможность обрабатывать Markdown-текст автоматически. Этот встроенный синтаксический анализ Markdown мы имеем благодаря парсеру Razor в ASP.NET Core, что открывает множество возможностей для упрощения создания текстового форматирования для таких вещей, как слияния (merges) и стандартные письма (form letters).

Рисунок 1 : Вывод TagHelper’а Markdown, отображаемый в стандартных шаблонах ASP.NET Core
Рисунок 1 : Вывод TagHelper’а Markdown, отображаемый в стандартных шаблонах ASP.NET Core

Тег-хелпер также поддерживает привязку модели с помощью атрибута markdown в теге, где MarkdownText — это свойство передаваемого объекта модели:

@model MarkdownModel

<markdown markdown="MarkdownText"/>

Прежде чем мы сможем использовать тег-хелпер, мы должны его зарегистрировать. Мы делаем это в __ViewImports.cshtml, чтобы TagHelper был доступен на всех страницах приложения. Обратите внимание, что MVC Views и Razor Pages используют отдельные наборы страниц, и если вы комбинируете их, вам необходимо добавить следующее для обеих локаций.

@addTagHelper *, Westwind.AspNetCore.Markdown

Создание TagHelper’а Markdown 

Давайте разберемся, как создать этот TagHelper. Интерфейс для создания TagHelper — это интерфейс процесса с одним методом, который принимает Context TagHelper’а для хранения информации об элементе, теге и содержимом, которую затем можно использовать для создания выходной строки для замены тега TagHelper и внедрения в содержимое Razor. Это очень простой и понятный интерфейс, с которым легко работать, и который не требует большого количества кода. Хорошей иллюстрацией того, насколько TagHelper Markdown мал, является то, что большая его часть является вспомогательным кодом, который не имеет ничего общего с логикой Markdown.

TagHelper инкапсулирует логику рендеринга через очень простой метод ProcessAsync(), который отображает фрагмент HTML-контента на странице в том месте, где определен TagHelper. Метод ProcessAsync() принимает в качестве входных данных Context TagHelper’а, чтобы вы могли получить элемент и атрибуты для ввода, и предоставляет выходные данные, в которые вы можете записать строковый вывод и создать встроенное содержимое.

Чтобы TagHelper’ы можно было использовать, они должны быть зарегистрированы в MVC либо на странице, либо, что более вероятно, в _ViewImports.cshtml проекта.

Чтобы создать TagHelper:

  • Создайте новый класс, наследующий TagHelper;

  • Создайте свою реализацию TagHelper с помощью ProcessAsync() или Process().

Чтобы использовать TagHelper в своем приложении:

  • Зарегистрируйте свой TagHelper в _ViewImports.cshtml;

  • Встройте <markdown> </markdown> в ваши страницы;

  • И вперед!

Нам нужно создать элемент управления контентом для TagHelper’а <markdown>, содержимое которого можно получить как Markdown, а затем преобразовать в HTML. При желании вы также можете использовать свойство Markdown для привязки Markdown-текста к отображению, поэтому, если Markdown представлен как данные в вашей модели, вы можете привязать его к этому свойству/атрибуту вместо того, чтобы предоставлять его как статический контент.

В листинге 1 показана простейшая реализация класса MarkdownTagHelper , выполняющего эти задачи.

Листинг 1: TagHelper Markdown для синтаксического анализа встроенного Markdown-контента

[HtmlTargetElement("markdown")]
public class MarkdownTagHelper : TagHelper
{

    [HtmlAttributeName("normalize-whitespace")]
    public bool NormalizeWhitespace { get; set; } = true;
    
    [HtmlAttributeName("markdown")]
    public ModelExpression Markdown { get; set; }
    
    
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        string content = null;
        if (Markdown != null)
            content = Markdown.Model?.ToString();
    
        if (content == null)            
            content = (await output.GetChildContentAsync(NullHtmlEncoder.Default)).GetContent(NullHtmlEncoder.Default);
    
        if (string.IsNullOrEmpty(content))
            return;
    
        content = content.Trim('\n', '\r');
    
        string markdown = NormalizeWhiteSpaceText(content);            
    
        var html = Markdown.Parse(markdown);
    
        output.TagName = null;  // Remove the <markdown> element
        output.Content.SetHtmlContent(html);
    } 
}

Обратите внимание, что свойство Markdown объявлено не как string, а как ModelExpression, потому что оно ожидает значение из свойства Model с соответствующим именем. Чтобы получить значение, выражение может использовать свойство Model, поэтому Markdown.Model извлекает значение Model, каким бы оно не было. Если это значение ненулевое, то именно оно и будет использовано, так как оно имеет приоритет над статическим контентом.

Если вместо этого определен статический контент, он будет извлечен при помощи метода GetChildContentAsync().GetContent(), который извлекает все, что содержится между тегами <markdown> </markdown>. Эти методы извлекают содержимое, а также выполняют любые встроенные выражения Razor. Чтобы гарантировать, что выражения Razor в тегах не будут преобразованы в HTML, передается NullHtmlEncoder.Default. Это важно, потому что я не хочу, чтобы мой Markdown-текст, переданный в парсер Markdown, был преобразован Razor. На это есть две причины: преобразование в HTML может испортить некоторые Markdown-теги в оцениваемых выражениях; парсер Markdown сам выполняет преобразование в HTML в рамках процесса синтаксического анализа Markdown-текста.

Как только мы получили из тега необработанный markdown-контент, нам нужно нормализовать пробелы, удалив лишние в начале текстового блока. В Markdown начальный пробел имеет важное значение, а четыре пробела или табуляция обозначают блок кода. Если вы не выровняете Markdown по левому краю, весь текст будет отображаться как код. В TagHelper можно установить атрибут normalize-whitespace (он по умолчанию true). Вы можете явно установить значение атрибута в false, если вы явно выравниваете текст по левому краю или если вам нужно оставить начальные пробелы как есть.

После того, как мы получили нормализованный Markdown-текст, нам достаточно просто вызвать Markdown.Parse(markdown) для получения HTML. Нам осталось только задать вывод output.Content. Здесь я использовал SetHtmlContent(), чтобы задать полученную строку, которая является результирующим HTML.

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


Завтра в 20:00 состоится открытое занятие «ASP.NET Core — подготовка и запуск простого веб-сервиса». На этом уроке создадим базовый web-api для сервиса, разработаем контроллер и настроим маршрутизацию точек доступа. Подключим источник данных, познакомимся с инструментами ручного тестирования сервиса и моделями развертывания приложения. Регистрация доступна по ссылке для всех желающих.

Комментарии (0)