OS XのMS Wordでの法律文書起案用JXA(JavaScript for Automation)スクリプト

はじめに

 今まで、法律文書のナンバリングに応じてインデントを自動設定するために、LibreOffice Basicでマクロを作り(法律文書起案用mi文法定義スクリプトとOpenOffice系マクロ)、VBA(MS Word)でマクロを作り(法律文書起案用MS Word用VBAマクロ)、その都度ソースコードをこのブログに置いてきました。
 そして今回、OS X(Macの現行OS)に備わったJavaScriptによる自動化機能、JavaScript for Automation(JXA)で、MS Wordのドキュメントに対して同じことをするスクリプトを作ってみましたので、またソースコードを置いておきます。JXAはOS X10.10 Yosemiteから使えるようです。OS X付属のScript Editorというアプリに貼り付ければ使えます(デフォルトの言語はJavaScriptではなくAppleScriptになっていますので注意)。
 MS Office 2016 for MacはまだちゃんとVBAが使えないようで(アップデートで徐々に改善されています)、私の作ったVBAも動きません。それで私も結局ずっと2011版を使っていたのですが、このJXAスクリプトなら2016版に対しても使えるわけです。まあ、2016版、まだ大きいバグとかあって使いにくいですけどね……。
 ちなみにVBAの方もLibreOffice Basicの方もたまに手を入れていて、その都度ブログ記事も更新して新しいソースコードを載せています。この記事もそうなると思います。
 Pagesでも同じようなことができたらそっち使いたいところなんですが、できないようですね。MS OfficeはVBAでできることがだいたいOS Xの自動化機能でもできるようです。JavaScriptなら正規表現も使えるし良いですね。

仕様

 MS Wordの選択範囲を処理します。
 例えば、段落頭の「第1 」「3 」「(2)ア 」などに応じてインデントを自動設定します。それに続く段落が「  」で始まれば、インデントを引き継ぎます(ぶら下がりは1字分だけになります)。フォントサイズは12ポイント。
 「① 」「② 」と列挙される場合は、その箇所が周囲より下がります。それに続く段落が「  」で始まれば、列挙前のインデントに戻ります。
 スペースが2つで始まって2つで終わる段落は中央揃えになります。スペースが3つで始まって3つで終わる段落は中央揃えの14ポイントになります。4つなら16ポイント。スペースは全角なら全部全角、半角なら全部半角と揃っていなければ効きません。
 スペースが2つで始まって1つで終わる段落は右揃え。
 なんかもう仕様を書くのもめんどくさい程度に大きくなってきました……(LibreOffice Basicの方もVBAの方もだいたい同じ仕様にアップデートしてあります。どれか1つに専念した方がいいとは思いますが)。

更新履歴

・2015年10月29日10時30分頃
 テンプレートから新規ドキュメントを作成して、そのドキュメントにクリップボードの中身をペーストして、インデントを自動設定する、という一連の流れを1つのスクリプトを実行するだけでできるようにしました。
 テンプレートごとに実行スクリプトを別に置くために、インデント自動設定部分を関数にしてライブラリに定義を置いたので、単純にこの記事からScript Editorにコードをコピペすれば動くというものではなくなってしまいました。
 そのライブラリを使って選択範囲にインデントを自動設定するだけのスクリプトも置いておきます。
・2015年10月29日11時頃
 実効速度が速くならないかなと思って、ライブラリの、正規表現とのマッチを試す文字列を短く切り取ってみました。少し速くなったような、変わっていないような。
・2015年10月29日13時40分頃
 段落頭の「第n 」「第n条」がないドキュメントは「1 」がトップレヴェル(左インデントが0)になるようにしました。
・2015年10月29日16時00分頃
 段落頭が全角スペース2つになる場合の処理が、3つでも4つでも同じように効いてしまっていた不具合を修正。
・2015年10月30日3時40分頃
 「段落頭の「第n 」「第n条」がないドキュメントは「1 」がトップレヴェルになるように」の処理のバグフィックス。
・2015年10月30日21時頃
 「段落頭の「第n 」「第n条」がないドキュメントは「1 」がトップレヴェルになるように」の処理のさらなるバグフィックス。
・2015年10月30日22時頃
 テンプレートから新規……のスクリプトで少し無駄なことをしていた(パスの表現をわざわざわかりにくい形式にしていた)ので修正。
・2015年10月31日18時頃
 中央揃えの段落のフォントサイズをもう一段階増やしました。両側にスペース5個で18ポイント。
 「① 」「② 」……の箇所は、必ずしも①から始めなくてもよいようにしました。
・2015年11月1日3時頃
 クリップボードの中身をプレーンテキストにしてからペーストするようにしました。
・2015年11月1日13時頃
 クリップボードの中身をプレーンテキストにしてからペーストする処理が間違っていたので直しました。

ライブラリ

 ”~/Library/Script Libraries/”に置きます。Script Librariesがなかったら作ります。スクリプトファイル自体には私は”kianLibrary”という名前をつけて置いています。
 引数として渡されたオブジェクトに対してインデント設定をする”kianIndent”という関数が定義されています。

function kianIndent (selection) {

var Word = new Application('Microsoft Word');
var paras = selection.paragraphs;

//トップレヴェルが「第n 」「第n条」か「n 」かの判定
var docStr = Word.activeDocument.textObject.content();
if ( /^第[0-9]+ /.test( docStr ) || /^第[0-9]+条/.test( docStr ) || /\r第[0-9]+ /.test( docStr ) || /\r第[0-9]+条/.test( docStr ) ) {	
	var top = 0;
} else {
	var top = -12;
}

for (var i = 0; i < paras.length; i++) {

	var paraStr = paras[i].textObject.content();
	var paraStrL = paraStr.substring( 0 , 7 );
	var paraStrR = paraStr.substring( paraStr.length -7 );
		
	//段落頭が全角スペース2つで始まる場合は前段落と同じにする
	if ( /^  [^ ]/.test( paraStrL ) && /[^ ]\r/.test( paraStrR ) && !/^[①-⑨] /.test( paras[i].previousParagraph().textObject.content() ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = paras[i].previousParagraph().paragraphLeftIndent();
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	//ナンバリングの処理
	} else if ( /^第[0-9]+ /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = 24;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[0-9]+ /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 24;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[0-9]+\([0-9]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = top + 36;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^\([0-9]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 36;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^\([0-9]+\)[ア-ン] /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = top + 48;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[ア-ン]+ /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 48;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[ア-ン]+\([ア-ン]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = top + 60;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^\([ア-ン]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 60;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^\([ア-ン]+\)[a-z]+ /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = top + 72;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[a-z]+ /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 72;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[a-z]+\([a-z]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -24
		paras[i].paragraphLeftIndent = top + 84;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^\([a-z]+\) /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = top + 84;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	//「① 」から「⑨ 」が列挙される場合は前段落より1字下げる
	} else if ( /^[①-⑨] /.test( paraStrL ) && !/^[①-⑨] /.test( paras[i].previousParagraph().textObject.content() ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = paras[i].previousParagraph().paragraphLeftIndent() +12;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( /^[①-⑨] /.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = paras[i].previousParagraph().paragraphLeftIndent();
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	//「① 」から「⑨ 」が列挙された後、「  」で始まる段落に続く場合は、列挙前のインデントに戻す
	} else if ( /^  [^ ]/.test( paraStrL ) && /[^ ]\r/.test( paraStrR ) && /^[①-⑨] /.test( paras[i].previousParagraph().textObject.content() ) ) {
		if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
			paras[i].wordWrap = true;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		} else if ( !/^[①-⑨] /.test( paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().textObject.content() ) ) {
			paras[i].firstLineIndent = -12
			paras[i].paragraphLeftIndent = paras[i].previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().previousParagraph().paragraphLeftIndent();
			paras[i].alignment = "align paragraph left";
			paras[i].textObject.fontObject.fontSize = 12;
		}
	//契約書用条数
	} else if ( /^第[0-9]+条/.test( paraStrL ) ) {
		paras[i].firstLineIndent = -12
		paras[i].paragraphLeftIndent = 12;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	//Align等をスペースで指定する場合
	} else if ( ( /^  [^ ]/.test( paraStrL ) && /[^ ] \r/.test( paraStrR ) ) || ( /^  [^ ]/.test( paraStrL ) && /[^ ] \r/.test( paraStrR ) ) ) {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph right";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	} else if ( ( /^  [^ ]/.test( paraStrL ) && /[^ ]  \r/.test( paraStrR ) ) ||  ( /^  [^ ]/.test( paraStrL ) && /[^ ]  \r/.test( paraStrR ) ) ) {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph center";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = false;
	} else if ( ( /^   [^ ]/.test( paraStrL ) && /[^ ]   \r/.test( paraStrR ) ) || ( /^   [^ ]/.test( paraStrL ) && /[^ ]   \r/.test( paraStrR ) ) ) {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph center";
		paras[i].textObject.fontObject.fontSize = 14;
		paras[i].wordWrap = false;
	} else if ( ( /^    [^ ]/.test( paraStrL ) && /[^ ]    \r/.test( paraStrR ) ) || ( /^    [^ ]/.test( paraStrL ) && /[^ ]    \r/.test( paraStrR ) ) ) {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph center";
		paras[i].textObject.fontObject.fontSize = 16;
		paras[i].wordWrap = false;
	} else if ( ( /^     [^ ]/.test( paraStrL ) && /[^ ]     \r/.test( paraStrR ) ) || ( /^     [^ ]/.test( paraStrL ) && /[^ ]     \r/.test( paraStrR ) ) ) {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph center";
		paras[i].textObject.fontObject.fontSize = 18;
		paras[i].wordWrap = false;
	//何とも一致しなかった場合のデフォルトの段落
	} else {
		paras[i].firstLineIndent = 0
		paras[i].paragraphLeftIndent = 0;
		paras[i].alignment = "align paragraph left";
		paras[i].textObject.fontObject.fontSize = 12;
		paras[i].wordWrap = true;
	}
}
}
テンプレートから新規ドキュメント→ペースト→インデント設定関数を実行、のスクリプト

 テンプレートから新規ドキュメントし、ペーストし、ドキュメント全体にインデント設定関数を実行するスクリプトです。
 "filepath = "の後の部分には使いたいMS Wordテンプレートのホームディレクトリからのパスを指定します。
 上記ライブラリに"kianLibrary"という名前を付けて置いてあることが前提になっています。別の名前を付ける場合は、"Library('kianLibrary')"の中身を変えます。

//使いたいMS Wordテンプレートのホームディレクトリからのパスを指定。
var filePath = '/Dropbox/kiansaiban.dotx';
//予め"~/Library/Script Libraries/"に"kianLibrary"を置いておく
kianLibrary = Library('kianLibrary');

//アプリケーションを変数に格納
var Finder = new Application('Finder');
Finder.includeStandardAdditions = true;
var Word = new Application('Microsoft Word');

//テンプレートから新規ドキュメントを作成しつつMS Wordを起動
var folderPath = Finder.pathTo( 'home folder', {from:'user domain'} );
var doc = Word.createNewDocument({attachedTemplate:folderPath+filePath});
Word.activate();

//クリップボードの中身をプレーンテキストにしてペースト
Word.pasteSpecial (doc.textObject,{dataType:'paste text'});
//ドキュメント全体に対してインデント設定
kianLibrary.kianIndent(doc.textObject);
選択範囲にインデント設定関数を実行するスクリプト

 上記ライブラリに"kianLibrary"という名前を付けて置いてあることが前提になっています。別の名前を付ける場合は、"Library('kianLibrary')"の中身を変えます。

kianLibrary = Library('kianLibrary');
var Word = new Application('Microsoft Word');

kianLibrary.kianIndent(Word.selection);