Last active
January 10, 2023 15:27
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file is just for naming |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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