sourceafEfan::EfanCompiler.fan

using afPlastic::PlasticCompilationErr
using afPlastic::PlasticClassModel
using afPlastic::PlasticCompiler
using afPlastic::SrcCodeSnippet

** Compiles efan templates into Fantom types. Compiled types extend `EfanRenderer` and have the 
** standard serialisation ctor:
** 
**    new make(|This|? f) { f?.call(this) }
** 
** This ensures you can create an instance of the render just by calling 'make()'. Call 'render()' 
** to render the efan template into a Str. 
** 
**    template := ...
**    efanType := EfanCompiler().compile(`index.efan`, template)
**    htmlStr  := efanType.make.render(...)
** 
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 EfanParser        parser 
    private const PlasticCompiler   plasticCompiler
        
    ** Create an 'EfanCompiler'.
    new make(|This|? in := null) {
        in?.call(this)
        parser          = EfanParser()      { it.srcCodePadding = this.srcCodePadding }
        plasticCompiler = PlasticCompiler() { it.srcCodePadding = this.srcCodePadding }
    }

    ** Standard compilation usage; the returned type extends `EfanRenderer`.
    ** 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.
    Type compile(Uri srcLocation, Str efanTemplate, Type? ctxType := null) {
        model   := PlasticClassModel(rendererClassName, true)
        return compileWithModel(srcLocation, efanTemplate, ctxType, model)
    }

    ** Intermediate compilation usage; the returned type extends `EfanRenderer`.
    ** The compiled renderer extends the given view helper mixins.
    ** 
    ** This method compiles a new Fantom Type so use judiciously to avoid memory leaks.
    ** 'srcLocation' is only used for Err msgs.
    Type compileWithHelpers(Uri srcLocation, Str efanTemplate, Type? ctxType := null, Type[] viewHelpers := Type#.emptyList) {
        model   := PlasticClassModel(rendererClassName, true)
        viewHelpers.each { model.extendMixin(it) }
        return compileWithModel(srcLocation, efanTemplate, ctxType, model)
    }
 
    ** Advanced compiler usage; the returned type extends `EfanRenderer`.
    ** The efan render methods are added to the given afPlastic model.
    ** 
    ** This method compiles a new Fantom Type so use judiciously to avoid memory leaks.
    ** 'srcLocation' is only used for Err msgs.
    Type compileWithModel(Uri srcLocation, Str efanTemplate, Type? ctxType, PlasticClassModel model) {
        if (!model.isConst)
            throw EfanErr(ErrMsgs.rendererModelMustBeConst(model))
 
        type        := (Type?) null
        ctxTypeSig  := (ctxType == null) ? "Obj?" : ctxType.signature
        renderCode  := "if (_ctx == null && ctxType != null && !ctxType.isNullable)\n"
        renderCode  += "    throw Err(\"${ErrMsgs.rendererCtxIsNull} \${ctxType.typeof.signature}\")\n"
        renderCode  += "if (_ctx != null && ctxType != null && !_ctx.typeof.fits(ctxType))\n"
        renderCode  += "    throw Err(\"ctx \${_ctx.typeof.signature} ${ErrMsgs.rendererCtxBadFit(ctxType)}\")\n"
        renderCode  += "\n"
        renderCode  += "${ctxTypeSig} ctx := _ctx\n"
        renderCode  += "\n"
        renderCode  += "_efanCtx := EfanRenderCtx.ctx(false) ?: EfanRenderCtx()\n"
        renderCode  += "_efanCtx.renderWithBuf(this, _af_code, _bodyFunc, _bodyObj) |->| {\n"
        renderCode  += parseIntoCode(srcLocation, efanTemplate)
        renderCode  += "}\n"
        
        model.usingType(EfanRenderCtx#)
        model.usingType(EfanErr#)
        model.extendMixin(EfanRenderer#)
        model.addField(Type?#, "_af_ctxType")
        model.overrideField(EfanRenderer#ctxType, "${ctxTypeSig}#", """throw Err("ctxType may not be set!")""")
        model.overrideMethod(EfanRenderer#_af_render, renderCode)
    
        try {
            type    = plasticCompiler.compileModel(model)

        } catch (PlasticCompilationErr err) {
            efanLineNo  := findEfanLineNo(err.srcCode.srcCode, err.errLineNo) ?: throw err
            srcCode := SrcCodeSnippet(srcLocation, efanTemplate)
            throw EfanCompilationErr(srcCode, efanLineNo, err.msg, srcCodePadding, err)
        }

        return type
    }

    ** 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.viewHelperMixinIsNotConst(it))
            if (!it.isPublic)
                throw EfanErr(ErrMsgs.viewHelperMixinIsNotPublic(it))
        }
        return viewHelpers
    }
    
    internal Str parseIntoCode(Uri srcLocation, Str efan) {
        data := EfanModel(efan.size)
        parser.parse(srcLocation, data, efan)
        code := data.toFantomCode
        
        return code
    }

    private Int? findEfanLineNo(Str[] fanCodeLines, Int errLineNo) {
        fanLineNo       := 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
    }
}