인디자인 [스크립트] 창에는 세 개의 폴더가 있다. [응용 프로그램], [커뮤니티], [사용자]가 그것이다. [응용 프로그램]은 딱봐도 기본적으로 제공되는 느낌이 들고, [사용자]는 내가 쓸 스크립트를 집어넣을 공간이라는 생각이 든다. 근데 [커뮤니티] 폴더는 당최 무엇인지 처음 봐서는 알 수 없다. [응용 프로그램]에서 [Samples]로 제공되는 스크립트들에 비해 뭔가 기능이 고도화(?)되어 있고, 좀 더 지엽적으로 특화된 느낌이 든다. 그래서 찾아봤더니 어도비 인디자인 커뮤니티에서 잘 만들어진 스크립트를 프로그램 내부적으로 제공하는 거였다. 인디자인 15.0.2 버전(InDesign 2020에 해당)부터 [스크립트] 창에 [커뮤니티] 폴더가 업데이트 되었다고 한다.
업데이트를 항상 최신으로 하는 게 아니라서, 어쩌다보니 내가 고수하고 있는 InDesign 2023(18.5.2) 한국어판 기준으로 [커뮤니티] 폴더에는 다섯 개의 스크립트가 있다. 어도비 홈페이지 설명으로는 ‘커뮤니티가 기여한 스크립트(Community-contributed scripts)’로 무려 열 여섯 개의 항목이 있다고 하는데, 어째 어플리케이션 안에 있는 건 다섯 개 뿐이다. 다섯 가지 스크립트 모두 기능이 좋고 정말 잘 만든 스크립트라서 능력이 되는 한에서 소개해보려고 한다.
우선 여기서는 ‘InsertTypographerQuote’에 대해서 이야기 해보겠다. 이 스크립트는 슈테판 라케트(Stefan Rakete)라는 독일인 스크립터(홈페이지 링크)가 만들었는데, 선택한 글귀에 인용 부호를 앞뒤로 삽입해준다. [커뮤니티] 폴더에 있는 다른 스크립트들은 그룹 없이 단독으로 파일이 있는데, 이것만 8개의 스크립트가 그룹으로 묶여있다. 독일, 영국, 프랑스, 스위스에서 사용하는 작은따옴표와 큰따옴표를 미리 제공하는 거였다. 스크립트 작성자가 독일 사람인 까닭에 사전 제공하는 파일이 서구중심적인 건 어쩔 수 없나보다. 사전 제공하는 게 그렇게 제한적일 뿐이지, 이 스크립트의 장점은 유니코드만 알면 인용구 앞뒤로 넣을 수 있는 걸 맘대로 할 수 있다는 점이다. 앞에 여는 기호를 넣으면, 반드시 닫는 기호가 필요한 경우는 적지 않다. 생각해보면 비단 따옴표 뿐만이 아니다.
게다가 따옴표는 한국어 키보드 자판에서는 기본적으로 제공하고 있기에 [글리프] 창을 굳이 열지 않아도 입력할 수 있다. 문제는 낫쇄(『, 』, 「, 」)나 화살괄호(〈, 〉, 《, 》) 같은 것들이다. 키보드 자판에 딸려있지 않아서 입력하려면 [글리프] 창을 열거나 여타의 특수한(?) 입력 과정을 거쳐야 한다. 그렇게 유니코드를 수정한 스크립트에 단축키를 달면 정말 드라마틱하게 수고가 덜어진다. 앞서 언급한 낫쇄(겹낫쇄와 홑낫쇄)와 화살괄호(겹화살괄호와 홑화살괄호)에 해당하는 유니코드로 네 개의 파일을 만들었다. 스크립트 코드가 깔끔하게 되어 있어 실행취소(맥 기준 cmd+z)만 누르면 입력이 한 번에 되돌려지는 것 또한 이 스크립트의 장점이다.
- [CP]스크립트 설치하기 ← 설치하는 법을 모르겠다면 참고!
코드 해설
* '겹낫쇄.jsx'를 대상으로 하지만, 폴더 내 다른 스크립트와 인용 부호가 달라지는 것 외에는 차이가 없다. 해설을 하면서 주석은 모두 제외했다. 하지만 스크립트 원본에는 모두 한국어로 번역해 그대로 넣어두었다.
#targetengine "setquotes"
Array.prototype.exists = function (x) {
for (var i = 0; i < this.length; i++) {
if (this[i] == x) return true;
}
return false;
}
function SetQuotes() {
}
SetQuotes.SELECTION_ALLOWED = ["Text", "TextStyleRange", "Word", "Paragraph", "TextColumn","Line","Character"];
SetQuotes.TYPOGRAPHERS_QUOTES_VALUE = undefined;
SetQuotes.QUOTES_START = "\u300E";
SetQuotes.QUOTES_END = "\u300F";
#targetengine은 익스텐드스크립트에만 있는 특수한 기능이다. 스크립트 작성자들의 말에 따르면 인디자인(부분적으로 인카피, 일러스트레이터)에서만 가용한 기능인 듯싶다. #targetengine 이후에 선언된 변수와 함수를 기억해줘서, 해당 변수/함수를 다시 선언할 필요가 없게 해준다. #targetengine 다음에는 세 가지 부류가 올 수 있는데 첫 번째는 완전히 공용인 ‘main’, 두 번째는 준공용인(public private) ‘session’ 등의 경우, 세 번째는 완전히 사적인(private private) 임의의 엔진 이름을 사용하는 경우다. 첫 번째 ‘main’은 기본 엔진으로, 그걸 덧붙이면 #targetengine은 아무것도 기억하지 못한다. 두 번째 ‘session’ 이름을 달면 어플리케이션이 실행되어 있는 한 변수와 함수를 기억한다. 세 번째, 임의의 엔진 이름을 쓰면 변수와 함수가 그 이름에 한해서 기억되기 때문에 좀 더 제한적으로 활용할 수 있다. 첫 줄 #targetengine “setquotes”는 세 번째인 임의의 엔진 이름을 사용한 경우다.
배열을 순회하는 것으로 스크립트가 시작되는데(Array.prototype.exists = function (x) {), 반복을 위해 가정한 i 값이 원본 배열의 수와 같을 경우, 참을 반환하고, 그렇지 않으면 거짓을 반환하라는 구문이다(for (var i = 0; i < this.length; i++) { if (this[i] == x) return true; } return false;}). 사실 자바스크립트 배열과 그 프로토타입에 대한 이해가 아직 부족해서, 이 부분에 대한 명확한 존재 이유를 알지 못한다.😢 추후에 이유를 알게 되면 덧붙이는 것으로 갈음하겠다.
곧이어 ‘SetQuotes’라는 이름으로 함수가 만들어진다(function SetQuotes() {) 함수는 일단 비워둔 채 식을 끝내는데, SetqQuotes 역시 객체이기에, 속성들이 덧붙여진다. 우선 ‘텍스트’ ‘텍스트 스타일 범위’ ‘단어’ ‘단락’ 텍스트 단’ ‘줄’ ‘문자’로 허용할 수 있는 선택지를 배열로 뒀고(SetQuotes.SELECTION_ALLOWED = ["Text", "TextStyleRange", "Word", "Paragraph", "TextColumn","Line","Character”];), ‘굽은 따옴표 사용’ 여부를 결정되지 않은 상태로 해놨다(SetQuotes.TYPOGRAPHERS_QUOTES_VALUE = undefined;). 그리고 이 부분이 가장 중요한데, 인용 시작 부호와 인용 끝 부호를 유니코드로 설정해두는 구문이다(SetQuotes.QUOTES_START = "\u300E"; SetQuotes.QUOTES_END = "\u300F"; ). 이 스크립트는 겹낫쇄 입력을 위해 수정한 것이므로 시작과 끝 부호가 각각 유니코드 U+300E, U+300F에 해당된다.
SetQuotes.prototype.init = function() {
var success = false;
if (app.documents.length > 0) {
if (app.selection.length > 0) {
var selOK = SetQuotes.checkSelection();
if (selOK) {
var curSel = app.selection[0];
SetQuotes.TYPOGRAPHERS_QUOTES_VALUE = app.activeDocument.textPreferences.typographersQuotes;
app.activeDocument.textPreferences.typographersQuotes = false;
success = true;
} else {
alert("선택이 유효하지 않습니다, 텍스트를 선택한 후 다시 시도하십시오." );
}
} else {
alert("텍스트가 선택되지 않았습니다, 텍스트를 선택한 후 다시 시도하십시오." );
}
} else {
alert("스크립트를 실행하려면 문서가 한 개 이상 열려 있어야 합니다." );
}
return success;
}
다음은 SetQuotes의 프로토타입 객체와 함께, 이것이 시작될 때 기능을 담은 함수가 만들어진다(SetQuotes.prototype.init = function() {). 함수 안의 범위에서 success라는 이름의 변수가 ‘거짓’으로 선언되는데, 여기서부터 밖에서 안으로 짝을 이루는 조건문이 이어진다. 문서의 개수가 한 개 이상이지 않으면(if (app.documents.length > 0) {), 첫 번째 경고가 발생한다(alert("스크립트를 실행하려면 문서가 한 개 이상 열려 있어야 합니다." ); 경고문은 영어였던 것을 번역했다). 선택된 게 하나도 없을 경우(if (app.selection.length > 0) {), 두 번째 경고가 발생한다(alert("텍스트가 선택되지 않았습니다, 텍스트를 선택한 후 다시 시도하십시오." );). 문서가 한 개 이상이고 선택된 게 있다면, 그 선택된 걸 체크하기 위해 우선 selOK라는 변수가 선언된다(var selOK = SetQuotes.checkSelection();). selOK는 객체 SetQuotes의 checkSelection이라는 속성으로 정의가 되었고, 그 기능은 추후에 정의된다. 어쨌든 checkSelection은 비어 있는 상태이지만, 그것에 부합한다고 가정되면 curSel이라는 변수를 선언한다(var curSel = app.selection[0];). curSel은 현재 선택된 것이다. 아울러 위에서 결정되지 않은 상태로 뒀던, ‘굽은 따옴표 사용’ 여부에 대한 속성을 현재 열려 있는 문서의 환경설정 [문자] 탭에서 ‘굽은 따옴표 사용’에 관한 것으로 직접적으로 정의한다(SetQuotes.TYPOGRAPHERS_QUOTES_VALUE = app.activeDocument.textPreferences.typographersQuotes;) 그리고 그것이 체크가 되어 있지 않으면(app.activeDocument.textPreferences.typographersQuotes = false;), 체크하는 것으로 변경한다(success = true;). 애초에 selOK에서 어긋날 경우, 세 번째 경고가 발생한다(alert("선택이 유효하지 않습니다, 텍스트를 선택한 후 다시 시도하십시오." );). 조건문을 통과하면 success의 값을 반환한다(return success;).
SetQuotes.prototype.reset= function() {
app.activeDocument.textPreferences.typographersQuotes = SetQuotes.TYPOGRAPHERS_QUOTES_VALUE;
}
SetQuotes 프로토타입 객체와 함수 조합이 계속 이어진다(SetQuotes.prototype.reset= function() {). 여기서는 앞서 설정한 현재 환경설정 [문자] 탭의 ‘굽은 따옴표 사용’ 여부가 체크된 상태로 SetQuotes의 설정으로 만드는 기능을 수행하는 듯싶다.
SetQuotes.checkSelection = function() {
var selOK = true;
var curSelection = app.selection;
if (curSelection.length > 1) return false;
var curItem = curSelection[0];
var curItemType = curItem.constructor.name;
var allowedItems = SetQuotes.SELECTION_ALLOWED;
var isAllowed = allowedItems.exists(curItemType);
if (!isAllowed) {
selOK = false;
}
return selOK;
}
여기서는 위에서 변수로 선언된 SetQuotes.checkSelection의 기능을 구체적으로 규정한다. 추가 변수가 다시 선언되는데, curSelection은 어떤 선택에 대한 것으로(var curSelection = app.selection;). 하나가 초과되어 선택되었을 경우는 허용되지 않는다(if (curSelection.length > 1) return false;) curItem 변수는 현재 선택된 것인데(var curItem = curSelection[0];), 그 생성자 이름(var curItemType = curItem.constructor.name;)이 스크립트 서두에서 허용되는 것으로 규정된 선택지에 한해야 한다(var allowedItems = SetQuotes.SELECTION_ALLOWED; var isAllowed = allowedItems.exists(curItemType);)는 내용이다. 그로부터 어긋나면, checkSelection 자체로서 기각된다(if (!isAllowed) { selOK = false;}).
SetQuotes.prototype.setQuotes = function () {
var curSel = app.selection[0];
var countInsertionPoints = curSel.insertionPoints.length;
var firstInsertionPoint = curSel.insertionPoints.firstItem();
var lastInsertionPoint = curSel.insertionPoints.lastItem();
var lastSelectedCharacterIsLineBreak = SetQuotes.getLastCharacterIsLineBreak(curSel);
if (lastSelectedCharacterIsLineBreak) {
lastInsertionPoint = curSel.insertionPoints.item(countInsertionPoints-2);
}
if (firstInsertionPoint.isValid && lastInsertionPoint.isValid) {
lastInsertionPoint.contents = SetQuotes.QUOTES_START;
curSel = app.selection[0];
if (lastSelectedCharacterIsLineBreak) {
lastInsertionPoint = curSel.insertionPoints.item(countInsertionPoints-2);
} else {
lastInsertionPoint = curSel.insertionPoints.lastItem();
}
lastInsertionPoint.contents = SetQuotes.QUOTES_END;
var quoteStartCharacter = curSel.characters.lastItem();
var firstCharacterOfSelection = curSel.characters.firstItem();
quoteStartCharacter.move(LocationOptions.AT_BEGINNING, firstCharacterOfSelection);
} else {
alert("인용 부호를 입력하지 못했습니다, 다시 시도해주십시오.");
}
}
이제 SetQuotes의 중심 기능에 대한 함수다. 앞서 선언된 변수가 재선언된다. curSel은 현재 선택된 것(var curSel = app.selection[0];), countInsertionPoins는 현재 선택된 것의 삽입점 개수다(var countInsertionPoints = curSel.insertionPoints.length;). firstInsertionPoint와 lastInsertionPoint는 각각 현재 선택된 것의 삽입점 중 첫 번째(curSel.insertionPoints.firstItem();)와 마지막(curSel.insertionPoints.lastItem();)을 상정한다. 그리고 lastSelectedCharacterIsLineBreak은 그 이름처럼 마지막으로 선택된 게 줄바꿈 문자인지를 보는 것으로 이하에서 그 내용이 정의된다.
우선 줄바꿈 문자를 무시하기 위해 lastSelectedCharacterIsLineBreak가 참일 경우, lastInsertionPoint가 현재 선택된 것의 삽입점 개수보다 둘 적은(lastInsertionPoint = curSel.insertionPoints.item(countInsertionPoints-2);), 그러니까 마지막 삽입점이 끝에서 두 번째 문자라는 규정을 한다. 줄바꿈 문자가 끝에서 첫 번째 문자일 것이므로, 그 전 문자로 규정되는 것이다.
이제 첫 번째 삽입점과 마지막 삽입점이 모두 유효하다고 했을 때(if (firstInsertionPoint.isValid && lastInsertionPoint.isValid) {), 마지막 삽입점 내용으로 스크립트 서두에서 쓴 바 있는 시작하는 인용부호로 설정한다는 것으로 이어진다(여기서 왜 끝나는 인용부호가 아닌 시작하는 인용부호인지 의아할 수 있지만, 이어지는 스크립트를 보면 우선 끝에 삽입 후 자리를 옮기는 것임을 알 수 있다). 그리고 선택된 것이 달라진 것(시작하는 인용부호가 추가된 후의 변화)을 재차 확인하기 위해 일전에 선언된 curSel이 다시금 불려온다. 그리고 마지막 문자가 줄바꿈 문자인지를 확인하고 그런 경우, 삽입점 끝에서 두 번째 문자로 규정하는 구문이 재차 배치된다(if (lastSelectedCharacterIsLineBreak) { lastInsertionPoint = curSel.insertionPoints.item(countInsertionPoints-2);). 만약 마지막 문자가 줄바꿈 문자가 아닌 경우에는 해당 삽입점의 마지막 항목이 마지막 삽입점이 된다(} else { lastInsertionPoint = curSel.insertionPoints.lastItem();.). 그리고 이번에는 마지막 삽입점 내용으로 끝나는 인용부호가 규정된다(lastInsertionPoint.contents = SetQuotes.QUOTES_END;). 그러면 인용부호는 현재 끝나는 인용부호와 시작하는 인용부호가 선택된 것의 끝쪽에 연달아 있는 상태인데, quoteStartCharacter 변수를 선언함으로써 그걸 옮길 준비를 한다. quoteStartCharacter는 현재 선택된 마지막 문자(var quoteStartCharacter = curSel.characters.lastItem();). 그리고 선택된 것의 첫 번째 문자를 특정하고(var firstCharacterOfSelection = curSel.characters.firstItem();), move 메소드로 그곳으로 문자를 옮기는 것이다(quoteStartCharacter.move(LocationOptions.AT_BEGINNING, firstCharacterOfSelection);). 이 과정이 제대로 되지 않으면, 경고가 발생한다(alert("인용 부호를 입력하지 못했습니다, 다시 시도해주십시오.”);).
SetQuotes.getLastCharacterIsLineBreak = function (curSel) {
lastCharacterIsLineBreak = false;
if (curSel.isValid) {
var lastCharacter = curSel.characters.lastItem();
if (lastCharacter.contents == "\r") {
lastCharacterIsLineBreak = true;
}
}
return lastCharacterIsLineBreak;
}
여기서는 lastCharacterIsLineBreak인지를 확인하는 기능을 다루는데, 일단 lastCharacterIsLineBreak는 ‘거짓’이라고 하면(lastCharacterIsLineBreak = false;), 현재 선택된 것이 유효할 때(if (curSel.isValid) {) lastChracter는 현재 선택된 마지막 문자(var lastCharacter = curSel.characters.lastItem();)라는 조건식을 일단 전개하고, 다시 그 속의 조건문으로 마지막 문자의 내용이 줄바꿈 문자인 경우(if (lastCharacter.contents == "\r") {), lastCharacterIsLineBreak는 참(lastCharacterIsLineBreak = true;)이 된다. 그리고 최종 값은 마지막 문자가 줄바꿈 문자인지 여부에 따라 참, 거짓으로 반환되는 것이다.
function runSetQuotes() {
var mySetQuotes = new SetQuotes();
var success = mySetQuotes.init();
if (success) {
mySetQuotes.setQuotes();
mySetQuotes.reset();
}
}
try {
app.doScript("runSetQuotes()", ScriptLanguage.JAVASCRIPT , [], UndoModes.ENTIRE_SCRIPT, "InsertTypographerQuote.jsx");
} catch (e) {
alert("스크립트를 실행하는 데 오류가 발생했습니다, 다시 시도해주십시오.");
}
이제 SetQuotes의 실행과 관련된 함수다. mySetQuotes를 새로운 SetQuotes라고 할 때(var mySetQuotes = new SetQuotes();), 여기서의 success는 새로운 SetQuotes 시작 단계에 돌입하는 것이다(var success = mySetQuotes.init();). 그렇게 새로운 SetQuotes가 시작 단계에 돌입하면(if (success) {), 마찬가지로 SetQuotes의 중심 기능도 실행하고(mySetQuotes.setQuotes();), 굽은 따옴표로 돌리는 기능도 실행한다(mySetQuotes.reset();). 이부분은 반복 가능성에 대한 규정인 듯싶다.
그리고 사용자 편의를 위해 doScript 기능으로 최종 정리를 한다(app.doScript("runSetQuotes()", ScriptLanguage.JAVASCRIPT , [], UndoModes.ENTIRE_SCRIPT, "InsertTypographerQuote.jsx”);). 만약 그렇게 실행이 안 될 경우에는 경고가 발생하는 예외 규정(} catch (e) { alert("스크립트를 실행하는 데 오류가 발생했습니다, 다시 시도해주십시오.”);)을 함으로써 스크립트는 마무리된다.