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