# JavaScript environment JavaScript actions and module extensions run inside PopClip's JavaScript environment. This environment provides properties and functions that let your scripts interact with PopClip. Scripts run in a secure JavaScript sandbox that cannot access the filesystem. ## PopClip globals PopClip predefines several global objects and functions in the JavaScript environment for extensions to use. These are documented in detail in the [JavaScript API Reference](https://www.popclip.app/dev/api/). The following is a summary of the commonly needed parts. **Tip: Complete definitions in one file** The same API is defined in [**popclip.d.ts**](https://www.popclip.app/dev/popclip.d.ts), a single TypeScript definitions file. Use it for editor autocomplete, or hand it to an AI coding assistant. ### Global `popclip` object #### Readonly Properties Scripts can access the selected text and other input via properties of the [`popclip global`](https://www.popclip.app/dev/api/interfaces/PopClip.html). Commonly used properties are: - `popclip.input.text`: the full plain text selection - `popclip.input.matchedText`: the part of the text matching the requirement or regex - `popclip.input.regexResult`: if regex was specified, this is an array containing the full result of the match, including any capture groups - `popclip.input.html`: the html backing the selection (if `captureHtml` is set) - `popclip.input.markdown`: the markdownified html (if `captureHtml` is set) - `popclip.input.data.urls`: array of detected web URLs - `popclip.context.browserUrl`, `popclip.context.browserTitle`: browser page URL and title, if available - `popclip.context.appName`, `popclip.context.appIdentifier`: app name and bundle identifier - `popclip.modifiers.command`, `popclip.modifiers.option`, `popclip.modifiers.shift`, `popclip.modifiers.control`: booleans for modifier keys pressed - `popclip.options`: an object with properties for each option, where the property name is the option's identifier. Option values can be either strings or booleans #### Methods Scripts can perform actions via calling methods on the [`popclip`](https://www.popclip.app/dev/api/interfaces/PopClip.html) global: - [`popclip.pasteText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#pastetext): paste a given string (similar to `paste-result`) - [`popclip.copyText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#copytext): copy a string to the clipboard (similar to `copy-result`) - [`popclip.showText()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showtext): show a string in the PopClip bar (similar to `show-result`) - [`popclip.openUrl()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#openurl): open a URL (similar to a URL action) - [`popclip.pressKey()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskey): presses a key combo (similar to a key press extension) - [`popclip.pressKeys()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#presskeys): presses a sequence of key combos, with optional waits between them - [`popclip.runAppleScript()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescript), [`popclip.runAppleScriptFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runapplescriptfile): run an AppleScript, from source text or from a file in the extension package (requires the `script` entitlement) - [`popclip.performCommand()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#performcommand): perform a cut, copy or paste command in the foreground app (simlar to the `before` and `after` steps) - [`popclip.runShortcut()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#runshortcut): run a macOS Shortcut by name (similar to a [Shortcut action](https://www.popclip.app/dev/shortcut-actions.md)) - [`popclip.revealFile()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#revealfile): show a file or folder in the Finder - [`popclip.showSuccess()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showsuccess), [`popclip.showFailure()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showfailure), [`popclip.showSettings()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#showsettings): show a check mark, shaking-X, or Pop up the extension's settings - [`popclip.signInRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#signinrequirederror), [`popclip.settingsRequiredError()`](https://www.popclip.app/dev/api/interfaces/PopClip.html#settingsrequirederror): construct errors that the action can throw to indicate that the user needs to sign in or adjust the extension's settings ### Global `util` object Where the methods on `popclip` _do_ something, the functions on the [`util`](https://www.popclip.app/dev/api/interfaces/Util.html) global are passive. They include general helpers — randomization, encoding, hashing, locale and time zone information, and macOS dictionary and spelling lookups. Unlike the methods on `popclip`, these can be called from a [population function](https://www.popclip.app/dev/js-modules.md#population-function) — see [Restrictions during population](https://www.popclip.app/dev/js-modules.md#restrictions-during-population). ### Global `pasteboard` object Scripts can also have direct read/write access the macOS clipboard via the [`pasteboard`](https://www.popclip.app/dev/api/interfaces/Pasteboard.html) global: - `pasteboard.text` - the current plain text content of the clipboard, a read/write property. ### Global `print()` function There is a global function [`print()`](https://www.popclip.app/dev/api/functions/print.html) for debug output. You can [view the debug output in the Console.app](https://www.popclip.app/dev/index.md#debug-output) and also in the [test harness](#test-harness). ## Language version and libraries PopClip's JavaScript engine is Apple's [JavaScriptCore](https://developer.apple.com/documentation/javascriptcore), which is part of macOS. Language features will vary depending on the macOS version PopClip is running on. However, you can assume availability of language features up to at least ES2023 on all macOS versions that PopClip supports (macOS 13 and later). **Tip: JavaScript reference** The website I use and recommend to learn about the JavaScript language, the Standard Library and other APIs, is [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference). ### Standard built-in objects For the [Standard Library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects), PopClip supplements the built-in JavaScript objects provided by macOS with polyfills from [core-js](https://github.com/zloirock/core-js). This means that you can use the latest features up to ES2023 on all macOS versions. ### Web APIs and Node globals PopClip provides a limited subset of the standard [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) that are normally available in a browser environment: - [URL](https://www.popclip.app/dev/api/classes/URL.html) and [URLSearchParams](https://www.popclip.app/dev/api/classes/URLSearchParams.html) - [XMLHttpRequest](https://www.popclip.app/dev/api/classes/XMLHttpRequest.html) - [setTimeout](https://www.popclip.app/dev/api/functions/setTimeout.html) and [clearTimeout](https://www.popclip.app/dev/api/functions/clearTimeout.html) - [setInterval](https://www.popclip.app/dev/api/functions/setInterval.html) and [clearInterval](https://www.popclip.app/dev/api/functions/clearInterval.html) - [structuredClone](https://www.popclip.app/dev/api/functions/structuredClone.html) Additionally, from the Node.js environment: - [Buffer](https://www.popclip.app/dev/api/classes/Buffer.html) Some further globals are present only as compatibility shims to support the bundled modules: `Blob`, `TextEncoder`, `atob` and `btoa`. These are reduced implementations, not recommended for direct use — prefer `Buffer` and the `util` encoding functions. ### Bundled libraries Some libraries from [NPM](https://www.npmjs.org/) are bundled within the PopClip app itself, and are available to load by scripts. These are: | Library | Version | Description | | ---------------------------- | ------- | ---------------------------------------- | | `axios` | 1.12.2 | HTTP client | | `buffer` | 6.0.3 | Node-compatible `Buffer` implementation | | `case-anything` | 2.1.13 | Case conversion utilities | | `content-type` | 1.0.5 | Parse HTTP `Content-Type` headers | | `dom-serializer` | 2.0.0 | Serialize DOM nodes to HTML | | `emoji-regex` | 10.6.0 | Regular expression matching emojis | | `entities` | 7.0.0 | HTML entity encoder/decoder | | `fast-json-stable-stringify` | 2.1.0 | Deterministic JSON stringify | | `fast-plist` | 0.1.3 | Parse and serialize macOS property lists | | `htmlparser2` | 10.0.0 | HTML parser | | `js-yaml` | 4.1.0 | YAML parser | | `linkedom` | 0.18.12 | Lightweight DOM implementation | | `linkifyjs` | 4.3.3 | Detect and linkify URLs in text | | `oauth-1.0a` | 2.2.6 | OAuth 1.0a signing helpers | | `rot13-cipher` | 1.0.0 | ROT13 encoder/decoder | | `sanitize-html` | 2.17.0 | HTML sanitizer | | `sucrase` | 3.35.1 | Fast TypeScript/JS transformer | | `turndown` | 7.2.1 | HTML to Markdown converter | | `valibot` | 1.1.0 | Validation and parsing library | Library modules are imported by name — see below. ## Importing other modules {#using-require} A script can import the [bundled libraries](#bundled-libraries), and other files from the extension package, using `import` syntax: ```javascript import axios from "axios"; // a bundled library import { helper } from "./helper.js"; // another file in the package import strings from "./data/strings.json"; // JSON parses to an object ``` Equivalently, you can call the `require()` function — `import` statements are converted to `require()` calls under the hood: ```javascript const axios = require("axios"); ``` ### Module resolution The module specifier string is interpreted as follows: - If it starts with `./` or `../`, it is a path to a file in the package directory, relative to the current file. - Otherwise, it is tried as a path relative to the root of the package directory; if no file is found there, it is then matched against the names of the [bundled libraries](#bundled-libraries). Paths beginning with `/`, or using `..` to go up outside the package directory, are not valid. The imported value is the module's exported value, or the parsed JSON object. Results are cached: importing the same specifier again returns the same instance. If nothing is found, or the path is invalid, the value is `undefined`. ### Supported file types The module loader can load the following file types: | File extension | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.js` | A JavaScript module, in [ES module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) or [CommonJS](https://www.typescriptlang.org/docs/handbook/2/modules.html#commonjs-syntax) format. | | `.ts` | A TypeScript module, likewise in either format. | | `.json` | A JSON file parsed into a JavaScript object. | If no file name extension is specified, PopClip will try `.js`, `.ts`, `.json` in order. ## Asynchronous operations and async/await Asynchronous operations are fully supported: your functions can be `async`, and you can use the `await` keyword when calling any function that returns a Promise. If a script starts asynchronous work — a network request, a timer — PopClip shows its spinner and waits until the last operation has finished. Clicking the spinner cancels all current operations. The action's result is always the script's own return value; values produced inside callbacks or timers do not become the result. As a convenience, PopClip supplies a global function `sleep()`, a promise-based wrapper around `setTimeout()`: ```javascript // #popclip // name: Await Test await sleep(5000); // 5 second delay popclip.showText("Boo!"); ``` ## Network access from JavaScript **Warning: Entitlement needed** To use XHR, the `network` entitlement must be present in the `entitlements` array in the extension's config. PopClip provides its own implementation of [`XMLHttpRequest`](https://www.popclip.app/dev/api/classes/XMLHttpRequest.html) (XHR). This is the only way for JavaScript code to access the network. PopClip is also bundled with the HTTP library [axios](https://axios-http.com/), which is an easier to use wrapper around XHR. Due to macOS's App Transport Security, requests to a named host must use `https:` — plain `http:` URLs throw a network error. The exception is that `http:` works for `localhost` and for numeric IP addresses, which is handy for talking to a server on the local machine or network. Here's an example extension snippet that downloads a selected URL's contents, and copies it to the clipboard: ```javascript // #popclip // name: Download Text // icon: symbol:square.and.arrow.down.fill // requirements: [url] // entitlements: [network] // after: copy-result import axios from "axios"; const response = await axios.get(popclip.input.data.urls[0]); /* note: there is no particular need to check the return status here. axios calls will throw an error if the HTTP status is not 200/2xx. */ return response.data; ``` For a more substantial axios example, see for example [Instant Translate](https://github.com/pilotmoon/PopClip-Extensions/tree/master/source/InstantTranslate.popclipext). ## TypeScript support PopClip has built-in support for [TypeScript](https://www.typescriptlang.org/). You can supply TypeScript source in any place where a JavaScript file can be specified. PopClip loads files with a `.js` extension as raw JavaScript, and loads files with a `.ts` extension as TypeScript. At load time, PopClip transpiles TypeScript files into JavaScript source. PopClip does not do any type validation on the TypeScript source. ### TypeScript configuration When working with TypeScript files you'll want to provide a [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) file. For my current recommended `compilerOptions`, see the one in the PopClip-Extensions repo: - [Example tsconfig.json for PopClip extensions](https://github.com/pilotmoon/PopClip-Extensions/blob/master/tsconfig.json) ### PopClip types package I have published the NPM package [`@popclip/types`](https://www.npmjs.com/package/@popclip/types), a TypeScript type definitions package to assist in developing extensions. This will enable autocomplete and type-checking in TypeScript-aware editors. Use an NPM-compatible JavaScript package manager to install both `typescript` itself and the types package in the directory where you are writing your extension code: ```bash npm install -D typescript @popclip/types ``` And then, in your `tsconfig.json` file, add an explicit reference to the types: ```json { "compilerOptions": { "types": ["@popclip/types"] } } ``` Once this is done, you should get autocomplete and type-checking in your editor and TypeScript's `tsc` will check your code for type errors: ```bash npx tsc --noEmit ``` ## Test Harness PopClip has a command-line mode that loads a JavaScript file into the PopClip environment and runs it. Optionally, if the file is a module, it can then call one of the module's exported functions. It is useful for running tests of your code in PopClip's environment, with the same libraries, globals etc. The test harness is activated by calling PopClip's executable (inside the PopClip.app package) with the parameter `run` followed by the filename to load and an optional function name to call. For example: ```bash /Applications/PopClip.app/Contents/MacOS/PopClip run myfile.js myfunc ``` If a function name is supplied, it will be called with no parameters. If the function is an `async` function or returns a `Promise`, the test harness will wait for the function to complete before exiting. If the function completes successfully, the return value of the function is printed to the console. The shell exit status will be: - 0 if the scipt loads and runs without error and the called function (if any) completes normally; - 1 if an error occurs (e.g. file not found, syntax error), or if the function throws an exception. Some notes: - Scripts can output strings with the global `print()` function (not `console.log()`). - When running in the test harness, the `popclip` object's properties will return blank data. Its methods can be called but some will not have any effect. - Scripts running in the test harness always have the network access entitlement. - The test harness is a somewhat experimental feature at present. Please reach out to me if something does not seem to work as expected. ### Example 'foo.ts': ```typescript print("file loading now"); function sayHi(x: string) { print(`hello ${x}`); } export async function test() { sayHi("there"); await sleep(500); sayHi("again"); return "that's all folks"; } ``` Test harness output: ![](https://www.popclip.app/dev/media/shot-harness-2.png "Example Test Harness output.")