Open22

Adobe ExtendScriptをgitで管理したい

astkastk

環境が変わるごとにスクリプトが消えているのでgitで管理してやりたい。

astkastk

リファレンスの場所をいつも忘れるので貼っておく。公式には似た名前のものもバージョン別のものも多いが、Scripting Guideが簡単なHowToで、JavaScriptに慣れておりExtendScriptに四苦八苦している人がメインで見るのはReferenceの方と覚えておけば良い。PDF本文からコピペできないのがイライラする。よく見るものは太字にしておく。

astkastk

とりあえずやっていこう。

astkastk

サブディレクトリを設置した場合にメニューで対応してくれるか試してみる。ownerがrootになっているのでsudoで作成する。

sudo mkdir test
sudo cp ImageTracing.jsx test

サブディレクトリ内のスクリプトもメニューで表示してくれる。とりあえずgitリポジトリをサブディレクトリとして設置することにする。

astkastk

my-extendscripts という名前のディレクトリを作る。

sudo mkdir my-extendscripts
sudo chown asataka:staff my-extendscripts
astkastk

結局ExtendScript Debuggerで動かした方が安心で、その場合プロジェクトはどこにあっても変わらないから、今のところアプリケーションフォルダ内にgitリポジトリを作る旨味が無いな。

astkastk

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

astkastk

インデントと循環参照のストップ処理を追加して、最終的にこれくらいになった。これである程度の用途では不都合はないはず。

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, [])
})()

スニペットとしての用途を想定して行数の少なさを指向したので正直読みづらいが動くので気にしない。

astkastk

JSON.stringify を実装するにあたり対応できていると嬉しいものを整理する。第二引数の replacer はあまり使ったことがなく実装するモチベーションが湧かないので今回は飛ばす。必要性の高いものから並べていくと

  1. Primitive型と Objejct, Array の表示
  2. 循環参照のブレーク
    • クラスインスタンスを表示するときなどに循環参照が含まれていることがあり落ちると使い勝手が悪いため
  3. インデント表示
  4. Function, Data など頻出オブジェクトの表示
  5. new Boolean などラッパーオブジェクトの valueOf 処理 (誰も使ってないと信じたい)

2番以降はprintfデバッグをしたい場合に欲しくなるので、ユースケース別に使い分けられると嬉しいかもしれない。気にせずフルスペックのコードをペタと貼って終わりにした方が楽かもしれないが、書き捨てのコードとはいえ読解困難なコードの塊が視界にちらつくのは精神衛生上よくないこともある。コードの複雑さは要求する機能に見合ったものが選べると良い。

というのを別スクラップで行った。

https://zenn.dev/asataka/scraps/5cf650e7ecc159

astkastk

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 を上書きできない
astkastk

もう少し 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 は別次元(ネイティブコードとの兼ね合いによる複雑な実装)にあるらしい。

astkastk

Node.jsでundefinedの振る舞いを確認してみる。

> undefined === undefined
true
> a = []
[]
> undefined === a[100]
true
> !!undefined
false
>

やはり、JavaScriptだとundefinedundefinedであればそれは同一のものと見做されている。

astkastk

TextFrameはコンストラクトできないらしい。でもinstanceofの対象には取れたよな。

const tf = new TextFrame()
// Error Code# 22: TextFrame does not have a constructor
astkastk

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の環境では一部のメソッドはプロトタイプチェーンに乗っからない、と結論付けることにする。

正直この理解でいいのか、そこまでの納得感はない。

astkastk

最後にもう一度確認してみよう。

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.prototypeundefined らしいです。もう諦める。

astkastk

複数の、単体で動作させるスクリプトをTypeScriptでコンパイルしたい。こんな感じで。

.
├── dist
│   ├── export-text-contents.js
│   └── rename-artboard-names.js
├── src
│   ├── export-text-contents.ts
│   └── rename-artboard-names.ts
└── tsconfig.json