Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ _site
/.nuke/temp
/build/bin
/build/obj
/Build/bin
/Build/obj
77 changes: 76 additions & 1 deletion Build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ string ProcessPage(AbsolutePath pageFile)
rawContent = StripFrontmatter(rawContent);
var content = string.IsNullOrEmpty(category) ? rawContent : BuildCategorySection(category);
content = ApplySiteReplacements(content);
content = TransformAlertsForPandoc(content);
return string.IsNullOrEmpty(title) ? content : $"<h1>{title}</h1>\n{content}";
}

Expand Down Expand Up @@ -180,6 +181,80 @@ static string ApplySiteReplacements(string content)
@"\(\/.+?(#\w+)\)", "($1)");
}

static string TransformAlertsForPandoc(string content)
{
var lines = content.Replace("\r\n", "\n").Split('\n');
var transformed = new List<string>(lines.Length);

for (var index = 0; index < lines.Length;)
{
if (!TryTransformAlert(lines, ref index, transformed))
{
transformed.Add(lines[index]);
index++;
}
}

return string.Join("\n", transformed);
}

static bool TryTransformAlert(string[] lines, ref int index, List<string> transformed)
{
var match = Regex.Match(lines[index], @"^> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$");
if (!match.Success)
return false;

var alertType = match.Groups[1].Value;
var alertLines = new List<string>();
index++;

while (index < lines.Length && lines[index].StartsWith(">"))
{
alertLines.Add(lines[index].Length == 1 ? "" : lines[index][2..]);
index++;
}

if (alertLines.Count == 0)
{
transformed.Add(lines[index - 1]);
return true;
}

var title = alertType switch
{
"NOTE" => "Note",
"TIP" => "Tip",
"IMPORTANT" => "Important",
"WARNING" => "Warning",
"CAUTION" => "Caution",
_ => alertType
};

var firstContentLineIndex = alertLines.FindIndex(line => !string.IsNullOrWhiteSpace(line));
if (firstContentLineIndex >= 0 && alertType == "IMPORTANT")
{
var exceptionMatch = Regex.Match(alertLines[firstContentLineIndex], @"^(Exceptions?):\s*(.*)$");
if (exceptionMatch.Success)
{
title = exceptionMatch.Groups[1].Value;
alertLines[firstContentLineIndex] = exceptionMatch.Groups[2].Value;
}
}

if (firstContentLineIndex >= 0)
{
var firstLine = alertLines[firstContentLineIndex];
alertLines[firstContentLineIndex] = string.IsNullOrWhiteSpace(firstLine)
? $"**{title}:**"
: $"**{title}:** {firstLine}";
}

foreach (var alertLine in alertLines)
transformed.Add(alertLine.Length == 0 ? ">" : $"> {alertLine}");

return true;
}

static void AppendRuleIfInCategory(StringBuilder content, AbsolutePath ruleFile, string category)
{
var rule = ruleFile.ReadAllText();
Expand Down Expand Up @@ -372,4 +447,4 @@ static string ExtractFrontmatterField(string content, string fieldName)
var match = Regex.Match(content, $@"---(.|\n)*?{Regex.Escape(fieldName)}\:\s*([^\r\n]+)");
return match.Success ? match.Groups[2].Value.Trim() : string.Empty;
}
}
}
52 changes: 51 additions & 1 deletion _includes/footer/custom.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
<!-- start custom footer snippets -->

<!-- end custom footer snippets -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const calloutTitles = {
note: "Note",
tip: "Tip",
important: "Important",
warning: "Warning",
caution: "Caution"
};

document.querySelectorAll(".page__content blockquote").forEach((blockquote) => {
const firstParagraph = blockquote.querySelector("p");
if (!firstParagraph) {
return;
}

const alertMatch = firstParagraph.innerHTML.match(/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:<br\s*\/?>)?\s*/i);
if (!alertMatch) {
return;
}

const alertType = alertMatch[1].toLowerCase();
let title = calloutTitles[alertType] || alertMatch[1];
let contentHtml = firstParagraph.innerHTML.replace(alertMatch[0], "");

if (alertType === "important") {
const exceptionMatch = contentHtml.match(/^\s*(Exceptions?):\s*/i);
if (exceptionMatch) {
title = exceptionMatch[1];
contentHtml = contentHtml.replace(exceptionMatch[0], "");
blockquote.classList.add("callout--exception");
}
}

blockquote.classList.add("callout", `callout--${alertType}`);

const titleElement = document.createElement("p");
titleElement.className = "callout__title";
titleElement.textContent = title;
blockquote.insertBefore(titleElement, blockquote.firstChild);

if (contentHtml.trim().length === 0) {
firstParagraph.remove();
} else {
firstParagraph.innerHTML = contentHtml;
}
});
});
</script>

<!-- end custom footer snippets -->
3 changes: 2 additions & 1 deletion _rules/0100.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Understanding these boundaries matters for several reasons:
- It guides when to introduce an abstraction (across a boundary) versus when to skip it (within a boundary).
- It helps decide when to duplicate small pieces of logic instead of pulling in a shared dependency that would increase coupling.

**Tip:** When in doubt whether something belongs inside or outside a boundary, ask yourself: "If this boundary were a separate deployable unit, would this dependency still make sense?"
> [!TIP]
> When in doubt whether something belongs inside or outside a boundary, ask yourself: "If this boundary were a separate deployable unit, would this dependency still make sense?"
3 changes: 2 additions & 1 deletion _rules/0110.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Prefer composition when:
- You want to reuse behavior without exposing or depending on the internals of another class.
- You need to vary behavior at runtime (e.g. through the [Strategy pattern](https://en.wikipedia.org/wiki/Strategy_pattern)).

**Exception:** Inheritance is appropriate when a true "is-a" relationship exists and when derived classes genuinely extend (rather than replace) the behavior of their base class. Always follow the [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) when using inheritance.
> [!IMPORTANT]
> Exception: Inheritance is appropriate when a true "is-a" relationship exists and when derived classes genuinely extend (rather than replace) the behavior of their base class. Always follow the [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) when using inheritance.
6 changes: 4 additions & 2 deletions _rules/0125.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ This applies primarily to:

For complex or domain-critical logic that must be consistent everywhere, a shared library is still the right choice. But always consider the [Rule of Three](https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)) as a practical guide: refactor duplication after the third occurrence and you've identified a real pattern. Also consider a source-only package as it avoids the binary dependencies normal packages have. Check out [Reflectify](https://github.com/dennisdoomen/reflectify?tab=readme-ov-file#readme) or [Pathy](https://github.com/dennisdoomen/pathy?tab=readme-ov-file#readme) for practical examples of that.

**Exception:**
Duplication in tests is often beneficial as it will make the tests easier to understand without the need to dig into all kinds of shared helper methods.
> [!IMPORTANT]
> Exception:
>
> Duplication in tests is often beneficial as it will make the tests easier to understand without the need to dig into all kinds of shared helper methods.
15 changes: 10 additions & 5 deletions _rules/1000.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ title: A class or interface should have a single purpose
---
A class or interface should have a single purpose within the system it functions in. In general, a class either represents a primitive type like an email address or ISBN number, an abstraction of some business concept, a plain data structure, or is responsible for orchestrating the interaction between other classes. It is never a combination of those. This rule is widely known as the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle), one of the S.O.L.I.D. principles.

**Tip:** A class with the word `And` in it is an obvious violation of this rule.
> [!TIP]
> A class with the word `And` in it is an obvious violation of this rule.

**Tip:** Use [Design Patterns](http://en.wikipedia.org/wiki/Design_pattern_(computer_science)) to communicate the intent of a class. If you can't assign a single design pattern to a class, chances are that it is doing more than one thing.
> [!TIP]
> Use [Design Patterns](http://en.wikipedia.org/wiki/Design_pattern_(computer_science)) to communicate the intent of a class. If you can't assign a single design pattern to a class, chances are that it is doing more than one thing.

**Tip:** If you can't find a good name for a class, or struggle to document what it does, it is probably doing too much.
> [!TIP]
> If you can't find a good name for a class, or struggle to document what it does, it is probably doing too much.

**Tip:** If you describe what a class does to an AI assistant and it identifies multiple responsibilities, that's a strong signal to split it up.
> [!TIP]
> If you describe what a class does to an AI assistant and it identifies multiple responsibilities, that's a strong signal to split it up.

**Note** If you create a class representing a primitive type you can greatly simplify its use by making it immutable. Consider using an immutable record type for such cases.
> [!NOTE]
> If you create a class representing a primitive type you can greatly simplify its use by making it immutable. Consider using an immutable record type for such cases.
3 changes: 2 additions & 1 deletion _rules/1002.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ For `internal` types, this is straightforward to apply: since callers are within

For `public` types, be more conservative. Removing a constructor parameter is a breaking API change, so consider whether the dependency is likely to be needed by future members before deciding to keep it out of the constructor.

**Exception:** Cross-cutting concerns such as logging or a clock abstraction (`TimeProvider`) are often needed broadly and may reasonably be injected through the constructor even if not every member uses them directly.
> [!IMPORTANT]
> Exception: Cross-cutting concerns such as logging or a clock abstraction (`TimeProvider`) are often needed broadly and may reasonably be injected through the constructor even if not every member uses them directly.
3 changes: 2 additions & 1 deletion _rules/1003.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ title: An interface should be small and focused
---
Interfaces should have a name that clearly explains their purpose or role in the system. Do not combine many vaguely related members on the same interface just because they were all on the same class. Separate the members based on the responsibility of those members, so that callers only need to call or implement the interface related to a particular task. This rule is more commonly known as the [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle).

**Tip:** Avoid taking the name of the class and slapping an `I` in front of it. Instead, consider using role-based names like `IFetchSomething` or `IProvideClusterwideLock` that describe what the interface does rather than what class implements it.
> [!TIP]
> Avoid taking the name of the class and slapping an `I` in front of it. Instead, consider using role-based names like `IFetchSomething` or `IProvideClusterwideLock` that describe what the interface does rather than what class implements it.
3 changes: 2 additions & 1 deletion _rules/1008.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ title: Avoid static classes
---
With the exception of extension method containers, static classes very often lead to badly designed code. They are also very difficult, if not impossible, to test in isolation, unless you're willing to use some very hacky tools.

**Note:** If you really need that static class, mark it as static so that the compiler can prevent instance members and instantiating your class. This relieves you of creating an explicit private constructor.
> [!NOTE]
> If you really need that static class, mark it as static so that the compiler can prevent instance members and instantiating your class. This relieves you of creating an explicit private constructor.
3 changes: 2 additions & 1 deletion _rules/1011.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ public class Square : Shape
}
```

**Note:** This rule is also known as the Liskov Substitution Principle, one of the [S.O.L.I.D.](http://www.lostechies.com/blogs/chad_myers/archive/2008/03/07/pablo-s-topic-of-the-month-march-solid-principles.aspx) principles.
> [!NOTE]
> This rule is also known as the Liskov Substitution Principle, one of the [S.O.L.I.D.](http://www.lostechies.com/blogs/chad_myers/archive/2008/03/07/pablo-s-topic-of-the-month-march-solid-principles.aspx) principles.
6 changes: 4 additions & 2 deletions _rules/1014.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ If you find yourself writing code like this then you might be violating the [Law

An object should not expose any other classes it depends on because callers may misuse that exposed property or method to access the object behind it. By doing so, you allow calling code to become coupled to the class you are using, and thereby limiting the chance that you can easily replace it in a future stage.

**Note:** Using a class that is designed using the [Fluent Interface](http://en.wikipedia.org/wiki/Fluent_interface) pattern seems to violate this rule, but it is simply returning itself so that method chaining is allowed.
> [!NOTE]
> Using a class that is designed using the [Fluent Interface](http://en.wikipedia.org/wiki/Fluent_interface) pattern seems to violate this rule, but it is simply returning itself so that method chaining is allowed.

**Exception:** Inversion of Control or Dependency Injection frameworks often require you to expose a dependency as a public property. As long as this property is not used for anything other than dependency injection I would not consider it a violation.
> [!IMPORTANT]
> Exception: Inversion of Control or Dependency Injection frameworks often require you to expose a dependency as a public property. As long as this property is not used for anything other than dependency injection I would not consider it a violation.
3 changes: 2 additions & 1 deletion _rules/1020.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ title: Avoid bidirectional dependencies
---
This means that two classes know about each other's public members or rely on each other's internal behavior. Refactoring or replacing one of those classes requires changes on both parties and may involve a lot of unexpected work. The most obvious way of breaking that dependency is to introduce an interface for one of the classes and using Dependency Injection.

**Exception:** Domain models such as defined in [Domain-Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) tend to occasionally involve bidirectional associations that model real-life associations. In those cases, make sure they are really necessary, and if they are, keep them in.
> [!IMPORTANT]
> Exception: Domain models such as defined in [Domain-Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) tend to occasionally involve bidirectional associations that model real-life associations. In those cases, make sure they are really necessary, and if they are, keep them in.
3 changes: 2 additions & 1 deletion _rules/1025.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ title: Classes should have state and behavior
---
In general, if you find a lot of data-only classes in your code base, you probably also have a few (static) classes with a lot of behavior (see [{{ site.default_rule_prefix }}1008](#{{ site.default_rule_prefix }}1008)). Use the principles of object-orientation explained in this section and move the logic close to the data it applies to.

**Exception:** The only exceptions to this rule are classes that are used to transfer data over a communication channel, also called [Data Transfer Objects](http://martinfowler.com/eaaCatalog/dataTransferObject.html), or a class that wraps several parameters of a method. For DTOs, consider using an immutable `record` type instead of a class to make the intent explicit.
> [!IMPORTANT]
> Exception: The only exceptions to this rule are classes that are used to transfer data over a communication channel, also called [Data Transfer Objects](http://martinfowler.com/eaaCatalog/dataTransferObject.html), or a class that wraps several parameters of a method. For DTOs, consider using an immutable `record` type instead of a class to make the intent explicit.
3 changes: 2 additions & 1 deletion _rules/1030.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Use a **record** when:
public record OrderSummary(Guid OrderId, decimal TotalAmount, DateTimeOffset CreatedAt);
```

**Note:** Record equality is shallow. If a record property is a collection (e.g. `List<T>` or an array), `Equals` and `GetHashCode` will not compare the contents, which can lead to subtle bugs.
> [!NOTE]
> Record equality is shallow. If a record property is a collection (e.g. `List<T>` or an array), `Equals` and `GetHashCode` will not compare the contents, which can lead to subtle bugs.

Use a **record struct** when the same criteria apply, but you also want value-type semantics: stack allocation, no null, and copy-on-assignment. This is appropriate for small, frequently passed immutable data such as coordinates, colors, or date ranges. Avoid mutable `record struct` — mutations on a copy don't affect the original, which leads to confusing bugs.

Expand Down
3 changes: 2 additions & 1 deletion _rules/1032.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ Prefer a single-method interface when:
- You want to use Default Interface Methods
- The interface is expected to gain additional members in the near future

**Note:** A simple lambda is technically compatible with a named delegate, but a named delegate offers better discoverability: find-usages, go-to-definition, and DI registration all work the same as with an interface.
> [!NOTE]
> A simple lambda is technically compatible with a named delegate, but a named delegate offers better discoverability: find-usages, go-to-definition, and DI registration all work the same as with an interface.

3 changes: 2 additions & 1 deletion _rules/1105.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ title: Use a method instead of a property
- If it returns a different result each time it is called, even if the arguments didn't change. For example, the `NewGuid` method returns a different value each time it is called.
- If the operation causes a side effect such as changing some internal state not directly related to the property (which violates the [Command Query Separation](http://martinfowler.com/bliki/CommandQuerySeparation.html) principle).

**Exception:** Populating an internal cache or implementing [lazy-loading](http://www.martinfowler.com/eaaCatalog/lazyLoad.html) is a good exception.
> [!IMPORTANT]
> Exception: Populating an internal cache or implementing [lazy-loading](http://www.martinfowler.com/eaaCatalog/lazyLoad.html) is a good exception.
3 changes: 2 additions & 1 deletion _rules/1130.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ You generally don't want callers to be able to change an internal collection, so

Be aware that `IEnumerable<T>` is often perceived as lazy-evaluated. If your collection is already materialized, consider returning `IReadOnlyCollection<T>` or `IReadOnlyList<T>` to make the intent clear.

**Exception:** Immutable collections such as `ImmutableArray<T>`, `ImmutableList<T>` and `ImmutableDictionary<TKey, TValue>`, as well as `FrozenSet<T>` and `FrozenDictionary<TKey, TValue>` prevent modifications from the outside and are thus allowed.
> [!IMPORTANT]
> Exception: Immutable collections such as `ImmutableArray<T>`, `ImmutableList<T>` and `ImmutableDictionary<TKey, TValue>`, as well as `FrozenSet<T>` and `FrozenDictionary<TKey, TValue>` prevent modifications from the outside and are thus allowed.
Loading
Loading