Files
plainleaf/libraries/Library/Std/Infrastructure/Library.md
T

8.5 KiB

#meta

This implements library management

Implementation

-- namespace
library = {
  libraryTag = "meta/library",
  remoteLibraryTag = "meta/library/remote",
  repositoryTag = "meta/repository"
}

-- Harcoded prefixes
local libraryPrefix = "Library/"
local repositoryPrefix = "Repositories/"

-- Schema of frontmatter of the library page
-- Schema of #meta/library objects
local librarySchema = {
  type = "object",
  properties = {
    files = schema.array(schema.string())
  }
}

local remoteLibrarySchema = {
  type = "object",
  properties = {
    name = schema.string(),
    uri = schema.string(),
  }
}

-- Repo management commands
command.define {
  name = "Library: Add Repository",
  run = function()
    local repoUri = editor.prompt("URI:")
    if not repoUri then
      return
    end
    local text = net.readURI(repoUri, {encoding="text/markdown"})
    local repoName = editor.prompt("Name:")
    if not repoName then
      return
    end
    local repoPage = repositoryPrefix .. repoName
    if space.pageExists(repoPage) then
      editor.flashNotification(repoPage .. " already exists", "error")
      return
    end
    space.writePage(repoPage, share.setFrontmatter({
      uri = repoUri,
      hash = share.contentHash(text),
      mode = "pull"
    }, text))
    editor.flashNotification "Library installed"
    editor.navigate(libPage)
    reloadEverything()
  end
}

command.define {
  name = "Library: Update All Repositories",
  run = function()
    for repo in query[[from index.tag(library.repositoryTag)]] do
      if repo.share then
        share.sharePage(repo.name)
      end
    end
    editor.flashNotification "All repositories updated"
  end
}

-- Library management commands
command.define {
  name = "Library: Install",
  run = function()
    local uri = editor.prompt("URI:")
    if not uri then
      return
    end
    local text = net.readURI(uri, {encoding="text/markdown"})
    local name = editor.prompt("Name (library will be saved under Library/<<Name>>):")
    if not name then
      return
    end
    local libPage = libraryPrefix .. name
    if space.pageExists(libPage) then
      editor.flashNotification(libPage .. " already exists", "error")
      return
    end
    space.writePage(libPage, share.setFrontmatter({
      uri = uri,
      hash = share.contentHash(text),
      mode = "pull"
    }, text))
    editor.flashNotification "Library installed"
    editor.navigate(libPage)
    reloadEverything()
  end
}

command.define {
  name = "Library: Update All",
  run = function()
    local installedLibraries = query[[from index.tag(library.libraryTag)]]
    for lib in installedLibraries do
      if library.update(lib.name, false) then
        editor.flashNotification("Updated " .. lib.name)
      end
    end
    editor.flashNotification "Update complete!"
    reloadEverything()
  end
}

command.define {
  name = "Library: Manager",
  run = function()
    editor.navigate("Library/Std/Pages/Library Manager")
  end
}

-- Look up an installed library by name
function library.getInstalled(uri)
  local installedFMs = query[[
    from index.tag(library.libraryTag)
    where _.share and _.share.uri == uri
  ]]
  if #installedFMs == 0 then
    return nil
  else
    return installedFMs[1]
  end
end

-- Install a library
function library.install(pageName, uri, currentHash)
  -- Fetch from remote URL
  local text = net.readURI(uri, {
    encoding = "text/markdown"
  })

  if not text then
    error("Could not fetch: " .. uri)
  end

  local remoteLibFM = index.extractFrontmatter(text).frontmatter
  local remoteHash = share.contentHash(text)

  local err = jsonschema.validateObject(librarySchema, remoteLibFM)
  if err then
    editor.flashNotification("Library frontmatter validation error: " .. err, "error")
    return
  end

  -- Check if update is required based on hash
  if currentHash and currentHash == remoteHash then
    return false
  end

  -- Ok, let's do this
  local baseUrl = urlDir(uri)
  text = share.setFrontmatter({
    uri = uri,
    hash = remoteHash,
    mode = "pull"
  }, text)
  
  -- First write the main file
  space.writePage(pageName, text)
  
  -- And download and write assets, if any
  if remoteLibFM.files then
    for file in remoteLibFM.files do
      print("Downloading file", file)
      local targetPath = pageName .. "/" .. file
      local data = net.readURI(baseUrl .. file, {encoding="application/octet-stream"})
      if not data then
        error("Could not fetch file: "..baseUrl..file)
      end
      space.writeFile(targetPath, data)
    end
  end
  return true
end

-- Update a library
function library.update(pageName, force)
  local text = space.readPage(pageName)
  local fm = index.extractFrontmatter(text).frontmatter
  if fm.share then
     return library.install(pageName, fm.share.uri, fm.share.hash)
  else
    print("No 'share' key found in frontmatter for " .. pageName)
  end
end

function library.remove(pageName)
  local text = space.readPage(pageName)

  if not text then
    error("Could not read: " .. pageName)
  end

  local fm = index.extractFrontmatter(text).frontmatter

  -- Remove associated files first
  if fm.files then
    for file in fm.files do
      local p = pageName .. "/" .. file
      space.deleteFile(p)
    end
  end
  
  -- Then page itself
  space.deletePage(pageName)
end

function library.installedLibrariesWidget()
  local rows = {}
  for lib in query[[from index.tag "meta/library"]] do
    table.insert(rows, dom.tr {
      dom.td { "[[" .. lib.name .. "|" .. lib.name .. "]]" },
      dom.td {
        not lib.share and "" or dom.span{
          widgets.button("Update", function()
            local updated = library.update(lib.name, true)
            if updated then
              editor.flashNotification "Updated."
              reloadEverything()
            else
              editor.flashNotification "No update required."
            end
          end),
          widgets.button("Remove", function()
            if editor.confirm("Are you sure?") then
              library.remove(lib.name)
              editor.flashNotification "Done!"
              reloadEverything()
            end
          end)  
        }
      }
    })
  end
  return widget.htmlBlock(dom.table {
    dom.thead {
      dom.tr {
        dom.td {"Library"},
        dom.td {"Action"}
      }
    },
    dom.tbody(rows)
  })
end

function library.installableLibrariesWidget()
  local rows = {}
  for lib in query[[from index.tag "meta/library/remote"]] do
    local installed = library.getInstalled(lib.uri)
    if not installed then
      table.insert(rows, dom.tr {
        dom.td { lib.name },
        dom.td { "[[" .. lib.page .. "|" .. lib.page .. "]]" },
        dom.td {
          widgets.button("Install", function()
            library.install(library.libPath(lib.name), lib.uri)
            editor.flashNotification "Done!"
            reloadEverything()
          end)
        }
      })
    end
  end
  return widget.htmlBlock(dom.table {
    dom.thead {
      dom.tr {
        dom.td {"Library"},
        dom.td {"Repository"},
        dom.td {"Action"}
      }
    },
    dom.tbody(rows)
  })
end

function library.installedRepositoriesWidget()
  local rows = {}
  for repo in query[[from index.tag(library.repositoryTag)]] do
    table.insert(rows, dom.tr {
      dom.td { "[[" .. repo.name .. "|" .. repo.name .. "]]" },
      dom.td {
        repo.share and dom.span {
          widgets.button("Update", function()
            if share.sharePage(repo.name) then
              editor.flashNotification "Repository updated"
              reloadEverything()
            else
              editor.flashNotification "No changes"
            end
          end),
          widgets.button("Remove", function()
            if editor.confirm("Are you sure?") then
              space.deletePage(repo.name)
              editor.flashNotification "Done!"
              reloadEverything()
            end
          end)
        } or ""
      }
    })
  end
  return widget.htmlBlock(dom.table {
    dom.thead {
      dom.tr {
        dom.td {"Repository"},
        dom.td {"Action"}
      }
    },
    dom.tbody(rows)
  })
end

-- Utility functions
local function urlDir(url)
  return string.match(url, "^(.*/)[^/]+$")
end

-- Construct full library path from name
function library.libPath(name)
  return libraryPrefix .. name
end

-- Extract name from full path
function library.libName(path)
  return path:sub(#libraryPrefix + 1)
end

function library.repoName(path)
  return path:sub(#repositoryPrefix + 1)
end

function reloadEverything()
  mq.awaitEmptyQueue("indexQueue")
  editor.reloadConfigAndCommands()
  codeWidget.refreshAll()
end