Problem: CI lua-typecheck-action reported three categories of errors: 1. parse.lua - multi-assignment of tonumber() results left y/m/d typed as number? rather than integer, failing os.time()'s field types 2. gcal.lua - url_encode returned str:gsub() which yields string+integer but the annotation declared @return string (redundant-return-value) 3. gcal.lua - calendar_id typed string? from find_or_create_calendar was passed to functions expecting string; the existing `if err` guard did not narrow the type for LuaLS Solution: replace the y/m/d multi-assignment with yn/mn/dn locals whose types resolve cleanly to integer; wrap the gsub return in parentheses to discard the count; add `or not calendar_id` to the error guard so LuaLS narrows calendar_id to string for the rest of the scope.
168 lines
3.9 KiB
Lua
168 lines
3.9 KiB
Lua
local config = require('pending.config')
|
|
|
|
---@class pending.parse
|
|
local M = {}
|
|
|
|
---@param s string
|
|
---@return boolean
|
|
local function is_valid_date(s)
|
|
local y, m, d = s:match('^(%d%d%d%d)-(%d%d)-(%d%d)$')
|
|
if not y then
|
|
return false
|
|
end
|
|
local yn = tonumber(y) --[[@as integer]]
|
|
local mn = tonumber(m) --[[@as integer]]
|
|
local dn = tonumber(d) --[[@as integer]]
|
|
if mn < 1 or mn > 12 then
|
|
return false
|
|
end
|
|
if dn < 1 or dn > 31 then
|
|
return false
|
|
end
|
|
local t = os.time({ year = yn, month = mn, day = dn })
|
|
local check = os.date('*t', t) --[[@as osdate]]
|
|
return check.year == yn and check.month == mn and check.day == dn
|
|
end
|
|
|
|
---@return string
|
|
local function date_key()
|
|
return config.get().date_syntax or 'due'
|
|
end
|
|
|
|
local weekday_map = {
|
|
sun = 1,
|
|
mon = 2,
|
|
tue = 3,
|
|
wed = 4,
|
|
thu = 5,
|
|
fri = 6,
|
|
sat = 7,
|
|
}
|
|
|
|
---@param text string
|
|
---@return string|nil
|
|
function M.resolve_date(text)
|
|
local lower = text:lower()
|
|
local today = os.date('*t') --[[@as osdate]]
|
|
|
|
if lower == 'today' then
|
|
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
|
|
end
|
|
|
|
if lower == 'tomorrow' then
|
|
return os.date(
|
|
'%Y-%m-%d',
|
|
os.time({ year = today.year, month = today.month, day = today.day + 1 })
|
|
) --[[@as string]]
|
|
end
|
|
|
|
local n = lower:match('^%+(%d+)d$')
|
|
if n then
|
|
return os.date(
|
|
'%Y-%m-%d',
|
|
os.time({
|
|
year = today.year,
|
|
month = today.month,
|
|
day = today.day + (
|
|
tonumber(n) --[[@as integer]]
|
|
),
|
|
})
|
|
) --[[@as string]]
|
|
end
|
|
|
|
local target_wday = weekday_map[lower]
|
|
if target_wday then
|
|
local current_wday = today.wday
|
|
local delta = (target_wday - current_wday) % 7
|
|
return os.date(
|
|
'%Y-%m-%d',
|
|
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
|
) --[[@as string]]
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
---@param text string
|
|
---@return string description
|
|
---@return { due?: string, cat?: string } metadata
|
|
function M.body(text)
|
|
local tokens = {}
|
|
for token in text:gmatch('%S+') do
|
|
table.insert(tokens, token)
|
|
end
|
|
|
|
local metadata = {}
|
|
local i = #tokens
|
|
local dk = date_key()
|
|
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d)$'
|
|
local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
|
|
|
|
while i >= 1 do
|
|
local token = tokens[i]
|
|
local due_val = token:match(date_pattern_strict)
|
|
if due_val then
|
|
if metadata.due then
|
|
break
|
|
end
|
|
if not is_valid_date(due_val) then
|
|
break
|
|
end
|
|
metadata.due = due_val
|
|
i = i - 1
|
|
else
|
|
local raw_val = token:match(date_pattern_any)
|
|
if raw_val then
|
|
if metadata.due then
|
|
break
|
|
end
|
|
local resolved = M.resolve_date(raw_val)
|
|
if not resolved then
|
|
break
|
|
end
|
|
metadata.due = resolved
|
|
i = i - 1
|
|
else
|
|
local cat_val = token:match('^cat:(%S+)$')
|
|
if cat_val then
|
|
if metadata.cat then
|
|
break
|
|
end
|
|
metadata.cat = cat_val
|
|
i = i - 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local desc_tokens = {}
|
|
for j = 1, i do
|
|
table.insert(desc_tokens, tokens[j])
|
|
end
|
|
local description = table.concat(desc_tokens, ' ')
|
|
|
|
return description, metadata
|
|
end
|
|
|
|
---@param text string
|
|
---@return string description
|
|
---@return { due?: string, cat?: string } metadata
|
|
function M.command_add(text)
|
|
local cat_prefix = text:match('^(%S.-):%s')
|
|
if cat_prefix then
|
|
local first_char = cat_prefix:sub(1, 1)
|
|
if first_char == first_char:upper() and first_char ~= first_char:lower() then
|
|
local rest = text:sub(#cat_prefix + 2):match('^%s*(.+)$')
|
|
if rest then
|
|
local desc, meta = M.body(rest)
|
|
meta.cat = meta.cat or cat_prefix
|
|
return desc, meta
|
|
end
|
|
end
|
|
end
|
|
return M.body(text)
|
|
end
|
|
|
|
return M
|