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