結局以下のようになりました

while(/^(.+?)$/gmo)
{
  my $line = $1;
  $nest++ if($line =~ /<[^/][^>]+>/);
  if($nest){
    $result .= "$line
";
  }else{
    $result .= "<p>$line</p>
";
  }
  $nest-- if($line =~ /</[^>]+>/);
}

PHPのコードに直すと、こんな感じか

    $nest = 0;
    $lines= explode("
", $text);
    $text = "";
    foreach($lines as $line)
    {
      if(preg_match("/<[^/][^>]+>/", $line)) $nest++;
      if($nest){
        $text .= "$line
";
      }else{
        $text .= "<p>$line</p>
";
      }
      if(preg_match("/</[^>]+>/", $line)) $nest--;
    }

PHPではm/〜/gに相当する物がないんですね、厄介です。
もちろんこれでやると、インラインタグが含まれる行もpで囲めないという問題に陥ってしまうので、インラインタグはないものとして処理しています。ほんとは<[^/][^>]+>あたりにブロックタグを列挙した方がいいんでしょうけどね。
また、おそらく、imgタグ、inputタグなどのいわゆるエンプティタグに出くわすと上手く動きません。まだまだ発展途上です。まあ、ブロックタグには幸いそういうタグはないので、この前提で処理している限り問題は起こらないと言えますが…。


ところで、はてなで使われている、Text::Hatenaモジュールは、一行ごとに処理をしているらしいです。text-hatena.js 公開 [てっく煮]で、text-hatena.jsというText::HatenaのJavaScript移植版が配布されています。これを見た限りだと、一行ずつ処理してやっているみたい。だから、一定領域にpタグを挿入しない なんてことも簡単に出来ます(というか恐らく、他の記法の中に段落タグを挿入することは出来ない)。
結局のところ、これははてなみたいなやり方が正しいのかもしれないなあ。上の正規表現なんかも結局行単位のスキャンになってるし。


行単位 か…。もうちょっと工夫すれば、今のものを使って何とか出来るかな。もう少し頑張ってみよう。
いいや、これじゃダメです。<%media%>なんかに反応してタグを壊してくれますね。