801 lines
23 KiB
JavaScript
801 lines
23 KiB
JavaScript
const VALID_DAYS = {
|
|
monday: 'Monday',
|
|
mon: 'Monday',
|
|
tuesday: 'Tuesday',
|
|
tue: 'Tuesday',
|
|
wednesday: 'Wednesday',
|
|
wed: 'Wednesday',
|
|
thursday: 'Thursday',
|
|
thu: 'Thursday',
|
|
friday: 'Friday',
|
|
fri: 'Friday',
|
|
saturday: 'Saturday',
|
|
sat: 'Saturday',
|
|
sunday: 'Sunday',
|
|
sun: 'Sunday',
|
|
}
|
|
|
|
const VALID_MONTHS = {
|
|
january: 'January',
|
|
jan: 'January',
|
|
february: 'February',
|
|
feb: 'February',
|
|
march: 'March',
|
|
mar: 'March',
|
|
april: 'April',
|
|
apr: 'April',
|
|
may: 'May',
|
|
june: 'June',
|
|
jun: 'June',
|
|
july: 'July',
|
|
jul: 'July',
|
|
august: 'August',
|
|
aug: 'August',
|
|
september: 'September',
|
|
sep: 'September',
|
|
october: 'October',
|
|
oct: 'October',
|
|
november: 'November',
|
|
nov: 'November',
|
|
december: 'December',
|
|
dec: 'December',
|
|
}
|
|
|
|
const ALL_MONTHS = Object.values(VALID_MONTHS).filter(
|
|
(v, i, a) => a.indexOf(v) === i,
|
|
)
|
|
|
|
// Helper function to validate word boundaries for date/time matches
|
|
// Prevents matching partial words like 'wed' in 'wedding', 'fri' in 'friend', etc.
|
|
const isValidWordBoundary = (text, matchIndex, matchLength) => {
|
|
const charBefore = matchIndex > 0 ? text[matchIndex - 1] : ' '
|
|
const charAfter =
|
|
matchIndex + matchLength < text.length
|
|
? text[matchIndex + matchLength]
|
|
: ' '
|
|
// Valid boundaries: spaces, punctuation, but NOT alphanumeric or word-forming characters
|
|
return !(
|
|
/[\p{L}\p{N}_]/u.test(charBefore) || /[\p{L}\p{N}_]/u.test(charAfter)
|
|
)
|
|
}
|
|
|
|
export const parsePriority = inputSentence => {
|
|
let sentence = inputSentence.toLowerCase()
|
|
const priorityMap = {
|
|
1: ['!p1', 'priority 1', 'high priority', 'urgent', 'asap', 'important'],
|
|
2: ['!p2', 'priority 2', 'medium priority'],
|
|
3: ['!p3', 'priority 3', 'low priority'],
|
|
4: ['!p4', 'priority 4'],
|
|
}
|
|
|
|
for (const [priority, terms] of Object.entries(priorityMap)) {
|
|
if (terms.some(term => sentence.includes(term))) {
|
|
return {
|
|
result: priority,
|
|
highlight: terms
|
|
.map(term => {
|
|
const index = sentence.indexOf(term)
|
|
return {
|
|
text: term,
|
|
start: index,
|
|
end: index + term.length,
|
|
}
|
|
})
|
|
.filter(term => term.start !== -1),
|
|
|
|
cleanedSentence: sentence.replace(
|
|
new RegExp(`(${terms.join('|')})`, 'g'),
|
|
'',
|
|
),
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
result: 0,
|
|
highlight: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
export const parseLabels = (inputSentence, userLabels) => {
|
|
let sentence = inputSentence.toLowerCase()
|
|
const currentLabels = []
|
|
const newLabels = []
|
|
const allHighlights = []
|
|
|
|
// Find all #label patterns in the sentence
|
|
// Use [\p{L}\p{N}_]+ to support Unicode letters (including umlauts) and numbers
|
|
const labelPattern = /#([\p{L}\p{N}_]+)/giu
|
|
const matches = [...inputSentence.matchAll(labelPattern)]
|
|
|
|
for (const match of matches) {
|
|
const labelName = match[1]
|
|
const fullMatch = match[0]
|
|
const startIndex = match.index
|
|
|
|
// Check if this label already exists
|
|
const existingLabel = userLabels.find(
|
|
label => label.name.toLowerCase() === labelName.toLowerCase(),
|
|
)
|
|
|
|
if (existingLabel) {
|
|
currentLabels.push(existingLabel)
|
|
} else {
|
|
// Create a new label object for new labels
|
|
newLabels.push({
|
|
name: labelName,
|
|
color: '#3b82f6', // Default blue color
|
|
isNew: true,
|
|
})
|
|
}
|
|
|
|
allHighlights.push({
|
|
text: fullMatch,
|
|
start: startIndex,
|
|
end: startIndex + fullMatch.length,
|
|
})
|
|
|
|
// Remove the label from the sentence
|
|
sentence = sentence.replace(fullMatch.toLowerCase(), '')
|
|
}
|
|
|
|
const allLabels = [...currentLabels, ...newLabels]
|
|
|
|
if (allLabels.length > 0) {
|
|
return {
|
|
result: allLabels,
|
|
newLabels: newLabels,
|
|
highlight: allHighlights,
|
|
cleanedSentence: inputSentence
|
|
.replace(labelPattern, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim(),
|
|
}
|
|
}
|
|
|
|
return {
|
|
result: null,
|
|
newLabels: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
|
|
export const parseRepeatV2 = inputSentence => {
|
|
const sentence = inputSentence.toLowerCase()
|
|
const lowerInputSentence = inputSentence.toLowerCase()
|
|
const result = {
|
|
frequency: 1,
|
|
frequencyType: null,
|
|
frequencyMetadata: {
|
|
days: [],
|
|
months: [],
|
|
unit: null,
|
|
time: new Date().toISOString(),
|
|
},
|
|
}
|
|
|
|
const patterns = [
|
|
{
|
|
frequencyType: 'day_of_the_month:every',
|
|
regex: /(\d+)(?:th|st|nd|rd)? of every month/i,
|
|
name: 'Every {day} of every month',
|
|
},
|
|
{
|
|
frequencyType: 'days_of_the_week:nth_occurrence',
|
|
regex:
|
|
/(first|second|third|fourth|last|\d+(?:st|nd|rd|th)?) (monday|tuesday|wednesday|thursday|friday|saturday|sunday) of (?:the )?month/i,
|
|
name: '{occurrence} {day} of the month',
|
|
},
|
|
{
|
|
frequencyType: 'days_of_the_week:nth_occurrence_multiple',
|
|
regex:
|
|
/((?:first|second|third|fourth|last|\d+(?:st|nd|rd|th)?)(?:,? (?:and |& )?))+\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)s? of (?:the )?month/i,
|
|
name: '{occurrences} {day} of the month',
|
|
},
|
|
{
|
|
frequencyType: 'daily',
|
|
regex: /(every day|daily|everyday)/i,
|
|
name: 'Every day',
|
|
},
|
|
{
|
|
frequencyType: 'daily:time',
|
|
regex: /every (morning|noon|afternoon|evening|night)/i,
|
|
name: 'Every {time} daily',
|
|
},
|
|
{
|
|
frequencyType: 'weekly',
|
|
regex: /(every week|weekly)/i,
|
|
name: 'Every week',
|
|
},
|
|
{
|
|
frequencyType: 'monthly',
|
|
regex: /(every month|monthly)/i,
|
|
name: 'Every month',
|
|
},
|
|
{
|
|
frequencyType: 'yearly',
|
|
regex: /every year/i,
|
|
name: 'Every year',
|
|
},
|
|
{
|
|
frequencyType: 'monthly',
|
|
regex: /every (?:other )?month/i,
|
|
name: 'Bi Monthly',
|
|
value: 2,
|
|
},
|
|
{
|
|
frequencyType: 'interval:2week',
|
|
regex: /(bi-?weekly|every other week)/i,
|
|
value: 2,
|
|
name: 'Bi Weekly',
|
|
},
|
|
{
|
|
frequencyType: 'interval',
|
|
regex: /every (\d+) (days?|weeks?|months?|years?)/i,
|
|
name: 'Every {frequency} {unit}',
|
|
},
|
|
{
|
|
frequencyType: 'interval:every_other',
|
|
regex: /every other (days?|weeks?|months?|years?)/i,
|
|
name: 'Every other {unit}',
|
|
},
|
|
{
|
|
frequencyType: 'days_of_the_week',
|
|
regex: /every ([\w, ]+(?:day)?(?:, [\w, ]+(?:day)?)*)/i,
|
|
name: 'Every {days}',
|
|
},
|
|
{
|
|
frequencyType: 'day_of_the_month',
|
|
regex: /(\d+)(?:st|nd|rd|th)? of ([\w ]+(?:(?:,| and |\s)[\w ]+)*)/i,
|
|
name: 'Every {day} days of {months}',
|
|
},
|
|
]
|
|
|
|
for (const pattern of patterns) {
|
|
const match = sentence.match(pattern.regex)
|
|
if (!match) continue
|
|
|
|
result.frequencyType = pattern.frequencyType
|
|
const unitMap = {
|
|
daily: 'days',
|
|
weekly: 'weeks',
|
|
monthly: 'months',
|
|
yearly: 'years',
|
|
}
|
|
|
|
switch (pattern.frequencyType) {
|
|
case 'daily':
|
|
case 'weekly':
|
|
case 'monthly':
|
|
case 'yearly':
|
|
result.frequencyType = 'interval'
|
|
result.frequency = pattern.value || 1
|
|
result.frequencyMetadata.unit = unitMap[pattern.frequencyType]
|
|
return {
|
|
result,
|
|
name: pattern.name,
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'interval':
|
|
result.frequency = parseInt(match[1], 10)
|
|
result.frequencyMetadata.unit = match[2]
|
|
return {
|
|
result,
|
|
name: pattern.name
|
|
.replace('{frequency}', result.frequency)
|
|
.replace('{unit}', result.frequencyMetadata.unit),
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'days_of_the_week':
|
|
result.frequencyMetadata.days = match[1]
|
|
.toLowerCase()
|
|
.split(/ and |,|\s/)
|
|
.map(day => day.trim())
|
|
.filter(day => {
|
|
// Validate that the day abbreviation is at proper word boundaries
|
|
// This prevents matches like 'wed' in 'wedding', 'fri' in 'friend'
|
|
if (!VALID_DAYS[day]) return false
|
|
// For short abbreviations (3 chars or less), validate word boundaries
|
|
if (day.length <= 3) {
|
|
const dayIndex = sentence.toLowerCase().indexOf(day)
|
|
if (dayIndex === -1) return false
|
|
return isValidWordBoundary(
|
|
sentence.toLowerCase(),
|
|
dayIndex,
|
|
day.length,
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
.map(day => VALID_DAYS[day])
|
|
if (!result.frequencyMetadata.days.length)
|
|
return { result: null, name: null, cleanedSentence: inputSentence }
|
|
return {
|
|
result,
|
|
name: pattern.name.replace(
|
|
'{days}',
|
|
result.frequencyMetadata.days.join(', '),
|
|
),
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'day_of_the_month':
|
|
result.frequency = parseInt(match[1], 10)
|
|
result.frequencyMetadata.months = match[2]
|
|
.toLowerCase()
|
|
.split(/ and |,|\s/)
|
|
.map(month => month.trim())
|
|
.filter(month => {
|
|
if (!VALID_MONTHS[month]) return false
|
|
// For short abbreviations (3 chars or less), validate word boundaries
|
|
if (month.length <= 3) {
|
|
const monthIndex = sentence.toLowerCase().indexOf(month)
|
|
if (monthIndex === -1) return false
|
|
return isValidWordBoundary(
|
|
sentence.toLowerCase(),
|
|
monthIndex,
|
|
month.length,
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
.map(month => VALID_MONTHS[month])
|
|
result.frequencyMetadata.unit = 'days'
|
|
return {
|
|
result,
|
|
name: pattern.name
|
|
.replace('{day}', result.frequency)
|
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
case 'interval:2week':
|
|
result.frequency = 2
|
|
result.frequencyMetadata.unit = 'weeks'
|
|
result.frequencyType = 'interval'
|
|
return {
|
|
result,
|
|
name: pattern.name,
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
case 'daily:time':
|
|
result.frequency = 1
|
|
result.frequencyMetadata.unit = 'days'
|
|
result.frequencyType = 'daily'
|
|
return {
|
|
result,
|
|
name: pattern.name.replace('{time}', match[1]),
|
|
// replace every x with ''
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'day_of_the_month:every':
|
|
result.frequency = parseInt(match[1], 10)
|
|
result.frequencyMetadata.months = ALL_MONTHS
|
|
result.frequencyMetadata.unit = 'days'
|
|
return {
|
|
result,
|
|
name: pattern.name
|
|
.replace('{day}', result.frequency)
|
|
.replace('{months}', result.frequencyMetadata.months.join(', ')),
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
case 'interval:every_other':
|
|
result.frequency = 2
|
|
result.frequencyMetadata.unit = match[1]
|
|
result.frequencyType = 'interval'
|
|
return {
|
|
result,
|
|
name: pattern.name.replace('{unit}', result.frequencyMetadata.unit),
|
|
highlight: [
|
|
{
|
|
text: pattern.name,
|
|
start: lowerInputSentence.indexOf(match[0]),
|
|
end: lowerInputSentence.indexOf(match[0]) + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'days_of_the_week:nth_occurrence':
|
|
const occurrenceText = match[1].toLowerCase()
|
|
const dayName = match[2].toLowerCase()
|
|
|
|
// Map occurrence words to numbers
|
|
const occurrenceMap = {
|
|
first: 1,
|
|
'1st': 1,
|
|
second: 2,
|
|
'2nd': 2,
|
|
third: 3,
|
|
'3rd': 3,
|
|
fourth: 4,
|
|
'4th': 4,
|
|
last: -1,
|
|
}
|
|
|
|
const occurrence =
|
|
occurrenceMap[occurrenceText] ||
|
|
parseInt(occurrenceText.replace(/\D/g, ''), 10)
|
|
|
|
if (!VALID_DAYS[dayName]) {
|
|
return { result: null, name: null, cleanedSentence: inputSentence }
|
|
}
|
|
|
|
result.frequencyType = 'days_of_the_week'
|
|
result.frequencyMetadata.days = [VALID_DAYS[dayName].toLowerCase()]
|
|
result.frequencyMetadata.weekPattern = 'week_of_month'
|
|
result.frequencyMetadata.occurrences = [occurrence]
|
|
|
|
const startIndex = inputSentence
|
|
.toLowerCase()
|
|
.indexOf(match[0].toLowerCase())
|
|
return {
|
|
result,
|
|
name: pattern.name
|
|
.replace('{occurrence}', match[1])
|
|
.replace('{day}', VALID_DAYS[dayName]),
|
|
highlight: [
|
|
{
|
|
text: inputSentence.substring(
|
|
startIndex,
|
|
startIndex + match[0].length,
|
|
),
|
|
start: startIndex,
|
|
end: startIndex + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
|
|
case 'days_of_the_week:nth_occurrence_multiple':
|
|
const occurrencesText = match[1].toLowerCase()
|
|
const dayName2 = match[2].toLowerCase()
|
|
|
|
if (!VALID_DAYS[dayName2]) {
|
|
return { result: null, name: null, cleanedSentence: inputSentence }
|
|
}
|
|
|
|
// Parse multiple occurrences like "first, second and third"
|
|
const occurrences = occurrencesText
|
|
.replace(/,?\s*(and|&)\s*/g, ' ')
|
|
.split(/\s+/)
|
|
.filter(word => word.trim())
|
|
.map(word => {
|
|
const cleanWord = word.replace(',', '').trim()
|
|
const occurrenceMap = {
|
|
first: 1,
|
|
'1st': 1,
|
|
second: 2,
|
|
'2nd': 2,
|
|
third: 3,
|
|
'3rd': 3,
|
|
fourth: 4,
|
|
'4th': 4,
|
|
last: -1,
|
|
}
|
|
return (
|
|
occurrenceMap[cleanWord] ||
|
|
parseInt(cleanWord.replace(/\D/g, ''), 10)
|
|
)
|
|
})
|
|
.filter(num => !isNaN(num))
|
|
|
|
result.frequencyType = 'days_of_the_week'
|
|
result.frequencyMetadata.days = [VALID_DAYS[dayName2].toLowerCase()]
|
|
result.frequencyMetadata.weekPattern = 'week_of_month'
|
|
result.frequencyMetadata.occurrences = occurrences
|
|
|
|
const startIndex2 = inputSentence
|
|
.toLowerCase()
|
|
.indexOf(match[0].toLowerCase())
|
|
return {
|
|
result,
|
|
name: pattern.name
|
|
.replace('{occurrences}', match[1])
|
|
.replace('{day}', VALID_DAYS[dayName2]),
|
|
highlight: [
|
|
{
|
|
text: inputSentence.substring(
|
|
startIndex2,
|
|
startIndex2 + match[0].length,
|
|
),
|
|
start: startIndex2,
|
|
end: startIndex2 + match[0].length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence.replace(match[0], '').trim(),
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
result: null,
|
|
name: null,
|
|
highlight: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
|
|
export const parseAssignees = (inputSentence, users) => {
|
|
const sentence = inputSentence.toLowerCase()
|
|
const result = []
|
|
const highlight = []
|
|
const matchedTexts = []
|
|
|
|
// Check for @Anyone first (special case)
|
|
const anyoneRegex = /@anyone(?=\s|$)/i
|
|
const anyoneMatch = inputSentence.match(anyoneRegex)
|
|
|
|
if (anyoneMatch) {
|
|
const index = inputSentence.search(anyoneRegex)
|
|
highlight.push({
|
|
text: anyoneMatch[0],
|
|
start: index,
|
|
end: index + anyoneMatch[0].length,
|
|
})
|
|
matchedTexts.push({ pattern: '@anyone', original: anyoneMatch[0] })
|
|
|
|
// For @Anyone, return empty result (no specific assignees)
|
|
let cleanedSentence = inputSentence
|
|
for (const matchedText of matchedTexts) {
|
|
cleanedSentence = cleanedSentence.replace(
|
|
new RegExp(
|
|
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
'gi',
|
|
),
|
|
'',
|
|
)
|
|
}
|
|
|
|
return {
|
|
result: [], // Empty assignees for @Anyone
|
|
isAnyone: true, // Flag to indicate @Anyone was used
|
|
highlight,
|
|
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
|
|
}
|
|
}
|
|
|
|
// Sort users by longest displayName first to avoid partial matches
|
|
const sortedUsers = users.sort(
|
|
(a, b) => (b.displayName?.length || 0) - (a.displayName?.length || 0),
|
|
)
|
|
|
|
for (const user of sortedUsers) {
|
|
if (!user.displayName) continue
|
|
|
|
// Only match on display name - use word boundaries for exact matching
|
|
const displayNamePattern = `@${user.displayName.toLowerCase()}`
|
|
// Use word boundary or space/end to ensure exact match, not partial
|
|
const exactMatchRegex = new RegExp(
|
|
`@${user.displayName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`,
|
|
'i',
|
|
)
|
|
const exactMatch = inputSentence.match(exactMatchRegex)
|
|
|
|
if (exactMatch && !result.some(r => r.userId === user.userId)) {
|
|
result.push(user)
|
|
const index = inputSentence.search(exactMatchRegex)
|
|
|
|
highlight.push({
|
|
text: exactMatch[0],
|
|
start: index,
|
|
end: index + exactMatch[0].length,
|
|
})
|
|
matchedTexts.push({
|
|
pattern: displayNamePattern,
|
|
original: exactMatch[0],
|
|
})
|
|
}
|
|
}
|
|
|
|
if (result.length > 0) {
|
|
let cleanedSentence = inputSentence
|
|
// Remove all matched assignee patterns using the original matched text
|
|
for (const matchedText of matchedTexts) {
|
|
cleanedSentence = cleanedSentence.replace(
|
|
new RegExp(
|
|
matchedText.original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
'gi',
|
|
),
|
|
'',
|
|
)
|
|
}
|
|
|
|
return {
|
|
result,
|
|
isAnyone: false,
|
|
highlight,
|
|
cleanedSentence: cleanedSentence.replace(/\s+/g, ' ').trim(),
|
|
}
|
|
}
|
|
|
|
return { result: null, isAnyone: false, cleanedSentence: inputSentence }
|
|
}
|
|
|
|
export const parsePoints = inputSentence => {
|
|
let sentence = inputSentence.toLowerCase()
|
|
const pointsPattern = /\*(\d+)\s*(?:points?)?/gi
|
|
const match = sentence.match(pointsPattern)
|
|
|
|
if (!match) {
|
|
return {
|
|
result: null,
|
|
highlight: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
|
|
// Extract the first points match
|
|
const pointsMatch = match[0]
|
|
const pointsValue = parseInt(pointsMatch.replace(/\D/g, ''), 10)
|
|
const startIndex = inputSentence
|
|
.toLowerCase()
|
|
.indexOf(pointsMatch.toLowerCase())
|
|
|
|
return {
|
|
result: pointsValue,
|
|
highlight: [
|
|
{
|
|
text: pointsMatch,
|
|
start: startIndex,
|
|
end: startIndex + pointsMatch.length,
|
|
},
|
|
],
|
|
cleanedSentence: inputSentence
|
|
.replace(
|
|
new RegExp(pointsMatch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
|
|
'',
|
|
)
|
|
.replace(/\s+/g, ' ')
|
|
.trim(),
|
|
}
|
|
}
|
|
|
|
export const parseDueDate = (inputSentence, chrono) => {
|
|
// Parse the due date using chrono
|
|
const parsedDueDate = chrono.parse(inputSentence, new Date(), {
|
|
forwardDate: true,
|
|
})
|
|
|
|
if (!parsedDueDate.length) {
|
|
return {
|
|
result: null,
|
|
highlight: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
|
|
// Select the first valid word-bounded date match
|
|
// Prevents false positives like "wed" in "wedding" while still allowing later valid matches
|
|
const dueDateMatch = parsedDueDate.find(
|
|
match =>
|
|
match.index !== -1 &&
|
|
isValidWordBoundary(inputSentence, match.index, match.text.length),
|
|
)
|
|
|
|
if (!dueDateMatch) {
|
|
return {
|
|
result: null,
|
|
highlight: [],
|
|
cleanedSentence: inputSentence,
|
|
}
|
|
}
|
|
const dueDateText = dueDateMatch.text
|
|
const dueDateStartIndex = dueDateMatch.index
|
|
const dueDateEndIndex = dueDateStartIndex + dueDateText.length
|
|
|
|
// Define words that might precede the due date and should be removed
|
|
const precedingWords = [
|
|
'starting',
|
|
'from',
|
|
'beginning',
|
|
'begin',
|
|
'commence',
|
|
'commencing',
|
|
]
|
|
|
|
// Look for preceding words before the due date
|
|
let cleanStartIndex = dueDateStartIndex
|
|
let highlightStartIndex = dueDateStartIndex
|
|
let precedingWord = ''
|
|
|
|
// Extract text before the due date to check for preceding words
|
|
const textBeforeDueDate = inputSentence.substring(0, dueDateStartIndex).trim()
|
|
|
|
for (const word of precedingWords) {
|
|
// Check if the text before due date ends with this preceding word
|
|
const wordPattern = new RegExp(`\\b${word}\\s*$`, 'i')
|
|
const match = textBeforeDueDate.match(wordPattern)
|
|
|
|
if (match) {
|
|
// Found a preceding word, include it in the text to be removed
|
|
const matchStart = textBeforeDueDate.length - match[0].length
|
|
cleanStartIndex = matchStart
|
|
highlightStartIndex = matchStart
|
|
precedingWord = match[0].trim()
|
|
break
|
|
}
|
|
}
|
|
|
|
// Create the highlight text
|
|
const fullHighlightText = precedingWord
|
|
? `${precedingWord} ${dueDateText}`
|
|
: dueDateText
|
|
|
|
// Create cleaned sentence by removing the full match (preceding word + due date)
|
|
const textToRemove = inputSentence.substring(cleanStartIndex, dueDateEndIndex)
|
|
const cleanedSentence = inputSentence
|
|
.replace(textToRemove, '')
|
|
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
|
.trim()
|
|
|
|
// If no specific time was mentioned, set to end of day (23:59:59)
|
|
// to indicate the date has no specific time tied to it (same convention as ChoreEdit)
|
|
let resultDate = dueDateMatch.start.date()
|
|
if (!dueDateMatch.start.isCertain('hour')) {
|
|
resultDate = new Date(resultDate)
|
|
resultDate.setHours(23, 59, 59, 0)
|
|
}
|
|
|
|
return {
|
|
result: resultDate,
|
|
highlight: [
|
|
{
|
|
text: fullHighlightText,
|
|
start: highlightStartIndex,
|
|
end: dueDateEndIndex,
|
|
},
|
|
],
|
|
cleanedSentence: cleanedSentence,
|
|
}
|
|
}
|