feat!: mass updating dependencies with repository merge
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
D. Moonfire 2023-07-10 12:18:30 -05:00
parent 6135a3fdbd
commit 2d7de86856
347 changed files with 18934 additions and 0 deletions

View file

@ -1366,5 +1366,6 @@ using(DataAccessAdapter dataAccessAdapter = new DataAccessAdapter(ConnectionStri
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue">2.0</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=F87CBA43E9CDCC41A45B39A2A2A25764/Scope/=2C285F182AC98D44B0B4F29D4D2149EC/Type/@EntryValue">InCSharpStatement</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gemtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tocks/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

View file

@ -0,0 +1,508 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
namespace MfGames.Gallium;
/// <summary>
/// A low-overhead entity with identification.
/// </summary>
public record Entity
{
public Entity()
: this(Interlocked.Increment(ref nextId))
{
}
private Entity(int id)
{
this.Id = id;
this.Components = ImmutableDictionary.Create<Type, object>();
}
/// <inheritdoc />
public virtual bool Equals(Entity? other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return this.Id == other.Id;
}
/// <inheritdoc />
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
private ImmutableDictionary<Type, object> Components { get; init; }
/// <summary>
/// The internal ID to ensure the entities are unique. Since we are not
/// worried about serialization or using the identifiers from one call
/// to another, we can use a simple interlocked identifier instead of
/// a factory or provider method.
/// </summary>
private static int nextId;
/// <summary>
/// Gets a value indicating whether the entity has a specific type of
/// component registered.
/// </summary>
/// <typeparam name="T1">The component type.</typeparam>
/// <returns>True if the type exists, otherwise false.</returns>
public bool Has<T1>()
{
return this.Has(typeof(T1));
}
/// <summary>
/// Gets a value indicating whether the entity has components of the given types
/// registered.
/// </summary>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <returns>
/// True if there are components of the given type exists, otherwise
/// false.
/// </returns>
public bool HasAll<T1, T2>()
{
return this.HasAll(typeof(T1), typeof(T2));
}
/// <summary>
/// Gets a value indicating whether the entity has components of the given types
/// registered.
/// </summary>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <typeparam name="T3">The third component type.</typeparam>
/// <returns>
/// True if there are components of the given type exists, otherwise
/// false.
/// </returns>
public bool HasAll<T1, T2, T3>()
{
return this.HasAll(typeof(T1), typeof(T2), typeof(T3));
}
/// <summary>
/// Gets a value indicating whether the entity has components of the given types
/// registered.
/// </summary>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <typeparam name="T3">The third component type.</typeparam>
/// <typeparam name="T4">The third component type.</typeparam>
/// <returns>
/// True if there are components of the given type exists, otherwise
/// false.
/// </returns>
public bool HasAll<T1, T2, T3, T4>()
{
return this.HasAll(typeof(T1), typeof(T2), typeof(T3), typeof(T4));
}
/// <summary>
/// Gets a value indicating whether the entity has a specific type of
/// component registered.
/// </summary>
/// <param name="type">The component type.</param>
/// <returns>True if the type exists, otherwise false.</returns>
public bool Has(Type type)
{
return this.Components.ContainsKey(type);
}
/// <summary>
/// Gets a value indicating whether the entity has components for all the given
/// types.
/// </summary>
/// <param name="t1">The component type.</param>
/// <param name="t2">The component type.</param>
/// <returns>True if the type exists, otherwise false.</returns>
public bool HasAll(
Type t1,
Type t2)
{
return this.Has(t1) && this.Components.ContainsKey(t2);
}
/// <summary>
/// Gets a value indicating whether the entity has components for all the given
/// types.
/// </summary>
/// <param name="t1">The component type.</param>
/// <param name="t2">The component type.</param>
/// <param name="t3">The component type.</param>
/// <returns>True if the type exists, otherwise false.</returns>
public bool HasAll(
Type t1,
Type t2,
Type t3)
{
return this.HasAll(t1, t2) && this.Components.ContainsKey(t3);
}
/// <summary>
/// Gets a value indicating whether the entity has components for all the given
/// types.
/// </summary>
/// <param name="t1">The component type.</param>
/// <param name="t2">The component type.</param>
/// <param name="t3">The component type.</param>
/// <param name="t4">The component type.</param>
/// <returns>True if the type exists, otherwise false.</returns>
public bool HasAll(
Type t1,
Type t2,
Type t3,
Type t4)
{
return this.HasAll(t1, t2, t3) && this.Components.ContainsKey(t4);
}
/// <summary>
/// Retrieves a registered component of the given type.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The registered object.</returns>
public TType Get<TType>()
{
return (TType)this.Components[typeof(TType)];
}
/// <summary>
/// Retrieves a registered component of the given type and casts it to
/// TType.
/// </summary>
/// <param name="type">The component key.</param>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The registered object.</returns>
public TType Get<TType>(Type type)
{
return (TType)this.Components[type];
}
/// <summary>
/// Gets the number of components registered in the entity.
/// </summary>
public int Count => this.Components.Count;
/// <summary>
/// Gets the given component type if inside the entity, otherwise the
/// default value.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The found component or default (typically null).</returns>
public TType? GetOptional<TType>()
{
return this.Has<TType>() ? this.Get<TType>() : default;
}
/// <summary>
/// Attempts to get the value, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value">The value if contained in the entity.</param>
/// <typeparam name="T1">The component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1>(out T1 value)
{
if (this.Has<T1>())
{
value = this.Get<T1>();
return true;
}
value = default!;
return false;
}
/// <summary>
/// Attempts to get the values, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value1">The value if contained in the entity.</param>
/// <param name="value2">The value if contained in the entity.</param>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1, T2>(
out T1 value1,
out T2 value2)
{
if (this.HasAll<T1, T2>())
{
value1 = this.Get<T1>();
value2 = this.Get<T2>();
return true;
}
value1 = default!;
value2 = default!;
return false;
}
/// <summary>
/// Attempts to get the values, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value1">The value if contained in the entity.</param>
/// <param name="value2">The value if contained in the entity.</param>
/// <param name="value3">The value if contained in the entity.</param>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <typeparam name="T3">The third component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1, T2, T3>(
out T1 value1,
out T2 value2,
out T3 value3)
{
if (this.HasAll<T1, T2, T3>())
{
value1 = this.Get<T1>();
value2 = this.Get<T2>();
value3 = this.Get<T3>();
return true;
}
value1 = default!;
value2 = default!;
value3 = default!;
return false;
}
/// <summary>
/// Attempts to get the values, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value1">The value if contained in the entity.</param>
/// <param name="value2">The value if contained in the entity.</param>
/// <param name="value3">The value if contained in the entity.</param>
/// <param name="value4">The value if contained in the entity.</param>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <typeparam name="T3">The third component type.</typeparam>
/// <typeparam name="T4">The fourth component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1, T2, T3, T4>(
out T1 value1,
out T2 value2,
out T3 value3,
out T4 value4)
{
if (this.HasAll<T1, T2, T3, T4>())
{
value1 = this.Get<T1>();
value2 = this.Get<T2>();
value3 = this.Get<T3>();
value4 = this.Get<T4>();
return true;
}
value1 = default!;
value2 = default!;
value3 = default!;
value4 = default!;
return false;
}
/// <summary>
/// Sets the component in the entity, regardless if there was a
/// component already registered.
/// </summary>
/// <param name="component">The component to register.</param>
/// <typeparam name="T1">The component type.</typeparam>
/// <returns>The entity for chaining.</returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Set<T1>(T1 component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (this.Components.TryGetValue(typeof(T1), out object? value)
&& value is T1
&& value.Equals(component))
{
return this;
}
return this with
{
Components = this.Components.SetItem(typeof(T1), component),
};
}
/// <summary>
/// Sets zero or more components into an entity in a single call. This does
/// not allow for specifying the data type; each item will be added with
/// the result of `component.GetType()`.
/// </summary>
/// <param name="components">
/// The components to add to the entity. Any null objects
/// will be ignored.
/// </param>
/// <returns>
/// A new Entity with the modified component collection if there is at
/// least one component to set, otherwise the same entity.
/// </returns>
public Entity SetAll(params object?[] components)
{
if (components.Length == 0)
{
return this;
}
ImmutableDictionary<Type, object> collection = this.Components;
foreach (object? component in components)
{
if (component != null)
{
collection = collection.SetItem(component.GetType(), component);
}
}
return this with
{
Components = collection,
};
}
/// <summary>
/// Adds a component to the entity.
/// </summary>
/// <param name="component">The component to register.</param>
/// <typeparam name="T1">The component type.</typeparam>
/// <returns>
/// The same entity if the component is already registered, otherwise a
/// cloned entity with the new component.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Add<T1>(T1 component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (this.Has<T1>())
{
throw new ArgumentException(
"An element with the same type ("
+ typeof(T1).FullName
+ ") already exists.",
nameof(component));
}
if (this.Components.TryGetValue(typeof(T1), out object? value)
&& value is T1
&& value.Equals(component))
{
return this;
}
return this with
{
Components = this.Components.Add(typeof(T1), component)
};
}
/// <summary>
/// Removes a component to the entity.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>
/// The same entity if the component is already removed, otherwise a
/// cloned entity without the new component.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Remove<TType>()
{
return this.Remove(typeof(TType));
}
/// <summary>
/// Removes a component to the entity.
/// </summary>
/// <returns>
/// The same entity if the component is already removed, otherwise a
/// cloned entity without the new component.
/// </returns>
/// <param name="type">The component type to remove.</param>
public Entity Remove(Type type)
{
if (!this.Has(type))
{
return this;
}
return this with
{
Components = this.Components.Remove(type)
};
}
/// <summary>
/// Gets the identifier of the entity. This should be treated as an
/// opaque field.
/// </summary>
public int Id { get; private init; }
/// <summary>
/// Creates a copy of the entity, including copying the identifier.
/// </summary>
/// <returns></returns>
public Entity ExactCopy()
{
return this with { };
}
/// <summary>
/// Creates a copy of the entity, including components, but with a new
/// identifier.
/// </summary>
/// <returns></returns>
public Entity Copy()
{
return this with
{
Id = Interlocked.Increment(ref nextId)
};
}
/// <summary>
/// Retrieves a list of the component types currently registered in the
/// Entity.
/// </summary>
/// <returns>An enumerable of the various component keys.</returns>
public IEnumerable<Type> GetComponentTypes()
{
return this.Components.Keys;
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Gallium-"

View file

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
public static class JoinEntityExtensions
{
/// <summary>
/// Merges two sets of entities using the identifier to determine which
/// entities are the same. The `merge` function takes both of the
/// entities with the Entity from the `input` first and the one from
/// `other` second. The returning entity is put into the collection. If
/// an entity from the input is not found in other, then it is just
/// passed on.
/// </summary>
/// <param name="input">The enumerable of entities to merge to.</param>
/// <param name="other">The collection of entities to merge from.</param>
/// <param name="merge">The callback to merge the two.</param>
/// <returns>An sequence of entities, merged and unmerged.</returns>
public static IEnumerable<Entity> JoinEntity(
this IEnumerable<Entity> input,
ICollection<Entity> other,
Func<Entity, Entity, Entity> merge)
{
return input.Join(other, a => a.Id, a => a.Id, merge);
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>An entity-component-system (ECS) based on LINQ and IEnumerable.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace MfGames.Gallium;
/// <summary>
/// Extension methods for selecting components from a list.
/// </summary>
public static class SelectComponentExtensions
{
/// <summary>
/// Retrieves a component from an entity and return it. If the entity does not have
/// the component, it will be
/// filtered out.
/// </summary>
/// <param name="entities">The entities to process.</param>
/// <typeparam name="T1">The component type being searched.</typeparam>
/// <returns>A sequence of T1.</returns>
public static IEnumerable<T1> SelectComponent<T1>(
this IEnumerable<Entity> entities)
{
foreach (Entity entity in entities)
{
if (entity.TryGet(out T1 v1))
{
yield return v1;
}
}
}
/// <summary>
/// Retrieves a component from an entity and return it. If the entity does not have
/// the component, it will be filtered out.
/// </summary>
/// <param name="entities">The entities to process.</param>
/// <param name="t1">The component type being searched.</param>
/// <returns>A sequence of T1.</returns>
public static IEnumerable<object> SelectComponent(
IEnumerable<Entity> entities,
Type t1)
{
foreach (Entity entity in entities)
{
if (entity.Has(t1))
{
yield return entity.Get<object>(t1);
}
}
}
}

View file

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
namespace MfGames.Gallium;
/// <summary>
/// Extension methods for selecting components from a list.
/// </summary>
public static class SelectComponentOrDefaultExtensions
{
/// <summary>
/// Retrieves a component from an entity and return it. If the entity does not have
/// the component, then null will be returned.
/// </summary>
/// <param name="entities">The entities to process.</param>
/// <param name="t1">The component type being searched.</param>
/// <returns>A sequence of T1 or nulls.</returns>
public static IEnumerable<object?> SelectComponent(
IEnumerable<Entity> entities,
Type t1)
{
foreach (Entity entity in entities)
{
if (entity.Has(t1))
{
yield return entity.Get<object>(t1);
}
yield return null;
}
}
/// <summary>
/// Retrieves a component from an entity and return it. If the entity does not have
/// the component, then the default value will be returned.
/// </summary>
/// <param name="entities">The entities to process.</param>
/// <typeparam name="T1">The component type being searched.</typeparam>
/// <returns>A sequence of T1.</returns>
public static IEnumerable<T1?> SelectComponentOrDefault<T1>(
this IEnumerable<Entity> entities)
{
foreach (Entity entity in entities)
{
if (entity.TryGet(out T1 v1))
{
yield return v1;
}
yield return default;
}
}
}

View file

@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
namespace MfGames.Gallium;
public static class SelectEntityExtensions
{
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, then entities without all the components are included. Otherwise, they
/// are excluded.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1>(
this IEnumerable<Entity> entities,
Func<Entity, T1, Entity?> selectWithComponents,
bool includeEntitiesWithoutComponents = true)
{
return entities.SelectEntity(
selectWithComponents,
includeEntitiesWithoutComponents ? a => a : a => null);
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, then entities without all the components are included. Otherwise, they
/// are excluded.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, Entity?> selectWithComponents,
bool includeEntitiesWithoutComponents = true)
{
return entities.SelectEntity(
selectWithComponents,
includeEntitiesWithoutComponents ? a => a : a => null);
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, then entities without all the components are included. Otherwise, they
/// are excluded.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the third component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2, T3>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, Entity?> selectWithComponents,
bool includeEntitiesWithoutComponents = true)
{
return entities.SelectEntity(
selectWithComponents,
includeEntitiesWithoutComponents ? a => a : a => null);
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, then entities without all the components are included. Otherwise, they
/// are excluded.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the third component.</typeparam>
/// <typeparam name="T4">The type of the fourth component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2, T3, T4>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, T4, Entity?> selectWithComponents,
bool includeEntitiesWithoutComponents = true)
{
return entities.SelectEntity(
selectWithComponents,
includeEntitiesWithoutComponents ? a => a : a => null);
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="selectWithoutComponents">
/// The optional transformation function for entities that do not have all the
/// components. If returns null,
/// then the entity will not be included.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1>(
this IEnumerable<Entity> entities,
Func<Entity, T1, Entity?> selectWithComponents,
Func<Entity, Entity?> selectWithoutComponents)
{
foreach (Entity entity in entities)
{
Entity? result = entity.TryGet(out T1 value1)
? selectWithComponents?.Invoke(entity, value1)
: selectWithoutComponents?.Invoke(entity);
if (result != null)
{
yield return result;
}
}
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="selectWithoutComponents">
/// The optional transformation function for entities that do not have all the
/// components. If returns null,
/// then the entity will not be included.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, Entity?> selectWithComponents,
Func<Entity, Entity?> selectWithoutComponents)
{
foreach (Entity entity in entities)
{
Entity? result = entity.TryGet(out T1 value1)
&& entity.TryGet(out T2 value2)
? selectWithComponents?.Invoke(entity, value1, value2)
: selectWithoutComponents?.Invoke(entity);
if (result != null)
{
yield return result;
}
}
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="selectWithoutComponents">
/// The optional transformation function for entities that do not have all the
/// components. If returns null,
/// then the entity will not be included.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the third component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2, T3>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, Entity?> selectWithComponents,
Func<Entity, Entity?> selectWithoutComponents)
{
foreach (Entity entity in entities)
{
Entity? result =
entity.TryGet(out T1 value1)
&& entity.TryGet(out T2 value2)
&& entity.TryGet(out T3 value3)
? selectWithComponents?.Invoke(
entity,
value1,
value2,
value3)
: selectWithoutComponents?.Invoke(entity);
if (result != null)
{
yield return result;
}
}
}
/// <summary>
/// Selects an entity from the given list, filtering on entities with
/// the given components.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="selectWithComponents">
/// The transformation function for the entity and selected components. If this
/// returns null, then the entity
/// will be filtered out.
/// </param>
/// <param name="selectWithoutComponents">
/// The optional transformation function for entities that do not have all the
/// components. If returns null,
/// then the entity will not be included.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the third component.</typeparam>
/// <typeparam name="T4">The type of the third component.</typeparam>
/// <returns>An enumeration of transformed entities.</returns>
public static IEnumerable<Entity> SelectEntity<T1, T2, T3, T4>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, T4, Entity?> selectWithComponents,
Func<Entity, Entity?> selectWithoutComponents)
{
foreach (Entity entity in entities)
{
Entity? result =
entity.TryGet(out T1 value1)
&& entity.TryGet(out T2 value2)
&& entity.TryGet(out T3 value3)
&& entity.TryGet(out T4 value4)
? selectWithComponents?.Invoke(
entity,
value1,
value2,
value3,
value4)
: selectWithoutComponents?.Invoke(entity);
if (result != null)
{
yield return result;
}
}
}
}

View file

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
/// <summary>
/// An extension method that handle SelectManyEntity which extracts all the
/// entities that match the given components,
/// passes the results to a select many callback, and then optionally includes the
/// ones that didn't have the components
/// before return.
/// </summary>
public static class SelectManyEntityExtensions
{
/// <summary>
/// Pulls out all the entities that match the given components into an enumeration,
/// passes it into the callback
/// function, and then optionally merges the entities that did not match before
/// returning.
/// </summary>
/// <param name="entities">The entities to process</param>
/// <param name="selectMany">
/// The callback function to manipulate the list of
/// entities.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, the include entities
/// without components.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <returns>An enumeration of entities.</returns>
public static IEnumerable<Entity> SelectManyEntity<T1>(
this IEnumerable<Entity> entities,
Func<IEnumerable<Entity>, IEnumerable<Entity>> selectMany,
bool includeEntitiesWithoutComponents = true)
{
SplitEntityEnumerations split = entities.SplitEntity<T1>();
IEnumerable<Entity> results = selectMany(split.HasAll);
if (includeEntitiesWithoutComponents)
{
results = results.Union(split.NotHasAll);
}
return results;
}
/// <summary>
/// Pulls out all the entities that match the given components into an enumeration,
/// passes it into the callback
/// function, and then optionally merges the entities that did not match before
/// returning.
/// </summary>
/// <param name="entities">The entities to process</param>
/// <param name="selectMany">
/// The callback function to manipulate the list of
/// entities.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, the include entities
/// without components.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <returns>An enumeration of entities.</returns>
public static IEnumerable<Entity> SelectManyEntity<T1, T2>(
this IEnumerable<Entity> entities,
Func<IEnumerable<Entity>, IEnumerable<Entity>> selectMany,
bool includeEntitiesWithoutComponents = true)
{
SplitEntityEnumerations split = entities.SplitEntity<T1, T2>();
IEnumerable<Entity> results = selectMany(split.HasAll);
if (includeEntitiesWithoutComponents)
{
results = results.Union(split.NotHasAll);
}
return results;
}
/// <summary>
/// Pulls out all the entities that match the given components into an enumeration,
/// passes it into the callback
/// function, and then optionally merges the entities that did not match before
/// returning.
/// </summary>
/// <param name="entities">The entities to process</param>
/// <param name="selectMany">
/// The callback function to manipulate the list of
/// entities.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, the include entities
/// without components.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the second component.</typeparam>
/// <returns>An enumeration of entities.</returns>
public static IEnumerable<Entity> SelectManyEntity<T1, T2, T3>(
this IEnumerable<Entity> entities,
Func<IEnumerable<Entity>, IEnumerable<Entity>> selectMany,
bool includeEntitiesWithoutComponents = true)
{
SplitEntityEnumerations split = entities.SplitEntity<T1, T2, T3>();
IEnumerable<Entity> results = selectMany(split.HasAll);
if (includeEntitiesWithoutComponents)
{
results = results.Union(split.NotHasAll);
}
return results;
}
/// <summary>
/// Pulls out all the entities that match the given components into an enumeration,
/// passes it into the callback
/// function, and then optionally merges the entities that did not match before
/// returning.
/// </summary>
/// <param name="entities">The entities to process</param>
/// <param name="selectMany">
/// The callback function to manipulate the list of
/// entities.
/// </param>
/// <param name="includeEntitiesWithoutComponents">
/// If true, the include entities
/// without components.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <typeparam name="T2">The type of the second component.</typeparam>
/// <typeparam name="T3">The type of the second component.</typeparam>
/// <typeparam name="T4">The type of the second component.</typeparam>
/// <returns>An enumeration of entities.</returns>
public static IEnumerable<Entity> SelectManyEntity<T1, T2, T3, T4>(
this IEnumerable<Entity> entities,
Func<IEnumerable<Entity>, IEnumerable<Entity>> selectMany,
bool includeEntitiesWithoutComponents = true)
{
SplitEntityEnumerations split = entities.SplitEntity<T1, T2, T3, T4>();
IEnumerable<Entity> results = selectMany(split.HasAll);
if (includeEntitiesWithoutComponents)
{
results = results.Union(split.NotHasAll);
}
return results;
}
}

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace MfGames.Gallium;
public record SplitEntityEnumerations(
IEnumerable<Entity> HasAll,
IEnumerable<Entity> NotHasAll)
{
/// <summary>
/// Gets a sequence of all entities that have all the given components.
/// </summary>
public IEnumerable<Entity> HasAll { get; } = HasAll;
/// <summary>
/// Gets the sequence of all entities that do not have all the given components.
/// </summary>
public IEnumerable<Entity> NotHasAll { get; } = NotHasAll;
}

View file

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
/// <summary>
/// Extension methods for IEnumerable&lt;Entity&gt; that split the entity into two
/// sequences, one that contains the
/// various components and the other list which does not.
/// </summary>
public static class SplitEntityExtensions
{
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given generic components
/// and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <typeparam name="T1">
/// A component to require to be in included in the first
/// list.
/// </typeparam>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity
<T1>(
this IEnumerable<Entity> entities,
Func<Entity, T1, bool>? test = null)
{
test ??= (
e,
v1) => true;
return entities.SplitEntity(
typeof(T1),
(
e,
v1) => test(e, (T1)v1));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given generic components
/// and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <typeparam name="T1">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T2">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity
<T1, T2>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, bool>? test = null)
{
test ??= (
e,
v1,
v2) => true;
return entities.SplitEntity(
typeof(T1),
typeof(T2),
(
e,
v1,
v2) => test(e, (T1)v1, (T2)v2));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given generic components
/// and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <typeparam name="T1">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T2">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T3">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity
<T1, T2, T3>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, bool>? test = null)
{
test ??= (
e,
v1,
v2,
v3) => true;
return entities.SplitEntity(
typeof(T1),
typeof(T2),
typeof(T3),
(
e,
v1,
v2,
v3) => test(e, (T1)v1, (T2)v2, (T3)v3));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given generic components
/// and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <typeparam name="T1">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T2">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T3">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <typeparam name="T4">
/// A component to require to be in included in the first list.
/// </typeparam>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity
<T1, T2, T3, T4>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, T4, bool>? test = null)
{
test ??= (
e,
v1,
v2,
v3,
v4) => true;
return entities.SplitEntity(
typeof(T1),
typeof(T2),
typeof(T3),
typeof(T4),
(
e,
v1,
v2,
v3,
v4) => test(e, (T1)v1, (T2)v2, (T3)v3, (T4)v4));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given component types and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="t1">The type of a required component.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity(
this IEnumerable<Entity> entities,
Type t1,
Func<Entity, object, bool> test)
{
return SplitEntity(
entities,
a => a.Has(t1) && test(a, a.Get<object>(t1)));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given component types and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="t1">The type of a required component.</param>
/// <param name="t2">The type of a required component.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity(
this IEnumerable<Entity> entities,
Type t1,
Type t2,
Func<Entity, object, object, bool> test)
{
return SplitEntity(entities, a => a.HasAll(t1, t2));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given component types and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="t1">The type of a required component.</param>
/// <param name="t2">The type of a required component.</param>
/// <param name="t3">The type of a required component.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity(
this IEnumerable<Entity> entities,
Type t1,
Type t2,
Type t3,
Func<Entity, object, object, object, bool> test)
{
return SplitEntity(entities, a => a.HasAll(t1, t2, t3));
}
/// <summary>
/// Splits the enumeration of entities into two separate enumerations, ones that
/// have the given component types and those which do not.
/// </summary>
/// <param name="entities">The entities to split into two lists.</param>
/// <param name="t1">The type of a required component.</param>
/// <param name="t2">The type of a required component.</param>
/// <param name="t3">The type of a required component.</param>
/// <param name="t4">The type of a required component.</param>
/// <param name="test">
/// An additional test function to determine if the entity is
/// included in the has list. If null, then entities with all the components will
/// be included.
/// </param>
/// <returns>A pair of enumerations, ones with the components and ones without.</returns>
public static SplitEntityEnumerations SplitEntity(
this IEnumerable<Entity> entities,
Type t1,
Type t2,
Type t3,
Type t4,
Func<Entity, object, object, object, object, bool> test)
{
return SplitEntity(
entities,
a => a.HasAll(t1, t2, t3, t4)
&& test(
a,
a.Get<object>(t1),
a.Get<object>(t2),
a.Get<object>(t3),
a.Get<object>(t4)));
}
private static SplitEntityEnumerations SplitEntity(
IEnumerable<Entity> entities,
Func<Entity, bool> keySelector)
{
if (entities == null)
{
throw new ArgumentNullException(nameof(entities));
}
IEnumerable<IGrouping<bool, Entity>> group = entities
.GroupBy(keySelector, a => a)
.ToList();
IEnumerable<Entity>? has = group
.Where(a => a.Key)
.SelectMany(a => a);
IEnumerable<Entity>? hasNot = group
.Where(a => !a.Key)
.SelectMany(a => a);
return new SplitEntityEnumerations(has, hasNot);
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
public static class WhereEntityExtensions
{
public static IEnumerable<Entity> WhereEntity<T1>(
this IEnumerable<Entity> entities,
Func<Entity, T1, bool> include)
{
return entities.Where(x => x.Has<T1>() && include(x, x.Get<T1>()));
}
public static IEnumerable<Entity> WhereEntity<T1, T2>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, bool> include)
{
return entities.Where(
x => x.HasAll<T1, T2>()
&& include(x, x.Get<T1>(), x.Get<T2>()));
}
public static IEnumerable<Entity> WhereEntity<T1, T2, T3>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, bool> include)
{
return entities.Where(
x => x.HasAll<T1, T2, T3>()
&& include(x, x.Get<T1>(), x.Get<T2>(), x.Get<T3>()));
}
public static IEnumerable<Entity> WhereEntity<T1, T2, T3, T4>(
this IEnumerable<Entity> entities,
Func<Entity, T1, T2, T3, T4, bool> include)
{
return entities.Where(
x => x.HasAll<T1, T2, T3, T4>()
&& include(
x,
x.Get<T1>(),
x.Get<T2>(),
x.Get<T3>(),
x.Get<T4>()));
}
}

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
public static class WhereEntityHasExtensions
{
public static IEnumerable<Entity> WhereEntityHas<T1>(
this IEnumerable<Entity> entities)
{
return entities.Where(x => x.Has<T1>());
}
public static IEnumerable<Entity> WhereEntityHasAll
<T1, T2>(this IEnumerable<Entity> entities)
{
return entities.Where(x => x.HasAll<T1, T2>());
}
public static IEnumerable<Entity> WhereEntityHasAll
<T1, T2, T3>(this IEnumerable<Entity> entities)
{
return entities.Where(x => x.HasAll<T1, T2, T3>());
}
public static IEnumerable<Entity> WhereEntityHasAll
<T1, T2, T3, T4>(this IEnumerable<Entity> entities)
{
return entities.Where(x => x.HasAll<T1, T2, T3, T4>());
}
}

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
namespace MfGames.Gallium;
public static class WhereEntityNotHasExtensions
{
public static IEnumerable<Entity> WhereEntityNotHas<T1>(
this IEnumerable<Entity> entities)
{
return entities.Where(x => !x.Has<T1>());
}
public static IEnumerable<Entity> WhereEntityNotHasAll
<T1, T2>(this IEnumerable<Entity> entities)
{
return entities.Where(x => !x.HasAll<T1, T2>());
}
public static IEnumerable<Entity> WhereEntityNotHasAll
<T1, T2, T3>(this IEnumerable<Entity> entities)
{
return entities.Where(x => !x.HasAll<T1, T2, T3>());
}
public static IEnumerable<Entity> WhereEntityNotHasAll
<T1, T2, T3, T4>(this IEnumerable<Entity> entities)
{
return entities.Where(x => !x.HasAll<T1, T2, T3, T4>());
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Locking-"

View file

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>Wrappers and patterns for working with ReaderWriterLockSlim.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,47 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <summary>
/// Defines a ReaderWriterLockSlim read-only lock.
/// </summary>
public class NestableReadLock : IDisposable
{
private readonly bool lockAcquired;
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="NestableReadLock" /> class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public NestableReadLock(ReaderWriterLockSlim readerWriterLockSlim)
{
// Keep track of the lock since we'll need it to release the lock.
this.readerWriterLockSlim = readerWriterLockSlim;
// If we already have a read or write lock, we don't do anything.
if (readerWriterLockSlim.IsReadLockHeld
|| readerWriterLockSlim.IsUpgradeableReadLockHeld
|| readerWriterLockSlim.IsWriteLockHeld)
{
this.lockAcquired = false;
}
else
{
readerWriterLockSlim.EnterReadLock();
this.lockAcquired = true;
}
}
/// <inheritdoc />
public void Dispose()
{
if (this.lockAcquired)
{
this.readerWriterLockSlim.ExitReadLock();
}
}
}
}

View file

@ -0,0 +1,48 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <summary>
/// Defines a ReaderWriterLockSlim upgradable read lock.
/// </summary>
public class NestableUpgradableReadLock : IDisposable
{
private readonly bool lockAcquired;
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="NestableUpgradableReadLock" />
/// class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public NestableUpgradableReadLock(
ReaderWriterLockSlim readerWriterLockSlim)
{
// Keep track of the lock since we'll need it to release the lock.
this.readerWriterLockSlim = readerWriterLockSlim;
// If we already have a read or write lock, we don't do anything.
if (readerWriterLockSlim.IsUpgradeableReadLockHeld
|| readerWriterLockSlim.IsWriteLockHeld)
{
this.lockAcquired = false;
}
else
{
readerWriterLockSlim.EnterUpgradeableReadLock();
this.lockAcquired = true;
}
}
/// <inheritdoc />
public void Dispose()
{
if (this.lockAcquired)
{
this.readerWriterLockSlim.ExitUpgradeableReadLock();
}
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <summary>
/// Defines a ReaderWriterLockSlim write lock.
/// </summary>
public class NestableWriteLock : IDisposable
{
private readonly bool lockAcquired;
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="NestableWriteLock" /> class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public NestableWriteLock(ReaderWriterLockSlim readerWriterLockSlim)
{
// Keep track of the lock since we'll need it to release the lock.
this.readerWriterLockSlim = readerWriterLockSlim;
// If we already have a read or write lock, we don't do anything.
if (readerWriterLockSlim.IsWriteLockHeld)
{
this.lockAcquired = false;
}
else
{
readerWriterLockSlim.EnterWriteLock();
this.lockAcquired = true;
}
}
/// <inheritdoc />
public void Dispose()
{
if (this.lockAcquired)
{
this.readerWriterLockSlim.ExitWriteLock();
}
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <inheritdoc />
/// <summary>
/// Defines a ReaderWriterLockSlim read-only lock.
/// </summary>
public class ReadLock : IDisposable
{
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="ReadLock" /> class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public ReadLock(ReaderWriterLockSlim readerWriterLockSlim)
{
this.readerWriterLockSlim = readerWriterLockSlim;
readerWriterLockSlim.EnterReadLock();
}
/// <inheritdoc />
public void Dispose()
{
this.readerWriterLockSlim.ExitReadLock();
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <summary>
/// Defines a ReaderWriterLockSlim read-only lock.
/// </summary>
public class UpgradableLock : IDisposable
{
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="UpgradableLock" /> class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public UpgradableLock(ReaderWriterLockSlim readerWriterLockSlim)
{
this.readerWriterLockSlim = readerWriterLockSlim;
readerWriterLockSlim.EnterUpgradeableReadLock();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or
/// resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.readerWriterLockSlim.ExitUpgradeableReadLock();
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Threading;
namespace MfGames.Locking
{
/// <summary>
/// Defines a ReaderWriterLockSlim read-only lock.
/// </summary>
public class WriteLock : IDisposable
{
private readonly ReaderWriterLockSlim readerWriterLockSlim;
/// <summary>
/// Initializes a new instance of the <see cref="WriteLock" /> class.
/// </summary>
/// <param name="readerWriterLockSlim">The reader writer lock slim.</param>
public WriteLock(ReaderWriterLockSlim readerWriterLockSlim)
{
this.readerWriterLockSlim = readerWriterLockSlim;
readerWriterLockSlim.EnterWriteLock();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or
/// resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.readerWriterLockSlim.ExitWriteLock();
}
}
}

View file

@ -0,0 +1,65 @@
using Markdig;
using Markdig.Extensions.Tables;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension method to control how links are processed inside blocks.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class GemtextPipeTableExtension : IMarkdownExtension
{
/// <summary>
/// Initializes a new instance of the <see cref="GemtextPipeTableExtension" />
/// class.
/// </summary>
/// <param name="options">The options.</param>
public GemtextPipeTableExtension(GemtextPipeTableOptions? options = null)
{
this.Options = options ?? new GemtextPipeTableOptions();
}
/// <summary>
/// Gets the options.
/// </summary>
public GemtextPipeTableOptions Options { get; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.PreciseSourceLocation = true;
if (!pipeline.BlockParsers.Contains<PipeTableBlockParser>())
{
pipeline.BlockParsers.Insert(0, new PipeTableBlockParser());
}
LineBreakInlineParser? lineBreakParser =
pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
{
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(
new PipeTableParser(lineBreakParser!, this.Options));
}
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.ObjectRenderers.Add(
new TableRenderer(
this.Options.OmitPreformatLines,
this.Options.ConfigureTableBuilder));
}
}

View file

@ -0,0 +1,21 @@
using System;
using ConsoleTableExt;
using Markdig.Extensions.Tables;
namespace MfGames.Markdown.Gemtext.Extensions;
public class GemtextPipeTableOptions : PipeTableOptions
{
/// <summary>
/// Gets or sets the table builder to control formatting.
/// </summary>
public Action<ConsoleTableBuilder>? ConfigureTableBuilder { get; set; }
/// <summary>
/// Gets or sets a value whether the preformat (backticks) fence should
/// not be emitted.
/// </summary>
public bool OmitPreformatLines { get; set; }
}

View file

@ -0,0 +1,54 @@
using Markdig;
using Markdig.Extensions.SmartyPants;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension to enable SmartyPants, but for Gemtext.
/// </summary>
public class GemtextSmartyPantsExtension : IMarkdownExtension
{
/// <summary>
/// Initializes a new instance of the <see cref="SmartyPantsExtension" /> class.
/// </summary>
/// <param name="options">The options.</param>
public GemtextSmartyPantsExtension(SmartyPantOptions? options)
{
this.Options = options ?? new SmartyPantOptions();
}
/// <summary>
/// Gets the options.
/// </summary>
public SmartyPantOptions Options { get; }
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<SmartyPantsInlineParser>())
{
// Insert the parser after the code span parser
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(
new SmartyPantsInlineParser());
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtextRenderer)
{
return;
}
if (!gemtextRenderer.ObjectRenderers
.Contains<GemtextSmartyPantRenderer>())
{
gemtextRenderer.ObjectRenderers.Add(
new GemtextSmartyPantRenderer(this.Options));
}
}
}

View file

@ -0,0 +1,28 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension method to retain HTML blocks as a code fenced block with
/// "html" as the data.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class HtmlAsCodeBlocks : IMarkdownExtension
{
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is GemtextRenderer gemtext)
{
gemtext.HtmlBlockFormatting = HtmlBlockFormatting.CodeBlock;
}
}
}

View file

@ -0,0 +1,38 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension method to control the depth of the headers in a file so that
/// the first one (maybe a header) is H1 but the others are decreased to
/// H2 or lower depending on their initial level.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class IncreaseHeaderDepthsAfterFirst : IMarkdownExtension
{
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
HeadingRenderer? heading =
gemtext.ObjectRenderers.Find<HeadingRenderer>();
if (heading != null)
{
heading.IncreaseHeaderDepthAfterFirst = true;
}
}
}

View file

@ -0,0 +1,63 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension method to control how links are processed inside blocks.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class SetBlockLinkHandling : IMarkdownExtension
{
public SetBlockLinkHandling(
BlockLinkHandling? blockLinkHandling = null,
EndLinkInlineFormatting? endLinkInlineFormatting = null,
int? nextFootnoteNumber = null)
{
this.BlockLinkHandling = blockLinkHandling;
this.EndLinkInlineFormatting = endLinkInlineFormatting;
this.NextFootnoteNumber = nextFootnoteNumber;
}
/// <summary>
/// Gets or sets how block links are handled. If this is null, then no
/// change is made to the current renderer.
/// </summary>
public BlockLinkHandling? BlockLinkHandling { get; set; }
/// <summary>
/// Gets or sets how links are formatted if they are gathered to the
/// end of the paragraph or document. If this is null, then no change
/// will be made.
/// </summary>
public EndLinkInlineFormatting? EndLinkInlineFormatting { get; set; }
/// <summary>
/// Gets or sets the next footnote number. If this is null, then no
/// change will be made.
/// </summary>
public int? NextFootnoteNumber { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.BlockLinkHandling = this.BlockLinkHandling
?? gemtext.BlockLinkHandling;
gemtext.EndLinkInlineFormatting = this.EndLinkInlineFormatting
?? gemtext.EndLinkInlineFormatting;
gemtext.NextFootnoteNumber = this.NextFootnoteNumber
?? gemtext.NextFootnoteNumber;
}
}

View file

@ -0,0 +1,49 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions;
/// <summary>
/// Extension method to turn all inline formatting from the default of
/// removing to render in normalizing in rendered form.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class SetInlineFormatting : IMarkdownExtension
{
/// <summary>
/// Gets or sets the override formatting for code lines
/// (backtick) spans.
/// </summary>
public InlineFormatting? Code { get; set; }
/// <summary>
/// Sets or sets the override formatting for all inlines.
/// </summary>
public InlineFormatting? Default { get; set; }
/// <summary>
/// Gets or sets the override formatting for emphasis (italic
/// and bold) spans.
/// </summary>
public InlineFormatting? Emphasis { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.InlineFormatting = this.Default ?? gemtext.InlineFormatting;
gemtext.EmphasisFormatting = this.Emphasis;
gemtext.CodeFormatting = this.Code;
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Markdown.Gemtext-"

View file

@ -0,0 +1,84 @@
using System;
using System.IO;
using Markdig;
using Markdig.Parsers;
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext;
/// <summary>
/// The static class that corresponds to Markdig.Markdown. This is written
/// with the same pattern, but since `Markdown` is a static, we can't tack
/// onto that.
/// </summary>
public static class MarkdownGemtext
{
private static readonly MarkdownPipeline DefaultPipeline;
static MarkdownGemtext()
{
DefaultPipeline = new MarkdownPipelineBuilder()
.Build();
}
/// <summary>
/// Converts the given Markdown
/// </summary>
/// <param name="markdown">A Markdown text.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <param name="context">A parser context used for the parsing.</param>
/// <returns>The result of the conversion</returns>
public static string ToGemtext(
string markdown,
MarkdownPipeline? pipeline = null,
MarkdownParserContext? context = null)
{
if (markdown == null)
{
throw new ArgumentNullException(nameof(markdown));
}
pipeline ??= DefaultPipeline;
MarkdownDocument document = MarkdownParser
.Parse(markdown, pipeline, context);
return ToGemtext(document, pipeline);
}
/// <summary>
/// Converts a Markdown document to HTML.
/// </summary>
/// <param name="document">A Markdown document.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
public static string ToGemtext(
this MarkdownDocument document,
MarkdownPipeline? pipeline = null)
{
// Make sure we have sane parameters.
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
pipeline ??= DefaultPipeline;
// Set up the writer to contain the markdown and the Gemtext
// renderer.
var writer = new StringWriter();
GemtextRenderer renderer = new(writer);
pipeline.Setup(renderer);
// Render the Markdown into Gemtext and re turn the results.
renderer.Render(document);
renderer.Writer.Flush();
return renderer.Writer.ToString() ?? string.Empty;
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>A MarkDig extension to render Markdown in Gemtext.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConsoleTableExt" Version="3.2.0" />
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Markdig" Version="0.31.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,33 @@
namespace MfGames.Markdown.Gemtext.Renderers;
/// <summary>
/// Describes how links are processed within a paragraph.
/// </summary>
public enum BlockLinkHandling
{
/// <summary>
/// Indicates that the paragraph should be broken apart and the link
/// included on its own line in the middle of the paragraph.
/// </summary>
InsertLine,
/// <summary>
/// Indicates that all the links in a paragraph should be gathered
/// and then emitted at the end of the paragraph. The text of the link
/// will be left in the paragraph.
/// </summary>
ParagraphEnd,
/// <summary>
/// Indicates that all the links in the document should be gathered
/// and then emitted at the end of the document. The text of the link
/// will be left in the paragraph.
/// </summary>
DocumentEnd,
/// <summary>
/// Indicates that the links themselves should be removed and just the
/// text included in the paragraph.
/// </summary>
Remove,
}

View file

@ -0,0 +1,21 @@
namespace MfGames.Markdown.Gemtext.Renderers;
/// <summary>
/// Describes how a paragraph link is formatted inside the text. This is
/// only used for `ParagraphLinkHandling.ParagraphEnd` and
/// `ParagraphLinkHandling.DocumentEnd`.
/// </summary>
public enum EndLinkInlineFormatting
{
/// <summary>
/// Indicates that a footnote notation (`[1]`) will be insert into the
/// text and then the link will be displayed with the URL when gathered.
/// </summary>
Footnote,
/// <summary>
/// Indicates that the text is put in as-is into the gathered link with
/// no footnote given in the block.
/// </summary>
Text,
}

View file

@ -0,0 +1,38 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// An Gemtext renderer for a <see cref="CodeBlock" /> and
/// <see cref="FencedCodeBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{CodeBlock}" />
public class CodeBlockRenderer : GemtextObjectRenderer<CodeBlock>
{
protected override void Write(GemtextRenderer renderer, CodeBlock obj)
{
// We need to have two lines above this.
renderer.EnsureTwoLines();
// Code blocks are always fenced, but we allow for additional text
// at the end of them which is only in `FencedCodeBlock`.
if (obj is FencedCodeBlock fenced)
{
renderer.WriteLine("```" + fenced.Info);
}
else
{
renderer.WriteLine("```");
}
renderer.WriteLeafRawLines(obj, true);
renderer.Write("```");
// If we aren't at the end of the container, then add some spacing.
if (!renderer.IsLastInContainer)
{
renderer.WriteLine();
renderer.WriteLine();
}
}
}

View file

@ -0,0 +1,23 @@
using Markdig.Extensions.CustomContainers;
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// An Gemtext renderer for a <see cref="CustomContainer" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{CodeBlock}" />
public class CustomContainerRenderer : GemtextObjectRenderer<CustomContainer>
{
protected override void Write(GemtextRenderer renderer, CustomContainer obj)
{
renderer.EnsureTwoLines();
renderer.WriteChildren(obj);
if (!renderer.IsLastInContainer)
{
renderer.WriteLine();
renderer.WriteLine();
}
}
}

View file

@ -0,0 +1,55 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// An Gemtext renderer for a <see cref="HeadingBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{HeadingBlock}" />
public class HeadingRenderer : GemtextObjectRenderer<HeadingBlock>
{
private int currentHeading;
/// <summary>
/// Gets or sets a value indicating whether the header depths are
/// increased after the first one.
/// </summary>
public bool IncreaseHeaderDepthAfterFirst { get; set; }
protected override void Write(
GemtextRenderer renderer,
HeadingBlock obj)
{
// Figure out the level we should be processing.
int level = obj.Level;
if (this.currentHeading++ > 0 && this.IncreaseHeaderDepthAfterFirst)
{
// Check the second header we see. If this header is H2 or
// higher, then we assume that the file has been already updated
// to handle the heading and we stop processing.
if (this.currentHeading == 2 && level != 1)
{
this.IncreaseHeaderDepthAfterFirst = false;
}
else
{
// We are bumping the heading levels up.
level++;
}
}
// Write out the prefix of the header.
string prefix = level switch
{
1 => "# ",
2 => "## ",
3 => "### ",
_ => "",
};
renderer.EnsureTwoLines();
renderer.Write(prefix);
renderer.WriteLeafInline(obj);
}
}

View file

@ -0,0 +1,32 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="GemtextBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextBlock}" />
public class HtmlBlockRenderer : GemtextObjectRenderer<HtmlBlock>
{
protected override void Write(GemtextRenderer renderer, HtmlBlock obj)
{
// If we are stripping out HTML blocks (default), then nothing to
// do with rendering.
if (renderer.HtmlBlockFormatting == HtmlBlockFormatting.Remove)
{
return;
}
// Otherwise, we treat this as a fenced code block.
renderer.EnsureTwoLines();
renderer.WriteLine("```html");
renderer.WriteLeafRawLines(obj, true);
renderer.WriteLine("```");
// If we aren't at the end of the container, then add some spacing.
if (!renderer.IsLastInContainer)
{
renderer.WriteLine();
}
}
}

View file

@ -0,0 +1,34 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="ListBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ListBlock}" />
public class ListRenderer : GemtextObjectRenderer<ListBlock>
{
protected override void Write(
GemtextRenderer renderer,
ListBlock listBlock)
{
// Lists need to be separated from the rest.
renderer.EnsureTwoLines();
// Go through each list item and write them out.
foreach (Block? item in listBlock)
{
// If the list only contains a link, then we just render the
// link instead.
var listItem = (ListItemBlock)item;
if (!listItem.OnlyHasSingleLink())
{
renderer.EnsureLine();
renderer.Write("* ");
}
renderer.WriteChildren(listItem);
}
}
}

View file

@ -0,0 +1,27 @@
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="MarkdownDocument" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ParagraphBlock}" />
public class MarkdownDocumentRenderer
: GemtextObjectRenderer<MarkdownDocument>
{
protected override void Write(
GemtextRenderer renderer,
MarkdownDocument obj)
{
// Simply write out the contents.
renderer.WriteChildren(obj);
// If we get to the end of the document and we have gathered links,
// and we are in DocumentEnd mode, then write out the links. We
// don't test for the mode here because if there are links, we
// should write them out.
LinkInlineRenderer.WriteGatheredLinks(renderer);
}
}

View file

@ -0,0 +1,43 @@
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="ParagraphBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ParagraphBlock}" />
public class ParagraphRenderer : GemtextObjectRenderer<ParagraphBlock>
{
protected override void Write(
GemtextRenderer renderer,
ParagraphBlock obj)
{
// If we aren't the first in the container, we need to break apart
// the lines to make it easier to read.
if (!renderer.IsFirstInContainer)
{
renderer.EnsureTwoLines();
}
// We need to save the state of the link rendering while handling
// this block.
if (obj.OnlyHasSingleLink())
{
renderer.WriteLeafInline(obj);
}
else
{
renderer.WhileLinkInsideBlock(
() => renderer.WriteLeafInline(obj));
}
// If we get to the end of the paragraph and we have gathered links,
// and we are in ParagraphEnd mode, then write out the links.
if (renderer.BlockLinkHandling == BlockLinkHandling.ParagraphEnd)
{
LinkInlineRenderer.WriteGatheredLinks(renderer);
}
}
}

View file

@ -0,0 +1,20 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="QuoteBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{QuoteBlock}" />
public class QuoteBlockRenderer : GemtextObjectRenderer<QuoteBlock>
{
protected override void Write(GemtextRenderer renderer, QuoteBlock obj)
{
string quoteIndent = obj.QuoteChar + " ";
renderer.EnsureTwoLines();
renderer.PushIndent(quoteIndent);
renderer.WriteChildren(obj);
renderer.PopIndent();
}
}

View file

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ConsoleTableExt;
using Markdig.Extensions.Tables;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
public class TableRenderer : GemtextObjectRenderer<Table>
{
private readonly Action<ConsoleTableBuilder>? configureTableBuilder;
private readonly bool omitPreformat;
public TableRenderer(
bool omitPreformat,
Action<ConsoleTableBuilder>? configureTableBuilder)
{
this.omitPreformat = omitPreformat;
this.configureTableBuilder = configureTableBuilder;
}
protected override void Write(GemtextRenderer renderer, Table table)
{
// Since Gemtext doesn't have a table format per-se, we are going
// to use ConsoleTableEx to make a nicely-formatted table and emit
// the lines directly. That should produce the desired result.
// Gather up information about the data since that is where the
// builder starts with.
bool hasHeader = false;
List<object> header = new();
List<List<object>> data = new();
Dictionary<int, TextAligntment> align = new();
foreach (TableRow row in table.OfType<TableRow>())
{
// If we haven't seen a header, then we include that.
if (!hasHeader && row.IsHeader)
{
header = GetCellValues(row);
SetAlignments(table, align, row);
continue;
}
// Otherwise, we treat it as a row and go through the columns.
List<object> cells = GetCellValues(row);
data.Add(cells);
}
// Set up the table.
ConsoleTableBuilder builder = ConsoleTableBuilder
.From(data)
.WithColumn(header.OfType<string>().ToArray())
.WithHeaderTextAlignment(align)
.WithTextAlignment(align);
this.configureTableBuilder?.Invoke(builder);
// Format the final table.
string formatted = builder.Export().ToString().TrimEnd();
// Write out the table including making sure two lines are above it.
renderer.EnsureTwoLines();
if (!this.omitPreformat)
{
renderer.WriteLine("```");
}
renderer.WriteLine(formatted);
if (!this.omitPreformat)
{
renderer.WriteLine("```");
renderer.WriteLine();
}
}
private static List<object> GetCellValues(TableRow row)
{
List<object> cells = new();
foreach (TableCell cell in row.OfType<TableCell>())
{
// Write out to a text since we can't have a callback while
// rendering the table cells.
using var writer = new StringWriter();
var innerRenderer = new GemtextRenderer(writer);
innerRenderer.Render(cell);
cells.Add(writer.ToString());
}
return cells;
}
private static void SetAlignments(
Table table,
Dictionary<int, TextAligntment> align,
TableRow row)
{
for (int i = 0; i < row.Count; i++)
{
// Copied from Markdig's version.
var cell = (TableCell)row[i];
int columnIndex = cell.ColumnIndex < 0
|| cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex =
columnIndex >= table.ColumnDefinitions.Count
? table.ColumnDefinitions.Count - 1
: columnIndex;
TableColumnAlign? alignment = table
.ColumnDefinitions[columnIndex]
.Alignment;
if (alignment.HasValue)
{
align[columnIndex] = alignment.Value switch
{
TableColumnAlign.Center => TextAligntment.Center,
TableColumnAlign.Left => TextAligntment.Left,
TableColumnAlign.Right => TextAligntment.Right,
_ => TextAligntment.Left,
};
}
}
}
}

View file

@ -0,0 +1,19 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
/// <summary>
/// A Gemtext renderer for a <see cref="ThematicBreakBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ThematicBreakBlock}" />
public class ThematicBreakRenderer
: GemtextObjectRenderer<ThematicBreakBlock>
{
protected override void Write(
GemtextRenderer renderer,
ThematicBreakBlock obj)
{
renderer.EnsureTwoLines();
renderer.WriteLine("---");
}
}

View file

@ -0,0 +1,17 @@
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext;
/// <summary>
/// A base class for Gemtext rendering <see cref="Block" /> and
/// <see cref="Inline" /> Markdown objects.
/// </summary>
/// <typeparam name="TObject">The type of the object.</typeparam>
/// <seealso cref="IMarkdownObjectRenderer" />
public abstract class GemtextObjectRenderer<TObject>
: MarkdownObjectRenderer<GemtextRenderer, TObject>
where TObject : MarkdownObject
{
}

View file

@ -0,0 +1,29 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="CodeInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{CodeInline}" />
public class CodeInlineRenderer : GemtextObjectRenderer<CodeInline>
{
protected override void Write(GemtextRenderer renderer, CodeInline obj)
{
const string Delimiter = "`";
InlineFormatting formatting = renderer.CodeFormattingResolved;
bool normalize = formatting == InlineFormatting.Normalize;
if (normalize)
{
renderer.Write(Delimiter);
}
renderer.Write(obj.Content);
if (normalize)
{
renderer.Write(Delimiter);
}
}
}

View file

@ -0,0 +1,19 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="DelimiterInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{DelimiterInline}" />
public class DelimiterInlineRenderer
: GemtextObjectRenderer<DelimiterInline>
{
protected override void Write(
GemtextRenderer renderer,
DelimiterInline obj)
{
renderer.Write(obj.ToLiteral());
renderer.WriteChildren(obj);
}
}

View file

@ -0,0 +1,31 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for an <see cref="EmphasisInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{EmphasisInline}" />
public class EmphasisInlineRenderer : GemtextObjectRenderer<EmphasisInline>
{
protected override void Write(
GemtextRenderer renderer,
EmphasisInline obj)
{
InlineFormatting formatting = renderer.EmphasisFormattingResolved;
bool normalize = formatting == InlineFormatting.Normalize;
string delimiter = new string('*', obj.DelimiterCount);
if (normalize)
{
renderer.Write(delimiter);
}
renderer.WriteChildren(obj);
if (normalize)
{
renderer.Write(delimiter);
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Net;
using Markdig.Extensions.SmartyPants;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="SmartyPant" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextEntityInline}" />
public class GemtextSmartyPantRenderer
: GemtextObjectRenderer<SmartyPant>
{
private static readonly SmartyPantOptions DefaultOptions = new();
private readonly SmartyPantOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="HtmlSmartyPantRenderer" /> class.
/// </summary>
/// <param name="options">The options.</param>
/// <exception cref="ArgumentNullException"></exception>
public GemtextSmartyPantRenderer(SmartyPantOptions? options)
{
this.options = options
?? throw new ArgumentNullException(nameof(options));
}
protected override void Write(GemtextRenderer renderer, SmartyPant obj)
{
if (!this.options.Mapping.TryGetValue(obj.Type, out string? text))
{
DefaultOptions.Mapping.TryGetValue(obj.Type, out text);
}
string? unicode = WebUtility.HtmlDecode(text);
renderer.Write(unicode);
}
}

View file

@ -0,0 +1,18 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="GemtextEntityInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextEntityInline}" />
public class HtmlEntityInlineRenderer
: GemtextObjectRenderer<HtmlEntityInline>
{
protected override void Write(
GemtextRenderer renderer,
HtmlEntityInline obj)
{
renderer.Write(obj.Transcoded);
}
}

View file

@ -0,0 +1,31 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="LineBreakInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LineBreakInline}" />
public class LineBreakInlineRenderer
: GemtextObjectRenderer<LineBreakInline>
{
/// <summary>
/// Gets or sets a value indicating whether to render this softline break as a
/// Gemtext hardline break tag (&lt;br /&gt;)
/// </summary>
public bool RenderAsHardlineBreak { get; set; }
protected override void Write(
GemtextRenderer renderer,
LineBreakInline obj)
{
if (obj.IsHard || this.RenderAsHardlineBreak)
{
renderer.EnsureTwoLines();
}
else
{
renderer.Write(" ");
}
}
}

View file

@ -0,0 +1,124 @@
using System.IO;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="LinkInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LinkInline}" />
public class LinkInlineRenderer : GemtextObjectRenderer<LinkInline>
{
/// <summary>
/// Writes out any gathered links in a block.
/// </summary>
/// <param name="renderer">The renderer being used.</param>
public static void WriteGatheredLinks(GemtextRenderer renderer)
{
// If we have no gathered links, then there is nothing to do.
if (renderer.GatheredLinks.Count <= 0)
{
return;
}
// Put some space between the previous object and this one, then
// write out each link which is already formatted.
renderer.WriteLine();
foreach (string? link in renderer.GatheredLinks)
{
renderer.WriteLine();
renderer.Write(link);
}
// Clear out the list of links.
renderer.GatheredLinks.Clear();
}
protected override void Write(GemtextRenderer renderer, LinkInline link)
{
// Figure out the various states we have.
bool outside = !renderer.LinkInsideBlock;
bool insert = !outside
&& renderer.BlockLinkHandling == BlockLinkHandling.InsertLine;
bool gather = !outside
&& renderer.BlockLinkHandling switch
{
BlockLinkHandling.DocumentEnd => true,
BlockLinkHandling.ParagraphEnd => true,
_ => false,
};
bool hasText = link.FirstChild != null;
bool footnotes = renderer.EndLinkInlineFormatting
== EndLinkInlineFormatting.Footnote;
// Bare links and ones where we insert into the paragraph have
// their own line.
string? url = link.GetDynamicUrl != null
? link.GetDynamicUrl() ?? link.Url
: link.Url;
if (outside || insert)
{
// Make sure we are at the beginning of the line before
// rendering the link.
renderer.EnsureLine();
renderer.Write("=> ");
renderer.Write(url);
// If we have text, we need a space after the URL and before
// the text.
if (hasText)
{
renderer.Write(" ");
}
}
// Render the text for the link if we have it.
if (hasText)
{
renderer.WriteChildren(link);
}
// If we are gathering, then write out a footnote.
if (gather)
{
int footnoteNumber = renderer.NextFootnoteNumber++;
string linkText = footnotes
? footnoteNumber + ": " + url
: GetLinkText(link);
if (footnotes)
{
renderer.Write($"[{footnoteNumber}]");
}
renderer.GatheredLinks.Add("=> " + url + " " + linkText);
}
// If we are inserting a line in the paragraph, we need a final
// newline so the text of the paragraph continues on the next
// line.
if (insert)
{
renderer.WriteLine();
}
}
private static string GetLinkText(LinkInline link)
{
// This little bit of nasty code basically spins up a new renderer
// to get the text of the link by itself. Then we return that
// directly so it can be rendered as a link.
StringWriter writer = new();
GemtextRenderer renderer = new(writer);
renderer.WriteChildren(link);
writer.Close();
string text = writer.ToString();
return text;
}
}

View file

@ -0,0 +1,38 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
/// <summary>
/// A Gemtext renderer for a <see cref="LiteralInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LiteralInline}" />
public class LiteralInlineRenderer : GemtextObjectRenderer<LiteralInline>
{
protected override void Write(
GemtextRenderer renderer,
LiteralInline obj)
{
// If we are inside a paragraph and we are doing inline formatting,
// then we need to trim the text if we are before or after a link.
string content = obj.Content.ToString();
BlockLinkHandling handling = renderer.BlockLinkHandling;
bool isInsert = handling == BlockLinkHandling.InsertLine;
bool inBlock = renderer.LinkInsideBlock;
if (inBlock && isInsert)
{
if (obj.PreviousSibling is LinkInline)
{
content = content.TrimStart();
}
if (obj.NextSibling is LinkInline)
{
content = content.TrimEnd();
}
}
// Write out the manipulated content.
renderer.Write(content);
}
}

View file

@ -0,0 +1,36 @@
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext;
/// <summary>
/// Various useful extension methods for Markdig classes.
/// </summary>
public static class MarkdigExtensions
{
/// <summary>
/// Determines if the paragraph only contains a link.
/// </summary>
/// <param name="obj">The object to inspect.</param>
/// <returns>True if there is only a link in the paragraph.</returns>
public static bool OnlyHasSingleLink(this ParagraphBlock obj)
{
return obj.Inline != null
&& obj.Inline.Count() == 1
&& obj.Inline.FirstChild is LinkInline;
}
/// <summary>
/// Determines if the list item only contains a link.
/// </summary>
/// <param name="obj">The object to inspect.</param>
/// <returns>True if there is only a link in the paragraph.</returns>
public static bool OnlyHasSingleLink(this ListItemBlock obj)
{
return obj.Count == 1
&& obj.LastChild is ParagraphBlock paragraphBlock
&& paragraphBlock.OnlyHasSingleLink();
}
}

View file

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.IO;
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers;
public class GemtextRenderer : TextRendererBase<GemtextRenderer>
{
/// <inheritdoc />
public GemtextRenderer(TextWriter writer)
: base(writer)
{
// Set up our default values.
this.NextFootnoteNumber = 1;
this.GatheredLinks = new List<string>();
// Default block renderers.
this.ObjectRenderers.Add(new CustomContainerRenderer());
this.ObjectRenderers.Add(new CodeBlockRenderer());
this.ObjectRenderers.Add(new HeadingRenderer());
this.ObjectRenderers.Add(new HtmlBlockRenderer());
this.ObjectRenderers.Add(new ListRenderer());
this.ObjectRenderers.Add(new MarkdownDocumentRenderer());
this.ObjectRenderers.Add(new ParagraphRenderer());
this.ObjectRenderers.Add(new QuoteBlockRenderer());
this.ObjectRenderers.Add(new ThematicBreakRenderer());
// Default inline renderers.
this.ObjectRenderers.Add(new CodeInlineRenderer());
this.ObjectRenderers.Add(new DelimiterInlineRenderer());
this.ObjectRenderers.Add(new EmphasisInlineRenderer());
this.ObjectRenderers.Add(new HtmlEntityInlineRenderer());
this.ObjectRenderers.Add(new LineBreakInlineRenderer());
this.ObjectRenderers.Add(new LinkInlineRenderer());
this.ObjectRenderers.Add(new LiteralInlineRenderer());
}
/// <summary>
/// Gets or sets how to handle links inside paragraphs and other blocks.
/// </summary>
public BlockLinkHandling BlockLinkHandling { get; set; }
/// <summary>
/// Gets or sets the optional formatting for code inlines (backticks).
/// If this is unset, then `InlineFormatting` will be used.
/// </summary>
public InlineFormatting? CodeFormatting { get; set; }
/// <summary>
/// Gets the actual formatting for code inlines (backticks) which
/// is either `CodeInlineFormatting` or `InlineFormatting` if that
/// is not set.
/// </summary>
public InlineFormatting CodeFormattingResolved =>
this.CodeFormatting ?? this.InlineFormatting;
/// <summary>
/// Gets or sets the optional formatting for emphasis (which includes
/// italics and bolds). If this is unset, then `InlineFormatting`
/// will be used.
/// </summary>
public InlineFormatting? EmphasisFormatting { get; set; }
/// <summary>
/// Gets the actual formatting for emphasis which is either
/// `EmphasisFormatting` or `InlineFormatting` if that isn't set.
/// </summary>
public InlineFormatting EmphasisFormattingResolved =>
this.EmphasisFormatting ?? this.InlineFormatting;
/// <summary>
/// Gets or sets the formatting for how links that are gathered at the
/// end of a paragraph or document are formatted inside the paragraph.
/// </summary>
public EndLinkInlineFormatting EndLinkInlineFormatting { get; set; }
/// <summary>
/// Gets the current list of formatted links that have been gathered
/// up to this point for rendering.
/// </summary>
public List<string> GatheredLinks { get; }
/// <summary>
/// Gets or sets the formatting rule for HTML blocks.
/// </summary>
public HtmlBlockFormatting HtmlBlockFormatting { get; set; }
/// <summary>
/// Gets or sets the default formatting for all inlines.
/// </summary>
public InlineFormatting InlineFormatting { get; set; }
/// <summary>
/// An internal processing flag that determines if the rendered link
/// is inside a block or not to trigger extra handling.
/// </summary>
public bool LinkInsideBlock { get; set; }
/// <summary>
/// Gets or sets the next footnote while rendering links.
/// </summary>
public int NextFootnoteNumber { get; set; }
/// <summary>
/// Ensures there are two blank lines before an element.
/// </summary>
/// <returns></returns>
public GemtextRenderer EnsureTwoLines()
{
if (this.previousWasLine)
{
return this;
}
this.WriteLine();
this.WriteLine();
return this;
}
/// <summary>
/// A wrapper method to push the state of LinkInsideBlock while
/// performing an action.
/// </summary>
/// <param name="action">The action to perform.</param>
public void WhileLinkInsideBlock(Action action)
{
bool oldState = this.LinkInsideBlock;
this.LinkInsideBlock = true;
action();
this.LinkInsideBlock = oldState;
}
/// <summary>
/// Writes the lines of a <see cref="LeafBlock" />
/// </summary>
/// <param name="leafBlock">The leaf block.</param>
/// <param name="writeEndOfLines">if set to <c>true</c> write end of lines.</param>
/// <returns>This instance</returns>
public GemtextRenderer WriteLeafRawLines(
LeafBlock leafBlock,
bool writeEndOfLines)
{
// Make sure we have sane input.
if (leafBlock == null)
{
throw new ArgumentNullException(nameof(leafBlock));
}
// If we have nothing to write, then don't do anything. Even though
// Markdig says this can't be null, `leafBlock.Lines` may be null
// according to the comments.
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (leafBlock.Lines.Lines == null)
{
return this;
}
// Go through the block and write out each of the lines.
StringLineGroup lines = leafBlock.Lines;
StringLine[] slices = lines.Lines;
for (int i = 0; i < lines.Count; i++)
{
if (!writeEndOfLines && i > 0)
{
this.WriteLine();
}
this.Write(ref slices[i].Slice);
if (writeEndOfLines)
{
this.WriteLine();
}
}
return this;
}
}

View file

@ -0,0 +1,18 @@
namespace MfGames.Markdown.Gemtext.Renderers;
/// <summary>
/// Describes the ways of formatting a HTML block.
/// </summary>
public enum HtmlBlockFormatting
{
/// <summary>
/// Indicates that HTML code blocks should just be removed.
/// </summary>
Remove,
/// <summary>
/// Indicates that HTML code blocks should be treated as blocks with
/// "html" as the type.
/// </summary>
CodeBlock,
}

View file

@ -0,0 +1,20 @@
namespace MfGames.Markdown.Gemtext.Renderers;
/// <summary>
/// Describes the ways of formatting inline elements such as emphasis,
/// strong, and other elements.
/// </summary>
public enum InlineFormatting
{
/// <summary>
/// Indicates that the inline should be remove and only the text
/// rendered.
/// </summary>
Remove,
/// <summary>
/// Indicates that the inline should be left in place in a normalized
/// form (such as converting `_italics_` into `*italics*`).
/// </summary>
Normalize,
}

View file

@ -0,0 +1,11 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Extensions;
public class WikiLink : LinkInline
{
public WikiLink()
{
this.IsClosed = false;
}
}

View file

@ -0,0 +1,47 @@
using Markdig;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
namespace MfGames.Markdown.Extensions;
/// <summary>
/// Translate `[[Bob]]` into `/bob/`.
/// </summary>
public class WikiLinkExtension : IMarkdownExtension
{
public WikiLinkExtension()
: this(null)
{
}
public WikiLinkExtension(WikiLinkOptions? options)
{
this.Options = options ?? new WikiLinkOptions();
}
public WikiLinkOptions Options { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
WikiLinkInlineParser? parser = pipeline.InlineParsers
.FindExact<WikiLinkInlineParser>();
if (parser != null)
{
return;
}
parser = new WikiLinkInlineParser(this.Options);
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(parser);
}
/// <inheritdoc />
public void Setup(
MarkdownPipeline pipeline,
IMarkdownRenderer renderer)
{
// No setup needed here because we're using LinkInline which does the
// bulk of the work.
}
}

View file

@ -0,0 +1,112 @@
using System.Linq;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Extensions;
public class WikiLinkInlineParser : InlineParser
{
private readonly WikiLinkOptions options;
public WikiLinkInlineParser(WikiLinkOptions options)
{
this.options = options;
this.OpeningCharacters = new[] { '[' };
}
/// <inheritdoc />
public override bool Match(
InlineProcessor processor,
ref StringSlice slice)
{
// We are looking for the `[[` opening for the tag and that the first
// one isn't escaped.
if (IsNotDelimiter(slice, '['))
{
return false;
}
// We need to loop over the entire link, including the `[[` and `]]`
// while keeping track since we'll swallow additional characters beyond
// the link.
int linkStart = slice.Start;
int linkEnd = slice.Start;
slice.Start += 2;
// Our content starts after the double '[['.
int contentStart = slice.Start;
// We need to find the end of the link (the `]]`).
while (IsNotDelimiter(slice, ']'))
{
slice.NextChar();
linkEnd = slice.Start;
}
// Pull out the components before we adjust for the ']]' for the end.
int contentEnd = linkEnd;
// Finish skipping over the `]]`.
slice.NextChar();
slice.NextChar();
// Format the label and the URL.
string content = slice.Text.Substring(
contentStart,
contentEnd - contentStart);
string[] contentParts = content.Split('|', 2);
string label = contentParts.Last();
string url = this.options.GetUrl(contentParts.First());
// Add in any trailing components. This merges the `'s` from
// `[[Dale]]'s` into the label.
while (this.options.IsTrailingLink(slice.CurrentChar))
{
label += slice.CurrentChar;
slice.NextChar();
linkEnd++;
}
// Create the link that we're replacing.
WikiLink link = new()
{
Span =
{
Start = processor.GetSourcePosition(
linkStart,
out int line,
out int column),
},
Line = line,
Column = column,
Url = url,
IsClosed = true,
};
link.AppendChild(
new LiteralInline()
{
Line = line,
Column = column,
Content = new StringSlice(label),
IsClosed = true,
});
// Replace the inline and then indicate we have a match.
processor.Inline = link;
return true;
}
private static bool IsNotDelimiter(
StringSlice slice,
char delimiter)
{
return slice.CurrentChar != delimiter
|| slice.PeekChar() != delimiter
|| slice.PeekCharExtra(-1) == '\\';
}
}

View file

@ -0,0 +1,31 @@
using System;
using Markdig.Helpers;
namespace MfGames.Markdown.Extensions;
public class WikiLinkOptions
{
public WikiLinkOptions()
{
this.GetUrl = a => a;
this.IsTrailingLink = a => a.IsAlpha() || a == '\'';
}
/// <summary>
/// The callback to determine the link from the given wiki link. This does
/// not include trailing additions or use the label (e.g., `(ab|cd)` would
/// get `ab` as the parameters of this function.
/// </summary>
public Func<string, string> GetUrl { get; set; }
/// <summary>
/// <para>
/// A callback to determine if the text after the link should be merged
/// with the link label. This allows links such as [[Dale]]'s to be turned
/// into "Dale's" but pointing to "Dale" as a page.
/// </para>
/// <para>The default is to include any character or the apostrophe.</para>
/// </summary>
public Func<char, bool> IsTrailingLink { get; set; }
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Markdown-"

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>Various extensions for MarkDig and classes for working with Markdown.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Markdig" Version="0.31.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,18 @@
using FluentValidation;
namespace MfGames.Nitride.Calendar;
public class CreateCalendarValidator : AbstractValidator<CreateCalender>
{
public CreateCalendarValidator()
{
this.RuleFor(x => x.Path)
.NotNull();
this.RuleFor(x => x.GetEventSummary)
.NotNull();
this.RuleFor(x => x.GetEventUrl)
.NotNull();
}
}

View file

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using MfGames.Nitride.Temporal;
using NodaTime;
using Zio;
namespace MfGames.Nitride.Calendar;
/// <summary>
/// Creates an iCalendar file from all the entities passed into the method
/// that have a NodaTime.Instant component. This will write both past and
/// future events.
/// </summary>
[WithProperties]
public partial class CreateCalender : OperationBase
{
private readonly TimeService clock;
private readonly IValidator<CreateCalender> validator;
public CreateCalender(
IValidator<CreateCalender> validator,
TimeService clock)
{
this.validator = validator;
this.clock = clock;
}
/// <summary>
/// Gets or sets a callback to get the summary of the event representing
/// the entity.
/// </summary>
public Func<Entity, string>? GetEventSummary { get; set; }
/// <summary>
/// Gets or sets a callback to get the optional URL of an event for
/// the entity.
/// </summary>
public Func<Entity, Uri?>? GetEventUrl { get; set; }
/// <summary>
/// Gets or sets the file system path for the resulting calendar.
/// </summary>
public UPath? Path { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
SplitEntityEnumerations split = input.SplitEntity<Instant>();
IEnumerable<Entity> datedAndCalendars =
this.CreateCalendarEntity(split.HasAll);
return datedAndCalendars.Union(split.NotHasAll);
}
private IEnumerable<Entity> CreateCalendarEntity(
IEnumerable<Entity> entities)
{
// Create the calendar in the same time zone as the rest of the system.
var calendar = new Ical.Net.Calendar();
calendar.TimeZones.Add(new VTimeZone(this.clock.DateTimeZone.Id));
// Go through the events and add all of them.
var input = entities.ToList();
IEnumerable<CalendarEvent> events =
input.Select(this.CreateCalendarEvent);
calendar.Events.AddRange(events);
// Create the iCalendar file.
var serializer = new CalendarSerializer();
string serializedCalendar = serializer.SerializeToString(calendar);
// Create the calendar entity and populate everything.
Entity calendarEntity = new Entity().Set(IsCalendar.Instance)
.Set(this.Path!.Value)
.SetTextContent(serializedCalendar);
// Return the results along with the new calendar.
return input.Union(new[] { calendarEntity });
}
private CalendarEvent CreateCalendarEvent(Entity entity)
{
Instant instant = entity.Get<Instant>();
var when = this.clock.ToDateTime(instant);
string summary = this.GetEventSummary!(entity);
Uri? url = this.GetEventUrl?.Invoke(entity);
var calendarEvent = new CalendarEvent
{
Summary = summary,
Start = new CalDateTime(when),
Url = url,
};
return calendarEvent;
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Nitride.Calendar-"

View file

@ -0,0 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Calendar;
/// <summary>
/// A marker component for identifying an entity that represents a calendar.
/// </summary>
[SingletonComponent]
public partial class IsCalendar
{
}

View file

@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>An extension to Nitride static site generator to generate iCalendar files.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Ical.Net" Version="4.2.0" />
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="Zio" Version="0.16.2" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,15 @@
using Autofac;
using MfGames.Nitride.Temporal.Setup;
namespace MfGames.Nitride.Calendar;
public static class NitrideCalendarBuilderExtensions
{
public static NitrideBuilder UseCalendar(this NitrideBuilder builder)
{
return builder
.UseTemporal()
.ConfigureContainer(x => x.RegisterModule<NitrideCalendarModule>());
}
}

View file

@ -0,0 +1,13 @@
using Autofac;
namespace MfGames.Nitride.Calendar;
public class NitrideCalendarModule : Module
{
/// <inheritdoc />
protected override void Load(ContainerBuilder builder)
{
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Feeds.Structure;
using MfGames.Nitride.Generators;
using NodaTime;
using Serilog;
using Zio;
namespace MfGames.Nitride.Feeds;
/// <summary>
/// Creates various feeds from the given input.
/// </summary>
[WithProperties]
public partial class CreateAtomFeed : OperationBase
{
private readonly ILogger logger;
private readonly IValidator<CreateAtomFeed> validator;
public CreateAtomFeed(
ILogger logger,
IValidator<CreateAtomFeed> validator)
{
this.logger = logger;
this.validator = validator;
this.GetAlternateMimeType = _ => "text/html";
}
/// <summary>
/// Gets or sets the base URL for all the links.
/// </summary>
public string? BaseUrl { get; set; }
/// <summary>
/// Gets or sets the alternate MIME type.
/// </summary>
public Func<Entity, string> GetAlternateMimeType { get; set; }
/// <summary>
/// Gets or sets the alternate URL associated with the feed.
/// </summary>
public Func<Entity, Uri>? GetAlternateUrl { get; set; }
/// <summary>
/// Gets or sets the callback to get the author for the feed.
/// </summary>
public Func<Entity, AtomAuthor>? GetAuthor { get; set; }
/// <summary>
/// Gets or sets the callback to get the entries associated with the
/// feed.
/// </summary>
public Func<Entity, IEnumerable<AtomEntry>>? GetEntries { get; set; }
/// <summary>
/// Gets or sets the identifier (typically a URL) of the feed.
/// </summary>
public Func<Entity, string>? GetId { get; set; }
/// <summary>
/// Gets or sets the callback to get the path of the generated feed.
/// </summary>
public Func<Entity, UPath>? GetPath { get; set; }
/// <summary>
/// Gets or sets the rights (license) of the feed.
/// </summary>
public Func<Entity, string>? GetRights { get; set; }
/// <summary>
/// A callback that gets the title of the feed from the given entity.
/// </summary>
public Func<Entity, string>? GetTitle { get; set; }
/// <summary>
/// Gets or sets the updated timestamp for the feed.
/// </summary>
public Func<Entity, Instant>? GetUpdated { get; set; }
/// <summary>
/// Gets or sets the URL associated with the feed.
/// </summary>
public Func<Entity, Uri>? GetUrl { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.SelectMany(this.CreateEntityFeed);
}
private IEnumerable<Entity> CreateEntityFeed(Entity entity)
{
// Create the top-level feed. All the nullable callbacks were
// verified in the function that calls this.
var feed = new AtomFeed
{
Title = this.GetTitle?.Invoke(entity),
Id = this.GetId?.Invoke(entity),
Rights = this.GetRights?.Invoke(entity),
Updated = this.GetUpdated?.Invoke(entity),
Url = this.GetUrl?.Invoke(entity),
AlternateUrl = this.GetAlternateUrl?.Invoke(entity),
AlternateMimeType = this.GetAlternateMimeType.Invoke(entity),
Author = this.GetAuthor?.Invoke(entity),
}.ToXElement();
// Go through all the items inside the feed and add them.
foreach (AtomEntry? entry in this.GetEntries!(entity))
{
feed.Add(entry.ToXElement());
}
// Create the feed entity and return both objects.
Entity feedEntity = new Entity().Set(IsFeed.Instance)
.Set(this.GetPath!(entity))
.SetTextContent(feed + "\n");
return new[] { entity, feedEntity };
}
}

View file

@ -0,0 +1,18 @@
using FluentValidation;
namespace MfGames.Nitride.Feeds;
public class CreateAtomFeedValidator : AbstractValidator<CreateAtomFeed>
{
public CreateAtomFeedValidator()
{
this.RuleFor(x => x.GetEntries)
.NotNull();
this.RuleFor(x => x.GetPath)
.NotNull();
this.RuleFor(x => x.GetTitle)
.NotNull();
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Nitride.Feeds-"

View file

@ -0,0 +1,14 @@
namespace MfGames.Nitride.Feeds;
/// <summary>
/// A marker component that indicates this entity has a feed associated with
/// it.
/// </summary>
public class HasFeed
{
public HasFeed()
{
}
public static HasFeed Instance { get; } = new();
}

View file

@ -0,0 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds;
/// <summary>
/// A marker component that indicates this page is a feed.
/// </summary>
[SingletonComponent]
public partial class IsFeed
{
}

View file

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>An extension to Nitride static site generator to generate Atom feeds.</Description>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="Zio" Version="0.16.2" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
using Autofac;
using MfGames.Nitride.Temporal.Setup;
namespace MfGames.Nitride.Feeds;
public static class NitrideFeedsBuilderExtensions
{
public static NitrideBuilder UseFeeds(this NitrideBuilder builder)
{
return builder.UseTemporal()
.ConfigureContainer(x => x.RegisterModule<NitrideFeedsModule>());
}
}

View file

@ -0,0 +1,13 @@
using Autofac;
namespace MfGames.Nitride.Feeds;
public class NitrideFeedsModule : Module
{
/// <inheritdoc />
protected override void Load(ContainerBuilder builder)
{
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -0,0 +1,42 @@
using System.Xml.Linq;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// The type-safe structure for an author element.
/// </summary>
[WithProperties]
public partial class AtomAuthor
{
/// <summary>
/// Gets or sets the name of the author.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement? ToXElement()
{
if (this.Name == null)
{
return null;
}
var author = new XElement(XmlConstants.AtomNamespace + "author");
if (!string.IsNullOrEmpty(this.Name))
{
author.Add(
new XElement(
XmlConstants.AtomNamespace + "name",
new XText(this.Name)));
}
return author;
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Xml.Linq;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// The type-safe structure for a entry's category element.
/// </summary>
[WithProperties]
public partial class AtomCategory
{
/// <summary>
/// Gets or sets the label associated with the category.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// Gets or sets the scheme associated with the category.
/// </summary>
public Uri? Scheme { get; set; }
/// <summary>
/// Gets or sets the term of the category.
/// </summary>
public string? Term { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement ToXElement()
{
if (this.Term == null)
{
throw new NullReferenceException("Category term cannot be null.");
}
var elem = new XElement(
XmlConstants.AtomNamespace + "category",
new XAttribute("term", this.Term));
if (this.Scheme != null)
{
elem.Add(new XAttribute("scheme", this.Scheme.ToString()));
}
if (!string.IsNullOrEmpty(this.Label))
{
elem.Add(new XAttribute("label", this.Label));
}
return elem;
}
}

View file

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using MfGames.Nitride.Generators;
using NodaTime;
using static MfGames.Nitride.Feeds.Structure.XmlConstants;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// The type-safe structure for an entry in the Atom feed.
/// </summary>
[WithProperties]
public partial class AtomEntry
{
/// <summary>
/// Gets or sets the author for the feed.
/// </summary>
public AtomAuthor? Author { get; set; }
/// <summary>
/// Gets or sets the categories associated with this entry.
/// </summary>
public IEnumerable<AtomCategory>? Categories { get; set; }
/// <summary>
/// Gets or sets the content of the entry.
/// </summary>
public string? Content { get; set; }
/// <summary>
/// Gets or sets the type of content (text, html) of the content.
/// </summary>
public string ContentType { get; set; } = "html";
/// <summary>
/// Gets or sets the ID of the feed.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Gets or sets the summary of the entry.
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// Gets or sets the type of content (text, html) of the summary.
/// </summary>
public string SummaryType { get; set; } = "html";
/// <summary>
/// Gets or sets the title of the Feed.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the timestamp that the feed was updated.
/// </summary>
public Instant? Updated { get; set; }
/// <summary>
/// Gets or sets the URL associated with this feed.
/// </summary>
public Uri? Url { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement? ToXElement()
{
var elem = new XElement(AtomNamespace + "entry");
AtomHelper.AddIfSet(elem, "title", this.Title);
if (this.Url != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("rel", "alternate"),
new XAttribute("href", this.Url.ToString())));
}
AtomHelper.AddIfSet(elem, "updated", this.Updated?.ToString("g", null));
AtomHelper.AddIfSet(elem, "id", this.Id);
AtomHelper.AddIfSet(elem, this.Author?.ToXElement());
if (this.Categories != null)
{
foreach (AtomCategory? category in this.Categories)
{
elem.Add(category.ToXElement());
}
}
if (!string.IsNullOrWhiteSpace(this.Summary))
{
elem.Add(
new XElement(
AtomNamespace + "summary",
new XAttribute("type", this.SummaryType),
new XText(this.Summary)));
}
if (!string.IsNullOrWhiteSpace(this.Content))
{
elem.Add(
new XElement(
AtomNamespace + "content",
new XAttribute("type", this.ContentType),
new XText(this.Content)));
}
return elem;
}
}

View file

@ -0,0 +1,101 @@
using System;
using System.Xml.Linq;
using NodaTime;
using static MfGames.Nitride.Feeds.Structure.XmlConstants;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// The type-safe structure of the top-level feed.
/// </summary>
public record AtomFeed
{
/// <summary>
/// Gets or sets the MIME type for the alternate URL.
/// </summary>
public string AlternateMimeType { get; set; } = "text/html";
/// <summary>
/// Gets or sets the alternate URL for this feed.
/// </summary>
public Uri? AlternateUrl { get; set; }
/// <summary>
/// Gets or sets the author for the feed.
/// </summary>
public AtomAuthor? Author { get; set; }
/// <summary>
/// Gets or sets the ID of the feed.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Gets or sets the rights (license) of the feed.
/// </summary>
public string? Rights { get; set; }
/// <summary>
/// Gets or sets the title of the Feed.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the timestamp that the feed was updated.
/// </summary>
public Instant? Updated { get; set; }
/// <summary>
/// Gets or sets the URL associated with this feed.
/// </summary>
public Uri? Url { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement ToXElement()
{
var elem = new XElement(AtomNamespace + "feed");
if (!string.IsNullOrWhiteSpace(this.Title))
{
elem.Add(
new XElement(
AtomNamespace + "title",
new XAttribute("type", "text"),
new XAttribute(XNamespace.Xml + "lang", "en"),
new XText(this.Title)));
}
if (this.Url != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("type", "application/atom+xml"),
new XAttribute("href", this.Url.ToString()),
new XAttribute("rel", "self")));
}
if (this.AlternateUrl != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("type", this.AlternateMimeType),
new XAttribute("href", this.AlternateUrl.ToString()),
new XAttribute("rel", "alternate")));
}
AtomHelper.AddIfSet(elem, "updated", this.Updated?.ToString("g", null));
AtomHelper.AddIfSet(elem, "id", this.Id);
AtomHelper.AddIfSet(elem, this.Author?.ToXElement());
AtomHelper.AddIfSet(elem, "rights", this.Rights);
return elem;
}
}

View file

@ -0,0 +1,33 @@
using System.Xml.Linq;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// Helper methods for working with XML elements.
/// </summary>
public static class AtomHelper
{
public static void AddIfSet(
XElement root,
XElement? elem)
{
if (elem != null)
{
root.Add(elem);
}
}
public static void AddIfSet(
XElement elem,
string name,
string? text)
{
if (!string.IsNullOrWhiteSpace(text))
{
elem.Add(
new XElement(
XmlConstants.AtomNamespace + name,
new XText(text)));
}
}
}

View file

@ -0,0 +1,21 @@
using System.Xml.Linq;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
/// Common constants used while generating feeds.
/// </summary>
public static class XmlConstants
{
/// <summary>
/// The XML namespace for Atom feeds.
/// </summary>
public static readonly XNamespace AtomNamespace =
"http://www.w3.org/2005/Atom";
/// <summary>
/// The XML namespace for media.
/// </summary>
public static readonly XNamespace MediaNamespace =
"http://search.yahoo.com/mrss/";
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Nitride.Gemtext-"

View file

@ -0,0 +1,12 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Gemtext;
/// <summary>
/// A marker component for indicating that an entity is Gemtext, the format
/// for text files using the Gemini protocol.
/// </summary>
[SingletonComponent]
public partial class IsGemtext
{
}

View file

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
<Description>An extension to Nitride static site generator to generate Gemtext output.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,12 @@
using Autofac;
namespace MfGames.Nitride.Gemtext;
public static class NitrideGemtextBuilderExtensions
{
public static NitrideBuilder UseGemtext(this NitrideBuilder builder)
{
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideGemtextModule>());
}
}

View file

@ -0,0 +1,11 @@
using Autofac;
namespace MfGames.Nitride.Gemtext;
public class NitrideGemtextModule : Module
{
/// <inheritdoc />
protected override void Load(ContainerBuilder builder)
{
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Internal class that consolidates all of the information needed to generate a
/// class for adding With* properties.
/// </summary>
public class ClassAttributeReference
{
/// <summary>
/// Gets the syntax for the class declaration.
/// </summary>
public ClassDeclarationSyntax ClassDeclaration { get; set; } = null!;
/// <summary>
/// Gets or sets the namespace associated with the class.
/// </summary>
public string Namespace { get; set; } = null!;
/// <summary>
/// Gets the using statements that are in the class.
/// </summary>
public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new();
}

View file

@ -0,0 +1,66 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Base class for classes marked with an attribute.
/// </summary>
public abstract class ClassAttributeSourceGeneratorBase<TSyntaxReceiver>
: ISourceGenerator
where TSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
public void Execute(GeneratorExecutionContext context)
{
// Get the generator infrastructure will create a receiver and
// populate it we can retrieve the populated instance via the
// context.
if (context.SyntaxReceiver is not TSyntaxReceiver syntaxReceiver)
{
return;
}
// Report any messages.
foreach (string? message in syntaxReceiver.Messages)
{
context.Warning(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(
new LinePosition(0, 0),
new LinePosition(0, 0))),
"{0}: Syntax Message: {1}",
this.GetType().Name,
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ReferenceList.Count == 0)
{
return;
}
// Go through each one.
foreach (ClassAttributeReference reference in syntaxReceiver
.ReferenceList)
{
this.GenerateClassFile(context, reference);
}
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => this.CreateSyntaxReceiver(context));
}
protected abstract TSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context);
protected abstract void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference reference);
}

View file

@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MfGames.Nitride.Generators;
public abstract class ClassAttributeSyntaxReceiverBase : ISyntaxReceiver
{
private readonly string attributeName;
private readonly GeneratorInitializationContext context;
public ClassAttributeSyntaxReceiverBase(
GeneratorInitializationContext context,
string attributeName)
{
this.context = context;
this.attributeName = attributeName;
this.ReferenceList = new List<ClassAttributeReference>();
this.Messages = new List<string>();
}
/// <summary>
/// Gets or sets a value indicating whether we should debug parsing attributes.
/// </summary>
public bool DebugAttributes { get; set; }
public List<string> Messages { get; }
/// <summary>
/// Gets or sets the name of the analyzed namespace.
/// </summary>
public string? Namespace { get; private set; }
public List<ClassAttributeReference> ReferenceList { get; }
public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// Check for namespaces.
switch (syntaxNode)
{
case CompilationUnitSyntax:
// Reset everything.
this.Namespace = null!;
this.UsingDirectiveList = new List<UsingDirectiveSyntax>();
break;
case NamespaceDeclarationSyntax syntax:
this.Namespace = syntax.Name.ToString();
return;
case FileScopedNamespaceDeclarationSyntax syntax:
this.Namespace = syntax.Name.ToString();
return;
case UsingDirectiveSyntax syntax:
this.UsingDirectiveList.Add(syntax);
return;
case ClassDeclarationSyntax:
break;
default:
return;
}
// We only care about class declarations.
if (syntaxNode is not ClassDeclarationSyntax cds)
{
return;
}
// See if the class has our set properties attribute.
var attributes = cds.AttributeLists
.AsEnumerable()
.SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString())
.ToList();
bool found = attributes
.Any(
x => x == this.attributeName
|| x == $"{this.attributeName}Attribute");
if (this.DebugAttributes)
{
this.Messages.Add(
string.Format(
"Parsing {0} found? {1} from attributes [{2}]",
cds.Identifier,
found,
string.Join(", ", attributes)));
}
if (found)
{
this.ReferenceList.Add(
new ClassAttributeReference
{
Namespace = this.Namespace!,
UsingDirectiveList = this.UsingDirectiveList,
ClassDeclaration = cds,
});
}
}
}

View file

@ -0,0 +1,158 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Various wrappers around the diagnostics to simplify generation.
/// </summary>
public static class CodeAnalysisExtensions
{
/// <summary>
/// Creates an error message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Error(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object?[] parameters)
{
Error(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates an error message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Error(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object?[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Error,
format,
parameters);
}
/// <summary>
/// Creates an informational message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Information(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object?[] parameters)
{
Information(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates an informational message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Information(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object?[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Info,
format,
parameters);
}
/// <summary>
/// Creates a warning message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Warning(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object?[] parameters)
{
Warning(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates a warning message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Warning(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object?[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Warning,
format,
parameters);
}
/// <summary>
/// Creates a message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
/// <param name="severity">The severity of the message.</param>
private static void Message(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
DiagnosticSeverity severity,
string format,
params object?[] parameters)
{
context.ReportDiagnostic(
Diagnostic.Create(
"GN" + ((int)messageCode).ToString("D4"),
"Nitride",
string.Format(format, parameters),
severity,
severity,
true,
severity is DiagnosticSeverity.Warning
or DiagnosticSeverity.Info
? 4
: 0,
location: location));
}
}

View file

@ -0,0 +1,13 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "MfGames.Nitride.Generators-"

View file

@ -0,0 +1,9 @@
namespace MfGames.Nitride.Generators;
/// <summary>
/// All the error messages produced by the generators.
/// </summary>
public enum MessageCode
{
Debug = 1,
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>cli</PackageTags>
<PackageProjectUrl>https://src.mfgames.com/mfgames-cil/mfgames-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
<Description>Common source generators for Nitride.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,91 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Implements a source generator that creates the additional properties
/// and methods for a singleton component including the constructor and
/// instance methods, along with extension methods for adding them to entities.
/// </summary>
[Generator]
public class SingletonComponentSourceGenerator
: ClassAttributeSourceGeneratorBase<SingletonComponentSyntaxReceiver>
{
protected override SingletonComponentSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{
return new SingletonComponentSyntaxReceiver(context);
}
protected override void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
buffer.AppendLine("#nullable enable");
// Copy the using statements from the file.
foreach (UsingDirectiveSyntax? uds in unit.UsingDirectiveList)
{
buffer.AppendLine(uds.ToString());
}
buffer.AppendLine();
// Create the namespace.
SyntaxToken cls = cds.Identifier;
buffer.AppendLine(
string.Join(
"\n",
$"using MfGames.Gallium;",
$"",
$"namespace {unit.Namespace}",
$"{{",
$" public partial class {cls}",
$" {{",
$" static {cls}()",
$" {{",
$" Instance = new {cls}();",
$" }}",
$"",
$" private {cls}()",
$" {{",
$" }}",
$"",
$" public static {cls} Instance {{ get; }}",
$" }}",
$"",
$" public static class {cls}Extensions",
$" {{",
$" public static bool Has{cls}(this Entity entity)",
$" {{",
$" return entity.Has<{cls}>();",
$" }}",
$"",
$" public static Entity Remove{cls}(this Entity entity)",
$" {{",
$" return entity.Remove<{cls}>();",
$" }}",
$"",
$" public static Entity Set{cls}(this Entity entity)",
$" {{",
$" return entity.Set({cls}.Instance);",
$" }}",
$" }}",
$"}}",
""));
// Create the source text and write out the file.
var sourceText = SourceText.From(buffer.ToString(), Encoding.UTF8);
context.AddSource(cls + ".Generated.cs", sourceText);
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class SingletonComponentSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public SingletonComponentSyntaxReceiver(
GeneratorInitializationContext context)
: base(context, "SingletonComponent")
{
}
}

View file

@ -0,0 +1,115 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Implements a source generator that creates Set* methods for the various
/// properties that also returns the same object for purposes of chaining
/// together calls.
/// </summary>
[Generator]
public class WithPropertiesSourceGenerator
: ClassAttributeSourceGeneratorBase<WithPropertiesSyntaxReceiver>
{
/// <inheritdoc />
protected override WithPropertiesSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{
return new WithPropertiesSyntaxReceiver(context);
}
protected override void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
buffer.AppendLine("#nullable enable");
// Copy the using statements from the file.
foreach (UsingDirectiveSyntax? uds in unit.UsingDirectiveList)
{
buffer.AppendLine(uds.ToString());
}
buffer.AppendLine();
// Create the namespace.
buffer.AppendLine($"namespace {unit.Namespace}");
buffer.AppendLine("{");
buffer.AppendLine($" public partial class {cds.Identifier}");
buffer.AppendLine(" {");
// Go through the properties of the namespace.
IEnumerable<PropertyDeclarationSyntax> properties = cds.Members
.Where(m => m.Kind() == SyntaxKind.PropertyDeclaration)
.Cast<PropertyDeclarationSyntax>();
bool first = true;
foreach (PropertyDeclarationSyntax pds in properties)
{
// See if we have a setter.
bool found = pds.AccessorList?.Accessors
.Any(x => x.Keyword.ToString() == "set")
?? false;
if (!found)
{
continue;
}
// If we aren't first, then add a newline before it.
if (first)
{
first = false;
}
else
{
buffer.AppendLine();
}
// Write some documentation.
buffer.AppendLine(" /// <summary>");
buffer.AppendLine(
string.Format(
" /// Sets the {0} value and returns the operation for chaining.",
pds.Identifier.ToString()));
buffer.AppendLine(" /// </summary>");
// We have the components for writing out a setter.
buffer.AppendLine(
string.Format(
" public virtual {0} With{1}({2} value)",
cds.Identifier,
pds.Identifier,
pds.Type));
buffer.AppendLine(" {");
buffer.AppendLine(
string.Format(" this.{0} = value;", pds.Identifier));
buffer.AppendLine(" return this;");
buffer.AppendLine(" }");
}
// Finish up the class.
buffer.AppendLine(" }");
buffer.AppendLine("}");
// Create the source text and write out the file.
var sourceText = SourceText.From(buffer.ToString(), Encoding.UTF8);
context.AddSource(cds.Identifier + ".Generated.cs", sourceText);
}
}

View file

@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class WithPropertiesSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public WithPropertiesSyntaxReceiver(GeneratorInitializationContext context)
: base(context, "WithProperties")
{
}
}

Some files were not shown because too many files have changed in this diff Show more