diff --git a/cleanrepo/Program.cs b/cleanrepo/Program.cs index 9e5e40f0..c14e6633 100644 --- a/cleanrepo/Program.cs +++ b/cleanrepo/Program.cs @@ -49,9 +49,76 @@ static async Task Main(string[] args) builder.Configuration.GetSection(nameof(Options)) .Bind(options); + options.ExcludeOrphanedIncludeFiles = ParseStringListOption( + builder.Configuration, + nameof(Options.ExcludeOrphanedIncludeFiles), + options.ExcludeOrphanedIncludeFiles); + await RunOptions(options); } + private static List? ParseStringListOption( + IConfigurationRoot configuration, + string optionName, + List? currentValue) + { + string key = $"{nameof(Options)}:{optionName}"; + + foreach (IConfigurationProvider provider in configuration.Providers.Reverse()) + { + if (provider.TryGet(key, out string? rawValue)) + { + return ParseStringListValue(rawValue); + } + + List children = [.. provider.GetChildKeys([], key)]; + if (children.Count == 0) + continue; + + List values = [.. children + .OrderBy(child => int.TryParse(child, out int index) ? index : int.MaxValue) + .ThenBy(child => child, StringComparer.InvariantCultureIgnoreCase) + .Select(child => + { + provider.TryGet($"{key}:{child}", out string? value); + return value?.Trim(); + }) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => value!.Trim('"')) + .Distinct(StringComparer.InvariantCultureIgnoreCase)]; + + return values; + } + + return currentValue; + } + + private static List ParseStringListValue(string? rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + return []; + + rawValue = rawValue.Trim(); + + if (rawValue.StartsWith('[') && rawValue.EndsWith(']')) + { + try + { + return JsonSerializer.Deserialize>(rawValue) ?? []; + } + catch (JsonException ex) + { + throw new ArgumentException($"Option must be a valid JSON array of strings.", ex); + } + } + + return [.. rawValue + .Split([',', ';', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(value => value.Trim('"')) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.InvariantCultureIgnoreCase)]; + } + static async Task RunOptions(Options options) { if (string.IsNullOrEmpty(options.Function)) @@ -237,7 +304,7 @@ static async Task RunOptions(Options options) $"in the '{options.TargetDirectory}' directory...\n"); // Extract hash keys from the dictionary. - List mediaFilesList = docFxRepo._imageRefs.Keys.ToList(); + List mediaFilesList = [.. docFxRepo._imageRefs.Keys]; if (mediaFilesList.Count == 0) { diff --git a/cleanrepo/action.yml b/cleanrepo/action.yml index ee2826e8..6888a3a3 100644 --- a/cleanrepo/action.yml +++ b/cleanrepo/action.yml @@ -7,7 +7,7 @@ branding: color: 'purple' inputs: function: - description: 'The function you want to run: FindOrphanedArticles, FindOrphanedImages, FindOrphanedIncludes, FindOrphanedSnippets, CatalogImages, CatalogImagesWithText, FilterImagesForText, ReplaceRedirectTargets, ReplaceWithRelativeLinks, or RemoveRedirectHops.' + description: 'The function you want to run: FindOrphanedArticles, FindOrphanedImages, FindOrphanedIncludes, FindOrphanedSnippets, ReplaceRedirectTargets, ReplaceWithRelativeLinks, or RemoveRedirectHops.' required: true docfx_directory: description: 'The directory that contains the docfx.json file for the docset.' @@ -18,6 +18,9 @@ inputs: url_base_path: description: 'The URL base path for your docset, for example, /dotnet or /windows/uwp.' required: true + exclude_include_files: + description: 'A list of file names or paths that should never be considered orphaned.' + default: '[]' runs: using: docker image: ../cleanrepo.Dockerfile @@ -30,3 +33,5 @@ runs: - ${{ inputs.target_directory }} - '/Options:UrlBasePath' - ${{ inputs.url_base_path }} + - '/Options:ExcludeOrphanedIncludeFiles' + - ${{ inputs.exclude_include_files }}