Files
meeting-assistant/MeetingAssistant/Workflow/WorkflowRulesEditorMarkdownLinkResolver.cs
T
codex e9825f5bf5
PR and Push Build/Test / build-and-test (push) Successful in 8m13s
Improve settings logs agent and renderer
2026-06-02 00:09:03 +02:00

181 lines
5.4 KiB
C#

namespace MeetingAssistant.Workflow;
internal sealed class WorkflowRulesEditorMarkdownLinkResolver
{
private static readonly string[] ImageUriSchemes = ["http", "https", "file"];
private readonly IReadOnlyList<string> rootPaths;
private readonly IReadOnlyList<string> recursiveRootPaths;
private readonly Dictionary<string, string> resolvedFiles = new(StringComparer.OrdinalIgnoreCase);
public WorkflowRulesEditorMarkdownLinkResolver(
IEnumerable<string> rootPaths,
IEnumerable<string>? recursiveRootPaths = null)
{
this.rootPaths = rootPaths
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => Path.GetFullPath(path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
this.recursiveRootPaths = (recursiveRootPaths ?? [])
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(path => Path.GetFullPath(path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
public static WorkflowRulesEditorMarkdownLinkResolver Empty { get; } = new([]);
public static WorkflowRulesEditorMarkdownLinkResolver FromOptions(MeetingAssistantOptions options)
{
var artifactRoots = new[]
{
VaultPath.Resolve(options.Vault, options.Vault.AssistantContextFolder),
VaultPath.Resolve(options.Vault, options.Vault.MeetingNotesFolder),
VaultPath.Resolve(options.Vault, options.Vault.SummariesFolder),
VaultPath.Resolve(options.Vault, options.Vault.TranscriptsFolder),
VaultPath.Resolve(options.Vault, options.Vault.ProjectsFolder)
};
return new WorkflowRulesEditorMarkdownLinkResolver([
..artifactRoots,
Environment.CurrentDirectory,
AppContext.BaseDirectory
], artifactRoots);
}
public bool TryResolveImageUri(string? target, out Uri uri)
{
uri = null!;
if (string.IsNullOrWhiteSpace(target))
{
return false;
}
if (Uri.TryCreate(target, UriKind.Absolute, out uri!) &&
ImageUriSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase))
{
return true;
}
if (TryResolveExistingFilePath(target, out var path))
{
uri = new Uri(path);
return true;
}
return false;
}
public string ResolveOpenTarget(string? target)
{
if (string.IsNullOrWhiteSpace(target))
{
return "";
}
if (Uri.TryCreate(target, UriKind.Absolute, out var uri))
{
return uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase)
? uri.LocalPath
: uri.ToString();
}
return TryResolveExistingFilePath(target, out var path)
? path
: target;
}
private bool TryResolveExistingFilePath(string target, out string path)
{
path = "";
if (Path.IsPathRooted(target) && File.Exists(target))
{
path = Path.GetFullPath(target);
return true;
}
if (resolvedFiles.TryGetValue(target, out var cachedPath))
{
path = cachedPath;
return true;
}
if (TryResolveDirectRelativePath(target, out path) ||
TryFindRelativePathInRoots(target, out path))
{
resolvedFiles[target] = path;
return true;
}
return false;
}
private bool TryResolveDirectRelativePath(string target, out string path)
{
foreach (var root in rootPaths)
{
try
{
var candidate = Path.GetFullPath(target, root);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
}
catch
{
}
}
path = "";
return false;
}
private bool TryFindRelativePathInRoots(string target, out string path)
{
path = "";
var normalizedSuffix = NormalizeRelativePath(target);
if (string.IsNullOrWhiteSpace(normalizedSuffix))
{
return false;
}
var fileName = Path.GetFileName(normalizedSuffix);
if (string.IsNullOrWhiteSpace(fileName))
{
return false;
}
foreach (var root in recursiveRootPaths.Where(Directory.Exists))
{
try
{
var match = Directory
.EnumerateFiles(root, fileName, SearchOption.AllDirectories)
.Where(candidate => NormalizeRelativePath(Path.GetRelativePath(root, candidate))
.EndsWith(normalizedSuffix, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(File.GetLastWriteTimeUtc)
.FirstOrDefault();
if (match is not null)
{
path = match;
return true;
}
}
catch
{
}
}
return false;
}
private static string NormalizeRelativePath(string path)
{
return path
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)
.TrimStart(Path.DirectorySeparatorChar);
}
}