feat!: mass updating dependencies with repository merge
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
6135a3fdbd
commit
2d7de86856
|
@ -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>
|
||||
|
|
508
src/MfGames.Gallium/Entity.cs
Normal file
508
src/MfGames.Gallium/Entity.cs
Normal 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;
|
||||
}
|
||||
}
|
13
src/MfGames.Gallium/GitVersion.yml
Normal file
13
src/MfGames.Gallium/GitVersion.yml
Normal 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-"
|
28
src/MfGames.Gallium/JoinEntityExtensions.cs
Normal file
28
src/MfGames.Gallium/JoinEntityExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
26
src/MfGames.Gallium/MfGames.Gallium.csproj
Normal file
26
src/MfGames.Gallium/MfGames.Gallium.csproj
Normal 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>
|
50
src/MfGames.Gallium/SelectComponentExtensions.cs
Normal file
50
src/MfGames.Gallium/SelectComponentExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
src/MfGames.Gallium/SelectComponentOrDefaultExtensions.cs
Normal file
53
src/MfGames.Gallium/SelectComponentOrDefaultExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
280
src/MfGames.Gallium/SelectEntityExtensions.cs
Normal file
280
src/MfGames.Gallium/SelectEntityExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
153
src/MfGames.Gallium/SelectManyEntityExtensions.cs
Normal file
153
src/MfGames.Gallium/SelectManyEntityExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
18
src/MfGames.Gallium/SplitEntityEnumerations.cs
Normal file
18
src/MfGames.Gallium/SplitEntityEnumerations.cs
Normal 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;
|
||||
}
|
300
src/MfGames.Gallium/SplitEntityExtensions.cs
Normal file
300
src/MfGames.Gallium/SplitEntityExtensions.cs
Normal file
|
@ -0,0 +1,300 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MfGames.Gallium;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for IEnumerable<Entity> 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);
|
||||
}
|
||||
}
|
47
src/MfGames.Gallium/WhereEntityExtensions.cs
Normal file
47
src/MfGames.Gallium/WhereEntityExtensions.cs
Normal 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>()));
|
||||
}
|
||||
}
|
31
src/MfGames.Gallium/WhereEntityHasExtensions.cs
Normal file
31
src/MfGames.Gallium/WhereEntityHasExtensions.cs
Normal 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>());
|
||||
}
|
||||
}
|
31
src/MfGames.Gallium/WhereEntityNotHasExtensions.cs
Normal file
31
src/MfGames.Gallium/WhereEntityNotHasExtensions.cs
Normal 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>());
|
||||
}
|
||||
}
|
13
src/MfGames.Locking/GitVersion.yml
Normal file
13
src/MfGames.Locking/GitVersion.yml
Normal 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-"
|
34
src/MfGames.Locking/MfGames.Locking.csproj
Normal file
34
src/MfGames.Locking/MfGames.Locking.csproj
Normal 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>
|
47
src/MfGames.Locking/NestableReadLock.cs
Normal file
47
src/MfGames.Locking/NestableReadLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
src/MfGames.Locking/NestableUpgradableReadLock.cs
Normal file
48
src/MfGames.Locking/NestableUpgradableReadLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
src/MfGames.Locking/NestableWriteLock.cs
Normal file
45
src/MfGames.Locking/NestableWriteLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
src/MfGames.Locking/ReadLock.cs
Normal file
30
src/MfGames.Locking/ReadLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
32
src/MfGames.Locking/UpgradableLock.cs
Normal file
32
src/MfGames.Locking/UpgradableLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
32
src/MfGames.Locking/WriteLock.cs
Normal file
32
src/MfGames.Locking/WriteLock.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
28
src/MfGames.Markdown.Gemtext/Extensions/HtmlAsCodeBlocks.cs
Normal file
28
src/MfGames.Markdown.Gemtext/Extensions/HtmlAsCodeBlocks.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
13
src/MfGames.Markdown.Gemtext/GitVersion.yml
Normal file
13
src/MfGames.Markdown.Gemtext/GitVersion.yml
Normal 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-"
|
84
src/MfGames.Markdown.Gemtext/MarkdownGemtext.cs
Normal file
84
src/MfGames.Markdown.Gemtext/MarkdownGemtext.cs
Normal 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;
|
||||
}
|
||||
}
|
28
src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj
Normal file
28
src/MfGames.Markdown.Gemtext/MfGames.Markdown.Gemtext.csproj
Normal 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>
|
33
src/MfGames.Markdown.Gemtext/Renderers/BlockLinkHandling.cs
Normal file
33
src/MfGames.Markdown.Gemtext/Renderers/BlockLinkHandling.cs
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("---");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 (<br />)
|
||||
/// </summary>
|
||||
public bool RenderAsHardlineBreak { get; set; }
|
||||
|
||||
protected override void Write(
|
||||
GemtextRenderer renderer,
|
||||
LineBreakInline obj)
|
||||
{
|
||||
if (obj.IsHard || this.RenderAsHardlineBreak)
|
||||
{
|
||||
renderer.EnsureTwoLines();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Write(" ");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
187
src/MfGames.Markdown.Gemtext/Renderers/GemtextRenderer.cs
Normal file
187
src/MfGames.Markdown.Gemtext/Renderers/GemtextRenderer.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
20
src/MfGames.Markdown.Gemtext/Renderers/InlineFormatting.cs
Normal file
20
src/MfGames.Markdown.Gemtext/Renderers/InlineFormatting.cs
Normal 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,
|
||||
}
|
11
src/MfGames.Markdown/Extensions/WikiLink.cs
Normal file
11
src/MfGames.Markdown/Extensions/WikiLink.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace MfGames.Markdown.Extensions;
|
||||
|
||||
public class WikiLink : LinkInline
|
||||
{
|
||||
public WikiLink()
|
||||
{
|
||||
this.IsClosed = false;
|
||||
}
|
||||
}
|
47
src/MfGames.Markdown/Extensions/WikiLinkExtension.cs
Normal file
47
src/MfGames.Markdown/Extensions/WikiLinkExtension.cs
Normal 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.
|
||||
}
|
||||
}
|
112
src/MfGames.Markdown/Extensions/WikiLinkInlineParser.cs
Normal file
112
src/MfGames.Markdown/Extensions/WikiLinkInlineParser.cs
Normal 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) == '\\';
|
||||
}
|
||||
}
|
31
src/MfGames.Markdown/Extensions/WikiLinkOptions.cs
Normal file
31
src/MfGames.Markdown/Extensions/WikiLinkOptions.cs
Normal 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; }
|
||||
}
|
13
src/MfGames.Markdown/GitVersion.yml
Normal file
13
src/MfGames.Markdown/GitVersion.yml
Normal 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-"
|
27
src/MfGames.Markdown/MfGames.Markdown.csproj
Normal file
27
src/MfGames.Markdown/MfGames.Markdown.csproj
Normal 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>
|
18
src/MfGames.Nitride.Calendar/CreateCalendarValidator.cs
Normal file
18
src/MfGames.Nitride.Calendar/CreateCalendarValidator.cs
Normal 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();
|
||||
}
|
||||
}
|
118
src/MfGames.Nitride.Calendar/CreateCalender.cs
Normal file
118
src/MfGames.Nitride.Calendar/CreateCalender.cs
Normal 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;
|
||||
}
|
||||
}
|
13
src/MfGames.Nitride.Calendar/GitVersion.yml
Normal file
13
src/MfGames.Nitride.Calendar/GitVersion.yml
Normal 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-"
|
11
src/MfGames.Nitride.Calendar/IsCalendar.cs
Normal file
11
src/MfGames.Nitride.Calendar/IsCalendar.cs
Normal 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
|
||||
{
|
||||
}
|
47
src/MfGames.Nitride.Calendar/MfGames.Nitride.Calendar.csproj
Normal file
47
src/MfGames.Nitride.Calendar/MfGames.Nitride.Calendar.csproj
Normal 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>
|
|
@ -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>());
|
||||
}
|
||||
}
|
13
src/MfGames.Nitride.Calendar/NitrideCalendarModule.cs
Normal file
13
src/MfGames.Nitride.Calendar/NitrideCalendarModule.cs
Normal 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);
|
||||
}
|
||||
}
|
135
src/MfGames.Nitride.Feeds/CreateAtomFeed.cs
Normal file
135
src/MfGames.Nitride.Feeds/CreateAtomFeed.cs
Normal 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 };
|
||||
}
|
||||
}
|
18
src/MfGames.Nitride.Feeds/CreateAtomFeedValidator.cs
Normal file
18
src/MfGames.Nitride.Feeds/CreateAtomFeedValidator.cs
Normal 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();
|
||||
}
|
||||
}
|
13
src/MfGames.Nitride.Feeds/GitVersion.yml
Normal file
13
src/MfGames.Nitride.Feeds/GitVersion.yml
Normal 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-"
|
14
src/MfGames.Nitride.Feeds/HasFeed.cs
Normal file
14
src/MfGames.Nitride.Feeds/HasFeed.cs
Normal 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();
|
||||
}
|
11
src/MfGames.Nitride.Feeds/IsFeed.cs
Normal file
11
src/MfGames.Nitride.Feeds/IsFeed.cs
Normal 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
|
||||
{
|
||||
}
|
46
src/MfGames.Nitride.Feeds/MfGames.Nitride.Feeds.csproj
Normal file
46
src/MfGames.Nitride.Feeds/MfGames.Nitride.Feeds.csproj
Normal 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>
|
14
src/MfGames.Nitride.Feeds/NitrideFeedsBuilderExtensions.cs
Normal file
14
src/MfGames.Nitride.Feeds/NitrideFeedsBuilderExtensions.cs
Normal 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>());
|
||||
}
|
||||
}
|
13
src/MfGames.Nitride.Feeds/NitrideFeedsModule.cs
Normal file
13
src/MfGames.Nitride.Feeds/NitrideFeedsModule.cs
Normal 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);
|
||||
}
|
||||
}
|
42
src/MfGames.Nitride.Feeds/Structure/AtomAuthor.cs
Normal file
42
src/MfGames.Nitride.Feeds/Structure/AtomAuthor.cs
Normal 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;
|
||||
}
|
||||
}
|
57
src/MfGames.Nitride.Feeds/Structure/AtomCategory.cs
Normal file
57
src/MfGames.Nitride.Feeds/Structure/AtomCategory.cs
Normal 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;
|
||||
}
|
||||
}
|
121
src/MfGames.Nitride.Feeds/Structure/AtomEntry.cs
Normal file
121
src/MfGames.Nitride.Feeds/Structure/AtomEntry.cs
Normal 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;
|
||||
}
|
||||
}
|
101
src/MfGames.Nitride.Feeds/Structure/AtomFeed.cs
Normal file
101
src/MfGames.Nitride.Feeds/Structure/AtomFeed.cs
Normal 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;
|
||||
}
|
||||
}
|
33
src/MfGames.Nitride.Feeds/Structure/AtomHelper.cs
Normal file
33
src/MfGames.Nitride.Feeds/Structure/AtomHelper.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
21
src/MfGames.Nitride.Feeds/Structure/XmlConstants.cs
Normal file
21
src/MfGames.Nitride.Feeds/Structure/XmlConstants.cs
Normal 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/";
|
||||
}
|
13
src/MfGames.Nitride.Gemtext/GitVersion.yml
Normal file
13
src/MfGames.Nitride.Gemtext/GitVersion.yml
Normal 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-"
|
12
src/MfGames.Nitride.Gemtext/IsGemtext.cs
Normal file
12
src/MfGames.Nitride.Gemtext/IsGemtext.cs
Normal 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
|
||||
{
|
||||
}
|
42
src/MfGames.Nitride.Gemtext/MfGames.Nitride.Gemtext.csproj
Normal file
42
src/MfGames.Nitride.Gemtext/MfGames.Nitride.Gemtext.csproj
Normal 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>
|
|
@ -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>());
|
||||
}
|
||||
}
|
11
src/MfGames.Nitride.Gemtext/NitrideGemtextModule.cs
Normal file
11
src/MfGames.Nitride.Gemtext/NitrideGemtextModule.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Autofac;
|
||||
|
||||
namespace MfGames.Nitride.Gemtext;
|
||||
|
||||
public class NitrideGemtextModule : Module
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
}
|
||||
}
|
27
src/MfGames.Nitride.Generators/ClassAttributeReference.cs
Normal file
27
src/MfGames.Nitride.Generators/ClassAttributeReference.cs
Normal 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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
158
src/MfGames.Nitride.Generators/CodeAnalysisExtensions.cs
Normal file
158
src/MfGames.Nitride.Generators/CodeAnalysisExtensions.cs
Normal 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));
|
||||
}
|
||||
}
|
13
src/MfGames.Nitride.Generators/GitVersion.yml
Normal file
13
src/MfGames.Nitride.Generators/GitVersion.yml
Normal 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-"
|
9
src/MfGames.Nitride.Generators/MessageCode.cs
Normal file
9
src/MfGames.Nitride.Generators/MessageCode.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace MfGames.Nitride.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// All the error messages produced by the generators.
|
||||
/// </summary>
|
||||
public enum MessageCode
|
||||
{
|
||||
Debug = 1,
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace MfGames.Nitride.Generators;
|
||||
|
||||
public class SingletonComponentSyntaxReceiver : ClassAttributeSyntaxReceiverBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SingletonComponentSyntaxReceiver(
|
||||
GeneratorInitializationContext context)
|
||||
: base(context, "SingletonComponent")
|
||||
{
|
||||
}
|
||||
}
|
115
src/MfGames.Nitride.Generators/WithPropertiesSourceGenerator.cs
Normal file
115
src/MfGames.Nitride.Generators/WithPropertiesSourceGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue