2013-04-07 65 views
2

我一直在努力爲我正在開發的HTML標記語言的基本文本創建解析器。內聯元素標記如下。使用正則表達式和DOMDocument遞歸處理標記

{*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) 

然後遍歷和$dom->createTextNode()$dom->createElement() + $dom->appendChild($dom->createTextNode())。不幸的是,這在嵌套標記時不起作用。

我只是難以遞歸地將我的標記處理成DOMDocument。我一直在閱讀,我需要編寫一個解析器,但無法找到適合的教程或代碼示例,尤其是在將它與使用DOMDocument創建元素和文本節點時相結合的情況下。

+0

你會發現很難用正則表達式解決很多這些問題。正則表達式不是用這種方式解析文檔的 - 你要找的是一個真正的解析器,可能就像[this](http://lime-php.sourceforge.net/) – 2013-04-07 03:03:32

回答

6

嵌套或遞歸結構通常超出解析的正則表達式的力量,並且您通常需要更強大的解析器。問題是你需要找到下一個令牌,這取決於先前的令牌,這不是正則表達式可以處理的(語言不再是常規的)。

但是,對於這樣一種簡單的語言,您不需要一個完整的具有正式語法的解析器生成器 - 您可以輕鬆地手動編寫簡單的解析器。你只有一個重要的狀態位 - 最後打開的標記。如果您的正則表達式與文本,新的打開標記或當前打開標記的相應關閉標記相匹配,則可以處理此任務。規則是:

  1. 如果您匹配文本,保存文本並繼續匹配。
  2. 如果您匹配打開的標籤,請保存打開的標籤,然後繼續匹配,直到找到打開的標籤或相應的關閉標籤。
  3. 如果您匹配關閉標籤,請停止查找當前打開的標籤並繼續匹配最後未關閉的標籤,文本或其他打開的標籤。

第二步是遞歸 - 每當您找到一個新的打開標記時,就會創建一個新的匹配上下文來查找相應的關閉標記。

這不是必需的,但通常解析器將生成一個簡單的樹結構來表示解析的文本 - 這被稱爲抽象語法樹。在生成語法所代表的內容之前,最好先生成一個語法樹。這使您可以靈活地操作樹或生成不同的輸出(例如,您可以輸出xml以外的內容)。

這是一個將這兩個想法結合起來並解析文本的解決方案。 (它還承認{{}}爲轉義序列即:單一字面{}。)

首先解析器:

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,然後將其附加到所需的容器元素。

+0

哇!今天就試圖理解這一點。一個問題,我如何指定以不同符號結尾的開始/結束標記,例如'{>小的<}或'{0} {?}? – esryl 2013-04-07 08:56:35

+0

'$ opentag'參數包含要查找的符號,因此當您進行遞歸調用時,只需告訴它要查找的內容即可。 – 2013-04-07 19:32:46

+0

嘿弗朗西斯,你的代碼非常好,我從它學到了很多樂趣,我不得不要求額外的問題來收緊語法匹配:http://stackoverflow.com/questions/15934295/parsing-markup-成抽象語法樹,使用正則表達式 – esryl 2013-04-10 19:30:10

1

如果你有一個正則表達式可以捕捉最外層的開放/關閉對的內容,你可以將這些捕獲的內容包裝到等價的HTML標籤中,然後通過重複相同的正則表達式(這將捕獲第二個到最外面的一對的內容)等等。

這種方法的問題是,如果/當一個開放的「標籤」沒有正確關閉,整個內容將會丟失,然後您無法緩存。

更可靠的方法可能是從頭到尾解析文本,當遇到開始標記時,將其添加到堆棧中並將其位置添加到堆棧中。無論何時遇到結束標記,如果它與堆棧頂部的開始標記不匹配,或者如果匹配,則用當前HTML結束標記替換當前結束標記,並從結束標記中彈出開始標記堆棧(並將其替換爲記錄位置處的等效開放HTML標籤)。

一個簡單的算法,分析可能是找到你的開啓或關閉標籤的第一個實例(例如,使用此正則表達式(\{[-*/>|])|(\}[-*/<|])),然後按上述處理,然後重複從當前位置是搜索,找到下一個標籤,等等...