PodoLay Plugin API Reference (Plugin API 1.9)
A single reference for everything available to a plugin's
main.js. If you're new, start with README.md (getting started, development workflow, review criteria). It matches the type definitions inpodolay-plugin.d.tsone to one — add/// <reference path="./podolay-plugin.d.ts" />in your code editor to get autocompletion.
Notation
| Marker | Meaning |
|---|---|
| 1.0 | Since API 1.0 — available to every plugin |
| 1.1 | Since API 1.1 — only for plugins whose manifest declares "apiVersion": "1.1" (an error under 1.0) |
| 1.2 | Since API 1.2 — only for plugins whose manifest declares "apiVersion": "1.2" |
| 1.3 | Since API 1.3 — only for plugins whose manifest declares "apiVersion": "1.3" (editor 2.13 or later) |
| 1.4 | Since API 1.4 — only for plugins whose manifest declares "apiVersion": "1.4" (editor 2.14.7 or later) |
| 1.5 | Since API 1.5 — only for plugins whose manifest declares "apiVersion": "1.5" (editor 2.14.12 or later) |
| 1.6 | Since API 1.6 — only for plugins whose manifest declares "apiVersion": "1.6" (editor 2.14.12 or later) |
| 1.7 | Since API 1.7 — only for plugins whose manifest declares "apiVersion": "1.7" (editor 2.16.2 or later) |
| 1.8 | Since API 1.8 — only for plugins whose manifest declares "apiVersion": "1.8" (editor 2.17.2 or later) |
| 1.9 | Since API 1.9 — only for plugins whose manifest declares "apiVersion": "1.9" (editor 2.18.0 or later) |
🔑 document:read | Requires that permission in the manifest's permissions (otherwise the call fails with 권한이 없습니다, "permission missing") |
| 🔓 | No permission required |
→ T | Returns a Promise<T> — every call that talks to the editor is asynchronous |
Contents
- Runtime environment
- Permissions
- Manifest
podolay-plugin.json podolayat a glance- Definitions —
graphics·tools·commands - Events —
podolay.on - Reading the document —
podolay.doc·podolay.text - Selection —
podolay.selection - Writing to the document —
podolay.editandtx - Batches —
podolay.batch - UI —
podolay.ui·podolayUIin panels - Storage and license —
podolay.storage·podolay.license - Uploading images —
podolay.assets - Package files —
podolay.package - Network —
fetch·WebSocket·EventSource - Data types
- Display list
- Limits at a glance
- Errors and timeouts
- Changes between versions
1. Runtime environment
| Item | Details |
|---|---|
| Where it runs | In a Web Worker inside a sandboxed iframe, isolated from the editor. It cannot reach the editor's page (DOM), sign-in details or other documents |
| What's available | The global podolay, worker APIs (self, setTimeout, OffscreenCanvas, TextEncoder, crypto.getRandomValues, crypto.randomUUID, …), crypto.subtle with digest only (SHA-1/256/384/512, from editor 2.14.14 — the worker isn't a secure context, so the editor computes it), and console.* (goes to the log window) |
| What's not | document and window (use panels for UI), XMLHttpRequest, importScripts, Worker (never), fetch (without a network permission) |
| Calling convention | Every call that talks to the editor returns a Promise. On failure it rejects with an Error (e.message explains why) |
| Units · coordinates | All lengths are in pt (1/72 inch). Object frames use spread coordinates (origin at the spread's top-left). Display lists use object-local coordinates |
| Page numbers | Zero-based — the same numbers as spreads[].pages[].index in doc.info() |
| When it starts | When the editor opens, it loads the installed, enabled plugins and runs each main.js once. Plugins don't run in observer, guest or reader mode, or in public windows |
| If it hangs | If a call isn't answered within 8 seconds and the plugin also fails to answer a liveness check, the editor turns the plugin off (§19) |
An error thrown at the top level of main.js is a "start failure" and turns the plugin off. Syntax errors are also written to the log window together with main.js:line.
main.js is loaded under the name <plugin id>/<main path> (e.g. com.example.qr/main.js), so the browser developer tools' Sources panel, error stacks and the log window show that name and the real line numbers. You can set breakpoints or pause with debugger; (§19).
2. Permissions
Declare them in the manifest's permissions. They are shown verbatim to the person installing the plugin, so declare only what you use (a review criterion).
| Permission | What it unlocks |
|---|---|
document:read | doc.info · doc.graphics · doc.graphic · doc.raw · doc.styles · doc.text · doc.find · doc.textStatus · doc.inlines · doc.texts · doc.table · assets.list · text.measure |
document:write | edit (all tx.*) · batch · assets.upload |
selection | selection.get · selection.set · selection.setText · text.selection · the selectionChange and textSelectionChange events |
ui | ui.showPanel (manifest contributes.panels) |
storage | storage.get · storage.set |
network:<host> | fetch, WebSocket and EventSource to that host (§15). privacyUrl is required |
| Without any permission | Defining graphics, tools and commands, on (except selectionChange and textSelectionChange), doc.getData, ui.toast · ui.closePanel · ui.postMessage · ui.onMessage · ui.progress, package.read · text · json, ui.pickFiles |
If a new version asks for more permissions, the editor keeps using the previous version until the user consents again in the portal.
3. Manifest podolay-plugin.json
3.1 Top-level fields
| Field | Required | Format | Description |
|---|---|---|---|
id | ✔ | Reverse domain com.example.name | Lowercase letters, digits and -, at least two dot-separated parts, up to 100 characters. It belongs to the developer who uploads it first |
name | ✔ | Localized string | Up to 100 characters (server) |
description | Localized string | Up to 2000 characters | |
version | ✔ | semver 1.2.0 | Every version is reviewed anew |
apiVersion | ✔ | "1.0" – "1.9" | The API version you use. Declare "1.1" – "1.9" to use that version's features. The editor won't load a version it doesn't know |
minEditor | semver | Load only in this editor version or later | |
main | ✔ | Path inside the package | The script to run in the worker |
icon | Image inside the package (svg, png, jpg, webp) | Store icon | |
permissions | Array of strings (up to 30) | §2 | |
privacyUrl | △ | https URL (up to 500 characters) | Required if you request any network permission |
contributes | Object | §3.2–§3.6 below | |
help | Object | Guide and samples — §3.8 | |
dataFrom | Array of plugin ids (up to 10) | 1.8 Your other plugins whose data this one may read — §3.9 |
3.2 contributes.graphics — graphic types (up to 50)
| Field | Required | Format | Description |
|---|---|---|---|
type | ✔ | Local name | The same name as in graphics.define(type, …) |
name | ✔ | Localized string | Inspector title, etc. |
usesFillStroke | true | Uses the frame's fill and stroke — the Inspector shows fill/stroke rows (the frame fill goes behind the drawing, the stroke in front) | |
defaultSize | [w, h] (0 < pt < 10000) | The size when the user just clicks with the tool (100×100 if omitted) | |
inspector | Array of fields (up to 50) | Controls shown in the Inspector when the object is selected | |
autoSize | true 1.3 | The data decides the size — when the editor changes the data it fits the frame with size |
3.3 Inspector fields
Changing a control updates that object's data[key], and the editor calls render again (one Undo step).
| Field | Required | Format | Description |
|---|---|---|---|
key | ✔ | Local name | Key in data |
label | ✔ | Localized string | |
type | ✔ | number · text · checkbox · select · color | |
min · max · step | Number | Range and arrow-key step of a number | |
unit | 'pt' · '%' · '' | Unit shown after a number | |
options | △ | [{ value: string or number, label: localized string }] (up to 100) | Required for select |
live | false 1.3 | number only — commit once after the control settles (0.3 s) instead of on every arrow/stepper click (one Undo step) | |
hideInline | true 1.3 | Hidden when the object is selected as an inline object (controls like alignment or size mode that mean nothing inside text) |
The value of a color field uses the color format.
3.4 contributes.tools — toolbar buttons (up to 10)
| Field | Required | Format | Description |
|---|---|---|---|
id | ✔ | Local name | The same name as in tools.define(id, …) (for custom) |
label | ✔ | Localized string | Tooltip |
icon | svg or png inside the package | A puzzle icon if omitted | |
kind | ✔ | createGraphic · custom | createGraphic = drag to create a graphicType object, like the rectangle tool (handled by the editor) · custom = mouse events are sent to the plugin |
graphicType | △ | A type from contributes.graphics | Required for createGraphic |
3.5 contributes.menus · contributes.keybindings (up to 50 each)
"menus": [{ "command": "number", "label": { "en": "Number objects", "ko": "번호 매기기" } }],
"keybindings": [{ "command": "number", "key": "mod+alt+n" }, { "command": "ipa", "key": "ctrl+alt+c" }]- Menu items go under the editor header's Plugins (puzzle) menu › plugin name › item. Choosing an item calls
commands.define(command, …). - 1.2 Context menus —
contextchooses where the item appears (a single value or an array;"header"if omitted). In a context menu, only the item label appears, below a separator at the bottom."menus": [ { "command": "makeQr", "label": "Convert to QR code", "context": "object", "when": { "kind": ["rectangle", "image"], "single": true } }, { "command": "regen", "label": "Redraw", "context": ["object", "header"], "when": { "type": ["qr"] } }, { "command": "lookup", "label": "Look up selected text", "context": "text" }, { "command": "stamp", "label": "Stamp here", "context": "canvas" } ]
context | Where it appears | Command argument (§5.3) |
|---|---|---|
header | The Plugins menu in the header (default) | None — 1.4 the text argument while text is being edited |
object | Object context menu — only when when matches | { context: 'object', ids, spread, point, inline? } |
text | Context menu while editing text (text frames, 1.4 table cells) | { context: 'text', id, selection: { start, end, text } } |
canvas | Context menu on an empty area | { context: 'canvas', spread, page, point } |
when (object menus only): kind = every selected object is one of these kinds (Graphic.kind), type = every one is this plugin's graphic of this type, single: true = only when exactly one object is selected. If omitted, the item appears whenever at least one object is selected. Items never appear in the menus shown while editing a table, a table cell or a footnote. They do appear in the menu of a selected inline object: ids is then a single stand-in object that exists only while it is selected (read and change it with doc.graphic, tx.set, setData and setPluginData, and the inline object changes; it cannot be removed or moved), and inline: { id, index } gives the story (text frame id) and the index used by doc.inlines and tx.removeInline.
- Shortcut
key=[mod+][ctrl+][alt+][shift+]<key>, where<key>isa–z,0–9orf1–f12.mod= ⌘ on macOS, Ctrl elsewhere. 1.4ctrl= ⌃ (Control) on macOS, the same Ctrl asmodelsewhere — for Mac combinations without ⌘ such as ⌃⌥C.ctrlneedsapiVersion1.4 or later (otherwise the manifest is invalid). Letter and digit keys needmod,ctrloralt(so they don't swallow typing). A shortcut that clashes with one the editor already uses is not bound, and a warning is logged (outside macOSctrlcounts as the same key asmod). - 1.4 Plugin shortcuts are checked before the editor's built-in key handling and also while text is being edited (before the text engine) — commands of 1.4 plugins then get the
textargument (§5.3). Shortcuts of plugins at 1.3 or lower are not called while text is being edited.
3.6 contributes.panels — floating panel windows (up to 10)
| Field | Required | Format | Description |
|---|---|---|---|
id | ✔ | Local name | ui.showPanel(id) |
title | ✔ | Localized string | Window title (plugin name — title) |
html | ✔ | html inside the package | §11.2 |
width · height | 120–1200 px | Default 280×360 |
3.7 Example
{
"id": "com.example.qr",
"name": { "en": "QR Code", "ko": "QR 코드" },
"version": "1.0.0",
"apiVersion": "1.1",
"main": "main.js",
"permissions": ["document:read", "document:write", "selection"],
"contributes": {
"graphics": [{ "type": "qr", "name": "QR", "defaultSize": [80, 80],
"inspector": [{ "key": "text", "label": { "en": "Text", "ko": "내용" }, "type": "text" }] }],
"tools": [{ "id": "qrTool", "label": "QR", "kind": "createGraphic", "graphicType": "qr", "icon": "icon.svg" }],
"menus": [{ "command": "regen", "label": { "en": "Regenerate", "ko": "다시 만들기" } }]
}
}3.8 help — guide and samples (manual, sample files, sample layouts)
Ship a guide and samples with your plugin. They are reviewed with the version, and users see them in portal › Gallery › plugin details, Guide and Samples tabs, in Guide & samples on the dashboard plugin card, and in editor plugin menu › Help (for the running version). Only signed-in users can see them — the gallery before sign-in shows the About tab only and nothing can be downloaded. The editor does not read help, so it doesn't depend on apiVersion.
"help": {
"manual": [
{ "id": "start", "title": { "en": "Getting started", "ko": "시작하기" },
"file": { "en": "docs/en/start.md", "ko": "docs/ko/start.md" } },
{ "id": "options", "title": "Options", "file": "docs/options.md" }
],
"samples": [
{ "id": "words", "title": { "en": "Sample word list" }, "description": { "en": "A CSV to try with the import command" },
"lang": "en", "file": "samples/en/words.csv" },
{ "id": "photos", "title": { "en": "Photos" }, "lang": "en", "file": "samples/en/Photo/" }
],
"layouts": [
{ "id": "0f8fad5b-d9cb-469f-a165-70867728950e", "lang": "en", "title": { "en": "Phonetic word book" } }
],
"defaultLang": "en",
"homepage": "https://example.com/plugin",
"supportUrl": "https://example.com/support"
}| Field | Format | Description |
|---|---|---|
manual | Array (up to 20 pages) | Guide pages, in table-of-contents order — id (up to 40 lowercase letters, digits and -), title (localized text, 100 chars), file (an .md in docs/, one path or one per language) |
samples | Array (100 per language) | Downloadable sample files — id, title, description (500 chars), file (a file in samples/, or a folder ending in / — users get it as one zip), lang |
layouts | Array (20 per language) | Sample layouts — id = the id of a layout in your Shared folder, title · description (default: the layout title), lang |
defaultLang | Language code | The language shown when there are no samples for the screen language (default en) |
homepage | https address (up to 500 chars) | Plugin website — shown as a link only |
supportUrl | https address (up to 500 chars) | Support or contact address |
Guide — Markdown: headings (# · ## · ###), paragraphs, lists, tables, bold, code, links, and images .
- Images only from the package (relative to that
.md; png, jpg, webp, svg). External image addresses are not drawn — only the alt text shows. - Links:
https://opens a new tab, other guide pages such asoptions.md#…switch to that page, anything else shows as text. - HTML is not rendered.
Samples per language — give sample files and layouts a lang (one code or an array: "ko" · ["zh-Hans", "zh-Hant"]) and they come first only on screens in that language. Without lang, every language.
- If no sample matches the screen language, the samples of
defaultLang(defaulten) are shown."zh"matcheszh-Hansandzh-Hant. - Samples in other languages are folded under 'Samples in other languages' — a Korean user can still get the English samples.
- Limits are counted per language (items without
langcount in every language): 100 sample files and 20 layouts (400 and 60 in total). - The guide page and Preview guide in the developer console let you switch the viewing language.
Folder samples — a file ending in /, such as "samples/en/Photo/", makes the whole folder (subfolders included) one item. Users get one zip the server makes (Photo.zip) instead of dozens of images one by one. For a per-language bundle, use one folder such as samples/ko/.
Sample files — the samples/ folder takes any file that doesn't run (docx, xlsx, pdf, idml, psd, images, text, csv …). Executables, scripts, HTML, SVG, archives and macro Office files (docm, xlsm …) are refused, and so is any file with an executable header (MZ, ELF, Mach-O, #!) whatever its name. Users always get them as downloads. The plugin itself can read them too, e.g. podolay.package.text('samples/words.csv').
Sample layouts — give users a real layout made with the plugin.
- In Portal › Developer › Prepare sample layouts, pick a layout and click Make a shared copy — a read-only copy appears in your Shared folder and the line for
help.layouts({ "id": … }) is shown (copy button). Sharing it from the dashboard card menu › Share (no recipients) does the same. Share copies can't be edited, so it stays fixed as this version's sample. - Put that line (the copy's id) in
help.layouts. - When you upload the version, the server checks that the id is a layout in your Shared folder (
PLUGIN_SAMPLE_LAYOUT_INVALID).
When a user clicks Import, the layout is copied into the user's Received Files folder as a normal (editable) layout — within the user's layout and storage limits. User fonts don't travel (the user sees other fonts). If you delete the share copy, the sample shows as removed by the developer.
Who can get them — the guide: anyone signed in (also before buying or installing). Sample files and layouts: only people who can use the plugin — free plugins need a paid plan (Starter or higher), paid plugins need a purchase (or an active trial — regardless of plan). You and admins always can.
Size — code, guide and samples together: a 20 MB zip (40 MB unpacked, 500 files). Use Preview guide on the version row in the developer console to check it before review.
3.9 dataFrom — reading your other plugins' data
1.8 — read only what another plugin of yours (say, a free edition → Pro) left in documents and the account. List that plugin's id in the manifest.
"apiVersion": "1.9",
"dataFrom": ["com.example.dbp"]| Where | What you get |
|---|---|
pluginDataFrom in object descriptions (doc.graphic · doc.graphics · doc.find …) | { "com.example.dbp": that plugin's pluginData } — only on objects that plugin marked |
plugin.data of plugin graphics | The data too when the graphic belongs to a dataFrom plugin |
doc.find({ tag, from: 'com.example.dbp' }) | Finds by that plugin's marks |
doc.getData('com.example.dbp') | That plugin's document data |
storage.getFrom('com.example.dbp') | 🔑 storage That plugin's account storage (null in development mode) |
- When you upload a version the server checks that every
dataFromplugin is one you uploaded (elsePLUGIN_DATA_FROM_INVALID). There is no writing — to change things, add your own marks. - A
fromthat isn't indataFromis an error.
4. podolay at a glance
| API | Version | Permission | Returns | What it does |
|---|---|---|---|---|
podolay.apiVersion | 1.0 | 🔓 | '1.7' | This editor's API version |
podolay.env | 1.0 | 🔓 | Object | Plugin, editor and language info |
graphics.define(type, def) | 1.0 | 🔓 | — | Graphic type — data → drawing |
tools.define(id, def) | 1.0 | 🔓 | — | Mouse handling for custom tools |
commands.define(id, fn) | 1.0 | 🔓 | — | Menu and shortcut commands |
on(name, fn) | 1.0 | 🔓 (selection for selection) | Unsubscribe function | Editor events |
doc.info() | 1.0 | document:read | → DocInfo | Document, spreads, pages, masters |
doc.graphics({spread}) | 1.0 | document:read | → Graphic[] | Objects on a spread |
doc.graphic(id) | 1.0 | document:read | → Graphic | null | A single object |
doc.raw() | 1.0 | document:read | → any | The whole document storage format |
doc.getData() | 1.0 | 🔓 | → any | This plugin's document data |
doc.styles() | 1.1 | document:read | → { para, char } | Paragraph and character styles |
doc.text(id) | 1.1 | document:read | → StoryPara[] | The text of a text object or text target |
doc.find(query) | 1.1 | document:read | → Graphic[] | Find template slots |
doc.textStatus(id) | 1.1 | document:read | → TextStatus | Text flow and overset |
doc.inlines(id) | 1.2 | document:read | → InlinePlaced[] | Inline objects |
doc.texts(q) | 1.4 | document:read | → TextPlace[] | Every text place in the document |
doc.table(target) | 1.6 | document:read | → TableInfo | Rows, columns, merges and header rows of a table |
doc.tableStyles() | 1.7 | document:read | → { table, cell } | Table and cell style names |
text.measure(story, width, opts) | 1.1 | document:read | → { height, lines } | Measure text before placing it |
text.selection() | 1.4 | selection | → TextSelection | null | The selection of the text being edited |
selection.get() | 1.0 | selection | → number[] | Selected objects |
selection.set(ids) | 1.0 | selection | → number[] | Select objects |
selection.setText(id, start, end?) | 1.4 | selection | → true | Select a text range and open it for editing |
edit(name, fn) | 1.0 | document:write | → { created, results } | Change the document (one Undo step) |
batch(name, fn) | 1.1 | document:write | → value of fn | Many edits as one Undo step |
ui.toast(msg, kind) | 1.0 | 🔓 | → void | Notification |
ui.progress(o) | 1.1 | 🔓 (inside batch) | → void | Progress window |
ui.showPanel(id) | 1.0 | ui | → void | Open a panel |
ui.closePanel(id) | 1.0 | 🔓 | → void | Close a panel |
ui.postMessage(panelId, msg) | 1.0 | 🔓 | → void | Send to a panel |
ui.onMessage(fn) | 1.0 | 🔓 | — | Receive from a panel |
ui.pickFiles(o) | 1.6 | 🔓 | → PickedFile[] | Read files or folders from the user's computer |
storage.get() · set(v) | 1.0 | storage | → any · → void | Per-account settings |
license.status() | 1.5 | 🔓 | → LicenseStatus | License and trial of a paid plugin |
assets.upload(bytes, opts) | 1.1 | document:write | → { asset, width, height, sha256?, reused? } | Turn an image into a document asset |
assets.list(q) | 1.6 | document:read | → AssetInfo[] | The document's image assets |
package.read(path) · text · json | 1.4 | 🔓 | → ArrayBuffer · → string · → any | Read a file in the package |
The tx operations are listed in the table in §9.2.
4.1 podolay.apiVersion · podolay.env
podolay.apiVersion // '1.7' — this editor's API version
podolay.env // { pluginId, pluginVersion, apiVersion, editorVersion, locale, permissions, dev }env key | Type | Description |
|---|---|---|
pluginId | string | Manifest id |
pluginVersion | string | Manifest version |
apiVersion | string | The editor's API version |
editorVersion | string | Editor version (2.11.0, etc.) |
locale | string | Editor UI language — ko en es de fr ja zh-Hans zh-Hant. Not the same as the document language |
permissions | string[] | Granted permissions (including network:…) |
dev | boolean | Whether the plugin was loaded in development mode (pluginDev) |
env is filled in before main.js starts, so you can read it from top-level code too.
5. Definitions — graphics · tools · commands
Make definitions once, at the top level of main.js. Calling again with the same name overwrites the earlier definition.
5.1 graphics.define(type, def)
1.0 🔓 — A plugin graphic type. It can only be used if the manifest's contributes.graphics has the same type.
podolay.graphics.define('grid', {
defaults: { rows: 10, cols: 20, color: '#c00' }, // a value or an (async) function
render(data, { width, height }) { // pure function — same input → same drawing
const items = [];
for (let r = 0; r <= data.rows; r++) {
const y = height * r / data.rows;
items.push({ t: 'line', x1: 0, y1: y, x2: width, y2: y, stroke: { color: data.color, width: 0.5 } });
}
return items; // or { items }
},
});def key | Type | Description |
|---|---|---|
defaults | value · () => value · async () => value | The data for an object newly created with the tool |
render | (data, { width, height }) => Drawing | Promise<Drawing> | Returns a display list |
size | (data, frame) => { width, height, x?, y? } | null 1.3 | Natural size — called only with autoSize: true in the manifest |
When render is called: when the object is created with the tool, on tx.create({ kind: 'plugin' }) and tx.setData, when an Inspector field changes, after the object is resized (0.25 s after resizing stops), when Undo/Redo changes its size, and when tx.set(id, { frame }) changes its size (with the current data if the same edit has no setData, in the same Undo step). width/height are always the size after that edit — changing frame and setData in one edit renders once at the new size, whatever the op order.
- The drawing is saved in the document — people without the plugin, reader mode, PDF and IDML all show and print that drawing. So
rendermust be fast (tens of ms) and pure. - If the object is resized and the plugin isn't available, the saved drawing is stretched.
- 1.3 Natural size — for graphics whose data decides their size (like a formula), declare
autoSize: truein the manifest and definesize(data, frame). Whenever the editor changes the data (an Inspector field,tx.setData, creating with the tool,tx.setInline), it callssizefirst, fits the frame to the result, renders at that size, and changes frame and data in one Undo step. Withoutx/ythe top-left corner stays put (compute them fromframeto keep the centre).null= keep the size. If the sameeditsets the size itself withtx.set(id, { frame }),sizeisn't called. For an inline object the text reflows to the new size. It isn't called when the user resizes with the handles (the object renders at the user's size). Sides are capped at 20000 pt and the call must answer within 8 s (otherwise, or on a malformed answer, the size stays and only a log entry is written). - If
renderdoesn't answer within 8 seconds or the drawing exceeds the limits, that call fails and the previous drawing stays.
5.2 tools.define(id, def)
1.0 🔓 — Mouse handling for kind: 'custom' tools. (createGraphic tools are handled by the editor and need no definition.)
podolay.tools.define('lasso', {
onDown(e) { pts = [[e.x, e.y]]; },
onMove(e) { pts.push([e.x, e.y]); return [{ t: 'path', d: toPath(pts), stroke: { color: '#09f', width: 1 } }]; }, // preview
async onUp(e) { await podolay.edit('Lasso', tx => tx.create({ kind: 'rectangle', frame: bbox(pts) })); },
});| Event key | Type | Description |
|---|---|---|
x · y | number | Spread coordinates, pt |
shift | boolean | |
alt | boolean | Option/Alt |
mod | boolean | ⌘ or Ctrl |
button | number | 0 = left |
- If
onDownoronMovereturns a display list (in spread coordinates), the editor draws it as a preview. Returningnullclears the preview. - While the editor waits for an answer, only the latest move is sent — a slow plugin never makes the editor lag.
- When the gesture ends (after
onUp), the preview is cleared. Calleditto leave something in the document.
5.3 commands.define(id, fn)
1.0 🔓 — A command invoked by a menu item or shortcut. fn may be async.
podolay.commands.define('regen', async () => { … });
podolay.commands.define('makeQr', async (arg) => { // 1.2 context menu — arg holds where it was invoked
if (arg?.context === 'object') for (const id of arg.ids) …;
});1.2 When invoked from a context menu (menus[].context), fn(arg) receives where it was invoked. From the header menu or a shortcut there is no argument (undefined). 1.4 Commands called from the header menu or a shortcut also get the same text argument as the text context menu while a text frame or a document table cell is being edited (1.4 plugins only — 1.3 and lower still get no argument).
arg.context | Fields |
|---|---|
object | ids = the selected object ids (if the right-clicked object wasn't selected, it alone is selected first) · spread = spread number · point = the right-clicked point (spread coordinates, pt) |
canvas | spread · page = the page number at that point (null outside any page) · point |
text | id = the text being edited — a text frame (thread head) id, 1.4 for a table cell { id: table, cell: [row, col] } (text target) · selection = { start, end, text } — the character range [start, end) in Graphic.text and the selected text (start = end for a bare caret) |
- When calling with a
textargument, the editor ends text editing (the text being typed is committed to the document) and then calls the command — sodoc.textandtx.insertInline(id, selection.start, …)see the same character positions. - When the command finishes, the editor reopens editing — unless the plugin called
selection.setorselection.setText: the original selection if the text is unchanged, otherwise a caret atoriginal end + characters added(right after the new text when the range was replaced). Nothing is reopened while abatchis running or if other text started being edited meanwhile.// 1.4 — manifest "keybindings": [{ "command": "ipa", "key": "ctrl+alt+c" }] (⌃⌥C on Mac · Ctrl+Alt+C elsewhere) podolay.commands.define('ipa', async (arg) => { if (arg?.context !== 'text' || arg.selection.start === arg.selection.end) return; const { start, end, text } = arg.selection; await podolay.edit('IPA', tx => tx.replaceRange(arg.id, start, end, { runs: [{ text: toIpa(text), font: 'Charis SIL' }] })); }); // afterwards the editor reopens editing with the caret right after the new text idsis passed even without theselectionpermission (the user chose those objects explicitly). Reading or changing the document still requiresdocument:read/document:write.- A command must finish within 60 seconds (after that the editor stops waiting and shows a "Plugin command failed" notification). Do longer work inside a
batch, or run it separately without awaiting it. - Thrown errors are shown as a notification and in the log.
6. Events — podolay.on
1.0 — podolay.on(name, fn) returns a function that unsubscribes.
const off = podolay.on('selectionChange', ({ ids }) => { … });
off(); // stop receiving| Name | Payload | When | Permission |
|---|---|---|---|
selectionChange | { ids: number[], inline? } | The selection changes | selection |
documentChange | { origins } | After the document changes (consecutive changes are coalesced over 0.2 s) | 🔓 |
dataChange | { id, before, after, origin } | After the data of one of this plugin's objects changes | 🔓 |
historyChange | {} | After the user performs Undo or Redo | 🔓 |
panelClose | { panelId: string } | The user closed one of this plugin's panel windows | 🔓 |
textSelectionChange 1.4 | { id, start, end, text } | null | After the caret or selection changes while editing text (once, 0.12 s after it stops), null when editing ends | selection |
selectionChange.inline={ id, index }when an inline object is selected — the story (text frame id) and theindexused bydoc.inlinesandtx.removeInline. The object inidsis then a stand-in that exists only while it is selected (same as the context menu'sarg.inline).documentChange.origins= why the document changed during those 0.2 s:user(user edits, the Inspector),plugin(a pluginedit),history(Undo/Redo).dataChangefires every time one object'sdataactually changes (not coalesced), only to the plugin that made the object.origin=inspector(an Inspector field),plugin(tx.setData),undoorredo. A re-render after a resize alone does not fire it, nor does changing an inline object withtx.setInline(including its Undo — that is a text change and only firesdocumentChange). When an inline object is edited in the Inspector,idis the stand-in that exists only while it is selected. To avoid fighting Undo, don't apply follow-up fixes onundo/redo.textSelectionChangeis not sent again for the same value. The value has the shape oftext.selection()—idis a text target (a text frame id or a table cell{ id, cell }). It isnullwhile editing a footnote or the text inside an inline object. Use it to preview the selected text in a panel.
Errors thrown inside an event handler are only logged.
7. Reading the document — podolay.doc · podolay.text
7.1 doc.info()
1.0 🔑 document:read → DocInfo
const info = await podolay.doc.info();
// { title, language, colorMode, pageSize: {width, height}, currentSpread,
// spreads: [{ index, pages: [{ index, x, y, width, height, master, label }] }],
// masters: [{ id, name }] }| Key | Description |
|---|---|
language | Document language (ko en ja …) |
colorMode | CMYK · RGB |
currentSpread | The spread number currently in view |
pages[].x·y | The page's position within the spread — to place an object on a page, use frame.x = page.x + … |
pages[].master | Master id (null if none) 1.1 |
pages[].label | Displayed page number — follows section numbering (iii, A-3 …) 1.1 |
masters | List of masters { id, name } 1.1 |
7.2 doc.graphics(q)
1.0 🔑 document:read → Graphic[] — Objects placed directly on the spread, in stacking order (back → front). A group lists the objects inside it in children.
q key | Default | Description |
|---|---|---|
spread | Current spread | Spread number |
Objects on the pasteboard (outside the pages), hidden objects and objects on hidden levels are included — tell them apart with visible and the frame. Master objects are not included (doc.find({ masters: true }), doc.graphic).
It returns one spread only — in a single-sided document every page is its own spread. To cover the whole document, loop over doc.info().spreads passing spread, or use doc.find. The mock editor keeps all pages in spread 0, so tests there don't show the difference — run code that looks for objects on other pages in the sandbox editor too.
7.3 doc.graphic(id)
1.0 🔑 document:read → Graphic | null — Looks up an object by id anywhere in the document (including inside groups and on masters). Returns null if not found.
See the Graphic type.
1.6 id may also be an inline object address { id: story, inline: n, path? } — reads an inline object or a child inside an inline group. The returned value has no id (inline objects have none), and group children carry path. frame is not in spread coordinates — where the object sits is the placed of doc.inlines. An address that doesn't exist is an error, not null.
7.4 doc.raw()
1.0 🔑 document:read → any — The whole document in its storage format. This is an internal format and may change between editor versions — read only what no other API provides, and write code that tolerates changes. It is read-only.
7.5 doc.getData()
1.0 🔓 → any — The value this plugin saved in the document with tx.setDocData (null if none). Other plugins' values are not visible.
7.6 doc.styles()
1.1 🔑 document:read → { para: StyleInfo[], char: StyleInfo[] } — The document's paragraph and character styles (excluding "None").
const { para, char } = await podolay.doc.styles();
// para: [{ id: -1, name: 'Basic', key: '$base' }, { id: 12, name: 'Title', basedOn: -1 }, …]| Key | Description |
|---|---|
id | Style id |
name | Display name. Built-in style names differ by UI language |
key | Built-in styles only — '$base' (Basic). Use this alias in a Story |
basedOn | Id of the style it is based on |
7.7 doc.text(id)
1.1 🔑 document:read → StoryPara[] — The text of a text object (a text frame, title text, or a shape containing text). For threaded frames, any frame id returns the whole story.
const story = await podolay.doc.text(id);
// [{ para: 'Title', runs: [{ text: 'Spring collection', char: '$base', font: 'Noto Sans KR', size: 28, color: {cmyk:[0,100,100,0]}, bold: true }] }, …]- Paragraph and character styles are given by name (
'$base'for the built-in one); direct formatting covers only font, size, color, bold and italic. - Variable text such as page numbers and footnote references comes with
variable: trueand its currently displayed value. - 1.2 Inline objects come as separate runs
{ inline: { index, kind, width, height, … } }(§16.8). Passing such a run back totx.setTextunchanged reuses that object — sodoc.text→ modify →setTextdoesn't lose inline images. For plugins withapiVersion1.1 or lower, the run'stextcontains the object replacement characterU+FFFC, as before. - 1.4
idmay also be a text target — a table cell{ id: table, cell: [row, col] }, an inline text frame{ id: story, inline: n }, a cell of an inline table{ id, inline, cell }. Before 1.4 a table gave the error '글이 있는 개체가 아닙니다' (not a text object), and inline text frames could only be read through thetextofdoc.inlines. 1.6 Text frames and table cells inside an inline group are{ id, inline, path }and{ id, inline, path, cell }. - 1.6 More direct formatting — the run's
tracking,hScale,vScale,baselineShift,underline,strike,positionandsmallCapsare included only when they differ from the default, and the paragraph'salign,leading,spaceBefore,spaceAfter,indentLeft,indentRightandindentFirstonly when they differ from its paragraph style (§16.6). Plugins on 1.5 or lower don't get them.
7.8 doc.find(query)
1.1 🔑 document:read → Graphic[] — Finds template slots: the objects that match all conditions (including inside groups, in spread order).
await podolay.doc.find({ text: '{{title}}' }); // text objects containing the placeholder
await podolay.doc.find({ tag: { slot: 'photo' }, page: 2 }); // objects on page 3 tagged by this plugin
await podolay.doc.find({ kind: 'image', spread: 0 });query key | Type | Description |
|---|---|---|
text | string | The text contains this string. For threaded text, only one frame (the head of the thread) matches |
tag | true · string or number · object | This plugin's pluginData — true = present, a value = equal (or pluginData.tag is equal), an object = those keys are equal |
kind | Graphic.kind | Kind |
spread | number | Only on that spread |
page | number (zero-based) | The center of the object's frame lies on that page |
masters | true | Include master spreads too (when spread and page aren't given) |
limit | number | Default 1000, max 5000 |
Objects don't have names. Find slots by placeholders in the text (
{{field}}), which designers can type in the editor, or bypluginDatatags attached through a plugin panel.
Search scope: objects on the pasteboard (outside the pages), hidden objects and objects on hidden levels are included (a template kept on the pasteboard or hidden is still found). Masters only with masters: true.
7.9 doc.textStatus(id)
1.1 🔑 document:read → TextStatus — Text flow, read from the same layout as the screen.
const st = await podolay.doc.textStatus(bodyId);
// { frame: 0, frames: [101, 102], lines: 7, chars: [0, 134], length: 458, height: 108.2, overflow: false }| Key | Description |
|---|---|
frame | This frame's position in the thread |
frames | The ids of the frames in the thread (one if not threaded) |
lines | Number of lines in this frame |
chars | Character range in this frame [start, end) |
length | Length of the whole story |
height | Height of the text in this frame (pt) |
overflow | Text overflows at the end of the thread (overset) |
If font slices haven't arrived yet, the values may change slightly a moment later.
7.10 text.measure(story, width, opts)
1.1 🔑 document:read → { height, lines } — Composes text without changing the document.
const { height, lines } = await podolay.text.measure(story, 180, { like: bodyId });| Argument | Description |
|---|---|
story | Story |
width | Width the text is set in, pt — the frame width minus its insets |
opts.like | A text object id — use that text's first formatting as the base (= the same formatting as tx.setText). Defaults to the document's "Basic" style |
Text is measured with the fonts loaded right now. Fonts and characters not yet used in the document may be measured with a substitute font at first, so check with doc.textStatus after placing the text. 1.2 If the Story contains inline object runs, their sizes are measured too ({ graphic: id } uses that object's size without moving it).
7.11 doc.inlines(id)
1.2 🔑 document:read → InlinePlaced[] — The inline objects (inline anchors) of a text object, in text order. For threaded frames, the whole story.
const list = await podolay.doc.inlines(bodyId);
// [{ index: 0, kind: 'image', width: 40, height: 30, image: …, pluginData: 'logo', offset: 12,
// placed: { frame: 102, x: 318.5, y: 240, width: 40, height: 30 } }, …]| Key | Description |
|---|---|
index | Which inline object this is in the story (zero-based) — the { index } of tx.removeInline and setText |
offset | Character position in Graphic.text (the object replacement character U+FFFC) |
baseline | 1.3 From the object's bottom edge up to the text baseline (pt) — omitted when 0 |
placed | Where it sits — frame = that text frame's id; x, y, width, height = spread coordinates (before the frame's rotation). null if it's in overset text |
| Others | Same as Graphic — width and height instead of id and frame (inline objects have no id) |
children[].path | 1.6 The position inside the group [child index, …], on the children of an inline group (and of groups inside it) — the path of an inline object address |
Positions are read from the same layout as the screen (they may shift slightly if font slices arrive late). Objects inside footnote text are not included.
1.8 id may also be a text target outside text frames — a table cell { id, cell }, an inline table cell { id, inline, cell } and so on. You get that text's inline objects in the same shape, with placed = null. Address them with { in: that text target, inline: index, path? } (for a catalog with a product picture in each table row, say).
const cell = { id: tableId, cell: [row, 0] };
const [pic] = await podolay.doc.inlines(cell); // the inline group in the cell
await podolay.edit('photo', tx => tx.setImage({ in: cell, inline: pic.index, path: [0] }, asset, 'fill'));7.12 doc.texts(q)
1.4 🔑 document:read → TextPlace[] — every text place in the document, in spread order. For plugins that sweep text (spelling, notation conversion) and must not miss table cells or inline text boxes.
for (const t of await podolay.doc.texts()) {
// { target: 12, kind: 'frame', text: '…' } · { target: { id: 30, cell: [0, 1] }, kind: 'cell', text: '…' } · …
if (t.text.includes('ph')) await podolay.edit('Replace', tx => tx.replaceText(t.target, 'ph', 'f'));
}| Key | Meaning |
|---|---|
target | A text target — pass it as is to doc.text and tx.* |
kind | frame = a text frame (one head per threaded story) · cell = a table cell · inline = an inline text frame · inlineCell = a cell of an inline table |
text | The text (same shape as Graphic.text — \n between paragraphs) |
q key | Meaning |
|---|---|
spread | Only that spread |
masters | true = master spreads too (when spread is not given) |
Objects inside groups are included; table cells covered by a merge are skipped. A text frame is followed by the inline text frames and table cells of its story. Up to 20,000 entries. Text on the pasteboard, hidden objects and objects on hidden levels is included. Masters only with masters: true.
7.13 text.selection()
1.4 🔑 selection → { id, start, end, text } | null — the selection of the text the user is editing. While a command called with a text argument (§5.3) runs, it returns that place (even though editing is closed for the moment).
id= a text target — a text frame (thread head) id or a table cell{ id, cell }.start·end= character positions inGraphic.text,text= the selected text (''for a bare caret).nullwhen no text is being edited. Footnotes and the text inside inline objects arenullfor now.- To show it in a panel, listening to the
textSelectionChangeevent is lighter.
7.14 doc.table(target)
1.6 🔑 document:read → TableInfo — the structure of a table. target = a table id on a spread (including inside groups) or an inline table { id: story, inline: n, path? }.
const t = await podolay.doc.table(tableId);
// { rows: 6, cols: 4, headerRows: 1, footerRows: 0, merges: [[2, 0, 2, 1]], rowHeights: [20, 18, …], colWidths: [80, 60, 60, 60],
// tableStyle: 'Striped', cellStyles: [[null, …], …] } ← 1.7| Key | Meaning |
|---|---|
rows · cols | Number of rows and columns |
headerRows | Number of header rows (top rows repeated when the table breaks across pages) |
footerRows | Number of footer rows (bottom rows repeated below every part but the last when the table breaks across pages; editor 2.15.0 or later — always 0 before) |
merges | Merged cells as [row, col, row count, col count] (zero-based, by the top-left cell) |
rowHeights · colWidths | Row heights and column widths (pt) |
tableStyle | 1.7 Name of the applied table style, or null |
cellStyles | 1.7 Per cell (row → column), the name of the cell style applied directly, or null (also for cells covered by a merge; region cell styles are in tableStyles() regions) |
Read a cell's text with doc.text({ id, cell }); add and remove rows with tx.insertRows · tx.removeRows; apply styles with tx.setTableStyle · tx.setCellStyle.
7.15 doc.tableStyles()
1.7 🔑 document:read — the document's table styles and cell styles (made in the editor's Inspector › Table Style).
const { table, cell } = await podolay.doc.tableStyles();
// table: [{ name: 'Striped', regions: { header: 'Header cell', body: null, footer: null, left: null, right: null }, alternating: 'rows' }]
// cell: [{ name: 'Header cell' }]regions = the cell style name used for each region (null if none — header, footer, left and right follow the body), alternating = alternating fills 'rows', 'cols' or null. Creating and editing styles is the user's job, so the API has no calls for it — plugins apply styles by name.
8. Selection — podolay.selection
8.1 selection.get()
1.0 🔑 selection → number[] — The ids of the selected objects.
8.2 selection.set(ids)
1.0 🔑 selection → number[] — Selects objects. Returns the ids that were actually selected.
- Up to 1000. Objects inside a group can't be selected (select the whole group).
- One spread only — only objects on the first object's spread are selected.
8.3 selection.setText(id, start, end?)
1.4 🔑 selection → true — selects [start, end) of that text and opens it for editing (without end, a caret at start). If it is on another spread or a master, the editor goes there (as Find does).
id= a text frame id or a document table cell{ id, cell }(text target). Not inside inline objects yet.0 ≤ start ≤ end ≤ text length(character positions inGraphic.text). Not during abatch.- Called inside a command run with a text argument, the editor doesn't restore the original selection after the command (the same goes for
selection.set).
9. Writing to the document — podolay.edit and tx
9.1 edit(name, fn)
1.0 🔑 document:write → { created: number[], results: any[] }
const { created, results } = await podolay.edit('Align', tx => {
tx.set(id, { frame: { x: 0, y: 0, width: 100, height: 50 } });
const k = tx.create({ kind: 'text', frame, story: 'Hello' }); // k = the op index within this edit
});
results[k]; // the new object's idfnis a synchronous function — don'tawaitinside it.tx.*only records what to do; the editor applies everything at once afterfnreturns.- One
edit= one Undo step for the user ("plugin name: name" in the Undo menu). If any op fails, nothing changes andeditthrows. - Up to 2000 ops per edit.
- Ops are applied in the order they were recorded — a later op sees the document as changed by earlier ones (in one edit,
replaceRangecan edit text duplicated bycopyRange). created= ids of the objects created (in creation order);results[op index]= each op's result ("Result" in the table below). Eachtx.*returns its op index.- If the user was editing text, the editor ends text editing before applying.
9.2 tx operations at a glance
| Operation | Version | Result results[op] | What it does |
|---|---|---|---|
create(o) | 1.0 | New object id | Create an object |
set(id, props) | 1.0 | null | Change common properties |
remove(id) | 1.0 | null | Delete |
setData(id, data) | 1.0 | null | Data of your own graphic (also redraws it) |
setPluginData(id, value) | 1.0 | null | This plugin's tag on any object |
setDocData(value) | 1.0 | null | Document-level data |
setText(id, story) | 1.1 | null | Replace the whole text (keeps template formatting) |
replaceText(id, search, text) | 1.1 | null | Replace placeholders |
setImage(id, asset, fit) | 1.1 | null | Put an image in a shape |
link(from, to) | 1.1 | null | Thread text frames |
unlink(from) | 1.1 | null | Unthread text frames |
addPages(o) | 1.1 | { pages } | Add pages |
duplicatePages(o) | 1.1 | { pages, copies } | Duplicate pages (including objects) |
removePages(pages) | 1.1 | null | Delete pages |
applyMaster(pages, master) | 1.1 | null | Apply a master |
insertInline(id, at, inline) | 1.2 | Number inserted | Insert an object into text |
removeInline(id, index) | 1.2 | null | Remove an inline object |
setInline(id, index, o) | 1.3 | null | Change an inline object in place |
replaceRange(id, start, end, what) | 1.3 | null · { start, end } ({ story }) | Replace a character range with text, formatted runs, an object or a Story |
formatRange(id, start, end, format) | 1.4 | characters formatted | Apply formatting only to a character range |
copyRange(from, to) | 1.6 | { start, end, inlines, dropped } | Duplicate a text range with its formatting |
appendText(id, story, o) | 1.6 | { start, end, inlines } | Append a Story with its paragraphs to the end of a text |
insertRows(target, o) | 1.6 | { rows } | Insert table rows (duplicating rows) |
removeRows(target, at, count?) | 1.6 | rows removed | Remove table rows |
autoflow(id, o) | 1.6 | { pages, frames, overflow } | Flow overset text onto new pages and frames |
duplicate(id, o) | 1.6 | { id, copies } | Duplicate a spread object |
setTableStyle(target, name, o?) | 1.7 | null | Apply or remove a table style |
setCellStyle(target, cells, name, o?) | 1.7 | null | Apply or remove a cell style |
1.4 The id of the operations that take text (setText · replaceText · replaceRange · formatRange) may also be a text target — changing the text of a table cell or an inline text frame becomes part of the Undo step that changes that table or host story. Inline objects can't be inserted into the text of inline objects (inline text frames; { inline } runs in setText, { inline } in replaceRange). Table cells take them from 1.9 — see below. 1.6 Text inside an inline group is { id, inline, path }, and the id of tx.set, tx.setPluginData and tx.setImage may also be an inline object address.
9.3 tx.create(o)
kind | Fields | Description |
|---|---|---|
plugin | type, frame, data? | Your own graphic type (render is called to produce the drawing) |
rectangle · ellipse | frame, fill?, stroke? | fill: a color · null (none). stroke: { color?, width? } · null |
line | frame, stroke? | From the frame's top-left to its bottom-right. Default black 1pt |
text | frame, text | Unformatted text |
text 1.1 | frame, story | A styled Story. Base = the document's "Basic" style |
image 1.1 | frame, asset, fit? | An image frame. For asset see §13; for fit see §9.11 |
Common: frame = spread coordinates in pt, spread? = spread number (default: the current spread). New objects go on the current level (layer).
9.4 tx.set(id, props)
| Property | Type | Description |
|---|---|---|
frame | Frame | Position and size (spread coordinates) |
rotation | number (degrees) | |
opacity | 0–1 | Out-of-range values are clamped |
visible | boolean | |
locked | boolean | |
fill | color · null | Solid fill · no fill |
stroke | { color?, width? } · null | Stroke · no stroke |
Objects inside groups can be changed too. Other properties (text frame options, etc.) will come in a later API version.
1.6 With an inline object address { id: story, inline: n, path? } as id, it changes an inline object or a child inside an inline group (one Undo step that changes the host story; locking follows the host story). Changing frame or rotation re-measures the group's frame and the size of the inline slot — the text reflows.
9.5 tx.remove(id)
Only objects placed directly on a spread (objects inside a group go with the whole group). Locked objects are not deleted (the whole edit fails).
9.6 tx.setData(id, data)
Only for this plugin's graphics (kind: 'plugin', same plugin). The editor calls render again and updates the drawing and data together. data must be at most 256 KB of JSON.
9.7 tx.setPluginData(id, value)
Attaches this plugin's tag to any object — it never mixes with other plugins' tags. value = JSON (up to 256 KB), null = remove. It is saved and copied along with the object, and read through doc.find({ tag }) and Graphic.pluginData. 1.6 id may also be an inline object address — tag a child inside an inline group and find it again through the children of doc.inlines.
9.8 tx.setDocData(value)
Saves this plugin's value at the document level (read it with doc.getData()). At most 256 KB of JSON; null = remove.
9.9 tx.setText(id, story)
1.1 — Replaces all the text of a text object (the whole story for threaded frames). The base is the current text's first paragraph and first character formatting — formatting the designer applied in the template is kept. Not allowed on locked objects.
tx.setText(titleId, 'Spring collection'); // template formatting as is
tx.setText(bodyId, [{ para: 'Body', runs: [{ text: 'List price ' }, { text: '$12.00', char: 'Emphasis' }] }]);9.10 tx.replaceText(id, search, text)
1.1 — Replaces every occurrence of search in the text with text. The surrounding formatting is kept.
searchis 1–1000 characters,textup to 100,000 characters (line breaks become spaces).- It only finds matches that lie entirely within one formatting run — if the formatting changes in the middle of a placeholder, it won't be found.
- Variable text such as page numbers and footnotes, and inline objects, are left alone. If nothing is found, nothing happens (not an error).
9.11 tx.setImage(id, asset, fit)
1.1 — Places or replaces an image in a shape (rectangle, ellipse, polygon, path, image frame, etc.; not tables). The image is clipped to the shape. Not allowed on locked objects.
fit | Description |
|---|---|
'fill' (default) | Covers the frame proportionally — the excess is cropped |
'fit' | Shows the whole image proportionally — leftover space stays empty |
'stretch' | Ignores the proportions and fills the frame exactly |
asset must be something this plugin uploaded in that window (assets.upload), 1.6 something assets.list returned, or an image path already in the document. After placement, the user can change it with the Fit options in the Inspector.
1.6 id may also be an inline object address — puts the image into an image box (shape) inside an inline group.
// into the second child of inline group #0, showing the whole image
await podolay.edit('Photo', tx => tx.setImage({ id: target, inline: 0, path: [1] }, asset, 'fit'));9.12 tx.link(from, to) · tx.unlink(from)
1.1 — Threads and unthreads text frames (same rules as threading directly in the editor).
link:frommust be the end of a thread (no next frame), andtoan empty text frame that doesn't belong to another story. Title text, text on a path and locked objects are not allowed.unlink: breaks the thread betweenfromand its next frame (the text of the following frames flows back into the earlier story).
9.13 Page operations
1.1 — Page numbers are zero-based and as they stand when that op runs (they shift if an earlier op adds or removes pages). The pages in the result are numbers after the whole edit has finished.
const r = await podolay.edit('Catalog pages', tx => tx.duplicatePages({ from: 2, times: 3 }));
r.results[0]; // { pages: [3, 4, 5], copies: [{ '120': 301, '121': 302 }, { … }, { … }] }| Operation | Arguments | Result |
|---|---|---|
addPages({ after?, count?, master? }) | after: after that page (default: at the end; -1 = at the start) · count: 1–200 · master: id, name or null (none); if omitted, the neighbouring page's master | { pages } |
duplicatePages({ from, count?, times?, after? }) | Duplicates count pages (default 1) starting at from, times times (1–500), after after (default: right after the original). Includes objects, masters and deleted master items (see Restore Deleted Master Items) | { pages, copies } — copies[k] = the k-th copy's { original object id: copy id } (1.6 children inside groups too) |
removePages(pages) | Array of page numbers. You can't delete every page | null |
applyMaster(pages, master) | master = id, name or null | null |
- An
editthat contains page operations is undone through one snapshot of the whole document (together with the other ops in the same edit). On failure, everything returns to the state before it started. - If the plan's page limit per document would be exceeded, the whole edit fails. One edit can add up to 1000 pages. Not allowed while editing masters.
tx.autoflowis a page operation too (same snapshot Undo, same page limit).copiesalso contains the ids of children inside groups (since the 1.6 editor — before, only top-level objects). Table cells have no id — a cell of the copied table is{ id: copied table, cell }.- Within the same
edit, the ids of new pages and copies aren't known yet — fill duplicated pages in the nextedit, and usebatchto make both one Undo step.
9.14 tx.insertInline · tx.removeInline
1.2 — An inline object (inline anchor) occupies one character position and flows with the text (it moves when lines rewrap or earlier text grows). You can put images, shapes, your own graphics and text boxes into text. Not allowed on locked text objects.
await podolay.edit('Insert logo', tx => {
tx.insertInline(bodyId, '{{logo}}', { kind: 'image', asset, width: 40, height: 30 }); // at every placeholder
tx.insertInline(bodyId, null, { graphic: badgeId }); // moves a spread object to the end of the text
});
await podolay.edit('Remove', tx => tx.removeInline(bodyId, 0));| Argument | Description |
|---|---|
id | The text object (the whole story for threaded frames) |
at | A string = placeholder — replaces every occurrence of it in the text with the object (only matches within one formatting run, as with replaceText) · a number = character position in Graphic.text (0 = start) · null or omitted = end of text |
inline | An Inline — a new object or { graphic: id } |
- The result
results[op]= the number of objects inserted. If the placeholder isn't found it's 0, not an error. Nothing is inserted inside variable text such as page numbers. - The inserted object takes on the formatting of the text at that position (e.g. the font size used to compute line height).
removeInline(id, index)— removes theindex-th inline object (along with its character) (theindexfromdoc.inlines).- You can also insert through Story runs — the
{ inline }run intx.create({ kind: 'text', story })andtx.setText.
9.15 tx.setInline · tx.replaceRange
1.3
await podolay.edit('Edit formula', tx => tx.setInline(bodyId, 2, { data: { tex: 'x^2' } })); // with autoSize, the size too
await podolay.edit('To formula', tx => tx.replaceRange(bodyId, sel.start, sel.end,
{ inline: { kind: 'plugin', type: 'math', data: { tex }, width: 30, height: 12, baseline: 3 } }));setInline(id, index, { data?, width?, height?, baseline? })— changes theindex-th inline object of textid(theindexofdoc.inlines) in place. Formatting, position and rotation stay. Only this plugin's graphics (kind: 'plugin'). Withwidth/heightthat size is used (greater than 0, up to 5000); with onlydataandautoSize,sizefits it.renderis called at the new size.indexcounts in the text as changed by earlier ops of the sameedit(inserting or removing objects before it shifts it).replaceRange(id, start, end, what)— replaces the character range[start, end). Positions are those ofGraphic.text(including the\nbetween paragraphs, same asdoc.find).what= text (a string or{ text }; line breaks become spaces) · 1.4{ runs: [{ text, char?, font?, size?, color?, bold?, italic? }] }·{ inline: Inline }. The formatting is that of the character atstart; a range across a paragraph end merges the paragraphs (keeping the first paragraph's formatting).{ runs }starts from the formatting of the character atstartand applies each run's character style → direct formatting (the same rules as Story runs; no inline runs, up to 1000 runs and 200,000 characters).{ inline }only into a text frame's text (a numericid) and, 1.9, a table cell's text (a text target withcell).start = endinserts. A range that cuts a variable run (such as a page number) in half fails (the whole run is fine). UnlikeinsertInlinewith a placeholder, only that range changes even if the same text appears elsewhere. 1.6{ story }— replaces the range with a Story, with paragraph styles and direct paragraph formatting (§9.18). The result is then{ start, end }.
9.16 tx.formatRange(id, start, end, format)
1.4 — applies formatting only to the character range [start, end); the text stays. On top of those characters' current formatting: the format's character style → direct formatting.
const r = await podolay.edit('IPA font', tx => tx.formatRange(sel.id, sel.start, sel.end, { font: 'Charis SIL', italic: false }));
r.results[0]; // the number of characters formattedformat key | Meaning |
|---|---|
char | Character style name or id ('$none' = only detach the style — direct formatting stays) |
font · size · color · bold · italic | Same as the direct formatting of Story runs (size 0.5–2000 pt) |
tracking · hScale · vScale · baselineShift · underline · strike · position · smallCaps | 1.6 Same as the additional direct formatting of Story runs |
- A format with no keys is an error.
results[op]= the number of characters formatted — 0 for an empty range (start = end), which changes nothing. - A range across paragraphs leaves the paragraphs as they are. A range that cuts a variable run (such as a page number) in half fails. An unknown style name fails the whole edit.
9.17 tx.copyRange(from, to)
1.6 — duplicates a text range with its formatting and inserts it somewhere else (the same text is fine). Paragraph styles, character styles, all direct formatting and inline objects (images, shapes, text boxes, groups, nested groups, inline tables) go along. Use it to repeat template text per record, as in database publishing.
| Argument | Meaning |
|---|---|
from | { id: text target, start, end } — the character range [start, end) to copy (a text target; character positions of Graphic.text) |
to.id | The text target to insert into |
to.at | A character position · 'end' (default) — where to insert |
to.newParagraph | true = if the insertion point isn't at the start of a paragraph, split the paragraph first (paste as a new paragraph). Default false |
- Paragraph formatting follows the paragraph end (
\n) (as in InDesign): paragraphs closed inside the copied range (those whose ending\nis included) keep the source's paragraph formatting; the open last piece takes the formatting of the paragraph it is inserted into. Exception: when it lands in the last paragraph of the text with nothing left of that paragraph (pasting into empty text, for instance), it keeps the source's last paragraph formatting. To move whole paragraphs, make the range include the paragraph end\n. - Result
results[op] = { start, end, inlines, dropped }—[start, end)= the inserted range in the target text (without the\nadded bynewParagraph),inlines= the ordinals of the inline objects in the inserted range (theindexofdoc.inlines),dropped= the number of footnote and endnote references left out. - Later ops in the same
editsee the duplicated text — if you can work out the range in advance, change it in the same edit. Usually you take the result and change it in the nextedit(example below). - A range containing inline objects can only be inserted into a text frame's text (
to.idis a number) and, 1.9, a table cell's text (to.idis{ id, cell }or{ id|in, inline, path?, cell }) — text frames inside inline objects can't hold inline objects yet. - Footnote and endnote references are not copied (the note text belongs to its original text) — they are left out and counted in
dropped. Other variable text, such as page numbers, is copied. - Nothing is inserted into locked text. An empty range changes nothing (
start = endis returned).
// Database publishing — duplicate the template text (whole paragraphs) at the end of the target text for each record, then fill the <field> tags
const tpl = (await podolay.doc.texts()).find(t => t.text.includes('<name>')); // also finds a template text box kept on the pasteboard
const len = tpl.text.length;
await podolay.batch('Build directory', async (signal) => {
for (const rec of records) {
signal.throwIfAborted();
const r = await podolay.edit('Duplicate', tx => tx.copyRange({ id: tpl.target, start: 0, end: len }, { id: bodyId, newParagraph: true }));
const { start, end } = r.results[0];
const { text } = await podolay.doc.graphic(bodyId); // the whole text (Graphic.text)
await podolay.edit('Fill', tx => {
// replace from the back so earlier positions don't shift
const hits = [...text.slice(start, end).matchAll(/<(\w+)>/g)].reverse();
for (const m of hits) tx.replaceRange(bodyId, start + m.index, start + m.index + m[0].length, String(rec[m[1]] ?? ''));
});
}
});9.18 tx.appendText · replaceRange with { story }
1.6 — inserts a Story with its paragraphs (paragraph styles and direct paragraph formatting) into a text. Unlike setText, it appends or replaces only a range instead of the whole text.
await podolay.edit('Append', tx => {
tx.appendText(bodyId, [{ para: 'Subhead', text: 'Chapter 3' }, { para: 'Body', text: '…' }]); // at the end, in a new paragraph
tx.replaceRange(bodyId, s, e, { story: [{ para: 'Quote', text: '…' }] }); // a range, with paragraphs
});appendText(id, story, { newParagraph? })— appends at the end of the text.newParagraphdefaults totrue(if the last paragraph has text, it is split first);false= continue the last paragraph. Result{ start, end, inlines }.replaceRange(id, start, end, { story })— deletes the range and inserts the Story there. Result{ start, end }.- Base formatting = the paragraph formatting of the paragraph at the insertion point and the character formatting of that character (the last character at the end of the text). On top of it: the Story's paragraph style → direct paragraph formatting, the run's character style → direct formatting. Paragraph formatting follows the paragraph end as in
copyRange— the Story's last paragraph is an open piece and takes the formatting of the paragraph it lands in (when appended as a new paragraph at the end, it keeps the Story's). - Inline runs
{ inline }only into a text frame's text and, 1.9, a table cell's text. The Story input limits (200,000 characters, 10,000 paragraphs) apply to one input — append several times to make the text longer.
9.19 tx.insertRows · tx.removeRows
1.6 — inserts and removes table rows. target = a table id on a spread (including inside groups) or an inline table { id: story, inline: n, path? }. The table's structure comes from doc.table.
// duplicate row 1 (the template row) once per record below the header row, then fill the cells
const r = await podolay.edit('Rows', tx => tx.insertRows(tableId, { after: 1, count: items.length, copyFrom: { row: 1 } }));
await podolay.edit('Fill', tx => r.results[0].rows.forEach((row, k) => tx.setText({ id: tableId, cell: [row, 0] }, items[k].name)));
await podolay.edit('Remove template row', tx => tx.removeRows(tableId, 1));
// append a row at the end of the body (above the footer rows) — with after: t.rows - 1 a table with footer rows gets another footer row
const t = await podolay.doc.table(tableId);
await podolay.edit('Append row', tx => tx.insertRows(tableId, { after: t.rows - t.footerRows - 1, copyFrom: { row: t.rows - t.footerRows - 1 } }));o of insertRows | Meaning |
|---|---|
after | Insert after this row (-1 = at the top) |
count | Rows to insert (1–1000). Defaults to copyFrom.count, else 1 |
copyFrom | { row, count?, table? } — repeats these rows (default 1 row) in turn, duplicating cell formatting, text, cell backgrounds, row heights, row border lines and cell borders. Without it, empty rows. 1.8 table = from another table (same number of columns) |
- Result
{ rows: [new row numbers…] }— numbers after the edit. - Merges: refused if a merged cell spans across the insertion point vertically, or if a merge reaches outside the rows to copy. If the rows to copy contain vertical merges,
countmust be a multiple ofcopyFrom.count. - Header and footer rows (as in the editor): a new row inside the header rows (
after < headerRows - 1) adds a header row; inside or below the footer rows (after ≥ rows - footerRows) adds a footer row. Rows inserted just below the header (after: headerRows - 1) or just above the footer (after: rows - footerRows - 1) are body rows. Removing footer rows reducesfooterRows. - 1.7 Rows duplicated with
copyFromkeep their cell style names (cellStylesindoc.table). Inserting and removing rows shifts the rows ofcellStyleswith them. - 1.8
copyFrom.table— copies rows of another table (a spread table id or an inline table address) with their merges, cell backgrounds, border lines, cell borders and row heights. A different number of columns is an error. For mixing rows of several template tables in one table. Cell borders (sides changed per cell) now travel too, also when copying within the same table. removeRows(target, at, count = 1)— removescountrows starting atat. Result = the number of rows removed. You can't remove every row.- A spread table goes into a table-structure Undo step; an inline table into the Undo step that changes the host story. Locked tables (for inline ones, a locked host story) are refused.
9.20 tx.autoflow(id, o)
1.6 — flows overset text (InDesign's Shift-click autoflow). While the end of the thread is overset, it inserts a page after the last frame's page and places a frame there at the same position, size and options (columns, insets, fill), threading it on. If the last frame is a primary text frame, the new page's master primary text frame is used.
await podolay.edit('Append', tx => tx.appendText(bodyId, longStory));
const r = await podolay.edit('Flow', tx => tx.autoflow(bodyId, { maxPages: 200 }));
r.results[0]; // { pages: [5, 6, 7], frames: [311, 312, 313], overflow: false }o key | Meaning |
|---|---|
master | Master of the new pages: id, name or null (none). If omitted, follows the neighbouring page |
maxPages | Maximum pages to insert (1–1000, default 100) |
- Result
{ pages, frames, overflow }— new page numbers (after the edit), new frame ids, and whether text is still overset (truewhenmaxPageswas reached or an object bigger than the frame stops the flow). - It is a page operation — snapshot Undo, the plan's page limit, not while editing masters (§9.13). Master text, a last frame on the pasteboard, title text and text on a path are refused.
- Layout is measured with the fonts loaded now — if font slices are still on their way, the flow may change slightly later.
9.21 tx.duplicate(id, o)
1.6 — duplicates an object placed directly on a spread (a group as a whole).
o key | Meaning |
|---|---|
spread | Spread to place it on (default: the original's spread) |
dx · dy | Offset in pt (default 0 — same position) |
Result { id, copies } — id = the copy's id, copies = { original id: copy id } including children inside groups. The new object is also in created.
9.22 tx.setTableStyle · tx.setCellStyle
1.7 — applies table and cell styles by name. target is the same as for doc.table.
await podolay.edit('Style tables', (tx) => {
tx.setTableStyle(tableId, 'Striped'); // the whole table — region cell styles and alternating fills too
tx.setCellStyle(tableId, [[3, 0], [3, 1]], 'Total cell'); // cells [row, col] (a cell covered by a merge means that merged cell)
tx.setTableStyle(otherId, 'Striped', { clearOverrides: true }); // values changed on cells take the style values too
tx.setCellStyle(tableId, [[0, 0]], null); // null = remove (values stay)
});- The values go into the table and cells (the same rules as table styles in the editor — values the user changed on a cell stay unless
clearOverrides). - An unknown name is an error. List the styles with
doc.tableStyles(). - Locked tables can't be changed. One undo reverts it (together with the other operations in the same
edit).
9.23 tx.mergeCells · tx.unmergeCells
1.8 — merges and unmerges cells. target is as in doc.table, and the range is [row, col, rows = 1, cols = 1].
await podolay.edit('title row', tx => tx.mergeCells(tableId, [row, 0, 1, 3])); // three cells of a row into one
await podolay.edit('unmerge', tx => tx.unmergeCells(tableId, [row, 0])); // unmerges merges meeting that cellmergeCells— merges inside the range are absorbed. A merge crossing the range is an error (widen the range or unmerge first). Result = whether it changed.unmergeCells— unmerges every merge meeting the range. Result = whether any was unmerged.- The merged cell shows the top-left cell's text (as the editor's cell merge does). Locked tables can't be changed.
10. Batches — podolay.batch
1.1 🔑 document:write → the value returned by fn
await podolay.batch('Build catalog', async (signal) => {
const r = await podolay.edit('Duplicate', tx => tx.duplicatePages({ from: 2, times: items.length }));
for (let k = 0; k < items.length; k++) {
signal.throwIfAborted(); // stop if the user cancelled
await podolay.ui.progress({ label: items[k].name, value: k + 1, max: items.length });
await podolay.edit('Fill', tx => tx.setText(r.results[0].copies[k][titleId], items[k].name));
}
});| Behavior | Details |
|---|---|
| Undo | All edits inside become one step (plugin name: name) |
| Saving | The edits inside show up on screen immediately; the whole document is saved once at the end |
| Failure | If fn throws, everything is rolled back to the state before the batch and batch rethrows the error |
| Cancel | The user clicks Cancel in the progress window → signal.aborted = true. The batch is rolled back and batch throws 일괄 작업이 취소되었습니다 ("the batch was cancelled") |
| If cancel is ignored | If the batch doesn't finish within 5 seconds, the editor rolls back by itself. After that, this plugin's edit calls are rejected until the batch ends |
| Meanwhile | The editor is blocked and the user can't edit (including Undo and shortcuts). The progress window appears only after 0.5 s. Other plugins' edit calls are rejected as well |
| Afterwards | The objects selected when the batch started are selected again if they still exist (also after a rollback) |
| Nesting | You can't call batch inside a batch |
| What remains | Image assets uploaded with assets.upload remain even after a rollback (if the document doesn't use them, they can be deleted in the gallery) |
| Time 1.6 | There is no total time limit. Editor-side API calls have no time limit either. The response time of a command (60 s) or a panel message (8 s) isn't treated as a failure while that plugin's batch is running — a command or panel-message handler may await a long batch. The editor keeps checking that the plugin is alive (if synchronous code blocks it from answering, it is turned off) |
| Screen 1.6 | The edits inside redraw the screen at most once every 250 ms (before: after every edit) — calling edit thousands of times doesn't let drawing outpace the work |
| Size | Up to 2000 ops per edit. A few dozen records per edit is recommended — too small means many round trips, too large makes a single apply long |
| Memory | Undo keeps two document snapshots, at the start and at the end (document JSON size × 2) — regardless of how many edits ran inside |
ui.progress({ label?, value?, max? }) 1.1 🔓 — Only inside a batch. The progress window's text (200 characters) and bar. Without max the bar is indeterminate; with value/max it shows something like 3 / 10.
signal is { aborted: boolean, throwIfAborted(): void }.
Inside a batch you can't call ui.pickFiles or selection.setText — pick files before the batch.
11. UI — podolay.ui · podolayUI in panels
11.1 ui.toast(message, kind)
1.0 🔓 — An editor notification. kind = 'info' (default) · 'success' · 'error'. It is prefixed with the plugin name and truncated at 300 characters.
11.2 Panel HTML and podolayUI
A panel is a floating window showing the html from contributes.panels. It runs under the same sandbox rules as the plugin — no network (even if the plugin has network permissions, the panel doesn't), inline scripts and styles only, images as data: URLs. The editor's dark default styling (text, buttons, inputs) is applied first, and your panel CSS can override it.
A panel can't call the editor API directly; it talks only to its own plugin's main.js:
// inside panel.html
podolayUI.postMessage({ type: 'apply', value: 3 }); // to ui.onMessage in main.js
podolayUI.onMessage((msg) => { … }); // from ui.postMessage in main.js- 1.6 Reading files in a panel —
<input type="file">(multiple,webkitdirectory), drag and drop andFileReaderall work inside the panel (asandbox="allow-scripts"iframe). Send the bytes to main.js withpodolayUI.postMessage(msg, transfer?)—transfer= an array ofArrayBuffers to hand over (moved, not copied; they become empty in the panel).ArrayBuffers in a message don't count toward the 4-million-character limit (no separate cap — only memory). To pick files without a panel, useui.pickFiles.<input type="file" id="f" webkitdirectory> <script> document.getElementById('f').onchange = async (e) => { const files = await Promise.all([...e.target.files].map(async (f) => ({ path: f.webkitRelativePath || f.name, bytes: await f.arrayBuffer() }))); podolayUI.postMessage({ type: 'files', files }, files.map((x) => x.bytes)); }; </script> - Keys stay inside the panel — keys pressed while the panel has focus (⌘Z, ⌘C, Delete, …) never reach the editor (the panel is a separate sandboxed iframe, so the editor can't receive them). ⌘Z in an input is undone by the browser in that input only and never mixes with the document's Undo. Document Undo works again once the user clicks the canvas.
11.3 ui.showPanel(id) · ui.closePanel(id)
1.0 — showPanel 🔑 ui: opens the panel (does nothing if it's already open). closePanel 🔓: closes it. When the user closes a panel, the panelClose event fires.
11.4 ui.postMessage · ui.onMessage
1.0 🔓
await podolay.ui.postMessage('main', { count: 3 }); // the panel must be open (otherwise an error)
podolay.ui.onMessage(async (msg, panelId) => { … }); // the panel's podolayUI.postMessage- Messages are sent by structured clone (JSON-like values). At most 4 million characters per message — 1.6
ArrayBuffers inside don't count. The editor hands theArrayBuffers of panel messages andpickFilesresults to the plugin without copying them. - Don't
awaitlong-running work in anonMessagehandler — the editor applies the 8-second limit to handler calls too. Run long work separately, e.g.void doWork(). 1.6 While that plugin'sbatchis running, going past 8 seconds isn't treated as a failure — a handler may start abatchandawaitit.
11.5 ui.pickFiles(o)
1.6 🔓 → PickedFile[] — reads files or a folder from the user's computer. Browsers open a file picker only from a click by the user, so the worker can't open one itself — the editor shows a picker window labelled with the plugin's name, and the user picks with its button. Only the files picked reach the plugin (which is why no permission is needed).
const files = await podolay.ui.pickFiles({ directory: true }); // a whole folder — photo folders, data files
// [{ name: 'a.jpg', path: 'photos/a.jpg', type: 'image/jpeg', size: 182733, bytes: ArrayBuffer }, …]
const csv = await podolay.ui.pickFiles({ accept: '.csv,.tsv' });
if (csv.length) rows = parse(new TextDecoder().decode(csv[0].bytes));o key | Meaning |
|---|---|
accept | Same form as <input accept> ('.csv', 'image/*', up to 500 characters) |
multiple | true = several files |
directory | true = pick a folder (every file inside it) |
| Returned key | Meaning |
|---|---|
name | File name |
path | For a folder, the relative path starting at the folder's name (photos/2024/a.jpg); otherwise the same as name |
type | The MIME type the browser knows ('' if unknown) |
size | Size in bytes |
bytes | ArrayBuffer — can be passed straight to assets.upload |
- Cancelling or picking nothing returns
[]. - Up to 5000 files and 1 GB in total per call (more is an error). An error if another picker is open or a
batchis running — pick files before thebatch. - A plugin with a panel can also pick with the panel's
<input type="file">(§11.2).
12. Storage and license — podolay.storage · podolay.license
1.0 🔑 storage
const s = (await podolay.storage.get()) ?? {};
await podolay.storage.set({ ...s, lastRun: Date.now() });- One JSON value (up to 64 KB) per account × plugin. It is independent of documents — use it for values that belong to the person, such as user settings and API keys.
- In development mode (
pluginDev) nothing is written to the server; the value stays in that window only. - Never put API keys in
main.js(it's distributed code anyone can read) — store the user's own key, entered in a panel, here instead. - 1.8
storage.getFrom(pluginId)— reads the storage of another plugin of yours listed in the manifest'sdataFrom, read only (moving a free edition's presets to Pro).
12.1 license.status()
1.5 🔓 → LicenseStatus
const lic = await podolay.license.status();
// { licensed: true, source: 'purchase', trialEndsAt: null }
if (lic.source === 'trial') podolay.ui.toast(`Trial until ${new Date(lic.trialEndsAt).toLocaleDateString()}`);| Key | Type | Description |
|---|---|---|
licensed | boolean | true if bought (including a license given by PodoLay) or the plugin is free, false during a trial |
source | string | free (free plugin) · purchase (bought) · grant (given by PodoLay) · trial (on a trial) · dev (development mode) |
trialEndsAt | string · null | When the trial ends (ISO) — if a trial was ever started |
- Paid plugins are sold by PodoLay at a price tier ($10 up to, but not including, $1,000). One purchase gives the account a permanent license — upgrading to new versions costs nothing more, and it works regardless of plan (even on the Free plan). If the plugin has trial days (0–30, 0 = no trial), it can be used for that long from the first install (once per account).
- The editor loads a paid plugin only for people who bought it or are on a trial — if the plugin is running, it may be used. Use this value for what to show, such as a trial notice. When the trial ends the plugin is no longer loaded from the next time the editor opens, and objects already made stay as their saved fallback images.
- As of 2026-10, only plugins made by PodoLay can be sold (the price row in the developer console). Sales by other developers open after confirmation with the payment provider.
- In development mode and the sandbox it is always
{ licensed: true, source: 'dev' }. In the mock editor, change it withcreateHarness(m, { license })orh.license.
13. Uploading images — podolay.assets
assets.upload(bytes, opts)
1.1 🔑 document:write → { asset: string, width: number, height: number, sha256?: string, reused?: boolean }
const res = await fetch('https://cdn.example.com/p/1.jpg'); // network permission
const { asset } = await podolay.assets.upload(await res.arrayBuffer(), { name: 'p1', type: 'image/jpeg' });
await podolay.edit('Photo', tx => tx.setImage(frameId, asset));| Argument | Description |
|---|---|
bytes | ArrayBuffer · Uint8Array · Blob — up to 30 MB |
opts.type | image/png · image/jpeg · image/gif · image/tiff · image/webp (converted to PNG) |
opts.name | File name (without extension, 80 characters) — shown in the gallery |
opts.reuse | 1.6 'name' = if the document has an asset with the same file name and the same byte size, use it instead of uploading · false (default) |
- Creates an image asset of this document under the user's account — the plan's storage limit and the server's format checks are the same as for a normal upload, and the original goes to print unchanged.
- Use the returned
assetwithtx.setImageortx.create({ kind: 'image' }).widthandheightare the original's pixel dimensions. - Images drawn with
OffscreenCanvascan be uploaded too, viacanvas.convertToBlob(). - 1.6 The same content isn't uploaded twice — the result carries the content hash
sha256(hex) andreused. If the same content was already uploaded in this window (while the editor is open), that asset is returned (reused: true).reuse: 'name'looks for an asset with the same name (name+ extension) and the same size among the document's assets on the server — for running the same photo folder again on a reopened document. To deduplicate by content across windows, keep asha256 → assetmap in the document data (tx.setDocData) and look it up first — server assets have no hash.const known = (await podolay.doc.getData())?.hashes ?? {}; // { sha256: asset } const used = new Set((await podolay.assets.list()).map((a) => a.asset)); for (const f of files) { const hash = await sha256(f.bytes); // compute it yourself before uploading (sha256 below) let asset = known[hash]; if (!asset || !used.has(asset)) ({ asset } = await podolay.assets.upload(f.bytes, { name: f.name.replace(/\.[^.]+$/, ''), type: f.type, reuse: 'name' })); known[hash] = asset; } await podolay.edit('Photo map', (tx) => tx.setDocData({ hashes: known })); async function sha256(bytes) { // the same hex as the upload result's sha256 const d = await crypto.subtle.digest('SHA-256', bytes); return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, '0')).join(''); }crypto.subtleexists from editor 2.14.14 (digestonly) — to support older editors, setminEditor: "2.14.14"or compute the hash in plain JS.
assets.list(q)
1.6 🔑 document:read → AssetInfo[] — this document's image assets (the document's image assets on the server, up to 5000).
const all = await podolay.assets.list(); // [{ asset, name, width, height, size, used }, …]
const one = await podolay.assets.list({ name: 'a.jpg' }); // only that file name| Key | Meaning |
|---|---|
asset | Asset path — use it with tx.setImage and tx.create({ kind: 'image' }) |
name | File name (with extension) — q.name matches it exactly |
width · height | Original pixels |
size | Size in bytes (null if unknown) |
used | Whether the document currently uses it |
- Assets in the list can be used by
txin this window (even if you didn't upload them). - The sandbox editor has no server, so it lists only what was uploaded in this window and what the document uses.
14. Package files — podolay.package
1.4 🔓 — reads files inside the plugin package. Keep large data such as dictionaries and conversion tables out of main.js and read it when needed.
const table = await podolay.package.json('data/ipa.json'); // → any
const words = await podolay.package.text('data/words.tsv'); // → string (UTF-8)
const bytes = new Uint8Array(await podolay.package.read('data/dict.bin')); // → ArrayBuffer| Rule | Details |
|---|---|
| Path | A relative path inside the package (data/x.json; a leading ./ is fine). No leading /, .. or \; up to 200 characters |
| Size | Up to 20 MB per file (the whole package: 20 MB zip, 40 MB unpacked) |
| File types | Only what a package may hold — js json html css svg png jpg jpeg webp woff2 txt md bin csv tsv (bin · csv · tsv were added with 1.4) |
| Development mode | The dev server serves them straight from the plugin folder — edited files apply from the next call |
| Mock editor | Reads from the plugin folder (the folder given to loadPlugin) by the same rules |
- Code can't be
imported — the worker loads onlymain.jsand the CSP blocks other scripts. Bundle code intomain.jswith a build step and read only data separately. - Keep what you read inside the plugin so it isn't read again —
let table; … table ??= await podolay.package.json('data/ipa.json');.
15. Network — fetch · WebSocket · EventSource
These three exist only if the manifest has a network:<host> permission (otherwise they're undefined).
"permissions": ["network:api.example.com", "network:*.cdn.example.com", "network:api.example.com:8443"],
"privacyUrl": "https://example.com/privacy"| Rule | Details |
|---|---|
| Hosts | Public names only — no IP addresses, localhost or private names (.local, .internal, etc.). *. covers one leading level only; broad patterns like *.com aren't allowed. Up to 10 |
| Schemes | https and wss only (development mode also allows http://localhost and 127.0.0.1) |
| Requests | No cookies, no referrer, no cache. Origin is null — **the remote server must allow * or null via CORS** |
| URLs | Absolute URLs only. For a URL that isn't allowed, fetch rejects and WebSocket/EventSource throw a SecurityError |
| Logging | Each request is logged in the log window as [network] GET host → status (paths and bodies are never logged) |
| Never present | XMLHttpRequest, importScripts and creating workers — never available (you can't download and run code) |
| Panels | Panels have no network access — main.js fetches the data and passes it to the panel |
| Databases | This is browser code, so it can't connect to a database directly — put an HTTP API in front of the database |
16. Data types
16.1 Colors
Writing (PodolayColor) | Meaning |
|---|---|
'#rgb' · '#rrggbb' | RGB |
{ cmyk: [c, m, y, k] } (0–100) | CMYK — goes to print as these ink values |
{ gray: k } (0–100) | K ink only |
When reading (Graphic.fill, etc.), values may also be spot colors { spot: 'name', tint: 0–100, rgb: '#rrggbb' }, or null if there's no fill.
16.2 Localized strings
Either a single string (for all languages) or { "en": "…", "ko": "…" }. The editor uses the value for the UI language, falling back to en, and then to any value. Each value is up to 200 characters (2000 for descriptions).
16.3 Local names · Frame — rectangles
- Local name: starts with an ASCII letter,
[A-Za-z0-9_-], up to 64 characters (graphic types; tool, command and panel ids; Inspector keys). - Frame:
{ x, y, width, height }in pt. Spread coordinates for objects.
16.4 Graphic — objects
{
id: 101, kind: 'text',
frame: { x: 40, y: 60, width: 200, height: 80 }, // spread coordinates (before rotation)
rotation: 0, skew: 0, opacity: 1, visible: true, locked: false,
fill: '#ffffff' | {cmyk} | {gray} | {spot, tint, rgb} | null,
stroke: { color, width } | null,
pluginData: …, // this plugin's tag (null if none)
plugin: { id, type, data? }, // plugin objects only — data only if it's your own plugin's
text: '…', // objects with text — the whole text (unformatted)
image: '/media/…', // objects with a placed image — its asset path (group children and inline objects too, from editor 2.14.13)
children: [ … ], // groups — from doc.graphics and doc.graphic
}kind = rectangle ellipse polygon line path polyline text titletext (title text) image table group plugin other.
16.5 DocInfo · TextStatus · StyleInfo
16.6 Story — text
'First paragraph\nSecond paragraph' // a string = one paragraph per line
[
{ para: 'Title', text: 'Spring collection' }, // a single run
{ para: '$base', runs: [
{ text: 'List price ' },
{ text: '$12.00', char: 'Emphasis', size: 14, color: { cmyk: [0, 100, 100, 0] }, bold: true },
] },
]| Paragraph key | Description |
|---|---|
para | Paragraph style name or id. The built-in "Basic" style is '$base' |
runs | Array of runs (up to 1000) |
text | Shorthand for a single run |
align | 1.6 Paragraph alignment — left · center · right · justify · justifyCenter · justifyRight · justifyAll · towardsSpine · awayFromSpine |
leading | 1.6 Leading — a multiple of the font size (0–20, e.g. 1.6) |
spaceBefore · spaceAfter | 1.6 Space before and after the paragraph, pt (−5000–5000) |
indentLeft · indentRight · indentFirst | 1.6 Left, right and first-line indent, pt (−5000–5000) |
| Run key | Description |
|---|---|
text | Text (line breaks become spaces — split paragraphs with the array) |
inline | 1.2 An inline object — §16.8. Don't use text in this run |
char | Character style name or id. '$base' · '$none' |
font | Font name |
size | pt (0.5–2000) |
color | Color |
bold · italic | boolean |
tracking | 1.6 Tracking — 1/1000 em (−1000–10000) |
hScale · vScale | 1.6 Horizontal and vertical scale, % (1–1000, default 100) |
baselineShift | 1.6 Baseline shift, pt (+ = up, −1000–1000) |
underline · strike · smallCaps | 1.6 Underline · strikethrough · small caps (boolean) |
position | 1.6 'super' (superscript) · 'sub' (subscript) · 'normal' |
- Formatting order: base → paragraph style (+ that paragraph style's character style) → 1.6 the paragraph's direct formatting → the run's character style → the run's direct formatting.
- 1.6 The paragraph's
align…indentFirstand the run'stracking…positionare accepted bysetText,create({ kind: 'text', story }),appendText,replaceRange({ runs },{ story }),formatRange(run formatting only) andtext.measure.doc.textincludes run formatting only when it differs from the default, and paragraph formatting only when it differs from the paragraph style — pass a Story you read back unchanged and the formatting stays. - Base: for
tx.create, the document's "Basic" style; fortx.setTextandtext.measure({like}), the first paragraph and first character formatting of that text. - An unknown style name makes the whole edit fail (so text is never filled in with the wrong formatting).
- One Story input can have up to 200,000 characters and 10,000 paragraphs — this is not a limit on the text itself. For longer text, append several times with
appendTextorcopyRange, and when it overflows, thread frames onto new pages withautoflow. - Combining marks and phonetic symbols (editor 2.14.7 or later): a combining mark in the text (such as the U+0301 acute accent, Unicode categories Mn and Me) is drawn on top of the preceding letter, at the font's mark position (GPOS) — on screen and in PDF. There is no need to normalize to NFC first; letters with no precomposed form, such as
ə́(U+0259 U+0301) orɔ́, can be written as they are. Two stacked marks (ế= e U+0302 U+0301) work too. IPA, extended Latin, spacing modifier letters (ː,ˈ), Greek and Cyrillic characters missing from the default font (Noto Sans KR) are drawn with Noto Sans (the same font on screen and in PDF). To use another IPA font, set it withfontor a character style — since 2026-10-27 the built-in fonts include the phonetic fontsCharis(SIL; Regular, Italic, Medium, SemiBold, Bold and their italics, 8 faces; named 'Charis SIL' up to version 6) andDoulos SIL(Regular only). Both are serif fonts with the full IPA set and mark positioning (GPOS mark), so a whole transcription is drawn in one font (screen and PDF). The font list comes from the server, but the editor only downloads the font file for text whosefontalone was changed from 2.14.10 on — in earlier versions such text is drawn with a fallback font until the font is picked once in the font picker or the layout is reopened (PDF uses the font from the start). Plugins relying on these fonts should declare"minEditor": "2.14.10". In fonts that cannot place marks on Latin letters, such as the default font (Noto Sans KR and other CJK fonts), only the letters followed by a combining mark are drawn with Noto Sans (Noto Serif for serif fonts) —ɑ́,á̀,ṍ(editor 2.14.8 or later). In editor 2.14.6 and earlier the mark is pushed aside or drawn over the next letter, and in 2.14.7ɑ́and two stacked marks are still misplaced in the default font — plugins that rely on this notation should declare"minEditor": "2.14.8"in the manifest.
16.7 FindQuery
§7.8.
16.8 Inline — inline objects
1.2 — The object given in a Story run { inline } or to tx.insertInline. It takes one of the forms below.
{ kind: 'image', asset, width: 40, height: 30, fit: 'fit' } // a new object
{ kind: 'plugin', type: 'qr', data: { url }, width: 24, height: 24 } // your own graphic (render is called to produce the drawing)
{ graphic: 205 } // moves an object from the spread into the text
{ index: 1 } // tx.setText only — keeps inline object #1 of the text as it was before| Form | Fields |
|---|---|
| New object | kind (plugin, rectangle, ellipse, line, text, image) + the fields of that kind in tx.create, with width and height (pt, greater than 0 and at most 5000) instead of frame, plus rotation? (degrees) and pluginData? (this plugin's tag — find it again with doc.inlines) |
{ graphic: id } | Moves an object placed directly on the spread (a group as a whole) — it leaves the spread and loses its id. Locked objects, threaded text frames and the text object itself aren't allowed. Only once per edit. Change its size, fill or tag beforehand with earlier ops in the same edit (tx.set, setPluginData) |
{ index } | tx.setText only — that inline object of the text before the replacement (just pass back the run returned by doc.text) |
- Inline objects sit in a line like characters — the bottom edge sits on the baseline, and a tall object makes its line taller. An object larger than the column becomes overset text.
- 1.3
baseline(pt, from 0 up to the height) = the distance from the object's bottom edge up to the text baseline. The object is lowered by that much so the baseline crosses it, and the part below the baseline counts toward the line depth (selection and click area cover the whole object). Use it for formulas with subscripts and denominators. Give it in the new-object form and intx.setInline. - A display list may draw outside the frame and isn't clipped — but selection, click area and line height only count the frame.
- You can't put another inline object inside the text of an inline text object (
kind: 'text'). - Once placed, the user can double-click an inline object in the editor to edit it in place.
16.9 TextTarget — text targets
1.4 — the address of a text, used by the APIs that take text (doc.text · doc.texts · tx.setText · replaceText · replaceRange · formatRange · 1.6 copyRange · appendText · selection.setText) and by the id of the command's text argument, text.selection and textSelectionChange.
| Form | Text it points to |
|---|---|
12 | A text object id — any frame of a threaded story means the whole story (since 1.1) |
{ id: table, cell: [row, col] } | A table cell (zero-based; cells covered by a merge are not allowed) |
{ id: story, inline: n } | An inline text frame — the index of doc.inlines |
{ id: story, inline: n, cell: [row, col] } | A cell of an inline table |
{ id: story, inline: n, path: [child index, …] } | 1.6 A text frame inside an inline group (§16.10) |
{ id: story, inline: n, path, cell: [row, col] } | 1.6 A cell of a table inside an inline group |
- The object forms need
apiVersion1.4 or later (otherwise the error '… 의 글 자리 { id, cell } · { id, inline } 는 apiVersion 1.4 이상에서 씁니다'). - Changing the text of an inline object is one Undo step that changes the host story. Locking follows the host story (for a table cell, the table).
- Inline objects can't be inserted into the text of table cells or inline objects (the editor can't either yet).
doc.textslists every text target. pathneedsapiVersion1.6 or later.
16.10 InlineTarget — inline object addresses
1.6 — inline objects have no id, so they are addressed by host story and ordinal. path goes further into the children of an inline group (and of groups inside it).
{ id: 12, inline: 0 } // inline object #0 of story 12
{ id: 12, inline: 0, path: [1] } // child #1 of that inline group
{ id: 12, inline: 0, path: [1, 0] }// child #0 of that child (a group)| Accepted by | What it does |
|---|---|
doc.graphic | Reads the object (no id; group children carry path) |
doc.table · tx.insertRows · tx.removeRows | An inline table |
tx.set · tx.setPluginData · tx.setImage | Changes the object |
| Everything that takes a text target (§16.9) | A text frame inside an inline group { id, inline, path } · a table cell { id, inline, path, cell } |
1.8 Objects in text outside text frames — { in: text target, inline, path? }. in is a text target (a table cell { id, cell }, an inline table cell { id, inline, cell } …), inline is the index from doc.inlines(that text target). Every place in the table above takes it. { id, inline, cell } still means 'a cell of an inline table' — an object inside a cell is { in: { id, cell }, inline }.
{ in: { id: 30, cell: [1, 0] }, inline: 0 } // inline object #0 in the text of cell [1, 0] of table 30
{ in: { id: 30, cell: [1, 0] }, inline: 0, path: [0] } // child #0 of that inline group (the picture frame)inline= theindexofdoc.inlines;path= use thepaththatdoc.inlinesanddoc.graphicput on group children (order inside the group, zero-based, up to 32 levels).- The editor builds the object from the host story's anchor data, changes it, and writes it back with one command that changes the host story — one Undo step; locking follows the host story. Changing the size re-measures the group frame and the inline slot, and the text reflows.
- Ordinals count in the text as changed by earlier ops of the same
edit(inserting or removing objects before it shifts them). - Text frames inside a group moved into text with
{ graphic: id }lose their ids, so use this address for them. - Inline object data in the document JSON (sandbox
raw) also holds a numeric id, but nothing uses it — the editor builds the object anew from the data on every layout. Inline objects duplicated withtx.copyRangemay carry the same id as the original; that's fine.
17. Display list
The drawing returned by render and by tool previews. render uses object-local coordinates (origin = the frame's top-left); tool previews use spread coordinates.
17.1 Items
t | Fields | Fill · stroke |
|---|---|---|
rect | x, y, w, h | fill stroke opacity |
ellipse | cx, cy, rx, ry | fill stroke opacity |
line | x1, y1, x2, y2, stroke (required) | opacity |
path | d (SVG path), rule?: 'nonzero'·'evenodd' | fill stroke opacity |
group | items | opacity (multiplied with the items' opacity) |
use | ref (definition name), x?, y?, scale? or m?: [a, b, c, d, e, f] 1.3 | fill stroke opacity (see 16.4) |
[
{ t: 'rect', x: 0, y: 0, w: 100, h: 50, fill: '#ff0000', stroke: { color: { cmyk: [0, 0, 0, 100] }, width: 1, dash: [3, 2] } },
{ t: 'ellipse', cx: 50, cy: 25, rx: 20, ry: 10, fill: { gray: 30 } },
{ t: 'line', x1: 0, y1: 0, x2: 100, y2: 50, stroke: { color: '#000', width: 0.5, cap: 'round' } },
{ t: 'path', d: 'M0 0 L10 0 Q20 5 10 10 Z', fill: '#09f', rule: 'evenodd' },
{ t: 'group', opacity: 0.5, items: [ … ] },
]17.2 Fill and stroke
| Key | Type |
|---|---|
fill | Color — open paths are not filled |
stroke | { color, width, cap?: 'butt'·'round'·'square', join?: 'miter'·'round'·'bevel', dash?: [number…] (up to 16) } |
opacity | 0–1 |
17.3 Path d
SVG path commands M L H V C S Q T A Z (uppercase = absolute, lowercase = relative). Q and T are converted to equivalent cubic curves, and arcs (A) to a series of cubic curves (compact flags are fine: a5 5 0 011 1). Each M starts a new subpath.
17.4 Reusable definitions (defs · use)
1.3 — To draw the same shape many times (e.g. glyph outlines in a formula), return { items, defs }. defs maps a name to an array of items, and { t: 'use', ref } places it (like SVG <symbol> / <use>). The drawing is stored in this compact form, so documents stay small.
- Names: start with a letter; letters, digits,
_,-; up to 32 characters. - Transform:
x, y(translate) andscale(> 0, uniform), or a 2×3 matrixm(x' = a·x + c·y + e, y' = b·x + d·y + f).mcannot be combined withx/y/scale. - Paint: only path/rect/ellipse items in the definition that have neither
fillnorstroketake theuse'sfill/stroke; items with their own paint keep it.opacityis multiplied. - Stroke width: a stroke given on
useis measured where theusesits (its ownscale/mis not applied). Strokes of items inside the definition scale with the transform. - Definitions may
useother definitions (up to 4 levels). Cycles are rejected. - Editors older than 2.13 can't draw a drawing containing
use(the object stays and shows as an empty box) — they don't load 1.3 plugins anyway.
{
defs: { x: [{ t: 'path', d: 'M0 0 L6 0 L6 8 Z' }] },
items: [
{ t: 'use', ref: 'x', x: 10, y: 20, fill: '#000' },
{ t: 'use', ref: 'x', x: 30, y: 20, scale: 0.7, fill: { cmyk: [0, 100, 0, 0] } },
],
}17.5 Limits
| Item | Limit |
|---|---|
| Number of items (incl. groups and definitions; a use counts as 1) | 5000 |
| Group nesting depth | 8 |
d of a single path | 200,000 characters |
Total of all path ds | 1,000,000 characters |
Number of dash values | 16 |
Number of definitions (defs) | 1000 |
use nesting | 4 levels |
| Shapes after expanding all uses | 50,000 |
Total path d after expanding all uses | 5,000,000 characters |
Definition paths are counted once (the 1,000,000-character total is measured on the stored drawing). A drawing that exceeds a limit or is malformed is rejected, and the reason is logged.
17.6 In other outputs
| Where | The display list |
|---|---|
| Screen · reader mode | The saved drawing as is (stretched if the size has changed) |
| The same drawing as vector paths — colors are printed as specified (CMYK, K) | |
| IDML (InDesign) | As a group of paths. InDesign has no even-odd fill, so holes may be filled in |
18. Limits at a glance
| Item | Limit |
|---|---|
| Answer to a single call (render, tool, event, panel message) | 8 s |
A single command (commands) | 60 s (1.6 not a failure while that plugin's batch is running — nor is the 8 s for panel messages) |
A whole batch · editor-side API calls | No time limit |
Plugin start (top level of main.js) | 8 s |
| Hang detection (liveness check after a timeout) | 2 s more — turned off if there's no answer |
Finishing a batch after cancel | 5 s |
| A single message | 4 million characters (1.6 ArrayBuffers don't count — only memory) |
Ops per edit | 2000 |
data · pluginData · setDocData value | 256 KB JSON |
storage | 64 KB JSON |
assets.upload | 30 MB |
| Story (one input — no limit on the text itself) | 200,000 characters · 10,000 paragraphs · 1000 runs per paragraph |
Inline object size · one insertInline op | 5000 pt · 1000 objects |
doc.find results | Default 1000, max 5000 |
selection.set | 1000 |
New pages per edit | 1000 (plus the plan's page limit) |
addPages.count · duplicatePages.times | 200 · 500 |
| Network hosts | 10 |
| Tools · panels · other contributes | 10 · 10 · 50 |
| Package zip | 20 MB · 500 files · 40 MB unpacked (code, guide and samples together) |
package.read | 20 MB per file · 200-character path |
doc.texts results | 20,000 |
One ui.pickFiles | 5000 files · 1 GB in total |
assets.list results | 5000 |
insertRows.count · autoflow.maxPages | 1000 · 1000 (default 100) |
Screen redraws inside a batch | Once every 250 ms |
| Plugins per developer · versions per plugin | 50 · 100 |
19. Errors and timeouts
- When an editor call fails, it rejects with an
Error. Messages are in Korean and say what was wrong and why, for example권한이 없습니다: document:write (매니페스트 permissions 에 넣으세요)("permission missing: document:write — add it to the manifest's permissions"),잠긴 개체는 지울 수 없습니다: 101("locked objects can't be deleted: 101"),단락 스타일이 없습니다: 제목("no such paragraph style: 제목"). editandbatchare all or nothing.- Timeouts: when the editor calls the plugin (render, tools, commands, events, panel messages) and gets no answer in time, only that call is treated as failed. The editor then asks "are you alive?", and if there's no answer within 2 seconds — meaning synchronous code is blocking — it turns the plugin off and notifies the user (reopen the editor to turn it back on). Split heavy computation into small pieces with
awaits in between. 1.6 Commands and panel messages keep waiting past their time while that plugin'sbatchis running, instead of failing (the liveness check continues). Abatchitself and editor-side API calls have no time limit. - Error locations: errors from plugin code show the first
main.jslocation —command boom: TypeError: … (com.example.qr/main.js:5:12),Uncaught ReferenceError: … (com.example.qr/main.js:8). Syntax errors appear as start failures with(…/main.js:line). - Log window: in development mode, editor › Plugins menu › Log: plugin name. It collects
console.*output, uncaught errors, failed calls and network requests (the same content also goes to the browser console). - Developer tools: plugin code runs in a worker inside a sandboxed iframe. In Chrome DevTools, find
<plugin id>/main.jsunder Sources to set breakpoints, and switch the Console's execution context to that worker to callpodolaydirectly (await podolay.doc.info()). Panels are separate iframes. - Checking without the editor:
node tools.mjs check <folder>— checks the manifest, definitions,renderand permissions using the editor's rules. Logs are also printed in the development server's terminal, and the editor reloads the plugin when you change a file. - Mock editor: unit-test commands,
editandbatchin Node with the SDK'spodolay-mock.mjs(it emulates the rules in this document; text composition is approximate). - Sandbox editor:
node tools.mjs dev <folder> --open— opens the real editor without signing in, on a new document that saves nothing to the server. Drive it from another terminal withtools.mjs run <command>,state,shot <page>(PNG) andtest <folder>(the sametest.mjs, in the real editor). - For the full debugging guide (checks, terminal logs, the log window, developer tools, common errors and how to fix them, the mock editor), see README.md §8 Debugging.
20. Changes between versions
| Version | Added |
|---|---|
| 1.0 | Graphics, tools, commands, shortcuts, panels, Inspector fields, doc.info/graphics/graphic/raw/getData, selection, edit (create · set · remove · setData · setPluginData · setDocData), ui, storage, events, display lists |
| 1.1 | Network permissions (network:), Story text (doc.styles/text/find, tx.create text story · setText · replaceText), images (assets.upload, tx.create image · setImage), pages (addPages · duplicatePages · removePages · applyMaster, master · label · masters in doc.info), text flow (link · unlink · doc.textStatus · text.measure), batch · ui.progress, results in the edit result, arc (A) curves |
| 1.2 | Inline objects (inline anchors): Story run { inline }, tx.insertInline · removeInline, doc.inlines, object runs in doc.text; context menus: manifest menus[].context (header · object · text · canvas) · when (kind · type · single), the arg argument of commands invoked from a context menu. Added in editor 2.13 (same API version): the inline-object context menu with arg.inline, selectionChange.inline, documentChange.origins, the dataChange event |
| 1.3 | Natural size (graphics[].autoSize · size(data, frame)), Inspector field options live · hideInline, tx.setInline · tx.replaceRange, baseline of inline objects, defs · use in display lists |
| 1.4 | Commands while editing text (header menus and shortcuts also get the text argument; editing reopens afterwards), text.selection, the textSelectionChange event, selection.setText, the ctrl shortcut modifier (⌃ on Mac), tx.formatRange and { runs } in tx.replaceRange, text targets (table cells { id, cell }, inline objects { id, inline }) and doc.texts, podolay.package.read · text · json (package file types gain bin · csv · tsv). Also in editor 2.14.7 (independent of the API version): combining marks drawn on top of the preceding letter; IPA, extended Latin, Greek and Cyrillic characters missing from the default font drawn with Noto Sans — §16.6 |
| 1.5 | podolay.license.status() — licenses of paid plugins (price tiers, trials, permanent licenses, independent of plan) (editor 2.14.12 or later) |
| 1.6 | For database publishing (editor 2.14.12 or later): tx.copyRange (duplicate text with its formatting), tx.appendText and { story } in tx.replaceRange (insert whole paragraphs), inline object addresses { id, inline, path? } (tx.set · setPluginData · setImage · doc.graphic · doc.table · text targets; path on group children in doc.inlines and doc.graphic), tables doc.table · tx.insertRows · tx.removeRows, tx.autoflow, tx.duplicate, podolay.ui.pickFiles and podolayUI.postMessage(msg, transfer) in panels, assets.list and reuse · sha256 · reused in assets.upload, Story run formatting (tracking · hScale · vScale · baselineShift · underline · strike · position · smallCaps) and paragraph formatting (align · leading · spaceBefore · spaceAfter · indent*). Editor side (independent of the API version): no command or panel-message timeout while a batch runs, screen redraws once every 250 ms inside a batch, copies of duplicatePages include group children |
| 1.7 | Table and cell styles: doc.tableStyles(), tableStyle · cellStyles in doc.table, tx.setTableStyle · tx.setCellStyle (editor 2.16.2 or later) |
| 1.9 | Table cell text takes inline objects too — when the text target is a table cell ({ id, cell }, { id|in, inline, path?, cell }), inline runs in setText, appendText and replaceRange ({ story }, { inline }) and ranges with inline objects in copyRange go in. Text frames inside inline objects still can't. An object wider than the cell's text width doesn't fit on a line (editor 2.18.0 or later) |
| 1.8 | Inline objects in table cells { in: text target, inline, path? } · doc.inlines(text target), tx.mergeCells · tx.unmergeCells, copyFrom.table in insertRows (from another table, with cell borders), manifest dataFrom (pluginDataFrom · doc.getData(from) · doc.find({ from }) · storage.getFrom) (editor 2.17.2 or later). Editor side (any version): doc.texts also lists text frames inside inline groups as { id, inline, path }, development log lines up to 200,000 characters (longer ones end in …[+N chars]) |
The API is additive only — 1.0 plugins run unchanged in a 1.1 editor. An older editor won't load a plugin that uses 1.1 features (judged by the manifest's apiVersion). If you need a specific editor feature, set minEditor.