I am looking to create a very simple, very basic nested table of contents in php which gets all the h1-6 and indents things appropriately. This means that if I have something like:
<h1>content</h1>
<h2>more content</h2>
I should get:
content
more content.
I know it will be css that creates the indents, that's fine, but how do I create a table of contents with working links to the content on the page?
apparently its hard to grasp what I am asking for...
I am asking for a function that reads an html document and pulls out all the h1-6 and makes a table of contents.
I used this package, it's pretty easy and straight forward to use.
https://github.com/caseyamcl/toc
Install via Composer by including the following in your composer.json file:
{
"require": {
"caseyamcl/toc": "^3.0",
}
}
Or, drop the src folder into your application and use a PSR-4 autoloader to include the files.
Usage
This package contains two main classes:
TOC\MarkupFixer: Adds id anchor attributes to any H1...H6 tags that do not already have any (you can specify which header tag levels to use at runtime)
TOC\TocGenerator: Generates a Table of Contents from HTML markup
Basic Example:
$myHtmlContent = <<<END
<h1>This is a header tag with no anchor id</h1>
<p>Lorum ipsum doler sit amet</p>
<h2 id='foo'>This is a header tag with an anchor id</h2>
<p>Stuff here</p>
<h3 id='bar'>This is a header tag with an anchor id</h3>
END;
$markupFixer = new TOC\MarkupFixer();
$tocGenerator = new TOC\TocGenerator();
// This ensures that all header tags have `id` attributes so they can be used as anchor links
$htmlOut = "<div class='content'>" . $markupFixer->fix($myHtmlContent) . "</div>";
//This generates the Table of Contents in HTML
$htmlOut .= "<div class='toc'>" . $tocGenerator->getHtmlMenu($myHtmlContent) . "</div>";
echo $htmlOut;
This produces the following output:
<div class='content'>
<h1 id="this-is-a-header-tag-with-no-anchor-id">This is a header tag with no anchor id</h1>
<p>Lorum ipsum doler sit amet</p>
<h2 id="foo">This is a header tag with an anchor id</h2>
<p>Stuff here</p>
<h3 id="bar">This is a header tag with an anchor id</h3>
</div>
<div class='toc'>
<ul>
<li class="first last">
<span></span>
<ul class="menu_level_1">
<li class="first last">
This is a header tag with an anchor id
<ul class="menu_level_2">
<li class="first last">
This is a header tag with an anchor id
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
For this you have just to search for the tags in the HTML code.
I wrote two functions (PHP 5.4.x).
The first one returns an array, that contains the data of the table of contents. The data is is only the headline it self, the id of the tag (if you want to use anchors) and a sub-table of content.
function get_headlines($html, $depth = 1)
{
if($depth > 7)
return [];
$headlines = explode('<h' . $depth, $html);
unset($headlines[0]); // contains only text before the first headline
if(count($headlines) == 0)
return [];
$toc = []; // will contain the (sub-) toc
foreach($headlines as $headline)
{
list($hl_info, $temp) = explode('>', $headline, 2);
// $hl_info contains attributes of <hi ... > like the id.
list($hl_text, $sub_content) = explode('</h' . $depth . '>', $temp, 2);
// $hl contains the headline
// $sub_content contains maybe other <hi>-tags
$id = '';
if(strlen($hl_info) > 0 && ($id_tag_pos = stripos($hl_info,'id')) !== false)
{
$id_start_pos = stripos($hl_info, '"', $id_tag_pos);
$id_end_pos = stripos($hl_info, '"', $id_start_pos);
$id = substr($hl_info, $id_start_pos, $id_end_pos-$id_start_pos);
}
$toc[] = [ 'id' => $id,
'text' => $hl_text,
'sub_toc' => get_headlines($sub_content, $depth + 1)
];
}
return $toc;
}
The second returns a string that formats the toc with HTML.
function print_toc($toc, $link_to_htmlpage = '', $depth = 1)
{
if(count($toc) == 0)
return '';
$toc_str = '';
if($depth == 1)
$toc_str .= '<h1>Table of Content</h1>';
foreach($toc as $headline)
{
$toc_str .= '<p class="headline' . $depth . '">';
if($headline['id'] != '')
$toc_str .= '<a href="' . $link_to_htmlpage . '#' . $headline['id'] . '">';
$toc_str .= $headline['text'];
$toc_str .= ($headline['id'] != '') ? '</a>' : '';
$toc_str .= '</p>';
$toc_str .= print_toc($headline['sub_toc'], $link_to_htmlpage, $depth+1);
}
return $toc_str;
}
Both functions are far away from being perfect, but they work fine in my tests. Feel free to improve them.
Notice: get_headlines is not a parser, so it does not work on broken HTML code and just crashes. It also only works with lowercase <hi>-tags.
How about this (although it can only do one H level) ...
function getTOC(string $html, int $level=1) {
$toc="";
$x=0;
$n=0;
$html1="";
$safety=1000;
while ( $x>-1 and $safety-->0 ) {
$html0=strtolower($html);
$x=strpos($html0, "<h$level");
if ( $x>-1 ) {
$y=strpos($html0, "</h$level>");
$part=strip_tags(substr($html, $x, $y-$x));
$toc .="<a href='#head$n'>$part</a>\n";
$html1.=substr($html,0,$x)."<a name='head$n'></a>".substr($html, $x, $y-$x+5)."\n";
$html=substr($html, $y+5);
$n++;
}
}
$html1.=$html;
$html=$toc."\n<HR>\n".$html1;
return $html;
}
This will create a basic list of links
$html="<html><body>";
$html.="<h1>Heading 1a</h1>One Two Three";
$html.="<h2>heading 2a</h2>Four Five Six";
$html.="<h1 class='something'>Heading 1b</h1>Seven Eight Nine";
$html.="<h2>heading 2b</h2>Ten Eleven Twelve";
$html.="</body></html>";
echo getTOC($html, 1);
gives...
<a href='#head0'>Heading 1a</a>
<a href='#head1'>Heading 1b</a>
<HR>
<html><body><a name='head0'></a><h1>Heading 1a</h1>
One Two Three<h2>heading 2a</h2>Four Five Six<a name='head1'></a><h1
class='something'>Heading 1b</h1>
Seven Eight Nine<h2>heading 2b</h2>Ten Eleven Twelve</body></html>
See https://onlinephp.io/c/fceb0 for a running example
This function return the string with appended table of content only for h2 tags. 100% tested code.
function toc($str){
$html = preg_replace('/]+\>/i', '$0 In This Article', $str, 1); //toc just after first image in content
$doc = new DOMDocument();
$doc->loadHTML($html);
// create document fragment
$frag = $doc->createDocumentFragment();
// create initial list
$frag->appendChild($doc->createElement('ul'));
$head = &$frag->firstChild;
$xpath = new DOMXPath($doc);
$last = 1;
// get all H1, H2, …, H6 elements
$tagChek = array();
foreach ($xpath->query('//*[self::h2]') as $headline) {
// get level of current headline
sscanf($headline->tagName, 'h%u', $curr);
array_push($tagChek,$headline->tagName);
// move head reference if necessary
if ($curr parentNode->parentNode;
}
} elseif ($curr > $last && $head->lastChild) {
// move downwards and create new lists
for ($i=$last; $ilastChild->appendChild($doc->createElement('ul'));
$head = &$head->lastChild->lastChild;
}
}
$last = $curr;
// add list item
$li = $doc->createElement('li');
$head->appendChild($li);
$a = $doc->createElement('a', $headline->textContent);
$head->lastChild->appendChild($a);
// build ID
$levels = array();
$tmp = &$head;
// walk subtree up to fragment root node of this subtree
while (!is_null($tmp) && $tmp != $frag) {
$levels[] = $tmp->childNodes->length;
$tmp = &$tmp->parentNode->parentNode;
}
$id = 'sect'.implode('.', array_reverse($levels));
// set destination
$a->setAttribute('href', '#'.$id);
// add anchor to headline
$a = $doc->createElement('a');
$a->setAttribute('name', $id);
$a->setAttribute('id', $id);
$headline->insertBefore($a, $headline->firstChild);
}
// echo $frag;
// append fragment to document
if(!empty($tagChek)):
$doc->getElementsByTagName('section')->item(0)->appendChild($frag);
return $doc->saveHTML();
else:
return $str;
endif;
}
Related
I am trying to process a HTML file with php as a DOM document. Processing is okay, but when I save the html document with $html->saveHTMLFile("file_out.html"); all link tags are converted from:
Click here: <a title="editable" href="http://somewhere.net">somewhere.net</a>
to
Click here: <a title="editable" href="http://somewhere.net"> somewhere.net </a>
I process the links as php scripts, maybe this makes a difference?
I cannot convert the < back to < with htmlentitites_decode() or such. Is there any other conversion or encoding I can use?
The php script looks like the following:
<?php
$text = $_POST["textareaX"];
$id = $_GET["id"];
$ref = $_GET["ref"];
$html = new DOMDocument();
$html->preserveWhiteSpace = true;
$html->formatOutput = false;
$html->substituteEntities = false;
$html->loadHTMLFile($ref.".html");
$elem = $html->getElementById($id);
$elem->nodeValue = $innerHTML;
if ($text == "")
{ $text = "--- No details. ---"; }
$newtext = "";
$words = explode(" ",$text);
foreach ($words as $word) {
if (strpos($word, "http://") !== false) {
$newtext .= "<a alt=\"editable\" href=\"".$word."\">".$word."</a>";
}
else {$newtext .= $word." ";}
}
$text = $newtext;
function setInnerHTML($DOM, $element, $innerHTML) {
$node = $DOM->createTextNode($innerHTML);
$children = $element->childNodes;
foreach ($children as $child) {
$element->removeChild($child);
}
$element->appendChild($node);
}
setInnerHTML($html, $elem, $text);
$html->saveHTMLFile($ref.".html");
header('Location: '."tracking.php?ref=$ref&user=unLock");
?>
We get the reference to a file from "id" and "ref" and the input data from array "textareaX". Next I open the file, identify the html element by id and replace its content (a link) with the input data from the textarea. I provide only the href in the textarea and the script builds the hyperlink from that. Next I plug this back into the original file and overwrite the input file.
When I write the new file though, the link <a href= ...> </a> is converted to <a href=...> </a>, which is a problem.
Here is part of your code with the issue identified:
<?php
function setInnerHTML($DOM, $element, $innerHTML) {
/*********************************
Well, there's your problem:
**********************************/
$node = $DOM->createTextNode($innerHTML);
$children = $element->childNodes;
foreach ($children as $child) {
$element->removeChild($child);
}
$element->appendChild($node);
}
?>
What you are doing is passing your new anchor (a) tag as a string then creating a text node out of it (text is just that - text, not HTML). The createTextNode function automatically encodes any HTML tags so that they will be visible as text when viewed by a browser (this is so you can present HTML as visible code on your page if you choose to).
What you need to do is create the element as HTML (not a text node) then append it:
<?php
function setInnerHTML($DOM, $element, $innerHTML) {
$f = $DOM->createDocumentFragment();
$f->appendXML($innerHTML);
$element->appendChild($f);
}
?>
This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Walk array recursively and print the path of the walk
Anyone can help me on this code?
<?php
function buildMenuWalk(&$array, &$depth, $currentDepth = 1)
{
# start new level html
$html = '';
# walk till the depth defined on the config
if($currentDepth > $depth)
{
return null;
}
# loop through all items in this level
foreach($array as $key => &$value)
{
# if not in area map continue
if(!is_numeric($key))
{
continue;
}
# if no <li> has been created yet, open the <ul>
$html .= empty( $html ) ? '<ul class="dropdown">' : '';
#extract the label from this level's array, designated by $labelKey
$label = isset( $value['areaname'] ) ? $value['areaname'] : '';
# open an <li>
$html .= '<li>';
# generate url
$url = '';
if($currentDepth == $depth)
{
$url = ' href="'.
$url .= '"';
}
# construct content inside the <li>
$html .= '<a' . $url .'>' . $label . '</a>';
# run the function again to grab children levels
if(is_array($value))
{
$html .= buildMenuWalk($value, $depth, $currentDepth + 1);
}
# close <li>
$html .= '</li>';
}
# close <ul> if was generated content on this level
$html .= !empty( $html ) ? '</ul>' : '';
return $html;
}
$depth = 2;
$config['content']['map'][1]['areaname'] = 'area_1';
$config['content']['map'][1][1]['areaname'] = 'block_1';
$config['content']['map'][2]['areaname'] = 'area_2';
$config['content']['map'][2][1]['areaname'] = 'block_1';
$config['content']['map'][2][2]['areaname'] = 'bloack_2';
echo buildMenuWalk($config['content']['map'], $depth);
?>
If you check the code above, Im using to display a menu recursively ...
If the script identify the menu reached the depth, will display href="" inside the tag. Inside this href I want add all parent areas on the recursion.
For example:
$config['content']['map'][2]['areaname'] = 'area_2';
$config['content']['map'][2][1]['areaname'] = 'block_1';
$config['content']['map'][2][2]['areaname'] = 'bloack_2';
When script reaches the bloack_2, I need display:
<a href="area_2=2&block_2=2">
Since its a multidimensional array and can grow to like 4-5 dimension, the output href should count all these levels. For example area_2=1&block_10=5&sub_area_1=5§ion_7=8 ...
I probably need some array to store all href path during the recursion, but I cant figure how to do it.
Thanks,
PS: The script will be used to build a dropdown menu. The parent levels don't need to be linked, so print the tag will display the child menu. The last child, will be linked, but need contain all parent params so the results can be filtered.
Link to the code running and returning values:
http://codepad.org/iyrcdfQP
Here is a modification of that same slice of code that makes the href= a little more like what you are describing, although it doesn't really make sense to me.
# generate url
$url = '';
if($currentDepth == $depth) {
$url = " href=".$GLOBALS['area_name']."=".$GLOBALS['area']."&$label=$key";
}
else {
$GLOBALS['area'] = $key;
$GLOBALS['area_name'] = $label;
}
I don't see why one would bother with recursoin and multiple dimensions in a world of 2 dimensions as the menu in a web page is!
I would go for 2 dimensions only, the first dimension is to keep all elements (without any criteria) and the second to keep infos about the element:
Check an example for yourself, you set which element to get the function displays it along with all ancestors and root elements of previous levels (as California is).
<?php
function buildMenuWalk($element,$menuElements)
{
for($i=0;$i<=$element;$i++)
{
//for a new level go to the next line
if(!isset($menuElements[$i]['ancestors']))
{
echo '<br>______________________________________</br>';
echo '<strong>',$menuElements[$i]['label'],'</strong> | ';
}
//if the element is reached display it along with ancestors
if($i==$element)
{
//echo all the ancestores
foreach($menuElements[$element]['ancestors'] as $value)
{
echo $menuElements[$value]['label'],' | ';
}
//display the element itself
echo '<font color=red>',$menuElements[$element]['label'],' | </font>';
}
}
}
//California
$menuElements[0]=Array('url'=>'http://www.California.com','label'=>'California');
$menuElements[1]=Array('url'=>'http://www.San Diego.com','label'=>'San Diego','ancestors'=>Array(0));
$menuElements[2]=Array('url'=>'http://www.San Jose.com','label'=>'San Jose','ancestors'=>Array(0,1));
$menuElements[3]=Array('url'=>'http://www.San Francisco.com','label'=>'San Francisco','ancestors'=>Array(0,1,2));
$menuElements[4]=Array('url'=>'http://www.Fresno.com','label'=>'San Francisco','ancestors'=>Array(0,1,2,3));
$menuElements[5]=Array('url'=>'http://www.Sacramento.com','label'=>'Sacramento','ancestors'=>Array(0,1,2,3,4));
//Wyoming
$menuElements[6]=Array('url'=>'http://www.Wyoming.com','label'=>'Wyoming');
$menuElements[7]=Array('url'=>'http://www.Cheyenne.com','label'=>'Cheyenne','ancestors'=>Array(6));
$menuElements[8]=Array('url'=>'http://www.Casper.com','label'=>'Casper','ancestors'=>Array(6,7));
$menuElements[9]=Array('url'=>'http://www.Laramie.com','label'=>'Laramie','ancestors'=>Array(6,7,8));
$menuElements[10]=Array('url'=>'http://www.Gillette.com','label'=>'Gillette','ancestors'=>Array(6,7,8,9));
$menuElements[11]=Array('url'=>'http://www.Rock Springs.com','label'=>'Rock Springs','ancestors'=>Array(6,7,8,9,10));
echo '<pre>';
buildMenuWalk(9,$menuElements);
?>
Since this function is recursively calling itself, you'll have to refer to a global variable to keep track of the previous tree. So here is the code that'll do what I think you want it to do. All of you have to change is the #generate url section.
# generate url
$url = '';
if($currentDepth == $depth) {
$url = " href=area=" . $GLOBALS['area'] . "&block=$key";
} else {
$GLOBALS['area'] = $key;
}
I'm trying to add consecutive classes to all list-items in a list with the class of 'nav'. Essentially, I want every list-item to have a class of 'nthChild-x', where x represents its position in the list. I'm a major noob to PHP, so be easy.
Here is the current markup:
<ul id="primaryNav" class="nav">
<li>Blah Blah Uno</li>
<li>Blah Blah Dos</li>
<li>Blah Blah Tres</li>
</ul>
I want this list to be rendered as the following:
<ul id="primaryNav" class="nav">
<li class="nthChild-1">Blah Blah Uno</li>
<li class="nthChild-3">Blah Blah Dos</li>
<li class="nthChild-3">Blah Blah Tres</li>
</ul>
Please don't reply with a JavaScript or JQuery solution. I know how to do this with JS but need this to be server-side. Also, I don't necessarily want to target the ID of the list because I'd rather do it once and target all lists (though that could be a start).
Any ideas?
You can use DOMDocument for that.
This one will work with existing classes and won't add the same class twice.
$dom = new DOMDocument;
$dom->loadHTML($html);
$lists = $dom->getElementsByTagName('ul');
foreach($lists as $list) {
$index = 1;
foreach($list->childNodes as $node) {
if ($node->nodeName != 'li') {
continue;
}
$class = array();
if ($node->hasAttribute('class')) {
$class = preg_split('/\s+/', $node->getAttribute('class'));
}
$addClass = 'nthChild-' . $index;
if (in_array($addClass, $class)) {
continue;
}
$class[] = $addClass;
$node->setAttribute('class', implode(' ', $class));
$index++;
}
}
$html = '';
foreach($dom->getElementsByTagName('body')->item(0)->childNodes as $element) {
$html .= $dom->saveXML($element, LIBXML_NOEMPTYTAG);
}
CodePad.
I suppose you don't use any template system (like Smarty) to generate this. Than you have to use cycle to generate the fields together with integer variable that will have number of iteration stored. It can be done for example like this:
<ul id="primaryNav" class="nav">
<?php
$values = array(1 => "Blah Blah Uno", 2 => "Blah Blah Dos", 3 => "Blah Blah Tres");
for ($i = 1; $i <= 3; $i++) {
echo "<li class=\"nthChild-" . $i . "\">" . $values[$i] . "</li>";
}
?>
</ul>
This solution is quite simple, of course there are many better ways how to do this.
If you only have the list formatted as you've shown here (not in a PHP array), you can do it like this
$markup = '<ul id="primaryNav" class="nav">
<li>Blah Blah Uno</li>
<li>Blah Blah Dos</li>
<li>Blah Blah Tres</li>
</ul>';
if (preg_match_all("/<li>(.*)<\/li>/U",$markup,$result) > 0)
{
$newMarkup = "<ul id=\"primaryNav\" class=\"nav\">\n";
$count = 0;
foreach ($result[1] as $listElement)
{
$count++;
$newMarkup .= "\t<li class=\"nThCild-{$count}\">$listElement</li>\n";
}
print $newMarkup."</ul>\n";
}
Which one of you crafty programmers can show me an elegant php coded solution for automatically generating a nested table of contents based on heading tags on the page?
So I have a html document thus:
<h1> Animals </h1>
Some content goes here.
Some content goes here.
<h2> Mammals </h2>
Some content goes here.
Some content goes here.
<h3> Terrestrial Mammals </h3>
Some content goes here.
Some content goes here.
<h3> Marine Mammals </h3>
Some content goes here.
Some content goes here.
<h4> Whales </h4>
Some content goes here.
Some content goes here.
More specifically, I want a linked table of contents in the form of a nested list of links to headings on the same page:
Table of Contents (automatically generated by PHP code)
Animals
Mammals
Terrestrial_Mammals
Marine_Mammals
Whales
I don't find it elegant, but might help in getting general idea how to create one ;)
It uses simple_html_dom to find and manipulate elements in original html
$htmlcode = <<< EOHTML
<h1> Animals </h1>
Some content goes here.
Some content goes here.
<h2> Mammals </h2>
Some content goes here.
Some content goes here.
<h3> Terrestrial Mammals </h3>
Some content goes here.
Some content goes here.
<h3> Marine Mammals </h3>
Some content goes here.
Some content goes here.
<h4> Whales </h4>
Some content goes here.
Some content goes here.
EOHTML;
// simpehtmldom or other dom manipulating library
require_once 'simple_html_dom.php';
$html = str_get_html($htmlcode);
$toc = '';
$last_level = 0;
foreach($html->find('h1,h2,h3,h4,h5,h6') as $h){
$innerTEXT = trim($h->innertext);
$id = str_replace(' ','_',$innerTEXT);
$h->id= $id; // add id attribute so we can jump to this element
$level = intval($h->tag[1]);
if($level > $last_level)
$toc .= "<ol>";
else{
$toc .= str_repeat('</li></ol>', $last_level - $level);
$toc .= '</li>';
}
$toc .= "<li><a href='#{$id}'>{$innerTEXT}</a>";
$last_level = $level;
}
$toc .= str_repeat('</li></ol>', $last_level);
$html_with_toc = $toc . "<hr>" . $html->save();
Here’s an example using DOMDocument:
$doc = new DOMDocument();
$doc->loadHTML($code);
// create document fragment
$frag = $doc->createDocumentFragment();
// create initial list
$frag->appendChild($doc->createElement('ol'));
$head = &$frag->firstChild;
$xpath = new DOMXPath($doc);
$last = 1;
// get all H1, H2, …, H6 elements
foreach ($xpath->query('//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]') as $headline) {
// get level of current headline
sscanf($headline->tagName, 'h%u', $curr);
// move head reference if necessary
if ($curr < $last) {
// move upwards
for ($i=$curr; $i<$last; $i++) {
$head = &$head->parentNode->parentNode;
}
} else if ($curr > $last && $head->lastChild) {
// move downwards and create new lists
for ($i=$last; $i<$curr; $i++) {
$head->lastChild->appendChild($doc->createElement('ol'));
$head = &$head->lastChild->lastChild;
}
}
$last = $curr;
// add list item
$li = $doc->createElement('li');
$head->appendChild($li);
$a = $doc->createElement('a', $headline->textContent);
$head->lastChild->appendChild($a);
// build ID
$levels = array();
$tmp = &$head;
// walk subtree up to fragment root node of this subtree
while (!is_null($tmp) && $tmp != $frag) {
$levels[] = $tmp->childNodes->length;
$tmp = &$tmp->parentNode->parentNode;
}
$id = 'sect'.implode('.', array_reverse($levels));
// set destination
$a->setAttribute('href', '#'.$id);
// add anchor to headline
$a = $doc->createElement('a');
$a->setAttribute('name', $id);
$a->setAttribute('id', $id);
$headline->insertBefore($a, $headline->firstChild);
}
// append fragment to document
$doc->getElementsByTagName('body')->item(0)->appendChild($frag);
// echo markup
echo $doc->saveHTML();
I found this method, by Alex Freeman (http://www.10stripe.com/articles/automatically-generate-table-of-contents-php.php):
preg_match_all('#<h[4-6]*[^>]*>.*?<\/h[4-6]>#',$html_string,$resultats);
//reformat the results to be more usable
$toc = implode("\n",$resultats[0]);
$toc = str_replace('<a name="','<a href="#',$toc);
$toc = str_replace('</a>','',$toc);
$toc = preg_replace('#<h([4-6])>#','<li class="toc$1">',$toc);
$toc = preg_replace('#<\/h[4-6]>#','</a></li>',$toc);
//plug the results into appropriate HTML tags
$toc = '<div id="toc">
<p id="toc-header">Table des matières</p>
<hr />
<ul>
'.$toc.'
</ul>
</div><br /><br />';
return $toc;
In the HTML, the headers have to be written as:
<h2><a name="target"></a>Text</h2>
Combined some of the above to make a nested index of the headings. This function also inserts links into html itself so it can be linked. Pure php no library needed.
function generateIndex($html)
{
preg_match_all('/<h([1-6])*[^>]*>(.*?)<\/h[1-6]>/',$html,$matches);
$index = "<ul>";
$prev = 2;
foreach ($matches[0] as $i => $match){
$curr = $matches[1][$i];
$text = strip_tags($matches[2][$i]);
$slug = strtolower(str_replace("--","-",preg_replace('/[^\da-z]/i', '-', $text)));
$anchor = '<a name="'.$slug.'">'.$text.'</a>';
$html = str_replace($text,$anchor,$html);
$prev <= $curr ?: $index .= str_repeat('</ul>',($prev - $curr));
$prev >= $curr ?: $index .= "<ul>";
$index .= '<li>'.$text.'</li>';
$prev = $curr;
}
$index .= "</ul>";
return ["html"=>$html,"index"=>$index];
}
Have a look at the TOC class. It allows generating table of contents from nested headings. h1 tag can be followed by any lower level h tag. The class uses recursion to extract the headings from article text
Short solution using SimpleHTMLDom :
public function getSummary($body)
{
$dom = new Htmldom($body);
$summ = "<ul>";
$prev = 2;
foreach($dom->find("h2,h3,h4") as $x => $htag)
{
$curr = intval(substr($htag->tag, -1));
$prev <= $curr ?: $summ .= "</ul>";
$prev >= $curr ?: $summ .= "<ul>";
$summ .= "<li>$htag->plaintext</li>";
$prev = $curr;
}
$summ .= "</ul>";
return $summ;
}
You have a very simple library for this caseyamcl/toc
$html='<h1>Title</h1>text<h2>...<h2>...';
$tocGenerator = new TOC\TocGenerator();
$toc = $tocGenerator->getHtmlMenu($html);
echo $htmlOut;
Bonus: If you want, he can fix the header without tag id by insert this code before.
$tocGenerator = new TOC\TocGenerator();
$html = $markupFixer->fix($html);
I am trying to add drop down menus to a drupal theme which uses text sliding door CSS rounding.
The current version uses a primary links injection of the span into the a tags, which works fine. But doesn't support drop down menus.
Working code:
<?php print theme('links', $primary_links, array('class' => 'links primary-links')) ?>
In the template with a template.php file addition:
<?php
// function for injecting spans inside anchors which we need for the theme's rounded corner background images
function strands_guybrush_links($links, $attributes = array('class' => 'links')) {
$output = '';
if (count($links) > 0) {
$output = '<ul'. drupal_attributes($attributes) .'>';
$num_links = count($links);
$i = 1;
foreach ($links as $key => $link) {
$class = $key;
// Add first, last and active classes to the list of links to help out themers.
if ($i == 1) {
$class .= ' first';
}
if ($i == $num_links) {
$class .= ' last';
}
if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '<front>' && drupal_is_front_page()))) {
$class .= ' active';
}
$output .= '<li'. drupal_attributes(array('class' => $class)) .'>';
if (isset($link['href'])) {
$link['title'] = '<span class="link">' . check_plain($link['title']) . '</span>';
$link['html'] = TRUE;
// Pass in $link as $options, they share the same keys.
$output .= l($link['title'], $link['href'], $link);
}
else if (!empty($link['title'])) {
// Some links are actually not links, but we wrap these in <span> for adding title and class attributes
if (empty($link['html'])) {
$link['title'] = check_plain($link['title']);
}
$span_attributes = '';
if (isset($link['attributes'])) {
$span_attributes = drupal_attributes($link['attributes']);
}
$output .= '<span'. $span_attributes .'>'. $link['title'] .'</span>';
}
$i++;
$output .= "</li>\n";
}
$output .= '</ul>';
}
return $output;
}
?>
So I have added the Nice Menu module which works well and allows the drop down menu functions for my navigation which is now addressed from the template using:
<?php print theme_nice_menu_primary_links() ?>
The issue is that the a tags need to have spans inside to allow for the selected state markup. I have tried every angle I could find to edit the drupal function menu_item_link which is used by nice menus to build the links.
E.g. I looked at the drupal forum for two days and no joy.
The lines in the module that build the links are:
function theme_nice_menu_build($menu) {
$output = '';
// Find the active trail and pull out the menus ids.
menu_set_active_menu_name('primary-links');
$trail = menu_get_active_trail('primary-links');
foreach ($trail as $item) {
$trail_ids[] = $item['mlid'];
}
foreach ($menu as $menu_item) {
$mlid = $menu_item['link']['mlid'];
// Check to see if it is a visible menu item.
if ($menu_item['link']['hidden'] == 0) {
// Build class name based on menu path
// e.g. to give each menu item individual style.
// Strip funny symbols.
$clean_path = str_replace(array('http://', '<', '>', '&', '=', '?', ':'), '', $menu_item['link']['href']);
// Convert slashes to dashes.
$clean_path = str_replace('/', '-', $clean_path);
$class = 'menu-path-'. $clean_path;
$class .= in_array($mlid, $trail_ids) ? ' active' : '';
// If it has children build a nice little tree under it.
if ((!empty($menu_item['link']['has_children'])) && (!empty($menu_item['below']))) {
// Keep passing children into the function 'til we get them all.
$children = theme('nice_menu_build', $menu_item['below']);
// Set the class to parent only of children are displayed.
$class .= $children ? ' menuparent ' : '';
// Add an expanded class for items in the menu trail.
$output .= '<li id="menu-'. $mlid .'" class="'. $class .'">'. theme('menu_item_link', $menu_item['link']);
// Build the child UL only if children are displayed for the user.
if ($children) {
$output .= '<ul>';
$output .= $children;
$output .= "</ul>\n";
}
$output .= "</li>\n";
}
else {
$output .= '<li id="menu-'. $mlid .'" class="'. $class .'">'. theme('menu_item_link', $menu_item['link']) .'</li>'."\n";
}
}
}
return $output;
}
As you can see the $output uses menu_item_link to parse the array into links and to added the class of active to the selected navigation link.
The question is how do I add a span inside the a tags OR how do I wrap the a tags with a span that has the active class to style the sliding door links?
If you want to wrap the a tags with a span, you can overwrite the theme_nice_menu_build and add your span to the output. If you want to inside the a tag you need to overwrite the menu_item_link.
You can overwrite a theme funciton by creation a function call your_theme_name_function_name and Drupal will use that function to render the markup instead of the default one. That way you can alter the markup any way you want. This function should be in your theme's template.php file.
A good way to start is to copy the function you want to overwrite and just alter to your likings.
A lot has happened since Drupal 4.7, I don't hope you use that. It's quite easy to insert span tags:
function your_theme_name_menu_item_link($link) {
if (empty($link['localized_options'])) {
$link['localized_options'] = array();
}
$link['localized_options']['html'] = TRUE;
return l('<span>' . $link['title'] . '</span>', $link['href'], $link['localized_options']);
}
I tested this and it works just fine.