using afPlastic::PlasticCompilationErr
using afPlastic::PlasticClassModel
using afPlastic::PlasticCompiler
** Compiles efan templates into `EfanRenderer` classes.
** Call 'render()' to render the efan template into a Str.
**
** template := ...
** renderer := EfanCompiler().compile(`index.efan`, template)
** htmlStr := renderer.render(...)
**
const class EfanCompiler {
private const EfanParser parser
** The name given to the 'ctx' variable in the render method.
const Str ctxVarName := "ctx"
** The class name given to compiled efan renderer instances.
const Str rendererClassName := "EfanRendererImpl"
** Expose 'PlasticCompiler' so it (and it's mutable srcCodePadding value) may be re-used by
** other projects, such as [afSlim]`http://repo.status302.com/doc/afSlim/#overview`.
const PlasticCompiler plasticCompiler
** Create an 'EfanCompiler'.
new make(|This|? in := null) {
in?.call(this)
this.plasticCompiler = PlasticCompiler()
this.parser = EfanParser(plasticCompiler)
}
** For use by afIoc.
new makeWithServices(PlasticCompiler plasticCompiler, |This|? in := null) {
in?.call(this)
this.plasticCompiler = plasticCompiler
this.parser = EfanParser(plasticCompiler)
}
** Standard compilation usage; compiles a new renderer from the given efanTemplate.
** 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 reporting Err msgs.
EfanRenderer compile(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 efan render methods are added to the given
** [afPlastic]`http://repo.status302.com/doc/afPlastic/#overview` model.
**
** The (optional) 'makeFunc' is used to create an 'EfanRenderer' instance from the supplied Type
** and meta data.
**
** This method compiles a new Fantom Type so use judiciously to avoid memory leaks.
** 'srcLocation' is only used for reporting Err msgs.
EfanRenderer compileWithModel(Uri srcLocation, Str efanTemplate, Type? ctxType, PlasticClassModel model, |Type, EfanMetaData->EfanRenderer|? makeFunc := null) {
if (!model.isConst)
throw EfanErr(ErrMsgs.rendererModelMustBeConst(model))
// check the ctx type here so it also works from renderEfan()
renderType := (Type?) null
ctxTypeSig := (ctxType == null) ? "Obj?" : ctxType.signature
renderCode := "ctxType := efanMetaData.ctxType\n"
renderCode += "if (_ctx == null && ctxType != null && !ctxType.isNullable)\n"
renderCode += " throw afEfan::EfanErr(\"${ErrMsgs.rendererCtxIsNull} \${ctxType.typeof.signature}\")\n"
renderCode += "if (_ctx != null && ctxType != null && !_ctx.typeof.fits(ctxType))\n"
renderCode += " throw afEfan::EfanErr(\"ctx \${_ctx.typeof.signature} ${ErrMsgs.rendererCtxBadFit(ctxType)}\")\n"
renderCode += "${ctxTypeSig} ctx := _ctx\n"
renderCode += "\n"
renderCode += parseIntoCode(srcLocation, efanTemplate)
// 'cos it'll be common to want to cast to it - I did!
// I spent half an hour tracking down why my cast didn't work!
model.usingType(EfanRenderer#)
model.extendMixin(EfanRenderer#)
model.addField(EfanMetaData#, "_af_efanMetaData")
model.overrideField(EfanRenderer#efanMetaData, "_af_efanMetaData", """throw Err("efanMetaData is read only.")""")
model.overrideMethod(EfanRenderer#_af_render, renderCode)
// model.addMethod(StrBuf#, "_af_code", "", "afEfan::EfanRenderCtx.peek.renderBuf")
model.addField(Log#, "_af_log").withInitValue("afEfan::EfanRenderer#.pod.log")
model.addField(Obj?#, "_af_code", """throw Err("_af_code is write only.")""",
"""if (_af_log.isDebug)
_af_log.debug("[_af_code] \${afEfan::EfanCtxStack.peek.nestedId} -> \${it.toStr.toCode}")
afEfan::EfanRenderCtx.peek.renderBuf.add(it)""")
// we need the special syntax of "_af_eval = XXXX" so we don't have to close any brackets
// model.addField(Obj?#, "_af_eval", """throw Err("_af_eval is write only.")""", "_af_code.add(it)")
model.addField(Obj?#, "_af_eval", """throw Err("_af_eval is write only.")""",
"""if (_af_log.isDebug)
_af_log.debug("[_af_eval] \${afEfan::EfanCtxStack.peek.nestedId} -> \${it.toStr.toCode}")
afEfan::EfanRenderCtx.peek.renderBuf.add(it)""")
efanMetaData := EfanMetaData {
it.srcLocation = srcLocation
it.ctxName = ctxVarName
it.ctxType = ctxType
it.efanTemplate = efanTemplate
it.efanSrcCode = model.toFantomCode
it.srcCodePadding= plasticCompiler.srcCodePadding
}
try {
renderType = plasticCompiler.compileModel(model)
} catch (PlasticCompilationErr err) {
efanMetaData.throwCompilationErr(err, err.errLineNo)
}
efan := (makeFunc != null)
? makeFunc(renderType, efanMetaData)
: CtorPlanBuilder(renderType).set("_af_efanMetaData", efanMetaData).makeObj
return efan
}
** 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)
return data.toFantomCode
}
}