blob: 51c552679657b7f4ec3b52ffd09e87edd49249b0 [file] [log] [blame]
<?php
/**
* File containing the ezcDocumentRstParser
*
* 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 RST documents
*
* RST is not describable by a context free grammar, so that the common parser
* approaches won't work.
*
* Parser basics
* -------------
*
* We decided to implement a parser roughly following the scheme of common
* shift reduce parsers with a dynamic lookahead.
*
* - Shifting:
*
* The shift step commonly tries to convert a token or a sequence of tokens
* to the respective AST node. In the case of RST we may need a dynamic
* lookahead to decide which type of AST node a token relates to, which is
* different from common LRn parsers.
*
* There is a map of parser tokens to internal methods for callbacks, which
* are called in the defined order if the main parser methods reach the
* respective token in the provided token array. Each shift method is called
* with the relating token and the array of subsequent, yet unhandled,
* tokens.
*
* These methods are expected to return either false, if the current token
* cannot be shifted by the called rule, true, when the token has been
* handled, but no document node has been created from it or a new
* ezcDocumentRstNode object, which is some AST node. When a shift method
* returned false the next shift method in the array is called to handle the
* token.
*
* The returned ezcDocumentRstNode objects are put on the document stack in
* the order they are found in the token array.
*
* - Reducing:
*
* The reduce step commonly tries to reduce matching structures, like finding
* the matching opening brace, when a closing brace has been added to the
* document stack. In this case all nodes between the two braces are
* aggregated into the brace node, so that a tree is created.
*
* The reductions array defines an array with a mapping of node types to
* rection callbacks, which are called if such a node has been added to the
* document stack. Each reduction method may either return false, if it could
* not handle the given node, or a new node. The reduction methods often
* manipulate the document stack, like searching backwards and aggregating
* nodes.
*
* If a reduction method returns a node the parser reenters the reduction
* process with the new node.
*
* The state of the RST parser heavily depends on the current indentation
* level, which is stored in the class property $indentation, and mainly
* modified in the special shift method updateIndentation(), which is called on
* each line break token.
*
* Some of the shift methods aggregate additional tokens from the token array,
* bypassing the main parser method. This should only be done, if no common
* handling is required for the aggregated tokens.
*
* Tables
* ------
*
* The handling of RST tables is quite complex and the affiliation of tokens to
* nodes depend on the line and character position of the token. In this case
* the tokens are first aggregated into their cell contexts and reenter the
* parser afterwards.
*
* For token lists, which are required to reenter the parser - independently
* from the current global parser state - the method reenterParser() takes such
* token lists, removes the overall indentation and returns a new document of
* the provided token array.
*
* @package Document
* @version //autogen//
*/
class ezcDocumentRstParser extends ezcDocumentParser
{
/**
* Current indentation of a paragraph / lsit item.
*
* @var int
*/
protected $indentation = 0;
/**
* For the special case of dense bullet lists we need to update the
* indetation right after we created a new paragraph in one action. We
* store the indetation to update past the paragraph creation in this case
* in this variable.
*
* @var int
*/
protected $postIndentation = null;
/**
* Array containing simplified shift ruleset
*
* We cannot express the RST 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(
ezcDocumentRstToken::WHITESPACE => array(
// This should always be the last rule in this section: We shift
// the whitespace, which could not be recognized as something else,
// as text.
'shiftWhitespaceAsText',
),
ezcDocumentRstToken::NEWLINE => array(
'shiftParagraph',
'updateIndentation',
'shiftAsWhitespace',
),
ezcDocumentRstToken::BACKSLASH => array(
'shiftBackslash',
),
ezcDocumentRstToken::SPECIAL_CHARS => array(
'shiftTitle',
'shiftTransition',
'shiftLineBlock',
'shiftInlineLiteral',
'shiftInlineMarkup',
'shiftInterpretedTextMarkup',
'shiftReference',
'shiftAnonymousHyperlinks',
'shiftExternalReference',
'shiftBlockquoteAnnotation',
'shiftBulletList',
'shiftEnumeratedList',
'shiftLiteralBlock',
'shiftComment',
'shiftAnonymousReference',
'shiftFieldList',
'shiftSimpleTable',
'shiftGridTable',
// This should always be the last rule in this section: We shift
// special character groups, which could not be recognized as
// something else, as text.
'shiftSpecialCharsAsText',
),
ezcDocumentRstToken::TEXT_LINE => array(
'shiftEnumeratedList',
'shiftText',
),
ezcDocumentRstToken::EOF => array(
'shiftDocument',
),
);
/**
* Array containing simplified reduce ruleset
*
* We cannot express the RST 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(
* ezcDocumentRstNode::DOCUMENT => 'reduceDocument'
* ...
* )
* </code>
*
* @var array
*/
protected $reductions = array(
// Also for ezcDocumentRstNode::SECTION, since the constants point to
// the same value.
ezcDocumentRstNode::DOCUMENT => array(
'reduceList',
'reduceSection',
),
ezcDocumentRstNode::TITLE => array(
'reduceTitle',
),
ezcDocumentRstNode::PARAGRAPH => array(
'reduceParagraph',
'reduceListItem',
'reduceBlockquoteAnnotationParagraph',
'reduceBlockquote',
),
ezcDocumentRstNode::COMMENT => array(
'reduceListItem',
),
ezcDocumentRstNode::DIRECTIVE => array(
'reduceListItem',
),
ezcDocumentRstNode::LITERAL_BLOCK => array(
'reduceListItem',
),
ezcDocumentRstNode::BULLET_LIST => array(
'reduceList',
),
ezcDocumentRstNode::ENUMERATED_LIST => array(
'reduceList',
),
ezcDocumentRstNode::ANNOTATION => array(
'reduceBlockquoteAnnotation',
),
ezcDocumentRstNode::MARKUP_EMPHASIS => array(
'reduceMarkup',
),
ezcDocumentRstNode::MARKUP_STRONG => array(
'reduceMarkup',
),
ezcDocumentRstNode::MARKUP_INTERPRETED => array(
'reduceInterpretedText',
'reduceInternalTarget',
),
ezcDocumentRstNode::MARKUP_SUBSTITUTION => array(
'reduceMarkup',
),
ezcDocumentRstNode::REFERENCE => array(
'reduceReference',
),
ezcDocumentRstNode::LINK_ANONYMOUS => array(
'reduceLink',
),
ezcDocumentRstNode::LINK_REFERENCE => array(
'reduceLink',
),
);
/**
* List of node types, which can be considered as inline text nodes.
*
* @var array
*/
protected $textNodes = array(
ezcDocumentRstNode::TEXT_LINE,
ezcDocumentRstNode::MARKUP_EMPHASIS,
ezcDocumentRstNode::MARKUP_STRONG,
ezcDocumentRstNode::MARKUP_INTERPRETED,
ezcDocumentRstNode::MARKUP_LITERAL,
ezcDocumentRstNode::MARKUP_SUBSTITUTION,
ezcDocumentRstNode::LINK_ANONYMOUS,
ezcDocumentRstNode::LINK_REFERENCE,
ezcDocumentRstNode::REFERENCE,
ezcDocumentRstNode::TARGET,
);
/**
* List of node types, which are valid block nodes, where we can
* indentation changes after, or which can be aggregated into sections.
*
* @var array
*/
protected $blockNodes = array(
ezcDocumentRstNode::PARAGRAPH,
ezcDocumentRstNode::BLOCKQUOTE,
ezcDocumentRstNode::SECTION,
ezcDocumentRstNode::BULLET_LIST,
ezcDocumentRstNode::ENUMERATED_LIST,
ezcDocumentRstNode::TABLE,
ezcDocumentRstNode::LITERAL_BLOCK,
ezcDocumentRstNode::COMMENT,
ezcDocumentRstNode::DIRECTIVE,
ezcDocumentRstNode::SUBSTITUTION,
ezcDocumentRstNode::NAMED_REFERENCE,
ezcDocumentRstNode::FOOTNOTE,
ezcDocumentRstNode::ANON_REFERENCE,
ezcDocumentRstNode::TRANSITION,
ezcDocumentRstNode::FIELD_LIST,
ezcDocumentRstNode::DEFINITION_LIST,
ezcDocumentRstNode::LINE_BLOCK,
);
/**
* 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 ezcDocumentRstStack
*/
protected $documentStack = null;
/**
* Array with title levels used by the document author in their order.
*
* @var array
*/
protected $titleLevels = array();
/**
* List of builtin directives, which do not aggregate more text the
* parameters and options. User defined directives always aggregate
* following indeted text.
*
* @var array
*/
protected $shortDirectives = array(
'note',
'notice',
'warning',
'danger',
'image',
);
/**
* PCRE regular expression for detection of URLs in texts.
*/
const REGEXP_INLINE_LINK = '(
(?:^|[\s,.!?])
(?# Ignore matching braces around the URL)
(<)?
(\[)?
(\()?
(?# Ignore quoting around the URL)
([\'"]?)
(?# Actually match the URL)
(?P<match>
(?P<url>[a-z]+://[^\s]*?) |
(?:mailto:)?(?P<mail>[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})
)
\4
(?(3)\))
(?(2)\])
(?(1)>)
(?# Ignore common punctuation after the URL)
[.,?!]?(?:\s|$)
)Sxm';
/**
* Construct new document
*
* @param ezcDocumentParserOptions $options
*/
public function __construct( ezcDocumentParserOptions $options = null )
{
parent::__construct( $options );
$this->documentStack = new ezcDocumentRstStack();
}
/**
* Shift- / reduce-parser for RST token stack
*
* @param array $tokens
* @return void
*/
public function parse( array $tokens )
{
$tokens = new ezcDocumentRstStack( $tokens );
/* DEBUG
echo "\n\nStart parser\n============\n\n";
// /DEBUG */
while ( ( $token = $tokens->shift() ) !== null )
{
/* DEBUG
echo "[T] Token: " . ezcDocumentRstToken::getTokenName( $token->type ) . " ({$token->type}) at {$token->line}:{$token->position}.\n";
// /DEBUG */
// First shift given token by the defined reduction methods
foreach ( $this->shifts[$token->type] as $method )
{
/* DEBUG
echo " [S] Try $method\n";
// /DEBUG */
if ( ( $node = $this->$method( $token, $tokens ) ) === false )
{
// The shift method cannot handle the token, go to next
continue;
}
/* DEBUG
echo " [=> Skip used.\n";
// /DEBUG */
if ( $node === true )
{
// The shift method handled the token, but did not return a
// new node, we just go to the next token...
continue 2;
}
// Call reduce methods for nodes as long the reduction methods
// recreate some node
$ruleNumber = 0;
$ruleType = 0;
do {
// Reset the rule counter on changes of the node type
if ( $ruleType !== $node->type )
{
$ruleNumber = 0;
}
if ( !isset( $this->reductions[$node->type] ) ||
!isset( $this->reductions[$node->type][$ruleNumber] ) )
{
// If there are no reduction rules for the node, just
// add it to the stack
/* DEBUG
echo " [R] Add '" . ezcDocumentRstNode::getTokenName( $node->type ) . "' to stack (" . ( count( $this->documentStack ) + 1 ) . " elements).\n";
// /DEBUG */
$this->documentStack->unshift( $node );
break;
}
$ruleType = $node->type;
$reduction = $this->reductions[$node->type][$ruleNumber++];
/* DEBUG
echo " [R] Reduce with $reduction.\n";
// /DEBUG */
$node = $this->$reduction( $node );
} while ( $node !== null );
// We found a matching rule, so that we can leave the loop
break;
}
}
// Check if we successfully reduced the document stack
if ( ( count( $this->documentStack ) !== 1 ) ||
( !( $document = $this->documentStack->rewind() ) instanceof ezcDocumentRstDocumentNode ) )
{
$node = isset( $document ) ? $document : $this->documentStack->rewind();
$this->triggerError(
E_PARSE,
'Expected end of file, got: ' . ezcDocumentRstNode::getTokenName( $node->type ) . ".",
null, null, null
);
}
/* DEBUG
// echo "\nResulting document:\n\n", $document->dump(), "\nTest result: ";
// /DEBUG */
return $document;
}
/**
* Re-align tokens
*
* Realign tokens, so that the line start positions start at 0 again, even
* they were indeted before.
*
* @param array $tokens
* @return array
*/
protected function realignTokens( $tokens )
{
/* DEBUG
static $c = 0;
file_put_contents( "tokens-reentered-$c.php", "<?php\n\n return " . var_export( $tokens, true ) . ";\n\n" );
// /DEBUG */
$firstToken = reset( $tokens );
if ( ( $firstToken->type === ezcDocumentRstToken::WHITESPACE ) &&
( $firstToken->position === 1 ) )
{
$offset = strlen( $firstToken->content );
}
else
{
$offset = $firstToken->position - ( $firstToken->type === ezcDocumentRstToken::WHITESPACE ? 0 : 1 );
}
$fixedTokens = array();
$lineOffset = $offset;
foreach ( $tokens as $nr => $token )
{
if ( ( $token->type === ezcDocumentRstToken::WHITESPACE ) &&
isset( $tokens[$nr + 1] ) &&
( $tokens[$nr + 1]->type === ezcDocumentRstToken::WHITESPACE ) )
{
// Skip multiple whitespace tokens in a row.
continue;
}
if ( $token->type === ezcDocumentRstToken::NEWLINE )
{
$lineOffset = false;
}
elseif ( $lineOffset === false )
{
if ( ( $firstToken->type === ezcDocumentRstToken::WHITESPACE ) &&
( $firstToken->position === 1 ) )
{
$lineOffset = min( $offset, strlen( $firstToken->content ) );
}
else
{
$lineOffset = min( $offset, $token->position - ( $token->type === ezcDocumentRstToken::WHITESPACE ? 0 : 1 ) );
}
}
if ( ( $token->type === ezcDocumentRstToken::WHITESPACE ) &&
( $token->position <= $lineOffset ) )
{
if ( strlen( $token->content ) <= $lineOffset )
{
// Just skip token, completely out of tokens bounds
continue;
}
else
{
// Shorten starting whitespace token
$token = clone $token;
$token->position = 1;
$token->content = substr( $token->content, $lineOffset );
$fixedTokens[] = $token;
}
}
else
{
$token = clone $token;
$token->position = max( 1, $token->position - $lineOffset );
$fixedTokens[] = $token;
}
}
// If required add a second newline, if the provided token array does
// not contain any newlines at the end.
if ( $token->type !== ezcDocumentRstToken::NEWLINE )
{
$fixedTokens[] = new ezcDocumentRstToken( ezcDocumentRstToken::NEWLINE, "\n", null, null );
}
$fixedTokens[] = new ezcDocumentRstToken( ezcDocumentRstToken::NEWLINE, "\n", null, null );
$fixedTokens[] = new ezcDocumentRstToken( ezcDocumentRstToken::EOF, null, null, null );
/* DEBUG
file_put_contents( "tokens-reentered-$c-fixed.php", "<?php\n\n return " . var_export( $fixedTokens, true ) . ";\n\n" );
++$c;
// /DEBUG */
return $fixedTokens;
}
/**
* Reenter parser with a list of tokens
*
* Returns a parsed document created from the given tokens. With optional,
* but default, reindetation of the tokens relative to the first token.
*
* @param array $tokens
* @param bool $reindent
* @return ezcDocumentRstDocumentNode
*/
protected function reenterParser( array $tokens, $reindent = true )
{
if ( count( $tokens ) < 1 )
{
return array();
}
// Fix indentation for all cell tokens, as they were a single document.
$fixedTokens = $reindent ? $this->realignTokens( $tokens ) : $tokens;
$parser = new ezcDocumentRstParser();
return $parser->parse( $fixedTokens );
}
/**
* Print a dump of the document stack
*
* This function is only for use during dubbing of the document stack
* structure.
*
* @return void
*/
protected function dumpStack()
{
foreach ( $this->documentStack as $nr => $node )
{
printf( "% 2d) %s\n", $nr, $node->dump() );
}
}
/**
* Update the current indentation after each newline.
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return bool
*/
protected function updateIndentation( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
// Indentation Whitespaces right after a title line are irrelevant and
// should just be skipped as text, so ignore this rule for them:
if ( ( isset( $this->documentStack[0] ) ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TITLE ) )
{
return false;
}
if ( isset( $tokens[0] ) &&
( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) )
{
// Remove the whitespace from the stack, as it is only for
// indentation and should not be converted to text.
$whitespace = $tokens->shift();
if ( isset( $tokens[0] ) &&
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) )
{
// This is just a blank line
/* DEBUG
echo ' -> Empty line.';
// /DEBUG */
return false;
}
$indentation = iconv_strlen( $whitespace->content, 'UTF-8' );
}
elseif ( isset( $tokens[0] ) &&
( $tokens[0]->position > 1 ) )
{
// While reparsing table cell contents we may miss some whitespaces
// and directly get an indented non-whitespace node, which is
// sufficant to also determine the indetation.
$indentation = $tokens[0]->position - 1;
$whitespace = false;
}
else
{
// No whitespace means an indentation level of 0
$indentation = 0;
}
// This is the special case for dense bullet lists. In case of bullet
// lists the indentation may also change right between two lines
// without an additional newline.
if ( isset( $this->documentStack[0] ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes, true ) ) &&
( isset( $tokens[0] ) ) &&
( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( in_array( $tokens[0]->content, array(
'*', '-', '+',
"\xe2\x80\xa2", "\xe2\x80\xa3", "\xe2\x81\x83"
) ) ) &&
isset( $tokens[1] ) &&
( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) )
{
// We update the indentation in this case and add a paragraph node
// to close the prior paragraph.
$this->postIndentation = $indentation;
/* DEBUG
echo " -> Bullet special paragraph update\n";
echo " => Updated indentation to {$indentation}.\n";
// /DEBUG */
$paragraph = new ezcDocumentRstParagraphNode( $token );
/* DEBUG
echo " => Paragraph post indentation set to {$paragraph->indentation}.\n";
// /DEBUG */
return $paragraph;
}
// There is also a special case for dense enumeration lists, very
// similar to bullet lists.
if ( isset( $this->documentStack[0] ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes, true ) ) &&
( $this->isEnumeratedList( $tokens ) ) )
{
// We update the indentation in this case and add a paragraph node
// to close the prior paragraph.
$this->postIndentation = $indentation;
/* DEBUG
echo " -> Enumeration list special paragraph update\n";
echo " => Updated indentation to {$indentation}.\n";
// /DEBUG */
$paragraph = new ezcDocumentRstParagraphNode( $token );
/* DEBUG
echo " => Paragraph post indentation set to {$paragraph->indentation}.\n";
// /DEBUG */
return $paragraph;
}
// If the current indentation is 0 and the indentation increased, with
// text line nodes as last items on the stack we got a definition list.
if ( ( $this->indentation === 0 ) &&
( $indentation > $this->indentation ) &&
( isset( $this->documentStack[0] ) ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes, true ) ) )
{
/* DEBUG
echo " => Definition list detected.\n";
// /DEBUG */
// Put indetation token back into the token stack.
if ( $whitespace !== false )
{
$tokens->unshift( $whitespace );
}
return $this->shiftDefinitionList( $token, $tokens );
}
// The indentation may only change after we reduced a block level
// element. There may be other spcial cases, which are handled
// elsewhere.
if ( ( $this->indentation !== $indentation ) &&
isset( $this->documentStack[0] ) &&
!in_array( $this->documentStack[0]->type, $this->blockNodes, true ) )
{
// Enumerated lists are relabeled to paragraphs, if the indentation
// level is changed right after the list item seemed to start.
for ( $i = 0; $i < count( $this->documentStack ); ++$i )
{
if ( in_array( $this->documentStack[$i]->type, $this->textNodes, true ) )
{
// This is just text, we are looking for the next block level element.
continue;
}
// If the last block level element was a enumerated list, just relabel.
if ( $this->documentStack[$i]->type === ezcDocumentRstNode::ENUMERATED_LIST )
{
// Readd enumeration lsit marker as plain text.
$textNode = new ezcDocumentRstTextLineNode( $this->documentStack[$i]->token );
$textNode->token->content = $this->documentStack[$i]->text;
$this->documentStack[$i] = $textNode;
// Reset indentation level.
$this->indentation = $indentation;
$this->postIndentation = null;
return false;
}
}
$this->triggerError(
E_PARSE,
"Unexpected indentation change from level {$this->indentation} to {$indentation}.",
null, $token->line, $token->position
);
}
// Otherwise indentation changes are fine, and we just update the
// current indentation level for later checks
$this->indentation = $indentation;
$this->postIndentation = null;
/* DEBUG
echo " => Updated indentation to {$indentation}.\n";
// /DEBUG */
return false;
}
/**
* Create new document node
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstDocumentNode
*/
protected function shiftDocument( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
// If there are any tokens left after the end of the file, something
// went seriously wrong in the tokenizer.
if ( count( $tokens ) )
{
$this->triggerError(
E_PARSE,
'Unexpected end of file.',
null, $token->line, $token->position
);
}
$this->documentStack->push( new ezcDocumentRstDocumentNode() );
return new ezcDocumentRstDocumentNode();
}
/**
* Escaping of special characters
*
* A backslash is used for character escaping, as defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#escaping-mechanism
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftBackslash( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( isset( $tokens[0] ) )
{
switch ( $tokens[0]->type )
{
case ezcDocumentRstToken::NEWLINE:
case ezcDocumentRstToken::WHITESPACE:
// Escaped whitespace characters are just removed, just
// like the backslash itself.
$tokens->shift();
/* DEBUG
echo " -> Remove escaped whitespace.\n";
// /DEBUG */
return true;
case ezcDocumentRstToken::BACKSLASH:
// A double backslash results in a backslash text node.
$tokens[0]->type = ezcDocumentRstToken::TEXT_LINE;
$tokens[0]->escaped = true;
/* DEBUG
echo " -> Transformed backslash to text.\n";
// /DEBUG */
return true;
case ezcDocumentRstToken::SPECIAL_CHARS:
case ezcDocumentRstToken::TEXT_LINE:
// A special character group is always escaped by a
// preceeding backslash
if ( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > 1 )
{
// Long special character group, so that we need to
// split it up.
$newToken = new ezcDocumentRstToken(
ezcDocumentRstToken::TEXT_LINE,
$tokens[0]->content[0], $tokens[0]->line, $tokens[0]->position
);
$newToken->escaped = true;
// Remove extracted part from old token
$tokens[0]->content = substr( $tokens[0]->content, 1 );
++$tokens[0]->position;
// Add new token to the beginning of the token stack.
/* DEBUG
echo " -> Add new split token.\n";
// /DEBUG */
$tokens->unshift( $newToken );
return true;
}
else
{
// Just convert token into a simple text node
/* DEBUG
echo " -> Transformed token into escaped text.\n";
// /DEBUG */
$tokens[0]->type = ezcDocumentRstToken::TEXT_LINE;
$tokens[0]->escaped = true;
return true;
}
}
}
}
/**
* Create new title node from titles with a top and bottom line
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftTitle( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->position !== 1 ) ||
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) )
{
// This is not a title line at all
return false;
}
// Handle literal block markers differently, they are followed by two
// newlines (maybe with whitespaces inbetween).
if ( ( ( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ||
( ( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[2]->type === ezcDocumentRstToken::NEWLINE ) ) ||
( iconv_strlen( $tokens[1]->content, 'UTF-8' ) > iconv_strlen( $token->content, 'UTF-8' ) ) ||
( ( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) &&
( isset( $tokens[2] ) ) &&
( ( iconv_strlen( $tokens[1]->content, 'UTF-8' ) + iconv_strlen( $tokens[2]->content, 'UTF-8' ) ) > iconv_strlen( $token->content, 'UTF-8' ) ) ) ) &&
isset( $this->documentStack[0] ) &&
!in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
// This seems to be something else, like a liteal block marker.
return false;
}
return new ezcDocumentRstTitleNode(
$token
);
}
/**
* Shift transistions, which are separators in the document.
*
* Transitions are specified here:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#transitions
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftTransition( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->position !== 1 ) ||
( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( iconv_strlen( $token->content, 'UTF-8' ) < 4 ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ||
( !isset( $tokens[1] ) ) ||
( $tokens[1]->type !== ezcDocumentRstToken::NEWLINE ) )
{
// This is not a transistion
return false;
}
return new ezcDocumentRstTransitionNode(
$token
);
}
/**
* Shift line blocks
*
* Shift line blocks, which are specified at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#line-blocks
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftLineBlock( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->position !== ( $this->indentation + 1 ) ) ||
( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $token->content !== '|' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) )
{
// This is not a line block
return false;
}
// Put everything back into the token list, as this makes it easier for
// us to read
$lines = array();
$tokens->unshift( $token );
if ( $this->indentation > 0 )
{
$tokens->unshift( new ezcDocumentRstToken(
ezcDocumentRstToken::WHITESPACE,
str_repeat( ' ', $this->indentation ),
$token->line, 1
) );
}
// Each line is introduced by '| ', optionally with the proper current
// indentation.
while ( ( ( $this->indentation === 0 ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) === $this->indentation ) ) &&
( $tokens[1]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[1]->content === '|' ) &&
( ( $tokens[2]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[2]->type === ezcDocumentRstToken::NEWLINE ) ) ) &&
( ( $this->indentation > 0 ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '|' ) &&
( ( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ) ) ) )
{
/* DEBUG
echo " -> Next line: {$tokens[0]->line}\n";
// /DEBUG */
if ( $this->indentation > 0 )
{
// Skip the indentation token, which length has already been
// checked.
$tokens->shift();
}
// Shift the line block marker
$line = array( $tokens->shift() );
$whitespace = $tokens->shift();
if ( $whitespace->type === ezcDocumentRstToken::NEWLINE )
{
// Properly handle empty line in line blocks.
/* DEBUG
echo " -> Skip empty line.\n";
// /DEBUG */
$lines[] = $line;
continue;
}
// Remove the leading space from the following whitespace token
if ( $whitespace->content !== ' ' )
{
/* DEBUG
echo " -> Shorten indentation.\n";
// /DEBUG */
$whitespace->content = substr( $whitespace->content, 1 );
$line[] = $whitespace;
}
// Read all tokens in current line und following lines.
/* DEBUG
echo " -> Read line block line tokens\n";
// /DEBUG */
do {
/* DEBUG
echo " -> Read tokens: ";
// /DEBUG */
do {
$line[] = $token = $tokens->shift();
/* DEBUG
echo ".";
// /DEBUG */
} while ( ( $token->type !== ezcDocumentRstToken::NEWLINE ) &&
isset( $tokens[0] ) );
/* DEBUG
echo "\n";
// /DEBUG */
} while ( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) >= ( $this->indentation + 2 ) ) &&
( $tokens->shift() ) );
$lines[] = $line;
}
// Transform aggregated tokens in proper AST structures
$node = new ezcDocumentRstLineBlockNode( $token );
$node->indentation = $this->indentation;
foreach ( $lines as $line )
{
$lineNode = new ezcDocumentRstLineBlockLineNode( array_shift( $line ) );
foreach ( $line as $token )
{
$lineNode->nodes[] = new ezcDocumentRstLiteralNode( $token );
}
$node->nodes[] = $lineNode;
}
// Rest the indentation and exit
$this->indentation = 0;
return $node;
}
/**
* Just keep text as text nodes
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftText( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
return new ezcDocumentRstTextLineNode(
$token
);
}
/**
* Shift a paragraph node on two newlines
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTitleNode
*/
protected function shiftParagraph( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( !isset( $tokens[0] ) ||
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ) &&
( !isset( $tokens[0] ) || !isset( $tokens[1] ) ||
( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[1]->type !== ezcDocumentRstToken::NEWLINE ) ) )
{
// For now we only check for paragraphs closed with two newlines.
/* DEBUG
echo " -> No following newline.\n";
// /DEBUG */
return false;
}
// Remove all following newlines except the last one.
while ( ( isset( $tokens[1] ) &&
( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ) ||
( isset( $tokens[1] ) && isset( $tokens[2] ) &&
( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[2]->type === ezcDocumentRstToken::NEWLINE ) ) )
{
$tokens->shift();
}
return new ezcDocumentRstParagraphNode(
$token
);
}
/**
* Check if token is an inline markup start token.
*
* For a user readable list of the following rules, see:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return boolean
*/
protected function isInlineStartToken( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
return ( // Rule 1
( ( !isset( $this->documentStack[0] ) ) ||
( ( $this->documentStack[0]->token->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( strpos( '\'"([{<-/:_', $this->documentStack[0]->token->content[0] ) !== false ) ) ||
( $this->documentStack[0]->token->type === ezcDocumentRstToken::WHITESPACE ) ||
( $token->position <= ( $this->indentation + 1 ) ) ) &&
// Rule 2
( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) &&
// Rule 5
( ( !isset( $this->documentStack[0] ) ) ||
( ( ( $this->documentStack[0]->token->content !== '"' ) || ( $tokens[0]->content !== '"' ) ) &&
( ( $this->documentStack[0]->token->content !== '\'' ) || ( $tokens[0]->content !== '\'' ) ) &&
( ( $this->documentStack[0]->token->content !== '(' ) || ( $tokens[0]->content !== ')' ) ) &&
( ( $this->documentStack[0]->token->content !== '[' ) || ( $tokens[0]->content !== ']' ) ) &&
( ( $this->documentStack[0]->token->content !== '{' ) || ( $tokens[0]->content !== '}' ) ) &&
( ( $this->documentStack[0]->token->content !== '<' ) || ( $tokens[0]->content !== '>' ) ) ) ) );
}
/**
* Check if token is an inline markup end token.
*
* For a user readable list of the following rules, see:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return boolean
*/
protected function isInlineEndToken( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
return ( // Rule 3
( isset( $this->documentStack[0] ) ) &&
( $this->documentStack[0]->token->type !== ezcDocumentRstToken::WHITESPACE ) &&
( $token->position > ( $this->indentation + 1 ) ) &&
// Rule 4
( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( strpos( '\'")]}>-/:.,;!?\\_', $tokens[0]->content[0] ) !== false ) ) &&
// Rule 5
( ( !isset( $this->documentStack[0] ) ) ||
( ( ( $this->documentStack[0]->token->content !== '"' ) || ( $tokens[0]->content !== '"' ) ) &&
( ( $this->documentStack[0]->token->content !== '\'' ) || ( $tokens[0]->content !== '\'' ) ) &&
( ( $this->documentStack[0]->token->content !== '(' ) || ( $tokens[0]->content !== ')' ) ) &&
( ( $this->documentStack[0]->token->content !== '[' ) || ( $tokens[0]->content !== ']' ) ) &&
( ( $this->documentStack[0]->token->content !== '{' ) || ( $tokens[0]->content !== '}' ) ) &&
( ( $this->documentStack[0]->token->content !== '<' ) || ( $tokens[0]->content !== '>' ) ) ) ) );
}
/**
* Detect inline literal
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-literals
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftInlineLiteral( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( $token->content !== '``' )
{
return false;
}
if ( $this->isInlineStartToken( $token, $tokens ) )
{
/* DEBUG
echo " -> Found inline literal.\n";
// /DEBUG */
$node = new ezcDocumentRstMarkupInlineLiteralNode( $token );
// Scan all subsequent tokens until we reach the closing token
/* DEBUG
echo " -> Inline literal tokens: ";
// /DEBUG */
$nodes = array();
while ( ( $literalToken = $tokens->shift() ) &&
( ( $literalToken->content !== '``' ) ||
// The common inline markup end check does not apply
// here, as whitespace is not required preceed the
// closing ``.
!( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( strpos( '\'")]}>-/:.,;!?\\_', $tokens[0]->content[0] ) !== false ) ) ) )
{
/* DEBUG
echo ".";
// /DEBUG */
$nodes[] = new ezcDocumentRstLiteralNode( $literalToken );
}
/* DEBUG
echo "\n";
// /DEBUG */
// We found a closing markup node.
if ( $literalToken !== null )
{
/* DEBUG
echo " => Shift inline literal node.\n";
// /DEBUG */
$node->nodes = $nodes;
return $node;
}
else
{
/* DEBUG
echo " => No closing mark detected.\n";
// /DEBUG */
// Put all tokens back on stack.
$nodes = array_reverse( $nodes );
foreach ( $nodes as $node )
{
$tokens->unshift( $node->token );
}
return false;
}
}
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Detect inline markup
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftInlineMarkup( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
switch ( $token->content )
{
case '*':
$class = 'ezcDocumentRstMarkupEmphasisNode';
break;
case '**':
$class = 'ezcDocumentRstMarkupStrongEmphasisNode';
break;
case '|':
$class = 'ezcDocumentRstMarkupSubstitutionNode';
break;
default:
// The found group of special characters are no inline markup,
// but maybe just text...
return false;
}
/* DEBUG
echo " -> Class: $class\n";
// /DEBUG */
if ( $this->isInlineStartToken( $token, $tokens ) )
{
// Create a markup open tag
/* DEBUG
echo " => Create opening tag: $class\n";
// /DEBUG */
return new $class( $token, true );
}
if ( $this->isInlineEndToken( $token, $tokens ) )
{
// Create a markup close tag
/* DEBUG
echo " => Create closing tag: $class\n";
// /DEBUG */
return new $class( $token, false );
}
// - Rule 6 is implicitely given by the tokenizer.
// - Rule 7 is ensured by the escaping rules, defined in the shiftBackslash method.
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Try to shift a interpreted text role
*
* Text role shifting is only called directly from the
* shiftInterpretedTextMarkup() method and tries to find the associated
* role.
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return mixed
*/
protected function shiftInterpretedTextRole( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
/* DEBUG
echo " -> Scan for a role\n";
// /DEBUG */
if ( ( ( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $token->content !== ':' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::TEXT_LINE ) ) &&
( ( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $token->content !== '`' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[0]->content !== ':' ) ) )
{
return false;
}
$behind = false;
$aggregated = array();
if ( $token->content === '`' )
{
$behind = true;
$token = $aggregated[] = $tokens->shift();
}
$role = '';
do {
$roleNameToken = $aggregated[] = $tokens->shift();
$role .= $roleNameToken->content;
} while ( isset( $tokens[0] ) &&
( ( $tokens[0]->type === ezcDocumentRstToken::TEXT_LINE ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( preg_match( '(^[._-]+$)', $tokens[0]->content ) ) ) ) );
// Check if this is really a valid role, otherwise put all checked
// tokens back on the stack and do not handle this as a interpreted
// text role.
if ( !preg_match( '(^[A-Za-z0-9._-]+$)', $role ) ||
!isset( $tokens[0] ) ||
( $tokens[0]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[0]->content !== ':' ) ||
( !$behind &&
( !isset( $tokens[1] ) ||
( $tokens[1]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[1]->content !== '`' ) ) ) )
{
/* DEBUG
echo " -> Not a role - reverting.\n";
// /DEBUG */
$tokens->prepend( $aggregated );
return false;
}
/* DEBUG
echo " -> Found role: $role\n";
// /DEBUG */
// Remove last colon from token list
$tokens->shift();
return $role;
}
/**
* Detect interpreted text inline markup
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#interpreted-text
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftInterpretedTextMarkup( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
$role = false;
if ( ( $token->content !== '`' ) &&
!( $role = $this->shiftInterpretedTextRole( $token, $tokens ) ) )
{
return false;
}
// If we found a valid role before the token, update the current token
// to the actual interpreted text markup start token
if ( $role !== false )
{
$token = $tokens->shift();
}
/* DEBUG
echo " -> Interpreted text markup found, role: $role\n";
// /DEBUG */
if ( $this->isInlineEndToken( $token, $tokens ) )
{
// Create a markup close tag
/* DEBUG
echo " => Create closing inline interpreted text tag.\n";
// /DEBUG */
$node = new ezcDocumentRstMarkupInterpretedTextNode( $token, false );
$node->role = $this->shiftInterpretedTextRole( $token, $tokens );
return $node;
}
if ( $this->isInlineStartToken( $token, $tokens ) )
{
// Create a markup open tag
/* DEBUG
echo " => Create opening inline interpreted text tag.\n";
// /DEBUG */
$node = new ezcDocumentRstMarkupInterpretedTextNode( $token, true );
$node->role = $role;
return $node;
}
// - Rule 6 is implicitely given by the tokenizer.
// - Rule 7 is ensured by the escaping rules, defined in the shiftBackslash method.
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Detect inline markup
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftAnonymousHyperlinks( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== '__' ) ||
( $token->position === 1 ) )
{
// __ is the anonymous hyperlink token, skip all other cheks for
// other special char tokens.
return false;
}
// For a user readable list of the following rules, see:
// http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
//
// For the anonymous hyperlink marker the same rules apply as for a
// common end marker.
if ( $this->isInlineEndToken( $token, $tokens ) )
{
// Create a markup close tag
return new ezcDocumentRstAnonymousLinkNode( $token );
}
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Tries to detect footnote type
*
* The type of the footnote
*
* @param array $name
* @return void
*/
protected function detectFootnoteType( array $name )
{
$firstToken = reset( $name );
switch ( $firstToken->content )
{
case '*':
return ezcDocumentRstFootnoteNode::SYMBOL;
case '#':
return count( $name ) === 1 ?
ezcDocumentRstFootnoteNode::AUTO_NUMBERED :
ezcDocumentRstFootnoteNode::LABELED;
default:
return is_numeric( $firstToken->content ) ?
ezcDocumentRstFootnoteNode::NUMBERED :
ezcDocumentRstFootnoteNode::CITATION;
}
}
/**
* Detect reference
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftReference( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( $token->content !== '_' )
{
// __ is the anonymous hyperlink token, skip all other cheks for
// other special char tokens.
return false;
}
// For a user readable list of the following rules, see:
// http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
//
// For the anonymous hyperlink marker the same rules apply as for a
// common end marker.
if ( // Custom rule to detect citation and footnote references
( ( isset( $this->documentStack[0] ) ) &&
( $this->documentStack[0]->token->content === ']' ) &&
( $this->documentStack[0]->token->type === ezcDocumentRstToken::SPECIAL_CHARS ) ) &&
// Rule 4
( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( strpos( '\'")]}>-/:.,;!?\\', $tokens[0]->content[0] ) !== false ) ) )
{
// Create a markup close tag
return new ezcDocumentRstReferenceNode( $token );
}
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Detect inline markup
*
* As defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftExternalReference( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( $token->content !== '_' )
{
// __ is the anonymous hyperlink token, skip all other cheks for
// other special char tokens.
return false;
}
// For a user readable list of the following rules, see:
// http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
//
// For the anonymous hyperlink marker the same rules apply as for a
// common end marker.
if ( $this->isInlineEndToken( $token, $tokens ) )
{
// Create a markup close tag
return new ezcDocumentRstExternalReferenceNode( $token );
}
// In other preconditions this is no inline markup, but maybe just text.
return false;
}
/**
* Blockquote annotations
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftBlockquoteAnnotation( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== '--' ) &&
( $token->content !== '---' ) &&
// Also the unicode character for EM-Dash is allowed
( $token->content !== "\x20\x14" ) )
{
// The special character group is not one of the allowed annotation markers
return false;
}
if ( !isset( $this->documentStack[0] ) ||
( $this->documentStack[0]->type !== ezcDocumentRstNode::BLOCKQUOTE ) ||
( $this->indentation === 0 ) )
{
// Annotations only follow blockquotes.
/* DEBUG
echo " -> Annotation not preceeded by blockquote.\n";
// /DEBUG */
return false;
}
// The section on blockquote annotations
// http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#block-quotes
// does not tell anything about the stuff, which may be used in there.
// We assume everything is possible like in normal paragraphs. The text
// is added during blockquote reduction.
return new ezcDocumentRstBlockquoteAnnotationNode( $token );
}
/**
* Is enumerated list?
*
* As defined at
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists
*
* Checks if the curretn token with thw following tokens may be an
* enumerated list. Used by the repective shifting method and when checking
* for indentation updates.
*
* Returns true, if the tokens may be an enumerated list, and false otherwise.
*
* @param ezcDocumentRstStack $tokens
* @param mixed $token
* @return void
*/
protected function isEnumeratedList( ezcDocumentRstStack $tokens, $token = null )
{
$tokens = $tokens->asArray( 5 );
if ( $token === null )
{
$token = array_shift( $tokens );
}
if ( $token === null )
{
return false;
}
// This pattern matches upper and lowercase roman numbers up 4999,
// normal integers to any limit and alphabetic chracters.
$enumeratedListPattern = '(^(?:(m{0,4}d?c{0,3}l?x{0,3}v{0,3}i{0,3}v?x?l?c?d?m?)|(M{0,4}D?C{0,3}L?X{0,3}V{0,3}I{0,3}V?X?L?C?D?M?)|([1-9]+[0-9]*)|([a-z])|([A-Z]))$)S';
$matchOrderType = array(
1 => ezcDocumentRstEnumeratedListNode::LOWER_ROMAN,
2 => ezcDocumentRstEnumeratedListNode::UPPER_ROMAN,
3 => ezcDocumentRstEnumeratedListNode::NUMERIC,
4 => ezcDocumentRstEnumeratedListNode::LOWERCASE,
5 => ezcDocumentRstEnumeratedListNode::UPPERCASE,
);
// Create enumerated list from list items surrounded by parantheses
if ( ( $token->content === '(' ) &&
isset( $tokens[1] ) &&
( $tokens[1]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[1]->content === ')' ) &&
isset( $tokens[2] ) &&
( ( $tokens[2]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[2]->type === ezcDocumentRstToken::NEWLINE ) ) &&
isset( $tokens[0] ) &&
( ( ( $tokens[0]->type === ezcDocumentRstToken::TEXT_LINE ) &&
( preg_match( $enumeratedListPattern, $tokens[0]->content, $match ) ) ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '#' ) ) ) )
{
/* DEBUG
echo " -> Found full framed enumeration list item.\n";
// /DEBUG */
foreach ( $matchOrderType as $number => $type )
{
if ( !empty( $match[$number] ) )
{
return $type;
}
}
return true;
}
// Create enumerated list from list items followed by a parantheses or
// a dot
if ( isset( $tokens[0] ) &&
( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( ( $tokens[0]->content === ')' ) ||
( $tokens[0]->content === '.' ) ) &&
isset( $tokens[1] ) &&
( ( $tokens[1]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ) &&
( ( ( $token->type === ezcDocumentRstToken::TEXT_LINE ) &&
( preg_match( $enumeratedListPattern, $token->content, $match ) ) ) ||
( ( $token->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $token->content === '#' ) ) ) )
{
/* DEBUG
echo " -> Found half framed enumeration list item.\n";
// /DEBUG */
foreach ( $matchOrderType as $number => $type )
{
if ( !empty( $match[$number] ) )
{
return $type;
}
}
return true;
}
/* DEBUG
echo " -> Not an enumeration list item.\n";
// /DEBUG */
return false;
}
/**
* Enumerated lists
*
* As defined at
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftEnumeratedList( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
// The bullet list should always start at the very beginning of a line
// / paragraph, so that the char postion should match the current
// identation level.
if ( $token->position !== ( $this->indentation + 1 ) )
{
/* DEBUG
echo " -> Indentation mismatch ({$token->position} <> {$this->indentation})\n";
// /DEBUG */
return false;
}
if ( !( $listType = $this->isEnumeratedList( $tokens, $token ) ) )
{
return false;
}
// We now know, that we have a bullet list, and can just shift the
// tokens accordingly.
// An opening brace is the only possible content before the actual list
// item identifier - skip it.
$content = '';
$indentationIncrease = 1;
if ( $token->content === '(' )
{
$token = $tokens->shift();
$content .= $token->content;
$indentationIncrease = 2;
}
// The bullet list should always start at the very beginning of a line
// / paragraph, so that the char postion should match the current
// identation level.
if ( $token->position !== ( $this->indentation + $indentationIncrease ) )
{
/* DEBUG
echo " -> Indentation mismatch ({$token->position} <> {$this->indentation})\n";
// /DEBUG */
return false;
}
$text = $token;
$char = $tokens->shift();
// Only shift next token, if it is a whitespace - preserve newlines
$whitespace = ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) ? $tokens->shift() : $tokens[0];
/* DEBUG
echo " => Indentation updated to {$this->indentation}.\n";
// /DEBUG */
$this->indentation = $text->position + iconv_strlen( $text->content, 'UTF-8' ) +
iconv_strlen( $whitespace->content, 'UTF-8' ) + iconv_strlen( $char->content, 'UTF-8' ) - 1;
$node = new ezcDocumentRstEnumeratedListNode( $text );
$node->text = $content . $text->content . $char->content . $whitespace->content;
$node->indentation = $this->indentation;
$node->listType = $listType;
return $node;
}
/**
* Bullet point lists
*
* As defined at
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftBulletList( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
// Check if the special character group matches the known bullet list
// starting characters.
if ( !in_array( $token->content, array(
'*', '-', '+',
"\xe2\x80\xa2", "\xe2\x80\xa3", "\xe2\x81\x83"
) ) )
{
return false;
}
// The bullet list should always start at the very beginning of a line
// / paragraph, so that the char postion should match the current
// identation level.
if ( $token->position !== ( $this->indentation + 1 ) )
{
/* DEBUG
echo " -> Indentation mismatch ({$token->position} <> {$this->indentation})\n";
// /DEBUG */
return false;
} // /DEBUG */
// The next token has to be a whitespace, which length also defines the
// new indentation level.
if ( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE )
{
/* DEBUG
echo " -> No whitespace.\n";
// /DEBUG */
return false;
}
$whitespace = $tokens->shift();
// Update indentation level
$this->indentation = $token->position + iconv_strlen( $whitespace->content, 'UTF-8' );
/* DEBUG
echo " => Indentation updated to {$this->indentation}.\n";
// /DEBUG */
// This seems to be a valid bullet list
$node = new ezcDocumentRstBulletListNode( $token );
$node->indentation = $this->indentation;
return $node;
}
/**
* Just keep text as text nodes
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTextLineNode
*/
protected function shiftWhitespaceAsText( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
return new ezcDocumentRstTextLineNode(
$token
);
}
/**
* Keep the newline as a single whitespace to maintain readability in
* texts.
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTextLineNode
*/
protected function shiftAsWhitespace( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( isset( $this->documentStack[0] ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes ) ) )
{
/* DEBUG
echo " => Add additional whitespace to last node.\n";
// /DEBUG */
$token->content = ' ';
return new ezcDocumentRstTextLineNode( $token );
}
return false;
}
/**
* Just keep text as text nodes
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstTextLineNode
*/
protected function shiftSpecialCharsAsText( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
return new ezcDocumentRstTextLineNode(
$token
);
}
/**
* Shift literal block
*
* Shift a complete literal block into one node. The behaviour of literal
* blocks is defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#literal-blocks
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftLiteralBlock( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== '::' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ||
( !isset( $tokens[1] ) ) ||
( $tokens[1]->type !== ezcDocumentRstToken::NEWLINE ) )
{
// Literal blocks only start by a double colon: '::', and has
// always to be followed by two newlines (marking a common
// paragraph).
return false;
}
// Check if we should add a text node to the stack first, including a
// single colon.
if ( ( $token->position > $this->indentation ) &&
( isset( $this->documentStack[0] ) ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes ) ) &&
( $this->documentStack[0]->token->type !== ezcDocumentRstToken::WHITESPACE ) )
{
$tokens->unshift(
new ezcDocumentRstToken(
ezcDocumentRstToken::SPECIAL_CHARS, '::', $token->line, $this->indentation
)
);
// Return a new text node first, the new pseudo-token starting the
// literal block will be handled in the next iteration.
/* DEBUG
echo " => Create new text node, handle the literal block later.\n";
// /DEBUG */
return new ezcDocumentRstTextLineNode(
new ezcDocumentRstToken(
ezcDocumentRstToken::TEXT_LINE, ':', $token->line, $token->position, true
)
);
}
// If the token is onyl preceeded by a textnode we put it back on the
// token stack an return a paragraph node first, to close the previous
// paragraph. In the next iteration the literal block will be handled.
if ( isset( $this->documentStack[0] ) &&
in_array( $this->documentStack[0]->type, $this->textNodes ) )
{
$tokens->unshift( $token );
/* DEBUG
echo " => Create a paragraph for the preceeding text stuff first.\n";
// /DEBUG */
return new ezcDocumentRstParagraphNode( $token );
}
// Skip all empty lines first
while ( $tokens[0]->type === ezcDocumentRstToken::NEWLINE )
{
/* DEBUG
echo " -> Skip newline.\n";
// /DEBUG */
$tokens->shift();
}
// Once we got the first line after the literal block start marker, we
// check for the quoting style
if ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE )
{
// In case of a whitespace indentation token, this is used
// completely as indentation marker.
/* DEBUG
echo " -> Detected whitespace indetation..\n";
// /DEBUG */
$baseIndetation = clone $tokens[0];
}
elseif ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS )
{
// In case of special characters we expect each line to start with
// the same single character, while the original content is
// preserved.
/* DEBUG
echo " -> Detected special character indetation..\n";
// /DEBUG */
$baseIndetation = new ezcDocumentRstToken(
ezcDocumentRstToken::SPECIAL_CHARS,
$tokens[0]->content[0], $tokens[0]->line, $tokens[0]->position
);
}
else
{
// In other case we got something unexpected.
return false;
}
$collected = array();
$minIndentation = iconv_strlen( $baseIndetation->content, 'UTF-8' );
while ( // Empty lines are inlcuded.
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
// All other lines must start with the determined base
// indentation
( ( $tokens[0]->type === $baseIndetation->type ) &&
( ( strpos( $tokens[0]->content, $baseIndetation->content ) === 0 ) ||
( ( $baseIndetation->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > $this->indentation ) ) ) ) )
{
$literalToken = $tokens->shift();
if ( $literalToken->type === ezcDocumentRstToken::NEWLINE )
{
// Nothing to do for empty lines, but they are included in the
// literal block.
/* DEBUG
echo " -> Collected plain newline.\n";
// /DEBUG */
$collected[] = new ezcDocumentRstLiteralNode( $literalToken );
continue;
}
if ( $baseIndetation->type === ezcDocumentRstToken::WHITESPACE )
{
// Remove whitespaces used for indentation in literal blocks
$minIndentation = min( iconv_strlen( $literalToken->content, 'UTF-8' ), $minIndentation );
/* DEBUG
echo " -> Minimum indentation: $minIndentation.\n";
// /DEBUG */
}
$collected[] = new ezcDocumentRstLiteralNode( $literalToken );
// Just collect everything until we reach a newline, the the check
// starts again.
/* DEBUG
echo " -> Collect: ";
// /DEBUG */
do {
/* DEBUG
echo ".";
// /DEBUG */
$collected[] = new ezcDocumentRstLiteralNode( $item = $tokens->shift() );
}
while ( $item->type !== ezcDocumentRstToken::NEWLINE );
/* DEBUG
echo "\n";
// /DEBUG */
}
/* DEBUG
echo " => Finished collecting.\n";
// /DEBUG */
// Readd the last newline to the token stack
$tokens->unshift( $item );
// When literal block is indented by whitespace, remove the minimum
// indentation. from each line.
if ( $baseIndetation->type === ezcDocumentRstToken::WHITESPACE )
{
foreach ( $collected as $node )
{
if ( ( $node->token->position <= 1 ) &&
( $node->token->type === ezcDocumentRstToken::WHITESPACE ) )
{
$node->token->content = substr( $node->token->content, $minIndentation );
}
}
}
// Nothing more could be collected, either because the indentation has
// been reduced, or the markers are missing. Create the literal block
// node.
$node = new ezcDocumentRstLiteralBlockNode( $token, $collected );
$node->indentation = $this->indentation;
return $node;
}
/**
* Read all token until one of the given tokens occurs
*
* Reads all tokens and removes them from the token stack, which do not
* match of the given tokens. Escaping is maintained.
*
* @param ezcDocumentRstStack $tokens
* @param array $until
* @return array
*/
protected function readUntil( ezcDocumentRstStack $tokens, array $until )
{
$quoted = false;
if ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '`' ) )
{
$tokens->shift();
$until = array( array(
'type' => ezcDocumentRstToken::SPECIAL_CHARS,
'content' => '`',
) );
$quoted = true;
}
$foundTokens = array();
$found = false;
do {
if ( $tokens[0]->type === ezcDocumentRstToken::BACKSLASH )
{
$backslash = $tokens->shift();
$this->shiftBackslash( $backslash, $tokens );
}
foreach ( $until as $check )
{
if ( ( !isset( $check['type'] ) ||
( $tokens[0]->type === $check['type'] ) ) &&
( !isset( $check['content'] ) ||
( $tokens[0]->content === $check['content'] ) ) )
{
$found = true;
break 2;
}
}
$foundTokens[] = $tokens->shift();
} while ( $found === false );
if ( $quoted )
{
$tokens->shift();
}
return $foundTokens;
}
/**
* Read multiple lines
*
* Reads the content of multiple indented lines, where the indentation can
* bei either handled strict, or lose, when literal text is expected.
*
* Returns an array with the collected tokens, until the indentation
* changes.
*
* @param ezcDocumentRstStack $tokens
* @param bool $strict
* @return array
*/
protected function readMutlipleIndentedLines( ezcDocumentRstStack $tokens, $strict = false )
{
/* DEBUG
echo " -> Read follow up text.\n";
// /DEBUG */
$collected = array();
if ( $tokens[0]->position > ( $this->indentation + 1 ) )
{
// The first token is a follow up token to something before. We
// ignore the indentation here, and read everything in the first
// line.
do {
$collected[] = $token = $tokens->shift();
} while ( $token->type !== ezcDocumentRstToken::NEWLINE );
}
/* DEBUG
echo " -> Found " . count( $collected ) . " tokens in same line.\n";
// /DEBUG */
// Now check for the actual indentation and aggregate everything which
// stays indented like this.
if ( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > $this->indentation ) )
{
/* DEBUG
echo " -> Whitespace indentation.\n";
// /DEBUG */
$indentation = clone $tokens[0];
}
else
{
// We require indenteation here, so return, when the follow up text
// is not indented at all.
/* DEBUG
echo " -> No indented text.\n";
// /DEBUG */
return $collected;
}
$minIndentation = iconv_strlen( $indentation->content, 'UTF-8' );
while ( ( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > $this->indentation ) ) )
{
if ( $tokens[0]->type === ezcDocumentRstToken::NEWLINE )
{
$collected[] = $token = $tokens->shift();
// Just skip empty lines
/* DEBUG
echo " -> Skip empty line.\n";
// /DEBUG */
continue;
}
// Update minimum indentation
$whitespace = $tokens->shift();
$minIndentation = min( iconv_strlen( $whitespace->content, 'UTF-8' ), $minIndentation );
// Read all further nodes until the next newline, and check for
// indentation again then.
/* DEBUG
echo " -> Collect: ";
// /DEBUG */
do {
/* DEBUG
echo ".";
// /DEBUG */
$collected[] = $token = $tokens->shift();
} while ( $token->type !== ezcDocumentRstToken::NEWLINE );
/* DEBUG
echo "\n";
// /DEBUG */
}
// Add last to to stack again, is useful for common reduction handling.
$tokens->unshift( $token );
// Remove minimum found indentation from indentation tokens.
foreach ( $collected as $token )
{
if ( $token->position <= 1 )
{
$token->content = substr( $token->content, $minIndentation );
}
}
/* DEBUG
echo " => Collected " . count( $collected ) . " tokens.\n";
// /DEBUG */
return $collected;
}
/**
* Shift directives
*
* Shift directives as a subaction of the shiftComment method, though the
* signature differs from the common shift methods.
*
* This method aggregated options and parameters of directives, but leaves
* the content aggregation to the common comment aggregation.
*
* @param ezcDocumentRstDirectiveNode $directive
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstDirectiveNode
*/
protected function shiftDirective( ezcDocumentRstDirectiveNode $directive, ezcDocumentRstStack $tokens )
{
// All nodes until the first newline are the so called parameters of
// the directive.
$parameters = '';
$directiveTokens = array();
while ( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE )
{
$token = $tokens->shift();
$parameters .= $token->content;
$directiveTokens[] = $token;
}
/* DEBUG
echo " -> Set directive parameter: $parameters\n";
// /DEBUG */
$directive->parameters = $parameters;
$directive->tokens = $directiveTokens;
$tokens->shift();
// If there are two newlines, there are no options.
if ( $tokens[0]->type == ezcDocumentRstToken::NEWLINE )
{
return $directive;
}
// After that there may be options, which are indented and start with a
// colon.
while ( isset( $tokens[0] ) &&
( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
isset( $tokens[1] ) &&
( $tokens[1]->content === ':' ) &&
isset( $tokens[3] ) &&
( $tokens[3]->content === ':' ) )
{
$tokens->shift();
$tokens->shift();
// Extract option name
$name = '';
while ( isset( $tokens[0] ) &&
$tokens[0]->content !== ':' )
{
$token = $tokens->shift();
$name .= $token->content;
}
$tokens->shift();
// Extract option value
$value = '';
while ( isset( $tokens[0] ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) )
{
$token = $tokens->shift();
$value .= $token->content;
}
// Assign option on directive
$tokens->shift();
$directive->options[$name] = $value;
/* DEBUG
echo " -> Set directive option: $name => $value\n";
// /DEBUG */
}
// Leave everything else up to the comment shifter
return $directive;
}
/**
* Handle special directives
*
* Handle special directives like replace, which require reparsing of the
* directives contents, which is not possible to do during visiting, but is
* required to already be done inside the parser.
*
* @param ezcDocumentRstSubstitutionNode $substitution
* @param ezcDocumentRstDirectiveNode $node
* @return ezcDocumentRstSubstitutionNode
*/
protected function handleSpecialDirectives( ezcDocumentRstSubstitutionNode $substitution, ezcDocumentRstDirectiveNode $node )
{
// Special handling for some directives
switch ( strtolower( $node->identifier ) )
{
case 'replace':
// Reenter the parser with the replace directive contents
// and assign the contents from the first paragraph to the
// substitution target node.
if ( !count( $node->tokens ) )
{
$substitution->nodes = array();
break;
}
$document = $this->reenterParser( $node->tokens );
$contents = array();
foreach ( $document->nodes as $section )
{
$contents = array_merge( $contents, $section->nodes );
}
$substitution->nodes = $contents;
break;
// The date and unicode directives would be required to be
// handled similarly.
}
return $substitution;
}
/**
* Shift comment
*
* Shift comments. Comments are introduced by '..' and just contain text.
* There are several other block, which are introduced the same way, but
* where the first token determines the actual type.
*
* This method implements the parsing and detection of those different
* items.
*
* Comments are basically described here, but there are crosscutting
* concerns throughout the complete specification:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#comments
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftComment( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== '..' ) ||
( $token->position > ( $this->indentation + 1 ) ) ||
( !isset( $tokens[0] ) ) ||
( ( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ) ||
( isset( $tokens[1] ) &&
( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ) )
{
// All types handled by this method are introduced by a token
// containing '..' at the very beginning of the line, followed by a
// whitespace.
return false;
}
// Ignore the following whitespace
$tokens->shift();
// The next tokens determine which type of structure we found, while
// everything which is not handled by a special case falls back to a
// comment.
$determined = false;
$substitution = null;
while ( !$determined )
{
switch ( true )
{
case $tokens[0]->type === ezcDocumentRstToken::TEXT_LINE:
// We may have found a directive. Aggregate the identifier and
// check for two colons after that.
$identifierTokens = array();
$identifier = '';
/* DEBUG
echo " -> This may be a directive...\n";
// /DEBUG */
while ( ( $tokens[0]->type === ezcDocumentRstToken::TEXT_LINE ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( in_array( $tokens[0]->content[0], array( '-', '_', '.' ) ) ) ) )
{
$identifierTokens[] = $iToken = $tokens->shift();
$identifier .= $iToken->content;
}
// Right after the identifier there should be a double colon,
// otherwise this is just a plain comment.
if ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '::' ) )
{
/* DEBUG
echo " -> Found directive.\n";
// /DEBUG */
$tokens->shift();
$node = new ezcDocumentRstDirectiveNode( $token, strtolower( trim( $identifier ) ) );
// The shiftDirective method aggregates options and
// parameters of the directive and the contents will be
// aggregated later by the common comment functionality.
$this->shiftDirective( $node, $tokens );
$determined = true;
}
else
{
/* DEBUG
echo " -> Just a comment.\n";
// /DEBUG */
// We create a comment node, where all following contents
// may also be aggregated.
$node = new ezcDocumentRstCommentNode( $token );
$determined = true;
// Add tokens used for type detection to the begin of
// the comment node.
foreach ( $identifierTokens as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
}
break;
case ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '|' ) ):
/* DEBUG
echo " -> Found a substitution target.\n";
// /DEBUG */
// We found a substitution directive. It is identified by the
// text between the pipes and the reenters this parsing
// process.
$name = array_merge(
array( $tokens->shift() ),
$this->readUntil( $tokens, array(
array(
'type' => ezcDocumentRstToken::NEWLINE,
),
array(
'type' => ezcDocumentRstToken::SPECIAL_CHARS,
'content' => '|',
),
) )
);
// Right after the identifier there should be a double colon,
// otherwise this is just a plain comment.
if ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '|' ) )
{
$name[] = $tokens->shift();
/* DEBUG
echo " -> Substitution target successfully found.\n";
// /DEBUG */
$substitution = new ezcDocumentRstSubstitutionNode( $token, array_slice( $name, 1, -1 ) );
// After we found a substitution directive, we reenter
// the process to find a associated directive.
// Skip following whitespace
if ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE )
{
$tokens->shift();
}
}
else
{
/* DEBUG
echo " -> Just a comment.\n";
// /DEBUG */
// We create a comment node, where all following contents
// may also be aggregated.
$node = new ezcDocumentRstCommentNode( $token );
$determined = true;
// Add tokens used for type detection to the begin of
// the comment node.
foreach ( $name as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
}
break;
case ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '[' ) ):
/* DEBUG
echo " -> Found a potential footnote target.\n";
// /DEBUG */
// We found a substitution directive. It is identified by the
// text between the pipes and the reenters this parsing
// process.
$name = array_merge(
array( $tokens->shift() ),
$this->readUntil( $tokens, array(
array(
'type' => ezcDocumentRstToken::NEWLINE,
),
array(
'type' => ezcDocumentRstToken::SPECIAL_CHARS,
'content' => ']',
),
) )
);
// Right after the identifier there should be a double colon,
// otherwise this is just a plain comment.
if ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === ']' ) )
{
$name[] = $tokens->shift();
/* DEBUG
echo " -> Footnote target successfully found.\n";
// /DEBUG */
$node = new ezcDocumentRstFootnoteNode( $token, $name = array_slice( $name, 1, -1 ), $this->detectFootnotetype( $name ) );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$content = $this->readMutlipleIndentedLines( $tokens, true );
$section = $this->reenterParser( $content );
$node->nodes = $section->nodes;
// There is nothing more to read. We can exit immediately.
return $node;
}
else
{
/* DEBUG
echo " -> Just a comment.\n";
// /DEBUG */
// We create a comment node, where all following contents
// may also be aggregated.
$node = new ezcDocumentRstCommentNode( $token );
$determined = true;
// Add tokens used for type detection to the begin of
// the comment node.
foreach ( $name as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
}
break;
case ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '_' ) ):
/* DEBUG
echo " -> Found a named reference target.\n";
// /DEBUG */
// We found a named reference target. It is identified by a
// starting underscrore, followed by the reference name,
// and the reference target.
$tokens->shift();
$name = $this->readUntil( $tokens, array(
array(
'type' => ezcDocumentRstToken::NEWLINE,
),
array(
'type' => ezcDocumentRstToken::SPECIAL_CHARS,
'content' => ':',
),
) );
// Right after the identifier there should be a double colon,
// otherwise this is just a plain comment.
if ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === ':' ) )
{
$tokens->shift();
$tokens->shift();
/* DEBUG
echo " -> Named reference target successfully found.\n";
// /DEBUG */
$node = new ezcDocumentRstNamedReferenceNode( $token, $name );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$content = $this->readMutlipleIndentedLines( $tokens, true );
foreach ( $content as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
// There is nothing more to read. We can exit immediately.
return $node;
}
else
{
/* DEBUG
echo " -> Just a comment.\n";
// /DEBUG */
// We create a comment node, where all following contents
// may also be aggregated.
$node = new ezcDocumentRstCommentNode( $token );
$determined = true;
// Add tokens used for type detection to the begin of
// the comment node.
foreach ( $name as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
}
break;
case ( ( $tokens[0]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content === '__' ) &&
( isset( $tokens[1] ) ) &&
( $tokens[1]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[1]->content === ':' ) ):
/* DEBUG
echo " -> Found a anonymous reference target.\n";
// /DEBUG */
// We found a anonymous reference target. It is identified
// by two starting underscrores, directly followed by a
// colon.
$tokens->shift();
$tokens->shift();
$tokens->shift();
$node = new ezcDocumentRstAnonymousReferenceNode( $token );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$content = $this->readMutlipleIndentedLines( $tokens, true );
foreach ( $content as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
// There is nothing more to read. We can exit immediately.
return $node;
default:
// Everything else starting with '..' is just a comment.
/* DEBUG
echo " -> Found comment.\n";
// /DEBUG */
$node = new ezcDocumentRstCommentNode( $token );
$determined = true;
break;
}
}
// Set current indentation on returned node
$node->indentation = $this->indentation;
// If this is part of a substitution reference, we return the
// substitution after the process and not just the plain node.
if ( $substitution !== null )
{
$substitution->nodes = array( $node );
$return = $substitution;
}
else
{
$return = $node;
}
// Check if this is a short directive - in this case we skip the
// following aggregation and return the directive directly.
if ( ( ( $node instanceof ezcDocumentRstDirectiveNode ) &&
( in_array( $node->identifier, $this->shortDirectives, true ) ) ) ||
( !count( $tokens ) ) )
{
// Handle special cases like the replace directive
if ( $substitution !== null )
{
$return = $this->handleSpecialDirectives( $substitution, $node );
}
return $return;
}
// Skip all empty lines first
$skippedLine = null;
while ( $tokens[0]->type === ezcDocumentRstToken::NEWLINE )
{
/* DEBUG
echo " -> Skip newline.\n";
// /DEBUG */
$skippedLine = $tokens->shift();
}
// Once we got the first line after the literal block start marker, we
// check for the quoting style
if ( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > $this->indentation ) )
{
// In case of a whitespace indentation token, this is used
// completely as indentation marker.
/* DEBUG
echo " -> Detected whitespace indetation..\n";
// /DEBUG */
$baseIndetation = clone $tokens[0];
}
else
{
// If no qouting could be detected, we are finished now, and the
// comment / directive / ... has no more content.
// Readd the last newline to the token stack
if ( $skippedLine !== null )
{
$tokens->unshift( $skippedLine );
}
// Handle special cases like the replace directive
if ( $substitution !== null )
{
$return = $this->handleSpecialDirectives( $substitution, $node );
}
return $return;
}
// Collect all contents, until the indentation changes.
$collected = array();
$directiveTokens = array();
while ( // Empty lines are inlcuded.
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
// All other lines must start with the determined base
// indentation
( ( $tokens[0]->type === $baseIndetation->type ) &&
( ( strpos( $tokens[0]->content, $baseIndetation->content ) === 0 ) ||
( ( $baseIndetation->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $tokens[0]->content, 'UTF-8' ) > $this->indentation ) ) ) ) )
{
$literalToken = $tokens->shift();
if ( $literalToken->type === ezcDocumentRstToken::NEWLINE )
{
// Nothing to do for empty lines, but they are included in the
// literal block.
/* DEBUG
echo " -> Collected plain newline.\n";
// /DEBUG */
$collected[] = new ezcDocumentRstLiteralNode( $literalToken );
$directiveTokens[] = $literalToken;
continue;
}
// Remove whitespaces used for indentation in literal blocks
/* DEBUG
echo " -> Handle whitespace indentation.\n";
// /DEBUG */
$directiveTokens[] = clone $literalToken;
$literalToken->content = substr( $literalToken->content, strlen( $baseIndetation->content ) );
$collected[] = new ezcDocumentRstLiteralNode( $literalToken );
// Just collect everything until we reach a newline, the the check
// starts again.
/* DEBUG
echo " -> Collect: ";
// /DEBUG */
do {
/* DEBUG
echo ".";
// /DEBUG */
$collected[] = new ezcDocumentRstLiteralNode( $item = $tokens->shift() );
$directiveTokens[] = $item;
}
while ( $item->type !== ezcDocumentRstToken::NEWLINE );
/* DEBUG
echo "\n";
// /DEBUG */
}
/* DEBUG
echo " => Finished collecting.\n";
// /DEBUG */
// Readd the last newline to the token stack
$tokens->unshift( $item );
// Nothing more could be collected, either because the indentation has
// been reduced, or the markers are missing. Add the aggregated
// contents to the node and return it.
$node->nodes = array_merge(
$node->nodes,
$collected
);
// For special directives like the replace directive we also need to
// aggregate all found tokens on the tokens property
if ( $node instanceof ezcDocumentRstDirectiveNode )
{
$node->tokens = array_merge(
$node->tokens,
$directiveTokens
);
if ( $node->identifier !== 'replace' )
{
$node->tokens = $this->realignTokens( $node->tokens );
}
}
// Handle special cases like the replace directive
if ( $substitution !== null )
{
$return = $this->handleSpecialDirectives( $substitution, $node );
}
return $return;
}
/**
* Shift anonymous reference target
*
* Shift the short version of anonymous reference targets, the long version
* is handled in the shiftComment() method. Both are specified at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#anonymous-hyperlinks
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftAnonymousReference( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== '__' ) ||
( $token->position !== 1 ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) )
{
// This does not fulfill the requirements for a short anonymous
// hyperling reference.
return false;
}
// Shift whitespace
$tokens->shift();
$node = new ezcDocumentRstAnonymousReferenceNode( $token );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$content = $this->readMutlipleIndentedLines( $tokens, true );
foreach ( $content as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
// There is nothing more to read. We can exit immediately.
return $node;
}
/**
* Shift field lists
*
* Shift field lists, which are introduced by a term surrounded by columns
* and any text following. Field lists are specified at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#field-lists
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftFieldList( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->content !== ':' ) ||
( $token->position > 1 ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( isset( $this->documentStack[0] ) &&
( in_array( $this->documentStack[0]->type, $this->textNodes ) ) ) )
{
// All types handled by this method are introduced by a token
// containing ':' at the very beginning of the line, followed by
// text.
return false;
}
$name = $this->readUntil( $tokens, array(
array(
'type' => ezcDocumentRstToken::NEWLINE,
),
array(
'type' => ezcDocumentRstToken::SPECIAL_CHARS,
'content' => ':',
),
)
);
if ( ( $tokens[0]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[0]->content !== ':' ) )
{
// Check that the read read stopped at the field list name end
// marker, otherwise this is just some random text, at least no
// valid field list.
$tokens->prepend( $name );
return false;
}
// Ignore the closing ':'.
$tokens->shift();
// Skip all empty lines before text starts
while ( ( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) ||
( ( $tokens[0]->type === ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[1]->type === ezcDocumentRstToken::NEWLINE ) ) )
{
$tokens->shift();
}
// Read all text, following the field list name
$node = new ezcDocumentRstFieldListNode( $token, $name );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$content = $this->readMutlipleIndentedLines( $tokens, true );
foreach ( $content as $subtoken )
{
$node->nodes[] = new ezcDocumentRstLiteralNode( $subtoken );
}
// There is nothing more to read. We can exit immediately.
return $node;
}
/**
* Read simple cells
*
* Read cells as defined in simple tables. Cells are maily structured by
* whitespaces, but may also exceed one cell.
*
* Returns an array with cells, ordered by their rows and columns.
*
* @param array $cellStarts
* @param ezcDocumentRstStack $tokens
* @return array
*/
protected function readSimpleCells( $cellStarts, &$tokens )
{
/* DEBUG
echo " -> Read simple table cells.";
// /DEBUG */
// Two dimensiponal structure with the actual cell contents.
$cellContents = array();
$row = -1;
// Read until we got some kind of definition line.
while ( ( ( !isset( $tokens[0] ) ) ||
( $tokens[0]->position > 1 ) ||
( $tokens[0]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( ( $tokens[0]->content[0] !== '=' ) &&
( $tokens[0]->content[0] !== '-' ) ) ||
( !isset( $tokens[1] ) ) ||
( ( $tokens[1]->type !== ezcDocumentRstToken::WHITESPACE ) &&
( $tokens[1]->type !== ezcDocumentRstToken::NEWLINE ) ) ) &&
( $token = $tokens->shift() ) )
{
// Increase row number, if the we get non-whitespace content in the
// first cell
if ( ( ( $row === -1 ) &&
( $token->position === 1 ) ) ||
( ( $row !== -1 ) &&
( $token->position === 1 ) &&
( $token->type !== ezcDocumentRstToken::WHITESPACE ) &&
( $token->type !== ezcDocumentRstToken::NEWLINE ) ) )
{
++$row;
$column = false;
/* DEBUG
echo "\n -> Row $row: ";
// /DEBUG */
}
// Determine column for current content.
foreach ( $cellStarts as $nr => $position )
{
if ( $position == $token->position )
{
$column = $nr;
/* DEBUG
echo "Increase: $column, ";
// /DEBUG */
break;
}
}
if ( $column === false )
{
$column = $nr;
/* DEBUG
echo "Init: $column, ";
// /DEBUG */
}
// Check if we need to split up the token, because of a single
// seperating whitespace at the column boundings.
if ( isset( $cellStarts[$column + 1] ) &&
( isset( $token->content[$split = $cellStarts[$column + 1] - $token->position - 1] ) ) &&
( strlen( $token->content ) > ( $split + 1 ) ) &&
( $token->content[$split] === ' ' ) )
{
/* DEBUG
echo "Split, ";
// /DEBUG */
$newToken = clone( $token );
$token->content = substr( $token->content, 0, $split );
$newToken->content = substr( $newToken->content, $split + 1 );
$newToken->position = $newToken->position + $split + 1;
$tokens->unshift( $newToken );
}
// Append contents to column
$cellContents[$row][$column][] = $token;
}
/* DEBUG
echo "\n";
// /DEBUG */
return $cellContents;
}
/**
* Read simple table specifications
*
* Read the column specification headers of a simple table and return the
* sizes of the specified columns in an array.
*
* @param ezcDocumentRstStack $tokens
* @return array
*/
protected function readSimpleTableSpecification( &$tokens )
{
// Detect the cell sizes inside of the simple table.
$tableSpec = array();
/* DEBUG
echo " -> Table specification: ";
// /DEBUG */
while ( isset( $tokens[0] ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) )
{
$specToken = $tokens->shift();
if ( ( ( $specToken->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( ( $specToken->content[0] === '=' ) ||
( $specToken->content[0] === '-' ) ) ) ||
( ( $specToken->type === ezcDocumentRstToken::WHITESPACE ) &&
( iconv_strlen( $specToken->content, 'UTF-8' ) >= 1 ) ) )
{
$tableSpec[] = array( $specToken->type, strlen( $specToken->content ) );
/* DEBUG
echo strlen( $specToken->content ), ", ";
// /DEBUG */
}
else
{
$this->triggerError(
E_PARSE,
'Invalid token in simple table specifaction.',
null, $specToken->line, $specToken->position
);
}
}
$tokens->shift();
/* DEBUG
echo "\n";
// /DEBUG */
return $tableSpec;
}
/**
* Shift simple table
*
* "Simple tables" are not defined by a complete grid, but only by top and
* bottome lines. There exact specification can be found at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#simple-tables
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftSimpleTable( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->position > 1 ) ||
( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $token->content[0] !== '=' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::WHITESPACE ) ||
( !isset( $tokens[1] ) ) ||
( $tokens[1]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[1]->content[0] !== '=' ) )
{
// Missing multiple special character groups only containing '=',
// separated by whitespaces, which introduce a simple table.
return false;
}
/* DEBUG
echo " -> Found simple table.\n";
// /DEBUG */
// Detect the cell sizes inside of the simple table.
$tokens->unshift( $token );
$tableSpec = $this->readSimpleTableSpecification( $tokens );
// Refactor specification to work with it more easily.
$cellStarts = array();
$position = 1;
foreach ( $tableSpec as $cell )
{
if ( $cell[0] === ezcDocumentRstToken::SPECIAL_CHARS )
{
$cellStarts[] = $position;
}
$position += $cell[1];
}
// Read all titles, which may be multiple rows, each sparated by '-'.
$titles = array();
do
{
$titles = array_merge(
$titles,
$this->readSimpleCells( $cellStarts, $tokens )
);
} while ( isset( $tokens[0] ) &&
( $tokens[0]->type == ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content[0] === '-' ) &&
( $tokens[0]->position === 1 ) &&
( isset( $tokens[1] ) ) &&
( ( $tokens[1]->type == ezcDocumentRstToken::WHITESPACE ) ||
( $tokens[1]->type == ezcDocumentRstToken::NEWLINE ) ) &&
// We ignore the actual header undeline table cell
// redefinition, as we detect this magically while reading
// the cells already.
$this->readSimpleTableSpecification( $tokens ) );
// After the titles we get another specification line, which should
// match the top specification
if ( $tableSpec !== $this->readSimpleTableSpecification( $tokens ) )
{
$this->triggerError(
E_WARNING,
'Table specification mismatch in simple table.',
null, $token->line, $token->position
);
}
if ( !isset( $tokens[0] ) ||
( $tokens[0]->type === ezcDocumentRstToken::NEWLINE ) )
{
// The simple table only contains a body
$contents = $titles;
$titles = array();
}
else
{
$contents = array();
do
{
$contents = array_merge(
$contents,
$this->readSimpleCells( $cellStarts, $tokens )
);
} while ( isset( $tokens[0] ) &&
( $tokens[0]->type == ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $tokens[0]->content[0] === '-' ) &&
( $tokens[0]->position === 1 ) &&
( isset( $tokens[1] ) ) &&
( $tokens[1]->type == ezcDocumentRstToken::WHITESPACE ) &&
// We ignore the actual header undeline table cell
// redefinition, as we detect this magically while reading
// the cells already.
$this->readSimpleTableSpecification( $tokens ) );
// After the titles we get another specification line, which should
// match the top specification
if ( $tableSpec !== $this->readSimpleTableSpecification( $tokens ) )
{
$this->triggerError(
E_WARNING,
'Table specification mismatch in simple table.',
null, $token->line, $token->position
);
}
}
// Reenter parser for table titels and contents, and create table AST
// from it.
$table = new ezcDocumentRstTableNode( $token );
if ( count( $titles ) )
{
$table->nodes[] = $head = new ezcDocumentRstTableHeadNode( $token );
$lastCell = null;
$lastCNr = null;
foreach ( $titles as $rNr => $row )
{
$head->nodes[$rNr] = $tableRow = new ezcDocumentRstTableRowNode( $token );
foreach ( $row as $cNr => $cell )
{
/* DEBUG
echo "\n -> Reenter parser for tokens $rNr, $cNr\n";
// /DEBUG */
$section = $this->reenterParser( $cell );
$tableRow->nodes[$cNr] = $tableCell = new ezcDocumentRstTableCellNode( reset( $cell ) );
$tableCell->nodes = $section->nodes;
// Set colspan, if required
if ( ( $lastCNr !== null ) &&
( $lastCNr < ( $cNr - 1 ) ) )
{
$lastCell->colspan = $cNr - $lastCNr;
}
$lastCNr = $cNr;
$lastCell = $tableCell;
}
$lastCNr = null;
}
}
$table->nodes[] = $body = new ezcDocumentRstTableBodyNode( $token );
$lastCell = null;
$lastCNr = null;
foreach ( $contents as $rNr => $row )
{
$body->nodes[$rNr] = $tableRow = new ezcDocumentRstTableRowNode( $token );
foreach ( $row as $cNr => $cell )
{
/* DEBUG
echo "\n -> Reenter parser for tokens $rNr, $cNr\n";
// /DEBUG */
$section = $this->reenterParser( $cell );
$tableRow->nodes[$cNr] = $tableCell = new ezcDocumentRstTableCellNode( reset( $cell ) );
$tableCell->nodes = $section->nodes;
// Set colspan, if required
if ( ( $lastCNr !== null ) &&
( $lastCNr < ( $cNr - 1 ) ) )
{
$lastCell->colspan = $cNr - $lastCNr;
}
$lastCNr = $cNr;
$lastCell = $tableCell;
}
$lastCNr = null;
}
return $table;
}
/**
* Read grid table specifications
*
* Read the column specification headers of a grid table and return the
* sizes of the specified columns in an array.
*
* @param ezcDocumentRstStack $tokens
* @return array
*/
protected function readGridTableSpecification( &$tokens )
{
// Detect the cell sizes inside of the grid table.
$tableSpec = array();
/* DEBUG
echo " -> Table specification: ";
// /DEBUG */
$i = -1;
do {
++$i;
while ( $tokens[$i]->type !== ezcDocumentRstToken::NEWLINE )
{
if ( ( $tokens[$i]->content === '+' ) &&
( ( isset( $tokens[$i - 1] ) ) &&
( ( $tokens[$i - 1]->type === ezcDocumentRstToken::NEWLINE ) ||
( ( $tokens[$i - 1]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( ( $tokens[$i - 1]->content[0] === '=' ) ||
( $tokens[$i - 1]->content[0] === '-' ) ) ) ) ) &&
( ( isset( $tokens[$i + 1] ) ) &&
( ( $tokens[$i + 1]->type === ezcDocumentRstToken::NEWLINE ) ||
( ( $tokens[$i + 1]->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( ( $tokens[$i + 1]->content[0] === '=' ) ||
( $tokens[$i + 1]->content[0] === '-' ) ) ) ) ) )
{
$tableSpec[] = $tokens[$i]->position;
}
++$i;
}
} while ( ( $tokens[$i]->type !== ezcDocumentRstToken::NEWLINE ) ||
( $tokens[$i + 1]->type !== ezcDocumentRstToken::NEWLINE ) );
/* DEBUG
echo "\n";
// /DEBUG */
$tableSpec = array_unique( $tableSpec );
sort( $tableSpec );
return $tableSpec;
}
/**
* Shift grid table
*
* In "Grid tables" the values are embedded in a complete grid visually
* describing a a table using characters.
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#grid-tables
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftGridTable( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
if ( ( $token->position > 1 ) ||
( $token->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $token->content !== '+' ) ||
( !isset( $tokens[0] ) ) ||
( $tokens[0]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[0]->content[0] !== '-' ) ||
( !isset( $tokens[1] ) ) ||
( $tokens[1]->type !== ezcDocumentRstToken::SPECIAL_CHARS ) ||
( $tokens[1]->content !== '+' ) )
{
// Missing multiple special character groups only containing '=',
// separated by whitespaces, which introduce a simple table.
return false;
}
/* DEBUG
echo " -> Found grid table.\n";
// /DEBUG */
// Detect the cell sizes inside of the grid table.
$rowOffset = $token->line;
$tokens->unshift( $token );
$tableSpec = $this->readGridTableSpecification( $tokens );
// Read all table tokens and extract the complete cell specification of
// the table.
$tableTokens = array();
$titleRow = 0;
$cells = array();
$row = 0;
while ( ( $tableTokens[] = $token = $tokens->shift() ) &&
// Read until we find two newlines, which indicate the end of
// the table
( ( $token->type !== ezcDocumentRstToken::NEWLINE ) ||
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ) )
{
if ( ( $position = array_search( $token->position, $tableSpec, true ) ) !== false )
{
/* DEBUG
echo " -> Token at cell position: ";
// /DEBUG */
// Token may be relevant for the table structure, as it resides
// at the entry points of the table specification.
switch ( true )
{
case ( ( $token->content === '+' ) &&
( isset( $tokens[0] ) ) &&
( $tokens[0]->content[0] === '=' ) ):
$titleRow = $row;
case ( ( $token->content === '+' ) &&
( isset( $tokens[0] ) ) &&
( $tokens[0]->content[0] === '-' ) ):
/* DEBUG
echo "Row breaker: $position\n";
// /DEBUG */
$cells[$row][$position] = ( $tokens[0]->content[0] === '-' ? 2 : 3 );
break;
case ( ( ( $token->content === '|' ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ) ||
( ( $token->content === '+' ) &&
( $tokens[0]->type !== ezcDocumentRstToken::NEWLINE ) ) ):
/* DEBUG
echo "Cell: $position\n";
// /DEBUG */
$cells[$row][$position] = 1;
break;
default:
/* DEBUG
echo "irrelevant\n";
// /DEBUG */
}
}
elseif ( $token->type === ezcDocumentRstToken::NEWLINE )
{
/* DEBUG
echo " -> Next row.\n";
// /DEBUG */
++$row;
}
}
// Dump cell structure
/* DEBUG
foreach ( $cells as $rNr => $row )
{
$lcNr = 0;
foreach ( $row as $cNr => $cell )
{
for ( $i = $lcNr; $i < ( $cNr - 1 ); ++$i ) echo " ";
echo ( $cell === 1 ? '|> ' : ( $cell === 2 ? '----' : '====' ) );
$lcNr = $cNr;
}
echo "\n";
}
// /DEBUG */
// Clean up cell structure: Remove cell seperators, which actually
// aren't cell seperators because they are not followed or preceeded by
// cell seperators.
/* DEBUG
echo " -> Clean up cell structure\n";
// /DEBUG */
$rowCount = count( $cells );
foreach ( $cells as $rNr => $row )
{
foreach ( $row as $cNr => $cell )
{
if ( $cell !== 1 )
{
// Skip everything but cell seperators
continue;
}
if ( ( $rNr > 0 ) &&
( !isset( $cells[$rNr - 1][$cNr] ) ) )
{
/* DEBUG
echo " -> Remove superflous cell seperator (NP) in $rNr * $cNr\n";
// /DEBUG */
unset( $cells[$rNr][$cNr] );
}
elseif ( ( $rNr < ( $rowCount - 1 ) ) &&
( !isset( $cells[$rNr + 1][$cNr] ) ) )
{
/* DEBUG
echo " -> Remove superflous cell seperator (NF) in $rNr * $cNr\n";
// /DEBUG */
unset( $cells[$rNr][$cNr] );
}
}
}
// Dump cell structure
/* DEBUG
foreach ( $cells as $rNr => $row )
{
$lcNr = 0;
foreach ( $row as $cNr => $cell )
{
for ( $i = $lcNr; $i < ( $cNr - 1 ); ++$i ) echo " ";
echo ( $cell === 1 ? '|> ' : ( $cell === 2 ? '----' : '====' ) );
$lcNr = $cNr;
}
echo "\n";
}
// /DEBUG */
$columnNumber = array();
$cellMapping = array();
$rowCount = count( $cells );
$cellCount = count( $tableSpec ) - 1;
// Initilize column number array
for ( $c = 0; $c < $cellCount; ++$c )
{
$rowNumber[$c] = 0;
}
// Create cell mapping array
for ( $r = 0; $r < $rowCount; ++$r )
{
for ( $c = 0; $c < $cellCount; ++$c )
{
if ( !isset( $cells[$r][$c] ) )
{
// No explicit cell definition given. Map to last cell in
// current row.
$row = $rowNumber[$c];
// It may happen for cell seperators, that the last cell is
// not available. It is save to skip this case.
if ( !isset( $cellMapping[$r][$c - 1] ) )
{
continue;
}
$lastCell = $cellMapping[$r][$c - 1];
if ( ( $lastCell[0] !== $row ) ||
( $lastCell[1] !== ( $c - 1 ) ) )
{
// Last cell has already been mapped, use the map
// destination from this cell. We do not need to do
// this recusively, because we iterate in single steps
// over the table and fix each redirection immediately,
// so all prior cells already point to their final
// location.
$cellMapping[$r][$c] = $lastCell;
}
else
{
// Otherwise map to last cell
$cellMapping[$r][$c] = array( $row, $c - 1 );
}
}
elseif ( $cells[$r][$c] === 1 )
{
// New cell, just add to mapping table.
$cellMapping[$r][$c] = array( $rowNumber[$c], $c );
}
elseif ( $cells[$r][$c] > 1 )
{
// We found a row breaker, so increase the future row
// number for the current column.
//
// The increased column number is the maximum of the
// current column + 1 and all other columns, because we
// want to keep up to a same row number in one row.
$rowNumber[$c] = max(
$rowNumber[$c] + 1,
max( $rowNumber ),
max( array_slice( $rowNumber, $c ) ) + 1
);
}
}
}
/* DEBUG
foreach ( $cellMapping as $rNr => $row )
{
echo $rNr, ": ";
foreach ( $row as $cNr => $cell )
{
echo "$cNr(", $cell[0], ", ", $cell[1], ") ";
}
echo "\n";
}
// /DEBUG */
// Iterate over cell mapping array to calculate cell spans
$cellSpans = array();
$rNr = 1;
foreach ( $cellMapping as $nr => $row )
{
// Determine maximum row number in current row
$maxNr = 0;
foreach ( $row as $cell )
{
$maxNr = max( $maxNr, $cell[0] );
}
if ( $rNr > $maxNr )
{
continue;
}
// Increase row and colspan depending on the cell pointer.
foreach ( $row as $cNr => $cell )
{
if ( ( $rNr === $cell[0] ) &&
( $cNr === $cell[1] ) )
{
// It is the cell itself
$cellSpans[$cell[0]][$cell[1]] = array( 1, 1 );
}
elseif ( $cNr === $cell[1] )
{
// Another cell pointer in the same column
$cellSpans[$cell[0]][$cell[1]][0]++;
}
elseif ( $rNr === $cell[0] )
{
// Another cell pointer in the same row
$cellSpans[$cell[0]][$cell[1]][1]++;
}
}
$rNr = $maxNr + 1;
}
// Now we can reiterate over the cell tokens array and assign all
// tokens to their correct cells.
$cell = 0;
$row = 0;
$contents = array();
$titles = array();
$current = &$titles;
foreach ( $tableTokens as $token )
{
// Newline tokens are only used to skip into the next row, but
// should also be added to each cell, to maintain the wrapping
// iside of cells.
if ( $token->type === ezcDocumentRstToken::NEWLINE )
{
if ( $row === $titleRow )
{
// Switch current cell storage to table contents, once we
// got past the title row seperator.
$current = &$contents;
}
// Sppend the newline token to all current cells
foreach ( $tableSpec as $col => $pos )
{
if ( isset( $cellMapping[$row] ) && isset( $cellMapping[$row][$col] ) )
{
list( $r, $c ) = $cellMapping[$row][$col];
$current[$r][$c][] = $token;
}
}
++$row;
$cell = 0;
continue;
}
// Check if this is a spec token, we want to ignore.
if ( ( ( $position = array_search( $token->position, $tableSpec, true ) ) !== false ) &&
( ( isset( $cells[$row][$position] ) ) ||
( ( $position + 1 ) >= count( $tableSpec ) ) ) )
{
// Skip spec token.
continue;
}
// Check if entered the next column by checking the current token
// position as the column offsets in the table spcification.
if ( $token->position >= $tableSpec[$cell + 1] )
{
++$cell;
}
// Get the actual destination cell from the table mapping array. If
// there is no entry in the cell mapping array, the token is a
// column breaker, and can safely be ignored.
if ( isset( $cellMapping[$row] ) && isset( $cellMapping[$row][$cell] ) )
{
list( $r, $c ) = $cellMapping[$row][$cell];
$current[$r][$c][] = $token;
}
}
/* DEBUG
echo " -> Table contents:\n";
foreach ( $contents as $rNr => $row )
{
echo $rNr, ": ";
foreach ( $row as $cNr => $cell )
{
printf( '% 4d ', count( $cell ) );
}
echo "\n";
}
// /DEBUG */
// Reenter parser for table titels and contents, and create table AST
// from it.
$table = new ezcDocumentRstTableNode( $token );
if ( count( $titles ) )
{
$table->nodes[] = $head = new ezcDocumentRstTableHeadNode( $token );
foreach ( $titles as $rNr => $row )
{
$head->nodes[$rNr] = $tableRow = new ezcDocumentRstTableRowNode( $token );
foreach ( $row as $cNr => $cell )
{
/* DEBUG
echo "\n -> Reenter parser for tokens $rNr, $cNr\n";
// /DEBUG */
$section = $this->reenterParser( $cell );
$tableRow->nodes[$cNr] = $tableCell = new ezcDocumentRstTableCellNode( reset( $cell ) );
$tableCell->nodes = $section->nodes;
$tableCell->rowspan = $cellSpans[$rNr][$cNr][0];
$tableCell->colspan = $cellSpans[$rNr][$cNr][1];
}
}
}
$table->nodes[] = $body = new ezcDocumentRstTableBodyNode( $token );
foreach ( $contents as $rNr => $row )
{
$body->nodes[$rNr] = $tableRow = new ezcDocumentRstTableRowNode( $token );
foreach ( $row as $cNr => $cell )
{
/* DEBUG
echo "\n -> Reenter parser for tokens $rNr, $cNr\n";
// /DEBUG */
$section = $this->reenterParser( $cell );
$tableRow->nodes[$cNr] = $tableCell = new ezcDocumentRstTableCellNode( reset( $cell ) );
$tableCell->nodes = $section->nodes;
$tableCell->rowspan = $cellSpans[$rNr][$cNr][0];
$tableCell->colspan = $cellSpans[$rNr][$cNr][1];
}
}
return $table;
}
/**
* Shift definition lists
*
* Shift definition lists, which are introduced by an indentation change
* without speration by a paragraph. Because of this the method is called
* form the updateIndentation method, which handles such indentation
* changes.
*
* The text for the definition and its classifiers is already on the
* document stack because of this.
*
* Definition lists are specified at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#definition-lists
*
* @param ezcDocumentRstToken $token
* @param ezcDocumentRstStack $tokens
* @return ezcDocumentRstMarkupEmphasisNode
*/
protected function shiftDefinitionList( ezcDocumentRstToken $token, ezcDocumentRstStack $tokens )
{
// Fetch definition list back from document stack, where the text nodes
// are stacked in reverse order.
$name = array();
/* DEBUG
echo " -> Fetch name from document stack: ";
// /DEBUG */
do {
$node = $this->documentStack->shift();
$name[] = $node->token;
/* DEBUG
echo '.';
// /DEBUG */
} while ( isset( $this->documentStack[0] ) &&
in_array( $this->documentStack[0]->type, $this->textNodes, true ) );
/* DEBUG
echo "\n";
// /DEBUG */
$node = new ezcDocumentRstDefinitionListNode( $token, array_reverse( $name ) );
// With the name we find the associated contents, which
// may span multiple lines, so that this is done by a
// seperate method.
$tokens->unshift( $token );
/* DEBUG
echo " -> Read definition list contents\n";
// /DEBUG */
$content = $this->readMutlipleIndentedLines( $tokens, true );
array_shift( $content );
$section = $this->reenterParser( $content );
$node->nodes = $section instanceof ezcDocumentRstDocumentNode ? $section->nodes : $section;
// There is nothing more to read. We can exit immediately.
return $node;
}
/**
* Reduce all elements to one document node.
*
* @param ezcDocumentRstTitleNode $node
* @return void
*/
protected function reduceTitle( ezcDocumentRstTitleNode $node )
{
if ( !isset( $this->documentStack[0] ) ||
!in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
// This is a title top line, just skip for now.
return $node;
}
// Pop all text lines from stack and aggregate them into the title
$nodes = array();
$titleTextLength = 0;
while ( ( isset( $this->documentStack[0] ) ) &&
in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
$nodes[] = $textNode = $this->documentStack->shift();
/* DEBUG
echo " -> Add ", ezcDocumentRstNode::getTokenName( $textNode->type ), " to title.\n";
// /DEBUG */
if ( ( $titleTextLength > 0 ) ||
( $textNode->token->type !== ezcDocumentRstToken::WHITESPACE ) )
{
$titleTextLength += iconv_strlen( $textNode->token->content, 'UTF-8' );
}
}
// Trim whitespaces in text nodes.
if ( isset( $nodes[0] ) &&
( $nodes[0]->type === ezcDocumentRstNode::TEXT_LINE ) )
{
$nodes[0]->token->content = rtrim( $nodes[0]->token->content );
}
$nodes = array_reverse( $nodes );
if ( isset( $nodes[0] ) &&
( $nodes[0]->type === ezcDocumentRstNode::TEXT_LINE ) )
{
$nodes[0]->token->content = rtrim( $nodes[0]->token->content );
}
$node->nodes = $nodes;
// There is one additional whitespace appended because of the newline -
// remove it:
--$titleTextLength;
// Check if the lengths of the top line and the text matches.
if ( ( $titleLength = iconv_strlen( $node->token->content, 'UTF-8' ) ) < $titleTextLength )
{
$this->triggerError(
E_NOTICE,
"Title underline length ({$titleLength}) is shorter then text length ({$titleTextLength}).",
null, $node->token->line, $node->token->position
);
}
// Check if the title has a top line
$titleType = $node->token->content[0];
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TITLE ) )
{
$doubleTitle = $this->documentStack->shift();
$titleType = $doubleTitle->token->content[0] . $titleType;
// Ensure title over and underline lengths matches, for docutils
// this is a severe error.
if ( iconv_strlen( $node->token->content, 'UTF-8' ) !== iconv_strlen( $doubleTitle->token->content, 'UTF-8' ) )
{
$this->triggerError(
E_WARNING,
"Title overline and underline mismatch.",
null, $node->token->line, $node->token->position
);
}
}
// Get section nesting depth for title
if ( isset( $this->titleLevels[$titleType] ) )
{
$depth = $this->titleLevels[$titleType];
}
else
{
$this->titleLevels[$titleType] = $depth = count( $this->titleLevels ) + 1;
}
// Prepend section element to document stack
return new ezcDocumentRstSectionNode(
$node->token, $node, $depth
);
}
/**
* 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 ezcDocumentRstNode $node
* @return void
*/
protected function reduceSection( ezcDocumentRstNode $node )
{
// Collected node for prior section
$collected = array();
$lastSectionDepth = -1;
// Include all paragraphs, tables, lists and sections with a higher
// nesting depth
while ( $child = $this->documentStack->shift() )
{
/* DEBUG
echo " -> Try node: " . ezcDocumentRstNode::getTokenName( $child->type ) . ".\n";
// /DEBUG */
if ( !in_array( $child->type, $this->blockNodes, true ) )
{
$this->triggerError(
E_PARSE,
"Unexpected node: " . ezcDocumentRstNode::getTokenName( $child->type ) . ".",
null, $child->token->line, $child->token->position
);
}
if ( $child->type === ezcDocumentRstNode::SECTION )
{
if ( ( $child->depth <= $node->depth ) &&
( $node->depth !== 0 ) )
{
// If the found section has a same or higher level, just
// put everything back on the stack
$child->nodes = array_merge(
$child->nodes,
$collected
);
$this->documentStack->unshift( $child );
/* DEBUG
echo " -> Leave on stack.\n";
$this->dumpStack();
// /DEBUG */
return $node;
}
// Reduce document, if reached
if ( ( $child->depth == 0 ) &&
( $node->depth == 0 ) )
{
/* DEBUG
echo " -> Aggregate in root document node.\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
$collected
);
return $child;
}
// Check for title depth incosistency
if ( ( $lastSectionDepth - $child->depth ) > 1 )
{
$this->triggerError(
E_PARSE,
"Title depth inconsitency.",
null, $child->token->line, $child->token->position
);
}
if ( ( $lastSectionDepth === -1 ) ||
( $lastSectionDepth > $child->depth ) )
{
// If the section level is higher then in our new node and
// lower the the last node, reduce sections.
/* DEBUG
echo " -> Reduce section {$child->depth}.\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.
$lastSectionDepth = $child->depth;
}
/* DEBUG
echo " -> Found another child...\n";
// /DEBUG */
array_unshift( $collected, $child );
}
// No reduction found, put things back on stack.
/* DEBUG
echo " -> Put everything back on stack.\n";
// /DEBUG */
$this->documentStack->prepend( array_reverse( $collected ) );
return $node;
}
/**
* Reduce blockquote annotation content
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceBlockquoteAnnotationParagraph( ezcDocumentRstNode $node )
{
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::ANNOTATION ) )
{
// The last paragraph was preceded by an annotation marker
$annotation = $this->documentStack->shift();
$annotation->nodes = $node;
return $annotation;
}
return $node;
}
/**
* Reduce blockquote annotation
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceBlockquoteAnnotation( ezcDocumentRstNode $node )
{
// Do not reduce before it is filled with content
if ( count( $node->nodes ) < 1 )
{
return $node;
}
// It has already ensured by the shift, that the marker is preceeded by
// a blockquote.
$this->documentStack[0]->annotation = $node;
$this->documentStack[0]->closed = true;
return null;
}
/**
* Reduce paragraph to blockquote
*
* Indented paragraphs are blockquotes, which should be wrapped in such a
* node.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceBlockquote( ezcDocumentRstNode $node )
{
if ( $node->indentation <= 0 )
{
// Apply rule only for indented paragraphs.
return $node;
}
// Check last node, if it is already a blockquote, append paragraph
// there.
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::BLOCKQUOTE ) &&
( $this->documentStack[0]->closed === false ) &&
( $this->documentStack[0]->indentation <= $node->indentation ) )
{
// Just append paragraph and exit
$quote = $this->documentStack->shift();
$quote->nodes[] = $node;
return $quote;
}
// Create a new blockquote
$blockquote = new ezcDocumentRstBlockquoteNode( $node->nodes[0]->token );
array_unshift( $blockquote->nodes, $node );
// Set blockquote indentation to the indentation of the last item on
// the document stack. This way it can be handled like other block
// level elements and already has the special markup.
$blockquote->indentation = isset( $this->documentStack[0] ) && isset( $this->documentStack[0]->indentation ) ? $this->documentStack[0]->indentation : 0;
return $blockquote;
}
/**
* Reduce paragraph to bullet lsit
*
* Indented paragraphs are bllet lists, if prefixed by a bullet list
* indicator.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceListItem( ezcDocumentRstNode $node )
{
$childs = array();
$lastIndentationLevel = $node->indentation;
// If this is the very first paragraph, we have nothing we can reduce
// to, so just skip this rule.
if ( !count( $this->documentStack ) )
{
/* DEBUG
echo " => Nothing to reduce to.\n";
// /DEBUG */
return $node;
}
// Include all paragraphs, lists and blockquotes
while ( $child = $this->documentStack->shift() )
{
if ( ( !$child instanceof ezcDocumentRstBlockNode ) ||
( $child->indentation < $node->indentation ) )
{
// We did not find a bullet list to reduce to, so it is time to
// put the stuff back to the stack and leave.
/* DEBUG
echo " -> No reduction target found, reached ", ezcDocumentRstNode::getTokenName( $child->type ), ".\n";
// /DEBUG */
$this->documentStack->prepend( array_merge(
$childs,
array( $child )
) );
return $node;
}
if ( ( ( $child->type === ezcDocumentRstNode::ENUMERATED_LIST ) ||
( $child->type === ezcDocumentRstNode::BULLET_LIST ) ) &&
( $child->indentation === $node->indentation ) )
{
// We found a bullet list for the current paragraph.
/* DEBUG
echo " => Found matching list.\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
array_reverse( $childs ),
array( $node )
);
$this->documentStack->prepend( array( $child ) );
return null;
}
if ( ( ( $child->type === ezcDocumentRstNode::ENUMERATED_LIST ) ||
( $child->type === ezcDocumentRstNode::BULLET_LIST ) ) &&
( $child->indentation < $lastIndentationLevel ) )
{
// The indentation level reduced during the processing of
// childs. We can reduce the found childs to the new child with
// lowest indentation.
/* DEBUG
echo " -> Reduce subgroup (" . count( $childs ) . " items).\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
array_reverse( $childs )
);
$childs = array();
}
// Else just append item to curernt child list, and update current
// indentation.
/* DEBUG
echo " -> Appending " . ezcDocumentRstNode::getTokenName( $child->type ) . ".\n";
// /DEBUG */
$childs[] = $child;
$lastIndentationLevel = $child->indentation;
}
// Clean up and return node
$this->documentStack->prepend( array_merge(
array( $node ),
$childs
) );
/* DEBUG
echo " => Done (" . count( $this->documentStack ) . " elements on stack).\n";
// /DEBUG */
return null;
}
/**
* Reduce item to bullet list
*
* Called for all items, which may be part of bullet lists. Depending on
* the indentation level we reduce some amount of items to a bullet list.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceList( ezcDocumentRstNode $node )
{
$childs = array();
$lastIndentationLevel = 0;
$nodeIndentation = $node instanceof ezcDocumentRstBlockNode ? $node->indentation : 0;
/* DEBUG
echo " - Indentation {$nodeIndentation}.\n";
// /DEBUG */
// Include all paragraphs, lists and blockquotes
while ( $child = $this->documentStack->shift() )
{
if ( ( !$child instanceof ezcDocumentRstBlockNode ) ||
( $child->indentation < $nodeIndentation ) )
{
// We did not find a list to reduce to, so it is time to put
// the stuff back to the stack and leave.
/* DEBUG
echo " -> No reduction target found, reached ", ezcDocumentRstNode::getTokenName( $child->type ), ".\n";
// /DEBUG */
$this->documentStack->unshift( $child );
break;
}
if ( ( $child->indentation === $nodeIndentation ) &&
( $child->type === $node->type ) &&
( ( $child->type === ezcDocumentRstNode::ENUMERATED_LIST ) ||
( $child->type === ezcDocumentRstNode::BULLET_LIST ) ) )
{
// We found a list on the same level, so this is a new list
// item.
/* DEBUG
echo " => Found same level list item.\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
$childs
);
$this->documentStack->prepend( array( $child ) );
return $node;
}
if ( ( ( $child->type === ezcDocumentRstNode::ENUMERATED_LIST ) ||
( $child->type === ezcDocumentRstNode::BULLET_LIST ) ) &&
( $child->indentation < $lastIndentationLevel ) )
{
// The indentation level reduced during the processing of
// childs. We can reduce the found childs to the new child with
// lowest indentation.
/* DEBUG
echo " -> Reduce subgroup (" . count( $childs ) . " items).\n";
// /DEBUG */
$child->nodes = array_merge(
$child->nodes,
array_reverse( $childs )
);
$childs = array();
}
// Else just append item to curernt child list, and update current
// indentation.
/* DEBUG
echo " -> Appending " . ezcDocumentRstNode::getTokenName( $child->type ) . ".\n";
// /DEBUG */
$childs[] = $child;
$lastIndentationLevel = $child->indentation;
}
// Clean up and return node
/* DEBUG
echo " => Done.\n";
// /DEBUG */
$this->documentStack->prepend( $childs );
return $node;
}
/**
* Reduce paragraph
*
* Aggregates all nodes which are allowed as subnodes into a paragraph.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceParagraph( ezcDocumentRstNode $node )
{
$found = 0;
// Include all paragraphs, tables, lists and sections with a higher
// nesting depth
$nodes = array();
while ( isset( $this->documentStack[0] ) &&
in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
// Convert single markup nodes back to text
$text = $this->documentStack->shift();
if ( in_array( $text->type, array(
ezcDocumentRstNode::MARKUP_EMPHASIS,
ezcDocumentRstNode::MARKUP_STRONG,
ezcDocumentRstNode::MARKUP_INTERPRETED,
ezcDocumentRstNode::MARKUP_LITERAL,
ezcDocumentRstNode::MARKUP_SUBSTITUTION,
) ) &&
( count( $text->nodes ) < 1 ) )
{
$text = new ezcDocumentRstTextLineNode( $text->token );
}
/* DEBUG
echo " -> Append text to paragraph\n";
// /DEBUG */
array_unshift( $nodes, $text );
++$found;
}
if ( $found > 0 )
{
// Reduce text nodes in single AST nodes
$textNode = null;
foreach ( $nodes as $nr => $child )
{
if ( $child->type === ezcDocumentRstNode::TEXT_LINE )
{
if ( $textNode === null )
{
$textNode = $child;
}
else
{
$textNode->token->content .= $child->token->content;
unset( $nodes[$nr] );
}
}
else
{
if ( $textNode !== null )
{
$textNode = null;
}
}
}
// Find inline markup in all paragraph text nodes
$linkedNodes = array();
foreach ( $nodes as $nr => $child )
{
if ( $child->type === ezcDocumentRstNode::TEXT_LINE )
{
// Find links in text nodes
while ( preg_match( self::REGEXP_INLINE_LINK, $child->token->content, $match ) )
{
$urlPosition = strpos( $child->token->content, $match['match'] );
// Create new token from text before URL.
$textToken = clone $child->token;
$textToken->content = substr( $child->token->content, 0, $urlPosition );
$linkedNodes[] = new ezcDocumentRstTextLineNode( $textToken );
// Create linked node from found URL
$linkToken = clone $child->token;
$linkToken->position += $urlPosition;
$linkToken->content = $match['match'];
$newChild = new ezcDocumentRstExternalReferenceNode( $linkToken );
$newChild->target = ( isset( $match['mail'] ) ? 'mailto:' . $match['mail'] : $match['url'] );
$newChild->nodes = array(
new ezcDocumentRstTextLineNode( $linkToken ),
);
$linkedNodes[] = $newChild;
// Check the child for other URLs
$child->token->position += $offset = $urlPosition + strlen( $match['match'] );
$child->token->content = substr( $child->token->content, $offset );
$child->position = $child->token->position;
}
}
// Add child, which had no more matches, or isn't plain inline text.
$linkedNodes[] = $child;
}
$node->indentation = $this->indentation;
$node->nodes = $linkedNodes;
/* DEBUG
echo " => Create paragraph with indentation {$node->indentation} (postIndent: {$this->postIndentation})\n";
// /DEBUG */
$this->indentation = ( $this->postIndentation !== null ? $this->postIndentation : 0 );
return $node;
}
}
/**
* Reduce markup
*
* Tries to find the opening tag for a markup definition.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceMarkup( ezcDocumentRstNode $node )
{
if ( $node->openTag === true )
{
// Opening tags are just added to the document stack and we exit
// the reuction method.
return $node;
}
$childs = array();
while ( isset( $this->documentStack[0] ) &&
in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
$child = $this->documentStack->shift();
if ( ( $child->type == $node->type ) &&
( $child->openTag === true ) )
{
/* DEBUG
echo " => Found matching tag.\n";
// /DEBUG */
// We found the nearest matching open tag. Append all included
// stuff as child nodes and add the closing tag to the document
// stack.
$node->nodes = $childs;
return $node;
}
/* DEBUG
echo " - Collected " . ezcDocumentRstNode::getTokenName( $child->type ) . " ({$child->token->content}).\n";
// /DEBUG */
// Append unusable but inline node to potential child list.
array_unshift( $childs, $child );
}
// We did not find an opening node.
//
// This is not a parse error, but in this case we just consider the
// closing node as text and reattach all found childs to the document
// stack.
/* DEBUG
echo " => Use as Text.\n";
// /DEBUG */
$this->documentStack->prepend( array_reverse( $childs ) );
return new ezcDocumentRstTextLineNode( $node->token );
}
/**
* Reduce interpreted text inline markup
*
* Tries to find the opening tag for a markup definition.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceInterpretedText( ezcDocumentRstNode $node )
{
if ( $node->openTag === true )
{
// Opening tags are just added to the document stack and we exit
// the reuction method.
return $node;
}
$childs = array();
while ( isset( $this->documentStack[0] ) &&
in_array( $this->documentStack[0]->type, $this->textNodes, true ) )
{
$child = $this->documentStack->shift();
if ( ( $child->type == $node->type ) &&
( $child->openTag === true ) )
{
/* DEBUG
echo " => Found matching tag.\n";
// /DEBUG */
// We found the nearest matching open tag. Append all included
// stuff as child nodes and add the closing tag to the document
// stack.
$node->nodes = $childs;
// Set the role from the opening tag, if defined there
if ( $child->role )
{
$node->role = $child->role;
}
return $node;
}
/* DEBUG
echo " - Collected " . ezcDocumentRstNode::getTokenName( $child->type ) . " ({$child->token->content}).\n";
// /DEBUG */
// Append unusable but inline node to potential child list.
array_unshift( $childs, $child );
}
// We did not find an opening node.
//
// This is not a parse error, but in this case we just consider the
// closing node as text and reattach all found childs to the document
// stack.
/* DEBUG
echo " => Use as Text.\n";
// /DEBUG */
$this->documentStack->prepend( array_reverse( $childs ) );
return new ezcDocumentRstTextLineNode( $node->token );
}
/**
* Reduce internal target
*
* Internal targets are listed before the literal markup block, so it may
* be found and reduced after we found a markup block.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceInternalTarget( ezcDocumentRstNode $node )
{
if ( ( $node->type !== ezcDocumentRstNode::MARKUP_INTERPRETED ) ||
( count( $node->nodes ) <= 0 ) )
{
// This is a irrelevant markup tags for this rules
/* DEBUG
echo " -> Irrelevant markup.\n";
// /DEBUG */
return $node;
}
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE ) &&
( $this->documentStack[0]->token->content === '_' ) )
{
// We found something, create target node and aggregate
// corresponding nodes.
$targetTextNode = $this->documentStack->shift();
$target = new ezcDocumentRstTargetNode( $targetTextNode->token );
$target->nodes = array( $node );
/* DEBUG
echo " -> Found new target.\n";
// /DEBUG */
return $target;
}
// Otherwise just do nothing and pass the old node through
/* DEBUG
echo " -> Skipped: No match.\n";
// /DEBUG */
return $node;
}
/**
* Reduce reference
*
* Reduce references as defined at:
* http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceReference( ezcDocumentRstNode $node )
{
// Pop closing brace
$closing = $this->documentStack->shift();
// Find all childs.
//
// May be multiple childs, since the references may consist of multiple
// characters with special chars ( *, # ) embedded.
$childs = array();
while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE ) )
{
$child = $this->documentStack->shift();
if ( ( $child->token->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( $child->token->content === '[' ) )
{
/* DEBUG
echo " => Found matching tag.\n";
// /DEBUG */
// We found the nearest matching open tag. Append all included
// stuff as child nodes and add the closing tag to the document
// stack.
$node->name = $childs;
$node->footnoteType = $this->detectFootnotetype( $childs );
return $node;
}
/* DEBUG
echo " - Collected " . ezcDocumentRstNode::getTokenName( $child->type ) . ".\n";
// /DEBUG */
// Append unusable but inline node to potential child list.
array_unshift( $childs, $child->token );
}
// We did not find an opening node.
//
// This is not a parse error, but in this case we just consider the
// closing node as text and reattach all found childs to the document
// stack.
/* DEBUG
echo " => Use as Text.\n";
// /DEBUG */
$this->documentStack->prepend( array_merge(
array( $closing ),
$childs
) );
return new ezcDocumentRstTextLineNode( $node->token );
}
/**
* Reduce link
*
* Uses the preceding element as the hyperlink content. This should be
* either a literal markup section, or just the last word.
*
* As we do not get workd content out of the tokenizer (too much overhead),
* we split out the previous text node up, in case we got one.
*
* @param ezcDocumentRstNode $node
* @return void
*/
protected function reduceLink( ezcDocumentRstNode $node )
{
if ( !isset( $this->documentStack[0] ) )
{
// This should never happen, though.
return;
}
// Check a special case for anonymous hyperlinks, that the beforelast
// token is not a '__'.
if ( isset( $this->documentStack[1] ) &&
( $this->documentStack[1]->token->content === '__' ) )
{
return new ezcDocumentRstTextLineNode( $node->token );
}
// Aggregate token to check for an embedded inline URL
$text = false;
if ( ( $this->documentStack[0]->type === ezcDocumentRstNode::MARKUP_INTERPRETED ) ||
( $this->documentStack[0]->type === ezcDocumentRstNode::MARKUP_SUBSTITUTION ) )
{
/* DEBUG
echo " - Scan literal for embedded URI.\n";
// /DEBUG */
$textNode = false;
$text = $this->documentStack->shift();
foreach ( $text->nodes as $child )
{
if ( $child->type !== ezcDocumentRstNode::TEXT_LINE )
{
/* DEBUG
echo " - Invalid subnode type found: " . ezcDocumentRstNode::getTokenName( $child->type ) . ".\n";
// /DEBUG */
$textNode = false;
break;
}
else
{
$textNode .= $child->token->content;
}
}
// If we could aggregate the texts easily, because there is only
// plain text included, we can check for an URL.
if ( ( $textNode !== false ) &&
( preg_match( '(\s*<(?P<url>[^>]+)>)', $textNode, $match ) ) )
{
// We found an URL - remove it from the text and use it as link
// target.
/* DEBUG
echo " - Matched embedded URI: " . $match['url'] . ".\n";
// /DEBUG */
$text = new ezcDocumentRstTextLineNode( $text->nodes[0]->token );
$text->token->content = str_replace( $match[0], '', $textNode );
$node->target = $match['url'];
}
$text = array( $text );
}
elseif ( ( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE ) &&
( strpos( $this->documentStack[0]->token->content, ' ' ) === false ) )
{
/* DEBUG
echo " - Spaceless text found, aggregating:\n";
// /DEBUG */
$text = array();
do {
$text[] = $this->documentStack->shift();
} while ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE ) &&
( ( $this->documentStack[0]->token->type === ezcDocumentRstToken::TEXT_LINE ) ||
( ( $this->documentStack[0]->token->type === ezcDocumentRstToken::SPECIAL_CHARS ) &&
( preg_match( '(^[_-]+$)', $this->documentStack[0]->token->content ) ) ) ) &&
( strpos( $this->documentStack[0]->token->content, ' ' ) === false ) );
// Split away everything after the first space, and include it in
// the reference target, if applicable
if ( isset( $this->documentStack[0] ) &&
( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE ) &&
( preg_match( '(^(?P<before>.*?)(?P<text>[A-Za-z0-9_-]+)$)s', $this->documentStack[0]->token->content, $match ) ) )
{
/* DEBUG
echo " - Splitting node: '{$this->documentStack[0]->token->content}' at $pos.\n";
// /DEBUG */
$token = clone $this->documentStack[0]->token;
$this->documentStack[0]->token->content = rtrim( $match['before'] );
$token->content = $match['text'];
$text[] = new ezcDocumentRstTextLineNode( $token );
}
$text = array_reverse( $text );
}
if ( $text !== false )
{
/* DEBUG
echo " - Return created node.\n";
// /DEBUG */
$node->nodes = $text;
return $node;
}
// If the single text node contains spaces we need to split it up here,
// because the link only covers the last word in the string.
if ( $this->documentStack[0]->type === ezcDocumentRstNode::TEXT_LINE )
{
// This is bit hackish, but otherwise the tokenizer would produce
// too large amounts of structs.
$words = explode( ' ', $this->documentStack[0]->token->content );
$this->documentStack[0]->token->content = implode( ' ', array_slice( $words, 0, -1 ) ) . ' ';
$token = clone $this->documentStack[0]->token;
$token->content = end( $words );
$text = new ezcDocumentRstTextLineNode( $token );
$node->nodes = array( $text );
return $node;
}
// We did not find a valid precedessor, so just convert to a text node.
return new ezcDocumentRstTextLineNode( $node->token );
}
}
?>