/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const EXPORTED_SYMBOLS = ["CalExtractParserService"]; const { CalExtractParser } = ChromeUtils.import( "resource:///modules/calendar/extract/CalExtractParser.jsm" ); const defaultRules = [ [ // Start clean up patterns. // remove last line preceding quoted message and first line of the quote [/^(\r?\n[^>].*\r?\n>+.*$)/, ""], // remove the rest of quoted content [/^(>+.*$)/, ""], // urls often contain dates dates that can confuse extraction [/^(https?:\/\/[^\s]+\s)/, ""], [/^(www\.[^\s]+\s)/, ""], // remove phone numbers [/^(\d-\d\d\d-\d\d\d-\d\d\d\d)/, ""], // remove standard signature [/^(\r?\n-- \r?\n[\S\s]+$)/, ""], // XXX remove timezone info, for now [/^(gmt[+-]\d{2}:\d{2})/i, ""], // End clean up patterns. [/^meet\b/i, "MEET"], [/^(we will|we'll|we)\b/i, "WE"], // Meridiem [/^(a[.]?m[.]?)/i, "AM"], [/^(p[.]?m[.]?)/i, "PM"], [/^(hours|hour|hrs|hr)\b/i, "HOURS"], [/^(minutes|min|mins)\b/i, "MINUTES"], [/^(days|day)\b/i, "DAYS"], // Words commonly used when specifying begin/end time or duration. [/^at\b/i, "AT"], [/^until\b/i, "UNTIL"], [/^for\b/i, "FOR"], // Units of time [/^(((0|1)?[0-9])|(2[0-4]))\b/, "HOUR_VALUE"], [/^\d+\b/, "NUMBER"], // Any text we don't know the meaning of. [/^\S+/, "TEXT"], // Whitespace [/^\s+/, ""], ], [ { name: "event-guess", patterns: ["subject", "meet", "start-time", "text*", "end-time"], action: ([, , startTime, , endTime]) => ({ type: "event-guess", startTime, endTime, priority: 0, }), }, { name: "event-guess", patterns: ["subject", "meet", "start-time", "text*", "duration-time"], action: ([, , startTime, , endTime]) => ({ type: "event-guess", startTime, endTime, priority: 0, }), }, { name: "subject", patterns: ["WE"], action: ([subject]) => ({ type: "subject", subject: subject.text, }), }, { name: "start-time", patterns: ["start-time-prefix", "meridiem-time"], action: ([, time]) => time, }, { name: "start-time-prefix", patterns: ["AT"], action: ([prefix]) => prefix.text, }, { name: "end-time", patterns: ["end-time-prefix", "meridiem-time"], action: ([, time]) => time, }, { name: "end-time-prefix", patterns: ["UNTIL"], action: ([prefix]) => prefix.text, }, { name: "meridiem-time", patterns: ["HOUR_VALUE", "meridiem"], action: ([hour, meridiem]) => ({ type: "meridiem-time", hour: Number(hour.text), meridiem, }), }, { name: "meridiem", patterns: ["AM"], action: () => "am", }, { name: "meridiem", patterns: ["PM"], action: () => "pm", }, { name: "duration-time", patterns: ["duration-prefix", "duration"], action: ([, duration]) => ({ type: "duration-time", duration, }), }, { name: "duration-prefix", patterns: ["FOR"], action: ([prefix]) => prefix.text, }, { name: "duration", patterns: ["NUMBER", "MINUTES"], action: ([value]) => Number(value.text), }, { name: "duration", patterns: ["NUMBER", "HOURS"], action: ([value]) => Number(value.text) * 60, }, { name: "duration", patterns: ["NUMBER", "DAYS"], action: ([value]) => Number(value.text) * 60 * 24, }, { name: "meet", patterns: ["MEET"], action: () => "meet", }, { name: "text", patterns: ["TEXT"], action: ([text]) => text, }, ], ]; /** * CalExtractParserServiceContext represents the context parsing and extraction * takes place in. It holds values used in various calculations. For example, * the current date. * * @typedef {object} CalExtractParserServiceContext * @property {Date} now - The Date to use when calculating start and relative * times. */ /** * CalExtractParserService provides a frontend to the CalExtractService. * It holds lexical and parse rules for multiple locales (or any string * identifier) that can be used on demand when parsing text. */ class CalExtractParserService { rules = new Map([["en-US", defaultRules]]); /** * Parses and extract the most relevant event creation data based on the * rules of the locale given. * * @param {string} source * @param {CalExtractParserServiceContext} context * @param {string} locale */ extract(source, ctx = { now: new Date() }, locale = "en-US") { let rules = this.rules.get(locale); if (!rules) { return null; } let [lex, parse] = rules; let parser = CalExtractParser.createInstance(lex, parse); let result = parser.parse(source).sort((a, b) => a - b)[0]; return result && convertDurationToEndTime(populateTimes(result, ctx.now)); } } /** * Populates the missing values of the startTime and endTime. * * @param {object?} guess - The result of CalExtractParserService.extract(). * @param {Date} now - A Date object representing the contextual date and * time. * * @returns {object} The result with the populated startTime and endTime. */ function populateTimes(guess, now) { return populateTime(populateTime(guess, now, "startTime"), now, "endTime"); } /** * Populates the missing values of the specified time property based on the Date * provided. * * @param {object?} guess * @param {Date} now * @param {string} prop * * @returns {object} */ function populateTime(guess, now, prop) { let time = guess[prop]; if (!time) { return guess; } if (time.hour && time.meridiem) { time.hour = normalizeHour(time.hour, time.meridiem); } time.year = time.year || now.getFullYear(); time.month = time.month || now.getMonth() + 1; time.day = time.day || now.getDay(); time.hour = time.hour || now.getHours(); time.minute = time.minute || now.getMinutes(); return guess; } /** * Coverts an hour using the Meridiem to a 24 hour value. * * @param {number} hour - The hour value. * @param {string} meridiem - "am" or "pm" * * @returns {number} */ function normalizeHour(hour, meridiem) { if (meridiem == "am" && hour == 12) { return hour - 12; } else if (meridiem == "pm" && hour != 12) { return hour + 12; } let dayStart = Services.prefs.getIntPref("calendar.view.daystarthour", 6); if (hour < dayStart && hour <= 11) { return hour + 12; } return hour; } /** * Takes care of converting an end duration to an actual time relative to the * start time detected (if any). * * @param {object} guess - Results from CalExtractParserService#extract() * * @returns {object} The result with the endTime property expanded. */ function convertDurationToEndTime(guess) { if (guess.startTime && guess.endTime && guess.endTime.type == "duration-time") { let startTime = guess.startTime; let duration = guess.endTime.duration; if (duration != 0) { let startDate = new Date(startTime.year, startTime.month - 1, startTime.day); if ("hour" in startTime) { startDate.setHours(startTime.hour); startDate.setMinutes(startTime.minute); } let endDate = new Date(startDate.getTime() + duration * 60 * 1000); let endTime = { type: "date-time" }; endTime.year = endDate.getFullYear(); endTime.month = endDate.getMonth() + 1; endTime.day = endDate.getDate(); if (endDate.getHours() != 0 || endDate.getMinutes() != 0) { endTime.hour = endDate.getHours(); endTime.minute = endDate.getMinutes(); } guess.endTime = endTime; } } return guess; }