* @license PHP License
* @package wb
* @subpackage Markup
*/
WBClass::load('WBString'
, 'WBMarkup_Handler_Xml2Wxml');
/**
* Markup Scanner Handler: XML2HTML
*
* Convert Wombat XML data to HTML
*
* @version 0.5.1
* @package wb
* @subpackage Markup
*/
class WBMarkup_Handler_Xml2Html extends WBMarkup_Handler_Xml2Wxml
{
/**
* actual wxml config
* @var WBConfig
*/
private $config;
/**
* List of WBMarkup_Listener
* @var array
*/
private $listener = array();
/**
* template engine
* @var patTemplate
*/
protected $tmpl;
/**
* template dir
* @var string
*/
private $tmplDir = '';
/**
* constructor
*
* Load config
*/
public function __construct()
{
$this->config = WBClass::create('WBConfig');
$this->config->load('wxml/config');
$this->initTmplEngine();
}
/**
* override default template dir
*
* @param string $dir
*/
public function setTmplDir($dir)
{
if (empty($dir)) {
return;
}
$this->tmplDir = $dir;
$this->initTmplEngine($dir);
}
/**
* Initialize Template Engine
*
* Load default convert if available
* @param string $dir
*/
private function initTmplEngine($dir = '')
{
if (empty($dir)) {
$dir = '__default';
}
$base = WBParam::get('wb/dir/base');
$tmpl = 'Wxml/converter/' . trim($dir, '/') . $this->getPatTemplateSuffix();;
if (!$this->tmpl) {
$this->tmpl = $this->createPatTemplate();
}
$this->tmpl->readTemplatesFromInput($tmpl);
}
/**
* Get patTemplate object
*
* @return patTemplate
*/
protected function createPatTemplate()
{
return WBClass::create('patTemplate');
}
/**
* Get File Name Suffix
*
* @return string
*/
protected function getPatTemplateSuffix()
{
return '.tmpl';
}
/**
* handler on start beginning of scan
*
*
*
* @param string $content
* @return bool usually true, false to stop the scanner
*/
public function onScanStart(&$content)
{
if (!parent::onScanStart($content)) {
return false;
}
foreach ($this->listener as $l) {
$l->onScanStart($content);
}
return true;
}
/**
* add tag listener
*
* @param WBMarkup_Listener $l
*/
public function addListener($l)
{
$l->onScanStart($this->content);
$this->listener[] = $l;
}
/**
* handler on start element
*
* @param string $ns The used namespace, if set. otherwise null
* @param string $tag the tag itself
* @param array $attributes the attributes of the found element
* @param bool $isEmpty true if it is a start- and closing-Element (e.g.
)
* @return bool usually true, false to stop the scanner
*/
public function onStartElement($ns, $tag, $attributes, $isEmpty)
{
if (!parent::onStartElement($ns, $tag, $attributes, $isEmpty)) {
return false;
}
foreach ($this->listener as $l) {
$l->onStartElement($this->doc[$this->depth]);
}
return true;
}
/**
* handler fo cData
*
* Simply add cDate to document
*
* Copied regular expression to find and replace URL from
* {@link http://snipplr.com/view/6889/regular-expressions-for-uri-validationparsing/}
* and removed parentheses
*
* @uses replaceUrlString()
* @param string $cData character data
* @return bool usually true, false to stop the scanner
*/
public function onCharacterData($cData)
{
// find and replace URL strings
if (strstr($cData, '://')) {
$regex = '/(https?|ftp):\\/\\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\\d+))?((?:\\/(?:[a-z0-9-._~!$&\'*+,;=:@]|%[0-9A-F]{2})*)*)(?:\\?((?:[a-z0-9-._~!$&\'*+,;=:\\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'*+,;=:\\/?@]|%[0-9A-F]{2})*))?/i';
$cData = preg_replace_callback($regex
, array($this, 'replaceUrlString')
, $cData);
}
return parent::onCharacterData($cData);
}
/**
* test handler on end element
*
* @param string $ns The used namespace, if set. otherwise null
* @param string $tag the tag itself
* @param bool $empty defines if the tag is empty
* @return bool usually true, false to stop the scanner
*/
public function onEndElement($ns, $tag, $empty)
{
// serialize node
if( !$this->depth ) {
return true;
}
$node = array_pop($this->doc);
// inform listener
foreach ($this->listener as $l) {
$l->onEndElement($node);
}
if ($node['ns']) {
$this->convert($node['ns'], $node['tag'], $node);
} else {
$this->convertDefault($node['tag'], $node);
}
--$this->depth;
if( $this->depth < 0 ) {
$this->depth = 0;
}
$this->doc[$this->depth]['cData'][] = $this->node2String($node);
return true;
}
/**
* called right after scan is complete
*
* @return bool usually true, false to stop the scanner
*/
public function onScanComplete()
{
$this->content = $this->node2String($this->doc[0]);
// inform listener
foreach ($this->listener as $l) {
$l->onScanComplete($this->content);
}
// inform converter
foreach (array_keys($this->con) as $con) {
$this->con[$con]->reset();
}
// flush listener
$this->listener = array();
return true;
}
/**
* convert node
*
* transform namespaced tags to HTML
*
* @todo convert empty namespace (standard HTML) to known WB namespace, if possible
* @param string $ns
* @param string $tag
* @param array $node
*/
protected function convert($ns, $tag, &$node)
{
$con = $this->getConverter($ns, $tag);
if (!$con) {
// remove tag
$html = array(
'ns' => null,
'tag' => '',
'attributes' => array(),
'isEmpty' => false,
'cData' => $node['cData']
);
$node = $html;
return;
}
if (!empty($this->tmplDir)) {
$con->setTmplDir($this->tmplDir);
}
return $con->toHtml($node);
}
/**
* convert node of default namespace
*
* @param string $tag
* @param array $node
*/
protected function convertDefault($tag, &$node)
{
if (!$this->tmpl || !$this->tmpl->exists('snippet_' . $tag)) {
$this->handleBr($node);
$this->insertAutoLink($node);
return;
}
$html = array(
'ns' => null,
'tag' => '',
'attributes' => array(),
'isEmpty' => false,
'cData' => array()
);
$cData = implode('', $node['cData']);
$this->tmpl->clearTemplate('snippet_' . $tag);
$this->tmpl->addVars('snippet_' . $tag, $node['attributes'], 'ATT_');
$this->tmpl->addVar('snippet_' . $tag, 'CDATA', $cData);
$html['cData'][] = $this->tmpl->getParsedTemplate('snippet_' . $tag);
$node = $html;
}
/**
* Handle BR
*
* Either strip or keep BR-tags
*
* @param array $node
*/
private function handleBr(&$node)
{
if (!empty($node['ns']) || 'br' != $node['tag']) {
return;
}
$action = $this->config->get('html/br/convert', 'keep');
switch (strtolower($action)) {
case 'strip':
$node = array(
'ns' => null,
'tag' => '',
'attributes' => array(),
'isEmpty' => false,
'cData' => array()
);
break;
default:
case 'keep':
break;
}
}
/**
* add auto links for each headline
*
* See in config whether to add a link to this tag.
*
* @param array $node
*/
private function insertAutoLink(&$node)
{
// only headline tags h1-h6
if (!empty($node['ns']) || !in_array($node['tag'], array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
return;
}
$tags = $this->config->get('html/heading/autolink/tag', array());
$title = $this->config->get('html/heading/autolink/title', 'Link to paragraph %s');
if (empty($tags)) {
return;
}
if (!in_array($node['tag'], $tags)) {
return;
}
$cData = $node['cData'];
$cData = implode("\n", $cData);
$cData = strip_tags($cData);
$cData = str_replace(' ', ' ', $cData);
$cData = trim($cData);
$title = sprintf($title, $cData);
$cData = substr($cData, 0, 40);
$cData = urlencode($cData);
array_unshift($node['cData'], '');
$class = $this->config->get('html/heading/autolink/class', 'wxml paragraph');
$cd = $this->config->get('html/heading/autolink/cdata', '¶');
array_push($node['cData'], sprintf('%s', $class, $title, $cData, $cd));
}
/**
* replace URL string with propper node
*
* @see preg_replace_callback()
* @param array $match
* @return string
*/
public function replaceUrlString($match)
{
// convert URL to node
$atts = array(
'type' => 'external',
'href' => $match[1] . '://' . $match[2] . $match[4],
'title' => ''
);
// parameter?
if (isset($match[5])) {
$atts['href'] .= '?' . $match[5];
}
$atts['title'] = urldecode($atts['href']);
$this->onStartElement('wb', 'a', $atts, false);
$this->doc[$this->depth]['cData'][] = $atts['title'];
$this->onEndElement('wb', 'a', false);
return array_pop($this->doc[$this->depth]['cData']);
}
}