PageRenderTime 60ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/DotLiquid/Context.cs

https://github.com/RevCBH/dotliquid
C# | 447 lines | 308 code | 54 blank | 85 comment | 90 complexity | 2efc8ea4088b059a0c2f56c026b9a807 MD5 | raw file
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text.RegularExpressions;
  7. using DotLiquid.Exceptions;
  8. using DotLiquid.Util;
  9. namespace DotLiquid
  10. {
  11. public class Context
  12. {
  13. private readonly bool _rethrowErrors;
  14. private Strainer _strainer;
  15. public List<Hash> Environments { get; private set; }
  16. public List<Hash> Scopes { get; private set; }
  17. public Hash Registers { get; private set; }
  18. public List<Exception> Errors { get; private set; }
  19. public Context(List<Hash> environments, Hash outerScope, Hash registers, bool rethrowErrors)
  20. {
  21. Environments = environments;
  22. Scopes = new List<Hash>();
  23. if (outerScope != null)
  24. Scopes.Add(outerScope);
  25. Registers = registers;
  26. Errors = new List<Exception>();
  27. _rethrowErrors = rethrowErrors;
  28. SquashInstanceAssignsWithEnvironments();
  29. }
  30. public Context()
  31. : this(new List<Hash>(), new Hash(), new Hash(), false)
  32. {
  33. }
  34. public Strainer Strainer
  35. {
  36. get { return (_strainer = _strainer ?? Strainer.Create(this)); }
  37. }
  38. /// <summary>
  39. /// Adds filters to this context.
  40. /// this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
  41. /// for that
  42. /// </summary>
  43. /// <param name="filters"></param>
  44. public void AddFilters(IEnumerable<Type> filters)
  45. {
  46. foreach (Type f in filters)
  47. Strainer.Extend(f);
  48. }
  49. public void AddFilters(params Type[] filters)
  50. {
  51. if (filters != null)
  52. AddFilters(filters.AsEnumerable());
  53. }
  54. public string HandleError(Exception ex)
  55. {
  56. Errors.Add(ex);
  57. if (_rethrowErrors)
  58. throw ex;
  59. if (ex is SyntaxException)
  60. return string.Format(Liquid.ResourceManager.GetString("ContextLiquidSyntaxError"), ex.Message);
  61. return string.Format(Liquid.ResourceManager.GetString("ContextLiquidError"), ex.Message);
  62. }
  63. public object Invoke(string method, List<object> args)
  64. {
  65. if (Strainer.RespondTo(method))
  66. return Strainer.Invoke(method, args);
  67. return args.First();
  68. //throw new FilterNotFoundException("Filter not found: '{0}'", method);
  69. }
  70. /// <summary>
  71. /// Push new local scope on the stack. use <tt>Context#stack</tt> instead
  72. /// </summary>
  73. /// <param name="newScope"></param>
  74. public void Push(Hash newScope)
  75. {
  76. if (Scopes.Count > 100)
  77. throw new StackLevelException(Liquid.ResourceManager.GetString("ContextStackException"));
  78. Scopes.Insert(0, newScope);
  79. }
  80. /// <summary>
  81. /// Merge a hash of variables in the current local scope
  82. /// </summary>
  83. /// <param name="newScopes"></param>
  84. public void Merge(Hash newScopes)
  85. {
  86. Scopes[0].Merge(newScopes);
  87. }
  88. /// <summary>
  89. /// Pop from the stack. use <tt>Context#stack</tt> instead
  90. /// </summary>
  91. public Hash Pop()
  92. {
  93. if (Scopes.Count == 1)
  94. throw new ContextException();
  95. Hash result = Scopes[0];
  96. Scopes.RemoveAt(0);
  97. return result;
  98. }
  99. /// <summary>
  100. /// pushes a new local scope on the stack, pops it at the end of the block
  101. ///
  102. /// Example:
  103. ///
  104. /// context.stack do
  105. /// context['var'] = 'hi'
  106. /// end
  107. /// context['var] #=> nil
  108. /// </summary>
  109. /// <param name="newScope"></param>
  110. /// <param name="callback"></param>
  111. /// <returns></returns>
  112. public void Stack(Hash newScope, Action callback)
  113. {
  114. Push(newScope);
  115. try
  116. {
  117. callback();
  118. }
  119. finally
  120. {
  121. Pop();
  122. }
  123. }
  124. public void Stack(Action callback)
  125. {
  126. Stack(new Hash(), callback);
  127. }
  128. public void ClearInstanceAssigns()
  129. {
  130. Scopes[0].Clear();
  131. }
  132. /// <summary>
  133. /// Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
  134. /// </summary>
  135. /// <param name="key"></param>
  136. /// <returns></returns>
  137. public object this[string key]
  138. {
  139. get { return Resolve(key); }
  140. set { Scopes[0][key] = value; }
  141. }
  142. public bool HasKey(string key)
  143. {
  144. return Resolve(key) != null;
  145. }
  146. /// <summary>
  147. /// Look up variable, either resolve directly after considering the name. We can directly handle
  148. /// Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
  149. /// later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
  150. /// Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
  151. ///
  152. /// Example:
  153. ///
  154. /// products == empty #=> products.empty?
  155. /// </summary>
  156. /// <param name="key"></param>
  157. /// <returns></returns>
  158. private object Resolve(string key)
  159. {
  160. switch (key)
  161. {
  162. case null:
  163. case "nil":
  164. case "null":
  165. case "":
  166. return null;
  167. case "true":
  168. return true;
  169. case "false":
  170. return false;
  171. case "blank":
  172. return new Symbol(o => o is IEnumerable && !((IEnumerable) o).Cast<object>().Any());
  173. case "empty":
  174. return new Symbol(o => o is IEnumerable && !((IEnumerable) o).Cast<object>().Any());
  175. }
  176. // Single quoted strings.
  177. Match match = Regex.Match(key, R.Q(@"^'(.*)'$"));
  178. if (match.Success)
  179. return match.Groups[1].Value;
  180. // Double quoted strings.
  181. match = Regex.Match(key, R.Q(@"^""(.*)""$"));
  182. if (match.Success)
  183. return match.Groups[1].Value;
  184. // Integer.
  185. match = Regex.Match(key, R.Q(@"^([+-]?\d+)$"));
  186. if (match.Success)
  187. return Convert.ToInt32(match.Groups[1].Value);
  188. // Ranges.
  189. match = Regex.Match(key, R.Q(@"^\((\S+)\.\.(\S+)\)$"));
  190. if (match.Success)
  191. return Range.Inclusive(Convert.ToInt32(Resolve(match.Groups[1].Value)),
  192. Convert.ToInt32(Resolve(match.Groups[2].Value)));
  193. // Floats.
  194. match = Regex.Match(key, R.Q(@"^([+-]?\d[\d\.|\,]+)$"));
  195. if (match.Success)
  196. {
  197. // For cultures with "," as the decimal separator, allow
  198. // both "," and "." to be used as the separator.
  199. // First try to parse using current culture.
  200. float result;
  201. if (float.TryParse(match.Groups[1].Value, out result))
  202. return result;
  203. // If that fails, try to parse using invariant culture.
  204. return float.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
  205. }
  206. return Variable(key);
  207. }
  208. /// <summary>
  209. /// Fetches an object starting at the local scope and then moving up
  210. /// the hierarchy
  211. /// </summary>
  212. /// <param name="key"></param>
  213. /// <returns></returns>
  214. private object FindVariable(string key)
  215. {
  216. Hash scope = Scopes.FirstOrDefault(s => s.ContainsKey(key));
  217. object variable = null;
  218. if (scope == null)
  219. {
  220. foreach (Hash e in Environments)
  221. if ((variable = LookupAndEvaluate(e, key)) != null)
  222. {
  223. scope = e;
  224. break;
  225. }
  226. }
  227. scope = scope ?? Environments.LastOrDefault() ?? Scopes.Last();
  228. variable = variable ?? LookupAndEvaluate(scope, key);
  229. variable = Liquidize(variable);
  230. if (variable is IContextAware)
  231. ((IContextAware) variable).Context = this;
  232. return variable;
  233. }
  234. /// <summary>
  235. /// Resolves namespaced queries gracefully.
  236. ///
  237. /// Example
  238. ///
  239. /// @context['hash'] = {"name" => 'tobi'}
  240. /// assert_equal 'tobi', @context['hash.name']
  241. /// assert_equal 'tobi', @context['hash["name"]']
  242. /// </summary>
  243. /// <param name="markup"></param>
  244. /// <returns></returns>
  245. private object Variable(string markup)
  246. {
  247. List<string> parts = R.Scan(markup, Liquid.VariableParser);
  248. Regex squareBracketed = new Regex(R.Q(@"^\[(.*)\]$"));
  249. string firstPart = parts.Shift();
  250. Match firstPartSquareBracketedMatch = squareBracketed.Match(firstPart);
  251. if (firstPartSquareBracketedMatch.Success)
  252. firstPart = Resolve(firstPartSquareBracketedMatch.Groups[1].Value).ToString();
  253. object @object;
  254. if ((@object = FindVariable(firstPart)) != null)
  255. {
  256. foreach (string forEachPart in parts)
  257. {
  258. Match partSquareBracketedMatch = squareBracketed.Match(forEachPart);
  259. bool partResolved = partSquareBracketedMatch.Success;
  260. object part = forEachPart;
  261. if (partResolved)
  262. part = Resolve(partSquareBracketedMatch.Groups[1].Value);
  263. // If object is a KeyValuePair, we treat it a bit differently - we might be rendering
  264. // an included template.
  265. if (@object is KeyValuePair<string, object> && ((KeyValuePair<string, object>) @object).Key == (string) part)
  266. {
  267. object res = ((KeyValuePair<string, object>) @object).Value;
  268. @object = Liquidize(res);
  269. }
  270. // If object is a hash- or array-like object we look for the
  271. // presence of the key and if its available we return it
  272. else if (IsHashOrArrayLikeObject(@object, part))
  273. {
  274. // If its a proc we will replace the entry with the proc
  275. object res = LookupAndEvaluate(@object, part);
  276. @object = Liquidize(res);
  277. }
  278. // Some special cases. If the part wasn't in square brackets and
  279. // no key with the same name was found we interpret following calls
  280. // as commands and call them on the current object
  281. else if (!partResolved && (@object is IEnumerable) && ((part as string) == "size" || (part as string) == "first" || (part as string) == "last"))
  282. {
  283. var castCollection = ((IEnumerable) @object).Cast<object>();
  284. if ((part as string) == "size")
  285. @object = castCollection.Count();
  286. else if ((part as string) == "first")
  287. @object = castCollection.FirstOrDefault();
  288. else if ((part as string) == "last")
  289. @object = castCollection.LastOrDefault();
  290. }
  291. // No key was present with the desired value and it wasn't one of the directly supported
  292. // keywords either. The only thing we got left is to return nil
  293. else
  294. {
  295. return null;
  296. }
  297. // If we are dealing with a drop here we have to
  298. if (@object is IContextAware)
  299. ((IContextAware) @object).Context = this;
  300. }
  301. }
  302. return @object;
  303. }
  304. private static bool IsHashOrArrayLikeObject(object obj, object part)
  305. {
  306. if (obj == null)
  307. return false;
  308. if ((obj is IDictionary && ((IDictionary) obj).Contains(part)))
  309. return true;
  310. if ((obj is IList) && (part is int))
  311. return true;
  312. if (TypeUtility.IsAnonymousType(obj.GetType()) && obj.GetType().GetProperty((string) part) != null)
  313. return true;
  314. if ((obj is IIndexable) && ((IIndexable) obj).ContainsKey((string) part))
  315. return true;
  316. return false;
  317. }
  318. private object LookupAndEvaluate(object obj, object key)
  319. {
  320. object value;
  321. if (obj is IDictionary)
  322. value = ((IDictionary) obj)[key];
  323. else if (obj is IList)
  324. value = ((IList) obj)[(int) key];
  325. else if (TypeUtility.IsAnonymousType(obj.GetType()))
  326. value = obj.GetType().GetProperty((string) key).GetValue(obj, null);
  327. else if (obj is IIndexable)
  328. value = ((IIndexable) obj)[key];
  329. else
  330. throw new NotSupportedException();
  331. if (value is Proc)
  332. {
  333. object newValue = ((Proc) value).Invoke(this);
  334. if (obj is IDictionary)
  335. ((IDictionary) obj)[key] = newValue;
  336. else if (obj is IList)
  337. ((IList) obj)[(int) key] = newValue;
  338. else if (TypeUtility.IsAnonymousType(obj.GetType()))
  339. obj.GetType().GetProperty((string) key).SetValue(obj, newValue, null);
  340. else
  341. throw new NotSupportedException();
  342. return newValue;
  343. }
  344. return value;
  345. }
  346. private static object Liquidize(object obj)
  347. {
  348. if (obj == null)
  349. return obj;
  350. if (obj is ILiquidizable)
  351. return ((ILiquidizable) obj).ToLiquid();
  352. if (obj is string)
  353. return obj;
  354. if (obj is IEnumerable)
  355. return obj;
  356. if (obj.GetType().IsPrimitive)
  357. return obj;
  358. if (obj is decimal)
  359. return obj;
  360. if (obj is DateTime)
  361. return obj;
  362. if (obj is TimeSpan)
  363. return obj;
  364. if (TypeUtility.IsAnonymousType(obj.GetType()))
  365. return obj;
  366. if (obj is KeyValuePair<string, object>)
  367. return obj;
  368. Func<object,object> f;
  369. if (Template.simpleTypeTransformers.TryGetValue(obj.GetType(), out f))
  370. return f(obj);
  371. if (obj.GetType().GetCustomAttributes(typeof(LiquidTypeAttribute), false).Count() > 0)
  372. {
  373. var attr = (LiquidTypeAttribute)obj.GetType().GetCustomAttributes(typeof(LiquidTypeAttribute), false).Single();
  374. return new DropProxy(obj, attr.DeclaredOnly);
  375. }
  376. throw new SyntaxException(Liquid.ResourceManager.GetString("ContextObjectInvalidException"), obj.ToString());
  377. }
  378. private void SquashInstanceAssignsWithEnvironments()
  379. {
  380. Dictionary<string, object> tempAssigns = new Dictionary<string, object>(Template.NamingConvention.StringComparer);
  381. Hash lastScope = Scopes.Last();
  382. foreach (string k in lastScope.Keys)
  383. foreach (Hash env in Environments)
  384. if (env.ContainsKey(k))
  385. {
  386. tempAssigns[k] = LookupAndEvaluate(env, k);
  387. break;
  388. }
  389. foreach (string k in tempAssigns.Keys)
  390. lastScope[k] = tempAssigns[k];
  391. }
  392. }
  393. }