Notepad++のXMLファイルをソートして整形して出力する:1発目

Notepad++には関数補完とコールチップ機能*1があって、その実現にXMLで書かれた設定ファイルを使用している。例えばC言語用のXMLはこんな感じ。

<?xml version="1.0" encoding="WINDOWS-1252" ?>
<NotepadPlus>
  <AutoComplete language="C">
    <Environment ignoreCase="no" startFunc="(" stopFunc=")" paramSeparator="," terminal=";" />
    <KeyWord name="#define" />
    <KeyWord name="#elif" />
    <!-- 中略 -->
    <KeyWord name="abort" func="yes">
      <Overload retVal="void" descr="C89: stdlib.h: stops the program">
        <Param name="void" />
      </Overload>
    </KeyWord>
    <!-- 以下略 -->
  </AutoComplete>
</NotepadPlus>

C言語用の設定ファイルはデフォルトで付いているのだが、個人的に気に入らないので*2書き直している。書き直すにあたりKeyWord要素をC言語の規格等で分けて記述している。こうするとデータの過不足を調べたりする時に都合が良いのだが、しかし都合が悪いことにNotepad++側ではKeyWord要素が属性nameの昇順にソートされていないと補完機能等がうまく動作しないようだ。

そこで作成したXMLを読み込み、KeyWord要素を属性nameの順でソートして、整形して出力するツールを作ってみることにした。

Rubyで初めてREXMLを使い*3、当初は真面目にXMLのノードを操作することで実現しようと四苦八苦していたのだが、うまくいかなかったので力技で解決してみた。

#!/usr/bin/ruby -w -Ks
#
#= Notepad++の関数補完用の設定XMLファイルをキーワード順にソートするツール
#
#author:: eel3 @ TRASH BOX
#date::   2011/02/24
#
#== 動作確認環境
# - ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mswin32] @ Windows XP Pro SP3
#

require 'rexml/document'

# XML文書を整形して出力するクラス
class XMLWriter
  # 初期化メソッド。出力先 _output_ を指定すること。
  def initialize(output, defindent = 0)
    @formatter = REXML::Formatters::Pretty.new
    @output = output
    @default_indent = ' ' * defindent
  end

  # 要素 _elem_ 以下のノードを整形して出力する
  def write(elem)
    @output << @default_indent
    @formatter.write elem, @output
    @output << "\n"
  end
end

# XML文書の文字列 _s_ 整形した文字列を返す
def format_xml(s)
  output = ''
  XMLWriter.new(output).write REXML::Document.new(s)
  output
end

# 設定XMLファイルの先頭部分のみを文字列として返す
def npp_xml_header(s)
  s.select {|i| /^<\?xml/ =~ i || /<NotepadPlus>/ =~ i || /<AutoComplete/ =~ i }.join
end

# 設定XMLファイルの末尾部分のみを文字列として返す
def npp_xml_footer(s)
  s.select {|i| /<\/AutoComplete/ =~ i || /<\/NotepadPlus>/ =~ i }.join
end

# 設定XMLファイルの子要素をソートして、結果を文字列として返す
def npp_sorted_childs(s)
  doc = REXML::Document.new(s)
  output = ''
  writer = XMLWriter.new(output)

  doc.root.elements[1].elements.to_a.sort {|a, b|
    if a.fully_expanded_name != b.fully_expanded_name
      a.fully_expanded_name <=> b.fully_expanded_name
    else
      a.attributes.get_attribute('name').value <=> b.attributes.get_attribute('name').value
    end
  }.each {|elem|
    writer.write elem
  }
  output
end

# メインルーチン
def main
  src_xml = readlines.join

  tmp_xml = npp_xml_header(src_xml)
  tmp_xml << npp_sorted_childs(src_xml)
  tmp_xml << npp_xml_footer(src_xml)

  print format_xml(tmp_xml)
end

main

まず第一段階として、KeyWord要素を設定ファイルの事実上のリーフに位置する要素と見なして、それより上位の要素を単純なテキスト処理で取り扱い、KeyWord要素と同じ階層の要素のみをXMLとしてREXMLを使って操作している。

第一段階でソートされたXML文書が文字列として生成されるが、この状態では正しくインデントされていない。改めてREXMLでXMLとして取り扱い、REXML::Formattersでお気楽に整形出力している。

設定ファイルを行単位で2回も舐めたり、REXMLで2回もXML文書を読み込んだりと、どう見ても力技でしかないのだが、手元で使う分には特に不満はない。というか総データ量の少なさとマシンスペックで誤魔化されてしまう所が恐ろしい。

とはいえ個人的に納得いかないので書き換えるつもりだ。

*1:関数の引数や戻り値、ちょっとした説明を表示する機能。

*2:C95やC99の予約語と標準ライブラリの単語が不足していたり、非標準の関数やらPOSIX APIやらGNU拡張やら何やらがごちゃ混ぜになっている所が気に入らない。

*3:実は自前でクラスを定義したのも初めて。