Adobe ExtendScriptをgitで管理したい
環境が変わるごとにスクリプトが消えているのでgitで管理してやりたい。
リファレンスの場所をいつも忘れるので貼っておく。公式には似た名前のものもバージョン別のものも多いが、Scripting Guideが簡単なHowToで、JavaScriptに慣れておりExtendScriptに四苦八苦している人がメインで見るのはReferenceの方と覚えておけば良い。PDF本文からコピペできないのがイライラする。よく見るものは太字にしておく。
- 公式 (adobe.com)
- Adobe Illustrator CC 2021 Scripting Guide(2021, PDF) - ioconsolerykerprodcnd.azureeedge.net
- Adobe Illustrator CC 2017 Scripting Guide(2017, PDF)
- Illustrator CC Scripting Guide(2013, PDF)
- Adobe Illustrator CC Scripting Reference: JavaScript(2013, PDF)
- JavaScript Tools Guide(CS5, 2010, PDF)
- Adobe Illustrator CS2 JavaScript Reference(2005, PDF)
- 有志のオンラインリファレンス
- その他参考
とりあえずやっていこう。
プリセットはアプリケーション内の Presets/<LANG>/Scripts
にある。
asataka@tailmoon ~ % ls /Applications/Adobe\ Illustrator\ 2022/Presets.localized/en_US/Scripts
ImageTracing.jsx SaveDocsAsPDF.jsx SaveDocsAsSVG.jsx
asataka@tailmoon ~ %
よくみた名前が並んでいる。
参考: https://designbundles.net/community/t/installing-scripts-in-illustrator/6418
サブディレクトリを設置した場合にメニューで対応してくれるか試してみる。ownerがrootになっているのでsudoで作成する。
sudo mkdir test
sudo cp ImageTracing.jsx test
サブディレクトリ内のスクリプトもメニューで表示してくれる。とりあえずgitリポジトリをサブディレクトリとして設置することにする。
my-extendscripts
という名前のディレクトリを作る。
sudo mkdir my-extendscripts
sudo chown asataka:staff my-extendscripts
githubに上げた。しばらくこれで運用してみる。
結局ExtendScript Debuggerで動かした方が安心で、その場合プロジェクトはどこにあっても変わらないから、今のところアプリケーションフォルダ内にgitリポジトリを作る旨味が無いな。
TypeScript化した。VSCodeのTaskとDebuggingの勉強になった。task.jsonにビルドタスクを設定して、launch.jsonでextendscript-debugのpreLaunchTaskで呼んでからデバッグする。だいぶ快適になった。
参考: https://code.visualstudio.com/docs/typescript/typescript-debugging
JSON出力したいけれどライブラリを読み込むのも面倒なので、コピペしやすいスニペットを自分で書いてみよう。とりあえずこんな感じになった。
type JsonPrimitive = Record<string, any> | Array<JsonPrimitive> | string | number | boolean | undefined
const stringifyAsJson = (v: JsonPrimitive): string => {
if (v instanceof Array) {
const sv: string[] = Array(v.length)
for (let i = 0; i < v.length; i++) sv[i] = stringifyAsJson(v[i])
return '[' + sv.join(', ') + ']'
}
if (v instanceof Object) {
const sv: string[] = []
for (let k in v) sv.push('"' + k + '": ' + stringifyAsJson((v as Record<string, JsonPrimitive>)[k]))
return '{' + sv.join(', ') + '}'
}
return typeof v === 'string' ? '"' + v + '"' : '' + v
}
gistで管理しよう: https://gist.github.com/asa-taka/e757b2145fba61de97f30b447e99ead2
インデントと循環参照のストップ処理を追加して、最終的にこれくらいになった。これである程度の用途では不都合はないはず。
type JsonValue = Record<string, any> | Array<JsonValue> | string | number | boolean | undefined | null
const stringifyAsJson = (() => {
const sp = (indent: number, lv: number) => (indent ? '\n' : '') + Array(lv * indent + 1).join(' ')
const printObj = (lp: string, tokens: string[], rp: string, indent: number, lv: number) => {
if (!tokens.length) return lp + rp
return lp + sp(indent, lv + 1) + tokens.join(',' + sp(indent, lv + 1)) + sp(indent, lv) + rp
}
const stringify = (v: JsonValue, indent: number, lv: number, path: any[]): string => {
if (!(v instanceof Object)) return typeof v === 'string' ? '"' + v + '"' : '' + v
for (let o of path) if (v === o) return '"[CIRCULAR]"'
if (v instanceof Function) return '"[Function: ' + v.name + ']"'
const sv: string[] = []
if (v instanceof Array) {
for (let e of v) sv.push(stringify(e, indent, lv + 1, path.concat([v])))
return printObj('[', sv, ']', indent, lv)
}
for (let k in v) sv.push('"' + k + '": ' + stringify(v[k], indent, lv + 1, path.concat([v])))
return printObj('{', sv, '}', indent, lv)
}
return (v: JsonValue, indent?: number) => stringify(v, indent || 0, 0, [])
})()
スニペットとしての用途を想定して行数の少なさを指向したので正直読みづらいが動くので気にしない。
JSON.stringify を実装するにあたり対応できていると嬉しいものを整理する。第二引数の replacer
はあまり使ったことがなく実装するモチベーションが湧かないので今回は飛ばす。必要性の高いものから並べていくと
- Primitive型と
Objejct
,Array
の表示- https://developer.mozilla.org/en-US/docs/Glossary/Primitive
- ExtendScriptはES3なので
bigint
とsymbol
は無い - 気をつけて作ったデータを出力する分にはこれでも問題ない場合も多い
- 循環参照のブレーク
- クラスインスタンスを表示するときなどに循環参照が含まれていることがあり落ちると使い勝手が悪いため
- インデント表示
-
Function
,Data
など頻出オブジェクトの表示 -
new Boolean
などラッパーオブジェクトのvalueOf
処理 (誰も使ってないと信じたい)
2番以降はprintfデバッグをしたい場合に欲しくなるので、ユースケース別に使い分けられると嬉しいかもしれない。気にせずフルスペックのコードをペタと貼って終わりにした方が楽かもしれないが、書き捨てのコードとはいえ読解困難なコードの塊が視界にちらつくのは精神衛生上よくないこともある。コードの複雑さは要求する機能に見合ったものが選べると良い。
というのを別スクラップで行った。
toString
とtoJSON
の使い分けが悩ましい。
ExtendScriptでprototypeの拡張はしてもいいんだろうか、と思いやってみたが上手くいかなかった。Function
に対してtoString
すると実装のスクリプトが帰ってくるので、それを利用して確認した。
interface TextFrame { toString(): string }
TextFrame.prototype.toString = function () { return this.contents }
app.activeDocument.selectObjectsOnActiveArtboard()
for (let item of app.activeDocument.selection) {
if (item instanceof TextFrame) {
$.writeln(item.toString())
// => [TextFrame ]
$.writeln(item.toString.toString())
// => function toString() { [native code] }
$.writeln(item.toString === TextFrame.prototype.toString)
// => false
}
}
なぜか TextFrame
のインスタンスで呼んだ toString
が帰ってこず、ネイティブコードのものが返ってくる。仮説を立てて考えてみる。
-
item
のプロトタイプチェーンのTextFrame
よりもsubな箇所に何かがいて、そのtoString
が呼ばれている - 単純に
TextFrame.prototype.toString
を上書きできない
Adobe Illustrator CC Scripting Reference: JavaScript(2013, PDF) - adobe.com を見ると、TextFrame
という項目はなく、TextFrameItem
という項目が存在するが、実際の環境上に存在するのは TextFrame
なので Types-for-Adobe が正しい。
$.write(TextFrame)
// => function TextFrame() { [native code] }
declare class TextFrameItem {}
$.write(TextFrameItem)
// => Error Code# 2: TextFrameItem is undefined
もう少し prototype
まわりを執拗に追ってみる。
interface TextFrame { toString(): string }
TextFrame.prototype.toString = function () { return this.contents }
$.writeln(TextFrame)
// => function TextFrame() { [native code] }
$.writeln(TextFrame.prototype)
// => undefined
// 'undefined' という文字を返すように、という実装になっているんだろう…
$.writeln(TextFrame.prototype === undefined)
// => false
// なのでこれは我々が普段見ている undefined とは別物
$.writeln(!!TextFrame.prototype)
// => true
// undefinedは普通booleanにするとfalseになる
$.writeln(TextFrame.prototype.toString)
// => function () { return this.contents; }
どうやら TextFrame.prototype
は別次元(ネイティブコードとの兼ね合いによる複雑な実装)にあるらしい。
Node.jsでundefinedの振る舞いを確認してみる。
> undefined === undefined
true
> a = []
[]
> undefined === a[100]
true
> !!undefined
false
>
やはり、JavaScriptだとundefined
はundefined
であればそれは同一のものと見做されている。
TextFrame
はコンストラクトできないらしい。でもinstanceof
の対象には取れたよな。
const tf = new TextFrame()
// Error Code# 22: TextFrame does not have a constructor
new
できないのが気になったので Function
かどうかを調べてみる。
$.writeln(TextFrame instanceof Function)
// => true
declare function TextFrame(): any
$.writeln(TextFrame())
// => undefined
Function
ではあるし、callableでもある。JavaScriptだとFunction
は基本的にnew
できるものだと思うので、TextFrame
はネイティブコードとの境界線上にあるよくわからないものということなのかな。toString
の振る舞いもそれで納得することにする。大人しく
stringifyTextFrame(textFrame)
のようなメソッドを定義して済ませるとしよう。
ちなみに TextFrame.prototype.toJSON
を定義して自前の jsonStringify
に与えたら、そちらはちゃんと呼び出されたので、TextFrame.prototype.toString
など、ExtendScriptの環境では一部のメソッドはプロトタイプチェーンに乗っからない、と結論付けることにする。
正直この理解でいいのか、そこまでの納得感はない。
最後にもう一度確認してみよう。
const printKeys = (obj: {}) => { for (let key in obj) $.writeln(key) }
interface TextFrame { toString(): string }
TextFrame.prototype.toString = function () { return this.contents }
printKeys(TextFrame.prototype)
// => toString
$.writeln(TextFrame.prototype)
// => undefined
toString
が定義されているはずの TextFrame.prototype
は undefined
らしいです。もう諦める。
https://twitter.com/peprintenpa/status/1478986573014499328?s=21 でご指摘いただいたように、prototype.toString
を定義してしまっているので、return this.contents
が評価されて undefined
が返っていたというオチでした。
複数の、単体で動作させるスクリプトをTypeScriptでコンパイルしたい。こんな感じで。
.
├── dist
│ ├── export-text-contents.js
│ └── rename-artboard-names.js
├── src
│ ├── export-text-contents.ts
│ └── rename-artboard-names.ts
└── tsconfig.json