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 131 132 133 134 135 136 137 138 139 140
|
-- Text processing functions.
local M = {}
local alphabet = '0123456789ABCDEF'
local atoi = {} ---@type table<string, integer>
local itoa = {} ---@type table<integer, string>
do
for i = 1, #alphabet do
local char = alphabet:sub(i, i)
itoa[i - 1] = char
atoi[char] = i - 1
atoi[char:lower()] = i - 1
end
end
--- Hex encode a string.
---
--- @param str string String to encode
--- @return string : Hex encoded string
function M.hexencode(str)
local enc = {} ---@type string[]
for i = 1, #str do
local byte = str:byte(i)
enc[2 * i - 1] = itoa[math.floor(byte / 16)]
enc[2 * i] = itoa[byte % 16]
end
return table.concat(enc)
end
--- Hex decode a string.
---
--- @param enc string String to decode
--- @return string? : Decoded string
--- @return string? : Error message, if any
function M.hexdecode(enc)
if #enc % 2 ~= 0 then
return nil, 'string must have an even number of hex characters'
end
local str = {} ---@type string[]
for i = 1, #enc, 2 do
local u = atoi[enc:sub(i, i)]
local l = atoi[enc:sub(i + 1, i + 1)]
if not u or not l then
return nil, 'string must contain only hex characters'
end
str[(i + 1) / 2] = string.char(u * 16 + l)
end
return table.concat(str), nil
end
--- Sets the indent (i.e. the common leading whitespace) of non-empty lines in `text` to `size`
--- spaces/tabs.
---
--- Indent is calculated by number of consecutive indent chars.
--- - The first indented, non-empty line decides the indent char (space/tab):
--- - `SPC SPC TAB …` = two-space indent.
--- - `TAB SPC …` = one-tab indent.
--- - Set `opts.expandtab` to treat tabs as spaces.
---
--- To "dedent" (remove the common indent), pass `size=0`:
--- ```lua
--- vim.print(vim.text.indent(0, ' a\n b\n'))
--- ```
---
--- To adjust relative-to an existing indent, call indent() twice:
--- ```lua
--- local indented, old_indent = vim.text.indent(0, ' a\n b\n')
--- indented = vim.text.indent(old_indent + 2, indented)
--- vim.print(indented)
--- ```
---
--- To ignore the final, blank line when calculating the indent, use gsub() before calling indent():
--- ```lua
--- local text = ' a\n b\n '
--- vim.print(vim.text.indent(0, (text:gsub('\n[\t ]+\n?$', '\n'))))
--- ```
---
--- @param size integer Number of spaces.
--- @param text string Text to indent.
--- @param opts? { expandtab?: number }
--- @return string # Indented text.
--- @return integer # Indent size _before_ modification.
function M.indent(size, text, opts)
vim.validate('size', size, 'number')
vim.validate('text', text, 'string')
vim.validate('opts', opts, 'table', true)
-- TODO(justinmk): `opts.prefix`, `predicate` like python https://docs.python.org/3/library/textwrap.html
opts = opts or {}
local tabspaces = opts.expandtab and (' '):rep(opts.expandtab) or nil
--- Minimum common indent shared by all lines.
local old_indent --[[@type number?]]
local prefix = tabspaces and ' ' or nil -- Indent char (space or tab).
--- Check all non-empty lines, capturing leading whitespace (if any).
--- @diagnostic disable-next-line: no-unknown
for line_ws, extra in text:gmatch('([\t ]*)([^\n]+)') do
line_ws = tabspaces and line_ws:gsub('[\t]', tabspaces) or line_ws
-- XXX: blank line will miss the last whitespace char in `line_ws`, so we need to check `extra`.
line_ws = line_ws .. (extra:match('^%s+$') or '')
if 0 == #line_ws then
-- Optimization: If any non-empty line has indent=0, there is no common indent.
old_indent = 0
break
end
prefix = prefix and prefix or line_ws:sub(1, 1)
local _, end_ = line_ws:find('^[' .. prefix .. ']+')
old_indent = math.min(old_indent or math.huge, end_ or 0)
end
-- Default to 0 if all lines are empty.
old_indent = old_indent or 0
prefix = prefix and prefix or ' '
if old_indent == size then
-- Optimization: if the indent is the same, return the text unchanged.
return text, old_indent
end
local new_indent = prefix:rep(size)
--- Replaces indentation of a line.
--- @param line string
local function replace_line(line)
-- Match the existing indent exactly; avoid over-matching any following whitespace.
local pat = prefix:rep(old_indent)
-- Expand tabs before replacing indentation.
line = not tabspaces and line
or line:gsub('^[\t ]+', function(s)
return s:gsub('\t', tabspaces)
end)
-- Text following the indent.
local line_text = line:match('^' .. pat .. '(.*)') or line
return new_indent .. line_text
end
return (text:gsub('[^\n]+', replace_line)), old_indent
end
return M
|