tags:

views:

238

answers:

4

In the string below, I want to replace <!--more--> with some text, FOOBAR, then truncate the string.

<p>The quick <a href="/">brown</a> fox jumps <!--more-->
over the <a href="/">lazy</a> dog.</p>

I've got to this point:

<p>The quick <a href="/">brown</a> fox jumps FOOBAR

... but as you can see, the <p> tag is not closed. Any ideas on how I could consistently balance the tags? I am pretty new to PHP.

The array I am working with looks like this:

array(2) {
  [0]=>
  string(50) "<p>The quick <a href="/">brown</a> fox jumps "
  [1]=>
  string(45) " over the <a href="/">lazy</a> dog.</p>"
}
A: 

As you state the problem, it's as easy as this:

str_replace('<!--more-->', 'FOOBAR', $original_text);

Perhaps if you update your question to explain what has an array to do with the whole problem will aid in interpreting the right question - (is the string <!--more--> supposed to be in the array?)

Seb
He wants to replace the text, then remove (truncate) everything after the replace, yet still preserve any closing tags.
Tim Lytle
Yeah, I added that truncate bit a minute after posting. Sorry. I have a feeling this is a "loaded" question, and it's more complicated than I originally thought because of all the possible tag combinations.
Jeff
+2  A: 

If possible, I would suggest parsing the HTML into a DOM and dealing with it that way, walking through text nodes until you find that string, then truncating the text node and deleting any further child nodes after that one (leaving the parent intact). Then re-serialize the DOM to HTML.

qid
Okay, as I said, I'm pretty new to PHP. How do I parse the HTML into a DOM (which php function)? Then I'm good until "re-serializing the DOM to HTML." Lol
Jeff
I'd really like to learn more about this comment. Am I on the right track at this page: http://php.net/manual/en/book.dom.php ?
Jeff
A: 

You would have to find all tags opened, but not closed, before the placeholder text. Insert the new text like you do now, and then close the tags afterwards.

Here is a sloppy example. I think this code will work with all valid HTML, but I'm not positive. And it will certainly accept invalid markup. but anyway:

$h = '<p>The quick <a href="/">brown</a> fox jumps <!--more-->
over the <a href="/">lazy</a> dog.</p>';

$parts = explode("<!--more-->", $h, 2);
$front = $parts[0];

/* Find all opened tags in the front string */
$tags = array();
preg_match_all("|<([a-z][\w]*)(?: +\w*=\"[\\w/%&=]+\")*>|i", $front, $tags, PREG_OFFSET_CAPTURE);
array_shift($tags); /* get rid of the complete match from preg_match_all */

/* Check if the opened arrays have been closed in the front string */
$unclosed = array();
foreach($tags as $t) {
    list($tag, $pos) = $t[0];
    if(strpos($front, "</".$tag, $pos) == false) {
     $unclosed[] = $tag;
    }
}    

/* Print the start, the replacement, and then close any open tags. */
echo $front;
echo "FOOBAR";
foreach($unclosed as $tag) {
    echo "</".$tag.">";
}

Outputs

<p>The quick <a href="/">brown</a> fox jumps FOOBAR</p>
gnud
+1  A: 

I haven't fully tested this yet, but it works for your example at least. Assumes well-formed XML.

<?php
$reader = new XMLReader;
$writer = new XMLWriter;

// load the XML string into the XMLReader
$reader->xml('<p>The quick <a href="/">brown</a> fox jumps <!--more--> over the <a href="/">lazy</a> dog.</p>');
// write the new XML to memory
$writer->openMemory();
$done = false;

// XMLReader::read() moves the current read location to the next node
while ( !$done && $reader->read()) {
    // choose action based on the node type
    switch ($reader->nodeType) {
        case XMLReader::ELEMENT:
            // read an element, so write it back to the output
            $writer->startElement($reader->name);
            if ($reader->hasAttributes) {
                // loop through all attributes and write them
                while($reader->moveToNextAttribute()) {
                    $writer->writeAttribute($reader->name, $reader->value);
                }
                // move back to the beginning of the element
                $reader->moveToElement();
            }
            // if the tag is empty, close it now
            if ($reader->isEmptyElement) {
                $writer->endElement();
            }
            break;
        case XMLReader::END_ELEMENT:
            $writer->endElement();
            break;
        case XMLReader::TEXT:
            $writer->text($reader->value);
            break;
        case XMLReader::COMMENT:
            // you  can change this to be more flexible if you need
            // e.g. preg_match, trim, etc.
            if (trim($reader->value) == 'more') {

                // write whatever you want in here. If you have xml text
                // you want to write verbatim, use writeRaw() instead of text()
                $writer->text('FOOBAR');

                // this is where the magic happens -- endDocument closes
                // any remaining open tags
                $writer->endDocument();
                // stop the loop (could use "break 2", but that gets confusing
                $done = true;
            }
            break;
    }
}
echo $writer->outputMemory();
mcrumley
Haven't tried this yet, but will in a minute. Is your code above similar to http://php.net/manual/en/book.dom.php? I haven't worked with classes yet, but I've been staring at that page for a while now, trying to figure it out.
Jeff
You could accomplish the same thing with the DOM objects, but I find XMLReader/XMLWriter easier to use for simple filters like this. DOMDocument works well for more complex manipulation like moving whole node trees around.
mcrumley
Thanks mcrumley. Wondering if you'd mind adding comments to some of the code to help me understand it. As it is, I can see I'd need to make a modification to the 'more' value because mine is actually a little more complicated than that.
Jeff
Great comments, Thanks! What kind of server overhead would this have versus a script like this: http://snipplr.com/view/3618/close-tags-in-a-htmlsnippet/ ? I only ask because this function will be called on every page load, typically a dozen times each.
Jeff
Sorry for the late response, I was out of town for the weekend. The timing for the two algorithms mostly depends on how complex the input is and where the break occurs. In the tests I ran, performance was about the same for what I would consider typical input. Your mileage may vary. In any case neither took more than .03 seconds to run 100 times.
mcrumley