Skip to content

Commit cc96062

Browse files
committed
Changed how posts are parsed to use a YAML parsing library. Added url to images for presentation slides. When displaying presentation slides, replace the relative path to a full path
1 parent a13db67 commit cc96062

File tree

11 files changed

+209
-201
lines changed

11 files changed

+209
-201
lines changed

src/ProgrammerAl.Site.Content/Posts/20240921-Presentation-AtlDevConf2024/post.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ Tags:
66
- C#
77
- .NET
88

9-
Slides:
10-
- https://raw.githubusercontent.com/ProgrammerAL/Presentations-2024/main/atlanta-developers-conference-2024/presentation.html
9+
Presentations:
10+
- Id: 1
11+
SlidesUrl: https://raw.githubusercontent.com/ProgrammerAL/Presentations-2024/main/atlanta-developers-conference-2024/presentation.html
12+
ImagesUrl: https://raw.githubusercontent.com/ProgrammerAL/Presentations-2024/refs/heads/main/atlanta-developers-conference-2024/presentation-images
1113

1214
---
1315

@@ -25,4 +27,3 @@ You can also view the slides in your browser by clicking <a href="/posts/2024092
2527
Writing code is only 40% of the work. Maintaining high-quality code is maybe another 20%. Reviewing PRs and updating dependencies are just some of the manual processes we have to deal with that regularly take us away from the fun part of development: writing code. But we're developers, and we can automate these processes. Nay, we MUST automate or risk becoming less efficient the longer we work on a codebase. Thankfully, the 20+ year lifespan of C# and .NET means it has some features built-in to help us enforce certain levels of code quality.
2628

2729
In this session, we'll look at the common pitfalls that take us away from writing code. We'll also look at different ways to automate those processes and discuss the pros and cons of those approaches. By the end, we'll have covered what you can do to set up your own pit of success for your C# projects.
28-

src/ProgrammerAl.Site/DynamicContentUpdater/DynamicContentUpdater.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
3030
<PackageReference Include="RazorLight" Version="2.3.1" />
3131
<PackageReference Include="Svg" Version="3.4.7" />
32+
<PackageReference Include="YamlDotNet" Version="16.1.3" />
3233
</ItemGroup>
3334

3435
<ItemGroup>
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Collections.Immutable;
4-
using System.Collections.ObjectModel;
3+
4+
using static DynamicContentUpdater.Entities.ParsedEntry;
55

66
namespace DynamicContentUpdater.Entities;
77

88
public record ParsedEntry(
99
string Title,
1010
DateOnly ReleaseDate,
1111
ImmutableArray<string> Tags,
12-
ImmutableArray<string> PresentationSlideUrls,
12+
ImmutableArray<PresentationEntry> Presentations,
1313
string Post,
14-
string FirstParagraph);
14+
string FirstParagraph)
15+
{
16+
public record PresentationEntry(int Id, string SlidesUrl, string SlideImagesUrl);
17+
}

src/ProgrammerAl.Site/DynamicContentUpdater/Outputters/PostMetadataOutputter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ public void Output(RuntimeConfig runtimeConfig, ImmutableArray<PostEntry> allPos
2222
foreach (var post in allPosts)
2323
{
2424
var comicLink = post.TryGetComicSvgLink(out var outComicLink) ? outComicLink : null;
25+
var presentations = post.Presentations.Select(x => new PostMetadata.PresentationData(x.Id, x.SlidesUrl, x.SlideImagesUrl)).ToImmutableArray();
2526
var metadata = new PostMetadata(
2627
Title: post.TitleHumanReadable,
2728
ComicImageLink: comicLink,
2829
ReleaseDate: post.ReleaseDate,
29-
PresentationSlideUrls: post.PresentationSlidesUrls);
30+
Presentations: presentations);
3031

3132
var outputDir = $"{runtimeConfig.OutputDirectory}/Posts/{post.TitleLink}";
3233
OutputUtils.WriteOutFileAsJson(metadata, outputDir, PostMetadata.FileName);
Lines changed: 97 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
using DynamicContentUpdater.Entities;
1+
#nullable enable
2+
using DynamicContentUpdater.Entities;
23
using DynamicContentUpdater.Utilities;
34

45
using ProgrammerAl.Site.DynamicContentUpdater;
5-
using ProgrammerAl.Site.Pages;
66

77
using System;
88
using System.Collections.Generic;
99
using System.Collections.Immutable;
1010
using System.Collections.ObjectModel;
1111
using System.Text;
1212

13+
using YamlDotNet.Serialization.NamingConventions;
14+
using YamlDotNet.Serialization;
15+
using ExCSS;
16+
using System.Linq;
17+
1318
namespace DynamicContentUpdater
1419
{
1520
public class PostParser
@@ -23,59 +28,96 @@ public PostParser(RuntimeConfig runtimeConfig)
2328

2429
public ParsedEntry ParseFromMarkdown(string rawEntry, string postName)
2530
{
26-
//Assumes a specific schema
27-
// Line 1: Title: <TITLE HERE>
28-
// Line 2: Published: <YYYY/MM/DD>
29-
// Line 3: Tags:
30-
// Line 4-??: - <Tag Name>
31-
// Line ??: PresentationSlides:
32-
// Line ??-??: - <Slide Url>
33-
// Header Ending: --- <Yes, 3 dashes>
34-
// ## <Some header. Usually "## Receiving the Quest">
35-
// First Paragraph
36-
// Rest of it: The blog post
37-
38-
int titleStartIndex = rawEntry.IndexOf("Title:");
39-
int titleEndIndex = rawEntry.IndexOf("\n", titleStartIndex);
40-
int titleLineLength = titleEndIndex - titleStartIndex;
41-
ReadOnlySpan<char> titleLine = rawEntry.AsSpan(titleStartIndex, titleLineLength);
42-
string title = ParseStringValueFromLine(titleLine);
43-
44-
int publishedDateStartIndex = rawEntry.IndexOf("Published:");
45-
int publishedDateEndIndex = rawEntry.IndexOf('\n', publishedDateStartIndex + 1);
46-
int publishedDateEndLength = publishedDateEndIndex - publishedDateStartIndex;
47-
ReadOnlySpan<char> publishedDateLine = rawEntry.AsSpan(publishedDateStartIndex, publishedDateEndLength);
48-
DateOnly publishedDate = ParseDateTimeFromLine(publishedDateLine);
49-
50-
int tagsLineStartIndex = rawEntry.IndexOf("Tags:");
51-
var tags = ParseListSection(rawEntry, tagsLineStartIndex);
52-
53-
int slidesLineStartIndex = rawEntry.IndexOf("Slides:");
54-
ImmutableArray<string> slideUrls;
55-
if (slidesLineStartIndex == -1)
56-
{
57-
slideUrls = ImmutableArray<string>.Empty;
58-
}
59-
else
60-
{
61-
slideUrls = ParseListSection(rawEntry, slidesLineStartIndex);
62-
}
31+
var deserializer = new DeserializerBuilder()
32+
.WithNamingConvention(PascalCaseNamingConvention.Instance)
33+
.Build();
34+
35+
int headerEndIndex = rawEntry.IndexOf("---");
36+
var headerText = rawEntry.Substring(0, headerEndIndex);
37+
38+
var properties = deserializer.Deserialize<PostPropertiesDto>(headerText);
39+
AssertProperties(properties);
40+
41+
var tags = properties!.Tags!.Select(x => x!).ToImmutableArray();
42+
var presentations = properties.Presentations!.Select(x => new ParsedEntry.PresentationEntry(x!.Id!.Value, x.SlidesUrl, x.SlideImagesUrl)).ToImmutableArray();
6343

64-
int headerCloseIndexStart = rawEntry.IndexOf("---") + 3;
65-
ReadOnlySpan<char> postSpan = rawEntry.AsSpan(headerCloseIndexStart + 1).Trim();
44+
var postStartIndex = headerEndIndex + 4;
45+
ReadOnlySpan<char> postSpan = rawEntry.AsSpan(postStartIndex).Trim();
6646
string post = SanitizePost(postSpan, postName);
6747

6848
string firstParagraphOfPost = GrabFirstParagraphOfPost(post);
6949

7050
return new ParsedEntry(
71-
title,
72-
publishedDate,
51+
properties.Title!,
52+
properties.Published!.Value,
7353
tags,
74-
slideUrls,
54+
presentations,
7555
post,
7656
firstParagraphOfPost);
7757
}
7858

59+
private void AssertProperties(PostPropertiesDto? properties)
60+
{
61+
if (properties is null)
62+
{
63+
throw new Exception($"Post properties object is null");
64+
}
65+
66+
AssertProperty(properties.Title, nameof(properties.Title));
67+
AssertProperty(properties.Published, nameof(properties.Published));
68+
AssertProperty(properties.Tags, nameof(properties.Tags));
69+
70+
//Presentations is optional
71+
if (properties.Presentations is object)
72+
{
73+
foreach (var presentation in properties.Presentations)
74+
{
75+
if (presentation is null)
76+
{
77+
throw new Exception($"Post presentation is null");
78+
}
79+
80+
AssertProperty(presentation.Id, nameof(presentation.Id));
81+
AssertProperty(presentation.SlidesUrl, nameof(presentation.SlidesUrl));
82+
AssertProperty(presentation.SlideImagesUrl, nameof(presentation.SlideImagesUrl));
83+
}
84+
}
85+
}
86+
87+
private void AssertProperty(string? value, string name)
88+
{
89+
if (string.IsNullOrWhiteSpace(value))
90+
{
91+
throw new Exception($"Post property is null or empty: {name}");
92+
}
93+
}
94+
95+
private void AssertProperty(int? value, string name)
96+
{
97+
if (!value.HasValue
98+
|| value.Value == 0)
99+
{
100+
throw new Exception($"Post property is null or empty: {name}");
101+
}
102+
}
103+
104+
private void AssertProperty(DateOnly? value, string name)
105+
{
106+
if (!value.HasValue)
107+
{
108+
throw new Exception($"Post property is null or empty: {name}");
109+
}
110+
}
111+
112+
private void AssertProperty(string?[]? value, string name)
113+
{
114+
if (value is null
115+
|| value.Any(x => string.IsNullOrWhiteSpace(x)))
116+
{
117+
throw new Exception($"Post array property is null has a null or empty item: {name}");
118+
}
119+
}
120+
79121
private string GrabFirstParagraphOfPost(string post)
80122
{
81123
string headerText = "##";
@@ -128,65 +170,20 @@ private string SanitizePost(ReadOnlySpan<char> postSpan, string postName)
128170

129171
return postText;
130172
}
173+
}
131174

132-
private ImmutableArray<string> ParseListSection(string text, int sectionTitleStartIndex)
133-
{
134-
var items = new List<string>();
135-
136-
var endOfLineIndex = text.IndexOf('\n', sectionTitleStartIndex);
137-
var endOfHeaderIndex = text.IndexOf("---");
138-
139-
//int startIndex = endOfLineIndex;
140-
var nextItemIndex = text.IndexOf('-', endOfLineIndex);
141-
while (nextItemIndex != -1
142-
&& nextItemIndex < endOfHeaderIndex)
143-
{
144-
var startIndex = nextItemIndex;
145-
if (text[startIndex] != '-')
146-
{
147-
break;
148-
}
149-
150-
startIndex++;//Skip the dash
151-
var endIndex = text.IndexOf('\n', startIndex + 1);
152-
153-
var length = endIndex - startIndex;
154-
var line = text.Substring(startIndex, length).Trim();
155-
156-
items.Add(line);
157-
158-
nextItemIndex = endIndex + 1;
159-
}
160-
161-
//while ((startIndex = localSpan.IndexOf('-') + 1) > 0)
162-
//{
163-
// localSpan = localSpan.Slice(startIndex);
164-
165-
// int endIndex = localSpan.IndexOf('\n');
166-
// string tag = localSpan.Slice(0, endIndex).Trim().ToString();
167-
// items.Add(tag);
168-
// localSpan = localSpan.Slice(endIndex + 1);
169-
//}
170-
171-
return items.ToImmutableArray();
172-
}
173-
174-
private string ParseStringValueFromLine(ReadOnlySpan<char> textLine)
175-
{
176-
int separatorIndex = textLine.IndexOf(':');
177-
if (separatorIndex > -1)
178-
{
179-
separatorIndex++;//index is first character after the colon
180-
return textLine.Slice(separatorIndex).Trim().ToString();
181-
}
182-
183-
return "Unknown Error";
184-
}
175+
public class PostPropertiesDto
176+
{
177+
public string? Title { get; set; }
178+
public DateOnly? Published { get; set; }
179+
public string?[]? Tags { get; set; }
180+
public PresentationDto?[]? Presentations { get; set; }
185181

186-
private DateOnly ParseDateTimeFromLine(ReadOnlySpan<char> textLine)
182+
public class PresentationDto
187183
{
188-
string dateTimeString = ParseStringValueFromLine(textLine);
189-
return DateOnly.ParseExact(dateTimeString, format: "yyyy/MM/dd", provider: null);
184+
public int? Id { get; set; }
185+
public string? SlidesUrl { get; set; }
186+
public string? SlideImagesUrl { get; set; }
190187
}
191188
}
192189
}

src/ProgrammerAl.Site/DynamicContentUpdater/Program.cs

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,22 @@ public static PostEntries LoadAllPostEntries(string contentPath, PostParser pars
112112
{
113113
var postNumber = i + 1;
114114

115+
var presentations = x.ParsedEntry.Presentations.Select(p => new PostEntry.PresentationEntry(p.Id, p.SlidesUrl, p.SlideImagesUrl)).ToImmutableArray();
116+
var postHtml = Markdown.ToHtml(x.ParsedEntry.Post, pipeline: markdownPipeline);
117+
var firstParagraphHtml = Markdown.ToHtml(x.ParsedEntry.FirstParagraph, pipeline: markdownPipeline);
118+
115119
return new PostEntry(
116-
postDirectoryLocalPath: x.PostDirectoryLocalPath,
117-
titleHumanReadable: x.ParsedEntry.Title,
118-
titleLink: x.PostName,//public url for the post, just the full name of it. ie 20210101-Title
119-
releaseDate: x.PostDate,
120-
tags: x.ParsedEntry.Tags,
121-
presentationSlidesUrls: x.ParsedEntry.PresentationSlideUrls,
122-
postMarkdown: x.ParsedEntry.Post,
123-
postHtml: Markdown.ToHtml(x.ParsedEntry.Post, pipeline: markdownPipeline),
124-
firstParagraphHtml: Markdown.ToHtml(x.ParsedEntry.FirstParagraph, pipeline: markdownPipeline),
125-
postNumber: postNumber,
126-
isDraft: false
120+
PostDirectoryLocalPath: x.PostDirectoryLocalPath,
121+
TitleHumanReadable: x.ParsedEntry.Title,
122+
TitleLink: x.PostName,//public url for the post, just the full name of it. ie draft_20210101-Title
123+
ReleaseDate: x.PostDate,
124+
Tags: x.ParsedEntry.Tags,
125+
Presentations: presentations,
126+
PostMarkdown: x.ParsedEntry.Post,
127+
PostHtml: postHtml,
128+
FirstParagraphHtml: firstParagraphHtml,
129+
PostNumber: postNumber,
130+
IsDraft: false
127131
);
128132
})
129133
.ToImmutableArray();
@@ -132,18 +136,21 @@ public static PostEntries LoadAllPostEntries(string contentPath, PostParser pars
132136
.Where(x => x.IsDraft)
133137
.Select((x, i) =>
134138
{
139+
var presentations = x.ParsedEntry.Presentations.Select(p => new PostEntry.PresentationEntry(p.Id, p.SlidesUrl, p.SlideImagesUrl)).ToImmutableArray();
140+
var postHtml = Markdown.ToHtml(x.ParsedEntry.Post, pipeline: markdownPipeline);
141+
var firstParagraphHtml = Markdown.ToHtml(x.ParsedEntry.FirstParagraph, pipeline: markdownPipeline);
135142
return new PostEntry(
136-
postDirectoryLocalPath: x.PostDirectoryLocalPath,
137-
titleHumanReadable: x.ParsedEntry.Title,
138-
titleLink: x.PostName,//public url for the post, just the full name of it. ie draft_20210101-Title
139-
releaseDate: x.PostDate,
140-
tags: x.ParsedEntry.Tags,
141-
presentationSlidesUrls: x.ParsedEntry.PresentationSlideUrls,
142-
postMarkdown: x.ParsedEntry.Post,
143-
postHtml: Markdown.ToHtml(x.ParsedEntry.Post, pipeline: markdownPipeline),
144-
firstParagraphHtml: Markdown.ToHtml(x.ParsedEntry.FirstParagraph, pipeline: markdownPipeline),
145-
postNumber: -1,
146-
isDraft: true
143+
PostDirectoryLocalPath: x.PostDirectoryLocalPath,
144+
TitleHumanReadable: x.ParsedEntry.Title,
145+
TitleLink: x.PostName,//public url for the post, just the full name of it. ie draft_20210101-Title
146+
ReleaseDate: x.PostDate,
147+
Tags: x.ParsedEntry.Tags,
148+
Presentations: presentations,
149+
PostMarkdown: x.ParsedEntry.Post,
150+
PostHtml: postHtml,
151+
FirstParagraphHtml: firstParagraphHtml,
152+
PostNumber: -1,
153+
IsDraft: true
147154
);
148155
})
149156
.ToImmutableArray();

src/ProgrammerAl.Site/ProgrammerAl.Site/Pages/PresentationSlides.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
<meta name="og:title" content="@(PostData.Metadata.Title)" />
1414
<meta name="twitter:title" content="@(PostData.Metadata.Title)" />
15-
<meta property="og:url" content="@($"https://ProgrammerAL.com/posts/{PostUrl}/slides/{Index}")" />
16-
<meta property="twitter:url" content="@($"https://ProgrammerAL.com/posts/{PostUrl}/slides/{Index}")" />
15+
<meta property="og:url" content="@($"https://ProgrammerAL.com/posts/{PostUrl}/slides/{Id}")" />
16+
<meta property="twitter:url" content="@($"https://ProgrammerAL.com/posts/{PostUrl}/slides/{Id}")" />
1717
</HeadContent>
1818
}
1919

0 commit comments

Comments
 (0)