uri.lua 3.6 KB

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