Ваш оригинальный код не так уж далеко. Однако вам нужно, чтобы регулярное выражение также соответствовало тексту, который вы хотите добавить (и для этого вам нужен текстовый узел). После каждого матча вам также нужно сдвигать смещение, где продолжать матч:
$str = "hi there! [1], how are you? [2]";
$dom = new DOMDocument('1.0');
$root= $dom->createElement('sentence', null);
$root= $dom->appendChild($root);
$root->setAttribute('attr-1', 'value-1'); # ...
$counter = 0;
$offset = 0;
while ($pos = preg_match('/(.*?)\[([1-9][0-9]*)\]/', $str, $matches, NULL, $offset)) {
list(, $text, $number) = $matches;
if (strlen($text)) {
$root->appendChild($dom->createTextNode($text));
}
if (strlen($number)) {
$counter++;
$root->appendChild($dom->createElement("child$counter", $number));
}
$offset += strlen($matches[0]);
}
Петля while
сравнима с if
, которую вы имели, просто превращая ее в петлю. Кроме того, текстовые узлы добавляются, если какой-либо текст соответствует (например, в вашей строке может быть [1] [2], поэтому текст будет пустым. Вывод этого примера:
<?xml version="1.0"?>
<sentence attr-1="value-1">
hi there! <child1>1</child1>, how are you? <child2>2</child2>
</sentence>
Редактировать Поработав немного, я пришел к выводу, что вы можете разделить проблему. Одна часть - это анализ строки, а другая - фактическая вставка узлов (например, textnode для текста и elementnode, если это число). Начиная сзади, это сразу выглядит практичным, вторая часть первая:
$dom = new DOMDocument('1.0');
$root = $dom->createElement('sentence', null);
$root = $dom->appendChild($root);
$root->setAttribute('attr-1', 'value-1'); # ...
$str = "hi there! [1], how are you? [2] test";
$it = new Tokenizer($str);
$counter = 0;
foreach ($it as $type => $string) {
switch ($type) {
case Tokenizer::TEXT:
$root->appendChild($dom->createTextNode($string));
break;
case Tokenizer::NUMBER:
$counter++;
$root->appendChild($dom->createElement("child$counter", $string));
break;
default:
throw new Exception(sprintf('Invalid type %s.', $type));
}
}
echo $dom->saveXML();
В этом примере мы вообще не заботимся о разборе. Мы получаем либо текст, либо число ($type
), и мы можем принять решение о вставке текстового узла или элемента. Таким образом, как бы разбор строки не выполнялся, этот код всегда будет работать. Если с ним возникнет проблема (например, $counter
больше не будет интересен), это не будет иметь ничего общего с разбором / токенизацией строки.
Сам синтаксический анализ был заключен в Iterator
, называемый Tokenizer
. Он содержит все, чтобы разбить строку на текстовые и числовые элементы. Он имеет дело со всеми деталями, например, что происходит, если после последнего числа есть текст и так далее:
class Tokenizer implements Iterator
{
const TEXT = 1;
const NUMBER = 2;
private $offset;
private $string;
private $fetched;
public function __construct($string)
{
$this->string = $string;
}
public function rewind()
{
$this->offset = 0;
$this->fetch();
}
private function fetch()
{
if ($this->offset >= strlen($this->string)) {
return;
}
$result = preg_match('/\[([1-9][0-9]*)\]/', $this->string, $matches, PREG_OFFSET_CAPTURE, $this->offset);
if (!$result) {
$this->fetched[] = array(self::TEXT, substr($this->string, $this->offset));
$this->offset = strlen($this->string);
return;
}
$pos = $matches[0][1];
if ($pos != $this->offset) {
$this->fetched[] = array(self::TEXT, substr($this->string, $this->offset, $pos - $this->offset));
}
$this->fetched[] = array(self::NUMBER, $matches[1][0]);
$this->offset = $pos + strlen($matches[0][0]);
}
public function current()
{
list(, $current) = current($this->fetched);
return $current;
}
public function key()
{
list($key) = current($this->fetched);
return $key;
}
public function next()
{
array_shift($this->fetched);
if (!$this->fetched) $this->fetch();
}
public function valid()
{
return (bool)$this->fetched;
}
}
Это сделало разделение двух проблем друг от друга. Вместо класса итератора можно также создать массив массивов или аналогичный, но я нашел итератор более полезным, поэтому быстро написал один.
Опять этот пример выводит XML в конце, так что здесь это пример. Обратите внимание, что я добавил текст после последнего элемента:
<?xml version="1.0"?>
<sentence attr-1="value-1">
hi there! <child1>1</child1>, how are you? <child2>2</child2> test
</sentence>