diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e98f81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Numerous always-ignore extensions +*.diff +*.err +*.orig +*.log +*.rej +*.swo +*.swp +*.vi +*~ +*.sass-cache + +# OS or Editor folders +.DS_Store +Thumbs.db +.cache +.project +.settings +.tmproj +*.esproj +nbproject + +# Dreamweaver added files +_notes +dwsync.xml + +# Komodo +*.komodoproject +.komodotools + +# Folders to ignore +.hg +.svn +.CVS +intermediate +publish +.idea + +# misc +node_modules +*.sublime-workspace diff --git a/Blick.module b/Blick.module new file mode 100644 index 0000000..a5bd459 --- /dev/null +++ b/Blick.module @@ -0,0 +1,224 @@ + + * @copyright Copyright (c) 2015, Christian Raunitschka + * + * @version 0.1.0 + * + * @filesource + * + * @see https://github.com/owzim/pw-blick + * @see http://raunitschka.de + * @see http://www.processwire.com + */ + +use owzim\Blick\AssetFactory; + +class Blick extends Wire implements Module +{ + + public static $configTypeKeys = array('css', 'js', 'img'); + public static $configSubKeys = array( + 'Path', 'Url', 'Markup', 'Default', 'Extension', + 'Variations', 'VariationSubDir', + 'Versioning', 'VersioningFormat', + 'Min', 'MinFormat', + ); + + /** + * $conf + * + * @var null + */ + protected $conf = null; + + /** + * getModuleInfo + * + * @return array + */ + public static function getModuleInfo() + { + return array( + 'title' => 'Blick', + 'summary' => 'A small helper module for including JS, CSS and image files', + 'version' => '0.1.0', + 'icon' => 'eye', + 'requires' => array('PHP>=5.4','ProcessWire>=2.5.5'), + 'singular' => true, + 'autoload' => function() { + $config = wire('config'); + $autoload = + isset($config->blick) && + isset($config->blick['autoloadAs']) && + is_string($config->blick['autoloadAs']) && + $config->blick['autoloadAs'] !== ''; + return $autoload; + }, + ); + } + + /** + * getDefaultConfig + * + * @return array + */ + public static function getDefaultConfig() + { + $config = wire('config'); + $paths = $config->paths; + $urls = $config->urls; + + return array( + 'jsPath' => $paths->templates . 'scripts', + 'jsUrl' => $urls->templates . 'scripts', + 'jsMarkup' => '', + 'jsDefault' => 'markup', + 'jsVersioning' => false, + 'jsVersioningFormat' => '?v={version}', + 'jsMin' => false, + 'jsMinFormat' => "{file}.min.{ext}", + + 'cssPath' => $paths->templates . 'styles', + 'cssUrl' => $urls->templates . 'styles', + 'cssMarkup' => '', + 'cssDefault' => 'markup', + 'cssVersioning' => false, + 'cssVersioningFormat' => '?v={version}', + 'cssMin' => false, + 'cssMinFormat' => "{file}.min.{ext}", + + 'imgPath' => $paths->templates . 'images', + 'imgUrl' => $urls->templates . 'images', + 'imgMarkup' => '{0}', + 'imgDefault' => 'markup', + 'imgVariations' => array(), + 'imgVariationSubDir' => 'variations', + 'imgVersioning' => false, + 'imgVersioningFormat' => '', + 'imgMin' => false, + 'imgMinFormat' => '', + + 'appendNewLine' => true, + ); + } + + /** + * init + * + */ + public function init() + { + require_once(__DIR__ . '/owzim/Blick/Autoloader.php'); + spl_autoload_register('\owzim\Blick\Autoloader::autoload'); + + $this->conf = self::getConfig(); + + if (isset($this->conf->autoloadAs)) { + $this->wire($this->conf->autoloadAs, $this); + } + } + + /** + * getConfig + * + * @param boolean $forceParse + * @return object + */ + public static function getConfig($forceParse = false) + { + static $parsed = null; + if (!is_null($parsed) && !$forceParse) return $parsed; + $wireConfig = wire('config'); + return $parsed = self::combineConfig( + self::getDefaultConfig(), + isset($wireConfig->blick) ? $wireConfig->blick : array(), + self::$configTypeKeys, + self::$configSubKeys + ); + } + + /** + * Combine default and custom config into one, + * create sub objects by given `$typeKeys` and extract `$subKeys` into them + * see example: http://3v4l.org/IoofQ + * + * @param array $default + * @param array $custom + * @param array $typeKeys + * @param array $subKeys + * @return object + */ + public static function combineConfig($default, $custom, $typeKeys, $subKeys) + { + $config = array_merge($default, $custom); + foreach ($typeKeys as $typeKey) { + $sub = $config[$typeKey] = new \stdClass(); + foreach ($subKeys as $subKey) { + $fullKey = "{$typeKey}{$subKey}"; + if (array_key_exists($fullKey, $config)) { + $sub->{lcfirst($subKey)} = $config[$fullKey]; + unset($config[$fullKey]); + } + } + } + return (object) $config; + } + + /** + * asset + * + * @param string $name + * @param string $type + * @param array $args + * @return ozwim\Blick\Asset|ozwim\Blick\Image + * + * @see js + * @see css + * @see img + */ + public function asset($name, $type, $args) + { + return AssetFactory::get($name, $type, $this->conf, $args); + } + + /** + * js + * + * @param string $name + * @return ozwim\Blick\Asset + */ + public function js($name) + { + return $this->asset($name, 'js', array_slice(func_get_args(), 1)); + } + + /** + * css + * + * @param string $name + * @return ozwim\Blick\Asset + */ + public function css($name) + { + return $this->asset($name, 'js', array_slice(func_get_args(), 1)); + } + + /** + * img + * + * @param string $name + * @return ozwim\Blick\Image + */ + public function img($name) + { + return $this->asset($name, 'img', array_slice(func_get_args(), 1)); + } +} diff --git a/owzim/blick/Asset.php b/owzim/blick/Asset.php new file mode 100644 index 0000000..84f8279 --- /dev/null +++ b/owzim/blick/Asset.php @@ -0,0 +1,323 @@ + + * @copyright Copyright (c) 2015, Christian Raunitschka + * + * @version 0.1.0 + * + * @filesource + * + * @property string $default + * @property string $markup + * @property string $version + * @property string $path + * @property string $url + */ + +namespace owzim\Blick; + +class Asset extends \WireData +{ + + /** + * Regex for determining if a given file name is a remote url + * + * @see http://regexr.com/3a5d0 + */ + const REMOTE_REGEX = '/^(https?\:)?\/\//'; + + /** + * Regex for determining if a given file name is an absolute url + */ + const ABSOLUTE_REGEX = '/^\/.*/'; + + /** + * @var string $_fullName + */ + protected $_fullName = null; + + /** + * @var string $type + */ + protected $type = null; + + /** + * @var object $conf + */ + protected $conf = null; + + /** + * @var array $args + */ + protected $args = null; + + /** + * @var string $pathPrefix + */ + protected $pathPrefix = ''; + + /** + * @var string $urlPrefix + */ + protected $urlPrefix = ''; + + /** + * @var string $filePrefix + */ + protected $filePrefix = ''; + + /** + * Constructor + * + */ + public function __construct($fullName, $type, $conf, $args = array()) + { + $this->type = $type; + $this->conf = $conf; + $this->args = $args; + $this->fullName = $fullName; + $this->default = $conf->default; + } + + /** + * Return a value depending on what's set to `$this->default`, which is used as + * the key + * + * @return string + */ + public function __toString() + { + return isset($this->{$this->default}) + ? $this->{$this->default} + : $this->markup; + } + + /** + * __get + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + $methodName = 'get' . ucfirst($name); + if ($name && method_exists($this, $methodName)) { + return $this->$methodName(); + } else { + return parent::__get($name); + } + } + + /** + * __isset + * + * @param string $name + * @return boolean + */ + public function __isset($name) + { + $methodName = 'get' . ucfirst($name); + if (method_exists($this, $methodName)) { + return true; + } else { + return parent::__isset($name); + } + } + + /** + * __set + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) { + $methodName = 'set' . ucfirst($name); + if (method_exists($this, $methodName)) { + return $this->$methodName($value); + } else { + return parent::__set($name, $value); + } + } + + /** + * setFullName + * + * @param string $fullName Examples: + * '/some/abs/url/main.js' + * '/some/abs/url/main' + * 'rel/url/main.js' + * 'rel/url/main' + * 'main.js' + * 'main' + * @see __set + * @return string + */ + public function setFullName($fullName) + { + $this->_fullName = $fullName; + + $this->isRemote = preg_match(self::REMOTE_REGEX, $fullName) ? true : false; + $this->isAbsolute = preg_match(self::ABSOLUTE_REGEX, $fullName) ? true : false; + + $pathPrefix = rtrim($this->conf->path, '/'); + $urlPrefix = rtrim($this->conf->url, '/'); + + // either '.' or 'http://url' + $filePrefix = pathinfo($fullName, PATHINFO_DIRNAME); + + // either '' or 'http://url' + $filePrefix = $filePrefix === '.' ? '' : $filePrefix; + + if ($this->isAbsolute) { + $filePrefix = ltrim($filePrefix, '/'); + $pathPrefix = $this->config->paths->root . $filePrefix; + $urlPrefix = $this->config->urls->root . $filePrefix; + $filePrefix = ''; + } + + $this->filename = pathinfo($fullName, PATHINFO_BASENAME); + $this->pathPrefix = $pathPrefix; + $this->urlPrefix = $urlPrefix; + $this->filePrefix = $filePrefix; + } + + /** + * getPath + * + * @see __get + * @return string + */ + protected function getPath() + { + if (!$this->isRemote) { + $filePrefix = $this->filePrefix ? "{$this->filePrefix}/" : ''; + $variationSubDir = $this->variationSubDir ? "{$this->variationSubDir}/" : ''; + return "{$this->pathPrefix}/{$filePrefix}{$variationSubDir}{$this->filename}"; + } else { + return ''; + } + } + + /** + * setFilename + * + * @param string $filename + * @see __set + * @return string + */ + protected function setFilename($filename) + { + // either 'js' or '' + $ext = pathinfo($filename, PATHINFO_EXTENSION); + + // 'js' + $ext = $ext ? "$ext" : $this->type; + + // 'index' + $name = pathinfo($filename, PATHINFO_FILENAME); + + $this->ext = $ext; + $this->name = $name; + } + + /** + * getDir + * + * @see __get + * @return string + */ + protected function getDir() + { + $filePrefix = $this->filePrefix ? "/{$this->filePrefix}" : ''; + $variationSubDir = $this->variationSubDir ? "/{$this->variationSubDir}" : ''; + return "{$this->pathPrefix}{$filePrefix}{$variationSubDir}"; + } + + /** + * getUrl + * + * @see __get + * @return string + */ + protected function getUrl() + { + if (!$this->isRemote) { + $filePrefix = $this->filePrefix ? "{$this->filePrefix}/" : ''; + $variationSubDir = $this->variationSubDir ? "{$this->variationSubDir}/" : ''; + return "{$this->urlPrefix}/{$filePrefix}{$variationSubDir}{$this->filename}{$this->param}"; + } else { + return $this->_fullName; + } + + } + + /** + * getFilename + * + * @see __get + * @return string + */ + protected function getFilename() + { + + if ($this->conf->min) { + $formatted = utils\str::format($this->conf->minFormat, array( + 'file' => $this->name, + 'ext' => $this->ext, + )); + $file = "{$this->dir}/$formatted"; + + if (file_exists($file)) { + return "$formatted"; + } + } + + return "{$this->name}.{$this->ext}"; + } + + /** + * getParam + * + * @see __get + * @return string + */ + protected function getParam() + { + if (!$this->conf->versioning) return ''; + return utils\str::format($this->conf->versioningFormat, array( + 'version' => $this->version + )); + } + + /** + * getMarkup + * + * @see __get + * @return string + */ + protected function getMarkup() + { + $markup = utils\str::format($this->conf->markup, array( + 'url' => $this->url, + 'path' => $this->path, + 'param' => $this->param, + 'version' => $this->version + )); + $markup = utils\str::format($markup, $this->args); + return "{$markup}{$this->nl}"; + } + + /** + * Get the modfied unix timestamp of `$filePath` + * + * @param string $filePath + * @return int + */ + protected function getVersion() + { + return file_exists($this->path) ? filemtime($this->path) : ''; + } +} diff --git a/owzim/blick/AssetFactory.php b/owzim/blick/AssetFactory.php new file mode 100644 index 0000000..48acc86 --- /dev/null +++ b/owzim/blick/AssetFactory.php @@ -0,0 +1,61 @@ + + * @copyright Copyright (c) 2015, Christian Raunitschka + * + * @version 0.1.0 + * + * @filesource + */ + +namespace owzim\Blick; + +class AssetFactory +{ + + /** + * + */ + const TYPE_IMG = 'img'; + + /** + * + */ + const TYPE_JS = 'js'; + + /** + * + */ + const TYPE_CSS = 'css'; + + /** + * http://regexr.com/3a5d0 + * + */ + const REMOTE_REGEX = '/^(https?\:)?\/\//'; + + /** + * Get a new Asset instance with properties depending on `$config` + * + * @param string $fullName + * @param string $type + * @param object $config + * @param array $args + * @return Asset + */ + public static function get($fullName, $type, $config, $args = array(), $forceNew = false) + { + static $cache = null; + if (is_null($cache)) $cache = array(); + $id = "{$fullName}-{$type}-" . implode('-', $args); + if (!$forceNew && array_key_exists($id, $cache)) return $cache[$id]; + + $class = __NAMESPACE__ . ($type === self::TYPE_IMG ? '\Image' : '\Asset'); + $asset = new $class($fullName, $type, $config->$type, $args); + + return $cache[$id] = $asset; + } +} diff --git a/owzim/blick/Autoloader.php b/owzim/blick/Autoloader.php new file mode 100644 index 0000000..e73308e --- /dev/null +++ b/owzim/blick/Autoloader.php @@ -0,0 +1,81 @@ + + * @author Christian (owzim) Raunitschka + * @version 1.0.5 + * @copyright Copyright (c) 2013, neuwaerts GmbH + * @copyright Copyright (c) 2015, Christian Raunitschka + * @filesource + * + * modified by Christian Raunitschka to use 'owzim\Blick' namespace + */ + +namespace owzim\Blick; + +/** + * Class Autoloader + * + * Generic autoloader (PSR-0 style) + * + * Can be registered as follows: + * + * + * require_once('/path/to/Blick/owzim/Blick/Autoloader.php'); + * spl_autoload_register('owzim\Blick\Autoloader::autoload'); + * + * + * @see http://www.php-fig.org/psr/0/ + */ +class Autoloader +{ + + static $basePath = null; + + /** + * @field boolean Flag signaling, whether the autoload callback is already registered + */ + protected static $isRegistered = false; + + /** + * Registers the autoload callback + * + * Does nothing, if already registered. + */ + public static function register() + { + if (self::$isRegistered) return; + + spl_autoload_register(array(__CLASS__, 'autoload')); + self::$isRegistered = true; + } + + /** + * Autoloader callback + * + * @param $className + */ + public static function autoload($className) + { + if (is_null(self::$basePath)) self::$basePath = + dirname(dirname(__FILE__)) . + DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR; + + $basePath = self::$basePath; + + $className = ltrim($className, '\\'); + $fileName = $basePath; + if ($lastNsPos = strrpos($className, '\\')) { + $namespace = substr($className, 0, $lastNsPos); + $className = substr($className, $lastNsPos + 1); + $fileName .= str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; + } + $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; + + if (!is_file($fileName)) return; + + require_once($fileName); + } +} diff --git a/owzim/blick/Image.php b/owzim/blick/Image.php new file mode 100644 index 0000000..a95c6b2 --- /dev/null +++ b/owzim/blick/Image.php @@ -0,0 +1,274 @@ + + * @copyright Copyright (c) 2015, Christian Raunitschka + * + * @version 0.1.0 + * + * @filesource + * + * @property string $default + * @property string $markup + * @property string $version + * @property string $path + * @property string $url + */ + +namespace owzim\Blick; + +class Image extends Asset +{ + /** + * borrowed from \PageImage::size and modified + * + * @param int $width + * @param int $height + * @param array $options + * @return Image + */ + function size($width, $height, $options = null) + { + + if($this->ext == 'svg') return $this; + + if(!is_array($options)) { + if(is_string($options)) { + // optionally allow a string to be specified with crop direction, for shorter syntax + if(strpos($options, ',') !== false) $options = explode(',', $options); // 30,40 + $options = array('cropping' => $options); + } else if(is_int($options)) { + // optionally allow an integer to be specified with quality, for shorter syntax + $options = array('quality' => $options); + } else if(is_bool($options)) { + // optionally allow a boolean to be specified with upscaling toggle on/off + $options = array('upscaling' => $options); + } else { + // unknown options type + $options = array(); + } + } + + $defaultOptions = array( + 'upscaling' => true, + 'cropping' => true, + 'quality' => 90, + 'hidpiQuality' => 40, + 'suffix' => array(), // can be array of suffixes or string of 1 suffix + 'forceNew' => false, // force it to create new image even if already exists + 'hidpi' => false, + 'cleanFilename' => false, // clean filename of historial resize information + ); + + $this->error = ''; + $configOptions = $this->config->imageSizerOptions; + if(!is_array($configOptions)) $configOptions = array(); + $options = array_merge($defaultOptions, $configOptions, $options); + + $width = (int) $width; + $height = (int) $height; + + if(strpos($options['cropping'], 'x') === 0 && preg_match('/^x(\d+)[yx](\d+)/', $options['cropping'], $matches)) { + $options['cropping'] = true; + $options['cropExtra'] = array((int) $matches[1], (int) $matches[2], $width, $height); + $crop = ''; + } else { + $crop = \ImageSizer::croppingValueStr($options['cropping']); + } + + $suffixStr = ''; + if(!empty($options['suffix'])) { + $suffix = is_array($options['suffix']) ? $options['suffix'] : array($options['suffix']); + sort($suffix); + foreach($suffix as $key => $s) { + $s = strtolower($this->wire('sanitizer')->fieldName($s)); + if(empty($s)) unset($suffix[$key]); + else $suffix[$key] = $s; + } + if(count($suffix)) $suffixStr = '-' . implode('-', $suffix); + } + + if($options['hidpi']) { + $suffixStr .= '-hidpi'; + if($options['hidpiQuality']) $options['quality'] = $options['hidpiQuality']; + } + + //$basename = $this->pagefiles->cleanBasename($this->basename(), false, false, false); + // cleanBasename($basename, $originalize = false, $allowDots = true, $translate = false) + $basename = $this->name; // i.e. myfile + if($options['cleanFilename'] && strpos($basename, '.') !== false) { + $basename = substr($basename, 0, strpos($basename, '.')); + } + $basename .= '.' . $width . 'x' . $height . $crop . $suffixStr . "." . $this->ext; // i.e. myfile.100x100.jpg or myfile.100x100nw-suffix1-suffix2.jpg + + + if ($this->conf->variationSubDir) { + $variationSubDir = "{$this->conf->variationSubDir}/"; + $subPath = "{$this->dir}/{$this->conf->variationSubDir}"; + } else { + $variationSubDir = ''; + $subPath = $this->dir; + } + + + if ($variationSubDir && !file_exists($subPath)) wireMkdir($subPath, true); + wireChmod($subPath, true); + + $filenameFinal = "$subPath/{$basename}"; + + + $tmpDir = "{$this->dir}/tmp_" . str_replace('.', '_', $basename); + if (!file_exists($tmpDir)) wireMkdir($tmpDir, true); + wireChmod($tmpDir, true); + + + + $filenameUnvalidated = "$tmpDir/$basename"; + + $exists = file_exists($filenameFinal); + + if(!$exists || $options['forceNew']) { + if($exists && $options['forceNew']) @unlink($filenameFinal); + if(file_exists($filenameUnvalidated)) @unlink($filenameUnvalidated); + + if(@copy($this->path, $filenameUnvalidated)) { + + try { + $sizer = new \ImageSizer($filenameUnvalidated); + $sizer->setOptions($options); + if($sizer->resize($width, $height) && @rename($filenameUnvalidated, $filenameFinal)) { + wireChmod($filenameFinal); + } else { + $this->error = "ImageSizer::resize($width, $height) failed for $filenameUnvalidated"; + } + } catch(\Exception $e) { + + $this->error = $e->getMessage(); + } + } else { + $this->error = "Unable to copy {$this->path} => $filenameUnvalidated"; + } + } + + $image = clone $this; + + // if desired, user can check for property of $image->error to see if an error occurred. + // if an error occurred, that error property will be populated with details + if($this->error) { + + // error condition: unlink copied file + if(is_file($filenameFinal)) @unlink($filenameFinal); + if(is_file($filenameUnvalidated)) @unlink($filenameUnvalidated); + + + // write an invalid image so it's clear something failed + // todo: maybe return a 1-pixel blank image instead? + $data = "This is intentionally invalid image data.\n$this->error"; + if(file_put_contents($filenameFinal, $data) !== false) wireChmod($filenameFinal); + + // we also tell PW about it for logging and/or admin purposes + $this->error($this->error); + } + + if(is_dir($tmpDir)) wireRmdir($tmpDir, true); + + + + $image->filename = pathinfo($filenameFinal, PATHINFO_BASENAME); + $image->variationSubDir = rtrim($variationSubDir, '/'); + + return $image; + } + + /** + * Get a variation by name, which can be configured in `$config->blick->imgVariations` + * + * Variations example + * + * ``` + * array( + * 'header' => array( + * 'width' => 960, + * 'height' => 360, + * 'options' => array( + * 'suffix' => 'header', + * ), + * ), + * 'person' => array( + * 'width' => 200, + * 'height' => 200, + * 'options' => array( + * 'suffix' => 'person', + * ), + * ), + * ) + * ``` + * + * @param string $name + * @return Image + */ + public function getVariation($name) + { + if (isset($this->conf->variations[$name])) { + $variation = $this->conf->variations[$name]; + $width = isset($variation['width']) ? $variation['width'] : null; + $height = isset($variation['height']) ? $variation['height'] : null; + $options = isset($variation['options']) ? $variation['options'] : array(); + + if (!is_null($width) && !is_null($height)) { + return $this->size($width, $height, $options); + } else if (!is_null($width)) { + return $this->width($width, $options); + } else if (!is_null($height)) { + return $this->height($height, $options); + } else { + return $this; + } + } + return $this; + } + + public function crop($x, $y, $width, $height, $options = array()) + { + + $x = (int) $x; + $y = (int) $y; + $width = (int) $width; + $height = (int) $height; + + if(empty($options['suffix'])) { + $options['suffix'] = array(); + } else if(!is_array($options['suffix'])) { + $options['suffix'] = array($options['suffix']); + } + + $options['suffix'][] = "cropx{$x}y{$y}"; + $options['cropExtra'] = array($x, $y, $width, $height); + $options['cleanFilename'] = true; + + return $this->size($width, $height, $options); + } + + public function width($n = 0, $options = array()) + { + return $this->size($n, 0, $options); + } + + /** + * Multipurpose: return the height of the Pageimage OR return an image sized with a given height (and proportional width) + * + * If given a height, it'll return a new Pageimage object sized to that height. + * If not given a height, it'll return the height of this Pageimage + * + * @param int $n Optional height + * @param array|string|int|bool $options Optional options (see size function) + * @return int|Pageimage + * + */ + public function height($n = 0, $options = array()) + { + return $this->size(0, $n, $options); + } +} diff --git a/owzim/blick/utils/str.php b/owzim/blick/utils/str.php new file mode 100644 index 0000000..32f3839 --- /dev/null +++ b/owzim/blick/utils/str.php @@ -0,0 +1,40 @@ + 'Jane', 'compliment' => 'stunning'] + * ); + * // returns 'Hello Jane, you look stunning!' + * ``` + * + * @param string $string + * @param array $values + * @return string The formatted string + */ + public static function format($string, array $values) + { + foreach ($values as $key => $value) { + $string = str_replace('{' . $key. '}', $value, $string); + } + return $string; + } +}