npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

placeholders-toolkit

v0.1.5

Published

A placeholders system for supplying dynamic content to Raycast extensions.

Downloads

25

Readme

Placeholders Toolkit

A placeholders system for supplying dynamic content to Raycast extensions.

Overview

This package provides the core functionality for the placeholders system seen in Raycast extensions such as PromptLab and Pins. The Placeholders Toolkit helps you create, manage, and apply complex placeholders, compose them into more powerful structures, and ultimately use them to give your users a more dynamic experience.

The Placeholders Toolkit supports a wide array of built-in placeholders, including placeholders for getting the current date and time, the current user, the list of upcoming calendar events, custom scripts, and more. You can also create your own placeholders, either programmatically or by specifying them in JSON. Users of your extensions can then use these placeholders to input dynamic content into your extension's commands and input fields. For example, you could create a journal extension that allows users to insert various placeholders to give context to their entries; or you could add support for convenient placeholders to a GitHub/GitLab-esque "Create Issue" command—for example, allowing users to type {{osVersion}} to insert their macOS version on-the-fly.

To get started, follow the brief tutorial below, then check out the API Reference.

Installation

npm install placeholders-toolkit

View on npm

Usage

Basic Usage

import { PLApplicator } from "placeholders-toolkit";

const myFunc = async () => {
  const initialText = "Hello, my name is {{user}}.";
  const substitutedText = await PLApplicator.bulkApply(initialText);
  console.log(substitutedText); // Hello, my name is steven.
}

Applying Placeholders

The {@link PLApplicator.bulkApply} method is the main method for applying placeholders. It takes a string and returns a promise that resolves to the string with all placeholders applied. Placeholders are applied in order of precedence, with faster placeholders being applied first, in general. Script placeholders, such as {{js:...}}, are applied last.

When using {@link PLApplicator.bulkApply}, you can provide a context object that will be passed to all placeholders. This allows you to pass information between placeholders, e.g.:

const p1 = PLCreator.newPlaceholder("test", { apply_fn: async (str, context) => {
  return { result: "", __my_data: "Hello" }; // Stores some data for the next placeholder to use.
} });

const p2 = PLCreator.newPlaceholder("test2", { apply_fn: async (str, context) => {
  const data = context?.__my_data as string; // Retrieves the data from the previous placeholder.
  return { result: data?.length ? `${data}, world!`: "Goodbye!" };
} });

PLApplicator.bulkApply("{{test}}{{test2}}", { customPlaceholders: [p1, p2] }).then((result) => {
  console.log(result); // Hello, world!
});

In the example above, notice how we pass [p1, p2] as the third argument to {@link PLApplicator.bulkApply}. This specifies a list of custom placeholders that {@link PLApplicator.bulkApply} will consider in addition to the default placeholders. If you don't want to use any custom placeholders, you can omit this argument. If you want to use only custom placeholders, you can overwrite the default placeholders by passing an empty array as the fourth argument.

{@link PLApplicator} has several methods in addition to {@link PLApplicator.bulkApply} that allow you to apply placeholders in different ways. For example, {@link PLApplicator.applyToString} applies placeholders sequentially without using a managed context object (though you can still implement one yourself if you want).

Creating Custom Placeholders

Single-Value Placeholders

You can create your own placeholders either programmatically or by specifying them in JSON. All of the following are valid, equivalent ways to create a placeholder:

const myPlaceholder = PLCreator.newPlaceholder("myPlaceholder", { replace_with: "Testing 1 2 3..." });

const myPlaceholder2 = PLCreator.buildPlaceholdersFromValueDict({ myPlaceholder2: "Testing 1 2 3..." })[0];

const myPlaceholder3 = PLLoader.loadPlaceholderFromJSONString(JSON.stringify({
  name: "myPlaceholder3",
  value: "Testing 1 2 3...",
}));

const myPlaceholder4: Placeholder = {
  name: "myPlaceholder4",
  regex: /{{myPlaceholder4}}/g,
  apply: async (str: string, context?: { [key: string]: unknown }) => {
    return { result: "Testing 1 2 3...", myPlaceholder4: "Testing 1 2 3..." };
  },
  constant: true,
  fn: async (content: string) => (await myPlaceholder4.apply(`{{myPlaceholder4}}`)).result,
  description: "",
  example: "",
  hintRepresentation: `{{myPlaceholder4}}`,
  fullRepresentation: `myPlaceholder4 (Custom)`,
  type: PlaceholderType.Informational,
  categories: [],
};

Since this placeholder always maps to a single value, we call it a single-value placeholder. To create several single-value placeholders at once, you can use the {@link PLCreator.buildPlaceholdersFromValueDict} method:

const singleValuePlaceholders = PLCreator.buildPlaceholdersFromValueDict({
  p1: "Testing",
  p2: "1",
  p3: "2",
  p4: "3",
  p5: "...",
});

Note that, when using {@link PLApplicator.bulkApply}, you can use existing placeholders when specifying the values for single-value placeholders, e.g.:

const singleValuePlaceholders = PLCreator.buildPlaceholdersFromValueDict({
  p6: "Hello, my name is {{user}}.",
  p7: "Today is {{day}}.",
});

PLApplicator.bulkApply("{{p6}} {{p7}}", { customPlaceholders: singleValuePlaceholders }).then((result) => {
  console.log(result); // Hello, my name is steven. Today is Monday.
});

As far as the placeholders system is concerned, p6 and p7 are just like any other single-value placeholders. However, when {@link PLApplicator.bulkApply} is called, the substitutions for user and day are applied after the substitutions for p6 and p7, so each placeholder is present in the string at the time it is applied.

Dynamic-Value Placeholders

To create a dynamic-value placeholder, a placeholder that maps to a function that returns a value, you can use the {@link PLCreator.buildPlaceholdersFromFnDict} method:

const dynamicValuePlaceholders = PLCreator.buildPlaceholdersFromFnDict({
  p8: async (str, context) => ({ result: str.includes("{{p9}}") ? "Hello. " : "Goodbye. " }),
  p9: async (str, context) => ({ result: "It is currently {{time}}." }),
});

PLApplicator.bulkApply("{{p8}} {{p9}}", { customPlaceholders: dynamicValuePlaceholders }).then((result) => {
  console.log(result); // Hello.  It is currently 13:00:56 PM.
});

Alternatively, you can create dynamic-value placeholders using {@link PLCreator.newPlaceholder} by specifying the apply_fn option:

const dynamicPlaceholder = PLCreator.newPlaceholder("dynamicPlaceholder", {
  apply_fn: async (str: string, context?: { [key: string]: unknown } | undefined) => {
    const res = str.length > 10 ? "long" : "short";
    return {
      result: res,
      dynamicPlaceholder: res,
    };
  }
});
Load Placeholders from Files

Suppose you have a JSON file custom_placeholders.json containing the following placeholder:

{
  "{{summarize( sentences=(\\d+))?:(([^{]|{(?!{)|{{[\\s\\S]*?}})*?)}}": {
    "name": "summarize",
    "description": "Summarizes text using macOS' native summarization tool. Accepts sentence count as an optional parameter.",
    "value": "{{as:set numSentences to \"$2\" \n if length of numSentences is 0 then set numSentences to 2 \n set numSentences to (numSentences as number) \n summarize \"$3\" in numSentences}}",
    "example": "{{summarize sentences=1:There's a snake in my boot!}}",
    "hintRepresentation": "{{summarize:...}}"
  }
}

You could then load the placeholder from the file using {@link PLLoader.loadPlaceholdersFromJSONFile}:

// Make sure to adjust the path to the file as necessary
const customPlaceholders = await PLLoader.loadPlaceholdersFromFile("/Users/exampleUser/Downloads/custom_placeholders.json");

const text = "{{summarize sentences=1:Say hello to the Store. A home for Extensions published by our community of Developers using our API. Find extensions to the tools you use in your day-to-day.}}";

const result = await PLApplicator.bulkApply(text, { customPlaceholders: customPlaceholders });
console.log(result); // A home for Extensions published by our community of Developers using our API.

Note how we used $2 to refer to the second capture group in the regex. Using this syntax, you can refer to any capture group in the regex, thus allowing you to create placeholders that accept parameters even when using the JSON format.

Advanced Usage

The placeholders system is designed to be flexible and extensible. You can use custom application functions to create placeholders that do anything you want, based on any criteria you want. For example, you could create a placeholder that transforms text to uppercase:

const uppercaseTransform = PLCreator.newPlaceholder("upper", { regex: /{{upper:[\s\S]*}}/g, apply_fn: async (str, context) => {
  const match = str.match(/{{upper:([\s\S]*)}}/);
  const res = match?.[1]?.toUpperCase() ?? "";
  return { result: res };
} });

PLApplicator.bulkApply("Hello, {{upper:world}}", { customPlaceholders: [uppercaseTransform] }).then((result) => {
  console.log(result); // Hello, WORLD
});

You could also create a placeholder that outputs a Markdown table:

const markdownTablePlaceholder = PLCreator.newPlaceholder("mdtable", { regex: /{{mdtable:(([\s\S]*?(,[\s\S]*?)*?)\|)*?([\s\S]*?(,[\s\S]*?)*?)}}/g, apply_fn: async (str, context) => {
  const match = str.match(/{{mdtable:(([\s\S]*?(,[\s\S]*?)*?)\|)*?([\s\S]*?(,[\s\S]*?)*?)}}/);
  const rows = match?.[4]?.split("|");
  const tableLines = rows?.map((row, index) => {
    const cols = row?.split(",");
    if (index == 1) {
      return `| ${cols.map(() => "---").join(" | ")} |\n| ${cols.join(" | ")} |`;
    } else {
      return `| ${cols.join(" | ")} |`;
    }
  });
  return { result: tableLines?.join("\n") || "" };
} });

PLApplicator.bulkApply("{{mdtable:A,B,C,D|E,F,G,H|I,J,K,L|M,N,O,P|Q,R,S,T|U,V,W,X|Y,Z,,}}", { customPlaceholders: [markdownTablePlaceholder] }).then((result) => {
  console.log(result);
  /*
  | A | B | C | D |
  | --- | --- | --- | --- |
  | E | F | G | H |
  | I | J | K | L |
  | M | N | O | P |
  | Q | R | S | T |
  | U | V | W | X |
  | Y | Z |  |  |
  */
});
Regex Construction

You can specify regexes to match placeholders in a few different ways. You can specify a RegExp or a regex string directly, or you can use the methods of {@link PLMatcher} to construct a regex programmatically. For example, the following will all match the same URL placeholder:

const m1 = /{{url( raw=(true|false))?:https?:\/\/[a-zA-Z0-9.\-#=?]+?}}/g;
const m2 = new RegExp(`{{url( raw=(true|false))?:https?:\\/\\/[a-zA-Z0-9.\\-#=?]+?}}`, "g")
const m3 = PLMatcher.Braced(`url( raw=(true|false))?:(${PLMatcher.HTTPURL().source})`);
const m4 = PLMatcher.Container("url", [PLMatcher.RawParameter("raw", PLMatcher.Bool(), { optional: true })], PLMatcher.HTTPURL());

However, if we print out the source of each of these regexes, we see that there are some significant differences:

m1: /{{url( raw=(true|false))?:https?:\/\/[a-zA-Z0-9.\-#=?]+?}}/g

m2: /{{url( raw=(true|false))?:https?:\/\/[a-zA-Z0-9.\-#=?]+?}}/g

m3: /{{(url( raw=(true|false))?:((?:(?:(?:https?):)\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9][a-z0-9_-]{0,62})?[a-z0-9]\.)+(?:[a-z]{2,}\.?))(?::\d{2,5})?(?:[\/?#]\S*)?))}}/g

m4: /{{url(\s*?(raw=(true|false))?):((?:(?:(?:https?):)\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9][a-z0-9_-]{0,62})?[a-z0-9]\.)+(?:[a-z]{2,}\.?))(?::\d{2,5})?(?:[\/?#]\S*)?)}}/g

Although these regexes match mostly the same strings, the latter two aim to match the URL standard (i.e. RFC 3986) more closely. You might not need the extra precision for your use case, so it's up to you to decide which style to use.

Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.

License

This project is licensed under the MIT License.