# Match Patterns


It's recommended that you go through the 5 Minute Quick Start first to understand how to create and install your plugin.

# Compact Form

Instead of writing out potentially 100s of forms for a command's match property, you can use compact form syntax.

eg. '[delete/remove] [/the ]previous [word/# words]' expands to:

  • delete previous word
  • remove previous word
  • delete the previous word
  • remove the previous word
  • delete previous # words
  • remove previous # words
  • delete the previous # words
  • remove the previous # words


  • You cannot nest brackets.
  • Priority is respected by the order in the brackets.

# Wildcard Matching

What if we want a plugin that accepts an arbitrary argument after some key words (AKA a slot)?

Let's make a plugin that shows the weather for any city when user says: weather for [city name] (eg. weather for Chiang Mai)

Easy peasy.

Use * in your match string to greedily match any words.


/// <reference types="lipsurf-types/extension"/>
declare const PluginBase: IPluginBase;

export default <IPluginBase & IPlugin> {...PluginBase, ...{
    niceName: 'Weather',
    match: /.*\.accuweather\.com/,
    version: '1.0.0',
    commands: [{
        name: 'Check the Weather',
        description: 'Check the weather for a given city.',
        // say it on any page (not just accuweather domain)
        global: true,
        match: '[weather/forecast] [for/in] *',
        pageFn: async (transcript: string, q: string) => {
            // https://api.accuweather.com/locations/v1/cities/autocomplete?q=chiang%20mai&apikey=d41dfd5e8a1748d0970cba6637647d96&language=en-us&get_param=value
            // ex resp: [{"Version":1,"Key":"317505","Type":"City","Rank":41,"LocalizedName":"Chiang Mai","Country":{"ID":"TH","LocalizedName":"Thailand"},"AdministrativeArea":{"ID":"50","LocalizedName":"Chiang Mai"}}]
            // https://www.accuweather.com/en/th/chiang-mai/317505/weather-forecast/317505
            const resp = await (await window.fetch(`https://api.accuweather.com/locations/v1/cities/autocomplete?q=${q}&apikey=d41dfd5e8a1748d0970cba6637647d96&language=en-us&get_param=value`)).json();
            let cityId = resp[0].Key;
            let countryCode = resp[0].Country.ID.toLowerCase();
            let cityName = resp[0].LocalizedName.replace(' ', '-');
            window.location.href = `https://www.accuweather.com/en/${countryCode}/${cityName}/${cityId}/weather-forecast/${cityId}`;

# Numeral Matching

Ain't nothin' to it.

Use # in your match string to match numerals or ordinals including ones that are spelled-out (ie. four-thousand)

Let's write a plugin that opens a tab with URL x for y minutes so that we can limit it's time wasting-ness. This might be useful if we need to check Facebook but don't want to get sucked into the feed for too long.


/// <reference types="lipsurf-types/extension"/>
declare const PluginBase: IPluginBase;

export default <IPluginBase & IPlugin> {...PluginBase, ...{
    niceName: 'Anti-procrastination',
    description: 'Helpers for overcoming procrastination.',
    match: /.*/,
    commands: [{
        name: 'Self Destructing Tab',
        description: 'Open a new tab with x website for y minutes. Useful for limiting the time-sucking power of sites like facebook, reddit, twitter etc.',
        global: true,
        match: 'open * for # minutes',
        pageFn: (transcript: string, siteStr: string, minutes: number) => {
            console.log(`site: ${siteStr}, minutes: ${minutes}`);

# Match Function

For the most advanced cases, you can write a function that takes in transcripts as they come down the wire and return undefined when there's no match, or an array of arguments to pass to the pageFn when there is a match.

Use cases include:

  • Match based on something on the page
  • Match based on some internal plugin state
  • Regex matching

Cake walk.

We need to make match an object of type DynamicMatch

How about a plugin for Gmail that moves the currently selected messages to a folder that the user commands to?


We could use the wildcard matching for this (eg. "move to *") but then we cant limit the user's choices to valid folders.

/// <reference types="lipsurf-types/extension"/>
declare const PluginBase: IPluginBase;

export default <IPluginBase & IPlugin> {...PluginBase, ...{
    niceName: 'Gmail',
    match: /^https:\/\/mail\.google\.com/,
    commands: [{
        name: 'Move to Folder',
        description: 'Move already selected emails to a spoken folder',
        match: {
            description: 'Say "move to [folder name]"',
            fn: (transcript: string) => {
                // exercise left to the reader...
        pageFn: () => {
            // exercise left to the reader...


The match function needs to check the whole transcript for matches and then return indexes for which parts it used. This way the transcript can have the proper section success highlighted (in green) that matches a command, and command chaining will work.