2

私は開発中の基本的なテキストから HTML マークアップ言語へのパーサーを作成するのに苦労しています。インライン要素のマークアップは次のとおりです。

{*strong*}
{/emphasis/}
{-strikethrough-}
{>small<}
{|code|}

私がテストしているサンプル文字列は次のとおりです。

tëstïng 汉字/漢字 testing {*strông{/ëmphäsïs{-strïkë{|côdë|}-}/}*} {*wôw*} 1, 2, 3

を使用しpreg_splitて、これを次のように変換できます。

$split = preg_split('%(\{.(?:[^{}]+|(?R))+.\})%',
    $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);

array (size=5)
  0 => string 'tëstïng 汉字/漢字 testing ' (length=32)
  1 => string '{*strông{/ëmphäsïs{-strïkë{|côdë|}-}/}*}' (length=48)
  2 => string ' ' (length=1)
  3 => string '{*wôw*}' (length=8)
  4 => string ' 1, 2, 3' (length=8)

次に、 and$dom->createTextNode()または$dom->createElement()+をループします$dom->appendChild($dom->createTextNode())。残念ながら、マークアップがネストされている場合、これは役に立ちません。

マークアップを DOMDocument に再帰的に処理する効率的な方法に困惑しています。パーサーを作成する必要があることを読み続けていますが、特にDOMDocumentを使用して要素およびテキストノードの作成と統合する場合に、従うことができる適切なチュートリアルまたはコード例が見つかりません。

4

2 に答える 2

6

ネストされた構造または再帰的な構造は通常、正規表現の解析能力を超えており、通常はより強力なパーサーが必要です。問題は、見つける必要がある次のトークンが以前のトークンに応じて変化することです。これは、正規表現で処理できるものではありません (言語はもはや正規表現ではありません)。

ただし、このような単純な言語の場合、正式な文法を備えた本格的なパーサー ジェネレーターは必要ありません。単純なパーサーは手動で簡単に作成できます。重要な状態は、最後に開いたタグの 1 つだけです。テキスト、新しい開始タグ、または現在の開始タグに対応する終了タグに一致する正規表現がある場合は、このタスクを処理できます。ルールは次のとおりです。

  1. テキストを照合する場合は、テキストを保存して照合を続行します。
  2. 開始タグに一致する場合は、開始タグを保存し、開始タグまたは対応する終了タグが見つかるまで一致を続けます。
  3. 終了タグに一致する場合は、現在開いているタグの検索を停止し、最後に閉じられていないタグ、テキスト、または別の開いているタグの一致を続行します。

ステップ 2 は再帰的です。新しい開始タグが見つかるたびに、対応する終了タグを探す新しい一致コンテキストを作成します。

これは必須ではありませんが、一般に、パーサーは解析されたテキストを表す単純なツリー構造を生成します。これは抽象構文ツリーとして知られています。通常、構文が表すものを作成する前に、まず構文ツリーを作成することをお勧めします。これにより、ツリーを操作したり、さまざまな出力を生成したりできる柔軟性が得られます (たとえば、xml 以外のものを出力できます)。

これらの両方のアイデアを組み合わせてテキストを解析するソリューションを次に示します。(また、単一のリテラルorを意味するエスケープ シーケンスとして{{orも認識します。)}}{}

最初のパーサー:

class ParseError extends RuntimeException {}

function str_to_ast($s, $offset=0, $ast=array(), $opentag=null) {
    if ($opentag) {
        $qot = preg_quote($opentag, '%');
        $re_text_suppl = '[^{'.$qot.']|{{|'.$qot.'[^}]';
        $re_closetag = '|(?<closetag>'.$qot.'\})';
    } else {
        $re_text_suppl = '[^{]|{{';
        $re_closetag = '';
    }
    $re_next = '%
        (?:\{(?P<opentag>[^{\s]))  # match an open tag
              #which is "{" followed by anything other than whitespace or another "{"
        '.$re_closetag.'  # if we have an open tag, match the corresponding close tag, e.g. "-}"
        |(?P<text>(?:'.$re_text_suppl.')+) # match text
            # we allow non-matching close tags to act as text (no escape required)
            # you can change this to produce a parseError instead
        %ux';
    while ($offset < strlen($s)) {
        if (preg_match($re_next, $s, $m, PREG_OFFSET_CAPTURE, $offset)) {
            list($totalmatch, $offset) = $m[0];
            $offset += strlen($totalmatch);
            unset($totalmatch);
            if (isset($m['opentag']) && $m['opentag'][1] !== -1) {
                list($newopen, $_) = $m['opentag'];
                list($subast, $offset) = str_to_ast($s, $offset, array(), $newopen);
                $ast[] = array($newopen, $subast);
            } else if (isset($m['text']) && $m['text'][1] !== -1) {
                list($text, $_) = $m['text'];
                $ast[] = array(null, $text);
            } else if ($opentag && isset($m['closetag']) && $m['closetag'][1] !== -1) {
                return array($ast, $offset);
            } else {
                throw new ParseError("Bug in parser!");
            }
        } else {
            throw new ParseError("Could not parse past offset: $offset");
        }
    }
    return array($ast, $offset);
}

function parse($s) {
    list($ast, $offset) = str_to_ast($s);
    return $ast;
}

これにより、「ノード」のリストである抽象構文ツリーが生成されます。ここで、各ノードは、array(null, $string)テキストの形式の配列、またはarray('-', array(...))タグ内のもの (つまり、型コードとノードの別のリスト) です。

この木を手に入れたら、それを使って好きなことをすることができます。たとえば、DOM ツリーを生成するために再帰的にトラバースできます。

function ast_to_dom($ast, DOMNode $n = null) {
    if ($n === null) {
        $dd = new DOMDocument('1.0', 'utf-8');
        $dd->xmlStandalone = true;
        $n = $dd->createDocumentFragment();
    } else {
        $dd = $n->ownerDocument;
    }
    // Map of type codes to element names
    $typemap = array(
        '*' => 'strong',
        '/' => 'em',
        '-' => 's',
        '>' => 'small',
        '|' => 'code',
    );

    foreach ($ast as $astnode) {
        list($type, $data) = $astnode;
        if ($type===null) {
            $n->appendChild($dd->createTextNode($data));
        } else {
            $n->appendChild(ast_to_dom($data, $dd->createElement($typemap[$type])));
        }
    }
    return $n;
}

function ast_to_doc($ast) {
    $doc = new DOMDocument('1.0', 'utf-8');
    $doc->xmlStandalone = true;
    $root = $doc->createElement('body');
    $doc->appendChild($root);
    ast_to_dom($ast, $root);
    return $doc;
}

以下は、より難しいテスト ケースを含むテスト コードです。

$sample = "tëstïng 汉字/漢字 {{ testing -} {*strông 
    {/ëmphäsïs {-strïkë *}also strike-}/} also {|côdë|}
    strong *} {*wôw*} 1, 2, 3";
$ast = parse($sample);
echo ast_to_doc($ast)->saveXML();

これにより、次のように出力されます。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<body>tëstïng 汉字/漢字 {{ testing -} <strong>strông 
    <em>ëmphäsïs <s>strïkë *}also strike</s></em> also <code>côdë</code>
    strong </strong> <strong>wôw</strong> 1, 2, 3</body>

が既にあり、DOMDocumentそれに解析済みのテキストを追加したい場合は、 を作成して直接 にDOMDocumentFragment渡し、これを目的のコンテナ要素に追加することをお勧めします。ast_to_dom

于 2013-04-07T07:12:43.617 に答える
1

最も外側のオープン/クローズ ペアのコンテンツをキャプチャする正規表現がある場合は、そのキャプチャしたコンテンツを同等の HTML タグでラップし、同じ正規表現を繰り返してその新しい文字列に再帰することができます (これにより、最も外側から 2 番目のペア) など。

このアプローチの問題は、開始「タグ」が適切に閉じられていない場合、コンテンツ全体が失われ、再帰できないことです。

より信頼性の高いアプローチは、テキストを最初から最後まで解析し、開始タグに遭遇したら、それとその位置をスタックに追加することです。終了タグが検出されるたびに、それがスタックの一番上にある開始タグと一致しない場合は無視されます。一致する場合は、現在の終了タグを同等の HTML 終了タグに置き換えて、開始タグをスタックします (記録された位置にある同等の開始 HTML タグに置き換えます)。

解析のための単純なアルゴリズムは、開始タグまたは終了タグの最初のインスタンスを見つけて (たとえば、この正規表現を使用して(\{[-*/>|])|(\}[-*/<|]))、上記のように処理し、現在の位置からその検索を繰り返して次のタグを見つける、などです。

于 2013-04-07T02:50:37.973 に答える