sourceafFandoc::HtmlDocWriter.fan

using fandoc::Code
using fandoc::Doc
using fandoc::DocElem
using fandoc::DocText
using fandoc::DocNodeId
using fandoc::DocWriter
using fandoc::Para
using fandoc::Heading
using fandoc::Image
using fandoc::Link
using fandoc::OrderedList
using fandoc::FandocParser

@Js
class HtmlDocWriter : DocWriter { 
    Log                 log                         := typeof.pod.log
    DocNodeId:Str       cssClasses                  := DocNodeId:Str[:] { it.def = "" }
    LinkResolver[]      linkResolvers               := LinkResolver[,]
    ElemProcessor[]     linkProcessors              := ElemProcessor[,]
    ElemProcessor[]     imageProcessors             := ElemProcessor[,]
    ElemProcessor[]     paraProcessors              := ElemProcessor[,]
    Str:PreProcessor    preProcessors               := Str:PreProcessor[:]
    ElemProcessor?      invalidLinkProcessor
    protected StrBuf    str                         := StrBuf()
    protected HtmlNode? htmlNode

    ** A simple HTML writer that mimics the original; no invalid links and no pre-block-processing.
    static HtmlDocWriter original() {
        HtmlDocWriter {
            it.linkResolvers = [
                LinkResolver.passThroughResolver,
            ]
        }
    }
    
    ** A HTML writer that performs pre-block-processing for tables and syntax colouring.
    ** 
    ** (EveryLayout processors are preferred over BootStrap.)
    static HtmlDocWriter fullyLoaded() {
        HtmlDocWriter {
            hdw := it
            it.invalidLinkProcessor = ElemProcessor.invalidLinkProcessor
            it.linkResolvers    = [
                LinkResolver.schemePassThroughResolver,
                LinkResolver.pathAbsPassThroughResolver,
                LinkResolver.idPassThroughResolver,
                LinkResolver.cssLinkResolver |Str? scheme, Uri url -> Uri?| {
                    hdw.linkResolvers.eachWhile { it.resolve(scheme, url) }
                },
                FandocLinkResolver(),
                LinkResolver.javascriptErrorResolver,
                LinkResolver.passThroughResolver,
            ]
            it.linkProcessors   = [
                ExternalLinkProcessor(),
                CssLinkProcessor(),
                MailtoProcessor("data-unscramble"),
                PdfLinkProcessor(),
            ]
            it.paraProcessors   = [
                CssPrefixProcessor(),
            ]
            it.imageProcessors  = [
                Html5VideoProcessor(),
                VimeoElProcessor(),
                YouTubeElProcessor(),
            ]
            it.preProcessors    = [
                "table"         : TableProcessor(),
                "html"          : PreProcessor.htmlProcessor,
                "div"           : DivProcessor(it),
            ]
            if (Env.cur.runtime != "js")
                it.preProcessors["syntax"] = SyntaxProcessor()
        }
    }
    
    ** Writes the given elem to a string.
    Str writeToStr(DocElem elem) {
        olds := str
        oldn := htmlNode
        buf  := StrBuf()
        str   = buf
        htmlNode = null
        elem.write(this)
        str   = olds
        htmlNode = oldn
        return buf.toStr
    }

    ** Parses the given string into a Fandoc document.
    ** Document headers are automatically parsed if they're supplied.
    ** Parsing errors are inserted into the start of the documents.
    Doc parse(Str fandoc, Str? loc := null) {
        // auto-detect headers - no legal fandoc should start with ***** unless it's a header!
        parser := FandocParser() { it.parseHeader = fandoc.trim.startsWith("*****"); silent = true }
        doc    := parser.parse(loc ?: "afFandoc", fandoc.in, true)
        if (parser.errs.size > 0) {
            lines := fandoc.splitLines
            msg   := "Fandoc errors" + (loc == null ? "" : " in ${loc}") + ":\n"

            // prepending errors is too specific to generalise in to afFandoc.
            parser.errs.eachr |err| {
                errLine := lines.getSafe(err.line, "").toCode
                p := Para().add(DocText("${err} - ")).add(Code().add(DocText(errLine))) { it.admonition = "parseErr" }
                doc.insert(0, p)
                msg += " - ${err} - ${errLine}\n"
            }
            log.warn(msg)
        }
        
        return doc
    }

    ** Writes the given fandoc to a string.
    Str parseAndWriteToStr(Str fandoc, Str? loc := null) {
        doc := parse(fandoc, loc)
        return writeToStr(doc)
    }   

    @NoDoc
    override Void docStart(Doc doc) { }
    
    @NoDoc
    override Void docEnd(Doc doc) { }
    
    @NoDoc
    override Void elemStart(DocElem elem) {
        if (elem.id == DocNodeId.doc) return
        cur := toHtmlNode(elem)
        htmlNode?.add(cur)
        htmlNode = cur
    }

    @NoDoc
    override Void elemEnd(DocElem elem) {
        if (elem.id == DocNodeId.doc) return
        if (htmlNode == null) return

        cur := htmlNode
        par := htmlNode?.parent
        res := null as Obj

        if (cur is HtmlElem) {
            switch (elem.id) {
                case DocNodeId.image    : res = processImage(cur)
                case DocNodeId.link     : res = processLink (cur)
                case DocNodeId.para     : res = processPara (cur)
                case DocNodeId.pre      : res = processPre  (cur)
            }
            
            if (res != null && res !== cur) {
                if (res isnot Str && res isnot HtmlNode)
                    throw UnsupportedErr("Unknown HtmlNode: ${res.typeof}")
                if (res is Str)
                    res = HtmlText(res, true)
                cur = cur.replaceWith(res)
            }
        }
        
        if (par == null) {
            cur.print(str.out)
            // this \n makes debugging the HTML source SOOO much easier! 
            str.addChar('\n')
        }
        
        htmlNode = par
    }

    @NoDoc
    override Void text(DocText docText) {
        htmlNode.elem.addText(docText.str)
    }   
    
    Str toHtml() { str.toStr }
    
    // ----

    virtual Obj? processImage(HtmlElem elem) {
        imageProcessors.eachWhile { it.process(elem) }
    }

    virtual Obj? processLink(HtmlElem elem) {
        linkProcessors.eachWhile { it.process(elem) }
    }
    
    virtual Obj? processPara(HtmlElem elem) {
        paraProcessors.eachWhile { it.process(elem) }
    }

    virtual Obj? processPre(HtmlElem elem) {
        body    := elem.text
        idx     := body.index("\n") ?: -1
        cmdTxt  := body[0..idx].trim
        cmd     := Uri(cmdTxt, false)       

        if (cmd?.scheme != null && preProcessors.containsKey(cmd.scheme)) {
            str     := StrBuf()
            preText := body[idx..-1]
            replace := preProcessors[cmd.scheme].process(elem, cmd, preText)
            return replace
        }
        return elem
    }

    virtual HtmlNode toHtmlNode(DocElem elem) {
        html := HtmlElem(elem.htmlName)
        
        html.id = elem.anchorId
        
        switch (elem.id) {
            case DocNodeId.para:
                para := (Para) elem
                if (para.admonition != null) {
                    admon := para.admonition.all { it.isUpper } ? para.admonition.lower : para.admonition
                    html.addClass(admon)
                }

            case DocNodeId.heading:
                heading := (Heading) elem
                if (html.id == null)
                    html.id = toId(heading.title)

            case DocNodeId.image:
                image := (Image) elem
                if (image.size != null) {
                    sizes := image.size.split('x')
                    html["width"]   = sizes.getSafe(0)?.trimToNull
                    html["height"]  = sizes.getSafe(1)?.trimToNull
                }
                src := resolveLink(html, image.uri)
                html["src"] = src ?: image.uri
                html["alt"] = image.alt

            case DocNodeId.link:
                link := (Link) elem
                url  := Uri(link.uri, false)
                href := resolveLink(html, link.uri)
                html["href"] = href ?: link.uri
        
            case DocNodeId.orderedList:
                ol := (OrderedList) elem
                html["style"] = "list-style-type: " + ol.style.htmlType
        }
        
        html.addClass(cssClasses[elem.id] ?: "")
        return html
    }
    
    ** Calls the 'LinkResolvers' looking for valid links.
    virtual Uri? resolveLink(HtmlElem html, Str url) {
        res := null as Uri
        uri := Uri(url, false)
        if (uri != null) {
            scheme := uri.scheme == null ? null : url[0..<uri.scheme.size]
            res = linkResolvers.eachWhile { it.resolve(scheme, uri) }
        }
        
        if (res == null) {
            html["data-invalidLink"] = url
            invalidLinkProcessor?.process(html)
        }

        return res
    }

    private static Str toId(Str humanName) {
        Str.fromChars(humanName.fromDisplayName.chars.findAll { it.isAlphaNum || it == '_' || it == '-' })
    }
}