codepad
[
create a new paste
]
login
|
about
Language:
C
C++
D
Haskell
Lua
OCaml
PHP
Perl
Plain Text
Python
Ruby
Scheme
Tcl
<?php error_reporting(E_ALL | E_STRICT); /** * Exception class for the XmlPrinter class. * @author Peter Goodman */ class XmlPrinterException extends Exception { } /** * A simple stack-based XML string constructor for creating XML. * @author Peter Goodman */ class XmlPrinter { // what type of tag is this? const TAG_CLOSING = 1, TAG_NON_CLOSING = 0; // exception error codes const ERROR_UNRECOVERABLE = 1, // these are usually programmer errors that ERROR_EMPTY_STACK = 2, // need to be fixed in the code ERROR_UNEXPECTED_TAG = 4, ERROR_UNFINISHED_DOCUMENT = 8; // stack of tags and the current indent level private $tags = array(), $indent = -1; /** * Construct the class and put in the base <xml> tag. This tag is non- * removable from the stack and isn't built in the usual way :) It's an * exception to the rule. */ public function __construct() { $this->open(' '); $xml =& $this->tags[$this->indent]; $xml['children'] = -1; $xml['content'] = '<?xml version="1.0"?>'; $this->indent = -1; } // destructor, obvious. public function __destruct() { unset($this->stack); } /** * Add some content to the current tag on the top of the stack. */ public function hasContent($content = "") { $tag =& $this->tags[$this->indent+1]; $nl = FALSE === strstr($content, "\n") ? '' : "\n"; // deal with ampersands $content = preg_replace("~&(?!amp)~", "&", $content); // wrap the content in a CDATA block if it contains tags if(preg_match("~[<>]~i", $content)) $content = "<![CDATA[{$nl}{$content}{$nl}]]>"; // what should the indent prefix be? $prefix = str_repeat("\t", $this->indent+1); // prefix the content $c = ""; foreach(explode("\n", $content) as $line) { if(strlen($line) > 0) { $tag['content_num_lines']++; $c .= $nl . $prefix . $line; } } // only one line, lets remove the prefix if($tag['content_num_lines'] == 1) $c = substr($c, strlen($prefix)); $tag['content'] .= $c; return $this; } /** * Push a tag onto the XML tag stack. */ public function open($name = '', $type = self::TAG_CLOSING) { // we can't push no tag onto the tag stack if(empty($name) || !is_string($name)) throw new XmlPrinterException("Tag name expected in push operation.", self::ERROR_UNRECOVERABLE); $this->indent++; $this->tags[] = array('name' => $name, 'closing' => (bool)$type, 'attr' => '', 'content' => '', 'children' => 0, 'content_num_lines' => 0); // get the parent tag and tell it that it has one more child. The // children is also incremented in the pop method; however, these // don't conflict because the lowest level child will have 0 children // and this will have already been called. Thus, 1 + 0 accurately // represents the number of children the node at the top of the stack // has when a leaf node is popped, and so it all bubbles up nicely. $parent =& $this->tags[$this->indent]; $parent['children']++; return $this; } /** * Pop the current tag off of the tag stack. */ public function close($expect = NULL) { // we've closed all tags, this is a guard against popping the xml // tag off the stack if($this->indent < 0) { $extra = !empty($expect) ? self::ERROR_UNRECOVERABLE : 0; throw new XmlPrinterException("No tags left to close.", self::ERROR_EMPTY_STACK | $extra); } // get the tag that we're popping off the stack $tag = array_pop($this->tags); // did we expect to close a certain tag name and it's not the same one // that the class expects? if($expect !== NULL && $tag['name'] != $expect) throw new XmlPrinterException("Malformed XML. Expected [{$expect}] " ."but found [{$tag['name']}].", self::ERROR_UNEXPECTED_TAG); // this builds the output from the inside out by putting the content // into the parent tag's content. $prefix = $this->indent > 0 ? str_repeat("\t", $this->indent) : ''; $suffix = !$tag['closing'] ? ' /' : ''; $content = "\n{$prefix}<". $tag['name'] . $tag['attr'] . $suffix .">"; // do we need to build the contents of this tag? if($tag['closing']) { // write this tag over multiple lines if($tag['content_num_lines'] > 1 || $tag['children'] > 0) $content .= "{$prefix}". $tag['content'] ."\n{$prefix}"; // write this tag over a single line else $content .= $tag['content']; $content .= '</'. $tag['name'] .">"; } // add stuff into the parent and modify its content and children // counter $parent =& $this->tags[$this->indent]; $parent['content'] .= $content; $parent['children'] += $tag['children']; $this->indent--; return $this; } /** * Append an attribute to the top tag in the stack. This is really just a * convenient shortcut. */ public function __call($attr, array $args = array()) { if(empty($args)) $args = array(""); return $this->hasAttr($attr, (string)$args[0]); } /** * Secondary function for attributes if an attribute, for example, has a * colon in its name or has the name push, pop, or one of the other names * of one of this class' functions. */ public function hasAttr($attr, $val = '') { // unrecoverable programmer error, attributes need to have a name if(empty($attr)) throw new XmlPrinterException("XML tag attribute must has a name.", self::ERROR_UNRECOVERABLE); $tag =& $this->tags[$this->indent+1]; // clean up any quotes in the attribute value so that they don't // conflict with. Single quotes are allowed to pass because the // attribute value is delimited with doubles $attr = (string)$attr; $val = preg_replace('~[\\\]*"~', '\"', (string)$val); $tag['attr'] .= " {$attr}=\"{$val}\""; return $this; } /** * Do the header call. */ public function doHeader() { header("Content-Type: text/xml"); } /** * Return the built XML string. */ public function __toString() { // whoops, we're not done building the xml document yet! if($this->indent > 0) throw new XmlPrinterException("Cannot export malformed XML document.", self::ERROR_UNFINISHED_DOCUMENT); return (string)$this->tags[0]['content']; } } /** * RSS Feed Printer. * @author Peter Goodman */ class RssFeedPrinter extends XmlPrinter { // some other helpful stuff const DATE_FORMAT = "D, d M Y H:i:s T"; private $done = FALSE, // are we done building the feed? $context = 'rss', // what is the parent tag? // obvious $required_tags = array('rss' => array('channel' => FALSE), 'channel' => array('title' => FALSE, 'link' => FALSE, 'description' => FALSE), 'item' => array()); /** * Construct and set up the rss top level tag. */ public function __construct() { parent::__construct(); $this->open('rss')->version("2.0") ->hasAttr("xmlns:atom", "http://www.w3.org/2005/Atom"); $this->context = 'rss'; } /** * Open the <channel> tag, set up the atom:link, and set the context. */ public function open_channel() { $this->open('channel'); $this->open('atom:link', XmlPrinter::TAG_NON_CLOSING) ->href(self::encodeURL())->rel("self") ->type("application/rss+xml")->close(); // if we have multiple channels in one feed foreach($this->required_tags['channel'] as $key => $val) $this->required_tags['channel'][$key] = FALSE; // set the context $this->context = 'channel'; return $this; } /** * Create the </channel> */ public function close_channel() { $this->close('channel'); $this->checkContext(); $this->context = 'rss'; return $this; } /** * Create the <item> and set the context. */ public function open_item() { $this->open('item'); $this->context = 'item'; return $this; } /** * Create the </item>. */ public function close_item() { $this->close('item'); $this->context = 'channel'; return $this; } /** * Print out the RSS. */ public function __toString() { if(!$this->done) $this->close('rss'); $this->done = TRUE; $this->context = 'rss'; $this->checkContext(); return parent::__toString(); } /** * Open a tag, if it's one of the required tags for this context then * mark it off that we've met that requirement. */ public function open($name = '', $type = parent::TAG_CLOSING) { // check off this required tag if(isset($this->required_tags[$this->context][$name])) $this->required_tags[$this->context][$name] = TRUE; return parent::open($name, $type); } /** * Check if all required tags have been used in this context and if not * throw an exception. */ public function checkContext() { $tags =& $this->required_tags[$this->context]; foreach($tags as $name => $used) { if(!$used) throw new XmlPrinterException("The tag [{$name}] is required ". "in the [{$this->context}] section."); // reset it $tags[$name] = FALSE; } } /** * Helper function to make sure URLs are nicely encoded for RSS. */ static public function encodeURL($url = NULL) { // default to the current url if($url === NULL) $url = "http://". $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]; // split up the url into its parts $url = parse_url($url); $get = array(); parse_str(isset($url['query']) ? $url['query'] : '', $get); // encode each GET parameter foreach($get as $key => $val) $get[$key] = rawurlencode($val); // rebuild the query string $url['query'] = http_build_query($get); // rebuild the url $ret = ''; $ret .= isset($url['scheme']) ? $url['scheme'] . '://' : ''; $ret .= isset($url['user']) ? $url['user'] : ''; $ret .= isset($url['pass']) ? ':'. $url['pass'] : ''; $ret .= isset($url['user']) ? '@' : ''; $ret .= isset($url['host']) ? $url['host'] : ''; $ret .= isset($url['path']) ? $url['path'] : ''; $ret .= isset($url['query']) ? $url['query'] : ''; $ret .= isset($url['fragment']) ? '#'. $url['fragment'] : ''; return $ret; } } /** * Function to create and output a basic RSS feed using a simple mapping * notation. * @author Peter Goodman */ function create_rss_feed(array $channel_map, $items, array $item_map = NULL) { // make sure we're getting something that we can loop over for $items if(!is_array($items) && !($items instanceof Traversable)) throw new UnexpectedValueException("Function [create_rss_feed] expected ". "array or iterator for RSS items."); // the default item mapping, includes all RSS 2.0 standard optional item // tags if(empty($item_map)) { $tags = array('title','link','description','author','category', 'comments','enclosure','guid','pubDate','source'); $item_map = array_combine($tags, $tags); // yes I'm lazy. } // bring in the feed builder/printer $rss = new RssFeedPrinter; $rss->open_channel(); // deal with <channel> and the top level tags in it foreach($channel_map as $tag => $value) __rss_tag($rss, $tag, $value); // deal with each <item> tag foreach($items as $item) { $rss->open_item(); // deal with tags within <item> by mapping properties/indexes in each // $item to the tags that go within the <item>. foreach($item_map as $tag => $index) { $value = NULL; // is this an object and does it have the property we're looking // for? if(is_object($item) && property_exists($item, $index)) $value = $item->$index; // is this an array, or does it have array access? if(NULL === $value && (is_array($item) || $item instanceof ArrayAccess)) if(isset($item[$index])) $value = $item[$index]; // if the value is null it means that the mapping didn't find // anything to map to in the item, so don't include this tag in // the item if(NULL === $value) continue; // deal with the tag and any special cases that need to be applied // to its values __rss_tag($rss, $tag, $value); } $rss->close_item(); } $rss->close_channel(); // output the feed $rss->doHeader(); echo $rss; } /** * Create the internal tags of an RSS feed while being aware of special cases. * @author Peter Goodman * @internal */ function __rss_tag(RssFeedPrinter &$rss, $tag, $value = '') { $rss->open($tag); // deal with special cases switch($tag) { // make sure the link query parameters are escaped properly case 'link': $rss->hasContent(RssFeedPrinter::encodeURL($value)); break; // make sure we format the date correctly case 'pubDate': if(is_string($value)) $value = strtotime($value); $rss->hasContent(date(RssFeedPrinter::DATE_FORMAT, (int)$value)); break; // assumption: this is a permalink case 'guid': $rss->isPermaLink("true") ->hasContent(RssFeedPrinter::encodeURL($value)); break; // everything else, flat assumption default: $rss->hasContent($value); break; } $rss->close(); }
Private
[
?
]
Run code