├── .gitignore ├── .editorconfig ├── .vscode └── tasks.json ├── webpack.config.js ├── src ├── lib │ ├── Project.js │ ├── ThingsDateTime.js │ ├── TasksParser.js │ ├── Symbols.js │ ├── AutoTagger.js │ ├── StreamParser.js │ ├── Task.js │ └── ThingsDate.js ├── date-picker.js ├── config.js └── add-tasks.js ├── package.json ├── CHANGELOG.md ├── dist ├── date-picker.js └── add-tasks.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 2 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | const config = { 4 | entry: { 5 | 'add-tasks': './src/add-tasks.js', 6 | 'date-picker': './src/date-picker.js' 7 | }, 8 | output: { 9 | filename: '[name].js', 10 | path: `${__dirname}/dist` 11 | }, 12 | module: { 13 | rules: [{ 14 | test: /\.js$/, 15 | exclude: /(node_modules|bower_components)/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/preset-env'], 20 | plugins: [require('@babel/plugin-proposal-object-rest-spread')] 21 | } 22 | } 23 | }] 24 | }, 25 | mode: 'none', 26 | plugins: [ 27 | new webpack.optimize.ModuleConcatenationPlugin() 28 | ] 29 | }; 30 | 31 | module.exports = config; 32 | -------------------------------------------------------------------------------- /src/lib/Project.js: -------------------------------------------------------------------------------- 1 | export class Project { 2 | 3 | constructor(name, tasks) { 4 | this._name = name; 5 | this._tasks = tasks; 6 | } 7 | 8 | toThingsObject() { 9 | // Get an array of unique (case-insensitive) headings 10 | let headings = this._tasks 11 | .filter(item => item.attributes.heading) 12 | .map(item => item.attributes.heading) 13 | .map(item => ({ value: item, lower: item.toLowerCase() })) 14 | .filter((elem, pos, arr) => arr.findIndex(item => item.lower == elem.lower) == pos) 15 | .map(item => ({ type: "heading", attributes: { title: item.value } })); 16 | 17 | let tasks = this._tasks.map(task => { 18 | task.attributes.list = this._name; 19 | return task; 20 | }); 21 | 22 | return [{ 23 | type: "project", 24 | attributes: { 25 | title: this._name, 26 | items: headings 27 | } 28 | }, ...tasks]; 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/ThingsDateTime.js: -------------------------------------------------------------------------------- 1 | import { ThingsDate } from './ThingsDate'; 2 | 3 | /** 4 | * A class representing a datetime in Things 5 | * @extends ThingsDate 6 | * */ 7 | export class ThingsDateTime extends ThingsDate { 8 | 9 | /** Create a new ThingsDate object */ 10 | constructor() { 11 | super(); 12 | this._allowTime = true; 13 | } 14 | 15 | /** 16 | * The date, with optional time. In addition to fuzzy values parseable by 17 | * DateJS, valid values include "someday", "anytime", "evening", and "tonight". 18 | * @type {String} 19 | */ 20 | set datetime(value) { 21 | this._datetime = value.trim().toLowerCase(); 22 | } 23 | 24 | /** 25 | * The time override. The datetime value's hour and minute 26 | * will be repolaced with the time override if specified. 27 | * @type {String} 28 | */ 29 | set timeOverride(value) { 30 | this._timeOverride = value.trim().toLowerCase(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thingy", 3 | "version": "1.3.2", 4 | "description": "A custom Drafts 5 action for processing Things tasks", 5 | "homepage": "https://actions.getdrafts.com/g/1HW", 6 | "scripts": { 7 | "build": "npx webpack", 8 | "watch": "npx webpack --watch" 9 | }, 10 | "keywords": [ 11 | "things", 12 | "drafts" 13 | ], 14 | "author": { 15 | "name": "Daniel G. Budiac", 16 | "email": "dan@budi.ac", 17 | "url": "http://dan.budi.ac/" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/dansays/thingy.git" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.0.0-beta.46", 25 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.46", 26 | "@babel/preset-env": "^7.0.0-beta.46", 27 | "babel-loader": "^8.0.0-beta.2", 28 | "webpack": "^4.6.0", 29 | "webpack-cli": "^2.0.15" 30 | }, 31 | "dependencies": { 32 | "drafts-template-parser": "^1.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/date-picker.js: -------------------------------------------------------------------------------- 1 | const prompt = Prompt.create(); 2 | prompt.title = 'Select a date...'; 3 | prompt.addButton('Today'); 4 | prompt.addButton('Tonight'); 5 | prompt.addButton('Tomorrow'); 6 | 7 | const today = Date.today(); 8 | const tomorrow = Date.today().addDays(1); 9 | const isTodayFriday = today.is().friday(); 10 | const isTomorrowFriday = tomorrow.is().friday(); 11 | const isWeekend = today.is().saturday() || today.is().sunday(); 12 | 13 | if (!isTodayFriday) { 14 | if (isWeekend) prompt.addButton('Next Friday'); 15 | else if (!isTomorrowFriday) prompt.addButton('Friday'); 16 | } 17 | 18 | prompt.addButton(isWeekend ? 'Next Weekend' : 'This Weekend'); 19 | prompt.addButton(isWeekend ? 'Monday' : 'Next Week'); 20 | prompt.addButton('Other...'); 21 | 22 | if (prompt.show()) { 23 | let date = prompt.buttonPressed; 24 | if (date == 'Other...') date = ''; 25 | 26 | const buttonMap = { 27 | 'Next Week': 'Monday', 28 | 'This Weekend': 'Saturday', 29 | 'Next Weekend': 'Next Saturday', 30 | 'Next Week': 'Monday' 31 | }; 32 | 33 | draft.setTemplateTag('pickeddate', buttonMap[date] || date); 34 | editor.activate();; 35 | } 36 | 37 | else { 38 | context.cancel(); 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/TasksParser.js: -------------------------------------------------------------------------------- 1 | import { Symbols } from './Symbols'; 2 | import { StreamParser } from './StreamParser'; 3 | import { Task } from './Task'; 4 | 5 | /** A class representing a Things task parser */ 6 | export class TasksParser { 7 | 8 | /** 9 | * Create a new Things task parser 10 | * @param {Autotagger} autotagger - An autotagger reference 11 | */ 12 | constructor(autotagger) { 13 | this._symbols = new Symbols(); 14 | this._streamParser = new StreamParser(this._symbols); 15 | this._autotagger = autotagger; 16 | } 17 | 18 | /** 19 | * Parse a document containing Things tasks, and associated 20 | * attributes decorated by the appropriate Emoji symbols 21 | * @param stream {String} - The document to parse 22 | * @return {Object} An object suitable to pass to the things:/// service 23 | */ 24 | parse(stream) { 25 | 26 | let items = this._streamParser.parse(stream); 27 | let tasks = items.map(item => { 28 | let task = new Task(this._autotagger); 29 | Object.keys(item).forEach(attr => { 30 | switch(attr) { 31 | case 'tags': task.addTags(item[attr]); break; 32 | case 'checklistItem': task.addChecklistItem(item[attr]); break; 33 | case 'notes': task.appendNotes(item[attr]); break; 34 | default: task[attr] = item[attr]; 35 | } 36 | }); 37 | return task; 38 | }) 39 | 40 | // Return an array of Things objects 41 | return tasks.map(task => task.toThingsObject()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // When times omit an AM/PM suffix and are before this hour, 2 | // we'll assume PM to avoid early morning alarms. 3 | export const earliestAmbiguousMorningHour = 6; 4 | 5 | // When a task date is today, and a reminder is set to a time 6 | // at this hour or later, we'll file it in the "evening" section. 7 | export const eveningStartsAtHour = 18; 8 | 9 | export const autotaggerRulesDraftTitle = 'Thingy Autotagger Rules'; 10 | export const newAutotaggerRulesMessage = `Welcome to Thingy! A draft with a few default Autotagger rules has been added to your inbox. Feel free to customize these as you see fit, and you can archive the draft if you don't want it cluttering up your inbox.`; 11 | 12 | export const defaultAutotaggerRules = 13 | `# ${autotaggerRulesDraftTitle} 14 | 15 | Starts with "Call" 🏷 Calls 16 | Starts with "Email" 🏷 Email 17 | Contains "Mom" 🏷 Mom 18 | Contains "Dad" 🏷 Dad 19 | 20 | Starts with "Waiting For|WF" 21 | 🏷 Waiting For 22 | 📆 Tomorrow 23 | ⚠️ 1 week 24 | 25 | Starts with "Drop off|Pick up|Deliver" 26 | 🏷 Errands 27 | `; 28 | 29 | export const reservedTemplateTags = [ 30 | 'body', 31 | 'clipboard', 32 | 'created_latitude', 33 | 'created_longitude', 34 | 'created', 35 | 'date', 36 | 'draft_open_url', 37 | 'draft', 38 | 'latitude', 39 | 'longitude', 40 | 'modified_latitude', 41 | 'modified_longitude', 42 | 'modified', 43 | 'selection_length', 44 | 'selection_start', 45 | 'selection', 46 | 'time', 47 | 'title', 48 | 'uuid' 49 | ]; 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3.2] 2018-06-18 4 | 5 | - **[ADDED]** Multi-line notes (thanks, [edgauthier](https://github.com/edgauthier)!) 6 | - **[FIXED]** Remove task order reversal step after Things bug fix 7 | - **[FIXED]** Various refactors and general code clean-up 8 | 9 | ## [1.3.1] 2018-05-03 10 | 11 | - **[FIXED]** Fix premature reminder time-based "This Evening" filing 12 | 13 | ## [1.3.0] 2018-04-30 14 | 15 | - **[ADDED]** Template tag processing 16 | - **[ADDED]** Simple date offset notation 17 | 18 | ## [1.2.0] 2018-04-29 19 | 20 | - **[ADDED]** Ability to create a new project 21 | 22 | ## [1.1.0] 2018-04-29 23 | 24 | - **[CHANGED]** Autotagger rules are now defined in a Draft note, rather than as regular expressions in code 25 | - **[ADDED]** Ability to assign a header to a task 26 | 27 | ## [1.0.2] 2018-04-27 28 | 29 | - **[FIXED]** Date picker runtime error when "other" selected (#1) 30 | - **[FIXED]** Relative dates sometimes parsed as past dates (#1) 31 | ## [1.0.1] 2018-04-26 32 | 33 | - **[CHANGED]** Draft not moved to trash after processing if flagged, or if a task is processed via selected text 34 | - **[CHANGED]** Updated `date-picker.js` to return value in a `[[when]]` template tag, rather than storing in the clipboard 35 | - **[CHANGED]** Prevent sending empty task list to Things, if no tasks are found 36 | - **[ADDED]** Additional autotagger instructions to documentation 37 | - **[FIXED]** Bring focus to the editor after date picker action has completed 38 | -------------------------------------------------------------------------------- /src/lib/Symbols.js: -------------------------------------------------------------------------------- 1 | /** A class representing a symbols dictionary */ 2 | export class Symbols { 3 | 4 | /** 5 | * Create a symbols dictionary 6 | * @param {Object} config - An optional object overriding one or 7 | * more symbol definitions 8 | */ 9 | constructor() { 10 | this._symbols = [ 11 | { symbol: '🏷', type: 'tags', format: 'csv' }, 12 | { symbol: '📁', type: 'list', format: 'string' }, 13 | { symbol: '📆', type: 'when', format: 'string' }, 14 | { symbol: '⏰', type: 'reminder', format: 'string' }, 15 | { symbol: '⚠️', type: 'deadline', format: 'string' }, 16 | { symbol: '📌', type: 'heading', format: 'string' }, 17 | { symbol: '🗒', type: 'notes', format: 'array' }, 18 | { symbol: '🔘', type: 'checklistItem', format: 'array' } 19 | ]; 20 | } 21 | 22 | /** 23 | * An array of all defined symbols. 24 | * @type {String[]} 25 | */ 26 | get all() { 27 | return this._symbols.map(item => item.symbol); 28 | } 29 | 30 | /** 31 | * Look up a symbol based on an attribute name 32 | * @param {String} type - A valid Things to-do attribute name 33 | * @return {String} 34 | */ 35 | getSymbol(type) { 36 | let item = this._lookup(type) 37 | return item && item.symbol; 38 | } 39 | 40 | /** 41 | * Look up an attribute name based on a symbol 42 | * @param {String} symbol - A symbol (emoji) 43 | * @return {String} 44 | */ 45 | getType(symbol) { 46 | let item = this._lookup(symbol); 47 | return item && item.type; 48 | } 49 | 50 | /** 51 | * Look up a datatype based on a symbol 52 | * @param {String} symbol - A symbol (emoji) 53 | */ 54 | getFormat(val) { 55 | let item = this._lookup(val); 56 | return item && item.format; 57 | } 58 | 59 | _lookup(val) { 60 | return this._symbols.find(item => item.symbol == val || item.type == val); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/AutoTagger.js: -------------------------------------------------------------------------------- 1 | import { StreamParser } from './StreamParser'; 2 | import { Symbols } from './Symbols'; 3 | 4 | /** A class representing a Things autotagger */ 5 | export class Autotagger { 6 | 7 | /** 8 | * Create a Things autotagger, including a small default dictionary 9 | * @param {Object} config - An array of objects to add to the dictionary 10 | */ 11 | constructor(config) { 12 | let symbols = new Symbols(); 13 | let parser = new StreamParser(symbols); 14 | let stream = parser.parse(config); 15 | 16 | this._dictionary = stream.map(item => { 17 | let rule = { pattern: this._parsePattern(item.title), ...item }; 18 | delete rule.title; 19 | return rule; 20 | }).filter(item => !!item.pattern); 21 | 22 | } 23 | 24 | /** 25 | * Parse a string for matching autotagging entries 26 | * @param {Object} obj - An object containing task attributes 27 | * @return {Object} An object containing updated task attributes 28 | */ 29 | parse(title) { 30 | const entries = [...this._dictionary] 31 | .filter(item => item.pattern.test(title)); 32 | 33 | let attributes = {}; 34 | entries.forEach(entry => { 35 | Object.keys(entry).forEach(key => { 36 | if (key == 'pattern') return; 37 | this._setProp(attributes, key, entry[key]) 38 | }); 39 | }); 40 | 41 | return attributes; 42 | } 43 | 44 | _parsePattern(title) { 45 | if (title.trim().toLowerCase() == 'all tasks') return /.+/; 46 | 47 | let pattern = /^(Starts with|Ends with|Contains|Matches) +"(.*)"$/i; 48 | let matches = pattern.exec(title); 49 | if (!matches || matches.length < 3) return; 50 | let regex = matches[2]; 51 | let escaped = this._escapeRegex(matches[2]); 52 | 53 | switch (matches[1].toLowerCase()) { 54 | case 'starts with': return new RegExp(`^(${escaped})\\b`, 'i'); 55 | case 'ends with': return new RegExp(`\\b(${escaped})$`, 'i'); 56 | case 'contains': return new RegExp(`\\b(${escaped})\\b`, 'i'); 57 | case 'matches': return new RegExp(regex, 'i'); 58 | } 59 | } 60 | 61 | _escapeRegex(value) { 62 | // Ommitting | since it'll be our delimiter 63 | let pattern = /[\\{}()[\]^$+*?.]/g; 64 | return value.replace(pattern, '\\$&'); 65 | } 66 | 67 | /** 68 | * Update an object property. If the value is an array, push it real good. 69 | * @param {Object} obj - A reference to the source object 70 | * @param {String} prop - The name of the property to set 71 | * @param {Array|String} val - The value of the property to set 72 | * @private 73 | */ 74 | _setProp(obj, prop, val) { 75 | if (Array.isArray(val)) { 76 | obj[prop] = obj[prop] || []; 77 | obj[prop].push(...val); 78 | } else { 79 | obj[prop] = val; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/StreamParser.js: -------------------------------------------------------------------------------- 1 | /** A class representing a stream parser */ 2 | export class StreamParser { 3 | 4 | /** 5 | * Create a new stream parser 6 | * @param symbols {Symbols} - A symbol dictionary object 7 | */ 8 | constructor(symbols) { 9 | this._symbols = symbols; 10 | } 11 | 12 | /** 13 | * Parse a stream containing items and associated attributes 14 | * decorated by Emoji symbols 15 | * @param doc {String} - The document to parse 16 | * @return {Object} An object suitable to pass to the things:/// service 17 | */ 18 | parse(stream) { 19 | stream = this._normalizeSymbols(stream); 20 | stream = this._trimWhitespace(stream); 21 | 22 | let all = []; // An array of task objects 23 | let current; // The current task object 24 | 25 | stream.split('\n').forEach(line => { 26 | let item = this._parseLine(line); 27 | if (!item) return; 28 | 29 | if (item.type == 'title') { 30 | current = {}; 31 | all.push(current); 32 | } 33 | 34 | switch (item.format) { 35 | case 'array': 36 | current[item.type] = current[item.type] || []; 37 | current[item.type].push(item.value.trim()); 38 | break; 39 | case 'csv': 40 | current[item.type] = [ 41 | ...(current[item.value] || '').split(','), 42 | ...item.value.split(',') 43 | ].map(item => item.trim()).filter(item => item.length > 0).join(','); 44 | break; 45 | default: 46 | current[item.type] = item.value.trim(); 47 | } 48 | 49 | }); 50 | 51 | return all; 52 | } 53 | 54 | /** 55 | * Normalize symbol-decorated attributes so they're each on their own line 56 | * @param {String} value - The document to parse 57 | * @return {String} A document with symbol-decorated 58 | * attributes on their own line 59 | * @private 60 | */ 61 | _normalizeSymbols(value = '') { 62 | let pattern = new RegExp(`(${this._symbols.all.join('|')})`, 'mg'); 63 | return value.replace(pattern, '\n$1'); 64 | } 65 | 66 | /** 67 | * Parse a line, mapping its symbol to a property 68 | * @param {String} line - The line to parse 69 | * @return {Object} An object with the parsed property type and value 70 | * @private 71 | */ 72 | _parseLine(line) { 73 | if (!line) return; 74 | 75 | // Lines with no symbol prefix are tasks. 76 | if (/^[a-z0-9]/i.test(line)) { 77 | return { type: 'title', value: line }; 78 | } 79 | 80 | let allSymbols = this._symbols.all; 81 | let propPattern = new RegExp(`^(${allSymbols.join('|')})\s*(.*)$`, 'g'); 82 | let propMatch = propPattern.exec(line); 83 | if (!propMatch || propMatch.length < 3) return; 84 | 85 | return { 86 | type: this._symbols.getType(propMatch[1]), 87 | format: this._symbols.getFormat(propMatch[1]), 88 | value: propMatch[2] 89 | }; 90 | } 91 | 92 | /** 93 | * Trim leading/trailing whitespace from each line of a document 94 | * @param {String} value - The value to trim 95 | * @return {String} A document with no leading or trailing whitespace 96 | * @private 97 | */ 98 | _trimWhitespace(value = '') { 99 | return value 100 | .replace(/^\s+(.+)/mg, '$1') 101 | .replace(/(.+)\s+$/mg, '$1'); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/add-tasks.js: -------------------------------------------------------------------------------- 1 | import * as config from './config'; 2 | import { TemplateTagParser } from 'drafts-template-parser'; 3 | import { Autotagger } from './lib/AutoTagger'; 4 | import { TasksParser } from './lib/TasksParser'; 5 | import { Project } from './lib/Project'; 6 | 7 | let configNote = getConfig(); 8 | let autotagger = new Autotagger(configNote) 9 | let parser = new TasksParser(autotagger); 10 | 11 | let document = getDocument(); 12 | let templateParser = new TemplateTagParser(document); 13 | 14 | templateParser.ask(); 15 | document = templateParser.parse(document).text; 16 | 17 | let data = parser.parse(document); 18 | 19 | let firstLine = document.split('\n')[0]; 20 | if (firstLine.startsWith('#')) { 21 | let title = firstLine.substring(1).trim(); 22 | let project = new Project(title, data); 23 | data = project.toThingsObject(); 24 | } 25 | 26 | let sent = sendToThings(data); 27 | 28 | if (draft.title == config.autotaggerRulesDraftTitle) { 29 | alert(`Oops! You probably don't want to add your Autotagger rules as Things tasks.`); 30 | context.cancel(); 31 | } else if (sent === false) { 32 | context.fail(); 33 | } else if (sent === undefined) { 34 | context.cancel('No tasks found'); 35 | } else { 36 | cleanup(); 37 | } 38 | 39 | //////////////////////////////////////////////////////////////////////////////// 40 | 41 | function getConfig() { 42 | let configNote = Draft.query(`# ${config.autotaggerRulesDraftTitle}`, 'all') 43 | .filter(d => d.content.startsWith(`# ${config.autotaggerRulesDraftTitle}`)) 44 | .filter(d => !d.isTrashed); 45 | 46 | if (configNote.length == 0) { 47 | configNote.push(addDefaultConfig()); 48 | } 49 | 50 | return configNote 51 | .map(draft => draft.content) 52 | .join('\n'); 53 | } 54 | 55 | function addDefaultConfig() { 56 | let configNote = Draft.create(); 57 | configNote.content = config.defaultAutotaggerRules; 58 | configNote.update(); 59 | alert(config.newAutotaggerRulesMessage); 60 | return configNote; 61 | } 62 | 63 | function getDocument() { 64 | if (typeof editor === 'undefined') return ''; 65 | if (draft.title == config.autotaggerRulesDraftTitle) return ''; 66 | return editor.getSelectedText() || editor.getText(); 67 | } 68 | 69 | function sendToThings(data) { 70 | if (typeof CallbackURL === 'undefined') return false; 71 | if (typeof context === 'undefined') return false; 72 | if (data.length == 0) { 73 | context.cancel('No tasks found'); 74 | return; 75 | } 76 | 77 | let callback = CallbackURL.create(); 78 | callback.baseURL = 'things:///json'; 79 | callback.addParameter('data', JSON.stringify(data)); 80 | return callback.open(); 81 | } 82 | 83 | function cleanup() { 84 | if (draft.isFlagged) return; 85 | if (draft.isArchived) return; 86 | if (draft.title == config.autotaggerRulesDraftTitle) return; 87 | if (editor.getSelectedText()) return; 88 | draft.isTrashed = true; 89 | draft.update(); 90 | Draft.create(); 91 | editor.activate(); 92 | } 93 | 94 | function getTemplateTags(doc) { 95 | let pattern = /\[\[([\w ]+)\]\]/g; 96 | let tags = []; 97 | let match; 98 | 99 | while (match = pattern.exec(doc)) { 100 | let name = match[1]; 101 | if (tags.indexOf(name) >= 0) continue; 102 | if (config.reservedTemplateTags.indexOf(name) >= 0) continue; 103 | tags.push(match[1]); 104 | } 105 | 106 | return tags; 107 | } 108 | 109 | function askTemplateQuestions(tags) { 110 | let prompt = Prompt.create(); 111 | prompt.title = 'Template Questions'; 112 | tags.forEach(tag => prompt.addTextField(tag, tag, '')); 113 | prompt.addButton('Okay'); 114 | return prompt.show() && prompt.fieldValues; 115 | } 116 | 117 | function setTemplateTags(doc, tags) { 118 | Object.keys(tags).forEach(tag => draft.setTemplateTag(tag, tags[tag])); 119 | return draft.processTemplate(doc); 120 | } 121 | -------------------------------------------------------------------------------- /dist/date-picker.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // define __esModule on exports 48 | /******/ __webpack_require__.r = function(exports) { 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ 68 | /******/ // Load entry module and return exports 69 | /******/ return __webpack_require__(__webpack_require__.s = 11); 70 | /******/ }) 71 | /************************************************************************/ 72 | /******/ ({ 73 | 74 | /***/ 11: 75 | /***/ (function(module, exports, __webpack_require__) { 76 | 77 | "use strict"; 78 | 79 | 80 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 81 | 82 | var prompt = Prompt.create(); 83 | prompt.title = 'Select a date...'; 84 | prompt.addButton('Today'); 85 | prompt.addButton('Tonight'); 86 | prompt.addButton('Tomorrow'); 87 | var today = Date.today(); 88 | var tomorrow = Date.today().addDays(1); 89 | var isTodayFriday = today.is().friday(); 90 | var isTomorrowFriday = tomorrow.is().friday(); 91 | var isWeekend = today.is().saturday() || today.is().sunday(); 92 | 93 | if (!isTodayFriday) { 94 | if (isWeekend) prompt.addButton('Next Friday');else if (!isTomorrowFriday) prompt.addButton('Friday'); 95 | } 96 | 97 | prompt.addButton(isWeekend ? 'Next Weekend' : 'This Weekend'); 98 | prompt.addButton(isWeekend ? 'Monday' : 'Next Week'); 99 | prompt.addButton('Other...'); 100 | 101 | if (prompt.show()) { 102 | var date = prompt.buttonPressed; 103 | if (date == 'Other...') date = ''; 104 | 105 | var buttonMap = _defineProperty({ 106 | 'Next Week': 'Monday', 107 | 'This Weekend': 'Saturday', 108 | 'Next Weekend': 'Next Saturday' 109 | }, "Next Week", 'Monday'); 110 | 111 | draft.setTemplateTag('pickeddate', buttonMap[date] || date); 112 | editor.activate(); 113 | ; 114 | } else { 115 | context.cancel(); 116 | } 117 | 118 | /***/ }) 119 | 120 | /******/ }); -------------------------------------------------------------------------------- /src/lib/Task.js: -------------------------------------------------------------------------------- 1 | import { ThingsDate } from './ThingsDate'; 2 | import { ThingsDateTime } from './ThingsDateTime'; 3 | 4 | /** Class representing a single Things to-do item. */ 5 | export class Task { 6 | 7 | /** 8 | * Create a Things to-do item. 9 | * @param {Autotagger} autotagger - A reference to an autotagger 10 | */ 11 | constructor(autotagger) { 12 | this._autotagger = autotagger; 13 | this._when = new ThingsDateTime(); 14 | this._deadline = new ThingsDate(); 15 | this.attributes = { tags: [], 'checklist-items': [] }; 16 | } 17 | 18 | /** 19 | * The deadline to apply to the to-do. Relative dates are 20 | * parsed with the DateJS library. 21 | * @type {String} 22 | */ 23 | set deadline(date) { 24 | this._deadline.date = date; 25 | this.attributes.deadline = this._deadline.toString(); 26 | } 27 | 28 | set heading(heading) { 29 | this.attributes.heading = heading.trim(); 30 | } 31 | 32 | /** 33 | * The title or ID of the project or area to add to. 34 | * @type {String} 35 | */ 36 | set list(nameOrId) { 37 | let prop = this._isItemId(nameOrId) ? 'list-id' : 'list'; 38 | this.attributes[prop] = nameOrId.trim(); 39 | } 40 | 41 | /** 42 | * The text to use for the notes field of the to-do. 43 | * @type {String} 44 | */ 45 | set notes(notes) { 46 | this.attributes.notes = notes.trim(); 47 | } 48 | 49 | /** 50 | * The time to set for the task reminder. Overrides any time 51 | * specified in "when". Fuzzy times are parsed with the 52 | * DateJS library. 53 | * @type {String} 54 | */ 55 | set reminder(time) { 56 | this._when.timeOverride = time.trim(); 57 | this.attributes.when = this._when.toString(); 58 | } 59 | 60 | /** 61 | * The title of the to-do. Value will be parsed by the 62 | * autotagger, matching the string against a dictionary 63 | * of regular expressions and auto-applying attributes 64 | * for all matches. 65 | * @type {String} 66 | */ 67 | set title(value) { 68 | this.attributes.title = value.trim(); 69 | 70 | let autotagged = this._autotagger.parse(value); 71 | if (!autotagged) return; 72 | 73 | const properties = 'list when reminder deadline heading checklistItem'; 74 | properties.split(' ').forEach(property => { 75 | if (!autotagged[property]) return; 76 | this[property] = autotagged[property]; 77 | }); 78 | 79 | this.addTags(autotagged.tags || ''); 80 | this.addChecklistItem(autotagged.checklistItem || []); 81 | this.appendNotes(autotagged.notes || ''); 82 | } 83 | 84 | /** 85 | * The start date of the to-do. Values can be "today", 86 | * "tomorrow", "evening", "tonight", "anytime", "someday", 87 | * or a fuzzy date string with an optional time value, 88 | * which will add a reminder. 89 | * @type {String} 90 | */ 91 | set when(value) { 92 | this._when.datetime = value.trim(); 93 | this.attributes.when = this._when.toString(); 94 | } 95 | 96 | /** 97 | * Add a checklist item to the to-do. 98 | * @param {String} item - The checklist item to add 99 | */ 100 | addChecklistItem(item) { 101 | if (item.trim) item = item.trim(); 102 | if (!Array.isArray(item)) item = [ item ]; 103 | this.addChecklistItems(item); 104 | } 105 | 106 | /** 107 | * Add an array of checklist items to the to-do 108 | * @param {String[]} items - An array of checklist items to add 109 | */ 110 | addChecklistItems(items = []) { 111 | items = items 112 | .filter(item => item.length > 0) 113 | .map(item => ({ 114 | type: 'checklist-item', 115 | attributes: { title: item.trim() } 116 | })); 117 | 118 | this.attributes['checklist-items'].push(...items); 119 | } 120 | 121 | /** 122 | * Add one or more tags to the to-do, separated by commas. 123 | * Tags that do not already exist will be ignored. 124 | * @param {String|String[]} tags - An array or comma-separated list of one or more tags 125 | */ 126 | addTags(tags) { 127 | if (typeof tags == 'string') tags = tags.split(','); 128 | this.attributes.tags.push(...tags.map(tag => tag.trim())); 129 | } 130 | 131 | /** 132 | * Appends a new line to the notes. 133 | * @param {String|String[]} notes - An array or single string of notes 134 | */ 135 | appendNotes(notes) { 136 | if (typeof notes == 'string') notes = [notes]; 137 | if (this.attributes.notes) { 138 | this.attributes.notes += '\n\n' + notes.join('\n\n'); 139 | } else { 140 | this.attributes.notes = notes.join('\n\n'); 141 | } 142 | } 143 | 144 | /** 145 | * Export the current to-do, with all defined attributes, 146 | * as an object to be passed to the things:/// URL scheme. 147 | * @see {@link https://support.culturedcode.com/customer/en/portal/articles/2803573#json|Things API documentation} 148 | * @return {Object} An object suitable to pass to the things:/// service 149 | */ 150 | toThingsObject() { 151 | return { 152 | type: 'to-do', 153 | attributes: this.attributes 154 | }; 155 | } 156 | 157 | /** 158 | * Test whether a string is a things item ID 159 | * @param {String} value - The item name or id to test 160 | * @private 161 | */ 162 | _isItemId(value) { 163 | const pattern = /^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/img; 164 | return pattern.test(value); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/lib/ThingsDate.js: -------------------------------------------------------------------------------- 1 | import * as config from '../config'; 2 | 3 | /** A class representing a date in Things */ 4 | export class ThingsDate { 5 | 6 | /** Create a new ThingsDate object */ 7 | constructor() {} 8 | 9 | /** 10 | * The date value. In addition to fuzzy values parseable by DateJS, 11 | * valid values include "someday", "anytime", "evening", and "tonight". 12 | * @type {String} 13 | */ 14 | set date(value = '') { 15 | this._datetime = value.trim().toLowerCase(); 16 | } 17 | 18 | /** 19 | * Convert to a Things-formatted string: YYYY-MM-DD@HH:MM, or 20 | * relative keyword: evening, someday, anytime. 21 | * @return {String} A Things-formatted date string 22 | */ 23 | toString() { 24 | // This is still way too big. Some of this stuff 25 | // should be parsed as values are set, and 26 | // private functions should reference object 27 | // properties, not passed parameters. 28 | 29 | // If a time override is present, but no date is specified, 30 | // assume this is a task for today. 31 | let timeOverride = this._timeOverride; 32 | let datetime = this._datetime || (timeOverride && 'today'); 33 | 34 | // Shorthand values like "someday" and "anytime" cannot have 35 | // an associated time, and are unparseable by DateJS. 36 | let isDateOnly = this._isDateOnlyShorthand(datetime); 37 | if (isDateOnly) return datetime; 38 | 39 | // Shorthand values like "tonight" need to be normalized to 40 | // "evening" for Things to understand. However, if there's 41 | // a time override specified, we'll change the value 42 | // to "today" so DateJS can do its thing. Assuming the 43 | // reminder is in the evening, it'll get changed back later. 44 | let isEvening = this._isEveningShorthand(datetime); 45 | if (isEvening && !timeOverride) return 'evening'; 46 | if (isEvening && timeOverride) datetime = 'today'; 47 | 48 | // DateJS will take relative dates like "1 week" without 49 | // complaint, but it interprets them as "1 week from the 50 | // first day of the year". Prepend a "+" to anchor to 51 | // today's date. 52 | datetime = datetime.replace( 53 | /^(\d+)\s*(h|d|w|m|y|minute|hour|day|week|month|year)(s?)/i, 54 | '+$1 $2$3' 55 | ); 56 | 57 | // DateJS won't understand dates like "in 2 weeks". 58 | // Reformat as "+2 weeks". 59 | datetime = datetime.replace(/^in\s+(\d)/i, '+$1'); 60 | 61 | // Offset shorthand 62 | let offset = this._parseOffset(datetime); 63 | let dateOffset = 0; 64 | if (offset) { 65 | datetime = offset.datetime; 66 | dateOffset = offset.offset; 67 | } 68 | 69 | // Parse the date with DateJS. If it's invalid, just pass 70 | // the raw value and let Things take a crack at it. 71 | let dt = Date.parse(datetime); 72 | if (!dt) return datetime; 73 | 74 | // Override time if we explicitly set a reminder 75 | if (timeOverride) { 76 | let time = Date.parse(timeOverride); 77 | if (time) { 78 | let hour = time.getHours(); 79 | let minute = time.getMinutes(); 80 | dt.set({ hour, minute }); 81 | } 82 | } 83 | 84 | // Sometimes relative dates, like "Monday", are 85 | // interpreted as "last Monday". If the date is in the 86 | // past, add a week. 87 | let isDatePast = this._isDatePast(dt); 88 | if (isDatePast) dt.add(1).week(); 89 | 90 | // If the time is expressed without an AM/PM suffix, 91 | // and it's super early, we probably meant PM. 92 | let isTooEarly = this._isTimeEarlyAndAmbiguous(datetime, dt); 93 | if (isTooEarly) dt.add(12).hours(); 94 | 95 | // Process date offset 96 | if (dateOffset != 0) dt.add(dateOffset).days(); 97 | 98 | // Return a date- or datetime-formatted string that 99 | // Things will understand. 100 | return this._formatThingsDate(dt, isEvening); 101 | } 102 | 103 | /** 104 | * Test whether a string is shorthand for "tonight" 105 | * @param {String} value - The string to test 106 | * @return {boolean} True if string equals evening shorthand keywords 107 | * @private 108 | */ 109 | _isEveningShorthand(value) { 110 | let pattern = /^((this )?evening|tonight)$/i; 111 | return pattern.test(value); 112 | } 113 | 114 | /** 115 | * Test whether a string is a dateless shorthand value 116 | * @param {String} value - The string to test 117 | * @return {boolean} True if string starts with "someday" or "anytime" 118 | * @private 119 | */ 120 | _isDateOnlyShorthand(value) { 121 | let pattern = /^(someday|anytime)/i; 122 | return pattern.test(value); 123 | } 124 | 125 | /** 126 | * Test whether a date is in the past 127 | * @param {Date} parsed - The datetime, parsed by DateJS 128 | * @return {boolean} True if the date is in the past 129 | * @private 130 | */ 131 | _isDatePast(parsed) { 132 | let date = parsed.clone().clearTime(); 133 | let today = Date.today().clearTime(); 134 | return date.compareTo(today) == -1; 135 | } 136 | 137 | /** 138 | * Test whether a time is ambiguously specified (lacking an am/pm 139 | * suffix) and possibly early in the morning. 140 | * @param {String} str - The raw, unparsed date 141 | * @param {DateJS} parsed - The datetime, parsed by DateJS 142 | * @return {boolean} True if AM/PM suffix is missing and hour is before 7 143 | * @private 144 | */ 145 | _isTimeEarlyAndAmbiguous(str, parsed) { 146 | let hasAmPmSuffix = /\d *[ap]m?\b/i.test(str); 147 | let earliest = config.earliestAmbiguousMorningHour; 148 | let isEarly = parsed.getHours() > 0 && parsed.getHours() < earliest; 149 | return !hasAmPmSuffix && isEarly; 150 | } 151 | 152 | _parseOffset(str) { 153 | let pattern = /^(.+)\s([+-]\d+)$/; 154 | let match = pattern.exec(str); 155 | if (!match) return; 156 | return { datetime: match[1], offset: parseInt(match[2]) }; 157 | } 158 | 159 | /** 160 | * Test whether a datetime is set to midnight 161 | * @param {DateJS} parsed - The datetime, parsed by DateJS 162 | * @return {boolean} True if time is midnight 163 | * @private 164 | */ 165 | _isTimeMidnight(parsed) { 166 | let hours = parsed.getHours(); 167 | let minutes = parsed.getMinutes(); 168 | return hours + minutes == 0; 169 | } 170 | 171 | /** 172 | * Format a DateJS datetime as a valid Things datetime string. 173 | * @param {DateJS} datetime - The datetime, parsed by DateJS 174 | * @param {boolean} forceEvening - Force to evening, overriding time value 175 | * @return {string} A Things-formatted date 176 | * @private 177 | */ 178 | _formatThingsDate(datetime, forceEvening) { 179 | let date = datetime.toString('yyyy-MM-dd'); 180 | let time = datetime.toString('@HH:mm'); 181 | 182 | if (this._isTimeMidnight(datetime)) time = ''; 183 | if (!this._allowTime) time = ''; 184 | 185 | let isToday = datetime.between(Date.today(), Date.today().addDays(1)); 186 | let isEvening = forceEvening || datetime.getHours() > config.eveningStartsAtHour; 187 | if (isToday && isEvening) date = 'evening'; 188 | 189 | return date + time; 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thingy: A Things Parser for Drafts 5 2 | 3 | ## Installation 4 | 5 | An action group is available for download in the 6 | [Drafts Action Directory](https://actions.getdrafts.com/g/1HW). If you wish to 7 | customize, run `npm run build` to generate the bundled script to import into Drafts. 8 | 9 | ## Overview 10 | 11 | Like most parsers, multiple tasks can be specified, one task per line: 12 | 13 | ``` 14 | Vacuum the rug 15 | Paint the lawn 16 | Mow the hedge 17 | Shave the chickens 18 | ``` 19 | 20 | If you select text in a draft, only that text will be processed. Properties can 21 | be defined by prefacing the value with an emoji. Keyboard actions are provided 22 | to facilitate easy entry. 23 | 24 | | | Property | Note | 25 | |----|----------------|-------------------------------------------------------------| 26 | | 🏷 | Tags | One or more, separated by commas | 27 | | 📁 | List | Must be exact project or area name or ID | 28 | | 📆 | When | Can be fuzzy date; including a time will set a reminder | 29 | | ⏰ | Reminder | Time only; can also be appended to "when" value | 30 | | ⚠️ | Deadline | Date only, time will be ignored | 31 | | 📌 | Heading | Heading name (exact; ignored if doesn't exist) | 32 | | 🔘 | Checklist item | Can include multiple item definitions | 33 | | 🗒 | Notes | Can include multiple notes, which will render as paragraphs | 34 | 35 | So now our tasks can have tags, be assigned to lists, and given notes, dates, 36 | and checklist items: 37 | 38 | ``` 39 | Vacuum the rug 🏷 Home 📁 Chores ⏰ 3:00 40 | Paint the lawn 🏷 Home 📁 Landscaping 41 | Mow the hedge 🏷 Home 📁 Landscaping 42 | Shave the chickens 🏷 Farm 📁 Livestock 📆 Tonight 7pm 43 | ``` 44 | 45 | Adding whitespace makes things a bit more readable. The keyboard actions 46 | included in the bundle automatically format properties on a new line, with 47 | indentation: 48 | 49 | ``` 50 | Vacuum the rug 51 | 📁 Chores 52 | 🏷 Home 53 | ⏰ 3:30 54 | 55 | Paint the lawn 56 | 📁 Landscaping 57 | 🏷 Home 58 | 📆 Next Saturday 59 | 🔘 Wash brushes and rollers 60 | 🔘 Lay down tarp to protect sidewalk 61 | 🔘 Make sure paint can lids are closed tightly! 62 | 63 | Mow the hedge 64 | 📁 Landscaping 65 | 🏷 Home 66 | ⚠️ 2 weeks 67 | 68 | Shave the chickens 69 | 📁 Livestock 70 | 🏷 Farm 71 | 📆 Tonight 7pm 72 | 🗒 Remember to use a fresh razor blade! 73 | ``` 74 | 75 | ## Dates 76 | 77 | While Things does an admirable job parsing natural language dates, I opted to 78 | employ DateJS to allow for additional flexibility. Some examples of valid dates: 79 | 80 | - Tonight 81 | - Next Saturday 82 | - in 3 weeks 83 | - +4d 84 | - July 85 | - Someday 86 | 87 | Any date can be offset by appending `+4` or `-7` to the string. This will 88 | add 4 days, or subtract 7 days, from the parsed date. (It may not seem very 89 | useful, but it comes in handy when [processing template tags](#template-tags). 90 | Keep reading.) 91 | 92 | A few things to note: 93 | 94 | - If only a time is specified, today's date will be assumed 95 | - If a time associated with today's date is after 5pm, the task will be 96 | automatically categorized in the "this evening" section. 97 | - If a specified time is before 7:00 and lacks an am/pm suffix, it will 98 | be assumed to be in the evening. We don't want accidental reminders at 5am. 99 | 100 | ## Autotagger 101 | 102 | Tags and other properties can be automatically applied to tasks based on 103 | pattern-based rules. When you first run the "Add Things Tasks" action, a new 104 | "Thingy Autotagger Rules" draft note will be placed in your inbox. This note 105 | will contain a handful of default autotagger rule definitions: 106 | 107 | ```markdown 108 | # Thingy Autotagger Rules 109 | 110 | Starts with "Call" 🏷 Calls 111 | Starts with "Email" 🏷 Email 112 | Contains "Mom" 🏷 Mom 113 | Contains "Dad" 🏷 Dad 114 | 115 | Starts with "Waiting For|WF" 116 | 🏷 Waiting For 117 | 📆 Tomorrow 118 | ⚠️ 1 week 119 | 120 | Starts with "Drop off|Pick up|Deliver" 121 | 🏷 Errands 122 | ``` 123 | 124 | You can edit or add to these rules as you see fit. Feel free to archive the 125 | draft if you don't want it cluttering up your inbox... just don't change the 126 | title. (Side note: You can have multiple config files if you want. Just make 127 | sure the title starts with "# Thingy Autotagger Rules".) 128 | 129 | Autotagger rules are defined just like tasks, but follow a specific notation: 130 | 131 | | Syntax | Description | 132 | | ------------------------- | --------------------------------------- | 133 | | `Starts with "Call"` | Task must start with "Call" | 134 | | `Ends with "ASAP"` | Task must end with "ASAP" | 135 | | `Contains "groceries"` | Task must contain the word "groceries" | 136 | | `Matches "^Bug( #\d+)?:"` | Task must match the regular expression | 137 | | `All tasks` | Match all tasks | 138 | 139 | All rules are case-insensitive, and all but regular expression rules must be 140 | anchored against word boundaries. For example, `Starts with "Call"` will 141 | match "Call Bob", but not "Callously berate Bob for constantly being late". 142 | 143 | Multiple options can be referenced, separated by the `|` character. For example, 144 | `Contains "groceries|grocery store|Whole Foods"` will match tasks that contain 145 | "groceries", "grocery store", or "Whole Foods". 146 | 147 | ## Projects 148 | 149 | When you give your document a title that begins with a hash mark, your tasks 150 | will be created as a part of a new project. Any headings referenced in 151 | task properties will be created. For example: 152 | 153 | ```markdown 154 | # Trip to Maui 155 | 156 | Pack luggage 157 | 🏷 Home 158 | 📌 Packing 159 | 🔘 Swimsuit 160 | 🔘 Flip-flops 161 | 🔘 Sunscreen 162 | 163 | Pack carry-on bag 164 | 🏷 Home 165 | 📌 Packing 166 | 🔘 Kindle 167 | 🔘 iPad 168 | 🔘 Chargers 169 | 170 | Take out the trash 171 | 🏷 Home 172 | 📌 Before Leaving 173 | 174 | Drop dog off at sitter 175 | 🏷 Home 176 | 📌 Before Leaving 177 | 178 | Make sure you have your tickets and passport! 179 | 🏷 Home 180 | 📌 Before Leaving 181 | ``` 182 | 183 | ## Template Tags 184 | 185 | Thingy will scan your document for template tags, and prompt you for 186 | values before processing. This comes in handy when combined with 187 | [project templates](#projects) and [date offsets](#dates): 188 | 189 | ```markdown 190 | # Pack for trip to [[City]] 191 | 192 | Pack luggage for trip to [[City]] 193 | 🏷 Home 194 | 📆 [[Departure Date]] -3 195 | ⚠️ [[Departure Date]] -1 196 | 197 | Take out the trash before leaving for [[City]] 198 | 🏷 Home 199 | 📆 [[Departure Date]] 200 | ⚠️ [[Departure Date]] 201 | ``` 202 | 203 | Note that template tag names are case sensitive, and can only contain letters, 204 | numbers, spaces, and underscores. Thingy will not prompt you for values for 205 | any [built-in Drafts template tags](https://agiletortoise.zendesk.com/hc/en-us/articles/202843484-Templates-and-Tags) 206 | your document contains. 207 | -------------------------------------------------------------------------------- /dist/add-tasks.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // define __esModule on exports 48 | /******/ __webpack_require__.r = function(exports) { 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ 68 | /******/ // Load entry module and return exports 69 | /******/ return __webpack_require__(__webpack_require__.s = 0); 70 | /******/ }) 71 | /************************************************************************/ 72 | /******/ ([ 73 | /* 0 */ 74 | /***/ (function(module, exports, __webpack_require__) { 75 | 76 | "use strict"; 77 | 78 | 79 | var config = _interopRequireWildcard(__webpack_require__(1)); 80 | 81 | var _draftsTemplateParser = __webpack_require__(2); 82 | 83 | var _AutoTagger = __webpack_require__(3); 84 | 85 | var _TasksParser = __webpack_require__(6); 86 | 87 | var _Project = __webpack_require__(10); 88 | 89 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } 90 | 91 | var configNote = getConfig(); 92 | var autotagger = new _AutoTagger.Autotagger(configNote); 93 | var parser = new _TasksParser.TasksParser(autotagger); 94 | var document = getDocument(); 95 | var templateParser = new _draftsTemplateParser.TemplateTagParser(document); 96 | templateParser.ask(); 97 | document = templateParser.parse(document).text; 98 | var data = parser.parse(document); 99 | var firstLine = document.split('\n')[0]; 100 | 101 | if (firstLine.startsWith('#')) { 102 | var title = firstLine.substring(1).trim(); 103 | var project = new _Project.Project(title, data); 104 | data = project.toThingsObject(); 105 | } 106 | 107 | var sent = sendToThings(data); 108 | 109 | if (draft.title == config.autotaggerRulesDraftTitle) { 110 | alert("Oops! You probably don't want to add your Autotagger rules as Things tasks."); 111 | context.cancel(); 112 | } else if (sent === false) { 113 | context.fail(); 114 | } else if (sent === undefined) { 115 | context.cancel('No tasks found'); 116 | } else { 117 | cleanup(); 118 | } //////////////////////////////////////////////////////////////////////////////// 119 | 120 | 121 | function getConfig() { 122 | var configNote = Draft.query("# ".concat(config.autotaggerRulesDraftTitle), 'all').filter(function (d) { 123 | return d.content.startsWith("# ".concat(config.autotaggerRulesDraftTitle)); 124 | }).filter(function (d) { 125 | return !d.isTrashed; 126 | }); 127 | 128 | if (configNote.length == 0) { 129 | configNote.push(addDefaultConfig()); 130 | } 131 | 132 | return configNote.map(function (draft) { 133 | return draft.content; 134 | }).join('\n'); 135 | } 136 | 137 | function addDefaultConfig() { 138 | var configNote = Draft.create(); 139 | configNote.content = config.defaultAutotaggerRules; 140 | configNote.update(); 141 | alert(config.newAutotaggerRulesMessage); 142 | return configNote; 143 | } 144 | 145 | function getDocument() { 146 | if (typeof editor === 'undefined') return ''; 147 | if (draft.title == config.autotaggerRulesDraftTitle) return ''; 148 | return editor.getSelectedText() || editor.getText(); 149 | } 150 | 151 | function sendToThings(data) { 152 | if (typeof CallbackURL === 'undefined') return false; 153 | if (typeof context === 'undefined') return false; 154 | 155 | if (data.length == 0) { 156 | context.cancel('No tasks found'); 157 | return; 158 | } 159 | 160 | var callback = CallbackURL.create(); 161 | callback.baseURL = 'things:///json'; 162 | callback.addParameter('data', JSON.stringify(data)); 163 | return callback.open(); 164 | } 165 | 166 | function cleanup() { 167 | if (draft.isFlagged) return; 168 | if (draft.isArchived) return; 169 | if (draft.title == config.autotaggerRulesDraftTitle) return; 170 | if (editor.getSelectedText()) return; 171 | draft.isTrashed = true; 172 | draft.update(); 173 | Draft.create(); 174 | editor.activate(); 175 | } 176 | 177 | function getTemplateTags(doc) { 178 | var pattern = /\[\[([\w ]+)\]\]/g; 179 | var tags = []; 180 | var match; 181 | 182 | while (match = pattern.exec(doc)) { 183 | var name = match[1]; 184 | if (tags.indexOf(name) >= 0) continue; 185 | if (config.reservedTemplateTags.indexOf(name) >= 0) continue; 186 | tags.push(match[1]); 187 | } 188 | 189 | return tags; 190 | } 191 | 192 | function askTemplateQuestions(tags) { 193 | var prompt = Prompt.create(); 194 | prompt.title = 'Template Questions'; 195 | tags.forEach(function (tag) { 196 | return prompt.addTextField(tag, tag, ''); 197 | }); 198 | prompt.addButton('Okay'); 199 | return prompt.show() && prompt.fieldValues; 200 | } 201 | 202 | function setTemplateTags(doc, tags) { 203 | Object.keys(tags).forEach(function (tag) { 204 | return draft.setTemplateTag(tag, tags[tag]); 205 | }); 206 | return draft.processTemplate(doc); 207 | } 208 | 209 | /***/ }), 210 | /* 1 */ 211 | /***/ (function(module, exports, __webpack_require__) { 212 | 213 | "use strict"; 214 | 215 | 216 | Object.defineProperty(exports, "__esModule", { 217 | value: true 218 | }); 219 | exports.reservedTemplateTags = exports.defaultAutotaggerRules = exports.newAutotaggerRulesMessage = exports.autotaggerRulesDraftTitle = exports.eveningStartsAtHour = exports.earliestAmbiguousMorningHour = void 0; 220 | // When times omit an AM/PM suffix and are before this hour, 221 | // we'll assume PM to avoid early morning alarms. 222 | var earliestAmbiguousMorningHour = 6; // When a task date is today, and a reminder is set to a time 223 | // at this hour or later, we'll file it in the "evening" section. 224 | 225 | exports.earliestAmbiguousMorningHour = earliestAmbiguousMorningHour; 226 | var eveningStartsAtHour = 18; 227 | exports.eveningStartsAtHour = eveningStartsAtHour; 228 | var autotaggerRulesDraftTitle = 'Thingy Autotagger Rules'; 229 | exports.autotaggerRulesDraftTitle = autotaggerRulesDraftTitle; 230 | var newAutotaggerRulesMessage = "Welcome to Thingy! A draft with a few default Autotagger rules has been added to your inbox. Feel free to customize these as you see fit, and you can archive the draft if you don't want it cluttering up your inbox."; 231 | exports.newAutotaggerRulesMessage = newAutotaggerRulesMessage; 232 | var defaultAutotaggerRules = "# ".concat(autotaggerRulesDraftTitle, "\n\nStarts with \"Call\" \uD83C\uDFF7 Calls\nStarts with \"Email\" \uD83C\uDFF7 Email\nContains \"Mom\" \uD83C\uDFF7 Mom\nContains \"Dad\" \uD83C\uDFF7 Dad\n\nStarts with \"Waiting For|WF\"\n \uD83C\uDFF7 Waiting For\n \uD83D\uDCC6 Tomorrow\n \u26A0\uFE0F 1 week\n\nStarts with \"Drop off|Pick up|Deliver\"\n \uD83C\uDFF7 Errands\n"); 233 | exports.defaultAutotaggerRules = defaultAutotaggerRules; 234 | var reservedTemplateTags = ['body', 'clipboard', 'created_latitude', 'created_longitude', 'created', 'date', 'draft_open_url', 'draft', 'latitude', 'longitude', 'modified_latitude', 'modified_longitude', 'modified', 'selection_length', 'selection_start', 'selection', 'time', 'title', 'uuid']; 235 | exports.reservedTemplateTags = reservedTemplateTags; 236 | 237 | /***/ }), 238 | /* 2 */ 239 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 240 | 241 | "use strict"; 242 | __webpack_require__.r(__webpack_exports__); 243 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TemplateTagParser", function() { return TemplateTagParser; }); 244 | class TemplateTagParser { 245 | 246 | constructor(template = draft.content) { 247 | this.template = template; 248 | } 249 | 250 | get tags() { 251 | const reservedTags = [ 252 | 'body', 253 | 'clipboard', 254 | 'created_latitude', 255 | 'created_longitude', 256 | 'created', 257 | 'date', 258 | 'draft_open_url', 259 | 'draft', 260 | 'latitude', 261 | 'longitude', 262 | 'modified_latitude', 263 | 'modified_longitude', 264 | 'modified', 265 | 'selection_length', 266 | 'selection_start', 267 | 'selection', 268 | 'time', 269 | 'title', 270 | 'uuid', 271 | ]; 272 | 273 | const pattern = /\[\[([\w ]+)\]\]/g; 274 | let tags = new Set(); 275 | let match; 276 | 277 | while (match = pattern.exec(this.template)) { 278 | tags.add(match[1]); 279 | } 280 | 281 | return Array.from(tags) 282 | .filter(tag => !reservedTags.includes(tag)); 283 | } 284 | 285 | ask() { 286 | let tags = this.tags; 287 | if (tags.length == 0) return true; 288 | 289 | let prompt = Prompt.create(); 290 | prompt.title = 'Template Questions'; 291 | tags.forEach(tag => prompt.addTextField(tag, tag, '')); 292 | prompt.addButton('Okay'); 293 | 294 | if (!prompt.show()) return false; 295 | tags.forEach(tag => { 296 | draft.setTemplateTag(tag, prompt.fieldValues[tag]); 297 | console.log(`Setting ${tag} to ${prompt.fieldValues[tag]}`); 298 | }); 299 | 300 | return true; 301 | } 302 | 303 | parse(str) { 304 | let text = draft.processTemplate(str); 305 | let html = MultiMarkdown.create().render(text); 306 | return { text, html }; 307 | } 308 | } 309 | 310 | 311 | /***/ }), 312 | /* 3 */ 313 | /***/ (function(module, exports, __webpack_require__) { 314 | 315 | "use strict"; 316 | 317 | 318 | Object.defineProperty(exports, "__esModule", { 319 | value: true 320 | }); 321 | exports.Autotagger = void 0; 322 | 323 | var _StreamParser = __webpack_require__(4); 324 | 325 | var _Symbols = __webpack_require__(5); 326 | 327 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 328 | 329 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 330 | 331 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 332 | 333 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 334 | 335 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } 336 | 337 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 338 | 339 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 340 | 341 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 342 | 343 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 344 | 345 | /** A class representing a Things autotagger */ 346 | var Autotagger = 347 | /*#__PURE__*/ 348 | function () { 349 | /** 350 | * Create a Things autotagger, including a small default dictionary 351 | * @param {Object} config - An array of objects to add to the dictionary 352 | */ 353 | function Autotagger(config) { 354 | var _this = this; 355 | 356 | _classCallCheck(this, Autotagger); 357 | 358 | var symbols = new _Symbols.Symbols(); 359 | var parser = new _StreamParser.StreamParser(symbols); 360 | var stream = parser.parse(config); 361 | this._dictionary = stream.map(function (item) { 362 | var rule = _objectSpread({ 363 | pattern: _this._parsePattern(item.title) 364 | }, item); 365 | 366 | delete rule.title; 367 | return rule; 368 | }).filter(function (item) { 369 | return !!item.pattern; 370 | }); 371 | } 372 | /** 373 | * Parse a string for matching autotagging entries 374 | * @param {Object} obj - An object containing task attributes 375 | * @return {Object} An object containing updated task attributes 376 | */ 377 | 378 | 379 | _createClass(Autotagger, [{ 380 | key: "parse", 381 | value: function parse(title) { 382 | var _this2 = this; 383 | 384 | var entries = _toConsumableArray(this._dictionary).filter(function (item) { 385 | return item.pattern.test(title); 386 | }); 387 | 388 | var attributes = {}; 389 | entries.forEach(function (entry) { 390 | Object.keys(entry).forEach(function (key) { 391 | if (key == 'pattern') return; 392 | 393 | _this2._setProp(attributes, key, entry[key]); 394 | }); 395 | }); 396 | return attributes; 397 | } 398 | }, { 399 | key: "_parsePattern", 400 | value: function _parsePattern(title) { 401 | if (title.trim().toLowerCase() == 'all tasks') return /.+/; 402 | var pattern = /^(Starts with|Ends with|Contains|Matches) +"(.*)"$/i; 403 | var matches = pattern.exec(title); 404 | if (!matches || matches.length < 3) return; 405 | var regex = matches[2]; 406 | 407 | var escaped = this._escapeRegex(matches[2]); 408 | 409 | switch (matches[1].toLowerCase()) { 410 | case 'starts with': 411 | return new RegExp("^(".concat(escaped, ")\\b"), 'i'); 412 | 413 | case 'ends with': 414 | return new RegExp("\\b(".concat(escaped, ")$"), 'i'); 415 | 416 | case 'contains': 417 | return new RegExp("\\b(".concat(escaped, ")\\b"), 'i'); 418 | 419 | case 'matches': 420 | return new RegExp(regex, 'i'); 421 | } 422 | } 423 | }, { 424 | key: "_escapeRegex", 425 | value: function _escapeRegex(value) { 426 | // Ommitting | since it'll be our delimiter 427 | var pattern = /[\\{}()[\]^$+*?.]/g; 428 | return value.replace(pattern, '\\$&'); 429 | } 430 | /** 431 | * Update an object property. If the value is an array, push it real good. 432 | * @param {Object} obj - A reference to the source object 433 | * @param {String} prop - The name of the property to set 434 | * @param {Array|String} val - The value of the property to set 435 | * @private 436 | */ 437 | 438 | }, { 439 | key: "_setProp", 440 | value: function _setProp(obj, prop, val) { 441 | if (Array.isArray(val)) { 442 | var _obj$prop; 443 | 444 | obj[prop] = obj[prop] || []; 445 | 446 | (_obj$prop = obj[prop]).push.apply(_obj$prop, _toConsumableArray(val)); 447 | } else { 448 | obj[prop] = val; 449 | } 450 | } 451 | }]); 452 | 453 | return Autotagger; 454 | }(); 455 | 456 | exports.Autotagger = Autotagger; 457 | 458 | /***/ }), 459 | /* 4 */ 460 | /***/ (function(module, exports, __webpack_require__) { 461 | 462 | "use strict"; 463 | 464 | 465 | Object.defineProperty(exports, "__esModule", { 466 | value: true 467 | }); 468 | exports.StreamParser = void 0; 469 | 470 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 471 | 472 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 473 | 474 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 475 | 476 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 477 | 478 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 479 | 480 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 481 | 482 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 483 | 484 | /** A class representing a stream parser */ 485 | var StreamParser = 486 | /*#__PURE__*/ 487 | function () { 488 | /** 489 | * Create a new stream parser 490 | * @param symbols {Symbols} - A symbol dictionary object 491 | */ 492 | function StreamParser(symbols) { 493 | _classCallCheck(this, StreamParser); 494 | 495 | this._symbols = symbols; 496 | } 497 | /** 498 | * Parse a stream containing items and associated attributes 499 | * decorated by Emoji symbols 500 | * @param doc {String} - The document to parse 501 | * @return {Object} An object suitable to pass to the things:/// service 502 | */ 503 | 504 | 505 | _createClass(StreamParser, [{ 506 | key: "parse", 507 | value: function parse(stream) { 508 | var _this = this; 509 | 510 | stream = this._normalizeSymbols(stream); 511 | stream = this._trimWhitespace(stream); 512 | var all = []; // An array of task objects 513 | 514 | var current; // The current task object 515 | 516 | stream.split('\n').forEach(function (line) { 517 | var item = _this._parseLine(line); 518 | 519 | if (!item) return; 520 | 521 | if (item.type == 'title') { 522 | current = {}; 523 | all.push(current); 524 | } 525 | 526 | switch (item.format) { 527 | case 'array': 528 | current[item.type] = current[item.type] || []; 529 | current[item.type].push(item.value.trim()); 530 | break; 531 | 532 | case 'csv': 533 | current[item.type] = _toConsumableArray((current[item.value] || '').split(',')).concat(_toConsumableArray(item.value.split(','))).map(function (item) { 534 | return item.trim(); 535 | }).filter(function (item) { 536 | return item.length > 0; 537 | }).join(','); 538 | break; 539 | 540 | default: 541 | current[item.type] = item.value.trim(); 542 | } 543 | }); 544 | return all; 545 | } 546 | /** 547 | * Normalize symbol-decorated attributes so they're each on their own line 548 | * @param {String} value - The document to parse 549 | * @return {String} A document with symbol-decorated 550 | * attributes on their own line 551 | * @private 552 | */ 553 | 554 | }, { 555 | key: "_normalizeSymbols", 556 | value: function _normalizeSymbols() { 557 | var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 558 | var pattern = new RegExp("(".concat(this._symbols.all.join('|'), ")"), 'mg'); 559 | return value.replace(pattern, '\n$1'); 560 | } 561 | /** 562 | * Parse a line, mapping its symbol to a property 563 | * @param {String} line - The line to parse 564 | * @return {Object} An object with the parsed property type and value 565 | * @private 566 | */ 567 | 568 | }, { 569 | key: "_parseLine", 570 | value: function _parseLine(line) { 571 | if (!line) return; // Lines with no symbol prefix are tasks. 572 | 573 | if (/^[a-z0-9]/i.test(line)) { 574 | return { 575 | type: 'title', 576 | value: line 577 | }; 578 | } 579 | 580 | var allSymbols = this._symbols.all; 581 | var propPattern = new RegExp("^(".concat(allSymbols.join('|'), ")s*(.*)$"), 'g'); 582 | var propMatch = propPattern.exec(line); 583 | if (!propMatch || propMatch.length < 3) return; 584 | return { 585 | type: this._symbols.getType(propMatch[1]), 586 | format: this._symbols.getFormat(propMatch[1]), 587 | value: propMatch[2] 588 | }; 589 | } 590 | /** 591 | * Trim leading/trailing whitespace from each line of a document 592 | * @param {String} value - The value to trim 593 | * @return {String} A document with no leading or trailing whitespace 594 | * @private 595 | */ 596 | 597 | }, { 598 | key: "_trimWhitespace", 599 | value: function _trimWhitespace() { 600 | var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 601 | return value.replace(/^\s+(.+)/mg, '$1').replace(/(.+)\s+$/mg, '$1'); 602 | } 603 | }]); 604 | 605 | return StreamParser; 606 | }(); 607 | 608 | exports.StreamParser = StreamParser; 609 | 610 | /***/ }), 611 | /* 5 */ 612 | /***/ (function(module, exports, __webpack_require__) { 613 | 614 | "use strict"; 615 | 616 | 617 | Object.defineProperty(exports, "__esModule", { 618 | value: true 619 | }); 620 | exports.Symbols = void 0; 621 | 622 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 623 | 624 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 625 | 626 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 627 | 628 | /** A class representing a symbols dictionary */ 629 | var Symbols = 630 | /*#__PURE__*/ 631 | function () { 632 | /** 633 | * Create a symbols dictionary 634 | * @param {Object} config - An optional object overriding one or 635 | * more symbol definitions 636 | */ 637 | function Symbols() { 638 | _classCallCheck(this, Symbols); 639 | 640 | this._symbols = [{ 641 | symbol: '🏷', 642 | type: 'tags', 643 | format: 'csv' 644 | }, { 645 | symbol: '📁', 646 | type: 'list', 647 | format: 'string' 648 | }, { 649 | symbol: '📆', 650 | type: 'when', 651 | format: 'string' 652 | }, { 653 | symbol: '⏰', 654 | type: 'reminder', 655 | format: 'string' 656 | }, { 657 | symbol: '⚠️', 658 | type: 'deadline', 659 | format: 'string' 660 | }, { 661 | symbol: '📌', 662 | type: 'heading', 663 | format: 'string' 664 | }, { 665 | symbol: '🗒', 666 | type: 'notes', 667 | format: 'array' 668 | }, { 669 | symbol: '🔘', 670 | type: 'checklistItem', 671 | format: 'array' 672 | }]; 673 | } 674 | /** 675 | * An array of all defined symbols. 676 | * @type {String[]} 677 | */ 678 | 679 | 680 | _createClass(Symbols, [{ 681 | key: "getSymbol", 682 | 683 | /** 684 | * Look up a symbol based on an attribute name 685 | * @param {String} type - A valid Things to-do attribute name 686 | * @return {String} 687 | */ 688 | value: function getSymbol(type) { 689 | var item = this._lookup(type); 690 | 691 | return item && item.symbol; 692 | } 693 | /** 694 | * Look up an attribute name based on a symbol 695 | * @param {String} symbol - A symbol (emoji) 696 | * @return {String} 697 | */ 698 | 699 | }, { 700 | key: "getType", 701 | value: function getType(symbol) { 702 | var item = this._lookup(symbol); 703 | 704 | return item && item.type; 705 | } 706 | /** 707 | * Look up a datatype based on a symbol 708 | * @param {String} symbol - A symbol (emoji) 709 | */ 710 | 711 | }, { 712 | key: "getFormat", 713 | value: function getFormat(val) { 714 | var item = this._lookup(val); 715 | 716 | return item && item.format; 717 | } 718 | }, { 719 | key: "_lookup", 720 | value: function _lookup(val) { 721 | return this._symbols.find(function (item) { 722 | return item.symbol == val || item.type == val; 723 | }); 724 | } 725 | }, { 726 | key: "all", 727 | get: function get() { 728 | return this._symbols.map(function (item) { 729 | return item.symbol; 730 | }); 731 | } 732 | }]); 733 | 734 | return Symbols; 735 | }(); 736 | 737 | exports.Symbols = Symbols; 738 | 739 | /***/ }), 740 | /* 6 */ 741 | /***/ (function(module, exports, __webpack_require__) { 742 | 743 | "use strict"; 744 | 745 | 746 | Object.defineProperty(exports, "__esModule", { 747 | value: true 748 | }); 749 | exports.TasksParser = void 0; 750 | 751 | var _Symbols = __webpack_require__(5); 752 | 753 | var _StreamParser = __webpack_require__(4); 754 | 755 | var _Task = __webpack_require__(7); 756 | 757 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 758 | 759 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 760 | 761 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 762 | 763 | /** A class representing a Things task parser */ 764 | var TasksParser = 765 | /*#__PURE__*/ 766 | function () { 767 | /** 768 | * Create a new Things task parser 769 | * @param {Autotagger} autotagger - An autotagger reference 770 | */ 771 | function TasksParser(autotagger) { 772 | _classCallCheck(this, TasksParser); 773 | 774 | this._symbols = new _Symbols.Symbols(); 775 | this._streamParser = new _StreamParser.StreamParser(this._symbols); 776 | this._autotagger = autotagger; 777 | } 778 | /** 779 | * Parse a document containing Things tasks, and associated 780 | * attributes decorated by the appropriate Emoji symbols 781 | * @param stream {String} - The document to parse 782 | * @return {Object} An object suitable to pass to the things:/// service 783 | */ 784 | 785 | 786 | _createClass(TasksParser, [{ 787 | key: "parse", 788 | value: function parse(stream) { 789 | var _this = this; 790 | 791 | var items = this._streamParser.parse(stream); 792 | 793 | var tasks = items.map(function (item) { 794 | var task = new _Task.Task(_this._autotagger); 795 | Object.keys(item).forEach(function (attr) { 796 | switch (attr) { 797 | case 'tags': 798 | task.addTags(item[attr]); 799 | break; 800 | 801 | case 'checklistItem': 802 | task.addChecklistItem(item[attr]); 803 | break; 804 | 805 | case 'notes': 806 | task.appendNotes(item[attr]); 807 | break; 808 | 809 | default: 810 | task[attr] = item[attr]; 811 | } 812 | }); 813 | return task; 814 | }); // Return an array of Things objects 815 | 816 | return tasks.map(function (task) { 817 | return task.toThingsObject(); 818 | }); 819 | } 820 | }]); 821 | 822 | return TasksParser; 823 | }(); 824 | 825 | exports.TasksParser = TasksParser; 826 | 827 | /***/ }), 828 | /* 7 */ 829 | /***/ (function(module, exports, __webpack_require__) { 830 | 831 | "use strict"; 832 | 833 | 834 | Object.defineProperty(exports, "__esModule", { 835 | value: true 836 | }); 837 | exports.Task = void 0; 838 | 839 | var _ThingsDate = __webpack_require__(8); 840 | 841 | var _ThingsDateTime = __webpack_require__(9); 842 | 843 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 844 | 845 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 846 | 847 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 848 | 849 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 850 | 851 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 852 | 853 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 854 | 855 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 856 | 857 | /** Class representing a single Things to-do item. */ 858 | var Task = 859 | /*#__PURE__*/ 860 | function () { 861 | /** 862 | * Create a Things to-do item. 863 | * @param {Autotagger} autotagger - A reference to an autotagger 864 | */ 865 | function Task(autotagger) { 866 | _classCallCheck(this, Task); 867 | 868 | this._autotagger = autotagger; 869 | this._when = new _ThingsDateTime.ThingsDateTime(); 870 | this._deadline = new _ThingsDate.ThingsDate(); 871 | this.attributes = { 872 | tags: [], 873 | 'checklist-items': [] 874 | }; 875 | } 876 | /** 877 | * The deadline to apply to the to-do. Relative dates are 878 | * parsed with the DateJS library. 879 | * @type {String} 880 | */ 881 | 882 | 883 | _createClass(Task, [{ 884 | key: "addChecklistItem", 885 | 886 | /** 887 | * Add a checklist item to the to-do. 888 | * @param {String} item - The checklist item to add 889 | */ 890 | value: function addChecklistItem(item) { 891 | if (item.trim) item = item.trim(); 892 | if (!Array.isArray(item)) item = [item]; 893 | this.addChecklistItems(item); 894 | } 895 | /** 896 | * Add an array of checklist items to the to-do 897 | * @param {String[]} items - An array of checklist items to add 898 | */ 899 | 900 | }, { 901 | key: "addChecklistItems", 902 | value: function addChecklistItems() { 903 | var _this$attributes$chec; 904 | 905 | var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 906 | items = items.filter(function (item) { 907 | return item.length > 0; 908 | }).map(function (item) { 909 | return { 910 | type: 'checklist-item', 911 | attributes: { 912 | title: item.trim() 913 | } 914 | }; 915 | }); 916 | 917 | (_this$attributes$chec = this.attributes['checklist-items']).push.apply(_this$attributes$chec, _toConsumableArray(items)); 918 | } 919 | /** 920 | * Add one or more tags to the to-do, separated by commas. 921 | * Tags that do not already exist will be ignored. 922 | * @param {String|String[]} tags - An array or comma-separated list of one or more tags 923 | */ 924 | 925 | }, { 926 | key: "addTags", 927 | value: function addTags(tags) { 928 | var _this$attributes$tags; 929 | 930 | if (typeof tags == 'string') tags = tags.split(','); 931 | 932 | (_this$attributes$tags = this.attributes.tags).push.apply(_this$attributes$tags, _toConsumableArray(tags.map(function (tag) { 933 | return tag.trim(); 934 | }))); 935 | } 936 | /** 937 | * Appends a new line to the notes. 938 | * @param {String|String[]} notes - An array or single string of notes 939 | */ 940 | 941 | }, { 942 | key: "appendNotes", 943 | value: function appendNotes(notes) { 944 | if (typeof notes == 'string') notes = [notes]; 945 | 946 | if (this.attributes.notes) { 947 | this.attributes.notes += '\n\n' + notes.join('\n\n'); 948 | } else { 949 | this.attributes.notes = notes.join('\n\n'); 950 | } 951 | } 952 | /** 953 | * Export the current to-do, with all defined attributes, 954 | * as an object to be passed to the things:/// URL scheme. 955 | * @see {@link https://support.culturedcode.com/customer/en/portal/articles/2803573#json|Things API documentation} 956 | * @return {Object} An object suitable to pass to the things:/// service 957 | */ 958 | 959 | }, { 960 | key: "toThingsObject", 961 | value: function toThingsObject() { 962 | return { 963 | type: 'to-do', 964 | attributes: this.attributes 965 | }; 966 | } 967 | /** 968 | * Test whether a string is a things item ID 969 | * @param {String} value - The item name or id to test 970 | * @private 971 | */ 972 | 973 | }, { 974 | key: "_isItemId", 975 | value: function _isItemId(value) { 976 | var pattern = /^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/img; 977 | return pattern.test(value); 978 | } 979 | }, { 980 | key: "deadline", 981 | set: function set(date) { 982 | this._deadline.date = date; 983 | this.attributes.deadline = this._deadline.toString(); 984 | } 985 | }, { 986 | key: "heading", 987 | set: function set(heading) { 988 | this.attributes.heading = heading.trim(); 989 | } 990 | /** 991 | * The title or ID of the project or area to add to. 992 | * @type {String} 993 | */ 994 | 995 | }, { 996 | key: "list", 997 | set: function set(nameOrId) { 998 | var prop = this._isItemId(nameOrId) ? 'list-id' : 'list'; 999 | this.attributes[prop] = nameOrId.trim(); 1000 | } 1001 | /** 1002 | * The text to use for the notes field of the to-do. 1003 | * @type {String} 1004 | */ 1005 | 1006 | }, { 1007 | key: "notes", 1008 | set: function set(notes) { 1009 | this.attributes.notes = notes.trim(); 1010 | } 1011 | /** 1012 | * The time to set for the task reminder. Overrides any time 1013 | * specified in "when". Fuzzy times are parsed with the 1014 | * DateJS library. 1015 | * @type {String} 1016 | */ 1017 | 1018 | }, { 1019 | key: "reminder", 1020 | set: function set(time) { 1021 | this._when.timeOverride = time.trim(); 1022 | this.attributes.when = this._when.toString(); 1023 | } 1024 | /** 1025 | * The title of the to-do. Value will be parsed by the 1026 | * autotagger, matching the string against a dictionary 1027 | * of regular expressions and auto-applying attributes 1028 | * for all matches. 1029 | * @type {String} 1030 | */ 1031 | 1032 | }, { 1033 | key: "title", 1034 | set: function set(value) { 1035 | var _this = this; 1036 | 1037 | this.attributes.title = value.trim(); 1038 | 1039 | var autotagged = this._autotagger.parse(value); 1040 | 1041 | if (!autotagged) return; 1042 | var properties = 'list when reminder deadline heading checklistItem'; 1043 | properties.split(' ').forEach(function (property) { 1044 | if (!autotagged[property]) return; 1045 | _this[property] = autotagged[property]; 1046 | }); 1047 | this.addTags(autotagged.tags || ''); 1048 | this.addChecklistItem(autotagged.checklistItem || []); 1049 | this.appendNotes(autotagged.notes || ''); 1050 | } 1051 | /** 1052 | * The start date of the to-do. Values can be "today", 1053 | * "tomorrow", "evening", "tonight", "anytime", "someday", 1054 | * or a fuzzy date string with an optional time value, 1055 | * which will add a reminder. 1056 | * @type {String} 1057 | */ 1058 | 1059 | }, { 1060 | key: "when", 1061 | set: function set(value) { 1062 | this._when.datetime = value.trim(); 1063 | this.attributes.when = this._when.toString(); 1064 | } 1065 | }]); 1066 | 1067 | return Task; 1068 | }(); 1069 | 1070 | exports.Task = Task; 1071 | 1072 | /***/ }), 1073 | /* 8 */ 1074 | /***/ (function(module, exports, __webpack_require__) { 1075 | 1076 | "use strict"; 1077 | 1078 | 1079 | Object.defineProperty(exports, "__esModule", { 1080 | value: true 1081 | }); 1082 | exports.ThingsDate = void 0; 1083 | 1084 | var config = _interopRequireWildcard(__webpack_require__(1)); 1085 | 1086 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } 1087 | 1088 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1089 | 1090 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 1091 | 1092 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 1093 | 1094 | /** A class representing a date in Things */ 1095 | var ThingsDate = 1096 | /*#__PURE__*/ 1097 | function () { 1098 | /** Create a new ThingsDate object */ 1099 | function ThingsDate() { 1100 | _classCallCheck(this, ThingsDate); 1101 | } 1102 | /** 1103 | * The date value. In addition to fuzzy values parseable by DateJS, 1104 | * valid values include "someday", "anytime", "evening", and "tonight". 1105 | * @type {String} 1106 | */ 1107 | 1108 | 1109 | _createClass(ThingsDate, [{ 1110 | key: "toString", 1111 | 1112 | /** 1113 | * Convert to a Things-formatted string: YYYY-MM-DD@HH:MM, or 1114 | * relative keyword: evening, someday, anytime. 1115 | * @return {String} A Things-formatted date string 1116 | */ 1117 | value: function toString() { 1118 | // This is still way too big. Some of this stuff 1119 | // should be parsed as values are set, and 1120 | // private functions should reference object 1121 | // properties, not passed parameters. 1122 | // If a time override is present, but no date is specified, 1123 | // assume this is a task for today. 1124 | var timeOverride = this._timeOverride; 1125 | var datetime = this._datetime || timeOverride && 'today'; // Shorthand values like "someday" and "anytime" cannot have 1126 | // an associated time, and are unparseable by DateJS. 1127 | 1128 | var isDateOnly = this._isDateOnlyShorthand(datetime); 1129 | 1130 | if (isDateOnly) return datetime; // Shorthand values like "tonight" need to be normalized to 1131 | // "evening" for Things to understand. However, if there's 1132 | // a time override specified, we'll change the value 1133 | // to "today" so DateJS can do its thing. Assuming the 1134 | // reminder is in the evening, it'll get changed back later. 1135 | 1136 | var isEvening = this._isEveningShorthand(datetime); 1137 | 1138 | if (isEvening && !timeOverride) return 'evening'; 1139 | if (isEvening && timeOverride) datetime = 'today'; // DateJS will take relative dates like "1 week" without 1140 | // complaint, but it interprets them as "1 week from the 1141 | // first day of the year". Prepend a "+" to anchor to 1142 | // today's date. 1143 | 1144 | datetime = datetime.replace(/^(\d+)\s*(h|d|w|m|y|minute|hour|day|week|month|year)(s?)/i, '+$1 $2$3'); // DateJS won't understand dates like "in 2 weeks". 1145 | // Reformat as "+2 weeks". 1146 | 1147 | datetime = datetime.replace(/^in\s+(\d)/i, '+$1'); // Offset shorthand 1148 | 1149 | var offset = this._parseOffset(datetime); 1150 | 1151 | var dateOffset = 0; 1152 | 1153 | if (offset) { 1154 | datetime = offset.datetime; 1155 | dateOffset = offset.offset; 1156 | } // Parse the date with DateJS. If it's invalid, just pass 1157 | // the raw value and let Things take a crack at it. 1158 | 1159 | 1160 | var dt = Date.parse(datetime); 1161 | if (!dt) return datetime; // Override time if we explicitly set a reminder 1162 | 1163 | if (timeOverride) { 1164 | var time = Date.parse(timeOverride); 1165 | 1166 | if (time) { 1167 | var hour = time.getHours(); 1168 | var minute = time.getMinutes(); 1169 | dt.set({ 1170 | hour: hour, 1171 | minute: minute 1172 | }); 1173 | } 1174 | } // Sometimes relative dates, like "Monday", are 1175 | // interpreted as "last Monday". If the date is in the 1176 | // past, add a week. 1177 | 1178 | 1179 | var isDatePast = this._isDatePast(dt); 1180 | 1181 | if (isDatePast) dt.add(1).week(); // If the time is expressed without an AM/PM suffix, 1182 | // and it's super early, we probably meant PM. 1183 | 1184 | var isTooEarly = this._isTimeEarlyAndAmbiguous(datetime, dt); 1185 | 1186 | if (isTooEarly) dt.add(12).hours(); // Process date offset 1187 | 1188 | if (dateOffset != 0) dt.add(dateOffset).days(); // Return a date- or datetime-formatted string that 1189 | // Things will understand. 1190 | 1191 | return this._formatThingsDate(dt, isEvening); 1192 | } 1193 | /** 1194 | * Test whether a string is shorthand for "tonight" 1195 | * @param {String} value - The string to test 1196 | * @return {boolean} True if string equals evening shorthand keywords 1197 | * @private 1198 | */ 1199 | 1200 | }, { 1201 | key: "_isEveningShorthand", 1202 | value: function _isEveningShorthand(value) { 1203 | var pattern = /^((this )?evening|tonight)$/i; 1204 | return pattern.test(value); 1205 | } 1206 | /** 1207 | * Test whether a string is a dateless shorthand value 1208 | * @param {String} value - The string to test 1209 | * @return {boolean} True if string starts with "someday" or "anytime" 1210 | * @private 1211 | */ 1212 | 1213 | }, { 1214 | key: "_isDateOnlyShorthand", 1215 | value: function _isDateOnlyShorthand(value) { 1216 | var pattern = /^(someday|anytime)/i; 1217 | return pattern.test(value); 1218 | } 1219 | /** 1220 | * Test whether a date is in the past 1221 | * @param {Date} parsed - The datetime, parsed by DateJS 1222 | * @return {boolean} True if the date is in the past 1223 | * @private 1224 | */ 1225 | 1226 | }, { 1227 | key: "_isDatePast", 1228 | value: function _isDatePast(parsed) { 1229 | var date = parsed.clone().clearTime(); 1230 | var today = Date.today().clearTime(); 1231 | return date.compareTo(today) == -1; 1232 | } 1233 | /** 1234 | * Test whether a time is ambiguously specified (lacking an am/pm 1235 | * suffix) and possibly early in the morning. 1236 | * @param {String} str - The raw, unparsed date 1237 | * @param {DateJS} parsed - The datetime, parsed by DateJS 1238 | * @return {boolean} True if AM/PM suffix is missing and hour is before 7 1239 | * @private 1240 | */ 1241 | 1242 | }, { 1243 | key: "_isTimeEarlyAndAmbiguous", 1244 | value: function _isTimeEarlyAndAmbiguous(str, parsed) { 1245 | var hasAmPmSuffix = /\d *[ap]m?\b/i.test(str); 1246 | var earliest = config.earliestAmbiguousMorningHour; 1247 | var isEarly = parsed.getHours() > 0 && parsed.getHours() < earliest; 1248 | return !hasAmPmSuffix && isEarly; 1249 | } 1250 | }, { 1251 | key: "_parseOffset", 1252 | value: function _parseOffset(str) { 1253 | var pattern = /^(.+)\s([+-]\d+)$/; 1254 | var match = pattern.exec(str); 1255 | if (!match) return; 1256 | return { 1257 | datetime: match[1], 1258 | offset: parseInt(match[2]) 1259 | }; 1260 | } 1261 | /** 1262 | * Test whether a datetime is set to midnight 1263 | * @param {DateJS} parsed - The datetime, parsed by DateJS 1264 | * @return {boolean} True if time is midnight 1265 | * @private 1266 | */ 1267 | 1268 | }, { 1269 | key: "_isTimeMidnight", 1270 | value: function _isTimeMidnight(parsed) { 1271 | var hours = parsed.getHours(); 1272 | var minutes = parsed.getMinutes(); 1273 | return hours + minutes == 0; 1274 | } 1275 | /** 1276 | * Format a DateJS datetime as a valid Things datetime string. 1277 | * @param {DateJS} datetime - The datetime, parsed by DateJS 1278 | * @param {boolean} forceEvening - Force to evening, overriding time value 1279 | * @return {string} A Things-formatted date 1280 | * @private 1281 | */ 1282 | 1283 | }, { 1284 | key: "_formatThingsDate", 1285 | value: function _formatThingsDate(datetime, forceEvening) { 1286 | var date = datetime.toString('yyyy-MM-dd'); 1287 | var time = datetime.toString('@HH:mm'); 1288 | if (this._isTimeMidnight(datetime)) time = ''; 1289 | if (!this._allowTime) time = ''; 1290 | var isToday = datetime.between(Date.today(), Date.today().addDays(1)); 1291 | var isEvening = forceEvening || datetime.getHours() > config.eveningStartsAtHour; 1292 | if (isToday && isEvening) date = 'evening'; 1293 | return date + time; 1294 | } 1295 | }, { 1296 | key: "date", 1297 | set: function set() { 1298 | var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 1299 | this._datetime = value.trim().toLowerCase(); 1300 | } 1301 | }]); 1302 | 1303 | return ThingsDate; 1304 | }(); 1305 | 1306 | exports.ThingsDate = ThingsDate; 1307 | 1308 | /***/ }), 1309 | /* 9 */ 1310 | /***/ (function(module, exports, __webpack_require__) { 1311 | 1312 | "use strict"; 1313 | 1314 | 1315 | Object.defineProperty(exports, "__esModule", { 1316 | value: true 1317 | }); 1318 | exports.ThingsDateTime = void 0; 1319 | 1320 | var _ThingsDate2 = __webpack_require__(8); 1321 | 1322 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 1323 | 1324 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1325 | 1326 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } _setPrototypeOf(subClass.prototype, superClass && superClass.prototype); if (superClass) _setPrototypeOf(subClass, superClass); } 1327 | 1328 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 1329 | 1330 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 1331 | 1332 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 1333 | 1334 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 1335 | 1336 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 1337 | 1338 | function _getPrototypeOf(o) { _getPrototypeOf = Object.getPrototypeOf || function _getPrototypeOf(o) { return o.__proto__; }; return _getPrototypeOf(o); } 1339 | 1340 | /** 1341 | * A class representing a datetime in Things 1342 | * @extends ThingsDate 1343 | * */ 1344 | var ThingsDateTime = 1345 | /*#__PURE__*/ 1346 | function (_ThingsDate) { 1347 | /** Create a new ThingsDate object */ 1348 | function ThingsDateTime() { 1349 | var _this; 1350 | 1351 | _classCallCheck(this, ThingsDateTime); 1352 | 1353 | _this = _possibleConstructorReturn(this, _getPrototypeOf(ThingsDateTime).call(this)); 1354 | _this._allowTime = true; 1355 | return _this; 1356 | } 1357 | /** 1358 | * The date, with optional time. In addition to fuzzy values parseable by 1359 | * DateJS, valid values include "someday", "anytime", "evening", and "tonight". 1360 | * @type {String} 1361 | */ 1362 | 1363 | 1364 | _createClass(ThingsDateTime, [{ 1365 | key: "datetime", 1366 | set: function set(value) { 1367 | this._datetime = value.trim().toLowerCase(); 1368 | } 1369 | /** 1370 | * The time override. The datetime value's hour and minute 1371 | * will be repolaced with the time override if specified. 1372 | * @type {String} 1373 | */ 1374 | 1375 | }, { 1376 | key: "timeOverride", 1377 | set: function set(value) { 1378 | this._timeOverride = value.trim().toLowerCase(); 1379 | } 1380 | }]); 1381 | 1382 | _inherits(ThingsDateTime, _ThingsDate); 1383 | 1384 | return ThingsDateTime; 1385 | }(_ThingsDate2.ThingsDate); 1386 | 1387 | exports.ThingsDateTime = ThingsDateTime; 1388 | 1389 | /***/ }), 1390 | /* 10 */ 1391 | /***/ (function(module, exports, __webpack_require__) { 1392 | 1393 | "use strict"; 1394 | 1395 | 1396 | Object.defineProperty(exports, "__esModule", { 1397 | value: true 1398 | }); 1399 | exports.Project = void 0; 1400 | 1401 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 1402 | 1403 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 1404 | 1405 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 1406 | 1407 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 1408 | 1409 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1410 | 1411 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 1412 | 1413 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 1414 | 1415 | var Project = 1416 | /*#__PURE__*/ 1417 | function () { 1418 | function Project(name, tasks) { 1419 | _classCallCheck(this, Project); 1420 | 1421 | this._name = name; 1422 | this._tasks = tasks; 1423 | } 1424 | 1425 | _createClass(Project, [{ 1426 | key: "toThingsObject", 1427 | value: function toThingsObject() { 1428 | var _this = this; 1429 | 1430 | // Get an array of unique (case-insensitive) headings 1431 | var headings = this._tasks.filter(function (item) { 1432 | return item.attributes.heading; 1433 | }).map(function (item) { 1434 | return item.attributes.heading; 1435 | }).map(function (item) { 1436 | return { 1437 | value: item, 1438 | lower: item.toLowerCase() 1439 | }; 1440 | }).filter(function (elem, pos, arr) { 1441 | return arr.findIndex(function (item) { 1442 | return item.lower == elem.lower; 1443 | }) == pos; 1444 | }).map(function (item) { 1445 | return { 1446 | type: "heading", 1447 | attributes: { 1448 | title: item.value 1449 | } 1450 | }; 1451 | }); 1452 | 1453 | var tasks = this._tasks.map(function (task) { 1454 | task.attributes.list = _this._name; 1455 | return task; 1456 | }); 1457 | 1458 | return [{ 1459 | type: "project", 1460 | attributes: { 1461 | title: this._name, 1462 | items: headings 1463 | } 1464 | }].concat(_toConsumableArray(tasks)); 1465 | } 1466 | }]); 1467 | 1468 | return Project; 1469 | }(); 1470 | 1471 | exports.Project = Project; 1472 | 1473 | /***/ }) 1474 | /******/ ]); --------------------------------------------------------------------------------