blob: dff3040e84b6fc53412fe25c2aaf444c535b50ef [file] [log] [blame]
<?php
/**
* File containing the ezcDocumentRstXhtmlVisitor class
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Document
* @version //autogen//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* HTML visitor for the RST AST.
*
* @package Document
* @version //autogen//
*/
class ezcDocumentRstXhtmlVisitor extends ezcDocumentRstVisitor
{
/**
* Mapping of class names to internal visitors for the respective nodes.
*
* @var array
*/
protected $complexVisitMapping = array(
'ezcDocumentRstSectionNode' => 'visitSection',
'ezcDocumentRstTextLineNode' => 'visitText',
'ezcDocumentRstMarkupInterpretedTextNode' => 'visitInterpretedTextNode',
'ezcDocumentRstExternalReferenceNode' => 'visitExternalReference',
'ezcDocumentRstMarkupSubstitutionNode' => 'visitSubstitutionReference',
'ezcDocumentRstTargetNode' => 'visitInlineTarget',
'ezcDocumentRstAnonymousLinkNode' => 'visitAnonymousReference',
'ezcDocumentRstBlockquoteNode' => 'visitBlockquote',
'ezcDocumentRstBulletListListNode' => 'visitBulletList',
'ezcDocumentRstEnumeratedListListNode' => 'visitEnumeratedList',
'ezcDocumentRstReferenceNode' => 'visitInternalFootnoteReference',
'ezcDocumentRstLineBlockNode' => 'visitLineBlock',
'ezcDocumentRstLineBlockLineNode' => 'visitLineBlockLine',
'ezcDocumentRstLiteralNode' => 'visitText',
'ezcDocumentRstCommentNode' => 'visitComment',
'ezcDocumentRstDefinitionListNode' => 'visitDefinitionListItem',
'ezcDocumentRstTableCellNode' => 'visitTableCell',
'ezcDocumentRstFieldListNode' => 'visitFieldListItem',
'ezcDocumentRstDirectiveNode' => 'visitDirective',
);
/**
* Direct mapping of AST node class names to docbook element names.
*
* @var array
*/
protected $simpleVisitMapping = array(
'ezcDocumentRstParagraphNode' => 'p',
'ezcDocumentRstMarkupEmphasisNode' => 'em',
'ezcDocumentRstMarkupStrongEmphasisNode' => 'strong',
'ezcDocumentRstMarkupInlineLiteralNode' => 'code',
'ezcDocumentRstBulletListNode' => 'li',
'ezcDocumentRstEnumeratedListNode' => 'li',
'ezcDocumentRstLiteralBlockNode' => 'pre',
'ezcDocumentRstTransitionNode' => 'hr',
'ezcDocumentRstDefinitionListListNode' => 'dl',
'ezcDocumentRstTableNode' => 'table',
'ezcDocumentRstTableHeadNode' => 'thead',
'ezcDocumentRstTableBodyNode' => 'tbody',
'ezcDocumentRstTableRowNode' => 'tr',
/*
'ezcDocumentRstMarkupInlineLiteralNode' => 'literal',
*/
);
/**
* Array with nodes, which can be ignored during the transformation
* process, they only provide additional information during preprocessing.
*
* @var array
*/
protected $skipNodes = array(
'ezcDocumentRstNamedReferenceNode',
'ezcDocumentRstAnonymousReferenceNode',
'ezcDocumentRstSubstitutionNode',
'ezcDocumentRstFootnoteNode',
);
/**
* DOM document
*
* @var DOMDocument
*/
protected $document;
/**
* Reference to head node
*
* @var DOMElement
*/
protected $head;
/**
* Current depth in document.
*
* @var int
*/
protected $depth = 0;
/**
* HTML rendering options
*
* @var ezcDocumentHtmlConverterOptions
*/
protected $options;
/**
* Create visitor from RST document handler.
*
* @param ezcDocumentRst $document
* @param string $path
* @return void
*/
public function __construct( ezcDocumentRst $document, $path )
{
$this->options = new ezcDocumentHtmlConverterOptions();
parent::__construct( $document, $path );
}
/**
* Property get access.
* Simply returns a given option.
*
* @throws ezcBasePropertyNotFoundException
* If a the value for the property options is not an instance of
* @param string $propertyName The name of the option to get.
* @return mixed The option value.
* @ignore
*
* @throws ezcBasePropertyNotFoundException
* if the given property does not exist.
*/
public function __get( $propertyName )
{
if ( $propertyName === 'options' );
{
return $this->options;
}
throw new ezcBasePropertyNotFoundException( $propertyName );
}
/**
* Sets an option.
* This method is called when an option is set.
*
* @param string $propertyName The name of the option to set.
* @param mixed $propertyValue The option value.
* @ignore
*
* @throws ezcBasePropertyNotFoundException
* if the given property does not exist.
* @throws ezcBaseValueException
* if the value to be assigned to a property is invalid.
* @throws ezcBasePropertyPermissionException
* if the property to be set is a read-only property.
*/
public function __set( $propertyName, $propertyValue )
{
if ( $propertyName === 'options' )
{
if ( $propertyValue instanceof ezcDocumentHtmlConverterOptions )
{
return $this->options = $propertyValue;
}
else
{
throw new ezcBaseValueException( $name, $value, 'ezcDocumentHtmlConverterOptions' );
}
}
throw new ezcBasePropertyNotFoundException( $propertyName );
}
/**
* Returns if a option exists.
*
* @param string $propertyName Option name to check for.
* @return bool Whether the option exists.
* @ignore
*/
public function __isset( $propertyName )
{
return ( $propertyName === 'options' );
}
/**
* Docarate RST AST
*
* Visit the RST abstract syntax tree.
*
* @param ezcDocumentRstDocumentNode $ast
* @return mixed
*/
public function visit( ezcDocumentRstDocumentNode $ast )
{
parent::visit( $ast );
// Create article from AST
$imp = new DOMImplementation();
$dtd = $imp->createDocumentType( 'html', '-//W3C//DTD XHTML 1.0 Transitional//EN', 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' );
$this->document = $imp->createDocument( 'http://www.w3.org/1999/xhtml', '', $dtd );
$root = $this->document->createElementNs( 'http://www.w3.org/1999/xhtml', 'html' );
$this->document->appendChild( $root );
$this->head = $this->document->createElement( 'head' );
$root->appendChild( $this->head );
// Append generator
$generator = $this->document->createElement( 'meta' );
$generator->setAttribute( 'name', 'generator' );
$generator->setAttribute( 'content', 'eZ Components; http://ezcomponents.org' );
$this->head->appendChild( $generator );
// Set content type and encoding
$type = $this->document->createElement( 'meta' );
$type->setAttribute( 'http-equiv', 'Content-Type' );
$type->setAttribute( 'content', 'text/html; charset=utf-8' );
$this->head->appendChild( $type );
$this->addStylesheets( $this->head );
$body = $this->document->createElement( 'body' );
$root->appendChild( $body );
// Visit all childs of the AST root node.
foreach ( $ast->nodes as $node )
{
$this->visitNode( $body, $node );
}
// Visit all footnotes at the document body
foreach ( $this->footnotes as $footnotes )
{
ksort( $footnotes );
$footnoteList = $this->document->createElement( 'ul' );
$footnoteList->setAttribute( 'class', 'footnotes' );
$body->appendChild( $footnoteList );
foreach ( $footnotes as $footnote )
{
$this->visitFootnote( $footnoteList, $footnote );
}
}
// Check that all required elements for a valid XHTML document exist
if ( $this->head->getElementsByTagName( 'title' )->length < 1 )
{
$title = $this->document->createElement( 'title', 'Empty document' );
$this->head->appendChild( $title );
}
return $this->document;
}
/**
* Visit single AST node
*
* Visit a single AST node, may be called for each node found anywhere
* as child. The current position in the DOMDocument is passed by a
* reference to the current DOMNode, which is operated on.
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitNode( DOMNode $root, ezcDocumentRstNode $node )
{
// Iterate over available visitors and use them to visit the nodes.
foreach ( $this->complexVisitMapping as $class => $method )
{
if ( $node instanceof $class )
{
return $this->$method( $root, $node );
}
}
// Check if we have a simple class to element name mapping
foreach ( $this->simpleVisitMapping as $class => $elementName )
{
if ( $node instanceof $class )
{
$element = $this->document->createElement( $elementName );
$root->appendChild( $element );
if ( $node->identifier !== null )
{
// $element->setAttribute( 'id', $node->identifier );
}
foreach ( $node->nodes as $child )
{
$this->visitNode( $element, $child );
}
return;
}
}
// Check if you should just ignore the node for rendering
foreach ( $this->skipNodes as $class )
{
if ( $node instanceof $class )
{
return;
}
}
// We could not find any valid visitor.
throw new ezcDocumentMissingVisitorException( get_class( $node ) );
}
/**
* Add stylesheets to header
*
* @param DOMElement $head
* @return void
*/
protected function addStylesheets( DOMElement $head )
{
if ( $this->options->styleSheets !== null )
{
foreach ( $this->options->styleSheets as $styleSheet )
{
$link = $this->document->createElement( 'link' );
$link->setAttribute( 'rel', 'Stylesheet' );
$link->setAttribute( 'type', 'text/css' );
$link->setAttribute( 'href', htmlspecialchars( $styleSheet ) );
$head->appendChild( $link );
}
}
else
{
$style = $this->document->createElement( 'style', htmlspecialchars( $this->options->styleSheet ) );
$style->setAttribute( 'type', 'text/css' );
$head->appendChild( $style );
}
}
/**
* Visit section node
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitSection( DOMNode $root, ezcDocumentRstNode $node )
{
if ( $this->depth <= 0 )
{
// Set document title from section
$title = $this->document->createElement(
'title',
htmlspecialchars( $this->nodeToString( $node->title ) )
);
$this->head->appendChild( $title );
}
$header = $this->document->createElement( 'h' . min( 6, ++$this->depth ) );
$root->appendChild( $header );
if ( $this->depth >= 6 )
{
$header->setAttribute( 'class', 'h' . $this->depth );
}
$reference = $this->document->createElement( 'a' );
$reference->setAttribute( 'name', htmlspecialchars( $node->reference ) );
$header->appendChild( $reference );
foreach ( $node->title->nodes as $child )
{
$this->visitNode( $header, $child );
}
foreach ( $node->nodes as $child )
{
$this->visitNode( $root, $child );
}
--$this->depth;
}
/**
* Helper function for URL escaping
*
* Escapes and returns the first value in a match array
*
* @param array $values
* @ignore
* @return string
*/
protected static function urlEscapeArray( array $values )
{
return urlencode( $values[0] );
}
/**
* Escape all special characters in URIs
*
* @param string $url
* @return string
*/
public function escapeUrl( $url )
{
return preg_replace_callback(
'([^a-z0-9._:/#&?@=-]+)',
array( 'ezcDocumentRstXhtmlVisitor', 'urlEscapeArray' ),
$url
);
}
/**
* Visit interpreted text node markup
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitInterpretedTextNode( DOMNode $root, ezcDocumentRstNode $node )
{
// If no role is specified, just recurse
if ( !isset( $node->role ) ||
( $node->role === false ) )
{
return $this->visitChildren( $root, $node );
}
try
{
$handlerClass = $this->rst->getRoleHandler( $node->role );
}
catch ( ezcDocumentRstMissingTextRoleHandlerException $e )
{
return $this->triggerError(
E_WARNING, $e->getMessage(),
null, $node->token->line, $node->token->position
);
}
$roleHandler = new $handlerClass( $this->ast, $this->path, $node );
if ( !$roleHandler instanceof ezcDocumentRstXhtmlTextRole )
{
return $this->triggerError(
E_WARNING, "Directive '{$handlerClass}' does not support HTML rendering.",
null, $node->token->line, $node->token->position
);
}
$roleHandler->toXhtml( $this->document, $root );
}
/**
* Visit external reference node
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitExternalReference( DOMNode $root, ezcDocumentRstNode $node )
{
if ( $node->target !== false )
{
$linkUrl = $this->escapeUrl( $node->target );
}
elseif ( ( $target = $this->getNamedExternalReference( $this->nodeToString( $node ) ) ) !== false )
{
$linkUrl = $this->escapeUrl( $target );
}
else
{
$target = $this->hasReferenceTarget( $this->nodeToString( $node ), $node );
$linkUrl = '#' . $this->escapeUrl( $target );
}
$link = $this->document->createElement( 'a' );
$link->setAttribute( 'href', $linkUrl );
$root->appendChild( $link );
foreach ( $node->nodes as $child )
{
$this->visitNode( $link, $child );
}
}
/**
* Visit internal reference node
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitInternalFootnoteReference( DOMNode $root, ezcDocumentRstNode $node )
{
$identifier = $this->tokenListToString( $node->name );
$target = $this->hasFootnoteTarget( $identifier, $node );
// The displayed label of a footnote may not be specified in
// docbook, so we just add the footnote node.
$link = $this->document->createElement( 'a', $target->number );
$link->setAttribute( 'href', '#' . $this->generateFootnoteReferenceLink( $target->name, $target->number ) );
$link->setAttribute( 'class', 'footnote' );
$root->appendChild( $link );
}
/**
* Visit inline target node
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitInlineTarget( DOMNode $root, ezcDocumentRstNode $node )
{
$link = $this->document->createElement( 'a' );
$link->setAttribute( 'name', $this->calculateId( $this->nodeToString( $node ) ) );
$root->appendChild( $link );
foreach ( $node->nodes as $child )
{
$this->visitNode( $link, $child );
}
}
/**
* Visit anonomyous reference node
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitAnonymousReference( DOMNode $root, ezcDocumentRstNode $node )
{
$target = $node->target !== false ? $node->target : $this->getAnonymousReferenceTarget();
$link = $this->document->createElement( 'a' );
$link->setAttribute( 'href', $this->escapeUrl( $target ) );
$root->appendChild( $link );
foreach ( $node->nodes as $child )
{
$this->visitNode( $link, $child );
}
}
/**
* Visit blockquotes
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitBlockquote( DOMNode $root, ezcDocumentRstNode $node )
{
$quote = $this->document->createElement( 'blockquote' );
$root->appendChild( $quote );
// Decoratre blockquote contents
foreach ( $node->nodes as $child )
{
$this->visitNode( $quote, $child );
}
// Add blockquote annotation
if ( !empty( $node->annotation ) )
{
$attribution = $this->document->createElement( 'div' );
$attribution->setAttribute( 'class', 'attribution' );
$quote->appendChild( $attribution );
$cite = $this->document->createElement( 'cite' );
$attribution->appendChild( $cite );
$this->visitChildren( $cite, $node->annotation->nodes );
}
}
/**
* Visit bullet lists
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitBulletList( DOMNode $root, ezcDocumentRstNode $node )
{
$list = $this->document->createElement( 'ul' );
$root->appendChild( $list );
$listTypes = array(
'*' => 'circle',
'+' => 'disc',
'-' => 'square',
"\xe2\x80\xa2" => 'disc',
"\xe2\x80\xa3" => 'circle',
"\xe2\x81\x83" => 'square',
);
// Not allowed in XHtml strict
// $list->setAttribute( 'type', $listTypes[$node->token->content] );
// Decoratre blockquote contents
foreach ( $node->nodes as $child )
{
$this->visitNode( $list, $child );
}
}
/**
* Visit enumerated lists
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitEnumeratedList( DOMNode $root, ezcDocumentRstNode $node )
{
$list = $this->document->createElement( 'ol' );
// Detect enumeration type
/* Not allowed in XHtml strict
switch ( true )
{
case preg_match( '(^m{0,4}d?c{0,3}l?x{0,3}v{0,3}i{0,3}v?x?l?c?d?m?$)', $node->token->content ):
$list->setAttribute( 'type', 'i' );
break;
case preg_match( '(^M{0,4}D?C{0,3}L?X{0,3}V{0,3}I{0,3}V?X?L?C?D?M?$)', $node->token->content ):
$list->setAttribute( 'type', 'I' );
break;
case preg_match( '(^[a-z]$)', $node->token->content ):
$list->setAttribute( 'type', 'a' );
break;
case preg_match( '(^[A-Z]$)', $node->token->content ):
$list->setAttribute( 'type', 'A' );
break;
}
// */
$root->appendChild( $list );
// Visit list contents
foreach ( $node->nodes as $child )
{
$this->visitNode( $list, $child );
}
}
/**
* Generate footnote reference link
*
* Generate an internal target name out of the footnote name, which may
* contain special characters, which are not allowed for URL anchors and
* are converted to alphanumeric strings by this method.
*
* @param string $name
* @param string $number
* @return string
*/
protected function generateFootnoteReferenceLink( $name, $number )
{
return '_footnote_' . str_replace(
$this->footnoteSymbols,
array(
'asterisk',
'dagger',
'double_dagger',
'section_mark',
'pilcrow',
'number_sign',
'spade',
'heart',
'diamond',
'club',
),
$name . '_' . $number
);
}
/**
* Visit footnote
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitFootnote( DOMNode $root, ezcDocumentRstNode $node )
{
$footnote = $this->document->createElement( 'li' );
$root->appendChild( $footnote );
$link = $this->document->createElement( 'a', $node->number );
$link->setAttribute( 'name', $this->generateFootnoteReferenceLink( $node->name, $node->number ) );
$footnote->appendChild( $link );
foreach ( $node->nodes as $child )
{
$this->visitNode( $footnote, $child );
}
}
/**
* Visit line block
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitLineBlock( DOMNode $root, ezcDocumentRstNode $node )
{
$block = $this->document->createElement( 'p' );
$block->setAttribute( 'class', 'lineblock' );
$root->appendChild( $block );
// Visit lines
foreach ( $node->nodes as $child )
{
foreach ( $child->nodes as $literal )
{
$block->appendChild( new DOMText(
( $literal->token->type !== ezcDocumentRstToken::NEWLINE ) ? $literal->token->content : ' '
) );
}
$break = $this->document->createElement( 'br' );
$block->appendChild( $break );
}
}
/**
* Visit line block line
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitLineBlockLine( DOMNode $root, ezcDocumentRstNode $node )
{
foreach ( $node->nodes as $child )
{
$this->visitNode( $root, $child );
}
$break = $this->document->createElement( 'br' );
$root->appendChild( $break );
}
/**
* Visit comment
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitComment( DOMNode $root, ezcDocumentRstNode $node )
{
$commentText = $this->nodeToString( $node );
$comment = new DOMComment( $commentText );
$root->appendChild( $comment );
}
/**
* Visit definition list item
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitDefinitionListItem( DOMNode $root, ezcDocumentRstNode $node )
{
$term = $this->document->createElement( 'dt', htmlspecialchars( $this->tokenListToString( $node->name ) ) );
$root->appendChild( $term );
$definition = $this->document->createElement( 'dd' );
$root->appendChild( $definition );
foreach ( $node->nodes as $child )
{
$this->visitNode( $definition, $child );
}
}
/**
* Visit table cell
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitTableCell( DOMNode $root, ezcDocumentRstNode $node )
{
$cell = $this->document->createElement( 'td' );
$root->appendChild( $cell );
if ( $node->rowspan > 1 )
{
$cell->setAttribute( 'rowspan', $node->rowspan );
}
if ( $node->colspan > 1 )
{
$cell->setAttribute( 'colspan', $node->colspan );
}
foreach ( $node->nodes as $child )
{
$this->visitNode( $cell, $child );
}
}
/**
* Visit field list item
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitFieldListItem( DOMNode $root, ezcDocumentRstNode $node )
{
$fieldName = strtolower( trim( $this->tokenListToString( $node->name ) ) );
$meta = $this->document->createElement( 'meta' );
$meta->setAttribute( 'name', htmlspecialchars( $fieldName ) );
$meta->setAttribute( 'content', htmlspecialchars( trim( $this->nodeToString( $node ) ) ) );
$this->head->appendChild( $meta );
}
/**
* Visit directive
*
* @param DOMNode $root
* @param ezcDocumentRstNode $node
* @return void
*/
protected function visitDirective( DOMNode $root, ezcDocumentRstNode $node )
{
try
{
$handlerClass = $this->rst->getDirectiveHandler( $node->identifier );
}
catch ( ezcDocumentRstMissingDirectiveHandlerException $e )
{
return $this->triggerError(
E_WARNING, $e->getMessage(),
null, $node->token->line, $node->token->position
);
}
$directiveHandler = new $handlerClass( $this->ast, $this->path, $node );
$directiveHandler->setSourceVisitor( $this );
if ( !$directiveHandler instanceof ezcDocumentRstXhtmlDirective )
{
return $this->triggerError(
E_WARNING, "Directive '{$handlerClass}' does not support HTML rendering.",
null, $node->token->line, $node->token->position
);
}
$directiveHandler->toXhtml( $this->document, $root );
}
}
?>