252 lines
6.6 KiB
Lua
252 lines
6.6 KiB
Lua
|
local m = require 'lpeglabel'
|
||
|
local matcher = require 'glob.matcher'
|
||
|
|
||
|
local function prop(name, pat)
|
||
|
return m.Cg(m.Cc(true), name) * pat
|
||
|
end
|
||
|
|
||
|
local function object(type, pat)
|
||
|
return m.Ct(
|
||
|
m.Cg(m.Cc(type), 'type') *
|
||
|
m.Cg(pat, 'value')
|
||
|
)
|
||
|
end
|
||
|
|
||
|
local function expect(p, err)
|
||
|
return p + m.T(err)
|
||
|
end
|
||
|
|
||
|
local parser = m.P {
|
||
|
'Main',
|
||
|
['Sp'] = m.S(' \t')^0,
|
||
|
['Slash'] = m.S('/')^1,
|
||
|
['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}')
|
||
|
+ m.Ct(m.V'Pattern')
|
||
|
+ m.T'Main Failed'
|
||
|
,
|
||
|
['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"'))
|
||
|
+ m.Ct(m.V'Unit')
|
||
|
,
|
||
|
['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')),
|
||
|
['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp',
|
||
|
['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp',
|
||
|
['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)),
|
||
|
['CSymbol'] = object('*', m.P'*')
|
||
|
+ object('?', m.P'?')
|
||
|
+ object('[]', m.V'Range')
|
||
|
,
|
||
|
['SimpleChar'] = m.P(1) - m.S',{}[]*?/',
|
||
|
['EscChar'] = m.P'\\' / '' * m.P(1),
|
||
|
['Char'] = object('char', m.Cs((m.V'EscChar' + m.V'SimpleChar')^1)),
|
||
|
['FSymbol'] = object('**', m.P'**'),
|
||
|
['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1,
|
||
|
['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1),
|
||
|
}
|
||
|
|
||
|
---@class gitignore
|
||
|
---@field pattern string[]
|
||
|
---@field options table
|
||
|
---@field errors table[]
|
||
|
---@field matcher table
|
||
|
---@field interface function[]
|
||
|
---@field data table
|
||
|
local mt = {}
|
||
|
mt.__index = mt
|
||
|
mt.__name = 'gitignore'
|
||
|
|
||
|
function mt:addPattern(pat)
|
||
|
if type(pat) ~= 'string' then
|
||
|
return
|
||
|
end
|
||
|
self.pattern[#self.pattern+1] = pat
|
||
|
if self.options.ignoreCase then
|
||
|
pat = pat:lower()
|
||
|
end
|
||
|
local states, err = parser:match(pat)
|
||
|
if not states then
|
||
|
self.errors[#self.errors+1] = {
|
||
|
pattern = pat,
|
||
|
message = err
|
||
|
}
|
||
|
return
|
||
|
end
|
||
|
for _, state in ipairs(states) do
|
||
|
self.matcher[#self.matcher+1] = matcher(state)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function mt:setOption(op, val)
|
||
|
if val == nil then
|
||
|
val = true
|
||
|
end
|
||
|
self.options[op] = val
|
||
|
end
|
||
|
|
||
|
---@param key string | "'type'" | "'list'"
|
||
|
---@param func function | "function (path) end"
|
||
|
function mt:setInterface(key, func)
|
||
|
if type(func) ~= 'function' then
|
||
|
return
|
||
|
end
|
||
|
self.interface[key] = func
|
||
|
end
|
||
|
|
||
|
function mt:callInterface(name, params)
|
||
|
local func = self.interface[name]
|
||
|
return func(params, self.data)
|
||
|
end
|
||
|
|
||
|
function mt:hasInterface(name)
|
||
|
return self.interface[name] ~= nil
|
||
|
end
|
||
|
|
||
|
function mt:checkDirectory(catch, path, matcher)
|
||
|
if not self:hasInterface 'type' then
|
||
|
return true
|
||
|
end
|
||
|
if not matcher:isNeedDirectory() then
|
||
|
return true
|
||
|
end
|
||
|
if #catch < #path then
|
||
|
-- if path is 'a/b/c' and catch is 'a/b'
|
||
|
-- then the catch must be a directory
|
||
|
return true
|
||
|
else
|
||
|
return self:callInterface('type', path) == 'directory'
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function mt:simpleMatch(path)
|
||
|
path = self:getRelativePath(path)
|
||
|
for i = #self.matcher, 1, -1 do
|
||
|
local matcher = self.matcher[i]
|
||
|
local catch = matcher(path)
|
||
|
if catch and self:checkDirectory(catch, path, matcher) then
|
||
|
if matcher:isNegative() then
|
||
|
return false
|
||
|
else
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return nil
|
||
|
end
|
||
|
|
||
|
function mt:finishMatch(path)
|
||
|
local paths = {}
|
||
|
for filename in path:gmatch '[^/\\]+' do
|
||
|
paths[#paths+1] = filename
|
||
|
end
|
||
|
for i = 1, #paths do
|
||
|
local newPath = table.concat(paths, '/', 1, i)
|
||
|
local passed = self:simpleMatch(newPath)
|
||
|
if passed == true then
|
||
|
return true
|
||
|
elseif passed == false then
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
function mt:getRelativePath(path)
|
||
|
local root = self.options.root or ''
|
||
|
if self.options.ignoreCase then
|
||
|
path = path:lower()
|
||
|
root = root:lower()
|
||
|
end
|
||
|
path = path:gsub('^[/\\]+', ''):gsub('[/\\]+', '/')
|
||
|
root = root:gsub('^[/\\]+', ''):gsub('[/\\]+', '/')
|
||
|
if path:sub(1, #root) == root then
|
||
|
path = path:sub(#root + 1)
|
||
|
path = path:gsub('^[/\\]+', '')
|
||
|
end
|
||
|
return path
|
||
|
end
|
||
|
|
||
|
---@param callback async fun(path: string)
|
||
|
---@param hook? async fun(ev: string, ...)
|
||
|
---@async
|
||
|
function mt:scan(path, callback, hook)
|
||
|
local files = {}
|
||
|
local list = {}
|
||
|
|
||
|
---@async
|
||
|
local function check(current)
|
||
|
local fileType = self:callInterface('type', current)
|
||
|
if fileType == 'file' then
|
||
|
if callback then
|
||
|
callback(current)
|
||
|
end
|
||
|
files[#files+1] = current
|
||
|
elseif fileType == 'directory' then
|
||
|
local result = self:callInterface('list', current)
|
||
|
if type(result) == 'table' then
|
||
|
for _, path in ipairs(result) do
|
||
|
local filename = path:match '([^/\\]+)[/\\]*$'
|
||
|
if filename
|
||
|
and filename ~= '.'
|
||
|
and filename ~= '..' then
|
||
|
list[#list+1] = path
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if not self:simpleMatch(path) then
|
||
|
check(path)
|
||
|
end
|
||
|
while #list > 0 do
|
||
|
local current = list[#list]
|
||
|
if not current then
|
||
|
break
|
||
|
end
|
||
|
list[#list] = nil
|
||
|
if hook then
|
||
|
hook('scan', current)
|
||
|
end
|
||
|
if not self:simpleMatch(current) then
|
||
|
check(current)
|
||
|
end
|
||
|
end
|
||
|
return files
|
||
|
end
|
||
|
|
||
|
function mt:__call(path)
|
||
|
path = self:getRelativePath(path)
|
||
|
return self:finishMatch(path)
|
||
|
end
|
||
|
|
||
|
return function (pattern, options, interface)
|
||
|
local self = setmetatable({
|
||
|
pattern = {},
|
||
|
options = {},
|
||
|
matcher = {},
|
||
|
errors = {},
|
||
|
interface = {},
|
||
|
data = {},
|
||
|
}, mt)
|
||
|
|
||
|
if type(options) == 'table' then
|
||
|
for op, val in pairs(options) do
|
||
|
self:setOption(op, val)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if type(pattern) == 'table' then
|
||
|
for _, pat in ipairs(pattern) do
|
||
|
self:addPattern(pat)
|
||
|
end
|
||
|
else
|
||
|
self:addPattern(pattern)
|
||
|
end
|
||
|
|
||
|
if type(interface) == 'table' then
|
||
|
for key, func in pairs(interface) do
|
||
|
self:setInterface(key, func)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return self
|
||
|
end
|