Skip to content

Commit

Permalink
Add PLChecker
Browse files Browse the repository at this point in the history
  • Loading branch information
SKaplanOfficial committed Nov 15, 2023
1 parent 99296b7 commit 4ab1fab
Show file tree
Hide file tree
Showing 15 changed files with 411 additions and 64 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Placeholders Toolkit Changelog

## [0.1.3] - 2023-11-15?

- Re-added the `{{selectedFileContents}}` (or just `{{contents}}`) placeholder.
- Deprecated `PLApplicator.checkForPlaceholders()` in favor of `PLChecker.checkForPlaceholders()`.
- Added `PLChecker.checkForPlaceholdersInRange()` and `PLChecker.getPlaceholderRanges()`.
- Fixed bug where `{{fileNames}}` would only return the last file name.

## [0.1.2] - 2023-11-11

- Removed debug console logs.

## [0.1.1] - 2023-11-10

- Added syntax examples for each placeholder.
- Added support for passing functions instead of strings when using JavaScript placeholders, e.g. `{{js:dialog(() => askAI("What is the capital of Canada?"))}}.
- Added `{{chooseFile}}`, `{{chooseFolder}}`, and `{{chooseApplication}}` placeholders.
- Added `{{write to="[path]":...}}` placeholder.
- General documentation improvements.

## [0.1.0] - 2023-11-07

- First generally stable release.
2 changes: 2 additions & 0 deletions lib/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export const applyToObjectValuesWithKeys = async (

/**
* Gets a list of placeholders that are included in a string.
* @deprecated Use {@link PLChecker.checkForPlaceholders} instead.
*
* @param str The string to check.
* @param options The options for applying placeholders.
* @param options.customPlaceholders The list of custom (user-defined) placeholders. Provide this if you have a separate list of custom placeholders.
Expand Down
141 changes: 141 additions & 0 deletions lib/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { DefaultPlaceholders } from "./defaultPlaceholders";
import { PLRange, Placeholder } from "./types";

/**
* Gets a list of placeholders that are included in a string.
* @param str The string to check.
* @param options The options for applying placeholders.
* @param options.customPlaceholders The list of custom (user-defined) placeholders. Provide this if you have a separate list of custom placeholders.
* @param options.defaultPlaceholders The list of default placeholders. Provide this if you have customized the order of default placeholders or added additional defaults.
* @param options.allPlaceholders The list of all placeholders (custom and default). Provide this if you have a single list of all placeholders.
* @param options.strict Whether to only include placeholders that are surrounded by double curly braces and match the placeholder regex. Defaults to false (will include placeholders that are not surrounded by double curly braces).
* @returns The list of {@link Placeholder} objects.
*/
export const checkForPlaceholders = async (
str: string,
options?: {
customPlaceholders?: Placeholder[];
defaultPlaceholders?: Placeholder[];
allPlaceholders?: Placeholder[];
strict?: boolean;
}
): Promise<Placeholder[]> => {
const sortedPlaceholders = options?.allPlaceholders
? options.allPlaceholders
: [
...(options?.customPlaceholders || []),
...(options?.defaultPlaceholders
? options.defaultPlaceholders
: Object.values(DefaultPlaceholders)),
];

const includedPlaceholders = sortedPlaceholders
.filter((placeholder) => {
return (
str.match(placeholder.regex) != undefined ||
(!options?.strict &&
(str.match(
new RegExp(
"(^| )" +
placeholder.regex.source.replace("{{", "").replace("}}", ""),
"g"
)
) != undefined ||
str.match(
new RegExp(
`(^| |:|})({?{?)${placeholder.name.replace(
/[!#+-]/g,
"\\$1"
)}(}?}?)`,
"g"
)
) != undefined))
);
})
.sort((placeholderA, placeholderB) => {
// Order definitive occurrences first
if (str.match(placeholderA.regex)) {
return -1;
} else if (str.match(placeholderB.regex)) {
return 1;
} else {
return 0;
}
});
return includedPlaceholders;
};

/**
* Gets a list of the ranges of placeholders that are included in a string.
* @param str The string to check.
* @param options The options for applying placeholders. Optional.
* @param options.customPlaceholders The list of custom (user-defined) placeholders. Provide this if you have a separate list of custom placeholders.
* @param options.defaultPlaceholders The list of default placeholders. Provide this if you have customized the order of default placeholders or added additional defaults.
* @param options.allPlaceholders The list of all placeholders (custom and default). Provide this if you have a single list of all placeholders.
* @param options.strict Whether to only include placeholders that are surrounded by double curly braces and match the placeholder regex. Defaults to false (will include placeholders that are not surrounded by double curly braces).
* @returns The list of placeholders and their ranges.
*/
export const getPlaceholderRanges = async (
str: string,
options?: {
customPlaceholders?: Placeholder[];
defaultPlaceholders?: Placeholder[];
allPlaceholders?: Placeholder[];
strict?: boolean;
}
): Promise<
{
placeholder: Placeholder;
range: PLRange;
}[]
> => {
const includedPlaceholders = await checkForPlaceholders(str, options);
const ranges = includedPlaceholders.map((placeholder) => {
const match = str.match(new RegExp(placeholder.regex.source));
if (match?.index) {
return {
placeholder,
range: {
startIndex: match.index,
endIndex: match.index + match[0].length,
},
};
}
});
return ranges.filter((range) => range != undefined) as {
placeholder: Placeholder;
range: PLRange;
}[];
};

/**
* Checks if a string contains placeholders in a given range.
* @param str The string to check.
* @param range The range to check.
* @param options The options for applying placeholders. Optional.
* @param options.customPlaceholders The list of custom (user-defined) placeholders. Provide this if you have a separate list of custom placeholders.
* @param options.defaultPlaceholders The list of default placeholders. Provide this if you have customized the order of default placeholders or added additional defaults.
* @param options.allPlaceholders The list of all placeholders (custom and default). Provide this if you have a single list of all placeholders.
* @param options.strict Whether to only include placeholders that are surrounded by double curly braces and match the placeholder regex. Defaults to false (will include placeholders that are not surrounded by double curly braces).
* @returns The list of placeholders contained within the range.
*/
export const checkForPlaceholdersInRange = async (
str: string,
range: PLRange,
options?: {
customPlaceholders?: Placeholder[];
defaultPlaceholders?: Placeholder[];
allPlaceholders?: Placeholder[];
strict?: boolean;
}
): Promise<Placeholder[]> => {
const substr = str.substring(range.startIndex, range.endIndex);
const includedPlaceholders = await checkForPlaceholders(substr, options);
const ranges = includedPlaceholders.map((placeholder) => {
const match = str.match(new RegExp(placeholder.regex.source));
if (match?.index) {
return placeholder
}
});
return ranges.filter((range) => range != undefined) as Placeholder[];
}
2 changes: 2 additions & 0 deletions lib/defaultPlaceholders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import FileMetadataPlaceholder from "./info-placeholders/metadata";
import FileNamesPlaceholder from "./info-placeholders/fileNames";
import SelectedFilesPlaceholder from "./info-placeholders/selectedFiles";
import SelectedTextPlaceholder from "./info-placeholders/selectedText";
import SelectedFileContentsPlaceholder from "./info-placeholders/selectedFileContents";
import { runJSInActiveTab } from "./utils";
import { Placeholder } from "./types";

Expand Down Expand Up @@ -164,6 +165,7 @@ const defaultPlaceholders = {
ClipboardTextPlaceholder,
SelectedTextPlaceholder,
SelectedFilesPlaceholder,
SelectedFileContentsPlaceholder,
FileNamesPlaceholder,
FileMetadataPlaceholder,
CurrentAppNamePlaceholder,
Expand Down
6 changes: 6 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export * as Placeholders from "./placeholders";
*/
export * as PLApplicator from "./apply";

/**
* Functions for checking if a string contains placeholders.
*/
export * as PLChecker from "./check";

/**
* Functions for loading placeholders from files or strings.
*/
Expand Down Expand Up @@ -50,6 +55,7 @@ export {
CustomPlaceholder,
PlaceholderCategory,
PlaceholderType,
PLRange,
} from "./types";

/**
Expand Down
13 changes: 5 additions & 8 deletions lib/info-placeholders/fileNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@ const FileNamesPlaceholder: Placeholder = {
apply: async (str: string, context?: { [key: string]: unknown }) => {
const files =
context && "selectedFiles" in context
? (context["selectedFiles"] as string)
: (await getSelectedFiles()).csv;
? (context["selectedFiles"] as string[])
: (await getSelectedFiles()).paths;
if (files.length == 0)
return { result: "", fileNames: "", selectedFiles: "" };
const fileNames = files
.split(", ")
.map((file) => file.split("/").pop())
.join(", ");
return { result: fileNames, fileNames: fileNames, selectedFiles: files };
return { result: "", fileNames: "", selectedFiles: [] };
const fileNames = files.map((file) => file.split("/").pop())
return { result: fileNames.join(", "), fileNames: fileNames, selectedFiles: files };
},
result_keys: ["fileNames", "selectedFiles"],
constant: true,
Expand Down
12 changes: 5 additions & 7 deletions lib/info-placeholders/selectedFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,22 @@ import { Placeholder, PlaceholderCategory, PlaceholderType } from "../types";

/**
* Placeholder for the paths of the currently selected files in Finder as a comma-separated list. If no files are selected, this will be replaced with an empty string.
*
*
* Syntax: `{{selectedFiles}}` or `{{selectedFile}}` or `{{files}}`
*/
const SelectedFilesPlaceholder: Placeholder = {
name: "selectedFiles",
regex: /{{(selectedFiles|selectedFile|files)}}/g,
rules: [RequireValue(async () => (await getSelectedFiles()).paths)],
apply: async (str: string, context?: { [key: string]: unknown }) => {
if (!context || !("selectedFiles" in context))
return { result: "", selectedFiles: "" };
try {
const files =
context && "selectedFiles" in context
? (context["selectedFiles"] as string)
: (await getSelectedFiles()).csv;
return { result: files, selectedFiles: files };
? (context["selectedFiles"] as string[])
: (await getSelectedFiles()).paths;
return { result: files.join(", "), selectedFiles: files };
} catch (e) {
return { result: "", selectedFiles: "" };
return { result: "", selectedFiles: [] };
}
},
result_keys: ["selectedFiles"],
Expand Down
2 changes: 2 additions & 0 deletions lib/placeholders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import FileMetadataPlaceholder from "./info-placeholders/metadata";
import FileNamesPlaceholder from "./info-placeholders/fileNames";
import SelectedFilesPlaceholder from "./info-placeholders/selectedFiles";
import SelectedTextPlaceholder from "./info-placeholders/selectedText";
import SelectedFileContentsPlaceholder from "./info-placeholders/selectedFileContents";

export {
GetPersistentVariablePlaceholder,
Expand All @@ -102,6 +103,7 @@ export {
ClipboardTextPlaceholder,
SelectedTextPlaceholder,
SelectedFilesPlaceholder,
SelectedFileContentsPlaceholder,
FileNamesPlaceholder,
FileMetadataPlaceholder,
CurrentAppNamePlaceholder,
Expand Down
15 changes: 15 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,18 @@ export type ExtensionCommand = {
*/
deeplink: string;
};

/**
* A placeholder range.
*/
export type PLRange = {
/**
* The start index of the placeholder in a string, inclusive.
*/
startIndex: number;

/**
* The end index of the placeholder in a string, non-inclusive.
*/
endIndex: number;
}
1 change: 0 additions & 1 deletion lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Extension, ExtensionCommand, JSONObject } from "./types";
import fetch from "node-fetch";
import { environment, getFrontmostApplication } from "@raycast/api";
import * as fs from "fs";
import {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "placeholders-toolkit",
"version": "0.1.2",
"version": "0.1.3",
"description": "A placeholders system for supplying dynamic content to Raycast extensions.",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
Expand Down
62 changes: 62 additions & 0 deletions tests/PLChecker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
jest.mock("node-fetch", () => ({ fetch: console.log("mocked fetch") }));
jest.mock("@raycast/utils", () => ({
runAppleScript: (script: string) => execScript(script, []).data,
}));

import { execScript } from "../lib/scripts";
import { PLChecker } from "../lib";

describe("Placeholder Checker Tests", () => {
it("should detect 3 distinct placeholders", async () => {
const str = "Hello, I am {{user}}. I am {{computerName}}. Today is {{day}}.";
const placeholders = await PLChecker.checkForPlaceholders(str);
expect(placeholders.length).toBe(3);
});

it("should detect 3 distinct placeholders not surrounded by double curly braces when not using strict mode", async () => {
const str = "Hello, I am user. I am computerName. Today is day.";
const placeholders = await PLChecker.checkForPlaceholders(str, {
strict: false,
});
expect(placeholders.length).toBe(3);
});

it("should not detect incomplete placeholders when using strict mode", async () => {
const str = "Hello, I am user. I am computerName. Today is day.";
const placeholders = await PLChecker.checkForPlaceholders(str, {
strict: true,
});
expect(placeholders.length).toBe(0);
});

it("should correctly identify the range of placeholders", async () => {
const str = "Hello, I am {{user}}. I am {{computerName}}. Today is {{day}}.";
const ranges = await PLChecker.getPlaceholderRanges(str);
expect(ranges.length).toBe(3);
expect(ranges[0].range.startIndex).toBe(54);
expect(ranges[0].range.endIndex).toBe(61);
expect(ranges[1].range.startIndex).toBe(27);
expect(ranges[1].range.endIndex).toBe(43);
expect(ranges[2].range.startIndex).toBe(12);
expect(ranges[2].range.endIndex).toBe(20);
});

it("should correctly identify placeholders in a given range", async () => {
const str = "Hello, I am {{user}}. I am {{computerName}}. Today is {{day}}.";
const ranges = await PLChecker.getPlaceholderRanges(str);
const placeholders = await PLChecker.checkForPlaceholdersInRange(
str,
ranges[0].range
);
expect(placeholders.length).toBe(1);
expect(placeholders[0].name).toBe("day");

const customRange = { startIndex: 26, endIndex: 45 };
const morePlaceholders = await PLChecker.checkForPlaceholdersInRange(
str,
customRange
);
expect(morePlaceholders.length).toBe(1);
expect(morePlaceholders[0].name).toBe("computerName");
});
});
Loading

0 comments on commit 4ab1fab

Please sign in to comment.