-2
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>
kovshenin
  • 31,813
  • 4
  • 35
  • 46
James
  • 42,081
  • 53
  • 136
  • 161
  • 3
    Where this list is coming from? – fabrik Oct 18 '10 at 10:54
  • 3
    @Happy: Let me clearify @fabrik's question: Where does this string come from? We already knew about `$ul` since it is provided in your question. – jwueller Oct 18 '10 at 10:58
  • 2
    *(related)* [Best Methods to parse HTML](http://stackoverflow.com/questions/3577641/best-methods-to-parse-html/3577662#3577662) – Gordon Oct 18 '10 at 11:12
  • 1
    @Happy: Let me clearify my clarification of @fabrik's question: Did _you_ generate that markup, or do you recieve this string from a certain third-party script? – jwueller Oct 18 '10 at 11:16
  • @elusive, that markup is generated by core function. Now we have $ul, with code inside (
      ...
    ). What's the problem?
    – James Oct 18 '10 at 11:21
  • @Happy I think what they are trying to find out is if your *core* function can add this class when it's run, instead of reparsing the entire markup *after* it's run. – Gordon Oct 18 '10 at 11:23
  • @Gordon, it can't. That's why I have to use regex like solution. – James Oct 18 '10 at 11:25
  • @Happy: The problem is, that manipulating an HTML-string is really not what you want to do when there is an easier way. @fabrik's question can be read like this: _"Can't we add that class while the rest of the HTML is generated?"_ Since adding this afterwards is only possible by knowing every possible combination (we usually use DOM or SAX parsers here). This is quite complicated and and thus not really recommendedable to do such things. They are the only clean way, though. It would be much easier and faster to add this beforehand. It's ok if you can't but that is what he asked. – jwueller Oct 18 '10 at 11:33
  • 1
    possible duplicate of [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 Oct 18 '10 at 12:07
  • @Gordon, there is no accepted solution in that topic. – James Oct 18 '10 at 18:01
  • @Happy After you changed the title and asked for both first and last element and a Regex, your question is definitely a duplicate now. There is plenty Regex solutions to pick from in the linked question. – Gordon Oct 22 '10 at 12:44
  • Same question - why regex? The DOM solution mentioned below will work fine. Plus, you might also want to try phpQuery (http://code.google.com/p/phpquery/) which gives you the ability to use CSS3 selectors inside your php code, very similar to what you do in jQuery (hence the name). – kovshenin Oct 29 '10 at 08:39

5 Answers5

37

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
  • 312,688
  • 75
  • 539
  • 559
  • 5
    +1! Nice! This is the most clean way i can think of (server-side). – jwueller Oct 18 '10 at 11:19
  • Thanks, but where did you get html/body? There is only
      ..
    code inside $ul.
    – James Oct 18 '10 at 11:23
  • 3
    @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 Oct 18 '10 at 11:28
  • This example doesn't work. On saveXML it gives code with head/> – James Oct 22 '10 at 12:05
  • @Happy No, it doesnt. It gives the outerHTML of the UL – Gordon Oct 22 '10 at 12:15
  • I've tryed echo $dom->saveXml($dom->getElementById( 'menu' )), and it gives code with head/> – James Oct 23 '10 at 18:40
  • @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 Oct 23 '10 at 21:26
10

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
  • 7,273
  • 2
  • 30
  • 35
4

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
  • 5,108
  • 1
  • 25
  • 38
  • 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 Oct 22 '10 at 12:47
3

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>
Toto
  • 89,455
  • 62
  • 89
  • 125
3

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 {
}
Wouter Dorgelo
  • 11,770
  • 11
  • 62
  • 80
  • 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 Oct 28 '10 at 03:16