#author("2017-02-11T18:21:30+09:00","","")
&size(24){XML開発の日};

このページでは、一太郎Arkの様々な面に焦点を当てて観る内容になってます。
 
一太郎Arkのソースコード公開や、各種プラグインのソースコードの公開などがあって、ユーザーの方々がプラグインを作ったりパッチをあてたりしていろいろ遊べるようになりました。
第四回XML開発者の日  もっとも遊ぶといっても、16万行のソースコードとAPIリファレンスだけではさすがに敷居が高いのも事実です。特に編集コマンドの拡張は一太郎Arkの内部設計の深い部分に関わるので、XMLもJavaもマスターしていて毎日DOM API で喋っている人でも、Arkのソースだけ読んで理解するのは大変です。
 そこで編集コマンドを拡張するプラグインを書くために必要な知識をまとめた簡単なチュートリアルとサンプルプラグインを作成し、10月28日に行われた「第四回XML開発者の日」(XMLフェスタ2000の最終日に開催)で発表させていただきました。当日会場で配布した資料を整理したものをここで紹介します。

番外編 特集・XML開発者の日 ~一太郎ArkプラグインによるXML編集~ 	 

#contents(); 	
//●あらまし
//●一太郎Arkとは
//●一太郎Arkソース公開
//●例題:ツリーフラグメント挿入プラグイン
//●関連情報
//●最後に

**あらまし [#c513bc74]
一太郎Ark[ark]は1999年12月に発売されたジャストシステムのワープロ製品です。100% Pure Java で実装されたことや、ファイルフォーマットに XHTML 1.0 を採用していることなどで、一部の人たちの間でかなり話題になっていたようです。

一太郎Arkはプラグインによって機能拡張できる特徴があり、実際の製品もコアモジュールと標準プラグインによって構成されていましたが、発売後しばらくはサードパーティーがプラグインを開発するためのまとまった技術資料が公開されていませんでした。

しかし2000年5月に一太郎Arkの全ソースコード[src]が公開され、企業や個人が自由にプラグインを開発できるようになりました。同時に開発者同士の情報交換のためのオフィシャルメーリングリスト Ark Developers ML [ml]も開設されました。

この発表では一太郎Ark上で複雑なXML文書の編集操作を支援するようなプラグインを作る方法について解説します。例題のプラグインは以前[ml]に投稿したものを大幅に書き換えたものです。

**一太郎Arkとは [#aba54fe6]
一太郎Arkがどんなアプリケーションなのかについて簡単に説明します。
***一太郎Arkの「こころざし」 [#q8a7afb3]
一言で言うと...「構造化文書版 Emacs」を目指しています。すなわち、
-プレーンテキストから XML へ 
-elisp から Java へ 

という方向で文書編集環境を進化させよう、というのがねらいです。

***一太郎Arkのアーキテクチャ [#r171ff5c]
全体のアーキテクチャはいわゆる MVC (model-view-controller)アーキテクチャです。

中核となる model 部分では、文書データの内部表現に DOM level 1 Core を、スタイル情報の内部表現は CSS level 2 に相当するデータ構造を保持しています。

&ref(mvc.gif);
 
**一太郎Arkソース公開 [#rdaacb29]
先述のように一太郎Arkのソースコードが公開されたことで、一般開発者が一太郎Arkをベースにしたアプリケーション開発が出来るようになりました。ここでは公開されたソースの扱い方についてライセンスや開発の作法の点での注意点を説明します。
 
***ライセンス [#v2517b91]
一太郎Arkおよび標準プラグインのソースコードは「一太郎Arkデベロッパーライセンス」に従うことを条件に公開されています。残念ながらこのライセンスはオープンソースライセンスではありません。このライセンスでできることとできないことを簡単にまとめておきます。

できること:
-開発・評価目的でソースおよびバイナリを利用できます。
-プラグインを開発しそのソースやバイナリを自由に配布できます。

できないこと:
-ソースおよびバイナリの業務利用はできません。(製品版の一太郎 Ark のユーザーの場合は業務にも利用できます。)
-ソースおよびバイナリの再配布はできません。
-公開したパッチをジャストシステムが利用することを制限できません。
(但しジャストシステムには製品に組み込んだ場合にパッチ作者の著作権表示を表示する義務があります。)

***プラグインとパッチ [#j0f7a466]
プラグインは動作中の一太郎Arkに動的に追加できるプログラムモジュールを指します。プラグインは基本的に製品版のユーザーがそのまま利用できる形態です。

パッチは改変したソースコードとオリジナルソースコードとの間の差分です。動作させるためにはオリジナルソースコードとビルド環境を用意して一太郎Arkをリビルドしなくてはなりません。

プラグインには一般ユーザーが利用できること、ライセンス上の制限がないことなどの利点があります。

***プラグイン/パッチで何ができるか [#vcb71822]
プラグインによる機能拡張の代表的な例には以下のようなものがあります。

-編集コマンドの拡張 (例: 特殊文字挿入、XSLT変換) 
-ファイル入出力の拡張(例: Word文書読み込み、FTP送受信、Zip圧縮展開) 
-表示の拡張(例: SVG表示) 
-言語リソースの追加(例: 中国語リソース、韓国語リソース) 

もっとも一太郎ArkのプラグインAPIは現状では決して十分整備されているとは言えません。そのため様々な制限があり、場合によってはソースコードにパッチを当ててプラグインAPIを拡充する必要があるでしょう。

**例題: ツリーフラグメント挿入プラグイン [#qd639bba]
ここでは任意の DOM ツリーフラグメントをカーソル位置に挿入するプラグイン(ツリーフラグメント挿入プラグイン)を例に、DOM API を利用した編集コマンドの拡張のしかたを解説します。

なお例題プラグインのソースコード一式とコンパイル済みの jar は [struct] からダウンロードできます。

***仕様 [#t3fb8908]
■ 機能
 XML で記述されたデータ構造を編集中の文書に挿入します。ネストした章節構造や複雑な表、ユーザー定義タグセットによるデータ構造などを簡単に作成できるようにします。挿入するデータ構造は外部の設定ファイルから読み込みます。

■ 操作
 キーカスタマイズファイルにプラグインで追加したカスタムアクションにキーを割り当てることでキーボードショートカットで挿入することもできます。

■ 設定ファイル
 設定ファイルは XHTML 形式で記述します。だいたい以下のような書式になります。

1:<?xml version="1.0"?>
2:  <html xmlns="http://www.w3.org/1999/xhtml"
3:        xmlns:s="http://www.horobi.com/xmlns/ark/struct">
4:  ...
5:	<s:structures>
6:
7:	  <s:struct>
8:	    <s:metainfo>
9:	      <s:name>構造体その 1</s:name>
10:	      ...
11:	    </s:metainfo>
12:	    <s:contents>
13:	      ...
14:	    </s:contents>
15:	  </s:struct>
16:
17:	  <s:struct>
18:	    ...
19:	  </s:struct>
20:
21:	</s:structures>
22:  </html>
 
個々のタグの意味は以下の通りです(プレフィクスを s と仮定しています。名前空間 URI は http://www.horobi.com/xmlns/ark/struct です)。

s:structures 
 ツリーフラグメント定義のリストを表します。s:struct を複数含みます。 
s:struct 
 一つのツリーフラグメントの定義を表します。s:metainfo と s:contents をそれぞれ一つずつ含みます。 
s:metainfo 
 ツリーフラグメントに関する情報を表します。s:name を複数含みます。 
s:name 
 ツリーフラグメントにつけた名前を表します(メニューに表示される名前です)。文字列を含みます。 
s:contents 
 挿入されるツリーフラグメントを表します。任意の内容(要素、テキスト、コメントなど)を含みます。

 なお、適切なスタイルシートをリンクした設定ファイルは一太郎 Ark で編集できます。設定ファイル編集用の設定ファイルを使って設定用タグを挿入できます。

■ 制限
話を簡単にするために例題のプラグインには以下の点は制限事項にしておきます。

-挿入位置が決め打ちになっています。必ずカーソル位置の直近の祖先のブロック要素の手前に(previous sibling として)挿入されます。
-挿入先の文書に挿入元の構造体が使用しているタグセットが含まれない場合に文書構造が不正になります。

***プラグインの作り方 [#d9b4deee]
一太郎Arkのプラグイン作成方法については、一太郎Arkのオフィシャルサイトに「プラグイン作成講座」[ples] が公開されていますので、基本事項はそちらに譲ることにして、編集コマンドの実装に関わる点だけ説明します。

APIの詳細は一太郎Arkソースコードダウンロードサイト[src]でJavadoc形式のAPIリファレンスが入手できます(日本語のみ)ので、そちらを参照して下さい。

■実装方法
編集コマンドを拡張するようなプラグインでは以下のインターフェースを実装する必要があります。

UIPlugin (jp.co.justsystem.ark.plugin.UIPlugin)
 プラグインのメインクラスでは UIPlugin インターフェースを実装します。これによってメニューアイテムとメニューを選択した時に呼ばれるアクションを登録できます。

Action (javax.swing.Action) 
 メニュー選択やキー入力で実行される動作の定義がActionです。プラグインで拡張する編集コマンドは Action#actionPerformed() で定義します。ただし、実際のDOMツリー操作の定義は EditCommand で実装します(理由は後述)。actionPerformed() には EditCommand を new してコマンドキューに詰め込む処理だけを書きます。 

EditCommand (jp.co.justsystem.ark.model.command.EditCommand) 
 実際に DOM API で DOM ツリーを操作するコードは EditCommand#execute() に書きます。

■キーカスタマイズとの連携
 一太郎Arkのメニューで「ツール - オプション...」で出てくるオプションダイアログには「キーカスタマイズ」という項目があって、キーカスタマイズファイルを設定できます。

キーカスタマイズファイルは XML 形式のファイルで、フォーマットに関しては jp.co.justsystem.ark.ui.keybind パッケージの API リファレンスに説明がありますが、便利なように付録にしておきます。

プラグインで追加したアクションについてもキーカスタマイズファイルでキーを割り当てることができます。例題プラグインの場合は、アクション名が "com_horobi_ark_struct.INSERT_BLOCK_STRUCT?" (? の部分はツリーフラグメントの番号)ですので、ctrl-T + 1 で最初のフラグメント、ctrl-T + 2 で二番目のフラグメントを挿入するようにキーを割り当てるには以下のようにします。

1:<?xml version="1.0" encoding="utf-8" ?>
2:<keyConfig>
3:  <binding context="document_inputmode" keymap="addKeymap" layer="ext"/>
4:  <binding context="document_resizemode" keymap="addKeymap" layer="ext"/>
5:  <keymap id="addKeymap" >
6:    <keymapRef stroke="CTRL+T" idref="CTRL_T_KEYMAP" />
7:  </keymap>
8:  <keymap id="CTRL_T_KEYMAP">
9:    <command stroke="CTRL+G" action="EnterInputMode" />
10:    <command stroke="1" action="com_horobi_ark_struct.INSERT_BLOCK_STRUCT0" />
11:    <command stroke="2" action="com_horobi_ark_struct.INSERT_BLOCK_STRUCT1" />
12:  </keymap>
13:</keyConfig>

***XML 文書の扱い方 [#h07fe1e8]
 設定ファイルで使う XML 文書のロード、編集中の文書内での扱いなどについて主な注意点について説明します。

■ 内蔵パーサの使い方
 一太郎Arkが内蔵しているXMLパーサ(jp.co.justsystem.io.sax.SAXDOMParser)は、文字コードを自動認識してくれません。正確には半自動認識のような機能があるのですが、UTF-16 の認識や認識後のパースのやり直し処理などはユーザーが自分でコードを書かないといけません。

必要なコードは 40 行程度ですが、普通の XML パーサなら 4 行ほどで済むところなのでかなり理不尽です。付録のソースコードにある _parseXML(String path) メソッド(299行目)のコードを遠慮なくコピーして使ってください。

一太郎Arkの内蔵パーサは単体のXMLパーサとして見るとDTDを全く見ないなど仕様適合性に一部問題があります。必要に応じて Xerces-J などの Java で実装された外部パーサを利用することも可能です。

ただしDOMパーサで構築したDOMツリーを文書の内部表現に使用する場合はArkのDOM実装を使ってDOMツリーを構築するように設定してください。Xerces-J の場合は以下のようにします。

	import org.apache.xerces.parsers.DOMParser;
	...
	  DOMParser parser = new DOMParser();
	  parser.setDocumentClassName("jp.co.justsystem.ark.model.document.ArkDocument");
	  ArkDocument doc = (ArkDocument)parser.getDocument();
	

■ DOMツリーの「正規化」(ファイル読み込み)
Ark は Ark の編集ポリシーに合わせて XHTML 文書内でのタグの使い方を一部制限しています。外部から読み込んだファイルはそのような制限を守っているとは限らないので、編集開始前に正規化(タグの変換や構造の補正)を行う必要があります。その正規化を行うのが XMLDOMTreeNormalizer (jp.co.justsystem.io.sax.XMLDOMTreeNormalizer)です。

 XMLDOMTreeNormalizer の挙動は実は文書に埋め込まれた(あるいは関連づけられた) CSS スタイルシートに依存します。たとえば以下のような XHTML ファイルを考えます。

001:<html>
002:  <head><title>title</title></head>
003:  <body xmlns="http://www.w3.org/1999/xhtml"
004:        xmlns:h="http://foo.bar.baz/hoge">
005:    <h:hoge>
006:      hoge
007:      <h1>heading</h1>
008:      hoge
009:    </h:hoge>
010:  </body>
011:</html>
	
この文書を XMLDOMTreeNormalizer で正規化すると以下のように h:hoge 要素がズタズタに引き裂かれてしまいます。

001:<html>
002:  <head><title>title</title></head>
003:  <body xmlns="http://www.w3.org/1999/xhtml"
004:        xmlns:h="http://foo.bar.baz/hoge">
005:    <h:hoge> hoge </h:hoge>
006:      <h1><h:hoge>heading</h:hoge></h1>
007:    <h:hoge> hoge </h:hoge>
008:  </body>
009:</html>
		
これは XMLDOMTreeNormalizer が h:hoge タグをインラインタグと見做して、「インラインタグがブロックタグの外側にあるのはおかしいじゃないか」と思ってタグの親子関係を入れ替えてしまうからです。

このような事態を回避するには以下のようにして CSS スタイルシートを文書に埋め込んでおく必要があります。

1:<html>
2:  <head><title>title</title>
3:  <style type="text/css"><![CDATA[
4:    h\:hoge { display:block; }
5:  ]]></style></head>
6:  <body xmlns="http://www.w3.org/1999/xhtml"
7:        xmlns:h="http://foo.bar.baz/hoge">
8:    <h:hoge> hoge </h:hoge>
9:      <h1><h:hoge>heading</h:hoge></h1>
10:    <h:hoge> hoge </h:hoge>
11:  </body>
12:</html>
		

■ DOMツリーの「正規化」(編集時)
 一太郎Arkでは一部の編集操作の際に、編集コマンドでは直接触っていない部分(例えばカーソル位置の段落全体)のツリー構造を変えてしまうことがあります。これは一種のゴミ掃除で、空になったインライン要素を削除しています(DocumentModelImpl#_normalizeSubTree() が実行しています)。元々空要素として使われる既知の要素型の要素(img 要素など)は意図的に削除しないようにしていますが、それ以外の要素は削除されてしまいます。

このため、ツリーフラグメント挿入プラグインで空のインライン要素を含むフラグメントを挿入した場合、挿入直後は大丈夫ですが、周辺で編集操作を行うと、正規化が実行されて突然空要素が消えてしまうという現象が発生します。

これを回避する簡単な方法はありません。適切なロジックで必要な空要素を残すように一太郎Ark本体のコードを書き換えるしかないでしょう。

***編集コマンドの作り方 [#w28fe832]
編集コマンドを正しく実装するには一太郎Arkのコマンド処理システムの構造をある程度理解しておく必要があります。DOMツリー編集自体はお馴染みの DOM API が使えますが、エディタのコマンドとして再表示や undo といった機能がうまく働くようにするためには色々なオマジナイが必要になります。 

ここではそういったオマジナイの唱え方とその意味を説明します。

■ コマンド処理の流れ
コマンドの実行はユーザーのメニュー選択やキー入力で始まり、コマンド実行結果が画面上の文書に反映されて終わります。一太郎Arkでは、Java の GUIシステムの特性に合わせた処理や、処理の効率化のために、コマンドは複数のスレッドが協調して処理するように設計されているため、やや複雑な処理をしています。
ここではそういったコマンド処理の流れの概略を説明します。

▼ UIイベント処理
コマンドはユーザーインターフェース(UI)で発生したイベント処理から始まります。Java のGUIシステムでは、UIイベント処理は EventDispatchスレッドという専用のスレッドの中で行われます。プラグインで実装した Action#actionPerformed() メソッドはこのスレッドで実行されます。

普通のJava アプリケーションでは再描画処理(これは後から再描画イベントが来たタイミングで処理するのが一般的です)以外のコマンド処理を、UIイベント処理の中で実行してしますが、一太郎ArkではUIイベント処理では実際のコマンド処理を予約するだけでリターンしてしまいます。

具体的にはコマンドオブジェクト(jp.co.justsystem.ark.command.Command を実装したもので EditCommand もその一種です)を作ってコマンドキューに追加し、コマンドの処理をCommandInvokerスレッドに処理を引き継ぎます。

▼ コマンドキューイング
コマンドキューの管理は CommandInvoker (jp.co.justsystem.ark.command.CommandInvoker)オブジェクトが管理しています。CommandInvoker は専用のスレッド内でキューに溜まったコマンドを処理していきます。Command#execute() を実行します。

 コマンドをキューイングして別スレッドで実行するのは EventDispatch スレッドを速やかに開放して見た目のレスポンスを良くする目的もありますが、コマンドのマージというもっと重要な目的があります。

 例えば高速に文字入力した場合、大量に発生した文字挿入コマンドを一文字ずつ律儀に処理していくと文字の数だけレイアウト処理が実行されます。コマンド処理の中ではレイアウト処理は比較的重い処理なので、そこがボトルネックになって処理の遅延が起こり、レスポンスが悪くなります。

 しかし、連続した文字挿入コマンドは一つの文字列挿入コマンドと等価ですので、最初の文字挿入コマンドの処理に手間取っている間に先行入力された複数の文字挿入コマンドを一つにまとめてしまう(コマンドのマージ)ことで、レイアウト処理の回数が大幅に減少し、レスポンスが向上します。

&ref(command.gif);
コマンドキューイング
▼ 再描画
レイアウト処理が終わるとJavaのGUIシステムに更新部分の再描画要求を出してCommandInvokerスレッドでのコマンド処理は終わります。実際の再描画は再描画イベントを処理する際にEventDispatchスレッド内で実行します。

■ 更新通知と再表示
ArkDocument(Ark の Document 実装)には、DOM ツリー更新通知イベントを受け取るリスナー(DocumentListener)を登録できます。

DocumentView は DOM ツリーの更新通知を受け取るために DocumentListener インターフェースを実装しています。DocumentView は DocumentModel と関連づけられたときに、DocumentModel の管理する ArkDocument に自分自身をリスナー登録します。以後 DocumentModel が DOM ツリーを更新する度に DocumentView に通知が行くようになります。

&ref(m-v.gif);
▼ DOM ツリー更新通知プロトコル
DOM ツリーの更新は DOM ツリーを編集するスレッド内でのメソッド呼び出しを介してリスナーに通知されます。通知には以下のような種類があります。

更新開始イベント (willBeUpdated)
 DOM ツリーが更新される直前に発生します。ノード更新通知が来るまで DOM ツリーは不安定な状態になっています。
リスナーは他のスレッドから自分を介した間接的な DOM へのアクセス(例えば view の再表示など)が発生しないように、このタイミングで何らかのロック操作を行います。更新処理に必要なリソースもここで確保しておきます。

ノード更新イベント (textChenged, elementChanged)
一区切りの DOM ツリー操作が終わる度に発生します。更新内容を記述したイベントオブジェクトがリスナーに送られます。イベント処理中は DOM ツリーは安定しています。リスナーは更新部分に対応するノード参照を更新するなどの作業を行います。
但しこのイベントを処理した後も引き続き DOM ツリー操作が行われる可能性があります。リスナーはこの時点ではまだ他のスレッドからの間接的な DOM アクセスを許してはいけません。

更新終了イベント(updated)
一連の DOM 操作が完了した後で発生します。ここから先は次に更新開始イベントが来るまで DOM ツリーは安定しています。リスナーはロックを解除します。更新開始時に確保したリソースはここで解放します。
これらのイベントは以下のような順番と回数で発生します(表記は DTD 風)。

&ref(protocol.gif);
willBeUpdated, (textChanged | elementChanged)*, updated

▼ 更新イベント詳細
更新イベントは更新内容によってさらに種類が細分されます。 

テキスト更新イベント(TextEvent) 
挿入(INSERT)
文字列挿入があった時にテキストノード内の先頭から何文字目に文字列を挿入されたかを通知するイベントです。
削除(DELETE)
文字列削除があった時にテキストノード内の先頭から何文字目から文字列が削除されたかを通知するイベントです。
変更(UPDATE)
テキストノードの内容全体が更新されたことを通知するイベントです。

要素更新イベント(ElementEvent)
子供挿入(INSERT)
要素にノード挿入があった時に
子供削除(REMOVE)

要素変更(UPDATE)
子孫ノードも含めた要素全体の更新を意味します。
属性変更(ATTRIBUTE)
要素に対する属性の追加/削除、属性値の変更を意味します。

更新イベントの種類は Ark の差分レイアウトエンジンの仕様に合わせて設計されています。Ark の差分レイアウトエンジンは兄弟要素間の差分は考慮しますが子孫要素間の差分は考慮していません。

▼ 更新通知のデフォルト動作
DOM ツリー更新通知の仕組みは Ark の DOM 実装内に埋め込まれており、デフォルトの動作として DOM API による編集操作でツリーが更新される度に owner document に登録されているリスナーに更新イベントを送信します。 

この時は更新開始イベントから更新終了イベントまでの一揃のイベントが送信されます。

▼ 更新通知の最適化
デフォルトの更新通知に頼ると更新通知が冗長になり実行効率が悪くなる場合があります。 

例えば Ark で改行キーを押したとき、DocumentModel では DOM ツリーに対して、かなり複雑な DOM API 呼び出しを行いますが、これをデフォルトの更新通知メカニズムをそのまま使って DocumentView に通知すると、DocumentView は DOM ツリーの中間状態をいちいち再レイアウト・再描画してしまいます。このような動作はユーザーを混乱させますし、操作に対する応答速度を低下させます。 

このような場合、編集コマンド実装者は更新通知コードを直接書いて、最適化した更新通知を行うことが出来ます。以下のような手順でデフォルトの更新通知を抑止して更新通知をハードコード出来ます(実際には Undo 関係の処理が必要なのでこれだけでは不十分です)。

 
001:ArkDocument arkdoc = (ArkDocument)document;
002:arkdoc.fireWillBeUpdated();
003:{一連のDOMツリー操作}
004:arkdoc.fireElementChanged(...);
005:{一連のDOMツリー操作}
006:arkdoc.fireElementChanged(...);
007:...
008:arkdoc.fireUpdated();

最適化の方法としては、例えば子孫要素の細々した更新を、親要素の UPDATE イベント一個にまとめてしまうという方法があります。しかしまとめ過ぎるとかえって効率が悪くなるので、リスナーの特性に合わせた調整が必要です。

例えばArk で改行キーを押した場合は、以下のような手順で二回の更新通知にまとめています。
カーソル位置の段落内容を切り詰める。
カーソル位置の段落要素の UPDATE イベントを通知。
新しい段落を作り切り詰められた残りをその段落に挿入。
新しい段落をカーソル位置の段落要素の親要素に挿入。
親要素の INSERT イベントを通知。

&ref(cut_up_block.gif);
■ アンドゥ/リドゥ
 Ark の編集コマンドは undo/redo をサポートする必要があります。編集後 undo 操作で元に戻って欲しいものには以下のものがあります。

-DOM ツリーの構造
-カーソル位置
-view の表示(更新通知)

▼ undo/redo の基本
Ark での undo/redo の基本は、操作の逆操作を undo バッファに記録しておくことです。逆操作は javax.swing.undo.UndoableEdit を実装することで定義します(以下、UndoableEdit を実装したクラスのインスタンスを undo 情報と呼びます)。

undo 情報を記録するには DocumentModel の postEdit() メソッドを使います。undo バッファには内部的に実行される細かい操作が逐一記録されますが、ユーザーが undo 操作を実行した場合はコマンド単位で操作が戻されます。undo バッファに積まれた undo 記録のどこからどこまでを一回の undo 操作で戻すかを指定するには DocumentModel のbeginUpdate(), endUpdate() メソッドを使用します。
まとめると以下のような順番でメソッド呼び出しを行います。

beginUpdate, postEdit*, endUpdate

▼ DOM 実装のデフォルト動作
Ark の DOM 実装には undo の仕組みが埋め込まれていて、DOM API による編集操作でツリーが更新される度に ArkDocument に登録された DocumentModel の postEdit() を呼び出して、逆操作情報を記録します。

更新通知の最適化をしないなら、キャレット位置の undo 記録以外は何もする必要はありません。

▼ 更新通知を最適化する場合
更新通知を最適化している場合は更新通知の逆操作を自分で undo バッファに積む必要があります。

更新通知の undo 情報クラスには以下のものがあります。
TextEventUndoableEdit
 テキストノード内の編集操作の更新通知の undo 情報クラス
ElementEventUndoableEdit
 要素編集操作の更新通知の undo 情報クラス
UpdatedUndoableEdit
 更新開始または更新終了時の更新通知の undo 情報クラス

まずコマンドで実行される一連の DOM ツリー操作の手前と後ろに UpdatedUndoableEditを積む必要があります。手前に積む undo 情報はundo 時に更新終了通知を、redo 時には更新開始通知を行います。後ろに積む undo 情報はその逆を行います。UpdatedUndoableEdit がどちらの振る舞いをするかはコンストラクタの最後の引数の boolean 値で指定できます。true が前者の、false が後者の振る舞いを指定します。

デフォルトの更新通知を無効にしても DOM ツリー操作の undo 記録自体は有効なので、DOM ツリー操作の手前に逆操作の更新通知をするための undo 情報を postEdit() します。redo 実行時には undo バッファを逆にたどるので、逆操作の更新通知をするための undo 情報は redo 時には何もしてはいけません。

&ref(undo.gif);
また逆に redo で順操作を行った後は順操作の更新通知が行われなくてはいけないので、DOM ツリー操作の後には順操作の更新通知をするための undo 情報も積んでおかなくてはなりません。この undo 情報は undo 時には何もしてはいけません。
 
undoバッファ
TextEventUndoableEdit と ElementEventUndoableEdit はこのような事情を反映して、undo/redo のどちらか片方の実行時だけに通知を行うように実装されています。どちらの undo 情報クラスもどちらの振る舞いをするかをコンストラクタの最後の引数の boolean 値で指定できます。true で undo 時に通知を、false で redo 時に通知を行うように指定します。

▼ カーソル移動の undo/redo
一つ一つのキャレット移動操作は頻繁に(そして気まぐれに)行われるので、undo/redo の対象にはなりませんが、編集操作の直前と直後にはカーソル位置や範囲選択は元に戻るのが望ましいため、編集操作の前後にカーソル位置を戻すための undo 情報 CaretUndoableEdit を積みます。

カーソル移動の undo 情報も更新通知の undo 情報と同様の理由で undo/redo どちらか片方でだけカーソル移動が実行されるようになっています。CaretUndoableEdit がどちらの振る舞いをするかはコンストラクタの最後の引数の boolean 値で指定できます。true で undo 時に移動を、false で redo 時に移動を行うように指定します。

■まとめ
付録のソースコードの InsertBlockStructCommand クラスが編集コマンドの実装例です。InsertBlockStructCommand#execute() メソッド(381行目)と、InsertBlockStructCommand#_insertBlockStruct() メソッド(426行目)には更新通知と undo 記録の定石のようなコードになっています。

TODO:挿入問題
例題とは言いながら使ってみると意外と使えるツリーフラグメント挿入プラグインですが、実用的に使うにはクリアしなくてはならない課題があります。

実際にこのプラグインを使ってみるとわかりますが、挿入位置が末端ブロック要素の兄弟に固定されているのは色々不便があります。例えば挿入した二つのセクションの隙間にもう一つセクションを挿入する、ということができません。

挿入前	挿入後

  <div class="section">
    <h2>第一章</h2>
    <p>...</p>
  </div>
  <div class="section">(A)
    <h2>(B)第三章</h2>
    <p>(C)...</p>
  </div>
 
 * かっこ内はカーソル位置を表す
	

  <div class="section">
    <h2>第一章</h2>
    <p>...</p>
  </div>
  <div class="section">
    <h2>第二章</h2>
    <p>...</p>
  </div>
  <div class="section">
    <h2>第三章</h2>
    <p>...</p>
  </div>
	

 解決策には二通りのアプローチがありそうです。

カーソル移動方式の拡張
通常ツリーの末端にしか止まらないカーソルを、ツリー上の任意の位置に止まれるようにすれば、挿入位置を明示的に指定できるはずです。上の図でいうと(A)の位置にカーソルが止まれば挿入ルールは今のままでもなんとかなりそうです。
Z軸方向(ノードの親子を辿る方向)のカーソル移動が必要になるため、新たなユーザーインターフェースを導入する必要があります。

挿入ルール定義言語による設定
設定ファイル上で挿入するフラグメント毎により複雑な挿入ルールを設定することでなんとかなるかもしれません。
例えば上の図で言うと(B)の位置にカーソルがある場合は <div class="section"> の兄弟として挿入して、(C)の位置にカーソルがある場合は <div class="section"> の子供ととして(ネストして)挿入する、というような設定ができるとよいかもしれません。
そのような設定をするための記述言語を導入する必要がありますが、その言語の仕様を決めるのは簡単ではなさそうです。

また、文書系のXMLを編集する場合、挿入操作は DOM API 的にノードとノードの隙間に挿入するだけでなく、ノードツリーを分割して伴う場合が少なくありません(例えば段落全体を含む範囲の copy & paste など)。この場合もどこまで深く分割するか、という問題が出てきます。

** 関連情報 [#ffdb8fde]
[struct] ツリーフラグメント挿入プラグイン
  http://www.horobi.com/ark/struct/

[ark] Ark Site
 http://www.justsystem.co.jp/ark/
 [ples] プラグイン作成講座
  http://www.justsystem.co.jp/ark/p_lesson/index.html
 [psrc] プラグインソースダウンロード
  http://www.justsystem.co.jp/ark/p_source/index.html

** 最後に [#r6143804]
「XML開発者の日」、当日は、参加者の皆様より貴重なフィードバックを沢山頂きました。本当にありがとうございます。今後の開発に生かしていきたいと思います。

トップ   差分 履歴 リロード   一覧 検索 最終更新   ヘルプ   最終更新のRSS