sourcecamFantomPlugin::FantomDocs.fan

// History:
//  Jan 28 13 tcolar Creation
//

using gfx
using fandoc
using compilerDoc
using web
using camembert

**
** FantomDoc
** Documentation provider for fantom
**
const class FantomDocs : PluginDocs
{
  const FantomPlugin plugin

  new make(FantomPlugin plugin)
  {
    this.plugin = plugin
    this.pluginName = this.typeof.pod.name
  }

  static const gfx::Image fanIcon  := gfx::Image(`fan://icons/x16/database.png`, false)

  override const gfx::Image? icon := fanIcon

  override const Str pluginName

  override const Str dis := "Fantom"

  override FileItem? findSrc(Str query)
  {
    // TODO: we could match more precisely (slot)
    if(query.contains("#"))
      query = query[0 ..< query.index("#")]
    info := FantomPlugin.cur.index.matchTypes(query, MatchKind.exact).first
    if(info.toFile == null || ! info.toFile.exists)
      return null
    return info != null ? FantomItem(info) : null
  }

  override Str html(WebReq req, Str query, MatchKind matchKind)
  {

    if(query.isEmpty)
      return podList
    else if(query == "axon-home")
      return axonLibs // TODO: -> to axon plugin
    else if(query.contains("ext-"))
      return axonDocs(req, query)// TODO: -> to axon plugin
    else if(query.contains("::"))
    {
      return itemDoc(req, query)
    }
    else
      return find(query, req.uri.path[0] == this.typeof.pod.name, matchKind)
  }

  ** List all pods except axon library pods
  Str podList()
  {
    Str pods := "<h2>Pod List</h2>"
    plugin.index.pods.each
    {
      if( ! it.isAxonPod || it.name=="skyspark" || it.name=="proj")
      {
        pods+="<a href='/${pluginName}/${it.name}::pod-doc'>$it.name</a> <br/>"
      }
    }
    return pods
  }

  ** List axon libraries / extensions
  Str axonLibs()
  {
    Str html := "<h2>Axon libraries</h2>"
    Str[] pods := [,]
    // axon libraries writen in Fantom
    plugin.index.pods.each |pod|
    {
      if(pod.isAxonPod)
        pods.add(pod.name)
    }
    // axon extensions (trio sources)
    plugin.index.trioInfo.keys.each |pod|
    {
      if(!pods.contains(pod))
        pods.add(pod)
    }
    pods.sort.each |pod|
    {
      link := toExtLink(pod)
      html += "<a href='${link}index'>$pod</a><br/>"
    }
    return html
  }

  private Str toExtLink(Str podName)
  {
    if(! podName.endsWith("Ext"))
      return "/${pluginName}/ext-${podName}/"
    return "/${pluginName}/ext-${podName[0..-4]}/"
  }

  ** Axon extensions/libs docs
  private Str axonDocs(WebReq req, Str text)
  {
    podName := text[0 ..< text.index("/")]
    if(podName.startsWith("ext-"))
    {
      if(podName=="ext-skyspark" || podName=="ext-proj")
        podName = podName[4 .. -1]
      else
        podName = "${podName[4 .. -1]}Ext"
    }

    item := text[text.index("/")+1 .. -1]

    pod := plugin.index.matchPods(podName.lower, MatchKind.exact).first
    if(pod == null)
      return "$podName not found !"
    trioInfo := plugin.index.trioInfo[podName]

    if(item == "index")
    {
      html := "<h2>$podName - Index</h2><h3><b>Functions:</b></h3>"
      // Axon libs functions
      pod.types.findAll{isAxonLib}.each |type|
      {
        type.slots.each
        {
          html += "<a href='funcs#${it.name}'>$it.name</a><br/>"
        }
      }
      if(trioInfo != null)
      {
        // axon extensions (trio sources)
        trioInfo.funcs.vals.sort.each
        {
          html += "<a href='funcs#${it.name}'>$it.name</a><br/>"
        }
        // tags
        html += "<h3><b>Tags:</b></h3>"
        trioInfo.tags.vals.sort.each
        {
          html += "<a href='tags#${it.name}'>$it.name</a><br/>"
        }
      }
      return html
    }
    else if(item == "funcs")
    {
      html := "<h2>$podName Funtions :</h2>"
      // fantom funcs
      html += readAxonTypeDoc(req, pod, trioInfo)
      return html
    }
    else if(item == "tags")
    {
      html := "<h2>$podName Tags :</h2>"
      // fantom funcs
      trioInfo.tags.each
      {
        html += "<div class='bg1'><a name='$it.name'></a><b>${it.name}</b>"
                    + "</div><div class='bg2'>$it.kind</div>"
                    + docStrToHtml(req, it.doc, true)
      }
      return html
    }
    else if(item == "src")
    {
      html := "<h2>$podName Axon src :</h2>"
      trioInfo.funcs.each
      {
        html += "<div class='bg1'><a name='$it.name'></a><b>${it.name}</b></div>"
             + "<pre>$it.src</pre>"
      }
      return html
    }

    return "TBD"
  }

  ** Get doc for an item(pod, type etc..)
  private Str itemDoc(WebReq req, Str fqn)
  {
    if(fqn.contains("::index")) fqn = fqn[0 ..< fqn.index("::")]
      if(fqn.contains("::pod-doc")) fqn = fqn[0 ..< fqn.index("::")]
      if(! fqn.contains("::"))
    {
      // pod
      info := plugin.index.matchPods(fqn.lower, MatchKind.exact).first
      if(info == null)
        return "$fqn not found !"
      text := "<h2>$info.name</h2>"
      info.types.each {text += "<a href='/${pluginName}/$it.qname'>$it.name</a>, "}
      text += "<hr/>"
      text += readPodDoc(req, info.podFile)
      return text
    }
    else
    {
      info := plugin.index.matchTypes(fqn.lower, MatchKind.exact).first
      if(info == null)
        return "$fqn not found !"
      text := "<h2>$info.qname</h2>"
      text += readTypeDoc(req, info.pod.podFile, info.name)
      return text
    }
  }

  ** Search pods, types, slots for items matching the query
  ** And returns a search result page
  ** IsFantom means wether it's for fantom or axon
  private Str find(Str query, Bool isFantom, MatchKind kind := MatchKind.startsWith,
                   Bool inclSlots := true)
  {
    index := plugin.index

    pods := index.matchPods(query, kind)
    results := ""
    if(! pods.isEmpty)
    {
      results += "<h2>Pods:</h2>"
      pods.each
      {
        results+="<a href='/${pluginName}/${it.name}::index'>$it</a> <br/>"
      }
    }
    TypeInfo[] types := isFantom ? index.matchTypes(query, kind) : [,]
    if(! types.isEmpty)
    {
      results += "<h2>Types:</h2>"
      types.each
      {
        results+="<a href='/${pluginName}/${it.qname}'>$it.qname</a> <br/>"
      }
    }
    if(inclSlots && isFantom)
    {
      slots := index.matchSlots(query, kind).findAll{ ! type.isAxonLib}
      if(! slots.isEmpty)
      {
        results += "<h2>Slots:</h2>"
        slots.each
        {
          results+="<a href='/${pluginName}/${it.type.qname}#$it.name'>$it.qname</a> <br/>"
        }
      }
    }

    if(isFantom)
      return results

     // ..... Axon only

    funcs := index.matchFuncs(query, kind)
    slots := index.matchSlots(query, kind).findAll{ type.isAxonLib}
    if( ! funcs.isEmpty || ! slots.isEmpty)
    {
      results += "<h2>Funcs:</h2>"
      slots.each |slot|
      {
        link := toExtLink(slot.type.pod.name)+"funcs#$slot.name"
        results+="<a href='$link'>$slot.qname</a><br/>"
      }
      funcs.each |func|
      {
        link := toExtLink(func.pod)+"funcs#$func.name"
        results+="<a href='$link'>$func.pod::$func.name</a><br/>"
      }
    }
    tags := index.matchTags(query, kind)
    if( ! tags.isEmpty)
    {
      results += "<h2>Tags:</h2>"
      tags.each |tag|
      {
        link := toExtLink(tag.pod)+"tags#$tag.name"
        results+="<a href='$link'>$tag.pod::$tag.name</a><br/>"
      }
    }

    return results
  }

  ** Parse Fandoc into HTML
  private Str docToHtml(WebReq req, DocFandoc? doc, Bool forAxon := false)
  {
    return docStrToHtml(req, doc.text, forAxon)
  }

  private Str docStrToHtml(WebReq req, Str? doc, Bool forAxon := false)
  {
    if(doc == null || doc.isEmpty) return "<br/>"
      buf := Buf()
    writer := forAxon ?
        axonWriter(req.uri.toStr, buf.out)
        : writer(req.uri.toStr, buf.out)
    FandocParser.make.parseStr(doc).write(writer)
    return buf.flip.readAllStr
  }

  ** Read doc of a pod
  private Str readPodDoc(WebReq req, File podFile)
  {
    result := "Failed to read pod doc !"
    try
    {
      doc := DocPod(podFile)
      if(doc.podDoc != null)
        result = docToHtml(req, doc.podDoc.doc)
      else
       result = doc.summary
    }
    catch(Err e) {Sys.log.err("Failed reading pod doc for $podFile.osPath", e)}
    return result
  }

  ** Read doc of a type
  private Str readTypeDoc(WebReq req, File podFile, Str typeName)
  {
    result := "Failed to read pod doc !"
    try
    {
      doc := DocPod(podFile)

      DocType? type := doc.type(typeName, false)

      Str summary := type?.doc != null ? docToHtml(req, type.doc) : doc.summary

      result = summary
      if(type!=null)
      {
        result+="<hr/>Slots: "
        type.slots.each
        {
          result += "<a href='#${it.name}'>$it.name</a>, "
        }
        result += "<hr/><div class='bg1'><b>Inheritance</b></div>"
        type.base.eachr{result += htmlType(it)+" - "}
        result += "<div class='bg1'><b>Local slots</b></div>"
        type.slots.each
        {
          result += "<div  class='bg2'><a name='$it.name'></a>"
                  +htmlSig(it) + "</div>"
                  + docToHtml(req, it.doc)
        }
      }
    }
    catch(Err e) {Sys.log.err("Failed reading pod doc for $podFile.osPath", e)}

    return result
  }

  ** Read doc of a type
  private Str readAxonTypeDoc(WebReq req, PodInfo pod, TrioInfo? info)
  {
    result := ""
    try
    {
      doc := DocPod(pod.podFile)

      libs := pod.types.findAll{it.isAxonLib}

      libs.each |lib|
      {
        DocType? type := doc.type(lib.name, false)

        if(type!=null)
        {
          type.slots.each
          {
            result += "<div  class='bg2'><a name='$it.name'></a>"
                    +htmlSig(it) + "</div>"
                    + docToHtml(req, it.doc, true)
          }
        }
      }
      if(info != null)
      {
        if( ! info.funcs.isEmpty)
        {
          link := toExtLink(pod.name)+"src"
          result += "<a href='$link'><b>View Sources</b></a><br/><br/>"
        }
        info.funcs.each
        {
          result += "<div class='bg2'><a name='$it.name'></a>"
                  +it.sig+ "</div>"
                  + docStrToHtml(req, it.doc, true)
        }
      }
    }
    catch(Err e) {Sys.log.err("Failed reading Axon docs for $pod.name", e)}

    return result
  }

  ** Beautified slot signature with links to types
  private Str htmlSig(DocSlot slot)
  {
    flags := slot.flags
    Str sig := DocFlags.toSlotDis(flags)
    if(slot is DocField)
    {
      field := slot as DocField
      sig += " "+ htmlType(field.type)
      sig += " <b>$slot.name </b>"
    }
    else if(slot is DocMethod)
    {
      method := slot as DocMethod
      sig += " " + htmlType(method.returns)
      sig += " <b>$method.name</b>"
      sig += "(";
      method.params.each
      {
        sig += htmlType(it.type)
          + (it.def != null ?" <i>${it.name}:=${it.def}</i>":" $it.name") + ", "
      }
      sig += ")"
    }
    return sig
  }

  ** Type signature with link
  private Str htmlType(DocTypeRef type)
  {
    return "<a href='/${pluginName}/$type.qname'>$type.dis</a> "
  }

  ** Html writer that deals with fixing the links to our server format
  ** /pod::type#slot
  internal HtmlDocWriter writer(Str curUri, OutStream out)
  {
    HtmlDocWriter writer := HtmlDocWriter(out)
    writer.onLink = |Link? link|
    {
      uri := link.uri
      if( ! uri.contains("::")) // relative links
      {
        if(uri.contains(".") && curUri.contains("::")) // to type in pod
           uri = curUri[1 ..< curUri.index("::") + 2] + uri
        else
          uri = "#$uri" // to slot in type
      }
      else
        uri = "/${pluginName}/$uri" // always start with the slash otherwise

      uri = uri.replace(".", "#") // slots are mapped into anchors
      link.uri = uri;
    }
    return writer
  }

  ** Axon docs behave differently
  internal HtmlDocWriter axonWriter(Str curUri, OutStream out)
  {
    HtmlDocWriter writer := HtmlDocWriter(out)
    writer.onLink = |Link? link|
    {
      uri := link.uri
      if(uri.startsWith("#")) return

      uri = uri.replace(".", "#")

      if(uri.startsWith("/")) return
      if(uri.contains("::"))
      {
        //if(uri.startWith("docskyspark::")) -> meh
        link.uri = "/$uri"
        return
      }

     /* ok : ... "relative" link
      with axon "relative" links could refer to a tab or function anywhere in the system
      so look it up and make it an absoulte link
      assume that tags/func names are unique across skyspark ... gotta be.*/

      infos := plugin.index.trioInfo.vals
      // lookup fantom axon func
      slot := plugin.index.matchSlots(uri, MatchKind.exact).find
      {
        it.type.isAxonLib
      }
      if(slot != null)
      {
        link.uri = toExtLink(slot.type.pod.name)+"funcs#$uri"
        return
      }
      // lookup trio func
      info := infos.find {it.funcs.containsKey(uri)}
      if(info != null)
      {
        link.uri = toExtLink(info.pod)+"funcs#$uri"
        return
      }
      // lookup trio tag
      info = infos.find {it.tags.containsKey(uri)}
      if(info != null)
      {
        link.uri = toExtLink(info.pod)+"tags#$uri"
        return
      }
    }
    return writer
  }

}