我一直在努力为我正在开发的基本文本到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)
然后穿过和$dom->createTextNode()
或$dom->createElement()
+ $dom->appendChild($dom->createTextNode())
。不幸的是,这对嵌套标记没有帮助。
我只是在递归地将标记处理到DOMDocument的有效方法上遇到了困难。我一直在阅读,我需要写一个解析器,但找不到一个合适的教程或代码示例,我可以遵循,特别是当集成它与元素和文本节点创建使用DOMDocument。
嵌套结构或递归结构通常超出正则表达式的解析能力,您通常需要更强大的解析器。问题在于,您需要查找的下一个标记取决于之前的标记,这不是正则表达式可以处理的(语言不再是正则的)。
然而,对于这样一个简单的语言,您不需要一个具有正式语法的完整的解析器生成器——您可以轻松地手工编写一个简单的解析器。只有一个状态位是重要的——最后一个打开的标记。如果正则表达式匹配文本、新的打开标记或与当前打开标记对应的关闭标记,则可以处理此任务。规则是:
- 如果你匹配文本,保存文本并继续匹配。
- 如果匹配一个开始标签,保存开始标签,并继续匹配,直到找到一个开始标签或相应的结束标签。
- 如果你匹配了一个关闭标签,停止寻找当前打开的标签,继续匹配最后一个未关闭的标签、文本或另一个打开的标签。
第二步是递归的——当你发现一个新的开始标记时,你创建一个新的匹配上下文来寻找相应的结束标记。
这不是必需的,但通常解析器会生成一个简单的树结构来表示解析后的文本——这被称为抽象语法树。在生成语法表示的内容之前,通常最好先生成语法树。这使您可以灵活地操作树或生成不同的输出(例如,您可以输出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
,然后将其附加到你想要的容器元素。
如果您有一个捕获最外层的打开/关闭对的内容的正则表达式,那么您可以将捕获的内容包装在等效的HTML标记中,然后通过重复相同的正则表达式递归到新字符串中(这将捕获第二个最外层对的内容),依此类推。
这种方法的问题是,如果/当开始的"标记"没有正确关闭时,整个内容将丢失,然后您无法递归到它。
更可靠的方法可能是从头到尾解析文本,当遇到开始标记时,将其及其位置添加到堆栈中。无论何时遇到结束标记,如果它与堆栈顶部的开始标记不匹配,则忽略它,或者如果它匹配,则用等效的HTML结束标记替换当前结束标记,并从堆栈中弹出开始标记(并用记录位置的等效HTML开始标记替换它)。
一个简单的解析算法可能是找到你的开始或结束标签的第一个实例(例如使用这个正则表达式('{[-*/>|])|('}[-*/<|])
),然后如上所述处理,然后从当前位置重复搜索以找到下一个标签,等等…