blob: 7b0e5a0b15f997a6428ccbca309e0f19105c0160 [file] [log] [blame]
<?php
/**
* File containing the ezcDocumentWikiParser 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//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Parser for wiki documents.
*
* @package Document
* @version //autogen//
*/
class ezcDocumentWikiParser extends ezcDocumentParser
{
/**
* Array containing simplified shift ruleset.
*
* We cannot express the Wiki syntax as a usual grammar using a BNF. With
* the pumping lemma for context free grammars [1] you can easily prove,
* that the word a^n b c^n d e^n is not a context free grammar, and this is
* what the title definitions are.
*
* This structure contains an array with callbacks implementing the shift
* rules for all tokens. There may be multiple rules for one single token.
*
* The callbacks itself create syntax elements and push them to the
* document stack. After each push the reduction callbacks will be called
* for the pushed elements.
*
* The array should look like:
* <code>
* array(
* WHITESPACE => array(
* reductionMethod,
* ...
* ),
* ...
* )
* </code>
*
* [1] http://en.wikipedia.org/wiki/Pumping_lemma_for_context-free_languages
*
* @var array
*/
protected $shifts = array(
'ezcDocumentWikiEscapeCharacterToken'
=> 'shiftEscapeToken',
'ezcDocumentWikiTitleToken'
=> 'shiftTitleToken',
'ezcDocumentWikiNewLineToken'
=> 'shiftNewLineToken',
'ezcDocumentWikiEscapeCharacterToken'
=> 'shiftEscapeToken',
'ezcDocumentWikiToken'
=> 'shiftWithTokenConversion',
);
/**
* Array containing simplified reduce ruleset.
*
* We cannot express the Wiki syntax as a usual grammar using a BNF. This
* structure implements a pseudo grammar by assigning a number of callbacks
* for internal methods implementing reduction rules for a detected syntax
* element.
*
* <code>
* array(
* ezcDocumentWikiNode::DOCUMENT => 'reduceDocument'
* ...
* )
* </code>
*
* @var array
*/
protected $reductions = array(
'ezcDocumentWikiTextNode' => array(
'reduceText',
),
'ezcDocumentWikiParagraphNode' => array(
'reduceParagraph',
),
'ezcDocumentWikiInvisibleBreakNode' => array(
'reduceLineNode',
),
'ezcDocumentWikiTitleNode' => array(
'reduceTitleToSection',
),
'ezcDocumentWikiSectionNode' => array(
'reduceLists',
'reduceSection',
),
'ezcDocumentWikiMatchingInlineNode' => array(
'reduceMatchingInlineMarkup',
),
'ezcDocumentWikiBlockquoteNode' => array(
'reduceBlockquoteNode',
),
'ezcDocumentWikiLinkEndNode' => array(
'reduceLinkNodes',
),
'ezcDocumentWikiImageEndNode' => array(
'reduceImageNodes',
),
'ezcDocumentWikiFootnoteEndNode' => array(
'reduceFootnoteNodes',
),
'ezcDocumentWikiBulletListItemNode' => array(
'reduceBulletListItem',
),
'ezcDocumentWikiEnumeratedListItemNode' => array(
'reduceEnumeratedListItem',
),
'ezcDocumentWikiTableRowNode' => array(
'reduceTableRow',
),
);
/**
* Contains a list of detected syntax elements.
*
* At the end of a successfull parsing process this should only contain one
* document syntax element. During the process it may contain a list of
* elements, which are up to reduction.
*
* Each element in the stack has to be an object extending from
* ezcDocumentRstNode, which may again contain any amount such objects.
* This way an abstract syntax tree is constructed.
*
* @var array
*/
protected $documentStack = array();
/**
* Flag if we are inside a line level node
*
* @var bool
*/
protected $insideLineToken = false;
/**
* Array with token node conversions.
*
* Token to node conversions are used for tokens, which do not require any
* additional checking of the tokens context. This is especially useful,
* because the wiki tokenizer already implement a lot of this logic.
*
* @var array
*/
protected $conversionsArray = array(
'ezcDocumentWikiEndOfFileToken' => 'ezcDocumentWikiDocumentNode',
'ezcDocumentWikiTextLineToken' => 'ezcDocumentWikiTextNode',
'ezcDocumentWikiWhitespaceToken' => 'ezcDocumentWikiTextNode',
'ezcDocumentWikiSpecialCharsToken' => 'ezcDocumentWikiTextNode',
'ezcDocumentWikiTitleToken' => 'ezcDocumentWikiTitleNode',
'ezcDocumentWikiParagraphIndentationToken' => 'ezcDocumentWikiBlockquoteNode',
'ezcDocumentWikiQuoteToken' => 'ezcDocumentWikiBlockquoteNode',
'ezcDocumentWikiPageBreakToken' => 'ezcDocumentWikiPageBreakNode',
'ezcDocumentWikiBulletListItemToken' => 'ezcDocumentWikiBulletListItemNode',
'ezcDocumentWikiEnumeratedListItemToken' => 'ezcDocumentWikiEnumeratedListItemNode',
'ezcDocumentWikiLiteralBlockToken' => 'ezcDocumentWikiLiteralBlockNode',
'ezcDocumentWikiTableRowToken' => 'ezcDocumentWikiTableRowNode',
'ezcDocumentWikiPluginToken' => 'ezcDocumentWikiPluginNode',
'ezcDocumentWikiBoldToken' => 'ezcDocumentWikiBoldNode',
'ezcDocumentWikiItalicToken' => 'ezcDocumentWikiItalicNode',
'ezcDocumentWikiUnderlineToken' => 'ezcDocumentWikiUnderlineNode',
'ezcDocumentWikiMonospaceToken' => 'ezcDocumentWikiMonospaceNode',
'ezcDocumentWikiSubscriptToken' => 'ezcDocumentWikiSubscriptNode',
'ezcDocumentWikiSuperscriptToken' => 'ezcDocumentWikiSuperscriptNode',
'ezcDocumentWikiDeletedToken' => 'ezcDocumentWikiDeletedNode',
'ezcDocumentWikiStrikeToken' => 'ezcDocumentWikiDeletedNode',
'ezcDocumentWikiInlineQuoteToken' => 'ezcDocumentWikiInlineQuoteNode',
'ezcDocumentWikiLineBreakToken' => 'ezcDocumentWikiLineBreakNode',
'ezcDocumentWikiInlineLiteralToken' => 'ezcDocumentWikiInlineLiteralNode',
'ezcDocumentWikiSeparatorToken' => 'ezcDocumentWikiSeparatorNode',
'ezcDocumentWikiTableHeaderToken' => 'ezcDocumentWikiTableHeaderSeparatorNode',
'ezcDocumentWikiExternalLinkToken' => 'ezcDocumentWikiExternalLinkNode',
'ezcDocumentWikiInterWikiLinkToken' => 'ezcDocumentWikiInterWikiLinkNode',
'ezcDocumentWikiInternalLinkToken' => 'ezcDocumentWikiInternalLinkNode',
'ezcDocumentWikiLinkStartToken' => 'ezcDocumentWikiLinkNode',
'ezcDocumentWikiLinkEndToken' => 'ezcDocumentWikiLinkEndNode',
'ezcDocumentWikiImageStartToken' => 'ezcDocumentWikiImageNode',
'ezcDocumentWikiImageEndToken' => 'ezcDocumentWikiImageEndNode',
'ezcDocumentWikiFootnoteStartToken' => 'ezcDocumentWikiFootnoteNode',
'ezcDocumentWikiFootnoteEndToken' => 'ezcDocumentWikiFootnoteEndNode',
);
/**
* Parse token stream.
*
* Parse an array of ezcDocumentWikiToken objects into a wiki abstract
* syntax tree.
*
* @param array $tokens
* @return ezcDocumentWikiDocumentNode
*/
public function parse( array $tokens )
{
/* DEBUG
echo "\n\nStart parser\n============\n\n";
// /DEBUG */
while ( ( $token = array_shift( $tokens ) ) !== null )
{
/* DEBUG
echo "[T] ({$token->line}:{$token->position}) Token: " . get_class( $token ) . " at {$token->line}:{$token->position}.\n";
// /DEBUG */
// First shift given token by the defined reduction methods
$node = false;
foreach ( $this->shifts as $class => $method )
{
if ( $token instanceof $class )
{
/* DEBUG
echo " - Handle token with ->$method\n";
// /DEBUG */
// Try to shift the token with current method
if ( ( $node = $this->$method( $token, $tokens ) ) !== false )
{
break;
}
}
}
// If the node is still null there was not matching shift rule.
if ( $node === false )
{
return $this->triggerError( E_PARSE,
"Could not find shift rule for token '" . get_class( $token ) . "'.",
$token->line, $token->position
);
}
// Token did not result in any node, it should just be ignored.
if ( $node === null )
{
continue;
}
/* DEBUG
echo "[N] Node: " . get_class( $node ) . " at {$node->token->line}:{$node->token->position}.\n";
// /DEBUG */
// Apply reductions to shifted node
do {
foreach ( $this->reductions as $class => $methods )
{
if ( $node instanceof $class )
{
foreach ( $methods as $method )
{
/* DEBUG
echo " - Handle node with ->$method\n";
// /DEBUG */
if ( ( $node = $this->$method( $node ) ) === null )
{
/* DEBUG
echo " - Reduced.\n";
// /DEBUG */
// The node has been handled, exit loop.
break 3;
}
// Check if the node class has changed and rehandle
// node in this case.
if ( !$node instanceof $class )
{
/* DEBUG
echo " - Try subsequent reductions...\n";
// /DEBUG */
continue 2;
}
}
}
}
} while ( false );
// Check if reductions have been applied, but still returned a
// node, just add to document stack in this case.
if ( $node !== null )
{
/* DEBUG
echo " => Prepend " . get_class( $node ) . " to document stack.\n";
// /DEBUG */
array_unshift( $this->documentStack, $node );
}
}
// Check if we successfully reduced the document stack
if ( ( count( $this->documentStack ) !== 1 ) ||
( !( $document = reset( $this->documentStack ) ) instanceof ezcDocumentWikiDocumentNode ) )
{
$node = isset( $document ) ? $document : reset( $this->documentStack );
$this->triggerError(
E_PARSE,
'Expected end of file, got: ' . get_class( $this->documentStack[1] ) . ".",
$this->documentStack[1]->token->line, $this->documentStack[1]->token->position
);
}
return $document;
}
/**
* Shift escape token.
*
* Escape tokens will cause that the following token is ignored in his
* common meaning. The following token is converted to plain text, while
* the escape token will be removed.
*
* @param ezcDocumentWikiToken $token
* @param array $tokens
* @return mixed
*/
protected function shiftEscapeToken( ezcDocumentWikiToken $token, array &$tokens )
{
// If there is nothing to escape, shift as text node
if ( !isset( $tokens[0] ) ||
( $tokens[0] instanceof ezcDocumentWikiWhitespaceToken ) ||
( $tokens[0] instanceof ezcDocumentWikiNewLineToken ) )
{
return new ezcDocumentWikiTextNode( $token );
}
// Otherwise shift the following token as text node
$token = array_shift( $tokens );
return new ezcDocumentWikiTextNode( $token );
}
/**
* Shift title token.
*
* Some wiki markup languages use a second title token at the end of the
* line instead of just a line break. In the case we are already inside a
* line token, just shift an invisible line break.
*
* @param ezcDocumentWikiToken $token
* @param array $tokens
* @return mixed
*/
protected function shiftTitleToken( ezcDocumentWikiToken $token, array &$tokens )
{
if ( $this->insideLineToken )
{
// If the title token is already the one in the next line reprepend
// it to the token list.
if ( $token->position === 0 )
{
array_unshift( $tokens, $token );
}
$this->insideLineToken = false;
return new ezcDocumentWikiInvisibleBreakNode( $token );
}
return false;
}
/**
* Shift new line token.
*
* Paragraphs are always indicated by multiple new line tokens. When
* detected we just shift a paragraph node, which the will be reduced with
* prior inline nodes.
*
* @param ezcDocumentWikiToken $token
* @param array $tokens
* @return mixed
*/
protected function shiftNewLineToken( ezcDocumentWikiToken $token, array &$tokens )
{
// Wiki markup knows a lot of markup, which is limited to one line. If
// a token starting su a line the $insideLineToken flag is set true and
// we shift an end marker to the stack for a single new line.
if ( $this->insideLineToken )
{
/* DEBUG
echo " -> End of line markup.\n";
// /DEBUG */
$this->insideLineToken = false;
return new ezcDocumentWikiInvisibleBreakNode( $token );
}
// Shift a single newline as a paragrapoh, if the following token is a
// block level token.
if ( isset( $tokens[0] ) &&
( $tokens[0] instanceof ezcDocumentWikiBlockMarkupToken ) &&
isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) )
{
/* DEBUG
echo " -> End of paragraph, because of block level token.\n";
// /DEBUG */
return new ezcDocumentWikiParagraphNode( $token );
}
// Only shift a paragraph node, if there are multiple new lines, and if
// there is already inline markup on the document stack.
if ( isset( $tokens[0] ) &&
( $tokens[0] instanceof ezcDocumentWikiNewLineToken ) &&
isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) )
{
// Remove all subsequent new line tokens.
do {
array_shift( $tokens );
} while ( isset( $tokens[0] ) &&
( $tokens[0] instanceof ezcDocumentWikiNewLineToken ) );
/* DEBUG
echo " -> End of paragraph, because of multiple newlines.\n";
// /DEBUG */
return new ezcDocumentWikiParagraphNode( $token );
}
// Return all other newlines as text nodes - they may be whitespaces
// required in text, if we are inside a paragraph node
if ( isset( $this->documentStack[0] ) &&
( ( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) ||
( $this->documentStack[0] instanceof ezcDocumentWikiParagraphNode ) ) )
{
return new ezcDocumentWikiTextNode( $token );
}
// Otherwise just ommit newline
return null;
}
/**
* Shift with token conversion.
*
* Token to node conversions are used for tokens, which do not require any
* additional checking of the tokens context. This is especially useful,
* because the wiki tokenizer already implement a lot of this logic.
*
* The actual conversions are specified in the class property
* $conversionsArray.
*
* @param ezcDocumentWikiToken $token
* @param array $tokens
* @return mixed
*/
protected function shiftWithTokenConversion( ezcDocumentWikiToken $token, array &$tokens )
{
foreach ( $this->conversionsArray as $tokenClass => $nodeClass )
{
if ( $token instanceof $tokenClass )
{
if ( $token instanceof ezcDocumentWikiLineMarkupToken )
{
$this->insideLineToken = true;
}
/* DEBUG
echo " -> Converted to $nodeClass (" . ( (int) $this->insideLineToken ) . ")\n";
// /DEBUG */
return new $nodeClass( $token );
}
}
return false;
}
/**
* Reduce text nodes.
*
* Reduce texts into single nodes, if the prior node is also a text node.
* This reduces the number of AST nodes required to represent texts
* drastically.
*
* @param ezcDocumentWikiTextNode $node
* @return mixed
*/
protected function reduceText( ezcDocumentWikiTextNode $node )
{
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiTextNode ) )
{
$this->documentStack[0]->token->content .= $node->token->content;
return null;
}
return $node;
}
/**
* Reduce paragraph.
*
* Paragraphs are reduce with all inline tokens, which have been added to
* the document stack before. If there are no inline nodes, the paragraph
* will be ommitted.
*
* @param ezcDocumentWikiParagraphNode $node
* @return mixed
*/
protected function reduceParagraph( ezcDocumentWikiParagraphNode $node )
{
// Collect inline nodes
$collected = array();
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) )
{
$inlineNode = array_shift( $this->documentStack );
// Convert markup nodes without matching equivalent or out of the
// normal context to text nodes.
if ( ( ( $inlineNode instanceof ezcDocumentWikiMatchingInlineNode ) &&
( $inlineNode->nodes === array() ) ) ||
( ( $inlineNode instanceof ezcDocumentWikiSeparatorNode ) ) ||
( ( $inlineNode instanceof ezcDocumentWikiImageNode ) &&
( $inlineNode->resource === array() ) ) ||
( ( $inlineNode instanceof ezcDocumentWikiFootnoteNode ) &&
( $inlineNode->nodes === array() ) ) ||
( ( $inlineNode instanceof ezcDocumentWikiLinkNode ) &&
( $inlineNode->link === array() ) ) )
{
$inlineNode = new ezcDocumentWikiTextNode( $inlineNode->token );
}
array_unshift( $collected, $inlineNode );
}
if ( !count( $collected ) )
{
// No tokens found, we can ommit the paragraph.
return null;
}
$node->nodes = array_values( $collected );
if ( isset( $this->documentStack[0] ) &&
( ( $this->documentStack[0] instanceof ezcDocumentWikiBulletListItemNode ) ||
( $this->documentStack[0] instanceof ezcDocumentWikiEnumeratedListItemNode ) ) &&
( $this->documentStack[0]->nodes === array() ) )
{
$paragraph = $node;
$node = array_shift( $this->documentStack );
$node->nodes[] = $paragraph;
}
return $node;
}
/**
* Reduce prior sections, if a new section has been found.
*
* If a new section has been found all sections with a higher depth level
* can be closed, and all items fitting into sections may be aggregated by
* the respective sections as well.
*
* @param ezcDocumentWikiSectionNode $node
*/
protected function reduceSection( ezcDocumentWikiSectionNode $node )
{
// Collected node for prior section
$collected = array();
$lastSectionLevel = -1;
// Include all paragraphs, tables, lists and sections with a higher
// nesting depth
while ( $child = array_shift( $this->documentStack ) )
{
/* DEBUG
echo " -> Try node: " . get_class( $child ) . ".\n";
// /DEBUG */
if ( !$child instanceof ezcDocumentWikiBlockLevelNode )
{
$this->triggerError(
E_PARSE,
"Unexpected node: " . get_class( $child ) . ".",
null, $child->token->line, $child->token->position
);
}
if ( $child instanceof ezcDocumentWikiSectionNode )
{
if ( $child->level <= $node->level )
{
$child->nodes = array_merge(
$child->nodes,
$collected
);
// If the found section has a same or higher level, just
// put it back on the stack
array_unshift( $this->documentStack, $child );
/* DEBUG
echo " -> Leave on stack.\n";
// /DEBUG */
return $node;
}
if ( ( $lastSectionLevel - $child->level ) > 1 )
{
$this->triggerError(
E_NOTICE,
"Title depth inconsitency.",
null, $child->token->line, $child->token->position
);
}
if ( ( $lastSectionLevel === -1 ) ||
( $lastSectionLevel > $child->level ) )
{
// If the section level is higher then in our new node and
// lower the the last node, reduce sections.
/* DEBUG
echo " -> Reduce section {$child->level}.\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
$collected
);
$collected = array();
}
// Sections on an equal level are just appended, for all
// sections we remember the last depth.
$lastSectionLevel = $child->level;
}
array_unshift( $collected, $child );
}
$node->nodes = array_merge(
$node->nodes,
$collected
);
return $node;
}
/**
* Reduce all elements to one document node.
*
* @param ezcDocumentWikiTitleNode $node
*/
protected function reduceTitleToSection( ezcDocumentWikiTitleNode $node )
{
if ( $node->nodes === array() )
{
// Title node has no content yet, skip for now.
return $node;
}
// Prepend section element to document stack
$section = new ezcDocumentWikiSectionNode( $node->token );
$section->nodes = array( $node );
return $section;
}
/**
* Reduce matching inline markup.
*
* Reduction rule for inline markup which is intended to have a matching
* counterpart in the same block level element.
*
* @param ezcDocumentWikiMatchingInlineNode $node
* @return mixed
*/
protected function reduceMatchingInlineMarkup( ezcDocumentWikiMatchingInlineNode $node )
{
// Collect inline nodes
$collected = array();
$class = get_class( $node );
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) &&
( ( !$this->documentStack[0] instanceof $class ) ||
( $this->documentStack[0]->nodes !== array() ) ) )
{
array_unshift( $collected, array_shift( $this->documentStack ) );
}
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof $class ) &&
( $this->documentStack[0]->nodes === array() ) )
{
// We found an empty matching node. Reduce
$markupNode = array_shift( $this->documentStack );
$markupNode->nodes = $collected;
return $markupNode;
}
else
{
// No matching node found, just leave it on the stack.
$this->documentStack = array_merge( array_reverse( $collected ), $this->documentStack );
return $node;
}
}
/**
* Reduce line node.
*
* Line nodes are closed at the end of their respective line. The end is
* marked by an ezcDocumentWikiInvisibleBreakNode.
*
* @param ezcDocumentWikiInvisibleBreakNode $node
* @return mixed
*/
protected function reduceLineNode( ezcDocumentWikiInvisibleBreakNode $node )
{
// Collect inline nodes
/* DEBUG
echo " -> Find childs for line level markup:";
// /DEBUG */
$collected = array();
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) )
{
array_unshift( $collected, array_shift( $this->documentStack ) );
/* DEBUG
echo ".";
// /DEBUG */
}
/* DEBUG
echo " " . count( $collected ) . " found.\n";
// /DEBUG */
if ( count( $collected ) &&
isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiLineLevelNode ) )
{
$lineNode = array_shift( $this->documentStack );
$lineNode->nodes = $collected;
/* DEBUG
echo " => Reduce line node: " . get_class( $lineNode ) . "\n";
// /DEBUG */
return $lineNode;
}
// No tokens found, we can ommit the break node, and put everything
// back on the document stack
/* DEBUG
echo " => Nothing found (", get_class( $this->documentStack[0] ), ") - line break ommitted.\n";
// /DEBUG */
$this->documentStack = array_merge( array_reverse( $collected ), $this->documentStack );
return null;
}
/**
* Reduce wiki links.
*
* Reduce links with all of their aggregated parameters.
*
* @param ezcDocumentWikiLinkEndNode $node
* @return mixed
*/
protected function reduceLinkNodes( ezcDocumentWikiLinkEndNode $node )
{
// Collect inline nodes
$parameters = array( array() );
$collected = array();
$parameter = 0;
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) &&
( !$this->documentStack[0] instanceof ezcDocumentWikiLinkNode ) )
{
$child = array_shift( $this->documentStack );
$collected[] = $child;
if ( $child instanceof ezcDocumentWikiSeparatorNode )
{
$parameters[++$parameter] = array();
}
else
{
array_unshift( $parameters[$parameter], $child );
}
}
// We could not find a corresponding start element, put everything back
// on stack and convert node into plain text
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiLinkNode ) )
{
$this->documentStack = array_merge( $collected, $this->documentStack );
return new ezcDocumentWikiTextNode( $node->token );
}
// Reverse parameter order
$parameters = array_reverse( $parameters );
$linkStart = array_shift( $this->documentStack );
$parameter = $linkStart->token->getLinkParameterOrder( count( $parameters ) );
foreach ( $parameter as $nr => $name )
{
$linkStart->$name = $parameters[$nr];
}
return $linkStart;
}
/**
* Reduce wiki image references.
*
* Reduce image references with all of their aggregated parameters.
*
* @param ezcDocumentWikiImageEndNode $node
* @return mixed
*/
protected function reduceImageNodes( ezcDocumentWikiImageEndNode $node )
{
// Collect inline nodes
$parameters = array( array() );
$collected = array();
$parameter = 0;
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) &&
( !$this->documentStack[0] instanceof ezcDocumentWikiImageNode ) )
{
$child = array_shift( $this->documentStack );
$collected[] = $child;
if ( $child instanceof ezcDocumentWikiSeparatorNode )
{
$parameters[++$parameter] = array();
}
else
{
array_unshift( $parameters[$parameter], $child );
}
}
// We could not find a corresponding start element, put everything back
// on stack and convert node into plain text
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiImageNode ) )
{
$this->documentStack = array_merge( $collected, $this->documentStack );
return new ezcDocumentWikiTextNode( $node->token );
}
// Reverse parameter order
$parameters = array_reverse( $parameters );
$linkStart = array_shift( $this->documentStack );
// Apply token parameters, which may be overwritten by parameters
// detected from parser
$linkStart->alignement = $linkStart->token->alignement;
$linkStart->width = $linkStart->token->width;
$linkStart->height = $linkStart->token->height;
$parameter = $linkStart->token->getImageParameterOrder( count( $parameters ) );
foreach ( $parameter as $nr => $name )
{
$linkStart->$name = $parameters[$nr];
}
return $linkStart;
}
/**
* Reduce wiki footnotes.
*
* Reduce inline footnotes
*
* @param ezcDocumentWikiFootnoteEndNode $node
* @return mixed
*/
protected function reduceFootnoteNodes( ezcDocumentWikiFootnoteEndNode $node )
{
// Collect inline nodes
$collected = array();
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) &&
( !$this->documentStack[0] instanceof ezcDocumentWikiFootnoteNode ) )
{
array_unshift( $collected, array_shift( $this->documentStack ) );
}
// We could not find a corresponding start element, put everything back
// on stack and convert node into plain text
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiFootnoteNode ) )
{
$this->documentStack = array_merge( array_reverse( $collected ), $this->documentStack );
return new ezcDocumentWikiTextNode( $node->token );
}
// Reverse parameter order
$footnote = array_shift( $this->documentStack );
$footnote->nodes = array_reverse( $collected );
return $footnote;
}
/**
* Reduce multiline blockquote nodes.
*
* Reduce multline block quote nodes, which are not already closed by line
* endings.
*
* @param ezcDocumentWikiBlockquoteNode $node
* @return mixed
*/
protected function reduceBlockquoteNode( ezcDocumentWikiBlockquoteNode $node )
{
// Collect inline nodes
$collected = array();
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiInlineNode ) &&
( ( !$this->documentStack[0] instanceof ezcDocumentWikiBlockquoteNode ) ||
( $this->documentStack[0]->nodes !== array() ) ) )
{
array_unshift( $collected, array_shift( $this->documentStack ) );
}
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0] instanceof ezcDocumentWikiBlockquoteNode ) &&
( $this->documentStack[0]->nodes === array() ) )
{
// We found an empty matching node. Reduce
$blockquote = array_shift( $this->documentStack );
$blockquote->nodes = $collected;
return $blockquote;
}
else
{
// No matching node found, just leave it on the stack.
$this->documentStack = array_merge( array_reverse( $collected ), $this->documentStack );
return $node;
}
}
/**
* Reduce bullet list items to list.
*
* Reduce list items to lists, and create new wrapping list nodes.
*
* @param ezcDocumentWikiBlockLevelNode $node
* @return mixed
*/
protected function reduceBulletListItem( ezcDocumentWikiBlockLevelNode $node )
{
// Do not reduce empty bullet list nodes, but wait until they are
// filled
if ( $node->nodes === array() )
{
return $node;
}
// If there is not already a list node which matches the properties of
// the current list item, create a new bullet list.
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiBulletListNode ) ||
( $this->documentStack[0]->level !== $node->level ) )
{
$list = new ezcDocumentWikiBulletListNode( $node->token );
}
else
{
$list = array_shift( $this->documentStack );
}
$list->nodes = array_merge( $list->nodes, array( $node ) );
return $list;
}
/**
* Reduce enumerated list items to list.
*
* Reduce list items to lists, and create new wrapping list nodes.
*
* @param ezcDocumentWikiBlockLevelNode $node
* @return mixed
*/
protected function reduceEnumeratedListItem( ezcDocumentWikiBlockLevelNode $node )
{
// Do not reduce empty enumerated list nodes, but wait until they are
// filled
if ( $node->nodes === array() )
{
return $node;
}
// If there is not already a list node which matches the properties of
// the current list item, create a new bullet list.
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiEnumeratedListNode ) ||
( $this->documentStack[0]->level !== $node->level ) )
{
$list = new ezcDocumentWikiEnumeratedListNode( $node->token );
}
else
{
$list = array_shift( $this->documentStack );
}
$list->nodes = array_merge( $list->nodes, array( $node ) );
return $list;
}
/**
* Merge lists recusively.
*
* Merge lists recusively
*
* @param array $lists
* @return ezcDocumentWikiListNode
*/
protected function mergeListRecursively( array $lists )
{
$list = array_shift( $lists );
$collected = array();
while ( $child = array_shift( $lists ) )
{
if ( $child->level > $list->level )
{
array_unshift( $collected, $child );
}
else
{
if ( count( $collected ) )
{
$list->nodes[] = $this->mergeListRecursively( $collected );
$collected = array();
}
$list->nodes = array_merge(
$list->nodes,
$child->nodes
);
}
}
return $list;
}
/**
* Reduce lists.
*
* Stack lists with higher indentation into each other and merge multiple
* lists of same type and indentation.
*
* @param ezcDocumentWikiBlockLevelNode $node
* @return mixed
*/
protected function reduceLists( ezcDocumentWikiBlockLevelNode $node )
{
$collected = array();
$documentStack = array();
$class = null;
$lastIndentation = 1;
while ( $child = array_shift( $this->documentStack ) )
{
/* DEBUG
echo " -> Found ", get_class( $child ), " - current indentation: $lastIndentation.\n";
// /DEBUG */
// Clean up, on:
// - List nodes have already be found, AND
// - Not a list node
// - A list node of a different type on the same or lower level
if ( ( !$child instanceof ezcDocumentWikiListNode ) ||
( ( $class !== null ) &&
( ( !$child instanceof ezcDocumentWikiListNode ) ||
( ( !$child instanceof $class ) &&
( $child->level <= $lastIndentation ) ) ) ) )
{
if ( count( $collected ) )
{
/* DEBUG
echo " -> Merge lists: ", count( $collected ), " entries.\n";
// /DEBUG */
$documentStack[] = $this->mergeListRecursively( $collected );
$class = null;
$collected = array();
}
if ( !$child instanceof ezcDocumentWikiListNode )
{
/* DEBUG
echo " -> Skip element.\n";
// /DEBUG */
$documentStack[] = $child;
continue;
}
}
// Collect all belonging lists
if ( ( $class === null ) ||
( $child instanceof $class ) )
{
$class = get_class( $child );
$lastIndentation = $child->level;
}
array_unshift( $collected, $child );
/* DEBUG
echo " -> Collect element ($class, $lastIndentation).\n";
// /DEBUG */
}
// Merge, when end of document is reached
if ( count( $collected ) )
{
/* DEBUG
echo " -> Merge lists: ", count( $collected ), " entries.\n";
// /DEBUG */
$documentStack[] = $this->mergeListRecursively( $collected );
}
$this->documentStack = $documentStack;
return $node;
}
/**
* Reduce table rows.
*
* Reduce the nodes aagregated for one table row into table cells, and
* merge the table rows into table nodes.
*
* @param ezcDocumentWikiTableRowNode $node
* @return mixed
*/
protected function reduceTableRow( ezcDocumentWikiTableRowNode $node )
{
// We only care about table rows which already have some contents
// assigned.
if ( $node->nodes === array() )
{
return $node;
}
$cells = array();
$separators = array();
$cell = ( $node->nodes[0] instanceof ezcDocumentWikiSeparatorNode ) ? -1 : 0;
foreach ( $node->nodes as $child )
{
if ( $child instanceof ezcDocumentWikiSeparatorNode )
{
$separators[++$cell] = $child;
continue;
}
$cells[$cell][] = $child;
}
// Transform aggregated contents in cell nodes
foreach ( $cells as $nr => $cellNodes )
{
$cells[$nr] = new ezcDocumentWikiTableCellNode( $separators[$nr]->token );
$cells[$nr]->nodes = $cellNodes;
}
$node->nodes = $cells;
// Merge the table row into a table.
if ( !isset( $this->documentStack[0] ) ||
( !$this->documentStack[0] instanceof ezcDocumentWikiTableNode ) )
{
$table = new ezcDocumentWikiTableNode( $node->token );
}
else
{
$table = array_shift( $this->documentStack );
}
$table->nodes[] = $node;
return $table;
}
}
?>