/*
* Simple HTML 4.01 parser class for PHP
*
* Copyright (c) 2007, Matthew Mondor
* ALL RIGHTS RESERVED.
*/
class HTMLParser
{
private $str = '';
private $str_len = 0;
private $str_i = 0;
private $callback = NULL;
private $udata = NULL;
private $state = NULL;
private $tag = NULL;
private $tag_len = 0;
private $attribute = NULL;
private $attributes = array();
private $last_free = NULL;
public function __construct($str, $function, &$udata)
{
$this->str = $str;
$this->str_len = strlen($str);
$this->callback = $function;
$this->udata = &$udata;
}
public function parse()
{
$this->state = 'state_0';
for ($this->str_i = 0; $this->str_i < $this->str_len; ) {
if (($x = $this->{$this->state}()) !== NULL)
return $x;
}
/* Send any remaining data */
if ($this->last_free < $this->str_i) {
$s = trim(substr($this->str, $this->last_free,
$this->str_i - $this->last_free));
if (strlen($s) > 0) {
$o = array(
'type' => 'content',
'content' => $s);
$f = &$this->callback;
if (($r = $f($o, $this->udata)) != NULL)
return $r;
}
}
/* NOTREACHED */
return NULL;
}
private function state_0()
{
$this->last_free = $this->str_i;
/* Free-run until '<', switch to state 1 if found */
for (; $this->str_i < $this->str_len; $this->str_i++) {
if ($this->str{$this->str_i} == '<') {
if ($this->last_free < $this->str_i) {
$s = trim(substr($this->str,
$this->last_free, $this->str_i -
$this->last_free));
if (strlen($s) > 0) {
$o = array(
'type' => 'content',
'content' => $s);
$f = &$this->callback;
if (($r = $f($o, $this->udata))
!= NULL)
return $r;
}
}
$this->state = 'state_1';
return NULL;
}
}
return NULL;
}
private function state_1()
{
/*
* Skip comment if any;
* error if reaching EOF before comment ends
*/
if ($this->str_i <= $this->str_len - 4 &&
($i = strpos($this->str, '', $this->str_i))
=== FALSE)
return "Expected '-->' but reached EOF";
$this->str_i = $i + 3;
$this->state = 'state_0';
return NULL;
}
/* Not a comment, switch to state 2 */
$this->state = 'state_2';
return NULL;
}
private function state_2()
{
/* Tag must start with '<' */
if ($this->str{$this->str_i} != '<')
return "Expected '<' at position {$this->str_i}";
/* Tag start, switch to state 3 */
$this->str_i++;
$this->state = 'state_3';
return NULL;
}
private function state_3()
{
/* Load tag name */
for ($this->tag = '', $this->tag_len = 0;
$this->str_i < $this->str_len &&
!ctype_space($this->str{$this->str_i}) &&
$this->str{$this->str_i} != '>';
$this->str_i++, $this->tag_len++)
$this->tag .= $this->str{$this->str_i};
if ($this->tag_len == 0)
return "Expected tag name at position {$this->str_i}";
/* Non-empty tag name, switch to state 4 */
$this->attributes = array();
$this->state = 'state_4';
return NULL;
}
private function state_4()
{
/* Skip whitespaces */
for (; $this->str_i < $this->str_len &&
ctype_space($this->str{$this->str_i}); $this->str_i++) ;
/*
* Upon end of tag, call the user function with the tag
* and switch to state 0.
*/
if ($this->str{$this->str_i} == '>') {
$this->str_i++;
$o = array(
'type' => 'tag',
'tag' => array(
'name' => strtolower($this->tag),
'attributes' => $this->attributes));
$f = &$this->callback;
if (($r = $f($o, $this->udata)) != NULL)
return $r;
$this->state = 'state_0';
return NULL;
}
/* Load attribute name */
for ($this->attribute = '';
$this->str_i < $this->str_len &&
!ctype_space($this->str{$this->str_i}) &&
$this->str{$this->str_i} != '>';
$this->str_i++)
$this->attribute .= $this->str{$this->str_i};
if (strlen($this->attribute) == 0)
return 'Expected attribute name at position ' .
"{$this->str_i}";
/* Skip whitespaces */
for (; $this->str_i < $this->str_len &&
ctype_space($this->str{$this->str_i}); $this->str_i++) ;
/*
* Nonempty attribute name, is it assigned a value?
* Switch to state 4 if empty, or 5 to assign it.
*/
if ($this->str{$this->str_i} != '=') {
$this->attributes[strtolower($this->attribute)] = NULL;
$this->state = 'state_4';
return NULL;
}
$this->str_i++;
$this->state = 'state_5';
return NULL;
}
private function state_5()
{
/* Skip whitespaces */
for (; $this->str_i < $this->str_len &&
ctype_space($this->str{$this->str_i}); $this->str_i++) ;
/* Assignment must start with '"' */
if ($this->str{$this->str_i} != '"')
return "Expected '\"' at position {$this->str_i}";
$this->str_i++;
/*
* Find ending '"', error if EOF; Assign attribute and switch
* back to state 4.
*/
if (($i = strpos($this->str, '"', $this->str_i)) === FALSE) {
--$this->str_i;
return "Expected closing '\"' to match position " .
"{$this->str_i} but reached EOF";
}
if ($i > $this->str_i) {
$this->attributes[strtolower($this->attribute)] =
substr($this->str, $this->str_i,
($i - $this->str_i));
$this->str_i = $i + 1;
} else {
$this->attributes[strtolower($this->attribute)] = NULL;
$this->str_i++;
}
$this->state = 'state_4';
return NULL;
}
}
/*
* Test the parser
*/
function po_callback(&$o, &$udata)
{
print_r($o);
/*
if ($o['type'] == 'content')
print($o['content']);
else {
print("<{$o['tag']['name']}");
$a = $o['tag']['attributes'];
foreach($a as $k => $v) {
if ($v != NULL)
print(" {$k}=\"{$v}\"");
else
print(" {$k}");
}
print('>');
$udata['tags']++;
}
*/
$udata['tags']++;
return NULL;
}
$p_udata = array();
$p_udata['tags'] = 0;
$po = new HTMLParser(file_get_contents('bookmarks.html'), 'po_callback',
$p_udata);
print($po->parse() . "\n");
print("Tags: {$p_udata['tags']}\n");
?>