Skip to content

Instantly share code, notes, and snippets.

@deanebarker
Last active January 10, 2023 15:27
Show Gist options
  • Save deanebarker/47f351a15360cf156df64e6e87bfb3ca to your computer and use it in GitHub Desktop.
Save deanebarker/47f351a15360cf156df64e6e87bfb3ca to your computer and use it in GitHub Desktop.
An example of how to retrieve template files for Liquid/Fluid templating in Optimizely CMS
public class FindTemplateAsBlockAsset : ITemplateSourceProvider
{
private int templateFolderId;
public FindTemplateAsBlockAsset(int templateFolderId)
{
this.templateFolderId = templateFolderId;
}
public string GetSource(string path)
{
// Try to find an asset for this
var repo = ServiceLocator.Current.GetInstance<IContentRepository>();
var currentItem = repo.Get<IContent>(new ContentReference(templateFolderId));
foreach (var segment in path.Split("/"))
{
currentItem = repo.GetChildren<IContent>(currentItem.ContentLink).FirstOrDefault(i => i.Name.ToLower() == segment.ToLower());
if (currentItem == null)
{
break;
}
if (segment.ToLower().EndsWith(".liquid"))
{
return currentItem.Property["TemplateCode"].Value.ToString();
}
}
return null;
}
}
public class FindTemplateAsMediaAsset : ITemplateSourceProvider
{
private int templateFolderId;
public FindTemplateAsMediaAsset(int templateFolderId)
{
this.templateFolderId = templateFolderId;
}
public string GetSource(string path)
{
// Try to find an asset for this
var repo = ServiceLocator.Current.GetInstance<IContentRepository>();
var currentItem = repo.Get<IContent>(new ContentReference(templateFolderId));
foreach (var segment in path.Split("/"))
{
currentItem = repo.GetChildren<IContent>(currentItem.ContentLink).FirstOrDefault(i => i.Name.ToLower() == segment.ToLower());
if (currentItem == null)
{
break;
}
if (segment.ToLower().EndsWith(".liquid"))
{
return currentItem.Property["TemplateCode"].Value.ToString();
}
}
return null;
}
}
public class FindTemplateOnFileSystem : ITemplateSourceProvider
{
private string templatePath;
public FindTemplateOnFileSystem(string templatePath)
{
this.templatePath = templatePath;
}
public string GetSource(string path)
{
var fullPath = Path.Combine(templatePath, path);
if (File.Exists(fullPath))
{
var content = File.ReadAllText(fullPath);
return content;
}
return null;
}
}
/*
services.Configure<FluidMvcViewOptions>(options =>
{
options.ViewsFileProvider = new MultiSourceTemplateProvider(
new FindTemplateAsBlockAsset(8), // Look in folder #8 for a TemplateBlock content object
new FindTemplateAsAsset(113), // Look in folder #113 for a media asset with a .liquid file attached
new FindTemplateOnFileSystem(@"C:\some\path\"), // Look on the file system next
);
});
*/
using Alloy.Liquid.Models.Blocks;
using EPiServer.ServiceLocation;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using System.Text;
namespace DeaneBarker.Optimizely.Fluid
{
public class MultiSourceTemplateProvider : IFileProvider
{
public List<ITemplateSourceProvider> Sources { get; set; } = new();
public MultiSourceTemplateProvider(params ITemplateSourceProvider[] templateSourceProviders)
{
if (templateSourceProviders != null)
{
Sources.AddRange(templateSourceProviders);
}
}
public IFileInfo GetFileInfo(string path)
{
// Clean up the path
// The path comes in weird, for some reason
path = path.Replace("\\", "/").TrimStart("/".ToCharArray());
// Iterate all the sources
foreach(var sourceProvider in Sources)
{
var sourceCode = sourceProvider.GetSource(path);
if(sourceCode != null)
{
// Found it, return this...
return new Template(sourceCode);
}
}
// No source returned anything
return NullTemplate.Instance;
}
// I don't think Fluid ever calls this (fingers crossed)
public IDirectoryContents GetDirectoryContents(string subpath) { throw new NotImplementedException(); }
public IChangeToken Watch(string filter) => TemplateCacheManager.Instance.GetToken(); // below...
}
public class Template : IFileInfo
{
private string fileContents;
public bool Exists => true;
public long Length => fileContents.Length;
public string PhysicalPath => null;
public string Name => null;
public DateTimeOffset LastModified => DateTimeOffset.Now;
public bool IsDirectory => false;
public Template(string _fileContents)
{
fileContents = _fileContents;
}
public Stream CreateReadStream()
{
return new MemoryStream(Encoding.UTF8.GetBytes(fileContents));
}
}
// This represents a "file" that was not found
// You still have to return an IFileInfo, you just need to set Exists to false
public class NullTemplate : IFileInfo
{
public static NullTemplate Instance = new();
public bool Exists => false;
public long Length => 0;
public string PhysicalPath => null;
public string Name => null;
public DateTimeOffset LastModified => DateTimeOffset.Now;
public bool IsDirectory => false;
public NullTemplate(string _ = null) { }
public Stream CreateReadStream()
{
return null;
}
}
public interface ITemplateSourceProvider
{
string GetSource(string path);
}
// Warning: this use's Microsoft's IChangeToken architecture which is...weird
// I barely understand this. Sebastian had to help me with
// Bottom line, when you want to clear the cache:
//
// TemplateCacheManager.Clear();
public class TemplateCacheManager
{
private CancellationTokenSource token;
public static TemplateCacheManager Instance { get; private set; }
static TemplateCacheManager()
{
Instance = new();
var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
contentEvents.PublishedContent += (s, e) => Instance.CheckForCacheClear(e.Content);
contentEvents.MovedContent += (s, e) => Instance.CheckForCacheClear(e.Content);
}
public TemplateCacheManager() { }
public IChangeToken Token { get; private set; }
public IChangeToken GetToken()
{
token = new CancellationTokenSource();
return new CancellationChangeToken(token.Token);
}
private void CheckForCacheClear(IContent content)
{
if(content is TemplateBlock)
{
Clear();
}
}
public void Clear()
{
token.Cancel();
}
}
}
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using EPiServer.Web.Templating;
using Fluid;
using Optimizely.CMS.Labs.LiquidTemplating.ViewEngine;
using System.ComponentModel.DataAnnotations;
namespace Alloy.Liquid.Models.Blocks;
/// <summary>
/// Used to insert a link which is styled as a button
/// </summary>
[SiteContentType(GUID = "426CF12D-1F01-4EA0-922F-0778314DDAF0")]
[SiteImageUrl]
public class TemplateBlock : SiteBlockData
{
[Display(Order = 1, GroupName = SystemTabNames.Content)]
[Required]
[ValidateLiquidParse]
[UIHint(AceEditor.UIHints.Handlebars)]
public virtual string TemplateCode { get; set; }
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class ValidateLiquidParse : ValidationAttribute
{
public ValidateLiquidParse()
{
}
public override bool IsValid(object value)
{
return GetParseException(value.ToString()) == null;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var message = GetParseException(value.ToString());
if(message != null)
{
return new ValidationResult($"Liquid parse error: {message}");
}
return ValidationResult.Success;
}
private string GetParseException(string liquidCode)
{
var parser = new CmsFluidViewParser(new Fluid.FluidParserOptions());
try
{
var template = parser.Parse(liquidCode);
}
catch (Exception e)
{
return e.Message;
}
return null;
}
}
[EditorDescriptorRegistration(TargetType = typeof(string))]
public class ConfigureTemplateCodeEditor : EditorDescriptor
{
public override void ModifyMetadata(
ExtendedMetadata metadata,
IEnumerable<Attribute> attributes)
{
base.ModifyMetadata(metadata, attributes);
if (metadata.PropertyName == "TemplateCode")
{
metadata.TemplateHint = "AceEditor_handlebars";
metadata.ClientEditingClass = "aceeditor/aceEditor";
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment