Обслуживание страниц Markdown как HTML на любом базовом сайте ASP.NET
![](https://habrastorage.org/getpro/habr/upload_files/77e/678/0a9/77e6780a953f39df7d75bedceb6a67f3.png)
Первую часть можно прочитать здесь.
Когда мы работаем над сайтом, неплохо иметь под рукой простой способ управлять разделом с документацией или блогом, просто перекидывая Markdown-файлы в нужную папку, которые затем используются на этих страницах в качестве контента, аккуратно вплетенного в айдентику бренда на нашем сайте, не так ли? В этой идее нет ничего нового — большинство CMS-систем и генераторов контента именно так и делают. Но это, как правило, специализированные инструменты, обособленные от нашего веб-приложения. Вот бы здорово было, если бы у нас был простой способ добавить поддержку Markdown на страницах нашего уже существующего веб-приложения?
Давайте же взглянем на middleware-компонент ASP.NET Core, который упрощает настройку папок для Markdown-файлов, которые используются в качестве контента нашего веб-сайта.
Большинство динамических сайтов содержат как минимум пару страниц с контентом, которые в основном статичны, а потому их легче создавать с помощью Markdown, а не “тегового супа” HTML.
Что нам нужно для работы с Markdown-страницами?
Вот что нам потребуется для работы со статическими Markdown-страницами:
Страница-оболочка, реализующая пользовательский интерфейс нашего сайта вокруг Markdown-контента
Область контента, в которую будет рендериться результирующий HTML
Файл с Markdown-текстом, который мы собираемся рендерить
(Опционально) YAML-парсинг для тайтла и хедеров
(Опционально) парсинг тайтла на основе хедера или имени файла.
Конфигурация папок для шаблона и параметров
Весь наш замысел предельно прост: для каждой сконфигурированной иерархии папок мы можем указать шаблон Razor, представляющий из себя страницу для размещения Markdown-контента. Эта страница достаточно примитивна — на практике, для большинства сайтов на этой странице не будет ничего, кроме встроенного Markdown-контента и ссылки на страницу _layout.cshtml
нашего сайта.
В этот шаблон Markdown-страницы передается модель, содержащая отрендеренный Markdown-текст, который мы затем можем встроить в шаблон в нужном месте. Мы можем создать шаблон любым удобным для нас способом: либо в виде отдельной HTML-страницы, либо просто ссылаясь на мастер-страницу, встроив свойство @Model.RenderedMarkdown
в страницу.
Нам также нужно создать конфигурацию для подключения middleware, которое указывает, с какими папками (или root) работать, и хотим ли мы обрабатывать URL без расширений в дополнение к обработке расширения .md.
Затем мы можем просто добавить файлы с расширением .md в папку wwwroot нашего сайта и сконфигурировать пути, точно так же как мы делали бы это со статическими html-файлами.
Приступая к работе
Если вы хотите пощупать middleware, которое я описываю в этой статье, вы можете установить его отсюда:
PM> Install-Package Westwind.AspNetCore.Markdown
Настройка Markdown Middleware
Мы можем начать использовать middleware сразу после установки соответствующего NuGet-пакета. Это делается путем подключения конфигурации middleware:
Запустим
AddMarkdown()
для конфигурации обработки страниц.Запустим
UseMarkdown()
для подключения middleware.Создадим шаблон представления Markdown (по умолчанию:
~/Views/__MarkdownPageTemplate.cshtml
).Создадим .md-файлы для нашего контента.
Следующий код сконфигурирует папку /posts/
для работы с Markdown-документами в вашем приложении:
public void ConfigureServices(IServiceCollection services)
{
services.AddMarkdown(config =>
{
config.AddMarkdownProcessingFolder("/posts/","~/Pages/__MarkdownPageTemplate.cshtml");
});
services.AddMvc();
}
В рамках этой базовой конфигурации мы указываем путь к папке, в которой будут размещены Markdown-документы. Мы можем сконфигурировать несколько папок, включая даже root (/). Мы также можем указать Razor-шаблон, который будет использоваться в качестве контейнера для markdown-контента.
Приведенный выше код является простым примером, не включающим некоторые дополнительные параметры, которые мы можем использовать в нашей конфигурации. Мы можем задать в конфигурации несколько путей, указать, как должны обрабатываться файлы, настроить предварительную обработку кода, предоставив обработчик, который будет запускаться перед рендерингом Markdown-документов, и мы также можем изменить конфигурацию нашего Markdown-конвейера MarkDig.
Затем нам нужно подключить это middleware к middleware-конвейеру с помощью app.UseMarkdown()
в методе Configure() Startup
:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMarkdown();
app.UseStaticFiles();
// MVC необходимо для рендеринга Razor-шаблона
app.UseMvc();
}
Создание Razor-страницы в качестве Markdown-контейнера
Markdown-middleware требует MVC для рендеринга Markdown-контента, чтобы переписать путь запроса и поместить текущий путь запроса в кастомный контроллер, предоставляемый этим компонентом. Затем контроллер рендерит Razor-шаблон, который мы указали в конфигурации AddMarkdown()
, показанной ранее.
Наиболее примитивный шаблонов выглядит так:
@modelWestwind.AspNetCore.Markdown.MarkdownModel
@{
ViewBag.Title = Model.Title;
Layout = "_Layout";
}
<div style="margin-top: 40px;">
@Model.RenderedMarkdown
</div>
В модели содержится два интересующих нас значения: RenderedMarkdown
и Title
, которые можно встроить в шаблон. Title
выводится из YAML-хедера title
, если таковой имеется, или из первого заголовочного тега # в документе. Если ничего из это нет, то тайтл остается не установленным. Если вы хотите передать другие значения в свое представление, конфигурация также поддерживает хук для предварительной обработки, который подключается к Markdown-запросу:
folderConfig.PreProcess = (model, controller) =>
{
// controller.ViewBag.Model = new MyCustomModel();
};
Модель также имеет свойства относительного и физического пути и обеспечивает доступ к конфигурации активной папки, поэтому ваш код предварительной обработки может выполнять сложную логику в зависимости от того, какой файл активирован, и может передавать эти данные в ваше представление через свойства контроллера ViewBag
или ViewData
.
На этом этапе мы уже можем начинать добавлять Markdown-файлы в нашу папку /wwwroot/posts/
. Мы создадим иерархию папок, которая соответствует типичной структуре поста в блоге, где будет дата и имя сообщения, как показано на рисунке 2, и добавим в нее один пост из моего блога вместе с парой изображений.
![Рисунок 2: Добавление markdown-файла с контентом в папку для рендеринга Рисунок 2: Добавление markdown-файла с контентом в папку для рендеринга](https://habrastorage.org/getpro/habr/upload_files/a99/cb9/5fb/a99cb95fbfa69eb382d801a5c2732da8.png)
Это все, что нужно, и теперь мы можем получить доступ к этой странице по следующему URL:
http://localhost:59805/posts/2018/03/23/MarkdownTagHelper.md
Или версия без расширения:
http://localhost:59805/posts/2018/03/23/MarkdownTagHelper
Конфигурация по умолчанию работает как с расширением .md, так и без него. Если расширение не указано, middleware просматривает каждый запрос без расширения, пытается добавить .md, проверяет, существует ли файл, и рендерит его. Результат показан на рисунке 3.
![Рисунок 3: Отрендеренный Markdown-документ на стандартном ASP.NET Core сайте Рисунок 3: Отрендеренный Markdown-документ на стандартном ASP.NET Core сайте](https://habrastorage.org/getpro/habr/upload_files/65b/796/0e4/65b7960e4b55ebf2f41f22e7018f7548.png)
Markdown-контент корректно отображается в виде HTML, и, как и ожидалось, мы получили соответствующий пользовательский интерфейс шаблонного ASP.NET Core сайта. Мы не вносили никаких изменений в стандартный шаблон New Project, но даже в этом случае этот markdown-текст прекрасно отображается на странице, согласуясь с контекстом пользовательского интерфейса сайта.
Еще пара улучшений
Есть одна вещь, которую определенно стоило бы улучшить: наш пример кода отображается скучным монохромным текстом без подсветки синтаксиса. Чтобы исправить это, мы теперь можем изменить наш Razor-шаблон, добавив немного JavaScript, чтобы реализовать подсветку синтаксиса, и добавить немного Bootstrap в наш стиль, чтобы он выглядел лучше. В листинге 2 показан наш обновленный шаблон:
Листинг 2: Razor-шаблон с подсветкой синтаксиса для HighlightJs
@model Westwind.AspNetCore.Markdown.MarkdownModel
@{
ViewBag.Title = Model.Title;
Layout = "_Layout";
}
@section Headers {
<style>
h3 {
margin-top: 50px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/* vs2015 theme specific*/
pre {
background: #1E1E1E;
color: #eee;
padding: 0.5em !important;
overflow-x: auto;
white-space: pre;
word-break: normal;
word-wrap: normal;
}
pre > code {
white-space: pre;
}
</style>
}
<div style="margin-top: 40px;">
@Model.RenderedMarkdown
</div>
@section Scripts {
<script src="~/lib/highlightjs/highlight.pack.js"></script>
<link href="~/lib/highlightjs/styles/vs2015.css" />
<script>
setTimeout(function () {
var pres = document.querySelectorAll("pre>code");
for (var i = 0; i < pres.length; i++) {hljs.highlightBlock(pres[i]);}
});
</script>
}
На рисунке 4 показано, как сейчас выглядит отрендеренная страница:
![Рисунок 4: Новая улучшенная страница Рисунок 4: Новая улучшенная страница](https://habrastorage.org/getpro/habr/upload_files/2b9/6a8/5ef/2b96a85ef57428454fd37c13832673b7.png)
Для форматирования и подсветки кода мы использовали JavaScript-библиотеку highlightJS. Чтобы использовать эту библиотеку от нас требуется ссылка на нее, тема (VS2015, тема, похожая на темную тему Visual Studio Code) и небольшой скрипт, который находит pre>code элементы на странице и применяет подсветку синтаксиса. Отрендеренный Markdown также включает атрибут языка, который HighlightJS понимает и использует для выбора соответствующего поддерживаемого языка. HighlightJS можно кастомизировать, и мы также можем создавать собственные пакеты, включающие нужные нам языки. Я создал свой собственный пакет, включающий наиболее распространенные языки .NET, Windows и Web, который вы можете найти в GitHub-репозитории библиотеки.
На данном этапе я могу просто поместить файлы Markdown в свою папку wwwroot/posts/
, и они будут отображаться как вполне самодостаточные страницы. Чудеса!
Создание собственного middleware для обработки Markdown-файлов
Так как же все это работает? Как вы, наверное, и сами догадываетесь, процесс создания middleware не очень сложен, но он включает довольно много подвижных частей, что является привычным делом при создании middleware.
Вот что нам потребуется:
Реализация middleware для обработки маршрутизации запросов.
Middleware-расширения, которые настраивают и подключают middleware.
Контроллер MVC, который обрабатывает запрос рендеринга
Razor-шаблон, используемый для визуализации отрендеренного HTML
Краткий справка по middleware
Middleware — это класс ASP.NET Core, который реализует метод InvokeAsync (HttpContext context). С другой стороны, Middleware также может быть реализовано непосредственно в классе Startup
или как часть Middleware-расширения через app.Use()
(для завершения middleware app.Run())
.
Идея, стоящая за middleware довольно проста: вы реализуете обработчик middleware, который получает объект context
и вызывает next(context), который передает контекст следующему middleware, определенному в цепочке, которое вызывает следующее middleware, и так далее, пока все middleware-компоненты не будут вызваны. Цепочка разворачивается, и каждый из этих вызовов возвращает свой статус обратно вверх по цепочке. По сути, обработчики middleware могут перехватывать входящие и исходящие запросы с помощью одной и той же реализации. Рисунок 5 иллюстрирует поток этого связанного взаимодействия middleware-компонентов. Обратите внимание, что порядок в конвейере играет важную роль значение. Таким образом, для правильной работы функция предварительной обработки, такая как аутентификация или CORS, должна выполняться до функции обслуживания страниц, такой как обслуживание MVC или StaticFile.
![Рисунок 5: Middleware-компоненты подключаются к обработке входящих и исходящих запросов Рисунок 5: Middleware-компоненты подключаются к обработке входящих и исходящих запросов](https://habrastorage.org/getpro/habr/upload_files/b73/a6b/346/b73a6b346b4f4ba1bf1ec14acb3088e3.png)
Реализация специального middleware-компонента обычно подразумевает создание самого middleware-компонента, а также нескольких middleware-расширений, используемых для конфигурации и подключения middleware с помощью app.Add<Middleware>()
и app.Use<middleware>()
, которые являются шаблоном, используемым большинством встроенных middleware-компонентов ASP.NET Core.
Реализация обработки Markdown-страниц в виде middleware
Основная задача нашего middleware для обработки Markdown-страниц — выяснить, запрашивает ли входящий запрос Markdown-документ, проверяя URL. Если запрос указывает на .md-файл Markdown, middleware фактически переписывает URL запроса и направляет его на кастомный хорошо известный эндпоинт контроллера, который предоставляется как часть библиотеки компонентов. Листинге 3 демонстрирует, как выглядит middleware.
Листинг 3: Middleware-компонент для обслуживания Markdown-страниц
public class MarkdownPageProcessorMiddleware
{
private readonly RequestDelegate _next;
private readonly MarkdownConfiguration _configuration;
private readonly IHostingEnvironment _env;
public MarkdownPageProcessorMiddleware(RequestDelegate next,
MarkdownConfiguration configuration,
IHostingEnvironment env)
{
_next = next;
_configuration = configuration;
_env = env;
}
public Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value;
if (path == null)
return _next(context);
bool hasExtension = !string.IsNullOrEmpty(Path.GetExtension(path));
bool hasMdExtension = path.EndsWith(".md");
bool isRoot = path == "/";
bool processAsMarkdown = false;
var basePath = _env.WebRootPath;
var relativePath = path;
relativePath = PathHelper
.NormalizePath(relativePath)
.Substring(1);
var pageFile = Path.Combine(basePath, relativePath);
// обрабатывает любой файл С расширением .md
foreach (var folder in
_configuration.MarkdownProcessingFolders)
{
if (!path.StartsWith(folder.RelativePath,
StringComparison.InvariantCultureIgnoreCase))
continue;
if (isRoot && folder.RelativePath != "/")
continue;
if (context.Request.Path.Value.EndsWith(".md",
StringComparison.InvariantCultureIgnoreCase))
{
processAsMarkdown = true;
}
else if (path.StartsWith(folder.RelativePath,
StringComparison.InvariantCultureIgnoreCase) &&
(folder.ProcessExtensionlessUrls && !hasExtension ||
hasMdExtension && folder.ProcessMdFiles))
{
if (!hasExtension && Directory.Exists(pageFile))
continue;
if (!hasExtension)
pageFile += ".md";
if (!File.Exists(pageFile))
continue;
processAsMarkdown = true;
}
if (processAsMarkdown)
{
// значения в пуше из контроллера
context.Items["MarkdownPath_PageFile"] = pageFile;
context.Items["MarkdownPath_OriginalPath"] = path;
context.Items["MarkdownPath_FolderConfiguration"] = folder;
// перезаписывает путь к нашему контроллеру
context.Request.Path = "/markdownprocessor/markdownpage";
break;
}
}
return _next(context);
} }
Очень важный элемент в этом middleware — Context.Path.Value
, который содержит текущий путь запроса. На основе этого пути компонент проверяет, указывает ли он либо на .md
напрямую, либо на URL без расширения, к которому он добавляет .md
, и проверяет наличие файла.
Если путь указывает на Markdown-файл, middleware устанавливает флаг и сохраняет исходный путь и путь к файлу в нескольких элементах контекста. Что еще более важно, он перзаписывает текущий путь, чтобы указать на хорошо известный MarkdownPageProcessorController, который имеет фиксированный маршрут /markdownprocessor/markdownpage
.
Переписать путь запроса в middleware в ASP.NET Core так же просто, как изменить свойство HttpContext.Path.
Универсальный Markdown-контроллер
Это перенаправит запрос на мой кастомный контроллер, который затем может отображать содержимое физического файла с использованием сконфигурированного Razor-шаблона:
[Route("markdownprocessor/markdownpage")]
public async Task<IActionResult> MarkdownPage()
Этот захардкоженный маршрут атрибутов будет найден, даже если он находится в отдельной библиотеке, получаемой через NuGet. Обратите внимание, что этот маршрут работает только в сочетании с middleware, поскольку он зависит от предустановленных значений Context.Items
, которые были сохранены middleware ранее в запросе. В листинге 4 показан экшн-метод, отвечающий за обслуживание Markdown-файла.
Листинг 4: Метод Controller, который обрабатывает Markdown-контент
[Route("markdownprocessor/markdownpage")]
public async Task<IActionResult> MarkdownPage()
{
var basePath = hostingEnvironment.WebRootPath;
var relativePath = HttpContext.Items["MarkdownPath_OriginalPath"] as string;
if (relativePath == null)
return NotFound();
var folderConfig =
HttpContext.Items["MarkdownPath_FolderConfiguration"]
as MarkdownProcessingFolder;
var pageFile = HttpContext.Items["MarkdownPath_PageFile"] as string;
if (!System.IO.File.Exists(pageFile))
return NotFound();
// string markdown = await File.ReadAllTextAsync(pageFile);
string markdown;
using (var fs = new FileStream(pageFile, FileMode.Open, FileAccess.Read))
using (StreamReader sr = new StreamReader(fs)) markdown = await sr.ReadToEndAsync();
if (string.IsNullOrEmpty(markdown))
return NotFound();
var model = ParseMarkdownToModel(markdown);
model.RelativePath = relativePath;
model.PhysicalPath = pageFile;
if (folderConfig != null)
{
model.FolderConfiguration = folderConfig;
folderConfig.PreProcess?.Invoke(model, this);
return View(folderConfig.ViewTemplate, model);
}
return View(MarkdownConfiguration.DefaultMarkdownViewTemplate, model);
}
Код начинается с получения переменных Context, которые были установлены в middleware при пересылке запроса, проверки файла и, если он найден, чтения его с диска и рендеринга в HTML. Модель создается и строится с важным тайтлом и отрендеренным HTML, которые используются непосредственно представлением, наряду с некоторой информацией о файле и информацией о конфигурации, которые могут использоваться либо внутри представления, либо в средстве предварительной обработки. Когда все сказано и сделано, модель отправляется в представление для рендеринга.
Расширения
После того, как вы создали middleware-компонент, его еще нужно подключить и добавить в конвейер. ASP.NET Core предоставляет универсальные функции для добавления типизированного middleware, но эти функции нелегко обнаружить. Лучший способ — сделать то, что делают нативные middleware, то есть предоставить методы расширения, расширяющие IServiceCollection
и IApplicationBuilder. В листинге 5 показан код расширения.
Листинг 5: Конфигурирование и подключение middleware к конвейеру
public static class MarkdownMiddlewareExtensions
{
public static IServiceCollection AddMarkdown(
this IServiceCollection services,
Action<MarkdownConfiguration> configAction = null)
{
var config = new MarkdownConfiguration();
configAction?.Invoke(config);
if (config.ConfigureMarkdigPipeline != null)
MarkdownParserMarkdig.ConfigurePipelineBuilder = config.ConfigureMarkdigPipeline;
config.MarkdownProcessingFolders = config.MarkdownProcessingFolders
.OrderBy(f => f.RelativePath)
.ToList();
services.AddSingleton(config);
return services;
}
public static IApplicationBuilder UseMarkdown(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MarkdownPageProcessorMiddleware>();
} }
Метод AddMarkdown()
предоставляет конфигурацию службы, создавая дефолтный объект конфигурации, а затем используя опциональный Action<MarkdownConfiguration>()
, который мы предоставляем для конфигурирования middleware. Это распространенный шаблон, который вызывает конфигурацию с задержкой при поступлении первого запроса. Последним действием метода является явное добавление конфигурации в контейнер внедрения зависимостей, чтобы ее можно было получить в middleware и контроллере посредством внедрения зависимостей.
UseMarkdown()
очень прост и просто делегирует builder.UseMiddleware<MarkdownPageProcessor>()
. Единственная цель метода-оболочки — предоставить обнаруживаемый метод в Startup.Configure()
, который согласуется с тем, как ведет себя встроенное middleware ASP.NET Core.
И вуаля!
Теперь у нас есть полностью реюзабельный механизм рендеринга Markdown-страниц, который можно легко подключить к любому приложению ASP.NET Core с помощью нескольких строк конфигурационного кода. С этого момента я могу просто поместить Markdown-файлы и связанные с ними ресурсы в свою папку wwwroot.
И на этом все. Markdown вам в помощь!
Приглашаем всех желающих на открытое занятие «Создание консольного калькулятора». На занятии сделаем с нуля консольный калькулятор, используя такие понятия как: ветвления, функции и циклы в C#. Регистрируйтесь по ссылке.