sourceafEfan::EfanParser.fan

using afPlastic::PlasticCompiler
using afPlastic::SrcCodeSnippet

** Parses efan template strings into Fantom code.
const class EfanParser {

    ** When generating code snippets for parsing Errs, this is the number of src code lines 
    ** the erroneous line will be padded with. 
    const Int       srcCodePadding      := 5

    ** Controls whether 'code' only lines are trimmed to remove (usually) unwanted line breaks.  
    const Bool      removeWhitespace    := true
    
    ** Name of the field that the generated template will be added to.
    const Str       fieldName           := "_efan_output"
    
    // don't contribute these, as currently, there are a lot of assumptions around <%# starting with <%
    private static const Str tokenEscapeStart       := "<%%"
    private static const Str tokenEscapeEnd         := "%%>"
    private static const Str tokenFanCodeStart      := "<%"
    private static const Str tokenCommentStart      := "<%#"
    private static const Str tokenEvalStart         := "<%="
    private static const Str tokenInstructionStart  := "<%?"
    private static const Str tokenEnd               := "%>"
    
    ** Standard it-block ctor. Use to set field values:
    ** 
    **   syntax: fantom
    **   parser := EfanParser {
    **       it.srcCodePadding   = 5
    **       it.removeWhitespace = true
    **       it.fieldName       = "_efan_output"
    **   }
    new make(|This|? f := null) { f?.call(this) }

    ** Parses the given 'efan' template to Fantom code.
    ParseResult parse(Uri srcLocation, Str efanTemplate) {
        srcSnippet  := SrcCodeSnippet(srcLocation, efanTemplate)
        efanModel   := EfanModel(srcSnippet, srcCodePadding, fieldName)
        doParse(srcLocation, efanModel, efanTemplate)
        return ParseResult {
            it.fantomCode   = efanModel.toFantomCode
            it.usings       = efanModel.usings
            it.fieldName    = this.fieldName
        }
    }
    
    internal Void doParse(Uri srcLocation, Pusher pusher, Str efanCode) {
        efanIn  := efanCode.in
        data    := ParserData(pusher, efanCode, removeWhitespace)
        while (efanIn.peekChar != null) {
            // escape chars can be in both text and blocks
            if (peekEq(efanIn, tokenEscapeStart)) {
                data.addChar('<').addChar('%')
                continue
            }

            if (peekEq(efanIn, tokenEscapeEnd)) {
                data.addChar('%').addChar('>')
                continue
            }

            if (data.inText && peekEq(efanIn, tokenCommentStart)) {
                data.push
                data.enteringComment
                continue
            }

            if (data.inText && peekEq(efanIn, tokenEvalStart)) {
                data.push
                data.enteringEval
                continue
            }

            if (data.inText && peekEq(efanIn, tokenInstructionStart)) {
                data.push
                data.enteringInstruction
                continue
            }

            if (data.inText && peekEq(efanIn, tokenFanCodeStart)) {
                data.push
                data.enteringFanCode
                continue
            }

            if (data.inBlock && peekEq(efanIn, tokenEnd)) {
                data.push
                data.exitingBlock
                continue
            }

            char := efanIn.readChar

            newLine := (char == '\n')
            
            // normalise new lines in blocks (leave template text as is)
            if (char == '\r') {
                newLine = true
                if (data.inBlock) {
                    if (peekEq(efanIn, "\n")) { }
                    char = '\n'
                } else {
                    if (peekEq(efanIn, "\n")) { 
                        data.addChar(char)
                        char = '\n'
                    }                   
                }
            }

            data.addChar(char)

            if (newLine) {
                if (!data.inBlock) {
                    data.push
                    data.flush
                }
                data.newLine
            }
        }
        
        if (data.inBlock) {
            errMsg  := "${data.blockType.name.toDisplayName} block not closed."
            srcCode := SrcCodeSnippet(srcLocation, efanCode)
            throw EfanParserErr(srcCode, efanCode.splitLines.size, errMsg, srcCodePadding)
        }

        data.push
        data.flush
    }

    ** If tag is next, consume it and return true
    private Bool peekEq(InStream in, Str tag) {
        p1 := in.readChar
        if (p1 == null) {
            return false
        }
        if (p1 != tag[0]) {
            in.unreadChar(p1)
            return false
        }
        if (tag.size == 1)
            return true

        p2 := in.readChar
        if (p2 == null) {
            in.unreadChar(p1)
            return false
        }
        if (p2 != tag[1]) {
            in.unreadChar(p2)
            in.unreadChar(p1)
            return false
        }
        if (tag.size == 2)
            return true
        
        p3 := in.readChar
        if (p3 == null) {
            in.unreadChar(p2)
            in.unreadChar(p1)
            return false
        }
        if (p3 != tag[2]) {
            in.unreadChar(p3)
            in.unreadChar(p2)
            in.unreadChar(p1)
            return false
        }
        if (tag.size == 3)
            return true
        
        throw UnsupportedErr("efan tags must be < 3 chars: $tag")

        // use the above hardcoded logic for speed
//      if (buf.avail < tag.size)
//          return false
//
//      peek := buf.readChars(tag.size)
//      if (peek == tag) {
//          return true
//      } else {
//          // BugFix: buf.seek doesn't take into account char encoding
//          peek.eachr { buf.unreadChar(it) }
//          return false
//      }
    }
}

** Contains Fantom code; the result of parsing efan templates.
const class ParseResult {
    
    ** Fantom src code.
    const Str   fantomCode
    
    ** List of 'using' statements.
    const Str[] usings
    
    ** Name of the 'StrBuf' variable / field that the generated template will be added to.
    const Str fieldName
    
    internal new make(|This| in) { in(this) }
}

internal class ParserData {
    private Pusher      pusher
    private StrBuf      buf
            BlockType   blockType       := BlockType.text
    private Int         lineNo          := 1
    private Int         lineNoToSend    := 1    // needed 'cos of multilines
    private Push[]      pushes          := [,]
    private Bool        removeWs

    new make(Pusher pusher, Str efanCode, Bool removeWhitespace) {
        this.pusher     = pusher
        this.buf        = StrBuf(efanCode.size)
        this.removeWs   = removeWhitespace
    }
    
    This addChar(Int char) {
        buf.addChar(char)
        return this
    }
    
    Void enteringFanCode() {
        blockType = BlockType.fanCode
    }
    Void enteringEval() {
        blockType = BlockType.eval
    }
    Void enteringComment() {
        blockType = BlockType.comment
    }
    Void enteringInstruction() {
        blockType = BlockType.instruction
    }
    Void exitingBlock() {
        pusher.onExit(lineNoToSend, blockType)
        lineNoToSend = lineNo
        blockType = BlockType.text
    }
    Bool inBlock() {
        blockType != BlockType.text
    }
    Bool inText() {
        blockType == BlockType.text
    }
    Void newLine() {
        flush
        lineNo++
    }
    Void push() {
        push := null as Push
        switch (blockType) {
            case BlockType.text         : push = Push(lineNo,       Pusher#onText)
            case BlockType.comment      : push = Push(lineNoToSend, Pusher#onComment)
            case BlockType.fanCode      : push = Push(lineNoToSend, Pusher#onFanCode)
            case BlockType.instruction  : push = Push(lineNoToSend, Pusher#onInstruction)
            case BlockType.eval         : push = Push(lineNoToSend, Pusher#onEval)
            default                     : throw Err("Unknown BlockType: $blockType")
        }
        push.blockType  = blockType
        push.line       = buf.toStr
        pushes.add(push)
        
        lineNoToSend = lineNo
        buf.clear
    }
    Void flush() {

        // do dat intelligent whitespace removal - only clear lines WITH non-text blocks!
        if (removeWs) {
            allEmptyOrClear := true
            anyCanClear     := false
            i               := 0
            while (i < pushes.size && allEmptyOrClear == true) {
                push := pushes[i++]
                if (!push.isEmpty && !push.canClear)
                    allEmptyOrClear = false
                if (push.canClear)
                    anyCanClear = true
            }
            if (allEmptyOrClear && anyCanClear)
                pushes = pushes.exclude(Push#isEmpty.func)
        }
        // the un-optimised code for above
//      if (removeWs && pushes.all { it.isEmpty || it.canClear } && pushes.any { it.canClear })
//          pushes = pushes.exclude { it.isEmpty }
        
        for (i := 0; i < pushes.size; ++i) {
            pushes[i].push(pusher)
        }
        pushes.clear
    }
}

internal class Push {
    static const BlockType[]    clearables := [BlockType.comment, BlockType.fanCode, BlockType.instruction]
    Method      method
    Int         lineNo
    BlockType?  blockType
    Str?        line
    new make(Int lineNo, Method method) {
        this.lineNo = lineNo
        this.method = method
    }
    Bool isEmpty() {
        blockType == BlockType.text && line.all(Int#isSpace.func)
    }
    Bool canClear() {
         clearables.contains(blockType)
    }
    Void push(Pusher pusher) {
        method.call(pusher, lineNo, line)
    }
}

internal enum class BlockType {
    text, comment, fanCode, eval, instruction; 
}

internal mixin Pusher {
    abstract Void onFanCode     (Int lineNo, Str fanCode)
    abstract Void onComment     (Int lineNo, Str comment)
    abstract Void onText        (Int lineNo, Str text)
    abstract Void onEval        (Int lineNo, Str fanCode)
    abstract Void onInstruction (Int lineNo, Str instruction)
    abstract Void onExit        (Int lineNo, BlockType blockType)
}