﻿<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text" xml:lang="en">Autofac</title>
  <link type="application/atom+xml" href="https://d.moonfire.us/tags/autofac/atom.xml" rel="self" />
  <link type="text/html" href="https://d.moonfire.us/tags/autofac/" rel="alternate" />
  <updated>2026-05-12T17:42:15Z</updated>
  <id>https://d.moonfire.us/tags/autofac/</id>
  <author>
    <name>D. Moonfire</name>
  </author>
  <rights>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</rights>
  <entry>
    <title>Using Nitride - Markdown</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/" />
    <updated>2025-06-10T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="markdig" scheme="https://d.moonfire.us/tags/" label="MarkDig" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <summary type="html">Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.
</summary>
    <content type="html">&lt;p&gt;Yesterday, I created an over-engineered program to copy a single file from one directory to another. Now, time to make it less overkill by transforming that Markdown file into simple HTML.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MarkDig&lt;/h2&gt;
&lt;p&gt;I don't like reinventing the wheel. I mean, I seem to keep doing it but I don't enjoy it. That was one reason why I tried another static site generators before working on &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;. However, when it comes to redoing a Markdown parser, even I'm not that foolish when there is already &lt;a href="/tags/markdig/"&gt;MarkDig&lt;/a&gt;, an excellent library for turning Markdown into HTML and extendable enough that I could also turn Markdown into &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; (a later post).&lt;/p&gt;
&lt;p&gt;In these cases, we need to tell Nitride how to do anything with Markdown since I didn't make it part of the core library. To do that, we need to pull in the NuGet package. While we're at it, we're also going to add the HTML processing library.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cd src/dotnet
$ dotnet add package MfGames.Nitride.Markdown
$ dotnet add package MfGames.Nitride.Html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the packages installed, we need to add those modules into the system. This is where &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; came in helpful since I just have to add a module for the package it will handle the registration of any operations, components, and systems that we need to use.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Program.cs
var builder = new NitrideBuilder(args)
    .UseIO(rootDirectory)
    .UseMarkdown()
    .UseHtml()
    .UseModule&amp;lt;WebsiteModule&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, I'm trying to follow the generic host pattern for the setup.&lt;/p&gt;
&lt;h2&gt;Identifying Markdown&lt;/h2&gt;
&lt;p&gt;While it may be obvious to convert any entity class that ends in &lt;code&gt;.md&lt;/code&gt; into &lt;code&gt;.html&lt;/code&gt;, we break this apart into separate steps. First, is that we identify a file as a Markdown file. This does two things, it adds the &lt;code&gt;MfGames.Nitride.Markdown.IsMarkdown&lt;/code&gt; as a component, and then treats the contents as text instead of binary.&lt;/p&gt;
&lt;p&gt;If you remember previously, we had this output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[00:41:57 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we're going to use a new operation, &lt;code&gt;MfGames.Nitride.Markdown.IdentifyMarkdownFromPath&lt;/code&gt;. This could be put in a central place, such as &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, but I found it is better to do this earlier than later so I usually put the identify process in the input methods. In this case, &lt;code&gt;PagesPipeline&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This operation doesn't take any parameters because it attempts to &amp;ldquo;do the right thing&amp;rdquo; with a minimal amount of effort. Running the code now produces this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:46:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The big things is that we have a new component, &lt;code&gt;IsMarkdown&lt;/code&gt;, and the &lt;code&gt;IBinaryContent&lt;/code&gt; changed to &lt;code&gt;ITextComponent&lt;/code&gt; which gives us some additional extension methods but also indicates that the file is a text file instead of treating it as a simple binary. It still hasn't loaded the file into memory, it just switched how it is handled.&lt;/p&gt;
&lt;p&gt;This is a separate step is because sometimes I want to keep a specific file as Markdown and not convert it to HTML. Also, there are times when I construct an &lt;code&gt;Entity&lt;/code&gt; directly without having a valid path and I just have to add the &lt;code&gt;IsHtml&lt;/code&gt; and &lt;code&gt;ITextContent&lt;/code&gt; and it then acts like every other file without having to have any special rules.&lt;/p&gt;
&lt;h2&gt;Content&lt;/h2&gt;
&lt;p&gt;Entities with &lt;code&gt;ITextComponent&lt;/code&gt; have a number of useful extension methods associated with them. (Technically, you can use these methods against any &lt;code&gt;Entity&lt;/code&gt; class and it will try to convert a binary to text if there is a &lt;code&gt;IBinaryContent&lt;/code&gt; and we want a &lt;code&gt;ITextContent&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;bool hasText = entity.HasTextContent();
string text = entity.GetTextContent();

entity.SetTextContent(stringValue);
entity.SetTextContent(stringBufferValue);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are also stored as a &lt;code&gt;ITextContent&lt;/code&gt; instead of &lt;code&gt;string&lt;/code&gt; or &lt;code&gt;StringBuffer&lt;/code&gt;. This is because the default interface is to leave the content on the disk and use &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; to retrieve it. However, as soon as &lt;code&gt;SetTextContent&lt;/code&gt; is used, then it keeps that value in memory for the rest of the execution. This is the point where memory pressure begins to increase.&lt;/p&gt;
&lt;p&gt;In the future, we could easily create a &lt;code&gt;ITextContent&lt;/code&gt; implementation that writes large text files to the disk to get them out of memory. However, even my largest chapter of twenty-five thousand words doesn't create too much of a problem so I haven't bothered trying to implement that at this point (but if I did, it would go into &lt;code&gt;//.cache&lt;/code&gt; in some manner).&lt;/p&gt;
&lt;h2&gt;Converting Markdown to HTML&lt;/h2&gt;
&lt;p&gt;Just identifying a file as Markdown doesn't do anything in itself. To convert it, we use another operation, &lt;code&gt;MfGames.Nitride.Markdown.ConvertMarkdownToHtml&lt;/code&gt;. This easily goes into the &lt;code&gt;StyleHtmlPipeline&lt;/code&gt; to handle the conversion and styling. It allows any MarkDig extension or plugin to be called as part of the setup, allow one to customize exactly how the output is generated.&lt;/p&gt;
&lt;p&gt;We also want to change the extension from &lt;code&gt;.md&lt;/code&gt; to &lt;code&gt;.html&lt;/code&gt;. I realize I should have baked that logic into the &lt;code&gt;ConvertMarkdownToHtml&lt;/code&gt; since it is a common operation, which I will do, but for now, we also need to use the &lt;code&gt;MfGames.Nitride.IO.Paths.ChangePathExtension&lt;/code&gt; to do that.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public StyleHtmlPipeline(
    ILogger&amp;lt;StyleHtmlPipeline&amp;gt; logger,
    SimplifiedMarkdownPipeline simplifiedMarkdownPipeline,
    ConvertMarkdownToHtml convertMarkdownToHtml,
    ChangePathExtension changePathExtension)
{
    _logger = logger;
    _changePathExtension = changePathExtension.WithExtension(&amp;quot;.html&amp;quot;);

    _convertMarkdownToHtml = convertMarkdownToHtml
        .WithConfigureMarkdown(builder =&amp;gt;
        {
            SmartyPantOptions smartyPantOptions = new();
            SmartyPantsExtension smartyPants = new(smartyPantOptions);

            builder
                .Use&amp;lt;GenericAttributesExtension&amp;gt;()
                .Use(smartyPants);
        });

    AddDependency(simplifiedMarkdownPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_convertMarkdownToHtml, cancellationToken)
        .Run(_changePathExtension, cancellationToken)
        .ToList();

    _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

    return list.ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this gets us:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
[01:00:43 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
$ find build -type f
build/typewriter/html/index.html
$ cat src/pages/typewriter/index.md 
# Typewriter Press
$ cat build/typewriter/html/index.html
cat build/typewriter/html/index.html
&amp;lt;h1&amp;gt;Typewriter Press&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now our over-engineered copy method has duplicated &lt;code&gt;markdown2html&lt;/code&gt; for a single file.&lt;/p&gt;
&lt;h2&gt;Components&lt;/h2&gt;
&lt;p&gt;You may notice that &lt;code&gt;IsMarkdown&lt;/code&gt; has been removed and &lt;code&gt;IsHtml&lt;/code&gt; was added. This is part of where I struggled with &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt; and lead me down the path of using components. I don't have to pre-define the different data types, purposes, or even formats of a file. Enums are great, but they don't allow easy extension but with an ECS, it's just a matter of adding and removing components based on the use.&lt;/p&gt;
&lt;p&gt;I've used components for a lot of things including identifying pages that should be in the blog archives, special notices, or pages that I want to ignore because they are aliases. I also embed indexes and lists into the pages to allow things like the &amp;ldquo;next&amp;rdquo; or &amp;ldquo;previous&amp;rdquo; links. If I was doing a web comic, I could have a per-character next/previous system easily implemented via those components.&lt;/p&gt;
&lt;p&gt;My other static site generators didn't even have the content type tagging Statiq did, which was a novel concept for me and one that I'm glad I had a chance to use. It simplified a lot of my logic and lead nicely into where I am today.&lt;/p&gt;
&lt;h2&gt;Planning Ahead&lt;/h2&gt;
&lt;p&gt;As I'm planning ahead, I'm going to do the following change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rename &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt; to &lt;code&gt;ContentPipeline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Move the logic I just added into a new pipeline called &lt;code&gt;BareHtmlPipeline&lt;/code&gt; and insert it into between &lt;code&gt;ContentPipeline&lt;/code&gt; and &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason for this is because RSS/Atom feeds use bare HTML to generate their content, so it makes sense to have that bare pipeline feed both of them while having the &lt;code&gt;ContentPipeline&lt;/code&gt; handle a lot of the linking and references that we'll need.&lt;/p&gt;
&lt;h2&gt;Directory Paths&lt;/h2&gt;
&lt;p&gt;One last thing for this post: I prefer paths that end in directory slashes instead of files. So, if create a contact page at &lt;code&gt;//src/pages/content.md&lt;/code&gt;, we want the HTML to be at &lt;code&gt;https://typewriter.press/content/&lt;/code&gt;. This is, creatively enough, another operation: &lt;code&gt;MfGames.Nitride.IO.Paths.MoveToIndexPath&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath,
    MoveToIndexPath moveToIndexPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;
    _moveToIndexPath = moveToIndexPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .Run(_moveToIndexPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we add a &lt;code&gt;contact.md&lt;/code&gt; page which a run gives us this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /contact/index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;ContentPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;BareHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 2 files
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/contact/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
$ cat src/pages/typewriter/contact.md 
# Contact Us

Our emails is [contact@typewriter.press](mailto:contact@typewriter.press).
$ cat build/typewriter/html/contact/index.html 
&amp;lt;h1&amp;gt;Contact Us&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Our emails is &amp;lt;a href=&amp;quot;mailto:contact@typewriter.press&amp;quot;&amp;gt;contact@typewriter.press&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the input is &lt;code&gt;//src/pages/contact.md&lt;/code&gt;, but through the pipeline, it is written out as &lt;code&gt;//build/typewriter/html/contact/index.html&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;Next up, handling front matter. We need that for many reasons, but one of the biggest reason is to provide metadata to generate styled HTML output.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Entities</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/" />
    <updated>2025-06-09T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="gatsbyjs" scheme="https://d.moonfire.us/tags/" label="GatsbyJS" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="serilog" scheme="https://d.moonfire.us/tags/" label="Serilog" />
    <summary type="html">The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.
</summary>
    <content type="html">&lt;p&gt;From yesterday's post, there was an dangling topic, the &lt;code&gt;Entity&lt;/code&gt; class. Unlike most of the static site generators I've worked with, &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; uses an ECS (&lt;a href="https://en.wikipedia.org/wiki/Entity_component_system"&gt;Entity Component System&lt;/a&gt;) to generate its files.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;History&lt;/h2&gt;
&lt;p&gt;I've worked on a number of static site generators over the years, including more than a few adhoc ones until I finally got something stable with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt; which carried me for almost a decade of heavy use. A lot of the patterns that Nitride was based on were on how I found I had to build my pages in Cobblestone, including having a separate &amp;ldquo;gather&amp;rdquo; step which wrote out temporary files and then the &amp;ldquo;process&amp;rdquo; step which formatted them and made everything pretty. I had to do that because I wanted to have cross-linking and references work across multiple Git projects.&lt;/p&gt;
&lt;p&gt;A good example is the &lt;a href="//fedran.com/"&gt;Fedran&lt;/a&gt; website. At the bottom of a &lt;a href="//fedran.com/sand-and-blood/chapter-001/"&gt;chapter&lt;/a&gt;, I have a list of characters and locations in the chapter. The links are built from data from the wiki project but the chapter information comes from the story Git repository. The gather step let me combine both of those together to generate those links.&lt;/p&gt;
&lt;p&gt;That also meant that I needed to have some supplementary information stored in the gather step (which was written to a &lt;code&gt;//build&lt;/code&gt; folder) to help with those linking.&lt;/p&gt;
&lt;p&gt;Near the end, once Cobblestone began to break under its abstraction and complexity, I wanted to get away from writing my own and switched to &lt;a href="/tags/gatsbyjs/"&gt;GatsbyJS&lt;/a&gt; which made a very pretty website, but it was painful to debug and develop. It only lasted a year or so before I wanted to move (though the Gatsby version of Fedran was probably the prettiest I had ever made). If anything, Gatsby ended up being an ecosystem that needed me to pay attention to update things and my cycle of coming around to update a project was too far for it so I spent days trying to get it working again to build it.&lt;/p&gt;
&lt;p&gt;So I tried out &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt;. I never successfully made a website out of it, but it introduced me to some really interesting concepts including pulling in data from other sources (Gatsby also did this, just never really used it). But with the C#, it had some really interesting problems. However, I quickly found that I was trying to do something that was outside of the planned usage, which involved me trying to extend an enumeration which involved basically rewriting large hunks of the system just to handle it. Plus, I really like &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; and I could never get it to play well with that.&lt;/p&gt;
&lt;p&gt;Which lead me into creating Nitride.&lt;/p&gt;
&lt;p&gt;Now, I want to say, both Gatsby and Statiq are both good systems. They just aren't good systems &lt;em&gt;for me&lt;/em&gt;. These don't do what I want them to do and they are designed around patterns that I'm not comfortable with. Even with the desire not to write my own, it wasn't enough.&lt;/p&gt;
&lt;h2&gt;Entity Component Systems&lt;/h2&gt;
&lt;p&gt;Nitride is build on &lt;a href="/tags/mfgames-gallium/"&gt;MfGames.Gallium&lt;/a&gt; which basically means the formal name of the system &lt;a href="https://en.wikipedia.org/wiki/Gallium_nitride"&gt;Gallium Nitride&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gallium nitride (GaN) is a binary III/V direct bandgap semiconductor commonly used in blue light-emitting diodes since the 1990s. The compound is a very hard material that has a Wurtzite crystal structure. Its wide band gap of 3.4 eV affords it special properties for applications in optoelectronics, high-power and high-frequency devices. For example, GaN is the substrate that makes violet (405 nm) laser diodes possible, without requiring nonlinear optical frequency doubling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gallium is a small ECS library. It doesn't intend to be fast or efficient, but it was written to match the idiomatic patterns of &lt;code&gt;System.Linq&lt;/code&gt;. I wanted something that wasn't going to reinvent the wheel, which also meant not making custom collection classes or lookup tables. Instead, I wanted to work with BCL primitives which is why much of the system is based on &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is some high-level documentation for Gallium on &lt;a href="//mfgames.com/mfgames-cil/docs/gallium/"&gt;its project page&lt;/a&gt; and its &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests"&gt;test&lt;/a&gt;, but I'll give the basics here.&lt;/p&gt;
&lt;p&gt;The core object is &lt;code&gt;MfGames.Gallium.Entity&lt;/code&gt;. This is a &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/src/MfGames.Gallium/Entity.cs"&gt;small class&lt;/a&gt; that has an integer identifier and basically a &lt;code&gt;Dictionary&amp;lt;Type, object&amp;gt;&lt;/code&gt; called &lt;code&gt;Components&lt;/code&gt;. It has some methods to make it easier, but basically you can set or get components from it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// This creates an empty entity.
Entity entity1 = new();

// This creates an entity with four components:
//   &amp;quot;System.Int32&amp;quot;: 123
//   &amp;quot;System.String&amp;quot;: &amp;quot;bob&amp;quot;
//   &amp;quot;System.IO.FileInfo&amp;quot;: ...
//   &amp;quot;System.IO.DirectoryInfo&amp;quot;: ...
Entity entity2 = new()
    .Set(123)
    .Set(&amp;quot;bob&amp;quot;)
    .SetAll(
        new FileInfo(&amp;quot;/tmp/bob.txt&amp;quot;),
        new DirectoryInfo(&amp;quot;/tmp&amp;quot;)
    );

Assert.True(entity2.HasComponent&amp;lt;FileInfo&amp;gt;);
Assert.False(entity2.HasComponent&amp;lt;decimal&amp;gt;);

Assert.True(123, entity2.Get&amp;lt;int&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In effect, the class of the component is the key. There are a number of other functions, the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EntityTests.cs"&gt;test class&lt;/a&gt; is probably the best documentation at this point.&lt;/p&gt;
&lt;p&gt;The systems part of the system is the LINQ-inspired collection classes. Most of them work off &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; with the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EnumerableEntityTests.cs"&gt;test class&lt;/a&gt; being the bulk of the documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Set up a list of entities.
List&amp;lt;Entity&amp;gt; list = ...;

// This gets all the entities that have a FileInfo.
var fileOnlyList = list.WhereEntityHas&amp;lt;FileInfo&amp;gt;().ToList();

// This gets all entities that have both a FileInfo and an int.
var fileAndIntList = list.WhereEntityHasAll&amp;lt;FileInfo, int&amp;gt;().ToList();

// Get a modified list based on components. This looks for all
// entities that have a FileInfo and then adds a marker component,
// IsHtml, into it.
IsHtml isHtml = IsHtml.Instance;

var modifiedList = list
    .SelectEntity&amp;lt;FileInfo&amp;gt;((entity, file) =&amp;gt; entity.Set(isHtml))
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a note, &lt;code&gt;Entity&lt;/code&gt; is &amp;ldquo;mostly&amp;rdquo; immutable. The &lt;code&gt;Set&lt;/code&gt; command will return a new &lt;code&gt;Entity&lt;/code&gt; instance with the same ID but with a different set of components. The &amp;ldquo;mostly&amp;rdquo; bit comes that the objects that it points to can be mutated.&lt;/p&gt;
&lt;p&gt;🛈 The lack of formal documentation for Gallium and Nitride is one of the reasons this is still alpha or beta level code.&lt;/p&gt;
&lt;h2&gt;Operations&lt;/h2&gt;
&lt;p&gt;The next concept needed for today's post is an operation which extends &lt;code&gt;MfGames.Nitride.IOperation&lt;/code&gt;. These are just easy ways of creating some functionality that works on an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; that can be combined together.&lt;/p&gt;
&lt;p&gt;Naturally, there are some extension methods that make it easier to work with operations with an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;IEnumerable&amp;lt;Entity&amp;gt; entityList = ...;

IEnumerable&amp;lt;Entity&amp;gt; otherList = someOperation1.Run(entityList);

IEnumerable&amp;lt;Entity&amp;gt; resultList = otherList
    .Run(someOperation2)
    .Run(someOperation3)
    .Run(someOperation4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it relatively easy to create easy processing through composition of one or more operations. Alternatively, we could easily just work directly off the enumerable.&lt;/p&gt;
&lt;h2&gt;Zio&lt;/h2&gt;
&lt;p&gt;There is no question, I'm fond of the &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; library. In this case, Nitride uses two big features with it: to create an abstract view of the project that is independent of the working directory and the &lt;code&gt;UPath&lt;/code&gt; class which normalizes paths.&lt;/p&gt;
&lt;p&gt;If you remember from the previous posts, we set a root directory in the &lt;code&gt;Program.cs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static async Task&amp;lt;int&amp;gt; Main(string[] args)
{
    var rootDirectory = new DirectoryInfo(Environment.CurrentDirectory);

    var builder = new NitrideBuilder(args)
        .UseIO(rootDirectory)
        .UseModule&amp;lt;WebsiteModule&amp;gt;();

    return await builder.RunAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This caused an &lt;code&gt;Zio.IFileSystem&lt;/code&gt; to be injected in the system which treats that directory as the &lt;code&gt;/&lt;/code&gt;. So, in our case where we had our index file in &lt;code&gt;//src/pages/index.md&lt;/code&gt;, it has a &lt;code&gt;UPath&lt;/code&gt; of &lt;code&gt;/src/pages/index.md&lt;/code&gt;. It doesn't seem like a lot, but it means that all paths in the system are always relative to the root of the project and there is no need to pass a root directory throughout the system which means path operations are considerably simplified.&lt;/p&gt;
&lt;p&gt;And the &lt;code&gt;UPath&lt;/code&gt; ensures a consistent pattern for accessing so this works on Windows, Mac, and Linux without a problem. Not to mention, any issues with &lt;code&gt;..&lt;/code&gt; are handled automatically.&lt;/p&gt;
&lt;p&gt;It also simplifies writing tests.&lt;/p&gt;
&lt;h2&gt;Copy File in Six Files&lt;/h2&gt;
&lt;p&gt;Put all that together, we can use two operations to create a massively over-engineered copy file operation using Nitride. Admittedly, the complexity will make sense later, but at the moment, we're going to read the file in the &lt;code&gt;PagesPipeline&lt;/code&gt;, through &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, through &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;, and finally write it out in &lt;code&gt;OutputHtmlPipeline&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Making Some Noise&lt;/h3&gt;
&lt;p&gt;To start with, let's put some logging to show how everything flows from pipeline to pipeline. To do that, we're going to inject an &lt;code&gt;ILogger&amp;lt;&amp;gt;&lt;/code&gt; into the constructor into all four pipelines and then put a logging message into the last three (the &lt;code&gt;PagesPipeline&lt;/code&gt; is going to do something different).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Content/SimplifiedMarkdownPipeline.cs
public class SimplifiedMarkdownPipeline : PipelineBase
{
    private readonly ILogger _logger;

    public SimplifiedMarkdownPipeline(
        ILogger&amp;lt;SimplifiedMarkdownPipeline&amp;gt; logger,
        PagesPipeline pagesPipeline)
    {
        _logger = logger;

        AddDependency(pagesPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = entities.ToList();

        _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, Nitride uses &lt;a href="/tags/serilog/"&gt;Serilog&lt;/a&gt;, so using &lt;code&gt;{Count:N0}&lt;/code&gt; does the right thing even thought we are using a logging abstraction for the actual call. When we run this, we get the following output (I'm not going to show the &lt;code&gt;just build&lt;/code&gt; call for these today):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:00:46 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:00:46 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:00:46 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.1079413
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.1097142
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🛈 I moved &lt;code&gt;NuGet.config&lt;/code&gt; to the root level because there is where the &lt;code&gt;Website.sln&lt;/code&gt; file was located and it felt right.&lt;/p&gt;
&lt;h3&gt;Reading Files&lt;/h3&gt;
&lt;p&gt;Now, to read a file, we are going to use the &lt;code&gt;MfGames.Nitride.IO.ReadFiles&lt;/code&gt; operation which reads file from the Zio file system and passes them through. We've already included that NuGet package in a previous step, so nothing to do there.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.IO.Contents;
using MfGames.Nitride.Pipelines;
using Microsoft.Extensions.Logging;

namespace Generator.Pipelines.Inputs;

public class PagesPipeline : PipelineBase
{
    private readonly ILogger _logger;
    private readonly ReadFiles _readFiles;

    public PagesPipeline(
        ILogger&amp;lt;PagesPipeline&amp;gt; logger,
        ReadFiles readFiles)
    {
        _logger = logger;
        _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = _readFiles
            .Run(cancellationToken)
            .ToList();

        _logger.LogInformation(
            &amp;quot;Read in {Count:N0} files from /src/pages&amp;quot;,
            list.Count);

        foreach (var entity in list)
        {
            _logger.LogInformation(&amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
                entity.Get&amp;lt;UPath&amp;gt;(),
                entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
        }

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires a bit of explanation. We are using dependency injection to insert the &lt;code&gt;ReadFiles&lt;/code&gt; operation into the constructor. This is not a singleton, so you can have multiple &lt;code&gt;ReadFiles&lt;/code&gt; if you need to read from multiple locations (or use a globbing operator to do it in one).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;_readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the constructor, I configure it. I could do this in the &lt;code&gt;RunAsync&lt;/code&gt; function but I found it made it easier to parse the files when I configure in the constructor. Like most things in Nitride, this uses a fluent coding style where each object sets the properties and returns itself to make a nice chained list of calls.&lt;/p&gt;
&lt;p&gt;There are two properties being set, &lt;code&gt;Pattern&lt;/code&gt; and &lt;code&gt;RemovePathPrefix&lt;/code&gt;. I use a source generator to automatically create the &lt;code&gt;With...&lt;/code&gt; pattern for fluent coding. The first one tells which files to read, relative to the root directory. This happen to use a globbing that allows me to grab all Markdown files in the source directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = _readFiles
    .Run(cancellationToken)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This run the _readFiles operation and gets a list of the files it read in. (I'm also going to put this in the &lt;code&gt;OutputHtmlPipeline&lt;/code&gt; to show how everything writes out.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;foreach (var entity in list)
{
    _logger.LogInformation(
        &amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
        entity.Get&amp;lt;UPath&amp;gt;(),
        entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is just some debugging to show how things are working and how Gallium works. In this case, the read files sets two components: the &lt;code&gt;UPath&lt;/code&gt; for the path of the file and a &lt;code&gt;IBinaryContent&lt;/code&gt; which is an abstraction to the binary content. Since we haven't done anything to the file, this just points to the file system itself without loading it into memory. That way, we can easily deal with gigabyte-sized files without overloading the system.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;return list.ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, we return the list of files that we just read in. With this change, running the build gets us this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:20:03 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:20:03 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:20:03 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.9543639
[14:20:04 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.9559424
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, that single file read in is then passed through the other pipelines because each of them return their input list and then the final pipeline gets those same entities and paths.&lt;/p&gt;
&lt;h3&gt;Changing Paths&lt;/h3&gt;
&lt;p&gt;Now the problem with the above example is that we are writing our output to the same location as we are reading. That isn't optional. What we want to do is strip off the &lt;code&gt;/src/pages/typewriter&lt;/code&gt; from the beginning of the read in path and add &lt;code&gt;/build/typewriter/html&lt;/code&gt; in front of it. This is easily done with two additional operators, &lt;code&gt;RemovePathPrefix&lt;/code&gt; and &lt;code&gt;AddPathPrefix&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To start with, let's remove the prefix from the read methods to normalize the paths.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    RemovePathPrefix removePathPrefix)
{
    _logger = logger;
    _removePathPrefix = removePathPrefix.WithPathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
    _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then we chain the operation using an extension method on operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_removePathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since each operation also takes an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; and removes the same type, they can flow from one operation to another. When we run the build, the output line looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding the output prefix is done on the output step:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we already have a list of entities, we can just use the extension method to pipe it through the operation and produce the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:28:35 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason &lt;code&gt;PagesPipeline&lt;/code&gt; doesn't use &lt;code&gt;entities.Run(...)&lt;/code&gt; is because that is the first pipeline and the &lt;code&gt;entities&lt;/code&gt; is going to be an empty list. Instead, we need to make entities, which is why we call &lt;code&gt;_readFiles.Run()&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3&gt;Writing Files&lt;/h3&gt;
&lt;p&gt;And the last bit for today's post is to write out files. This, creatively enough, uses the &lt;code&gt;WriteFiles&lt;/code&gt; operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .Run(_writeFiles)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we've already done all the fancy operations to adjust the path so it writes out the files in the proper location:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ find build -type f
build/typewriter/html/index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convenience Methods&lt;/h3&gt;
&lt;p&gt;Now, adding and removing path prefixes is pretty common, so both the &lt;code&gt;ReadFiles&lt;/code&gt; and &lt;code&gt;WriteFiles&lt;/code&gt; have methods to do those in a single call.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles)
{
    _logger = logger;
    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles.WithAddPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;And there you go, an over-engineered system for copying a single file from one directory to another. In the next post of this series, we'll turn that Markdown file into an HTML page.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
</feed>
