Skip to content

Commit

Permalink
Merge branch 'release/0.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
bobthecow committed Dec 8, 2010
2 parents a39c6f6 + f2d677e commit a448453
Show file tree
Hide file tree
Showing 36 changed files with 2,595 additions and 153 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test/spec"]
path = test/spec
url = http://github.com/mustache/spec.git
228 changes: 169 additions & 59 deletions Mustache.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
*/
class Mustache {

public $_otag = '{{';
public $_ctag = '}}';

/**
* Should this Mustache throw exceptions when it finds unexpected tags?
*
Expand Down Expand Up @@ -85,6 +82,15 @@ class Mustache {
*/
const PRAGMA_UNESCAPED = 'UNESCAPED';

/**
* Constants used for section and tag RegEx
*/
const SECTION_TYPES = '\^#\/';
const TAG_TYPES = '#\^\/=!<>\\{&';

public $_otag = '{{';
public $_ctag = '}}';

protected $_tagRegEx;

protected $_template = '';
Expand Down Expand Up @@ -191,7 +197,7 @@ public function __toString() {
* @return string Rendered Mustache template.
*/
protected function _renderTemplate($template) {
$template = $this->_renderSection($template);
$template = $this->_renderSections($template);
return $this->_renderTags($template);
}

Expand All @@ -202,18 +208,9 @@ protected function _renderTemplate($template) {
* @param string $template
* @return string
*/
protected function _renderSection($template) {
$otag = preg_quote($this->_otag, '/');
$ctag = preg_quote($this->_ctag, '/');
$regex = '/' . $otag . '(\\^|\\#)\\s*(.+?)\\s*' . $ctag . '\\s*([\\s\\S]+?)' . $otag . '\\/\\s*\\2\\s*' . $ctag . '\\s*/ms';

$matches = array();
while (preg_match($regex, $template, $matches, PREG_OFFSET_CAPTURE)) {
$section = $matches[0][0];
$offset = $matches[0][1];
$type = $matches[1][0];
$tag_name = trim($matches[2][0]);
$content = $matches[3][0];
protected function _renderSections($template) {
while ($section_data = $this->_findSection($template)) {
list($section, $offset, $type, $tag_name, $content) = $section_data;

$replace = '';
$val = $this->_getVariable($tag_name);
Expand Down Expand Up @@ -267,6 +264,102 @@ protected function _renderSection($template) {
return $template;
}

/**
* Prepare a section RegEx string for the given opening/closing tags.
*
* @access protected
* @param string $otag
* @param string $ctag
* @return string
*/
protected function _prepareSectionRegEx($otag, $ctag) {
return sprintf(
'/(?:(?<=\\n)[ \\t]*)?%s(?<type>[%s])(?<tag_name>.+?)%s\\n?/s',
preg_quote($otag, '/'),
self::SECTION_TYPES,
preg_quote($ctag, '/')
);
}

/**
* Extract a section from $template.
*
* This is a helper function to find sections needed by _renderSections.
*
* @access protected
* @param string $template
* @return array $section, $offset, $type, $tag_name and $content
*/
protected function _findSection($template) {
$regEx = $this->_prepareSectionRegEx($this->_otag, $this->_ctag);

$section_start = null;
$section_type = null;
$content_start = null;

$search_offset = 0;

$section_stack = array();
$matches = array();
while (preg_match($regEx, $template, $matches, PREG_OFFSET_CAPTURE, $search_offset)) {

$match = $matches[0][0];
$offset = $matches[0][1];
$type = $matches['type'][0];
$tag_name = trim($matches['tag_name'][0]);

$search_offset = $offset + strlen($match);

switch ($type) {
case '^':
case '#':
if (empty($section_stack)) {
$section_start = $offset;
$section_type = $type;
$content_start = $search_offset;
}
array_push($section_stack, $tag_name);
break;
case '/':
if (empty($section_stack) || ($tag_name !== array_pop($section_stack))) {
if ($this->_throwsException(MustacheException::UNEXPECTED_CLOSE_SECTION)) {
throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION);
}
}

if (empty($section_stack)) {
$section = substr($template, $section_start, $search_offset - $section_start);
$content = substr($template, $content_start, $offset - $content_start);

return array($section, $section_start, $section_type, $tag_name, $content);
}
break;
}
}

if (!empty($section_stack)) {
if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) {
throw new MustacheException('Unclosed section: ' . $section_stack[0], MustacheException::UNCLOSED_SECTION);
}
}
}

/**
* Prepare a pragma RegEx for the given opening/closing tags.
*
* @access protected
* @param string $otag
* @param string $ctag
* @return string
*/
protected function _preparePragmaRegEx($otag, $ctag) {
return sprintf(
'/%s%%\\s*(?<pragma_name>[\\w_-]+)(?<options_string>(?: [\\w]+=[\\w]+)*)\\s*%s\\n?/s',
preg_quote($otag, '/'),
preg_quote($ctag, '/')
);
}

/**
* Initialize pragmas and remove all pragma tags.
*
Expand All @@ -282,10 +375,8 @@ protected function _renderPragmas($template) {
return $template;
}

$otag = preg_quote($this->_otag, '/');
$ctag = preg_quote($this->_ctag, '/');
$regex = '/' . $otag . '%\\s*([\\w_-]+)((?: [\\w]+=[\\w]+)*)\\s*' . $ctag . '\\n?/s';
return preg_replace_callback($regex, array($this, '_renderPragma'), $template);
$regEx = $this->_preparePragmaRegEx($this->_otag, $this->_ctag);
return preg_replace_callback($regEx, array($this, '_renderPragma'), $template);
}

/**
Expand All @@ -298,8 +389,8 @@ protected function _renderPragmas($template) {
*/
protected function _renderPragma($matches) {
$pragma = $matches[0];
$pragma_name = $matches[1];
$options_string = $matches[2];
$pragma_name = $matches['pragma_name'];
$options_string = $matches['options_string'];

if (!in_array($pragma_name, $this->_pragmasImplemented)) {
throw new MustacheException('Unknown pragma: ' . $pragma_name, MustacheException::UNKNOWN_PRAGMA);
Expand All @@ -308,7 +399,7 @@ protected function _renderPragma($matches) {
$options = array();
foreach (explode(' ', trim($options_string)) as $o) {
if ($p = trim($o)) {
$p = explode('=', trim($p));
$p = explode('=', $p);
$options[$p[0]] = $p[1];
}
}
Expand Down Expand Up @@ -353,7 +444,6 @@ protected function _getPragmaOptions($pragma_name) {
return (is_array($this->_localPragmas[$pragma_name])) ? $this->_localPragmas[$pragma_name] : array();
}


/**
* Check whether this Mustache instance throws a given exception.
*
Expand All @@ -367,6 +457,23 @@ protected function _throwsException($exception) {
return (isset($this->_throwsExceptions[$exception]) && $this->_throwsExceptions[$exception]);
}

/**
* Prepare a tag RegEx for the given opening/closing tags.
*
* @access protected
* @param string $otag
* @param string $ctag
* @return string
*/
protected function _prepareTagRegEx($otag, $ctag) {
return sprintf(
'/(?<whitespace>(?<=\\n)[ \\t]*)?%s(?<type>[%s]?)(?<tag_name>.+?)(?:\\2|})?%s(?:\\s*(?=\\n))?/s',
preg_quote($otag, '/'),
self::TAG_TYPES,
preg_quote($ctag, '/')
);
}

/**
* Loop through and render individual Mustache tags.
*
Expand All @@ -382,22 +489,31 @@ protected function _renderTags($template) {
$otag_orig = $this->_otag;
$ctag_orig = $this->_ctag;

$otag = preg_quote($this->_otag, '/');
$ctag = preg_quote($this->_ctag, '/');

$this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s";
$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);

$html = '';
$matches = array();
while (preg_match($this->_tagRegEx, $template, $matches, PREG_OFFSET_CAPTURE)) {
$tag = $matches[0][0];
$offset = $matches[0][1];
$modifier = $matches[1][0];
$tag_name = trim($matches[2][0]);
$modifier = $matches['type'][0];
$tag_name = trim($matches['tag_name'][0]);

if (isset($matches['whitespace']) && $matches['whitespace'][1] > -1) {
$whitespace = $matches['whitespace'][0];
} else {
$whitespace = null;
}

$html .= substr($template, 0, $offset);
$html .= $this->_renderTag($modifier, $tag_name);
$template = substr($template, $offset + strlen($tag));

$next_offset = $offset + strlen($tag);
if ((substr($html, -1) == "\n") && (substr($template, $next_offset, 1) == "\n")) {
$next_offset++;
}
$template = substr($template, $next_offset);

$html .= $this->_renderTag($modifier, $tag_name, $whitespace);
}

$this->_otag = $otag_orig;
Expand All @@ -418,23 +534,8 @@ protected function _renderTags($template) {
* @throws MustacheException Unmatched section tag encountered.
* @return string
*/
protected function _renderTag($modifier, $tag_name) {
protected function _renderTag($modifier, $tag_name, $whitespace) {
switch ($modifier) {
case '#':
case '^':
if ($this->_throwsException(MustacheException::UNCLOSED_SECTION)) {
throw new MustacheException('Unclosed section: ' . $tag_name, MustacheException::UNCLOSED_SECTION);
} else {
return '';
}
break;
case '/':
if ($this->_throwsException(MustacheException::UNEXPECTED_CLOSE_SECTION)) {
throw new MustacheException('Unexpected close section: ' . $tag_name, MustacheException::UNEXPECTED_CLOSE_SECTION);
} else {
return '';
}
break;
case '=':
return $this->_changeDelimiter($tag_name);
break;
Expand All @@ -443,16 +544,26 @@ protected function _renderTag($modifier, $tag_name) {
break;
case '>':
case '<':
return $this->_renderPartial($tag_name);
return $this->_renderPartial($tag_name, $whitespace);
break;
case '{':
// strip the trailing } ...
if ($tag_name[(strlen($tag_name) - 1)] == '}') {
$tag_name = substr($tag_name, 0, -1);
}
case '&':
if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) {
return $this->_renderEscaped($tag_name);
} else {
return $this->_renderUnescaped($tag_name);
}
break;
case '#':
case '^':
case '/':
// remove any leftovers from _renderSections
return '';
break;
}

if ($this->_hasPragma(self::PRAGMA_UNESCAPED)) {
Expand Down Expand Up @@ -502,9 +613,10 @@ protected function _renderUnescaped($tag_name) {
* @param string $tag_name
* @return string
*/
protected function _renderPartial($tag_name) {
protected function _renderPartial($tag_name, $whitespace = '') {
$view = clone($this);
return $view->render($this->_getPartial($tag_name));

return $whitespace . preg_replace('/\n(?!$)/s', "\n" . $whitespace, $view->render($this->_getPartial($tag_name)));
}

/**
Expand All @@ -516,13 +628,12 @@ protected function _renderPartial($tag_name) {
* @return string
*/
protected function _changeDelimiter($tag_name) {
$tags = explode(' ', $tag_name);
$this->_otag = $tags[0];
$this->_ctag = $tags[1];
list($otag, $ctag) = explode(' ', $tag_name);
$this->_otag = $otag;
$this->_ctag = $ctag;

$this->_tagRegEx = $this->_prepareTagRegEx($this->_otag, $this->_ctag);

$otag = preg_quote($this->_otag, '/');
$ctag = preg_quote($this->_ctag, '/');
$this->_tagRegEx = '/' . $otag . "([#\^\/=!<>\\{&])?(.+?)\\1?" . $ctag . "+/s";
return '';
}

Expand All @@ -542,7 +653,6 @@ protected function _pushContext(&$local_context) {
$this->_context = $new;
}


/**
* Remove the latest context from the stack.
*
Expand Down Expand Up @@ -575,7 +685,7 @@ protected function _popContext() {
* @return string
*/
protected function _getVariable($tag_name) {
if ($this->_hasPragma(self::PRAGMA_DOT_NOTATION) && $tag_name != '.') {
if ($tag_name != '.' && strpos($tag_name, '.') !== false && $this->_hasPragma(self::PRAGMA_DOT_NOTATION)) {
$chunks = explode('.', $tag_name);
$first = array_shift($chunks);

Expand Down
3 changes: 1 addition & 2 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,8 @@ And render it:
Known Issues
------------

* Sections don't respect delimiter changes -- `delimiters` example currently fails with an
* Things get weird when you change delimiters inside a section -- `delimiters` example currently fails with an
"unclosed section" exception.
* Mustache isn't always very good at whitespace.


See Also
Expand Down
Loading

1 comment on commit a448453

@bobthecow
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Added tests against the (forthcoming) Official Mustache Spec. If you're interested in running them, you've gotta update submodules: git submodule update --init
  • Updated Mustache.php to pass all tests against the Official Mustache Spec, including:
    • Fixed issues rendering recursive partials and nested sections.
    • Fixed a horde of whitespace quirks.
    • Updated the examples tests to match the spec.
  • This release will change whitspace output in most templates. The change is in the right direction, but consider this your warning :)

Please sign in to comment.