ImageSharp/SixLabors.Fonts/Unicode/StateAutomation/StateMachine.cs
2026-08-03 22:31:27 +02:00

156 lines
5.0 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts;
using System;
using System.Collections.Generic;
using System.Linq;
namespace UnicodeTrieGenerator.StateAutomation {
internal class StateMachine
{
private const int InitialState = 1;
private const int FailState = 0;
/// <summary>
/// Initializes a new instance of the <see cref="StateMachine"/> class.
/// </summary>
/// <param name="stateTable">The state table.</param>
/// <param name="accepting">The accepting states.</param>
/// <param name="tags">The tags.</param>
public StateMachine(int[][] stateTable, bool[] accepting, string[][] tags)
{
this.StateTable = stateTable;
this.Accepting = accepting;
this.Tags = tags;
}
/// <summary>
/// Gets the state table.
/// </summary>
public int[][] StateTable { get; }
/// <summary>
/// Gets the accepting states.
/// </summary>
public bool[] Accepting { get; }
/// <summary>
/// Gets the tags.
/// </summary>
public string[][] Tags { get; }
/// <summary>
/// Returns an iterable object that yields pattern matches over the input sequence.
/// </summary>
/// <param name="input">The input sequence.</param>
/// <returns>The <see cref="IEnumerable{StateMatch}"/>.</returns>
public IEnumerable<StateMatch> Match(ReadOnlySpan<int> input)
{
int state = InitialState;
int? startRun = null;
int? lastAccepting = null;
List<StateMatch> matches = new(input.Length);
for (int i = 0; i < input.Length; i++)
{
int c = input[i];
int lastState = state;
state = this.StateTable[state][c];
if (state == FailState)
{
// yield the last match if any.
if (startRun != null && lastAccepting != null && lastAccepting >= startRun)
{
matches.Add(new StateMatch()
{
StartIndex = startRun.Value,
EndIndex = lastAccepting.Value,
Tags = this.Tags[lastState]
});
}
// reset the state as if we started over from the initial state
state = this.StateTable[InitialState][c];
startRun = null;
}
// start a run if not in the failure state
if (state != FailState && startRun == null)
{
startRun = i;
}
// if accepting, mark the potential match end
if (this.Accepting[state])
{
lastAccepting = i;
}
// reset the state to the initial state if we get into the failure state
if (state == FailState)
{
state = InitialState;
}
}
// yield the last match if any.
if (startRun != null && lastAccepting != null && lastAccepting >= startRun)
{
matches.Add(new StateMatch()
{
StartIndex = startRun.Value,
EndIndex = lastAccepting.Value,
Tags = this.Tags[state]
});
}
return matches;
}
/// <summary>
/// For each match over the input sequence, action functions matching
/// the tag definitions in the input pattern are called with the startIndex,
/// length, and the sequence to be sliced.
/// </summary>
/// <param name="input">The input sequence.</param>
/// <param name="actions">The collection of actions.</param>
public void Apply(int[] input, Dictionary<string, Action<int, int, ArraySlice<int>>> actions)
{
foreach (StateMatch match in this.Match(input))
{
foreach (string tag in match.Tags)
{
if (actions.TryGetValue(tag, out Action<int, int, ArraySlice<int>>? action))
{
action(match.StartIndex, match.EndIndex, new ArraySlice<int>(input, match.StartIndex, match.EndIndex + 1 - match.StartIndex));
}
}
}
}
}
internal class StateMatch : IEquatable<StateMatch?>
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public IList<string> Tags { get; set; } = Array.Empty<string>();
public override bool Equals(object? obj) => this.Equals(obj as StateMatch);
public bool Equals(StateMatch? other)
=> other is not null
&& this.StartIndex == other.StartIndex
&& this.EndIndex == other.EndIndex
&& this.Tags.SequenceEqual(other.Tags);
public override int GetHashCode()
=> HashCode.Combine(this.StartIndex, this.EndIndex, this.Tags);
}
}