sourceafBedSheetMoustache::MoustacheTemplates.fan

using afConcurrent::SynchronizedFileMap
using afIoc::Inject
using afConcurrent::ActorPools
using afIocConfig::Config
using afPlastic::SrcCodeSnippet
using mustache::Mustache
using mustache::MustacheParser

** (Service) -
** A cache of 'Mustache' templates.
const mixin MoustacheTemplates {
    
    ** Renders a Moustache template.
    abstract Str renderFromStr(Str template, Obj? context := null, [Str:Mustache]? partials := null, Obj?[]? callStack := null, Str? indentStr := null)

    ** Renders a Moustache template. The template is cached for future use.
    abstract Str renderFromFile(File templateFile, Obj? context := null, [Str:Mustache]? partials := null, Obj?[]? callStack := null, Str? indentStr := null)

}

internal const class MoustacheTemplatesImpl : MoustacheTemplates {
    
    @Inject @Config { id="afMoustache.templateTimeout" }
    private const Duration templateTimeout
    
    private const SynchronizedFileMap cache
    
    new make(ActorPools actorPools, |This|in) { 
        in(this)
        cache = SynchronizedFileMap(actorPools["afBedSheetMoustache.fileCache"], templateTimeout)
    }
    
    override Str renderFromStr(Str template, Obj? context := null, [Str:Mustache]? partials := null, Obj?[]? callStack := null, Str? indentStr := null) {
        moustache := compile(`/rendered/from/str`, template)
        return moustache.render(context, partials ?: [:], callStack ?: [,], indentStr ?: Str.defVal)
    }

    override Str renderFromFile(File templateFile, Obj? context := null, [Str:Mustache]? partials := null, Obj?[]? callStack := null, Str? indentStr := null) {
        moustache := getTemplateFromFile(templateFile)
        return moustache.render(context, partials ?: [:], callStack ?: [,], indentStr ?: Str.defVal)
    }
    
    private Mustache getTemplateFromFile(File file) {
        cache.getOrAddOrUpdate(file) |->Obj| {
            return compile(file.normalize.uri, file.readAllStr)
        }
    }

    private Mustache compile(Uri loc, Str src) {
        try {
            return Mustache(src.in)
            
        } catch (ParseErr err) {
            // the (?s) is to allow '.' to match \n
            reg := Regex<|(?s)^Line ([0-9]+?): (.+)|>.matcher(err.msg)
            if (!reg.find)
                throw err
            
            line    := reg.group(1).toInt
            msg     := reg.group(2).splitLines.join.replace("\t", " ")  // take out the new line chars
            srcCode := SrcCodeSnippet(loc, src)
            throw MoustacheErr(srcCode, line, msg, 5)
        }
    }
}