views:

392

answers:

5
echo $ul; // gives this code:

<ul id="menu">
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class">...</li>
</ul>

How to add some class for the first and the last <li>?

Need a regex solution.

echo $ul; should give (if we add class my_class for the last <li>):

<ul id="menu">
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class my_class">...</li>
</ul>
+7  A: 

Alternatively, you could use "#menu li:last-child" in your CSS instead of a class name, that way you don't have to modify your PHP code.

wildpeaks
You should mention that this is CSS3 and incompatible/ignored in older browsers. I would do this kind of stuff via JavaScript.
elusive
This is the option I was thinking of +1
Robimp
+26  A: 

The DOM solution

$dom = new DOMDocument;
$dom->loadHTML( $ul );
$xPath = new DOMXPath( $dom );
$xPath->query( '/html/body/ul/li[last()]/@class' )
      ->item( 0 )
      ->value .= ' myClass';

echo $dom->saveXml( $dom->getElementById( 'menu' ) );

If you know the HTML to be valid, you can also use loadXML instead. That would make DOM not add ther HTML skeleton. Note that you have to change the XPath to '/ul/li[last()]/@class' then.

In case you are not familiar with XPath queries, you can also use the regular DOM interface, e.g.

$dom = new DOMDocument;
$dom->loadHTML( $ul );
$liElements = $dom->getElementsByTagName( 'li' );
$lastLi  = $liElements->item( $liElements->length-1 );
$classes = $lastLi->getAttribute( 'class' ) . ' myClass';
$lastLi->setAttribute( 'class',  $classes );
echo $dom->saveXml( $dom->getElementById( 'menu' ) );

EDIT Since you changed the question to have classes for first and last now, here is how to do that using XPath. This assumes your markup is valid XHTML. If not, switch back to loadHTML (see code above):

$dom = new DOMDocument;
$dom->loadXML( $html );
$xpath = new DOMXPath( $dom );
$first = $xpath->query( '/ul/li[1]/@class' )->item( 0 );
$last = $xpath->query( '/ul/li[last()]/@class' )->item( 0 );
$last->value .= ' last';
$first->value .= ' first';
echo $dom->saveXML( $dom->documentElement );
Gordon
+1! Nice! This is the most clean way i can think of (server-side).
elusive
Thanks, but where did you get html/body? There is only <ul>..</ul> code inside $ul.
Happy
@Happy Like it says in the answer: if you use `loadHTML` to load the markup, DOM will switch to it's HTML parser module, which will add the HTML skeleton, e.g. doctype, html, body elements. You want `loadHTML` if your HTML is not valid XHTML. If you've got valid markup, you can use `loadXML` instead.
Gordon
This example doesn't work. On saveXML it gives code with head/><body
Happy
@Happy No, it doesnt. It gives the outerHTML of the UL
Gordon
I've tryed echo $dom->saveXml($dom->getElementById( 'menu' )), and it gives code with head/><body
Happy
@Happy see the [documentation for `saveXML()` please](http://de.php.net/manual/en/domdocument.savexml.php). Passing a node to saveXML outputs only the specific node.
Gordon
+2  A: 

If you MUST use regex for this(not exactly to be advised). I think this should work...

$replacement1 = "<li\s.*?class="(.*?)".*?>.*?</li>\s</ul>";
$string1 = "$1 class_last";
$ul = preg_replace($ul, $replacement1, $string1);)

$replacement2 = "<ul.*?>\s<li\s.*?class="(.*?)".*?>";
$string2 = "$1 class_first";
$ul = preg_replace($ul, $replacement2, $string2);)
red-X
Also see question I closevoted this one for: [Need a regex to add css class to first and last list item ](http://stackoverflow.com/questions/328623/need-a-regex-to-add-css-class-to-first-and-last-list-item)
Gordon
+2  A: 

If you really want to do that job with regex, you could try :

$ul = '
<ul id="menu">
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class">...</li>
</ul>
';

// explode input string to an array
$lines = explode("\n", $ul);

$found_first = 0;   // is the first li founded
$found_last  = 0;   // index of the last li
$class_first = "class_first"; // class for the first li
$class_last  = "class_last";  // class for the last li

// loop on all lines
for ($i = 0; $i < count($lines); $i++) {
  $line = $lines[$i];

  // the line begins with <li
  if (preg_match("/^<li/", $line)) {

    // is it the first one
    if (!$found_first) {
      // add the class
      $lines[$i] = preg_replace('/ class="([^"]+?)"/', " class=\"$1 $class_first\"", $lines[$i]);
      // the first li have been found
      $found_first = 1;
    }
    // memo the last line proceded
    $found_last = $i;
  }
}

// this will add class_last even if the last li
// is also the first one (ie: only one li)
if ($found_last) {
  $lines[$found_last] = preg_replace('/ class="([^"]+?)"/', " class=\"$1 $class_last\"", $lines[$found_last]);
}

$ul = implode("\n", $lines);

echo $ul;

Ouput:

<ul id="menu">
<li id="some_id" class="some_class class_first">...</li>
<li id="some_id" class="some_class">...</li>
<li id="some_id" class="some_class class_last">...</li>
</ul>
M42
+1  A: 

You can use counters:

<?php
$list = array('aaa', 'bbb', 'ccc', 'ddd');
$items = count($list); // count items in list
$i = 1; // set counter to one, because first item in list will be item number: 1

echo '<ul>';

// create loop
foreach($list as $value) {

  // first item
  if($i == 1) {
    $class = 'some_class first_class';

  // last item
  } elseif ($i == $items) {
    $class = 'some_class last_class';

  // not first / not last item
  } else {
    $class = 'some_class';
  }

  echo '<li id="some_id" class="'.  $class .'">' . $value . '</li>';
  $i++; // raise $i by one
}

echo '</ul>';
?>

Will output:

<ul>
<li id="some_id" class="some_class first_class">aaa</li>
<li id="some_id" class="some_class">bbb</li>
<li id="some_id" class="some_class">ccc</li>
<li id="some_id" class="some_class last_class">ddd</li>
</ul>

However, my suggestion would be:

<ul id="menu">
  <li>aaa</li>
  <li>bbb</li>
  <li>ccc</li>
  <li>ddd</li>
</ul>

Within your css:

#menu {
}

#menu li:first-child {
}

#menu li {
}

#menu li:last-child {
}
Enrico Pallazzo
CSS3 is not a solution I would go with if he can change it before outputting. Plus, his `$ul` has already build the list elements, he needs, therefore he can't loop over like you mentioned. He only has a string.
Jason