HTMLDocumentの構造

JEditorPaneでhtmlファイルを読み込むと、HTMLDocument形式で保持します。 HTMLDocument内のデータ構造は独自形式です。 API仕様の記述だけではHTMLDocumentの構造がよく分からなかったので、構造を調べるコードを作ってみました。

調べた内容はhtml形式で標準エラー出力に吐き出します。 リダイレクトでファイルに保存してください。

JEditorPaneで扱えるDocumentはいくつかありますが、HTMLDocumentを含めて全てのDocumentはElementのツリー構造になっています。 ツリーのルート要素はgetDefaultRootElementメソッドで得ます。 htmlファイルを読み込んでからツリーに展開するまで少し時間がかかります。 JEditorPane.setPageの直後に調べてもまだDocumentには反映されていません。 展開後DocumentEventが発行されるので、DocumentListener.changedUpdateの中で調べます。 ここで得たElementはjavax.swing.textパッケージのものであり、org.w3c.dom.Elementではないので注意しましょう。

Document doc = _editorPane.getDocument();
Element elem = doc.getDefaultRootElement();

Elementを再帰的に探索してHTMLDocumentの構造を調べます。 Elementには属性のコレクション(AttributeSet)が収められていて、その中にHTMLのタグ名やHTMLタグの属性などが記録されています。 サンプルコードでは調べた構造をhtml形式で出力するので、子のELementはdivタグで囲んで出力してます。

private void printElements(Element elem, int depth)
{
    printAttributes(elem);
    
    int count = elem.getElementCount();
    if(0 < count)
    {
        System.err.print("<div class=\"depth" + (depth % 3) + "\">");
        for(int i = 0; i < count; i++)
        {
            printElements(elem.getElement(i), depth + 1);
        }
        System.err.print("</div>");
    }
}

AttributeSet.getAttributesで属性キーのEnumerationが得られます。 キーはString型ではないので注意しましょう。 例えば、nameという文字列表現のキーで得られる属性があるのを知っていてAttributeSet.getAttribute("name")としてもnullしか返って来ません。 Enumerationが返すObjectをそのまま使って属性を得ましょう。

属性がHTML.Tag.CONTENTの場合はテキストコンテンツを表しています。 これはXMLのDOMドキュメントの#TEXTに当たります。 サンプルコードでは後述のelementToStringメソッドで文字列を取り出して表示します。 それ以外のクラスの場合、「属性名 : クラス別の書式」で表示します。

private void printAttributes(Element elem)
{
    String str = "<ul>";
    
    AttributeSet attrs = elem.getAttributes();
    Enumeration attrNames = attrs.getAttributeNames();
    
    while(attrNames.hasMoreElements() )
    {
        Object attrName = attrNames.nextElement();
        Object attr = attrs.getAttribute(attrName);
        
        if(attr == HTML.Tag.CONTENT)
        {
            str += "<li>" + elementToString(elem) + "</li>";
        }
        else
        {
            str += "<li>" + attrName.toString() + " : ";
            str += attributeToString(attr) + "</li>";
        }
    }
    str += "</ul>";
    
    System.err.print(str);
}

HTML.Tag.CONTENTの内容の文字列はAttributeSetに記録されていません。 HTMLDocument.getTextで調べます。 getTextに始点と長さを渡すと、htmlタグを取り除いた文字列を返します。 文字列の始点と長さはElementから調べます。

getTextで改行コードが帰ってくることもあります。 サンプルコードでは、改行コードを「改行」という文字列で置き換えています。

private String elementToString(Element elem)
{
    String res = null;
    Document doc = elem.getDocument();
    int elemStart = elem.getStartOffset();
    int elemLen = elem.getEndOffset() - elemStart;
    try
    {
        res = doc.getText(elemStart, elemLen);
    }
    catch(BadLocationException exc)
    {
        exc.printStackTrace();
    }
    
    // 要素の内容が改行ならば「改行」という文字列を返す
    if("\n".equals(res) )
    {
        res = "改行";
    }
    // その他の場合
    else
    {
        res = "「" + res + "」";
    }
    
    return res;
}

次のコードはAttributeSetに収められている各属性をクラス別の書式で文字列表現にして返すメソッドです。 最初にソースを書いたときはattr.toStringとattrのクラス名を連結して返すだけだったのですが、収められているクラスが限られている様なのでクラス毎に書式を変えて返すようにしました。

private String attributeToString(Object attr)
{
    String className = attr.getClass().getName();
    if("java.lang.String".equals(className) )
        return "\"" + attr.toString() + "\"";
    else if("java.lang.Boolean".equals(className) )
        return attr.toString() + "(bool)";
    else if("javax.swing.text.html.HTML$Tag".equals(className) )
        return attr.toString() + "タグ";
    else if("javax.swing.text.html.HTML$UnknownTag".equals(className) )
        return attr.toString() + "タグ(Unknown)";
    else if("javax.swing.text.SimpleAttributeSet".equals(className) )
        return attr.toString() + " (SimpleAttributeSet)";
    
    return "[" + attr.getClass().getName() + "]";
}

構造のメモ

出力例を見て分かるとおり、Elementのツリー構造はhtmlの構造そのままではなく、HTMLDocument用に再構成して収められています。

まず目に付くのはp-impliedタグです。 API仕様によると「すべてのテキストコンテンツは、段落要素内に存在しなければならない」というHTMLDocument独自のルールがあり、pタグで囲まれていないコンテンツはこの擬似タグで囲まれます。 htmlヘッダでもtdタグの中身でも、段落要素が無い場合は全て囲まれるのがhtmlとの違いです。 また、前述の通りテキストコンテンツはHTML.Tag.CONTENT擬似タグで囲まれます。

一部が文字修飾されたテキストコンテンツはタグの入れ子が展開され、段落要素内に並べられます。 修飾の詳細については調べていません。 Aタグもテキストコンテンツに変換して収められます。 リンク先はSimpleAttributeSetに収められているようです。

inputやtextareaなど、フォームのコンポーネントを表すタグにはmodel属性が追加されます。 model属性の中身はToggleButtonModelやPlainDocumentなどのswingモデルクラスやテキストのコンテナクラスです。 モデルクラスにリスナーを追加すればユーザの操作を把握できます。

タグ/タイプクラス備考
text FixedLengthDocument PlainDocumentのサブクラス。 HTMLDocument内のprivateクラスなのでimportできない。 PlainDocumentにキャストして使う。 insertStringメソッドで挿入できる文字列の長さに制限がかかる。 inputタグのmaxlength属性で指定された長さを越えると無視される。
password FixedLengthDocument
checkbox ToggleButtonModel -
radio ToggleButtonModel -
file PlainDocument ボタンのモデルにはアクセスできない。
button - jdk1.6(html3.2)では未対応
image DefaultButtonModel -
select(list) OptionListModel DefaultListModelのサブクラスでListSelectionModelを実装する。 パッケージ専用クラスなのでimportできない。 どちらかにキャストして使う。
select(combobox) OptionComboBoxModel DefaultComboBoxModelのサブクラス。 パッケージ専用クラスなのでimportできない。 DefaultComboBoxModelにキャストして使う。 初期選択を復元するsetInitialSelectionメソッドが追加されているが、DefaultComboBoxModelにキャストして使うのでOptionComboBoxModelのメソッドにはアクセスできない。
textarea TextAreaDocument PlainDocumentのサブクラス。 パッケージ専用クラスなのでimportできない。 PlainDocumentにキャストして使う。 初期テキストを復元するresetメソッドが追加されているが、PlainDocumentにキャストして使うのでTextAreaDocumentのメソッドにはアクセスできない。
reset DefaultButtonModel -
submit DefaultButtonModel -

selectの選択項目やtextareaの初期テキストなど、swingコンポーネントに設定されるデータはElementツリーに登録されない場合があります。