neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

uri.lua (3701B)


      1 -- TODO: This is implemented only for files currently.
      2 -- https://tools.ietf.org/html/rfc3986
      3 -- https://tools.ietf.org/html/rfc2732
      4 -- https://tools.ietf.org/html/rfc2396
      5 
      6 local M = {}
      7 local sbyte = string.byte
      8 local schar = string.char
      9 local tohex = require('bit').tohex
     10 local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*'
     11 local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*'
     12 local PATTERNS = {
     13  -- RFC 2396
     14  -- https://tools.ietf.org/html/rfc2396#section-2.2
     15  rfc2396 = "^A-Za-z0-9%-_.!~*'()",
     16  -- RFC 2732
     17  -- https://tools.ietf.org/html/rfc2732
     18  rfc2732 = "^A-Za-z0-9%-_.!~*'()%[%]",
     19  -- RFC 3986
     20  -- https://tools.ietf.org/html/rfc3986#section-2.2
     21  rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/",
     22 }
     23 
     24 ---Converts hex to char
     25 ---@param hex string
     26 ---@return string
     27 local function hex_to_char(hex)
     28  return schar(tonumber(hex, 16))
     29 end
     30 
     31 ---@param char string
     32 ---@return string
     33 local function percent_encode_char(char)
     34  return '%' .. tohex(sbyte(char), 2)
     35 end
     36 
     37 ---@param uri string
     38 ---@return boolean
     39 local function is_windows_file_uri(uri)
     40  return uri:match('^file:/+[a-zA-Z]:') ~= nil
     41 end
     42 
     43 ---URI-encodes a string using percent escapes.
     44 ---@param str string string to encode
     45 ---@param rfc "rfc2396" | "rfc2732" | "rfc3986" | nil
     46 ---@return string encoded string
     47 function M.uri_encode(str, rfc)
     48  local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
     49  return (str:gsub('([' .. pattern .. '])', percent_encode_char)) -- clamped to 1 retval with ()
     50 end
     51 
     52 ---URI-decodes a string containing percent escapes.
     53 ---@param str string string to decode
     54 ---@return string decoded string
     55 function M.uri_decode(str)
     56  return (str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)) -- clamped to 1 retval with ()
     57 end
     58 
     59 ---Gets a URI from a file path.
     60 ---@param path string Path to file
     61 ---@return string URI
     62 function M.uri_from_fname(path)
     63  local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') ---@type string?, string?
     64  local is_windows = volume_path ~= nil
     65  if is_windows then
     66    assert(fname)
     67    path = volume_path .. M.uri_encode(fname:gsub('\\', '/'))
     68  else
     69    path = M.uri_encode(path)
     70  end
     71  local uri_parts = { 'file://' }
     72  if is_windows then
     73    table.insert(uri_parts, '/')
     74  end
     75  table.insert(uri_parts, path)
     76  return table.concat(uri_parts)
     77 end
     78 
     79 ---Gets a URI from a bufnr.
     80 ---@param bufnr integer
     81 ---@return string URI
     82 function M.uri_from_bufnr(bufnr)
     83  local fname = vim.api.nvim_buf_get_name(bufnr)
     84  local volume_path = fname:match('^([a-zA-Z]:).*')
     85  local is_windows = volume_path ~= nil
     86  local scheme ---@type string?
     87  if is_windows then
     88    fname = fname:gsub('\\', '/')
     89    scheme = fname:match(WINDOWS_URI_SCHEME_PATTERN)
     90  else
     91    scheme = fname:match(URI_SCHEME_PATTERN)
     92  end
     93  if scheme then
     94    return fname
     95  else
     96    return M.uri_from_fname(fname)
     97  end
     98 end
     99 
    100 ---Gets a filename from a URI.
    101 ---@param uri string
    102 ---@return string filename or unchanged URI for non-file URIs
    103 function M.uri_to_fname(uri)
    104  local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri)
    105  if scheme ~= 'file' then
    106    return uri
    107  end
    108  local fragment_index = uri:find('#')
    109  if fragment_index ~= nil then
    110    uri = uri:sub(1, fragment_index - 1)
    111  end
    112  uri = M.uri_decode(uri)
    113  --TODO improve this.
    114  if is_windows_file_uri(uri) then
    115    uri = uri:gsub('^file:/+', ''):gsub('/', '\\') --- @type string
    116  else
    117    uri = uri:gsub('^file:/+', '/') ---@type string
    118  end
    119  return uri
    120 end
    121 
    122 ---Gets the buffer for a uri.
    123 ---Creates a new unloaded buffer if no buffer for the uri already exists.
    124 ---@param uri string
    125 ---@return integer bufnr
    126 function M.uri_to_bufnr(uri)
    127  return vim.fn.bufadd(M.uri_to_fname(uri))
    128 end
    129 
    130 return M