From c8a879f73869d3b7cdd131729f042ad2e053f59f Mon Sep 17 00:00:00 2001 From: Iain MacDonald Date: Fri, 4 May 2012 00:25:24 +0100 Subject: [PATCH 1/3] Feature tweaks and ehancements context.php: Allow accessing first, second or third elements from an array Access does not depend on array keys being [0,1,2,3...] filters.php: Access second or third index from items Added json filter Limiting words could sometimes cut an html tag in half tags.php: Allow start index in {% for %} tag --- h2o/context.php | 81 +++++++++++++++++++++++------------------- h2o/filters.php | 94 +++++++++++++++++++++++++++---------------------- h2o/tags.php | 79 +++++++++++++++++++++-------------------- 3 files changed, 137 insertions(+), 117 deletions(-) diff --git a/h2o/context.php b/h2o/context.php index 20ab752..ca8dd2e 100644 --- a/h2o/context.php +++ b/h2o/context.php @@ -9,21 +9,21 @@ class H2o_Context implements ArrayAccess { public $scopes; public $options; public $autoescape = true; - + private $arrayMethods = array('first'=> 0, 'last'=> 1, 'length'=> 2, 'size'=> 3); static $lookupTable = array(); - + function __construct($context = array(), $options = array()){ if (is_object($context)) $context = get_object_vars($context); $this->scopes = array($context); - - if (isset($options['safeClass'])) + + if (isset($options['safeClass'])) $this->safeClass = array_merge($this->safeClass, $options['safeClass']); - - if (isset($options['autoescape'])) + + if (isset($options['autoescape'])) $this->autoescape = $options['autoescape']; - + $this->options = $options; } @@ -54,13 +54,13 @@ function offsetGet($key) { } return; } - + function offsetSet($key, $value) { if (strpos($key, '.') > -1) throw new Exception('cannot set non local variable'); return $this->scopes[0][$key] = $value; } - + function offsetUnset($key) { foreach ($this->scopes as $layer) { if (isset($layer[$key])) unset($layer[$key]); @@ -83,11 +83,11 @@ function isDefined($key) { return $this->offsetExists($key); } /** - * - * - * + * + * + * * Variable name - * + * * @param $var variable name or array(0 => variable name, 'filters' => filters array) * @return unknown_type */ @@ -96,15 +96,15 @@ function resolve($var) { # if $var is array - it contains filters to apply $filters = array(); if ( is_array($var) ) { - + $name = array_shift($var); $filters = isset($var['filters'])? $var['filters'] : array(); - - } + + } else $name = $var; - + $result = null; - + # Lookup basic types, null, boolean, numeric and string # Variable starts with : (:users.name) to short-circuit lookup if ($name[0] === ':') { @@ -116,12 +116,12 @@ function resolve($var) { } elseif ($name === 'false') { $result = false; - } + } elseif (preg_match('/^-?\d+(\.\d+)?$/', $name, $matches)) { $result = isset($matches[1])? floatval($name) : intval($name); } elseif (preg_match('/^"([^"\\\\]*(?:\\.[^"\\\\]*)*)"|' . - '\'([^\'\\\\]*(?:\\.[^\'\\\\]*)*)\'$/', $name)) { + '\'([^\'\\\\]*(?:\\.[^\'\\\\]*)*)\'$/', $name)) { $result = stripcslashes(substr($name, 1, -1)); } } @@ -131,7 +131,7 @@ function resolve($var) { $result = $this->applyFilters($result,$filters); return $result; } - + function getVariable($name) { # Local variables. this gives as a bit of performance improvement if (!strpos($name, '.')) @@ -147,7 +147,14 @@ function getVariable($name) { if (isset($object[$part])) { $object = $object[$part]; } elseif ($part === 'first') { - $object = isset($object[0])?$object[0]:null; + $o = array_slice($object, 0, 1); + $object = count($o) ? $o[0] : null; + } elseif ($part === 'second') { + $o = array_slice($object, 1, 1); + $object = count($o) ? $o[0] : null; + } elseif ($part === 'third') { + $o = array_slice($object, 2, 1); + $object = count($o) ? $o[0] : null; } elseif ($part === 'last') { $last = count($object)-1; $object = isset($object[$last])?$object[$last]:null; @@ -161,7 +168,7 @@ function getVariable($name) { if (isset($object->$part)) $object = $object->$part; elseif (is_callable(array($object, $part))) { - $methodAllowed = in_array(get_class($object), $this->safeClass) || + $methodAllowed = in_array(get_class($object), $this->safeClass) || (isset($object->h2o_safe) && ( $object->h2o_safe === true || in_array($part, $object->h2o_safe) ) @@ -176,12 +183,12 @@ function getVariable($name) { } function applyFilters($object, $filters) { - + foreach ($filters as $filter) { $name = substr(array_shift($filter), 1); $args = $filter; - - if (isset(h2o::$filters[$name])) { + + if (isset(h2o::$filters[$name])) { foreach ($args as $i => $argument) { # name args if (is_array($argument)) { @@ -201,26 +208,26 @@ function applyFilters($object, $filters) { } function escape($value, $var) { - + $safe = false; $filters = (is_array($var) && isset($var['filters']))? $var['filters'] : array(); foreach ( $filters as $filter ) { - + $name = substr(array_shift($filter), 1); $safe = !$safe && ($name === 'safe'); - + $escaped = $name === 'escape'; } - + $should_escape = $this->autoescape || isset($escaped) && $escaped; - + if ( ($should_escape && !$safe)) { $value = htmlspecialchars($value); - } - + } + return $value; - } + } function externalLookup($name) { if (!empty(self::$lookupTable)) { @@ -238,7 +245,7 @@ class BlockContext { var $h2o_safe = array('name', 'depth', 'super'); var $block, $index; private $context; - + function __construct($block, $context, $index) { $this->block =& $block; $this->context = $context; @@ -256,9 +263,9 @@ function depth() { function super() { $stream = new StreamWriter; $this->block->parent->render($this->context, $stream, $this->index+1); - return $stream->close(); + return $stream->close(); } - + function __toString() { return "[BlockContext : {$this->block->name}, {$this->block->filename}]"; } diff --git a/h2o/filters.php b/h2o/filters.php index a8cf3d3..2272da8 100644 --- a/h2o/filters.php +++ b/h2o/filters.php @@ -7,11 +7,17 @@ class CoreFilters extends FilterCollection { static function first($value) { return $value[0]; } - + static function second($value) { + return isset($value[1]) ? $value[1] : null; + } + static function third($value) { + return isset($value[2]) ? $value[2] : null; + } + static function last($value) { return $value[count($value) - 1]; } - + static function join($value, $delimiter = ', ') { return join($delimiter, $value); } @@ -19,7 +25,7 @@ static function join($value, $delimiter = ', ') { static function length($value) { return count($value); } - + static function urlencode($data) { if (is_array($data)) { $result; @@ -32,13 +38,13 @@ static function urlencode($data) { return urlencode($data); } } - + static function hyphenize ($string) { $rules = array('/[^\w\s-]+/'=>'','/\s+/'=>'-', '/-{2,}/'=>'-'); $string = preg_replace(array_keys($rules), $rules, trim($string)); return $string = trim(strtolower($string)); } - + static function urlize( $string, $truncate = false ) { $reg_exUrl = "/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/"; preg_match_all($reg_exUrl, $string, $matches); @@ -61,7 +67,10 @@ static function urlize( $string, $truncate = false ) { } return $string; } - + static function json($object) + { + return json_encode($object); + } static function set_default($object, $default) { return !$object ? $default : $object; } @@ -73,24 +82,24 @@ static function humanize($string) { $string = preg_replace('/\s+/', ' ', trim(preg_replace('/[^A-Za-z0-9()!,?$]+/', ' ', $string))); return capfirst($string); } - + static function capitalize($string) { return ucwords(strtolower($string)) ; } - + static function titlize($string) { return self::capitalize($string); } - + static function capfirst($string) { $string = strtolower($string); return strtoupper($string{0}). substr($string, 1, strlen($string)); } - + static function tighten_space($value) { return preg_replace("/\s{2,}/", ' ', $value); } - + static function escape($value, $attribute = false) { return htmlspecialchars($value, $attribute ? ENT_QUOTES : ENT_NOQUOTES); } @@ -100,26 +109,27 @@ static function escapejson($value) { // This function encodes the entire data structure, and strings get quotes around them. return json_encode($value); } - + static function force_escape($value, $attribute = false) { return self::escape($value, $attribute); } - + static function e($value, $attribute = false) { return self::escape($value, $attribute); } - + static function safe($value) { return $value; } - + static function truncate ($string, $max = 50, $ends = '...') { - return (strlen($string) > $max ? substr($string, 0, $max).$ends : $string); + return (strlen($string) > $max ? substr($string, 0, $max).$ends : $string); } - + static function limitwords($text, $limit = 50, $ends = '...') { if (strlen($text) > $limit) { - $words = str_word_count($text, 2); + // html tags can sometimes get cut up, so include '' as word characters + $words = str_word_count($text, 2, ''); $pos = array_keys($words); if (isset($pos[$limit])) { @@ -136,7 +146,7 @@ static function filesize ($bytes, $round = 1) { return '0 bytes'; elseif ($bytes === 1) return '1 byte'; - + $units = array( 'bytes' => pow(2, 0), 'kB' => pow(2, 10), 'BM' => pow(2, 20), 'GB' => pow(2, 30), @@ -157,14 +167,14 @@ static function filesize ($bytes, $round = 1) { static function currency($amount, $currency = 'USD', $precision = 2, $negateWithParentheses = false) { $definition = array( - 'EUR' => array('�','.',','), 'GBP' => '�', 'JPY' => '�', + 'EUR' => array('€','.',','), 'GBP' => '£', 'JPY' => '¥', 'USD'=>'$', 'AU' => '$', 'CAN' => '$' ); $negative = false; $separator = ','; $decimals = '.'; $currency = strtoupper($currency); - + // Is negative if (strpos('-', $amount) !== false) { $negative = true; @@ -184,7 +194,7 @@ static function currency($amount, $currency = 'USD', $precision = 2, $negateWith if (round($amount, $precision) === $zero) { $amount = $zero; } - + if (isset($definition[$currency])) { $symbol = $definition[$currency]; if (is_array($symbol)) @@ -202,11 +212,11 @@ class HtmlFilters extends FilterCollection { static function base_url($url, $options = array()) { return $url; } - + static function asset_url($url, $options = array()) { return self::base_url($url, $options); } - + static function image_tag($url, $options = array()) { $attr = self::htmlAttribute(array('alt','width','height','border'), $options); return sprintf('', $url, $attr); @@ -220,17 +230,17 @@ static function css_tag($url, $options = array()) { static function script_tag($url, $options = array()) { return sprintf('', $url); } - + static function links_to($text, $url, $options = array()) { $attrs = self::htmlAttribute(array('ref'), $options); $url = self::base_url($url, $options); return sprintf('%s', $url, $attrs, $text); } - + static function links_with ($url, $text, $options = array()) { return self::links_to($text, $url, $options); } - + static function strip_tags($text) { $text = preg_replace(array('//'), array(' <', '> '),$text); return strip_tags($text); @@ -241,11 +251,11 @@ static function linebreaks($value, $format = 'p') { return HtmlFilters::nl2br($value); return HtmlFilters::nl2pbr($value); } - + static function nl2br($value) { return str_replace("\n", "
\n", $value); } - + static function nl2pbr($value) { $result = array(); $parts = preg_split('/(\r?\n){2,}/m', $value); @@ -257,7 +267,7 @@ static function nl2pbr($value) { protected static function htmlAttribute($attrs = array(), $data = array()) { $attrs = self::extract(array_merge(array('id', 'class', 'title', "style"), $attrs), $data); - + $result = array(); foreach ($attrs as $name => $value) { $result[] = "{$name}=\"{$value}\""; @@ -277,24 +287,24 @@ protected static function extract($attrs = array(), $data=array()) { class DatetimeFilters extends FilterCollection { static function date($time, $format = 'jS F Y H:i') { - if ($time instanceof DateTime) + if ($time instanceof DateTime) $time = (int) $time->format('U'); - if (!is_numeric($time)) + if (!is_numeric($time)) $time = strtotime($time); - + return date($format, $time); } static function relative_time($timestamp, $format = 'g:iA') { - if ($timestamp instanceof DateTime) + if ($timestamp instanceof DateTime) $timestamp = (int) $timestamp->format('U'); $timestamp = is_numeric($timestamp) ? $timestamp: strtotime($timestamp); - + $time = mktime(0, 0, 0); $delta = time() - $timestamp; $string = ''; - + if ($timestamp < $time - 86400) { return date("F j, Y, g:i a", $timestamp); } @@ -309,7 +319,7 @@ static function relative_time($timestamp, $format = 'g:iA') { else if ($delta >= 3600) $string .= "1 hour "; $delta %= 3600; - + if ($delta > 60) $string .= floor($delta / 60) . " minutes "; else @@ -318,13 +328,13 @@ static function relative_time($timestamp, $format = 'g:iA') { } static function relative_date($time) { - if ($time instanceof DateTime) + if ($time instanceof DateTime) $time = (int) $time->format('U'); $time = is_numeric($time) ? $time: strtotime($time); $today = strtotime(date('M j, Y')); $reldays = ($time - $today)/86400; - + if ($reldays >= 0 && $reldays < 1) return 'today'; else if ($reldays >= 1 && $reldays < 2) @@ -346,13 +356,13 @@ static function relative_date($time) { else return date('l, F j, Y',$time ? $time : time()); } - + static function relative_datetime($time) { $date = self::relative_date($time); - + if ($date === 'today') return self::relative_time($time); - + return $date; } } diff --git a/h2o/tags.php b/h2o/tags.php index c207ce2..471d9fa 100644 --- a/h2o/tags.php +++ b/h2o/tags.php @@ -1,6 +1,6 @@ body = $parser->parse('endif', 'else'); - + if ($parser->token->content === 'else') $this->else = $parser->parse('endif'); @@ -82,7 +82,7 @@ function __construct($argstring, $parser, $position = 0) { } function render($context, $stream) { - if ($this->test($context)) + if ($this->test($context)) $this->body->render($context, $stream); elseif ($this->else) $this->else->render($context, $stream); @@ -96,11 +96,12 @@ function test($context) { class For_Tag extends H2o_Node { public $position; - private $iteratable, $key, $item, $body, $else, $limit, $reversed; + private $iteratable, $key, $item, $body, $else, $start = 0, $limit, $reversed; private $syntax = '{ ([a-zA-Z][a-zA-Z0-9-_]*)(?:,\s?([a-zA-Z][a-zA-Z0-9-_]*))? \s+in\s+ ([a-zA-Z][a-zA-Z0-9-_]*(?:\.[a-zA-Z_0-9][a-zA-Z0-9_-]*)*)\s* # Iteratable name + (?:start\s*:\s*(\d+))?\s* (?:limit\s*:\s*(\d+))?\s* (reversed)? # Reverse keyword }x'; @@ -108,17 +109,17 @@ class For_Tag extends H2o_Node { function __construct($argstring, $parser, $position) { if (!preg_match($this->syntax, $argstring, $match)) throw new TemplateSyntaxError("Invalid for loop syntax "); - + $this->body = $parser->parse('endfor', 'else'); - + if ($parser->token->content === 'else') $this->else = $parser->parse('endfor'); - $match = array_pad($match, 6, ''); - list(,$this->key, $this->item, $this->iteratable, $this->limit, $this->reversed) = $match; - - if ($this->limit) - $this->limit = (int) $this->limit; + $match = array_pad($match, 7, ''); + list(,$this->key, $this->item, $this->iteratable, $this->start, $this->limit, $this->reversed) = $match; + + $this->start = (int) $this->start; + $this->limit = (int) $this->limit; # Swap value if no key found if (!$this->item) { @@ -140,11 +141,13 @@ function render($context, $stream) { if ($this->reversed) $iteratable = array_reverse($iteratable); - if ($this->limit) - $iteratable = array_slice($iteratable, 0, $this->limit); + if($this->limit) + $iteratable = array_slice($iteratable, $this->start, $this->limit); + else + $iteratable = array_slice($iteratable, $this->start); $length = count($iteratable); - + if ($length) { $parent = $context['loop']; $context->push(); @@ -152,14 +155,14 @@ function render($context, $stream) { foreach($iteratable as $key => $value) { $is_even = $idx % 2; $rev_count = $length - $idx; - + if ($this->key) { $context[$this->key] = $key; } $context[$this->item] = $value; $context['loop'] = array( 'parent' => $parent, - 'first' => $idx === 0, + 'first' => $idx === 0, 'last' => $rev_count === 1, 'odd' => !$is_even, 'even' => $is_even, @@ -170,7 +173,7 @@ function render($context, $stream) { 'revcounter0' => $rev_count - 1 ); $this->body->render($context, $stream); - ++$idx; + ++$idx; } $context->pop(); } elseif ($this->else) @@ -183,7 +186,7 @@ class Block_Tag extends H2o_Node { public $position; public $stack; private $syntax = '/^[a-zA-Z_][a-zA-Z0-9_-]*$/'; - + function __construct($argstring, $parser, $position) { if (!preg_match($this->syntax, $argstring)) throw new TemplateSyntaxError('Block tag expects a name, example: block [content]'); @@ -222,7 +225,7 @@ class Extends_Tag extends H2o_Node { public $position; public $nodelist; private $syntax = '/^["\'](.*?)["\']$/'; - + function __construct($argstring, $parser, $position = 0) { if (!$parser->first) throw new TemplateSyntaxError('extends must be first in file'); @@ -241,7 +244,7 @@ function __construct($argstring, $parser, $position = 0) { $parser->storage['templates'], $this->nodelist->parser->storage['templates'] ); $parser->storage['templates'][] = $this->filename; - + if (!isset($this->nodelist->parser->storage['blocks']) || !isset($parser->storage['blocks'])) return ; @@ -255,7 +258,7 @@ function __construct($argstring, $parser, $position = 0) { } } } - + function render($context, $stream) { $this->nodelist->render($context, $stream); } @@ -327,19 +330,19 @@ class With_Tag extends H2o_Node { private $variable, $shortcut; private $nodelist; private $syntax = '/^([\w]+(:?\.[\w\d]+)*)\s+as\s+([\w]+(:?\.[\w\d]+)?)$/'; - + function __construct($argstring, $parser, $position = 0) { if (!preg_match($this->syntax, $argstring, $matches)) throw new TemplateSyntaxError('Invalid with tag syntax'); - + # extract the long name and shortcut list(,$this->variable, ,$this->shortcut) = $matches; $this->nodelist = $parser->parse('endwith'); } - + function render($context, $stream) { $variable = $context->getVariable($this->variable); - + $context->push(array($this->shortcut => $variable)); $this->nodelist->render($context, $stream); $context->pop(); @@ -349,17 +352,17 @@ function render($context, $stream) { class Cycle_Tag extends H2o_Node { private $uid; private $sequence; - + function __construct($argstring, $parser, $pos) { $args = h2o_parser::parseArguments($argstring); - + if (count($args) < 2) { throw new Exception('Cycle tag require more than two items'); } - $this->sequence = $args; + $this->sequence = $args; $this->uid = '__cycle__'.$pos; } - + function render($context, $stream) { if (!is_null($item = $context->getVariable($this->uid))) { $item = ($item + 1) % count($this->sequence); @@ -378,10 +381,10 @@ class Load_Tag extends H2o_Node { function __construct($argstring, $parser, $pos = 0) { $this->extension = stripcslashes(preg_replace("/^[\"'](.*)[\"']$/", "$1", $argstring)); - + if ($parser->runtime->searchpath) $this->appendPath($parser->runtime->searchpath); - + $parser->storage['included'][$this->extension] = $file = $this->load(); $this->position = $pos; } @@ -393,7 +396,7 @@ function render($context, $stream) { function appendPath($path) { $this->searchpath[] = $path; } - + private function load() { if (isset(h2o::$extensions[$this->extension])) { return true; @@ -416,7 +419,7 @@ class Debug_Tag extends H2o_Node { function __construct($argstring, $parser, $pos = 0) { $this->argument = $argstring; } - + function render($context, $stream) { if ($this->argument) { $object = $context->resolve(symbol($this->argument)); @@ -444,7 +447,7 @@ function __construct($argstring, $parser, $pos=0) { $this->format = "D M j G:i:s T Y"; } } - + function render($contxt, $stream) { $time = date($this->format); $stream->write($time); @@ -453,7 +456,7 @@ function render($contxt, $stream) { class Autoescape_Tag extends H2o_Node { protected $enable; - + function __construct($argstring, $parser, $pos = 0) { if ($argstring === 'on') $this->enable = true; @@ -463,7 +466,7 @@ function __construct($argstring, $parser, $pos = 0) { "Invalid syntax : autoescape on|off " ); } - + function render($context, $stream) { $context->autoescape = $this->enable; } From 37df99f5d1edddab5f9ee00a5aa066651d23147a Mon Sep 17 00:00:00 2001 From: Iain MacDonald Date: Thu, 28 Jun 2012 11:40:43 +0100 Subject: [PATCH 2/3] Removed second and third contexts --- h2o/context.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/h2o/context.php b/h2o/context.php index ca8dd2e..16fa3ff 100644 --- a/h2o/context.php +++ b/h2o/context.php @@ -149,12 +149,6 @@ function getVariable($name) { } elseif ($part === 'first') { $o = array_slice($object, 0, 1); $object = count($o) ? $o[0] : null; - } elseif ($part === 'second') { - $o = array_slice($object, 1, 1); - $object = count($o) ? $o[0] : null; - } elseif ($part === 'third') { - $o = array_slice($object, 2, 1); - $object = count($o) ? $o[0] : null; } elseif ($part === 'last') { $last = count($object)-1; $object = isset($object[$last])?$object[$last]:null; From 3a688217a4fc9d1159b894071c9f142da7564274 Mon Sep 17 00:00:00 2001 From: Iain MacDonald Date: Thu, 28 Jun 2012 12:13:02 +0100 Subject: [PATCH 3/3] Changed access to last context so that it doesn't depend on sequential numerical indexes --- h2o/context.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/h2o/context.php b/h2o/context.php index 16fa3ff..dcee4f6 100644 --- a/h2o/context.php +++ b/h2o/context.php @@ -150,8 +150,8 @@ function getVariable($name) { $o = array_slice($object, 0, 1); $object = count($o) ? $o[0] : null; } elseif ($part === 'last') { - $last = count($object)-1; - $object = isset($object[$last])?$object[$last]:null; + $o = array_slice($object, -1, 1); + $object = count($o) ? $o[0] : null; } elseif ($part === 'size' or $part === 'length') { return count($object); } else {