Merge pull request #5568 from Codinlab/Tokens

Token chaining improvements
This commit is contained in:
Sébastien Ros
2015-09-08 16:08:17 -07:00
8 changed files with 124 additions and 32 deletions

View File

@@ -7,7 +7,7 @@ namespace Orchard.DynamicForms.Tokens {
public void Describe(DescribeContext context) {
context.For("FormSubmission", T("Dynamic Form submission"), T("Dynamic Form Submission tokens for use in workflows handling the Dynamic Form Submitted event."))
.Token("Field:*", T("Field:<field name>"), T("The posted field value to access."))
.Token("Field:*", T("Field:<field name>"), T("The posted field value to access."), "Text")
.Token("IsValid:*", T("IsValid:<field name>"), T("The posted field validation status."))
;
}
@@ -15,10 +15,20 @@ namespace Orchard.DynamicForms.Tokens {
public void Evaluate(EvaluateContext context) {
context.For<FormSubmissionTokenContext>("FormSubmission")
.Token(token => token.StartsWith("Field:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Field:".Length) : null, GetFieldValue)
.Chain(FilterChainParam, "Text", GetFieldValue)
.Token(token => token.StartsWith("IsValid:", StringComparison.OrdinalIgnoreCase) ? token.Substring("IsValid:".Length) : null, GetFieldValidationStatus);
}
private object GetFieldValue(string fieldName, FormSubmissionTokenContext context) {
private static Tuple<string, string> FilterChainParam(string token) {
int tokenLength = "Field:".Length;
int chainIndex = token.IndexOf('.');
if (token.StartsWith("Field:", StringComparison.OrdinalIgnoreCase) && chainIndex > tokenLength)
return new Tuple<string, string>(token.Substring(tokenLength, chainIndex - tokenLength), token.Substring(chainIndex + 1));
else
return null;
}
private string GetFieldValue(string fieldName, FormSubmissionTokenContext context) {
return context.PostedValues[fieldName];
}

View File

@@ -31,21 +31,33 @@ namespace Orchard.Taxonomies.Tokens {
context.For<TaxonomyField>("TaxonomyField")
.Token("Terms", field => String.Join(", ", field.Terms.Select(t => t.Name).ToArray()))
.Token(
token => {
var index = 0;
return (token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) && int.TryParse(token.Substring("Terms:".Length), out index)) ? index.ToString() : null;
},
(token, t) => {
var index = Convert.ToInt32(token);
return index + 1 > t.Terms.Count() ? null : t.Terms.ElementAt(index).Name;
.Token(FilterTokenParam,
(index, field) => {
var term = field.Terms.ElementAtOrDefault(Convert.ToInt32(index));
return term != null ? term.Name : null;
})
// todo: extend Chain() in order to accept a filter like in Token() so that we can chain on an expression
.Chain("Terms:0", "Content", t => t.Terms.ElementAt(0))
.Chain("Terms:1", "Content", t => t.Terms.ElementAt(1))
.Chain("Terms:2", "Content", t => t.Terms.ElementAt(2))
.Chain("Terms:3", "Content", t => t.Terms.ElementAt(3))
.Chain(FilterChainParam, "Content", (index, field) => field.Terms.ElementAtOrDefault(Convert.ToInt32(index)))
;
}
private static string FilterTokenParam(string token) {
int index = 0;
return (token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) && int.TryParse(token.Substring("Terms:".Length), out index)) ? index.ToString() : null;
}
private static Tuple<string, string> FilterChainParam(string token) {
int tokenLength = "Terms:".Length;
int index = 0;
int chainIndex = token.IndexOf('.');
if (!token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) || chainIndex <= tokenLength)
return null;
if (int.TryParse(token.Substring(tokenLength, chainIndex - tokenLength), out index)) {
return new Tuple<string, string>(index.ToString(), token.Substring(chainIndex + 1));
}
else {
return null;
}
}
}
}

View File

@@ -7,5 +7,6 @@ namespace Orchard.Tokens {
public abstract EvaluateFor<TData> Chain(string token, string chainTarget, Func<TData, object> chainValue);
public abstract EvaluateFor<TData> Token(Func<string, TData, object> tokenValue);
public abstract EvaluateFor<TData> Token(Func<string, string> filter, Func<string, TData, object> tokenValue);
public abstract EvaluateFor<TData> Chain(Func<string, Tuple<string, string>> filter, string chainTarget, Func<string, TData, object> chainValue);
}
}

View File

@@ -153,6 +153,27 @@ namespace Orchard.Tokens.Implementation {
}
return this;
}
public override EvaluateFor<TData> Chain(Func<string, Tuple<string, string>> filter, string chainTarget, Func<string, TData, object> chainValue) {
var subTokens = _context.Tokens
.Where(kv => kv.Key.Contains('.'))
.Select(kv => {
var filterResult = filter(kv.Key);
return filterResult != null ? new Tuple<string, string, string>(filterResult.Item1, filterResult.Item2, kv.Value) : null;
})
.Where(st => st != null)
.ToList();
if (!subTokens.Any()) {
return this;
}
foreach(var chainGroup in subTokens.GroupBy(st => st.Item1)) {
var subValues = _context._manager.Evaluate(chainTarget, chainGroup.ToDictionary(cg => cg.Item2, cg => cg.Item3), new Dictionary<string, object> { { chainTarget, chainValue(chainGroup.Key, _data) } });
foreach (var subValue in subValues) {
_context.Values[subValue.Key] = subValue.Value;
}
}
return this;
}
}
private class EvaluateForSilent<TData> : EvaluateFor<TData> {
@@ -175,6 +196,10 @@ namespace Orchard.Tokens.Implementation {
public override EvaluateFor<TData> Chain(string token, string chainTarget, Func<TData, object> chainValue) {
return this;
}
public override EvaluateFor<TData> Chain(Func<string, Tuple<string, string>> filter, string chainTarget, Func<string, TData, object> chainValue) {
return this;
}
}
}

View File

@@ -36,7 +36,7 @@ namespace Orchard.Tokens.Providers {
.Token("DisplayUrl", T("Display Url"), T("Url to display the content."), "Url")
.Token("EditUrl", T("Edit Url"), T("Url to edit the content."), "Url")
.Token("Container", T("Container"), T("The container Content Item."), "Content")
.Token("Body", T("Body"), T("The body text of the content item."), "Content")
.Token("Body", T("Body"), T("The body text of the content item."), "Text")
;
// Token descriptors for fields

View File

@@ -13,38 +13,42 @@ namespace Orchard.Tokens.Providers {
public void Describe(DescribeContext context) {
context.For("Text", T("Text"), T("Tokens for text strings"))
.Token("Limit:*", T("Limit:<text length>[,<ellipsis>]"), T("Limit text to specified length and append an optional ellipsis text."))
.Token("Format:*", T("Format:<text format>"), T("Optional format specifier (e.g. foo{0}bar). See format strings at <a target=\"_blank\" href=\"http://msdn.microsoft.com/en-us/library/az4se3k1.aspx\">Standard Formats</a> and <a target=\"_blank\" href=\"http://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx\">Custom Formats</a>"), "DateTime")
.Token("TrimEnd:*", T("TrimEnd:<chars|number>"), T("Trims the specified characters or number of them from the end of the string."), "Text")
.Token("Format:*", T("Format:<text format>"), T("Optional format specifier (e.g. foo{0}bar)."))
.Token("TrimEnd:*", T("TrimEnd:<chars|number>"), T("Trims the specified characters or number of them from the end of the string."))
.Token("UrlEncode", T("Url Encode"), T("Encodes a URL string."), "Text")
.Token("HtmlEncode", T("Html Encode"), T("Encodes an HTML string."), "Text")
.Token("LineEncode", T("Line Encode"), T("Replaces new lines with <br /> tags."))
.Token("LineEncode", T("Line Encode"), T("Replaces new lines with <br /> tags."), "Text")
;
}
public void Evaluate(EvaluateContext context) {
context.For<String>("Text", () => "")
.Token( // {Text}
// {Text}
.Token(
token => token == String.Empty ? String.Empty : null,
(token, d) => d.ToString())
.Token( // {Text.Limit:<length>[,<ellipsis>]}
token => {
if (token.StartsWith("Limit:", StringComparison.OrdinalIgnoreCase)) {
var param = token.Substring("Limit:".Length);
return param;
}
return null;
},
// {Text.Limit:<length>[,<ellipsis>]}
.Token(
token => FilterTokenParam("Limit:", token),
(token, t) => Limit(t, token))
// {Text.Format:<formatstring>}
.Token(
token => token.StartsWith("Format:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Format:".Length) : null,
(token, d) => String.Format(d,token))
.Token(token => token.StartsWith("TrimEnd:", StringComparison.OrdinalIgnoreCase) ? token.Substring("TrimEnd:".Length) : null, TrimEnd)
token => FilterTokenParam("Format:", token),
(token, d) => String.Format(d, token))
// {Text.TrimEnd:<chars|number>}
.Token(token => FilterTokenParam("TrimEnd:", token), TrimEnd)
.Token("UrlEncode", HttpUtility.UrlEncode)
.Chain("UrlEncode", "Text", HttpUtility.UrlEncode)
.Token("HtmlEncode", HttpUtility.HtmlEncode)
.Chain("HtmlEncode", "Text", HttpUtility.HtmlEncode)
.Token("LineEncode", text => text.Replace(System.Environment.NewLine, "<br />"))
.Chain("LineEncode", "Text", text => text.Replace(System.Environment.NewLine, "<br />"))
;
}
private static string FilterTokenParam(string tokenName, string token) {
return token.StartsWith(tokenName, StringComparison.OrdinalIgnoreCase) ? token.Substring(tokenName.Length) : null;
}
private static string TrimEnd(string token, string param) {

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Security;
@@ -24,6 +25,9 @@ namespace Orchard.Tokens.Tests {
context.For("Date")
.Token("Now", T("Now"), T("Current system date in short date format. You can chain a .NET DateTime format string to customize."));
context.For("Users")
.Token("Users[*:]", T("Users"), T("A user by its username"), "User");
}
public void Evaluate(EvaluateContext context) {
@@ -36,6 +40,7 @@ namespace Orchard.Tokens.Tests {
context.For<IUser>("User", () => new TestUser { UserName = "CurrentUser" })
.Token("Name", u => u.UserName)
.Token("Email", u => u.Email)
.Token("Birthdate", u => "Nov 15")
.Chain("Birthdate", "DateTime", u => new DateTime(1978, 11, 15));
@@ -45,6 +50,29 @@ namespace Orchard.Tokens.Tests {
context.For<DateTime>("DateTime")
.Token((token, value) => value.ToString(token));
context.For<TestUser[]>("Users", () => new TestUser[] {
new TestUser { UserName = "User1", Email = "user1@test.com" },
new TestUser { UserName = "User2", Email = "user2@test.com" },
new TestUser { UserName = "User3", Email = "user3@test.com" }
})
.Token(
(token) => token.StartsWith("User:", StringComparison.OrdinalIgnoreCase) ? token.Substring("User:".Length) : null,
(userName, users) => users.Where(u => u.UserName == userName).Select(u => u.UserName).FirstOrDefault()
)
.Chain(
(token) => {
int tokenLength = "User:".Length;
int chainIndex = token.IndexOf('.');
if (token.StartsWith("User:", StringComparison.OrdinalIgnoreCase) && chainIndex > tokenLength)
return new Tuple<string, string>(token.Substring(tokenLength, chainIndex - tokenLength), token.Substring(chainIndex + 1));
else
return null;
},
"User",
(userName, users) => users.Where(u => u.UserName == userName).FirstOrDefault()
);
}
}

View File

@@ -40,6 +40,18 @@ namespace Orchard.Tokens.Tests {
Assert.That(_tokenizer.Replace("{Site.CurrentUser.Birthdate.yyyy}", null), Is.EqualTo("1978"));
}
[Test]
public void TestParameterizedTokens() {
Assert.That(_tokenizer.Replace("{Users.User:User2}", null), Is.EqualTo("User2"));
Assert.That(_tokenizer.Replace("{Users.User:FakeUser}", null), Is.EqualTo(""));
}
[Test]
public void TestParameterizedChainedTokens() {
Assert.That(_tokenizer.Replace("{Users.User:User2.Email}", null), Is.EqualTo("user2@test.com"));
Assert.That(_tokenizer.Replace("{Users.User:FakeUser.Email}", null), Is.EqualTo(""));
}
[Test]
public void TestMissingTokens() {
Assert.That(_tokenizer.Replace("[{Site.NotAToken}]", null), Is.EqualTo("[]"));