fix: Fixes string format of cliloc arguments (#1015)
This commit is contained in:
parent
47d427ed6e
commit
8031de4738
3 changed files with 456 additions and 478 deletions
|
|
@ -96,20 +96,6 @@ public static class Localization
|
||||||
public static string GetText(int number, string lang = FallbackLanguage) =>
|
public static string GetText(int number, string lang = FallbackLanguage) =>
|
||||||
TryGetLocalization(lang, number, out var entry) ? entry.Text : null;
|
TryGetLocalization(lang, number, out var entry) ? entry.Text : null;
|
||||||
|
|
||||||
public static string Format(int number, string lang = FallbackLanguage) => GetText(number, lang);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a formatted string of the localization entry using the <see cref="FallbackLanguage" />.
|
|
||||||
/// Uses <see cref="string.Format"/> under the hood.
|
|
||||||
/// Note: This method is not recommended since it uses almost double the memory and 50% more processing.
|
|
||||||
/// Instead use Format with string interpolation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="number">Localization number</param>
|
|
||||||
/// <param name="args">An object array containing zero or more objects to format</param>
|
|
||||||
/// <returns>A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments</returns>
|
|
||||||
public static string Format(int number, params object[] args) =>
|
|
||||||
!TryGetLocalization(number, out var entry) ? null : string.Format(entry.StringFormatter, args);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a formatted string of the localization entry using the specified language.
|
/// Creates a formatted string of the localization entry using the specified language.
|
||||||
/// Uses <see cref="string.Format"/> under the hood.
|
/// Uses <see cref="string.Format"/> under the hood.
|
||||||
|
|
@ -120,8 +106,8 @@ public static class Localization
|
||||||
/// <param name="lang">Language in ISO 639-2 format</param>
|
/// <param name="lang">Language in ISO 639-2 format</param>
|
||||||
/// <param name="args">An object array containing zero or more objects to format</param>
|
/// <param name="args">An object array containing zero or more objects to format</param>
|
||||||
/// <returns>A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments</returns>
|
/// <returns>A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments</returns>
|
||||||
public static string Format(int number, string lang, params object[] args) =>
|
public static string Format(int number, string lang = FallbackLanguage, params object[] args) =>
|
||||||
!TryGetLocalization(lang, number, out var entry) ? null : string.Format(entry.StringFormatter, args);
|
TryGetLocalization(lang, number, out var entry) ? entry.Format(args) : null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a localization entry using the <see cref="FallbackLanguage" />.
|
/// Gets a localization entry using the <see cref="FallbackLanguage" />.
|
||||||
|
|
@ -171,7 +157,7 @@ public static class Localization
|
||||||
public static PooledArraySpanFormattable Format(
|
public static PooledArraySpanFormattable Format(
|
||||||
int number, string lang,
|
int number, string lang,
|
||||||
[InterpolatedStringHandlerArgument("number", "lang")]
|
[InterpolatedStringHandlerArgument("number", "lang")]
|
||||||
ref LocalizationInterpolationHandler handler
|
ref LocalizationEntry.LocalizationInterpolationHandler handler
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var chars = handler.ToPooledArray(out var length);
|
var chars = handler.ToPooledArray(out var length);
|
||||||
|
|
@ -191,7 +177,7 @@ public static class Localization
|
||||||
public static PooledArraySpanFormattable Format(
|
public static PooledArraySpanFormattable Format(
|
||||||
int number,
|
int number,
|
||||||
[InterpolatedStringHandlerArgument("number")]
|
[InterpolatedStringHandlerArgument("number")]
|
||||||
ref LocalizationInterpolationHandler handler
|
ref LocalizationEntry.LocalizationInterpolationHandler handler
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var chars = handler.ToPooledArray(out var length);
|
var chars = handler.ToPooledArray(out var length);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
*************************************************************************/
|
*************************************************************************/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Server.Buffers;
|
using Server.Buffers;
|
||||||
|
|
@ -82,7 +84,23 @@ public class LocalizationEntry
|
||||||
builder.Dispose();
|
builder.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Format(params object[] args) => string.Format(StringFormatter, args);
|
public string Format(params object[] args)
|
||||||
|
{
|
||||||
|
if (args == null || args.Length == 0 || StringFormatter == null)
|
||||||
|
{
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
if (args[i] is string s && s[0] == '#' && int.TryParse(s.AsSpan(1), out var number))
|
||||||
|
{
|
||||||
|
args[i] = Localization.GetText(number, Language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(StringFormatter, args);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a formatted string of the localization entry.
|
/// Creates a formatted string of the localization entry.
|
||||||
|
|
@ -101,4 +119,437 @@ public class LocalizationEntry
|
||||||
handler = default; // Defensive clear
|
handler = default; // Defensive clear
|
||||||
return new PooledArraySpanFormattable(chars, length);
|
return new PooledArraySpanFormattable(chars, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[InterpolatedStringHandler]
|
||||||
|
public ref struct LocalizationInterpolationHandler
|
||||||
|
{
|
||||||
|
private static string[] _empty = Array.Empty<string>();
|
||||||
|
|
||||||
|
private char[]? _arrayToReturnToPool;
|
||||||
|
private Span<char> _chars;
|
||||||
|
private int _pos;
|
||||||
|
|
||||||
|
private int _index;
|
||||||
|
private string?[] _slices;
|
||||||
|
private string? _current;
|
||||||
|
private string _lang;
|
||||||
|
|
||||||
|
public LocalizationInterpolationHandler(int literalLength, int formattedCount, LocalizationEntry entry, out bool isValid)
|
||||||
|
{
|
||||||
|
_slices = entry.TextSlices;
|
||||||
|
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(256);
|
||||||
|
isValid = true;
|
||||||
|
|
||||||
|
_pos = 0;
|
||||||
|
_index = 0;
|
||||||
|
_current = null;
|
||||||
|
_lang = entry.Language;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, string lang, out bool isValid)
|
||||||
|
{
|
||||||
|
if (Localization.TryGetLocalization(lang, number, out var entry))
|
||||||
|
{
|
||||||
|
_slices = entry.TextSlices;
|
||||||
|
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(256);
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_slices = _empty;
|
||||||
|
_chars = _arrayToReturnToPool = default;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos = 0;
|
||||||
|
_index = 0;
|
||||||
|
_current = null;
|
||||||
|
_lang = lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool MoveNext()
|
||||||
|
{
|
||||||
|
if ((uint)_index >= (uint)_slices.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_current = _slices[_index++];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool ReadyToAppend()
|
||||||
|
{
|
||||||
|
if (!MoveNext())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_current == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendStringDirect(_current);
|
||||||
|
return MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendLiteral(string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendStringDirect(string value)
|
||||||
|
{
|
||||||
|
if (value.TryCopyTo(_chars[_pos..]))
|
||||||
|
{
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GrowThenCopyString(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted<T>(T value)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? s;
|
||||||
|
if (value is IFormattable)
|
||||||
|
{
|
||||||
|
// If the value can format itself directly into our buffer, do so.
|
||||||
|
if (value is ISpanFormattable)
|
||||||
|
{
|
||||||
|
int charsWritten;
|
||||||
|
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, default)) // constrained call avoiding boxing for value types
|
||||||
|
{
|
||||||
|
Grow();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos += charsWritten;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = ((IFormattable)value).ToString(format: null, default); // constrained call avoiding boxing for value types
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s = value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s is not null)
|
||||||
|
{
|
||||||
|
AppendStringDirect(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted<T>(T value, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? s;
|
||||||
|
if (value is IFormattable)
|
||||||
|
{
|
||||||
|
// If the value can format itself directly into our buffer, do so.
|
||||||
|
if (value is ISpanFormattable)
|
||||||
|
{
|
||||||
|
int charsWritten;
|
||||||
|
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, default)) // constrained call avoiding boxing for value types
|
||||||
|
{
|
||||||
|
Grow();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos += charsWritten;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = ((IFormattable)value).ToString(format, default); // constrained call avoiding boxing for value types
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s = value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s is not null)
|
||||||
|
{
|
||||||
|
AppendStringDirect(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted<T>(T value, int alignment)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
AppendFormatted(value);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted<T>(T value, int alignment, string? format)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingPos = _pos;
|
||||||
|
AppendFormatted(value, format);
|
||||||
|
if (alignment != 0)
|
||||||
|
{
|
||||||
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.TryCopyTo(_chars[_pos..]))
|
||||||
|
{
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GrowThenCopySpan(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format = null)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftAlign = false;
|
||||||
|
if (alignment < 0)
|
||||||
|
{
|
||||||
|
leftAlign = true;
|
||||||
|
alignment = -alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paddingRequired = alignment - value.Length;
|
||||||
|
if (paddingRequired <= 0)
|
||||||
|
{
|
||||||
|
// The value is as large or larger than the required amount of padding,
|
||||||
|
// so just write the value.
|
||||||
|
AppendFormatted(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the value along with the appropriate padding.
|
||||||
|
EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
|
||||||
|
if (leftAlign)
|
||||||
|
{
|
||||||
|
value.CopyTo(_chars[_pos..]);
|
||||||
|
_pos += value.Length;
|
||||||
|
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
||||||
|
_pos += paddingRequired;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
||||||
|
_pos += paddingRequired;
|
||||||
|
value.CopyTo(_chars[_pos..]);
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
|
||||||
|
AppendFormatted<object?>(value, alignment, format);
|
||||||
|
|
||||||
|
public void AppendFormatted(string? value)
|
||||||
|
{
|
||||||
|
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value?.TryCopyTo(_chars[_pos..]) == true)
|
||||||
|
{
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedSlow(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AppendFormatted(string? value, int alignment, string? format = null) =>
|
||||||
|
AppendFormatted<string?>(value, alignment, format);
|
||||||
|
|
||||||
|
private bool TryAppendClilocNumber(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
value[0] != '#' ||
|
||||||
|
!int.TryParse(value[1..], out var number) ||
|
||||||
|
!Localization.TryGetLocalization(_lang, number, out var entry)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = entry.Text;
|
||||||
|
|
||||||
|
if (text.TryCopyTo(_chars[_pos..]))
|
||||||
|
{
|
||||||
|
_pos += text.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendFormattedSlow(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
||||||
|
{
|
||||||
|
var charsWritten = _pos - startingPos;
|
||||||
|
|
||||||
|
var leftAlign = false;
|
||||||
|
if (alignment < 0)
|
||||||
|
{
|
||||||
|
leftAlign = true;
|
||||||
|
alignment = -alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paddingNeeded = alignment - charsWritten;
|
||||||
|
if (paddingNeeded > 0)
|
||||||
|
{
|
||||||
|
EnsureCapacityForAdditionalChars(paddingNeeded);
|
||||||
|
|
||||||
|
if (leftAlign)
|
||||||
|
{
|
||||||
|
_chars.Slice(_pos, paddingNeeded).Fill(' ');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_chars.Slice(startingPos, charsWritten).CopyTo(_chars[(startingPos + paddingNeeded)..]);
|
||||||
|
_chars.Slice(startingPos, paddingNeeded).Fill(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos += paddingNeeded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void AppendFormattedSlow(string? value)
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
EnsureCapacityForAdditionalChars(value.Length);
|
||||||
|
value.CopyTo(_chars[_pos..]);
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void EnsureCapacityForAdditionalChars(int additionalChars)
|
||||||
|
{
|
||||||
|
if (_chars.Length - _pos < additionalChars)
|
||||||
|
{
|
||||||
|
Grow(additionalChars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void GrowThenCopyString(string value)
|
||||||
|
{
|
||||||
|
Grow(value.Length);
|
||||||
|
value.CopyTo(_chars[_pos..]);
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void GrowThenCopySpan(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
Grow(value.Length);
|
||||||
|
value.CopyTo(_chars[_pos..]);
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
||||||
|
private void Grow(int additionalChars)
|
||||||
|
{
|
||||||
|
GrowCore((uint)_pos + (uint)additionalChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
||||||
|
private void Grow()
|
||||||
|
{
|
||||||
|
GrowCore((uint)_chars.Length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void GrowCore(uint requiredMinCapacity)
|
||||||
|
{
|
||||||
|
var newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, 1073741823));
|
||||||
|
var arraySize = (int)Math.Clamp(newCapacity, 256, int.MaxValue);
|
||||||
|
|
||||||
|
var newArray = ArrayPool<char>.Shared.Rent(arraySize);
|
||||||
|
_chars[.._pos].CopyTo(newArray);
|
||||||
|
|
||||||
|
var toReturn = _arrayToReturnToPool;
|
||||||
|
_chars = _arrayToReturnToPool = newArray;
|
||||||
|
|
||||||
|
if (toReturn is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ReadOnlySpan<char> Text => _chars[.._pos];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
internal void Clear()
|
||||||
|
{
|
||||||
|
var toReturn = _arrayToReturnToPool;
|
||||||
|
this = default; // defensive clear
|
||||||
|
if (toReturn is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToStringAndClear()
|
||||||
|
{
|
||||||
|
if (MoveNext() && _current != null)
|
||||||
|
{
|
||||||
|
AppendStringDirect(_current);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new string(Text);
|
||||||
|
Clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char[] ToPooledArray(out int length)
|
||||||
|
{
|
||||||
|
if (MoveNext() && _current != null)
|
||||||
|
{
|
||||||
|
AppendStringDirect(_current);
|
||||||
|
}
|
||||||
|
|
||||||
|
length = _pos;
|
||||||
|
return _arrayToReturnToPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,459 +0,0 @@
|
||||||
/*************************************************************************
|
|
||||||
* ModernUO *
|
|
||||||
* Copyright 2019-2022 - ModernUO Development Team *
|
|
||||||
* Email: hi@modernuo.com *
|
|
||||||
* File: LocalizationInterpolationHandler.cs *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
*************************************************************************/
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Server;
|
|
||||||
|
|
||||||
[InterpolatedStringHandler]
|
|
||||||
public ref struct LocalizationInterpolationHandler
|
|
||||||
{
|
|
||||||
private static string[] _empty = Array.Empty<string>();
|
|
||||||
|
|
||||||
private char[]? _arrayToReturnToPool;
|
|
||||||
private Span<char> _chars;
|
|
||||||
private int _pos;
|
|
||||||
|
|
||||||
private int _index;
|
|
||||||
private string?[] _slices;
|
|
||||||
private string? _current;
|
|
||||||
private string _lang;
|
|
||||||
|
|
||||||
public LocalizationInterpolationHandler(int literalLength, int formattedCount, LocalizationEntry entry, out bool isValid)
|
|
||||||
{
|
|
||||||
_slices = entry.TextSlices;
|
|
||||||
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(256);
|
|
||||||
isValid = true;
|
|
||||||
|
|
||||||
_pos = 0;
|
|
||||||
_index = 0;
|
|
||||||
_current = null;
|
|
||||||
_lang = entry.Language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, out bool isValid)
|
|
||||||
: this(literalLength, formattedCount, number, Localization.FallbackLanguage, out isValid)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, string lang, out bool isValid)
|
|
||||||
{
|
|
||||||
if (Localization.TryGetLocalization(lang, number, out var entry))
|
|
||||||
{
|
|
||||||
_slices = entry.TextSlices;
|
|
||||||
_chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(256);
|
|
||||||
isValid = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_slices = _empty;
|
|
||||||
_chars = _arrayToReturnToPool = default;
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pos = 0;
|
|
||||||
_index = 0;
|
|
||||||
_current = null;
|
|
||||||
_lang = lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private bool MoveNext()
|
|
||||||
{
|
|
||||||
if ((uint)_index >= (uint)_slices.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_current = _slices[_index++];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private bool ReadyToAppend()
|
|
||||||
{
|
|
||||||
if (!MoveNext())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_current == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendStringDirect(_current);
|
|
||||||
return MoveNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendLiteral(string value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendStringDirect(string value)
|
|
||||||
{
|
|
||||||
if (value.TryCopyTo(_chars[_pos..]))
|
|
||||||
{
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GrowThenCopyString(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? s;
|
|
||||||
if (value is IFormattable)
|
|
||||||
{
|
|
||||||
// If the value can format itself directly into our buffer, do so.
|
|
||||||
if (value is ISpanFormattable)
|
|
||||||
{
|
|
||||||
int charsWritten;
|
|
||||||
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, default)) // constrained call avoiding boxing for value types
|
|
||||||
{
|
|
||||||
Grow();
|
|
||||||
}
|
|
||||||
|
|
||||||
_pos += charsWritten;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
s = ((IFormattable)value).ToString(format: null, default); // constrained call avoiding boxing for value types
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
s = value?.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s is not null)
|
|
||||||
{
|
|
||||||
AppendStringDirect(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value, string? format)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? s;
|
|
||||||
if (value is IFormattable)
|
|
||||||
{
|
|
||||||
// If the value can format itself directly into our buffer, do so.
|
|
||||||
if (value is ISpanFormattable)
|
|
||||||
{
|
|
||||||
int charsWritten;
|
|
||||||
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, default)) // constrained call avoiding boxing for value types
|
|
||||||
{
|
|
||||||
Grow();
|
|
||||||
}
|
|
||||||
|
|
||||||
_pos += charsWritten;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
s = ((IFormattable)value).ToString(format, default); // constrained call avoiding boxing for value types
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
s = value?.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s is not null)
|
|
||||||
{
|
|
||||||
AppendStringDirect(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value, int alignment)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startingPos = _pos;
|
|
||||||
AppendFormatted(value);
|
|
||||||
if (alignment != 0)
|
|
||||||
{
|
|
||||||
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted<T>(T value, int alignment, string? format)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startingPos = _pos;
|
|
||||||
AppendFormatted(value, format);
|
|
||||||
if (alignment != 0)
|
|
||||||
{
|
|
||||||
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted(ReadOnlySpan<char> value)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.TryCopyTo(_chars[_pos..]))
|
|
||||||
{
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GrowThenCopySpan(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format = null)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var leftAlign = false;
|
|
||||||
if (alignment < 0)
|
|
||||||
{
|
|
||||||
leftAlign = true;
|
|
||||||
alignment = -alignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
var paddingRequired = alignment - value.Length;
|
|
||||||
if (paddingRequired <= 0)
|
|
||||||
{
|
|
||||||
// The value is as large or larger than the required amount of padding,
|
|
||||||
// so just write the value.
|
|
||||||
AppendFormatted(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the value along with the appropriate padding.
|
|
||||||
EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
|
|
||||||
if (leftAlign)
|
|
||||||
{
|
|
||||||
value.CopyTo(_chars[_pos..]);
|
|
||||||
_pos += value.Length;
|
|
||||||
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
|
||||||
_pos += paddingRequired;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
|
||||||
_pos += paddingRequired;
|
|
||||||
value.CopyTo(_chars[_pos..]);
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
|
|
||||||
AppendFormatted<object?>(value, alignment, format);
|
|
||||||
|
|
||||||
public void AppendFormatted(string? value)
|
|
||||||
{
|
|
||||||
if (!ReadyToAppend() || TryAppendClilocNumber(value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value?.TryCopyTo(_chars[_pos..]) == true)
|
|
||||||
{
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AppendFormattedSlow(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AppendFormatted(string? value, int alignment, string? format = null) =>
|
|
||||||
AppendFormatted<string?>(value, alignment, format);
|
|
||||||
|
|
||||||
private bool TryAppendClilocNumber(ReadOnlySpan<char> value)
|
|
||||||
{
|
|
||||||
if (
|
|
||||||
value[0] != '#' ||
|
|
||||||
!int.TryParse(value[1..], out var number) ||
|
|
||||||
!Localization.TryGetLocalization(_lang, number, out var entry)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = entry.Text;
|
|
||||||
|
|
||||||
if (text.TryCopyTo(_chars[_pos..]))
|
|
||||||
{
|
|
||||||
_pos += text.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AppendFormattedSlow(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
|
||||||
{
|
|
||||||
var charsWritten = _pos - startingPos;
|
|
||||||
|
|
||||||
var leftAlign = false;
|
|
||||||
if (alignment < 0)
|
|
||||||
{
|
|
||||||
leftAlign = true;
|
|
||||||
alignment = -alignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
var paddingNeeded = alignment - charsWritten;
|
|
||||||
if (paddingNeeded > 0)
|
|
||||||
{
|
|
||||||
EnsureCapacityForAdditionalChars(paddingNeeded);
|
|
||||||
|
|
||||||
if (leftAlign)
|
|
||||||
{
|
|
||||||
_chars.Slice(_pos, paddingNeeded).Fill(' ');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_chars.Slice(startingPos, charsWritten).CopyTo(_chars[(startingPos + paddingNeeded)..]);
|
|
||||||
_chars.Slice(startingPos, paddingNeeded).Fill(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
_pos += paddingNeeded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private void AppendFormattedSlow(string? value)
|
|
||||||
{
|
|
||||||
if (value is not null)
|
|
||||||
{
|
|
||||||
EnsureCapacityForAdditionalChars(value.Length);
|
|
||||||
value.CopyTo(_chars[_pos..]);
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void EnsureCapacityForAdditionalChars(int additionalChars)
|
|
||||||
{
|
|
||||||
if (_chars.Length - _pos < additionalChars)
|
|
||||||
{
|
|
||||||
Grow(additionalChars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private void GrowThenCopyString(string value)
|
|
||||||
{
|
|
||||||
Grow(value.Length);
|
|
||||||
value.CopyTo(_chars[_pos..]);
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private void GrowThenCopySpan(ReadOnlySpan<char> value)
|
|
||||||
{
|
|
||||||
Grow(value.Length);
|
|
||||||
value.CopyTo(_chars[_pos..]);
|
|
||||||
_pos += value.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
|
||||||
private void Grow(int additionalChars)
|
|
||||||
{
|
|
||||||
GrowCore((uint)_pos + (uint)additionalChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
|
||||||
private void Grow()
|
|
||||||
{
|
|
||||||
GrowCore((uint)_chars.Length + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void GrowCore(uint requiredMinCapacity)
|
|
||||||
{
|
|
||||||
var newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, 1073741823));
|
|
||||||
var arraySize = (int)Math.Clamp(newCapacity, 256, int.MaxValue);
|
|
||||||
|
|
||||||
var newArray = ArrayPool<char>.Shared.Rent(arraySize);
|
|
||||||
_chars[.._pos].CopyTo(newArray);
|
|
||||||
|
|
||||||
var toReturn = _arrayToReturnToPool;
|
|
||||||
_chars = _arrayToReturnToPool = newArray;
|
|
||||||
|
|
||||||
if (toReturn is not null)
|
|
||||||
{
|
|
||||||
ArrayPool<char>.Shared.Return(toReturn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal ReadOnlySpan<char> Text => _chars[.._pos];
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
internal void Clear()
|
|
||||||
{
|
|
||||||
var toReturn = _arrayToReturnToPool;
|
|
||||||
this = default; // defensive clear
|
|
||||||
if (toReturn is not null)
|
|
||||||
{
|
|
||||||
ArrayPool<char>.Shared.Return(toReturn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToStringAndClear()
|
|
||||||
{
|
|
||||||
if (MoveNext() && _current != null)
|
|
||||||
{
|
|
||||||
AppendStringDirect(_current);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new string(Text);
|
|
||||||
Clear();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public char[] ToPooledArray(out int length)
|
|
||||||
{
|
|
||||||
if (MoveNext() && _current != null)
|
|
||||||
{
|
|
||||||
AppendStringDirect(_current);
|
|
||||||
}
|
|
||||||
|
|
||||||
length = _pos;
|
|
||||||
return _arrayToReturnToPool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue