PodoLay

PodoLay 플러그인 SDK

SDK 1.9.0 · API 1.9 · JavaScript 로 편집기에 기능을 더합니다

PodoLay 플러그인 API 레퍼런스 (Plugin API 1.9)

플러그인 main.js 에서 쓸 수 있는 모든 것을 한곳에 모은 참조 문서다. 처음이라면 README.md(시작하기·개발 흐름·검수 기준)부터 읽는다. 타입 정의 podolay-plugin.d.ts 와 1:1 로 같다 — 에디터에서 /// <reference path="./podolay-plugin.d.ts" /> 를 쓰면 자동완성이 된다.

표기

표시뜻
1.0API 1.0 부터 — 모든 플러그인
1.1API 1.1 부터 — 매니페스트에 "apiVersion": "1.1" 을 적은 플러그인만 (1.0 이면 오류)
1.2API 1.2 부터 — 매니페스트에 "apiVersion": "1.2" 를 적은 플러그인만
1.3API 1.3 부터 — 매니페스트에 "apiVersion": "1.3" 을 적은 플러그인만 (편집기 2.13 부터)
1.4API 1.4 부터 — 매니페스트에 "apiVersion": "1.4" 를 적은 플러그인만 (편집기 2.14.7 부터)
1.5API 1.5 부터 — 매니페스트에 "apiVersion": "1.5" 를 적은 플러그인만 (편집기 2.14.12 부터)
1.6API 1.6 부터 — 매니페스트에 "apiVersion": "1.6" 을 적은 플러그인만 (편집기 2.14.12 부터)
1.7API 1.7 부터 — 매니페스트에 "apiVersion": "1.7" 을 적은 플러그인만 (편집기 2.16.2 부터)
1.8API 1.8 부터 — 매니페스트에 "apiVersion": "1.8" 을 적은 플러그인만 (편집기 2.17.2 부터)
1.9API 1.9 부터 — 매니페스트에 "apiVersion": "1.9" 를 적은 플러그인만 (편집기 2.18.0 부터)
🔑 document:read매니페스트 permissions 에 그 권한이 있어야 한다 (없으면 호출이 권한이 없습니다 로 실패)
🔓권한이 필요 없다
→ TPromise<T> 로 돌려준다 — 편집기와 말하는 호출은 모두 비동기다

목차

  1. 실행 환경
  2. 권한
  3. 매니페스트 podolay-plugin.json
  4. podolay 한눈에 보기
  5. 정의 — graphics · tools · commands
  6. 이벤트 — podolay.on
  7. 문서 읽기 — podolay.doc · podolay.text
  8. 선택 — podolay.selection
  9. 문서 쓰기 — podolay.edit 와 tx
  10. 일괄 작업 — podolay.batch
  11. 화면 — podolay.ui · 패널의 podolayUI
  12. 저장소·라이선스 — podolay.storage · podolay.license
  13. 그림 올리기 — podolay.assets
  14. 패키지 파일 — podolay.package
  15. 네트워크 — fetch · WebSocket · EventSource
  16. 데이터 형식
  17. 그림 목록 (Display List)
  18. 제한값 한눈에 보기
  19. 오류와 시간 제한
  20. 판 사이 바뀐 것

1. 실행 환경

항목내용
어디서 도는가편집기와 떨어진 샌드박스 iframe 안의 Web Worker. 편집기의 화면(DOM)·로그인 정보·다른 문서에 닿지 않는다
쓸 수 있는 것전역 podolay, 워커 API(self·setTimeout·OffscreenCanvas·TextEncoder·crypto.getRandomValues·crypto.randomUUID 등), crypto.subtle 은 digest 만(SHA-1·256·384·512, 편집기 2.14.14 부터 — 워커가 보안 컨텍스트가 아니라 편집기가 대신 잰다), console.*(로그 창으로)
없는 것document·window(화면은 패널로), XMLHttpRequest·importScripts·Worker(늘), fetch(네트워크 권한이 없으면)
호출 방식편집기와 말하는 호출은 모두 Promise. 실패하면 Error 로 거절된다 (e.message 에 까닭)
단위·좌표길이는 모두 pt(1/72 인치). 개체 프레임은 스프레드 좌표(스프레드 왼쪽 위가 원점). 그림 목록은 개체 로컬 좌표
쪽 번호0 부터. doc.info() 의 spreads[].pages[].index 와 같은 번호
언제 시작하나편집기를 열 때 설치·켠 플러그인을 불러와 main.js 를 한 번 실행한다. 관찰자·게스트·독자 모드·공개 창에서는 돌지 않는다
멈추면호출에 8초 안에 답하지 못하고 살아 있는지 묻는 신호에도 답이 없으면 편집기가 플러그인을 끈다 (§19)

main.js 의 맨 위에서 던진 오류는 '시작 실패'가 되어 플러그인이 꺼진다. 구문 오류도 main.js:줄 과 함께 로그 창에 남는다.

main.js 는 <플러그인 id>/<main 경로>(예 com.example.qr/main.js)라는 이름으로 불러온다 — 브라우저 개발자 도구의 Sources·오류 스택·로그 창에 그 이름과 실제 줄 번호로 보인다. 중단점을 걸거나 debugger; 를 넣어 멈출 수 있다(§19).


2. 권한

매니페스트 permissions 에 적는다. 설치하는 사람에게 그대로 보이므로 쓰는 것만 적는다(검수 기준).

권한쓸 수 있게 되는 것
document:readdoc.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:writeedit (모든 tx.*) · batch · assets.upload
selectionselection.get · selection.set · selection.setText · text.selection · selectionChange·textSelectionChange 이벤트
uiui.showPanel (매니페스트 contributes.panels)
storagestorage.get · storage.set
network:<호스트>그 호스트로 fetch·WebSocket·EventSource (§15). privacyUrl 필수
권한 없이 되는 것graphics·tools·commands 정의, on(selectionChange·textSelectionChange 빼고), doc.getData, ui.toast·ui.closePanel·ui.postMessage·ui.onMessage·ui.progress, package.read·text·json, ui.pickFiles

새 판에서 권한을 늘리면 사용자가 포털에서 다시 동의할 때까지 편집기는 옛 판을 계속 쓴다.


3. 매니페스트 podolay-plugin.json

3.1 맨 위 필드

필드필수형식설명
id✔역도메인 com.example.name소문자·숫자·-, 점으로 나눈 2단 이상, 100자 이하. 처음 올린 개발자의 것이 된다
name✔언어별 글100자 이하(서버)
description언어별 글2000자 이하
version✔semver 1.2.0판마다 새로 검수
apiVersion✔"1.0" ~ "1.9"쓰는 API 판. 1.1~1.9 기능을 쓰려면 그 판을 적는다. 편집기가 모르는 판이면 불러오지 않는다
minEditorsemver이 편집기 판 이상에서만 불러온다
main✔패키지 안 경로워커에서 돌릴 스크립트
icon패키지 안 그림(svg·png·jpg·webp)스토어 아이콘
permissions문자열 배열 (30개 이하)§2
privacyUrl△https 주소 (500자 이하)네트워크 권한이 있으면 필수
contributes객체아래 §3.2~§3.6
help객체사용법·예제 — §3.8
dataFrom플러그인 id 배열 (10개 이하)1.8 읽을 수 있는 내 다른 플러그인 — §3.9

3.2 contributes.graphics — 그래픽 형식 (50개 이하)

필드필수형식설명
type✔로컬 이름graphics.define(type, …) 과 같은 이름
name✔언어별 글인스펙터 제목 등
usesFillStroketrue프레임의 칠·선을 쓴다 — 인스펙터에 칠·선 줄이 보인다 (프레임 칠은 그림 뒤, 선은 앞)
defaultSize[w, h] (0 < pt < 10000)툴로 클릭만 했을 때의 크기 (없으면 100×100)
inspector필드 배열 (50개 이하)개체를 고르면 인스펙터에 나오는 칸
autoSizetrue 1.3데이터가 크기를 정한다 — 편집기가 데이터를 바꿀 때 size 로 프레임을 맞춘다

3.3 인스펙터 필드

칸을 바꾸면 그 개체의 data[key] 가 바뀌고 편집기가 render 를 다시 부른다(Undo 한 단계).

필드필수형식설명
key✔로컬 이름data 의 키
label✔언어별 글
type✔number · text · checkbox · select · color
min · max · step수number 의 범위·화살표 한 칸
unit'pt' · '%' · ''number 뒤에 붙는 단위
options△[{ value: 문자열·수, label: 언어별 글 }] (100개 이하)select 에 필수
livefalse 1.3number 만 — 화살표·스테퍼를 누를 때마다가 아니라 멈춘 뒤(0.3초) 한 번 바꾼다(Undo 한 단계)
hideInlinetrue 1.3글 속 개체로 고를 때는 숨긴다 (정렬·크기 모드처럼 글 속에서 뜻이 없는 칸)

color 칸의 값은 색 형식이다.

3.4 contributes.tools — 툴바 단추 (10개 이하)

필드필수형식설명
id✔로컬 이름tools.define(id, …) 과 같은 이름 (custom 일 때)
label✔언어별 글풍선 도움말
icon패키지 안 svg·png없으면 퍼즐 아이콘
kind✔createGraphic · customcreateGraphic = 사각형 툴처럼 끌어서 graphicType 개체를 만든다(편집기가 처리) · custom = 마우스 이벤트를 플러그인에 보낸다
graphicType△contributes.graphics 의 typecreateGraphic 에 필수

3.5 contributes.menus · contributes.keybindings (각 50개 이하)

"menus":       [{ "command": "number", "label": { "en": "Number objects", "ko": "번호 매기기" } }],
"keybindings": [{ "command": "number", "key": "mod+alt+n" }, { "command": "ipa", "key": "ctrl+alt+c" }]
  • 메뉴는 편집기 헤더의 플러그인(퍼즐) 메뉴 › 플러그인 이름 › 항목으로 들어간다. 항목을 고르면 commands.define(command, …) 이 불린다.
  • 1.2 우클릭 메뉴 — context 로 항목이 나올 곳을 고른다(하나 또는 배열, 빼면 "header"). 우클릭 메뉴에서는 맨 아래 가름줄 뒤에 항목 이름만 나온다.
    "menus": [
      { "command": "makeQr",  "label": "QR 코드로",       "context": "object", "when": { "kind": ["rectangle", "image"], "single": true } },
      { "command": "regen",   "label": "다시 그리기",     "context": ["object", "header"], "when": { "type": ["qr"] } },
      { "command": "lookup",  "label": "고른 글 찾아보기", "context": "text" },
      { "command": "stamp",   "label": "여기에 도장",     "context": "canvas" }
    ]
context나오는 곳명령의 인자 (§5.3)
header헤더의 플러그인 메뉴 (기본)없음 — 1.4 글 편집 중이면 text 인자
object개체 우클릭 메뉴 — when 이 맞을 때만{ context: 'object', ids, spread, point, inline? }
text글 편집 중 우클릭 메뉴 (텍스트 프레임·1.4 표 칸){ context: 'text', id, selection: { start, end, text } }
canvas빈 곳 우클릭 메뉴{ context: 'canvas', spread, page, point }

when (object 메뉴만): kind = 고른 개체가 모두 이 종류 중 하나(Graphic.kind), type = 모두 이 플러그인의 이 그래픽 형식, single: true = 하나만 골랐을 때. 빼면 개체를 하나라도 골랐으면 늘 나온다. 표 편집·표 칸·각주 편집 중의 메뉴에는 나오지 않는다. 글 속 개체를 고른 메뉴에도 나온다 — 그때 ids 는 고르는 동안만 있는 대리 개체 하나이고(doc.graphic·tx.set·setData·setPluginData 로 읽고 고치면 그 글 속 개체가 바뀐다, 지우기·옮기기는 안 된다), inline: { id, index } = 글(텍스트 프레임 id)과 doc.inlines·tx.removeInline 의 index.

  • 단축키 key = [mod+][ctrl+][alt+][shift+]<키>. <키> 는 a–z·0–9·f1–f12. mod = macOS ⌘, 그 밖 Ctrl. 1.4 ctrl = macOS ⌃(Control), 그 밖에서는 mod 와 같은 Ctrl — Mac 에서 ⌃⌥C 처럼 ⌘ 아닌 조합을 쓸 때. ctrl 은 apiVersion 1.4 이상에서만(그 아래면 매니페스트 오류). 글자·숫자 키는 mod·ctrl·alt 중 하나가 있어야 한다(글자 입력을 가로채지 않게). 편집기가 이미 쓰는 단축키와 겹치면 붙지 않고 로그에 경고가 남는다(Mac 이 아니면 ctrl 은 mod 와 같은 키로 본다).
  • 1.4 플러그인 단축키는 편집기의 내장 키 처리보다 먼저 보고, 글 편집 중에도 듣는다(글 엔진보다 먼저) — 1.4 플러그인의 명령은 그때 text 인자를 받는다(§5.3). 1.3 이하 플러그인의 단축키는 글 편집 중에는 불리지 않는다.

3.6 contributes.panels — 떠 있는 패널 창 (10개 이하)

필드필수형식설명
id✔로컬 이름ui.showPanel(id)
title✔언어별 글창 제목 (플러그인 이름 — 제목)
html✔패키지 안 html§11.2
width · height120–1200 px기본 280×360

3.7 예

{
  "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 — 사용법·예제 (매뉴얼·예제 파일·예제 레이아웃)

플러그인과 함께 사용자에게 사용법과 예제를 준다. 판과 함께 검수되고, 사용자는 포털 › 갤러리 › 플러그인 상세의 '사용법'·'예제' 탭, 대시보드 플러그인 카드의 '사용법·예제', 편집기 플러그인 메뉴 › 도움말(돌고 있는 판)에서 본다. 로그인한 사람만 볼 수 있다 — 로그인 전 갤러리에서는 소개만 보이고 아무것도 받을 수 없다. 편집기는 help 를 읽지 않으므로 apiVersion 과 상관없다.

"help": {
  "manual": [
    { "id": "start",   "title": { "ko": "시작하기", "en": "Getting started" },
      "file": { "ko": "docs/ko/start.md", "en": "docs/en/start.md" } },
    { "id": "options", "title": "Options", "file": "docs/options.md" }
  ],
  "samples": [
    { "id": "words", "title": { "ko": "단어 목록 예" }, "description": { "ko": "가져오기 명령에 넣어 볼 CSV" },
      "lang": "ko", "file": "samples/ko/words.csv" },
    { "id": "photos", "title": { "en": "Photos" }, "lang": "en", "file": "samples/en/Photo/" }
  ],
  "layouts": [
    { "id": "0f8fad5b-d9cb-469f-a165-70867728950e", "lang": "ko", "title": { "ko": "발음기호 단어장" } }
  ],
  "defaultLang": "en",
  "homepage": "https://example.com/plugin",
  "supportUrl": "https://example.com/support"
}
필드형식설명
manual배열 (20쪽 이하)매뉴얼 쪽 — 차례대로 목차가 된다. id(소문자·숫자·- 40자 이하), title(언어별 글 100자), file(docs/ 안의 .md, 경로 하나 또는 언어별 경로)
samples배열 (한 언어에 100개)내려받을 예제 파일 — id, title, description(500자), file(samples/ 안 파일, 또는 / 로 끝나는 폴더 — 사용자는 zip 하나로 받는다), lang
layouts배열 (한 언어에 20개)예제 레이아웃 — id = 내 '공유' 폴더에 있는 레이아웃의 id, title·description(없으면 레이아웃 제목), lang
defaultLang언어 코드화면 언어의 예제가 없을 때 보일 언어 (기본 en)
homepagehttps 주소 (500자 이하)플러그인 홈페이지 — 링크만 둔다
supportUrlhttps 주소 (500자 이하)지원·문의 주소

매뉴얼 — 마크다운: 제목(#·##·###)·문단·목록·표·굵게·코드·링크, 그리고 그림 ![설명](../img/step1.png).

  • 그림은 패키지 안 파일만(그 .md 기준 상대 경로, png·jpg·webp·svg). 외부 그림 주소는 그리지 않고 설명 글만 보인다.
  • 링크: https:// 는 새 탭, options.md#… 처럼 다른 매뉴얼 쪽은 그 쪽으로, 그 밖은 글자만.
  • HTML 은 그리지 않는다.

언어별 예제 — 예제 파일·레이아웃에 lang(언어 코드 하나나 배열: "ko"·["zh-Hans", "zh-Hant"])을 달면 그 언어 화면에서만 앞에 보인다. 없으면 모든 언어.

  • 화면 언어에 맞춘 예제가 하나도 없으면 defaultLang(기본 en)의 예제를 보인다. "zh" 는 zh-Hans·zh-Hant 에 맞는다.
  • 다른 언어의 예제는 '다른 언어 예제' 로 접혀 보인다 — 한국 사용자도 영어 예제를 받을 수 있다.
  • 한도는 언어마다 센다(lang 없는 항목은 모든 언어에 센다): 예제 파일 100개·레이아웃 20개 (전체 400·60).
  • 사용법 페이지와 개발자 콘솔의 '매뉴얼 미리 보기'에서 보기 언어를 바꿔 볼 수 있다.

폴더 예제 — "file": "samples/en/Photo/" 처럼 / 로 끝나면 그 폴더(하위 폴더 포함) 전체가 항목 하나다. 사용자는 서버가 묶은 zip(Photo.zip) 하나로 받는다 — 그림 수십 장을 한 장씩 받지 않아도 된다. 언어별 묶음은 samples/ko/ 처럼 폴더 하나로.

예제 파일 — samples/ 폴더에는 실행되지 않는 파일이면 무엇이든 넣을 수 있다(docx·xlsx·pdf·idml·psd·그림·글·csv …). 실행 파일·스크립트·HTML·SVG·압축 파일·매크로 오피스 파일(docm·xlsm …)은 받지 않고, 이름과 상관없이 실행 파일 머리(MZ·ELF·Mach-O·#!)가 있어도 받지 않는다. 사용자에게는 늘 내려받기로만 간다. 플러그인 안에서도 podolay.package.text('samples/words.csv') 처럼 읽을 수 있다.

예제 레이아웃 — 플러그인을 쓴 실제 레이아웃을 준다.

  1. 포털 › 개발자 › 예제 레이아웃 준비 에서 레이아웃을 고르고 '공유 사본 만들기' — 내 '공유' 폴더에 고칠 수 없는 사본이 생기고, help.layouts 에 넣을 줄({ "id": … })이 보인다(복사 단추). 대시보드 카드 메뉴 › 공유(받는 사람 없이)로 만들어도 같다. 공유 사본은 고칠 수 없으니 이 판의 예제로 고정된다.
  2. 그 줄(사본 id)을 help.layouts 에 넣는다.
  3. 판을 올릴 때 서버가 그 id 가 내 '공유' 폴더의 레이아웃인지 본다(PLUGIN_SAMPLE_LAYOUT_INVALID).

사용자가 가져오기를 누르면 그 레이아웃이 사용자의 '받은 파일들' 폴더에 보통 레이아웃(고칠 수 있는)으로 복사된다 — 사용자의 레이아웃 수·저장 공간 한도 안에서. 사용자 서체는 따라가지 않는다(사용자 화면에서는 다른 서체로 보인다). 공유 사본을 지우면 그 예제는 '개발자가 지웠습니다'로 바뀐다.

누가 받을 수 있나 — 매뉴얼은 로그인한 누구나(사기·설치하기 전에도). 예제 파일·예제 레이아웃은 플러그인을 쓸 수 있는 사람만: 무료 플러그인은 유료 요금제(Starter 이상), 유료 플러그인은 산 사람(체험 중 포함 — 요금제와 무관). 개발자 본인·관리자는 늘.

크기 — 코드·매뉴얼·예제를 모두 합쳐 zip 20MB(풀어서 40MB, 파일 500개). 개발자 콘솔의 판 줄에서 '매뉴얼 미리 보기'로 검수 전에 확인할 수 있다.

3.9 dataFrom — 내 다른 플러그인의 데이터 읽기

1.8 — 같은 개발자가 낸 다른 플러그인(예: 무료판 → Pro)이 문서와 계정에 남긴 것을 읽기만 한다. 매니페스트에 그 플러그인 id 를 적는다.

"apiVersion": "1.9",
"dataFrom": ["com.example.dbp"]
읽는 곳하는 일
개체 설명(doc.graphic·doc.graphics·doc.find…)의 pluginDataFrom{ "com.example.dbp": 그 플러그인의 pluginData } — 그 플러그인이 표시를 단 개체만
플러그인 그래픽의 plugin.datadataFrom 플러그인의 그래픽이면 그 데이터도 준다
doc.find({ tag, from: 'com.example.dbp' })그 플러그인의 표시로 찾는다
doc.getData('com.example.dbp')그 플러그인의 문서 데이터
storage.getFrom('com.example.dbp')🔑 storage 그 플러그인의 계정 저장소 (개발 모드에서는 null)
  • 서버가 판을 올릴 때 dataFrom 의 플러그인이 내가 올린 플러그인인지 본다(아니면 PLUGIN_DATA_FROM_INVALID). 쓰기는 없다 — 바꾸려면 내 표시로 새로 단다.
  • dataFrom 에 없는 id 를 from 으로 주면 오류.

4. podolay 한눈에 보기

API판권한돌려줌하는 일
podolay.apiVersion1.0🔓'1.7'이 편집기의 API 판
podolay.env1.0🔓객체플러그인·편집기·언어 정보
graphics.define(type, def)1.0🔓—그래픽 형식 — 데이터 → 그림
tools.define(id, def)1.0🔓—custom 툴의 마우스 처리
commands.define(id, fn)1.0🔓—메뉴·단축키 명령
on(name, fn)1.0🔓 (선택은 selection)구독 해제 함수편집기 이벤트
doc.info()1.0document:read→ DocInfo문서·스프레드·쪽·마스터
doc.graphics({spread})1.0document:read→ Graphic[]스프레드의 개체
doc.graphic(id)1.0document:read→ Graphic | null개체 하나
doc.raw()1.0document:read→ any문서 저장 형식 전체
doc.getData()1.0🔓→ any이 플러그인의 문서 데이터
doc.styles()1.1document:read→ { para, char }단락·문자 스타일
doc.text(id)1.1document:read→ StoryPara[]텍스트 개체·글 자리의 글
doc.find(query)1.1document:read→ Graphic[]템플릿 자리 찾기
doc.textStatus(id)1.1document:read→ TextStatus글 흐름·넘침
doc.inlines(id)1.2document:read→ InlinePlaced[]글 속 개체들
doc.texts(q)1.4document:read→ TextPlace[]문서의 모든 글 자리
doc.table(target)1.6document:read→ TableInfo표의 행·열·병합·머리행
doc.tableStyles()1.7document:read→ { table, cell }표·칸 스타일 이름
text.measure(story, width, opts)1.1document:read→ { height, lines }넣기 전에 글 재기
text.selection()1.4selection→ TextSelection | null편집 중인 글의 선택
selection.get()1.0selection→ number[]고른 개체
selection.set(ids)1.0selection→ number[]개체 고르기
selection.setText(id, start, end?)1.4selection→ true글 범위를 골라 편집 열기
edit(name, fn)1.0document:write→ { created, results }문서 바꾸기 (Undo 한 단계)
batch(name, fn)1.1document:write→ fn 의 값여러 edit 를 Undo 한 단계로
ui.toast(msg, kind)1.0🔓→ void알림
ui.progress(o)1.1🔓 (batch 안)→ void진행 창
ui.showPanel(id)1.0ui→ void패널 열기
ui.closePanel(id)1.0🔓→ void패널 닫기
ui.postMessage(panelId, msg)1.0🔓→ void패널에 보내기
ui.onMessage(fn)1.0🔓—패널에서 받기
ui.pickFiles(o)1.6🔓→ PickedFile[]사용자 PC 의 파일·폴더 읽기
storage.get() · set(v)1.0storage→ any · → void계정별 설정
license.status()1.5🔓→ LicenseStatus유료 플러그인의 라이선스·체험
assets.upload(bytes, opts)1.1document:write→ { asset, width, height, sha256?, reused? }그림을 문서 자산으로
assets.list(q)1.6document:read→ AssetInfo[]문서의 그림 자산 목록
package.read(path) · text · json1.4🔓→ ArrayBuffer · → string · → any패키지 안 파일 읽기

tx 의 연산은 §9.2 표.

4.1 podolay.apiVersion · podolay.env

podolay.apiVersion   // '1.7' — 이 편집기의 API 판
podolay.env          // { pluginId, pluginVersion, apiVersion, editorVersion, locale, permissions, dev }
env 키형식설명
pluginId문자열매니페스트 id
pluginVersion문자열매니페스트 version
apiVersion문자열편집기의 API 판
editorVersion문자열편집기 판 (2.11.0 등)
locale문자열편집기 UI 언어 — ko en es de fr ja zh-Hans zh-Hant. 문서 언어와 다르다
permissions문자열[]받은 권한 (network:… 포함)
dev불개발 모드(pluginDev)로 불러왔는가

env 는 main.js 가 시작되기 전에 채워진다 — 맨 위 코드에서도 읽을 수 있다.


5. 정의 — graphics · tools · commands

정의는 main.js 맨 위에서 한 번 한다. 같은 이름으로 다시 부르면 덮어쓴다.

5.1 graphics.define(type, def)

1.0 🔓 — 플러그인 그래픽 형식. 매니페스트 contributes.graphics 에 같은 type 이 있어야 쓸 수 있다.

podolay.graphics.define('grid', {
  defaults: { rows: 10, cols: 20, color: '#c00' },          // 값 또는 (async) 함수
  render(data, { width, height }) {                          // 순수 함수 — 같은 입력 → 같은 그림
    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;                                            // 또는 { items }
  },
});
def 키형식설명
defaults값 · () => 값 · async () => 값툴로 새로 만들 때의 data
render(data, { width, height }) => Drawing | Promise<Drawing>그림 목록을 돌려준다
size(data, frame) => { width, height, x?, y? } | null 1.3자연 크기 — 매니페스트 autoSize: true 일 때만 불린다

render 가 불리는 때: 툴로 만들 때, tx.create({ kind: 'plugin' })·tx.setData, 인스펙터 칸을 바꿀 때, 개체 크기가 바뀐 뒤(멈춘 뒤 0.25초), Undo/Redo 로 크기가 달라졌을 때, tx.set(id, { frame }) 으로 크기를 바꿨을 때(같은 edit 의 setData 가 없으면 지금 데이터로, 같은 Undo 단계). width·height 는 늘 그 edit 가 끝난 뒤의 크기다 — 같은 edit 에서 frame 과 setData 를 함께 바꾸면 op 순서와 상관없이 새 크기로 한 번 불린다.

  • 그림은 문서에 저장된다 — 플러그인이 없는 사람·독자 모드·PDF·IDML 에서도 그 그림으로 보이고 인쇄된다. 그러니 render 는 빠르고(수십 ms) 순수해야 한다.
  • 크기가 바뀌었는데 플러그인이 없으면 저장된 그림을 늘려 그린다.
  • 1.3 자연 크기 — 수식처럼 데이터가 크기를 정하는 그래픽은 매니페스트에 autoSize: true 를 적고 size(data, frame) 을 정의한다. 편집기가 데이터를 바꿀 때(인스펙터 칸 · tx.setData · 툴로 만들 때 · tx.setInline) 먼저 size 를 불러 그 프레임으로 맞추고, 그 크기로 render 한 뒤 프레임과 데이터를 한 Undo 단계로 바꾼다. x·y 를 빼면 왼쪽 위를 지킨다(가운데를 지키려면 frame 으로 계산해 준다). null = 크기를 그대로. 같은 edit 에서 tx.set(id, { frame }) 으로 크기를 직접 정하면 size 를 부르지 않는다. 글 속 개체면 바뀐 크기대로 글이 다시 흐른다. 사용자가 손잡이로 크기를 바꿀 때는 부르지 않는다(사용자가 정한 크기로 render). 변의 상한은 20000pt, 8초 안에 답해야 한다(넘거나 틀리면 로그만 남기고 크기를 그대로 둔다).
  • render 가 8초 안에 답하지 않거나 그림이 제한을 넘으면 그 호출은 실패하고 이전 그림이 남는다.

5.2 tools.define(id, def)

1.0 🔓 — kind: 'custom' 툴의 마우스 처리. (createGraphic 툴은 편집기가 처리하므로 정의가 필요 없다.)

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 } }]; },  // 미리보기
  async onUp(e) { await podolay.edit('올가미', tx => tx.create({ kind: 'rectangle', frame: bbox(pts) })); },
});
이벤트 키형식설명
x · y수스프레드 좌표 pt
shift불
alt불Option/Alt
mod불⌘ 또는 Ctrl
button수0 = 왼쪽
  • onDown·onMove 가 그림 목록(스프레드 좌표)을 돌려주면 편집기가 미리보기로 그린다. null 이면 미리보기를 지운다.
  • 답을 기다리는 동안의 움직임은 마지막 것만 보낸다 — 느려도 편집기가 밀리지 않는다.
  • 끝나면(onUp 뒤) 미리보기는 지워진다. 문서에 남기려면 edit 를 부른다.

5.3 commands.define(id, fn)

1.0 🔓 — 메뉴 항목·단축키가 부르는 명령. fn 은 async 여도 된다.

podolay.commands.define('regen', async () => { … });
podolay.commands.define('makeQr', async (arg) => {          // 1.2 우클릭 메뉴 — arg 에 고른 자리
  if (arg?.context === 'object') for (const id of arg.ids) …;
});

1.2 우클릭 메뉴(menus[].context)에서 불리면 fn(arg) 에 그 자리가 온다. 헤더 메뉴·단축키에서는 인자가 없다(undefined). 1.4 헤더 메뉴·단축키로 부른 명령도 글 상자·문서의 표 칸을 편집 중이면 우클릭 글 메뉴와 같은 text 인자를 받는다(1.4 플러그인만 — 1.3 이하는 예전처럼 인자 없음).

arg.context필드
objectids = 고른 개체 id 들(우클릭한 개체가 고른 것 밖이면 그것만 고른 뒤) · spread = 스프레드 번호 · point = 우클릭한 자리(스프레드 좌표 pt)
canvasspread · page = 그 자리의 쪽 번호(쪽 밖이면 null) · point
textid = 편집하던 글 — 텍스트 프레임(체인 머리) id, 1.4 표 칸이면 { id: 표, cell: [행, 열] } (글 자리) · selection = { start, end, text } — Graphic.text 의 글자 자리 [start, end) 와 고른 글(캐럿만이면 start = end)
  • text 인자로 부를 때 편집기는 글 편집을 끝내고(쓰던 글이 문서에 들어간다) 명령을 부른다 — 그래서 doc.text·tx.insertInline(id, selection.start, …) 이 같은 글자 자리를 본다.
  • 명령이 끝나면 편집을 다시 연다 — 플러그인이 selection.set·selection.setText 를 부르지 않았으면, 글이 그대로면 원래 선택을, 바뀌었으면 캐럿을 원래 end + 늘어난 글자 수 자리에 둔다(범위를 넣은 글로 바꾸면 넣은 글 바로 뒤). batch 가 돌고 있거나 그 사이 다른 글을 편집하기 시작했으면 두지 않는다.
    // 1.4 — 매니페스트 "keybindings": [{ "command": "ipa", "key": "ctrl+alt+c" }] (Mac ⌃⌥C · 그 밖 Ctrl+Alt+C)
    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' }] }));
    });   // 끝나면 편집기가 넣은 글 바로 뒤에 캐럿을 두고 편집을 다시 연다
  • ids 는 selection 권한 없이도 온다(사용자가 그 개체에 대고 고른 것이므로). 문서를 읽고 바꾸려면 여전히 document:read·document:write.
  • 명령 하나는 60초 안에 끝나야 한다(넘으면 편집기가 기다림을 그만두고 "플러그인 명령이 실패했습니다" 알림). 더 긴 일은 batch 안에서 하거나 기다리지 않게 따로 돌린다.
  • 던진 오류는 알림과 로그로 보인다.

6. 이벤트 — podolay.on

1.0 — podolay.on(name, fn) 은 구독을 풀 함수를 돌려준다.

const off = podolay.on('selectionChange', ({ ids }) => { … });
off();   // 그만 받기
이름받는 값언제권한
selectionChange{ ids: number[], inline? }고른 개체가 바뀔 때selection
documentChange{ origins }문서가 바뀐 뒤 (잇따른 변경은 0.2초 모아 한 번)🔓
dataChange{ id, before, after, origin }이 플러그인 개체의 data 가 바뀐 뒤🔓
historyChange{}사용자가 Undo·Redo 한 뒤🔓
panelClose{ panelId: string }사용자가 이 플러그인의 패널 창을 닫았을 때🔓
textSelectionChange 1.4{ id, start, end, text } | null글 편집 중 캐럿·선택이 바뀐 뒤(멈추고 0.12초 뒤 한 번), 편집이 끝나면 nullselection
  • selectionChange.inline = 글 속 개체를 골랐을 때 { id, index } — 글(텍스트 프레임 id)과 doc.inlines·tx.removeInline 의 index. 그때 ids 의 개체는 고르는 동안만 있는 대리 개체다(우클릭 메뉴의 arg.inline 과 같다).
  • documentChange.origins = 그 0.2초 동안 바뀐 까닭들: user(사용자 편집·인스펙터) · plugin(플러그인 edit) · history(Undo·Redo).
  • dataChange 는 개체 하나의 data 가 실제로 바뀔 때마다(모으지 않음), 그 개체를 만든 플러그인에만 온다. origin = inspector(인스펙터 칸) · plugin(tx.setData) · undo · redo. 크기만 바뀌어 다시 그린 것과 글 속 개체를 tx.setInline 으로 바꾼 것(그 Undo 포함 — 글이 바뀐 것으로 documentChange 만 온다)은 오지 않는다. 글 속 개체를 인스펙터로 고치면 id 는 고르는 동안만 있는 대리 개체다. 플러그인 쪽 정리를 Undo 와 다투지 않게 하려면 undo·redo 에서는 고치지 않는다.
  • textSelectionChange 는 같은 값이면 다시 오지 않는다. 값은 text.selection() 과 같은 꼴 — id 는 글 자리(글 상자 id 또는 표 칸 { id, cell }). 각주·글 속 개체 안의 글을 편집 중이면 null. 패널에 고른 글의 미리보기를 보일 때 쓴다.

이벤트 처리기 안에서 던진 오류는 로그에만 남는다.


7. 문서 읽기 — 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 }] }
키설명
language문서 언어 (ko en ja …)
colorModeCMYK · RGB
currentSpread지금 보고 있는 스프레드 번호
pages[].x·y쪽의 스프레드 안 자리 — 쪽 위에 개체를 놓을 때 frame.x = page.x + …
pages[].master마스터 id (없으면 null) 1.1
pages[].label쪽 번호 표시 — 섹션 번호 매기기를 따른다 (iii, A-3 …) 1.1
masters마스터 목록 { id, name } 1.1

7.2 doc.graphics(q)

1.0 🔑 document:read → Graphic[] — 스프레드에 바로 든 개체, 쌓인 순서(뒤 → 앞). 그룹은 children 에 안의 개체가 든다.

q 키기본설명
spread지금 스프레드스프레드 번호

대지(쪽 밖)에 놓인 개체·숨긴 개체·숨긴 레벨의 개체도 든다 — visible 과 프레임으로 가려 쓴다. 마스터의 개체는 들지 않는다(doc.find({ masters: true })·doc.graphic).

스프레드 하나만 돌려준다 — 단면 문서는 쪽마다 스프레드가 따로다. 문서 전체를 보려면 doc.info().spreads 를 돌며 spread 를 주거나 doc.find 를 쓴다. 모의 편집기는 모든 쪽을 스프레드 0 하나에 두므로 이 차이가 시험에서 드러나지 않는다 — 다른 쪽의 개체를 찾는 코드는 샌드박스 편집기에서도 돌려 본다.

7.3 doc.graphic(id)

1.0 🔑 document:read → Graphic | null — 문서 어디서든(그룹 안·마스터 포함) id 로. 없으면 null.

Graphic 형식.

1.6 id 에 글 속 개체 주소 { id: 글, inline: 서수, path? } 도 준다 — 글 속 개체와 글 속 그룹 안의 자식을 읽는다. 돌려준 값에는 id 가 없고(글 속 개체에는 id 가 없다), 그룹 자식에는 path 가 붙는다. frame 은 스프레드 좌표가 아니다 — 놓인 자리는 doc.inlines 의 placed. 없는 주소면 null 이 아니라 오류다.

7.4 doc.raw()

1.0 🔑 document:read → any — 문서 저장 형식 전체. 내부 형식이라 편집기 판이 바뀌면 달라질 수 있다 — 다른 API 로 안 되는 것만 읽고, 바뀌어도 견디게 쓴다. 고칠 수는 없다.

7.5 doc.getData()

1.0 🔓 → any — 이 플러그인이 tx.setDocData 로 문서에 저장한 값(없으면 null). 다른 플러그인의 값은 보이지 않는다.

7.6 doc.styles()

1.1 🔑 document:read → { para: StyleInfo[], char: StyleInfo[] } — 문서의 단락·문자 스타일('없음' 빼고).

const { para, char } = await podolay.doc.styles();
// para: [{ id: -1, name: '기본', key: '$base' }, { id: 12, name: '제목', basedOn: -1 }, …]
키설명
id스타일 id
name표시 이름. 내장 스타일의 이름은 UI 언어마다 다르다
key내장 스타일만 — '$base'(기본). Story 에는 이 별칭을 쓴다
basedOn기준 스타일 id

7.7 doc.text(id)

1.1 🔑 document:read → StoryPara[] — 텍스트 개체(텍스트 프레임·제목용 텍스트·글이 든 도형)의 글. 연결된 프레임이면 어느 프레임 id 든 글 전체.

const story = await podolay.doc.text(id);
// [{ para: '제목', runs: [{ text: '봄 신상품', char: '$base', font: 'Noto Sans KR', size: 28, color: {cmyk:[0,100,100,0]}, bold: true }] }, …]
  • 단락 스타일·문자 스타일은 이름으로(내장은 '$base'), 직접 서식은 서체·크기·색·굵게·기울임만 담는다.
  • 페이지 번호·각주 참조 같은 변수 글은 variable: true 와 지금 보이는 값으로 온다.
  • 1.2 글 속 개체는 따로 된 런 { inline: { index, kind, width, height, … } } 로 온다(§16.8). 이 런을 그대로 tx.setText 에 돌려주면 그 개체를 다시 쓴다 — doc.text → 고치기 → setText 로 글 속 그림을 잃지 않는다. apiVersion 이 1.1 이하인 플러그인에는 예전처럼 런의 text 에 개체 자리 글자 U+FFFC 가 든다.
  • 1.4 id 에 글 자리도 준다 — 표 칸 { id: 표, cell: [행, 열] }, 글 속 텍스트 프레임 { id: 글, inline: 서수 }, 글 속 표의 칸 { id, inline, cell }. 그 전에는 표가 '글이 있는 개체가 아닙니다' 오류였고, 글 속 텍스트 프레임은 doc.inlines 의 text 로 읽기만 했다. 1.6 글 속 그룹 안의 글 상자·표 칸은 { id, inline, path }·{ id, inline, path, cell }.
  • 1.6 더 많은 직접 서식 — 런의 tracking·hScale·vScale·baselineShift·underline·strike·position·smallCaps 는 기본값과 다를 때만, 단락의 align·leading·spaceBefore·spaceAfter·indentLeft·indentRight·indentFirst 는 그 단락 스타일과 다를 때만 싣는다(§16.6). 1.5 이하 플러그인에는 싣지 않는다.

7.8 doc.find(query)

1.1 🔑 document:read → Graphic[] — 템플릿 자리 찾기. 조건을 모두 만족하는 개체(그룹 안 포함, 스프레드 순서).

await podolay.doc.find({ text: '{{title}}' });                  // 글에 자리표시자가 든 텍스트 개체
await podolay.doc.find({ tag: { slot: 'photo' }, page: 2 });     // 3쪽의, 이 플러그인이 표시한 개체
await podolay.doc.find({ kind: 'image', spread: 0 });
query 키형식설명
text문자열글에 이 글이 들어 있다. 연결된 글은 프레임 하나(체인 머리)만
tagtrue · 문자열·수 · 객체이 플러그인의 pluginData — true = 있음, 값 = 같음(또는 pluginData.tag 가 같음), 객체 = 그 키들이 같음
kindGraphic.kind종류
spread수그 스프레드에서만
page수 (0부터)개체 프레임의 가운데가 그 쪽 안
masterstrue마스터 스프레드도 (spread·page 를 주지 않았을 때)
limit수기본 1000, 최대 5000

개체에는 이름이 없다. 디자이너가 화면에서 만들 수 있는 글 속 자리표시자({{field}})나, 플러그인 패널로 붙인 pluginData 표시로 자리를 찾는다.

찾는 범위: 대지(쪽 밖)에 놓인 개체·숨긴 개체·숨긴 레벨의 개체도 든다(템플릿 원본을 대지에 두거나 숨겨 두어도 찾는다). 마스터는 masters: true 일 때만.

7.9 doc.textStatus(id)

1.1 🔑 document:read → TextStatus — 화면과 같은 레이아웃에서 읽은 글 흐름.

const st = await podolay.doc.textStatus(bodyId);
// { frame: 0, frames: [101, 102], lines: 7, chars: [0, 134], length: 458, height: 108.2, overflow: false }
키설명
frame체인 안에서 이 프레임의 순번
frames체인의 프레임 id 들 (이어지지 않았으면 하나)
lines이 프레임에 든 줄 수
chars이 프레임에 든 글자 범위 [시작, 끝)
length글 전체 길이
height이 프레임의 글 높이 (pt)
overflow체인 끝에서 글이 넘쳤다

서체 조각이 아직 오지 않았으면 잠시 뒤 값이 조금 달라질 수 있다.

7.10 text.measure(story, width, opts)

1.1 🔑 document:read → { height, lines } — 문서를 바꾸지 않고 글을 짜 본다.

const { height, lines } = await podolay.text.measure(story, 180, { like: bodyId });
인자설명
storyStory
width글이 놓이는 폭 pt — 프레임 안쪽 여백을 뺀 폭
opts.like텍스트 개체 id — 그 글의 첫 서식을 바탕으로 (= tx.setText 와 같은 서식). 없으면 문서의 '기본'

지금 불러온 서체로 잰다. 문서에 아직 쓰이지 않은 서체·글자는 처음엔 대체 서체로 재일 수 있으니, 넣은 뒤 doc.textStatus 로 확인한다. 1.2 Story 에 글 속 개체 런이 있으면 그 크기도 잰다({ graphic: id } 는 옮기지 않고 그 개체의 크기로).

7.11 doc.inlines(id)

1.2 🔑 document:read → InlinePlaced[] — 텍스트 개체의 글 속 개체(인라인 앵커)들, 글 순서. 연결된 프레임이면 글 전체.

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 } }, …]
키설명
index이 글에서 몇 번째 글 속 개체인가 (0부터) — tx.removeInline·setText 의 { index }
offsetGraphic.text 에서의 글자 자리 (개체 자리 글자 U+FFFC)
baseline1.3 개체 아래 끝에서 글의 기준선까지(pt) — 0 이면 빠진다
placed놓인 자리 — frame = 그 텍스트 프레임 id, x·y·width·height = 스프레드 좌표(프레임을 돌리기 전). 넘친 글 속이면 null
그 밖Graphic 과 같다 — id·frame 대신 width·height (글 속 개체에는 id 가 없다)
children[].path1.6 글 속 그룹의 자식(과 그 안 그룹의 자식)에 붙는 그룹 안 자리 [자식 번호, …] — 글 속 개체 주소의 path

자리는 화면과 같은 레이아웃에서 읽는다(서체 조각이 늦게 오면 조금 달라질 수 있다). 각주 글 속의 개체는 들지 않는다.

1.8 id 에 글 상자 밖의 글 자리도 준다 — 표 칸 { id, cell }, 글 속 표 칸 { id, inline, cell } 등. 그 글의 글 속 개체를 같은 꼴로 주되 placed 는 null 이다. 그 개체는 { in: 그 글 자리, inline: index, path? }로 가리킨다 (표 한 행에 상품 그림을 넣는 카탈로그 등).

const cell = { id: tableId, cell: [row, 0] };
const [pic] = await podolay.doc.inlines(cell);                       // 칸 안의 글 속 그룹
await podolay.edit('사진', tx => tx.setImage({ in: cell, inline: pic.index, path: [0] }, asset, 'fill'));

7.12 doc.texts(q)

1.4 🔑 document:read → TextPlace[] — 문서의 모든 글 자리, 스프레드 순서. 글을 훑어 바꾸는 플러그인(맞춤법·표기 바꾸기)이 표 칸·글 속 글상자까지 빠짐없이 돌 때 쓴다.

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('바꾸기', tx => tx.replaceText(t.target, 'ph', 'f'));
}
키설명
target글 자리 — doc.text·tx.* 에 그대로 넘긴다
kindframe = 글 상자(연결된 글은 머리 하나) · cell = 표 칸 · inline = 글 속 텍스트 프레임 · inlineCell = 글 속 표의 칸
text글 (Graphic.text 와 같은 꼴 — 단락 사이 \n)
q 키설명
spread그 스프레드만
masterstrue = 마스터 스프레드도 (spread 를 주지 않았을 때)

그룹 안 개체도 들고, 병합에 덮인 표 칸은 빠진다. 글 상자 다음에 그 글의 글 속 텍스트 프레임·표 칸이 온다. 2만 개까지. 대지(쪽 밖)·숨긴 개체·숨긴 레벨의 글도 든다. 마스터는 masters: true 일 때만.

7.13 text.selection()

1.4 🔑 selection → { id, start, end, text } | null — 사용자가 편집 중인 글의 선택. 글 자리로 부른 명령(§5.3)이 도는 동안은(편집이 잠시 끝나 있어도) 그 자리를 돌려준다.

  • id = 글 자리 — 글 상자(체인 머리) id 또는 표 칸 { id, cell }. start·end = Graphic.text 의 글자 자리, text = 고른 글(캐럿만이면 '').
  • 글을 편집하고 있지 않으면 null. 각주·글 속 개체 안의 글은 아직 null.
  • 패널에 보여 줄 때는 textSelectionChange 이벤트를 받는 편이 가볍다.

7.14 doc.table(target)

1.6 🔑 document:read → TableInfo — 표의 짜임. target = 스프레드의 표 id(그룹 안 포함) 또는 글 속 표 { id: 글, inline: 서수, 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: '줄무늬', cellStyles: [[null, …], …] }   ← 1.7
키설명
rows · cols행·열 수
headerRows머리행 수 (쪽을 넘을 때 되풀이되는 위 행들)
footerRows꼬리행 수 (쪽을 넘을 때 마지막이 아닌 조각 아래에 되풀이되는 아래 행들, 편집기 2.15.0 부터 — 그 전에는 늘 0)
merges병합된 칸 [행, 열, 행 수, 열 수] 들 (0부터, 왼쪽 위 칸 기준)
rowHeights · colWidths행 높이·열 폭 (pt)
tableStyle1.7 입힌 표 스타일 이름, 없으면 null
cellStyles1.7 칸마다(행 → 열) 직접 건 칸 스타일 이름, 없거나 병합에 덮인 칸은 null (영역 칸 스타일은 tableStyles() 의 regions)

칸의 글은 doc.text({ id, cell }), 행을 늘리고 줄이는 것은 tx.insertRows·tx.removeRows, 스타일은 tx.setTableStyle·tx.setCellStyle.

7.15 doc.tableStyles()

1.7 🔑 document:read — 문서의 표 스타일·칸 스타일 (편집기 › 인스펙터 › 표 스타일에서 만든 것).

const { table, cell } = await podolay.doc.tableStyles();
// table: [{ name: '줄무늬', regions: { header: '머리 칸', body: null, footer: null, left: null, right: null }, alternating: 'rows' }]
// cell:  [{ name: '머리 칸' }]

regions = 영역마다 쓰는 칸 스타일 이름(없으면 null — 머리·꼬리·왼쪽·오른쪽은 본문을 따른다), alternating = 번갈아 칠 'rows'·'cols'·null. 스타일을 만들고 고치는 것은 사용자의 일이라 API 에 없다 — 플러그인은 이름으로 입힌다.


8. 선택 — podolay.selection

8.1 selection.get()

1.0 🔑 selection → number[] — 고른 개체 id.

8.2 selection.set(ids)

1.0 🔑 selection → number[] — 개체를 고른다. 실제로 고른 id 를 돌려준다.

  • 1000개까지. 그룹 안 개체는 고를 수 없다(그룹째로).
  • 한 스프레드만 — 첫 개체의 스프레드에 있는 것만 고른다.

8.3 selection.setText(id, start, end?)

1.4 🔑 selection → true — 그 글의 [start, end) 를 골라 글 편집을 연다(end 를 빼면 start 에 캐럿). 다른 스프레드·마스터에 있으면 그리로 간다(찾기와 같다).

  • id = 글 상자 id 또는 문서의 표 칸 { id, cell } (글 자리). 글 속 개체 안의 글은 아직 안 된다.
  • 0 ≤ start ≤ end ≤ 글 길이 (Graphic.text 의 글자 자리). batch 중에는 안 된다.
  • 글 자리로 부른 명령 안에서 부르면 명령이 끝난 뒤 편집기가 원래 선택으로 되돌리지 않는다(selection.set 도 같다).

9. 문서 쓰기 — podolay.edit 와 tx

9.1 edit(name, fn)

1.0 🔑 document:write → { created: number[], results: any[] }

const { created, results } = await podolay.edit('정렬', tx => {
  tx.set(id, { frame: { x: 0, y: 0, width: 100, height: 50 } });
  const k = tx.create({ kind: 'text', frame, story: '안녕' });   // k = 이 편집 안의 op 순번
});
results[k];   // 새 개체 id
  • fn 은 동기 함수다 — 안에서 await 하지 않는다. tx.* 는 할 일을 적기만 하고, fn 이 끝난 뒤 편집기가 한꺼번에 적용한다.
  • 한 번의 edit = 사용자에게 Undo 한 단계 (Undo 메뉴에 "플러그인 이름: name"). 하나라도 실패하면 아무것도 바뀌지 않고 edit 가 던진다.
  • 한 번에 op 2000개까지.
  • op 는 적힌 차례대로 적용된다 — 뒤 op 는 앞 op 가 바꾼 문서를 본다(같은 edit 에서 copyRange 로 복제한 글을 replaceRange 로 고칠 수 있다).
  • created = 만든 개체 id (만든 순서), results[op 순번] = op 마다의 결과(아래 표의 '결과'). tx.* 는 op 순번을 돌려준다.
  • 사용자가 글을 고치고 있었으면 편집을 끝낸 뒤 적용한다.

9.2 tx 연산 한눈에

연산판결과 results[op]하는 일
create(o)1.0새 개체 id개체 만들기
set(id, props)1.0null공통 속성 바꾸기
remove(id)1.0null지우기
setData(id, data)1.0null자기 그래픽의 데이터 (그림도 다시)
setPluginData(id, value)1.0null아무 개체에 이 플러그인의 표시
setDocData(value)1.0null문서 단위 데이터
setText(id, story)1.1null글 전체 바꾸기 (템플릿 서식 유지)
replaceText(id, search, text)1.1null자리표시자 바꾸기
setImage(id, asset, fit)1.1null도형에 그림 넣기
link(from, to)1.1null텍스트 프레임 잇기
unlink(from)1.1null텍스트 프레임 끊기
addPages(o)1.1{ pages }쪽 추가
duplicatePages(o)1.1{ pages, copies }쪽 복제 (개체까지)
removePages(pages)1.1null쪽 지우기
applyMaster(pages, master)1.1null마스터 적용
insertInline(id, at, inline)1.2끼운 수글 속에 개체 끼우기
removeInline(id, index)1.2null글 속 개체 빼기
setInline(id, index, o)1.3null글 속 개체를 제자리에서 고치기
replaceRange(id, start, end, what)1.3null · { start, end }({ story })글자 범위를 글·서식 런·개체·Story 로 바꾸기
formatRange(id, start, end, format)1.4서식 입힌 글자 수글자 범위에 서식만 입히기
copyRange(from, to)1.6{ start, end, inlines, dropped }글 범위를 서식째 복제해 끼우기
appendText(id, story, o)1.6{ start, end, inlines }글 끝에 Story 를 단락째 붙이기
insertRows(target, o)1.6{ rows }표 행 끼우기 (행 복제)
removeRows(target, at, count?)1.6지운 행 수표 행 지우기
autoflow(id, o)1.6{ pages, frames, overflow }넘친 글을 새 쪽·프레임으로 흘리기
duplicate(id, o)1.6{ id, copies }스프레드 개체 복제
setTableStyle(target, name, o?)1.7null표 스타일 입히기·떼기
setCellStyle(target, cells, name, o?)1.7null칸 스타일 입히기·떼기

1.4 글을 받는 연산(setText·replaceText·replaceRange·formatRange)의 id 는 글 자리도 받는다 — 표 칸·글 속 텍스트 프레임의 글을 바꾸면 그 표·주인 글을 바꾸는 Undo 한 단계에 들어간다. 글 속 개체 안의 글(글 속 텍스트 프레임)에는 글 속 개체를 끼울 수 없다(setText 의 { inline } 런·replaceRange 의 { inline }). 표 칸은 1.9 부터 끼울 수 있다 — 아래. 1.6 글 속 그룹 안의 글은 { id, inline, path }, 그리고 tx.set·tx.setPluginData·tx.setImage 의 id 는 글 속 개체 주소도 받는다.

9.3 tx.create(o)

kind필드설명
plugintype, frame, data?자기 그래픽 형식 (render 를 불러 그림을 만든다)
rectangle · ellipseframe, fill?, stroke?fill: 색 · null(없음). stroke: { color?, width? } · null
lineframe, stroke?프레임의 왼쪽 위 → 오른쪽 아래. 기본 검정 1pt
textframe, text서식 없는 글
text 1.1frame, story스타일 있는 Story. 바탕 = 문서의 '기본' 스타일
image 1.1frame, asset, fit?그림 프레임. asset 은 §13. fit 은 §9.11

공통: frame = 스프레드 좌표 pt, spread? = 스프레드 번호(기본 지금 스프레드). 새 개체는 지금 레벨(레이어)에 들어간다.

9.4 tx.set(id, props)

속성형식설명
frameFrame자리·크기 (스프레드 좌표)
rotation수 (도)
opacity0–1범위 밖은 잘린다
visible불
locked불
fill색 · null단색 칠 · 칠 없음
stroke{ color?, width? } · null선 · 선 없음

그룹 안 개체도 바꿀 수 있다. 이 밖의 속성(글 프레임 옵션 등)은 다음 API 판에서.

1.6 id 에 글 속 개체 주소 { id: 글, inline: 서수, path? } 를 주면 글 속 개체·글 속 그룹 안의 자식을 바꾼다(주인 글을 바꾸는 Undo 한 단계, 잠금은 주인 글을 따른다). frame·rotation 을 바꾸면 그룹의 프레임과 글 속 자리의 크기를 다시 잰다 — 글이 다시 흐른다.

9.5 tx.remove(id)

스프레드에 바로 든 개체만(그룹 안 개체는 그룹째로). 잠긴 개체는 지우지 않는다(편집 전체가 실패).

9.6 tx.setData(id, data)

이 플러그인의 그래픽(kind: 'plugin', 같은 플러그인)만. 편집기가 render 를 다시 불러 그림·데이터를 함께 바꾼다. data 는 JSON 256KB 이하.

9.7 tx.setPluginData(id, value)

아무 개체에나 이 플러그인의 표시를 붙인다 — 다른 플러그인의 표시와 섞이지 않는다. value = JSON(256KB 이하), null = 지움. 개체와 함께 저장·복사되고 doc.find({ tag })·Graphic.pluginData 로 읽는다. 1.6 id 에 글 속 개체 주소도 준다 — 글 속 그룹 안 자식에 표시를 붙여 두고 doc.inlines 의 children 으로 다시 찾는다.

9.8 tx.setDocData(value)

문서 단위로 이 플러그인의 값을 저장한다 (doc.getData() 로 읽음). JSON 256KB 이하, null = 지움.

9.9 tx.setText(id, story)

1.1 — 텍스트 개체의 글 전체를 바꾼다(연결된 프레임이면 글 전체). 바탕 = 지금 글의 첫 단락·첫 글자 서식 — 디자이너가 템플릿에 입혀 둔 서식이 남는다. 잠긴 개체는 안 된다.

tx.setText(titleId, '봄 신상품');                                      // 템플릿 서식 그대로
tx.setText(bodyId, [{ para: '본문', runs: [{ text: '정가 ' }, { text: '12,000원', char: '강조' }] }]);

9.10 tx.replaceText(id, search, text)

1.1 — 글 속 search 를 모두 text 로 바꾼다. 둘레 서식은 그대로.

  • search 는 1–1000자, text 는 10만 자까지(줄바꿈은 빈칸).
  • 한 서식 덩어리 안에 다 든 것만 찾는다 — 자리표시자 중간에서 서식이 바뀌면 못 찾는다.
  • 페이지 번호·각주 같은 변수 글, 글 속 개체는 건드리지 않는다. 못 찾으면 아무 일도 하지 않는다(오류 아님).

9.11 tx.setImage(id, asset, fit)

1.1 — 도형(사각형·원·다각형·패스·그림 프레임 등, 표 제외)에 그림을 넣거나 바꾼다. 도형 모양 안에 잘려 그려진다. 잠긴 개체는 안 된다.

fit설명
'fill' (기본)비율대로 프레임을 덮는다 — 넘친 곳은 잘림
'fit'비율대로 다 보이게 — 남는 곳은 빈다
'stretch'비율을 버리고 프레임에 꽉

asset 은 이 플러그인이 그 창에서 올린 것(assets.upload)·1.6 assets.list 가 보여 준 것이거나 문서에 이미 있는 그림 경로만 된다. 놓은 뒤 사용자가 인스펙터의 프레임 맞춤으로 바꿀 수 있다.

1.6 id 에 글 속 개체 주소도 준다 — 글 속 그룹 안의 그림 상자(도형)에 그림을 넣는다.

// 글 속 그룹(0번)의 둘째 자식에 사진을, 비율대로 다 보이게
await podolay.edit('사진', tx => tx.setImage({ id: target, inline: 0, path: [1] }, asset, 'fit'));

9.12 tx.link(from, to) · tx.unlink(from)

1.1 — 텍스트 프레임을 잇고 끊는다(편집기에서 직접 잇는 것과 같은 규칙).

  • link: from 은 체인의 끝(다음이 없어야), to 는 다른 글에 속하지 않은 빈 텍스트 프레임. 제목용 텍스트·패스 위 글·잠긴 개체는 안 된다.
  • unlink: from 과 그 다음 프레임 사이를 끊는다 (뒤 프레임들의 글은 앞 글로 모인다).

9.13 쪽 연산

1.1 — 쪽 번호는 0부터, 그 op 를 부르는 때의 번호(앞 op 가 쪽을 늘리거나 지우면 밀린다). 결과의 pages 는 편집이 다 끝난 뒤의 번호.

const r = await podolay.edit('카탈로그 쪽', tx => tx.duplicatePages({ from: 2, times: 3 }));
r.results[0];   // { pages: [3, 4, 5], copies: [{ '120': 301, '121': 302 }, { … }, { … }] }
연산인자결과
addPages({ after?, count?, master? })after 그 쪽 뒤(기본 맨 끝, -1 = 맨 앞) · count 1–200 · master id·이름·null(없음), 빼면 이웃 쪽의 마스터{ pages }
duplicatePages({ from, count?, times?, after? })from 부터 count 쪽(기본 1)을 times 번(1–500) 복제해 after 뒤에(기본 원본 바로 뒤). 개체·마스터·지운 마스터 항목까지{ pages, copies } — copies[k] = k 번째 사본의 { 원본 개체 id: 사본 id } (1.6 그룹 안 자식도)
removePages(pages)쪽 번호 배열. 모든 쪽을 지울 수는 없다null
applyMaster(pages, master)master = id·이름·nullnull
  • 쪽 연산이 든 edit 는 문서 전체 스냅숏 하나로 Undo 된다(같은 edit 의 다른 op 도 함께). 실패하면 시작 전으로 되돌아간다.
  • 요금제의 문서당 쪽 수 한도를 넘으면 편집 전체가 실패한다. 한 edit 에서 늘릴 수 있는 쪽은 1000쪽. 마스터 편집 중에는 안 된다.
  • tx.autoflow 도 쪽 op 다(같은 스냅숏 Undo·쪽 수 한도).
  • copies 에는 그룹 안 자식의 id 도 든다(1.6 편집기부터 — 예전엔 맨 위 개체만). 표 칸에는 id 가 없다 — 사본 표의 칸은 { id: 사본 표, cell }.
  • 같은 edit 안에서는 새 쪽·사본의 id 를 아직 모른다 — 복제한 뒤 채우기는 다음 edit 에서 하고, 둘을 Undo 한 단계로 묶으려면 batch.

9.14 tx.insertInline · tx.removeInline

1.2 — 글 속 개체(인라인 앵커)는 글자 한 자리를 차지하고 글과 함께 흐른다(줄이 바뀌거나 앞 글이 늘면 따라 움직인다). 그림·도형·자기 그래픽·글상자를 글 속에 넣을 수 있다. 잠긴 텍스트 개체에는 안 된다.

await podolay.edit('로고 넣기', tx => {
  tx.insertInline(bodyId, '{{logo}}', { kind: 'image', asset, width: 40, height: 30 });   // 자리표시자마다
  tx.insertInline(bodyId, null, { graphic: badgeId });                                    // 스프레드의 개체를 글 끝으로 옮김
});
await podolay.edit('빼기', tx => tx.removeInline(bodyId, 0));
인자설명
id텍스트 개체 (연결된 프레임이면 글 전체)
at문자열 = 자리표시자 — 글 속 그 글을 모두 개체로 바꾼다(한 서식 덩어리 안에 다 든 것만, replaceText 와 같다) · 수 = Graphic.text 의 글자 자리(0 = 처음) · null·생략 = 글 끝
inlineInline — 새 개체 또는 { graphic: id }
  • 결과 results[op] = 끼운 수. 자리표시자를 못 찾으면 0 이고 오류가 아니다. 페이지 번호 같은 변수 글 안에는 끼우지 않는다.
  • 끼운 개체의 서식(줄 높이 계산에 쓰는 글자 크기 등)은 그 자리 글의 서식이다.
  • removeInline(id, index) — index 번째 글 속 개체(그 글자째)를 뺀다 (doc.inlines 의 index).
  • Story 런으로도 넣을 수 있다 — tx.create({ kind: 'text', story })·tx.setText 의 런 { inline }.

9.15 tx.setInline · tx.replaceRange

1.3

await podolay.edit('수식 고치기', tx => tx.setInline(bodyId, 2, { data: { tex: 'x^2' } }));       // autoSize 면 크기도
await podolay.edit('수식으로', 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? }) — 글 id 의 index 번째 글 속 개체(doc.inlines 의 index)를 그 자리에서 고친다. 서식·자리·회전은 그대로다. 이 플러그인의 그래픽(kind: 'plugin')만. width·height 를 주면 그 크기(0 초과 5000 이하), 빼고 data 만 주며 autoSize 면 size 로 맞춘다. 새 크기로 render 를 부른다. index 는 같은 edit 의 앞 op 가 바꾼 글에서 센다(앞 op 가 개체를 끼우거나 빼면 밀린다).
  • replaceRange(id, start, end, what) — 글자 범위 [start, end) 를 바꾼다. 글자 자리는 Graphic.text 의 자리(단락 사이 \n 포함, doc.find 와 같다). what = 글(문자열 · { text }, 줄바꿈은 빈칸) · 1.4 { runs: [{ text, char?, font?, size?, color?, bold?, italic? }] } · { inline: Inline }. 서식은 start 글자의 것이고, 범위가 단락 끝을 넘으면 단락이 합쳐진다(앞 단락의 단락 서식). { runs } 는 start 글자의 서식을 바탕으로 런마다 문자 스타일 → 직접 서식을 입힌다(Story 런과 같은 규칙, 글 속 개체 런은 안 되고 1000개·20만 자까지). { inline } 은 글 상자의 글(id 가 수)과 1.9 표 칸의 글(글 자리에 cell)에만. start = end 면 끼우기다. 페이지 번호 같은 변수 글을 반쯤 자르는 범위는 실패한다(통째로는 된다). 자리표시자로 찾는 insertInline 과 달리 같은 글이 여럿이어도 그 자리만 바뀐다. 1.6 { story } — 범위를 Story 로, 단락 스타일·단락 직접 서식째 바꾼다(§9.18). 이때 결과는 { start, end }.

9.16 tx.formatRange(id, start, end, format)

1.4 — 글자 범위 [start, end) 의 글은 그대로 두고 서식만 입힌다. 그 글자들의 지금 서식 위에 format 의 문자 스타일 → 직접 서식 순서로.

const r = await podolay.edit('IPA 서체', tx => tx.formatRange(sel.id, sel.start, sel.end, { font: 'Charis SIL', italic: false }));
r.results[0];   // 서식이 입혀진 글자 수
format 키설명
char문자 스타일 이름·id ('$none' = 스타일 참조만 뗀다 — 직접 서식은 남는다)
font · size · color · bold · italicStory 런의 직접 서식과 같다 (size 0.5–2000pt)
tracking · hScale · vScale · baselineShift · underline · strike · position · smallCaps1.6 Story 런의 더 많은 직접 서식과 같다
  • 하나도 없으면 오류. 결과 results[op] = 서식이 입혀진 글자 수 — 빈 범위(start = end)면 0 이고 글을 바꾸지 않는다.
  • 범위가 단락을 넘어도 단락은 그대로다. 페이지 번호 같은 변수 글을 반쯤 자르는 범위는 실패한다. 없는 스타일 이름이면 편집 전체가 실패한다.

9.17 tx.copyRange(from, to)

1.6 — 글 범위를 서식째 복제해 다른 자리(같은 글이어도 된다)에 끼운다. 단락 스타일·문자 스타일·모든 직접 서식·글 속 개체(그림·도형·글 상자·그룹·중첩 그룹·글 속 표)가 그대로 간다. 데이터베이스 퍼블리싱처럼 템플릿 글을 레코드마다 되풀이할 때 쓴다.

인자설명
from{ id: 글 자리, start, end } — 복사할 글자 범위 [start, end) (글 자리, Graphic.text 의 글자 자리)
to.id끼울 글 자리
to.at글자 자리 · 'end'(기본) — 끼울 곳
to.newParagraphtrue = 끼울 자리가 단락 처음이 아니면 먼저 단락을 나눈다(새 단락에 붙이기). 기본 false
  • 단락 서식은 단락 끝(\n)을 따라간다(인디자인과 같다): 복사한 범위 안의 닫힌 단락(끝 \n 까지 든 단락)은 원본의 단락 서식, 열린 마지막 조각은 끼운 자리 단락의 서식을 갖는다. 다만 글 끝 단락에 그 단락의 남은 글 없이 붙으면(빈 글에 붙이기 등) 원본 마지막 단락의 서식이다. 단락째 옮기려면 범위가 단락 끝 \n 까지 들게 잡는다.
  • 결과 results[op] = { start, end, inlines, dropped } — [start, end) = 대상 글에서 끼운 범위(newParagraph 로 나눈 \n 은 빼고), inlines = 끼운 범위에 든 글 속 개체의 서수들(doc.inlines 의 index), dropped = 옮기지 못해 뺀 각주·미주 참조 수.
  • 같은 edit 의 뒤 op 는 복제된 글을 본다 — 돌려받을 범위를 미리 셀 수 있으면 같은 edit 에서 고친다. 보통은 결과를 받아 다음 edit 에서 고친다(아래 예).
  • 글 속 개체가 든 범위는 글 상자의 글(to.id 가 수)과 1.9 표 칸의 글(to.id 가 { id, cell }·{ id|in, inline, path?, cell })에만 붙인다 — 글 속 개체 안의 글 상자는 아직 글 속 개체를 갖지 못한다.
  • 각주·미주 참조는 옮기지 않는다(주석 글은 원래 글에 매여 있다) — 빼고 dropped 로 센다. 페이지 번호 같은 그 밖의 변수 글은 옮긴다.
  • 잠긴 글에는 끼우지 않는다. 범위가 비면 아무것도 바꾸지 않는다(start = end 를 돌려준다).
// 데이터베이스 퍼블리싱 — 템플릿 글(단락째)을 레코드마다 대상 글 끝에 복제하고 <필드> 자리를 채운다
const tpl = (await podolay.doc.texts()).find(t => t.text.includes('<name>'));   // 대지에 둔 템플릿 글 상자도 찾는다
const len = tpl.text.length;
await podolay.batch('명단 만들기', async (signal) => {
  for (const rec of records) {
    signal.throwIfAborted();
    const r = await podolay.edit('복제', 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);                     // 글 전체 (Graphic.text)
    await podolay.edit('채우기', tx => {
      // 뒤에서부터 바꿔야 앞 자리가 밀리지 않는다
      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 의 { story }

1.6 — Story 를 단락째(단락 스타일·단락 직접 서식째) 글에 끼운다. setText 처럼 글 전체를 바꾸지 않고 붙이거나 범위만 바꾼다.

await podolay.edit('덧붙이기', tx => {
  tx.appendText(bodyId, [{ para: '소제목', text: '3장' }, { para: '본문', text: '…' }]);          // 글 끝에, 새 단락에서
  tx.replaceRange(bodyId, s, e, { story: [{ para: '인용', text: '…' }] });                        // 범위를 단락째
});
  • appendText(id, story, { newParagraph? }) — 글 끝에 붙인다. newParagraph 기본 true(글 끝 단락에 글이 있으면 먼저 단락을 나눈다), false = 마지막 단락에 이어 붙인다. 결과 { start, end, inlines }.
  • replaceRange(id, start, end, { story }) — 범위를 지우고 그 자리에 Story 를 끼운다. 결과 { start, end }.
  • 바탕 서식 = 끼울 자리 단락의 단락 서식과 그 글자(글 끝이면 마지막 글자)의 글자 서식. 그 위에 Story 의 단락 스타일 → 단락 직접 서식, 런의 문자 스타일 → 직접 서식. 단락 서식은 copyRange 와 같이 단락 끝을 따라간다 — Story 의 마지막 단락은 열린 조각이라 끼운 자리 단락의 서식을 갖는다(글 끝에 새 단락으로 붙이면 Story 의 것).
  • 글 속 개체 런 { inline } 은 글 상자의 글과 1.9 표 칸의 글에만. Story 입력 한도(20만 자·1만 단락)는 한 번의 입력에 대한 것이다 — 여러 번 붙여 글을 더 길게 만들 수 있다.

9.19 tx.insertRows · tx.removeRows

1.6 — 표의 행을 끼우고 지운다. target = 스프레드의 표 id(그룹 안 포함) 또는 글 속 표 { id: 글, inline: 서수, path? }. 표의 짜임은 doc.table.

// 1행(템플릿 행)을 레코드 수만큼 복제해 머리행 아래에 넣고 칸을 채운다
const r = await podolay.edit('행', tx => tx.insertRows(tableId, { after: 1, count: items.length, copyFrom: { row: 1 } }));
await podolay.edit('채우기', tx => r.results[0].rows.forEach((row, k) => tx.setText({ id: tableId, cell: [row, 0] }, items[k].name)));
await podolay.edit('템플릿 행 지우기', tx => tx.removeRows(tableId, 1));

// 표 끝(꼬리행 위)에 행을 붙인다 — after: t.rows - 1 이면 꼬리행이 있는 표에서는 꼬리행이 는다
const t = await podolay.doc.table(tableId);
await podolay.edit('행 붙이기', tx => tx.insertRows(tableId, { after: t.rows - t.footerRows - 1, copyFrom: { row: t.rows - t.footerRows - 1 } }));
insertRows 의 o설명
after이 행 뒤에 끼운다 (-1 = 맨 앞)
count끼울 행 수 (1–1000). 기본 = copyFrom.count, 그것도 없으면 1
copyFrom{ row, count?, table? } — 이 행들(기본 1행)을 차례로 되풀이해 칸 서식·글·칸 배경·행 높이·행 경계선·칸 테두리까지 복제한다. 없으면 빈 행. 1.8 table = 다른 표(열 수가 같아야)에서
  • 결과 { rows: [새 행 번호…] } — 편집이 끝난 뒤의 번호.
  • 병합: 끼울 자리를 위아래로 가로지르는 병합 칸이 있으면 거절한다. 복사할 행 밖으로 걸친 병합도 거절. 복사할 행에 세로 병합이 있으면 count 는 copyFrom.count 의 배수여야 한다.
  • 머리행·꼬리행 (편집기와 같다): 새 행 자리가 머리행 안이면(after < headerRows - 1) 머리행이, 꼬리행 안이나 그 아래면(after ≥ rows - footerRows) 꼬리행이 는다. 머리행 바로 아래(after: headerRows - 1)와 꼬리행 바로 위(after: rows - footerRows - 1)에 끼운 행은 본문이다. 꼬리행을 지우면 꼬리행이 준다.
  • 1.7 copyFrom 으로 복제한 행은 칸 스타일 이름(doc.table 의 cellStyles)도 따라온다. 행을 끼우고 지우면 cellStyles 의 행도 함께 밀린다.
  • 1.8 copyFrom.table — 다른 표(스프레드 표 id 나 글 속 표 주소)의 행을 병합·칸 배경·경계선·칸 테두리·행 높이째 복제한다. 열 수가 다르면 오류. 여러 템플릿 표의 행을 한 표에 섞을 때. 칸 테두리(칸마다 바꾼 변)도 이제 함께 옮긴다(같은 표에서 복제할 때도).
  • removeRows(target, at, count = 1) — at 부터 count 행을 지운다. 결과 = 지운 행 수. 모든 행을 지울 수는 없다.
  • 스프레드의 표는 표 구조 Undo, 글 속 표는 주인 글을 바꾸는 Undo 에 든다. 잠긴 표(글 속이면 주인 글)는 안 된다.

9.20 tx.autoflow(id, o)

1.6 — 넘친 글을 흘린다(인디자인의 Shift-클릭 자동 흐름). 체인 끝이 넘치는 동안 마지막 프레임의 쪽 뒤에 쪽을 끼우고, 그 쪽의 같은 자리·크기·옵션(단·인셋·칠)에 프레임을 놓아 잇는다. 마지막 프레임이 기본 텍스트 프레임이면 새 쪽 마스터의 기본 텍스트 프레임을 쓴다.

await podolay.edit('붙이기', tx => tx.appendText(bodyId, longStory));
const r = await podolay.edit('흘리기', tx => tx.autoflow(bodyId, { maxPages: 200 }));
r.results[0];   // { pages: [5, 6, 7], frames: [311, 312, 313], overflow: false }
o 키설명
master새 쪽의 마스터 id·이름·null(없음). 빼면 이웃 쪽을 따른다
maxPages끼울 쪽의 상한 (1–1000, 기본 100)
  • 결과 { pages, frames, overflow } — 새 쪽 번호(편집이 끝난 뒤), 새 프레임 id, 다 흘리고도 남았는가(maxPages 에 닿았거나 프레임보다 큰 개체로 더 흐르지 않을 때 true).
  • 쪽 op 다 — 문서 스냅숏 Undo·요금제 쪽 수 한도·마스터 편집 중 안 됨(§9.13). 마스터의 글·대지에 놓인 마지막 프레임·제목용 텍스트·패스 위 글은 안 된다.
  • 레이아웃은 지금 불러온 서체로 잰다 — 아직 오지 않은 서체 조각이 있으면 나중에 흐름이 조금 달라질 수 있다.

9.21 tx.duplicate(id, o)

1.6 — 스프레드에 바로 든 개체(그룹째)를 복제한다.

o 키설명
spread놓을 스프레드 번호 (기본 원본의 스프레드)
dx · dy옮길 거리 pt (기본 0 — 같은 자리)

결과 { id, copies } — id = 사본 id, copies = 그룹 안 자식까지 { 원본 id: 사본 id }. 새 개체는 created 에도 든다.

9.22 tx.setTableStyle · tx.setCellStyle

1.7 — 표·칸 스타일을 이름으로 입힌다. target 은 doc.table 과 같다.

await podolay.edit('표 꾸미기', (tx) => {
    tx.setTableStyle(tableId, '줄무늬');                         // 표 전체 — 영역 칸 스타일·번갈아 칠까지
    tx.setCellStyle(tableId, [[3, 0], [3, 1]], '합계 칸');      // 칸 [행, 열] 들 (병합에 덮인 칸이면 그 병합 칸)
    tx.setTableStyle(otherId, '줄무늬', { clearOverrides: true }); // 칸에서 따로 바꾼 값도 스타일 값으로
    tx.setCellStyle(tableId, [[0, 0]], null);                    // null = 떼기 (값은 그대로)
});
  • 값은 표·칸에 그대로 들어간다(편집기의 표 스타일 규칙과 같다 — 사용자가 칸에서 따로 바꾼 값은 남고, clearOverrides 면 지운다).
  • 없는 이름이면 오류. 스타일 목록은 doc.tableStyles().
  • 잠긴 표는 바꿀 수 없다. 되돌리기 한 번에 돌아간다(같은 edit 안의 다른 연산과 함께).

9.23 tx.mergeCells · tx.unmergeCells

1.8 — 칸을 병합하고 푼다. target 은 doc.table 과 같고, 범위는 [행, 열, 행 수 = 1, 열 수 = 1].

await podolay.edit('제목 행', tx => tx.mergeCells(tableId, [row, 0, 1, 3]));   // 한 행의 세 칸을 하나로
await podolay.edit('풀기', tx => tx.unmergeCells(tableId, [row, 0]));           // 그 칸과 겹치는 병합을 푼다
  • mergeCells — 범위 안에 든 병합은 흡수한다. 범위에 걸치는 병합이 있으면 오류(범위를 넓히거나 먼저 푼다). 결과 = 바뀌었는가.
  • unmergeCells — 범위와 겹치는 병합을 모두 푼다. 결과 = 푼 것이 있는가.
  • 병합된 칸의 글은 왼쪽 위 칸의 것이 보인다(편집기의 칸 병합과 같다). 잠긴 표는 안 된다.

10. 일괄 작업 — podolay.batch

1.1 🔑 document:write → fn 이 돌려준 값

await podolay.batch('카탈로그 만들기', async (signal) => {
  const r = await podolay.edit('복제', tx => tx.duplicatePages({ from: 2, times: items.length }));
  for (let k = 0; k < items.length; k++) {
    signal.throwIfAborted();                                         // 사용자가 취소했으면 멈춘다
    await podolay.ui.progress({ label: items[k].name, value: k + 1, max: items.length });
    await podolay.edit('채우기', tx => tx.setText(r.results[0].copies[k][titleId], items[k].name));
  }
});
동작내용
Undo안의 모든 edit 가 한 단계 (플러그인 이름: name)
저장안의 edit 는 화면에만 바로 보이고, 끝날 때 문서 전체를 한 번 저장한다
실패fn 이 던지면 시작 전으로 되돌리고 batch 가 그 오류를 던진다
취소사용자가 진행 창의 취소 → signal.aborted = true. 되돌리고 batch 가 일괄 작업이 취소되었습니다 를 던진다
취소를 무시하면5초 안에 끝내지 않으면 편집기가 직접 되돌린다. 그 뒤 이 플러그인의 edit 는 batch 가 끝날 때까지 거절된다
그동안편집기는 막혀 사용자가 편집할 수 없다(Undo·단축키 포함). 진행 창은 0.5초가 지나야 보인다. 다른 플러그인의 edit 도 거절된다
끝난 뒤시작 때 고른 개체가 남아 있으면 다시 고른다(되돌려도)
겹침batch 안에서 batch 를 부를 수 없다
남는 것되돌려도 assets.upload 로 올린 그림 자산은 남는다(문서에서 안 쓰면 갤러리에서 지울 수 있다)
시간 1.6총 시간 한도는 없다. 편집기 쪽 API 호출에도 시간 한도가 없다. 명령(60초)·패널 메시지(8초)의 응답 시간은 그 플러그인의 batch 가 도는 동안 실패로 보지 않는다 — 명령·패널 메시지 처리기에서 긴 batch 를 await 해도 된다. 멈췄는지는 계속 묻는다(동기 코드로 막혀 답하지 못하면 꺼진다)
화면 1.6안의 edit 는 화면을 250ms 에 한 번만 다시 그린다(예전엔 edit 마다) — edit 를 수천 번 불러도 그리기가 일을 앞지르지 않는다
크기edit 하나 2000 op 까지. 권장은 edit 하나에 레코드 수십 개 — 너무 잘게 나누면 왕복이, 너무 크게 묶으면 한 번의 적용이 길어진다
메모리Undo 는 시작·끝의 문서 스냅숏 두 벌이다(문서 JSON 크기 × 2) — 안의 edit 수와 상관없다

ui.progress({ label?, value?, max? }) 1.1 🔓 — batch 안에서만. 진행 창의 글(200자)과 막대. max 가 없으면 막대가 오가는 모양, value/max 를 주면 3 / 10 처럼 보인다.

signal 은 { aborted: boolean, throwIfAborted(): void }.

batch 안에서는 ui.pickFiles·selection.setText 를 부를 수 없다 — 파일은 batch 전에 고른다.


11. 화면 — podolay.ui · 패널의 podolayUI

11.1 ui.toast(message, kind)

1.0 🔓 — 편집기 알림. kind = 'info'(기본) · 'success' · 'error'. 앞에 플러그인 이름이 붙고 300자에서 자른다.

11.2 패널 html 과 podolayUI

패널은 contributes.panels 의 html 을 띄운 떠 있는 창이다. 플러그인과 같은 샌드박스 규칙으로 돈다 — 네트워크 없음(네트워크 권한이 있어도 패널에는 없다), 인라인 스크립트·스타일만, 그림은 data:. 편집기의 어두운 기본 모양(글자·단추·입력 칸)이 먼저 깔리고 패널 CSS 로 덮을 수 있다.

패널은 편집기 API 를 직접 부르지 못하고 자기 플러그인의 main.js 와만 말한다:

// panel.html 안
podolayUI.postMessage({ type: 'apply', value: 3 });     // main.js 의 ui.onMessage 로
podolayUI.onMessage((msg) => { … });                    // main.js 의 ui.postMessage 에서
  • 1.6 패널에서 파일 읽기 — 패널(sandbox="allow-scripts" iframe) 안의 <input type="file">(multiple·webkitdirectory)·끌어 놓기·FileReader 가 그대로 된다. 읽은 바이트는 podolayUI.postMessage(msg, transfer?) 로 main.js 에 넘긴다 — transfer = 넘길 ArrayBuffer 배열(복사하지 않고 옮긴다, 그 뒤 패널에서는 빈다). 메시지 속 ArrayBuffer 는 400만 자 한도에 세지 않는다(따로 상한 없음 — 메모리만). 패널 없이 고르려면 ui.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>
  • 키는 패널 안에서 끝난다 — 패널에 포커스가 있을 때 누른 키(⌘Z·⌘C·Delete 등)는 편집기로 가지 않는다(따로 된 샌드박스 iframe 이라 편집기가 그 키를 받지 못한다). 입력 칸의 ⌘Z 는 브라우저가 그 칸에서만 되돌리고, 문서 Undo 와 섞이지 않는다. 문서 Undo 는 사용자가 캔버스를 누른 뒤에 된다.

11.3 ui.showPanel(id) · ui.closePanel(id)

1.0 — showPanel 🔑 ui: 패널을 연다(이미 열려 있으면 그대로). closePanel 🔓: 닫는다. 사용자가 닫으면 panelClose 이벤트.

11.4 ui.postMessage · ui.onMessage

1.0 🔓

await podolay.ui.postMessage('main', { count: 3 });              // 패널이 열려 있어야 한다 (아니면 오류)
podolay.ui.onMessage(async (msg, panelId) => { … });             // 패널의 podolayUI.postMessage
  • 메시지는 구조화 복제로 간다(JSON 같은 값). 한 메시지 400만 자 이하 — 1.6 그 안의 ArrayBuffer 는 세지 않는다. 편집기는 패널 메시지·pickFiles 결과의 ArrayBuffer 를 복사하지 않고 플러그인에 넘긴다.
  • onMessage 처리기에서 오래 걸리는 일을 await 하지 않는다 — 편집기가 처리기 호출에도 8초 제한을 둔다. 오래 걸리는 일은 void doWork() 처럼 따로 돌린다. 1.6 다만 그 플러그인의 batch 가 도는 동안은 8초를 넘겨도 실패로 보지 않는다 — 처리기에서 batch 를 시작해 await 해도 된다.

11.5 ui.pickFiles(o)

1.6 🔓 → PickedFile[] — 사용자 PC 의 파일이나 폴더를 읽는다. 브라우저는 사용자가 누른 곳에서만 파일 고르기를 열 수 있어 워커는 직접 열지 못한다 — 그래서 편집기가 플러그인 이름을 단 고르기 창을 띄우고, 사용자가 그 창의 단추로 고른다. 고른 파일만 플러그인에 간다(그래서 권한이 따로 없다).

const files = await podolay.ui.pickFiles({ directory: true });            // 폴더째 — 사진 폴더·데이터 파일
// [{ 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 키설명
accept<input accept> 와 같은 꼴 ('.csv', 'image/*', 500자까지)
multipletrue = 여러 파일
directorytrue = 폴더를 고른다 (그 안의 파일 모두)
돌려주는 키설명
name파일 이름
path폴더를 고르면 그 폴더 이름부터의 상대 경로(photos/2024/a.jpg), 아니면 name 과 같다
type브라우저가 아는 MIME 형식 (모르면 '')
size바이트 수
bytesArrayBuffer — 그대로 assets.upload 에 넘길 수 있다
  • 취소하거나 아무것도 고르지 않으면 [].
  • 한 번에 5000개, 모두 1GB 까지(넘으면 오류). 다른 고르기 창이 열려 있거나 batch 중이면 오류 — 파일은 batch 전에 고른다.
  • 패널이 있는 플러그인은 패널의 <input type="file"> 로 골라도 된다(§11.2).

12. 저장소·라이선스 — podolay.storage · podolay.license

1.0 🔑 storage

const s = (await podolay.storage.get()) ?? {};
await podolay.storage.set({ ...s, lastRun: Date.now() });
  • 계정 × 플러그인마다 하나의 JSON 값(64KB 이하). 문서와 무관 — 사용자 설정·API 키처럼 사람에게 딸린 값에.
  • 개발 모드(pluginDev)에서는 서버에 쓰지 않고 그 창에만 남는다.
  • API 키를 main.js 에 넣지 말 것(배포되는 코드라 누구나 본다) — 사용자가 패널에 넣은 자기 키를 여기 둔다.
  • 1.8 storage.getFrom(pluginId) — 매니페스트 dataFrom의 내 다른 플러그인의 저장소를 읽기만 한다(무료판의 프리셋을 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(`체험 중 — ${new Date(lic.trialEndsAt).toLocaleDateString()} 까지`);
키형식설명
licensed불샀거나(PodoLay 가 준 것 포함) 무료 플러그인이면 true, 체험 중이면 false
source문자열free(무료 플러그인) · purchase(샀다) · grant(PodoLay 가 줬다) · trial(체험 중) · dev(개발 모드)
trialEndsAt문자열 · null체험이 끝나는 때(ISO) — 체험한 적이 있으면
  • 유료 플러그인은 PodoLay 가 가격 단계($9 이상 $1,000 미만, $9·19·29…999 처럼 9로 끝남)에서 값을 정해 판다. 한 번 사면 계정에 영구 라이선스 — 새 판으로 업그레이드해도 다시 내지 않고, 요금제와 상관없이(무료 요금제에서도) 돈다. 체험 일수(0~30일, 0 = 체험 없음)가 있으면 처음 설치한 때부터 그동안 쓸 수 있다(계정마다 한 번).
  • 편집기는 산 사람·체험 중인 사람에게만 유료 플러그인을 불러온다 — 플러그인이 돌고 있다면 쓸 수 있는 상태다. 이 값은 체험 중 안내처럼 화면에 보일 것을 정할 때 쓴다. 체험이 끝나면 다음에 편집기를 열 때부터 불러오지 않고, 이미 만든 개체는 저장된 대체 그림으로 남는다.
  • 2026-10 현재 유료로 팔 수 있는 것은 PodoLay 가 만든 플러그인뿐이다(개발자 콘솔의 가격 줄). 다른 개발자의 판매는 결제 대행사와 확인한 뒤 연다.
  • 개발 모드·샌드박스에서는 늘 { licensed: true, source: 'dev' }. 모의 편집기는 createHarness(m, { license })·h.license 로 바꿔 시험한다.

13. 그림 올리기 — 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 권한
const { asset } = await podolay.assets.upload(await res.arrayBuffer(), { name: 'p1', type: 'image/jpeg' });
await podolay.edit('사진', tx => tx.setImage(frameId, asset));
인자설명
bytesArrayBuffer · Uint8Array · Blob — 30MB 까지
opts.typeimage/png · image/jpeg · image/gif · image/tiff · image/webp(PNG 로 바뀜)
opts.name파일 이름(확장자 없이, 80자) — 갤러리에 보인다
opts.reuse1.6 'name' = 문서 자산 중 같은 파일 이름·같은 바이트 수인 것이 있으면 올리지 않고 그것을 쓴다 · false(기본)
  • 사용자 계정으로 이 문서의 이미지 자산을 만든다 — 요금제 저장 한도·서버 형식 검사는 보통 업로드와 같고, 인쇄에는 원본이 그대로 간다.
  • 돌려준 asset 을 tx.setImage·tx.create({ kind: 'image' }) 에 쓴다. width·height 는 원본 픽셀.
  • OffscreenCanvas 로 그린 그림도 canvas.convertToBlob() 으로 올릴 수 있다.
  • 1.6 같은 내용은 다시 올리지 않는다 — 결과에 내용 해시 sha256(16진)과 reused 가 실린다. 이 창(편집기를 연 동안)에서 같은 내용을 이미 올렸으면 그 자산을 돌려준다(reused: true). reuse: 'name' 은 서버의 문서 자산에서 같은 이름(name + 확장자)·같은 크기를 찾는다 — 다시 연 문서에서 같은 사진 폴더를 다시 돌릴 때. 창을 넘어 내용으로 가리려면 플러그인이 sha256 → asset 표를 문서 데이터(tx.setDocData)에 적어 두고 다음에 먼저 찾는다 — 서버 자산에는 해시가 없다.
    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);                                  // 올리기 전에 직접 잰다 (아래 sha256)
      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('사진 표', (tx) => tx.setDocData({ hashes: known }));
    
    async function sha256(bytes) {                                         // 업로드 결과의 sha256 과 같은 16진
      const d = await crypto.subtle.digest('SHA-256', bytes);
      return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, '0')).join('');
    }
    crypto.subtle 은 편집기 2.14.14 부터 있다(digest 만) — 그 전 편집기를 받쳐야 하면 minEditor: "2.14.14" 를 적거나, 해시를 순수 JS 로 잰다.

assets.list(q)

1.6 🔑 document:read → AssetInfo[] — 이 문서의 그림 자산(서버의 문서 이미지 자산, 5000개까지).

const all = await podolay.assets.list();                  // [{ asset, name, width, height, size, used }, …]
const one = await podolay.assets.list({ name: 'a.jpg' });  // 그 파일 이름만
키설명
asset자산 경로 — tx.setImage·tx.create({ kind: 'image' }) 에 쓴다
name파일 이름(확장자 포함) — q.name 은 이것과 똑같은 것만
width · height원본 픽셀
size바이트 수 (모르면 null)
used지금 문서에서 쓰는 중인가
  • 목록에 나온 자산은 이 창에서 tx 가 쓸 수 있다(올리지 않았어도).
  • 샌드박스 편집기에는 서버가 없어 이 창에서 올린 것과 문서가 쓰는 것만 나온다.

14. 패키지 파일 — podolay.package

1.4 🔓 — 플러그인 패키지 안의 파일을 읽는다. 사전·변환표 같은 큰 데이터를 main.js 와 떼어 두고 필요할 때 읽는다.

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
규칙내용
경로패키지 안 상대 경로(data/x.json, 앞의 ./ 는 된다). / 로 시작·..·\ 안 됨, 200자 이하
크기파일 하나 20MB 까지 (패키지 전체는 zip 20MB·풀어서 40MB)
파일 종류패키지에 넣을 수 있는 것만 — js json html css svg png jpg jpeg webp woff2 txt md bin csv tsv (bin·csv·tsv 는 1.4 와 함께 더함)
개발 모드개발 서버가 플러그인 폴더에서 바로 준다 — 고친 파일은 다음 호출부터
모의 편집기플러그인 폴더(loadPlugin 에 준 폴더)에서 같은 규칙으로 읽는다
  • 코드를 import 할 수는 없다 — 워커는 main.js 만 싣고 CSP 가 그 밖의 스크립트를 막는다. 코드는 빌드로 main.js 에 묶고, 데이터만 따로 읽는다.
  • 읽은 값은 플러그인 안에 두면 다시 읽지 않는다 — let table; … table ??= await podolay.package.json('data/ipa.json');.

15. 네트워크 — fetch · WebSocket · EventSource

매니페스트에 network:<호스트> 가 있을 때만 이 셋이 있다(없으면 undefined).

"permissions": ["network:api.example.com", "network:*.cdn.example.com", "network:api.example.com:8443"],
"privacyUrl": "https://example.com/privacy"
규칙내용
호스트공개 이름만 — IP·localhost·사설 이름(.local·.internal 등) 안 됨. *. 는 맨 앞 한 단계, *.com 같은 넓은 것 안 됨. 10개까지
스킴https · wss 만 (개발 모드는 http://localhost·127.0.0.1 도)
요청쿠키·리퍼러 없음, 캐시 없음. Origin 은 null — **상대 서버가 CORS 로 * 나 null 을 허용해야** 한다
주소절대 주소만. 허용되지 않은 주소면 fetch 는 거절, WebSocket·EventSource 는 SecurityError
로그요청마다 로그 창에 [network] GET 호스트 → 상태 (경로·본문은 남기지 않는다)
없는 것XMLHttpRequest·importScripts·워커 만들기 — 늘 없다(코드를 내려받아 돌릴 수 없다)
패널패널에는 네트워크가 없다 — main.js 가 받아 패널에 넘긴다
DB브라우저 코드라 DB 에 바로 붙을 수 없다 — DB 앞에 HTTP API 를 둔다

16. 데이터 형식

16.1 색

쓸 때 (PodolayColor)뜻
'#rgb' · '#rrggbb'RGB
{ cmyk: [c, m, y, k] } (0–100)CMYK — 인쇄 잉크 그대로 간다
{ gray: k } (0–100)K 잉크 하나

읽을 때(Graphic.fill 등)는 위에 더해 별색 { spot: '이름', tint: 0–100, rgb: '#rrggbb' }, 칠이 없으면 null.

16.2 언어별 글

문자열 하나(모든 언어) 또는 { "en": "…", "ko": "…" }. 편집기는 UI 언어의 값을, 없으면 en, 그것도 없으면 아무 값을 쓴다. 값마다 200자(설명은 2000자).

16.3 로컬 이름 · Frame — 사각형

  • 로컬 이름: 영문으로 시작하는 [A-Za-z0-9_-] 64자 이하 (그래픽 type, 툴·명령·패널 id, 인스펙터 key).
  • Frame: { x, y, width, height } pt. 개체는 스프레드 좌표.

16.4 Graphic — 개체

{
  id: 101, kind: 'text',
  frame: { x: 40, y: 60, width: 200, height: 80 },   // 스프레드 좌표 (돌리기 전)
  rotation: 0, skew: 0, opacity: 1, visible: true, locked: false,
  fill: '#ffffff' | {cmyk} | {gray} | {spot, tint, rgb} | null,
  stroke: { color, width } | null,
  pluginData: …,            // 이 플러그인의 표시 (없으면 null)
  plugin: { id, type, data? },   // 플러그인 개체만 — data 는 자기 플러그인 것일 때만
  text: '…',                // 글이 있는 개체 — 글 전체(서식 없음)
  image: '/media/…',        // 그림을 넣은 개체 — 그 에셋 경로 (그룹 자식·글 속 개체도, 편집기 2.14.13 부터)
  children: [ … ],          // 그룹 — doc.graphics·doc.graphic 에서
}

kind = rectangle ellipse polygon line path polyline text titletext(제목용 텍스트) image table group plugin other.

16.5 DocInfo · TextStatus · StyleInfo

§7.1 · §7.9 · §7.6.

16.6 Story — 글

'첫 단락\n둘째 단락'                                           // 문자열 = 줄마다 단락
[
  { para: '제목', text: '봄 신상품' },                         // 런 하나
  { para: '$base', runs: [
      { text: '정가 ' },
      { text: '12,000원', char: '강조', size: 14, color: { cmyk: [0, 100, 100, 0] }, bold: true },
  ] },
]
단락 키설명
para단락 스타일 이름·id. 내장 '기본'은 '$base'
runs런 배열 (1000개 이하)
text런 하나의 줄임
align1.6 단락 정렬 — left · center · right · justify · justifyCenter · justifyRight · justifyAll · towardsSpine · awayFromSpine
leading1.6 행간 — 글자 크기에 곱하는 배수 (0–20, 예 1.6)
spaceBefore · spaceAfter1.6 단락 앞·뒤 간격 pt (−5000–5000)
indentLeft · indentRight · indentFirst1.6 왼쪽·오른쪽·첫 줄 들여쓰기 pt (−5000–5000)
런 키설명
text글 (줄바꿈은 빈칸 — 단락은 배열로 나눈다)
inline1.2 글 속 개체 — §16.8. 이 런에는 text 를 쓰지 않는다
char문자 스타일 이름·id. '$base' · '$none'
font서체 이름
sizept (0.5–2000)
color색
bold · italic불
tracking1.6 자간 — 1/1000 em (−1000–10000)
hScale · vScale1.6 가로·세로 비율 % (1–1000, 기본 100)
baselineShift1.6 기준선 이동 pt (+ = 위, −1000–1000)
underline · strike · smallCaps1.6 밑줄 · 취소선 · 작은 대문자 (불)
position1.6 'super'(위 첨자) · 'sub'(아래 첨자) · 'normal'
  • 서식 순서: 바탕 → 단락 스타일(+그 단락 스타일의 문자 스타일) → 1.6 단락의 직접 서식 → 런의 문자 스타일 → 런의 직접 서식.
  • 1.6 단락의 align~indentFirst·런의 tracking~position 은 setText·create({ kind: 'text', story })·appendText·replaceRange({ runs }·{ story })·formatRange(런 서식만)·text.measure 가 받는다. doc.text 는 런 서식은 기본값과 다를 때만, 단락 서식은 그 단락 스타일과 다를 때만 싣는다 — 읽은 Story 를 그대로 돌려주면 서식이 남는다.
  • 바탕: tx.create 는 문서의 '기본' 스타일, tx.setText·text.measure({like}) 는 그 글의 첫 단락·첫 글자 서식.
  • 없는 스타일 이름이면 편집 전체가 실패한다(틀린 채 채워지지 않게).
  • Story 입력 한 번은 20만 자·1만 단락까지다 — 글 자체의 한도가 아니다. 더 긴 글은 appendText·copyRange 로 여러 번 붙이고, 넘치면 autoflow 로 프레임을 이어 쪽으로 나눈다.
  • 결합 부호·발음기호 (편집기 2.14.7 부터): 글에 든 결합 부호(U+0301 강세 부호 등, 유니코드 범주 Mn·Me)는 바로 앞 글자 위, 서체의 mark 자리(GPOS)에 얹혀 그려진다 — 화면·PDF 모두. NFC 로 미리 합칠 필요가 없고, ə́(U+0259 U+0301)·ɔ́ 처럼 합쳐진 한 글자가 없는 것도 그대로 넣으면 된다. 부호 둘(ế = e U+0302 U+0301)도 된다. 기본 서체(Noto Sans KR)에 없는 IPA·라틴 확장·간격 수식 글자(ː·ˈ)·그리스·키릴 글자는 Noto Sans 로 그린다(화면과 PDF 가 같은 서체). 다른 IPA 서체를 쓰려면 font·문자 스타일로 정한다 — 2026-10-27 부터 기본 서체에 발음기호 서체 Charis(SIL, 보통·기울임·Medium·SemiBold·굵게와 각 기울임 8면, 6판까지의 이름은 'Charis SIL')와 Doulos SIL(보통체 하나)이 있다. 둘 다 IPA 전체와 결합 부호 자리(GPOS mark)를 갖춘 세리프체라 발음기호 전체가 한 서체로 그려진다(화면·PDF). 서체 목록은 서버가 준다. 단 font 만 바꿔 넣은 글의 서체 파일을 편집기가 받는 것은 2.14.10 부터라, 그 전 판에서는 서체 고르기 창으로 한 번 고르거나 레이아웃을 다시 열기 전까지 대체 서체로 그려진다(PDF 는 처음부터 그 서체) — 이 서체에 기대면 "minEditor": "2.14.10". 기본 서체처럼 라틴 글자 위에 부호를 얹는 정보가 없는 서체(Noto Sans KR 등 한·중·일 서체)에서는 결합 부호가 붙은 그 글자만 Noto Sans(명조 계열은 Noto Serif)로 그린다 — ɑ́·á̀·ṍ (편집기 2.14.8 부터). 편집기 2.14.6 이하에서는 부호가 옆으로 밀리거나 다음 글자 위에 그려지고, 2.14.7 은 기본 서체의 ɑ́·부호 둘이 어긋난다 — 이 표기에 기대는 플러그인은 매니페스트에 "minEditor": "2.14.8" 을 적는다.

16.7 FindQuery

§7.8.

16.8 Inline — 글 속 개체

1.2 — Story 런 { inline }·tx.insertInline 에 주는 개체. 셋 중 하나다.

{ kind: 'image', asset, width: 40, height: 30, fit: 'fit' }            // 새 개체
{ kind: 'plugin', type: 'qr', data: { url }, width: 24, height: 24 }  // 자기 그래픽 (render 를 불러 그림을 만든다)
{ graphic: 205 }                                                      // 스프레드의 개체를 글 속으로 옮긴다
{ index: 1 }                                                          // tx.setText 에서만 — 바꾸기 전 글의 1번 글 속 개체를 그대로
꼴필드
새 개체kind(plugin·rectangle·ellipse·line·text·image) + tx.create 의 그 종류 필드에서 frame 대신 width·height(pt, 0 초과 5000 이하), rotation?(도), pluginData?(이 플러그인의 표시 — doc.inlines 로 다시 찾는다)
{ graphic: id }스프레드에 바로 든 개체(그룹째로)를 옮긴다 — 스프레드에서 빠지고 id 가 없어진다. 잠긴 개체·연결된 텍스트 프레임·그 글 자신은 안 된다. 한 edit 에서 한 번만. 크기·칠·표시는 같은 edit 의 앞 op(tx.set·setPluginData)로 먼저 바꾼다
{ index }tx.setText 에서만 — 바꾸기 전 글의 그 글 속 개체(doc.text 가 돌려준 런을 그대로 돌려주면 된다)
  • 글 속 개체는 글자처럼 줄에 놓인다 — 아래 끝이 기준선에 놓이고, 개체가 크면 그 줄이 높아진다. 단보다 크면 넘친 글이 된다.
  • 1.3 baseline(pt, 0 이상 높이 이하) = 개체 아래 끝에서 글의 기준선까지. 주면 그만큼 내려 놓아 기준선이 개체를 지나고, 기준선 아래 몫은 줄 깊이에 들어간다(선택·클릭 영역도 개체 전체). 첨자·분모가 있는 수식에 쓴다. 새 개체 꼴과 tx.setInline 에서 준다.
  • 그림 목록은 프레임 밖으로 나가도 잘리지 않고 그려진다 — 다만 선택·클릭 영역과 줄 높이는 프레임만 센다.
  • 글 속 개체의 글(kind: 'text') 안에 다시 글 속 개체는 넣을 수 없다.
  • 놓인 뒤에는 사용자가 편집기에서 글 속 개체를 더블클릭해 제자리에서 고칠 수 있다.

16.9 TextTarget — 글 자리

1.4 — 글을 받는 API(doc.text·doc.texts·tx.setText·replaceText·replaceRange·formatRange·1.6 copyRange·appendText·selection.setText)와 명령의 text 인자·text.selection·textSelectionChange 의 id 가 쓰는 글의 주소.

꼴가리키는 글
12텍스트 개체 id — 연결된 프레임이면 어느 id 든 글 전체 (1.1 부터)
{ id: 표, cell: [행, 열] }표 칸 (0부터, 병합에 덮인 칸은 안 됨)
{ id: 글, inline: 서수 }글 속 텍스트 프레임 — doc.inlines 의 index
{ id: 글, inline: 서수, cell: [행, 열] }글 속 표의 칸
{ id: 글, inline: 서수, path: [자식 번호, …] }1.6 글 속 그룹 안의 글 상자 (§16.10)
{ id: 글, inline: 서수, path, cell: [행, 열] }1.6 글 속 그룹 안 표의 칸
  • 객체 꼴은 apiVersion 1.4 이상 (그 아래면 '… 의 글 자리 { id, cell } · { id, inline } 는 apiVersion 1.4 이상에서 씁니다' 오류).
  • 글 속 개체의 글을 바꾸면 주인 글을 바꾸는 Undo 한 단계가 된다. 잠금은 주인 글(표 칸이면 표)을 따른다.
  • 표 칸·글 속 개체 안의 글에는 글 속 개체를 끼울 수 없다(편집기도 아직). 모든 글 자리는 doc.texts 로 얻는다.
  • path 는 apiVersion 1.6 이상.

16.10 InlineTarget — 글 속 개체 주소

1.6 — 글 속 개체에는 id 가 없어서 주인 글과 서수로 가리킨다. 글 속 그룹(과 그 안 그룹)의 자식은 path 로 더 들어간다.

{ id: 12, inline: 0 }              // 글 12 의 0번 글 속 개체
{ id: 12, inline: 0, path: [1] }   // 그 글 속 그룹의 1번 자식
{ id: 12, inline: 0, path: [1, 0] }// 그 자식(그룹)의 0번 자식
받는 곳하는 일
doc.graphic그 개체를 읽는다 (id 없음, 그룹 자식에 path)
doc.table · tx.insertRows · tx.removeRows글 속 표
tx.set · tx.setPluginData · tx.setImage그 개체를 바꾼다
글 자리를 받는 곳 (§16.9)글 속 그룹 안의 글 상자 { id, inline, path } · 표 칸 { id, inline, path, cell }

1.8 글 상자 밖의 글에 든 개체 — { in: 글 자리, inline, path? }. in 은 글 자리(표 칸 { id, cell }, 글 속 표 칸 { id, inline, cell } …), inline 은 doc.inlines(그 글 자리) 의 index. 위 표의 모든 곳이 받는다. { id, inline, cell } 은 전처럼 '글 속 표의 칸' 이다 — 칸 안의 개체는 { in: { id, cell }, inline }.

{ in: { id: 30, cell: [1, 0] }, inline: 0 }            // 표 30 의 [1, 0] 칸 글의 0번 글 속 개체
{ in: { id: 30, cell: [1, 0] }, inline: 0, path: [0] } // 그 글 속 그룹의 0번 자식 (그림 상자)
  • inline = doc.inlines 의 index, path = doc.inlines·doc.graphic 이 그룹 자식에 단 path 를 그대로 쓴다(그룹 안 차례, 0부터, 32단까지).
  • 편집기는 주인 글의 앵커 데이터로 개체를 지어 고친 뒤 주인 글을 바꾸는 명령 하나로 쓴다 — Undo 한 단계, 잠금은 주인 글을 따른다. 크기를 바꾸면 그룹 프레임·글 속 자리 크기를 다시 재고 글이 다시 흐른다.
  • 서수는 같은 edit 의 앞 op 가 바꾼 글에서 센다(앞 op 가 개체를 끼우거나 빼면 밀린다).
  • { graphic: id } 로 글 속에 옮긴 그룹 안의 글 상자는 id 가 없어지므로 이 주소로 다룬다.
  • 문서 JSON(샌드박스 raw)의 글 속 개체 데이터에도 숫자 id 가 들어 있지만 쓰지 않는다 — 편집기는 조판할 때마다 데이터에서 개체를 새로 짓는다. tx.copyRange 로 복제한 글 속 개체는 원본과 같은 id 를 지닐 수 있고, 그래도 괜찮다.

17. 그림 목록 (Display List)

render·툴 미리보기가 돌려주는 그림. render 는 개체 로컬 좌표(원점 = 프레임 왼쪽 위), 툴 미리보기는 스프레드 좌표.

17.1 항목

t필드칠·선
rectx, y, w, hfill stroke opacity
ellipsecx, cy, rx, ryfill stroke opacity
linex1, y1, x2, y2, stroke(필수)opacity
pathd(SVG 경로), rule?: 'nonzero'·'evenodd'fill stroke opacity
groupitemsopacity (안의 불투명도와 곱한다)
useref(정의 이름), x?, y?, scale? 또는 m?: [a, b, c, d, e, f] 1.3fill stroke opacity (아래 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색 — 열린 경로에는 칠하지 않는다
stroke{ color, width, cap?: 'butt'·'round'·'square', join?: 'miter'·'round'·'bevel', dash?: [수…] (16개 이하) }
opacity0–1

17.3 경로 d

SVG 경로 명령 M L H V C S Q T A Z (대문자 = 절대, 소문자 = 상대). Q·T 는 같은 모양의 3차 곡선으로, 원호 A 는 3차 곡선들로 바뀐다(플래그를 붙여 써도 된다: a5 5 0 011 1). M 마다 새 하위 경로.

17.4 다시 쓰는 정의 (defs·use)

1.3 — 같은 모양(수식의 글자 윤곽 등)을 여러 번 그릴 때는 { items, defs } 로 돌려준다. defs 는 이름 → 항목 배열이고, { t: 'use', ref } 가 그 자리에 놓는다 (SVG <symbol>·<use>). 저장되는 것은 짧은 목록 그대로라 문서가 비대해지지 않는다.

  • 이름: 글자로 시작, 글자·숫자·_·- 32자까지.
  • 변환: x, y(옮기기)·scale(0보다 큼, 가로·세로 같이) 또는 2×3 행렬 m (x' = a·x + c·y + e, y' = b·x + d·y + f). m 과 x·y·scale 은 함께 쓸 수 없다.
  • 칠: 정의 안에서 fill·stroke 가 하나도 없는 path·rect·ellipse 만 use 의 fill·stroke 를 받는다. 칠이나 선이 있는 항목은 제 것 그대로. opacity 는 곱한다.
  • 선 굵기: use 에 준 선은 use 가 놓인 좌표 기준 (use 의 scale·m 은 곱하지 않는다). 정의 안 항목의 선은 변환을 따라 늘어난다.
  • 정의 안에서 다른 정의를 use 할 수 있다 (4단까지). 서로 부르면(순환) 거절된다.
  • 2.13 보다 옛 편집기는 use 가 든 그림을 그리지 못한다(개체는 남고 빈 상자로 보인다) — 1.3 플러그인은 그 편집기에서 불러오지 않는다.
{
  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 제한

항목한도
항목 수 (묶음·정의 안 포함, use 도 1개)5000
묶음 깊이8
경로 하나의 d20만 자
경로 d 합100만 자
dash 값 수16
정의(defs) 수1000
use 겹침4단
use 를 모두 편 뒤 그림 수5만
use 를 모두 편 뒤 경로 d 합500만 자

정의의 경로 글은 한 번만 센다 (d 합 100만 자는 저장된 그대로의 합). 넘거나 틀리면 그 그림은 거절되고 로그에 까닭이 남는다.

17.6 다른 곳에서

어디그림 목록은
화면·독자 모드저장된 그림 그대로 (크기가 달라졌으면 늘려 그림)
PDF같은 그림이 벡터 패스로 — 색은 인쇄 색 그대로(CMYK·K)
IDML(InDesign)패스 그룹으로. InDesign 에는 짝수-홀수 칠이 없어 구멍이 메워질 수 있다

18. 제한값 한눈에 보기

항목한도
호출 하나의 답 (render·툴·이벤트·패널 메시지)8초
명령(commands) 하나60초 (1.6 그 플러그인의 batch 가 도는 동안은 넘겨도 실패로 보지 않음 — 패널 메시지의 8초도)
batch 전체 · 편집기 쪽 API 호출시간 한도 없음
플러그인 시작(main.js 맨 위)8초
막힘 판정 (시간을 넘긴 뒤 살아 있는지)2초 더 — 답이 없으면 꺼짐
취소 뒤 batch 를 끝내기까지5초
메시지 하나400만 자 (1.6 ArrayBuffer 는 세지 않음 — 메모리만)
edit 한 번의 op2000
data·pluginData·setDocData 값JSON 256KB
storageJSON 64KB
assets.upload30MB
Story (입력 한 번 — 글 자체에는 한도 없음)20만 자 · 1만 단락 · 단락당 런 1000
글 속 개체 크기 · insertInline 한 op5000pt · 1000개
doc.find 결과기본 1000, 최대 5000
selection.set1000
한 edit 의 새 쪽1000 (그리고 요금제 쪽 수 한도)
addPages.count · duplicatePages.times200 · 500
네트워크 호스트10
툴 · 패널 · 그 밖 contributes10 · 10 · 50
패키지 zip20MB · 파일 500개 · 풀어서 40MB (코드·매뉴얼·예제 모두)
package.read파일 하나 20MB · 경로 200자
doc.texts 결과20000
ui.pickFiles 한 번5000개 · 모두 1GB
assets.list 결과5000
insertRows.count · autoflow.maxPages1000 · 1000 (기본 100)
batch 안 화면 다시 그리기250ms 에 한 번
개발자당 플러그인 · 플러그인당 판50 · 100

19. 오류와 시간 제한

  • 편집기 호출은 실패하면 Error 로 거절된다. 메시지는 한국어로, 무엇이 왜 틀렸는지 적혀 있다 (권한이 없습니다: document:write (매니페스트 permissions 에 넣으세요), 잠긴 개체는 지울 수 없습니다: 101, 단락 스타일이 없습니다: 제목 …).
  • edit·batch 는 전부 되거나 아무것도 안 된다.
  • 시간 제한: 편집기가 플러그인을 부를 때(render·툴·명령·이벤트·패널 메시지) 그 시간 안에 답이 없으면 그 호출만 실패로 친다. 이어서 "살아 있는가"를 묻고 2초 안에 답이 없으면 — 동기 코드로 막혀 있다는 뜻이므로 — 플러그인을 끄고 사용자에게 알린다(다시 켜려면 편집기를 새로 연다). 무거운 계산은 잘게 나누어 await 사이에 둔다. 1.6 명령·패널 메시지는 그 플러그인의 batch 가 도는 동안 시간을 넘겨도 실패로 치지 않고 계속 기다린다(살아 있는지는 계속 묻는다). batch 자체와 편집기 쪽 API 호출에는 시간 한도가 없다.
  • 오류의 자리: 플러그인 코드에서 난 오류는 main.js 의 첫 자리를 붙여 보인다 — command boom: TypeError: … (com.example.qr/main.js:5:12), Uncaught ReferenceError: … (com.example.qr/main.js:8). 구문 오류는 시작 실패로 (…/main.js:줄).
  • 로그 창: 개발 모드에서 편집기 › 플러그인 메뉴 › 로그. console.*, 잡지 못한 오류, 실패한 호출, 네트워크 요청이 모인다(같은 내용이 브라우저 콘솔에도).
  • 개발자 도구: 플러그인 코드는 샌드박스 iframe 안의 워커에서 돈다. Chrome 개발자 도구의 Sources 에서 <플러그인 id>/main.js 를 찾아 중단점을 걸고, Console 의 실행 컨텍스트를 그 워커로 바꾸면 podolay 를 직접 부를 수 있다(await podolay.doc.info()). 패널은 따로 된 iframe 이다.
  • 편집기 없이 검사: node tools.mjs check <폴더> — 매니페스트·정의·render·권한을 편집기와 같은 규칙으로. 개발 서버 터미널에도 로그가 찍히고 파일을 고치면 편집기가 다시 불러온다.
  • 모의 편집기: SDK 의 podolay-mock.mjs 로 명령·edit·batch 를 Node 에서 단위 시험한다(이 문서의 규칙을 그대로 흉내 냄, 조판은 어림).
  • 샌드박스 편집기: node tools.mjs dev <폴더> --open — 로그인 없이, 서버에 저장하지 않는 새 문서로 진짜 편집기를 연다. 다른 터미널에서 tools.mjs run <명령>·state·shot <쪽>(PNG)·test <폴더>(같은 test.mjs 를 진짜 편집기에서)로 조종한다.
  • 디버깅 방법 전체(검사·터미널 로그·로그 창·개발자 도구·자주 보는 오류와 고치는 법·모의 편집기)는 README.md §8 디버깅.

20. 판 사이 바뀐 것

판더해진 것
1.0그래픽·툴·명령·단축키·패널·인스펙터 필드, doc.info/graphics/graphic/raw/getData, selection, edit(create·set·remove·setData·setPluginData·setDocData), ui, storage, 이벤트, 그림 목록
1.1네트워크 권한(network:), Story 글(doc.styles/text/find, tx.create text story·setText·replaceText), 그림(assets.upload, tx.create image·setImage), 쪽(addPages·duplicatePages·removePages·applyMaster, doc.info 의 master·label·masters), 글 흐름(link·unlink·doc.textStatus·text.measure), batch·ui.progress, edit 결과의 results, 원호 A 곡선
1.2글 속 개체(인라인 앵커): Story 런 { inline }, tx.insertInline·removeInline, doc.inlines, doc.text 의 개체 런; 우클릭 메뉴: 매니페스트 menus[].context(header·object·text·canvas)·when(kind·type·single), 우클릭에서 불린 명령의 인자 arg. 편집기 2.13 에서 더함(판은 그대로): 글 속 개체 우클릭 메뉴와 arg.inline·selectionChange.inline, documentChange.origins, dataChange 이벤트
1.3자연 크기(graphics[].autoSize·size(data, frame)), 인스펙터 필드 live·hideInline, tx.setInline·tx.replaceRange, 글 속 개체의 baseline, 그림 목록의 defs·use
1.4글 편집 중 명령(헤더 메뉴·단축키도 text 인자, 끝나면 편집을 다시 연다)·text.selection·textSelectionChange 이벤트·selection.setText, 단축키 수식키 ctrl(Mac ⌃), tx.formatRange·tx.replaceRange 의 { runs }, 글 자리(표 칸 { id, cell }·글 속 개체 { id, inline })와 doc.texts, podolay.package.read·text·json(패키지 확장자에 bin·csv·tsv). 편집기 2.14.7 에서 함께(판과 무관): 결합 부호를 앞 글자 위에 그림, 기본 서체에 없는 IPA·라틴 확장·그리스·키릴 글자는 Noto Sans — §16.6
1.5podolay.license.status() — 유료 플러그인(가격 단계·체험·영구 라이선스, 요금제와 무관)의 라이선스 (편집기 2.14.12 부터)
1.6데이터베이스 퍼블리싱용 (편집기 2.14.12 부터): tx.copyRange(서식째 글 복제)·tx.appendText·tx.replaceRange 의 { story }(단락째 끼우기), 글 속 개체 주소 { id, inline, path? }(tx.set·setPluginData·setImage·doc.graphic·doc.table·글 자리, doc.inlines·doc.graphic 그룹 자식의 path), 표 doc.table·tx.insertRows·tx.removeRows, tx.autoflow, tx.duplicate, podolay.ui.pickFiles·패널 podolayUI.postMessage(msg, transfer), assets.list·assets.upload 의 reuse·sha256·reused, Story 의 런 서식(tracking·hScale·vScale·baselineShift·underline·strike·position·smallCaps)과 단락 서식(align·leading·spaceBefore·spaceAfter·indent*). 편집기 쪽(판과 무관): batch 중 명령·패널 메시지 시간 제한 없음, batch 안 화면 그리기 250ms 에 한 번, duplicatePages 의 copies 에 그룹 자식
1.7표·칸 스타일: doc.tableStyles(), doc.table 의 tableStyle·cellStyles, tx.setTableStyle·tx.setCellStyle (편집기 2.16.2 부터)
1.9표 칸의 글도 글 속 개체를 받는다 — 글 자리가 표 칸({ id, cell }·{ id|in, inline, path?, cell })이면 setText·appendText·replaceRange({ story }·{ inline })의 글 속 개체 런과 copyRange 의 글 속 개체가 든 범위가 들어간다. 글 속 개체 안의 글 상자는 아직. 칸 글 폭보다 넓은 개체는 줄에 들어가지 못한다 (편집기 2.18.0 부터)
1.8표 칸 안의 글 속 개체 { in: 글 자리, inline, path? }·doc.inlines(글 자리), tx.mergeCells·tx.unmergeCells, insertRows 의 copyFrom.table(다른 표에서, 칸 테두리째), 매니페스트 dataFrom(pluginDataFrom·doc.getData(from)·doc.find({ from })·storage.getFrom) (편집기 2.17.2 부터). 편집기 쪽(판과 무관): doc.texts 가 글 속 그룹 안의 글 상자도 { id, inline, path } 로, 개발 로그 한 줄 200,000자(넘으면 …[+N chars])

API 는 더하기만 한다 — 1.0 플러그인은 1.1 편집기에서 그대로 돈다. 1.1 기능을 쓰는 플러그인을 옛 편집기가 만나면 불러오지 않는다(매니페스트 apiVersion 으로 판단). 특정 편집기 기능이 필요하면 minEditor 를 적는다.