Fan

 

class

docCompiler::UriMapper

sys::Obj
  docCompiler::DocCompilerSupport
    docCompiler::UriMapper
//
// Copyright (c) 2007, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   11 May 07  Brian Frank  Creation
//

using compiler
using fandoc

**
** UriMapper is used to normalize fandoc URIs into hrefs to
** their HTML file representation using relative URLs.
**
class UriMapper : DocCompilerSupport
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  new make(DocCompiler compiler)
    : super(compiler)
  {
  }

//////////////////////////////////////////////////////////////////////////
// Access
//////////////////////////////////////////////////////////////////////////

  **
  ** Given a fandoc uri string, map it to a relative URL
  ** to the resource's HTML.
  **
  Uri map(Str fandocUri, Location loc)
  {
    this.fandocUri    = fandocUri
    this.loc          = loc
    this.frag         = null
    this.targetUri    = null
    this.targetIsCode = false
    this.targetIsSlot = false

    // if document internal fragment identifer then
    // bail now before we do any work or use our cache
    // which spans all the documents in the current pod
    // reset working fields
    if (fandocUri.startsWith("#"))
      return targetUri = Uri.fromStr(fandocUri)

    // if absolute then bail
    if (fandocUri.startsWith("http:")  ||
        fandocUri.startsWith("https:") ||
        fandocUri.startsWith("ftp:")   ||
        fandocUri.startsWith("mailto:"))
      return targetUri = Uri.fromStr(fandocUri)

    // check the cache
    cached := cache[fandocUri]
    if (cached != null)
    {
      targetUri    = cached.targetUri
      targetIsCode = cached.targetIsCode
      return targetUri
    }

    // strip off fragment if specified
    pound := fandocUri.index("#")
    if (pound != null)
    {
      // if URI is to frag within doc we are done
      if (pound == 0) return targetUri = Uri.fromStr(fandocUri)

      // split off fragment identifier
      this.fandocUri = fandocUri[0..<pound]
      this.frag = fandocUri[pound+1..-1]
    }

    // map
    try
    {
      if (fandocUri.startsWith("@"))
        mapSymbol
      else if (fandocUri.contains("::"))
        mapPod
      else
        mapTypeOrFile(compiler.pod, this.fandocUri)

      // cache for next time
      if (!targetIsSlot)
        cache[fandocUri] = CachedUri(targetUri, targetIsCode)
    }
    catch (CompilerErr e)
    {
      targetUri = fandocUri.toUri
    }
    catch (Err e)
    {
      errReport(CompilerErr("Cannot map uri '$fandocUri'", loc, e))
      targetUri = fandocUri.toUri
    }

    // return result
    return targetUri
  }

//////////////////////////////////////////////////////////////////////////
// Map
//////////////////////////////////////////////////////////////////////////

  private Void mapSymbol()
  {
    Symbol? symbol

    // full qname
    colons := fandocUri.index("::")
    if (colons != null)
    {
      podName := fandocUri[1..<colons]
      pod := Pod.find(podName, false)
      if (pod == null) throw err("Unknown pod '$podName'", loc)

      name := fandocUri[colons+2..-1]
      symbol = pod.symbol(name)
    }
    else
    {
      // look up in current pod
      name := fandocUri[1..-1]
      symbol = compiler.pod.symbol(name, false)
    }

    if (symbol == null) throw err("Unknown symbol '$fandocUri'", loc)
    targetIsCode = true
    targetUri = toUri(symbol.pod, "pod-meta.html", symbol.name)
  }

  private Void mapPod()
  {
    // lookup pod
    colons := fandocUri.index("::")
    podName := fandocUri[0..<colons]
    pod := Pod.find(podName, false)
    if (pod == null) throw err("Unknown pod '$podName'", loc)

    rest := fandocUri[colons+2..-1]
    mapTypeOrFile(pod, rest)
  }

  private Void mapTypeOrFile(Pod pod, Str s)
  {
    typeName := s
    Str? rest := null

    if (s == "index")
    {
      targetUri = toUri(pod, "index.html", frag)
      return
    }

    dot := s.index(".")
    if (dot != null)
    {
      typeName = s[0..<dot]
      rest = s[dot+1..-1]
    }
    else if (compiler.curType != null)
    {
      // if this string maps to a slot in the current
      // type being processes - then slot name trumps
      t := compiler.curType
      slot := t.slot(s, false)
      if (slot != null)
      {
        targetIsSlot = true
        targetUri = toUri(slot.parent.pod, "${slot.parent.name}.html", slot.name)
        return
      }
    }

    // first try type in pod
    t := pod.findType(typeName, false)
    if (t != null)
    {
      mapSlot(t, rest)
      return
    }

    // if dot ext, this must be a filename which we don't do yet
    // TODO: eventually we need we need a standard mechanism
    // to determine which resources to copy to the doc directory
    // probably under some specific dir, which we can map
    if (rest != null)
    {
      if (fandocUri.endsWith(".html"))
      {
        echo("WARNING: Need to fix unresolved fandoc uri bug: $fandocUri")
        targetUri = fandocUri.toUri
      }
      else
      {
        throw err("Unresolved fandoc uri '$fandocUri'", loc)
      }
    }

    // try to find fandoc file in pod
    fandocFile := pod.files.find |File f->Bool|
    {
      return f.basename == typeName && f.ext == "fandoc"
    }

    if (fandocFile == null)
      throw err("Unresolved fandoc uri '$fandocUri'", loc)

    // if this a fragment identifier within the
    // fandoc file then check that it exists
    if (frag != null)
    {
      // ensure we've parsed the fandoc file into memory
      // to create a table of all the fragment identifiers
      if (fandocFrags[fandocUri] == null)
        fandocFrags[fandocUri] = parseFandocFrags(fandocFile)

      // check that the fragment exists
      if (!fandocFrags[fandocUri].frags.containsKey(frag))
        throw err("Unknown fragment '#$frag' in '$fandocUri'", loc)
    }

    // we can now build our uri
    targetUri = toUri(pod, "${fandocFile.basename}.html", frag)
  }

  private Void mapSlot(Type t, Str? rest)
  {
    targetIsCode = true
    Str? slotFrag := null

    // if rest it must be a slot name
    if (rest != null)
    {
      slot := t.slot(rest, false)
      if (slot == null)
        throw err("Unresolved uri to slot '${t.qname}.$rest'", loc)
      if (slot.parent != t)
        throw err("Uri to inherited slot '${t.qname}.$rest' is declared on '$slot.parent'", loc)
      slotFrag = slot.name
      targetIsSlot = true
    }

    targetUri = toUri(t.pod, "${t.name}.html", slotFrag)
  }

  private Uri toUri(Pod pod, Str uri, Str? frag)
  {
    if (pod != compiler.pod) uri = "../$pod.name/" + uri
    if (frag != null) uri = uri + "#" + frag
    return Uri.fromStr(uri)
  }

//////////////////////////////////////////////////////////////////////////
// FandocFrag Parse
//////////////////////////////////////////////////////////////////////////

  private FandocFrags parseFandocFrags(File f)
  {
    try
    {
      log.debug("    Parse fandoc frag [$fandocUri]")
      docFrags := FandocFrags.make
      f.readAllLines.each |Str line|
      {
        x := line.index("[#")
        if (x != null)
        {
          y := line.index("]", x)
          if (y != null)
          {
            frag := line[x+2..<y]
            docFrags.frags[frag] = frag
          }
        }
      }
      return docFrags
    }
    catch (Err e)
    {
      e.trace
      throw err("Cannot parse fandoc file for fragments: $f", loc)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  Str? fandocUri
  Str? frag
  Location? loc
  Uri? targetUri
  Bool targetIsCode
  Bool targetIsSlot
  internal Str:CachedUri cache := Str:CachedUri[:]
  internal Str:FandocFrags fandocFrags := Str:FandocFrags[:]
}

internal class CachedUri
{
  new make(Uri uri, Bool isCode)
  {
    targetUri    = uri
    targetIsCode = isCode
  }

  Uri targetUri
  Bool targetIsCode
}

internal class FandocFrags
{
  Str:Str frags := Str:Str[:]
}