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. 如果匹配文本,请保存文本并继续匹配。
  2. 如果匹配打开标签,请保存打开标签,然后继续匹配,直到找到打开标签或相应的关闭标签。
  3. 如果您匹配一个关闭标签,请停止查找当前打开的标签并继续匹配最后一个未关闭的标签、文本或另一个打开的标签。

第二步是递归的——每当你找到一个新的打开标签时,你就创建一个新的匹配上下文来寻找相应的关闭标签。

这不是必需的,但通常解析器会生成一个简单的树结构来表示已解析的文本——这被称为抽象语法树。在生成语法所代表的内容之前,通常最好先生成语法树。这使您可以灵活地操作树或产生不同的输出(例如,您可以输出 xml 以外的其他内容。)

这是一个结合了这些想法并解析您的文本的解决方案。(它也将{{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>

如果您已经有 aDOMDocument并且想要向其中添加一些已解析的文本,我建议您创建 aDOMDocumentFragment并将其ast_to_dom直接传递给,然后将其附加到所需的容器元素中。

于 2013-04-07T07:12:43.617 回答
1

如果您有一个捕获最外层打开/关闭对的内容的正则表达式,则可以将该捕获的内容包装在等效的 HTML 标记中,然后通过重复相同的正则表达式递归到该新字符串(这将捕获倒数第二对),依此类推。

这种方法的问题是,如果/当一个打开的“标签”没有正确关闭时,整个内容都会丢失,然后你就不能递归进去。

更可靠的方法可能是从头到尾解析文本,当遇到开始标签时,将其及其位置添加到堆栈中。每当遇到结束标记时,如果它与堆栈顶部的开始标记不匹配,则将其忽略,或者如果匹配,则将当前结束标记替换为等效的 HTML 结束标记,并从堆栈顶部弹出开始标记堆栈(并将其替换为记录位置的等效打开 HTML 标记)。

一个简单的解析算法可能是找到你的开始或结束标签的第一个实例(例如使用这个正则表达式(\{[-*/>|])|(\}[-*/<|])),然后按上面的方法处理,然后从当前位置重复搜索以找到下一个标签,等等......

于 2013-04-07T02:50:37.973 回答