Advanced Extending SXA Scriban

GitHub - scriban/scriban: A fast, powerful, safe and lightweight scripting  language and engine for .NET

The Basic Model

Adding simple Scriban functions to Sitecore’s SXA is fairly straightforward. The following

	public class GetItemMethod : IGenerateScribanContextProcessor
	{
		private IContext _context;
		public GetItemMethod(IContext context)
		{
			_context = context ?? throw new ArgumentException(nameof(context));
		}
		public void Process(GenerateScribanContextPipelineArgs args)
		{
			args.GlobalScriptObject.Import("sc_getitem", new GetItem((object id) => { return _context.Database.GetItem(id as string); }));
		}
		private delegate Item GetItem(object id);
	}
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <generateScribanContext>
        <processor type="{YOUR NAMESPACE].GetItemMethod,[YOUR ASSEMBLY]" resolve="true" />
      </generateScribanContext>
    </pipelines>
  </sitecore>
</configuration>

This allows you to use the scriban function {sc_getItem ‘[MY GUID]’ } and have it return the item in the current context database that has that ID. This model works great when you’re working with basic primitives and Sitecore items, but loses it’s power when you want something more powerful.

Advanced Dictionary method

Making use of SXA’s dictionary feature is great and i tend to do it a lot. In an MVC world i tended to go with my https://jeffdarchuk.com/2019/10/23/sxa-advanced-dictionary/ approach. Bringing in the code from this other blogpost, below is the additions to make it work with Scriban.

	public class AutoDictionaryMethods : AddFieldRendererFunction, IGenerateScribanContextProcessor
	{
		private IAutoDictionaryRepository _autoDictionary;
		public AutoDictionaryMethods(IPageMode iPageMode, IAutoDictionaryRepository autoDictionary):base(iPageMode)
		{
			_autoDictionary = autoDictionary ?? throw new ArgumentException(nameof(autoDictionary));
		}
		public new void Process(GenerateScribanContextPipelineArgs args)
		{
			this.RenderingWebEditingParams = args.RenderingWebEditingParams;
			RenderField renderField = new RenderField(this.RenderDictionaryEntry);
			args.GlobalScriptObject.Import("sc_autodictionary", renderField);
		}
		public string RenderDictionaryEntry(object key, object defaultValue, ScriptArray parameters = null)
		{
			return this.RenderFieldImpl(_autoDictionary.GetDictionaryItem(key as string, defaultValue as string), AutoDictionary.Templates.SxaDictionaryItem.Fields.Phrase, parameters);
		}
		private delegate string RenderField(object key, object defaultValue, ScriptArray parameters = null);
	}

Note: A configuration identical to the above sc_getitem is needed for this one.

Scriban Partials

Sunglasses-reaction-recursion

However there are some limitations, for example you can’t use this model to execute some additional Scriban. In .NET MVC terms I was looking for something like a partial rendering and I started wondering if it was possible. Sure enough it is, with a little creative digging. First we need to inject into the runtime scripts processor located in the renderVariantField pipeline.

	public class RuntimeScriptObjects : RenderScriban
	{
		private IVariantFieldParser _variantFieldParser;
		public RuntimeScriptObjects(IVariantFieldParser variantFieldParser, IScribanTemplateRenderer templateRenderer):base(templateRenderer)
		{
			_variantFieldParser = variantFieldParser ?? throw new ArgumentException(nameof(variantFieldParser));
		}
		public RuntimeScriptObjects(IScribanTemplateRenderer renderer) : base(renderer) { }
		public override void RenderField(RenderVariantFieldArgs args)
		{
			this.VariantTemplate = args.VariantField as Sitecore.XA.Foundation.Scriban.Fields.VariantScriban;
			if (this.VariantTemplate == null)
				return;
			if (this.VariantTemplate.ScribanTemplate == null)
				this.VariantTemplate.ScribanTemplate = this.ScribanTemplateRenderer.Parse(this.VariantTemplate.Template, this.VariantTemplate.Path);
			TemplateContext templateContext = this.ScribanTemplateRenderer.GetTemplateContext(args.IsControlEditable, args.RenderingWebEditingParams);
			templateContext.PushCulture(this.Context.Language.CultureInfo);
			string empty = string.Empty;
			string text;
			if (this.VariantTemplate.ScribanTemplate.HasErrors)
			{
				text = string.Join("<br/>", this.VariantTemplate.ScribanTemplate.Messages.Select<LogMessage, string>((Func<LogMessage, string>)(m => HttpUtility.HtmlEncode(m.ToString())))) + "<br/>";
			}
			else
			{
				this.AddRuntimeScriptObjects(templateContext, this.VariantTemplate, args);
				try
				{
					text = this.ScribanTemplateRenderer.Render(this.VariantTemplate.ScribanTemplate, templateContext);
				}
				catch (Exception ex)
				{
					Log.Error(ex.Message, ex, (object)this);
					text = HttpUtility.HtmlEncode(ex.Message);
				}
			}
			Control control = (Control)new LiteralControl(text);
			if (!string.IsNullOrWhiteSpace(this.VariantTemplate.Tag))
			{
				HtmlGenericControl tag = new HtmlGenericControl(this.VariantTemplate.Tag);
				this.AddClass(tag, this.VariantTemplate.CssClass);
				this.AddWrapperDataAttributes((RenderingVariantFieldBase)this.VariantTemplate, args, tag);
				this.MoveControl(control, (Control)tag);
				control = (Control)tag;
			}
			args.ResultControl = control;
			args.Result = this.RenderControl(args.ResultControl);
		}
		public void AddDelegateToFunction(RenderVariantFieldArgs args, ScriptObject scriptObject)
		{

			scriptObject.Import("sc_delegateto", new DelegateToDeligate((Item item, Item variant) => {
				VariantScriban variantScriban = new VariantScriban(variant);
				variantScriban.ItemName = variant.Name;
				variantScriban.Path = GetTemplatePath(variant);
				variantScriban.Tag = variant.Fields[Templates.VariantScriban.Fields.Tag].GetEnumValue();
				variantScriban.Template = variant[Templates.VariantScriban.Fields.Template];
				variantScriban.CssClass = variant[Templates.VariantScriban.Fields.CssClass];
				variantScriban.ChildItems = variant.Children.Count > 0 ? _variantFieldParser.ParseVariantFields(variant, variant.Parent, false) : new List<BaseVariantField>();

				RenderVariantFieldArgs variantFieldArgs = new RenderVariantFieldArgs()
				{
					VariantField = variantScriban,
					Item = item,
					HtmlHelper = args.HtmlHelper,
					IsControlEditable = args.IsControlEditable,
					IsFromComposite = args.IsFromComposite,
					RenderingWebEditingParams = args.RenderingWebEditingParams,
					RendererMode = args.RendererMode,
					Model = args.Model
				};
				this.PipelineManager.Run("renderVariantField", variantFieldArgs);
				if (!string.IsNullOrEmpty(variantFieldArgs.Result))
					return variantFieldArgs.Result;
				if (variantFieldArgs.ResultControl != null)
					return this.RenderControl(variantFieldArgs.ResultControl);
				
				return string.Empty;
			}));
	}
		protected new virtual void AddRuntimeScriptObjects(TemplateContext templateContext, VariantScriban variantTemplate, RenderVariantFieldArgs args)
		{
			ScriptObject scriptObject = new ScriptObject();
			scriptObject.Add("i_item", args.Item);
			scriptObject.Add("o_model", args.Model);
			scriptObject.Import("sc_placeholder", (Delegate) new RenderPlaceholder(this.RenderPlaceholderImpl));
			if (args.Parameters != null && args.Parameters.ContainsKey("geospatial"))
				scriptObject.Add("o_geospatial", args.Parameters["geospatial"]);
			this.AddChildExecutionFunction(variantTemplate, args, scriptObject);
			this.AddChildEvaluationFunction(variantTemplate, args, scriptObject);
			this.AddDelegateToFunction(args, scriptObject);
			templateContext.PushGlobal(scriptObject);
		}

		private string GetTemplatePath(Item variantItem)
		{
			if (variantItem.Parent == null || variantItem.InheritsFrom(Sitecore.XA.Foundation.Variants.Abstractions.Templates.VariantsGrouping.ID))
				return string.Empty;
			return this.GetTemplatePath(variantItem.Parent) + "/" + variantItem.Name;
		}
		private delegate string RenderPlaceholder(string placeholderKey, Item item = null);
		private delegate string DelegateToDeligate(Item contextItem, Item variantItem);
	}
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderVariantField>
        <processor patch:instead="*[@type='Sitecore.XA.Foundation.Scriban.Pipelines.RenderVariantField.RenderScriban, Sitecore.XA.Foundation.Scriban']" type="[YOUR NAMESPACE].RuntimeScriptObjects, [YOUR ASSEMBLY]" resolve="true" />
      </renderVariantField>
    </pipelines>
  </sitecore>
</configuration>

The above code is mostly a copy of the RenderScriban type in Sitecore.XA.Foundation.Scriban with one notable exception. the DelegateTo functionality has been added. This allows Scriban objects to delegate rendering to other Scriban objects. For example you could have a common recurring snippet of Scriban that no longer needs to be copy/pasted. However this is less about the specific use case and more about the technique of enabling scriban to render other scriban. Hopefully you can think of some splendidly clever ways to make this work for you.