sourcecamNodePlugin::NodeDocs.fan

// History:
//  Feb 05 13 tcolar Creation
//

using gfx
using util
using camembert
using web
using concurrent

**
** NodeDocProvider
** Provide standard Node docs (bundled)
**
const class NodeDocs : PluginDocs
{
  const AtomicRef _docs := AtomicRef()

  new make()
  {
    // Aynchronously building the doc index
    Actor(ActorPool(), |Obj? obj -> Obj?|
    {
      try
      {
        Str:Obj? theDocs := JsonInStream(((File)`fan://camNodePlugin/res/node.json`.get).in).readJson
        // TODO: persists the scanned version instead of doing this at each start
        _docs.val = scan(theDocs)
      }
      catch(Err e)
      {
        Sys.log.err("Failed loading Node.js docs !", e)
      }
      return null
    }).send("run")
  }

  override const Image? icon := Image(`fan://camNodePlugin/res/node.png`, false)

  ** name of the plugin responsible
  override Str pluginName() {this.typeof.pod.name}

  ** User friendly dsplay name
  override Str dis() {"Node.js"}

  ** Return a FileItem for the document matching the current source file (if known)
  ** Query wil be what's in the helPane serach box, ie "fwt::Combo#make" (not prefixed by plugin name)
  override FileItem? findSrc(Str query) {null}

  ** Return html for a given path
  ** Note, the query will be prefixed with the plugin name for example /fantom/fwt::Button
  **
  ** TODO: actually use matchkind
  override Str html(WebReq req, Str query, MatchKind matchKind)
  {
    docs := ([Uri:NodeDoc]?) _docs.val

    if(docs == null)
      return "Docs not ready yet."

    return show(docs, query)
  }

  Str show(Uri:NodeDoc docs, Str query)
  {
    uri := query.lower.trim.toUri
    doc := docs[uri]

    html := StrBuf()

    if(query.trim.isEmpty)
    {
      // index
      html.add("<h2>Node.js Documentation</h2>")
      children(docs, uri).sort.each
      {
        child := docs[it]
        html.add(link(it, child.dis)+"<br/>")
      }
      return html.toStr
    }

    if(doc == null)
    {
      html.add("<h2>Search results:</h2>")
      docs.keys.sort.each
      {
        if(it.path[-1].lower.contains(uri.toStr.lower))
        {
          html.add(link(it, it.toStr)+"<br/>")
        }
      }
      return html.toStr
    }

    html.add("<h2>$doc.dis</h2>")
    html.add(doc.desc)

    NodeDoc[] kids := [,]
    NodeDoc[] classes := [,]

    children(docs, uri).sort.each
    {
      kid := docs[it]
      if(kid.type == "class")
        classes.add(kid)
      else
        kids.add(kid)
    }

    html.add("<hr/>")
    kids.each |child|
    {
      html.add("<a href='#$child.name'>$child.name</a> ")
    }
    html.add("<hr/>")

    if( ! classes.isEmpty)
    {
      html.add("<div class='bg1'>Classes:</div>")
      classes.each |child|
      {
        html.add(link(`$uri/$child.name`, child.name)+"<br/>")
      }
      html.add("<hr/>")
    }

    kids.each |child|
    {
      html.add("<a name='$child.name'><div class='bg1'>$child.dis</div>")
      html.add("</a>$child.desc")
    }

    return html.toStr
  }

  Uri:NodeDoc scan(Obj? obj, Str path := "")
  {
    Uri:NodeDoc docs := [:]
    if(obj == null) return docs
    if(obj is Map)
    {
      map := (obj as Str:Obj?)
      nm := map["name"]?.toStr
      if(nm != null)
      {
        doc := NodeDoc(map)
        docs["$path$doc.name".lower.toUri] = doc
        path += "$nm/"
      }
      asMap(obj).each
      {
        docs.setAll(scan(it, path))
      }
    }
    else if(obj is List)
    {
      asList(obj).each
      {
        docs.setAll(scan(it, path))
      }
    }
    return docs.toImmutable
  }

  Str notFound() {return "Not found !"}

  Str link(Uri to, Str nm)
  {
    return "<a href='/camNodePlugin/$to/'>$nm</a>"
  }

  static Str:Obj? asMap(Obj? data)
  {
    return (Str:Obj?) data
  }

  static [Str:Obj?][]? asList(Obj? data)
  {
    return ([Str:Obj?][]?) data
  }

  static Uri[] children(Uri:NodeDoc docs, Uri item)
  {
    sw := item.path.isEmpty ? "" : item.pathStr+"/"
    return docs.keys.findAll {it.path.size == item.path.size + 1 && it.pathStr.startsWith(sw)}
  }
}

** Node documentation "tree"
@Serializable
const class NodeDoc
{
  const Str name := ""
  const Str dis := ""
  const Str desc := ""
  const Str type := ""

  new make(Str:Obj? map)
  {
    name = map["name"]?.toStr ?: ""
    desc = map["desc"]?.toStr ?: ""
    dis  = map["textRaw"]?.toStr ?: ""
    type = map["type"]?.toStr ?: ""
  }
}