diff --git a/README.md b/README.md index 13109c8..315ca11 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ dotnet tool install --global OrchardCoreContrib.PoExtractor ## Usage ```powershell -extractpo [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}] +extractpo [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}] [--liquid-processor-configuration {path to JSON file}] ``` ### Description @@ -60,6 +60,19 @@ When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are > Console.WriteLine("Imported resource name: {0}", ResourceNames.ShoppingCart); > ``` +- **`--liquid-processor-configuration {path to JSON file}`** + +Specifies the path to a JSON file with `LiquidProcessorConfiguration`, containing `InlineTags` and `BlockTags` arrays used to register custom Liquid tags during parsing. + +Example: + +```json +{ + "InlineTags": ["resources", "link", "script", "style", "yourcustomtag"], + "BlockTags": ["scriptblock", "styleblock"] +} +``` + ## Uninstallation ```powershell diff --git a/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProcessorConfiguration.cs b/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProcessorConfiguration.cs new file mode 100644 index 0000000..2fb02f3 --- /dev/null +++ b/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProcessorConfiguration.cs @@ -0,0 +1,8 @@ +namespace OrchardCoreContrib.PoExtractor.Liquid; + +public class LiquidProcessorConfiguration +{ + public IReadOnlyList InlineTags { get; set; } = []; + + public IReadOnlyList BlockTags { get; set; } = []; +} diff --git a/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs b/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs index ff64820..3f7f25d 100644 --- a/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs +++ b/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs @@ -18,14 +18,34 @@ public class LiquidProjectProcessor : IProjectProcessor /// /// Initializes a new instance of the /// - public LiquidProjectProcessor() + public LiquidProjectProcessor(LiquidProcessorConfiguration configuration = null) { - var liquidViewOptions = Options.Create(new LiquidViewOptions()); + configuration ??= new LiquidProcessorConfiguration(); + + var liquidViewOptions = new LiquidViewOptions(); + liquidViewOptions.LiquidViewParserConfiguration.Add(parser => + { + foreach (var tag in NormalizeTags(configuration.InlineTags)) + { + parser.RegisterParserTag(tag, parser.ArgumentsListParser, static (_, _, _, _) => ValueTask.FromResult(Fluid.Ast.Completion.Normal)); + } + + foreach (var tag in NormalizeTags(configuration.BlockTags)) + { + parser.RegisterParserBlock(tag, parser.ArgumentsListParser, static (_, _, _, _, _) => ValueTask.FromResult(Fluid.Ast.Completion.Normal)); + } + }); + var fileParserOptions = Options.Create(new FluidParserOptions()); - _parser = new LiquidViewParser(liquidViewOptions, fileParserOptions); + _parser = new LiquidViewParser(Options.Create(liquidViewOptions), fileParserOptions); } + private static IEnumerable NormalizeTags(IReadOnlyList tags) => tags? + .Where(static tag => !string.IsNullOrWhiteSpace(tag)) + .Distinct(StringComparer.Ordinal) + ?? []; + /// public void Process(string path, string basePath, LocalizableStringCollection localizableStrings) { @@ -44,6 +64,10 @@ public void Process(string path, string basePath, LocalizableStringCollection lo { ProcessTemplate(template, liquidVisitor, file); } + else + { + Console.WriteLine($"Error: {errors}, file: {file}"); + } } } diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index b5d98d6..f0560e5 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Text.Json; using McMaster.Extensions.CommandLineUtils; using OrchardCore.Modules; using OrchardCoreContrib.PoExtractor.DotNet; @@ -37,6 +38,37 @@ public static void Main(string[] args) var ignoredProjects = app.Option("-i|--ignore ", "Ignores extracting PO files from a given project(s).", CommandOptionType.MultipleValue); var localizers = app.Option("--localizer ", "Specifies the name of the localizer(s) that will be used during the extraction process.", CommandOptionType.MultipleValue); var single = app.Option("-s|--single ", "Specifies the single output file.", CommandOptionType.SingleValue); + var liquidProcessorConfigurationPath = app.Option( + "--liquid-processor-configuration ", + "Specifies a path to a JSON file with LiquidProcessorConfiguration (InlineTags and BlockTags).", + CommandOptionType.SingleValue, + option => option.OnValidate(_ => + { + if (!option.HasValue()) + { + return ValidationResult.Success; + } + + if (!File.Exists(option.Value())) + { + return new ValidationResult("Liquid processor configuration must be an existing local file."); + } + + try + { + LoadLiquidProcessorConfiguration(option.Value()); + + return ValidationResult.Success; + } + catch (JsonException ex) + { + return new ValidationResult($"Liquid processor configuration must be valid JSON: {ex.Message}"); + } + catch (Exception ex) + { + return new ValidationResult($"Liquid processor configuration could not be read: {ex.Message}"); + } + })); var plugins = app.Option( "-p|--plugin ", "A path or web URL with HTTPS scheme to a C# script (.csx) file which can define further " + @@ -69,6 +101,9 @@ public static void Main(string[] args) var projectFiles = new List(); var projectProcessors = new List(); + var liquidProcessorConfiguration = liquidProcessorConfigurationPath.HasValue() + ? LoadLiquidProcessorConfiguration(liquidProcessorConfigurationPath.Value()) + : new LiquidProcessorConfiguration(); if (language.Value() == Language.CSharp) { @@ -90,7 +125,7 @@ public static void Main(string[] args) if (template.Value() == TemplateEngine.Both) { projectProcessors.Add(new RazorProjectProcessor()); - projectProcessors.Add(new LiquidProjectProcessor()); + projectProcessors.Add(new LiquidProjectProcessor(liquidProcessorConfiguration)); } else if (template.Value() == TemplateEngine.Razor) { @@ -98,7 +133,7 @@ public static void Main(string[] args) } else if (template.Value() == TemplateEngine.Liquid) { - projectProcessors.Add(new LiquidProjectProcessor()); + projectProcessors.Add(new LiquidProjectProcessor(liquidProcessorConfiguration)); } if (plugins.Values.Count > 0) @@ -161,4 +196,10 @@ public static void Main(string[] args) app.Execute(args); } + + private static LiquidProcessorConfiguration LoadLiquidProcessorConfiguration(string path) => + string.IsNullOrWhiteSpace(path) || !File.Exists(path) + ? new LiquidProcessorConfiguration() + : JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? new LiquidProcessorConfiguration(); } diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/ProgramTests.cs b/test/OrchardCoreContrib.PoExtractor.Tests/ProgramTests.cs index 83b7d05..dcce89e 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/ProgramTests.cs +++ b/test/OrchardCoreContrib.PoExtractor.Tests/ProgramTests.cs @@ -60,4 +60,68 @@ public void Main_NoTemplateOption_UsesBothRazorAndLiquid() } } } + + [Fact] + public void Main_LiquidProcessorConfigurationOption_LoadsLiquidTagsFromJsonFile() + { + // Arrange + var root = Path.Combine(Path.GetTempPath(), "PoExtractorTests", Guid.NewGuid().ToString("N")); + var input = Path.Combine(root, "input"); + var output = Path.Combine(root, "output"); + var project = Path.Combine(input, "TestModule"); + var configurationPath = Path.Combine(root, "liquid-config.json"); + + Directory.CreateDirectory(project); + Directory.CreateDirectory(output); + + File.WriteAllText(Path.Combine(project, "TestModule.csproj"), """ + + + net10.0 + + +"""); + + File.WriteAllText(Path.Combine(project, "index.liquid"), """ +{% scriptblock at:"FootScript" %} +{{ "Hello from Liquid configured tag" | t }} +{% endscriptblock %} +"""); + + File.WriteAllText(configurationPath, """ +{ + "InlineTags": [], + "BlockTags": ["scriptblock"] +} +"""); + + var potFileName = "liquid-config.pot"; + + try + { + // Act + Program.Main( + [ + input, + output, + "--template", "Liquid", + "--liquid-processor-configuration", configurationPath, + "--single", potFileName + ]); + + // Assert + var potPath = Path.Combine(output, potFileName); + Assert.True(File.Exists(potPath)); + + var pot = File.ReadAllText(potPath); + Assert.Contains("Hello from Liquid configured tag", pot); + } + finally + { + if (Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + } }