MilkScript

Automate tasks with MilkScript.

menu

Quickstart: Tasks from note

If you often find yourself creating the same set of tasks over and over again, the following script might be a good timesaver.

How it works

  1. The script gets a selected task.
  2. After that, it gets the last modified note of the task.
  3. Then it creates tasks, notes, and subtasks from the note.

Syntax

List # Sun & Fun
Task * Organize Alice's birthday party #Inbox !1 ^Nov 4
Note Buy stock invitations from a retail store.

Be sure to include the party's start and end time, your phone number, directions to the party location, and the party's theme.
Subtask (level-1) ** Rent a bouncy castle ^tomorrow !2
Subtask (level-2) *** Book the clown ^tomorrow !2 #phone

For example, a note like this:

* Organize Alice's birthday party #Inbox !1 ^Nov 4 A rough agenda for the party: 0:00 to 0:15: Everyone arrives. 0:15 to 0:30: Guests eat snacks or meals. 0:30 to 0:50: Play organized games. 0:50 to 1:10: Alice opens presents. 1:10 to 1:30: Sing Happy Birthday and eat cake. 1:30 to 2:00: Unorganized playtime. Say goodbye. ** Send invitations ^today !1 #mail Buy stock invitations from a retail store. Be sure to include the party's start and end time, your phone number, directions to the party location, and the party's theme. ** Book the clown ^tomorrow !2 #phone ** Rent a bouncy castle ^tomorrow !2 ** Order cupcakes ^in 2 days

will generate the following task:

Set it up

  1. Open the web app or desktop app.
  2. Click the MilkScript button at the top right, then click New...
  3. Copy the code below and paste it into the script editor.
const selectedTasks = rtm.getSelectedTasks();

if (selectedTasks.length !== 1) {
  throw new Error("Zero or more than one task is selected.");
}

const note = selectedTasks
  .flatMap(task => task.getNotes())
  .reduce((result, note) =>
    result == null || note.getModifiedDate() > result.getModifiedDate() ?
      note : result, null);

if (note == null) {
  throw new Error("The selected task has no notes.");
}

const handlers = [ handleNote, handleTask, handleList ];

const context = note.getContent()
    .split('\n')
    .flatMap(line => handlers.map(handler => [line, handler]))
    .reduce((context, [ line, handler ]) => handler(line, context), {
      hierarchy : [],
      lists : new Map(
          rtm.getLists().map(list => [list.getName().toLowerCase(), list])),
      smartLists : new Set(rtm.getSmartLists().map(
          smartList => smartList.getName().toLowerCase()))
    });

const [task] = context.hierarchy;
if (task != null && context.note != null && context.note.trim().length > 0) {
  task.addNote(context.note);
}

/**
 * @return {number}
 * @param {string} whitespace
 * @param {string} bullets
 */
function getTaskLevel(whitespace, bullets) {
  return bullets.length > 1 ? bullets.length - 1
                            : Math.floor(whitespace.length / 2);
}

/**
 * @return {string}
 * @param {string} string
 */
function stripRedundantIndentation(string) {
  const reduntantIndentation =
      [...string.matchAll(/^[^\S\r\n]*(?=\S)/gm) ].reduce(
          (indentation, [ whitespace ]) =>
              Math.min(indentation, whitespace.length),
          Number.MAX_SAFE_INTEGER);

  return string.replace(
    new RegExp(`^[^\S\r\n]{${reduntantIndentation}}`, 'gm'), '');
};

/**
 * @return {!rtm.Task}
 * @param {string} name
 * @param {(?rtm.Task|?rtm.List)=} parent — Optional parent task or list.
 */
function addTask(name, parent) {
  return parent == null              ? rtm.addTask(name, true)
         : parent.addSubtask != null ? parent.addSubtask(name, true)
                                     : parent.addTask(name, true);
}

/**
 * @typedef {{
 *   hierarchy: !Array,
 *   note: string|undefined,
 *   list: rtm.List|undefined,
 *   lists: Map,
 *   smartLists: Set
 * }} Context
 */

/**
 * @return {!Context}
 * @param {string} line
 * @param {!Context} context
 */
function handleNote(line, context) {
  const [note] = line.match(/^\s*(?:[^\s\#\-\+\*]|$(?!.)).*/) ?? [];
  const [task] = context.hierarchy;

  if (note == null &&
      context.note != null &&
      context.note.trim().length > 0 &&
      task != null) {
    task.addNote(stripRedundantIndentation(context.note));

    return {...context, note : undefined};
  }

  return {
    ...context,
    note: note == null         ? null :
          context.note == null ? note :
          `${context.note}\n${note}`
  };
}

/**
 * @return {!Context}
 * @param {string} line
 * @param {!Context} context
 */
function handleTask(line, context) {
  const [_, whitespace, bullets, name] =
      line.match(/^(\s*)([\-\+\*]+)\s*(\S+.*)/) ?? [];
  if (name == null) {
    return context;
  }

  const [task, ...ancestors] = context.hierarchy;

  const level = getTaskLevel(whitespace, bullets);
  if (level === ancestors.length) {
    const [parent] = ancestors;

    return {
      ...context,
      hierarchy : [ addTask(name, parent ?? context.list), ...ancestors ]
    };
  }

  if (level < ancestors.length) {
    const [parent, ...grandparents] =
      ancestors.slice(ancestors.length - level);

    return {
      ...context,
      hierarchy : [
        addTask(name, parent ?? context.list),
        ...(parent != null ? [ parent ] : []), ...grandparents
      ]
    };
  }

  return {
    ...context,
    hierarchy : [
      addTask(name, task ?? context.list), ...(task != null ? [ task ] : []),
      ...ancestors
    ]
  };
}

/**
 * @return {!Context}
 * @param {string} line
 * @param {!Context} context
 */
function handleList(line, context) {
  const [_, listName] = line.match(/^\s*#\s*(\S+.*)/) ?? [];

  if (listName == null) {
    return context;
  }

  const existingList = context.lists.get(listName.toLowerCase());
  if (existingList != null) {
    return {...context, list : existingList};
  }

  const uniqueName =
      [...Array(100).keys() ]
          .map(value => `${listName}${value > 0 ? ' ' + value : ''}`)
          .find(name => !context.lists.has(name.toLowerCase()) &&
                        !context.smartLists.has(name.toLowerCase()));
  if (uniqueName == null) {
    return context;
  }

  const list = rtm.addList(uniqueName);

  return {
    ...context,
    list : list,
    lists : context.lists.set(list.getName().toLowerCase(), list)
  };
}
  1. At the top left, click Untitled script.
  2. Enter a name for your script (e.g. Create Tasks from Note), then close the script editor.

Try it out

  1. Add a new task named Template: Travel Document Checklist.
  2. Add the following note to the task:
# Travel Document Checklist * Valid forms of ID Passport, driver's license, travel visa, permanent resident card, birth certificate, etc. * Copies of all IDs Make copies of all the valid IDs you plan to bring with you and store them separately from your original IDs. * Confirmation Documents Print confirmations of all travel arrangements, including transportation, lodging, excursions and activities. * Visa Documents For safety, make a paper copy of your visa for international trips. This is also helpful if you have an electronic visa you cannot access. * Travel Protection Documents Bring a copy of your travel protection plan, which lists your coverage details as well as helpful contact information. * Emergency Contacts If traveling internationally, have the country's version of "911" and the embassy's number programmed into your phone and printed. Also, have contacts for your family and friends printed in case your phone stops working. * Credit Cards Preferably bring more than one. If you lose one credit card, you have a spare. * Boarding Passes Be sure to bring boarding passes for any planes, buses, or trains on your itinerary. * Emergency Cash Have extra money available - U.S. Dollars or the currency for the country you are in.
  1. Click the MilkScript button at the top right.
  2. Click the recently created script, then click Yes, run script.
  3. When the script execution completes, check your lists for the newly created Travel Document Checklist.