PHP DOMXPath->query()/->evaluate() not matching inner text - php

I am currently trying to create a pure PHP menu traversal system - it's because I'm doing an impromptu project for some people but they want as little JS as possible (i.e: none) and ideally pure PHP.
I have a menu which looks like this:
ul {
list-style-type: none;
}
nav > ul.sidebar-list ul.sub {
display: none;
}
nav > ul.sidebar-list ul.sub.active {
display: block;
}
<nav class="sidebar" aria-labelledby="primary-navigation">
<ul class="sidebar-list">
<!--each element has a sub-menu which is initially hidden by css when the page is loaded. Via php the appropriate path the current page and top-level links will be visible only-->
<li>Home</li>
<!--sub-items-->
<ul class="sub active">
<li>Barn</li>
<li>Activities</li>
<ul class="sub active">
<li>News</li>
<li>Movements</li>
<li>Reviews</li>
<li>About Us</li>
<li>Terms of Use</li>
</ul>
</ul>
<li>Events</li>
<ul class="sub">
<li>Overview</li>
<li>Farming</li>
<li>Practises</li>
<li>Links</li>
<ul class="sub">
<li>Another Farm</li>
<li>24m</li>
</ul>
</ul>
</ul>
</nav>
In order to attempt to match the title inner-text of the page to a menu-item innertext (probably not the best way of doing things but I'm still learning php) I run:
$menu = new DOMDocument();
assert($menu->loadHTMLFile($menu_path), "Loading nav.html (menu file) failed");
//show content to log of the html document
error_log("HTML file: \n\n".$menu->textContent);
//set up a query to find an element matching the title string found
$xpath = new DOMXPath($menu);
$menu_query = "//a/li[matches(text(), '$title_text', 'i')]";
$elements = $xpath->query($menu_query);
error_log($elements ? ("Result of xpath query is: ".print_r($elements, TRUE)): "The xpath query for searching the menu is incorrect and will not find you anything!\ntype of return: ".gettype($elements));
I get the correct return at: https://www.freeformatter.com/xpath-tester.html but in the script I don't. I have tried many different combinations of the text matching such as: //x:a/x:li[lower-case(text())='$title_text'] but always an empty node list.

PHP uses XPath 1.0. matches is an XPath 2.0 function, so you would have seen warnings in your error log if you were looking for them.
PHP Warning: DOMXPath::query(): xmlXPathCompOpEval: function matches not found in php shell code on line 1
PHP Stack trace:
PHP 1. {main}() php shell code:0
PHP 2. DOMXPath->query() php shell code:1
A simple case-sensitive match can be done with an equality check.
$title_text = "Farming";
$menu_query = "//a/li[. = '$title_text']";
But the case-insensitive search involves translating the characters from upper to lower case:
$title_text = "FaRmInG";
$title_text = strtolower($title_text);
$menu_query = "//a/li[translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = '$title_text']";
In either case we end up with a NodeList that can be iterated through:
$html = <<< HTML
<nav class="sidebar" aria-labelledby="primary-navigation">
<ul class="sidebar-list">
<!--each element has a sub-menu which is initially hidden by css when the page is loaded. Via php the appropriate path the current page and top-level links will be visible only-->
<li>Home</li>
<!--sub-items-->
<ul class="sub active">
<li>Barn</li>
<li>Activities</li>
<ul class="sub active">
<li>News</li>
<li>Movements</li>
<li>Reviews</li>
<li>About Us</li>
<li>Terms of Use</li>
</ul>
</ul>
<li>Events</li>
<ul class="sub">
<li>Overview</li>
<li>Farming</li>
<li>Practises</li>
<li>Links</li>
<ul class="sub">
<li>Another Farm</li>
<li>24m</li>
</ul>
</ul>
</ul>
</nav>
HTML;
$menu = new DOMDocument();
$menu->loadHTML($html);
$xpath = new DOMXPath($menu);
$elements = $xpath->query($menu_query);
foreach ($elements as $element) {
print_r($element);
}

Related

How to get parent and nested elements by DOMDocument?

In a typical HTML as
<ol>
<li>
<span>parent</span>
<ul>
<li><span>nested 1</span></li>
<li><span>nested 2</span></li>
</ul>
</li>
</ol>
I try to get the contents of <li> elements but I need to get the parent and those nested under ul separately.
If go as
$ols = $doc->getElementsByTagName('ol');
foreach($ols as $ol){
$lis = $ol->getElementsByTagName('li');
// here I need li immediately under <ol>
}
$lis is all li elements including both parent and nested ones.
How can I get li elements one level under ol by ignoring deeper levels?
There are two approaches to this, the first is how you are working with getElementsByTagName(), the idea would be just to pick out the first <li> tag and assume that it is the correct one...
$ols = $doc->getElementsByTagName('ol');
foreach($ols as $ol){
$lis = $ol->getElementsByTagName('li')[0];
echo $doc->saveHTML($lis).PHP_EOL;
}
This echoes...
<li>
<span>parent</span>
<ul>
<li><span>nested 1</span></li>
<li><span>nested 2</span></li>
</ul>
</li>
which should work - BUT is not exact enough at times.
The other method would be to use XPath, where you can specify the levels of the document tags you want to retrieve. This uses //ol/li, which is any <ol> tag with an immediate descendant <li> tag.
$xp = new DOMXPath($doc);
$lis = $xp->query("//ol/li");
foreach ( $lis as $li ) {
echo $doc->saveHTML($li);
}
this also gives...
<li>
<span>parent</span>
<ul>
<li><span>nested 1</span></li>
<li><span>nested 2</span></li>
</ul>
</li>

Trying to edit html page using php DomDocument class, but neither any errors show up nor it adds <li> and <a> tags to my static page.

I am trying to edit my html file using php based on the user input. When i run the code it dose not give me any error but at the same time it does not make any changes to the file.
Here is the PHP code.
$doc = new DOMDocument();
//validate the document
$doc->validateOnParse = true;
//libxml_use_internal_errors(true);
#$doc->loadHTMLFile('index.html');
$doc->formatOutput = true;
//navigating to the ul
$ul=$doc->getElementById('ul');
$href= "/".$account_number."/".$account_number."-".$profile_name.".html";
$element="Acctount_ID:".$account_number;
$ul=$doc->getElementById('homeSubmenu');
//create the li
$li=$doc->createElement('li');
$new_li=$ul->appendChild($li);
//create the link
$a = $doc->createElement('a');
$new_ul= $new_li->appendChild($a);
$new_ul->setAttribute('href', $href );
$new_ul->setAttribute('target','joe');
$new_ul->setAttribute('title', $element);
echo $doc->saveHTML();
include('./index.html');
//end of the php.
?>
Here is the html code.
<ul class="list-unstyled components">
<p>AWS Accounts</p>
<li class="active">
<a href="#homeSubmenu" data-toggle="collapse" aria-
expanded="true" class="">Accounts</a>
<ul class="list-unstyled collapse in" id="homeSubmenu" aria-
expanded="true" style="">
<li><a href="report1/report.html" target="joe">Account
ID: 930621594025</a></li>
<li><a href="report2/report-rahul.html"
target="joe">Account ID: 725840360046</a></li>
<li><a href="report3/report-chenna.html"
target="joe">Account ID: 624704405165</a></li>
</ul>
</li>
<ul class="list-unstyled CTAs">
<li></li>
<li></li>
</ul>

How to add dropdown CSS class to menus with children in Drupal 7? (without modules)

How can I create a function to scan all menu items from Drupal 7 system and if there is a nested ul, add dropdown CSS classes to the nested ul and add a custom attribute to the parent li container? Im using UIKIT which will automatically create the dropdowns.
Here's my current menu HTML output:
<ul class="menu">
<li class="first last expanded">
<a title="" href="/node/add">Add content</a>
<ul class="menu">
<li class="first leaf">
<a title="article" href="/node/add/article">Article</a></li>
<li class="leaf">
<a title="page" href="/node/add/page">Basic page</a></li>
<li class="last leaf"><a title="blog" href="/node/add/blog">Blog entry</a></li>
</ul>
</li>
</ul>
Here's what I need it to be:
<ul class="menu">
<li class="first last expanded" data-uk-dropdown>
<a title="" href="/node/add">Add content</a>
<ul class="menu uk-dropdown">
<li class="first leaf">
<a title="article" href="/node/add/article">Article</a></li>
<li class="leaf">
<a title="page" href="/node/add/page">Basic page</a></li>
<li class="last leaf"><a title="blog" href="/node/add/blog">Blog entry</a></li>
</ul>
</li>
</ul>
Im looking for the simplest approach possible.
You can crawl menu tree your self and write out menu HTML as you like. Used that and should be something like:
$tree = menu_tree_all_data('menu_machine_name');
Also, if I remember well, if you do only that your active (current) menu item won't be marked any way, and for marking it you have to also call (after getting $tree variable) :
menu_tree_add_active_path($tree);
But again, if I remember well, that function is only available if you install "Menu block" module...
Print out $tree variable after that and organize your code to crawl recursively menu tree you collected.

How to have the class="selected" depending on what the current page/url is

This is my first post so forgive as I am just new in the world of web development.
Normally, when I try to make a website, I create a file called header.html and footer.html so that I only change data once in all of the pages rather than having multiple same headers on many html files. And include them all in a php file together with the content and the php codes that comes per page.
Now my problem is because I only have 1 header, the css is designed in a way that whatever the current menu/tab is, it will be marked as "selected" so that its obvious to the user what page they are currently in.
My question is how do I solve this problem:
1.) To have the class="selected" depending on what the current page/url is.
<!--Menu Starts-->
<div class="menu">
<div id="smoothmenu" class="ddsmoothmenu">
<ul>
<li>Home</li>
<li>About </li>
<li>Services </li>
<li>Features</li>
<li>Support
<ul>
<li>Support 1</li>
<li>Support 2</li>
</ul>
</li>
</ul>
</div>
</div>
<!-- Menu Ends--!>
Thank You :)
If you're looking for a non-javascript / php approach...
First you need to determine which nav-link should be set as active and then add the selected class. The code would look something like this
HTML within php file
Call a php function inline within the hyperlink <a> markup passing in the links destination request uri
<ul>
<li><a href="index.php" <?=echoSelectedClassIfRequestMatches("index")?>>Home</a></li>
<li><a href="about.php" <?=echoSelectedClassIfRequestMatches("about")?>>About</a> </li>
<li><a href="services.php" <?=echoSelectedClassIfRequestMatches("services")?>>Services</a> </li>
<li><a href="features.php" <?=echoSelectedClassIfRequestMatches("features")?>>Features</a></li>
<li>Support
<ul>
<li><a href="support1.php" <?=echoSelectedClassIfRequestMatches("support1")?>>Support 1</a></li>
<li><a href="support2.php" <?=echoSelectedClassIfRequestMatches("support2")?>>Support 2</a></li>
</ul>
</li>
</ul>
PHP function
The php function simply needs to compare the passed in request uri and if it matches the current page being rendered output the selected class
<?php
function echoSelectedClassIfRequestMatches($requestUri)
{
$current_file_name = basename($_SERVER['REQUEST_URI'], ".php");
if ($current_file_name == $requestUri)
echo 'class="selected"';
}
?>
You could ID each link and use JavaScript/Jquery to add the selected class to the appropriate link.
<!--Menu Starts-->
<div class="menu">
<div id="smoothmenu" class="ddsmoothmenu">
<ul>
<li id="home-page">Home</li>
<li id="about-page">About </li>
<li id="services-page">Services </li>
<li id="features-page">Features</li>
<li id="support-page">Support
<ul>
<li id="support1-page">Support 1</li>
<li id="support2-page">Support 2</li>
</ul>
</li>
</ul>
</div>
</div>
<!-- Menu Ends--!>
On your content page use jQuery to do something like:
$(document).ready(function(){
$("#features-page").addClass("selected");
});
Another method you could use is:
Add class element based on the name of the page
Give each link a separate id then use jQuery on the individual pages.
<li>Home</li>
<li>About </li>
<li>Services </li>
<li>Features</li>
<li>Support
<ul>
<li>Support 1</li>
<li>Support 2</li>
</ul>
</li>
On the services page:
$(document).ready(function(){
$("#services").addClass("selected");
});
Or even better as robertc pointed out in the comments, there is no need to even bother with the id's just make the jquery this:
$(document).ready(function(){
$("[href='services.php']").addClass("selected");
});
One variant on Chris's approach is to output a particular class to identify the page, for example on the body element, and then use fixed classes on the menu items, and a CSS rule that targets them matching. For example, this page:
<DOCTYPE HTML>
<head>
<title>I'm the about page</title>
<style type="text/css">
.about .about,
.index .index,
.services .services,
.features .features {
font-weight: bold;
}
</style>
</head>
<body class="<?php echo basename(__FILE__, ".php"); ?>">
This is a menu:
<ul>
<li>Home</li>
<li>About </li>
<li>Services </li>
<li>Features</li>
</ul>
</body>
...is pretty light on dynamic code, but should achieve the objective; if you save it as "about.php", then the About link will be bold, but if you save it as "services.php", then the Services link will be bold, etc.
If your code structure suits it, you might be able to simply hardcode the page's body class in the page's template file, rather than using any dynamic code for it. This approach effectively gives you a way of moving the "logic" for the menu system out of the menu code, which will always remain the same for every page, and up to a higher level.
As an added bonus, you can now use pure CSS to target other things based on the page you're on. For example, you could turn all the h1 elements on the index.php page red just using more CSS:
.index h1 { color: red; }
You can do it from simple if and PHP page / basename() function..
<!--Menu Starts-->
<div class="menu">
<div id="smoothmenu" class="ddsmoothmenu">
<ul>
<li><a href="index.php" <?php if (basename($_SERVER['PHP_SELF']) == "index.php") { ?> class="selected" <?php } ?>>Home</a></li>
<li><a href="about.php" <?php if (basename($_SERVER['PHP_SELF']) == "about.php") { ?> class="selected" <?php } ?>>About</a> </li>
<li><a href="services.php" <?php if (basename($_SERVER['PHP_SELF']) == "services.php") { ?> class="selected" <?php } ?>>Services</a> </li>
<li><a href="features.php" <?php if (basename($_SERVER['PHP_SELF']) == "features.php") { ?> class="selected" <?php } ?>>Features</a></li>
</ul>
</div>
</div>
Sorry for my bad English, however may be it could help. You can use jQuery for this task. For this you need to match the page url to the anchor of menu and then add class selected to it. for example the jQuery code would be
jQuery('[href='+currentURL+']').addClass('selected');

Remove unnecessary li

echo $nav gives code like this:
<ul>
<li class="someclass">sometext
<ul>
<li class="someclass">sometext</li>
<li class="spacer"></li>
<li class="someclass">sometext</li>
<li class="spacer"></li>
<li class="someclass">sometext</li>
<li class="spacer"></li>
<li class="someclass">sometext</li>
<li class="spacer"></li>
</ul>
</li>
<li class="spacer"></li>
<li class="someclass">sometext</li>
<li class="spacer"></li>
</ul>
There are list items with class spacer inside each child ul, after each normal list item.
How do I remove the spacer list items which are grandchildren of the main list, using PHP?
Example: <ul> <li> <ul> <li class="spacer">
I'm searching for a regular expression, which should erase <li class="spacer"></li> only in a child <ul> element.
If you don't have access to the $nav variable to remove it (which you likely do) then I'd just use CSS to hide it, something like this should work:
li ul li.spacer {
display:none;
}
If however you have access to $nav - delete that spacer li from the code. Simples.
Also, on a side note. having empty elements like that on the page as "spacers" is semantically bad. This should be handled via CSS, add margins/padding on other elements on the page, don't use a class of spacer, if you do then you may as well go back to using stray <br /> tags everywhere to create spaces.
$xml = new SimpleXMLElement($nav);
$spacers = $xml->xpath('li//li[#class="spacer"]');
foreach($spacers as $i => $n) {
unset($spacers[$i][0]);
}
echo $xml->asXML();
This is converting to XML (use a recent PHP 5.3 version and DOMDocument to export to HTML). Output:
<?xml version="1.0"?>
<ul>
<li class="someclass">sometext
<ul>
<li class="someclass">sometext</li>
<li class="someclass">sometext</li>
<li class="someclass">sometext</li>
<li class="someclass">sometext</li>
</ul>
</li>
<li class="spacer"/>
<li class="someclass">sometext</li>
<li class="spacer"/>
</ul>
How about str_replace?
$nav = str_replace('<li class="spacer"></li>','',$nav);
edited code below
Based on the new requirement this code works. I know its hacky and sloppy but it works:
$temp = explode("\n",$nav);
for ($i=0;$i<count($temp);$i++) {
if (strstr($temp[$i],"<ul>")) {
$nested_ul = 1;
}
if (strstr($temp[$i],"</ul>")) {
$nested_ul = 0;
}
if ($nested_ul==0) {
if (!strstr($temp[$i],"spacer")) {
$new_nav .= $temp[$i]."\n";
}
} else {
$new_nav .= $temp[$i]."\n";
}
}
echo $new_nav;
"Easily" is relative. It depends on a few things. If you want, modify where the $nav is getting generated from.
use preg_replace to replace the li tags:
$new_nav = preg_replace('/<li class="spacer"></li>/', '', $nav);
echo $nav;
There are multiple ways:
Do not create it. It will be easier if you do not create something you do not want. It will be easier to maintain. So if you have any control over what is generated into $var string, just change it.
Simply replace it like that: str_replace('<li class="spacer"></li>', $var).
Use some HTML parser and remove the nodes.
Use JavaScript to remove <li class="spacer"></li> on client side.
Use substr_replace and strpos instead of str_replace, and specify an offset just after the first spacer.
http://www.php.net/manual/en/function.substr-replace.php
http://www.php.net/manual/en/function.strpos.php
Add the following CSS
ul ul li.spacer { display: none; }
Try this:
$nav = str_replace('<li class="spacer"></li>', '', $nav);

Categories