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