Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dotnet tool install --global OrchardCoreContrib.PoExtractor
## Usage

```powershell
extractpo <INTPUT_PATH> <OUTPUT_PATH> [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}]
extractpo <INPUT_PATH> <OUTPUT_PATH> [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}] [--liquid-processor-configuration {path to JSON file}]
```

### Description
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OrchardCoreContrib.PoExtractor.Liquid;

public class LiquidProcessorConfiguration
{
public IReadOnlyList<string> InlineTags { get; set; } = [];

public IReadOnlyList<string> BlockTags { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,34 @@ public class LiquidProjectProcessor : IProjectProcessor
/// <summary>
/// Initializes a new instance of the <see cref="LiquidProjectProcessor"/>
/// </summary>
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<string> NormalizeTags(IReadOnlyList<string> tags) => tags?
.Where(static tag => !string.IsNullOrWhiteSpace(tag))
.Distinct(StringComparer.Ordinal)
?? [];

/// <inheritdoc/>
public void Process(string path, string basePath, LocalizableStringCollection localizableStrings)
{
Expand All @@ -44,6 +64,10 @@ public void Process(string path, string basePath, LocalizableStringCollection lo
{
ProcessTemplate(template, liquidVisitor, file);
}
else
{
Console.WriteLine($"Error: {errors}, file: {file}");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger instead?

@safinilnur safinilnur May 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hishamco I'm not sure we shold use logger instead. I don't see the reason why we should do this. Also it will require changes in several files, so maybe, if we will do that, lets do in a separate PR then?

But I can do this, just, please, approve that we want to bring logger in this PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just mean that we don't use logger in the tool right now and the code which introduces a logger will mix with the changes I did (we will get ditry git commit history)

}
}
}

Expand Down
45 changes: 43 additions & 2 deletions src/OrchardCoreContrib.PoExtractor/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using McMaster.Extensions.CommandLineUtils;
using OrchardCore.Modules;
using OrchardCoreContrib.PoExtractor.DotNet;
Expand Down Expand Up @@ -37,6 +38,37 @@ public static void Main(string[] args)
var ignoredProjects = app.Option("-i|--ignore <IGNORED_PROJECTS>", "Ignores extracting PO files from a given project(s).", CommandOptionType.MultipleValue);
var localizers = app.Option("--localizer <LOCALIZERS>", "Specifies the name of the localizer(s) that will be used during the extraction process.", CommandOptionType.MultipleValue);
var single = app.Option("-s|--single <FILE_NAME>", "Specifies the single output file.", CommandOptionType.SingleValue);
var liquidProcessorConfigurationPath = app.Option(
"--liquid-processor-configuration <FILE_NAME>",
"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 <FILE_NAME_OR_HTTPS_URL>",
"A path or web URL with HTTPS scheme to a C# script (.csx) file which can define further " +
Expand Down Expand Up @@ -69,6 +101,9 @@ public static void Main(string[] args)

var projectFiles = new List<string>();
var projectProcessors = new List<IProjectProcessor>();
var liquidProcessorConfiguration = liquidProcessorConfigurationPath.HasValue()
? LoadLiquidProcessorConfiguration(liquidProcessorConfigurationPath.Value())
: new LiquidProcessorConfiguration();

if (language.Value() == Language.CSharp)
{
Expand All @@ -90,15 +125,15 @@ 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)
{
projectProcessors.Add(new RazorProjectProcessor());
}
else if (template.Value() == TemplateEngine.Liquid)
{
projectProcessors.Add(new LiquidProjectProcessor());
projectProcessors.Add(new LiquidProjectProcessor(liquidProcessorConfiguration));
}

if (plugins.Values.Count > 0)
Expand Down Expand Up @@ -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<LiquidProcessorConfiguration>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? new LiquidProcessorConfiguration();
}
64 changes: 64 additions & 0 deletions test/OrchardCoreContrib.PoExtractor.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"), """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
""");

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);
}
}
}
}
Loading