diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 158648f..70e91bd 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -23,7 +23,6 @@ type ISession with [] let MESSAGES = "messages" - /// The HTTP item key for loading the session let private sessionLoadedKey = "session-loaded" @@ -132,6 +131,7 @@ let redirectToGet url : HttpHandler = fun _ ctx -> task { } /// The MIME type for podcast episode JSON chapters +[] let JSON_CHAPTERS = "application/json+chapters" @@ -185,10 +185,10 @@ let viewForTheme themeId template next ctx (viewCtx: AppViewContext) = task { // Render view content... match! Template.Cache.get themeId template ctx.Data with | Ok contentTemplate -> - let forLayout = { updated with Content = Template.render contentTemplate updated } + let forLayout = { updated with Content = Template.render contentTemplate updated ctx.Data } // ...then render that content with its layout match! Template.Cache.get themeId (if isHtmx ctx then "layout-partial" else "layout") ctx.Data with - | Ok layoutTemplate -> return! htmlString (Template.render layoutTemplate forLayout) next ctx + | Ok layoutTemplate -> return! htmlString (Template.render layoutTemplate forLayout ctx.Data) next ctx | Error message -> return! Error.server message next ctx | Error message -> return! Error.server message next ctx } @@ -199,7 +199,7 @@ let bareForTheme themeId template next ctx viewCtx = task { let withContent = task { if updated.Content = "" then match! Template.Cache.get themeId template ctx.Data with - | Ok contentTemplate -> return Ok { updated with Content = Template.render contentTemplate updated } + | Ok contentTemplate -> return Ok { updated with Content = Template.render contentTemplate updated ctx.Data } | Error message -> return Error message else return Ok viewCtx @@ -210,7 +210,7 @@ let bareForTheme themeId template next ctx viewCtx = task { match! Template.Cache.get themeId "layout-bare" ctx.Data with | Ok layoutTemplate -> return! - (messagesToHeaders completeCtx.Messages >=> htmlString (Template.render layoutTemplate completeCtx)) + (messagesToHeaders completeCtx.Messages >=> htmlString (Template.render layoutTemplate completeCtx ctx.Data)) next ctx | Error message -> return! Error.server message next ctx | Error message -> return! Error.server message next ctx diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 4dc7880..579909e 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -27,7 +27,7 @@ type WebLogMiddleware(next: RequestDelegate, log: ILogger) = /// Middleware to check redirects for the current web log -type RedirectRuleMiddleware(next: RequestDelegate, log: ILogger) = +type RedirectRuleMiddleware(next: RequestDelegate, _log: ILogger) = /// Shorthand for case-insensitive string equality let ciEquals str1 str2 = diff --git a/src/MyWebLog/Template.fs b/src/MyWebLog/Template.fs index b41fe0f..568203c 100644 --- a/src/MyWebLog/Template.fs +++ b/src/MyWebLog/Template.fs @@ -1,8 +1,14 @@ module MyWebLog.Template +open System +open System.Collections.Generic +open System.IO +open System.Text open Fluid open Fluid.Values open Giraffe.ViewEngine +open Microsoft.AspNetCore.Antiforgery +open Microsoft.Extensions.FileProviders open MyWebLog open MyWebLog.ViewModels @@ -48,10 +54,21 @@ module private Helpers = /// Fluid template options customized with myWebLog filters -let options = +let options () = let sValue = StringValue >> VTask + let it = TemplateOptions.Default it.MemberAccessStrategy.MemberNameStrategy <- MemberNameStrategies.SnakeCase + [ // Domain types + typeof; typeof; typeof; typeof; typeof; typeof + typeof; typeof + // View models + typeof; typeof; typeof; typeof; typeof + typeof; typeof + // Framework types + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof ] + |> List.iter it.MemberAccessStrategy.Register // A filter to generate an absolute link it.Filters.AddFilter("absolute_link", fun input _ ctx -> sValue (permalink input ctx.App.WebLog.AbsoluteUrl)) @@ -132,12 +149,14 @@ let options = // A filter to retrieve the value of a meta item from a list // (shorter than `{% assign item = list | where: "Name", [name] | first %}{{ item.value }}`) it.Filters.AddFilter("value", - fun input args _ -> - let items = input.ToObjectValue() :?> MetaItem list - let name = args.At(0).ToStringValue() - match items |> List.tryFind (fun it -> it.Name = name) with - | Some item -> item.Value - | None -> $"-- {name} not found --" + fun input args ctx -> + let name = args.At(0).ToStringValue() + let picker (value: FluidValue) = + let item = value.ToObjectValue() :?> MetaItem + if item.Name = name then Some item.Value else None + (input :?> ArrayValue).Values + |> Seq.tryPick picker + |> Option.defaultValue $"-- {name} not found --" |> sValue) it @@ -234,11 +253,13 @@ let parser = it + +open MyWebLog.Data + /// Cache for parsed templates module Cache = open System.Collections.Concurrent - open MyWebLog.Data /// Cache of parsed templates let private _cache = ConcurrentDictionary () @@ -277,6 +298,37 @@ module Cache = _cache.Clear() +/// A file provider to retrieve files by theme +type ThemeFileProvider(themeId: ThemeId, data: IData) = + + interface IFileProvider with + + member _.GetDirectoryContents _ = + raise <| NotImplementedException "The theme file provider does not support directory listings" + + member _.GetFileInfo path = + match data.Theme.FindById themeId |> Async.AwaitTask |> Async.RunSynchronously with + | Some theme -> + match theme.Templates |> List.tryFind (fun t -> t.Name = path) with + | Some template -> + { new IFileInfo with + member _.Exists = true + member _.IsDirectory = false + member _.LastModified = DateTimeOffset.Now + member _.Length = int64 template.Text.Length + member _.Name = template.Name.Split '/' |> Array.last + member _.PhysicalPath = null + member _.CreateReadStream() = + new MemoryStream(Encoding.UTF8.GetBytes template.Text) } + | None -> NotFoundFileInfo path + | None -> NotFoundFileInfo path + + member _.Watch _ = + raise <| NotImplementedException "The theme file provider does not support watching for changes" + + /// Render a template to a string -let render (template: IFluidTemplate) (viewCtx: AppViewContext) = - template.Render(TemplateContext(viewCtx, options, true)) +let render (template: IFluidTemplate) (viewCtx: AppViewContext) data = + let opts = options () + opts.FileProvider <- ThemeFileProvider(viewCtx.WebLog.ThemeId, data) + template.Render(TemplateContext(viewCtx, opts, true)) diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json index 207a6c5..3c2eae7 100644 --- a/src/MyWebLog/appsettings.json +++ b/src/MyWebLog/appsettings.json @@ -1,5 +1,5 @@ { - "Generator": "myWebLog 2.2", + "Generator": "myWebLog 3", "Logging": { "LogLevel": { "MyWebLog.Handlers": "Information"