HTML整形モジュール

さぼり気味ですみません。
前にもちょっと書きましたが、Perlモジュールで
ソースコードの整形/圧縮サイトを作成しておりまして。
JavascriptSQLは簡単に出来たので次はHTML整形!だったのですが…

HTML::Tidyでハマる

HTML::Tidy
多分このライブラリを使ってるモジュール。
これを使用すれば楽ちんかなーと思ってたのですが、
まずcpanのインストールでハマる。
結局普通にインストール出来なかったので以下のURLを参考にインストール。*1
http://www.drk7.jp/MT/archives/001256.html


手順は以下の通り。

  1. yum install tidy libtidy libtidy-devel
  2. cpan -i HTML::Tidy (テストでエラー)
  3. cd /root/.cpan/build/HTML-Tidy-1.08
  4. make
  5. make install

一応これでインストールは出来た。
が、ここからが長かった…


まず、マニュアルを見ながらサンプル実行してみたけど
全く表示されない。
というかコンフィグファイルの設定もよくわからない。*2


ちまたのサイトのHTMLだと大抵warningが出る。
さらにwarningがある場合は整形したHTMLも出力されないらしい…


しまいには全てのwarningを取り除いた結果が、
セグメントフォールト
ここにたどり着くまで結構かかったのに、なんという仕打ち…


整形ツールとかいっぱいあるし、
別に自前でやらなくてもいいやーとか自暴自棄になる。
で、しばらく放っておいたのですが、
偶然別のHTML整形モジュールを発見。


HTML::PrettyPrinterを見つける

HTML::PrettyPrinter
KO・RE・DA!
慣れない英語マニュアルをがんばって翻訳読みつつも、
丁寧なExampleが載っていたので何とか使えるようなりました!


ただ問題が2点ほど。

  1. 微妙に文字化けする
  2. XHTMLには未対応

1点目はHTML::Entitiesの問題っぽい。
いろいろ試行錯誤したところ、オプションで何とかなりそう。*3

  my $hpp = new HTML::PrettyPrinter::XHTML;
  $hpp->entities('<>&"');

entitiesにこの文字列を設定するだけでだいぶ改善された。


2点目ですが、タグの終りが

/>

こんな形式だと、整形されたHTMLが変なことになります。
これは元々対応していないっぽいので、モジュール書いてみました*4
XHTML.pm

package HTML::PrettyPrinter::XHTML;
use base qw( HTML::PrettyPrinter );

use HTML::Entities;
use HTML::Element     1.56;
use HTML::Tagset;

use constant ALWAYS     => -1;
use constant NEVER      => 0;
use constant DEPENDS    => 1;
use constant AFTER_ATTR => 1;

use constant HPP        => '_hpp_'; 

# tags where HTML::Element::as_HTML() is used
%noformattags = map {$_ => 1} qw(pre xmp plaintext listing script style);

sub _format {
  # do the actual formating
  my ($self, 
      $elem,   # HTML::Element to format
      $indent, # number of spaces for indent inside parent element
      $accu,   # working buffer for current line
      $pos,    # current position in line
      $nl,     # number of newlines at current position
      $wsp,    # whitespace at current position? (boolean)
      $ai,     # indent at accu start
      $bp,     # possition of last possible breakpoint
      $bpi     # indent at last possible breakpoint
     ) = @_;
  #my $pos = length $accu;

  if ($self->skip($elem)) {
    # ignore this element
    return ($accu, $pos, $nl, 0, $wsp, $ai, $bp, $bpi);
  }

  # BEFORE ELEMENT
  my $req_nl = $self->nl_before($elem);  # required newlines
  if ($req_nl && ($wsp || 
		  $self->allow_forced_nl() && $self->force_nl($elem))) {
    # line break legal;
    if (!$nl) {
      $self->_add_line($accu,$ai);
      $accu = '';
      $ai = $indent;
      $nl = 1;
      $pos = 0;
      $wsp = 1;
      $bp = 0;
    }
    # already at a new line
    if ($nl < $req_nl) {
      # more empty lines required
      $self->_add_lines((' ') x ($req_nl - $nl));
      $nl = $req_nl;
    }
  }
  
  # ELEMENT
  my $tag = $elem->tag;
  if ($noformattags{$tag} || $tag =~ m/^~/ ) {
    # use HTML::Element::as_HTML 
    my $sav_uc = $HTML::Element::html_uc; # save data;
    $HTML::Element::html_uc = $self->uppercase;
    my $i_str = $self->_tab($indent); # indent string

    # get the lines 
    my @lines = split('\n',$elem->as_HTML($self->entities, $i_str));
    # append to accu
    my $len_l1 = length($lines[0]) - length($i_str);
    if (!$nl) {
	 # still something in the accu
	 if ($ai + $pos + $len_l1  > $self->linelength) {
	   # linebreak at start required
	   if ($wsp) {
	     #  whitespace at current position
	     unshift @lines, $self->_tab($ai).$accu;
	   }
	   elsif ($bp) {
	     # use last breakpoint
	     my $last_line = substr($accu,0,$bp,'');
	     $self->_add_line($last_line,$ai);
	     $bp = 0;
	     $accu =~ s/^\s//;
	     # replace i_str by accu 
	     substr($lines[0],0,0,$self->_tab($bpi).$accu);
	   }
	   else {
	     # no line break possible => replace i_str by accu 
	     substr($lines[0],0,0,$self->_tab($ai).$accu);
	   }
	 } # if line break required
	 else {
	   substr($lines[0],0,0,$self->_tab($ai).$accu.($wsp?' ':''));
	 }
    }
  
    if ($#lines) {
      # multiple lines => append all but the last to array
      $self->_add_lines(@lines[0..$#lines-1]);
      $bp = 0;
    }
    else {
      # compensate for indent now in accu
      $bp += length $self->_tab($ai) if $bp;
    }
    # prepare accu
    $accu = $lines[-1];
    $pos = length($accu) - length($self->_tab($ai)); #compansate for indent
    $ai = 0;
    $wsp = 0;
    $nl = 0;
    
    # ready
    $HTML::ELement::html_uc = $sav_uc; # restore
  } # if handled by HTML::Element->as_HTML()
  else {
    # let PrettyPrinter do it.
    
    # START TAG
    my $tstr = $self->uppercase? "<\U$tag" : "<$tag";
    
    # add to accu => wrap neccessary?
    ($accu, $ai, $pos, $bp, $bpi) = 
      $self->_add2accu($tstr,$indent,$accu,$pos,$ai,$bp,$bpi,$wsp);
    $nl = 0;
    my $cin = $indent + $self->indent($elem);
    
    # ATTRIBUTES
    my (@attr) = $self->_attributes($elem);
   
    my $xhtml=0; 
    foreach my $a (@attr) {
      if ($a eq '/="/"'){
         $xhtml=1;
      }else{
         ($accu, $ai, $pos, $bp, $bpi) = 
         $self->_add2accu($a,$cin,$accu,$pos,$ai,$bp,$bpi,1);
      }
    }
    
    # close start tag
    $wsp = 0;
    if ( $self->wrap_at_tagend == ALWAYS || 
	 @attr && $self->wrap_at_tagend == AFTER_ATTR) {
      # if breakpoint at end of the start tag
      $bp = $pos;
      $bpi = $cin;
    }
    $accu .= ($xhtml==0)?'>':' />';
    $pos++;
    
    $req_nl = $self->nl_inside($elem);

    # CONTENT
    foreach my $c ($elem->content_list()) {
      if (ref $c) {
	# ELEMENT => recursive call
	($accu, $pos, $nl, $req_nl, $wsp, $ai, $bp, $bpi) 
	  = $self->_format($c,$cin,$accu,$pos,$nl,$wsp,$ai,$bp,$bpi);
      }
      else {
	# TEXT
	if ($req_nl && substr($c,0,1) eq ' ') {
	  # starts with white space => can insert requested newlines
	  $self->_add_line($accu,$ai);
	  $self->_add_lines((' ') x ($req_nl -1)) if $req_nl> 1;
	  $accu = '';
	  $ai = $cin;
	  $pos = 0;
	  $bp = 0;
	  $nl = $req_nl;
	}
	encode_entities($c,$self->entities);
	my @words = split(/\s/,$c);
	foreach my $w (@words) {
	  ($accu, $ai, $pos, $bp, $bpi) = 
	    $self->_add2accu($w,$cin,$accu,$pos,$ai,$bp,$bpi,$wsp);
	  $wsp = 1; # add whitespace after word
	} # foreach word
	$nl = 0 if $pos;
	$wsp = (substr($c,-1,1) eq ' '); # whitespace at end of text segment?
      }  # else TEXT
    }   # foreach content

    # NEWLINES BEFORE END TAG
    my $rqnl = $self->nl_inside($elem);
    $req_nl = $rqnl if $rqnl > $req_nl;
    $req_nl -= $nl;

    # END TAG
    $ai = $indent unless $pos; # use indent outside element for end tag
    unless ($HTML::Element::emptyElement{$tag} ||
	    ($HTML::Element::optionalEndTag{$tag} && !$self->endtag($elem))) {
      # if endtag required
      if ($req_nl > 0 && $wsp) {
	# if new lines required before endtag
	$self->_add_line($accu,$ai);
	$accu = '';
	$pos = 0;
	$bp = 0;
	$ai = $indent;
	$self->_add_lines((' ') x ($req_nl-1)) if $req_nl-1;
	$req_nl = 0;
      }

      my $etstr = $self->uppercase? "</\U$tag>" : "</$tag>";
      ($accu, $ai, $pos, $bp, $bpi) = 
	$self->_add2accu($etstr,$indent,$accu,$pos,$ai,$bp,$bpi,$wsp);
      $req_nl = 0;
      $nl = 0;
      $wsp = 0;
    } 
  } # else formating by HTML::PrettyPrinter
  
  # NEWLINES AFTER ELEMENT
  my $rqnl = $self->nl_after($elem);
  $req_nl = $rqnl if $rqnl > $req_nl;
  if ($req_nl && $self->allow_forced_nl() && $self->force_nl($elem)) {
    # force newlines
    $self->_add_line($accu,$ai);
    $self->_add_lines((' ') x ($req_nl -1));
    $accu = '';
    $ai = $indent;
    $pos = 0;
    $bp = 0;
    $nl = $req_nl;
    $req_nl = 0;
  }
  return ($accu, $pos, $nl, $req_nl, $wsp, $ai, $bp, $bpi);
}

1;

__END__

はてなダイアリーにファイルのアップロードが無かったので直書き…
長ったらしく書いてますが、_formatメソッドに2、3行追加してるだけです。*5
これでほぼ理想通りになった!


あー長かった。
これでやっと本来の勉強(FlexとかJava)が出来そうだ〜

*1:HTML::Tidyのバグ?http://www.perlmonks.org/?node_id=674251

*2:参考:http://html.idena.jp/gui.html

*3:参考:http://crusherfactory.net/~yas_/000155.php

*4:cpanモジュールの初パッチ。ドキドキ

*5:$xhtmlフラグの部分