2006-07-23 23:29:12 +00:00
|
|
|
<?php
|
|
|
|
|
2006-08-20 21:59:41 +00:00
|
|
|
/**
|
|
|
|
* Takes tokens makes them well-formed (balance end tags, etc.)
|
2011-03-22 00:26:41 +00:00
|
|
|
*
|
|
|
|
* Specification of the armor attributes this strategy uses:
|
|
|
|
*
|
|
|
|
* - MakeWellFormed_TagClosedError: This armor field is used to
|
|
|
|
* suppress tag closed errors for certain tokens [TagClosedSuppress],
|
|
|
|
* in particular, if a tag was generated automatically by HTML
|
|
|
|
* Purifier, we may rely on our infrastructure to close it for us
|
|
|
|
* and shouldn't report an error to the user [TagClosedAuto].
|
2006-08-20 21:59:41 +00:00
|
|
|
*/
|
2006-07-23 23:29:12 +00:00
|
|
|
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
|
|
|
|
{
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 17:44:27 +00:00
|
|
|
/**
|
2008-10-01 08:10:41 +00:00
|
|
|
* Array stream of tokens being processed.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @type HTMLPurifier_Token[]
|
2007-06-24 17:44:27 +00:00
|
|
|
*/
|
2008-10-01 08:10:41 +00:00
|
|
|
protected $tokens;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
/**
|
2013-10-13 19:53:51 +00:00
|
|
|
* Current token.
|
|
|
|
* @type HTMLPurifier_Token
|
2008-10-01 08:10:41 +00:00
|
|
|
*/
|
2013-10-13 19:53:51 +00:00
|
|
|
protected $token;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zipper managing the true state.
|
|
|
|
* @type HTMLPurifier_Zipper
|
|
|
|
*/
|
|
|
|
protected $zipper;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
/**
|
|
|
|
* Current nesting of elements.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @type array
|
2008-10-01 08:10:41 +00:00
|
|
|
*/
|
|
|
|
protected $stack;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
/**
|
|
|
|
* Injectors active in this stream processing.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @type HTMLPurifier_Injector[]
|
2008-10-01 08:10:41 +00:00
|
|
|
*/
|
|
|
|
protected $injectors;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
/**
|
|
|
|
* Current instance of HTMLPurifier_Config.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @type HTMLPurifier_Config
|
2008-10-01 08:10:41 +00:00
|
|
|
*/
|
|
|
|
protected $config;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
/**
|
|
|
|
* Current instance of HTMLPurifier_Context.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @type HTMLPurifier_Context
|
2008-10-01 08:10:41 +00:00
|
|
|
*/
|
|
|
|
protected $context;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2013-07-16 11:56:14 +00:00
|
|
|
/**
|
|
|
|
* @param HTMLPurifier_Token[] $tokens
|
|
|
|
* @param HTMLPurifier_Config $config
|
|
|
|
* @param HTMLPurifier_Context $context
|
|
|
|
* @return HTMLPurifier_Token[]
|
|
|
|
* @throws HTMLPurifier_Exception
|
|
|
|
*/
|
|
|
|
public function execute($tokens, $config, $context)
|
|
|
|
{
|
2006-08-31 20:33:07 +00:00
|
|
|
$definition = $config->getHTMLDefinition();
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-10-02 22:50:59 +00:00
|
|
|
// local variables
|
2008-05-26 04:05:48 +00:00
|
|
|
$generator = new HTMLPurifier_Generator($config, $context);
|
2009-02-20 00:17:49 +00:00
|
|
|
$escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
|
2011-03-22 00:26:41 +00:00
|
|
|
// used for autoclose early abortion
|
2013-05-22 00:19:59 +00:00
|
|
|
$global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config);
|
2007-12-05 01:26:28 +00:00
|
|
|
$e = $context->get('ErrorCollector', true);
|
2008-10-01 08:10:41 +00:00
|
|
|
$i = false; // injector index
|
2013-10-13 19:53:51 +00:00
|
|
|
list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens);
|
|
|
|
if ($token === NULL) {
|
|
|
|
return array();
|
|
|
|
}
|
2013-07-16 11:56:14 +00:00
|
|
|
$reprocess = false; // whether or not to reprocess the same token
|
2008-10-01 08:10:41 +00:00
|
|
|
$stack = array();
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-10-02 22:50:59 +00:00
|
|
|
// member variables
|
2013-07-16 11:56:14 +00:00
|
|
|
$this->stack =& $stack;
|
|
|
|
$this->tokens =& $tokens;
|
2013-10-13 19:53:51 +00:00
|
|
|
$this->token =& $token;
|
|
|
|
$this->zipper =& $zipper;
|
2013-07-16 11:56:14 +00:00
|
|
|
$this->config = $config;
|
2008-10-01 08:10:41 +00:00
|
|
|
$this->context = $context;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-10-02 22:50:59 +00:00
|
|
|
// context variables
|
2008-10-01 08:10:41 +00:00
|
|
|
$context->register('CurrentNesting', $stack);
|
2013-10-13 19:53:51 +00:00
|
|
|
$context->register('InputZipper', $zipper);
|
2013-07-16 11:56:14 +00:00
|
|
|
$context->register('CurrentToken', $token);
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 04:22:28 +00:00
|
|
|
// -- begin INJECTOR --
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 17:44:27 +00:00
|
|
|
$this->injectors = array();
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 21:35:34 +00:00
|
|
|
$injectors = $config->getBatch('AutoFormat');
|
2008-06-09 01:23:05 +00:00
|
|
|
$def_injectors = $definition->info_injector;
|
2007-06-24 21:35:34 +00:00
|
|
|
$custom_injectors = $injectors['Custom'];
|
|
|
|
unset($injectors['Custom']); // special case
|
|
|
|
foreach ($injectors as $injector => $b) {
|
2009-04-09 04:53:19 +00:00
|
|
|
// XXX: Fix with a legitimate lookup table of enabled filters
|
2013-07-16 11:56:14 +00:00
|
|
|
if (strpos($injector, '.') !== false) {
|
|
|
|
continue;
|
|
|
|
}
|
2007-06-24 21:35:34 +00:00
|
|
|
$injector = "HTMLPurifier_Injector_$injector";
|
2013-07-16 11:56:14 +00:00
|
|
|
if (!$b) {
|
|
|
|
continue;
|
|
|
|
}
|
2007-06-28 13:06:15 +00:00
|
|
|
$this->injectors[] = new $injector;
|
2007-06-24 02:27:57 +00:00
|
|
|
}
|
2008-06-09 01:23:05 +00:00
|
|
|
foreach ($def_injectors as $injector) {
|
|
|
|
// assumed to be objects
|
|
|
|
$this->injectors[] = $injector;
|
|
|
|
}
|
2007-06-24 21:35:34 +00:00
|
|
|
foreach ($custom_injectors as $injector) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if (!$injector) {
|
|
|
|
continue;
|
|
|
|
}
|
2007-06-24 21:35:34 +00:00
|
|
|
if (is_string($injector)) {
|
|
|
|
$injector = "HTMLPurifier_Injector_$injector";
|
|
|
|
$injector = new $injector;
|
|
|
|
}
|
|
|
|
$this->injectors[] = $injector;
|
2007-06-24 02:45:38 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 17:44:27 +00:00
|
|
|
// give the injectors references to the definition and context
|
|
|
|
// variables for performance reasons
|
2008-10-01 08:10:41 +00:00
|
|
|
foreach ($this->injectors as $ix => $injector) {
|
2007-12-05 01:26:28 +00:00
|
|
|
$error = $injector->prepare($config, $context);
|
2013-07-16 11:56:14 +00:00
|
|
|
if (!$error) {
|
|
|
|
continue;
|
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
array_splice($this->injectors, $ix, 1); // rm the injector
|
2007-12-05 01:26:28 +00:00
|
|
|
trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
|
2007-06-24 17:44:27 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-24 04:22:28 +00:00
|
|
|
// -- end INJECTOR --
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2011-03-22 00:26:41 +00:00
|
|
|
// a note on reprocessing:
|
2008-10-01 08:10:41 +00:00
|
|
|
// In order to reduce code duplication, whenever some code needs
|
|
|
|
// to make HTML changes in order to make things "correct", the
|
|
|
|
// new HTML gets sent through the purifier, regardless of its
|
|
|
|
// status. This means that if we add a start token, because it
|
|
|
|
// was totally necessary, we don't have to update nesting; we just
|
|
|
|
// punt ($reprocess = true; continue;) and it does that for us.
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-12-05 01:26:28 +00:00
|
|
|
// isset is in loop because $tokens size changes during loop exec
|
2013-10-13 19:53:51 +00:00
|
|
|
for (;;
|
|
|
|
// only increment if we don't need to reprocess
|
|
|
|
$reprocess ? $reprocess = false : $token = $zipper->next($token)) {
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// check for a rewind
|
2013-10-13 19:53:51 +00:00
|
|
|
if (is_int($i)) {
|
2008-10-01 08:10:41 +00:00
|
|
|
// possibility: disable rewinding if the current token has a
|
|
|
|
// rewind set on it already. This would offer protection from
|
|
|
|
// infinite loop, but might hinder some advanced rewinding.
|
2013-10-13 19:53:51 +00:00
|
|
|
$rewind_offset = $this->injectors[$i]->getRewindOffset();
|
|
|
|
if (is_int($rewind_offset)) {
|
|
|
|
for ($j = 0; $j < $rewind_offset; $j++) {
|
|
|
|
if (empty($zipper->front)) break;
|
|
|
|
$token = $zipper->prev($token);
|
2008-10-01 07:14:28 +00:00
|
|
|
// indicate that other injectors should not process this token,
|
2017-03-07 04:31:28 +00:00
|
|
|
// but we need to reprocess it. See Note [Injector skips]
|
2013-10-13 19:53:51 +00:00
|
|
|
unset($token->skip[$i]);
|
|
|
|
$token->rewind = $i;
|
|
|
|
if ($token instanceof HTMLPurifier_Token_Start) {
|
2013-07-16 11:56:14 +00:00
|
|
|
array_pop($this->stack);
|
2013-10-13 19:53:51 +00:00
|
|
|
} elseif ($token instanceof HTMLPurifier_Token_End) {
|
|
|
|
$this->stack[] = $token->start;
|
2013-07-16 11:56:14 +00:00
|
|
|
}
|
2008-10-01 07:14:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
$i = false;
|
Implement Injector->handleEnd, with lots of refactoring for injector.
Previous design of injector streaming involved editability only to start, empty
and text tokens, because they could be safely modified without causing formedness
errors. By modifying notifyEnd to operate before MakeWellFormed's safeguards
kick into effect, it can be converted into a handle function, allowing for
arbitrary modification of end tags.
This change involved quite a bit of restructuring of the MakeWellFormed code,
including the moving of end of document tags to inside the loop, so rewinding
on those tags would be functional, increased reuse of the end tag codepath by
code that inserts end tags (as they could be changed out from under you), and
processToken modified to have an extra parameter to force re-processing of
a token if the original token was an end token.
We're not exactly sure if handleEnd works at this point, but the important
talking point about this refactoring is that nothing else broke. Also, a number
of convenience functions were moved from AutoParagraph to the Injector
supertype (specifically: forward, forwardToEndToken, backward, and current).
Signed-off-by: Edward Z. Yang <edwardzyang@thewritingpot.com>
2008-10-01 04:54:51 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
Implement Injector->handleEnd, with lots of refactoring for injector.
Previous design of injector streaming involved editability only to start, empty
and text tokens, because they could be safely modified without causing formedness
errors. By modifying notifyEnd to operate before MakeWellFormed's safeguards
kick into effect, it can be converted into a handle function, allowing for
arbitrary modification of end tags.
This change involved quite a bit of restructuring of the MakeWellFormed code,
including the moving of end of document tags to inside the loop, so rewinding
on those tags would be functional, increased reuse of the end tag codepath by
code that inserts end tags (as they could be changed out from under you), and
processToken modified to have an extra parameter to force re-processing of
a token if the original token was an end token.
We're not exactly sure if handleEnd works at this point, but the important
talking point about this refactoring is that nothing else broke. Also, a number
of convenience functions were moved from AutoParagraph to the Injector
supertype (specifically: forward, forwardToEndToken, backward, and current).
Signed-off-by: Edward Z. Yang <edwardzyang@thewritingpot.com>
2008-10-01 04:54:51 +00:00
|
|
|
// handle case of document end
|
2013-10-13 19:53:51 +00:00
|
|
|
if ($token === NULL) {
|
2008-10-01 07:14:28 +00:00
|
|
|
// kill processing if stack is empty
|
2013-07-16 11:56:14 +00:00
|
|
|
if (empty($this->stack)) {
|
|
|
|
break;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// peek
|
2008-10-01 08:10:41 +00:00
|
|
|
$top_nesting = array_pop($this->stack);
|
|
|
|
$this->stack[] = $top_nesting;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2011-03-22 00:26:41 +00:00
|
|
|
// send error [TagClosedSuppress]
|
2008-10-01 07:14:28 +00:00
|
|
|
if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
|
|
|
|
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// append, don't splice, since this is the end
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = new HTMLPurifier_Token_End($top_nesting->name);
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// punt!
|
|
|
|
$reprocess = true;
|
|
|
|
continue;
|
Implement Injector->handleEnd, with lots of refactoring for injector.
Previous design of injector streaming involved editability only to start, empty
and text tokens, because they could be safely modified without causing formedness
errors. By modifying notifyEnd to operate before MakeWellFormed's safeguards
kick into effect, it can be converted into a handle function, allowing for
arbitrary modification of end tags.
This change involved quite a bit of restructuring of the MakeWellFormed code,
including the moving of end of document tags to inside the loop, so rewinding
on those tags would be functional, increased reuse of the end tag codepath by
code that inserts end tags (as they could be changed out from under you), and
processToken modified to have an extra parameter to force re-processing of
a token if the original token was an end token.
We're not exactly sure if handleEnd works at this point, but the important
talking point about this refactoring is that nothing else broke. Also, a number
of convenience functions were moved from AutoParagraph to the Injector
supertype (specifically: forward, forwardToEndToken, backward, and current).
Signed-off-by: Edward Z. Yang <edwardzyang@thewritingpot.com>
2008-10-01 04:54:51 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2013-10-13 19:53:51 +00:00
|
|
|
//echo '<br>'; printZipper($zipper, $token);//printTokens($this->stack);
|
2010-05-18 03:22:51 +00:00
|
|
|
//flush();
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-23 17:11:05 +00:00
|
|
|
// quick-check: if it's not a tag, no need to process
|
2008-10-01 07:14:28 +00:00
|
|
|
if (empty($token->is_tag)) {
|
2008-01-19 20:23:01 +00:00
|
|
|
if ($token instanceof HTMLPurifier_Token_Text) {
|
2008-10-01 07:14:28 +00:00
|
|
|
foreach ($this->injectors as $i => $injector) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if (isset($token->skip[$i])) {
|
2017-03-07 04:31:28 +00:00
|
|
|
// See Note [Injector skips]
|
2013-07-16 11:56:14 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($token->rewind !== null && $token->rewind !== $i) {
|
|
|
|
continue;
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
// XXX fuckup
|
|
|
|
$r = $token;
|
|
|
|
$injector->handleText($r);
|
|
|
|
$token = $this->processToken($r, $i);
|
2008-10-01 07:14:28 +00:00
|
|
|
$reprocess = true;
|
|
|
|
break;
|
|
|
|
}
|
2007-06-22 21:32:56 +00:00
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
// another possibility is a comment
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-07-05 07:11:29 +00:00
|
|
|
if (isset($definition->info[$token->name])) {
|
|
|
|
$type = $definition->info[$token->name]->child->type;
|
|
|
|
} else {
|
|
|
|
$type = false; // Type is unknown, treat accordingly
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-29 00:24:59 +00:00
|
|
|
// quick tag checks: anything that's *not* an end tag
|
|
|
|
$ok = false;
|
2008-07-05 07:11:29 +00:00
|
|
|
if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
|
2008-10-01 08:10:41 +00:00
|
|
|
// claims to be a start tag but is empty
|
2013-07-16 11:56:14 +00:00
|
|
|
$token = new HTMLPurifier_Token_Empty(
|
|
|
|
$token->name,
|
|
|
|
$token->attr,
|
|
|
|
$token->line,
|
|
|
|
$token->col,
|
|
|
|
$token->armor
|
|
|
|
);
|
2007-06-29 00:24:59 +00:00
|
|
|
$ok = true;
|
2008-07-05 07:11:29 +00:00
|
|
|
} elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
|
2007-06-29 00:24:59 +00:00
|
|
|
// claims to be empty but really is a start tag
|
2013-10-13 19:53:51 +00:00
|
|
|
// NB: this assignment is required
|
|
|
|
$old_token = $token;
|
|
|
|
$token = new HTMLPurifier_Token_End($token->name);
|
|
|
|
$token = $this->insertBefore(
|
|
|
|
new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor)
|
2013-07-16 11:56:14 +00:00
|
|
|
);
|
2008-10-01 08:10:41 +00:00
|
|
|
// punt (since we had to modify the input stream in a non-trivial way)
|
2008-10-01 07:14:28 +00:00
|
|
|
$reprocess = true;
|
|
|
|
continue;
|
2008-01-19 20:23:01 +00:00
|
|
|
} elseif ($token instanceof HTMLPurifier_Token_Empty) {
|
2007-06-29 00:24:59 +00:00
|
|
|
// real empty token
|
|
|
|
$ok = true;
|
2008-01-19 20:23:01 +00:00
|
|
|
} elseif ($token instanceof HTMLPurifier_Token_Start) {
|
2007-06-29 00:24:59 +00:00
|
|
|
// start tag
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-23 17:11:05 +00:00
|
|
|
// ...unless they also have to close their parent
|
2008-10-01 08:10:41 +00:00
|
|
|
if (!empty($this->stack)) {
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2011-03-22 00:26:41 +00:00
|
|
|
// Performance note: you might think that it's rather
|
|
|
|
// inefficient, recalculating the autoclose information
|
|
|
|
// for every tag that a token closes (since when we
|
|
|
|
// do an autoclose, we push a new token into the
|
|
|
|
// stream and then /process/ that, before
|
|
|
|
// re-processing this token.) But this is
|
|
|
|
// necessary, because an injector can make an
|
|
|
|
// arbitrary transformations to the autoclosing
|
|
|
|
// tokens we introduce, so things may have changed
|
|
|
|
// in the meantime. Also, doing the inefficient thing is
|
|
|
|
// "easy" to reason about (for certain perverse definitions
|
|
|
|
// of "easy")
|
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
$parent = array_pop($this->stack);
|
|
|
|
$this->stack[] = $parent;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2013-05-22 00:19:59 +00:00
|
|
|
$parent_def = null;
|
|
|
|
$parent_elements = null;
|
|
|
|
$autoclose = false;
|
2008-07-05 07:11:29 +00:00
|
|
|
if (isset($definition->info[$parent->name])) {
|
2013-05-22 00:19:59 +00:00
|
|
|
$parent_def = $definition->info[$parent->name];
|
|
|
|
$parent_elements = $parent_def->child->getAllowedElements($config);
|
|
|
|
$autoclose = !isset($parent_elements[$token->name]);
|
2008-07-05 07:11:29 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2010-03-10 05:58:37 +00:00
|
|
|
if ($autoclose && $definition->info[$token->name]->wrap) {
|
2013-07-16 11:56:14 +00:00
|
|
|
// Check if an element can be wrapped by another
|
|
|
|
// element to make it valid in a context (for
|
2010-05-18 03:22:51 +00:00
|
|
|
// example, <ul><ul> needs a <li> in between)
|
2010-03-10 05:58:37 +00:00
|
|
|
$wrapname = $definition->info[$token->name]->wrap;
|
|
|
|
$wrapdef = $definition->info[$wrapname];
|
|
|
|
$elements = $wrapdef->child->getAllowedElements($config);
|
2010-05-18 03:22:51 +00:00
|
|
|
if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) {
|
2010-03-10 05:58:37 +00:00
|
|
|
$newtoken = new HTMLPurifier_Token_Start($wrapname);
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = $this->insertBefore($newtoken);
|
2010-03-10 05:58:37 +00:00
|
|
|
$reprocess = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-12-20 18:06:00 +00:00
|
|
|
$carryover = false;
|
2013-05-22 00:19:59 +00:00
|
|
|
if ($autoclose && $parent_def->formatting) {
|
2008-12-20 18:06:00 +00:00
|
|
|
$carryover = true;
|
|
|
|
}
|
|
|
|
|
2008-07-05 07:11:29 +00:00
|
|
|
if ($autoclose) {
|
2011-03-22 00:26:41 +00:00
|
|
|
// check if this autoclose is doomed to fail
|
|
|
|
// (this rechecks $parent, which his harmless)
|
|
|
|
$autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
|
|
|
|
if (!$autoclose_ok) {
|
|
|
|
foreach ($this->stack as $ancestor) {
|
|
|
|
$elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
|
|
|
|
if (isset($elements[$token->name])) {
|
|
|
|
$autoclose_ok = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if ($definition->info[$token->name]->wrap) {
|
|
|
|
$wrapname = $definition->info[$token->name]->wrap;
|
|
|
|
$wrapdef = $definition->info[$wrapname];
|
|
|
|
$wrap_elements = $wrapdef->child->getAllowedElements($config);
|
|
|
|
if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
|
|
|
|
$autoclose_ok = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2008-12-20 18:06:00 +00:00
|
|
|
}
|
2011-03-22 00:26:41 +00:00
|
|
|
if ($autoclose_ok) {
|
|
|
|
// errors need to be updated
|
|
|
|
$new_token = new HTMLPurifier_Token_End($parent->name);
|
|
|
|
$new_token->start = $parent;
|
|
|
|
// [TagClosedSuppress]
|
|
|
|
if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
|
|
|
|
if (!$carryover) {
|
|
|
|
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
|
|
|
|
} else {
|
|
|
|
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
|
|
|
|
}
|
2008-12-20 18:06:00 +00:00
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
if ($carryover) {
|
|
|
|
$element = clone $parent;
|
|
|
|
// [TagClosedAuto]
|
|
|
|
$element->armor['MakeWellFormed_TagClosedError'] = true;
|
|
|
|
$element->carryover = true;
|
|
|
|
$token = $this->processToken(array($new_token, $token, $element));
|
|
|
|
} else {
|
|
|
|
$token = $this->insertBefore($new_token);
|
|
|
|
}
|
2011-03-22 00:26:41 +00:00
|
|
|
} else {
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = $this->remove();
|
2008-12-20 18:06:00 +00:00
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
$reprocess = true;
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2006-07-23 23:29:12 +00:00
|
|
|
}
|
2007-06-29 00:24:59 +00:00
|
|
|
$ok = true;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-29 00:24:59 +00:00
|
|
|
if ($ok) {
|
2007-12-05 01:26:28 +00:00
|
|
|
foreach ($this->injectors as $i => $injector) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if (isset($token->skip[$i])) {
|
2017-03-07 04:31:28 +00:00
|
|
|
// See Note [Injector skips]
|
2013-07-16 11:56:14 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($token->rewind !== null && $token->rewind !== $i) {
|
|
|
|
continue;
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$r = $token;
|
|
|
|
$injector->handleElement($r);
|
|
|
|
$token = $this->processToken($r, $i);
|
2008-10-01 07:14:28 +00:00
|
|
|
$reprocess = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!$reprocess) {
|
|
|
|
// ah, nothing interesting happened; do normal processing
|
|
|
|
if ($token instanceof HTMLPurifier_Token_Start) {
|
2008-10-01 08:10:41 +00:00
|
|
|
$this->stack[] = $token;
|
2008-10-01 07:14:28 +00:00
|
|
|
} elseif ($token instanceof HTMLPurifier_Token_End) {
|
2013-07-16 11:56:14 +00:00
|
|
|
throw new HTMLPurifier_Exception(
|
|
|
|
'Improper handling of end tag in start code; possible error in MakeWellFormed'
|
|
|
|
);
|
2007-06-24 02:45:38 +00:00
|
|
|
}
|
2007-06-23 17:44:28 +00:00
|
|
|
}
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-23 17:11:05 +00:00
|
|
|
// sanity check: we should be dealing with a closing tag
|
2008-06-27 05:33:48 +00:00
|
|
|
if (!$token instanceof HTMLPurifier_Token_End) {
|
2008-10-01 08:10:41 +00:00
|
|
|
throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
|
2008-06-27 05:33:48 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2006-07-23 23:29:12 +00:00
|
|
|
// make sure that we have something open
|
2008-10-01 08:10:41 +00:00
|
|
|
if (empty($this->stack)) {
|
2006-08-15 23:58:18 +00:00
|
|
|
if ($escape_invalid_tags) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if ($e) {
|
|
|
|
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
|
2008-06-27 05:33:48 +00:00
|
|
|
} else {
|
2013-07-16 11:56:14 +00:00
|
|
|
if ($e) {
|
|
|
|
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = $this->remove();
|
2006-08-15 23:58:18 +00:00
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
$reprocess = true;
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 19:40:31 +00:00
|
|
|
// first, check for the simplest case: everything closes neatly.
|
|
|
|
// Eventually, everything passes through here; if there are problems
|
|
|
|
// we modify the input stream accordingly and then punt, so that
|
|
|
|
// the tokens get processed again.
|
2008-10-01 08:10:41 +00:00
|
|
|
$current_parent = array_pop($this->stack);
|
2006-07-23 23:29:12 +00:00
|
|
|
if ($current_parent->name == $token->name) {
|
2008-06-27 20:09:14 +00:00
|
|
|
$token->start = $current_parent;
|
2008-10-01 19:40:31 +00:00
|
|
|
foreach ($this->injectors as $i => $injector) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if (isset($token->skip[$i])) {
|
2017-03-07 04:31:28 +00:00
|
|
|
// See Note [Injector skips]
|
2013-07-16 11:56:14 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($token->rewind !== null && $token->rewind !== $i) {
|
|
|
|
continue;
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$r = $token;
|
|
|
|
$injector->handleEnd($r);
|
|
|
|
$token = $this->processToken($r, $i);
|
2008-10-01 19:40:31 +00:00
|
|
|
$this->stack[] = $current_parent;
|
|
|
|
$reprocess = true;
|
|
|
|
break;
|
|
|
|
}
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2006-07-23 23:29:12 +00:00
|
|
|
// okay, so we're trying to close the wrong tag
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-23 17:11:05 +00:00
|
|
|
// undo the pop previous pop
|
2008-10-01 08:10:41 +00:00
|
|
|
$this->stack[] = $current_parent;
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-23 17:11:05 +00:00
|
|
|
// scroll back the entire nest, trying to find our tag.
|
|
|
|
// (feature could be to specify how far you'd like to go)
|
2008-10-01 08:10:41 +00:00
|
|
|
$size = count($this->stack);
|
2006-07-23 23:29:12 +00:00
|
|
|
// -2 because -1 is the last element, but we already checked that
|
|
|
|
$skipped_tags = false;
|
2008-10-01 08:10:41 +00:00
|
|
|
for ($j = $size - 2; $j >= 0; $j--) {
|
|
|
|
if ($this->stack[$j]->name == $token->name) {
|
|
|
|
$skipped_tags = array_slice($this->stack, $j);
|
2006-07-23 23:29:12 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
// we didn't find the tag, so remove
|
2006-07-23 23:29:12 +00:00
|
|
|
if ($skipped_tags === false) {
|
2006-08-15 23:58:18 +00:00
|
|
|
if ($escape_invalid_tags) {
|
2013-07-16 11:56:14 +00:00
|
|
|
if ($e) {
|
|
|
|
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
|
2008-06-27 05:33:48 +00:00
|
|
|
} else {
|
2013-07-16 11:56:14 +00:00
|
|
|
if ($e) {
|
|
|
|
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
|
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = $this->remove();
|
2006-08-15 23:58:18 +00:00
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
$reprocess = true;
|
2006-07-23 23:29:12 +00:00
|
|
|
continue;
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
// do errors, in REVERSE $j order: a,b,c with </a></b></c>
|
|
|
|
$c = count($skipped_tags);
|
|
|
|
if ($e) {
|
|
|
|
for ($j = $c - 1; $j > 0; $j--) {
|
|
|
|
// notice we exclude $j == 0, i.e. the current ending tag, from
|
2011-03-22 00:26:41 +00:00
|
|
|
// the errors... [TagClosedSuppress]
|
2008-10-01 08:10:41 +00:00
|
|
|
if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
|
|
|
|
$e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
|
|
|
|
}
|
2007-06-26 15:07:07 +00:00
|
|
|
}
|
2006-07-23 23:29:12 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 08:10:41 +00:00
|
|
|
// insert tags, in FORWARD $j order: c,b,a with </a></b></c>
|
2008-12-20 18:06:00 +00:00
|
|
|
$replace = array($token);
|
2008-10-01 08:10:41 +00:00
|
|
|
for ($j = 1; $j < $c; $j++) {
|
|
|
|
// ...as well as from the insertions
|
|
|
|
$new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
|
|
|
|
$new_token->start = $skipped_tags[$j];
|
2008-12-20 18:06:00 +00:00
|
|
|
array_unshift($replace, $new_token);
|
|
|
|
if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
|
2011-03-22 00:26:41 +00:00
|
|
|
// [TagClosedAuto]
|
2008-12-20 18:06:00 +00:00
|
|
|
$element = clone $skipped_tags[$j];
|
|
|
|
$element->carryover = true;
|
|
|
|
$element->armor['MakeWellFormed_TagClosedError'] = true;
|
|
|
|
$replace[] = $element;
|
|
|
|
}
|
2008-10-01 08:10:41 +00:00
|
|
|
}
|
2013-10-13 19:53:51 +00:00
|
|
|
$token = $this->processToken($replace);
|
2008-10-01 08:10:41 +00:00
|
|
|
$reprocess = true;
|
2008-10-01 19:40:31 +00:00
|
|
|
continue;
|
2006-07-23 23:29:12 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2007-06-26 19:33:37 +00:00
|
|
|
$context->destroy('CurrentToken');
|
2013-10-13 19:53:51 +00:00
|
|
|
$context->destroy('CurrentNesting');
|
|
|
|
$context->destroy('InputZipper');
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2013-10-13 19:53:51 +00:00
|
|
|
unset($this->injectors, $this->stack, $this->tokens);
|
|
|
|
return $zipper->toArray($token);
|
2008-06-27 05:33:48 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-06-27 05:33:48 +00:00
|
|
|
/**
|
2008-10-01 07:14:28 +00:00
|
|
|
* Processes arbitrary token values for complicated substitution patterns.
|
|
|
|
* In general:
|
2008-12-06 07:28:20 +00:00
|
|
|
*
|
2008-10-01 07:14:28 +00:00
|
|
|
* If $token is an array, it is a list of tokens to substitute for the
|
2008-10-01 08:10:41 +00:00
|
|
|
* current token. These tokens then get individually processed. If there
|
|
|
|
* is a leading integer in the list, that integer determines how many
|
|
|
|
* tokens from the stream should be removed.
|
2008-12-06 07:28:20 +00:00
|
|
|
*
|
2008-10-01 08:10:41 +00:00
|
|
|
* If $token is a regular token, it is swapped with the current token.
|
2008-12-06 07:28:20 +00:00
|
|
|
*
|
2008-10-01 07:14:28 +00:00
|
|
|
* If $token is false, the current token is deleted.
|
2008-12-06 07:28:20 +00:00
|
|
|
*
|
2008-10-01 08:10:41 +00:00
|
|
|
* If $token is an integer, that number of tokens (with the first token
|
|
|
|
* being the current one) will be deleted.
|
2008-12-06 07:28:20 +00:00
|
|
|
*
|
2013-07-16 11:56:14 +00:00
|
|
|
* @param HTMLPurifier_Token|array|int|bool $token Token substitution value
|
|
|
|
* @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if
|
2008-10-01 08:10:41 +00:00
|
|
|
* this is not an injector related operation.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @throws HTMLPurifier_Exception
|
2008-10-01 07:14:28 +00:00
|
|
|
*/
|
2013-07-16 11:56:14 +00:00
|
|
|
protected function processToken($token, $injector = -1)
|
|
|
|
{
|
2017-01-04 19:35:52 +00:00
|
|
|
// Zend OpCache miscompiles $token = array($token), so
|
|
|
|
// avoid this pattern. See: https://github.com/ezyang/htmlpurifier/issues/108
|
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// normalize forms of token
|
2013-07-16 11:56:14 +00:00
|
|
|
if (is_object($token)) {
|
2017-01-04 19:35:52 +00:00
|
|
|
$tmp = $token;
|
|
|
|
$token = array(1, $tmp);
|
2013-07-16 11:56:14 +00:00
|
|
|
}
|
|
|
|
if (is_int($token)) {
|
2017-01-04 19:35:52 +00:00
|
|
|
$tmp = $token;
|
|
|
|
$token = array($tmp);
|
2013-07-16 11:56:14 +00:00
|
|
|
}
|
|
|
|
if ($token === false) {
|
|
|
|
$token = array(1);
|
|
|
|
}
|
|
|
|
if (!is_array($token)) {
|
|
|
|
throw new HTMLPurifier_Exception('Invalid token type from injector');
|
|
|
|
}
|
|
|
|
if (!is_int($token[0])) {
|
|
|
|
array_unshift($token, 1);
|
|
|
|
}
|
|
|
|
if ($token[0] === 0) {
|
|
|
|
throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
// $token is now an array with the following form:
|
|
|
|
// array(number nodes to delete, new node 1, new node 2, ...)
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
$delete = array_shift($token);
|
2013-10-13 19:53:51 +00:00
|
|
|
list($old, $r) = $this->zipper->splice($this->token, $delete, $token);
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
if ($injector > -1) {
|
2017-03-07 04:31:28 +00:00
|
|
|
// See Note [Injector skips]
|
|
|
|
// Determine appropriate skips. Here's what the code does:
|
|
|
|
// *If* we deleted one or more tokens, copy the skips
|
|
|
|
// of those tokens into the skips of the new tokens (in $token).
|
|
|
|
// Also, mark the newly inserted tokens as having come from
|
|
|
|
// $injector.
|
2008-10-01 07:14:28 +00:00
|
|
|
$oldskip = isset($old[0]) ? $old[0]->skip : array();
|
|
|
|
foreach ($token as $object) {
|
|
|
|
$object->skip = $oldskip;
|
|
|
|
$object->skip[$injector] = true;
|
|
|
|
}
|
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2013-10-13 19:53:51 +00:00
|
|
|
return $r;
|
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
}
|
2008-12-06 07:28:20 +00:00
|
|
|
|
2008-10-01 07:14:28 +00:00
|
|
|
/**
|
2011-03-22 00:26:41 +00:00
|
|
|
* Inserts a token before the current token. Cursor now points to
|
|
|
|
* this token. You must reprocess after this.
|
2013-07-16 11:56:14 +00:00
|
|
|
* @param HTMLPurifier_Token $token
|
2008-06-27 05:33:48 +00:00
|
|
|
*/
|
2013-07-16 11:56:14 +00:00
|
|
|
private function insertBefore($token)
|
|
|
|
{
|
2013-10-13 19:53:51 +00:00
|
|
|
// NB not $this->zipper->insertBefore(), due to positioning
|
|
|
|
// differences
|
2013-10-22 21:17:41 +00:00
|
|
|
$splice = $this->zipper->splice($this->token, 0, array($token));
|
|
|
|
|
|
|
|
return $splice[1];
|
2008-06-27 05:33:48 +00:00
|
|
|
}
|
2008-10-01 07:14:28 +00:00
|
|
|
|
2008-06-27 05:33:48 +00:00
|
|
|
/**
|
2008-10-01 08:10:41 +00:00
|
|
|
* Removes current token. Cursor now points to new token occupying previously
|
2011-03-22 00:26:41 +00:00
|
|
|
* occupied space. You must reprocess after this.
|
2008-06-27 05:33:48 +00:00
|
|
|
*/
|
2013-07-16 11:56:14 +00:00
|
|
|
private function remove()
|
|
|
|
{
|
2013-10-13 19:53:51 +00:00
|
|
|
return $this->zipper->delete();
|
2008-06-27 05:33:48 +00:00
|
|
|
}
|
2006-07-23 23:29:12 +00:00
|
|
|
}
|
|
|
|
|
2017-03-07 04:31:28 +00:00
|
|
|
// Note [Injector skips]
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
// When I originally designed this class, the idea behind the 'skip'
|
|
|
|
// property of HTMLPurifier_Token was to help avoid infinite loops
|
|
|
|
// in injector processing. For example, suppose you wrote an injector
|
|
|
|
// that bolded swear words. Naively, you might write it so that
|
|
|
|
// whenever you saw ****, you replaced it with <strong>****</strong>.
|
|
|
|
//
|
|
|
|
// When this happens, we will reprocess all of the tokens with the
|
|
|
|
// other injectors. Now there is an opportunity for infinite loop:
|
|
|
|
// if we rerun the swear-word injector on these tokens, we might
|
|
|
|
// see **** and then reprocess again to get
|
|
|
|
// <strong><strong>****</strong></strong> ad infinitum.
|
|
|
|
//
|
|
|
|
// Thus, the idea of a skip is that once we process a token with
|
|
|
|
// an injector, we mark all of those tokens as having "come from"
|
|
|
|
// the injector, and we never run the injector again on these
|
|
|
|
// tokens.
|
|
|
|
//
|
|
|
|
// There were two more complications, however:
|
|
|
|
//
|
|
|
|
// - With HTMLPurifier_Injector_RemoveEmpty, we noticed that if
|
|
|
|
// you had <b><i></i></b>, after you removed the <i></i>, you
|
|
|
|
// really would like this injector to go back and reprocess
|
|
|
|
// the <b> tag, discovering that it is now empty and can be
|
|
|
|
// removed. So we reintroduced the possibility of infinite looping
|
|
|
|
// by adding a "rewind" function, which let you go back to an
|
|
|
|
// earlier point in the token stream and reprocess it with injectors.
|
|
|
|
// Needless to say, we need to UN-skip the token so it gets
|
|
|
|
// reprocessed.
|
|
|
|
//
|
|
|
|
// - Suppose that you successfuly process a token, replace it with
|
|
|
|
// one with your skip mark, but now another injector wants to
|
|
|
|
// process the skipped token with another token. Should you continue
|
|
|
|
// to skip that new token, or reprocess it? If you reprocess,
|
|
|
|
// you can end up with an infinite loop where one injector converts
|
|
|
|
// <a> to <b>, and then another injector converts it back. So
|
|
|
|
// we inherit the skips, but for some reason, I thought that we
|
|
|
|
// should inherit the skip from the first token of the token
|
|
|
|
// that we deleted. Why? Well, it seems to work OK.
|
|
|
|
//
|
|
|
|
// If I were to redesign this functionality, I would absolutely not
|
|
|
|
// go about doing it this way: the semantics are just not very well
|
|
|
|
// defined, and in any case you probably wanted to operate on trees,
|
|
|
|
// not token streams.
|
|
|
|
|
2008-12-06 09:24:59 +00:00
|
|
|
// vim: et sw=4 sts=4
|