ネストされた構造または再帰的な構造は通常、正規表現の解析能力を超えており、通常はより強力なパーサーが必要です。問題は、見つける必要がある次のトークンが以前のトークンに応じて変化することです。これは、正規表現で処理できるものではありません (言語はもはや正規表現ではありません)。
ただし、このような単純な言語の場合、正式な文法を備えた本格的なパーサー ジェネレーターは必要ありません。単純なパーサーは手動で簡単に作成できます。重要な状態は、最後に開いたタグの 1 つだけです。テキスト、新しい開始タグ、または現在の開始タグに対応する終了タグに一致する正規表現がある場合は、このタスクを処理できます。ルールは次のとおりです。
- テキストを照合する場合は、テキストを保存して照合を続行します。
- 開始タグに一致する場合は、開始タグを保存し、開始タグまたは対応する終了タグが見つかるまで一致を続けます。
- 終了タグに一致する場合は、現在開いているタグの検索を停止し、最後に閉じられていないタグ、テキスト、または別の開いているタグの一致を続行します。
ステップ 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