sourceafEfan::EfanCompiler.fan

using compiler::CompilerErr

** Compiles efan templates into Fantom code; maybe used outside of [afIoc]`http://repo.status302.com/doc/afIoc/#overview`.
const class EfanCompiler {
    
    ** The name given to the 'ctx' variable in the render method. 
    public const  Str                   ctxVarName          := "ctx"
    
    ** When generating code snippets to report compilation Errs, this is the number of lines of src 
    ** code the erroneous line should be padded with.  
    public const  Int                   srcCodePadding      := 5 
    
    private const Str                   rendererClassName   := "EfanRenderer"  
    private const PlasticPodCompiler    podCompiler         := PlasticPodCompiler() 
    private const EfanParser            parser              := EfanParser() 
    
    ** Create an 'EfanCompiler'.
    new make(|This|? in := null) {
        in?.call(this)

        // create new services for non-afIoc projects
        if (podCompiler == null)
            podCompiler = PlasticPodCompiler()
        if (parser == null)
            parser = EfanParser()
    }

    ** Compiles a new renderer from the given efanTemplate.
    ** This method compiles a new Fantom Type so use judiciously to avoid memory leaks.
    ** 'srcLocation' is only used for Err msgs.
    EfanRenderer compile(Uri srcLocation, Str efanTemplate, Type? ctxType, Type[] viewHelpers := Type#.emptyList) {
        model   := PlasticClassModel(rendererClassName, false)
        viewHelpers.each { model.extendMixin(it) }

        model.usingType(EfanRenderer#)
        model.usingType(EfanBodyRenderer#)
        
        model.addField(|EfanBodyRenderer t|?#,  "_bodyFunc")
        model.addField(EfanBodyRenderer?#,      "_bodyObj")
        model.addField(StrBuf#,                 "_afCode")
        
        model.addMethod(EfanBodyRenderer#, "renderEfan", "EfanRenderer renderer, Obj? ctx", "EfanBodyRenderer(renderer, ctx, _afCode)")
        model.addMethod(Void#, "renderBody", Str.defVal, "_bodyFunc?.call(_bodyObj)")
        
        type        := (Type?) null
        renderCode  := parseIntoCode(srcLocation, efanTemplate)
        renderSig   := (ctxType == null) ? "" : "${ctxType.qname} ${ctxVarName}"
        model.addMethod(Str#, "render", renderSig, renderCode)
        
        try {
            type    = compileCode(model.toFantomCode, rendererClassName)

        } catch (PlasticCompilationErr err) {
            efanLineNo  := findEfanLineNo(err.srcErrLoc) ?: throw err
            efanErrLoc  := SrcErrLocation(srcLocation, efanTemplate, efanLineNo, err.msg)
            throw EfanCompilationErr(efanErrLoc, srcCodePadding, err)
        }

        return EfanRenderer(type, ctxType, efanTemplate.size)       
    }

    ** Called by afbedSheetEfan - ensures all given ViewHelper types are valid. 
    @NoDoc
    static Type[] validateViewHelpers(Type[] viewHelpers) {
        viewHelpers.each { 
            if (!it.isMixin)
                throw EfanErr(ErrMsgs.viewHelperMixinIsNotMixin(it))
            if (it.isConst)
                throw EfanErr(ErrMsgs.viewHelperMixinIsConst(it))
            if (!it.isPublic)
                throw EfanErr(ErrMsgs.viewHelperMixinIsNotPublic(it))
        }
        return viewHelpers
    }
    
    internal Str parseIntoCode(Uri srcLocation, Str efan) {
        data := EfanModel(efan.size, "_afCode.add")
        parser.parse(srcLocation, data, efan)
        return data.toFantomCode
    }

    private Type compileCode(Str fanCode, Str className) {
        pod     := podCompiler.compile(fanCode)
        type    := pod.type(className)
        return type
    }
    
    private Int? findEfanLineNo(SrcErrLocation plasticErrLoc) {
        fanCodeLines    := plasticErrLoc.srcCode
        fanLineNo       := plasticErrLoc.errLineNo - 1  // from 1 to 0 based
        reggy           := Regex<|^\s+?// --> ([0-9]+)$|>
        efanLineNo      := (Int?) null
        
        while (fanLineNo > 0 && efanLineNo == null) {
            code := fanCodeLines[fanLineNo]
            reg := reggy.matcher(code)
            if (reg.find) {
                efanLineNo = reg.group(1).toInt
            } else {
                fanLineNo--
            }
        }
        
        return efanLineNo
    }
}