mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-10 23:27:21 +00:00
Added the BeyondFacts Lab as a prototype (#1175)
This commit is contained in:
1 parent
db8dae1cf6
commit
b6c3fbb5db
90 files changed
+5936
-209
No files matched your search
@@ -0,0 +1,320 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedTools;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a C# string literal from source code and gives the string the compiler makes of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The I18N collector reads the texts of T() and TB() from the source files, and the app looks up their
|
||||
/// translations by a hash of the string it gets at runtime. Both have to agree on every character, so this reader
|
||||
/// follows the compiler: escape sequences of regular literals, doubled quotes of verbatim literals, and the removed
|
||||
/// indentation of multi-line raw literals. Interpolated literals are not read, since their text is known only at
|
||||
/// runtime. A line break in the source stays as it is; I18NKeyHash makes line endings equal.
|
||||
/// </remarks>
|
||||
public static class CSharpStringLiteral
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the literal at the start of the source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source, starting with the first quote of the literal, or with the @ of a verbatim literal.</param>
|
||||
/// <param name="value">The string the compiler makes of the literal.</param>
|
||||
/// <param name="length">The number of characters the literal takes in the source.</param>
|
||||
/// <returns>Whether the source starts with a complete literal this reader understands.</returns>
|
||||
public static bool TryRead(ReadOnlySpan<char> source, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
if (source.IsEmpty)
|
||||
return false;
|
||||
|
||||
if (source[0] == '@')
|
||||
return TryReadVerbatim(source, out value, out length);
|
||||
|
||||
if (source[0] != '"')
|
||||
return false;
|
||||
|
||||
var quotes = CountQuotes(source);
|
||||
if (quotes >= 3)
|
||||
return TryReadRaw(source, quotes, out value, out length);
|
||||
|
||||
if (quotes == 2)
|
||||
{
|
||||
length = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryReadRegular(source, out value, out length);
|
||||
}
|
||||
|
||||
private static bool TryReadRegular(ReadOnlySpan<char> source, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var index = 1; index < source.Length; index++)
|
||||
{
|
||||
var character = source[index];
|
||||
switch (character)
|
||||
{
|
||||
case '"':
|
||||
value = builder.ToString();
|
||||
length = index + 1;
|
||||
return true;
|
||||
|
||||
case '\r' or '\n':
|
||||
return false;
|
||||
|
||||
case '\\':
|
||||
if (!TryReadEscape(source, ref index, builder))
|
||||
return false;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
builder.Append(character);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the escape sequence whose backslash is at the index, and leaves the index at its last character.
|
||||
/// </summary>
|
||||
private static bool TryReadEscape(ReadOnlySpan<char> source, ref int index, StringBuilder builder)
|
||||
{
|
||||
if (++index >= source.Length)
|
||||
return false;
|
||||
|
||||
switch (source[index])
|
||||
{
|
||||
case '\'': builder.Append('\''); return true;
|
||||
case '"': builder.Append('"'); return true;
|
||||
case '\\': builder.Append('\\'); return true;
|
||||
case '0': builder.Append('\0'); return true;
|
||||
case 'a': builder.Append('\a'); return true;
|
||||
case 'b': builder.Append('\b'); return true;
|
||||
case 'e': builder.Append('\u001b'); return true;
|
||||
case 'f': builder.Append('\f'); return true;
|
||||
case 'n': builder.Append('\n'); return true;
|
||||
case 'r': builder.Append('\r'); return true;
|
||||
case 't': builder.Append('\t'); return true;
|
||||
case 'v': builder.Append('\v'); return true;
|
||||
|
||||
case 'u':
|
||||
if (!TryReadHex(source, index + 1, 4, 4, out var utf16, out var utf16Digits))
|
||||
return false;
|
||||
|
||||
builder.Append((char)utf16);
|
||||
index += utf16Digits;
|
||||
return true;
|
||||
|
||||
case 'U':
|
||||
if (!TryReadHex(source, index + 1, 8, 8, out var utf32, out var utf32Digits) || utf32 > 0x10FFFF)
|
||||
return false;
|
||||
|
||||
builder.Append(char.ConvertFromUtf32(utf32));
|
||||
index += utf32Digits;
|
||||
return true;
|
||||
|
||||
case 'x':
|
||||
if (!TryReadHex(source, index + 1, 1, 4, out var variable, out var variableDigits))
|
||||
return false;
|
||||
|
||||
builder.Append((char)variable);
|
||||
index += variableDigits;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadHex(ReadOnlySpan<char> source, int start, int minDigits, int maxDigits, out int value, out int digits)
|
||||
{
|
||||
value = 0;
|
||||
digits = 0;
|
||||
while (digits < maxDigits && start + digits < source.Length && char.IsAsciiHexDigit(source[start + digits]))
|
||||
digits++;
|
||||
|
||||
return digits >= minDigits && int.TryParse(source.Slice(start, digits), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
private static bool TryReadVerbatim(ReadOnlySpan<char> source, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
if (source.Length < 2 || source[1] != '"')
|
||||
return false;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var index = 2; index < source.Length; index++)
|
||||
{
|
||||
if (source[index] != '"')
|
||||
{
|
||||
builder.Append(source[index]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Two quotes stand for one:
|
||||
if (index + 1 < source.Length && source[index + 1] == '"')
|
||||
{
|
||||
builder.Append('"');
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
value = builder.ToString();
|
||||
length = index + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadRaw(ReadOnlySpan<char> source, int quotes, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
|
||||
// A raw literal spans several lines when nothing but whitespace follows its opening quotes:
|
||||
var afterOpening = quotes;
|
||||
while (afterOpening < source.Length && IsWhitespace(source[afterOpening]))
|
||||
afterOpening++;
|
||||
|
||||
return afterOpening < source.Length && IsLineBreak(source[afterOpening])
|
||||
? TryReadMultiLineRaw(source, quotes, afterOpening, out value, out length)
|
||||
: TryReadSingleLineRaw(source, quotes, out value, out length);
|
||||
}
|
||||
|
||||
private static bool TryReadSingleLineRaw(ReadOnlySpan<char> source, int quotes, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
for (var index = quotes; index < source.Length; index++)
|
||||
{
|
||||
if (IsLineBreak(source[index]))
|
||||
return false;
|
||||
|
||||
if (source[index] != '"')
|
||||
continue;
|
||||
|
||||
var run = CountQuotes(source[index..]);
|
||||
if (run < quotes)
|
||||
{
|
||||
index += run - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (run > quotes || index == quotes)
|
||||
return false;
|
||||
|
||||
value = source[quotes..index].ToString();
|
||||
length = index + quotes;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadMultiLineRaw(ReadOnlySpan<char> source, int quotes, int lineBreakAfterOpening, out string value, out int length)
|
||||
{
|
||||
value = string.Empty;
|
||||
length = 0;
|
||||
|
||||
var lines = new List<(int Start, int End, int LineBreakLength)>();
|
||||
var lineStart = lineBreakAfterOpening + LineBreakLength(source, lineBreakAfterOpening);
|
||||
while (lineStart <= source.Length)
|
||||
{
|
||||
var lineEnd = lineStart;
|
||||
while (lineEnd < source.Length && !IsLineBreak(source[lineEnd]))
|
||||
lineEnd++;
|
||||
|
||||
// Is this the line which closes the literal?
|
||||
var indentationEnd = lineStart;
|
||||
while (indentationEnd < lineEnd && IsWhitespace(source[indentationEnd]))
|
||||
indentationEnd++;
|
||||
|
||||
if (indentationEnd < lineEnd && source[indentationEnd] == '"')
|
||||
{
|
||||
var run = CountQuotes(source[indentationEnd..lineEnd]);
|
||||
if (run >= quotes)
|
||||
{
|
||||
if (run > quotes || lines.Count == 0)
|
||||
return false;
|
||||
|
||||
var indentation = source[lineStart..indentationEnd];
|
||||
if (!TryRemoveIndentation(source, lines, indentation, out value))
|
||||
return false;
|
||||
|
||||
length = indentationEnd + quotes;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (lineEnd >= source.Length)
|
||||
return false;
|
||||
|
||||
var lineBreakLength = LineBreakLength(source, lineEnd);
|
||||
lines.Add((lineStart, lineEnd, lineBreakLength));
|
||||
lineStart = lineEnd + lineBreakLength;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the content lines of a multi-line raw literal without the indentation of its closing line, which every
|
||||
/// line has to start with. A line of whitespace only may be shorter, and becomes empty then.
|
||||
/// </summary>
|
||||
private static bool TryRemoveIndentation(ReadOnlySpan<char> source, List<(int Start, int End, int LineBreakLength)> lines, ReadOnlySpan<char> indentation, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
var builder = new StringBuilder();
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
{
|
||||
var (start, end, lineBreakLength) = lines[index];
|
||||
var line = source[start..end];
|
||||
if (line.StartsWith(indentation, StringComparison.Ordinal))
|
||||
builder.Append(line[indentation.Length..]);
|
||||
else if (!IsWhitespaceOnly(line) || line.Length > indentation.Length)
|
||||
return false;
|
||||
|
||||
// The line break after the last content line belongs to the closing line:
|
||||
if (index < lines.Count - 1)
|
||||
builder.Append(source.Slice(end, lineBreakLength));
|
||||
}
|
||||
|
||||
value = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CountQuotes(ReadOnlySpan<char> source)
|
||||
{
|
||||
var count = 0;
|
||||
while (count < source.Length && source[count] == '"')
|
||||
count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int LineBreakLength(ReadOnlySpan<char> source, int index) =>
|
||||
source[index] == '\r' && index + 1 < source.Length && source[index + 1] == '\n' ? 2 : 1;
|
||||
|
||||
private static bool IsLineBreak(char character) => character is '\r' or '\n' or '\u0085' or '\u2028' or '\u2029';
|
||||
|
||||
private static bool IsWhitespace(char character) => character is not ('\r' or '\n' or '\u0085' or '\u2028' or '\u2029') && char.IsWhiteSpace(character);
|
||||
|
||||
private static bool IsWhitespaceOnly(ReadOnlySpan<char> line)
|
||||
{
|
||||
foreach (var character in line)
|
||||
if (!IsWhitespace(character))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user