初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
using System.IO;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Configuration;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils.OS;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
public class CustomEvent
|
||||
{
|
||||
public EventType Type { get; set; } = EventType.None;
|
||||
public string Data { get; set; } = string.Empty;
|
||||
|
||||
public CustomEvent() { }
|
||||
|
||||
public CustomEvent(EventType type, string data)
|
||||
{
|
||||
Type = type;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public void Raise() => Raise(Type, Data);
|
||||
|
||||
/// <summary>
|
||||
/// 静态入口:根据 EventType 分发给对应的 action 执行。
|
||||
/// </summary>
|
||||
public static void Raise(EventType type, string arg)
|
||||
{
|
||||
if (type == EventType.None) return;
|
||||
ModBase.Log($"[Control] 执行自定义事件: {type}, {arg}");
|
||||
|
||||
try
|
||||
{
|
||||
if (ActionMap.TryGetValue(type, out var action))
|
||||
action(arg, type);
|
||||
else
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Event.Error.UnknownType", type.ToString()),
|
||||
Lang.Text("Event.Error.Title"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Event.Error.ExecutionFailed", type, arg),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg));
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetCustomVariable(string name, string defaultValue = "") =>
|
||||
States.CustomVariables.GetValueOrDefault(name, defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// 将 arg 按 '|' 分割为参数数组,空串统一返回 [""]。
|
||||
/// </summary>
|
||||
private static string[] SplitArgs(string arg) => arg.Split('|');
|
||||
|
||||
/// <summary>
|
||||
/// 将 \\n 替换为 Windows 换行符 \r\n。
|
||||
/// </summary>
|
||||
private static string FixNewlines(string s) => s.Replace("\\n", "\r\n");
|
||||
|
||||
/// <summary>
|
||||
/// 禁止自定义主页写入的高危设置黑名单
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> SecuritySensitiveSettingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"LaunchAdvanceRun",
|
||||
"VersionAdvanceRun"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EventType → 执行逻辑 的字典映射,O(1) 分发。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<EventType, Action<string, EventType>> ActionMap = new()
|
||||
{
|
||||
[EventType.OpenUrl] = _OpenUrl,
|
||||
[EventType.OpenFile] = _OpenFileOrCommand,
|
||||
[EventType.ExecuteCommand] = _OpenFileOrCommand,
|
||||
[EventType.LaunchGame] = _LaunchGame,
|
||||
[EventType.CopyText] = _CopyText,
|
||||
[EventType.RefreshHomepage] = _Refresh,
|
||||
[EventType.RefreshPage] = _Refresh,
|
||||
[EventType.DailyFortune] = _DailyFortune,
|
||||
[EventType.ClearTrash] = _ClearTrash,
|
||||
[EventType.ShowDialog] = _ShowDialog,
|
||||
[EventType.ShowHint] = _ShowHint,
|
||||
[EventType.SwitchPage] = _SwitchPage,
|
||||
[EventType.ImportModpack] = _ModpackInstall,
|
||||
[EventType.InstallModpack] = _ModpackInstall,
|
||||
[EventType.DownloadFile] = _DownloadFile,
|
||||
[EventType.ModifySetting] = _WriteSetting,
|
||||
[EventType.WriteSetting] = _WriteSetting,
|
||||
[EventType.ModifyVariable] = _WriteVariable,
|
||||
[EventType.WriteVariable] = _WriteVariable,
|
||||
[EventType.OpenHelp] = (_, __) => ModBase.OpenWebsite("https://docs.pclc.cc/ce"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 打开网页。校验 https?:// 前缀,非 file 协议。
|
||||
/// </summary>
|
||||
private static void _OpenUrl(string arg, EventType type)
|
||||
{
|
||||
arg = arg.Replace('\\', '/');
|
||||
if (!arg.Contains("://") || arg.StartsWithF("file", true))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Event.Error.UrlRequired"), Lang.Text("Event.Error.Title"));
|
||||
return;
|
||||
}
|
||||
HintService.Hint(Lang.Text("Event.OpenUrl.Opening", arg));
|
||||
ModBase.RunInThread(() => ModBase.OpenWebsite(arg));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开文件 / 执行命令。复用 GetAbsoluteUrls 解析路径后 ProcessInterop.Start 启动。
|
||||
/// </summary>
|
||||
private static void _OpenFileOrCommand(string arg, EventType type)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
ModBase.RunInThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var urls = GetAbsoluteUrls(args[0], type);
|
||||
if (!EventSafetyConfirm($"{urls[0]}{(args.Length >= 2 ? " " + args[1] : "")}"))
|
||||
return;
|
||||
ProcessInterop.Start(urls[0], args.Length >= 2 ? args[1] : "");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Event.Error.ExecutionFailed", type, arg),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏。支持 \current 指代当前选中实例,可选 ServerIp。
|
||||
/// </summary>
|
||||
private static void _LaunchGame(string arg, EventType type)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
if (args[0] == "\\current")
|
||||
{
|
||||
if (ModInstanceList.McMcInstanceSelected is null)
|
||||
throw new InvalidOperationException(Lang.Text("Event.LaunchGame.SelectVersion"));
|
||||
args[0] = ModInstanceList.McMcInstanceSelected.Name;
|
||||
}
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
var launchOptions = new ModLaunch.McLaunchOptions
|
||||
{
|
||||
ServerIp = args.Length >= 2 ? args[1] : null,
|
||||
instance = new McInstance(args[0])
|
||||
};
|
||||
if (ModLaunch.McLaunchStart(launchOptions))
|
||||
HintService.Hint(Lang.Text("Event.LaunchGame.Starting", args[0]));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制文本到剪贴板。
|
||||
/// </summary>
|
||||
private static void _CopyText(string arg, EventType _) => ModBase.ClipboardSet(arg);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新主页 / 刷新当前页面。要求当前 pageRight 实现 IRefreshable。
|
||||
/// </summary>
|
||||
private static void _Refresh(string arg, EventType _)
|
||||
{
|
||||
if (ModMain.frmMain?.pageRight is IRefreshable refreshable)
|
||||
{
|
||||
ModBase.RunInUiWait(() => refreshable.Refresh());
|
||||
if (string.IsNullOrEmpty(arg))
|
||||
HintService.Hint(Lang.Text("Event.Refresh.Success"), HintType.Success);
|
||||
}
|
||||
else
|
||||
HintService.Hint(Lang.Text("Event.Refresh.NotSupported"), HintType.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 今日人品。
|
||||
/// </summary>
|
||||
private static void _DailyFortune(string _, EventType __) => PageToolsTest.Jrrp();
|
||||
|
||||
/// <summary>
|
||||
/// 清理垃圾。异步执行 RubbishClear。
|
||||
/// </summary>
|
||||
private static void _ClearTrash(string _, EventType __) =>
|
||||
ModBase.RunInThread(PageToolsTest.RubbishClear);
|
||||
|
||||
/// <summary>
|
||||
/// 弹出消息框。参数:Title|Content[|ButtonText]。
|
||||
/// </summary>
|
||||
private static void _ShowDialog(string arg, EventType type)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
if (args.Length == 1)
|
||||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "Title|Content"));
|
||||
ModMain.MyMsgBox(
|
||||
FixNewlines(args[1]),
|
||||
FixNewlines(args[0]),
|
||||
args.Length > 2 ? args[2] : Lang.Text("Common.Action.Confirm"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出提示条。参数:Message[|HintType](HintType = Info / Success / Error,兼容旧版 Finish / Critical)。
|
||||
/// </summary>
|
||||
private static void _ShowHint(string arg, EventType _)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
var hintType = args.Length == 1
|
||||
? HintType.Info
|
||||
: (HintType)Enum.Parse(typeof(HintType), NormalizeHintTypeName(args[1]), true);
|
||||
HintService.Hint(FixNewlines(args[0]), hintType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将旧版 HintType 名称映射到当前名称,保持对旧主页的兼容。
|
||||
/// </summary>
|
||||
private static string NormalizeHintTypeName(string name) => name.ToLowerInvariant() switch
|
||||
{
|
||||
"finish" => nameof(HintType.Success),
|
||||
"critical" => nameof(HintType.Error),
|
||||
_ => name
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 切换页面。参数:PageType[|PageSubType]。
|
||||
/// </summary>
|
||||
private static void _SwitchPage(string arg, EventType _)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
var page = (FormMain.PageType)Enum.Parse(typeof(FormMain.PageType), args[0], true);
|
||||
var sub = args.Length == 1
|
||||
? FormMain.PageSubType.Default
|
||||
: (FormMain.PageSubType)Enum.Parse(typeof(FormMain.PageSubType), args[1], true);
|
||||
ModMain.frmMain?.PageChange(page, sub);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入 / 安装整合包。触发 ModModpack.ModpackInstall()。
|
||||
/// </summary>
|
||||
private static void _ModpackInstall(string _, EventType __) =>
|
||||
ModBase.RunInUi(ModModpack.ModpackInstall);
|
||||
|
||||
/// <summary>
|
||||
/// 下载文件。参数:Url[|SavePath[|FileName]],校验 http/https 前缀并弹安全确认。
|
||||
/// </summary>
|
||||
private static void _DownloadFile(string arg, EventType _)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
args[0] = args[0].Replace('\\', '/');
|
||||
if (!args[0].StartsWithF("http://", true) && !args[0].StartsWithF("https://", true))
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Event.Error.DownloadUrlRequired"), Lang.Text("Event.Error.Title"));
|
||||
return;
|
||||
}
|
||||
if (!EventSafetyConfirm(Lang.Text("Event.Download.Confirm", args[0])))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
PageToolsTest.StartCustomDownload(args[0],
|
||||
args.Length >= 2 ? args[1] : ModBase.GetFileNameFromPath(args[0]),
|
||||
args.Length >= 3 ? args[2] : null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
PageToolsTest.StartCustomDownload(args[0], Lang.Text("Common.State.Unknown"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 / 修改设置。参数:SettingName|Value。
|
||||
/// </summary>
|
||||
private static void _WriteSetting(string arg, EventType type)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
if (args.Length == 1)
|
||||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "SettingName|Value"));
|
||||
if (ConfigService.TryGetConfigItemNoType(args[0], out var item) && item.Source != ConfigSource.SharedEncrypt)
|
||||
{
|
||||
if (!CanWriteSettingFromCustomEvent(args[0]))
|
||||
return;
|
||||
item.SetValueNoType(args[1], ModInstanceList.McMcInstanceSelected?.PathInstance);
|
||||
}
|
||||
if (args.Length == 2)
|
||||
HintService.Hint(Lang.Text("Event.Setting.Written", args[0], args[1]), HintType.Success);
|
||||
}
|
||||
private static bool CanWriteSettingFromCustomEvent(string key)
|
||||
{
|
||||
if (!SecuritySensitiveSettingKeys.Contains(key))
|
||||
return true;
|
||||
ModBase.Log($"[Control] 已阻止自定义事件写入高危设置:{key}", ModBase.LogLevel.Developer);
|
||||
HintService.Hint(Lang.Text("Event.Safety.DangerousSettingBlocked", key), HintType.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 / 修改自定义变量。参数:VariableName|Value。
|
||||
/// </summary>
|
||||
private static void _WriteVariable(string arg, EventType type)
|
||||
{
|
||||
var args = SplitArgs(arg);
|
||||
if (args.Length == 1)
|
||||
throw new ArgumentException(Lang.Text("Event.Error.MissingArgs", type.ToString(), "VariableName|Value"));
|
||||
States.CustomVariables[args[0]] = args[1];
|
||||
States.CustomVariables = States.CustomVariables; // 触发 ConfigPropertyChanged
|
||||
if (args.Length == 2)
|
||||
HintService.Hint(Lang.Text("Event.Variable.Written", args[0], args[1]), HintType.Success);
|
||||
}
|
||||
|
||||
public static string[] GetAbsoluteUrls(string relativeUrl, EventType type)
|
||||
{
|
||||
relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\');
|
||||
var pclDir = Path.Combine(Basics.ExecutableDirectory, "PCL");
|
||||
|
||||
if (relativeUrl.Contains(":\\"))
|
||||
{
|
||||
ModBase.Log($"[Control] 自定义事件中由绝对路径 {type}: {relativeUrl}");
|
||||
return [relativeUrl, pclDir];
|
||||
}
|
||||
if (File.Exists(Path.Combine(pclDir, relativeUrl)))
|
||||
{
|
||||
var fullPath = Path.Combine(pclDir, relativeUrl);
|
||||
var resolved = Path.GetFullPath(fullPath);
|
||||
if (!resolved.StartsWith(pclDir, StringComparison.OrdinalIgnoreCase))
|
||||
throw new UnauthorizedAccessException(Lang.Text("Event.Error.FileNotFound", relativeUrl));
|
||||
ModBase.Log($"[Control] 自定义事件中由相对 PCL 文件夹的路径 {type}: {fullPath}");
|
||||
return [fullPath, pclDir];
|
||||
}
|
||||
if (type is EventType.OpenFile or EventType.ExecuteCommand)
|
||||
{
|
||||
ModBase.Log($"[Control] 自定义事件中直接 {type}: {relativeUrl}");
|
||||
return [relativeUrl, pclDir];
|
||||
}
|
||||
throw new FileNotFoundException(Lang.Text("Event.Error.FileNotFound", relativeUrl), relativeUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全确认对话框。已由用户勾选"不再询问"时跳过。
|
||||
/// </summary>
|
||||
private static bool EventSafetyConfirm(string message)
|
||||
{
|
||||
if (States.Hint.HomepageCommand)
|
||||
return true;
|
||||
|
||||
return ModMain.MyMsgBox(
|
||||
Lang.Text("Event.Safety.ConfirmMessage", message),
|
||||
Lang.Text("Event.Safety.Title"),
|
||||
Lang.Text("Common.Action.Continue"),
|
||||
Lang.Text("Event.Safety.ContinueAlways"),
|
||||
Lang.Text("Common.Action.Cancel")) switch
|
||||
{
|
||||
1 => true,
|
||||
2 => States.Hint.HomepageCommand = true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
[ContentProperty("Events")]
|
||||
public class CustomEventCollection : IEnumerable<CustomEvent>
|
||||
{
|
||||
private readonly List<CustomEvent> _events = new();
|
||||
|
||||
public List<CustomEvent> Events => _events;
|
||||
|
||||
public IEnumerator<CustomEvent> GetEnumerator() => Events.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
public static class CustomEventService
|
||||
{
|
||||
public static readonly DependencyProperty EventsProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"Events",
|
||||
typeof(CustomEventCollection),
|
||||
typeof(CustomEventService),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static void SetEvents(DependencyObject d, CustomEventCollection value) =>
|
||||
d.SetValue(EventsProperty, value);
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static CustomEventCollection GetEvents(DependencyObject d)
|
||||
{
|
||||
if (d.GetValue(EventsProperty) is null)
|
||||
d.SetValue(EventsProperty, new CustomEventCollection());
|
||||
return (CustomEventCollection)d.GetValue(EventsProperty);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty EventTypeProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"EventType",
|
||||
typeof(EventType),
|
||||
typeof(CustomEventService),
|
||||
new PropertyMetadata(EventType.None));
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static void SetEventType(DependencyObject d, EventType value) =>
|
||||
d.SetValue(EventTypeProperty, value);
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static EventType GetEventType(DependencyObject d) =>
|
||||
(EventType)d.GetValue(EventTypeProperty);
|
||||
|
||||
public static readonly DependencyProperty EventDataProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"EventData",
|
||||
typeof(string),
|
||||
typeof(CustomEventService),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static void SetEventData(DependencyObject d, string value) =>
|
||||
d.SetValue(EventDataProperty, value);
|
||||
|
||||
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
|
||||
public static string GetEventData(DependencyObject d) =>
|
||||
(string)d.GetValue(EventDataProperty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace PCL
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
None = 0,
|
||||
OpenUrl,
|
||||
OpenFile,
|
||||
ExecuteCommand,
|
||||
LaunchGame,
|
||||
CopyText,
|
||||
RefreshHomepage,
|
||||
RefreshPage,
|
||||
DailyFortune,
|
||||
ClearTrash,
|
||||
ShowDialog,
|
||||
ShowHint,
|
||||
SwitchPage,
|
||||
ImportModpack,
|
||||
InstallModpack,
|
||||
DownloadFile,
|
||||
ModifySetting,
|
||||
WriteSetting,
|
||||
ModifyVariable,
|
||||
WriteVariable,
|
||||
OpenHelp
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace PCL
|
||||
{
|
||||
public static class EventTypeMapper
|
||||
{
|
||||
private static readonly Dictionary<string, string> ChineseToEnglish = new()
|
||||
{
|
||||
{ "打开网页", "OpenUrl" },
|
||||
{ "打开文件", "OpenFile" },
|
||||
{ "执行命令", "ExecuteCommand" },
|
||||
{ "启动游戏", "LaunchGame" },
|
||||
{ "复制文本", "CopyText" },
|
||||
{ "刷新主页", "RefreshHomepage" },
|
||||
{ "刷新页面", "RefreshPage" },
|
||||
{ "今日人品", "DailyFortune" },
|
||||
{ "清理垃圾", "ClearTrash" },
|
||||
{ "弹出窗口", "ShowDialog" },
|
||||
{ "弹出提示", "ShowHint" },
|
||||
{ "切换页面", "SwitchPage" },
|
||||
{ "导入整合包", "ImportModpack" },
|
||||
{ "安装整合包", "InstallModpack" },
|
||||
{ "下载文件", "DownloadFile" },
|
||||
{ "修改设置", "ModifySetting" },
|
||||
{ "写入设置", "WriteSetting" },
|
||||
{ "修改变量", "ModifyVariable" },
|
||||
{ "写入变量", "WriteVariable" },
|
||||
{ "打开帮助", "OpenHelp" },
|
||||
{ "刷新帮助", "OpenHelp" },
|
||||
};
|
||||
|
||||
public static readonly HashSet<string> UnsupportedTypes = new(StringComparer.Ordinal)
|
||||
{
|
||||
"内存优化", "加入房间", "检查更新"
|
||||
};
|
||||
|
||||
public static bool TryToEnglish(string chineseName, out string englishName) =>
|
||||
ChineseToEnglish.TryGetValue(chineseName, out englishName);
|
||||
|
||||
public static bool IsUnsupportedType(string name) =>
|
||||
UnsupportedTypes.Contains(name);
|
||||
|
||||
public static bool TryParse(string value, out EventType result)
|
||||
{
|
||||
result = EventType.None;
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
if (Enum.TryParse(value, true, out result))
|
||||
return true;
|
||||
|
||||
if (TryToEnglish(value, out var englishName))
|
||||
return Enum.TryParse(englishName, true, out result);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL
|
||||
{
|
||||
public static partial class XamlEventSanitizer
|
||||
{
|
||||
public class SanitizeResult
|
||||
{
|
||||
public string SanitizedXaml { get; set; } = "";
|
||||
public List<string> UnsupportedTypesFound { get; set; } = new();
|
||||
public List<string> UnrecognizedTypes { get; set; } = new();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(local:CustomEventService\.EventType\s*=\s*"")([^""]+)("")")]
|
||||
private static partial Regex EventTypeAttributeRegex();
|
||||
|
||||
[GeneratedRegex(@"(<local:CustomEventService\.EventType\s*>\s*)([^<]+?)(\s*</local:CustomEventService\.EventType\s*>)")]
|
||||
private static partial Regex EventTypePropertyElementRegex();
|
||||
|
||||
[GeneratedRegex(@"(<local:CustomEvent\.Type\s*>\s*)([^<]+?)(\s*</local:CustomEvent\.Type\s*>)")]
|
||||
private static partial Regex CustomEventTypePropertyElementRegex();
|
||||
|
||||
[GeneratedRegex(@"(<local:CustomEvent\s+[^>]*?\bType\s*=\s*"")([^""]+)("")")]
|
||||
private static partial Regex LocalCustomEventTypeAttributeRegex();
|
||||
|
||||
[GeneratedRegex(@"<local:CustomEvent\s+[^>]*?\bType\s*=\s*""([^""]+)""[^>]*/\s*>")]
|
||||
private static partial Regex LocalCustomEventSelfClosingRegex();
|
||||
|
||||
[GeneratedRegex(@"<local:CustomEvent\s+[^>]*?\bType\s*=\s*""([^""]+)""[^>]*>")]
|
||||
private static partial Regex LocalCustomEventOpenTagRegex();
|
||||
|
||||
[GeneratedRegex(@"<Setter\b[^>]*?\bProperty\s*=\s*""local:CustomEventService\.EventType""[^>]*?\bValue\s*=\s*""([^""]+)""[^>]*(?:/\s*>|>\s*</Setter\s*>)")]
|
||||
private static partial Regex SetterEventTypePropertyFirstRegex();
|
||||
|
||||
[GeneratedRegex(@"<Setter\b[^>]*?\bValue\s*=\s*""([^""]+)""[^>]*?\bProperty\s*=\s*""local:CustomEventService\.EventType""[^>]*(?:/\s*>|>\s*</Setter\s*>)")]
|
||||
private static partial Regex SetterEventTypeValueFirstRegex();
|
||||
|
||||
public static SanitizeResult Sanitize(string xaml)
|
||||
{
|
||||
var result = new SanitizeResult();
|
||||
var sanitized = xaml;
|
||||
|
||||
sanitized = EventTypeAttributeRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[2].Value;
|
||||
return ReplaceEventType(match, rawValue, result);
|
||||
});
|
||||
|
||||
sanitized = EventTypePropertyElementRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[2].Value.Trim();
|
||||
return ReplaceEventType(match, rawValue, result);
|
||||
});
|
||||
|
||||
sanitized = CustomEventTypePropertyElementRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[2].Value.Trim();
|
||||
return ReplaceOrRemovePropertyElement(match, rawValue, result);
|
||||
});
|
||||
|
||||
sanitized = LocalCustomEventTypeAttributeRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[2].Value;
|
||||
return ReplaceEventType(match, rawValue, result);
|
||||
});
|
||||
|
||||
sanitized = LocalCustomEventSelfClosingRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[1].Value;
|
||||
return _RemoveBadCustomEventElement(match, rawValue, result);
|
||||
});
|
||||
|
||||
sanitized = LocalCustomEventOpenTagRegex().Replace(sanitized, match =>
|
||||
{
|
||||
var rawValue = match.Groups[1].Value;
|
||||
if (_IsBadType(rawValue, result))
|
||||
{
|
||||
var elementName = "local:CustomEvent";
|
||||
var afterTag = sanitized[(match.Index + match.Length)..];
|
||||
var closeLen = FindMatchingCloseTag(afterTag, elementName);
|
||||
if (closeLen < 0) return match.Value;
|
||||
return afterTag[closeLen..];
|
||||
}
|
||||
return match.Value;
|
||||
});
|
||||
|
||||
sanitized = SetterEventTypePropertyFirstRegex().Replace(sanitized, match =>
|
||||
_ReplaceSetter(match, match.Groups[1].Value, result));
|
||||
|
||||
sanitized = SetterEventTypeValueFirstRegex().Replace(sanitized, match =>
|
||||
_ReplaceSetter(match, match.Groups[1].Value, result));
|
||||
|
||||
RemoveElementsForTypes(result.UnsupportedTypesFound, ref sanitized);
|
||||
RemoveElementsForTypes(result.UnrecognizedTypes, ref sanitized);
|
||||
|
||||
result.UnsupportedTypesFound = result.UnsupportedTypesFound.Distinct().ToList();
|
||||
result.UnrecognizedTypes = result.UnrecognizedTypes.Distinct().ToList();
|
||||
|
||||
result.SanitizedXaml = sanitized;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool _IsBadType(string rawValue, SanitizeResult result)
|
||||
{
|
||||
if (EventTypeMapper.IsUnsupportedType(rawValue))
|
||||
{
|
||||
result.UnsupportedTypesFound.Add(rawValue);
|
||||
return true;
|
||||
}
|
||||
if (!Enum.TryParse<EventType>(rawValue, true, out _)
|
||||
&& !EventTypeMapper.TryToEnglish(rawValue, out _))
|
||||
{
|
||||
result.UnrecognizedTypes.Add(rawValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string _RemoveBadCustomEventElement(
|
||||
Match match, string rawValue, SanitizeResult result)
|
||||
{
|
||||
return _IsBadType(rawValue, result) ? "" : match.Value;
|
||||
}
|
||||
|
||||
private static void RemoveElementsForTypes(List<string> types, ref string sanitized)
|
||||
{
|
||||
var snapshot = types.ToList();
|
||||
foreach (var type in snapshot)
|
||||
sanitized = RemoveElementsWithEventType(sanitized, type, types);
|
||||
}
|
||||
|
||||
private static string ReplaceEventType(Match match, string rawValue, SanitizeResult result)
|
||||
{
|
||||
if (EventTypeMapper.TryToEnglish(rawValue, out var englishName))
|
||||
return match.Groups[1].Value + englishName + match.Groups[3].Value;
|
||||
|
||||
if (Enum.TryParse<EventType>(rawValue, true, out _))
|
||||
return match.Value;
|
||||
|
||||
if (EventTypeMapper.IsUnsupportedType(rawValue))
|
||||
{
|
||||
result.UnsupportedTypesFound.Add(rawValue);
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
result.UnrecognizedTypes.Add(rawValue);
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
private static string ReplaceOrRemovePropertyElement(
|
||||
Match match, string rawValue, SanitizeResult result)
|
||||
{
|
||||
if (_IsBadType(rawValue, result))
|
||||
return "";
|
||||
return ReplaceEventType(match, rawValue, result);
|
||||
}
|
||||
|
||||
private static string _ReplaceSetter(Match match, string rawValue, SanitizeResult result)
|
||||
{
|
||||
if (_IsBadType(rawValue, result))
|
||||
return "";
|
||||
if (EventTypeMapper.TryToEnglish(rawValue, out var englishName))
|
||||
return match.Value.Replace($"Value=\"{rawValue}\"", $"Value=\"{englishName}\"");
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
private static string RemoveElementsWithEventType(string xaml, string eventTypeValue, List<string> trackingList)
|
||||
{
|
||||
var escaped = Regex.Escape(eventTypeValue);
|
||||
|
||||
var selfClosingPattern = $@"<[\w:]+[^>]*\s+local:CustomEventService\.EventType\s*=\s*""{escaped}""[^>]*/\s*>";
|
||||
xaml = Regex.Replace(xaml, selfClosingPattern, match =>
|
||||
{
|
||||
trackingList.Add(eventTypeValue);
|
||||
return "";
|
||||
}, RegexOptions.Compiled);
|
||||
|
||||
var openTagPattern = $@"<[\w:]+[^>]*\s+local:CustomEventService\.EventType\s*=\s*""{escaped}""[^>]*>";
|
||||
xaml = Regex.Replace(xaml, openTagPattern, match =>
|
||||
{
|
||||
var elementName = Regex.Match(match.Value, @"<([\w:]+)").Groups[1].Value;
|
||||
var afterTag = xaml[(match.Index + match.Length)..];
|
||||
var closeLen = FindMatchingCloseTag(afterTag, elementName);
|
||||
if (closeLen < 0) return match.Value;
|
||||
|
||||
trackingList.Add(eventTypeValue);
|
||||
return afterTag[closeLen..];
|
||||
}, RegexOptions.Compiled);
|
||||
|
||||
var propertyElementPattern = $@"<local:CustomEventService\.EventType\s*>\s*{escaped}\s*</local:CustomEventService\.EventType\s*>";
|
||||
xaml = Regex.Replace(xaml, propertyElementPattern, match =>
|
||||
{
|
||||
var beforeMatch = xaml[..match.Index];
|
||||
var lastOpenMatch = Regex.Match(beforeMatch, @"<([\w:]+)[^>]*>\s*$", RegexOptions.RightToLeft);
|
||||
if (!lastOpenMatch.Success) return match.Value;
|
||||
var parentElementName = lastOpenMatch.Groups[1].Value;
|
||||
|
||||
var afterProperty = xaml[(match.Index + match.Length)..];
|
||||
var parentCloseMatch = Regex.Match(afterProperty, $@"</{parentElementName}\s*>");
|
||||
if (!parentCloseMatch.Success) return match.Value;
|
||||
|
||||
trackingList.Add(eventTypeValue);
|
||||
return "";
|
||||
}, RegexOptions.Compiled);
|
||||
|
||||
return xaml;
|
||||
}
|
||||
|
||||
private static int FindMatchingCloseTag(string text, string elementName)
|
||||
{
|
||||
var closePattern = $@"</{elementName}\s*>";
|
||||
var closeMatch = Regex.Match(text, closePattern);
|
||||
return closeMatch.Success ? closeMatch.Index + closeMatch.Length : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user