فهرست منبع

Merge f2b0bd5da7aaf8d88e294ec5647b81687147221e into 13d9a0514bf506b84ea5122f44ceefb0e5c6e3f8

Adam Merrifield 12 سال پیش
والد
کامیت
89e37b571d
100فایلهای تغییر یافته به همراه5232 افزوده شده و 967 حذف شده
  1. 7 3
      index.php
  2. BIN
      lib/.DS_Store
  3. 2805 0
      lib/Michelf/Markdown.php
  4. 40 0
      lib/Michelf/MarkdownExtra.php
  5. 6 2
      lib/gilbitron/Pico.php
  6. 0 9
      lib/twig/AUTHORS
  7. 4 7
      lib/twig/Autoloader.php
  8. 0 474
      lib/twig/CHANGELOG
  9. 37 12
      lib/twig/Compiler.php
  10. 5 5
      lib/twig/CompilerInterface.php
  11. 278 121
      lib/twig/Environment.php
  12. 232 0
      lib/twig/Error.php
  13. 31 0
      lib/twig/Error/Loader.php
  14. 1 2
      lib/twig/Error/Runtime.php
  15. 1 2
      lib/twig/Error/Syntax.php
  16. 28 0
      lib/twig/ExistsLoaderInterface.php
  17. 147 35
      lib/twig/ExpressionParser.php
  18. 0 0
      lib/twig/Extension.php
  19. 441 107
      lib/twig/Extension/Core.php
  20. 9 3
      lib/twig/Extension/Debug.php
  21. 37 7
      lib/twig/Extension/Escaper.php
  22. 0 0
      lib/twig/Extension/Optimizer.php
  23. 0 0
      lib/twig/Extension/Sandbox.php
  24. 113 0
      lib/twig/Extension/Staging.php
  25. 64 0
      lib/twig/Extension/StringLoader.php
  26. 10 11
      lib/twig/ExtensionInterface.php
  27. 18 4
      lib/twig/Filter.php
  28. 6 2
      lib/twig/Filter/Function.php
  29. 8 3
      lib/twig/Filter/Method.php
  30. 4 2
      lib/twig/Filter/Node.php
  31. 23 0
      lib/twig/FilterCallableInterface.php
  32. 42 0
      lib/twig/FilterInterface.php
  33. 11 3
      lib/twig/Function.php
  34. 6 2
      lib/twig/Function/Function.php
  35. 8 3
      lib/twig/Function/Method.php
  36. 5 3
      lib/twig/Function/Node.php
  37. 23 0
      lib/twig/FunctionCallableInterface.php
  38. 10 8
      lib/twig/FunctionInterface.php
  39. 0 31
      lib/twig/LICENSE
  40. 15 13
      lib/twig/Lexer.php
  41. 5 5
      lib/twig/LexerInterface.php
  42. 13 17
      lib/twig/Loader/Array.php
  43. 135 0
      lib/twig/Loader/Chain.php
  44. 220 0
      lib/twig/Loader/Filesystem.php
  45. 17 17
      lib/twig/Loader/String.php
  46. 6 7
      lib/twig/LoaderInterface.php
  47. 1 2
      lib/twig/Markup.php
  48. 4 5
      lib/twig/Node.php
  49. 1 2
      lib/twig/Node/AutoEscape.php
  50. 1 2
      lib/twig/Node/Block.php
  51. 1 2
      lib/twig/Node/BlockReference.php
  52. 1 2
      lib/twig/Node/Body.php
  53. 1 2
      lib/twig/Node/Do.php
  54. 38 0
      lib/twig/Node/Embed.php
  55. 1 2
      lib/twig/Node/Expression.php
  56. 0 0
      lib/twig/Node/Expression/Array.php
  57. 0 0
      lib/twig/Node/Expression/AssignName.php
  58. 0 0
      lib/twig/Node/Expression/Binary.php
  59. 0 0
      lib/twig/Node/Expression/Binary/Add.php
  60. 0 0
      lib/twig/Node/Expression/Binary/And.php
  61. 0 0
      lib/twig/Node/Expression/Binary/BitwiseAnd.php
  62. 0 0
      lib/twig/Node/Expression/Binary/BitwiseOr.php
  63. 0 0
      lib/twig/Node/Expression/Binary/BitwiseXor.php
  64. 0 0
      lib/twig/Node/Expression/Binary/Concat.php
  65. 0 0
      lib/twig/Node/Expression/Binary/Div.php
  66. 0 0
      lib/twig/Node/Expression/Binary/Equal.php
  67. 0 0
      lib/twig/Node/Expression/Binary/FloorDiv.php
  68. 0 0
      lib/twig/Node/Expression/Binary/Greater.php
  69. 0 0
      lib/twig/Node/Expression/Binary/GreaterEqual.php
  70. 0 0
      lib/twig/Node/Expression/Binary/In.php
  71. 0 0
      lib/twig/Node/Expression/Binary/Less.php
  72. 0 0
      lib/twig/Node/Expression/Binary/LessEqual.php
  73. 0 0
      lib/twig/Node/Expression/Binary/Mod.php
  74. 0 0
      lib/twig/Node/Expression/Binary/Mul.php
  75. 0 0
      lib/twig/Node/Expression/Binary/NotEqual.php
  76. 0 0
      lib/twig/Node/Expression/Binary/NotIn.php
  77. 0 0
      lib/twig/Node/Expression/Binary/Or.php
  78. 0 0
      lib/twig/Node/Expression/Binary/Power.php
  79. 0 0
      lib/twig/Node/Expression/Binary/Range.php
  80. 0 0
      lib/twig/Node/Expression/Binary/Sub.php
  81. 1 2
      lib/twig/Node/Expression/BlockReference.php
  82. 171 0
      lib/twig/Node/Expression/Call.php
  83. 0 0
      lib/twig/Node/Expression/Conditional.php
  84. 0 0
      lib/twig/Node/Expression/Constant.php
  85. 1 2
      lib/twig/Node/Expression/ExtensionReference.php
  86. 36 0
      lib/twig/Node/Expression/Filter.php
  87. 2 3
      lib/twig/Node/Expression/Filter/Default.php
  88. 35 0
      lib/twig/Node/Expression/Function.php
  89. 2 2
      lib/twig/Node/Expression/GetAttr.php
  90. 4 0
      lib/twig/Node/Expression/MethodCall.php
  91. 15 3
      lib/twig/Node/Expression/Name.php
  92. 1 2
      lib/twig/Node/Expression/Parent.php
  93. 5 1
      lib/twig/Node/Expression/TempName.php
  94. 32 0
      lib/twig/Node/Expression/Test.php
  95. 1 2
      lib/twig/Node/Expression/Test/Constant.php
  96. 2 3
      lib/twig/Node/Expression/Test/Defined.php
  97. 1 2
      lib/twig/Node/Expression/Test/Divisibleby.php
  98. 1 2
      lib/twig/Node/Expression/Test/Even.php
  99. 1 2
      lib/twig/Node/Expression/Test/Null.php
  100. 1 2
      lib/twig/Node/Expression/Test/Odd.php

+ 7 - 3
index.php

@@ -1,4 +1,7 @@
 <?php
 <?php
+// ini_set('display_errors',1); 
+// error_reporting(E_ALL);
+
 /*
 /*
  * Pico v0.1
  * Pico v0.1
  */
  */
@@ -11,9 +14,10 @@ define('THEMES_DIR', ROOT_DIR .'themes/');
 define('CACHE_DIR', LIB_DIR .'cache/');
 define('CACHE_DIR', LIB_DIR .'cache/');
 
 
 require('config.php');
 require('config.php');
-require(LIB_DIR .'markdown.php');
-require(LIB_DIR .'twig/lib/Twig/Autoloader.php');
-require(LIB_DIR .'pico.php');
+require(LIB_DIR .'Michelf/Markdown.php');
+require(LIB_DIR .'Michelf/MarkdownExtra.php');
+require(LIB_DIR .'Twig/Autoloader.php');
+require(LIB_DIR .'gilbitron/Pico.php');
 $pico = new Pico();
 $pico = new Pico();
 
 
 ?>
 ?>

BIN
lib/.DS_Store


+ 2805 - 0
lib/Michelf/Markdown.php

@@ -0,0 +1,2805 @@
+<?php
+#
+# Markdown  -  A text-to-HTML conversion tool for web writers
+#
+# PHP Markdown  
+# Copyright (c) 2004-2013 Michel Fortin  
+# <http://michelf.com/projects/php-markdown/>
+#
+# Original Markdown  
+# Copyright (c) 2004-2006 John Gruber  
+# <http://daringfireball.net/projects/markdown/>
+#
+namespace Michelf;
+
+#
+# The following two constants are deprecated: avoid using them, they'll
+# disappear when the Lib branch becomes the only one to be updated.
+#
+# You can get the parser's version using the constant inside of the parser
+# class: \Michelf\Markdown::MARKDOWNLIB_VERSION.
+#
+
+const  MARKDOWN_VERSION  =  "1.0.1p";  # Sun 13 Jan 2013
+const  MARKDOWNEXTRA_VERSION  =  "1.2.6";  # Sun 13 Jan 2013
+
+
+#
+# Markdown Parser Class
+#
+
+class Markdown {
+
+	### Version ###
+
+	const  MARKDOWNLIB_VERSION  =  "1.3-beta4";
+
+	### Simple Function Interface ###
+
+	static function defaultTransform($text) {
+	#
+	# Initialize the parser and return the result of its transform method.
+	# This will work fine for derived classes too.
+	#
+		# Take parser class on which this function was called.
+		$parser_class = \get_called_class();
+
+		# try to take parser from the static parser list
+		static $parser_list;
+		$parser =& $parser_list[$parser_class];
+
+		# create the parser it not already set
+		if (!$parser)
+			$parser = new $parser_class;
+
+		# Transform text using parser.
+		return $parser->transform($text);
+	}
+
+	### Configuration Variables ###
+
+	# Change to ">" for HTML output.
+	var $empty_element_suffix = " />";
+	var $tab_width = 4;
+	
+	# Change to `true` to disallow markup or entities.
+	var $no_markup = false;
+	var $no_entities = false;
+	
+	# Predefined urls and titles for reference links and images.
+	var $predef_urls = array();
+	var $predef_titles = array();
+
+
+	### Parser Implementation ###
+
+	# Regex to match balanced [brackets].
+	# Needed to insert a maximum bracked depth while converting to PHP.
+	var $nested_brackets_depth = 6;
+	var $nested_brackets_re;
+	
+	var $nested_url_parenthesis_depth = 4;
+	var $nested_url_parenthesis_re;
+
+	# Table of hash values for escaped characters:
+	var $escape_chars = '\`*_{}[]()>#+-.!';
+	var $escape_chars_re;
+
+
+	function __construct() {
+	#
+	# Constructor function. Initialize appropriate member variables.
+	#
+		$this->_initDetab();
+		$this->prepareItalicsAndBold();
+	
+		$this->nested_brackets_re = 
+			str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
+			str_repeat('\])*', $this->nested_brackets_depth);
+	
+		$this->nested_url_parenthesis_re = 
+			str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
+			str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
+		
+		$this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
+		
+		# Sort document, block, and span gamut in ascendent priority order.
+		asort($this->document_gamut);
+		asort($this->block_gamut);
+		asort($this->span_gamut);
+	}
+
+
+	# Internal hashes used during transformation.
+	var $urls = array();
+	var $titles = array();
+	var $html_hashes = array();
+	
+	# Status flag to avoid invalid nesting.
+	var $in_anchor = false;
+	
+	
+	function setup() {
+	#
+	# Called before the transformation process starts to setup parser 
+	# states.
+	#
+		# Clear global hashes.
+		$this->urls = $this->predef_urls;
+		$this->titles = $this->predef_titles;
+		$this->html_hashes = array();
+		
+		$in_anchor = false;
+	}
+	
+	function teardown() {
+	#
+	# Called after the transformation process to clear any variable 
+	# which may be taking up memory unnecessarly.
+	#
+		$this->urls = array();
+		$this->titles = array();
+		$this->html_hashes = array();
+	}
+
+
+	function transform($text) {
+	#
+	# Main function. Performs some preprocessing on the input text
+	# and pass it through the document gamut.
+	#
+		$this->setup();
+	
+		# Remove UTF-8 BOM and marker character in input, if present.
+		$text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
+
+		# Standardize line endings:
+		#   DOS to Unix and Mac to Unix
+		$text = preg_replace('{\r\n?}', "\n", $text);
+
+		# Make sure $text ends with a couple of newlines:
+		$text .= "\n\n";
+
+		# Convert all tabs to spaces.
+		$text = $this->detab($text);
+
+		# Turn block-level HTML blocks into hash entries
+		$text = $this->hashHTMLBlocks($text);
+
+		# Strip any lines consisting only of spaces and tabs.
+		# This makes subsequent regexen easier to write, because we can
+		# match consecutive blank lines with /\n+/ instead of something
+		# contorted like /[ ]*\n+/ .
+		$text = preg_replace('/^[ ]+$/m', '', $text);
+
+		# Run document gamut methods.
+		foreach ($this->document_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+		
+		$this->teardown();
+
+		return $text . "\n";
+	}
+	
+	var $document_gamut = array(
+		# Strip link definitions, store in hashes.
+		"stripLinkDefinitions" => 20,
+		
+		"runBasicBlockGamut"   => 30,
+		);
+
+
+	function stripLinkDefinitions($text) {
+	#
+	# Strips link definitions from text, stores the URLs and titles in
+	# hash references.
+	#
+		$less_than_tab = $this->tab_width - 1;
+
+		# Link defs are in the form: ^[id]: url "optional title"
+		$text = preg_replace_callback('{
+							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
+							  [ ]*
+							  \n?				# maybe *one* newline
+							  [ ]*
+							(?:
+							  <(.+?)>			# url = $2
+							|
+							  (\S+?)			# url = $3
+							)
+							  [ ]*
+							  \n?				# maybe one newline
+							  [ ]*
+							(?:
+								(?<=\s)			# lookbehind for whitespace
+								["(]
+								(.*?)			# title = $4
+								[")]
+								[ ]*
+							)?	# title is optional
+							(?:\n+|\Z)
+			}xm',
+			array(&$this, '_stripLinkDefinitions_callback'),
+			$text);
+		return $text;
+	}
+	function _stripLinkDefinitions_callback($matches) {
+		$link_id = strtolower($matches[1]);
+		$url = $matches[2] == '' ? $matches[3] : $matches[2];
+		$this->urls[$link_id] = $url;
+		$this->titles[$link_id] =& $matches[4];
+		return ''; # String that will replace the block
+	}
+
+
+	function hashHTMLBlocks($text) {
+		if ($this->no_markup)  return $text;
+
+		$less_than_tab = $this->tab_width - 1;
+
+		# Hashify HTML blocks:
+		# We only want to do this for block-level HTML tags, such as headers,
+		# lists, and tables. That's because we still want to wrap <p>s around
+		# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+		# phrase emphasis, and spans. The list of tags we're looking for is
+		# hard-coded:
+		#
+		# *  List "a" is made of tags which can be both inline or block-level.
+		#    These will be treated block-level when the start tag is alone on 
+		#    its line, otherwise they're not matched here and will be taken as 
+		#    inline later.
+		# *  List "b" is made of tags which are always block-level;
+		#
+		$block_tags_a_re = 'ins|del';
+		$block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
+						   'script|noscript|form|fieldset|iframe|math|svg|'.
+						   'article|section|nav|aside|hgroup|header|footer|'.
+						   'figure';
+
+		# Regular expression for the content of a block tag.
+		$nested_tags_level = 4;
+		$attr = '
+			(?>				# optional tag attributes
+			  \s			# starts with whitespace
+			  (?>
+				[^>"/]+		# text outside quotes
+			  |
+				/+(?!>)		# slash not followed by ">"
+			  |
+				"[^"]*"		# text inside double quotes (tolerate ">")
+			  |
+				\'[^\']*\'	# text inside single quotes (tolerate ">")
+			  )*
+			)?	
+			';
+		$content =
+			str_repeat('
+				(?>
+				  [^<]+			# content without tag
+				|
+				  <\2			# nested opening tag
+					'.$attr.'	# attributes
+					(?>
+					  />
+					|
+					  >', $nested_tags_level).	# end of opening tag
+					  '.*?'.					# last level nested tag content
+			str_repeat('
+					  </\2\s*>	# closing nested tag
+					)
+				  |				
+					<(?!/\2\s*>	# other tags with a different name
+				  )
+				)*',
+				$nested_tags_level);
+		$content2 = str_replace('\2', '\3', $content);
+
+		# First, look for nested blocks, e.g.:
+		# 	<div>
+		# 		<div>
+		# 		tags for inner block must be indented.
+		# 		</div>
+		# 	</div>
+		#
+		# The outermost tags must start at the left margin for this to match, and
+		# the inner nested divs must be indented.
+		# We need to do this before the next, more liberal match, because the next
+		# match will start at the first `<div>` and stop at the first `</div>`.
+		$text = preg_replace_callback('{(?>
+			(?>
+				(?<=\n\n)		# Starting after a blank line
+				|				# or
+				\A\n?			# the beginning of the doc
+			)
+			(						# save in $1
+
+			  # Match from `\n<tag>` to `</tag>\n`, handling nested tags 
+			  # in between.
+					
+						[ ]{0,'.$less_than_tab.'}
+						<('.$block_tags_b_re.')# start tag = $2
+						'.$attr.'>			# attributes followed by > and \n
+						'.$content.'		# content, support nesting
+						</\2>				# the matching end tag
+						[ ]*				# trailing spaces/tabs
+						(?=\n+|\Z)	# followed by a newline or end of document
+
+			| # Special version for tags of group a.
+
+						[ ]{0,'.$less_than_tab.'}
+						<('.$block_tags_a_re.')# start tag = $3
+						'.$attr.'>[ ]*\n	# attributes followed by >
+						'.$content2.'		# content, support nesting
+						</\3>				# the matching end tag
+						[ ]*				# trailing spaces/tabs
+						(?=\n+|\Z)	# followed by a newline or end of document
+					
+			| # Special case just for <hr />. It was easier to make a special 
+			  # case than to make the other regex more complicated.
+			
+						[ ]{0,'.$less_than_tab.'}
+						<(hr)				# start tag = $2
+						'.$attr.'			# attributes
+						/?>					# the matching end tag
+						[ ]*
+						(?=\n{2,}|\Z)		# followed by a blank line or end of document
+			
+			| # Special case for standalone HTML comments:
+			
+					[ ]{0,'.$less_than_tab.'}
+					(?s:
+						<!-- .*? -->
+					)
+					[ ]*
+					(?=\n{2,}|\Z)		# followed by a blank line or end of document
+			
+			| # PHP and ASP-style processor instructions (<? and <%)
+			
+					[ ]{0,'.$less_than_tab.'}
+					(?s:
+						<([?%])			# $2
+						.*?
+						\2>
+					)
+					[ ]*
+					(?=\n{2,}|\Z)		# followed by a blank line or end of document
+					
+			)
+			)}Sxmi',
+			array(&$this, '_hashHTMLBlocks_callback'),
+			$text);
+
+		return $text;
+	}
+	function _hashHTMLBlocks_callback($matches) {
+		$text = $matches[1];
+		$key  = $this->hashBlock($text);
+		return "\n\n$key\n\n";
+	}
+	
+	
+	function hashPart($text, $boundary = 'X') {
+	#
+	# Called whenever a tag must be hashed when a function insert an atomic 
+	# element in the text stream. Passing $text to through this function gives
+	# a unique text-token which will be reverted back when calling unhash.
+	#
+	# The $boundary argument specify what character should be used to surround
+	# the token. By convension, "B" is used for block elements that needs not
+	# to be wrapped into paragraph tags at the end, ":" is used for elements
+	# that are word separators and "X" is used in the general case.
+	#
+		# Swap back any tag hash found in $text so we do not have to `unhash`
+		# multiple times at the end.
+		$text = $this->unhash($text);
+		
+		# Then hash the block.
+		static $i = 0;
+		$key = "$boundary\x1A" . ++$i . $boundary;
+		$this->html_hashes[$key] = $text;
+		return $key; # String that will replace the tag.
+	}
+
+
+	function hashBlock($text) {
+	#
+	# Shortcut function for hashPart with block-level boundaries.
+	#
+		return $this->hashPart($text, 'B');
+	}
+
+
+	var $block_gamut = array(
+	#
+	# These are all the transformations that form block-level
+	# tags like paragraphs, headers, and list items.
+	#
+		"doHeaders"         => 10,
+		"doHorizontalRules" => 20,
+		
+		"doLists"           => 40,
+		"doCodeBlocks"      => 50,
+		"doBlockQuotes"     => 60,
+		);
+
+	function runBlockGamut($text) {
+	#
+	# Run block gamut tranformations.
+	#
+		# We need to escape raw HTML in Markdown source before doing anything 
+		# else. This need to be done for each block, and not only at the 
+		# begining in the Markdown function since hashed blocks can be part of
+		# list items and could have been indented. Indented blocks would have 
+		# been seen as a code block in a previous pass of hashHTMLBlocks.
+		$text = $this->hashHTMLBlocks($text);
+		
+		return $this->runBasicBlockGamut($text);
+	}
+	
+	function runBasicBlockGamut($text) {
+	#
+	# Run block gamut tranformations, without hashing HTML blocks. This is 
+	# useful when HTML blocks are known to be already hashed, like in the first
+	# whole-document pass.
+	#
+		foreach ($this->block_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+		
+		# Finally form paragraph and restore hashed blocks.
+		$text = $this->formParagraphs($text);
+
+		return $text;
+	}
+	
+	
+	function doHorizontalRules($text) {
+		# Do Horizontal Rules:
+		return preg_replace(
+			'{
+				^[ ]{0,3}	# Leading space
+				([-*_])		# $1: First marker
+				(?>			# Repeated marker group
+					[ ]{0,2}	# Zero, one, or two spaces.
+					\1			# Marker character
+				){2,}		# Group repeated at least twice
+				[ ]*		# Tailing spaces
+				$			# End of line.
+			}mx',
+			"\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", 
+			$text);
+	}
+
+
+	var $span_gamut = array(
+	#
+	# These are all the transformations that occur *within* block-level
+	# tags like paragraphs, headers, and list items.
+	#
+		# Process character escapes, code spans, and inline HTML
+		# in one shot.
+		"parseSpan"           => -30,
+
+		# Process anchor and image tags. Images must come first,
+		# because ![foo][f] looks like an anchor.
+		"doImages"            =>  10,
+		"doAnchors"           =>  20,
+		
+		# Make links out of things like `<http://example.com/>`
+		# Must come after doAnchors, because you can use < and >
+		# delimiters in inline links like [this](<url>).
+		"doAutoLinks"         =>  30,
+		"encodeAmpsAndAngles" =>  40,
+
+		"doItalicsAndBold"    =>  50,
+		"doHardBreaks"        =>  60,
+		);
+
+	function runSpanGamut($text) {
+	#
+	# Run span gamut tranformations.
+	#
+		foreach ($this->span_gamut as $method => $priority) {
+			$text = $this->$method($text);
+		}
+
+		return $text;
+	}
+	
+	
+	function doHardBreaks($text) {
+		# Do hard breaks:
+		return preg_replace_callback('/ {2,}\n/', 
+			array(&$this, '_doHardBreaks_callback'), $text);
+	}
+	function _doHardBreaks_callback($matches) {
+		return $this->hashPart("<br$this->empty_element_suffix\n");
+	}
+
+
+	function doAnchors($text) {
+	#
+	# Turn Markdown link shortcuts into XHTML <a> tags.
+	#
+		if ($this->in_anchor) return $text;
+		$this->in_anchor = true;
+		
+		#
+		# First, handle reference-style links: [link text] [id]
+		#
+		$text = preg_replace_callback('{
+			(					# wrap whole match in $1
+			  \[
+				('.$this->nested_brackets_re.')	# link text = $2
+			  \]
+
+			  [ ]?				# one optional space
+			  (?:\n[ ]*)?		# one optional newline followed by spaces
+
+			  \[
+				(.*?)		# id = $3
+			  \]
+			)
+			}xs',
+			array(&$this, '_doAnchors_reference_callback'), $text);
+
+		#
+		# Next, inline-style links: [link text](url "optional title")
+		#
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  \[
+				('.$this->nested_brackets_re.')	# link text = $2
+			  \]
+			  \(			# literal paren
+				[ \n]*
+				(?:
+					<(.+?)>	# href = $3
+				|
+					('.$this->nested_url_parenthesis_re.')	# href = $4
+				)
+				[ \n]*
+				(			# $5
+				  ([\'"])	# quote char = $6
+				  (.*?)		# Title = $7
+				  \6		# matching quote
+				  [ \n]*	# ignore any spaces/tabs between closing quote and )
+				)?			# title is optional
+			  \)
+			)
+			}xs',
+			array(&$this, '_doAnchors_inline_callback'), $text);
+
+		#
+		# Last, handle reference-style shortcuts: [link text]
+		# These must come last in case you've also got [link text][1]
+		# or [link text](/foo)
+		#
+		$text = preg_replace_callback('{
+			(					# wrap whole match in $1
+			  \[
+				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
+			  \]
+			)
+			}xs',
+			array(&$this, '_doAnchors_reference_callback'), $text);
+
+		$this->in_anchor = false;
+		return $text;
+	}
+	function _doAnchors_reference_callback($matches) {
+		$whole_match =  $matches[1];
+		$link_text   =  $matches[2];
+		$link_id     =& $matches[3];
+
+		if ($link_id == "") {
+			# for shortcut links like [this][] or [this].
+			$link_id = $link_text;
+		}
+		
+		# lower-case and turn embedded newlines into spaces
+		$link_id = strtolower($link_id);
+		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
+
+		if (isset($this->urls[$link_id])) {
+			$url = $this->urls[$link_id];
+			$url = $this->encodeAttribute($url);
+			
+			$result = "<a href=\"$url\"";
+			if ( isset( $this->titles[$link_id] ) ) {
+				$title = $this->titles[$link_id];
+				$title = $this->encodeAttribute($title);
+				$result .=  " title=\"$title\"";
+			}
+		
+			$link_text = $this->runSpanGamut($link_text);
+			$result .= ">$link_text</a>";
+			$result = $this->hashPart($result);
+		}
+		else {
+			$result = $whole_match;
+		}
+		return $result;
+	}
+	function _doAnchors_inline_callback($matches) {
+		$whole_match	=  $matches[1];
+		$link_text		=  $this->runSpanGamut($matches[2]);
+		$url			=  $matches[3] == '' ? $matches[4] : $matches[3];
+		$title			=& $matches[7];
+
+		$url = $this->encodeAttribute($url);
+
+		$result = "<a href=\"$url\"";
+		if (isset($title)) {
+			$title = $this->encodeAttribute($title);
+			$result .=  " title=\"$title\"";
+		}
+		
+		$link_text = $this->runSpanGamut($link_text);
+		$result .= ">$link_text</a>";
+
+		return $this->hashPart($result);
+	}
+
+
+	function doImages($text) {
+	#
+	# Turn Markdown image shortcuts into <img> tags.
+	#
+		#
+		# First, handle reference-style labeled images: ![alt text][id]
+		#
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  !\[
+				('.$this->nested_brackets_re.')		# alt text = $2
+			  \]
+
+			  [ ]?				# one optional space
+			  (?:\n[ ]*)?		# one optional newline followed by spaces
+
+			  \[
+				(.*?)		# id = $3
+			  \]
+
+			)
+			}xs', 
+			array(&$this, '_doImages_reference_callback'), $text);
+
+		#
+		# Next, handle inline images:  ![alt text](url "optional title")
+		# Don't forget: encode * and _
+		#
+		$text = preg_replace_callback('{
+			(				# wrap whole match in $1
+			  !\[
+				('.$this->nested_brackets_re.')		# alt text = $2
+			  \]
+			  \s?			# One optional whitespace character
+			  \(			# literal paren
+				[ \n]*
+				(?:
+					<(\S*)>	# src url = $3
+				|
+					('.$this->nested_url_parenthesis_re.')	# src url = $4
+				)
+				[ \n]*
+				(			# $5
+				  ([\'"])	# quote char = $6
+				  (.*?)		# title = $7
+				  \6		# matching quote
+				  [ \n]*
+				)?			# title is optional
+			  \)
+			)
+			}xs',
+			array(&$this, '_doImages_inline_callback'), $text);
+
+		return $text;
+	}
+	function _doImages_reference_callback($matches) {
+		$whole_match = $matches[1];
+		$alt_text    = $matches[2];
+		$link_id     = strtolower($matches[3]);
+
+		if ($link_id == "") {
+			$link_id = strtolower($alt_text); # for shortcut links like ![this][].
+		}
+
+		$alt_text = $this->encodeAttribute($alt_text);
+		if (isset($this->urls[$link_id])) {
+			$url = $this->encodeAttribute($this->urls[$link_id]);
+			$result = "<img src=\"$url\" alt=\"$alt_text\"";
+			if (isset($this->titles[$link_id])) {
+				$title = $this->titles[$link_id];
+				$title = $this->encodeAttribute($title);
+				$result .=  " title=\"$title\"";
+			}
+			$result .= $this->empty_element_suffix;
+			$result = $this->hashPart($result);
+		}
+		else {
+			# If there's no such link ID, leave intact:
+			$result = $whole_match;
+		}
+
+		return $result;
+	}
+	function _doImages_inline_callback($matches) {
+		$whole_match	= $matches[1];
+		$alt_text		= $matches[2];
+		$url			= $matches[3] == '' ? $matches[4] : $matches[3];
+		$title			=& $matches[7];
+
+		$alt_text = $this->encodeAttribute($alt_text);
+		$url = $this->encodeAttribute($url);
+		$result = "<img src=\"$url\" alt=\"$alt_text\"";
+		if (isset($title)) {
+			$title = $this->encodeAttribute($title);
+			$result .=  " title=\"$title\""; # $title already quoted
+		}
+		$result .= $this->empty_element_suffix;
+
+		return $this->hashPart($result);
+	}
+
+
+	function doHeaders($text) {
+		# Setext-style headers:
+		#	  Header 1
+		#	  ========
+		#  
+		#	  Header 2
+		#	  --------
+		#
+		$text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
+			array(&$this, '_doHeaders_callback_setext'), $text);
+
+		# atx-style headers:
+		#	# Header 1
+		#	## Header 2
+		#	## Header 2 with closing hashes ##
+		#	...
+		#	###### Header 6
+		#
+		$text = preg_replace_callback('{
+				^(\#{1,6})	# $1 = string of #\'s
+				[ ]*
+				(.+?)		# $2 = Header text
+				[ ]*
+				\#*			# optional closing #\'s (not counted)
+				\n+
+			}xm',
+			array(&$this, '_doHeaders_callback_atx'), $text);
+
+		return $text;
+	}
+	function _doHeaders_callback_setext($matches) {
+		# Terrible hack to check we haven't found an empty list item.
+		if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
+			return $matches[0];
+		
+		$level = $matches[2]{0} == '=' ? 1 : 2;
+		$block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+	function _doHeaders_callback_atx($matches) {
+		$level = strlen($matches[1]);
+		$block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+
+
+	function doLists($text) {
+	#
+	# Form HTML ordered (numbered) and unordered (bulleted) lists.
+	#
+		$less_than_tab = $this->tab_width - 1;
+
+		# Re-usable patterns to match list item bullets and number markers:
+		$marker_ul_re  = '[*+-]';
+		$marker_ol_re  = '\d+[\.]';
+		$marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+
+		$markers_relist = array(
+			$marker_ul_re => $marker_ol_re,
+			$marker_ol_re => $marker_ul_re,
+			);
+
+		foreach ($markers_relist as $marker_re => $other_marker_re) {
+			# Re-usable pattern to match any entirel ul or ol list:
+			$whole_list_re = '
+				(								# $1 = whole list
+				  (								# $2
+					([ ]{0,'.$less_than_tab.'})	# $3 = number of spaces
+					('.$marker_re.')			# $4 = first list item marker
+					[ ]+
+				  )
+				  (?s:.+?)
+				  (								# $5
+					  \z
+					|
+					  \n{2,}
+					  (?=\S)
+					  (?!						# Negative lookahead for another list item marker
+						[ ]*
+						'.$marker_re.'[ ]+
+					  )
+					|
+					  (?=						# Lookahead for another kind of list
+					    \n
+						\3						# Must have the same indentation
+						'.$other_marker_re.'[ ]+
+					  )
+				  )
+				)
+			'; // mx
+			
+			# We use a different prefix before nested lists than top-level lists.
+			# See extended comment in _ProcessListItems().
+		
+			if ($this->list_level) {
+				$text = preg_replace_callback('{
+						^
+						'.$whole_list_re.'
+					}mx',
+					array(&$this, '_doLists_callback'), $text);
+			}
+			else {
+				$text = preg_replace_callback('{
+						(?:(?<=\n)\n|\A\n?) # Must eat the newline
+						'.$whole_list_re.'
+					}mx',
+					array(&$this, '_doLists_callback'), $text);
+			}
+		}
+
+		return $text;
+	}
+	function _doLists_callback($matches) {
+		# Re-usable patterns to match list item bullets and number markers:
+		$marker_ul_re  = '[*+-]';
+		$marker_ol_re  = '\d+[\.]';
+		$marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
+		
+		$list = $matches[1];
+		$list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol";
+		
+		$marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
+		
+		$list .= "\n";
+		$result = $this->processListItems($list, $marker_any_re);
+		
+		$result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
+		return "\n". $result ."\n\n";
+	}
+
+	var $list_level = 0;
+
+	function processListItems($list_str, $marker_any_re) {
+	#
+	#	Process the contents of a single ordered or unordered list, splitting it
+	#	into individual list items.
+	#
+		# The $this->list_level global keeps track of when we're inside a list.
+		# Each time we enter a list, we increment it; when we leave a list,
+		# we decrement. If it's zero, we're not in a list anymore.
+		#
+		# We do this because when we're not inside a list, we want to treat
+		# something like this:
+		#
+		#		I recommend upgrading to version
+		#		8. Oops, now this line is treated
+		#		as a sub-list.
+		#
+		# As a single paragraph, despite the fact that the second line starts
+		# with a digit-period-space sequence.
+		#
+		# Whereas when we're inside a list (or sub-list), that line will be
+		# treated as the start of a sub-list. What a kludge, huh? This is
+		# an aspect of Markdown's syntax that's hard to parse perfectly
+		# without resorting to mind-reading. Perhaps the solution is to
+		# change the syntax rules such that sub-lists must start with a
+		# starting cardinal number; e.g. "1." or "a.".
+		
+		$this->list_level++;
+
+		# trim trailing blank lines:
+		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+		$list_str = preg_replace_callback('{
+			(\n)?							# leading line = $1
+			(^[ ]*)							# leading whitespace = $2
+			('.$marker_any_re.'				# list marker and space = $3
+				(?:[ ]+|(?=\n))	# space only required if item is not empty
+			)
+			((?s:.*?))						# list item text   = $4
+			(?:(\n+(?=\n))|\n)				# tailing blank line = $5
+			(?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
+			}xm',
+			array(&$this, '_processListItems_callback'), $list_str);
+
+		$this->list_level--;
+		return $list_str;
+	}
+	function _processListItems_callback($matches) {
+		$item = $matches[4];
+		$leading_line =& $matches[1];
+		$leading_space =& $matches[2];
+		$marker_space = $matches[3];
+		$tailing_blank_line =& $matches[5];
+
+		if ($leading_line || $tailing_blank_line || 
+			preg_match('/\n{2,}/', $item))
+		{
+			# Replace marker with the appropriate whitespace indentation
+			$item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
+			$item = $this->runBlockGamut($this->outdent($item)."\n");
+		}
+		else {
+			# Recursion for sub-lists:
+			$item = $this->doLists($this->outdent($item));
+			$item = preg_replace('/\n+$/', '', $item);
+			$item = $this->runSpanGamut($item);
+		}
+
+		return "<li>" . $item . "</li>\n";
+	}
+
+
+	function doCodeBlocks($text) {
+	#
+	#	Process Markdown `<pre><code>` blocks.
+	#
+		$text = preg_replace_callback('{
+				(?:\n\n|\A\n?)
+				(	            # $1 = the code block -- one or more lines, starting with a space/tab
+				  (?>
+					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
+					.*\n+
+				  )+
+				)
+				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
+			}xm',
+			array(&$this, '_doCodeBlocks_callback'), $text);
+
+		return $text;
+	}
+	function _doCodeBlocks_callback($matches) {
+		$codeblock = $matches[1];
+
+		$codeblock = $this->outdent($codeblock);
+		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+
+		# trim leading newlines and trailing newlines
+		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
+
+		$codeblock = "<pre><code>$codeblock\n</code></pre>";
+		return "\n\n".$this->hashBlock($codeblock)."\n\n";
+	}
+
+
+	function makeCodeSpan($code) {
+	#
+	# Create a code span markup for $code. Called from handleSpanToken.
+	#
+		$code = htmlspecialchars(trim($code), ENT_NOQUOTES);
+		return $this->hashPart("<code>$code</code>");
+	}
+
+
+	var $em_relist = array(
+		''  => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
+		'_' => '(?<=\S|^)(?<!_)_(?!_)',
+		);
+	var $strong_relist = array(
+		''   => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
+		'__' => '(?<=\S|^)(?<!_)__(?!_)',
+		);
+	var $em_strong_relist = array(
+		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
+		'___' => '(?<=\S|^)(?<!_)___(?!_)',
+		);
+	var $em_strong_prepared_relist;
+	
+	function prepareItalicsAndBold() {
+	#
+	# Prepare regular expressions for searching emphasis tokens in any
+	# context.
+	#
+		foreach ($this->em_relist as $em => $em_re) {
+			foreach ($this->strong_relist as $strong => $strong_re) {
+				# Construct list of allowed token expressions.
+				$token_relist = array();
+				if (isset($this->em_strong_relist["$em$strong"])) {
+					$token_relist[] = $this->em_strong_relist["$em$strong"];
+				}
+				$token_relist[] = $em_re;
+				$token_relist[] = $strong_re;
+				
+				# Construct master expression from list.
+				$token_re = '{('. implode('|', $token_relist) .')}';
+				$this->em_strong_prepared_relist["$em$strong"] = $token_re;
+			}
+		}
+	}
+	
+	function doItalicsAndBold($text) {
+		$token_stack = array('');
+		$text_stack = array('');
+		$em = '';
+		$strong = '';
+		$tree_char_em = false;
+		
+		while (1) {
+			#
+			# Get prepared regular expression for seraching emphasis tokens
+			# in current context.
+			#
+			$token_re = $this->em_strong_prepared_relist["$em$strong"];
+			
+			#
+			# Each loop iteration search for the next emphasis token. 
+			# Each token is then passed to handleSpanToken.
+			#
+			$parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+			$text_stack[0] .= $parts[0];
+			$token =& $parts[1];
+			$text =& $parts[2];
+			
+			if (empty($token)) {
+				# Reached end of text span: empty stack without emitting.
+				# any more emphasis.
+				while ($token_stack[0]) {
+					$text_stack[1] .= array_shift($token_stack);
+					$text_stack[0] .= array_shift($text_stack);
+				}
+				break;
+			}
+			
+			$token_len = strlen($token);
+			if ($tree_char_em) {
+				# Reached closing marker while inside a three-char emphasis.
+				if ($token_len == 3) {
+					# Three-char closing marker, close em and strong.
+					array_shift($token_stack);
+					$span = array_shift($text_stack);
+					$span = $this->runSpanGamut($span);
+					$span = "<strong><em>$span</em></strong>";
+					$text_stack[0] .= $this->hashPart($span);
+					$em = '';
+					$strong = '';
+				} else {
+					# Other closing marker: close one em or strong and
+					# change current token state to match the other
+					$token_stack[0] = str_repeat($token{0}, 3-$token_len);
+					$tag = $token_len == 2 ? "strong" : "em";
+					$span = $text_stack[0];
+					$span = $this->runSpanGamut($span);
+					$span = "<$tag>$span</$tag>";
+					$text_stack[0] = $this->hashPart($span);
+					$$tag = ''; # $$tag stands for $em or $strong
+				}
+				$tree_char_em = false;
+			} else if ($token_len == 3) {
+				if ($em) {
+					# Reached closing marker for both em and strong.
+					# Closing strong marker:
+					for ($i = 0; $i < 2; ++$i) {
+						$shifted_token = array_shift($token_stack);
+						$tag = strlen($shifted_token) == 2 ? "strong" : "em";
+						$span = array_shift($text_stack);
+						$span = $this->runSpanGamut($span);
+						$span = "<$tag>$span</$tag>";
+						$text_stack[0] .= $this->hashPart($span);
+						$$tag = ''; # $$tag stands for $em or $strong
+					}
+				} else {
+					# Reached opening three-char emphasis marker. Push on token 
+					# stack; will be handled by the special condition above.
+					$em = $token{0};
+					$strong = "$em$em";
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$tree_char_em = true;
+				}
+			} else if ($token_len == 2) {
+				if ($strong) {
+					# Unwind any dangling emphasis marker:
+					if (strlen($token_stack[0]) == 1) {
+						$text_stack[1] .= array_shift($token_stack);
+						$text_stack[0] .= array_shift($text_stack);
+					}
+					# Closing strong marker:
+					array_shift($token_stack);
+					$span = array_shift($text_stack);
+					$span = $this->runSpanGamut($span);
+					$span = "<strong>$span</strong>";
+					$text_stack[0] .= $this->hashPart($span);
+					$strong = '';
+				} else {
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$strong = $token;
+				}
+			} else {
+				# Here $token_len == 1
+				if ($em) {
+					if (strlen($token_stack[0]) == 1) {
+						# Closing emphasis marker:
+						array_shift($token_stack);
+						$span = array_shift($text_stack);
+						$span = $this->runSpanGamut($span);
+						$span = "<em>$span</em>";
+						$text_stack[0] .= $this->hashPart($span);
+						$em = '';
+					} else {
+						$text_stack[0] .= $token;
+					}
+				} else {
+					array_unshift($token_stack, $token);
+					array_unshift($text_stack, '');
+					$em = $token;
+				}
+			}
+		}
+		return $text_stack[0];
+	}
+
+
+	function doBlockQuotes($text) {
+		$text = preg_replace_callback('/
+			  (								# Wrap whole match in $1
+				(?>
+				  ^[ ]*>[ ]?			# ">" at the start of a line
+					.+\n					# rest of the first line
+				  (.+\n)*					# subsequent consecutive lines
+				  \n*						# blanks
+				)+
+			  )
+			/xm',
+			array(&$this, '_doBlockQuotes_callback'), $text);
+
+		return $text;
+	}
+	function _doBlockQuotes_callback($matches) {
+		$bq = $matches[1];
+		# trim one level of quoting - trim whitespace-only lines
+		$bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
+		$bq = $this->runBlockGamut($bq);		# recurse
+
+		$bq = preg_replace('/^/m', "  ", $bq);
+		# These leading spaces cause problem with <pre> content, 
+		# so we need to fix that:
+		$bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', 
+			array(&$this, '_doBlockQuotes_callback2'), $bq);
+
+		return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n";
+	}
+	function _doBlockQuotes_callback2($matches) {
+		$pre = $matches[1];
+		$pre = preg_replace('/^  /m', '', $pre);
+		return $pre;
+	}
+
+
+	function formParagraphs($text) {
+	#
+	#	Params:
+	#		$text - string to process with html <p> tags
+	#
+		# Strip leading and trailing lines:
+		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
+
+		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+		#
+		# Wrap <p> tags and unhashify HTML blocks
+		#
+		foreach ($grafs as $key => $value) {
+			if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
+				# Is a paragraph.
+				$value = $this->runSpanGamut($value);
+				$value = preg_replace('/^([ ]*)/', "<p>", $value);
+				$value .= "</p>";
+				$grafs[$key] = $this->unhash($value);
+			}
+			else {
+				# Is a block.
+				# Modify elements of @grafs in-place...
+				$graf = $value;
+				$block = $this->html_hashes[$graf];
+				$graf = $block;
+//				if (preg_match('{
+//					\A
+//					(							# $1 = <div> tag
+//					  <div  \s+
+//					  [^>]*
+//					  \b
+//					  markdown\s*=\s*  ([\'"])	#	$2 = attr quote char
+//					  1
+//					  \2
+//					  [^>]*
+//					  >
+//					)
+//					(							# $3 = contents
+//					.*
+//					)
+//					(</div>)					# $4 = closing tag
+//					\z
+//					}xs', $block, $matches))
+//				{
+//					list(, $div_open, , $div_content, $div_close) = $matches;
+//
+//					# We can't call Markdown(), because that resets the hash;
+//					# that initialization code should be pulled into its own sub, though.
+//					$div_content = $this->hashHTMLBlocks($div_content);
+//					
+//					# Run document gamut methods on the content.
+//					foreach ($this->document_gamut as $method => $priority) {
+//						$div_content = $this->$method($div_content);
+//					}
+//
+//					$div_open = preg_replace(
+//						'{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
+//
+//					$graf = $div_open . "\n" . $div_content . "\n" . $div_close;
+//				}
+				$grafs[$key] = $graf;
+			}
+		}
+
+		return implode("\n\n", $grafs);
+	}
+
+
+	function encodeAttribute($text) {
+	#
+	# Encode text for a double-quoted HTML attribute. This function
+	# is *not* suitable for attributes enclosed in single quotes.
+	#
+		$text = $this->encodeAmpsAndAngles($text);
+		$text = str_replace('"', '&quot;', $text);
+		return $text;
+	}
+	
+	
+	function encodeAmpsAndAngles($text) {
+	#
+	# Smart processing for ampersands and angle brackets that need to 
+	# be encoded. Valid character entities are left alone unless the
+	# no-entities mode is set.
+	#
+		if ($this->no_entities) {
+			$text = str_replace('&', '&amp;', $text);
+		} else {
+			# Ampersand-encoding based entirely on Nat Irons's Amputator
+			# MT plugin: <http://bumppo.net/projects/amputator/>
+			$text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', 
+								'&amp;', $text);;
+		}
+		# Encode remaining <'s
+		$text = str_replace('<', '&lt;', $text);
+
+		return $text;
+	}
+
+
+	function doAutoLinks($text) {
+		$text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', 
+			array(&$this, '_doAutoLinks_url_callback'), $text);
+
+		# Email addresses: <address@domain.foo>
+		$text = preg_replace_callback('{
+			<
+			(?:mailto:)?
+			(
+				(?:
+					[-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
+				|
+					".*?"
+				)
+				\@
+				(?:
+					[-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+				|
+					\[[\d.a-fA-F:]+\]	# IPv4 & IPv6
+				)
+			)
+			>
+			}xi',
+			array(&$this, '_doAutoLinks_email_callback'), $text);
+
+		return $text;
+	}
+	function _doAutoLinks_url_callback($matches) {
+		$url = $this->encodeAttribute($matches[1]);
+		$link = "<a href=\"$url\">$url</a>";
+		return $this->hashPart($link);
+	}
+	function _doAutoLinks_email_callback($matches) {
+		$address = $matches[1];
+		$link = $this->encodeEmailAddress($address);
+		return $this->hashPart($link);
+	}
+
+
+	function encodeEmailAddress($addr) {
+	#
+	#	Input: an email address, e.g. "foo@example.com"
+	#
+	#	Output: the email address as a mailto link, with each character
+	#		of the address encoded as either a decimal or hex entity, in
+	#		the hopes of foiling most address harvesting spam bots. E.g.:
+	#
+	#	  <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
+	#        &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
+	#        &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
+	#        &#101;&#46;&#x63;&#111;&#x6d;</a></p>
+	#
+	#	Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+	#   With some optimizations by Milian Wolff.
+	#
+		$addr = "mailto:" . $addr;
+		$chars = preg_split('/(?<!^)(?!$)/', $addr);
+		$seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed.
+		
+		foreach ($chars as $key => $char) {
+			$ord = ord($char);
+			# Ignore non-ascii chars.
+			if ($ord < 128) {
+				$r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
+				# roughly 10% raw, 45% hex, 45% dec
+				# '@' *must* be encoded. I insist.
+				if ($r > 90 && $char != '@') /* do nothing */;
+				else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
+				else              $chars[$key] = '&#'.$ord.';';
+			}
+		}
+		
+		$addr = implode('', $chars);
+		$text = implode('', array_slice($chars, 7)); # text without `mailto:`
+		$addr = "<a href=\"$addr\">$text</a>";
+
+		return $addr;
+	}
+
+
+	function parseSpan($str) {
+	#
+	# Take the string $str and parse it into tokens, hashing embeded HTML,
+	# escaped characters and handling code spans.
+	#
+		$output = '';
+		
+		$span_re = '{
+				(
+					\\\\'.$this->escape_chars_re.'
+				|
+					(?<![`\\\\])
+					`+						# code span marker
+			'.( $this->no_markup ? '' : '
+				|
+					<!--    .*?     -->		# comment
+				|
+					<\?.*?\?> | <%.*?%>		# processing instruction
+				|
+					<[!$]?[-a-zA-Z0-9:_]+	# regular tags
+					(?>
+						\s
+						(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
+					)?
+					>
+				|
+					<[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag
+				|
+					</[-a-zA-Z0-9:_]+\s*> # closing tag
+			').'
+				)
+				}xs';
+
+		while (1) {
+			#
+			# Each loop iteration seach for either the next tag, the next 
+			# openning code span marker, or the next escaped character. 
+			# Each token is then passed to handleSpanToken.
+			#
+			$parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
+			
+			# Create token from text preceding tag.
+			if ($parts[0] != "") {
+				$output .= $parts[0];
+			}
+			
+			# Check if we reach the end.
+			if (isset($parts[1])) {
+				$output .= $this->handleSpanToken($parts[1], $parts[2]);
+				$str = $parts[2];
+			}
+			else {
+				break;
+			}
+		}
+		
+		return $output;
+	}
+	
+	
+	function handleSpanToken($token, &$str) {
+	#
+	# Handle $token provided by parseSpan by determining its nature and 
+	# returning the corresponding value that should replace it.
+	#
+		switch ($token{0}) {
+			case "\\":
+				return $this->hashPart("&#". ord($token{1}). ";");
+			case "`":
+				# Search for end marker in remaining text.
+				if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', 
+					$str, $matches))
+				{
+					$str = $matches[2];
+					$codespan = $this->makeCodeSpan($matches[1]);
+					return $this->hashPart($codespan);
+				}
+				return $token; // return as text since no ending marker found.
+			default:
+				return $this->hashPart($token);
+		}
+	}
+
+
+	function outdent($text) {
+	#
+	# Remove one level of line-leading tabs or spaces
+	#
+		return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
+	}
+
+
+	# String length function for detab. `_initDetab` will create a function to 
+	# hanlde UTF-8 if the default function does not exist.
+	var $utf8_strlen = 'mb_strlen';
+	
+	function detab($text) {
+	#
+	# Replace tabs with the appropriate amount of space.
+	#
+		# For each line we separate the line in blocks delemited by
+		# tab characters. Then we reconstruct every line by adding the 
+		# appropriate number of space between each blocks.
+		
+		$text = preg_replace_callback('/^.*\t.*$/m',
+			array(&$this, '_detab_callback'), $text);
+
+		return $text;
+	}
+	function _detab_callback($matches) {
+		$line = $matches[0];
+		$strlen = $this->utf8_strlen; # strlen function for UTF-8.
+		
+		# Split in blocks.
+		$blocks = explode("\t", $line);
+		# Add each blocks to the line.
+		$line = $blocks[0];
+		unset($blocks[0]); # Do not add first block twice.
+		foreach ($blocks as $block) {
+			# Calculate amount of space, insert spaces, insert block.
+			$amount = $this->tab_width - 
+				$strlen($line, 'UTF-8') % $this->tab_width;
+			$line .= str_repeat(" ", $amount) . $block;
+		}
+		return $line;
+	}
+	function _initDetab() {
+	#
+	# Check for the availability of the function in the `utf8_strlen` property
+	# (initially `mb_strlen`). If the function is not available, create a 
+	# function that will loosely count the number of UTF-8 characters with a
+	# regular expression.
+	#
+		if (function_exists($this->utf8_strlen)) return;
+		$this->utf8_strlen = create_function('$text', 'return preg_match_all(
+			"/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", 
+			$text, $m);');
+	}
+
+
+	function unhash($text) {
+	#
+	# Swap back in all the tags hashed by _HashHTMLBlocks.
+	#
+		return preg_replace_callback('/(.)\x1A[0-9]+\1/', 
+			array(&$this, '_unhash_callback'), $text);
+	}
+	function _unhash_callback($matches) {
+		return $this->html_hashes[$matches[0]];
+	}
+
+}
+
+
+#
+# Temporary Markdown Extra Parser Implementation Class
+#
+# NOTE: DON'T USE THIS CLASS
+# Currently the implementation of of Extra resides here in this temporary class.
+# This makes it easier to propagate the changes between the three different
+# packaging styles of PHP Markdown. When this issue is resolved, this
+# MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra
+# will contain the code. So please use \Michelf\MarkdownExtra and ignore this
+# one.
+#
+
+class _MarkdownExtra_TmpImpl extends \Michelf\Markdown {
+
+	### Configuration Variables ###
+
+	# Prefix for footnote ids.
+	var $fn_id_prefix = "";
+	
+	# Optional title attribute for footnote links and backlinks.
+	var $fn_link_title = "";
+	var $fn_backlink_title = "";
+	
+	# Optional class attribute for footnote links and backlinks.
+	var $fn_link_class = "footnote-ref";
+	var $fn_backlink_class = "footnote-backref";
+
+	# Optional class prefix for fenced code block.
+	var $code_class_prefix = "";
+	# Class attribute for code blocks goes on the `code` tag;
+	# setting this to true will put attributes on the `pre` tag instead.
+	var $code_attr_on_pre = false;
+	
+	# Predefined abbreviations.
+	var $predef_abbr = array();
+
+
+	### Parser Implementation ###
+
+	function __construct() {
+	#
+	# Constructor function. Initialize the parser object.
+	#
+		# Add extra escapable characters before parent constructor 
+		# initialize the table.
+		$this->escape_chars .= ':|';
+		
+		# Insert extra document, block, and span transformations. 
+		# Parent constructor will do the sorting.
+		$this->document_gamut += array(
+			"doFencedCodeBlocks" => 5,
+			"stripFootnotes"     => 15,
+			"stripAbbreviations" => 25,
+			"appendFootnotes"    => 50,
+			);
+		$this->block_gamut += array(
+			"doFencedCodeBlocks" => 5,
+			"doTables"           => 15,
+			"doDefLists"         => 45,
+			);
+		$this->span_gamut += array(
+			"doFootnotes"        => 5,
+			"doAbbreviations"    => 70,
+			);
+		
+		parent::__construct();
+	}
+	
+	
+	# Extra variables used during extra transformations.
+	var $footnotes = array();
+	var $footnotes_ordered = array();
+	var $footnotes_ref_count = array();
+	var $footnotes_numbers = array();
+	var $abbr_desciptions = array();
+	var $abbr_word_re = '';
+	
+	# Give the current footnote number.
+	var $footnote_counter = 1;
+	
+	
+	function setup() {
+	#
+	# Setting up Extra-specific variables.
+	#
+		parent::setup();
+		
+		$this->footnotes = array();
+		$this->footnotes_ordered = array();
+		$this->footnotes_ref_count = array();
+		$this->footnotes_numbers = array();
+		$this->abbr_desciptions = array();
+		$this->abbr_word_re = '';
+		$this->footnote_counter = 1;
+		
+		foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
+			if ($this->abbr_word_re)
+				$this->abbr_word_re .= '|';
+			$this->abbr_word_re .= preg_quote($abbr_word);
+			$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+		}
+	}
+	
+	function teardown() {
+	#
+	# Clearing Extra-specific variables.
+	#
+		$this->footnotes = array();
+		$this->footnotes_ordered = array();
+		$this->footnotes_ref_count = array();
+		$this->footnotes_numbers = array();
+		$this->abbr_desciptions = array();
+		$this->abbr_word_re = '';
+		
+		parent::teardown();
+	}
+	
+	
+	### Extra Attribute Parser ###
+
+	# Expression to use to catch attributes (includes the braces)
+	var $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}';
+	# Expression to use when parsing in a context when no capture is desired
+	var $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}';
+
+	function doExtraAttributes($tag_name, $attr) {
+	#
+	# Parse attributes caught by the $this->id_class_attr_catch_re expression
+	# and return the HTML-formatted list of attributes.
+	#
+	# Currently supported attributes are .class and #id.
+	#
+		if (empty($attr)) return "";
+		
+		# Split on components
+		preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches);
+		$elements = $matches[0];
+
+		# handle classes and ids (only first id taken into account)
+		$classes = array();
+		$id = false;
+		foreach ($elements as $element) {
+			if ($element{0} == '.') {
+				$classes[] = substr($element, 1);
+			} else if ($element{0} == '#') {
+				if ($id === false) $id = substr($element, 1);
+			}
+		}
+
+		# compose attributes as string
+		$attr_str = "";
+		if (!empty($id)) {
+			$attr_str .= ' id="'.$id.'"';
+		}
+		if (!empty($classes)) {
+			$attr_str .= ' class="'.implode(" ", $classes).'"';
+		}
+		return $attr_str;
+	}
+
+
+	### HTML Block Parser ###
+	
+	# Tags that are always treated as block tags:
+	var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption';
+						   
+	# Tags treated as block tags only if the opening tag is alone on its line:
+	var $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
+	
+	# Tags where markdown="1" default to span mode:
+	var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
+	
+	# Tags which must not have their contents modified, no matter where 
+	# they appear:
+	var $clean_tags_re = 'script|math|svg';
+	
+	# Tags that do not need to be closed.
+	var $auto_close_tags_re = 'hr|img|param|source|track';
+	
+
+	function hashHTMLBlocks($text) {
+	#
+	# Hashify HTML Blocks and "clean tags".
+	#
+	# We only want to do this for block-level HTML tags, such as headers,
+	# lists, and tables. That's because we still want to wrap <p>s around
+	# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+	# phrase emphasis, and spans. The list of tags we're looking for is
+	# hard-coded.
+	#
+	# This works by calling _HashHTMLBlocks_InMarkdown, which then calls
+	# _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" 
+	# attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
+	#  _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
+	# These two functions are calling each other. It's recursive!
+	#
+		if ($this->no_markup)  return $text;
+
+		#
+		# Call the HTML-in-Markdown hasher.
+		#
+		list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
+		
+		return $text;
+	}
+	function _hashHTMLBlocks_inMarkdown($text, $indent = 0, 
+										$enclosing_tag_re = '', $span = false)
+	{
+	#
+	# Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
+	#
+	# *   $indent is the number of space to be ignored when checking for code 
+	#     blocks. This is important because if we don't take the indent into 
+	#     account, something like this (which looks right) won't work as expected:
+	#
+	#     <div>
+	#         <div markdown="1">
+	#         Hello World.  <-- Is this a Markdown code block or text?
+	#         </div>  <-- Is this a Markdown code block or a real tag?
+	#     <div>
+	#
+	#     If you don't like this, just don't indent the tag on which
+	#     you apply the markdown="1" attribute.
+	#
+	# *   If $enclosing_tag_re is not empty, stops at the first unmatched closing 
+	#     tag with that name. Nested tags supported.
+	#
+	# *   If $span is true, text inside must treated as span. So any double 
+	#     newline will be replaced by a single newline so that it does not create 
+	#     paragraphs.
+	#
+	# Returns an array of that form: ( processed text , remaining text )
+	#
+		if ($text === '') return array('', '');
+
+		# Regex to check for the presense of newlines around a block tag.
+		$newline_before_re = '/(?:^\n?|\n\n)*$/';
+		$newline_after_re = 
+			'{
+				^						# Start of text following the tag.
+				(?>[ ]*<!--.*?-->)?		# Optional comment.
+				[ ]*\n					# Must be followed by newline.
+			}xs';
+		
+		# Regex to match any tag.
+		$block_tag_re =
+			'{
+				(					# $2: Capture whole tag.
+					</?					# Any opening or closing tag.
+						(?>				# Tag name.
+							'.$this->block_tags_re.'			|
+							'.$this->context_block_tags_re.'	|
+							'.$this->clean_tags_re.'        	|
+							(?!\s)'.$enclosing_tag_re.'
+						)
+						(?:
+							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
+							(?>
+								".*?"		|	# Double quotes (can contain `>`)
+								\'.*?\'   	|	# Single quotes (can contain `>`)
+								.+?				# Anything but quotes and `>`.
+							)*?
+						)?
+					>					# End of tag.
+				|
+					<!--    .*?     -->	# HTML Comment
+				|
+					<\?.*?\?> | <%.*?%>	# Processing instruction
+				|
+					<!\[CDATA\[.*?\]\]>	# CData Block
+				|
+					# Code span marker
+					`+
+				'. ( !$span ? ' # If not in span.
+				|
+					# Indented code block
+					(?: ^[ ]*\n | ^ | \n[ ]*\n )
+					[ ]{'.($indent+4).'}[^\n]* \n
+					(?>
+						(?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n
+					)*
+				|
+					# Fenced code block marker
+					(?<= ^ | \n )
+					[ ]{0,'.($indent+3).'}~{3,}
+									[ ]*
+					(?:
+					\.?[-_:a-zA-Z0-9]+ # standalone class name
+					|
+						'.$this->id_class_attr_nocatch_re.' # extra attributes
+					)?
+					[ ]*
+					\n
+				' : '' ). ' # End (if not is span).
+				)
+			}xs';
+
+		
+		$depth = 0;		# Current depth inside the tag tree.
+		$parsed = "";	# Parsed text that will be returned.
+
+		#
+		# Loop through every tag until we find the closing tag of the parent
+		# or loop until reaching the end of text if no parent tag specified.
+		#
+		do {
+			#
+			# Split the text using the first $tag_match pattern found.
+			# Text before  pattern will be first in the array, text after
+			# pattern will be at the end, and between will be any catches made 
+			# by the pattern.
+			#
+			$parts = preg_split($block_tag_re, $text, 2, 
+								PREG_SPLIT_DELIM_CAPTURE);
+			
+			# If in Markdown span mode, add a empty-string span-level hash 
+			# after each newline to prevent triggering any block element.
+			if ($span) {
+				$void = $this->hashPart("", ':');
+				$newline = "$void\n";
+				$parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
+			}
+			
+			$parsed .= $parts[0]; # Text before current tag.
+			
+			# If end of $text has been reached. Stop loop.
+			if (count($parts) < 3) {
+				$text = "";
+				break;
+			}
+			
+			$tag  = $parts[1]; # Tag to handle.
+			$text = $parts[2]; # Remaining text after current tag.
+			$tag_re = preg_quote($tag); # For use in a regular expression.
+			
+			#
+			# Check for: Code span marker
+			#
+			if ($tag{0} == "`") {
+				# Find corresponding end marker.
+				$tag_re = preg_quote($tag);
+				if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)'.$tag_re.'(?!`)}',
+					$text, $matches))
+				{
+					# End marker found: pass text unchanged until marker.
+					$parsed .= $tag . $matches[0];
+					$text = substr($text, strlen($matches[0]));
+				}
+				else {
+					# Unmatched marker: just skip it.
+					$parsed .= $tag;
+				}
+			}
+			#
+			# Check for: Fenced code block marker.
+			#
+			else if (preg_match('{^\n?([ ]{0,'.($indent+3).'})(~+)}', $tag, $capture)) {
+				# Fenced code block marker: find matching end marker.
+				$fence_indent = strlen($capture[1]); # use captured indent in re
+				$fence_re = $capture[2]; # use captured fence in re
+				if (preg_match('{^(?>.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text,
+					$matches)) 
+				{
+					# End marker found: pass text unchanged until marker.
+					$parsed .= $tag . $matches[0];
+					$text = substr($text, strlen($matches[0]));
+				}
+				else {
+					# No end marker: just skip it.
+					$parsed .= $tag;
+				}
+			}
+			#
+			# Check for: Indented code block.
+			#
+			else if ($tag{0} == "\n" || $tag{0} == " ") {
+				# Indented code block: pass it unchanged, will be handled 
+				# later.
+				$parsed .= $tag;
+			}
+			#
+			# Check for: Opening Block level tag or
+			#            Opening Context Block tag (like ins and del) 
+			#               used as a block tag (tag is alone on it's line).
+			#
+			else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) ||
+				(	preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) &&
+					preg_match($newline_before_re, $parsed) &&
+					preg_match($newline_after_re, $text)	)
+				)
+			{
+				# Need to parse tag and following text using the HTML parser.
+				list($block_text, $text) = 
+					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
+				
+				# Make sure it stays outside of any paragraph by adding newlines.
+				$parsed .= "\n\n$block_text\n\n";
+			}
+			#
+			# Check for: Clean tag (like script, math)
+			#            HTML Comments, processing instructions.
+			#
+			else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) ||
+				$tag{1} == '!' || $tag{1} == '?')
+			{
+				# Need to parse tag and following text using the HTML parser.
+				# (don't check for markdown attribute)
+				list($block_text, $text) = 
+					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
+				
+				$parsed .= $block_text;
+			}
+			#
+			# Check for: Tag with same name as enclosing tag.
+			#
+			else if ($enclosing_tag_re !== '' &&
+				# Same name as enclosing tag.
+				preg_match('{^</?(?:'.$enclosing_tag_re.')\b}', $tag))
+			{
+				#
+				# Increase/decrease nested tag count.
+				#
+				if ($tag{1} == '/')						$depth--;
+				else if ($tag{strlen($tag)-2} != '/')	$depth++;
+
+				if ($depth < 0) {
+					#
+					# Going out of parent element. Clean up and break so we
+					# return to the calling function.
+					#
+					$text = $tag . $text;
+					break;
+				}
+				
+				$parsed .= $tag;
+			}
+			else {
+				$parsed .= $tag;
+			}
+		} while ($depth >= 0);
+		
+		return array($parsed, $text);
+	}
+	function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
+	#
+	# Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
+	#
+	# *   Calls $hash_method to convert any blocks.
+	# *   Stops when the first opening tag closes.
+	# *   $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
+	#     (it is not inside clean tags)
+	#
+	# Returns an array of that form: ( processed text , remaining text )
+	#
+		if ($text === '') return array('', '');
+		
+		# Regex to match `markdown` attribute inside of a tag.
+		$markdown_attr_re = '
+			{
+				\s*			# Eat whitespace before the `markdown` attribute
+				markdown
+				\s*=\s*
+				(?>
+					(["\'])		# $1: quote delimiter		
+					(.*?)		# $2: attribute value
+					\1			# matching delimiter	
+				|
+					([^\s>]*)	# $3: unquoted attribute value
+				)
+				()				# $4: make $3 always defined (avoid warnings)
+			}xs';
+		
+		# Regex to match any tag.
+		$tag_re = '{
+				(					# $2: Capture whole tag.
+					</?					# Any opening or closing tag.
+						[\w:$]+			# Tag name.
+						(?:
+							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
+							(?>
+								".*?"		|	# Double quotes (can contain `>`)
+								\'.*?\'   	|	# Single quotes (can contain `>`)
+								.+?				# Anything but quotes and `>`.
+							)*?
+						)?
+					>					# End of tag.
+				|
+					<!--    .*?     -->	# HTML Comment
+				|
+					<\?.*?\?> | <%.*?%>	# Processing instruction
+				|
+					<!\[CDATA\[.*?\]\]>	# CData Block
+				)
+			}xs';
+		
+		$original_text = $text;		# Save original text in case of faliure.
+		
+		$depth		= 0;	# Current depth inside the tag tree.
+		$block_text	= "";	# Temporary text holder for current text.
+		$parsed		= "";	# Parsed text that will be returned.
+
+		#
+		# Get the name of the starting tag.
+		# (This pattern makes $base_tag_name_re safe without quoting.)
+		#
+		if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
+			$base_tag_name_re = $matches[1];
+
+		#
+		# Loop through every tag until we find the corresponding closing tag.
+		#
+		do {
+			#
+			# Split the text using the first $tag_match pattern found.
+			# Text before  pattern will be first in the array, text after
+			# pattern will be at the end, and between will be any catches made 
+			# by the pattern.
+			#
+			$parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
+			
+			if (count($parts) < 3) {
+				#
+				# End of $text reached with unbalenced tag(s).
+				# In that case, we return original text unchanged and pass the
+				# first character as filtered to prevent an infinite loop in the 
+				# parent function.
+				#
+				return array($original_text{0}, substr($original_text, 1));
+			}
+			
+			$block_text .= $parts[0]; # Text before current tag.
+			$tag         = $parts[1]; # Tag to handle.
+			$text        = $parts[2]; # Remaining text after current tag.
+			
+			#
+			# Check for: Auto-close tag (like <hr/>)
+			#			 Comments and Processing Instructions.
+			#
+			if (preg_match('{^</?(?:'.$this->auto_close_tags_re.')\b}', $tag) ||
+				$tag{1} == '!' || $tag{1} == '?')
+			{
+				# Just add the tag to the block as if it was text.
+				$block_text .= $tag;
+			}
+			else {
+				#
+				# Increase/decrease nested tag count. Only do so if
+				# the tag's name match base tag's.
+				#
+				if (preg_match('{^</?'.$base_tag_name_re.'\b}', $tag)) {
+					if ($tag{1} == '/')						$depth--;
+					else if ($tag{strlen($tag)-2} != '/')	$depth++;
+				}
+				
+				#
+				# Check for `markdown="1"` attribute and handle it.
+				#
+				if ($md_attr && 
+					preg_match($markdown_attr_re, $tag, $attr_m) &&
+					preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
+				{
+					# Remove `markdown` attribute from opening tag.
+					$tag = preg_replace($markdown_attr_re, '', $tag);
+					
+					# Check if text inside this tag must be parsed in span mode.
+					$this->mode = $attr_m[2] . $attr_m[3];
+					$span_mode = $this->mode == 'span' || $this->mode != 'block' &&
+						preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag);
+					
+					# Calculate indent before tag.
+					if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
+						$strlen = $this->utf8_strlen;
+						$indent = $strlen($matches[1], 'UTF-8');
+					} else {
+						$indent = 0;
+					}
+					
+					# End preceding block with this tag.
+					$block_text .= $tag;
+					$parsed .= $this->$hash_method($block_text);
+					
+					# Get enclosing tag name for the ParseMarkdown function.
+					# (This pattern makes $tag_name_re safe without quoting.)
+					preg_match('/^<([\w:$]*)\b/', $tag, $matches);
+					$tag_name_re = $matches[1];
+					
+					# Parse the content using the HTML-in-Markdown parser.
+					list ($block_text, $text)
+						= $this->_hashHTMLBlocks_inMarkdown($text, $indent, 
+							$tag_name_re, $span_mode);
+					
+					# Outdent markdown text.
+					if ($indent > 0) {
+						$block_text = preg_replace("/^[ ]{1,$indent}/m", "", 
+													$block_text);
+					}
+					
+					# Append tag content to parsed text.
+					if (!$span_mode)	$parsed .= "\n\n$block_text\n\n";
+					else				$parsed .= "$block_text";
+					
+					# Start over with a new block.
+					$block_text = "";
+				}
+				else $block_text .= $tag;
+			}
+			
+		} while ($depth > 0);
+		
+		#
+		# Hash last block text that wasn't processed inside the loop.
+		#
+		$parsed .= $this->$hash_method($block_text);
+		
+		return array($parsed, $text);
+	}
+
+
+	function hashClean($text) {
+	#
+	# Called whenever a tag must be hashed when a function inserts a "clean" tag
+	# in $text, it passes through this function and is automaticaly escaped, 
+	# blocking invalid nested overlap.
+	#
+		return $this->hashPart($text, 'C');
+	}
+
+
+	function doHeaders($text) {
+	#
+	# Redefined to add id and class attribute support.
+	#
+		# Setext-style headers:
+		#	  Header 1  {#header1}
+		#	  ========
+		#  
+		#	  Header 2  {#header2 .class1 .class2}
+		#	  --------
+		#
+		$text = preg_replace_callback(
+			'{
+				(^.+?)								# $1: Header text
+				(?:[ ]+ '.$this->id_class_attr_catch_re.' )?	 # $3 = id/class attributes
+				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
+			}mx',
+			array(&$this, '_doHeaders_callback_setext'), $text);
+
+		# atx-style headers:
+		#	# Header 1        {#header1}
+		#	## Header 2       {#header2}
+		#	## Header 2 with closing hashes ##  {#header3.class1.class2}
+		#	...
+		#	###### Header 6   {.class2}
+		#
+		$text = preg_replace_callback('{
+				^(\#{1,6})	# $1 = string of #\'s
+				[ ]*
+				(.+?)		# $2 = Header text
+				[ ]*
+				\#*			# optional closing #\'s (not counted)
+				(?:[ ]+ '.$this->id_class_attr_catch_re.' )?	 # $3 = id/class attributes
+				[ ]*
+				\n+
+			}xm',
+			array(&$this, '_doHeaders_callback_atx'), $text);
+
+		return $text;
+	}
+	function _doHeaders_callback_setext($matches) {
+		if ($matches[3] == '-' && preg_match('{^- }', $matches[1]))
+			return $matches[0];
+		$level = $matches[3]{0} == '=' ? 1 : 2;
+		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
+		$block = "<h$level$attr>".$this->runSpanGamut($matches[1])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+	function _doHeaders_callback_atx($matches) {
+		$level = strlen($matches[1]);
+		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
+		$block = "<h$level$attr>".$this->runSpanGamut($matches[2])."</h$level>";
+		return "\n" . $this->hashBlock($block) . "\n\n";
+	}
+
+
+	function doTables($text) {
+	#
+	# Form HTML tables.
+	#
+		$less_than_tab = $this->tab_width - 1;
+		#
+		# Find tables with leading pipe.
+		#
+		#	| Header 1 | Header 2
+		#	| -------- | --------
+		#	| Cell 1   | Cell 2
+		#	| Cell 3   | Cell 4
+		#
+		$text = preg_replace_callback('
+			{
+				^							# Start of a line
+				[ ]{0,'.$less_than_tab.'}	# Allowed whitespace.
+				[|]							# Optional leading pipe (present)
+				(.+) \n						# $1: Header row (at least one pipe)
+				
+				[ ]{0,'.$less_than_tab.'}	# Allowed whitespace.
+				[|] ([ ]*[-:]+[-| :]*) \n	# $2: Header underline
+				
+				(							# $3: Cells
+					(?>
+						[ ]*				# Allowed whitespace.
+						[|] .* \n			# Row content.
+					)*
+				)
+				(?=\n|\Z)					# Stop at final double newline.
+			}xm',
+			array(&$this, '_doTable_leadingPipe_callback'), $text);
+		
+		#
+		# Find tables without leading pipe.
+		#
+		#	Header 1 | Header 2
+		#	-------- | --------
+		#	Cell 1   | Cell 2
+		#	Cell 3   | Cell 4
+		#
+		$text = preg_replace_callback('
+			{
+				^							# Start of a line
+				[ ]{0,'.$less_than_tab.'}	# Allowed whitespace.
+				(\S.*[|].*) \n				# $1: Header row (at least one pipe)
+				
+				[ ]{0,'.$less_than_tab.'}	# Allowed whitespace.
+				([-:]+[ ]*[|][-| :]*) \n	# $2: Header underline
+				
+				(							# $3: Cells
+					(?>
+						.* [|] .* \n		# Row content
+					)*
+				)
+				(?=\n|\Z)					# Stop at final double newline.
+			}xm',
+			array(&$this, '_DoTable_callback'), $text);
+
+		return $text;
+	}
+	function _doTable_leadingPipe_callback($matches) {
+		$head		= $matches[1];
+		$underline	= $matches[2];
+		$content	= $matches[3];
+		
+		# Remove leading pipe for each row.
+		$content	= preg_replace('/^ *[|]/m', '', $content);
+		
+		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
+	}
+	function _doTable_callback($matches) {
+		$head		= $matches[1];
+		$underline	= $matches[2];
+		$content	= $matches[3];
+
+		# Remove any tailing pipes for each line.
+		$head		= preg_replace('/[|] *$/m', '', $head);
+		$underline	= preg_replace('/[|] *$/m', '', $underline);
+		$content	= preg_replace('/[|] *$/m', '', $content);
+		
+		# Reading alignement from header underline.
+		$separators	= preg_split('/ *[|] */', $underline);
+		foreach ($separators as $n => $s) {
+			if (preg_match('/^ *-+: *$/', $s))		$attr[$n] = ' align="right"';
+			else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"';
+			else if (preg_match('/^ *:-+ *$/', $s))	$attr[$n] = ' align="left"';
+			else									$attr[$n] = '';
+		}
+		
+		# Parsing span elements, including code spans, character escapes, 
+		# and inline HTML tags, so that pipes inside those gets ignored.
+		$head		= $this->parseSpan($head);
+		$headers	= preg_split('/ *[|] */', $head);
+		$col_count	= count($headers);
+		
+		# Write column headers.
+		$text = "<table>\n";
+		$text .= "<thead>\n";
+		$text .= "<tr>\n";
+		foreach ($headers as $n => $header)
+			$text .= "  <th$attr[$n]>".$this->runSpanGamut(trim($header))."</th>\n";
+		$text .= "</tr>\n";
+		$text .= "</thead>\n";
+		
+		# Split content by row.
+		$rows = explode("\n", trim($content, "\n"));
+		
+		$text .= "<tbody>\n";
+		foreach ($rows as $row) {
+			# Parsing span elements, including code spans, character escapes, 
+			# and inline HTML tags, so that pipes inside those gets ignored.
+			$row = $this->parseSpan($row);
+			
+			# Split row by cell.
+			$row_cells = preg_split('/ *[|] */', $row, $col_count);
+			$row_cells = array_pad($row_cells, $col_count, '');
+			
+			$text .= "<tr>\n";
+			foreach ($row_cells as $n => $cell)
+				$text .= "  <td$attr[$n]>".$this->runSpanGamut(trim($cell))."</td>\n";
+			$text .= "</tr>\n";
+		}
+		$text .= "</tbody>\n";
+		$text .= "</table>";
+		
+		return $this->hashBlock($text) . "\n";
+	}
+
+	
+	function doDefLists($text) {
+	#
+	# Form HTML definition lists.
+	#
+		$less_than_tab = $this->tab_width - 1;
+
+		# Re-usable pattern to match any entire dl list:
+		$whole_list_re = '(?>
+			(								# $1 = whole list
+			  (								# $2
+				[ ]{0,'.$less_than_tab.'}
+				((?>.*\S.*\n)+)				# $3 = defined term
+				\n?
+				[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+			  )
+			  (?s:.+?)
+			  (								# $4
+				  \z
+				|
+				  \n{2,}
+				  (?=\S)
+				  (?!						# Negative lookahead for another term
+					[ ]{0,'.$less_than_tab.'}
+					(?: \S.*\n )+?			# defined term
+					\n?
+					[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+				  )
+				  (?!						# Negative lookahead for another definition
+					[ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition
+				  )
+			  )
+			)
+		)'; // mx
+
+		$text = preg_replace_callback('{
+				(?>\A\n?|(?<=\n\n))
+				'.$whole_list_re.'
+			}mx',
+			array(&$this, '_doDefLists_callback'), $text);
+
+		return $text;
+	}
+	function _doDefLists_callback($matches) {
+		# Re-usable patterns to match list item bullets and number markers:
+		$list = $matches[1];
+		
+		# Turn double returns into triple returns, so that we can make a
+		# paragraph for the last item in a list, if necessary:
+		$result = trim($this->processDefListItems($list));
+		$result = "<dl>\n" . $result . "\n</dl>";
+		return $this->hashBlock($result) . "\n\n";
+	}
+
+
+	function processDefListItems($list_str) {
+	#
+	#	Process the contents of a single definition list, splitting it
+	#	into individual term and definition list items.
+	#
+		$less_than_tab = $this->tab_width - 1;
+		
+		# trim trailing blank lines:
+		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
+
+		# Process definition terms.
+		$list_str = preg_replace_callback('{
+			(?>\A\n?|\n\n+)					# leading line
+			(								# definition terms = $1
+				[ ]{0,'.$less_than_tab.'}	# leading whitespace
+				(?!\:[ ]|[ ])				# negative lookahead for a definition
+											#   mark (colon) or more whitespace.
+				(?> \S.* \n)+?				# actual term (not whitespace).	
+			)			
+			(?=\n?[ ]{0,3}:[ ])				# lookahead for following line feed 
+											#   with a definition mark.
+			}xm',
+			array(&$this, '_processDefListItems_callback_dt'), $list_str);
+
+		# Process actual definitions.
+		$list_str = preg_replace_callback('{
+			\n(\n+)?						# leading line = $1
+			(								# marker space = $2
+				[ ]{0,'.$less_than_tab.'}	# whitespace before colon
+				\:[ ]+						# definition mark (colon)
+			)
+			((?s:.+?))						# definition text = $3
+			(?= \n+ 						# stop at next definition mark,
+				(?:							# next term or end of text
+					[ ]{0,'.$less_than_tab.'} \:[ ]	|
+					<dt> | \z
+				)						
+			)					
+			}xm',
+			array(&$this, '_processDefListItems_callback_dd'), $list_str);
+
+		return $list_str;
+	}
+	function _processDefListItems_callback_dt($matches) {
+		$terms = explode("\n", trim($matches[1]));
+		$text = '';
+		foreach ($terms as $term) {
+			$term = $this->runSpanGamut(trim($term));
+			$text .= "\n<dt>" . $term . "</dt>";
+		}
+		return $text . "\n";
+	}
+	function _processDefListItems_callback_dd($matches) {
+		$leading_line	= $matches[1];
+		$marker_space	= $matches[2];
+		$def			= $matches[3];
+
+		if ($leading_line || preg_match('/\n{2,}/', $def)) {
+			# Replace marker with the appropriate whitespace indentation
+			$def = str_repeat(' ', strlen($marker_space)) . $def;
+			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
+			$def = "\n". $def ."\n";
+		}
+		else {
+			$def = rtrim($def);
+			$def = $this->runSpanGamut($this->outdent($def));
+		}
+
+		return "\n<dd>" . $def . "</dd>\n";
+	}
+
+
+	function doFencedCodeBlocks($text) {
+	#
+	# Adding the fenced code block syntax to regular Markdown:
+	#
+	# ~~~
+	# Code block
+	# ~~~
+	#
+		$less_than_tab = $this->tab_width;
+		
+		$text = preg_replace_callback('{
+				(?:\n|\A)
+				# 1: Opening marker
+				(
+					~{3,} # Marker: three tilde or more.
+				)
+				[ ]*
+				(?:
+					\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
+				|
+					'.$this->id_class_attr_catch_re.' # 3: Extra attributes
+				)?
+				[ ]* \n # Whitespace and newline following marker.
+				
+				# 4: Content
+				(
+					(?>
+						(?!\1 [ ]* \n)	# Not a closing marker.
+						.*\n+
+					)+
+				)
+				
+				# Closing marker.
+				\1 [ ]* \n
+			}xm',
+			array(&$this, '_doFencedCodeBlocks_callback'), $text);
+
+		return $text;
+	}
+	function _doFencedCodeBlocks_callback($matches) {
+		$classname =& $matches[2];
+		$attrs     =& $matches[3];
+		$codeblock = $matches[4];
+		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
+		$codeblock = preg_replace_callback('/^\n+/',
+			array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock);
+
+		if ($classname != "") {
+			if ($classname{0} == '.')
+				$classname = substr($classname, 1);
+			$attr_str = ' class="'.$this->code_class_prefix.$classname.'"';
+		} else {
+			$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs);
+		}
+		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
+		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
+		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
+		
+		return "\n\n".$this->hashBlock($codeblock)."\n\n";
+	}
+	function _doFencedCodeBlocks_newlines($matches) {
+		return str_repeat("<br$this->empty_element_suffix", 
+			strlen($matches[0]));
+	}
+
+
+	#
+	# Redefining emphasis markers so that emphasis by underscore does not
+	# work in the middle of a word.
+	#
+	var $em_relist = array(
+		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'*' => '(?<=\S|^)(?<!\*)\*(?!\*)',
+		'_' => '(?<=\S|^)(?<!_)_(?![a-zA-Z0-9_])',
+		);
+	var $strong_relist = array(
+		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'**' => '(?<=\S|^)(?<!\*)\*\*(?!\*)',
+		'__' => '(?<=\S|^)(?<!_)__(?![a-zA-Z0-9_])',
+		);
+	var $em_strong_relist = array(
+		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?=\S|$)(?![\.,:;]\s)',
+		'***' => '(?<=\S|^)(?<!\*)\*\*\*(?!\*)',
+		'___' => '(?<=\S|^)(?<!_)___(?![a-zA-Z0-9_])',
+		);
+
+
+	function formParagraphs($text) {
+	#
+	#	Params:
+	#		$text - string to process with html <p> tags
+	#
+		# Strip leading and trailing lines:
+		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
+		
+		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
+
+		#
+		# Wrap <p> tags and unhashify HTML blocks
+		#
+		foreach ($grafs as $key => $value) {
+			$value = trim($this->runSpanGamut($value));
+			
+			# Check if this should be enclosed in a paragraph.
+			# Clean tag hashes & block tag hashes are left alone.
+			$is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
+			
+			if ($is_p) {
+				$value = "<p>$value</p>";
+			}
+			$grafs[$key] = $value;
+		}
+		
+		# Join grafs in one text, then unhash HTML tags. 
+		$text = implode("\n\n", $grafs);
+		
+		# Finish by removing any tag hashes still present in $text.
+		$text = $this->unhash($text);
+		
+		return $text;
+	}
+	
+	
+	### Footnotes
+	
+	function stripFootnotes($text) {
+	#
+	# Strips link definitions from text, stores the URLs and titles in
+	# hash references.
+	#
+		$less_than_tab = $this->tab_width - 1;
+
+		# Link defs are in the form: [^id]: url "optional title"
+		$text = preg_replace_callback('{
+			^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?:	# note_id = $1
+			  [ ]*
+			  \n?					# maybe *one* newline
+			(						# text = $2 (no blank lines allowed)
+				(?:					
+					.+				# actual text
+				|
+					\n				# newlines but 
+					(?!\[\^.+?\]:\s)# negative lookahead for footnote marker.
+					(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed 
+									# by non-indented content
+				)*
+			)		
+			}xm',
+			array(&$this, '_stripFootnotes_callback'),
+			$text);
+		return $text;
+	}
+	function _stripFootnotes_callback($matches) {
+		$note_id = $this->fn_id_prefix . $matches[1];
+		$this->footnotes[$note_id] = $this->outdent($matches[2]);
+		return ''; # String that will replace the block
+	}
+
+
+	function doFootnotes($text) {
+	#
+	# Replace footnote references in $text [^id] with a special text-token 
+	# which will be replaced by the actual footnote marker in appendFootnotes.
+	#
+		if (!$this->in_anchor) {
+			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
+		}
+		return $text;
+	}
+
+	
+	function appendFootnotes($text) {
+	#
+	# Append footnote list to text.
+	#
+		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', 
+			array(&$this, '_appendFootnotes_callback'), $text);
+	
+		if (!empty($this->footnotes_ordered)) {
+			$text .= "\n\n";
+			$text .= "<div class=\"footnotes\">\n";
+			$text .= "<hr". $this->empty_element_suffix ."\n";
+			$text .= "<ol>\n\n";
+			
+			$attr = " rev=\"footnote\"";
+			if ($this->fn_backlink_class != "") {
+				$class = $this->fn_backlink_class;
+				$class = $this->encodeAttribute($class);
+				$attr .= " class=\"$class\"";
+			}
+			if ($this->fn_backlink_title != "") {
+				$title = $this->fn_backlink_title;
+				$title = $this->encodeAttribute($title);
+				$attr .= " title=\"$title\"";
+			}
+			$num = 0;
+			
+			while (!empty($this->footnotes_ordered)) {
+				$footnote = reset($this->footnotes_ordered);
+				$note_id = key($this->footnotes_ordered);
+				unset($this->footnotes_ordered[$note_id]);
+				$ref_count = $this->footnotes_ref_count[$note_id];
+				unset($this->footnotes_ref_count[$note_id]);
+				unset($this->footnotes[$note_id]);
+				
+				$footnote .= "\n"; # Need to append newline before parsing.
+				$footnote = $this->runBlockGamut("$footnote\n");				
+				$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', 
+					array(&$this, '_appendFootnotes_callback'), $footnote);
+				
+				$attr = str_replace("%%", ++$num, $attr);
+				$note_id = $this->encodeAttribute($note_id);
+
+				# Prepare backlink, multiple backlinks if multiple references
+				$backlink = "<a href=\"#fnref:$note_id\"$attr>&#8617;</a>";
+				for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
+					$backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>&#8617;</a>";
+				}
+				# Add backlink to last paragraph; create new paragraph if needed.
+				if (preg_match('{</p>$}', $footnote)) {
+					$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
+				} else {
+					$footnote .= "\n\n<p>$backlink</p>";
+				}
+				
+				$text .= "<li id=\"fn:$note_id\">\n";
+				$text .= $footnote . "\n";
+				$text .= "</li>\n\n";
+			}
+			
+			$text .= "</ol>\n";
+			$text .= "</div>";
+		}
+		return $text;
+	}
+	function _appendFootnotes_callback($matches) {
+		$node_id = $this->fn_id_prefix . $matches[1];
+		
+		# Create footnote marker only if it has a corresponding footnote *and*
+		# the footnote hasn't been used by another marker.
+		if (isset($this->footnotes[$node_id])) {
+			$num =& $this->footnotes_numbers[$node_id];
+			if (!isset($num)) {
+				# Transfer footnote content to the ordered list and give it its
+				# number
+				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
+				$this->footnotes_ref_count[$node_id] = 1;
+				$num = $this->footnote_counter++;
+				$ref_count_mark = '';
+			} else {
+				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
+			}
+			
+			if ($this->fn_link_class != "") {
+				$class = $this->fn_link_class;
+				$class = $this->encodeAttribute($class);
+				$attr .= " class=\"$class\"";
+			}
+			if ($this->fn_link_title != "") {
+				$title = $this->fn_link_title;
+				$title = $this->encodeAttribute($title);
+				$attr .= " title=\"$title\"";
+			}
+			
+			$attr = str_replace("%%", $num, $attr);
+			$node_id = $this->encodeAttribute($node_id);
+			
+			return
+				"<sup id=\"fnref$ref_count_mark:$node_id\">".
+				"<a href=\"#fn:$node_id\"$attr>$num</a>".
+				"</sup>";
+		}
+		
+		return "[^".$matches[1]."]";
+	}
+		
+	
+	### Abbreviations ###
+	
+	function stripAbbreviations($text) {
+	#
+	# Strips abbreviations from text, stores titles in hash references.
+	#
+		$less_than_tab = $this->tab_width - 1;
+
+		# Link defs are in the form: [id]*: url "optional title"
+		$text = preg_replace_callback('{
+			^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?:	# abbr_id = $1
+			(.*)					# text = $2 (no blank lines allowed)	
+			}xm',
+			array(&$this, '_stripAbbreviations_callback'),
+			$text);
+		return $text;
+	}
+	function _stripAbbreviations_callback($matches) {
+		$abbr_word = $matches[1];
+		$abbr_desc = $matches[2];
+		if ($this->abbr_word_re)
+			$this->abbr_word_re .= '|';
+		$this->abbr_word_re .= preg_quote($abbr_word);
+		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
+		return ''; # String that will replace the block
+	}
+	
+	
+	function doAbbreviations($text) {
+	#
+	# Find defined abbreviations in text and wrap them in <abbr> elements.
+	#
+		if ($this->abbr_word_re) {
+			// cannot use the /x modifier because abbr_word_re may 
+			// contain significant spaces:
+			$text = preg_replace_callback('{'.
+				'(?<![\w\x1A])'.
+				'(?:'.$this->abbr_word_re.')'.
+				'(?![\w\x1A])'.
+				'}', 
+				array(&$this, '_doAbbreviations_callback'), $text);
+		}
+		return $text;
+	}
+	function _doAbbreviations_callback($matches) {
+		$abbr = $matches[0];
+		if (isset($this->abbr_desciptions[$abbr])) {
+			$desc = $this->abbr_desciptions[$abbr];
+			if (empty($desc)) {
+				return $this->hashPart("<abbr>$abbr</abbr>");
+			} else {
+				$desc = $this->encodeAttribute($desc);
+				return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
+			}
+		} else {
+			return $matches[0];
+		}
+	}
+
+}
+
+
+?>

+ 40 - 0
lib/Michelf/MarkdownExtra.php

@@ -0,0 +1,40 @@
+<?php
+#
+# Markdown Extra  -  A text-to-HTML conversion tool for web writers
+#
+# PHP Markdown Extra
+# Copyright (c) 2004-2013 Michel Fortin  
+# <http://michelf.com/projects/php-markdown/>
+#
+# Original Markdown
+# Copyright (c) 2004-2006 John Gruber  
+# <http://daringfireball.net/projects/markdown/>
+#
+namespace Michelf;
+
+
+# Just force Michelf/Markdown.php to load. This is needed to load
+# the temporary implementation class. See below for details.
+\Michelf\Markdown::MARKDOWNLIB_VERSION;
+
+#
+# Markdown Extra Parser Class
+#
+# Note: Currently the implementation resides in the temporary class
+# \Michelf\MarkdownExtra_TmpImpl (in the same file as \Michelf\Markdown).
+# This makes it easier to propagate the changes between the three different
+# packaging styles of PHP Markdown. Once this issue is resolved, the
+# _MarkdownExtra_TmpImpl will disappear and this one will contain the code.
+#
+
+class MarkdownExtra extends \Michelf\_MarkdownExtra_TmpImpl {
+
+	### Parser Implementation ###
+
+	# Temporarily, the implemenation is in the _MarkdownExtra_TmpImpl class.
+	# See note above.
+
+}
+
+
+?>

+ 6 - 2
lib/pico.php → lib/gilbitron/Pico.php

@@ -1,4 +1,8 @@
 <?php 
 <?php 
+/**
+ * use the Michelf namespaces
+ */
+use \Michelf\Markdown, \Michelf\MarkdownExtra;
 
 
 class Pico {
 class Pico {
 
 
@@ -51,9 +55,9 @@ class Pico {
 	function parse_content($content)
 	function parse_content($content)
 	{
 	{
 		$content = str_replace('%base_url%', $this->base_url(), $content);
 		$content = str_replace('%base_url%', $this->base_url(), $content);
-		$content = Markdown($content);
+		$my_html = MarkdownExtra::defaultTransform($content);
 
 
-		return $content;
+		return $my_html;
 	}
 	}
 
 
 	function read_file_meta($content)
 	function read_file_meta($content)

+ 0 - 9
lib/twig/AUTHORS

@@ -1,9 +0,0 @@
-Twig is written and maintained by the Twig Team:
-
-Lead Developer:
-
-- Fabien Potencier <fabien.potencier@symfony-project.org>
-
-Project Founder:
-
-- Armin Ronacher <armin.ronacher@active-4.com>

+ 4 - 7
lib/twig/lib/Twig/Autoloader.php → lib/twig/Autoloader.php

@@ -12,15 +12,14 @@
 /**
 /**
  * Autoloads Twig classes.
  * Autoloads Twig classes.
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Autoloader
 class Twig_Autoloader
 {
 {
     /**
     /**
      * Registers Twig_Autoloader as an SPL autoloader.
      * Registers Twig_Autoloader as an SPL autoloader.
      */
      */
-    static public function register()
+    public static function register()
     {
     {
         ini_set('unserialize_callback_func', 'spl_autoload_call');
         ini_set('unserialize_callback_func', 'spl_autoload_call');
         spl_autoload_register(array(new self, 'autoload'));
         spl_autoload_register(array(new self, 'autoload'));
@@ -29,11 +28,9 @@ class Twig_Autoloader
     /**
     /**
      * Handles autoloading of classes.
      * Handles autoloading of classes.
      *
      *
-     * @param  string  $class  A class name.
-     *
-     * @return boolean Returns true if the class has been loaded
+     * @param string $class A class name.
      */
      */
-    static public function autoload($class)
+    public static function autoload($class)
     {
     {
         if (0 !== strpos($class, 'Twig')) {
         if (0 !== strpos($class, 'Twig')) {
             return;
             return;

+ 0 - 474
lib/twig/CHANGELOG

@@ -1,474 +0,0 @@
-* 1.6.4 (2012-04-02)
-
- * fixed PHP notice in Twig_Error::guessTemplateLine() introduced in 1.6.3
- * fixed performance when compiling large files
- * optimized parent template creation when the template does not use dynamic inheritance
-
-* 1.6.3 (2012-03-22)
-
- * fixed usage of Z_ADDREF_P for PHP 5.2 in the C extension
- * fixed compilation of numeric values used in templates when using a locale where the decimal separator is not a dot
- * made the strategy used to guess the real template file name and line number in exception messages much faster and more accurate
-
-* 1.6.2 (2012-03-18)
-
- * fixed sandbox mode when used with inheritance
- * added preserveKeys support for the slice filter
- * fixed the date filter when a DateTime instance is passed with a specific timezone
- * added a trim filter
-
-* 1.6.1 (2012-02-29)
-
- * fixed Twig C extension
- * removed the creation of Twig_Markup instances when not needed
- * added a way to set the default global timezone for dates
- * fixed the slice filter on strings when the length is not specified
- * fixed the creation of the cache directory in case of a race condition
-
-* 1.6.0 (2012-02-04)
-
- * fixed raw blocks when used with the whitespace trim option
- * made a speed optimization to macro calls when imported via the "from" tag
- * fixed globals, parsers, visitors, filters, tests, and functions management in Twig_Environment when a new one or new extension is added
- * fixed the attribute function when passing arguments
- * added slice notation support for the [] operator (syntactic sugar for the slice operator)
- * added a slice filter
- * added string support for the reverse filter
- * fixed the empty test and the length filter for Twig_Markup instances
- * added a date function to ease date comparison
- * fixed unary operators precedence
- * added recursive parsing support in the parser
- * added string and integer handling for the random function
-
-* 1.5.1 (2012-01-05)
-
- * fixed a regression when parsing strings
-
-* 1.5.0 (2012-01-04)
-
- * added Traversable objects support for the join filter
-
-* 1.5.0-RC2 (2011-12-30)
-
- * added a way to set the default global date interval format
- * fixed the date filter for DateInterval instances (setTimezone() does not exist for them)
- * refactored Twig_Template::display() to ease its extension
- * added a number_format filter
-
-* 1.5.0-RC1 (2011-12-26)
-
- * removed the need to quote hash keys
- * allowed hash keys to be any expression
- * added a do tag
- * added a flush tag
- * added support for dynamically named filters and functions
- * added a dump function to help debugging templates
- * added a nl2br filter
- * added a random function
- * added a way to change the default format for the date filter
- * fixed the lexer when an operator ending with a letter ends a line
- * added string interpolation support
- * enhanced exceptions for unknown filters, functions, tests, and tags
-
-* 1.4.0 (2011-12-07)
-
- * fixed lexer when using big numbers (> PHP_INT_MAX)
- * added missing preserveKeys argument to the reverse filter
- * fixed macros containing filter tag calls
-
-* 1.4.0-RC2 (2011-11-27)
-
- * removed usage of Reflection in Twig_Template::getAttribute()
- * added a C extension that can optionally replace Twig_Template::getAttribute()
- * added negative timestamp support to the date filter
-
-* 1.4.0-RC1 (2011-11-20)
-
- * optimized variable access when using PHP 5.4
- * changed the precedence of the .. operator to be more consistent with languages that implements such a feature like Ruby
- * added an Exception to Twig_Loader_Array::isFresh() method when the template does not exist to be consistent with other loaders
- * added Twig_Function_Node to allow more complex functions to have their own Node class
- * added Twig_Filter_Node to allow more complex filters to have their own Node class
- * added Twig_Test_Node to allow more complex tests to have their own Node class
- * added a better error message when a template is empty but contain a BOM
- * fixed "in" operator for empty strings
- * fixed the "defined" test and the "default" filter (now works with more than one call (foo.bar.foo) and for both values of the strict_variables option)
- * changed the way extensions are loaded (addFilter/addFunction/addGlobal/addTest/addNodeVisitor/addTokenParser/addExtension can now be called in any order)
- * added Twig_Environment::display()
- * made the escape filter smarter when the encoding is not supported by PHP
- * added a convert_encoding filter
- * moved all node manipulations outside the compile() Node method
- * made several speed optimizations
-
-* 1.3.0 (2011-10-08)
-
-no changes
-
-* 1.3.0-RC1 (2011-10-04)
-
- * added an optimization for the parent() function
- * added cache reloading when auto_reload is true and an extension has been modified
- * added the possibility to force the escaping of a string already marked as safe (instance of Twig_Markup)
- * allowed empty templates to be used as traits
- * added traits support for the "parent" function
-
-* 1.2.0 (2011-09-13)
-
-no changes
-
-* 1.2.0-RC1 (2011-09-10)
-
- * enhanced the exception when a tag remains unclosed
- * added support for empty Countable objects for the "empty" test
- * fixed algorithm that determines if a template using inheritance is valid (no output between block definitions)
- * added better support for encoding problems when escaping a string (available as of PHP 5.4)
- * added a way to ignore a missing template when using the "include" tag ({% include "foo" ignore missing %})
- * added support for an array of templates to the "include" and "extends" tags ({% include ['foo', 'bar'] %})
- * added support for bitwise operators in expressions
- * added the "attribute" function to allow getting dynamic attributes on variables
- * added Twig_Loader_Chain
- * added Twig_Loader_Array::setTemplate()
- * added an optimization for the set tag when used to capture a large chunk of static text
- * changed name regex to match PHP one "[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*" (works for blocks, tags, functions, filters, and macros)
- * removed the possibility to use the "extends" tag from a block
- * added "if" modifier support to "for" loops
-
-* 1.1.2 (2011-07-30)
-
- * fixed json_encode filter on PHP 5.2
- * fixed regression introduced in 1.1.1 ({{ block(foo|lower) }})
- * fixed inheritance when using conditional parents
- * fixed compilation of templates when the body of a child template is not empty
- * fixed output when a macro throws an exception
- * fixed a parsing problem when a large chunk of text is enclosed in a comment tag
- * added PHPDoc for all Token parsers and Core extension functions
-
-* 1.1.1 (2011-07-17)
-
- * added a performance optimization in the Optimizer (also helps to lower the number of nested level calls)
- * made some performance improvement for some edge cases
-
-* 1.1.0 (2011-06-28)
-
- * fixed json_encode filter
-
-* 1.1.0-RC3 (2011-06-24)
-
- * fixed method case-sensitivity when using the sandbox mode
- * added timezone support for the date filter
- * fixed possible security problems with NUL bytes
-
-* 1.1.0-RC2 (2011-06-16)
-
- * added an exception when the template passed to "use" is not a string
- * made 'a.b is defined' not throw an exception if a is not defined (in strict mode)
- * added {% line \d+ %} directive
-
-* 1.1.0-RC1 (2011-05-28)
-
-Flush your cache after upgrading.
-
- * fixed date filter when using a timestamp
- * fixed the defined test for some cases
- * fixed a parsing problem when a large chunk of text is enclosed in a raw tag
- * added support for horizontal reuse of template blocks (see docs for more information)
- * added whitespace control modifier to all tags (see docs for more information)
- * added null as an alias for none (the null test is also an alias for the none test now)
- * made TRUE, FALSE, NONE equivalent to their lowercase counterparts
- * wrapped all compilation and runtime exceptions with Twig_Error_Runtime and added logic to guess the template name and line
- * moved display() method to Twig_Template (generated templates should now use doDisplay() instead)
-
-* 1.0.0 (2011-03-27)
-
- * fixed output when using mbstring
- * fixed duplicate call of methods when using the sandbox
- * made the charset configurable for the escape filter
-
-* 1.0.0-RC2 (2011-02-21)
-
- * changed the way {% set %} works when capturing (the content is now marked as safe)
- * added support for macro name in the endmacro tag
- * make Twig_Error compatible with PHP 5.3.0 >
- * fixed an infinite loop on some Windows configurations
- * fixed the "length" filter for numbers
- * fixed Template::getAttribute() as properties in PHP are case sensitive
- * removed coupling between Twig_Node and Twig_Template
- * fixed the ternary operator precedence rule
-
-* 1.0.0-RC1 (2011-01-09)
-
-Backward incompatibilities:
-
- * the "items" filter, which has been deprecated for quite a long time now, has been removed
- * the "range" filter has been converted to a function: 0|range(10) -> range(0, 10)
- * the "constant" filter has been converted to a function: {{ some_date|date('DATE_W3C'|constant) }} -> {{ some_date|date(constant('DATE_W3C')) }}
- * the "cycle" filter has been converted to a function: {{ ['odd', 'even']|cycle(i) }} -> {{ cycle(['odd', 'even'], i) }}
- * the "for" tag does not support "joined by" anymore
- * the "autoescape" first argument is now "true"/"false" (instead of "on"/"off")
- * the "parent" tag has been replaced by a "parent" function ({{ parent() }} instead of {% parent %})
- * the "display" tag has been replaced by a "block" function ({{ block('title') }} instead of {% display title %})
- * removed the grammar and simple token parser (moved to the Twig Extensions repository)
-
-Changes:
-
- * added "needs_context" option for filters and functions (the context is then passed as a first argument)
- * added global variables support
- * made macros return their value instead of echoing directly (fixes calling a macro in sandbox mode)
- * added the "from" tag to import macros as functions
- * added support for functions (a function is just syntactic sugar for a getAttribute() call)
- * made macros callable when sandbox mode is enabled
- * added an exception when a macro uses a reserved name
- * the "default" filter now uses the "empty" test instead of just checking for null
- * added the "empty" test
-
-* 0.9.10 (2010-12-16)
-
-Backward incompatibilities:
-
- * The Escaper extension is enabled by default, which means that all displayed
-   variables are now automatically escaped. You can revert to the previous
-   behavior by removing the extension via $env->removeExtension('escaper')
-   or just set the 'autoescape' option to 'false'.
- * removed the "without loop" attribute for the "for" tag (not needed anymore
-   as the Optimizer take care of that for most cases)
- * arrays and hashes have now a different syntax
-     * arrays keep the same syntax with square brackets: [1, 2]
-     * hashes now use curly braces (["a": "b"] should now be written as {"a": "b"})
-     * support for "arrays with keys" and "hashes without keys" is not supported anymore ([1, "foo": "bar"] or {"foo": "bar", 1})
- * the i18n extension is now part of the Twig Extensions repository
-
-Changes:
-
- * added the merge filter
- * removed 'is_escaper' option for filters (a left over from the previous version) -- you must use 'is_safe' now instead
- * fixed usage of operators as method names (like is, in, and not)
- * changed the order of execution for node visitors
- * fixed default() filter behavior when used with strict_variables set to on
- * fixed filesystem loader compatibility with PHAR files
- * enhanced error messages when an unexpected token is parsed in an expression
- * fixed filename not being added to syntax error messages
- * added the autoescape option to enable/disable autoescaping
- * removed the newline after a comment (mimicks PHP behavior)
- * added a syntax error exception when parent block is used on a template that does not extend another one
- * made the Escaper extension enabled by default
- * fixed sandbox extension when used with auto output escaping
- * fixed escaper when wrapping a Twig_Node_Print (the original class must be preserved)
- * added an Optimizer extension (enabled by default; optimizes "for" loops and "raw" filters)
- * added priority to node visitors
-
-* 0.9.9 (2010-11-28)
-
-Backward incompatibilities:
- * the self special variable has been renamed to _self
- * the odd and even filters are now tests:
-     {{ foo|odd }} must now be written {{ foo is odd }}
- * the "safe" filter has been renamed to "raw"
- * in Node classes,
-        sub-nodes are now accessed via getNode() (instead of property access)
-        attributes via getAttribute() (instead of array access)
- * the urlencode filter had been renamed to url_encode
- * the include tag now merges the passed variables with the current context by default
-   (the old behavior is still possible by adding the "only" keyword)
- * moved Exceptions to Twig_Error_* (Twig_SyntaxError/Twig_RuntimeError are now Twig_Error_Syntax/Twig_Error_Runtime)
- * removed support for {{ 1 < i < 3 }} (use {{ i > 1 and i < 3 }} instead)
- * the "in" filter has been removed ({{ a|in(b) }} should now be written {{ a in b }})
-
-Changes:
- * added file and line to Twig_Error_Runtime exceptions thrown from Twig_Template
- * changed trans tag to accept any variable for the plural count
- * fixed sandbox mode (__toString() method check was not enforced if called implicitly from complex statements)
- * added the ** (power) operator
- * changed the algorithm used for parsing expressions
- * added the spaceless tag
- * removed trim_blocks option
- * added support for is*() methods for attributes (foo.bar now looks for foo->getBar() or foo->isBar())
- * changed all exceptions to extend Twig_Error
- * fixed unary expressions ({{ not(1 or 0) }})
- * fixed child templates (with an extend tag) that uses one or more imports
- * added support for {{ 1 not in [2, 3] }} (more readable than the current {{ not (1 in [2, 3]) }})
- * escaping has been rewritten
- * the implementation of template inheritance has been rewritten
-   (blocks can now be called individually and still work with inheritance)
- * fixed error handling for if tag when a syntax error occurs within a subparse process
- * added a way to implement custom logic for resolving token parsers given a tag name
- * fixed js escaper to be stricter (now uses a whilelist-based js escaper)
- * added the following filers: "constant", "trans", "replace", "json_encode"
- * added a "constant" test
- * fixed objects with __toString() not being autoescaped
- * fixed subscript expressions when calling __call() (methods now keep the case)
- * added "test" feature (accessible via the "is" operator)
- * removed the debug tag (should be done in an extension)
- * fixed trans tag when no vars are used in plural form
- * fixed race condition when writing template cache
- * added the special _charset variable to reference the current charset
- * added the special _context variable to reference the current context
- * renamed self to _self (to avoid conflict)
- * fixed Twig_Template::getAttribute() for protected properties
-
-* 0.9.8 (2010-06-28)
-
-Backward incompatibilities:
- * the trans tag plural count is now attached to the plural tag:
-    old: `{% trans count %}...{% plural %}...{% endtrans %}`
-    new: `{% trans %}...{% plural count %}...{% endtrans %}`
-
- * added a way to translate strings coming from a variable ({% trans var %})
- * fixed trans tag when used with the Escaper extension
- * fixed default cache umask
- * removed Twig_Template instances from the debug tag output
- * fixed objects with __isset() defined
- * fixed set tag when used with a capture
- * fixed type hinting for Twig_Environment::addFilter() method
-
-* 0.9.7 (2010-06-12)
-
-Backward incompatibilities:
- * changed 'as' to '=' for the set tag ({% set title as "Title" %} must now be {% set title = "Title" %})
- * removed the sandboxed attribute of the include tag (use the new sandbox tag instead)
- * refactored the Node system (if you have custom nodes, you will have to update them to use the new API)
-
- * added self as a special variable that refers to the current template (useful for importing macros from the current template)
- * added Twig_Template instance support to the include tag
- * added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? "minimum" : "base" %})
- * added a grammar sub-framework to ease the creation of custom tags
- * fixed the for tag for large arrays (some loop variables are now only available for arrays and objects that implement the Countable interface)
- * removed the Twig_Resource::resolveMissingFilter() method
- * fixed the filter tag which did not apply filtering to included files
- * added a bunch of unit tests
- * added a bunch of phpdoc
- * added a sandbox tag in the sandbox extension
- * changed the date filter to support any date format supported by DateTime
- * added strict_variable setting to throw an exception when an invalid variable is used in a template (disabled by default)
- * added the lexer, parser, and compiler as arguments to the Twig_Environment constructor
- * changed the cache option to only accepts an explicit path to a cache directory or false
- * added a way to add token parsers, filters, and visitors without creating an extension
- * added three interfaces: Twig_NodeInterface, Twig_TokenParserInterface, and Twig_FilterInterface
- * changed the generated code to match the new coding standards
- * fixed sandbox mode (__toString() method check was not enforced if called implicitly from a simple statement like {{ article }})
- * added an exception when a child template has a non-empty body (as it is always ignored when rendering)
-
-* 0.9.6 (2010-05-12)
-
- * fixed variables defined outside a loop and for which the value changes in a for loop
- * fixed the test suite for PHP 5.2 and older versions of PHPUnit
- * added support for __call() in expression resolution
- * fixed node visiting for macros (macros are now visited by visitors as any other node)
- * fixed nested block definitions with a parent call (rarely useful but nonetheless supported now)
- * added the cycle filter
- * fixed the Lexer when mbstring.func_overload is used with an mbstring.internal_encoding different from ASCII
- * added a long-syntax for the set tag ({% set foo %}...{% endset %})
- * unit tests are now powered by PHPUnit
- * added support for gettext via the `i18n` extension
- * fixed twig_capitalize_string_filter() and fixed twig_length_filter() when used with UTF-8 values
- * added a more useful exception if an if tag is not closed properly
- * added support for escaping strategy in the autoescape tag
- * fixed lexer when a template has a big chunk of text between/in a block
-
-* 0.9.5 (2010-01-20)
-
-As for any new release, don't forget to remove all cached templates after
-upgrading.
-
-If you have defined custom filters, you MUST upgrade them for this release. To
-upgrade, replace "array" with "new Twig_Filter_Function", and replace the
-environment constant by the "needs_environment" option:
-
-  // before
-  'even'   => array('twig_is_even_filter', false),
-  'escape' => array('twig_escape_filter', true),
-
-  // after
-  'even'   => new Twig_Filter_Function('twig_is_even_filter'),
-  'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true)),
-
-If you have created NodeTransformer classes, you will need to upgrade them to
-the new interface (please note that the interface is not yet considered
-stable).
-
- * fixed list nodes that did not extend the Twig_NodeListInterface
- * added the "without loop" option to the for tag (it disables the generation of the loop variable)
- * refactored node transformers to node visitors
- * fixed automatic-escaping for blocks
- * added a way to specify variables to pass to an included template
- * changed the automatic-escaping rules to be more sensible and more configurable in custom filters (the documentation lists all the rules)
- * improved the filter system to allow object methods to be used as filters
- * changed the Array and String loaders to actually make use of the cache mechanism
- * included the default filter function definitions in the extension class files directly (Core, Escaper)
- * added the // operator (like the floor() PHP function)
- * added the .. operator (as a syntactic sugar for the range filter when the step is 1)
- * added the in operator (as a syntactic sugar for the in filter)
- * added the following filters in the Core extension: in, range
- * added support for arrays (same behavior as in PHP, a mix between lists and dictionaries, arrays and hashes)
- * enhanced some error messages to provide better feedback in case of parsing errors
-
-* 0.9.4 (2009-12-02)
-
-If you have custom loaders, you MUST upgrade them for this release: The
-Twig_Loader base class has been removed, and the Twig_LoaderInterface has also
-been changed (see the source code for more information or the documentation).
-
- * added support for DateTime instances for the date filter
- * fixed loop.last when the array only has one item
- * made it possible to insert newlines in tag and variable blocks
- * fixed a bug when a literal '\n' were present in a template text
- * fixed bug when the filename of a template contains */
- * refactored loaders
-
-* 0.9.3 (2009-11-11)
-
-This release is NOT backward compatible with the previous releases.
-
-  The loaders do not take the cache and autoReload arguments anymore. Instead,
-  the Twig_Environment class has two new options: cache and auto_reload.
-  Upgrading your code means changing this kind of code:
-
-      $loader = new Twig_Loader_Filesystem('/path/to/templates', '/path/to/compilation_cache', true);
-      $twig = new Twig_Environment($loader);
-
-  to something like this:
-
-      $loader = new Twig_Loader_Filesystem('/path/to/templates');
-      $twig = new Twig_Environment($loader, array(
-        'cache' => '/path/to/compilation_cache',
-        'auto_reload' => true,
-      ));
-
- * deprecated the "items" filter as it is not needed anymore
- * made cache and auto_reload options of Twig_Environment instead of arguments of Twig_Loader
- * optimized template loading speed
- * removed output when an error occurs in a template and render() is used
- * made major speed improvements for loops (up to 300% on even the smallest loops)
- * added properties as part of the sandbox mode
- * added public properties support (obj.item can now be the item property on the obj object)
- * extended set tag to support expression as value ({% set foo as 'foo' ~ 'bar' %} )
- * fixed bug when \ was used in HTML
-
-* 0.9.2 (2009-10-29)
-
- * made some speed optimizations
- * changed the cache extension to .php
- * added a js escaping strategy
- * added support for short block tag
- * changed the filter tag to allow chained filters
- * made lexer more flexible as you can now change the default delimiters
- * added set tag
- * changed default directory permission when cache dir does not exist (more secure)
- * added macro support
- * changed filters first optional argument to be a Twig_Environment instance instead of a Twig_Template instance
- * made Twig_Autoloader::autoload() a static method
- * avoid writing template file if an error occurs
- * added $ escaping when outputting raw strings
- * enhanced some error messages to ease debugging
- * fixed empty cache files when the template contains an error
-
-* 0.9.1 (2009-10-14)
-
-  * fixed a bug in PHP 5.2.6
-  * fixed numbers with one than one decimal
-  * added support for method calls with arguments ({{ foo.bar('a', 43) }})
-  * made small speed optimizations
-  * made minor tweaks to allow better extensibility and flexibility
-
-* 0.9.0 (2009-10-12)
-
- * Initial release

+ 37 - 12
lib/twig/lib/Twig/Compiler.php → lib/twig/Compiler.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Compiles a node to PHP code.
  * Compiles a node to PHP code.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Compiler implements Twig_CompilerInterface
 class Twig_Compiler implements Twig_CompilerInterface
 {
 {
@@ -25,6 +24,7 @@ class Twig_Compiler implements Twig_CompilerInterface
     protected $debugInfo;
     protected $debugInfo;
     protected $sourceOffset;
     protected $sourceOffset;
     protected $sourceLine;
     protected $sourceLine;
+    protected $filename;
 
 
     /**
     /**
      * Constructor.
      * Constructor.
@@ -37,6 +37,11 @@ class Twig_Compiler implements Twig_CompilerInterface
         $this->debugInfo = array();
         $this->debugInfo = array();
     }
     }
 
 
+    public function getFilename()
+    {
+        return $this->filename;
+    }
+
     /**
     /**
      * Returns the environment instance related to this compiler.
      * Returns the environment instance related to this compiler.
      *
      *
@@ -70,9 +75,14 @@ class Twig_Compiler implements Twig_CompilerInterface
         $this->lastLine = null;
         $this->lastLine = null;
         $this->source = '';
         $this->source = '';
         $this->sourceOffset = 0;
         $this->sourceOffset = 0;
-        $this->sourceLine = 0;
+        // source code starts at 1 (as we then increment it when we encounter new lines)
+        $this->sourceLine = 1;
         $this->indentation = $indentation;
         $this->indentation = $indentation;
 
 
+        if ($node instanceof Twig_Node_Module) {
+            $this->filename = $node->getAttribute('filename');
+        }
+
         $node->compile($this);
         $node->compile($this);
 
 
         return $this;
         return $this;
@@ -92,7 +102,7 @@ class Twig_Compiler implements Twig_CompilerInterface
     /**
     /**
      * Adds a raw string to the compiled code.
      * Adds a raw string to the compiled code.
      *
      *
-     * @param  string $string The string
+     * @param string $string The string
      *
      *
      * @return Twig_Compiler The current compiler instance
      * @return Twig_Compiler The current compiler instance
      */
      */
@@ -119,6 +129,11 @@ class Twig_Compiler implements Twig_CompilerInterface
         return $this;
         return $this;
     }
     }
 
 
+    /**
+     * Appends an indentation to the current PHP code after compilation.
+     *
+     * @return Twig_Compiler The current compiler instance
+     */
     public function addIndentation()
     public function addIndentation()
     {
     {
         $this->source .= str_repeat(' ', $this->indentation * 4);
         $this->source .= str_repeat(' ', $this->indentation * 4);
@@ -129,7 +144,7 @@ class Twig_Compiler implements Twig_CompilerInterface
     /**
     /**
      * Adds a quoted string to the compiled code.
      * Adds a quoted string to the compiled code.
      *
      *
-     * @param  string $value The string
+     * @param string $value The string
      *
      *
      * @return Twig_Compiler The current compiler instance
      * @return Twig_Compiler The current compiler instance
      */
      */
@@ -143,7 +158,7 @@ class Twig_Compiler implements Twig_CompilerInterface
     /**
     /**
      * Returns a PHP representation of a given value.
      * Returns a PHP representation of a given value.
      *
      *
-     * @param  mixed $value The value to convert
+     * @param mixed $value The value to convert
      *
      *
      * @return Twig_Compiler The current compiler instance
      * @return Twig_Compiler The current compiler instance
      */
      */
@@ -192,12 +207,21 @@ class Twig_Compiler implements Twig_CompilerInterface
     public function addDebugInfo(Twig_NodeInterface $node)
     public function addDebugInfo(Twig_NodeInterface $node)
     {
     {
         if ($node->getLine() != $this->lastLine) {
         if ($node->getLine() != $this->lastLine) {
-            $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
+            $this->write("// line {$node->getLine()}\n");
+
+            // when mbstring.func_overload is set to 2
+            // mb_substr_count() replaces substr_count()
+            // but they have different signatures!
+            if (((int) ini_get('mbstring.func_overload')) & 2) {
+                // this is much slower than the "right" version
+                $this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "\n");
+            } else {
+                $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
+            }
             $this->sourceOffset = strlen($this->source);
             $this->sourceOffset = strlen($this->source);
             $this->debugInfo[$this->sourceLine] = $node->getLine();
             $this->debugInfo[$this->sourceLine] = $node->getLine();
 
 
             $this->lastLine = $node->getLine();
             $this->lastLine = $node->getLine();
-            $this->write("// line {$node->getLine()}\n");
         }
         }
 
 
         return $this;
         return $this;
@@ -231,12 +255,13 @@ class Twig_Compiler implements Twig_CompilerInterface
      */
      */
     public function outdent($step = 1)
     public function outdent($step = 1)
     {
     {
-        $this->indentation -= $step;
-
-        if ($this->indentation < 0) {
-            throw new Twig_Error('Unable to call outdent() as the indentation would become negative');
+        // can't outdent by more steps than the current indentation level
+        if ($this->indentation < $step) {
+            throw new LogicException('Unable to call outdent() as the indentation would become negative');
         }
         }
 
 
+        $this->indentation -= $step;
+
         return $this;
         return $this;
     }
     }
 }
 }

+ 5 - 5
lib/twig/lib/Twig/CompilerInterface.php → lib/twig/CompilerInterface.php

@@ -12,24 +12,24 @@
 /**
 /**
  * Interface implemented by compiler classes.
  * Interface implemented by compiler classes.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 interface Twig_CompilerInterface
 interface Twig_CompilerInterface
 {
 {
     /**
     /**
      * Compiles a node.
      * Compiles a node.
      *
      *
-     * @param  Twig_NodeInterface $node The node to compile
+     * @param Twig_NodeInterface $node The node to compile
      *
      *
      * @return Twig_CompilerInterface The current compiler instance
      * @return Twig_CompilerInterface The current compiler instance
      */
      */
-    function compile(Twig_NodeInterface $node);
+    public function compile(Twig_NodeInterface $node);
 
 
     /**
     /**
      * Gets the current PHP code after compilation.
      * Gets the current PHP code after compilation.
      *
      *
      * @return string The PHP code
      * @return string The PHP code
      */
      */
-    function getSource();
+    public function getSource();
 }
 }

+ 278 - 121
lib/twig/lib/Twig/Environment.php → lib/twig/Environment.php

@@ -12,12 +12,11 @@
 /**
 /**
  * Stores the Twig configuration.
  * Stores the Twig configuration.
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Environment
 class Twig_Environment
 {
 {
-    const VERSION = '1.6.4';
+    const VERSION = '1.12.3-DEV';
 
 
     protected $charset;
     protected $charset;
     protected $loader;
     protected $loader;
@@ -36,6 +35,7 @@ class Twig_Environment
     protected $functions;
     protected $functions;
     protected $globals;
     protected $globals;
     protected $runtimeInitialized;
     protected $runtimeInitialized;
+    protected $extensionInitialized;
     protected $loadedTemplates;
     protected $loadedTemplates;
     protected $strictVariables;
     protected $strictVariables;
     protected $unaryOperators;
     protected $unaryOperators;
@@ -50,9 +50,8 @@ class Twig_Environment
      *
      *
      * Available options:
      * Available options:
      *
      *
-     *  * debug: When set to `true`, the generated templates have a __toString()
-     *           method that you can use to display the generated nodes (default to
-     *           false).
+     *  * debug: When set to true, it automatically set "auto_reload" to true as
+     *           well (default to false).
      *
      *
      *  * charset: The charset used by the templates (default to utf-8).
      *  * charset: The charset used by the templates (default to utf-8).
      *
      *
@@ -60,7 +59,7 @@ class Twig_Environment
      *                         templates (default to Twig_Template).
      *                         templates (default to Twig_Template).
      *
      *
      *  * cache: An absolute path where to store the compiled templates, or
      *  * cache: An absolute path where to store the compiled templates, or
-     *           false to disable compilation cache (default)
+     *           false to disable compilation cache (default).
      *
      *
      *  * auto_reload: Whether to reload the template is the original source changed.
      *  * auto_reload: Whether to reload the template is the original source changed.
      *                 If you don't provide the auto_reload option, it will be
      *                 If you don't provide the auto_reload option, it will be
@@ -69,14 +68,18 @@ class Twig_Environment
      *  * strict_variables: Whether to ignore invalid variables in templates
      *  * strict_variables: Whether to ignore invalid variables in templates
      *                      (default to false).
      *                      (default to false).
      *
      *
-     *  * autoescape: Whether to enable auto-escaping (default to true);
+     *  * autoescape: Whether to enable auto-escaping (default to html):
+     *                  * false: disable auto-escaping
+     *                  * true: equivalent to html
+     *                  * html, js: set the autoescaping to one of the supported strategies
+     *                  * PHP callback: a PHP callback that returns an escaping strategy based on the template "filename"
      *
      *
      *  * optimizations: A flag that indicates which optimizations to apply
      *  * optimizations: A flag that indicates which optimizations to apply
      *                   (default to -1 which means that all optimizations are enabled;
      *                   (default to -1 which means that all optimizations are enabled;
-     *                   set it to 0 to disable)
+     *                   set it to 0 to disable).
      *
      *
-     * @param Twig_LoaderInterface   $loader  A Twig_LoaderInterface instance
-     * @param array                  $options An array of options
+     * @param Twig_LoaderInterface $loader  A Twig_LoaderInterface instance
+     * @param array                $options An array of options
      */
      */
     public function __construct(Twig_LoaderInterface $loader = null, $options = array())
     public function __construct(Twig_LoaderInterface $loader = null, $options = array())
     {
     {
@@ -89,7 +92,7 @@ class Twig_Environment
             'charset'             => 'UTF-8',
             'charset'             => 'UTF-8',
             'base_template_class' => 'Twig_Template',
             'base_template_class' => 'Twig_Template',
             'strict_variables'    => false,
             'strict_variables'    => false,
-            'autoescape'          => true,
+            'autoescape'          => 'html',
             'cache'               => false,
             'cache'               => false,
             'auto_reload'         => null,
             'auto_reload'         => null,
             'optimizations'       => -1,
             'optimizations'       => -1,
@@ -99,16 +102,17 @@ class Twig_Environment
         $this->charset            = $options['charset'];
         $this->charset            = $options['charset'];
         $this->baseTemplateClass  = $options['base_template_class'];
         $this->baseTemplateClass  = $options['base_template_class'];
         $this->autoReload         = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
         $this->autoReload         = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
-        $this->extensions         = array(
-            'core'      => new Twig_Extension_Core(),
-            'escaper'   => new Twig_Extension_Escaper((bool) $options['autoescape']),
-            'optimizer' => new Twig_Extension_Optimizer($options['optimizations']),
-        );
         $this->strictVariables    = (bool) $options['strict_variables'];
         $this->strictVariables    = (bool) $options['strict_variables'];
         $this->runtimeInitialized = false;
         $this->runtimeInitialized = false;
         $this->setCache($options['cache']);
         $this->setCache($options['cache']);
         $this->functionCallbacks = array();
         $this->functionCallbacks = array();
         $this->filterCallbacks = array();
         $this->filterCallbacks = array();
+
+        $this->addExtension(new Twig_Extension_Core());
+        $this->addExtension(new Twig_Extension_Escaper($options['autoescape']));
+        $this->addExtension(new Twig_Extension_Optimizer($options['optimizations']));
+        $this->extensionInitialized = false;
+        $this->staging = new Twig_Extension_Staging();
     }
     }
 
 
     /**
     /**
@@ -251,13 +255,14 @@ class Twig_Environment
     /**
     /**
      * Gets the template class associated with the given string.
      * Gets the template class associated with the given string.
      *
      *
-     * @param string $name The name for which to calculate the template class name
+     * @param string  $name  The name for which to calculate the template class name
+     * @param integer $index The index if it is an embedded template
      *
      *
      * @return string The template class name
      * @return string The template class name
      */
      */
-    public function getTemplateClass($name)
+    public function getTemplateClass($name, $index = null)
     {
     {
-        return $this->templateClassPrefix.md5($this->loader->getCacheKey($name));
+        return $this->templateClassPrefix.md5($this->getLoader()->getCacheKey($name)).(null === $index ? '' : '_'.$index);
     }
     }
 
 
     /**
     /**
@@ -297,13 +302,14 @@ class Twig_Environment
     /**
     /**
      * Loads a template by name.
      * Loads a template by name.
      *
      *
-     * @param  string  $name  The template name
+     * @param string  $name  The template name
+     * @param integer $index The index if it is an embedded template
      *
      *
      * @return Twig_TemplateInterface A template instance representing the given template name
      * @return Twig_TemplateInterface A template instance representing the given template name
      */
      */
-    public function loadTemplate($name)
+    public function loadTemplate($name, $index = null)
     {
     {
-        $cls = $this->getTemplateClass($name);
+        $cls = $this->getTemplateClass($name, $index);
 
 
         if (isset($this->loadedTemplates[$cls])) {
         if (isset($this->loadedTemplates[$cls])) {
             return $this->loadedTemplates[$cls];
             return $this->loadedTemplates[$cls];
@@ -311,10 +317,10 @@ class Twig_Environment
 
 
         if (!class_exists($cls, false)) {
         if (!class_exists($cls, false)) {
             if (false === $cache = $this->getCacheFilename($name)) {
             if (false === $cache = $this->getCacheFilename($name)) {
-                eval('?>'.$this->compileSource($this->loader->getSource($name), $name));
+                eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name));
             } else {
             } else {
                 if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
                 if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
-                    $this->writeCacheFile($cache, $this->compileSource($this->loader->getSource($name), $name));
+                    $this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name));
                 }
                 }
 
 
                 require_once $cache;
                 require_once $cache;
@@ -349,7 +355,7 @@ class Twig_Environment
             }
             }
         }
         }
 
 
-        return $this->loader->isFresh($name, $time);
+        return $this->getLoader()->isFresh($name, $time);
     }
     }
 
 
     public function resolveTemplate($names)
     public function resolveTemplate($names)
@@ -546,6 +552,10 @@ class Twig_Environment
      */
      */
     public function getLoader()
     public function getLoader()
     {
     {
+        if (null === $this->loader) {
+            throw new LogicException('You must set a loader first.');
+        }
+
         return $this->loader;
         return $this->loader;
     }
     }
 
 
@@ -616,29 +626,29 @@ class Twig_Environment
      */
      */
     public function addExtension(Twig_ExtensionInterface $extension)
     public function addExtension(Twig_ExtensionInterface $extension)
     {
     {
+        if ($this->extensionInitialized) {
+            throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
+        }
+
         $this->extensions[$extension->getName()] = $extension;
         $this->extensions[$extension->getName()] = $extension;
-        $this->parsers = null;
-        $this->visitors = null;
-        $this->filters = null;
-        $this->tests = null;
-        $this->functions = null;
-        $this->globals = null;
     }
     }
 
 
     /**
     /**
      * Removes an extension by name.
      * Removes an extension by name.
      *
      *
+     * This method is deprecated and you should not use it.
+     *
      * @param string $name The extension name
      * @param string $name The extension name
+     *
+     * @deprecated since 1.12 (to be removed in 2.0)
      */
      */
     public function removeExtension($name)
     public function removeExtension($name)
     {
     {
+        if ($this->extensionInitialized) {
+            throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
+        }
+
         unset($this->extensions[$name]);
         unset($this->extensions[$name]);
-        $this->parsers = null;
-        $this->visitors = null;
-        $this->filters = null;
-        $this->tests = null;
-        $this->functions = null;
-        $this->globals = null;
     }
     }
 
 
     /**
     /**
@@ -670,8 +680,11 @@ class Twig_Environment
      */
      */
     public function addTokenParser(Twig_TokenParserInterface $parser)
     public function addTokenParser(Twig_TokenParserInterface $parser)
     {
     {
-        $this->staging['token_parsers'][] = $parser;
-        $this->parsers = null;
+        if ($this->extensionInitialized) {
+            throw new LogicException('Unable to add a token parser as extensions have already been initialized.');
+        }
+
+        $this->staging->addTokenParser($parser);
     }
     }
 
 
     /**
     /**
@@ -681,27 +694,8 @@ class Twig_Environment
      */
      */
     public function getTokenParsers()
     public function getTokenParsers()
     {
     {
-        if (null === $this->parsers) {
-            $this->parsers = new Twig_TokenParserBroker();
-
-            if (isset($this->staging['token_parsers'])) {
-                foreach ($this->staging['token_parsers'] as $parser) {
-                    $this->parsers->addTokenParser($parser);
-                }
-            }
-
-            foreach ($this->getExtensions() as $extension) {
-                $parsers = $extension->getTokenParsers();
-                foreach($parsers as $parser) {
-                    if ($parser instanceof Twig_TokenParserInterface) {
-                        $this->parsers->addTokenParser($parser);
-                    } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
-                        $this->parsers->addTokenParserBroker($parser);
-                    } else {
-                        throw new Twig_Error_Runtime('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances');
-                    }
-                }
-            }
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->parsers;
         return $this->parsers;
@@ -733,8 +727,11 @@ class Twig_Environment
      */
      */
     public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
     public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
     {
     {
-        $this->staging['visitors'][] = $visitor;
-        $this->visitors = null;
+        if ($this->extensionInitialized) {
+            throw new LogicException('Unable to add a node visitor as extensions have already been initialized.', $extension->getName());
+        }
+
+        $this->staging->addNodeVisitor($visitor);
     }
     }
 
 
     /**
     /**
@@ -744,11 +741,8 @@ class Twig_Environment
      */
      */
     public function getNodeVisitors()
     public function getNodeVisitors()
     {
     {
-        if (null === $this->visitors) {
-            $this->visitors = isset($this->staging['visitors']) ? $this->staging['visitors'] : array();
-            foreach ($this->getExtensions() as $extension) {
-                $this->visitors = array_merge($this->visitors, $extension->getNodeVisitors());
-            }
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->visitors;
         return $this->visitors;
@@ -757,13 +751,25 @@ class Twig_Environment
     /**
     /**
      * Registers a Filter.
      * Registers a Filter.
      *
      *
-     * @param string               $name   The filter name
-     * @param Twig_FilterInterface $filter A Twig_FilterInterface instance
+     * @param string|Twig_SimpleFilter               $name   The filter name or a Twig_SimpleFilter instance
+     * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance
      */
      */
-    public function addFilter($name, Twig_FilterInterface $filter)
+    public function addFilter($name, $filter = null)
     {
     {
-        $this->staging['filters'][$name] = $filter;
-        $this->filters = null;
+        if ($this->extensionInitialized) {
+            throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
+        }
+
+        if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
+            throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter');
+        }
+
+        if ($name instanceof Twig_SimpleFilter) {
+            $filter = $name;
+            $name = $filter->getName();
+        }
+
+        $this->staging->addFilter($name, $filter);
     }
     }
 
 
     /**
     /**
@@ -774,12 +780,12 @@ class Twig_Environment
      *
      *
      * @param string $name The filter name
      * @param string $name The filter name
      *
      *
-     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exists
+     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist
      */
      */
     public function getFilter($name)
     public function getFilter($name)
     {
     {
-        if (null === $this->filters) {
-            $this->getFilters();
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         if (isset($this->filters[$name])) {
         if (isset($this->filters[$name])) {
@@ -824,11 +830,8 @@ class Twig_Environment
      */
      */
     public function getFilters()
     public function getFilters()
     {
     {
-        if (null === $this->filters) {
-            $this->filters = isset($this->staging['filters']) ? $this->staging['filters'] : array();
-            foreach ($this->getExtensions() as $extension) {
-                $this->filters = array_merge($this->filters, $extension->getFilters());
-            }
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->filters;
         return $this->filters;
@@ -837,13 +840,25 @@ class Twig_Environment
     /**
     /**
      * Registers a Test.
      * Registers a Test.
      *
      *
-     * @param string             $name The test name
-     * @param Twig_TestInterface $test A Twig_TestInterface instance
+     * @param string|Twig_SimpleTest             $name The test name or a Twig_SimpleTest instance
+     * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
      */
      */
-    public function addTest($name, Twig_TestInterface $test)
+    public function addTest($name, $test = null)
     {
     {
-        $this->staging['tests'][$name] = $test;
-        $this->tests = null;
+        if ($this->extensionInitialized) {
+            throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
+        }
+
+        if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
+            throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest');
+        }
+
+        if ($name instanceof Twig_SimpleTest) {
+            $test = $name;
+            $name = $test->getName();
+        }
+
+        $this->staging->addTest($name, $test);
     }
     }
 
 
     /**
     /**
@@ -853,26 +868,55 @@ class Twig_Environment
      */
      */
     public function getTests()
     public function getTests()
     {
     {
-        if (null === $this->tests) {
-            $this->tests = isset($this->staging['tests']) ? $this->staging['tests'] : array();
-            foreach ($this->getExtensions() as $extension) {
-                $this->tests = array_merge($this->tests, $extension->getTests());
-            }
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->tests;
         return $this->tests;
     }
     }
 
 
+    /**
+     * Gets a test by name.
+     *
+     * @param string $name The test name
+     *
+     * @return Twig_Test|false A Twig_Test instance or false if the test does not exist
+     */
+    public function getTest($name)
+    {
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
+        }
+
+        if (isset($this->tests[$name])) {
+            return $this->tests[$name];
+        }
+
+        return false;
+    }
+
     /**
     /**
      * Registers a Function.
      * Registers a Function.
      *
      *
-     * @param string                 $name     The function name
-     * @param Twig_FunctionInterface $function A Twig_FunctionInterface instance
+     * @param string|Twig_SimpleFunction                 $name     The function name or a Twig_SimpleFunction instance
+     * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance
      */
      */
-    public function addFunction($name, Twig_FunctionInterface $function)
+    public function addFunction($name, $function = null)
     {
     {
-        $this->staging['functions'][$name] = $function;
-        $this->functions = null;
+        if ($this->extensionInitialized) {
+            throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
+        }
+
+        if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
+            throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction');
+        }
+
+        if ($name instanceof Twig_SimpleFunction) {
+            $function = $name;
+            $name = $function->getName();
+        }
+
+        $this->staging->addFunction($name, $function);
     }
     }
 
 
     /**
     /**
@@ -883,12 +927,12 @@ class Twig_Environment
      *
      *
      * @param string $name function name
      * @param string $name function name
      *
      *
-     * @return Twig_Function|false A Twig_Function instance or false if the function does not exists
+     * @return Twig_Function|false A Twig_Function instance or false if the function does not exist
      */
      */
     public function getFunction($name)
     public function getFunction($name)
     {
     {
-        if (null === $this->functions) {
-            $this->getFunctions();
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         if (isset($this->functions[$name])) {
         if (isset($this->functions[$name])) {
@@ -933,11 +977,8 @@ class Twig_Environment
      */
      */
     public function getFunctions()
     public function getFunctions()
     {
     {
-        if (null === $this->functions) {
-            $this->functions = isset($this->staging['functions']) ? $this->staging['functions'] : array();
-            foreach ($this->getExtensions() as $extension) {
-                $this->functions = array_merge($this->functions, $extension->getFunctions());
-            }
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->functions;
         return $this->functions;
@@ -946,13 +987,32 @@ class Twig_Environment
     /**
     /**
      * Registers a Global.
      * Registers a Global.
      *
      *
+     * New globals can be added before compiling or rendering a template;
+     * but after, you can only update existing globals.
+     *
      * @param string $name  The global name
      * @param string $name  The global name
      * @param mixed  $value The global value
      * @param mixed  $value The global value
      */
      */
     public function addGlobal($name, $value)
     public function addGlobal($name, $value)
     {
     {
-        $this->staging['globals'][$name] = $value;
-        $this->globals = null;
+        if ($this->extensionInitialized || $this->runtimeInitialized) {
+            if (null === $this->globals) {
+                $this->globals = $this->initGlobals();
+            }
+
+            /* This condition must be uncommented in Twig 2.0
+            if (!array_key_exists($name, $this->globals)) {
+                throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
+            }
+            */
+        }
+
+        if ($this->extensionInitialized || $this->runtimeInitialized) {
+            // update the value
+            $this->globals[$name] = $value;
+        } else {
+            $this->staging->addGlobal($name, $value);
+        }
     }
     }
 
 
     /**
     /**
@@ -962,16 +1022,37 @@ class Twig_Environment
      */
      */
     public function getGlobals()
     public function getGlobals()
     {
     {
+        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
+            return $this->initGlobals();
+        }
+
         if (null === $this->globals) {
         if (null === $this->globals) {
-            $this->globals = isset($this->staging['globals']) ? $this->staging['globals'] : array();
-            foreach ($this->getExtensions() as $extension) {
-                $this->globals = array_merge($this->globals, $extension->getGlobals());
-            }
+            $this->globals = $this->initGlobals();
         }
         }
 
 
         return $this->globals;
         return $this->globals;
     }
     }
 
 
+    /**
+     * Merges a context with the defined globals.
+     *
+     * @param array $context An array representing the context
+     *
+     * @return array The context merged with the globals
+     */
+    public function mergeGlobals(array $context)
+    {
+        // we don't use array_merge as the context being generally
+        // bigger than globals, this code is faster.
+        foreach ($this->getGlobals() as $key => $value) {
+            if (!array_key_exists($key, $context)) {
+                $context[$key] = $value;
+            }
+        }
+
+        return $context;
+    }
+
     /**
     /**
      * Gets the registered unary Operators.
      * Gets the registered unary Operators.
      *
      *
@@ -979,8 +1060,8 @@ class Twig_Environment
      */
      */
     public function getUnaryOperators()
     public function getUnaryOperators()
     {
     {
-        if (null === $this->unaryOperators) {
-            $this->initOperators();
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->unaryOperators;
         return $this->unaryOperators;
@@ -993,8 +1074,8 @@ class Twig_Environment
      */
      */
     public function getBinaryOperators()
     public function getBinaryOperators()
     {
     {
-        if (null === $this->binaryOperators) {
-            $this->initOperators();
+        if (!$this->extensionInitialized) {
+            $this->initExtensions();
         }
         }
 
 
         return $this->binaryOperators;
         return $this->binaryOperators;
@@ -1014,17 +1095,93 @@ class Twig_Environment
         return array_keys($alternatives);
         return array_keys($alternatives);
     }
     }
 
 
-    protected function initOperators()
+    protected function initGlobals()
+    {
+        $globals = array();
+        foreach ($this->extensions as $extension) {
+            $globals = array_merge($globals, $extension->getGlobals());
+        }
+
+        return array_merge($globals, $this->staging->getGlobals());
+    }
+
+    protected function initExtensions()
     {
     {
+        if ($this->extensionInitialized) {
+            return;
+        }
+
+        $this->extensionInitialized = true;
+        $this->parsers = new Twig_TokenParserBroker();
+        $this->filters = array();
+        $this->functions = array();
+        $this->tests = array();
+        $this->visitors = array();
         $this->unaryOperators = array();
         $this->unaryOperators = array();
         $this->binaryOperators = array();
         $this->binaryOperators = array();
-        foreach ($this->getExtensions() as $extension) {
-            $operators = $extension->getOperators();
 
 
-            if (!$operators) {
-                continue;
+        foreach ($this->extensions as $extension) {
+            $this->initExtension($extension);
+        }
+        $this->initExtension($this->staging);
+    }
+
+    protected function initExtension(Twig_ExtensionInterface $extension)
+    {
+        // filters
+        foreach ($extension->getFilters() as $name => $filter) {
+            if ($name instanceof Twig_SimpleFilter) {
+                $filter = $name;
+                $name = $filter->getName();
+            } elseif ($filter instanceof Twig_SimpleFilter) {
+                $name = $filter->getName();
+            }
+
+            $this->filters[$name] = $filter;
+        }
+
+        // functions
+        foreach ($extension->getFunctions() as $name => $function) {
+            if ($name instanceof Twig_SimpleFunction) {
+                $function = $name;
+                $name = $function->getName();
+            } elseif ($function instanceof Twig_SimpleFunction) {
+                $name = $function->getName();
             }
             }
 
 
+            $this->functions[$name] = $function;
+        }
+
+        // tests
+        foreach ($extension->getTests() as $name => $test) {
+            if ($name instanceof Twig_SimpleTest) {
+                $test = $name;
+                $name = $test->getName();
+            } elseif ($test instanceof Twig_SimpleTest) {
+                $name = $test->getName();
+            }
+
+            $this->tests[$name] = $test;
+        }
+
+        // token parsers
+        foreach ($extension->getTokenParsers() as $parser) {
+            if ($parser instanceof Twig_TokenParserInterface) {
+                $this->parsers->addTokenParser($parser);
+            } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
+                $this->parsers->addTokenParserBroker($parser);
+            } else {
+                throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances');
+            }
+        }
+
+        // node visitors
+        foreach ($extension->getNodeVisitors() as $visitor) {
+            $this->visitors[] = $visitor;
+        }
+
+        // operators
+        if ($operators = $extension->getOperators()) {
             if (2 !== count($operators)) {
             if (2 !== count($operators)) {
                 throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension)));
                 throw new InvalidArgumentException(sprintf('"%s::getOperators()" does not return a valid operators array.', get_class($extension)));
             }
             }
@@ -1049,12 +1206,12 @@ class Twig_Environment
         if (false !== @file_put_contents($tmpFile, $content)) {
         if (false !== @file_put_contents($tmpFile, $content)) {
             // rename does not work on Win32 before 5.2.6
             // rename does not work on Win32 before 5.2.6
             if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) {
             if (@rename($tmpFile, $file) || (@copy($tmpFile, $file) && unlink($tmpFile))) {
-                chmod($file, 0644);
+                @chmod($file, 0666 & ~umask());
 
 
                 return;
                 return;
             }
             }
         }
         }
 
 
-        throw new Twig_Error_Runtime(sprintf('Failed to write cache file "%s".', $file));
+        throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file));
     }
     }
 }
 }

+ 232 - 0
lib/twig/Error.php

@@ -0,0 +1,232 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Twig base exception.
+ *
+ * This exception class and its children must only be used when
+ * an error occurs during the loading of a template, when a syntax error
+ * is detected in a template, or when rendering a template. Other
+ * errors must use regular PHP exception classes (like when the template
+ * cache directory is not writable for instance).
+ *
+ * To help debugging template issues, this class tracks the original template
+ * name and line where the error occurred.
+ *
+ * Whenever possible, you must set these information (original template name
+ * and line number) yourself by passing them to the constructor. If some or all
+ * these information are not available from where you throw the exception, then
+ * this class will guess them automatically (when the line number is set to -1
+ * and/or the filename is set to null). As this is a costly operation, this
+ * can be disabled by passing false for both the filename and the line number
+ * when creating a new instance of this class.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Error extends Exception
+{
+    protected $lineno;
+    protected $filename;
+    protected $rawMessage;
+    protected $previous;
+
+    /**
+     * Constructor.
+     *
+     * Set both the line number and the filename to false to
+     * disable automatic guessing of the original template name
+     * and line number.
+     *
+     * Set the line number to -1 to enable its automatic guessing.
+     * Set the filename to null to enable its automatic guessing.
+     *
+     * By default, automatic guessing is enabled.
+     *
+     * @param string    $message  The error message
+     * @param integer   $lineno   The template line where the error occurred
+     * @param string    $filename The template file name where the error occurred
+     * @param Exception $previous The previous exception
+     */
+    public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null)
+    {
+        if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+            $this->previous = $previous;
+            parent::__construct('');
+        } else {
+            parent::__construct('', 0, $previous);
+        }
+
+        $this->lineno = $lineno;
+        $this->filename = $filename;
+
+        if (-1 === $this->lineno || null === $this->filename) {
+            $this->guessTemplateInfo();
+        }
+
+        $this->rawMessage = $message;
+
+        $this->updateRepr();
+    }
+
+    /**
+     * Gets the raw message.
+     *
+     * @return string The raw message
+     */
+    public function getRawMessage()
+    {
+        return $this->rawMessage;
+    }
+
+    /**
+     * Gets the filename where the error occurred.
+     *
+     * @return string The filename
+     */
+    public function getTemplateFile()
+    {
+        return $this->filename;
+    }
+
+    /**
+     * Sets the filename where the error occurred.
+     *
+     * @param string $filename The filename
+     */
+    public function setTemplateFile($filename)
+    {
+        $this->filename = $filename;
+
+        $this->updateRepr();
+    }
+
+    /**
+     * Gets the template line where the error occurred.
+     *
+     * @return integer The template line
+     */
+    public function getTemplateLine()
+    {
+        return $this->lineno;
+    }
+
+    /**
+     * Sets the template line where the error occurred.
+     *
+     * @param integer $lineno The template line
+     */
+    public function setTemplateLine($lineno)
+    {
+        $this->lineno = $lineno;
+
+        $this->updateRepr();
+    }
+
+    public function guess()
+    {
+        $this->guessTemplateInfo();
+        $this->updateRepr();
+    }
+
+    /**
+     * For PHP < 5.3.0, provides access to the getPrevious() method.
+     *
+     * @param string $method    The method name
+     * @param array  $arguments The parameters to be passed to the method
+     *
+     * @return Exception The previous exception or null
+     *
+     * @throws BadMethodCallException
+     */
+    public function __call($method, $arguments)
+    {
+        if ('getprevious' == strtolower($method)) {
+            return $this->previous;
+        }
+
+        throw new BadMethodCallException(sprintf('Method "Twig_Error::%s()" does not exist.', $method));
+    }
+
+    protected function updateRepr()
+    {
+        $this->message = $this->rawMessage;
+
+        $dot = false;
+        if ('.' === substr($this->message, -1)) {
+            $this->message = substr($this->message, 0, -1);
+            $dot = true;
+        }
+
+        if ($this->filename) {
+            if (is_string($this->filename) || (is_object($this->filename) && method_exists($this->filename, '__toString'))) {
+                $filename = sprintf('"%s"', $this->filename);
+            } else {
+                $filename = json_encode($this->filename);
+            }
+            $this->message .= sprintf(' in %s', $filename);
+        }
+
+        if ($this->lineno && $this->lineno >= 0) {
+            $this->message .= sprintf(' at line %d', $this->lineno);
+        }
+
+        if ($dot) {
+            $this->message .= '.';
+        }
+    }
+
+    protected function guessTemplateInfo()
+    {
+        $template = null;
+        foreach (debug_backtrace() as $trace) {
+            if (isset($trace['object']) && $trace['object'] instanceof Twig_Template && 'Twig_Template' !== get_class($trace['object'])) {
+                if (null === $this->filename || $this->filename == $trace['object']->getTemplateName()) {
+                    $template = $trace['object'];
+                }
+            }
+        }
+
+        // update template filename
+        if (null !== $template && null === $this->filename) {
+            $this->filename = $template->getTemplateName();
+        }
+
+        if (null === $template || $this->lineno > -1) {
+            return;
+        }
+
+        $r = new ReflectionObject($template);
+        $file = $r->getFileName();
+
+        $exceptions = array($e = $this);
+        while (($e instanceof self || method_exists($e, 'getPrevious')) && $e = $e->getPrevious()) {
+            $exceptions[] = $e;
+        }
+
+        while ($e = array_pop($exceptions)) {
+            $traces = $e->getTrace();
+            while ($trace = array_shift($traces)) {
+                if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) {
+                    continue;
+                }
+
+                foreach ($template->getDebugInfo() as $codeLine => $templateLine) {
+                    if ($codeLine <= $trace['line']) {
+                        // update template line
+                        $this->lineno = $templateLine;
+
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}

+ 31 - 0
lib/twig/Error/Loader.php

@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Exception thrown when an error occurs during template loading.
+ *
+ * Automatic template information guessing is always turned off as
+ * if a template cannot be loaded, there is nothing to guess.
+ * However, when a template is loaded from another one, then, we need
+ * to find the current context and this is automatically done by
+ * Twig_Template::displayWithErrorHandling().
+ *
+ * This strategy makes Twig_Environment::resolveTemplate() much faster.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Error_Loader extends Twig_Error
+{
+    public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null)
+    {
+        parent::__construct($message, false, false, $previous);
+    }
+}

+ 1 - 2
lib/twig/lib/Twig/Error/Runtime.php → lib/twig/Error/Runtime.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Exception thrown when an error occurs at runtime.
  * Exception thrown when an error occurs at runtime.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Error_Runtime extends Twig_Error
 class Twig_Error_Runtime extends Twig_Error
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Error/Syntax.php → lib/twig/Error/Syntax.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Exception thrown when a syntax error occurs during lexing or parsing of a template.
  * Exception thrown when a syntax error occurs during lexing or parsing of a template.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Error_Syntax extends Twig_Error
 class Twig_Error_Syntax extends Twig_Error
 {
 {

+ 28 - 0
lib/twig/ExistsLoaderInterface.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Adds an exists() method for loaders.
+ *
+ * @author Florin Patan <florinpatan@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
+ */
+interface Twig_ExistsLoaderInterface
+{
+    /**
+     * Check if we have the source code of a template, given its name.
+     *
+     * @param string $name The name of the template to check if we can load
+     *
+     * @return boolean If the template source code is handled by this loader or not
+     */
+    public function exists($name);
+}

+ 147 - 35
lib/twig/lib/Twig/ExpressionParser.php → lib/twig/ExpressionParser.php

@@ -18,8 +18,7 @@
  * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
  * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
  * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
  * @see http://en.wikipedia.org/wiki/Operator-precedence_parser
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_ExpressionParser
 class Twig_ExpressionParser
 {
 {
@@ -89,9 +88,19 @@ class Twig_ExpressionParser
     {
     {
         while ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '?')) {
         while ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '?')) {
             $this->parser->getStream()->next();
             $this->parser->getStream()->next();
-            $expr2 = $this->parseExpression();
-            $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'The ternary operator must have a default value');
-            $expr3 = $this->parseExpression();
+            if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+                $expr2 = $this->parseExpression();
+                if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+                    $this->parser->getStream()->next();
+                    $expr3 = $this->parseExpression();
+                } else {
+                    $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine());
+                }
+            } else {
+                $this->parser->getStream()->next();
+                $expr2 = $expr;
+                $expr3 = $this->parseExpression();
+            }
 
 
             $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
             $expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
         }
         }
@@ -158,7 +167,7 @@ class Twig_ExpressionParser
                 } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
                 } elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
                     $node = $this->parseHashExpression();
                     $node = $this->parseHashExpression();
                 } else {
                 } else {
-                    throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine());
+                    throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine(), $this->parser->getFilename());
                 }
                 }
         }
         }
 
 
@@ -252,7 +261,7 @@ class Twig_ExpressionParser
             } else {
             } else {
                 $current = $stream->getCurrent();
                 $current = $stream->getCurrent();
 
 
-                throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine());
+                throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine(), $this->parser->getFilename());
             }
             }
 
 
             $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
             $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
@@ -287,30 +296,31 @@ class Twig_ExpressionParser
 
 
     public function getFunctionNode($name, $line)
     public function getFunctionNode($name, $line)
     {
     {
-        $args = $this->parseArguments();
         switch ($name) {
         switch ($name) {
             case 'parent':
             case 'parent':
+                $args = $this->parseArguments();
                 if (!count($this->parser->getBlockStack())) {
                 if (!count($this->parser->getBlockStack())) {
-                    throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line);
+                    throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
                 }
                 }
 
 
                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
-                    throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line);
+                    throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename());
                 }
                 }
 
 
                 return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
                 return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
             case 'block':
             case 'block':
-                return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line);
+                return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
             case 'attribute':
             case 'attribute':
+                $args = $this->parseArguments();
                 if (count($args) < 2) {
                 if (count($args) < 2) {
-                    throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line);
+                    throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
                 }
                 }
 
 
                 return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line);
                 return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line);
             default:
             default:
-                if (null !== $alias = $this->parser->getImportedFunction($name)) {
+                if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
                     $arguments = new Twig_Node_Expression_Array(array(), $line);
                     $arguments = new Twig_Node_Expression_Array(array(), $line);
-                    foreach ($args as $n) {
+                    foreach ($this->parseArguments() as $n) {
                         $arguments->addElement($n);
                         $arguments->addElement($n);
                     }
                     }
 
 
@@ -320,7 +330,8 @@ class Twig_ExpressionParser
                     return $node;
                     return $node;
                 }
                 }
 
 
-                $class = $this->getFunctionNodeClass($name);
+                $args = $this->parseArguments(true);
+                $class = $this->getFunctionNodeClass($name, $line);
 
 
                 return new $class($name, $args, $line);
                 return new $class($name, $args, $line);
         }
         }
@@ -351,24 +362,44 @@ class Twig_ExpressionParser
                     }
                     }
                 }
                 }
             } else {
             } else {
-                throw new Twig_Error_Syntax('Expected name or number', $lineno);
+                throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename());
+            }
+
+            if ($node instanceof Twig_Node_Expression_Name && null !== $alias = $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
+                if (!$arg instanceof Twig_Node_Expression_Constant) {
+                    throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename());
+                }
+
+                $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
+                $node->setAttribute('safe', true);
+
+                return $node;
             }
             }
         } else {
         } else {
             $type = Twig_TemplateInterface::ARRAY_CALL;
             $type = Twig_TemplateInterface::ARRAY_CALL;
 
 
-            $arg = $this->parseExpression();
-
             // slice?
             // slice?
+            $slice = false;
             if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
             if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+                $slice = true;
+                $arg = new Twig_Node_Expression_Constant(0, $token->getLine());
+            } else {
+                $arg = $this->parseExpression();
+            }
+
+            if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+                $slice = true;
                 $stream->next();
                 $stream->next();
+            }
 
 
+            if ($slice) {
                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
                 if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
                     $length = new Twig_Node_Expression_Constant(null, $token->getLine());
                     $length = new Twig_Node_Expression_Constant(null, $token->getLine());
                 } else {
                 } else {
                     $length = $this->parseExpression();
                     $length = $this->parseExpression();
                 }
                 }
 
 
-                $class = $this->getFilterNodeClass('slice');
+                $class = $this->getFilterNodeClass('slice', $token->getLine());
                 $arguments = new Twig_Node(array($arg, $length));
                 $arguments = new Twig_Node(array($arg, $length));
                 $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
                 $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
 
 
@@ -399,10 +430,10 @@ class Twig_ExpressionParser
             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
             if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
                 $arguments = new Twig_Node();
                 $arguments = new Twig_Node();
             } else {
             } else {
-                $arguments = $this->parseArguments();
+                $arguments = $this->parseArguments(true);
             }
             }
 
 
-            $class = $this->getFilterNodeClass($name->getAttribute('value'));
+            $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
 
 
             $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
             $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
 
 
@@ -416,17 +447,62 @@ class Twig_ExpressionParser
         return $node;
         return $node;
     }
     }
 
 
-    public function parseArguments()
+    /**
+     * Parses arguments.
+     *
+     * @param Boolean $namedArguments Whether to allow named arguments or not
+     * @param Boolean $definition     Whether we are parsing arguments for a function definition
+     */
+    public function parseArguments($namedArguments = false, $definition = false)
     {
     {
         $args = array();
         $args = array();
         $stream = $this->parser->getStream();
         $stream = $this->parser->getStream();
 
 
-        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
+        $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
         while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
             if (!empty($args)) {
             if (!empty($args)) {
                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
                 $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
             }
             }
-            $args[] = $this->parseExpression();
+
+            if ($definition) {
+                $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name');
+                $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine());
+            } else {
+                $value = $this->parseExpression();
+            }
+
+            $name = null;
+            if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) {
+                $token = $stream->next();
+                if (!$value instanceof Twig_Node_Expression_Name) {
+                    throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename());
+                }
+                $name = $value->getAttribute('name');
+
+                if ($definition) {
+                    $value = $this->parsePrimaryExpression();
+
+                    if (!$this->checkConstantExpression($value)) {
+                        throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename());
+                    }
+                } else {
+                    $value = $this->parseExpression();
+                }
+            }
+
+            if ($definition) {
+                if (null === $name) {
+                    $name = $value->getAttribute('name');
+                    $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine());
+                }
+                $args[$name] = $value;
+            } else {
+                if (null === $name) {
+                    $args[] = $value;
+                } else {
+                    $args[$name] = $value;
+                }
+            }
         }
         }
         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
         $stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
 
 
@@ -439,7 +515,7 @@ class Twig_ExpressionParser
         while (true) {
         while (true) {
             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
             $token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
             if (in_array($token->getValue(), array('true', 'false', 'none'))) {
             if (in_array($token->getValue(), array('true', 'false', 'none'))) {
-                throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine());
+                throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename());
             }
             }
             $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
             $targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
 
 
@@ -466,23 +542,59 @@ class Twig_ExpressionParser
         return new Twig_Node($targets);
         return new Twig_Node($targets);
     }
     }
 
 
-    protected function getFunctionNodeClass($name)
+    protected function getFunctionNodeClass($name, $line)
+    {
+        $env = $this->parser->getEnvironment();
+
+        if (false === $function = $env->getFunction($name)) {
+            $message = sprintf('The function "%s" does not exist', $name);
+            if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) {
+                $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
+            }
+
+            throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
+        }
+
+        if ($function instanceof Twig_SimpleFunction) {
+            return $function->getNodeClass();
+        }
+
+        return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function';
+    }
+
+    protected function getFilterNodeClass($name, $line)
     {
     {
-        $functionMap = $this->parser->getEnvironment()->getFunctions();
-        if (isset($functionMap[$name]) && $functionMap[$name] instanceof Twig_Function_Node) {
-            return $functionMap[$name]->getClass();
+        $env = $this->parser->getEnvironment();
+
+        if (false === $filter = $env->getFilter($name)) {
+            $message = sprintf('The filter "%s" does not exist', $name);
+            if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) {
+                $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
+            }
+
+            throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
+        }
+
+        if ($filter instanceof Twig_SimpleFilter) {
+            return $filter->getNodeClass();
         }
         }
 
 
-        return 'Twig_Node_Expression_Function';
+        return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
     }
     }
 
 
-    protected function getFilterNodeClass($name)
+    // checks that the node only contains "constant" elements
+    protected function checkConstantExpression(Twig_NodeInterface $node)
     {
     {
-        $filterMap = $this->parser->getEnvironment()->getFilters();
-        if (isset($filterMap[$name]) && $filterMap[$name] instanceof Twig_Filter_Node) {
-            return $filterMap[$name]->getClass();
+        if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) {
+            return false;
+        }
+
+        foreach ($node as $n) {
+            if (!$this->checkConstantExpression($n)) {
+                return false;
+            }
         }
         }
 
 
-        return 'Twig_Node_Expression_Filter';
+        return true;
     }
     }
 }
 }

+ 0 - 0
lib/twig/lib/Twig/Extension.php → lib/twig/Extension.php


+ 441 - 107
lib/twig/lib/Twig/Extension/Core.php → lib/twig/Extension/Core.php

@@ -48,7 +48,7 @@ class Twig_Extension_Core extends Twig_Extension
     /**
     /**
      * Sets the default timezone to be used by the date filter.
      * Sets the default timezone to be used by the date filter.
      *
      *
-     * @param DateTimeZone|string $timezone  The default timezone string or a DateTimeZone object
+     * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object
      */
      */
     public function setTimezone($timezone)
     public function setTimezone($timezone)
     {
     {
@@ -62,15 +62,19 @@ class Twig_Extension_Core extends Twig_Extension
      */
      */
     public function getTimezone()
     public function getTimezone()
     {
     {
+        if (null === $this->timezone) {
+            $this->timezone = new DateTimeZone(date_default_timezone_get());
+        }
+
         return $this->timezone;
         return $this->timezone;
     }
     }
 
 
     /**
     /**
      * Sets the default format to be used by the number_format filter.
      * Sets the default format to be used by the number_format filter.
      *
      *
-     * @param integer $decimal The number of decimal places to use.
-     * @param string $decimalPoint The character(s) to use for the decimal point.
-     * @param string $thousandSep The character(s) to use for the thousands separator.
+     * @param integer $decimal      The number of decimal places to use.
+     * @param string  $decimalPoint The character(s) to use for the decimal point.
+     * @param string  $thousandSep  The character(s) to use for the thousands separator.
      */
      */
     public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
     public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
     {
     {
@@ -109,6 +113,7 @@ class Twig_Extension_Core extends Twig_Extension
             new Twig_TokenParser_Spaceless(),
             new Twig_TokenParser_Spaceless(),
             new Twig_TokenParser_Flush(),
             new Twig_TokenParser_Flush(),
             new Twig_TokenParser_Do(),
             new Twig_TokenParser_Do(),
+            new Twig_TokenParser_Embed(),
         );
         );
     }
     }
 
 
@@ -121,49 +126,53 @@ class Twig_Extension_Core extends Twig_Extension
     {
     {
         $filters = array(
         $filters = array(
             // formatting filters
             // formatting filters
-            'date'          => new Twig_Filter_Function('twig_date_format_filter', array('needs_environment' => true)),
-            'format'        => new Twig_Filter_Function('sprintf'),
-            'replace'       => new Twig_Filter_Function('strtr'),
-            'number_format' => new Twig_Filter_Function('twig_number_format_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('format', 'sprintf'),
+            new Twig_SimpleFilter('replace', 'strtr'),
+            new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('abs', 'abs'),
 
 
             // encoding
             // encoding
-            'url_encode'       => new Twig_Filter_Function('twig_urlencode_filter'),
-            'json_encode'      => new Twig_Filter_Function('twig_jsonencode_filter'),
-            'convert_encoding' => new Twig_Filter_Function('twig_convert_encoding'),
+            new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'),
+            new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'),
+            new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'),
 
 
             // string filters
             // string filters
-            'title'      => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)),
-            'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)),
-            'upper'      => new Twig_Filter_Function('strtoupper'),
-            'lower'      => new Twig_Filter_Function('strtolower'),
-            'striptags'  => new Twig_Filter_Function('strip_tags'),
-            'trim'       => new Twig_Filter_Function('trim'),
-            'nl2br'      => new Twig_Filter_Function('nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
+            new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('upper', 'strtoupper'),
+            new Twig_SimpleFilter('lower', 'strtolower'),
+            new Twig_SimpleFilter('striptags', 'strip_tags'),
+            new Twig_SimpleFilter('trim', 'trim'),
+            new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
 
 
             // array helpers
             // array helpers
-            'join'    => new Twig_Filter_Function('twig_join_filter'),
-            'sort'    => new Twig_Filter_Function('twig_sort_filter'),
-            'merge'   => new Twig_Filter_Function('twig_array_merge'),
+            new Twig_SimpleFilter('join', 'twig_join_filter'),
+            new Twig_SimpleFilter('split', 'twig_split_filter'),
+            new Twig_SimpleFilter('sort', 'twig_sort_filter'),
+            new Twig_SimpleFilter('merge', 'twig_array_merge'),
+            new Twig_SimpleFilter('batch', 'twig_array_batch'),
 
 
             // string/array filters
             // string/array filters
-            'reverse' => new Twig_Filter_Function('twig_reverse_filter', array('needs_environment' => true)),
-            'length'  => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)),
-            'slice'   => new Twig_Filter_Function('twig_slice', array('needs_environment' => true)),
+            new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)),
+            new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)),
+            new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)),
+            new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)),
 
 
             // iteration and runtime
             // iteration and runtime
-            'default' => new Twig_Filter_Node('Twig_Node_Expression_Filter_Default'),
-            '_default' => new Twig_Filter_Function('_twig_default_filter'),
-
-            'keys'    => new Twig_Filter_Function('twig_get_array_keys_filter'),
+            new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')),
+            new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'),
 
 
             // escaping
             // escaping
-            'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
-            'e'      => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
+            new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
+            new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
         );
         );
 
 
         if (function_exists('mb_get_info')) {
         if (function_exists('mb_get_info')) {
-            $filters['upper'] = new Twig_Filter_Function('twig_upper_filter', array('needs_environment' => true));
-            $filters['lower'] = new Twig_Filter_Function('twig_lower_filter', array('needs_environment' => true));
+            $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true));
+            $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true));
         }
         }
 
 
         return $filters;
         return $filters;
@@ -177,11 +186,12 @@ class Twig_Extension_Core extends Twig_Extension
     public function getFunctions()
     public function getFunctions()
     {
     {
         return array(
         return array(
-            'range'    => new Twig_Function_Function('range'),
-            'constant' => new Twig_Function_Function('constant'),
-            'cycle'    => new Twig_Function_Function('twig_cycle'),
-            'random'   => new Twig_Function_Function('twig_random', array('needs_environment' => true)),
-            'date'     => new Twig_Function_Function('twig_date_converter', array('needs_environment' => true)),
+            new Twig_SimpleFunction('range', 'range'),
+            new Twig_SimpleFunction('constant', 'twig_constant'),
+            new Twig_SimpleFunction('cycle', 'twig_cycle'),
+            new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)),
+            new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)),
+            new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true)),
         );
         );
     }
     }
 
 
@@ -193,15 +203,16 @@ class Twig_Extension_Core extends Twig_Extension
     public function getTests()
     public function getTests()
     {
     {
         return array(
         return array(
-            'even'        => new Twig_Test_Node('Twig_Node_Expression_Test_Even'),
-            'odd'         => new Twig_Test_Node('Twig_Node_Expression_Test_Odd'),
-            'defined'     => new Twig_Test_Node('Twig_Node_Expression_Test_Defined'),
-            'sameas'      => new Twig_Test_Node('Twig_Node_Expression_Test_Sameas'),
-            'none'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
-            'null'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
-            'divisibleby' => new Twig_Test_Node('Twig_Node_Expression_Test_Divisibleby'),
-            'constant'    => new Twig_Test_Node('Twig_Node_Expression_Test_Constant'),
-            'empty'       => new Twig_Test_Function('twig_test_empty'),
+            new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')),
+            new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')),
+            new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')),
+            new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')),
+            new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
+            new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
+            new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')),
+            new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')),
+            new Twig_SimpleTest('empty', 'twig_test_empty'),
+            new Twig_SimpleTest('iterable', 'twig_test_iterable'),
         );
         );
     }
     }
 
 
@@ -219,11 +230,11 @@ class Twig_Extension_Core extends Twig_Extension
                 '+'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
                 '+'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
             ),
             ),
             array(
             array(
-                'b-and'  => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
-                'b-xor'  => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
-                'b-or'   => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 'or'     => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 'or'     => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 'and'    => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 'and'    => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+                'b-or'   => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+                'b-xor'  => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+                'b-and'  => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '=='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '=='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '!='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '!='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '<'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
                 '<'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
@@ -258,22 +269,32 @@ class Twig_Extension_Core extends Twig_Extension
         $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
         $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
         $arguments = null;
         $arguments = null;
         if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
         if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
-            $arguments = $parser->getExpressionParser()->parseArguments();
+            $arguments = $parser->getExpressionParser()->parseArguments(true);
         }
         }
 
 
-        $class = $this->getTestNodeClass($parser->getEnvironment(), $name);
+        $class = $this->getTestNodeClass($parser, $name, $node->getLine());
 
 
         return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine());
         return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine());
     }
     }
 
 
-    protected function getTestNodeClass(Twig_Environment $env, $name)
+    protected function getTestNodeClass(Twig_Parser $parser, $name, $line)
     {
     {
+        $env = $parser->getEnvironment();
         $testMap = $env->getTests();
         $testMap = $env->getTests();
-        if (isset($testMap[$name]) && $testMap[$name] instanceof Twig_Test_Node) {
-            return $testMap[$name]->getClass();
+        if (!isset($testMap[$name])) {
+            $message = sprintf('The test "%s" does not exist', $name);
+            if ($alternatives = $env->computeAlternatives($name, array_keys($env->getTests()))) {
+                $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
+            }
+
+            throw new Twig_Error_Syntax($message, $line, $parser->getFilename());
+        }
+
+        if ($testMap[$name] instanceof Twig_SimpleTest) {
+            return $testMap[$name]->getNodeClass();
         }
         }
 
 
-        return 'Twig_Node_Expression_Test';
+        return $testMap[$name] instanceof Twig_Test_Node ? $testMap[$name]->getClass() : 'Twig_Node_Expression_Test';
     }
     }
 
 
     /**
     /**
@@ -290,18 +311,18 @@ class Twig_Extension_Core extends Twig_Extension
 /**
 /**
  * Cycles over a value.
  * Cycles over a value.
  *
  *
- * @param ArrayAccess|array $values An array or an ArrayAccess instance
- * @param integer           $i      The cycle value
+ * @param ArrayAccess|array $values   An array or an ArrayAccess instance
+ * @param integer           $position The cycle position
  *
  *
  * @return string The next value in the cycle
  * @return string The next value in the cycle
  */
  */
-function twig_cycle($values, $i)
+function twig_cycle($values, $position)
 {
 {
     if (!is_array($values) && !$values instanceof ArrayAccess) {
     if (!is_array($values) && !$values instanceof ArrayAccess) {
         return $values;
         return $values;
     }
     }
 
 
-    return $values[$i % count($values)];
+    return $values[$position % count($values)];
 }
 }
 
 
 /**
 /**
@@ -310,8 +331,10 @@ function twig_cycle($values, $i)
  * - a random character from a string
  * - a random character from a string
  * - a random integer between 0 and the integer parameter
  * - a random integer between 0 and the integer parameter
  *
  *
- * @param Twig_Environment             $env    A Twig_Environment instance
- * @param Traversable|array|int|string $values The values to pick a random item from
+ * @param Twig_Environment                 $env    A Twig_Environment instance
+ * @param Traversable|array|integer|string $values The values to pick a random item from
+ *
+ * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is).
  *
  *
  * @return mixed A random value from the given sequence
  * @return mixed A random value from the given sequence
  */
  */
@@ -328,6 +351,9 @@ function twig_random(Twig_Environment $env, $values = null)
     if ($values instanceof Traversable) {
     if ($values instanceof Traversable) {
         $values = iterator_to_array($values);
         $values = iterator_to_array($values);
     } elseif (is_string($values)) {
     } elseif (is_string($values)) {
+        if ('' === $values) {
+            return '';
+        }
         if (null !== $charset = $env->getCharset()) {
         if (null !== $charset = $env->getCharset()) {
             if ('UTF-8' != $charset) {
             if ('UTF-8' != $charset) {
                 $values = twig_convert_encoding($values, 'UTF-8', $charset);
                 $values = twig_convert_encoding($values, 'UTF-8', $charset);
@@ -370,7 +396,7 @@ function twig_random(Twig_Environment $env, $values = null)
  * @param string                       $format   A format
  * @param string                       $format   A format
  * @param DateTimeZone|string          $timezone A timezone
  * @param DateTimeZone|string          $timezone A timezone
  *
  *
- * @return string The formatter date
+ * @return string The formatted date
  */
  */
 function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
 function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
 {
 {
@@ -379,17 +405,34 @@ function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $
         $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
         $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
     }
     }
 
 
-    if ($date instanceof DateInterval || $date instanceof DateTime) {
-        if (null !== $timezone) {
-            $date->setTimezone($timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone));
-        }
-
+    if ($date instanceof DateInterval) {
         return $date->format($format);
         return $date->format($format);
     }
     }
 
 
     return twig_date_converter($env, $date, $timezone)->format($format);
     return twig_date_converter($env, $date, $timezone)->format($format);
 }
 }
 
 
+/**
+ * Returns a new date object modified
+ *
+ * <pre>
+ *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
+ * </pre>
+ *
+ * @param Twig_Environment  $env      A Twig_Environment instance
+ * @param DateTime|string   $date     A date
+ * @param string            $modifier A modifier string
+ *
+ * @return DateTime A new date object
+ */
+function twig_date_modify_filter(Twig_Environment $env, $date, $modifier)
+{
+    $date = twig_date_converter($env, $date, false);
+    $date->modify($modifier);
+
+    return $date;
+}
+
 /**
 /**
  * Converts an input to a DateTime instance.
  * Converts an input to a DateTime instance.
  *
  *
@@ -407,28 +450,32 @@ function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $
  */
  */
 function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
 function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
 {
 {
+    // determine the timezone
+    if (!$timezone) {
+        $defaultTimezone = $env->getExtension('core')->getTimezone();
+    } elseif (!$timezone instanceof DateTimeZone) {
+        $defaultTimezone = new DateTimeZone($timezone);
+    } else {
+        $defaultTimezone = $timezone;
+    }
+
     if ($date instanceof DateTime) {
     if ($date instanceof DateTime) {
+        $date = clone $date;
+        if (false !== $timezone) {
+            $date->setTimezone($defaultTimezone);
+        }
+
         return $date;
         return $date;
     }
     }
 
 
     $asString = (string) $date;
     $asString = (string) $date;
-
     if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
     if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
-        $date = new DateTime('@'.$date);
-        $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
-    } else {
-        $date = new DateTime($date);
+        $date = '@'.$date;
     }
     }
 
 
-    // set Timezone
-    if (null !== $timezone) {
-        if (!$timezone instanceof DateTimeZone) {
-            $timezone = new DateTimeZone($timezone);
-        }
-
-        $date->setTimezone($timezone);
-    } elseif (($timezone = $env->getExtension('core')->getTimezone()) instanceof DateTimeZone) {
-        $date->setTimezone($timezone);
+    $date = new DateTime($date, $defaultTimezone);
+    if (false !== $timezone) {
+        $date->setTimezone($defaultTimezone);
     }
     }
 
 
     return $date;
     return $date;
@@ -443,7 +490,7 @@ function twig_date_converter(Twig_Environment $env, $date = null, $timezone = nu
  *
  *
  * @param Twig_Environment    $env          A Twig_Environment instance
  * @param Twig_Environment    $env          A Twig_Environment instance
  * @param mixed               $number       A float/int/string of the number to format
  * @param mixed               $number       A float/int/string of the number to format
- * @param int                 $decimal      The number of decimal points to display.
+ * @param integer             $decimal      The number of decimal points to display.
  * @param string              $decimalPoint The character(s) to use for the decimal point.
  * @param string              $decimalPoint The character(s) to use for the decimal point.
  * @param string              $thousandSep  The character(s) to use for the thousands separator.
  * @param string              $thousandSep  The character(s) to use for the thousands separator.
  *
  *
@@ -468,15 +515,19 @@ function twig_number_format_filter(Twig_Environment $env, $number, $decimal = nu
 }
 }
 
 
 /**
 /**
- * URL encodes a string.
+ * URL encodes a string as a path segment or an array as a query string.
  *
  *
- * @param string $url A URL
- * @param bool   $raw true to use rawurlencode() instead of urlencode
+ * @param string|array $url A URL or an array of query parameters
+ * @param bool         $raw true to use rawurlencode() instead of urlencode
  *
  *
  * @return string The URL encoded value
  * @return string The URL encoded value
  */
  */
 function twig_urlencode_filter($url, $raw = false)
 function twig_urlencode_filter($url, $raw = false)
 {
 {
+    if (is_array($url)) {
+        return http_build_query($url, '', '&');
+    }
+
     if ($raw) {
     if ($raw) {
         return rawurlencode($url);
         return rawurlencode($url);
     }
     }
@@ -586,6 +637,36 @@ function twig_slice(Twig_Environment $env, $item, $start, $length = null, $prese
     return null === $length ? substr($item, $start) : substr($item, $start, $length);
     return null === $length ? substr($item, $start) : substr($item, $start, $length);
 }
 }
 
 
+/**
+ * Returns the first element of the item.
+ *
+ * @param Twig_Environment $env  A Twig_Environment instance
+ * @param mixed            $item A variable
+ *
+ * @return mixed The first element of the item
+ */
+function twig_first(Twig_Environment $env, $item)
+{
+    $elements = twig_slice($env, $item, 0, 1, false);
+
+    return is_string($elements) ? $elements[0] : current($elements);
+}
+
+/**
+ * Returns the last element of the item.
+ *
+ * @param Twig_Environment $env  A Twig_Environment instance
+ * @param mixed            $item A variable
+ *
+ * @return mixed The last element of the item
+ */
+function twig_last(Twig_Environment $env, $item)
+{
+    $elements = twig_slice($env, $item, -1, 1, false);
+
+    return is_string($elements) ? $elements[0] : current($elements);
+}
+
 /**
 /**
  * Joins the values to a string.
  * Joins the values to a string.
  *
  *
@@ -613,6 +694,38 @@ function twig_join_filter($value, $glue = '')
     return implode($glue, (array) $value);
     return implode($glue, (array) $value);
 }
 }
 
 
+/**
+ * Splits the string into an array.
+ *
+ * <pre>
+ *  {{ "one,two,three"|split(',') }}
+ *  {# returns [one, two, three] #}
+ *
+ *  {{ "one,two,three,four,five"|split(',', 3) }}
+ *  {# returns [one, two, "three,four,five"] #}
+ *
+ *  {{ "123"|split('') }}
+ *  {# returns [1, 2, 3] #}
+ *
+ *  {{ "aabbcc"|split('', 2) }}
+ *  {# returns [aa, bb, cc] #}
+ * </pre>
+ *
+ * @param string  $value     A string
+ * @param string  $delimiter The delimiter
+ * @param integer $limit     The limit
+ *
+ * @return array The split string as an array
+ */
+function twig_split_filter($value, $delimiter, $limit = null)
+{
+    if (empty($delimiter)) {
+        return str_split($value, null === $limit ? 1 : $limit);
+    }
+
+    return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
+}
+
 // The '_default' filter is used internally to avoid using the ternary operator
 // The '_default' filter is used internally to avoid using the ternary operator
 // which costs a lot for big contexts (before PHP 5.4). So, on average,
 // which costs a lot for big contexts (before PHP 5.4). So, on average,
 // a function call is cheaper.
 // a function call is cheaper.
@@ -709,15 +822,15 @@ function twig_sort_filter($array)
 function twig_in_filter($value, $compare)
 function twig_in_filter($value, $compare)
 {
 {
     if (is_array($compare)) {
     if (is_array($compare)) {
-        return in_array($value, $compare);
+        return in_array($value, $compare, is_object($value));
     } elseif (is_string($compare)) {
     } elseif (is_string($compare)) {
-        if (!strlen((string) $value)) {
+        if (!strlen($value)) {
             return empty($compare);
             return empty($compare);
         }
         }
 
 
         return false !== strpos($compare, (string) $value);
         return false !== strpos($compare, (string) $value);
-    } elseif (is_object($compare) && $compare instanceof Traversable) {
-        return in_array($value, iterator_to_array($compare, false));
+    } elseif ($compare instanceof Traversable) {
+        return in_array($value, iterator_to_array($compare, false), is_object($value));
     }
     }
 
 
     return false;
     return false;
@@ -728,11 +841,11 @@ function twig_in_filter($value, $compare)
  *
  *
  * @param Twig_Environment $env        A Twig_Environment instance
  * @param Twig_Environment $env        A Twig_Environment instance
  * @param string           $string     The value to be escaped
  * @param string           $string     The value to be escaped
- * @param string           $type       The escaping strategy
+ * @param string           $strategy   The escaping strategy
  * @param string           $charset    The charset
  * @param string           $charset    The charset
  * @param Boolean          $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
  * @param Boolean          $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
  */
  */
-function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $charset = null, $autoescape = false)
+function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
 {
 {
     if ($autoescape && is_object($string) && $string instanceof Twig_Markup) {
     if ($autoescape && is_object($string) && $string instanceof Twig_Markup) {
         return $string;
         return $string;
@@ -748,7 +861,7 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $cha
 
 
     $string = (string) $string;
     $string = (string) $string;
 
 
-    switch ($type) {
+    switch ($strategy) {
         case 'js':
         case 'js':
             // escape all non-alphanumeric characters
             // escape all non-alphanumeric characters
             // into their \xHH or \uHHHH representations
             // into their \xHH or \uHHHH representations
@@ -756,10 +869,46 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $cha
                 $string = twig_convert_encoding($string, 'UTF-8', $charset);
                 $string = twig_convert_encoding($string, 'UTF-8', $charset);
             }
             }
 
 
-            if (null === $string = preg_replace_callback('#[^\p{L}\p{N} ]#u', '_twig_escape_js_callback', $string)) {
+            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
+                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
+            }
+
+            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string);
+
+            if ('UTF-8' != $charset) {
+                $string = twig_convert_encoding($string, $charset, 'UTF-8');
+            }
+
+            return $string;
+
+        case 'css':
+            if ('UTF-8' != $charset) {
+                $string = twig_convert_encoding($string, 'UTF-8', $charset);
+            }
+
+            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
+                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
+            }
+
+            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string);
+
+            if ('UTF-8' != $charset) {
+                $string = twig_convert_encoding($string, $charset, 'UTF-8');
+            }
+
+            return $string;
+
+        case 'html_attr':
+            if ('UTF-8' != $charset) {
+                $string = twig_convert_encoding($string, 'UTF-8', $charset);
+            }
+
+            if (0 == strlen($string) ? false : (1 == preg_match('/^./su', $string) ? false : true)) {
                 throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
                 throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
             }
             }
 
 
+            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string);
+
             if ('UTF-8' != $charset) {
             if ('UTF-8' != $charset) {
                 $string = twig_convert_encoding($string, $charset, 'UTF-8');
                 $string = twig_convert_encoding($string, $charset, 'UTF-8');
             }
             }
@@ -771,7 +920,7 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $cha
 
 
             // Using a static variable to avoid initializing the array
             // Using a static variable to avoid initializing the array
             // each time the function is called. Moving the declaration on the
             // each time the function is called. Moving the declaration on the
-            // top of the function slow downs other escaping types.
+            // top of the function slow downs other escaping strategies.
             static $htmlspecialcharsCharsets = array(
             static $htmlspecialcharsCharsets = array(
                 'iso-8859-1' => true, 'iso8859-1' => true,
                 'iso-8859-1' => true, 'iso8859-1' => true,
                 'iso-8859-15' => true, 'iso8859-15' => true,
                 'iso-8859-15' => true, 'iso8859-15' => true,
@@ -798,8 +947,15 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $cha
 
 
             return twig_convert_encoding($string, $charset, 'UTF-8');
             return twig_convert_encoding($string, $charset, 'UTF-8');
 
 
+        case 'url':
+            if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+                return str_replace('%7E', '~', rawurlencode($string));
+            }
+
+            return rawurlencode($string);
+
         default:
         default:
-            throw new Twig_Error_Runtime(sprintf('Invalid escape type "%s".', $type));
+            throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: html, js, url, css, and html_attr).', $strategy));
     }
     }
 }
 }
 
 
@@ -817,15 +973,15 @@ function twig_escape_filter_is_safe(Twig_Node $filterArgs)
     return array('html');
     return array('html');
 }
 }
 
 
-if (function_exists('iconv')) {
+if (function_exists('mb_convert_encoding')) {
     function twig_convert_encoding($string, $to, $from)
     function twig_convert_encoding($string, $to, $from)
     {
     {
-        return iconv($from, $to, $string);
+        return mb_convert_encoding($string, $to, $from);
     }
     }
-} elseif (function_exists('mb_convert_encoding')) {
+} elseif (function_exists('iconv')) {
     function twig_convert_encoding($string, $to, $from)
     function twig_convert_encoding($string, $to, $from)
     {
     {
-        return mb_convert_encoding($string, $to, $from);
+        return iconv($from, $to, $string);
     }
     }
 } else {
 } else {
     function twig_convert_encoding($string, $to, $from)
     function twig_convert_encoding($string, $to, $from)
@@ -840,13 +996,89 @@ function _twig_escape_js_callback($matches)
 
 
     // \xHH
     // \xHH
     if (!isset($char[1])) {
     if (!isset($char[1])) {
-        return '\\x'.substr('00'.bin2hex($char), -2);
+        return '\\x'.strtoupper(substr('00'.bin2hex($char), -2));
     }
     }
 
 
     // \uHHHH
     // \uHHHH
     $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
     $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
 
 
-    return '\\u'.substr('0000'.bin2hex($char), -4);
+    return '\\u'.strtoupper(substr('0000'.bin2hex($char), -4));
+}
+
+function _twig_escape_css_callback($matches)
+{
+    $char = $matches[0];
+
+    // \xHH
+    if (!isset($char[1])) {
+        $hex = ltrim(strtoupper(bin2hex($char)), '0');
+        if (0 === strlen($hex)) {
+            $hex = '0';
+        }
+
+        return '\\'.$hex.' ';
+    }
+
+    // \uHHHH
+    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
+
+    return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' ';
+}
+
+/**
+ * This function is adapted from code coming from Zend Framework.
+ *
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license   http://framework.zend.com/license/new-bsd New BSD License
+ */
+function _twig_escape_html_attr_callback($matches)
+{
+    /*
+     * While HTML supports far more named entities, the lowest common denominator
+     * has become HTML5's XML Serialisation which is restricted to the those named
+     * entities that XML supports. Using HTML entities would result in this error:
+     *     XML Parsing Error: undefined entity
+     */
+    static $entityMap = array(
+        34 => 'quot', /* quotation mark */
+        38 => 'amp',  /* ampersand */
+        60 => 'lt',   /* less-than sign */
+        62 => 'gt',   /* greater-than sign */
+    );
+
+    $chr = $matches[0];
+    $ord = ord($chr);
+
+    /**
+     * The following replaces characters undefined in HTML with the
+     * hex entity for the Unicode replacement character.
+     */
+    if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) {
+        return '&#xFFFD;';
+    }
+
+    /**
+     * Check if the current character to escape has a name entity we should
+     * replace it with while grabbing the hex value of the character.
+     */
+    if (strlen($chr) == 1) {
+        $hex = strtoupper(substr('00'.bin2hex($chr), -2));
+    } else {
+        $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8');
+        $hex = strtoupper(substr('0000'.bin2hex($chr), -4));
+    }
+
+    $int = hexdec($hex);
+    if (array_key_exists($int, $entityMap)) {
+        return sprintf('&%s;', $entityMap[$int]);
+    }
+
+    /**
+     * Per OWASP recommendations, we'll use hex entities for any other
+     * characters where a named entity does not exist.
+     */
+
+    return sprintf('&#x%s;', $hex);
 }
 }
 
 
 // add multibyte extensions if possible
 // add multibyte extensions if possible
@@ -934,8 +1166,7 @@ if (function_exists('mb_get_info')) {
     }
     }
 }
 }
 // and byte fallback
 // and byte fallback
-else
-{
+else {
     /**
     /**
      * Returns the length of a variable.
      * Returns the length of a variable.
      *
      *
@@ -979,11 +1210,11 @@ else
 /* used internally */
 /* used internally */
 function twig_ensure_traversable($seq)
 function twig_ensure_traversable($seq)
 {
 {
-    if (is_array($seq) || (is_object($seq) && $seq instanceof Traversable)) {
+    if ($seq instanceof Traversable || is_array($seq)) {
         return $seq;
         return $seq;
-    } else {
-        return array();
     }
     }
+
+    return array();
 }
 }
 
 
 /**
 /**
@@ -1006,5 +1237,108 @@ function twig_test_empty($value)
         return 0 == count($value);
         return 0 == count($value);
     }
     }
 
 
-    return false === $value || (empty($value) && '0' != $value);
+    return '' === $value || false === $value || null === $value || array() === $value;
+}
+
+/**
+ * Checks if a variable is traversable.
+ *
+ * <pre>
+ * {# evaluates to true if the foo variable is an array or a traversable object #}
+ * {% if foo is traversable %}
+ *     {# ... #}
+ * {% endif %}
+ * </pre>
+ *
+ * @param mixed $value A variable
+ *
+ * @return Boolean true if the value is traversable
+ */
+function twig_test_iterable($value)
+{
+    return $value instanceof Traversable || is_array($value);
+}
+
+/**
+ * Renders a template.
+ *
+ * @param string  template       The template to render
+ * @param array   variables      The variables to pass to the template
+ * @param Boolean with_context   Whether to pass the current context variables or not
+ * @param Boolean ignore_missing Whether to ignore missing templates or not
+ * @param Boolean sandboxed      Whether to sandbox the template or not
+ *
+ * @return string The rendered template
+ */
+function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false)
+{
+    if ($withContext) {
+        $variables = array_merge($context, $variables);
+    }
+
+    if ($isSandboxed = $sandboxed && $env->hasExtension('sandbox')) {
+        $sandbox = $env->getExtension('sandbox');
+        if (!$alreadySandboxed = $sandbox->isSandboxed()) {
+            $sandbox->enableSandbox();
+        }
+    }
+
+    try {
+        return $env->resolveTemplate($template)->display($variables);
+    } catch (Twig_Error_Loader $e) {
+        if (!$ignoreMissing) {
+            throw $e;
+        }
+    }
+
+    if ($isSandboxed && !$alreadySandboxed) {
+        $sandbox->disableSandbox();
+    }
+}
+
+/**
+ * Provides the ability to get constants from instances as well as class/global constants.
+ *
+ * @param string      $constant The name of the constant
+ * @param null|object $object   The object to get the constant from
+ *
+ * @return string
+ */
+function twig_constant($constant, $object = null)
+{
+    if (null !== $object) {
+        $constant = get_class($object).'::'.$constant;
+    }
+
+    return constant($constant);
+}
+
+/**
+ * Batches item.
+ *
+ * @param array   $items An array of items
+ * @param integer $size  The size of the batch
+ * @param string  $fill  A string to fill missing items
+ *
+ * @return array
+ */
+function twig_array_batch($items, $size, $fill = null)
+{
+    if ($items instanceof Traversable) {
+        $items = iterator_to_array($items, false);
+    }
+
+    $size = ceil($size);
+
+    $result = array_chunk($items, $size, true);
+
+    if (null !== $fill) {
+        $last = count($result) - 1;
+        $result[$last] = array_merge(
+            $result[$last],
+            array_fill(0, $size - count($result[$last]), $fill)
+        );
+    }
+
+    return $result;
 }
 }

+ 9 - 3
lib/twig/lib/Twig/Extension/Debug.php → lib/twig/Extension/Debug.php

@@ -17,11 +17,17 @@ class Twig_Extension_Debug extends Twig_Extension
      */
      */
     public function getFunctions()
     public function getFunctions()
     {
     {
-        // dump is safe if var_dump is overriden by xdebug
-        $isDumpOutputHtmlSafe = extension_loaded('xdebug') && (false === get_cfg_var('xdebug.overload_var_dump') || get_cfg_var('xdebug.overload_var_dump')) && get_cfg_var('html_errors');
+        // dump is safe if var_dump is overridden by xdebug
+        $isDumpOutputHtmlSafe = extension_loaded('xdebug')
+            // false means that it was not set (and the default is on) or it explicitly enabled
+            && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump'))
+            // false means that it was not set (and the default is on) or it explicitly enabled
+            // xdebug.overload_var_dump produces HTML only when html_errors is also enabled
+            && (false === ini_get('html_errors') || ini_get('html_errors'))
+        ;
 
 
         return array(
         return array(
-            'dump' => new Twig_Function_Function('twig_var_dump', array('is_safe' => $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)),
+            new Twig_SimpleFunction('dump', 'twig_var_dump', array('is_safe' => $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)),
         );
         );
     }
     }
 
 

+ 37 - 7
lib/twig/lib/Twig/Extension/Escaper.php → lib/twig/Extension/Escaper.php

@@ -10,11 +10,11 @@
  */
  */
 class Twig_Extension_Escaper extends Twig_Extension
 class Twig_Extension_Escaper extends Twig_Extension
 {
 {
-    protected $autoescape;
+    protected $defaultStrategy;
 
 
-    public function __construct($autoescape = true)
+    public function __construct($defaultStrategy = 'html')
     {
     {
-        $this->autoescape = $autoescape;
+        $this->setDefaultStrategy($defaultStrategy);
     }
     }
 
 
     /**
     /**
@@ -45,13 +45,44 @@ class Twig_Extension_Escaper extends Twig_Extension
     public function getFilters()
     public function getFilters()
     {
     {
         return array(
         return array(
-            'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe' => array('all'))),
+            new Twig_SimpleFilter('raw', 'twig_raw_filter', array('is_safe' => array('all'))),
         );
         );
     }
     }
 
 
-    public function isGlobal()
+    /**
+     * Sets the default strategy to use when not defined by the user.
+     *
+     * The strategy can be a valid PHP callback that takes the template
+     * "filename" as an argument and returns the strategy to use.
+     *
+     * @param mixed $defaultStrategy An escaping strategy
+     */
+    public function setDefaultStrategy($defaultStrategy)
+    {
+        // for BC
+        if (true === $defaultStrategy) {
+            $defaultStrategy = 'html';
+        }
+
+        $this->defaultStrategy = $defaultStrategy;
+    }
+
+    /**
+     * Gets the default strategy to use when not defined by the user.
+     *
+     * @param string $filename The template "filename"
+     *
+     * @return string The default strategy to use for the template
+     */
+    public function getDefaultStrategy($filename)
     {
     {
-        return $this->autoescape;
+        // disable string callables to avoid calling a function named html or js,
+        // or any other upcoming escaping strategy
+        if (!is_string($this->defaultStrategy) && is_callable($this->defaultStrategy)) {
+            return call_user_func($this->defaultStrategy, $filename);
+        }
+
+        return $this->defaultStrategy;
     }
     }
 
 
     /**
     /**
@@ -74,4 +105,3 @@ function twig_raw_filter($string)
 {
 {
     return $string;
     return $string;
 }
 }
-

+ 0 - 0
lib/twig/lib/Twig/Extension/Optimizer.php → lib/twig/Extension/Optimizer.php


+ 0 - 0
lib/twig/lib/Twig/Extension/Sandbox.php → lib/twig/Extension/Sandbox.php


+ 113 - 0
lib/twig/Extension/Staging.php

@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Internal class.
+ *
+ * This class is used by Twig_Environment as a staging area and must not be used directly.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Extension_Staging extends Twig_Extension
+{
+    protected $functions = array();
+    protected $filters = array();
+    protected $visitors = array();
+    protected $tokenParsers = array();
+    protected $globals = array();
+    protected $tests = array();
+
+    public function addFunction($name, $function)
+    {
+        $this->functions[$name] = $function;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFunctions()
+    {
+        return $this->functions;
+    }
+
+    public function addFilter($name, $filter)
+    {
+        $this->filters[$name] = $filter;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFilters()
+    {
+        return $this->filters;
+    }
+
+    public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
+    {
+        $this->visitors[] = $visitor;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNodeVisitors()
+    {
+        return $this->visitors;
+    }
+
+    public function addTokenParser(Twig_TokenParserInterface $parser)
+    {
+        $this->tokenParsers[] = $parser;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTokenParsers()
+    {
+        return $this->tokenParsers;
+    }
+
+    public function addGlobal($name, $value)
+    {
+        $this->globals[$name] = $value;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getGlobals()
+    {
+        return $this->globals;
+    }
+
+    public function addTest($name, $test)
+    {
+        $this->tests[$name] = $test;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTests()
+    {
+        return $this->tests;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'staging';
+    }
+}

+ 64 - 0
lib/twig/Extension/StringLoader.php

@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Extension_StringLoader extends Twig_Extension
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getFunctions()
+    {
+        return array(
+            new Twig_SimpleFunction('template_from_string', 'twig_template_from_string', array('needs_environment' => true)),
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'string_loader';
+    }
+}
+
+/**
+ * Loads a template from a string.
+ *
+ * <pre>
+ * {% include template_from_string("Hello {{ name }}") }}
+ * </pre>
+ *
+ * @param Twig_Environment $env      A Twig_Environment instance
+ * @param string           $template A template as a string
+ *
+ * @return Twig_Template A Twig_Template instance
+ */
+function twig_template_from_string(Twig_Environment $env, $template)
+{
+    static $loader;
+
+    if (null === $loader) {
+        $loader = new Twig_Loader_String();
+    }
+
+    $current = $env->getLoader();
+    $env->setLoader($loader);
+    try {
+        $template = $env->loadTemplate($template);
+    } catch (Exception $e) {
+        $env->setLoader($current);
+
+        throw $e;
+    }
+    $env->setLoader($current);
+
+    return $template;
+}

+ 10 - 11
lib/twig/lib/Twig/ExtensionInterface.php → lib/twig/ExtensionInterface.php

@@ -12,8 +12,7 @@
 /**
 /**
  * Interface implemented by extension classes.
  * Interface implemented by extension classes.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 interface Twig_ExtensionInterface
 interface Twig_ExtensionInterface
 {
 {
@@ -24,61 +23,61 @@ interface Twig_ExtensionInterface
      *
      *
      * @param Twig_Environment $environment The current Twig_Environment instance
      * @param Twig_Environment $environment The current Twig_Environment instance
      */
      */
-    function initRuntime(Twig_Environment $environment);
+    public function initRuntime(Twig_Environment $environment);
 
 
     /**
     /**
      * Returns the token parser instances to add to the existing list.
      * Returns the token parser instances to add to the existing list.
      *
      *
      * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
      * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
      */
      */
-    function getTokenParsers();
+    public function getTokenParsers();
 
 
     /**
     /**
      * Returns the node visitor instances to add to the existing list.
      * Returns the node visitor instances to add to the existing list.
      *
      *
      * @return array An array of Twig_NodeVisitorInterface instances
      * @return array An array of Twig_NodeVisitorInterface instances
      */
      */
-    function getNodeVisitors();
+    public function getNodeVisitors();
 
 
     /**
     /**
      * Returns a list of filters to add to the existing list.
      * Returns a list of filters to add to the existing list.
      *
      *
      * @return array An array of filters
      * @return array An array of filters
      */
      */
-    function getFilters();
+    public function getFilters();
 
 
     /**
     /**
      * Returns a list of tests to add to the existing list.
      * Returns a list of tests to add to the existing list.
      *
      *
      * @return array An array of tests
      * @return array An array of tests
      */
      */
-    function getTests();
+    public function getTests();
 
 
     /**
     /**
      * Returns a list of functions to add to the existing list.
      * Returns a list of functions to add to the existing list.
      *
      *
      * @return array An array of functions
      * @return array An array of functions
      */
      */
-    function getFunctions();
+    public function getFunctions();
 
 
     /**
     /**
      * Returns a list of operators to add to the existing list.
      * Returns a list of operators to add to the existing list.
      *
      *
      * @return array An array of operators
      * @return array An array of operators
      */
      */
-    function getOperators();
+    public function getOperators();
 
 
     /**
     /**
      * Returns a list of global variables to add to the existing list.
      * Returns a list of global variables to add to the existing list.
      *
      *
      * @return array An array of global variables
      * @return array An array of global variables
      */
      */
-    function getGlobals();
+    public function getGlobals();
 
 
     /**
     /**
      * Returns the name of the extension.
      * Returns the name of the extension.
      *
      *
      * @return string The extension name
      * @return string The extension name
      */
      */
-    function getName();
+    public function getName();
 }
 }

+ 18 - 4
lib/twig/lib/Twig/Filter.php → lib/twig/Filter.php

@@ -12,10 +12,12 @@
 /**
 /**
  * Represents a template filter.
  * Represents a template filter.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
-abstract class Twig_Filter implements Twig_FilterInterface
+abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface
 {
 {
     protected $options;
     protected $options;
     protected $arguments = array();
     protected $arguments = array();
@@ -26,6 +28,8 @@ abstract class Twig_Filter implements Twig_FilterInterface
             'needs_environment' => false,
             'needs_environment' => false,
             'needs_context'     => false,
             'needs_context'     => false,
             'pre_escape'        => null,
             'pre_escape'        => null,
+            'preserves_safety'  => null,
+            'callable'          => null,
         ), $options);
         ), $options);
     }
     }
 
 
@@ -59,11 +63,21 @@ abstract class Twig_Filter implements Twig_FilterInterface
             return call_user_func($this->options['is_safe_callback'], $filterArgs);
             return call_user_func($this->options['is_safe_callback'], $filterArgs);
         }
         }
 
 
-        return array();
+        return null;
+    }
+
+    public function getPreservesSafety()
+    {
+        return $this->options['preserves_safety'];
     }
     }
 
 
     public function getPreEscape()
     public function getPreEscape()
     {
     {
         return $this->options['pre_escape'];
         return $this->options['pre_escape'];
     }
     }
+
+    public function getCallable()
+    {
+        return $this->options['callable'];
+    }
 }
 }

+ 6 - 2
lib/twig/lib/Twig/Filter/Function.php → lib/twig/Filter/Function.php

@@ -12,8 +12,10 @@
 /**
 /**
  * Represents a function template filter.
  * Represents a function template filter.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 class Twig_Filter_Function extends Twig_Filter
 class Twig_Filter_Function extends Twig_Filter
 {
 {
@@ -21,6 +23,8 @@ class Twig_Filter_Function extends Twig_Filter
 
 
     public function __construct($function, array $options = array())
     public function __construct($function, array $options = array())
     {
     {
+        $options['callable'] = $function;
+
         parent::__construct($options);
         parent::__construct($options);
 
 
         $this->function = $function;
         $this->function = $function;

+ 8 - 3
lib/twig/lib/Twig/Filter/Method.php → lib/twig/Filter/Method.php

@@ -12,15 +12,20 @@
 /**
 /**
  * Represents a method template filter.
  * Represents a method template filter.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 class Twig_Filter_Method extends Twig_Filter
 class Twig_Filter_Method extends Twig_Filter
 {
 {
-    protected $extension, $method;
+    protected $extension;
+    protected $method;
 
 
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     {
     {
+        $options['callable'] = array($extension, $method);
+
         parent::__construct($options);
         parent::__construct($options);
 
 
         $this->extension = $extension;
         $this->extension = $extension;

+ 4 - 2
lib/twig/lib/Twig/Filter/Node.php → lib/twig/Filter/Node.php

@@ -12,8 +12,10 @@
 /**
 /**
  * Represents a template filter as a node.
  * Represents a template filter as a node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 class Twig_Filter_Node extends Twig_Filter
 class Twig_Filter_Node extends Twig_Filter
 {
 {

+ 23 - 0
lib/twig/FilterCallableInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a callable template filter.
+ *
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
+ */
+interface Twig_FilterCallableInterface
+{
+    public function getCallable();
+}

+ 42 - 0
lib/twig/FilterInterface.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a template filter.
+ *
+ * Use Twig_SimpleFilter instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
+ */
+interface Twig_FilterInterface
+{
+    /**
+     * Compiles a filter.
+     *
+     * @return string The PHP code for the filter
+     */
+    public function compile();
+
+    public function needsEnvironment();
+
+    public function needsContext();
+
+    public function getSafe(Twig_Node $filterArgs);
+
+    public function getPreservesSafety();
+
+    public function getPreEscape();
+
+    public function setArguments($arguments);
+
+    public function getArguments();
+}

+ 11 - 3
lib/twig/lib/Twig/Function.php → lib/twig/Function.php

@@ -12,10 +12,12 @@
 /**
 /**
  * Represents a template function.
  * Represents a template function.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
-abstract class Twig_Function implements Twig_FunctionInterface
+abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface
 {
 {
     protected $options;
     protected $options;
     protected $arguments = array();
     protected $arguments = array();
@@ -25,6 +27,7 @@ abstract class Twig_Function implements Twig_FunctionInterface
         $this->options = array_merge(array(
         $this->options = array_merge(array(
             'needs_environment' => false,
             'needs_environment' => false,
             'needs_context'     => false,
             'needs_context'     => false,
+            'callable'          => null,
         ), $options);
         ), $options);
     }
     }
 
 
@@ -60,4 +63,9 @@ abstract class Twig_Function implements Twig_FunctionInterface
 
 
         return array();
         return array();
     }
     }
+
+    public function getCallable()
+    {
+        return $this->options['callable'];
+    }
 }
 }

+ 6 - 2
lib/twig/lib/Twig/Function/Function.php → lib/twig/Function/Function.php

@@ -13,8 +13,10 @@
 /**
 /**
  * Represents a function template function.
  * Represents a function template function.
  *
  *
- * @package    twig
- * @author     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 class Twig_Function_Function extends Twig_Function
 class Twig_Function_Function extends Twig_Function
 {
 {
@@ -22,6 +24,8 @@ class Twig_Function_Function extends Twig_Function
 
 
     public function __construct($function, array $options = array())
     public function __construct($function, array $options = array())
     {
     {
+        $options['callable'] = $function;
+
         parent::__construct($options);
         parent::__construct($options);
 
 
         $this->function = $function;
         $this->function = $function;

+ 8 - 3
lib/twig/lib/Twig/Function/Method.php → lib/twig/Function/Method.php

@@ -13,15 +13,20 @@
 /**
 /**
  * Represents a method template function.
  * Represents a method template function.
  *
  *
- * @package    twig
- * @author     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 class Twig_Function_Method extends Twig_Function
 class Twig_Function_Method extends Twig_Function
 {
 {
-    protected $extension, $method;
+    protected $extension;
+    protected $method;
 
 
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array())
     {
     {
+        $options['callable'] = array($extension, $method);
+
         parent::__construct($options);
         parent::__construct($options);
 
 
         $this->extension = $extension;
         $this->extension = $extension;

+ 5 - 3
lib/twig/lib/Twig/Function/Node.php → lib/twig/Function/Node.php

@@ -12,10 +12,12 @@
 /**
 /**
  * Represents a template function as a node.
  * Represents a template function as a node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
-class Twig_Function_Node extends Twig_Filter
+class Twig_Function_Node extends Twig_Function
 {
 {
     protected $class;
     protected $class;
 
 

+ 23 - 0
lib/twig/FunctionCallableInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a callable template function.
+ *
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
+ */
+interface Twig_FunctionCallableInterface
+{
+    public function getCallable();
+}

+ 10 - 8
lib/twig/lib/Twig/FunctionInterface.php → lib/twig/FunctionInterface.php

@@ -13,8 +13,10 @@
 /**
 /**
  * Represents a template function.
  * Represents a template function.
  *
  *
- * @package    twig
- * @author     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * Use Twig_SimpleFunction instead.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 interface Twig_FunctionInterface
 interface Twig_FunctionInterface
 {
 {
@@ -23,15 +25,15 @@ interface Twig_FunctionInterface
      *
      *
      * @return string The PHP code for the function
      * @return string The PHP code for the function
      */
      */
-    function compile();
+    public function compile();
 
 
-    function needsEnvironment();
+    public function needsEnvironment();
 
 
-    function needsContext();
+    public function needsContext();
 
 
-    function getSafe(Twig_Node $filterArgs);
+    public function getSafe(Twig_Node $filterArgs);
 
 
-    function setArguments($arguments);
+    public function setArguments($arguments);
 
 
-    function getArguments();
+    public function getArguments();
 }
 }

+ 0 - 31
lib/twig/LICENSE

@@ -1,31 +0,0 @@
-Copyright (c) 2009 by the Twig Team, see AUTHORS for more details.
-
-Some rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-
-    * Redistributions in binary form must reproduce the above
-      copyright notice, this list of conditions and the following
-      disclaimer in the documentation and/or other materials provided
-      with the distribution.
-
-    * The names of the contributors may not be used to endorse or
-      promote products derived from this software without specific
-      prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 15 - 13
lib/twig/lib/Twig/Lexer.php → lib/twig/Lexer.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Lexes a template string.
  * Lexes a template string.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Lexer implements Twig_LexerInterface
 class Twig_Lexer implements Twig_LexerInterface
 {
 {
@@ -30,6 +29,9 @@ class Twig_Lexer implements Twig_LexerInterface
     protected $filename;
     protected $filename;
     protected $options;
     protected $options;
     protected $regexes;
     protected $regexes;
+    protected $position;
+    protected $positions;
+    protected $currentVarBlockLine;
 
 
     const STATE_DATA            = 0;
     const STATE_DATA            = 0;
     const STATE_BLOCK           = 1;
     const STATE_BLOCK           = 1;
@@ -59,10 +61,10 @@ class Twig_Lexer implements Twig_LexerInterface
         $this->regexes = array(
         $this->regexes = array(
             'lex_var'             => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A',
             'lex_var'             => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A',
             'lex_block'           => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A',
             'lex_block'           => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A',
-            'lex_raw_data'        => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*endraw\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s',
+            'lex_raw_data'        => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*(?:end%s)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s',
             'operator'            => $this->getOperatorRegex(),
             'operator'            => $this->getOperatorRegex(),
             'lex_comment'         => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s',
             'lex_comment'         => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s',
-            'lex_block_raw'       => '/\s*raw\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As',
+            'lex_block_raw'       => '/\s*(raw|verbatim)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As',
             'lex_block_line'      => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As',
             'lex_block_line'      => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As',
             'lex_tokens_start'    => '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s',
             'lex_tokens_start'    => '/('.preg_quote($this->options['tag_variable'][0], '/').'|'.preg_quote($this->options['tag_block'][0], '/').'|'.preg_quote($this->options['tag_comment'][0], '/').')('.preg_quote($this->options['whitespace_trim'], '/').')?/s',
             'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A',
             'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A',
@@ -73,8 +75,8 @@ class Twig_Lexer implements Twig_LexerInterface
     /**
     /**
      * Tokenizes a source code.
      * Tokenizes a source code.
      *
      *
-     * @param  string $code     The source code
-     * @param  string $filename A unique identifier for the source code
+     * @param string $code     The source code
+     * @param string $filename A unique identifier for the source code
      *
      *
      * @return Twig_TokenStream A token stream instance
      * @return Twig_TokenStream A token stream instance
      */
      */
@@ -176,7 +178,7 @@ class Twig_Lexer implements Twig_LexerInterface
                 // raw data?
                 // raw data?
                 if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) {
                 if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) {
                     $this->moveCursor($match[0]);
                     $this->moveCursor($match[0]);
-                    $this->lexRawData();
+                    $this->lexRawData($match[1]);
                 // {% line \d+ %}
                 // {% line \d+ %}
                 } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) {
                 } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) {
                     $this->moveCursor($match[0]);
                     $this->moveCursor($match[0]);
@@ -184,12 +186,14 @@ class Twig_Lexer implements Twig_LexerInterface
                 } else {
                 } else {
                     $this->pushToken(Twig_Token::BLOCK_START_TYPE);
                     $this->pushToken(Twig_Token::BLOCK_START_TYPE);
                     $this->pushState(self::STATE_BLOCK);
                     $this->pushState(self::STATE_BLOCK);
+                    $this->currentVarBlockLine = $this->lineno;
                 }
                 }
                 break;
                 break;
 
 
             case $this->options['tag_variable'][0]:
             case $this->options['tag_variable'][0]:
                 $this->pushToken(Twig_Token::VAR_START_TYPE);
                 $this->pushToken(Twig_Token::VAR_START_TYPE);
                 $this->pushState(self::STATE_VAR);
                 $this->pushState(self::STATE_VAR);
+                $this->currentVarBlockLine = $this->lineno;
                 break;
                 break;
         }
         }
     }
     }
@@ -223,7 +227,7 @@ class Twig_Lexer implements Twig_LexerInterface
             $this->moveCursor($match[0]);
             $this->moveCursor($match[0]);
 
 
             if ($this->cursor >= $this->end) {
             if ($this->cursor >= $this->end) {
-                throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable'));
+                throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable'), $this->currentVarBlockLine, $this->filename);
             }
             }
         }
         }
 
 
@@ -284,10 +288,10 @@ class Twig_Lexer implements Twig_LexerInterface
         }
         }
     }
     }
 
 
-    protected function lexRawData()
+    protected function lexRawData($tag)
     {
     {
-        if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) {
-            throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "block"'));
+        if (!preg_match(str_replace('%s', $tag, $this->regexes['lex_raw_data']), $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) {
+            throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s" block', $tag), $this->lineno, $this->filename);
         }
         }
 
 
         $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
         $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
@@ -330,8 +334,6 @@ class Twig_Lexer implements Twig_LexerInterface
 
 
             $this->popState();
             $this->popState();
             ++$this->cursor;
             ++$this->cursor;
-
-            return;
         }
         }
     }
     }
 
 

+ 5 - 5
lib/twig/lib/Twig/LexerInterface.php → lib/twig/LexerInterface.php

@@ -12,18 +12,18 @@
 /**
 /**
  * Interface implemented by lexer classes.
  * Interface implemented by lexer classes.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
  */
 interface Twig_LexerInterface
 interface Twig_LexerInterface
 {
 {
     /**
     /**
      * Tokenizes a source code.
      * Tokenizes a source code.
      *
      *
-     * @param  string $code     The source code
-     * @param  string $filename A unique identifier for the source code
+     * @param string $code     The source code
+     * @param string $filename A unique identifier for the source code
      *
      *
      * @return Twig_TokenStream A token stream instance
      * @return Twig_TokenStream A token stream instance
      */
      */
-    function tokenize($code, $filename = null);
+    public function tokenize($code, $filename = null);
 }
 }

+ 13 - 17
lib/twig/lib/Twig/Loader/Array.php → lib/twig/Loader/Array.php

@@ -17,10 +17,9 @@
  * source code of the template). If you don't want to see your cache grows out of
  * source code of the template). If you don't want to see your cache grows out of
  * control, you need to take care of clearing the old cache file by yourself.
  * control, you need to take care of clearing the old cache file by yourself.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
-class Twig_Loader_Array implements Twig_LoaderInterface
+class Twig_Loader_Array implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
 {
 {
     protected $templates;
     protected $templates;
 
 
@@ -51,11 +50,7 @@ class Twig_Loader_Array implements Twig_LoaderInterface
     }
     }
 
 
     /**
     /**
-     * Gets the source code of a template, given its name.
-     *
-     * @param  string $name The name of the template to load
-     *
-     * @return string The template source code
+     * {@inheritdoc}
      */
      */
     public function getSource($name)
     public function getSource($name)
     {
     {
@@ -68,11 +63,15 @@ class Twig_Loader_Array implements Twig_LoaderInterface
     }
     }
 
 
     /**
     /**
-     * Gets the cache key to use for the cache for a given template name.
-     *
-     * @param  string $name The name of the template to load
-     *
-     * @return string The cache key
+     * {@inheritdoc}
+     */
+    public function exists($name)
+    {
+        return isset($this->templates[(string) $name]);
+    }
+
+    /**
+     * {@inheritdoc}
      */
      */
     public function getCacheKey($name)
     public function getCacheKey($name)
     {
     {
@@ -85,10 +84,7 @@ class Twig_Loader_Array implements Twig_LoaderInterface
     }
     }
 
 
     /**
     /**
-     * Returns true if the template is still fresh.
-     *
-     * @param string    $name The template name
-     * @param timestamp $time The last modification time of the cached template
+     * {@inheritdoc}
      */
      */
     public function isFresh($name, $time)
     public function isFresh($name, $time)
     {
     {

+ 135 - 0
lib/twig/Loader/Chain.php

@@ -0,0 +1,135 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2011 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads templates from other loaders.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Loader_Chain implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
+{
+    private $hasSourceCache = array();
+    protected $loaders;
+
+    /**
+     * Constructor.
+     *
+     * @param Twig_LoaderInterface[] $loaders An array of loader instances
+     */
+    public function __construct(array $loaders = array())
+    {
+        $this->loaders = array();
+        foreach ($loaders as $loader) {
+            $this->addLoader($loader);
+        }
+    }
+
+    /**
+     * Adds a loader instance.
+     *
+     * @param Twig_LoaderInterface $loader A Loader instance
+     */
+    public function addLoader(Twig_LoaderInterface $loader)
+    {
+        $this->loaders[] = $loader;
+        $this->hasSourceCache = array();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSource($name)
+    {
+        $exceptions = array();
+        foreach ($this->loaders as $loader) {
+            if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) {
+                continue;
+            }
+
+            try {
+                return $loader->getSource($name);
+            } catch (Twig_Error_Loader $e) {
+                $exceptions[] = $e->getMessage();
+            }
+        }
+
+        throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(', ', $exceptions)));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function exists($name)
+    {
+        $name = (string) $name;
+
+        if (isset($this->hasSourceCache[$name])) {
+            return $this->hasSourceCache[$name];
+        }
+
+        foreach ($this->loaders as $loader) {
+            if ($loader instanceof Twig_ExistsLoaderInterface && $loader->exists($name)) {
+                return $this->hasSourceCache[$name] = true;
+            }
+
+            try {
+                $loader->getSource($name);
+
+                return $this->hasSourceCache[$name] = true;
+            } catch (Twig_Error_Loader $e) {
+            }
+        }
+
+        return $this->hasSourceCache[$name] = false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCacheKey($name)
+    {
+        $exceptions = array();
+        foreach ($this->loaders as $loader) {
+            if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) {
+                continue;
+            }
+
+            try {
+                return $loader->getCacheKey($name);
+            } catch (Twig_Error_Loader $e) {
+                $exceptions[] = get_class($loader).': '.$e->getMessage();
+            }
+        }
+
+        throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(' ', $exceptions)));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isFresh($name, $time)
+    {
+        $exceptions = array();
+        foreach ($this->loaders as $loader) {
+            if ($loader instanceof Twig_ExistsLoaderInterface && !$loader->exists($name)) {
+                continue;
+            }
+
+            try {
+                return $loader->isFresh($name, $time);
+            } catch (Twig_Error_Loader $e) {
+                $exceptions[] = get_class($loader).': '.$e->getMessage();
+            }
+        }
+
+        throw new Twig_Error_Loader(sprintf('Template "%s" is not defined (%s).', $name, implode(' ', $exceptions)));
+    }
+}

+ 220 - 0
lib/twig/Loader/Filesystem.php

@@ -0,0 +1,220 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Loads template from the filesystem.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
+{
+    protected $paths;
+    protected $cache;
+
+    /**
+     * Constructor.
+     *
+     * @param string|array $paths A path or an array of paths where to look for templates
+     */
+    public function __construct($paths)
+    {
+        $this->setPaths($paths);
+    }
+
+    /**
+     * Returns the paths to the templates.
+     *
+     * @param string $namespace A path namespace
+     *
+     * @return array The array of paths where to look for templates
+     */
+    public function getPaths($namespace = '__main__')
+    {
+        return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array();
+    }
+
+    /**
+     * Returns the path namespaces.
+     *
+     * The "__main__" namespace is always defined.
+     *
+     * @return array The array of defined namespaces
+     */
+    public function getNamespaces()
+    {
+        return array_keys($this->paths);
+    }
+
+    /**
+     * Sets the paths where templates are stored.
+     *
+     * @param string|array $paths     A path or an array of paths where to look for templates
+     * @param string       $namespace A path namespace
+     */
+    public function setPaths($paths, $namespace = '__main__')
+    {
+        if (!is_array($paths)) {
+            $paths = array($paths);
+        }
+
+        $this->paths[$namespace] = array();
+        foreach ($paths as $path) {
+            $this->addPath($path, $namespace);
+        }
+    }
+
+    /**
+     * Adds a path where templates are stored.
+     *
+     * @param string $path      A path where to look for templates
+     * @param string $namespace A path name
+     *
+     * @throws Twig_Error_Loader
+     */
+    public function addPath($path, $namespace = '__main__')
+    {
+        // invalidate the cache
+        $this->cache = array();
+
+        if (!is_dir($path)) {
+            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path));
+        }
+
+        $this->paths[$namespace][] = rtrim($path, '/\\');
+    }
+
+    /**
+     * Prepends a path where templates are stored.
+     *
+     * @param string $path      A path where to look for templates
+     * @param string $namespace A path name
+     *
+     * @throws Twig_Error_Loader
+     */
+    public function prependPath($path, $namespace = '__main__')
+    {
+        // invalidate the cache
+        $this->cache = array();
+
+        if (!is_dir($path)) {
+            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path));
+        }
+
+        $path = rtrim($path, '/\\');
+
+        if (!isset($this->paths[$namespace])) {
+            $this->paths[$namespace][] = $path;
+        } else {
+            array_unshift($this->paths[$namespace], $path);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSource($name)
+    {
+        return file_get_contents($this->findTemplate($name));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCacheKey($name)
+    {
+        return $this->findTemplate($name);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function exists($name)
+    {
+        $name = (string) $name;
+        if (isset($this->cache[$name])) {
+            return true;
+        }
+
+        try {
+            $this->findTemplate($name);
+
+            return true;
+        } catch (Twig_Error_Loader $exception) {
+            return false;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isFresh($name, $time)
+    {
+        return filemtime($this->findTemplate($name)) <= $time;
+    }
+
+    protected function findTemplate($name)
+    {
+        $name = (string) $name;
+
+        // normalize name
+        $name = preg_replace('#/{2,}#', '/', strtr($name, '\\', '/'));
+
+        if (isset($this->cache[$name])) {
+            return $this->cache[$name];
+        }
+
+        $this->validateName($name);
+
+        $namespace = '__main__';
+        if (isset($name[0]) && '@' == $name[0]) {
+            if (false === $pos = strpos($name, '/')) {
+                throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
+            }
+
+            $namespace = substr($name, 1, $pos - 1);
+
+            $name = substr($name, $pos + 1);
+        }
+
+        if (!isset($this->paths[$namespace])) {
+            throw new Twig_Error_Loader(sprintf('There are no registered paths for namespace "%s".', $namespace));
+        }
+
+        foreach ($this->paths[$namespace] as $path) {
+            if (is_file($path.'/'.$name)) {
+                return $this->cache[$name] = $path.'/'.$name;
+            }
+        }
+
+        throw new Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])));
+    }
+
+    protected function validateName($name)
+    {
+        if (false !== strpos($name, "\0")) {
+            throw new Twig_Error_Loader('A template name cannot contain NUL bytes.');
+        }
+
+        $parts = explode('/', $name);
+        $level = 0;
+        foreach ($parts as $part) {
+            if ('..' === $part) {
+                --$level;
+            } elseif ('.' !== $part) {
+                ++$level;
+            }
+
+            if ($level < 0) {
+                throw new Twig_Error_Loader(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
+            }
+        }
+    }
+}

+ 17 - 17
lib/twig/lib/Twig/Loader/String.php → lib/twig/Loader/String.php

@@ -12,22 +12,21 @@
 /**
 /**
  * Loads a template from a string.
  * Loads a template from a string.
  *
  *
+ * This loader should only be used for unit testing as it has many limitations
+ * (for instance, the include or extends tag does not make any sense for a string
+ * loader).
+ *
  * When using this loader with a cache mechanism, you should know that a new cache
  * When using this loader with a cache mechanism, you should know that a new cache
  * key is generated each time a template content "changes" (the cache key being the
  * key is generated each time a template content "changes" (the cache key being the
  * source code of the template). If you don't want to see your cache grows out of
  * source code of the template). If you don't want to see your cache grows out of
  * control, you need to take care of clearing the old cache file by yourself.
  * control, you need to take care of clearing the old cache file by yourself.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
-class Twig_Loader_String implements Twig_LoaderInterface
+class Twig_Loader_String implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
 {
 {
     /**
     /**
-     * Gets the source code of a template, given its name.
-     *
-     * @param  string $name The name of the template to load
-     *
-     * @return string The template source code
+     * {@inheritdoc}
      */
      */
     public function getSource($name)
     public function getSource($name)
     {
     {
@@ -35,11 +34,15 @@ class Twig_Loader_String implements Twig_LoaderInterface
     }
     }
 
 
     /**
     /**
-     * Gets the cache key to use for the cache for a given template name.
-     *
-     * @param  string $name The name of the template to load
-     *
-     * @return string The cache key
+     * {@inheritdoc}
+     */
+    public function exists($name)
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
      */
      */
     public function getCacheKey($name)
     public function getCacheKey($name)
     {
     {
@@ -47,10 +50,7 @@ class Twig_Loader_String implements Twig_LoaderInterface
     }
     }
 
 
     /**
     /**
-     * Returns true if the template is still fresh.
-     *
-     * @param string    $name The template name
-     * @param timestamp $time The last modification time of the cached template
+     * {@inheritdoc}
      */
      */
     public function isFresh($name, $time)
     public function isFresh($name, $time)
     {
     {

+ 6 - 7
lib/twig/lib/Twig/LoaderInterface.php → lib/twig/LoaderInterface.php

@@ -12,32 +12,31 @@
 /**
 /**
  * Interface all loaders must implement.
  * Interface all loaders must implement.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 interface Twig_LoaderInterface
 interface Twig_LoaderInterface
 {
 {
     /**
     /**
      * Gets the source code of a template, given its name.
      * Gets the source code of a template, given its name.
      *
      *
-     * @param  string $name The name of the template to load
+     * @param string $name The name of the template to load
      *
      *
      * @return string The template source code
      * @return string The template source code
      *
      *
      * @throws Twig_Error_Loader When $name is not found
      * @throws Twig_Error_Loader When $name is not found
      */
      */
-    function getSource($name);
+    public function getSource($name);
 
 
     /**
     /**
      * Gets the cache key to use for the cache for a given template name.
      * Gets the cache key to use for the cache for a given template name.
      *
      *
-     * @param  string $name The name of the template to load
+     * @param string $name The name of the template to load
      *
      *
      * @return string The cache key
      * @return string The cache key
      *
      *
      * @throws Twig_Error_Loader When $name is not found
      * @throws Twig_Error_Loader When $name is not found
      */
      */
-    function getCacheKey($name);
+    public function getCacheKey($name);
 
 
     /**
     /**
      * Returns true if the template is still fresh.
      * Returns true if the template is still fresh.
@@ -49,5 +48,5 @@ interface Twig_LoaderInterface
      *
      *
      * @throws Twig_Error_Loader When $name is not found
      * @throws Twig_Error_Loader When $name is not found
      */
      */
-    function isFresh($name, $time);
+    public function isFresh($name, $time);
 }
 }

+ 1 - 2
lib/twig/lib/Twig/Markup.php → lib/twig/Markup.php

@@ -12,8 +12,7 @@
 /**
 /**
  * Marks a content as safe.
  * Marks a content as safe.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Markup implements Countable
 class Twig_Markup implements Countable
 {
 {

+ 4 - 5
lib/twig/lib/Twig/Node.php → lib/twig/Node.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Represents a node in the AST.
  * Represents a node in the AST.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node implements Twig_NodeInterface
 class Twig_Node implements Twig_NodeInterface
 {
 {
@@ -134,12 +133,12 @@ class Twig_Node implements Twig_NodeInterface
      *
      *
      * @param  string The attribute name
      * @param  string The attribute name
      *
      *
-     * @return mixed  The attribute value
+     * @return mixed The attribute value
      */
      */
     public function getAttribute($name)
     public function getAttribute($name)
     {
     {
         if (!array_key_exists($name, $this->attributes)) {
         if (!array_key_exists($name, $this->attributes)) {
-            throw new Twig_Error_Runtime(sprintf('Attribute "%s" does not exist for Node "%s".', $name, get_class($this)));
+            throw new LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, get_class($this)));
         }
         }
 
 
         return $this->attributes[$name];
         return $this->attributes[$name];
@@ -188,7 +187,7 @@ class Twig_Node implements Twig_NodeInterface
     public function getNode($name)
     public function getNode($name)
     {
     {
         if (!array_key_exists($name, $this->nodes)) {
         if (!array_key_exists($name, $this->nodes)) {
-            throw new Twig_Error_Runtime(sprintf('Node "%s" does not exist for Node "%s".', $name, get_class($this)));
+            throw new LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, get_class($this)));
         }
         }
 
 
         return $this->nodes[$name];
         return $this->nodes[$name];

+ 1 - 2
lib/twig/lib/Twig/Node/AutoEscape.php → lib/twig/Node/AutoEscape.php

@@ -18,8 +18,7 @@
  *
  *
  * If autoescaping is disabled, then the value is false.
  * If autoescaping is disabled, then the value is false.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_AutoEscape extends Twig_Node
 class Twig_Node_AutoEscape extends Twig_Node
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Block.php → lib/twig/Node/Block.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Represents a block node.
  * Represents a block node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Block extends Twig_Node
 class Twig_Node_Block extends Twig_Node
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/BlockReference.php → lib/twig/Node/BlockReference.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Represents a block call node.
  * Represents a block call node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_BlockReference extends Twig_Node implements Twig_NodeOutputInterface
 class Twig_Node_BlockReference extends Twig_Node implements Twig_NodeOutputInterface
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Body.php → lib/twig/Node/Body.php

@@ -12,8 +12,7 @@
 /**
 /**
  * Represents a body node.
  * Represents a body node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Body extends Twig_Node
 class Twig_Node_Body extends Twig_Node
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Do.php → lib/twig/Node/Do.php

@@ -12,8 +12,7 @@
 /**
 /**
  * Represents a do node.
  * Represents a do node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Do extends Twig_Node
 class Twig_Node_Do extends Twig_Node
 {
 {

+ 38 - 0
lib/twig/Node/Embed.php

@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents an embed node.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_Node_Embed extends Twig_Node_Include
+{
+    // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
+    public function __construct($filename, $index, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null)
+    {
+        parent::__construct(new Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
+
+        $this->setAttribute('filename', $filename);
+        $this->setAttribute('index', $index);
+    }
+
+    protected function addGetTemplate(Twig_Compiler $compiler)
+    {
+        $compiler
+            ->write("\$this->env->loadTemplate(")
+            ->string($this->getAttribute('filename'))
+            ->raw(', ')
+            ->string($this->getAttribute('index'))
+            ->raw(")")
+        ;
+    }
+}

+ 1 - 2
lib/twig/lib/Twig/Node/Expression.php → lib/twig/Node/Expression.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Abstract class for all nodes that represents an expression.
  * Abstract class for all nodes that represents an expression.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 abstract class Twig_Node_Expression extends Twig_Node
 abstract class Twig_Node_Expression extends Twig_Node
 {
 {

+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Array.php → lib/twig/Node/Expression/Array.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/AssignName.php → lib/twig/Node/Expression/AssignName.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary.php → lib/twig/Node/Expression/Binary.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Add.php → lib/twig/Node/Expression/Binary/Add.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/And.php → lib/twig/Node/Expression/Binary/And.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/BitwiseAnd.php → lib/twig/Node/Expression/Binary/BitwiseAnd.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/BitwiseOr.php → lib/twig/Node/Expression/Binary/BitwiseOr.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/BitwiseXor.php → lib/twig/Node/Expression/Binary/BitwiseXor.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Concat.php → lib/twig/Node/Expression/Binary/Concat.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Div.php → lib/twig/Node/Expression/Binary/Div.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Equal.php → lib/twig/Node/Expression/Binary/Equal.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/FloorDiv.php → lib/twig/Node/Expression/Binary/FloorDiv.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Greater.php → lib/twig/Node/Expression/Binary/Greater.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/GreaterEqual.php → lib/twig/Node/Expression/Binary/GreaterEqual.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/In.php → lib/twig/Node/Expression/Binary/In.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Less.php → lib/twig/Node/Expression/Binary/Less.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/LessEqual.php → lib/twig/Node/Expression/Binary/LessEqual.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Mod.php → lib/twig/Node/Expression/Binary/Mod.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Mul.php → lib/twig/Node/Expression/Binary/Mul.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/NotEqual.php → lib/twig/Node/Expression/Binary/NotEqual.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/NotIn.php → lib/twig/Node/Expression/Binary/NotIn.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Or.php → lib/twig/Node/Expression/Binary/Or.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Power.php → lib/twig/Node/Expression/Binary/Power.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Range.php → lib/twig/Node/Expression/Binary/Range.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Binary/Sub.php → lib/twig/Node/Expression/Binary/Sub.php


+ 1 - 2
lib/twig/lib/Twig/Node/Expression/BlockReference.php → lib/twig/Node/Expression/BlockReference.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Represents a block call node.
  * Represents a block call node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_BlockReference extends Twig_Node_Expression
 class Twig_Node_Expression_BlockReference extends Twig_Node_Expression
 {
 {

+ 171 - 0
lib/twig/Node/Expression/Call.php

@@ -0,0 +1,171 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
+{
+    protected function compileCallable(Twig_Compiler $compiler)
+    {
+        $callable = $this->getAttribute('callable');
+
+        $closingParenthesis = false;
+        if ($callable) {
+            if (is_string($callable)) {
+                $compiler->raw($callable);
+            } elseif (is_array($callable) && $callable[0] instanceof Twig_ExtensionInterface) {
+                $compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', $callable[0]->getName(), $callable[1]));
+            } else {
+                $type = ucfirst($this->getAttribute('type'));
+                $compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', $type, $this->getAttribute('name')));
+                $closingParenthesis = true;
+            }
+        } else {
+            $compiler->raw($this->getAttribute('thing')->compile());
+        }
+
+        $this->compileArguments($compiler);
+
+        if ($closingParenthesis) {
+            $compiler->raw(')');
+        }
+    }
+
+    protected function compileArguments(Twig_Compiler $compiler)
+    {
+        $compiler->raw('(');
+
+        $first = true;
+
+        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
+            $compiler->raw('$this->env');
+            $first = false;
+        }
+
+        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
+            if (!$first) {
+                $compiler->raw(', ');
+            }
+            $compiler->raw('$context');
+            $first = false;
+        }
+
+        if ($this->hasAttribute('arguments')) {
+            foreach ($this->getAttribute('arguments') as $argument) {
+                if (!$first) {
+                    $compiler->raw(', ');
+                }
+                $compiler->string($argument);
+                $first = false;
+            }
+        }
+
+        if ($this->hasNode('node')) {
+            if (!$first) {
+                $compiler->raw(', ');
+            }
+            $compiler->subcompile($this->getNode('node'));
+            $first = false;
+        }
+
+        if ($this->hasNode('arguments') && null !== $this->getNode('arguments')) {
+            $callable = $this->hasAttribute('callable') ? $this->getAttribute('callable') : null;
+
+            $arguments = $this->getArguments($callable, $this->getNode('arguments'));
+
+            foreach ($arguments as $node) {
+                if (!$first) {
+                    $compiler->raw(', ');
+                }
+                $compiler->subcompile($node);
+                $first = false;
+            }
+        }
+
+        $compiler->raw(')');
+    }
+
+    protected function getArguments($callable, $arguments)
+    {
+        $parameters = array();
+        $named = false;
+        foreach ($arguments as $name => $node) {
+            if (!is_int($name)) {
+                $named = true;
+                $name = $this->normalizeName($name);
+            }
+            $parameters[$name] = $node;
+        }
+
+        if (!$named) {
+            return $parameters;
+        }
+
+        if (!$callable) {
+            throw new LogicException(sprintf('Named arguments are not supported for %s "%s".', $this->getAttribute('type'), $this->getAttribute('name')));
+        }
+
+        // manage named arguments
+        if (is_array($callable)) {
+            $r = new ReflectionMethod($callable[0], $callable[1]);
+        } elseif (is_object($callable) && !$callable instanceof Closure) {
+            $r = new ReflectionObject($callable);
+            $r = $r->getMethod('__invoke');
+        } else {
+            $r = new ReflectionFunction($callable);
+        }
+
+        $definition = $r->getParameters();
+        if ($this->hasNode('node')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
+            array_shift($definition);
+        }
+        if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
+            foreach ($this->getAttribute('arguments') as $argument) {
+                array_shift($definition);
+            }
+        }
+
+        $arguments = array();
+        $pos = 0;
+        foreach ($definition as $param) {
+            $name = $this->normalizeName($param->name);
+
+            if (array_key_exists($name, $parameters)) {
+                $arguments[] = $parameters[$name];
+                unset($parameters[$name]);
+            } elseif (array_key_exists($pos, $parameters)) {
+                $arguments[] = $parameters[$pos];
+                unset($parameters[$pos]);
+                ++$pos;
+            } elseif ($param->isDefaultValueAvailable()) {
+                $arguments[] = new Twig_Node_Expression_Constant($param->getDefaultValue(), -1);
+            } elseif ($param->isOptional()) {
+                break;
+            } else {
+                throw new Twig_Error_Syntax(sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+            }
+        }
+
+        foreach (array_keys($parameters) as $name) {
+            throw new Twig_Error_Syntax(sprintf('Unknown argument "%s" for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+        }
+
+        return $arguments;
+    }
+
+    protected function normalizeName($name)
+    {
+        return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name));
+    }
+}

+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Conditional.php → lib/twig/Node/Expression/Conditional.php


+ 0 - 0
lib/twig/lib/Twig/Node/Expression/Constant.php → lib/twig/Node/Expression/Constant.php


+ 1 - 2
lib/twig/lib/Twig/Node/Expression/ExtensionReference.php → lib/twig/Node/Expression/ExtensionReference.php

@@ -12,8 +12,7 @@
 /**
 /**
  * Represents an extension call node.
  * Represents an extension call node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_ExtensionReference extends Twig_Node_Expression
 class Twig_Node_Expression_ExtensionReference extends Twig_Node_Expression
 {
 {

+ 36 - 0
lib/twig/Node/Expression/Filter.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2009 Armin Ronacher
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call
+{
+    public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null)
+    {
+        parent::__construct(array('node' => $node, 'filter' => $filterName, 'arguments' => $arguments), array(), $lineno, $tag);
+    }
+
+    public function compile(Twig_Compiler $compiler)
+    {
+        $name = $this->getNode('filter')->getAttribute('value');
+        $filter = $compiler->getEnvironment()->getFilter($name);
+
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'filter');
+        $this->setAttribute('thing', $filter);
+        $this->setAttribute('needs_environment', $filter->needsEnvironment());
+        $this->setAttribute('needs_context', $filter->needsContext());
+        $this->setAttribute('arguments', $filter->getArguments());
+        if ($filter instanceof Twig_FilterCallableInterface || $filter instanceof Twig_SimpleFilter) {
+            $this->setAttribute('callable', $filter->getCallable());
+        }
+
+        $this->compileCallable($compiler);
+    }
+}

+ 2 - 3
lib/twig/lib/Twig/Node/Expression/Filter/Default.php → lib/twig/Node/Expression/Filter/Default.php

@@ -16,14 +16,13 @@
  *  {{ var.foo|default('foo item on var is not defined') }}
  *  {{ var.foo|default('foo item on var is not defined') }}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Filter_Default extends Twig_Node_Expression_Filter
 class Twig_Node_Expression_Filter_Default extends Twig_Node_Expression_Filter
 {
 {
     public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null)
     public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null)
     {
     {
-        $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('_default', $node->getLine()), $arguments, $node->getLine());
+        $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('default', $node->getLine()), $arguments, $node->getLine());
 
 
         if ('default' === $filterName->getAttribute('value') && ($node instanceof Twig_Node_Expression_Name || $node instanceof Twig_Node_Expression_GetAttr)) {
         if ('default' === $filterName->getAttribute('value') && ($node instanceof Twig_Node_Expression_Name || $node instanceof Twig_Node_Expression_GetAttr)) {
             $test = new Twig_Node_Expression_Test_Defined(clone $node, 'defined', new Twig_Node(), $node->getLine());
             $test = new Twig_Node_Expression_Test_Defined(clone $node, 'defined', new Twig_Node(), $node->getLine());

+ 35 - 0
lib/twig/Node/Expression/Function.php

@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Function extends Twig_Node_Expression_Call
+{
+    public function __construct($name, Twig_NodeInterface $arguments, $lineno)
+    {
+        parent::__construct(array('arguments' => $arguments), array('name' => $name), $lineno);
+    }
+
+    public function compile(Twig_Compiler $compiler)
+    {
+        $name = $this->getAttribute('name');
+        $function = $compiler->getEnvironment()->getFunction($name);
+
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'function');
+        $this->setAttribute('thing', $function);
+        $this->setAttribute('needs_environment', $function->needsEnvironment());
+        $this->setAttribute('needs_context', $function->needsContext());
+        $this->setAttribute('arguments', $function->getArguments());
+        if ($function instanceof Twig_FunctionCallableInterface || $function instanceof Twig_SimpleFunction) {
+            $this->setAttribute('callable', $function->getCallable());
+        }
+
+        $this->compileCallable($compiler);
+    }
+}

+ 2 - 2
lib/twig/lib/Twig/Node/Expression/GetAttr.php → lib/twig/Node/Expression/GetAttr.php

@@ -13,12 +13,12 @@ class Twig_Node_Expression_GetAttr extends Twig_Node_Expression
 {
 {
     public function __construct(Twig_Node_Expression $node, Twig_Node_Expression $attribute, Twig_Node_Expression_Array $arguments, $type, $lineno)
     public function __construct(Twig_Node_Expression $node, Twig_Node_Expression $attribute, Twig_Node_Expression_Array $arguments, $type, $lineno)
     {
     {
-        parent::__construct(array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), array('type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false), $lineno);
+        parent::__construct(array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), array('type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'disable_c_ext' => false), $lineno);
     }
     }
 
 
     public function compile(Twig_Compiler $compiler)
     public function compile(Twig_Compiler $compiler)
     {
     {
-        if (function_exists('twig_template_get_attributes')) {
+        if (function_exists('twig_template_get_attributes') && !$this->getAttribute('disable_c_ext')) {
             $compiler->raw('twig_template_get_attributes($this, ');
             $compiler->raw('twig_template_get_attributes($this, ');
         } else {
         } else {
             $compiler->raw('$this->getAttribute(');
             $compiler->raw('$this->getAttribute(');

+ 4 - 0
lib/twig/lib/Twig/Node/Expression/MethodCall.php → lib/twig/Node/Expression/MethodCall.php

@@ -13,6 +13,10 @@ class Twig_Node_Expression_MethodCall extends Twig_Node_Expression
     public function __construct(Twig_Node_Expression $node, $method, Twig_Node_Expression_Array $arguments, $lineno)
     public function __construct(Twig_Node_Expression $node, $method, Twig_Node_Expression_Array $arguments, $lineno)
     {
     {
         parent::__construct(array('node' => $node, 'arguments' => $arguments), array('method' => $method, 'safe' => false), $lineno);
         parent::__construct(array('node' => $node, 'arguments' => $arguments), array('method' => $method, 'safe' => false), $lineno);
+
+        if ($node instanceof Twig_Node_Expression_Name) {
+            $node->setAttribute('always_defined', true);
+        }
     }
     }
 
 
     public function compile(Twig_Compiler $compiler)
     public function compile(Twig_Compiler $compiler)

+ 15 - 3
lib/twig/lib/Twig/Node/Expression/Name.php → lib/twig/Node/Expression/Name.php

@@ -19,7 +19,7 @@ class Twig_Node_Expression_Name extends Twig_Node_Expression
 
 
     public function __construct($name, $lineno)
     public function __construct($name, $lineno)
     {
     {
-        parent::__construct(array(), array('name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false), $lineno);
+        parent::__construct(array(), array('name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false), $lineno);
     }
     }
 
 
     public function compile(Twig_Compiler $compiler)
     public function compile(Twig_Compiler $compiler)
@@ -34,19 +34,31 @@ class Twig_Node_Expression_Name extends Twig_Node_Expression
             }
             }
         } elseif ($this->isSpecial()) {
         } elseif ($this->isSpecial()) {
             $compiler->raw($this->specialVars[$name]);
             $compiler->raw($this->specialVars[$name]);
+        } elseif ($this->getAttribute('always_defined')) {
+            $compiler
+                ->raw('$context[')
+                ->string($name)
+                ->raw(']')
+            ;
         } else {
         } else {
             // remove the non-PHP 5.4 version when PHP 5.3 support is dropped
             // remove the non-PHP 5.4 version when PHP 5.3 support is dropped
             // as the non-optimized version is just a workaround for slow ternary operator
             // as the non-optimized version is just a workaround for slow ternary operator
             // when the context has a lot of variables
             // when the context has a lot of variables
-            if (version_compare(phpversion(), '5.4.0RC1', '>=') && ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables())) {
+            if (version_compare(phpversion(), '5.4.0RC1', '>=')) {
                 // PHP 5.4 ternary operator performance was optimized
                 // PHP 5.4 ternary operator performance was optimized
                 $compiler
                 $compiler
                     ->raw('(isset($context[')
                     ->raw('(isset($context[')
                     ->string($name)
                     ->string($name)
                     ->raw(']) ? $context[')
                     ->raw(']) ? $context[')
                     ->string($name)
                     ->string($name)
-                    ->raw('] : null)')
+                    ->raw('] : ')
                 ;
                 ;
+
+                if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
+                    $compiler->raw('null)');
+                } else {
+                    $compiler->raw('$this->getContext($context, ')->string($name)->raw('))');
+                }
             } else {
             } else {
                 $compiler
                 $compiler
                     ->raw('$this->getContext($context, ')
                     ->raw('$this->getContext($context, ')

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Parent.php → lib/twig/Node/Expression/Parent.php

@@ -13,8 +13,7 @@
 /**
 /**
  * Represents a parent node.
  * Represents a parent node.
  *
  *
- * @package    twig
- * @author     Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Parent extends Twig_Node_Expression
 class Twig_Node_Expression_Parent extends Twig_Node_Expression
 {
 {

+ 5 - 1
lib/twig/lib/Twig/Node/Expression/TempName.php → lib/twig/Node/Expression/TempName.php

@@ -17,6 +17,10 @@ class Twig_Node_Expression_TempName extends Twig_Node_Expression
 
 
     public function compile(Twig_Compiler $compiler)
     public function compile(Twig_Compiler $compiler)
     {
     {
-        $compiler->raw('$_')->raw($this->getAttribute('name'))->raw('_');
+        $compiler
+            ->raw('$_')
+            ->raw($this->getAttribute('name'))
+            ->raw('_')
+        ;
     }
     }
 }
 }

+ 32 - 0
lib/twig/Node/Expression/Test.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Node_Expression_Test extends Twig_Node_Expression_Call
+{
+    public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno)
+    {
+        parent::__construct(array('node' => $node, 'arguments' => $arguments), array('name' => $name), $lineno);
+    }
+
+    public function compile(Twig_Compiler $compiler)
+    {
+        $name = $this->getAttribute('name');
+        $test = $compiler->getEnvironment()->getTest($name);
+
+        $this->setAttribute('name', $name);
+        $this->setAttribute('type', 'test');
+        $this->setAttribute('thing', $test);
+        if ($test instanceof Twig_TestCallableInterface || $test instanceof Twig_SimpleTest) {
+            $this->setAttribute('callable', $test->getCallable());
+        }
+
+        $this->compileCallable($compiler);
+    }
+}

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Test/Constant.php → lib/twig/Node/Expression/Test/Constant.php

@@ -18,8 +18,7 @@
  *  {% endif %}
  *  {% endif %}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Constant extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Constant extends Twig_Node_Expression_Test
 {
 {

+ 2 - 3
lib/twig/lib/Twig/Node/Expression/Test/Defined.php → lib/twig/Node/Expression/Test/Defined.php

@@ -19,8 +19,7 @@
  * {% endif %}
  * {% endif %}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test
 {
 {
@@ -35,7 +34,7 @@ class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test
 
 
             $this->changeIgnoreStrictCheck($node);
             $this->changeIgnoreStrictCheck($node);
         } else {
         } else {
-            throw new Twig_Error_Syntax('The "defined" test only works with simple variables', $this->getLine());
+            throw new Twig_Error_Syntax('The "defined" test only works with simple variables', $this->getLine(), $compiler->getFilename());
         }
         }
     }
     }
 
 

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Test/Divisibleby.php → lib/twig/Node/Expression/Test/Divisibleby.php

@@ -16,8 +16,7 @@
  *  {% if loop.index is divisibleby(3) %}
  *  {% if loop.index is divisibleby(3) %}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Divisibleby extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Divisibleby extends Twig_Node_Expression_Test
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Test/Even.php → lib/twig/Node/Expression/Test/Even.php

@@ -16,8 +16,7 @@
  *  {{ var is even }}
  *  {{ var is even }}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Even extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Even extends Twig_Node_Expression_Test
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Test/Null.php → lib/twig/Node/Expression/Test/Null.php

@@ -16,8 +16,7 @@
  *  {{ var is none }}
  *  {{ var is none }}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Null extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Null extends Twig_Node_Expression_Test
 {
 {

+ 1 - 2
lib/twig/lib/Twig/Node/Expression/Test/Odd.php → lib/twig/Node/Expression/Test/Odd.php

@@ -16,8 +16,7 @@
  *  {{ var is odd }}
  *  {{ var is odd }}
  * </pre>
  * </pre>
  *
  *
- * @package twig
- * @author  Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
  */
  */
 class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test
 class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test
 {
 {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است