Skip to content

Commit

Permalink
feat: Fluid template engine, -e to specify engine and -x for managing…
Browse files Browse the repository at this point in the history
… template files' extension associations (#29)
  • Loading branch information
Seddryck authored Oct 24, 2024
1 parent 6a0ac63 commit ac087bc
Show file tree
Hide file tree
Showing 18 changed files with 477 additions and 28 deletions.
4 changes: 3 additions & 1 deletion docs/_data/navigation_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
- title: Usage
docs:
- supported-format-engine
- cli-options
- basic-usage
- other-options
- use-stdin-stdout
- specify-engine-parser
104 changes: 104 additions & 0 deletions docs/_docs/cli-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: CLI options
tags: [quick-start, cli-usage]
---
## General format for options

### Shortcut Option (Single Character)

Shortcut options are prefixed with a single hyphen (-) and can be followed directly by the value without a space, though a space can be used if preferred.
**Example:**

- Without space: `-tpath/to/template`
- With space: `-t path/to/template`

### Long Version Option (Double Dash)

Long options are prefixed with double hyphens (--), the first letter of the option is uppercase, and the option name is followed by an equal sign (=) to assign its value.
**Example:** `--Template=path/to/template`

## Option Values

### Single Value

An option can take a single value, which is provided right after the shortcut or the `=` sign for long options.
**Example**: `-s path/to/source` or `--Source=path/to/source`

### Switch Values

If an option accepts a boolean, this one can be omitted.
**Example**: `-i` or `--StdIn` or `--StdIn=true`

### Multiple Values

If an option accepts multiple values, the values should be separated by a semicolon (;).
**Example**: `--Sources=file1.yaml;file2.json;file3.xml`

### Key-Value Pairs

For key-value pairs, the key is followed by a colon (:), and then the value. Multiple key-value pairs, if allowed, should be separated by a semicolon (;).
**Example**: `--Extensions=txt:handlebars;liquid:fluid`

## Didot Options Explained

### Template Option

Shortcut: `-t`
Long: `--Template`
Description: Specifies the path to the template file.
Accept: single value.
Mandatory: yes.
Example: `-tpath/to/template` or `--Template=path/to/template`

### Engine option

Shortcut: -e
Long: --Engine
Description: Specifies the template engine to use (scriban, fluid, dotliquid, handlebars, smartformat, stringtemplate).
Accept: single value. When omitted Didot will select the engine based on the extension of the template file.
Example: `-efluid` or `--Engine=fluid`

### Source Option

Shortcut: `-s`
Long: `--Source`
Description: Specifies the path to the source file. If omitted, input can be taken from StdIn.
Accept: single value.
Exclusive: can't be set with the parameter `--StdIn`
Example: `-spath/to/source` or `--Source=path/to/source`

### Parser Option

Shortcut: -p
Long: --Parser
Description: Specifies the parser to use (YAML, JSON, XML).
Accept: single value.
Mandatory: no expect if `--StdIn` is specified. When omitted Didot will select the parser based on the extension of the source file
Example: `-pYAML` or `--Parser=YAML`

### StdIn Option

Shortcut: `-i`
Long: --StdIn
Description: Specifies the input to the source data as coming from the StdIn.
Accept: switch value.
Exclusive: can't be set with the parameter `--Source`
Example: `-i` or `--StdIn`

### Output Option

Shortcut: `-o`
Long: `--Output`
Description: Specifies the path to the generated output file. If omitted, output is rendered to StdOut.
Accept: single value.
Mandatory: no.
Example: `-opath/to/output` or `--Output=path/to/output`

### Extension option

Shortcut: -x
Long: --Extension
Description: Specifies additional or replacing link between extensions and engine for automatic detection
Accept: multiple key-value pairs.
Mandatory: no.
Example: `-xtxt:handlebars;liquid:fluid` or `--Extension=txt:handlebars;liquid:fluid`
2 changes: 1 addition & 1 deletion docs/_docs/installation-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ tags: [quick-start, installation]

Example:

```pwsh
```powershell
https://github.com/Seddryck/Didot/releases/download/v0.13.0/Didot-0.13.0-net7.0-win-x64.zip
```

Expand Down
58 changes: 58 additions & 0 deletions docs/_docs/specify-engine-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Specify template engine or parser
tags: [cli-usage]
---
## Specify the data parser

- `-p, --Parser`: Defines the parser to use when the source data is provided through the console. Accepted values are `yaml`, `json` or `xml`. This option is required only when the `--Source` argument is omitted or if the extension of the source file is not recognized to determine the parser.

```powershell
didot -t template.hbs -s data.txt -p json -o output.txt
```

In this example:

- `template.hbs` is the Handlebars template file.
- `data.txt` is the source file.
- `json` is the parser of input data from the source file.
- `output.txt` is the file where the output will be rendered.

## Specify the template engine

### Direct specification

- `-e, --Engine`: Defines the template engine to use independantly of the template file extension. Accepted values are `scriban`, `dotliquid`, `fluid`, `handlebars`, `stringtemplate`, `smartformat`.

```powershell
didot -t template.txt -s data.json -e handlebars -o output.txt
```

In this example:

- `template.txt` is the template file.
- `data.json` is the data JSON file.
- `handlebars` is the template engine to use.
- `output.txt` is the file where the output will be rendered.

### Add or replace extension associations

- `-x, --Extensions`: Defines the association of a file's extension with a template engine. More than one can be specified.

```powershell
didot -t template.txt -s data.json -x txt:handlebars;liquid:fluid -o output.txt
```

In this example:

- `template.txt` is the template file.
- `data.json` is the data JSON file.
- `txt:handlebars;liquid:fluid` is associating Handlbars to the extension `.txt` and Fluid to the extension `.liquid`.
- `output.txt` is the file where the output will be rendered.

By default following file's extension association are registered:

- `.scriban` to Scriban
- `.liquid` to DotLiquid
- `.hbs` to Handlebars
- `.smart` to SmartFormat
- `.st` and `.stg` to StringTemplate
8 changes: 7 additions & 1 deletion docs/_docs/supported-format-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ Didot utilizes some templating engines, which allow for powerful and flexible te
- Supports customizable scripting with rich expressions and filters.
- Can work with JSON and YAML data sources.
- Typical Use Case: Config file generation, reports, email templates, or any templating scenario not tied to a specific web framework.
- **Liquid**: Templates with the `.liquid` extension are parsed using a dotLiquid template engine. DotLiquid is a .NET port of the Liquid templating engine used by platforms like Shopify.
- **DotLiquid**: Templates with the `.liquid` extension are parsed using a dotLiquid template engine. DotLiquid is a .NET port of the Liquid templating engine used by platforms like Shopify.
- Secure (no access to system objects), making it ideal for user-generated templates.
- Allows both dynamic and static templating.
- Supports filters, tags, and various control flow structures.
- Typical Use Case: SaaS applications, dynamic content rendering, email templates.
- **Fluid**: Fully compatible with `.liquid` templates. The Fluid template engine is a fast and secure .NET-based port of the Liquid templating language.
- Optimized for performance, with careful memory management and faster parsing.
- Highly secure (does not expose system objects) and well-suited for user-generated content and environments requiring strict control over input and output.
- Rich templating features, supporting filters, tags, loops, and conditionals.
- Flexible and customizable, making it easy to extend the engine with custom filters or tags.
- Typical Use Case: Applications with complex data bindings, dynamic content generation in websites, CMS platforms, and document templating systems.
- **Handlebars**: Templates with the `.hbs` extension are parsed using a Handlebars template engine. Handlebars C# port of the popular JavaScript Handlebars templating engine.
- Simple syntax for generating HTML or text files from templates.
- Support for helpers, partial templates, and block helpers.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Other options
title: Usage of StdIn and StdOut
tags: [cli-usage]
---
## With data from the console
Expand All @@ -8,7 +8,7 @@ tags: [cli-usage]
- `-p, --Parser`: Defines the parser to use when the source data is provided through the console. Accepted values are `yaml`, `json` or `xml`. This option is required only when the `--Source` argument is omitted or if the extension of the source file is not recognized to determine the parser.

<sub>CMD:</sub>
```cmd
```bash
type data.json | didot --StdIn -t template.hbs -p json -o output.txt
```

Expand Down
6 changes: 6 additions & 0 deletions src/Didot.Cli/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public class Options
[Option('p', "Parser", Required = false, HelpText = "The parser to use when reading from StdIn.")]
public string? Parser { get; set; }

[Option('e', "Engine", Required = false, HelpText = "Force a specific engine.")]
public string? Engine { get; set; }

[Option('x', "Extension", Required = false, Separator = ';', HelpText = "Associate an extension to a specific engine.")]
public required IEnumerable<string> Extensions { get; set; }

[Option('o', "Output", Required = false, HelpText = "Path to the generated file.")]
public string? Output { get; set; }
}
41 changes: 36 additions & 5 deletions src/Didot.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ static Stream copyInStream()
var parserFactory = new FileBasedSourceParserFactory();
var parser = parserFactory.GetSourceParser(sourceExtension);

var templateExtension = new FileInfo(opts.Template).Extension;
var engineFactory = new FileBasedTemplateEngineFactory();
var engine = engineFactory.GetTemplateEngine(templateExtension);
var engine = GetTemplateEngine(opts);

var printer = new Printer(engine, parser);
using var template = File.OpenRead(opts.Template);
Expand All @@ -63,9 +61,42 @@ static Stream copyInStream()
File.WriteAllText(opts.Output, output);
}

private static ITemplateEngine GetTemplateEngine(Options opts)
{
var engineFactory = new FileBasedTemplateEngineFactory();
if (opts.Engine is not null)
return engineFactory.GetTemplateEngineByTag(opts.Engine);
else
{
if (opts.Extensions is not null && opts.Extensions.Any())
{
foreach (var extensionAssociation in opts.Extensions)
{
if (!string.IsNullOrWhiteSpace(extensionAssociation))
{
var split = extensionAssociation.Split(':');
if (split.Length != 2)
throw new Exception(extensionAssociation);
(string extension, var engineTag) = (split[0], split[1]);
var engineInstance = engineFactory.GetTemplateEngineByTag(engineTag);
engineFactory.AddOrReplaceEngine(extension, engineInstance);
}
}
}
var templateExtension = new FileInfo(opts.Template).Extension;
return engineFactory.GetTemplateEngineByExtension(templateExtension);
}
}

static void HandleParseError(IEnumerable<Error> errs)
{
// Handle errors here (e.g., show help message)
System.Console.WriteLine("Error parsing arguments.");
Console.WriteLine("Error parsing arguments.");
foreach (var error in errs)
{
if (error is UnknownOptionError unknown)
Console.WriteLine($"{unknown.Tag}: {unknown.Token}");
else
Console.WriteLine(error);
}
}
}
1 change: 1 addition & 0 deletions src/Didot.Core/Didot.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Domemtech.StringTemplate4" Version="4.3.0" />
<PackageReference Include="DotLiquid" Version="2.2.692" />
<PackageReference Include="Fluid.Core" Version="2.11.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Scriban" Version="5.10.0" />
Expand Down
77 changes: 68 additions & 9 deletions src/Didot.Core/TemplateEngines/FileBasedTemplateEngineFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,73 @@
namespace Didot.Core.TemplateEngines;
public class FileBasedTemplateEngineFactory
{
public ITemplateEngine GetTemplateEngine(string extension)
=> extension.ToLowerInvariant() switch
protected Dictionary<string, ITemplateEngine> engines = new();

public FileBasedTemplateEngineFactory()
{
Initialize();
}

protected virtual void Initialize()
{
engines.Clear();
engines.Add(".scriban", new ScribanWrapper());
engines.Add(".liquid", new DotLiquidWrapper());
engines.Add(".hbs", new HandlebarsWrapper());
engines.Add(".smart", new SmartFormatWrapper());
engines.Add(".st", new StringTemplateWrapper());
engines.Add(".stg", new StringTemplateWrapper());
}

public virtual void AddOrReplaceEngine(string extension, ITemplateEngine engine)
{
extension = NormalizeExtension(extension);

if (engines.ContainsKey(extension))
engines[extension] = engine;
else
engines.Add(extension, engine);
}

protected virtual string NormalizeExtension(string extension)
{
extension = extension.Trim().ToLowerInvariant();
if (!extension.StartsWith('.'))
extension = $".{extension}";
return extension;
}

public ITemplateEngine GetTemplateEngineByExtension(string extension)
{
extension = NormalizeExtension(extension);
if (engines.TryGetValue(extension, out var engine))
return engine;
throw new NotSupportedException(nameof(extension));
}

public Dictionary<string, Type> ListEngineByTags()
{
var asm = typeof(ITemplateEngine).Assembly;
var types = asm.GetTypes()
.Where(t => t.Namespace == GetType().Namespace
&& typeof(ITemplateEngine).IsAssignableFrom(t)
&& !t.IsInterface
&& !t.IsAbstract);

var dict = new Dictionary<string, Type>();
foreach (var type in types)
{
".scriban" => new ScribanWrapper(),
".liquid" => new DotLiquidWrapper(),
".hbs" => new HandlebarsWrapper(),
".smart" => new SmartFormatWrapper(),
".st" or ".stg" => new StringTemplateWrapper(),
_ => throw new NotSupportedException()
};
var tag = type.Name.Replace("Wrapper", string.Empty).Trim().ToLowerInvariant();
dict.Add(tag, type);
}
return dict;
}

public ITemplateEngine GetTemplateEngineByTag(string tag)
{
tag = tag.Trim().ToLowerInvariant();
if (!ListEngineByTags().TryGetValue(tag, out var engineType))
throw new Exception(tag);
return (ITemplateEngine)Activator.CreateInstance(engineType)!;
}
}
Loading

0 comments on commit ac087bc

Please sign in to comment.