PodoLay 플러그인 API 레퍼런스 (Plugin API 1.9)
플러그인
main.js에서 쓸 수 있는 모든 것을 한곳에 모은 참조 문서다. 처음이라면 README.md(시작하기·개발 흐름·검수 기준)부터 읽는다. 타입 정의podolay-plugin.d.ts와 1:1 로 같다 — 에디터에서/// <reference path="./podolay-plugin.d.ts" />를 쓰면 자동완성이 된다.
표기
| 표시 | 뜻 |
|---|---|
| 1.0 | API 1.0 부터 — 모든 플러그인 |
| 1.1 | API 1.1 부터 — 매니페스트에 "apiVersion": "1.1" 을 적은 플러그인만 (1.0 이면 오류) |
| 1.2 | API 1.2 부터 — 매니페스트에 "apiVersion": "1.2" 를 적은 플러그인만 |
| 1.3 | API 1.3 부터 — 매니페스트에 "apiVersion": "1.3" 을 적은 플러그인만 (편집기 2.13 부터) |
| 1.4 | API 1.4 부터 — 매니페스트에 "apiVersion": "1.4" 를 적은 플러그인만 (편집기 2.14.7 부터) |
| 1.5 | API 1.5 부터 — 매니페스트에 "apiVersion": "1.5" 를 적은 플러그인만 (편집기 2.14.12 부터) |
| 1.6 | API 1.6 부터 — 매니페스트에 "apiVersion": "1.6" 을 적은 플러그인만 (편집기 2.14.12 부터) |
| 1.7 | API 1.7 부터 — 매니페스트에 "apiVersion": "1.7" 을 적은 플러그인만 (편집기 2.16.2 부터) |
| 1.8 | API 1.8 부터 — 매니페스트에 "apiVersion": "1.8" 을 적은 플러그인만 (편집기 2.17.2 부터) |
| 1.9 | API 1.9 부터 — 매니페스트에 "apiVersion": "1.9" 를 적은 플러그인만 (편집기 2.18.0 부터) |
🔑 document:read | 매니페스트 permissions 에 그 권한이 있어야 한다 (없으면 호출이 권한이 없습니다 로 실패) |
| 🔓 | 권한이 필요 없다 |
→ T | Promise<T> 로 돌려준다 — 편집기와 말하는 호출은 모두 비동기다 |
목차
- 실행 환경
- 권한
- 매니페스트
podolay-plugin.json podolay한눈에 보기- 정의 —
graphics·tools·commands - 이벤트 —
podolay.on - 문서 읽기 —
podolay.doc·podolay.text - 선택 —
podolay.selection - 문서 쓰기 —
podolay.edit와tx - 일괄 작업 —
podolay.batch - 화면 —
podolay.ui· 패널의podolayUI - 저장소·라이선스 —
podolay.storage·podolay.license - 그림 올리기 —
podolay.assets - 패키지 파일 —
podolay.package - 네트워크 —
fetch·WebSocket·EventSource - 데이터 형식
- 그림 목록 (Display List)
- 제한값 한눈에 보기
- 오류와 시간 제한
- 판 사이 바뀐 것
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: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 (모든 tx.*) · batch · assets.upload |
selection | selection.get · selection.set · selection.setText · text.selection · selectionChange·textSelectionChange 이벤트 |
ui | ui.showPanel (매니페스트 contributes.panels) |
storage | storage.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 기능을 쓰려면 그 판을 적는다. 편집기가 모르는 판이면 불러오지 않는다 |
minEditor | semver | 이 편집기 판 이상에서만 불러온다 | |
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 | ✔ | 언어별 글 | 인스펙터 제목 등 |
usesFillStroke | true | 프레임의 칠·선을 쓴다 — 인스펙터에 칠·선 줄이 보인다 (프레임 칠은 그림 뒤, 선은 앞) | |
defaultSize | [w, h] (0 < pt < 10000) | 툴로 클릭만 했을 때의 크기 (없으면 100×100) | |
inspector | 필드 배열 (50개 이하) | 개체를 고르면 인스펙터에 나오는 칸 | |
autoSize | true 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 에 필수 |
live | false 1.3 | number 만 — 화살표·스테퍼를 누를 때마다가 아니라 멈춘 뒤(0.3초) 한 번 바꾼다(Undo 한 단계) | |
hideInline | true 1.3 | 글 속 개체로 고를 때는 숨긴다 (정렬·크기 모드처럼 글 속에서 뜻이 없는 칸) |
color 칸의 값은 색 형식이다.
3.4 contributes.tools — 툴바 단추 (10개 이하)
| 필드 | 필수 | 형식 | 설명 |
|---|---|---|---|
id | ✔ | 로컬 이름 | tools.define(id, …) 과 같은 이름 (custom 일 때) |
label | ✔ | 언어별 글 | 풍선 도움말 |
icon | 패키지 안 svg·png | 없으면 퍼즐 아이콘 | |
kind | ✔ | createGraphic · custom | createGraphic = 사각형 툴처럼 끌어서 graphicType 개체를 만든다(편집기가 처리) · custom = 마우스 이벤트를 플러그인에 보낸다 |
graphicType | △ | contributes.graphics 의 type | createGraphic 에 필수 |
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.4ctrl= macOS ⌃(Control), 그 밖에서는mod와 같은 Ctrl — Mac 에서 ⌃⌥C 처럼 ⌘ 아닌 조합을 쓸 때.ctrl은apiVersion1.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 · height | 120–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) |
homepage | https 주소 (500자 이하) | 플러그인 홈페이지 — 링크만 둔다 |
supportUrl | https 주소 (500자 이하) | 지원·문의 주소 |
매뉴얼 — 마크다운: 제목(#·##·###)·문단·목록·표·굵게·코드·링크, 그리고 그림 .
- 그림은 패키지 안 파일만(그
.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') 처럼 읽을 수 있다.
예제 레이아웃 — 플러그인을 쓴 실제 레이아웃을 준다.
- 포털 › 개발자 › 예제 레이아웃 준비 에서 레이아웃을 고르고 '공유 사본 만들기' — 내 '공유' 폴더에 고칠 수 없는 사본이 생기고,
help.layouts에 넣을 줄({ "id": … })이 보인다(복사 단추). 대시보드 카드 메뉴 › 공유(받는 사람 없이)로 만들어도 같다. 공유 사본은 고칠 수 없으니 이 판의 예제로 고정된다. - 그 줄(사본 id)을
help.layouts에 넣는다. - 판을 올릴 때 서버가 그 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.data | dataFrom 플러그인의 그래픽이면 그 데이터도 준다 |
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.apiVersion | 1.0 | 🔓 | '1.7' | 이 편집기의 API 판 |
podolay.env | 1.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.0 | document:read | → DocInfo | 문서·스프레드·쪽·마스터 |
doc.graphics({spread}) | 1.0 | document:read | → Graphic[] | 스프레드의 개체 |
doc.graphic(id) | 1.0 | document:read | → Graphic | null | 개체 하나 |
doc.raw() | 1.0 | document:read | → any | 문서 저장 형식 전체 |
doc.getData() | 1.0 | 🔓 | → any | 이 플러그인의 문서 데이터 |
doc.styles() | 1.1 | document:read | → { para, char } | 단락·문자 스타일 |
doc.text(id) | 1.1 | document:read | → StoryPara[] | 텍스트 개체·글 자리의 글 |
doc.find(query) | 1.1 | document:read | → Graphic[] | 템플릿 자리 찾기 |
doc.textStatus(id) | 1.1 | document:read | → TextStatus | 글 흐름·넘침 |
doc.inlines(id) | 1.2 | document:read | → InlinePlaced[] | 글 속 개체들 |
doc.texts(q) | 1.4 | document:read | → TextPlace[] | 문서의 모든 글 자리 |
doc.table(target) | 1.6 | document:read | → TableInfo | 표의 행·열·병합·머리행 |
doc.tableStyles() | 1.7 | document:read | → { table, cell } | 표·칸 스타일 이름 |
text.measure(story, width, opts) | 1.1 | document:read | → { height, lines } | 넣기 전에 글 재기 |
text.selection() | 1.4 | selection | → TextSelection | null | 편집 중인 글의 선택 |
selection.get() | 1.0 | selection | → number[] | 고른 개체 |
selection.set(ids) | 1.0 | selection | → number[] | 개체 고르기 |
selection.setText(id, start, end?) | 1.4 | selection | → true | 글 범위를 골라 편집 열기 |
edit(name, fn) | 1.0 | document:write | → { created, results } | 문서 바꾸기 (Undo 한 단계) |
batch(name, fn) | 1.1 | document:write | → fn 의 값 | 여러 edit 를 Undo 한 단계로 |
ui.toast(msg, kind) | 1.0 | 🔓 | → void | 알림 |
ui.progress(o) | 1.1 | 🔓 (batch 안) | → void | 진행 창 |
ui.showPanel(id) | 1.0 | ui | → 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.0 | storage | → any · → void | 계정별 설정 |
license.status() | 1.5 | 🔓 | → LicenseStatus | 유료 플러그인의 라이선스·체험 |
assets.upload(bytes, opts) | 1.1 | document:write | → { asset, width, height, sha256?, reused? } | 그림을 문서 자산으로 |
assets.list(q) | 1.6 | document:read | → AssetInfo[] | 문서의 그림 자산 목록 |
package.read(path) · text · json | 1.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 | 필드 |
|---|---|
object | ids = 고른 개체 id 들(우클릭한 개체가 고른 것 밖이면 그것만 고른 뒤) · spread = 스프레드 번호 · point = 우클릭한 자리(스프레드 좌표 pt) |
canvas | spread · page = 그 자리의 쪽 번호(쪽 밖이면 null) · point |
text | id = 편집하던 글 — 텍스트 프레임(체인 머리) 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초 뒤 한 번), 편집이 끝나면 null | selection |
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 …) |
colorMode | CMYK · 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.
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 | 문자열 | 글에 이 글이 들어 있다. 연결된 글은 프레임 하나(체인 머리)만 |
tag | true · 문자열·수 · 객체 | 이 플러그인의 pluginData — true = 있음, 값 = 같음(또는 pluginData.tag 가 같음), 객체 = 그 키들이 같음 |
kind | Graphic.kind | 종류 |
spread | 수 | 그 스프레드에서만 |
page | 수 (0부터) | 개체 프레임의 가운데가 그 쪽 안 |
masters | true | 마스터 스프레드도 (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 });| 인자 | 설명 |
|---|---|
story | Story |
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 } |
offset | Graphic.text 에서의 글자 자리 (개체 자리 글자 U+FFFC) |
baseline | 1.3 개체 아래 끝에서 글의 기준선까지(pt) — 0 이면 빠진다 |
placed | 놓인 자리 — frame = 그 텍스트 프레임 id, x·y·width·height = 스프레드 좌표(프레임을 돌리기 전). 넘친 글 속이면 null |
| 그 밖 | Graphic 과 같다 — id·frame 대신 width·height (글 속 개체에는 id 가 없다) |
children[].path | 1.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.* 에 그대로 넘긴다 |
kind | frame = 글 상자(연결된 글은 머리 하나) · cell = 표 칸 · inline = 글 속 텍스트 프레임 · inlineCell = 글 속 표의 칸 |
text | 글 (Graphic.text 와 같은 꼴 — 단락 사이 \n) |
q 키 | 설명 |
|---|---|
spread | 그 스프레드만 |
masters | true = 마스터 스프레드도 (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) |
tableStyle | 1.7 입힌 표 스타일 이름, 없으면 null |
cellStyles | 1.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]; // 새 개체 idfn은 동기 함수다 — 안에서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.0 | null | 공통 속성 바꾸기 |
remove(id) | 1.0 | null | 지우기 |
setData(id, data) | 1.0 | null | 자기 그래픽의 데이터 (그림도 다시) |
setPluginData(id, value) | 1.0 | null | 아무 개체에 이 플러그인의 표시 |
setDocData(value) | 1.0 | null | 문서 단위 데이터 |
setText(id, story) | 1.1 | null | 글 전체 바꾸기 (템플릿 서식 유지) |
replaceText(id, search, text) | 1.1 | null | 자리표시자 바꾸기 |
setImage(id, asset, fit) | 1.1 | null | 도형에 그림 넣기 |
link(from, to) | 1.1 | null | 텍스트 프레임 잇기 |
unlink(from) | 1.1 | null | 텍스트 프레임 끊기 |
addPages(o) | 1.1 | { pages } | 쪽 추가 |
duplicatePages(o) | 1.1 | { pages, copies } | 쪽 복제 (개체까지) |
removePages(pages) | 1.1 | null | 쪽 지우기 |
applyMaster(pages, master) | 1.1 | null | 마스터 적용 |
insertInline(id, at, inline) | 1.2 | 끼운 수 | 글 속에 개체 끼우기 |
removeInline(id, index) | 1.2 | null | 글 속 개체 빼기 |
setInline(id, index, o) | 1.3 | null | 글 속 개체를 제자리에서 고치기 |
replaceRange(id, start, end, what) | 1.3 | null · { 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.7 | null | 표 스타일 입히기·떼기 |
setCellStyle(target, cells, name, o?) | 1.7 | null | 칸 스타일 입히기·떼기 |
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 | 필드 | 설명 |
|---|---|---|
plugin | type, frame, data? | 자기 그래픽 형식 (render 를 불러 그림을 만든다) |
rectangle · ellipse | frame, fill?, stroke? | fill: 색 · null(없음). stroke: { color?, width? } · null |
line | frame, stroke? | 프레임의 왼쪽 위 → 오른쪽 아래. 기본 검정 1pt |
text | frame, text | 서식 없는 글 |
text 1.1 | frame, story | 스타일 있는 Story. 바탕 = 문서의 '기본' 스타일 |
image 1.1 | frame, asset, fit? | 그림 프레임. asset 은 §13. fit 은 §9.11 |
공통: frame = 스프레드 좌표 pt, spread? = 스프레드 번호(기본 지금 스프레드). 새 개체는 지금 레벨(레이어)에 들어간다.
9.4 tx.set(id, props)
| 속성 | 형식 | 설명 |
|---|---|---|
frame | Frame | 자리·크기 (스프레드 좌표) |
rotation | 수 (도) | |
opacity | 0–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·이름·null | null |
- 쪽 연산이 든
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·생략 = 글 끝 |
inline | Inline — 새 개체 또는 { 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 · italic | Story 런의 직접 서식과 같다 (size 0.5–2000pt) |
tracking · hScale · vScale · baselineShift · underline · strike · position · smallCaps | 1.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.newParagraph | true = 끼울 자리가 단락 처음이 아니면 먼저 단락을 나눈다(새 단락에 붙이기). 기본 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자까지) |
multiple | true = 여러 파일 |
directory | true = 폴더를 고른다 (그 안의 파일 모두) |
| 돌려주는 키 | 설명 |
|---|---|
name | 파일 이름 |
path | 폴더를 고르면 그 폴더 이름부터의 상대 경로(photos/2024/a.jpg), 아니면 name 과 같다 |
type | 브라우저가 아는 MIME 형식 (모르면 '') |
size | 바이트 수 |
bytes | ArrayBuffer — 그대로 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));| 인자 | 설명 |
|---|---|
bytes | ArrayBuffer · Uint8Array · Blob — 30MB 까지 |
opts.type | image/png · image/jpeg · image/gif · image/tiff · image/webp(PNG 로 바뀜) |
opts.name | 파일 이름(확장자 없이, 80자) — 갤러리에 보인다 |
opts.reuse | 1.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
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 | 런 하나의 줄임 |
align | 1.6 단락 정렬 — left · center · right · justify · justifyCenter · justifyRight · justifyAll · towardsSpine · awayFromSpine |
leading | 1.6 행간 — 글자 크기에 곱하는 배수 (0–20, 예 1.6) |
spaceBefore · spaceAfter | 1.6 단락 앞·뒤 간격 pt (−5000–5000) |
indentLeft · indentRight · indentFirst | 1.6 왼쪽·오른쪽·첫 줄 들여쓰기 pt (−5000–5000) |
| 런 키 | 설명 |
|---|---|
text | 글 (줄바꿈은 빈칸 — 단락은 배열로 나눈다) |
inline | 1.2 글 속 개체 — §16.8. 이 런에는 text 를 쓰지 않는다 |
char | 문자 스타일 이름·id. '$base' · '$none' |
font | 서체 이름 |
size | pt (0.5–2000) |
color | 색 |
bold · italic | 불 |
tracking | 1.6 자간 — 1/1000 em (−1000–10000) |
hScale · vScale | 1.6 가로·세로 비율 % (1–1000, 기본 100) |
baselineShift | 1.6 기준선 이동 pt (+ = 위, −1000–1000) |
underline · strike · smallCaps | 1.6 밑줄 · 취소선 · 작은 대문자 (불) |
position | 1.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 글 속 그룹 안 표의 칸 |
- 객체 꼴은
apiVersion1.4 이상 (그 아래면 '… 의 글 자리 { id, cell } · { id, inline } 는 apiVersion 1.4 이상에서 씁니다' 오류). - 글 속 개체의 글을 바꾸면 주인 글을 바꾸는 Undo 한 단계가 된다. 잠금은 주인 글(표 칸이면 표)을 따른다.
- 표 칸·글 속 개체 안의 글에는 글 속 개체를 끼울 수 없다(편집기도 아직). 모든 글 자리는
doc.texts로 얻는다. path는apiVersion1.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 | 필드 | 칠·선 |
|---|---|---|
rect | x, y, w, h | fill stroke opacity |
ellipse | cx, cy, rx, ry | fill stroke opacity |
line | x1, y1, x2, y2, stroke(필수) | opacity |
path | d(SVG 경로), rule?: 'nonzero'·'evenodd' | fill stroke opacity |
group | items | opacity (안의 불투명도와 곱한다) |
use | ref(정의 이름), x?, y?, scale? 또는 m?: [a, b, c, d, e, f] 1.3 | fill 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개 이하) } |
opacity | 0–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 |
경로 하나의 d | 20만 자 |
경로 d 합 | 100만 자 |
dash 값 수 | 16 |
정의(defs) 수 | 1000 |
| use 겹침 | 4단 |
| use 를 모두 편 뒤 그림 수 | 5만 |
use 를 모두 편 뒤 경로 d 합 | 500만 자 |
정의의 경로 글은 한 번만 센다 (d 합 100만 자는 저장된 그대로의 합). 넘거나 틀리면 그 그림은 거절되고 로그에 까닭이 남는다.
17.6 다른 곳에서
| 어디 | 그림 목록은 |
|---|---|
| 화면·독자 모드 | 저장된 그림 그대로 (크기가 달라졌으면 늘려 그림) |
| 같은 그림이 벡터 패스로 — 색은 인쇄 색 그대로(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 한 번의 op | 2000 |
data·pluginData·setDocData 값 | JSON 256KB |
storage | JSON 64KB |
assets.upload | 30MB |
| Story (입력 한 번 — 글 자체에는 한도 없음) | 20만 자 · 1만 단락 · 단락당 런 1000 |
글 속 개체 크기 · insertInline 한 op | 5000pt · 1000개 |
doc.find 결과 | 기본 1000, 최대 5000 |
selection.set | 1000 |
한 edit 의 새 쪽 | 1000 (그리고 요금제 쪽 수 한도) |
addPages.count · duplicatePages.times | 200 · 500 |
| 네트워크 호스트 | 10 |
| 툴 · 패널 · 그 밖 contributes | 10 · 10 · 50 |
| 패키지 zip | 20MB · 파일 500개 · 풀어서 40MB (코드·매뉴얼·예제 모두) |
package.read | 파일 하나 20MB · 경로 200자 |
doc.texts 결과 | 20000 |
ui.pickFiles 한 번 | 5000개 · 모두 1GB |
assets.list 결과 | 5000 |
insertRows.count · autoflow.maxPages | 1000 · 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.5 | podolay.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 를 적는다.