Skip to content
Merged
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
124 changes: 101 additions & 23 deletions src/TextTemplate/TemplateEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
private readonly IDictionary<string, object> _rootData;
private readonly object? _current;
private readonly object? _rootCurrent;
private bool _lastPipelineWasAssignment;
private static readonly Dictionary<string, Func<object?[], object?>> PipelineFuncs = new()
{
["lower"] = args => args.Length > 0 ? args[0]?.ToString()?.ToLowerInvariant() : null,
Expand All @@ -141,7 +142,7 @@
if (args.Length == 0) return string.Empty;
string fmt = args[0]?.ToString() ?? string.Empty;
var rest = args.Skip(1).ToArray();
return SprintfFormatter.Format(fmt, rest);

Check warning on line 145 in src/TextTemplate/TemplateEngine.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'object?[]' cannot be used for parameter 'args' of type 'object[]' in 'string SprintfFormatter.Format(string format, params object[] args)' due to differences in the nullability of reference types.
},
["html"] = args => System.Net.WebUtility.HtmlEncode(string.Concat(args.Select(a => a?.ToString()))),
["js"] = args => System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(string.Concat(args.Select(a => a?.ToString()))),
Expand Down Expand Up @@ -173,7 +174,7 @@
}
if (current is IDictionary dict)
{
current = dict[key];

Check warning on line 177 in src/TextTemplate/TemplateEngine.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'object? IDictionary.this[object key]'.
continue;
}
current = null;
Expand Down Expand Up @@ -282,13 +283,11 @@

public override string VisitPlaceholder(GoTextTemplateParser.PlaceholderContext context)
{
var commands = context.pipeline().command();
object? result = null;
bool first = true;
foreach (var cmd in commands)
object? result = EvaluatePipeline(context.pipeline());
if (_lastPipelineWasAssignment)
{
result = ExecuteCommand(cmd, first ? null : result);
first = false;
_lastPipelineWasAssignment = false;
return string.Empty;
}
return result?.ToString() ?? string.Empty;
}
Expand Down Expand Up @@ -342,7 +341,23 @@

public override string VisitRangeBlock(GoTextTemplateParser.RangeBlockContext context)
{
var sourceObj = ResolvePath(context.rangeClause().path());
object? sourceObj;
var clause = context.rangeClause();
var rcType = clause.GetType();
object? pipelineObj = rcType.GetMethod("pipeline")?.Invoke(clause, null);
if (clause.varList() != null && pipelineObj is GoTextTemplateParser.PipelineContext pc)
{
sourceObj = EvaluatePipeline(pc);
}
else
{
var pathCtx = rcType.GetMethod("path")?.Invoke(clause, null) as GoTextTemplateParser.PathContext;
if (pathCtx != null)
sourceObj = ResolvePath(pathCtx);
else
sourceObj = null;
}

if (sourceObj is not IEnumerable)
{
if (context.elseBlock() != null)
Expand Down Expand Up @@ -520,31 +535,44 @@

if (text.StartsWith("$"))
{
if (text == "$" || text == "$.")
return _rootCurrent;
text = text.StartsWith("$.") ? text.Substring(2) : text.Substring(1);
if (string.IsNullOrEmpty(text))
return _rootCurrent;
var segmentsRoot = ParseSegments(text);
object? currentRoot = _rootData;
foreach (var seg in segmentsRoot)
if (text == "$" || text.StartsWith("$."))
{
string rootText = text.StartsWith("$.") ? text.Substring(2) : string.Empty;
if (rootText.Length == 0)
return _rootCurrent;
var segRoot = ParseSegments(rootText);
object? curRoot = _rootData;
foreach (var seg in segRoot)
{
if (curRoot == null)
return null;
curRoot = ResolveSegment(curRoot, seg);
}
return curRoot;
}

var segmentsVar = ParseSegments(text);
if (segmentsVar.Count == 0 || segmentsVar[0] is not VariableName varSeg)
return null;
_data.TryGetValue(varSeg.Name, out object? current);
for (int idx = 1; idx < segmentsVar.Count; idx++)
{
if (currentRoot == null)
if (current == null)
return null;
currentRoot = ResolveSegment(currentRoot, seg);
current = ResolveSegment(current, segmentsVar[idx]);
}
return currentRoot;
return current;
}

var segments = ParseSegments(text);
object? current = _data;
object? currentDefault = _data;
foreach (var seg in segments)
{
if (current == null)
if (currentDefault == null)
return null;
current = ResolveSegment(current, seg);
currentDefault = ResolveSegment(currentDefault, seg);
}
return current;
return currentDefault;
}

private object? EvaluateExpr(GoTextTemplateParser.ExprContext context)
Expand Down Expand Up @@ -612,6 +640,22 @@

private object? EvaluatePipeline(GoTextTemplateParser.PipelineContext context)
{
_lastPipelineWasAssignment = false;

// Use reflection to support optional var assignment fields
GoTextTemplateParser.VarListContext? varList = null;
bool isColoneq = false;
bool isAssign = false;
var type = context.GetType();
var varListMethod = type.GetMethod("varList");
if (varListMethod != null)
varList = varListMethod.Invoke(context, null) as GoTextTemplateParser.VarListContext;
if (varList != null)
{
isColoneq = type.GetMethod("COLONEQ")?.Invoke(context, null) != null;
isAssign = type.GetMethod("ASSIGN")?.Invoke(context, null) != null;
}

var commands = context.command();
object? result = null;
bool first = true;
Expand All @@ -620,6 +664,25 @@
result = ExecuteCommand(cmd, first ? null : result);
first = false;
}

if (varList != null)
{
foreach (var v in varList.varName())
{
string name = v.GetText().TrimStart('$');
if (isAssign)
{
if (_data.ContainsKey(name))
_data[name] = result!;
}
else // COLONEQ
{
_data[name] = result!;
}
}
_lastPipelineWasAssignment = true;
}

return result;
}

Expand Down Expand Up @@ -718,11 +781,26 @@
public PathReference(string path) => Path = path;
}

private sealed class VariableName
{
public string Name { get; }
public VariableName(string name) => Name = name;
}

private static List<object> ParseSegments(string text)
{
var result = new List<object>();
int i = 0;
if (text.StartsWith(".")) i++;
if (text.StartsWith("$") && text.Length > 1 && text[1] != '.')
{
i = 1;
int start = i;
while (i < text.Length && (char.IsLetterOrDigit(text[i]) || text[i] == '_')) i++;
var varName = text.Substring(start, i - start);
result.Add(new VariableName(varName));
if (i < text.Length && text[i] == '.') i++;
}
else if (text.StartsWith(".")) i++;
while (i < text.Length)
{
if (text[i] == '.')
Expand Down
Loading