sourceafEfan::EfanMeta.fan

using afPlastic::SrcCodeSnippet
using concurrent::AtomicRef
using concurrent::Actor

** Meta data about an efan template. 
** 
** Generated by the 'EfanCompiler'.
const class EfanMeta {

    ** The 'Type' of the compiled efan template.
    const Type type

    ** The generated fantom code of the efan template (for the inquisitive).
    const Str typeSrc

    ** Where the template originated from. Example, 'file://layout.efan'. 
    const Uri templateLoc

    ** The original efan template source string.
    const Str templateSrc
    
    ** The 'ctx' parameter type (if any) the template was compiled against.
    const Type? ctxType

    ** The name of the 'ctx' variable (if any) the template was compiled with. 
    const Str? ctxName
    
    ** The rendering method on 'type'.
    const Method renderMethod
    
    ** The key in 'Actor.locals' that stores the 'StrBuf' that efan renders to.
    ** See EFan
    internal
    const Str strBufKey
    
    // plastic will always need the concurrent pod (to store JVM unique pod name),
    // so we're not adding any extra dependencies here
    internal const AtomicRef    instanceRef := AtomicRef()
    internal const Int          srcCodePadding

    @NoDoc
    new make(|This|? in := null) {
        in?.call(this)
        if (null == this.type)          this.type           = Void#
        if (null == this.typeSrc)       this.typeSrc        = ""
        if (null == this.templateLoc)   this.templateLoc    = `wherever`
        if (null == this.templateSrc)   this.templateSrc    = ""
    }

    ** Renders the efan template.
    ** 
    ** More specifically, this creates an instance of 'type' (via 'Type.make') and calls the efan 
    ** render method with the given 'ctx' argument. 
    ** 
    ** Convenience for:
    ** 
    **   syntax: fantom
    **   renderFrom(instance, ctx)
    Str render(Obj? ctx) {
        renderFrom(instance, ctx)
    }

    ** Calls the render method on the given template. 
    ** Any errs thrown are wrapped in an 'EfanErr' that shows where in the efan template the error occurred.
    Str renderFrom(Obj instance, Obj? ctx) {
        if (instance.typeof.fits(type).not)
            throw ArgErr("Given instance does not fit template type: ${instance.typeof.qname} => ${type.qname}")

        // cater for nested renders (see TestLayout)
        old := Actor.locals[strBufKey]
        Actor.locals[strBufKey] = StrBuf()
        try return renderMethod.call(instance, ctx)
        catch (Err err)
            throw efanRuntimeErr(err)
        finally {
            if (old == null)
                Actor.locals.remove(strBufKey)
            else
                Actor.locals[strBufKey] = old
        }
    }
    
    ** Returns an instance of 'type' via 'type.make()'. 
    ** 
    ** If 'type' is const and no 'ctorParams' are specified then the instance is cached and returned in later invocations.
    Obj instance(Obj[]? ctorParams := null) {
        if ((ctorParams == null || ctorParams.isEmpty) && type.isConst) {
            if (instanceRef.val == null)
                instanceRef.val = type.make
            return instanceRef.val
        }
        return type.make(ctorParams)
    }
    
    ** Converts the given err to a 'EfanRuntimeErr' that shows where in the efan template the error occurred.
    ** If the given err can not be converted, it is returned as is.
    ** 
    ** For advanced use only.  
    @NoDoc
    Err efanRuntimeErr(Err cause) {
        regex   := Regex.fromStr("^\\s*?${type.qname}\\.${renderMethod.name}\\s\\(${type.pod.name}:([0-9]+)\\)\$")
        trace   := cause.traceToStr
        srcCodeLineNo := trace.splitLines.eachWhile |line -> Int?| {
            reggy   := regex.matcher(line)
            return reggy.find ? reggy.group(1).toInt : null
        }
        
        if (srcCodeLineNo == null)
            return cause

        templateLineNo  := findTemplateLineNo(typeSrc, srcCodeLineNo) ?: throw cause
        srcCodeSnippet  := SrcCodeSnippet(templateLoc, templateSrc)
        efanErr         := EfanRuntimeErr(srcCodeSnippet, templateLineNo, cause.msg, srcCodePadding, cause)
        
        // allow afxEfan to convert EfanErrs to AfxErrs
        newErr          := Env.cur.index("afEfan.errFn").eachWhile |qname| {
            try return Method.findMethod(qname, false)?.call(efanErr) as Err
            catch { /* Meh */ return null }
        } ?: efanErr
        
        return newErr
    }

    internal static Int? findTemplateLineNo(Str typeSrc, Int srcCodeLineNo) {
        fanLineNo       := srcCodeLineNo - 1    // from 1 to 0 based
        reggy           := Regex<|\s+?// \(efan\) --> ([0-9]+)$|>
        efanLineNo      := (Int?) null
        fanCodeLines    := typeSrc.splitLines
        
        while (fanLineNo > 0 && efanLineNo == null) {
            code := fanCodeLines[fanLineNo]
            reg := reggy.matcher(code)
            if (reg.find) {
                efanLineNo = reg.group(1).toInt
            } else {
                fanLineNo--
            }
        }
        return efanLineNo
    }
}