sourceafSlim::Slim.fan

using afPlastic
using afEfan

** (Service) - 
** Non-caching service methods for parsing and compiling Slim templates efan templates, and for rendering HTML.
** 
** For further information on the 'ctx' parameter, see 
** [efan: Template Context]`http://eggbox.fantomfactory.org/pods/afEfan/doc#templateContext`
** 
** Note: This class is available as a service in IoC v3 under the 'root' scope with an ID of 'afSlim::Slim'.
const class Slim {
    
    ** The void tag ending style for compiled templates
            const TagStyle tagStyle

    private const EfanCompiler      efanCompiler    := EfanCompiler()
    private const SlimParser        slimParser
    private const SlimComponent[]   components
    private const Method            localeMeth
    
    ** Creates a 'Slim' instance, setting the ending style for tags.
    ** 
    ** Default opts:
    ** 
    ** pre>
    ** table:
    ** name            default            desc
    ** --------------  -----------------  ----
    ** 'tagStyle'      'TagStyle.html'    Describes how tags are ended.
    ** 'components'    'SlimComponent[]'  SlimComponents to use.
    ** 'localeMethod'  'Slim#localeFn'    The static method used to obtain L10N translations.
    ** <pre
    new make([Str:Obj]? opts := null) {
        // this opts ctor makes this Slim class an easy AFX service to contribute to! 
        this.tagStyle   = (opts?.get("tagStyle"     ) as TagStyle)          ?: TagStyle.html
        this.components = (opts?.get("components"   ) as SlimComponent[])   ?: SlimComponent#.emptyList
        this.localeMeth = (opts?.get("localeMethod" ) as Method)            ?: #localeFn
        this.slimParser = SlimParser(tagStyle, this.components, localeMeth)
    }
    
    ** Parses the given slim template into an efan template.
    ** 
    ** 'srcLocation' may anything - used for meta information only.
    Str parseFromStr(Str slimTemplate, Uri? srcLocation := null) {
        srcLocation =  srcLocation ?: `from/slim/template`
        tree := SlimLineRoot()
        slimParser.parse(srcLocation, slimTemplate, tree)
        buf  := StrBuf(slimTemplate.size)
        efan := tree.toEfan(buf).toStr.trim

        if (efan.startsWith("%>"))
            efan = efan[2..-1]
        if (efan.endsWith("<%#"))
            efan = efan[0..-4]
        
        return efan
    }

    ** Parses the given slim file into an efan template.
    Str parseFromFile(File slimFile) {
        srcLocation :=  slimFile.normalize.uri
        return parseFromStr(slimFile.readAllStr, srcLocation)
    }

    ** Compiles a renderer from the given slim template.
    ** 
    ** 'srcLocation' may be anything - it is used for meta information only.
    EfanMeta compileFromStr(Str slimTemplate, Type? ctxType := null, Type[]? viewHelpers := null, Uri? srcLocation := null) {
        srcLocation =  srcLocation ?: `from/slim/template`
        efan        := this.parseFromStr(slimTemplate, srcLocation)
        template    := efanCompiler.compile(srcLocation, efan, ctxType, viewHelpers ?: Type#.emptyList)
        return template
    }

    ** Compiles a renderer from the given slim file.
    EfanMeta compileFromFile(File slimFile, Type? ctxType := null, Type[]? viewHelpers := null) {
        srcLocation := slimFile.normalize.uri
        efan        := this.parseFromStr(slimFile.readAllStr, srcLocation)
        template    := efanCompiler.compile(srcLocation, efan, ctxType, viewHelpers ?: Type#.emptyList)
        return template
    }

    ** Renders the given slim template into HTML.
    ** 
    ** 'srcLocation' may anything - used for meta information only.
    Str renderFromStr(Str slimTemplate, Obj? ctx := null, Type[]? viewHelpers := null, Uri? srcLocation := null) {
        template := this.compileFromStr(slimTemplate, ctx?.typeof, viewHelpers, srcLocation)
        return template.render(ctx)
    }

    ** Renders the given slim template file into HTML.
    Str renderFromFile(File slimFile, Obj? ctx := null, Type[]? viewHelpers := null) {
        template := this.compileFromFile(slimFile, ctx?.typeof, viewHelpers)
        return template.render(ctx)
    }
    
    ** Returns a locale string for the given key and args.
    ** Translations are looked up in the given pod (may be a 'Pod', 'Str', 'Type', or instance).
    ** 
    ** If 'locale' is null, the thread's current Locale is used. 
    ** 
    ** Args may be interpolated with '${1}'. For more than 4 args, pass a list as 'arg1'.
    ** For named args, pass a Map as 'arg1' - '${someKey}'
    static Str localeStr(Obj poddy, Str key, Locale? locale := null, Obj? arg1 := null, Obj? arg2 := null, Obj? arg3 := null, Obj? arg4 := null) {
        // look for a pod that may contain the translation - ignoring afPlastic abominations
        pod := poddy as Pod
        
        if (pod == null && poddy is Str)
            pod = Pod.find(poddy, false)

        if (pod == null) {
            type := poddy as Type
            if (type == null)
                type = poddy.typeof
            while (type.pod.name.startsWith("afPlastic"))
                type = type.base
            pod = type.pod
        }

        // Env.cur.locale handles the fallbacks as needed
        locale = locale ?: Locale.cur
        str := Env.cur.locale(pod, key, null, locale)

        // backdoor for testing - let poddy also be a Map
        if (str == null && poddy is Map)
            str = ((Str:Str) poddy)["${key}.${locale}"]

        if (str == null) {
            pod.log.warn("Could NOT find locale str - ${pod.name}::${key}")
            return "???"
        }
        
        if (arg1 != null && arg1 is List == false && arg1 is Map == false)
            str = str.replace("\${1}", arg1.toStr)
        if (arg2 != null)
            str = str.replace("\${2}", arg2.toStr)
        if (arg3 != null)
            str = str.replace("\${3}", arg3.toStr)
        if (arg4 != null)
            str = str.replace("\${4}", arg4.toStr)
        
        if (arg1 is List) {
            list    := (Obj?[]) arg1
            list.each |Obj? obj, Int index| {
                x := index + 1 // F4 forced me!
                objNotNull := obj ?: "null"  
                str = str.replace("\${$x}", objNotNull.toStr)
            }
        }
        if (arg1 is Map) {
            map     := (Str:Obj?) arg1
            map.each |Obj? value, Str keyStr|{
                valueNotNull := value ?: "null" 
                str = str.replace("\${$keyStr}", valueNotNull.toStr)
            }
        }
        
        return str
    }
    
    @NoDoc
    static Str localeFn(Obj poddy, Str key, Obj? arg1 := null, Obj? arg2 := null, Obj? arg3 := null, Obj? arg4 := null) {
        // defer to localeStr() as it is more useful / generic as it also takes a Locale
        localeStr(poddy, key, null, arg1, arg2, arg3, arg4)
    }
    
    Str debugStr() {
        buf := StrBuf()
        max := components.reduce(32) |Int max, component->Int| {  max.max(component.name.size) } as Int
        max  = (max + 1).min(128)   
        
        buf.add("Tag Style:").addChar('\n').add("  ").add(tagStyle).addChar('\n').addChar('\n')
        
        buf.add("Components:").addChar('\n')
        components.each |component| {
            buf.add("  ").add(component.name)
            
            pad := "." * (max - component.name.size)
            buf.addChar(' ').add(pad).add(" : ").add(component.typeof.qname)
            
            if (component.typeof.method("toStr").isOverride)
                buf.add(" - ").add(component)
            buf.addChar('\n')
        }
        if (components.isEmpty)
            buf.add("  none\n")
        buf.addChar('\n')

        buf.add("Locale Method:").addChar('\n').add("  ").add(localeMeth)
        
        return buf.toStr
    }
    
    override Str toStr() {
        this.debugStr
    }
}