TL;DR I'm looking for a function to create a nested <ol> list from a one-dimensional array in PHP.
1) Currently I have this simplified markup in my test page:
<h2>Spiders</h2>
<h2>Beetles</h2>
<h3>External morphology</h3>
<h4>Head</h4>
<h4>Thorax</h4>
<h4>Legs</h4>
<h3>Anatomy and physiology</h3>
<h2>Ants</h2>
2) Which then is captured by a pretty simple function into an one-dimensional array like this:
array
(
0 => "H2 Spiders",
1 => "H2 Beetles",
2 => "H3 External morphology",
3 => "H4 Head",
4 => "H4 Thorax",
5 => "H4 Legs",
6 => "H3 Anatomy and physiology"
7 => "H2 Ants"
);
3) This is the tricky part, because I use the next loop with these overly complicated if statements to populate a multi-dimensional array.
$toc = array ();
//
foreach ($array as $value) {
$value_arr = explode(' ', $value, 2);
$depth = str_replace("H", "", $value_arr[0]);
$content = $value_arr[1];
//
if ($depth == 1) $toc[$title] = null;
elseif ($depth == 2) {
if (empty (end ($toc))) $toc[array_key_last ($toc)] = array ($title => null);
else $toc[array_key_last ($toc)][$title] = null;
} elseif ($depth == 3) {
if (empty (end ($toc[array_key_last ($toc)]))) $toc[array_key_last ($toc)][array_key_last ($toc[array_key_last ($toc)])] = array ($title => null);
else $toc[array_key_last ($toc)][array_key_last ($toc[array_key_last ($toc)])][$title] = '';
}
}
Output:
Array (
[Spiders] =>
[Beetles] => Array
(
[External morphology] => Array
(
[Head] =>
[Thorax] =>
[Legs] =>
)
[Anatomy and physiology] =>
)
[Ants] =>
)
4) And finally be parsed with this function into an perfectly indented html list.
function table_of_contents ($toc, $output = '') {
foreach ($toc as $key => $value) {
$output = "$output <li><a href='##" . sanitize_title ($key) . "'>$key</a>" . (is_array ($value) ? table_of_contents ($value) : null) . '</li>';
}
//
return "<ol>$output</ol>";
}
//
table_of_contents ($toc);
-
Spiders Beetles External morphology Head Thorax Legs Anatomy and physiology Ants
Everything works fine in the 1st, 2nd and 4th steps, but my current approach has the drawback that only allows me up to three levels of depth from the first array in the 3rd step.
My question is whether there is a more efficient and cleaner way to create the multi-dimensional array with (maybe) a recursive function or something like that?
It might be easier to parse your input using preg_match_all; this can give you arrays of depths and associated values. You can then iterate through those arrays, opening an <ol> when the depth increases, and closing it when the depth decreases:
$html = '<h2>Spiders</h2>
<h2>Beetles</h2>
<h3>External morphology</h3>
<h4>Head</h4>
<h4>Thorax</h4>
<h4>Legs</h4>
<h3>Anatomy and physiology</h3>
<h2>Ants</h2>';
preg_match_all('/<h(\d)>([^<]+)/', $html, $matches);
$cdepth = $matches[1][0] - 1;
foreach ($matches[1] as $key => $depth) {
if ($depth > $cdepth) {
echo "\n" . str_repeat(' ', $cdepth * 4) . "<ol>\n";
}
elseif ($depth < $cdepth) {
echo "</li>\n" . str_repeat(' ', $depth * 4) . "</ol>\n" . str_repeat(' ', $depth * 4) . "</li>\n";
}
else {
echo "</li>\n";
}
$cdepth = $depth;
echo str_repeat(' ', $cdepth * 4) . "<li>{$matches[2][$key]}";
}
while ($cdepth-- >= $matches[1][0]) {
echo "</li>\n" . str_repeat(' ', $cdepth * 4) . "</ol>\n";
}
Output:
<ol>
<li>Spiders</li>
<li>Beetles
<ol>
<li>External morphology
<ol>
<li>Head</li>
<li>Thorax</li>
<li>Legs</li>
</ol>
</li>
<li>Anatomy and physiology</li>
</ol>
</li>
<li>Ants</li>
</ol>
Demo on 3v4l.org
Update
If you want the third stage array for other reasons than output, you can generate it with this recursive function (which still works from the output of preg_match_all):
$html = '<h2>Spiders</h2>
<h2>Beetles</h2>
<h3>External morphology</h3>
<h4>Head</h4>
<h4>Thorax</h4>
<h4>Legs</h4>
<h5>Feet</h5>
<h3>Anatomy and physiology</h3>
<h2>Ants</h2>';
function push_values(&$k, $depth, $content) {
$output = array();
$cdepth = $depth[$k];
while ($k < count($depth)) {
if ($depth[$k] == $cdepth) {
$output[$content[$k]] = '';
$k++;
}
elseif ($depth[$k] > $cdepth) {
$output[$content[$k-1]] = push_values($k, $depth, $content);
}
else {
return $output;
}
}
return $output;
}
preg_match_all('/<h(\d)>([^<]+)/', $html, $matches);
$key = 0;
print_r(push_values($key, $matches[1], $matches[2]));
Output:
Array
(
[Spiders] =>
[Beetles] => Array
(
[External morphology] => Array
(
[Head] =>
[Thorax] =>
[Legs] => Array
(
[Feet] =>
)
)
[Anatomy and physiology] =>
)
[Ants] =>
)
Demo on 3v4l.org
Lastly, I would suggest a minor fix for the first code posted by #Nick causing an undesirable HTML output in the next scenario, where a depth difference greater than two will change the most superficial siblings; for example, the <h3> after <h6> become <h5> and the next <h2> was parsed as <h4>:
$html = '<h2>Spiders</h2>
<h2>Beetles</h2>
<h3>External morphology</h3>
<h4>Head</h4>
<h4>Thorax</h4>
<h4>Legs</h4>
<h5>Feet</h5>
<h6>Toes</h6>
<h3>Anatomy and physiology</h3>
<h2>Ants</h2>';
foreach ($matches[1] as $key => $depth) {
if ($depth > $cdepth) {
echo "\n" . str_repeat(' ', $cdepth * 4) . "<ol>\n";
}
elseif ($depth < $cdepth) {
echo "</li>\n" . str_repeat(' ', $depth * 4) . "</ol>\n" . str_repeat(' ', $depth * 4) . "</li>\n";
}
else {
echo "</li>\n";
}
$cdepth = $depth;
echo str_repeat(' ', $cdepth * 4) . "<li>{$matches[2][$key]}";
}
while ($cdepth-- >= $matches[1][0]) {
echo "</li>\n" . str_repeat(' ', $cdepth * 4) . "</ol>\n";
}
Output:
<ol>
<li>Spiders</li>
<li>Beetles
<ol>
<li>External morphology
<ol>
<li>Head</li>
<li>Thorax</li>
<li>Legs
<ol>
<li>Feet
<ol>
<li>Toes</li>
</ol>
</li>
<li>Anatomy and physiology</li>
</ol>
</li>
<li>Ants</li>
</ol>
</li></ol></li></ol>
-
To address that I just added another while statement to put the correct amount of </li></ol> before adding the next <li> element, which now can correctly validate the W3C inspector.
foreach ($matches[1] as $key => $depth) {
if ($depth > $cdepth) {
echo "\n" . str_repeat(' ', $cdepth * 4) . "<ol>\n";
}
elseif ($depth < $cdepth) {
while ($cdepth -- > $depth) {
echo "</li>\n" . str_repeat(' ', $depth * 4) . "</ol>\n" . str_repeat(' ', $depth * 4) . "\n";
}
}
else {
echo "</li>\n";
}
$cdepth = $depth;
echo str_repeat(' ', $cdepth * 4) . "<li>{$matches[2][$key]}";
}
while ($cdepth-- >= $matches[1][0]) {
echo "</li>\n" . str_repeat(' ', $cdepth * 4) . "</ol>\n";
}
Output:
<ol>
<li>Spiders</li>
<li>Beetles
<ol>
<li>External morphology
<ol>
<li>Head</li>
<li>Thorax</li>
<li>Legs
<ol>
<li>Feet
<ol>
<li>Toes</li>
</ol>
</li>
</ol>
</li>
</ol>
<li>Anatomy and physiology</li>
</ol>
<li>Ants</li>
</ol>
Related
I want to add active class only in first element of li
this is an amp code ...jquery is not working in this properly
how will I achieve this kind of logic please help
function custom_table_of_contents($content) {
global $tableOfContents;
global $checkvalue;
$tableOfContents = '';
$tableOfContents = "<ul class='toc-item-list'>";
$checkvalue = 0;
$index = 1;
$indexes = [2 => 1, 3 => 0, 4 => 0, 5 => 0, 6 => 0];
// Insert the IDs and create the TOC.
$content = preg_replace_callback('#<(h[1-6])(.*?)>(.*?)</\1>#si', function ($matches) use (&$index, &$tableOfContents, &$indexes, &$checkvalue) {
$tag = $matches[1];
$title = strip_tags($matches[3]);
$hasId = preg_match('/id=(["\'])(.*?)\1[\s>]/si', $matches[2], $matchedIds);
$id = $hasId ? $matchedIds[2] : sanitize_title($title);
// Generate the prefix based on the heading value.
//$prefix = '';
foreach (range(2, $tag[1]) as $i) {
if ($i == $tag[1]) {
$indexes[$i] += 1;
}
//$prefix .= $indexes[$i] . '.';
}
$title = "$title";
//Check the tag value
if($checkvalue<$tag)
{
//Condition where previous tag value is lesser than the current one
$tableOfContents .= '<ul>';
} else if ($checkvalue==$tag) {
//Condition where previous tag value is same as that of current one
} else if($checkvalue>$tag){
$tableOfContents .='</ul>';
}
$checkvalue=$tag;
$tableOfContents .= "<li class='$id active'><a href='#$id'>$title</a></li>";
if ($hasId) {
return $matches[0];
}
return sprintf('<%s%s id="%s">%s</%s>', $tag, $matches[2], $id, $matches[3], $tag);
}, $content);
$tableOfContents .= '</ul>';
echo $tableOfContents;
return $content;
}
If you want add active class for first <LI> tag replace below lines
$activeClass = ($checkvalue<$tag)?"active":"";
$tableOfContents .= "<li class='$id $activeClass'><a href='#$id'>$title</a></li>";
Full Code
function custom_table_of_contents($content) {
global $tableOfContents;
global $checkvalue;
$tableOfContents = '';
$tableOfContents = "<ul class='toc-item-list'>";
$checkvalue = 0;
$index = 1;
$indexes = [2 => 1, 3 => 0, 4 => 0, 5 => 0, 6 => 0];
// Insert the IDs and create the TOC.
$content = preg_replace_callback('#<(h[1-6])(.*?)>(.*?)</\1>#si', function ($matches) use (&$index, &$tableOfContents, &$indexes, &$checkvalue) {
$tag = $matches[1];
$title = strip_tags($matches[3]);
$hasId = preg_match('/id=(["\'])(.*?)\1[\s>]/si', $matches[2], $matchedIds);
$id = $hasId ? $matchedIds[2] : sanitize_title($title);
// Generate the prefix based on the heading value.
//$prefix = '';
foreach (range(2, $tag[1]) as $i) {
if ($i == $tag[1]) {
$indexes[$i] += 1;
}
//$prefix .= $indexes[$i] . '.';
}
$title = "$title";
//Check the tag value
if($checkvalue<$tag)
{
//Condition where previous tag value is lesser than the current one
$tableOfContents .= '<ul>';
}
else if ($checkvalue==$tag)
{
//Condition where previous tag value is same as that of current one
}
else if($checkvalue>$tag){
$tableOfContents .='</ul>';
}
$checkvalue=$tag;
$activeClass = ($checkvalue<$tag)?"active":"";
$tableOfContents .= "<li class='$id $activeClass'><a href='#$id'>$title</a></li>";
if ($hasId) {
return $matches[0];
}
return sprintf('<%s%s id="%s">%s</%s>', $tag, $matches[2], $id, $matches[3], $tag);
}, $content);
$tableOfContents .= '</ul>';
echo $tableOfContents;
return $content;
}
I have the below code to list the Array of terms, I am placing a comma between the terms if there are more than one term assigned to the post.
$terms = get_terms('my_term', $args);
if (!empty($terms) && !is_wp_error($terms)) {
$count = count($terms);
$i = 0;
$term_list = '<span>';
foreach ($terms as $term) {
$i++;
$term_list .= '#<span>' . $term->name . '</span>';
if ($count != $i) {
$term_list .= ', ';
} else {
$term_list .= '</span>';
}
}
}
Now, I would like place a & between the last two terms instead of a , if there are more than one term assigned to the post.
I think it is easier to solve it with an array.
$terms = get_terms('my_term', $args);
if (!empty($terms) && !is_wp_error($terms)) {
$term_array = [];
foreach ($terms as $term) {
$term_array[] = '#<span>' . $term->name . '</span>';
}
if(count($term_array) > 1){
$last = array_pop($term_array);
$term_list = '<span>' . implode(', ', $term_array) . '</span>';
$term_list .= ' & ' . $last;
} else {
$term_list = '<span>' . $term_array[0] . '</span>';
}
}
OR:
$terms = get_terms('my_term', $args);
if (!empty($terms) && !is_wp_error($terms)) {
$count = count($terms);
$i = 1;
$term_list = '<span>';
foreach ($terms as $term) {
$term_list .= '#<span>' . $term->name . '</span>';
if($i !== $count){
if($i === $count - 1){
$term_list .= ' & ';
} else {
$term_list .= ', ';
}
}
$i++;
}
$term_list .= '</span>';
}
Check if $count is equal to $i + 1:
if ($count != $i) {
if ($count == $i + 1)
$term_list .= '& ';
else
$term_list .= ', ';
} else {
$term_list .= '</span>';
}
That should do it.
You could check if the $count is equal to $i.
$i = 1;
if ($count != $i) {
$term_list .= ', ';
} else if ($count == $i) {
$term_list .= '& ';
} else {
$term_list .= '</span>';
}
Find the last element of the array, then just check each item within your loop against lastelement, then do your 'magic' ;).
Example:
$array = array('a' => 1,'b' => 2,'c' => 3);
$lastElement = end($array);
foreach($array as $k => $v) {
echo $v . '<br/>';
if($v == $lastElement) {
// 'Now you know that $v is the last element, do something';
}
}
I looking for a way (regex, snippet ,plugin etc) to convert the old arrays with the new php syntax with sublimeText.
// Old synthax
$var = array(
'foo' => 'bar'
);
// New synthax
$var = [
'foo' => 'bar'
];
someone has an idea ?
I found a script that does the job perfectly !
https://github.com/thomasbachem/php-short-array-syntax-converter
I found out that it is also possible to do this with the php codesniffer: https://github.com/squizlabs/PHP_CodeSniffer
phpcbf src/ --standard=Generic --sniffs=Generic.Arrays.DisallowLongArraySyntax
In this example you have to replace src/ with the folder containing the scripts. Alternatively you can provide a file name.
Maybe a bit late, but I created my own. Maybe not pretty, but it does do the job I want. If you do not like tab, change \t in indent function to 2 or 4 spaces.
function loopArray(array $array, $loopcount = 0) {
$returnString = ($loopcount == 0) ? "[\n" : "";
$tabKey = indent($loopcount + 2);
$tabValue = indent($loopcount + 3);
$lastKey = array_key_last($array);
foreach ($array as $key => $value) {
$totalChildren = count($array[$key]);
$returnString .= $tabKey . '"' . $key . '" => ';
if ($totalChildren == 0) $returnString .= '[]';
if ($totalChildren > 0 && is_array($array[$key])) $returnString .= '[' . "\n";
if (is_array($value)) {
$returnString .= loopArray($value, $loopcount + 1);
} else {
if ($totalChildren == 1) $returnString .= '"' . $value . '"';
if ($totalChildren > 1) $returnString .= $tabValue . '"' . $value . '"' . ",\n";
}
$returnString .= ($lastKey == $key) ? "\n" . indent($loopcount+1) . "]" : ",\n";
}
return $returnString;
}
function indent($amount) {
return str_repeat("\t", $amount);
}
/** use function below only prior to php 7.3 */
function array_key_last(array $array) {
$key = NULL;
if ( is_array( $array ) ) {
end( $array );
$key = key( $array );
}
return $key;
}
I am new at php, so please be kind.
I am building a script that gets the number of facebook likes from facebook pages.
Then it sorts them, I have found a way to add the page's profile picture using css, however the only class I am able to add is a url. how can I give each thumbnail it's own class, which I can then apply the css to?
here is my code:
function array_sort($array, $on, $order=SORT_ASC)
{
$new_array = array();
$sortable_array = array();
if (count($array) > 0) {
foreach ($array as $k => $v) {
if (is_array($v)) {
foreach ($v as $k2 => $v2) {
if ($k2 == $on) {
$sortable_array[$k] = $v2;
}
}
} else {
$sortable_array[$k] = $v;
}
}
switch ($order) {
case SORT_ASC:
asort($sortable_array);
break;
case SORT_DESC:
arsort($sortable_array);
break;
}
foreach ($sortable_array as $k => $v) {
$new_array[$k] = $array[$k];
}
}
return $new_array;
}
function getLikes($arr){
$urls = "";
// Add urls to check for likes
for($i = 0;$i < count($arr);$i++) {
if($urls != "") $urls .= ",https://www.facebook.com/";
$urls .= $arr[$i];
}
// Retreive info from Facebook
$xml = simplexml_load_file("http://api.facebook.com/restserver.php?method=links.getStats&urls=https://www.facebook.com/" . $urls);
$likes = array();
// Loop through the result and populate an array with the likes
for ($i = 0;$i < count($arr);$i++) {
$url = $xml->link_stat[$i]->url;
$counts = (int)$xml->link_stat[$i]->like_count;
$likes[] = array('likes' => $counts,'url' => $url);number_format(1000, 0, '.', ',');
}
return $likes;
}
$array = array("kylieminogue","SiaMusic","iggyazalea");
$likes = getLikes($array);
$likes = array_sort($likes, 'likes', SORT_DESC);
foreach ($likes as $key => $val) {
$final = number_format($val['likes'], 0, '.', ',');
echo "<li class='facebook'><div class='fb-page'><div class='rank'>" . $key . "</div>" . "<div class='thumb " . $val['url'] . "'><div class='link'>" . $val['url'] . "</div></div>" . "<div class='likes'>" . $final . "</div></div></li><br />";
}
If you do this in getLikes(), inside the second loop:
$likes[] = array(
'likes' => $counts,
'url' => $url,
// create a hopefully unique class name
'class' => strtolower($arr[$i]) . '-' . $i
);
// After this you call number_format without receiving its value, why?
Then in the HTML you change
"<div class='thumb " . $val['url'] . "
for
"<div class='thumb " . $val['class'] . "
Is this what you mean?
When I run this code:
foreach ($tree as $node) {
echo str_repeat(' ', $node->tree_depth * 4) . $node->id . PHP_EOL;
}
I get well formatted text like:
Food
Fruit
Red
Cherry
Strawberry
Cool
Not cool
Yellow
Banana
Meat
Beef
Pork
But I want to create a list with <ul><li>...:
I tried with:
echo '<ul>';
$prev_depth = 0;
foreach($table->fetchTree() as $row) {
if ($row->tree_depth > $prev_depth) {
echo '<li><ul>';
} else if ($row->tree_depth < $prev_depth) {
echo '</li></ul>';
}
echo '<li>' . $row->name . '</li>';
$prev_depth = $row->tree_depth;
}
echo '</ul>';
But I have some extra ul tags and so on. I lost 2 days on this so if you can help me please post here...
Try this algorithm:
$tree = array(
array('Food', 0),
array('Fruit', 1),
array('Red', 2),
array('Cherry', 3),
array('Strawberry', 3),
array('Cool', 4),
array('Not cool', 4),
array('Yellow', 2),
array('Banana', 3),
array('Meat', 0),
array('Beef', 1),
array('Pork', 1),
);
$depth = -1;
$flag = false;
foreach ($tree as $row) {
while ($row[1] > $depth) {
echo "<ul>\n", "<li>";
$flag = false;
$depth++;
}
while ($row[1] < $depth) {
echo "</li>\n", "</ul>\n";
$depth--;
}
if ($flag) {
echo "</li>\n", "<li>";
$flag = false;
}
echo $row[0];
$flag = true;
}
while ($depth-- > -1) {
echo "</li>\n", "</ul>\n";
}
Here you just need to replace $tree by $table->fetchTree(), $row[0] by $row->name and $row[1] by $row->tree_depth.
Try this code instead:
<?php
echo "<ul>\n";
$tree = array(
array('Food', 0),
array('Fruit', 1),
array('Red', 5),
array('Cherry', 3),
array('Strawberry', 3),
array('Cool', 4),
array('Not cool', 4),
array('Yellow', 2),
array('Banana', 3),
array('Meat', 0),
array('Beef', 4),
array('Pork', 2),
);
$depth = 0;
foreach ($tree as $node) {
if ($node[1] > $depth)
echo str_repeat("<ul>\n", $node[1] - $depth);
if ($node[1] < $depth)
echo str_repeat("</ul>\n", $depth - $node[1]);
$depth = $node[1];
echo "<li>" . $node[0] . "\n";
}
echo str_repeat("</ul>\n", $depth+1);
?>
I've updated it to output fewer <li> tags, thereby reducing the number of bullets. But on the other hand, this will generate HTML that wont validate since a jump of more than one level will result in a <ul><ul> being generated.